ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ZooKeeper原理与实战:从Hadoop高可用到分布式协调服务

ZooKeeper原理与实战:从Hadoop高可用到分布式协调服务 直接从标题聊起。“ZooKeeper 知多少”这个问题我在好几个社群和面试场合里都被反复问到过。很多人第一次接触它是从报名Hadoop集群开始的——毕竟当年Hadoop 2.X版本里NameNode的高可用全靠它撑场子。但如果你只是为了装一个集群去把ZooKeeper跑起来那真的很亏因为这玩意儿本身要解决的分布式协调问题远比“配一个HA”值钱。这篇就按我实际走过的路子从原理讲到Hadoop整合实战再往下深挖客户端开发和日常排障帮你把ZooKeeper这课一次性补扎实。绝大多数人第一次见ZooKeeper是在Hadoop的部署文档里这直接导致了一个常见误区以为ZooKeeper是Hadoop的附属组件。实际上它是个完全独立的分布式协调服务Hadoop只是它的一个经典使用方。Google当年的Chubby锁服务是它的灵感来源而ZooKeeper是把它做成了开源界的通用标准。我更喜欢用一个朴素的比喻来解释它它本质上就是分布式系统的“通信中转站状态记事本”。分布式环境里多个节点之间不能共享内存但很多场景又需要它们对某个状态达成一致——比如谁是主节点、哪个服务存了哪些元数据、配置文件的某个值当前是什么。ZooKeeper就把这些需要多方共识的信息放到一个树状存储空间里再通过一套严格的顺序机制保证大家看到的数据先后一致。要理解ZooKeeper的定位有四个核心概念必须吃透分别是数据模型、节点类型、监听机制、以及ZAB协议。先说数据模型。ZooKeeper的命名空间长得非常像文件系统是一个树形结构每个节点叫Znode。路径就是访问它的Key比如/hadoop/namenode、/services/order。但Znode和文件系统的目录有个重要区别Znode既可以当容器用本身也可以存数据而且默认限制在1MB以内。这个1MB的设计是刻意为之因为ZooKeeper的核心场景是小数据、高吞吐的一致性协调而不是大数据存储。每个Znode自带一个Stat结构相当于文件的元数据里面有事务IDzxid、版本号、时间戳、数据长度这些字段。版本号特别关键后面讲分布式锁的时候乐观锁就是靠它实现的——你更新数据时可以带上期望的version一旦实际版本对不上更新直接报错。再讲节点类型这块是ZooKeeper的实用基础。总共有四种组合持久节点、临时节点、持久顺序节点、临时顺序节点。持久节点一旦创建就一直在除非主动删除临时节点跟着创建它的会话走会话一断比如客户端崩溃或网络超时节点自动消失这个特性在选主和心跳上报场景里简直是大杀器。顺序节点则是在路径末尾追加一个全局递增的序号利用这个序号可以做很多分布式算法比如公平锁。组合出来的持久顺序节点、临时顺序节点是后面整合实战和开发高级功能的主力。然后说监听机制。ZooKeeper允许客户端对某个Znode设置Watch当这个节点的数据发生变化、子节点列表变化、或者节点被删除时客户端会收到一个通知事件。注意这个通知是一次性的收到之后如果还想继续监听必须重新注册。这个设计常被吐槽但它背后的意图是很明确的减少服务端压力避免长连接里堆积大量重复通知。在实际开发中我通常的做法是封装一层监听管理器收到事件后自动重新设置Watch对外暴露出持续监听的能力。最后是ZAB协议这是ZooKeeper最硬核的部分。ZooKeeper集群是典型的主从架构一个Leader节点负责处理所有写请求其余Follower节点同步数据、处理读请求。ZAB协议保证了Leader挂掉之后新选出的Leader能继承之前已提交的全部数据不会丢也不会乱。这个过程包括崩溃恢复和原子广播两个阶段崩溃恢复阶段要做数据同步把旧Leader的已提交事务同步给新Leader和所有新加入的Follower原子广播阶段则类似一个两阶段的提交简版保证所有节点以相同的顺序执行写操作。很多人纠结ZooKeeper与Paxos、Raft的区别。我个人的理解是ZAB更像是为“单Leader 数据强一致”这个场景量身定制的协议它利用zxid的递增关系天然划清了“哪些事务可以提交、哪些需要丢弃”。相较于Paxos的学习门槛ZAB要直白许多。这也是ZooKeeper能够在很长一段时间里成为分布式协调事实标准的原因之一。到这里如果能把数据模型、节点类型、Watch、ZAB这四件事情讲明白ZooKeeper的原理题基本就不会再怕了。接下来进入实操部分。2.1 安装方式与版本选择ZooKeeper的安装非常亲民。官方提供了三种方式单机模式、伪集群模式、集群模式。很多人做本地学习习惯直接跑单机版配置最简适合先感受一下命令行操作。但我的建议是就算你只有一台电脑也尽量用伪集群模式起步也就是在三个不同端口上起三个进程模拟一个真正的三节点集群。因为很多分布式协调的坑比如会话超时、Leader选举、节点宕机后的行为只有在多节点环境里才能逼出来。版本选择上目前使用最广的稳定线是3.6.x新版本3.7.x、3.8.x也有不少人在生产上用。3.5以后一个重要变化是默认带内嵌管理控制台和新的四字命令白名单机制有些旧脚本直接跑echo stat | nc localhost 2181会没反应因为四字命令默认被限制了需要在配置里显式开启。这个细节很容易踩后面排查部分我会细说。2.2 关键配置项逐字段拆解拿到安装包解压后conf/zoo_sample.cfg是个很好的起点把它复制成zoo.cfg再改。核心配置项就这几个tickTime基础时间单元单位毫秒。它被用来计算会话超时和心跳间隔。默认2000意味着一格是2秒。会话超时的最小值通常是2 * tickTime即4秒。dataDirZooKeeper存储快照文件的目录非常重要。这个目录建议放在独立的磁盘分区上因为写入延迟直接影响所有写操作的性能。我见过因为dataDir所在的磁盘IO被打满整个集群写延迟飙升到几百毫秒的案例。clientPort客户端连接端口默认2181。集群模式下每台机器要对得上。initLimitFollower启动后与Leader完成数据同步的最大时间单位是tickTime。如果同步超过这个时间Follower会被判定为无效并丢弃连接。集群规模大、数据多的时候这个值要适当放大默认10格就是20秒。syncLimitLeader与Follower在运行期心跳检测的最大间隔数。单位同样基于tickTime。超过这个时间没收到心跳Leader就会把对应的Follower移出可用列表。server.Ahost:port1:port2集群节点列表。A是节点编号对应dataDir下的myid文件port1是节点间通信端口port2是Leader选举端口。这两个端口不要漏掉很多人配置只写了通信端口结果集群起不来就是因为选举端口没配对。一个三节点最小配置长这样直接抄没问题tickTime2000 initLimit10 syncLimit5 dataDir/data/zookeeper clientPort2181 server.1node1:2888:3888 server.2node2:2888:3888 server.3node3:2888:38882.3 启动流程与状态验证启动前记得在每台节点的dataDir目录下创建myid文件内容就是节点编号。比如node1的myid写入数字1node2写2以此类推。这个文件没写好集群永远起不来而且日志报错很隐晦只说找不到节点对应配置。启动命令是zkServer.sh start查看状态用zkServer.sh status。正常情况下三节点里会有一个显示leader其余显示follower。如果全部显示standalone赶紧检查是不是只启动了一个节点或者客户端端口没配对。我还习惯用JConsole去连JMX端口查看更细的运行时指标。ZooKeeper启动脚本里默认会暴露JMX通过JConsole连上之后能看到每个节点的连接数、未处理请求数、数据树大小等指标。性能调优时比较有用。接下来是重头戏Hadoop和ZooKeeper的整合。当年网上的资料把这事儿渲染得很神秘但实际上套路非常固定今天给你拆到每一步。3.1 Hadoop整合ZooKeeper到底解决了什么先明确整合目的。早期Hadoop 1.X是单NameNode架构NameNode一挂整个HDFS就不可用了这是典型的单点故障。引入ZooKeeper后可以搭建NameNode高可用集群两个NameNode一个Active一个Standby它们通过JournalNode共享编辑日志。Active节点把每次元数据操作写入JournalNodeStandby节点从JournalNode读取日志并同步到自己的内存状态。一旦Active故障ZooKeeper会感知到并通知Standby切换为Active。这个机制里有三个角色都依赖ZooKeeperZKFCZooKeeper Failover Controller、Active/Standby节点的选举、以及JournalNode的一致性协同。ZKFC是一个独立的守护进程随NameNode启动它通过ZooKeeper创建临时节点来抢占Active状态谁抢到了谁就是主。这不是Hadoop自己想出来的玩法就是ZooKeeper临时节点加Watch机制的典型应用。3.2 整合前的基础环境准备在动手配置之前先把环境捋顺。假设你有三台机器每台内存4GB以上操作系统是LinuxJDK已经装好推荐JDK8或JDK11。三台机器分别叫node1、node2、node3它们互相配好免密SSH登录时间同步用NTP或chrony做好——分布式环境时间不一致会引发很多诡异问题ZooKeeper对时间偏差特别敏感建议偏差控制在1秒以内。Hadoop版本建议直接用Apache Hadoop 3.3.x高版本对HA的支持已经非常成熟配置项也比旧版清晰。ZooKeeper集群建议先独立启动好确认zkServer.sh status已经能看到leader和follower再开始动Hadoop的配置。整合排错时把问题域隔离得越清楚越省时间。3.3 核心配置文件逐项说明Hadoop的HA配置最关键的三个文件是core-site.xml、hdfs-site.xml、yarn-site.xml。core-site.xml里配置默认文件系统为hdfs://mycluster其中mycluster是一个逻辑名称在hdfs-site.xml里映射到两个NameNode。同时还要指定ZooKeeper地址列表让ZKFC知道去哪里竞争锁configuration property namefs.defaultFS/name valuehdfs://mycluster/value /property property nameha.zookeeper.quorum/name valuenode1:2181,node2:2181,node3:2181/value /property /configurationhdfs-site.xml要配置的就多很多。先是dfs.replication默认3副本如果只有3台数据节点这个值正好然后是指定NameNode的标识和地址映射假设我的两个NameNode分别叫nn1和nn2那么需要配置property namedfs.nameservices/name valuemycluster/value /property property namedfs.ha.namenodes.mycluster/name valuenn1,nn2/value /property property namedfs.namenode.rpc-address.mycluster.nn1/name valuenode1:8020/value /property property namedfs.namenode.rpc-address.mycluster.nn2/name valuenode2:8020/value /property property namedfs.namenode.http-address.mycluster.nn1/name valuenode1:9870/value /property property namedfs.namenode.http-address.mycluster.nn2/name valuenode2:9870/value /property接下来还有两个关键部分。一个是共享编辑日志的实现类一般用org.apache.hadoop.hdfs.qjournal.client.QuorumJournalManager对应配置property namedfs.namenode.shared.edits.dir/name valueqjournal://node1:8485;node2:8485;node3:8485/mycluster/value /property另一个是ZKFC的自动故障转移配置开启后Hadoop会借助ZooKeeper完成自动选举property namedfs.ha.automatic-failover.enabled/name valuetrue/value /property property namedfs.client.failover.proxy.provider.mycluster/name valueorg.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider/value /property最后把所有配置同步到另外两台机器并且保证每台机器的hadoop-env.sh里JAVA_HOME正确不然集群启动时一堆类加载异常。3.4 JournalNode与ZKFC的启动顺序这一步资料里五花八门我的实操顺序非常固定按这个来基本不会翻车第一先在node1、node2、node3上启动JournalNode命令是hdfs --daemon start journalnode它会协同维护共享编辑日志目录。启动完成后理论上三台JournalNode会形成一个小集群可以看日志确认它们是否互相建立连接。第二在node1上首次格式化NameNode注意用hdfs namenode -format格式化完成后把node1上的元数据目录整体拷贝到node2上保证两个NameNode初始状态一致。这个动作叫“同步元数据”不做的话Standby往往会因为元数据不一致而无法正常切换。第三在node1上执行hdfs zkfc -formatZK这一步会在ZooKeeper上创建HA相关节点类似/hadoop-ha/mycluster用来记录两个NameNode的Active锁和状态。第四先通过hdfs --daemon start namenode启动node1的NameNode再同样的步骤启动node2上的NameNode。正常情况下node1会成为Activenode2是Standby。第五在node1上启动JournalNode和DataNode的整套进程然后node2、node3依次启动DataNode。最后用hdfs haadmin -getAllServiceState查看两个NameNode的状态。如果一切顺利你会看到类似这样的输出node1:8020 active node2:8020 standby看到active和standby并立说明整合已经成功了一大半。此时再验证自动故障转移主动把node1上的NameNode进程kill掉等十几秒后node2应该自动切换为active。这一步强烈建议在测试环境多做几次因为生产环境真发生故障时你没有临场试错的机会。3.5 顺手把YARN的HA也配了HDFS配好了YARN的资源管理器ResourceManager也不能晾着。YARN HA的思路类似通过ZooKeeper实现两个ResourceManager的选主。核心配置在yarn-site.xml里property nameyarn.resourcemanager.ha.enabled/name valuetrue/value /property property nameyarn.resourcemanager.cluster-id/name valuecluster1/value /property property nameyarn.resourcemanager.ha.rm-ids/name valuerm1,rm2/value /property property nameyarn.resourcemanager.hostname.rm1/name valuenode1/value /property property nameyarn.resourcemanager.hostname.rm2/name valuenode2/value /property property nameyarn.resourcemanager.webapp.address.rm1/name valuenode1:8088/value /property property nameyarn.resourcemanager.webapp.address.rm2/name valuenode2:8088/value /property property nameyarn.resourcemanager.zk-address/name valuenode1:2181,node2:2181,node3:2181/value /property这里还有一个很容易漏的配置yarn.resourcemanager.recovery.enabled要设置为true并且配置对应的状态存储实现为ZooKeeper。否则即使在ZooKeeper上做了选主RM重启后也无法从状态中恢复之前提交的任务信息。对于跑生产作业的集群漏掉这个环节等于白配。YARN配置完成后只需要先启动第一个RM再启动第二个RM等大约三十秒通过Web界面或日志确认其中一个处于ACTIVE状态另一个是STANDBY状态就算落地了。整合和Hadoop对接搞定后ZooKeeper更多的价值在于日常开发中。这一个章节我们聊真刀真枪的客户端使用。4.1 原生客户端还是高级客户端库Java项目里操纵ZooKeeper选项就那几个原生的org.apache.zookeeper.ZooKeeper类或者封装程度更高、API更友好的Apache Curator。我的看法是学习阶段用原生理解底层机制生产开发阶段直接用Curator避免再造轮子。原生客户端的典型流程是这样的创建ZooKeeper实例传入连接串和会话超时然后通过回调或阻塞等待连接建立。接着就可以create、getData、setData、delete这些常规操作了。但原生API写起来比较啰嗦尤其Watch需要自己重新注册稍不留神就漏导致监听逻辑失效。Curator的优势在于把重注册Watch这类机械操作做成了框架级能力还提供了一套CuratorFramework 的配方Recipes比如分布式锁、选主、分布式队列这些原本要手写的算法它全都有了。用Curator实现分布式锁五秒钟就能写出来大概是这样一个骨架CuratorFramework client CuratorFrameworkFactory.newClient( node1:2181,node2:2181,node3:2181, new ExponentialBackoffRetry(1000, 3) ); client.start(); InterProcessMutex lock new InterProcessMutex(client, /locks/order); if (lock.acquire(5, TimeUnit.SECONDS)) { try { // 这里就是临界区业务逻辑放这里 } finally { lock.release(); } }这段代码背后的原理是InterProcessMutex会在ZooKeeper上创建一个临时顺序节点多个客户端同时竞争锁时会按顺序排队。序号最小的那个客户端持有锁其他客户端则监听前一个节点。前一个节点释放或者会话断开自动删除后一个客户端就能获得锁。整个过程是公平的天然避开了一把分布式锁常见的惊群效应。4.2 手写一个服务注册发现模块分布式服务架构中我们经常需要把服务提供方的IP端口注册到ZooKeeper上让消费方能够动态感知。这个需求的实现用上临时节点刚刚好服务提供方在约定的根路径下创建一个临时节点节点数据里带上自己的地址和端口服务消费方通过监听根路径的子节点变化实时拿到可用服务列表。关键点在于临时节点随着服务端会话断开自动消失所以服务端异常崩溃时注册信息也能自动清理不需要额外的心跳机制去清理脏数据。这个特性带来的优势非常大省掉了一堆自研的注册中心代码。我一般在生产环境用Curator的ServiceDiscovery配方但为了让你彻底理解我用原生API写了一个最简版的注册逻辑思路如下连接ZooKeeper。检查根路径是否存在不存在则先创建。使用create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)创建临时顺序节点。节点数据写入JSON字符串字段为{host:127.0.0.1,port:8080,protocol:http}。消费方用getChildren获取所有子节点再逐个getData读取详细数据同时对这些路径设置Watch。收到NodeChildrenChanged事件后重新拉取子节点列表并再次注册Watch。这个模式一直到今天都是很多自研微服务框架底层的通用做法。理解一次受益很多。4.3 配置中心与发布订阅ZooKeeper的第二大类高频用法是配置中心。把业务配置放在配置节点下客户端启动时全量拉取运行时注册一个数据变更Watch。配置更新时通过运维平台调用一次setData所有订阅了该节点的客户端都会收到NodeDataChanged事件随后拉取新值热加载。这比传统的改配置文件、重启进程要现代得多。实际项目中我常用Curator的NodeCache或PathChildrenCache来做这层封装它们内部已经把重注册Watch的事情处理好了你只需定义事件处理回调。需要特别提醒的是ZooKeeper不适合做高频配置的配置中心。有些团队试图用它管理秒级变化的限流阈值结果是ZooKeeper集群压力很大因为每次变更都会触发一整轮广播和通知。低频变更、状态协调的场景才适合它。这部分内容是这篇文章里最值钱的段落之一。我把这几年自己和同行实际踩过的坑整理成了速查与排查清单遇到问题可以按图索骥。5.1 四字命令没反应或提示权限不足很多人在集群刚装好时用echo stat | nc localhost 2181检查状态结果什么都没返回。3.5.0以后的版本里四字命令默认不开放需要在zoo.cfg中配置4lw.commands.whiteliststat, ruok, conf, clients这里的ruok是个很有意思的命令它返回imok表示ZooKeeper进程活着但实际上它只代表进程存在不代表集群健康。检查集群健康更可靠的是stat它会显示当前节点的角色、节点数、收到的连接数、发送接收包的延迟等。如果配置了白名单依然没反应检查一下系统里有没有防火墙拦截2181端口。很多云服务器默认安全组只开部分端口ZooKeeper的2181、2888、3888、Hadoop的8020、9870、8485、8088都得在安全组里显式放开。这个坑在云上部署时几乎必踩而且报错形式特别多排查起来很耗时间直接把端口全部列出来核对一遍最快。5.2 连接数暴涨到上限导致写入失败ZooKeeper默认最大连接数是60这个值在入门资料里没人提。当微服务实例多起来、监控探针又多时连接数很快会触顶表现是新的客户端连不上老的连接还在超时重试。现象上就是集群状态看起来正常但业务一直报连接拒绝。解决思路很简单在zoo.cfg里调大最大连接数maxClientCnxns2000同时要在客户端这边做好连接复用而不是每个线程都新建一个ZooKeeper连接。一个应用进程对应一个ZooKeeper连接实例内部用线程安全的方式共享它这是最佳实践。频繁创建和销毁连接不仅浪费资源还会在服务端留下大量TIME_WAIT状态。5.3 频繁的Leader选举与“抖动”集群运行中如果时不时出现Leader切换先别急着骂ZooKeeper。这个问题的根源大概率是服务器负载过高或JVM GC停顿过长导致Leader的心跳发送不及时Follower认为自己掉了线发起新一轮选举。这就像一群人接力传棒拿到棒的人动作慢了一拍队友就开始怀疑他是不是掉队了立刻推举新人。排查时先看三台机器的CPU、内存、磁盘IO重点关注GC日志。如果注意到Full GC频繁且停顿时间长调优JVM堆大小和GC算法比调整ZooKeeper参数更有效。ZooKeeper的JVM堆不必给太大反而堆太大时GC停顿更明显我一般控制在2GB到4GB视数据树大小而定。如果确认服务器资源正常再检查syncLimit是不是设得太小。网络抖动明显的机房环境适当把syncLimit放大到8或10可以给Leader和Follower之间的心跳留出余量。5.4 ZooKeeper集群脑裂的误读关于脑裂网上说法混乱。其实ZooKeeper的设计天然规避了脑裂它的选举机制要求超过半数的节点同意才能选出Leader所以集群最多只会有一个Leader。比如三节点集群中两个节点之间网络断了被隔离的那个节点因为凑不到大多数会进入只读状态不会自己当Leader数据保持安全。但这不代表没有风险如果网络分区隔离了过半数的节点那些节点仍然能选出新Leader并继续服务而旧的Leader已经联系不到大多数节点会主动退位。在这个切换窗口内业务的读写请求可能短暂失败或超时但不会出现两个节点同时向外部提供写服务。理解这一点能帮助你区分“网络故障导致服务短暂不可用”和“设计缺陷导致数据不一致”这两类问题前者是运维要解决的后者才需要架构调整。5.5 会话超时设置的经验值会话超时参数sessionTimeout直接影响故障感知的灵敏度。设太短客户端进程稍有GC停顿ZooKeeper就认为会话断开临时节点全被清理注册信息全部丢失设太长Leader挂掉后业务感知故障的间隔变大高可用切换变慢。我的经验值是常规微服务场景用10到30秒比较合理重IO、长GC的应用可以用到60秒。特别要注意的是服务端对该值有上下限约束客户端传入的值如果低于最小值或高于最大值会被悄悄修正。这意味着你设置5秒最终生效可能是4秒此类细节在排查超时问题时能省很多事。5.6 数据目录损坏与恢复dataDir下的快照文件如果因为断电或磁盘故障出现损坏ZooKeeper启动时会拒绝加载并报错。处理原则是优先从最近的完好快照恢复必要时牺牲一小部分最近的事务更新。恢复步骤是先备份当前的dataDir然后删掉损坏的快照重新启动。如果事务日志dataLogDir完好启动后ZooKeeper会从快照加日志继续干活。这里有个从运维老手那里学到的教训生产环境一定把dataDir和dataLogDir分开到不同磁盘否则快照和事务日志同时损坏时恢复基本无望。另外磁盘阵列或云盘的硬件故障不是靠运气能扛过去的一定要提前建立备份与恢复演练机制。现在回头看整个ZooKeeper的体系它的核心价值确实不在某个函数或配置项上而是提供了一整套分布式系统都需要的共识与协调模型。从Hadoop高可用到服务注册、分布式锁、配置中心到处都有它的影子。我个人在实际使用中的几点心得放在这里给大家参考。第一学习ZooKeeper不要只停留在“能跑起来”的阶段建议亲手用它实现一个分布式锁或者服务注册中心原理才会真正变成自己的东西。第二在Hadoop整合时先把ZooKeeper集群状态调好再动Hadoop配置分步验证能省掉大量混合排错的时间。第三不管什么环境dataDir独立磁盘、四字命令白名单、JVM堆大小这些生产级细节从一开始就按正确姿势来后面能少很多麻烦。最后再分享一个小技巧调试ZooKeeper相关问题可以在日志配置里开启DEBUG级别查看每个客户端连接的具体状态变化。你会发现很多“莫名其妙的断连”和“超时”在日志里都写着清清楚楚的原因。学会读日志比会敲一百个命令更管用。
返回列表