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

最近,合规的要求逐渐渗透到技术层面。最近的一段时间,总有产品来找,说要实现这样那样的合规需求。或者是,做一些什么合规的调查问卷。我感觉,合规也主要就是说对于用户信息的存储、访问需要规范化,然后用户能够逐渐开始掌控自己的数据。然后还有一些人事上、组织架构上的变动对于技术层面造成的冲击,比如说一个业务被调度到其他部门去了,然后我们还在一直和这个业务共享数据库等资源。这个时候费用问题就突出了,就是说钱该算在那边。虽然是一个公司、一个事业群,但感觉可能是由于公司内部的核算机制问题,对于这些成本问题还是比较较真的,总会掰扯什么:他们用了我们的数据库,钱还在我们这边什么的。有时候感觉,这些都是属于内部消耗,并不是出于发展的眼光看待问题。另外,别的业务可能由于什么需求调整,需要修改一些公用的数据库结构或者接口,就比较麻烦了。因为这个时候,我们为他们花费工时来做这些事情,并不能得到任何肯定。但是,又会被那边的人催着,天天轰炸。自己夹在中间,感觉就比较难受。 然后,我上个周报提到的PHP网关迁移的问题,其实前一两个月就开始在做了。其实也早就写好了,但是在后续切换流量到新服务的时候,总是会遇到这样那样的问题。具体表现就是,测试环境流量切换很久了没有人来找,一切正式环境这样那样的人就来找了。所这个接口突然用不了,那个接口突然报错什么的。这种情况下,由于问题发生在线上,只能先把流量切回去再分析问题。一来二回,一个星期就过去了。有时候,切换了4、5天才会有人来找。这样一个流程下来久更久了。 leader一直说这件事情做了很久如何,但实际的情况就是这样。虽然这个服务的后续具体的代码编写和维护工作并非我来做了,但是我也知道一个新的服务要能够完全替代就有的服务必须有一个这样的流程。因为在缺乏文档和测试用例的情况下,你不知道一个接口有什么暗藏的机制,也无法完全清楚我写的代码是否和原来的等效。所以执行层面,很多事情没有明确汇报,但是我感觉管理上也要能体会到。虽然过程比较反复、艰难,但最终这个服务的重构工作在本周还是完成了,所有的流量都迁移到新的服务上了。也就是代表,这个老PHP网关的生命周期终结了。 我接手主要的后台业务也快半年了,现在发现刚接手的时候由于缺乏经验,总感觉别人的技术高深莫测。但是,自己接触久了才知道里面有这样那样不完善的地方。还存在一些设计缺陷,非常严重的缺陷都有。比如说把Redis当作数据库使用,说白了就是没有设置缓存过期时间,然后Redis里面的数据又很重要不能丢失。这对服务里面还存在着一些缺乏文档、维护人员早已离职的老旧服务,这些老旧服务基本上平时无法来仔细研究。但这些服务中总有那么一两个依然在被访问,或者早就不能访问了,客户这几天突然发现然后急着催我们解决。很多安全问题也需要整改,某几个API账号有一些高位的权限需要收敛,但是我并不知道这个几个账号具体是做什么的。这些问题,就是日常频繁遇到的,很难避免。

十一月 15, 2023

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

本周我发现有些服务框架写得并不是很好,特别是某些Java框架。当CPU占用达到40%左右的时候,就会出现大量的超时现象。这些服务,CPU核心和内存容量不是不多,工作线程数目也不是不够。但是,就是跑不满CPU。用Java性能工具分析发现,其实发现大部分工作线程处于Idel或者Waiting状态。目前综合所有情况分析,还是百思不得其解。NIO也用上了,还用的是Netty框架,但是吞吐量就是上不去。通过对于线程的分析,发现并没有特别繁忙的业务线程。推断应该是IO或者某种等待机制导致了这种低处理效率。 这次我是准备通过削减节点数量来降本,在执行前并没有考虑特别多的因素。所以我在削减节点容量的时候,只是通过查看CPU的负载来判断节点是否能够承受。当我将北京地域的workload平均CPU占用率提升到35%-40%的时候,整个服务出现了大量的超时现象。当时把我吓了一跳。后面从监控上分析,几乎是整个背景地域的节点都超时了,也就是处于一种”休克“状态。 本周也继续重构某一个老的PHP网关服务,新的网关用Java写成。但是我其实不是很赞成网关用Java,毕竟Java这种语言的执行特性很大程度上决定了它不适合特别高的并发。并且,我们目前所用的都是JDK 8,并没有引入轻量级的线程,每个4c8g容器内线程最多也就800个,多了线程切换开销就会特别大。所以单个容器的吞吐量有限,承载同样的流量需要更多的容器。刚开始我是用Go重写的,这个框架很不错而且也有专门的团队维护,但是leader还是让我用部门自研的那个Java框架。可能人事上的考虑居多吧,无奈,我还是先写吧。

十一月 15, 2023

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

本周主要处理一个节前发现的风险项。某个服务,在使用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