C# / .NET Деплой и наблюдаемость .NET: publish, Docker, OpenTelemetry и перф
0%

Деплой и наблюдаемость .NET: publish, Docker, OpenTelemetry и перф

Деплой и наблюдаемость .NET

Код, прошедший тесты, ещё не приносит пользы — его нужно собрать в артефакт, доставить и наблюдать в проде. Эта статья про то, как превратить приложение в бинарник или контейнер и как понять, что происходит внутри работающего сервиса.

Публикация: варианты сборки артефакта

dotnet publish создаёт готовый к запуску набор файлов. Есть три основных режима.

1. Framework-dependent (FDD) — самый лёгкий артефакт; требует установленного .NET runtime на целевой машине.

dotnet publish -c Release -o out
# out/ содержит .dll и зависимости; запуск: dotnet MyApp.dll

2. Self-contained (SCD) — тащит runtime внутри; работает на машине без .NET, но артефакт большой (десятки МБ). Указываете RID (runtime identifier) целевой платформы.

dotnet publish -c Release -r linux-x64 --self-contained true -o out
# out/ содержит нативный исполняемый файл + весь runtime

3. Single-file / trimmed — упаковать в один файл и обрезать неиспользуемый код:

dotnet publish -c Release -r linux-x64 --self-contained true \
  -p:PublishSingleFile=true -p:PublishTrimmed=true

Trimming уменьшает размер, но опасен для кода, активно использующего рефлексию — тестируйте результат. Как выбрать: для контейнеров обычно FDD поверх официального runtime-образа (меньше слой, общий базовый образ), для standalone-утилит и десктопа — self-contained.

Native AOT (кратко)

Native AOT компилирует приложение в нативный бинарник заранее, без JIT. Результат: мгновенный старт (важно для serverless и CLI), меньше памяти, нет зависимости от runtime. Цена: ограничения на рефлексию и динамическую генерацию кода, дольше сборка.

dotnet publish -c Release -r linux-x64 -p:PublishAot=true

С .NET 8+ Minimal API и многие библиотеки AOT-совместимы. Отлично подходит для маленьких высоконагруженных сервисов и функций, где важен холодный старт. Не всё совместимо — проверяйте предупреждения AOT-анализатора. Подробно: learn.microsoft.com/dotnet/core/deploying/native-aot.

Контейнеризация для ASP.NET Core

Стандарт доставки — Docker. Каноничный подход — multi-stage build: собираем в образе с SDK, а запускаем в лёгком образе с одним runtime.

# --- Этап сборки ---
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src

# Сначала копируем только csproj и восстанавливаем — кэш слоёв Docker
COPY ["src/MyApp.Api/MyApp.Api.csproj", "src/MyApp.Api/"]
RUN dotnet restore "src/MyApp.Api/MyApp.Api.csproj"

# Теперь весь код и публикация
COPY . .
RUN dotnet publish "src/MyApp.Api/MyApp.Api.csproj" -c Release -o /app/publish \
    --no-restore /p:UseAppHost=false

# --- Этап рантайма (лёгкий образ, только runtime) ---
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
WORKDIR /app
COPY --from=build /app/publish .

# Не root — базовое требование безопасности
USER $APP_UID
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]

Важные детали:

  • Порядок слоёв. Копирование .csproj и restore до копирования всего кода позволяет Docker кэшировать восстановление пакетов — сборка при изменении только кода становится мгновенной.
  • chiseled-образы (mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled) — минимальные, без шелла и пакетного менеджера, меньше поверхность атаки.
  • Не root. Официальные образы .NET 8+ определяют $APP_UID для непривилегированного пользователя.
  • Альтернатива Dockerfile — dotnet publish -t:PublishContainer (встроенная сборка образов SDK, без Dockerfile вовсе) или Buildpacks.

Health checks

Оркестратор (Kubernetes) должен знать, жив ли контейнер и готов ли принимать трафик. Для этого — health checks.

builder.Services.AddHealthChecks()
    .AddNpgSql(connectionString, name: "db")       // проверка БД
    .AddCheck("self", () => HealthCheckResult.Healthy());

var app = builder.Build();

// liveness — процесс жив (не проверяет зависимости)
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false
});
// readiness — готов принимать трафик (проверяет БД и т.п.)
app.MapHealthChecks("/health/ready");

Различайте liveness (перезапустить, если завис) и readiness (не слать трафик, пока зависимости не готовы). Путать их опасно: если readiness падает при недоступности БД и вы повесили на него liveness — Kubernetes начнёт бесконечно перезапускать здоровый под, ухудшая ситуацию.

Наблюдаемость: три столпа

Наблюдаемость (observability) отвечает на вопрос «что происходит внутри системы, судя по её выходным сигналам». Три столпа:

Современный .NET — OpenTelemetry-native: логи, метрики и трейсинг строятся на встроенных API (ILogger, System.Diagnostics.Metrics, System.Diagnostics.Activity), а OpenTelemetry экспортирует их куда угодно по стандарту OTLP.

Логи: ILogger и структурное логирование

ILogger<T> — встроенная абстракция. Ключевой принцип — структурное логирование: не склеивайте значения в строку, передавайте их как именованные поля. Тогда лог можно фильтровать и агрегировать по полям.

