简介:279个开箱即用的Shell脚本(2024年新版)是一份面向Linux/Unix运维人员、系统管理员与安全爱好者的实用合集,属于软件/插件类运维工具,覆盖自动化任务、安全防护、日志分析和数据库备份等高频场景。文档详细展示了Dos攻击防范脚本,能通过分析Nginx日志自动屏蔽异常IP;Linux系统发送告警脚本,可在风险出现时调用mailx发送邮件通知;MySQL数据库备份单循环与多循环两套方案,分别满足整库备份和逐表细粒度备份需求;还有Nginx访问日志按天切割、访问日志统计分析与网卡实时流量查看等脚本。脚本内容涉及iptables防火墙规则、awk日志统计、mysqldump备份、cron定时任务等核心命令,读者既可快速复制部署,也能按生产环境二次调整。资源包共1个文件,为4.69MB的PDF文档,适合离线查阅。已有154人学习该合集,适合希望通过Shell简化服务器维护、提高故障响应效率的初中级运维人员。
1. 279 个 shell 脚本,到底能省多少事?
凌晨两点被监控电话叫醒,Nginx 日志里同一个 IP 正在疯狂刷请求,这是每个搞过线上运维的人都经历过的场景。我当时手动执行封禁命令,等到把 IP 加进 iptables,攻击流量已经持续了快一个小时。后来我把这套判断和封禁流程固化成了 shell 脚本,交给 crontab 每分钟跑一次,再也没在半夜被叫醒过。这套 279 个开箱即用的 shell 脚本,就是把运维日常里最高频的操作全部脚本化:Dos 攻击 IP 自动封禁、MySQL 数据库备份、Nginx 日志切割与分析、服务器初始化加固、批量服务器监控、企业微信告警,基本覆盖了一台 Linux 服务器从上线到日常维护的所有阶段。适合刚入门的运维新手照着抄作业,也适合想摆脱重复手工操作的熟手直接拿走改改用。
2. Dos 攻击防范与告警:从日志特征识别到 iptables 自动封禁
2.1 先看清攻击特征:从 Nginx 访问日志里数出异常 IP
判断一个 IP 是不是在攻击,最直接的办法不是看流量,而是看它短时间内发起请求的频率。正常用户一秒点几下页面顶天了,攻击脚本一秒能刷几十个请求。脚本里用了一条经典的管道命令组合,把当天日志里每五分钟内请求数超过阈值的 IP 挑出来。
# 获取当前日期,格式为 01/Dec/2024:14:30 DATE=$(date +%d/%b/%Y:%H:%M) # 指定 Nginx 日志文件路径 LOG_FILE=/usr/local/nginx/logs/demo2.access.log # 取日志最后 5000 行,过滤出当前时间段的记录,按 IP 统计访问次数 ABNORMAL_IP=$(tail -n5000 $LOG_FILE | grep $DATE | awk '{a[$1]++}END{for(i in a)if(a[i]>10)print i}')这段逻辑拆开看并不复杂。tail -n5000只取日志最后 5000 行,避免在大日志文件上执行全量扫描时消耗太多 IO;grep $DATE过滤出当前这一分钟内的请求,DATE变量的格式是%d/%b/%Y:%H:%M,对应日志里[01/Dec/2024:14:30:12]这种时间戳的前 16 位,正好能匹配到分钟粒度;最后的awk以 IP 为 key 做计数,a[i]>10是触发阈值,也就是一小时内同一个 IP 请求数超过 10 次就判定为异常。
这里有一个值得注意的选型细节:为什么用tail -n5000而不是直接grep整个文件?因为线上 Nginx 日志一天轻松到几百 MB,全量扫描会把脚本的执行时间从秒级拖到分钟级,攻击 IP 早就换了好几轮了。只扫描最后 5000 行,绝大多数攻击流量都能命中。脚本的触发阈值 10 次也不是拍脑袋定的,真实场景里要根据业务调——静态资源站、API 接口、图片站差异很大,我一般先用下面的分析脚本跑一天看正常 IP 的请求分布,再定阈值。
2.2 封禁动作与幂等判断:iptables 规则怎么加不重复
筛选出异常 IP 只是第一步,真正动手封禁的是下面这段。这里最关键的设计是加了一个幂等判断:iptables -vnL | grep -c "$IP"检查这个 IP 是否已经在防火墙规则里,避免重复追加导致规则表膨胀。
# 循环处理每个异常 IP for IP in $ABNORMAL_IP; do # 检查 iptables 的 INPUT 链中是否已存在该 IP 的封禁规则 if [ $(iptables -vnL |grep -c "$IP") -eq 0 ]; then # 追加 DROP 规则,丢弃来自该 IP 的所有数据包 iptables -I INPUT -s $IP -j DROP # 记录封禁时间和 IP 到日志 echo "$(date +'%F_%T') $IP" >> /tmp/drop_ip.log fi doneiptables -I INPUT -s $IP -j DROP是把规则插入到 INPUT 链的头部,-I和-A的区别要搞清楚:-I插到最前面,匹配优先级更高,适合封禁这种需要立刻生效的场景;-A追加到末尾,如果前面有 ACCEPT 规则先命中,封禁就失效了。封禁记录写到/tmp/drop_ip.log,方便事后溯源。
这段脚本配合 crontab 每分钟执行一次,就能做到攻击 IP 从出现到封禁的延迟在一分钟以内。但脚本本身有个隐藏问题:它只封了 DROP 规则,没有做持久化。iptables的规则存在内存里,重启后全部丢失。我自己的习惯是把规则单独保存一份:
# 保存当前 iptables 规则到文件 iptables-save > /etc/iptables.rules # 在 rc.local 里加一行开机自动恢复 echo "iptables-restore < /etc/iptables.rules" >> /etc/rc.local2.3 邮件告警:mailx 的安装、配置与触发时机
封禁动作做完,得让运维知道发生了什么。脚本里用 mailx 发告警邮件,安装和配置分两步走。yum install mailx装包后,编辑/etc/mail.rc配置外部 SMTP 服务器,这里踩过一个坑:网易和腾讯的 SMTP 都要求使用授权码,不能用邮箱登录密码。
# 编辑 /etc/mail.rc 追加以下配置 set from=yourname@163.com set smtp=smtp.163.com set smtp-auth-user=yourname@163.com set smtp-auth-password=your-authentication-code set smtp-auth=login配置里的smtp-auth-password填的是 SMTP 授权码,不是邮箱密码。有人在这直接填了邮箱密码,结果 mailx 报SMTP: 535 Error: authentication failed,排查了半天才发现是授权码的问题。告警脚本的触发点放在iptables -I INPUT执行成功之后,因为echo "$(date +'%F_%T') $IP" >> /tmp/drop_ip.log只是本地记录,邮件才是真正通知到人的手段。
完整链路是:crontab 定时跑 → 检测到异常 IP → iptables 封禁 → 写入日志 → 邮件通知。这个脚本放线上跑之前,建议先在测试环境拿一条自己的出口 IP 试试封禁和告警流程是否都通,别等被攻击的时候才发现 mailx 的 SMTP 配置是错的。
3. MySQL 自动备份与 Nginx 日志切割:数据安全和日志轮转的正确姿势
3.1 MySQL 数据库备份单循环:适合中小库的快速方案
数据库备份脚本的核心逻辑是遍历数据库列表,逐个执行mysqldump导出。脚本用了mysql命令查询数据库列表,再排除掉系统自带库,只备份业务库。这个脚本我在小公司用的时候很顺手,库不多、单库不大,跑一次也就几分钟。
#!/bin/bash # 当前时间作为备份文件名的一部分 DATE=$(date +%F_%H-%M-%S) HOST=localhost USER=backup PASS=123.com BACKUP_DIR=/data/db_backup # 查询所有数据库,排除系统库 DB_LIST=$(mysql -h$HOST -u$USER -p$PASS -s -e "show databases;" 2>/dev/null | egrep -v "Database|information_schema|mysql|performance_schema|sys") # 逐个数据库执行备份 for DB in $DB_LIST; do BACKUP_NAME=$BACKUP_DIR/${DB}_${DATE}.sql # 导出单库,失败时输出日志 if ! mysqldump -h$HOST -u$USER -p$PASS -B $DB > $BACKUP_NAME 2>/dev/null; then echo "$BACKUP_NAME 备份失败!" fi done-B $DB参数是--databases的简写,导出的 SQL 文件里会自带CREATE DATABASE和USE语句,恢复的时候不用手动建库,直接mysql < backup.sql就能还原。2>/dev/null把错误信息丢弃了,这是当时写脚本的一个草率决定,虽然代码里给了失败提示,但看不到具体报错原因,后面真正遇到备份失败时排查全靠猜。现在我会改成2>> /var/log/db_backup_error.log,把错误落盘,失败时至少有据可查。
这个方案适合中小库的另一个原因是备份粒度粗。一个库一个文件,恢复的时候要么全量恢复,要么手动sed切分数据表,非常痛苦。库的数量多、单库几百张表的业务,就得看多循环方案了。
3.2 MySQL 数据库备份多循环:大库分表备份的精细方案
多循环脚本在单循环的基础上多套了一层表的循环。每个数据库单独建一个目录,目录下每张表一个 SQL 文件。这样恢复单表时只需要找到对应文件,不用把整个库都导进来。
#!/bin/bash DATE=$(date +%F_%H-%M-%S) HOST=localhost USER=backup PASS=123.com BACKUP_DIR=/data/db_backup # 获取数据库列表 DB_LIST=$(mysql -h$HOST -u$USER -p$PASS -s -e "show databases;" 2>/dev/null | egrep -v "Database|information_schema|mysql|performance_schema|sys") # 外层循环:遍历数据库 for DB in $DB_LIST; do # 按库名创建备份目录 BACKUP_DB_DIR=$BACKUP_DIR/${DB}_${DATE} [ ! -d $BACKUP_DB_DIR ] && mkdir -p $BACKUP_DB_DIR &>/dev/null # 查询当前库的所有表 TABLE_LIST=$(mysql -h$HOST -u$USER -p$PASS -s -e "use $DB;show tables;" 2>/dev/null) # 内层循环:逐表备份 for TABLE in $TABLE_LIST; do BACKUP_NAME=$BACKUP_DB_DIR/${TABLE}.sql # 导出单表,失败时输出提示 if ! mysqldump -h$HOST -u$USER -p$PASS $DB $TABLE > $BACKUP_NAME 2>/dev/null; then echo "$BACKUP_NAME 备份失败!" fi done done内层循环的mysqldump命令少了-B参数,改成直接指定库名和表名,这样导出的 SQL 里不包含建库语句,恢复单表时直接指定库即可。备份目录结构是db_backup/库名_时间戳/表名.sql,找某一张表的备份只需要知道库名和表名,ls一下目录就定位了。
| 对比项 | 单循环方案 | 多循环方案 |
|---|---|---|
| 备份粒度 | 整个库一个文件 | 每张表一个文件 |
| 恢复效率 | 全量恢复快,单表恢复慢 | 单表恢复快,全量恢复需拼接 |
| 文件数量 | 库的数量 | 所有表的总数 |
| 适合场景 | 库多表少、单库不大 | 单库表多、需要单表恢复 |
两个脚本都用for循环遍历,这正是 shell 处理批量重复任务的典型写法。如果你对 for 循环的语法还不熟,这个脚本里能看到完整的模式:for 变量 in 列表; do ... done,内层循环嵌套外层循环时,用 tab 缩进区分层级,脚本跑起来看输出就能确认执行到了哪一层。
3.3 Nginx 访问日志按天切割:mv 旧日志加 kill -USR1
Nginx 日志切割是个经典需求,因为access.log如果不切分,一年下来能长到几十 GB,grep查一次日志要等半天。切割脚本的核心操作就两步:把当天的日志文件改名归档,然后让 Nginx 重新打开新的日志文件。
#!/bin/bash LOG_DIR=/usr/local/nginx/logs YESTERDAY_TIME=$(date -d "yesterday" +%F) LOG_MONTH_DIR=$LOG_DIR/$(date +"%Y-%m") LOG_FILE_LIST="default.access.log" # 按月份归档,不存在则创建目录 for LOG_FILE in $LOG_FILE_LIST; do [ ! -d $LOG_MONTH_DIR ] && mkdir -p $LOG_MONTH_DIR # 把昨天的日志改名移动到归档目录 mv $LOG_DIR/$LOG_FILE $LOG_MONTH_DIR/${LOG_FILE}_${YESTERDAY_TIME} done # 向 Nginx 主进程发送 USR1 信号,重新打开日志文件 kill -USR1 $(cat /var/run/nginx.pid)mv $LOG_DIR/$LOG_FILE $LOG_MONTH_DIR/${LOG_FILE}_${YESTERDAY_TIME}是把昨天的日志改名为default.access.log_2024-12-01,按年-月目录归档。date -d "yesterday" +%F取的是昨天日期,配合 crontab 每天凌晨 0 点跑,正好把前一天的日志收走。
kill -USR1 $(cat /var/run/nginx.pid)是切割脚本的灵魂所在。Nginx 主进程收到 USR1 信号后会重新打开日志文件,如果不发这个信号,Nginx 的文件句柄还指向旧文件,mv 之后日志照旧往旧文件里写,虽然文件名变了但文件没变,切割就失败了。cat /var/run/nginx.pid读的是 Nginx 的 pid 文件,如果 Nginx 不是通过默认路径启动的,这个路径要改成你自己的 pid 文件位置。
日志归档的目录结构是logs/2024-12/default.access.log_2024-12-01,我见过有人把归档目录建成了logs/20241201这种按天建的,一个月后目录数量就是 30 个,不如按年-月建目录再在文件名上带日期,查询时ls logs/2024-12/一眼能看全一个月的文件。
4. 服务器系统初始化与批量监控:新机器到手后的第一件事
4.1 新机器系统配置:时区、SELinux、防火墙与历史命令时间戳
每次拿到一台新服务器,重复这些初始化操作实在浪费时间。这套脚本把安全基线的配置固化了下来,CentOS 6 和 CentOS 7 的差异也做了判断处理。
#!/bin/bash # 设置时区并同步时间 ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # 配置每小时 NTP 时间同步 if ! crontab -l |grep ntpdate &>/dev/null; then (echo "* 1 * * * ntpdate time.windows.com >/dev/null 2>&1";crontab -l) |crontab fi # 禁用 SELinux sed -i '/SELINUX/{s/permissive/disabled/}' /etc/selinux/config # 按系统版本关闭防火墙 if egrep "7.[0-9]" /etc/redhat-release &>/dev/null; then systemctl stop firewalld systemctl disable firewalld elif egrep "6.[0-9]" /etc/redhat-release &>/dev/null; then service iptables stop chkconfig iptables off fi # 历史命令显示操作时间 if ! grep HISTTIMEFORMAT /etc/bashrc; then echo 'export HISTTIMEFORMAT="%F %T `whoami` "' >> /etc/bashrc fi # SSH 会话超时时间设置 if ! grep "TMOUT=600" /etc/profile &>/dev/null; then echo "export TMOUT=600" >> /etc/profile fi # 禁止 root 远程登录 sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config # 禁止定时任务向 root 发送邮件 sed -i 's/^MAILTO=root/MAILTO=""/' /etc/crontabegrep "7.[0-9]" /etc/redhat-release这个判断很实用,同一套脚本要在 CentOS 6 和 CentOS 7 上都跑通,防火墙命令完全不同。CentOS 7 用systemctl管理 firewalld,CentOS 6 得用service iptables stop加chkconfig iptables off。很多新手在 CentOS 7 上执行service iptables stop会得到iptables: unrecognized service,就是因为两个版本的防火墙体系不一样。
export HISTTIMEFORMAT="%F %T \whoami` "这行让 history 命令输出带上时间和用户。等真出事了要审计是谁在什么时候执行了什么命令时,这条配置能省去很多扯皮。TMOUT=600是设置 SSH 会话空闲 10 分钟自动断开,防止有人挂着终端忘了退出。这两条都是改了全局配置文件的,执行完记得source /etc/profile` 或重新登录才生效。
sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config把 root 远程登录禁用了,这个操作建议在所有初始化脚本确认能正常登录后再执行,否则一旦 sshd restart 前配置写错,远程就连不上了。稳妥做法是先备份一份cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak,真出问题时还能恢复。
4.2 内核参数与文件描述符:高并发场景的系统层面设置
文件描述符和内核参数这两个配置优化的是系统在高并发下的承载能力。limits.conf控制单进程能打开的文件数,sysctl.conf里几个 TCP 参数调整了连接回收和队列深度。
# 设置最大打开文件数 if ! grep "* soft nofile 65535" /etc/security/limits.conf &>/dev/null; then cat >> /etc/security/limits.conf << EOF * soft nofile 65535 * hard nofile 65535 EOF fi # 系统内核参数优化 cat >> /etc/sysctl.conf << EOF net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_tw_buckets = 20480 net.ipv4.tcp_max_syn_backlog = 20480 net.core.netdev_max_backlog = 262144 net.ipv4.tcp_fin_timeout = 20 EOF # 减少 SWAP 使用 echo "0" > /proc/sys/vm/swappiness这几个内核参数选的都是对日常 Web 服务影响最大的一批。net.ipv4.tcp_syncookies = 1开启 SYN cookies,能缓解 SYN Flood 攻击;tcp_max_tw_buckets = 20480限制了 TIME_WAIT 状态的 socket 数量,避免大量短连接时 TIME_WAIT 耗尽内存;tcp_max_syn_backlog = 20480增大半连接队列长度,高并发握手场景下减少丢包。vm.swappiness = 0让内存优先使用物理内存、尽量少用 swap,避免 swap 进出造成性能抖动。
这里有个边界要讲清楚:tcp_fin_timeout = 20缩短了 FIN_WAIT_2 状态的等待时间,对短连接频繁的业务效果明显,但不适合长连接业务,因为连接可能还没发完数据就被回收了。sysctl.conf的修改执行sysctl -p生效,limits.conf的修改对新登录会话生效,已经在线的进程不会立刻生效。swappiness那个 echo 只是临时生效,重启后要重新设置,正确做法是写入sysctl.conf。
4.3 并发获取批量服务器 hostname:用&后台任务加wait汇总
监控 100 台服务器的磁盘利用率,如果一台一台 ssh 过去执行命令,串行跑完一轮要十几分钟,监控脚本根本没意义。项目里给了一个并发思路:用for循环配合&后台执行,最后wait等所有后台进程结束。
#!/bin/bash # 读取主机信息文件,格式: IP 用户名 端口 HOST_INFO=host.info # 遍历所有主机 IP for IP in $(awk '/^[^#]/{print $1}' $HOST_INFO); do # 按 IP 关联取用户名和端口 USER=$(awk -v ip=$IP 'ip==$1{print $2}' $HOST_INFO) PORT=$(awk -v ip=$IP 'ip==$1{print $3}' $HOST_INFO) TMP_FILE=/tmp/disk.tmp # 远程执行 df -h 获取磁盘使用率 ssh -p $PORT $USER@$IP 'df -h' > $TMP_FILE # 解析使用率,/dev/ 开头的是真实分区 USE_RATE_LIST=$(awk 'BEGIN{OFS="="}/^\/dev/{print $NF,int($5)}' $TMP_FILE) # 逐分区检查是否超过阈值 for USE_RATE in $USE_RATE_LIST; do PART_NAME=${USE_RATE%=*} USE_RATE=${USE_RATE#*=} # 使用率超 80% 输出告警 if [ $USE_RATE -ge 80 ]; then echo "Warning: $PART_NAME Partition usage $USE_RATE%!" fi done done这个脚本用了文件来中转数据:ssh的结果写到TMP_FILE,再用awk从临时文件解析分区名和使用率。USE_RATE_LIST=$(awk ...)里print $NF,int($5)用=做分隔符,外层循环再用${USE_RATE%=*}和${USE_RATE#*=}做字符串切分,分别拿到分区名和使用率。
获取 hostname 并计时的脚本用到了 shell 并发的一个经典范式:{ ... } &把耗时操作丢到后台,wait等待全部完成。这种做法比xargs -P更直观,适合固定主机列表的场景。
#!/bin/bash # 所有主机 IP 列表 ALL_HOSTS=(IP1 IP2 IP3) # 并发 ssh 获取 hostname 并记录耗时 for host in ${ALL_HOSTS[*]}; do { start_time=$(date +'%s') ssh $host "hostname" &>/dev/null sleep 2 stop_time=$(date +'%s') time_consuming=$((stop_time-start_time)) # 结果写入文件 echo "$host: $time_consuming" >> hostname.txt }& done wait # 按耗时排序取最短的机器 host=$(sort -n -k 2 hostname.txt | head -1 | awk -F':' '{print $1}') # 输出这台机器 top 结果 ssh $host "top -b -n 1"两个脚本都具备实际可用的形态,但有个共同的问题:把 SSH 的密码交给了ssh命令,实际运行时需要通过密钥认证登录,或者借助sshpass传密码。纯手动运维 100 台服务器不现实,建议配好 SSH key 后再跑这类脚本,否则每台机器都会停在密码输入等人工操作,wait会等上一整轮。
5. 排查与避坑:五个把脚本改成事故现场的操作
5.1 日志切割后 Nginx 还在写旧文件
现象:按天切割脚本跑了几天后,发现归档目录里昨天的日志文件在持续变大,新的请求还在往里写。
原因:mv只是把文件名改了,Nginx 进程的文件句柄还指向原来的 inode。不发送kill -USR1信号,Nginx 根本不知道日志文件已经被挪走,继续往同一个 inode 写数据。
解决:切割脚本最后必须执行kill -USR1 $(cat /var/run/nginx.pid)。验证方法很简单,执行完切割后ls -l /usr/local/nginx/logs/看是否生成了新的access.log文件,再tail -f看新请求是否落到了新文件里。如果 Nginx 长时间没有新请求,可以用curl访问一下你的站点触发一条访问日志。
5.2 iptables 封禁规则重启后全部丢失
现象:服务器重启后,之前被封禁的攻击 IP 又能访问了,防火墙规则表一片空白。
原因:iptables的规则保存在内核内存中,不落盘的话重启即失。用iptables -vnL查看规则时,显示的规则都是临时的。
解决:执行iptables-save > /etc/sysconfig/iptables保存规则,CentOS 6 上重启后iptables服务会自动恢复;CentOS 7 上恢复规则需要安装iptables-services包,或者把iptables-restore < /etc/sysconfig/iptables写入/etc/rc.local开机执行。还有个细节:/etc/rc.local在 CentOS 7 上默认没有执行权限,要chmod +x /etc/rc.local一次。
5.3 mysqldump 备份文件是空文件
现象:备份目录里生成了 SQL 文件,但文件大小是 0,命令行手动执行同样的命令能正常导出。
原因:脚本里的2>/dev/null把 mysqldump 的报错吞了,很典型的场景是密码变量里带了特殊字符,比如PASS="My@Pass",在执行mysql -p$PASS时被 shell 解析成了别的含义。另一个常见原因是crontab执行时的环境变量和手动执行不同,PATH里没有 mysql 的安装路径。
解决:把2>/dev/null改成2>>/var/log/db_backup_error.log记录错误日志。密码变量用mysql --defaults-extra-file=/etc/mysql_backup.cnf的方式传递,把账号密码写进配置文件并chmod 600,避免特殊字符被 shell 转义搞挂。脚本里统一写绝对路径/usr/local/mysql/bin/mysqldump,杜绝 crontab 环境变量不一致的问题。
5.4 awk 统计把内网 IP 和 CDN 节点全封了
现象:Dos 防范脚本跑完,发现 iptables 里封禁了一大串内网 IP,外网攻击 IP 反而漏掉了。
原因:awk '{a[$1]++}统计的是访问来源 IP,如果 Nginx 前面挂了 CDN 或负载均衡,脚本封的是 CDN 节点的 IP,因为 CDN 转发请求时默认不改写$remote_addr,Nginx 看到的全是 CDN 的回源 IP。
解决:统计前先用grep -v排除内网网段和已知 CDN IP。常见做法是改成这样:
# 过滤掉内网 IP 和 CDN 回源 IP ABNORMAL_IP=$(tail -n5000 $LOG_FILE | grep $DATE | \ grep -v -E "^(10\.|192\.168\.|172\.16\.|127\.)" | \ awk '{a[$1]++}END{for(i in a)if(a[i]>10)print i}')或者让 Nginx 在日志里记录$http_x_forwarded_for,用真实客户端 IP 做统计。依赖 CDN 回源 IP 做封禁是没有意义的,因为 CDN 节点 IP 本来就是共享的、合法的。
5.5 企业微信告警脚本的 access_token 失效
现象:企业微信告警脚本跑了几天后突然不推送了,查看日志发现gettoken接口返回invalid credential。
原因:企业微信的access_token有效期是 7200 秒,脚本里每次实例化DLF类都会重新请求一次 token,但如果代码长时间运行没有重新实例化,token 就过期了。另一个原因是corpsecret配置错了应用密钥,或者应用被删除导致 token 获取失败。
解决:token 获取逻辑单独抽出来,每次发消息前检查 token 的剩余有效时间,剩余不足 600 秒时重新获取。最简单的方式是每次发送前都调一次_get_token(),虽然多一次 HTTP 请求,但避免过期问题。corpsecret去企业微信后台对应应用的「企业可信 IP」里核对,应用被删除或密钥重置后旧密钥会立即失效,脚本也要同步更新。
6. 进阶用法:把 279 个脚本变成自己的工具库
脚本拿回来直接用会踩很多配置上的坑,因为每台服务器的环境不一样。我的习惯是先把脚本按功能重新组织一下:安全防御单独一个目录,数据库备份单独一个目录,日志处理一个目录,监控告警一个目录。然后给每个脚本加一个统一的参数入口,比如备份脚本改成可以通过命令行传入数据库账号和备份目录:
#!/bin/bash # 用法: ./db_backup.sh -u backup_user -p password -d /data/backup while getopts "u:p:d:h" opt; do case $opt in u) DB_USER=$OPTARG;; p) DB_PASS=$OPTARG;; d) BACKUP_DIR=$OPTARG;; h) echo "Usage: $0 -u user -p pass -d backup_dir"; exit 0;; esac done统一了参数入口后,再写一个总控脚本去调用它们,配合 crontab 做成定时任务。定时任务的写法有个注意点:统一把脚本路径和日志路径都写成绝对路径,避免环境变量差异导致的“手动执行正常、crontab 执行就挂”的玄学问题。
# crontab 配置示例 # 每分钟检测一次异常 IP * * * * * /usr/local/bin/dos_check.sh >> /var/log/dos_check.log 2>&1 # 每天凌晨 2 点备份数据库 0 2 * * * /usr/local/bin/db_backup.sh -u backup -p '你的密码' -d /data/backup >> /var/log/db_backup.log 2>&1 # 每天凌晨 0 点切割 Nginx 日志 0 0 * * * /usr/local/bin/nginx_log_cut.sh >> /var/log/nginx_cut.log 2>&1还有个习惯值得养成:每个脚本开头都用set -e加上trap记录异常退出时的行号,线上排查问题时能直接定位到挂在哪一行。从那以后我每次部署新脚本,都强制走一遍“手动执行 → 故意制造错误 → 看错误日志 → 挂到 crontab → 观察三天”的流程,血泪经验告诉我,跳过任何一步都会在某个凌晨付出代价。这套 279 个脚本的合集里不是所有脚本都适合直接上线,但把它们当参考模板、按自己的业务改造成工具库,能少写很多重复代码。希望帮到你。
本文还有配套的精品资源,点击获取