本篇文章为学习笔记,代码来源于:https://github.com/pibigstar/go-docker
1. 命令参数
cmdArray
中存放的是定义之外的参数(Flag
中的参数属于定义过的参数),在之后调用init
命令时使用。比如./go-docker run -d -v /root/test:/test --name test busybox sh
中sh
属于定义之外的参数,会被存放在cmdArray
// 创建namespace隔离的容器进程
// 启动容器
var runCommand = cli.Command{
Name: "run",
Usage: "Create a container with namespace and cgroups limit",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ti",
Usage: "enable tty",
},
cli.StringFlag{
Name: "m",
Usage: "memory limit",
},
cli.StringFlag{
Name: "cpushare",
Usage: "cpushare limit",
},
cli.StringFlag{
Name: "cpuset",
Usage: "cpuset limit",
},
cli.StringFlag{
Name: "v",
Usage: "docker volume",
},
cli.BoolFlag{
Name: "d",
Usage: "detach container",
},
cli.StringFlag{
Name: "name",
Usage: "container name",
},
cli.StringSliceFlag{
Name: "e",
Usage: "docker env",
},
cli.StringFlag{
Name: "net",
Usage: "container network",
},
cli.StringSliceFlag{
Name: "p",
Usage: "port mapping",
},
},
Action: func(ctx *cli.Context) error {
if len(ctx.Args()) < 1 {
return fmt.Errorf("missing container args")
}
res := &subsystem.ResourceConfig{
MemoryLimit: ctx.String("m"),
CpuSet: ctx.String("cpuset"),
CpuShare: ctx.String("cpushare"),
}
// cmdArray 为容器运行后,执行的第一个命令信息
// cmdArray[0] 为镜像名, .Tail() 是去掉第一个后的全部参数
var cmdArray []string
for _, arg := range ctx.Args().Tail() {
cmdArray = append(cmdArray, arg)
}
tty := ctx.Bool("ti")
detach := ctx.Bool("d")
if tty && detach {
return fmt.Errorf("ti and d paramter can not both provided")
}
containerName := ctx.String("name")
volume := ctx.String("v")
net := ctx.String("net")
// 要运行的镜像名
imageName := ctx.Args().Get(0)
envs := ctx.StringSlice("e")
ports := ctx.StringSlice("p")
Run(cmdArray, tty, res, containerName, imageName, volume, net, envs, ports)
return nil
},
}
2. 程序运行
func Run(cmdArray []string, tty bool, res *subsystem.ResourceConfig, containerName, imageName, volume, net string, envs, ports []string) {
containerID := container.GenContainerID(10)
if containerName == "" {
containerName = containerID
}
parent, writePipe := container.NewParentProcess(tty, volume, containerName, imageName, envs)
if parent == nil {
logrus.Errorf("failed to new parent process")
return
}
if err := parent.Start(); err != nil {
logrus.Errorf("parent start failed, err: %v", err)
return
}
// 记录容器信息
err := container.RecordContainerInfo(parent.Process.Pid, cmdArray, containerName, containerID)
if err != nil {
logrus.Errorf("record container info, err: %v", err)
}
// 添加资源限制
cgroupMananger := cgroups.NewCGroupManager("go-docker")
// 删除资源限制
defer cgroupMananger.Destroy()
// 设置资源限制
cgroupMananger.Set(res)
// 将容器进程,加入到各个subsystem挂载对应的cgroup中
cgroupMananger.Apply(parent.Process.Pid)
// 设置网络
if net != "" {
// 初始化容器网络
err = network.Init()
if err != nil {
logrus.Errorf("network init failed, err: %v", err)
return
}
containerInfo := &container.ContainerInfo{
Id: containerID,
Pid: strconv.Itoa(parent.Process.Pid),
Name: containerName,
PortMapping: ports,
}
if err := network.Connect(net, containerInfo); err != nil {
logrus.Errorf("connect network, err: %v", err)
return
}
}
// 设置初始化命令
sendInitCommand(cmdArray, writePipe)
if tty {
// 等待父进程结束
err := parent.Wait()
if err != nil {
logrus.Errorf("parent wait, err: %v", err)
}
// 删除容器工作空间
err = container.DeleteWorkSpace(containerName, volume)
if err != nil {
logrus.Errorf("delete work space, err: %v", err)
}
// 删除容器信息
container.DeleteContainerInfo(containerName)
}
}
func sendInitCommand(comArray []string, writePipe *os.File) {
command := strings.Join(comArray, " ")
logrus.Infof("command all is %s", command)
_, _ = writePipe.WriteString(command)
_ = writePipe.Close()
}
2.1 过程示意图
整个过程可以看作一个引导进程,由它去创建和启动init
进程,init
进程中运行着容器。
init
进程需要优先启动,获取它的PID,从而对该进程进行网络配置和信息记录WorkSpace
实际上是容器的工作目录,在init
进程中会将文件系统切换到该目录下- 资源限制作用于引导进程,而
init
进程是它的子进程,将继承父进程的资源限制,子进程也可以修改限制,但不能超过父进程的限制
实践中,程序无法实现后台运行,容器所在进程会在主进程结束后被1号进程接管,随后马上就会被结束掉。
TODO:我目前还不知道如何创建后台进程,之后有机会再来完善这个功能。
2.2 资源释放
初始化进程创建的资源:
-
子进程:
init
-
工作空间:读写层和挂载点
-
容器信息记录:
/var/run/docker/config.json
-
虚拟网卡
前台运行时,可以通过ctrl+c
来结束init
进程的执行,之后程序将删除工作空间和容器信息。因为虚拟网卡在容器的命名空间中,所以容器init
停止后,该资源将不复存在;资源限制也是创建在init
进程中的文件系统,也会随进程结束而无效。
3. 创建父进程
父进程实际上是运行docker init
命令:切换文件系统后执行/proc/self/exe init
。
通过创建管道,阻塞了进程的运行,但获取到了进程的PID以进行后续配置。启动但阻塞是因为资源还没有完全初始化,但后面的初始化需要用到进程的PID,在该进程的命名空间中进行配置,初始化结束后关闭writePipe
,进程将继续运行。(pipe写入端未关闭,读取端将陷入阻塞)
parent, writePipe := container.NewParentProcess(tty, volume, containerName, imageName, envs)
if parent == nil {
logrus.Errorf("failed to new parent process")
return
}
if err := parent.Start(); err != nil {
logrus.Errorf("parent start failed, err: %v", err)
return
}
// 创建一个会隔离namespace进程的Command
func NewParentProcess(tty bool, volume, containerName, imageName string, envs []string) (*exec.Cmd, *os.File) {
readPipe, writePipe, _ := os.Pipe()
// 调用自身,传入 init 参数,也就是执行 initCommand
cmd := exec.Command("/proc/self/exe", "init")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
}
if tty {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
} else {
// 把日志输出到文件里
logDir := path.Join(common.DefaultContainerInfoPath, containerName)
if _, err := os.Stat(logDir); err != nil && os.IsNotExist(err) {
err := os.MkdirAll(logDir, os.ModePerm)
if err != nil {
logrus.Errorf("mkdir container log, err: %v", err)
}
}
logFileName := path.Join(logDir, common.ContainerLogFileName)
file, err := os.Create(logFileName)
if err != nil {
logrus.Errorf("create log file, err: %v", err)
}
cmd.Stdout = file
}
// 设置额外文件句柄
cmd.ExtraFiles = []*os.File{
readPipe,
}
// 设置环境变量
cmd.Env = append(os.Environ(), envs...)
err := NewWorkSpace(volume, containerName, imageName)
if err != nil {
logrus.Errorf("new work space, err: %v", err)
}
// 指定容器初始化后的工作目录
cmd.Dir = common.MntPath
return cmd, writePipe
}
3.1 创建WorkSpace
- 只读层和读写层属于主机的命名空间下,挂载点位于容器内
- 只读层存放的是镜像文件
- 读写层是对只读层的增量修改,并不会修改只读层的数据
- 在挂载点中写入的数据实际上写入到了读写层
- 挂载关系:
imagePath
和writeLayPath
联合挂载到了mntPath
;mntPath
被挂载到了hostPath
// 创建容器运行时目录
func NewWorkSpace(volume, containerName, imageName string) error {
// 1. 创建只读层
err := createReadOnlyLayer(imageName)
if err != nil {
logrus.Errorf("create read only layer, err: %v", err)
return err
}
// 2. 创建读写层
err = createWriteLayer(containerName)
if err != nil {
logrus.Errorf("create write layer, err: %v", err)
return err
}
// 3. 创建挂载点,将只读层和读写层挂载到指定位置
err = CreateMountPoint(containerName, imageName)
if err != nil {
logrus.Errorf("create mount point, err: %v", err)
return err
}
// 4. 设置宿主机与容器文件映射
mountVolume(containerName, imageName, volume)
return nil
}
3.1.1 创建只读层
// 根据镜像创建只读层
func createReadOnlyLayer(imageName string) error {
imagePath := path.Join(common.RootPath, imageName)
_, err := os.Stat(imagePath)
if err != nil && os.IsNotExist(err) {
err := os.MkdirAll(imagePath, os.ModePerm)
if err != nil {
logrus.Errorf("mkdir image path, err: %v", err)
return err
}
}
// 解压 /root/imageName.tar
imageTarPath := path.Join(common.RootPath, fmt.Sprintf("%s.tar", imageName))
if _, err = exec.Command("tar", "-xvf", imageTarPath, "-C", imagePath).CombinedOutput(); err != nil {
logrus.Errorf("tar image tar,path: %s, err: %v", imageTarPath, err)
return err
}
return nil
}
3.1.2 创建读写层
// 创建读写层
func createWriteLayer(containerName string) error {
writeLayerPath := path.Join(common.RootPath, common.WriteLayer, containerName)
_, err := os.Stat(writeLayerPath)
if err != nil && os.IsNotExist(err) {
err = os.MkdirAll(writeLayerPath, os.ModePerm)
if err != nil {
logrus.Errorf("mkdir write layer, err: %v", err)
return err
}
}
return nil
}
3.1.3 创建挂载点
func CreateMountPoint(containerName, imageName string) error {
mntPath := path.Join(common.MntPath, containerName)
_, err := os.Stat(mntPath)
if err != nil && os.IsNotExist(err) {
err := os.MkdirAll(mntPath, os.ModePerm)
if err != nil {
logrus.Errorf("mkdir mnt path, err: %v", err)
return err
}
}
// 将宿主机上关于容器的读写层和只读层挂载到 /root/mnt/容器名 里
writeLayPath := path.Join(common.RootPath, common.WriteLayer, containerName)
imagePath := path.Join(common.RootPath, imageName)
dirs := fmt.Sprintf("dirs=%s:%s", writeLayPath, imagePath)
cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mntPath)
if err := cmd.Run(); err != nil {
logrus.Errorf("mnt cmd run, err: %v", err)
return err
}
return nil
}
3.1.4 挂载卷
宿主机和容器的文件映射
func mountVolume(containerName, imageName, volume string) {
if volume != "" {
volumes := strings.Split(volume, ":")
if len(volumes) > 1 {
// 创建宿主机中文件路径
parentPath := volumes[0]
if _, err := os.Stat(parentPath); err != nil && os.IsNotExist(err) {
if err := os.MkdirAll(parentPath, os.ModePerm); err != nil {
logrus.Errorf("mkdir parent path: %s, err: %v", parentPath, err)
}
}
// 创建容器内挂载点
containerPath := volumes[1]
containerVolumePath := path.Join(common.MntPath, containerName, containerPath)
if _, err := os.Stat(containerVolumePath); err != nil && os.IsNotExist(err) {
if err := os.MkdirAll(containerVolumePath, os.ModePerm); err != nil {
logrus.Errorf("mkdir volume path path: %s, err: %v", containerVolumePath, err)
}
}
// 把宿主机文件目录挂载到容器挂载点中
dirs := fmt.Sprintf("dirs=%s", parentPath)
cmd := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", containerVolumePath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
logrus.Errorf("mount cmd run, err: %v", err)
}
}
}
}
3.2 进程运行的时机
在刚刚启动init
进程时,由于pipe的写入端没有关闭,程序会阻塞在ioutil.ReadAll(pipe)
// 本容器执行的第一个进程
// 使用mount挂载proc文件系统
// 以便后面通过`ps`等系统命令查看当前进程资源的情况
func RunContainerInitProcess() error {
cmdArray := readUserCommand()
if cmdArray == nil || len(cmdArray) == 0 {
return fmt.Errorf("get user command in run container")
}
// 挂载
err := setUpMount()
if err != nil {
logrus.Errorf("set up mount, err: %v", err)
return err
}
// 在系统环境 PATH中寻找命令的绝对路径
path, err := exec.LookPath(cmdArray[0])
if err != nil {
path = cmdArray[0]
}
err = syscall.Exec(path, cmdArray[0:], os.Environ())
if err != nil {
return err
}
return nil
}
func readUserCommand() []string {
// 指 index 为 3的文件描述符,
// 也就是 cmd.ExtraFiles 中 我们传递过来的 readPipe
pipe := os.NewFile(uintptr(3), "pipe")
bs, err := ioutil.ReadAll(pipe)
if err != nil {
logrus.Errorf("read pipe, err: %v", err)
return nil
}
msg := string(bs)
return strings.Split(msg, " ")
}
当主进程(引导进程)执行到sendInitCommand(cmdArray, writePipe)
时,由于writePipe.Close()
,ioutil.ReadAll(pipe)
将返回数据,init
进程继续运行。
func Run(cmdArray []string, tty bool, res *subsystem.ResourceConfig, containerName, imageName, volume, net string, envs, ports []string) {
...
// 记录容器信息
// 添加资源限制
// 设置网络
// 设置初始化命令
sendInitCommand(cmdArray, writePipe)
if tty {
...
}
}
func sendInitCommand(comArray []string, writePipe *os.File) {
command := strings.Join(comArray, " ")
logrus.Infof("command all is %s", command)
_, _ = writePipe.WriteString(command)
_ = writePipe.Close()
}
4. 记录容器信息
便于之后通过docker
命令查看容器信息(运行情况)
// 记录容器信息
func RecordContainerInfo(containerPID int, cmdArray []string, containerName, containerID string) error {
info := &ContainerInfo{
Pid: strconv.Itoa(containerPID),
Id: containerID,
Command: strings.Join(cmdArray, ""),
Name: containerName,
CreateTime: time.Now().Format("2006-01-02 15:04:05"),
Status: common.Running,
}
dir := path.Join(common.DefaultContainerInfoPath, containerName)
_, err := os.Stat(dir)
if err != nil && os.IsNotExist(err) {
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
logrus.Errorf("mkdir container dir: %s, err: %v", dir, err)
return err
}
}
fileName := fmt.Sprintf("%s/%s", dir, common.ContainerInfoFileName)
file, err := os.Create(fileName)
if err != nil {
logrus.Errorf("create config.json, fileName: %s, err: %v", fileName, err)
return err
}
bs, _ := json.Marshal(info)
_, err = file.WriteString(string(bs))
if err != nil {
logrus.Errorf("write config.json, fileName: %s, err: %v", fileName, err)
return err
}
return nil
}
/var/run/docker/$name/config.json
中记录的容器信息如下:
5. 添加资源限制
5.1 Cgroup机制
借助Linux Cgroup来实现对一组进程及子进程的资源限制、控制和统计,资源包括CPU、内存、存储、网络等。
Cgroup 完成资源限制主要通过下面三个组件:
- cgroup: 是对进程分组管理的一种机制
- subsystem: 是一组资源控制的模块
- hierarchy: 把一组 cgroup 串成一个树状结构 (可让其实现继承)
可以将
/sys/fs/cgroup
中的模块看作subsystem,在subsystem中创建的目录看作cgroup,创建的目录继承父目录的属性,看作hierarchy
/sys/fs/cgroup
下的目录属于subsystem
(资源控制模块),比如memory
用来控制内存使用。
举个例子:/sys/fs/cgroup/memory
,在该路径下创建的目录,将会继承其属性,system.slice
和user.slice
即为创建的目录。将PID写入tasks文件中即可将对应进程添加到该cgroup
/sys/fs/cgroup/memory/user.slice
通过向memory.limit_in_bytes
写入数据来限制内存使用
5.2 数据结构
每个资源(memory、cpu、cpuset)都实现了Subsystem
接口
// 资源限制配置
type ResourceConfig struct {
// 内存限制
MemoryLimit string
// CPU时间片权重
CpuShare string
// CPU核数
CpuSet string
}
/**
将cgroup抽象成path, 因为在hierarchy中,cgroup便是虚拟的路径地址
*/
type Subystem interface {
// 返回subsystem名字,如 cpu,memory
Name() string
// 设置cgroup在这个subSystem中的资源限制
Set(cgroupPath string, res *ResourceConfig) error
// 移除这个cgroup资源限制
Remove(cgroupPath string) error
// 将某个进程添加到cgroup中
Apply(cgroupPath string, pid int) error
}
var (
Subsystems = []Subystem{
&MemorySubSystem{},
// 设置tasks时,这两个必须同时设置
&CpuSubSystem{},
&CpuSetSubSystem{},
}
)
5.3 设置资源限制
// 添加资源限制
cgroupMananger := cgroups.NewCGroupManager("go-docker")
// 删除资源限制
defer cgroupMananger.Destroy()
// 设置资源限制
cgroupMananger.Set(res)
// 将容器进程,加入到各个subsystem挂载对应的cgroup中
cgroupMananger.Apply(parent.Process.Pid)
通过CGroupManager
对多种资源进行统一配置
type CGroupManager struct {
Path string
}
func NewCGroupManager(path string) *CGroupManager {
return &CGroupManager{Path: path}
}
func (c *CGroupManager) Set(res *subsystem.ResourceConfig) {
for _, subsystem := range subsystem.Subsystems {
err := subsystem.Set(c.Path, res)
if err != nil {
logrus.Errorf("set %s err: %v", subsystem.Name(), err)
}
}
}
func (c *CGroupManager) Apply(pid int) {
for _, subsystem := range subsystem.Subsystems {
err := subsystem.Apply(c.Path, pid)
if err != nil {
logrus.Errorf("apply task, err: %v", err)
}
}
}
func (c *CGroupManager) Destroy() {
for _, subsystem := range subsystem.Subsystems {
err := subsystem.Remove(c.Path)
if err != nil {
logrus.Errorf("remove %s err: %v", subsystem.Name(), err)
}
}
}
只举一个例子:对memory
资源的限制
type MemorySubSystem struct {
apply bool
}
func (*MemorySubSystem) Name() string {
return "memory"
}
func (m *MemorySubSystem) Set(cgroupPath string, res *ResourceConfig) error {
subsystemCgroupPath, err := GetCgroupPath(m.Name(), cgroupPath, true)
if err != nil {
logrus.Errorf("get %s path, err: %v", cgroupPath, err)
return err
}
if res.MemoryLimit != "" {
m.apply = true
// 设置cgroup内存限制,
// 将这个限制写入到cgroup对应目录的 memory.limit_in_bytes文件中即可
err := ioutil.WriteFile(path.Join(subsystemCgroupPath, "memory.limit_in_bytes"), []byte(res.MemoryLimit), 0644)
if err != nil {
return err
}
}
return nil
}
func (m *MemorySubSystem) Remove(cgroupPath string) error {
subsystemCgroupPath, err := GetCgroupPath(m.Name(), cgroupPath, false)
if err != nil {
return err
}
return os.RemoveAll(subsystemCgroupPath)
}
func (m *MemorySubSystem) Apply(cgroupPath string, pid int) error {
if m.apply {
subsystemCgroupPath, err := GetCgroupPath(m.Name(), cgroupPath, false)
if err != nil {
return err
}
tasksPath := path.Join(subsystemCgroupPath, "tasks")
err = ioutil.WriteFile(tasksPath, []byte(strconv.Itoa(pid)), os.ModePerm)
if err != nil {
logrus.Errorf("write pid to tasks, path: %s, pid: %d, err: %v", tasksPath, pid, err)
return err
}
}
return nil
}
6. 网络配置
6.1 数据结构
区分网络、网络端点、网络驱动接口:网络设备由名称、IP段和驱动名称唯一确定,驱动用来连接不同的网络设备。网络端点可以理解为容器内部网络与主机网络的连接点,配置了端口映射。eth0
和docker0
都属于网络设备。
// 网络
type Network struct {
Name string
IpRange *net.IPNet
Driver string
}
// 网络端点
type Endpoint struct {
ID string `json:"id"`
Device netlink.Veth `json:"dev"`
IPAddress net.IP `json:"ip"`
MacAddress net.HardwareAddr `json:"mac"`
Network *Network
PortMapping []string
}
// 网络驱动接口
type NetworkDriver interface {
// 驱动名
Name() string
// 创建网络
Create(subnet string, name string) (*Network, error)
// 删除网络
Delete(network Network) error
// 连接容器网络端点到网络
Connect(network *Network, endpoint *Endpoint) error
// 从网络上移除容器网络端点
Disconnect(network Network, endpoint *Endpoint) error
}
网络设备可以通过ip link show
查看,如下图所示。
lo
和eth0
是默认存在的网络接口cni-podman0
是host命名空间下的网络接口,用于桥接host和container,属于上面的Network
veth45a9497e@if2
是container命名空间下的网络接口,也称虚拟网卡(不直接和物理网卡相绑定),会随着容器进程的创建而创建,作用在容器进程中的命名空间。通过设置master cni-podman0
关联到host命名空间。
cni-podman0
可用于多个容器,不会自动删除;veth
和容器命名空间绑定,容器一旦停止,将不复存在(一个进程对应一个命名空间)Driver
用于创建Veth
,建立容器和主机的联系- 之后通过
ip link set $link netns $ns
将这个Veth
移动到容器的命名空间下
6.2 运行流程
// 设置网络
if net != "" {
// 初始化容器网络
err = network.Init()
if err != nil {
logrus.Errorf("network init failed, err: %v", err)
return
}
containerInfo := &container.ContainerInfo{
Id: containerID,
Pid: strconv.Itoa(parent.Process.Pid),
Name: containerName,
PortMapping: ports,
}
if err := network.Connect(net, containerInfo); err != nil {
logrus.Errorf("connect network, err: %v", err)
return
}
}
6.2.1 初始化网络
-
假设host存在一个名为
bridge
的网络接口,存入bridgeDriver
-
遍历
/var/run/docker/network/network/
,载入其中的网络设备(创建的网络设备会存放在该目录下) -
梳理一下:此时拥有了
drivers
和networks
,即网络设备和其对应的驱动(网络设备在软件层面上对物理网卡的描述;驱动是将不同网络设备关联起来的一段程序)
// 初始化网络驱动
func Init() error {
var bridgeDriver = BridgeNetworkDriver{}
drivers[bridgeDriver.Name()] = &bridgeDriver
if _, err := os.Stat(common.DefaultNetworkPath); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(common.DefaultNetworkPath, os.ModePerm); err != nil {
return err
}
}
// 递归遍历目录
err := filepath.Walk(common.DefaultNetworkPath, func(nwPath string, info os.FileInfo, err error) error {
if strings.HasSuffix(nwPath, "/") {
return nil
}
_, nwName := path.Split(nwPath)
nw := &Network{
Name: nwName,
}
if err := nw.load(nwPath); err != nil {
logrus.Errorf("error load network: %s", err)
}
networks[nwName] = nw
return nil
})
if err != nil {
logrus.Errorf("file path walk, err: %v", err)
return err
}
logrus.Infof("networks: %v", networks)
return nil
}
6.2.2 连接网络
// 连接网络
func Connect(networkName string, containerInfo *container.ContainerInfo) error {
network, ok := networks[networkName]
if !ok {
return fmt.Errorf("no Such network: %s", networkName)
}
// 分配容器IP地址
ip, err := ipAllocator.Allocate(network.IpRange)
if err != nil {
return err
}
// 创建网络端点
ep := &Endpoint{
ID: fmt.Sprintf("%s-%s", containerInfo.Id, networkName),
IPAddress: ip,
Network: network,
PortMapping: containerInfo.PortMapping,
}
// 调用网络驱动挂载和配置网络端点
if err = drivers[network.Driver].Connect(network, ep); err != nil {
return err
}
// 给容器的namespace配置容器网络设备IP地址
if err = configEndpointIpAddressAndRoute(ep, containerInfo); err != nil {
return err
}
// 配置端口映射
err = configPortMapping(ep, containerInfo)
if err != nil {
logrus.Errorf("config port mapping, err: %v", err)
return err
}
return nil
}
1 配置网络端点
创建并启动一个虚拟网络接口veth
。veth
与主机中的bridge
接口建立了关联,发送到veth
的数据将被转发到bridge
接口,由该接口通过物理网卡发送到互联网。veth
之后会被移动到容器的命名空间下。
func (d *BridgeNetworkDriver) Connect(network *Network, endpoint *Endpoint) error {
bridgeName := network.Name
br, err := netlink.LinkByName(bridgeName)
if err != nil {
return err
}
la := netlink.NewLinkAttrs()
la.Name = endpoint.ID[:5]
la.MasterIndex = br.Attrs().Index
endpoint.Device = netlink.Veth{
LinkAttrs: la,
PeerName: "cif-" + endpoint.ID[:5],
}
if err = netlink.LinkAdd(&endpoint.Device); err != nil {
logrus.Errorf("add endpoint device, err: %v", err)
return err
}
if err = netlink.LinkSetUp(&endpoint.Device); err != nil {
logrus.Errorf("add endpoint device: %v", err)
return err
}
return nil
}
2 配置容器网络IP
进入到容器的网络namespace中,启动容器中的虚拟网卡veth
,并配置默认路由(目的地+网关),代码中表示:所有路径的请求都统一转发到网关ep.Network.IpRange.IP
// 给容器的namespace配置容器网络设备IP地址
func configEndpointIpAddressAndRoute(ep *Endpoint, cinfo *container.ContainerInfo) error {
peerLink, err := netlink.LinkByName(ep.Device.PeerName)
if err != nil {
logrus.Errorf("fail config endpoint: %v", err)
return err
}
defer enterContainerNetns(&peerLink, cinfo)()
interfaceIP := *ep.Network.IpRange
interfaceIP.IP = ep.IPAddress
if err = setInterfaceIP(ep.Device.PeerName, interfaceIP.String()); err != nil {
return fmt.Errorf("%v,%s", ep.Network, err)
}
if err = setInterfaceUP(ep.Device.PeerName); err != nil {
return err
}
if err = setInterfaceUP("lo"); err != nil {
return err
}
_, cidr, _ := net.ParseCIDR("0.0.0.0/0")
defaultRoute := &netlink.Route{
LinkIndex: peerLink.Attrs().Index,
Gw: ep.Network.IpRange.IP,
Dst: cidr,
}
if err = netlink.RouteAdd(defaultRoute); err != nil {
return err
}
return nil
}
3 进入容器NS
- 主要是通过系统调用实现,在网络配置结束后,将回到之前的namespace
- 进入的是容器的namespace,即
init
进程的namespace;之前的namespace是主进程
func enterContainerNetns(enLink *netlink.Link, cinfo *container.ContainerInfo) func() {
f, err := os.OpenFile(fmt.Sprintf("/proc/%s/ns/net", cinfo.Pid), os.O_RDONLY, 0)
if err != nil {
logrus.Errorf("error get container net namespace, %v", err)
}
nsFD := f.Fd()
runtime.LockOSThread()
// 修改veth peer 另外一端移到容器的namespace中
if err = netlink.LinkSetNsFd(*enLink, int(nsFD)); err != nil {
logrus.Errorf("set link netns, err: %v", err)
}
// 获取当前的网络namespace
origns, err := netns.Get()
if err != nil {
logrus.Errorf("get current netns, err: %v", err)
}
// 设置当前进程到新的网络namespace,并在函数执行完成之后再恢复到之前的namespace
if err = netns.Set(netns.NsHandle(nsFD)); err != nil {
logrus.Errorf("error set netns, %v", err)
}
return func() {
netns.Set(origns)
origns.Close()
runtime.UnlockOSThread()
f.Close()
}
}
4 配置端口映射
- 端口映射是为了从主机访问容器内部,以便通过主机暴露服务,由ip+port唯一确定,运行时只需输入ex_port:in_port是因为ip在程序运行时都是已知的
- 容器将请求发送到主机,使用主机的网络下载数据,是通过配置网卡实现的,由虚拟网卡将报文转发到主机的物理网卡上
// 配置端口映射关系
func configPortMapping(ep *Endpoint, cinfo *container.ContainerInfo) error {
for _, pm := range ep.PortMapping {
portMapping := strings.Split(pm, ":")
if len(portMapping) != 2 {
logrus.Errorf("port mapping format error, %v", pm)
continue
}
iptablesCmd := fmt.Sprintf("-t nat -A PREROUTING -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s",
portMapping[0], ep.IPAddress.String(), portMapping[1])
cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...)
//err := cmd.Run()
output, err := cmd.Output()
if err != nil {
logrus.Errorf("iptables Output, %v", output)
continue
}
}
return nil
}