Простой service discovery в Prometheus через Consul

Закон Парето (принцип Парето, принцип 80/20) — «20 % усилий дают 80 % результата, а остальные 80 % усилий — лишь 20 % результата».
Wikipedia

Приветствую тебя, дорогой читатель!

Моя первая статья на Хабр посвящена простому и, надеюсь, полезному решению, сделавшим для меня сбор метрик в Prometheus с разнородных серверов удобным. Я затрону некоторые подробности, в которые многие могли не погружаться, эксплуатируя Prometheus, и поделюсь своим подходом по организации в нём легковесного service discovery.
Для этого понадобится: Prometheus, HashiCorp Consul, systemd, немного кода на Bash и осознание происходящего.

Если интересно узнать, как все это связано и как оно работает, добро пожаловать под кат.

Prometheus + Bash + Consul

Встречайте: Prometheus

Мое знакомство с Prometheus произошло, когда возникла необходимость собирать метрики с кластера Kubernetes. Пробежавшись по материалам в интернете стало понятно, что работать с Prometheus и его pull-моделью, очень удобно, когда он самостоятельно узнаёт о сервисах, с которых необходимо собирать метрики. Для настройки Prometheus под Kubernetes в конфигурационном файле prometheus.yml есть директива kubernetes_sd_configs. Она отвечает за коммуникацию с kube-apiserver с целью получения IP-адресов и метаинформации о pod’ах в кластере с которых нужно собирать метрики.

scrape_configs: - job_name: kubernetes-pods   kubernetes_sd_configs:   - role: pod   bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token   tls_config:     insecure_skip_verify: true   relabel_configs:   - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]     action: keep     regex: true   - source_labels: [__meta_kubernetes_pod_ip, __meta_kubernetes_pod_annotation_prometheus_io_port]     action: replace     regex: (.+);(.+)     replacement: $1:$2     target_label: __address__   - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]     action: replace     regex: (.+)     target_label: __metrics_path__   - action: labelmap     regex: __meta_(kubernetes_namespace|kubernetes_pod_name|kubernetes_pod_label_manifest_sha1|kubernetes_pod_node_name)

С первого взгляда картинка не сильно понятная, но с документацией и в несколько экспериментов разобраться можно. Помимо role=pod есть и другие роли у kubernetes_sd_configs.

Наблюдая, как эффективно Prometheus узнает о сервисах в Kubernetes, например, о DaemonSet prometheus/node_exporter, сразу появилось желание реализовать аналогичный подход для сбора метрик и мониторинга доступности сервисов вне кластера Kubernetes: node_exporter, Zookeeper, Kafka, ClickHouse, CEPH, Elasticsearch, Tarantool …

Писать каждый раз targets в static_configs при добавлении нового сервера в текущий кластер Kafka или OSD в CEPH ну совсем не про удобное управление инфраструктурой. А еще это нужно не забывать делать. Да, можно все автоматизировать, например через Ansible, но что делать, если на некоторое время отключить один CEPH OSD для обслуживания. Тогда нужно еще и автоматизировать временное выведение этого сервиса из конфига prometheus.yml. В итоге получается огромная куча слоев автоматизации, которые тоже нужно не забывать запускать. Именно слоев, потому что одна такая автоматизация порождает необходимость в другой. Так еще ко всему этому сильно разрастается prometheus.yml, в котором нужно не просто перечислять сервисы, а еще делить их на разные job_name для удобного доступа к метрикам. Тут легко прикинуть сколько строк в prometheus.yml нам создаст один кластер Kafka из 6 брокеров. А если кластеров 3. И добавим к ним еще 3 кластера ClickHouse, в каждом по 4 ноды. А уж про CEPH и вообще говорить страшно. А еще на каждом сервере не по одному сервису, с которого нужно собирать метрики — про prometheus/node_exporter не надо забывать. Представив всю свою инфраструктуру в количественной мере можно прикинуть и размер prometheus.yml при использовании только static_configs.

Don’t specify default values unnecessarily: simple, minimal configuration will make errors less likely.

Kubernetes Documentation, Configuration Best Practices, General Configuration Tips.

Админы почему-то очень любят хранить в конфигах строки по-умолчанию в виде комментариев, создавая из файла конфигурации настоящую свалку. Не нужно этого делать!
В файле конфигурации должны быть только важные строки, отвечающие за работу приложения. Файл, который шел в инсталляционном пакете, можно оставить рядом, добавив ему постфикс .default или .original. А можно вообще ничего не хранить рядом и в файл конфигурации, в шапке, положить 2-3 полезные ссылки на описание этого файла или его содержимое по-умолчанию во внешнем источнике: документация, GitHub репозиторий проекта. Совсем не сложно сделать файл конфигурации удобным и читаемым для коллег и себя самого, в будущем.

Вспомним про HashiCorp Consul

Нужна была максимально простая и понятная автоматизация управления сервисами в prometheus.yml. Самое знакомое что сразу бросалось в глаза из документации Prometheus — consul_sd_configs. То есть, Prometheus, посредством HashiCorp Consul, может узнавать о сервисах с которых нужно собирать метрики и заодно мониторить их доступность. Например:

scrape_configs: - job_name: SERVICE_NAME   consul_sd_configs:   - server: consul.example.com     scheme: https     tags: [test] # dev|test|stage|prod|...     services: [prometheus-SERVICE_NAME-exporter]

Будучи уже знакомым с Consul, я знал, что сам он не узнает о сервисах даже на тех машинах, на которых запущен его agent. Через его простое HTTP API сервис нужно зарегистрировать в Consul, а еще его можно разрегестрировать. Ведь бывает, что сервис больше никому не нужен: попробовали, и не подошел. В полном объеме я никогда не использовал Consul. Возникали только задачи, с которыми Consul отлично справлялся частью своих функций: KV-хранилище, HashiCorp Vault, кластеризация Traefik. И, например, никогда не нужен был его DNS. Вот и сейчас задача стояла не усложнить себе жизнь, запуская на каждом сервере по дополнительном сервису в виде Consul agent. Достаточно запустить один инстанс Consul, который будет принимать запросы в HTTP API и к нему будет обращаться Prometheus за списком адресов, по которым расположены те или иное сервисы. Идеально, если это все будет по HTTPS, хотя сеть и так закрытая.

На такой волне стало понятно, что уже имеющийся в Kubernetes StatefulSet Consul, обеспечивающий кластерную работу Traefik, можно использовать для service discovery в Prometheus. И даже Ingress в локальную сеть у него уже был, так как было пару моментов использования Consul’s web UI. Бонусом Traefik обеспечивал ему HTTPS-транспорт с сертификатом от Let`s Encrypt через DNS Challenge.

consul.yml

# https://consul.io/docs/agent/options.html  --- apiVersion: v1 kind: Service metadata:   name: consul   labels:     app: consul spec:   selector:     app: consul   ports:   - name: http     port: 8500  --- apiVersion: apps/v1 kind: StatefulSet metadata:   name: consul   labels:     app: consul spec:   serviceName: consul   selector:     matchLabels:       app: consul   volumeClaimTemplates:   - metadata:       name: data     spec:       storageClassName: cephfs       accessModes:       - ReadWriteOnce       resources:         requests:           storage: 1Gi   template:     metadata:       labels:         app: consul     spec:       automountServiceAccountToken: false       terminationGracePeriodSeconds: 60       containers:       - name: consul         image: consul:1.6         volumeMounts:         - name: data           mountPath: /consul/data         args:         - agent         - -server         - -client=0.0.0.0         - -bind=127.0.0.1         - -bootstrap         - -bootstrap-expect=1         - -disable-host-node-id         - -dns-port=0         - -ui         ports:         - name: http           containerPort: 8500         readinessProbe:           initialDelaySeconds: 10           httpGet:             port: http             path: /v1/agent/members         livenessProbe:           initialDelaySeconds: 30           httpGet:             port: http             path: /v1/agent/members         resources:           requests:             cpu: 0.2             memory: 256Mi  --- apiVersion: extensions/v1beta1 kind: Ingress metadata:   name: consul   labels:     app: consul   annotations:     traefik.ingress.kubernetes.io/frontend-entry-points: http,https     traefik.ingress.kubernetes.io/redirect-entry-point: https spec:   rules:   - host: consul.example.com     http:       paths:       - backend:           serviceName: consul           servicePort: http

На этом этапе у меня имелся Prometheus, который готов забрать из Consul адреса сервисов и начать собирать с них метрики, и инстанс Consul, в котором можно регистрировать сервисы. Возникает задача автоматизировать децентрализованную регистрацию сервисов во время запуска на любом сервере инфраструктуры. И на помощь приходит простой Bash.

Время для Bash и systemd.service

В своей инфраструктуре я давно использую иммутабельную CoreOS Container Linux. Было даже дело рассказать об этой ОС на DevOpsConf Russia 2018. Я и по сей день использую эту ОС как основную для запуска сервисов в эфемерных Docker контейнерах, оборачивая в systemd.service. Но только теперь это Flatcar Linux, разработчики которой подхватили идею и не дали ей погибнуть, обеспечив полную совместимость с CoreOS Container Linux. А перейти с CoreOS на Flatcar можно простым обновлением системы!

Все сервисы на моём сервере — это systemd.service. А systemd — это очень мощный инструмент, который активно развивается и решает очень много задач в Linux. И есть в systemd.service, в секции [Service], такие параметры как ExecStartPost, ExecStop. Они-то и помогут мне с регистрацией и разрегистрацией сервиса в Consul.
Опишу сразу на примере юнита prometheus-node-exporter.service. Этот сервис примечателен тем, что запускается абсолютно на каждом сервере и ему одному в static_configs можно было бы выделить больше 100 строк.

[Unit] After=docker.service [Service] Environment=CONSUL_URL=https://consul.example.com ExecStartPre=-/usr/bin/docker rm --force %N ExecStart=/usr/bin/docker run \     --name=%N \     --rm=true \     --network=host \     --pid=host \     --volume=/:/rootfs:ro \     --label=logger=json \     --stop-timeout=30 \     prom/node-exporter:v0.18.1 \     --log.format=logger:stdout?json=true \     --log.level=error ExecStartPost=/opt/bin/consul-service register -e prod -n %N -p 9100 -t prometheus,node-exporter ExecStop=/opt/bin/consul-service deregister -e prod -n %N ExecStop=-/usr/bin/docker stop %N Restart=always StartLimitInterval=0 RestartSec=10 KillMode=process [Install] WantedBy=multi-user.target

Как раз вот тут появляется некий /opt/bin/consul-service. У него всего пара переменных системного окружения и несколько обязательных аргументов, которые я опишу поподробней.

Переменные окружения:

  • CONSUL_URL — адрес Consul в котором будут регистрироваться сервисы во время запуска.
  • CONSUL_TOKEN — он же HTTP-заголовок «X-Consul-Token», позволяющий ограничить доступ в HTTP API Consul. Его можно сформировать в Consul web UI, а в последних версиях на него еще навесили ACLs.

Обязательные аргументы:

  • register/deregister — всегда первый аргумент. Отвечает за регистрацию сервиса и его разрегистрацию.
  • -e — окружение в котором запускается сервис — [dev|test|prod|…].
  • -n — название сервиса, например prometheus-node-exporter. В примере выше используется переменная %N, которая в systemd.unit присваивает значение из названия самого юнита.
  • -p — порт на котором сервис будет отдавать в Prometheus метрики. Используется только во время регистрации сервиса в Consul.
  • -t — произвольные теги через запятую. С их помощью можно иначе фильтровать выборки сервисов в Consul. Используется только во время регистрации сервиса в Consul.

Использование consul-service в systemd.service оказалось очень практичным и преподнесло один очень приятный бонус. systemd.service во время старта, а именно ExecStartPost с consul-service register, регистрирует сервис в Consul, и Prometheus сразу же может забирать с него метрики, а также мониторить его доступность. Но самое удобное, когда нужно ненадолго оставить сервис или перезагрузить сервер. В этот момент выполняются все ExecStop, в том числе consul-service deregister, и сервис разрегистрируется. Consul и следом Prometheus о нем забывают, и тогда нет никакого лишнего оповещения о недоступности намеренно остановленного сервиса. Особенно это практично, когда работы проводятся в ночное время одним инженером, а другой в этот момент в ответе за Service Availability и получает пугающие его оповещения о недоступности сервиса. Но если сервис упал, и никто специально для проведения работ его не останавливал, и разрегистрации сервиса не было, то он будет под мониторингом со всеми вытекающими.

Очень важным моментом в работе consul-service является правильно установленные hostname сервера и search (домены поиска) в resolv.conf. Этот же адрес должен присутствовать в локальном DNS-сервере, что как минимум best practice при работе с серверами в локальной среде.

Относитесь к своим серверам так, как хотите, чтобы относились к вам!

Если вы до сих пор работаете со своими серверами в локальной сети по IP, то самое время присвоить себе тоже какой-нибудь идентификатор и обязать коллег обращаться к вам только по нему. И больше ни на какие обращения не реагировать. Так будет справедливо и быстро осознается ценность имени.

consul-service написан на чистом Bash в ~60 строчек. В зависимостях у него только хорошо известный всем cURL и Bash. Уверен, что админ без опыта разработки бегло разберется в нескольких строчках этого скрипта. Лицензия этого макро-решения MIT, поэтому если появится необходимость доработать его под свои нужды, не стесняйтесь.

Тот самый scrape_configs в prometheus.yml для prometheus/node_exporter, запущенном на всех серверах, лаконичен и не зависит никак от количества серверов.

scrape_configs: - job_name: node-exporter   consul_sd_configs:   - server: consul.example.com     scheme: https     tags: [prod]     services: [prometheus-node-exporter]

tags: [prod] тут отвечает за выборку инстансов только production среды. Это как раз тот самый аргумент -e в consul-service, который по факту превращается в один из тегов сервиса при попадании в Consul.

На мои сервера с Flatcar Linux доставка скрипта в /opt/bin осуществляется посредством Ansible. Playbook всего на пару tasks. Но это реализуется и любым другим инструментом, хоть строчкой в crontab.

tasks: - name: Create directory "/opt/bin"   with_items: [/opt/bin]   file:     state: directory     path: "{{ item }}"     owner: root     group: root     mode: 0755 - name: Download "consul-service.sh" to "/opt/bin/consul-service"   get_url:     url: https://raw.githubusercontent.com/devinotelecom/consul-service/master/consul-service.sh     dest: /opt/bin/consul-service     owner: root     group: root     mode: 0755     force: yes

О том как Ansible, а точнее Python, попадает и работает на серверах Flatcar Linux думаю, можно рассказать в будущих статьях. В планах посвятить целый цикл статей про иммутабельную ОС Flatcar Linux.

В заключение

Наверное, можно считать всю эту конструкцию неким велосипедом. Но в эксплуатации и передачи коллегам такого велосипеда на поддержку стало сразу ясно, что он простой как «рама+колеса+педали+руль», и ездить на нем удобно. И почти в обслуживании не нуждается.
А лучшей похвалой этой статье станет мысль читающего — “Интересное решение! Надо попробовать!”.

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *