一次openresty的网关性能优化之旅

我们公司是使用openresty作为网关,用于进行权限认证,反向代理,以及一些请求的处理。在使用过程中,我们遇到过这种情况,大部分时候openresty的性能都很好,cpu使用率都很低,但是有时候会突然cpu飙升,nginx的cpu打满,然后请求处理开始失败。但是这种情况出现次数少,持续时间不长,没法把cpu火焰图抓出来查看原因。

因此这里进行一次网关性能优化的探索,主要有两个目的,一是提高openresty的网关性能,处理偶现的这种异常情况。二是希望能有一种通用的智能的openresty限流方案,能确保在极限请求压力下,openresty能通过拒绝一部分请求的方式保障大部分请求正常被处理。

一、openresty网关性能优化

这里首先尝试探索openresty网关的性能优化方案。主要的思路就是先模拟出线上出现问题的情况,因为线上情况比较复杂,不好完全模拟,这里设计一个接口,将线上最常见的鉴权代码放进去,然后限制openresty的cpu上限,接着用高并发压测这个接口,把openresty的cpu打满,模拟线上的情况。

模拟后通过监控查看openresty的qps以及接口成功率,以这两者的乘积,即每秒的成功请求数作为最终压测的结果。 后面通过几种方案修改并重复压测,通过每秒成功数来判断方案是否有效提高了openresty的性能。

1.1 压测环境介绍

这里首先介绍下压测环境:

  1. openresty使用k8s部署,将其cpu limit设置为0.3,通过一个较低的cpu上线能更加容易的把cpu打满,方便压测。
  2. 压测工具使用hey,hey -t 5 -n 999999999 -c 3700 -q 1 'http://172.16.1.97:8001/test/nomal/request' 通过这种命令持续压测,-c 代表并发是3700,-q代表每秒每个并发最多请求一次,这样限制了总的qps最多为3700。
  3. 接口/test/nomal/request的逻辑如下,简单点说的话,就是先查redis查找认证信息,没查到就从mongo数据库中找,并设置到redis中供下次使用,是很常见的授权认证方式。这里简化了下,大家可以简单看下代码体会下。后面做了个处理,如果redis或者mongo中查到了数据,则返回200状态码,否则返回503状态码,这样可以在nginx监控或者压测结果中比较清楚的看出来成功率。
bash 复制代码
       location /test/nomal/request {
             content_by_lua_block {
               function getAuthInfo(id)
                 local authInfo = redis:get("L-" .. id)
                 if not authInfo then
                  local authInfo = get_from_mongo(id)
                  if authInfo and cjson.encode(authInfo) ~= "" then
                    redis:set("L-" .. id, authInfo)
                  else
                    return nil
                  end
                end
                return authInfo
              end

                local id = getAuthInfo("testId")
                if not id or tostring(id)=="" then
                  return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
                end
                return ngx.exit(ngx.HTTP_OK)
            }
       }
  1. redis和mongo都做了限流保护,避免因为网关占用了太多连接,导致其他服务连接redis或者mongo出现问题,redis的连接池数量是500,超过这个数量的redis请求会被放在一个400长度的队列中,超出这个队列的请求会直接失败。
1.2 线上环境模拟

这里首先模拟线上环境,把openresty的cpu打满,并且通过抓取火焰图来分析cpu使用情况。压测命令都是一样的,这个后续不再说明: hey -t 5 -n 999999999 -c 3700 -q 1 'http://172.16.1.97:8001/test/nomal/request'

测试结果:

qps: 2500

成功率:14%

每秒成功请求数:2500*14%=350

导出的lua火焰图:

分析:

从导出的lua火焰图可以看出,lua的大部分cpu都被用在了mongo库的处理上,因此我们下一步的优化方向就是通过减少mongo请求来提升处理能力。这里推荐下,如果想要配置同款的nginx监控和lua导出火焰图,可以查看笔者之前的文章:openresty监控 以及 openresty容器导出火焰图

1.3 优化mongo请求

仔细查看日志后发现,因为cpu打满了,openresty的处理速度变慢,很多redis的连接可能等待了一秒都没被处理,这就超过了redis的连接超时时间,导致缓存穿透,请求都去查mongo了,即使mongo连接也有限制,但是查询mongo的库估计比较占用cpu,导致这部分cpu使用率较高。

