Docker Compose 网络设置,docker容器下的微服务网络通信实践

Scroll Down

来源:NanCheung`s Blog
链接:https://blog.nancheung.com /archives/400535
商业转载请联系作者获得授权,非商业转载请注明出处。



基本概念

默认情况下,每一个Compose都会为其中的容器创建一个网络,服务的每个容器都会加入该网络中。这样,在同一个网络下的容器就可被互相访问,不仅如此,每个容器还能以自己的容器名称作为hostname被其他容器访问。

默认情况下,应用程序的网络名称基于Compose的工程名称,而项目名称基于docker-compose.yml所在目录的名称。如需修改工程名称,可使用--project-name标识或COMPOSE_PORJECT_NAME环境变量。

举个例子,我的博客,docker-compose.yaml在路径为 /root/compose/ 的目录中,并且docker-compose.yaml如下所示:

version: '3.7'
services:
    caddy:
        image: caddy
        ports:
          - 80:80
          - 443:443
          
    blog:
        image: blog

当我们运行docker-compose up时,将会执行以下几步:

  • 创建一个名为 compose_default 的网络;

    可以使用 docker network ls 查看docker创建的网络,我这边就创建了另一个compose_default的网络

    image.png

  • 使用caddy服务的配置创建容器,它以“caddy”这个名称加入网络compose_default;

  • 使用blog服务的配置创建容器,它以“blog”这个名称加入网络compose_default。

容器间可使用服务名称(caddy或blog)作为hostname相互访问。例如,caddy这个服务可使用blog 访问到blog容器。假设blog服务的端口是80端口,那么caddy服务可以通过 blog:80 访问到blog服务。

同时,也可以发现我blog容器没有配置端口映射。因为caddy和blog已经处于一个网络下,它们互相通过host:port的方式进行容器间的通信,无需把端口暴露给宿主机了。

指定自定义网络

一些场景下,默认的网络配置满足不了我们的需求,此时我们可使用networks命令自定义网络。networks命令允许我们创建更加复杂的网络拓扑并指定自定义网络驱动和选项。不仅如此,我们还可使用networks将服务连接到不是由Compose管理的、外部创建的网络。

如下,我们在其中定义了两个自定义网络。

version: '3.7'
services:
    caddy:
        image: caddy
        ports:
          - 80:80
          - 443:443
        networks:
          - n1
          - n2
          
    v2ray:
        image: v2ray
        networks:
          - n1
          
    blog:
        image: blog
        networks:
          - n2

networks:
  n1:
    driver: d1
  n2:
    driver: d2
    driver_opts:
      foo: "1"
      bar: "2"

其中,v2ray服务与blog服务隔离,两者分别使用自己的网络;caddy服务可与两者通信。

由本例不难发现,使用networks命令,即可方便实现服务间的网络隔离与连接。

配置默认网络

除自定义网络外,我们也可为默认网络自定义配置。

version: '3.7'
services:
    caddy:
        image: caddy
        ports:
          - 80:80
          - 443:443
          
    v2ray:
        image: v2ray
          
    blog:
        image: blog

networks:
    default:
        driver: default-driver-1

这样,就可为该应用指定自定义的网络驱动。

使用已存在的网络

一些场景下,我们并不需要创建新的网络,而只需加入已存在的网络,此时可使用external选项。示例:

networks:
  default:
    external:
      name: old-network

Docker连接两个容器

在Docker中,容器之间的链接是一种很常见的操作:它提供了访问其中的某个容器的网络服务而不需要将所需的端口暴露给Docker Host主机的功能。

在不使用Docker Compose的时候,将两个容器链接起来使用—link参数,相对来说比较简单,以caddy镜像为例子:

docker run --rm --name test1 -d caddy  #开启一个实例test1
docker run --rm --name test2 --link test1 -d caddy #开启一个实例test2并与test1建立链接

这样,test2test1便建立了链接,就可以在test2中使用访问test1中的服务了。

如果使用Docker Compose,那么这个事情就更简单了,Docker Compose中对该特性的支持同样是很方便的。

Docker Compose 链接外部容器的几种方式

还是以上面的nginx镜像为例子,编辑docker-compose.yml文件为:

version: '3.7'
services:
  test2:
    image: caddy
    depends_on:
      - test1
    links:
      - test1
  test1:
    image: caddy

最终效果与使用普通的Docker命令docker run xxxx建立的链接并无区别。这只是一种最为理想的情况。

  1. 如果容器没有定义在同一个docker-compose.yml文件中,应该如何链接它们呢?
  2. 又如果定义在docker-compose.yml文件中的容器需要与docker run xxx启动的容器链接,需要如何处理?

针对这两种典型的情况,下面给出我个人测试可行的办法:

方式一:让需要链接的容器同属一个外部网络

我们还是使用caddy镜像来模拟这样的一个情景:假设我们需要将两个使用Docker Compose管理的caddy容器(test1test2)链接起来,使得test2能够访问test1中提供的服务,这里我们以能ping通为准。

首先,我们定义容器test1docker-compose.yml文件内容为:

version: '3.7'
services:
  test2:
    image: caddy
    container_name: test1
    networks:
      - default
      - app_net
networks:
  app_net:
    external: true

容器test2内容与test1基本一样,只是多了一个external_links,需要特别说明的是:最近发布的Docker版本已经不需要使用external_links来链接容器,容器的DNS服务可以正确的作出判断,因此如果你需要兼容较老版本的Docker的话,那么容器test2docker-compose.yml文件内容为:

version: '3.7'
services:
  test2:
    image: caddy
    networks:
      - default
      - app_net
    external_links:
      - test1
    container_name: test2
networks:
  test_net:
    external: true

否则的话,test2docker-compose.ymltest1的定义完全一致,不需要额外多指定一个external_links。相关的问题请参见stackoverflow上的相关问题:docker-compose + external container

正如你看到的那样,这里两个容器的定义里都使用了同一个外部网络test_net,因此,我们需要在启动这两个容器之前通过以下命令再创建外部网络:

docker network create test_net

之后,通过docker-compose up -d命令启动这两个容器,然后执行docker exec -it test2 ping test1,你将会看到如下的输出:

docker exec -it test2 ping test1
PING test1 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: icmp_seq=0 ttl=64 time=0.091 ms
64 bytes from 172.18.0.2: icmp_seq=1 ttl=64 time=0.146 ms
64 bytes from 172.18.0.2: icmp_seq=2 ttl=64 time=0.150 ms
64 bytes from 172.18.0.2: icmp_seq=3 ttl=64 time=0.145 ms
64 bytes from 172.18.0.2: icmp_seq=4 ttl=64 time=0.126 ms
64 bytes from 172.18.0.2: icmp_seq=5 ttl=64 time=0.147 ms

证明这两个容器是成功链接了,反过来在test1中pingtest2也是能够正常ping通的。

如果我们通过docker run --rm --name test3 -d caddy这种方式来先启动了一个容器(test3)并且没有指定它所属的外部网络,而需要将其与test1或者test2链接的话,这个时候手动链接外部网络即可:

docker network connect test_net test3

这样,三个容器都可以相互访问了.

方式二:更改需要链接的容器的网络模式

通过更改你想要相互链接的容器的网络模式为bridge,并指定需要链接的外部容器(external_links)即可。与同属外部网络的容器可以相互访问的链接方式一不同,这种方式的访问是单向的。

还是以caddy容器镜像为例子,如果容器实例caddy1需要访问容器实例caddy2,那么caddy2doker-compose.yml定义为:

version: '3.7'
services:
  nginx2:
    image: caddy
    container_name: caddy2
    network_mode: bridge

与其对应的,caddy1docker-compose.yml定义为:

version: '3.7'
services:
  nginx1:
    image: caddy
    external_links:
      - caddy2
    container_name: caddy1
    network_mode: bridge
复制代码

需要特别说明的是,这里的external_links是不能省略的,而且caddy1的启动必须要在caddy2之后,否则可能会报找不到容器caddy2的错误。

接着我们使用ping来测试下连通性:

$ docker exec -it caddy1 ping caddy2
PING caddy2 (172.17.0.4): 56 data bytes
64 bytes from 172.17.0.4: icmp_seq=0 ttl=64 time=0.141 ms
64 bytes from 172.17.0.4: icmp_seq=1 ttl=64 time=0.139 ms
64 bytes from 172.17.0.4: icmp_seq=2 ttl=64 time=0.145 ms

$ docker exec -it caddy2 ping caddy1 #caddy2 to caddy1
ping: unknown host

以上也能充分证明这种方式是属于单向联通的。

在实际应用中根据自己的需要灵活的选择这两种链接方式,如果想偷懒的话,大可选择第二种。不过我更推荐第一种,不难看出无论是联通性还是灵活性,较为更改网络模式的第二种都更为友好。

在我们的微服务部署中,一套服务使用同一个compose工程,同属于一个网络,不向宿主机暴露端口,只进行容器间的网络通信,无疑大大增强了安全性。