从上表中我们可以看到, Meinheld WSGI 的速度堪比 NoneJS 和 Go。虽然其包含了固有的阻塞设计,但是它的速度依旧远超前四个框架,这四个框架都是 Python 异步解决方案。所以,不要相信所谓的异步系统一定快的谬论。尽管它们是并发执行的,但是除了并发还有很多其他的东西要考虑。
虽然我只是使用了简单的 “Hello world” 应用程序来测试这些微框架,但是这足以清晰展示许多解决方案在服务框架上的开销。
上述测试结果都是基于在圣保罗地区推出的 AWS c4.2Xlarge 实例,该实例有 8 个虚拟 CPU 中心,自带默认的共享租赁、HVM 虚拟化和磁盘存储。 这台机器上的的系统是 Ubuntu 16.04.1 LTS(Xenial Xerus),搭载 Linux 4.4.0–53-generic x86_64 内核。使用的 CPU 是 Xeon® CPU E5–2666 v3 @ 2.90GHz CPU。我使用的是从源代码编译出来的 Python 3.6。
公平起见,其他所有的“竞争者”,包括 Go 语言,都使用单进程模式。 使用 wrk 来加载测试服务, 一个线程, 100 个链接, 每个链接有 24 个同时的请求(pipelined),总计 2400 个平行请求。
HTTP 管道化图示(图片来自维基)
在 Japronto 中, HTTP 管道是非常重要的,因为当它处理请求时,管道是一种优化方法。
大多数服务器以非管道化的方式处理管道式请求。他们也不想着去优化这一点(实际上, Sanic 和 Meinheld 会背地里偷偷删除那些来自管道式客户端的请求,这违反了 HTTP 1.1 协议)。
简单来说,管道是一项技术,依靠它,在同一个 TCP 连接中,客户端不必等待接受上一次的请求响应后再发送下一个请求。为了保持整个通讯过程的完整性,服务器会以与请求相同的顺序返回响应。
具体的优化细节
当客户端使用管道将很多小型的 GET 请求一起发送的时候,在服务器端,这些请求则很有可能会落在同一个 TCP 包中,然后会被一次系统调用同时读入。
在系统调用中,与数据在进程内空间的转移相比,将数据从内核空间转移用户空间是非常耗时的。这也是为什么尽可能少进行系统调用的原因。
当 Japronto 接收数据并成功从中解析多个请求后,它会尝试尽快执行所有请求,以正确的顺序粘合响应,然后使用一次系统调用进行回写。实际上,如果使用了 scatter/gather IO 系统调用,则可以在粘合过程中使用内核进行加速,但是 Japronto 暂时还没使用这项系统调用。
请注意,上述的快速处理流程可能不总会被实现,因为有一些请求特别长,等待它们会无端增加很多不必要的延时。
当具体实现处理时务必要格外的小心,你需要考虑系统调用的时间与接受完整请求的时间。
Japronta 使用插值法,从连续分组数据中计算第 50 位百分比,给出中值为 1,214,440 RPS
除了对管道客户端请求的延时写入外,代码还采用了其他几项技术:
Japronto 几乎是由 C 语言完成的,其分析器,协议,连接处理器,路由,请求以及响应都被写为了 C 扩展代码。
除非明确需要,Japronto 极力推迟创建与之内部结构对应的 Python 对象。例如,直到需要显示时,首部字段字典才会被创建。所有的字段边界会被提前标记,但是只有当第一次调用的时候,首部键值的标准化与字符串对象的创建才会真正完成。
Japronto 使用由 C 语言编写的且性能卓越的 picohttpparser 解析器来解析状态码,首部字段以及分块的 HTTP 报文实体。 picohttpparser 使用现代 CPU 的文本处理指令和 SSE4.2 扩展来快速的实现 HTTP 字段边缘的匹配。I/O 操作则由性能超好的 uvloop 实现,它本身就是 libuv 的一个封装。在底层,这提供了读写就绪异步通知的 epoll 系统调用的桥梁,
Picohttpparser 基于 SSE4.2 and CMPESTRI x86_64 指令进行解析
Python 是一种自带垃圾收集器的语言,所以在设计高性能系统的时候要格外小心,避免为垃圾收集增加不必要的负担。在 Japronto 的内部实现中尽量避免循环引用的出现,并尽可能少的进行内存的分配与回收。它会在所谓的“竞技场”中预分配一些对象。如果这些对象不再被引用时,它会尽量去重新使用这些对象而不是将其删除。
所有分配的内存都是 4KB 的整数倍。内部结构都被精心布局以便那些经常被使用的数据会在内存中相互聚集,从而最大限度减少缓存丢失的可能性。
Japronto 尽量不去进行缓冲区之间复制,并且就地执行很多操作。例如:它会在进行路由匹配之前先对路径进行百分比解码操作。
开源 互相帮助
过去的三个月我一直致力于 Japronto 的开发工作,经常周末、周内连轴转。为此我放下了日常的编程工作,全身心的投入到 Japronto 这个项目中。
现在我觉得是时候来把我的工作成果分享在社区上了。
如今的 Japronto 实现了一个非常可靠的功能集:
1. HTTP 1.x 实现,并支持分块上传
2. 全面支持 HTTP 管道
3. 可配置的持久连接
4. 支持异步与同步
5. 基于 forking 的 Master-multiworker 模型
6. 支持热修复
7. 简单路由
接下来想要实现异步 Websockets 和 HTTP 响应流化。
此外,如果您的公司正在寻找一位从事 DevOps 相关工作的狂热 Python 开发人员。我非常乐意毛遂自荐。我将会考虑全球范围内的工作地点。
结束语
本文中所提到的技术并不仅仅局限于 Python。 这些技术也可以使用其他语言来实现,例如: Ruby, Java 甚至是 PHP。 我也很有兴趣去实现相关工作,但是除非有人赞助, 我才会去做。