参考:
https://wiki.archlinuxcn.org/wiki/Systemd-nspawn

为什么我不用 Docker?
因为我的需求是在我个人主机上跑一个完整的 Linux 小系统,里面运行几个服务,并不是作为生产环境部署微服务,所以 Docker 抽象出来的分层镜像对我的场景没啥太大的作用,我觉得反而还是一种负担 ();我也不愿意 Docker 修改我主机上的任何配置即使我能够修好 (启动Docker后会破坏KVM的桥接网络)

介绍

Systemd-nspawnchroot 命令类似,是个终极版的 chroot。

systemd-nspawn 可以在轻量的命名空间容器中运行命令或系统。它比 chroot 强大的地方是,它完全虚拟了文件系统层次结构、进程树、各种 IPC 子系统以及主机名。

systemd-nspawn 将容器中各种内核接口的访问限制为只读,像是 /sys, /proc/sys/sys/fs/selinux。网络接口和系统时钟不能从容器内更改,不能创建设备节点。不能从容器中重启宿主机,也不能加载内核模块。

相比 LXCLibvirt, systemd-nspawn 更容易配置。

创建并启动一个最小的 Arch Linux 容器

需要先下载 arch-install-scripts 用于下载一个基本的 Arch 系统。其中至少需要安装包组 base 包。后续需要啥包可以进入容器完成安装。(最好下个 nvim)

1
sudo pacstrap -K -c ~/MyContainer base [additional packages/groups]

一旦安装完成后,进入容器中并设置 root 密码:

1
2
3
sudo systemd-nspawn -D ~/MyContainer
passwd
logout

最后, 启动容器:

1
sudo systemd-nspawn -b -D ~/MyContainer

参数 -b 将会启动这个容器(比如:以 PID=1 运行 systemd), 而不是仅仅启动一个 shell, 而参数 -D 指定成为容器根目录的目录。

容器启动后,输入密码以 “root” 身份登录。

可以在容器内运行 poweroff 来关闭容器。在主机端,容器可以通过 machinectl 工具进行控制。

注意:要从容器内终止 session,请按住 Ctrl 并快速地按 ] 三下。

管理

默认 systemd-nspawn 选项

要明白非常重要的一点是通过 machinectlsystemd-nspawn@.service 启动的容器所使用的默认选项与通过 systemd-nspawn 命令手动启动的有所不同。 通过服务启动所使用的额外选项有:

  • -b/--boot – 管理的容器会自动搜索一个 init 程序,并以 PID 1 的形式调用它。
  • --network-veth 关联于 --private-network – 管理的容器获得一个虚拟网络接口,并与主机网络断开连接。 详情看 #网络
  • -U – 如果内核支持,管理的容器默认使用 user_namespaces(7) 特性。 解释请看 #无特权容器
  • --link-journal=try-guest

这些行为可以在每个容器配置文件中被覆盖, 详情看 #配置

配置

网络

systemd-nspawn 容器可以使用 主机网络 或者 私有网络 _:

  • 在主机网络模式下,容器可以完全访问主机网络。这意味着容器将能够访问主机上的所有网络服务,来自容器的数据包将在外部网络中显示为来自主机(即共享同一 IP 地址)。
  • 在私有网络模式下,容器与主机的网络断开连接,这使得容器无法使用所有网络接口,但环回设备和明确分配给容器的接口除外。为容器设置网络接口有多种不同的方法:
    • 可以将现有接口分配给容器(例如,如果您有多个以太网设备)。
    • 可以创建一个与现有接口(即 VLAN 接口)相关联的虚拟网络接口,并将其分配给容器。
    • 可以创建主机和容器之间的虚拟以太网链接。

在后一种情况下,容器的网络是完全隔离的(与外部网络以及其他容器),由管理员来配置主机和容器之间的网络。这通常涉及创建一个网桥 network bridge 来连接多个(物理或虚拟)接口,或者在多个接口之间设置一个 NATNetwork Address Translation

主机网络模式适用于应用程序容器 ,它不运行任何网络软件来配置分配给容器的接口。当你从 shell 运行 systemd-nspawn 时,主机联网是默认模式

另一方面,私有网络模式适用于应与主机系统隔离的 “系统容器”。创建虚拟以太网链路是一个非常灵活的工具,可以创建复杂的虚拟网络。这是由 machinectlsystemd-nspawn@.service 启动的容器的默认模式

不同的启动方式,会导致网络的模式不同

使用 “macvlan” 或者 “ipvlan” 接口

