Nginx静态资源配置优化
2021-04-17 Server服务器上同时运行有多个服务,于是配置了 Nginx 作为统一的入口,根据路由前缀将请求转发到不同的服务上。
HTTP API,WebSocket,TCP forwarding,static content,之前各个服务一直相安无事,但最近出现了问题,服务端 WebSocket 的连接总数偶尔会急剧下降,然后随着客户端的断线重连逐渐恢复。
对多次故障发生时间点附近的服务器日志进行对比分析后,确认 WebSocket 服务本身没有问题,问题出在 Nginx 提供的静态资源服务上。
问题分析
先根据监控日志找到故障发生的时间点,然后查看该时间点前后的 WebSocket 服务端日志,发现是 Socket.IO 的 ping
在设定的超时时间内没有收到客户端的 pong
响应,导致服务端主动关闭了连接。
又检查了同一服务器上其它服务的日志,发现在相同的一段时间内各个服务都没有收到新请求,Nginx 的 access.log
也有一段明显的空白期,因此初步怀疑是网络波动导致的。
但随着故障的多次发生,该故障由单纯网络波动引起的可能性变得越来越小,同事对比了多次故障日志后发现 Nginx 日志空白期前的最后几个请求都是图片资源,且所处目录在同一块 NFS 磁盘上,该磁盘存放的是用户上传时间较早的一部分静态资源,由于访问频率不高专门降低了磁盘读写速率,于是推测是这部分静态资源阻塞住了 Nginx,进而影响了其它服务在特定时间段内与用户的正常通信。
模拟复现
查看服务器上的 Nginx 配置文件,提取其中有关静态资源的选项后得到最小化配置如下:
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
sendfile_max_chunk 1m;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
server {
listen 8080;
server_name _;
root /srv/http;
}
}
其中的 sendfile
,tcp_nopush
,tcp_nodelay
等选项均参考了 Nginx Docs: Serving Static Content 中的推荐配置,没有看出来明显的问题。
读取文件阻塞的情况不太方便直接利用 NFS 进行模拟,于是本地使用 FUSE 在 /srv/http/mnt/
文件夹上挂载了一个虚拟文件系统,通过在自定义的 getattr
或 read
调用中加入等待时间来模拟不同情况下的阻塞:
#define FUSE_USE_VERSION 31
#include <fuse.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
const char filename[] = "slow";
const char contents[] = "123456789abcdef\n";
void debug_printf(const char *format, ...)
{
char now_time_str[20];
time_t timer = time(NULL);
strftime(now_time_str, 20, "%Y-%m-%d %H:%M:%S", localtime(&timer));
printf("%s \033[1;32m[I]\033[0m ", now_time_str);
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
void *s_init(struct fuse_conn_info *conn, struct fuse_config *cfg)
{
debug_printf("call s_init()\n");
return NULL;
}
int s_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
{
debug_printf("call s_getattr(%s)\n", path);
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0)
{
stbuf->st_mode = 0040755;
return 0;
}
else if (strncmp(path + 1, filename, strlen(filename)) == 0)
{
// sleep(60);
stbuf->st_mode = 0100444;
stbuf->st_size = strlen(contents);
return 0;
}
else
return -2;
}
int s_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags)
{
debug_printf("call s_readdir(%s)\n", path);
filler(buf, ".", NULL, 0, 0);
filler(buf, "..", NULL, 0, 0);
char name[20];
for (int i = 0; i < 10; i++)
{
sprintf(name, "%s_%d", filename, i);
filler(buf, name, NULL, 0, 0);
}
return 0;
}
int s_open(const char *path, struct fuse_file_info *fi)
{
debug_printf("call s_open(%s)\n", path);
return 0;
}
int s_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi)
{
debug_printf("call s_read(%s)\n", path);
// sleep(60);
size_t len = strlen(contents);
if (offset >= len)
return 0;
size = size > len - offset ? len - offset : size;
memcpy(buf, contents + offset, size);
return size;
}
const struct fuse_operations s_op = {
.init = s_init,
.getattr = s_getattr,
.readdir = s_readdir,
.open = s_open,
.read = s_read,
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &s_op, NULL);
}
在 ArchLinux 上安装 fuse3
依赖后,需要在 /etc/fuse.conf
中取消 user_allow_other
的注释,方便 Nginx 默认的 http 用户访问当前用户挂载的 fuse。然后使用 gcc -Wall slow_mount.c `pkg-config fuse3 --cflags --libs` -o slow_mount
得到编译结果,最后执行 ./slow_mount -f -o allow_other -o auto_unmount /srv/http/mnt
将其挂载至 /srv/http/mnt/
。
此时在 /srv/http/mnt/
目录下可以看到 slow_0
至 slow_9
十个文本文件,其内容均为 123456789abcdef\n
,访问过程中 slow_mount
会在日志中打印出具体的调用过程。例如执行 curl -v 127.0.0.1:8080/mnt/slow_0
,对应输出为:
将 Nginx 配置文件中的 worker_processes
值减小至 2 来方便快速占满所有 worker,在 /srv/http/
目录下创建内容为 ok 的文本文件 ok.txt
模拟同一 Nginx 上的其它服务,取消 slow_mount.c
源代码中 s_read()
函数内 sleep(60)
一行的注释重新编译挂载。
To Be Continued…
- 一边
watch -n 1 curl -s 127.0.0.1:8080/ok.txt
,一边tail -f /var/log/nginx/access.log
,方便观察到 Nginx 卡住 s_read()
加sleep(60)
进行测试s_getattr()
加sleep(60)
进行测试- FUSE 多线程机制,何时创建新线程,同时访问单个文件的冲突
- Nginx sendfile 具体系统调用实现
- Nginx aio 效果