Udostępnij za pośrednictwem


Używanie serwera bazy danych uruchomionego jako kontener

Napiwek

Ta zawartość jest fragmentem książki eBook, architektury mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET dostępnych na platformie .NET Docs lub jako bezpłatnego pliku PDF, który można odczytać w trybie offline.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

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 masz żadnej zależności zewnętrznej i po prostu uruchamia polecenie uruchamia docker-compose up całą aplikację. 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-composepolecenia , następujące docker run polecenie może uruchomić ten kontener:

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, rozmieszczając je przy użyciu danych podczas uruchamiania, jak wyjaśniono w poniższej sekcji.

Posiadanie programu SQL Server uruchomionego jako kontener nie jest tylko przydatne w przypadku pokazu, w którym być może nie masz dostępu do wystąpienia programu 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

Rozmieszczanie przy użyciu danych testowych podczas uruchamiania aplikacji internetowej

Aby dodać dane do bazy danych podczas uruchamiania aplikacji, możesz dodać kod podobny do Main poniższego do metody w Program klasie projektu internetowego interfejsu 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 rozmieszczania bazy danych podczas uruchamiania kontenera jest 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. Ta logika ponawiania jest obsługiwana przez metodę MigrateDbContext() rozszerzenia, 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 ...
}

Istnieje jednak ważny połowu. Baza danych w pamięci nie obsługuje wielu ograniczeń specyficznych dla określonej bazy danych. Na przykład możesz dodać unikatowy indeks w kolumnie w modelu EF Core i napisać test względem bazy danych w pamięci, aby sprawdzić, czy nie pozwala dodać zduplikowanych 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 wygodny, 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ągłej integracji/ciągłego wdrażania.

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.

Usługa Redis udostępnia obraz platformy Docker z usługą Redis. Ten obraz jest dostępny w usłudze Docker Hub pod tym adresem URL:

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

Kontener Usługi Docker Redis można uruchomić bezpośrednio, wykonując następujące polecenie interfejsu wiersza polecenia platformy 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 na podstawie obrazu redis i publikowanie portu 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 basket-api docker-compose.override.yml mikrousługa dla przykładu eShopOnContainers definiuje parametry połączenia do użycia 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.