现在使用 OpenResty 作为反向代理,结合 Redis Cluster 进行服务发现,来动态管理 API 请求,是一种很常见的方案。我们工作中出于测试需要,希望快速搭建一套最小集群,期间出现过一个小问题,我想正好借此讨论一下 Docker 的网络架构。
场景描述
计划的部署方案如下:
- Redis 服务:通过独立的 docker-compose 配置启动,作为服务发现的存储后端
- OpenResty 服务:通过另一个 docker-compose 配置启动,作为反向代理,需要从 Redis 中读取上游服务信息。
两个服务都部署在一台 EC2,项目目录结构大致如下:
1 | project/ |
Redis 的 docker-compose.yml 示例:
1 | version: '3' |
OpenResty 的 docker-compose.yml 示例:
1 | version: '3' |
问题现象:127.0.0.1 无法访问 Redis
在 OpenResty 的 Lua 代码中,我们尝试连接 Redis:
1 | local redis = require "resty.redis" |
运行后,日志中出现连接失败的错误:
1 | failed to connect to redis: connection refused |
这让人感到困惑:明明 Redis 已经通过 -p 6379:6379 映射到了宿主机的 6379 端口,为什么在容器内访问 127.0.0.1:6379 却连接不上呢?
Docker 网络架构解析
Docker 的网络隔离原理
要理解这个问题,我们需要了解 Docker 的网络架构。Docker 使用 Linux namespace 技术实现网络隔离,每个容器都有自己独立的网络命名空间,包括:
- 独立的网络接口
- 独立的 IP 地址
- 独立的路由表
- 独立的端口空间
当一个容器启动时,Docker 会为其创建一个虚拟网络环境。在这个环境中:
127.0.0.1(localhost) 指向的是容器自身,而不是宿主机- 容器有自己的 IP 地址,通常在 Docker 创建的虚拟网桥(默认是
docker0)的子网中 - 端口映射(
-p 6379:6379)只是在宿主机上建立了一个转发规则
网络架构示意图
1 | ┌─────────────────────────────────────────────┐ |
为什么 127.0.0.1 不工作?
当 OpenResty 容器内的代码尝试连接 127.0.0.1:6379 时:
- 这个请求在 OpenResty 容器的网络命名空间内处理
127.0.0.1指向 OpenResty 容器自己,而不是宿主机- OpenResty 容器内并没有运行 Redis 服务
- 因此连接失败
虽然宿主机的 6379 端口映射到了 Redis 容器,但这个映射对于 OpenResty 容器来说是”看不见”的,因为它们在不同的网络命名空间中。
正确的连接方式
方案一:使用宿主机网关 IP
每个 Docker 网络都有一个网关 IP,通常是该网络的第一个 IP。对于默认的 bridge 网络,这个 IP 通常是 172.17.0.1。
在容器内,可以通过以下方式获取网关 IP:
1 | # 在容器内执行 |
或者查看 /etc/hosts:
1 | cat /etc/hosts |
修改 OpenResty 的连接代码:
1 | local redis = require "resty.redis" |
原理:网关 IP 是容器访问宿主机的桥梁。通过网关 IP,容器可以访问宿主机上暴露的端口,从而通过端口映射访问到 Redis 容器。
方案二:使用 host.docker.internal (推荐用于开发环境)
Docker Desktop 提供了一个特殊的 DNS 名称 host.docker.internal,它会自动解析为宿主机的 IP:
1 | local ok, err = red:connect("host.docker.internal", 6379) |
这种方式更加便携,但需要注意:
- Docker Desktop for Mac/Windows 默认支持
- Linux 上需要在启动容器时添加
--add-host=host.docker.internal:host-gateway
方案三:使用共享 Docker 网络(最佳实践)
最优雅的解决方案是让两个 docker-compose 项目使用同一个 Docker 网络:
首先创建一个外部网络:
1 | docker network create shared-network |
修改 Redis 的 docker-compose.yml:
1 | version: '3' |
修改 OpenResty 的 docker-compose.yml:
1 | version: '3' |
然后在 OpenResty 中直接使用服务名连接:
1 | -- 直接使用服务名,Docker DNS 会自动解析 |
Docker Compose 的服务名连接机制
为什么服务名可以直接使用?
当你在同一个 docker-compose.yml 文件中定义多个服务时:
1 | version: '3' |
在 OpenResty 容器中,你可以直接使用 redis 作为主机名连接 Redis。这是因为:
1. Docker 内置 DNS 服务器
Docker 为每个自定义网络(非默认 bridge 网络)提供了一个内置的 DNS 服务器。当容器启动时:
- 容器的
/etc/resolv.conf会被配置为使用 Docker 的 DNS 服务器(通常是127.0.0.11) - Docker DNS 会维护一个服务名到容器 IP 的映射表
查看容器内的 DNS 配置:
1 | # 在容器内执行 |
2. 服务发现机制
Docker Compose 会为 compose 文件中定义的每个服务:
- 创建一个 DNS 条目,名称就是服务名
- 将服务名解析为该服务对应容器的 IP 地址
- 如果服务有多个副本(scale),会采用轮询(round-robin)方式返回不同的 IP
示例:
1 | # 在 OpenResty 容器内 |
3. 网络别名 (Network Aliases)
Docker 还支持为服务设置网络别名:
1 | version: '3' |
这样,除了 redis 之外,redis-master 和 cache-server 也都可以解析到同一个容器。
4. 跨 Compose 项目的服务发现
默认情况下,不同 docker-compose 项目的服务无法通过服务名互相访问,因为它们在不同的网络中。这就是为什么我们需要:
- 使用共享的外部网络(方案三)
- 或者通过宿主机的端口映射(方案一、二)
使用共享网络后,Docker DNS 会在该网络范围内解析所有加入该网络的服务名。
最佳实践建议
开发环境
对于本地开发,推荐使用共享网络方案:
1 | # 创建共享网络 |
生产环境
生产环境中,建议:
- 使用 Docker Swarm 或 Kubernetes:它们提供了更强大的服务发现机制
- 使用服务网格(Service Mesh):如 Istio、Linkerd,提供更高级的流量管理
- 使用独立的服务发现组件:如 Consul、Etcd,不依赖于容器编排平台
配置管理
将 Redis 连接地址配置化:
1 | -- config.lua |
在 docker-compose.yml 中设置环境变量:
1 | services: |
总结
Docker 的网络隔离是通过 Linux namespace 实现的,每个容器都有独立的网络栈。关键要点:
- 容器内的
127.0.0.1指向容器自身,而不是宿主机 - 端口映射是宿主机层面的,容器之间无法直接通过 localhost 访问
- 容器间通信的正确方式:
- 使用容器 IP(不推荐,IP 可能变化)
- 使用宿主机网关 IP + 端口映射(适合跨 compose 项目)
- 使用共享 Docker 网络 + 服务名(最佳实践)
- Docker Compose 的服务名解析依赖于 Docker 内置的 DNS 服务器,自动将服务名映射到容器 IP
理解 Docker 的网络模型,不仅能解决容器间通信问题,也能帮助我们更好地设计微服务架构,希望这篇文章能帮你更好理解 Docker 的网络隔离。