Docker 网络
Docker网络概述
Docker 作为当下主流容器技术,其网络通信是其中比较重要的一环,Docker实际使用中会遇到如下的情况
-
单机Docker网络通信
-
多机Docker网络通信
在单机的情况下,Docker提供了5种网络模式供我们选择
- bridge网络模式
- host网络模式
- other-container网络模式
- none网络模式
- 自定义模式
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 cli
与Docker 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)
然后最终调用到
daemon
的containerStart
方法去启动一个容器而我们的网络初始化也在这里
从上文中我们了解到,docker run
一个容器的过程中,会发送两个请求到daemon
中,第一次创建一个容器,实际上是对一个容器的内存抽象,这里会将各种参数配置进去,当然包括我们的网络配置,第二次请求实际创建了容器,网络的实际的初始化在这个阶段进行。
Bridge网络模式
在源码中bridge模式,会在
create container
的过程中配置到抽象的容器实例中,然后在start container的过程中,通过daemon.allocateNetwork(container)
来分配网络接口设备信息
其中会调用
daemon.connectToNetwork(container, defaultNetName, nConf.EndpointSettings, updateSetting)
该函数会创建相应的
sandbox
、endpoint
然后将endpoint
跟sandbox
绑定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主要通过以下方式实现
- Docker Daemon 在创建容器时,会在
host
上创建两个虚拟网络接口设备,例如veth1
和veth2
,保证一个接受
到的网络报文都能传给另外一方 - Docker Daemon 将
veth1
加到docker1
的网桥上,从而转发host
的网络报文 - 将
veth1
挂到container
的网络空间下面,并改为eth0
,这样一来就实现了container
的网络隔离以及跟host
及外网的网络通信
如此虽然已经实现了container的网络隔离和网络连通性,但依然无法使外界请求到达container
如此我还需要使用NAT的方式,将网络报文发往container
所以当container
需要暴露服务时,内部的服务是需要监听容器ip
和port
的,这样才能使用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,没有自己独立的ip
和port
,而且中间经过了转发,是有一定的传输效率的
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
创建独立的隔离网络环境,所以该模式的container
和host
一起使用eth0
, 即docker
的ip
和端口,就是host
的ip
和端口
在模式下,我们明显脱离了一层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
共享其他容器的网络环境,则至少这两个容器之间不存在网络隔离,而这两个容器又与宿主机以及除此之外其他的容器存在网络隔离。
实现该模式只需要将namespace
使用other container
的namespace
即可
在这种模式下的Docker Container
可以通过localhost
来访问namespace
下的其他容器,传输效率较高。虽然多个容器共享网络环境,但是多个容器形成的整体依然与宿主机以及其他容器形成网络隔离。另外,这种模式还节约了一定数量的网络资源。但是需要注意的是,它并没有改善容器与宿主机以外世界通信的情况。
none模式
该模式顾名思义,即不设置任何的网络模式,一旦设置改模式,容器内只有lp回环网络,不会再有其他的网络资源。所以相当隔壁,相当的纯粹。
自定义模式
上文提到docker
有none
的模式,在此模式,docker
对container
的网络基本不做任何设置,所以我们可以自定义网络,从而实现我们所面对的业务场景。这也恰巧体现了Docker
设计理念的开放。
容器间通信
通过以上我们了解到了docker
的网络模式,我们面对了一个实际场景就是容器之间的通信,因为我们的服务可能是多服务的组成共同对外服务,例如传统的lnmp
服务我们需要php
、mysql
、nginx
甚至我们还需要redis
的三方服务。这些服务各自为一个容器,我们需要他们互相通信,组成一个服务对外提供。
这里我们主要使用Docker Bridge
模式,从前文我们知道在bridge
的模式下,我们的container
的网络设备与docker0
连通,所以我们的eth0
会被分配在docker0
下面的网络子网,一般为172.17.x.1
这个网段下面。
所以我们可以通过以下三种方式
-
容器的ip通信
我们可以通过
docker inspect
查询到container
到内部ip进行通信,但是这样会导致硬编码,而且从新开启一个container
之后ip又变了,也不方便迁移 -
host的端口映射
通过绑定
host
的ip
的port
的进行通信,但是依赖于外部端口进行有限的通信,做不到灵活性 -
通过容器名
可以使用容器名,通过
docker
的link
机制通信。这种方式通过docker
的link
机制可以通过一个name来和另一个容器通信,link机制方便了容器去发现其它的容器并且可以安全的传递一些连接信息给其它的容器。使用name给容器起一个别名,方便记忆和使用。即使容器重启了,地址发生了变化,不会影响两个容器之间的连接
我们主要使用第三种方式进行主机内同学,我们可以使用docker-composer
通过docker-compose.yaml
定义一个服务,docker-composer
能根据配置文件帮我创建一个多容器服务互相通信的应用服务
以下是一个laravel
、php
、nginx
、mysql
、redis
组成的一个服务通过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 源码解析