SRE工程师Shell脚本生产环境最佳实践

2026/04/27 SRE 自动化运维 Shell 共 11398 字,约 33 分钟

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 开发流程

  1. 需求分析:明确脚本的功能和目标
  2. 设计阶段:制定脚本结构和实现方案
  3. 编码实现:遵循最佳实践编写代码
  4. 测试验证:在测试环境验证功能
  5. 代码审查:团队成员审查代码质量
  6. 部署上线:部署到生产环境
  7. 监控维护:监控脚本执行情况,定期维护

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 核心原则

  1. 安全性:优先考虑脚本的安全性,避免权限提升和输入注入
  2. 可靠性:添加错误处理和日志记录,确保脚本稳定运行
  3. 可维护性:使用清晰的命名和模块化设计,便于后续维护
  4. 可扩展性:设计灵活的配置机制,适应不同环境和需求
  5. 性能:优化脚本执行效率,避免资源浪费

6.2 脚本开发清单

  • 脚本头部包含完整的元数据
  • 使用 set -euo pipefail 提高安全性
  • 实现详细的日志记录
  • 添加输入验证和错误处理
  • 考虑边缘情况和异常处理
  • 编写测试用例验证功能
  • 文档完整,包括使用方法和故障排除
  • 版本控制和变更管理
  • 定期审查和更新脚本

6.3 常见陷阱与避免方法

陷阱风险避免方法
硬编码密码安全漏洞使用环境变量或加密文件
缺乏错误处理脚本崩溃使用 set -e 和错误处理函数
无日志记录难以排查实现结构化日志记录
权限设置不当安全风险遵循最小权限原则
无限循环资源耗尽设置循环次数限制和超时机制
未验证输入注入攻击对所有输入进行验证和过滤

总结

Shell脚本是SRE工程师的得力助手,掌握生产环境脚本开发的最佳实践,不仅能提高工作效率,还能确保系统的稳定性和安全性。

核心要点

  1. 规范先行:建立统一的脚本编码规范和开发流程
  2. 安全第一:始终考虑脚本的安全性,避免潜在风险
  3. 可靠性保障:添加完善的错误处理和日志记录
  4. 自动化集成:将脚本纳入CI/CD和监控体系
  5. 持续改进:定期审查和优化脚本性能

延伸学习:更多面试相关的Shell脚本问题,请参考 SRE面试题解析:你写过哪些类型的Shell脚本


参考资料

文档信息

Search

    Table of Contents