🚞

石头盘VPS改造2: LVM + Bcache + ZSwap + Zram

大类
CloudResource
Env
技术标签
云服务-白嫖
环境增强
优先级
Medium
状态
Maintaining
开始日期
Jan 2, 2026
最后更新
Jan 3, 2026
Public
Public
💡
之前那篇lvmcache的改进版,换成bcache之后好配了非常多!

转为LVM + Bcache

要想转换,首先需要离线环境(也就是root没有mount的环境)
所以我们需要把initramfs单独放到一个boot分区:
  • 安装dracut
  • 配置好分区相关的工具,通过rd.break选项进入initramfs shell
 
进入之后,我们就可以resize2fs shrink我们的root ext4,并在末尾分出来一个boot分区了!
我这家VPS用的是GPT分区表+Legacy BIOS引导,比较坑,需要额外增加一个bios_grub分区作为跳板来MBR引导grub。
划分好boot分区后,先grub-install一下,然后进入原来的系统,mount /boot后把原来的boot内容复制进去,然后dracut -f并update-grub就可以了

转为LVM + Bcache

有了单独的boot分区就可以开始转换分区格式了。
首先是把原来的裸ext4转为LVM + Bcache,用这个工具就可以。
blocks
g2pUpdated Dec 29, 2025
但是这个工具年久失修,基于Python3.3,我们走pyright + AI让他升级到Python 3.9并修一下错误,顺便合并一下别人的PR,这样就可以用uv来管理了
blocks
NyaMistyUpdated Jan 2, 2026
 
由于这个工具用的是python,所以还不能简单直接弄到initramfs里面用,想了许久,在pyenv和uv中选择了uv来作为self-contained的python环境,直接打包进dracut。选uv是因为uv不需要编译,避免浪费很多时间。
同时,这个工具用了augeas的库,augeas除了so库以外还有相应的脚本,需要一并打包进去
 
  • /usr/lib/dracut/modules.d/99misty-utils/module-setup.sh
#!/bin/bash check() { return 0 } depends() { echo bash echo busybox_post return 0 } install() { for cmd in lsblk blkid fdisk sfdisk cfdisk parted wipefs \ mkfs mkfs.ext4 mkfs.xfs mkfs.vfat mkfs.btrfs \ mount umount losetup dd hexdump \ xfs_admin xfs_copy xfs_estimate xfs_fsr xfs_info xfs_logprint xfs_metadump xfs_ncheck xfs_repair xfs_scrub xfs_spaceman xfs_bmap xfs_db xfs_freeze xfs_growfs xfs_io xfs_mdrestore xfs_mkfile xfs_quota xfs_rtcp xfs_scrub_all \ nilfs-clean nilfs_cleanerd nilfs-resize nilfs-tune \ reiserfsck reiserfstune tune2fs tunefs.reiserfs \ resize2fs \ lvm pvcreate pvscan vgcreate vgscan vgchange lvcreate lvremove lvs vgs pvs \ grub-bios-setup grub-fstest grub-kbdcomp grub-mkconfig grub-mkimage grub-mkpasswd-pbkdf2 grub-mkstandalone grub-probe grub-script-check \ grub-editenv grub-glue-efi grub-macbless grub-mkdevicemap grub-mklayout grub-mkrelpath grub-mount grub-reboot grub-set-default \ grub-file grub-install grub-menulst2cfg grub-mkfont grub-mknetdir grub-mkrescue grub-ntldr-img grub-render-label grub-syslinux2cfg \ tee wc sort uniq grep find xargs \ vim nano awk \ pv \ ; do inst $(command -v $cmd) || true done }
 
  • /usr/lib/dracut/modules.d/90python3/module-setup.sh
#!/bin/bash check() { return 0; } depends() { echo bash; return 0; } install() { export UV=/usr/local/bin/uv export UV_DIR=/opt/uv export UV_TOOL_DIR=$UV_DIR/tools export UV_TOOL_BIN_DIR=$UV_DIR/tools export UV_PYTHON_INSTALL_DIR=$UV_DIR/python $UV python install 3.9 inst "$UV" #inst_dir "$UV_DIR" src=$UV_DIR # 先确保根目录存在 inst_dir "$src" # 递归遍历:目录 -> inst_dir;文件/链接 -> inst # -L 跟随软链接把目标也带进去(你想“全拷贝”一般需要这个) while IFS= read -r -d '' p; do # 跳过根本身(已经 inst_dir 过) [[ "$p" == "$src" ]] && continue if [[ -d "$p" ]]; then inst_dir "$p" else # inst 会把文件放到同路径;若是 ELF,dracut 会自动拉依赖 inst "$p" fi done < <(find -L "$src" -print0) }
 
配置好后就很简单了:
  • 先转为LVM
# 挂载boot分区,用来存日志 mkdir boot mount /dev/sda2 boot # 转换为LVM /opt/uv/tools/bin/blocks --debug to-lvm --vg-name vg0 /dev/sda1 2>&1 | tee -a boot/blocks.log
  • 然后是bcache
# 确保我们的vg里,只有目标LV是active的 lvchange -an lvchange -ay /dev/vg0/Root # 转换为Bcache(需要重启后再来一次) /opt/uv/tools/bin/blocks --debug to-bcache /dev/vg0/Root 2>&1 | tee -a boot/blocks2.log
 
这样就完成全部的转换了,bcache在没有cache dev的情况下也能直接挂载,并且自带的udev rule做的就非常好,很方便

用Zram作为bcache caching device

bcache的行为是这样的:
  • 如果backing device没挂到caching device上,那会自动出现/dev/bcacheX
    • 有两种情况,一种是你从来没挂过,这个时候state显示为no cache
    • 另一种是你非writeback模式时在cache device丢了之后手动echo 1 > /sys/block/bcacheX/bcache/running,这时候state为clean
  • 否则他会注册到/sys/block/XXX/bcache下面,需要你自己手动echo到running才会激活/dev节点
  • cache device 需要register才能生效,register之后,会自动attach到相同的cache set uuid的backing device上并生效
  • 如果是writeback模式
    • 如果cache device丢了,state会显示inconsistent,直接echo到running不会改变这个状态,只会让他无cache启动
    • 此时detach后state会回到no cache状态
    • 再attach后会进入dirty状态(dirty只有writeback模式的时候才会出现)
  • 如何判断是否启用了cache?
    • state其实并不能正确给出是否启用,只能给出一个状态
    • 可以通过看/sys/block/bcacheX/bcache下是否存在cache目录来判断是否启用
    • 同时cache也会指向/sys/fs/bcache/XXXX的cache set文件夹
 
这就比LVM cache智能多了,我们脚本也能简化不少
 
 
  • /usr/lib/dracut/modules.d/70zram/module-setup.sh
#!/bin/bash check() { return 0; } depends() { echo bcache; return 0; } installkernel() { # 需要 zram、loop,bcache相关的东西由bcache dracut模块提供 instmods zram loop } install() { # 工具:含 losetup、zramctl 和 LVM 全家桶 echo "PATH during install: $PATH" >>/tmp/dracut-debug.log ls -al /usr/sbin/losetup >>/tmp/dracut-debug.log 2>&1 command -v losetup >>/tmp/dracut-debug.log 2>&1 || echo "losetup not found" >>/tmp/dracut-debug.log inst_multiple zramctl losetup \ make-bcache bcache-super-show \ dmsetup wipefs blockdev awk sed tee grep udevadm # 挂载 root 之前执行 inst_hook initqueue/finished 99 "$moddir/zram-bcache.sh" }
  • /usr/lib/dracut/modules.d/70zram/zram-bcache.sh
#!/bin/sh # 阶段:initqueue/finished(00) # 策略:一次触发只检查一次;成功或放弃时写锁 set -eu # ===== 日志 ===== LOG_DIR=/run/initramfs LOG_FILE=$LOG_DIR/70zram.log LOCK_DONE=$LOG_DIR/70zram.done mkdir -p "$LOG_DIR" exec > >(tee -a "$LOG_FILE") 2>&1 set -x ts() { date +%H:%M:%S 2>/dev/null || echo time; } log() { echo "[70zram $(ts)] $*"; } # ===== 参数 ===== CSET_UUID="${CSET_UUID:-2ffef9c2-b4ff-493f-8f65-7e899231dbcd}" for tok in $(cat /proc/cmdline); do case "$tok" in rd.zram.csetuuid=*) CSET_UUID="${tok#rd.zram.csetuuid=}";; esac done CACHE_SZ="${CACHE_SZ:-2G}" log "Bcache cset-uuid: ${CSET_UUID}, zram cache size: ${CACHE_SZ}" activate_all_bcache() { : "${CSET_UUID:?CSET_UUID is not set}" for d in /sys/block/*; do [ -d "$d" ] || continue run="$d/bcache/running" [ -w "$run" ] || continue cur="$(cat "$run" 2>/dev/null | tr -d ' \t\r\n' || true)" if [ "$cur" = "0" ]; then # 仅在 running=0 时才置为 1 echo 1 > "$run" sleep 2 # wait for bcache to init fi detach="$d/bcache/detach" attach="$d/bcache/attach" state="$(cat "$d/bcache/state" || true)" if [ "$state" = "no cache" ]; then # 先 detach(不存在/不可写就跳过) if [ -w "$detach" ]; then echo 1 | tee "$detach" 2>/dev/null || true fi fi # 再 attach 到指定 CSET_UUID(不存在/不可写就跳过) if [ -w "$attach" ]; then echo "$CSET_UUID" | tee "$attach" || true fi done } # 已完成就直接返回 [ -f "$LOCK_DONE" ] && { activate_all_bcache log "already done; skip"; exit 0; } # “完成并激活”帮助函数 finish_and_activate() { touch "$LOCK_DONE" activate_all_bcache } # ===== B) zram -> loop(512B) ===== modprobe zram || true sleep 2 command -v zramctl 2>&1 || { log "zramctl missing; done & activate without cache."; finish_and_activate; exit 0; } ZDEV="$(zramctl -f -s "$CACHE_SZ" -a lz4)" || { log "zramctl failed; done & activate without cache."; finish_and_activate; exit 0; } log "zram: ${ZDEV}" modprobe loop || true command -v losetup >/dev/null 2>&1 || { log "losetup missing; done & activate without cache."; finish_and_activate; exit 0; } LOOPDEV="$(losetup --sector-size=512 -fP --show "$ZDEV" 2>/dev/null)" || { log "losetup failed; done & activate without cache."; finish_and_activate; exit 0; } log "loop on zram: ${LOOPDEV}" ln -sf "$(basename "$LOOPDEV")" /dev/cachebrd [ -b "$LOOPDEV" ] || { log "loop not block; done & activate without cache."; finish_and_activate; exit 0; } SZ="$(blockdev --getsize64 "$LOOPDEV" 2>/dev/null || echo 0)" [ "$SZ" -gt 0 ] || { log "loop size=0; done & activate without cache."; finish_and_activate; exit 0; } log "loop size: $SZ bytes" # ===== C) 创建bcache cache dev ===== modprobe bcache || true sleep 2 wipefs -a "$LOOPDEV" || true make-bcache -C "$LOOPDEV" --cset-uuid "$CSET_UUID" echo "$LOOPDEV" | tee /sys/fs/bcache/register 2>&1 || { log "failed to register bcache cache device"; finish_and_activate; exit 0; } finish_and_activate exit 0
 

在initramfs里面启用网络

其实直接用这个项目就可以了:
dracut-sshd
Github
dracut-sshd
Owner
gsauthof
Updated
Jan 2, 2026
  • 首先安装dracut-network包
  • 然后安装这个项目
GRUB_CMDLINE_LINUX_DEFAULT="rd.shell rd.debug rd.timeout=90 rd.neednet=1 ifname=bootnet:00:01:02:12:23:45 ip=192.168.1.112::192.168.1.1:255.255.255.0:box:bootnet:none nameserver=1.1.1.1"
  • 这样就可以在initramfs里面上网了!
    • 记得include一些curl之类的命令(
  • 不过这样打出来的镜像虽然可以上网,并且开了ssh,但是登陆不上去,因为root用户在/etc/shadow中没有密码,导致sshd认为root用户locked了
    • 可以自己手动在initramfs里面用vim来随便写一个密码,比如$1$salt$123456789012345678901/
    • 也可以改镜像
notion image
 

关机自动回写writeback buffer

可以通过journalctl -b -1查看上次启动到关机的完整日志
  • /etc/systemd/system/detach_writeback_bcache.service
    • 又被gpt坑了
[Unit] Description=bcache: detach (flush writeback) then attach back at shutdown After=local-fs.target boot.mount boot.automount Wants=local-fs.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=/bin/true ExecStop=/usr/local/bin/detach_writeback_bcache TimeoutStopSec=infinity [Install] WantedBy=sysinit.target
  • /usr/local/bin/detach_writeback_bcache
#!/usr/bin/env bash set -euo pipefail LOG_FILE=/boot/detach_writeback_bcache.log exec > >(tee -a "$LOG_FILE") 2>&1 log() { echo "[bcache-shutdown] $*"; } # 可调:轮询间隔/打印间隔(秒) POLL_INTERVAL="${BCACHE_POLL_INTERVAL:-1}" human_to_bytes() { # 输入如:0.0k / 12.3M / 4.0G / 1024 # 输出近似 bytes(用于判断是否为 0) awk ' function mul(u) { if (u=="k" || u=="K") return 1024 if (u=="M") return 1024*1024 if (u=="G") return 1024*1024*1024 if (u=="T") return 1024*1024*1024*1024 if (u=="P") return 1024*1024*1024*1024*1024 if (u=="E") return 1024*1024*1024*1024*1024*1024 return 1 } { v=$1 # 末尾单位 u=substr(v, length(v), 1) if (u ~ /[0-9]/) { printf "%.0f\n", v+0; exit } n=substr(v, 1, length(v)-1) + 0 printf "%.0f\n", n * mul(u) } ' } main() { shopt -s nullglob for bcdir in /sys/class/block/bcache*/bcache; do [[ -d "$bcdir" ]] || continue dev="$(basename "$(dirname "$bcdir")")" # 你要求:没有 cache => 实际上没 cache,无需处理 if [[ ! -e "$bcdir/cache" ]]; then log "$dev: no cache link; skip" continue fi state="$(cat "$bcdir/state" 2>/dev/null || echo "")" if [[ "$state" != "dirty" ]]; then log "$dev: state=$state (not dirty); skip" continue fi # 从 cache symlink 解析 cacheset uuid(detach 前先记下来) cache_path="$(readlink -f "$bcdir/cache" || true)" if [[ -z "$cache_path" || ! -d "$cache_path" ]]; then log "$dev: cache link resolved to '$cache_path' (missing); skip" continue fi uuid="$(basename "$cache_path")" if [[ ! -w "$bcdir/detach" || ! -w "$bcdir/attach" ]]; then log "$dev: detach/attach not writable; skip" continue fi mode="$(cat "$bcdir/cache_mode" 2>/dev/null || echo "?")" dirty_data="$(cat "$bcdir/dirty_data" 2>/dev/null || echo "?")" log "$dev: begin flush via detach; cache_mode=$mode dirty_data=$dirty_data cacheset=$uuid" sync || true # 触发 detach(有些实现不阻塞,所以后面要循环确认) if ! (echo 1 | tee "$bcdir/detach" >/dev/null); then log "$dev: detach with '1' failed; retry with uuid" echo "$uuid" | tee "$bcdir/detach" >/dev/null fi # 循环检查:直到 dirty_data 归零 且 已经 no cache(或 cache 链接消失) t=0 last_log=0 while :; do # cache 是否还在(detach 完成后通常会消失) has_cache=0 [[ -e "$bcdir/cache" ]] && has_cache=1 state_now="$(cat "$bcdir/state" 2>/dev/null || echo "")" dirty_now="$(cat "$bcdir/dirty_data" 2>/dev/null || echo "0")" dirty_bytes="$(printf "%s\n" "$dirty_now" | human_to_bytes)" # 完成条件:脏数据为 0,且 detach 完成(cache symlink 消失或 state=no cache) if [[ "$dirty_bytes" -le 0 ]] && { [[ "$has_cache" -eq 0 ]] && [[ "$state_now" == "no cache" ]]; }; then log "$dev: flush complete after detach (state=$state_now dirty_data=$dirty_now)" break fi # 周期性打印进度 log "$dev: waiting... state=$state_now dirty_data=$dirty_now has_cache=$has_cache" last_log=$t sleep "$POLL_INTERVAL" t=$((t + POLL_INTERVAL)) done if false; then # attach 回去(前提:cacheset 目录还在) if [[ -d "/sys/fs/bcache/$uuid" ]]; then log "$dev: attach back to $uuid" echo "$uuid" | tee "$bcdir/attach" >/dev/null log "$dev: attach done" else log "$dev: cacheset $uuid missing; leave detached" fi fi sync || true done } main "$@"

剩余的配置

剩下的Zswap配置参考前面那篇的 “内存配置调优” 部分 :)