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

本周主要处理一个节前发现的风险项。某个服务,在使用Redis的时候并没有为键设置TTL,而是寄希望于redis的淘汰策略。我看这个服务使用的Redis设置了LRU淘汰策略。这种策略看似比较完美,但是当某一较短时间段内,写入流量很大的时候,存在隐患。这个时候Redis会触发淘汰过程,并集中尽力于此,以求能腾出足够的空间。这意味着,Redis不能很好地执行查询等正常操作了。这会造成业务层对Redis的读写延时均出现剧烈波动。到现在,我已经遇到过2次这样的问题了。而且不设置TTL,Redis一直处于100%的使用率状态,我们从容量上并不能获知按照现在的业务量配置多少购买容量足够。业务低峰时期是否能够通过缩容降低成本也是不能确定的。所以,为Redis留出一个足够的可用空间是十分重要的。 所以,我需要改造这个服务,使之对每个写入的新键设置TTL。按照Redis的逻辑,如果键在写入的时候已经存在,则也会被设置一个TTL。为了业务稳定,我不求所有的键都设置TTL。所以舍弃了在遍历Redis寻找未设置TTL的键的方案。其实我调研过这个方案,在遍历的时候,使用需要游标来做。设置的TTL长短也有讲究,为了防止同一时间大量键过期造成延时的大波动,设置TTL的时候在基础时长后加入了随机时长。假设基础时长为T,那么加入随机时长后,TTL的长度会处于T~2T之间。这意味着,要求存留时间越长的键,过期的分布也越广。这能够确保业务延时更加平稳,防止数据库压力过大。

六月 21, 2023

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

从一月底到二月初,都属于春节的范畴。期间,负责保障春节阶段的运行,需要随时待命处理线上问题。我一直处于一种担忧的状态,好在线上问题并没有主动找上门。整个春节期间维持整体不动是最好的。 这周我在评估一个重要的需求,所带来的影响。我认为,对于一个新的业务需求,特别是应用于一个复杂的业务系统,需要考虑多方面的影响。如果这个时候,对这个系统并不是特别熟悉,经验不多的话,最好选择做最小改动。这不是保守,而是将影响控制在你可以想象地到的地方。因为你不会知道在什么地方,某个违背直觉的机制在运行重要的业务逻辑。得出这样的结论,并不是出于我的想象,这篇文章写于半年后,到我写这篇文章的时候,我已经遇到至少两次这样的事情了。当时我对某个服务做出了大刀阔斧的调整,在调整完的时候,一切正常。发布后,也看似正常。直到若干星期之后,我偶然发现了某个串联上下游的机制,它差点受到我的影响。在承接一个业务系统的时候,很大概率它是转接和很多手的,藏有很多你不知道的历史,所以框架、核心逻辑能不动就不动。 然后,本周彻底解决了针对一个安全加密服务加密接口不兼容中文的问题。主要的问题是,它将加密的原文直接作为Redis的Key。当原文中存在中文,会发现虽然Key存储了,但是找不到。我的方案是在存储的时候,Key的内容不要直接包含任何业务原文,先取哈希。这样既能够避免一些编码、兼容的问题,也可以大大提升安全性。

六月 21, 2023

理解 Kubernetes 中的 CPU 资源分配机制

Kubernetes(k8s)是一种流行的容器编排平台,它允许开发人员在云环境中部署、管理和自动化容器化应用程序。在 Kubernetes 中,CPU 资源的分配是一个关键的问题,它直接影响着应用程序的性能和可靠性。本文将介绍 Kubernetes 中的 CPU 资源分配机制,包括 CPU 请求和限制、CPU Share 机制以及 CPU 调度器等相关概念,以帮助开发人员更好地控制容器的 CPU 分配,从而提高应用程序的性能和可靠性。 CPU 分配单位 在 Kubernetes 中,CPU 的分配单位是 millicpu(毫核),一个 CPU 资源等于 1000 毫核。例如,一个 Pod 请求 0.5 个 CPU 资源,可以表示为 500 毫核。它是基于 Linux 内核的 CPU 分配机制实现的。在 Linux 内核中,CPU 时间是以时间片的形式分配的,每个时间片通常为几毫秒。Kubernetes 利用这个特性,将 CPU 时间片分配单位转换为毫核。 CPU 资源分配机制 在 Kubernetes 中,CPU 资源的分配是通过两种方式来实现的:CPU 请求和限制。CPU 请求用于告诉 Kubernetes 调度器,该 Pod 至少需要多少 CPU 资源才能正常运行;CPU 限制用于告诉 Kubernetes,该 Pod 最多可以使用多少 CPU 资源。如果节点上可用的 CPU 资源不足以满足 Pod 的 CPU 请求,该 Pod 就无法调度到该节点上运行。如果节点上可用的 CPU 资源不足以满足 Pod 的 CPU 限制,该 Pod 仍然可以运行,但可能会受到 CPU 资源的限制,导致运行缓慢或者出现其他问题。 需要注意的是,CPU 请求和限制是 Kubernetes 中的两个独立的概念。通常情况下,应该根据应用程序的实际需求来设置 CPU 请求和限制。如果设置的 CPU 请求过低,可能会导致 Pod 无法正常运行或者运行缓慢;如果设置的 CPU 限制过高,可能会导致节点上的其他 Pod 的 CPU 资源不足,影响整个集群的性能和可用性。 ...

六月 20, 2023

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

这一周主要是确保在春节前各类服务的稳定。近期发现某个服务经常在流量高峰时段报超时,我提醒转交服务负责人处理。但是几天过去,服务负责人依然无法说明原因。只好亲自处理这个问题,因为报警已经十分严重,部分节点超时率能够达到20%。在这段时间中,应该是由于临近假期,流量大幅度上涨,12月底相比已经上涨了100%。所以首先是怀疑服务的承载力不足,所以先进行了一次扩容。但是,扩容后并未解决此问题,告警频率和超时率基本未变化。这种情况下,对服务的源代码进行分析后发现,该服务的接口会首先调用一个下游服务,然后再异步地向数据库中插入数据。因为异步操作并不阻塞工作线程,所以首先应该怀疑是这个下游调用的问题(实际上,我在数据库那边纠结了很久)。 从终端登录进一个节点检查日志,发现所有下游调用走的居然是同一个IP和端口,并且走的是稻草人节点。所以我先将该服务做上云操作,排除稻草人转发损失造成的影响。上云完成后,刚切流量的时候发现某个地域(不是原先的地域)下有大量超时产生,做了扩容后也无解,十分疑惑,所以暂时切回来了。我非常奇怪,为什么上云之后,超时率却变得更高了。从云上监控分析问题,发现在切流量后,下游服务的某个云上节点CPU占用非常高,而其他的节点却很低。开始怀疑是负载均衡的问题,所以又回去检查服务的源代码。果然,该服务从注册中心拿到下游服务可用节点列表后,只会调用列表中的第一项。所以某个地域的几乎所有流量都集中在下有服务的某一个节点上了。而原先通过稻草人转发的时候,稻草人调用云上节点的时候会做一次负载均衡。所以,最初的问题应该是流量大量上涨,该服务却没有负载均衡,所以导致某个稻草人过载,进而导致超时。稻草人是代理节点本身没有逻辑,所以能够处理的并发数更大,超时问题不是很明显。而一旦上云,流量会直接集中在某个业务节点上,大量流量一齐涌入,会瞬间导致该业务节点大批量超时。 有了上述的论断后,回去找稻草人的监控,发现某个稻草人确实已经过载了,CPU占用居然达到了95以上。知道原因后,接下来继续上云,先最大限度消除超时告警,然后再改代码添加随机负载均衡算法。这样做是因为节前修改代码,流程上会比较麻烦。所以首先,我加倍了下游服务的单个节点的核数,这大大增加处理能力。然后,做切流量的操作,一个下游节点的CPU占用数突然升高,最终一直保持在比其他节点高很多的比例上,这是意料之中的事情。当整个服务平稳下来后,上云就成功了,这个时候告警已经消除,超时率归零。接着,就是修改改服务的代码,添加负载均衡的机制,在各个流程审批通过后为该服务发布新的代码版本。 在我发布完新版本后,下游服务的各个节点的CPU占用就接近了,最终问题解决。

二月 14, 2023

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

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