微信号:infoqchina

介绍:有内容的技术社区媒体

从金拱门餐厅联想到的分布式系统设计思维

2017-11-09 08:00 周明耀
作者|周明耀
编辑|小智
其实在生活中,大到组织架构,小到琐碎日常,都能学到一些经验、总结出一些知识。共性往往隐藏在特性之中。你需要做的,可能仅仅只是细心观察而已?

前几天中午开会开到 12 点半,错过了食堂吃饭时间,只能大家一起去麦当劳吃午饭。写这篇文章,既是有感于麦当劳餐厅的管理模式,也是由于这次就餐在订餐、取餐过程中发生了一些小事,让我联想到了分布式软件设计,我在 InfoQ 开设技术专栏之初就说过技术来源于生活细节,那么今天就来系统性谈谈由麦当劳餐厅(这里并不特指麦当劳,我相信也有类似的其他店家,这里只是以麦当劳作为举例)所想到的分布式系统设计思维及问题。

结合现阶段的观察,我有以下观点:

  • 店长负责制 ->主从模式(Master/Slave);

  • 订单处理方式 ->两阶段提交;

  • 员工角色拆分 ->微服务设计;

  • 多个任务接收 ->微服务设计之后的服务横向扩展;

  • 订单处理过程上屏 ->任务队列设计;

  • 座位设计模式 ->容器化管理。

我逐一聊聊我的看法,欢迎您留言指教。

店长负责制

每家麦当劳都会有一名当值经理,这位经理负责当前整个店的运营,如果遇到某个岗位繁忙的情况,那么他会临时调动其他相对空闲的员工支援,那么这就是典型的 Master/Slave 架构。

我们来看看较为典型的 Matser/Slave 架构设计。以 HBase 为例,看看 HBase 的 RegionServer 设计方式。在 HBase 内部,所有的用户数据以及元数据的请求,在经过 Region 的定位,最终会落在 RegionServer 上,并由 RegionServer 实现数据的读写操作。RegionServer 是 HBase 集群运行在每个工作节点上的服务。它是整个 HBase 系统的关键所在,一方面它维护了 Region 的状态,提供了对于 Region 的管理和服务;另一方面,它与 HMaster 交互,上传 Region 的负载信息上传,参与 HMaster 的分布式协调管理。

HRegionServer 与 HMaster 以及 Client 之间采用 RPC 协议进行通信。HRegionServer 向 HMaster 定期汇报节点的负载状况,包括 RS 内存使用状态、在线状态的 Region 等信息。在该过程中 HRegionServer 扮演了 RPC 客户端的角色,而 HMaster 扮演了 RPC 服务器端的角色。HRegionServer 内置的 RpcServer 实现了数据更新、读取、删除的操作,以及 Region 涉及到 Flush、Compaction、Open、Close、Load 文件等功能性操作。

Region 是 HBase 数据存储和管理的基本单位。HBase 使用 RowKey 将表水平切割成多个 HRegion,从 HMaster 的角度,每个 HRegion 都纪录了它的 StartKey 和 EndKey(第一个 HRegion 的 StartKey 为空,最后一个 HRegion 的 EndKey 为空),由于 RowKey 是排序的,因而 Client 可以通过 HMaster 快速的定位每个 RowKey 在哪个 HRegion 中。HRegion 由 HMaster 分配到相应的 HRegionServer 中,然后由 HRegionServer 负责 HRegion 的启动和管理,和 Client 的通信,负责数据的读 (使用 HDFS)。每个 HRegionServer 可以同时管理 1000 个左右的 HRegion。

如果我们需要把固定店长负责制改为全员负责制,有没有类似案例?有,Apache Cassandra 就没有固定的中心节点(无中心化设计),只有协调者节点(理论上所有节点都可以作为协调者,每次请求有且仅有一个),那么它是怎么做到的呢?

首先我们来看看 Gossip 协议。Cassandra 集群中的节点没有主次之分,通过 Gossip 协议,可以知道集群中有哪些节点,以及这些节点的状态如何等等。每一条 Gossip 消息上都有一个版本号,节点可以对接收到的消息进行版本比对,从而得知哪些消息是我需要更新的,哪些消息是我有而别人没有的,然后互相倾听吐槽,确保二者得到的信息相同,这很像现实生活中的八卦,一传十,十传百,最后尽人皆知。在 Cassandra 启动时,会启动 Gossip 服务,Gossip 服务启动后会启动一个任务 GossipTask,每秒钟运行一次,这个任务会周期性与其他节点进行通信。回到麦当劳餐厅,是不是每个员工之间也需要有对讲机,实时互相告知目前自己的工作状态和订单信息,这样才能做到无固定店长管理制?

既然 Cassandra 是无中心化的,那么如何来计算各个节点上存储的数据之间的差异呢?如何判断自己申请的那一份数据就是最新版本的?这里我要引出 Snitch(告密者)机制了。

现实生活中的“告密者”会向别人告密谁有需要寻找的东西,Snitch 机制在 Cassandra 集群里也起到了这样的作用。Snitch 机制的功能是决定集群每个节点之间的相关性(值),这个值被用于决定从哪个节点读取或写入数据。此外,由于 Snitch 机制收集网络拓扑内的信息,这样 Cassandra 可以有效路由各类外部请求。Snitch 机制解决了节点与集群内其他节点之间的关系问题(前面介绍的 Gossip 解决了节点之间的消息交换问题)。当 Cassandra 收到一个读请求,需要去做的是根据一致性级别联系对应的数据副本。为了支持最快速度的读取请求,Cassandra 选择单一副本读取全部数据,然后要求附加数据副本的 hash 值,以此确保读取的数据是最新的数据并返回。Snitch 的角色是帮助验证返回最快的副本,这些副本又被用于读取数据。回到麦当劳餐厅,如果互相都知道了谁有哪些产品,那么完成订单配单步骤就不难了。

正是有了 Gossip 协议和 Snitch 机制,才在通信层基本满足无中心化的设计,当然还有很多其他因素一起参与,这里不逐一介绍。

订单处理方式

麦当劳和其他商家一样,都在追求每日最大订单处理量,你懂的,更多的订单意味着更多的利润。为了加快销售速度,麦当劳采用了异步处理模式。当你在收银台下单后,收营员会进行下单,下单过程结束后你就进入到了制作订单队列。有了这个队列,营业员和制作员之间的串行耦合方式被解耦了,这样营业员就可以继续接收订单,即便制作员手上的订单已经开始排队了也没关系。店里忙的时候,会动态增加几个制作员,他们之间本身也处在一个竞争环境下,就好像分布式环境下会存在多个计算节点一样。因此,麦当劳的这种制作过程,本质上就是一个分布式处理过程:订单队列属于中心节点的待运行任务队列,每个制作员是一个计算节点,无论采用申请还是被分配策略,他们都可以正常干活。这让我联想到了两阶段提交,为什么麦当劳不采用两阶段提交协议呢?

以上我介绍的所有这些策略都不同于两阶段提交,什么是两阶段提交?它是分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。

两阶段提交协议一般将事务的提交过程分成了两个阶段:

  • 提交事务请求

  • 执行事务请求

核心是对每个事务都采用先尝试后提交的处理方式,因此它是一个强一致性的算法。

虽然说两阶段提交算法具有原理简单、实现方便的优点,但是它也有一些缺点:

  • 同步阻塞:所有参与该事务操作的逻辑都处于阻塞状态;

  • 单点问题:协调者(营业员、订单排队队列)如果出现问题,整个两阶段提交流程将无法运转,参与者的资源将会处于锁定状态;

  • 数据不一致:协调者向所有的参与者发送 Commit 请求之后,由于局部网络问题,会出现部分参与者没有收到 Commit 请求,进一步造成数据不一致现象。麦当劳的例子中,如果有多个队列排队时,会存在问题;

  • 太过保守:参与者出现异常时,协调者只能通过其自身的超时机制来判断是否需要中断事务,即任意一个节点的失败都会导致整个事务的失败。

两阶段提交需要依赖于不同的准备和执行步骤。在麦当劳这个例子里面,如果采用两阶段提交,顾客需要在收银台等待食物制作完毕,他得自己拿着钱。然后,钱、收据、食物会做一次一手交钱,一手交货的交换。营业员和顾客在交易完成前都不能离开。两阶段提交可以让生活更加简单,但是也会伤害到消息的自由流通,因为本质上它是基于各方之间的有状态交易资源的异步动作。

员工角色拆分

如果你去小型的快餐店,你会发现只有 1 名员工,他既要负责下单、收银,也要负责事物制作、打包、售后,这样自然速度就慢了。麦当劳把员工分为了几个不同的角色,这些角色在干活的时候互相不干扰,通过通信机制(订单系统信息投影屏幕)方式协同工作,这样可以最大化生产效率。即使出现意外情况,例如某位员工中暑了,店长可以立即协调一名员工补上他的位置,而不需要和其他角色的员工产生交互成本。

这样的设计方式和微服务架构比较类似,下面这张图是一个典型的传统单体型服务架构模式:

单体型架构比较适合小项目,缺点比较明显:

  • 开发效率低:所有的开发在一个项目改代码,递交代码相互等待,代码冲突不断

  • 代码维护难:代码功能耦合在一起,新人不知道何从下手

  • 部署不灵活:构建时间长,任何小修改必须重新构建整个项目,这个过程往往很长

  • 稳定性不高:一个微不足道的小问题,可以导致整个应用挂掉

  • 扩展性不够:无法满足高并发情况下的业务需求

微服务是指开发一个单个小型的但有业务功能的服务,每个服务都有自己的处理和轻量通讯机制,可以部署在单个或多个服务器上。微服务也指一种种松耦合的、有一定的有界上下文的面向服务架构。也就是说,如果每个服务都要同时修改,那么它们就不是微服务,因为它们紧耦合在一起;如果你需要掌握一个服务太多的上下文场景使用条件,那么它就是一个有上下文边界的服务。

经过微服务架构的拆分,上面描述的单体型架构成了下面这张微服务架构图:

多个任务接收

你会发现,为了加快订单接收速度,同时又要在有限的营业窗口的限制的前提下加快速度,怎么办?麦当劳引入了自动订餐机器,这样就允许客户通过触摸式接触屏点选自己需要的食品,可以任意组合,然后点击下单,通过“微信、支付宝”等手机支付方式正式下单,接着你的订单就进入到了统一的任务排队队列,这和通过点单员下单是完全一样的。

由于有了微服务架构的支撑及通信框架的交互设计,点单这个工作变成了一个完全独立的工作,如图所示:

横向可扩展能力是微服务化设计之后很容易实现的功能,可以通过服务发现方式实现各个进城之间的状态信息交互,这里就不多介绍了。

订单处理过程上屏

订单下单后,我们的订单就进入到了麦当劳的任务排队队列,他们家的排队队列分为两个,分别是待完成队列、已完成队列,如图所示:

这种队列设计是分布式计算应用领域常用的设计和处理方案,看似简单,其实相当复杂,内容处理过程容易出现很多问题,我这里根据那天等待取餐时发生的几件小事,注意聊聊出现的几个异常情况:

异常数据干扰

我们在排队取餐,由于取餐是通过一名员工人工喊话的,她既要喊人取餐,又要处理各种售后请求(例如拿番茄酱、询问订单情况等等),这样她很容易被干扰,进而出现手忙脚乱的情况。联想到分布式环境下,如果我们不对任务的信息进行校验,那么很容易混入异常任务(可能是任务信息被截断了,也可能是非法攻击),对于核心服务的保护是必不可少的,分布式环境下无论是否存在中心服务,我们都需要仔细思考各种异常保护,所以貌似流程、业务简单,其实并不是那么容易的。

排在前面的订单迟迟没有做完

如上面这张图所示,理想情况下是按照订单号逐一完成,这样也会让所有顾客都满意,先来先得嘛。但是实际情况是不是这样呢?我一位同事等了很久,原因是由于他的订单中有一样食物迟迟没有启动制作过程,这是因为调度服务内部经过性价比评估,发现只有他存在这个需求,所以一直在等待其他顾客点选相同产品后再一起制作,现在先把那些很多顾客都在等待的产品做出来,避免更多的人等待。回到分布式技术领域,对于这种情况我们其实也在一直遇到,比如先下派资源需求大的任务,还是先下派小的?这就相当于先让大块的石头填满盒子,然后让小块的石头塞入细小的缝隙。所有设计都是需要根据实际业务情况和实际测试得出的,而不是拍脑袋。

订单完成前已经发货

我的那位同事,他等了很久还没有拿到事物,所以有点怒了。这时候负责取餐的员工随意给了他一份食品,但是没有标注正在排队的任务为“已完成”状态,相当于人为多复制了一个任务,一个任务变成两个任务了,数据库内出现了脏数据。等真正的任务完成时,由于无人取餐,导致陷入了死循环,负责取餐的员工一直在喊“1104 号顾客请取餐”,但是无人应答。

对于这类问题,在分布式环境下也是很容易发生的。例如,计算节点在执行任务过程中离线了,一直没有上报任务执行状态,中心节点认为它离线了,就把运行在它上面的任务标注为“未调度任务”,重新下派,这时候如果离线的计算节点又恢复了正常,那么就会有两个相同的任务同时运行。这种异常其实很难避免,它本身是分布式环境下任务容错的一种设计方案,需要整个数据流闭环内协同处理,特别是任务的下游系统尽可能参与。做好分布式系统,真不容易。

座位设计模式

由于快餐的就餐时间一般都较短,所以顾客流动速度很快,看着每个座位上的顾客频繁更换,让我联想到了 Kubernetes 管理容器的设计模式。Kubernetes 知道整个餐厅的可用资源(座位)数目及使用情况,每个座位上的顾客就是临时启动的一个容器,用完就销毁,资源释放后,其他顾客可以接着资源。

来看看 Kubernetes 的相关概念。Kubernetes 以 RESTFul 形式开放接口,用户可操作的 REST 对象有三个:

  • pod:是 Kubernetes 最基本的部署调度单元,可以包含 container,逻辑上表示某种应用的一个实例。比如一个 web 站点应用由前端、后端及数据库构建而成,这三个组件将运行在各自的容器中,那么我们可以创建包含三个 container 的 pod。

  • service:是 pod 的路由代理抽象,用于解决 pod 之间的服务发现问题。因为 pod 的运行状态可动态变化 (比如切换机器了、缩容过程中被终止了等),所以访问端不能以写死 IP 的方式去访问该 pod 提供的服务。service 的引入旨在保证 pod 的动态变化对访问端透明,访问端只需要知道 service 的地址,由 service 来提供代理。

  • replicationController:是 pod 的复制抽象,用于解决 pod 的扩容缩容问题。通常,分布式应用为了性能或高可用性的考虑,需要复制多份资源,并且根据负载情况动态伸缩。通过 replicationController,我们可以指定一个应用需要几份复制,Kubernetes 将为每份复制创建一个 pod,并且保证实际运行 pod 数量总是与该复制数量相等 (例如,当前某个 pod 宕机时,自动创建新的 pod 来替换)。

可以看到,service 和 replicationController 只是建立在 pod 之上的抽象,最终是要作用于 pod 的,那么它们如何跟 pod 联系起来呢?这就要引入 label 的概念:label 其实很好理解,就是为 pod 加上可用于搜索或关联的一组 key/value 标签,而 service 和 replicationController 正是通过 label 来与 pod 关联的。如下图所示,有三个 pod 都有 label 为"app=backend",创建 service 和 replicationController 时可以指定同样的 label:"app=backend",再通过 label selector 机制,就将它们与这三个 pod 关联起来了。例如,当有其他 frontend pod 访问该 service 时,自动会转发到其中的一个 backend pod。

技术来源于生活,我希望自己能够从生活中感悟出更多的技术设想,每天的思考肯定有利于自己的技术进步,与君共勉!

今日话题

在你的日常生活中,有没有哪些从前忽略的小细节,意外地和软件开发领域的经验有异曲同工之妙的?

快快留言告诉我们吧!

作者介绍

周明耀,2004 年毕业于浙江大学,工学硕士。13 年软件研发经验,近 10 年技术团队管理经验,4 年分布式计算、大数据技术经验。出版书籍包括《大话 Java 性能优化》、《深入理解 JVM&G1 GC》、《技术领导力 - 码农如何才能带团队》,个人公众号“麦克叔叔每晚 10 点说”出品人。个人微信号 michael_tec。

今日荐文

点击下方图片即可阅读

禅与互联网技术:龙泉寺的程序员们

小贴士

正如本文开头所言,生活中处处可见软件开发的思维,生活中也日渐充斥着 AI 技术带来的新变化。时下最火热的技术非 AI 莫属,那么,目前到底都有哪些 AI 落地案例呢?机器学习、深度学习、NLP、图像识别等技术又该如何用来解决业务问题?

AICon2018 全球人工智能技术大会上,我们邀请了来自 360、京东、腾讯、百度、Etsy、微软、饿了么、摩拜、搜狗、携程、微博等国内外知名企业的 AI 应用落地负责人,前来分享他们可供参考的最新 AI 落地案例和技术探索,或许可以给你一些启发,目前大会 8 折报名火热进行中,点击“阅读原文”了解详情!


 
InfoQ 更多文章 软件开发行业里,那些被视而不见的大问题 禅与互联网技术:龙泉寺的程序员们 为什么想学好人工智能,就一定要建立起「系统」的概念? 前端框架新选择——基于MVVM的San Q新闻丨阿里云占中国市场近五成份额;谷歌欲借TensorFlow重返中国;Kafka迎来1.0.0版本,正式告别四位数版本号
猜您喜欢 深入解析String中的intern EMC Mobile 3.2 操作演示视频 从线程中取得结果 福利来了!AngularJS资源大集锦 Awk课程系列