【IT168 技术】很长一段时间,无态服务被看成通向伸缩扩展的必由之路,无态架构容易水平扩展,只需要一个round-robin的负载平衡就可以在无态服务之间分发请求。
但是,无态服务也有一些问题,由于状态都保存在数据库中,每次请求都要进入数据库增加了延迟,当然可以增加一层缓存,但是缓存会带来数据一致性等问题的新的复杂性。
那么什么是有态服务呢?有态服务是将数据运送给函数(shipping data to function),而无态服务是通过函数访问数据库数据,是将函数送给数据( shipping functions to data);这两种服务哪种更好呢?目前来看,好像有关有态服务的资料不是很多。
Caitie McCaffrey在 Strange Loop演讲中创新地进行了有态服务方面的探索。
他有以下观点:
(1)无态服务是浪费的。
无态服务确实工作得很好,将数据放在数据库中,通过增加新的无态服务实例实现水平扩展伸缩。
问题是,应用总是有状态的,如果一个数据库达到访问极限怎么办?我们需要sharding切分关系数据库或者使用NoSQL,这些都会放弃强一致性,从而导致数据库部分的抽象泄漏到服务中。
Data Shipping数据运送范式:客户端发出一个服务请求,服务与数据库交互,数据库返回一些数据,服务再做一些计算,响应回复给客户端,然后数据就从服务中消失。下一个请求通过负载平衡球分发到不同的机器上,上面整个过程就重新再来一遍。
你已经注意到同一个客户端在通过负载平衡器后到不同的服务后做同样的事情,这是浪费的,同一个客户端其实有一个session会话时间,我们为什么不利用会话保存上次请求的状态。
(2)有态服务更容易编程。
数据本地化:服务器保存着同一客户端的上次请求的状态,这样好处是,后面再次同样请求服务时,可以不必再访问数据库再次进行计算,从而降低延时,这对于数据集中性应用很有用处,客户端只需要一次性操作一系列大量数据,然后将结果保存在服务中,下次再次请求将立即快速返回响应。
函数运送范式:客户端发出服务请求,启动一个会话,第一次数据库还是需要访问,获得数据,数据保存在服务中(其实类似缓存,不过缓存的生命周期不是application级别,而是session会话级别,也就是这个缓存跟随客户端的会话建立和销毁)。一旦被处理好的数据保存在服务中,下次同样客户端发出请求到同样服务器的这个服务时,它就会操作在内存中的上次请求处理好的数据。避免了到数据库的额外开销,降低了延迟,即使数据库当机,请求也能被服务处理,因为服务不必再访问数据库了。
有态服务会导致更高的可用性和更强的一致性。在CAP理论中,当我们确定分区以后,我们需要在可用性和不同级别的一致性之间选择,CP是选择一致性超过可用性,而AP是选择可用性超过一致性。
如果你必须需要有更高的可用性,你选择AP,但是你必须有读写操作(read-your-write),单调的纯读或单独的纯写。那么有态服务就能帮助你实现。
如果你实现粘性sticky连接,也就是说,对于同一个客户端,每次请求连接都会分发到同样一个服务器中,因为那个服务器中保存着这个客户端的会话状态,那么你就能实现读写操作(read-your-write)的高一致性,通过管道化的随机访问内存。
在论文Werner Vogel 2007中,读写操作(read-your-write) 会话和单调monotonic一致性能够通过客户端粘到同一个服务器的方式实现,在一个分布式环境中,如果同一客户端访问的总是相同服务器,就能确保读写操作(read-your-write)和单调读,这对于负载平衡和失败恢复管理来说不是非常难,但是是一种简单的解决方案,使用粘性会话stick session,显式化提供一种符合客户端逻辑的公开方式。
粘性连接给予客户端一个更容易的可质疑方式,与其担心数据会从数据库被拉进许多不同服务器从而导致的数据并发性,你让每个客户端只和相同的一个服务器交互,会很容易帮助你思考业务实现,特别是在一个分布式的编程环境中时。
那么问题来了,将某个客户端与同一服务器始终粘在一起,会导致某个服务器负载特别大,还有如果这个服务器负载特别大怎么办?因此提出了Cluster Membership解决方案,一个客户端可以和一个集群中任何一个服务器交互,因为一个集群中这些服务器中状态都是相同的,有三种Cluster Membership类型:Static静态, Gossip协议, Consensus一致系统.
Static静态是最简单的方式,看上去最蠢也许正好符合你需要,一个配置文件包含一个集群中节点服务器的所有IP地址,这个配置文件分发到每个服务器上,当然问题不能实时知道哪个服务器正好空闲,也会导致某个服务器负载过大,而且不能失败恢复,如果服务器失败,名单中必须被替代,配置文件必须被更新分发。难以扩展集群本身规模,在一个集群中增加新机器,整个机器所有服务器必须重启。
动态Cluster Membership:节点可以动态增加删除,采取两种方式来处理集群中服务器状态复制:gossip protocol 和1 consensus system
gossip协议:带来超强可用性,通过发送消息将状态在集群中扩散,消息是有关他们和谁交互,谁还活着,哪个服务器已经死机了,每个机器以自己的意愿收集数据,有自己关于集群的世界观。在一个稳定的状态中,集群中所有机器会最终汇聚converge 到一个相同的认识观,这个认识观就是:大家最终会都知道哪个服务器真的死了,哪个还活着。当网络出现错误,或者有新的服务器增减,集群中的不同服务器就会有不同的有关集群整体的世界观了。有一点需要平衡,因为没有了必需的协调工作,所以你获得了高可用性,每个机器都能给予自己的世界观做决定,但是你的代码得能够处理在失败问题出现时需要路由到不同节点的这种不确定性情况。
Consensus一致系统:更强调一致性,集群中所有节点都有一致的世界观,一致性系统控制着集群中每个成员,当配置文件改变,所有的节点都会基于一致性系统更新它们的世界观,因为一致性系统如同专权者一样牢牢把握着整个集群的真实情况。
问题是:如果一致性系统自己不可用了,那么所有节点也就狗屁了,因为它们谁都不相信谁。谁也不认识彼此。
还有问题:因为一致性系统属于一种协调角色,协调工作加入分布式系统必然导致性能下降。
在这种情况下,尽量避免在一致性系统下寻求高可用性。
上面谈了集群内部状态的复制问题,剩下问题是,如何将工作跨集群分发,有三种类型:Random Placement, Consistent Hashing(一致性哈希), Distributed Hash Tables(分布式哈希表).
Random Placement:沉默意味着效率,当基于大量数据进行查询时,需要许多数据基于集群分发。适合采取这种方式,
一致性哈希:这是一种确定性的Placement,也就是将客户端请求基于一致性哈希这种确定性方案发送到某个服务器,哈希也许基于会话ID或用户ID,依赖工作负载是如何被分区的。也就是说,如果你依据用户ID的大小范围对服务器进行分区,那么采取基于用户ID的哈希方式。类似Cassandra 采取的也是这种方式。这种方式的问题是hotspots,许多请求会被哈希分发到相同节点,某个节点服务器会过载变慢,一致性哈希并不允许工作任务从热点区域移除,这样你必须为你的集群准备足够的多余的空间,以防止它成为热点区域,因为一旦成为热点就无法动态移除不断加入的工作任务了。
分布式哈希(DHT):这是一种非确定性的Placement,在以恶搞分布式哈希表中哈希被用来定位请求工作发送到哪个服务器,DHT保有集群中所有节点的指向,因为没有强迫工作任务分发到哪个指定服务器,因此是不确定的分发方案,如果某个节点不可用或变成热点,可以容易重新分发客户端请求工作到其他服务器。
最后,该演讲谈了当前现实中三大系统采取了有态服务架构,这三个系统都是鼎鼎大名:Facebook的Scuba;Uber的Ringpop和微软的Orlean。