在日常运维中经常会遇到一些莫名的故障,有时候甚至没有人会告诉你故障前到底做过哪些异常操作,这时候就需要借助History记录的历史命令去分析,一定情况下History可以快速定位问题原因。
History是Shell的内置命令,可通过系统默认Shell的man手册查看。执行history命令将显示在终端输入并执行的历史命令,系统默认保留1000条。与History相关的环境变量有很多,其中HISTFILE和HISTSIZE最为重要: HISTFILE用于指定存放历史命令文件的位置,默认保存在~/.bash_history(用户家目录下);HISTSIZE为历史命令记录的条数,默认1000。在每次session结束之后,系统会将本次不超过HISTSIZE的执行命令(如超过则选择最后HISTSIZE执行的命令)保存到该用户设置的HISTFILE文件里。
HISTFILE和HISTSIZE涉及全局环境,相关调整需要特别慎重,下面就介绍近期一个由于该文件设置不当引发的生产问题。
一、问题介绍
某日早上监控发现部分数据库服务器出现归档日志为关闭状态,无法获取归档日志使用率的报警。系统部门随即展开排查:
(1)登陆异常服务器出现提示性警告信息
-sh: /dev/null:Permission denied
-sh: /dev/null:Permission denied
-sh: /dev/null:Permission denied
-sh: /dev/null:Permission denied
-sh: /dev/null:Permission denied
-sh: /dev/null:Permission denied
-sh: /dev/null:Permission denied
检查系统日志发现存在/dev/null is not a device的报错
2018-05-21T04:06:51.094710+08:00xxxx systemd[1]: /dev/null is not a device.
对/dev/null文件核对,发现该文件类型和权限均发生改变
-rw------- 1 root root 0 May 21 07:20 /dev/null
正常情况下/dev/null为字符设备文件(c),权限666.
crw-rw-rw- 1 rootbin 1, 3 May 25 22:16 /dev/null
(2)在服务器上执行lsof|grep “/dev/null”发现/dev/null被重命名为/dev/null-(根据设备号1,3判断)且该文件被删除但句柄未释放(/dev/null-均处于deleted状态),再次验证3)中/dev/null被修改;
java 40333 40390 sysop 0r CHR 1,3 0t0 1029 /dev/null- (deleted)
java 40333 40390 sysop 1w CHR 1,3 0t0 1029 /dev/null- (deleted)
java 40333 40390 sysop 2w CHR 1,3 0t0 1029 /dev/null- (deleted)
java 40333 40391 sysop 0r CHR 1,3 0t0 1029 /dev/null- (deleted)
java 40333 40391 sysop 1w CHR 1,3 0t0 1029 /dev/null- (deleted)
java 40333 40391 sysop 2w CHR 1,3 0t0 1029 /dev/null- (deleted)
java 40333 40392 sysop 0r CHR 1,3 0t0 1029 /dev/null- (deleted)
(3)删除异常服务器上/dev/null文件并通过mknod命令重建,重建后系统恢复正常。
/dev/null又称为空设备,它丢弃一切写入其中的数据,读取它则会立即得到一个EOF。系统服务和数据库程序都会使用该文件,若该文件出现异常,系统和应用程序均会受影响。
因此初步确认数据库异常是由于/dev/null文件类型和属性被修改导致。
二、原因初探
虽然问题原因已明确,但排查仍未结束,部分数据库服务器同时发生异常绝非偶然,如果找不到修改/dev/null的真凶,那么问题还会再次发生。
进一步分析发现异常服务器操作系统版本均为SuSE12SP3,随即对全量SUSE服务器进行排查,进一步验证上述结论。
结合异常服务器登录日志和/dev/null报错日志,异常前某固定IP曾实施过批量任务下发。通过与应用维护部门沟通,确认该IP每周日会例行执行脚本对纳管服务器进行用户抽取,以核对UCM系统用户纳管清单,而在问题发生前开发部门刚调整过用户抽取动作。
比对调整前后下发的脚本,发现调整后的脚本中增加了exportHISTFILE=/dev/null;export HISTSIZE=0。该命令的作用是将HISTORY日志记录到/dev/null中,并将HISTORY的记录条目设置为0。
进一步比对和测试发现,对于SuSE12SP3仅当用户使用的shell为sh时(sh为软链接,链接到/bin/bash),执行“exportHISTFILE=/dev/null;export HISTSIZE=0;”问题才会重现。
三、明确根源
本次问题具有很明显的操作系统版本指向,纳管服务器包含SuSE多个版本,但只有SuSE12SP3受到影响,不同版本具体有什么差异?带着这样的困惑系统专业人员重点对SuSE12SP3的新特性安排了分析,最终确认该问题为SUSE12SP3所使用的BASH版本特性引起。详细分析过程如下:
(1)SUSE12SP3之前的系统采用的BASH 4.2版本;SUSE12SP3使用BASH 4.3版本。BASH4.3版本对HISTORY日志的覆盖写入进行调整,从原先文件开始(0)处实现文件的覆盖写变为通过rename函数替换旧的history文件来实现覆盖写。
(2)虽然服务器上/bin/sh为软链接文件链接到/bin/bash,但是2种shell仍存在差异:使用sh将启用posix模式,即当执行sh时,和bash--posix相同。bash在默认模式下,保存history强制为追加写入(force_append_historybash=1); 而在posix模式下,不会强制设置追加写入(force_append_historybash=0), 但当新产生的history记录条目大于 HISTSIZE设置值,则将进行覆盖写入
(3)对于使用sh的用户在执行export HISTFILE=/dev/null;export HISTSIZE=0;后,新产生的的history记录条目一定会大于HISTSIZE设置值0(export本身也是一条命令),再加上sh使用BASH的posix模式,因此HISTORY日志将开启覆盖写入。对于BASH 4.3版本,命令执行结束后系统会将HISTORY的记录文件/dev/null进行rename并根据HISTORY的设置重新生成该文件(不会参考原文件的类型和权限),此时生成的/dev/null文件不再是设备文件,而是供HISTORY记录的普通文件(权限600)。
这也是SUSE12SP3环境/dev/null文件类型和权限发生变化的根本原因。
四、小结
本次问题是由于HISTFILE错误设置为/dev/null引起,且不同版本内核差异加剧了问题的复杂性。后续除对于新版本特性变化要更加深入研究外,还提出如下改进建议:
1)HISTFILE的调整需谨慎,切勿设置为系统文件,例如/dev/null,防止系统文件被覆盖或修改,保证系统正常运行。
2)下发脚本应针对不同操作系统版本进行充分验证后再在生产环境实施。