@eric1989
2017-02-20T19:42:44.000000Z
字数 2142
阅读 1061
票号中心是一个事件驱动的架构。数据库用于保存历史数据和快照数据。而票号中心程序自身内存内则拥有着最新的数据。这也意味着票号中心是一个有状态的服务。由于单点故障的存在,因此需要设计一种可靠的主备容灾机制。由于票号中心是有状态的,如果两个票号中心同时对外提供服务就会导致出现数据混乱的情况。因此设计的主备机制需要确保主备集群中只有一台对外提供服务。抽象即可得到以下问题:共享可靠存储的有状态计算服务,在确保只能有一个节点提供服务的情况,如何完成主备机制
通过上面的介绍,我们可以得出整个主备机制的约束。
在这个前提下,为了实现主备,提供了一种称之为"哨兵"的主备切换机制。该机制包含两个个模块:哨兵和仲裁者。哨兵与仲裁者之间通过RPC方式进行通讯(通讯协议并不重要,在工程实践中,为了简化开发,建议采用RPC或者简单的私有协议)。
哨兵在启动后会用周期时间N向仲裁者发送授权请求(如果在已经获得授权的情况,则成为心跳信息)。哨兵内部存在一个随机时间T的定时器。每次哨兵成功向仲裁者发送请求时会重置该定时器。而当定时器超时,哨兵会认为自身无法联系上仲裁者,会终止自身的进程。时间约束T>>N(在工程实践中,可以让T=20s,N=500ms)。
如果哨兵的授权请求被批准,则哨兵会初始化启动计算服务。成功后将自身转化为激活状态。哨兵只会由待命状态转化为激活状态。
为了方便监测,哨兵在启动时会监听一个端口作为自身的身份标识。如果哨兵在启动时发现此端口被占用,则会终止自身程序。以确保在一个运行环境中只有一个提供服务的哨兵进程。
仲裁者负责接收哨兵的授权请求,并且在允许的情况下批准该请求。
为了保证架构简单,仲裁者需要可靠的持久化授权信息并且自身做到无状态(无状态就可以很容易主备避免单点)。仲裁者需要可靠持久化的信息有
授权信息(频繁变化)
名称 | 类型 | 备注 |
---|---|---|
granted_sentinel | string | 授权激活被批准的哨兵id |
granted_time | datetime | 上一次给予授权的时间 |
哨兵信息(静态数据)
名称 | 类型 | 备注 |
---|---|---|
sentinel_id | string | 哨兵id |
ip | string | 该哨兵的链接ip |
port | int | 该哨兵的监听端口 |
仲裁者优先在内存中存储授权信息,并周期时间G下持久化到可靠性存储(工程中建议G为1s)。而一旦仲裁者需要更新授权信息,需要作废内存数据并且操作可靠性存储中的数据,并以其中的数据为准。
仲裁者在先到先得的原则下批准请求,同时,批准请求需要具备以下几个条件之一。
1. 可靠性存储中不存在授权数据(系统的初始化状态)。
2. 曾经得到授权的哨兵,在时间M(M>>T且M>>G)内未发送过心跳包续约授权,并且检测到该哨兵的端口并不存活。
3. 当前请求授权的哨兵就是之前得到授权的哨兵。
算法的目的是为了保证当备份节点上线提供计算能力的时候,主节点必然已经下线这个事实。在时间约束M>>T>>N的情况下,如果主节点在定时器T到达时仍然无法向仲裁者发送心跳则会终止,而仲裁者在足够长的安全时间(M-T)后监测主节点确实不存活(通过监测ip和端口情况),则会批准备份节点的授权请求。因此,在备份节点上线的时候,主节点必然是下线状态。
通过上面的算法基础,我们可以得到如下的部署拓扑
从算法基础可知,授权批准是计算服务初始化计算能力的关键,因此仲裁者的存活是很重要的。为了避免单点故障以及保持架构的简单,对于仲裁者而言使用负载均衡策略部署。常规而言,2台物理机器已经足够。同时,为了不引入额外的依赖(例如Nginx类的负载前端),哨兵内部直接存储了仲裁者的服务列表(通常来说就是2个)。哨兵发送请求时从仲裁者列表中随机选择,如果失败则选择剩余的重试。如果都失败,则认为本次请求未曾送达。
对于使用计算服务的客户端而言,在当前的容灾机制下(后面会说到容灾机制的演进方向),客户端需要内置静态的路由列表。客户端选择计算服务节点的逻辑流程如下:
哨兵与仲裁者是单向通讯,请求由哨兵发起,仲裁者进行响应。
参数:
名称 | 类型 | 备注 |
---|---|---|
sentinel_id | string | 请求授权的哨兵id |
响应:
名称 | 类型 | 备注 |
---|---|---|
granted | boolean | 授权是否被批准 |