如何确定 Docker 容器中的 PostgreSQL 运行 是否已使用 Marten 完成初始化?

How to determine whether PostgreSQL running in a Docker container is done initializing with Marten?

我基本上是在写一些程序来对给定 table 增长的 PostgreSQL 的插入性能进行基准测试,我想确保当我使用 Marten 时插入数据,数据库已完全准备好接受插入。

我正在使用 Docker.DotNet 生成一个新容器 运行 宁最新的 PostgreSQL 图像,但即使容器处于 running 状态,有时也不是该容器内的 Postgre 运行ning 的情况。

当然,我可以添加一个 Thread. Sleep,但这有点随机,所以我更希望有一些确定性的东西来确定数据库何时准备好接受插入?

public static class Program
{
    public static async Task Main(params string[] args)
    {
        const string imageName = "postgres:latest";
        const string containerName = "MyProgreSQL";

        var client = new DockerClientConfiguration(Docker.DefaultLocalApiUri).CreateClient();
        var containers = await client.Containers.SearchByNameAsync(containerName);

        var container = containers.SingleOrDefault();
        if (container != null)
        {
            await client.Containers.StopAndRemoveContainerAsync(container);
        }

        var createdContainer = await client.Containers.RunContainerAsync(new CreateContainerParameters
        {
            Image = imageName,

            HostConfig = new HostConfig
            {
                PortBindings = new Dictionary<string, IList<PortBinding>>
                {
                    {"5432/tcp", new List<PortBinding>
                    {
                        new PortBinding
                        {
                            HostPort = "5432"
                        }
                    }}
                },
                PublishAllPorts = true
            },
            Env = new List<string>
            {
                "POSTGRES_PASSWORD=root",
                "POSTGRES_USER=root"
            },
            Name = containerName
        });

        var containerState = string.Empty;
        while (containerState != "running")
        {
            containers = await client.Containers.SearchByNameAsync(containerName);
            container = containers.Single();
            containerState = container.State;
        }

        var store = DocumentStore.For("host=localhost;database=postgres;password=root;username=root");

        var stopwatch = new Stopwatch();
        using (var session = store.LightweightSession())
        {
            var orders = OrderHelpers.FakeOrders(10000);
            session.StoreObjects(orders);
            stopwatch.Start();
            await session.SaveChangesAsync();
            var elapsed = stopwatch.Elapsed;
            // Whatever else needs to be done...
        }
    }
}

仅供参考,如果我 运行ning 上面的程序而没有在行 await session.SaveChangesAsync(); 之前暂停,我 运行ning 就会进入以下异常:

Unhandled Exception: Npgsql.NpgsqlException: Exception while reading from stream ---> System.IO.EndOfStreamException: Attempted to read past the end of the streams.
   at Npgsql.NpgsqlReadBuffer.<>c__DisplayClass31_0.<<Ensure>g__EnsureLong|0>d.MoveNext() in C:\projects\npgsql\src\Npgsql\NpgsqlReadBuffer.cs:line 154
   --- End of inner exception stack trace ---
   at Npgsql.NpgsqlReadBuffer.<>c__DisplayClass31_0.<<Ensure>g__EnsureLong|0>d.MoveNext() in C:\projects\npgsql\src\Npgsql\NpgsqlReadBuffer.cs:line 175
--- End of stack trace from previous location where exception was thrown ---
   at Npgsql.NpgsqlConnector.<>c__DisplayClass161_0.<<ReadMessage>g__ReadMessageLong|0>d.MoveNext() in C:\projects\npgsql\src\Npgsql\NpgsqlConnector.cs:l
ine 955
--- End of stack trace from previous location where exception was thrown ---
   at Npgsql.NpgsqlConnector.Authenticate(String username, NpgsqlTimeout timeout, Boolean async) in C:\projects\npgsql\src\Npgsql\NpgsqlConnector.Auth.cs
:line 26
   at Npgsql.NpgsqlConnector.Open(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken) in C:\projects\npgsql\src\Npgsql\NpgsqlConne
ctor.cs:line 425
   at Npgsql.ConnectorPool.AllocateLong(NpgsqlConnection conn, NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken) in C:\projects\
npgsql\src\Npgsql\ConnectorPool.cs:line 246
   at Npgsql.NpgsqlConnection.<>c__DisplayClass32_0.<<Open>g__OpenLong|0>d.MoveNext() in C:\projects\npgsql\src\Npgsql\NpgsqlConnection.cs:line 300