public class OrderService(ILogger<OrderService> logger)
{
    public async Task PlaceAsync(Order order, CancellationToken ct)
    {
        // ✅ структурно: OrderId и Total станут отдельными полями в JSON-логе
        logger.LogInformation("Placing order {OrderId} for {Total}", order.Id, order.Total);
        // ❌ так теряется структура:
        // logger.LogInformation($"Placing order {order.Id} for {order.Total}");
    }
}

Обратите внимание: {OrderId} — не интерполяция, а плейсхолдер шаблона; значения идут аргументами и сохраняются как поля. Для продакшена популярен Serilog — гибкие sink-и (файл, Seq, Elasticsearch), обогащение контекстом:

builder.Host.UseSerilog((ctx, cfg) => cfg
    .ReadFrom.Configuration(ctx.Configuration)
    .Enrich.FromLogContext()
    .WriteTo.Console(new Serilog.Formatting.Compact.CompactJsonFormatter()));

В контейнерах логируйте в stdout в JSON — оркестратор и агрегатор соберут сами. Не пишите в файлы внутри контейнера. Правильные уровни: Information для бизнес-событий, Warning для аномалий, Error для сбоев с исключением; не логируйте секреты и PII.

Метрики

Метрики — числовые агрегаты (счётчики, гистограммы, gauge). Встроенный API — System.Diagnostics.Metrics.Meter:

public sealed class OrderMetrics
{
    private readonly Counter<long> _placed;
    private readonly Histogram<double> _duration;

    public OrderMetrics(IMeterFactory factory)
    {
        var meter = factory.Create("MyApp.Orders");
        _placed = meter.CreateCounter<long>("orders.placed");
        _duration = meter.CreateHistogram<double>("orders.duration", unit: "ms");
    }

    public void OrderPlaced(double ms)
    {
        _placed.Add(1);
        _duration.Record(ms);
    }
}

ASP.NET Core, HttpClient, EF Core уже эмитят богатые встроенные метрики (латентность запросов, коды ответов). Экспортируйте их в Prometheus/OTLP и стройте дашборды и алерты по «золотым сигналам»: latency, traffic, errors, saturation.

Распределённый трейсинг

Трейс показывает путь одного запроса через все сервисы — где потрачено время, где упало. В .NET трейсинг строится на Activity/ActivitySource; контекст (trace id) автоматически прокидывается через HttpClient и заголовки W3C Trace Context.

private static readonly ActivitySource Source = new("MyApp.Orders");

public async Task PlaceAsync(Order order, CancellationToken ct)
{
    using var activity = Source.StartActivity("PlaceOrder");
    activity?.SetTag("order.id", order.Id);
    // ... вложенные вызовы (БД, HTTP) автоматически станут дочерними span-ами
}

Собираем всё через OpenTelemetry

builder.Services.AddOpenTelemetry()
    .ConfigureResource(r => r.AddService("MyApp.Api"))
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddOtlpExporter())                       // отправка в коллектор/бэкенд
    .WithMetrics(m => m
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()              // GC, потоки, память
        .AddMeter("MyApp.Orders")
        .AddOtlpExporter());

Один стандарт (OTLP), любой бэкенд (Jaeger, Tempo, Prometheus, Grafana, Honeycomb, Datadog). Не привязывайтесь к вендору — эмитьте по OpenTelemetry. Для локальной разработки удобен .NET Aspire dashboard — из коробки показывает логи, метрики и трейсы. Подробно: opentelemetry.io/docs/languages/net.

Производительность и профилирование

Наблюдаемость сказала «медленно» — дальше нужны инструменты для «почему». В .NET они кросс-платформенные и ставятся как global tools:

dotnet tool install -g dotnet-counters
dotnet tool install -g dotnet-trace
dotnet tool install -g dotnet-dump
dotnet tool install -g dotnet-gcdump

# Живые метрики процесса: CPU, память, GC, очередь пула потоков
dotnet-counters monitor -p <PID> --counters System.Runtime,Microsoft.AspNetCore.Hosting

# Собрать трассу для анализа в PerfView/Speedscope
dotnet-trace collect -p <PID> --duration 00:00:30

# Дамп памяти и анализ утечек
dotnet-gcdump collect -p <PID>

Что смотреть в первую очередь:

  • ThreadPool Queue Length растёт — где-то блокируете потоки (sync-over-async из статьи про конкурентность), пул голодает.
  • Gen 2 / LOH коллекции часты — много долгоживущих или крупных аллокаций; оптимизируйте аллокации, пулинг.
  • Высокий CPU в GC — давление на кучу; ищите горячие аллокации.

Для бенчмаркинга кода — BenchmarkDotNet, золотой стандарт микробенчмарков в .NET (корректно прогревает, статистически обрабатывает, показывает аллокации). Не измеряйте производительность Stopwatch-ом в цикле — получите шум. Глубоко про перф и внутренности пишет Stephen Toub в ежегодных «Performance Improvements in .NET» (devblogs.microsoft.com/dotnet).

Что дальше

SDLC и лучшие ресурсы — как всё это складывается в жизненный цикл разработки: CI/CD, релизы, безопасность зависимостей и курированный список лучших источников по .NET.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов