06|打破次元壁:容器该如何与外界互联互通

你好,我是Chrono。

在前面的几节课里,我们已经学习了容器、镜像、镜像仓库的概念和用法,也知道了应该如何创建镜像,再以容器的形式启动应用。

不过,用容器来运行“busybox”“hello world”这样比较简单的应用还好,如果是Nginx、Redis、MySQL这样的后台服务应用,因为它们运行在容器的“沙盒”里,完全与外界隔离,无法对外提供服务,也就失去了价值。这个时候,容器的隔离环境反而成为了一种负面特性。

所以,容器的这个“小板房”不应该是一个完全密闭的铁屋子,而是应该给它开几扇门窗,让应用在“足不出户”的情况下,也能够与外界交换数据、互通有无,这样“有限的隔离”才是我们真正所需要的运行环境。

那么今天,我就以Docker为例,来讲讲有哪些手段能够在容器与外部系统之间沟通交流。

如何拷贝容器内的数据

我们首先来看看Docker提供的 cp 命令,它可以在宿主机和容器之间拷贝文件,是最基本的一种数据交换功能。

试验这个命令需要先用 docker run 启动一个容器,就用Redis吧:

docker run -d --rm redis

图片

注意这里使用了 -d--rm 两个参数,表示运行在后台,容器结束后自动删除,然后使用 docker ps 命令可以看到Redis容器正在运行,容器ID是“062”。

docker cp 的用法很简单,很类似Linux的“cp”“scp”,指定源路径(src path)和目标路径(dest path)就可以了。如果源路径是宿主机那么就是把文件拷贝进容器,如果源路径是容器那么就是把文件拷贝出容器,注意需要用容器名或者容器ID来指明是哪个容器的路径。

假设当前目录下有一个“a.txt”的文件,现在我们要把它拷贝进Redis容器的“/tmp”目录,如果使用容器ID,命令就会是这样:

docker cp a.txt 062:/tmp

接下来我们可以使用 docker exec 命令,进入容器看看文件是否已经正确拷贝了:

docker exec -it 062 sh

图片

可以看到,在“/tmp”目录下,确实已经有了一个“a.txt”。

现在让我们再来试验一下从容器拷贝出文件,只需要把 docker cp 后面的两个路径调换一下位置:

docker cp 062:/tmp/a.txt ./b.txt

这样,在宿主机的当前目录里,就会多出一个新的“b.txt”,也就是从容器里拿到的文件。

如何共享主机上的文件

docker cp 的用法模仿了操作系统的拷贝命令,偶尔一两次的文件共享还可以应付,如果容器运行时经常有文件来往互通,这样反复地拷来拷去就显得很麻烦,也很容易出错。

你也许会联想到虚拟机有一种“共享目录”的功能。它可以在宿主机上开一个目录,然后把这个目录“挂载”进虚拟机,这样就实现了两者共享同一个目录,一边对目录里文件的操作另一边立刻就能看到,没有了数据拷贝,效率自然也会高很多。

沿用这个思路,容器也提供了这样的共享宿主机目录的功能,效果也和虚拟机几乎一样,用起来很方便,只需要在 docker run 命令启动容器的时候使用 -v 参数就行,具体的格式是“宿主机路径:容器内路径”。

我还是以Redis为例,启动容器,使用 -v 参数把本机的“/tmp”目录挂载到容器里的“/tmp”目录,也就是说让容器共享宿主机的“/tmp”目录:

docker run -d --rm -v /tmp:/tmp redis

然后我们再用 docker exec 进入容器,查看一下容器内的“/tmp”目录,应该就可以看到文件与宿主机是完全一致的。

docker exec -it b5a sh    # b5a是容器ID

你也可以在容器里的“/tmp”目录下随便做一些操作,比如删除文件、建立新目录等等,再回头观察一下宿主机,会发现修改会即时同步,这就表明容器和宿主机确实已经共享了这个目录。

-v 参数挂载宿主机目录的这个功能,对于我们日常开发测试工作来说非常有用,我们可以在不变动本机环境的前提下,使用镜像安装任意的应用,然后直接以容器来运行我们本地的源码、脚本,非常方便。

