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

在全国范围(如广州、深圳、上海、北京)部署大量客户端的配置下发服务中,面对节假日、活动上线等高并发场景,传统的单地数据中心架构难以支撑。本文分享我当属在“配置下发服务”层面进行的多地部署、数据层优化及缓存机制升级,阐述其技术背景、方案设计、落地实践与收获。 背景 我服务的客户端分布在广州、深圳、上海、北京等全国多个城市,通常需要根据运营策略获取各种动态或静态配置,比如节日活动、功能开关、故障临时屏蔽等。这些配置由服务端统一下发,客户端根据自身属性(地域、版本等)拉取对应内容。 随着客户端规模扩大,特别是在活动上线、客户端批量启动的场景中,配置拉取请求会在短时间内剧增,导致服务端QPS暴涨,严重时甚至会拖垮主服务节点。我虽在容器层实现了全国多地的 Kubernetes Workload 部署,但数据层(包括数据库和 Redis)仍集中在广州。所有请求即便从北京或成都的容器发出,最终还是要跨地域访问广州的数据中心,延迟高、压力集中、体验差。 此外,虽然服务中使用了容器内缓存机制,但考虑到容器本身应保持无状态,其运行内存不适合长时间、海量地持有配置缓存。一旦配置数据规模扩大至几百兆甚至上 GB 级别,运维和资源成本都急剧上升。 为了解决这些问题,我决定从数据层和缓存层入手,进行更彻底的多地部署改造。 设计思路 基于上述挑战,我设计的方案主要包括: 主数据库实例(写 + 读)部署在广州,运营人员在这个实例上进行配置管理、写入操作。 各地部署 只读副本(Read‑Only)数据库实例,例如上海、北京、成都等地。各地容器连接对应地区的只读实例以拉取配置。 Redis 亦做多地部署:每个地域有自己的 Redis 实例,容器拉取缓存优先从本地 Redis;若缓存未命中,再访问本地只读数据库或主数据库。 在配置下发平台中,对不同 Kubernetes Workload 打标签(如地域、版本、标签等),客户端根据标签拉取对应配置。运营人员仍在统一平台管理配置。配置平台写入主数据库,然后同步至只读副本,各地容器读取对应地域的副本。 架构优势 读请求分散到各地只读副本,减轻主数据库压力。 客户端拉配置网络路径变短、延迟下降、体验提升。 配置写入集中在主实例,确保配置管理统一、操作简便。 架构符合“写少、读多”的典型场景:操作多为配置读取,写入(新增活动、屏蔽功能等)相对少。 实施 根据上面的设计,我继续保留广州作为主写中心,同时在全国多个区域(如北京、上海、成都)部署只读副本,配合就近的 Redis 实例,实现配置服务的分布式读请求调度。主数据库仍由运营人员维护和写入更新,通过异步同步机制,将数据分发到各地只读副本。各地的容器服务则连接本地副本,从而避免跨地域拉取配置数据。 Redis 也采用相似策略,在每个部署地域都设立实例,服务端优先从 Redis 缓存中获取所需配置,未命中时才回源数据库。这样既减轻数据库压力,也提升了配置拉取效率。为了让客户端能快速命中就近服务节点,我基于 DNS 做了地域级别的接入调度,同时结合 Kubernetes Workload 的标签机制,自动下发对应配置。所有运营配置仍在部署在广州的“统一平台”进行集中管理,通过主数据库推送同步,容器按标签识别自身身份,拉取所需数据。 落地效果 经过部署实践,这套方案在实际运行中带来了显著改善。各地客户端的配置拉取延迟明显降低,主数据库的读请求压力几乎被完全转移到了只读副本上。整个系统在活动流量高峰时表现稳定,再无集中节点被打爆的风险。 几点经验 虽然配置更新不频繁,但读请求量大;因此采用“主写/多地只读”模型非常匹配。 多地部署数据库及缓存时,必须评估网络延迟、数据同步延迟与一致性需求:对配置下发场景,最终一致性即可,无需强同步。 缓存层(Redis)地域就近部署效果更好,尽可能避免网络访问延迟拖累。 K8s Workload 与标签管理是核心:标签化管理便于灵活控制不同客户端区域/版本对应的配置。 地域入口(DNS 或其他流量导向机制)必须支持解析到就近入口,以保证客户端走最近节点。 容器无状态设计仍然要坚持:缓存尽量外部化,避免容器失效导致缓存丢失或资源浪费。 写 + 同步机制需要监控:虽然配置更新少,但副本同步延迟还是要监控,避免某地配置未及时生效。 安全权限划分要明确:主实例写权限、只读副本只读账户、运营平台权限划分。

2025年11月10日

先行动而非先推理

很多时候,我在做事前都会去考虑很久利弊得失。就比如:我去做了这件事会遇到哪些情况?这些情况下我该怎么办?那些情况下会不如人意?我用的这些个方法真的好吗?等等。不得不说,这伴随了我的很多的决策,特别是在一些比较大的决策下,这些事先的思考真的有用处。所以,我在做很多事情之前,都会去想一下这些问题。如果我的结论是:这件事情目前条件不成熟,或者这件事情很麻烦,亦或者这件事情目前前置条件没达到。那么我就不会去做这件事情,以免投入了很多,却收效甚微。但是,最近我就发现,我这样将这个方法扩大化并不好。因为很多事情,对我来说是新的,在我现有的经历和经验下,对这些事情的认知还处于比较幼稚的阶段。这种情况下,我无法正确地去推理上面说的这些问题,从而去得出正确的结论。 具体来说,利用常识是真的能枚举一些可以预见的情况的,理论上,这些情况会发生。但是,实际上,一件事情,或者一个项目,这些理论上会发生的事情都有它们会发生的概率。这些概率又由许许多多的前置变量决定,如果对这件事情的认知不足,很难说对这些变量抓大放小,找出主要。这样就容易放大一些变量的影响,最终导致预测的事情其实并不会发生。另外,在很多情况下,我也会对一件事物的性质和反应对出某种推断,特别是对一类人的推断,比如说我觉得某些人不友善,某些人友善这样。但实际上,在你真正地接触这些人后,你才能知道他们是否真如你想得那样友善。或者说,他们对于你和他们接触的真正反应,只有你去接触后才知道。这些事情是无法进行预先推断的,除非建立在你经常和别人打交道的基础上。所以,在很多时候,我对于某些事物,某些人群的判断,是先入为主的,是从别人那里听来的,模糊的,加入了自己想象的一种判断。大多数时候,这些判断并不准确。如果我想让这些判断准确,那么我就要先行动起来,去做这些事情。 先行动,也就是说在对事情有个轮廓,模糊的前提下,优先行动起来,放低姿态去尝试,然后再根据反馈的结果看看是否可行。如果反馈良好,完全可以继续行动。如果反馈不好,也不必立即收手,可以分析一下原因,或者拿着这些具体的案例问问其他过来人。不论哪一种情况,都比先不动,然后想一大堆,这样如何那样如何,来得具体、实在与真切。很多我们认为实践中会遇到的比较大的问题,往往会在真正的实践中找到思路。反之,在实践中真正遇到的大的问题,在想法中往往预料不到。所以,先激活自己,去迈出第一步,在对事情认知比较模糊的情况下,比什么都重要。 先推理,我认为是建立在对事情已经有一个比较成熟的把握的情况下,特别是在这个事情所述的领域,我并不是一个纯粹的新手。而推理的准确度,建立在我在这个领域的学习与实践的成果的叠加。也就是说,推理需要真真切切的样本,这些样本需要自己得到,而不是纯粹从别人那里“听来”,更重要地是自己通过实践“寻来”。这种情况下,有了这些样本作为基础,对事情的推理预测的把握才能更大,对于事物的认知才能更加准确。这样,后续就能不断靠推理来规避一些在这该领域下的陷阱,从而让自己能够跑得更快更稳。 如果综合上面的论述,我可以发现,总是应该先去行动,因为推理没有行动的结果作为支撑,是摇摇欲坠的。先行动而非先推理,确实是可以作为我在对于一些自己并不熟悉的领域的一种行事的习惯,从而避免先入为主,然后想太多,最后什么都没做,白白错过了可能的机遇。

