Appearance
服务器初始化脚本
服务器初始化脚本更适合做成工具箱:脚本入口是一个文件,里面提供多个子命令,比如 check、set-hostname、write-hosts、config-time、install-tools、config-sysctl、config-limits。日常维护时按需要执行单个动作;全新机器交付时,再用 init-all 把常用动作串起来。
脚本内部用正常的 Bash 写法,函数名、变量名、子命令保持英文。中文放在帮助信息、日志、错误提示和检查输出里。这样既方便平时阅读输出,也不影响编辑器、shellcheck、代码搜索和团队维护。
一、工具箱结构
工具箱脚本的核心是“单项动作能独立执行”。初始化不是每次都从零开始,很多时候只是补一个配置、复查一次状态,或者批量扩容节点时重复执行固定动作。
| 子命令 | 作用 | 示例 |
|---|---|---|
help | 显示帮助 | bash server-toolbox.sh help |
check | 只读检查系统状态 | bash server-toolbox.sh check |
set-hostname | 设置主机名 | bash server-toolbox.sh set-hostname k3s-server-01 |
write-hosts | 追加 hosts 记录 | bash server-toolbox.sh write-hosts hosts.cluster |
config-time | 配置 chrony 时间同步 | bash server-toolbox.sh config-time 192.168.10.1 |
install-tools | 安装常用排障工具 | bash server-toolbox.sh install-tools |
config-sysctl | 写入基础内核参数 | bash server-toolbox.sh config-sysctl |
config-limits | 写入文件句柄和进程限制 | bash server-toolbox.sh config-limits |
init-all | 串联常用初始化动作 | bash server-toolbox.sh init-all k3s-server-01 hosts.cluster |
全局参数保持简单:
| 参数 | 作用 |
|---|---|
--dry-run | 只打印将要执行的动作,不修改系统 |
--log-file <path> | 指定日志文件,默认 /tmp/server-toolbox.log |
示例:
bash
# 查看帮助
bash server-toolbox.sh help
# 只读检查系统状态
bash server-toolbox.sh check
# 预览时间同步配置动作,不修改系统
bash server-toolbox.sh --dry-run config-time 192.168.10.1
# 正式初始化时把日志写入 /var/log
bash server-toolbox.sh --log-file /var/log/server-toolbox.log init-all k3s-server-01 hosts.cluster二、通用函数
通用函数负责日志、错误退出、命令执行、配置备份、root 权限检查。日志输出用中文,执行状态能直接保存到变更记录里。
bash
#!/usr/bin/env bash
set -euo pipefail
dry_run=0
log_file="/tmp/server-toolbox.log"
os_id=""
os_like=""
command_name="help"
command_args=()
log_info() {
printf '[信息] %s %s\n' "$(date '+%F %T')" "$*" | tee -a "$log_file"
}
log_error() {
printf '[错误] %s %s\n' "$(date '+%F %T')" "$*" | tee -a "$log_file" >&2
}
die() {
log_error "$*"
exit 1
}
run_cmd() {
log_info "执行命令: $*"
# dry-run 只打印动作,用于批量执行前确认脚本会改哪些地方
if [ "$dry_run" -eq 1 ]; then
return 0
fi
"$@"
}
require_root() {
# dry-run 不实际修改系统,可以在普通用户下预览
if [ "$dry_run" -eq 1 ]; then
return 0
fi
[ "$(id -u)" -eq 0 ] || die "当前操作需要 root 权限"
}
backup_file() {
local file_path="$1"
local backup_path
[ -e "$file_path" ] || return 0
backup_path="${file_path}.$(date '+%F-%H%M%S').bak"
log_info "备份文件: $file_path -> $backup_path"
if [ "$dry_run" -eq 0 ]; then
cp -a "$file_path" "$backup_path"
fi
}run_cmd 接收命令和参数,不接收拼好的字符串,能减少引号和转义问题。需要重定向写文件时,单独在函数里处理,不把 >> 这种 shell 语法塞进 run_cmd。
三、参数解析
参数解析分成全局参数和子命令。全局参数只在命令前面出现,解析到第一个非全局参数时,后面的内容都交给子命令。
bash
show_help() {
cat <<'EOF'
用法:
bash server-toolbox.sh [全局参数] <子命令> [参数]
全局参数:
--dry-run 只打印将要执行的动作,不修改系统
--log-file <路径> 指定日志文件,默认 /tmp/server-toolbox.log
-h, --help, help 显示帮助
子命令:
check 只读检查系统状态
set-hostname <名称> 设置主机名
write-hosts <文件> 将文件内容追加到 /etc/hosts,已存在的行会跳过
config-time [NTP服务器] 安装并配置 chrony,默认 ntp.aliyun.com
install-tools 安装常用排障工具
config-sysctl 写入基础 sysctl 参数
config-limits 写入 limits 文件句柄和进程数限制
init-all <主机名> [hosts文件] 执行常用初始化动作
示例:
bash server-toolbox.sh check
bash server-toolbox.sh --dry-run config-time 192.168.10.1
bash server-toolbox.sh set-hostname k3s-server-01
bash server-toolbox.sh write-hosts hosts.cluster
bash server-toolbox.sh init-all k3s-server-01 hosts.cluster
EOF
}
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run)
dry_run=1
shift
;;
--log-file)
log_file="${2:-}"
[ -n "$log_file" ] || die "--log-file 缺少路径"
shift 2
;;
-h|--help|help)
command_name="help"
shift
break
;;
*)
command_name="$1"
shift
break
;;
esac
done
command_args=("$@")
}参数解析不追求复杂 CLI 框架。Shell 脚本只要能把固定子命令分清楚,错误信息说清楚,就够日常初始化使用。
四、发行版识别
软件包名称和服务名称在不同发行版上不完全一致。脚本先识别系统,再决定使用 yum、dnf 还是 apt。
bash
detect_os() {
[ -r /etc/os-release ] || die "未找到 /etc/os-release,无法识别系统"
# shellcheck disable=SC1091
. /etc/os-release
os_id="${ID:-}"
os_like="${ID_LIKE:-}"
log_info "识别系统: ID=$os_id ID_LIKE=$os_like"
}
is_rhel_like() {
[[ "$os_id" =~ ^(rhel|centos|rocky|almalinux)$ || "$os_like" == *"rhel"* ]]
}
is_debian_like() {
[[ "$os_id" =~ ^(debian|ubuntu)$ || "$os_like" == *"debian"* ]]
}ID_LIKE 能覆盖一部分衍生发行版。脚本只适配当前环境里会用到的系统,比写一堆很少验证的兼容分支更可靠。
五、只读体检
check 不修改系统,适合初始化前、初始化后、交接机器和变更复查时执行。输出保留真实状态,而不是只打印“正常”。
bash
cmd_check() {
log_info "开始系统体检"
echo "== 系统信息 =="
hostnamectl --static 2>/dev/null || hostname
cat /etc/os-release | grep -E '^(NAME|VERSION)=' || true
uname -r
echo
echo "== 时间同步 =="
timedatectl status --no-pager | grep -E 'Time zone|System clock synchronized|NTP service' || true
chronyc tracking 2>/dev/null | grep -E 'Reference ID|Leap status' || true
echo
echo "== 网络信息 =="
ip -brief addr
ip route | head -n 5
echo
echo "== 磁盘和内存 =="
df -h
free -h
swapon --show || true
echo
echo "== 监听端口 =="
ss -lntup
echo
echo "== 系统参数 =="
sysctl net.ipv4.ip_forward vm.swappiness 2>/dev/null || true
ulimit -n
}输出中包含主机名、时间同步、IP、磁盘、端口和关键参数,回看日志时能判断当时机器处在什么状态。
六、配置子命令
配置子命令按同一套规则处理:校验参数、确认当前状态、必要时备份配置、执行修改、输出复查方式。
设置主机名:
bash
cmd_set_hostname() {
local hostname_value="${1:-}"
local current
[ -n "$hostname_value" ] || die "缺少主机名,例如:set-hostname k3s-server-01"
current="$(hostnamectl --static 2>/dev/null || hostname)"
if [ "$current" = "$hostname_value" ]; then
log_info "主机名已经是 $hostname_value"
return 0
fi
require_root
run_cmd hostnamectl set-hostname "$hostname_value"
log_info "主机名已设置为 $hostname_value"
}追加 hosts:
bash
cmd_write_hosts() {
local hosts_file="${1:-}"
local line
[ -n "$hosts_file" ] || die "缺少 hosts 文件,例如:write-hosts hosts.cluster"
[ -r "$hosts_file" ] || die "hosts 文件不可读: $hosts_file"
require_root
backup_file /etc/hosts
while IFS= read -r line; do
[ -n "$line" ] || continue
[[ "$line" == \#* ]] && continue
if grep -Fxq "$line" /etc/hosts; then
log_info "hosts 记录已存在: $line"
continue
fi
log_info "追加 hosts 记录: $line"
if [ "$dry_run" -eq 0 ]; then
printf '%s\n' "$line" >> /etc/hosts
fi
done < "$hosts_file"
log_info "hosts 写入完成,可用 getent hosts <主机名> 复查解析结果"
}安装 chrony:
bash
install_chrony_if_needed() {
if command -v chronyc >/dev/null 2>&1; then
log_info "chrony 已安装"
return 0
fi
log_info "安装 chrony"
if is_rhel_like; then
if command -v dnf >/dev/null 2>&1; then
run_cmd dnf install -y chrony
else
run_cmd yum install -y chrony
fi
return 0
fi
if is_debian_like; then
run_cmd apt update
run_cmd apt install -y chrony
return 0
fi
die "当前系统暂未适配 chrony 安装: $os_id"
}配置时间同步:
bash
cmd_config_time() {
local ntp_server="${1:-ntp.aliyun.com}"
local conf
local service
require_root
detect_os
if is_rhel_like; then
conf="/etc/chrony.conf"
service="chronyd"
elif is_debian_like; then
conf="/etc/chrony/chrony.conf"
service="chrony"
else
die "当前系统暂未适配 chrony 配置: $os_id"
fi
install_chrony_if_needed
backup_file "$conf"
log_info "写入 chrony 配置: $conf"
if [ "$dry_run" -eq 0 ]; then
cat > "$conf" <<EOF
# 由 server-toolbox.sh 管理
server ${ntp_server} iburst
makestep 1.0 3
rtcsync
EOF
fi
run_cmd systemctl enable --now "$service"
run_cmd systemctl restart "$service"
log_info "时间同步已配置,复查命令: timedatectl status; chronyc sources -v"
}makestep 1.0 3 是比较常见的生产写法:服务启动初期允许大偏差快速校正,后续运行中尽量通过微调保持时间稳定。测试环境频繁恢复快照时,时间偏差可能更明显,恢复后用 timedatectl status 和 chronyc tracking 复查即可。
安装基础工具:
bash
cmd_install_tools() {
require_root
detect_os
if is_rhel_like; then
if command -v dnf >/dev/null 2>&1; then
run_cmd dnf install -y vim curl wget tar unzip lsof net-tools bind-utils bash-completion chrony
else
run_cmd yum install -y vim curl wget tar unzip lsof net-tools bind-utils bash-completion chrony
fi
return 0
fi
if is_debian_like; then
run_cmd apt update
run_cmd apt install -y vim curl wget tar unzip lsof net-tools dnsutils bash-completion chrony
return 0
fi
die "当前系统暂未适配基础工具安装: $os_id"
}写 sysctl:
bash
cmd_config_sysctl() {
local conf="/etc/sysctl.d/99-base.conf"
require_root
backup_file "$conf"
log_info "写入内核参数: $conf"
if [ "$dry_run" -eq 0 ]; then
cat > "$conf" <<'EOF'
# 由 server-toolbox.sh 管理
# 开启 IPv4 转发;容器网络、路由转发、LVS 等场景会用到
net.ipv4.ip_forward = 1
# 降低系统主动使用 swap 的倾向,具体值仍需结合业务内存情况
vm.swappiness = 10
EOF
fi
run_cmd sysctl --system
}写 limits:
bash
cmd_config_limits() {
local conf="/etc/security/limits.d/99-base.conf"
require_root
backup_file "$conf"
log_info "写入资源限制: $conf"
if [ "$dry_run" -eq 0 ]; then
cat > "$conf" <<'EOF'
# 由 server-toolbox.sh 管理
# nofile 控制最大打开文件数,高并发服务经常需要调高
* soft nofile 65535
* hard nofile 65535
# nproc 控制最大进程数,避免默认值过小影响批量任务或服务进程
* soft nproc 65535
* hard nproc 65535
EOF
fi
log_info "limits 已写入,新登录会话生效;systemd 服务还要看 unit 里的 LimitNOFILE"
}七、完整脚本
完整脚本如下,文件名示例为 server-toolbox.sh。
bash
#!/usr/bin/env bash
set -euo pipefail
dry_run=0
log_file="/tmp/server-toolbox.log"
os_id=""
os_like=""
command_name="help"
command_args=()
show_help() {
cat <<'EOF'
用法:
bash server-toolbox.sh [全局参数] <子命令> [参数]
全局参数:
--dry-run 只打印将要执行的动作,不修改系统
--log-file <路径> 指定日志文件,默认 /tmp/server-toolbox.log
-h, --help, help 显示帮助
子命令:
check 只读检查系统状态
set-hostname <名称> 设置主机名
write-hosts <文件> 将文件内容追加到 /etc/hosts,已存在的行会跳过
config-time [NTP服务器] 安装并配置 chrony,默认 ntp.aliyun.com
install-tools 安装常用排障工具
config-sysctl 写入基础 sysctl 参数
config-limits 写入 limits 文件句柄和进程数限制
init-all <主机名> [hosts文件] 执行常用初始化动作
示例:
bash server-toolbox.sh check
bash server-toolbox.sh --dry-run config-time 192.168.10.1
bash server-toolbox.sh set-hostname k3s-server-01
bash server-toolbox.sh write-hosts hosts.cluster
bash server-toolbox.sh init-all k3s-server-01 hosts.cluster
EOF
}
log_info() {
printf '[信息] %s %s\n' "$(date '+%F %T')" "$*" | tee -a "$log_file"
}
log_error() {
printf '[错误] %s %s\n' "$(date '+%F %T')" "$*" | tee -a "$log_file" >&2
}
die() {
log_error "$*"
exit 1
}
run_cmd() {
log_info "执行命令: $*"
# dry-run 只打印动作,用于批量执行前确认脚本会改哪些地方
if [ "$dry_run" -eq 1 ]; then
return 0
fi
"$@"
}
require_root() {
# dry-run 不实际修改系统,可以在普通用户下预览
if [ "$dry_run" -eq 1 ]; then
return 0
fi
[ "$(id -u)" -eq 0 ] || die "当前操作需要 root 权限"
}
backup_file() {
local file_path="$1"
local backup_path
[ -e "$file_path" ] || return 0
backup_path="${file_path}.$(date '+%F-%H%M%S').bak"
log_info "备份文件: $file_path -> $backup_path"
if [ "$dry_run" -eq 0 ]; then
cp -a "$file_path" "$backup_path"
fi
}
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run)
dry_run=1
shift
;;
--log-file)
log_file="${2:-}"
[ -n "$log_file" ] || die "--log-file 缺少路径"
shift 2
;;
-h|--help|help)
command_name="help"
shift
break
;;
*)
command_name="$1"
shift
break
;;
esac
done
command_args=("$@")
}
detect_os() {
[ -r /etc/os-release ] || die "未找到 /etc/os-release,无法识别系统"
# shellcheck disable=SC1091
. /etc/os-release
os_id="${ID:-}"
os_like="${ID_LIKE:-}"
log_info "识别系统: ID=$os_id ID_LIKE=$os_like"
}
is_rhel_like() {
[[ "$os_id" =~ ^(rhel|centos|rocky|almalinux)$ || "$os_like" == *"rhel"* ]]
}
is_debian_like() {
[[ "$os_id" =~ ^(debian|ubuntu)$ || "$os_like" == *"debian"* ]]
}
cmd_check() {
log_info "开始系统体检"
echo "== 系统信息 =="
hostnamectl --static 2>/dev/null || hostname
cat /etc/os-release | grep -E '^(NAME|VERSION)=' || true
uname -r
echo
echo "== 时间同步 =="
timedatectl status --no-pager | grep -E 'Time zone|System clock synchronized|NTP service' || true
chronyc tracking 2>/dev/null | grep -E 'Reference ID|Leap status' || true
echo
echo "== 网络信息 =="
ip -brief addr
ip route | head -n 5
echo
echo "== 磁盘和内存 =="
df -h
free -h
swapon --show || true
echo
echo "== 监听端口 =="
ss -lntup
echo
echo "== 系统参数 =="
sysctl net.ipv4.ip_forward vm.swappiness 2>/dev/null || true
ulimit -n
}
cmd_set_hostname() {
local hostname_value="${1:-}"
local current
[ -n "$hostname_value" ] || die "缺少主机名,例如:set-hostname k3s-server-01"
current="$(hostnamectl --static 2>/dev/null || hostname)"
if [ "$current" = "$hostname_value" ]; then
log_info "主机名已经是 $hostname_value"
return 0
fi
require_root
run_cmd hostnamectl set-hostname "$hostname_value"
log_info "主机名已设置为 $hostname_value"
}
cmd_write_hosts() {
local hosts_file="${1:-}"
local line
[ -n "$hosts_file" ] || die "缺少 hosts 文件,例如:write-hosts hosts.cluster"
[ -r "$hosts_file" ] || die "hosts 文件不可读: $hosts_file"
require_root
backup_file /etc/hosts
while IFS= read -r line; do
[ -n "$line" ] || continue
[[ "$line" == \#* ]] && continue
if grep -Fxq "$line" /etc/hosts; then
log_info "hosts 记录已存在: $line"
continue
fi
log_info "追加 hosts 记录: $line"
if [ "$dry_run" -eq 0 ]; then
printf '%s\n' "$line" >> /etc/hosts
fi
done < "$hosts_file"
log_info "hosts 写入完成,可用 getent hosts <主机名> 复查解析结果"
}
install_chrony_if_needed() {
if command -v chronyc >/dev/null 2>&1; then
log_info "chrony 已安装"
return 0
fi
log_info "安装 chrony"
if is_rhel_like; then
if command -v dnf >/dev/null 2>&1; then
run_cmd dnf install -y chrony
else
run_cmd yum install -y chrony
fi
return 0
fi
if is_debian_like; then
run_cmd apt update
run_cmd apt install -y chrony
return 0
fi
die "当前系统暂未适配 chrony 安装: $os_id"
}
cmd_config_time() {
local ntp_server="${1:-ntp.aliyun.com}"
local conf
local service
require_root
detect_os
if is_rhel_like; then
conf="/etc/chrony.conf"
service="chronyd"
elif is_debian_like; then
conf="/etc/chrony/chrony.conf"
service="chrony"
else
die "当前系统暂未适配 chrony 配置: $os_id"
fi
install_chrony_if_needed
backup_file "$conf"
log_info "写入 chrony 配置: $conf"
if [ "$dry_run" -eq 0 ]; then
cat > "$conf" <<EOF
# 由 server-toolbox.sh 管理
server ${ntp_server} iburst
makestep 1.0 3
rtcsync
EOF
fi
run_cmd systemctl enable --now "$service"
run_cmd systemctl restart "$service"
log_info "时间同步已配置,复查命令: timedatectl status; chronyc sources -v"
}
cmd_install_tools() {
require_root
detect_os
if is_rhel_like; then
if command -v dnf >/dev/null 2>&1; then
run_cmd dnf install -y vim curl wget tar unzip lsof net-tools bind-utils bash-completion chrony
else
run_cmd yum install -y vim curl wget tar unzip lsof net-tools bind-utils bash-completion chrony
fi
return 0
fi
if is_debian_like; then
run_cmd apt update
run_cmd apt install -y vim curl wget tar unzip lsof net-tools dnsutils bash-completion chrony
return 0
fi
die "当前系统暂未适配基础工具安装: $os_id"
}
cmd_config_sysctl() {
local conf="/etc/sysctl.d/99-base.conf"
require_root
backup_file "$conf"
log_info "写入内核参数: $conf"
if [ "$dry_run" -eq 0 ]; then
cat > "$conf" <<'EOF'
# 由 server-toolbox.sh 管理
# 开启 IPv4 转发;容器网络、路由转发、LVS 等场景会用到
net.ipv4.ip_forward = 1
# 降低系统主动使用 swap 的倾向,具体值仍需结合业务内存情况
vm.swappiness = 10
EOF
fi
run_cmd sysctl --system
}
cmd_config_limits() {
local conf="/etc/security/limits.d/99-base.conf"
require_root
backup_file "$conf"
log_info "写入资源限制: $conf"
if [ "$dry_run" -eq 0 ]; then
cat > "$conf" <<'EOF'
# 由 server-toolbox.sh 管理
# nofile 控制最大打开文件数,高并发服务经常需要调高
* soft nofile 65535
* hard nofile 65535
# nproc 控制最大进程数,避免默认值过小影响批量任务或服务进程
* soft nproc 65535
* hard nproc 65535
EOF
fi
log_info "limits 已写入,新登录会话生效;systemd 服务还要看 unit 里的 LimitNOFILE"
}
cmd_init_all() {
local hostname_value="${1:-}"
local hosts_file="${2:-}"
[ -n "$hostname_value" ] || die "缺少主机名,例如:init-all k3s-server-01 hosts.cluster"
cmd_set_hostname "$hostname_value"
if [ -n "$hosts_file" ]; then
cmd_write_hosts "$hosts_file"
fi
cmd_install_tools
cmd_config_time "ntp.aliyun.com"
cmd_config_sysctl
cmd_config_limits
cmd_check
}
main() {
parse_args "$@"
case "$command_name" in
help)
show_help
;;
check)
cmd_check "${command_args[@]}"
;;
set-hostname)
cmd_set_hostname "${command_args[@]}"
;;
write-hosts)
cmd_write_hosts "${command_args[@]}"
;;
config-time)
cmd_config_time "${command_args[@]}"
;;
install-tools)
cmd_install_tools "${command_args[@]}"
;;
config-sysctl)
cmd_config_sysctl "${command_args[@]}"
;;
config-limits)
cmd_config_limits "${command_args[@]}"
;;
init-all)
cmd_init_all "${command_args[@]}"
;;
*)
show_help >&2
die "未知子命令: $command_name"
;;
esac
}
main "$@"八、批量执行
节点清单 nodes.txt:
text
192.168.10.11 k3s-server-01
192.168.10.12 k3s-server-02
192.168.10.13 k3s-server-03hosts 片段 hosts.cluster:
text
192.168.10.11 k3s-server-01
192.168.10.12 k3s-server-02
192.168.10.13 k3s-server-03批量执行示例:
bash
#!/usr/bin/env bash
set -euo pipefail
node_file="nodes.txt"
hosts_file="hosts.cluster"
while read -r ip hostname_value; do
[ -n "${ip:-}" ] || continue
[[ "$ip" == \#* ]] && continue
echo "===== 初始化 $ip / $hostname_value ====="
# 分发工具箱脚本和 hosts 片段
scp server-toolbox.sh "$hosts_file" "root@${ip}:/root/"
# 远端执行组合命令;BatchMode 避免认证失败时卡在交互输入
ssh -o BatchMode=yes -o ConnectTimeout=5 "root@${ip}" \
"bash /root/server-toolbox.sh init-all '$hostname_value' /root/$hosts_file"
done < "$node_file"批量执行前适合跑 dry-run:
bash
ssh -o BatchMode=yes -o ConnectTimeout=5 root@192.168.10.11 \
"bash /root/server-toolbox.sh --dry-run init-all k3s-server-01 /root/hosts.cluster"dry-run 只打印动作,不真正修改系统。确认主机名、hosts 文件、NTP 源都对上后,再执行正式初始化。
九、和系统基线的关系
脚本只是把固定动作做成入口。每个配置项背后的含义仍然要能看懂,比如 chrony 的 makestep 为什么只在启动初期做大偏差校正,limits.d 为什么不一定影响 systemd 服务,sysctl.d 为什么比直接改 /etc/sysctl.conf 更好管理。对应的系统层说明放在 系统初始化基线。