自己动手写docker3
目录:
容器网络
网络虚拟化技术介绍
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
流程为
- 获取网关IP
- 配置网络
- 保存网络配置
创建网络
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
流程为
- 获取容器IP
- 创建网络端点
- 配置网络端点
- 创建端口映射
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
流程为
- 删除网络网关IP
- 删除网络设备
- 删除网络配置文件
方法对应
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网络的流程
- 创建虚拟设备bridge
- 设置bridge地址和路由
- 启动bridge
- 设置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
}
容器跨主机网络
- 跨主机通信需要保证宿主机之间的网段是不一致的,才能进行通信,会使用kv数据等来实现,通过全局锁等方式保证一致性
- 跨宿主机通信的实现方式有两种,路由和封包
封包只需要主机之间连通,但是性能损耗较大;而路由性能好,但是需要一些路由配置或者特定的协议
高级实践
创建服务省略,直接介绍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
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的实现就是一个虚拟机