共用方式為


使用以容器身分執行的資料庫伺服器

小提示

此內容是適用於容器化 .NET 應用程式的電子書.NET 微服務架構摘錄,可在 .NET Docs 或免費下載的 PDF 中取得,可脫機讀取。

.NET 微服務架構的容器化 .NET 應用程式電子書封面縮圖。

您可以在一般獨立伺服器、內部部署叢集或 Azure SQL DB 等雲端的 PaaS 服務上,擁有您的資料庫 (SQL Server、PostgreSQL、MySQL 等)。 不過,針對開發和測試環境,讓資料庫以容器身分執行是很方便的,因為您沒有任何外部相依性,而且只要執行 docker-compose up 命令就會啟動整個應用程式。 讓這些資料庫作為容器使用非常適合進行整合測試,因為資料庫是在容器中啟動並一律填入相同的範本資料,這樣可以使測試更具可預測性。

在 eShopOnContainers 中,有一個名為 sqldata 的容器,如 docker-compose.yml 檔案中所定義,它運行 Linux 上的 SQL Server 實例,該實例包含所有需要 SQL 資料庫的微服務。

微服務中的關鍵點是,每個微服務都有自己的相關數據,因此應該有自己的資料庫。 不過,資料庫可以是任何地方。 在此情況下,它們全都位於相同的容器中,以盡可能降低 Docker 記憶體需求。 請記住,這是一個夠好的解決方案,適合用於開發,也或許適用於測試,但不適用於生產環境。

範例應用程式中的 SQL Server 容器會使用docker-compose.yml檔案中的下列 YAML 程式代碼進行設定,當您執行 docker-compose up時會執行。 請注意,YAML 程式代碼已合併來自泛型docker-compose.yml檔案和docker-compose.override.yml檔案的組態資訊。 (通常您會將環境設定與與 SQL Server 映像相關的基底或靜態資訊分開。

  sqldata:
    image: mcr.microsoft.com/mssql/server:2017-latest
    environment:
      - SA_PASSWORD=Pass@word
      - ACCEPT_EULA=Y
    ports:
      - "5434:1433"

以類似的方式,而不是使用 docker-compose,下列 docker run 命令可以執行該容器:

docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Pass@word' -p 5433:1433 -d mcr.microsoft.com/mssql/server:2017-latest

不過,如果您要部署 eShopOnContainers 之類的多容器應用程式,使用 命令會更方便, docker-compose up 以便部署應用程式所需的所有容器。

當您第一次啟動此 SQL Server 容器時,容器會使用您提供的密碼來初始化 SQL Server。 一旦 SQL Server 以容器的形式執行,您可以透過任何一般 SQL 連線來更新資料庫,例如從 SQL Server Management Studio、Visual Studio 或 C# 程式代碼連線。

eShopOnContainers 應用程式會在啟動時通過植入示例資料來初始化每個微服務的資料庫,具體流程如下一節所述。

讓 SQL Server 以容器身分執行,不僅適用於您可能無法存取 SQL Server 實例的示範場景,也在其他情境中非常有用。 如前所述,它也適用於開發和測試環境,因此您可以藉由植入新的範例數據,輕鬆地執行從全新 SQL Server 映像和已知數據開始的整合測試。

其他資源

在 Web 應用程式啟動時植入測試數據

若要在應用程式啟動時將資料新增至資料庫,您可以將如下的程式代碼新增至 Main Web API 專案類別中的 Program 方法:

public static int Main(string[] args)
{
    var configuration = GetConfiguration();

    Log.Logger = CreateSerilogLogger(configuration);

    try
    {
        Log.Information("Configuring web host ({ApplicationContext})...", AppName);
        var host = CreateHostBuilder(configuration, args);

        Log.Information("Applying migrations ({ApplicationContext})...", AppName);
        host.MigrateDbContext<CatalogContext>((context, services) =>
        {
            var env = services.GetService<IWebHostEnvironment>();
            var settings = services.GetService<IOptions<CatalogSettings>>();
            var logger = services.GetService<ILogger<CatalogContextSeed>>();

            new CatalogContextSeed()
                .SeedAsync(context, env, settings, logger)
                .Wait();
        })
        .MigrateDbContext<IntegrationEventLogContext>((_, __) => { });

        Log.Information("Starting web host ({ApplicationContext})...", AppName);
        host.Run();

        return 0;
    }
    catch (Exception ex)
    {
        Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", AppName);
        return 1;
    }
    finally
    {
        Log.CloseAndFlush();
    }
}

在容器啟動時套用移轉並植入資料庫有一個重要的注意事項。 由於資料庫伺服器可能因為任何原因而無法使用,因此您必須在等候伺服器可用時處理重試。 此重試邏輯是由 MigrateDbContext() 擴充方法處理,如下列程式代碼所示:

public static IWebHost MigrateDbContext<TContext>(
    this IWebHost host,
    Action<TContext,
    IServiceProvider> seeder)
      where TContext : DbContext
{
    var underK8s = host.IsInKubernetes();

    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;

        var logger = services.GetRequiredService<ILogger<TContext>>();

        var context = services.GetService<TContext>();

        try
        {
            logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name);

            if (underK8s)
            {
                InvokeSeeder(seeder, context, services);
            }
            else
            {
                var retry = Policy.Handle<SqlException>()
                    .WaitAndRetry(new TimeSpan[]
                    {
                    TimeSpan.FromSeconds(3),
                    TimeSpan.FromSeconds(5),
                    TimeSpan.FromSeconds(8),
                    });

                //if the sql server container is not created on run docker compose this
                //migration can't fail for network related exception. The retry options for DbContext only
                //apply to transient exceptions
                // Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service)
                retry.Execute(() => InvokeSeeder(seeder, context, services));
            }

            logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name);
            if (underK8s)
            {
                throw;          // Rethrow under k8s because we rely on k8s to re-run the pod
            }
        }
    }

    return host;
}

