自己动手写docker3

时间:Jan. 17, 2021 分类:

目录:

容器网络

网络虚拟化技术介绍

namespace可以将网络空间进行独立,网络的配置依赖Linux的虚拟网络设备来实现功能和拓扑,有Veth、Bridge、802.1.q VLAN device、TAP等

Veth

Veth是成对出现的虚拟网络设备,在容器虚拟化场景中,Veth经常是连接不同的namespace

# 创建虚拟网络
ip netns add ns1
ip netns add ns2
# 创建veth
ip link add veth0 type veth peer name veth1 
# 将两个veth移动到两个namespace
ip link set veth0 netns ns1
ip link set veth1 netns ns2
# 在ns1的namespace查看网络设备
ip netns exec ns1 ip link
# 为每个veth设置网络地址和namespace的路由
ip netns exec ns1 ifconfig veth0 172.18.0.2/24 up
ip netns exec ns2 ifconfig veth1 172.18.0.3/24 up
ip netns exec ns1 route add default dev veth0
ip netns exec ns2 route add default dev veth1
# 测试ping
ip netns exec ns1 ping -c 1 172.18.0.3

Briage

Briage为网桥,可以理解为交换机(brctl可以yum install bridge-utils -y)

# 创建虚拟网络
ip netns add ns1
# 创建veth
ip link add veth0 type veth peer name veth1 
ip link veth1 setns ns1
# 创建网桥
brctl addbr br0
# 挂载网络设备
brctl addif br0 eth0
brctl addif br0 veth0

路由表

ip link set veth0 up
ip link set br0 up 
ip setns exec ns1 ifconfig veth1 172.18.0.2/24 up
# 设置ns1中流量都经过veth1流出
ip setns exec ns1 route add default dev veth1
# 宿主机上172.18.0.0/24网络的请求路由到br0网桥
route add -net 172.18.0.0/24 dev br0

iptables

iptables主要应用两种策略

  • MASQUERADE
  • DNAT

MASQUERADE可以将请求包的源地址转换为网络设备地址

例如veth1发出的数据包如果请求宿主机外部的IP,不知道如何路由回来

配置如下

sysctl -w net.ipv4.conf.all.forwarding=1
iptables -t nat -A POSTROUTING -s 172.18.0.0/24 -o eth0 -j MASQUERADE

这样到宿主机通过MASQUERADE转换为eth0的IP,在回到eth0网卡的时候会再将目的IP转为veth1的IP

DNAT常用于将内部端口映射到外部

例如ns中的服务需要对宿主机外提供服务

配置如下

iptables -t nat -A PREROUTING -p tcp -m tcp --dport 80 -j DNAT --to-destination

Go网络库介绍

net库,支持常见协议的IO,例如TCP,UDP,DNS,UnixSocket,主要用于处理网络地址和对应数据结构转换

  • net.IP,定义ip地址结构,可以通过ParseIP和String进行转换
  • net.IPNet,定义ip段结构,可以通过ParseCIDR和String进行转换

github.com/vishvananda/netlink库,用于操作网络接口,路由表等

  • netlink.LinkAdd()

github.com/vishvananda/netns库,用于进入到net namespace中进行网络配置

  • netns.Set(continer_netns)

数据结构

网络

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
}

IPAM用于从子网中分配IP地址

创建网络

创建网络对应docker的命令为

docker network create --subnet 192.168.0.0/24 --driver briage testbrigenet

流程为

  1. 获取网关IP
  2. 配置网络
  3. 保存网络配置

创建网络

func CreateNetwork(driver, subnet, name string) error {
    // 获取net.IPNet对象
    _, cidr, _ := net.ParseCIDR(subnet)
    // 通过IPAM获取网关IP
    ip, err := ipAllocator.Allocate(cidr)
    if err != nil {
        return err
    }
    cidr.IP = ip
    // 调用指定的网络驱动创建网络
    nw, err := drivers[driver].Create(cidr.String(), name)
    if err != nil {
        return err
    }
    // 将网络数据保存在文件系统
    return nw.dump(defaultNetworkPath)
}

将网络配置保存

