分布式系统关注点——“无状态”详解(147)

发布于2019-04-21 20:41:26

本文中我们开始聊一些让系统更简单,更容易维护的东西——“易伸缩”,首当其冲的第一篇文章就是“stateless”,也叫“无状态”。

一、初识“状态”

我们首先举个例子。
 
开发Z哥对运维Y弟喊:“Y弟,现在系统好卡,刚上了一波活动,赶紧帮我加几台机器上去顶一下。”
 
Y弟回复说:“没问题,分分钟搞定”。
 
然后就发现数据库的压力迅速上升,DBA就吼了:“Z哥,你丫的搞什么呢?数据库要被你弄垮了”。
 
然后客服那边接框也爆炸了,越来越多的用户说刚登陆后没多久,操作着就退出了,接着登陆,又退出了,到底还做不做生意了。
 
这个案例中的问题,产生的根本原因是因为系统中存在着大量“有状态”的业务处理过程。

二、“有状态”和“无状态”

N.Wirth曾经在它1984年出版的书中将程序的定义经典的概括为:程序=数据结构+算法。(这个概括也是这本书的书名)
 
这是一个很有意思的启发,受它的影响,z哥认为程序做的事情本质就是“数据的移动和组合”,以此来达到我们所期望的结果。而如何移动、如何组合是由“算法”来定的,所以z哥延伸出一个新的定义:数据+算法=成果。
 
通过程序处理所得到的“成果”其实和你平时生活中完成的任何事情所得到的“成果”是一样的。任何一个“成果”都是你通过一系列的“行动”将最开始的“原料”进行加工、转化,最终得到你所期望的“成果”。
 
image
 
比如,你将常温的水,通过“倒入水壶”、“通电加热”等工作后变成了100度的水,就是这样一个过程。
 
正如烧水的例子,大多数时候得到一个“成果”往往需要好几道“行动”才能完成。

image

这个时候如果想降低这几道“行动”总的成本(如:时间)该怎么办呢?
自然就是提炼出反复要做的事情,让其只做一次。而这个事情在程序中,就是将一部分“数据”放到一个“暂存区”(一般就是本地内存),以提供给相关的“行动”共用。

image

但是如此一来,就导致了需要增加一道关系,以表示每一个“行动”与哪一个“暂存区”关联。因为在程序里,“行动”可能是“多线程”的。
 
这时,这个“行动”就变成“有状态”的了。

image
 
题外话:共用同一个“暂存区”的多个“行动”所处的环境经常被称作“上下文”。
 
我们再来深入聊聊“有状态”。
 
“暂存区”里存的是“数据”,所以可以理解为“有数据”就等价于“有状态”。
 
“数据”在程序中的作用范围分为“局部”和“全局”(对应局部变量和全局变量),因此“状态”其实也可以分为两种,一种是局部的“会话状态”,一种是全局的“资源状态”。
 
题外话:因为有些服务端不单单负责运算,还会提供其自身范围内的“数据”出去,这些“数据”属于服务端完整的一部分,被称作“资源”。所以,理论上资源可以被每个会话来使用,因此是全局的状态。
 
本文聊的“有状态”都指的是“会话状态”。
 
与“有状态”相反的是“无状态”,“无状态”意味着每次“加工”的所需的“原料”全部由外界提供,服务端内部不做任何的“暂存区”。并且请求可以提交到服务端的任意副本节点上,处理结果都是完全一样的。
 
有一类方法天生是“无状态”,就是负责表达移动和组合的“算法”。因为它的本质就是:

  1. 接收“原料”(入参)
  2. “加工”并返回“成果”(出参)
     
    为什么网上主流的观点都在说要将方法多做成“无状态”的呢?
     
    因为我们更习惯于编写“有状态”的代码,但是“有状态”不利于系统的易伸缩性和可维护性。
     
    在分布式系统中,“有状态”意味着一个用户的请求必须被提交到保存有其相关状态信息的服务器上,否则这些请求可能无法被理解,导致服务器端无法对用户请求进行自由调度(例如双11的时候临时加再多的机器都没用)。
     
    同时也导致了容错性不好,倘若保有用户信息的服务器宕机,那么该用户最近的所有交互操作将无法被透明地移送至备用服务器上,除非该服务器时刻与主服务器同步全部用户的状态信息。
     
    但是如果想获得更好的伸缩性,就需要尽量将“有状态”的处理机制改造成“无状态”的处理机制。

三、“无状态”化处理

将“有状态”的处理过程改造成“无状态”的,思路比较简单,内容不多。
 
首先,状态信息前置,丰富入参,将处理需要的数据尽可能都通过上游的客户端放到入参中传过来。

image

当然,这个方案的弊端也很明显:网络数据包的大小会更大一些。
 
另外,客户端与服务端的交互中如果涉及到多次交互,则需要来回传递后续服务端处理中所需的数据,以避免需要在服务端暂存。

image
(橙色请求,绿色响应)
 
这些改造的目的都是为了尽量少出现类似下面的代码。

func(){
    return i++;
}

而是变成:

func(i){
    return i+1;
}
 

要更好的做好这个“无状态”化的工作,依赖于你在架构设计或者项目设计中的合理分层。
 
尽量将会话状态相关的处理上浮到最前面的层,因为只有最前面的层才与系统使用者接触,如此一来,其它的下层就可以将“无状态”作为一个普遍性的标准去做。
 
与此同时,由于会话状态集中在最前面的层,所以哪怕真的状态丢失了,重建状态的成本相对也小很多。
 
比如三层架构的话,保证BLL和DAL都不要有状态,代码的可维护性大大提高。
 
如果是分布式系统的话,保证那些被服务化的程序都不要有状态。除了能提高可维护性,也大大有利于做灰度发布、A/B测试。
 
题外话:在这里,提到做分层的目的是为了说明,只有将IO密集型程序和CPU密集型程序分离,才是通往“无状态”真正的出路。一旦分离后,CPU密集型的程序自然就是“无状态”了。
 
如此也能更好的做“弹性扩容”。因为常见的需要“弹性扩容”的场景一般指的就是CPU负荷过大的时候。
 
最后,如果前面的都不合适,可以将共享存储作为降级预案来运用,如远程缓存、数据库等。然后当状态丢失的时候可以从这些共享存储中恢复。
 
所以,最理想的状态存放点。要么在最前端,要么在最底层的存储层。

image

四、总结

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