序言:
笔者要在线上服务器 load 日志并且重放来测一些机器性能指标. 模拟机器资源比较少, 相对的被模拟的线上机器日志量大, 假设线上单机 qps 有 1w, 那么 5 台机器组成的集群 5w 个 qps. 模拟机器压测客户端需要比 5w 个 qps 更快, 才有比较意义.
第一章: HTTP 初体验
正所谓 "人生苦短, 我用 python",python 自带了 urllib2,urllib3 以及第三方的 request. 支持的代理访问, 添加请求头基本满足功能需求. 笔者用 urllib2+multiprocessing 库顺利了码完代码运行之, 查看 qps 只有 2k 多, 这显然远远低于需求. 在加大进程数到 cpu 核数的数倍之多, 也发现 python 仅能达到 3k 多. 事出必有因, 于是笔者便通过监控界面和 shell 小工具来找机器各种茬.
第二章:"中世纪黑暗期"
中世纪是黑暗漫长的时期, 你做了很多事情, 但却输出很少, 留下来的是尝试后的经验总结. 从 cpu, 内存, 硬盘, 网络各方面数据看. cpu 使用率 90% 多, 内存用满, 硬盘 wa 很低, 网络千兆网卡满载. 最首先的是把千兆网卡机器替换成万兆网卡机器. 查看 timewait 的连接数达到 1w3 多. 那就先优化下看起来是 "瓶颈" 的东西. 配置 tcp_timestamps=1, tcp_tw_reuse=1, tpc_tw_recycle=1.sysctl -p 生效下最新的配置, timewait 连接数没下去, 并发数没上来. 既然硬件该做的设置都完了, 那为什么别人家的露娜那么秀, 我家的就是一坨屎呢.
再回过头来考虑程序架构问题. 反省自己, 首先 urllib2,request 库是网络 io 阻塞的, 其次网络是短连接的, 再次这么多进程切换系统开销也很大. 在广袤的互联网海洋中遨游了一番, 得出的结论就是 grequest 库可能是个解决办法. gevent 是个协程库, 它使用 greenlet 库提供的基于 libev 实现的高性能异步网络框架. Perfect! 看起来是那么的完美. 于是又尝试重写了程序. 可是性能还是没有上去. 那到底是不是 python 语言自身的限制问题, 导致 cpu 高居不下, 并发量又上不去呢? 这里留个疑问, 到文章的最后再来回答这个问题.
第三章: 豁朗开朗
不甘心并且不再纠结于 python, 用当下网红 golang 重写下. golang 的协程库号称是性能优秀, 语言层面支持并行的, 易于书写的利器. 写完跑一跑, 并发量还是上不去. 一直保持打死都不放弃的精神, 笔者再次用 go 的第二性能利器自带的 golang pprof 分析下代码的瓶颈. pprof 生成的报告还可以用 uber 第三方组件 go-torch 生成更直观的火焰图. 如图 1 所示. 从火焰图查看出 runtime.gcBgMarkWorker(gc: 垃圾回收器), 并且 runtime.mallocgc 也占用大量 cpu 时间. 接着进行内存占用分析, 使用 go tool pprof -alloc_space replay1 /tmp/mem.prof 查看如图 2 所示, 敲入 top10 命令, 发现 pull_worker 累加分配了 600 多 G 内存, 占比 93%,list pull_worker 命令找到该函数的瓶颈点. 这个 r4 变量的初始化放在一个 for 循环内, r4 是用于临时读取响应 body, 这个 r4 每次请求都重复分配, 导致内存居高不下, 解决办法是把他放在 for 循环外.
终章: 总结
好了, 至此单机并发量最高可以到 3w 了, 也差不多达到计划的目标了. 用两台这种机器组成的肉鸡就可以满足 5w qps 的请求了. 再来回答之前留下来的问题, python 语言并发上不去只是因为, 库不支持从外面提供读 buffer 读取响应 body, 导致内存暴增, 这不是语言本身的问题. 相信 python 并没有那么差. 同时, 也熟悉了一门新利器 go 语言. go 的原生协程支持和性能分析利器还是非常直观非常好用的, 力荐!!
图 1: 性能瓶颈前的 cpu 火焰图
图 2: 找到内存使用最多的函数
找到增长最多的代码
来源: https://www.qcloud.com/developer/article/1160803