Nginx 多进程并发写同一文件

Nginx 多进程并发写同一文件

  • 前言
  • 背景知识
    • 进程与文件句柄
    • 系统调用与库函数
    • logrotate 文件切割
  • 总结
    • 存在的问题
    • 解决方案
  • 参考资料

前言

最近在开发一个自定义的 nginx logger 模块,准备替代 ngx.log,但在开发过程中遇到了一些问题,进而查阅相关资料,最终得出一些有关 “多进程读写同一文件” 的潜在问题与结论。

背景知识

进程与文件句柄

Nginx 为 master-worker 进程模型,worker 进程通过 COW(Copy-On-Write) fork master 进程的内存空间,其中就包括进程表项:

Nginx 多进程并发写同一文件_第1张图片

Nginx 多进程并发写同一文件_第2张图片

对于在 init 阶段由 master 创建的文件句柄,通过写时复制,各 worker 将会拥有与 master 进程相同的文件句柄(文件指针)。

对于这个文件偏移量,有几点需要搞清楚:在用 open() 函数打开文件时如果没有加上 O_APPEND 标志,那么这个文件表的文件偏移量为 0;加上的话,它会把 V 节点中的当前文件长度赋给文件偏移量;写完文件之后(没有关闭文件描述符),文件长度会变化,相应的当前偏移量也随着文件长度的变化而变化。这里需要注意,是写完一个文件之后(也就是 write() 函数执行完之后),偏移量才会改变。

到这里的话,基本上就清楚了:如果写操作是一个原子操作的话,那么父子进程(本质是不同进程享有同一文件指针)写同一个文件不会出现任何问题;如果不是原子操作的话,有可能在父进程的 write() 函数没有返回之前又执行了子进程的 write() 函数,由于当前文件偏移量没有改变,所以会覆盖掉原先内容。Unix操作系统提供了一个原子操作的方法,那就是打开文件的时候设置 O_APPEND 标志。这样做可以使得内核在每次写操作之前将进程的当前偏移量设置到该文件的末尾。

有用结论:

  1. 只要是在 init 阶段被创建的文件句柄,将被所有 woker 共享,即所有 worker 拥有同一个文件句柄(指针)。
  2. 想要拥有同一文件句柄的不同进程对文件并发写入不出现问题,需要写入操作是原子的。
  3. 想要拥有不同文件句柄的不同进程对文件并发写入不出现问题,需要(设置文件偏移量 + 写入)操作是原子的,可以通过设置文件句柄的 O_APPEND 标志 来实现。

系统调用与库函数

主要注意下面几个点:

  1. 库函数是封装了系统调用的函数,系统调用比库函数更底层。
  2. 所有系统调用都是原子操作,而库函数不一定(大部分都不是)。
  3. C 语言的 write() 函数是系统调用,而 fwrite() 是库函数。lua IO 库的 write() 也是库函数(底层调用 C 的 fwrite())。

有用结论:

  1. 对于文件操作而言,库函数与系统调用最大的区别是:
    1. 库函数会先写缓冲区,再写磁盘,而系统调用不写缓冲区。
    2. 系统调用是原子的,库函数不是。

logrotate 文件切割

In order to rotate log files, they need to be renamed first. After that USR1 signal should be sent to the master process. The master process will then re-open all currently open log files and assign them an unprivileged user under which the worker processes are running, as an owner. After successful re-opening, the master process closes all open files and sends the message to worker process to ask them to re-open files. Worker processes also open new files and close old files right away. As a result, old files are almost immediately available for post processing, such as compression.

有用结论:

  1. Nginx 各进程(master 与 worker)是通过 reopen 文件来更新文件句柄的。

总结

存在的问题

  1. 持有相同文件句柄的不同进程并发写文件,如果写操作不是原子的,会出现问题。
  2. 持有不同文件句柄的不同进程并发写文件,哪怕写操作是原子的,也可能出现问题。
  3. Lua IO 库并不是原子的,写日志有可能被覆盖。
  4. Nginx 通过 USR1 信号 reopen 文件,仅 reopen Nginx 自身打开的文件,不 reopen 用户打开的文件。

解决方案

如果要使用自定义的日志模块:

  1. 在 init 阶段创建文件句柄,通过 Nginx 的进程模型与操作系统的 COW 使得所有 worker 进程持有相同文件句柄。或使用追加模式打开文件,利用操作系统追加写入文件的原子性。
  2. 通过 LuaJIT FFI 调用 C 的 write(),实现原子写操作。
  3. 切割日志时不再发送 USR1 信号,直接发送 HUP 信号,进行 reload。

参考资料

https://github.com/openresty/lua-nginx-module/#init_by_lua
http://lua-users.org/lists/lua-l/2011-04/msg00932.html
http://nginx.org/en/docs/control.html
https://ms2008.github.io/2016/09/21/openresty-log/
https://blog.csdn.net/wishfly/article/details/533289
https://zhuanlan.zhihu.com/p/129098006
https://blog.csdn.net/wk_bjut_edu_cn/article/details/80314780
https://blog.csdn.net/zy010101/article/details/84202404
https://blog.csdn.net/weixin_34362991/article/details/92041042

你可能感兴趣的:(网络编程,nginx)