微服务项目如何在Mac+Docker Compose上高效调试?

1,286 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

如果你也遇到了 Mac 中使用 Docker Compose调试微服务的问题,那么推荐你继续往下看。本文介绍一种通过特殊域名 host.docker.internal 解决 Mac下Docker "内外网"服务相互通信的解决方案(Docker 在 Mac 中不支持 host 网络模式)。

引言

如果你的开源项目使用了微服务架构,动不动5-10个服务,你肯定会遇到一系列拆分后带来的新问题,比如作者最近在Mac上调试微服务项目时,就遇到了一个很痛的问题:

使用 Docker Compose 一键启动所有服务后,某天当我想Debug某一个服务时,我发现死活调不通它的依赖(运行在容器中)。

你肯定想,这么简单,你做个端口映射不就行了吗?实际上我也是这么想的,但这个事情没有那么简单。

拆分后的服务结构

按照go-zero框架的指导思想: 2AAD8FC9-D796-48B4-A6A4-C2B346327123.png

我把系统划分成了3层:

  • API网关:使用envoy统一汇聚所有BFF的http接口,对外提供一个域名即可访问所有服务。
  • BFF层:业务聚合层,依赖RPC层的多个服务,对外提供http接口,客户端可以直接调,也可以走API网关代理。
  • RPC层:功能实现层,提供grpc接口供BFF层调用。

BFF和RPC层之间使用了 ETCD 做服务注册和发现,redis、mysql、mongo、kafka等中间件都跑在Docker中。

拆出来后,目前已初步实现的服务如下(总计6个):

├── api        # BFF层,提供HTTP接口,都是go mod项目
│   ├── apichat
│   ├── apiuser
│   └── wspush
├── pkg        # 所有服务共用的包,go mod 项目
├── rpc        # rpc层,提供grpc接口,也是 go mod项目,BFF会调用多个grpc服务获取前端需要的所有数据后返回
│   ├── chat
│   ├── pushjob
│   └── user
├── go.work    # 使用go 1.18的go work功能,管理所有子项目
├── go.work.sum

我们看到,现在总共有6个服务要启动:

  • BFF层:apichat、apiuser、wspush
  • rpc层:chat、pushJob、user

按照依赖关系:如果要调试apichat,需要启动user、chat、pushjob、wspush4个服务,于是我开始遇到一系列微服务拆分带来的问题,其中最优先需要解决的就是Debug调试的问题

拆分后,我遇到的调试问题

这个问题是这样的,我们来上一张图,还原一下当时的场景:

ADB810FB-5977-4D0E-BC30-F833E8DAA1D4.png

  • 宿主机上,我打开了一个 goland,准备使用Debug模式调试一下 wspush 这个服务。
  • 而wspush服务,依赖的 user-rpc 和 chat-rpc 运行在Docker中,因为Mac不支持 host 模式,所以在Mac中Docker创建了一个子网
  • 另外,user和chat2个rpc服务,会把自己注册到 etcd 上(同样在Docker内),以便被 wspush 发现并且访问。
  • wspush 访问 etcd 已经做了端口映射

此时,我发现上面红X的那条线,也就是 wspush 要调用 user-rpc服务的 grpc 接口,始终调不通

当然,调试问题只是微服务拆分后我遇到的问题之一,但是限于篇幅和从严重级别看,这里只着重说明 Mac中Docker网络隔离带来的调试问题和解决方案实操!完整内容请移步:Mac本地微服务调试实战和踩坑记录

原因

首先排除了和etcd的通信问题,初始化的时候没有报错。那么很可能就是 user-rpc 注册到etcd上的ip地址,宿主机不认识导致的了。

我们确认下 user-rpc 注册到 etcd 上的IP(使用 kratos 框架,它会把服务注册到 /microservices 下):

$ etcdctl --endpoints=127.0.0.1:2379 get /microservices --prefix

输出:

/microservices/rpc-user/db5b60b4559a

{"id":"db5b60b4559a","name":"rpc-user","version":"e1eba40","metadata":{},"endpoints":["[grpc://172.16.0.2:9000]()"]}

我们看到rpc-user服务注册的ip是 172.16.0.2:9000,也就是Docker容器的本地IP,通过翻 kratos 注册etcd的源码也证实了这一点:

func (a *App) buildInstance() (*registry.ServiceInstance, error) {
   endpoints := make([]string, 0, len(a.opts.endpoints))
   for _, e := range a.opts.endpoints {
      endpoints = append(endpoints, e.String())
   }

   // 如果没有指定 etcd 注册的endpoint,则使用 grpc 服务监听的 endpoint注册
   // 也就是容器的IP
   if len(endpoints) == 0 {
      for _, srv := range a.opts.servers {
         if r, ok := srv.(transport.Endpointer); ok {
            e, err := r.Endpoint()
            if err != nil {
               return nil, err
            }
            endpoints = append(endpoints, e.String())
         }
      }
   }
   return &registry.ServiceInstance{
      ID:        a.opts.id,
      Name:      a.opts.name,
      Version:   a.opts.version,
      Metadata:  a.opts.metadata,
      Endpoints: endpoints,
   }, nil
}

宿主机是192网段,Docker容器是172网段,相当于2个子网,故没有增加路由的情况下,宿主机192网段是不认识 172.16.0.2 这个IP的,也就解释了为什么服务会调不通。

解决方法

找到了问题,我们也能很快想到方案:

  1. 宿主机的路由表增加IP的转发规则:可以参考这篇文章:Docker 多服务器网络配置(简单路由)
  2. host网络:直接让Docker使用宿主机IP,大家同在一个局域网,也就能通信了。奈何 Mac 不支持,所以行不通!
  3. 使用外网IP注册到ETCD中:因为我们配置了端口映射,宿主机通过 192.168.200.197:8000 是能连接到容器内服务的。

路由表相对复杂,建议通过使用外网IP(宿主机IP)注册到ETCD的方式解决,考虑到团队内每一个人IP不一样,故最好通过 host 的方式暴露,幸好Docker为我们提供了这个特殊的host,它就是 host.docker.internal

host.docker.internal 的作用

其实我已经想到了另外一个问题:如果我调试的是grpc服务,api服务运行在容器中呢?

也就是说容器里的api服务依赖宿主机的grpc服务,那么如何从容器访问宿主机呢

stack overflow上有人问,我要从Docker中连接宿主机的mysql(From inside of a Docker container, how do I connect to the localhost of the machine?),要怎么实现呢?答案就是使用 host.docker.internal 代替 127.0.0.1,也就是说通过这个域名,我们能通过容器访问宿主机上的服务。

所以,这是为什么上面推荐使用域名注册服务的第二个原因。

简单总结下:

  • 宿主机 -> Docker容器:直接使用本机IP+容器映射端口访问
  • Docker容器 -> 宿主机:通过host.docker.internal+宿主机端口访问

因为这个问题挺影响开发和调试,于是下面再详细介绍一下具体的操作步骤。

Docker和宿主机通信配置

环境

假设环境如下:

  • 宿主机IP
    • 服务:apiuser(依赖user)
    • IP:192.168.1.100
    • 端口:8100
  • Docker容器
    • user
      • IP:172.16.0.2
      • 端口:9100
    • etcd
      • IP:172.16.0.6
      • 端口2379

更改宿主机hosts文件

先更改宿主机 host 配置(解决宿主机通过域名访问容器的通信问题):

$ sudo vim /etc/hosts
 192.168.200.197 host.docker.internal

配置文件

然后上面说了,服务需要以域名的方式注册。宿主机和容器的grpc服务,都需要使用 host 方式暴露服务,故配置文件中需要能指定注册服务的域名:

registry:
  etcd:
    endpoints: [ 127.0.0.1:2379 ]
    register_server_name: rpc-chat
    # 不管是宿主机还是容器,都经过这个host进行通信
    register_end_point: "host.docker.internal:9100"

代码

另外,代码中也需要增加支持:

func newApp(logger kratoslog.Logger, config *conf.Bootstrap, gs *grpc.Server, registry *etcd.Registry) *kratos.App {
   var endpoint = kratos.Endpoint([]*url.URL{}...)
   // 这里读取配置文件中的服务注册地址,然后注册到 etcd 中
   if config.Registry.Etcd.RegisterEndPoint != "" {
      endpoint = kratos.Endpoint(&url.URL{Host: config.Registry.Etcd.RegisterEndPoint, Scheme: "grpc"})
   }
   return kratos.New(
      kratos.ID(id),
      kratos.Name(Name),
      kratos.Version(Version),
      kratos.Metadata(map[string]string{}),
      kratos.Logger(logger),
      kratos.Server(
         gs,
      ),
      kratos.Registrar(registry),
      endpoint,
   )
}

查看etcd

现在查看 etcd,服务的注册地址变成 grpc://host.docker.internal:9100

$ etcdctl --endpoints=127.0.0.1:2379 get /microservices --prefix

输出:

/microservices/rpc-user/4fc113cd7436
{"id":"4fc113cd7436","name":"rpc-user","version":"7c5a0e6","metadata":{},"endpoints":["grpc://host.docker.internal:9100"]}

经过这个域名通信后,本地调试一般会遇到4种场景:

  • 宿主机 -> 宿主机:通过 host.docker.internal 通信
  • 宿主机 -> 容器:通过 host.docker.internal 通信
  • 容器 -> 容器:通过 host.docker.internal 通信
  • 容器 -> 宿主机:通过 host.docker.internal 通信

下面挨个验证一下,是否可行!

验证

宿主机到宿主机通过 host.docker.internal 通信

707A28D0-F375-4737-9898-0B9BA7A4911C.png

本机启动apiuser和其依赖grpc服务user,user注册到etcd的地址为:grpc://host.docker.internal:9100

9EB5FE1D-FEDA-4804-A5D4-0CBA8565B0C7.png

发送请求后得到响应,验证通过!

宿主机到容器

宿主机启动apiuser,其依赖grpc服务部署到容器中。

9A56E17B-EBF8-4A4A-BC1B-2ABAB82B993A.png

DE2B81E2-3891-4187-9C80-72901A9F311E.png

发送请求后同样得到响应,验证通过!

容器到容器通过 host.docker.internal 通信

ED52EABA-E709-404F-B85C-E9D34C587E7F.png

4C73083C-65A9-40AB-B515-ADB6187D62D6.png

发送请求后同样得到响应,验证通过!

容器到宿主机

DCBF82B3-D459-4B87-9DCD-49BFE31E6306.png

0062C808-11E9-4EEF-954E-CA0B208F315C.png

发送请求后同样得到响应,验证通过!

总结

本文介绍了通过Docker Compose单机编排微服务可能遇到的一些问题和解决方案,特别是对使用mac开发的同学。如何解决mac docker不支持host网络模式,使用etcd场景下服务调试的宿主机和容器的网络通信问题,希望能对您有所帮助!

参考: