用 Python 实现每秒处理 120 万次 HTTP 请求

用 Python 做到每秒处理上百万次 HTTP 请求 , 可能吗?也许不能 , 但直到最近 , 这已成为现实 。
很多公司都在为了提升程序的执行性能和降低服务器的运营成本 , 而放弃 Python 去选择其它编程语言 , 其实这样做并不是必须 , 因为 Python 完全可以胜任这些任务 。
Python 社区最近做了大量关于性能的优化 。CPython 3.6 重写了新的字典从而全面提升解析器的执行性能 。由于引入更快的调用规则和字典查询缓存 , CPython 3.7 甚至还要更快 。
我们可以用 PyPy 的 Just-in-Time 来编译复杂的科学计算任务 , NumPy 的测试套件也优化了和 C 扩展的兼容性 , 同时 PyPy 还计划于今年晚些时候做到和 Python 3.5 保持一致 。
这些振奋人心的变化激励着我想要有所创新 , Python 所擅长的领域众多 , 我选择了其中一个:Web 和 MicroServices 开发 。
了解 Japronto!Japronto 是一个全新的 , 为微服务量身打造的微框架 。实现它的主要目标包含够快、可扩展和轻量化 。的确它快的吓人 , 甚至远比 NodeJS 和 Go 还要快的多的多 。要感谢 asyncio , 让我可以同时编写同步和异步代码 。

用 Python 实现每秒处理 120 万次 HTTP 请求

文章插图
 
【用 Python 实现每秒处理 120 万次 HTTP 请求】Python 的微框架(蓝色)、NodeJS 和 Go (绿色) 和 Japronto (紫色)
勘误表:用户 @heppu 提到 , 如果谨慎点用 Go 的 stdlib HTTP 服务器可以写出比上图的 Go 快 12% 的代码 。另外 fasthttp 也是一个非常棒的 Go 服务器 , 同样的测试中它的性能几乎只比 Japronto 低 18% 。真是太棒了!更多细节查可以看 
https://github.com/squeaky-pl/japronto/pull/12 和 
https://github.com/squeaky-pl/japronto/pull/14
用 Python 实现每秒处理 120 万次 HTTP 请求

文章插图
 
我们可以看到其实 Meinheld WSGI 服务器已经和 NodeJS 和 Go 的性能差不多了 。尽管它用的是阻塞式设计 , 但还是要比前面那四个要快的多 , 前面四个用的是异步的 Python 解决方案 。所以 , 不要轻易相信别人那些关于异步系统总是比同步系统更快的说法 , 虽然都是并发处理的问题 , 但事实远不如想象的那么简单 。
虽然我只是用 "Hello World" 来完成上面这个关于微框架的测试 , 但它清晰的展现了各种服务器框架的处理能力 。
这些测试是在一台亚马逊 AWS EC2 的 c4.2xlarge 实例上完成的 , 它有 8 VCPUs , 数据中心选在圣保罗区域 , 共享主机、HVM 虚拟化、普通磁盘 。操作系统是 Ubuntu 16.04.1 LTS (Xenial Xerus) , 内核为 linux 4.4.0–53-generic x86_64 。操作系统显示的 CPU 是 Xeon® E5–2666 v3 @ 2.90GHz 。Python 我用的版本是 3.6 , 刚从源码编译来的 。
公平起见 , 所有程序 , 包括 Go , 都只运行在单个处理器内核上 。测试工具为 wrk , 参数是 1 个线程 , 100 个链接和每个链接 24 个请求(累计并发 2400 次请求) 。
用 Python 实现每秒处理 120 万次 HTTP 请求

文章插图
 
HTTP 流水线(图片来自 Wikipedia)
HTTP 流水线在这里起着决定性的因素 , 因为 Japronto 用它来做执行并发请求的优化 。
大多数服务器把来自客户端的流水线和非流水线请求都一视同仁 , 用同样的方法处理 , 并没有做针对性的优化 。(实际上 Sanic 和 Meinheld 也是默默的把流水线请求当做非流水线来处理 , 这违反了 HTTP 1.1 协议)
简单来说 , 通过流水线技术 , 客户端不用等到服务器端返回 , 就可以在同一条 TCP 链接上继续发送后续的请求 。为了保障通讯的完整性 , 服务器端会按照请求的顺序逐个把结果返回给客户端 。
细节优化过程当一堆小的 GET 请求被客户端以流水线打包发送过来 , 服务器端很可能只需要一次系统调用 , 读取一个 TCP 数据包就能拿到全部的请求 。
系统调用 , 以及在内核空间到用户空间之间移动数据 , 相比起在进程内部移动数据 , 成本要高的多 。这就是为什么不到万不得已 , 要尽可能少做系统调用的次数 。
当 Japronto 收到数据并成功解析出请求序列时 , 它会尝试尽可能快的把这些请求执行完成 , 并以正确的顺序合并所有结果 , 然后只执行一次系统调用发送数据给客户端 。实际上因为有 scatter/gather IO 这样的系统调用 , 合并的工作并不需要自己去完成 , 只不过 Japronto 暂时还没有用到这些功能 。


推荐阅读