2025年11月10日

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

看了一下当时的周报,值得说的事情也就是关于日志整理的事情,然后就是关于一个配置下发服务的多地部署。关于多地部署的事情,下一篇技术周报再详细说明。 首先来说说,日志方面的事情。现在的话,线上有很多服务。然后目前 DEBUG 的日志,是用了腾讯云的 CLS。而目前存在一些问题,这些服务的日志有些分散,不太好找。如果一个请求跨越了多个服务,有可能需要研发去跨越几个日志集然后来定位问题。然后就是,这些服务技术栈并不统一,并没有一个统一的日志监控框架来监控这些服务的状态。目前追踪跨服务的请求使用 TraceID,TraceID 是在请求进入网关服务或者入口服务的时候自动生成的。TraceID 有可能是纯数字,也有可能是数字带字母。后续的服务之间的请求基本上会带有 TraceID 这个字段,用于追踪请求。然而,也还有很多请求内容里不带 TraceID,导致服务没办法从请求本身来拿到 TraceID,只能索性重新生成一个,所以这些请求在各个服务之间的 TraceID 都会不同。总而言之,理论上,正常情况下,一个 TraceID 将伴随一个请求,从进入追踪范围到出追踪范围为止。 另外还有个问题,就是这些个日志集中,日志内带有的字段也不太统一,有写日志集的请求内容的字段叫 X,有些又叫 Y。有些字段日志的结构上有,实际输出的日志中却没有填写。如此这些,都增加了新的研发上手定位 BUG 原因的学习成本。上述这些问题,都很让人头疼。 还有一个就是降本增效的问题,在目前的已有日志的基础上需要看看还有哪些日志可以精简优化的。整个部门,每个月的单单存储和整理日志成本都十多万。而我负责的这一块的业务量又占大头,日志量也是非常大的,每天大概有 540 亿条日志。所以,我需要在这一块多下功夫。 对于当前的的 TraceID 问题,我采取先把同样技术栈服务的 TraceID 的生成方式和传递方式都改为统一的。对于 Java 技术栈的服务,用的无非都是 Spring Boot 框架。所以我对每个服务都增加了一个统一的拦截器,来拦截请求和响应。对于请求,这些 Java 服务处理的都是文本类型的请求,基本上都是 JSON 格式编码的。所以,无外乎就是检查 JSON 字段中是否有相应的 traceId 字段,如果有的话,那就拿来用。具体就是把 TraceID 放置到线程私有化存储中(MDC),伴随着这个线程处理。这个方法也有局限性,有些请求会在某些步骤上进行异步处理,转移到其他线程池的线程上处理了。这样,TraceID 的追踪就丢失了。我的办法是写一个包装类,接管所有异步处理的需求,参数与调用方式和原来一模一样,然后在其中检查上一个线程中是否含有 TraceID 字段,然后把 TraceID 字段先放到包装类产生的对象中。切换到另一个线程后,这个线程先看看包装类对象中是否有上一个线程存下的 TraceID,如果有就提取这个 TraceID 然后记录到自己的私有化存储中。这样 TraceID 的追踪就不会中断了。 请求拦截器的 TraceID 生成过程,也可以提一下。由于很多以前的老服务使用了二进制的请求格式,技术原因导致了这些请求无法灵活变更。一旦更改了请求,那么上下游都需要重新编译生成编解码器,然后重新部署。这对于日志改造来说,工作量不可接受。然后有很多请求中,TraceID 是 long 类型的,而非字符串。所以我就需要照顾这些老服务,大家都统一使用 long 类型的 TraceID。然后,我还希望在 TraceID 中带有一些额外的信息,比如说这个请求是从哪个地方被赋予 TraceID 的。如果这个信息能直接从 TraceID 中看出来,那就再好不过了。所以我不是简单地随机生成一串数字当作 TraceID,而是保留前面的 4 位数字。前两位用 99 作为开头,表示这个是使用了统一后的 TraceID 方案,和以前的纯数字的 TraceID 区分开来。然后,后两位数字按序号表示各个服务。01,表示网关服务 A;02,表示入口服务 B 等等,这个我来手动分配,最后会把分配方案写到文档中。后面的数字,都是随机生成的。 ...

2024年11月17日

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

本周有个插入的事项,就是说我们目前在使用某云服务商的 API,对于其中的 API 鉴权,那边有更安全的方案。原先,我们应用调取云产品的功能时,是通过一个 AccessKey 和 AccessSecret 来的。在调用的时候,需要在 SDK 中提供这两个值,然后就可以在应用中使用 SDK 提供的云产品 API 了。原先的安全要求是不能在代码中存储这两个值,公司会有某种自动扫描机制,通过这个机制可以检测代码仓库中的敏感信息,这两个鉴权用的字段也当然囊括在内。所以,我们一般都会把这两个值写在配置文件中,安全性会更好一些,也方便更改。 而现在他们说这两个值是静态的,无法在泄露的情况下快速进行更换。毕竟我们服务一般都有几十上百个容器,而目前来说,启动配置虽然已经集中于各种配置中心里了,但是在技术上是无法实现秒级别的动态更改的,必须在修改配置后重新启动容器。而容器的启动是无法通过一次性重启所有容器来完成,必须进行分批重启以保证服务的平稳,没有十几分钟甚至几十分钟是搞不定的。 所以云提供商的技术团队提出了一个新的方案,通过向我们提供一些必要的而且泄露后不容易造成直接影响的凭据(好像是一个密钥文件),然后通过在线认证的机制下发动态的验证信息。这个过程比我上文提到的验证机制复杂许多,特别是我们需要在容器中纳入这样一个配置文件,或者直接在 JAR 包中纳入。然后我们需要引入一个 SDK,来专门做这个动态验证信息的获取,然后将这个信息通过某种方式传递给云产品的 SDK,最后才能实现云产品 API 的调用。 这个具体的过程并不是我来跟进,我把这些问题交给其他同事了,由他们来具体执行。我只是作为云账号权限的控制者,为他们提供密钥文件并给他们提供正确的方向即可。

2024年4月15日

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

本周值得注意的事项就是,测试人员那边反馈在测试环境下,某些账号突然无法正常使用的问题。目前整个账号的体系还是在沿用老的系统。而在当前老系统中,账号信息使用某种特殊的数据存储分成多个模块存储,每个模块相当于一张表。每张表都各有侧重,有些侧重于与其他账号之间的关联关系,有些侧重于查询等等。这些账号无法使用的问题在于,其中最重要的一张表也就是主表,这个用户的记录消失了。在其他表中该用户的相关记录还是存在的。 这就引发了我的各种猜想,是否是测试人员测试的过程中的环境弄混了?或者是某个服务的配置有问题导致了部分应该写在测试环境的数据写到了生产环境去了?说代码写得有问题导致了某些极端情况下,用户的主表信息写入失败?通过调查发现,最终要的查询表中,数据结构还是完整的,而且能够反推出用户注册时候的微信账号。查询表只有在账号创建的时候才会去写,后续基本不会再修改了。而主表中的数据应该是和查询表在用户注册的时候一块写入。 对于这个问题,后续推断应该是数据模块发生了某种问题导致数据丢失。而在我咨询数据模块相关维护人员的时候,我还没说清楚问题,他们就说是测试环境并不保证服务的可用性,可能发生任何问题。那么这个问题可能就无解了,因为如果是数据模块这层问题,我作为使用方基本没有任何能力来自己解决。除非我能替换掉这个数据模块,使用更加稳定可靠的解决方案。那对于这个问题,我只能够然同事手动清理查询表中的索引关系,然后在让用户重新进入应用。当用户重新进入应用的时候,由于没有索引就相当于没有这个用户,此时就会重新创建用户。后续几个月遇到了十几例相同的问题,没有办法,让同事添加了一段自动处理这个问题的业务代码,以免去每次手动处理的麻烦。 这个问题第一次出现将近一年后,由于某个重要客户反馈了这个问题。而又正好这个重要客户当时半个月之内反馈的我们的各种问题有点多,领导对此十分困扰,所以对于我这个问题他特别重视。领导后续找到我让我推动解决这件事,我同意执行,因为我也想知道问题具体是什么。现在能够确定的是,问题出现在数据存储层,而这一层又不是我们直接控制,我们只是使用这样一个产品。这个产品已经很老了,维护人员很少了,而且还有其他事情。他们不愿意来主动寻找问题,也是情有可原的。所以想要推动他们的维护人员来解决,就只能抓住具体且直接的证据来证明他们的问题在哪里。 我以当时的了解,知道数据层分为缓存和落盘数据库,而且经过一年在各种问题的解决过程中学习,我也能很熟练调取分析落盘数据库里的数据了。我调取了落盘数据库中的用户 ID 的倒序情况,发现从某个用户 ID 开始,后面就没有了。最近一年,用户报问题的用户 ID 以及新注册的用户 ID 是远远大于这个 ID 的。我又去日志中获取了一个最新注册的用户 ID,查询数据层,发现是有的。说明缓存中是有正常数据的,但是由于某种原因数据并没有落到数据库中,而是在缓存满后直接消失了。 这已经能够说明问题了。于是我将这些调查记录截图发给数据层产品的维护人员,并提了一个工单,推动他们来解决这个问题。该问题的脉络我已经提前分析地很清楚明了了,而且又有直接的证据,他们也开始认真对待这个问题。后面确实,他们重启某个模块后,这个问题就好了。而且最终他们承认问题与缓存层落库的机制有关系。

2024年4月15日

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

本周相关的一些事项,我认为最值得讲得就是某个数据库接入层服务的给每一条数据加上键过期时间。这个数据库接入层是一个 Java 服务,本质上做的工作就是将 Redis 与数据库结合起来。对上的接口遵循某一个公司内部的特定协议,而对下则使用开源且很多人使用的 Java 包来访问 Redis 或者数据库。访问 Redis 使用的是 lettuce,而访问数据库用的就是 mybatis。原来访问 Redis 其实用的是 Jedis,后面之所以改用 lettuce,是因为它在高并发下的性能/资源表现更好一些。我认为具体地主要体现在多路复用连接,使得不需要在连接池中维护很多连接,总之就是在连接上的开销不会很大。 然后对于数据库来说,并非直接写入,而是在消息队列中写入一条插入指令并附上一些上下文。这样消息中包含了写入数据库需要的所有信息了。然后这条消息队列也是自己在消费,消费的时候将数据写入数据库,如果成功就 ACK 消息,失败了就让这条消息 5 分钟后再回来。 这个服务总的读写策略就是,如果一条数据查询来了,Redis 中没有数据时就将数据库中的数据拉到 Redis。然后返回 Redis 中的数据。Redis 中有数据则直接返回。对于写入数据来说,这条数据会双写 Redis 和数据库。修改数据的时候,操作复杂得多。因为这个 Redis 中数据是使用 HSET 的方式进行存储的,然后一条修改指令可能只是修改 HSET 中的一部分字段。这个就需要对一些具体的情况进行讨论了,数据库指令也是根据具体情况来构造。 值得一提的事写数据库的时候,目前看前人升级了策略,不直接拿消息中的数据来写,而是消费到消息时,直接从 Redis 中读数据(这个时候 Redis 是早就被双写了的),然后直接将 Redis 中读区到的数据写入数据库。我认为这样做的好处是,让数据库中的信息尽快更新。因为从消息投递到消息队列再到消费的过程中,这一条数据可能已经修改了好几次了。 在这几周我发现这个服务在 Redis 中的缓存并未设置过期时间,而是依赖 Redis 实例中设置的统一过期策略来进行数据淘汰。这个策略是在 Redis 数据存满的时候才会执行的,这就会引入一个风险。根据我们云上分布式 Redis 的设计,如果 Redis 存储一直都是满的然后这个时候有大量的数据写入操作(某种特殊的业务高峰期),这样云上分布式 Redis 为了一次性腾出足够的空间会集中进行淘汰操作。这个时候无法处理任何请求,这种情况将持续几分钟。这对于业务来说,存在较大影响,因为这个时候上游业务服务将无法读或者写入数据了。而现实情况就是,这个服务的 Redis 一直处于满的状态。 为了降低风险,最好就是为每一条数据设置过期时间。使得 Redis 中存储的始终是较热的数据。而设置过期时间,采取的策略是取一个固定的时间 Y,然后再生成一个均匀分布的浮点随机数因子 a,从 0-1 变化。然后我们对于某一条数据的过期时间就是 Y+aY。这样设置能尽可能避免某些情况下大量数据集中过期造成的某段时间内数据库压力过大问题。 当时 Redis 中有 64GB 的数据,且整个 Redis 实例是满的。我评估了一下,如果只是增量进行数据更新,确保新的数据是设置了过期时间的,这样应该能让存储空间缓慢降下来。最终应该是会剩下少量的数据没有更新,占比应该不大,因为我们的业务不是存量业务,大部分用户应该会继续使用。 ...

2024年4月14日

关于个人对AWS服务的使用感受

原文经过 AI 润色,非本人文风。 从 2022 年 11 月开始,我开始使用 AWS 服务,已经接近 1 年的时间了,而且不是免费套餐。从一开始对它的使用感到新奇和陌生,到现在对它的了解更加深入,期间经历了很多有趣的事情。那么,为什么我选择使用 AWS 呢?因为我个人有一些需要在 AWS 上运行的服务,比如博客网站(Wordpress)、Git 仓库、RSS 订阅等等。当时,我觉得 AWS 在云服务市场占有最大的份额,所以它的解决方案应该已经非常成熟了。对于 AWS,我还是有一定的信任,愿意花钱购买适合的服务,以确保我的个人托管业务能够长期稳定运行。 在开始使用 AWS 之前,我也尝试过免费套餐,但是一直没有找到合适的需求场景,所以用了一段时间后就不再使用了。这样,我的免费套餐周期就白白浪费了。 当我第一次接触 AWS 的时候,我首先尝试了 EC2 产品,并试用了 1 核心 1G 内存的 T2.micro 实例,但后来发现内存不够用,导致机器卡死。因此,我决定换成 T2.small 型机器,它拥有 1 核心 2G 内存,并且我还增加了 SWAP 分区,每个月大概需要花费 17 美元。虽然这个价格包括了带宽费用,但我仍然觉得有些贵。然后,我开始考虑是否可以使用 AWS 的保留实例,听说可以减少 60%-70%的费用,看起来非常划算。于是,我下定决心购买了三年的保留实例,先支付了 170 美元,然后每个月承诺支付 4.75 美元。这样一来,每个月的费用大约是 8.84 美元,这个价格看起来相对合理。 然后遇到了一个坑,当时我配置了 30G 的系统盘和 10G 的数据盘,以为 30G 的系统盘是免费的。然而,我又考虑到数据的安全性,因此还增加了定期快照备份,以为快照的费用不会很多。最后,月度账单一出来,竟然要 20 美元!我很惊讶钱都花在哪里了? 我在控制台上查找,又在 Cost Explorer 上进行了进一步的查询,才发现系统盘和数据盘的费用都按照我申请的规格计算了!没想到,AWS 竟然是按照服务器和云硬盘分别计算价格的。而且,我还不知道如何配置第二代 SSD,虽然性能很好,但我用不到这么高的性能,而且价格比第三代 SSD 贵 20%!还有,几个快照居然每个月费用超过了 7 美元。而且,费用查找界面非常复杂,一个选项下可能有很多项目,需要一个一个取消勾选,慢慢查找到底是哪一项花费了多少钱。 ...

2023年11月24日

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

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

2023年11月15日

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

本周我发现有些服务框架写得并不是很好,特别是某些 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 框架。可能人事上的考虑居多吧,无奈,我还是先写吧。

2023年11月15日

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

本周主要处理一个节前发现的风险项。某个服务,在使用 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 之间。这意味着,要求存留时间越长的键,过期的分布也越广。这能够确保业务延时更加平稳,防止数据库压力过大。

2023年6月21日