在 docker-compose 中访问 GRPC 服务

Accessing the GRPC Service within docker-compose

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

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

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

  1. 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。

  1. GRPC 客户端使用以下 Uri 进行调用 http://basket-api:81
  2. 此外,还有一个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

您应该能够使用 docker 从您的 docker-compose 文件生成的 DNS 名称。您的 GRPC 客户端应该能够在 http://grpcserver:5103

到达服务器

使用 docker-compose,您可以简单地通过使用服务名称和您在容器中公开的端口在容器之间进行通信。

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

Adds a middleware that extracts the specified path base from request path and postpend it to the request path base.

UsePathBase