社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  docker

梦境亦相通:上手 Docker Network

图雀社区 • 4 年前 • 330 次点击  
阅读 159

梦境亦相通:上手 Docker Network

在实际应用中,不同的服务之间是需要通信的,例如后端 API 和数据库;幸运的是,Docker 为我们提供了网络(Network)机制,能够轻松实现容器互联。这篇文章将带你轻松上手 Docker 网络,学会使用默认网络和自定义网络,成为一名能够连接多个“梦境”的筑梦师!

上一篇教程中,我们带你了解了镜像和容器这两大关键的概念,熟悉了常用的 docker 命令,并成功地容器化了第一个应用。但是,那只是我们“筑梦之旅”的序章。接下来,我们将实现后端 API 服务器 + 数据库的容器化。

我们为你准备好了应用程序代码,请运行以下命令:

# 如果你看了上一篇教程,仓库已经克隆下来了
cd docker-dream
git fetch origin network-start
git checkout network-start

# 如果你打算直接从这篇教程开始
git clone -b network-start https://github.com/tuture-dev/docker-dream.git
cd docker-dream
复制代码

和之前容器化前端静态页面服务器相比,多了一个难点:服务器和数据库分别是两个独立的容器,但是服务器需要连接和访问数据库,怎么实现跨容器之间的通信?

在《盗梦空间》中,不同的梦境之间是无法连接的,然而幸运的是在 Docker 中是可以的——借助 Docker Network。

提示

在早期,Docker 容器可以通过 docker run 命令的 --link 选项来连接容器,但是 Docker 官方宣布这种方式已经过时,并有可能被移除 (参考文档)。而本文将讲解 Docker 官方推荐的方式连接容器:自定义网络(User-defined Networks)。

Network 类型

Network,顾名思义就是“网络”,能够让不同的容器之间相互通信。首先有必要要列举一下 Docker Network 的五种驱动模式(driver):

  • bridge:默认的驱动模式,即“网桥”,通常用于单机(更准确地说,是单个 Docker 守护进程)
  • overlay:Overlay 网络能够连接多个 Docker 守护进程,通常用于集群,后续讲 Docker Swarm 的文章会重点讲解
  • host:直接使用主机(也就是运行 Docker 的机器)网络,仅适用于 Docker 17.06+ 的集群服务
  • macvlan:Macvlan 网络通过为每个容器分配一个 MAC 地址,使其能够被显示为一台物理设备,适用于希望直连到物理网络的应用程序(例如嵌入式系统、物联网等等)
  • none:禁用此容器的所有网络

这篇文章将围绕默认的 Bridge 网络驱动展开。没错,就是连接不同梦境的那座“桥”。

小试牛刀

我们还是通过一些小实验来理解和感受 Bridge Network。与上一节不同的是,我们将使用 Alpine Linux 镜像作为实验原材料,因为:

  • 非常轻量小巧(整个镜像仅 5MB 左右)
  • 功能丰富,比“瑞士军刀” Busybox 还要完善

网桥网络可分为两类:

  1. 默认网络(Docker 运行时自带,不推荐用于生产环境)
  2. 自定义网络(推荐使用)

让我们分别实践一下吧。

默认网络

这个小实验的内容如下图所示:

我们会在默认的 bridge 网络上连接两个容器 alpine1alpine2。 运行以下命令,查看当前已有的网络:

docker network ls
复制代码

应该会看到以下输出(注意你机器上的 ID 很有可能不一样):

NETWORK ID          NAME                DRIVER              SCOPE
cb33efa4d163        bridge              bridge              local
010deedec029        host                host                local
772a7a450223        none                null                local
复制代码

这三个默认网络分别对应上面的 bridgehostnone 网络类型。接下来我们将创建两个容器,分别名为 alpine1alpine2,命令如下:

docker run -dit --name alpine1 alpine
docker run -dit --name alpine2 alpine
复制代码

-dit-d(后台模式)、-i(交互模式)和 -t(虚拟终端)三个选项的合并。通过这个组合,我们可以让容器保持在后台运行而不会退出(没错,相当于是在“空转”)。

docker ps 命令确定以上两个容器均在后台运行:

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
501559d2fab7        alpine              "/bin/sh"           2 seconds ago       Up 1 second                             alpine2
18bed3178732        alpine              "/bin/sh"           3 seconds ago       Up 2 seconds                            alpine1
复制代码

