Развертывание в Docker и Kubernetes образа StatsD+Grafana

    В начале моего изучения Docker и Kubernetes мне нехватало простого и понятного примера, с которым можно было бы «поиграться», изучая особенности этой среды. Этой статьей хотелось бы закрыть этот пробел. Здесь я расскажу об интеграции .NET Core приложения с Telegraf и Grafana, о том, как шлются метрики и как деплоить в Docker и Kubernetes. Примеры в статье расчитаны на тех, кто начинает изучать данную область, но базовыми понятиями желательно обладать, чтобы полностью понять статью. В ней описано, как развернуть контейнер, в котором есть StatsD, InfluxDB и Grafana, а также, как отправлять метрики различных типов из приложения.

    Примеры в этой статье выполнялись на Kubernetes, идущем с Docker for Windows. Но если у вас Windows Home Edition, то вам не удастся его поставить. Если у вас Windows Professional, то проблем возникнуть не должно. Тут есть пара советов по установке kubernetes на Windows. На Linux должно всё работать нормально, проверял на Ubuntu, 18.04 LTS.

    Демонстрация


    Сперва, давайте посмотрим, как всё, о чем буду рассказывать, выглядит в действии. В этом примере вы запустите два приложения: одно отправляет запросы другому, второе выполняет некоторую сложную задачу, и оба приложения отправляют некоторые StatsD метрики, которые вы увидите в Grafana. Перед выполнением шагов, описанных здесь, вам необходимо убедиться, что Kubernetes установлен на вашем компьютере. Далее просто следуйте приведенным ниже инструкциям, и вы увидите некоторый результат. Правда, некоторые настройки вам нужно будет выполнить самостоятельно.

    $ git clone https://github.com/xtrmstep/DockerNetSample
    $ cd .\DockerNetSample\
    $ kubectl apply -f .\src\StatsDServer\k8s-deployment.yaml
    $ .\build.ps1
    $ .\run.ps1
    

    Теперь в браузере можно загрузить URL localhost:3003 и, используя учетные данные root/root, можно получить доступ к интерфейсу Grafana. Метрики уже отправляются, и вы можете попробовать добавить свой дэшбоард. После некоторой настройки вы можете получить что-то вроде этого.



    Когда вы решите всё остановить и очистить ресурсы, просто закройте два окна с рабочими процессами и выполните следующие команды, которые очистят объекты в Kubernetes:

    $ kubectl delete svc stats-tcp
    $ kubectl delete svc stats-udp
    $ kubectl delete deployment stats
    

    Теперь давайте поговорим, что произошло.

    Вы задеплоили StatsD, InfluxDb и Grafana на локальный Kubernetes


    Не так давно открыл для себя, что DockerHub имеет много полезных и уже подготовленных образов. В своём примере я использую один из таких. Репозиторий с образом вы можете найти здесь. Этот образ содержит InfluxDB, Telegraf (StatsD) и Grafana, которые уже настроены для совместной работы. Есть два распространенных способа деплоя образов: первый — это используя docker-compose в Docker, и второй — это деплой на Kubernetes. Оба способа дают возможность деплоить сразу несколько компонетов (образов), описывать настройки сети и другие параметры. Я кратко расскажу о docker-compose, но более детально остановлюсь на развертывании в Kubernetes. Кстати, недавно стало возможным деплоить docker-compose в Kubernetes.

    Развертывание с помощью Docker-Compose


    Docker-compose распространяется вместе с Docker для Windows. Но вам нужно проверить, какую версию вы можете использовать в своем YAML. Для этого надо узнать версию установленного Docker и по матрице совместимости найти нужную версию. Я установил Docker версии 19.03.5, поэтому я могу использовать файл версии 3.x. Но я буду использовать 2 для совместимости с прошлыми версиями Docker. Вся необходимая информация для написание docker-compose файлы уже есть в описании репозитория: имя образа и порты.

    version: '2'
    services:
      stats:
        image: samuelebistoletti/docker-statsd-influxdb-grafana:latest
        ports:
          - "3003:3003"
          - "3004:8888"
          - "8086:8086"
          - "8125:8125/udp"
    

    В секции ports я делаю видимыми порты из контейнера через порты хост-системы. Если я этого не сделаю, я не смогу получить доступ к ресурсам в контейнере из хост-системы, потому что они будут видны только внутри Docker. Грубо говоря, я не смогу загрузить Grafana в браузере. Подробнее о маппинге портов вы можете почитать здесь. После составления файла, можно его задеплоить. По умолчанию, docker-compose ищет файл docker-compose.yaml в текущей папке, поэтому вы можете запустить его с минимальными параметрами, просто выполнив команду docker-compose up. Это развернет контейнер в Docker. Я буду использовать дополнительные параметры для явного указания файла и запуска контейнера в фоновом режиме.

    $ docker-compose -f docker-compose.yaml up -d
    $ docker-compose stop
    

    Развёртывание в Kubernetes


    На первый взгляд, развертывание в Kubernetes выглядит несколько сложнее, поскольку вам необходимо определить Deployment, Service и другие параметры. Я прибегаю к небольшой хитрости, которая экономит мне время на написание YAML-файлов для Kubernetes. Сначала я деплою все в минимальной конфигурации в кластер, используя kubectl. А затем экспортирую нужные мне объекты, как YAML и уже потом дописываю необходимые настройки.
    Заметка о Kubernetes, устанавливаемом на виртуальных машинах в GCP
    Я пытался использовать Kubernetes, который развернут на Compute Engine в GCP и столкнулся с проблемой при развертывании сервиса LoadBalancer. Он остаётся в состоянии pending и не получает внешний IP-адрес. Это обстоятельство препятствует доступу к сервису из Интернета, даже если вы настроили сеть. Для этого есть решение, которое требует установки ingress сервиса и использования NodePort, согласно совету на Stackoverflow.

    Развёртывание с kubectl


    Итак, давайте создадим развертывание из образа. Имя stats — это имя развертывания, которое я сам дал для этого объекта. Вы можете использовать другое имя.

    $ kubectl run stats --image=samuelebistoletti/docker-statsd-influxdb-grafana:latest --image-pull-policy=Always
    

    Эта команда создаст Deployment, который, в свою очередь, создаст Pod и ReplicaSet в k8s. Чтобы получить доступ к Grafana, мне нужно создать сервис и выставить порты. Это можно сделать с помощью службы NodePort или LoadBalancer. В большинстве случаев вы будете создавать службы LoadBlancer. Почитать больше об этом можно тут.

    $ kubectl expose deployment stats --type=LoadBalancer --port=3003 --target-port=3003
    

    Эта команда также сопоставит порт хоста 3003 (--port) с портом в контейнере (--target-port). После выполнения команды вы сможете получить доступ к Grafana, загрузив URL localhost:3003 в браузере. Вы можете проверить созданные объекты в k8s с помощью этой команды:

    $ kubectl get all
    

    Вы должны увидеть что-то, как на это картинке:



    Экспорт YAML конфигурации


    На данный момент, развернутая система ещё не то, что мне нужно, но я могу использовать её, как черновик. Экспортируем конфигурацию YAML:

    $ kubectl get deployment,service stats -o yaml --export > exported.yaml
    

    Экспортированный файл будет содержать определения Deployment и Service с текущей конфигурацией. Мне нужно будет удалить ненужные настройки и добавить маппинг портов. Окончательный минималистский вариант может выглядеть следующим образом:

    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: stats
    spec:
      replicas: 1
      selector:
        matchLabels:
          run: stats
      template:
        metadata:
          labels:
            run: stats
        spec:
          containers:
          - image: samuelebistoletti/docker-statsd-influxdb-grafana:latest
            imagePullPolicy: Always
            name: stats
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: stats-tcp
    spec:
      type: LoadBalancer
      ports:
        - name: grafana
          protocol: TCP
          port: 3003
          targetPort: 3003
        - name: influxdb-admin
          protocol: TCP
          port: 3004
          targetPort: 8888
        - name: influxdb
          protocol: TCP
          port: 8086
          targetPort: 8086
      selector:
        run: stats
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: stats-udp
    spec:
      type: LoadBalancer
      ports:
        - name: telegraf
          protocol: UDP
          port: 8125
          targetPort: 8125
      selector:
        run: stats
    

    Заметка об использовании протоколов TCP/UDP
    Вы не сможете создать службу типа LoadBalancer, которая поддерживает протоколы TCP и UDP. Это известное ограничение, и сообщество пытается найти какое-то решение. Между тем вы можете создать два отдельных сервиса для каждого из типов протоколов.
    Перед использованием нового файла очистите существующие ресурсы в Kubernetes, а затем используйте команду kubectl apply.

    $ kubectl delete svc stats
    $ kubectl delete deployment stats
    
    $ kubectl apply -f k8s-deployment.yaml
    

    Вы только что задеплоили образ в Kubernetes с правильными настройками портов. Используя URL, упоминавшийся выше, вы можете открыть Grafana, но теперь еще можно слать и метрики StatsD. В следующем разделе я немного расскажу про метрики.

    StatsD протокол


    Протокол StatsD очень прост, вы, даже, можете создать свою собственную клиентскую библиотеку, если это очень нужно. Здесь вы можете прочитать больше о датаграммах StatsD. Этот протокол поддерживает такие метрики, как счетчики (counter), время (timing), измерения (gauge) и т.д.

    counter.name:1|c
    timing.name:320|ms
    gauge.name:333|g
    

    Counters используются для подсчета количества некоторых событий. Обычно, вы просто всегда увеличиваете некоторый счетчик (bucket в StatsD). Агрегация делается на более низком уровне, поэтому когда вы будете получать значение за секунды, минуты, вам не придется заниматься дополнительными подсчетами, и в Grafana вы увидите число в секундах, минутах и так далее, как пожелаете.

    Timing используется для измерения продолжительности какого-либо процесса. Например, этот показатель просто идеально подходит для измерения продолжительности веб-запроса.

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

    Метрики в .NET Core сервисе


    Вам понадобится пакет NuGet JustEat.StatsD. Он имеет хорошее описание на GitHub. Так что, просто следуйте его инструкциям, чтобы сделать свою собственную конфигурацию и зарегистрироваться в IoC.

    В качестве примера, давайте возьмем API, где некоторый метод, когда вызван, ставит расчет в очередь потоков, используя ThreadPool. Логика API допускает только определенное количество параллельно выполняемых вычислений. Допустим, вы хотите знать следующее о вашем сервисе:

    • Сколько запросов приходит?
    • Сколько запросов ожидает, прежде чем ThreadPool выдаст поток?
    • Сколько времени занимает операция?
    • Насколько быстро заканчиваются свободные потоки в API?

    Вот как может выглядеть сбор метрик в коде:

    public override async Task<FactorialReply> Factorial(FactorialRequest request, ServerCallContext context)
    {
        // Obtain the number of available threads in ThreadPool
        ThreadPool.GetAvailableThreads(out var availableThreads, out _);
        // The number of available threads is the example of Gauge metric
        // Send gauge metric to StatsD (using JustEat.StatsD nuget)
        _stats.Gauge(availableThreads, "GaugeAvailableThreads");
        
        // Increment a counter metric for incoming requests
        _stats.Increment("CountRequests");
        
        // The method _stats.Time() will calculate the time while the _semaphoreSlim.WaitAsync() were waiting
        // and send the metric to StatsD
        await _stats.Time("TimeWait", async f => await _semaphoreSlim.WaitAsync());
    
        try
        {
            // Again measure time length of calculation and send it to StatsD 
            var result = await _stats.Time("TimeCalculation", async t => await CalculateFactorialAsync(request.Factor));
    
            // Increment a counter of processed requests
            _stats.Increment("CountProcessed");
    
            return await Task.FromResult(new FactorialReply
            {
                Result = result
            });
        }
        finally
        {
            _semaphoreSlim.Release();
        }
    }
    

    В коде количество одновременных вычислений контролируется классом SemaphoreSlim. Если число параллельных выполнений превысит максимальное, оно остановит выполнение и будет ждать завершения какого-либо другого потока.

    Комментарии 8

      +1

      Следует добавить, что в указанном примере нет никакой персистенции данных (или я ее не увидел) — т.е. каждый перезапуск контейнера/кубернетеса будет приводить к обнулению данных. Неприятно, но для демонстрации возможностей — вполне достаточно. Для продакшена все придется делать совсем по-другому.
      Ну, и добавлю, что из TICK стэка реально чудесен только телеграф. Коллеги из https://t.me/metrics_ru утверждали, что они сталкивались с случайными его зависаниям, когда вроде телеграф работает, а метрики не льются, но я лично достоверных таких случаев не знаю. А вот InfluxDB — это боль. Сама идея иметь графито-подобную базу — она хороша. Но вот с реализацией есть нюансы. Эти постоянные смены формата базы (отсутствие обратной совместимости), отсутствие кластеризации (есть только в платной версии), отсутствие внятного тулинга и нюансы с потреблением памяти (не раз видел, как инфлакс давился вроде бы обычными запросами, только на большую глубину истории) ставят большой вопрос о пригодности к эксплуатации этой БД в продуктовой среде. Хотя я знаю крупные компании, которые с этой БД живут.....


      И уж я мог бы не упоминать, что это вечная битва — statsd вариант отправки метрик и prometheus формат с опросом метрик снаружи (и есть ощущение, что прометеус вариант именно в применении к долгоживущим сервисам победил)

        0

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

          0

          Первый кейс похож на правду. Ну, будет у него (у каждого инстанса приложения) свой буфер. Перезапускаемся — у нас счетчик сбрасывается, но и пофиг. Если это случается не слишком часто — проблемы не будет. Если часто — да, мы попросту будем видеть фуфломицин, а не данные. Т.е. счетчик запросов у нас нарастает, пока мы не ребутнем приложение. Можно попробовать его сбрасывать в момент, когда у нас прометеус приходит за данными, но если у нас несколько прометеусов скрейпят один таргет, то нам придется их как-то отличать. Вот разработчики нетдата столкнулись с этой же проблемой и им пришлось закостылить https://github.com/netdata/netdata/tree/master/backends/prometheus


          Далее. Если у тебя несколько инстансов приклада — ты сам должен решить ОК ли это, если у каждого инстанса своя метрика, или ты хочешь агрегированную.
          Ну, и в конечном счете — всегда ПРОЩЕ считать статистику запросов не на приложении самом, а не прокси перед ним (ingress, service mesh).


          Писать в логи и сделать из анализатор, который будет отдавать Прометеусу метрики?

          не будет ли это слишком ресурсоемко? Плюс это ставит вопрос гарантированности доставки логов, иначе нельзя будет верно посчитать количество запросов


          И это мы еще не говорим про обработку батч-джобов (кратковременных) или потоковой обработки данных.

            0

            Ну вот с точки зрения учёта нескольких инстансов и кратковременных и потоков, снимать метрики через через логи (централизованные или частично централизованные, типа stdout/stderr докер логов на каждой ноде кластера) наиболее разумным видится как с точки зрения минимальных изменениях в приложениях, а то и вообще без них, так и с точки зрения сохранности метрик для нескольких инстансов Прометеуса и между запусками.


            На прокси может быть и лучше с какой-то точки зрения, но особо дела не меняет с точки зрения сбора, по-моему, кроме того что возможности конфигурирования часто сильно ограничены, а написаны прокси на нестандартных для команды языках типа Си или Гоу. Ну и у приложения есть более точные метрики типа время ответа базы данных

      0
      Спасибо. Сам в начале пути и подобные примеры помогают.

      А какую литературу / обучалки порекомендуете, когда основные понятия уже прочитаны / прощупаны по верхам, и надо уже начать углубляться, что бы лучше понимать, как оно всё работает (как проводить диагностику, траблшутинг, какие-то best practices и т.п.)?
        +1
        Мне понравились курсы от Google на Coursera. Очень понятно и с возможностью реально попробовать все в лабораторных. А относительно других Ваших вопросов пока ничего не могу сказать. Планирую в другой статье больше уделить внимания написанию Dockerfile для .NET Core и различных сеттингов деплойментов, в том числе конфиг-мапы. Не знаю, будет ли это Вам полезно.
        0

        -

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое