文档

如何避免多进程/多客户端并发写同一日志文件可能出现的异常?

更新时间:

问题现象

文件存储 NAS(Apsara File Storage NAS)为多客户端提供了统一名字空间的文件共享读写能力,但在多进程/多客户端并发写同一个文件的场景中(典型的例如并发写同一个日志文件),各进程分别维护了独立的文件描述符及写入位置等上下文信息,而NFS协议本身并没有提供Atomic Append语义的支持,因此可能会出现写覆盖、交叉、串行等异常现象。

解决方案

  • (推荐)不同进程/客户端写入同一文件系统的不同文件中,后续分析处理时再进行归并,这个方案能够很好地解决并发写入导致的问题,同时无需使用文件锁,不会对性能造成影响。

  • 对于并发追加写同一个文件(如日志)的场景,可以使用文件锁+seek机制来保证写入的原子性和一致性。但是文件锁+seek是一个比较耗时的操作,可能会对性能产生显著的影响。下面将对这种方式进行简单介绍,以供参考。

flock+seek使用方法

由于NFS协议本身没有提供对Atomic Append语义的支持,因此当并发写入同一文件末尾(如日志)时,很可能会出现相互覆盖的情况。在Linux中,通过使用flock+seek的方式,可以在NFS文件系统上做到模拟Atomic Append,对并发追加写入同一文件提供保护和支持。

使用方式如下:

  1. 调用fd=open(filename, O_WRONLY | O_APPEND | O_DIRECT) 以追加写的方式打开文件,并且指定O_DIRECT(直写,不通过Page Cache),获得文件描述符fd。

  2. 调用flock(fd, LOCK_EX|LOCK_NB) 尝试获取文件锁,如果获取失败(如锁已被占用)则会返回错误,此时可以继续重试或进行错误处理。

  3. 文件锁获取成功后,调用lseek(fd, 0, SEEK_END) 将fd当前的写入偏移定位到文件末尾。

  4. 执行正常的写操作,此时写入位置应该是文件的末尾,并且由于有文件锁的保护,不会出现并发写入相互覆盖的问题。

  5. 写操作执行完成后,调用flock(fd, LOCK_UN) 释放文件锁。

下面是一个简单的C语言示例程序:

#define _GNU_SOURCE
#include<stdlib.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<sys/file.h>
#include<time.h>

const char *OUTPUT_FILE = "/mnt/blog";
int WRITE_COUNT = 50000;

int do_lock(int fd)
{
    int ret = -1;
    while (1)
    {
        ret = flock(fd, LOCK_EX | LOCK_NB);
        if (ret == 0)
        {
            break;
        }
        usleep((rand() % 10) * 1000);
    }
    return ret;
}

int do_unlock(int fd)
{
    return flock(fd, LOCK_UN);
}

int main()
{
        int fd = open(OUTPUT_FILE, O_WRONLY | O_APPEND | O_DIRECT);
        if (fd < 0)
        {
                printf("Error Open\n");
                exit(-1);
        }
        for (int i = 0; i < WRITE_COUNT; ++i)
        {
                char *buf = "one line\n";

                /* Lock file */
                int ret = do_lock(fd);
                if (ret != 0)
                {
                        printf("Lock Error\n");
                        exit(-1);
                }

                /* Seek to the end */
                ret = lseek(fd, 0, SEEK_END);
                if (ret < 0)
                {
                        printf("Seek Error\n");
                        exit(-1);
                }

                /* Write to file */
                int n = write(fd, buf, strlen(buf));
                if (n <= 0)
                {
                        printf("Write Error\n");
                        exit(-1);
                }

                /* Unlock file */
                ret = do_unlock(fd);
                if (ret != 0)
                {
                        printf("UnLock Error\n");
                        exit(-1);
                }
        }
        return 0;
}

更详细的flock()使用方式,请参见Linux file locking mechanisms - Flock, Lockf, and Fcntl

说明

在NAS文件系统上使用flock()需要您的Linux内核版本在2.6.12及以上,如果您的内核版本较低,请使用fcntl()调用。