可扩展伸缩架构中的状态

  提到状态,我们总是伴随着可变的、并发、隔离和作用域等词语,精确定义如下:状态是有关存储信息的技术名词,任何一个时间程序能够立即访问到。简单地说,状态是一种可能被行为操作改变的数据,是一种可变的纯数据。

全局状态类似于我们通常讲的全局变量,为什么我们需要全局变量?因为这个全局变量包含着全局状态,可以全局共享,很显然,如果所有程序都共用一个数据库,那么数据库无疑是最常见的全局状态。如果将全局状态放在程序的全局变量中,那么会使得我们的各个使用这个全局变量的程序部分会紧紧耦合在一起。

在面向对象编程中,一个对象可以看成是由一些数据组成的,包含一些访问这些数据的操作方法。这些对象内部数据由于会被对象方法改变,这属于对象内部的状态,OOP强烈建议改变状态的行为和状态应该放在一起(放在一个对象中)。这样才能保证,在前置条件和后置条件下使得代码更容易测试。

使用继承来共享代码是一个坏主意,状态的改变行为将位于不同的父子继承文件中,即使他们最终是在运行时是单一对象,这也会影响代码的可读性。

函数式编程是通过避免可变状态来解决这种复杂性,这种函数的输出完全依赖其输入,但是回避可变状态不只是简单闭上眼睛,如同掩耳盗铃,毕竟我们还要面对状态,下面是Scala处理状态代码:

case  class  Simple(seed:  Long)  extends RNG {
    def  nextInt: (Int, RNG) = {
      val  newSeed = (seed *  0x5DEECE66DL +  0xBL) &  0xFFFFFFFFFFFFL 
      val  nextRNG = Simple(newSeed)
      val  n = (newSeed >>>  16).toInt
      (n, nextRNG)
    }
  }

 

这是一个随机数产生类,依赖于先前种子产生新的随机数,OOP会将老的种子作为对象状态,每次nextInt方法被调用时改变这个状态,而FP函数编程则是封装种子在结果元素中,这样每件事都是不可变的,函数的结果是依赖其输入,这称为透明引用。

Akka是实现Actor模型的工具集,这个模型是混合了OOP和FP风格处理状态,每个Actor管理自己的状态,但是操作状态的动作是按消息顺序发生的,因此任何时刻不存在两个行为同时改变状态,从而避免了锁。

在服务层中处理状态的总结:

  • 隔离
  • 尽可能避免状态
  • 状态应该被指定软件管理
  • 默认不可变
  • 状态和行为要捆绑在一起

下面我们看看状态如何在系统层的情况。

状态是能够瞬间访问的数据,但是状态生命周期?请求作用域?会话作用域?什么时候能够导入存储到持久介质上?下面看看状态在系统层面的几个生命周期:

1. HTTP请求周期: 在一个HTTP请求对象是持有一个有限状态机,这个周期相当短,这样我们只能让状态保留在内存中,大部分时间我们能通过失败重试的方式简化,而不是使用Akka持久层复杂技术。

2.会话周期: HTTP是一个无状态协议,注意时间是我们状态定义中的基础,这就意味着HTTP并没有内建机制跟踪状态,而会话则是针对同一个客户端多个请求在服务器保有的状态,但是会话状态是无法扩展伸缩的,因为这导致有状态服务,而无状态服务可以根据负载平衡器分发请求到不同的无态服务,如果是有态服务,每次请求只能粘牢指定服务器,要么将会话状态在服务器之间复制,如果状态比较多,复制会无故耗费服务器的处理性能。

3.流周期:这是接近实时分析,状态存在一个时间窗口,用来进行分析比如计算平均值或计数或最大值。

4.持久周期:一些数据会比创建它的代码寿命长,需要保存到磁盘。

数据库作为状态单一来源

我们认为尽可能避免状态是一个好设计,无状态服务虽然好,但不代表不会操作数据,不会和有状态数据打交道,无态服务可以将状态委托给数据存储,或使用Servlerless架构,这不代表没有服务器,但是你将状态委托给专家专业平台处理。

委托我们的持久状态到数据库是一个好主意,当负载增加以后,系统会开始变得缓慢,我们这时会使用缓存,同样,如果我们需要对数据库进行全文本搜索,数据库可能就不会很擅长,这样我们会针对不同的查询进行优化,同时要保持这些不同状态查询视图的同步。

当多个应用同时修改同一个数据存储时,会有各种情况:

1.竞争情况:如果两个客户端同时修改同一行记录,如何避免同时争夺呢?数据库的ACID属性帮助你处理并发问题。

2.冲突恢复,即使ACID帮助实现原子并发操作,如果第一个更新成功,但是第二个修改失败怎么办?这可能需要2PC两段事务提交机制。但是2PC事务难以横向扩展伸缩,在分布式系统中根据CAP定理,会有很差的性能。

 

日志

Kafka这样的消息系统能够实现日志的抽象,从而帮助同步状态的不同视图,以恶搞日志是一种带有顺序消息的集合,这个顺序对于分布式系统非常重要,Kafka提供了publish-subsribe发布-订阅模型,包括topic主题,分区,broker或消费者,日志成为状态核心。下面看看Kafka是如何实现ACID?

1. Atomicity原子性:如果一个日志消息消费者发生问题怎么办?比如从Kafka读取消息后写入缓存或数据库出错怎么办?每个消费者会保有一个指针,使用Kafka的专用名词就是offset,这个指针或偏差会指向日志中最后成功的一个消息,当消费者有可用时,能消费者这些消息并完成事务,我们可以认为其是最终原子性,这对于大多数分布式系统已经足够。

2.Isolation隔离性:竞争出现是因为没有顺序,没有人排队就会出现哄抢现象,而顺序在Kafka是由日志顺序保证。

3.Durability持久性:Kafka有强的持久保证,消息会写到磁盘在几个broker之间复制,不要使用Kafka作为状态长期保存,可以将消息备份到亚马逊S3或Hadoop。

4.Consistency一致性:当消费者从日志中读取消息时是有采集率的,这实际解耦了生产者和消费者,使用日志作为缓冲buffer,这就导致我们的系统状态是最终一致性,这个过程是异步过程。

 

全新角度总结Twitter Facebook和LinkedIn业务模型与架构

扩展性主题

数据库事务