Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Nginx的字节级限速原理

Nginx的字节级限速原理

作者头像
陶辉
发布于 2023-10-18 03:16:43
发布于 2023-10-18 03:16:43
80400
代码可运行
举报
文章被收录于专栏:陶辉笔记陶辉笔记
运行总次数:0
代码可运行

有同学反馈:在配置Nginx四层限速时,proxy_upload_rate和proxy_download_rate有一定的概率不生效。我按照他的步骤也能复现,但这与官方Nginx很稳定(相对其他开源软件)的印象并不相符,是不是Nginx的官方BUG呢?这里的真实原因,其实是Nginx字节限速机制与时间更新频率的协商导致的,这篇文章我们就来研究下Nginx的字节限速。

首先看下测试场景:基于UDP协议搭建四层代理(UDP协议更简单,更容易复现BUG),在nginx.conf中配置每秒最大上传10个字节:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
proxy_upload_rate 10;

客户端先发送10字节,服务器接收到后(用回包触发)客户端立刻再次发送10字节,预期服务器将在1秒后收到第2个报文,但实际上服务器有可能立刻收到报文,即proxy_upload_rate未生效或者不可控!一旦配置项处于不可解释的状态,这对于严谨的应用场景是不可接受的。而这个现象的原因,本质上是目前Nginx实现机制所致,接下来我会基于1.21版本的源码上解释其原理。

基于字节的限速实现原理

首先,我们要明确上例属于Nginx中的哪种限速。由于Nginx使用了内核协议栈,因此Nginx既不能对Packet级别的报文、也不能对TCP连接建立进行限速,而是只能在用户态基于调用socket编程API的时机,在字节转发速率、应用层协议的HTTP请求上(如官方的limit_req)做限制。

对于TCP协议的限速,当限速低于当前TCP连接的传输速率时,是通过零通告窗口来降低传输速率的。其具体作用原理为:作为接收端的Nginx所在服务器上,会有一个接收缓冲区,比如Linux中的tcp_rmem:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
net.ipv4.tcp_rmem = 4096	131072	6291456

tcp_rmem由TCP滑动窗口和socket读缓冲区共用,当Nginx在Epoll返回“可读”IO事件时,却不去读取socket数据,那么,当tcp_rmem被接收到的数据占满后,接收滑动窗口就会变为0,此时TCP连接的对端就会收到零窗口通知,进而停止发送数据,如下图所示:

UDP协议与之类似,只不过因为没有重传机制,新收到的UDP报文会被直接丢弃。

对于字节转发速率的限制,Nginx正是通过上述机制生效的。无论是四层的proxy_upload_rate和proxy_download_rate,或者是七层的limit_rate,Nginx都是基于每秒转发字节数进行限速的,区别只在于,四层的2个指令都是在socket接收时生效,而七层则在socket发送到下游客户端(单向)时生效(这里并不基于TCP滑动窗口生效,因为Nginx只需保证降低发送HTTP响应的速率即可达到设计目标)。

下图是我以STREAM四层为例,画出的限速流程示意图:

可以看到,在执行socket.read函数前会先计算一次限速公式,如果已经达到限速阈值,则根据计算出的等待时间添加定时器退出,此后就有可能出现TCP零窗口或者UDP报文丢弃;反之,才会将socket缓冲区中的数据拷贝到用户态转发。当然,在接收完数据后,还会做一次限速计算,此操作不影响本次数据的转发,只影响当前事件下是否会连续多次读取socket缓冲区。

开篇提到的限速失效问题关键就在于图中的限速公式。

Nginx的限速计算公式

先来看Nginx计算限速的关键代码,它在ngx_stream_proxy_module.c文件的ngx_stream_proxy_process函数中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (limit_rate) { 
    //limit_rate就是proxy_upload_rate(上行)或者proxy_download_rate(下行)的值
    limit = (off_t) limit_rate * (ngx_time() - u->start_sec + 1)
            - *received;
    //limit<=0就达到了限速条件
    if (limit <= 0) {
        delay = (ngx_msec_t) (- limit * 1000 / limit_rate + 1);
        //delay定时器到达后,才会继续转发数据
        ngx_add_timer(src->read, delay);
        break;
    }
}

这个公式同时适用于上、下游的数据转发,当从下游客户端转发数据时,limit_rate值为nginx.conf配置文件的proxy_upload_rate指令,而从上游服务器转发数据时,limit_rate值则为配置文件的proxy_download_rate指令,而received指针指向已接收到的字节总数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (from_upstream) {
    limit_rate = u->download_rate;
    received = &u->received;
} else {
    limit_rate = u->upload_rate;
    received = &s->received;
}

