
Привет, Хабр!
Думаю, для вас не секрет, что в последние годы контейнеризация вышла в лидеры на 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 скрипт) Мы исходили из следующих соображений:
Нужно было как-то высчитывать потребляемую память конкретным процессом dotnet. В нашем случае это и будет процесс с наибольшим потреблением;
Нужно было считывать id процесса;
Необходимо было сравнивать потребляемую память конкретным процессом dotnet с переменной и увеличивать эту переменную;
А еще была необходимость в создании и архивации дампов таким образом, чтобы они занимали не так много места на учетной записи хранения;
Нам хотелось, чтобы дамп имел красивое название, в котором бы отображалось имя сервиса, дата и время. Для этого в 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.
