zk2etcd 是一款同步 zookeeper 数据到 etcd 的工具
在云原生大浪潮下,业务都逐渐上 k8s,许多业务以前使用 zookeeper 作为注册中心,现都逐渐倾向更加贴近云原生的 etcd。
在业务向云原生迁移改造的过程中,可能需要将 zookeeper 中注册的数据同步到 etcd,且可能需要两者长期共存,实时同步增量数据。
本项目就是为了解决 zookeeper 数据同步到 etcd 而生。
examples 下提供部署到 k8s 的 yaml 示例。
参考 CHANGELOG
当其它同步工具 (比如etcd同步工具) 也在 etcd 的相同目录下进行同步时,如何保证不干扰 (不勿删其它工具写入的 key) ?
zk2etcd 引入 redis 存储,记录本工具向 etcd 写入过的 key,在同步时如果要删除 etcd 中的数据,先判断一下该 key 是否为本工具写入,如果不是就忽略,避免勿删其它工具写入的 key。
zk2etcd sync
运行期间会 watch zookeeper 的数据变更,并将其同步到 etcd 中,原理图:
一些技术难点与实现细节:
- 关于 watch: zookeeper 存储是树状结构,所以需要通过递归的 list watch children 遍历所有节点才能实现 watch 所有数据的变化。
- 关于新增: 在没有提前知道完整 key 的情况下,zookeeper 无法感知新增事件,只能收到 ChildrenChanged 事件,zk2etcd 在收到 ChildrenChanged 事件后拿到所有 children 并与 etcd 中数据进行计算对比,得出哪个 key 是新增的,并将其 put 到 etcd 中。
- 关于删除: zookeeper 的 list children watch 可以收到本节点被删除的事件,由于 zk2etcd 通过递归对所有节点进行了 list children watch,可以感知到所有节点的删除的事件,当收到后会同步删除 etcd 中的数据。
- 每次对 etcd 进行增删时也会同步增删 redis 中的数据,以便全量同步发生删除时判断 key 是否为本工具写入。
- 由于 zookeeper 的 watch 是一次性的,所以每次收到事件后又必须重新 watch,两次 watch 之间理论上是可能丢失件的,这也是 zookeeper 本身 watch 机制的缺陷。
- 由于 zookeeper 是树状结构,而 etcd v3 是扁平结构,etcd 无法像 zookeeper 一样 list children,也就无法取到某个目录下的子节点,只能按照 prefix 来 list 所有 key,所以无法对比出某个目录下,zk 中没有,etcd 中有的情况。当 zookeeper 的某个 delete 事件丢失后,只能依赖全量同步来 fix。
zk2etcd diff
可以对比出 zookeeper 与 etcd 中数据中的数据差异,以便观察同步一致性的情况,原理图:
- 通过拉取 zookeeper 与 etcd 中的全量数据进行对比,查出 etcd 中哪些 key 是多余的(应该删除),哪些是缺失的(应该补齐)。
- 判断多余 key 会查 redis,判断多余的 key 是否为本工具写入,如果不是则将其移出 (忽略该 key),认为它不是多余的。
zk2etcd diff
带上 --fix
则表示进行手动全量同步一次,即拿到 diff 结果,删除 etcd 中多余的 key,补齐缺失的 key。
zk2etcd sync
在启动时会全量同步一次,然后会周期性全量同步一次来 fix 掉 zookeeper 与 etcd 中数据存在的一些差异。
zk2etcd 主要提供 sync
和 diff
两个核心的子命令:
- sync 用于同步 zookeeper 中数据到 etcd,启动时全量同步一次,然后增量实时同步,期间还会定期全量同步进行 fix,避免 watch zookeeper 时遗漏某些 event 导致数据不一致。
- diff 用于检测 zookeeper 与 etcd 中数据的差异,也提供 fix 可选项来手动 fix 数据差异。
具体启动参数及其解释详见 help:
$ zk2etcd sync --help
sync data from zookeeper to etcd
Usage:
zk2etcd sync [flags]
Flags:
--concurrent uint the concurreny of syncing worker (default 50)
--enable-event-log enable event log
--etcd-cacert string verify certificates of TLS-enabled secure servers using this CA bundle
--etcd-cert string identify secure client using this TLS certificate file
--etcd-key string identify secure client using this TLS key file
--etcd-servers string comma-separated list of etcd servers address
--fullsync-interval duration the interval of full sync, set to 0s to disable it (default 5m0s)
-h, --help help for sync
--log-level string log output level,possible values: 'debug', 'info', 'warn', 'error', 'panic', 'fatal' (default "info")
--redis-password string redis password
--redis-server string redis server address
--redis-username string redis username
--zookeeper-exclude-prefix string comma-separated list of zookeeper path prefix to be excluded (default "/dubbo/config")
--zookeeper-prefix string comma-separated list of zookeeper path prefix to be synced (default "/dubbo")
--zookeeper-servers string comma-separated list of zookeeper servers address
compare keys between zk and etcd
Usage:
zk2etcd diff [flags]
Flags:
--concurrent uint the concurreny of syncing worker (default 50)
--enable-event-log enable event log
--etcd-cacert string verify certificates of TLS-enabled secure servers using this CA bundle
--etcd-cert string identify secure client using this TLS certificate file
--etcd-key string identify secure client using this TLS key file
--etcd-servers string comma-separated list of etcd servers address
--fix set to true will fix the data diff between zk and etcd
-h, --help help for diff
--log-level string log output level,possible values: 'debug', 'info', 'warn', 'error', 'panic', 'fatal' (default "info")
--max-round int max diff round, must >= 1 (default 1)
--redis-password string redis password
--redis-server string redis server address
--redis-username string redis username
--round-interval duration the interval of every diff round (default 3s)
--zookeeper-exclude-prefix string comma-separated list of zookeeper path prefix to be excluded (default "/dubbo/config")
--zookeeper-prefix string comma-separated list of zookeeper path prefix to be synced (default "/dubbo")
--zookeeper-servers string comma-separated list of zookeeper servers address
sync 启动示例:
$ zk2etcd sync
--redis-server=redis.test.svc.cluster.local:6379 \
--redis-password=hello:world \
--etcd-servers=etcd.test.svc.cluster.local:2379 \
--zookeeper-servers=zookeeper.test.svc.cluster.local:2181 \
--zookeeper-exclude-prefix=/dubbo/config,/roc/test \
--zookeeper-prefix=/dubbo,/roc \
--log-level=info \
--enable-event-log \
--fullsync-interval=5m \
--concurrent=50
若 etcd 需要证书,可以配置下证书相关参数:
--etcd-cacert=/certs/ca.crt \
--etcd-cert=/certs/cert.pem \
--etcd-key=/certs/key.pem
使用 diff 检测数据差异示例:
$ zk2etcd diff
--redis-server=redis.test.svc.cluster.local:6379 \
--redis-password=hello:world \
--etcd-servers=etcd.test.svc.cluster.local:2379 \
--zookeeper-servers=zookeeper.test.svc.cluster.local:2181 \
--zookeeper-exclude-prefix=/dubbo/config,/roc/test \
--zookeeper-prefix=/dubbo,/roc \
--log-level=info \
--concurrent=50
若需要 etcd 证书,同 sync 加上证书相关参数即可。
若检测完需要立即 fix,可以再加上 --fix
参数。
zk2etcd 每个版本都有对应的容器镜像 (imroc/zk2etcd
),tag 与 版本号一致。
通过 docker 启动示例:
$ docker run -d imroc/zk2etcd zk2etcd sync \
--redis-server=redis.test.svc.cluster.local:6379 \
--redis-password=hello:world \
--etcd-servers=etcd.test.svc.cluster.local:2379 \
--zookeeper-servers=zookeeper.test.svc.cluster.local:2181 \
--zookeeper-exclude-prefix=/dubbo/config,/roc/test \
--zookeeper-prefix=/dubbo,/roc \
--log-level=info \
--fullsync-interval=5m \
--concurrent=50
$ docker run -it imroc/zk2etcd zk2etcd diff \
--redis-server=redis.test.svc.cluster.local:6379 \
--redis-password=hello:world \
--etcd-servers=etcd.test.svc.cluster.local:2379 \
--zookeeper-servers=zookeeper.test.svc.cluster.local:2181 \
--zookeeper-exclude-prefix=/dubbo/config,/roc/test \
--zookeeper-prefix=/dubbo,/roc \
--log-level=info \
--fullsync-interval=5m \
--concurrent=50
部署到 Kubernetes 中的 YAML 示例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: debug
spec:
replicas: 1
selector:
matchLabels:
app: debug
template:
metadata:
labels:
app: debug
spec:
serviceAccountName: zk2etcd
containers:
- name: debug
image: imroc/zk2etcd:1.3.0
volumeMounts:
- mountPath: /certs
name: etcd-certs
command:
- zk2etcd
- sync
- '--redis-server=redis.test.svc.cluster.local:6379'
- '--redis-password=hello:world'
- '--enable-event-log'
- '--etcd-servers=etcd.test.svc.cluster.local:2379'
- '--zookeeper-servers=zookeeper.test.svc.cluster.local:2181'
- '--zookeeper-exclude-prefix=/dubbo/config,/roc/test'
- '--zookeeper-prefix=/dubbo,/roc'
- '--log-level=info'
- '--fullsync-interval=5m'
- '--concurrent=50'
- '--etcd-cacert=/certs/ca.crt'
- '--etcd-cert=/certs/cert.pem'
- '--etcd-key=/certs/key.pem'
volumes:
- name: certs
secret:
secretName: etcd-certs
---
apiVersion: v1
kind: Service
metadata:
name: zk2etcd
labels:
app: zk2etcd
spec:
type: ClusterIP
ports:
- port: 80
protocol: TCP
name: http
selector:
app: zk2etcd
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: zk2etcd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: zk2etcd
rules:
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- '*'
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: zk2etcd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: zk2etcd
subjects:
- kind: ServiceAccount
name: zk2etcd
zk2etcd sync
支持高可用部署,--leader-elect
默认为 true,表示启用 leader 选举,原理是利用 k8s api 注册租约,各个 zk2etcd 副本竞争租约,只能有一个副本在运行,当运行的副本因某种原因无法正常工作时,其它的副本将竞争租约选举出新的 leader 来继续运行。
目前高可用部署需要将 zk2etcd 部署在 k8s 中,且使用 RBAC 授予 lease 的读写权限(见上面的 yaml 示例)。
zk2etcd 提供了几个 HTTP 接口,端口为 80 (暂不支持自定义):
/log
: 用于动态调整日志级别,POST 方法,只接收一个level
参数,自带的 log 子命令封装了此调用,使用示例:zk2etcd log --level=debug --zk2etcd-addr=http://zk2etcd:80
。/debug
: golang 的 pprof 接口。使用示例:go tool pprof -http=:9090 http://zk2etcd.test.svc.cluster.local:80/debug/pprof/heap
/metrics
: 提供 prometheus 指标。
- 若用 k8s 部署,启动参数中若有双引号或其它特殊字符,最外层用单引号括起来,避免 yaml 格式问题导致启动失败,示例:
- '--etcd-key="/certs/key.pem"'
。 - 在 k8s 中部署且需要使用 etcd 证书,可以将证书、密钥与根证书存储到 secret 中,然后再 pod 中挂载 secret,启动参数引用相应的文件路径。
zk2etcd 通过 80 端口 (暂时写死) 暴露 metrics,路径 /metrics
,包含以下 metrics:
- zk2etcd_etcd_op_total: etcd 操作次数统计,以下是示例 promql
- 统计etcd写入次数: sum(irate(zk2etcd_etcd_op_total{op="put"}[2m]))
- 统计etcd删除次数: sum(irate(zk2etcd_etcd_op_total{op="delete"}[2m]))
- etcd 操作失败统计: sum by (status,op)(rate(zk2etcd_etcd_op_total{status!="success"}[1m]))
- zk2etcd_etcd_op_duration_seconds: etcd操作时延统计,以下是示例 promql
- etcd p95时延: histogram_quantile(0.95, sum(rate(zk2etcd_etcd_op_duration_seconds_bucket[1m])) by (le))
- etcd p99时延: histogram_quantile(0.99, sum(rate(zk2etcd_etcd_op_duration_seconds_bucket[1m])) by (le))
- zk2etcd_zk_op_total: zk 操作次数统计,以下是示例 promql
- zk 操作失败统计: sum by (status,op)(rate(zk2etcd_zk_op_total{status!="success"}[1m]))
- zk 各类型操作 rps: sum by (op) (irate(zk2etcd_zk_op_total[2m]))
- zk2etcd_zk_op_duration_seconds: zk操作时延统计,以下是示例 promql
- zk p95时延: histogram_quantile(0.95, sum(rate(zk2etcd_zk_op_duration_seconds_bucket[1m])) by (le))
- zk p99时延: histogram_quantile(0.99, sum(rate(zk2etcd_zk_op_duration_seconds_bucket[1m])) by (le))
- zk2etcd_fixed_total: 全量同步 fix 时,删除或补齐 etcd 数据次数统计(通常是增量同步时丢失event导致的部分数据不同步)
- 统计因etcd数据缺失导致的数据补齐操作次数: sum(rate(zk2etcd_fixed_total{type="put"}[1m]))
- 统计因etcd数据多余导致的数据删除操作次数: sum(rate(zk2etcd_fixed_total{type="delete"}[1m]))
- 统计全部fix操作: sum(rate(zk2etcd_fixed_total[1m]))
- zk2etcd_zk_conn_total: zk连接数量
- zk2etcd_zk_connect_total: zk连接次数
grafana 面板示例:
- 全量同步
- 增量同步
- 版本管理 (version 子命令打印详细信息, 版本号/commit/buiddate 等)
- 日志增强 (自定义 level + json 输出)
- 并发度控制
- 数据一致性 (周期性全量检测+watch delete)
- 支持配置 etcd 证书
- 支持配置多个 zk prefix
- 检测 zk 与 etcd 数据差异的 diff 能力
- 支持 prometheus 指标监控
- 容灾与自愈能力
- 支持与 etcd 同步工具共存(不勿删其它同步工具写入的数据,需引入外部存储记录状态)
- 日志更丰富的自定义(如文件存储,事件日志)
- 日志记录事务id,将一系列操作串起来
- 支持动态加载配置(如日志级别)