这里我举一个简单的例子。比如我本机上只有Python 2.7,但我想用Python 3开发,如果同时安装Python 2和Python 3很容易就会把系统搞乱,所以我就可以这么做:

  1. 先使用 docker pull 拉取一个Python 3的镜像,因为它打包了完整的运行环境,运行时有隔离,所以不会对现有系统的Python 2.7产生任何影响。
  2. 在本地的某个目录编写Python代码,然后用 -v 参数让容器共享这个目录。
  3. 现在就可以在容器里以Python 3来安装各种包,再运行脚本做开发了。
docker pull python:alpine
docker run -it --rm -v `pwd`:/tmp python:alpine sh

显然,这种方式比把文件打包到镜像或者 docker cp 会更加灵活,非常适合有频繁修改的开发测试工作。

如何实现网络互通

现在我们使用 docker cpdocker run -v 可以解决容器与外界的文件互通问题,但对于Nginx、Redis这些服务器来说,网络互通才是更要紧的问题。

网络互通的关键在于“打通”容器内外的网络,而处理网络通信无疑是计算机系统里最棘手的工作之一,有许许多多的名词、协议、工具,在这里我也没有办法一下子就把它都完全说清楚,所以只能从“宏观”层面讲个大概,帮助你快速理解。

Docker提供了三种网络模式,分别是nullhostbridge

null是最简单的模式,也就是没有网络,但允许其他的网络插件来自定义网络连接,这里就不多做介绍了。

host的意思是直接使用宿主机网络,相当于去掉了容器的网络隔离(其他隔离依然保留),所有的容器会共享宿主机的IP地址和网卡。这种模式没有中间层,自然通信效率高,但缺少了隔离,运行太多的容器也容易导致端口冲突。

host模式需要在 docker run 时使用 --net=host 参数,下面我就用这个参数启动Nginx:

docker run -d --rm --net=host nginx:alpine

为了验证效果,我们可以在本机和容器里分别执行 ip addr 命令,查看网卡信息:

ip addr                    # 本机查看网卡
docker exec xxx ip addr    # 容器查看网卡

图片

图片

可以看到这两个 ip addr 命令的输出信息是完全一样的,比如都是一个网卡ens160,IP地址是“192.168.10.208”,这就证明Nginx容器确实与本机共享了网络栈。

第三种bridge,也就是桥接模式,它有点类似现实世界里的交换机、路由器,只不过是由软件虚拟出来的,容器和宿主机再通过虚拟网卡接入这个网桥(图中的docker0),那么它们之间也就可以正常的收发网络数据包了。不过和host模式相比,bridge模式多了虚拟网桥和网卡,通信效率会低一些。

图片

和host模式一样,我们也可以用 --net=bridge 来启用桥接模式,但其实并没有这个必要,因为Docker默认的网络模式就是bridge,所以一般不需要显式指定。

下面我们启动两个容器Nginx和Redis,就像刚才说的,没有特殊指定就会使用bridge模式:

docker run -d --rm nginx:alpine    # 默认使用桥接模式
docker run -d --rm redis           # 默认使用桥接模式

然后我们还是在本机和容器里执行 ip addr 命令(Redis容器里没有ip命令,所以只能在Nginx容器里执行):

图片

对比一下刚才host模式的输出,就可以发现容器里的网卡设置与宿主机完全不同,eth0是一个虚拟网卡,IP地址是B类私有地址“172.17.0.2”。

我们还可以用 docker inspect 直接查看容器的ip地址:

docker inspect xxx |grep IPAddress

图片

这显示出两个容器的IP地址分别是“172.17.0.2”和“172.17.0.3”,而宿主机的IP地址则是“172.17.0.1”,所以它们都在“172.17.0.0/16”这个Docker的默认网段,彼此之间就能够使用IP地址来实现网络通信了。

如何分配服务端口号

使用host模式或者bridge模式,我们的容器就有了IP地址,建立了与外部世界的网络连接,接下来要解决的就是网络服务的端口号问题。

你一定知道,服务器应用都必须要有端口号才能对外提供服务,比如HTTP协议用80、HTTPS用443、Redis是6379、MySQL是3306。第4讲我们在学习编写Dockerfile的时候也看到过,可以用 EXPOSE 指令声明容器对外的端口号。

一台主机上的端口号数量是有限的,而且多个服务之间还不能够冲突,但我们打包镜像应用的时候通常都使用的是默认端口,容器实际运行起来就很容易因为端口号被占用而无法启动。

解决这个问题的方法就是加入一个“中间层”,由容器环境例如Docker来统一管理分配端口号,在本机端口和容器端口之间做一个“映射”操作,容器内部还是用自己的端口号,但外界看到的却是另外一个端口号,这样就很好地避免了冲突。

端口号映射需要使用bridge模式,并且在 docker run 启动容器时使用 -p 参数,形式和共享目录的 -v 参数很类似,用 : 分隔本机端口和容器端口。比如,如果要启动两个Nginx容器,分别跑在80和8080端口上:

docker run -d -p 80:80 --rm nginx:alpine
docker run -d -p 8080:80 --rm nginx:alpine

这样就把本机的80和8080端口分别“映射”到了两个容器里的80端口,不会发生冲突,我们可以用curl再验证一下:

图片

使用 docker ps 命令能够在“PORTS”栏里更直观地看到端口的映射情况:

图片

小结

好了,今天我们一起学习了容器与外部系统之间沟通交流的几种方法。

你会发现,这些方法几乎消除了容器化的应用和本地应用因为隔离特性而产生的差异,而因为镜像独特的打包机制,容器技术显然能够比apt/yum更方便地安装各种应用,绝不会“污染”已有的系统。

今天的课里我列举了Python、Nginx等例子,你还可以举一反三,借鉴它们把本地配置文件加载到容器里适当的位置,再映射端口号,把Redis、MySQL、Node.js都运行起来,让容器成为我们工作中的得力助手。

照例简单小结一下这次的要点:

  1. docker cp 命令可以在容器和主机之间互相拷贝文件,适合简单的数据交换。
  2. docker run -v 命令可以让容器和主机共享本地目录,免去了拷贝操作,提升工作效率。
  3. host网络模式让容器与主机共享网络栈,效率高但容易导致端口冲突。
  4. bridge网络模式实现了一个虚拟网桥,容器和主机都在一个私有网段内互联互通。
  5. docker run -p 命令可以把主机的端口号映射到容器的内部端口号,解决了潜在的端口冲突问题。

课下作业

最后是课下作业时间,给你留两个思考题:

  1. 你能说出今天学的 docker cp 命令和第4讲Dockerfile里的COPY指令有什么区别吗?
  2. 你觉得host模式和bridge模式各有什么优缺点,在什么场景下应用最合适?

欢迎积极留言讨论,我会第一时间给你回复,如果有收获也欢迎你转发给身边的朋友一起学习。

下节课是实战演练,下节课见。

精选留言

  • xmr

    2022-07-05 09:51:09

    1.第四节的COPY是在构建镜像时把本地文件拷贝到镜像,而本节的cp命令是在容器运行后容器和宿主机互相拷贝文件,一个构建时,一个运行时。
    2.host性能好,隔离性差;bridge隔离性好,性能差了一点。host一般用在集群的边界需要和集群外通信的场景,比如ingress-nginx;bridge用于集群内部,如无特殊需求默认都是bridge。
    作者回复

    awesome

    2022-07-05 10:08:35

  • 马以

    2022-07-04 13:03:52

    1:第四节的copy命令是在容器启动过程中的COPY命令,该命令应该是在声明了“namespace”之后,所以这个时候进程看到的世界是一个隔离的环境;
    而这里的COPY更像是站在“上帝视角(宿主机操作系统层面)”进行拷贝,所以这里不受“namespace”的约束;
    2:host就是简单粗暴效率高,适合小规模集群的简单拓扑结构;bridge适合大规模集群,有了bridge就有更多的可操作空间,比如XLAN和VXLAN这些,它可以提供更多的可定制化服务,比如流量控制、灰度策略这些,从而像flannel和Calico这些组件才有了更多的发挥余地。
    作者回复

    说的很好。

    COPY指令会把文件拷贝进镜像,“永远存在”,而cp命令是暂时的。

    2022-07-04 15:27:19

  • 🙄

    2022-07-04 20:55:36

    COPY会生成新的layer,对于镜像是永久的。copy是在当前layer执行一个命令,不生成新的layer
    作者回复

    great

    2022-07-05 12:20:49

  • SuperSnow

    2022-07-04 07:53:42

    docker run -d --rm redis
    --rm不是删除命令么?为什么要在这里用呢?
    作者回复

    --rm是告诉docker容器运行完毕后直接删除,就是为了省事,不用再手动删除了。

    2022-07-04 08:56:08

  • peter

    2022-07-04 14:21:12

    请教老师几个问题啊:
    Q1:docker exec XXX ip addr
    容器查询IP的命令中,“XXX”是任意的吗?
    Q2:“Redis 容器里没有 ip 命令”,为什么?
    如果Redis 容器里不能使用 ip 命令,那么,可以用“docker inspect”来查看其ip地址吗?
    Q3:“宿主机的 IP 地址则是“172.17.0.1””,哪里显示了宿主机的IP地址?图上并没有显示啊。
    Q4:“docker inspect adb”查出的不仅仅是redis和nginx的IP吧,是查出所有容器的IP吗?
    必须用adb吗? 用其他命令不行吗?
    Q5: “curl 127.1:8080 -I”,这个命令中“127.1”是“127.0.0.1”的缩写吗?
    作者回复


    1.XXX就是容器id,或者是容器名。

    2.Redis镜像里没有安装ip命令,当然就无法使用了。“docker inspect”是docker的功能,与容器和镜像都没有关系。

    3.截图里没有,这个IP地址是docker预留的。

    4.“docker inspect”后面要接容器id或者名字,“adb”就是当时的容器id

    5.是的,我就是图省事。

    2022-07-04 15:29:41

  • 逗逼师父

    2022-07-04 21:53:40

    1. docker cp是操作宿主和容器之间的文件传输,Dockfile里的COPY命令是在制作镜像时使用的。
    2. host效率高,bridge更加灵活;host在平时开发时比较适合,因为我们的电脑一般端口不容易冲突;bridge的话更适合在测试或者生产环境中去使用,可以灵活配置应用端口。
    作者回复

    awesome

    2022-07-05 12:21:01

  • 凉人。

    2022-07-05 10:06:21

    hi,想问个问题,如果我们用网桥模式,任然是外部一个端口映射容器内一个端口,这有什么优势吗
    作者回复

    当然有优势,每个容器可以自己用任意端口,不用担心冲突,开发变简单了,维护的成本也可以减少。

    2022-07-05 12:22:57

  • 美妙的代码

    2022-07-04 15:40:04

    老师,怎么修改容器的网络模式,host/bridge 一般情况下都没有设置它,直接运行容器,怎么知道容器运行在什么模式下?
    作者回复

    默认是bridge模式,可以用ip addr命令,看容器里的网络情况,有veth就是bridge,直接用宿主机网卡就是host。

    2022-07-04 16:21:41

  • 六月

    2022-07-06 00:07:57

    有一个问题想问一下,docker挂载目录进容器,宿主机的文件会覆盖掉容器的同名目录,但是,有时候想反过来,在挂载的目录里输出容器内原本同名目录下的文件,而不是被宿主机挂载的空目录覆盖掉。这个能实现么
    作者回复

    好像是不能吧,可以去看一下docker的官网文档。

    2022-07-06 09:42:03

  • 朱雯

    2022-07-04 23:08:11

    问题1: cp命令是容器运行时拷贝进去 有点类似拷贝进入内存 copy是永久拷贝进镜像 等于是放入磁盘
    问题2:实践中 一直使用bridge模式 为了隔离而花费那么一点效率我觉得完全值得。
    最后 我一直以为只有bridge模式 当时面试官问我 docker容器的运行模式有哪些 我直接告诉他没什么网络模式 我就这么用的 现在才彻底搞明白😄
    作者回复

    其实docker的模式我们简单了解一下就好,因为后面主要学的是Kubernetes。

    2022-07-05 12:21:42

  • 2022-07-04 22:15:58

    mac 下的 网络 是有问题的,不能和宿主进行通信
    作者回复

    哪个环境?我用VMWare是没有问题的。可以一起讨论解决。

    2022-07-05 10:07:22

  • ゝ骑着小车去兜风。

    2022-07-12 10:46:11

    老师你好,请教个问题。bridge模式下的容器ip可以理解成一个虚拟ip吧,这个虚拟ip在容器中可以理解为容器自己的ip,而在宿主机上可以直接与容器ip ping通,它们之家的通信是由虚拟网卡进行通信。如果当前这个容器的程序访问baidu的话,baidu服务端接收到的请求方ip应该还是容器对应的宿主机ip吧?
    作者回复

    是的,容器的IP地址是私有地址,要出去肯定要经过宿主机的网卡,也就是用宿主机的IP地址。

    2022-07-12 13:24:25

  • lesserror

    2022-07-05 21:32:19

    老师,有几个小问题:

    Q1:使用-v挂载目录的时候,宿主机路径和容器内路径有什么讲究么? 我的理解是这两个路径没有的话,系统都会帮我们自动去创建吗?并做对应关系?

    Q2: 图中的「vethxxx」代表的是什么呢?
    作者回复


    1.挂载目录可以理解成是目录映射,没什么特别的,不要想的太复杂。docker会自动创建目录。

    2.vethxxx就是虚拟网卡,在容器里用ip addr可以查看。

    2022-07-06 09:44:03

  • zz

    2022-07-04 17:34:28

    问题1:
    COPY命令的路径必须是”构建上下文路径“里的,不能随意指定文件。要从本地向镜像拷贝文件,就必须把这些文件放到一个专门的目录;
    cp命令,可以在宿主机和容器之间拷贝文件;
    问题2:
    host模式,网络没有与宿主机隔离,优点:效率高,缺点:容器内部使用的端口就是主机的端口,容易造成宿主机的端口冲突;
    bridge模式:容器和足主机都在一耳光私有网段内互通互联,优点:容器端口和主机端口相互独立,易于端口的管理;缺点:bridge模式多了虚拟网桥和网卡,通信效率会低一些。
    作者回复

    great

    2022-07-04 18:45:22

  • 出云

    2022-07-04 11:10:14

    docker cp 命令 的操作对象是容器, Dockerfile 中 COPY指令的操作对象是容器镜像。
    作者回复

    good

    2022-07-04 15:23:52

  • 拾掇拾掇

    2022-07-04 10:37:55

    给运行的容器挂载目录,很复杂;建议大家运行容器前,挂载下目录
    作者回复

    good

    2022-07-04 15:29:46

  • 程帝洲

    2022-10-16 00:50:12

    老师,你一般用什么方式追加端口的。
    作者回复

    首先要开发应用的时候定义好端口,然后在Dockerfile里expose暴露端口,最后在运行时映射端口。

    2022-10-16 18:03:23

  • nc_ops

    2022-09-16 11:12:49

    关于bridge模式有点疑问,“宿主机和容器在一个丝网网段内互联互通”,那如果宿主机的私网地址不在docker的默认网段,比如宿主机私网地址是192.168.0.1/24,那bridge模式下创建出的容器的地址是和宿主机在同一网段,还是在docker的默认网段呢?
    作者回复

    宿主机会由一个虚拟网卡接入docker的私有网段,然后docker容器都在这个网段里。

    2022-09-16 13:57:19

  • pyhhou

    2022-07-10 01:57:05

    思考题

    1. 首先是场景不同,docker cp 的是针对已经构建好的容器,而 Dockerfile 中的 COPY 则是用在容器的构建中。其次是对象不同,docker cp 两边的对象分别是宿主机目录以及容器中的目录,而 Dockerfile 中的 COPY 对象其实只有一个,就是容器,或者说是构建容器时的上下文环境。另外,功能也稍有区别,docker cp 如果发现指定的目录不存在就会报错,而 Dockerfile 中的 COPY 则会自动创建

    2. host 运用在宿主机和容器的应用少且不会频繁的动态挂载新的应用的场景下,而 bridge 可以更好地将宿主机和容器隔离开来,这样可以不受约束地在不同的容器中运行相同或者不同应用。个人觉得 bridge 模式受众面更广,毕竟创建容器的初衷就是为了隔离宿主机的环境,而网络又是环境中极其重要的一环,所以 bridge 对于容器是非常有必要的,可能这也是为什么 bridge 被设定为默认选项
    作者回复

    amazing!

    2022-07-10 12:04:31

  • 拾掇拾掇

    2022-07-04 09:51:20

    1.我觉得两者差不多,都是把本机文件或目录复制到容器内,只不过dockerfile里的copy指令需要"构建上下文"
    2. 1).host:与本机共享网络栈,ip地址都相同,最大的好处就是性能,如果对网络传输效率要求比较高,可以选择host;最大的弊端就是不够灵活,端口过多可能会容易与本机端口冲突
    2).bridge:类似交换机、路由器当网络中转,性能没host好,通信效率也低,但是不容易端口冲突
    作者回复

    great

    2022-07-04 15:25:46