Delen via


Een databaseserver gebruiken die wordt uitgevoerd als een container

Aanbeveling

Deze inhoud is een fragment uit het eBook, .NET Microservices Architecture for Containerized .NET Applications, beschikbaar op .NET Docs of als een gratis downloadbare PDF die offline kan worden gelezen.

.NET Microservices Architectuur voor Gecontaineriseerde .NET Toepassingen eBook omslagthumbnail.

U kunt uw databases (SQL Server, PostgreSQL, MySQL, enzovoort) hebben op normale zelfstandige servers, in on-premises clusters of in PaaS-services in de cloud, zoals Azure SQL DB. Voor ontwikkel- en testomgevingen is het echter handig om uw databases uit te voeren als containers, omdat u geen externe afhankelijkheid hebt en de opdracht gewoon de docker-compose up hele toepassing start. Het gebruik van deze databases als containers is ook handig voor integratietests, omdat de database wordt gestart in de container en altijd wordt gevuld met dezelfde voorbeeldgegevens, zodat tests voorspelbaarder kunnen zijn.

In eShopOnContainers is er een container met de naam sqldata, zoals gedefinieerd in het docker-compose.yml-bestand , die een SQL Server voor Linux-exemplaar uitvoert met de SQL-databases voor alle microservices die er een nodig hebben.

Een belangrijk punt in microservices is dat elke microservice eigenaar is van de gerelateerde gegevens, zodat deze een eigen database moet hebben. De databases kunnen echter overal zijn. In dit geval bevinden ze zich allemaal in dezelfde container om docker-geheugenvereisten zo laag mogelijk te houden. Houd er rekening mee dat dit een goede oplossing is voor ontwikkeling en, misschien, testen maar niet voor productie.

De SQL Server-container in de voorbeeldtoepassing is geconfigureerd met de volgende YAML-code in het docker-compose.yml-bestand, dat wordt uitgevoerd wanneer u deze uitvoert docker-compose up. De YAML-code bevat geconsolideerde configuratiegegevens uit het algemene docker-compose.yml-bestand en het docker-compose.override.yml-bestand. (Meestal scheidt u de omgevingsinstellingen van de basis- of statische informatie met betrekking tot de SQL Server-installatiekopieën.)

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

Op een vergelijkbare manier kan met de volgende docker-compose opdracht in plaats van docker rundie container worden uitgevoerd:

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

Als u echter een toepassing met meerdere containers implementeert, zoals eShopOnContainers, is het handiger om de docker-compose up opdracht te gebruiken, zodat alle vereiste containers voor de toepassing worden geïmplementeerd.

Wanneer u deze SQL Server-container voor het eerst start, initialiseert de container SQL Server met het wachtwoord dat u opgeeft. Zodra SQL Server wordt uitgevoerd als een container, kunt u de database bijwerken door verbinding te maken via een gewone SQL-verbinding, zoals sql Server Management Studio, Visual Studio of C#-code.

De eShopOnContainers-toepassing initialiseert elke microservicedatabase met voorbeeldgegevens door deze te seeden met gegevens bij het opstarten, zoals wordt uitgelegd in de volgende sectie.

Het uitvoeren van SQL Server als container is niet alleen handig voor een demo waar u mogelijk geen toegang hebt tot een exemplaar van SQL Server. Zoals vermeld, is het ook ideaal voor ontwikkel- en testomgevingen, zodat u eenvoudig integratietests kunt uitvoeren vanaf een schone SQL Server-installatiekopieën en bekende gegevens door nieuwe voorbeeldgegevens te seeden.

Aanvullende bronnen

Zaaien met testgegevens bij het opstarten van de webtoepassing

Als u gegevens wilt toevoegen aan de database wanneer de toepassing wordt gestart, kunt u code als de volgende toevoegen aan de methode in de MainProgram klasse van het web-API-project:

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