这个公式的设计思想是:如果ngx_time()当前时间与u->start_sec请求处理的起始时间的时间差内,转发的字节数received超出了limit_rate限制,就要立刻停止转发数据,其中暂停的时间是delay毫秒。虽然这个公式由STREAM四层使用,但HTTP七层也差不多,参见ngx_http_write_filter_module.c文件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (r->limit_rate) {
    limit = (off_t) r->limit_rate * (ngx_time() - r->start_sec + 1)
            - (c->sent - r->limit_rate_after);

    if (limit <= 0) {
        c->write->delayed = 1;
        delay = (ngx_msec_t) (- limit * 1000 / r->limit_rate + 1);
        ngx_add_timer(c->write, delay);

        c->buffered |= NGX_HTTP_WRITE_BUFFERED;

        return NGX_AGAIN;
    }
}

为了方便理解,我们继续以四层代码为例说明问题。当应该限速时,公式返回的limit变量就会大于0,而本文开头提到的测试场景,第2秒发送10字节时,*received的值肯定是10(第1秒转发),而ngx_time() - u->start_sec预期为1,所以limit的值预期为10*(1+1)-10,也就是10,进而delay值应为1秒才对。但实测结果却是有很大概率limit为0,导致Nginx没有限速。这是什么原因呢?

Nginx的时间更新方式

其实公式中的“变量”只可能是时间,毕竟limit_rate是配置文件中的指令,*received是已转发字节,这两者都不可能出错。所以,问题肯定出在u->start_sec或者ngx_time()的精准度上!前者u->start_sec的赋值很简单,参见ngx_stream_proxy_handler函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
static void
ngx_stream_proxy_handler(ngx_stream_session_t *s)
{
    u->start_sec = ngx_time();
}

当Nginx接收到下游客户端的数据,准备向上游服务器建立会话连接时,u->start_sec就被初始化为当前时间。再来看ngx_time函数是如何返回系统时间的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
extern volatile ngx_time_t  *ngx_cached_time;

#define ngx_time()           ngx_cached_time->sec
#define ngx_timeofday()      (ngx_time_t *) ngx_cached_time

Nginx在新版本实现中为了优化性能,使用了缓存时间ngx_cached_time(即使调用ngx_timeofday函数返回的也是缓存时间),这带来了2个问题:

  1. ngx_time函数只取了秒,直接舍弃了毫秒精度(连四舍五入也没有考虑);
  2. ngx_cached_time的更新频率必然影响限速的时间精度。

本文开头问题与上述二者都有关系。忽略毫秒必然带来最大1秒的时间误差,而ngx_cached_time的更新频率会在此基础上放大ngx_time() - u->start_sec的误差,再来看下ngx_cached_time是如何更新的。

ngx_time_update函数负责更新ngx_cached_time,在谈其调用频率之前,先来看看它在多线程上的锁优化设计,这也有微小的时间精度降低:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#define ngx_gettimeofday(tp)  (void) gettimeofday(tp, NULL);
void
ngx_time_update(void)
{
    ngx_tm_t         tm, gmt;
    time_t           sec;
    ngx_uint_t       msec;
    ngx_time_t      *tp;
    struct timeval   tv;

    //加锁
    if (!ngx_trylock(&ngx_time_lock)) {
        return;
    }
    //真实执行操作系统的gettimeofday系统调用
    ngx_gettimeofday(&tv);

    sec = tv.tv_sec;
    msec = tv.tv_usec / 1000;

    tp = &cached_time[slot];

    if (tp->sec == sec) {
        tp->msec = msec;
        ngx_unlock(&ngx_time_lock);
        return;
    }
    //循环复用cached_time数组
    if (slot == NGX_TIME_SLOTS - 1) {
        slot = 0;
    } else {
        slot++;
    }

    tp = &cached_time[slot];

    tp->sec = sec;
    tp->msec = msec;

    //volatile类型的原子操作
    ngx_cached_time = tp;

    //解锁
    ngx_unlock(&ngx_time_lock);
}

Nginx支持多线程(虽然用得不多),因此为了减少加锁操作,Nginx使用了含有64个元素的数组cached_time循环复用保存时间,这样读时间时就省去了加锁操作,只在更新时才会加锁并通过变更slot的值移动循环数组:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#define NGX_TIME_SLOTS   64

static ngx_time_t        cached_time[NGX_TIME_SLOTS];
static ngx_uint_t        slot;
static ngx_atomic_t      ngx_time_lock;

volatile ngx_time_t     *ngx_cached_time;

ngx_time_update调用完成后,ngx_cached_time就会保存最新的时间。 再来看ngx_time_update函数的调用时机,这里主要参见Linux epoll多路复用机制中的ngx_epoll_process_events函数(这是更新时间的固定代码段):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
    events = epoll_wait(ep, event_list, (int) nevents, timer);

    if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
        ngx_time_update();
    }
}

