rpc学习
- 传统意义的RPC为长连接调用,HTTP也可以理解为一种RPC只是为短连接。HTTP1.1引入KeepAlive可以保持HTTP连接长时间不断开。google gRPC建立在http2.0的基础上。
- tcp流分割方法:特殊字符结尾发,长度标记法
- RPC设计的原则:
- 消息边界的确定
- 消息的结构
- 消息压缩
- 流量的优化
- 半包问题: 因为读是非阻塞的,意味着当我们想要读取 100 个字节时,我们可能经历了多次 read 调用,第一次读了 10 个字节,第二次读了 30 个字节,然后又读了 80 个字节。凑够了 100 个字节时,我们就可以解码出一个完整的请求对象进行处理了,还剩余的 20 个字节又是后面请求消息的一部分。这就是所谓的半包问题。
- 服务发现技术:zookeeper和etcd等
- 服务注册——服务节点在启动时将自己的服务地址注册到中间节点
- 服务查找——客户端启动时去中间节点查询服务地址列表
- 服务变更通知——客户端在中间节点上订阅依赖服务列表的变更事件。当依赖的服务列表变更时,中间节点负责将变更信息实时通知给客户端。
- 信号处理: 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架构如下:
主要完成的功能如下:
- 实现出一个 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学习
- redis传输协议,redis使用resp协议,它是一种直观的文本协议,优势在于实现异常简单,解析性能极好。redis将传输的结构数据分为5种最小类型,单元结束时统一使用\r\n
- 单行字符串 以+符号开头
- 多行字符串 以$符号开头,后跟字符串长度
- 整数值 以:符号开头,后跟整数的字符串形式
- 错误消息 以-符号开头
- 数组 以*号开头,后跟数组的长度
- redis设计缺陷, 连接重连
- 开发高可用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"]
}
最后的协议如下图所示
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 进程间通信原理与实战——图文版