[摸不着头脑.JPG]
== 1 ==
说来也巧,7月6号在这里发了一篇 Linux 下删点日志也能搞死人,讲了下删文件可能存在的一些坑,到了第二周、12号,一个 B 厂(此 B 厂非鄙厂)的朋友问了我个问题:
“用 sed 处理文本文件,想要只保留最后 1k 行,怎么写比较合适?”
[黑人问号.JPG]
这问题让我觉得有点想吐槽,但我还是忍住了,我说这用 tail 不就好么?
但他的要求是,得直接修改原文件。
所以他想到了 sed 的 -i 参数(--in-place,即原地修改)。
还有一个原因是,之前用过类似这种方式来输出第 6 ~ 8 行:
$ sed -ne "6,8p"
虽然我们可以用 $ 来替换 8 ,表示输出到文件末尾,但 sed 又不支持 "$-1000,$" 这种写法,有点尴尬。
当然,变通的办法不是没有,比如我们可以这样计算出起始行的行号:
echo `wc -l $filename | awk '{print $1}'` - 1000 | bc
只不过看起来一点都不优雅。
注:stackoverflow上某位大佬竟然有个只用 sed 就能解决的写法,但我没看懂,就不写了,反正一样也不优雅。
== 2 ==
于是就像很多pen友吐槽我那篇里的面试题一样,我说:
[你这需求很不合理啊.JPG]
他解释说,线上部署的 EMR 服务托管了很多开源组件,各个组件的日志都不是很规范,需要用一些外围脚本来做清理,日常是用 logrotate 做清理;但偶发的异常,可能会导致某个组件在短时间内输出大量日志、打爆磁盘。
—— 所以其实原始需求是清理过度膨胀的日志。
根据前文所述,线上清理日志是需要注意的 —— 否则可能会导致文件虽然删了,但是磁盘空间并不释放的情况。
但 sed -i 是否会导致这种情况呢?
== 3 ==
由于文件在 fs 里的存储类似数组(或者很多数组串成的链表,类似C++的 deque),而 sed 的操作可能会需要修改任意位置,因此从逻辑上来说,直接修改原文件并不合适;不过稳妥起见还是要确认下。
打开 sed 的源码,我们可以看到:
static void open_next_file (const char *name, struct input *) {
...
if (in_place_extension) {
...
output_file.fp = ck_mkstemp (&input->out_file_name, tmpdir, "sed", write_mode);
...
}
static void closedown (struct input *) {
if (in_place_extension && output_file.fp != NULL) {
...
ck_rename (target_name, backup_file_name, input->out_file_name);
...
}
大概意思是,当开启了 -i 选项时,会新建一个临时文件,并在处理结束的时候将它重命名为原文件,这和前面的推测一致。
ck_rename 函数里调用了 glibc 的 rename,然鹅 rename 的文档没有明确提及被替换的文件会怎么样。
文档不够,实操来凑:
在终端 1 打开一个文件
$ echo 123456 > 1.txt
$ python
>>> f = open("1.txt", "w")
在终端 2 用 sed 原地修改该文件(不做备份)
# @Terminal#2
$ sed -i'*' -e 's/345/666/' 1.txt
$ cat 1.txt
126666
观察 python 打开的文件:
$ readlink /proc/723/fd/3
/tmp/1.txt (deleted)
注:723 是 python 的 pid;进程打开的第一个文件 fd 通常是 3(0/1/2对应stdin/out/err)。
[果然是坑.JPG]
== 4 ==
既然此路不通,只能改道。
解决方案倒是很简单粗暴:线上既然已经部署了 logrotate ,还是考虑用它做日志切分。
虽然 logrotate 并不支持按日志尺寸切割,不过加个外挂实现这一点并不难,定时检测日志大小,超过阈值后主动调用 logrotate --force 就好了。
大概像这样:
size=`stat -c%s $logfile`
if [ $size -gt $threshold ]; then
logrotate --force $CONFIG_FILE
fi
注:这几句只是示例,没有实际测试过。
== 5 ==
技术问题倒是解决了,但值得注意的是,这有个日常出现得很频繁的沟通问题,其模式是:
小 A 遇到了问题 X。
为了解决 X,他想到了方案 S。
在实现 S 的过程中,他发现困惑 P。
于是他问小 B :“P是什么?” 或 “要如何处理 P?”
然鹅接下来的沟通常常是这样
B 问:为什么要处理P?
A 答:因为想实现 S。
B 问:为什么要实现S?
A 答:因为遇到了问题 X。
[熊猫头捂脸.JPG]
我在 Lark 上搜索“背景是什么”,平均每个月都能看到好几条,其中很多都是类似的场景。
这种模式显著增加了沟通成本,尤其是在 IM 这种 RTT 比较长的场景。
更糟的场景是,有时候 S 并不是解决 A 的合适方案、甚至是错误方案,本文所述例子就是一个典型。
如果在提问时,能够带上问题的背景(视实际场合简述或详述),就能有效降低沟通成本。
== 6 ==
本篇就到这里,小结一下:
删除被打开的文件不会释放对应磁盘空间;
sed 的 in-place 其实是新建临时文件,再用 rename 替换原文件;
rename 操作相当于删了目标文件;
日志的切割还是适合用 logrotate ,--force 参数有时会很有用;
沟通的时候注意描述问题的背景,可以降低沟通成本。
感谢阅读以及对表情包的脑补,本文完。
推荐阅读: