Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Подсказка
Это фрагмент из электронной книги «Архитектура микрослужб .NET для контейнеризованных приложений .NET», доступной в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно прочитать в автономном режиме.
Базы данных (SQL Server, PostgreSQL, MySQL и т. д.) можно использовать на обычных автономных серверах, в локальных кластерах или в службах PaaS в облаке, например в базе данных SQL Azure. Однако для сред разработки и тестирования базы данных, работающие как контейнеры, удобны, поскольку у вас нет внешних зависимостей и достаточно запустить команду docker-compose up
, чтобы запустить всё приложение. Наличие этих баз данных в качестве контейнеров также отлично подходит для тестов интеграции, так как база данных запускается в контейнере и всегда заполняется одинаковыми примерами данных, поэтому тесты могут быть более предсказуемыми.
SQL Server, работающий в качестве контейнера с базой данных, связанной с микрослужбами
В eShopOnContainers есть контейнер с именем sqldata
, как определено в файле docker-compose.yml , который запускает экземпляр SQL Server для Linux с базами данных SQL для всех микрослужб, которым требуется один.
Ключевой точкой микрослужб является то, что каждая микрослужба владеет своими связанными данными, поэтому она должна иметь собственную базу данных. Однако базы данных могут находиться в любом месте. В этом случае они находятся в одном контейнере, чтобы обеспечить максимально низкие требования к памяти Docker. Имейте в виду, что это достаточно хорошее решение для разработки и, возможно, тестирования, но не для рабочей среды.
Контейнер SQL Server в примере приложения настроен с помощью следующего YAML-кода в файле docker-compose.yml, который выполняется при запуске 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 и известных данных, заполняя новые образцы данных.
Дополнительные ресурсы
Запуск образа Docker SQL Server в Linux, Mac или Windows
https://learn.microsoft.com/sql/linux/sql-server-linux-setup-dockerПодключение и запрос SQL Server в Linux с помощью sqlcmd
https://learn.microsoft.com/sql/linux/sql-server-linux-connect-and-query-sqlcmd
Заполнение тестовых данных при запуске веб-приложения
Чтобы добавить данные в базу данных при запуске приложения, можно добавить следующий код Main
в метод в Program
классе проекта веб-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();
}
}
Существует важное замечание при применении миграций и заполнении базы данных во время запуска контейнера. Так как сервер базы данных может быть недоступен по какой-либо причине, необходимо обрабатывать повторные попытки во время ожидания доступности сервера. Эта логика повторных попыток обрабатывается методом 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. Эту конфигурацию можно указать в методе ConfigureServices класса Startup в проекте веб-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 ...
}
Есть одно важное условие. База данных в памяти не поддерживает множество ограничений, относящихся к определенной базе данных. Например, вы можете добавить уникальный индекс в столбец в модели EF Core и написать тест в базе данных в памяти, чтобы убедиться, что он не позволяет добавить дубликатное значение. Но при использовании базы данных в памяти нельзя обрабатывать уникальные индексы в столбце. Таким образом, база данных в памяти не ведет себя точно так же, как и реальная база данных SQL Server, она не эмулирует ограничения, связанные с базой данных.
Даже поэтому база данных в памяти по-прежнему полезна для тестирования и прототипирования. Но если вы хотите создать точные тесты интеграции, которые учитывают поведение конкретной реализации базы данных, необходимо использовать реальную базу данных, например SQL Server. Для этого запуск сервера SQL Server в контейнере является отличным выбором и более точным решением, чем использование поставщика базы данных EF Core InMemory.
Использование службы кэша Redis, работающей в контейнере
Redis можно запускать в контейнере, особенно для разработки и тестирования, а также для сценариев проверки концепции. Этот сценарий удобен, потому что все ваши зависимости могут работать на контейнерах, что подходит не только для локальных машин разработки, но и для тестовых сред в конвейерах CI/CD.
Однако при запуске Redis в рабочей среде лучше искать высокодоступное решение, например Redis Microsoft Azure, которое выполняется как PaaS (платформа как услуга). В коде просто необходимо изменить строки подключения.
Redis предоставляет образ Docker с Redis. Этот образ доступен из Docker Hub по этому URL-адресу:
https://hub.docker.com/_/redis/
Вы можете напрямую запустить контейнер Docker Redis, выполнив следующую команду Командной строки Docker в командной строке:
docker run --name some-redis -d redis
Образ Redis включает открытый порт:6379 (порт, используемый Redis), поэтому стандартное связывание контейнеров сделает его автоматически доступным для других контейнеров.
В eShopOnContainers микрослужба использует кэш Redis, basket-api
работающий в качестве контейнера. Этот basketdata
контейнер определяется как часть файла docker-compose.yml, содержащего несколько контейнеров, как показано в следующем примере:
#docker-compose.yml file
#...
basketdata:
image: redis
expose:
- "6379"
Этот код в docker-compose.yml определяет контейнер с именем basketdata
, основанный на образе Redis, и публикующий порт 6379 для внутреннего использования. Эта конфигурация означает, что она будет доступна только из других контейнеров, работающих в узле Docker.
Наконец, в файле docker-compose.override.yml микрослужба basket-api
для примера eShopOnContainers определяет строку подключения, используемую для этого контейнера Redis:
basket-api:
environment:
# Other data ...
- ConnectionString=basketdata
- EventBusConnection=rabbitmq
Как упоминалось ранее, имя микрослужбы basketdata
разрешается внутренней сетью Docker DNS.