Nginx open_file_cache模块 文件描述符缓存
在我前面的博客介绍了nginx缓存,但是nginx还有一个很重要的缓存功能只针对于打开的文件句柄以及源信息叫做open_file_cache,open_file_cahce对我们优化nginx性能也是非常有帮助的。
NGINX虽然已经对静态内容做过优化。但在高流量网站的情况下,仍然可以使用open_file_cache进一步提高性能。 NGINX缓存将最近使用的文件描述符和相关元数据(如修改时间,大小等)存储在缓存中。缓存不会存储所请求文件的内容。
下面只是大概列出来到底缓存了什么,每个缓存对应其源代码,可以看出缓存了文件句柄fd,缓存了文件句柄就意味着不用每次都close一个文件再open一个文件,减少了系统调用的操作。
同时文件大小,文件修改时间也缓存了,在查询文件的时候可能会遇到错误信息,这些信息也被缓存了,比如403,下一次就不需要再打开文件确定是否失败了。目录等等也被缓存了。
被缓存的文件元信息包括:
fd,文件被打开一次后,fd保留使用
size
path
last modified time
…
启用 nginx 的 open_file_cache 指令可以对打开的文件句柄进行缓存,从而节约昂贵的 open() 系统调用。通过扩大这个缓存的容量可以提高线上的实际命中率。但是缓存容量并不是越大越好,比如当达到 20000 个元素的容量时,共享内存的锁就成了瓶颈。 (Nginx 的 open_file_cache 相关配置可以缓存静态文件的元信息,在这些静态文件被频繁访问时可以显着提升性能)
open_file_cache模块
1 open_file_cache
启用此指令将存储以下信息的缓存:
打开的文件描述符和相关元数据,如大小,修改时间等
文件和目录的存在与查找相关的任何错误,例如“权限被拒绝”,“文件未找到”等
缓存定义固定大小,并且在溢出期间,它移除最近最少使用(LRU)元素。
缓存在一段时间不活动之后逐出元素。 默认情况下禁用该指令。
如下例子:
http{ open_file_cache max=1000 inactive=20s; }
在上述配置中,为1,000个元素定义了一个缓存。 inactive参数配置到期时间为20秒。 没有必要为该指令设置非活动时间段,默认情况下,非活动时间段为60秒。
2 open_file_cache_valid
Syntax: open_file_cache_valid time; Default: open_file_cache_valid 60s; Context: http, server, location
NGINX的open_file_cache保存信息的快照。 由于信息在源处更改,快照可能在一段时间后无效。 open_file_ cache_valid指令定义时间段(以秒为单位),之后将重新验证open_file_cache中的元素。默认情况下,60秒后重新检查元素。 如下例子:
http{ open_file_cache_valid 30s; }
确认经过60s之后再去看看缓存的内容是否有效,如果发生了更新,那么需要更新。这样做的原因是因为虽然缓存了文件,但是有其他的进程,比如用户或者其他的服务没有通过nginx在修改文件,这会导致nginx缓存的fd句柄指向的文件不是最新的文件,特别是配置的时间特别大,缓存文件句柄数特别多的时候,很有可能导致客户拿到的是过期的文件,所以要设置这个时间要保证在这个时间以后,如果磁盘上的文件发生变化,那么我们还可以去获取新的文件。
如果你的静态文件内容变化频繁并且对时效性要求较高,一般应该把 open_file_cache_valid 设置的小一些,以便及时检测和更新。
如果变化相当不频繁的话,那就可以设置大一点,在变化后用 reload nginx 的方式来强制更新缓存。
对静态文件访问的 error 和 access log 不关心的话,可以关闭已提升效率。
3 open_file_cache_min_uses
Syntax: open_file_cache_min_uses number; Default: open_file_cache_min_uses 1; Context: http, server, location
此指令可用于配置最小访问次数以将元素标记为活动使用。 默认情况下,最小访问次数设置为1次或更多次(至少要访问多少次才能留在缓存当中)。如下例子
http{ open_file_cache_min_uses 4; }
4 open_file_cache_errors
Syntax: open_file_cache_errors on | off; Default: open_file_cache_errors off; Context: http, server, location
对一些访问文件错误的信息是否进行缓存,默认是off的
如前所述,NGINX可以缓存在文件访问期间发生的错误。但是这需要通过设置open_file_cache_errors指令来启用。 如果启用错误缓存,则在访问资源(不查找资源)时,NGINX会报告相同的错误。默认情况下,错误缓存设置为关闭。
http{ open_file_cache_errors on; }
这里有个配置示例:
open_file_cache max=64 inactive=30d; open_file_cache_min_uses 8; open_file_cache_valid 3m;
max=64 表示设置缓存文件的最大数目为 64, 超过此数字后 Nginx 将按照 LRU 原则丢弃冷数据。
inactive=30d 与 open_file_cache_min_uses 8 表示如果在 30 天内某文件被访问的次数低于 8 次,那就将它从缓存中删除。
open_file_cache_valid 3m 表示每 3 分钟检查一次缓存中的文件元信息是否是最新的,如果不是则更新之。
一般建议配置为
open_file_cache max=10000 inactive=30s; open_file_cache_valid 60s; open_file_cache_min_uses 2; open_file_cache_errors on;
为什么只缓存文件元信息而不缓存文件内容?
这个问题的关键是 sendfile(2).
Nginx 在 serve 静态文件的时候用的是 sendfile(2), 当然前提是你配置了 sendfile on, sendfile(2) 直接在 kernel space 内传输数据,对比使用 read(2)/write(2) 省去了两次 kernel space 与 user space 之间的数据拷贝。而同时这些被频繁读取的静态文件的内容会被 OS 缓存到 kernel space。在这样的机制下,我们缓存中有文件的 fd 和 size,直接调用 sendfile(2) 就可以了。
如果要 Nginx 连内容一起缓存,那就需要每次文件变化都要用 read(2) 将数据从 kernel space 复制到 user space,然后放在 user space,每次应答请求的时候再从 user space 复制到 kernel space 然后写入 socket。比起前面的方式,这样的方式毫无优点。
说了这么多来看看效果
不开启open_file_cache,注释掉指令
server { listen 80; server_name www.test.com; charset utf-8; root html; location / { # open_file_cache max=10 inactive=60s; # open_file_cache_min_uses 1; # open_file_cache_valid 60s; # open_file_cache_errors on; } } [root@www ~]# /usr/local/nginx/sbin/nginx -s reload [root@www ~]# ps -ef | grep nginx | grep -v grep | grep -v master nginx 57654 55384 0 10:26 ? 00:00:00 nginx: worker process #strace可以跟踪系统调用,这个时候可以跟进访问,我的nginx只有一个worker进程 [root@www ~]# strace -p 57654 #这个时候没人去访问可以看到挂在epoll_wait上面 strace: Process 57654 attached epoll_wait(10, [root@www ~]# curl localhost:80 现在开始去访问 [root@www ~]# strace -p 57654 strace: Process 57654 attached epoll_wait(10, [{EPOLLIN, {u32=13620784, u64=13620784}}], 512, -1) = 1 accept4(7, {sa_family=AF_INET, sin_port=htons(37808), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_NONBLOCK) = 4 epoll_ctl(10, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=13621248, u64=13621248}}) = 0 epoll_wait(10, [{EPOLLIN, {u32=13621248, u64=13621248}}], 512, 60000) = 1 recvfrom(4, "GET / HTTP/1.1\r\nUser-Agent: curl"..., 1024, 0, NULL, NULL) = 73 stat("/usr/local/nginx/html/index.html", {st_mode=S_IFREG|0644, st_size=612, ...}) = 0 open("/usr/local/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 5 fstat(5, {st_mode=S_IFREG|0644, st_size=612, ...}) = 0 setsockopt(4, SOL_TCP, TCP_CORK, [1], 4) = 0 writev(4, [{"HTTP/1.1 200 OK\r\nServer: nginx/1"..., 253}], 1) = 253 sendfile(4, 5, [0] => [612], 612) = 612 write(6, "127.0.0.1 - - [08/Jun/2020:10:29"..., 90) = 90 close(5) = 0 setsockopt(4, SOL_TCP, TCP_CORK, [0], 4) = 0 epoll_wait(10, [{EPOLLIN|EPOLLRDHUP, {u32=13621248, u64=13621248}}], 512, 65000) = 1 recvfrom(4, "", 1024, 0, NULL, NULL) = 0 close(4) = 0 epoll_wait(10, 可以看到epoll_ait返回了accept建立了一个新的TCP连接,之后调用recvfrom获取到请求的内容,然后使用stat看看访问的首页内容是否存在,可以看到是存在的并且大小是st_size=612字节,而且访问的权限是st_mode=S_IFREG|0644 0644。之后打开文件获取到句柄open("/usr/local/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 5,现在拿着这个句柄将数据返回,数据的返回时通过sendfile(4, 5, [0] => [612], 612) = 612。
这里需要注意sennfile是优化的一个关键点,因为sendfile是一个0拷贝技术,即磁盘中不需要读到用户态,再到用户态发到内核态,再从网卡发出去,而是有了sendfile调用直接告诉文件,以及文件偏移量,直接就在内核态之中把磁盘上的内容发到网卡上,所以说这个性能是很高的。
但是引入了open("/usr/local/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 5和
close(5) 就没有必要了,因为做sendfile就没有必要去做open和close的,因为nginx性能作为用户态没必要去打开的(这是我们优化的关键点)。
通过senfile将数据发给了客户端,又做了一次epoll_wait(10, [{EPOLLIN|EPOLLRDHUP, {u32=13621248, u64=13621248}}], 512, 65000) = 1,epoll_wait(10,,在这里等着以后的请求。
开启open_file_cache,开启指令
server { listen 80; server_name www.test.com; charset utf-8; root html; location / { open_file_cache max=10 inactive=60s; open_file_cache_min_uses 1; open_file_cache_valid 60s; open_file_cache_errors on; } } #nginx重新reload [root@www ~]# curl localhost:80 [root@www ~]# strace -p 58695 strace: Process 58695 attached epoll_wait(10, [{EPOLLIN, {u32=13521056, u64=13521056}}], 512, -1) = 1 accept4(7, {sa_family=AF_INET, sin_port=htons(37812), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_NONBLOCK) = 3 epoll_ctl(10, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=13521520, u64=13521520}}) = 0 epoll_wait(10, [{EPOLLIN, {u32=13521520, u64=13521520}}], 512, 60000) = 1 recvfrom(3, "GET / HTTP/1.1\r\nUser-Agent: curl"..., 1024, 0, NULL, NULL) = 73 open("/usr/local/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 8 fstat(8, {st_mode=S_IFREG|0644, st_size=612, ...}) = 0 setsockopt(3, SOL_TCP, TCP_CORK, [1], 4) = 0 writev(3, [{"HTTP/1.1 200 OK\r\nServer: nginx/1"..., 253}], 1) = 253 sendfile(3, 8, [0] => [612], 612) = 612 write(5, "127.0.0.1 - - [08/Jun/2020:10:48"..., 90) = 90 setsockopt(3, SOL_TCP, TCP_CORK, [0], 4) = 0 epoll_wait(10, [{EPOLLIN|EPOLLRDHUP, {u32=13521520, u64=13521520}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) = 0 epoll_wait(10, #第一次没有做缓存,可以看到使用了 open("/usr/local/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 8 close(3) #再去访问一次 [root@www ~]# curl localhost:80 [{EPOLLIN, {u32=13521056, u64=13521056}}], 512, -1) = 1 accept4(7, {sa_family=AF_INET, sin_port=htons(37816), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_NONBLOCK) = 3 epoll_ctl(10, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=13521521, u64=13521521}}) = 0 epoll_wait(10, [{EPOLLIN, {u32=13521521, u64=13521521}}], 512, 60000) = 1 recvfrom(3, "GET / HTTP/1.1\r\nUser-Agent: curl"..., 1024, 0, NULL, NULL) = 73 stat("/usr/local/nginx/html/index.html", {st_mode=S_IFREG|0644, st_size=612, ...}) = 0 setsockopt(3, SOL_TCP, TCP_CORK, [1], 4) = 0 writev(3, [{"HTTP/1.1 200 OK\r\nServer: nginx/1"..., 253}], 1) = 253 sendfile(3, 8, [0] => [612], 612) = 612 write(5, "127.0.0.1 - - [08/Jun/2020:10:50"..., 90) = 90 setsockopt(3, SOL_TCP, TCP_CORK, [0], 4) = 0 epoll_wait(10, [{EPOLLIN|EPOLLRDHUP, {u32=13521521, u64=13521521}}], 512, 65000) = 1 recvfrom(3, "", 1024, 0, NULL, NULL) = 0 close(3) 可以看到第二次访问接收到请求直接调用了sendfile(3, 8, [0] => [612], 612) = 612 没有见到上面的 open("/usr/local/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 8 fstat(8, {st_mode=S_IFREG|0644, st_size=612, ...}) = 0 其实这里就减少了系统调用。当nginx访问量非常大的时候,这是很有帮助的。而且open_file_cache不仅仅用于返回的静态文件,它对所有的打开文件类型操作都是有效的,不管是日志文件还是缓存文件等等。所以要确认我们的使用环境如果资源文件经常被nginx以外的进程经常访问修改发生变化,那么需要正确设置超时时间。(当我们的文件频繁的被nginx以外的进程修改时,那么需要保证其超时时间是合理的,被业务场景可以接收的)