从小白到架构师 基于 sanic 打造 python web 框架

易寒 · 2019年03月06日 · 1880 次阅读

0x00 Why

为何做这件事,在去年的一个项目中,算法同学要使用在线模型训练,不得不使用 python 的 tf 框架,这样我们不得不是 python web 框架,当时因为团队里面没人懂 python 相关的知识,只是简单的用 tornado 搭建的一个。但是在后期使用过程中,压测发现了 tornado 在低耗时的接口不够稳定,波动比较大。我们的接口一般 20ms,但是经常波动到 40ms,并发其实也不大。所以经历了痛苦过后,为了满足后续类似的场景,我们需要打造一款内部使用的 python web 框架,能够支持低耗时接口的稳定性,以及稍微性能强点。

0x01 How

基于以上的出发点,开始调研 python web 相关性能比较高的框架,看到了 sanic。说是性能比较高。先看下 sanic 作者开发 sanic 的动机,是因为他看下了下面这篇文章:

uvloop: Blazing fast Python networking

是因为 python3.4 推出了 asyncio,解决令人诟病的异步 io 性能问题,3.5 后推出了 uvloop,基于 libuv,libuv 是一个使用 C 语言实现的高性能异步 I/O 库,uvloop 用来代替 asyncio 默认事件循环,可以进一步加快异步 I/O 操作的速度,而 tornado 在 python3 中还没有使用 uvloop。

首先来看下 uvloop 的性能表现:

在这里插入图片描述

0x02 tornado vs sanic

口说无凭,直接压测看数据,我们同时构造两个框架的一个接口,接口里面没有任务逻辑,就是简单的返回一个 helloworld,都是起一个进程,实测的数据

  • sanic:18000/s
  • tornado:5000/s

就框架本身而言,sanic 确实性能高。然后我又对各个耗时阶段的接口进行压测,来判断不同耗时,两个框架的表现。

Sanic

Sleep(ms)\Thread 1 10 50 100
1 1-3 之间波动,波动范围为 0%~300%,周期性的 1-6 之间波动,波动范围为 0%~600%,周期性的 1-11 之间波动,波动范围为 0%~1100%,周期性的 1-7 之间波动,波动范围为 0%~700%,周期性的
10 11-15 之间波动,波动范围为 10%~50%,周期性的 12-17 之间波动,波动范围为 20%~70%,周期性的 11-20 之间波动,波动范围为 10%~100%,周期性的 11-31 之间波动,波动范围为 10%~310%,周期性的
50 51-56,周期性的波动 54-60,周期性 54-71,周期性的 51-61,周期的
100 101-106 之间波动,波动范围为 1%~6%,周期性的 103-110 之间波动,波动范围为 3%~10%,周期性的 103-120 之间波动,波动范围为 3%~20%,周期性的 105-130 之间波动,波动范围为 5%~30%,周期性的

Tornado

Sleep(ms)\Thread 1 10 50 100
1 2-4 之间波动,波动范围为 200%~400%,周期性的 6-11 之间波动,波动范围为 600%~1100%,周期性的 21-56 之间波动,波动范围为 2100%~5600%,周期性的 50-106 之间波动,波动范围为 5000%~10600%,周期性的
10 11-15 之间波动,波动范围为 110%~150%,周期性的 11-18 之间波动,波动范围为 110%~180%,周期性的 28-43 之间波动,波动范围为 280%~430%,周期性的 62-104 之间波动,波动范围为 620%~1040%,周期性的
50 52-57 之间波动,周期性的 54-62,周期性的 51-80,周期性的 53-75,周期性的
100 101-108 之间波动,波动范围为 1%~8%,周期性的 101-111 之间波动,波动范围为 1%~11%,周期性的 101-111 之间波动,波动范围为 1%~11%,周期性的 101-140 之间波动,波动范围为 1%~40%,周期性的

result

sleep 1ms&100 个线程压的情况

Sanic 波动为 1-7ms,而 Tornado 已经最低到 50ms,最高飙到 100 多 ms,显然在 10ms 以内的接口,并发超过 50 的,不适合使用 Tornado,推荐使用 Sanic,Sanic 赢一局,Sanic1:0Tornado

sleep 10ms &100 个线程压的情况

Sanic 波动为 11-31ms,而 Tornado 已经最低到 62 了,最高到 100 多毫秒,显然在 100 毫秒内,并发超过 50 的时候,还是 Sanic 更胜一筹,Sanic 再赢一局,Sanic2:0Tornado

sleep 50ms&100 个线程压的情况

Sanic 波动为 51-61,Tornado 为 53-73,两个框架之间差别不大,建议使用 Sanic,Sanic3:0Tornado。

sleep 100ms & 100 个线程压的情况

Sanic 波动为 105 到 130,Tornado 波动为 101-140,选择谁都可以。这次打个平手Sanic2:0Tornado

综上所述,sanic 性能确实优于 tornado。所以新框架采用 sanic 来开发。

0x04 框架设计

  • log 库:传递 logid 和自我生成 logid,且能支撑多进程,不丢日志的情况。
  • redis 库:拥有连接池,
  • mysql 库:拥有连接池。
  • python 版本库管理要简单,易发布,易部署。
  • 要有利用新框架新建项目,更新框架的工具,不用用户每次自己下载然后 copy,改名。

