腾小云导读

老零碎的代码,是每一个程序员都不想去触碰的畛域,秉着能跑就行的准则,任由其自生自灭。本期就给大家讲讲,接手一套故障频发的简单老零碎须要从哪些地方着手。内容包含:代码串讲、监控建设和告警治理、代码缺点修复、研发流程建设。在细节上,联合腾讯研发生态,介绍有哪些工具能够应用,也介绍一些告警治理、代码 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 其余优化成果

01、我的项目背景

内容架构为 QB 搜寻提供内容接入、计算、散发的全套服务。通过多年的疾速迭代,内容架构包含 93 个服务,光接入主链路就波及 7 个服务,反对多种接口类型。其分支定制策略多且散,数据流向混淆,且有泛滥 bug。

项目组接手这套架构的晚期,每天收到大量的业务故障反馈以及服务本身告警。即使投入小组一半的人力做运维反对,仍旧忙得焦头烂额。无奈根治零碎稳定性缺点的同时,项目组还须要持续承接新业务,而新业务又持续裸露零碎缺点,陷入一直好转的负循环,漏洞百出。没有人有信念去承诺零碎稳定性,团队口碑、开发者的信念都处于解体的边缘。

在此严厉的局势下,项目组全员投入启动稳定性治理专项,让团队进入零碎稳固的正循环

02、服务监控

监控能够帮忙咱们把握服务运行时状态,发现、感知服务异常情况。通过看到问题 - 定位问题 - 修复问题来更快的相熟模块架构和代码实现细节。上面分两局部介绍,如何利用监控达成稳定性优化。

2.1 平台自带监控

若服务的部署、公布、运行托管于公共平台,则这些平台可能会提供容器资源应用状况的监控,包含:CPU 利用率监控、内存使用率监控、磁盘使用率监控等服务通常应该关注的外围指标。咱们的服务部署在123 平台(司内平台),有如下罕用的监控。

平台自带监控:

内部资源平台监控:

  • 数据库连接数监控: 查看服务应用 DB 是否全是长连贯,应用完没有及时 disconnect 。
  • 数据库慢查问监控: SQL 命令是否不合理,DB 表是否索引设置不合理。
  • 数据库 CPU 监控: 查看服务是否全副连的 DB 主机,对于只读的场景可抉择用只读账号优先读备机,升高 DB 压力。

其余诸如腾讯云 Redis 等内部资源也有相干的慢查问监控,亦是对应查看。

2.2 业务定制监控

平台自带的监控让咱们掌控服务根本运行状态。咱们还需在业务代码中减少业务自定义监控,以掌控业务层面的运行状况。

上面介绍常见的监控欠缺办法,以让各位对于业务运行状态尽在把握之中。

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 或新增谬误类型时,均会主动笼罩在此条告警配置内,保护成本低,配置简略。

03、串讲文档

监控能够帮忙咱们理解服务运行的体现,想要“深度清理”服务潜在问题,咱们还须要对我的项目做代码级接手。在稳定性治理专项中,咱们要求每个外围模块都产出一份串讲文档,而后穿插学习,使得开发者不单相熟本人负责的模块,也对残缺零碎链路各模块性能有大略的了解,防止窥豹一斑。

3.1 串讲文档是什么

代码串讲指的是接手同学在浏览并了解模块代码后,零碎的向别人介绍对该模块的掌握情况。代码串讲文档则是贯通串讲过程中的分享资料,须要对架构设计、代码实现、部署、监控、理想化思考等方面进行具体介绍,既帮忙其他同学更好的了解整个模块,也便于评估接手同学对我的项目的了解水平。

代码串讲文档通常包含以下内容:模块次要性能,上下游关系、整体架构、子模块的具体介绍、模块研发和上线流程、模块的要害指标等等。在写串讲文档的时候也通常须要思考很多问题:这个性能为什么须要有?设计思路是这样的?技术上如何实现?最初是怎么利用的?

3.2 为什么须要串讲文档

3.3 怎么输入串讲文档

增代码串讲文档的时候,须要从2个方面进行思考——读者角度和作者角度。

作者角度: 须要论述作者对系统和代码的了解和把握,同时也须要思考各项细节:这个性能为什么须要有、设计思路是怎么的、技术上如何实现、最初是怎么利用的等等。

读者角度: 须要思考指标受众是哪些,尽可能地把读者当成技术小白,思考读者须要理解什么信息,如何能力更好地了解代码的实现和作用。