通过以下命令查看默认的 bridge 网络的详情:

docker network inspect bridge
复制代码

应该会输出 JSON 格式的网络详细数据:

[
  {
    "Name": "bridge",
    "Id": "cb33efa4d163adaa61d6b80c9425979650d27a0974e6d6b5cd89fd743d64a44c",
    "Created": "2020-01-08T07:29:11.102566065Z",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
      "Driver": "default",
      "Options": null,
      "Config": [
        {
          "Subnet": "172.17.0.0/16",
          "Gateway": "172.17.0.1"
        }
      ]
    },
    "Internal": false,
    "Attachable": false,
    "Ingress": false,
    "ConfigFrom": {
      "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {
      "18bed3178732b5c7a37d7ad820c111fac72a6b0f47844401d60a18690bd37ee5": {
        "Name": "alpine1",
        "EndpointID": "9c7d8ee9cbd017c6bbdfc023397b23a4ce112e4957a0cfa445fd7f19105cc5a6",
        "MacAddress": "02:42:ac:11:00:02",
        "IPv4Address": "172.17.0.2/16",
        "IPv6Address": ""
      },
      "501559d2fab736812c0cf181ed6a0b2ee43ce8116df9efbb747c8443bc665b03": {
        "Name": "alpine2",
        "EndpointID": "da192d61e4b2df039023446830bf477cc5a9a026d32938cb4a350a82fea5b163",
        "MacAddress": "02:42:ac:11:00:03"


    
,
        "IPv4Address": "172.17.0.3/16",
        "IPv6Address": ""
      }
    },
    "Options": {
      "com.docker.network.bridge.default_bridge": "true",
      "com.docker.network.bridge.enable_icc": "true",
      "com.docker.network.bridge.enable_ip_masquerade": "true",
      "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
      "com.docker.network.bridge.name": "docker0",
      "com.docker.network.driver.mtu": "1500"
    },
    "Labels": {}
  }
]
复制代码

我们重点要关注的是两个字段:

  • IPAM:IP 地址管理信息(IP Address Management),可以看到网关地址为 172.17.0.1(由于篇幅有限,想要了解网关的同学可自行查阅计算机网络以及 TCP/IP 协议方面的资料)
  • Containers:包括此网络上连接的所有容器,可以看到我们刚刚创建的 alpine1alpine2,它们的 IP 地址分别为 172.17.0.2172.17.0.3(后面的 /16 是子网掩码,暂时不用考虑)

提示

如果你熟悉 Go 模板语法,可以通过 -fformat)参数过滤掉不需要的信息。例如我们只想查看 bridge 的网关地址:

$ docker network inspect --format '{{json .IPAM.Config }}' bridge
[{"Subnet":"172.17.0.0/16","Gateway":"172.17.0.1"}]
复制代码

让我们进入 alpine1 容器中:

docker attach alpine1
复制代码

注意

attach 命令只能进入设置了交互式运行的容器(也就是在启动时加了 -i 参数)。

如果你看到前面的命令提示符变成 / #,说明我们已经身处容器之中了。我们通过 ping 命令测试一下网络连接情况,首先 ping 一波图雀社区的主站 tuture.co(-c 参数代表发送数据包的数量,这里我们设为 5):

/ # ping -c 5 tuture.co
PING tuture.co (150.109.19.98): 56 data bytes
64 bytes from 150.109.19.98: seq=2 ttl=37 time=65.294 ms
64 bytes from 150.109.19.98: seq=3 ttl=37 time=65.425 ms
64 bytes from 150.109.19.98: seq=4 ttl=37 time=65.332 ms

--- tuture.co ping statistics ---
5 packets transmitted, 3 packets received, 40% packet loss
round-trip min/avg/max = 65.294/65.350/65.425 ms
复制代码

OK,虽然丢了几个包,但是可以连上(取决于你的网络环境,全丢包也是正常的)。由此可见,容器内可以访问主机所连接的全部网络(包括 localhost)。

接下来测试能否连接到 alpine2,在刚才 docker network inspect 命令的输出中找到 alpine2 的 IP 为 172.17.0.3,尝试能否 ping 通:

/ # ping -c 5 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.147 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.103 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.102 ms
64 bytes from 172.17.0.3: seq=3 ttl=64 time=0.125 ms
64 bytes from 172.17.0.3: seq=4 ttl=64 time=0.125 ms

--- 172.17.0.3 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.102/0.120/0.147 ms
复制代码

完美!我们能够从 alpine1 中访问 alpine2 容器。作为练习,你可以自己尝试一下能否从 alpine2 容器中 ping 通 alpine1 哦。

注意

如果你不想让 alpine1 停下来,记得通过 Ctrl + P + Ctrl + Q(按住 Ctrl,然后依次按 P 和 Q 键)“脱离”(detach,也就是刚才 attach 命令的反义词)容器,而不是按 Ctrl + D 哦。

自定义网络

如果你跟着上面一路试下来,会发现默认的 bridge 网络存在一个很大的问题:只能通过 IP 地址相互访问。这毫无疑问是非常麻烦的,当容器数量很多的时候难以管理,而且每次的 IP 都可能发生变化。

而自定义网络则很好地解决了这一问题。在同一个自定义网络中,每个容器能够通过彼此的名称相互通信,因为 Docker 为我们搞定了 DNS 解析工作,这种机制被称为服务发现(Service Discovery)。具体而言,我们将创建一个自定义网络 my-net,并创建 alpine3alpine4 两个容器,连上 my-net,如下图所示。

让我们开始动手吧。首先创建自定义网络 my-net

docker network create my-net
# 由于默认网络驱动为 bridge,因此相当于以下命令
# docker network create --driver bridge my-net
复制代码

查看当前所有的网络:

docker network ls
复制代码

可以看到刚刚创建的 my-net

NETWORK ID          NAME                DRIVER              SCOPE
cb33efa4d163        bridge              bridge              local
010deedec029        host                host                local
feb13b480be6        my-net              bridge              local
772a7a450223        none                null                local
复制代码

创建两个新的容器 alpine3alpine4

docker run -dit --name alpine3 --network my-net alpine
docker run -dit --name alpine4 --network my-net alpine
复制代码

可以看到,我们通过 --network 参数指定容器想要连接的网络(也就是刚才创建的 my-net)。

提示

如果在一开始创建并运行容器时忘记指定网络,那么下次再想指定网络时,可以通过 docker network connect 命令再次连上(第一个参数是网络名称 my-net,第二个是需要连接的容器 alpine3):

docker network connect my-net alpine3
复制代码

进入到 alpine3 中,测试能否 ping 通 alpine4

$ docker attach alpine3
/ # ping -c 5 alpine4
PING alpine4 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.247 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.176 ms
64 bytes from 172.19.0.3: seq=2 ttl=64 time=0.180 ms
64 bytes from 172.19.0.3: seq=3 ttl=64 time=0.176 ms
64 bytes from 172.19.0.3: seq=4 ttl=64 time=0.161 ms

--- alpine4 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.161/0.188/0.247 ms
复制代码

可以看到 alpine4 被自动解析成了 172.19.0.3。我们可以通过 docker network inspect 来验证一下:

$ docker network inspect --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}} {{end}}' my-net
alpine4: 172.19.0.3/16 alpine3: 172.19.0.2/16
复制代码

可以看到 alpine4 的 IP 的确为 172.19.0.3,解析是正确的!

一些收尾工作

实验做完了,让我们把之前所有的容器全部销毁:

docker rm -f alpine1 alpine2 alpine3 alpine4
复制代码

把创建的 my-net 也删除:

docker network rm my-net
复制代码

动手实践

容器化服务器

我们首先对后端服务器也进行容器化。创建 server/Dockerfile,代码如下:

FROM node:10

# 指定工作目录为 /usr/src/app,接下来的命令全部在这个目录下操作
WORKDIR /usr/src/app

# 将 package.json 拷贝到工作目录
COPY package.json .

# 安装 npm 依赖
RUN npm config set registry https://registry.npm.taobao.org && npm install

# 拷贝源代码
COPY . .

# 设置环境变量(服务器的主机 IP 和端口)
ENV MONGO_URI=mongodb://dream-db:27017/todos
ENV HOST=0.0.0.0
ENV PORT=4000

# 开放 4000 端口
EXPOSE 4000

# 设置镜像运行命令
CMD [ "node", "index.js" ]
复制代码

