背景
电子商务系统大量使用mysql数据库作为其交易和存储的系统; 随着商户和用户量的不断增长,mysql中存储的数据量会越来越大,这时把所有数据存储在一张表或者一个数据库中会极大的影响系统的性能和安全。 分库分表是业界一个比较通用的方案,并且也比较成熟。
为了进行分库分表,我们需要为业务表中设置一个唯一的id;举个商品中心的例子:为了把一个租户下的所有菜品,菜价,菜品分类放在一下,会在所有这些表上加上一个全局唯一的租户id。
全局id算法
经过我们前期的调研和讨论,我们最终选择了twitter的snowflake(详细介绍请参考分布式自增ID服务。), 算法生成64位的id如下:
未用
|
毫秒数
|
datacenterId
|
workId
|
毫秒内序列号
|
---|---|---|---|---|
1bit | 41bit | 5bit | 5bit | 12bit |
该算法在本地进程运行效率非常高,但datacenterId, 和workId需要在一个集群中被分配成唯一的;在实际应用中,datacenterId可能没有,那workId就是10个bit。
下面章节将重点介绍唯一workId的生成过程。
zookeeper生成唯一的workId
workId分配算法在zookeeper中的节点
0
invoicing标识进销存服务的节点。
lock是实现分布式锁的节点,Lock_i是临时顺序节点。
workId节点下存储每一个机器节点,key=ip1, data=workId1 (算法保证workId不重复),为永久节点。
zookeeper的节点类型
类型
|
描述
|
---|---|
持久节点(PERSISTENT) | 在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失 |
持久顺序节点(PERSISTENT_SEQUENTIAL) | 这类节点包含持久节点的特性;额外的特性是,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。在创建此类节点中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。 |
临时节点(EPHEMERAL) | 和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。 |
临时顺序节点(EPHEMERAL_SEQUENTIAL) | 临时自动编号节点;当客户端和服务器的session超时后,节点被删除;在被创建时每个节点被自动的编号。 |
生成唯一workId的流程图
其中在zookeeper中实现互斥锁是算法的难点。
Zookeeper实现互斥锁的流程图
zookeeper源码中分布式锁的源码分析(/zookeeper-3.5.1-alpha/src/recipes/lock/src/c/src/zoo_lock.c)
主要的逻辑代码在zkr_lock_operation()中, 解释主要逻辑
staticintzkr_lock_operation(zkr_lock_mutex_t *mutex, struct timespec *ts) {
-------------------- //省略部分代码
//获取Locks下所有的节点
ret = retry_getchildren(zh, path, &vectorst, ts, retry);
if(ret != ZOK)
returnret;
struct String_vector *vector = &vectorst;
mutex->id = lookupnode(vector, prefix); // 获取当前节点的id
if(mutex->id == NULL) {
//当前id不存在,则创建一个临时顺序节点
ret = zoo_create(zh, buf, NULL, 0, mutex->acl,
ZOO_EPHEMERAL|ZOO_SEQUENCE, retbuf, (len+20));
}
if(mutex->id != NULL) {
ret = ZCONNECTIONLOSS;
ret = retry_getchildren(zh, path, vector, ts, retry);
if(ret != ZOK) {
LOG_WARN(("could not connect to server"));
returnret;
}
//sort this list, 按照节点的编号排序,
sort_children(vector);
owner_id = vector->data[0]; //获取最小编号的节点
mutex->ownerid = strdup(owner_id);
id = mutex->id;
char* lessthanme = child_floor(vector->data, vector->count, id); // 获取比自己编号小的节点
if(lessthanme != NULL){ //证明当前最小编号的节点不是我自己, 该程序不能获得锁
---------- //省略部分代码
ret = retry_zoowexists(zh, last_child, &lock_watcher_fn, mutex, | }
&stat, ts, retry); //比自己编号小的节点是一个列表,观察该列表中编号最大的节点
//这样比观察父节点/Locks的变化有优势,能够有效的减少“惊群效应”
---------- //省略部分代码
} else{
//获得了该锁
}
}
zookeeper高可用实践
Zookeeper中的几个重要角色
角色名
|
描述
|
参与写
|
参与读
|
---|---|---|---|
领导者(Leader) | Leader作为整个ZooKeeper集群的主节点,负责响应所有对ZooKeeper状态变更的请求; 领导者负责进行投票的发起和决议,更新系统状态,处理写请求。 |
必然参与 | 可以参与 |
跟随者(Follwer) | 响应本服务器上的读请求外,follower还要处理leader的提议,并在leader提交该提议时在本地进行提交。 |
必然参与 | 可以参与 |
观察者(Observer) | 观察者可以接收客户端的读写请求,并将写请求转发给Leader,但Observer节点不参与投票过程, 只同步leader状态,Observer的目的是为了,扩展系统,提高读取速度;3.3.0版本以上才有这个角色。 |
不参与 | 主要参与 |
客户端(Client) | 执行读写请求的发起方。 |
|
|
某公司之前的部署模式(在同一机房部署Follow, Leader节点):
缺点:当client读量增加后,可以通过增加集群的Follower来提升系统的读性能;
但随着Follower节点数据的增加,系统的写性能会有很大的影响(所有的follower都要参与提议的投票过程,这样follower节点越多,参与的决议投票的follower就越多);
zookeeper集群之前有过读流量和用户乱用client,导致拖垮主集群的casestudy。
基础架构组反馈某部门的读流量特别小,当前zookeeper集群按照按这种模式部署的。
美团公司当前的部署模式(优化后):
集群部署的说明:
类型
|
描述
|
职责
|
是否存储数据
|
---|---|---|---|
主机房 | 由Leader/Follower构成的投票集群(对应之前的部署模式) | 负责集群的读写请求 | 存储 |
机房A | 由Observer构成的ZK集群 | 负责处理读请求,转发client的写请求到主机房 | 存储 |
机房B | 由Observer构成的ZK集群 | 负责处理读请求,转发client的写请求到主机房 | 存储 |
优点:
客户端能够在本机房读取所需要的数据,减少跨机房的调用延迟。
Observer机器发生故障,或者机房之间的链路发生故障, 不会影响到zookeeper主集群的使用
workId生成算法弱依赖zookeeper的实践
因为workId生成算法只在程序初次部署,或者重启的时候需要访问zookeeper,并且该配置后续一直都不会更改,可以考虑存储在zookeeper中的信息,也在本地文件或者配置中心中保存一份。
容错逻辑:
配置中心在设计的时候就有本地缓存(缓存文件),可以直接复用配置中心写本地文件的逻辑,而不用额外的写一个新的本地文件。
参考文献:
zookeeper的整体介绍: http://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/
zookeeper的部署实践: http://www.cnblogs.com/sunddenly/p/4143306.html
zookeeper sre: zookeeper集群架构