通常,代码串讲文档能够蕴含以下几个局部:

04、代码品质

代码品质很大水平上决定服务的稳定性。在对代码中业务逻辑 bug 进行修复的同时,咱们也对服务的启动、数据库压力及互斥资源管理做优化。

4.1 业务逻辑bug

4.1.1 内存透露

如下图代码所示,应用 malloc 分配内存,但没有 free,导致内存泄露。该代码为 C 语言格调,古代 C++ 应用智能指针能够防止该问题。

4.1.2 空指针拜访成员变量

如下图所示的代码,如果一个非虚成员函数没有应用成员变量,因编译期的动态绑定,空指针也能够胜利调用该成员函数。但如果该成员函数应用了成员变量,那么空指针调用该函数时则会 core。该类问题在接入零碎仓库中比拟广泛,倡议所有指针都要进行正当的初始化。

4.2 进攻编程

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。

4.3 Go-python 内存泄露问题

如下图 85-86 行代码所示,应用 Go-python 调用 python 脚本,将 Go 的 string 转为PyString,此时 kv 为 PyObject。局部 PyObject 须要在函数完结时调用 DecRef,缩小援用计数,以确保资源开释,否则会造成内存泄露。

断定根据是间接看 python sdk 的源码正文,如果有 New Reference , 那么必须在应用结束时开释内存,Borrowed Reference 不须要手动开释。

4.4 正确应用内部库

4.4.1 Kafka Message 完结字符

当生产者为 Go 服务时,写入 kafka 的音讯字符串不会带有完结字符 '\0'。当生产者为 C++ 服务时,写入 kafka 的音讯字符串会带有完结字符 '\0'。

如下图 481 行代码所示,C++中应用 librdkafka 获取生产数据时,需传入音讯长度,而不是依赖程序自行寻找 '\0' 结束符。

4.5 防止有限重试导致雪崩

如下图所示代码所示,失败之后立马重试。当呈现问题时,一直立刻重试,会导致雪崩。给重试加上 sleep,减缓上游雪崩的速度,留下缓冲的工夫。

4.6 实在初始化

如果每次服务启动都存在肯定的成功率抖动,须要查看服务注册前的初始化流程,看看是不是存在异步初始化,导致未实现初始化即提供对外服务。如下图 48 行所示正文,原代码中初始化代码为异步函数,服务对外提供服务时可能还没有实现初始化。

4.7 资源隔离

时延/成功率要求不同的服务接口,倡议应用不同的解决线程。如下图中几个 service,之前均共用同一个解决线程池。其中 secure\_review\_service 解决耗时长,流量不稳固,其余接口流量大且时延敏感,线上呈现过 secure\_review\_service 刹时流量波峰,将解决线程全副占住且队列积压,导致其余 service 超时失败。

4.8 数据库压力优化

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 互斥资源管理

4.9.1 防止连贯占用

接入零碎服务的 mysql 连贯全副应用了连接池治理。每一个连贯在应用完之后,须要及时开释,否则该连贯就会被占住,最终连接池无资源可用。下图所示的 117 行连贯开释为有效代码,因为提前 return。乏味的是,这个 bug 和另外一个 bug 组合起来,解决了没有连贯可用的问题:当没有连贯可用时,获取的连贯则会为空。该服务不会对连贯判空,导致服务 core 重启,连接池从新初始化,又有可用的连贯了。针对互斥的资源,要进行及时开释。

4.9.2 应用 RAII 开释资源

下图所示的 225 行代码,该工作为互斥资源,只能由一个节点取得该工作并执行该工作。GetAllValueText 执行完该工作之后,应该开释该工作。然而在 240 行提前 return 时,没有及时开释该资源。

优化后,咱们应用 ScopedDeferred 确保函数执行实现,退出之前肯定会执行资源开释。

05、告警治理

告警轰炸是接手服务初期常见的问题。除了前述的代码品质优化,咱们还解决下述几类告警:

  • 全链路超时配置不合理: 上游服务的超时工夫,大于上游调用它的超时工夫,导致多个服务超时告警轰炸、两头链路服务有效期待等。
  • 业务未隔离: 某个业务流量突增引起全链路队列阻塞,影响到其余业务。
  • 申请阻塞: 申请线程中的解决耗时过长,导致申请队列拥挤,申请音讯得不到及时处理。

5.1 全链路超时工夫正当设置

未经治理的长链路服务,因为超时设置不合理导致的异常现象:

  • 超时告警轰炸: A 调用 B,B 调用 C,当 C 异样时,C 有超时告警,B 也有超时告警。在稳定性治理剖析过程中,C 是谬误本源,因此 B 的超时告警没有价值,当链路较长时,会因某一个底层服务的谬误,导致海量的告警轰炸。
  • 服务有效期待: A 调用 B,B 调用 C,当 A->B 超时的时候,B 还在等 C 的回包,此时 B 的期待是无价值的。

这两种景象是因为超时工夫配置不合理导致的,对此咱们制订了“超时不扩散准则”,某个服务的超时不应该通过框架扩散传递到它的间接调用者,此准则要求某个服务调用上游的超时必须小于上游调用它的超时工夫。

5.2 基于业务分 set 进行资源隔离

针对某个业务的流量突增影响其余业务的问题,咱们可将重点业务基于 set 做隔离部署。确保非凡业务只运行于独立节点组上,当他流量暴涨时,不烦扰其余业务安稳运行,升高损失范畴。

5.3 高耗时计算应用线程池

如下图红色局部 372 行所示,在申请响应线程中进行长耗时的解决,占住申请响应线程,导致申请队列阻塞,后续申请得不到及时处理。如下图绿色局部 368 行所示,咱们将耗时解决逻辑转到线程池中异步解决,从而不占住申请响应线程。

06、研发流程

在研发流程上,咱们沿用司内其余技术产品积攒的 CICD 建设教训,包含以下措施:

07、优化成果总结

7.1 健全的CICD

7.1.1 代码合入

在稳定性专项优化前,内容架构的服务没有正当的代码合入机制,导致骨干代码呈现违反编码标准、安全漏洞、圈复杂度高、bug等代码问题。

优化后,咱们应用对立的蓝盾流水线模板给所有服务加上流水线,以保障上述问题能被自动化工具、人工代码评审拦挡。

7.1.2 服务公布

在稳定性优化前,内容架构服务公布均为人工操作,没有 checklist 机制、审批机制、主动回滚机制,有很大安全隐患。

优化后,咱们应用对立的蓝盾流水线模板给所有服务加上 XAC 流水线,实现了提醒公布人在公布前自查看、double\_check 后审批通过、线上出问题时一键回滚。

7.2 更齐备的可观测性

7.2.1 多维度监控

在稳定性优化前,内容架构服务的监控笼罩不全,自定义监控均应用一维的属性监控,这导致多维拼接的监控项泛滥、无奈自由组合多个维度,给监控查看带来不便。

优化后,咱们用更少的监控项笼罩更多的监控场景。

7.2.2 业务监控和负责人制度

在稳定性优化前,内容架构服务简直没有业务维度监控。优化后,咱们加上了重要模块的多个业务维度监控,譬如:数据断流、音讯组件音讯挤压等,同时还建设值班 owner、服务 owner 制度,确保有告警、有跟进。

7.2.3 trace 欠缺与断流排查文档建设

在稳定性优化前,尽管已有上报鹰眼 trace 日志,但上报不残缺、上报有误、不足排查手册等问题,导致对数据处理全流程的跟踪调查十分艰难。

优化后,修改并补全了 trace 日志,建设配套排查文档,case 解决从不可考察变成可高效率考察。

7.3代码bug修复

7.3.1 内存泄露修复

在稳定性优化前,咱们察看到有3个服务存在内存泄露,例如代码品质章节中形容内存泄露问题。

7.3.2 coredump 修复 & 性能 bug 修复

在稳定性优化前,历史代码中存在诸多 bug 与可能导致 coredump 的隐患代码。

咱们在稳定性优化时解决了如下 coredump 与 bug:

  • JSON 解析前未严格查看,导致 coredump 。
  • 服务还未初始化实现即接流,导致服务重启时被调成功率猛跌。
  • 服务初始化时没有同步加载配置,导致服务启动后缺失配置而调用失败。
  • Kafka 生产完立即 Commit,导致服务重启时,音讯未理论解决完,音讯可能失落/
  • 日志参数类型谬误,导致启动日志疯狂报错写满磁盘。

7.4 服务被调成功率优化

在稳定性优化前,局部内容架构服务的被调成功率不迭 99.5% ,且个别服务存在重大的毛刺问题。优化后,咱们确保了服务运行稳固,调用成功率放弃在 99.9%以上。

7.5 内部存储应用优化

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点开奖。