作者 | 黄晓路(脉坤)
不知不觉已经写到了最后一部分,回顾经历的这段架构演进历程,内心还是非常感慨。在商业竞争中,技术团队和其他团队一样,作为组织的一部分,应该是荣辱与共的。但是,就行业而言,饿了么创造的社会价值也是显而易见的,而且,饿了么塑造了这个行业最初的模样,对社会创造的价值影响是长远的。
回顾前两篇文章:
第四阶段:Cloud Ready
这个阶段,技术体系承担了日均千万级的单量,通过建设多数据中心的技术体系,提供了数据中心节点跨地域水平展开的能力,从根本上解决了扩展性的问题。为业务发展提供足够的系统容量保障。同时,架构演变到了Cloud-Ready的状态,也为我们技术体系演进提供了更多可能性。
这个阶段我们面临的是前所未有的挑战,架构也因此迭代到了全新阶段
1、数据中心容量到达上限,如何避免成为业务发展的瓶颈
2、两次大规模组织融合,技术体系如何融合
3、全面上云后技术体系如何演进
多数据中心
2017 年 5 月,建成了多数据中心的技术体系。后面很多看起来轻而易举的事情 —— 午高峰线上故障容灾、全面上云、全站底层容器及调度系统的升级等等 —— 没有这次架构演进将会很艰难。
体会和教训:
多数据中心——多数据中心核心解决高可用诉求和容量带来的性能需求:
高可用:多个数据中心节点解决了单点问题。
性能和扩展性:通过部署在不同地域的节点,解决了两个问题。
离用户更近:具备地域属性的用户群体,和他们相关的运算和数据,离得更近。尤其适用于业务范围地域分布广的场景——全国甚至全球。
水平扩展能力:理论上可以在任何的地域拉起数据中心节点,解决业务快速发展带来的容量压力,从而解决性能瓶颈。具备多数据中心能力的架构,可以看作广义的分布式体系。
基于以上几点,多数据中心架构有它适用的场景:
用户有地域分布的特征
业务对可用性和弹性扩展能力有很高的要求
反之,以下场景就不适合多数据中心架构:
数据必须集中处理
业务没有按地域分布的特征
要求强一致、不能接受系统环境的不确定性、系统不具备弹性的架构体系
架构非常简单、业务量很小,分布式节点和冗余没有带来太多收益
三种形态:主备(active-passive)模式,对等(active-active )模式,以上两者的混合。
主备:通常的同城双中心,在一个地理位置上相近的区域(比如云上的一个region内),建立起两个数据中心(比如云上的availability zone),组成一个虚拟的数据中心,流量按照一定策略负载均衡到两个数据中心。无状态的服务,或者只读的请求,通过负载均衡策略,可以同时分发到两个数据中心处理,这个场景下,但是涉及到写请求只能在一个数据中心处理。存储服务的主节点故障会触发主备切换,写操作由另一个数据中心的节点承担。容灾为主,扩展性稍差,两个数据中心的时延要求严格,所以不能太远(通常同城),极端情况下,region故障,或者region容量达到上限,会比较被动,优点在于改造起来简单。
对等:跨区域部署的,基于地理位置的“分布式”数据中心。数据中心之间没有主次,可互为容灾对象,理论上可以水平扩展到更多节点。通常数据中心会部署在不同区域(云上的不同region),容灾能力和扩展能力都更强,缺点在于改造成本和对中间件等基础设施的要求高。
混合:更适合多数据中心建设从主备演进到对等模式后的形态
两种模式如下图所示:
上下文
对于饿了么而言,核心诉求首先是扩展性,然后才是容灾。根据当时业务团队评估,在年底行业的单量就会翻番,当时的系统是无法承担这个业务量的,就地扩容是第一选择,但是当时的 IDC 供应商告诉我们没有这么多的资源,不是机器或者机架问题,是没有机房了,新机房在建,按照进度,交付时间风险非常大。在这个背景下,我们启动了多数据中心的建设,并选择了对等架构。
从业务开发进场改造,到完成线上切流,我们用了 3 个月的时间。但是,所依赖的基础设施和架构建设,是在之前几年时间积累起来的。
体会和教训——选型考量
为什么不采取同城双中心?如果仅仅是为了容灾,那可能也足够了,但是,加上扩展的需求就有问题。我们的数据中心,消耗资源最多的并不是承担业务逻辑的SOA服务类应用,算力消耗的大户实际上是基础设施中间件 —— 消息队列、分布式缓存、监控体系、各种类型的数据库等等。同城双中心主备的架构,写操作只能在一个机房,比如数据库等只能是主/备,写操作是没有办法充分利用多数据中心的优势水平扩展的,如遇到单机房容量瓶颈,比较被动。
多数据中心建设架构选型还是要依据自己的业务特点,综合考虑到系统的现状、能承担的冗余成本、以及业务规划,选择适合的架构。
基础设施
解耦
多数据中心可以水平扩展的前提是,各个数据中心之间需要解耦,从用户端发起请求开始,一笔交易内的调用链封闭在当前数据中心完成,尽量减少跨数据中心的RPC请求。与此同时,还要求数据中心之间互为容灾的角色。依赖两个组件解决这个问题:Data replication service 和 Gateway。
Data Replication Service
作为数据同步服务,这个组件主要解决以下问题:
1、数据中心之间的数据复制:出于容灾的需要,数据在一个数据中心写入后,需要同步到其他数据中心
2、循环复制阻断:确保一条数据的复制是单向的
3、数据变更事件广播:供业务方订阅用于更新缓存、异步解耦等等场景
4、写冲突的记录和告警:确保第一时间发现数据冲突介入解决
体会和教训——数据同步和复制
上海数据中心上线切流成功的那一天,我们心里面还是没底,担心数据同步出错。系统挂了可以恢复,各数据中心节点间数据发生不一致的话,数据订正的成本很高。多数据中心架构实施后的几年里面,发生过一次 DRC(我们的数据复制服务)变更引入的bug,导致同步出错,好在影响的数据库实例范围可控,但是仍然花了一个通宵订正数据。如果是大规模出问题,后果不堪设想,技术团队可以下课了。
还有一个教训,DRC 被滥用了,因为我们的疏忽,对数据变更事件订阅服务的治理和管控不足,各业务方随意订阅其他业务方的DRC事件广播。因为 DRC 是基于数据库的 bin log 生成的事件,也就是说,订阅方直接监听这个事件,等同于直接访问数据库,如果这个数据库不属于订阅方的业务领域,那么订阅方业务就和数据 owner 的领域数据库耦合在一起了。因此也没少吃亏,某业务系统 S 订阅了商户端领域数据库的部分 DRC 消息,商户端领域的开发团队在一次重构过程中,有部分相关的表不得不双写了半年,还下线不掉,因为 S 的技术团队没空改造……。合理的做法应该是,只允许领域服务订阅自己数据库的 DRC 事件,其他领域作为消费方应该订阅这个领域服务封装的对外消息广播。
Gateway
API Gateway 一开始是由中间件团队负责的。后来,包括 API Gateway 在内的大网关,收口到了大前端团队来负责。之所以称为“大网关”,是因为包括了几个部分:
1、DNS(云提供的基础设施):将域名解析到就近的网关,简单讲就是北方的用户请求解析到北方的网关
2、Anti-DDOS层(云提供的基础设施):流量清洗,应对异常攻击
3、软负载(4层/7层,云提供的基础设施):负责到API Gateway的负载均衡,卸载TLS 。
4、狭义的网关API Gateway(内部系统名称Sopush),这个组件主要解决以下问题:
5、根据一定规则,将请求路由按照一定规则路由到指定的数据中心,我们的路由规则核心location based,这个是由业务特点决定的,绝大多数场景下,导购、交易、商户履约、物流配送在一个3公里的地域范围内发生;基于这个能力,单个数据中心发生故障的时候,API Gateway能够通过re-route执行流量切换。因此,这个组件重写前最早一版叫API Router。
6、常见的网关功能:应对(正常)流量冲击、卸载TLS(后来前移到7层的SLB上了)、限流、一些安全策略等等。
7、Http2/Server Push:这个是Sopush最早构建的初衷
8、所谓的Web API层(内部系统名称Httpizza):Web API层负责对后端的领域服务做聚合、裁剪、协议转换,要和领域服务解耦开来。传统的做法,这一层服务由各业务领域的后端团队按需开发部署出来一个façade层,随着越来越多这一层的中间件出现,大家倾向于通过配置来实现这一层。这一层和前端业务逻辑的关联性很强,是“前端的后端”(BFF),前端团队来负责更合适。
和领域服务解耦之后,通过支持mock,开发阶段前端不需要等待后端交付就可以完成初步调试。用户端上接口契约发生变化,如果不涉及到领域数据的变化,后端领域服务不需要变更,调整web api层就可以;反过来,后端的重构,只要端上接口契约不变,Web API这一层配置更改即可。
和API gateway不同的是,这一层需要处理body。有些解决方案支持以AOP的方式自动生成RESTful API,以java为例在领域服务代码中,通过annotation 把领域服务变成同时支持Web API的方式,虽然便利,实际上把领域服务和面向端的RESTful API耦合在一起了,而且想要支持多个服务聚合也比较困难,所以我们没有采取这一方式。各业务线的前端团队,通过配置策略就可以交付这一层的需求。而这一层作为中间件,运维则由大前端团队完成。
大前端团队则演变成一个全栈的开发团队,Java/ Go/ Node.js/Javascript 是他们的主要技术栈。
下图是网关层演变过程的图示:
体会和教训——网关由来
这个组件的历史,要追溯到2016年的第一次517大促,在517之后,我们构建了大促体系,这个网关就是产物之一,我们当时需要在网关层通过cache-control植入缓存策略,同时需要一些灵活的、细粒度限流策略,包括支持plugin来扩展网关的功能,更有意义的是,我们将网关前移到数据中心之外——云上了,这一层是大促流量冲击的第一层(除了云厂商提供的流量清洗和软负载以外),因此需要有足够强的水平扩展能力。
在这之前,我们的公网入口是F5和NetScaler。一个几乎酿成事故的事件,让我们下定决心优化架构:当时由于一个配置错误,导致本来应该命中CDN缓存的用户端请求全部回源,瞬间F5承接了巨大的流量冲击(正常流量,非恶意攻击),那天看着监控曲线,在午高峰单量最高的几分钟,F5的CPU以接近100%的负载挺过去了,如果那个时候单量再高一点,或者午高峰持续的时间再长一点,估计就是一次大事故,因为这层硬件设备没办法水平扩展,一旦过载就无计可施了。后来,基于大促的网关架构,将整个数据中心的网关都迁移过来,建成了现在大网关体系的一部分。
服务体系
业务系统:设计和弹性
多数据中心核心的基础设施之一的 Data Replication Service ,使得点对点的多数据中心架构成为可能的同时,隐含了一个要求,就是业务系统需要接受最终一致,因为跨地域的 Data Replication 网络延时是同机房的几十倍,理想状态下,华东到华北 30ms ,华南到华北 60ms 。在发生单个数据中心故障转移的时候,还可能进一步加剧时延造成的影响。还要求业务系统有足够的弹性,能够容忍 Fail Over 切流时发生的系统抖动,在底层基础设施恢复后,第一时间通过重试、补偿等机制来确保业务系统的自愈。
多数据中心环境下,很多设计上本来就存在的缺陷会更容易暴露出来。举个例子,假如一些领域逻辑没有收口,比如订单,多个系统冗余了订单数据,这个时候,在订单状态变更过程中,发生了切流,各个订单数据的副本由于在不同的数据库中,同步延时是不一致的,会造成下游系统和交易系统的订单状态冲突,下游的履约逻辑无法继续推进。另一个例子,就是缓存,如果把缓存当持久化存储,认为缓存不会丢,不考虑大量击穿缓存对数据源的冲击,那么在切流的时候,也会造成大麻烦。还有,如果缓存设置太长的TTL,或者没有TTL,那么流量切回(Fail Back)的时候,就会访问到不新鲜的数据。再比如 Session ,如果采取的是 Session Affinity 的机制,那么切流后会导致大量用户登出。
这就是为什么需要 Anti-Fragile 的设计,要求我们的应用的设计尽量遵循 Disposable 、Stateless 和 Share Nothing 的原则。
Naming service
早期我们是基于 HAProxy 来做负载均衡代理,访问数据库、队列这些后端资源,这套架构依赖运维工程师频繁介入维护,在系统规模比较小的时候,是够用的。随着业务的发展,我们集群变得越来越大,应用服务日渐增多,上下游节点的扩缩容变得频繁,与此同时,快速迭代和变更带来系统抖动也越来越多,对系统的弹性要求也更高了,尤其是全面容器化后, stateless 的节点发生故障随时拉起重建,需要相应的注册和发现机制,再加上多套环境,这个架构体系已经力不从心。为了应对这系列新问题,我们的解决方案是内部称之为 naming service 的体系。负责 rediscluster 代理 corvus 的那支中间件团队,他们贡献的一个组件 Samaritan(简称 sam ),起到了至关重要的作用。这个体系也成为为我们架构演进的开端。
基于这个架构,应用服务访问数据库等资源使用的是“code name” —— 这是业务系统的开发工程师在申请数据库、缓存、消息队列等资源的时候,自己替这个资源取的一个应用内唯一的别名,那么,代码中访问相应的资源,在任意环境(dev/test/product)和多个数据中心都使用同一个 “code name”,中间件治理系统在初始化分配资源的时候,会和这个“code name”在服务注册中心注册关联,在运行时,与应用部署在一起的 sam 会解决 code name 到实际物理资源的 mapping ,并充当集群代理的角色,负责负载均衡,同时发生 failover 的时候,通过注册和发现机制,让业务系统无感。这个时候,原先充当代理层访问 redis cluster 的中间件 corvus 自然被 sam 替代掉了。架构演化如下:
看到这个架构,很容易想到 Service Mesh 和 sidecar。
Service Mesh 概念火热以后,sidecar/ambassador 的模式引起广泛关注,不过,那时候我们还没有 service mesh 的概念。在 2016 年上线 sam 的初衷,只是为了替换 Haproxy ,先有 sam 才有的 naming service 。上线 naming service 的原因也比较简单,是为了解决多个环境(dev/test/prod)下,多套连接串的问题,同时,使得所有后端服务的注册和发现,统一到我们的注册系统当中。
这套体系需要sidecar,正好我们手上有 sam ,所以就改一改用上了。它保障了我们后来多数据中心架构得以顺利实施,快速上线。现在回过头来看,勉强算是 mesh 的雏形, naming service 体系,跟现在类似 istio 强大而复杂的控制面和数据面相比不可同日而语。不过,基于这套架构体系,演化到 Service Mesh,也顺理成章。
Job
还有一类应用是 scheduled job ,包含两部分,一个是 job 引擎,一个是 service 。我们的 job 引擎本身是一个工作流引擎(eworkflow),eworkflow负责按照配置的规则触发对 service 的调用, service 负责业务逻辑的实现,避免 job 单点的同时,充分利用 service 集群可以水平扩展的运算能力。
一个常见的应用场景是批量处理数据,我们的多数据中心架构下,每个数据中心有全量的数据(failover的考量),所以Job所调用的服务需要支持数据的过滤,只处理归属当前数据中心的数据;另一类场景就是消费队列消息,调用 service 进行处理,这类场景在对消息的顺序没有要求的情况下,可以利用集群的吞吐能力增强消费能力。
存储、消息和缓存
DAL
DAL 层在多数据中心建设中,主要是纠偏的作用,非当前数据中心的写入请求会被拦截,确保同一个时刻一条数据只会在一个数据中心写入,避免数据冲突。数据持久化前,打上的数据分片标签也是由 DAL 层植入,数据所属的分片是不变的,但是分片归属(可写入)的数据中心是可以动态变化的。DAL 的开发团队,还负责了另一个核心的系统——Global Zone Servce(GZS),这个系统负责流量路由规则的下发和订阅管理,以及 failover 流量切换的控制中心,网关和 DAL 所执行的容灾操作均来自于 GZS 的指令。
跨地域的 data replication 会有延时,failover 切流的过程中 DAL 会短时禁写,判断数据同步是否完成,正常情况下数秒内会完成这一过程,数据同步完成才会放开写操作,这段时间内业务是有损的。在极端情况下,短时间无法同步完成,会评估是否强行切流。这个 Failover 切流策略是结合外卖的业务特点制定的,优先保可用性,然后才是一致性,也就是说,第一时间保障业务恢复止损,确保尽快切流,切流完成后,用户通常重试(刷新页面)就可以操作成功。
数据库
一旦采用了多数据中心的架构,各个数据中心的数据库是相互独立的,意味着依据自增 id 作为主键(或者作为主键的一部分)的机制,会在数据同步后,发生冲突,所以我们设定了不同的步长,留够 buffer ,错开生成的 id 。历史遗留的问题,有些 id 还是 int32 ,多数据中心架构下很快会溢出,需要花点时间去推进改造。
消息队列和缓存
这部分我们针对多数据中心所做的定制并不多。更多的关注点是设计原则上。虽然,很多queue和cache中间件都支持持久化机制,但是和数据库不同,它们擅长的场景和设计初衷也不在于持久化存储。所以,我们在设计系统的时候,仅仅把 redis 当缓存,而不是不能容忍数据丢失的存储,基于 cluster 通过缓存的分片,cache aside 等等机制和模式,queue 也类似,通过支持分片,补偿机制等等,保障系统的可用性,并在发生 failover 的时候,业务系统能短时间内恢复正常。
底层设备
在极短的时间内,系统运维团队交付了大量的服务器,上架装机,网络规划,专线租用等等,拉起了一个全新的数据中心,所以,如果要自建数据中心,没有一个专业的系统运维团队,真的搞不定,这是一个复杂而专业的领域——现在云厂商比较成熟的IaaS屏蔽了这些复杂性。
DevOps
之前提到,我们推进了和环境相关的 configuration 与 code 的分离的改造,以及基于 naming service 的架构体系,这为支持多数据中心的部署体系打下了基础。
稳定性保障
监控
早期的监控体系,业务系统使用的是ELK三件套,也曾经尝试过 influxdb 作为监控指标的时间序列数据库,操作系统和网络是 Zabbix 。随着架构的演进和业务的发展,对监控系统提出了新的要求:
1、所有监控指标和面板都收口到一个系统
2、支持基于自定义 tag 的指标聚合。多数据中心架构下,要求监控系统能够支持单个数据中心、多个数据中心、数据中心内部业务分片(我们的业务特征是基于地理围栏)等多个维度的聚合监控、告警,比如商品详情页在某个省的实时访问量,某个应用在某个数据中心的 qps 等等。自定义业务维度的聚合也是一个比较强的诉求,比如根据订单来源渠道(小程序、App等等)聚合。
3、监控面板上,采样频率 10 秒一个点,海量的监控数据需要压缩存储,每天指标未压缩过的数据量达到 PB 级,而我们要支持 1 个月内的历史指标查询。
4、trace 需求,比如根据订单号查出从创建到最后履约的调用链条(其中包括同步、异步的调用,并且识别出来跨数据中心的调用)
5、因为外卖的业务特点,午高峰和晚高峰指标会远远高于其他时段,而周末的曲线特征和工作日也有差别,传统的基于阈值的告警无法满足要求,需要支持基于趋势和同环比的曲线拟合。下图所示说明了告警面临的挑战:
为满足以上所有需求,我们采用了自研的监控体系—— EMonitor,以及时间序列数据库 LinDB ,很好的支持了我们的以上需求。
体会和教训:监控告警的陷阱
有一次午高峰,从监控面板上看很多核心业务指标掉了一半,手机上报警推送声音此起彼伏,但是线下运营团队、线上舆情、客服投诉都没有任何反馈,我们正在评估是否需要启动 fail over 流程的时候,负责监控系统的同学说,监控系统有几台机器挂了 —— 看来监控系统也需要监控 —— 好在有。
告警也有两难的地方,告警缺失,导致无法第一时间响应,故障损失会加剧,但是如果太多的话,手机不停收到告警推送,就会变成噪音,反而对告警麻木。这里涉及到一个争议话题,到底“业务异常”算不算异常,比如 password 不对导致登录失败、由于id不对获取不到某些数据(商户、菜品、订单),有两种处理方式:
1、将其包装在返回值里面,不作为异常;
2、包装为异常抛出(当然生成stack trace,会损失一点点性能);
这类“业务异常”每时每刻都在发生,如果非常多就会使所谓的“系统异常”——比如timeout、NPE、service unavailable这类需要引起重视的异常被淹没。内部也发生过激烈的讨论,险些引发“圣战”。不管采取上面哪种方式,这个时候都需要采取措施降噪,投入对告警的治理。
全链路压测
前面提到第一次 517 大促,全链路压测团队建立起来了,并在一系列大促及日常容量保障,多数据中心建设中起到至关重要的作用。在每个发布窗口,通常有数以百计的发布变更,系统容量是动态变化的,如何确保整体容量保持在健康水位?多数据中心架构体系下,如何保障发生 failover 的时候,剩余的数据中心能够承担切换过来的流量?新建数据中心能否达到容量预期?系统关键路径的瓶颈在哪?大促到来的时候,会场和营销流量是否会冲击到午、晚高峰的常规业务?没有全链路压测,这些风险都无法识别。
体会和教训:全链路压测
涉及到全链路压测的时候,很多人都认为,为了不影响生产,可以在测试环境完成。以我们的经验,这样测试出来的结果,参考价值不大。因为随着业务达到一定体量,系统到一定复杂度以后,调用链路很长,频繁的需求迭代,导致涉及到的上下游服务容量是动态变化的,每次全链路压测的瓶颈都会发生变化。这些都没有办法在测试环境等比例缩小模拟出来。
线上的全链路压测,我们关注的重点是:
测试数据和生产的隔离,测试数据标签的全链路传递识别,避免测试数据在生产环境透出,避免测试数据进入大数据的离线计算中
完善的监控体系,压测过程中发现系统瓶颈。如果影响到线上,及时发现,停止压测
具备弹性的系统,压测过程中一旦出现故障,压测停止后,系统能迅速自愈
自动化压测平台,是全链路压测能够周期性执行的基础
全链路压测的常态化,定期压测,能及时发现系统瓶颈隐患
最重要的就是一个自动化测试团队,这个团队除了要了解业务,构建起全链路测试的 test case ,设计测试方案,还要熟悉中间件,了解系统部署的拓扑结构,识别链路瓶颈,具备较强的开发能力,搭建全链路自动化压测体系。
在线上执行的是 load test ,目的是评估容量是否达到预期,而不是为了压出系统极限的 stress test 。有必要选择在业务低谷时段压测。
架构全景
体会和教训:多数据中心容灾
经过一段时间,多数据中心架构体系的不断完善,多次 failover 切流都比较顺利,从决策操作到业务曲线完全恢复正常,最慢的情况都在数分钟内完成。确实在午高峰“救”过我们的系统很多次,但这也是一把双刃剑。太顺利就容易盲目自信,忽略潜在的风险。
一度系统的稳定性过于依赖 failover 切流,最频繁的时候有一段时间,每周至少计划外触发 failover 一次,发布回滚、限流、降级、重启这类常规快速恢复的手段反倒用得少了,客观上造成设计上的懒惰,很多系统的健壮性和弹性设计逐渐显得不足。此外,种种原因,也导致一些决策或者决策的依据变形,对风险评估不足,草率启动了 failover 流程。有两个典型的案例:
一个外部渠道的下单曲线出现抖动,短时无法恢复,虽然在全局上看该渠道单量占比无足轻重的业务,却启动了全站 failover 流程,结果,故障扩散到了所有数据中心,变成了一个耗时很长才恢复的全局事故。在切流的过程中是有损的,启动全站 failover 本身就是高风险的过程,不得不启动流程止损的时候,往往发生在业务高峰,那么,承接流量导入的数据中心,在原有流量的基础上,除了会叠加来自于故障数据中心常规流量的冲击,还有来自于系统逐步恢复过程中用户重试、系统重试的冲击,从监控曲线上,很多应用的 QPS 曲线会出现类似秒杀场景的瞬时尖刺,这对任何系统而言都存在很大的不确定性。因此,应该是在出现重大故障的时候采取的最后手段,是为了防止出现灾难性的后果,因此,在启动 failover 流程前,需要两害相权取其轻,如果当前的风险远远小于切流带来的风险,那就不能盲目启动,所以,决策需要有相应的 SOP 和合理的评估标准作为依据,更重要的是坚持这些标准后面的原则和初衷,否则很容易被各种因素干扰,系统发生抖动的时候决定不启动 failover 流程有时候要承担更大的压力。下图为 failover 切流过程中的业务曲线 .
另一个案例,是底层网络问题导致的故障,从持续时间和业务量下跌比例看,触发了我们启动 failover 切流的流程,决策上没问题。流量切换完成后,绝大多数核心系统都迅速自愈了,但是物流履约下游的一个系统,迟迟没有恢复,把预期内的损失扩大了。云上系统、多数据中心的系统健壮性至关重要,弹性设计是不能忽视的一环。设计上的缺陷,在放到这类极端场景下就会暴露出来,因为数据同步的跨地域延时,会有一定概率,在切流后出现数据短时间不一致,特别是网络故障导致的,会加剧这一问题。以上面的系统为例,由于它冗余了包括运单状态在内很多运单中心的数据(客观上把自己变成了另一个运单中心),造成运单中心无法收口运单领域的逻辑,这个糟糕的设计造成了在系统抖动的时候,数据同步一旦发生延迟,冗余的状态和运单中心发生冲突,导致无法往下推进流程,再加上应对最终一致的场景,这个系统弹性设计不足,在 failover 切流的时候被放大了。
Failover 方案的演练至关重要,否则在需要用到的时候,发现有问题就很被动。演练的频率,切流以及回切流程 SOP 的验证,演练后的复盘,都是需要考虑的事情。每次事故发生的时候,临场的应对是决定性的。尤其是在长时间没有出现故障的情况下,演练甚至突袭能检验故障真正发生的时候,相关团队是 Available 的,容灾系统是 Dependable 的,应急响应方案和一线工程师的排障能力是 Capable 的。
在多数据中心的架构下,定期做流量切换还有一个作用就是验证各数据中心的容量,确保能承接 failover 切换过来的流量叠加。
融合
百度外卖
百度外卖的融合,从一开始把零售和物流研发从上海交到原百度外卖在北京研发团队主导负责到后面的调整融合,我们也经历了不少曲折。
快速拆分
在团队融合上,为了最大限度的保证团队的稳定,留住团队,选择了很短的时间内,把两块核心业务交由原百度外卖的北京技术团队负责。
1、同城零售体系和外卖之间相对而言,在业务上是两个相对独立的领域,从业务形态和发展阶段来看,两个业务都有不少差异,零售需要更多探索和试错的空间,系统需要更多的自主和灵活性。
2、物流技术体系交到北京原百度外卖研发团队负责主导之后,有一个组织结构调整就是技术团队分成专送和众包两条业务线,众包交由原百度外卖北京的研发团队负责,专送则继续由上海的团队负责。后来,花了不少功夫来解决众包稳定性的问题,交付效率也受到了掣肘,这个过程中,众包系统经历了两次重写。
再说康威定律
物流组织结构调整后,技术团队分成了专送和众包两个部门。这里说明一下背景,即时配送运力主要分四类:一类是专送,由饿了么平台主导的线下配送团队;第二类是众包,是个人经过审核后,在平台上注册成为骑手,第三类是三方配送公司提供运力,通过开放平台对接;第四类是商家自配送,商户自己解决配送问题。融合过程中,众包的开发团队选择了在百度云上重新搭建一套众包体系。
结果是,增加了一系列冗余的系统和数据。百度外卖云上系统和饿了么融合之初是两套体系,而且数据中心也分布在不同的地域及不同的云厂商。众包运力离不开对上下游的依赖,为此,要在百度云上搭建起众包系统,不得不冗余很多上游运单中心的数据,甚至一些主数据也冗余维护了一套。到了后期,相当于出现了两个物流系统,其中有很多冗余重复的部分。
这个不合理的系统架构和技术团队组织结构按照专送和众包切分有着很大的关系。众包是运力的一种,而不是独立的物流体系,只是配送任务的承接方,因为它和其他运力一样共享上游的运单系统、分流系统、还有包括地理围栏在内的众多主数据系统。因为专送和众包两个部门的划分方式,使得本来是两个运力形态演变成了两个物流系统。除了系统冗余以外,团队膨胀的速度,也超过了物流业务量整体增长的速度,仅仅因为单量在各运力形态的占比发生了变化:占比变多的某一个运力线,需求、数据量等等相对增多;而其他的运力形态业务占比降低,则相对会更少一些,但是物流整体的业务量的变化是远小于某一个运力线的占比变化幅度的。
而这类运力形态之间的占比关系不是一尘不变的,会动态发生变化。不能因为这个因素,频繁的扩张或者收缩团队。合理的拆分是按照运单、分流调度、运力等等领域切分,每个团队的业务都会涉及到专送、众包、第三方等所有运力形态,各领域收口自己的系统和数据,减少了数据和系统的冗余,同时,也不会因为单量比例在各运力之间发生变化,导致有些团队需求激增,人员膨胀,另一些团队人员冗余的局面。按照这一原则,调整组织架构,系统架构也随之调整,按领域划分,合并同类项。
组织结构和系统架构互为促进互为掣肘,技术团队组织结构的调整,伴随着技术体系和整体架构向前演进或倒退。
融入技术体系
随着各系统的交互越来越频繁,为了降低日渐增加的复杂度和冗余成本,百度云上的外卖系统最终完成了技术融合。
阿里巴巴
阿里云
基于多数据中心架构,技术团队在 2018 年国庆节期间,在云上拉起一个数据中心(第三个数据中心),并承担 30% 的日常流量,在发生故障转移的时候,能够承担 50% 的流量。为我们最终在云上构建所有数据中心,下线自建数据中心做准备。在上云过程中,业务开发团队主要的时间,是花在全链路的集成测试以及性能测试上,开发工作量不多。中间件团队前期的时间花在云上资源的申请、部署和初始化(需要的资源量巨大)、以及调度系统和云上调度系统的对接上。如果没有之前建设的多数据中心架构,迁移上云要多花数月甚至更长的时间成本,承担更大的稳定性风险。迁移上云后,通过压测以及午高峰的监控,我们发现服务间的 RPC 延时情况和自建数据中心有些不同,尤其是在午高峰的时候,一开始还是比较担心,平稳度过一周的午高峰后,基本确认各业务系统的弹性和容量冗余可以消化这些差别。
入“中台”
数仓和大数据工具第一时间融入了数据中台,这部分通用性较强,数据中台产品也很成熟,很顺利的完成了任务。上海大数据离线数据中心至此完成历史使命。搜索推荐、风控也相继根据自己的实际情况,分别融入主搜和集团风控引擎。在线业务系统层面,融入业务中台体系,是最深层次的融合,基于电商模式的架构,和基于本地化(Location Based Service)商业模式的系统架构,由于买家和卖家在地域上的关系、商家履约方式、物流履约及调度方式、商品品类和库存特征、交易履约生命周期、即时性要求的差别客观存在,并且如果还要做好故障隔离,故障发生时和电商分别独立采取容灾处理措施,对于外卖的在线系统融入到电商为主的业务中台而言是一个很有挑战的过程。
上云
辗转腾讯、百度、阿里多个云厂商,当时我们都是云上最大的(外部)业务方,还不包括少部分系统在上面的其他云厂商。5 年多里面,跟随着国内云厂商的成长,我们也从部分依赖云,到逐步采用云上体系构建所有系统。对于上云企业而言,表面上云的优势在于规模带来的平均硬件成本、维护成本的降低,但是,这不意味着云上搭建系统付出的费用,一定就比自建付出的硬件成本外加工程师的人员成本要低。成本里面影响的因素很复杂,比如某些具体云上产品的规模、成熟度、硬件成本等等。
业务系统从一开始就在云上构建,优势在于:
1、云厂商分布在各地的大小数据中心,有足够的容量短时间内解决业务方系统随需扩展的要求。相比单个数据中心带来的有限扩展性和容灾能力,是一个很大的提升。
2、云上 IaaS、PaaS 后面负责运营的工程师、架构师,集中了在各大小技术公司中,被各种线上事故反复毒打过的一批人。小到 MySQL、Redis 这类单个应用系统层面微观的基础设施,大到 Kubernetes 和 Istio 这类数据中心层面宏观的基础设施。业务方不需要再用事故来付高昂的学费锻炼出这样的团队,或者高昂的费用来招聘这类经验丰富的工程师,来搭建和维护自己的基础设施。当然,不是说云上的基础设施不会挂,挂还是要挂的(这里不是针对某一个云厂商),只是大概率他们有能力发现和恢复起来比我们自己维护的快。
3、伴随着技术的更迭和发展,系统实际上是越来越复杂的,云带来的便利,屏蔽了底层系统的复杂度,云的体系架构本身也在积极进化,业务系统能够随着云体系的进化,及时升级自己的底层架构体系。
但是不意味着业务系统上云一劳永逸,从传统的架构体系,到 cloud ready,再到 cloud native ,业务系统还有很多事情要做,其中重要的几件事:
1、Disposable 和 stateless 的系统,可重入和幂等的设计。基于容器化体系构建的数据中心,接纳不确定性,注重快速恢复,要求应用基于 fail gracefully & rapidly, start rapidly 这类反脆弱的原则设计。从单机操作系统演变到云上操作系统,系统向分层越来越多,越来越复杂的趋势演变,复杂也带来了更多脆弱性,基础设施局部的系统抖动(transient fault)不可避免,对业务系统的自愈能力也提出了要求。
2、可替换的基础设施和快速搭建的业务系统。基于标准化的基础设施(IaaS或者PaaS)构建自己的系统。标准化的基础设施意味着,无论在自己的数据中心还是公有云上,业务系统都不需要改造可以快速搭建、拉起完整的链路。这里的“标准”,指的是业界大多数厂商采用的事实标准(比如 Kubernetes 在容器调度领域的地位,它开放的 CRI/CNI/CSI 接口),或者待形成的标准(比如在 service mesh 里面力争成为标准的 SMI ,虽然对面有 istio 这个庞然大物)—— 这需要技术团队的评估和判断。这也是依赖接口(标准)而不是实现(产品)的设计原则体现。客观上也达到一个效果 —— not locked in。
3、当业务达到一定体量,系统架构上,在数据中心维度可水平扩展是一个必然的发展阶段,只有这样,才能充分利用云厂商分布在全国甚至全球的数据中心节点,发挥容量弹性优势,并享有随之而来的容灾能力。
探索和尝试
治理
业务架构治理
我们在梳理一个业务领域的架构的时候,通常只能保证梳理的那一刻相对较新。但是随着时间的推移,架构图很容易过时,业务系统在迭代,那么系统的架构图,无法及时同步,下次梳理又需要投入大量的精力。领域服务之间的关系、核心流程的时序图,都是如此。监控、全链路压测所关注的关键路径,都是靠人工梳理。实时性和准确性都可能存在问题,有时候靠的是经验,有时候要翻代码,更多的是看监控识别上下游,才理得清楚。那最新的架构怎么获取?架构图的数据源都在实时监控系统里。
所以,我们尝试了从监控数据里面入手,关键路径的入口通常是比较明确的(比如订单提交的 RESTfulAPI ),从入口开始,可用从监控系统中拉出一次请求的调用链,拿到某个入口API的所有请求后,借助大数据平台的算力,基于一些规则可以通过离线计算,把该入口的所有调用链收敛到的几个重要分支——通常都和业务场景有关。基于的规则之一,就是遇到逻辑分支的时候,关键路径所在的分支通常调用量都更大。收敛出来的几个调用链模型大概率就是大家所关注的关键路径。据此生成关键路径的时序图,业务团队补充时序图的元数据,比如场景描述,不可降级的节点,等等,那么可以迭代的架构信息初始化就完成了,后续,只要系统定期从监控数据里面刷新调用链模型,就能够反馈出来关键路径时序图的变化。
基于这个思路,服务之间的依赖关系,到服务所在系统之间的依赖关系,再到各业务领域之间的依赖关系,都可以动态的更新。架构体系的梳理,基于一个产品化的工具体系来运营,架构图也不会随着时间推移腐化掉,我们手里就能保持有一张最新的系统“地图”。
中间件治理
其实我们做了很多中间件治理产品,缓存、代理、消息队列、soa 服务等等。但是,遗憾的是,有些工具的使用视角是中间件开发团队,另一方面,这些控制台是一个个孤岛,没有关联起来,形成一个平台。关于这个平台的设想,是从一个应用的 owner 为视角展开应用治理,比如一个服务类应用开发工程师,进入这个系统,可以看到这个服务的健康状况总览,监控数据,依赖的上下游服务和中间件元数据,查看并实时变更这个服务的一些配置信息——比如限流阈值、熔断状态、降级开关、黑白名单、多数据中心的容量管理等等,进一步,可以申请资源,实施灰度上线策略,蓝绿发布,在一个平台内解决,而不是切换多个系统完成。
这个产品之下,依赖的是各个中间件提供的相互协作的接口来完成。围绕着一个应用,能看到这个应用在全局内的全景信息,应用 owner 通过自助完成从资源申请、系统上线、运营治理、健康监控、下线回收,整个应用生命周期的管理。
多泳道
随着系统的复杂性和稳定性要求的提高,同一个领域服务,在不同场景内,会被不同的上游系统依赖,而这些场景的的重要性、安全性、 SLA 要求、请求的流量特征都是完全不同的,出于隔离的需要,越来越多的业务开发团队开始采取单应用多集群的方式部署,以应对来自不同场景的调用方。随着业务上这类诉求的增加,建设多泳道的议题被提出来,我们曾经规划过三个泳道:
1、金丝雀:部署在这个泳道内的集群,承担的是系统全量上线前,或者全面启动灰度前的流量,通常,一个产品,想让自己的员工先“吃狗粮”,也可以先部署到这个泳道。这个诉求最强烈,现在已经通过“Pre-ProductEnvironment”的建设,部分落地。容易被忽视的是,这个泳道也是生产环境,需要避免被滥用而引发事故。出于成本和复杂度的考虑,数据存储很难做到彻底隔离。
2、在线服务:承担着核心的在线流量,例如用户端、商户端、开放平台、骑手履约这些关键路径上的应用场景,都在这个泳道内处理。
3、离线系统:一些公司内运营人员使用的管理台、离线的 scheduled job 所依赖的服务在这个泳道内部署独立集群。那么一些运营操作、离线 job 对线上系统的影响可以一定程度上隔离。这个泳道现在并没有从在线服务中分离出来。
云上的迭代
平台化建设
先来看看物流运单履约的一个初步规划讨论稿,这张粗颗粒度的图只是为了完整说明尝试平台化建设的初衷举的一个例子,忽略了很多细节,不完全反映现状。
从上图我们看到,物流运单履约相对稳定的运单核心领域逻辑,和易变的业务逻辑,边界还是比较分明的——变化一个来自于运力侧(众包、专送、第三方等等)、一个是来自于入单侧(外卖、零售、跑腿、开放平台等等),我们关注的重点是,怎么在保障核心领域逻辑的稳定性同时,还能灵活应对随时发生变化的业务需求,保证交付效率,并且有充分的隔离。
上云以后,我们开始尝试在 Kubernetes 体系下,实现我们灵活扩展和充分隔离的诉求。利用 pod ,把稳定的核心领域逻辑和可变的业务逻辑通过主容器和辅助容器部署在一起,但是又保持独立性,随需而变的业务逻辑能够和相对稳定的领域逻辑分开独立发布、变更(需要对 kubernetes 做一些扩展)。除了用 sidecar 来解耦以外,还有另一个思路,就是平台定义 SPI ,接入方基于 FaaS 实现自己的逻辑。
这个体系下,当有新的上游入单来源或者下游其他运力入驻的时候,运单中心核心的领域逻辑不变,代码不需要更改,接入方只需要遵循运单中心定义的接入点,双方的发布、变更互相独立。灰度阶段,如果其中一个运力系统出问题,也不会影响到已有的其他运力体系。以上,基于我们现有的技术体系,从开发、部署、运行整个流程的 POC 已经跑通,基于 FaaS 的模式没有尝试。如下图所示:
Service mesh
上云之前,自建的数据中心里是基于 swarm 调度。sidecar(sam)和业务系统部署在一个 container 里面,这个毕竟还是违背容器单进程模型的。
前面的内部架构图上我们可以看到,内部服务实例上运行着包括 SOA 框架在内的很多 sdk 和引擎,上云之后,基于 kubernetes ,通过把 sidecar 容器注入到业务容器所在 pod 里面,可以将这些逻辑从 SDK 迁移到 sidecar(sam)里面(类似 Mecha 的机制,比如微软的 dapr),进一步丰富 service mesh data plane 的能力,多技术栈和框架 SDK 升级困难的难题也能够得以解决。
多数据中心的路由策略,数据中心(可用区)封闭策略,跨数据中心访问代理,包括多泳道建设,data plane 都可以充当核心的角色。Istio 很强大,但是大型系统成功案例不够丰富,再加上和 CNCF 渐行渐远,可以先观察,也可以选择适合自己的轻量级 mesh 方案,比如基于 SMI:微软的 OSM , containous(traefik背后的公司)的 maesh 等等,都提供了另一种思路。
结语
以上就是饿了么技术体系随着业务成长,从日均百万到千万单量的迭代轨迹。
架构是规划出来,还是演进出来的?至少从上面的历程来看,我们的演进多于规划。虽然像多数据中心这类架构,是有业务需求支撑,自上而下规划的,但是,在实践过程中,不论是业务系统设计原则,还是核心的中间件,在很大程度上已经具备了支撑多数据中心的条件,这是演进的结果,否则,我们也不可能在这么短的时间建成多数据中心的架构。
后续全面上云这一个“规划”是顺理成章的事情,因为这个时候的架构已经 cloud ready 。“Uncle Bob: Architecture is about intent, not frameworks” —— 从我们架构的演化历程来看,为解决一个个现实的问题提出的解决方案,不是为追逐“流行”而构建,更不是源自权威的指令。架构的规划在这里面的作用,更多的是在发现问题和解决问题的过程中,借鉴业界的实践经验来推进整体迭代,约束设计原则。
在计算机工程领域,大多数情况下,遇到的问题都是有前人的经验可以借鉴的,指导原则也是现成的。我们的架构演进,包括多数据中心建设,如果没有左耳朵耗子还有毕玄这些前辈的指导,也不可能那么顺利。
还有一个关键的因素就是团队,技术心态是不是开放包容的,技术栈构成是不是多样性的,工程师文化是不是这个团队文化的一部分,也一样影响着架构的现状和未来演进。自始至终,真正引导我们解决问题,潜移默化的推进我们架构演进的,一直是一线工程师,有幸在这个团队和大家并肩作战。
补充说明:本文所描述的是截至2019 年前后的架构体系。进入 2020 年前后,伴随着组织结构调整,中间件替换,业务系统入集团中台的需要等等,很多核心系统已经重写或者在重写中,架构及其演进方向也已经发生了变化。