QEMU Live Migration 下 pgbench 持续读写零中断
在两台真实机器上演示 QEMU Live Migration,VM 内运行 PostgreSQL,外部 pgbench 2 并发持续读写,
架构总览
Node A (9.134.130.23) Node B (21.6.160.243)
┌────────────────────────────┐ ┌────────────────────────────┐
│ [pgbench-ns] 10.0.0.1 │ │ │
│ │ (veth) │ │ │
│ neon-br0 │ │ neon-br0 │
│ ├── tap-mig ─ QEMU(1G) │ │ ├── tap-mig ─ QEMU(1G) │
│ └── neon-vxlan0 │ │ └── neon-vxlan0 │
│ │ UDP:4789 │ │ │ UDP:4789 │
│ eth:9.134.130.23 ◄──────────────────► eth:21.6.160.243 │
│ │ │ │
│ VM: PostgreSQL on tmpfs │ QMP migrate│ QEMU -incoming │
│ IP: 10.0.0.7/24 │ ─────────►│ tcp:0:20187 │
│ QMP: /tmp/qmp.sock │ │ │
└────────────────────────────┘ └────────────────────────────┘
关键设计:
PG 数据放 tmpfs → 所有数据随 QEMU 内存迁移 → 完美避免磁盘状态不一致
pgbench 通过 namespace + veth 接入 neon-br0 → 和 VM 同一 overlay 网络
迁移流程:
- Node A VM 运行 PostgreSQL(tmpfs 上),pgbench 持续 2 并发读写
- Node B QEMU 以
-incoming模式等待 - QMP 触发 migrate,QEMU 搬内存(含 PG 全部数据)
- 迁移完成,pgbench 继续运行,零失败事务
环境信息
| 项目 | 值 |
|---|---|
| Node A | 9.134.130.23 (TencentOS 4.2) |
| Node B | 21.6.160.243 (TencentOS 3.2) |
| SSH 端口 | 36000 |
| QEMU | Docker 容器内 QEMU 8.1.5 |
| QEMU 加速 | -accel tcg(无 KVM) |
| Docker 镜像 | qemu-alpine(Alpine 3.19 + qemu-system-x86_64) |
| VM 内存 | 1024MB(PG + pgbench 数据需要更多内存) |
| VM 磁盘 | alpine-pg.qcow2(Alpine 3.19 + PostgreSQL 16.11) |
| PG 数据目录 | tmpfs(/var/lib/postgresql/data,512MB) |
| 迁移端口 | 20187 |
| VXLAN VNI | 100 |
| VM Overlay IP | 10.0.0.7/24 |
| pgbench NS IP | 10.0.0.1/24 |
| pgbench 参数 | -c 2 -j 1 -s 5(2 并发,scale=5,50 万行) |
| 启动方式 | -kernel + -initrd(直接内核启动,绕过 bootloader) |
Phase A:镜像准备(一次性)
制作包含 Alpine + PostgreSQL 的 qcow2 镜像,两端使用相同镜像。
A1:搭建临时 NAT 网络(让 VM 访问互联网)
bash
# === Node A 上执行 ===
sudo -i
# 创建临时 bridge + tap
ip link add br-setup type bridge
ip addr add 192.168.100.1/24 dev br-setup
ip link set br-setup up
ip tuntap add mode tap name tap-setup
ip link set tap-setup master br-setup
ip link set tap-setup up
# NAT MASQUERADE
HOST_IFACE=$(ip route | grep default | awk '{print $5}' | head -1)
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -s 192.168.100.1/24 -o $HOST_IFACE -j MASQUERADE
iptables -A FORWARD -i br-setup -o $HOST_IFACE -j ACCEPT
iptables -A FORWARD -i $HOST_IFACE -o br-setup -m state --state RELATED,ESTABLISHED -j ACCEPT
A2:启动 VM 安装 Alpine 到磁盘
bash
# 创建 2G qcow2 磁盘
qemu-img create -f qcow2 /tmp/alpine-pg.qcow2 2G
# 启动 QEMU(ISO + 磁盘 + 临时网络)
docker run -d --privileged --net=host \
--name qemu-install \
-v /tmp:/tmp \
qemu-alpine \
qemu-system-x86_64 \
-machine q35 -accel tcg -display none -no-reboot -cpu max \
-m 1024M -smp cpus=1 \
-cdrom /tmp/alpine.iso \
-drive file=/tmp/alpine-pg.qcow2,if=virtio,cache=none \
-netdev tap,id=net0,ifname=tap-setup,script=no,downscript=no \
-device virtio-net-pci,netdev=net0 \
-chardev socket,id=char0,path=/tmp/qemu-serial.sock,server=on,wait=off \
-serial chardev:char0 \
-qmp unix:/tmp/qmp.sock,server,nowait
# 等待启动(TCG 约 60 秒)
sleep 60
连入串口(root 无密码):
bash
socat -,rawer UNIX-CONNECT:/tmp/qemu-serial.sock
VM 内配网:
bash
ip link set eth0 up
ip addr add 192.168.100.10/24 dev eth0
ip route add default via 192.168.100.1
# 使用宿主机的 DNS(从 /etc/resolv.conf 获取)
echo -e "nameserver 9.218.230.76\nnameserver 9.218.228.72" > /etc/resolv.conf
# 验证
ping -c 2 192.168.100.1 # 应成功
wget -q -O /dev/null http://dl-cdn.alpinelinux.org/alpine/v3.19/main/x86_64/APKINDEX.tar.gz && echo OK
A3:安装 Alpine 到磁盘 + PostgreSQL
bash
# === VM 内执行 ===
# 配置仓库
setup-apkrepos -1
# 安装分区工具
apk add e2fsprogs sfdisk
# 分区 + 格式化
echo ";" | sfdisk /dev/vda
mkfs.ext4 -F /dev/vda1
# 挂载
mount -t ext4 /dev/vda1 /mnt
# 安装 Alpine 到磁盘
KERNELOPTS="console=ttyS0,115200" setup-disk -m sys /mnt
# 安装内核和 bootloader
apk add --root /mnt linux-virt syslinux
# 安装 MBR
dd if=/usr/share/syslinux/mbr.bin of=/dev/vda bs=440 count=1
extlinux --install /mnt/boot
# 配置 bootloader(使用相对路径,因为 /boot 就是分区根目录)
cat > /mnt/boot/extlinux.conf << 'EOF'
DEFAULT virt
LABEL virt
KERNEL vmlinuz-virt
INITRD initramfs-virt
APPEND root=/dev/vda1 modules=ext4 console=ttyS0,115200
EOF
# 安装 PostgreSQL
apk add --root /mnt postgresql16 postgresql16-contrib
实际使用:直接内核启动
TCG 下 extlinux 可能加载慢或不稳定。实际我们从 qcow2 中提取 vmlinuz-virt 和
initramfs-virt,用 QEMU
-kernel/-initrd直接启动,更快更稳定。
创建 PG 启动脚本
bash
# === VM 内执行 ===
cat > /mnt/usr/local/bin/start-pg-tmpfs.sh << 'SCRIPT'
#!/bin/sh
set -e
PGDATA="/var/lib/postgresql/data"
echo "=== Starting PostgreSQL on tmpfs ==="
# Create runtime directory
mkdir -p /run/postgresql
chown postgres:postgres /run/postgresql
# Mount tmpfs for PG data (data lives in RAM, migrates with QEMU memory)
mkdir -p "$PGDATA"
mount -t tmpfs -o size=512M tmpfs "$PGDATA"
chown postgres:postgres "$PGDATA"
chmod 700 "$PGDATA"
echo "[OK] tmpfs mounted at $PGDATA"
# Initialize database
su postgres -c "initdb -D $PGDATA --no-locale --encoding=UTF8"
echo "[OK] initdb completed"
# Configure PostgreSQL for remote access
cat >> "$PGDATA/postgresql.conf" << 'PGCONF'
listen_addresses = '*'
shared_buffers = 128MB
max_connections = 50
wal_level = minimal
max_wal_senders = 0
fsync = off
synchronous_commit = off
full_page_writes = off
PGCONF
# Allow all connections (trust, no password)
cat > "$PGDATA/pg_hba.conf" << 'HBA'
local all all trust
host all all 0.0.0.0/0 trust
host all all ::/0 trust
HBA
echo "[OK] PostgreSQL configured"
# Start PostgreSQL
su postgres -c "pg_ctl -D $PGDATA -l /tmp/pg.log start"
sleep 2
# Create benchmark database
su postgres -c "createdb benchdb"
echo "[OK] benchdb created"
# Verify
su postgres -c "pg_isready"
echo "=== PostgreSQL ready ==="
SCRIPT
chmod +x /mnt/usr/local/bin/start-pg-tmpfs.sh
关键设计:PG 数据放 tmpfs
- PostgreSQL 的数据目录(
PGDATA)mount 在 tmpfs 上- tmpfs 的数据存在于 VM 内存中
- QEMU Live Migration 传输全部 VM 内存
- 因此 PG 数据随迁移自动搬到目标端,完美避免磁盘状态不一致
- 生产环境中 Neon 使用共享存储(Pageserver),这里用 tmpfs 模拟同样效果
配置串口 console
bash
# 确保 ttyS0 有 getty
grep -q 'ttyS0::respawn' /mnt/etc/inittab || \
echo 'ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100' >> /mnt/etc/inittab
# 允许 root 无密码登录
sed -i 's|^root:.*|root::0:0:root:/root:/bin/ash|' /mnt/etc/passwd
echo ttyS0 >> /mnt/etc/securetty
# fstab
echo "/dev/vda1 / ext4 defaults 0 1" > /mnt/etc/fstab
A4:关机,提取内核,拷贝到 Node B
bash
# VM 内
sync
umount /mnt
poweroff
bash
# === Node A 宿主上执行 ===
docker rm -f qemu-install
# 提取内核和 initrd(用于 -kernel 直接启动)
modprobe nbd max_part=8
qemu-nbd --connect=/dev/nbd0 /tmp/alpine-pg.qcow2
sleep 2
mount /dev/nbd0p1 /mnt
cp /mnt/boot/vmlinuz-virt /tmp/vmlinuz-virt
cp /mnt/boot/initramfs-virt /tmp/initramfs-virt
chmod 644 /tmp/vmlinuz-virt /tmp/initramfs-virt
umount /mnt
qemu-nbd --disconnect /dev/nbd0
# 拷贝到 Node B
scp -P 36000 /tmp/alpine-pg.qcow2 /tmp/vmlinuz-virt /tmp/initramfs-virt 21.6.160.243:/tmp/
# 校验
md5sum /tmp/alpine-pg.qcow2
ssh -p 36000 21.6.160.243 'md5sum /tmp/alpine-pg.qcow2'
# 两端 md5 必须一致
# 清理临时网络
HOST_IFACE=$(ip route | grep default | awk '{print $5}' | head -1)
iptables -t nat -D POSTROUTING -s 192.168.100.1/24 -o $HOST_IFACE -j MASQUERADE
ip link del tap-setup
ip link del br-setup
实测镜像大小:alpine-pg.qcow2 约 143MB(Alpine + PostgreSQL 16.11)
Phase B:迁移测试
B1:两台机器 --- 搭建 Overlay 网络
⚠️ Node A 和 Node B 都要执行 (修改 LOCAL_IP/REMOTE_IP)。
同
neon_mignet.mdStep 2,这里不再赘述原理。
bash
# === 两台机器都执行(修改 IP) ===
sudo -i
# Node A:
# LOCAL_IP="9.134.130.23"
# REMOTE_IP="21.6.160.243"
# Node B:
# LOCAL_IP="21.6.160.243"
# REMOTE_IP="9.134.130.23"
LOCAL_IP="9.134.130.23"
REMOTE_IP="21.6.160.243"
sysctl -w net.ipv4.ip_forward=1
# Bridge
ip link add name neon-br0 type bridge
ip link set neon-br0 up
sysctl -w net.bridge.bridge-nf-call-iptables=0 2>/dev/null || true
# VXLAN
ip link add neon-vxlan0 type vxlan id 100 local ${LOCAL_IP} dstport 4789 learning
ip link set neon-vxlan0 master neon-br0
ip link set neon-vxlan0 up
# FDB broadcast
bridge fdb append 00:00:00:00:00:00 dev neon-vxlan0 dst ${REMOTE_IP}
# TAP for QEMU
if [ ! -c /dev/net/tun ]; then
mkdir -p /dev/net; mknod /dev/net/tun c 10 200; chmod 666 /dev/net/tun
fi
ip tuntap add mode tap name tap-mig
ip link set tap-mig master neon-br0
ip link set tap-mig up
B2:Node A --- 创建 pgbench 客户端 Namespace
bash
# === 仅 Node A 执行 ===
# 安装 PostgreSQL 客户端(提供 psql + pgbench)
yum install -y postgresql postgresql-contrib
# 创建 namespace(模拟客户端,通过 veth 接入 neon-br0)
ip netns add pgbench-ns
ip link add veth-pg type veth peer name veth-br-pg
ip link set veth-br-pg master neon-br0
ip link set veth-br-pg up
ip link set veth-pg netns pgbench-ns
# 分配 IP(必须与 VM 在同一 /24 子网)
ip netns exec pgbench-ns ip addr add 10.0.0.1/24 dev veth-pg
ip netns exec pgbench-ns ip link set veth-pg up
ip netns exec pgbench-ns ip link set lo up
注意 :pgbench namespace 的 IP(10.0.0.1)必须和 VM IP(10.0.0.7)
在同一个 /24 子网内,否则 L2 转发不生效,ARP 无法到达。
B3:Node A --- 启动源 VM
bash
# === 仅 Node A 执行 ===
VM_MAC="52:54:00:12:34:56"
docker run -d --privileged --net=host \
--name qemu-source \
-v /tmp:/tmp \
qemu-alpine \
qemu-system-x86_64 \
-machine q35 -accel tcg -display none -no-reboot -cpu max \
-m 1024M -smp cpus=1 \
-kernel /tmp/vmlinuz-virt \
-initrd /tmp/initramfs-virt \
-append "root=/dev/vda1 modules=ext4 console=ttyS0,115200" \
-drive file=/tmp/alpine-pg.qcow2,if=virtio,cache=none \
-netdev tap,id=mig,ifname=tap-mig,script=no,downscript=no \
-device virtio-net-pci,netdev=mig,mac=${VM_MAC} \
-chardev socket,id=char0,path=/tmp/qemu-serial.sock,server=on,wait=off \
-serial chardev:char0 \
-qmp unix:/tmp/qmp.sock,server,nowait
# 等待 ~90 秒(TCG 模式 + 磁盘启动)
sleep 90
关键参数差异(对比 neon_mignet.md):
-m 1024M:PG + pgbench 数据需要 1G 内存(原来 512M)-kernel+-initrd+-append:直接内核启动,不依赖 bootloader- 无
-cdrom:从 qcow2 磁盘启动完整 Alpine 系统
B4:VM 内 --- 配网 + 启动 PostgreSQL
连入串口:
bash
socat -,rawer UNIX-CONNECT:/tmp/qemu-serial.sock
# 输入 root(无密码)
VM 内执行:
bash
# 配置 overlay IP
ip link set eth0 up
ip addr add 10.0.0.7/24 dev eth0
# 启动 PostgreSQL(tmpfs 上)
/usr/local/bin/start-pg-tmpfs.sh
# 应看到: === PostgreSQL ready ===
# 验证
su postgres -c "pg_isready"
# 应看到: /run/postgresql:5432 - accepting connections
Ctrl+C 断开串口。或者pkill socat。
B5:初始化 pgbench 数据
bash
# === Node A 宿主上执行 ===
# 测试连接
ip netns exec pgbench-ns psql -h 10.0.0.7 -p 5432 -U postgres -c "SELECT version();" benchdb
# 应看到: PostgreSQL 16.11
# 初始化 pgbench(scale=5,50 万行)
ip netns exec pgbench-ns pgbench -h 10.0.0.7 -p 5432 -U postgres -i -s 5 benchdb
# 约 9 秒完成
# 验证
ip netns exec pgbench-ns psql -h 10.0.0.7 -p 5432 -U postgres -c "SELECT count(*) FROM pgbench_accounts;" benchdb
# count
# --------
# 500000
B6:启动持续 pgbench
bash
# === Node A 宿主上执行 ===
ip netns exec pgbench-ns bash -c \
'pgbench -h 10.0.0.7 -p 5432 -U postgres -c 2 -j 1 -T 600 -P 1 benchdb \
> /tmp/pgbench-result.txt 2>&1 &'
# 确认运行
sleep 3
tail -3 /tmp/pgbench-result.txt
# 应看到:
# progress: 1.0 s, 69.0 tps, lat 26.415 ms stddev 10.728, 0 failed
# progress: 2.0 s, 89.0 tps, lat 22.441 ms stddev 2.979, 0 failed
B7:Node B --- 启动目标 QEMU
bash
# === 仅 Node B 执行 ===
VM_MAC="52:54:00:12:34:56"
docker run -d --privileged --net=host \
--name qemu-target \
-v /tmp:/tmp \
qemu-alpine \
qemu-system-x86_64 \
-machine q35 -accel tcg -display none -no-reboot -cpu max \
-m 1024M -smp cpus=1 \
-kernel /tmp/vmlinuz-virt \
-initrd /tmp/initramfs-virt \
-append "root=/dev/vda1 modules=ext4 console=ttyS0,115200" \
-drive file=/tmp/alpine-pg.qcow2,if=virtio,cache=none \
-netdev tap,id=mig,ifname=tap-mig,script=no,downscript=no \
-device virtio-net-pci,netdev=mig,mac=${VM_MAC} \
-chardev socket,id=char0,path=/tmp/qemu-serial.sock,server=on,wait=off \
-serial chardev:char0 \
-qmp unix:/tmp/qmp.sock,server,nowait \
-incoming tcp:0:20187
# 验证
sleep 3
ss -tlnp | grep 20187 # 应看到 LISTEN
echo '{"execute":"qmp_capabilities"}{"execute":"query-status"}' | socat - UNIX-CONNECT:/tmp/qmp.sock
# 应看到 "status": "inmigrate"
B8:触发迁移 A → B
bash
# === Node A 上执行 ===
(
echo '{"execute":"qmp_capabilities"}'
sleep 0.5
echo '{"execute":"migrate","arguments":{"uri":"tcp:21.6.160.243:20187"}}'
) | socat - UNIX-CONNECT:/tmp/qmp.sock
B9:查询迁移状态
bash
echo '{"execute":"qmp_capabilities"}{"execute":"query-migrate"}' | \
socat - UNIX-CONNECT:/tmp/qmp.sock
# 等待 "status": "completed"
实测数据(A → B):
total-time: 3705 ms downtime: 118 ms transferred: 352.4 MB(1024MB 中有效数据约 350MB)
B10:验证迁移结果
bash
# pgbench 仍在运行,检查结果
tail -5 /tmp/pgbench-result.txt
# 应看到 TPS 从 ~90 降到 ~12(跨节点 VXLAN),但 0 failed
# 数据完整性
ip netns exec pgbench-ns psql -h 10.0.0.7 -p 5432 -U postgres \
-c "SELECT count(*) FROM pgbench_accounts;" benchdb
# count
# --------
# 500000
# Node B 上确认 VM 在运行
# ssh -p 36000 21.6.160.243
echo '{"execute":"qmp_capabilities"}{"execute":"query-status"}' | \
socat - UNIX-CONNECT:/tmp/qmp.sock
# 应看到 "status": "running"
B11:迁移回来 B → A
bash
# === Node A 上:准备 incoming QEMU ===
docker rm -f qemu-source
rm -f /tmp/qmp.sock /tmp/qemu-serial.sock
qemu-img create -f qcow2 /tmp/alpine-pg.qcow2 2G
VM_MAC="52:54:00:12:34:56"
docker run -d --privileged --net=host \
--name qemu-incoming \
-v /tmp:/tmp \
qemu-alpine \
qemu-system-x86_64 \
-machine q35 -accel tcg -display none -no-reboot -cpu max \
-m 1024M -smp cpus=1 \
-kernel /tmp/vmlinuz-virt \
-initrd /tmp/initramfs-virt \
-append "root=/dev/vda1 modules=ext4 console=ttyS0,115200" \
-drive file=/tmp/alpine-pg.qcow2,if=virtio,cache=none \
-netdev tap,id=mig,ifname=tap-mig,script=no,downscript=no \
-device virtio-net-pci,netdev=mig,mac=${VM_MAC} \
-chardev socket,id=char0,path=/tmp/qemu-serial.sock,server=on,wait=off \
-serial chardev:char0 \
-qmp unix:/tmp/qmp.sock,server,nowait \
-incoming tcp:0:20187
# 验证
sleep 2
ss -tlnp | grep 20187
bash
# === Node B 上:触发迁移 ===
(
echo '{"execute":"qmp_capabilities"}'
sleep 0.5
echo '{"execute":"migrate","arguments":{"uri":"tcp:9.134.130.23:20187"}}'
) | socat - UNIX-CONNECT:/tmp/qmp.sock
实测数据(B → A):
total-time: 4328 ms downtime: 133 ms transferred: 356.9 MB
B12:清理环境
bash
# === 两台机器都执行 ===
# 停止容器
docker rm -f qemu-source qemu-incoming qemu-target 2>/dev/null
# 清理网络
ip netns del pgbench-ns 2>/dev/null
ip link del veth-br-pg 2>/dev/null
ip link del tap-mig 2>/dev/null
ip link del neon-vxlan0 2>/dev/null
ip link del neon-br0 2>/dev/null
# 清理文件
rm -f /tmp/qmp.sock /tmp/qemu-serial.sock /tmp/pgbench-result.txt
实测验证结果
总体结果
| 指标 | A → B | B → A |
|---|---|---|
| 迁移总时间 | 3.7s | 4.3s |
| VM 停机时间 (downtime) | 118ms | 133ms |
| 传输数据量 | 352.4MB | 356.9MB |
| pgbench 失败事务 | 0 | 0 |
| 迁移前 TPS | ~90 | ~9 |
| 迁移后 TPS | ~12 | ~110 |
| 迁移前延迟 | ~22ms | ~235ms |
| 迁移后延迟 | ~163ms | ~18ms |
| 数据完整性 | 500,000 ✓ | 500,000 ✓ |
A → B 迁移详细 TPS 变化
progress: 37.0 s, 87.0 tps, lat 22.834 ms, 0 failed ← 迁移前(本机)
progress: 38.0 s, 88.0 tps, lat 22.993 ms, 0 failed
progress: 39.0 s, 84.0 tps, lat 23.536 ms, 0 failed
progress: 40.0 s, 77.0 tps, lat 25.993 ms, 0 failed ← 开始内存拷贝,带宽竞争
progress: 41.0 s, 74.0 tps, lat 27.101 ms, 0 failed
progress: 42.0 s, 77.0 tps, lat 25.980 ms, 0 failed
progress: 43.0 s, 88.0 tps, lat 22.664 ms, 0 failed
progress: 44.0 s, 94.0 tps, lat 21.243 ms, 0 failed
...
progress: 50.0 s, 72.0 tps, lat 27.640 ms, 0 failed ← 进入收敛阶段
progress: 51.0 s, 70.0 tps, lat 28.700 ms, 0 failed
progress: 52.0 s, 75.0 tps, lat 26.363 ms, 0 failed
progress: 53.0 s, 86.0 tps, lat 23.775 ms, 0 failed
progress: 54.0 s, 31.0 tps, lat 60.472 ms, 0 failed ← VM 暂停,最后一批 dirty pages
progress: 55.0 s, 12.0 tps, lat 165.337 ms, 0 failed ← 切换完成,走 VXLAN
progress: 56.0 s, 12.0 tps, lat 164.814 ms, 0 failed
关键观察:
- 迁移期间(pre-copy 阶段)TPS 只从 90 降到 70,仅下降 22%
- VM 暂停瞬间(s54)TPS 降到 31,但 零失败
- 118ms downtime 远小于 TCP RTO(200ms),连接不断
B → A 迁移详细 TPS 变化
progress: 23.0 s, 8.0 tps, lat 245.517 ms, 0 failed ← 迁移前(走 VXLAN)
progress: 24.0 s, 8.0 tps, lat 241.113 ms, 0 failed
progress: 25.0 s, 20.0 tps, lat 107.182 ms, 0 failed ← 切换中
progress: 26.0 s, 108.0 tps, lat 18.610 ms, 0 failed ← 回到本机!TPS 暴涨
progress: 27.0 s, 107.0 tps, lat 18.621 ms, 0 failed
progress: 28.0 s, 105.0 tps, lat 19.127 ms, 0 failed
关键观察:
- 迁移回来后 TPS 从 8 直接跳到 108(13 倍提升!),因为不再走 VXLAN
- 延迟从 245ms 降到 18ms
- 同样 零失败事务
为什么 pgbench 不断连?
这是本实验的核心问题:QEMU 暂停 VM 118ms,TCP 连接为什么不断?
为什么迁移前后 VM 的 IP 不变?
迁移前后 pgbench 始终连接 10.0.0.7:5432,IP 完全不变。这不是巧合,而是 三层设计共同保证 的:
1. VM 内部网络状态随内存一起迁移
VM 的 overlay IP(10.0.0.7/24)是在 VM 内部的 eth0 上配置的(ip addr add 10.0.0.7/24 dev eth0)。
QEMU Live Migration 传输的是 整个 VM 内存,包括:
- Linux 内核的网络栈状态(IP 地址、路由表、ARP 缓存)
- TCP 连接状态(socket、seq/ack 号、窗口大小)
- PostgreSQL 进程的所有内存
因此迁移后 VM "醒来"时,它的网络配置和迁移前完全一致,VM 自身甚至不知道自己被搬过了。
2. MAC 地址两端一致
源端和目标端 QEMU 都使用相同的 MAC 地址 52:54:00:12:34:56:
-device virtio-net-pci,netdev=mig,mac=52:54:00:12:34:56 # 两端完全一致
迁移后 VM 用同样的 MAC 发包,对 bridge 和 VXLAN 来说就像同一台设备换了位置。
3. VXLAN FDB learning 自动更新转发路径
迁移完成后,VM 在 Node B 上恢复运行,第一个包(通常是 ARP 或 TCP ACK)从 Node B 的 tap-mig 发出,
经过 neon-br0 → neon-vxlan0。Node A 的 VXLAN 收到后通过 FDB learning 自动学习到:
MAC 52:54:00:12:34:56 → 走 VXLAN 隧道 → Node B (21.6.160.243)
之后 pgbench 发往 10.0.0.7 的包就自动走 VXLAN 到达 Node B。整个过程不需要任何手动干预。
迁移前后客户端访问 PG 的完整路径
迁移前(VM 在 Node A)--- 本地路径,延迟 ~22ms:
pgbench (pgbench-ns, 10.0.0.1)
│
│ veth-pg ←──veth pair──→ veth-br-pg
│ │
│ neon-br0 (Node A) ← 本地 L2 bridge
│ │
│ tap-mig
│ │
│ QEMU virtio-net
│ │
│ VM eth0 (10.0.0.7)
│ │
▼ PostgreSQL :5432
所有流量在 Node A 本地 通过 bridge 转发,不经过物理网络,延迟极低。
迁移后(VM 在 Node B)--- VXLAN 跨节点路径,延迟 ~163ms:
pgbench (pgbench-ns, 10.0.0.1) [仍在 Node A]
│
│ veth-pg ←──veth pair──→ veth-br-pg
│ │
│ neon-br0 (Node A)
│ │
│ neon-vxlan0 ← FDB 已学习: MAC→Node B
│ │
│ ┌─── VXLAN 封装 ───┐
│ │ outer src: 9.134.130.23 │
│ │ outer dst: 21.6.160.243 │
│ │ UDP:4789, VNI:100 │
│ └──────────────────┘
│ │
│ ═══ 物理网络 ═══ ← 跨节点传输
│ │
│ ┌─── VXLAN 解封装 ──┐
│ └──────────────────┘
│ │
│ neon-vxlan0 (Node B)
│ │
│ neon-br0 (Node B)
│ │
│ tap-mig (Node B)
│ │
│ QEMU virtio-net
│ │
│ VM eth0 (10.0.0.7) ← 同一个 IP,同一个 VM
│ │
▼ PostgreSQL :5432
跨节点 VXLAN 封装增加了约 50 字节开销 + 物理网络延迟,TPS 从 ~90 降到 ~12,延迟从 ~22ms 升到 ~163ms。
迁移回来后(VM 回到 Node A)--- 恢复本地路径:
FDB 再次 learning 到 MAC 在本地,流量回到第一种路径。TPS 从 ~8 直接跳到 ~108,延迟从 ~245ms 降到 ~18ms。
核心要点 :pgbench 全程只知道
10.0.0.7:5432这一个地址。IP 不变 + MAC 不变 + VXLAN FDB 自动学习 = 客户端完全无感知的热迁移。
TCP 重传机制覆盖了暂停窗口
pgbench (Node A) PostgreSQL (VM)
│ │
│── SQL query ──────────► │
│ │ ← VM 被 QEMU 暂停(~118ms)
│ │ TCP 包在 virtio-net 队列中等待
│ │
│ (等待回复, 没有超时) │ ← VM 恢复运行
│ │── response ──────────►
│◄── response ───────── │
│ │
│ 对 pgbench 来说,这只是 │
│ 一次较慢的查询(~160ms) │
关键时间窗口:
- QEMU downtime: 118ms(VM 暂停时间)
- TCP RTO(初始重传超时): 200ms(Linux 默认最小 200ms)
- pgbench 超时: 无(默认无限等待)
118ms < 200ms → TCP 还没来得及判定丢包,VM 就已经恢复了。
tmpfs 的作用
如果 PG 数据在磁盘上(qcow2),迁移后数据可能不一致(两端磁盘不同步)。
把数据放 tmpfs 上,数据就存在于 VM 内存中,随迁移一起搬过去,
和 Neon 使用 Pageserver 共享存储的效果完全一致。
和 Neon 生产环境的对应
| 本文实验 | Neon 生产环境 |
|---|---|
| Alpine VM + PostgreSQL 16 on tmpfs | neonvm-runner 中的 Postgres VM |
| PG 数据在 tmpfs(随内存迁移) | PG 数据在 Pageserver(共享存储) |
| pgbench 通过 namespace + veth 连接 | 应用通过 overlay 网络连接 Postgres |
| 手动 QMP migrate | neonvm-controller 自动触发迁移 |
| 0 失败事务,118ms downtime | 目标:<200ms downtime,TCP 连接不断 |
| VXLAN overlay 自动 FDB 切换 | 同样的 VXLAN overlay + FDB learning |
Neon 代码对照
| 本文步骤 | Neon 源文件 | 函数 |
|---|---|---|
| 创建 neon-br0 | neonvm-vxlan-controller/cmd/main.go |
createBrigeInterface() |
| 创建 neon-vxlan0 | neonvm-vxlan-controller/cmd/main.go |
createVxlanInterface() |
| FDB 广播条目 | neonvm-vxlan-controller/cmd/main.go |
updateFDB() |
| 创建 tap-mig + 接入 bridge | neonvm-runner/cmd/net.go |
overlayNetwork() |
| QEMU 参数 | neonvm-runner/cmd/main.go |
buildQEMUCmd() |
| QMP migrate 命令 | neonvm-controller |
迁移调度逻辑 |
| VM 1G 内存 | neonvm-runner |
-m 参数动态生成 |
常见问题
Q: 为什么 VM 内存要 1G?原来 512M 不够吗?
512M 时 PG shared_buffers(128MB) + pgbench 数据(scale=5, ~80MB) + OS + initdb 临时空间
会导致 OOM。1G 留足余量。实测迁移只传输了 ~350MB(有效页),不是全部 1024MB。
Q: 为什么用 -kernel/-initrd 而不是从磁盘 bootloader 启动?
TCG 模式下 extlinux 启动不稳定,且启动时间更长。
直接内核启动 -kernel vmlinuz-virt -initrd initramfs-virt -append "root=/dev/vda1 ..."
绕过 bootloader,更快更可靠。
注意:两端必须使用相同的 kernel/initrd 文件。
Q: 迁移后 TPS 为什么从 90 降到 12?
因为 pgbench 的 TCP 连接现在要走 VXLAN 隧道跨节点(Node A → Node B),
每个 SQL 往返增加了 ~25ms 的网络延迟。这不是 PG 的问题,是纯网络延迟。
在 Neon 生产环境中,客户端通常在 overlay 网络内,迁移后延迟变化很小。
Q: pgbench -T 600 为什么给这么长时间?
TCG 模式下 1G 内存迁移需要 3-4 秒。600 秒留足够的时间:
- 等待 pgbench 稳定(10 秒)
- 执行迁移(4 秒)
- 迁移后验证(随意)
实际只运行了 ~140 秒就手动终止了。
Q: fsync=off + synchronous_commit=off 安全吗?
在 tmpfs 上这完全安全------数据本来就在内存中,fsync 到 tmpfs 只是 no-op。
关闭这些选项提高了 TPS,让测试结果更明显。
生产环境中 Neon 有自己的 WAL 持久化机制(Safekeeper)。
Q: 如果 downtime > 200ms 怎么办?
TCP 会触发重传,pgbench 可能出现失败事务。应对方案:
- pgbench 加
-C(每事务重连),但 TPS 会大幅下降 - 减小 VM 内存或 dirty rate,降低迁移时间
- 使用 KVM(downtime 通常 <50ms)
- QEMU 迁移参数调优(
downtime-limit、max-bandwidth)