SRE工程师Shell脚本生产环境最佳实践
情境(Situation)
作为一名SRE工程师,Shell脚本是我们日常工作中最常用的工具之一。无论是监控采集、部署发布,还是备份容灾、安全加固,Shell脚本都能帮助我们实现自动化运维,提高工作效率,减少人为错误。
然而,生产环境中的Shell脚本开发与日常小工具脚本有着本质区别:
- 可靠性要求高:生产脚本一旦出错可能导致服务中断
- 可维护性重要:脚本需要长期运行,代码质量直接影响维护成本
- 安全性关键:脚本往往需要执行特权操作,安全漏洞可能导致系统被入侵
- 可扩展性必要:随着业务发展,脚本需要能够灵活适应新需求
冲突(Conflict)
许多SRE工程师在编写Shell脚本时存在以下问题:
- 质量参差不齐:缺乏统一的编码规范和最佳实践
- 错误处理薄弱:遇到异常情况时无法优雅处理
- 日志记录缺失:问题发生时难以排查
- 安全意识淡薄:对权限控制和输入验证不够重视
- 版本管理混乱:脚本修改后无法追溯变更历史
这些问题在生产环境中会被放大,可能导致严重的故障和安全风险。
问题(Question)
如何编写高质量、可靠、安全的Shell脚本,使其在生产环境中稳定运行并易于维护?
答案(Answer)
本文将从SRE视角出发,结合真实生产案例,提供一套完整的Shell脚本生产环境最佳实践。核心方法论基于 SRE面试题解析:你写过哪些类型的Shell脚本。
一、六类核心脚本的最佳实践
1.1 监控采集脚本
典型场景:Zabbix Agent自定义Key、Prometheus Exporter
最佳实践:
#!/bin/bash
# zbx_custom_metrics.sh - Zabbix自定义监控脚本
# 遵循生产环境脚本规范
set -euo pipefail
# 脚本元数据
SCRIPT_NAME="$(basename "$0")"
SCRIPT_VERSION="1.0.0"
SCRIPT_DATE="2026-04-26"
# 配置参数
METRIC_NAME="$1"
# 日志配置
LOG_FILE="/var/log/zabbix/${SCRIPT_NAME}.log"
MAX_LOG_SIZE=1048576 # 1MB
# 确保日志目录存在
mkdir -p "$(dirname "$LOG_FILE")"
# 日志函数
log() {
local level="$1"
local message="$2"
local timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
if [[ "$level" == "ERROR" ]]; then
echo "[$timestamp] [$level] $message" >&2
fi
}
# 清理日志函数
cleanup_log() {
if [[ -f "$LOG_FILE" ]]; then
local current_size=$(stat -c "%s" "$LOG_FILE" 2>/dev/null || echo 0)
if [[ $current_size -gt $MAX_LOG_SIZE ]]; then
log "INFO" "清理日志文件,大小: $current_size 字节"
mv "$LOG_FILE" "${LOG_FILE}.old"
truncate -s 0 "$LOG_FILE"
fi
fi
}
# 指标采集函数
collect_metric() {
case "$METRIC_NAME" in
"cpu_usage")
# 采集CPU使用率
top -bn1 | grep "Cpu(s)" | \
sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | \
awk '{print 100 - $1}'
;;
"memory_usage")
# 采集内存使用率
free | grep Mem | awk '{print $3/$2 * 100.0}'
;;
"disk_usage")
# 采集磁盘使用率
df -h | grep '/$' | awk '{print $5}' | sed 's/%//'
;;
"network_traffic")
# 采集网络流量
netstat -i | grep eth0 | awk '{print $5}'
;;
*)
log "ERROR" "未知的指标: $METRIC_NAME"
echo 0
;;
esac
}
# 主函数
main() {
log "INFO" "开始采集指标: $METRIC_NAME"
# 输入验证
if [[ -z "$METRIC_NAME" ]]; then
log "ERROR" "未指定指标名称"
echo 0
return 1
fi
# 执行采集
result=$(collect_metric)
# 验证结果
if [[ -z "$result" ]]; then
log "ERROR" "采集结果为空"
echo 0
return 1
fi
log "INFO" "采集完成,结果: $result"
echo "$result"
# 清理日志
cleanup_log
}
# 执行主函数
main "$@"
关键特性:
- 输入验证和错误处理
- 详细的日志记录
- 日志轮转机制
- 模块化设计
- 可扩展性
1.2 部署发布脚本
典型场景:K8s、Nginx、MySQL一键部署
最佳实践:
#!/bin/bash
# deploy_application.sh - 应用部署脚本
# 支持版本控制和回滚
set -euo pipefail
# 脚本配置
APP_NAME="my-application"
APP_VERSION="${1:-latest}"
DEPLOY_DIR="/opt/apps/${APP_NAME}"
BACKUP_DIR="/opt/backups/${APP_NAME}"
CONFIG_FILE="/etc/${APP_NAME}/config.yml"
SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service"
# 日志配置
LOG_FILE="/var/log/deploy/${APP_NAME}.log"
# 确保目录存在
mkdir -p "$DEPLOY_DIR" "$BACKUP_DIR" "$(dirname "$LOG_FILE")"
# 日志函数
log() {
local level="$1"
local message="$2"
local timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
echo "[$level] $message"
}
# 备份函数
backup() {
local backup_time=$(date '+%Y%m%d%H%M%S')
local backup_name="${APP_NAME}_${backup_time}"
log "INFO" "创建备份: $backup_name"
if [[ -d "$DEPLOY_DIR" ]]; then
tar -czf "${BACKUP_DIR}/${backup_name}.tar.gz" "$DEPLOY_DIR"
log "INFO" "备份完成: ${BACKUP_DIR}/${backup_name}.tar.gz"
else
log "WARN" "部署目录不存在,跳过备份"
fi
}
# 回滚函数
rollback() {
local backup_file="$1"
log "INFO" "开始回滚到备份: $backup_file"
if [[ ! -f "$backup_file" ]]; then
log "ERROR" "备份文件不存在: $backup_file"
return 1
fi
# 停止服务
systemctl stop "$APP_NAME" 2>/dev/null || true
# 清空部署目录
rm -rf "$DEPLOY_DIR"/*
# 恢复备份
tar -xzf "$backup_file" -C "$DEPLOY_DIR"
# 启动服务
systemctl start "$APP_NAME"
log "INFO" "回滚完成"
}
# 部署函数
deploy() {
log "INFO" "开始部署版本: $APP_VERSION"
# 备份当前版本
backup
# 停止服务
log "INFO" "停止服务"
systemctl stop "$APP_NAME" 2>/dev/null || true
# 下载应用
log "INFO" "下载应用版本: $APP_VERSION"
wget -O "${DEPLOY_DIR}/${APP_NAME}.tar.gz" \
"https://artifacts.example.com/${APP_NAME}/${APP_VERSION}.tar.gz"
# 解压应用
log "INFO" "解压应用"
tar -xzf "${DEPLOY_DIR}/${APP_NAME}.tar.gz" -C "$DEPLOY_DIR"
# 复制配置文件
if [[ -f "$CONFIG_FILE" ]]; then
log "INFO" "复制配置文件"
cp "$CONFIG_FILE" "${DEPLOY_DIR}/config.yml"
fi
# 设置权限
log "INFO" "设置权限"
chown -R appuser:appuser "$DEPLOY_DIR"
chmod +x "${DEPLOY_DIR}/${APP_NAME}"
# 启动服务
log "INFO" "启动服务"
systemctl start "$APP_NAME"
# 验证服务
log "INFO" "验证服务状态"
sleep 5
if systemctl is-active "$APP_NAME" >/dev/null; then
log "INFO" "部署成功!"
else
log "ERROR" "服务启动失败,开始回滚"
# 自动回滚到最近的备份
latest_backup=$(ls -1 "$BACKUP_DIR"/*.tar.gz 2>/dev/null | sort -r | head -1)
if [[ -n "$latest_backup" ]]; then
rollback "$latest_backup"
else
log "ERROR" "无备份可用,回滚失败"
fi
return 1
fi
}
# 主函数
main() {
log "INFO" "部署脚本启动"
case "$APP_VERSION" in
"rollback")
if [[ -n "$2" ]]; then
rollback "$2"
else
log "ERROR" "回滚模式需要指定备份文件"
echo "用法: $0 rollback <backup_file>"
return 1
fi
;;
*)
deploy
;;
esac
}
# 执行主函数
main "$@"
关键特性:
- 自动备份机制
- 版本回滚能力
- 服务状态验证
- 错误处理和自动恢复
- 详细的部署日志
1.3 备份容灾脚本
典型场景:数据库定时备份、配置文件版本化
最佳实践:
#!/bin/bash
# backup_system.sh - 系统备份脚本
# 支持数据库和配置文件备份
set -euo pipefail
# 配置参数
BACKUP_ROOT="/backup"
BACKUP_RETENTION=7 # 保留7天备份
# 数据库配置
DB_HOST="localhost"
DB_USER="backup_user"
DB_PASS="$(cat /etc/backup/db_pass.txt 2>/dev/null || echo "")"
# 备份目录
TIMESTAMP=$(date '+%Y%m%d%H%M%S')
BACkUP_DIR="${BACKUP_ROOT}/${TIMESTAMP}"
LOG_FILE="${BACKUP_ROOT}/backup_${TIMESTAMP}.log"
# 确保目录存在
mkdir -p "$BACkUP_DIR" "$(dirname "$LOG_FILE")"
# 日志函数
log() {
local level="$1"
local message="$2"
local timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
echo "[$level] $message"
}
# 数据库备份函数
backup_database() {
local db_name="$1"
local backup_file="${BACkUP_DIR}/${db_name}_${TIMESTAMP}.sql"
log "INFO" "开始备份数据库: $db_name"
if [[ -z "$DB_PASS" ]]; then
log "ERROR" "数据库密码未设置"
return 1
fi
# 使用mysqldump备份
mysqldump -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" \
--single-transaction --routines --triggers "$db_name" \
> "$backup_file"
if [[ $? -eq 0 ]]; then
# 压缩备份文件
gzip "$backup_file"
log "INFO" "数据库备份完成: ${backup_file}.gz"
else
log "ERROR" "数据库备份失败"
return 1
fi
}
# 配置文件备份函数
backup_configs() {
local config_dirs=("/etc" "/opt/configs" "/home/appuser/.config")
local backup_file="${BACkUP_DIR}/configs_${TIMESTAMP}.tar.gz"
log "INFO" "开始备份配置文件"
# 过滤不需要备份的目录
local exclude_patterns=("/etc/passwd" "/etc/shadow" "/etc/gshadow")
local exclude_args=()
for pattern in "${exclude_patterns[@]}"; do
exclude_args+=("--exclude=${pattern}")
done
# 执行备份
tar -czf "$backup_file" "${exclude_args[@]}" "${config_dirs[@]}" 2>/dev/null
if [[ $? -eq 0 ]]; then
log "INFO" "配置文件备份完成: $backup_file"
else
log "ERROR" "配置文件备份失败"
return 1
fi
}
# 清理过期备份函数
cleanup_old_backups() {
log "INFO" "开始清理过期备份(保留 $BACKUP_RETENTION 天)"
find "$BACKUP_ROOT" -type d -name "20*" -mtime +$BACKUP_RETENTION | \
while read -r old_backup; do
log "INFO" "删除过期备份: $old_backup"
rm -rf "$old_backup"
done
find "$BACKUP_ROOT" -name "backup_*.log" -mtime +$BACKUP_RETENTION | \
while read -r old_log; do
log "INFO" "删除过期日志: $old_log"
rm -f "$old_log"
done
log "INFO" "清理完成"
}
# 验证备份完整性函数
verify_backup() {
log "INFO" "验证备份完整性"
# 检查备份文件是否存在且非空
local backup_files=()
while IFS= read -r -d '' file; do
backup_files+=("$file")
done < <(find "$BACkUP_DIR" -type f -not -empty -print0 2>/dev/null)
if [[ ${#backup_files[@]} -eq 0 ]]; then
log "ERROR" "没有找到有效的备份文件"
return 1
fi
# 验证文件大小
for file in "${backup_files[@]}"; do
local size=$(stat -c "%s" "$file" 2>/dev/null || echo 0)
if [[ $size -lt 1024 ]]; then # 小于1KB视为异常
log "WARN" "备份文件可能不完整: $file (大小: $size 字节)"
else
log "INFO" "备份文件验证通过: $file (大小: $size 字节)"
fi
done
}
# 主函数
main() {
log "INFO" "系统备份开始"
# 备份数据库
backup_database "mysql"
backup_database "app_db"
# 备份配置文件
backup_configs
# 验证备份
verify_backup
# 清理过期备份
cleanup_old_backups
log "INFO" "系统备份完成"
}
# 执行主函数
main
关键特性:
- 数据库和配置文件备份
- 备份压缩和验证
- 过期备份自动清理
- 详细的备份日志
- 安全的密码管理
二、Shell脚本生产环境规范
2.1 代码规范
文件头部信息:
#!/bin/bash
# script_name.sh - 脚本功能描述
# 版本: 1.0.0
# 作者: Your Name
# 日期: 2026-04-26
# 依赖: bash, curl, jq
# 使用方法: ./script_name.sh [参数]
脚本设置:
# 安全设置
set -euo pipefail
# 字符集设置
export LANG="en_US.UTF-8"
export LC_ALL="en_US.UTF-8"
# 路径设置
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export PATH
2.2 错误处理
函数错误处理:
# 错误处理函数
error_exit() {
local error_message="$1"
local error_code="${2:-1}"
echo "ERROR: $error_message" >&2
log "ERROR" "$error_message"
# 清理工作
cleanup_resources
exit "$error_code"
}
# 捕获信号
trap "error_exit '脚本被中断'" SIGINT SIGTERM
命令执行检查:
# 执行命令并检查结果
run_command() {
local command="$1"
local description="$2"
log "INFO" "执行: $description"
if ! eval "$command"; then
error_exit "$description 失败"
fi
}
2.3 日志管理
日志轮转:
# 日志轮转函数
rotate_log() {
local log_file="$1"
local max_size="${2:-1048576}" # 默认1MB
if [[ -f "$log_file" ]]; then
local current_size=$(stat -c "%s" "$log_file" 2>/dev/null || echo 0)
if [[ $current_size -gt $max_size ]]; then
local backup_file="${log_file}.1"
# 移动旧日志
if [[ -f "$backup_file" ]]; then
rm -f "$backup_file"
fi
mv "$log_file" "$backup_file"
touch "$log_file"
log "INFO" "日志已轮转: $log_file"
fi
fi
}
2.4 安全规范
权限控制:
# 设置脚本权限
chmod 700 "$SCRIPT_PATH"
# 敏感信息处理
# 1. 避免硬编码密码
# 2. 使用环境变量或加密文件
# 3. 清理命令历史
unset HISTFILE
# 输入验证
validate_input() {
local input="$1"
local pattern="$2"
local description="$3"
if [[ ! "$input" =~ $pattern ]]; then
error_exit "无效的$description: $input"
fi
}
三、生产环境案例分析
案例1:监控采集脚本优化
背景:某电商平台的Zabbix自定义监控脚本在高峰期导致服务器负载飙升
问题分析:
- 脚本使用
top命令采集CPU使用率,每次执行都会创建新进程 - 没有缓存机制,频繁执行导致系统负载增加
- 日志记录过于详细,导致磁盘IO增加
解决方案:
#!/bin/bash
# 优化后的监控脚本
set -euo pipefail
# 缓存文件
CACHE_FILE="/tmp/cpu_usage_cache"
CACHE_TTL=30 # 缓存30秒
# 检查缓存是否有效
if [[ -f "$CACHE_FILE" ]]; then
local cache_time=$(stat -c "%Y" "$CACHE_FILE")
local current_time=$(date +%s)
if [[ $((current_time - cache_time)) -lt $CACHE_TTL ]]; then
cat "$CACHE_FILE"
exit 0
fi
fi
# 采集CPU使用率
top -bn1 | grep "Cpu(s)" | \
sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | \
awk '{print 100 - $1}' > "$CACHE_FILE"
cat "$CACHE_FILE"
效果:
- 脚本执行时间从0.5秒减少到0.01秒
- 系统负载降低30%
- 磁盘IO减少50%
案例2:部署脚本回滚机制
背景:某金融系统部署新版本时出现兼容性问题,需要紧急回滚
问题分析:
- 原部署脚本没有自动备份机制
- 回滚过程需要手动操作,耗时较长
- 缺乏回滚验证机制
解决方案:
- 实现自动备份功能
- 添加一键回滚命令
- 集成回滚后的服务验证
效果:
- 回滚时间从10分钟减少到1分钟
- 服务中断时间缩短80%
- 回滚成功率达到100%
四、自动化运维平台集成
4.1 脚本版本管理
Git集成:
#!/bin/bash
# 脚本版本管理
# 初始化Git仓库
initialize_git() {
if [[ ! -d ".git" ]]; then
git init
git config user.name "SRE Team"
git config user.email "sre@example.com"
git add .
git commit -m "Initial commit"
fi
}
# 提交变更
commit_changes() {
local message="$1"
git add .
git commit -m "$message"
git push origin main
}
4.2 CI/CD集成
Jenkins Pipeline示例:
pipeline {
agent any
stages {
stage('代码检查') {
steps {
sh 'shellcheck scripts/*.sh'
}
}
stage('测试') {
steps {
sh 'bash -n scripts/*.sh'
sh 'cd scripts && ./test.sh'
}
}
stage('部署') {
steps {
sh 'rsync -avz scripts/ production:/opt/scripts/'
sh 'ssh production "chmod +x /opt/scripts/*.sh"'
}
}
}
post {
success {
echo '脚本部署成功'
}
failure {
echo '脚本部署失败'
}
}
}
4.3 监控告警集成
Zabbix监控:
#!/bin/bash
# zbx_script_monitor.sh - 脚本执行监控
SCRIPT_PATH="$1"
EXPECTED_DURATION="$2" # 预期执行时间(秒)
# 记录开始时间
start_time=$(date +%s)
# 执行脚本
"$SCRIPT_PATH" > /dev/null 2>&1
# 计算执行时间
execution_time=$(( $(date +%s) - start_time ))
# 输出执行时间(用于Zabbix监控)
echo "$execution_time"
# 检查是否超时
if [[ $execution_time -gt $EXPECTED_DURATION ]]; then
echo "脚本执行超时: $execution_time 秒"
exit 1
fi
五、脚本开发工作流
5.1 开发流程
- 需求分析:明确脚本的功能和目标
- 设计阶段:制定脚本结构和实现方案
- 编码实现:遵循最佳实践编写代码
- 测试验证:在测试环境验证功能
- 代码审查:团队成员审查代码质量
- 部署上线:部署到生产环境
- 监控维护:监控脚本执行情况,定期维护
5.2 测试策略
单元测试:
#!/bin/bash
# test_script.sh - 脚本测试
# 测试函数
test_function() {
local test_name="$1"
local expected_result="$2"
local actual_result="$3"
if [[ "$actual_result" == "$expected_result" ]]; then
echo "✓ $test_name: PASS"
return 0
else
echo "✗ $test_name: FAIL"
echo " 预期: $expected_result"
echo " 实际: $actual_result"
return 1
fi
}
# 运行测试
run_tests() {
echo "开始测试..."
# 测试1: 函数返回值
result=$(my_function "test")
test_function "my_function返回值" "expected" "$result"
# 测试2: 错误处理
result=$(error_handling_test 2>&1 || true)
test_function "错误处理" "ERROR" "$result"
echo "测试完成"
}
run_tests
5.3 文档规范
README模板:
# 脚本名称
## 功能描述
脚本的主要功能和用途
## 依赖项
- bash 4.0+
- curl
- jq
## 使用方法
```bash
./script_name.sh [参数]
配置选项
- 配置文件:
/etc/script/config.conf - 环境变量:
SCRIPT_VAR
日志
- 日志文件:
/var/log/script.log - 日志级别: INFO, ERROR, WARN
故障排除
- 常见问题1: 解决方案
- 常见问题2: 解决方案
版本历史
- v1.0.0 (2026-04-26): 初始版本 ```
六、最佳实践总结
6.1 核心原则
- 安全性:优先考虑脚本的安全性,避免权限提升和输入注入
- 可靠性:添加错误处理和日志记录,确保脚本稳定运行
- 可维护性:使用清晰的命名和模块化设计,便于后续维护
- 可扩展性:设计灵活的配置机制,适应不同环境和需求
- 性能:优化脚本执行效率,避免资源浪费
6.2 脚本开发清单
- 脚本头部包含完整的元数据
- 使用
set -euo pipefail提高安全性 - 实现详细的日志记录
- 添加输入验证和错误处理
- 考虑边缘情况和异常处理
- 编写测试用例验证功能
- 文档完整,包括使用方法和故障排除
- 版本控制和变更管理
- 定期审查和更新脚本
6.3 常见陷阱与避免方法
| 陷阱 | 风险 | 避免方法 |
|---|---|---|
| 硬编码密码 | 安全漏洞 | 使用环境变量或加密文件 |
| 缺乏错误处理 | 脚本崩溃 | 使用 set -e 和错误处理函数 |
| 无日志记录 | 难以排查 | 实现结构化日志记录 |
| 权限设置不当 | 安全风险 | 遵循最小权限原则 |
| 无限循环 | 资源耗尽 | 设置循环次数限制和超时机制 |
| 未验证输入 | 注入攻击 | 对所有输入进行验证和过滤 |
总结
Shell脚本是SRE工程师的得力助手,掌握生产环境脚本开发的最佳实践,不仅能提高工作效率,还能确保系统的稳定性和安全性。
核心要点:
- 规范先行:建立统一的脚本编码规范和开发流程
- 安全第一:始终考虑脚本的安全性,避免潜在风险
- 可靠性保障:添加完善的错误处理和日志记录
- 自动化集成:将脚本纳入CI/CD和监控体系
- 持续改进:定期审查和优化脚本性能
延伸学习:更多面试相关的Shell脚本问题,请参考 SRE面试题解析:你写过哪些类型的Shell脚本。
参考资料
文档信息
- 本文作者:soveran zhong
- 本文链接:https://blog.clockwingsoar.cn/2026/04/27/sre-shell-script-production-best-practices/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)