可见,每处理一批IO事件时,只要flags参数中携带了NGX_UPDATE_TIME标志,就会更新时间。那么,究竟何时会携带NGX_UPDATE_TIME标志位呢?这里要参见worker进程中循环调用的ngx_process_events_and_timers函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ngx_uint_t  flags;

    if (ngx_timer_resolution) {
        flags = 0;
    } else {
        flags = NGX_UPDATE_TIME;
    }

    (void) ngx_process_events(cycle, timer, flags);
}

这里又多出了一个ngx_timer_resolution,这又是什么鬼?从官方文档中可以看到:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Syntax:	timer_resolution interval;
Default:Context:	main
Reduces timer resolution in worker processes, thus reducing the number of gettimeofday() system calls made. By default, gettimeofday() is called each time a kernel event is received.

timer_resolution是用于降低时间更新频率的。当然,默认情况下我们并不会配置timer_resolution,此时每批Epoll IO事件都会更新一次时间。

到这里,终于可以彻底回答本文开头的问题了。虽然Nginx的限速公式没有问题,但是Nginx时间精度却有2个问题,导致公式中的时间差ngx_time() - u->start_sec存在秒级的计算误差: 1、在系统不繁忙时,舍弃毫秒会导致最大1秒的误差; 2、时间更新频率则受到timer_resolution指令、epoll事件的批次数量、锁优化设计下的时间数组更新误差、worker进程的延迟调度等因素综合影响。

所以,我们在验证或者设计测试场景时,需要将上述2个因素都纳入考虑。同时,在Nginx更新版本时,综合评估Nginx源码设计的变动,就能更准确的掌握限速的要理。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-09-132,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
nginx源代码分析–event事件驱动初始化
2.进入函数ngx_init_cycle,调用每一个核心模块的create_conf
全栈程序员站长
2022/07/13
3860
Nginx的时间管理
gettimeofday()的开销 在Linux中,Nginx通过gettimeofday()获取系统当前时间; gettimeofday是C库提供的函数(不是系统调用),它封装了内核里的sys_gettimeofday系统调用。 Linux的系统调用通过int 80h实现,用系统调用号来区分入口函数,步骤大致如下: 1 API将系统调用号存入EAX,然后通过中断调用使系统进入内核态; 2 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用); 3 系统调用完成相应功能,将返回值存入EAX,返回到中断处理函数; 4 中断处理函数返回到API中; 5 API将EAX返回给应用程序
星哥玩云
2022/07/03
5540
Nginx——事件驱动机制(雷霆追风问题,负载均衡)
所有的worker进程都在ngx_worker_process_cycle方法中循环处理事件,处理分发事件则在ngx_worker_process_cycle方法中调用ngx_process_events_and_timers方法,循环调用该方法就是 在处理全部事件,这正是事件驱动机制的核心。该方法既会处理普通的网络事件,也会处理定时器事件。
全栈程序员站长
2022/07/06
4510
Nginx——事件驱动机制(雷霆追风问题,负载均衡)
nginx upstream模块完整逻辑源码分析
1.启动upstream。 2.连接上游服务器。 3.向上游发送请求。 4.接收上游响应(包头/包体)。 5.结束请求。
stan1ey
2021/01/23
3.1K0
Nginx静态资源服务的配置
上面配置中的http、server、location等都是指令块。指令块配置项之后是否如参数(例如 location /),取决于解析这个块配置项的模块。
mazhen
2023/11/24
3.7K0
Nginx静态资源服务的配置
nginx限速,带宽,IP;
限制向客户端传送响应数据的速度,可以用来限制客户端的下载速度。参数rate的单位是字节/秒,0为关闭限速。
拓荒者
2019/03/15
7.6K0
nginx限速,带宽,IP;
Nginx(五):http反向代理的实现
上一篇nginx的文章中,我们理解了整个http正向代理的运行流程原理,主要就是事件机制接入,header解析,body解析,然后遍历各种checker,以及详细讲解了其正向代理的具体实现过程。这已经让我们对整个nginx有了较深入的了解,但nginx核心固然重要,但其扩展功能才是其吸引大家的地方。而它的扩展功能又是无穷无尽的,这是好事又是坏事,好事是功能特别多,坏事是我们不可能都能探究其每个模块。
烂猪皮
2021/01/28
1K0
Nginx(五):http反向代理的实现
Nginx限速指令limit_rate完成带宽控制
Nginx的http核心模块ngx_http_core_module中提供limit_rate指令可以用于控制速度,limit_rate_after用于设置http请求传输多少字节后开始限速。
子润先生
2021/06/24
9610
高并发场景,nginx怎么限速
我们会通过一些简单的示例展示Nginx限速限流模块是如何工作的,然后结合代码讲解其背后的算法和原理。
后端技术探索
2018/12/05
2K0
高并发场景,nginx怎么限速
深入理解Nginx模块开发与架构解析
1.Nginx特点:更快、高扩展性、高可靠性、低内存消耗、单机支持10万以上的并发连接、热部署、最自由的BSD许可协议
硬核项目经理
2019/08/06
7020
深入理解nginx stream proxy 模块的ssl连接原理
  我一直来对ssl建立连接的过程一知半解,以前分析nginx代码的时候一旦碰到ssl连接部分的代码都是直接跳过,前面在分析ngx_http_upstream_dynamic_module的时候正好想到了是不是可以给它添加一个能够支持https健康检查的功能,所以今天决定沉下心来仔细分析一下nginx本身的与上游服务器建立连接的实现逻辑。
