这篇文章记录一台小规格云服务器从购买、首次登录,到磁盘扩容、内存优化、安全检查、流量统计和微信告警的完整过程。

最初的选购和配置思路参考了这篇云服务器记录,但本文中的分区、内核和流量阈值均按这台实例的实际情况重新核对。免费流量额度及计费规则可能调整,应以自己的云控制台为准。

中间还踩过一次比较严重的坑:在仅有约 40MB 的 /boot 分区上直接升级内核,重启后系统无法识别根磁盘,SSH 也随之超时。最终通过重新写入操作系统恢复,并重新完成全部配置。

1. 服务器配置与用途

本次使用的实例规格如下:

项目配置
CPU2 vCPU
内存512MB
系统盘5GB ESSD
公网带宽80Mbps 峰值,按流量计费
操作系统Alpine Linux 3.20.2
初始内核6.6.48-0-virt

512MB 内存理论上也能启动精简后的 Ubuntu Server,但可用空间会明显更紧。Alpine 使用 BusyBox、musl 和 apk,基础系统很小,更适合这种配置。

2. 安全组与首次登录

安全组入方向只开放 SSH:

协议端口来源
TCP22自己当前公网 IP /32

不建议为了省事把 SSH 来源长期设置为 0.0.0.0/0。如果家庭宽带公网 IP 发生变化,再到安全组中更新来源即可。

在本地 Windows 命令行登录:

ssh root@<服务器公网IP>

首次连接会看到主机指纹确认提示。确认 IP 与实例无误后输入 yes。密码输入过程中不会显示字符,这是 SSH 的正常行为。

如果创建实例时选择了"创建后设置"登录凭证,可以先从云控制台终端进入系统,再设置 root 密码:

passwd root

两类 SSH 报错含义不同:

报错常见原因
Permission denied用户名、密码或 SSH 认证配置不正确
Connection timed out安全组、客户端公网 IP、SSH 服务或系统启动异常

3. 初始状态检查

登录后先做只读检查,不要立即升级系统:

cat /etc/alpine-release
uname -a
free -m
df -h /
cat /proc/partitions
fdisk -l /dev/vda
mount | grep ' on / '
cat /etc/fstab
ip addr
ip route
netstat -lntp
rc-status -a

当时的关键结果是:

Alpine:     3.20.2
Kernel:     6.6.48-0-virt
Memory:     431MiB 可见
Swap:       0
Disk:       /dev/vda 共 5GB
Root:       /dev/vda2 只有约 976MB
Boot:       /dev/vda1 约 47.6MB
Filesystem: ext4

虽然云盘容量是 5GB,但自定义镜像中的根分区没有自动扩展,只使用了约 1GB。剩余磁盘空间还没有分配给 /dev/vda2。

ip addr 中只看到类似 172.18.x.x 的私网地址也是正常的。云厂商通常在虚拟网络层把公网 IP 映射到实例,公网 IP 不一定直接出现在系统网卡上。

4. 扩展根分区到 5GB

镜像已经配置了 Alpine 3.20 的阿里云镜像源,但 community 仓库被注释。先备份配置并启用它:

cp /etc/apk/repositories /etc/apk/repositories.bak
sed -i 's|^#http://mirrors.aliyun.com/alpine/v3.20/community|http://mirrors.aliyun.com/alpine/v3.20/community|' /etc/apk/repositories
apk update

安装分区和 ext4 扩容工具:

apk add cloud-utils-growpart e2fsprogs e2fsprogs-extra util-linux

扩展第二个分区,再在线扩展 ext4 文件系统:

growpart /dev/vda 2
resize2fs /dev/vda2

验证:

lsblk -f
df -h / /boot

最终结果:

/dev/vda2  4.9G  63.9M  4.6G   1% /
/dev/vda1 39.7M  23.9M 12.5M  66% /boot

根分区已经使用完整云盘,/boot 仍保持约 40MB。这是当前镜像的分区设计,不应在没有迁移和备份方案的情况下强行调整。

5. 一次内核升级导致的启动故障

扩容后,我曾执行完整的:

apk upgrade

它把 linux-virt 从 6.6.48 升级到了 6.6.142。当时 /boot 只剩约 12.5MB,新的内核、initramfs 与 VirtIO 驱动链没有正确形成可启动组合。

