最近用 Python 写了一个 RPC 服务,之前在 Ubuntu 上跑,换到 Windows 上之后突然发现一个神奇的问题。我的目的是在一个循环里定时获取设备状态,然后广播给订阅的客户端。获取设备状态是一个 RPC 接口。大致结构如下:
python
proxy = xmlrpc.client.ServerProxy(f"http://localhost:8080/", allow_none=True)
subscribers = [] # 客户端连接
running = True
async def periodic_pusher():
while running:
await asyncio.sleep(0.05)
if subscribers:
data = proxy.get_data()
payload = json.dumps(data)
# 将 payload 广播给客户端
async def main(port):
asyncio.create_task(periodic_pusher())
async with websockets.serve(handler, "0.0.0.0", port):
await asyncio.Future()
任务很简单,客户端通过 websocket 连接上来,然后订阅数据,这里为了照护客户端的实现者,所以转了一手。
因为配环境还挺麻烦的,而且我的数据并不复杂,所以选了 python 标准库自带的 xmlrpc 作为 rpc 库。换到 Windows 上以后,客户端突然变得异常卡顿,打印日志后发现差不多两秒多才能从服务的收到一个条数据,不管循环里 sleep 的时间调的多小都没用。
开始怀疑是异步任务调度的问题,试了纯 sleep 加 print,还是飕飕的。然后是排查服务端响应,还真有问题。
python
def rpc_get_xxx():
data = None
while data is None:
data = robot.get_xxx()
time.sleep(0.01)
return data
因为获取数据 API 可能会返回 None,所以用了循环来保证一定能获取到数据,data=None 导致一定会进入循环,也就导致 sleep 一定至少会执行一次,带来不必要的延时。知错就改,只要把 data=None 改成 data=robot.get_xxx() 就可以了。
再次测试,但是问题依然存在,rpc 服务端的延时顶多几十毫秒,websocket 客户端那边是一两秒的问题。没别的办法加日志吧,在 rpc 调用前后加上日志,发现一次 rpc 调用就要两秒多,为进一步定位问题,在 rpc 服务端也加上日志。试了几次,结果如下:
before rpc: 1763365566.3031723
before get: 1763365568.3533354
after get: 1763365568.3543353
after rpc: 1763365568.3543353
before rpc: 1763367026.1716492
before get: 1763367028.228031
after get: 1763367028.228031
after rpc: 1763367028.228031
before rpc: 1763367290.863952
before get: 1763367292.9115446
after get: 1763367292.9115446
after rpc: 1763367292.9135447
rpc 客户端发起请求到服务端收到请求居然花了两秒多?这也太离谱了。。。
正当我怀疑 xmlrpc 库到底靠不靠谱的时候,找到看一篇帖子:
真是救命稻草,当我把 rpc 客户端的 ip 地址换成 127.0.0.1 之后,还真就解决了。一次调用几十毫秒就完成了:
before rpc: 1763367422.9848952
before get: 1763367422.9868922
after get: 1763367422.9868922
after rpc: 1763367422.9880338
这才正常嘛,不过这个坑是真坑爹啊。
其实这个问题也问过 AI,它坚持说是 getfqdn 反解 DNS 的问题,还给了一段测试代码:
python
import socket, time
t0 = time.perf_counter()
socket.getfqdn('localhost') # 模拟 xmlrpc 内部调用
print("getfqdn 耗时:", time.perf_counter() - t0)
但是实际上这段代码运行起来非常快:
getfqdn 耗时: 0.012044099974445999
帖子里也说了跳过 getfqdn 并没有用,所以应该不是它的问题。