目录

  1. 概述
  2. 服务的状态
    1. 有状态服务
      1. 有状态服务的数据局部性
      2. 有状态服务的强一致性
      3. 有状态应用的粘性连接
      4. 图解有状态服务
      5. 有状态服务的无状态化处理
      6. 运行有状态工作负载的挑战
    2. 无状态服务
      1. 无状态服务是如何工作的
      2. 无状态应用程序的最佳实践
      3. 为什么无状态服务很重要
      4. 如何采用无状态应用程序
    3. 总结
  3. 无状态和有状态的容器管理
  4. 无状态主从集群设计
  5. 结论
  6. 附录

有状态服务无状态服务是两种不同的服务架构,两者的不同之处在于对于服务状态的处理,本文将主要记录:什么是状态?不同的状态会对服务产生什么样的影响、以及为什么如今都倡导无状态服务?

概述

状态化的判断是指两个来自相同发起者的请求在服务器端是否具备上下文关系。基于此,有/无状态的应用程序特点如下:

  • 无状态应用程序或进程是不保存或引用有关先前操作的信息的东西。每次它都像第一次一样从头开始执行每个操作,并使用 CDN 或 Web 服务器的功能来处理每个短期请求。

例如,有人在搜索引擎中搜索问题并按下了 Enter 按钮。如果搜索操作由于某种原因被中断或关闭,您必须重新进行刚刚的搜索操作,因为没有为您之前的请求保存数据。

  • 有状态应用程序会记住用户的特定详细信息,例如个人资料、首选项和用户操作。这些信息被视为系统的「 状态 」

例如:在网购时,每次您选择一个商品并将其添加到您的购物车时,您都会将它与之前添加的商品一起添加,并最终导航到结帐页面,每次添加新商品时,不会丢失之前的任何信息

简而言之,无状态服务不会记录服务状态不同请求之间也是没有任何关系;而有状态服务则刚好相反,不同的请求之间是有关联关系的。判断一个服务状态性最简单的标识就是:两个来自相同发起者的请求在服务端是否具有上下文关系

RedHat 对于应用的状态具有下列的见解:

The state of an application (or anything else, really) is its condition or quality of being at a given moment in time–its state of being. Whether something is stateful or stateless depends on how long the state of interaction with it is being recorded and how that information needs to be stored.

应用(或其他任何事物)的状态是指它在特定时间的状况或品质,即当前应用的一种存在属性(运行中或宕机了)。要判断一个应用是有状态还是无状态的,取决于和这个应用交互的过程中,当前这种交互状态维持的时间以及是否需要在这个过程中需要存储的信息

服务的状态

下文将详细介绍有状态服务无状态服务的定义,两者的区别。讨论为什么如今大家都呼吁无状态服务,对于现有的有状态服务如何无状态化?

有状态服务

有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,随后根据用户身份对请求执行不同的处理流程,典型的设计就是 web 应用中的 session。使用 Session 来维系登录用户的上下文信息。此时就算使用无状态的 http 协议,最终也会由于 session 的介入将此变为有状态服务

例如,用户登录案例:用户登录后,我们把登录者的信息保存在服务端 session 中,并且给用户一个 cookie ,它对应着服务器存储的 session。在下次请求时,用户请求携带 cookie 访问服务,服务器就能识别到对应 session,从而找到用户的信息

😣 如今大家普遍不喜欢使用有状态服务,就是因为有状态服务存在以下的缺点:

  • 服务端保存大量数据,增加服务端压力
  • 服务端保存用户状态,无法进行水平扩展
  • 客户端请求依赖服务端,多次请求必须访问同一台服务器

尽管有状态服务存在许多缺陷,但是有状态服务仍然存在一些优点:

有状态服务可以做到较好的数据局部性(可通过函数传递范例来实现)、高可用和强并发模式。当状态是共享的跨调用时,开发是容易的,由于不需要额外的持久存储,通常有状态能够做到低延时优化

有状态服务的数据局部性

数据局部性是指每个请求都会被路由到可以操作数据的机器上。当一个请求做到时第一次命中了数据存储,之后处理数据的请求离开了服务,将来来自内存中的数据可以让类似的请求更快的找到同样的服务。如此进行的结果就是能够达到低延时响应,毋需再去访问数据存储。这就是「 函数传递范例 」,是有状态服务和无状态服务区别的关键所在

有状态服务的强一致性

有状态的服务通常会导致服务的强一致性。有状态的服务可以构建一种粘性的链接,也就是说客户端的请求总是会被路由到最初为之提供服务的服务器主机上。以此方式实现的服务,可以增加 AP 系统的一致性力度。这种强一致性模式包括线性随机访问内存读你所写(Read your Write)。Werner Vogels 在他的文章中总结了这些内容

无状态的服务很容易的通过给后端添加服务器和前端的负载均衡实现横向的扩展。此类应用拥有叫做「 数据运送范例 」的方式,就是数据被请求时是来自后端的数据存储为请求提供,在未来的请求中,若相同的数据被请求时,是不会去关心这些请求是从哪个服务实例来的,因为服务实例是无状态的.McCaffrey 谈到在通信频繁的应用中这种架构简直是一种浪费,因为这些应用要在服务端与客户端之间频繁的通信,而且在此类应用中有状态的服务显然是一种更好的选择

有状态应用的粘性连接

粘性连接可以使用持久性的连接来实现,但是会带来负载在后端分布不均的问题,这就会导致客户端捆绑到服务器,而有些服务器不能得到充分利用,最终导致部分服务器负载过多。其中一个减轻此种后端压力的方法就是一旦达到某个阀值就拒绝再来的请求(提前拒绝请求)。

非粘性的服务还可以通过路由的逻辑来实现,这可以使得任何的客户端通过获得正确的路由来找到任何的服务器。此实现会带来两个问题,路由到集群成员(谁在我的集群中?)和工作分布(谁来做?)。集群成员可以是静态的也可以是动态的。后者可以通过使用 gossip 协议共识系统来实现。工作分布则有更多的实现机制-随机替代、一致性哈希、以及分布式哈希表

McCaffrey 在他的演讲中列出了一些构建有状态服务的陷阱,其中包括没有绑定的数据结构导致的内存问题、类似长期的垃圾回收暂停重载状态时出现的内存管理问题等。状态重载会在恢复和部署新代码时发生,这两者都会像第一次从数据库中获取数据那样付出高昂的代价。

图解有状态服务

程序做的事情本质上就是数据的移动和组合,以此来达到我们所期望的结果。其中如何移动、如何组合是由「 算法 」来定的。任何一个「 结果 」都是通过一系列的「 行动 」将最开始的「 原料 」进行加工、转化得来。

比如,你将常温的水,通过倒入水壶通电加热等工作后将常温的水变成了 100 度的水,这是常见的烧水过程。这个过程需要好几道行动才能得到结果。

这个时候如果想降低这几道行动的成本,天然的想法就是提炼出反复要做的事情,将各个独立的行动并行化。在程序中的实现就是将一部分数据放到一个「暂存区」中以提供给相关的行动共用

但是如此一来,就导致了需要增加一道关系,以表示每一个行动与哪一个「暂存区」关联。一旦拥有了关联关系,此时行动就变成了「有状态」

共用同一个「暂存区」的多个行动所处的环境经常被称作「上下文」。「暂存区」里存的是「数据」,所以可以理解为有数据就等价于有状态。「数据」在程序中的作用范围分为「局部」和「全局」(对应局部变量和全局变量),因此「状态」其实也可以分为两种,一种是局部的「会话状态」,一种是全局的「资源状态」

因为有些服务端不单单负责运算,还会提供其自身范围内的「数据」出去,这些「数据」属于服务端完整的一部分,被称作「资源」。所以,理论上「资源」可以被每个「会话」来使用,因此是全局的状态

与「有状态」相反的是「无状态」,「无状态」意味着每次加工所需的原料全部由外界提供,服务端内部不做任何的「暂存区」。并且请求可以提交到服务端的任意副本节点上,处理结果都是完全一样的

有一类方法天生是「无状态」,就是负责表达移动和组合的「 算法 」。因为它的本质就是:接收「 原料 」「 加工 」并返回「 成果 」

如果想获得更好的伸缩性以及容错性,就需要尽量将「有状态」的处理机制改造成「无状态」的处理机制

有状态服务的无状态化处理

⛵ 「有状态」的处理过程是可以改造成「无状态」的处理过程对,具体的改造步骤如下:

  1. 状态信息前置,丰富入参,将处理需要的数据尽可能都通过上游的客户端放到入参中传过来

这种方案的弊端会让网络数据包的体积更大一些

  1. 客户端与服务端的交互中如果涉及到多次交互,则需要来回传递后续服务端处理中所需的数据,以避免需要在服务端暂存。例子如下图,图中橙色表示请求,绿色表示响应

通过上述两步改造的目的都是为了尽量少出现类似下面的代码:

1
2
3
int func(){
return i++;
}

而是多出现这样的代码:

1
2
3
int func(i){
return i+1;
}

要更好的做好「无状态」化的工作,基本依赖于在架构设计或者项目设计中的合理分层。尽量将会话状态相关的处理上浮到最前面的层,因为只有最前面的层才与系统使用者接触,如此一来,其它的下层就可以将「无状态」作为一个普遍性的标准去做。与此同时,由于会话状态集中在最前面的层,所以哪怕真的状态丢失了,重建状态的成本相对也小很多。比如三层架构的话,保证 BLL(业务逻辑层) 和 DAL(数据访问层) 都不要有状态,代码的可维护性大大提高

在这里,提到做分层的目的是为了说明,只有将 IO 密集型程序和 CPU 密集型程序分离,才是通往「无状态」真正的出路。一旦分离后,CPU 密集型的程序自然就是「无状态」了.如此也能更好的做「弹性扩容」。因为常见的需要「弹性扩容」的场景一般指的就是 CPU 负荷过大的时候

  1. 如果前面的都不合适,可以将共享存储作为降级预案来运用,如远程缓存、数据库等。然后当状态丢失的时候可以从这些共享存储中恢复

所以,最理想的状态存放点,要么在最前端,要么在最底层的存储层

任何事物都是有两面性的,正如前面提到的,我们并不是要所有的业务处理都改造成「无状态」,而只是挑其中的一部分。最终还是看价值,看性价比。例如,将一个以状态为核心的即时聊天工具的所有处理过程都改造成「无状态」的,就有点得不偿失了

运行有状态工作负载的挑战

运行有状态工作负载有多种挑战:

  • 资源隔离:目前市场上的许多容器编排解决方案仍然只涉及一种尽力而为的资源分配方法,例如 CPU、内存和存储。这可能适用于无状态微服务,但对于有状态微服务来说,这可能是一种灾难性的方法,客户交易或数据由于性能不可靠而丢失
  • 持久存储:每个有状态数据服务可能需要或支持不同类型的存储类型(例如块设备或分布式文件系统),并且确定有状态服务的后备存储类型可能具有挑战性

无状态服务

无状态服务对于客户端的单次请求的处理,不依赖于其他请求,处理一次请求的信息都包含在该请求里。最典型的就是通过 cookie 保存 token 的方式传输请求数据。最终,客户端每次请求都携带自描述信息,服务端通过这些信息识别客户端身份,在服务端本地并不保存任何客户端请求者信息。就算有多个服务端,它们对同一个请求响应的结果也是完全一致的。

✨无状态服务特点

  • 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
  • 服务端的集群和状态对客户端透明
  • 服务端可以任意的迁移和伸缩
  • 减小服务端存储压力

无状态服务的实例可能会因为一些原因停止或者重新创建(如扩容时)。这时,这些停止的实例里的所有信息(除日志和监控数据外)都将丢失(重启容器即会丢失)。因此如果您的容器实例里需要保留重要的信息,并希望随时可以备份以便于以后可以恢复的话,那么建议您创建有状态服务。

❓ 为什么网上主流的观点都在说要将方法多做成「无状态」的呢?

  • 一方面,因为我们更习惯于编写「有状态」的代码,但是「有状态」不利于系统的易伸缩性和可维护性。在分布式系统中,「有状态」意味着一个用户的请求必须被提交到保存有其相关状态信息的服务器上,否则这些请求可能无法被其他机器理解,导致服务器端无法对用户请求进行自由调度,如此对分布式集群的水平扩展非常不友好。

  • 另一方面,有状态服务也导致了容错性不好,倘若保有用户信息的服务器宕机,那么该用户最近的所有交互操作将无法被透明地移送至备用服务器上,除非该服务器时刻与主服务器同步全部用户的状态信息。

无状态服务是如何工作的

无状态架构意味着应用程序依赖于第三方存储,因为它不会在内存或磁盘上存储任何类型的状态。它请求的所有数据都必须从其他有状态服务(数据库)中获取或数据本身就存在于 CRUD 请求中

对于无状态应用,任何一个请求首先会发送到负载均衡器上,随后负载均衡器将请求负载到无状态服务的任何一个副本上,因为服务中的所有数据都存储在其他地方(通常是具有持久存储的数据库)所以不同副本上对于请求执行相同逻辑。由于状态信息与每个请求一起发送到服务器,服务器通过该服务器继续为请求提供服务。Load-balancer 不需要担心将请求路由到同一台服务器,真正实现了负载均衡。 其中 JSON Web Token (JWT) 广泛用于创建无状态应用程序

然而对于有状态应用是不同的,当有状态应用程序中的并发用户数量增加时,添加更多运行相同程序的服务器(横向扩展),并使用负载平衡器在这些服务器之间均匀分配负载。但是由于每个服务器「 记住 」每个登录用户的状态,因此有必要将负载均衡器配置为「粘性模式 」。粘性模式下,负载均衡器将每个用户的请求发送到响应该用户先前请求的同一台服务器。这违背了负载均衡的目的,此时就算对系统进行了水平扩展,也会由于粘性负载的关系,整个系统的负载并不均衡。

✨ 以下是无状态应用程序的 5 个主要优点:

  • 消除创建/使用会话的开销(减少服务端的内存使用、消除会话国旗问题)
  • 横向扩展以满足现代用户的需求
  • 按需添加/删除应用程序的新实例
  • 它允许跨各种应用程序的一致性
  • 无状态使应用程序更易于使用和可维护

无状态应用程序的最佳实践

无状态应用不惜一切代价避免 session,主要是以下几点原因:

  • session 为系统增加了不必要的复杂性,但为系统带来的价值却非常少。session 重现错误十分困难,由于所有内容都存储在服务器端,因此很难修复与会话相关的错误。
  • 会话无法扩展。如果应用程序的负载呈指数增长,应当将请求负载到不同的服务器。如果使用会话,需要将所有会话复制到所有服务器。该系统变得非常复杂且容易出错。

如此也并不是说会话就是一个很糟糕的技术,会话仅对特定用例有用,例如 FTP(文件传输协议)。对于共享 Dropbox 等用例,有状态会话会增加额外的开销,而无状态则是完美的方式。会话功能使用 cookie 复制,在客户端缓存

为什么无状态服务很重要

如今倡导无状态简而言之就是为了让系统得到一种特性:伸缩性(scaliability)

❓ 以前有状态应用程序运行良好,为什么还需要无状态应用程序?

有状态的应用程序适合规模较小的应用程序,应对于复杂的系统有状态架构存在一些问题。首先,当用户在服务器上引用一个状态时,用户打开了很多不完整的会话和交易发生,在 Stateful 系统中,客户端计算出的状态,系统应该让连接保持多久?如何在服务器端验证客户端是否崩溃或与会话断开连接?在维护文档更改和回滚的同时如何跟踪用户的操作?这些都是有状态应用程序带来的难题。因此,想要回避这些难题,最好使用无状态服务。

Facebook 不断使用无状态服务。当服务器使用 Facebook API 请求最近消息的列表时,它会发出一个带有令牌和日期的 GET 请求。响应独立于任何服务器状态,一切都以缓存的形式存储在客户端的机器上。类似地,调用 POST 命令,在不考虑服务器状态的情况下,在标头中传递带有授权/身份验证数据的复杂主体与上一个、当前和下一个请求没有关系。在无状态中,客户端不会等待来自服务器的同步。

REST 是设计、架构和开发现代 API 的主流方式,值得注意的是:REST 就是一种典型的无状态架构。

如何采用无状态应用程序

⛵ 以下是采用无状态应用程序的 5 个步骤

  1. 适应和开发新应用程序。采用无状态应用程序起初可能是一项艰巨的任务,因为它是一种新范式。但是,通过正确的思维方式,可以在不保持任何状态的情况下适应和开发新的应用程序。
  2. 使用微服务开发应用程序。在此步骤中,将出于部署目的进行容器化。容器最擅长运行无状态工作负载。当需要管理的多个容器增加时,可以考虑切换到 Kubernetes 等云编排和管理工具来运行大量容器
  3. 容器化微服务应用程序。从资源点找到运行安全的容器的最佳位置,并维护应用程序的高可用性
  4. 将存储附加到无状态临时文件。附加到无状态的存储是短暂的。组织必须从无状态容器开始,因为它们更容易适应这种类型的架构,并且与单体应用程序分离并独立扩展
  5. 应用 REST 哲学。后端应该使用 REST 设计模式来构建应用程序。REST 哲学不是维护状态,只是在客户端稍微维护 cookie 和本地存储

总结

🆚 无状态服务和有状态服务主要有以下区别

  • 实例数量:无状态服务可以有一个或多个实例,因此支持两种服务容量调节模式;有状态服务只能有一个实例,不允许创建多个实例,因此也不支持服务容量调节模式

  • 存储卷:无状态服务可以有存储卷,也可以没有,即使有也无法备份存储卷里面的数据;有状态服务必须要有存储卷,并且在创建服务时,必须指定给该存储卷分配的磁盘空间大小

  • 数据存储:无状态服务运行过程中的所有数据(除日志和监控数据)都存在容器实例里的文件系统中,如果实例停止或者删除,则这些数据都将丢失,无法找回;而对于有状态服务,凡是已经挂载了存储卷的目录下的文件内容都可以随时进行备份,备份的数据可以下载,也可以用于恢复新的服务。但对于没有挂载卷的目录下的数据,仍然是无法备份和保存的,如果实例停止或者删除,这些非挂载卷里的文件内容同样会丢失。

无状态服务是指该服务的实例可以将一部分数据随时进行备份,并且在创建一个新的有状态服务时,可以通过备份恢复这些数据,以达到数据持久化的目的。有状态服务只能有一个实例,因此不支持「 自动服务容量调节 」。一般来说,数据库服务或者需要在本地文件系统存储配置文件或其它永久数据的应用程序可以创建使用有状态服务。要想创建有状态服务,必须满足几个前提:

  • 待创建的服务镜像(image)的 Dockerfile 中必须定义了存储卷(Volume),因为只有存储卷所在目录里的数据可以被备份
  • 创建服务时,必须指定给该存储卷分配的磁盘空间大小。
  • 如果创建服务的同时需要从之前的一个备份里恢复数据,那么还要指明该存储卷用哪个备份恢复。

