之前一直使用docker来部署服务,但是docker的运作方式对于备份是不太方便的:如果不stop掉容器,那么备份的时候很可能会发生“对正在写入的数据库进行备份”这种灾难性的事情;其他次要原因包括大量的image占据的无效备份空间、坚持使用iptables导致不能用nftables等等。相对而言,linux container同样提供了application和host隔离的安全性,而且每个container都是一个独立的linux环境,配置起来更加直观;网络比docker更加灵活、host可以进行更深度的控制;最重要的是,可以用snapshot + zfs send的方式进行可靠备份。
安装
根据Archwiki的指导安装lxd包,然后system enable --now lxd
来开机启动lxd daemon,这样才能通过后面的配置让容器也开机启动。如果报错提示端口已占用,那么需要排查主机上的DNS和DHCP(server)是不是监听得太广泛了——lxd使用dnsmasq监听它创建的网络(默认是lxdbr0)上的67端口以及53端口来为容器们分配IP和提供DNS解析,所以主机上特别是DNS服务不要配置成监听全部interface;可以改成只监听127.0.0.1以及LAN的gateway(比如10.0.0.1),这样就不冲突了。
在成功启动lxd.service
之后,来做id mapping。为了安全性,通常会使用unprivileged containers,简单而言就是通过uid/gid mapping的方式让容器里的root到了主机上是一个没有权限的用户,从而保护主机不被篡改。参考archwiki用usermod -v 1000000-1000999999 -w 1000000-1000999999 root
来设置就好。
然后先做lxc init
,但是跳过储存池的那个问题,我们稍后搞定。
搞定储存池的问题。lxd需要设置储存池来给容器们用,虽然官方提供了很多storage driver,但是最适合我的是ZFS。这里我是把某个SSD整个提供给ZFS管理了。执行以下几步创建pool并添加到lxc里面:
1
2
3
4
# 用zpool创建一个名为lxd1的储存池
zpool create lxd1 /dev/sda
# 将lxd1储存池添加到lxd(注意用的是lxc命令)并命名为pool1
lxc storage create pool1 zfs source=lxd1
之后就可以创建容器了。创建新的容器的命令格式是:
1
lxc launch 源:系统/版本/架构 容器名字 -s 储存池名字
例如在刚才创建的储存池pool1上创建一个ubuntu 22.04 LTS(代号jammy)的amd64架构的容器,名字叫ubuntu: lxc launch images:ubuntu/jammy/amd64 ubuntu -s pool1
。说到源,众所周知在国内通常用清华的源,通过以下步骤添加并检查源、列出源中的镜像或特定镜像:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 添加清华的镜像源并命名为mirror-images
lxc remote add mirror-images https://mirrors.tuna.tsinghua.edu.cn/lxc-images/ --protocol=simplestreams --public
# 列出现有的源,应该能看到有一项URL是清华的名为mirror-images的源
lxc remote list
# 列出清华源里的镜像
lxc image list mirror-images:
# 列出特定系统、特定tag甚至特定架构的image
lxc image list mirror-images:
lxc image list mirror-images:ubuntu
lxc image list mirror-images:ubuntu/jammy
lxc image list mirror-images:ubuntu/jammy/amd64
# 使用镜像源
lxc launch mirror-images:ubuntu/jammy/amd64 ubuntu -s pool1
如果在开始的时候忘了搞定储存池或者launch的时候忘了指定,会提示没有储存设备而初始化失败;在弄好储存池之后,可以通过把命令中的launch
换成init
并在结尾补上-s 储存池名字
来完成容器初始化,并执行lxc start 容器名字
来启动容器——如果launch
时一切顺利成功初始化,就会自动start,无需手动start。
登录与网络
然后就面临了第一个困境,不知道root密码怎么login呢?此时使用连接到容器的第一种方法:lxc exec 容器名字 bash
1,就会获得一个root身份的容器内的shell了;执行passwd
就可以改root密码了。除此之外,还有另一种方法来连接容器(但是必须知道密码),用lxc console 容器名字
来获得如同实体机上的tty的界面,输入用户名和密码来登录;使用ctrl + a,(两个键都松开之后)q,来脱离。
登录之后可能会发现怎么没网啊?对于host已经有完整的防火墙规则的情况,由于nftables目前的状况——只要是相同hook,任意namespace的任何chain的reject/drop命令会造成全局效果;比如我们自己的规则里的forward只要设置了policy drop
或者最终规则是无限定的reject
,那么lxd创建的规则的forward的accept就会被无视并被我们的规则给拒绝掉,并且不受priority影响。解决办法就是参考lxd的规则,复制一份到自己的规则里。以默认的lxdbr0为例,需要放到我们的规则里的包括:
- input的53(DNS)、67(DHCP)
- forward的
- 从lxdbr0到WAN口的(容器访问Internet)
- 从WAN口到lxdbr0的established,related状态的
- 从lxdbr0到lxdbr0的(容器间通信)
至于NAT规则,我们交给lxd的规则去处理就行,不用放到我们的规则里。所以大概会呈现这么些规则:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
table inet myfilter {
chain input {
type filter hook input priority filter; policy drop;
# 其他规则
iifname "lxdbr0" tcp dport 53 accept
iifname "lxdbr0" udp dport 53 accept
iifname "lxdbr0" icmp type { destination-unreachable, time-exceeded, parameter-problem } accept
iifname "lxdbr0" udp dport 67 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
# 其他规则
iifname "wan" ct state established,related accept
iifname "lxdbr0" oifname "wan" accept
iifname "lxdbr0" oifname "lxdbr0" accept
}
}
有了这几条规则之后,容器就可以快乐上网了;聪明人也能够看出来,容器可以通过gateway来访问主机上的服务;主机可以通过容器的IP直接访问容器,那就有很多玩法了~
备份
因为我们这个配置下lxd是跟zfs联动的,所以备份只需要两步2:
1
2
3
4
5
6
# 创建备份:lxc snapshot 容器名字 snapshot名字
lxc snapshot ubuntu bak1
# 导出binary格式备份
zfs send lxd1/containers/ubuntu@snapshot-bak1 > /mnt/backupdisk/ubuntu.bin
# 如果有需要的话,删除snapshot节省空间
# lxc delete ubuntu/bak1
这是非常公式化的,甚至可以用bash脚本来全自动备份:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/bash
# 感谢ChatGPT写的脚本
# 这里指定哪些container要备份
containers=("ubuntu1" "ubuntu2")
# 这里设置备份目录
BAKDIR=/mnt/backupdisk
for container in "${containers[@]}"; do
# 关机来保证没有在读写数据库
lxc stop "$container"
# Take a snapshot of the container
lxc snapshot "$container" backup_snapshot
# 可以开机了
lxc start "$container"
# Export the snapshot
zfs send lxd1/containers/"$container"@snapshot-backup_snapshot > ${BAKDIR}/"$container".bin
# Delete the snapshot
lxc delete "$container"/backup_snapshot
done