Zookeeper介绍
首先介绍下Zookeeper的背景、数据类型、使用场景以及ZAB协议,让大家对Zookeeper有一个清晰的认识。
Zookeeper概述
ZooKeeper是一个分布式的、开放源码的分布式协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。由于Hadoop生态系统中很多项目都依赖于zookeeper,如Pig,Hive等, 似乎很像一个动物园管理员,于是取名为Zookeeper。 Zookeeper官网地址为http://zookeeper.apache.org/。
Zookeeper特点
- 顺序一致性
- 原子性
- 单一视图
- 可靠性
- 实时性
Zookeeper使用场景
- 名字服务
- 配置管理
- 集群管理
- 集群选举
- 分布式锁
- 队列管理
- 消息订阅
Zookeeper节点状态
- LOOKING:寻找Leader状态,处于该状态需要进入选举流程
- LEADING:领导者状态,处于该状态的节点说明是角色已经是Leader
- FOLLOWING:跟随者状态,表示Leader已经选举出来,当前节点角色是Follower
- OBSERVER:观察者状态,表明当前节点角色是Observer,Observer节点不参与投票,只负责同步Leader状态
Zookeeper数据类型
- Zookeeper的数据结构非常类似于文件系统。是由节点组成的树形结构。不同的是文件系统是由文件夹和文件来组成的树,而Zookeeper中是由Znode来组成的树。每一个Znode里都可以存放一段数据,Znode下还可以挂载零个或多个子Znode节点,从而组成一个树形结构。
- 节点类型
- 持久化节点(PERSISTENT):znode节点的数据不会丢失,除非是客户端主动delete
- 持久化顺序节点(PERSISTENT_SEQUENTIAL):znode节点会根据当前已经存在的znode节点编号自动加1
- 临时节点:临时节点(EPHEMERAL):当session中断后会被删除
- 临时顺序节点(EPHEMERAL_SEQUENTIAL):znode节点编号会自动加 1,当session中断后会被删除
- ContainerNode:3.5.3版本引入,用来解决分布式锁场景下产生大量孤儿节点的问题(搭配PERSISTENT使用)
- TTLNode:3.5.3版本引入,当在TTL时间内节点没有被修改并且没有子节点将自动被删除(搭配PERSISTENT、PERSISTENT_SEQUENTIAL使用)
Zookeeper数据版本
Zookeeper的每个ZNode上都会存储数据,对应到每个ZNode,Zookeeper都会为其维护一个叫做Stat的数据结构,Stat中记录的内容如下:
- cZxid: 节点创建时的zxid
- ctime: 节点创建时间
- mZxid: 最后一次更新的zxid
- mtime: 最后一次更新的时间
- pZxid: 子节点的最后版本
- cversion: 子节点数据更新次数
- dataVersion: 节点数据更新次数
- aclVersion: acl的变更次数
- ephemeralOwner: 如果znode是临时节点,则值为所有者的sessionId;如果不是临时节点,则为零
- dataLength: 节点的数据长度
- numChildren: 子节点个数
Watcher
Watcher(事件监听器)是 Zookeeper提供的一种 发布/订阅的机制。Zookeeper允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,Zookeeper服务端会将事件通知给订阅的客户端。该机制是 Zookeeper实现分布式协调的重要特性。
- watcher特点
- 轻量级:一个callback函数。
- 异步性:不会block正常的读写请求。
- 主动推送:Watch被触发时,由 Zookeeper 服务端主动将更新推送给客户端。
- 一次性:数据变化时,Watch 只会被触发一次。如果客户端想得到后续更新的通知,必须要在 Watch 被触发后重新注册一个 Watch。
- 仅通知:仅通知变更类型,不附带变更后的结果。
- 顺序性:如果多个更新触发了多个 Watch ,那 Watch 被触发的顺序与更新顺序一致
- watcher使用注意事项。
- 由于watcher是一次性的,所以需要自己去实现永久watch
- 如果被watch的节点频繁更新,会出现“丢数据”的情况
- watcher数量过多会导致性能下降
Session
zookeeper会为每个客户端分配一个session,类似于web服务器一样,用来标识客户端的身份。
- Session作用
- 客户端标识
- 超时检查
- 请求的顺序执行
- 维护临时节点的生命周期
- watcher通知
- Session状态
- CONNECTING
- CONNECTED
- RECONNECTING
- RECONNECTED
- CLOSED
- Session属性
- sessionID:会话ID,全局唯一
- TimeOut:会话超时时间
- TickTime:下次会话超时时间点
- isClosing:会话是否已经被关闭
- SessionID构造
- 高8位代表创建Session时所在的zk节点的id
- 中间40位代表zk节点当前角色在创建的时候的时间戳
- 低16位是一个计数器,初始值为0
ACL
在Zookeeper中,node的ACL是没有继承关系的。ACL表现形式:scheme:permissions。
- Scheme
- World:它下面只有一个id, 叫anyone。world:anyone代表任何人都有权
- Auth:通过user:password的形式认证,支持Kerberos
- Digest:使用user:password的形式认证
- Ip:通过IP的粒度来控制权限,支持网段
- Super:对应的id拥有超级权限,可以做任何事情
- Permission
- CREATE(c): 创建权限,可以在在当前node下创建child node
- DELETE(d): 删除权限,可以删除当前的node
- READ(r): 读权限,可以获取当前node的数据,可以list当前node所有的child nodes
- WRITE(w): 写权限,可以向当前node写数据
- ADMIN(a): 管理权限,可以设置当前node的permission
API
ZAB
ZAB 是 ZooKeeper Atomic Broadcast (ZooKeeper 原子广播协议)的缩写,它是特别为 ZooKeeper 设计的崩溃可恢复的原子消息广播算法。ZooKeeper 使用 Leader来接收并处理所有事务请求,并采用 ZAB 协议,将服务器数据的状态变更以事务 Proposal 的形式广播到所有的 Follower 服务器上去。这种主备模型架构保证了同一时刻集群中只有一个服务器广播服务器的状态变更,因此能够很好的保证事物的完整性和顺序性。 Zab协议有两种模式,它们分别是恢复模式(recovery)和广播模式(broadcast)。当服务启动或者在leader崩溃后,Zab就进入了恢复模式,当leader被选举出来,且大多数follower完成了和leader的状态同步以后, 恢复模式就结束了,ZAB开始进入广播模式。
选主流程
当Leader崩溃或者Leader失去大多数的Follower时,Zookeeper处于恢复模式,在恢复模式下需要重新选举出一个新的Leader,让所有的 Server都恢复到一个正确的状态。Zookeeper的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。
- Basic paxos:当前Server发起选举的线程,向所有Server发起询问,选举线程收到所有回复,计算zxid最大Server,并推荐此为Leader,若此提议获得n/2+1票通过(过半同意),此为Leader,否则重复上述流程,直到Leader选出。
- Fast paxos:某Server首先向所有Server提议自己要成为Leader,当其它Server收到提议以后,解决epoch和 zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。(即提议方解决其他所有epoch和 zxid的冲突,即为Leader)。
数据同步
当集群重新选举出Leader后,所有的Follower需要和Leader同步数据,确保集群数据的一致性。
- 数据同步方式
- SNAP-全量同步
- 条件:peerLastZxid<minCommittedLog
- 说明:证明二者数据差异太大,follower数据过于陈旧,leader发送快照SNAP指令给follower全量同步数据,即leader将所有数据全量同步到follower
- DIFF-增量同步
- 条件:minCommittedLog<=peerLastZxid<=maxCommittedLog
- 说明:证明二者数据差异不大,follower上有一些leader上已经提交的提议proposal未同步,此时需要增量提交这些提议即可
- TRUNC-仅回滚同步
- 条件:peerLastZxid>minCommittedLog
- 说明:证明follower上有些提议proposal并未在leader上提交,follower需要回滚到zxid为minCommittedLog对应的事务操作
- TRUNC+DIFF-回滚+增量同步
- 条件:minCommittedLog<=peerLastZxid<=maxCommittedLog
- 说明:leader a已经将事务truncA提交到本地事务日志中,但没有成功发起proposal协议进行投票就宕机了;然后集群中剔除原leader a重新选举出新leader b,又提交了若干新的提议proposal,然后原leader a重新服务又加入到集群中说明:此时a,b都有一些对方未提交的事务,若b是leader, a需要先回滚truncA然后增量同步新leader b上的数据。
过半同意
当数据同步完成后,集群开始从恢复模式进入广播模式,开始接受客户端的事物请求。 当只有Leader或少数机器批准执行某个任务时,则极端情况下Leader和这些少量机器挂掉,则无法保证新Leader知道之前已经批准该任务,这样就违反了数据可靠性。所以Leader在批准一个任务之前应该保证集群里大部分的机器知道这个提案,这样即使Leader挂掉,选举出来的新Leader也会从其他Follower处获取这个提案。而如果Leader要求所有Follower都同意才执行提案也不行,此时若有一个机器挂掉,Leader就无法继续工作,这样的话整个集群相当于单节点,无法保证可靠性。
Zookeeper运维
介绍完Zookeeper的基本知识后,接下来从运维的角度来了解下zookeeper。
部署
启动模式
- 单机模式:在一台机器上启动一个zookeeper进程
- 伪集群模式:在一台机器上启动>=3个zookeeper进程,组成1个集群
- 集群模式:在>=3台机器上各启动1个zookeeper进程,组成1个集群
配置文件
- tickTime
- initLimit
- syncLimit
- dataDir
- clientPort
- autopurge.snapRetainCount
- autopurge.purgeInterval
- snapCount
- maxClientCnxns
- minSessionTimeout
- maxSessionTimeout
监控
端口监控
- 实现:
nc -z 127.1 2181
- 监控指标:
- port.2181.alive
- 报警策略:
- 【P1】2181端口不可用
进程监控
- 实现:
cat /proc/$PID/status|egrep '(FDSize|^Vm|^Rss|Threads|ctxt_switches)'
- 监控指标:
- PLUGIN.zk_proc.FDSize
- PLUGIN.zk_proc.Threads
- PLUGIN.zk_proc.VmData
- PLUGIN.zk_proc.VmExe
- PLUGIN.zk_proc.VmHWM
- PLUGIN.zk_proc.VmLck
- PLUGIN.zk_proc.VmLib
- PLUGIN.zk_proc.VmPeak
- PLUGIN.zk_proc.VmPTE
- PLUGIN.zk_proc.VmRSS
- PLUGIN.zk_proc.VmSize
- PLUGIN.zk_proc.VmStk
- PLUGIN.zk_proc.VmSwap
- PLUGIN.zk_proc.VmSwap
- PLUGIN.zk_proc.VoluntaryCtx
- PLUGIN.zk_proc.NonvoluntaryCtx
- 报警策略:
- 【P2】threads大于300
JVM监控
- 实现:
jstat -gcutil $PID
- 监控指标:
- PLUGIN.jvm-monitor.FGC.count
- PLUGIN.jvm-monitor.FGC.count.total
- PLUGIN.jvm-monitor.FGC.time
- PLUGIN.jvm-monitor.FGC.time.total
- PLUGIN.jvm-monitor.mem.size.kb
- PLUGIN.jvm-monitor.YGC.average.time
- PLUGIN.jvm-monitor.YGC.count
- PLUGIN.jvm-monitor.YGC.count.total
- PLUGIN.jvm-monitor.YGC.time
- PLUGIN.jvm-monitor.YGC.time.total
- 报警策略:
- 无
四字监控
- 实现:
echo mntr|nc 127.1 2181
echo srvr|nc 127.1 2181
- 监控指标:
- PLUGIN.flw-monitor.zk_approximate_data_size
- PLUGIN.flw-monitor.zk_data_rate
- PLUGIN.flw-monitor.zk_ephemerals_count
- PLUGIN.flw-monitor.zk_followers
- PLUGIN.flw-monitor.zk_synced_followers
- PLUGIN.flw-monitor.zk_max_file_descriptor_count
- PLUGIN.flw-monitor.zk_open_file_descriptor_count
- PLUGIN.flw-monitor.zk_avg_latency
- PLUGIN.flw-monitor.zk_max_latency
- PLUGIN.flw-monitor.zk_min_latency
- PLUGIN.flw-monitor.zk_num_alive_connections
- PLUGIN.flw-monitor.zk_conns_rate
- PLUGIN.flw-monitor.zk_outstanding_requests
- PLUGIN.flw-monitor.zk_packets_received
- PLUGIN.flw-monitor.zk_received_rate
- PLUGIN.flw-monitor.zk_packets_sent
- PLUGIN.flw-monitor.zk_sent_rate
- PLUGIN.flw-monitor.zk_pending_syncs
- PLUGIN.flw-monitor.zk_server_type
- PLUGIN.flw-monitor.zk_version
- PLUGIN.flw-monitor.zk_watch_count
- PLUGIN.flw-monitor.zk_watch_rate
- PLUGIN.flw-monitor.zk_znode_count
- PLUGIN.flw-monitor.zk_znode_rate
- 报警策略:
- 【p2】zookeeper服务数据大于500M
- 【p2】zookeeper服务节点数大于100w
- 【p2】zookeeper服务leader变更
- 【p2】zookeeper服务outstandingRequests大于100
- 【p2】zookeeper服务连接数大于1w
- 【p2】zookeeper服务watch数大于10w
- 四字命令介绍:
- conf
- envi
- cons
- crst
- srst
- srvr
- stat
- mntr
- ruok
- wchs
- wchc
- wchp
- dump
日志监控
- 实现:
grep xxx zookeeper.log
- 监控指标:
- PLUGIN.log-monitor.connection_broken_pipe
- PLUGIN.log-monitor.connection_reset_by_peer
- PLUGIN.log-monitor.leader_error
- PLUGIN.log-monitor.len_error
- PLUGIN.log-monitor.stream_exception
- PLUGIN.log-monitor.too_many_connections
- PLUGIN.log-monitor.unexpected_exception
- 报警策略:
- 【p2】zookeeper单个请求大于1M
- 【p2】zookeeper单台客户端连接数大于60
Zxid监控
- 实现:
#!/usr/bin/env python
import json
import socket
import time
import re
import os
import sys
def get_zxid(port):
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
try:
s.connect(('127.0.0.1', port))
except Exception:
return -2
s.send('srvr')
data = s.recv(10240)
s.close()
for line in data.split('n'):
if line.startswith('Zxid'):
return eval(line.split(':')[1].strip() + " & 0xffffffff")
return -1
if __name__ == '__main__':
port=2181
res_maps = []
data1 = get_zxid(port)
time.sleep(1)
data2 = get_zxid(port)
if data1 < 0 or data2 < 0:
sys.exit(1)
map1 = {}
map1['name'] = 'cur_zxid'
map1['value'] = data2
map1['timestamp'] = int("%d" % time.time())
map1["tags"] = {"ZKPort": str(port)}
res_maps.append(map1)
map2 = {}
zxid_rate = (data2 - data1)
map2['name'] = 'zxid_rate'
map2['value'] = (data2 - data1)
map2['timestamp'] = int("%d" % time.time())
map2["tags"] = {"ZKPort": str(port)}
res_maps.append(map2)
map3 = {}
map3['name'] = 'zxid_left_hour'
### (0xffffffff - cur_zxid)/zxid_rate/60/60
if zxid_rate == 0:
map3['value'] = 4294967295-data2
else:
map3['value'] = (4294967295-data2)/zxid_rate/60/60
map3['timestamp'] = int("%d" % time.time())
map3["tags"] = {"ZKPort": str(port)}
res_maps.append(map3)
print json.dumps(res_maps)
- 监控指标:
- PLUGIN.zxid_monitor.cur_zxid 当前的zxid
- PLUGIN.zxid_monitor.zxid_rate zxid的增长速率
- PLUGIN.zxid_monitor.zxid_left_hour zxid溢出剩余的小时数
- 报警策略:
- 【p2】zxid在12小时后即将用完
容量监控
- 实现:
# 容量计算方法:
# 1.每个指标设置不同的权重,综合计算容量水位
# 2.最差的指标作为容量水位
min(cpu_used/cpu_max,cons_num/cons_max,data_num/data_max,watch_num/watch_max,outstanding_num/outstanding_max)
- 监控指标:
- PLUGIN.zk_util.percent 容量百分比
- 报警策略:
- 【p2】容量使用率超过80%
日常运维
抓包分析
#get from https://github.com/pyinx/zk-sniffer
zk-sniffer -device=eth0 -port=2181
分析log文件
#!/bin/sh
function help(){
echo "-----------------"
echo "HELP: $0 LogFile"
echo "-----------------"
exit 1
}
if [ $# -ne 1 ]
then
help
fi
LogFile=$1
if [ ! -f $LogFile ]
then
echo "ERROR: $LogFile not found"
exit 1
fi
zkDir=/usr/local/zookeeper
JAVA_OPTS="$JAVA_OPTS -Djava.ext.dirs=$zkDir:$zkDir/lib"
java $JAVA_OPTS org.apache.zookeeper.server.LogFormatter "$LogFile"
分析snapshot文件
#!/bin/sh
function help(){
echo "-----------------"
echo "HELP: $0 SnapshotFile"
echo "-----------------"
exit 1
}
if [ $# -ne 1 ]
then
help
fi
file=$1
if [ ! -f $file ]
then
echo "ERROR: $file not found"
exit 1
fi
zkDir=/usr/local/zookeeper
JAVA_OPTS="$JAVA_OPTS -Djava.ext.dirs=$zkDir:$zkDir/lib"
java $JAVA_OPTS org.apache.zookeeper.server.SnapshotFormatter "$file"
zkcli.sh批量执行
zkCli.sh -server localhost:2181 <<EOF
ls /
get /
quit
EOF
大量watch场景排查
#!/bin/bash
rm -f con_ip.txt path_count.txt session_count.txt session_ip.txt watch_path.txt watch_sess.txt
#记录session和watch的path
echo wchc|nc 127.1 2181 > watch_sess.txt
#记录所有的ip连接
echo cons|nc 127.1 2181 > con_ip.txt
#记录session和watch的count数
> session_count.txt
last=1
sesion=$(sed -n '1p' watch_sess.txt)
for i in `grep -n '^0x' watch_sess.txt |awk -F: '{print $1}'`
do
if [ $i -eq $last ]
then
continue
fi
x=$(let last++)
y=$(let i--)
let x=last+1
let y=i-1
count=$(sed -n ''$x','$y'p' watch_sess.txt|wc -l)
echo "$sesion $count" >> session_count.txt
last=$i
sesion=$(sed -n ''$i'p' watch_sess.txt)
done
#把ip和session关联起来
> session_ip.txt
while read sess count
do
n=$(grep $sess con_ip.txt -c)
if [ $n -eq 1 ]
then
ip=$(grep $sess con_ip.txt|awk -F: '{print $1}'|sed -n 's# /##p')
else
ip="NULL"
fi
echo "$count $ip $sess" >> session_ip.txt
done < session_count.txt
#记录每个path watch的session
echo wchp |nc 127.1 2181 > watch_path.txt
#记录每个path的watch数量
> path_count.txt
last=""
next=""
while read line
do
if [ $(echo $line|grep '^/' -c) -eq 1 ]
then
last=$next
next=$line
if [ ${last}x != "x" ]
then
echo "$count $last" >> path_count.txt
fi
count=0
else
let count++
fi
done < watch_path.txt
echo "$count $last" >> path_count.txt
#打印watch数最高的Top10 IP列表
awk '{a[$2]+=$1}END{for (i in a)print a[i],i}' session_ip.txt |sort -nr -k1|head
#打印watch数最高的Top10 Path列表
awk '{a[$2]+=$1}END{for (i in a)print a[i],i}' path_count.txt |sort -nr -k1|head
一些经验
使用建议
- 数据大小不超过500M:
- 风险:数据过大会导致集群恢复时间过长、GC加重、客户端超时增多
- 单机连接数不超过2w:
- 风险:连接数过高会导致集群恢复时间过长(zookeeper在选举之前会主动关闭所有的连接,如果这时候不断有新的连接进来会导致zookeeper一直在关闭连接,无法进行选举)
- watch数不超过100w:
- 风险:watch数过高会影响集群的写入性能
- 不要维护一个超大集群:
- 风险:稳定性风险高、故障影响面大、运维不可控
一点思考
Zookeeper是不是已经足够稳定了,一经部署就不再需要关注了呢?答案当然是否定的,目前我们在运维过程中还存在如下几个痛点:
- Zxid频繁溢出
- 不记录请求日志
- ACL不支持节点继承
- 不具备限流能力
面临这些问题,我们是如何解决的呢?
- 针对Zxid溢出的问题,目前官方还没有给出修复方案。有网友提了个PR但是官方没有merge,需要自己打patch,稳定性风险较高,暂不考虑。希望官方在3.6.0版本能解决这个问题。
- 针对后面三个问题,我们开发了proxy来解决。目前proxy已经开发完成,并且在我们生产环境中稳定运行了2个多月。但是proxy还有许多需要完善的地方,希望大家一起来提PR。