Flynn's Studio

线程安全的无锁日志

Word count: 736Reading time: 2 min
2024/02/02

因为是同步写入,所以不具有在生产环境中的实际应用价值,只是基于对无锁写入方案的一个探讨。
具体源码可见:Fly-Log

日志系统应该满足的要求

  1. 高效,作为一个日志系统,不应该占据太多资源
  2. 简洁,尽量不要引入太复杂的依赖(log4cpp库),给系统开发带来难度
  3. 线程安全,服务端的各个线程都能同时读写日志
  4. 轮替问题,如果半年到一年的日志放到一个文件会导致文件过大
  5. 及时保存,程序故障导致异常退出,如果日志还留在缓冲区就会导致丢失

难点问题及其解决

当我选择弃用log4cpp等庞大的库的时候,就意味着需要自己去解决3-5等问题。

线程安全

解决线程安全应当考虑不引入锁,因为加锁会带来复杂性和性能问题,所以应当考虑更高效的解决方案。

  1. 使用O_APPEND方式打开文件,这个标记让write写出的内容添加到文件末尾,移动文件指针与输出内容是原子的,由操作系统来保证原子性。因此这个标记保证在多线程/多进程调用write也能够保持输出的内容不会相互覆盖错乱,nginx的日志也利用了这个标记来达到多进程不干扰。
  2. 每一次log,都会生成包括了时间的最终输出字符串,调用write,写出到日志系统的文件描述符fd。当write返回时,日志已经写到操作系统,不管程序是否崩溃,只要操作系统不崩溃,那么输出的内容就会保存到日志文件中。

轮替问题

有锁方案

轮替的过程中,需要关闭当前文件并打开新文件,让新的内容写到新文件中,在多线程环境下就需要锁来同步所有线程的日志输出操作,避免写入到不合法的文件描述符中。

无锁方案

可以使用posix中的dup2来实现无锁轮替文件。

1
2
3
4
5
6
7
8
//轮替时,首先重命名已打开的日志文件,保持打开状态,
rename(filename, newname);
//然后创建新的日志文件
fd = open(filename,...);
//使用dup2系统函数把fd(新)复制到fd_(旧)上
dup2(fd, fd_);
//关闭fd(新)
close(fd);

其中dup2是原子操作,它会关闭fd_并且把fd_也指向fd打开的文件。因此fd_这个文件描述符总是保持打开状态,并且值不变,但是前后指向了不同的文件,完全不会影响其他线程调用write(fd_, …)等操作。另一边write也是个原子操作,它与dup2不会交叉进行,因此保证了日志系统的正确性。

CATALOG
  1. 1. 日志系统应该满足的要求
  2. 2. 难点问题及其解决
    1. 2.0.1. 线程安全
    2. 2.0.2. 轮替问题
      1. 2.0.2.1. 有锁方案
      2. 2.0.2.2. 无锁方案