docker环境下,所有的程序都运行在一个独立的空间中(类似于虚拟机),不同容器之间无法互相访问。要想访问容器中的应用,必须要借助于网络。容器能够被外界访问,这就是容器暴露。容器作为服务提供给外界访问,这是服务暴露。由于docker经常以微服务的形式存在,所以本文重点在于服务暴露。当然在介绍服务暴露之前,会介绍容器暴露作为基础。无论怎么样的,访问容器和访问服务都需要网络。
1.访问容器
访问容器分两种,一种是机器上的容器访问另一个容器,另外一个是机器之外的其他主机(可以浏览器,也可以是其他应用)访问容器。下面分段介绍。
容器之间互访
容器之间互访又分为两种,一种是同一个机器上的互访,另外一种是不同机器上的互访。同一个机器上的访问需要借助于bridge(桥接网络)。所有在本机上创建的容器的都会连接到一个默认的网桥中(网络的名称是bridge),bridge的一端连着容器的eth0虚拟网卡,另外一端连着主机上的虚拟网络docker0,如下图:
这个默认的网络主要是用于给容器分配ip地址。但是处于这个默认的bridge中的容器并不能直接通信,想要通信需要使用自定义网桥。自定义网桥也是一个虚拟网络,其本质是设定iptables转发。原理是根据容器的IP地址的所在域,将外部访问某一个实体网卡端口的流量转发到这个IP上。从上面这段话上,可以知道,容器之间互访,iptables必须要满足两个条件:
- 容器之间通信的端口可以访问,本机上不同的容器通信使用的
loopback网络,所以和iptables没有关系。但是不同主机之间的容器互访需要这个端口打开。具体的端口通常是随机的一个大号端口,通常设定两台机器互访无限制即可。 - iptables 转发要打开,具体打开需要下面两个语句:
#打开IPv4转发功能
sysctl net.ipv4.conf.all.forwarding=1
#打开防火墙转发策略
sudo iptables -P FORWARD ACCEPT
#上面这两个语句都不是永久的,永久的需要写入具体的配置文件。一般在安装集群的时候都会要求上面两个永久写入配置文件。请参考docker集群的安装
还需要说的一点,由于容器的生命周期较短,经常会重启,所以它的IP地址不是固定的,如果想要直接访问容器中的应用,可以通过下面的语句获取ip地址:docker inspect $CONTAINER_ID | grep IPAddress
由于IP的不固定性(但是docker的ip不是随机的,而是按照一定顺序使用的),通常需要用到DNS,即创建容器时就将容器的IP地址写入DNS,那样的话就可以用过DNS中设定的名称访问容器了。具体和DNS配置的信息我还没有研究过,都是用docker现成的,后续如果需要用到可以研究一下。
上面讲的配置微微有点偏题,在同一个主机上的不同容器互访没有作用(但是对下文很有作用,这里就提前打好基础。兵马未动粮草先行,😃)。下面介绍如何让同一个机器上的两个容器通信。
同一台主机上的容器通信
docker官方提供了一个方案,就是将多个容器编入同一个docker-compose,这样的话docker会为这个compose创建一个默认的自定义网络,处于这个自定义网络中的容器是互相访问没有限制的。并且这个自定义网络中的容器之间都互相配置了DNS解析,可以直接使用容器的名称代替IP地址访问。下面演示一下docker-compose。
首先下面是我写好的一份docker-compose文件,保存为docker-compose.yml即可。version: "3"
services:
portainer-web:
image: "rdsource.tp-link.net:8088/portainer/portainer"
command: --templates http://portainer-templates/templates.json
restart: on-failure
ports:
- "9000:9000"
volumes:
- "/opt/docker_data/portainer:/data"
- "/var/run/docker.sock:/var/run/docker.sock"
networks:
- portainer_net
portainer-templates:
image: "portainer-templates"
volumes:
- "/opt/docker_data/portainer_template/templates.json:/usr/share/nginx/html/templates.json"
expose:
- "80"
restart: on-failure
networks:
- portainer_net
networks:
portainer_net:
driver: bridge
ipam:
config:
- subnet: 172.81.0.0/16
上面这个compose创建了两个应用,一个web应用portainer-web,应用的端口是9000,一个用于web应用的静态资源portainer-template(用nginx做代理),可以看到portainer-web访问portainer-template的80端口是直接使用它的名称而不是IP地址。同时这个compose还在最后创建了一个自定义网络,平时不需要手动创建,这次只是为了演示。上面两个应用都通过networks关键词加入了这个网络(为什么是networks呢?因为一个容器理所当然可以加入很多网络啦。😆)。在docker-compose文件所在目录中执行docker-compose up -d(-d 是表示后台运行)。这样docker就会启动了上述两个应用,并且还有一个叫portainer_net的bridge网络。[root@itdocker portainer]# docker-compose up -d
Creating network "portainer_portainer_net" with driver "bridge"
Creating portainer_portainer-web_1 ... done
Creating portainer_portainer-templates_1 ... done
下面进入portainer-templates容器中看看是否可以访问portainer-web。/ # ping portainer-web
PING portainer-web (172.81.0.2): 56 data bytes
64 bytes from 172.81.0.2: seq=0 ttl=64 time=0.197 ms
64 bytes from 172.81.0.2: seq=1 ttl=64 time=0.132 ms
64 bytes from 172.81.0.2: seq=2 ttl=64 time=0.182 ms
64 bytes from 172.81.0.2: seq=3 ttl=64 time=0.186 ms
64 bytes from 172.81.0.2: seq=4 ttl=64 time=0.175 ms
64 bytes from 172.81.0.2: seq=5 ttl=64 time=0.131 ms
^C
--- portainer-web ping statistics ---
6 packets transmitted, 6 packets received, 0% packet loss
可以看到已经ping通了。
用wget测试一下/ # wget portainer-web:9000
Connecting to portainer-web:9000 (172.81.0.2:9000)
index.html 100% |********************************************************************************************************************************************| 2748 0:00:00 ETA
wget也成功了。这就说明两个容器之间的互访已经没有问题了。
这是docker-compose的方式(k8s集群中也提供了类似的方式),也是docker提供的同一个主机中容器间互访的解决方案,步骤主要是创建一个bridge网络,然后添加DNS解析,实现容器间互访。
不同主机的容器(服务)互访
这里说的不同主机间的容器互访指的是同一个集群中的不同的主机。不在同一个集群中主机中容器互访是通过主机暴露端口实现的,下文会讲到。同一个集群中的不同主机的容器互访要解决的问题主要有两个:
- 服务发现。容器在集群中可能动态分布到不同主机中,ip也不固定,需要有一个中间件管理这个服务分布信息
- 要有一个网络,无论其容器分布在哪个主机,哪个IP,其在这个网络上暴露的IP不会变,不然很有可能其他容器无法访问这个容器。
在集群中实现容器互访,docker提供了一种overlay网络,这个overlay网络是基于swarm集群的。docker swarm集群是docker原生的集群管理工具,和docker集成度很高,所以用起来非常方便。overlay的架构图如下:
overlay网络是受Docker Universal Control Plane (UCP)控制的,UCP负责集群中不同主机中容器管理。UCP将跨主机的容器作为服务(service),服务发现也由UCP实现。但是这个原生的服务发现在一些简单的环境中还可以,但是一旦到了大型集群中,能力就有点捉襟见肘了(主要原因还是起步太晚,尚未成熟)。工业界比较成熟的方案是Etcd,k8s默认就是采用了这个方案。swarm在小型环境下还不错,并且支持windows,对于一些企业的小型架构来说已经够了。
一旦创建了一个swarm集群,那么docker就会有默认的overlay网络,叫ingress,可以自定义这个ingress网络,如修改其网域和网关等等。每一个主机都通过一个bridge和这个overlay网络连接。处于overlay网络中的服务默认是负载均衡的。即如果一个服务有多个容器,那么流量会均摊到这些容器上,采用轮询的方式选择容器(round robin)。在docker swarm中创建一个服务(只能创建服务,创建单个容器不要集群)会指定replica,表明这个服务的后端有多少个进程,这些进程以容器的形式随机分布在各个节点上。那么其他容器如何访问这个服务呢?通过服务发现,发布服务时,会将服务随机绑定在overlay网络上的某个端口上,一旦检测到这个端口的流量,就会在UCP中找到服务对应的容器,获取它的IP地址和暴露的端口(服务的容器在哪个节点,Ip是多少都是注册在UCP中),UCP将这个流量采用轮询的方式转发到对应容器中。这样一来,容器之间就能够通讯了,这是外界访问这个服务也是同样的原理。在外界看来,服务是暴露的端口,只要访问集群中任意一个节点的某个端口,就能实现访问服务。服务之间互访需要一个自定义的overlay网络而不是用默认的overlay(通过docker stack部署会自动创建),这样不同的服务之间就能互相访问了。某一个主机单单的容器只要和加入这个网络,也能实现互访,不然只能通过其暴露的端口进行访问。
在k8s中还要这个稍微复杂一点,这点以后再讲。
2.外部访问容器
稍微学习过docker的都知道访问容器是通过容器暴露端口,并将端口映射到主机端口上。外部通过主机ip+端口进行访问。对于服务也是一样的,虽然服务的容器是随机分布到一些节点中,但是通过任意节点+端口都能访问这个应用,无论这个节点上是否有对应容器(同理,就算指定了节点+端口,也不一定能访问这个节点的容器),具体访问哪个节点是轮询的(对于用户来说基本上就是随机的了)。
这种情况下,如果docker上有上百个服务,那么用户访问特定应用就需要记忆特定的端口,长期以往应用管理也非常不方便。用户反映某个应用不能访问,告诉管理员端口,然后管理员还要查询这个端口对应哪个容器。天哪。😢。docker swarm 没有提供关于这个问题的解决方案,而相对成熟的k8s使用nginx解决了这个问题,使得用户可以使用子域名访问不同的应用。在这个模式下,所有的web应用都通过主机上的80端口,监控80端口的nginx根据访问的域名将请求转发到后台的服务中,如何实现将在下一篇文章中写。
总结
docker简化了访问容器和互相访问的操作,使得几个命令就可以实现外部和内部访问特定容器和服务。这个模式在小规模集群中非常好用,但是一旦涉及到大的集群,涉及到集群分类等等,swarm就有心无力了。在这种情况下,k8s是更好的选择。同时k8s还提供了一整套集群监控管理方案,相对于swarm更加成熟。小打小闹适合用swarm,上手极快。生产环境中建议用k8s。