Hacking Limbo

Reading / Coding / Hacking

Docker 服务发现机制的简单实现

如果不想折腾 Kubernetes 或 Docker Swarm, 又需要一个能快速搭建并投入使用的 Docker 服务发现机制,这篇文章可能可以给你一点灵感。

Docker 容器的服务发现涉及到两个问题:

  1. DNS 注册 - 将 service name 映射到容器 IP, 并随容器的启动 / 停止实时更新。
  2. IP 路由 - 保证容器 IP 能被所有机器访问到。

以下是我解决这两个问题的思路。

DNS 注册

这里要用到两个开源组件,RegistratorCoreDNS. 前者负责监听 Docker 容器的启动和停止,根据容器的名字或 label 生成 DNS 信息,以 SkyDNS 2 的格式写入 etcd; 后者从 etcd 读取 DNS 信息以提供 service name 的解析。

具体配置是:

  1. 搭建 etcd - 照着 https://play.etcd.io 建议的参数来启动即可。
  2. 在每台机器上启动一个 Registrator 实例,监听本机的 Docker 容器事件。关键参数:registrator -cleanup -internal=true -explicit=true skydns2://HOST_IP:2379/cluster.local.
  3. 在每台机器上启动一个 CoreDNS 实例,数据源设为 etcd 的 /skydns, 响应本机的 DNS 解析请求。
  4. 修改 Docker daemon 的 DNS server 为 127.0.0.1. UPDATED: 错的,应该写宿主机 IP.

遇到的一些问题:

  • Registrator 生成的名字不太符合 DNS 规范,可以在容器启动时指定 SERVICE_ID 覆盖掉默认值。另外这个项目看起来已经很久没有更新了,用的还是 etcd v2 API.

  • CoreDNS 解析域名时会返回当前「目录」的所有记录,比如 redis.qcloud.local 的解析包含 redis-1redis-2 两个实例的 IP.

  • 覆盖 Docker daemon 的 DNS server 后,CoreDNS 容器也会受影响(虽然指定了 --network=host),导致后者的 /etc/resolv.conf 里写的也是 127.0.0.1, 所有 DNS 查询变成无限递归。

  • Docker daemon 的 DNS server 不能写 127.0.0.1, 因为 CoreDNS 监听的是宿主机的 IP.

IP 路由

Docker 容器与宿主机使用的是不同的 IP 网段,比如宿主机的 CIDR 是 172.30.0.0/16, 而容器网络用的是 10.32.0.0/16. 默认情况下宿主机所在子网不太可能给容器 IP 网段配置路由,意味着我们只能自行解决路由问题。

如果宿主机是 Vultr / DigitalOcean / Linode 的 VPS, 在开启 Private Network 功能之后,直接给每台机器添加类似 10.32.0.0/16 via 172.30.x.x dev ethX 的路由即可。

而类似 AWS / GCP / 腾讯云这类比较「正规」的云服务,就稍微麻烦一点,因为他们的虚拟机网卡通常都开启了 IP 包的来源 / 目标地址检查1,所有不属于 VPC 子网网段的 IP 包都没法转发。简单粗暴的解决方法是在 VPC 路由表里添加容器 IP CIDR 的路由规则。

有精力折腾的话也可以试试 Calico 或 Flannel 的 IP-in-IP 方案。

对外暴露服务

解决上述两个问题,只能让不同机器间的 Docker 容器互相访问——如果想将服务暴露出去,比如提供公网的 HTTP 服务,还需要一些反向代理的配置。以 Nginx (OpenResty) 为例,如果所有服务都绑定在 *.example.com 这个公网域名上,可以将子域名映射为集群内的 service name, 再 proxy pass 过去。

具体实现:

  1. 后端服务 abc 的容器指定环境变量 SERVICE_8000_ID=web-0, 这里 8000 是 abc 服务暴露的端口。
  2. Registrator 生成对应的 DNS SRV 记录 web-0.abc.cluster.local.
  3. 在 Nginx server 里用 server_name ~(?P<subdomain>[^.]+).example.com; 抽取子域名,比如访问 abc.example.comsubdomain 就是 abc.
  4. access_by_lua 作用域里用 ngx.var.subdomain .. '.cluster.local' 拼接出 service name.
  5. resty.dns.resolver 解析 service name, 从 A 记录拿到 upstream IP, 从名为 web-[0-9]+ 的 SRV 记录拿到端口号,记录到 ngx.ctx
  6. Nginx upstream 用 balancer_by_lua_blockngx.ctx 里取出 IP 和端口号,作为 proxy_pass 的目标地址。

完整的 Nginx 配置和 Lua 代码见这个 Gist.

参考


  1. AWS 可以关掉这个检查,见官方文档里提到的 Changing the Source or Destination Checking, 其他云服务就不太清楚了。