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