在 docker-compose 中访问 GRPC 服务

Posted

技术标签:

【中文标题】在 docker-compose 中访问 GRPC 服务【英文标题】:Accessing the GRPC Service within docker-compose 【发布时间】:2021-10-26 23:00:42 【问题描述】:

我目前正在关注eShopOnContainers 的教程,我决定尝试测试 GRPC 功能,类似于项目。

我正在尝试构建的是 GRPC 客户端和 GRPC 服务,它们都托管在 docker 上并且可以相互通信。现在,我设法让它工作,如果你在 GRPC 客户端中查看 Startup.cs,这个 Uri http://host.docker.internal:5104 设法进行调用并获得响应。

然而,最初的 eshopOnContainers 项目使用http://basket-api:81 路径,它更好,在我看来更易于维护。它还使用了更多的组件和一些配置:

    GRPC 服务在 Startup.cs 中使用以下内容:app.UsePathBase("/basket-api") Original project 以及 Program.cs 中的一些配置来监听端口:
BuildWebHost
...
.ConfigureKestrel(options =>
        
            var ports = GetDefinedPorts(configuration);
            options.Listen(IPAddress.Any, ports.httpPort, listenOptions =>
            
                listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
            );

            options.Listen(IPAddress.Any, ports.grpcPort, listenOptions =>
            
                listenOptions.Protocols = HttpProtocols.Http2;
            );

        )
...

Original project httpPort 的端口是 80,grpcPort 的端口是 81。

    GRPC 客户端使用以下 Uri 进行调用 http://basket-api:81 另外,还部署了一个 Envoy 代理,它的规则如下,但我认为最重要的部分是规则 b-shortb-long 和集群 basket,我认为这会导致最终 URL 为 basket-api:80(根据集群配置)。

我真的不明白,如果它最终调用 GRPC 服务为什么需要端口 81,但如果有更多知识的人能解释一下就好了。

admin:
  access_log_path: "/dev/null"
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 8001
static_resources:
  listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 80
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: eshop_backend_route
            virtual_hosts:
            - name: eshop_backend
              domains:
              - "*"
              routes:
              - name: "c-short"
                match:
                  prefix: "/c/"
                route:
                  auto_host_rewrite: true
                  prefix_rewrite: "/catalog-api/"
                  cluster: catalog
              - name: "c-long"
                match:
                  prefix: "/catalog-api/"
                route:
                  auto_host_rewrite: true
                  cluster: catalog
              - name: "o-short"
                match:
                  prefix: "/o/"
                route:
                  auto_host_rewrite: true
                  prefix_rewrite: "/ordering-api/"
                  cluster: ordering
              - name: "o-long"
                match:
                  prefix: "/ordering-api/"
                route:
                  auto_host_rewrite: true
                  cluster: ordering
              - name: "h-long"
                match:
                  prefix: "/hub/notificationhub"
                route:
                  auto_host_rewrite: true
                  cluster: signalr-hub
                  timeout: 300s
                  upgrade_configs:
                    upgrade_type: "websocket"
                    enabled: true
              - name: "b-short"
                match:
                  prefix: "/b/"
                route:
                  auto_host_rewrite: true
                  prefix_rewrite: "/basket-api/"
                  cluster: basket
              - name: "b-long"
                match:
                  prefix: "/basket-api/"
                route:
                  auto_host_rewrite: true
                  cluster: basket
              - name: "agg"
                match:
                  prefix: "/"
                route:
                  auto_host_rewrite: true
                  prefix_rewrite: "/"
                  cluster: shoppingagg
          http_filters:
          - name: envoy.router
          access_log:
          - name: envoy.file_access_log
            filter:
              not_health_check_filter: 
            config:
              json_format:
                time: "%START_TIME%"
                protocol: "%PROTOCOL%"
                duration: "%DURATION%"
                request_method: "%REQ(:METHOD)%"
                request_host: "%REQ(HOST)%"
                path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
                response_flags: "%RESPONSE_FLAGS%"
                route_name: "%ROUTE_NAME%"
                upstream_host: "%UPSTREAM_HOST%"
                upstream_cluster: "%UPSTREAM_CLUSTER%"
                upstream_local_address: "%UPSTREAM_LOCAL_ADDRESS%"
              path: "/tmp/access.log"
  clusters:
  - name: shoppingagg
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    hosts:
    - socket_address:
        address: webshoppingagg
        port_value: 80
  - name: catalog
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    hosts:
    - socket_address:
        address: catalog-api
        port_value: 80
  - name: basket
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    hosts:
    - socket_address:
        address: basket-api
        port_value: 80
  - name: ordering
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    hosts:
    - socket_address:
        address: ordering-api
        port_value: 80
  - name: signalr-hub
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    hosts:
    - socket_address:
        address: ordering-signalrhub
        port_value: 80

问题

在我的方法中,我假设如果我完全跳过 Envoy 代理组件并使用 http://basket-api:80 调用服务,它会设法找到它,但不幸的是没有运气。现在我不确定我的端口是否错误或我的 URI 是否错误,但我相信我正在遵循与原始项目类似的方法,只是跳过代理。**

我也可能误解了我的 Docker 配置,但我没有看到任何可疑元素。

