多线程下写日志
鄙人最近遇到了一个奇特的线上事故,记录一下,以备记忆。
鄙人所在的部门负责给公司提供各种基础库,即基础架构部门。最近某别的部门用本部门提供的支持多线程版本的日志库后,出现这样一个奇特的现象:当磁盘被日志写满以后,他们的数据文件的头部被写上了最新的日志!就是说,别的部门的程序的数据文件被日志数据给污染了。
这里先不介绍这个事故的原因。先说下这个日志库的写日志过程,其流程大致如下:
step1 如果log的fd为-1,就重新通过C函数open再打开一个log_fd;
step2 写log内容,即ssize_t size = write(log_fd, buf, len);
step3 如果size等于len,则转step 7,否则继续step 8;
step4 对log_fd加mutex锁;
step5 close(log_fd);
step6 log_fd赋值为-1;
step7 解锁;
step8 return。
然后用户进程的逻辑大致为:
logic1 当有新数据的时候,通过C函数open打开一个data_fd;
logic2 write(data_fd, data, data_len);
logic3 close(data_fd)。
假设这样一个场景:有线程A先写了一条日志,线程B紧随其后也来写一条日志,线程C正常进行数据文件写操作。通过假设一下情况,在磁盘满的时候可能复现这个bug。假设程序运行过程是:
过程1 线程A执行到step6,此时log_fd为3,线程A给log_fd赋值为-1;
过程2 线程B执行到step2,此时log_fd仍然为3,但其实线程A已经将fd为3的文件关闭掉了,所以线程B持有的这个log_fd此时为非法描述符;
过程3 线程C执行到logic2,因为线程A已经将fd 3归还给os,所以os此时把3给了data_fd,即data_fd为3。
如果这个假设成立,那么在过程线程B的log内容一定会写到数据文件中。
为了验证这个情况,先在公司的linux服务器建立了一个loop文件系统,简历步骤如下:
1 dd if=/dev/zero of=/home/alex/100.img bs=1M count=100 # 在这一步建立一个容量为100M的loop设备
2 mkfs.ext2 100.img # 将loop设备格式化为ext2格式
3 mount 100.img /home/alex/vfs -o loop -n # 简历loop设备,将/home/alex/vfs目录映射到这个loop设备上
由于设备的容量非常小,上面的bug就很容易重现,重现逻辑很简单,首先设置log的文件目录是/home/alex/vfs,然后启动1000个线程,每个线程的逻辑函数如下:
void* thread_func(void *arg) { char log_file_name[32]; sprintf(log_file_name, "/tmp/%d", (int)(int_ptr)(arg)); //data file name for (int idx = 0; idx < 10000; idx++) { int fd = open(log_file_name, O_CREAT | O_APPEND | O_RDWR, 0666); log_debug("1234567890qwertyuiopasdfghjklzxcvbnmQWERhello, %d", (int)(int_ptr)(arg)); close(fd); } pthread_exit(NULL); return NULL; } #define MAX_THEAD_NUM 1000 int main() { // set log path: /home/alex/vfs/ pthread_t id[MAX_THEAD_NUM]; for (int idx = 0; idx < MAX_THEAD_NUM; idx++) { pthread_create(id + idx, NULL, thread_func, (void*)(intptr_t)(idx)); } for (idx = 0; idx < thread_num; idx++) { pthread_join(id[idx], NULL); } return 0; }
上面的线程函在循环里不断的打开关闭数据文件,即其大小为0。程序运行完毕后,数据文件的大小若不为0,就能验证上面的假设。
运行以上测试程序后,观察到大量的数据文件的大小不为0,其内容为log内容。其实这个事故还算幸运,若用户的数据不是输出到磁盘,而是通过tcp socket发送到别的机器,那么这个bug会导致tcp的流内容莫名其妙的乱掉,那问题就更不容易追查了。
验证了假设,问题就很容易解决了。解决方法有两种:
第一种方法,对fd加引用计数。即线程每获取一个新的log_fd时候,要增加一次引用次数;如果出现write失败的情况,要减小一次引用次数,当引用次数为0的时候,再close掉log_fd;
第二种方法,使用tls(thread local storage)形式的log file。即每个线程都打开同一个log file,open函数的“mode参数”为“O_CREAT | O_APPEND | O_RDWR”,当写失败的时候,各自close各自的log_fd。
最终我采用了第二种解决方法,很好地解决了问题。如果你有更好的方法,欢迎评论。
此记,谢绝转载。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。