「数据一致性」和「高可用」其实本质是一个通过提升复杂度让整体更完善的方式。
本文主要讲一些让系统更简单,更容易维护的东西——「易伸缩」,首当其冲的主题就是「stateless」,也叫「无状态」。
服务的“状态”
无状态的服务
客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份。服务端不保存任何客户端请求者信息。
无状态的好处?
- 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
- 服务端的集群和状态对客户端透明 =-服务端可以任意的迁移和伸缩 =-减小服务端存储压力
什么是有状态?
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 tomcat 中的 session。
例如登录:用户登录后,我们把登录者的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session。然后下次请求,用户携带 cookie 值来,我们就能识别到对应 session,从而找到用户的信息。
有状态的缺点是什么?
• 服务端保存大量数据,增加服务端压力
• 服务端保存用户状态,无法进行水平扩展
• 客户端请求依赖服务端,多次请求必须访问同一台服务器
状态化的判断是指两个来自相同发起者的请求在服务器端是否具备上下文关系。
如果是状态化请求,那么服务器端一般都要保存请求的相关信息,每个请求可以默认地使用以前的请求信息。
而无状态的请求,服务器端的处理信息必须全部来自于请求所携带的信息以及可以被所有请求所使用的公共信息。
无状态的服务器程序,最著名的就是WEB服务器。
状态化的服务器有更广阔的应用范围,比如MSN、网络游戏等服务器。他在服务端维护每个连接的状态信息,服务端在接收到每个连接的发送的请求时,可以从本地存储的信息来重现上下文关系。
纯函数式编程,就是无状态的。有状态,也叫有副作用。
无状态的服务易伸缩: 很容易的通过给后端添加服务器和前端的负载均衡实现横向的扩展。
当系统中存在着大量「有状态」的业务处理过程时,伸缩扩展就会变得复杂起来。
「有状态」和「无状态」
N.Wirth曾经在它1984年出版的书中将程序的定义经典的概括为:程序=数据结构+算法。(这个概括也是这本书的书名)
程序做的事情本质就是“数据的移动和组合”,以此来达到我们所期望的结果。而如何移动、如何组合是由“算法”来定的。
通过程序处理所得到的“成果”其实和你平时生活中完成的任何事情所得到的“成果”是一样的。任何一个“成果”都是你通过一系列的“行动”将最开始的“原料”进行加工、转化,最终得到你所期望的“成果”。
比如,你将常温的水,通过“倒入水壶”、“通电加热”等工作后变成了100度的水,就是这样一个过程。
正如烧水的例子,大多数时候得到一个“成果”往往需要好几道“行动”才能完成。
这个时候如果想降低这几道“行动”总的成本(如:时间)该怎么办呢?
自然就是提炼出反复要做的事情,让其只做一次。而这个事情在程序中,就是将一部分“数据”放到一个「暂存区」(一般就是本地内存),以提供给相关的“行动”共用。
image
但是如此一来,就导致了需要增加一道关系,以表示每一个“行动”与哪一个「暂存区」关联。因为在程序里,“行动”可能是「多线程」的。
这时,这个“行动”就变成「有状态」的了。
题外话:共用同一个「暂存区」的多个“行动”所处的环境经常被称作「上下文」。
我们再来深入聊聊「有状态」。
「暂存区」里存的是「数据」,所以可以理解为“有数据”就等价于“有状态”。
「数据」在程序中的作用范围分为「局部」和「全局」(对应局部变量和全局变量),因此「状态」其实也可以分为两种,一种是局部的「会话状态」,一种是全局的「资源状态」。
题外话:因为有些服务端不单单负责运算,还会提供其自身范围内的「数据」出去,这些「数据」属于服务端完整的一部分,被称作「资源」。所以,理论上「资源」可以被每个「会话」来使用,因此是全局的状态。
与「有状态」相反的是「无状态」,「无状态」意味着每次“加工”的所需的“原料”全部由外界提供,服务端内部不做任何的「暂存区」。并且请求可以提交到服务端的任意副本节点上,处理结果都是完全一样的。
有一类方法天生是「无状态」,就是负责表达移动和组合的“算法”。因为它的本质就是:
- 接收“原料”(入参)
- “加工”并返回“成果”(出参)
为什么网上主流的观点都在说要将方法多做成「无状态」的呢?
因为我们更习惯于编写「有状态」的代码,但是「有状态」不利于系统的易伸缩性和可维护性。
在分布式系统中,「有状态」意味着一个用户的请求必须被提交到保存有其相关状态信息的服务器上,否则这些请求可能无法被理解,导致服务器端无法对用户请求进行自由调度(例如双11的时候临时加再多的机器都没用)。
同时也导致了容错性不好,倘若保有用户信息的服务器宕机,那么该用户最近的所有交互操作将无法被透明地移送至备用服务器上,除非该服务器时刻与主服务器同步全部用户的状态信息。
但是如果想获得更好的伸缩性,就需要尽量将「有状态」的处理机制改造成「无状态」的处理机制。
「无状态」化处理
将「有状态」的处理过程改造成「无状态」的,思路比较简单,内容不多。
首先,状态信息前置,丰富入参,将处理需要的数据尽可能都通过上游的客户端放到入参中传过来。
当然,这个方案的弊端也很明显:网络数据包的大小会更大一些。
另外,客户端与服务端的交互中如果涉及到多次交互,则需要来回传递后续服务端处理中所需的数据,以避免需要在服务端暂存。
▲橙色请求,绿色响应
这些改造的目的都是为了尽量少出现类似下面的代码。
func(){
return i++;
}
而是变成:
func(i){
return i+1;
}
要更好的做好这个「无状态」化的工作,依赖于你在架构设计或者项目设计中的合理分层。
尽量将会话状态相关的处理上浮到最前面的层,因为只有最前面的层才与系统使用者接触,如此一来,其它的下层就可以将「无状态」作为一个普遍性的标准去做。
与此同时,由于会话状态集中在最前面的层,所以哪怕真的状态丢失了,重建状态的成本相对也小很多。
比如三层架构的话,保证BLL和DAL都不要有状态,代码的可维护性大大提高。
如果是分布式系统的话,保证那些被服务化的程序都不要有状态。除了能提高可维护性,也大大有利于做灰度发布、A/B测试。
题外话:在这里,提到做分层的目的是为了说明,只有将IO密集型程序和CPU密集型程序分离,才是通往「无状态」真正的出路。一旦分离后,CPU密集型的程序自然就是「无状态」了。
如此也能更好的做「弹性扩容」。因为常见的需要「弹性扩容」的场景一般指的就是CPU负荷过大的时候。
最后,如果前面的都不合适,可以将共享存储作为降级预案来运用,如远程缓存、数据库等。然后当状态丢失的时候可以从这些共享存储中恢复。
所以,最理想的状态存放点。要么在最前端,要么在最底层的存储层。
任何事物都是有两面性的,正如前面提到的,我们并不是要所有的业务处理都改造成「无状态」,而只是挑其中的一部分。最终还是看“价值”,看“性价比”。
比如,将一个以“状态”为核心的即时聊天工具的所有处理过程都改造成「无状态」的,就有点得不偿失了。
CAP理论
CAP理论的意思是说,一个分布式系统无法同时满足三个条件 : 一致性、可用性、分区容忍性。
CAP分别代表:
C:consistency,数据在多个副本中能保持一致的状态。
A:Availability,整个系统在任何时刻都能提供可用的服务
P:Partition tolerance,分区容错性,在出现分区的情况下依然能提供服务。
CAP 猜想就是说在C,A,P之间最多只能存在两个。
Lynch在2002年发表论文证明了这个猜想,将它上升到定理的层面。
一致性,数据要保证一致,保证准确性
可用性,我们的服务要保证24小时可用
分区容忍性,访问量太大了,要扩容,体现为系统的可伸缩性了,部署多个实例或副本
但是呢,扩容了,保证了可用性,数据一致性怎么保证?
副本这么多,同步机制太难做好了。
互联网公司一般会选择保证AP,保证高可用,但是一致性呢,该怎么办?CAP理论并不完全适用于指导实际的工程开发,所以对于一致性,一般会这样去考虑:
强一致性,必须保证一致性,任意时刻都能读到最新值。
弱一致性,写入新值后,在副本上可能读出来,也可能读不出来。
最终一致性,在某个时间后,能够读到最新的值。
有个经典有趣的问题:拜占庭将军问题。
拜占庭将军问题 (Byzantine Generals Problem),其故事背景是这样的:拜占庭位于现在土耳其的伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国国土辽阔,为了防御目的,因此每个军队都分隔很远,将军与将军之间只能靠信差传消息。 在战争的时候,拜占庭军队内所有将军必需达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,军队可能有叛徒和敌军间谍,这些叛徒将军们会扰乱或左右决策的过程。这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,这就是拜占庭将军问题。
从单机服务到集群化
构建单机服务非常简单,但如果单机服务可靠性或性能不足,就需要多机器共同承担某项服务。集群化包含以下三种情况:
- 无状态主备集群
仅有一台主机完成任务,且没有本地状态,其余从机机器待命,一旦主机宕机,从机选主成为主机。
- 有状态主备集群
仅有一台主机完成任务,有本地状态,其余从机机器待命,一旦主机宕机,从机选主成为主机。
- 无状态的主从集群
所有机器没有本地状态,理论上机器可以无限叠加,共同向外界提供同一服务。解决方案就是dubbo+zookeeper。
4.有状态的主从集群
所有机器都有本地状态,共同向外界提供同一服务。一旦某台机器宕机,需要主机协调其他从机代理其本地状态的任务。Paxos、raft和ZAB等一众分布式一致性算法的终极目标就是解决该问题。
参考资料
http://blog.sina.com.cn/s/blog_7d1968e20102xaej.html
https://coolshell.cn/articles/10910.html
https://www.jianshu.com/p/b0c8c9fb4763
Kotlin 开发者社区
国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。
越是喧嚣的世界,越需要宁静的思考。
标签:状态,请求,详解,分布式系统,一致性,服务器,服务端,客户端 From: https://blog.51cto.com/u_15236724/5946878