Er is een belangrijke kanttekening bij het toepassen van migraties en het seeden van een database tijdens het opstarten van een container. Omdat de databaseserver om welke reden dan ook niet beschikbaar is, moet u nieuwe pogingen afhandelen terwijl wordt gewacht tot de server beschikbaar is. Deze logica voor opnieuw proberen wordt verwerkt door de MigrateDbContext() extensiemethode, zoals wordt weergegeven in de volgende code:

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

De volgende code in de aangepaste klasse CatalogContextSeed vult de gegevens in.

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

Wanneer u integratietests uitvoert, is het handig om gegevens te genereren die consistent zijn met uw integratietests. U kunt alles helemaal zelf maken, inclusief een exemplaar van SQL Server dat wordt uitgevoerd in een container, is ideaal voor testomgevingen.

EF Core InMemory-database versus SQL Server die als een container wordt uitgevoerd

Een andere goede keuze bij het uitvoeren van tests is het gebruik van de Entity Framework InMemory-databaseprovider. U kunt deze configuratie opgeven in de methode ConfigureServices van de opstartklasse in uw web-API-project:

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

Er is echter een belangrijke vangst. De in-memory database biedt geen ondersteuning voor veel beperkingen die specifiek zijn voor een bepaalde database. U kunt bijvoorbeeld een unieke index toevoegen aan een kolom in uw EF Core-model en een test schrijven op basis van uw in-memory database om te controleren of u hiermee geen dubbele waarde kunt toevoegen. Maar wanneer u de in-memory database gebruikt, kunt u geen unieke indexen voor een kolom verwerken. Daarom gedraagt de in-memory database zich niet precies hetzelfde als een echte SQL Server-database. Het emuleren van databasespecifieke beperkingen is daarom niet mogelijk.

Toch is een in-memory database nog steeds nuttig voor het testen en maken van prototypen. Maar als u nauwkeurige integratietests wilt maken die rekening houden met het gedrag van een specifieke database-implementatie, moet u een echte database zoals SQL Server gebruiken. Daarom is het uitvoeren van SQL Server in een container een uitstekende keuze en nauwkeuriger dan de EF Core InMemory-databaseprovider.

Een Redis-cacheservice gebruiken die wordt uitgevoerd in een container

U kunt Redis uitvoeren op een container, met name voor ontwikkeling en testen en voor proof-of-concept-scenario's. Dit scenario is handig, omdat u al uw afhankelijkheden kunt uitvoeren op containers, niet alleen voor uw lokale ontwikkelcomputers, maar voor uw testomgevingen in uw CI/CD-pijplijnen.

Wanneer u Redis echter in productie uitvoert, is het beter om te zoeken naar een oplossing met hoge beschikbaarheid, zoals Redis Microsoft Azure, die wordt uitgevoerd als een PaaS (Platform as a Service). In uw code hoeft u alleen uw verbindingsreeksen te wijzigen.

Redis biedt een Docker-image met Redis. Deze afbeelding is beschikbaar via Docker Hub op deze URL:

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

U kunt rechtstreeks een Docker Redis-container uitvoeren door de volgende Docker CLI-opdracht uit te voeren in de opdrachtprompt:

docker run --name some-redis -d redis

De Redis-installatiekopie bevat expose:6379 (de poort die door Redis wordt gebruikt), zodat standaardcontainerkoppelingen deze automatisch beschikbaar maken voor de gekoppelde containers.

In eShopOnContainers gebruikt de basket-api microservice een Redis-cache die als container wordt uitgevoerd. Deze basketdata container wordt gedefinieerd als onderdeel van het bestand met meerdere containers docker-compose.yml, zoals wordt weergegeven in het volgende voorbeeld:

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

Deze code in de docker-compose.yml definieert een container met de naam basketdata op basis van de redis-image en publiceert poort 6379 intern. Deze configuratie betekent dat deze alleen toegankelijk is vanuit andere containers die worden uitgevoerd in de Docker-host.

Ten slotte definieert de microservice voor het voorbeeld eShopOnContainers in het docker-compose.override.yml bestand basket-api de verbindingsreeks die moet worden gebruikt voor die Redis-container:

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

Zoals eerder vermeld, wordt de naam van de microservice basketdata omgezet door de DNS van het interne netwerk van Docker.