极客时间已完结课程限时免费阅读

45 | 弹力设计篇之“服务的状态”

45 | 弹力设计篇之“服务的状态”-极客时间

45 | 弹力设计篇之“服务的状态”

讲述:杨超

时长12:04大小11.03M

你好,我是陈皓,网名左耳朵耗子。
之前在我们讲的幂等设计中,为了过滤掉已经处理过的请求,其中需要保存处理过的状态,为了把服务做成无状态的,我们引入了第三方的存储。而这一篇中,我们来聊聊服务的状态这个话题。我认为,只有清楚地了解了状态这个事,我们才有可能设计出更好或是更有弹力的系统架构。
所谓“状态”,就是为了保留程序的一些数据或是上下文。比如之前幂等性设计中所说的需要保留每一次请求的状态,或是像用户登录时的 Session,我们需要这个 Session 来判断这个请求的合法性,还有一个业务流程中需要让多个服务组合起来形成一个业务逻辑的运行上下文 Context。这些都是所谓的状态。
我们的代码中基本上到处都是这样的状态。

无状态的服务 Stateless

一直以来,无状态的服务都被当作分布式服务设计的最佳实践和铁律。因为无状态的服务对于扩展性和运维实在是太方便了。没有状态的服务,可以随意地增加和减少节点,同样可以随意地搬迁。而且,无状态的服务可以大幅度降低代码的复杂度以及 Bug 数,因为没有状态,所以也没有明显的“副作用”。
基本上来说,无状态的服务和“函数式编程”的思维方式如出一辙。在函数式编程中,一个铁律是,函数是无状态的。换句话说,函数是 immutable 不变的,所有的函数只描述其逻辑和算法,根本不保存数据,也不会修改输入的数据,而是把计算好的结果返回出去,哪怕要把输入的数据重新拷贝一份并只做少量的修改(关于函数式编程可以参看我在 CoolShell 上的文章《函数式编程》)。
但是,现实世界是一定会有状态的。这些状态可能表现在如下的几个方面。
程序调用的结果。
服务组合下的上下文。
服务的配置。
为了做出无状态的服务,我们通常需要把状态保存到其他的地方。比如,不太重要的数据可以放到 Redis 中,重要的数据可以放到 MySQL 中,或是像 ZooKeeper/Etcd 这样的高可用的强一致性的存储中,或是分布式文件系统中。
于是,我们为了做成无状态的服务,会导致这些服务需要耦合第三方有状态的存储服务。一方面是有依赖,另一方面也增加了网络开销,导致服务的响应时间也会变慢。
所以,第三方的这些存储服务也必须要做成高可用高扩展的方式。而且,为了减少网络开销,还需要在无状态的服务中增加缓存机制。然而,下次这个用户的请求并不一定会在同一台机器,所以,这个缓存会在所有的机器上都创建,也算是一种浪费吧。
这种“转移责任”的玩法也催生出了对分布式存储的强烈需求。正如之前在《分布式系统架构的本质》系列文章中谈到的关键技术之一的“状态 / 数据调度”所说的,因为数据层的 scheme 众多,所以,很难做出一个放之四海皆准的分布式存储系统。
这也是为什么无状态的服务需要依赖于像 ZooKeeper/Etcd 这样的高可用的有强一致的服务,或是依赖于底层的分布式文件系统(像开源的 Ceph 和 GlusterFS)。而现在分布式数据库也开始将服务和存储分离,也是为了让自己的系统更有弹力。

有状态的服务 Stateful

在今天看来,有状态的服务看上去的确比较“反动”,但是,我们也需要比较一下它和无状态服务的优劣。
正如上面所说的,无状态服务在程序 Bug 上和水平扩展上有非常优秀的表现,但是其需要把状态存放在一个第三方存储上,增加了网络开销,而在服务内的缓存需要在所有的服务实例上都有(因为每次请求不会都落在同一个服务实例上),这是比较浪费资源的。
而有状态的服务有这些好处。
数据本地化(Data Locality)。一方面状态和数据是本机保存,这方面不但有更低的延时,而且对于数据密集型的应用来说,这会更快。
更高的可用性和更强的一致性。也就是 CAP 原理中的 A 和 C。
为什么会这样呢?因为对于有状态的服务,我们需要对于客户端传来的请求,都必须保证其落在同一个实例上,这叫 Sticky Session 或是 Sticky Connection。这样一来,我们完全不需要考虑数据要被加载到不同的节点上去,而且这样的模型更容易理解和实现。
可见,最重要的区别就是,无状态的服务需要我们把数据同步到不同的节点上,而有状态的服务通过 Sticky Session 做数据分片(当然,同步有同步的问题,分片也有分片的问题,这两者没有谁比谁好,都有 trade-off)。
这种 Sticky Session 是怎么实现的呢?
最简单的实现就是用持久化的长连接。就算是 HTTP 协议也要用长连接。或是通过一个简单的哈希(hash)算法,比如,通过 uid 求模的方式,走一致性哈希的玩法,也可以方便地做水平扩展。
然而,这种方式也会带来问题,那就是,节点的负载和数据并不会很均匀。尤其是长连接的方式,连上了就不断了。所以,玩长连接的玩法一般都会有一种叫“反向压力 (Back Pressure)”。也就是说,如果服务端成为了热点,那么就主动断连接,这种玩法也比较危险,需要客户端的配合,否则容易出 Bug。
如果要做到负载和数据均匀的话,我们需要有一个元数据索引来映射后端服务实例和请求的对应关系,还需要一个路由节点,这个路由节点会根据元数据索引来路由,而这个元数据索引表会根据后端服务的压力来重新组织相关的映射。
当然,我们可以把这个路由节点给去掉,让有状态的服务直接路由。要做到这点,一般来说,有两种方式。一种是直接使用配置,在节点启动时把其元数据读到内存中,但是这样一来增加或减少节点都需要更新这个配置,会导致其它节点也一同要重新读入。
另一种比较好的做法是使用到 Gossip 协议,通过这个协议在各个节点之间互相散播消息来同步元数据,这样新增或减少节点,集群内部可以很容易重新分配(听起来要实现好真的好复杂)。
在有状态的服务上做自动化伸缩的是有一些相关的真实案例的。比如,Facebook 的 Scuba,这是一个分布式的内存数据库,它使用了静态的方式,也就是上面的第一种方式。Uber 的 Ringpop 是一个开源的 Node.js 的根据地理位置分片的路由请求的库(开源地址为:https://github.com/uber-node/ringpop-node )。
还有微软的 Orleans,Halo 4 就是基于其开发的,其使用了 Gossip 协议,一致性哈希和 DHT 技术相结合的方式。用户通过其 ID 的一致性哈希算法映射到一个节点上,而这个节点保存了这个用户对应的 DHT,再通过 DHT 定位到处理用户请求的位置,这个项目也是开源的(开源地址为: https://github.com/dotnet/orleans )。
关于可扩展的有状态服务,这里强烈推荐 Twitter 的美女工程师 Caitie McCaffrey 的演讲 Youtube 视频《Building Scalable Stateful Service》(演讲 PPT),其文字版是在 High Scalability 上的这篇文章《Making the Case for Building Scalable Stateful Services in the Modern Era》

服务状态的容错设计

在容错设计中,服务状态是一件非常复杂的事。尤其对于运维来说,因为你要调度服务就需要调度服务的状态,迁移服务的状态就需要迁移服务的数据。在数据量比较大的情况下,这一点就变得更为困难了。
虽然上述有状态的服务的调度通过 Sticky Session 的方式是一种方式,但我依然觉得理论上来说虽然可以这么干,实际在运维的过程中,这么干还是件挺麻烦的事儿,不是很好的玩法。
很多系统的高可用的设计都会采取数据在运行时就复制的方案,比如:ZooKeeper、Kafka、Redis 或是 ElasticSearch 等等。在运行时进行数据复制就需要考虑一致性的问题,所以,强一致性的系统一般会使用两阶段提交。
这要求所有的节点都需要有一致的结果,这是 CAP 里的 CA 系统。而有的系统采用的是大多数人一致就可以了,比如 Paxos 算法,这是 CP 系统。
但我们需要知道,即使是这样,当一个节点挂掉了以后,在另外一个地方重新恢复这个节点时,这个节点需要把数据同步过来才能提供服务。然而,如果数据量过大,这个过程可能会很漫长,这也会影响我们系统的可用性。
所以,我们需要使用底层的分布式文件系统,对于有状态的数据不但在运行时进行多节点间的复制,同时为了避免挂掉,还需要把数据持久化在硬盘上,这个硬盘可以是挂载到本地硬盘的一个外部分布式的文件卷。
这样当节点挂掉以后,以另外一个宿主机上启动一个新的服务实例时,这个服务可以从远程把之前的文件系统挂载过来。然后,在启动的过程中就装载好了大多数的数据,从而可以从网络其它节点上同步少量的数据,因而可以快速地恢复和提供服务。
这一点,对于有状态的服务来说非常关键。所以,使用一个分布式文件系统是调度有状态服务的关键。

小结

好了,我们来总结一下今天分享的主要内容。首先,我讲了无状态的服务。无状态的服务就像一个函数一样,对于给定的输入,它会给出唯一确定的输出。它的好处是很容易运维和伸缩,但需要底层有分布式的数据库支持。
接着,我讲了有状态的服务,它们通过 Sticky Session、一致性 Hash 和 DHT 等技术实现状态和请求的关联,并将数据同步到分布式数据库中;利用分布式文件系统,还能在节点挂掉时快速启动新实例。下节课,我们讲述补偿事务。希望对你有帮助。
也欢迎你分享一下你所实现的分布式服务是无状态的,还是有状态的?用到了哪些技术?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
分享给需要的人,Ta购买本课程,你将得29
生成海报并分享

赞 16

提建议

上一篇
44 | 弹力设计篇之“幂等性设计”
下一篇
46 | 弹力设计篇之“补偿事务”
unpreview
 写留言

精选留言(24)

  • mingshun
    2018-05-22
    这是近几年所在的团队维护的其中一个重要系统的过程:最初为提高处理性能和水平扩展性,就从有状态往无状态发展。但随着数据量越来越大时,分布式存储就成了瓶颈,经常因为存储系统同步不及时导致不同节点读到的数据不一致。而后又回到有状态,但不在节点间使用分布式存储,因为数据量实在太大。为了在出现故障能快速恢复,每个节点做成双机热备。每改一次架构都要分好几次在周末深夜做数据迁移,多么痛的领悟啊! 理想情况下,计算密集型系统应该让存储向计算方移动,即做成无状态的;存储密集型系统应该让计算向存储移动,即做成有状态的。而上述架构来回修改的最终体会是,当计算和存储都密集的时候,应该整台机器地加,而且每台机器都要求有高的计算和存储性能。 然而说到底,存储其实又是由独立于CPU之外的一个计算单元来处理,本质似乎又回到了计算。现在设计分布式系统时总是先想着怎么分离计算和存储,最终发现计算和存储又是如此密不可分。 感觉我后半段的表述不太成立,打码久了不写人话的后果啊,望谅解!
    展开
    共 3 条评论
    68
  • null
    2020-01-12
    单体应用跟微服务应用的区别是单体应用是有状态的,微服务应用的是无状态的;单体应用向微服务应用的发展的一个重要体现就是将应用的状态从应用实例中转移到第三方中间件中.例如会话信息,传统的Java项目是将会话存储在tomcat等servlet容器中对其他的java应用隔离,这样导致的结果是如果想扩展服务的之后只能扩展单台服务器.而微服务能则将会话技术放到redis等nosql中间件中或者直接存到客户端中(jwt),微服务中的所有实例都可读取到 我认为分布式微服务应用还是无状态的比较好,因为现有的有状态分布式应用技术不成熟,gossip协议会导致羊群响应,导致而且如果数据大的情况下宽带会十分巨大.而现有的微服务解决方案依赖第三方中间件,这些中间件天生支持扩展分布式,让专业的工具做专业的事.如果服务要做到有状态的话每个服务中存储的状态数据需要做到一致(不管请求到达那个实例产生的结果一致),这也需要多分数据冗余,这样的话还不如放在第三方中间件中,像redis,mysql集群分布式方案十分丰富而且用的人也多 其实我们可以在有状态跟无状态之间做折中;我们可以参照java线程内存模型的形式,把第三方中间件当作JVM的主内存,微服务实例当作java线程对于主内存的拷贝,每个java线程只会拷贝自己需要访问的数据,微服务实例可以将数据缓存到guava cache或者ecache等进程级的缓存中;当微服务实例需要访问某个值的时候先去进程级缓存中查找,找不到再到集中式缓存中查找;更新某个值的时候,微服务本身先更新值,再将值更新到集中式缓存中,最好时候mq中间件通知其他的微服务实例更新自己进程中的缓存值,当然这里可以在计算更新值的时候避免缓存击穿事件分布式分布式锁技术
    展开
    共 3 条评论
    13
  • Sdylan
    2019-12-27
    存在错别字:结点-节点;元数据索引中对应关键-对应关系。本节笔记如下: ## 服务状态 ### 什么是状态 状态:保留程序的一些数据或是上下文: * 程序调用的结果。 * 服务组合下的上下文。 * 服务的配置。 ### 服务状态类型 1. 无状态的服务 * 可以随意扩展或删除节点 * 依赖第三方储存系统以及自身构建缓存,第三方存储系统: * Redis * MySQL * 高可用强一直:ZK/Etcd * 依赖有状态存储服务,存在网络开销;自身构建缓存浪费资源 2. 有状态的服务 * 数据本地化(Data Locality),延时低 * 更高的可用性和更强的一致性 * 特点:请求映射到同一个节点, 出现:Sticky Session 或是 Sticky Connection * 如何实现Sticky Connection * 长链接或一致性hash,出现服务热点问题 * 做到负载和数据均匀方案 * 路由节点 * 元数据索引:映射后端服务实例和请求的对应关系表 * 路由结点:根据元数据索引来路由,而这个元数据索引表会根据后端服务的压力来重新组织相关的映射 * 直连,不好动态扩展 * Gossip 协议,广播节点信息同步元数据 * 案例:Facebook 的 Scuba、 Orleans * 资料:《Making the Case for Building Scalable Stateful Services in the Modern Era》 3. 有无状态服务的对比 无状态的服务需要我们把数据同步到不同的结点上,而有状态的服务通过 Sticky Session 做数据分片(当然,同步有同步的问题,分片也有分片的问题,这两者没有谁比谁好,都有 trade-off)。
    展开
    共 2 条评论
    6
  • 北极点
    2018-03-09
    这里的这些方案,总体下来感觉架构上需要超丰富的经验!协调一大帮人来弄这个。现在接触到的系统没有这么复杂!有问题一般一个小点一个小点的优化。读了文章还是感觉很收益!谢谢!
    6
  • Geek_Heiko
    2020-01-08
    阅读笔记小结: 首先,我们先认识到,一个服务的状态通常是包含: 服务处理所需的元数据、与其它服务组合完成一个业务逻辑的组合上下文和服务的配置。 分布式系统架构中,为了便于系统服务增删和调度,往往要求其服务设计成“无状态”的服务,及类似于函数式编程,其中的服务只管描述其处理逻辑和算法,无需在其中保存上述的"状态"。具体到分布式系统的实现方案是,将这些状态,统一耦合到一个高可用的强一致性的第三方状态存储服务中,这过程主要带来了大量的网络开销、业务处理过程的数据同步及其带来的处理速度降低的问题,且每个服务都需要相对额外地耦合且依赖于该第三方服务接口,对应地,已有的优化方案是底层采用分布式文件系统提高状态的调度和同步速度,无状态的服务添加一些缓存机制。 然而,对于一些数据密集型的应用,无状态服务的无法比拟有状态服务的性能和满足实际需求指标。有状态服务的数据存放在本地,处理延迟低,且其可用性和一致性更强。当从另一方面来说,从整个系统的角度来看,服务间的通信和整个系统的各方面如数据和负载的均衡会带来一定的压力。因为业务层面的数据同步需要该服务将保持长连接,随着一个接一个业务的完成需要接入,该服务将很快成为"热点",若没有适时的断开,其资源必将消耗殆尽。现今的处理方式主要是引入一个元数据索引和路由节点,将后端服务和服务请求进行映射,以在一定程度上调和后端服务的压力。还有一种处理方式是不引入其他的,让有状态的服务直接充当路由的功能,每次服务节点的启动将配置的元数据读到内存中,然后系统的各个节点统一采用 Gossip 协议以便达到集群内的相关同步。 最后,一个业务及其所在的系统是有其生命周期及其不断地演化着的,其中的服务可以是无状态的,业务及其所在的系统是不能的(不然就失去其存在的意义)。对于这些"状态”的冗余备份和快速调度,我们采用在运行时只要有机会就进行相关的复制,“状态”的同步,我们采用在前面的操作前后分两阶段提交的方案以保持其一致性。最后一点是,使用合适的分布式文件系统是有状态的服务调度的关键。
    展开
    4
  • slark
    2020-01-29
    服务的状态,很简单:有状态、无状态 实际,状态一定都是有的,但服务的状态可以通过服务间的转移,把他们迁移到第三方存储上去。比如Redis、MySQL、Zookeeper/Etcd或者分布式文件系统,通过这样实现了服务的扩缩容和维护便利。但也增加了和第三方系统的耦合,以及请求链路和响应时间。 有状态,通常可以把状态保存在实例的本地,可以保证更高的可用性和更强的一致性。为了让请求落到同一个实例,可能需要采用数据分片的方式,比如用长连接、用hash分片。这样会导致不同节点的请求、数据存储不平衡。所以也通常需要添加路由节点,或者每个实例自己具有路由的功能(相互间需要同步元数据)。同时,在节点奔溃时候,要考虑到如何尽快恢复原始数据,如果有底层分布式文件系统的支持,就会比较便利
    展开
    3
  • Ryoma
    2019-12-28
    文中说 有状态的服务:有更高的可用性 不知道这里应该怎么理解,有状态的服务应该很容易单点故障,可用性怎么会好呢
    共 3 条评论
    3
  • 星空下
    2019-10-04
    两阶段提交和一致性算法讲的有点混
    共 2 条评论
    3
  • 夏书
    2019-06-26
    有状态化拿CPA理论来衡量优点牵强,当我以结果为导向,来辨别有无状态。 如果分片设计了,在一定场景上无副作用。 我是否可以理解为是无状态
    3
  • 罗杰.菲の樂
    2020-07-14
    突然发现Twitter 的美女工程师 Caitie McCaffrey现在加入微软的Azure了
    3
  • 知行合一
    2020-01-03
    一般都是无状态的服务,即使有些状态也是需要在服务启动或者访问的过程中自动加载,可以视为无状态,这也是依赖于各种分布式存储和中间件的结果。
    2
  • 番茄炒西红柿
    2019-12-31
    问一个问题,因为无状态的服务也要通过状态转移到分布式存储系统。所以是否可以认为分布式存储系统也是有状态的服务
    2
  • edisonhuang
    2019-07-05
    服务按照状态来分有无状态服务和有状态服务。 无状态服务易于水平扩展,没有副作用,是比较符合函数式编程的理念。但是现实中的真实场景往往是有状态的,因此做到无状态服务就需要把相应状态数据转移到数据服务层来同步,会增大系统的时延和服务同步的复杂性。 相对的是有状态的服务,有状态的服务数据和处理逻辑会保存在同一台机同一个进程,数据加载满足局部性原理,减少了网络调度的消耗,服务响应变快。但也会带来负载难均衡的问题,服务运维扩展也不容易。
    展开
    2
  • Geek_fb3db2
    2018-12-04
    讲了蛮多的无状态和有状态服务优缺点,但是总体来说无状态服务设计非常复杂,感觉需要非常高的系统架构,那么实际使用中,无状态使用多还是有状态的呢
    2
  • 流迷的咸菜
    2018-04-12
    虽然有状态的服务可以通过sticky session的方式将数据本地化,但是当这个sticky session expire的时候,或者服务处理完成之后,其相关的数据仍然是要同步到数据库中的吧?

    作者回复: 不用,一般来说就是一个客户端cookie,或是uid分布一下,或是服务端的一个缓存。

    2
  • 迷途书童
    2020-06-13
    如果能系统的讲一下有状态的服务适合什么样的场景,无状态的服务适合什么样的场景有完美了
    1
  • 文刂 氵共 超
    2019-12-23
    学习笔记 https://mubu.com/colla/4ShlioLRIAM
    1
  • 2018-05-10
    期待工作流引擎实现细节
    1
  • 流迷的咸菜
    2018-04-12
    文中提到,同步有同步的问题,分片也有分片的问题。同步的话主要是数据一致性和效率问题。那么分片的话,请问,主要问题是数据在其他片区上,需要远程获取数据,和对其他片区数据的写问题吗?
    1
  • 昵称
    2018-04-03
    sixcky session和DHT哪里有详细的介绍,这篇文章里好多名词都不认识

    作者回复: 自行Google吧

    1