可以看到这个 Dockerfile 比上一篇教程中的要复杂不少。每一行的含义已经注释在代码中了,我们来看一看多了哪些新东西:

  • RUN 指令用于在容器中运行任何命令,这里我们通过 npm install 安装所有项目依赖(当然之前配置了一下 npm 镜像,可以安装得快一点)
  • ENV 指令用于向容器中注入环境变量,这里我们设置了 数据库的连接字符串 MONGO_URI注意这里给数据库取名为 dream-db,后面就会创建这个容器),还配置了服务器的 HOSTPORT
  • EXPOSE 指令用于开放端口 4000。之前在用 Nginx 容器化前端项目时没有指定,是因为 Nginx 基础镜像已经开放了 8080 端口,无需我们设置;而这里用的 Node 基础镜像则没有开放,需要我们自己去配置
  • CMD 指令用于指定此容器的启动命令(也就是 docker ps 查看时的 COMMAND 一列),对于服务器来说当然就是保持运行状态。在后面“回忆与升华”部分会详细展开。

注意

初次尝试容器的朋友很容易犯的一个错误就是忘记将服务器的 hostlocalhost127.0.0.1)改成 0.0.0.0,导致服务器无法在容器之外被访问到(我自己学习的时候也浪费了很多时间)。

与之前前端容器化类似,创建 server/.dockerignore 文件,忽略服务器日志 access.lognode_modules,代码如下:

node_modules
access.log
复制代码

在项目根目录下运行以下命令,构建服务器镜像,指定名称为 dream-server

docker build -t dream-server server
复制代码

连接服务器与数据库

根据之前的知识,我们为现在的“梦想清单”应用创建一个自定义网络 dream-net

docker network create dream-net
复制代码

我们使用官方的 mongo 镜像创建并运行 MongoDB 容器,命令如下:

docker run --name dream-db --network dream-net -d mongo
复制代码

我们指定容器名称为 dream-db(还记得这个名字吗),所连接的网络为 dream-net,并且在后台模式下运行(-d)。

提示

你也许会问,为什么这里开启容器的时候没有指定端口映射呢?因为在同一自定义网络中的所有容器会互相暴露所有端口,不同的应用之间可以更轻松地相互通信;同时,除非通过 -p--publish)手动开放端口,网络之外无法访问网络中容器的其他端口,实现了良好的隔离性。网络之内的互操作性网络内外的隔离性也是 Docker Network 的一大优势所在。

危险!

这里我们在开启 MongoDB 数据库容器时没有设置任何鉴权措施(例如设置用户名和密码),所有连接数据库的请求都可以任意修改数据,在生产环境是极其危险的。后续文章中我们会讲解如何在容器中管理机密信息(例如密码)。

然后运行服务器容器:

docker run -p 4000:4000 --name dream-api --network dream-net -d dream-server
复制代码

查看服务器容器的日志输出,确定 MongoDB 连接成功:

$ docker logs dream-api                                                       
Server is running on http://0.0.0.0:4000
Mongoose connected.
复制代码

接着你可以通过 Postman 或者 curl 来测试一波服务器 API (localhost:4000 ),这里为了节约篇幅就省略了。当然你也可以直接跳过,因为马上我们就可以通过前端来操作数据了!

容器化前端页面

正如上一篇文章所实现的那样,在项目根目录下,通过以下命令进行容器化:

docker build -t dream-client client
复制代码

然后运行容器:

docker run -p 8080:80 --name client -d dream-client
复制代码

可以通过 docker ps 命令检验三个容器是否全部正确开启:

最后,访问 localhost:8080

可以看到,我们在最后刷新了几次页面,数据记录也都还在,说明我们带有数据库的全栈应用跑起来了!让我们通过交互式执行的方式进入到数据库容器 dream-db 中,通过 Mongo Shell 简单地查询一波刚才的数据:

$ docker exec -it dream-db mongo
MongoDB shell version v3.4.10
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 3.4.10
Welcome to the MongoDB shell.
For interactive help, type "help".
> use todos
switched to db todos
> db.getCollection('todos').find()
{ "_id" : ObjectId("5e171fda820251a751aae6f5"), "completed" : true, "text" : "了解 Docker Network", "timestamp" : ISODate("2020-01-09T12:43:06.865Z"), "__v" : 0 }
{ "_id" : ObjectId("5e171fe08202517c11aae6f6"), "completed" : true, "text" : "搭建默认网络", "timestamp" : ISODate("2020-01-09T12:43:12.205Z"), "__v" : 0 }
{ "_id" : ObjectId("5e171fe3820251d1a4aae6f7"), "completed" : false, "text" : "搭建自定义网络", "timestamp" : ISODate("2020-01-09T12:43:15.962Z"), "__v" : 0 }
复制代码

完美!然后按 Ctrl + D 就可以退出来了。

回忆与升华

理解命令:梦境的主旋律

每个容器自从被创建之时,就注定要运行一道命令(Command),就好像在筑梦时要安排一个主旋律、一个基调那样。之前在运行 docker ps 的时候,你应该也注意到了 COMMAND 一栏,正是每个容器所运行的命令。那么我们怎么指定容器的命令呢?又能不能运行新的命令呢?

首先,我们主要通过两种方式指定容器的命令:

通过 Dockerfile 提供默认命令

在构建镜像时,我们可以在 Dockerfile 的最后通过 CMD 指令指定命令,例如在构建后端服务器时的 [ "node", "server.js" ] 命令。在指定命令时,我们有三种写法:

  • CMD ["executable","param1","param2"](exec 格式,推荐
  • CMD ["param1","param2"](需要结合 Entrypoint 使用)
  • CMD command param1 param2(shell 格式)

其中 executable 代表可执行文件的路径,例如 node/bin/shparam1param2 代表参数。我们在后续讨论 Dockerfile 的高阶使用时会讨论 Entrypoint 的使用,这篇文章不会涉及

注意

在使用第一种 exec 格式时,必须使用双引号,因为整个命令将以 JSON 格式被解析。

提示

如果要执行变量替换等 Shell 操作,例如 echo $HOME,直接写成 ["echo", "$HOME"] 是无效的,需要改写成 ["sh", "-c", "echo $HOME"]

创建或运行容器时指定命令

在创建或运行容器时,通过添加命令参数可以覆盖构建镜像时指定的命令,例如:

docker run nginx echo hello
复制代码

通过指定 echo hello 命令参数,就会让这个容器输出一个 hello 然后退出,而不会运行默认的 nginx -g 'daemon off;'

当然,正如第一篇文章所实践的,我们还可以指定命令为 bash(或 shmongonode 等其他交互式程序),然后结合 -it 选项,就可以进入容器中交互式运行了。

通过 exec 运行新的命令

通过 docker exec,我们可以让已经运行中的容器执行新的命令。例如,对于我们之前的 dream-db 容器,我们通过 mongodump 命令来创建数据库备份:

docker exec dream-db mongodump
复制代码

然后可以进一步通过 docker exec -it 来进入 dream-db 中进行交互式运行,检查刚才导出的 dump 目录:

$ docker exec -it dream-db bash
root@c51d9355d8da:/# ls dump/
admin  todos
复制代码

同样地,按 Ctrl + D 退出就可以了。

提示

你也许会好奇,为什么在 docker run 交互式执行的时候按 Ctrl + D 就容器就直接停止了,而在 docker exec 的情况下退出却不会导致容器停止呢?因为 docker exec -it 相当于在现有的容器上运行了一个新的终端进程,而不会影响之前的主命令进程。只要主进程不结束,容器就不会停止。

小诀窍:如何轻松记住几十个 Docker 命令?

在刚才的实战中,我们也接触了很多新的 Docker 命令,怎么记住那么多命令呢?其实 docker 大部分命令都符合以下格式:

docker <对象类型> <操作名称> [其他选项和参数]
复制代码
  • 对象类型:到目前,我们接触的 Docker 对象类型包括容器 container镜像 image网络 network
  • 操作名称:操作可以分为两大类:1)适用于所有对象的操作,例如 lsrminspectprune 等等;2)对象专属操作,例如容器专有的 run 操作,镜像专有的 build 操作,以及网络专有的 connect 操作等等
  • 其他选项和参数:可通过 help 命令或 --help 查阅每个命令具体的选项和参数

由于部分命令很常用,Docker 还提供了方便的简写命令,例如显示当前所有容器 docker container ls,可以简写成 docker ps

我们首先复习一下容器(Container)对象上的命令吧(红色代表适用于所有对象的操作,蓝色代表此对象的专有操作):

再复习一下镜像(Image)对象上的命令:

最后复习一下网络(Network)对象上的命令:

至此,这篇教程也结束了。但是我们的筑梦之旅才刚刚开始——还有很多问题没有解决:1)现在前端应用还无法在除了本地以外的环境使用(因为访问的后端 API 是硬编码的 localhost);2)还没有真正部署到远程机器;3)MongoDB 还处于“裸奔”的状态(没设置密码)。不要方,我们在接下里的教程中就会去解决哦。

想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/52815
 
330 次点击