Как мы придумали автоматизацию дампов .Net используя «sidecar»

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Привет, Хабр! 

Думаю, для вас не секрет, что в последние годы контейнеризация вышла в лидеры на DevOps благодаря своим возможностям, включая эффективное использование ресурсов и гибкость. Так что Microsoft и Docker потратили немало времени на создание удобной среды, в которой можно было бы провести запуск приложений .NET внутри контейнеров. 

Наша команда в разработке использует Kubernetes кластеры, в которых разворачиваются контейнеры на базе Linux систем с различными .Net приложениями и сервисами. Так что в какой-то момент мы встали перед вопросом, как проводить мониторинг не только контейнеров, но и дампов.

За помощью мы обратились к всемогущему интернету, и после нескольких часов изучения данного вопроса, наш выбор пал  на использование “sidecar” контейнеров.

Если же вы ранее не сталкивались с “sidecar” контейнерами, то могу немного пояснить. Sidecar-контейнер — это дополнительный контейнер, который запускается рядом с основным контейнером приложений внутри того же пода. Грубо говоря, эта схема позволяет добавить некоторые расширения функциональности приложения в основном контейнере без внесения в него дополнительных изменений. 

Наша команда применяла эту идею для профилирования / отладки контейнеров .NET Linux. Лично я выделил следующие преимущества этого подхода: 

  • Контейнеры приложений не нуждаются в повышенных привилегиях;

  • Образы контейнеров приложений остаются, в основном, без изменений. Они не раздуваются пакетами инструментов, которые не требуются для запуска приложений;

  • Профилирование не использует ресурсы контейнера приложения, которые обычно ограничиваются квотой.

Мы выводили все мониторинги приложений и контейнеров в отдельную систему мониторинга, поэтому нам нужно было автоматизировать создание дампов.

Из-за некоторых особенностей проекта стандартные образы dotnet-monitor различных версий не подходили или работали не корректно. Так, например, если в деплойменте стоит не одна, а несколько реплик для приложений, то дампы работают, увы, только на одной реплике.

Стандартные контейнеры dotnet-monitor (или dotnet/nightly/monitor) можно посмотреть на докерхабе тут.

Сразу предупреждаю, что они подойдут далеко не всем, особенно если учитывать версию .Net, специфику проекта, а также особенности основных контейнеров.

Увы, в нашем случае через sidecar не удалось реализовать корректную работу и создание дампов приложений дотнет с помощью имеющихся образов dotnet-monitor. Так что, подумав как следует, мы решили сделать sidecar контейнер для .Net 5

Пример докер файла:

FROM mcr.microsoft.com/dotnet/aspnet:5.0-bullseye-slim AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:5.0-bullseye-slim AS build
RUN mkdir /root/.dotnet/tools
RUN dotnet tool install dotnet-counters --global
RUN dotnet tool install dotnet-trace --global
RUN dotnet tool install dotnet-dump --global
RUN dotnet tool install dotnet-gcdump --global
FROM base AS final
WORKDIR /app
COPY --from=build /root/.dotnet/tools /root/.dotnet/tools
ENV PATH="/root/.dotnet/tools:${PATH}"
RUN apt-get update && apt-get install -y procps vim nano zip

Потом мы добавили в деплоймент сервиса в кубернейтс: 

shareProcessNamespace: true

Это поможет sidecar контейнеру видеть процессы основного контейнера.

Также мы с командой примонтировали общую папку для обоих контейнеров. По итогу получился такой вот yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: dotnet-mymonitor
spec:
 replicas: 1
 selector:
   matchLabels:
     app: dotnet-mymonitor
 template:
   metadata:
     labels:
       app: dotnet-mymonitor
   spec:
     volumes:
     - name: diagnostics
       emptyDir: {}
     Containers:
     shareProcessNamespace: true
     - name: server
       image: mcr.microsoft.com/dotnet/core/samples:aspnetapp
       ports:
       - containerPort: 80
       volumeMounts:
         - mountPath: /tmp
           name: diagnostics
     - name: sidecar
       image: <myregistry>/dotnet/monitor:1.0.0
       volumeMounts:
         - name: diagnostics
           mountPath: /tmp

<myregistry> — указывается хранилище контейнеров. 

Попробовали запустить это образ рядом с основным контейнером приложения и сделать dump из командной строки sidecar контейнера.

 dotnet-dump ps
 dotnet-dump collect -p id_процесса -o /tmp/dump.dmp

Это сработало! 

Следующим этапом для нас стало автоматизирование.

В нашем случае по мониторингам было видно, что некоторые сервисы потребляют много оперативной памяти. Со временем это потребление только возрастает. 

Проанализировав сложившеюся ситуацию наша команда пришла к следующему алгоритму снятия дампов:

  • Необходимо снимать дамп при достижении сервисом значения по оперативной памяти равное 1Гб; 

  • После этого необходимо увеличивать переменную для сравнения значения вдвое: т.е. следующий дамп увеличивается на 2 Гб, потом на 4 Гб и так далее.

Теперь за нами осталась лишь реализация.

Первое, что может спросить любой разработчик, кто хоть немного использует кубернейт, так это куда складировать дампы и где стоит их хранить? Вполне очевидно, что если дамп делается внутри памяти пода, то после рестарта пода его не будет.

Поэтому нам необходимо было примонтировать хранилище ко всем необходимым сервисам. Учитывая, что проект был на Ажуре, то все эти проблемы можно было легко решить за счет использования стандартной учётной записи хранения — Standard_LRS.

Вот так выглядит сам yaml файл для создания класса хранения:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: dump
provisioner: kubernetes.io/azure-file
mountOptions:
  - dir_mode=0777
  - file_mode=0777
  - uid=0
  - gid=0
  - mfsymlinks
  - cache=strict
  - actimeo=30
parameters:
  skuName: Standard_LRS
  shareName: dump-share
allowVolumeExpansion: true
reclaimPolicy: Retain

Хотел бы также оставить небольшой комментарий по последним двум параметрам: 

  • allowVolumeExpansion: true — необходим для возможности изменения размера PVC из кубера;

  • reclaimPolicy: Retain — для динамически подготовленных PersistentVolumes. Политика возврата по умолчанию — «Удалить» (Delete). 

Это означает, что динамически подготовленный том автоматически устраняется, когда пользователь удаляет соответствующий PersistentVolumeClaim. 

Меня такое автоматическое поведение не очень устраивало, ведь в моем случае куда целесообразнее было использовать политику «Сохранить» (Retain). А при использовании политики «Сохранить», если пользователь удаляет PersistentVolumeClaim, соответствующий PersistentVolume не удаляется. Вместо этого он перемещается в фазу выпуска, где все его данные можно восстановить вручную.

Иными словами, даже удалив все упоминания об SC, PVC, PV из кубера, данные дампов останутся в учётной записи хранения в Ажуре. 

PVC создавался следующим скриптом: 

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: service-dump
  namespace: default
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: dump
  resources:
    requests:
      storage: 10Gi

Ну и, соответственно, монтирование к sidecar контейнеру происходит следующим образом:

…
spec:
      volumes:
        - name: dump-storage
          persistentVolumeClaim:
            claimName: service-dump
…
          volumeMounts:
            - name: dump-storage
              mountPath: /tmp/dumps
…

По алгоритму автоматизации дампов мы рассматривали два варианта:

  • Через cron

  • В цикле

Моя команда решила пойти вторым путем. Тут стоит отметить, что через крон есть определенные особенности реализации, но этот вариант тоже вполне рабочий.

И вот у нас уже написался небольшой bash скрипт) Мы исходили из следующих соображений:

  1. Нужно было как-то высчитывать потребляемую память конкретным процессом dotnet. В нашем случае это и будет процесс с наибольшим потреблением;

  2. Нужно было считывать id процесса;

  3. Необходимо было сравнивать потребляемую память конкретным процессом dotnet с переменной и увеличивать эту переменную;

  4. А еще была необходимость в создании и архивации дампов таким образом, чтобы они занимали не так много места на учетной записи хранения;

  5. Нам хотелось, чтобы дамп имел красивое название, в котором бы отображалось имя сервиса, дата и время. Для этого в sidecar контейнер в деплойменте передавались соответствующие параметры. 

В итоге скрипт (script.sh) выглядит так: 

*его я монтировал в configmap

#!/usr/bin/env bash
mem=$(ps aux | awk '{print $6}' | sort -rn | head -1)
mb=$(($mem/1024))
archiveDumpPath="/tmp/dumps/$SERVICE-$(date +"%Y%m%d%H%M%S").zip"
fullPath="/tmp/$PROJECT-$(date +"%Y%m%d%H%M%S").dump"
echo "mem:" $mb" project:" $SERVICE "use:" $USE_MEMORY
if [ "$mb" -gt "$USE_MEMORY" ]; then
export USE_MEMORY=$(($USE_MEMORY*2))
pid=$(dotnet-dump ps | awk '{print $1}')
dotnet-dump collect -p $pid -o $fullPath
zip $fullPath.zip $fullPath
mv $fullPath.zip $archiveDumpPath
rm $fullPath
Fi

Думаю, тут не стоит расписывать объяснение ко всем строкам. Эту информацию можно с легкостью найти в интернете. Остановлюсь лишь на интересном способе просмотра “top1 максимально потребляемой памяти”: 

ps aux | awk '{print $6}' | sort -rn | head -1

Я достаточно долго размышлял над тем как лучше это сделать… И, если у кого-то найдутся более интересные способы, то буду очень рад увидеть их в комментариях.

В итоге, учитывая вышеописанное, деплоймент самого sidecar изменился до следующего варианта:

name: sidecar
          image: '<myregistry>/monitor:1.0.2'
          command:
            - /bin/sh
          args:
            - '-c'
            - while true; do . /app/script.sh; sleep 1m;done #ежеминутный
          env:
            - name: USE_MEMORY
              value: '1024'
            - name: SERVICE
              value: <project-or-service-name>
          resources: {}
          volumeMounts:
            - name: diagnostics
              mountPath: /tmp
            - name: dump
              mountPath: /tmp/dumps
            - name: moto-dumps
              mountPath: /app/script.sh
              subPath: script.sh

Следующая схема заработала корректно и позволила автоматизировать дампы .Net для сервисов и приложений, что, в свою очередь, повысило оперативность в выявлении ошибок в коде и скорость разработки. 

P.S.: Для удобства скачивания дампов и работы с учетными записями хранения Ажуры лично мне понравилось использовать Microsoft Azure Storage Explorer.

Источник: https://habr.com/ru/company/sdventures/blog/653277/


Интересные статьи

Интересные статьи

С радостью представляем вам сводку новостей о первом релизе PVS-Studio в 2022 году. Сегодня поговорим об улучшениях в работе с Unreal Engine и ASP.NET Core проектами, новых диагностиках, а также о т...
Данная статья содержит небольшое введение в JIT-компиляцию и .NET Core (отныне .NET 5, .NET 6 и так далее), а также несколько практических примеров ускорения запуска приложений на .NET. Данные советы ...
.NET 6 в процессе разработки, и я хотел поделиться некоторыми из моих любимых новых API в .NET и ASP.NET Core, которые вам обязательно понравятся. Почему они понравятся? Потому что они разработаны при...
Введение Несколько лет назад, мы решили, что настало время поддержать SIMD код в .NET. Мы представили пространство имен System.Numerics с типами Vector2, Vector3,Vector4 и Vector<T>. Эти т...
Сегодня мы поговорим о перспективах становления Битрикс-разработчика и об этапах этого пути. Статья не претендует на абсолютную истину, но даёт жизненные ориентиры.