👉腾小云导读
老系统的代码,是每一个程序员都不想去触碰的领域,秉着能跑就行的原则,任由其自生自灭。本期就给大家讲讲,接手一套故障频发的复杂老系统需要从哪些地方着手。内容包括:代码串讲、监控建设和告警治理、代码缺陷修复、研发流程建设。在细节上,结合腾讯研发生态,介绍有哪些工具可以使用,也介绍一些告警治理、代码 bug 修复的经验、研发流程建设等。欢迎阅读。👉看目录,点收藏
1 项目背景2 服务监控 2.1 平台自带监控 2.2 业务定制监控3 串讲文档 3.1 串讲文档是什么 3.2 为什么需要串讲文档 3.3 怎么输出串讲文档4 代码质量 4.1 业务逻辑 bug 4.2 防御编程 4.3 Go-python 内存泄露问题 4.4 正确使用外部库 4.5 避免无限重试导致雪崩 4.6 真实初始化 4.7 资源隔离 4.8 数据库压力优化 4.9 互斥资源管理5 警告治理 5.1 全链路超时时间合理设置 5.2 基于业务分 set 进行资源隔离 5.3 高耗时计算使用线程池6 研发流程
7 优化效果总结 7.1 健全的 CICD 7.2 更完备的可观测性 7.3 代码 bug 修复 7.4 服务被调成功率优化 7.5 外部存储使用优化 7.6 CPU 用量下降 7.7 代码质量提升 7.8 其他优化效果
内容架构为 QB 搜索提供内容接入、计算、分发的全套服务。经过多年的快速迭代,内容架构包括 93 个服务,光接入主链路就涉及 7 个服务,支持多种接口类型。其分支定制策略多且散,数据流向混杂,且有众多 bug。项目组接手这套架构的早期,每天收到大量的业务故障反馈以及服务自身告警。即便投入小组一半的人力做运维支持,依旧忙得焦头烂额。无法根治系统稳定性缺陷的同时,项目组还需要继续承接新业务,而新业务又继续暴露系统缺陷,陷入不断恶化的负循环,漏洞百出。没有人有信心去承诺系统稳定性,团队口碑、开发者的信心都处于崩溃的边缘。在此严峻的形势下,项目组全员投入启动稳定性治理专项,让团队进入系统稳定的正循环。监控可以帮助我们掌握服务运行时状态,发现、感知服务异常情况。通过看到问题 - 定位问题 - 修复问题来更快的熟悉模块架构和代码实现细节。下面分两部分介绍,如何利用监控达成稳定性优化。若服务的部署、发布、运行托管于公共平台,则这些平台可能会提供容器资源使用情况的监控,包括:CPU 利用率监控、内存使用率监控、磁盘使用率监控等服务通常应该关注的核心指标。我们的服务部署在123 平台(司内平台),有如下常用的监控。监控类型 | 解析 |
服务运行监控 | 123 平台的 tRPC 自定义监控:event_alarm 可监控服务异常退出。 |
节点服务资源使用监控 | 内存使用率监控:检查是否存在内存泄露,会不会不定期 OOM。 |
磁盘使用率监控:检查日志是否打印的过多,日志滚动配置是否合理。 |
CPU 使用率监控: 如存在定期/不定期的 CPU 毛刺,则检查对应时段请求/定期任务是否实现不合理。如CPU 配额高而使用率低,则检查是配置不合理导致资源浪费,还是服务实现不佳导致 CPU 打不上去。 |
外部资源平台监控:
数据库连接数监控:检查服务使用 DB 是否全是长连接,使用完没有及时 disconnect 。 数据库慢查询监控:SQL 命令是否不合理,DB 表是否索引设置不合理。数据库 CPU 监控:检查服务是否全部连的 DB 主机,对于只读的场景可选择用只读账号优先读备机,降低 DB 压力。其他诸如腾讯云 Redis 等外部资源也有相关的慢查询监控,亦是对应检查。 |
平台自带的监控让我们掌控服务基本运行状态。我们还需在业务代码中增加业务自定义监控,以掌控业务层面的运转情况。下面介绍常见的监控完善方法,以让各位对于业务运行状态尽在掌握之中。 2.2.1 在主/被调监控中增加业务错误码
一般来说,后台服务如果无法正常完成业务逻辑,会将错误码和错误详情写入到业务层的回包结构体中,然后在框架层返回成功。这种实现导致我们无法从平台自带的主/被调监控中直观看出有多少请求是没有正常结果的。一个服务可能看似运行平稳,基于框架层判断的被调成功率 100%,但其中却有大量没有正常返回结果的请求,在我们的监控之外。此时如果我们想要对这些业务错误做监控,需要上报这些维度:请求来源(主调服务、主调容器、主调 IP、主调 SET)、被调容器、错误码、错误数,这和被调监控有极大重合,存在资源浪费,并且主、被调服务都有开发成本。若服务框架支持,我们应该尽可能使用框架层状态包来返回业务自定义的错误码。以tRPC框架为例服务的<被调监控 - 返回码>中,会上报框架级错误码和业务错误码:框架错误码是纯数字,业务错误码是<业务协议_业务码>。如此一来,就可以在主/被调服务的平台自带监控中看到业务维度的返回码监控。 2.2.2 在主/被调监控中注入业务标识
有时候一个接口会承担多个功能,用请求参数区分执行逻辑A / 逻辑B。在监控中,我们看到这个接口失败率高,需要进一步下钻是 A 失败高还是 B 失败高、A/B 分别占了多少请求量等等。对这种诉求,我们可以在业务层上报一个带有各种主调属性的多维监控以及接口参数维度用于下钻分析。但这种方式会造成自定义监控和被调监控字段的重叠浪费。更好的做法是:将业务维度注入到框架的被调监控中,复用被调监控中的主调服务、节点、SET、被调接口、IP、容器等信息。 2.2.3 单维属性上报升级成多维属性上报
单维属性监控指的是上报单个字符串(维度)、一个浮点数值以及这个浮点数值对应的统计策略。多维监控指的是上报多个字符串(维度)、多个浮点数值以及每个浮点数值对应的统计策略。下面以错误信息上报场景为例,说明单维监控的缺点以及如何切换为多维上报。作为后台服务,处理逻辑环节中我们需要监控上报各类关键错误,以便及时感知错误、介入处理。内容架构负责多个不同业务的接入处理,每个业务具有全局唯一的资源标识,下面称为 ResID。当错误出现时,做异常上报时,我们需要上报 “哪个业务”,出现了“什么错误”。使用单维上报时,我们只能将二者拼接在一起,上报 string (ResID + "." + ErrorMsg)。对不同的 ResID 和不同的 ErrorMsg,监控图是铺平展开的,只能逐个查看,无法对 ResID 下钻查看每类错误分别出现了多少次或者对 ErrorMsg 下钻,查看此错误分别在哪些 ID 上出现。除了查看上的不便,多维属性监控配置告警也比单维监控便利。对于单维监控,我们需要对每一个可能出现的组合配置条件:维度值 = XXX,错误数 > N 则告警。一旦出现了新的组合,比如新接入了一个业务,我们就需要再去加告警配置,十分麻烦且难以维护。而对于多维监控表,我们只需设置一条告警配置:对 ResID && 错误维度 下钻,错误数 > N 则告警。新增 ResID 或新增错误类型时,均会自动覆盖在此条告警配置内,维护成本低,配置简单。监控可以帮助我们了解服务运行的表现,想要“深度清理”服务潜在问题,我们还需要对项目做代码级接手。在稳定性治理专项中,我们要求每个核心模块都产出一份串讲文档,而后交叉学习,使得开发者不单熟悉自己负责的模块,也对完整系统链路各模块功能有大概的理解,避免窥豹一斑。代码串讲指的是接手同学在阅读并理解模块代码后,系统的向他人介绍对该模块的掌握情况。代码串讲文档则是贯穿串讲过程中的分享材料,需要对架构设计、代码实现、部署、监控、理想化思考等方面进行详细介绍,既帮助其他同学更好的理解整个模块,也便于评估接手同学对项目的理解程度。代码串讲文档通常包括以下内容:模块主要功能,上下游关系、整体架构、子模块的详细介绍、模块研发和上线流程、模块的关键指标等等。在写串讲文档的时候也通常需要思考很多问题:这个功能为什么需要有?设计思路是这样的?技术上如何实现?最后是怎么应用的? 3.2 为什么需要串讲文档
原因
| 解析 |
确保代码走读的质量 | 串讲文档涵盖了模块最重要的几个部分,要求开发人员编写串讲文档的这些章节,可保障他在走读此模块时高度关注到这些部分,并总结输出成文档,确保了代码学习的深度和质量。 |
强化理解 | 编写串讲文档的过程,也是对模块各方面的提炼总结。在编写的过程中,开发人员可能会发现走读中未想到的问题。通过编写代码串讲文档,开发人员可以更好地理解整个系统的结构和实现,加强对代码和系统的理解,为后续接手、维护该系统提供了帮助。 |
团队知识沉淀和积累 | 串讲文档是团队内部进行知识分享和沟通的载体,也是团队知识不断沉淀积累的过程,可以作为后续新人加入团队时了解系统的第一手材料,也可以作为其他同学后续多次翻阅、了解该系统的材料。 |
增代码串讲文档的时候,需要从2个方面进行考虑——读者角度和作者角度。作者角度:需要阐述作者对系统和代码的理解和把握,同时也需要思考各项细节:这个功能为什么需要有、设计思路是怎样的、技术上如何实现、最后是怎么应用的等等。
读者角度:需要考虑目标受众是哪些,尽可能地把读者当成技术小白,思考读者需要了解什么信息,如何才能更好地理解代码的实现和作用。文档构成
| 信息
|
模块主要功能 | 首先需要明确模块的主要功能,并在后续的串讲中更加准确地介绍代码的实现。 |
上下游关系 | 在介绍每个模块时,需要明确它与其他模块之间的上下游关系,即模块之间的调用关系,这有助于了解模块之间的依赖关系,从而更好地理解整个系统的结构和实现。 |
名词解释 | 对一些关键词、专业术语和缩写词等专业术语进行解释,不同团队使用的术语和缩写可能不同,名词解释可以减少听众的阅读难度,提高沟通效率。 |
整体架构 | 介绍整个系统的设计思路、实现方案和架构选择的原因以及优缺点。 |
子模块的详细解读 | 在介绍每个子模块时,需要对其进行详细的解读,包括该模块的具体功能、实现方式、代码实现细节等方面。 |
模块研发和上线流程 | 介绍模块的研发和上线流程,包括需求分析、设计、开发、测试、上线等环节。这有助于了解模块的开发过程和上线流程,以及研发团队的工作流程。 |
模块的关键指标 | 介绍模块的关键指标,包括不限于业务指标、服务监控、成本指标等。 |
模块当前存在问题及可能的解决思路 | 主要用于分析该模块目前存在的问题,并提出可能的解决思路和优化方案。 |
理想化的思考 | 对该模块或整个系统的未来发展和优化方向的思考和展望,可以对模块的长期目标、技术选型、创新探索、稳定性建设等方面进行思考。 |
中长线工作安排 | 结合系统现状和理想化思考,做出可行的中长线工作计划。 |
串讲中的问题 | 这部分用于记录串讲中的问题及解答,通常是Q&A的形式,串讲中的问题也可能是系统设计相关的问题,后续将作为todo项加入到工作安排中。 |
代码质量很大程度上决定服务的稳定性。在对代码中业务逻辑 bug 进行修复的同时,我们也对服务的启动、数据库压力及互斥资源管理做优化。 4.1.1 内存泄漏
如下图代码所示,使用 malloc 分配内存,但没有 free,导致内存泄露。该代码为 C 语言风格,现代 C++ 使用智能指针可以避免该问题。 4.1.2 空指针访问成员变量
如下图所示的代码,如果一个非虚成员函数没有使用成员变量,因编译期的静态绑定,空指针也可以成功调用该成员函数。但如果该成员函数使用了成员变量,那么空指针调用该函数时则会 core。该类问题在接入系统仓库中比较普遍,建议所有指针都要进行合理的初始化。 4.2.1 输入防御
如下图所示,如果发生了错误且没有提前返回,request 将引发 panic。针对输入,在没有约定的情况下,建议加上常见的空指针判断及异常判断。
4.2.2 数组长度防御-1
如下图所示,当 url 长度超过 512 时,将会被截断,导致产出错误的url。建议针对字符串数组的长度进行合理的初始化,亦或者使用string来替代字符数组。 4.2.3 数组长度防御-2
如下图所示,老代码不判断数组长度,直接取值。当出现异常数据时,该段代码则会core。建议在每次取值时,基于上下文做防御判断。 4.2.4 野指针问题
下图中的ts指针指向内容和 create_time 一致。当 create_time 被 free 之后,ts 指针就变成了野指针。该代码为 C 语言风格代码,很容易出现内存方面的问题。建议修改为现代 C++风格。下图中,临时变量存储的是 queue 中的值的引用。当 queue pop 后,此值会被析构;而变量引用的存储空间也随之释放,访问此临时变量可能出现未定义的行为。 4.2.5 全局资源写防护
同时读写全局共有资源,尤其生成唯一 id,要考虑并发的安全性。这里直接通过查询 DB 获取最大的 res_id,转成 int 后加一,作为新增资源的唯一 id。如果并发度超过 1,很可能会出现 id 重复,影响后续操作逻辑。 4.2.6 lua 添加 json 解析防御
如下图所示的 lua 脚本中,使用 cjson 将字符串转换 json_object。当 data_obj 不是合法的 json 格式字符串时,decode 接口会返回 nil。修复前的脚本未防御返回值为空的情况,一旦传入非法字符串,lua 脚本就会引发 coredump。如下图 85-86 行代码所示,使用 Go-python 调用 python 脚本,将 Go 的 string 转为PyString,此时 kv 为 PyObject。部分 PyObject 需要在函数结束时调用 DecRef,减少引用计数,以确保资源释放,否则会造成内存泄露。判定依据是直接看 python sdk 的源码注释,如果有 New Reference , 那么必须在使用完毕时释放内存,Borrowed Reference 不需要手动释放。 4.4.1 Kafka Message 结束字符
当生产者为 Go 服务时,写入 kafka 的消息字符串不会带有结束字符 '\0'。当生产者为 C++ 服务时,写入 kafka 的消息字符串会带有结束字符 '\0'。如下图 481 行代码所示,C++中使用 librdkafka 获取消费数据时,需传入消息长度,而不是依赖程序自行寻找 '\0' 结束符。如下图所示代码所示,失败之后立马重试。当出现问题时,不断立即重试,会导致雪崩。给重试加上 sleep,减缓下游雪崩的速度,留下缓冲的时间。如果每次服务启动都存在一定的成功率抖动,需要检查服务注册前的初始化流程,看看是不是存在异步初始化,导致未完成初始化即提供对外服务。如下图 48 行所示注释,原代码中初始化代码为异步函数,服务对外提供服务时可能还没有完成初始化。
时延/成功率要求不同的服务接口,建议使用不同的处理线程。如下图中几个 service,之前均共用同一个处理线程池。其中 secure_review_service 处理耗时长,流量不稳定,其他接口流量大且时延敏感,线上出现过 secure_review_service 瞬时流量波峰,将处理线程全部占住且队列积压,导致其他 service 超时失败。 4.8.1 分批拉取
当某个表数据很多或单次传输量很大,拉取节点也很多的时候,数据库的压力就会很大。这个时候,可以使用分批读取。下图 308-343 行代码,修改了 sql 语句,从一批拉取改为分批拉取。 4.8.2 读备机
如果业务场景为只读不写数据,且对一致性要求不高,可以将读配置修改为从备机读取。mysql 集群一般只会有单个主机、多个备机,备机压力较小。如下图 44 行代码所示,使用readuser,主机压力得到改善。 4.8.3 控制长链接个数
需要使用 mysql 长链接的业务,需要合理配置长链接个数,尤其是服务节点数很多的情况下。连接数过多会导致 mysql 实例内存使用量大,甚至 OOM;此外 mysql 的连接数是刚性限制,超过阈值后,客户端无法正常建立 mysql 连接,程序逻辑可能无法正常运转。 4.8.4 建好索引
使用 mysql 做大表检索时,应该建立与查询条件对应的索引。本次优化中,我们根据 DB 慢查询统计,找到有大表未建查询适用的索引,导致 db 负载高,查询速度慢。 4.8.5 实例拆分
非分布式数据库 (如 mariaDB) 存储空间是有物理上限的,需要预估好数据量,数据量过多及时进行合理的拆库。 4.8.6 分布式数据库负载均衡
分布式数据库一般应用在海量数据场景,使用中需要留意节点间负载均衡,否则可能出现单机瓶颈,拖垮整个集群性能。如下图是内容架构使用到的 hbase,图中倒数两列分别为请求数和region 数,从截图可看出集群的 region 分布较均衡,但部分节点请求量是其他节点几倍。造成图中请求不均衡的原因是集群中有一张表,有废弃数据占用大量 region,导致使用中的 region 在节点间分布不均,由此导致请求不均。解决方法是清理废弃数据,合并空数据 region。 4.9.1 避免连接占用
接入系统服务的 mysql 连接全部使用了连接池管理。每一个连接在使用完之后,需要及时释放,否则该连接就会被占住,最终连接池无资源可用。下图所示的 117 行连接释放为无效代码,因为提前 return。有趣的是,这个 bug 和另外一个 bug 组合起来,解决了没有连接可用的问题:当没有连接可用时,获取的连接则会为空。该服务不会对连接判空,导致服务 core 重启,连接池重新初始化,又有可用的连接了。针对互斥的资源,要进行及时释放。 4.9.2 使用 RAII 释放资源
下图所示的 225 行代码,该任务为互斥资源,只能由一个节点获得该任务并执行该任务。GetAllValueText 执行完该任务之后,应该释放该任务。然而在 240 行提前 return 时,没有及时释放该资源。优化后,我们使用 ScopedDeferred 确保函数执行完成,退出之前一定会执行资源释放。告警轰炸是接手服务初期常见的问题。除了前述的代码质量优化,我们还解决下述几类告警:全链路超时配置不合理。下游服务的超时时间,大于上游调用它的超时时间,导致多个服务超时告警轰炸、中间链路服务无效等待等。 业务未隔离。某个业务流量突增引起全链路队列阻塞,影响到其他业务。请求阻塞。请求线程中的处理耗时过长,导致请求队列拥堵,请求消息得不到及时处理。 |
未经治理的长链路服务,因为超时设置不合理导致的异常现象:- 超时告警轰炸:A 调用 B,B 调用 C,当 C 异常时,C 有超时告警,B 也有超时告警。在稳定性治理分析过程中,C 是错误根源,因而 B 的超时告警没有价值,当链路较长时,会因某一个底层服务的错误,导致海量的告警轰炸。
- 服务无效等待:A 调用 B,B 调用 C,当 A->B 超时的时候,B 还在等 C 的回包,此时 B 的等待是无价值的。
这两种现象是因为超时时间配置不合理导致的,对此我们制定了“超时不扩散原则”,某个服务的超时不应该通过框架扩散传递到它的间接调用者,此原则要求某个服务调用下游的超时必须小于上游调用它的超时时间。
针对某个业务的流量突增影响其他业务的问题,我们可将重点业务基于 set 做隔离部署。确保特殊业务只运行于独立节点组上,当他流量暴涨时,不干扰其他业务平稳运行,降低损失范围。
如下图红色部分 372 行所示,在请求响应线程中进行长耗时的处理,占住请求响应线程,导致请求队列阻塞,后续请求得不到及时处理。如下图绿色部分 368 行所示,我们将耗时处理逻辑转到线程池中异步处理,从而不占住请求响应线程。
在研发流程上,我们沿用司内其他技术产品积累的 CICD 建设经验,包括以下措施:
研发方式 | 具体流程 |
建设统一镜像 | 统一开发镜像,任意服务都可以在统一镜像下开发编译 |
配置工蜂仓库 | 保护 master 分支,代码经过评审才可合入 规范分支和 tag 命名,便于后续信息追溯 规范化 commit 信息,确保信息可读,有条理,同样便于后续信息追溯 |
|
|
建设蓝盾流水线 | MR 流水线:提交 MR 时执行的流水线,涵盖代码静态检查、单元测试、接口测试,确保合入 master 的代码质量达到基本水平 提交构建流水线:MR 合入后执行的流水线,同样涵盖代码检查,单元测试,接口测试,确保 master 代码随时可发布 XAC 发布流水线:发布上线的流水线,执行固化的灰度->全量流程,避免人工误操作 |
|
|
落实代码评审机制 | 确保代码合入时,经过了至少一位同事的检查和评审,保障代码质量、安全 |
优化效果总结
7.1 健全的CICD
7.1.1 代码合入
在稳定性专项优化前,内容架构的服务没有合理的代码合入机制,导致主干代码出现违背编码规范、安全漏洞、圈复杂度高、bug等代码问题。
优化后,我们使用统一的蓝盾流水线模板给所有服务加上流水线,以保证上述问题能被自动化工具、人工代码评审拦截。
7.1.2 服务发布
在稳定性优化前,内容架构服务发布均为人工操作,没有 checklist 机制、审批机制、自动回滚机制,有很大安全隐患。优化后,我们使用统一的蓝盾流水线模板给所有服务加上 XAC 流水线,实现了提示发布人在发布前自检查、double_check 后审批通过、线上出问题时一键回滚。
在稳定性优化前,内容架构服务的监控覆盖不全,自定义监控均使用一维的属性监控,这导致多维拼接的监控项泛滥、无法自由组合多个维度,给监控查看带来不便。
优化后,我们用更少的监控项覆盖更多的监控场景。
7.2.2 业务监控和负责人制度
在稳定性优化前,内容架构服务几乎没有业务维度监控。优化后,我们加上了重要模块的多个业务维度监控,譬如:数据断流、消息组件消息挤压等,同时还建立值班 owner、服务 owner 制度,确保有告警、有跟进。
7.2.3 trace 完善与断流排查文档建设
在稳定性优化前,虽然已有上报鹰眼 trace 日志,但上报不完整、上报有误、缺乏排查手册等问题,导致对数据处理全流程的跟踪调查非常困难。
优化后,修正并补全了 trace 日志,建设配套排查文档,case 处理从不可调查变成可高效率调查。
7.3.1 内存泄露修复
在稳定性优化前,我们观察到有3个服务存在内存泄露,例如代码质量章节中描述内存泄露问题。
7.3.2 coredump 修复 & 功能 bug 修复
在稳定性优化前,历史代码中存在诸多 bug 与可能导致 coredump 的隐患代码。
我们在稳定性优化时解决了如下 coredump 与 bug:
- JSON 解析前未严格检查,导致 coredump 。
- 服务还未初始化完成即接流,导致服务重启时被调成功率猛跌。
- 服务初始化时没有同步加载配置,导致服务启动后缺失配置而调用失败。
- Kafka 消费完立刻 Commit,导致服务重启时,消息未实际处理完,消息可能丢失/
|
在稳定性优化前,部分内容架构服务的被调成功率不及 99.5% ,且个别服务存在严重的毛刺问题。优化后,我们确保了服务运行稳定,调用成功率保持在 99.9%以上。
7.5.1 MDB 性能优化
在稳定性优化前,内容架构各服务对MDB的使用存在以下问题:低效/全表SQL查询、所有服务都读主库、数据库连接未释放等问题。造成MDB主库的CPU负载过高、慢查询过多等问题。优化后,主库CPU使用率、慢查询数都有大幅下降。
7.5.2 HBase 性能优化
在稳定性优化前,内容架构服务使用的 HBase 存在单节点拖垮集群性能的问题。
优化后,对废弃数据进行了清理,合并了空数据 region,使得 HBase 调用的 P99 耗时有所下降。
7.6 CPU 用量下降
在稳定性优化前,内容架构服务使用的线程模型是老旧的 spp 协程。spp 协程在在高吞吐低延迟场景下性能有所不足,且未来 trpc 会废弃 spp。
我们将重要服务的线程模型升级成了 Fiber 协程,带来更高性能和稳定性的同时,服务CPU利用率下降了 15%。
7.7 代码质量提升
在稳定性优化前,内容架构服务存在很多不规范代码。例如不规范命名、魔术数字和常量、超长函数等。每个服务都存在几十个代码扫描问题,最大圈复杂度逼近 100。
优化后,我们对不规范代码进行了清扫,修复规范问题,优化代码命名,拆分超长函数。并在统一清理后,通过 MR 流水线拦截,保护后续合入不会引入新的规范类问题。
7.8 其他优化效果
经过本轮治理,我们的服务告警量大幅度下降。以系统中的核心处理服务为例,告警数量从159条/天降低到了0条/天。业务 case 数从 22 年 12 月接手以来的 18 个/月下降到 4 个/月。值班投入从最初的 4+ 人力,降低到 0.8 人力。
我们项目组在完成稳定性接手之后,下一步将对全系统做理想化重构,进一步提升迭代效率、运维效率。希望这些经验也对你接管/优化旧系统有帮助。如果觉得内容有用,欢迎分享。
各位开发者接手过什么样的老项目或者老代码,遇到了什么难题和心得?可以在评论区讨论分享。我们将选取1则最有创意的分享,送出腾讯云开发者-文化衫1个(见下图)。5月22日中午12点开奖。
关注并星标腾讯云开发者
第一时间看鹅厂技术干货