本文主要介绍知乎订单系统后台语言栈的改造升级过程,包括过程中遇到的一些坑和问题。图片来自宝途网。首先,我想通过这篇文章为其他应用服务转型提供参考经验。其次,我想总结一下对订单系统的理解。迁移背景随着知乎整体技术栈的变化,原有的Python技术栈逐渐被抛弃,新的Go、Java技术栈逐渐浮出水面。知乎交易系统的稳定性比其他业务系统的稳定性要重要得多,因为交易系统核心环节出现故障,不仅会造成数据问题,还会造成严重的资金损失。随着公司业务的不断壮大和发展,交易场景变得更加复杂,重构和优化在所难免。因为语言的特点,Python虽然一开始很酷,但是后期的维护成本逐渐增加。不过Python在数据分析和人工智能方向上还是有很大优势的,只是目前在交易领域似乎不太适合。从技术生态的角度来说,使用Java作为交易系统会更有优势,所以接下来要说的就是知乎订单系统语言栈的改造。还有一个因素是Python的GIL锁使得它无法发挥多核的优势,性能受到很大限制。在实际情况中,我们遇到过很多次因为主线程挂掉导致的可用性故障,所以我们下定决心迁移旧系统。.要想做好前期准备工作,必先利其器。语言栈的改造首先要明确改造的三个开发过程,即MRO(Migration、Reconstruction、Optimization)。迁移:就是把原来的语言代码复制到新语言项目中,按照新语言的工程实现风格来做。其中最忌将代码优化和bug修复混为一谈,这样容易产生新的问题,增加验证代码的难度。重构:目的是提高项目代码的可维护性和可迭代性,使代码更加优雅易读,可以在迁移完成后进行。优化:通过调整模块依赖、调用关系、接口字段等,降低项目的复杂度,提高合理性。对于语言栈的改造,必须做迁移过程。如何选择重构和优化,可以根据模块的功能划分子任务,分别对解决方案进行评估。参考依据是同时优化或重构现有模块会带来多少直接和间接的收益:收益:完成新旧语言栈的转换,更好的系统可维护性,更清晰的模块边界。成本:需要投入的人力成本、迁移过程中并行开发的成本、阻塞更高价值工作的损失。风险:引入新的错误,增加测试的复杂性。在风险可控的前提下,必须权衡成本和收益。一般有两种方案可供参考:一是锁定需求,开发人力,一步上线。二是小步快进,迭代上线,分批交付。基于以上分析,人力成本是这个转型过程中比较重要的因素,所以采用迁移方案来压缩人力成本,降低引入bug的风险,同时也有很好的可测试性。并且为了不阻塞业务需求,采用小步骤分批交付,最多两周作为交付的迭代周期。迁移计划决定交付方式。接下来,我们需要对当前系统中的功能模块进行梳理,做好任务的拆分和调度。迁移前的知乎交易系统业务为虚拟物品交易场景,交易路径较短,用户从购买到消费内容的流程如下:浏览商品详情页面生成订单,进入收银员和用户付款并确认订单发货付款后用户返回详情页购买特定商品7天无理由退货。当时订单系统支持的功能还不多,业务模型和订单模型也不够抽象。订单系统业务如下:完成订单模块拆分后,新旧系统如何无缝切换?怎么实现没有业务感?如何保证交易系统的稳定性?存储和模型没有改变。①接口验证无论是迁移到哪个阶段,总是需要对order接口进行调整,从order操作的角度可以分为read操作和write操作,read需要实现不同的验证方案接口和写入接口。写入操作可通过白名单测试和灰度验证上线,接口异常输出到IM工具及时响应。主要写操作相关的接口有:订单创建接口。订单绑定支付表单提交界面。用户支付后回调确认接口。用户发起退款界面。下图是AB平台的流量配置界面:下图是一些事务警告通知消息:读操作往往伴随着写操作。我们利用平台的录音和回放功能来检查接口的一致性,通过对比找出差异来排查问题。主要读取操作界面有:获取支付方式列表界面获取订单支付履行状态界面获取充值列表界面批量查询用户新客户状态界面“第三只眼”可以及时反映系统的健康状态,发出告警信息及时帮助我们分析问题,在出现故障时迅速缩小排查范围。平台层已经支持硬件、数据库、中间件的监控,这里我们只需要梳理一下应用的监控指标即可。日志监控:请求日志、服务端错误日志。订单业务指标:订单量、完成订单量、丢失订单量、链数据、首次履行、异常数量补偿机制、履行量、各种通知事件、耗时成功履行、P95耗时履行、准时率/成功率,以及支付业务指标:支付渠道性能延迟P95支付性能延迟P95用户购买完成耗时P95③可用性保证在整个交付过程中,我们必须对改造前后的SLA提供一致的可用性保证。可以看看下面的衡量标准:一般情况下,可用性的3个9都是每年宕机时间在8.76小时左右。不同的系统、不同的用户规模对系统的可用性有不同的要求。对边缘服务的要求可能会低一些,但是对于核心链路场景,TPS可能不高,但是一定要保证高可用性。如何保证或提高服务的SLA是我们接下来要讨论的。一般有两个影响因素:MTBF(MeanTimeBetweenFailures):系统服务的平均故障间隔时间MTTR(MeanTimeToRecover):系统服务的平均故障间隔时间恢复时间是指我们应该降低故障频率,因为并确保故障可以快速恢复。基于这两点,我们在做系统平滑过渡的时候,一定要对所有的case进行充分的测试,并进行灰度解决和流量录放,发现异常立即回滚,定位后重新灰度问题解决了。④MTTR快速响应,持续监控:感知系统稳定性的第一步是监控,监控反映系统的健康状态,辅助定位问题。监控有两个方向:第一个方向是基于指标的监控,将监控安排在系统代码中进行各种实时管理,数据通过配置报表进行上报和呈现。基础设施提供的机器监控和接口粒度的响应稳定性监控:物理资源监控,如CPU、硬盘、内存、网络IO等中间件监控、消息队列、缓存、Nginx等服务接口、HTTP、RPC接口等。数据库监控,连接数,QPS,TPS,缓存命中率,主从延迟等。业务数据层面的多维度监控,分为客户端和服务端两个角度。客户端侧,监控服务端接口成功率、支付成功率等维度:服务端侧,从单量突变、环比变化、交易每个阶段的耗时。以上两点是基于公司的statsd组件进行业务管理,配置Grafana监控行情,实时展示系统的健康状态。第二个方向是日志监控,主要依托公司的ELK日志分析平台和Sentry异常捕获平台。通过哨兵平台,可以及时发现系统告警日志和新的异常,便于快速定位异常代码位置。ELK平台可以详细记录关键日志,分析产生的场景和重现问题,协助修复问题。⑤异常告警基于以上实时监控数据配置异常告警指标,可以提前预知故障风险,及时发布告警信息。但是,需要提醒什么阈值?对应的故障等级是多少?首先,我们需要在交易的黄金环节制定更严格的预警指标,做好从下单、提单、确认付款到履行发货的每一个环节的配置,配置的严苛程度是按升序分为Info、Warning和Critical。根据人员类别和通知方式说明报警通道:IM中的预警消息截图如下:订单主要报警点如下:核心界面掉单率异常,突然变化订单完成率的增加,以及交易每个阶段的耗时增加。订单成功率过低⑥MTBF降低失败率系统监控告警和日志系统可以帮助我们快速发现和定位问题,及时止损。接下来提到的质量提升可以帮助我们降低故障率,避免损失。主要从两个方向进行说明:标准化验收计划:开发完成后,包括逻辑功能和单元测试,优先保证单条测试线覆盖保证分支覆盖。然后在联调测试环境中测试自己,通过后提交测试给QA同学。QA同学可以在测试环境同时进行功能验收和接口测试,测试通过后部署到Staging环境。在Staging环境中执行功能验收并通过。可根据实际情况选择性使用灰度投递和双读验证。上线后,最后需要进行回归测试。统一编码协议,多轮CR保证:代码上线前,通常需要至少经过两次CodeReview。如果MR太小,就在工作站拉一个同事去CR。超过100行的改动需要开会讨论,两次审稿人的侧重点也不一样。第一次review要注意编码风格,可以避免写的自由发挥带来的一些陷阱,从而在组内沉淀出一个相对统一的编码协议,建立编码稳定性的基本共识,提高代码质量.二审要注意代码逻辑。这里要注意一点,如果明确了只需要迁移,那么就不要优化那些很难理解的旧逻辑,因为很可能不了解背景就写。一个错误被带到网上(看到几次)。另外,这也是比较验证的好办法,验证过线后再进行优化。只有明确目的和流程,并遵循这个流程,才能更快更好地交付优质代码。一致性保证:每个微服务都有自己的数据库,微服务内部的数据一致性由数据库事务保证,在Java中使用Spring的@Transaction注解可以轻松实现。对于跨微服务的分布式事务,支付、订单、会员这三个微服务之间采用最终一致性,类似于TCC模式的两阶段提交。订单通过全局issuer生成订单ID,然后根据订单ID创建支付。一。用户支付后,订单会改变状态,并通知会员微服务。如果合约成功履行,交易将结束,如果合约失败,将触发退款。如果用户未付款,订单系统将处理订单和付款单。对应一致性保证,我们对订单接口做了两方面的处理:分布式锁:针对上游支付消息监听、支付HTTP回调、订单主动查询支付结果,三种同步机制分别基于订单ID,然后锁定后处理,保证同步机制不会被并发处理。接口是幂等的:加锁后查询订单状态,处理成功则响应成功。否则处理后响应成功,保证上游消息不会被重复处理。订单的下游履行是通过使用订单ID作为幂等键来实现的,保证同一个订单不会被重复履行,并使用ACK机制来保证履行后不会重复传递给下游。其中,分布式锁采用etcd锁,通过锁租约续约机制和数据库唯一索引进一步保证数据一致性。补偿模式,虽然我们通过各种手段来保证系统的最终一致性,但是在分布式环境中,会有很多因素,比如网络抖动、磁盘IO、数据库异常等,都可能导致我们的处理被打断了。这时候我们有两种补偿机制来恢复我们的处理:带惩罚机制的延迟重试:如果通知中断,或者没有收到下游的ACK响应,可以将任务放入延迟队列进行有限次数的重试,重试间隔依次递增。人工处理最后一次处理失败告警。定时任务覆盖:为了防止上述机制失效,我们的覆盖方案是定时扫描处理异常中断的订单。如果仍然处理不成功,则手工处理告警。事后总结①目标回顾目标一:统一技术栈,降低项目维护成本。目标结果是推出旧的订单系统。目标二:简化订购流程,降低终端接入成本。目标结果是后端统一接口,端集成SDK。②执行计划迁移分为三个主要阶段:第一阶段是迁移逻辑,即客户端发起的HTTP请求被转发到RPC接口,然后由新系统执行。第一阶段,所有新的功能需求都在新系统上开发,旧系统只需要日常维护。第二阶段是通过与客户端同学的合作,将目前知乎所有的下单场景进行迁移整合,提供统一的下单购买接口。同时,客户端还提供了统一的交易SDK。新组件相对更稳定和可监控。灰度在去年底放量后全面上线。第二阶段统一了接口层,更有利于系统的维护和稳定。随着新版本的发布,老接口的流量变得很低,大大降低了下一阶段迁移的风险。第三阶段是旧的HTTP接口的迁移。新系统承载来自各个端点的请求,并提供相同规格的HTTP接口。最后通过修改NGINX配置完成接口迁移。三期迁移完成后,老系统终于下线了。③执行结果截至本文撰写时,语言栈已100%迁移至新系统,旧系统已完全下线。系统服务共下线12个,对外HTTP接口32个,RPC接口21个,后台HTTP接口15个。根据光环指数,迁移前后,P95界面耗时平均降低约40%,硬件资源消耗降低约20%。根据压测结果对比,迁移后支撑的业务容量提升了10倍左右。系统迁移的完成只是阶段性的胜利,接下来系统还需要经过一些小的操作来消除病灶。要点如下:不断细化监控粒度,优化告警配置,持续提升服务稳定性。Python的硬翻译还是需要不断的重构和优化。这里借鉴了DDD的设计思想。改进对市场的监控,并通过数据驱动的运营优化我们的流程。项目回顾总结和业务科普展示,增强人员对业务细节的理解。④问题整理和迁移也不是一帆风顺的,期间遇到了很多奇怪的问题,所以头发真的没掉。问题一:迁移到一半有新需求来,没有人手填怎么办?其实很大一部分考虑是人手不足,现状不允许锁定需求。那只能写两遍,优先支持需求,后面再迁移。如果人手充足,可以选择一个团队维护新系统,一个团队维护旧系统。问题2:明明问了,但是为什么log出不来?不要怀疑平台的问题,首先要从自己身上找问题。总结一下两个原因:一是新老系统的迁移点过于分散,导致灰度难以控制。另一种是忘记操作灰度开关,导致流量无法成功导流到新系统。这里需要注意的一点是,在迁移过程中,应尽快在线交付。问题三:公司Java基础服务不完善,很多基础平台不支持怎么办?所以自己开发了分布式延迟队列、分布式定时任务等组件,这里就不多说了。问题四:迁移过程中如何保证两个系统数据的一致性?首先,我们前面提到的是系统代码迁移,但数据存储不变,这意味着两个系统处理的数据会存在竞争。解决方法是在处理的时候加分布式锁,接口的处理也是幂等的。这样即使上下游系统在同步数据的时候,也可以避免竞争,保证数据的一致性。对于用户支付后将支付结果同步到订单系统的机制,采用推拉机制:用户支付后,订单主动轮询支付结果,即主动拉取数据。支付系统发送的MQ消息被订单系统监听,属于被动推送。支付成功后触发的订单系统的HTTP回调机制,也是被动推送。以上三种机制的结合,使得我们的系统数据一致性有了比较高的保障。我们要知道,一个系统绝不是100%可靠的。作为交易支付的核心环节,需要多重机制来保证数据的一致性。问题五:用户付款后没有收到会员权益怎么办?在交易过程中,下单、支付、会员是三个独立的服务。如果订单遗失支付留言或会员遗失订单留言,将导致用户无法享受到会员权益。最终一致性同步机制在上一个问题中已经提到,可能会因为中间件或者网络故障导致消息不同步。这时候可以再增加一个补偿机制,通过定时任务扫描未完成的订单,主动查看支付状态,然后再去会员商户履行合同。这是一种自下而上的策略,可以保证数据的最终一致性。⑤业务沉淀从接到项目到现在也是从一无所知逐渐加深对订单系统理解的过程,同时也对当前交易的业务和业务结构有了了解。交易系统作为支付系统的上层系统,本身提供商品管理能力、交易回单能力、合同履约核销能力。周边业务子系统主要针对业务内容资源的管理。业务的收款和绩效管理只需接入交易系统即可,降低业务开发的复杂度。收货流程如下:商家自定义商品详情页,然后通过详情页底部的调用终端能力进入订单收银台。这里客户端需要调用业务后台接口获取商品详情,然后调用交易底部栏的展示接口获取底部按钮。用户通过底部按钮进入收银机后,可在收银台选择支付方式和优惠券,点击确认支付即可转账微信或支付宝支付。收银机显示和获取支付参数的接口由交易系统提供。后台确认收到订单后,通知商家履行合同,客户端返回详情页,用户进入内容播放页面享受权益。通过调用业务后台与交易系统后台的接口完成合约履约核销流程。现在知乎主要从事虚拟商品交易。一个大致的交易流程如下图所示:用户从浏览商品到进入收银台下单支付,再回到内容页面消费内容。随着业务的发展,不同的交易场景和交易流程叠加,系统开始变得复杂,一个交易的业务结构逐渐浮出水面。订单系统主要承载知乎内外的各种交易服务,提供稳定可靠的交易场景支持。主要分为以下几个部分:第一,产品服务层,为用户可以感受到的交互界面,为这些页面提供统一的订单支付API网关。然后是订单服务层,由上层网关调用,提供不同场景下的交易服务支持。再往下是订单字段层,承载订单的核心逻辑代码。首先是用户购买所需的计算价格聚合,然后是管理订单模型的交易聚合,最后是购买产品后履行处理的发货聚合。底层是基础支撑服务层,主要提供基础服务支撑和交易所依赖的一些服务。最后是运营服务,提供交易相关的后台功能支持。⑥方法实践归根结底,无论是系统迁移方案,还是对架构的理解,都离不开参与者的理解和认知。一个优秀的方案或者合适的架构不是设计出来的,而是迭代出来的。人类的认知也是如此,需要不断的迭代升级。与许多方法论一样,PDCA循环为我们提供了改进路径:计划:明确我们的迁移目标,调查现状并指定计划。执行:实现计划。检查:总结分析哪些地方做得好,哪些地方做得不好。行动调整:总结经验教训,在下一个周期解决。很多时候,也许你只做了前两步,但后两步对你的提升会有很大的帮助。因此,一个项目的回顾和CodeReview是非常重要的。只有语言的交流与碰撞,才能更容易打破固有思维,提升商业认知。作者:张旭,知乎后端开发工程师,主要负责知乎业务相关系统的研发,专注于电子商务交易营销领域。编辑:陶佳龙来源:转载自公众号高可用架构(ID:ArchNotes)
