dotnet / orleans

Cloud Native application framework for .NET
https://docs.microsoft.com/dotnet/orleans
MIT License
10.07k stars 2.03k forks source link

Unable to successfully use docker (compose) port mapping for silo gateway ports #8437

Open orcnz opened 1 year ago

orcnz commented 1 year ago

I have a sample repository that I am working with to understand how Orleans clusters behave, using docker compose to experiment locally. However, I am unable to successfully use docker's port mapping to allow my client application to connect to all of the silo containers in the cluster. It might be that my mental model of how this works is not correct, or some quirk of the configuration/setup.

I am using docker compose to start a Redis container and three Orleans Silo containers.

I am expecting to be able to leave the default ports configured in the containers and use dockers port mapping to map a unique port to the default gateway port in each silo container. However this is not currently working in this sample.

Some more details:

The docker-compose.yml file

version: "3.8"
services:
  redis:
    image: redis:7.0
    ports:
      - "6379:6379"
  silo1:
    build: .
    ports:
      - "8080:8080"
      - "30000:30000"
    environment:
      - Redis:ConnectionString=redis:6379
    depends_on:
      - redis
  silo2:
    build: .
    ports:
      - "8081:8080"
      - "30001:30000"
    environment:
      - Redis:ConnectionString=redis:6379
    depends_on:
      - redis
  silo3:
    build: .
    ports:
      - "8082:8080"
      - "30002:30000"
    environment:
      - Redis:ConnectionString=redis:6379
    depends_on:
      - redis

(note the internal ports on all three silo containers are the same, only the external ports are different)

The server code

await Host.CreateDefaultBuilder(args)
    .UseOrleans((context, silo) =>
    {
        silo.UseRedisClustering(context.Configuration["Redis:ConnectionString"] ?? "localhost")
            .UseDashboard()
            .ConfigureLogging(logger => logger.AddConsole());
    })
    .RunConsoleAsync();

The client code (snip)

var host = Host.CreateDefaultBuilder(args)
    .UseOrleansClient(client =>
    {
        client.UseStaticClustering(
            new IPEndPoint(IPAddress.Loopback, 30000),
            new IPEndPoint(IPAddress.Loopback, 30001),
            new IPEndPoint(IPAddress.Loopback, 30002));
    })
    .ConfigureLogging(logging => logging.AddConsole())
    .Build();

await host.StartAsync();

I am able to connect the client application to the silo cluster, but only silo1 on the exposed port 30000.

If I try and connect the client to silo2 on the exposed port 30001 that maps to port 30000 in the container, I get the following errors on the client:

