ZooKeeper-案例(注册中心)

38 阅读4分钟

作者介绍:简历上没有一个精通的运维工程师。请点击上方的蓝色《运维小路》关注我,下面的思维导图也是预计更新的内容和当前进度(不定时更新)。

前面我们介绍介绍了几个常用的代理服务器,本章节我们讲来讲解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已经不可用,而注册中心也宕机,则业务才会出现中断。

运维小路

一个不会开发的运维!一个要学开发的运维!一个学不会开发的运维!欢迎大家骚扰的运维!

关注微信公众号《运维小路》获取更多内容。