分布式核心技术之:协调设计解析
简介: 分布式协同是分布式应用中不可缺少的,通常担任协调者的角色,或者说是将多机协同的职责从分布式应用中独立出来,以减少系统的耦合性和增强扩充性。Apache 的 Zookeeper,Google 的 Chubby 都是分布式协同的实现者。fourinone 实际上可以单独当做 Zookeeper 用,它使用最少的代码实现了 Zookeeper 的所有功能,并且力图做到功能更强但是使用更简洁。本文以 fourinone 为例详细解析了分布式协调的设计原理。
Fourinone(中文名字“四不像”)是一个四合一分布式计算框架,在写这个框架之前,我对分布式计算进行了长时间的思考,也看了老外写的其他开源框架。当我们把复杂的 Hadoop 当作一门学科学习时,似乎忘记了我们想解决问题的初衷:我们仅仅是想写个程序把几台甚至更多的机器一起用起来计算,把更多的 CPU 和内存利用上,来解决我们数量大和计算复杂的问题。当然这个过程中要考虑到分布式的协同和故障处理。如果仅仅是为了实现这个简单的初衷,为什么一切会那么复杂。我觉的自己可以写一个更简单的东西,它不需要过度设计,只需要看上去更酷一点,更小巧一点,功能更强一点。于是我将自己对分布式的理解融入到这个框架中,考虑到底层实现技术的相似性,我将 Hadoop,Zookeeper,MQ, 分布式缓存四大主要的分布式计算功能合为一个框架内,对复杂的分布式计算应用进行了大量简化和归纳。
Fourinone2.0 提供了一个 4 合 1 分布式框架和简单易用的编程 API,实现对多台计算机 CPU、内存、硬盘的统一利用,从而获取到强大计算能力去解决复杂问题。由于多计算机物理上独立,Fourinone 框架也提供完整的分布式协同和锁以及简化 MQ 功能,用于实现多机的协作和通讯。
- Fourinone 框架提供了一系列并行计算模式(农民工 / 包工头 / 职介绍 / 手工仓库)用于利用多机多核 CPU 的计算能力;
- 提供完整的分布式缓存和小型缓存用于利用多机内存能力;
- 使提供像操作本地文件一样操作远程文件(访问、并行读写、拆分、排它、复制、解析、事务等)用于利用多机硬盘存储能力;
- 由于多计算机物理上独立,Fourinone 框架也提供完整的分布式协同和锁以及简化 MQ 功能,用于实现多机的协作和通讯。
Fourinone 采用 Java 开发,2.0 版本整体大小 150k,就一个 jar 和一个配置文件,没有任何依赖,很方便嵌入式开发使用。
Fourinone 下载请见参考资源。
Zookeeper 是 Hadoop 生态系统中协同实现,提供协调服务包括分布式锁,统一命名等。Chubby 是 Google 分布式系统中的协同实现,和 Zookeeper 类似。Zookeeper 实际上是 Google 的 Chubby 一个开源的实现。Paxos 算法是 1989 年由莱斯利.兰伯特提出,此算法被广泛用于处理分布式系统的消息传递一致性。
Zookeeper 的配置中心实现更像一个文件系统,文件系统中的所有文件形成一个树状结构。Zookeeper 维护着这样的树形层次结构,树中的节点称为 znode, 每个 znode 存储的数据有小于 1m 的大小限制。zookeeper 对 znode 提供了几种类型:临时 znode、持久 znode、顺序 node 等几种类型,用于不同的一致性需求。在 znode 发生变化时,通过“观察”(watch)机制可以让客户端得到通知。可以针对 ZooKeeper 服务的“操作”来设置观察,该服务的其他操作可以触发观察。ZooKeeper 服务的“操作”包括一些对 znode 添加修改获取操作。ZooKeeper 采用一种类似 Paxos 的算法实现领导者选举,用于解决集群宕机的一致性和协同保障。总体上,Zookeeper 提供了一个分布式协同系统,包括配置维护、名字服务、分布式同步、组服务等功能,并将相关操作接口提供给用户。
Zookeeper 下载和 Paxos 详细说明请见参考资源。
fourinone 对分布式协同的实现, 是通过建立一个 domain、node 两层结构的节点信息去完成。domain 可以是分类或者包,node 可以是具体属性。domain 和 node 都是自己根据需求设计命名,比如可以将 domain 命名为“a.b.c...”表示一个树型类目。
一个 domain 下可以有很多个 node,每个 node 只指定一个 domain,可以通过 domain 返回它下面所有的 node。
domain 不需要单独建立,通常在建立 node 时,如果不存在 domain 会自动创建。
如果 domain 下没有 node 了,该 domain 会自动删除。
如果删除 domain,该 domain 下面 node 也都会删除。
每个 node 下可以存放一个值,可以是任意对象。
所有的节点信息存放在 parkserver 里,parkserver 提供协同者的功能。如下图所示:
图 1. 协调实现架构
![](https://i-blog.csdnimg.cn/blog_migrate/49efd319f2d10c3e3da32afcf5883594.png)
从上图可以看到,其他分布式进程可以通过 parkserver 的用户接口:ParkLocal,对节点进行增加、修改、删除、指定心跳、指定权限等操作,并且结合 parkserver 提供同步备份、领导者选举、过期时间设置等功能,共同来实现众多分布式协同功能。
我们举个例子,说明两个分布式应用完成协同功能的流程:
- 分布式应用 A 通过 ParkLocal 在 ParkServer 上创建一个 domain/node 的节点,并且在节点里存放相应的 value,这个节点以及它的 value 值代表分布式应用 A 的某种协同信息,它存放在 ParkServer 上用于向分布式应用 B 分享。
- 分布式应用 B 通过 ParkLocal 操作 ParkServer,对它上面的分布式应用 A 建立的这个 domain/node 节点进行监听,如果节点 value 发生变化,那么分布式应用 B 可以获取到这个 value,并进行相应的业务处理,这样便将各自独立的分布式应用 A 和 B 协同了起来。
- 由于 ParkServer 保存着用于协同的节点和信息,为了防止 ParkServer 宕机导致整体故障,ParkServer 配置为一主多备的关系,互相同步信息,在故障时可以进行领导者选举,切换到备用 ParkServer 上继续提供协同服务。
关于分布式协同的场景还有很多,可以参考:
- 分布式配置,多个机器的应用公用一个配置信息,并且挂掉能够通过领导者选举进行恢复,详细见 指南和demo
- 分布式锁,多个机器竞争一个锁,当某个机器释放锁或者挂掉,其他机器可以竞争到锁继续,详细见 指南和demo
- 集群管理,集群内机器可以互相感知和领导者选举,详见 指南和demo
我们下面介绍 ParkLocal 几个核心的 API,其他的 API 使用类似,更多的可以去查看源码:
- 创建 node
清单 1
public ObjectBean create(String domain, String node, Serializable obj);
通过上面方法往 ParkServer 里建立一个节点,需要指定 domain 和 node 名称,如果第一次创建,domain 不存在,ParkServer 会自动创建,因此不需要单独创建 domain 的 API。
如果 domain 和 node 都存在,重复创建会失败。
节点的值是一个 Serializable 对象,它必须要是可序列化的,因为需要进行网络传输。
- 创建心跳属性节点
public ObjectBean create(String domain, String node, Serializable obj, boolean heartbeat);
有的情况,我们需要创建了节点后,创建进程跟 ParkServer 保持心跳连接的,如果创建进程死掉了,那这个节点会自动被删除掉,心跳属性节点很适合在集群管理等场景上的应用。 - 获取 node
public ObjectBean get(String domain, String node);
根据 domain 和 node 名称获取对应的对象,但是返回是一个 ObjectBean 的封装对象,我们要拿到原始对象,可以通过 toObject() 获取,ObjectBean 也封装了 domain 和 node 的名称信息,可以通过 getDomain 和 getNode 获取。
- 获取最新 node,需要传入旧 node 进行对照
public ObjectBean getLastest(String domain, String node, ObjectBean ob);
getLastest 是一个很有用的方法,可以获取到该节点的最新版本,它获取最新版本的方式是需要将旧版本的 ObjectBean 传进去,ParkServer 会进行对比,如果发现对象值更新了,就返回一个新的 ObjectBean,如果没有更新变化,就返回 null。
- 添加 node 的事件监听
public void addLastestListener(String domain, String node, ObjectBean ob, \ LastestListener liser);
如果我们需要检测 ParkServer 上节点是否变化,可以通过上面 getLastest 方法轮询最新值,也可以通过事件方式响应。
我们需要对一个节点进行监听,事件响应方式监控配置信息发生变化,需要实现一个 LastestListener 的事件接口并进行注册,当信息变化时,会产生事件并获取到变化后的对象进行处理,LastestListener 的 happenLastest 方法有个 boolean 返回值,如果返回 false,它会一直监控配置信息变化,继续有新的变化时还会进行事件调用;如果返回 true,它完成本次事件调用后就终止。
如果初次注册节点的事件监听,可以指定一个初始对比值 ObjectBean ob,代表事件是相对于该值的变化或持续变化;如果不需要初始对比值,可以传入 null,只要目前节点的值不为 null,就会产生事件。
- domain 的事件监听注册跟 node 使用类似,只是初始对比值为 List<ObjectBean>。
- public ObjectBean create(String domain, String node, Serializable obj, AuthPolicy auth);
- 通过上面方法创建 node 时,可以指定一个权限参数,有只读(AuthPolicy.OP_READ)、读写(AuthPolicy.OP_READ_WRITE)、所有(AuthPolicy.OP_ALL)三种属性,默认为 AuthPolicy.OP_ALL
- 注意:这里的权限属性是指创建进程对其他使用进程的权限约束,而不包括它自己。也就是对 node 的创建进程来说,它拥有对该 node 和 domain 所有操作权限(读写删,只要它不退出或者中止)
- 假设现在创建了一个 domain 为 d,node 为 n 的节点, 对于其他使用进程来说,操作权限如下表所示:
表 1. 操作权限
权限 \ 其他进程 | 读(get)n | 写(update)n | 删(delete)n | 删(delete)d |
---|---|---|---|---|
AuthPolicy.OP_READ | Yes | No | No | No |
AuthPolicy.OP_READ_WRITE | Yes | Yes | No | No |
AuthPolicy.OP_ALL | Yes | Yes | Yes | No |
从上表可以发现,当创建进程指定 node 的权限为 AuthPolicy.OP_ALL 时,其他使用进程可以删除该 node,但是不能删除其 domain,这是为什么呢?
因为 domain 下通常还有其他 node,它们的权限并不都是 AuthPolicy.OP_ALL,比如还有一个 n1 的 node 权限为 AuthPolicy.OP_READ,按照正常操作,该使用进程无法删除 n1。假设它可以删除 domain,那么它最后间接删除了 n1,于是发生了悖论,因此,为了避免风险,所有的使用进程只能根据权限删除 node,但是无法删除 domain。
不过你允许承担这样的删除风险,也可以在创建进程里强行指定该 domain 可删除,通过在 domain 创建后,调用:
public boolean setDeletable(String domain); |
该方法只能被 domain 的创建进程调用,其他使用进程没有权限调用。
强行指定可删除后,其他进程可以直接删除该 domain 及所含 node 并忽略后果。
Zookeeper 无疑是一款成功的开源产品,并拥有广泛的信任者和应用场景,做产品对比和列举优势往往容易引起激烈争论,会被认为是在宣传和引导产品使用。这里我们仅仅从技术角度阐述几点优势,Zookeeper 做为一个 chubby 和 paxos 模仿品,缺乏创新型的设计改进,它仍然存在以下缺点:
- 树型配置节点的繁琐复杂,性能低下。为了保证这种结构,Zookeeper 需要维持一套虚拟文件结构的开销,对于目录结构深的树节点,造成性能影响,而配置信息结构实际上往往不一定需要树结构。
- “观察”(watch)机制的僵化设计:zookeeper 没有获取最新版本信息的方法支持,它只能粗暴的在每次写入更新等方法时注册一个 watch,当这些方法被调用后就回调,它不考虑信息内容是否变化,对于没有使信息内容发生改变的更新,zookeeper 仍然会回调,并且 zookeeper 的回调比较呆板,它只能用一次,如果信息持续变化,必须又重新注册 watch。而 fourinone 的事件处理则可以自由控制是否持续响应信息变化。
- 领导者选举机制实现的太过局限,集群只有两个节点,zookeeper 无法进行领导者选举,zookeeper 的领导者选举必须要奇数节点的奇怪限制。另外,ZooKeeper 的领导者选举实现虽然比原始的 Paxos 要简化,但是它仍然存在领导者(Leader)、跟随者(Follower)、观察者(observer)、学习者 (Learner)等众多角色和跟随状态(Following)、寻找状态(Looking)、观察状态(Observing)、领导状态 (Leading)等复杂状态。相对于 fourinone 的领导者选举,zookeeper 仍然不够直观简洁,难以用较少配置和代码演示。
- Windows 系统上几乎不支持,需要安装 Linux 壳,并且仅建议用于学习研究。Fourinone 支持 Windows、Linux 集群混合使用。
Fourinone 提出一种新的分布式协同系统设计,在满足 Zookeeper 所有功能下,并克服了以上缺点,提出了新的配置结构、变化事件机制、简化的领导者选举实现,能更好的满足分布式协调需求。
paxos 在维持领导者选举或者是变量修改一致性上,采取一种类似议会投票的过半同意机制,比如设定一个领导者,需要将此看为一个议案,征求过半同意,每个节点通过一个议案会有编号记录,再次收到此领导者的不同人选,发现已经有编号记录便驳回,最后以多数通过的结果为准。
简言之,paxos 对每个节点的并发修改采取编号记录的方式保持一致性,对多个节点的并发修改采取少数服从多数的方式保持一致性。paxos 有点类似分布式二阶段提交方式,但是又不同,二阶段提交不能多数节点同意,必须是全部同意。为了保持过半节点同意的约束,paxos 算法往往要求节点总数为奇数。
Fourinone 选取领导者采取的是一种谦让方式,集群中节点会先询问其他节点是否愿意当领导者,没人愿意它才担任;如果已经有了领导了,那它就谦让;正因为大家都谦让,不互相争抢,领导者之间能避免冲突保持一致性。一旦确定了领导者,就只跟该领导者打交道,所有对变量的操作都是通过领导者进行,不会再去操作其他候选节点,操作结果由领导者统一同步到候选节点,跟上面 paxos 算法保证一致性的方式是不一样的。paxos 算法会去访问和操作所有节点征求同意,最后以多数节点的结果生效。
下面是一个操作节点的演示 demo,请留意各自节点的权限范围,程序说明:
- ParkServerDemo: 启动 parkserver(它的 IP 端口已经在配置文件的 PARK 部分的 SERVERS 指定
- ParkSet:往 parkserver 里创建了 d1n1、d2n2、d3n3、d4n4 共 4 个节点,分别对应只读、读写,所有,所有 + 强行删除权限
- ParkGet:依次对 d1n1、d2n2、d3n3、d4n4 进行读、写、删除、删除 domain 操作,观察结果输出,如果没有权限操作,parkserver 会输出信息,并且操作返回的结果对象为空
启动命令和顺序:
javac – classpath fourinone.jar; *.java java – classpath fourinone.jar; ParkServerDemo java – classpath fourinone.jar; ParkSet java – classpath fourinone.jar; ParkGet |
如果没有 fourinone.jar,可以到以下地址下载:http://www.skycn.com/soft/68321.html
下载
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
Demo 示例代码 | demo.zip | 122KB | HTTP |
学习
- 参考 Fourinone 2.0 下载地址。
- 参考 Zookeeper 下载地址 。
- 参考 Paxos 算法。
- developerWorks 云计算站点 提供了有关云计算的更新资源,包括
- 云计算 简介。
- 更新的 技术文章和教程,以及网络广播,让您的开发变得轻松,专家研讨会和录制会议 帮助您成为高效的云开发人员。
- 连接转为云计算设计的 IBM 产品下载和信息。
- 关于 社区最新话题 的活动聚合。
讨论
- 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。