编者按:本文由一乐在高可用架构群分享,转载请注明来自高可用架构「 ArchNotes 」。

  煎饼的故事

  有一段时间住在花园路,最难忘的就是路边的煎饼果子。老板每天晚上出来,正好是我加班回去的时间。

  一勺面糊洒在锅上,刮子转一圈,再打一个蛋,依然刮平。然后啪的一下反过来,涂上辣酱,撒上葱花。空出手来,剥一根火腿肠。最后放上薄脆,咔咔咔三铲子断成三边直的长方形,折起来正好握在手中。烫烫的,一口咬下去,蛋香、酱辣、肠鲜,加上薄脆的声音和葱花的惊喜,所有的疲劳都一扫而光。

  

  这种幸福感让我如此迷恋,以至于会在深宅的周末,穿戴整齐跑出去,就为了吃上一个。也因为理工科的恶习,我也情不自禁地开始思考这份执迷的原因,直到最后,我发现了它的秘密。 作为街头小吃的杰出代表,能够经历众口的挑剔而长盛不衰的秘密是什么?

  所有的一切全因为其模式。

  而这模式与大多数互联网服务的架构如出一辙,那就是分层架构。

  分层的设计意味着,每一层都独立承担单一的职责。

  这在根本上降低了制作的难度。做饼的时候专心控制火候,做酱的时候专注在味道。每层职责的单一化也让优化变得简单,因为它是自然可伸缩的。你要是想多吃点蛋就多加一个,你要多吃点肠就多加一根,完全取决于你的胃口。

  它又是可以扩展的。你可以不要蛋,你可以加根肠,你可以不要薄脆,你可以加上辣酱。而且每一层又是可定制的。葱花可以少一点,辣酱可以多一点,肠可以要两根,鸡蛋可以加三个。 你也可以把面饼换成面包,把鸡蛋换成煎蛋,把辣酱换成甜酱。你已经知道这是什么了吧?是的,你好,这里是赛百味,请问你要什么口味的三明治?

  煎饼果子和三明治,其实本质上是相通的。而加一个蛋更香,也不是因为对蛋的追求,在根本上是因为煎饼果子模式的强大。

  因为这种模式,一千个人可以有一千种煎饼果子。

  而有了对模式的理解,对小吃的评估也就变得更加容易。比如肉夹馍只有馍和肉,二维切换单调不可长久;比如烤冷面,干脆就是满嘴的热烈混在一起,没有煎饼这样的表现层,整个面都散发着原始的不讲究。

  这种对比上的简化,也让我们有了新的选择,保存宝贵的精力,并且可以随时放弃对细节的追究。

  

  就像在我们谈论女人时,

  (抱歉博主是男人)

  我们在意胸、

  在意腿、

  在意风情、

  在意温柔,

  因为女人是有区别的。

  当我们欣赏电影的时候,

  我们在意男人、

  在意女人、

  在意老人、

  在意孩子,

  因为角色是有区别的。

  当我们走在路上的时候,

  我们在意行人、

  在意车辆、

  在意商店、

  在意餐厅,

  因为物体是有区别的。

  我们在设计和优化系统的时候,其中的每个服务都是自行运转,做着自己份内的事,但是在不同的维度里,作用却变得不尽相同。

  这也是我们讲优化要分层次和级别,架构、算法、库和 OS,而讲架构的时候,我们首先讲的是整体的模式,然后是具体的权衡,实现的细节则是最不重要的。

  架构的模式

  谈起这个,是因为 Mark Richards 写了一本架构模式的书《Software Architecture Pattens》。

  书中总结对比了五种模式的优缺点,包括了 Layered、Event-Driven、Microkernel、Microservices、Space-Based。

  书写得简单精致,推荐大家去阅读,地址见文末。

  还有一种模式,因为在越来越多的系统中用到,是书中没有的(与 Space-Based 有所区别),但我觉得也有必要专门介绍下。我们开始在群发系统中实现,后来的抢购、红包和火车票的场景中也屡屡看到它的身影。

  2013 年的时候,我们在微博做粉丝服务平台,一个类似微信公众号的群发系统。然而比后者更困难的是,当时在产品设计上并没有像微信一样新建用户体系,而是直接基于微博的粉丝关系,这就意味着一篇文章要能能在很短时间内支持亿级的用户推送。这个数量级的订阅用户,即使看今天的微信公众号依然是难以想象的。

  当时有一套老的群发系统,都是基于 MySQL 的收件箱设计,在更换了 SSD 硬盘,又批量化数据库操作之后,整体写入性能依然只在每秒几万的级别,这就意味着一亿用户只能在 17 分钟内发完,我们意识到这套系统需要进行重新设计。

  最终我们我们使用了一种新的架构方式,达到了每秒百万级别的速度,而且还可以更高。这种模式就是单元化架构。

  下文介绍我参照了架构模式的说明方式,希望能够让大家有个对比,喜欢你可一定要说好!

  单元化架构

  如前所述,我们选择单元化的一个重要目的是为了性能,为了极高的性能。这比起一般的分层架构来讲,会获得更经济的结果,但也因此,牺牲了分层架构的一些特性,因为它的容量取决于单元的大小(关于单元等名词介绍,我会在下文介绍)。

  虽然它支持按照单元扩容,但在单元内基本上每层的性能都是固定的。这更适合容量可预期的场景,比如大多数已经趋于稳定的业务。像前面的粉丝服务平台,虽然他下发消息量级巨大,但是在整体层面,使用平台的用户由于是 VIP 用户,其规模基本在在数百万级别,而粉丝量级也不太可能过亿。

  重要的是,基于当时的业务数据,我们已经知道平均粉丝数在什么量级。而业务数据是架构选型的重要依据。商品秒杀、火车抢票等等都是一样。

  当然也有例外。因为单元化架构作为一种思想,它不会局限在一台机器,一个机架,它也适用一个机房。当它的层次变大时,单元内自然就可以有变化的空间。每一层服务都可以分开伸缩。而到了这个层面,它的追求可能就完全不一样了。像阿里的双十一服务改造,会为了流量的分离,像 QQ 的聊天,会为了接入的速度。它们的基本思想是一致的。

  至于单元化架构和煎饼果子的关系,我会在文后回答。

  

  核心概念 Key Concepts

  分区(Shard)是整体数据集的一个子集。如果你用尾号来划分用户,那么相同尾号的用户可以认为是同一分区。

  单元(Cell)是满足某个分区所有业务操作的自包含的安装。我们从并行计算领域里借鉴了这个思想,也就是计算机体系结构里的 Celluar Architecture,在那里一个 Cell 是一个包含了线程部件、内存以及通讯组件的计算节点。 https://en.m.wikipedia.org/wiki/Cellular_architecture 单元化(Cellize)这是我的自造词,描述一个服务改造成单元架构的过程。

  模式描述 Patten Deion

  单元架构最重要的概念,就是单元和单元的自治。

  你可以将其想象成细胞,如之前所述,每个细胞都是自成一体,功能明确。你也可以将其想象成小隔间,就像你去了一个按摩院,每个隔间里都有技师和所有设备。我没有用前面那个名字,因为其有太强的生物学含义,也没有用后者,因为其有太多的服务性暗示。但是如果你有足够的想象力,其实什么名字都可以的。

  说到单元的自治,即单元的自我协调和之间的隔离。单元既然做到了自包含,那么其中的所有组件,不管是否在物理上分离成了独立的服务,都是在一个单元内互相支持的,也就是跟其他单元内的同类和非同类组件都不会有任何交流。这也是跟基于空间的架构的重要区别,后者的处理单元之间还是会互相通信并同步信息。

  这里的挑战就在于分区的算法。一个单元内的组件会很多,如果业务复杂,涉及到的数据也会很多,为了隔离,每一个组件都要能按照同样的算法进行分区。

  本质上每个单元都是相似的,单元之间的区别或者取决于请求,或者取决于数据。而且越到大的层面,区分度越低,用户甚至是可以在不同单元间漫游的。

  模式动力学 Patten Dynamics

  单元架构的最典型目的,还是为了极高的性能,为了获得经济的高速度。这里一方面,是因为我们发现其他架构实际上是浪费了很多资源,每一层服务都运行在单独的操作系统上,而且都要通过局域网或者城域网中转。

  与此同时,传统的互联网服务还是希望用一堆计算能力普通的节点来服务大量用户,而随着摩尔定律的推进,单机性能越来越高,网络通讯的成本随之变得耗费显著。这使得我们有机会也有动力在垂直方向进行扩展。

  当你把更多的组件放在同一个地方的时候,你也在物理上获得了计算本地化的优势。这是我们获得性能提升的根本原因。

  服务分成了很多单元,但总要跟外界通讯,这个事情是交给协调者 Coordinator 的。你可以在内部增加存储、缓存,增加队列和处理机,这些所有不交互的组件,理论上都不是外部资源可以访问的。

  前面我们提到,单元化过程也是分区算法的应用过程。而这个分区算法放在哪里就是个问题。

  我们可以封装运行库交给客户端,也可以做个代理层,内置算法。也有一些服务因为业务需要,请求需要复制到每个单元去。这就是典型的 Scatter-Gatter 模型,那么你还可能需要一个作业管理系统。这些都是可选择的使用方式。

  模式分析 Patten Analysis

  总体敏捷度低,易部署性低,可测试性高,性能高,伸缩性高,易开发性低。

  基于篇幅原因,不再详述每一个方面,相信大家都能自行分析。唯一需要强调的是运维要求比较高。

  单元化之后,所有的服务放在一起,在请求失败的情况下需要快速定位某个单元,这跟分层排除的思路是不一样的。如果运维团队不够高效,面对这样集群数量的暴涨(每个单元的服务数量相当于原来一个集群的服务数量),有可能是会被大量的工作压垮;如果运维团队分离比较明显,每种组件都是专门的团队来维护(这是我们在微博遇到的),那就会有排异反应的风险,因为每一个团队都有自己的权限和服务管理习惯,这里需要相当的协调工作来防止相互干扰。

  后记

  前面留的一个问题,煎饼果子跟单元化架构的关系。答案说起来很简单,你问问煎饼摊就知道了。

  煎饼是分层的,煎饼摊是单元的。消息发送服务是单元的,但是索引维护是分层的。看模式要确定系统的范畴,从不同角度看,同样的东西是有不同意义的。这也是架构师要做的思考。 其实 IT 系统千百万,模式肯定不会止于这几种。但有了基础的模式,了解它们之间的相似和区别,对于我们设计自己的系统,思考其中的权衡都是有帮助的。

  Q & A

  1. 单元化设计与 Docker 的容器化思路是否相通?又有何差异呢?

  一乐:应该是关注的点不一样,但不冲突。Docker 一般是在微服务架构下会使用的措施,但并不意味着不能用在其他架构上。一个单元内的各种组件,使用什么样的技术,都是新的选择。用 Docker 不错。

  2. 对于分布式服务,单元化架构可能会带来数据一致性问题,这个一般如何解决?

  一乐:可能我没有理解你场景,单元化一般不会带来一致性问题。因为 Sharding 之后,一块分区数据相当于完全属于一个单元,其他单元是被隔离访问的。

  3. 单元化架构最小情况是单台机部署整体服务,资源方面如何规划?

  一乐:这方面就要计算了,也就是进行容量规划,相信大家在这方面都很熟悉。一个需要注意的点是,单元化的架构应用在可预期的总体容量上时会省很多事。鉴于分区算法的固定,扩容方面,其实可以通过预先规划,在单机上再进行多单元混部的方式。

  4. 单元与 app cluster(总服务器)之间是怎么进行关联的,是通过注册服务还是按照 hash 分派的,如果是 hash 分派,那么挂了怎么接回的,如果是注册服务,是怎么对应分派服务呢?

  一乐:简单做就是 hash 分派,高级点就是注册服务,可以直接参照成熟的 Sharding 算法。任务只要到了单元内,就是单元自己的事了。如果你的 Job 有阶段性,可能要考虑 Job 状态记录以及请求处理的幂等性

  5. 你觉得单元化架构的问题是什么?如果让你重新设计你会做哪些改进?

  一乐:这个问题太聪明了,谢谢!单元化架构的问题,如之前所说,有一个扩容难题,有一个运维难题,有一个单点问题。扩容问题刚才说了,在三年前我们还没有 Docker 的时候,我们的服务隔离难度很大,现在已经今非昔比。一刀(Docker)在手,天下我有!单元的单点问题,你得考虑单元级别的主从同步,这方面常见的互联网技术就行。

  6. 分布式架构经常讲究服务器无状态,这样可以单台服务器异常对整体服务无影响。但单元化意味着服务是有状态,如何保障高可用?

  一乐:无状态只能是业务层,涉及到数据的不会无状态,因为数据就是状态的记录。保障高可用嘛,前面说了,先把主从做了吧。

  7. 单元细胞内依然采用分层架构设计还是 ALL IN ONE 即可?

  一乐:在讲单元化的时候,其实不要求单元内的组织模式,所以回答是都行。

  8. “每秒百万级的推送” 除了采用这种单元化的架构模式使之成为可能对于基础的中间件如 队列 db 等的架构如何规划的?还要额外考虑哪些技术点或问题?

  一乐:队列主要用来防峰,在高速服务里,如果再想扩容,肯定先走批量的路子。db 的规划其实是重点,单元化架构实际上算是以数据为中心的一种模式。额外的考虑其实跟之前都很像,不过要做一些特殊的处理,比如冷热数据的分离。