高并发情形下 如何保证无效的情形各个用户得到的数

  这个配置参考于cache服务器varnish的推薦配置和SunOne 服务器系统优化的推荐配置

  varnish调优推荐配置的地址为:

  不过varnish推荐的配置是有问题的,实际运行表明"net.ipv4.tcp_fin_timeout = 3"的配置会导致页面经瑺打不开;并且当网友使用的是IE6浏览器时访问网站一段时间后,所有网页都会

  打不开重启浏览器后正常。可能是国外的网速快吧峩们国情决定需要调整"net.ipv4.tcp_fin_timeout = 10",在10s的情况下,一切正常(实际运行结论)

  修改完毕后,执行:

  命令生效为了保险起见,也可以reboot系统

  linux系统优化完网络必须调高系统允许打开的文件数才能支持大的并发,默认1024是远远不够的


原文出自【比特网】,转载请保留原文链接:

}

好吧最近我特么是跟高并发杠仩了。

单例模式想必很很常见,而往往单例模式跟static相关单例模式的初衷是为了在任何条件下我只得到一个实例,包括类和变量而往往需要我们用static关键字去修饰达到单例的效果。最近高并发接触得比较多使用缓存就需要用单例。因为你针对某一个key的缓存只可能定义成“一份”所以缓存类的实例需要用到单例模式。但是在高并发的条件下控制不好的话,很容易出问题下面写个小例子,就能看出是什么问题了……

而我们再模拟使用到单例模式的情形:

看看TestAction中定义的线程TestThread该线程被启动了2次,(模拟并发)并且2次都是传入同一个参數(模拟相同条件)"count"。

浏览器输入TestAction注解的Url可发现控制台打印如下:

再次输入该Url,打印如下:

问题已很明显了线程第一次执行时,集合夲来为[a.b.c]被它修改(add("d"))之后集合被覆盖为[a,b,c,d]了;同理,第二次输入Url之后集合又被线程第二次执行时覆盖为[a,b,c,d,d]了·,所以此次在进行add("d")操作之后,集匼被覆盖为[a,b,c,d,d,d]啦以此类推……

其实这种问题是比较容易被忽视的,并发条件下你对一个“公共”的变量(一般是由static修饰),常见场景如缓存嘚操作(这里是add("d"))修改会不断更新【最初】的变量值,【新】的线程再次访问时得到的已经不是【最初】的值了。这显然是不对的峩们需要做到对一个公共变量进行多线程访问时,线程与线程之间的访问不彼此影响即:线程不会修改公共的变量值,不影响其他线程嘚访问

注意:需要注意这种情况只涉及到线程需要对拿到的公共变量修改时,纯读取的话没必要注意这个问题。

如何解决呢我们只需拷贝一个公共变量的“副本”,即可达到想要的效果:

改变Test3的方法如下:

 copyList是公共变量的副本这样,当有N个线程去访问公共变量时得箌的是副本,你之后再对该副本进行任何操作都不会影响公共变量,从而不影响其他线程对该公共变量的访问确保其他线程拿到的都昰【最初】的公共变量。

}

2015年5月开始接触订单系统的研发7朤负责订单研发组;度过单体应用到服务化这个阶段。
2016年初搭建订单的测试团队订单拆分为正逆向后,主要负责正向和交付部分
2017年做叻一些平台搭建的探索。
2018年初负责整个订单正逆向和交付年中将下单、购物车部分一起归并,年底和商户订单部分整合形成交易中台。
2019年10月从交易中台转出近期做了一小段时间的组织效能和架构。

我为什么会写这篇文章究其缘由:

一是自己在交易域做了 4 年,有很多呮有我才知道才能串起来的故事,想把这些记录并保留下来

二是发现后边的很多同学看交易体系时,一接触就是分布式、SOA、每日百万、千万数据量只知道它是这个样子,很难理解背后的思考和缘由伴随自己这几年的经验,想让大家能够更容易的理解这个演化过程的原因和历程有甘有苦。

三是很多总结也好方法论也好,更多是去除了“糟粕”呈现在大家面前这里可能会稍微加一点“毒鸡汤”,現实不一定那么美好我们有很多抉择,现在回过头来看也许是庆幸,也许是错误

这篇文章希望通过一些发展的故事和思考来给读者呈现整个历程,大家可以看到非常多野蛮生长的痕迹并会附带一些思考和总结,但不会像快餐式的总结很多大道理

那我们就从2012年的太古时期讲起。

在谈订单之前我们往前再考古考古,在太古时代有一套使用 Python 写的系统,叫做 Zeus 的系统这个 Zeus 包含了当时饿了么最核心的几夶模块,比如订单、用户、餐厅这些统统在一个代码库中,并且部署在同一台机器 Zeus 之外还有两大核心,即饿了么 PC 也就是很多老人常提的「主站」,以及面向商户的 NaposPC 这些系统通过 Thrif 协议通信。除开这条链路之外所有杂乱的内部功能,全在一个叫 walle 的系统中这个 Walle 系统是采用 PHP 写的。

那么当时的 Zeus 大概长这个样子:

的简称,这个名词沿用到了今天成为交易正向的订单部分,甚至一段时间是订单组的代名词

Zeus 在后来其实经过了一定的重构,叫做 Zeus2 但具体时间已不可考。

2014 年 10 月我到饿了么来面试面试官是商户端负责人磊哥。 12 月 1 日我入职饿了麼, HR 领着带着一脸萌新的我到磊哥面前时,磊哥把我带到 JN 面前说“这就是那个实习生”,然后扭头就跑了后来得知,当时面试结束後磊哥和 JN 同学说,刚刚面了一个实习生凑合能用,正巧商户组有计划转型 Java 而佳宁还很缺 python 的人,然后就骗了 JN 一顿饭把我卖了

回到正題,在 2014 年 12 月~ 2014 年 4 月这几个月的时间里我配合完成了一个更老的 BD 系统后端迁移到 Walis ,并且在我的导师转岗到 CI 团队后自己完成了 Walis 从单应用迁移箌分布式应用。

对我来说完全是运气和缘分...

接近 2015 年 5 月的时候,我的主管JN同学,有一天突然找到我看起来很兴奋,告诉我公司打算荿立一个订单组,这个订单组由他来负责除了他之外,他唯独选中了我(大概是因为上段我提到的一些经历在可选的人里,还凑合~)说昰我怎么怎么让他相中,这个男人忽悠起人来一套一套的。

作为一个技术人员内心非常沸腾。一是高并发、高流量、分布式这些耳熟能详的高大上名词之前只是听说过不曾想这么快就能够接触到这样的系统;二是我们此前做的系统很“边缘”,有多边缘呢白天几乎沒什么请求, BD 走访商户回来恰巧晚上才是高峰期,即使是晚上关键的单接口也就偶尔几个、十几个请求,是当时那种挂 2 个小时才可能囿人发现挂半天不一定有人叫的系统,那时候我们幸福的晚上 7 点前就下班了第一次发布的时候非常郑重的和我说,可能要加班到晚上 8 點半

之所以选择 JN 做订单组负责人,因为他虽然是个前端工程师起家做的是“边缘”后台系统,但却是对整个公司所有系统和业务都比較熟悉的人很适合发展订单系统。

嗯没错,这个组在成立前一天一直只有我们两个人。当时的我还没毕业除了兴奋,更多的是忐忑

2015 年 5 月 12 日,订单组正式成立成立当天,拉来了隔壁组的 ZH (是个PHPer招进来的时候是计划去接Walle),然后聊到一半的时候当时的部门总监跑过來,说正巧有个小哥哥当天入职还不错,正好给订单组吧是个 Java 工程师。于是乎成立当天,我们人数翻了一倍变成了 4 个人。

我们给洎己的第一个任务: 读代码理业务,画图和 CTO 申请到了 1 个月的时间来缓冲,这段时间不接任何业务需求!

分别请来了订单的前主程、Python 框架負责人、Zeus 系应用运维负责人给我们讲解实际上,每个人的分享也就 1 个多小时那一个月真是从几万行 Python 代码,没有任何产品文档极其稀尐的注释,一行行的啃每个人解读一部分。我最后汇总把整个订单的生命周期、关键操作、关键业务逻辑画在了一张大图里,这张图我们后来用了一年多。

其实当时年中旬的饿了么,产研规模已经达到几百人左右新 CTO ,雪峰老师是年初加入饿了么整个基础设施的起步是 2015 年下半年,整个体系的飞速搭建是在 2016 年

可以说是正处于相当混乱,又高速发展的时期我们称那个时间是一边开着跑车一边换轮胎。

和订单真正密切相关的第一个 Super 任务大概是从 6 月左右开始 --- Zeus 解耦,HC老师是 Python 框架的负责人也是个人最佩服和敬仰的技术专家之一,在美國举行 Qcon 上作为首席架构师介绍过当时饿了么整体技术架构。刚才在太古时期已经说到 Zeus 是一个巨型单体应用,为了今后各个部分能够快速发展降低耦合和牵连影响等,公司启动了 zeus 解耦项目总之就两个字,拆分

经过 1 个多月的密集会议,完成了拆分的方案说的似乎没那么难,但是这场口水战当时打的不可开交拆分后不同的服务归属于谁?模块和模块之间并没有切分的那么干净A和B服务中的边界怎么萣等等一系列问题。当时的我还不够格参与讨论

结论是, Zeus 将要拆分成下边的几个主服务:

每个被拆分后的服务随之进行的是新的一波重構和拆分。例如从 zeus.eos 分离出来 biz.booking 拿走了下单和购物车部分能力;分离出来 biz.ugc 拿走了订单评价相关能力。

拆分主要经历的几个阶段:
1、(7月份)共享代碼仓库按模块独立运行。即把 Zeus 所有代码都打包到服务器后,按照划分在特定机器上只将特定模块单独启动,开放特定端口
2、(8月份) Proxy 階段。即在原服务中要迁出去的接口上增加一个代理,可以代理到新服务的接口由服务注册中心开关能力来控制切换流量大小。
3、(8月份至9月初)脚本、模块的完全切分改造
4、(9月份)代码仓库独立。使用了 Git 的核弹武器 filter-branch 将模块中的代码和变更历史,完全完整的从原代码库中汾离而此时部署却仍然为混布,在发布工具中某个独立应用发布后实际是替换了 Zeus 这个大项目下的某个目录。
5、(9月份)配置独立原来的配置由 saltstack 刷到服务器上,被服务器上多个应用所共用我们将其直接改成使用服务注册中心的配置下发能力获取单个应用配置。在这个阶段吔基本上过渡到了软负载
6、(次年3月份)物理部署独立。当然这是解耦二期的内容了

当然,这次拆分还带来了另外一个产物, Python 的 SOA 框架 zeus_corezeus_core 偠大概在 4 月份左右先于业务服务被拆分出来。

整个解耦一期持续了大概半年时间。在期间没有发生因为拆分导致的事故,也几乎没有什么冒烟想想当时没有用什么高深的东西,工具落后没有专职测试,完全靠着一帮早期工程师和运维同学的技术素养

仍然是在 2015 年,夶概是 9、10 月左右确定分库分表要开始实施而分库分表的方案,在我介入时已经几乎敲定并由 CI 部门的 DAL 团队主导。

一是扛不住并发当时峩们的订单库的 MySQL 是采取 1 主 5 从的架构,还有 1 台做 MHA DB 不太能承受住当时的并发压力,并且对风险的抵抗能力非常的弱。业务如果做一些活动沒提前告知我们的从库一旦挂了一个,就只能来回切严重的时候只能大量限流。而且那段时间,作为技术我们也在祈祷美团外卖別在高峰期挂,美团外卖一旦挂了流量就会有一部分流到饿了么,我们就开始也紧张起来了同样的,那段时间我们整站挂了,美团外卖也不太能扛得住大家都在经历相似的发展阶段。

二是 DDL 成本太高业务又处于战斗高峰。当时饿了么的单量在日均百万出头有一些業务需求,希望在订单上新增字段然而,我们找到 DBA 评估的时候给的答案是,乐观估计需要停服 3 小时悲观估计要 5 小时,并且需要 CEO 审批显然,这个风险技术团队难以接受,而业务团队也无法接受那么投机取巧的方案,就是在预留的 Json 扩展字段中不断的塞这种方式一萣程度上缓解了很长一段时间的压力,然而也埋下了非常多的隐患。

当然还有一些特殊的业务场景以及一些开放出去颗粒度很大的接ロ,会产生一些性能极差的 SQL 都会引爆全站。

一次更新操作逻辑如下:

我们其实是做了两维 Sharding 两个维度都是 120 个分片,但是可以通过三种方式蕗由(用户 ID、商户ID、订单ID)写入优先保证无效的情形用户维度成功。由于资源的原因用户和商户分片是交错混合部署的。

(加粗部分其实是囿一些坑的这个特殊定制也是饿了么唯一,如果有兴趣以后可以展开)

更具体分库分表的技术细节不在这里展开大致经历了几个阶段:

1、淛定新的订单号生成规则,并完成改造接入
2、数据双写,读旧对比数据。
3、对不兼容的 SQL 进行改造比如跨分片的排序、统计,不带shardingkey的SQL等等
4、数据双写,读新(与3有部分同步进行)
5、完成数据库切换,数据写新读新

这段日子,作为业务团队大部分时间其实花在第三部汾,也曾奋斗过好几次到凌晨3、4点

在 2016 年的春节前夕,为了顶过业务峰值和系统稳定我们甚至把 DB 里的数据做归档只留最近 15 天内的订单

记嘚最终切换的那一天,大概在 2016 年 3 月中旬我和几位同学早上 5 点多就到了公司,天蒙蒙亮整个饿了么开始停服,然后阻断写请求完成 DB 指姠的配置,核对无误恢复写请求,核验业务无误慢慢放开前端流量,重新开服整个过程核心部分大概 10 分钟,整个停服到完全开放持續了半个小时

到了第二天,我们才得以导入最近 3 个月的历史订单

这次变更做完,我们基本摆脱了 DB 的瓶颈和痛点(当然后边的故事告诉峩们,有时候还是有点天真的~~~)

那个时期也是在 15 年的 7 月左右,受到一些架构文章的影响也是因为 JN 提到了这一点,我们决定做订单的消息廣播主要目的是为了进一步解耦。

在调研了 RabbitMQ、NSQ、RocketMQ、Kafka、ActiveMQ 之后我得出的最终结论,选型还是 RabbitMQ 其实当时我认为,RocketMQ 更为适合特别是顺序消息的特性,在交易某些业务场景下能够提供天然的支持然而,运维团队主要的运维经验是在 RabbitMQ 框架团队和运维团队的同学很自信,自从搭建以来也没有出过任何问题,稳的一匹如果选择 RabbitMQ ,就能够得到运维团队的天然支持这对于我们当时的业务团队来说,能够避免很哆风险

于是由框架团队承接了对 RabbitMQ 进行一轮严谨的性能测试,给出部分性能指标这一场测试,最终搭建了一个 3Broker 组成的集群单独为订单垺务,在此之前只有一个 MQ 节点服务于 Zeus 体系的异步消息任务。

为了保证无效的情形对交易主流程不产生影响然后在 Client 端 SOA 框架进行了一系列嘚容错改造,主要是针对连接 MQ 集群时的发送超时、断开等容错消息发送异步进行且重试一定次数。最终全新搭建了由 3 个节点组成的 MQ 集群订单的消息最终发往这个集群。

期间其实踩了一个小坑。虽然框架团队已经进行了异常情况的容错但毕竟消息广播的发送时机是和主流程状态扭转紧密相连的,代码在上线前当时一向谨慎的我,为首次上线加上了一个消息发送的开关那是一个晚上,大概 8 点多现茬回想,当时灰度和观察时间是有一些短的当我全部发布完成后,很快监控上显著看到接口开始严重超时(我们当时采用框架默认的超時设定, 30s其实这个配置很严重),进而产生了大量接口严重超时很明显,有什么拖慢了接口交易曲线断崖式的下降,我立马就被NOC 进行叻 on call 迅速将消息发送的开关关闭,恢复也是一瞬间的事情然后,人肉跑到架构团队前边跪求协助排查原因(终归还是当时的自己太菜)

当晚,我们开、关、开、关、开、关...流量从 5% 、10% 、30% 等等不同尝试、验证之后,最后得出的结论是和当时的 HAProxy 配置有关,由于 HAProxy 提前关闭了和 RabbitMQ 集群的连接服务的 Client 仍然拿着坏死的连接去请求,进而造成了这次问题并且, Client 确实没对这种超时进行容错在调整了 HAProxy 的链接超时配置之后,症状就消除了虽然,从日志上看遗留有一些隐患

此时,是长这样的每个接入的业务方需要申请一个 Topic , Topic 之下挂多少 Queue 可以根据业务需求自己确定

这个物理架构部署稳定运行了不到1年时间就存在不少问题,下章会再展开

在使用上,当时定下了这么几条原则:
1、订单不对外直接暴露自身状态而是以事件的方式对外暴露。因为状态是一个描述而事件则代表了一个动作,同时可以将订单状态细节和接入方解耦
2、消息广播仅用于广播事件,而不用于数据同步如消费者需要更多的数据则反查订单数据接口,时间戳包含事件产生时间和发送時间(时间是后来加上的)即消息体包括 header 信息,仅放入用于解释这个事件的内容还包括交易双方主键和一些能够用于做通用过滤或二次路甴的信息。
3、消费者在消费消息时应当保证无效的情形自身的幂等性同时应当让自己在消费时无状态。如果一定要顺序消费那么自行通过Redis等方案实现。
4、消费者接入时 Topic 和 Queue 需要按照一定命名规范,同时 Queue 的最大积压深度为 10k ,超过则舍弃消费者要明确自身是否接受消息鈳损,同时要保证无效的情形自身的消费性能按照当时评估,消息堆积到达百万时会使得整个集群性能下降 10% (在全局架构的建议下,我們还提供了以 Redis 为介质作为镜像存储了订单事件,不过体验并不够优雅)

而这套消息广播的逻辑架构一直持续使用到今天,在解耦上产生叻巨大的红利

15 年中旬到 16 年初,我们处在每天的单量在百万以上并逐步快速增长这么一个阶段

在那个时期,也看了很多架构文章ESB、SOA、微服务、CQRS、EventSource 等等,我们也在积极探讨订单系统如何重构以支撑更高的并发。当时听的最多的是京东的 OFC ,还特地买了《京东技术解密》茬研读不过很快得出结论,几乎无太大参考价值主要原因是京东的 OFC ,很明显是由零售业务的特性决定的很多 OFC 里的概念,作为入行尚淺的我们套到餐饮 O2O ,几乎难以理解但我们还是深受其影响,给小组取了一个相似的缩写OSC,Order Service Center

由于手头上这套订单已经服役了 3 年多,公司的主要语言栈从人数上也由 Python 倾向到 Java 没多久,我们打算重写这套订单体系于是,我设计了一套架构体系以 osc 为应用的域前缀。这套體系的核心理念: 订单是为了保持交易时刻的快照尽可能的保持自己的简洁,减少对各方的依赖减轻作为数据通道的作用。

我们选取的語言栈选型是 Java 也就是计划开始转型 Java 。(很不巧我们真正转型到 Java 最后发生在 2019 年).

此时,正值 9 月很巧的是,公司开始第一次开始设立新服务嘚架构评审制度我这个方案,大概就是参与评审的 Top1、2 小白鼠新鲜的大锤正等着敲人。

其实在那之后的1年回过头来看,还挺感谢这次架构评审不是因为通过了,而是因为被拒绝了

说来也好笑,那一次依稀记得参与架构评审的评委成员, DA 负责人、基础 OPS 负责人、入职沒多久的一个架构师

架构师当时的提问关注点在这套架构是能够用1年还是3年,而基础OPS负责人的提问特别有意思,他问了第一个问题這套系统是关键路径吗?我心想这不是废话吗,我直接回答最中间那部分是的。

然后第二个问题出了问题,这个应用可以降级吗峩一想,这不也是废话吗这个链路当然没法降级,这是最核心最基础的链路公司的核心业务就是围绕交易。(可能是双方的理解不在一個频道上)

于是,他给的结论是关键路径,又是核心的订单没法降级,一旦出了问题大家都没饭吃。于是评审结束结论是不通过。

交易团队一直没有专职的测试也就是说,所有的内容都是由研发自测来保证无效的情形的。而公司当时的自动化测试非常的弱几乎所有的测试都是依靠手工进行。但是我此时觉得非常有必要拿到测试资源。我强烈的要求成立一个测试小组来给订单上线质量加上一層防护

当时还发生了一些有趣的事情,据 JN 去了解框架团队是没有测试的,然而他们似乎没出什么问题当时他们很自豪的解释,技术憑什么不应该自己保障代码的质量简直理直气壮,无懈可击我觉得这个观点有一些理想,研发自己可能没那么容易发现自己的错误引入另外一批人从另外一个角度切入,能够进一步提升质量的保障毕竟这个系统是如此的重要和高风险,但是我们也并不应该建立一个呮能提供“点点点”的测试团队

最后,在和 JN 长时间的沟通后我们确定了当时测试小组的定位和职责: 保证无效的情形代码质量是研发自巳应尽的责任,测试开发在此基础上主要提供工具支持,让测试成本降低同时在精力允许的情况,提供一定程度的测试保障

于是,茬 2016 年 2、3 月左右交易团队来了第一位测试,差不多在 4 月的时候测试 HC 达到了 4 人,整个测试小组由我来负责

第一件事情,搭建自动化集成測试

技术栈上的选择,采用了 RobotFramework 主要原因是整个团队当时仍然以 Python 为主要语言,测试开发同学实际上 Python 和 Java 也都能写;另外一点是 RobotFramwork 的关键字驱動有一套自己的规范,和系统相关的lib可以被提炼出来即使做语言栈转型时,成本也不会很高

除了测试的流程规范和标准外,开始想搭建一个平台用于管理测试用例、执行情况和执行报告。

这套体系我命名为 WeBot :

  • Jenkins 来实际调配在何处执行并且满足执行计划的管理
  • 基于 Django 搭建了一个简单的管理界面,用来管理用例和测试报告并使得每一个测试用例可以被作为一个单元随意组装,如果对 Java - - 很熟悉的同学这里莋一个近似的类比,这里每一个用例都可以当成一个 SPI
  • 另外引入了 Docker 来部署 slave 的环境,用的很浅虽然当时饿了么在生产还没使用 Docker (饿了么生产仩的容器化应该在 17 年左右)。

想想自己当时在测试环境玩的还是蛮欢乐的很喜欢折腾。

测试单元: Bussiness Library 其实是对 SOA 服务接口到 RobotFramwork 中的一层封装每一個测试单元可以调用一个或多个接口完成一次原子的业务活动。

校验组件: 提供了对返回值或者额外配置对Redis、数据库数据的校验。

集成测試: 多个测试单元串行编排起来就完成了一个集成测试用例其中每个测试单元执行后,请求的入参和出餐在集成测试用例的运行域内任哬地方都是可以获取到的。

回归测试: 选取多个集成测试可以当成一个方案,配置执行

这样就实现了多层级不同粒度的复用。根据集成測试和回归测试的方案搭配后台会编译生成对应的 Robot 文件。

这个项目最后其实失败了。最主要的原因测试开发的同学在开发上能力还鈈足,而界面上需要比较多的前端开发工作一开始我直接套用了 Django 的扩展管理界面 xadmin ,进行了简单的扩展然而当时的精力,不允许自己花呔多精力在上边内置的前端组件在体验上有一些硬伤,反而导致效率不高直到 5 月份,基本放弃了二次开发

但这次尝试也带来了另外嘚一些成果。我们相当于舍弃了使用系统管理用例而 Jenkins + RobotFramwork 的组合被保留了下来。我们把写好的一些集成测试用例托管在 Git 上研发会把自己开發好的分支部署在指定环境,每天凌晨拉取执行研发会在早上根据自动化测试报告来看最近一次要发布的内容是否有问题。同时也允許研发手动执行,文武和晓东两位同学在这块贡献了非常多的精力

这个自动化集成回归的建立,为后续几次订单系统的拆分和小范围重構提供了重要的保障让研发胆子更大,步子能够迈得更长了研发自己会非常积极的使用这套工具,尝到了很多显而易见的甜头

第二件事情,搭建性能测试

记得在 15 年刚刚接触订单的时候,有幸拜访了还没来饿了么但后来成为饿了么全局架构负责人的 XL 老师,谈及如何莋好订单系统重点提及的一点,也是压测

当时有一些问题和性能、容量有一些关系,我们没有什么提前预知的能力比如,在我们完荿 sharding 前有一次商户端上线了一次订单列表改版因为使用了现有的一个通用接口(这个接口粒度很粗,条件组合自由度很强)我们都没能预先評估,这个查询走了一个性能极差的索引当时午高峰接近,一个几 k QPS 的查询接口从库突然( 15 年我们的监控告警体系还没有那么完备)就被打垮了,从库切一个挂一个不得不采取接口无差别限流 50% 才缓过来,整个持续了接近半个小时最后追溯到近期变更,商户端回滚了这次变哽才真的恢复而事后排查,造成此次事故的慢 SQL QPS 大概几百左右。

整个公司的性能测试组建早于我这边的规划,但是当时公司的性能测試是为了 517 外卖节服务的有一波专门的测试同学,这是饿了么第一次造节这件事的筹备和实施其实花了很长时间。

在压测的时候需要不斷的解决问题重复再压测,这件事使得当时很多同学见到了近铁城市广场每一个小时的样子回忆那段时光,我记得最晚的一次大概昰 5 月 6 号,我们到楼下已经是凌晨 5 点半我到家的时候两旁的路灯刚刚关。

上边是一点题外话虽然全链路压测一定会带上我们,但是我们吔有一些全链路压不到的地方还有一些接口或逻辑需要单独进行,需要随时进行

技术选型上选择了 Locust ,因为 Python 的 SOA 框架及其组件可以带来極大的便利。此前在做公司级的全链路压测时是基于 JMeter 的, JMeter 并不是很容易和 Java 的 SOA 框架进行集成需要有一个前端 HaProxy 来做流量的分流,不能直接使用软负载这在当时造成了一定的不便性。另外一个原因 Locust 的设计理念,可以使一些性能测试的用例更为贴近业务实际场景只观测 QPS 指標,有时候会有一些失真

有了全链路性能测试团队在前边趟坑,其实我自己性能测试能力的搭建很快就完成了整个搭建过程花费了 1 个哆月, 8、9 月基本可以对域内服务自行组织性能测试性能测试人员包括研发的学习,需要一点过程很快,我们这个小组的性能测试就铺開到整个部门内使用包括之后和金融团队合并之后。

这次搭建使得我们在对外提供接口时对自己服务负载和性能上限有一定的预期,規避了一些有性能隐患的接口上线特别是面向商户端复杂查询条件;也能够模拟高并发场景,在我们一些重构的阶段提前发现了一些並发锁和调用链路依赖问题。

第三件事情随机故障演练。

一开始的雏形其实很简单大致的思路是:

1、 在测试环境单拉出一个专门的环境,有单独的监控和 DB
2、构造一个 Client ,模拟用户行为造数(我们自动化集成测试积累的经验就排上用场了。
3、提供了一个工具来构建被依赖垺务的 Mock Server 解决长链路服务依赖问题。Mock Server 可以根据输入返回一些设定好的输出
4、另外,框架团队帮忙做了一些手脚发了一个特殊版本,使嘚我们可以对流量打标可以根据 Client 对流量的标记,来让 Mock Server 模拟阻塞、超时等一些异常行为反馈到我们的被测 server 上。

这是一个很简单的雏形洏订单经过我们的几次治理,对外依赖已经很少所以不到 2、3 天就完全成型。但仅仅是玩具而已并不具备足够的参考意义。因为并发没囿做的很高 Mock Server 能够做的事情也有限。

在专项同学和运维同学的帮助下Kennel 在 2016 年的 10 月左右初步可用。这个工具提供了诸如: 模拟网络丢包;接口異常注入;摘除集群中的某节点;暴力干掉服务进程等等

这东西大家之前都没尝试过,我们也不知道能够测出什么来我在11月的时候想莋第一波尝试,我尝试制定了 5 个需要验收的场景:
2、某个接口异常引起整个服务雪崩
3、集群中某个节点重启或者机器重启调用方反应明显
4、集群某个节点CPU负载变高,负载不均
5、服务是单点的集群行为不一致

根据这几个场景,在测试同学中挑选一个人牵头实施不同服务的測试报告略有差异,其中一份的部分截图如下:


通过对交易主要的几个服务测试一轮之后我们确实发现了一些隐患:

  • 一些情况下部署的集群囷服务注册中心机器数量可能不一致,即服务节点被暴力干掉后服务注册中心不能主动发现和踢出。这是一个比较大的隐患
  • 每个集群嘟存在负载不均的现象,个别机器可能 CPU 利用率会偏高(和负载均衡策略有关)
  • 进行“毁灭打击”自恢复时,某几个节点的 CPU 利用率会显著高于其他节点几个小时之后才会逐渐均匀。(和负载均衡策略有关)
  • 单节点 CPU 负载较高时负载均衡不会将流量路由到其它节点,即使这部分请求性能远差于其它节点甚至出现很多超时。(和负载均衡、熔断的实现机制有关Python 的 SOA 是在服务端做的熔断,而客户端没有)
  • 大量服务的超时设置配置有误框架支持配置软超时和硬超时,软超时只告警不阻断然而默认的硬超时长达 20s 之久,很多服务只配置了- 软超时甚至没有配置这其实是一个低级错误埋下的严重隐患,可能会没法避免一些雪崩
  • 个别场景下超时配置失效,通过对调用链路的埋点以及和框架团隊复现,最后锁定是一些使用消息队列发送消息的场景Python 框架是利用了Gevent 来实现高并发的支持,框架没能抓住这个超时

这个项目,几个道悝显而易见我们做了很多设计和防范,都必须结合故障演练来进行验收无论是低级错误还是设计不足,能够一定程度提前发现

当然峩们也造成了一些失误,一条信心满满的补偿链路(平时不work)自己攻击的时候,它失效了后来发现是某次变更埋下的隐患。自己亲手造的鍋含着泪也要往身上背,但我反而更觉得故障演练是更值得去做的谁能保证无效的情形真正的故障来临时,不是一个更严重的事故

除了系统利好外,人员也拿到了很多收益比如测试和研发同学经过这个项目的实时,对我们的 trace 和 log 系统在使用上炉火纯青对我们 SOA 框架的運作了解也更为透彻,这里的很多隐患和根因就是测试同学刨根挖底找到的。高水准的 QA 同学很重要提升 QA 同学的水平也同样重要。

当然除了测试团队的工作外,单元测试我们也没有落下在 16 年长时间保持 80%~90% 的一个代码行覆盖率。

伴随体量上涨的一系列问题

2016 年年初主要瓶颈茬数据库在上文其实已经提到了分库分表的事,可以稍微喘口气到了 6 月,大家最担忧的变成了 Redis 。当时 Zabbix 只能监控到机器的运行情况 Zabbix 其实也在逐步下线中, SRE 团队搭建了一套时效更高的机器指标收集体系直接读取了 Linux 的一些数据,然而整个 Redis 运行情况仍然完全是黑盒。

饿叻么在 twemproxy 和 codis 上也踩了不少坑 redis-cluster 在业界还没被大规模使用,于是自研了一套 Redis proxy: corvus 还提供了强大指标上报,可以监控到 redis 的内存、链接、 hit 率、key 数量、傳输数据量等等正好在这个时间点推出,用以取代 twemproxy 这使得 Redis 的治理迎来转机。

我们配合进行了这次迁移还真是不迁不知道,一迁吓一跳

当时我们使用 Reids 主要有三个用途,一是缓存类似表和接口纬度;二是分布式锁,部分场景用来防并发写;三是餐厅流水号的生成代碼已经是好几年前的前人写的。

老的使用姿势把表级缓存和接口缓存,配置在一个集群中;其余配置在另外一个集群但是在使用上,框架包装了两种 Client 有不同的容错机制(即是否强依赖或可击穿)。

大家都知道外卖交易有个特点一笔订单在短时间内,交易阶段的推进会更赽因此订单缓存的更新更频繁,我们在短暂灰度验证 Redis 集群的可用性之后就进行了全面切换(当时的具体切换方案细节记不太清了,现在囙想起来其实可以有更稳妥的方案)

参照原缓存的集群是 55G , OPS 准备了一个 100G 的集群在切换后 10min 左右,集群内存就占满了

我们得出一个惊人的結论...旧集群的 55G ,之前就一直是超的(巧了配合我们迁移的OPS也叫超哥)。

从监控指标上看keys 增长很快而ttl下降也很快,我们很快锁定了两个接口 query_order 和 count_order ,当时这两个接口高峰期前者大概是 7k QPS 后者是10k QPS ,这两个接口之前的rt上看一点问题也没有平均也就 10ms 。

还得从我们的业务场景说起这兩个接口的主要作用是查询一段时间内某家餐厅的订单,为了保证无效的情形商家能够尽快的看到新订单商户端是采取了轮询刷新的机淛,而这个问题主要出在查询参数上这两个接口使用了接口级缓存,所谓的接口级缓存就是把入参生成个 Hash 作为 key ,把返回值作为 value cache 起来, ttl 为秒级咋一看没什么问题。如果查询参数的时间戳截止时间是当天最后一秒的话,确实是的看到这我相信很多人已经猜到,截止時间戳传入的其实是当前时刻这是一个滑动的时间,也就引发了 cache 接近 100% miss 的同时高频的塞入了新的数据。

(因为新旧集群的内存回收策略不┅样新集群在这种情况下,频繁 GC 会引发性能指标抖动剧烈)

这两个 cache 其实没任何用处...回滚过了一天后,经过灰度全面去掉了这两个接口嘚 cache ,我们又进行了一次切换顺带将接口级缓存和表级缓存拆分到两个集群。

接着我们又发现了一些有趣的事情...

先来看看,我们业务单量峰值的大致曲线对外卖行业来说,一天有两个峰值中午和傍晚,中午要显著高于傍晚

切换后那天的下午大概 3 点多,内存再次爆了... 内存占用曲线近似下图:

紧急扩容后,我们一直观察到了晚上最后的曲线变成了下图,从 hit 率上看也有一定提升(具体数据已不可考,茬 88%~95% 之间后来达到 98% 以上)。

为什么和业务峰值不太一样...

其实还是要结合业务来说很简单,商户端当时的轮询有多个场景最长是查询最近 3 忝内的订单,还有一个页面单独查询当天订单

后端在轮询时查了比前端每页需要的更多条目,并且并不是每个商户当天订单一开始就昰大于一页的,因此随着当天时间的推移,出现了上边的现象

为什么以前的性能指标又没看出什么问题呢?一是和旧 Redis 集群的内存回收筞略选取有关二是 QPS 的量很高,如果只看平均响应时间差的指标被平均了, hit 率也被平均拉高了

嗯,解决了这个问题之后又又发现了噺的问题...

大概1、2点这个夜深人静的时候,被 oncall 叫起来监控发现内存使用急剧飙升。

我们锁定到一个调用量不太正常的接口上又是 query_order。前段ㄖ子清结算刚刚改造,就是在这种夜深人静的时候跑账当时我们的账期比较长(这个是由于订单可退天数的问题,下文还有地方会展开)这时候会拉取大量历史订单,导致占用了大量内存而我们的表级缓存时效是 12h ,如果不做清理对早高峰可能会产生一定的影响。后来峩们次日就提供了一个不走缓存的接口单独给到清结算。

这里核心的问题在于 我们服务化也就不到 1 年的时间,服务的治理还不能做到佷精细服务开放出去的接口,暴露在内网中谁都可以来调用,我们的接口协议也是公开的任何人都很容易知道查阅到接口,并且茬公司的老人路子都比较野(不需要对接,有啥要啥没有就自己加)。Git 仓库代码合并权限和发布权限早在 15 年底就回收管控了但那一刻 SOA 化还未完全,接口授权直到很后边才支持

Redis 的使用还是需要建立在深刻理解业务场景基础上,并且关注各类指标

缓存机制的改进 
我们当时的緩存机制是这样的:

1、有一条独立的链路来做缓存的更新,对原有服务入侵性较小
3、有 MQ 削峰同时还有一级 Redis,做了聚合进一步减小并发

茬很多场景,是一套蛮优秀的架构

1、用到了两级队列,链路较长

驱动我们改造的原因也源自一次小事故。

商户订单列表的查询其实根據的是订单状态来查获取到的订单应当是支付好了的。然而有一部分错误的判断逻辑放在了当时商户端接单后端,这个逻辑会判断订單上的流水号是否是0(默认值)如果是0推断出订单还未支付,就将订单过滤掉

在那次事故中,缓存更新组件跪了(并且没有人知道...虽然这个架构是框架的某些同学早期设计的但太稳定了以至于都被遗忘...)。由于缓存更新的不够及时拿到了过时的数据,表象就是商户看不到蔀分新订单,看到的时候已经被超时未接单自动取消的逻辑取消了,真是精彩的组合...

后边改造成下边的样子:

相比起来这个架构链路就減少了很多,而且实时性得到了保障但是为了不阻塞流程,进行了一定的容错这就必须增加一条监控补偿链路。这次改进之后我们竝马去除了对 ZeroMQ 在代码和配置上的依赖。

分库分表做完后我们对 MQ 没有什么信心,在接下来的几个月MQ 接连出了几次异常...真的是墨菲定律,遺憾的是我们只是感觉它要出事情而不知道它哪里会出事情

在之前的章节,我提到过曾经搭建了一套订单消息广播机制基于这套消息為契机,商户端针对高频轮询做了一个技术优化希望通过长连接,推拉结合减小轮询的压力。简单介绍一下这套方案商户端有一个後端服务,接收订单的消息广播如果有新订单(即刚刚扭转到完成支付商家可见的订单),会通过与端上的长连接推送触达到端上接着端仩会触发一次主动刷新,并发出触达声音提醒商户原先的轮询则增加时间间隔,降低频次

那么问题在哪? 有部分时候,蓝色这条线整體花费的时间居然比红色这条线更少,也就是说一部分比例的请求兜到外网溜一圈比内网数据库的主从同步还快。

商户端提出要轮主库禽兽啊,显然这个频次,想是不用想的不可能答应,毕竟之前轮询从库还打挂过由消费者在本地 hold 一段时间再消费,也不太友好畢竟有时候,快不一定是好事情那么我们能不能让它慢一点出来?

于是binding 的拓扑被我们改成了这样,前段粉红的这个 Queue 使用了 RabbitMQ 死进队列嘚特性(即消息设置一个过期时间,等过期时间到了就可以从队列中舍弃或挪到另外的地方):

眼前的问题解决了但也埋了坑,对 RabbitMQ 和架构设计稍有经验的同学应该很快意识到这里犯了什么错误。binding 关系这类 Meta 信息每一个 Broker 都会存储用于路由。然而消息的持久化却是在 Queue 中,而 queue 只会存在一个节点本来是集群,在这个时候拓扑中靠前的一部分变成了单点。

回到我一开始提到的 MQ 集群事故因为一些原因牵连,我们这個 MQ 集群某些节点跪了很不幸,包含这个粉红粉红的 Queue 于此同时,暴露了另外一个问题这个拓扑结构,不能自动化运维得依靠一定的囚工维护,重建新的节点 meta 信息需要从旧节点导出导入,但是会产生一定的冲突并且,早期我们的 Topic 和 Queue 的声明没有什么经验没有根据消費者实际的消费情况来分配 Queue ,使得部分节点过热权衡自动运维和相对的均衡之下,后边的做法实际是随机选择了一个节点来声明 Queue 。

之後我们做了两个改进一是拓扑结构支持在服务的配置文件中声明,随服务启动时自动到 MQ 中声明;二是由商户端后端服务接到新单消息來轮询时,对新单by单单独请求一次(有 cache如果 miss 会路由到主库)。

于是消息的拓扑结构变成了下边这样:

仍然是上边这个故事的上下文,我们回箌影响这次事故的原因根据我们对 RabbitMQ 集群的性能测试,这个吞吐应该能够承受然而 CPU 负载非常的高,还影响了生产者发送消息(触发了 RabbitMQ 的自保护机制)甚至挂掉。

经过架构师的努力下最后追溯到,这次事故的原因在于商户端使用的公共 SOA 框架中,消息队列的客户端是部门洎己独立封装的,这个客户端没有很好理解 RabbitMQ 的一些 Client 参数(例如 get 和 fetch 模式, fetch 下的 prefetch_count参数等)其实这个参数需要一定的计算才能得到合理值,否则即使机器还有 CPU 可用,消费能力也上不去

和订单的关系又是什么?答案是 混布这个集群通过 vhost 将不同业务的消息广播隔开,因此上边部署了订单、运单、商户端转接的消息等

在事故发生当天,运营技术部老大一声令下无论怎么腾挪机器,当天都必须搭建出一个独立消息广播集群给到订单运营技术部和我们,联合所有的消费方当天晚上,即搭建了一个7节点的集群将订单的消息广播从中单独拆出来。

(一年后这个集群也到了瓶颈,而且无法通过扩容解决主要原因,一是消费方没有使用RabbitMQ的特性来监听消息而是本地过滤,导致白白耗费一部分处理资源;二是随着集群规模的上升连接数达到了瓶颈。后者我们在生产者额外发了一份消息到新搭建的一个集群得到了┅定的缓解。真正解决还是在饿了么在 RabbitMQ 栽了这么多跟头,使用 Go 自研的 MaxQ 取代 RabbitMQ 之后)

PS: 如果时光倒流,当初的改进项里会提前加一个第三点,针对使用*这个通配符来订阅消息的都要求订阅方根据真实需要更改。这里腐化的原因主要还是把控和治理的力度不够,标准和最佳實践建议在最初的说明文档就有后续也提供了一些可供调整参数的计算公式,不能完全指望所有消费者都是老实人也不完全由技术运營来把控,服务提供方是需要

2015 年下旬到 2016 年上旬,饿了么的早餐业务虽然单量占比不高,但对当时技术架构冲击感是比较大的。

一开始外卖和早餐的交互是这样的:

我猜这时候一定会有小朋友有一堆问号...
1、早餐独立于餐饮完全搭建了一套新的体系(用户、店铺、订单、配送等等)。
2、因为支付没法独立搞而支付在2016年初之前,是耦合在用户系统里的并且,这套支付就是纯粹为外卖定制的

于是,作为「创噺」部门的「创新业务」为了快速试错,完全自己搭建了一套完整的电商雏形而为了使用支付,硬凑着“借”用了外卖的交易链路這个方案是早餐的研发同学和支付的研发同学确定并实施的,订单无感知的当了一把工具人

当初我知道的时候,就已经长这样了我是什么时候知道的,出锅的时候很真实。当时 PPE 和 PROD 没有完全隔离一次错误的操作导致 PROD 的异步任务被拉取到 PPE ,再经过一次转移最后没有 worker 消費导致订单被取消。

在 2016 年初业务方提过来一个需求,希望饿了么配送会员卡的售卖能够线上化此前是做了实体卡依靠骑手线下推销的方式。正好经过之前的架构评审,我们也需要一个流量较小的业务模式来实践我们新的架构设想,于是就有了我们这套虚拟商品售賣的订单系统。

我们抽象了一套最简单的状态模型:

1、天下所有的交易万变不离其宗,主要的节点是较为稳定的
2、C 端购买行为较为简單,而 B 端的交付则可能千变万化
3、越是核心的系统,越应该保持简单

上下游交互如上,商品的管理、营销、导购等都交给业务团队洎己,交易系统最核心的职责是提供一条通路和承载交易的数据

在数据上的设计,买卖双方、标的物、进行阶段这三个是当时我们认為较为必要的,当然现在我可以给出更为标准的模型,但是当时,我们真没想那么多

所以,交易主表拆成了两

一张基础表,包含主要买方ID、买方ID、状态码、业务类型、支付金额业务类型是用来区分不同买卖方体系的。

另一张成为扩展表包含标的物列表、营销信息列表、收货手机号等等,属于明细允许业务方有一定的自由空间。

(PS: 事后来看标的物、营销信息等等,虽然是可供上游自己把控的泹是需要对范式从代码层面进行约束,否则治理会比较麻烦业务方真是什么都敢塞...)

拆两张表,背后的原因一是订单一旦生成,快照的職责就几乎完成了剩下最关键的是状态维护,高频操作也集中在状态上那么让每条记录足够的小有助于保障核心流程;二是参照餐饮訂单的经验, 2/3 的存储空间是用在了明细上特别是几个 Json 字段。

整个虚拟订单系统搭建好之后很多平台售卖性质的业务都通过这套系统接叺,对我们自身来说接入成本开发+测试只需要 2~3 天以内,而整个业务上线一般一个星期以内就可以我们很开心,前台业务团队也很开心因为没有大规模查询的场景,很长一段时间稳定支持每日几十万的成单,几十核的资源绰绰有余

这其实是一个简单的平台化系统的雛形了。

围绕交易我们其实还衍生出一些业务,广义上当时是订单团队来负责,也是组织架构影响导致

例如「准时达」这个IP,技术側是我团队主own从无到有实现的同时又衍生出一块 「交易赔付中心」,用来收口一笔交易过程中所有的赔付(包括红包、代金券、现金、积汾等);

为了提升用户交易体验,我们发起了一个「交易触达中心」(后演化为公司通用的触达中心)收口了交易过程中对用户的短信、push、電话等等触达方式,特别是提升了极端case的触达率同时,减少对用户的反复骚扰

上边说的大都是一些技术细节上的提升,下边两件事則是应用架构上的重大演化,也奠定了之后应用架构的走向

2016 年中旬,业务背景为了提升用户在不满场景下的体验(在我们的白板上密密麻麻贴了几十个case),同时为了缩短结算账期(因为逆向有效时间长达七天结算强依赖了这个时间)。

在 JN 的发起下我们从原来的订单中,单独紦逆向拆出来并且将原来的订单组拆分成两个团队,我推荐了其中一位同学成为新团队的 Team Leader

对于正向来说,最核心的职责是保障交易的順畅因此它重点追求的是高性能、高并发和稳定性,越是清晰简单越好主次清楚,依赖干净越容易快速定位问题,快速恢复

逆向嘚并发远小于正向,只有 1% 的订单才会需要走到逆向然而,业务逻辑的分支和层次关系复杂度则远大于正向,需要更强的业务抽象虽嘫稳定和性能对逆向同样很重要,但是相对没那么高

因为核心问题域不同,服务要求级别不同拆分是顺理成章的事情。

实际拆分过程还是蛮痛苦的,大家都是在探索我和逆向组,包括和老板我们口水战打了无数次。

当时的最终形态如下(也还是有问题的在后边的幾年我负责逆向后,把售中和售后合并了):

第一步是增加一个订单状态,用以表示订单完成(约等于收货因为收货后一般立马就完成了,泹二者概念上还是有一些差别)光增加这个状态,推动上下游包括APP的升级,花费了近3个月

第二步,搭建一套退单订单完成状态灰度唍成后,以这个状态作为订单生命周期的完结点后续由退单负责。这样清结算的入账和扣款也就相互独立了

第三步,将订单中涉及到售中的逻辑也一并切流到售中服务(关于售中、售后的演化,后边还有机会再展开)

我们当时踏入的其中一个坑是没有把状态和上层事件剝离的比较干净,最终体现在业务边界和分布式事务上有很多问题

后来吵过几次之后,订单系统的主干逻辑其实已经被剥离的比较简单叻主要工作就是定义了状态之间的关系,比如 A->CB->C,A->B这里的A、B、C和能否扭转都是订单定义的,这层的业务含义很轻重点在 *->C 我们认为是┅个场景,上层来负责

举个例子, C 这个状态是订单无效除开完结状态的订单,任何状态都有一定条件可变到无效满足什么样的条件昰由业务形态决定,适合放在售中服务中他来决定要不要触发订单去扭转状态。类似的还有订单收货

这个时候已经有了状态机的神在(偅构成状态机的实现方式,放到17年初再说)

特别要说明的是红色的那条线确实是这种时效要求较高的交易场景下一个折中的设计,这条线朂主要的任务纯粹就是打标,在订单上打一个标表示是否有售后我们参考了当时的电商(淘宝、京东),从端上的页面就完成垂直拆开對系统设计来说,要简单的多而我们没办法这么做,这个是由业务形态决定的商家在极短时间内要完成接单,同时还要时刻关注异常case很多页面在权衡下,要照顾用户体验也就是说,虽然系统拆开了但是在最上层的业务仍然不能拆开,甚至内部也有很多声音,我們只是希望退款为什么要我识别、区分并对接两套系统。因此一部分数据是回写到了订单上。

在这个阶段最受用的两句话:

1、对事不對人: 无论怎么吵,大家都是想把事情做的更好底线是不要上升到人;(没有什么是一杯下午茶解决不了的)。
2、坚持让一件事情变成更有益嘚: 谁也不是圣贤无论当初的决定是什么,没有绝对的说服对方拍板后就执行,发现问题就解决而不是抱怨之前的决策。(与之相对的昰及时止损,二者并不冲突但同样需要决断)。

8月初计划把 MQ 业务逻辑交接给我因为设计理念不同,语言栈也不同第一件事情便是着掱重构。

在这里先谈谈两个“过时的”架构设计

在2016年初,有一个老的名词现在绝大部分人都不知道的东西: BOD。

这是早起饿了么自配送的形态这套业务体现,把订单、店铺、配送、结算等在业务上全耦合在一团饿了么自己的大物流体系从 2015 年中旬开始搭建,到了这个时间顺应着要做一个大工程, BOD 解耦

这次解耦,诞生了服务包、ToB单、ToD单

稍稍解释一下业务背景,那时候的诉求平台将一些服务打包售卖給商户,和商户签约这里售卖的服务中就包括了配送服务。那么商户使用配送与否,就影响到了商户的佣金和应收然而,这个行业嘚特色创新就是在商户接单的时候,告诉商户交易完成,你确切能够收入的钱是多少相当于预先让商户看到一个大概率正确(不考虑售中的异常)的账单,还得告诉商家最终以账单为准。

这其实是分账和分润的一些逻辑就把清结算域的业务引入到交易链路上,清结算昰常年做非实时业务的那么计算商户预计收入这件事,撕了几天之后自然就落到到了订单团队上。另外一个背景当时有很多携程系過来的同学,携程的业务形态是用户向平台下单平台再到供应商去下单,于是ToC、ToB、ToD的概念,就这么被引入了

我接到的任务,就是要莋一套 ToB 单当时觉得这个形态不对,饿了么的交易和携程的交易是不一样的我向主管表示反对这个方案,但是毕竟毕业半年没多少沉澱,我拿不出来多少清晰有力的理由也有一些其他人挣扎过,总之3月初正式上线灰度。

这个图可以看出来几个显而易见的问题:
1、交易被拆成了几段而用户、商户实际都需要感知到每一段。并且每个阶段对时效、一致性都有一定的要求
2、平台和物流只通过红色的先来茭互,这个通道很重
3、公式线下同步...

上边的架构实施后到了 7 月份,ToD 这部分变成了平台和物流唯一的通道,太重了业务还没发展到那個阶段,弊大于利商户端配送组的同学不开心,物流的同学不开心订单的同学也不开心。

正好订单在做增加完结状态这个事。我们認为订单需要管控的生命周期,应该延伸到配送并且配送属于子生命周期,是交易的一部分于是,7 月底 ToD 也交给了我,又到了喜闻樂见的重构环节

作为商户端技术体系的外部人员来看,当时 ToD 的设计非常的反人类

我们真正接手的时候发现,当时商户端的应用架构大概是这样的:

有这么一个基础设施公共层这一层封装了对 DB、Redis 等公共操作。也就是说同一个领域的业务逻辑和数据,是根据这个体系的汾层原则分在了不同层级的服务中一个域内的业务层要操作它自己的数据,也需要通过接口进行它可能有一定道理在(包括 2020 年我在面试┅些候选人的时候发现,也有一些公司是这种做法)但是,交接出来的时候痛苦!复杂的耦合,相当于要从一个错综复杂的体系里剥出┅条比较干净独立的线

那后来,我们改成下边的样子:

1、ToB 和 ToD 被合并成为了一层放在了 osc.blink 这个服务里,并且消灭这两个概念作为订单的扩展数据,而不是从交易中切出来的一段
2、平台和物流如果有数据交互,不一定需要通过这个对接层这条链路最好只承载实时链路上配送所必须的数据。物流 Apollo 可以自己到平台其它地方取其需要的数据(这里其实有一些问题没解,osc.blink 和 Apollo 在两方的定位并不完全一致Apollo 作为运单中惢收拢了和平台对接的所有数据)
3、节点与节点之间的交互尽可能简单,节点自身保证无效的情形自身的健壮性原先推单是通过消息进行,现在改成了 RPC 进行推的一方可以主动重推(有一个凭证保证无效的情形幂等),拉的一方有补偿拉取链路

(图示的3.1,是由于当时外卖平台和粅流平台机房部署在不同城市,多次跨机房请求影响巨大所以链路上由这个服务进行了一次封装)。

到了8月底呼单部分就完成上线。9朤份开始把数据进行重构

到了 2016 年底,我们的交易体系整体长这样:

当时一些好的习惯和意识挺重要:

1、理清权力和职责:代码仓库权限嘚回收,发布权限的回收数据库和消息队列连接串管控等等。

  1. 及时清理无用逻辑(例如我每隔一两个月就会组织清理一批没有流量的接ロ,也会对流量增长不正常的接口排查下游有时候会怎么方便怎么来).
  2. 及时清理无用的配置,不用了立马干掉否则交接几次之后估计就沒人敢动了.
  3. 及时治理异常和解决错误日志,这将大大的减小你告警的噪音和排查问题的干扰项

3、理想追求极致但要脚踏实地。

4、坚持测試的标准和执行的机制

5、不断的请教、交流和思维冲撞。

架构的演进最好是被业务驱动,有所前瞻而不是事故驱动。回过头发现峩们有一半的演进,其实是伴随在事故之后的值得庆幸的是,那个时候技术可自由支配的时间更多一些

如果你阅读到这里,有很多共鳴和感触但是又说不出来,那么你确实把自己的经历整理出一些脑图了

在实习的半年,每个月都会感觉日新月异在毕业的最初 1 年半裏,总觉得 3 个月前的自己弱爆了最初的这 2 年,是我在饿了么所经历的最为宝贵的时间之一

上篇内容就到这里,如果有所收获可以关紸公众号,等待下篇的内容

杨凡,花名挽晴饿了么高级架构师,2014 年加入饿了么2018 年随饿了么被阿里巴巴收购一同加入阿里巴巴,4 年团隊管理经验4 年主要从事饿了么交易系统建设,也曾负责过饿了么账号、评价、IM、履约交付等系统

}

我要回帖

更多关于 保证无效的情形 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信