简介

LXC 是 Linux Container 的简写。Linux Container 是一种内核虚拟化技术,可以提供轻量级的虚拟化以便隔离进程和资源。大名鼎鼎的 Docker 在早期版本使用的底层容器引擎便是 LXC,不过 Docker 的目标是创建应用级容器,而 LXC 的目标是创建系统级容器,所以使用 LXC 更容易获得接近虚拟机的体验。


环境

LXC 也是利用了 Linux 内核的 cgroup 和 namespace 特性来实现容器的资源隔离,因此也对 Linux 的内核版本有较高的要求。这里使用 Ubuntu 18.04 Server 作为容器的宿主机,内核版本为 4.15.0-58-generic

教程

安装

安装命令行工具

安装非常简单,在 Ubuntu 上使用 apt-get install lxc 即可。lxc 包里包含了所有的命令,之后就可以看到系统多了很多 lxc- 开头的命令。

root@localhost:~# lxc-
lxc-attach         lxc-checkpoint     lxc-create         lxc-freeze         lxc-snapshot       lxc-unfreeze       lxc-wait
lxc-autostart      lxc-config         lxc-destroy        lxc-info           lxc-start          lxc-unshare        
lxc-cgroup         lxc-console        lxc-device         lxc-ls             lxc-stop           lxc-update-config  
lxc-checkconfig    lxc-copy           lxc-execute        lxc-monitor        lxc-top            lxc-usernsexec

LXC 安装成功后会自动创建一个叫 lxcbr0 的网桥,如下:

lxcbr0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 10.0.2.1  netmask 255.255.255.0  broadcast 0.0.0.0
        ether 00:16:3e:00:00:00  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

之后运行的容器的虚拟网卡都会接入到这个网桥了。LXC 还为这个网桥配置了一个 DHCP 服务,配置文件在 /etc/dnsmasq.d/lxc。这样容器支持 DHCP 的话就可以直接获取到可用的 IP 地址了。

快速开始

创建容器

lxc 安装后 在 /usr/share/lxc/templates 为我们提供了几个容器安装的模板:

  • lxc-download:通过网络获取容器镜像来创建容器。
  • lxc-local:通过本地已有的镜像来创建容器。
  • lxc-busybox:使用 busybox 来创建最基本的容器。
  • lxc-oci:通过 oci 标准的镜像来创建容器。

创建容器通过使用 lxc-create 命令来完成,此命令会调用指定的模板来完成容器的创建,我们先通过 lxc-busybox 这个模板创建一个最简单的容器:

1

lxc-create -t busybox -n busybox

lxc-create 命令通过 -t 参数指定要使用的模板,-n 参数指定创建的容器的名称。之后使用 lxc-ls 可以看到所有已经创建的容器,所有的容器默认都保存在 /var/lib/lxc 中。

root@localhost:~# ls /var/lib/lxc/
busybox
root@localhost:~# ls /var/lib/lxc/busybox/
config  rootfs
root@localhost:~# ls /var/lib/lxc/busybox/rootfs/
bin      dev  home  lib64  null  ram0  sbin     sys  tty   tty1  urandom  var
console  etc  lib   mnt    proc  root  selinux  tmp  tty0  tty5  usr      zero
root@localhost:~#

每个容器对应 /var/lib/lxc 下的每个目录,目录内存放着这个容器的配置文件和根文件系统。lxc-create 还可以指定使用 lvmCeph RBDzfsloop 文件系统来作为容器的根目录,默认使用的是 dir 的方式与宿主机共享文件系统。目录的方式胜在简单,但是却无法支持其他文件系统带来的高级特性,比如磁盘配额、快照等。

启动容器

使用 lxc-info 命令可以查看指定容器的状态,使用 lxc-start 来启动容器:

root@localhost:~# lxc-info busybox
Name:           busybox
State:          STOPPED
root@localhost:~# lxc-start busybox
root@localhost:~# lxc-info busybox
Name:           busybox
State:          RUNNING
PID:            5288
CPU use:        0.01 seconds
BlkIO use:      0 bytes
Memory use:     1.77 MiB
KMem use:       1.40 MiB
Link:           veth7F3KFW
TX bytes:      1.51 KiB
RX bytes:      1.87 KiB
Total bytes:   3.37 KiB
root@localhost:~#

容器启动后的第一个进程的 ID 是 5288,查看此进程的父进程可以看到是一个内核进程:

root@localhost:~# cat /proc/5288/status |grep PPid
PPid:    5281
root@localhost:~# ps 5281
PID TTY      STAT   TIME COMMAND
5281 ?        Ss     0:00 [lxc monitor] /var/lib/lxc busybox
root@localhost:~#
连接到容器

容器启动后,我们可以通过 lxc-attach 命令来运行容器内的命令,默认会运行容器内的 shell 来供我们操作容器。

root@localhost:~# lxc-attach busybox 

BusyBox v1.27.2 (Ubuntu 1:1.27.2-2ubuntu3.2) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ # ifconfig
eth0      Link encap:Ethernet  HWaddr 00:16:3E:88:E3:4A  
        inet6 addr: fe80::216:3eff:fe88:e34a/64 Scope:Link
        UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
        RX packets:25 errors:0 dropped:0 overruns:0 frame:0
        TX packets:15 errors:0 dropped:0 overruns:0 carrier:0
        collisions:0 txqueuelen:1000 
        RX bytes:2526 (2.4 KiB)  TX bytes:1962 (1.9 KiB)

lo        Link encap:Local Loopback  
        inet addr:127.0.0.1  Mask:255.0.0.0
        inet6 addr: ::1/128 Scope:Host
        UP LOOPBACK RUNNING  MTU:65536  Metric:1
        RX packets:0 errors:0 dropped:0 overruns:0 frame:0
        TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
        collisions:0 txqueuelen:1000 
        RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

~ #

进入容器后就可以执行容器内的所有命令了,可以看到,容器默认分配了一个 eth0 的网卡,但是并没有获取到 IP 地址。LXC 容器会尽量让一个容器就像是一个虚拟机一样,因此在容器内可以自主的配置 IP,甚至使用 reboot 命令重启容器都可以。接下来给容器分配一个 IP 地址,然后看看是否可以与主机互访。

容器内操作:

~ # ifconfig eth0 10.0.2.100
~ # ip ro add default via 10.0.2.1
~ # ping 180.76.76.76
PING 180.76.76.76 (180.76.76.76): 56 data bytes
64 bytes from 180.76.76.76: seq=0 ttl=127 time=4.649 ms
64 bytes from 180.76.76.76: seq=1 ttl=127 time=7.460 ms
64 bytes from 180.76.76.76: seq=2 ttl=127 time=4.998 ms
64 bytes from 180.76.76.76: seq=3 ttl=127 time=4.852 ms

--- 180.76.76.76 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 4.649/5.489/7.460 ms
~ #

需要注意的是,给容器分配的 IP 地址要和宿主机的 lxcbr0 网桥的地址在同一网段,并且容器内将默认网关设置为宿主机网桥的 IP 即可实现容器内联网。接着执行 telnetd 命令允许在外部通过 telnet 程序远程连接到此容器。

宿主机已经可以 ping 通容器:

root@localhost:~# ping -c 4 10.0.2.100
PING 10.0.2.100 (10.0.2.100) 56(84) bytes of data.
64 bytes from 10.0.2.100: icmp_seq=1 ttl=64 time=0.046 ms
64 bytes from 10.0.2.100: icmp_seq=2 ttl=64 time=0.058 ms
64 bytes from 10.0.2.100: icmp_seq=3 ttl=64 time=0.047 ms
64 bytes from 10.0.2.100: icmp_seq=4 ttl=64 time=0.045 ms

--- 10.0.2.100 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3070ms
rtt min/avg/max/mdev = 0.045/0.049/0.058/0.005 ms
root@localhost:~#

除了使用 lxc-attach 还可以使用 lxc-console 来登陆容器,此命令会连接到容器内 Linux 的控制台,所以必须提供必要的用户名和密码才可以登陆进容器内部。

更改容器内密码:

~ # passwd
passwd: no record of root in /etc/shadow, using /etc/passwd
Changing password for root
New password: 
Bad password: too weak
Retype password: 
passwd: password for root changed by root
~ #

宿主机使用 lxc-console 来登陆容器:

root@localhost:~# lxc-console busybox

Connected to tty 1
Type <Ctrl+a q> to exit the console, <Ctrl+a Ctrl+a> to enter Ctrl+a itself

busybox login: root
Password: 

BusyBox v1.27.2 (Ubuntu 1:1.27.2-2ubuntu3.2) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ #

可以看到,成功输入用户名和密码后也进入了容器。

停止并删除容器

使用 lxc-stop 来停止容器,使用 lxc-destroy 来彻底删除容器。

root@localhost:~# lxc-stop busybox
root@localhost:~# lxc-destroy busybox
lxc-destroy: busybox: tools/lxc_destroy.c: main: 271 Destroyed container busybox
root@localhost:~#

需要注意的是,如果想直接删除一个正在运行的容器,可以使用 lxc-destroy -f 来强制删除。

容器管理

在快速开始一章中,已经体验了 LXC 的基本用法,下面的章节将会讲解如何对容器进行更高级的管理,让容器运行的更像是虚拟机一样。

创建容器

容器的创建分两种,一种是由 root 用户创建的特权容器,一种是由普通用户创建的非特权容器。非特权容器有一些限制,比如无法创建设备节点等。而在特权模式下,让 Docker 运行在 LXC 中,甚至 LXC 嵌套 LXC 运行都成了可能。下面所有的例子创建的都是特权容器。

获取镜像模板

busybox 只提供了一个极小的更适合用于嵌入式 Linux 的基本文件系统,下面就看看如何使用 LXC 运行 CentOS、Ubuntu 等完整的 Linux 发行版吧!

例如,想要运行一个基于 CentOS 发行版的容器,首先需要下载 CentOS 的容器镜像模板,之后需要运行容器时,会自动从下载好的模板解压出容器的根文件系统到指定的 lvmzfsloop 或者 dir 等存储设备,并启动容器。

LXC 提供了 lxc-download 模板来通过网络获取容器镜像:

root@localhost:~# lxc-create -t download -n ubuntu-1604
Setting up the GPG keyring
Downloading the image index
---
DIST    RELEASE    ARCH    VARIANT    BUILD
---
alpine    3.10    amd64    default    20190923_13:00
alpine    3.10    arm64    default    20190923_13:00
.
.
.
---

Distribution: 
ubuntu
Release: 
xenial
Architecture: 
amd64

Downloading the image index
Downloading the rootfs
Downloading the metadata
The image cache is now ready
Unpacking the rootfs

---
You just created an Ubuntu xenial amd64 (20190923_07:42) container.

To enable SSH, run: apt install openssh-server
No default root or user password are set by LXC.

命令执行后会列出所有镜像的索引,接着会让你输入要下载的发行版、版本号、架构信息。经过片刻等待,Ubuntu 16.04 的容器就创建成功了。所有下载的镜像都缓存在 /var/cache/lxc/download 中,下一次创建此镜像模板的容器时就不会再次通过网络下载了。

也可以使用 lxc-create -t download --help 来查看模板支持的一些命令,例如安装 CentOS 7 过程可以省略为 lxc-create -t download -n centos7 -- -d centos -r 7 -a amd64。需要注意的是命令行当中的 -- 参数,此参数之后的参数才会会当作模板参数传递给模板。

启动容器

接着启动此容器,并验证是否是 Ubuntu 16.04 的发行版:

root@localhost:~# lxc-start ubuntu-1604
root@localhost:~# lxc-attach ubuntu-1604
root@ubuntu-1604:~# python3 -m platform
Linux-4.15.0-58-generic-x86_64-with-Ubuntu-16.04-xenial
root@ubuntu-1604:~#

lxc-start 默认在后台启动容器,如果想切换至前端启动,可以使用 -F 参数,同时容器启动过程中的一些控制台输出也会被打印出来了。

远程连接容器

LXC 没有提供远程连接容器的方式,如果打算为 LXC 写一套 Web 管理工具,并想实现在 Web 端操作容器,可以采用将 lxc-attach 或者 lxc-console 的输入输出通过 WebSocket 的方式与前端通信。

或者通过 SSH 的方式远程连接到容器,这样就需要在每个容器内配置 SSH 服务。例如在刚才创建好的容器中安装 SSH 服务:

root@ubuntu-1604:~# apt-get install openssh-server -y
root@ubuntu-1604:~# netstat -anpt
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      610/sshd        
tcp6       0      0 :::22                   :::*                    LISTEN      610/sshd        
root@ubuntu-1604:~#

由于新启动的容器还是空密码,所以还需要设置一个密码才可以登录:

root@ubuntu-1604:~# passwd
Enter new UNIX password: 
Retype new UNIX password: 
passwd: password updated successfully
root@ubuntu-1604:~#

允许使用 root 用户登录并重启服务:

sed -i 's/^PermitRootLogin prohibit-password/PermitRootLogin yes/g' /etc/ssh/sshd_config
service ssh restart

这样就可以在宿主机上通过 ssh 连接到容器了,如果想在局域网内也可以 ssh 连接到容器,可以将 lxcbr0 与物理网卡桥接起来,并配置局域网的 IP 地址。

容器资源限制

对于虚拟机来说,在创建虚拟机时为虚拟机分配的内存大小 就是对虚拟机能使用的内存的限制。对于容器而言,限制容器可使用的硬件资源 都是通过 cgroup 来实现的。

LXC 提供了 lxc-cgroup 命令来操作容器的控制组,默认容器是没有任何限制的。

限制内存使用

在容器内使用 free -m 命令查看内存使用情况:

root@ubuntu-1604:~# free -m
            total        used        free      shared  buff/cache   available
Mem:           3921          18        3802           8          99        3902
Swap:          3920           0        3920
root@ubuntu-1604:~#

可以看到总内存是和宿主机内存相同的,接下来使用 lxc-cgroup 来限制内存的使用:

宿主机上执行:

root@ubuntu-1604:~# lxc-cgroup ubuntu-1604 memory.limit_in_bytes 256M

容器内查看:

root@ubuntu-1604:~# free -m
            total        used        free      shared  buff/cache   available
Mem:            256          18         137           8          99         237
Swap:          3920           0        3920
root@ubuntu-1604:~#

可以看到允许使用的最大内存已经变成了 256M。但是这样只是临时更改了容器允许使用的内存,在容器重启后限制就会消失,如果想在重启重启后也保持这样的限制,可以更改每个容器的配置文件。

例如容器 ubuntu-1604 的配置文件就在 /var/lib/lxc/ubuntu-1604/config 在文件的最后添加如下内容:

lxc.cgroup.memory.limit_in_bytes = 512M

接着使用 lxc-stop ubuntu-1604 和 lxc-start ubuntu-1604 停止并启动容器,直接在容器内使用 reboot 命令是无效的。

root@localhost:~# lxc-attach ubuntu-1604
root@ubuntu-1604:~# free -m
            total        used        free      shared  buff/cache   available
Mem:            512          11         492           8           8         500
Swap:          3920           0        3920
root@ubuntu-1604:~#

容器重启后可用内存变为了 512M。对于交换分区,我们也可以通过 cgroup 来限制其使用,其限制字段名为 memory.memsw.limit_in_bytes,但是限制交换分区使用后通过 free 命令看到的依旧是宿主机的交换分区大小。

限制CPU使用

与内存限制的简单粗暴不同,对于 CPU 资源的限制复杂了一些,因为 CPU 有不同维度的限制条件,例如仅允许容器运行在 CPU 的某个核心上,或者仅允许容器在一段时间内获取多久的 CPU 执行时间片。这里只简单的限制容器可以在哪几个核心上使用。

宿主机上执行:

root@ubuntu-1604:~# lxc-cgroup ubuntu-1604 cpuset.cpus "0,1"

表示仅允许容器使用 CPU 的第 0 个和第 1 个核心。容器内查看:

root@ubuntu-1604:~# cat /proc/cpuinfo |grep processor
processor    : 0
processor    : 1
root@ubuntu-1604:~#

容器内只可以看到 CPU 的两个核心了。同样,如果想要在容器重启后限制依然生效,可以修改容器的配置文件 新增以下内容:

lxc.cgroup.cpuset.cpus = "0,1"
限制存储空间

如果容器的存储使用的是 dir 则与宿主机共享存储,如果使用了 lvmzfs 等文件系统则可以通过给容器分配独立的分区方式来限制容器可用的存储空间。这里使用 loop 设备模拟硬盘分区的方式来限制容器存储使用。

创建使用 loop 设备的容器并启动:

12

lxc-create -B loop -t download -n ubuntu-1604-loop --fssize 1G -- -d ubuntu -r xenial -a amd64lxc-start ubuntu-1604-loop

可以看到,dir 和 loop 的区别如下:

root@localhost:~# ls /var/lib/lxc/ubuntu-1604
config  rootfs
root@localhost:~# ls /var/lib/lxc/ubuntu-1604-loop/
config  rootdev  rootfs
root@localhost:~# cat /var/lib/lxc/ubuntu-1604/config |grep rootfs
lxc.rootfs.path = dir:/var/lib/lxc/ubuntu-1604/rootfs
root@localhost:~# cat /var/lib/lxc/ubuntu-1604-loop/config |grep rootfs
lxc.rootfs.path = loop:/var/lib/lxc/ubuntu-1604-loop/rootdev
root@localhost:~# ls /var/lib/lxc/ubuntu-1604-loop/rootfs
root@localhost:~# ls -lh /var/lib/lxc/ubuntu-1604-loop/rootdev 
-rw------- 1 root root 1.1G Sep 25 03:36 /var/lib/lxc/ubuntu-1604-loop/rootdev
root@localhost:~#

虽然 ubuntu-1604-loop 容器依然保留了 rootfs 目录,但内容确是空的。容器内也可以看到根文件系统的总空间只有 1G 了:

root@localhost:~# lxc-attach ubuntu-1604-loop 
root@ubuntu-1604-loop:~# df -hT
Filesystem     Type   Size  Used Avail Use% Mounted on
/dev/loop0     ext4   976M  372M  537M  41% /
none           tmpfs  492K     0  492K   0% /dev
tmpfs          tmpfs  2.0G     0  2.0G   0% /dev/shm
tmpfs          tmpfs  2.0G  8.1M  2.0G   1% /run
tmpfs          tmpfs  5.0M     0  5.0M   0% /run/lock
tmpfs          tmpfs  2.0G     0  2.0G   0% /sys/fs/cgroup
root@ubuntu-1604-loop:~#
网卡接口限速

对容器网卡流量进行限制使用了 Linux 内核的流量控制功能,通过 tc 命令进行管理。tc 命令的操作比较复杂,所幸有人封装好了对接口进行限速的脚本并开源到了 Github 上,我们只需要下载下来使用就可以了。

宿主机上安装 wondershaper 工具:

12

git clone https://github.com/magnific0/wondershaper.gitcp wondershaper/wondershaper /usr/local/sbin/

windershaper 工具使用非常简单,使用 -a 参数指定网卡接口名称,-c 清理所有规则,-d 限制下载速率,-u 限制上传速率,单位是 Kbps。需要注意的是 Linux 并不能很准确的控制下载速度,而且对于提供服务的容器或虚拟机而言,一般需要限制的是上传速率。

root@localhost:~# wondershaper
USAGE: /usr/local/sbin/wondershaper [-hcs] [-a <adapter>] [-d <rate>] [-u <rate>]

Limit the bandwidth of an adapter

OPTIONS:
-h           Show this message
-a <adapter> Set the adapter
-d <rate>    Set maximum download rate (in Kbps) and/or
-u <rate>    Set maximum upload rate (in Kbps)
-p           Use presets in /etc/conf.d/wondershaper.conf
-c           Clear the limits from adapter
-s           Show the current status of adapter
-v           Show the current version

MODES:
wondershaper -a <adapter> -d <rate> -u <rate>
wondershaper -c -a <adapter>
wondershaper -s -a <adapter>

EXAMPLES:
wondershaper -a eth0 -d 1024 -u 512
wondershaper -a eth0 -u 512
wondershaper -c -a eth0

root@localhost:~#

接着找到要限制流量的容器对应的网卡接口 并限制容器的上行速度为 1Mbps

root@localhost:~# lxc-info ubuntu-1604 | egrep "IP|Link"
IP:             10.0.2.238
Link:           vethQFAM44
root@localhost:~# wondershaper -a vethQFAM44 -c
root@localhost:~# wondershaper -a vethQFAM44 -d 1024
root@localhost:~#

我也不清楚为什么设置容器的上行速率使用的是 -d 参数,实际测试发现此参数能控制容器的上传速率。

在容器内运行一个 Web 服务进行测试:

root@localhost:~# lxc-attach ubuntu-1604
root@ubuntu-1604:~# dd if=/dev/zere of=img bs=1M count=128
dd: failed to open '/dev/zere': No such file or directory
root@ubuntu-1604:~# dd if=/dev/zero of=img bs=1M count=128
128+0 records in
128+0 records out
134217728 bytes (134 MB, 128 MiB) copied, 0.0789097 s, 1.7 GB/s
root@ubuntu-1604:~# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 ...

宿主机上使用 wget 命令下载测试:

root@localhost:~# wget http://10.0.2.238/img 
--2019-09-25 08:47:46--  http://10.0.2.238/img
Connecting to 10.0.2.238:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 134217728 (128M) [application/octet-stream]
Saving to: ‘img’

img        0%[                     ]   1.04M   120KB/s    eta 18m 4s

可以看到,速度保持在了 1Mbps 下。

挂起和恢复容器

正在运行中的容器可以随时暂停和恢复,使用 lxc-freeze 命令挂起一个容器,此时容器内所有正在运行的程序都将被强制挂起:

root@localhost:~# lxc-freeze ubuntu-1604
root@localhost:~# ps aux |grep python3
root       7921  0.0  0.4  55988 17520 pts/4    D+   07:33   0:01 python3 -m http.server 80
root       8222  0.0  0.0  13136  1104 pts/2    S+   09:07   0:00 grep --color=auto python3
root@localhost:~# cat /proc/7921/status |grep State
State:    D (disk sleep)
root@localhost:~# kill -9 7921
root@localhost:~# cat /proc/7921/status |grep State
State:    D (disk sleep)
root@localhost:~#

可以看到,刚才容器内运行的 python 进程已经是不可中断的休眠状态了,此时即使使用 kill -9 也依然无法杀掉此状态的进程。使用 lxc-unfreeze 解除容器的挂起状态:

root@localhost:~# lxc-unfreeze ubuntu-1604
root@localhost:~# cat /proc/7921/status |grep State
cat: /proc/7921/status: No such file or directory
root@localhost:~#

在容器内可以看到 python 进程已经退出,并显示 Killed,看来 kill -9 可能会迟到,但永远不会缺席。

容器状态监控

LXC 有三个命令工具提供了容器状态监控,分别是 lxc-monitorlxc-toplxc-wait

lxc-monitor

这个命令可以监控单个或多个容器的状态变化,例如监听所有名称以 ubuntu 开头的容器状态:

1

lxc-monitor ubuntu*

接着打开另外一个终端,重启一个容器:

root@localhost:~# lxc-stop ubuntu-1604-loop
root@localhost:~# lxc-start ubuntu-1604-loop
root@localhost:~#

可以看到,lxc-monitor 输出了被监听的容器的状态变化:

root@localhost:~# lxc-monitor ubuntu*
'ubuntu-1604-loop' exited with status [0]
'ubuntu-1604-loop' changed state to [STOPPING]
'ubuntu-1604-loop' changed state to [STOPPED]
'ubuntu-1604-loop' changed state to [STARTING]
'ubuntu-1604-loop' changed state to [RUNNING]
lxc-wait

此命令是等待容器到达某一状态后便退出。

root@localhost:~# lxc-wait ubuntu-1604-loop -s RUNNING
root@localhost:~# lxc-wait ubuntu-1604-loop -s STOPPED

ubuntu-1604-loop 容器已经是运行状态,所以命令直接就退出了,而等待容器状态变化为 STOPPED 是,命令发送了阻塞。接着停止此容器,可以看到 lxc-wait 发现被监控容器是 STOPPED 状态后就退出了。

如果想同时监听多个状态,可以使用 lxc-wait ubuntu-1604-loop -s "RUNNING|STOPPED" 这种方式。

lxc-top

此命令可以查看所有容器的运行状态

Container                   CPU          CPU          CPU                                BlkIO        Mem       KMem
Name                       Used          Sys         User                    Total(Read/Write)       Used       Used
ubuntu-1604                0.39         0.25         0.10         4.00 KiB(  0.00   /4.00 KiB)  17.83 MiB   4.53 MiB
ubuntu-1604-loop           2.24         1.48         0.28      54.32 MiB(54.14 MiB/180.00 KiB)  72.16 MiB   6.16 MiB
TOTAL 2 of 2               2.63         1.73         0.38      54.32 MiB(54.14 MiB/184.00 KiB)  89.99 MiB  10.68 MiB
root@localhost:~#
网卡管理
网卡配置项

容器启动后默认只有一个可用的 eth0 网卡接口,查看容器的配置文件可以找到网卡相关的配置项:

cat /var/lib/lxc/ubuntu-1604/config
...
# Network configuration
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:bc:27:d1
...

网卡相关配置如下:

  • lxc.net.0.type:第 0 块网卡的类型,如果是第二块网卡,则为 lxc.net.1.type 可选类型如下:
  • empty:不创建网卡,容器仅有 lo 网卡
  • veth:创建一个对等网卡,该网卡的一端分配给容器,另一端与 lxc.net.0.link 指定的网桥桥接
  • vlan:创建一个由 lxc.net.0.link 指定的虚拟局域网接口分配给容器,vlan的标识符可由 lxc.net.0.vlan.id 指定
  • phys:将 lxc.net.0.link 指定的网卡接口分配给容器。
  • macvlan:创建一个 macvlan 接口,该接口和由 lxc.net.0.link 指定的接口相连接
  • lxc.net.0.name:指定虚拟网卡接口的名称,默认是 eth 开头。
  • lxc.net.0.link:指定进行真实网络通信的网卡。
  • lxc.net.0.flags:指定网卡的状态,up 激活接口,down 关闭接口。
  • lxc.net.0.hwaddr:指定虚拟网卡的 MAC 地址,默认情况该值会自动分配。
分配物理网卡给容器

为容器新增一块虚拟网卡非常简单,只需要模范示例配置,将其中的 0 改为 1 就会新增一块 eth1 的网卡了,下面试试直接分配一块物理网卡给容器,需要先保证宿主机有一块额外的物理网卡,如果是虚拟机可以先为宿主机新增一块。

将以下内容追加到容器配置文件,将物理网卡分配给容器:

lxc.net.1.type = phys
lxc.net.1.link = ens35
lxc.net.1.flags = up

ens35 是宿主机上第二块物理网卡的名称,不同的 Linux 环境网卡接口名称也不尽相同。然后重启被修改的容器:

root@localhost:~# lxc-stop ubuntu-1604 && lxc-start ubuntu-1604
root@localhost:~# lxc-attach ubuntu-1604
root@ubuntu-1604:~# ifconfig
ens35     Link encap:Ethernet  HWaddr 00:0c:29:95:b4:7b  
        inet6 addr: fe80::20c:29ff:fe95:b47b/64 Scope:Link
        UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
        RX packets:0 errors:0 dropped:0 overruns:0 frame:0
        TX packets:9 errors:0 dropped:0 overruns:0 carrier:0
        collisions:0 txqueuelen:1000 
        RX bytes:0 (0.0 B)  TX bytes:726 (726.0 B)

eth0      Link encap:Ethernet  HWaddr 00:16:3e:bc:27:d1  
        inet addr:10.0.2.238  Bcast:10.0.2.255  Mask:255.255.255.0
        inet6 addr: fe80::216:3eff:febc:27d1/64 Scope:Link
        UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
        RX packets:13 errors:0 dropped:0 overruns:0 frame:0
        TX packets:12 errors:0 dropped:0 overruns:0 carrier:0
        collisions:0 txqueuelen:1000 
        RX bytes:1519 (1.5 KB)  TX bytes:1452 (1.4 KB)

lo        Link encap:Local Loopback  
        inet addr:127.0.0.1  Mask:255.0.0.0
        inet6 addr: ::1/128 Scope:Host
        UP LOOPBACK RUNNING  MTU:65536  Metric:1
        RX packets:0 errors:0 dropped:0 overruns:0 frame:0
        TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
        collisions:0 txqueuelen:1000 
        RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

root@ubuntu-1604:~#

可以看到容器内部也可以使用这块网卡了,接着为此网卡配置一个静态 IP,看看从外部是否可以直接访问:

root@ubuntu-1604:~# ifconfig ens35 10.0.1.11/24
root@ubuntu-1604:~#

需要注意的是,这个 IP 必须是此物理网卡所在网段的 IP 才可以。在 Windows 下直接访问容器:

Microsoft Windows [版本 10.0.18362.356]
(c) 2019 Microsoft Corporation。保留所有权利。

C:\Users\yunfwe\Desktop>ssh root@10.0.1.11
The authenticity of host '10.0.1.11 (10.0.1.11)' can't be established.
ECDSA key fingerprint is SHA256:WGzRElNn4jypkZLIwVzDcqbDDi7vqCb29UyAPbC8Oqs.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '10.0.1.11' (ECDSA) to the list of known hosts.
root@10.0.1.11's password:
Welcome to Ubuntu 16.04.6 LTS (GNU/Linux 4.15.0-58-generic x86_64)

* Documentation:  https://help.ubuntu.com
* Management:     https://landscape.canonical.com
* Support:        https://ubuntu.com/advantage
Last login: Wed Sep 25 01:58:44 2019
root@ubuntu-1604:~#
桥接物理网卡

直接分配物理网卡虽然实现了在外部直接访问宿主机内部的容器了,但是显然太浪费资源,每一个容器都要分配一个物理网卡。我们可以在宿主机创建一个网桥,然后将物理网卡接入到网桥内,之后再将容器的虚拟网卡也接入到网桥,这样就可以实现桥接模式了。

首先删除之前为容器分配物理网卡的配置项并重启容器,然后在宿主机上新增网桥,并将第二块物理网卡加入到网桥中。

宿主机执行:

root@localhost:~# brctl addbr netbr0 
root@localhost:~# brctl addif netbr0 ens35
root@localhost:~# ifconfig netbr0 up
root@localhost:~# ifconfig ens35 up
root@localhost:~# brctl show
bridge name    bridge id        STP enabled    interfaces
lxcbr0        8000.00163e000000    no        vethA75TNV
                                        vethWL2CSX
netbr0        8000.000c2995b47b    no        ens35
root@localhost:~#

lxcbr0 是 LXC 自动帮我们创建的网桥,网桥内已经有两个接口,对端便是两个容器的 eth0 网卡。接下来我们可以重新创建一个容器,然后将容器配置文件中的 lxc.net.0.link 改为我们新创建的网桥,或者在现有的容器中新增一块虚拟网卡,然后连接到网桥上。

修改容器配置文件,新增如下配置:

lxc.net.1.type = veth
lxc.net.1.link = netbr0
lxc.net.1.flags = up

重启此容器并查看 netbr0 网桥已经有多少个接口:

root@localhost:~# lxc-stop ubuntu-1604 && lxc-start ubuntu-1604
root@localhost:~# brctl show
bridge name    bridge id        STP enabled    interfaces
lxcbr0        8000.00163e000000    no        vethA75TNV
                                        vethGHBNA6
netbr0        8000.000c2995b47b    no        ens35
                                        veth479Q5D
root@localhost:~#

为容器内接口分配 IP 并远程连接:

root@localhost:~# lxc-attach ubuntu-1604
root@ubuntu-1604:~# ifconfig eth1 10.0.1.11/24
root@ubuntu-1604:~# ifconfig eth1
eth1      Link encap:Ethernet  HWaddr 66:15:70:46:7e:bf  
        inet addr:10.0.1.11  Bcast:10.0.1.255  Mask:255.255.255.0
        inet6 addr: fe80::6415:70ff:fe46:7ebf/64 Scope:Link
        UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
        RX packets:44 errors:0 dropped:0 overruns:0 frame:0
        TX packets:22 errors:0 dropped:0 overruns:0 carrier:0
        collisions:0 txqueuelen:1000 
        RX bytes:4625 (4.6 KB)  TX bytes:1588 (1.5 KB)

root@ubuntu-1604:~#

Windows 上远程连接成功:

C:\Users\yunfwe\Desktop>ssh root@10.0.1.11
root@10.0.1.11's password:
Welcome to Ubuntu 16.04.6 LTS (GNU/Linux 4.15.0-58-generic x86_64)

* Documentation:  https://help.ubuntu.com
* Management:     https://landscape.canonical.com
* Support:        https://ubuntu.com/advantage
Last login: Thu Sep 26 03:01:21 2019 from 10.0.1.1
root@ubuntu-1604:~#

当前网桥的配置是临时生效的,如果需要开机自动配置 还请查看你当前所使用的发行版的网络管理器如何配置网桥。

设备管理

LXC 提供了 lxc-device 命令将宿主机的硬件设备直接分配给容器。其实运用到的技术是 cgroup 的设备白名单和在容器内创建设备相应的设备文件。

lxc-device 命令的使用非常简单,使用 add 直接指定要将宿主机 /dev 目录下的哪个设备分配给容器就可以了,删除可以使用 del

分配硬盘分区到容器

宿主机执行:

1

lxc-device ubuntu-1604 add /dev/sda2

容器内查看:

root@localhost:~# lxc-attach ubuntu-1604
root@ubuntu-1604:~# ls /dev/sda2 
/dev/sda2
root@ubuntu-1604:~# mount /dev/sda2 /mnt/
root@ubuntu-1604:~# ls /mnt/
bin   cdrom  etc   initrd.img      lib    lost+found  mnt  proc  run   snap  swap.img  tmp  var      vmlinuz.old
boot  dev    home  initrd.img.old  lib64  media       opt  root  sbin  srv   sys       usr  vmlinuz
root@ubuntu-1604:~#
分配 tunnel 设备到容器

默认在容器内是没有 /dev/net/tun 设备文件的,因此一大部分的 VPN 应用都无法在容器内正常运行。利用 lxc-device 命令可以很方便的将 tunnel 设备分配给容器:lxc-device ubuntu-1604 add /dev/net/tun,但是用这种方法在容器重启后分配的设备文件将失效。

我们可以在配置文件中指定容器允许的设备文件白名单,容器在启动过程中会默认分配一些设备文件,在容器的配置文件中有一行是 lxc.include = /usr/share/lxc/config/common.conf 打开此文件,可以看到这些默认会创建的设备文件:

## Allow specific devices
### /dev/null
lxc.cgroup.devices.allow = c 1:3 rwm
### /dev/zero
lxc.cgroup.devices.allow = c 1:5 rwm
### /dev/full
lxc.cgroup.devices.allow = c 1:7 rwm
### /dev/tty
lxc.cgroup.devices.allow = c 5:0 rwm
### /dev/console
lxc.cgroup.devices.allow = c 5:1 rwm
### /dev/ptmx
lxc.cgroup.devices.allow = c 5:2 rwm
### /dev/random
lxc.cgroup.devices.allow = c 1:8 rwm
### /dev/urandom
lxc.cgroup.devices.allow = c 1:9 rwm
### /dev/pts/*
lxc.cgroup.devices.allow = c 136:* rwm
### fuse
lxc.cgroup.devices.allow = c 10:229 rwm

Linux tunnel 设备号是 c 10 200,如果我们需要在将来创建的容器都默认分配 tunnel 设备,可以直接修改 /usr/share/lxc/config/common.conf,否则只修改容器的配置文件即可:

容器配置文件最后添加以下内容:

### tunnel 
lxc.cgroup.devices.allow = c 10:200 rwm

现在只是允许了容器内使用 /dev/net/tun 设备,容器并不会自动创建此设备文件,我们可以在容器的 /etc/rc.local 中写入创建设备文件的命令来完成启动容器后自动创建。

编辑容器内的 /etc/rc.local 在最后的 exit 0 之前写入以下内容:

mkdir /dev/net -p
mknod /dev/net/tun c 10 200

然后重启容器:

root@localhost:~# lxc-stop ubuntu-1604 && lxc-start ubuntu-1604
root@localhost:~# lxc-attach ubuntu-1604
root@ubuntu-1604:~# ls /dev/net/tun
/dev/net/tun
root@ubuntu-1604:~#
目录挂载

如果想在多个容器内共享同一个目录,可以采用挂载宿主机同一目录的方式。

首先在宿主机创建一个目录用于容器见共享:

root@localhost:~# mkdir /data
root@localhost:~# echo hello > /data/world
root@localhost:~#

修改容器配置文件,新增如下内容:

lxc.mount.entry = /data data none rw,bind,create=dir 0 0

lxc.mount.entry 的写法跟 /etc/fstab 的写法一样,上面的内容就是将宿主机的 /data 目录挂载到容器内的 /data 目录,容器内的路径开头不可以写 / 否则会无法挂载。rw,bind,create=dir 是挂载选项,表示以读写方式挂载,如果容器内文件夹不存在则自动创建。

root@localhost:~# lxc-stop ubuntu-1604 && lxc-start ubuntu-1604
root@localhost:~# lxc-attach ubuntu-1604
root@ubuntu-1604:~# ls /data
world
root@ubuntu-1604:~# cat /data/world 
hello
root@ubuntu-1604:~#

我们还可以利用挂载的特性,将设备文件直接挂载到容器内,比如上面的 tunnel 设备文件还可以用以下方式自动挂载:

lxc.mount.entry = /dev/net/tun dev/net/tun none rw,bind,create=file 0 0
容器权限

在 Linux2.2 内核开始将超级用户的权限分为若干独立的子权限,每个子权限可独立的禁止或者打开,注意,一旦剥夺了根用户的某一权限,那么除非重启系统,否则无法恢复该权限。下面对这些权限进行简单的介绍:

  • CAP_CHOWN:允许改变文件的所有权
  • CAP_DAC_OVERRIDE:忽略对文件的所有 DAC 访问限制
  • CAP_DAC_READ_SEARCH:忽略所有对读、搜索操作的限制
  • CAP_FOWNER:如果文件属于进程的 UID,就取消对文件的限制
  • CAP_FSETID:允许设置 setuid 位
  • CAP_KILL:允许对不属于自己的进程发送信号
  • CAP_SETGID:允许改变组 ID
  • CAP_SETUID:允许改变用户 ID
  • CAP_SETPCAP:允许向其它进程转移能力以及删除其它进程的任意能力
  • CAP_LINUX_IMMUTABLE:允许修改文件的不可修改 (IMMUTABLE) 和只添加 (APPEND-ONLY) 属性
  • CAP_NET_BIND_SERVICE: 允许绑定到小于 1024 的端口
  • CAP_NET_BROADCAST:允许网络广播和多播访问
  • CAP_NET_ADMIN:允许执行网络管理任务:接口、防火墙和路由等。
  • CAP_NET_RAW:允许使用原始 (raw) 套接字
  • CAP_IPC_LOCK:允许锁定共享内存片段
  • CAP_IPC_OWNER: 忽略 IPC 所有权检查
  • CAP_SYS_MODULE: 插入和删除内核模块
  • CAP_SYS_RAWIO:允许对 ioperm/iopl 的访问
  • CAP_SYS_CHROOT:允许使用 chroot() 系统调用
  • CAP_SYS_PTRACE:允许跟踪任何进程
  • CAP_SYS_PACCT:允许配置进程记帐 (process accounting)
  • CAP_SYS_ADMIN:允许执行系统管理任务:加载/卸载文件系统、设置磁盘配额、开/关交换设备和文件等。
  • CAP_SYS_BOOT:允许重新启动系统
  • CAP_SYS_NICE:允许提升优先级,设置其它进程的优先级
  • CAP_SYS_RESOURCE:忽略资源限制
  • CAP_SYS_TIME:允许改变系统时钟
  • CAP_SYS_TTY_CONFIG:允许配置 TTY 设备
  • CAP_MKNOD:允许使用 mknod() 系统调用
  • CAP_LEASE:Allow taking of leases on files

容器的配置文件提供了 lxc.cap.drop 来允许我们运行的容器抛弃某些权限,例如我们要抛弃容器的创建设备文件和更改 IP 地址的权限,追加以下配置到容器的配置文件:

lxc.cap.drop =  mknod net_admin

权限的名称不需要写开头的 CAP_ 使用小写即可,接着重启容器:

root@localhost:~# lxc-stop ubuntu-1604 && lxc-start ubuntu-1604
root@localhost:~# lxc-attach ubuntu-1604
root@ubuntu-1604:~# ifconfig eth1 10.0.1.11/24
SIOCSIFADDR: Operation not permitted
SIOCSIFFLAGS: Operation not permitted
SIOCSIFNETMASK: Operation not permitted
root@ubuntu-1604:~# mknod test c 10 200
mknod: test: Operation not permitted
root@ubuntu-1604:~#
容器开机自启

容器默认在系统启动时不会自动启动,但是提供了 lxc-autostart 命令来帮我们启动所有设置了开机自启的容器。 我们可以在容器的配置文件中添加以下内容来让容器自启动:

lxc.start.auto = 1
lxc.start.delay = 10
lxc.group = onboot

lxc.start.auto 表示允许自启。lxc.start.delay 表示启动延迟,在处理互相依赖的容器中,启动延迟会比较有用。lxc.group 表示定义启动组,这样就可以通过 lxc-autostart -g onboot 来启动所有属于 onboot 组的容器了。

当设置好容器开机自启动后,将 lxc-autostart 命令添加到宿主机的 /etc/rc.local 中,这样就可以实现开机时自动启动容器了。

容器备份

可以在停掉或冻结要备份的容器后直接对 /var/lib/lxc/ 下的容器目录进行归档备份,如果想克隆容器,直接将容器目录复制一份,然后改下复制后的容器配置文件中的网卡配置相关冲突的地方即可。

附录

LXC 工具集的使用上还是比较原始简单,还有个更好用的 LXC 管理工具:LXD。该工具类似于 Docker,启动一个守护进程,然后通过 lxc 命令对容器和镜像进行管理,并提供了更灵活的配置和更完善的官方文档。还有容器快照、热迁移等高级功能。

  • LXD官方文档:Under Maintenance……. — lxd latest documentation