28

饿了么技术往事(中)

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzU4NzU0MDIzOQ%3D%3D&%3Bmid=2247490841&%3Bidx=1&%3Bsn=d5ef12b413afbae2b717a4d6a5bef2c4
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ERrIra7.jpg!mobile

在上一篇文章《 饿了么技术往事(上) 》中,我介绍了饿了么最早期 All in One 阶段的架构,以及第二阶段业务系统拆分与团队运营的一些思考,以及我对于架构师职责的感受,接下来我会详细介绍饿了么全面服务化的架构演进历程。

一、中间件

业务线的工程师深陷到快速的迭代和业务复杂性当中,业务的快速增长、外卖行业午晚高峰业务特点带来的并发挑战,领域拆分后所需的服务体系框架支撑,责任自然落到了中间件团队。

当时中间件团队主要负责的三件事就是发布系统、SOA框架、统一的数据访问层。

1. 发布系统

外卖业务周末的单量通常比工作日要高,但是工作日事故率要高于周末,为什么?变更是万恶之源,周末很少发布。所以,发布系统接手管控,取消手动发布的模式,解决发布回滚的问题,通过发布自动化提高效率的同时,回收服务器的权限,降低安全和稳定性的隐患。当然发布系统的作用远不止于此,后续这个体系及其团队充当起了基础架构演进的核心角色。这个是后话了。

2. SOA 框架

SOA框架是支撑业务服务的骨架。和多数类似框架一样,为应对复杂的服务体系,服务注册和发现,常见的基于Design for failure的设计,熔断、限流、舱壁、多集群隔离这些功能都一样。但是,较特殊的地方在于——我们有两套SOA框架,Java 版和 Python 版。前面提到,我们有两个主要的技术栈 —— Java 和 Python,使得我们凡是需要 SDK 的地方,都需要支持两种语言,毫无疑问会对增加中间件团队负担。在当时确实是个难题,这个现在当然也有解,后面会提到。

体会和教训——是否应该统一技术栈?

关于是否应该统一技术栈,没有一个标准的答案。每个公司的技术栈和技术体系,有其形成的背景,如同架构一样,不放在上下文里面讨论合理性,往往没有结果,烟囱型也好、L型也好,只要是适合自己的技术和架构就好。

Python 技术栈当时已经支撑了很多核心系统,推翻现有系统,换技术栈的时间成本不可忽视。而当时市场竞争非常激烈,对于饿了么这样的创业公司,数据、时间和人是最宝贵的。而且,有一支能力非常强的 Python 技术团队,从里面抽调部分工程师,支撑 Python 技术栈的中间件建设,也不会带来额外的人力成本。维护两个技术栈,中间件团队的负担会增加,但是,换取的是时间和优秀的工程师,还是划算。这些 Python 工程师里面,负责业务系统的很多人后来也成长为独挡一面的角色,跟上了业务快速增长的步伐(后续会有相关的内容分享)。而负责中间件的 Python 工程师,他们的一些创造性实践,也为我们后续架构演进奠定了基础。

好的技术体系和架构,起决定性的不是技术栈,最终还是优秀的工程师。

3. 数据访问层

因为多 技术栈的存在,DAL 层选择了中心化的方案,而没有采取 SDK 。 统一的数据访问层为后续分库分表、限流保护、多数据中心上线后的数据纠偏打下了基础。 为了保证系统有足够强的吞吐能力,DAL 层采取了异步 IO 的方案来处理出入流量,中间件的最高境界是大家会忘记它的存在,DAL 层涉及到底层和数据库的交互,尤为敏感,而这个中间件几乎做到了,没有出现过重大事故,也很少有开发吐槽这一层的问题。 后来,这个一直稳健的团队在饿了么多数据中心建设当中,负责了核心的流量调度及容灾切换管控体系。 大家都习惯了叫 DAL,很多人不知道这个系统叫 Athena。

基于 DAL 的上线,DBA 和 DA 这个时期就忙着给各个团队做分库分表的事情:

  • 按业务功能领域切分——拆库

  • 按照访问频率、动静态属性等等规则——垂直分表

  • 基于Hash Partition(需要注意的是避免热点和Rebalance带来的成本)—— 水平Sharding

总之就是选择合适的 Partition 策略,降低数据库单个实例的负载。存储后来能支撑住千万级单量,除了上游队列的削峰、缓存的缓冲、数据库读写分离以外,也得益于适当的 Data Partition 策略。

二、大前端

