Använda en databasserver som körs som en container
Dricks
Det här innehållet är ett utdrag från eBook, .NET Microservices Architecture for Containerized .NET Applications, tillgängligt på .NET Docs eller som en kostnadsfri nedladdningsbar PDF som kan läsas offline.
Du kan ha dina databaser (SQL Server, PostgreSQL, MySQL osv.) på vanliga fristående servrar, i lokala kluster eller i PaaS-tjänster i molnet som Azure SQL DB. För utvecklings- och testmiljöer är det dock praktiskt att låta dina databaser köras som containrar, eftersom du inte har något externt beroende och bara kör docker-compose up
kommandot startar hela programmet. Att ha dessa databaser som containrar är också bra för integreringstester, eftersom databasen startas i containern och alltid fylls med samma exempeldata, så att tester kan vara mer förutsägbara.
SQL Server körs som en container med en mikrotjänstrelaterad databas
I eShopOnContainers finns det en container med namnet sqldata
, enligt definitionen i filen docker-compose.yml som kör en SQL Server för Linux-instans med SQL-databaserna för alla mikrotjänster som behöver en.
En viktig punkt i mikrotjänster är att varje mikrotjänst äger sina relaterade data, så den bör ha en egen databas. Databaserna kan dock finnas var som helst. I det här fallet finns de alla i samma container för att hålla Docker-minneskraven så låga som möjligt. Tänk på att detta är en tillräckligt bra lösning för utveckling och kanske testning men inte för produktion.
SQL Server-containern i exempelprogrammet konfigureras med följande YAML-kod i filen docker-compose.yml, som körs när du kör docker-compose up
. Observera att YAML-koden har konsoliderad konfigurationsinformation från den generiska docker-compose.yml-filen och docker-compose.override.yml-filen. (Vanligtvis skulle du separera miljöinställningarna från den grundläggande eller statiska information som är relaterad till SQL Server-avbildningen.)
sqldata:
image: mcr.microsoft.com/mssql/server:2017-latest
environment:
- SA_PASSWORD=Pass@word
- ACCEPT_EULA=Y
ports:
- "5434:1433"
På liknande sätt kan följande docker run
kommando i stället för att använda docker-compose
köra containern:
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Pass@word' -p 5433:1433 -d mcr.microsoft.com/mssql/server:2017-latest
Men om du distribuerar ett program med flera containrar som eShopOnContainers är det enklare att använda docker-compose up
kommandot så att det distribuerar alla nödvändiga containrar för programmet.
När du startar den här SQL Server-containern för första gången initierar containern SQL Server med det lösenord som du anger. När SQL Server körs som en container kan du uppdatera databasen genom att ansluta via en vanlig SQL-anslutning, till exempel från SQL Server Management Studio, Visual Studio eller C#-kod.
eShopOnContainers-programmet initierar varje mikrotjänstdatabas med exempeldata genom att så den med data vid start, enligt beskrivningen i följande avsnitt.
Att SQL Server körs som en container är inte bara användbart för en demo där du kanske inte har åtkomst till en instans av SQL Server. Som nämnts är det också bra för utvecklings- och testmiljöer så att du enkelt kan köra integreringstester från en ren SQL Server-avbildning och kända data genom att seeda nya exempeldata.
Ytterligare resurser
Kör SQL Server Docker-avbildningen på Linux, Mac eller Windows
https://learn.microsoft.com/sql/linux/sql-server-linux-setup-dockerAnslut och fråga SQL Server på Linux med sqlcmd
https://learn.microsoft.com/sql/linux/sql-server-linux-connect-and-query-sqlcmd
Seeding med testdata vid start av webbprogram
Om du vill lägga till data i databasen när programmet startas kan du lägga till kod som följande i Main
-metoden i Program
klassen för webb-API-projektet:
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();
}
}
Det finns en viktig varning vid tillämpning av migreringar och seeding av en databas under containerstart. Eftersom databasservern kanske inte är tillgänglig av någon anledning måste du hantera återförsök i väntan på att servern ska vara tillgänglig. Den här logiken MigrateDbContext()
för återförsök hanteras av tilläggsmetoden, enligt följande kod:
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;
}
Följande kod i den anpassade klassen CatalogContextSeed fyller i data.
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" }
};
}
}
När du kör integreringstester är det användbart att ha ett sätt att generera data som är konsekventa med dina integreringstester. Att kunna skapa allt från grunden, inklusive en instans av SQL Server som körs på en container, är bra för testmiljöer.
EF Core InMemory-databas jämfört med SQL Server som körs som en container
Ett annat bra val när du kör tester är att använda Entity Framework InMemory-databasprovidern. Du kan ange den konfigurationen i metoden ConfigureServices för startklassen i ditt webb-API-projekt:
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 ...
}
Det finns dock en viktig fångst. Den minnesinterna databasen stöder inte många begränsningar som är specifika för en viss databas. Du kan till exempel lägga till ett unikt index på en kolumn i EF Core-modellen och skriva ett test mot din minnesintern databas för att kontrollera att du inte kan lägga till ett duplicerat värde. Men när du använder den minnesinterna databasen kan du inte hantera unika index i en kolumn. Därför fungerar inte den minnesinterna databasen på exakt samma sätt som en riktig SQL Server-databas– den emulerar inte databasspecifika begränsningar.
Trots detta är en minnesintern databas fortfarande användbar för testning och prototyper. Men om du vill skapa korrekta integreringstester som tar hänsyn till beteendet för en specifik databasimplementering måste du använda en riktig databas som SQL Server. Därför är det bra att köra SQL Server i en container och mer exakt än EF Core InMemory-databasprovidern.
Använda en Redis-cachetjänst som körs i en container
Du kan köra Redis på en container, särskilt för utveckling och testning och för konceptbevisscenarier. Det här scenariot är praktiskt eftersom du kan ha alla dina beroenden som körs på containrar, inte bara för dina lokala utvecklingsdatorer, utan för dina testmiljöer i DINA CI/CD-pipelines.
Men när du kör Redis i produktion är det bättre att leta efter en lösning med hög tillgänglighet som Redis Microsoft Azure, som körs som en PaaS (plattform som en tjänst). I koden behöver du bara ändra dina anslutningssträng.
Redis tillhandahåller en Docker-avbildning med Redis. Avbildningen är tillgänglig från Docker Hub på den här URL:en:
https://hub.docker.com/_/redis/
Du kan köra en Docker Redis-container direkt genom att köra följande Docker CLI-kommando i kommandotolken:
docker run --name some-redis -d redis
Redis-avbildningen innehåller expose:6379 (porten som används av Redis), så standardcontainerlänkning gör den automatiskt tillgänglig för de länkade containrarna.
I eShopOnContainers basket-api
använder mikrotjänsten en Redis-cache som körs som en container. Containern basketdata
definieras som en del av filen med flera containrar docker-compose.yml , enligt följande exempel:
#docker-compose.yml file
#...
basketdata:
image: redis
expose:
- "6379"
Den här koden i docker-compose.yml definierar en container med namnet basketdata
baserat på redis-avbildningen och publicerar port 6379 internt. Den här konfigurationen innebär att den endast kommer att vara tillgänglig från andra containrar som körs i Docker-värden.
I filen docker-compose.override.ymlbasket-api
definierar mikrotjänsten för exemplet eShopOnContainers anslutningssträng som ska användas för redis-containern:
basket-api:
environment:
# Other data ...
- ConnectionString=basketdata
- EventBusConnection=rabbitmq
Som tidigare nämnts löses namnet på mikrotjänsten basketdata
av Docker interna nätverks-DNS.