重启后的表现是:

  • 系统无法正常识别 /dev/vda 根磁盘;
  • SSH 22 端口无法建立连接;
  • 本地只看到 Connection timed out;
  • 普通重启和"重新初始化云盘"没有解决问题。

最终恢复方式是在云控制台执行"更换操作系统",使用原来的自定义 Alpine 镜像重新写入系统盘,然后重新扩容和配置。

恢复后再次确认三处内核版本一致:

uname -r
apk info -v | grep '^linux-virt-'
ls -1 /lib/modules

结果为:

6.6.48-0-virt
linux-virt-6.6.48-r0
6.6.48-0-virt

这台机器目前保留可正常启动的 6.6.48-0-virt。后续只执行 apk update 和按需 apk add <软件包>,不直接运行完整 apk upgrade,尤其不要在没有处理 /boot 空间和 initramfs 的情况下升级 linux-virt。

6. 配置 384MB ZRAM

这台机器只有约 431MiB 可见内存,并且初始没有 Swap。ZRAM 会在内存中创建压缩交换区,用少量 CPU 换取更高的有效内存容量,比磁盘 Swap 更适合这种小实例。

先检查内核支持:

modprobe zram
ls -l /dev/zram0
cat /sys/block/zram0/comp_algorithm

本机支持:

lzo lzo-rle [lz4] lz4hc

方括号表示当前算法,默认已经是速度较好的 lz4。

6.1 临时验证

先临时启用 384MB:

zramctl /dev/zram0 --size 384M
mkswap -L zram0 /dev/zram0
swapon -p 100 /dev/zram0
zramctl
swapon --show
free -h

确认无误后,安装 Alpine 官方管理服务:

apk add zram-init zram-init-openrc
cp /etc/conf.d/zram-init /etc/conf.d/zram-init.bak

默认配置会创建两个 ZRAM 设备,第二个还准备挂载到 /tmp,不适合本机。因此只保留一个 Swap 设备:

sed -i \
  -e 's/^num_devices=.*/num_devices=1/' \
  -e 's/^flag0=.*/flag0=100/' \
  -e 's/^size0=.*/size0=384/' \
  -e 's/^maxs0=.*/maxs0=2/' \
  -e 's/^algo0=.*/algo0=lz4/' \
  /etc/conf.d/zram-init

相关配置最终为:

load_on_start=yes
unload_on_stop=yes
num_devices=1
type0=swap
flag0=100
size0=384
maxs0=2
algo0=lz4
labl0=zram_swap

6.2 切换为 OpenRC 服务

因为前面已经手动创建过临时 ZRAM,所以先关闭并重置它:

swapoff /dev/zram0
zramctl --reset /dev/zram0

再交给 OpenRC 管理:

rc-service zram-init start
rc-update add zram-init default

重启后验证:

rc-service zram-init status
zramctl
swapon --show
free -h

结果如下:

/dev/zram0  lz4  384M  [SWAP]
priority:   100
service:    started

ZRAM 的 384M 是逻辑容量,不会在启动时一次性占满 384MB 内存。只有产生交换数据时才逐步使用压缩后的物理内存。

7. 做一次只读安全检查

由于系统来自自定义镜像,正式使用前检查账号、公钥、端口、服务和计划任务。

7.1 可登录账号

awk -F: '$7 !~ /(nologin|false)$/ {print $1 ":" $3 ":" $6 ":" $7}' /etc/passwd

结果只有 root 具备普通交互 Shell;sync、shutdown 和 halt 是 Alpine 内置系统账号。

7.2 root SSH 公钥

if [ -f /root/.ssh/authorized_keys ]; then
    ssh-keygen -lf /root/.ssh/authorized_keys
else
    echo "no authorized_keys"
fi

本机没有预装陌生公钥。

7.3 监听端口和开机服务

netstat -lntup
rc-status default

对外监听的只有 SSH 22 端口。默认服务为:

acpid
crond
sshd
openntpd
zram-init

0.0.0.0:22 和 :::22 分别表示 IPv4 与 IPv6 监听,并不代表开放了两个不同端口。真正允许哪些公网地址访问,仍由云安全组决定。

7.4 定时任务、启动脚本和进程

cat /etc/crontabs/root
ls -la /etc/local.d
ps aux

