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

时间进入二零二三年,今年是将一个辛苦的一年。今年将面临几方面的挑战,一个是将原先部署在物理服务器上的数据都迁移上云。然后就是,加快培养几个团队新人,让他们尽快承接目前的主要业务所涉及的服务,并且要求能够独立解决用户问题并对服务做出优化。这样我就能将一些工作转交到他们名下,专注今年预估耗时较长的重要目标。还有就是,个人的在技术和其他方面学习也到了一个攻坚阶段,这些方面决定了我未来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,防止污染下一个请求的追踪信息。

一月 20, 2023

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

本周我感染了新冠病毒,一共在家待了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占用率双高的情况也很少再出现。

一月 19, 2023

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

这一周,主要的工作是针对某个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来跟踪这次请求及其关联响应。对于同步请求来说,发送请求后会被阻塞,直到响应到来为止。对弈异步请求来说,会在Ticket中填入callback函数,当响应到来时,会通过TicketNumber(Ticket的唯一索引)找到对应的Ticket并调用其中事先填入的callback函数,进行后续的处理。 该框架对NIO的操作,在底层用到了Java提供的NIO库的能力。上面所提到的TCP链接,其实就是NIO库中的SocketChannel。那么该框架如何分割各个数据包呢,该框架通过Buffer来存储目前已经读到的数据,而我们常用的RPC协议,会将该包的字节数存在包的首部,只需要比较Buffer与字节数的大小即可知道包是否读全了。如果包没有读全,就继续等待后续的数据到来。如果包读全了,则按照字节数规定的大小从Buffer中分割出相应的数据来处理即可。在该框架中,会额外从一个线程池中取出工作线程来进行有关于Ticket的后续处理。目前从现有的代码来看,这个工作线程池只用来做这件事情,它的默认工作线程个数与核心数相同,最大线程数是核心数的两倍。 根据Java NIO库的定义,Channel中规定了几个IO操作,Selector会轮询检查这些操作是否就绪,如果就绪就会返回SelectionKey。SelectionKey中包含了从就绪通道中进行正确操作的必要参数。 目前经过阅读代码,从客户端侧并未发现有什么明显的问题。所使用的方案基本上是成熟且稳定的,那有可能问题出现在服务端,这还有待后续的熟梳理。

一月 19, 2023

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

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

十二月 13, 2022

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

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

十二月 9, 2022

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

这一周,我主要在优化一个服务。这个服务是用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。 根据文档,ConnectionPool的优势在于,多个同一地址的http或者http/2请求可以共用同一个连接。但需要注意,这个共享的前提是服务端支持http长连接。 另外,这周还主要去看了腾讯云的高级架构师TCP的相关内容,因为考试安排在周末。这个TCP考试比原来的架构师、从业者考试难度大了些,还是需要好好准备的。上班没太多时间看,所以我重物直接复习到周六凌晨5点,所幸最后考过了。我应该会写一篇文章来专门谈谈这个考试。

十一月 27, 2022

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

这是我的第一篇有关于工作中遇到的技术问题的周期性回顾,所以本期的技术回顾主要是总结长期以来的经历,为以后的技术回顾开一个好头。 来公司工作了快半年了,最近我从客户端开发转成了后台开发。这是我希望的,但其实不是我要求的。因为个人感觉,在中国科技行业目前的职业生涯规划中,后台开发可以探索的东西会稍微多一些,接触问题的规模也会大很多。其实客户端也大有可为,我的第一个比较成熟的开源项目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技术栈对于面向生产的后台项目来说,确实是个省心、好找工作的技术栈。 在公司工作半年,其实算上实习都快一年了。总的来说,对于后台这块,我一直在捣鼓优化、缓存、线程这些东西。每天都在分析各种告警,有些是业务上的,有些是技术上问题。对于业务上的问题,只能好好地了解背景。对于技术上的问题,需要扩展自己的知识面,静下心来研究。包括一些目前自己感觉难以解决的问题,比如说在容器化部署过程中,总有一些容器存在偶发的超时问题,一直不能确定到底是容器的问题还是后台应用的问题。我现在思考,这一块需要一些更加深入的知识,比如说CPU、内存、网络的虚拟化问题。 今天先这样吧,还有点其他事情要做。

十一月 20, 2022

自己总结出养成习惯的一些实用经验

很多人都会想养成一个好习惯,因为一个好习惯能够然仍长期做一件好的事情。这件好的事情,每做一次都会带来一定的好的影响。而一个好的影响的一次次叠加,经过比较长的一段时间,就能使得一个人的某个方面产生巨大的良性改变。而很多人想养成习惯却难以坚持下来。反而求助于很多外力来“监督”自己,这样反倒让自己变得更加痛苦。 我觉得想养成习惯而不得根本原因是缺乏内在的强大的动机。比如说,你想学习一门外语。你知道,学一门外语能够有种种好处。但是,你其实内心清楚,这些好处只不过是锦上添花,并不会影响你的前途。自己是骗不了自己的,我这里不是要说要不断暗示自己学习一门外语很重要。我这里想建议,你要让学习一门外语真的对你来说很重要,这才是有效的。比如说,你报了一个出国留学的计划,是你人生规划的一部分,花了很多钱签订了合同。你现在是不学也不行了,不学,你的人生计划就完不成,为了签订合同所花费的钱也打了水漂。这样,才会产生强大的内驱力。这种内驱力一旦产生,它会在你的内心不断提醒你,不断鞭策你,让你自动去拿起外语书。除非,你有什么拒绝它的理由,不然你的怠惰在你自己这里就过不了关。 另一个我发现的重要一点是,习惯的订立要顺应自然,最主要的是要顺应自己的内心。不能说,给自己定一个自己不喜欢的习惯。你不喜欢某件事情,你非要去强迫自己做,那最终是做不成的。喜不喜欢一件事情,主要看你先做这件事情前会不会有一种愉悦感,你能想到你做这件事后,你能收获什么,这让你感到愉悦。有一种愉悦感,能够时不时地提醒你去做一件事。 第三点,长期坚持而非一直坚持。不能说要强迫自己一直坚持某种习惯,因为随着自己生活的继续,习惯是会被慢慢改变的。长期坚持,这意味着在一段很长的时间内,你是会经常做这个事情的。偶尔的一两次没有做,不代表习惯没有了或者是习惯被打破了。因为事务的发展不是一帆风顺的,总有反复。所以给自己一点做不一样事情或者不做这件事情的时间,让自己处于一种比较放松的状态。记住,盈满则亏,让自己总是游刃有余一些,容许一些小的偷懒,养成习惯的过程中就不会出现大的报复性波动。在养成习惯的过程中,你可能会发现这个习惯并不是一个好习惯,或者并不适合你。这个时候请果断放弃这个习惯。因为养成习惯是一个正向反馈,是自己不断激励自己,不断有所收获,而不是一个自己和自己内心拉扯的过程。 还有一点就是,习惯的养成不能影响自己的正常作息。不能说我今天要背单词,由于一些突发情况没有做成,然后在深夜也要背完,挤占了自己的睡觉时间。这是得不偿失的,反而会因为自己的生活周期的失调,陷入一种疲于奔命的情况,导致习惯越来越难以养成。 最后,如何知道一个习惯是否已经养成?当你发现,你今天不做这件事情的时候,好像缺了点什么的时候。或者,某个时间会自动地想起去做这件事的时候。这个时候就可以认为习惯养成了,你只要顺着来,享受习惯的惯性带给你的快乐,在这个过程中,你就能不断收获自己在某一方面的成长和快乐。

一月 1, 1999