Docker 网络

Docker网络概述

Docker 作为当下主流容器技术,其网络通信是其中比较重要的一环,Docker实际使用中会遇到如下的情况

  • 单机Docker网络通信

  • 多机Docker网络通信

在单机的情况下,Docker提供了5种网络模式供我们选择

  1. bridge网络模式
  2. host网络模式
  3. other-container网络模式
  4. none网络模式
  5. 自定义模式

docker源码 github.com/docker/docker/api/types/container/hostconfig_unix.go

在源码里我们可以看到几种模式的定义

// NetworkName 返回网络栈的名称
func (n NetworkMode) NetworkName() string {
   if n.IsBridge() {
      return "bridge"
   } else if n.IsHost() {
      return "host"
   } else if n.IsContainer() {
      return "container"
   } else if n.IsNone() {
      return "none"
   } else if n.IsDefault() {
      return "default"
   } else if n.IsUserDefined() {
      return n.UserDefined()
   }
   return ""
}

在多机的情况下Docker的网络互通有以下方式

  • docker网桥实现跨主机连接
  • Open vSwitch实现跨主机容器连接
  • 使用weave实现跨主机容器连接
  • 等等(还有许多方式的网络互通)

Docker单机网络

在开始下面的内容之前,我们先梳理一下docker cli 从执行docker run ….命令到docker daemon的过程中,发生了什么。

Dokcer cliDocker daemon 的交互其实是典型的cs模式,而且其中的交互是使用请求(socket, http)的方式进行的,Docker daemon通过开启一个服务器来监听请求,例如docker run ...会发生以下的请求

![未命名表单 (https://blog.zhuxingzhao.com/post-images/1623600316756.png)

Dcoker Server是以restful-api的形式管理的,可以看到对container的操作api就可以知道

	// HEAD
	router.NewHeadRoute("/containers/{name:.*}/archive", r.headContainersArchive),
	// GET
	router.NewGetRoute("/containers/json", r.getContainersJSON),
	router.NewGetRoute("/containers/{name:.*}/export", r.getContainersExport),
	router.NewGetRoute("/containers/{name:.*}/changes", r.getContainersChanges),
	router.NewGetRoute("/containers/{name:.*}/json", r.getContainersByName),
	router.NewGetRoute("/containers/{name:.*}/top", r.getContainersTop),
	router.NewGetRoute("/containers/{name:.*}/logs", r.getContainersLogs),
	router.NewGetRoute("/containers/{name:.*}/stats", r.getContainersStats),
	router.NewGetRoute("/containers/{name:.*}/attach/ws", r.wsContainersAttach),
	router.NewGetRoute("/exec/{id:.*}/json", r.getExecByID),
	router.NewGetRoute("/containers/{name:.*}/archive", r.getContainersArchive),
	// POST
	router.NewPostRoute("/containers/create", r.postContainersCreate),
	router.NewPostRoute("/containers/{name:.*}/kill", r.postContainersKill),
	router.NewPostRoute("/containers/{name:.*}/pause", r.postContainersPause),
	router.NewPostRoute("/containers/{name:.*}/unpause", r.postContainersUnpause),
	router.NewPostRoute("/containers/{name:.*}/restart", r.postContainersRestart),
	router.NewPostRoute("/containers/{name:.*}/start", r.postContainersStart),
	router.NewPostRoute("/containers/{name:.*}/stop", r.postContainersStop),
	router.NewPostRoute("/containers/{name:.*}/wait", r.postContainersWait),
	router.NewPostRoute("/containers/{name:.*}/resize", r.postContainersResize),
	router.NewPostRoute("/containers/{name:.*}/attach", r.postContainersAttach),
	router.NewPostRoute("/containers/{name:.*}/copy", r.postContainersCopy), // Deprecated since 1.8, Errors out since 1.12
	router.NewPostRoute("/containers/{name:.*}/exec", r.postContainerExecCreate),
	router.NewPostRoute("/exec/{name:.*}/start", r.postContainerExecStart),
	router.NewPostRoute("/exec/{name:.*}/resize", r.postContainerExecResize),
	router.NewPostRoute("/containers/{name:.*}/rename", r.postContainerRename),
	router.NewPostRoute("/containers/{name:.*}/update", r.postContainerUpdate),
	router.NewPostRoute("/containers/prune", r.postContainersPrune),
	router.NewPostRoute("/commit", r.postCommit),
	// PUT
	router.NewPutRoute("/containers/{name:.*}/archive", r.putContainersArchive),
	// DELETE
	router.NewDeleteRoute("/containers/{name:.*}", r.deleteContainers)

docker run举例
docker cli会进行run 后面的解析

github.com/docker-ce/components/cli/cli/command/container/opts.go

会有每一个跟container通用选项的源码设置

run命令一些自己的参数设在

github.com/docker-ce/components/cli/cli/command/container/run.go

我们可以在源码中看到如下的代码,说明run会首先创建一个container

createResponse, err := createContainer(ctx, dockerCli, containerConfig, &opts.createOptions)

然后该函数会会在其中检查opts参数的pull属性判断需不需要拉取images,如果需要就会先执行createContainer定义的一个pullAndTagImages的函数,取发送一个请求拉取一个image
随后就会执行真正的dockerCli.Client().ContainerCreate方法将参数传递到Docker Daemon中开始创建一个container

serverResp, err := cli.post(ctx, "/containers/create", query, body, nil)

可以看到dockerCli.Client().ContainerCreate发送了一个post请求到了Docker Daemon
然后将我们的目光放到Docker Daemon开启的server

github.com/docker-ce/components/engine/api/server/router/container/container.go
从中我们可以看到/containers/create路由的注册

router.NewPostRoute("/containers/create", r.postContainersCreate),

注册的handler函数会执行其中的

ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
	Name:             name,
	Config:           config,
	HostConfig:       hostConfig,
	NetworkingConfig: networkingConfig,
	AdjustCPUShares:  adjustCPUShares,
}) 

将调用实际Docker daemon方法来进行容器的创建

func (daemon *Daemon) ContainerCreate(params types.ContainerCreateConfig) (containertypes.ContainerCreateCreatedBody, error) {
return daemon.containerCreate(createOpts{
	params:                  params,
	managed:                 false,
	ignoreImagesArgsEscaped: false})
}

最终通过一系列的设置网络,layer等最终创建了一个容器并返回给了docker cli

随后docker cli会发起start container请求

router.NewPostRoute("/containers/{name:.*}/start", r.postContainersStart),

这个handler会进行参数校验后执行

s.backend.ContainerStart(vars["name"], hostConfig, checkpoint, checkpointDir)

然后最终调用到daemoncontainerStart方法去启动一个容器而我们的网络初始化也在这里

从上文中我们了解到,docker run一个容器的过程中,会发送两个请求到daemon中,第一次创建一个容器,实际上是对一个容器的内存抽象,这里会将各种参数配置进去,当然包括我们的网络配置,第二次请求实际创建了容器,网络的实际的初始化在这个阶段进行。

Bridge网络模式

在源码中bridge模式,会在create container的过程中配置到抽象的容器实例中,然后在start container的过程中,通过

daemon.allocateNetwork(container)

来分配网络接口设备信息

其中会调用

daemon.connectToNetwork(container, defaultNetName, nConf.EndpointSettings, updateSetting)

该函数会创建相应的sandboxendpoint然后将endpointsandbox绑定

Sandbox

一个Sandbox包含了一个容器网络栈的配置。其中包括了对容器的网卡,路由表以及对DNS设置的管理。通常,一个Sandbox的实现可以是一个Linux Network Namespace,一个FreeBSD Jail或者其他类似的东西。一个Sandbox可以包含多个处于不同Network的Endpoint。

Endpoint

Endpoint将一个Sandbox加入一个Network。Endpoint的实现可以是一个veth对,一个Open vSwitch internal port或者其他类似的东西。一个Endpoint只能属于一个Network和一个Sandbox。

Network

Network是一个能够互相通信的Endpoint的集合。Network的实现可以是一个Linux网桥,一个VLAN等等。

bridge网络是docker默认的网络模式,该模式为Docker Container创建了一个独立的网络空间,保证容器内的进程组使用独立的网络环境,实现了容期间,容器和主机之间的网络空间隔离。还可以通过host上的网桥(docker0)来连通容器内部的网络空间和host的网络空间,实现容器和host的网络通信,甚至和外网的通信。

Bridge主要通过以下方式实现

  1. Docker Daemon 在创建容器时,会在host上创建两个虚拟网络接口设备,例如veth1veth2,保证一个接受
    到的网络报文都能传给另外一方
  2. Docker Daemon 将veth1加到docker1的网桥上,从而转发host的网络报文
  3. veth1挂到container的网络空间下面,并改为eth0,这样一来就实现了container的网络隔离以及跟host及外网的网络通信

如此虽然已经实现了container的网络隔离和网络连通性,但依然无法使外界请求到达container如此我还需要使用NAT的方式,将网络报文发往container

所以当container需要暴露服务时,内部的服务是需要监听容器ipport的,这样才能使用NAT的方式转发到对应的内部服务。在linux上主要使用iptables来进行网络转发(当然需要 linux支持ipv4网络报文转发的能力, 在Docker Daemon启动时,会检查linux是否支持ipv4转发,不能ipv4转发是无法启动Docker Daemon的)

具体iptables如下

iptables -I FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

该模式的有点很明显,隔离了container的网络空间,但时缺点也很明显,在暴露内部服务时,依然依赖于host的端口和公网ip,没有自己独立的ipport,而且中间经过了转发,是有一定的传输效率的

host网络模式

从上文中我们知道,网络初始化在start container的过程中

initializeNetworking方法中我们可以看到如果是host模式就与host共享网络资源

if container.HostConfig.NetworkMode.IsHost() {
   if container.Config.Hostname == "" {
      container.Config.Hostname, err = os.Hostname()
      if err != nil {
         return err
      }
   }
}

该模式就如同名字一样,就是共享host的网络空间,并没有为container创建独立的隔离网络环境,所以该模式的containerhost一起使用eth0, 即dockerip和端口,就是hostip和端口

17

在模式下,我们明显脱离了一层nat转发的过程,保证了网络传输的可靠性和传输速度。在对网络环境不要求的一些服务同时又要求网络性能的情况下,该种模式是很有必要的,与bridge模式形成了很好的不出

other-container网络模式

接上文在initializeNetworking方法中可以看到如果是container就会将该其他容器的网络空间与该容器绑定,从而共享网络空间

if container.HostConfig.NetworkMode.IsContainer() {
   // we need to get the hosts files from the container to join
   nc, err := daemon.getNetworkedContainer(container.ID, container.HostConfig.NetworkMode.ConnectedContainer())
   if err != nil {
      return err
   }

   err = daemon.initializeNetworkingPaths(container, nc)
   if err != nil {
      return err
   }

   container.Config.Hostname = nc.Config.Hostname
   container.Config.Domainname = nc.Config.Domainname
   return nil
}

这个模式是docker一个特殊的网络模式,在这个模式下的container会借助其他容器的网络环境,之所以称为“特别”,是因为这个模式下容器的网络隔离性会处于bridge桥接模式与host模式之间。Docker Container共享其他容器的网络环境,则至少这两个容器之间不存在网络隔离,而这两个容器又与宿主机以及除此之外其他的容器存在网络隔离。

17

实现该模式只需要将namespace使用other containernamespace即可

在这种模式下的Docker Container可以通过localhost来访问namespace下的其他容器,传输效率较高。虽然多个容器共享网络环境,但是多个容器形成的整体依然与宿主机以及其他容器形成网络隔离。另外,这种模式还节约了一定数量的网络资源。但是需要注意的是,它并没有改善容器与宿主机以外世界通信的情况。

none模式

该模式顾名思义,即不设置任何的网络模式,一旦设置改模式,容器内只有lp回环网络,不会再有其他的网络资源。所以相当隔壁,相当的纯粹。

自定义模式

上文提到dockernone的模式,在此模式,dockercontainer的网络基本不做任何设置,所以我们可以自定义网络,从而实现我们所面对的业务场景。这也恰巧体现了Docker设计理念的开放。

容器间通信

通过以上我们了解到了docker的网络模式,我们面对了一个实际场景就是容器之间的通信,因为我们的服务可能是多服务的组成共同对外服务,例如传统的lnmp服务我们需要phpmysqlnginx甚至我们还需要redis的三方服务。这些服务各自为一个容器,我们需要他们互相通信,组成一个服务对外提供。

这里我们主要使用Docker Bridge模式,从前文我们知道在bridge的模式下,我们的container的网络设备与docker0连通,所以我们的eth0会被分配在docker0下面的网络子网,一般为172.17.x.1这个网段下面。

所以我们可以通过以下三种方式

  1. 容器的ip通信

    我们可以通过docker inspect查询到container到内部ip进行通信,但是这样会导致硬编码,而且从新开启一个container之后ip又变了,也不方便迁移

  2. host的端口映射

    通过绑定hostipport的进行通信,但是依赖于外部端口进行有限的通信,做不到灵活性

  3. 通过容器名

    可以使用容器名,通过dockerlink机制通信。这种方式通过dockerlink机制可以通过一个name来和另一个容器通信,link机制方便了容器去发现其它的容器并且可以安全的传递一些连接信息给其它的容器。使用name给容器起一个别名,方便记忆和使用。即使容器重启了,地址发生了变化,不会影响两个容器之间的连接

我们主要使用第三种方式进行主机内同学,我们可以使用docker-composer通过docker-compose.yaml定义一个服务,docker-composer能根据配置文件帮我创建一个多容器服务互相通信的应用服务

以下是一个laravelphpnginxmysqlredis组成的一个服务通过docker-compose.yaml编排

version: '2'

services:
  nginx:
    depends_on:
      - "php"
    image: "nginx"
    restart: unless-stopped
    volumes:
      - "$PWD/deploy/nginx:/etc/nginx/conf.d"
      - "$PWD:/var/www/html"
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    ports:
      - "80:80"
      - "443:443"
    networks:
      - app-network
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
  certbot:
    image: certbot/certbot
    restart: unless-stopped
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

  php:
    image: "php"
    restart: unless-stopped
    volumes:
      - "$PWD:/var/www/html"
    build:
      context: .
      dockerfile: "Dockerfile"
    working_dir: /var/www/html
#    entrypoint:
#      - ls /var/www/html
#      - chmod +x /var/www/html/deploy/run
#      - /var/www/html/deploy/run
    networks:
      - app-network


  mysql:
    image: mysql:5.7
    volumes:
      - "$PWD/data/mysql:/var/lib/mysql"
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: xxxx
      MYSQL_DATABASE: xxx
      MYSQL_USER: xxxx
      MYSQL_PASSWORD: xxx
    ports:
      - "3306:3306"
    networks:
      - app-network


  redis:
    image: "redis"
    restart: unless-stopped
    ports:
      - "6379:6379"
    networks:
      - app-network
#    volumes:

networks:
  app-network:
    driver: bridge

从而用docker-compose up即可开启一套容器服务

参考资料

docker 源码解析