Apache Druid: огляд, запуск в Kubernetes та моніторинг з Prometheus

Автор |  18/09/2022
 

Apache Druid – колонкова база даних, орієнтована на роботу з великими обсягами даних, що поєднує в собі можливості та переваги Time-Series Database, Data Warehouse та пошукової системи.

Загальне завдання – налаштувати моніторинг кластера Druid в Kubernetes, для чого спочатку подивимося, що це взагалі таке і як воно все працює, а потім запустимо Друїд і помацаємо його руками.

Для запуску самого Druid у Kubernetes використовуємо druid-operator, для збору метрик – druid-exporter, для моніторингу – Kubernetes Prometheus Stack, а запускати все це будемо у Minikube.

Огляд Apache Druid

Головна фіча Друїда особисто для мене це те, що система розроблялася для використання в хмарах типу AWS та Kubernetes, тому має чудові можливості зі скейлінгу, зберігання даних та відновлення роботи у разі падіння одного з сервісів.

Крім того, Друїд це:

  • колонковий формат зберігання даних: для обробки завантажується лише необхідна таблиця, що значно збільшує швидкість обробки запитів. Крім того, для швидкого сканування та агрегації даних Друїд оптимізує сховище колонки відповідно до її типу даних.
  • масштабована розподілена система: кластера Друїда можуть розташовуватися на десятках і сотнях окремих серверів
  • паралельна обробка даних: Друїд може обробляти кожен запит паралельно на незалежних інстансах
  • self-healing та self-balancing: у будь-який момент можна додати або видалити нові ноди в кластер, при цьому кластер сам виконує балансування запитів та перемикання між ними без будь-якого даунтайму. Більше того, Друїд дозволяє без даунтайму виконувати і оновлення версій
  • власне, cloud-native и fault-tolerant архитектура: Друїд від початку спроектований до роботи у розподілених системах. Після отримання нових даних, він відразу ж копіює їх у свій Deep Storage, в ролі якого можуть бути сервіси зберігання даних типу Amazon S3, HDFS або мережна файлова система, і дозволяє відновити дані навіть у тому випадку, якщо всі друїдні сервери впадуть. Якщо впала лише частина серверів Друїда – вбудована реплікація даних дозволить продовжувати виконання запитів (вже сверблять руки повбивати його ноди, і подивитися на реакцію Друїда – але не цього разу)
  • ідекси для швидкого пошуку: Друїд використовує стислі Bitmap-індекси, дозволяючи виконувати швидкий пошук по кількох колонках
  • Time-based partitioning: за замовчуванням Друїд розділяє дані у часі, дозволяючи створювати додаткові сегменти з інших полях. В результаті, при виконанні запитів з тимчасовими проміжками будуть використані лише необхідні сегменти даних

Архітектура та компоненти Druid

Загальна архітектура:

Або так:

Див. Processes and servers.

Three-server configuration – Master, Query, Data

Друїд має декілька внутрішніх сервісів, що використовуються для роботи з даними, і ці сервіси можна групувати за трьома типами серверів – Master, Query і Data, див. Pros and cons of colocation.

Master servers – управління додаванням нових даних та доступністю (ingest/indexing) – відповідають за запуск нових завдань щодо додавання нових даних та координацію доступності даних:

  • Coordinator: відповідає за розміщення сегментів даних на конкретні ноди з Historical процесами
  • Overlord: відповідають за розміщення завдань з додавання даних на Middle Managers і за координацію передачі створених сегментів у Deep Store

Query servers – управління запитами від клієнтів

  • Broker: отримує запити від клієнтів, визначає які з Historical або Middle Manager процесів/нід містять необхідні сегменти, і з вихідного запиту клієнта формує та відправляє підзапит (sub-query) кожному з цих процесів, після чого отримує від них відповіді, формує з них загальну відповідь з даними для клієнта, і надсилає йому.
    При цьому Historical відповідають на підзапити, які відносяться до сегментів даних, які вже зберігаються в Deep Store, а Middle Manager – відповідають на запити, які відносяться до нещодавно отриманих даних, які ще знаходяться в пам’яті і не відправлені в Deep Store
  • Router: опціональний сервіс, який надає загальний API для роботи з Брокерами, Оверлордами та Координаторами, хоча їх можна використовувати і безпосередньо. Крім того, Router надає Druid Console – WebUI для роботи з кластером, побачимо її трохи згодом

Data servers – керують додаванням нових даних у кластер і зберігають дані для клієнтів:

  • Historical: “головні робочі конячки” (с) Друїда, які обслуговують сховище та запити до “історичних” даних, тобто. тим, які вже знаходяться в Deep Store – завантажують звідти сегменти на локальний диск хоста, на якому працює Historical процес, та формують відповіді за цими даними для клієнтів
  • Middle Manager: обробляє додавання нових даних у кластер – читають дані із зовнішніх джерел та створюють з них нові сегменти даних, які потім завантажуються у Deep Store
    • Peons: за додавання даних до кластера може відповідати кілька завдань. Для логування та ізоляції ресурсів Middle Manager створює Peons, які є окремими процесами JVM і відповідають за виконання конкретного завдання від Middle Manager. Запускається завжди на тому ж хості, де запущено процес Middle Manager, який породив Peon-процес
  • Indexer: альтернатива Middle Manager і Peon – замість створення окремої JVM на кожне завдання від Middle Manager, Indexer виконує їх в окремих потоках своєї JVM.
    Розробляється як простіша альтернатива Middle Manager і Peon, поки є експерементальною фічею, але в майбутньому замінить Middle Manager і Peon

Окрім внутрішніх процесів, для роботи Друїд також використовує зовнішні сервіси – Deep storage, Metadata storage та ZooKeeper:

  • deep storage: використовується для зберігання всіх даних, доданих у систему і є розподіленим сховищем, доступним кожному серверу Друїда. Це можуть бути Amazon S3, HDFS або NFS
  • metadata storage: використовується для зберігання внутрішніх метаданих, таких як інформація про використання сегментів та поточні завдання. У ролі такого сховища можуть бути класичні СУДБ типу PostgreSQL чи MySQL
  • ZooKeeper: використовується для service discovery та координації роботи сервісів (у Kubernetes замість нього можна використовувати druid-kubernetes-extensions, спробуємо також, але в інший раз)

Druid data flow

Подивимося, як виконується додавання даних (Data ingest) та відповіді клієнтам (Data query, Query Answering).

Data ingest

Додавання даних можна розділити на два етапи: у першому, інстанси Middle Manager запускають завдання з індексування, ці завдання створюють сегменти даних і відправляють їх у Deep Store, а в другому – Historical інстанси завантажують сегменти даних із Deep store, щоб використовувати їх при формуванні відповіді на запити клієнтів.

Перший етап – отримання даних із зовнішніх джерел, індексування та створення сегментів даних, та їх завантаження в Deep store:

При цьому в процесі створення сегментів даних вони вже доступні для виконання запитів по них.

Другий етап – Coordinator опитує Metadata store у пошуках нових сегментів. Як тільки процес Coordinator-а їх знаходить, він вибирає інстанс Historical, який повинен завантажити сегмент з Deep store, щоб він став доступним для обробки запитів:

Coordinator опитує Metadata Store у пошуках нових сегментів. Як тільки він їх знаходить, Coordinator вибирає інстанс Historical, який має завантажити сегмент із Deep store. Коли сегмент завантажений – Historical готовий використовувати його для обробки запитів

Data query

Клієнти надсилають запити до Broker безпосередньо або через Router.

Коли такий запит отримано, Broker визначає, які процеси обслуговують необхідні сегменти даних.

Якщо дані вимагають сегментів одночасно і з Deep store (якісь старі), і з Middle Manager (які отримуємо прямо зараз зі стриму), то Брокер формує та надсилає окремі підзапити до Historical та Middle Manager, де кожен з них виконає свою частину та поверне дані Брокера. Брокер їх агрегує і відповідає клієнту з фінальним результатом.

Далі, спробуємо цю справу розгорнути і потикати гілочкою, а потім зверху додамо моніторингу.

Запуск Druid в Kubernetes

Запускаємо Minikube, додаємо пам’яті і ЦПУ, щоб спокійно запускати всі поди, тут виділяємо 24 RAM і 8 ядер – там Java, най їсть:

[simterm]

$ minikube start --memory 12000 --cpus 8
😄  minikube v1.26.1 on Arch "rolling"
✨  Automatically selected the virtualbox driver
👍  Starting control plane node minikube in cluster minikube
🔥  Creating virtualbox VM (CPUs=8, Memory=12000MB, Disk=20000MB) ...
🐳  Preparing Kubernetes v1.24.3 on Docker 20.10.17 ...
    ▪ Generating certificates and keys ...
    ▪ Booting up control plane ...
    ▪ Configuring RBAC rules ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟  Enabled addons: storage-provisioner, default-storageclass
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

[/simterm]

Перевіряємо:

[simterm]

$ minikube status
minikube
type: Control Plane
host: Running
kubelet: Running
apiserver: Running
kubeconfig: Configured

[/simterm]

І ноди:

[simterm]

$ kubectl get node
NAME       STATUS   ROLES           AGE   VERSION
minikube   Ready    control-plane   41s   v1.24.3

[/simterm]

Переходимо до встановлення Apache Druid operator.

Установка Druid Operator

Створюємо Namespace для оператора:

[simterm]

$ kubectl create namespace druid-operator
namespace/druid-operator created

[/simterm]

Встановлюємо оператор у цей неймспейс:

[simterm]

$ git clone https://github.com/druid-io/druid-operator.git 
$ cd druid-operator/
$ helm -n druid-operator install cluster-druid-operator ./chart

[/simterm]

Перевіряємо под:

[simterm]

$ kubectl -n druid-operator get pod
NAME                                     READY   STATUS    RESTARTS   AGE
cluster-druid-operator-9c8c44f78-8svhc   1/1     Running   0          49s

[/simterm]

Переходимо до створення кластера.

Запуск Druid Cluster

Створюємо неймспейс:

[simterm]

$ kubectl create ns druid
namespace/druid created

[/simterm]

Встановлюємо ZooKeeper:

[simterm]

$ kubectl -n druid apply -f examples/tiny-cluster-zk.yaml
service/tiny-cluster-zk created
statefulset.apps/tiny-cluster-zk created

[/simterm]

Для створення тестового кластера використовуємо конфіг із прикладів – examples/tiny-cluster.yaml.

Створюємо кластер:

[simterm]

$ kubectl -n druid apply -f examples/tiny-cluster.yaml
druid.druid.apache.org/tiny-cluster created

[/simterm]

Перевіряємо ресурси:

[simterm]

$ kubectl -n druid get all
NAME                                    READY   STATUS              RESTARTS   AGE
pod/druid-tiny-cluster-brokers-0        0/1     ContainerCreating   0          21s
pod/druid-tiny-cluster-coordinators-0   0/1     ContainerCreating   0          21s
pod/druid-tiny-cluster-historicals-0    0/1     ContainerCreating   0          21s
pod/druid-tiny-cluster-routers-0        0/1     ContainerCreating   0          21s
pod/tiny-cluster-zk-0                   1/1     Running             0          40s

NAME                                      TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                      AGE
service/druid-tiny-cluster-brokers        ClusterIP   None         <none>        8088/TCP                     21s
service/druid-tiny-cluster-coordinators   ClusterIP   None         <none>        8088/TCP                     21s
service/druid-tiny-cluster-historicals    ClusterIP   None         <none>        8088/TCP                     21s
service/druid-tiny-cluster-routers        ClusterIP   None         <none>        8088/TCP                     21s
service/tiny-cluster-zk                   ClusterIP   None         <none>        2181/TCP,2888/TCP,3888/TCP   40s

NAME                                               READY   AGE
statefulset.apps/druid-tiny-cluster-brokers        0/1     21s
statefulset.apps/druid-tiny-cluster-coordinators   0/1     21s
statefulset.apps/druid-tiny-cluster-historicals    0/1     21s
statefulset.apps/druid-tiny-cluster-routers        0/1     21s
statefulset.apps/tiny-cluster-zk                   1/1     40s

[/simterm]

Чекаємо, поки піди перейдуть у Running (хвилин 5 зайняло), і прокидаємо порт до Druid Router:

[simterm]

$ kubectl port-forward svc/druid-tiny-cluster-routers 8888:8088 -n druid
Forwarding from 127.0.0.1:8888 -> 8088
Forwarding from [::1]:8888 -> 8088

[/simterm]

Відкриваємо Druid Console – http://localhost:8888:

Можна заглянути в Druid Services – побачимо ті ж сервіси, що бачили у вигляді Kubernetes-подів:

Ось тут є непогане коротке відео з прикладом завантаження тестових даних – заради інтересу можна пройтися.

Переходимо до встановлення Prometheus.

Установка Kube Prometheus Stack

Створюємо неймспейс:

[simterm]

$ kubectl create ns monitoring
namespace/monitoring created

[/simterm]

Встановлюємо KPS:

[simterm]

$ helm -n monitoring install kube-prometheus-stack prometheus-community/kube-prometheus-stack
NAME: kube-prometheus-stack
LAST DEPLOYED: Tue Sep 13 15:24:22 2022
NAMESPACE: monitoring
STATUS: deployed
...

[/simterm]

Перевіряємо поди:

[simterm]

$ kubectl -n monitoring get po
NAME                                                        READY   STATUS              RESTARTS   AGE
alertmanager-kube-prometheus-stack-alertmanager-0           0/2     ContainerCreating   0          22s
kube-prometheus-stack-grafana-595f8cff67-zrvxv              3/3     Running             0          42s
kube-prometheus-stack-kube-state-metrics-66dd655687-nkxpb   1/1     Running             0          42s
kube-prometheus-stack-operator-7bc9959dd6-d52gh             1/1     Running             0          42s
kube-prometheus-stack-prometheus-node-exporter-rvgxw        1/1     Running             0          42s
prometheus-kube-prometheus-stack-prometheus-0               0/2     Init:0/1            0          22s

[/simterm]

Чекаємо кілька хвилин, поки всі стануть Running і відкриваємо собі доступ до Prometheus:

[simterm]

$ kubectl -n monitoring port-forward svc/kube-prometheus-stack-prometheus 9090:9090
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090

[/simterm]

Відкриваємо собі доступ до Grafana:

[simterm]

$ kubectl -n monitoring port-forward svc/kube-prometheus-stack-grafana 8080:80
Forwarding from 127.0.0.1:8080 -> 3000
Forwarding from [::1]:8080 -> 3000

[/simterm]

Grafana dashboard

Отримуємо пароль від Grafana – знаходимо її Secret:

[simterm]

$ kubectl -n monitoring get secret | grep graf
kube-prometheus-stack-grafana                                  Opaque               3      102s

[/simterm]

І отримуємо значення:

[simterm]

$ kubectl -n monitoring get secret kube-prometheus-stack-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
prom-operator

[/simterm]

Для Grafana є готова community дашборда, поки додамо її, а взагалі потім зробимо свою.

Відкриваємо в браузері localhost:8080, логінимся, переходимо в Dashboards > Import:

Вказуємо ID 12155, завантажуємо:

ВибираємоPrometheus як datasource:

Отримуємо таку борду, але поки що без даних:

Запуск Druid Exporter

Клонуємо репозиторій:

[simterm]

$ cd ../
$ git clone https://github.com/opstree/druid-exporter.git
$ cd druid-exporter/

[/simterm]

Перевіряємо Druid Router Service – нам треба його повне ім’я, щоб налаштувати Exporter:

[simterm]

$ kubectl -n druid get svc
NAME                              TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                      AGE
...
druid-tiny-cluster-routers        ClusterIP   None         <none>        8088/TCP                     4m3s
...

[/simterm]

Встановлюємо експортер в неймспейсmonitoring, в параметрі druidURL вказуємо URL сервісу Router нашого Друїд-кластера, дивилися вище, його порт, включаємо створення Kubernetes ServiceMonitor та неймспейс, в якому у нас працює Prometheus (monitoring):

[simterm]

$ helm -n monitoring install druid-exporter ./helm/ --set druidURL="http://druid-tiny-cluster-routers.druid.svc.cluster.local:8088" --set druidExporterPort="8080" --set logLevel="debug" --set logFormat="text" --set serviceMonitor.enabled=true --set serviceMonitor.namespace="monitoring"
NAME: druid-exporter
LAST DEPLOYED: Tue Sep 13 15:28:25 2022
NAMESPACE: monitoring
STATUS: deployed
...

[/simterm]

Відкриваємо доступ до Exporter Service:

[simterm]

$ kubectl -n monitoring port-forward svc/druid-exporter-prometheus-druid-exporter 8989:8080
Forwarding from 127.0.0.1:8989 -> 8080
Forwarding from [::1]:8989 -> 8080

[/simterm]

Перевіряємо метрики:

[simterm]

$ curl -s localhost:8989/metrics | grep -v '#' | grep druid_
druid_completed_tasks 0
druid_health_status{druid="health"} 1
druid_pending_tasks 0
druid_running_tasks 0
druid_waiting_tasks 0

[/simterm]

Окей – вже щось, хоча поки що мало. Додаємо збір даних у Prometheus, а потім додамо ще метрик.

Prometheus ServiceMonitor

Перевіряємо Prometheus Service Discovery – тут повинні бути всі наші СервісМонітори:

Перевірити, чи створено Druid ServiceMonitor в Kubernetes :

[simterm]

$ kubectl -n monitoring get servicemonitors
NAME                                             AGE
druid-exporter-prometheus-druid-exporter         87s
...

[/simterm]

Перевіряємо ресурс Prometheus - йогоserviceMonitorSelector:

[simterm]

$ kubectl -n monitoring get prometheus -o yaml | yq .items[].spec.serviceMonitorSelector.matchLabels
{
  "release": "kube-prometheus-stack"
}

[/simterm]

Редагуємо Druid ServiceMonitor:

[simterm]

$ kubectl -n monitoring edit servicemonitor druid-exporter-prometheus-druid-exporter

[/simterm]

Додаємо лейблу release: kube-prometheus-stack, щоб Prometheus почав з цього ServiceMonitor збирати метрики:

Чекаємо на хвилину-дві, перевіряємо монітори ще раз:

Перевіряємо метрики:

 

Окей – тепер у нас є Druid Cluster, є Prometheus, який збирає метрики – залишилося налаштувати ці самі метрики.

Apache Druid Monitoring

Налаштування моніторингу Друїда включає Emitters і Monitors: емітери “пушать” метрики з Друїда “назовні”, а монітори визначають те, які саме метрики будуть доступні. При цьому для деяких метрик потрібно включати додаткові розширення.

Див:

Усі сервіси мають загальні метрики, наприклад query/time, та свої власні, наприклад для Broker є метрика sqlQuery/time, які можуть бути задані для конкретного сервісу через його runtime.properties.

Druid Metrics Emitters

Щоб включити “емітинг” метрик – редагуємо examples/tiny-cluster.yaml та в common.runtime.properties додаємо:

druid.emitter=http
druid.emitter.logging.logLevel=debug
druid.emitter.http.recipientBaseUrl=http://druid-exporter-prometheus-druid-exporter.monitoring.svc.cluster.local:8080/druid

Зберігаємо, оновлюємо кластер:

[simterm]

$ kubectl -n druid apply -f examples/tiny-cluster.yaml 
druid.druid.apache.org/tiny-cluster configured

[/simterm]

Чекаємо хвилину-дві, щоб піди перезапустилися і почали збиратися метрики, перевіряємо метрики в Експортері:

[simterm]

$ curl -s localhost:8989/metrics | grep -v '#' | grep druid_ | head
druid_completed_tasks 0
druid_emitted_metrics{datasource="",host="druid-tiny-cluster-brokers-0:8088",metric_name="avatica-remote-JsonHandler-Handler/Serialization",service="druid-broker"} 0
druid_emitted_metrics{datasource="",host="druid-tiny-cluster-brokers-0:8088",metric_name="avatica-remote-ProtobufHandler-Handler/Serialization",service="druid-broker"} 0
druid_emitted_metrics{datasource="",host="druid-tiny-cluster-brokers-0:8088",metric_name="avatica-server-AvaticaJsonHandler-Handler/RequestTimings",service="druid-broker"} 0
druid_emitted_metrics{datasource="",host="druid-tiny-cluster-brokers-0:8088",metric_name="avatica-server-AvaticaProtobufHandler-Handler/RequestTimings",service="druid-broker"} 0
druid_emitted_metrics{datasource="",host="druid-tiny-cluster-brokers-0:8088",metric_name="jetty-numOpenConnections",service="druid-broker"} 1
druid_emitted_metrics{datasource="",host="druid-tiny-cluster-coordinators-0:8088",metric_name="compact-task-count",service="druid-coordinator"} 0
druid_emitted_metrics{datasource="",host="druid-tiny-cluster-coordinators-0:8088",metric_name="compactTask-availableSlot-count",service="druid-coordinator"} 0
druid_emitted_metrics{datasource="",host="druid-tiny-cluster-coordinators-0:8088",metric_name="compactTask-maxSlot-count",service="druid-coordinator"} 0
druid_emitted_metrics{datasource="",host="druid-tiny-cluster-coordinators-0:8088",metric_name="coordinator-global-time",service="druid-coordinator"} 2

[/simterm]

Появились druid_emitted_metrics – чудово.

У них у лейбліexported_service вказується з якого сервісу метрика отримана, а в metric_name – яка саме метрика.

Перевіряємо в Prometheus:

Чудово!

Але хочеться більше за метрик – “Need more metrics, my lord!” (c)

Druid Monitors

Наприклад, хочеться знати споживання CPU кожним із процесів/сервісом.

Щоб мати можливість бачити всі системні метрики (без Prometheus Node Exporter) – включаємо org.apache.druid.java.util.metrics.SysMonitor, а для даних по роботі JVM – додамо org.apache.druid.java.util.metrics.JvmMonitor.

У той же файл examples/tiny-cluster.yaml до блоку common.runtime.properties додаємо:

...
druid.monitoring.monitors=["org.apache.druid.java.util.metrics.SysMonitor", "org.apache.druid.java.util.metrics.JvmMonitor"]
...

Зберігаємо, оновлюємо кластер, чекаємо на рестарт подів, і через пару хвилин перевіряємо метрики:

Інша справа!

І дашборда в Графані:

Чудово.

У Running/Failed Tasks у нас все ще No Data, тому що нічого не запускали. Якщо завантажити тестові дані із вже згаданого відео – то з’являться та ці метрики.

Залишилося насправді ще багато чого:

  • додати PostgreSQL як Metadata store
  • налаштувати збір метрик з нього
  • протестувати роботу без ZooKeeper (druid-kubernetes-extensions)
  • потестуватиPrometheus Emitter
  • спробувати Indexer замість Middle Manager
  • налаштувати збір логів (куди? поки не придумали, але швидше за все Promtail && Loki)
  • ну і власне – зібрати нормальну дашборду для Графани

А вже маючи моніторинг можна буде поганяти якісь тести навантаження і погратися з тюнінгом продуктивності кластера, але це все вже в наступних частинах.

Посилання по темі