ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Docker容器网络互连实战:从bridge网络到Compose服务互通

Docker容器网络互连实战:从bridge网络到Compose服务互通 说实话我刚开始用Docker那会儿最头疼的不是镜像怎么拉而是容器之间的网络问题。跑一个MySQL容器再跑一个业务应用容器业务死活连不上MySQL明明在宿主机上可以用localhost访问MySQL换成容器内部IP反而不通。后来翻了几天资料踩了一堆坑才算把Docker容器网络互连这块理清楚。这篇文章就围绕“Docker容器网络互连”这个主题把容器网络的底层逻辑、实操方法、常见场景和排错方式完整过一遍。适合刚开始学Docker、想搞懂容器之间通信原理以及用Docker Compose搭MySQL、Redis这类本地开发环境的朋友。不管你是用Docker Desktop还是在Linux服务器上裸装Docker这篇文章里的思路都能直接用。1. 容器网络互连先搞懂Docker网络的底层逻辑1.1 为什么容器之间不能“天然”互相访问很多新手第一次跑两个容器时都会有一个疑问容器不都是跑在同一台机器上吗为什么不能直接通过IP访问原因是Docker容器天生就是隔离的。每个容器启动时Docker都会为它创建一套独立的网络命名空间相当于给每个容器发了一个独立的“小房间”房间里只有容器自己能看到自己的网卡、IP、路由表。宿主机能看到所有房间的外门但房间和房间之间默认是隔开的。用生活里的例子类比每个容器像写字楼里的独立办公室每间办公室有自己的门牌号IP地址。两间办公室的人要沟通必须知道对方的门牌号而且还需要有内部通讯录把名字映射到门牌号。这里的“内部通讯录”就是Docker里的自定义网络和内置DNS。所以“容器网络互连”本质上不是把容器放到同一个IP段而是让多个容器能共享一套通信机制让它们能通过IP或者名字准确找到对方。1.2 Docker内置的几种网络驱动Docker安装完成后系统里会默认存在几个网络驱动。用docker network ls就能看到NETWORK ID NAME DRIVER SCOPE xxxxxxxxxxxx bridge bridge local xxxxxxxxxxxx host host local xxxxxxxxxxxx none null local这几个网络驱动分别对应不同的使用场景驱动适用场景通信范围主要特点bridge单机内容器通信最常用同网桥下的容器互通容器有独立IP支持端口映射隔离性较好host容器需要直接用宿主机网络栈与宿主机共享网络性能高但容器没有独立IP端口直接占用宿主机端口none容器不需要网络无完全隔离适合跑离线任务或安全敏感任务overlay跨主机的Swarm、K8s集群跨节点容器互通适合多机部署时使用单机场景用不到macvlan容器需要直接接入局域网与外部物理网络互通每个容器有局域网真实IP配置较复杂依赖物理交换机在本地开发和测试环境里90%的场景用的都是bridge网络。后面要讲的MySQL、Redis互连、Compose编排默认也都是基于bridge实现的。所以这篇文章的重心会放在bridge网络上。1.3 自定义bridge网络比默认bridge网络强在哪Docker默认的bridge网络叫docker0只要不主动指定网络运行的容器都会被塞进这个默认网络里。但默认bridge网络有一个特别容易踩的坑它不支持通过容器名直接解析IP。什么意思假设你启动了一个容器叫mysql-db又启动了一个容器叫app在默认bridge网络下app容器里执行ping mysql-db是解析不了域名的会直接报“Name or service not known”。你只能先查mysql-db容器的IP地址再用IP去访问。问题在于容器重启后IP地址是会变的。今天mysql-db的IP是172.17.0.2明天重启可能就变成172.17.0.5你写在应用配置里的IP就失效了。解决这个问题的方法就是手动创建一个自定义bridge网络docker network create my-net只要容器都挂到my-net这个自定义网络里Docker内置的DNS就会自动生效容器之间可以直接用容器名互相访问。自定义网络还支持动态加入和退出权限控制也更灵活真正做到了“同一个网络下的容器才互通不同网络默认隔离”。这也是我早期踩坑后总结的第一条经验不要图省事用默认bridge网络一定要用自定义网络或者直接用Compose默认创建的网络。2. 容器网络互连实操从端口映射到容器名互通2.1 端口映射不等于容器互连很多人会有个误区觉得给容器做了端口映射比如docker run -p 3306:3306 mysql然后另一个容器就能通过宿主机IP加3306端口访问MySQL了。这个想法不完全对。端口映射解决的是“外部访问容器”的问题把宿主机的某个端口流量转发到容器的某个端口。但容器之间的通信路径和外部完全不同同一网络下的两个容器数据包是直接通过网桥转发的根本不需要经过宿主机物理网卡更不需要“映射端口”这个概念。而且有个很现实的坑如果你在容器A里尝试通过宿主机的IP加映射端口去访问容器B很多场景下是通的但某些网络模式下会时通时不通因为Docker的iptables规则对来自不同来源的流量处理方式不一致。把“端口映射”当成“容器互连”来用迟早会翻车。所以我们要明确分工端口映射给宿主机外部、浏览器、客户端去访问容器用自定义网络给容器之间互相访问用。2.2 让两个容器通过容器名互相访问真正让两个容器互通只需要三步。第一步创建一个自定义网络docker network create my-net可以选择子网段如果你不指定Docker会自动分配一般不用管除非有IP冲突。第二步启动两个容器并都挂到my-net上docker run -d --name mysql-db --network my-net -e MYSQL_ROOT_PASSWORDroot123 mysql:8.0 docker run -d --name app --network my-net -e DB_HOSTmysql-db myapp第三步进入app容器里测试连通性docker exec -it app bash ping mysql-db这时你会发现ping mysql-db能直接解析出IP地址并且能通。这不只是ping通这么简单app容器里的JDBC连接串直接写jdbc:mysql://mysql-db:3306/test就能连上MySQL。IP换多少次都不怕因为容器名这个“DNS名字”是稳定的。这个机制的原理是当容器连接到用户自定义网络时Docker自带的嵌入式DNS服务会自动把容器名注册成域名。容器之间查询域名时Docker会先查这个内部DNS解析到对应容器的IP。2.3 动态加入网络与网络隔离有时候容器已经跑起来了才发现忘记加自定义网络。不需要重建容器用docker network connect命令动态加入就行docker network connect my-net app同理如果想把某个容器从网络里摘除可以执行docker network disconnect my-net app这个操作在排查网络问题时特别有用。我遇到过一种情况同一个项目里有两个子网一个给数据层用一个给接入层用只让上层服务连接应用服务不让它直接连数据库。设计思路是这样的网络挂载的容器设计意图front-netnginx, app接收外部流量调用应用服务back-netapp, mysql, redis应用服务访问数据库、缓存隔离效果nginx 无法直接访问 mysql避免攻击面扩散缩小故障半径app容器同时加入两个网络nginx只加front-netMySQL和Redis只加back-net这样即便nginx被别人打穿了也接触不到数据库。这种网络隔离设计在生产环境里非常常见理解了自定义网络的内置DNS之后操作起来也很简单。3. Docker Desktop与镜像源本地开发中的网络体验3.1 Docker Desktop的容器网络差异如果你用的是Docker Desktop而不是Linux上的原生Docker有些网络特性需要特别留意。Docker Desktop在macOS和Windows上并不是直接跑在宿主机里的它实际上是先启动了一个轻量级Linux虚拟机然后在虚拟机里跑Docker引擎。所有容器都在这台虚拟机里运行。因此端口映射是虚拟机负责完成的你在宿主机上用localhost访问容器服务一般没问题因为Docker Desktop会自动把宿主机端口转发到虚拟机里。但在容器内部访问宿主机服务时就有一个大坑容器里执行localhost访问到的是容器自己不是Mac/Windows主机。比如你宿主机上装了MySQL监听在3306你想让容器A去连这个MySQL直接在容器A里写localhost:3306是连不上的。Docker Desktop提供了一个特殊域名来访问宿主机host.docker.internal。docker run --rm alpine ping host.docker.internal这个域名在Docker Desktop环境下会自动解析到宿主机地址。所以在容器里要连宿主机上的服务时数据库连接串应该写成jdbc:mysql://host.docker.internal:3306/test这点非常建议刚接触Docker Desktop的朋友提前记住能省下大量排查时间。另外在严格的生产Linux环境里没有这个域名容器要访问宿主机服务一般用宿主机在局域网里的真实IP。所以写代码时最好把数据库地址做成配置项不要写死在代码里。3.2 镜像下载慢与镜像仓库配置实操从“docker镜像下载慢”这个高频问题说起。默认情况下Docker会从Docker Hub拉取镜像服务器在国外没有加速的情况下拉一个几百MB的镜像能等到怀疑人生。解决办法是配置镜像加速器也就是把拉取镜像的源切换得近一些。Docker Desktop的配置方法比较直观打开Settings - Docker Engine在JSON配置里加入registry-mirrors字段然后点击Apply Restart。{ registry-mirrors: [ https://你的加速地址.mirror.aliyuncs.com ] }Linux原生命令行安装的Docker配置路径在/etc/docker/daemon.json没有这个文件就新建一个修改后执行systemctl reload docker。需要注意几点镜像加速只对Docker Hub的公共镜像有效对某些特定第三方镜像仓库不一定生效改完配置后之前已经拉取过的镜像层会走本地缓存只有新镜像才会感受到加速效果如果你的公司或团队有自建的镜像仓库比如Harbor那在docker pull之前需要先配置insecure-registries或者完整的仓库地址然后重新打tag再推送这其实也是一个网络信任域的问题和“容器网络互连”同样属于Docker网络体系的一部分。我个人的习惯是日常开发用Docker Desktop 镜像加速公司级部署用自建镜像仓库本地拉不到私有镜像时先去检查是不是仓库地址写错了、证书没加、或者防火墙没放行端口而不是第一时间怀疑Docker本身有问题。4. 真实场景用Docker Compose搭建MySQL 8.0与Redis主从的网络配置4.1 Compose默认网络与服务名互访当项目里容器数量开始变多再一个个手动docker run和docker network connect就太累了这时候用Docker Compose来编排是最省心的。热词里反复提到的“docker compose”其实天然自带一套网络管理能力。启动一个Compose项目时Compose会自动创建一个网络命名规则通常是“项目名_default”。这个网络里所有服务默认都能互通而且服务名就是域名可以直接互相访问。随便举个例子一个项目里同时起了MySQL和Redis其他服务直接通过服务名连它们就行version: 3.8 services: mysql: image: mysql:8.0 container_name: mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: test ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql networks: - app-net redis-master: image: redis:7 container_name: redis-master command: [redis-server, --appendonly, yes] ports: - 6379:6379 networks: - app-net redis-slave: image: redis:7 container_name: redis-slave command: [redis-server, --slaveof, redis-master, 6379] depends_on: - redis-master networks: - app-net networks: app-net:这个文件里三个服务都挂在同一个app-net网络下。应用容器里连接MySQL直接写mysql:3306连Redis主节点直接写redis-master:6379。Compose会自动处理DNS解析不需要手动去查IP。注意我在redis-slave的命令里用了--slaveof redis-master 6379这里必须保证从节点能解析到redis-master这个容器名所以两个Redis容器必须放在同一个网络里。这正是容器网络互连在生产场景里的直接应用。4.2 MySQL 8.0容器常见网络相关坑用Compose跑MySQL 8.0时有几个和网络高度相关的问题值得提前说。第一个是权限问题。容器能ping通MySQL了但连接时报Host xxx is not allowed to connect to this MySQL server这个错误很容易让人误以为是Docker网络没配好实际是MySQL自身授权问题。默认的root用户只允许从localhost连接容器里访问时IP是网桥分配的IP不在授权范围内。解决方法是进入MySQL容器执行授权SQLCREATE USER test% IDENTIFIED BY test123; GRANT ALL PRIVILEGES ON *.* TO test%; FLUSH PRIVILEGES;第二个是端口映射冲突。本地跑多个MySQL实例时宿主机3306端口被占用会导致启动失败。这时要么换一个宿主机端口映射比如3307:3306要么直接不给数据层容器做端口映射只让同一Compose网络里的应用容器访问它。第三个是数据卷的问题。容器删除重建后如果MySQL数据目录没有挂载到宿主机数据会丢失。数据丢了之后新容器和旧容器网络配置如果不一样应用还是连不上这会让人误判为“网络问题”实际上数据卷问题。网络互连只解决通不通数据卷解决数据在不在这两个问题经常同时出现排错时要分开看。4.3 Redis主从复制中的网络要点Redis主从复制比MySQL更依赖容器网络互连。因为从节点启动时必须能够主动连接到主节点如果网络不同主从复制就会一直失败。回到上面的Compose文件redis-slave容器启动后会执行redis-server --slaveof redis-master 6379它会向redis-master这个域名发起连接。因为两个服务在同一个app-net网络里内置DNS能解析到主节点的容器IP所以复制链路能正常建立。如果你不是用Compose而是手动启动两个Redis容器最常见的错误是两个容器分别处于不同网络。比如一个用默认bridge一个用redis-net这时即使配置了replicaof 172.17.0.2 6379容器重启后IP一变复制就断了。正确做法是启动时就都加入同一个自定义网络。还有一个更容易踩的坑主节点只做了宿主机端口映射比如16379:6379从节点配置replicaof 宿主机IP 16379。这种方案在Linux服务器上偶尔能通因为从容器访问宿主机IP可能绕到了物理网卡通过iptables转发回去但在Docker Desktop的Mac/Windows环境下从容器里访问宿主机IP经常不通反而更推荐用host.docker.internal去连宿主机的映射端口。条件允许的话还是“同网络、服务名互访”最稳。5. 常见问题排查与避坑心得5.1 容器网络排查命令速查遇到容器间连不上时我一般按下面这个顺序排查基本能定位90%的问题# 查看所有网络 docker network ls # 查看某个网络里挂了哪些容器 docker network inspect my-net # 查看容器实际IP和网络配置 docker inspect mysql-db # 查看端口映射情况 docker port mysql-db # 进入容器内部测试连通性 docker exec -it app bash ping mysql-db容器内的镜像如果是最小化的Alpine默认没有ping可以临时用docker run一个带网络工具的镜像来测试docker run --rm --network my-net alpine ping mysql-db如果业务端口测试可以用nc或者curl再不行就进容器里装一个docker exec -it app bash apt-get update apt-get install -y iputils-ping netcat-openbsd下面这张速查表是我在实际排错过程中整理的覆盖面很广问题现象可能原因排查命令解决思路ping容器名解析不了容器不在同一个自定义网络docker network inspect my-net把容器加入同一网络能ping通但服务连不上应用监听地址不对或端口不对docker logs 容器名检查服务端监听0.0.0.0还是127.0.0.1MySQL报权限错误MySQL用户授权host不对进入容器查看user表授权给%或对应IP段Redis主从一直连不上slave网络无法解析masterredis-cli -h redis-master ping确保主从在同一网络用服务名配置容器重启后应用断连应用配置里写死了容器IPdocker inspect 查看IP变化改用容器名/服务名访问宿主机映射端口访问不了防火墙或安全组拦截netstat -tlnp / firewall-cmd放行宿主机对应端口Docker Desktop下容器访问不了宿主机服务容器内localhost指向容器自身容器内执行hostname使用host.docker.internal访问宿主机5.2 三个特别容易忽略的细节第一个是容器名冲突。自定义网络的DNS解析依赖容器名所以同一个网络下所有容器名必须唯一。如果你先启动了一个redis容器再启动第二个也叫redis的容器后者会直接创建失败。解决办法是给不同环境加后缀比如redis-master、redis-slave。第二个是docker network prune的误操作。这个命令会清除所有没被容器使用的网络如果你手工创建了一堆网络但暂时没有容器挂在上面执行一次prune可能就把你的网络规划全干掉了。下次启动容器时才发现网络不存在容易误判为“Docker坏了”。我的建议是网络名称写清楚用途不要把临时网络留着不用确需清理前先docker network ls确认一下。第三个是端口映射的“假通”现象。在Docker Desktop里宿主机访问映射端口往往很顺畅但另一个容器通过宿主机IP加映射端口去访问行为并不稳定。本质原因是Docker Desktop的网络栈多了一层虚拟机转发iptables规则复杂容易触发回环问题。所以再次强调容器之间互访不要依赖宿主机端口映射直接用自定义网络和服务名。5.3 我的网络规划建议回到文章开头的问题为什么我一开始配了好多端口映射容器之间还是不通归根结底是因为我没有一开始就规划好网络。现在我的做法非常固定大家可以参考。第一每个独立业务域单独建一个自定义网络。比如数据层一个网络、应用层一个网络、接入层一个网络服务需要跨层访问时容器可以同时加入多个网络但不会出现一个超大网络把无关容器全部连在一起的情况。第二内部服务优先不做端口映射。同一网络里的容器通过服务名互访不需要暴露宿主机端口。对外才暴露必要的端口比如HTTP的80/8080数据库只在调试需要时才临时映射到宿主机端口。第三尽量用Docker Compose统一管理。Compose自动创建网络、自动做DNS解析比手动敲命令更不容易出错。多个Compose项目如果确实需要互访可以用external网络把它们拉进同一个网络空间里这个操作在稍大一点的项目中几乎必用。第四网络变化后及时更新配置。很多容器连不上的问题根源在于容器重启后IP变了但应用配置里还写旧IP。不要在应用代码里依赖容器IP要用服务名、容器名或者在K8s环境里用Service名称。只要这个习惯养成了容器怎么重启都不用担心。这套“先规划、再启动”的网络思路我在本地开发和测试环境里用了很久最大的感受是把容器网络这层理解透了Docker的很多“玄学”问题都会变成明确的命令排查和配置调整不再需要靠重启大法碰运气。最后再分享一个小技巧遇到容器间连不上先别急着改端口映射第一步先运行docker network inspect看看它们到底是不是在同一个网络里。大多数时候答案就藏在那一长串Containers列表里看一眼你就知道有没有把容器放对地方了。
返回列表