作者介绍:简历上没有一个精通的运维工程师。请点击上方的蓝色《运维小路》关注我,下面的思维导图也是预计更新的内容和当前进度(不定时更新)。
前面我们介绍介绍了几个常用的代理服务器,本章节我们讲来讲解Zookeeper这个中间件。
我们前面讲解了ZooKeeper的基本操作和原理,后面几个小节将通过几个案例来更加深入ZooKeeper。我们的第一个案例就是把ZooKeeper作为注册中心,来实现多个服务的通信,这样完成服务之间的解耦。
准备工作
实际上这个在JAVA领域使用会更多一点,但是JAVA是属于半编译类型的语言,为了方便测试和演示,所以这里我们采用的python来演示。由于这里方便使用Python3测试,所以这里我采用的Rocky9.5的系统。
#先安装pip工具
#再安装python支持zk的库
yum -y install python-pip && pip install kazoo
演示工作
这里设计了2个服务,服务a和服务b,他们之间有调用关系,但是他们默认不知道对方的地址。
服务a
服务a为了方便演示本身不对外提供服务,他只定时向服务b发送消息,但是服务a是不知道服务b的地址,他需要通过链接ZooKeeper去获取服务b的地址。
import json
import socket
import time
import threading
from kazoo.client import KazooClient
from kazoo.exceptions import NoNodeError
class ProgramA:
def __init__(self, zk_hosts):
# ZK 连接配置
self.zk = KazooClient(hosts=zk_hosts)
self.zk.start()
# 服务元数据
self.service_name = "a"
self.registration_path = f"/services/{self.service_name}"
# 目标服务配置
self.target_service = "b"
self.target_path = f"/services/{self.target_service}"
self.target_address = None
# 注册持久节点
self._register()
self._setup_watch()
self._start_sender()
def _register(self):
"""注册持久节点(节点数据可空)"""
data = json.dumps({"desc": "message_sender"}).encode()
self.zk.ensure_path(self.registration_path)
self.zk.set(self.registration_path, data)
print(f"[A] 持久节点已注册: {self.registration_path}")
def _unregister(self):
"""主动注销节点"""
try:
self.zk.delete(self.registration_path)
print(f"[A] 节点已注销: {self.registration_path}")
except NoNodeError:
pass
def _setup_watch(self):
"""监听B服务地址变化"""
@self.zk.DataWatch(self.target_path)
def watch_node(data, stat):
if data:
self.target_address = json.loads(data.decode())
print(f"[A] 发现B服务地址: {self.target_address}")
else:
self.target_address = None
print("[A] B服务不可用")
def _send_message(self):
"""发送消息到B服务"""
if not self.target_address:
print("[A] 当前无可用B服务地址")
return
try:
with socket.socket() as s:
s.settimeout(5)
s.connect((self.target_address['host'], self.target_address['port']))
msg = f"Hello from A @ {time.ctime()}".encode()
s.sendall(msg)
print(f"[A] 消息已发送至 {self.target_address}")
except Exception as e:
print(f"[A] 发送失败: {str(e)}")
def _start_sender(self):
"""启动定时发送线程"""
def sender():
while True:
self._send_message()
time.sleep(10)
threading.Thread(target=sender, daemon=True).start()
def run(self):
try:
while True: time.sleep(1)
except KeyboardInterrupt:
self._unregister()
self.zk.stop()
if __name__ == "__main__":
# 使用多个ZK地址(示例)
service = ProgramA(zk_hosts="192.168.31.140:2181,192.168.31.141:2181,192.168.31.142:2181")
service.run()
这里我们先启动服务a,这个时候服务b还未启动。
[root@localhost ~]# python a.py
[A] 持久节点已注册: /services/a
[A] B服务不可用
[A] 当前无可用B服务地址
[A] B服务不可用
[A] 发现B服务地址: {'host': '0.0.0.0', 'port': 9999, 'timestamp': 1745683444.3150668}
[A] 消息已发送至 {'host': '0.0.0.0', 'port': 9999, 'timestamp': 1745683444.3150668}
服务b
import json
import socket
import time
import threading
from kazoo.client import KazooClient
class ProgramB:
def __init__(self, zk_hosts, host, port):
# ZK 连接配置
self.zk = KazooClient(hosts=zk_hosts)
self.zk.start()
# 服务元数据
self.service_name = "b"
self.registration_path = f"/services/{self.service_name}"
self.host = host
self.port = port
# 初始化流程
self._register()
self._start_server()
def _register(self):
"""注册带地址的持久节点"""
data = json.dumps({
"host": self.host,
"port": self.port,
"timestamp": time.time()
}).encode()
self.zk.ensure_path(self.registration_path)
self.zk.set(self.registration_path, data)
print(f"[B] 服务地址已注册: {self.registration_path}")
def _unregister(self):
"""下线时删除节点"""
try:
self.zk.delete(self.registration_path)
print(f"[B] 节点已注销: {self.registration_path}")
except Exception as e:
print(f"[B] 注销失败: {str(e)}")
def _handle_connection(self, conn):
"""处理消息连接"""
with conn:
try:
while True:
data = conn.recv(1024)
if not data: break
print(f"[B] 收到消息: {data.decode()}")
except ConnectionResetError:
print("[B] 客户端异常断开")
def _start_server(self):
"""启动TCP服务"""
self.server = socket.socket()
self.server.bind((self.host, self.port))
self.server.listen(5)
print(f"[B] 服务启动于 {self.host}:{self.port}")
def accept_loop():
while True:
conn, addr = self.server.accept()
print(f"[B] 新连接来自 {addr}")
threading.Thread(target=self._handle_connection, args=(conn,)).start()
threading.Thread(target=accept_loop, daemon=True).start()
def run(self):
try:
while True: time.sleep(1)
except KeyboardInterrupt:
self._unregister()
self.zk.stop()
self.server.close()
if __name__ == "__main__":
# 使用多ZK地址 + 指定监听端口
service = ProgramB(
zk_hosts="192.168.31.140:2181,192.168.31.141:2181,192.168.31.142:2181",
host="0.0.0.0",
port=9999
)
[root@localhost ~]# python b.py
[B] 服务地址已注册: /services/b
[B] 服务启动于 0.0.0.0:9999
[B] 新连接来自 ('127.0.0.1', 45162)
[B] 收到消息: Hello from A @ Sun Apr 27 00:04:09 2025
[B] 新连接来自 ('127.0.0.1', 33904)
[B] 收到消息: Hello from A @ Sun Apr 27 00:04:19 2025
[B] 新连接来自 ('127.0.0.1', 38686)
[B] 收到消息: Hello from A @ Sun Apr 27 00:04:29 2025
这样我们实现把ZooKeeper作为注册中心的作用,a和b这两个服务,并不需要知道对方的地址就可以实现通信。
如果我这个时候关闭ZooKeeper,那么a和b还可以正常通信,当然这个需要要看代码是如何设计的。
下图是我关闭ZooKeeper的服务器的情况下a和b还是可以正常通信的,但是如果这个时候被调用的服务b也宕机,由于a知道原理的b已经不可用,而注册中心也宕机,则业务才会出现中断。
运维小路
一个不会开发的运维!一个要学开发的运维!一个学不会开发的运维!欢迎大家骚扰的运维!
关注微信公众号《运维小路》获取更多内容。