一、ACL 介绍 ZooKeeper 作为一个分布式协调框架，内部存储的都是一些分布式系统运行时状态的元数据，尤其是一些涉及到分布式锁，master 选举和分布式协调等应用场景的数据，数据的访问权限涉及到 ZooKeeper 运行时安全。为了保障 ZooKeeper 的数据安全，提供了一套完整的 ACL (Access Control List) 权限控制机制来保障数据的安全。 ACL 的实现和 UNIX 的实现非常相似：它采用权限位来控制哪些操作被允许，哪些操作被禁止。但是和标准的 UNIX 权限不同的是，Znode 没有限制用户（user，即文件的所有者），组（group）和其他（world）。Zookeepr 是没有所有者的概念的。 每个 ZNode 的 ACL 是独立的，且子节点不会继承父节点的 ACL。例如：Znode/app 对于 ip 为 172.16.16.1 只有只读权限，而 /app/status 是 world 可读，那么任何人都可以获取 /app/status。所以在 Zookeeper 中权限是没有继承和传递关系的，每个 Znode 的权限都是独立存在的。 Zookeeper 支持可插拔的权限认证方案，分为三个维度：scheme，user，permission。通常表示为 [scheme:id:permissions]，其中 Scheme 表示使用何种方式来进行访问控制（使用何种权限验证机制），id 代表用户，Permission 表示有什么权限（权限组合字符串）。 一个 ZooKeeper 的节点 (znode) 存储两部分内容：数据和状态，状态中包含 ACL 信息。创建一个 znode 会产生一个 ACL 列表，列表中每个 ACL 包括以上三个维度。 二、ACL 的构成 2.1、scheme（权限模式） scheme 有以下几种类型： world：world 下只有一个 id，即只有一个用户，也就是 anyone，那么组合的写法就是 world:anyone:[permissions]。 ip：当设置为 ip 指定的 ip 地址，此时限制 ip 进行访问，比如 ip:192.168.77.130:[permissions]。也可以通过配置 IP 网段的方式，例如 “ip:192.168.28.1/24:[permissions]”，表示针对 192.168.28.* 整个 IP 段进行权限控制。 auth：代表认证登录，需要注册用户获取权限后才可以登录访问，形式为 auth:userpassword:[permissions]。 digest：需要对密码加密才能访问，组合形式为：digest:username:BASE64 (SHA1 (password)):[permissions]。具体的实现是由 DigestAuthenticationProvider.generateDigest () 方法来保证的。 super：代表超级管理员，拥有所有的权限，可以操作 ZooKeeper 上的任意节点数据。 auth 与 digest 的区别就是，前者使用明文密码进行登录，后者使用密文密码进行登录。 setAcl /path auth:lee:lee:cdrwa 与 setAcl /path digest:lee:BASE64 (SHA1 (password)):cdrwa 是等价的，在通过 addauth digest lee:lee 后都能操作指定节点的权限。在实际情况中，digest 要更为常用一些。 注意的是， exists 操作和 getAcl 操作并不受 ACL 许可控制，因此任何客户端可以查询节点的状态和节点的 ACL 。 2.2、id（授权对象） 对于不同的 scheme，授权的对象是不同的： IP 模式：授权对象通常是一个 ip 地址或者是一个 ip 段； Digest 模式：授权对象通常是自定义中的 username； World 模式：授权对象是 anyone； Super 模式：授权对象和 Digest 中的相同； 2.3、permissions（权限） 权限字符串缩写 crdwa ： CREATE：创建子节点权限（允许对子节点 Create 操作）； READ：访问节点 / 子节点权限（允许对本节点 GetChildren 和 GetData 操作）； WRITE：设置节点数据权限（允许对本节点 SetData 操作）； DELETE：删除子节点权限（允许对子节点 Delete 操作）； ADMIN：管理员权限（允许对本节点 setAcl 操作）； 三、ACL 命令 3.1、ACL 命令行操作 getAcl：获取某个节点的 ACL 权限信息； setAcl：设置某个节点的 ACL 权限信息； addauth：输入认证授权信息，注册时输入明文密码（登录），但是在 zk 的系统里，密码是以加密后的形式存在的； 3.2、ACL 命令行 world 使用 getAcl 命令获取某个节点的 acl 权限信息，示例： [zk: localhost:2181 (CONNECTED) 0] create /testDir/testAcl test-data # 创建一个子节点 Created /testDir/testAcl [zk: localhost:2181 (CONNECTED) 1] getAcl /testDir/testAcl # 获取该节点的 acl 权限信息 'world,'anyone # 默认为 world : cdrwa 使用 setAcl 命令设置某个节点的 acl 权限信息，示例： [zk: localhost:2181 (CONNECTED) 2] setAcl /testDir/testAcl world:anyone:crwa # 设置该节点的 acl 权限 cZxid = 0x67 ctime = Tue Aug 28 13:12:29 CST 2018 mZxid = 0x67 mtime = Tue Aug 28 13:12:29 CST 2018 pZxid = 0x67 cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 9 numChildren = 0 [zk: localhost:2181 (CONNECTED) 3] getAcl /testDir/testAcl 'world,'anyone : crwa # 设置成功后，该节点就少了 d 权限 [zk: localhost:2181 (CONNECTED) 4] create /testDir/testAcl/xyz xyz-data # 创建子节点 Created /testDir/testAcl/xyz [zk: localhost:2181 (CONNECTED) 5] delete /testDir/testAcl/xyz # 删除该子节点 Authentication is not valid : /testDir/testAcl/xyz # 由于没有 d 权限，所以提示无法删除 3.3、ACL 命令行 auth 使用 auth 来设置权限的时候，需要在 zk 里注册一个用户才可以。示例： [zk: localhost:2181 (CONNECTED) 6] addauth digest user1:123456 # 需要先添加一个用户 [zk: localhost:2181 (CONNECTED) 7] setAcl /testDir/testAcl auth:user1:123456:crwa # 然后才可以拿着这个用户去设置权限 cZxid = 0x67 ctime = Tue Aug 28 13:12:29 CST 2018 mZxid = 0x67 mtime = Tue Aug 28 13:12:29 CST 2018 pZxid = 0x69 cversion = 1 dataVersion = 0 aclVersion = 2 ephemeralOwner = 0x0 dataLength = 9 numChildren = 1 [zk: localhost:2181 (CONNECTED) 8] getAcl /testDir/testAcl 'digest,'user1:HYGa7IZRm2PUBFiFFu8xY2pPP/s= # 密码是以密文的形式存储的 : crwa 如果设置了权限后，再次进行设置，就可以不用加上用户名和密码了。而且就算是使用其他用户进行设置，也只会根据第一次设置的用户来进行配置： [zk: localhost:2181 (CONNECTED) 9] setAcl /testDir/testAcl auth::crw [zk: localhost:2181 (CONNECTED) 10] setAcl /testDir/testAcl auth:test:test:crw [zk: localhost:2181 (CONNECTED) 11] getAcl /testDir/testAcl 'digest,'user1:HYGa7IZRm2PUBFiFFu8xY2pPP/s= # 依旧是第一次设置的用户 : crw 3.4、ACL 命令行 digest 由于之前使用 auth 做实验的时候已经登录了用户 ，所以在使用 digest 做实验前，需要先退出一下客户端，再重新连接，这样之前登录的用户就会自动退出。退出用户后才能使用 digest 设置权限，示例： [zk: localhost:2181 (CONNECTED) 2] create /names/testDigest digest-data # 创建子节点 Created /names/testDigest [zk: localhost:2181 (CONNECTED) 3] addauth digest user1:123456 # 添加用户 [zk: localhost:2181 (CONNECTED) 4] getAcl /names/testDigest 'world,'anyone : cdrwa [zk: localhost:2181 (CONNECTED) 5] setAcl /names/testDigest digest:user1:HYGa7IZRm2PUBFiFFu8xY2pPP/s=:crwa # 使用 digest 来设置权限 cZxid = 0x72 ctime = Tue Aug 28 14:37:05 CST 2018 mZxid = 0x72 mtime = Tue Aug 28 14:37:05 CST 2018 pZxid = 0x72 cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 11 numChildren = 0 [zk: localhost:2181 (CONNECTED) 6] getAcl /names/testDigest 'digest,'user1:HYGa7IZRm2PUBFiFFu8xY2pPP/s= : crwa 可以看到，digest 和 auth 除了一个使用明文一个使用密文之外，其他都是一致的，所以它俩的区别就只是密文和明文的区别。在线上环境中，一般使用 digest 比较多，因为密文安全一些。 3.5、ACL 命令行 ip 这种方式和限制 ip 是一样的，就是设置只允许某一个 ip 有权限操作。示例： [zk: localhost:2181 (CONNECTED) 7] create /names/testip ip-data # 创建子节点 Created /names/testip [zk: localhost:2181 (CONNECTED) 8] getAcl /names/testip 'world,'anyone : cdrwa [zk: localhost:2181 (CONNECTED) 9] setAcl /names/testip ip:192.168.190.129:cdrwa # 使用 ip 来设置权限 cZxid = 0x74 ctime = Tue Aug 28 15:03:56 CST 2018 mZxid = 0x74 mtime = Tue Aug 28 15:03:56 CST 2018 pZxid = 0x74 cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 7 numChildren = 0 [zk: localhost:2181 (CONNECTED) 10] getAcl /names/testip 'ip,'192.168.190.129 # 只允许这个 ip 拥有该节点的 cdrwa 权限 : cdrwa [zk: localhost:2181 (CONNECTED) 11] get /names/testip Authentication is not valid : /names/testip # 由于本机的 ip 不是 192.168.190.129，所以获取失败 3.6、ACL 命令行 super 超级管理员 基本所有的系统都会拥有一个超级管理员用户，zk 也不例外。当我们给一些节点设置了权限，但是却发现设置错误了，导致节点无法正常访问，那么这时候普通用户是无法解决这种问题的，只能使用超级管理员用户来重新设置权限或删除节点。所以本节简单演示一下，如何配置管理员用户： 1、修改 zkServer.sh 增加 super 管理员 [root@study-01 /usr/local/zookeeper-3.4.11/bin]# vim zkServer.sh # 找到 nohup 那一行，加入以下内容 "-Dzookeeper.DigestAuthenticationProvider.superDigest=user1:HYGa7IZRm2PUBFiFFu8xY2pPP/s=" 注：user1 是用户名，后面那一大串是加密后的密文密码 2、修改完之后，需要重启 zookeeper 服务才能生效 ./zkServer.sh restart 3、使用超级管理员用户 [root@study-01 ~]# zkCli.sh [zk: localhost:2181 (CONNECTED) 9] ls /names/ip # ls 一个无权限的节点 Authentication is not valid : /names/ip # 可以看到，权限不够 [zk: localhost:2181 (CONNECTED) 10] addauth digest user1:123456 # 登录超级管理员用户，这里登录用的是明文密码 [zk: localhost:2181 (CONNECTED) 11] ls /names/ip # 然后再次 ls [] # 这次就可以 ls 到了 [zk: localhost:2181 (CONNECTED) 12] get /names/ip # 也可以 get 信息 ip-data cZxid = 0x51 ctime = Mon Apr 23 21:02:42 CST 2018 mZxid = 0x51 mtime = Mon Apr 23 21:02:42 CST 2018 pZxid = 0x51 cversion = 0 dataVersion = 0 aclVersion = 1 ephemeralOwner = 0x0 dataLength = 7 numChildren = 0 [zk: localhost:2181 (CONNECTED) 13] delete /names/testip # 删除节点也可以 四、ACL 的常用使用场景 开发 / 测试环境分离，开发者无权操作测试库的节点，只能读取。 生产环境上控制指定 ip 的服务可以访问相关的节点，防止混乱。 五、恢复 zk 不使用认证 若不再需要用户密码认证，也可以取消。 # 取消认证 setAcl /world:anyone:cdrwa # 或者给 zk 添加跳过验证 skipACL = yes
一、Zookeeper 日志 Zookeeper 服务器会产生三类日志：事务日志、快照日志和 log4j 日志。 在 zookeeper 默认配置文件 zoo.cfg（可以修改文件名）中有一个配置项 dataDir，该配置项用于配置 zookeeper 快照日志和事务日志的存储地址。在官方提供的默认参考配置文件 zoo_sample.cfg 中，只有 dataDir 配置项。其实在实际应用中，还可以为事务日志专门配置存储地址，配置项名称为 dataLogDir，在 zoo_sample.cfg 中并未体现出来。 在没有 dataLogDir 配置项的时候，zookeeper 默认将事务日志文件和快照日志文件都存储在 dataDir 对应的目录下。建议将事务日志（dataLogDir）与快照日志（dataLog）单独配置，因为当 zookeeper 集群进行频繁的数据读写操作是，会产生大量的事务日志信息，将两类日志分开存储会提高系统性能，而且，可以允许将两类日志存在在不同的存储介质上，减少磁盘压力。 log4j 用于记录 zookeeper 集群服务器运行日志，该日志的配置地址在 conf/ 目录下的 log4j.properties 文件中，该文件中有一个配置项为 zookeeper.log.dir=.，表示 log4j 日志文件在与执行程序（zkServer.sh）在同一目录下。当执行 zkServer.sh 时，在该文件夹下会产生 zookeeper.out 日志文件。 二、事务日志 事务日志指 zookeeper 系统在正常运行过程中，针对所有的更新操作，在返回客户端 “更新成功” 的响应前，zookeeper 会保证已经将本次更新操作的事务日志已经写到磁盘上，只有这样，整个更新操作才会生效。 根据上文所述，可以通过 zoo.cfg 文件中的 dataLogDir 配置项找到事物日志存储地点： 在 datalog/ 目录下存在一个文件夹 version-2，该文件夹中保存着事物日志文件: 日志文件的命名规则为 log.，文件大小为 64MB 表示写入该日志的第一个事务的 ID，十六进制表示。 三、事务日志可视化 Zookeeper 的事务日志为二进制文件，不能通过 vim 等工具直接访问。其实可以通过 zookeeper 自带的 jar 包读取事务日志文件。 首先将 libs 中的 slf4j-api-1.6.1.jar 文件和 zookeeper 根目录下的 zookeeper-3.4.9.jar 文件复制到临时文件夹 tmplibs 中，然后执行如下命令： java -classpath .:slf4j-api-1.7.25.jar:zookeeper-3.4.14.jar org.apache.zookeeper.server.LogFormatter/opt/zookeeper/transactionLog/version-2/log.504e25800 > a.txt 其中：/opt/zookeeper/transactionLog/version-2/log.4 是日志路径下的日志文件。 命令是在 zookeeper 的 bin 目录下执行的，因此使用.:lib。 zookeeper-3.4.14.jar 表示我所使用的 zk 版本是 3.4.12 版本。 四、快照日志 Zookeeper 的数据在内存中是以树形结构进行存储的，而快照就是每隔一段时间就会把整个 DataTree 的数据序列化后存储在磁盘中，这就是 zookeeper 的快照文件。 zookeeper 快照日志的存储路径同样可以在 zoo.cfg 中查看，如上文截图所示。访问 dataDir 路径可以看到 version-2 文件夹: 从截图中可以看出，zookeeper 快照文件的命名规则为 snapshot.**，其中 ** 表示 zookeeper 触发快照的那个瞬间，提交的最后一个事务的 ID。 五、日志清理 在 zookeeper 3.4.0 以后，zookeeper 提供了自动清理 snapshot 和事务日志功能，通过配置 zoo.cfg 下的。 autopurge.snapRetainCount 和 autopurge.purgeInterval 这两个参数实现日志文件的定时清理： autopurge.snapRetainCount 这个参数指定了需要保留的文件数目，默认保留 3 个。 autopurge.purgeInterval 这个参数指定了清理频率，单位是小时，需要填写一个 1 或者更大的数据，默认 0 表示不开启自动清理功能。 六、事务日志和快照日志的关联 ZooKeeper 集群中的每个服务器节点每次接收到写操作请求时，都会先将这次请求发送给 leader，leader 将这次写操作转换为带有状态的事务，然后 leader 会对这次写操作广播出去以便进行协调。当协调通过 (大多数节点允许这次写) 后，leader 通知所有的服务器节点，让它们将这次写操作应用到内存数据库中，并将其记录到事务日志中。当事务日志记录的次数达到一定数量后 (默认 10W 次)，就会将内存数据库序列化一次，使其持久化保存到磁盘上，序列化后的文件称为 "快照文件"。每次拍快照都会生成新的事务日志。有了事务日志和快照，就可以让任意节点恢复到任意时间点 (只要没有清理事务日志和快照)。 事务日志和快照日志可用于 zookeeper 恢复数据，假设 t1 时刻保存了快照日志，t1 到 t2 时刻保存了事务日志 (这段时间内没有再新增快照)，那么 t2 时刻重启 zookeeper，数据不会丢失 快照相当于将当前的 zookeeper 状态记录了下来，保留了快照之后，该快照之前创建的快照和事务日志丢失是没有影响的。 如果 t1 时刻保存了快照，但是 t1 到 t2 的事务日志丢失并且后面也没有快照，那么 t1 到 t2 之间的数据无法恢复，只能恢复到 t1 时刻的状态。
一、dubbo配置 1.1、配置原则 JVM 启动 -D 参数优先，这样可以使用户在部署和启动时进行参数重写，比如在启动时需改变协议的端口。 XML 次之，如果在 XML 中有配置，则 dubbo.properties 中的相应配置项无效。 Properties 最后，相当于缺省值，只有 XML 没有配置时，dubbo.properties 的相应配置项才会生效，通常用于共享公共配置，比如应用名。 1.2、重试次数 失败自动切换，当出现失败，重试其它服务器，但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。 重试次数配置如下： <dubbo:service retries="2" /> 或 <dubbo:reference retries="2" /> 或 <dubbo:reference> <dubbo:method name="findFoo" retries="2" /> </dubbo:reference> 1.3、超时时间 由于网络或服务端不可靠，会导致调用出现一种不确定的中间状态（超时）。为了避免超时导致客户端资源（线程）挂起耗尽，必须设置超时时间。 1、Dubbo消费端 全局超时配置 <dubbo:consumer timeout="5000" /> 指定接口以及特定方法超时配置 <dubbo:reference interface="com.foo.BarService" timeout="2000"> <dubbo:method name="sayHello" timeout="3000" /> </dubbo:reference> 2、Dubbo服务端 全局超时配置 <dubbo:provider timeout="5000" /> 指定接口以及特定方法超时配置 <dubbo:provider interface="com.foo.BarService" timeout="2000"> <dubbo:method name="sayHello" timeout="3000" /> </dubbo:provider> 3、配置原则 dubbo 推荐在 Provider 上尽量多配置 Consumer 端属性： 1、作服务的提供者，比服务使用方更清楚服务性能参数，如调用的超时时间，合理的重试次数，等等。 2、在 Provider 配置后，Consumer 不配置则会使用 Provider 的配置值，即 Provider 配置可以作为 Consumer 的缺省值。否则，Consumer 会使用 Consumer 端的全局设置，这对于 Provider 不可控的，并且往往是不合理的。 配置的覆盖规则： 方法级配置别优于接口级别，即小 Scope 优先 。 Consumer 端配置 优于 Provider 配置 优于全局配置。 最后是 Dubbo Hard Code 的配置值（见配置文档）。 1.4、版本号 当一个接口实现，出现不兼容升级时，可以用版本号过渡，版本号不同的服务相互间不引用。 可以按照以下的步骤进行版本迁移： 在低压力时间段，先升级一半提供者为新版本。 再将所有消费者升级为新版本。 然后将剩下的一半提供者升级为新版本。 老版本服务提供者配置： <dubbo:service interface="com.foo.BarService" version="1.0.0" /> 新版本服务提供者配置： <dubbo:service interface="com.foo.BarService" version="2.0.0" /> 老版本服务消费者配置： <dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" /> 新版本服务消费者配置： <dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" /> 如果不需要区分版本，可以按照以下的方式配置： <dubbo:reference id="barService" interface="com.foo.BarService" version="*" /> 二、高可用 2.1、zookeeper宕机与dubbo直连 现象：zookeeper 注册中心宕机，还可以消费 dubbo 暴露的服务。 原因： 健壮性： 监控中心宕掉不影响使用，只是丢失部分采样数据。 数据库宕掉后，注册中心仍能通过缓存提供服务列表查询，但不能注册新服务。 注册中心对等集群，任意一台宕掉后，将自动切换到另一台。 注册中心全部宕掉后，服务提供者和服务消费者仍能通过本地缓存通讯。 服务提供者无状态，任意一台宕掉后，不影响使用。 服务提供者全部宕掉后，服务消费者应用将无法使用，并无限次重连等待服务提供者恢复。 高可用：通过设计，减少系统不能提供服务的时间； 2.2、集群下dubbo负载均衡配置 在集群负载均衡时，Dubbo 提供了多种均衡策略，缺省为 random 随机调用。 负载均衡策略： Random LoadBalance 随机，按权重设置随机概率。 在一个截面上碰撞的概率高，但调用量越大分布越均匀，而且按概率使用权重后也比较均匀，有利于动态调整提供者权重。 RoundRobin LoadBalance 轮循，按公约后的权重设置轮循比率。 存在慢的提供者累积请求的问题，比如：第二台机器很慢，但没挂，当请求调到第二台时就卡在那，久而久之，所有请求都卡在调到第二台上。 LeastActive LoadBalance 最少活跃调用数，相同活跃数的随机，活跃数指调用前后计数差。 使慢的提供者收到更少请求，因为越慢的提供者的调用前后计数差会越大。 ConsistentHash LoadBalance 一致性 Hash，相同参数的请求总是发到同一提供者。 当某一台提供者挂时，原本发往该提供者的请求，基于虚拟节点，平摊到其它提供者，不会引起剧烈变动。算法参见：http://en.wikipedia.org/wiki/Consistent_hashing 缺省只对第一个参数 Hash，如果要修改，请配置 <dubbo:parameter key="hash.arguments" value="0,1" /> 缺省用 160 份虚拟节点，如果要修改，请配置 <dubbo:parameter key="hash.nodes" value="320" /> 2.3、整合hystrix，服务熔断与降级处理 1、服务降级 什么是服务降级？ 当服务器压力剧增的情况下，根据实际业务情况及流量，对一些服务和页面有策略的不处理或换种简单的方式处理，从而释放服务器资源以保证核心交易正常运作或高效运作。 可以通过服务降级功能临时屏蔽某个出错的非关键服务，并定义降级后的返回策略。 向注册中心写入动态配置覆盖规则： RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null")); 其中： mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值，不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。 还可以改为 mock=fail:return+null 表示消费方对该服务的方法调用在失败后，再返回 null 值，不抛异常。用来容忍不重要服务不稳定时对调用方的影响。 2、集群容错 在集群调用失败时，Dubbo 提供了多种容错方案，缺省为 failover 重试。 集群容错模式： Failover Cluster 失败自动切换，当出现失败，重试其它服务器。通常用于读操作，但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。 重试次数配置如下： <dubbo:service retries="2" /> 或 <dubbo:reference retries="2" /> 或 <dubbo:reference> <dubbo:method name="findFoo" retries="2" /> </dubbo:reference> Failfast Cluster 快速失败，只发起一次调用，失败立即报错。通常用于非幂等性的写操作，比如新增记录。 Failsafe Cluster 失败安全，出现异常时，直接忽略。通常用于写入审计日志等操作。 Failback Cluster 失败自动恢复，后台记录失败请求，定时重发。通常用于消息通知操作。 Forking Cluster 并行调用多个服务器，只要一个成功即返回。通常用于实时性要求较高的读操作，但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 Broadcast Cluster 广播调用所有提供者，逐个调用，任意一台报错则报错 [2]。通常用于通知所有提供者更新缓存或日志等本地资源信息。 集群模式配置 按照以下示例在服务提供方和消费方配置集群模式 <dubbo:service cluster="failsafe" /> 或 <dubbo:reference cluster="failsafe" /> 3、整合hystrix Hystrix 旨在通过控制那些访问远程系统、服务和第三方库的节点，从而对延迟和故障提供更强大的容错能力。Hystrix具备拥有回退机制和断路器功能的线程和信号隔离，请求缓存和请求打包，以及监控和配置等功能。 1、配置spring-cloud-starter-netflix-hystrix spring boot 官方提供了对 hystrix 的集成，直接在 pom.xml 里加入依赖： <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> <version>1.4.4.RELEASE</version> </dependency> 然后在 Application 类上增加@EnableHystrix来启用 hystrix starter： @SpringBootApplication @EnableHystrix public class ProviderApplication { 2、配置Provider端 在Dubbo的Provider上增加@HystrixCommand配置，这样子调用就会经过 Hystrix 代理。 @Service(version = "1.0.0") public class HelloServiceImpl implements HelloService { @HystrixCommand(commandProperties = { @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000") }) @Override public String sayHello(String name) { // System.out.println("async provider received: " + name); // return "annotation: hello, " + name; throw new RuntimeException("Exception to show hystrix enabled."); } } 3、配置Consumer端 对于Consumer端，则可以增加一层method调用，并在method上配置@HystrixCommand。当调用出错时，会走到fallbackMethod = "reliable"的调用里。 @Reference(version = "1.0.0") private HelloService demoService; @HystrixCommand(fallbackMethod = "reliable") public String doSayHello(String name) { return demoService.sayHello(name); } public String reliable(String name) { return "hystrix fallback value"; } 三、dubbo原理 3.1、RPC原理 一次完整的RPC调用流程（同步调用，异步另说）如下： 1）服务消费方（client）调用以本地调用方式调用服务； 2）client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体； 3）client stub找到服务地址，并将消息发送到服务端； 4）server stub收到消息后进行解码； 5）server stub根据解码结果调用本地的服务； 6）本地服务执行并将结果返回给server stub； 7）server stub将返回结果打包成消息并发送至消费方； 8）client stub接收到消息，并进行解码； 9）服务消费方得到最终结果。 RPC框架的目标就是要2~8这些步骤都封装起来，这些细节对用户来说是透明的，不可见的。 3.2、netty通信原理 Netty是一个异步事件驱动的网络应用程序框架， 用于快速开发可维护的高性能协议服务器和客户端。它极大地简化并简化了TCP和UDP套接字服务器等网络编程。 BIO：(Blocking IO) NIO (Non-Blocking IO) Selector 一般称 为选择器 ，也可以翻译为 多路复用器， Connect（连接就绪）、Accept（接受就绪）、Read（读就绪）、Write（写就绪） Netty基本原理： 3.3、dubbo原理 1、dubbo原理-框架设计 config 配置层：对外配置接口，以 ServiceConfig, ReferenceConfig 为中心，可以直接初始化配置类，也可以通过 spring 解析配置生成配置类 proxy 服务代理层：服务接口透明代理，生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心，扩展接口为 ProxyFactory registry 注册中心层：封装服务地址的注册与发现，以服务 URL 为中心，扩展接口为 RegistryFactory, Registry, RegistryService cluster 路由层：封装多个提供者的路由及负载均衡，并桥接注册中心，以 Invoker 为中心，扩展接口为 Cluster, Directory, Router, LoadBalance monitor 监控层：RPC 调用次数和调用时间监控，以 Statistics 为中心，扩展接口为 MonitorFactory, Monitor, MonitorService protocol 远程调用层：封装 RPC 调用，以 Invocation, Result 为中心，扩展接口为 Protocol, Invoker, Exporter exchange 信息交换层：封装请求响应模式，同步转异步，以 Request, Response 为中心，扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer transport 网络传输层：抽象 mina 和 netty 为统一接口，以 Message 为中心，扩展接口为 Channel, Transporter, Client, Server, Codec serialize 数据序列化层：可复用的一些工具，扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool 2、dubbo原理-启动解析、加载配置信息 3、dubbo原理-服务暴露 4、dubbo原理-服务引用 5、dubbo原理-服务调用
一、dubbo核心概念 1.1、简介 Apache Dubbo (incubating) 是一款高性能、轻量级的开源 Java RPC 框架，它提供了三大核心能力：面向接口的远程方法调用，智能容错和负载均衡，以及服务自动注册和发现。 官网：http://dubbo.apache.org/ 1.2、基本概念 服务提供者(Provider)：暴露服务的服务提供方，服务提供者在启动时，向注册中心注册自己提供的服务。 服务消费者(Consumer): 调用远程服务的服务消费方，服务消费者在启动时，向注册中心订阅自己所需的服务，服务消费者，从提供者地址列表中，基于软负载均衡算法，选一台提供者进行调用，如果调用失败，再选另一台调用。 注册中心(Registry)：注册中心返回服务提供者地址列表给消费者，如果有变更，注册中心将基于长连接推送变更数据给消费者。 监控中心(Monitor)：服务消费者和提供者，在内存中累计调用次数和调用时间，定时每分钟发送一次统计数据到监控中心。 调用关系说明： 服务容器负责启动，加载，运行服务提供者。 服务提供者在启动时，向注册中心注册自己提供的服务。 服务消费者在启动时，向注册中心订阅自己所需的服务。 注册中心返回服务提供者地址列表给消费者，如果有变更，注册中心将基于长连接推送变更数据给消费者。 服务消费者，从提供者地址列表中，基于软负载均衡算法，选一台提供者进行调用，如果调用失败，再选另一台调用。 服务消费者和提供者，在内存中累计调用次数和调用时间，定时每分钟发送一次统计数据到监控中心。 二、dubbo环境搭建 2.1、【windows】-安装zookeeper 1、下载zookeeper 网址：https://archive.apache.org/dist/zookeeper/zookeeper-3.4.13/ 2、解压zookeeper 解压运行zkServer.cmd，初次运行会报错，没有zoo.cfg配置文件。 3、修改zoo.cfg配置文件 将 conf 下的 zoo_sample.cfg 复制一份改名为 zoo.cfg 即可。 注意几个重要位置： dataDir=./ 临时数据存储的目录（可写相对路径） clientPort=2181 zookeeper的端口号 修改完成后再次启动 zookeeper。 4、使用zkCli.cmd测试 ls /：列出zookeeper根下保存的所有节点 create –e /atguigu 123：创建一个atguigu节点，值为123 get /test：获取/test节点的值 2.2、【windows】-安装dubbo-admin管理控制台 dubbo 本身并不是一个服务软件。它其实就是一个 jar 包能够帮你的 java 程序连接到zookeeper，并利用 zookeeper 消费、提供服务。所以不用在 Linux 上启动什么 dubbo 服务。 但是为了让用户更好的管理监控众多的 dubbo 服务，官方提供了一个可视化的监控程序，不过这个监控即使不装也不影响使用。 1、下载dubbo-admin https://github.com/apache/incubator-dubbo-ops 2、进入目录，修改dubbo-admin配置 修改 src\main\resources\application.properties 指定zookeeper地址 3、打包dubbo-admin mvn clean package -Dmaven.test.skip=true 4、运行dubbo-admin java -jar dubbo-admin-0.0.1-SNAPSHOT.jar 注意：【有可能控制台看着启动了，但是网页打不开，需要在控制台按下ctrl+c即可】 默认使用 root/root 登录。 2.3、【linux】-安装zookeeper 1、安装jdk 1、下载jdk http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 2、上传到服务器并解压 3、设置环境变量 vim /etc/profile 文件末尾加入下面配置 export JAVA_HOME=/usr/local/java/jdk1.8.0_171 export JRE_HOME=${JAVA_HOME}/jre export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH=${JAVA_HOME}/bin:$PATH 4、使环境变量生效&测试JDK 2、安装zookeeper 1、下载zookeeper 网址：https://archive.apache.org/dist/zookeeper/zookeeper-3.4.11/ wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.11/zookeeper-3.4.11.tar.gz 2、解压 3、移动到指定位置并改名为 zookeeper。 3、开机启动zookeeper 1）复制如下脚本 #!/bin/bash #chkconfig:2345 20 90 #description:zookeeper #processname:zookeeper ZK_PATH=/usr/local/zookeeper export JAVA_HOME=/usr/local/java/jdk1.8.0_171 case $1 in start) sh $ZK_PATH/bin/zkServer.sh start;; stop) sh $ZK_PATH/bin/zkServer.sh stop;; status) sh $ZK_PATH/bin/zkServer.sh status;; restart) sh $ZK_PATH/bin/zkServer.sh restart;; *) echo "require start|stop|status|restart" ;; esac 2）把脚本注册为Service 3）增加权限 4、配置zookeeper 1、初始化zookeeper配置文件 拷贝/usr/local/zookeeper/conf/zoo_sample.cfg。 到同一个目录下改个名字叫zoo.cfg。 2、启动zookeeper 2.4、【linux】-安装dubbo-admin管理控制台 1、安装 Tomcat8（旧版dubbo-admin是war，新版是jar不需要安装Tomcat） 1、下载 Tomcat8 并解压 https://tomcat.apache.org wget http://mirrors.shu.edu.cn/apache/tomcat/tomcat-8/v8.5.32/bin/apache-tomcat-8.5.32.tar.gz 2、解压移动到指定位置 3、开机启动 tomcat8 3、开机启动 tomcat8 复制如下脚本 #!/bin/bash #chkconfig:2345 21 90 #description:apache-tomcat-8 #processname:apache-tomcat-8 CATALANA_HOME=/opt/apache-tomcat-8.5.32 export JAVA_HOME=/opt/java/jdk1.8.0_171 case $1 in start) echo "Starting Tomcat..." $CATALANA_HOME/bin/startup.sh ;; stop) echo "Stopping Tomcat..." $CATALANA_HOME/bin/shutdown.sh ;; restart) echo "Stopping Tomcat..." $CATALANA_HOME/bin/shutdown.sh sleep 2 echo echo "Starting Tomcat..." $CATALANA_HOME/bin/startup.sh ;; *) echo "Usage: tomcat {start|stop|restart}" ;; esac 4、注册服务&添加权限 chkconfig --addd tomcat8 chmod +x tomcat8 5、启动服务&访问 tomcat 测试 2、安装dubbo-admin dubbo 本身并不是一个服务软件。它其实就是一个 jar 包能够帮你的java程序连接到 zookeeper，并利用 zookeeper 消费、提供服务。所以不用在 Linux 上启动什么 dubbo 服务。 但是为了让用户更好的管理监控众多的dubbo服务，官方提供了一个可视化的监控程序，不过这个监控即使不装也不影响使用。 1、下载dubbo-admin https://github.com/apache/incubator-dubbo-ops 2、进入目录，修改dubbo-admin配置 修改 src\main\resources\application.properties 指定zookeeper地址. 3、打包dubbo-admin mvn clean package -Dmaven.test.skip=true 4、运行dubbo-admin java -jar dubbo-admin-0.0.1-SNAPSHOT.jar 默认使用root/root登录。
一、简介 ZooKeeper 是用来协调 (同步) 分布式进程的服务，提供了一个简单高性能的协调内核，用户可以在此之上构建更多复杂的分布式协调功能。脑裂通常会出现在集群环境中，比如 ElasticSearch、Zookeeper 集群，而这些集群环境有一个统一的特点，就是它们有一个大脑，比如 ElasticSearch 集群中有 Master 节点，Zookeeper 集群中有 Leader 节点。 二、奇数节点 zookeeper 容错指的是：当宕掉几个 zookeeper 节点服务器之后，剩下的个数必须大于宕掉的个数，也就是剩下的节点服务数必须大于 n/2，这样 zookeeper 集群才可以继续使用，无论奇偶数都可以选举 leader。例如 5 台 zookeeper 节点机器最多宕掉 2 台，还可以继续使用，因为剩下 3 台大于 5/2。那么为什么最好为奇数个节点呢？是在以最大容错服务器个数的条件下，会节省资源。比如，最大容错为 2 的情况下，对应的 zookeeper 服务数，奇数为 5，而偶数为 6，也就是 6 个 zookeeper 服务的情况下最多能宕掉 2 个服务，所以从节约资源的角度看，没必要部署 6 (偶数) 个 zookeeper 服务节点。 zookeeper 集群有这样一个特性：集群中只要有过半的机器是正常工作的，那么整个集群对外就是可用的。也就是说如果有 2 个 zookeeper 节点，那么只要有 1 个 zookeeper 节点死了，那么 zookeeper 服务就不能用了，因为 1 没有过半，所以 2 个 zookeeper 的死亡容忍度为 0；同理，要是有 3 个 zookeeper，一个死了，还剩下 2 个正常的，过半了，所以 3 个 zookeeper 的容忍度为 1；同理也可以多列举几个：2->0; 3->1; 4->1; 5->2; 6->2 就会发现一个规律，2n 和 2n-1 的容忍度是一样的，都是 n-1，所以为了更加高效，何必增加那一个不必要的 zookeeper 呢。 根据以上可以得出结论：从资源节省的角度来考虑，zookeeper 集群的节点最好要部署成奇数个。 三、集群脑裂场景说明 对于一个集群，想要提高这个集群的可用性，通常会采用多机房部署，比如现有一个由 6 台 zkServer 所组成的一个集群，部署在了两个机房： 正常情况下，此集群只会有一个 Leader，那么如果机房之间的网络断了之后，两个机房内的 zkServer 还是可以相互通信的，如果不考虑过半机制，那么就会出现每个机房内部都将选出一个 Leader。 这就相当于原本一个集群，被分成了两个集群，出现了两个 "大脑"，这就是所谓的 脑裂现象。对于这种情况，其实也可以看出来，原本应该是统一的一个集群对外提供服务的，现在变成了两个集群同时对外提供服务，如果过了一会，断了的网络突然联通了，那么此时就会出现问题了，两个集群刚刚都对外提供服务了，数据该怎么合并，数据冲突怎么解决等等问题。刚刚在说明脑裂场景时有一个前提条件就是没有考虑过半机制，所以实际上 Zookeeper 集群中是不会轻易出现脑裂问题的，原因在于过半机制。 zookeeper 的过半机制：在领导者选举的过程中，如果某台 zkServer 获得了超过半数的选票，则此 zkServer 就可以成为 Leader 了。 举个简单的例子：如果现在集群中有 5 台 zkServer，那么 half=5/2=2，那么也就是说，领导者选举的过程中至少要有三台 zkServer 投了同一个 zkServer，才会符合过半机制，才能选出来一个 Leader。 问题 1：zookeeper 选举的过程中为什么一定要有一个过半机制验证？因为这样不需要等待所有 zkServer 都投了同一个 zkServer 就可以选举出来一个 Leader 了，这样比较快，所以叫快速领导者选举算法。 问题 2：过半机制中为什么是大于，而不是大于等于呢？这就是更脑裂问题有关系了，比如回到上文出现脑裂问题的场景。 当机房中间的网络断掉之后，机房 1 内的三台服务器会进行领导者选举，但是此时过半机制的条件是 "节点数 > 3"，也就是说至少要 4 台 zkServer 才能选出来一个 Leader，所以对于机房 1 来说它不能选出一个 Leader，同样机房 2 也不能选出一个 Leader，这种情况下整个集群当机房间的网络断掉后，整个集群将没有 Leader。 而如果过半机制的条件是 "节点数 >= 3"，那么机房 1 和机房 2 都会选出一个 Leader，这样就出现了脑裂。这就可以解释为什么过半机制中是大于而不是大于等于，目的就是为了防止脑裂。 假设我们现在只有 5 台机器，也部署在两个机房： 此时过半机制的条件是 "节点数 > 2"，也就是至少要 3 台服务器才能选出一个 Leader，此时机房件的网络断开了，对于机房 1 来说是没有影响的，Leader 依然还是 Leader，对于机房 2 来说是选不出来 Leader 的，此时整个集群中只有一个 Leader。所以可以简单总结得出，有了过半机制，对于一个 Zookeeper 集群来说，要么没有 Leader，要么只有 1 个 Leader，这样 zookeeper 也就能避免了脑裂问题。 四、集群脑裂问题处理 4.1、什么是脑裂 简单点来说，脑裂 (Split-Brain) 就是比如当你的 cluster 里面有两个节点，它们都知道在这个 cluster 里需要选举出一个 master。那么当它们两个之间的通信完全没有问题的时候，就会达成共识，选出其中一个作为 master。但是如果它们之间的通信出了问题，那么两个结点都会觉得现在没有 master，所以每个都把自己选举成 master，于是 cluster 里面就会有两个 master。 对于 Zookeeper 来说有一个很重要的问题，就是到底是根据一个什么样的情况来判断一个节点死亡 down 掉了。 在分布式系统中这些都是有监控者来判断的，但是监控者也很难判定其他的节点的状态，唯一一个可靠的途径就是心跳，Zookeeper 也是使用心跳来判断客户端是否仍然活着。 使用 ZooKeeper 来做 Leader HA 基本都是同样的方式：每个节点都尝试注册一个象征 leader 的临时节点，其他没有注册成功的则成为 follower，并且通过 watch 机制，监控着 leader 所创建的临时节点，Zookeeper 通过内部心跳机制来确定 leader 的状态，一旦 leader 出现意外 Zookeeper 能很快获悉并且通知其他的 follower，其他 flower 在之后作出相关反应，这样就完成了一个切换，这种模式也是比较通用的模式，基本大部分都是这样实现的。但是这里面有个很严重的问题，如果注意不到会导致短暂的时间内系统出现脑裂，因为心跳出现超时可能是 leader 挂了，但是也可能是 zookeeper 节点之间网络出现了问题，导致 leader 假死的情况，leader 其实并未死掉，但是与 ZooKeeper 之间的网络出现问题导致 Zookeeper 认为其挂掉了然后通知其他节点进行切换，这样 follower 中就有一个成为了 leader，但是原本的 leader 并未死掉，这时候 client 也获得 leader 切换的消息，但是仍然会有一些延时，zookeeper 需要通讯需要一个一个通知，这时候整个系统就很混乱可能有一部分 client 已经通知到了连接到新的 leader 上去了，有的 client 仍然连接在老的 leader 上，如果同时有两个 client 需要对 leader 的同一个数据更新，并且刚好这两个 client 此刻分别连接在新老的 leader 上，就会出现很严重问题。 这里做下小总结：假死：由于心跳超时 (网络原因导致的) 认为 leader 死了，但其实 leader 还存活着。脑裂：由于假死会发起新的 leader 选举，选举出一个新的 leader，但旧的 leader 网络又通了，导致出现了两个 leader ，有的客户端连接到老的 leader，而有的客户端则连接到新的 leader。 4.2、原因 主要原因是 Zookeeper 集群和 Zookeeper client 判断超时并不能做到完全同步，也就是说可能一前一后，如果是集群先于 client 发现，那就会出现上面的情况。同时，在发现并切换后通知各个客户端也有先后快慢。一般出现这种情况的几率很小，需要 leader 节点与 Zookeeper 集群网络断开，但是与其他集群角色之间的网络没有问题，还要满足上面那些情况，但是一旦出现就会引起很严重的后果，数据不一致。 4.3、如何解决 要解决 Split-Brain 脑裂的问题，一般有下面几种种方法： Quorums (法定人数) 方式：比如 3 个节点的集群，Quorums = 2, 也就是说集群可以容忍 1 个节点失效，这时候还能选举出 1 个 lead，集群还可用。比如 4 个节点的集群，它的 Quorums = 3，Quorums 要超过 3，相当于集群的容忍度还是 1，如果 2 个节点失效，那么整个集群还是无效的。这是 zookeeper 防止 "脑裂" 默认采用的方法。 采用 Redundant communications (冗余通信) 方式：集群中采用多种通信方式，防止一种通信方式失效导致集群中的节点无法通信。 Fencing (共享资源) 方式：比如能看到共享资源就表示在集群中，能够获得共享资源的锁的就是 Leader，看不到共享资源的，就不在集群中。 仲裁机制方式。 启动磁盘锁定方式。 要想避免 zookeeper"脑裂" 情况其实也很简单，在 follower 节点切换的时候不在检查到老的 leader 节点出现问题后马上切换，而是在休眠一段足够的时间，确保老的 leader 已经获知变更并且做了相关的 shutdown 清理工作了然后再注册成为 master 就能避免这类问题了，这个休眠时间一般定义为与 zookeeper 定义的超时时间就够了，但是这段时间内系统可能是不可用的，但是相对于数据不一致的后果来说还是值得的。 4.4、方案 4.4.1、Quorums zooKeeper 默认采用了 Quorums 这种方式来防止 "脑裂" 现象，即只有集群中超过半数节点投票才能选举出 Leader。这样的方式可以确保 leader 的唯一性，要么选出唯一的一个 leader，要么选举失败。在 zookeeper 中 Quorums 有 3 个作用： 集群中最少的节点数用来选举 leader 保证集群可用。 通知客户端数据已经安全保存前集群中最少数量的节点数已经保存了该数据。一旦这些节点保存了该数据，客户端将被通知已经安全保存了，可以继续其他任务。而集群中剩余的节点将会最终也保存了该数据。 假设某个 leader 假死，其余的 followers 选举出了一个新的 leader。这时，旧的 leader 复活并且仍然认为自己是 leader，这个时候它向其他 followers 发出写请求也是会被拒绝的。因为每当新 leader 产生时，会生成一个 epoch 标号 (标识当前属于那个 leader 的统治时期)，这个 epoch 是递增的，followers 如果确认了新的 leader 存在，知道其 epoch，就会拒绝 epoch 小于现任 leader epoch 的所有请求。那有没有 follower 不知道新的 leader 存在呢，有可能，但肯定不是大多数，否则新 leader 无法产生。Zookeeper 的写也遵循 quorum 机制，因此，得不到大多数支持的写是无效的，旧 leader 即使各种认为自己是 leader，依然没有什么作用。 4.4.2、冗余心跳线 添加冗余的心跳线，例如双线条线，尽量减少 “裂脑” 发生机会。 4.4.3、启用磁盘锁 正在服务一方锁住共享磁盘，"裂脑" 发生时，让对方完全 "抢不走" 共享磁盘资源。但使用锁磁盘也会有一个不小的问题，如果占用共享盘的一方不主动 "解锁"，另一方就永远得不到共享磁盘。现实中假如服务节点突然死机或崩溃，就不可能执行解锁命令。后备节点也就接管不了共享资源和应用服务。于是有人在 HA 中设计了 "智能" 锁。即正在服务的一方只在发现心跳线全部断开 (察觉不到对端) 时才启用磁盘锁。平时就不上锁了。 4.4.4、设置仲裁机制 例如设置参考 IP (如网关 IP)，当心跳线完全断开时，2 个节点都各自 ping 一下 参考 IP，不通则表明断点就出在本端，不仅 "心跳"、还兼对外 "服务" 的本端网络链路断了，即使启动 (或继续) 应用服务也没有用了，那就主动放弃竞争，让能够 ping 通参考 IP 的一端去起服务。更保险一些，ping 不通参考 IP 的一方干脆就自我重启，以彻底释放有可能还占用着的那些共享资源。
一、Zookeeper 入门 1.1、概述 Zookeeper 是一个开源的分布式的，为分布式框架提供协调服务的 Apache 项目。 Zookeeper 工作机制 Zookeeper 从设计模式角度来理解：是一个基于观察者模式设计的分布式服务管理框架，它负责存储和管理大家都关心的数据，然后接受观察者的注册，一旦这些数据的状态发生变化，Zookeeper 就将负责通知已经在 Zookeeper 上注册的那些观察者做出相应的反应。 1.2、特点 1、Zookeeper：一个领导者（Leader），多个跟随者（Follower）组成的集群。 2、集群中只要有半数以上节点存活，Zookeeper 集群就能正常服务。所以 Zookeeper 适合安装奇数台服务器。 3、全局数据一致：每个 Server 保存一份相同的数据副本，Client 无论连接到哪个 Server，数据都是一致的。 4、更新请求顺序执行，来自同一个 Client 的更新请求按其发送顺序依次执行。 5、数据更新原子性，一次数据更新要么成功，要么失败。 6、实时性，在一定时间范围内，Client 能读到最新数据。 1.3、数据结构 ZooKeeper 数据模型的结构与 Unix 文件系统很类似，整体上可以看作是一棵树，每个节点称做一个 ZNode。每一个 ZNode 默认能够存储 1MB 的数据，每个 ZNode 都可以通过其路径唯一标识。 1.4、应用场景 提供的服务包括：统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等。 1.4.1、统一命名服务 在分布式环境下，经常需要对应用 / 服务进行统一命名，便于识别。 例如：IP 不容易记住，而域名容易记住。 1.4.2、统一配置管理 1）分布式环境下，配置文件同步非常常见。 （1）一般要求一个集群中，所有节点的配置信息是 一致的，比如 Kafka 集群。 （2）对配置文件修改后，希望能够快速同步到各个节点上。 2）配置管理可交由 ZooKeeper 实现。 （1）可将配置信息写入 ZooKeeper 上的一个 Znode。 （2）各个客户端服务器监听这个 Znode。 （3）一旦 Znode 中的数据被修改，ZooKeeper 将通知 各个客户端服务器。 1.4.3、统一集群管理 1）分布式环境中，实时掌握每个节点的状态是必要的。 （1）可根据节点实时状态做出一些调整。 2）ZooKeeper 可以实现实时监控节点状态变化 （1）可将节点信息写入 ZooKeeper 上的一个 ZNode。 （2）监听这个 ZNode 可获取它的实时状态变化。 1.4.4、服务器动态上下线 1.4.5、软负载均衡 在 Zookeeper 中记录每台服务器的访问数，让访问数最少的服务器去处理最新的客户端请求 1.5、下载地址 1）官网首页： https://zookeeper.apache.org/ 2）下载截图 3）下载 Linux 环境安装的 tar 包 二、Zookeeper 本地安装 2.1、本地模式安装 1）安装前准备 （1）安装 JDK （2）拷贝 apache-zookeeper-3.5.7-bin.tar.gz 安装包到 Linux 系统下 （3）解压到指定目录 tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/ （4）修改名称 mv apache-zookeeper-3.5.7-bin/zookeeper-3.5.7 2）配置修改 （1）将 /opt/module/zookeeper-3.5.7/conf 这个路径下的 zoo_sample.cfg 修改为 zoo.cfg； mv zoo_sample.cfg zoo.cfg （2）打开 zoo.cfg 文件，修改 dataDir 路径： vim zoo.cfg 修改如下内容： dataDir=/opt/module/zookeeper-3.5.7/zkData （3）在 /opt/module/zookeeper-3.5.7/ 这个目录上创建 zkData 文件夹 mkdir zkData 3）操作 Zookeeper （1）启动 Zookeeper [atguigu@hadoop102 zookeeper-3.5.7]$ bin/zkServer.sh start （2）查看进程是否启动 [atguigu@hadoop102 zookeeper-3.5.7]$ jps 4020 Jps 4001 QuorumPeerMain （3）查看状态 atguigu@hadoop102 zookeeper-3.5.7]$ ./bin/zkServer.sh staus ZooKeeper JMX enabled by default Using config: /opt/module/zookeeper-3.5.7/bin/../conf/zoo.cfg Usage: ./bin//zkServer.sh [--config <conf-dir>] {start|start-foreground|stop|restart|status|print-cmd} （4）启动客户端 [atguigu@hadoop102 zookeeper-3.5.7]$ bin/zkCli.sh （5）退出客户端： [zk: localhost:2181 (CONNECTED) 0] quit （6）停止 Zookeeper [atguigu@hadoop102 zookeeper-3.5.7]$ bin/zkServer.sh stop 2.2、配置参数解读 Zookeeper 中的配置文件 zoo.cfg 中参数含义解读如下： 1）tickTime = 2000：通信心跳时间，Zookeeper 服务器与客户端心跳时间，单位毫秒 2）initLimit = 10：LF 初始通信时限 Leader 和 Follower 初始连接时能容忍的最多心跳数（tickTime 的数量） 3）syncLimit = 5：LF 同步通信时限 Leader 和 Follower 之间通信时间如果超过 syncLimit * tickTime，Leader 认为 Follwer 死掉，从服务器列表中删除 Follwer。 4）dataDir：保存 Zookeeper 中的数据 注意：默认的 tmp 目录，容易被 Linux 系统定期删除，所以一般不用默认的 tmp 目录。 5）clientPort = 2181：客户端连接端口，通常不做修改。 三、Zookeeper 集群操作 3.1、集群操作 3.1.1、集群安装 1）集群规划 在 hadoop102、hadoop103 和 hadoop104 三个节点上都部署 Zookeeper。 2）解压安装 （1）在 hadoop102 解压 Zookeeper 安装包到 /opt/module/ 目录下 [atguigu@hadoop102 software]$ tar -zxvf apache-zookeeper-3.5.7bin.tar.gz -C /opt/module/ （2）修改 apache-zookeeper-3.5.7-bin 名称为 zookeeper-3.5.7 [atguigu@hadoop102 module]$ mv apache-zookeeper-3.5.7-bin/zookeeper-3.5.7 3）配置服务器编号 （1）在 /opt/module/zookeeper-3.5.7/ 这个目录下创建 zkData [atguigu@hadoop102 zookeeper-3.5.7]$ mkdir zkData （2）在 /opt/module/zookeeper-3.5.7/zkData 目录下创建一个 myid 的文件 [atguigu@hadoop102 zkData]$ vi myid 在文件中添加与 server 对应的编号（注意：上下不要有空行，左右不要有空格） 2 注意：添加 myid 文件，一定要在 Linux 里面创建，在 notepad++ 里面很可能乱码。 （3）拷贝配置好的 zookeeper 到其他机器上 [atguigu@hadoop102 module ]$ xsync zookeeper-3.5.7 并分别在 hadoop103、hadoop104 上修改 myid 文件中内容为 3、4。 4）配置 zoo.cfg 文件 （1）重命名 /opt/module/zookeeper-3.5.7/conf 这个目录下的 zoo_sample.cfg 为 zoo.cfg： [atguigu@hadoop102 conf]$ mv zoo_sample.cfg zoo.cfg （2）打开 zoo.cfg 文件 [atguigu@hadoop102 conf]$ vim zoo.cfg 修改数据存储路径配置 dataDir=/opt/module/zookeeper-3.5.7/zkData 增加如下配置 #######################cluster########################## server.2=hadoop102:2888:3888 server.3=hadoop103:2888:3888 server.4=hadoop104:2888:3888 （3）配置参数解读 server.A=B:C:D。 A 是一个数字，表示这个是第几号服务器； 集群模式下配置一个文件 myid，这个文件在 dataDir 目录下，这个文件里面有一个数据就是 A 的值，Zookeeper 启动时读取此文件，拿到里面的数据与 zoo.cfg 里面的配置信息比较从而判断到底是哪个 server。 B 是这个服务器的地址； C 是这个服务器 Follower 与集群中的 Leader 服务器交换信息的端口； D 是万一集群中的 Leader 服务器挂了，需要一个端口来重新进行选举，选出一个新的 Leader，而这个端口就是用来执行选举时服务器相互通信的端口。 （4）同步 zoo.cfg 配置文件 [atguigu@hadoop102 conf]$ xsync zoo.cfg 5）集群操作 （1）分别启动 Zookeeper [atguigu@hadoop102 zookeeper-3.5.7]$ bin/zkServer.sh start [atguigu@hadoop103 zookeeper-3.5.7]$ bin/zkServer.sh start [atguigu@hadoop104 zookeeper-3.5.7]$ bin/zkServer.sh start （2）查看状态 [atguigu@hadoop102 zookeeper-3.5.7]$ bin/zkServer.sh status ZooKeeper JMX enabled by default Using config: /opt/module/zookeeper-3.5.7/bin/../conf/zoo.cfg Client port found: 2181. Client address: localhost. Mode: follower [atguigu@hadoop103 zookeeper-3.5.7]$ bin/zkServer.sh status ZooKeeper JMX enabled by default Using config: /opt/module/zookeeper-3.5.7/bin/../conf/zoo.cfg Client port found: 2181. Client address: localhost. Mode: leader [atguigu@hadoop104 zookeeper-3.5.7]$ bin/zkServer.sh status ZooKeeper JMX enabled by default Using config: /opt/module/zookeeper-3.5.7/bin/../conf/zoo.cfg Client port found: 2181. Client address: localhost. Mode: follower 3.1.2、选举机制 Zookeeper 选举机制 —— 第一次启动 SID：服务器 ID。用来唯一标识一台 ZooKeeper 集群中的机器，每台机器不能重 复，和 myid 一致。 ZXID：事务 ID。ZXID 是一个事务 ID，用来 标识一次服务器状态的变更。在某一时刻， 集群中的每台机器的 ZXID 值不一定完全一 致，这和 ZooKeeper 服务器对于客户端 “更 新请求” 的处理逻辑有关。 Epoch：每个 Leader 任期的代号。没有 Leader 时同一轮投票过程中的逻辑时钟值是 相同的。每投完一次票这个数据就会增加。 （1）服务器 1 启动，发起一次选举。服务器 1 投自己一票。此时服务器 1 票数一票，不够半数以上（3 票），选举无法完成，服务器 1 状态保持为 LOOKING； （2）服务器 2 启动，再发起一次选举。服务器 1 和 2 分别投自己一票并交换选票信息：此时服务器 1 发现服务器 2 的 myid 比自己目前投票推举的（服务器 1） 大，更改选票为推举服务器 2。此时服务器 1 票数 0 票，服务器 2 票数 2 票，没有半数以上结果，选举无法完成，服务器 1，2 状态保持 LOOKING （3）服务器 3 启动，发起一次选举。此时服务器 1 和 2 都会更改选票为服务器 3。此次投票结果：服务器 1 为 0 票，服务器 2 为 0 票，服务器 3 为 3 票。此时服 务器 3 的票数已经超过半数，服务器 3 当选 Leader。服务器 1，2 更改状态为 FOLLOWING，服务器 3 更改状态为 LEADING； （4）服务器 4 启动，发起一次选举。此时服务器 1，2，3 已经不是 LOOKING 状态，不会更改选票信息。交换选票信息结果：服务器 3 为 3 票，服务器 4 为 1 票。此时服务器 4 服从多数，更改选票信息为服务器 3，并更改状态为 FOLLOWING； （5）服务器 5 启动，同 4 一样当小弟。 Zookeeper 选举机制 —— 非第一次启动 （1）当 ZooKeeper 集群中的一台服务器出现以下两种情况之一时，就会开始进入 Leader 选举： 服务器初始化启动。 服务器运行期间无法和 Leader 保持连接。 （2）而当一台机器进入 Leader 选举流程时，当前集群也可能会处于以下两种状态： 集群中本来就已经存在一个 Leader。 对于第一种已经存在 Leader 的情况，机器试图去选举 Leader 时，会被告知当前服务器的 Leader 信息，对于该机器来说，仅仅需要和 Leader 机器建立连 接，并进行状态同步即可。 集群中确实不存在 Leader。 假设 ZooKeeper 由 5 台服务器组成，SID 分别为 1、2、3、4、5，ZXID 分别为 8、8、8、7、7，并且此时 SID 为 3 的服务器是 Leader。某一时刻，3 和 5 服务器出现故障，因此开始进行 Leader 选举。 （EPOCH，ZXID，SID ） （EPOCH，ZXID，SID ） （EPOCH，ZXID，SID ） SID 为 1、2、4 的机器投票情况： （1，8，1） （1，8，2） （1，7，4） 选举 Leader 规则： EPOCH 大的直接胜出 EPOCH 相同，事务 id 大的胜出 事务 id 相同，服务器 id 大的胜出 3.1.3、ZK 集群启动停止脚本 1）在 hadoop102 的 /home/atguigu/bin 目录下创建脚本 [atguigu@hadoop102 bin]$ vim zk.sh 在脚本中编写如下内容 #!/bin/bash case $1 in "start"){ for i in hadoop102 hadoop103 hadoop104 do echo ---------- zookeeper $i 启动 ------------ ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start" done };; "stop"){ for i in hadoop102 hadoop103 hadoop104 do echo ---------- zookeeper $i 停止 ------------ ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop" done };; "status"){ for i in hadoop102 hadoop103 hadoop104 do echo ---------- zookeeper $i 状态 ------------ ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status" done };; esac 2）增加脚本执行权限 [atguigu@hadoop102 bin]$ chmod u+x zk.sh 3）Zookeeper 集群启动脚本 [atguigu@hadoop102 module]$ zk.sh start 4）Zookeeper 集群停止脚本 [atguigu@hadoop102 module]$ zk.sh stop 3.2、客户端命令行操作 3.2.1、命令行语法 1）启动客户端 [atguigu@hadoop102 zookeeper-3.5.7]$ bin/zkCli.sh -server hadoop102:2181 2）显示所有操作命令 [zk: hadoop102:2181 (CONNECTED) 0] help ZooKeeper -server host:port cmd args addauth scheme auth close config [-c] [-w] [-s] connect host:port create [-s] [-e] [-c] [-t ttl] path [data] [acl] delete [-v version] path deleteall path delquota [-n|-b] path get [-s] [-w] path getAcl [-s] path history listquota path ls [-s] [-w] [-R] path ls2 path [watch] printwatches on|off quit reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3 [,...]*]] | [-add serverId=host:port1:port2;port3 [,...]]* [-remove serverId [,...]*] redo cmdno removewatches path [-c|-d|-a] [-l] rmr path set [-s] [-v version] path data setAcl [-s] [-v version] [-R] path acl setquota -n|-b val path stat [-w] path sync path Command not found: Command not found help 3.2.2、znode 节点数据信息 1）查看当前 znode 中所包含的内容 [zk: hadoop102:2181 (CONNECTED) 0] ls / [zookeeper] 2）查看当前节点详细数据 [zk: hadoop102:2181 (CONNECTED) 2] ls -s / [zookeeper] cZxid = 0x0 ctime = Thu Jan 01 08:00:00 CST 1970 mZxid = 0x0 mtime = Thu Jan 01 08:00:00 CST 1970 pZxid = 0x0 cversion = -1 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 0 numChildren = 1 （1）czxid：创建节点的事务 zxid 每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。事务 ID 是 ZooKeeper 中所有修改总的次序。每次修改都有唯一的 zxid，如果 zxid1 小于 zxid2，那么 zxid1 在 zxid2 之前发生。 （2）ctime：znode 被创建的毫秒数（从 1970 年开始） （3）mzxid：znode 最后更新的事务 zxid （4）mtime：znode 最后修改的毫秒数（从 1970 年开始） （5）pZxid：znode 最后更新的子节点 zxid （6）cversion：znode 子节点变化号，znode 子节点修改次数 （7）dataversion：znode 数据变化号 （8）aclVersion：znode 访问控制列表的变化号 （9）ephemeralOwner：如果是临时节点，这个是 znode 拥有者的 session id。如果不是临时节点则是 0。 （10）dataLength：znode 的数据长度 （11）numChildren：znode 子节点数量 3.2.3、节点类型（持久 / 短暂 / 有序号 / 无序号） 说明：创建 znode 时设置顺序标识，znode 名称后会附加一个值，顺序号是一个单调递增的计数器，由父节点维护。 注意：在分布式系统中，顺序号可以被用于为所有的事件进行全局排序，这样客户端可以通过顺序号推断事件的顺序。 持久（Persistent）：客户端和服务器端断开连接后，创建的节点不删除 短暂（Ephemeral）：客户端和服务器端断开连接后，创建的节点自己删除 （1）持久化目录节点 客户端与 Zookeeper 断开连接后，该节点依旧存在。 （2）持久化顺序编号目录节点 客户端与 Zookeeper 断开连接后，该节点依旧存在，只是 Zookeeper 给该节点名称进行顺序编号。 （3）临时目录节点 客户端与 Zookeeper 断开连接后，该节点被删除。 （4）临时顺序编号目录节点 客户端与 Zookeeper 断开连接后，该节点被删除 ，只是 Zookeeper 给该节点名称进行顺序编号。 1）分别创建 2 个普通节点（永久节点 + 不带序号） [zk: localhost:2181 (CONNECTED) 3] create /sanguo "diaochan" Created /sanguo [zk: localhost:2181 (CONNECTED) 4] create /sanguo/shuguo "liubei" Created /sanguo/shuguo 注意：创建节点时，要赋值 2）获得节点的值 [zk: hadoop102:2181 (CONNECTED) 6] get -s /sanguo diaochan cZxid = 0x200000002 ctime = Fri Feb 17 12:55:16 CST 2023 mZxid = 0x200000002 mtime = Fri Feb 17 12:55:16 CST 2023 pZxid = 0x200000003 cversion = 1 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 8 numChildren = 1 [zk: hadoop102:2181 (CONNECTED) 7] get -s /sanguo/shuguo liubei cZxid = 0x200000003 ctime = Fri Feb 17 12:55:24 CST 2023 mZxid = 0x200000003 mtime = Fri Feb 17 12:55:24 CST 2023 pZxid = 0x200000003 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 6 numChildren = 0 3）创建带序号的节点（永久节点 + 带序号） （1）先创建一个普通的根节点 /sanguo/weiguo [zk: hadoop102:2181 (CONNECTED) 8] create /sanguo/weiguo "caocao" Created /sanguo/weiguo （2）创建带序号的节点 [zk: hadoop102:2181 (CONNECTED) 8] create /sanguo/weiguo "caocao" Created /sanguo/weiguo [zk: hadoop102:2181 (CONNECTED) 9] create -s /sanguo/weiguo/zhangliao "zhangliao" Created /sanguo/weiguo/zhangliao0000000000 [zk: hadoop102:2181 (CONNECTED) 10] create -s /sanguo/weiguo/zhangliao "zhangliao" Created /sanguo/weiguo/zhangliao0000000001 [zk: hadoop102:2181 (CONNECTED) 11] create -s /sanguo/weiguo/xuchu "xuchu" Created /sanguo/weiguo/xuchu0000000002 如果原来没有序号节点，序号从 0 开始依次递增。如果原节点下已有 2 个节点，则再排序时从 2 开始，以此类推。 4）创建短暂节点（短暂节点 + 不带序号 or 带序号） （1）创建短暂的不带序号的节点 [zk: hadoop102:2181 (CONNECTED) 12] create -e /sanguo/wuguo "zhouyu" Created /sanguo/wuguo （2）创建短暂的带序号的节点 [zk: hadoop102:2181 (CONNECTED) 13] create -e -s /sanguo/wuguo "zhouyu" Created /sanguo/wuguo0000000003 （3）在当前客户端是能查看到的 [zk: hadoop102:2181 (CONNECTED) 15] ls /sanguo [shuguo, weiguo, wuguo, wuguo0000000003] （4）退出当前客户端然后再重启客户端 [zk: localhost:2181 (CONNECTED) 12] quit [atguigu@hadoop104 zookeeper-3.5.7]$ bin/zkCli.sh （5）再次查看根目录下短暂节点已经删除 [zk: localhost:2181 (CONNECTED) 4] ls /sanguo [shuguo, weiguo] 5）修改节点数据值 [zk: localhost:2181 (CONNECTED) 6] set /sanguo/weiguo "simayi" 3.2.4、监听器原理 客户端注册监听它关心的目录节点，当目录节点发生变化（数据改变、节点删除、子目录节点增加删除）时，ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序。 1、监听原理详解 1）首先要有一个 main () 线程 2）在 main 线程中创建 Zookeeper 客户端，这时就会创建两个线 程，一个负责网络连接通信（connet），一个负责监听（listener）。 3）通过 connect 线程将注册的监听事件发送给 Zookeeper。 4）在 Zookeeper 的注册监听器列表中将注册的监听事件添加到列表中。 5）Zookeeper 监听到有数据或路径变化，就会将这个消息发送给 listener 线程。 6）listener 线程内部调用了 process () 方法 2、常见的监听 1）监听节点数据的变化 get path [watch] 2）监听子节点增减的变化 ls path [watch] 1）节点的值变化监听 （1）在 hadoop104 主机上注册监听 /sanguo 节点数据变化 [zk: localhost:2181 (CONNECTED) 26] get -w /sanguo （2）在 hadoop103 主机上修改 /sanguo 节点的数据 [zk: localhost:2181 (CONNECTED) 1] set /sanguo "xisi" （3）观察 hadoop104 主机收到数据变化的监听 WATCHER:: WatchedEvent state:SyncConnected type:NodeDataChanged path:/sanguo 注意：在 hadoop103 再多次修改 /sanguo 的值，hadoop104 上不会再收到监听。因为注册一次，只能监听一次。想再次监听，需要再次注册。 2）节点的子节点变化监听（路径变化） （1）在 hadoop104 主机上注册监听 /sanguo 节点的子节点变化 [zk: localhost:2181 (CONNECTED) 1] ls -w /sanguo [shuguo, weiguo] （2）在 hadoop103 主机 /sanguo 节点上创建子节点 [ [zk: localhost:2181 (CONNECTED) 2] create /sanguo/jin "simayi" Created /sanguo/jin （3）观察 hadoop104 主机收到子节点变化的监听 WATCHER:: WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/sanguo 注意：节点的路径变化，也是注册一次，生效一次。想多次生效，就需要多次注册。 3.2.5、节点删除与查看 1）删除节点 [zk: localhost:2181 (CONNECTED) 4] delete /sanguo/jin 2）递归删除节点 [zk: localhost:2181 (CONNECTED) 15] deleteall /sanguo/shuguo 3）查看节点状态 [zk: localhost:2181 (CONNECTED) 5] stat /sanguo cZxid = 0x200000002 ctime = Fri Feb 17 12:55:16 CST 2023 mZxid = 0x20000000e mtime = Fri Feb 17 13:07:17 CST 2023 pZxid = 0x200000011 cversion = 9 dataVersion = 1 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 4 numChildren = 1 3.3、客户端 API 操作 前提：保证 hadoop102、hadoop103、hadoop104 服务器上 Zookeeper 集群服务端启动。 详见代码。 3.4、客户端向服务端写数据流程 写流程之写入请求直接发送给 Leader 节点 写流程之写入请求发送给 follower 节点 四、服务器动态上下线监听案例 4.1、需求 某分布式系统中，主节点可以有多台，可以动态上下线，任意一台客户端都能实时感知到主节点服务器的上下线。 4.2、需求分析 服务器动态上下线 4.3、具体实现 [zk: localhost:2181 (CONNECTED) 10] create /servers "servers" Created /servers 五、ZooKeeper 分布式锁案例 5.1、原生 Zookeeper 实现分布式锁案例 5.2、Curator 框架实现分布式锁案例 1）原生的 Java API 开发存在的问题。 （1）会话连接是异步的，需要自己去处理。比如使用 CountDownLatch。 （2）Watch 需要重复注册，不然就不能生效。 （3）开发的复杂性还是比较高的。 （4）不支持多节点删除和创建。需要自己去递归。 2）Curator 是一个专门解决分布式锁的框架，解决了原生 Java API 开发分布式遇到的问题。 详情请查看官方文档：https://curator.apache.org/index.html