Apache ZooKeeper 是一个开源服务,可以实现高度可靠的分布式协调。它常被应用于分布式系统中,管理配置信息、命名服务、分布式同步、法定人数和状态。此外,一些分布式系统依靠 ZooKeeper 来实现共识、leader 选举和组管理。
本文将通过以下主题,讨论在 Rocky Linux 9 上安装和配置满足生产环境高可用性及弹性要求的 Zookeeper 集群:
完成本教程需要四个节点:一个节点作为 Ansible 控制节点,三个节点部署 Zookeeper 集群。
四个节点分别为 2 核 CPU、2 GB 内存的虚拟机。
ZooKeeper 将数据保存在内存中以实现高吞吐量和低延迟,因此生产环境要提供更高内存的主机,建议 8 GB 内存。低内存可能产生 JVM 交换,导致 ZooKeeper 服务延迟。高延迟可能会导致客户端会话超时等问题,影响系统稳定。
Ansilbe 节点信息:
# IP:10.211.55.18
# 主机名:automate-host.aiops.red
# 系统版本:Rocky Linux release 9.1
Ansible Inventory hosts:
[zookeeper]
zk1.server.aiops.red
zk2.server.aiops.red
zk3.server.aiops.red
[prometheus]
prometheus.server.aiops.red
Zookeeper 节点信息:
# zookeeper1
# IP:10.211.55.31
# 主机名:zk1.server.aiops.red
# 系统版本:Rocky Linux release 9.1
# zookeeper2
# IP:10.211.55.32
# 主机名:zk2.server.aiops.red
# 系统版本:Rocky Linux release 9.1
# zookeeper3
# IP:10.211.55.33
# 主机名:zk3.server.aiops.red
# 系统版本:Rocky Linux release 9.1
为使安装过程顺利进行,节点应满足以下要求。
Zookeeper 集群节点时钟须保持同步。
要自动化实现时钟同步,可以参考 “Linux 9 自动化部署 NTP 服务”。
Ansible 控制节点能够解析 Zookeeper 节点的主机名,通过主机名访问 Zookeeper 节点。
要实现主机名称解析,可以在 Ansible 控制节点的 /etc/hosts 文件中指定 Zookeeper 节点的 IP、主机名条目,或者参考 “Linux 9 自动化部署 DNS 服务” 一文配置 DNS 服务。
Ansible 控制节点可以免密登录 Zookeeper 节点,并能够免密执行 sudo。可以参考 “Linux 9 自动化部署 NTP 服务” 中的 “部署环境要求” 一节实现。
在本文的最后部分,演示了通过 Prometheus 自动化监控 Zookeeper 集群。如要完成此部分,需要准备好 Prometheus 监控系统。有关 Prometheus 监控系统的自动化部署,可以参考 ”Linux 9 自动化配置 Prometheus 监控系统“。
在满足以上要求后,开始 Zookeeper 集群的部署。
Linux 的 dnf 仓库没有提供 Zookeeper 的安装包,因此需要从 Apache Zookeeper 官网下载。应将下载包和安装配置 Zookeeper 分成两个 Ansible Role 实现。因为包下载一次即可,无需多次执行。
执行以下命令,创建 download_zookeeper Role:
ansible-galaxy role init --init-path ~/roles download_zookeeper
cd ~/roles/download_zookeeper
图 35.1 创建下载 Zookeeper 的角色
在 tasks/main.yml 文件中,编辑下载 Zookeeper 的任务:
---
# tasks file for download_zookeeper
- name: create download directory task
ansible.builtin.file:
path: "{{ download_path }}"
state: directory
- name: download and unarchive tarball task
ansible.builtin.unarchive:
src: "{{ download_url }}"
dest: "{{ download_path }}"
remote_src: true
extra_opts:
- --strip-components=1
在 ~/playbooks/ 目录下创建 deploy_zookeeper/ 目录:
mkdir ~/playbooks/deploy_zookeeper
cd ~/playbooks/deploy_zookeeper
图 35.2 创建下载 Zookeeper 的 Playbook
创建 download_zookeeper.yaml Playbook 文件,内容如下:
---
- name: download zookeeper tarball play
hosts: localhost
become: false
gather_facts: false
vars_files:
- variables.yaml
roles:
- role: download_zookeeper
tags: download_zookeeper
...
创建变量文件:
mkdir vars
touch vars/variables.yaml
在 vars/variables.yaml 中添加需要的变量:
download_path: ~/software/zookeeper_tools/zookeeper
download_url: https://downloads.apache.org/zookeeper/stable/apache-zookeeper-3.6.3-bin.tar.gz
执行 download_zookeeper.yaml Playbook,下载安装包:
ansible-playbook download_zookeeper.yaml
图 35.3 下载 Zookeeper 安装包
使用 tree 命令查看下载文件的目录结构:
tree -L 1 ~/software/zookeeper_tools/zookeeper
图 35.4 Zookeeper 目录结构
下载的版本为当前稳定版,可以在 https://downloads.apache.org/zookeeper/stable/ 查看。
Zookeeper 服务依赖 Java 环境才能运行,因此需要在运行 Zookeeper 的节点上部署 JDK。
有关 JDK 自动化部署的详细介绍,可以参考 ”Linux 9 自动化部署 JDK“。在 JDK 部署完成后,继续下面的步骤。
在节点和安装包准备好后,本节讨论 Zookeeper 集群的安装。
Zookeeper 集群依赖 SID 进行快速选举,SID 的值必须写入数据目录的 myid 文件中。通过定义 Local Facts 变量,为节点设置 SID 值。
创建三个 Facts 文件:
for sid in $(seq 1 3); do echo -e "[var]
sid=${sid}" > zk${sid}.fact; done
图 35.5 创建 Local Facts 变量文件
为 Zookeeper 节点创建 /etc/ansible/facts.d/ 目录:
ansible zookeeper -m file -a "path=/etc/ansible/facts.d state=directory" -b
图 35.6 创建 facts.d 目录
将三个文件拷贝到对应节点的 /etc/ansible/facts.d/:
ansible zk1.server.aiops.red -m copy -a "src=zk1.fact dest=/etc/ansible/facts.d/zk.fact" -b
ansible zk2.server.aiops.red -m copy -a "src=zk2.fact dest=/etc/ansible/facts.d/zk.fact" -b
ansible zk3.server.aiops.red -m copy -a "src=zk3.fact dest=/etc/ansible/facts.d/zk.fact" -b
图 35.7 拷贝 fact 文件到对应的托管节点
使用以下命令创建 Role:
ansible-galaxy role init --init-path ~/roles deploy_zookeeper
cd ~/roles/deploy_zookeeper
图 35.8 创建部署 Zookeeper 的角色
使用独立用户运行服务,可以提高隔离性和安全性。因此,第一个任务是创建用于启动 Zookeeper 服务的用户。
编辑 tasks/main.yml 文件,添加创建用户的任务:
- name: "create {{ user_name }} user task"
ansible.builtin.user:
name: "{{ user_name }}"
home: "{{ home_dir }}"
shell: /sbin/nologin
需要为任务定义两个变量,user_name 表示创建的用户名;home_dir 用户家目录。Zookeeper 文件将被安装在 home_dir 目录。
变量可以在 defaults/main.yml 文件或 vars/main.yml 文件中定义,前者定义的变量,可以被 Playbook 中的变量覆盖。
Zookeeper 将所有配置、状态数据持久化在磁盘上,这样即使重启后,服务仍能正常工作。
在 tasks/main.yml 中编写第二个任务,为 Zookeeper 创建数据目录:
- name: create data directory task
ansible.builtin.file:
path: "{{ home_dir }}/data/zookeeper"
state: directory
owner: "{{ user_name }}"
group: "{{ user_name }}"
mode: 0755
将下载好的 Zookeeper 安装文件,拷贝到 home_dir 下。编辑 tasks/main.yml 文件,添加以下两个任务:
- name: copy the installation file task
ansible.posix.synchronize:
src: "{{ download_path }}"
dest: "{{ home_dir }}"
- name: set the file owner task
ansible.builtin.shell: "chown {{ user_name }}.{{ user_name }} {{ home_dir }} -R"
ansible.posix.synchronize 模块在此处以 root 用户同步文件。因此,在文件同步结束后,使用 chown 修改目录的属主及属组。
Zookeeper 集群中的节点应为奇数个,这样就能形成有效的法定人数。法定人数是指在提交事务前,需要同意的最小节点数。法定人数为奇数,才能形成多数。偶数个节点可能导致平局,这将意味着节点不会达成多数或共识。
在 templates/ 目录下,创建 zoo.cfg.j2 文件,内容如下:
tickTime=2000
dataDir={{ home_dir }}/data/zookeeper
clientPort={{ client_port }}
maxClientCnxns=60
initLimit=10
syncLimit=5
{% for host in groups['zookeeper'] %}
server.{{ hostvars[host].ansible_local.zk.var['sid'] }}={{ host }}:{{ follower_port }}:{{ leader_port }}
{% endfor %}
模板配置文件解析:
sid 的值来自于之前定义的 Local Facts 变量,通过 hostvars[host] 从对应主机获取 sid。
准备好了模板文件,接下来在 tasks/main.yml 中定义将模板配置文件渲染为 Zookeeper 配置文件的任务:
- name: render configuration file task
ansible.builtin.template:
src: zoo.cfg.j2
dest: "{{ home_dir }}/zookeeper/conf/zoo.cfg"
notify: restart zookeeper service handler
notify: restart zookeeper service handler 定义了当配置文件有变化时,执行 ”restart zookeeper service handler“,重启 Zookeeper 服务。
为 Zookeeper 生成 myid 文件,该文件的内容应与 sid 一致。创建 templates/myid.j2 文件,内容如下:
{{ ansible_local.zk.var['sid'] }}
在 tasks/main.yml 中定义将模板渲染为 myid 文件的任务:
- name: render myid file task
ansible.builtin.template:
src: myid.j2
dest: "{{ home_dir }}/data/zookeeper/myid"
myid 文件应存放在 Zookeeper 的 dataDir 目录下。
创建启动 Zookeeper 服务的单元文件模板,templates/zookeeper.service.j2 内容如下:
[Unit]
Description=Zookeeper Daemon
Documentation=http://zookeeper.apache.org
Requires=network.target
After=network.target
[Service]
Type=forking
WorkingDirectory={{ home_dir }}/zookeeper
User={{ user_name }}
Group={{ user_name }}
ExecStart={{ home_dir }}/zookeeper/bin/zkServer.sh start {{ home_dir }}/zookeeper/conf/zoo.cfg
ExecStop={{ home_dir }}/zookeeper/bin/zkServer.sh stop {{ home_dir }}/zookeeper/conf/zoo.cfg
ExecReload={{ home_dir }}/zookeeper/bin/zkServer.sh restart {{ home_dir }}/zookeeper/conf/zoo.cfg
TimeoutSec=30
Restart=on-failure
[Install]
WantedBy=default.target
在 tasks/main.yml 中定义将模板文件渲染为 systemd 单元文件的任务:
- name: render systemd unit file task
ansible.builtin.template:
src: zookeeper.service.j2
dest: /usr/lib/systemd/system/zookeeper.service
需要在 Firewalld 上开启 Zookeeper 服务的相关端口。编辑 tasks/main.yml 文件,添加以下任务:
- name: turn on zk ports in the firewalld task
ansible.builtin.firewalld:
port: "{{ item }}"
permanent: true
immediate: true
state: enabled
with_items:
- "{{ client_port }}/tcp"
- "{{ follower_port }}/tcp"
- "{{ leader_port }}/tcp"
Zookeeper 在启动时,需要为其指定 JAVA_HOME 变量。编辑 tasks/main.yml 文件,添加在 zkEnv.sh 文件中设置该变量的任务:
- name: set JAVA_HOME in zkEnv.sh file task
ansible.builtin.lineinfile:
path: "{{ home_dir }}/zookeeper/bin/zkEnv.sh"
line: "export JAVA_HOME=/opt/jdk19"
insertafter: "/usr/bin/env"
JAVA_HOME 变量的值要设置为真实值,通过 “Linux 9 自动化部署 JDK” 安装的 JDK,路径为 /opt/jdk19。
安装和配置已经进行完成,可以启动 Zookeeper 服务了。编辑 tasks/main.yml 文件,添加启动任务:
- name: enable and start the zk service task
ansible.builtin.systemd:
name: zookeeper.service
state: started
enabled: true
daemon_reload: true
在这个任务中,会重载单元文件,启动 zookeeper 服务,并把该服务设置为开机启动。
在 handlers/main.yml 文件中,添加名为 ”restart zookeeper service handler“ 的 Handlers:
---
# handlers file for deploy_zookeeper
- name: restart zookeeper service handler
ansible.builtin.systemd:
name: zookeeper.service
state: restarted
创建 ~/playbooks/deploy_zookeeper/deploy.yaml 文件,内容如下:
---
- name: deploy zookeeper cluster play
hosts: zookeeper
become: true
gather_facts: true
vars_files:
- variables.yaml
roles:
- role: deploy_zookeeper
tags: deploy_zookeeper
...
Playbook 文件中用到了 variables.yaml 变量文件,在该文件中添加需要的变量。
vars/variables.yaml 文件内容如下:
download_path: ~/software/zookeeper_tools/zookeeper
download_url: https://downloads.apache.org/zookeeper/stable/apache-zookeeper-3.6.3-bin.tar.gz
home_dir: /opt/zk
user_name: zk
client_port: 2181
follower_port: 2888
leader_port: 3888
执行 deploy.yaml Playbook 文件,部署 Zookeeper 集群:
ansible-playbook deploy.yaml
图 35.9 部署 Zookeeper 集群
在集群成功部署后,可以对集群进行测试。
登录 Zookeeper 集群的某个节点,执行 zkCli.sh 命令:
ssh zk1.server.aiops.red
sudo su -
/opt/zk/zookeeper/bin/zkCli.sh -server zk2.server.aiops.red:2181
执行以下命令,创建、查看、删除 znode:
# 创建
create /hello "aiops"
# 查看
get /hello
# 删除
delete /hello
图 35.10 验证 Zookeeper 集群
znodes 是 ZooKeeper 中的基本抽象,类似于文件系统上的文件和目录。ZooKeeper 将其数据维护在分层命名空间中,znodes 是该命名空间的数据寄存器。
成功测试了两个 ZooKeeper 节点之间的连接。还通过 create、get 和 delete znodes 学习了基本的 znode 管理。Zookeeper 集群已成功部署,可以使用 ZooKeeper 了。
本节讨论 Zookeeper 的自动化监控。监控服务使用 Prometheus 完成,因此需要准备好 Prometheus 监控系统。可参考上一篇文章完成 Prometheus 的自动化部署。
从 3.6.0 版本开始,Zookeeper 内置的指标系统提供了丰富的指标,方便用户对其监控。编辑 Zookeeper 模板配置文件 templates/zoo.cfg.j2,添加 metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider:
tickTime=2000
dataDir={{ home_dir }}/data/zookeeper
clientPort={{ client_port }}
maxClientCnxns=60
initLimit=10
syncLimit=5
{% for host in groups['zookeeper'] %}
server.{{ hostvars[host].ansible_local.zk.var['sid'] }}={{ host }}:{{ follower_port }}:{{ leader_port }}
{% endfor %}
metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
该配置启用了 Prometheus MetricsProvider,Zookeeper 服务将新增对 TCP 7000 端口的监听。
编辑 Zookeeper Role tasks/main.yml,在 firewalld 任务增加 7000 端口:
- name: turn on zk ports in the firewalld task
ansible.builtin.firewalld:
port: "{{ item }}"
permanent: true
immediate: true
state: enabled
with_items:
- "{{ client_port }}/tcp"
- "{{ follower_port }}/tcp"
- "{{ leader_port }}/tcp"
- "7000/tcp"
再次运行 Playbook 使配置文件生效:
ansible-playbook deploy.yaml
图 35.11 再次执行 Playbook,启用 Prometheus MetricsProvider
创建名为 zk_monitor.yaml Playbook 文件,添加以下任务向 Prometheus 的配置文件插入抓取 Zookeeper 目标的配置:
- name: zk monitor play
hosts: prometheus
become: true
gather_facts: false
tasks:
- name: insert scrape zk block task
ansible.builtin.blockinfile:
path: /opt/monitor/prometheus/prometheus.yaml
insertafter: EOF
marker: "# add zk scrape {mark}"
marker_begin: "begin ==>"
marker_end: "end ==>"
create: true
state: present
block: |2
- job_name: "zk"
static_configs:
- targets:
{% for host in groups["zookeeper"] %}
- "{{ host }}:7000"
{% endfor %}
notify: restart prometheus service handler
ansible.builtin.blockinfile 任务将在 promethues.yaml 文件末尾添加抓取 Zookeeper 指标的配置。
有了指标,接下来配置 Dashboard。
在 zk_monitor.yaml 文件中,新增下载 Grafana Dashboard 的任务。因为 Grafana 与 Prometheus 是同一主机,因此该任务与 “insert scrape zk block task” 任务定义在同一 Play 中:
- name: downlaod zookeeper dashboard json file task
ansible.builtin.get_url:
url: https://grafana.com/api/dashboards/10465/revisions/4/download
dest: /var/lib/grafana/dashboard/zookeeper.json
创建 files/ 目录,并在该目录下创建规则文件 zk_rule.yml,内容如下:
groups:
- name: zk-alert-example
rules:
- alert: ZooKeeper server is down
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Instance {{ $labels.instance }} ZooKeeper server is down"
description: "{{ $labels.instance }} of job {{$labels.job}} ZooKeeper server is down: [{{ $value }}]."
- alert: create too many znodes
expr: znode_count > 1000000
for: 1m
labels:
severity: warning
annotations:
summary: "Instance {{ $labels.instance }} create too many znodes"
description: "{{ $labels.instance }} of job {{$labels.job}} create too many znodes: [{{ $value }}]."
- alert: create too many connections
expr: num_alive_connections > 50 # suppose we use the default maxClientCnxns: 60
for: 1m
labels:
severity: warning
annotations:
summary: "Instance {{ $labels.instance }} create too many connections"
description: "{{ $labels.instance }} of job {{$labels.job}} create too many connections: [{{ $value }}]."
- alert: znode total occupied memory is too big
expr: approximate_data_size /1024 /1024 > 1 * 1024 # more than 1024 MB(1 GB)
for: 1m
labels:
severity: warning
annotations:
summary: "Instance {{ $labels.instance }} znode total occupied memory is too big"
description: "{{ $labels.instance }} of job {{$labels.job}} znode total occupied memory is too big: [{{ $value }}] MB."
- alert: set too many watch
expr: watch_count > 10000
for: 1m
labels:
severity: warning
annotations:
summary: "Instance {{ $labels.instance }} set too many watch"
description: "{{ $labels.instance }} of job {{$labels.job}} set too many watch: [{{ $value }}]."
- alert: a leader election happens
expr: increase(election_time_count[5m]) > 0
for: 1m
labels:
severity: warning
annotations:
summary: "Instance {{ $labels.instance }} a leader election happens"
description: "{{ $labels.instance }} of job {{$labels.job}} a leader election happens: [{{ $value }}]."
- alert: open too many files
expr: open_file_descriptor_count > 300
for: 1m
labels:
severity: warning
annotations:
summary: "Instance {{ $labels.instance }} open too many files"
description: "{{ $labels.instance }} of job {{$labels.job}} open too many files: [{{ $value }}]."
- alert: fsync time is too long
expr: rate(fsynctime_sum[1m]) > 100
for: 1m
labels:
severity: warning
annotations:
summary: "Instance {{ $labels.instance }} fsync time is too long"
description: "{{ $labels.instance }} of job {{$labels.job}} fsync time is too long: [{{ $value }}]."
- alert: take snapshot time is too long
expr: rate(snapshottime_sum[5m]) > 100
for: 1m
labels:
severity: warning
annotations:
summary: "Instance {{ $labels.instance }} take snapshot time is too long"
description: "{{ $labels.instance }} of job {{$labels.job}} take snapshot time is too long: [{{ $value }}]."
- alert: avg latency is too high
expr: avg_latency > 100
for: 1m
labels:
severity: warning
annotations:
summary: "Instance {{ $labels.instance }} avg latency is too high"
description: "{{ $labels.instance }} of job {{$labels.job}} avg latency is too high: [{{ $value }}]."
- alert: JvmMemoryFillingUp
expr: jvm_memory_bytes_used / jvm_memory_bytes_max{area="heap"} > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "JVM memory filling up (instance {{ $labels.instance }})"
description: "JVM memory is filling up (> 80%)
labels: {{ $labels }} value = {{ $value }}
"
将 zk_rule.yml 文件拷贝至 Prometheus 安装目录下:
- name: copy rule file to prometheus task
ansible.builtin.copy:
src: zk_rule.yml
dest: /opt/monitor/prometheus/zk_rule.yml
owner: monitor
group: monitor
在 Prometheus 配置文件 prometheus.yaml 插入引用 zk_rule.yml 规则文件的行:
- name: insert line
lineinfile:
path: /opt/monitor/prometheus/prometheus.yaml
insertafter: "rule_files:"
line: " - zk_rule.yml"
notify: restart prometheus service handler
在 zk_monitor.yaml 中定义 Handlers:
handlers:
- name: restart prometheus service handler
ansible.builtin.systemd:
name: prometheus.service
state: restarted
最后执行 zk_monitor.yaml Playbook 文件:
ansible-playbook zk_monitor.yaml
图 35.12 监控 Zookeeper
登录 Grafana,查看 Dashboard,点击 “Zookeeper by Prometheus”,Cluster 选择 “zk”,浏览器将显示 Zookeeper 的监控页面:
图 35.13 Zookeeper 监控页面
在 Prometheus 的 rules 页面,能够看到为 Zookeeper 配置的报警规则,如下图:
图 35.14 Zookeeper 报警规则
本教程演示了 Zookeeper 集群的自动化部署、简单的数据操作,以及通过 Prometheus 对 Zookeeper 集群进行监控。教程同样适用于其他基于 RPM 的 Linux 发行版。
来源:魏文第
页面更新:2024-04-30
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号