错误堆栈:

RpcException: Status(StatusCode="Unavailable", Detail="Error starting gRPC call. HttpRequestException: Resource temporarily unavailable (basket-api:81) SocketException: Resource temporarily unavailable", DebugException="System.Net.Http.HttpRequestException: Resource temporarily unavailable (basket-api:81)
---> System.Net.Sockets.SocketException (11): Resource temporarily unavailable
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)
at System.Net.Sockets.Socket.<ConnectAsync>g__WaitForConnectWithCancellation|283_0(AwaitableSocketAsyncEventArgs saea, ValueTask connectTask, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.DefaultConnectAsync(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
at System.Net.Http.ConnectHelper.ConnectAsync(Func`3 callback, DnsEndPoint endPoint, HttpRequestMessage requestMessage, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at System.Net.Http.ConnectHelper.ConnectAsync(Func`3 callback, DnsEndPoint endPoint, HttpRequestMessage requestMessage, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.GetHttp2ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
at Grpc.Shared.TelemetryHeaderHandler.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at Grpc.Net.Client.Internal.GrpcCall`2.RunCall(HttpRequestMessage request, Nullable`1 timeout)")

代码

GRPC 客户端

Index.cshtml.cs

 public void OnGet()
        
            var response = _greeterClient.SayHello(new HelloRequest
            
                Name = "Bob"
            );
            Debug.WriteLine(response.Message);
        

Startup.cs

        public void ConfigureServices(IServiceCollection services)
        
            services.AddRazorPages();
            services.AddGrpcClient<Greeter.GreeterClient>((services, options) =>
            
                // This one works
                //options.Address = new Uri("http://host.docker.internal:5104");

                // This one doesn't
                options.Address = new Uri("http://basket-api:80");
            );
        

GRPC 服务(默认 GRPC 模板稍作调整)

Program.cs

 public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                
                    webBuilder.ConfigureKestrel(options =>
                    
                        options.Listen(IPAddress.Any, 80, listenOptions =>
                        
                            listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
                        );
                        options.Listen(IPAddress.Any, 81, listenOptions =>
                        
                            listenOptions.Protocols = HttpProtocols.Http2;
                        );
                    );
                    webBuilder.UseStartup<Startup>();
                );

Startup.cs

public class Startup
    
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        
            services.AddGrpc();
        

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        
            if (env.IsDevelopment())
            
                app.UseDeveloperExceptionPage();
            

            app.UsePathBase("/basket-api");

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            
                endpoints.MapGrpcService<GreeterService>();

                endpoints.MapGet("/", async context =>
                
                    await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
                );
            );
        
    

Docker 编写

docker-compose.yml

version: '3.4'

services:
  grpcserver:
    image: $DOCKER_REGISTRY-grpcserver
    build:
      context: .
      dockerfile: GrpcServer/Dockerfile

  grpcclient:
    image: $DOCKER_REGISTRY-grpcclient
    build:
      context: .
      dockerfile: GrpcClient/Dockerfile

docker-compose.override.yml

version: '3.4'

services:
  grpcserver:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://0.0.0.0:80
    ports:
      - "5103:80"
      - "5104:81"
    volumes:
      - $APPDATA/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
      - $APPDATA/ASP.NET/Https:/root/.aspnet/https:ro
  grpcclient:
    environment: 
      - ASPNETCORE_ENVIRONMENT=Development
    ports:
      - "5121:80"
    volumes:
      - $APPDATA/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
      - $APPDATA/ASP.NET/Https:/root/.aspnet/https:ro

【问题讨论】:

【参考方案1】:

您应该能够使用 docker-compose 文件中 docker 生成的 DNS 名称。您的 GRPC 客户端应该能够访问 http://grpcserver:5103 上的服务器

使用 docker-compose,您只需使用服务名称和您在容器中公开的端口即可在容器之间进行通信。

[编辑] 从路径中删除了扩展名,因为 UsePathBase()

添加一个中间件,从请求路径中提取指定的路径库并将其附加到请求路径库中。

UsePathBase

【讨论】:

您是绝对正确的,根据您的信息,我实际上已将被调用的 URL 更改为“grpcserver:81”,这绝对有效。不过,我也验证了一件事:我不小心把原来的 `app.UsePathBase("/basket-api");` 留在了 GRPC 服务中,但使用上述 URL 的解决方案仍然有效。你知道吗,这怎么可能? 我想我只是想通了:在原来的解决方案中,他们也是将 GRPC Service 与 WebAPI 结合起来,这就是使用这个东西的地方。无论如何,感谢您的帮助,我将其标记为答案。 看起来该函数会从 URL 中删除该基本路径(如果存在) - 如果您的路由通过反向代理进入,这将很有用。 docs.microsoft.com/en-us/dotnet/api/…

以上是关于在 docker-compose 中访问 GRPC 服务的主要内容,如果未能解决你的问题,请参考以下文章

如何在自己的 Yocto 包中访问 protoc 编译器并引用 gRPC 库

如何使用 python 在云 pub/sub 中访问 grpc?

grpc提供http访问方式

如何在 docker-compose 中访问 Postgres 数据库

首次使用 protobuf-net c# 访问 gRPC 端点时性能缓慢

编译gRPC