检查结果:

  • root crontab 只有 Alpine 默认的周期维护任务;
  • /etc/local.d 只有 README,没有自定义启动脚本;
  • 进程均为内核线程、网络、日志、时间同步、SSH 和终端服务;
  • 没有发现陌生下载器、代理或异常后台进程。

8. 使用 vnStat 持久化统计流量

我最初尝试安装阿里云主机监控 Agent,但它在这套自定义 Alpine 环境中安装失败。检查后没有发现残留进程或 OpenRC 服务,因此改用更轻量的 vnStat。

安装:

apk add vnstat

Alpine 会同时安装 vnstat-openrc。先启动守护进程,让它创建数据库并发现 eth0:

rc-update add vnstatd default
rc-service vnstatd start
rc-service vnstatd status

验证数据库中的接口:

vnstat --iflist
vnstat --dbiflist
vnstat -i eth0

如果数据库已经存在但没有 eth0,可以在守护进程启动后执行:

vnstat --add -i eth0
rc-service vnstatd restart

不要在数据库首次创建前急着运行 vnstat --add,否则可能看到数据库不存在的提示。

8.1 将保存间隔调整为 1 分钟

vnStat 默认每 5 分钟保存一次数据库。80Mbps 满速 5 分钟理论上可产生约 3GB 流量,告警可能过晚,因此将保存间隔改为 1 分钟。

原配置行以分号开头,分号表示注释:

;SaveInterval 5

备份并启用新值:

cp /etc/vnstat.conf /etc/vnstat.conf.bak
sed -i 's/^;SaveInterval[[:space:]].*/SaveInterval 1/' /etc/vnstat.conf
rc-service vnstatd restart

验证实际配置:

vnstat --showconfig | grep '^SaveInterval'
rc-service vnstatd status

结果应为:

SaveInterval 1
status: started

常用查询命令:

vnstat -i eth0       # 摘要
vnstat -d -i eth0    # 每日流量
vnstat -m -i eth0    # 每月流量

其中 rx 是接收流量,tx 是服务器发送流量。按流量计费时重点关注 tx。

需要注意,vnStat 只统计安装之后这台机器网卡看到的流量,不等同于云厂商账单,也看不到同一账号下其他产品共享的流量额度。

9. 通过 Server酱发送微信告警

Server酱提供一个简单的 HTTP 接口,可以把服务器通知发送到微信。本次只在超过阈值时推送,不做每日汇报,因此产生的网络流量可以忽略。

先在 Server酱官网 使用微信登录、绑定推送通道,并获取以 SCT 开头的 SendKey。

安装 curl 并创建凭证目录:

apk add curl
mkdir -p /etc/traffic-guard
chmod 700 /etc/traffic-guard
umask 077

安全读取 SendKey。下面命令中的 SCT_SENDKEY 是变量名,不需要替换:

read -r -s -p "Paste SendKey (hidden): " SCT_SENDKEY

看到提示后再粘贴真正的 SendKey 并回车,输入内容不会显示。随后保存:

printf '\n'
printf '%s\n' "$SCT_SENDKEY" > /etc/traffic-guard/serverchan.key
chmod 600 /etc/traffic-guard/serverchan.key
unset SCT_SENDKEY

只验证格式,不输出密钥:

ls -l /etc/traffic-guard/serverchan.key
grep -q '^SCT' /etc/traffic-guard/serverchan.key \
  && echo "SendKey format OK" \
  || echo "SendKey format invalid"

10. 创建 18GB 微信告警脚本

脚本使用 vnStat 2.12 自带的结构化阈值判断:

vnstat -i eth0 --alert 0 3 monthly tx 18 GB

这里使用十进制 GB,不是二进制 GiB。脚本达到阈值后发送一次微信消息,并创建当月标记文件,避免每分钟重复推送。

创建 /usr/local/sbin/traffic-alert:

#!/bin/sh

PATH=/usr/sbin:/usr/bin:/sbin:/bin
IFACE=eth0
LIMIT_GB=18
KEY_FILE=/etc/traffic-guard/serverchan.key
STATE_DIR=/var/lib/traffic-guard
MONTH=$(date +%Y-%m)
MARKER="$STATE_DIR/warned-$MONTH-${LIMIT_GB}GB"

mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"

if [ ! -s "$KEY_FILE" ]; then
    logger -t traffic-alert "ServerChan key file is missing or empty"
    exit 1
fi

if [ "${1:-}" = "--test" ]; then
    notice_title="Alpine ECS traffic alert test"
    notice_body="Scheduled monthly traffic alert is working."
    create_marker=0
else
    [ -e "$MARKER" ] && exit 0

    if vnstat -i "$IFACE" --alert 0 3 monthly tx "$LIMIT_GB" GB; then
        exit 0
    fi

    notice_title="Alpine ECS traffic warning"
    notice_body="Monthly outbound traffic on $IFACE has exceeded ${LIMIT_GB} GB.

$(vnstat -m -i "$IFACE")

Check Alibaba Cloud CDT usage and stop unexpected traffic."
    create_marker=1
fi

notice_key=$(cat "$KEY_FILE")
notice_response=$(curl -sS --connect-timeout 10 --max-time 20 \
    --data-urlencode "title=$notice_title" \
    --data-urlencode "desp=$notice_body" \
    "https://sctapi.ftqq.com/${notice_key}.send") || exit 1
unset notice_key

if ! printf '%s' "$notice_response" |
    grep -Eq '"code"[[:space:]]*:[[:space:]]*0([,}])'; then
    logger -t traffic-alert "ServerChan rejected the notification"
    exit 1
fi
unset notice_response

if [ "$create_marker" -eq 1 ]; then
    : > "$MARKER"
    chmod 600 "$MARKER"
fi

logger -t traffic-alert "Notification sent successfully"
exit 0

设置权限并检查语法:

chmod 700 /usr/local/sbin/traffic-alert
sh -n /usr/local/sbin/traffic-alert

sh -n 没有输出代表语法检查通过。测试微信推送:

/usr/local/sbin/traffic-alert --test
echo "exit=$?"

返回 exit=0 且微信收到消息,说明脚本正常。测试模式不会创建 18GB 告警标记。

11. 每分钟自动检查

先把 root crontab 备份到 crontab 目录之外,避免备份文件被 crond 当成新任务:

cp /etc/crontabs/root /root/crontab.root.before-traffic-alert

幂等添加任务,重复执行也不会添加多行:

grep -qxF '* * * * * /usr/local/sbin/traffic-alert' /etc/crontabs/root \
  || echo '* * * * * /usr/local/sbin/traffic-alert' >> /etc/crontabs/root

重启并验证 crond:

rc-service crond restart
rc-service crond status
tail -n 5 /etc/crontabs/root

最终任务为:

* * * * * /usr/local/sbin/traffic-alert

未达到阈值时,脚本只查询本地 SQLite 数据库,不访问互联网。只有超过 18GB 或手动执行 --test 时,才会向 Server酱发送一个几 KB 到几十 KB 的 HTTPS 请求。

需要停用告警时,只删除这一条任务:

sed -i '\|^\* \* \* \* \* /usr/local/sbin/traffic-alert$|d' /etc/crontabs/root
rc-service crond restart

12. 最终状态

完成配置并重启后,最终状态如下:

项目最终结果
内核6.6.48-0-virt
根分区约 4.9GB
可用内存约 431MiB
ZRAM384MB,lz4,优先级 100
公网监听仅 SSH 22
流量统计vnStat,数据每分钟保存
微信告警月出站流量超过十进制 18GB 时通知一次

最终验证命令:

uname -r
df -h / /boot
free -h
zramctl
swapon --show
rc-service zram-init status
rc-service vnstatd status
rc-service crond status
netstat -lntup
vnstat -m -i eth0

13. 这次配置最重要的经验

  1. 云盘容量变大,不代表镜像中的根分区会自动扩展,必须同时扩分区和文件系统。
  2. 512MB 实例更适合 Alpine;ZRAM 能显著降低轻量服务因内存不足退出的概率。
  3. 自定义镜像不能无脑执行完整系统升级,尤其要先检查 /boot、内核模块和 initramfs。
  4. SSH 超时和密码错误是两类故障,应分别从网络链路与认证配置排查。
  5. vnStat 的统计是本机视角,不等于云厂商账单,但很适合做低成本的早期告警。
  6. 80Mbps 只是峰值能力,按流量计费时速度越高,异常流量消耗额度也越快。
  7. 告警凭证必须单独保存在权限为 600 的文件中,不能硬编码进公开脚本。

到这里,这台 2 核 512MB 的 Alpine 小服务器已经完成基础初始化,可以作为 Linux、网络和轻量后端实验环境继续使用。