有状态应用和流程是可以周而复始、反复发生的应用和流程,例如网上银行或电子邮件。这些操作是在先前的事务背景下执行的,当前事务可能会受到先前事务的影响。正因如此,有状态应用在每次处理用户的请求时都会使用相同的服务器。如果有状态事务被中断,其上下文和历史记录会被存储下来,这样就可以或多或少地从上次中断的地方继续。有状态应用会跟踪诸如窗口位置、设置首选项和近期活动等内容。我们可以把有状态事务视为与同一个人进行的定期对话。

有状态和无状态应用程序在互联网中无处不在,但是现代大部分软件是以无状态方式构建的,这是因为可伸缩性是如今大型系统非常重要的一个因素

无状态和有状态的容器管理

有状态现在已成为容器存储的主体,因此现在的问题从要不要使用有状态容器,变成了该如何使用这些有状态容器?

最初,容器被构建为无状态,因为这比较符合其便携、灵活的特性。但随着容器的广泛使用,人们开始对现有的有状态应用进行容器化处理(重新设计和重新封装,以实现容器的便捷扩缩容)。究竟是使用有状态还是无状态容器,要取决于构建哪种类型的应用以及用途。如果只是临时需要信息(快速而短暂),无状态便是解决之道。但如果应用需要更多的内存来存储从一个会话到下一个会话的操作,则应用采用有状态方式

正因为这个原因,有状态应用越来越像无状态应用,无状态应用向有状态应用靠近的趋势,如:存在一个无状态的应用,它不需要长期存储,但允许服务器使用 Cookie 来跟踪同一客户端的请求。尽管无状态应用程序以不同的方式工作,但它们不会在服务器上存储任何状态。

它们使用 DB 来存储所有信息。DB 是有状态的,即它具有附加的持久存储。通常,用户请求使用凭据登录,LB 后面的任何服务器都会处理该请求,生成一个 auth 令牌,将其存储在 DB 中,然后将令牌返回给前端的客户端。下一个请求与令牌一起发送,现在,无论哪个服务器处理请求,它都会将令牌与数据库中的信息进行匹配,并授予用户登录权限。每个请求都是独立的,与前一个或下一个请求没有任何联系,就像 REST 一样。尽管无状态应用程序有一个额外的调用 DB 的开销,但这些应用程序在水平扩展方面非常出色,这对于可能拥有数百万用户的现代应用程序至关重要。

无状态主从集群设计

构建单机服务非常简单,但如果单机服务可靠性或性能不足,就需要多机器共同承担某项服务。集群化包含以下四种情况:

集群格式含义
无状态主备集群仅有一台主机完成任务,且没有本地状态,其余从机机器待命,一旦主机宕机,从机选主成为主机
有状态主备集群仅有一台主机完成任务,有本地状态,其余从机机器待命,一旦主机宕机,从机选主成为主机
无状态的主从集群所有机器没有本地状态,理论上机器可以无限叠加,共同向外界提供同一服务。
有状态的主从集群所有机器都有本地状态,共同向外界提供同一服务。一旦某台机器宕机,需要主机协调其他从机代理其本地状态的任务

结论

有时您必须构建有状态的服务,这不会自动损害您进行 SaaS 开发的准备。但是,您需要确保对有状态服务进行某种扩展,并计划备份和快速灾难恢复。虽然这几乎总是可能的,但工作量可能比在无状态微服务上获得更好结果所需的工作要多得多。

附录

RedHat
Stateful and Stateless Applications Best Practices and Advantages
分布式系统中的“无状态”和“有状态”详解
构建可伸缩的有状态服务
【高可用架构】理解有状态服务和无状态服务
Service statelessness principle
无状态服务 VS 有状态服务
PRAM consistency
Service statelessness principle
分布式基础 5-无状态主从集群设计