问题来源
一个应用服务由于业务需要,存在频繁的大文件上传情况。运维反馈我们的服务经常会跑满上传带宽,影响其他团队的正常业务。
目的
对指定应用实现上传带宽的限制
解决方案
第一版
由于该应用服务未实现容器化部署,所以第一版方案简单粗暴:由运维人员在服务器上通过软件对服务器指定网卡进行限速
方案如下:
- 使用工具软件:
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 日志,还可以进行一个信息记录,方便后续问题定位
- 避免给每个实例开通访问外网的权限,保证网络和数据的安全性
方案实现如下:
- 构建一个 nginx 容器,在
Dockerfile
中打包配置文件 - 部署该容器
- 修改应用
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 转发的问题一样可以通过域名解析的方式解决,这样可以不需要通过配置去修改请求头中的域名。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于