用 python 实现每秒处理 120 万次 http 请求-mile米乐体育
用 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 的微框架(蓝色)、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
我们可以看到其实 meinheld wsgi 服务器已经和 nodejs 和 go 的性能差不多了。尽管它用的是阻塞式设计,但还是要比前面那四个要快的多,前面四个用的是异步的 python mile米乐体育的解决方案。所以,不要轻易相信别人那些关于异步系统总是比同步系统更快的说法,虽然都是并发处理的问题,但事实远不如想象的那么简单。
虽然我只是用 “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 次请求)。
http 流水线(图片来自 wikipedia)
http 流水线在这里起着决定性的因素,因为 japronto 用它来做执行并发请求的优化。
大多数服务器把来自客户端的流水线和非流水线请求都一视同仁,用同样的方法处理,并没有做针对性的优化。(实际上 sanic 和 meinheld 也是默默的把流水线请求当做非流水线来处理,这违反了 http 1.1 协议)
简单来说,通过流水线技术,客户端不用等到服务器端返回,就可以在同一条 tcp 链接上继续发送后续的请求。为了保障通讯的完整性,服务器端会按照请求的顺序逐个把结果返回给客户端。
细节优化过程
当一堆小的 get 请求被客户端以流水线打包发送过来,服务器端很可能只需要一次系统调用,读取一个 tcp 数据包就能拿到全部的请求。
系统调用,以及在内核空间到用户空间之间移动数据,相比起在进程内部移动数据,成本要高的多。这就是为什么不到万不得已,要尽可能少做系统调用的次数。
当 japronto 收到数据并成功解析出请求序列时,它会尝试尽可能快的把这些请求执行完成,并以正确的顺序合并所有结果,然后只执行一次系统调用发送数据给客户端。实际上因为有 scatter/gather io 这样的系统调用,合并的工作并不需要自己去完成,只不过 japronto 暂时还没有用到这些功能。
然而事情并不总是那么完美,有时候请求需要耗费很长时间去处理,等待完成的过程增加了不必要的延迟。
当我们做优化时,有必要考虑系统调用的成本和请求的预期完成时间。
经过优化 japronto 拿到了 1,214,440 rps 的成绩
除了利用客户端流水线请求,和优化调用,还有一些其它可用的技术。
japronto 几乎都是用 c 写的。包含解析器、协议、链接管理、路由、请求、应答等对象都是用 c 扩展写的。
japronto 力图做到 python 的懒加载,比如,协议头的字典只有在被试图请求到时才会被创建,另外一系列的对象也只有在第一次使用时才会被创建。
japronto 使用超牛逼的 picohttpparser c 库来解析状态、协议头以及分片的 http 消息体。picohttpparser 是直接调用现代 cpu 集成的 sse4.2 扩展文本处理指令去快速匹配 http 标记的边界(那些 10 年前的老 x86_64 cpu 都有这玩意儿)。i/o 用到了超棒的 uvloop,它是一个 libuv 的封装,在最底层,它是调用 epoll 来提供异步读写通知。
picohttpparser 依赖 sse4.2 和 cmpestri x86_64 的特性做解析
python 是有垃圾收集功能的语言,为避免不必要的增加垃圾收集器的压力,在设计高性能系统时一定要多加注意。japronto 的内部被设计的尝试避免循环引用和尽可能少的分配、释放内存,它会预先申请一块区域来存放对象各种,同时尝试在后续请求中重用那些没有被继续引用的 python 的对象,而不是将那些对象直接扔掉。
这些预先申请的内存的大小被固定为 4kb 的倍数。内部结构会非常小心和频繁的使用这些连续的内存区域,以减少缓存失效的可能性。
japronto 会尽可能避免不必要的缓存间复制,只在正确的位置执行操作。比如,在处理路由时,先做 url 解码再进行路由匹配。
开源贡献者们,我需要你们的帮助
我已经连续不断的开发 japronto 超过三个月,不光在每一个工作日,周末也无休。除了每天的工作外,我把所有时间精力都投入到这个项目上了。
我想是时候和社区分享我的劳动果实了。
japronto 已经可靠的实现了下面这些功能:
- 实现 http 1.x 并且支持分片上传
- 完整支持 http 流水线
- 可配置是否让链接 keep-alive
- 支持同步和异步视图
- master-multiworker 多任务处理
- 代码热加载
- 简单易用的路由规则
下一次,我将深入研究关于 websockets 和 http 异步应答数据流。
写文档和做测试还有许多工作要做,如果你有兴趣加入我,请在 twitter 上直接联系我. 这里是 japronto 的 github 项目仓库.
同时,如果你的公司正在寻找熟悉性能优化和 devops 的 python 工程师,我很乐意为你效劳,在全球任何地方都可以。
结束语
上面提到的所有技术不只适用于 python,也同样可以被应用到其它语言,如 ruby、javascript,甚至 php 等。我非常感兴趣去付诸实践,但是,除非有人能在这事上投入资金支持,恐怕我没有足够的精力去完成。
在此我要感谢 python 社区为优化性能所付出的持续投入。尤其是 victor stinner @victorstinner、inada naoki @methane 和 yury selivanov @1st1 以及整个 pypy 团队。
献给我挚爱的 python。