问题症状与解决方法
Oracle服务器由于无法上外网,所以做了个crontab的定时任务,每天定时和内部的一台ntp服务器同步。但没过两周,时间又不准了,差了近10秒。
首先排除了这个时间差是在凌晨同步完后的几小时内造成的。以“ntpdate crontab”作为关键字搜索,很容易找到了原因:坑爹的crontab重置了PATH环境变量,所以执行ntpdate命令的时候报“command not found”。
解决方法也很简单:通过whereis ntpdate的命令查出ntpdate的位置,改为完整路径调用,即“/usr/sbin/ntpdate”。另外作为以防万一,也将同步间隔从1天缩小到1小时。
为了避免下次栽坑,我们需要知道crontab到底设置了哪些PATH环境变量。
通过“vi /etc/crontab”打开文件,可以看到如下的内容:
1 | SHELL=/bin/bash |
可以看到PATH里明明是包含/usr/sbin,也就是ntpdate的路径。那么为什么依然会报“/bin/sh: ntpdate command not found”的错误?
(另外一个疑点是看起来是用/bin/bash执行的,为什么报/bin/sh)
crontab ≠ crontab
这里是比较容易混淆的地方:
- crontab -e是用户任务调度的命令
- /etc/crontab是系统任务调度的配置文件
用户任务调度
每个用户可以通过crontab -e创建自己的定时任务调度。创建的任务会放到/var/spool/cron/{用户名}的文件里。
系统在启动的时候会由/etc/init.d启动crond守护进程(参考资料: daemon to execute scheduled commands - Linux man page
https://linux.die.net/man/8/crond))。crond会将/var/spool/cron目录下的crontab文件载入内存,并在每分钟检查是否有需要执行的任务。(我怀疑就是这个机制导致Linux的定时任务的最小间隔是分钟而不是秒。crond的任务如果执行完没关闭的话,会残留下来很多crond进程。如果任务写得有问题的话可能会把进程数都吃光,导致无法ssh登录。)
输出会通过系统内邮件发给对应的用户。
我们可以通过执行一个“echo $PATH”的定时任务,查看cron的PATH环境变量。结果如下:
1 | PATH=/usr/bin:/bin |
没有ntpdate所在的/usr/sbin目录。这就能解释“command not found”这个报错的原因了。
系统任务调度
/etc/crontab文件也是由crond守护进程扫描并调用的。但差别在于3点:
- /etc/crontab文件中额外增加了/sbin和/usr/sbin两个目录
- 通过/bin/bash执行
- 可以执行的用户
我理解/etc/crontab文件的意义在于: - /etc/passwd中某些用户是不允许登录的系统账号(例如mysql用户)。虽然也可以通过crontab -u配置,但总不如汇总在一个文件里查看起来方便
- 方便添加公共环境变量。通过crontab -e执行需要制定环境变量的命令的时候,需要在命令前先添加一个export。而这些环境变量可以直接写在/etc/crontab文件的顶部
想起来之前sudo的时候也在环境变量的时候栽过坑,所以也总结一下环境变量相关知识。
环境变量层级
对于每个进程来说,环境变量保存在/proc/$PID/environ这个文件里($PID是进程号)。每个环境变量的键值对之间是通过\x0这个字符分割的,所以可以通过如下的命令打印当前进程用到的环境变量:
1 | sed 's:\x0:\n:g' /proc/$PID/environ |
这些环境变量是由多个层级的环境变量值拼成的。
环境变量分为几个层级:
- 全局
- 用户级(Per User)
- 会话级(Per Session)
全局
全局的环境变量主要在两个文件里:
- /etc/environment:推荐加在这个文件里
- /etc/profile:只针对登录的shell有效
此外,bash命令也会自带一些环境变量。/etc/locale.conf文件里也带有一个LANG=”en_US.UTF-8”的环境变量。
之前遇到过一些登录SSH可以成功执行的命令,通过gitlab ci无法执行。原因主要也是因为依赖于一些只在/etc/profile中出现的变量。
用户级
某个变量可能只有某个用户才需要。这种时候就需要用户级别的。用户级的变量保存在/.bashrc和/.bash_profile等文件中(表示用户的home目录)。另外也可以看到/.bash_profile其实就是调用/.bashrc。/.bash_profile文件如下:
例如要在用户的PATH里添加一个目录/home/my_user/bin,可以修改
1 | export PATH="${PATH}:/home/my_user/bin" |
然后通过source ~/.bash_profile命令更新变量。
会话级
有些时候可能只是想在某次登录会话期间,让环境变量临时生效。这时候就靠export命令:
1 | export PATH="${PATH}:/home/my_user/tmp/usr/bin" |
像pwd命令读取的就是用户当前会话中所在路径。
sudo
sudo需要专门拎出来说,是由于之前遇到过一个坑:
在一次pip安装lib的时候发生过,有一个命令在root下能成功执行的命令,普通用户sudo执行却会失败。
最终发现的原因是sudo下的PATH环境变量被重置成一个最小化的子集了。可以用文本编辑器打开/etc/sudoers文件,找到”secure_path”那一行:
1 | Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin |
而那个命令是在/usr/local/bin(忘了还是/usr/local/sbin了)下,自然就执行失败了。
更多可以参考这篇:技术|Linux有问必答:如何为sudo命令定义PATH环境变量。但我不推荐文中修改secure_path的做法。sudo的secure_path这么设置自然有其安全上的考量。改为完整路径调用或通过export临时添加环境变量即可。
参考资料
这篇讲环境变量的比较全。虽然是ArchLinux的Wiki,但对于其他Linux的发行版也基本通用。
Environment variables - ArchWiki
为什么虚拟机的时钟会产生偏差,这篇文章从原理上解释了原因。(话说Docker就不存在这个问题,容器里想改时间也不能改,要错一起错)
奔跑在虚拟化大路上的你 请看一看路边的荆棘 - Netis