1 问题描述
开发修改线上日志文件xxx.log,该文件由tomcat实时在写入,执行:
$ sudo sed -i 's/十一月/Nov/g' xxx.log
这行脚本的目的是将日志中的“十一月”替换为“Nov”,执行结束后导致java应用没有再持续写追加任何日志。
2 What happened
2.1 sed in place edit原理
查看sed manpage:
$ man sed
-i[SUFFIX], --in-place[=SUFFIX]
edit files in place (makes backup if extension supplied). The default operation mode is to break
symbolic and hard links. This can be changed with --follow-symlinks and --copy.
从中可以得知 sed -i 命令会破坏硬链接和软链接,可以自己测试一下:
$ cat f.txt
aaaaabbbbbb
$ stat f.txt
File: `f.txt'
Size: 12 Blocks: 8 IO Block: 4096 regular file
Device: 815h/2069d Inode: 132413 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 501/ tong) Gid: ( 501/ tong)
Access: 2013-11-10 18:05:24.763863104 +0800
Modify: 2013-11-10 18:05:10.882862432 +0800
Change: 2013-11-10 18:05:10.882862432 +0800
$ sed -i 's/a/b/g' f.txt
$ cat f.txt
bbbbbbbbbbb
$ stat f.txt
File: `f.txt'
Size: 12 Blocks: 8 IO Block: 4096 regular file
Device: 815h/2069d Inode: 132406 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 501/ tong) Gid: ( 501/ tong)
Access: 2013-11-10 18:06:05.677865084 +0800
Modify: 2013-11-10 18:06:02.284864920 +0800
Change: 2013-11-10 18:06:02.285864920 +0800
可以看到,在sed修改f.txt之前,用命令 stat f.txt 显示该文件的 Inode: 132413,而在sed修改之后变成了 Inode: 132406. 这意味这 sed -i 命令实际上删除了原文件,生成了一份同名的修改过后的backup(inode会在下面介绍)。
进一步google “sed -i” 发现,该操作不是真正的 “edit in place”,而是会创建临时文件,之后将临时文件替换原文件,同时调用unlink方法将原文件删除:
sed creates these temporary files when the "in place" (-i) option is turned on. In the normal course, sed actually deletes the files (that is what happens in cygwin) using a call to the 'unlink' library.
sed -i will unlink the file it modifies, so syslog/postfix will continue writing to a nonexistent file.
2.2 unlink打开的文件
介绍相关内容前,先了解下inode是什么。
在linux文件系统中,一个文件的存储由两部分组成,分别是inode和block块。其中block块存储文件的内容,而inode存储的信息包括:文件字节数,文件拥有者的User ID,文件的Group ID,文件的权限,链接数(即有多少文件名指向这个inode),时间戳,block位置等信息。用上文的 stat filename 命令可以查看inode相关信息,这里和本文密切相关的是链接数。
一个inode对应一个文件,但是可以对应多个文件名(有多个链接数),可以用 ln 命令建立链接,用 ls -li 命令查看链接次数变化:
$ ls -li
132406 -rw-rw-r-- 1 tong tong 12 Nov 10 18:06 f.txt
$ ln f.txt f_1.txt
$ ls -li
132406 -rw-rw-r-- 2 tong tong 12 Nov 10 18:06 f_1.txt
132406 -rw-rw-r-- 2 tong tong 12 Nov 10 18:06 f.txt
上面的输出中,132406为inode号,唯一标识了一个inode,可见两个文件名都指向了一个inode。
unlink 操作就是解除一个文件名和inode的对应关系:
$ unlink f_1.txt
$ ls -li
132406 -rw-rw-r-- 1 tong tong 12 Nov 10 18:06 f.txt
当unlink一个打开的文件会发生什么呢?查找unlink系统调用函数,有如下介绍:
unlink() deletes a name from the filesystem. If that name was the last link to a file and no processes have the file open the file is deleted and the space it was using is made available for reuse.
If the name was the last link to a file but any processes still have the file open the file will remain in existence until the last file descriptor referring to it is closed.
也就是说,当文件只有一个链接且没有在其它进程中打开时,unlink操作会删除该文件并释放空间;如果文件只有一个链接,但是unlink操作时该文件在另外一个程序中打开,那么只有当程序操作完成时,该文件才能被删除,即删除操作被postpone了。
因此放在本文的case当中,当对 xxx.log 执行 sed -i 操作后,生成了一个新的xxx.log(inode不一样),而旧的xxx.log被unlink,但直到java应用完成对当天日志的写操作后,旧日志才被删除(也就是日志白写了),从而之后看到的xxx.log没有追加新的内容。
3 Tips1: Finding an unlinked open file
如何查找一个已经被打开,但同时又没有链接数的文件?用 lsof +L1 命令,示例(beta环境):
$ ls -li rt.log
3408701 -rw-r--r-- 1 tomcat tomcat 1271800888 Nov 10 23:26 rt.log
$ sudo unlink rt.log
$ sudo lsof +L1
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
java 20664 tomcat 45w REG 252,7 1272466559 0 3408701 /home/q/www/testapplication/logs/rt.log (deleted)
其中NLINK表示链接数,该命令的含义大家可以查看 man lsof.
4 Tips2: Retriving an unlinked open file
假如删除了一个正打开的文件,比如正在写入的日志,有没有办法找回来呢?答案是肯定的,接着Tips1的操作,现在来找回rt.log中的内容:
$ sudo cat /proc/20664/fd/45 > ~/rt_back.log
上例中,20664为之前打开rt.log的PID,45为rt.log在PID中打开的文件句柄。
5 总结
如果再次遇到修改正在写入的日志该怎么做? 暂时想到三种方法,有更好的办法欢迎补充:
1. 用sed -ic xxx.log,其中 -c 选项可以不用破坏文件的软硬链接,修改之后inode不会变,但前提是确保 sed 版本支持 -c 功能;
2. 修改日志文件之前 stop tomcat应用,修改完之后再重启,该方法会影响线上服务,特别是对于单服务而言,不推荐;
3. 等待tomcat写完当天的日志后再进行修改,这种方法最稳妥,但是时效性差。