于是我们可以进行代码优化,只有在redis查询成功,但是没查到数据的情况下才去查mongo,这样就能避免redis失败时穿透到mongo。

bash 复制代码
              function getAuthInfo(id)
                 local authInfo,err = redis:get("L-" .. id)
                 if not authInfo and not err then
                  local authInfo = get_from_mongo(id)

测试结果:

qps: 2816

成功率:27.4%

每秒成功请求数:2816*27.4%=771

导出的lua火焰图:

分析:

从压测的结果可以看出,qps提高了,错误率反而下降了,每秒成功请求数是之前的2倍,说明优化是有效的,但是错误率还是有点高,这个是否有办法降低。目前比较容易想到的方式是限流,只要我们根据一定指标把多余的请求拒绝掉,是否就可以确保openresty始终能处理一定量的请求了呢。

1.4 限流

我们希望找一种限流方案,最好是比较智能的方案,可以根据一个固定的阈值触发。比如一个openresty实例在固定cpu下只能处理1000个请求,那最好是能找到个指标,当超出这个指标时就进行限流,把超过openresty处理能力的请求拒绝掉,从而最大化的保障openresty处理能力,避免因为雪崩导致大部分请求无法被正常处理。

这里额外说明下为什么不用常见的漏桶和令牌等限流方式,因为我们觉得这个方式的阈值比较固定,不够智能,很多时候可能阈值达到了,但是nginx实际上还没跑满,或者nginx已经跑满出问题了,但是qps阈值还没达到。我们希望找到一个更智能的限流方式。

这个我们尝试了几个指标,都不太理想,这里直接说结论,最能体现nginx到达处理能力瓶颈的指标就是cpu使用率,当其达到阈值时就是openresty出问题的时候。 这里的探索我们后面再说明,先来看修改方案和测试结果。

我们首先简单做了个限流,当检测到openresty cpu跑满时就开始限流,按照比例限制流量,比如20%的请求直接失败,并且如果cpu没有降下来就继续提升限流比例,cpu降下来了就降低限流比例。 不过目前没有实现完整的方案,而是先简单做了一版,手动控制cpu是否跑满的开关,限流也是默认是20%,也就是默认限制20%的请求。

测试结果:

qps: 3084

成功率:29.7%

每秒成功请求数:3084*29.7%=915

导出的nginx火焰图对比:

分析:

这里的火焰图是限流之前和之后的nginx cpu火焰图,黑圈中的unknown部分就是lua代码使用的cpu,可以看出限流后确实lua cpu使用率降低了,同时每秒成功请求数也提升了16%左右。但是并没有达到我们希望的理想效果,异常率还是有点高,这部分需要后面继续优化下限流方案。

这里我们重新考虑下redis,因为redis的连接池导致有部分请求失败,但是实际上redis支持的连接数上限是很高的,是否可以通过调整redis连接池配置的方式进一步优化?

1.4 redis连接池配置调整

我们这里不直接修改redis的连接池的最大连接数,修改连接数用满时的存放redis请求的队列的最大值,之前是400,我们改成2000来进行测试下。

测试结果:

qps: 2424

成功率:56.6%

每秒成功请求数:2424*56.6%=1,371

分析:

我们发现qps下降了,但是请求的成功率上升了很多,从而每秒成功请求数也上升了很多。这里分析下原因,应该是因为我们使用hey的压测是3700并发,并且每个连接每秒最多请求一次,通过这种方式控制总qps最多是3700/秒。 这里把redis请求放入了队列,导致更多的请求能够被成功处理,只是请求本身的延迟变高了,因此在每个连接上的qps就降低了。

举个例子,本来一个连接1秒一次,因为redis超过队列,就失败了,而队列加长后,可能变成每个连接的请求2秒才处理完成,但是redis请求在队列中等待,请求处理成功了,因此qps降低了,但是请求成功率上升了。

二、openresty网关限流方案探索

这里探索下openresty网关的限流方案,我们希望找到的最理想的方案就是能有一个指标可以明确表示出nginx的处理能力,这个指标出问题就说明nginx超过了其最大处理能力,请求会出问题。因此最主要的步骤就是找出这个能体现nginx处理能力是否到达阈值的指标。