--- End of stack trace from previous location where exception was thrown ---
   at Npgsql.NpgsqlConnection.Open() in C:\projects\npgsql\src\Npgsql\NpgsqlConnection.cs:line 153
   at Marten.Storage.Tenant.generateOrUpdateFeature(Type featureType, IFeatureSchema feature)
   at Marten.Storage.Tenant.ensureStorageExists(IList`1 types, Type featureType)
   at Marten.Storage.Tenant.ensureStorageExists(IList`1 types, Type featureType)
   at Marten.Storage.Tenant.StorageFor(Type documentType)
   at Marten.DocumentSession.Store[T](T[] entities)
   at Baseline.GenericEnumerableExtensions.Each[T](IEnumerable`1 values, Action`1 eachAction)
   at Marten.DocumentSession.StoreObjects(IEnumerable`1 documents)
   at Benchmark.Program.Main(String[] args) in C:\Users\eperret\Desktop\Benchmark\Benchmark\Program.cs:line 117
   at Benchmark.Program.<Main>(String[] args)

[编辑]

我接受了一个答案,但由于 Docker.DotNet 中关于健康参数等价性的错误,我无法利用答案中给出的解决方案(我仍然认为 docker 的正确翻译.NET 客户端中的命令,如果可能的话)将是最好的解决方案。与此同时,这就是我解决问题的方法,我基本上轮询了预期 运行 在健康检查中的命令,直到结果正常:

Program.cs,代码的实际内容:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Benchmark.DockerClient;
using Benchmark.Domain;
using Benchmark.Utils;
using Docker.DotNet;
using Docker.DotNet.Models;
using Marten;
using Microsoft.Extensions.Configuration;

namespace Benchmark
{
    public static class Program
    {
        public static async Task Main(params string[] args)
        {
            var configurationBuilder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json");

            var configuration = new Configuration();
            configurationBuilder.Build().Bind(configuration);

            var client = new DockerClientConfiguration(DockerClient.Docker.DefaultLocalApiUri).CreateClient();
            var containers = await client.Containers.SearchByNameAsync(configuration.ContainerName);

            var container = containers.SingleOrDefault();
            if (container != null)
            {
                await client.Containers.StopAndRemoveContainerAsync(container.ID);
            }

            var createdContainer = await client.Containers.RunContainerAsync(new CreateContainerParameters
            {
                Image = configuration.ImageName,
                HostConfig = new HostConfig
                {
                    PortBindings = new Dictionary<string, IList<PortBinding>>
                    {
                        {$@"{configuration.ContainerPort}/{configuration.ContainerPortProtocol}", new List<PortBinding>
                        {
                            new PortBinding
                            {
                                HostPort = configuration.HostPort
                            }
                        }}
                    },
                    PublishAllPorts = true
                },
                Env = new List<string>
                {
                    $"POSTGRES_USER={configuration.Username}",
                    $"POSTGRES_PASSWORD={configuration.Password}"
                },
                Name = configuration.ContainerName
            });

            var isContainerReady = false;

            while (!isContainerReady)
            {
                var result = await client.Containers.RunCommandInContainerAsync(createdContainer.ID, "pg_isready -U postgres");
                if (result.stdout.TrimEnd('\n') == $"/var/run/postgresql:{configuration.ContainerPort} - accepting connections")
                {
                    isContainerReady = true;
                }
            }

            var store = DocumentStore.For($"host=localhost;" +
                                          $"database={configuration.DatabaseName};" +
                                          $"username={configuration.Username};" +
                                          $"password={configuration.Password}");

            // Whatever else needs to be done...
    }
}

ContainerOperationsExtensions.cs 中定义的扩展:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Docker.DotNet;
using Docker.DotNet.Models;

namespace Benchmark.DockerClient
{
    public static class ContainerOperationsExtensions
    {
        public static async Task<IList<ContainerListResponse>> SearchByNameAsync(this IContainerOperations source, string name, bool all = true)
        {
            return await source.ListContainersAsync(new ContainersListParameters
            {
                All = all,
                Filters = new Dictionary<string, IDictionary<string, bool>>
                {
                    {"name", new Dictionary<string, bool>
                        {
                            {name, true}
                        }
                    }
                }
            });
        }

        public static async Task StopAndRemoveContainerAsync(this IContainerOperations source, string containerId)
        {
            await source.StopContainerAsync(containerId, new ContainerStopParameters());
            await source.RemoveContainerAsync(containerId, new ContainerRemoveParameters());
        }

        public static async Task<CreateContainerResponse> RunContainerAsync(this IContainerOperations source, CreateContainerParameters parameters)
        {
            var createdContainer = await source.CreateContainerAsync(parameters);
            await source.StartContainerAsync(createdContainer.ID, new ContainerStartParameters());
            return createdContainer;
        }

        public static async Task<(string stdout, string stderr)> RunCommandInContainerAsync(this IContainerOperations source, string containerId, string command)
        {
            var commandTokens = command.Split(" ", StringSplitOptions.RemoveEmptyEntries);

            var createdExec = await source.ExecCreateContainerAsync(containerId, new ContainerExecCreateParameters
            {
                AttachStderr = true,
                AttachStdout = true,
                Cmd = commandTokens
            });

            var multiplexedStream = await source.StartAndAttachContainerExecAsync(createdExec.ID, false);

            return await multiplexedStream.ReadOutputToEndAsync(CancellationToken.None);
        }
    }
}

Docker.cs 获取本地 docker api uri:

using System;
using System.Runtime.InteropServices;

namespace Benchmark.DockerClient
{
    public static class Docker
    {
        static Docker()
        {
            DefaultLocalApiUri = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 
                ? new Uri("npipe://./pipe/docker_engine")
                : new Uri("unix:/var/run/docker.sock");
        }

        public static Uri DefaultLocalApiUri { get; }
    }
}

我建议您使用自定义 healtcheck 检查数据库是否准备好接受连接。

我不熟悉 Docker 的 .NET 客户端,但以下 docker run 命令显示了您应该尝试的内容:

docker run --name postgres \
    --health-cmd='pg_isready -U postgres' \
    --health-interval='10s' \
    --health-timeout='5s' \
    --health-start-period='10s' \
    postgres:latest

应根据您的需要更新时间参数。

配置此健康检查后,您的应用程序必须等待容器处于“健康”状态,然后才能尝试连接到数据库。在这种特殊情况下,状态“healthy”意味着命令 pg_isready -U postgres 已成功(因此数据库已准备好接受连接)。

您的容器状态可以通过以下方式检索:

docker inspect --format "{{json .State.Health.Status }}" postgres