其他团队还在拼命追赶业务、填坑补课的时候,大前端团队满足业务需求的同时,还为开源社区贡献出了非常优秀的产品 Element。就在大家认为这支团队会继续在前端领域上一骑绝尘下去的时候,令人没有想到的是,这个团队几年后会爆发出巨大的潜力,成为整个架构体系升级中一个举足轻重的角色。为什么叫大前端,因为他们和传统的前端团队做的事情不太一样,后面会讲到。

体会和教训——找到优秀的工程师多么不容易

招聘优秀的工程师,持续招聘优秀的工程师,这是一句正确的废话。但是有多难,带过团队的应该都深有体会,特别是你的公司还没有自带光环的情况下。优秀的工程师会吸引来更多更优秀的工程师,反之亦然,面试这个过程是双向的,尤其是优秀的工程师。有业务压力的时候,主管很容易扛不住,降低要求。当时大前端团队校招淘汰率还是挺惊人的,换来的是这个团队的工程师很高的技术素养和基本功,为后面成为一个真正的全栈团队打下了的基础。

Leader的个人能力,决定了他(她)是这个团队的地基还是天花板。

三、大数据

基于 Hadoop、Spark、HBase 的经典大数据架构这个时候也搭建起来了,因为是自建的数据中心,所以这些产品都需要有一个专业的团队来运维,因此大数据也有了自己的运维和中间件团队。在这个阶段,在线和离线数据同步、数据治理上面还不完善,因为产品化还在路上,很多工具缺失,导致很多团队都要自己直接去从数仓取数,不得不维持运营团队支撑定制化的手工取数需求。各个团队喊得最多的就是大数据的人不够,想要自己做。核心还是业务发展太快。后面随着大数据团队逐渐壮大,更多强援加入,各个产品相继成熟才得以缓解。

四、风控安全

这是一个不得不说,但是也不能说太多的团队,所以这部分只能务虚一些,任何一个到了一定规模的企业,风控安全团队是“真”底线。其他技术团队在面对这个同样是负责技术的团队面前,有时候确实也挺一言难尽的,这个时候高层的支持至关重要。尤其是从 0 开始建设这个团队,对内的扫盲和对外风控,一样艰难。

如果说一个技术公司,系统毁了,有什么还能留下来,就还能重建,那肯定是数据(现在可能还要加一个算法模型)。有什么缺失了,随时都可能垮掉,那肯定是风控安全。

饿了么的这支风控安全团队,对内、对外、对线上、对线下、对其他……都面临很多挑战和冲突,堪称业务专家的羊毛党和无孔不入的黑客,确实令人叹为观止。而我们的风控也经历了从开始的粗粒度约束、到依赖业务规则针对各种补贴、账期等场景兜底、再到依赖算法模型实时风控主动拦截的阶段。

如果大家身边有做风控安全的同学,请珍惜,哪怕他们有时候看到系统到处是窟窿的时候,脾气暴躁。因为他们整天面对这么多黑暗面,还能对这个世界报以希望。开个玩笑,从人道的角度出发,这个团队需要定期的心理按摩。

这个阶段,我们初尝了算法的威力。一开始只有搜索,但是还没有推荐召回系统,当时给推荐系统的物理机是我们能拿得出手的最好的物理机,其他业务系统分配的大都是虚机。系统上线以后,效果、转化率都还不错。之后不久这一待遇被另一个团队承包——负责配送履约的智能调度团队,大数据、机器学习、算法模型需要充分发挥功效,需要长时间紧贴业务、深刻理解业务,在智能调度领域我们也做过不少艰难的尝试、吃过不小苦头,直到我们有了自己的算法专家团队。

这个阶段我们还经历了第一次外卖行业的大促——517大促,让大家真切感受到了这个市场的巨大潜力,同时系统的一系列短板也暴露无遗,除了积累了大促的经验以外,更大的收获是让我们看到架构还有很大的升级空间。还收获了一支全链路压测团队,他们在今后架构升级以及系统质量、容量等稳定性保障过程中,扮演了关键角色。

在饿了么技术往事系列文章的开篇,我提到了饿了么的技术体系经历了以下四个阶段:

  1. 核心系统 All in one 的早期架构;

  2. 以系统领域化拆分、业务系统和中间件等基础设施分离为基础的全面服务化的架构;

  3. 随着自动化平台、容器调度体系成熟,治理从传统运维向 DevOps 转变的基础设施体系;

  4. 多数据中心体系基础上的 Cloud Ready 架构成型。

现在我们前两个阶段基本完成了,开始了相对而言最艰难的阶段了……

第三阶段:脆弱的系统,苦逼的运维

这个阶段,我们的业务已经发展到一定规模,系统的长时间抖动或者崩溃,很容易上热搜,尤其是饭点时段。发生事故时候,冲在第一线的除了各业务线的工程师,还有运维团队,他们往往是最先响应,排障冲在第一线的团队。这个阶段说是靠他们生扛顶住了稳定性的压力也不为过:日常基础设施部署、事故发生时的应急响应、事故发生后的基础设施优化和改进措施落地,他们都承担了很多。

事故的教训,也让我们学会了遵循一系列业界积累下来的设计原则,为架构演进到下一阶段打下基础。

业务领域拆分、基础设施和业务系统分别建设后,给业务快速发展解绑了。但是包括稳定性在内的一系列挑战依然需要面对:

  1. 基础设施部署的标准化

  2. 系统的生命周期怎么管理?

  3. 每次故障都是昂贵的学费,故障可以避免吗?

  4. 复杂性带来的挑战:团队里面几乎没有人面临过这个体量的业务、这个复杂度的系统。快速交付的同时,如何保证系统的稳定和健壮?

  5. 我们的系统架构接下来如何演进?

1. DevOps

因为云上资源的灵活性,我们在云上搭建了两个测试环境:alpha作为开发环境,用于软件工程师日常开发调试;beta作为集成测试环境,用于测试工程师完成系统交付上线前的集成、回归测试。费了九牛二虎之力才达成所有团队的共识,推动beta环境的系统和数据的完整性建设。在这里面发挥重要作用的,除了各个业务的开发、测试、运维团队,还有一个就是之前提到的负责发布系统的团队,这个团队不仅仅提供了一个简单的发布系统,基于持续集成和持续部署实现的开发、测试、生产环境相似化,是我们的系统架构继续演进的开端。

技术团队职责细分后,运维团队提供了保姆式的服务,这把双刃剑的另一面,就是开发团队很容易形成惰性,对自己的系统管生不管养,对系统的容量、治理关心不够,因为有运维团队。这就带来很多问题,代码不是运维工程师写的,但是有些团队系统甚至是运维工程师部署的。因为开发团队最贴近业务需求,需求变更可能带来未来的潜在容量风险,他们比较有发言权;而容量水位的现状反过来是运维团队更了解。因为这个时候,很多基础设施运维还没完全自动化,所以难以统一化、标准化,每个运维工程师都有自己的运维风格,日常排障上,有时候需要开发和运维一起才能完成。

此外,只生不养的思维方式,客观上也容易造成算力成本变成糊涂账。这个时候,开发、部署、系统运营(治理)角色的不统一带来的问题就会凸显。

应用Owner要成为名副其实的Owner,需要有应用的全景视角,对应用生命周期的把控能力。这个阶段,开始推动从虚拟化到容器化的转型,发布系统从一个简单的CI、CD的体系,延伸到了算力和调度的领域。基于一系列运维自动化工具的建设和全面容器化调度的实施,从而带来标准化的运维,才能把开发工程师(应用的Owner)推到应用完整的生命周期运营的位置上,胜任DevOps的角色。这个时候,事实上底层的算力平台,已经具备云上PaaS的雏形了。

在这个过程中,也做了不少尝试,比如,为了提高 alpha/beta 这两个测试环境的基础设施交付效率,有过一段时间基于 slack 的 ChatOps 实践,工程师都比较欢迎;还有过 Infrastructure as Code 和 GitOps 的实践,很可惜当时各方面条件和时机都不够成熟,没有持续推广。

体会和教训—— DevOps

  • alpha 和 beta 环境:

工程师在开发机上自测是不是就可以了,“在我机器上是好的”这句话估计开发工程师都说过或者听过,在开发阶段提供alpha环境,目的就是为了开发、测试、生产环境的尽量接近,避免由于开发、测试、生产三个阶段由于环境差异巨大带来的问题。解决不了“在我机器上是好的”这个问题,没有办法大规模顺利上云。工程师自己的电脑,某种程度上是一台“mommy server”,上面运行着需要的一切环境,而且每个工程师的祖传环境还不一样,这类环境在生产上是不可复制的。

  • Build & Release:

怎么做到高质量快速交付,保证系统的稳定?

在快速迭代的同时,做到快速试错、快速纠错、快速回退。需要发布系统做到每个编译的版本、每次发布的版本,像代码一样,可回溯可跟踪。关键在于build和release是immutable的

首先,build和release有唯一的ID,才可追溯,可回滚;

其次,是配置分离,把和环境(dev/test/product)相关的config从代码中剥离开来,否则系统很难迁移,更不用说大规模上云。第一反应可能是,把和环境相关的config写在xml或者yaml文件就可以了,但是,这些文件也是代码。