func (nw *Network) dump(dumpPath string) error {
    // 判断网络配置目录是否存在,不存在就创建
    if _, err := os.Stat(dumpPath); err != nil {
        if os.IsNotExist(err) {
            os.MkdirAll(dumpPath, 0644)
        } else {
            return err
        }
    }

    nwPath := path.Join(dumpPath, nw.Name)
    nwFile, err := os.OpenFile(nwPath, os.O_TRUNC | os.O_WRONLY | os.O_CREATE, 0644)
    if err != nil {
        logrus.Errorf("error:", err)
        return err
    }
    defer nwFile.Close()

    nwJson, err := json.Marshal(nw)
    if err != nil {
        logrus.Errorf("error:", err)
        return err
    }

    _, err = nwFile.Write(nwJson)
    if err != nil {
        logrus.Errorf("error:", err)
        return err
    }
    return nil
}

创建容器并连接网络

对应docker命令

docker run -it -p 80:80 --net testbridgenet why

流程为

  1. 获取容器IP
  2. 创建网络端点
  3. 配置网络端点
  4. 创建端口映射
func Connect(networkName string, cinfo *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", cinfo.Id, networkName),
        IPAddress: ip,
        Network: network,
        PortMapping: cinfo.PortMapping,
    }
    // 调用网络驱动挂载和配置网络端点
    if err = drivers[network.Driver].Connect(network, ep); err != nil {
        return err
    }
    // 到容器的namespace配置容器网络设备IP地址
    if err = configEndpointIpAddressAndRoute(ep, cinfo); err != nil {
        return err
    }

    return configPortMapping(ep, cinfo)
}

获取网络列表

对应docker命令

docker network list

在启动的时候将网络配置加载到内存

func Init() error {
    var bridgeDriver = BridgeNetworkDriver{}
    drivers[bridgeDriver.Name()] = &bridgeDriver

    if _, err := os.Stat(defaultNetworkPath); err != nil {
        if os.IsNotExist(err) {
            os.MkdirAll(defaultNetworkPath, 0644)
        } else {
            return err
        }
    }

    filepath.Walk(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
    })

    //logrus.Infof("networks: %v", networks)

    return nil
}

进行查看的时候将network打印即可

func ListNetwork() {
    w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)
    fmt.Fprint(w, "NAME\tIpRange\tDriver\n")
    // 遍历
    for _, nw := range networks {
        fmt.Fprintf(w, "%s\t%s\t%s\n",
            nw.Name,
            nw.IpRange.String(),
            nw.Driver,
        )
    }
    // 输出到标准输出
    if err := w.Flush(); err != nil {
        logrus.Errorf("Flush error %v", err)
        return
    }
}

删除网络

对应docker命令

docker network remove testbridgenet

流程为

  1. 删除网络网关IP
  2. 删除网络设备
  3. 删除网络配置文件

方法对应

func DeleteNetwork(networkName string) error {
    nw, ok := networks[networkName]
    if !ok {
        return fmt.Errorf("No Such Network: %s", networkName)
    }

    if err := ipAllocator.Release(nw.IpRange, &nw.IpRange.IP); err != nil {
        return fmt.Errorf("Error Remove Network gateway ip: %s", err)
    }

    if err := drivers[nw.Driver].Delete(*nw); err != nil {
        return fmt.Errorf("Error Remove Network DriverError: %s", err)
    }

    return nw.remove(defaultNetworkPath)
}

容器地址分配

容器地址分配使用了bitmap算法,是一个通过数据位偏移实现的

直接初始化IPAM对象即可

const ipamDefaultAllocatorPath = "/var/run/mydocker/network/ipam/subnet.json"

type IPAM struct {
    SubnetAllocatorPath string
    Subnets *map[string]string
}

var ipAllocator = &IPAM{
    SubnetAllocatorPath: ipamDefaultAllocatorPath,
}

加载子网和子网分配的IP地址等信息

func (ipam *IPAM) load() error {
    if _, err := os.Stat(ipam.SubnetAllocatorPath); err != nil {
        if os.IsNotExist(err) {
            return nil
        } else {
            return err
        }
    }
    subnetConfigFile, err := os.Open(ipam.SubnetAllocatorPath)
    defer subnetConfigFile.Close()
    if err != nil {
        return err
    }
    subnetJson := make([]byte, 2000)
    n, err := subnetConfigFile.Read(subnetJson)
    if err != nil {
        return err
    }

    err = json.Unmarshal(subnetJson[:n], ipam.Subnets)
    if err != nil {
        log.Errorf("Error dump allocation info, %v", err)
        return err
    }
    return nil
}

也有对应写入的方法

func (ipam *IPAM) dump() error {
    ipamConfigFileDir, _ := path.Split(ipam.SubnetAllocatorPath)
    if _, err := os.Stat(ipamConfigFileDir); err != nil {
        if os.IsNotExist(err) {
            os.MkdirAll(ipamConfigFileDir, 0644)
        } else {
            return err
        }
    }
    subnetConfigFile, err := os.OpenFile(ipam.SubnetAllocatorPath, os.O_TRUNC | os.O_WRONLY | os.O_CREATE, 0644)
    defer subnetConfigFile.Close()
    if err != nil {
        return err
    }

    ipamConfigJson, err := json.Marshal(ipam.Subnets)
    if err != nil {
        return err
    }

    _, err = subnetConfigFile.Write(ipamConfigJson)
    if err != nil {
        return err
    }

    return nil
}

进行IP分配

func (ipam *IPAM) Allocate(subnet *net.IPNet) (ip net.IP, err error) {
    // 存放网段中地址分配信息的数组
    ipam.Subnets = &map[string]string{}

    // 从文件中加载已经分配的网段信息
    err = ipam.load()
    if err != nil {
        log.Errorf("Error dump allocation info, %v", err)
    }

    _, subnet, _ = net.ParseCIDR(subnet.String())

    // 获取网段子网对应的固定位长度和位数
    one, size := subnet.Mask.Size()

    // 如果没有进行过分配就直接初始化网段
    if _, exist := (*ipam.Subnets)[subnet.String()]; !exist {
        (*ipam.Subnets)[subnet.String()] = strings.Repeat("0", 1 << uint8(size - one))
    }

    // 进行分配
    for c := range((*ipam.Subnets)[subnet.String()]) {
        if (*ipam.Subnets)[subnet.String()][c] == '0' {
            ipalloc := []byte((*ipam.Subnets)[subnet.String()])
            ipalloc[c] = '1'
            (*ipam.Subnets)[subnet.String()] = string(ipalloc)
            ip = subnet.IP
            for t := uint(4); t > 0; t-=1 {
                []byte(ip)[4-t] += uint8(c >> ((t - 1) * 8))
            }
            ip[3]+=1
            break
        }
    }
    // 进行保存
    ipam.dump()
    return
}

对应的地址释放

func (ipam *IPAM) Release(subnet *net.IPNet, ipaddr *net.IP) error {
    ipam.Subnets = &map[string]string{}

    _, subnet, _ = net.ParseCIDR(subnet.String())

    err := ipam.load()
    if err != nil {
        log.Errorf("Error dump allocation info, %v", err)
    }

    c := 0
    releaseIP := ipaddr.To4()
    releaseIP[3]-=1
    for t := uint(4); t > 0; t-=1 {
        c += int(releaseIP[t-1] - subnet.IP[t-1]) << ((4-t) * 8)
    }

    ipalloc := []byte((*ipam.Subnets)[subnet.String()])
    ipalloc[c] = '0'
    (*ipam.Subnets)[subnet.String()] = string(ipalloc)

    ipam.dump()
    return nil
}

创建Bridge网络

创建Bridge网络的流程

  1. 创建虚拟设备bridge
  2. 设置bridge地址和路由
  3. 启动bridge
  4. 设置SNAT规则
func (d *BridgeNetworkDriver) initBridge(n *Network) error {
    // 1. 创建虚拟设备bridge
    // try to get bridge by name, if it already exists then just exit
    bridgeName := n.Name
    if err := createBridgeInterface(bridgeName); err != nil {
        return fmt.Errorf("Error add bridge: %s, Error: %v", bridgeName, err)
    }

    // 2. 设置bridge地址和路由
    // Set bridge IP
    gatewayIP := *n.IpRange
    gatewayIP.IP = n.IpRange.IP

    if err := setInterfaceIP(bridgeName, gatewayIP.String()); err != nil {
        return fmt.Errorf("Error assigning address: %s on bridge: %s with an error of: %v", gatewayIP, bridgeName, err)
    }

    // 3. 启动bridge
    if err := setInterfaceUP(bridgeName); err != nil {
        return fmt.Errorf("Error set bridge up: %s, Error: %v", bridgeName, err)
    }

    // 4. 设置SNAT规则
    // Setup iptables
    if err := setupIPTables(bridgeName, n.IpRange); err != nil {
        return fmt.Errorf("Error setting iptables for %s: %v", bridgeName, err)
    }

    return nil
}

创建虚拟设备bridge

func createBridgeInterface(bridgeName string) error {
    // 判断设备是否存在
    _, err := net.InterfaceByName(bridgeName)
    if err == nil || !strings.Contains(err.Error(), "no such network interface") {
        return err
    }

    // create *netlink.Bridge object
    la := netlink.NewLinkAttrs()
    la.Name = bridgeName

    br := &netlink.Bridge{la}
    // LinkADD创建设备的,等价于ip link add xxx
    if err := netlink.LinkAdd(br); err != nil {
        return fmt.Errorf("Bridge creation failed for bridge %s: %v", bridgeName, err)
    }
    return nil
}

设置bridge地址和路由

func setInterfaceIP(name string, rawIP string) error {
    retries := 2
    var iface netlink.Link
    var err error
    for i := 0; i < retries; i++ {
        iface, err = netlink.LinkByName(name)
        if err == nil {
            break
        }
        log.Debugf("error retrieving new bridge netlink link [ %s ]... retrying", name)
        time.Sleep(2 * time.Second)
    }
    if err != nil {
        return fmt.Errorf("Abandoning retrieving the new bridge link from netlink, Run [ ip link ] to troubleshoot the error: %v", err)
    }
    ipNet, err := netlink.ParseIPNet(rawIP)
    if err != nil {
        return err
    }
    // netlink.AddrAdd给网络接口配置地址,如果有网段信息,会自动添加路由
    addr := &netlink.Addr{ipNet, "", 0, 0, nil}
    return netlink.AddrAdd(iface, addr)
}

设置SNAT,Golang没有设置iptable规则的包,需要通过命令来设置了

func setupIPTables(bridgeName string, subnet *net.IPNet) error {
    iptablesCmd := fmt.Sprintf("-t nat -A POSTROUTING -s %s ! -o %s -j MASQUERADE", subnet.String(), bridgeName)
    cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...)
    //err := cmd.Run()
    output, err := cmd.Output()
    if err != nil {
        log.Errorf("iptables Output, %v", output)
    }
    return err
}

删除的时候将相关资源删除即可,这里貌似没有删路由规则

func (d *BridgeNetworkDriver) Delete(network Network) error {
    bridgeName := network.Name
    br, err := netlink.LinkByName(bridgeName)
    if err != nil {
        return err
    }
    return netlink.LinkDel(br)
}

基于Bridge网络创建容器

整体流程

容器添加veth对

func Connect(networkName string, cinfo *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", cinfo.Id, networkName),
        IPAddress: ip,
        Network: network,
        PortMapping: cinfo.PortMapping,
    }
    // 调用网络驱动挂载和配置网络端点
    if err = drivers[network.Driver].Connect(network, ep); err != nil {
        return err
    }
    // 到容器的namespace配置容器网络设备IP地址
    if err = configEndpointIpAddressAndRoute(ep, cinfo); err != nil {
        return err
    }

    return configPortMapping(ep, cinfo)
}

veth对一端挂载到网桥

func (d *BridgeNetworkDriver) Connect(network *Network, endpoint *Endpoint) error {
    // 获取网桥
    bridgeName := network.Name
    br, err := netlink.LinkByName(bridgeName)
    if err != nil {
        return err
    }

    // 创建veth配置对象
    la := netlink.NewLinkAttrs()
    la.Name = endpoint.ID[:5]
    // 将veth的一段指向到网桥
    la.MasterIndex = br.Attrs().Index

    endpoint.Device = netlink.Veth{
        LinkAttrs: la,
        PeerName:  "cif-" + endpoint.ID[:5],
    }
    // 
    if err = netlink.LinkAdd(&endpoint.Device); err != nil {
        return fmt.Errorf("Error Add Endpoint Device: %v", err)
    }
    # 启动veth
    if err = netlink.LinkSetUp(&endpoint.Device); err != nil {
        return fmt.Errorf("Error Add Endpoint Device: %v", err)
    }
    return nil
}

veth另一端在namespace配置

func configEndpointIpAddressAndRoute(ep *Endpoint, cinfo *container.ContainerInfo) error {
    // 获取veth的另一端
    peerLink, err := netlink.LinkByName(ep.Device.PeerName)
    if err != nil {
        return fmt.Errorf("fail config endpoint: %v", err)
    }
    // 将veth另一端放入容器,以下的操作都在容器内完成,执行完函数后恢复为默认网络空间
    defer enterContainerNetns(&peerLink, cinfo)()
    // 获取网络容器内IP和网段
    interfaceIP := *ep.Network.IpRange
    interfaceIP.IP = ep.IPAddress

    // 设置容器内网络IP
    if err = setInterfaceIP(ep.Device.PeerName, interfaceIP.String()); err != nil {
        return fmt.Errorf("%v,%s", ep.Network, err)
    }

    // 启动容器内的veth
    if err = setInterfaceUP(ep.Device.PeerName); err != nil {
        return err
    }

    // 默认情况下Net Namespace中的lo网卡是关闭
    if err = setInterfaceUP("lo"); err != nil {
        return err
    }

    // 设置外部请求(0.0.0.0/0)都通过veth访问
    _, cidr, _ := net.ParseCIDR("0.0.0.0/0")

    defaultRoute := &netlink.Route{
        LinkIndex: peerLink.Attrs().Index,
        Gw: ep.Network.IpRange.IP,
        Dst: cidr,
    }
    // 添加路由,veth到网桥
    if err = netlink.RouteAdd(defaultRoute); err != nil {
        return err
    }

    return nil
}

上边调用enterContainer进入容器

func enterContainerNetns(enLink *netlink.Link, cinfo *container.ContainerInfo) func() {
    // 获取容器的net namespace
    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()
    // 锁定当前的线程,如果不锁定,goroutine可能会被调度到别的线程,不能保证一直在所需要的网络空间
    runtime.LockOSThread()

    // 修改veth peer 另外一端移到容器的namespace中
    if err = netlink.LinkSetNsFd(*enLink, int(nsFD)); err != nil {
        logrus.Errorf("error set link netns , %v", err)
    }

    // 获取当前的网络namespace
    origns, err := netns.Get()
    if err != nil {
        logrus.Errorf("error get current netns, %v", err)
    }

    // 设置当前进程到新的网络namespace,并在函数执行完成之后再恢复到之前的namespace
    if err = netns.Set(netns.NsHandle(nsFD)); err != nil {
        logrus.Errorf("error set netns, %v", err)
    }
    return func () {
        // 恢复到之前的net namespace
        netns.Set(origns)
        // 关闭namespace文件
        origns.Close()
        // 取消线程绑定
        runtime.UnlockOSThread()
        f.Close()
    }
}

使用iptables添加端口映射

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
}

容器跨主机网络

  1. 跨主机通信需要保证宿主机之间的网段是不一致的,才能进行通信,会使用kv数据等来实现,通过全局锁等方式保证一致性
  2. 跨宿主机通信的实现方式有两种,路由和封包

封包只需要主机之间连通,但是性能损耗较大;而路由性能好,但是需要一些路由配置或者特定的协议

高级实践

创建服务省略,直接介绍runc

runc

在过去的几年Linux有Cgroup,Namespace,capability,Secomp和Apparmor等功能,Docker就重度使用了这些技术。由于容器技术是一系列难以理解和神秘系统特性的集合,所以docker公司将其整合为runc

runc是一个轻量级的容器运行引擎,包括

  • 完全支持namespace
  • 原生支持安全特性:Selinux,capability,Secomp和Apparmor等
  • CRIU项目下支持支持容器热迁移
  • 一份正式的容器标准

OCI标准包bundle,做的是如何把容器和配置数据存储在磁盘,以便运行时读取,包含2个模块

  • config.json,包括mount,process,user,hostname,platform(架构amd64,系统类型linux),Hook等
  • 容器的root文件系统

使用runc run 就可以根据当前的config进行创建,参考createContainer方法

containerd

2016年12月14日,docker将containerd剥离,捐赠到开源社区。

containerd可以作为daemon独立在linux和windows上管理容器的生命周期,通过unixsocket暴露grpc的API进行容器的操作,包括启动停止,拉取镜像,网络存储管理等,具体运行由runc完成,只要是符合OCI规范的容器都支持

功能主要有三个

  • Distribution 用于与镜像仓库交互
  • Bundle 管理磁盘上镜像的子系统
  • Runtime 创建容器,管理容器的子系统

kubernetes CRI

kubelet通过grpc框架通过unixsocket与CRIshim进行通信。

server只需要提供unixsocket启动的CRIshim,protocolbuffers提供ImageService和RuntimeService即可

CRI的核心概念就是PodSandbox和container,不同的运行时对PodSandbox的实现不一样,Hypervisor的实现就是一个虚拟机