warn: Orleans.Runtime.ClientClusterManifestProvider[0]
      Error trying to get cluster manifest from gateway S127.0.0.1:30001:0
      Orleans.Runtime.OrleansMessageRejectionException: Exception while sending message: Orleans.Runtime.Messaging.ConnectionFailedException: Unable to connect to endpoint S127.0.0.1:30001:0. See InnerException
 ---> Orleans.Networking.Shared.SocketConnectionException: Unable to connect to 127.0.0.1:30001. Error: ConnectionRefused
   at Orleans.Networking.Shared.SocketConnectionFactory.ConnectAsync(EndPoint endpoint, CancellationToken cancellationToken) in /_/src/Orleans.Core/Networking/Shared/SocketConnectionFactory.cs:line 54
   at Orleans.Runtime.Messaging.ConnectionFactory.ConnectAsync(SiloAddress address, CancellationToken cancellationToken) in /_/src/Orleans.Core/Networking/ConnectionFactory.cs:line 61
   at Orleans.Runtime.Messaging.ConnectionManager.ConnectAsync(SiloAddress address, ConnectionEntry entry) in /_/src/Orleans.Core/Networking/ConnectionManager.cs:line 193
   --- End of inner exception stack trace ---
   at Orleans.Runtime.Messaging.ConnectionManager.ConnectAsync(SiloAddress address, ConnectionEntry entry) in /_/src/Orleans.Core/Networking/ConnectionManager.cs:line 221
   at Orleans.Runtime.Messaging.ConnectionManager.GetConnectionAsync(SiloAddress endpoint) in /_/src/Orleans.Core/Networking/ConnectionManager.cs:line 106
   at Orleans.Runtime.Messaging.MessageCenter.<SendMessage>g__SendAsync|30_0(MessageCenter messageCenter, ValueTask`1 connectionTask, Message msg) in /_/src/Orleans.Runtime/Messaging/MessageCenter.cs:line 224
         at Orleans.Serialization.Invocation.ResponseCompletionSource`1.GetResult(Int16 token) in /_/src/Orleans.Serialization/Invocation/ResponseCompletionSource.cs:line 230
         at System.Threading.Tasks.ValueTask`1.ValueTaskSourceAsTask.<>c.<.cctor>b__4_0(Object state)
      --- End of stack trace from previous location ---
         at Orleans.Runtime.ClientClusterManifestProvider.RunAsync() in /_/src/Orleans.Core/Manifest/ClientClusterManifestProvider.cs:line 92

And on the server I see this errors:

orleanssandbox-silo2-1  | info: Orleans.Runtime.Messaging.NetworkingTrace[0]
orleanssandbox-silo2-1  |       Establishing connection to endpoint S127.0.0.1:30001:0
orleanssandbox-silo2-1  | warn: Orleans.Runtime.Messaging.NetworkingTrace[0]
orleanssandbox-silo2-1  |       Connection attempt to endpoint S127.0.0.1:30001:0 failed
orleanssandbox-silo2-1  |       Orleans.Networking.Shared.SocketConnectionException: Unable to connect to 127.0.0.1:30001. Error: ConnectionRefused
orleanssandbox-silo2-1  |          at Orleans.Networking.Shared.SocketConnectionFactory.ConnectAsync(EndPoint endpoint, CancellationToken cancellationToken) in /_/src/Orleans.Core/Networking/Shared/SocketConnectionFactory.cs:line 54
orleanssandbox-silo2-1  |          at Orleans.Runtime.Messaging.ConnectionFactory.ConnectAsync(SiloAddress address, CancellationToken cancellationToken) in /_/src/Orleans.Core/Networking/ConnectionFactory.cs:line 61
orleanssandbox-silo2-1  |          at Orleans.Runtime.Messaging.ConnectionManager.ConnectAsync(SiloAddress address, ConnectionEntry entry) in /_/src/Orleans.Core/Networking/ConnectionManager.cs:line 193

What am I missing? Why does the docker port mapping appear not to be working for the gateway ports? It is working in the same file for the Orleans dashboard that I am exposing on each silo container, ports 8080 for silo1, 8081 for silo2 and 8082 for silo3.

orcnz commented 1 year ago

I do have a workaround in place, however I am not sure this is how silo ports are (should be) handled in a more dynamic scale in/out environment like a working cluster.

The workaround I have involves passing a unique gateway port in to each silo container via environment variables, and also setting a passthrough port mapping in the docker-compose.yml file.

The docker-compose.yml file now looks like this:

version: "3.8"
services:
  redis:
    image: redis:7.0
    ports:
      - "6379:6379"
  silo1:
    build: .
    ports:
      - "8080:8080"
      - "30000:30000"
    environment:
      - Redis:ConnectionString=redis:6379
      - Orleans:GatewayPort=30000
    depends_on:
      - redis
  silo2:
    build: .
    ports:
      - "8081:8080"
      - "30001:30001"
    environment:
      - Redis:ConnectionString=redis:6379
      - Orleans:GatewayPort=30001
    depends_on:
      - redis
  silo3:
    build: .
    ports:
      - "8082:8080"
      - "30002:30002"
    environment:
      - Redis:ConnectionString=redis:6379
      - Orleans:GatewayPort=30002
    depends_on:
      - redis

(note the gateway port mappings for all the silos are passthrough i.e. "30000:30000", "30001:30001" and "30002:30002" and the ports are static and passed in via the environment section.)

The server code now looks like this:

using Orleans.Configuration;

await Host.CreateDefaultBuilder(args)
    .UseOrleans((context, silo) =>
    {
        silo.UseRedisClustering(context.Configuration["Redis:ConnectionString"] ?? "localhost")
            .UseDashboard()
            .Configure<EndpointOptions>(options =>
            {
                options.GatewayPort = int.Parse(context.Configuration["Orleans:GatewayPort"] ?? "30000");
            })
            .ConfigureLogging(logger => logger.AddConsole());
    })
    .RunConsoleAsync();

And now I can connect the client application using all three silos:

var host = Host.CreateDefaultBuilder(args)
    .UseOrleansClient(client =>
    {
        client.UseStaticClustering(
            new IPEndPoint(IPAddress.Loopback, 30000),
            new IPEndPoint(IPAddress.Loopback, 30001),
            new IPEndPoint(IPAddress.Loopback, 30002));
    })
    .ConfigureLogging(logging => logging.AddConsole())
    .Build();

Is there a better way to do this that doesn't involve having to set different configuration for each silo?