使用 Nginx 做代理服务器实现上传限速

本贴最后更新于 263 天前,其中的信息可能已经斗转星移

问题来源

一个应用服务由于业务需要,存在频繁的大文件上传情况。运维反馈我们的服务经常会跑满上传带宽,影响其他团队的正常业务。

目的

对指定应用实现上传带宽的限制

解决方案

第一版

由于该应用服务未实现容器化部署,所以第一版方案简单粗暴:由运维人员在服务器上通过软件对服务器指定网卡进行限速

方案如下:

  • 使用工具软件:wondershaper
  • 配置如下
     [wondershaper]
     
     # Adapter
     IFACE="eth0"
     
     # Download rate in Kbps
     DSPEED="1048576"
     
     # Upload rate in Kbps
     USPEED="153600"
     
     # 无关配置略去
     
     ### EOF         
    

第二版

由于机房迁移和其他各种原因,该应用现在需要实现容器化部署。于是引入新的问题:如何在对容器化部署之后的应用实例进行限速处理

方案一

沿用之前的思路,通过软件限速,只不过这次变成在容器中集成 wondershaper,然后对容器实例进行限速。

方案缺点:

  • 对每个实例进行限速,如果是集群化部署,不能很好的权衡总体的上传带宽限制。

    eg: 现有 3 个实例,每个实例上传速度限制 10m/s。如果三个实例同时触发上传任务,上传速度就会达到 30m/s。

    由于实例个数可能会根据具体业务量变化,所以该方案灵活度和约束力不足。

  • 要在容器中进行网卡限速,需要在实例启动时获取更高的网络权限,使用如下 docker 命令获取:

    docker run --cap-add=NET_ADMIN <image_name>

正是由于上述缺点,公司的部署平台不支持自定义容器启动命令,所以需要另寻他法。

方案二

经过和架构师的沟通,意识到可以搭建一个 nginx 代理服务器,进行正向代理(可理解为一个外网的代理出口),并且在代理的同时,进行上传限速。

理论上来说,本方案有如下好处:

  • 作为统一的外网出口,在出口处进行上传限速。这样无论后面是多少服务多少实例,都可以完美的进行带宽限制。
  • 通过 nginx 日志,还可以进行一个信息记录,方便后续问题定位
  • 避免给每个实例开通访问外网的权限,保证网络和数据的安全性

方案实现如下:

  1. 构建一个 nginx 容器,在 Dockerfile 中打包配置文件
  2. 部署该容器
  3. 修改应用 nacos 配置,原外网域名修改为 nginx 代理服务器的域名或者 IP 加端口