码农心语
2024/04/09
1.3K0
深入理解nginx stream proxy 模块的ssl连接原理
Nginx(二): worker 进程处理流程框架解析
Nginx 启动起来之后,会有几个进程运行:1. master 进程接收用户命令并做出响应; 2. worker 进程负责处理各网络事件,并同时接收来自master的处理协调命令;
烂猪皮
2021/01/28
1.4K0
Nginx(二): worker 进程处理流程框架解析
HAproxy 代理技术原理探究
HAProxy是一款提供高可用性、负载均衡以及基于TCP(第四层)和HTTP(第七层)应用的代理软件
星哥玩云
2022/07/04
3700
深入理解nginx的请求限流模块
  当构建高流量的Web应用程序时,保护服务器免受过多请求的影响是至关重要的。过多的请求可能会导致服务器过载,降低性能甚至导致系统崩溃。为了解决这个问题,nginx提供了一个强大的请求限速模块。该模块允许您根据自定义规则限制客户端请求的速率,并且还可以使用延迟机制来平滑处理超出限制的请求。在本文中,我们将深入探讨nginx的请求限速模块,了解它的工作原理、配置选项以及如何在实际应用中使用它来保护您的服务器免受恶意或异常请求的影响。
码农心语
2024/04/09
1.2K0
深入理解nginx的请求限流模块
nginx源码阅读(4)单进程epoll流程解析
我们这里以单进程启动为例 nginx.c中的main 函数调用ngx_single_process_cycle
golangLeetcode
2022/08/02
4710
通过源码理解http层和tcp层的keep-alive
很久没更新文章了,今天突然想到这个问题,打算深入理解一下。我们知道建立tcp连接的代价是比较昂贵的,三次握手,慢开始,或者建立一个连接只为了传少量数据。这时候如果能保存连接,那会大大提高效率。下面我们通过源码来看看keep-alive的原理。本文分成两个部分
theanarkh
2020/06/19
8980
Tengine/Nginx限速简介及配置
Nginx主要有两种限速方式:按连接数限速(ngx_http_limit_conn_module)、按请求速率限速(ngx_http_limit_req_module)。超出限制的请求会直接拒绝,可防御简单的cc攻击
4xx.me
2022/06/09
8960
Tengine/Nginx限速简介及配置
nginx的延迟关闭
最近业务方反馈线上 Nginx 经常会打出一些『奇怪』的 access 日志,奇怪之处在于这些日志的 request_time 值总是正好 upstream_response_time 的值大5秒,于是我就帮他们查看了一下导致这个问题的原因,本文记录一下最终调查的结论以及过程。
后端技术探索
2019/04/25
3.9K0
Web应用程序限速方法
一般来说Web应用程序的开发者不太关心网络限速的问题。所以通常写的程序逻辑基本认为用户提交上来的数据速率越快越好;用户下载文件时,下载越快越好。但现实情况是服务器的带宽不是无限的,通常我们并不希望某一个用户的极速下载导致其它用户感觉此Web应用程序不可用。这样就带来了网络速率的需求。我在实际工作中大概总结出好几种限速办法,在这里记录以备忘。 ngx_http_core_module限制下载速率 最简单是直接使用ngx_http_core_module中的limit_rate、limit_rate_after
jeremyxu
2018/05/10
3.4K0
nginx状态码处理源码分析
nginx状态码分为五大类: 100-199 用于指定客户端应相应的某些动作。 200-299 用于表示请求成功。 300-399 用于已经移动的文件并且常被包含在定位头信息中指定新的地址信息。 400-499 用于指出客户端的错误。 500-599 用于支持服务器错误。
stan1ey
2021/06/07
2.1K0
nginx状态码处理源码分析
相关推荐
nginx源代码分析–event事件驱动初始化
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档