我们首先参考了java的web服务器的架构,java的请求是在worker线程中一个一个处理的,未被处理的请求被放在一个队列中,因此我们可以监控这个队列,只要队列里面堆积的请求数超过一定数量,就代表到达了web服务器的处理能力上限,我们就可以直接丢弃新请求进行限流,避免服务器被冲垮。

2.1 架构区别

但是在nginx中我们发现这种方案并不适用,因为nginx和java的web服务器处理模型不一样。java的web服务器虽然也有异步,但是异步比较少,像是redis和mong这种数据库处理基本都是同步的,因此他处理请求都是一个一个处理,上一个处理完成了再处理下一个,因为查询redis和mongo的同步导致请求的处理过程也基本是同步的。

而nginx不一样,nginx是异步模型,请求被nginx的worker进程accept接收后进行处理时,数据到了就会执行lua代码进行处理,等lua执行到redis查询时,这个是一个io,nginx会异步处理,直接切换下一个请求并处理,等到redis的回复数据到了之后再回来处理之前的请求。这就导致监控他的队列没什么效果,因为他接收请求的速度很快,只要有cpu就会把请求接进来,用队列监控的方式没有效果。

那么监控正在处理中的请求数量有用么? 这个可以通过nginx的ngx.var.connections_writing变量来查询,他代表nginx正在处理的请求数,但是我们发现这个也不好使,因为你不能判断到多少的时候nginx是有问题的。那用什么指标比较合适呢?下面列一下我们尝试使用作为阈值的指标。

2.2 可能的指标
  1. socket中未读的数据,epollo中待处理的io数量:如果nginx来不及处理,则建立的socket中会有来不及处理的数据,Established 状态下的 Recv-Q会变大,同时因为worker实例因为cpu跑满也没时间去accept连接,则LISTEN状态下的Recv-Q也会变大,但是这个指标并不能很好的获取并用于限流,因为nginx没到极限时,这些值也会有,只是多和少的区别。
  2. ngx.var.connections_writing数量,nginx正在处理的请求数:这个值没法很好的体现是否堆积,因为他代表当前正在处理的请求,等于qps 乘以 延迟,比如有些请求比较慢,但是qps不高,也会导致这个值有一定的数量,无法用于判断现在是否过载。

因此,我感觉目前最合适的指标就是nginx实例的cpu使用率,这个指标显而易见,但是也是最有效的。

2.3 cpu过载的后果

假设此时qps超过了nginx本身的处理能力,cpu打满是必然现象,因为nginx会异步的不断的接收新请求并进行处理,只要cpu没满,就能接收更多的请求并处理。

cpu打满后,请求会变慢,有些io如redis,mysql和mongo请求就会超时,导致虽然请求返回200,但是状态码是失败的。同时达到最大值后,nginx从socket中获取数据处理请求的速度变慢,socket中的请求会一直得不到处理,甚至nginx建立redis连接都会失败,从而连接池中的redis连接下降,进一步导致雪崩,本来能处理的请求也处理不了了。

结论:

综上所述,只要nginx的cpu没跑满,就代表nginx没问题,可以处理更多请求。但是一旦nginx的cpu跑满,就容易出现雪崩状态,所有的请求都正在被处理,但是因为cpu不足,请求会因为各种原因失败,整体失败率大幅上升,造成雪崩。

如果出现nginx cpu跑满的情况时,可以通过动态限流或者增加nginx实例避免雪崩。

相关推荐
_Pluto_1 小时前
RabbitMQ的消息安全性
后端
踏浪无痕1 小时前
微服务日志与调用链打通方案:一个简单但实用的思路
后端
爱叫啥叫啥2 小时前
ESP32Mini打印机:多状态的LED灯驱动
后端
人道领域2 小时前
SSM框架从入门到入土(RESTful风格)
后端·restful
野犬寒鸦2 小时前
WebSocket协同编辑:高性能Disruptor架构揭秘及项目中的实战应用
java·开发语言·数据库·redis·后端
AntBlack2 小时前
上下求索,2025年我用AI写了哪些东西
后端·ai编程·年终总结
消失的旧时光-19432 小时前
第二十一课:系统是怎么一步步拆坏的?——单体到模块化实践(完整工程版)
java·spring boot·后端·架构
wanderful_2 小时前
自定义用户体系下 Django 业务模块开发踩坑与通用解决方案(技术分享版)
后端·python·django