基于以上的设计,调研了一下用到的库。

sanic

  • python3.7+
  • sanic 最新版

redis

redis-py:官方推荐,配合 hiredis-py 会得到性能的极致提升。

mysql

pymysqlpool:mysql 连接池,包装pymysql提供连接池的功能。
还可以使用 pymysql+dbutils自己编写连接池的代码。

config

配置文件支持 ini 格式的,保持跟 go 的一样。
ConfigParser

官方使用 py 的配置文件,采用该方式更方便。

logging

  • 日志库要有 logid
  • sanic 自带有 log 库,使用的就是 python 自带的基础库 logging。
  • 解决多进程丢日志的问题使用concurrent-log-handler,不要使用ConcurrentLogHandler很久不更新了,bug 多。
  • concurrent-log-handler 只支持日志大小滚动,不支持时间滚动。
  • 目前 python 最吃香的 log 库是loguru,我们也集成了,该库支持多线程,虽然不是进程安全的,但是也有方式支持多进程 log。
  • 需要对比下这两个库在 sanic 下性能,选择一个最优的。
  • logging(logging.handlers.TimedRotatingFileHandler 多进程丢日志)
  • logging(concurrent-log-handler),带了进程锁以后严重影响了框架性能.
  • loguru
  • 对于高 qps 的场景下,可以让其丢日志,使用普通的 TimedRotatingFileHandler。对于 qps 要求低的场景,建议使用 concurrent-log-handler 保证多进程下日志不丢失。

pipenv

  • python 依赖包管理
  • python 项目迁移

click 构建命令行工具

利用click开发。一条命令就能新建一个新的项目,直接可运行。

框架性能

目前框架开发已经完成,内部已经发布了第一个版本,对该框架的性能也进行了一个压测。

sanic 空逻辑性能测试(只返回一个 json 串)

  • 1 个线程压 1 个进程 qps:6000
  • 1 个线程压 2 个进程 qps:6600
  • 2 个线程压 1 个进程 qps:10000
  • 4 个线程压 1 个进程 qps:9600,单进程支持 4 个并发时出现瓶颈。

单进程处理能力比较强,建议优先单进程

  • 2 个线程压 2 个进程 qps:11000
  • 4 个线程压 4 个进程 qps:13000
  • 8 个线程压 8 个进程 qps:14000
  • 16 个线程压 8 个进程 qps:13000,瓶颈出现

sanic 加入 log 测试(打印一条 log 到文件,多进程不丢日志,启用进程安全的 log)

  • 1 个线程压 1 个进程 qps:1500
  • 1 个线程压 2 个进程 qps:1300
  • 2 个线程压 1 个进程 qps:1900
  • 4 个线程压 1 个进程 qps:2100,但是耗时已经上来了。
  • 2 个线程压 2 个进程 qps:2000

  • 4 个线程压 4 个进程 qps:2400,耗时已经增加许多,瓶颈

  • 8 个线程压 8 个进程 qps:2600,耗时增加。

  • 16 个线程压 8 个进程 qps:2800,耗时已经飙升了。

sanic 加入 log 测试(打印一条 log 到文件,多进程丢日志,不启用进程安全的 log)

  • 1 个线程压 1 个进程 qps:3200

  • 1 个线程压 2 个进程 qps:2200

  • 2 个线程压 1 个进程 qps:4800

  • 4 个线程压 1 个进程 qps:4800

  • 2 个线程压 2 个进程 qps:4000

  • 4 个线程压 4 个进程 qps:5200

  • 8 个线程压 8 个进程 qps:4400

sanic 加入 redis 性能测试

  • redis(set)操作,1 个线程压 1 个进程 qps:3700

  • redis(set)操作,1 个线程压 2 个进程 qps:3200,反而降低了。

  • redis(set)操作,2 个线程压 1 个进程 qps:4700

  • redis(set)操作,4 个线程压 1 个进程 qps:4600,单进程出现瓶颈。

  • redis(set)操作,2 个线程压 2 个进程 qps:5500

  • redis(set)操作,4 个线程压 4 个进程 qps:8100

  • redis(set)操作,8 个线程压 8 个进程 qps:8000,下降,瓶颈

  • redis(set)操作,16 个线程压 8 个进程 qps:8300,瓶颈

Redis 稳定性较好,且增加线程或者进程呈线性增长,直到到达瓶颈。且尝试增加服务能力的时候,最好是起和并发量一样的进程数,不然来回切进程反而会降低 qps。

压测结果分析

建议采用单进程,开启普通 log,无包装多进程安全的 log。
最后我们测试了下 4 个线程压一个进程,打印一条 log(普通 log),set 一次 redis 的业务,qps:3200

在不要求日志不丢的,耗时可以放宽的情况下,可以提升进程数来提升性能。

0x05

虽然 sanic 框架性能很好,但是 python 自身的性能,以及 python 库的一些影响,仍然不能达到 go 的性能。但是应付一些中小型项目是可以了。后面持续优化,有机会开源出来。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册