歌曲是<<千与千寻>>的插曲, 很好听
前言
性能优化的主体是一个 http 代理服务, 关于http 代理服务的开发选型, 我们有几种调研方案:
测试的机器性能: 单核nginx 处理 echo, 1w2 qps, ★★★★★为满分
- nginx c 扩展(upstream 模块): 性能很好, 转发请求有1w2/2=6k qps; 由于需要识别服务, 做策略(记状态), 略复杂的转发重试逻辑, 开发不是很方便
性能: ★★★★★
效率: ★★ - golang: 语言本身性能没问题, http 模块性能较差, 转发模式下只有3k qps, 是 c 扩展的一半; 丰富的扩展和简单的语法使得开发非常容易
性能: ★★
效率: ★★★★★ - openresty: 即 nginx+lua 方式, 在简单的转发测试中性能很好, 与 c 模块无差别, 在增加业务逻辑后有一定性能下降; 扩展很多, 配合 nginx, 开发比较方便
性能: ★★★★
效率: ★★★★
经过对比, 最终选择使用 openresty 开发, 服务具体的功能有:
- 全局唯一的动态拉取的配置
- 确定服务唯一名称
- 根据配置的策略确定后端, 并代理请求
- 后端失效屏蔽, 多次重试等逻辑
在增加所有业务逻辑后, 实际测试的性能由6k降低到3k, 损失一半
为了能够进一步降低资源占用效率, 试图对当前的逻辑进行优化, 效果是qps 由3k 增加到4k
分析工具
分析openresty的性能问题可以使用一个叫做火焰图的工具, 因为要安装内核 debug 包, 不推荐内核较低的开发机上折腾, 比较方便的过程如下:
- 安装虚拟机, mac 有 virtualbox/pd(四百多快一年)
- 安装一个开源 linux 操作系统最新版, 推荐 fedora
- 安装stap(如果没有附带), 执行stap-pre(自动安装所需要的包)
- 下载 github.com/openresty/n…, 如果使用了 lua 代码, 将所需(所有)文件拷贝到nginx 目录, 与 conf 同级(或将所需 lua 目录拷贝到工具所在目录)
- 启动 nginx 服务, 起压力, 查看 worker 的 pid
- cd 到4步骤所在的目录, 依次执行
./ngx-sample-lua-bt -p pid --luajit20 -t 10 > tmp.bt
./fix-lua-bt tmp.bt > flame.bt
perl ./stackcollapse-stap.pl flame.bt > flame.cbt
perl ./flamegraph.pl flame.cbt > flame.svg
最后生成的 svg 用浏览器打开即可, 打开后, 火焰图呈现的状态为: 横向为某过程占用的 cpu比例, 纵向为调用深度
如果出现横向很长的单个调用, 可能存在 cpu 热点
优化点
减少全局变量
在同一个请求中, 不同阶段之间, 不同调用之前可以通过 ngx.ctx 与 ngx.var 传递全局变量, 这两个调用非常消耗性能, 在 echo 测试中, 一条 ctx 赋值就能将整体 qps 下降接近1000(12000 -> 11000), 大概相当于3000个local变量赋值操作
原因可能与全局变量需要加锁, 元表方法调用等相关, 不太明白根本原因
总结一下, var 与 ctx 变量尽量少用, 在大部分场景下, 重新计算消耗的 cpu 都要比这样存起来更少
减少每个阶段的钩子
具体来说, 每个*_by_lua都会造成一定的性能下降, 数量级在百左右, 转发模块本来有四个*_by_lua的处理, 优化后, 变成两个
有两个阶段, header_filter_by_lua 与 body_filter_by_lua只有在出错时(请求没有发送给后端, 直接返回)才会起作用, 不应该在每个请求中都使用
总结一下, 尽量减少*_by_lua的阶段
减少函数调用
语法比函数调用更省 cpu, 比如连接两个字符串, string.format("%s%s", x, y)
就要比 x .. y
慢, 在大量使用字符串连接之后, 这种慢会影响到 qps
所以, 尽可能使用语法而不是函数调用, 对性能有正优化
合理使用jit
编译 openresty 时, 使用--with-luajit
打开 jit 功能在大多数情况下有利于性能提升
一般情况下, lua 代码总是被 lua 解释器解释执行的, jit 是一个优化执行的项目, 包括解释执行和编译执行
即使是 luajit, 所有的代码一开始也是被解释执行的, 只有一部分能被编译成机器码的方法执行频率超过某个阈值时
才会被编译成机器码以提高执行效率, 所以, 能通过 jit 加速的条件有两个, 一个是一部分方法
, 另一个是执行频率
这要求我们在热代码中尽可能不使用不能被编译成机器码的方法(简称为 NYI), 这些方法在 wiki.luajit.org/NYI(moonbingbing.gitbooks.io/openresty-b…)中有记录, 如果开启了jit 但是大量使用NYI 语法, 反而会降低代码效率, 对于耗时较多的NYI 方法, 可以考虑适当使用缓存进行保存
使用openresty 提供的方法
具体使用上, 尽可能少使用非 openresty 提供的方法, 除了以上说的 jit 的原因外, 还因为阻塞问题
比如 sleep, 使用 os.execute("sleep 1")
与 ngx.sleep(1)
, 前者是阻塞的, 后者可以让出 cpu, 对性能的影响很大
openresty 提供的所有方法都是非阻塞的, 在 io等待时都可以让出 cpu(比如 resty-开头的数据库驱动, ngx.方法等等), 推荐使用, 而lua 自己提供的方法一般不是(如 os 库, time 库等等), 不推荐使用
使用开源库
在”转发请求”这种场景下, 使用 openresty 的 balancer 库性能要比使用自己编写的 http 库(即使使用了 ngx.tcp 非阻塞 socket)要快很多(字符串操作在 c 中完成), 而且代码简洁
小结
openresty 非常适合做 http 代理服务器的开发, 与 go/php 相比有自己鲜明的特色(性能/效率), 配合合理的开发习惯, 可以在满足需求的情况下, 较大提高开发效率
打赏