前言:
说到 docker, 大家都懂. 但是 LXC 可能就比较陌生. Docker 的起源于 LXC.LXC 的英文全称是 Linux Container, 相比较其他虚拟机而言, 是一种轻量级虚拟化技术, 它介于 Chroot(Linux 的一个改变根目录挂载点的机制) 和完整开发的虚拟机之间. LXC 不使用单独的内核资源, 但是可以创建一个类似的 Linux 操作系统环境.
Linux Daemon(LXD) 是一个轻量级容器管理程序, 他是凌驾于 LXC 之上而衍生的一套外部管理工具. Docker 也使用类似技术. LXD 使用了 LXC API 来管理 LXC, 而且新增 RESTful API.
这边文章通过研究 LXC 的隔离特征来说明容器的一些原理.
一, 独立的命名空间
每个容器都有一套独立的 Linux 环境命名空间. 命名空间的作用是对每一个环境做隔离, 使用环境的用户来看, 好像是一个新的机器环境. 命名空间是 Linux 内核用来隔离内核资源的方式. 通过 namespace 可以让一些进程只能看到与自己相关的一部分资源, 而另外一些进程也只能看到与它们自己相关的资源, 这两拨进程根本就感觉不到对方的存在. 具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace 中.
一套独立的环境需要做到有用户 id 独立, 进程 id 独立, root 根目录独立, 网络独立, UTS(UNIX Time-sharing System 的缩写, 主机名和 NIS 域名) 独立.
图 1, 查看 lxc 隔离依赖的要点
二, 用户和用户组 ID 独立
2.1 uid 的映射
不同的 namespace 中用户可以有相同的 UID 和 GID, 它们之间互相不影响. 父子 namespace 之间可以进行用户映射, 如父 namespace (宿主机) 的普通用户映射到子 namespace (容器) 的 root 用户, 以减少子 namespace 的 root 用户操作父 namespace 的风险. user namespace 功能虽然在很早就出现了, 但是直到 Linux kernel 3.8 之后这个功能才趋于完善.
比如 / etc/subuid 文件描述了容器内的 id 和容器外 id 的对应关系. root:300000:65536 的意思
是: root 启动的容器, 那么容器外是从 300000 开始到 365536 的范围对应着容器内 1-65536 的范围
图 2,uid 的映射
2.2 uid 的取值范围
每个 Linux 系统要求的 uid 可选范围都不一样, 但是 uid 通常是由 32 位, 也就是最大值可以是 2^31-2(范围: 1~4294967295). 路径 / etc/login.defs 文件的 UID_MIN,UID_MAX 限定了用户 useradd 新用户自己设置 uid 的最小值和最大值, 2^31-1 是个无效 id, 实验测试以下命令不能成功.
useradd -u 4294967296 test
Uid 的取值区间范围作了划分, 不同发行商的 Linux 系统有不一样的划分, 但是一般是这么约定:
0-99 系统用户 uid:
100-500: 系统管理管理员程序或者安装脚本产生的用户
1000-x: 用户 uid, 系统登录 uid 限制于了登录 uid 的最小值和最大值
网络 uid: 更偏向于高值
6553x:nobody
三, 进程 ID 独立
3.1 进程 id 的映射
图 3, 容器内外进程的映射关系
3.2 进程 id 的取值范围
每个 Linux 系统要求的 pid 可选范围都不一样, 最大值可以设置到 4194304 (= 222) .
在 64 位可以设置到 222 差不多 400w 左右. Linux 代码设计者认为这个数已经足够了. 如果是 32 位, pid 最大值是 32768. 进程 id 一旦赋予就不会改变.
图 4, 进程 id 的最大值
每个系统的设置查看 cat /proc/sys/kernel/pid_max.
我在 Ubuntu 16 和 CentOS 7 分别得到了 131072(= 217) 和 32768(= 215).
3.1 那如果到达了最大值, 会怎么样?
Pid 是顺序产生的. 当 pid 到达最大值, 它会从 0 继续开始找最近可用的 pid, 如果都没有 pid, 会报错.
四, UTS(hostname 独立)
以下测试了 hostname 在每个命名空间是独立存在的.
- unshare --uts --fork bash
- hostname// 查看继承的原来 hostname
- hostname modified // 修改成新的 hostname
- hostname // 确认修改成功
- eixt // 退出
- hostname // 此时 hostname 还是原来的 hostname
图 5,hostname 独立
五, 网络隔离
在每个容器所需要的最少网卡接口包括默认的 lo 回环 link(这个有什么用):
除开 lo, 一般隔离的网络还有一对 veth-pair. 除了 veth-pair,Linux 网卡方式还有其他
这个可以使用 ip link help 查看到. 这里提一下, ip 是个很强大的命令, 包含但不止于 ifconfig 和 route 的功能, 是个很强大的工具, link 这里指的是链路层. 对应的 ip addr 是网络层.
图 6, 网络连接 type
5.1 veth 网卡
那么 veth 网卡是什么: veth: 一对网络设备, 包含两个设备, 一个设备通过 lxc.network.link 网桥连接另一个网卡设备.
容器通过 veth 网卡对, veth 总是成对出现, 两个 veth 网卡分别位于两个命名空间, 实现容器内外的通信. 在 veth 设备任意一端接收到的数据, 都会从另一端发送出去.
图 7,veth 网卡对
还比如下面这个宿主机有两个容器, veth0 和 veth1 是一对, veth2 和 veth3 是一对. 并且在宿主机的 veth0 和 veth2 设备通过 NAT 连接到外网. 这个 NAT 是通过在 iptables 追加一个 NAT 的 masquerade 规则实现的. 当然在发送数据前, 需要经过 iptables MASQUERADE 规则将源地址改成宿主机 ip, 这样才能接收到响应数据包. 而宿主机网卡接收到的数据会通过 iptables DNAT 根据端口号修改目的地址和端口为容器的 ip 和端口, 然后根据路由规则发送到网桥 docker0 中, 并最终由网桥 docker0 发送到对应的容器中.
图 8, 容器网卡全局图
5.2,lxc 网络观察
一个 veth 网卡对和 NAT 桥接例子, 左半部分是容器内, 右半部分是宿主机器. eth0 和 vethK57NDU 是一个 veth 对. 发送或接收给一个 veth 的数据包相当于丢给另一个 veth 网卡. 然后 vethK57NDU 是通过 NAT 桥接网络到 lxcbr0.
图 9, 宿主机和容器的网卡配置
进一步查看 vethK57NDU 和 lxcbr0 的桥接关系:
图 10, 宿主机的网桥设置
所以在容器内丢给 eth0 的数据包就是相当于丢给宿主机的 lxdbr0.
还有个问题是, 那么容器内他可能是访问 8.8.8.8 的因特网, 那么这个 lxdbr0 并没有连接外网, 那么数据包是怎么路由出去的. 这里其实还有个 NAT 的 MASQUERADE, 通过这个规则, 外网的数据包最终会通过有联网的 ens33 外网网卡发出去.
图 11,nat 的 ip 路由规则
容器内的网络配置可以通过查看到, 容器内执行 lxc profile edit default
图 12, 容器的网卡设置
5.3 做个试验验证下
- sudo ip netns add mynamespace // 创建一个名字叫 mynamespace 的网络命名空间
- sudo ip netns list mynamespace // 列出这个命名空间
- sudo ls -l /var/run/netns/ // 在 var 目录下生成一个文件夹
- total 0 -r-------- 1 root root 0 Nov 10 22:24 mynamespace
- sudo ip netns exec mynamespace bash // 进入这个命名空间
- ip link add vethMYTEST type veth peer name eth0 // 新增一个 veth 网卡对, 两个名字分别叫 vethMYTEST 和 eth0
- ip link list // 查看所有的网卡链路层, 可以看到刚添加的两个设备
- ip address add 10.0.3.78/24 dev eth0 // 为 eth0 添加 ip 地址
- ip link set eth0 up // 启动 eth0 网络层
- ip address list // 查看所有的网卡 ip 层
- ip link set vethMYTEST netns 1 // 将 vethMYTEST 设备移出去, 移到宿主机
- exit // 退出去到宿主机
- ping -c 2 10.0.3.78 // 宿主机 ping mynamespace 命名空间的 eth0 网卡, 发现还不可达
- sudo brctl addif lxcbr0 vethMYTEST // 做网桥设置, 把 vethMYTEST 连接到 lxcbr0, 此时 ping 到 lxcbr0 的数据包就可以到命名空间里的 eth0
- ping -c 2 10.0.3.78 // 这时已经可达
- sudo ip netns exec mynamespce bash// 再次进入这个命名空间
- ping 8.8.8.8 // 内部发送给外部的包不可达
- traceroute 8.8.8.8// 并没有找到 eth0 网卡
- ip route// 观察没有到网关的路由默认规则
- sudo route add default gw 10.0.3.1 eth0// 新增一条到网关的路由默认规则
- ping 8.8.8.8//ping 测试成功
来源: https://www.qcloud.com/developer/article/1582284