二零二三年一月第一周技术周报

时间进入二零二三年,今年是将一个辛苦的一年。今年将面临几方面的挑战,一个是将原先 部署在物理服务器上的数据都迁移上云。然后就是,加快培养几个团队新人,让他们尽快承 接目前的主要业务所涉及的服务,并且要求能够独立解决用户问题并对服务做出优化。这样 我就能将一些工作转交到他们名下,专注今年预估耗时较长的重要目标。还有就是,个人的 在技术和其他方面学习也到了一个攻坚阶段,这些方面决定了我未来 7~8 年的人生方向。 这一周,我的主要的精力在几个服务的日志框架与日志追踪的设计与规范上。首先是解决日 志追踪的问题,为了能够跨服务地对调用过程中产生的日志进行统一的追踪,需要现将 TraceId 统一。但是目前这几个服务 TraceId 的类型层次不齐,有使用 Long 类型的也有 用字符串的。并且,这些服务所使用的语言和技术栈也不一致。直接使用一些标准的分布式 追踪框架中的 TraceId 应该无法支持所有服务目前的情况,只能用在一些技术栈比较新的 服务上。 所以,从兼容性与改造的简洁性考虑,准备采用自定义生成的 Long 类型的数值 作为 TraceId 并且限定位数为 16 位。前四位以 99 开头,作为统一 TraceId 的特征,然 后剩余两位标识服务。接下来 4 位为当前的微秒数,最后的 8 位为两组四位随机数拼接。 虽然说这种 TraceId 并不能保证唯一性,但是在当前这种情况下是足够用的。如果服务为 Java 技术栈,生成 TraceId 需要充分考虑到线程竞争的情况,最好为每个线程分配一个随 机数发生器。或者,直接使用 TreadLocal。 Java 服务在处理请求时,会提取请求中的 TraceId。如果 TraceId 是 99 开头的,就不生成新的 TraceId 了。如果不是的话,按照 上述方式产生 TracId 并存储于 MDC 中。当遇到异步执行的时候,需要注意拷贝 MDC 中的 内容到旁线程,不然会丢失追踪信息。当需要调用下游服务时,需要将存储的 TaceId 传递 给下游服务。最后,当请求处理完毕时,需要清空 MDC,防止污染下一个请求的追踪信息。

2023年1月20日

二零二二年十二月第四周技术回顾

本周我感染了新冠病毒,一共在家待了 9 天。在此期间,工作上的最主要的事情是评估一 款小程序的推广上线,对我所负责的基础服务体系的影响。这款小程序切中了当时国人的需 求,预计有大量流量涌入,可能对基础服务体系的核心服务造成冲击。原先,他们有也一个 功能将要上线,流量也很大,我已经评估过并且扩容了。但是,这次还是在他们推送了上亿 的量级的通知后,发生了大批量的超时现象。 早上八点钟,躺在床上修养的我就被叫起 来,说是登录接口出现大量超时现象。 我马上拿出笔记本,连上内网一看,这个接口流量直接翻了 20 多倍。我都捏了一把汗,首 先是怀疑业务容器数量不足,无法承接这么大的流量。然后通过筛选日志,发现并不是这个 问题。问题分为两个部分,其一是内部某个接口有限流,当前流量已经超出限流;另一个是 缓存数据库挂接的数据库打满。 内部接口限流比较好解决,通过紧急呼叫找到了对应的同 事调高了限流的阈值。另一个缓存数据库,由于大量老用户涌入导致不断要从数据库中读取 老数据到缓存中,而且频率随着流量的增长不断提升,最终打满了缓存后面挂接的数据库的 I/O。后续针对几个核心的缓存数据库,再次做了一批扩容,提升了缓存容量和节点数量以 应对后续更大的流量和并发。 在日常维护上面,主要是将某个网关服务 Pod 的 k8s 调度策略做了调整,将其调度到由 CVM 虚拟化直接支持(一层调度)的计算节点上。原先,这个服务调度的到的计算节点是我 们公司采购的物理服务器上虚拟出来的 CVM,用作计算节点。调度到这上面以后,再于这个 CVM 上虚拟出一个执行空间(参考 Docker 技术),然后将 Pod 调度到这个节点开始运行 (二层调度)。一个计算节点上,可能存在几个甚至十几个 Pod。这些后台程序都运行于同 一台操作系统中,并且只是执行空间不同罢了。这就导致了 CPU、内存、网卡等资源的隔离 性不强,常常会互相干扰。并且当这台 CVM 计算节点需要重启或者升级的时候,其上的所 有 Pod 都会被驱逐。 计算节点操作系统中的风吹草动,会影响到其上所有 Pod。这对于这种网关这种可靠性和延 迟要求苛刻的服务,来说并不合适。 而一层调度计算节点,其实是由云来分配计算资源并 虚拟出 CVM 来单独执行这个程序,CVM 资源是从整个云资源池中取得。并且这种方式支持 k8s,因为这种新的调度方式会虚拟成 k8s 上的一种“超级节点”。k8s 可以直接将 Pod 调 度到超级节点上,Pod 可以直接基于一层调度运行,也就是直接运行在一个单独 CVM 上。 这种方式隔离性强,互相之间不会受到影响。由于每个 CVM 只运行一个节点,所以当单独 一个 Pod 出现问题时,可以针对这个 Pod 的问题进行修复,而不会影响到其他 Pod。 通 过修改调度策略,将服务的所有 Pod 调度到超级节点后,整个服务运行更加稳定了。在高 峰期超时率和 CPU 占用率双高的情况也很少再出现。 ...