自訂 CatalogContextSeed 類別中的下列程式代碼會填入數據。

public class CatalogContextSeed
{
    public static async Task SeedAsync(IApplicationBuilder applicationBuilder)
    {
        var context = (CatalogContext)applicationBuilder
            .ApplicationServices.GetService(typeof(CatalogContext));
        using (context)
        {
            context.Database.Migrate();
            if (!context.CatalogBrands.Any())
            {
                context.CatalogBrands.AddRange(
                    GetPreconfiguredCatalogBrands());
                await context.SaveChangesAsync();
            }
            if (!context.CatalogTypes.Any())
            {
                context.CatalogTypes.AddRange(
                    GetPreconfiguredCatalogTypes());
                await context.SaveChangesAsync();
            }
        }
    }

    static IEnumerable<CatalogBrand> GetPreconfiguredCatalogBrands()
    {
        return new List<CatalogBrand>()
       {
           new CatalogBrand() { Brand = "Azure"},
           new CatalogBrand() { Brand = ".NET" },
           new CatalogBrand() { Brand = "Visual Studio" },
           new CatalogBrand() { Brand = "SQL Server" }
       };
    }

    static IEnumerable<CatalogType> GetPreconfiguredCatalogTypes()
    {
        return new List<CatalogType>()
        {
            new CatalogType() { Type = "Mug"},
            new CatalogType() { Type = "T-Shirt" },
            new CatalogType() { Type = "Backpack" },
            new CatalogType() { Type = "USB Memory Stick" }
        };
    }
}

當您執行整合測試時,若要產生與整合測試一致的數據,會很有用。 能夠從頭開始建立所有專案,包括容器上執行的 SQL Server 實例,非常適合測試環境。

EF Core InMemory 資料庫與以容器身分執行的 SQL Server

執行測試的另一個好選擇是使用 Entity Framework InMemory 資料庫提供者。 您可以在 Web API 專案中 Startup 類別的 ConfigureServices 方法中指定該組態:

public class Startup
{
    // Other Startup code ...
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IConfiguration>(Configuration);
        // DbContext using an InMemory database provider
        services.AddDbContext<CatalogContext>(opt => opt.UseInMemoryDatabase());
        //(Alternative: DbContext using a SQL Server provider
        //services.AddDbContext<CatalogContext>(c =>
        //{
            // c.UseSqlServer(Configuration["ConnectionString"]);
            //
        //});
    }

    // Other Startup code ...
}

不過,這裡有一個重要的條件。 記憶體內部資料庫不支援特定資料庫特定的許多條件約束。 例如,您可能會在 EF Core 模型中的數據行上新增唯一索引,並針對記憶體內部資料庫撰寫測試,以確認它不會讓您新增重複的值。 但是當您使用記憶體內部資料庫時,您無法處理資料行上的唯一索引。 因此,記憶體內部資料庫的行為與實際的 SQL Server 資料庫不完全相同,它不會模擬資料庫特定的條件約束。

即便如此,記憶體內部資料庫仍然適用於測試和原型設計。 但是,如果您想要建立將特定資料庫實作行為納入考慮的精確整合測試,則必須使用 SQL Server 之類的真實資料庫。 為此,在容器中執行 SQL Server 是絕佳的選擇,且比 EF Core InMemory 資料庫提供者更精確。

使用在容器中執行的 Redis 快取服務

您可以在容器上執行 Redis,特別是針對開發和測試,以及概念證明案例。 此情境非常方便,因為您可以讓所有依賴項都運行在容器上,不僅適用於本地開發機器,還適用於 CI/CD 管線中的測試環境。

不過,當您在生產環境中執行 Redis 時,最好尋找如 Redis Microsoft Azure 等高可用性解決方案,其會以 PaaS(平臺即服務)的形式執行。 在您的程式代碼中,您只需要變更連接字串。

Redis 提供具有 Redis 的 Docker 映像。 此網址可從 Docker Hub 取得該映射:

https://hub.docker.com/_/redis/

您可以在命令提示字元中執行下列 Docker CLI 命令,直接執行 Docker Redis 容器:

docker run --name some-redis -d redis

Redis 映像包含對 6379 埠的公開(Redis 所使用的埠),因此標準容器連結能讓該埠自動對被連結的容器可用。

在 eShopOnContainers 中, basket-api 微服務會使用以容器身分執行的 Redis 快取。 該 basketdata 容器定義為多容器 docker-compose.yml 檔案的一部分,如下列範例所示:

#docker-compose.yml file
#...
  basketdata:
    image: redis
    expose:
      - "6379"

docker-compose.yml中的這個程式代碼會根據 redis 映像定義名為 basketdata 的容器,並在內部發佈埠 6379。 此設定表示它只能從 Docker 主機內執行的其他容器存取。

最後,在 docker-compose.override.yml 檔案中, basket-api eShopOnContainers 範例的微服務會定義要用於該 Redis 容器的連接字串:

  basket-api:
    environment:
      # Other data ...
      - ConnectionString=basketdata
      - EventBusConnection=rabbitmq

如前所述,微服務 basketdata 的名稱會由 Docker 的內部網路 DNS 解析。