I/O内核设计
所以,我建议重新设计所有IO子系统,即,用C(或其他无GIL的语言)写个库来处理IO,这样IO与程序主线程无关了。它应该与python主线程使用类似消息(messaging)的机制来通信。但是,不能发送Python对象,也不要在I/O线程内持有GIL。 I/O内核要支持多种协议,每个协议应该:(a)处理握手,(b)把流切分成消息,这样完整的消息才会被转发给主线程。如果可以设计连接细节,比如自动故障切换(automatic failover)的主从关系(master/slave relations),就更好了。 I/O线程应该可以解析名字,处理连接请求,能够订阅DNS名字变化,以及其他的一些高级特性。 注意,这些不仅仅对python有用,也适用于其他有GIL的脚本语言。事实上,对无GIL的语言,也能很好地工作,但可能没那个必要。 先前的做法这个思路部分存在于很多产品中:
也许还有更多的例子,我仍然没有看到用单独线程来创建统一的I/O内核的尝试。如果你知道,告诉我。 这种模式和最近出现的Ambassador模式很像。Ambassador是个进程,存在于每台机器,进行服务发现,但是通过自身代理所有连接,即,所有服务都连接到localhost上Ambassador监听的端口,然后Ambassador把连接转发到真正的服务上去。类似的,I/O内核也应该代替主线程进行服务发现、与服务进行通信(协议仍然与Ambassador使用的那个有很大不同)。 意义所在难道是为了性能上能提升几毫秒? 对。事实上,当使用多个服务来处理单个前端请求时,毫秒级的延迟累积地相当快。而且,这种技术可以在CPU使用率接近100%时,能挽救非线性增长的延迟。 还是为了保持持久连接?如果你足够小心地经常放弃CPU,它们在传统的异步I/O上也工作得很好。 对。如果你在用异步I/O,那你已经非常出色了,因为很多人根本没有看到这种必要。但是服务发现需要多少异步库才算合理?(我的回答是:一个都不需要) 但用异步I/O也能进行服务发现。 当然。但是没人这样做。我认为应该趁机也解决这个问题。 下面是我设想的I/O内核应该做的任务: 检测和统计对CPU密集型的任务来说,定期发送统计会比较困难。这个需要被修复。主线程应该递增在某个内存区域的计数器,而完全与I/O线程无关。 而且我们获取请求-响应计时器的精确时间戳。通常它们对python主循环的工作量评估严重不准。 在正确的服务发现帮助下,我们甚至可以在真正的用户试图在此worker上执行请求前,知道哪些必要的服务不可用。 调试假设你可以让Python在任何时间获取状态。首先,我们总会有一批在处理中的请求。而且,我们可以像统计一样,贴上一些标记点。最后,我们可以使用类似错误处理器(faulthandler)所用的一种方法来找出主线程的栈。 关键在于,有个线程可以回应调试请求,甚至是当主线程在做CPU密集型的事情,或者因为某些原因挂起时。 管道请求应该尽可能通过管道传输,即,不管哪个前端请求需要数据库请求,我们通过一个数据库连接发送全部。 这样,db连接数就可以很少,而且允许我们统计哪份副本较慢。 名字发现我们不仅要解析DNS名字(不管我们选择的是什么名字解析方案),还要当名字变化时获取更新,比如zookeeper中的设置watch。 这个过程必须对应用透明,并且在应用开始请求前,连接已经存在。 统一既然I/O内核就位,各个python的I/O框架只需要支持内核支持的协议。所有的新协议应该在内核里完成。这促使框架在方便上和效率上竞争,而不是协议的支持上。 节流(Throttling)即使在Java和Go这些可以自由使用线程的语言里,也需要控制客户端的连接数。该设计,不管到底哪个库才是网络请求的真正执行者,允许控制应用中单个位置处的请求数目。 设计随想下面是关于设计I/O内核的一些随想(没有顺序),有些可能在最终设计时会被去掉。
结束当然建立这样的工具不是一个周末就可以完成的事情。这是一份艰苦的工作,而且是无限长的旅程。 直到现在也适合重新思考为什么我们使用Python操控网络。最近的工具比如稳定的libuv和Rust语言可以极大的简化建立I/O内核。当然可以明智的使用go-python来原型化(prototype)代码,但这容易做但是不是长久之计。 论及的方案,使用起来很简洁。期望在未来我们能使用python建立高效,高性能的服务,尤其是在动态网络配置方面,从而在性能差异明显的问题上不需要使用其他语言重写所有内容。 |