Udostępnij za pośrednictwem


Używanie serwera bazy danych uruchomionego jako kontener

Wskazówka

Ta treść jest fragmentem eBooka "Architektura mikrousług .NET dla konteneryzowanych aplikacji .NET", dostępnego na .NET Docs lub jako bezpłatny plik PDF do pobrania i czytania w trybie offline.

Miniatura okładki eBooka „Architektura mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET”.

Możesz mieć bazy danych (SQL Server, PostgreSQL, MySQL itp.) na zwykłych serwerach autonomicznych, w klastrach lokalnych lub w usługach PaaS w chmurze, takich jak usługa Azure SQL DB. Jednak w przypadku środowisk programistycznych i testowych uruchamianie baz danych jako kontenerów jest wygodne, ponieważ nie ma żadnych zależności zewnętrznych, a samo uruchomienie polecenia docker-compose up rozpoczyna działanie całej aplikacji. Posiadanie tych baz danych jako kontenerów jest również doskonałe do testów integracji, ponieważ baza danych jest uruchamiana w kontenerze i zawsze jest wypełniana tymi samymi przykładowymi danymi, dzięki czemu testy mogą być bardziej przewidywalne.

W usłudze eShopOnContainers istnieje kontener o nazwie sqldata, zgodnie z definicją w pliku docker-compose.yml , który uruchamia wystąpienie programu SQL Server dla systemu Linux z bazami danych SQL dla wszystkich mikrousług, które ich potrzebują.

Kluczowym punktem w mikrousługach jest to, że każda mikrousługa jest właścicielem powiązanych danych, więc powinna mieć własną bazę danych. Bazy danych mogą jednak znajdować się w dowolnym miejscu. W takim przypadku wszystkie znajdują się w tym samym kontenerze, aby zapewnić możliwie najmniejsze wymagania dotyczące pamięci platformy Docker. Należy pamiętać, że jest to wystarczająco dobre rozwiązanie do programowania i, być może, testowanie, ale nie dla środowiska produkcyjnego.

Kontener programu SQL Server w przykładowej aplikacji jest skonfigurowany przy użyciu następującego kodu YAML w pliku docker-compose.yml, który jest wykonywany po uruchomieniu polecenia docker-compose up. Należy pamiętać, że kod YAML zawiera skonsolidowane informacje o konfiguracji z ogólnego pliku docker-compose.yml i pliku docker-compose.override.yml. (Zazwyczaj ustawienia środowiska należy oddzielić od podstawowych lub statycznych informacji związanych z obrazem programu SQL Server).

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

W podobny sposób, zamiast używać docker-compose, można uruchomić ten kontener za pomocą następującego polecenia docker run.

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

Jeśli jednak wdrażasz aplikację z wieloma kontenerami, na przykład eShopOnContainers, bardziej wygodne jest użycie docker-compose up polecenia , aby wdrożyć wszystkie wymagane kontenery dla aplikacji.

Po pierwszym uruchomieniu tego kontenera programu SQL Server kontener inicjuje program SQL Server przy użyciu podanego hasła. Po uruchomieniu programu SQL Server jako kontenera możesz zaktualizować bazę danych, łącząc się za pośrednictwem dowolnego zwykłego połączenia SQL, takiego jak program SQL Server Management Studio, program Visual Studio lub kod języka C#.

Aplikacja eShopOnContainers inicjuje każdą bazę danych mikrousług z przykładowymi danymi, uzupełniając je na starcie, jak opisano w poniższej sekcji.

Posiadanie SQL Server uruchomionego jako kontener jest użyteczne nie tylko w przypadku demonstracji, gdy nie masz dostępu do instancji SQL Server. Jak wspomniano, doskonale nadaje się również do środowisk programistycznych i testowych, dzięki czemu można łatwo uruchamiać testy integracji, zaczynając od czystego obrazu programu SQL Server i znanych danych, rozmieszczając nowe przykładowe dane.

Dodatkowe zasoby

Inicjowanie za pomocą danych testowych podczas uruchamiania aplikacji webowej

Aby dodać dane do bazy danych podczas uruchamiania aplikacji, możesz dodać kod podobny do poniższego do metody w klasie Main projektu interfejsu Web API:

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();
    }
}

Podczas stosowania migracji i wypełniania bazy danych podczas uruchamiania kontenera istnieje ważne zastrzeżenie. Ponieważ serwer bazy danych może nie być dostępny z jakiegokolwiek powodu, należy obsługiwać ponawianie prób podczas oczekiwania na dostępność serwera. Logika ponawiania jest obsługiwana przez metodę rozszerzenia MigrateDbContext(), jak pokazano w poniższym kodzie.

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;
}

Poniższy kod w klasie custom CatalogContextSeed wypełnia dane.

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" }
        };
    }
}

Podczas uruchamiania testów integracji przydatne jest generowanie danych spójnych z testami integracji. Możliwość tworzenia wszystkich elementów od podstaw, w tym wystąpienia programu SQL Server uruchomionego w kontenerze, doskonale nadaje się do środowisk testowych.

Baza danych programu EF Core InMemory a program SQL Server uruchomiony jako kontener

Innym dobrym wyborem podczas uruchamiania testów jest użycie dostawcy bazy danych Programu Entity Framework InMemory. Tę konfigurację można określić w metodzie ConfigureServices klasy Startup w projekcie internetowego interfejsu API:

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 ...
}

Jest jednak ważny haczyk. Baza danych w pamięci nie obsługuje wielu ograniczeń specyficznych dla określonej bazy danych. Na przykład możesz dodać unikatowy indeks do kolumny w modelu EF Core i napisać test dla bazy danych w pamięci, aby sprawdzić, czy nie pozwala dodać zduplikowanej wartości. Jednak w przypadku korzystania z bazy danych w pamięci nie można obsługiwać unikatowych indeksów w kolumnie. W związku z tym baza danych w pamięci nie zachowuje się dokładnie tak samo jak rzeczywista baza danych programu SQL Server — nie emuluje ograniczeń specyficznych dla bazy danych.

Mimo to baza danych w pamięci jest nadal przydatna do testowania i tworzenia prototypów. Jeśli jednak chcesz utworzyć dokładne testy integracji, które uwzględniają zachowanie określonej implementacji bazy danych, musisz użyć prawdziwej bazy danych, takiej jak SQL Server. W tym celu uruchomienie programu SQL Server w kontenerze jest doskonałym wyborem i dokładniejsze niż dostawca bazy danych EF Core InMemory.

Korzystanie z usługi Redis Cache uruchomionej w kontenerze

Usługę Redis można uruchamiać w kontenerze, szczególnie na potrzeby programowania i testowania oraz scenariuszy weryfikacji koncepcji. Ten scenariusz jest korzystny, ponieważ możesz mieć wszystkie zależności działające na kontenerach — nie tylko dla lokalnych maszyn deweloperskich, ale także dla środowisk testowych w potokach CI/CD.

Jednak po uruchomieniu usługi Redis w środowisku produkcyjnym lepiej jest wyszukać rozwiązanie o wysokiej dostępności, takie jak Redis Microsoft Azure, które działa jako usługa PaaS (platforma jako usługa). W kodzie wystarczy zmienić parametry połączenia.

Redis udostępnia obraz Dockera z Redis. Ten obraz jest dostępny w usłudze Docker Hub pod tym adresem URL:

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

Kontener Docker Redis można uruchomić bezpośrednio, wykonując następujące polecenie CLI Docker w wierszu polecenia.

docker run --name some-redis -d redis

Obraz usługi Redis zawiera element expose:6379 (port używany przez usługę Redis), dlatego standardowe łączenie kontenerów spowoduje automatyczne udostępnienie go połączonym kontenerom.

W aplikacji eShopOnContainers basket-api mikrousługa używa pamięci podręcznej Redis działającej jako kontener. Ten basketdata kontener jest definiowany jako część pliku docker-compose.yml z wieloma kontenerami, jak pokazano w poniższym przykładzie:

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

Ten kod w docker-compose.yml definiuje kontener o nazwie basketdata, który bazuje na obrazie redis i publikuje port 6379 wewnętrznie. Ta konfiguracja oznacza, że będzie ona dostępna tylko z innych kontenerów uruchomionych na hoście platformy Docker.

Na koniec w pliku docker-compose.override.yml mikrousługa z przykładowego projektu eShopOnContainers definiuje ciąg połączenia dla tego kontenera Redis.

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

Jak wspomniano wcześniej, nazwa mikrousługi basketdata jest rozpoznawana przez wewnętrzną sieć platformy Docker DNS.