2023年1月19日

二零二二年十二月第三周技术回顾

这一周,主要的工作是针对某个 Java 服务进行优化。该服务一直存在 CPU 占用率无法提 升上来的问题。首先考虑的问题是,该服务是否存在工作线程不足的问题。后面发现,并非 CPU 占用无法提升,而是 CPU 占用提升后会导致较多的超时问题。该服务很久以前就有反 馈性能不足,不建议继续使用。 所以我感觉,这个问题来自于框架,而非业务代码。 经过对框架代码进行阅读和梳理,该 框架使用 netty 作为 NIO 服务器框架,并且会在执行业务逻辑的时候分发业务处理任务到 工作线程。然后工作线程来处理业务逻辑。之前框架有个问题就是,工作线程数太少,难以 应对高 IO 的情况。而现在的情况下,推断并非上次一样的问题,从日志来看工作线程数是 足够的。会不会是客户端的问题。在整个微服务架构下,该服务会作为客户端去调用其他服 务的接口。 这可能是一个切入点。通过阅读代码,发现该服务框架通过实现 Java 的 InvocationHandler 接口来生成代理类 ObjectProxy,该 ObjectProxy 接管对其他服务的 RPC 调用。业务代码中,通过 RPC 的方式发起对其他服务接口的调用时,ObjectProxy 会 通过它关联的 ProtocolInvoker 来获取目标服务对应有效节点列表(这个有效节点列表每 隔 30s 刷新一次)。 而后,通过将列表传入负载均衡器 LoadBalancer 获得本次调用的目标节点。然后,就通过 具体协议对应的 Invoker 类向目标节点发起调用。Invoker 负责管理与目标服务之间的长 连接,在调用的时候会选取一个连接来发送请求并接收响应。具体的请求方式与此次调用类 型是异步还是同步有关。 客户端需要调用的每个目标服务由多个节点组成。对于每个节 点,该框架都会默认创建两个 I/O 线程负责网络 I/O 传输(NIO 模式)。另外,该框架会 为每个节点分别默认创建和处理器个数相当的 TCP 连接。每个 I/O 线程包含有一个 Selector 对连接的相关的事件进行轮询监听。而在每个 TCP 连接上会建立一个 TCPSession,每当需要发送一次请求时,会建立一个 Ticket 来跟踪这次请求及其关联响 应。对于同步请求来说,发送请求后会被阻塞,直到响应到来为止。 ...

2023年1月19日

二零二二年十二月第二周技术周报

这一周的工作主要是对我负责的这一方面的工作的一个梳理,目前发现了许多的问题。这些 问题主要集中在数据上云方面,目前的问题主要是如何安全上云、怎么改造目前的单地域部 署方案、如何修复云下数据和云上数据之间的不一致。另外,发现还是有一些服务在使用云 下的数据库,这些云下数据库按道理是要废弃的。但是,这些服务都是一些老服务,代码改 动会带来一些风险,这就需要在行动前调查清楚。 调查的方面包括,现有的数据上云辅助 服务的基本原理和相关的代码逻辑细节,最好能够尽早发现其中存在的问题并及时修复。 另外一方面,是数据上云过程需要实时监控,尽量全面地对服务接口调用质量、超时率、写 失败率、不一致率等等有个清晰的把握。这一方面最好能从上报监控和日志监控两方面来 做。而多地部署方案,目前打算采用一主多从,主从之间单方面复制,只写主库,从库只读 这几条原则入手。采用多地部署方案主要是提升服务稳定性,降低大多数请求的延迟,提升 服务质量,消除跨地域的链路不稳定带来的影响。多地部署所带来的数据同步延迟不容忽 视,必须有一个可以接受的延迟,这一块要从理论和监控两个方面来把握。 再就是,掌握 一门脚本语言很重要。特别是当有大量重复的事情需要处理,或者需要分析一些数据来获得 一个结论的时候。能够较为熟练地掌握类似 Python 这样的脚本语言,有很大的优势。但 是,如果说拿 Python 来写一个大型程序的话,我觉得是不明智的。每种编程语言就像不同 的刀,都可以拿来切菜,但是有些刀更适合用于切肉或切骨头。

2022年12月14日

二零二二年十二月第一周技术周报

这一周的工作,总结说来,主要就是将一个核心服务上云,然后不断将云下的节点切换成流 量转发节点。上云的第一步就是,在云上环境部署该服务的节点:迁移配置文件、环境,然 后根据稳定版本的代码编译适用于云上环境的镜像,然后让服务在云上环境跑起来。服务跑 起来后并且测试完成后,云上节点目前是没有任何流量的,这个时候需要将云下的部分流量 转发到云上,首先是要将云下的部分节点替换成转发节点,转发节点的作用是将主调打过来 的流量转发到云上的节点。后续可以利用这部分流量来观察云上节点的工作状态,检查异 常,可以称之为“流量灰度”。流量灰度一般控制在整体流量的 1%-5%作用,要看服务的关键 性来调整比例,测试环境下可以适当多一些,25%-50%都可以。流量灰度是发生问题时,或 者接到问题单时,只需要关闭云下的流量转发节点即可。当然,上面的一切必须先要在测试 环境操作,测试妥当了再按照同样的方案谨慎操作生产环境。 有一些后台体系错综复杂, 一次调用涉及多个服务,业务逻辑扑朔迷离,这时候尽量控制变量一段时间内仅仅做出一次 改动,等观察一段时间后,情况稳定下来了再做下一步操作。或者可以把大的步骤拆分成几 个小的步骤,一段时间内只做一小步,观察一段时间后再继续推进下一小步。这样不容易出 错。 接下来,如果灰度验证通过了(一般持续一个星期)。 接下来就是切换路由了。切换路由就是将云上的服务节点的路由直接切换到云上节点,在这 以后其他云上的主调服务将直接访问该服务的云上节点,而不会再去访问云下节点。在切换 路由的时候必须特别小心,因为大量的线上流量会直接打在云上节点上。在操作之前首先, 需要根据往常数据计算云上各个地域下需要的节点数量,一般是按照高峰期流量水平来计 算,如果不够要扩容,以免由于容量不够造成线上大量超时。这个时候,云上节点数宁愿多 一些,因为多了后续还能够慢慢缩回来,成本不高。但是如果少了造成大量超时的话,节点 扩容是需要时间的(主要是资源调度与服务启动耗时),这个时候容易造成用户投诉。如果 由于超时引发客户端大量重试,就有可能带垮整个服务,引发线上事故。当时,我是挑了一 个流量少的时候操作,这样能够减少由于切换抖动造成的影响。 切换了路由后,云下节点 就可以逐渐全部换成转发节点,将云下的流量都转发到云上来处理。然后通知主调服务的负 责人,尽量把服务上云。因为,转发节点转发到云上是有开销的,会增加延迟。

2022年12月9日

二零二二年十一月第四周技术周报

这一周,我主要在优化一个服务。这个服务是用 Java 编写的。生产环境流量不大的时候, 也会出现调用批量超时的现象。而且发送超时的时候,CPU 占用率很低。经过观察,CPU 占 用一直就上不去。这个时候就推测是不是线程都阻塞在了某个操作上,导致这个问题。我所 接触到的大多数服务,包括这个服务,都是 IO 密集型的服务。 这类服务涉及大量的 RPC 调用,当 RPC 调用的时候,工作线程会阻塞,导致无法处理其他 请求。所以这类服务的工作线程数都会设置得很大,确保有多余线程来处理 IO 请求,防止 由于大部分线程阻塞导致后续请求得不到处理,最终导致大批量超时。 这其实是有问题 的,虽然说目前 Java 在处理 Socket 和解析 Request 的时候已经采取了 NIO 的模式,但 是对于请求的逻辑处理依然采用工作线程池的方式。当遇到工作线程阻塞在 IO 请求上的时 候,这个工作线程只能等待 IO 请求完成或者超时。如果说,IO 请求所返回的结果并不是 该请求最终结果的依赖,这种情况会将请求提交到其他线程池处理。这个时候它的处理对于 该工作线程来说是异步的。 最后经过排查,这个服务由于框架问题,有关于工作线程数量的配置并未被读取。这导致了 该服务启动时采取了默认的工作线程的数量设置,也就是工作线程数和 CPU 核心数相同。 所以,工作线程在处理下游 RPC 调用时,只要下游接口的耗时稍微有波动,就会导致大量 请求超时。这个时候,由于工作线程大部分时间阻塞在等待 IO 操作返回上,所以 CPU 占 用也是很少的。 如何排查的呢,首先需要在日志框架中设置打印当前线程名称,对于 logback 日志框架,就是在日志格式设置中加上%t。然后,对单个节点在测试环境进行压 测,查看在一定 QPS 下,该服务对于请求的处理情况。最好能给这个请求的逻辑代码加上 StopWatch,辅助进行分析。最后发现,输出日志的线程只有几个,不对劲。然后,使用 jstack 过滤出线程数量。发现确实只有四个。 可以解释一下,这个 Java 服务使用的框架采用了 netty 框架进行请求的处理,最后会转 交到 netty 的工作线程池对请求的逻辑进行处理。在这里,netty 工作线程池对应的前缀 就是 nioEventLoopGroup-5。另外可以从 cpu 累计时间来辅助证明,确实只有这 4 个线程 在处理所有的请求的主要逻辑。对于这种服务来说,生产环境下,工作线程数量一般设置为 800~1000,并且会严格设置 RPC 调用的超时时间防止大量工作线程被阻塞,最终导致节点 的吞吐量急剧下降。 在更新框架后,该问题得到了解决。另外,我还发发现了该服务的一 个主要接口涉及基于 http 协议的下游调用。该调用通过操作 OkHttp 库来进行,我发现调 用并未设置超时时间。这是错误的情况,如果遇到极端情况,下游迟迟不返回,那么工作线 程就会阻塞很久。所以,必须对下游调用设置一个合理的超时时间来保护上下游调用之间的 通畅,防止雪崩现象的发生。超时时间一般分为三种,连接超时、读超时、写超时,都需要 设置。另外,对于可能存在大量的 Http 调用情况,我开启了 OkHttp 的 ConnectionPool。 ...

2022年11月28日

二零二二年十一月第三周技术回顾

这是我的第一篇有关于工作中遇到的技术问题的周期性回顾,所以本期的技术回顾主要是总 结长期以来的经历,为以后的技术回顾开一个好头。 来公司工作了快半年了,最近我从客 户端开发转成了后台开发。这是我希望的,但其实不是我要求的。因为个人感觉,在中国科 技行业目前的职业生涯规划中,后台开发可以探索的东西会稍微多一些,接触问题的规模也 会大很多。其实客户端也大有可为,我的第一个比较成熟的开源项目 GpgFrontend 就是一 个客户端项目。我在其中投入了大量的时间,解决了大量的问题。尤其是编译问题、平台兼 容性问题、稳定性问题。其探索的深度,问题的难度,都是不小的。 我目前觉得,为什么客户端和后端开发会被分出高下来,那是因为心没静下来,总想着往上 爬。另外,对于技术的追求本来无所谓客户端、前端或者后台,他们背后的技术思想本质上 是通用的,但是目前将职业看得太重,反而以职业分工来将技术划分地分明。我觉得这是不 好的,所以虽然我是目前是一个后台开发,但我绝不能认为我仅仅是个后台开发,其他技术 看都不看,学都不学,自己把自己的思维框起来。 现在职位转成了后台开发,我反而不知 道该怎么做了。后台开发,我目前记得,我自己写过的最早的后台项目,是我 15 岁的时候 用 Nodejs 写的一个天文论坛 Stelescope 。用了当时流行的 Express 框架。现在我还记得,当时第一次接触 MongoDB、登录登出的状态保存、异步回调 等等技术概念。我印象最深的是异步回调,当时用了很久才理解。因为我当时连进程、线程 的基本概念都搞得不是很清楚,更别说什么异步回调和闭包了。 那时,网上还在争论 Nodejs 和 PHP 孰优孰劣,Nodejs 到底适合什么场景,然后就是异步 回调和多进程并行孰优孰劣等等。当时记得异步回调不能被阻塞,如果遇到阻塞操作会使用 一种非阻塞的 API,这种 API 能够立即返回,将阻塞操作放到一边处理,主线程依然能够 继续处理后续的代码。等阻塞操作处理完了,就会通知主线程执行回调函数来处理阻塞操作 的结果。 后续,我还接触了 Python 的后台开发,写了一个简单的班级学业考勤管理系统 (SP)。在这里,我接触了 MVC 的思想,我理 解为模型、视图和控制器。这是一种重要的思想,那时候前端的概念还不是很明细,前后端 分离还不是主流思想。在当时,服务器负责动态页面的生成,由请求触发控制器做出响应。 控制器基于模型进行计算,并最终通过模板引擎将模型渲染成页面,而后页面被返回到用户 浏览器。就这么一个过程。记得当时,会在服务端定义很多模板,模板里选一些地方镂空, 待会存放数据。或者是定义一个小的卡片控件,放在 for 语句中,待会用模板引擎生成出 很多卡片出来。在当时,我作为后台开发要考虑方方面面,包括页面漂不漂亮,数据安全, 响应快不快等等。 然后大一下开始,我就接触了 Spring 框架,具体用的就是 SpringBoot。这个时候,才真 正了解到了关系型数据库。原来我对于关系型数据库的理解,仅仅是会安装,配置。记得很 深刻,当时接触到了前后端分离的思想,觉得这是个好东西。拿出来和王老师讨论,说我们 的全员育人管理系统用前后端分离最好。王老师非常开明,和我聊了很多,同意了我的方 案。前后端分离,顾名思义,就是将动态页面的渲染放到用户那边,服务器仅仅负责数据的 处理和存储。这样有助于分工和解耦,虽然当时网上也有很多质疑的声音,我还是觉得这是 趋势。当时害痴迷于一种 RestfulAPI 的接口规范,觉得如果这样做我们连写项目文档也可 以免了。但是现实很骨感,在实践中对于一些复杂的情况就难以保持 RestfulAPI 的风格。 通过与 SpringBoot 打交道,我学会了很多有关后台的东西,目前我也在用这些原来的经 验。我在大学本科生涯中,写了很多的 SpringBoot 项目。到现在,我进了公司发现部门的 技术栈就是 Java,新的项目用的框架都是 SpringBoot,反倒用上了。虽然说我当时很厌恶 Java,觉得它臃肿与繁琐,包括现在也没有什么好感,但是 Java 的生态确实是很强大的, 想找个什么组件很容易,而且 Java 组件都是成熟的、良好维护的并且文档齐全。Java 技 术栈对于面向生产的后台项目来说,确实是个省心、好找工作的技术栈。 ...

2022年11月21日