Dockerfile 如下:

 FROM nginx:stable-alpine3.17-slim
 
 # 将本地主机的 nginx.conf 拷贝到容器内。
 COPY ./conf.d/*.conf /etc/nginx/conf.d/
 
 # 删除默认配置文件,排除默认配置的干扰
 RUN rm -rf /etc/nginx/conf.d/default.conf
 
 EXPOSE 80

nginx 配置文件如下:

 server {
     listen       80;   
 
     client_max_body_size   1G;
 
     location /a-url {
         # 指定该 URL 的上传带宽限制为 10MB/s
         limit_rate   10m;
         # 重写 url
         rewrite /a-url/(.*)$ /$1 break;
         proxy_pass https://a-service.com;
 
         proxy_set_header   Host                 a-service.com;
         # 禁用缓存
         expires off;
     }
 
     location /b-url {
         # 指定该 URL 的上传带宽限制为 10MB/s
         limit_rate   10m;
         # 重写 url
         rewrite /b-url/(.*)$ /$1 break;
         proxy_pass https://a-service.com;
 
         proxy_set_header   Host                 a-service.com;
         # 禁用缓存
         expires off;
     }
 }
 

踩坑记录

坑一

由于是 http 转发到 https,所以会涉及到外网服务端的证书认证。我们一般做反向代理的时候,都会设置 :

 proxy_set_header    Host    $host

但是在这里我们需要保证转发请求头中的 Host 和目的域名是一致的,保证证书认证通过,所以设置为:

 proxy_set_header   Host     目标域名

坑二

如果存在多个外网服务需要代理,我们一般会通过请求前缀去区分,比如 /a-url 对应 a-service/b-url 对应 b-service

所以,我们需要增加 url 重写的处理,否则转发后的 url 也会携带原请求前缀,导致请求失败。例如:

  • 内网请求:nginx.a.com/a-url/v1/api
  • 目标请求:a-service.com/v1/api
  • 转发后的请求:a-service.com/a-url/v1/api

重写逻辑如下:

 # 重写 url
 rewrite /b-url/(.*)$ /$1 break;

坑三

实际使用过程中,发现日志经常出现如下告警:

 a client request body is buffered to a temporary file /var/cache/nginx/client_temp/0000009460

这是 Nginx 服务器的一个警告消息,表示客户端请求主体被缓冲到了临时文件中。通常情况下,Nginx 会将客户端的请求数据保存在内存中处理,并尝试将响应数据直接发送回客户端。但如果请求主体较大或者处理时间较长,则可能会导致内存溢出或阻塞问题。

为了避免这种情况发生,Nginx 默认使用磁盘缓存来暂时保存客户端请求主体。当处理完整个请求后,缓存文件会自动删除。

解决方案:

通过修改以下配置项来控制缓存:

  • client_body_buffer_size:定义单个请求主体的最大大小,默认为 8KB。
  • client_body_temp_path:定义用于保存请求主体临时文件的路径,默认为/var/cache/nginx/client_temp/ 。
  • client_max_body_size:限制单个 HTTP 请求数量的大小,在超过此值时返回错误代码 413 Request Entity Too Large。
 # 禁用缓存
 client_body_buffer_size 0;
 # 不限制请求体大小
 client_max_body_size 0;

注意事项:

  • client_body_buffer_size 设置为 0 可能会影响性能和安全性,因此建议根据业务需要适当调整该参数
  • 在高流量网站上运行期间关闭 Nginx 缓存可能会导致性能问题,因此建议谨慎修改配置

根据实际业务量去调整对应配置,由于我的业务场景是分片上传,每次请求最大 50M,所以我做了如下配置

client_body_buffer_size 50M;
client_max_body_size 500M;

延伸一下

本次只是遇到了 HTTP 协议的上传限速问题,如果是其他的协议,比如 FTP 呢?

同样的 nginx 也支持 tcp/udp 协议层的转发,配置如下:

stream {
    server {
        listen 443; #监听的端口
        proxy_pass transfer.sh:443; #转发的目标IP和端口号

        # 添加带宽限制
        proxy_upload_rate 1m; #设置带宽限制为1MB/s
    }
}

上述配置需要添加在默认的 nginx.conf 主配置文件中

nginx 从 1.9.0 版本开始,新增了 ngx_stream_core_module 模块,使 nginx 支持四层,实现 TCP 和 UDP 代理。默认编译的时候该模块并未编译进去,需要编译的时候添加--with-stream,使其支持 stream 代理

上述解决方案同样存在一个坑:HTTPS 证书认证的问题

测试过程如下:

dd if=/dev/urandom of=testfile bs=10M count=1

curl -T testfile https://{nginx-ip}:443/test1

# 报错如下
curl: (51) Unable to communicate securely with peer: requested domain name does not match the server's certificate.

解决方案:其实,如果内网服务器无法访问外网,只能通过该 nginx 代理服务器出外网的话,我们完全可以在内网添加如下 DNS 域名解析:

{nginx-ip} {外网服务域名}

这样原始请求的域名与目标域名一致,就不存在证书认证问题了。

同理,之前的 HTTP 转发的问题一样可以通过域名解析的方式解决,这样可以不需要通过配置去修改请求头中的域名。

  • NGINX

    NGINX 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。 NGINX 是由 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,第一个公开版本 0.1.0 发布于 2004 年 10 月 4 日。

    311 引用 • 546 回帖 • 33 关注
  • Docker

    Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的操作系统上。容器完全使用沙箱机制,几乎没有性能开销,可以很容易地在机器和数据中心中运行。

    476 引用 • 899 回帖
  • 实战
    3 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...