饿了么技术往事
小结:
1、从技术骨干再到技术团队负责人这一转变过程中,很容易被忽略的就是团队的人员结构。
2、领域职责没有收口,带来很多一致性问题。
领域边界的分歧
3、
Leader的个人能力,决定了他(她)是这个团队的地基还是天花板。
4、
业务领域拆分、基础设施和业务系统分别建设后,给业务快速发展解绑了。但是包括稳定性在内的一系列挑战依然需要面对:
-
基础设施部署的标准化
-
系统的生命周期怎么管理?
-
每次故障都是昂贵的学费,故障可以避免吗?
-
复杂性带来的挑战:团队里面几乎没有人面临过这个体量的业务、这个复杂度的系统。快速交付的同时,如何保证系统的稳定和健壮?
-
我们的系统架构接下来如何演进?
6、
首先,build和release有唯一的ID,才可追溯,可回滚;
其次,是配置分离,把和环境(dev/test/product)相关的config从代码中剥离开来,否则系统很难迁移,更不用说大规模上云。第一反应可能是,把和环境相关的config写在xml或者yaml文件就可以了,但是,这些文件也是代码。
类似的,将这些随环境变化的config写在发布流水线的脚本里面,都不是彻底分离的方式。因为发布环境会发生变化,可能将来有更多的测试环境、更多的数据中心、每个数据中心里面可能还有多泳道。
因此,要做到“build once, deploy many times/every where”,config要存储在环境的上下文中,比如开发、测试、生产环境各自有一个配置中心,线上系统拉起的时候,先从配置中心拉取配置信息。要衡量环境相关的config和代码是否已经分离,看看能不能开源就知道了(抛开价值和代码质量不谈)。
7、
不是 case by case 的解决问题
8、
所以云上系统更强调算力的抽象,CPU核数、内存、网络带宽,把数据中心看作一个超级计算机,和 CPU 具备纠错机制一样,云上基础设施不是不会发生错误,只是结合它的“操作系统”(比如 Kubernetes),提供的是纠错能力(比如容器的故障转移 —— 故障容器销毁,新容器拉起,本质上也是冗余),而云上业务系统需要适配这类纠错机制实现自己的自愈 —— 面向云编程 —— 接受短时间的抖动(Transient Fault)会不时发生的这一个事实。
9、
https://mp.weixin.qq.com/s/SRirBH0EmyFdca5FuFVkwg
饿了么技术往事(上)
饿了么的技术体系,经历了以下四个阶段:
- 核心系统 All in one 的早期架构;
- 以系统领域化拆分、业务系统和中间件等基础设施分离为基础的全面服务化的架构;
- 随着自动化平台、容器调度体系成熟,治理从传统运维向 DevOps 转变的基础设施体系;
- 多数据中心体系基础上的Cloud Ready架构成型。
第一阶段:All in one
这是饿了么技术体系早期的样子,技术联合创始人带着一群 Geek,从0到1,搭建了最早的技术体系,支撑了百万级的单量。
这个阶段,业务一路狂奔,技术拼命追赶业务,唯快不破。
技术栈以 Python 为主,兼有部分 PHP 的系统,单机多应用的混布模式,应用发布、系统运维基本上是开发工程师敲命令行完成。核心的业务系统,商户、用户、交易都共享一个 codebase ,建立在一个名为 zeus 的系统下。短时间内业务飞速增长,在线数据库已经支撑不了 ETL 的需求,大数据体系、数仓开始建立。
大数据所在的上海数据中心,在线业务系统所在的北京数据中心开始搭建,这两个数据中心见证了我们架构的变迁,直到后来整体上云,最早的时候,技术团队40多人,有时候需要创始人跑到机房里面搬服务器。
系统跟不上业务发展速度的时候,核心系统经历过一些间歇性宕机的尴尬阶段。一些刚刚开始开拓的业务系统,也经历了系统刚上线就连续宕机,不得不临时放慢业务的阶段。但是这个过程也有收获,很多开发工程师线上排障能力非常强,脚本玩得很溜。
这个时期的工程师经常一人身兼数职,前端、后端、开发、测试、运维部署,对业务的理解也很深刻,甚至身兼技术和产品的角色。
收获和教训——文化的重要性
早期饿了么有一个鲜明的特点,就是大家都非常有担当,开放且包容。推卸、逃避责任的事情很少发生。虽然当时很多事故回过头来看比较严重,但是,组织对技术人员成长中的错误也相对包容。整个团队比较扁平,上下级之间的技术争论是常有的事情,但是都能就技术论技术。
饿了么的工程师文化氛围还是挺强烈的:工程师想着服务器的资源利用率能不能再压榨一下;在决定大规模使用Redis之前,会去读Redis的源码;很多方案,都是找个吧台和白板快速讨论,快速达成共识,落地上线。可能也是这个氛围,吸引到很多相同味道的人,形成了技术团队的文化。技术团队创始人最初形成的文化能延续下来,是这个团队从最初的几十人发展到上千人,还保有凝聚力和执行力的基础。
第二阶段:拆迁和基建
技术系统架构影响了业务交付效率,那么就需要重构甚至重建系统;如果是组织结构的不合理,阻碍了系统架构的迭代,成为业务发展瓶颈,那么就需要调整组织结构。
打个比方,如果说业务系统是赛车,那么基础设施就是跑道。基础设施也是这个阶段我们建设的重点,为承接将来业务快速增长打下基础。
这个阶段我们面临几个问题:
- Python为主的技术栈,现有的工程师单兵作战能力都很强,但是当时市场上的兵源严重不足。
- All in one的系统,各业务领域没有划分,业务模块之间代码交错,影响到了交付效率,需要给业务快速发展松绑。
- 基础设施和业务系统开发没有分开,身兼数职的开发工程师,在基础设施运维、中间件开发、前后端业务系统开发各个方面,各有长短。
- 传统的手工上线、部署、运维、监控模式 —— SSH到服务器上手工执行脚本,效率低下,事故发生时恢复时间长,发布后难以回滚。
成建制
随着业务量迅速增长,业务系统日渐复杂,技术团队也随之扩张,当时人才市场上 Java 工程师还是比 Python 的工程师来源多,有更大的选择余地,因此,以 Java 为主的多个领域开始逐步壮大,形成了 Python 和 Java 两大技术栈体系。
大前端、移动端、多个领域的后端业务应用、运维、中间件、风控安全、大数据、项目管理等多个角色分工,不同角色的工程师做自己擅长的事情。在这个阶段,系统和组织形成了业务开发、运维、共享组件及中间件几个体系架构。
业务系统
1. 业务领域划分
单体架构的系统开始按照领域拆分。All in one 的系统,通过划分业务领域,各个领域的技术骨干认领掉所负责的领域,组织结构相应调整,才很艰难的完成了划分。导购、搜索推荐、营销、交易、金融、公共服务、商户商品、商户履约、客服,新建的物流运单、分流、调度等系统,大数据数仓,逐步识别出自己领域、子领域及相应模块。在这个过程中,也有一些骨干在接手所负责领域后,没有在第一时间重视人力资源,导致交付能力不足,成为业务发展的瓶颈。从技术骨干再到技术团队负责人这一转变过程中,很容易被忽略的就是团队的人员结构。
2. 系统拆分
随着各自领域内的需求快速迭代,系统也迅速膨胀,相互之间的依赖、领域边界也开始变得错综复杂。原来可以“闭环”的,现在需要交互了,甚至原来可以直接动其他领域的代码提PR,访问别人的数据库,现在都不行了。从单体到领域切分服务,改变原有的思维方式,导致很多不适应,也走了一些弯路:导购域为了性能,自己落了一份商户商品数据缓存供商品查询,从而需要理解商户端领域的业务,订阅这类主数据的变更,而商户端的这部分数据就没有办法收口,缓存的新鲜度保障、底层数据结构变更、系统重构都比较麻烦;交易域麻烦也不少,一些领域为了不依赖交易,自己冗余了大部分的订单数据;物流履约领域则是下游存在多份运单数据冗余,导致领域职责没有收口,带来很多一致性问题,系统的复杂度也随之增加,系统交互和沟通成本也上升了。
而另一个极端,则是系统拆得过散,频繁的互相调用依赖,把本该高内聚的系统拆成低内聚了。订单和物流都经历了过度的异步化带来的痛苦,故障恢复时间过长,复杂性和排障成本增加。这段时间,领域边界的分歧也是一个头疼的事情。
体会和教训——康威定律、技术文化
订单履约体系里面,有一个隶属于商户域的团队,负责推单系统,该系统主要职责是承接商户呼叫配送,并将订单推送到物流运单中心。因为想减少对订单系统的依赖,以防订单系统挂了,无法履约,开发团队冗余了很多订单的数据,为此,要同时考虑订单和运单的正向、逆向,自身的可用性,系统也被设计得比较复杂。
在这个过程中,一旦有项目涉及到物流和推单系统交互,两个团队就经常发生领域边界的分歧,涉及到一些从订单取数的场景,物流团队认为,“这部分逻辑我不应该理解,你们应该从上游系统取到推给我们”,而这个隶属于商户域的团队则认为,“这部分不是推单的领域内的数据,而且推单系统自己也用不到,物流需要应该自己去取”。
类似问题反复出现,反复讨论,消耗掉大家很多时间,而且可见的将来还会发生。链路过长也带来稳定性隐患。最终,负责订单域的团队,来负责推单系统,订单领域内的逻辑可以闭环,这个问题就迎刃而解了。
所以,涉及到两个领域边界的时候,一旦相似问题反复出现,我们可能要考虑一下康威定律。
关于争论:系统设计阶段的激烈争论是非常合理的,充分的讨论会大大减少方案出现硬伤的概率,开发阶段也少受返工之苦。技术讨论围绕技术合理性,就事论事的展开,不要因为技术以外原因推脱或者权威说得算,讨论完大家才能很坦然的接受决定,最重要的是,参与的工程师都能充分理解最终方案的利弊和取舍,落地不会有偏差,出了问题不推诿。这也是很多团队技术氛围吸引人的一个地方,文化不是口号,是日常的这些细节和实践。
运营体系
1. 团队负责软件交付的业务运维、负责底层操作系统和硬件交付的系统运维、负责数据库的DBA、稳定性保障团队,相继成立。
2. 监控告警
集群实例的数量急剧扩张,登录到服务器上查看日志的运维模式已经不现实,也被基于遥测的监控体系所替代。随着7*24小时的NOC团队(故障应急响应)建立,基于ELK的监控体系也搭建起来,将核心指标投到了监控墙上。
体会和教训:监控告警机制的意义
互联网应用到了一定复杂度以后,庞大的集群,尤其容器化之后,IP动态分配,日志数量巨大,日志数据繁杂而离散,监控和排障需要借助的是和卫星排障一样的思路——遥测。日志系统要支持聚合和查询,监控需要实时收集采样各种指标、监控面板可以随时查看系统当前健康状况、告警机制第一时间发现系统冒烟。
有一次出现了一个问题,从监控面板上看一切正常,后来发现根因是一个int32 溢出的bug,导致下单失败,但是,为什么监控看不出来?因为代码里面把异常吞掉了,调用返回成功,而我们核心指标用的是下单接口的成功调用量指标和一些异常指标。
经过关键指标的治理,我们的监控面板关注三类核心指标:
- 业务指标 —— 实时关注业务整体的健康状况,从这个指标可以很直观的看到,业务的受损程度、时长和影响面,比如单量什么时候掉的、掉了多少比例、掉了多久,关键页面访问的成功率怎么样。对于上面订单的案例而言,需要在下单成功后打点(由负责订单落库的实现逻辑来完成),确保真实反映业务状况。需要注意的是这类指标通常涉及到敏感的业务信息,因此需要做一些处理。
- 应用指标 —— 关注应用实时的健康状况,应用本身及直接上下游的调用量、时延、成功率、异常等。为了安全起见,应该注意敏感系统信息不露出。特别是涉及到金融、安全领域的业务系统。
- 系统指标 —— 关注中间件、操作系统层面的实时健康状况,Network Input/Output、CPU load & Utilization、Memory Usage等。故障发生时,这些指标通常会先后发生异常,需要关注哪个是因哪个是果,避免被误导。
当然以上还不够,监控还有一些需要关注的地方,随着业务的发展,对我们的监控系统带来更多挑战,后面一个阶段,监控体系会有翻天覆地的变化。
这个事故给我们的另一个教训就是,关键路径和非关键路径的隔离:那个int32 溢出的bug是在非关键路径上触发的,当时还没有梳理出来关键路径,所以非关键路径故障影响到关键路径的事故时有发生。随着关键路径的梳理,后续服务降级的能力也才开始逐步建设起来。
3. 业务运维
业务运维团队担负起了很多业务系统运行时环境的初始化工作,比如虚拟机、HAProxy、Nginx、Redis、RabbitMQ、MySQL这些组件的初始化工作,容量评估工作。稳定性保障工作也是主要职责之一。这些分工让开发工程师可以更专心于业务系统的交付,但也是一把双刃剑,这是后话了。4. 系统运维
随着专业运维团队的建立,系统从物理机迁移到了虚拟机上,单机单应用(Service Instance per VM)是这个阶段的主要部署模式。硬件设备的运维,网络规划,慢慢都更专业规范起来,CMDB也开始构建。5. DBA/DA
数据库的运营统一收口到这个团队,负责数据库容量规划,可靠性保障,索引优化等等,交付了包括数据库监控体系在内的很多数据库运营工具产品,与此同时,他们也参与到业务系统的数据架构设计和评估选型当中。6. 制度
故障等级定义、架构评审机制、全局项目机制也相继出炉。制度的建立、执行和以人为本,三件事情,从来难统一,得不到人的认可,则执行会打折,背离制度设立初衷,所以,制度也需要迭代。制度是底线,制度覆盖不到的地方靠团队文化来维持,但是,如果把文化当制度来执行,就得不偿失了。体会和教训——架构师到底要做什么?
架构师是一个角色,而不是职级。这个角色的职责包含但不仅仅限于以下方面:
- 业务系统的技术方案设计和迭代规划
- 非功能需求的定义和方案设计、技术选型
- 现有架构的治理、领域边界的划分、设计原则与现实(技术债)的取舍和平衡
- 架构未来的演进
但是,如果不够深入,以上很难落地。从事前设计,到事中交付阶段的跟进,事后线上系统运营及反馈、持续的优化迭代,都需要架构师充分主导或者参与。
敏捷开发,设计是很容易被忽略掉的一环,制度上,我们通过组织由运维、中间件、业务开发、数据库、安全风控各领域的架构师组成的评审会议,在算力和基础设施资源申请通过前,审核评估设计方案。
实际上,这个上线前的“事前预防”也还是有局限性,因为这个时候设计方案已经出来了,虽然设计文档评审前已经提前提交,但是没有能够参与到整个生命周期,深入程度有限,只能做到兜底。更好的机制是每个团队都有架构师的角色,在设计过程中充分参与,这才是真正的“事前”。
所以,很长一段时间,跨领域的重点项目或者整个技术中心的项目,架构师才会主导或者相对深入的介入,而日常迭代,在设计方案、领域边界各方有重大分歧的时候,架构师往往是被动的卷入,变成居委会大妈的角色。覆盖的领域太广,架构师这个时候成为了瓶颈。而业务系统的架构恰恰是由日常的迭代逐步构建而成。后来全局架构组成立,架构师才真正开始分领域深入到更多业务的日常交付当中:
- 设计方案的负责人,作为Owner,为方案负责,并跟进交付,有权力有义务;
- 构建影响力。除了日常和开发工程师一起并肩交付,没有捷径,这个需要比较长的时间和项目的积累,很难一蹴而就,这个过程中如果得到一线工程师和开发经理的认可,影响力就会逐渐形成,有更强的推动力,反之,则会逐步丧失影响力。架构师如果没有影响力,一切无从谈起。因为组织上,架构师不一定在一线开发工程师或者开发经理的汇报线上。中立客观的态度,开放的心智,也是构建影响力的关键。影响力是架构师 Leadership 的体现。
- 稳定性的保障,永远是架构师的核心关注点之一,稳定性指标是架构师必须要背负的。其中包括交付质量、可用性、弹性、系统容量等等,不深入到具体领域,很难去提供保障。为此,架构师需要有调动资源保障稳定性的权力。
- 技术文化的影响,架构规划的贯彻,除了常规的宣讲、分享以外,更为重要的是日常项目交付、技术讨论过程中的潜移默化。设计思想、设计原则、技术文化认同,这些不是说教能形成的,是在一个个项目迭代、一次次线上排障、一场场事故复盘会中达到共识的。
作者:黄晓路(花名:脉坤),2015年10月加入饿了么,负责全局架构的工作。
饿了么技术往事(中)
在上一篇文章《饿了么技术往事(上)》中,我介绍了饿了么最早期 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大促,让大家真切感受到了这个市场的巨大潜力,同时系统的一系列短板也暴露无遗,除了积累了大促的经验以外,更大的收获是让我们看到架构还有很大的升级空间。还收获了一支全链路压测团队,他们在今后架构升级以及系统质量、容量等稳定性保障过程中,扮演了关键角色。
在饿了么技术往事系列文章的开篇,我提到了饿了么的技术体系经历了以下四个阶段:
-
核心系统 All in one 的早期架构;
-
以系统领域化拆分、业务系统和中间件等基础设施分离为基础的全面服务化的架构;
-
随着自动化平台、容器调度体系成熟,治理从传统运维向 DevOps 转变的基础设施体系;
-
多数据中心体系基础上的 Cloud Ready 架构成型。
现在我们前两个阶段基本完成了,开始了相对而言最艰难的阶段了……
第三阶段:脆弱的系统,苦逼的运维
这个阶段,我们的业务已经发展到一定规模,系统的长时间抖动或者崩溃,很容易上热搜,尤其是饭点时段。发生事故时候,冲在第一线的除了各业务线的工程师,还有运维团队,他们往往是最先响应,排障冲在第一线的团队。这个阶段说是靠他们生扛顶住了稳定性的压力也不为过:日常基础设施部署、事故发生时的应急响应、事故发生后的基础设施优化和改进措施落地,他们都承担了很多。
事故的教训,也让我们学会了遵循一系列业界积累下来的设计原则,为架构演进到下一阶段打下基础。
业务领域拆分、基础设施和业务系统分别建设后,给业务快速发展解绑了。但是包括稳定性在内的一系列挑战依然需要面对:
-
基础设施部署的标准化
-
系统的生命周期怎么管理?
-
每次故障都是昂贵的学费,故障可以避免吗?
-
复杂性带来的挑战:团队里面几乎没有人面临过这个体量的业务、这个复杂度的系统。快速交付的同时,如何保证系统的稳定和健壮?
-
我们的系统架构接下来如何演进?
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 的角色,为云上运维相关平台产品提供解决方案,或者凭借丰富的云上系统落地实施经验,为各上云企业提供实施方案。
另一个方向,由于合规和其他原因,还有部分没有上云的企业,依然需要基础设施运维工程师。随着云逐渐变成和水电煤一样的社会基础设施,运维工程师只写操作系统脚本、实施部署的时代已经渐行渐远了。
架构的历次演进,和几次事故或者险些酿成事故的“冒烟”事件,有着很大的关系:
-
交易系统崩溃的“饿死了”事故,我们开始分离关键路径和非关键路径,建设了非关键路径的降级能力。故障应急响应常规三板斧:重启、回滚、降级,至此完备。
-
第一次 517 大促入口崩溃的事故,是我们核心系统上云的开端。
-
F5 的 CPU 被打满,让我们意识到网关作为入口难以扩展的巨大风险,从而基于重新构建的大网关体系,取代了 F5 这一层硬件负载均衡。大网关体系是我们多数据中心架构最核心的系统之一。
-
基于 VIP 的 keepalived+HaProxy 负载均衡体系下,各种 failover 和上下游频繁扩缩容过程中,相关的稳定性冒烟或者事故频发,促成了充当 data plane 的 sidecar 上线,这是我们构建类 Service Mesh 架构体系最重要的组件。
-
核心交换机 bug 引发的数据中心故障,对我们下决心建设多数据中心体系有着很大的影响
关于这些事故和架构的故事,随着架构的演进,后面会逐个展开。
那个时候,我们常常自嘲是“事故驱动”型开发(Disaster Driven Development)。很多工程师除了自己的工位,在公司里面最有“感情”的就是整面墙都是监控大屏的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月加入饿了么,负责全局架构的工作。