Postgresql热迁移pgbench持续读写零中断

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 网络

迁移流程

  1. Node A VM 运行 PostgreSQL(tmpfs 上),pgbench 持续 2 并发读写
  2. Node B QEMU 以 -incoming 模式等待
  3. QMP 触发 migrate,QEMU 搬内存(含 PG 全部数据)
  4. 迁移完成,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.md Step 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-br0neon-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 可能出现失败事务。应对方案:

  1. pgbench 加 -C(每事务重连),但 TPS 会大幅下降
  2. 减小 VM 内存或 dirty rate,降低迁移时间
  3. 使用 KVM(downtime 通常 <50ms)
  4. QEMU 迁移参数调优(downtime-limitmax-bandwidth
相关推荐
高铭杰5 小时前
neon源码分析(3)写入流程
postgresql·neon
王仲肖6 小时前
PostgreSQL 统计信息 — 完整总结与优化指南
数据库·postgresql
有想法的py工程师6 小时前
PostgreSQL vs PolarDB:Checkpoint 调优策略深度对比(高频 vs 低频)
大数据·数据库·postgresql
星星也在雾里7 小时前
MySQL 数据迁移到 PostgreSQL 实战教程
数据库·mysql·postgresql
运维 小白19 小时前
PostgreSQL高可用(Patroni + etcd + Keepalived)
数据库·postgresql·etcd
l1t1 天前
DeepSeek总结的Postgres 性能衰退
postgresql
青城山下————1 天前
CentOS 7 安装 PostgreSQL 13(国内镜像 + 远程访问)完整实践教程
linux·postgresql·centos
ycjunhua1 天前
windows 安装PostgreSQL 数据库
数据库·windows·postgresql
Mr.徐大人ゞ1 天前
2-6.pg特性功能之系列规则介绍和使用
postgresql