原创

Nginx在HTTP/2协议下使用limit_req限流出现响应延迟问题排查处理

最近在nginx中间层增加了限流配置,限流方式是通过limit_req模块限制server_name的请求速率,防止并发请求瞬间全部打到后端模块。Nginx配置完成后,只通过脚本在本地并发测试限流生效后就没关注了,但是最近突然发现通过CND代理的请求如果触发了限流策略,会导致在限速内通过的请求响应时间越来越长,记录一下该问题的分析处理过程。

一、Nginx限流配置说明

Nginx限流是通过limit_req模块实现的,该模块通过漏桶算法限制请求速率,配置方法如下:

1.在http块中定义zone

配置语法:limit_req_zone zone=<名称>:<大小> rate=<速率>;

rate速率说明:

速率参数写法速率参数含义
10r/s每秒放行10个请求(该速率为平均速率,即每100毫秒放行1个请求)
60r/m每分钟放行60个请求(该速率为平均速率,即每1秒放行1个请求)

常用zone定义:

#按IP限流
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
#按域名限流
limit_req_zone $server_name zone=perserver:10m rate=12r/s;
#按自定义key限流(如用户ID)
limit_req_zone $api_user zone=peruser:10m rate=5r/s;

注:使用自定义key限流的时候需要通过map指令动态生成限流键。

我们现在的限流是使用的按域名限流规则,规则如下:

2.在server块中应用限流规则

配置语法:limit_req zone=<名称> [burst=<数值>] [nodelay | delay=<数值>];

应用规则配置:

limit_req zone=perserver burst=12 nodelay;
limit_req_status 429;

应用规则参数说明:

  • burst:允许的最大突发请求,如果不设置该参数默认值为0,nginx会严格按照rate设置的速率放行;
  • nodelay:burst内的突发请求立即放行,超出burst的请求立即拒绝,如果不设置该参数则突发请求排队,按rate速速率逐个处理;
  • delay:burst内的突发请求前N个立即放行,第N+1个请求到burst上限之间的请求按照按rate速率逐个处理;

二、遇到的问题说明

1.问题描述

使用HTTP/1.1协议的时候,触发限流策略后,响应时间正常;在使用HTTP/2协议的时候,当触发限流策略后,nginx日志中显示总请求响应时间request_time在逐渐增加,但是后端响应时间upstream_response_time却还维持原来的零点几秒正常响应范围内,日志截图如下:

2.python测试脚本

import requests
import threading
import time

# 测试链接地址
URL = "https://blog.eyyyye.com/"

def send_batch_requests(count):
    """
    发送指定数量的并发请求
    :param count: 请求总数
    """
    print(f"\n 正在发送 {count} 个请求到 {URL} ...")
    
    results = []
    lock = threading.Lock()

    def worker(index):
        try:
            start_time = time.time()
            r = requests.get(URL, timeout=5)
            elapsed = time.time() - start_time
            status = r.status_code
            # 判断是否被限流
            msg = "OK" if status == 200 else f"BLOCKED ({status})"
            print(f"  [请求 {index+1}] 状态码: {status} | 耗时: {elapsed:.3f}s | {msg}")
            
            with lock:
                results.append(status)
        except Exception as e:
            print(f"  [请求 {index+1}] 错误: {e}")
            with lock:
                results.append(0)

    threads = []
    for i in range(count):
        t = threading.Thread(target=worker, args=(i,))
        threads.append(t)
        t.start()

    for t in threads:
        t.join()

    # 统计结果
    success = results.count(200)
    blocked = len(results) - success
    print(f"\n 统计结果: 成功 {success} 个, 被拦截/失败 {blocked} 个")
    print("-" * 30)

if __name__ == "__main__":
    while True:
        try:
            user_input = input("\n请输入要发送的请求数量 (输入 q 退出): ")
            if user_input.lower() == 'q':
                break
            
            num = int(user_input)
            if num > 0:
                send_batch_requests(num)
            else:
                print("请输入一个正整数。")
        except ValueError:
            print("输入无效,请输入数字。")
        except KeyboardInterrupt:
            print("\n程序已退出。")
            break

3.问题复现

虽然python中的requests工具不支持HTTP/2协议,但是我们使用的阿里云ESA开启了HTTP2,所以我们只要在本地电脑使用测试脚本,直接向域名发起请求就可以测试复现。测试的时候在本地同时开启多个脚本,每个脚本控制少量并发,按顺序执行依次发起请求,就可以模拟持续触发限流的情况了,我们这里测试方法是本地开启5个测试脚本,每个脚本20个并发去测试。

测试脚本执行:

nginx日志:

可以看到日志中的request_time时间一直在增长,问题已经复现成功。

三、问题排查过程

接下来我们就需要考虑这个问题发生的原因和怎么去处理了。

根据日志中upstream_response_time的响应时间一直是稳定状态,那么我们可以先排除后端的问题,同时因为HTTP/1.1的响应时间又是正常的,那么我们可以暂时推断问题原因可能出在HTTP/2和限流模块limit_req的配合上面。

再仔细看一下nginx日志,我们发现所有被限制的请求,request_time时间都是0:

那么limit_req模块也没有什么问题了,剩下的可能就只有HTTP/2的问题了,我们先找AI问一下HTTP/2的特性:

通过分析HTTP/2的特点,我们发现可能影响响应时间有多路复用、流优先级和流量控制。先看多路复用,这个功能是在同一个TCP连接中可以并行处理多个请求或者响应,但是我们的并发的多个请求通过ESA透传到nginx的,每个请求都是一条TCP连接,所以不是这个功能影响的,同时流优先级也是在同一条TCP连接中,也不应该影响响应时间,那会是流量控制功能么,我们来让AI解释一下HTTP/2的流量控制原理:

看这个描述,HTTP/2的滑动窗口同样也是在单个TCP连接中生效,貌似也不是影响响应时间的原因呀,那么还有哪里漏掉了呢?有没有可能是ESA的问题呢?我们绕过ESA直接请求源站测试下:

好像不太对,为什么HTTP/1.1也出现这个并发响应延迟的问题了呢,之前测试为什么没发现呢。回忆一下之前测试的流程,是在服务器上直接测试的,再去测试看看:

可以看到服务器上脚本测试结果request_time和upstream_response_time基本一样,而在本地用户电脑上请求的时候request_time响应时间逐渐增加upstream_response_time还保持在零点几秒内,那么问题原因可能就是网络部分了,我们看一下测试的时候网络状况:

可以看到服务器的出口带宽已经跑满了。那么我们折腾了半天排查的并发响应延迟的问题原因就确定了,是并发数量太多,而服务器出口带宽不够导致的网络拥塞排队。

四、问题处理

既然确定了原因,那就可以开始着手处理了。处理方法嘛,要么花钱升级带宽,要么调整nginx配置优化响应数据包大小。

现在我们每个响应大概50KB左右,所以我们可以开启gzip压缩来降低响应数据包大小:

gzip on;
gzip_types  application/json;
gzip_min_length 1000;
gzip_comp_level 3;

重启nginx生效后再测试看看:

可以看到响应内容大小body_bytes_sent已经从50K压缩到9KB左右了,这样在限速内的请求就再也不会超过带宽啦。


扩展说明:

1.nginx开启gzip压缩会占用cpu资源,压缩等级越高占用cpu越多,一般为了平衡CPU,开到3级就够了。

2.参考文档1:https://nginx.org/en/docs/http/ngx_http_limit_req_module.html

3.参考文档2:https://www.rfc-editor.org/rfc/rfc7540.html

正文到此结束
本文目录