RPC学习笔记

本文深入探讨RPC(远程过程调用),包括其设计原则、半包问题、服务发现技术如Zookeeper和Etcd。通过Python的asyncore库实践异步模型,并介绍Redis的RESP协议。此外,文章还讨论了protobuf在RPC中的应用,以及如何构建一个包含服务发现、子进程管理和信号处理的分布式RPC架构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

rpc学习
  1. 传统意义的RPC为长连接调用,HTTP也可以理解为一种RPC只是为短连接。HTTP1.1引入KeepAlive可以保持HTTP连接长时间不断开。google gRPC建立在http2.0的基础上。
  2. tcp流分割方法:特殊字符结尾发,长度标记法
  3. RPC设计的原则:
  • 消息边界的确定
  • 消息的结构
  • 消息压缩
  • 流量的优化
  1. 半包问题: 因为读是非阻塞的,意味着当我们想要读取 100 个字节时,我们可能经历了多次 read 调用,第一次读了 10 个字节,第二次读了 30 个字节,然后又读了 80 个字节。凑够了 100 个字节时,我们就可以解码出一个完整的请求对象进行处理了,还剩余的 20 个字节又是后面请求消息的一部分。这就是所谓的半包问题。
  2. 服务发现技术:zookeeper和etcd等
  • 服务注册——服务节点在启动时将自己的服务地址注册到中间节点
  • 服务查找——客户端启动时去中间节点查询服务地址列表
  • 服务变更通知——客户端在中间节点上订阅依赖服务列表的变更事件。当依赖的服务列表变更时,中间节点负责将变更信息实时通知给客户端。
  1. 信号处理: SIGKILL信号比较暴力,对方进程会立即crash,可以通过SIGTERM和SIGINT信号通知对方退出,并设置相应的信号处理函数。ctrl + c会发送SIGINT信号,该信号默认的处理函数就是退出进程。
import time
import signal


def ignore(sig, frame):  # 啥也不干,就忽略信号
    pass

signal.signal(signal.SIGINT, ignore)

while True:
    print "hello"
    time.sleep(1)

你试试狂按 ctrl+c,进程依旧打转,只是这 hello 输出的要比平时快一点,似乎不再受到 sleep 的影响。

为什么呢?因为 sleep 函数总是要被 SIGINT 信号打断的,不管你有没有设置信号处理函数,只不过因为有 while True 循环在保护着。

分布式RPC架构如下:

image
主要完成的功能如下:

  • 实现出一个 PreForking 异步模型的单机 RPC 服务器;
  • 然后将服务挂接到 ZooKeeper 的树节点上;
  • 再编写客户端消费者从 ZooKeeper 中读取服务节点地址,连接 RPC 服务器进行交互;
  • 同时还要监听 ZooKeeper 树节点的变更,在 RPC 服务器节点变动时能动态调整服务列表地址。
  • 单机服务器的内容会比之前要复杂一些,因为要考虑周全,对子进程进行管理,要处理信号监听和子进程收割等,这也是对上节理论内容的实战开发应用。

python asyncore库

Python 内置的异步 IO 库。考虑到编写原生的事件轮询和异步读写的逻辑比较复杂,要考虑的细节非常多。所以 Python 对这一块的逻辑代码做了一层封装,简化了异步逻辑的处理,使用起来非常方便。asyncore负责socket事件轮询,用户编写代码时只需要提供回调方法即可,asyncore会在相应的事件到来时,调用用户提供的回调方法。比如当serversocket的read事件到来时,会自动调用handle_accept方法, 当socket的read事件到来时,调用handle_read方法。

# coding: utf8

import json
import struct
import socket
import asyncore
from cStringIO import StringIO


class RPCHandler(asyncore.dispatcher_with_send):  # 客户套接字处理器必须继承 dispatcher_with_send

    def __init__(self, sock, addr):
        asyncore.dispatcher_with_send.__init__(self, sock=sock)
        self.addr = addr
        self.handlers = {
            "ping": self.ping
        }
        self.rbuf = StringIO()  # 读缓冲区由用户代码维护,写缓冲区由 asyncore 内部提供

    def handle_connect(self):  # 新的连接被 accept 后回调方法
        print self.addr, 'comes'

    def handle_close(self):  # 连接关闭之前回调方法
        print self.addr, 'bye'
        self.close()

    def handle_read(self):  # 有读事件到来时回调方法
        while True:
            content = self.recv(1024)
            if content:
                self.rbuf.write(content)
            if len(content) < 1024:
                break
        self.handle_rpc()

    def handle_rpc(self):  # 将读到的消息解包并处理
        while True:  # 可能一次性收到了多个请求消息,所以需要循环处理
            self.rbuf.seek(0)
            length_prefix = self.rbuf.read(4)
            if len(length_prefix) < 4:  # 不足一个消息
                break
            length, = struct.unpack("I", length_prefix)
            body = self.rbuf.read(length)
            if len(body) < length:  # 不足一个消息
                break
            request = json.loads(body)
            in_ = request['in']
            params = request['params']
            print in_, params
            handler = self.handlers[in_]
            handler(params)  # 处理消息
            left = self.rbuf.getvalue()[length + 4:]  # 消息处理完了,缓冲区要截断
            self.rbuf = StringIO()
            self.rbuf.write(left)
        self.rbuf.seek(0, 2)  # 将游标挪到文件结尾,以便后续读到的内容直接追加

    def ping(self, params):
        self.send_result("pong", params)

    def send_result(self, out, result):
        response = {"out": out, "result": result}
        body = json.dumps(response)
        length_prefix = struct.pack("I", len(body))
        self.send(length_prefix)  # 写入缓冲区
        self.send(body) # 写入缓冲区


class RPCServer(asyncore.dispatcher):  # 服务器套接字处理器必须继承 dispatcher

    def __init__(self, host, port):
        asyncore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind((host, port))
        self.listen(1)

    def handle_accept(self):
        pair = self.accept()
        if pair is not None:
            sock, addr = pair
            RPCHandler(sock, addr)


if __name__ == '__main__':
    RPCServer("localhost", 8080)
    asyncore.loop()
Redis学习
  1. redis传输协议,redis使用resp协议,它是一种直观的文本协议,优势在于实现异常简单,解析性能极好。redis将传输的结构数据分为5种最小类型,单元结束时统一使用\r\n
  • 单行字符串 以+符号开头
  • 多行字符串 以$符号开头,后跟字符串长度
  • 整数值 以:符号开头,后跟整数的字符串形式
  • 错误消息 以-符号开头
  • 数组 以*号开头,后跟数组的长度
  1. redis设计缺陷, 连接重连
  2. 开发高可用rpc库
  • struct是python内置的二进制解码编码库
protobuf使用
  • protobuf输出使用一系类键值对格式进行传输,值如果有重复就是用列表的形式进行传输
  • key通过tag和type组成,tag表示字段名称,type表示字段类型
    pb例子
message Person {
    required string user_name        = 1;  // 必须字段
    optional int64  favourite_number = 2;  // 可选字段
    repeated string interests        = 3;  // 列表类型
}
编译成js后的格式如下
var person = new Person{
    user_name: "Martin",
    favourite_number: 1337,
    interests: ["daydreaming", "hacking"]
}

最后的协议如下图所示
image

RPC课程例子 上接RPC架构

继上节的异步 prefork 服务器,下面的代码增加了服务发现、子进程收割、信号处理功能。异步多进程服务共享同样的监听地址,所以只需要父进程注册服务即可。

  • 父进程需要设置 SIGCHLD 信号处理函数收割意外退出的子进程,避免僵尸进程。
  • 父进程需要在进程退出之前杀死所有子进程并收割之。
  • 父进程需要在退出时关闭 zk 会话,立即释放临时节点。
  • 父进程需要考虑 waitpid 被其它信号处理函数打断时进行重试。
  • 父进程在杀死子进程时有可能遇到子进程已经提前死掉了,这时会爆出异常需要进行捕获。
import os
import sys
import math
import json
import errno
import struct
import signal
import socket
import asyncore
from cStringIO import StringIO
from kazoo.client import KazooClient


class RPCHandler(asyncore.dispatcher_with_send):

    def __init__(self, sock, addr):
        asyncore.dispatcher_with_send.__init__(self, sock=sock)
        self.addr = addr
        self.handlers = {
            "ping": self.ping,
            "pi": self.pi
        }
        self.rbuf = StringIO()

    def handle_connect(self):
        print self.addr, 'comes'

    def handle_close(self):
        print self.addr, 'bye'
        self.close()

    def handle_read(self):
        while True:
            content = self.recv(1024)
            if content:
                self.rbuf.write(content)
            if len(content) < 1024:
                break
        self.handle_rpc()

    def handle_rpc(self):
        while True:
            self.rbuf.seek(0)
            length_prefix = self.rbuf.read(4)
            if len(length_prefix) < 4:
                break
            length, = struct.unpack("I", length_prefix)
            body = self.rbuf.read(length)
            if len(body) < length:
                break
            request = json.loads(body)
            in_ = request['in']
            params = request['params']
            print os.getpid(), in_, params
            handler = self.handlers[in_]
            handler(params)
            left = self.rbuf.getvalue()[length + 4:]
            self.rbuf = StringIO()
            self.rbuf.write(left)
        self.rbuf.seek(0, 2)

    def ping(self, params):
        self.send_result("pong", params)

    def pi(self, n):
        s = 0.0
        for i in range(n+1):
            s += 1.0/(2*i+1)/(2*i+1)
        result = math.sqrt(8*s)
        self.send_result("pi_r", result)

   def send_result(self, out, result):
        response = {"out": out, "result": result}
        body = json.dumps(response)
        length_prefix = struct.pack("I", len(body))
        self.send(length_prefix)
        self.send(body)


class RPCServer(asyncore.dispatcher):

    zk_root = "/demo"
    zk_rpc = zk_root + "/rpc"

    def __init__(self, host, port):
        asyncore.dispatcher.__init__(self)
        self.host = host
        self.port = port
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind((host, port))
        self.listen(1)
        self.child_pids = []
        if self.prefork(10):  # 产生子进程
            self.register_zk()  # 注册服务
            self.register_parent_signal()  # 父进程善后处理
        else:
            self.register_child_signal()  # 子进程善后处理

   def prefork(self, n):
        for i in range(n):
            pid = os.fork()
            if pid < 0:  # fork error
                raise
            if pid > 0:  # parent process
                self.child_pids.append(pid)
                continue
            if pid == 0:
                return False  # child process
        return True

    def register_zk(self):
        self.zk = KazooClient(hosts='127.0.0.1:2181')
        self.zk.start()
        self.zk.ensure_path(self.zk_root)  # 创建根节点
        value = json.dumps({"host": self.host, "port": self.port})
        # 创建服务子节点
        self.zk.create(self.zk_rpc, value, ephemeral=True, sequence=True)
        
    def exit_parent(self, sig, frame):
        self.zk.stop()  # 关闭 zk 客户端
        self.close()  # 关闭 serversocket
        asyncore.close_all()  # 关闭所有 clientsocket
        pids = []
        for pid in self.child_pids:
            print 'before kill'
            try:
                os.kill(pid, signal.SIGINT)  # 关闭子进程
                pids.append(pid)
            except OSError, ex:
                if ex.args[0] == errno.ECHILD:  # 目标子进程已经提前挂了
                    continue
                raise ex
            print 'after kill', pid
        for pid in pids:
            while True:
                try:
                    os.waitpid(pid, 0)  # 收割目标子进程
                    break
                except OSError, ex:
                    if ex.args[0] == errno.ECHILD:  # 子进程已经割过了
                        break
                    if ex.args[0] != errno.EINTR:
                        raise ex  # 被其它信号打断了,要重试
            print 'wait over', pid
            
    def reap_child(self, sig, frame):
        print 'before reap'
        while True:
            try:
                info = os.waitpid(-1, os.WNOHANG)  # 收割任意子进程
                break
            except OSError, ex:
                if ex.args[0] == errno.ECHILD:
                    return  # 没有子进程可以收割
                if ex.args[0] != errno.EINTR:
                    raise ex  # 被其它信号打断要重试
        pid = info[0]
        try:
            self.child_pids.remove(pid)
        except ValueError:
            pass
        print 'after reap', pid

    def register_parent_signal(self):
        signal.signal(signal.SIGINT, self.exit_parent)
        signal.signal(signal.SIGTERM, self.exit_parent)
        signal.signal(signal.SIGCHLD, self.reap_child)  # 监听子进程退出

    def exit_child(self, sig, frame):
        self.close()  # 关闭 serversocket
        asyncore.close_all()  # 关闭所有 clientsocket
        print 'all closed'

   def register_child_signal(self):
        signal.signal(signal.SIGINT, self.exit_child)
        signal.signal(signal.SIGTERM, self.exit_child)

    def handle_accept(self):
        pair = self.accept()  # 接收新连接
        if pair is not None:
            sock, addr = pair
            RPCHandler(sock, addr)


if __name__ == '__main__':
    host = sys.argv[1]
    port = int(sys.argv[2])
    RPCServer(host, port)
    asyncore.loop()  # 启动事件循环

客户端代码如下:
我们给上节的客户端代码增加了获取服务列表功能,并持续监听服务列表变更,然后循环随机选出一个可用服务器发送 ping 指令和 pi 指令输出服务器反馈。当服务列表变更时,我们需要将新的服务列表和内存中现有的服务列表进行比对,创建新的连接,关闭旧的连接。

import json
import time
import struct
import socket
import random
from kazoo.client import KazooClient

zk_root = "/demo"

G = {"servers": None}  # 全局变量,RemoteServer 对象列表


class RemoteServer(object):  # 封装 rpc 套接字对象

    def __init__(self, addr):
        self.addr = addr
        self._socket = None

    @property
    def socket(self):  # 懒惰连接
        if not self._socket:
            self.connect()
        return self._socket

    def ping(self, twitter):
        return self.rpc("ping", twitter)

    def pi(self, n):
        return self.rpc("pi", n)

    def rpc(self, in_, params):
        sock = self.socket
        request = json.dumps({"in": in_, "params": params})
        length_prefix = struct.pack("I", len(request))
        sock.send(length_prefix)
        sock.sendall(request)
        length_prefix = sock.recv(4)
        length, = struct.unpack("I", length_prefix)
        body = sock.recv(length)
        response = json.loads(body)
        return response["out"], response["result"]
    
def get_servers():
    zk = KazooClient(hosts="127.0.0.1:2181")
    zk.start()
    current_addrs = set()  # 当前活跃地址列表

    def watch_servers(*args):  # 闭包函数
        new_addrs = set()
        # 获取新的服务地址列表,并持续监听服务列表变动
        for child in zk.get_children(zk_root, watch=watch_servers):
            node = zk.get(zk_root + "/" + child)
            addr = json.loads(node[0])
            new_addrs.add("%s:%d" % (addr["host"], addr["port"]))
        # 新增的地址
        add_addrs = new_addrs - current_addrs
        # 删除的地址
        del_addrs = current_addrs - new_addrs
        del_servers = []
        # 先找出所有的待删除 server 对象
        for addr in del_addrs:
            for s in G["servers"]:
                if s.addr == addr:
                    del_servers.append(s)
                    break
        # 依次删除每个 server
        for server in del_servers:
            G["servers"].remove(server)
            current_addrs.remove(server.addr)
        # 新增 server
        for addr in add_addrs:
            G["servers"].append(RemoteServer(addr))
            current_addrs.add(addr)

    # 首次获取节点列表并持续监听服务列表变更
    for child in zk.get_children(zk_root, watch=watch_servers):
        node = zk.get(zk_root + "/" + child)
        addr = json.loads(node[0])
        current_addrs.add("%s:%d" % (addr["host"], addr["port"]))
    G["servers"] = [RemoteServer(s) for s in current_addrs]
    return G["servers"]


def random_server():  # 随机获取一个服务节点
    if G["servers"] is None:
        get_servers()  # 首次初始化服务列表
    if not G["servers"]:
        return
    return random.choice(G["servers"])


if __name__ == '__main__':
    for i in range(100):
        server = random_server()
        if not server:
            break  # 如果没有节点存活,就退出
        time.sleep(0.5)
        try:
            out, result = server.ping("ireader %d" % i)
            print server.addr, out, result
        except Exception, ex:
            server.close()  # 遇到错误,关闭连接
            print ex
        server = random_server()
        if not server:
            break  # 如果没有节点存活,就退出
        time.sleep(0.5)
        try:
            out, result = server.pi(i)
            print server.addr, out, result
        except Exception, ex:
            server.close()  # 遇到错误,关闭连接
            print ex
后续推荐文章
  • 《TCP Sockets 编程》
  • 《UNIX 环境高级编程》
  • 《Redis 设计与实现》
  • 《Python 源码剖析》
  • EINTR 的正确使用方式
  • kazoo api
  • Python 多进程编程基础——图文版
  • Python 进程间通信原理与实战——图文版
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值