This repository has been archived on 2023-11-13. You can view files and clone it, but cannot push or open issues or pull requests.
blog/_posts/2023-02-21-cron_and_inotify.md
2023-06-03 15:58:09 +08:00

403 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
layout : post
title : "定时任务和实时文件监听"
subtitle : "cron VS inotify"
date : 2023-02-21 10:07:18
author : "Manford Fan"
catalog : false
header-img : "img/post-bg-universe.jpg"
tags :
- Cron
- Inotify
- Monitor
---
Cron服务我是一直有在使用的比如开机启动比如定时任务这在构建自己的平台的时候是非常常见的需求而且大多数情况下Cron服务表现的非常出色但并不是没有缺点和不适用的场景。Inotify tools是一个工具集里面包含三个常用的命令功能是实时监控指定的文件或者文件夹每当发生变化即使非常细微inotify也能及时的识别并报告出来。可以看出这两者差别还是挺大的这就意味着互补性挺强的。
## 一、Cron定时任务
`cron`服务是Linux系统元老级的服务进程了很多系统服务也会依赖于该服务。它的功能是固定周期定时循环执行某个命令或者脚本如果想要在未来某个时间执行一次动作可以考虑`at`命令,这个命令用法比较简单,使用`man`看下手册就知道该怎么使用,在这里就不做记录了。主要使用到的命令是`crontab`,本身的用法也比较简单,只不过有些规则比较绕,很容易设置完不生效。
`crontab`命令是用来为不同用户安装,卸载以及列出使用`cron`服务的工具,每个用户可以单独设定自己的`crontab`文件各个用户的文件都被放在`/var/spool/cron/crontabs`文件夹中Debian11系统, 一般情况下,十分不建议手动编辑。
### 1. 相关文件
- /etc/cron.allow: 记录**允许**使用`crontab`命令的用户,每行一个用户
- /etc/cron.deny: 记录**不允许**使用`crontab`命令的用户,每行一个用户
- 两个文件只存在其一,以存在的文件为准
- 两个文件都不存在,以系统设定为准,都不允许或者都允许,**root用户**始终被允许
- 两个文件都存在,则以`cron.allow`为准
### 2. 命令选项
|Option|Descrition|
|:-|:-|
|-e|edit编辑对应用户的crontab文件|
|-r|remove移除对应用户的crontab文件**谨慎使用**|
|-i|interactive配合-r使用删除文件前确认|
|-l|list列出对应用户的crontab文件内容|
|-u|user指定用户设定或者列出crontab文件内容|
### 3. 文件内容
`/var/spool/cron/crontabs`文件夹下的crontab文件内容分为三部分环境变量设定cron命令以及注释每一部分的内容都是以行为单位不可交叉环境变量一般放在文件开头部分。需要注意虽然crontab文件本身定义了一些环境变量详见命令的`man`手册但是最好还是自行将使用到的环境变量写在文件中以避免运行不生效另外该文件不会做通常的bash替换所以不能识别当前环境下的变量值。
```bash
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/var/lib/snapd/snap/bin:/root/bin"
SHELL="/usr/bin/bash"
# * * * * *
# - - - - -
# | | | | |
# | | | | +----- 星期几 (0 - 7) (Sunday=0 or 7, Sunday on this platform)
# | | | +---------- 月份 (1 - 12)
# | | +--------------- 几号 (1 - 31)
# | +-------------------- 小时 (0 - 23)
# +------------------------- 分钟 (0 - 59)
# at the beginning of every hour, update blog content & backup mysql
50 * * * * /usr/bin/bash /opt/scripts/sql_backup.sh
# at 02:10 of every day, do rclone sync things
10 2 * * * /usr/bin/bash /opt/scripts/rclone/rclone_sync.sh
10 2 * * * /usr/bin/bash /opt/scripts/hosts_update.sh
# at 03:20 of every Sunday, do backup things
20 3 * * 7 /usr/bin/bash /opt/scripts/backups.sh >> /opt/logs/backups.log 2>&1
# every half day, at 00:00 or 12:00, will do the transfermation between dos and unix format of all .md file
0 */12 * * * /usr/bin/find /opt/ -regextype posix-extended -regex ".*\.log|.*\.conf|.*\.sh|.*\.md" -exec /usr/bin/dos2unix {} \; > /dev/null 2>&1
# 00:05 of every day, do force reload rclone & alist service to refresh contents
5 0 * * * /usr/bin/bash /opt/scripts/rclone/rclone_alist_automount.sh -c
# 5 0 * * * /usr/bin/bash /opt/scripts/rclone/rclone_cloudreve_automount.sh -c
5 0 * * * /usr/bin/bash /opt/scripts/rclone/rclone_onedrive_automount.sh -c
# every mimute, check if there is any file name or number change in /opt/media.koel
# * * * * * /usr/bin/bash /opt/scripts/koel_update.sh
# every 10 mimutes, check if rclone & alist service is normal or not
*/10 * * * * /usr/bin/bash /opt/scripts/rclone/rclone_alist_automount.sh
# */10 * * * * /usr/bin/bash /opt/scripts/rclone/rclone_cloudreve_automount.sh
*/10 * * * * /usr/bin/bash /opt/scripts/rclone/rclone_onedrive_automount.sh
# do repo update @ 01:30 everyday
30 1 * * * /usr/bin/bash /opt/scripts/github_update.sh >> /opt/logs/github_update.log 2>&1
# reboot every Thursday 00:00
# 0 0 * * 4 /usr/sbin/reboot
# when reboot
@reboot /usr/bin/aria2c --conf-path=/etc/aria2/aria2c.conf -D
@reboot /usr/local/bin/jupyter lab --allow-root
@reboot /usr/bin/python3 /opt/source-code/calibre-web/cps.py
@reboot /usr/bin/qbittorrent-nox
@reboot /usr/bin/mount -t ext4 -w /dev/sdb1 /opt/webdav/wd
@reboot /usr/bin/bash /opt/scripts/jekyll_update.sh >> /opt/logs/jekyll_update.log
# @reboot /opt/source-code/clash/clash
# NO, because rclone depends alist.service
# @reboot nohup /usr/bin/rclone mount ALIST:/ /opt/webdav --allow-other --vfs-cache-mode full --vfs-cache-max-size 10G --vfs-read-ahead 100M --vfs-cache-max-age 4h --cache-dir /tmp/vfs-cache --bwlimit-file 20M --bwlimit 100M --log-file /opt/logs/rclone.log --log-level NOTICE --vfs-read-chunk-size-limit 10m --buffer-size 50M --attr-timeout 5m --transfers=6 --multi-thread-streams=6 --allow-non-empty > /dev/null 2>&1 &
# # acme auto renew cert
43 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
```
### 4. cron命令书写规则
如上述的文件实例内容cron命令的格式为`时间周期 + 命令`,命令不需要多解释,可以是单行命令,也可以是一个脚本的执行;对于时间周期,`crontab`提供了丰富的格式,以应对不同的使用场景。
```text
# * * * * *
# - - - - -
# | | | | |
# | | | | +----- 星期几 (0 - 7) (Sunday=0 or 7, Sunday on this platform)
# | | | +---------- 月份 (1 - 12)
# | | +--------------- 几号 (1 - 31)
# | +-------------------- 小时 (0 - 23)
# +------------------------- 分钟 (0 - 59)
```
通常情况下,周期时间是用五个数字来表示的,不设定的情况下是五个星(* * * * *),代表的含义以及对应的可选范围如上。根据不同的系统发行版,有些系统还支持使用星期和月份的缩写,来替代数字,比如`Mar`表示三月,`Fri`表示星期五等等。另外对于需要跟随系统启动,或者每天,每周,每月,每年运行一次的任务,还可以使用如下:
- @hourly :每小时运行一次,相当于"0 * * * *"
- @midnight :同@daily
- @daily :一天运行一次,相当于"0 0 * * *"
- @weekly :一周运行一次,相当于"0 0 * * 0"
- @monthly :一个月运行一次,相当于"0 0 1 * *"
- @annually :一年运行一次,相当于"0 0 1 1 *"
- @yearly :同@annually
- @reboot :重启时运行一次
除了如上指定的形式,`crontab`还支持更加多样化的规则设定,有些系统发行版支持执行列表和范围同时存在,比如"1,5, 10-40 * * * *",但是有些不支持,需要根据使用的系统来判断。
```bash
# 每5min打印一次hello
*/5 * * * * COMMAND
# 每8h执行一次命令00:0008:00和16:00
0 */8 * * * COMMAND
# 仅在01:0009:00和12:00执行命令
0 1,9,12 * * * COMMAND
# 在每年3-5月份的4号凌晨03:05 -- 03:30之间每分钟执行一次命令
5-30 3 4 3-5 * COMMAND
# 每小时的第一分钟第五分钟以及第10-40分钟每分钟执行一次命令
1,5, 10-40 * * * * COMMAND
```
### 5. 日志记录
一般情况下可以通过`cron`命令来设定需要记录日志的级别,如下图所示,可以使用`-L loglevel`参数来指定该服务需要记录日志的内容loglevel为设定的等级可有想要设定的内容代表的数字累加而得到。默认是level 1只记录开始的动作设定为0表示关闭服务的日志记录功能设定为15表示开启所有类型的日志记录相应的如果服务下有很多指令还是建议不要开启全日志记录会占用比较大的空间。
![cron_log_level](/img/posts/cron_log_level.png 'cron命令日志记录参数')
> **日志存放路径:`/var/log/syslog`**
## 二、Inotify实时文件监控
也不是最近的需求了,其实这个博客系统也是使用[Jekyll](https://jekyllrb.com/)构建的,每次有新的博文增加或者修改都需要手动运行一下`jekyll b -s ...`,虽然提供了`--watch --incremental`参数可以实时监控博文的更新修改情况,一种情况是修改***_config.yml***时,不生效,需要停止进程重新运行;另一种情况是,运行一段时间之后还是会发现某些博文没有被及时更新上线。直到最近在学习使用[Just the Docs](https://github.com/just-the-docs/just-the-docs)构建知识体系,发现实时监控文件变化的诉求更加迫切。
### 1. 简介
*inotify tools*是Linux内核2.6.13 (June 18, 2005)版本新增的一个子系统API它提供了一种监控文件系统基于inode的事件的机制可以监控文件系统的变化如文件修改、新增、删除等并可以将相应的事件通知给应用程序。这个工具集包含如下三个命令统计使用最多的是`initofywait`,因为在收集资料时发现,绝大部分现有的资料都是针对`inotifywait`,而且不能说完全相同吧,只能说绝大部分是*Copy & Paste*。
### 2. 命令使用
inotify-tools是一个C库和一组命令行组成的工具集提供了Linux下inotify工作的简单接口。也是一种强大的、细粒度的、异步文件系统监控机制它满足各种各样的文件监控需要可以监控文件系统的访问属性、读写属性、权限属性、删除创建、移动等操作因为调用的是内核的API可以监控文件发生的一切变化。
#### a. inotifywait
该命令的作用就是使用`inotify`接口监控文件或者文件夹的变化特别适合用在shell脚本中它可以被设定成触发一次之后退出也可以被设定成持续监控并输出触发的结果。
> **支持的选项**
|Options|Descriptions|
|:-|:-|
|@<file>|监控文件夹时,排除部分文件夹,绝对或相对路径取决于被监控文件夹的格式|
|--fromfile <file>|从文件中读取要监控的文件夹,以@开头的行表示排除在外|
|-m, --monitor|持续监控而不是触发一次就退出|
|-d, --daemon|同 -m不过不会输出在stdout上而是输出到文件所以必须指定-o参数|
|-o, --output|输出触发的事件内容到文件|
|-s, --syslog|把错误信息输出到syslog而不是stderr|
|-r. --recursive|递归监控文件夹,没有深度限制,即使新创建的文件夹也会被监控,软链接除外|
|-q, --quiet|安静模式,-q只输出事件信息-qq什么都不会输出|
|--exclude <pattern>|排除文件或者文件夹,可使用扩展正则表达式,**区分**大小写|
|--excludei <pattern>|排除文件或者文件夹,可使用扩展正则表达式,**不区分**大小写|
|t <seconds>, timeout <seconds>|如果指定时间内没有触发,就会退出,不指定则无限等待|
|e <event>, event <event>|指定要监听的事件,可指定多个,逗号分隔|
|timefmt <fmt>|指定输出时间的格式必须要遵守strftime规则`date`|
|format <fmt>|类似于printf的语法自定义输出触发事件的格式|
对于`--format <fmt>`参数用户可以根据如下指定格式进行自定义输出每行限制4000个字符
- %w监控文件时输出为触发事件的文件
- %f监控文件夹时输出为触发事件的文件否则为空字符串
- %e输出为触发的事件名称多个事件以逗号分隔
- %Xe输出为触发的事件名称多个事件用字符"X"分隔
- %T输出为使用`timefmt <fmt>`参数指定的时间格式
**特别需要注意**`--exclude(i)`参数在命令行中只能使用一次,使用多次的话,结果是只会执行最后一次的表达式指定的文件或者目录;`@`参数后面直接跟要排除的文件夹的路径,**且只能是文件夹路径**,两者之间没有空格;`--fromfile`后面跟一个文件,文件内容是要监控的**文件夹路径**或者要排除的**文件夹路径**。所以`@``--fromfile`这两个参数只能处理**文件夹路径**不能排除文件从加粗的频率看博主大概率是在这方面吃过亏的QAQ
总结一下,关于排除文件和文件夹,`--exclude(i)`参数既可以排除文件,也可以排除文件夹,还可以使用扩展正则表达式,功能最全面;`@``--fromfile`这两个参数只能处理**文件夹路径**,不能排除文件;有一种情况`--fromfile`后面的文件内容可以是**文件路径**,那就是有很多零散的文件需要监控,这个时候就肯定没有也没必要有需要排除的文件或者文件夹了,因为指定的就是文件路径,不想监控,可以直接不写。
```bash
# 很明显watch.list里面放的是文件夹因为使用了 -r 参数
# 如下inotify命令表示监控 /root/TODO/ 文件夹但是排除其下的exclude/yes/ 和 nope/ 文件夹
# 也排除文件名是todo.txt的文件而且排除的是所有todo.txt文件
$ cat /root/watch.list
/root/TODO
@/root/TODO/exclude
$ inotifywait -mrq --fromfile /root/watch.list --timefmt "%Y-%m-%d %H:%M:%S" --format "%T %w%f %e" -e delete,move,close_write --exclude '/root/TODO/nope/|^.*/todo.txt' @/root/TODO/yes
```
> **可被监控的事件类型**
|Event Name|Description|
|:-|:-|
|access|监控之下的文件被读取|
|modify|监控之下的文件被修改|
|attrib|监控之下的文件属性发生变化,权限,日期信息等|
|close_write|监控之下的文件被可写模式打开后关闭,**但这并不意味着文件有写入**|
|close_nowrite|监控之下的文件被只读模式打开后关闭|
|close|无特殊意义,不单独出现,一般跟在上述两种关闭事件之后,表示文件关闭|
|open|监控之下的文件被打开|
|move_to|文件或者文件夹**被移动到了被监控的目录下**,被监控路径下的移动都属于此类|
|move_from|文件或者文件夹被移动到了**被监控的目录之外**,被监控路径下的移动也都属于此类|
|move|表示包含上述两种移动事件,不会打印出现|
|move_self|-|
|create|监控之下,文件或者文件夹被创建|
|delete|监控之下,文件或者文件夹被删除|
|delete_self|-|
|unmount|被监控的文件或者文件夹所在的文件系统被unmount之后触发|
一般常用的事件类型是create/delete/modify/open/close_write/move因为`inotifywait`监控的粒度非常的详细,所以每操作一个文件的时候,命令都会输出详细的步骤,体现在程序中就是一次文件修改/创建操作,导致循环被执行多次,造成一定程度的资源浪费,合理选择触发的事件类型可以一定程度上规避此问题。
> **输出格式**
`inotifywait`命令会将启动时的采集分析信息输出到stderr把触发的事件信息输出到stdout默认的输出格式是按照输出每行的组成分别是"*watched_filename EVENT_NAMES event_filename*"
```text
watched_filename: 被监控的文件名,如果监控的是目录,则是触发事件的文件所在目录,最后一个字符一定是'/'
EVENT_NAMES : 触发的事件名称,可参考上述"事件类型"
event_filename : 该部分仅当监控目录的时候才会输出,是触发事件的文件名
```
#### b. inotifywatch
`inotifywatch`命令的主要功能是使用inotify接口对某个目录下的文件操作事件类型数量做统计分析该命令也是监控文件的各种变化但是并不执行命令而只是将每个文件触发的事件类型次数做了一个统计并且事件类型统计可以按照自定义格式排序。该命令可被监控的事件类型和`inotifywait`完全一致,命令支持的参数比`inotifywait`没有`-m/-d/-o/-s`,但是多两个`-a/-d`,仅作不同之处的记录。
|Options|Descriptions|
|:-|:-|
|-a <event>, --ascending <event>|按照某种事件类型**升序**统计|
|-d <event>, --descending <event>|按照某种事件类型**降序**统计|
```bash
# 监控/root/TODO文件夹并按照access升序统计
[root@CrossChain] [~]
> inotifywatch -r -a access /root/TODO/ -e modify,open,create,access -t 60
Establishing watches...
Finished establishing watches, now collecting statistics.
total access modify open filename
383 7 4 372 /root/TODO/nope/
411 16 4 391 /root/TODO/just-the-docs-0.4.0.rc3/
1116 29 4 1083 /root/TODO/
# 监控/root/TODO文件夹并按照access降序统计
[root@CrossChain] [~]
> inotifywatch -r -d access /root/TODO/ -e modify,open,create,access -t 60
Establishing watches...
Finished establishing watches, now collecting statistics.
total access modify open filename
1296 38 6 1252 /root/TODO/
582 20 5 557 /root/TODO/yes/
216 9 2 205 /root/TODO/just-the-docs-0.4.0.rc3/
```
#### c. inotify-hookable
该命令Man手册中解释的功能是*inotifyhookable blocking commandline interface to inotify*。我理解应该是该命令提供了一个阻塞命令行执行的接口,当文件被检测到有变化才会触发式执行。据命令的开发者描述,这个小工具是为了替代“*the functionality offered by Placks Filesys::Notify::Simple*”,简化了原来需要写脚本,①文件系统发生变化,②`inotify`检测到变化,③执行指定文件变化时需要运行的命令,而现在使用`inotify-hookable`就可以在一个命令中完成之前需要三步才能完成的事情。
而且相较于`inotifywait/inotifywatch`使用的时notify7接口该命令使用inotify2接口Man Page中描述该接口速度非常的快以至于需要处理好该命令发送事件类型的速度以保证其他程序可以正确收到信号并执行。
> **支持的选项**
|Event Name|Description|
|:-|:-|
|-w, --watch-directories|指定需要监控的文件夹,可使用多次来监控不同的文件夹|
|-f, --watch-files|指定需要监控的文件,可使用多次来监控不同的文件,与-w不冲突|
|-r, --[no-]recursive|对指定的文件夹执行递归操作,默认开启|
|-c, --on-modify-command|当文件改变时,命令会执行|
|-C, --on-modify-path-command|是一个键值对的形式key代表一个扩展正则表达式value表示对匹配改正则表达式的文件或者文件夹需要执行的命令|
|-t, --buffer-time|该命令执行的非常快当某个命令的操作可能造成被检测为两个批次所以添加了等待时间100ms如果100ms内没有接收到其他的事件触发则立即发送当前的事件|
|-i, --ignore-paths|使用正则表达式指定不需要监控的文件或者文件夹|
|-d, --debug|debug模式输出的信息更加详细|
|-q, --[no-]quiet|安静模式,输出简洁的信息|
如下是两个示例程序,理论上`inotify-hookable`命令后面可以监控非常多的文件和文件夹,只要注意使用`--on-modify-path-command``--on-modify-command`这两个选项设定好命令执行范围即可。另外,同一个脚本中只能有一个`inotify-hookable`命令出现,不能出现多个。该命令大多数情况下可以替代多个`inotifywait`脚本,实现统一的管理。
```bash
# 示例程序-1
inotify-hookable \
--watch-directories /opt/source-code/blog \
--watch-directories /opt/source-code/document/python \
--watch-directories /root/TODO \
--ignore-paths /opt/source-code/blog/.git/ \
--ignore-paths /opt/source-code/blog/img/avatar.jpg \
--ignore-paths /root/TODO/nope \
--ignore-paths /root/TODO/.*\.txt \
--on-modify-path-command "(^/opt/source-code/blog/.*)=(echo blog)" \
--on-modify-path-command "(^/opt/source-code/document/python/.*)=(echo python)" \
--on-modify-path-command "(^/root/TODO/.*)=(echo TODO)"
# 示例程序-2
inotify-hookable \
--watch-files /root/TODO/nope/todo.txt \
--on-modify-path-command "(^/root/TODO/nope)=(echo todo)" \
--watch-files /root/TODO/yes/hook.log \
--on-modify-command "echo hook" \
```
虽然理论上`inotify-hookable`命令可以将多个`inotifywait`命令写的脚本整合成一个脚本,实际面临着一个问题,使用`inotifywait`命令写的脚本一般都会有一定的长度,因为涉及到业务逻辑处理,所以需要在**脚本-1**中定义好功能实现函数,根据传参确定执行的部分,然后在**脚本-2**的`inotify-hookable`命令中调用脚本-1如下是一个真实的例子实际执行的时候会将对应的部分展开执行。最后运行结果表明`inotify-hookable`的执行速度**远远快于**`inotifywait`命令!!!
```bash
# 脚本-1
#!/bin/bash
# update Just the Docs -- Python
function python_update() {
echo ====================================================
echo `date`
echo $directory$filename $action
rm -rf /opt/websites/just-the-docs/python
jekyll b -s /opt/source-code/document/python -d /opt/websites/just-the-docs/python
echo -e '\n'
}
# update Jekyll blog
function blog_update() {
echo ====================================================
echo `date`
echo $directory$filename $action
rm -rf /opt/websites/blog
let numOfAvatar=`ls /opt/websites/nav/assets/images/logos/ | wc -l`
let randNumber=$RANDOM%$numOfAvatar
cp /opt/websites/nav/assets/images/logos/${randNumber}.jpg /opt/websites/homepage/assets/img/logo.jpg -rf
cp /opt/websites/nav/assets/images/logos/${randNumber}.jpg /opt/websites/nav/assets/images/logos/avatar.jpg -rf
cp /opt/websites/nav/assets/images/logos/${randNumber}.jpg /opt/source-code/blog/img/avatar.jpg -rf
jekyll b -s /opt/source-code/blog/ -d /opt/websites/blog/
echo -e '\n'
}
if [[ $1 == 'blog' ]]; then
blog_update
elif [[ $1 == 'python' ]]; then
python_update
else
echo Wrong
fi
# ===========================================================
# 脚本-2
#!/bin/bash
# 监控文件变化,自动生成内容
inotify-hookable \
--watch-directories /opt/source-code/blog \
--watch-directories /opt/source-code/document/python \
--ignore-paths /opt/source-code/blog/.git/ \
--ignore-paths /opt/source-code/blog/img/avatar.jpg \
--on-modify-path-command "(^/opt/source-code/blog/.*)=($(blog_update))" \
--on-modify-path-command "(^/opt/source-code/document/python/.*)=($(python_update))"
```
### 3. 参数配置
该特性在Linux内核2.6.13版本以后才能支持inotify软件。在没有安装inotify软件之前在"/proc/sys/fs/inotify"文件夹下应该有`max_queued_events/max_user_instances/max_user_watches`这三个文件,这三个文件中的默认值都是可以被修改的,以监听更大范围的文件,但在修改之前,要确保自己知道在做什么。
|文件|默认|描述|
|:-|:-|:-|
|max_queued_events|16384|设置inotify实例事件队列可容纳的事件数量|
|max_user_instances|128|设置每个用户可以运行的inotifywait或inotifywatch命令的进程数|
|max_user_watches|65536|设置inotifywait或inotifywatch命令可以监视的文件数量单进程|
> **默认是Debian11系统**
## 三、参考文档
- [Github inotify tools](https://github.com/inotify-tools/inotify-tools)
- [Inotifywait Mannal Page](https://linux.die.net/man/1/inotifywait)
- [Inotifywatch Mannal Page](https://linux.die.net/man/1/inotifywatch)
- [Inotify-hookable Mannal Page](https://manpages.debian.org/unstable/inotify-hookable/inotify-hookable.1p.en.html)