AWS: моніторинг AWS OpenSearch Service кластеру з CloudWatch

Автор |  18/09/2025

Продовжуємо нашу подорож з AWS OpenSearch Service.

Що ми маємо – це маленький кластер AWS OpenSearch Service, 3 трьома data nodes, використовується в ролі vector store для AWS Bedrock Knowledge Bases.

Попередні частини:

  1. AWS: знайомство з OpenSearch Service в ролі vector store
  2. AWS: створення OpenSearch Service cluster та налаштування аутентифікації і авторизації
  3. Terraform: створення AWS OpenSearch Service cluster та юзерів

Вже мали перший production incident 🙂

Запустили якийсь пошук без фільтрів, і наші t3.small.search вмерли через CPU.

Тому давайте глянемо що у нас є з моніторингу всього цього щастя.

Зараз зробимо щось базове, просто з метриками CloudWatch, але в плані моніторингу OpenSearch є кілька рішень:

  • метрики CloudWatch самого OpenSearchService – дані по CPU, Memory, JVM, які ми можемо збирати до VictoriaMetrics і генерити алерти або використати в Grafana dashboard, див. Monitoring OpenSearch cluster metrics with Amazon CloudWatch
  • CloudWatch Events, які генерить OpenSearch Service – див. Monitoring OpenSearch Service events with Amazon EventBridge, можемо їх через SNS відправляти до Opsgenie, а звідти до Slack
  • логи в CloudWatch Logs – можемо збирати в VictoriaLogs, і генерити якісь метрики і алерти, але я під час нашого production incent нічого цікавого в логах не побачив, див. Monitoring OpenSearch logs with Amazon CloudWatch Logs
  • Monitors самого OpenSearch – вміє в Anomaly Detection та власний Alerting, є навіть окремий Terraform resource opensearch_monitor, див. також Configuring alerts in Amazon OpenSearch Service
  • і є Prometheus Exporter Plugin, який відкриває ендпоінт для збору метрик з Prometheus/VictoriaMetrics (але в AWS OpenSearch Managed його додати не можна, хоча сапорт обіцяє, ще feature request є – може колись додадуть)

CloudWatch метрики

Метрик досить багато, але з того, що може бути цікавим нам – враховуючи те, що у нас нема виділених master та coordinator nodes, і ми не використовуємо ultra-warm та cold інстансами.

Cluster metrics:

  • ClusterStatus: green/yellow/red – основний показник стану кластеру, контроль активності шардів даних
  • Shards: active/unassigned/delayedUnassigned/activePrimary/initializing/relocating – більш детальна інформація по стану шардів, але тут просто загальна кількість, без деталізації по конкретним індексам
  • Nodes: кількість нод в кластері – знаючи, скільки має бути живих нод – можемо алертити, коли якась нода відвалиться
  • SearchableDocuments: не те щоб саме для нас було дуже цікаво, але можливо буде корисним потім, аби бачити що взагалі твориться в індексах
  • CPUUtilization: відсоток використання CPU разом на всіх нодах, і це прям must-have
  • FreeStorageSpace: теж корисно моніторити
  • ClusterIndexWritesBlocked: чи все ОК із записами в індекс
  • JVMMemoryPressure та OldGenJVMMemoryPressure: відсоток використання пам’яті JVM heap – далі окремо копнемо в моніторинг JVM, бо це прям окремий геморой
  • AutomatedSnapshotFailure: мабуть, good to know, якщо бекап сфейлиться
  • CPUCreditBalance: нам корисно, бо ми на t3 інстансах (але у нас в CloudWatch її нема)
  • 2xx, 3xx, 4xx, 5xx`: дані по HTTP-запитам і помилкам
    • я тут збираю тільки 5хх для алертів
  • ThroughputThrottle і IopsThrottle: в RDS ми стикались з проблемами доступу до диску, тому варто помоніторити і тут, див. PostgreSQL: AWS RDS Performance and monitoring
    • тут треба буде дивитись на метрики з EBS volume metrics, але для початку можна просто додати алерти на Throttle взагалі
  • HighSwapUsage: аналогічно до попередніх метрик – колись мали біду в RDS, тому краще помоніторити і тут

EBS volume metrics – тут в принципі стандартні метрики EBS, як і для EC2 або RDS:

  • ReadLatency та WriteLatency: затримки читання/запису
    • іноді бувають спайки, тому можна додати
  • ReadThroughput та WriteThroughput: “пропускна здатність”? загальне навантаження на диск, давайте скажемо так
  • DiskQueueDepth: черга I/O операцій
    • у нас в CloudWatch пуста (поки що?), тому скіпаємо
  • ReadIOPS та WriteIOPS: кількість операцій читання/запису на секунду

Instance metrics – тут метрики по кожному OpenSearch інстансу (не серверу, EC2, а самого OpenSearch) на кожній ноді:

  • FetchLatency та FetchRate: як швидко отримуємо дані з шардів (але в CloudWatch теж не знайшов)
  • ThreadCount: кількість потоків в операційній системі, які були створені JVM (Garbadge Collector threads, search threads, write/index threads, etc)
    • в CloudWatch значення стабільне, але в Grafana для загальної картини поки можна додати, подивимось, чи буде там щось цікаве
  • ShardReactivateCount: як часто шарди зі станів cold/inactive переводяться в активні, що потребує ресурсів операційної системи і CPU та пам’яті; ну… може бути, треба глянути чи воно взагалі у нас має якісь значення
    • але в CloudWatch теж нічого – “did not match any metrics
  • ConcurrentSearchRate та ConcurrentSearchLatency: кількість і швидкість одночасних запитів на пошук – може бути цікавим, якщо довго висять багато паралельних запитів
    • але у нас (поки що?) ці значення постійно на нулі, тому скіпаємо
  • SearchRate: кількість пошукових запитів на хвилину, корисно для загальної картини
  • SearchLatency: швидкість виконання пошукових запитів, мабуть, дуже корисно, можна навіть алерт прикрутити
  • IndexingRate та IndexingLatency: аналогічно, але для індексації нових документів
  • SysMemoryUtilization: відсоток використання пам’яті на дата-ноді, але це не дасть повноцінної картини, треба дивитись на пам’ять JVM
  • JVMGCYoungCollectionCount та JVMGCOldCollectionCount: кількість запусків Garbage Collectors, корисно разом з даними по JVM memory, поговоримо далі детальніше
  • SearchTaskCancelled та SearchShardTaskCancelled: про погані новини 🙂 якщо задачі канселяються – щось явно йде не так (або юзер сам перервав виконання запиту, або HTTP connection reset, або таймаути, чи навантаження на кластер)
    • але у нас завжди по нулях, навіть коли кластер падав, тому поки сенсу збору цих метрик не бачу
  • ThreadpoolIndexQueue та ThreadpoolSearchQueue: кількість задач на індексацію та пошук в черзі, коли їх забагато – маємо ThreadpoolIndexRejected та ThreadpoolSearchRejected
    • ThreadpoolIndexQueue в CloudWatch нема взагалі, а ThreadpoolSearchQueue є, але теж постійно в нулях, тому поки скіпаємо
  • ThreadpoolIndexRejected та ThreadpoolSearchRejected: власне, вище
    • в CloudWatch картина аналогічна – ThreadpoolIndexRejected нема взагалі, ThreadpoolSearchRejected в нулях
  • ThreadpoolIndexThreads та ThreadpoolSearchThreads: максимальна кількість потоків операційної системи для індексації та пошуку, якщо всі зайняті – то запити підуть в ThreadpoolIndexQueue/ThreadpoolSearchQueue
    • в OpenSearch є кілька типів пулів для потоків – search, index, write і т.д., і для кожного пулу є показник threads (скільки виділено), queue – черга, rejected – відхилено, бо черга переповнена, див. OpenSearch Threadpool
    • в Node Stats API (GET _nodes/stats/thread_pool) є показник active threads, але в CloudWatch такого не бачу
    • ThreadpoolIndexThreads у нас в CloudWatch взагалі нема, а ThreadpoolSearchThreads статична, поки, думаю, можна скіпнути їхній моніторинг
  • PrimaryWriteRejected: відхилені операції записи в primary-шарди через проблеми в thread pool write або index, чи навантаження на дата-ноді
    • в CloudWatch поки пусті, але додамо збір і алерт
  • ReplicaWriteRejected: відхилені операції записи в replica-шарди – в primary документ додано, але не може записати в репліку
    • в CloudWatch поки пусті, але додамо збір і алерт

k-NN metrics – нам корисно, бо у нас vector store з k-NN:

  • KNNCacheCapacityReached: коли кеш повністю зайнятий (див. далі)
  • KNNEvictionCount: як часто дані з кешу видаляються – ознака, що пам’яті не вистачає
  • KNNGraphMemoryUsage: використання off-heap пам’яті під графи самого вектору
  • KNNGraphQueryErrors: кількість помилок при пошуку в векторах
    • в CloudWatch поки пусті, але додамо збір і алерт
  • KNNGraphQueryRequests: загальна кількість запитів до k-NN graphs
  • KNNHitCount та KNNMissCount: скільки результатів було повернуто з кешу, а скільки довелось зчитувати з диску
  • KNNTotalLoadTime: швидкість завантаження з диску в кеш (великі графи або завантажений EBS – буде рости час)

Моніторинг Memory

Давайте подумаємо як нам основнім показники помоніторити, і першим – пам’ять, бо це ж Java.

Що у нас є по пам’яті з метрик?

  • SysMemoryUtilization: відсоток використання пам’яті на сервері (дата-ноді) взагалі
  • JVMMemoryPressure: загальний відсоток використання JVM Heap; JVM Heap по дефолту виділяється в 50% від пам’яті серверу, але не більше 32 гіг
  • OldGenJVMMemoryPressure: див. далі
  • KNNGraphMemoryUsage: про це говорили в першому пості – AWS: знайомство з OpenSearch Service в ролі vector store
    • в CloudWatch ще є метрика KNNGraphMemoryUsagePercentage – але в документації її нема

kNN Memory usage

Спершу коротенько про пам’ять під k-NN.

Отже, на EC2 у нас виділяється пам’ять під JVM Heap (50% доступної на сервері), і окремо – off-heap для OpenSearch vector store, де він тримає графи та кеш  vectore store – див. Approximate k-NN search, плюс під саму операційну систему і її файловий кеш.

Якоїсь метрики типу “KNNGraphMemoryAvailable” у нас нема, але маючи KNNGraphMemoryUsagePercentage та KNNGraphMemoryUsage можемо її порахувати:

  • KNNGraphMemoryUsage: у нас зараз 662 мегабайти
  • KNNGraphMemoryUsagePercentage: 60%

Значить, під k-NN graphs виділяється 1 гігабайт поза JVM Heap memory (це на t3.medium.search).

З документації k-Nearest Neighbor (k-NN) search in Amazon OpenSearch Service:

OpenSearch Service uses half of an instance’s RAM for the Java heap (up to a heap size of 32 GiB). By default, k-NN uses up to 50% of the remaining half

Знаючи, що у нас зараз t3.medium.search, на яких видається 4 гігабайти пам’яті – 2 GB йде під JVM Heap, і 1 гігабайт – під k-NN графи.

Основну частину KNNGraphMemory використовує k-NN cache, тобто частина оперативної пам’яті системи, в якій OpenSearch тримає HNSW-графи з векторних індексів, аби не зчитувати їх кожного разу з диску (див.  k-NN clear cache).

Тому корисно мати графіки по EBS IOPS та використанню k-NN cache.

JVM Memory usage

Окей, давайте згадувати що там в Java взагалі відбувається, див. What Is Java Heap Memory?, OpenSearch Heap Size Usage and JVM Garbage Collection та Understanding the JVMMemoryPressure metric changes in Amazon OpenSearch Service.

Якщо дуже спрощено, то:

  • Stack Memory: окрім JVM Heap маємо Stack, який виділяється кожному потоку, де він тримає свої змінні, посилання, параметри запуску
    • задається через -Xss, дефолтне значення від 256 кілобайт до 1 мегабайту, див. Understanding Threads and Locks (не знайшов, як подивитись в OpenSearch Service)
    • якщо маємо багато threads – буде багато пам’яті під їхні стеки
    • очищується, коли thread вмирає
  • Heap Space:
    • використовується для виділення пам’яті, яка доступна всім потокам
    • керується Garbage Collectors (GC)
    • в контексті OpenSearch у нас тут будуть кеши пошуку і індексацій

В Heap memory у нас є:

  • Young Generation: свіженькі дані, усі нові об’єкти
    • дані звідси або видаляються зовсім, або переміщаються в Old Generation
  • Old Generation: сам код процесу OpenSearch, кеші, індексні структури Lucene, великі масиви

Якщо OldGenJVMMemoryPressure забитий – значить, Garbage Collector не може його почистити, бо на дані є посилання, і тоді маємо проблему – бо в Heap нема місця для нових даних, і JVM може впасти з помилкою OutOfMemoryError.

Взагалі “heap pressure” – це коли в Young Gen і Old Gen мало вільної пам’яті, і нема де розмістити нові дані, аби відповісти клієнтам.

Це призводить до частого запуску Garbage Collector, що займає час та ресурси системи – замість обробки запитів від клієнтів.

В результаті latency зростає, індексація нових документів гальмує, або взагалі отримуємо ClusterIndexWritesBlocked – аби уникнути Java OutOfMemoryError, бо при індексації OpenSearch спочатку пише дані в Heap, а потім “скидається” на диск.

Див. Key JVM Metrics to Monitor for Peak Java Application Performance.

Отже – для картини використання пам’яті моніторимо:

  • SysMemoryUtilization – для загальної картини по стану EC2
    • в нашому випадку тут буде стабільно близько 90%, але це ОК
  • JVMMemoryPressure – для загальної картини по JVM
    • має регулярно чиститись з Garbage Collector (GC)
    • якщо постійно вище 80-90% – є проблеми з запуском GC
  • OldGenJVMMemoryPressure – для даних по Old Generation Heap
    • має бути на рівні 30-40%, якщо вище і не вичищається – то проблеми або з кодом, або з GC
  • KNNGraphMemoryUsage – в нашому випадку треба для загальної картини

І варто додати алерти на HighSwapUsage – у нас вже відбувався активний swapping, коли запустились на t3.small.search, і це показник того, що пам’яті недостатньо.

Збір метрик до VictoriaMetrics

Власне, як вибрати метрики?

Спершу шукаємо їх в CloudWatch Metrics, і дивимось чи взагалі метрика є, і чи вона повертає якісь цікаві дані.

Наприклад, SysMemoryUtilization дає інфу.

Отуто у нас на t3.small.search був спайк, після якого кластер впав:

А ось метрика HighSwapUsage – теж до переїзду на t3.medium.search:

ClusterStatus є:

Shards є, але це по всім індексам, і нема можливості фільтрувати по окремим:

Ну і треба мати на увазі, що збір метрик з CloudWatch теж коштує грошей за API-запити, тому все підряд збирати не варто.

Взагалі для збору метрик з CloudWatch ми користуємось YACE (Yet Another CloudWatch Exporter), але він не підтримує OpenSearch Managed cluser – див. Features.

Тому беремо звичайний експортер – CloudWatch Exporter.

У нас він деплоїться з Helm-чарту моніторингу (див. VictoriaMetrics: створення Kubernetes monitoring stack з власним Helm-чартом), додаємо йому новий конфіг:

...

prometheus-cloudwatch-exporter:
  enabled: true
  serviceAccount:
    name: "cloudwatch-sa"
    annotations:
      eks.amazonaws.com/sts-regional-endpoints: "true"
  serviceMonitor:
    enabled: true
  config: |-
    region: us-east-1
    metrics:

    - aws_namespace: AWS/ES
      aws_metric_name: KNNGraphMemoryUsage
      aws_dimensions: [ClientId, DomainName, NodeId]
      aws_statistics: [Average]

    - aws_namespace: AWS/ES
      aws_metric_name: SysMemoryUtilization
      aws_dimensions: [ClientId, DomainName, NodeId]
      aws_statistics: [Average]

    - aws_namespace: AWS/ES
      aws_metric_name: JVMMemoryPressure
      aws_dimensions: [ClientId, DomainName, NodeId]
      aws_statistics: [Average]

    - aws_namespace: AWS/ES
      aws_metric_name: OldGenJVMMemoryPressure
      aws_dimensions: [ClientId, DomainName, NodeId]
      aws_statistics: [Average]

Зверніть увагу, що для різних метрик можуть бути різні Dimenstions – перевіряємо їх в CloudWatch:

Деплоїмо, перевіряємо:

І навіть цифри вийшли такі, як ми рахували в першому пості – маємо ~130000 документів в production index, по формулі num_vectors * 1.1 * (4*1024 + 8*16) виходить 604032000 байт, або 604.032 мегабайт.

А на графіку маємо 662261 kilobytes – це 662 мегабайти, але по всім індексам разом.

Тепер у VictoriaMetrics у нас є метрики aws_es_knngraph_memory_usage_average, aws_es_sys_memory_utilization_average, aws_es_jvmmemory_pressure_average, aws_es_old_gen_jvmmemory_pressure_average.

Аналогічно додаємо решту.

Для пошуку того, як саме метрики називаються в VictoriaMetrics/Prometheus – відкриваємо порт до CloudWatch Exporter:

$ kk port-forward svc/atlas-victoriametrics-prometheus-cloudwatch-exporter 9106

І з curl та grep шукаємо метрики:

$ curl -s localhost:9106/metrics | grep aws_es
# HELP aws_es_cluster_status_green_maximum CloudWatch metric AWS/ES ClusterStatus.green Dimensions: [ClientId, DomainName] Statistic: Maximum Unit: Count
# TYPE aws_es_cluster_status_green_maximum gauge
aws_es_cluster_status_green_maximum{job="aws_es",instance="",domain_name="atlas-kb-prod-cluster",client_id="492***148",} 1.0 1758014700000
# HELP aws_es_cluster_status_yellow_maximum CloudWatch metric AWS/ES ClusterStatus.yellow Dimensions: [ClientId, DomainName] Statistic: Maximum Unit: Count
# TYPE aws_es_cluster_status_yellow_maximum gauge
aws_es_cluster_status_yellow_maximum{job="aws_es",instance="",domain_name="atlas-kb-prod-cluster",client_id="492***148",} 0.0 1758014700000
# HELP aws_es_cluster_status_red_maximum CloudWatch metric AWS/ES ClusterStatus.red Dimensions: [ClientId, DomainName] Statistic: Maximum Unit: Count
# TYPE aws_es_cluster_status_red_maximum gauge
aws_es_cluster_status_red_maximum{job="aws_es",instance="",domain_name="atlas-kb-prod-cluster",client_id="492***148",} 0.0 1758014700000
...

Створення Grafana dahsboard

ОК, метрики з CloudWatch маємо – їх поки вистачить.

Подумаємо, що ми хочемо бачити в Grafana.

Загальна ідея – така собі “overview” дашборда, де на одній борді будуть відображатись всі головні дані по кластеру.

Які метрики зараз є, і як ми їх можемо використати в Grafana – я їх тут собі виписував, аби не заплутатись, бо їх вийшло багатенько:

  • aws_es_cluster_status_green_maximum, aws_es_cluster_status_yellow_maximum, aws_es_cluster_status_red_maximum: можна зробити одну Stats панель
  • aws_es_nodes_maximum: теж якусь Stats панель – знаємо, скільки має бути, і будемо робити червоним, коли Data Nodes менше, ніж має бути
  • aws_es_searchable_documents_maximum: просто інтересу заради – графіком покажемо кількість документів разом в усіх індексах
  • aws_es_cpuutilization_average: одним графіком по кожній ноді, і якусь Stats з загальною інформацією і різними кольорами
  • aws_es_free_storage_space_maximum: просто Stats
  • aws_es_cluster_index_writes_blocked_maximum: не став додавати в Grafana, тільки алерт
  • aws_es_jvmmemory_pressure_average: графік і Stats
  • aws_es_old_gen_jvmmemory_pressure_average: десь поруч, теж графіком + Stats
  • aws_es_automated_snapshot_failure_maximum: це просто для алерта
  • aws_es_5xx_maximum: і графік, і Stats
  • aws_es_iops_throttle_maximum: графік, аби бачити в порівнянні з іншими даними типу CPU/Mem usage
  • aws_es_throughput_throttle_maximum: графік
  • aws_es_high_swap_usage_maximum: і графік, і Stats – графік, аби бачити в порівнянні з CPU/дисками
  • aws_es_read_latency_average: графік
  • aws_es_write_latency_average: графік
  • aws_es_read_throughput_average: не став додавати, бо забагато графіків
  • aws_es_write_throughput_average: не став додавати, бо забагато графіків
  • aws_es_read_iops_average: графік, корисно, аби розуміти роботу кешу k-NN – якщо його мало (а ми тестили на t3.small.search з 2 гігабайтами загальної пам’яті) – то читання з диску буде багато
  • aws_es_write_iops_average: аналогічно
  • aws_es_thread_count_average: не став додавати, бо воно доволі статичне і якось сильно корисної інформації не побачив
  • aws_es_search_rate_average: теж просто графік
  • aws_es_search_latency_average: аналогічно, десь поруч
  • aws_es_sys_memory_utilization_average: ну, воно постійно буде десь під 90%, поки прибрав з Grafana, але додав в алерти
  • aws_es_jvmgcyoung_collection_count_average: графік, бачити як часто викликається
  • aws_es_jvmgcold_collection_count_average: графік, бачити як часто викликається
  • aws_es_primary_write_rejected_average: графік, але поки не став додавати, бо забагато графіків – тільки алерт
  • aws_es_replica_write_rejected_average: графік, але поки не став додавати, бо забагато графіків – тільки алерт
  • k-NN:
    • aws_es_knncache_capacity_reached_maximum: тільки для warning-алерту
    • aws_es_knneviction_count_average: не став додавати, хоча може бути цікавим
    • aws_es_knngraph_memory_usage_average: не став додавати
    • aws_es_knngraph_memory_usage_percentage_maximum: графік, замість aws_es_knngraph_memory_usage_average
    • aws_es_knngraph_query_errors_maximum: тільки алерт
    • aws_es_knngraph_query_requests_sum: графік
    • aws_es_knnhit_count_maximum: графік
    • aws_es_knnmiss_count_maximum:  графік
    • aws_es_knntotal_load_time_sum: було непогано мати графік, але нема місця на борді

VictoriaMetrics/Prometheus sum(), avg() та max()

Спершу давайте згадаємо які у нас є функції для агрегації даних.

З CloudWatch для OpenSearch ми будемо отримувати два основні типи – counter та gauge:

$ curl -s localhost:9106/metrics | grep cpuutil
# HELP aws_es_cpuutilization_average CloudWatch metric AWS/ES CPUUtilization Dimensions: [ClientId, DomainName, NodeId] Statistic: Average Unit: Percent
# TYPE aws_es_cpuutilization_average gauge
aws_es_cpuutilization_average{job="aws_es",instance="",domain_name="atlas-kb-prod-cluster",node_id="BzX51PLwSRCJ7GrbgB4VyA",client_id="492***148",} 10.0 1758099600000
...

Різниця між ними:

  • counter: значення може тільки збільшувати значення
  • gauge: значення може збільшуватись і зменшуватись

Тут у нас “TYPE aws_es_cpuutilization_average gauge“, бо використання CPU може і збільшуватись, і зменшуватись.

Див. чудово документацію VictoriaMetrics – Prometheus Metrics Explained: Counters, Gauges, Histograms & Summaries:

Як ми його можемо використати в графіках?

Якщо ми просто подивимось на значення – то у нас тут є набір лейбл, кожна формує власні тайм-серії:

  • aws_es_cpuutilization_average{node_id="BzX51PLwSRCJ7GrbgB4VyA"} == 9
  • aws_es_cpuutilization_average{node_id="IIEcajw5SfmWCXe_AZMIpA"} == 28
  • aws_es_cpuutilization_average{node_id="lrsnwK1CQgumpiXfhGq06g"} == 8

З sum() без лейбл ми просто отримаємо суму всіх значень:

Якщо зробимо sum by (node_id) – то отримаємо значення для конкретної тайм-серії, яка тут буде збігатись з вибіркою без sum by ():

(значення міняється, поки пишу і роблю запити)

З max() без фільтрів – отримаємо просто максимальне значення, вибране з усіх отриманих тайм-серій:

А з avg() – середнє значення всіх значень, тобто сума всіх значень поділена на кількість тайм-серій:

(41+46+12)/3
33

Власне, чому я про це став писати окремо – бо з sum() навіть із by (node_id) іноді можна отримати такі во спайки:

Хоча без sum() їх нема:

А траплялись вони через те, що в цей момент перестворювався Pod з CloudWatch Exporter:

І в цей момент ми отримували дані зі старого поду, і з нового.

Тому тут варіант або використовувати max(), або просто avg(). Хоча max() все ж, мабуть, краще, бо нам цікаві “найгірші” показники.

Окей – з цим розібрались, погнали робити дашборду.

Cluster status

Тут хочеться на одній Stats панелі бачити всі три значення – Green, Yellow, Red.

Але так як в Grafana у нас нема if/else, то зробимо “костиль”.

Збираємо всі три метрики, і результат кожної множимо на 1, 2, чи 3:

sum(aws_es_cluster_status_green_maximum) by (domain_name) * 1 +
sum(aws_es_cluster_status_yellow_maximum) by (domain_name) * 2 +
sum(aws_es_cluster_status_red_maximum) by (domain_name) * 3

Відповідно, якщо aws_es_cluster_status_green_maximum == 1, то 1 * 1 == 1, а aws_es_cluster_status_yellow_maximum == 0 і aws_es_cluster_status_red_maximum будуть == 0 – то і множення поверне 0.

А якщо aws_es_cluster_status_green_maximum стане 0, але aws_es_cluster_status_red_maximum буде 1 – то 1 * 2 отримаємо 3, і по значенню 3 будемо міняти показник в Stats-панелі

І додаємо Value mappings з текстом і кольорами:

Отримуємо такий результат:

Nodes status

Тут все просто – знаємо потрібну кількість, поточну отримуємо з aws_es_nodes_maximum:

sum(aws_es_nodes_maximum) by (domain_name)

І знов через Value mappings задаємо значення і кольори:

На випадок, якщо колись збільшимо кількість нод, і забудемо оновити тут значення для “ОК” – то додаємо третій статус, ERR:

CPUUtilization: Stats

Тут зробимо кросивенько – з типом візуалізації Gauge:

avg(aws_es_cpuutilization_average) by (domain_name)

Задаємо Text size та Unit:

І Thresholds:

Description непогано генерить ChatGPT – корисно і девелоперам, і нам самим через півроку, або просто беремо опис з документації AWS:

The percentage of CPU usage for data nodes in the cluster. Maximum shows the node with the highest CPU usage. Average represents all nodes in the cluster.

Додаємо решту Stats:

CPUUtilization: Graph

Тут виведемо графік по CPU кожної ноди – середнє за 5 хвилин:

max(avg_over_time(aws_es_cpuutilization_average[5m])) by (node_id)

І ось теж приклад того, як з sum() з’являлись спайки, яких не було насправді:

Тому робимо max().

Задамо Gradient mode == Opacity, і Unit == percent:

Задаємо Color scheme і Thresholds, включаємо Show thresholds:

В Data links можна задати лінку на сторінку DataNode Health в AWS Console:

https://us-east-1.console.aws.amazon.com/aos/home?region=us-east-1#opensearch/domains/atlas-kb-prod-cluster/data_Node/${__field.labels.node_id}

Всі доступні поля – по Ctrl+Space:

Actions, мабуть, не так давно з’явилось, ще не використовував, але виглядає цікаво – можна щось пушнути:

JVMMemoryPressure: Graph

Тут нам цікаво бачити чи не “залипає” використання пам’яті, і як часто запускається Garbage Collector.

Запит простий – можна зробити max by (node_id), але я зробив просто загальну картину по кластеру:

max(aws_es_jvmmemory_pressure_average)

І графік аналогічно попередньому:

В Desription додаємо пояснення “коли хвилюватись”:

Represents the percentage of JVM heap in use (young + old generation).
Values below 75% are normal. Sustained pressure above 80% indicates frequent GC and potential performance degradation.
Values consistently > 85–90% mean heap exhaustion risk and may trigger ClusterIndexWritesBlocked – investigate immediately.

JVMGCYoungCollectionCount and JVMGCOldCollectionCount

Дуже корисний графік, аби бачити як часто зпускаються Garbage Collects.

В запиті використаємо increase[1m] – побачити як змінилось значення за хвилину:

max(increase(aws_es_jvmgcyoung_collection_count_average[1m])) by (domain_name)

І для Old Gen:

max(increase(aws_es_jvmgcold_collection_count_average[1m])) by (domain_name)

Unit – ops/sec, Decimals задаємо 0, аби мати тільки цілі значення:

KNNHitCount vs KNNMissCount

Тут зробимо дані на секунду – rate():

sum(rate(aws_es_knnhit_count_average[5m]))

І для Cache Miss:

sum(rate(aws_es_knnmiss_count_average[5m]))

Unit ops/s, кольори можемо задати через Overrides:

Статистика тут, до речі, дуже так собі – стабільно багато Cache missed, але чому – поки не розібрались.

Фінальний результат

Збираємо всі графіки, і отримуємо щось таке:

t3.small.search vs t3.medium.search на графіках

І приклад того, як нестача ресурсів, в першу чергу пам’яті, відображається на графіках: у нас були t3.medium.search, потім ми повернули t3.small.search, аби подивитись як воно на перформанс вплине.

t3.small.search – це лише 2 гігабайти пам’яті і 2 ядра CPU.

З цих 2 гіг пам’яті 1 гіг під JVM Heap, 500 мегабайт під k-NN memory, і 500 залишалось на решту процесів.

Ну і результати, цілком очікувані:

  • Garbage Collectors стали запускатись постійно, бо треба було чистити пам’ять, якої не вистачало
  • Read IOPS виріс, бо постійно з диска завантажувались дані до JVM Heap Young і k-NN
  • Search Latency виріс, бо не всі дані були в кеші, і чекали I/O-операцій з диску
  • і CPU utilization підскочив – бо CPU був завантажений і Garbage Collectors, і читанням з диску

Створення Alerts

Ще можна глянути рекомендації від AWS – Recommended CloudWatch alarms for Amazon OpenSearch Service.

OpenSearch ClusterStatus Yellow та OpenSearch ClusterStatus Red: тут просто якщо більше ніж 0:

...
      - alert: OpenSearch ClusterStatus Yellow
        expr: sum(aws_es_cluster_status_yellow_maximum) by (domain_name, node_id) > 0
        for: 1s
        labels:
          severity: warning
          component: backend
          environment: prod
        annotations:
          summary: 'OpenSearch ClusterStatus Yellow status detected'
          description: |-
            The primary shards for all indexes are allocated to nodes in the cluster, but replica shards for at least one index are not
            *OpenSearch Doman*: `{{ "{{" }} $labels.domain_name }}`
          grafana_opensearch_overview_url: 'https://{{ .Values.monitoring.root_url }}/d/b2d2dabd-a6b4-4a8a-b795-270b3e200a2e/aws-opensearch-cluster-cloudwatch'

      - alert: OpenSearch ClusterStatus Red
        expr: sum(aws_es_cluster_status_red_maximum) by (domain_name, node_id) > 0
        for: 1s
        labels:
          severity: critical
          component: backend
          environment: prod
        annotations:
          summary: 'OpenSearch ClusterStatus RED status detected!'
          description: |-
            The primary and replica shards for at least one index are not allocated to nodes in the cluster
            *OpenSearch Doman*: `{{ "{{" }} $labels.domain_name }}`
          grafana_opensearch_overview_url: 'https://{{ .Values.monitoring.root_url }}/d/b2d2dabd-a6b4-4a8a-b795-270b3e200a2e/aws-opensearch-cluster-cloudwatch'
...

Через labels у нас реалізований роутинг алертів в Opsgenie до потрібних каналів Slack, а анотація grafana_opensearch_overview_url використовуються для додавання лінки на Grafana в повідомленні в Slack:

OpenSearch CPUHigh – якщо більше 20% протягом 10 хвилин:
      - alert: OpenSearch CPUHigh
        expr: sum(aws_es_cpuutilization_average) by (domain_name, node_id) > 20
        for: 10m
...

OpenSearch Data Node down – якщо нода впала:

      - alert: OpenSearch Data Node down
        expr: sum(aws_es_nodes_maximum) by (domain_name) < 3
        for: 1s
        labels:
          severity: critical
...

aws_es_free_storage_space_maximum – нам поки сенсу нема.

OpenSearch Blocking Write – алертимо, якщо почались блоки на write:
...
      - alert: OpenSearch Blocking Write
        expr: sum(aws_es_cluster_index_writes_blocked_maximum) by (domain_name) >= 1
        for: 1s
        labels:
          severity: critical
...

Ну і решта алертів, які я поки що додав:

...
      - alert: OpenSearch AutomatedSnapshotFailure 
        expr: sum(aws_es_automated_snapshot_failure_maximum) by (domain_name) >= 1
        for: 1s
        labels:
          severity: critical
...
      - alert: OpenSearch 5xx Errors 
        expr: sum(aws_es_5xx_maximum) by (domain_name) >= 1
        for: 1s
        labels:
          severity: critical
...
      - alert: OpenSearch IopsThrottled
        expr: sum(aws_es_iops_throttle_maximum) by (domain_name) >= 1
        for: 1s
        labels:
          severity: warning
...
      - alert: OpenSearch ThroughputThrottled
        expr: sum(aws_es_throughput_throttle_maximum) by (domain_name) >= 1
        for: 1s
        labels:
          severity: warning
...
      - alert: OpenSearch SysMemoryUtilization High Warning
        expr: avg(aws_es_sys_memory_utilization_average) by (domain_name) >= 95
        for: 5m
        labels:
          severity: warning
...
      - alert: OpenSearch PrimaryWriteRejected High
        expr: sum(aws_es_primary_write_rejected_maximum) by (domain_name) >= 1
        for: 1s
        labels:
          severity: critical
...
      - alert: OpenSearch KNNGraphQueryErrors High
        expr: sum(aws_es_knngraph_query_errors_maximum) by (domain_name) >= 1
        for: 1s
        labels:
          severity: critical
...
      - alert: OpenSearch KNNCacheCapacityReached
        expr: sum(aws_es_knngraph_query_errors_maximum) by (domain_name) >= 1
        for: 1s
        labels:
          severity: warning
...

По ходу використання подивимось, що ще можна додати.