crontab ntpdate同步失败与Linux环境变量

问题症状与解决方法

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
2
3
4
5
6
7
8
9
10
11
12
13
14
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root

# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed

可以看到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。
例如要在用户的PATH里添加一个目录/home/my_user/bin,可以修改
/.bash_profile文件如下:

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

本文永久链接 [ https://galaxyyao.github.io/2019/05/23/crontab-ntpdate同步失败与Linux环境变量/ ]