您可以在现有的物理接口(即 VLAN 接口)上创建一个虚拟接口,并将其添加到容器中,而不是创建一个虚拟的以太网链路(其主机端可能被添加到桥接中,也可能没有)。该虚拟接口将与底层主机接口进行桥接,从而使容器暴露在外部网络中,从而使其能够通过 DHCP 从与主机相连的同一局域网中获得一个独特的 IP 地址。

systemd-nspawn 提供两个选项:

  • --network-macvlan=<interface> – 虚拟接口的 MAC 地址将与底层物理 interface 不同,并被命名为 mv-interface
  • --network-ipvlan=<interface> – 虚拟接口的 MAC 地址将与底层物理 interface 相同,并命名为 iv-interface。​

所有选项都意味着 --private-network.

实践

我用的无线网卡,想通过 ipvlan 模式为容器配置独立网络。用的 shell 启动,所以需要显式设置参数。
先查看网卡名:

1
ip addr

假设无线网卡名为 wlp4s0,启动容器:

1
2
3
4
sudo systemd-nspawn \
-b \
-D ~/MyContainer \
--network-ipvlan=wlp4s0

进入容器后,查看网络接口。会发现一个名为 iv-wlp4s0(格式通常为 iv-<父接口名>)的接口已存在,但处于 unmanaged 状态,未被 systemd-networkd 管理:

1
2
3
4
5
6
ip link show

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: iv-wlp4s0@if3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether 14:13:33:1e:7e:83 brd ff:ff:ff:ff:ff:ff link-netnsid 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: iv-wlp4s0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
link/ether 14:13:33:1e:7e:83 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 2408:8459:3430:64ac:1613:33ff:fe1e:7e83/64 scope global dynamic mngtmpaddr noprefixroute
valid_lft 7191sec preferred_lft 7191sec
inet6 fe80::1413:3300:11e:7e83/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever
1
2
3
4
5
6
7
8
9
10
11
12
13
14
networkctl status iv-wlp4s0
● 2: iv-wlp4s0
Link File: n/a
Network File: n/a
State: off (unmanaged)
Online state: unknown
Type: ether
Kind: ipvlan
Hardware Address: 14:13:33:1e:7e:83 (AzureWave Technology Inc.)
MTU: 1500 (min: 68, max: 65535)
QDisc: noop
IPv6 Address Generation Mode: eui64
Mode: L2 (bridge)
Number of Queues (Tx/Rx): 1/1

开始配置网络:
先用宿主机查看这个局域网网关是哪个:(假设网关地址为 192.168.233.43

1
ip route

在容器内创建并编辑网络配置文件,例如 /etc/systemd/network/80-ipvlan.network

1
2
3
4
5
6
7
nvim /etc/systemd/network/80-ipvlan.network
# 编辑文件
[Match]
Name=iv-wlp4s0
[Network]
DHCP=yes
DNS=8.8.8.8

重启 systemd-networkd 让它接管接口,重载配置:

1
2
3
systemctl restart systemd-networkd

networkctl reload

有时候可能会需要一个静态的 ipv4/ipv6,依旧可以在网络配置文件配置:

1
2
3
4
5
6
7
8
9
[Match]
Name=iv-wlp4s0

[Network]
Address=192.168.233.105/24
# 必须上面宿主机看到的网关
Gateway=192.168.233.43
DNS=8.8.8.8
DHCP=no

重载配置后查看,已经分到静态 ip,可以尝试 ping 下网关与其他局域网设备,但要注意此时宿主机与容器是 ping 不通的(宿主机与 ipvlan 容器不通,首要原因是 ipvlan 的内核驱动设计,无线环境的特点加剧并固化了这一现象。详细原因与解决方案见文章末):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
networkctl status iv-wlp4s0
● 2: iv-wlp4s0
Link File: n/a
Network File: /etc/systemd/network/80-ipvlan.network
State: routable (configured)
Online state: online
Type: ether
Kind: ipvlan
Hardware Address: 14:13:33:1e:7e:83 (AzureWave Technology Inc.)
Permanent Hardware Address: ce:99:99:7b:7a:de
MTU: 1500 (min: 68, max: 65535)
QDisc: noqueue
IPv6 Address Generation Mode: eui64
Mode: L2 (bridge)
Number of Queues (Tx/Rx): 1/1
Address: 192.168.233.105
2408:8459:3421:2d43:1613:33ff:fe1e:7e83
fe80::1413:3300:11e:7e83
Gateway: 192.168.233.43
fe80::18ab:48ff:febc:26b1
DNS: 8.8.8.8
2408:8459:3421:2d43::18
Activation Policy: up
Required For Online: yes
DHCPv6 Client DUID: DUID-EN/Vendor:0000ab11cf850bdcc7458033
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: iv-wlp4s0@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 14:13:33:1e:7e:83 brd ff:ff:ff:ff:ff:ff permaddr ce:99:99:7b:7a:de link-netnsid 0
inet 192.168.233.105/24 brd 192.168.233.255 scope global iv-wlp4s0
valid_lft forever preferred_lft forever
inet6 2408:8459:3421:2d43:1613:33ff:fe1e:7e83/64 scope global dynamic mngtmpaddr noprefixroute
valid_lft 6890sec preferred_lft 6890sec
inet6 fe80::1413:3300:11e:7e83/64 scope link proto kernel_ll
valid_lft forever preferred_lft forever

以同样的方式开启第二个容器,检测容器之间是否能互通:

1
2
3
4
5
6
7
8
9
ping -c 3 192.168.233.105
PING 192.168.233.105 (192.168.233.105) 56(84) bytes of data.
64 bytes from 192.168.233.105: icmp_seq=1 ttl=64 time=0.052 ms
64 bytes from 192.168.233.105: icmp_seq=2 ttl=64 time=0.067 ms
64 bytes from 192.168.233.105: icmp_seq=3 ttl=64 time=0.072 ms

--- 192.168.233.105 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2014ms
rtt min/avg/max/mdev = 0.052/0.063/0.072/0.008 ms

答案是可以的。

总结

使用 ipvlan 的话,容器与宿主机之间是不通的,但是容器与容器之间可以相通。

原因:

  1. ipvlan 的设计限制:ipvlan 子接口与父接口共享同一个 MAC 地址但处于不同的网络命名空间。内核的 ipvlan 驱动明确禁止了从子接口发往父接口自身 IP 地址的流量,以防止网络栈中出现回环和状态混乱。此限制与物理介质(有线或无线)无关,在有线网络下同样存在。
  2. 无线环境的叠加限制:在 Wi-Fi 环境中,大多数无线网卡驱动和接入点(AP)会默认启用“客户端隔离”并禁用“Hairpin Mode”。这进一步阻止了同一无线客户端上不同 IP(宿主机 IP 和容器 IP)之间通过无线射频路径通信,使得连通即使通过底层驱动允许也变得困难。

为什么容器之间可以直接互通?
内核为每个容器创建一个关联到主接口ipvlan 子接口。多个容器(网络命名空间)直接共享主机的一个物理接口,无需创建额外的桥接设备,子接口间的流量转发由内核 ipvlan 驱动直接处理,完全绕过了物理网卡的外部收发队列,从而实现了极高的转发效率。具体工作模式有两种:

  1. L2 模式(默认):二层交换
    • 所有 ipvlan 子接口处于同一个二层网络(相同 IP 子网)。它们共享主接口的 MAC 地址,依靠不同的 IP 地址进行区分。
    • 同一主机上的容器通信:数据包在内核中直接从源容器的 ipvlan 子接口转发到目标容器的 ipvlan 子接口,不经过主接口的物理层,性能极高。
    • 与外部通信:出口流量由主接口直接发出,但源 MAC 地址会被特殊处理(使用主接口 MAC)。
  2. L3 模式三层路由
    • 每个 ipvlan 子接口配置在不同的三层子网。主机内核的 ipvlan 驱动自动充当这些子网间的路由器
    • 所有通信(包括同一主机上的容器间通信)都是三层路由。数据包在内核的 ipvlan 路由逻辑中完成转发,同样不经过物理网卡的对外发包环节。这提供了更好的网络隔离,并避免了二层模式下的某些 ARP 限制。

但是想要容器与宿主机连通的话也不难,加上 veth 就好了。

最后的话展示一下资源消耗:

1
2
3
4
5
6
7
8
9
10
11
systemd-cgtop machine.slice

CGroup Tasks %CPU Memory
machine.slice 13 - 24.8M
machine.slice/arch-container.scope 13 - 24.8M
machine.slice/arch-container.scope/payload 13 - 24.8M
machine.slice/…payload/dev-hugepages.mount - - 8K
machine.slice/…er.scope/payload/init.scope 1 - 1.1M
machine.slice/…s-fs-fuse-connections.mount - - 4K
machine.slice/….scope/payload/system.slice 8 - 15.2M
machine.slice/…er.scope/payload/user.slice 4 - 5.4M