类似的,将这些随环境变化的config写在发布流水线的脚本里面,都不是彻底分离的方式。因为发布环境会发生变化,可能将来有更多的测试环境、更多的数据中心、每个数据中心里面可能还有多泳道。

因此,要做到“build once, deploy many times/every where”,config要存储在环境的上下文中,比如开发、测试、生产环境各自有一个配置中心,线上系统拉起的时候,先从配置中心拉取配置信息。要衡量环境相关的config和代码是否已经分离,看看能不能开源就知道了(抛开价值和代码质量不谈)。

  • OPS

接触过传统的运维工程师都知道,这是一群责任心极强的人(删库跑路,铲平数据中心的事情是不可能干出来的,虽然有能力……),他们维护着系统的底线,第一次517大促事故的时候,我们靠运维工程师救了大家一命。

但是,即使有操作的SOP,只要是人,执行重复任务的次数足够多,总会犯错。而每个资深的运维工程师,都有自己祖传的脚本,一夫当关万夫莫开,但是休假就麻烦了,特别是在高铁上信号不好的时候……最佳实践→ SOP → 脚本 → 自动化工具产品,沿着这个路径迭代似乎不可避免。

传统的运维工程师角色的演进方向,一个是为云上的IaaS/PaaS服务,对操作系统和底层硬件有着丰富经验的,还是运维工程师,他们当中开发能力强的,转型SRE,对运维产品理解深的,可以选择 Technical Product Manager 的角色,为云上运维相关平台产品提供解决方案,或者凭借丰富的云上系统落地实施经验,为各上云企业提供实施方案。

另一个方向,由于合规和其他原因,还有部分没有上云的企业,依然需要基础设施运维工程师。随着云逐渐变成和水电煤一样的社会基础设施,运维工程师只写操作系统脚本、实施部署的时代已经渐行渐远了。

架构的历次演进,和几次事故或者险些酿成事故的“冒烟”事件,有着很大的关系:

  1. 交易系统崩溃的“饿死了”事故,我们开始分离关键路径和非关键路径,建设了非关键路径的降级能力。故障应急响应常规三板斧:重启、回滚、降级,至此完备。

  2. 第一次 517 大促入口崩溃的事故,是我们核心系统上云的开端。

  3. F5 的 CPU 被打满,让我们意识到网关作为入口难以扩展的巨大风险,从而基于重新构建的大网关体系,取代了 F5 这一层硬件负载均衡。大网关体系是我们多数据中心架构最核心的系统之一。

  4. 基于 VIP 的 keepalived+HaProxy 负载均衡体系下,各种 failover 和上下游频繁扩缩容过程中,相关的稳定性冒烟或者事故频发,促成了充当 data plane 的 sidecar  上线,这是我们构建类 Service Mesh 架构体系最重要的组件。

  5. 核心交换机 bug 引发的数据中心故障,对我们下决心建设多数据中心体系有着很大的影响

关于这些事故和架构的故事,随着架构的演进,后面会逐个展开。

那个时候,我们常常自嘲是“事故驱动”型开发( D isaster D riven D evelopment)。很多工程师除了自己的工位,在公司里面最有“感情”的就是整面墙都是监控大屏的NOC作战室,大小事故、各种大促活动值守,熬夜全链路压测,里面常常挤满熟悉的面孔。

体会和教训——

(1)事故复盘

事故复盘和定期的故障验尸总结会是一个很好的机制。很容易被忽略的是,除了找到事故发生的 root cause,还需要从中发现存在的隐患,而不是 case by case 的解决问题,复盘的目的是阻止类似的事情再次发生,必要的时候,可以引入业务、产品、技术共同解决。

另一个陷阱是,故障复盘变成追责的过程,那么参与复盘的各方就很容易陷入互相指责、洗脱责任的怪圈,反而忘记了复盘的根本目的,也容易浪费大量时间,引起不必要的内耗。只要是参与复盘的人,都是有责任在身上的,为将来的故障负责,如果类似事故再次发生,或者没有在复盘中发现应该发现的隐患,参与的人都难辞其咎。

复盘结果要避免惩罚为目的 —— 除非违反了规章制度(底线,不排除有些是恶法,但不在讨论范围内)。否则甩锅、不作为的氛围会日渐滋生,自省有担当和有作为的个人或者团队,很容易成为吃亏的一方。事故复盘的过程,是了解各个团队甚至组织文化的一个视角。

(2)弹性设计

物流、交易经历事故后,各自采取的措施再次印证了,反脆弱的设计是我们的应用发展到今天的核心设计思路之一。

传统思路是基于一个上下文可控的理想系统环境下做出的设计,尽量避免一切意外的发生。而反脆弱的设计,恰恰假设黑天鹅事件一定发生,是墨菲定律的信徒,开句玩笑话,云厂商如果承诺你“我们一定会挂”,你一定要珍惜,你面对的是一个坦诚相待的乙方,值得托付。这不是推责给云厂商,这是由云上基础设施的特征决定的,大多数场景下,云上提供的服务是基于大规模标准化服务器(Off-the-shelf hardware)构建的虚拟化、容器化基础设施(Immutable Servers),而不是超高规格的个性化定制独占设备(Snowflake Servers)——无法规模化,成本也会大规模上升,因此,会更注重快速恢复能力,水平扩展能力,整体的健壮性,而不是具体某一个单机 SLA。

所以云上系统更强调算力的抽象,CPU核数、内存、网络带宽,把数据中心看作一个超级计算机,和 CPU 具备纠错机制一样,云上基础设施不是不会发生错误,只是结合它的“操作系统”(比如 Kubernetes),提供的是纠错能力(比如容器的故障转移 —— 故障容器销毁,新容器拉起,本质上也是冗余),而云上业务系统需要适配这类纠错机制实现自己的自愈 —— 面向云编程 —— 接受短时间的抖动(Transient Fault)会不时发生的这一个事实。

物流通过补偿机制增强自己的健壮性,交易引入 chaos engineering,都是基于这个上下文。要求应用是 stateless 或者 disposable 的,目的是为了 crash 后能够迅速拉起,快速自愈——所以,尽量分布式缓存,尽量少本地缓存,应用拉起时初始化的工作尽量少,交给独立的服务干这些事。业界的很多模式实践:bulkhead, circuit breaker, compensation transaction, retry都是指向提升系统的弹性(resilience),足够健壮的系统能够在经历系统抖动后,迅速自愈。

故障和意外一样,难以避免。我们能做的是减少人祸,敬畏生产环境,因为一次故障影响的可能是骑手一天的生计、商户一天的营收、用户的一日三餐。同时,提高系统的健壮性和自愈的能力,在故障发生的时候,尽可能的避免演变成更大的灾难,及时止损。

2. 黑天鹅

这个阶段,我们经历了一个大事故,起因就是核心交换机挂了,可能有人问,不都堆叠的吗,不都有主备吗,不都自动切换的吗,说得都对,但是都挂了。因为交换机的一个bug,主备切换后,备机也很快被网络风暴打挂,没经历过我们也不相信。这次又“饿死了”,我们只能坐等供应商的工程师抱着设备打车到机房更换,这个时候,一群人挤在应急响应指挥室(NOC作战室)里一点办法都没有。

在第一次517大促之后,我们就开始第一次容灾尝试了,当时采取的是最快最简单粗暴的方案,用最短的时间,在云上搭建一个了灾备环境并跑通了业务链路。但这是一个冷备的环境,冷备最大的风险,就是日常没有流量,真正 failover 切换的时候,有比较大的不确定性。这次事故再加上另一个因素,我们下决心将技术体系推进到下一个阶段。

体会和教训——上云

2016年第一次517大促,10点开抢的瞬间,我们系统崩掉了,要不是当时一个很稳的运维工程师,淡定操作限流,可能不少人在饿了么的职业生涯当时就结束了。因为对当时的基于Nginx和部分自研插件的网关层比较自信,不相信网关层会顶不住,所以全链路压测的时候根本没有压这一层,事后复盘的时候发现是操作系统一个参数配置的问题,如果压测一定能重现。

因为业务的效果很好,大促就成为常态,事实上第一次大促,我们是在自己的IDC里面用常规业务系统来扛的,所以影响到了非大促的正常交易。后面专门针对大促高并发大流量的场景设计了一套系统,也是隔离、排队、CDN、限流这些常规的套路,没什么特别的。但是,对我们影响更深远的在于,这套体系完全是在云上搭建的,2016年之前虽然云上有系统,但是生产环境流量很少,顶多是短信触达这类系统在上面,更多是用于搭建测试环境。在当时看来,云上强大的流量清洗、资源 scale out 能力,很适合大促的场景,后面,这套体系经历了多次大促,没有波澜。

在云上搭建大促体系以及灾备节点的经历,让我们后续在云上搭建全站的网关,并进一步构建整个数据中心,有了非常大的信心。下一篇我将继续介绍饿了么架构演变到了Cloud-Ready的状态,技术体系演进为业务发展提供了更多可能性。

作者介绍:黄晓路(脉坤),2015年10月加入饿了么,负责全局架构的工作。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK