聊聊知乎订单系统迁移
本文主要介绍知乎订单系统后端语言栈的转型升级过程,包括其间踩过的一些坑和遇到的一些问题。一来是想通过本篇文章为其它应用服务转型提供借鉴经验,二来是总结对于订单系统的理解。鉴于文字功底不足,对于业务理解不充分的地方,欢迎留言交流。
迁移背景
随着知乎整体技术栈的变化,原有的 Python 技术栈逐渐被抛弃,新的 Go 和 Java 技术栈逐渐兴起。知乎交易系统的稳定性相比其它业务系统的稳定性重要很多,因为交易系统核心链路发生故障不仅会造成数据问题,还会造成严重的资损问题。随着公司业务的不断壮大发展,交易场景变得复杂,重构和优化难以避免,因为语言特性,Python 虽然开始撸代码很爽,但是后期的维护成本慢慢变高,不过 Python 在数据分析和人工智能方向上还是有很大优势的,只是在交易领域目前看起来不太合适。从技术生态上来说,用 Java 做交易系统会更有优势,所以接下来要说的知乎订单系统语言栈转型。另外一个因素是 Python 的 GIL 锁导致它无法发挥多核的优势,性能上受到很大限制,在实际情况中遇到过多次主线程被 hang 住导致的可用性故障,所以坚定决心来迁移掉旧系统。
前期准备
工欲善其事,必先利其器。
语言栈转型首先要明确转型的三个开发流程,即 MRO (Migration, Reconstruction, Optimization)
-
迁移 就是把原语言代码照着抄一遍到新语言项目上,按照新语言的工程实现风格来做就可以。其间最忌掺杂代码优化和 bug 修复,会容易引起新的问题,增加验证代码的难度。
-
重构 目的是提高项目代码的可维护性和可迭代性,让代码更优雅和易读懂,可以放到迁移完成来做。
-
优化 通过在模块依赖、调用关系、接口字段等方面的调整来降低项目的复杂性,提高合理性。
对于语言栈转型来说,迁移流程是肯定要做的,重构和优化如何选择,可以按模块划分功能拆成子任务来分别评估方案,参考依据为现有模块如果同时优化或重构带来的直接收益和间接收益有多少。
基于以上分析,在本次转型过程中,人力成本是一个更重要的因素,所以采用只迁移的方案,来压缩人力成本,降低 bug 引入风险的同时也具有很好的可测试性。并且为了不阻塞业务需求,采用小步快走的方式分批交付,以最长两周作为一个迭代周期进行交付。
迁移方案
确定了交付方式,下面我们需要梳理当前系统中的功能模块,做好任务拆分和排期计划。知乎交易系统在迁移前的业务是针对虚拟商品的交易场景,交易路径比较短,用户从购买到消费内容的流程如下:
-
在商品详情页浏览
-
生成订单进入收银台和用户支付
-
确认支付后订单交付
-
用户回到详情页消费内容
-
特定商品的七天无理由退款
当时订单系统支持的功能还不多,业务模型和订单模型没有足够地抽象,梳理订单系统业务如下:
接口验证
不论是在迁移的哪个阶段,总需要调整订单接口,可以从订单操作角度分为读操作和写操作,需要针对读接口和写接口做不同的验证方案。
写操作可以通过白名单测试以及灰度放量的方式进行验证上线,将接口未预期异常输出到 IM 工具以得到及时响应。主要的写操作相关接口有:
-
订单的创建接口。
-
订单绑定支付单的提交接口。
-
用户支付后回调确认接口。
-
用户发起退款接口。
下图展示的是 AB 平台的流量配置界面:
读操作往往伴随在写操作中。我们利用平台的录制回放功能进行接口的一致性检查,通过对比得出差异排查问题。主要的读操作接口有:
-
获取支付方式列表接口
-
获取订单支付履约状态接口
-
获取充值列表接口
-
批量查询用户新客状态接口
下图展示的是流量录制回放系统的数据大盘:
一般 3 个 9 的可用性全年宕机时间约为 8.76 小时,不同系统不同用户规模对于系统可用性的要求不一样,边缘业务的要求可能会低一些,但是对于核心链路场景 TPS 可能不高,但是必须要求保证高可用级别。如何保证或者提升服务的 SLA 是我们接下来要探讨的内容,一般有下面两个影响因素:
-
MTBF (Mean Time Between Failures) 系统服务平均故障时间间隔
-
MTTR (Mean Time To Recover) 系统服务平均故障恢复时长
也就是说我们要尽可能地降低故障频率,并确保出现故障后可以快速恢复。基于这两点我们在做系统平稳过渡时,要充分测试所有 case ,并且进行灰度方案和流量录制回放,发现异常立即回滚,定位问题解决后再重新灰度。
MTTR 快速响应
持续监控
感知系统稳定性的第一步就是监控,通过监控来反映系统的健康状况以及辅助定位问题,监控有两个方向:
第一个方向是指标型监控,这里监控是在系统代码中安排各种实时打点,上报数据后通过配置报表呈现出来的。
-
基础设施提供的机器监控以及接口粒度的响应稳定性监控。
-
物理资源监控,如 CPU、硬盘、内存、网络 IO 等。
-
中间件监控,消息队列、缓存、Nginx 等。
-
服务接口,HTTP、RPC 接口等。
-
数据库监控,连接数、QPS、TPS、缓存命中率、主从延迟等。
-
业务数据层面的多维度监控,从客户端和服务端两个角度来划分。
-
从客户端角度来监控服务端的接口成功率,支付成功率等维度。
-
从服务端角度从单量突变、环比变化、交易各阶段耗时等维度持续监控。
以上两点基于公司的 statsd 组件进行业务打点,通过配置 Grafana 监控大盘实时展示系统的健康状况。
基于以上实时监控数据配置异常告警指标,能够提前预知故障风险,并及时发出告警信息。然而达到什么阈值需要告警?对应的故障等级是多少呢?
首先我们要在交易的黄金链路上制定比较严格的告警指标,从下单、提单、确认支付到履约发货的每个环节做好配置,配置的严重程度依次递增分为 Info、Warning、Critical。按照人员类别和通知手段来举例说明告警渠道:
订单主要预警点如下:
-
核心接口异常
-
掉单率、成单率突变
-
交易各阶段耗时增加
-
用户支付后履约耗时增加
-
下单成功率过低
MTBF 降低故障率
系统监控告警以及日志系统可以帮我们快速的发现和定位问题,以及时止损。接下来说的质量提升则可以帮助我们降低故障发生率以避免损失,主要从两个方向来说明:
规范化的验收方案
其中分布式锁采用 etcd 锁,通过锁租约续期机制以及数据库唯一索引来进一步保障数据的一致性。
补偿模式,虽然我们通过多种手段来保证了系统最终一致,但是分布式环境下会有诸多的因素,如网络抖动、磁盘 IO、数据库异常等都可能导致我们的处理中断。这时我们有两种补偿机制来恢复我们的处理:
带惩罚机制的延时重试
如果通知中断,或者未收到下游的 ACK 响应,则可以将任务放到延迟队列进行有限次的重试,重试间隔逐次递增。最后一次处理失败报警人工处理。
定时任务兜底
为了防止以上机制都失效,我们的兜底方案是定时扫描异常中断的订单再进行处理。如果处理依然失败则报警人工处理。
事后总结
目标回顾
目标一:统一技术栈,降低项目维护成本。目标结果是下线旧订单系统。
目标二:简化下单流程,降低端接入成本。目标结果是后端统一接口,端上整合 SDK。
执行计划
迁移的执行总共分成了三个大阶段:
-
第一阶段是迁移逻辑,即将客户端发起的 HTTP 请求转发到 RPC 接口,再由新系统执行。第一阶段做到所有的新功能需求都在新系统上开发,旧系统只需要日常维护。
-
第二阶段是通过和客户端同学合作,迁移并整合当前知乎所有下单场景,提供统一的下单购买接口,同时客户端也统一提供交易 SDK,新组件相对更加稳定和可监控,在经过灰度放量后于去年底完全上线。第二阶段做到了接口层的统一,更利于系统的维护和稳定,随着新版的发布,旧接口流量已经变得很低,大大降低了下阶段迁移的风险。
-
第三阶段是旧 HTTP 接口迁移,由新系统承载所有端的请求,提供相同规格的 HTTP 接口,最后通过修改 NGINX 配置完成接口迁移。第三阶段迁移完成后旧系统最终实现了下线。
执行结果
截至此文撰写时间,语言栈已经 100% 迁移到新的系统上,旧系统已经完全下线,总计下线 12 个系统服务, 32 个对外 HTTP 接口,21 个 RPC 接口,15 个后台 HTTP 接口。
根据 halo 指标,迁移前后接口 P95 耗时平均减少约 40%,硬件资源消耗减少约 20%。根据压测结果比较,迁移后支撑的业务容量增长约 10 倍。
系统迁移完成只是取得了阶段性的胜利,接下来系统还需要经过一些小手术来消除病灶,主要是以下几点:
-
不断细化监控粒度,优化告警配置,继续提高服务的稳定性。
-
对于 Python 的硬翻译还需要不断重构和优化,这里借鉴 DDD 设计思想。
-
完善监控大盘,通过数据驱动来运营优化我们的流程。
-
项目复盘总结以及业务普及宣讲,提升人员对于业务细节的认知。
问题整理
迁移总是不能一帆风顺的,其间遇到了很多奇奇怪怪的问题,为此头发是真没少掉。
问题 1:迁移了一半新需求来了,又没有人力补上来怎么办?
迁移后再做重构和优化过程,其实很大一部分考量是因为人力不足啊,而且现状也不允许锁定需求。那么只能写两遍了,优先支持需求,后面再迁移。如果人力充足可以选择一个小组维护新的系统一个小组维护旧的系统。
问题 2:我明明请求了,可日志怎么就是不出来呢?
不要怀疑平台的问题,要先从自身找问题。总结两个原因吧,一个是新旧系统的迁移点太分散导致灰度不好控制,另一个是灰度开关忘记操作了,导致流量没有成功导到新系统上。这里要注意一个点就是在迁移过程中要尽可能的快速交付上线。
问题 3:公司 Java 基础服务不够完善,很多基础平台没有支持怎么办?
于是自研了分布式延迟队列、分布式定时任务等组件,这里就不展开聊了。
问题 4:如何保证迁移过程中两个系统数据的一致性?
首先我们前面讲到的是系统代码迁移,而数据存储不变,也就是说两个系统处理的数据会存在竞争,解决的办法是在处理时加上分布式锁,同时接口的处理也是要幂等的。这样即使在上下游系统做数据同步的时候也能避免竞争,保证数据的一致性。就用户支付后支付结果同步到订单系统这一机制来说,采用推拉的机制。① 用户支付后订单主动轮询支付结果,则是在主动拉取数据。② 支付系统发出 MQ 消息被订单系统监听到,这是被动推送。③ 支付成功后触发的订单系统 HTTP 回调机制,这也是被动推送。以上三种机制结合使用使得我们系统数据一致性有一个比较高的保障。我们要知道,一个系统绝非 100% 可靠,作为交易支付的核心链路,需要有多条机制保证数据的一致性。
问题 5:用户支付后没有收到会员权益是怎么回事?
在交易过程中,订单、支付、会员是三个独立的服务,如果订单丢失了支付的消息或者会员丢失了订单的消息都会导致用户收不到会员权益。上一个问题中已经讲到最终一致性同步机制,可能因为中间件或者网络故障导致消息无法同步,这时可以再增加一个补偿机制,通过定时任务扫描未完成的订单,主动检查支付状态后去会员业务履约,这是兜底策略,可保障数据的最终一致。
业务沉淀
从接收项目到现在也是对订单系统从懵懂到逐渐加深理解的一个过程,对于当前交易的业务和业务架构也有了一个理解。
交易系统本身作为支付系统的上层系统,提供商品管理能力、交易收单能力、履约核销能力。外围业务子系统主要关注业务内容资源的管理。业务的收单履约管理接入交易系统即可,可减轻业务的开发复杂度。收单流程展示如下:
-
业务定制商品详情页,然后通过详情页底栏调用端能力进入订单收银台。在这里客户端需要调用业务后端接口来获取商品详情,然后调用交易底栏的展示接口获取底部按钮的情况。
-
用户通过底部按钮进入收银台后,在收银台可以选择支付方式和优惠券,点击确认支付调起微信或者支付宝付款。收银台展示以及获取支付参数的接口由交易系统提供。
-
订单后台确认收款后会通知业务履约,用户端会回到详情页,用户在详情页进入内容播放页享受权益。履约核销流程是业务后端与交易系统后端的接口调用来完成的。
用户经历了从商品的浏览到进入收银台下单支付,再回到内容页消费内容。随着业务的发展,不同的交易场景和交易流程叠加,系统开始变得复杂,一个交易的业务架构慢慢呈现。
-
Plan 计划,明确我们迁移的目标,调研现状指定计划。
-
Do 执行,实现计划中的内容。
-
Check 检查,归纳总结,分析哪些做好了,还有什么问题。
-
Action 调整,总结经验教训,在下一个循环中解决。
很多时候,也许你只做了前两步,但其实后两步对你的提升会有很大帮助。所以一个项目的复盘,一次 Code Review 很重要,有语言的交流和碰撞才更容易打破你的固有思维,做到业务认知的提升。
参考文章
https://mp.weixin.qq.com/s/eKc8qoqNCgqrnont2nYNgA
https://zhuanlan.zhihu.com/p/138222300
https://blog.csdn.net/g6U8W7p06dCO99fQ3/article/details/103415254
招聘信息
知乎技术团队大量岗位持续招聘中,欢迎感兴趣的同学加入我们,可投简历至:luohuijuan@zhihu.com
本文文字及图片出自 InfoQ
共有 1 条讨论