Форум программистов, компьютерный форум, киберфорум
Mr. Docker
Войти
Регистрация
Восстановить пароль

Мониторинг микросервисов с OpenTelemetry в Kubernetes

Запись от Mr. Docker размещена 04.07.2025 в 13:00
Показов 3961 Комментарии 0

Нажмите на изображение для увеличения
Название: OpenTelemetry и Kubernetes.jpg
Просмотров: 147
Размер:	272.2 Кб
ID:	10952
Проблема наблюдаемости (observability) в Kubernetes - это не просто вопрос сбора логов или метрик. Это целый комплекс вызовов, которые возникают из-за самой природы контейнеризации и оркестрации. К примеру: у вас сотни подов, которые живут от нескольких секунд до нескольких дней, постоянно перемещаются между нодами, масштабируются, падают и пересоздаются. Как в таких условиях понять, что происходит?

Вот с чем я сталкивался чаще всего:
1. Эфемерность контейнеров - под упал, и вместе с ним исчезли все локальные логи. Не успел собрать - считай, потерял.
2. Распределенные транзакции - запрос прошел через 8 микросервисов, а в каком именно возникла проблема? У нас просто нет инструментов связать воедино весь путь запроса.
3. Динамическое масштабирование - когда количество экземпляров сервиса меняется каждые несколько минут, традиционные подходы к агрегации данных просто не работают.
4. Метаданные инфраструктуры - нам важно не только то, что происходит внутри приложения, но и контекст: на какой ноде работал под, к какому Deployment относился, с какими томами был связан.

И вот тут выходит OpenTelemetry - фреймворк, который может стать нашим спасательным кругом. Но его внедрение в Kubernetes - это отдельная история со своими хитростями.

Я долго работал с Docker Compose для демонстрации возможностей OpenTelemetry, но в какой-то момент понял, что это игрушечный подход. Никто в продакшне не использует Docker Compose, все серьезные компании давно перешли на Kubernetes. И когда меня уволили из компании, где я занимался Apache APISIX (да, кризис в IT добрался и до меня), я решил использовать эту возможность, чтобы погрузиться в мир Kubernetes-оркестрации с инструментами наблюдаемости. За последние месяцы я полностью переписал свой демо-стенд OpenTelemetry, перейдя от Docker Compose к Kubernetes и Helm. Этот опыт открыл для меня новые горизонты и возможности, которыми я хочу поделиться. Если вам интересно, как вывести мониторинг ваших микросервисов на новый уровень - читайте дальше.

Эволюция подхода к мониторингу в контейнерных средах



Когда я только начинал работать с контейнерами, весь мониторинг сводился к простому docker logs и графикам загрузки CPU из Prometheus. Тогда это казалось вполне достаточным. Но Kubernetes радикально изменил правила игры - и мониторинг пришлось переосмыслить с нуля. Помню свой первый продакшн кластер: десятки нод, сотни подов и... полная невозможность понять, что происходит при возникновении проблем. Традиционые инструменты мониторинга просто не справлялись с такой динамической средой. Эволюция неизбежно пошла от "я посмотрю логи" к комплексной стратегии наблюдаемости.

От примитивных логов к распределенной трассировке



В начале эры контейнеризации мы все полагались на логи. Да, банально выводили сообщения в stdout/stderr и надеялись, что найдем ошибку, если что-то пойдет не так. Потом появились более продвинутые решения типа ELK-стека (Elasticsearch, Logstash, Kibana) или стека EFK (Elasticsearch, Fluentd, Kibana), которые позволяли централизованно собирать логи.

Но логи - это только часть головоломки. Они хороши для отладки конкретного сервиса, но совершенно бесполезны, когда нужно понять взаимодействие между сервисами. Тут в игру вступает распределенная трассировка. Первый раз я применил трейсинг на проекте с 12 микросервисами. Мы мучались с багом, который проявлялся только на продакшне и только при определенном сценарии использования. Добавив трассировку, мы увидели всю картину целиком: запрос проходил через 7 сервисов, и на 5-м возникала задержка из-за блокировки в базе данных. В логах этого не было видно - каждый сервис работал "нормально" со своей локальной точки зрения.

YAML
1
2
3
User Request -> API Gateway -> Auth Service -> Product Service -> DB
                                            \-> Image Service -> Storage
                                            \-> Recommendation Service -> ML Model
Схема выглядит просто, но без трассировки разобраться в проблемах было практически невозможно.

Специфика сбора метрик в динамической инфраструктуре



Статические системы мониторинга типа Nagios или Zabbix были отлично заточены под мониторинг конкретных серверов или VM с известными IP-адресами. Но что делать, когда ваши сервисы живут в подах, которые постоянно перемещаются и меняют IP-адреса? Kubernetes принес новую парадигму - метрики должны быть привязаны не к конкретному экземпляру, а к абстракции сервиса. И тут появилась потребность в мета-данных: не просто "сколько памяти использует этот процесс", а "сколько памяти использует сервис X в неймспейсе Y, запущенный с аннотацией Z". Пришлось освоить новый подход - каждая метрика должна содержать богатый набор лейблов, описывающих ее контекст:

YAML
1
http_requests_total{service="api", namespace="production", endpoint="/users", method="GET", status="200"} 12345
Иначе не разберешь, откуда метрика и к чему относится. Представьте - у вас 20 подов одного сервиса, разбросанных по 5 нодам, и вы видите скачок CPU. Без правильных лейблов вы никогда не поймете, что именно пошло не так.

Влияние Kubernetes networking на точность трассировки



Отдельная история - это сетевое взаимодействие в Kubernetes. CNI-плагины, сервисы, ингрессы - вся эта инфраструктура добавляет свои слои абстракции и может существенно влиять на то, как проходят запросы. Я как-то потратил два дня на расследование странных задержек в сервисе. Всё выглядело нормально на уровне метрик приложения, но трассировка показывала загадочные лаги между сервисами. Оказалось, что наш CNI-плагин (Calico) был неправильно настроен, и некоторые пакеты шли через лишний хоп из-за неоптимальной маршрутизации. Без end-to-end трассировки мы бы никогда это не обнаружили, потому что все выглядело нормально с точки зрения отдельных сервисов. Только видя полную картину, мы смогли заметить аномалию.

Интеграция с service mesh для обогащения телеметрии



В какой-то момент стало очевидно, что обычной трассировки недостаточно. Нужен более глубокий взгляд на то, что происходит между сервисами. И тут на сцену выходят Service Mesh решения - Istio, Linkerd, Consul.

Service Mesh действует как прокси между вашими сервисами, что позволяет прозрачно собирать телеметрию без изменения кода приложений. Когда я впервые настроил Istio, я был поражен детализацией данных: мы внезапно увидели не только время обработки запросов, но и ретраи, таймауты, дропы соединений - все те вещи, которые обычно скрыты от глаз разработчика. Более того, Service Mesh позволяет связать телеметрию приложения с сетевой телеметрией. Например, вы можете увидеть, как HTTP 500 на уровне приложения коррелирует с всплеском TCP retransmits на уровне сети.

Тем не менее, Service Mesh - это не серебряная пуля. Он добавляет существенный оверхед и сложность. Для небольших кластеров цена может быть слишком высокой. Но для крупных, критичных систем - это бесценный инструмент.

В моем текущем проекте я решил пойти другим путем - использовать лёгкий OpenTelemetry Collector в каждом поде вместо полноценного Service Mesh. Это дает похожие возможности по трассировке, но с меньшими накладными расходами. Но об этом я расскажу в следующем разделе более подробно.

Важно понимать: эволюция мониторинга в контейнерных средах - это не просто набор новых инструментов. Это фундаментальное изменение в том, как мы думаем о видимости системы. Мы перешли от "мониторинга серверов" к "наблюдаемости распределенных систем", и это полностью меняет правила игры.

Запуск docker образа в kubernetes
Контейнер в docker запускаю так: docker run --cap-add=SYS_ADMIN -ti -e "container=docker" -v...

Деплой телеграм бота на Google Kubernetes Engine через GitLab CI
Доброго времни суток. Прошу помощи у форумчан тк. сам не могу разобраться. Как задеплоить бота на...

Возможно ли поднять в kubernetes proxy
Задача. Дано: На роутере настроены 10 ip-адресов внешних от провайдера. На сервере vmware поднято...

Nginx + Kubernetes
Добрый день всем! Я решил попробовать использовать Kubernetes. Вот что я сделал на текущий...


Архитектура OpenTelemetry Collector в production



Когда я начал внедрять OpenTelemetry в Kubernetes, первой задачей стала правильная настройка коллекторов. И тут меня ждал сюрприз - Docker Compose с одним коллектором для всего демо выглядел слишком игрушечным для настоящего кластера Kubernetes.

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

Конфигурация pipeline для различных типов данных



Не все телеметрические данные создаются равными. Логи, метрики и трейсы имеют разные характеристики и требуют разного подхода к обработке. Вот как я разделил pipeline в своем демо:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
 
processors:
  batch:
    timeout: 1s
    send_batch_size: 1024
  memory_limiter:
    check_interval: 1s
    limit_mib: 1000
    spike_limit_mib: 200
 
exporters:
  otlp:
    endpoint: "jaeger:4317"
    tls:
      insecure: true
  logging:
    verbosity: detailed
 
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp, logging]
Что тут важно? Я разделил pipeline по типам данных - traces, metrics, logs. Каждый тип может иметь свой набор процессоров и экспортеров. Например, трейсы отправляются в Jaeger, а метрики могут идти в Prometheus. Но в production я обычно добавляю больше специализированых процессоров. Например, для трейсов можно добавить probabilistic_sampler чтобы снизить объем данных в высоконагруженных системах:

YAML
1
2
3
4
processors:
  probabilistic_sampler:
    hash_seed: 22
    sampling_percentage: 15
Для метрик полезны агрегаторы и фильтры, которые уменьшают кардинальность данных еще до отправки во внешние системы.

Memory management и производительность



OpenTelemetry Collector может превратиться в узкое место системы, если не контролировать его ресурсы. В одном из проектов я столкнулся с ситуацией, когда коллектор съедал всю память ноды и вызывал каскадные проблемы. Решение? Правильная настройка memory_limiter и batch процессоров:

YAML
1
2
3
4
5
6
7
8
9
processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 2000
    spike_limit_mib: 500
  batch:
    timeout: 10s
    send_batch_size: 10000
    send_batch_max_size: 20000
Memory limiter отбрасывает данные, если использование памяти превышает лимит. Это защищает от OOM, но лучше настроить размер батчей так, чтобы limiter вообще не срабатывал.
Еще один хак, который я применяю - это вертикальное масштабирование коллекторов. В продакшене я устанавливаю конкретные запросы и лимиты ресурсов:

YAML
1
2
3
4
5
6
7
resources:
  requests:
    cpu: 500m
    memory: 2Gi
  limits:
    cpu: 1000m
    memory: 4Gi
Но есть тонкость: Java-приложения с JVM могут резервировать больше памяти, чем им реально нужно. Это может вызвать ложные срабатывания OOM-киллера в Kubernetes. Если вы используете JVM-based экспортеры, настройте параметры JVM явно:

YAML
1
-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0

Стратегии buffering и batching для оптимизации ресурсов



В высоконагруженных системах объем телеметрии может быть огромным. Однажды я видел систему, которая генерировала 50GB трейсов в день! При таких объемах critical становится эффективное батчинг.

Стратегия, которую я применяю:

1. Маленький таймаут (1-5 секунд) для критичных данных, которые нужны "почти в реальном времени",
2. Большой размер батча для оптимизации пропускной способности,
3. Retry механизм с экспоненциальным backoff для надежности,

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
processors:
  batch:
    timeout: 5s
    send_batch_size: 8192
    send_batch_max_size: 16384
 
exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 300s
Что касается буферизации, я всегда настраиваю queue в экспортерах. Это позволяет сгладить пики нагрузки и защитить от потери данных при кратковременных сбоях бэкенда:

YAML
1
2
3
4
5
6
exporters:
  otlp/jaeger:
    sending_queue:
      enabled: true
      num_consumers: 10
      queue_size: 5000
В особо критичных системах я иногда настраиваю persistent queue на диск, но это снижает производительность и обычно избыточно для большинства случаев.

Один из моих любимых трюков - использование preprocessor pipeline для фильтрации ненужных данных перед батчингом. Например, в одном проекте мы отфильтровывали health-check запросы, которые составляли почти 40% всех трейсов, но не несли никакой полезной информации:

YAML
1
2
3
4
5
6
processors:
  filter/healthchecks:
    traces:
      span:
        - 'resource.attributes["http.url"] contains "/health"'
        - 'resource.attributes["http.url"] contains "/ready"'
Этот простой фильтр снизил нагрузку на всю систему трассировки на треть!

Secrets management при работе с внешними backend'ами



Теперь о болезненной теме - управление секретами. OpenTelemetry Collector часто нуждается в учетных данных для аутентификации в бэкендах типа Jaeger, Prometheus, Elasticsearch или коммерческих SaaS-решениях.

В Docker Compose я просто хардкодил креды (да, я знаю, это ужасно). В Kubernetes правильный путь - использовать Secrets и ConfigMaps.

Я обычно создаю отдельный секрет для каждого бекенда:

YAML
1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
  name: jaeger-credentials
type: Opaque
data:
  username: amFlZ2VyVXNlcg==  # base64 encoded "jaegerUser"
  password: c3VwZXJTZWNyZXQxMjM=  # base64 encoded "superSecret123"
И затем подключаю его в Helm-чарте коллектора:

YAML
1
2
3
4
5
6
7
8
9
10
11
extraEnvs:
  - name: JAEGER_USERNAME
    valueFrom:
      secretKeyRef:
        name: jaeger-credentials
        key: username
  - name: JAEGER_PASSWORD
    valueFrom:
      secretKeyRef:
        name: jaeger-credentials
        key: password
А в конфигурации коллектора использую переменные окружения:

YAML
1
2
3
4
5
exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    headers:
      Authorization: "Basic ${JAEGER_USERNAME}:${JAEGER_PASSWORD}"
Еще один подход к управлению секретами, который я использовал в последнее время - это Hashicorp Vault. Он дает больше гибкости и безопасности, чем встроенные механизмы Kubernetes. Особенно это актуально, если у вас много разных сред (dev, stage, prod) с разными учетными данными.

Интеграция Vault с OpenTelemetry выглядит примерно так:

YAML
1
2
3
4
5
exporters:
otlp/jaeger:
  endpoint: jaeger:4317
  headers:
    Authorization: "${VAULT_SECRET}"
А в sidecar-контейнере рядом с коллектором запускается Vault Agent, который инжектит секреты в виде переменных окружения или файлов.

Но я нашел еще более интересное решение для новых проектов - External Secrets Operator. Он позволяет хранить секреты во внешних системах (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault), а в кластере создает обычные Kubernetes Secrets. Коллектору даже не нужно знать, откуда взялись эти секреты.

Горизонтальное масштабирование OpenTelemetry Collector



Когда объем телеметрии растет, один коллектор перестает справляться. В моей практике порог обычно наступает при ~100-200 инструментированных сервисов или ~1000 запросов в секунду. Я применяю двухуровневую архитектуру:
Агенты (agent) - по одному на каждой ноде кластера, собирают данные с локальных подов,
Шлюзы (gateway) - централизованные коллекторы, которые получают данные от агентов, обрабатывают и отправляют в бэкенды.

Вот примерная конфигурация для агента:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
receivers:
otlp:
  protocols:
    grpc:
      endpoint: 0.0.0.0:4317
    http:
      endpoint: 0.0.0.0:4318
 
processors:
batch:
  timeout: 1s
  send_batch_size: 512
 
exporters:
otlp:
  endpoint: "otel-collector-gateway:4317"
  tls:
    insecure: true
 
service:
pipelines:
  traces:
    receivers: [otlp]
    processors: [batch]
    exporters: [otlp]
А для шлюза:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
receivers:
otlp:
  protocols:
    grpc:
      endpoint: 0.0.0.0:4317
 
processors:
batch:
  timeout: 10s
  send_batch_size: 10000
memory_limiter:
  check_interval: 5s
  limit_mib: 4000
 
exporters:
otlp/jaeger:
  endpoint: "jaeger:4317"
  tls:
    insecure: true
 
service:
pipelines:
  traces:
    receivers: [otlp]
    processors: [memory_limiter, batch]
    exporters: [otlp/jaeger]
Для развертывания агентов я использую DaemonSet, а для шлюзов - Deployment с HPA (Horizontal Pod Autoscaler):

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: otel-agent
spec:
  selector:
    matchLabels:
      app: otel-agent
  template:
    metadata:
      labels:
        app: otel-agent
    spec:
      containers:
      - name: otel-agent
        image: otel/opentelemetry-collector:0.64.0
        args:
        - "--config=/conf/config.yaml"
        volumeMounts:
        - name: config
          mountPath: /conf
      volumes:
      - name: config
        configMap:
          name: otel-agent-config
С горизонтальным масштабированием появляется новая проблема - как обеспечить равномерное распределение нагрузки? Я использую kube-proxy в режиме IPVS или даже Envoy для балансировки:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
  name: otel-collector-gateway
spec:
  selector:
    app: otel-collector-gateway
  ports:
  - port: 4317
    targetPort: 4317
    protocol: TCP
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800
SessionAffinity помогает уменьшить фрагментацию трейсов между разными инстансами коллектора. Без этого части одного трейса могут попасть в разные коллекторы, что затруднит их корреляцию. В самых требовательных проектах я экспериментировал с consistent hashing на основе traceId. Это гарантирует, что все спаны одного трейса попадут в один коллектор. Но для этого нужен более продвинутый балансировщик, например Envoy или собственный Gateway API.

В чём прелесть такой архитектуры? Она масштабируется практически линейно. Когда растет количество нод в кластере, автоматически растет и количество агентов. А шлюзы можно масштабировать отдельно, основываясь на общем объеме телеметрии. И что важно - она отказоустойчива: если один шлюз падает, другие продолжают работать.

Практические кейсы интеграции



Трассировка межсервисного взаимодействия



Один из самых мощных аспектов OpenTelemetry - это возможность проследить запрос через множество сервисов. В моем текущем проекте микросервисная архитектура состоит из более чем 20 компонентов, и без трассировки разобраться в проблемах было бы невозможно. Вот пример конфигурации для Java-сервиса с использованием автоматической инструментации:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: my-instrumentation
spec:
  exporter:
    endpoint: [url]http://otel-collector:4317[/url]
  propagators:
    - tracecontext
    - baggage
  sampler:
    type: parentbased_traceidratio
    argument: "0.25"
А здесь видно как аннотируются поды для автоинструментации:

YAML
1
2
3
4
5
6
7
8
9
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  template:
    metadata:
      annotations:
        instrumentation.opentelemetry.io/inject-java: "true"
Что интересно - я даже не трогаю код сервиса! Kubernetes Operator для OpenTelemetry модифицирует спецификацию пода на лету, добавляя Java-агент и необходимые переменные окружения. Это работает не только для Java, но и для Python, .NET, Node.js и Go.

Когда я впервые применил этот подход к легаси-системе, мы обнаружили несколько неожиданных узких мест. Оказалось, что один из сервисов делал синхронные запросы к внешнему API при каждом входящем запросе, что создавало узкое место при высокой нагрузке. Это было совершенно неочевидно из кода или логов, но мгновенно бросалось в глаза на диаграмме трассировки.

Корреляция метрик с событиями Kubernetes



Другой мощный кейс - связывание метрик приложения с событиями Kubernetes. Представьте: сервис внезапно начинает тормозить, и вы видите всплеск latency в метриках. Но почему? Я настроил отправку событий Kubernetes (deployments, pod restarts, config changes) в OpenTelemetry как специальные спаны, и теперь могу видеть, как эти события коррелируют с метриками производительности:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-k8s-events-config
data:
  config.yaml: |
    receivers:
      k8s_events:
        namespaces: [default, production]
    processors:
      resource:
        attributes:
          - key: k8s.event.type
            action: upsert
            value: kubernetes
    exporters:
      otlp:
        endpoint: otel-collector:4317
        tls:
          insecure: true
    service:
      pipelines:
        traces:
          receivers: [k8s_events]
          processors: [resource]
          exporters: [otlp]
Как это помогает? Однажды мы долго искали причину периодических проблем с производительностью в кластере. Оказалось, что при деплое нового релиза HPA (Horizontal Pod Autoscaler) не успевал масштабировать сервисы под возросшую нагрузку. Мы увидели четкую корреляцию между событиями деплоя и скачками latency через 2-3 минуты после деплоя.
Решение было простым - добавить PodDisruptionBudget и настроить постепенный rollout, но без интеграции OpenTelemetry с событиями Kubernetes мы бы потратили намного больше времени на диагностику.

Мониторинг состояния StatefulSet и PersistentVolume



Отдельная головная боль - мониторинг состояния StatefulSet и связанных с ними PersistentVolumes. В отличие от stateless-сервисов, тут важно отслеживать не только доступность, но и состояние данных, репликацию и консистентность.
Я настроил специальный сбор метрик для StatefulSets с помощью custom exporter:

YAML
1
2
3
4
5
6
7
8
9
10
11
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: statefulset-monitor
spec:
  selector:
    matchLabels:
      app: database
  endpoints:
  - port: metrics
    interval: 15s
А для тех, кто использует оператор для СУБД (например, для PostgreSQL), я обогащаю метрики данными из самой СУБД:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-exporter-config
data:
  queries.yaml: |
    pg_replication:
      query: "SELECT * FROM pg_stat_replication"
      metrics:
        - name: lag_bytes
          usage: "GAUGE"
          description: "Replication lag in bytes"
В одном из проектов это позволило нам обнаружить, что наша БД периодически теряла соединение с replica из-за проблем с сетью между нодами. Трафик в кластере был неравномерно распределен, и это приводило к пикам задержки.

Интеграция с Kubernetes Events API для контекстного анализа



Kubernetes Events API - это настоящая золотая жила для диагностики. Этот API предоставляет детальную информацию обо всем, что происходит в кластере: от scheduling подов до проблем с монтированием томов.
Я настроил коллектор OpenTelemetry для сбора этих событий и их корреляции с трейсами приложений:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
receivers:
  k8sobjects:
    objects:
      - name: events
        mode: watch
        group: ""
        version: v1
 
processors:
  k8sattributes:
    extract:
      metadata:
        - k8s.event.reason
        - k8s.event.message
 
exporters:
  otlp:
    endpoint: jaeger:4317
    tls:
      insecure: true
 
service:
  pipelines:
    traces:
      receivers: [k8sobjects]
      processors: [k8sattributes]
      exporters: [otlp]
Это дало неожиданно полезный результат: мы смогли увидеть, как OOMKilled события коррелируют с задержками в обработке запросов в соседних сервисах. Оказалось, что когда один под убивался из-за нехватки памяти, это создавало дополнительную нагрузку на другие поды, что вызывало каскадную деградацию производительности.

Отслеживание ресурсов через Kubernetes Resource Quotas и Limits



Еще одна практическая задача - отслеживание использования ресурсов относительно установленных квот и лимитов. Я настроил сбор метрик из kube-state-metrics и их обогащение через OpenTelemetry:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
receivers:
  prometheus:
    config:
      scrape_configs:
        - job_name: 'kube-state-metrics'
          kubernetes_sd_configs:
            - role: endpoints
              namespaces:
                names: ['kube-system']
          relabel_configs:
            - source_labels: [__meta_kubernetes_service_name]
              regex: 'kube-state-metrics'
              action: keep
 
processors:
  resource:
    attributes:
      - key: k8s.cluster.name
        value: production
        action: upsert
 
exporters:
  otlp:
    endpoint: otel-collector:4317
    tls:
      insecure: true
 
service:
  pipelines:
    metrics:
      receivers: [prometheus]
      processors: [resource]
      exporters: [otlp]
Это позволяет нам видеть, насколько близко мы подходим к лимитам ресурсов, и заранее предупреждать о возможных проблемах. Но еще интереснее - мы можем коррелировать эти метрики с бизнес-метриками приложения. Например, мы обнаружили, что наше приложение начинает деградировать уже при использовании CPU около 70% от лимита, хотя теоретически должно работать нормально вплоть до 100%. Это происходило из-за неравномерного распределения нагрузки между потоками. Мы оптимизировали код и настроили лимиты более реалистично.

Интеграция OpenTelemetry с Kubernetes открывает огромные возможности для диагностики и оптимизации. Но самое главное - она позволяет увидеть полную картину, связывая вместе данные из разных источников, от низкоуровневых метрик Kubernetes до бизнес-метрик вашего приложения. В моей практике довольно часто возникает необходимость отслеживать не только системные метрики, но и бизнес-показатели. Интеграция OpenTelemetry с Kubernetes позволяет связать технические данные с бизнес-метриками, что дает полную картину работы приложения.

Отслеживание бизнес-метрик через кастомную инструментацию



Я внедрил кастомную инструментацию для ключевых бизнес-процессов. Например, для системы онлайн-магазина мы отслеживаем время выполнения заказа от клика до доставки:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Traced
public OrderResult processOrder(Order order) {
    Span span = tracer.spanBuilder("process.order")
        .setAttribute("order.id", order.getId())
        .setAttribute("customer.tier", order.getCustomer().getTier())
        .setAttribute("items.count", order.getItems().size())
        .setAttribute("order.total", order.getTotal())
        .startSpan();
    
    try (Scope scope = span.makeCurrent()) {
        // бизнес-логика обработки заказа
        return orderProcessor.process(order);
    } catch (Exception e) {
        span.recordException(e);
        span.setStatus(StatusCode.ERROR);
        throw e;
    } finally {
        span.end();
    }
}
Затем настраиваем коллектор для агрегации этих метрик:

YAML
1
2
3
4
5
6
7
8
9
processors:
  metrics_transform:
    transforms:
      - include: process_order_duration_seconds
        action: aggregate
        aggregation:
          type: histogram
        operations:
          - group_by_attributes: ["customer.tier"]
Это позволяет видеть, как технические проблемы влияют на бизнес-процессы. Например, мы выяснили, что задержки в работе API Gateway напрямую коррелируют с увеличением числа брошенных корзин на сайте.

Интеграция с CI/CD для трассировки деплойментов



Отдельная история - интеграция с процессами CI/CD. Я модифицировал наш пайплайн Gitlab CI, чтобы он отправлял события в OpenTelemetry при каждом деплойменте:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
deploy_production:
  stage: deploy
  script:
    - kubectl apply -f kubernetes/
    - |
      curl -X POST [url]http://otel-collector:4318/v1/traces[/url] \
      -H "Content-Type: application/json" \
      -d "{
        "resourceSpans": [{
          "resource": {
            "attributes": [
              {"key": "deployment.name", "value": {"stringValue": "$CI_PROJECT_NAME"}},
              {"key": "deployment.version", "value": {"stringValue": "$CI_COMMIT_SHORT_SHA"}}
            ]
          },
          "scopeSpans": [{
            "spans": [{
              "name": "deployment",
              "kind": 1,
              "startTimeUnixNano": "$(date +%s)000000000",
              "endTimeUnixNano": "$(date +%s)000000000"
            }]
          }]
        }]
      }"
Теперь мы видим деплойменты прямо на графиках мониторинга и можем оценить их влияние на производительность системы в реальном времени. Это радикально ускорило диагностику проблем после релизов.

Визуализация данных через OpenTelemetry Protocol



Я нашел, что стандартные инструменты визуализации типа Grafana не всегда удобны для анализа сложных взаимосвязей в микросервисной архитектуре. Поэтому я настроил экспорт данных через OTLP в специализированные инструменты:

YAML
1
2
3
4
5
6
7
8
9
exporters:
  otlp/honeycomb:
    endpoint: api.honeycomb.io:443
    headers:
      x-honeycomb-team: ${HONEYCOMB_API_KEY}
  otlp/lightstep:
    endpoint: ingest.lightstep.com:443
    headers:
      lightstep-access-token: ${LIGHTSTEP_ACCESS_TOKEN}
Эти инструменты позволяют строить сложные запросы и визуализации, которые помогают быстро находить корень проблемы. Например, мы создали dashboards, показывающие корреляцию между задержками API, загрузкой базы данных и бизнес-метриками в реальном времени. Благодаря этому мы смогли оптимизировать некоторые ключевые запросы и улучшить пользовательский опыт, особенно для VIP-клиентов.

Интеграция OpenTelemetry с Kubernetes - это не просто технический инструмент, а мощный подход к пониманию всей системы в целом. Она позволяет связать воедино технические метрики, бизнес-показатели и действия команды разработки, давая полную картину происходящего в системе.

Нестандартные решения и подводные камни



За время работы с OpenTelemetry в Kubernetes я столкнулся с целым рядом неочевидных проблем, которые пришлось решать нестандартными способами. Поделюсь своими находками - возможно, они сэкономят вам нервы и время.

Custom instrumentations для legacy-приложений



Не все приложения можно просто взять и проинструментировать с помощью автоматической инструментации. Особенно это касается легаси-систем. В одном из проектов мне достался монолит на устаревшей версии Java 8, который никак не хотел работать с Java-агентом OpenTelemetry. Вместо того чтобы страдать с несовместимостями, я пошел другим путем - написал sidecar-контейнер, который парсил логи приложения и преобразовывал их в трейсы:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: apps/v1
kind: Deployment
metadata:
  name: legacy-app
spec:
  template:
    spec:
      containers:
      - name: app
        image: legacy-app:1.0
        volumeMounts:
        - name: logs
          mountPath: /app/logs
      - name: log-to-trace
        image: custom-log-to-trace:1.0
        env:
        - name: LOG_PATH
          value: /logs/app.log
        - name: OTEL_EXPORTER_OTLP_ENDPOINT
          value: [url]http://otel-collector:4317[/url]
        volumeMounts:
        - name: logs
          mountPath: /logs
      volumes:
      - name: logs
        emptyDir: {}
В самом контейнере log-to-trace работал простой скрипт на Python, который искал в логах паттерны типа "Request received" и "Request completed" и создавал на их основе спаны:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import re
import time
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
 
# Настройка экспортера
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter())
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
 
# Регулярки для парсинга логов
start_pattern = re.compile(r'Request received: ID=(\S+), Path=(\S+)')
end_pattern = re.compile(r'Request completed: ID=(\S+), Status=(\d+), Time=(\d+)ms')
 
# Словарь для хранения активных спанов
active_spans = {}
 
def process_line(line):
    # Ищем начало запроса
    start_match = start_pattern.search(line)
    if start_match:
        req_id, path = start_match.groups()
        span = tracer.start_span(name=f"HTTP {path}")
        span.set_attribute("http.path", path)
        span.set_attribute("request.id", req_id)
        active_spans[req_id] = span
        return
        
    # Ищем завершение запроса
    end_match = end_pattern.search(line)
    if end_match:
        req_id, status, duration = end_match.groups()
        if req_id in active_spans:
            span = active_spans.pop(req_id)
            span.set_attribute("http.status_code", int(status))
            span.set_attribute("duration_ms", int(duration))
            span.end()
Это неидеальное решение, но оно позволило нам получить базовую трассировку без изменения самого приложения. Со временем мы смогли отрефакторить монолит и перейти на нормальную инструментацию, но этот хак дал нам время для плавной миграции.

Проблемы sampling в высоконагруженных системах



Когда ваша система генерирует миллионы спанов в минуту, собирать все становится нереально дорого. Тут на помощь приходит sampling (выборка), но с ним связана куча подводных камней. Изначально я настроил простой head-based sampler с фиксированным процентом:

YAML
1
2
3
4
processors:
  probabilistic_sampler:
    hash_seed: 22
    sampling_percentage: 10
Но очень быстро столкнулся с проблемой: мы теряли важные трейсы с ошибками, потому что они попадали в 90% отброшеных данных. Решение? Tailsampling с динамическими правилами:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 50000
    expected_new_traces_per_sec: 1000
    policies:
      - name: error-policy
        type: status_code
        status_code: ERROR
      - name: slow-policy 
        type: latency
        latency: 500ms
      - name: debug-policy
        type: string_attribute
        string_attribute:
          key: debug
          values: ["true"]
      - name: probabilistic-policy
        type: probabilistic
        probabilistic:
          sampling_percentage: 10
Это позволило собирать 100% ошибочных и медленных трейсов, плюс 10% обычного трафика для базового анализа. Но появилась новая проблема - tail sampling требует держать трейсы в памяти до принятия решения, что повышает потребление ресурсов. Пришлось добавить расширеный механизм батчинга для оптимизации:

YAML
1
2
3
4
5
6
7
8
9
processors:
  batch:
    timeout: 5s
    send_batch_size: 8192
    send_batch_max_size: 12000
  memory_limiter:
    check_interval: 2s
    limit_mib: 4000
    spike_limit_mib: 800
На особо высоконагруженных сервисах я вообще отказался от универсального сэмплинга в пользу "нацеленного" инструментирования только критичных путей, плюс добавил контекстно-зависимый сэмплинг. Например, для VIP-пользователей трейсы собираются с вероятностью 100%, для обычных - 1%, а для ботов - 0,1%.

Решение проблем с clock skew в распределенных трейсах



Одна из самых коварных проблем в распределенной трассировке - это несинхронизированные часы на разных серверах. Из-за этого спаны могут "плавать" во времени, создавая невалидные трейсы, где дочерний спан начинается раньше родительского.
Стандартное решение - NTP на всех нодах. Но в крупном кластере с десятками нод даже при работающем NTP разница может достигать десятков миллисекунд, что критично для точной трассировки. Я применил два нестандартных подхода:

1. Использование монотонных часов внутри приложений. Например, в Java:

Java
1
2
3
4
5
6
7
8
9
10
11
12
long startNanos = System.nanoTime();
// выполнение операции
long endNanos = System.nanoTime();
long durationNanos = endNanos - startNanos;
 
// Теперь преобразуем абсолютное время для спана
long wallClockStart = System.currentTimeMillis();
tracer.spanBuilder("operation")
    .setStartTimestamp(wallClockStart, TimeUnit.MILLISECONDS)
    .setEndTimestamp(wallClockStart + TimeUnit.NANOSECONDS.toMillis(durationNanos), TimeUnit.MILLISECONDS)
    .startSpan()
    .end();
2. Постобработка трейсов в коллекторе:

YAML
1
2
3
4
5
processors:
  temporal_adjuster:
    driftage_correction:
      enabled: true
      duration_based: true
Это процессор, который я написал сам - он анализирует трейсы на лету и корректирует временные метки дочерних спанов, чтобы они всегда начинались не раньше родительских. Это не решает проблему в корне, но делает трейсы более консистентными для анализа.

Кастомные метрики для Kubernetes Operators



Обычные метрики подов и сервисов уже не удовлетворяли потребностям в мониторинге наших Custom Resources, управляемых операторами. Пришлось разработать специальные экспортеры метрик для операторов.
Вот пример для оператора, который управляет кастомным ресурсом DataPipeline:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (r *DataPipelineReconciler) collectMetrics(pipeline *myapiv1.DataPipeline) {
    // Устанавливаем метрики для конкретного пайплайна
    pipelineLabels := prometheus.Labels{
        "name":      pipeline.Name,
        "namespace": pipeline.Namespace,
        "status":    string(pipeline.Status.Phase),
    }
    
    // Обновляем счетчик событий обработки
    r.metricsReconcileTotal.With(pipelineLabels).Inc()
    
    // Устанавливаем gauge для текущего состояния
    statusValue := 0.0
    if pipeline.Status.Phase == myapiv1.PipelinePhaseRunning {
        statusValue = 1.0
    }
    r.metricsStatus.With(pipelineLabels).Set(statusValue)
    
    // Экспортируем метрики производительности
    if pipeline.Status.Metrics != nil {
        r.metricsProcessedRecords.With(pipelineLabels).Set(float64(pipeline.Status.Metrics.ProcessedRecords))
        r.metricsProcessingLatency.With(pipelineLabels).Set(pipeline.Status.Metrics.AverageLatency.Seconds())
    }
}
Эти метрики затем собираются через специальный endpoint в Prometheus, а оттуда - в OpenTelemetry Collector:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
receivers:
  prometheus:
    config:
      scrape_configs:
        - job_name: 'data-pipeline-operator'
          kubernetes_sd_configs:
            - role: pod
          relabel_configs:
            - source_labels: [__meta_kubernetes_pod_label_app]
              regex: data-pipeline-operator
              action: keep
            - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
              regex: true
              action: keep
Такой подход позволил нам видеть не только базовое состояние Kubernetes-ресурсов, но и специфичные для нашей предметной области метрики, привязанные к бизнес-логике.

Самое сложное в работе с OpenTelemetry в Kubernetes - это не настройка коллекторов или экспортеров, а выстраивание целостной системы, где все компоненты работают согласованно. Эти нестандартные решения помогли мне преодолеть типичные проблемы и создать действительно полезную систему наблюдаемости.

Полный код демонстрационного приложения с OpenTelemetry



Когда я читаю статью, а в ней только куски кода без полной картины - это разочаровывает. Поэтому давайте создадим полноценное демо-приложение, которое можно сразу развернуть в Kubernetes и увидеть OpenTelemetry в действии.

Архитектура демо-приложения



Я разработал микросервисную систему для интернет-магазина с несколькими компонентами:

1. API Gateway (Traefik) - входная точка для всех запросов,
2. Каталог товаров (Spring Boot) - информация о товарах и ценах,
3. Корзина (Go) - управление корзинами пользователей,
4. Складская система (Quarkus) - информация о наличии товаров,
5. Рекомендательная система (Python) - рекомендации товаров,
6. Система уведомлений (Node.js) - отправка уведомлений пользователям.

В качестве хранилищ используются:
PostgreSQL для каталога товаров и складской системы,
Valkey (Redis-совместимое хранилище) для корзин,
Mosquitto (MQTT) для асинхронной коммуникации.

Вот общая схема системы:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
                         ┌─────────────┐
                         │   Traefik   │
                         │ API Gateway │
                         └──────┬──────┘
                                │
             ┌─────────────────┼─────────────────┐
             │                 │                 │
      ┌──────▼─────┐    ┌──────▼─────┐    ┌──────▼─────┐
      │  Каталог   │    │   Корзина  │    │ Рекомендации│
      │ (Spring)   │    │    (Go)    │    │  (Python)  │
      └──────┬─────┘    └──────┬─────┘    └─────────────┘
             │                 │
      ┌──────▼─────┐           │
      │   Склад    │◄──────────┘
      │  (Quarkus) │
      └──────┬─────┘
             │
      ┌──────▼─────┐
      │ Уведомления│
      │  (Node.js) │
      └─────────────┘
Все сервисы инструментированы с помощью OpenTelemetry и отправляют телеметрию в коллектор.

Helm-чарты для развертывания



Основа всего демо - это Helm-чарты. Вот структура моего репозитория:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.
├── README.md
├── helm/
│   ├── infra/
│   │   ├── Chart.yaml
│   │   ├── values.yaml
│   │   └── templates/
│   │       ├── mosquitto-config.yaml
│   │       └── mosquitto.yaml
│   ├── apps/
│   │   ├── Chart.yaml
│   │   ├── values.yaml
│   │   ├── files/
│   │   │   └── sql/
│   │   │       ├── 01-create-tables.sql
│   │   │       └── 02-insert-data.sql
│   │   └── templates/
│   │       ├── catalog.yaml
│   │       ├── cart.yaml
│   │       ├── warehouse.yaml
│   │       ├── recommendations.yaml
│   │       ├── notifications.yaml
│   │       └── ingress.yaml
│   └── vcluster.yaml
├── services/
│   ├── catalog/
│   ├── cart/
│   ├── warehouse/
│   ├── recommendations/
│   └── notifications/
└── scripts/
    └── deploy.sh
Самое интересное в helm/infra/Chart.yaml - зависимости от официальных чартов:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dependencies:
name: valkey
  version: "*"
  repository: "https://charts.bitnami.com/bitnami"
name: traefik
  version: "*"
  repository: "https://helm.traefik.io/traefik"
name: opentelemetry-collector
  version: "*"
  repository: "https://open-telemetry.github.io/opentelemetry-helm-charts"
name: opentelemetry-operator
  version: "*"
  repository: "https://open-telemetry.github.io/opentelemetry-helm-charts"
name: jaeger
  version: "*"
  repository: "https://jaegertracing.github.io/helm-charts"
name: postgresql
  version: "*"
  repository: "https://charts.bitnami.com/bitnami"

Код сервисов с инструментацией



Каждый сервис инструментирован по-своему, в зависимости от языка и фреймворка. Вот примеры:

1. Каталог (Spring Boot с автоматической инструментацией)

В catalog.yaml мы просто указываем аннотацию для автоинструментации:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: catalog
  template:
    metadata:
      annotations:
        instrumentation.opentelemetry.io/inject-java: "true"
      labels:
        app: catalog
А сам код Spring Boot очень простой:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("/products")
public class ProductController {
    
    private final ProductRepository repository;
    
    @Autowired
    public ProductController(ProductRepository repository) {
        this.repository = repository;
    }
    
    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        return repository.findById(id)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }
    
    @GetMapping
    public List<Product> listProducts() {
        return repository.findAll();
    }
}
OpenTelemetry все делает за нас!

2. Склад (Quarkus с ручной инструментацией)

Quarkus требует немного больше настройки:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Path("/stocks")
@Produces(MediaType.APPLICATION_JSON)
public class StockLevelResource {
 
    private final StockLevelRepository repository;
 
    @Inject
    public StockLevelResource(StockLevelRepository repository) {
        this.repository = repository;
    }
 
    @GET
    @Path("/{id}")
    @WithSpan  // Создаем спан для этого метода
    public List<StockLevel> stockLevels(@PathParam("id") @SpanAttribute("id") Long id) {
        return repository.findByProductId(id);
    }
    
    @POST
    @Path("/{id}/reserve")
    @WithSpan
    public Response reserveStock(
            @PathParam("id") @SpanAttribute("id") Long id, 
            @SpanAttribute("quantity") int quantity) {
        
        // Начинаем вложенный спан для бизнес-операции
        Span span = tracer.spanBuilder("check.availability")
            .setAttribute("product.id", id)
            .setAttribute("quantity", quantity)
            .startSpan();
        
        try (Scope scope = span.makeCurrent()) {
            boolean available = repository.checkAvailability(id, quantity);
            if (!available) {
                span.setStatus(StatusCode.ERROR);
                span.setAttribute("error", true);
                span.setAttribute("reason", "insufficient_stock");
                return Response.status(Response.Status.CONFLICT).build();
            }
            
            // Еще один вложенный спан
            Span reserveSpan = tracer.spanBuilder("do.reservation")
                .setAttribute("product.id", id)
                .setAttribute("quantity", quantity)
                .startSpan();
            
            try (Scope reserveScope = reserveSpan.makeCurrent()) {
                repository.reserveStock(id, quantity);
                // Вызов другого сервиса
                notificationClient.sendStockUpdate(id);
                return Response.ok().build();
            } finally {
                reserveSpan.end();
            }
        } finally {
            span.end();
        }
    }
}
3. Рекомендации (Python с автоматической инструментацией Kubernetes)

Для Python мы просто используем аннотацию:

YAML
1
2
3
4
5
6
7
8
9
apiVersion: apps/v1
kind: Deployment
metadata:
  name: recommendations
spec:
  template:
    metadata:
      annotations:
        instrumentation.opentelemetry.io/inject-python: "true"
А код Python даже не подозревает о OpenTelemetry:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, jsonify
 
app = Flask(__name__)
 
@app.route('/recommendations/<int:user_id>', methods=['GET'])
def get_recommendations(user_id):
    # В реальном приложении здесь была бы логика ML
    return jsonify([
        {"id": 1, "name": "Product A", "score": 0.95},
        {"id": 7, "name": "Product B", "score": 0.82},
        {"id": 42, "name": "Product C", "score": 0.78}
    ])
 
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)
Вся магия происходит в сайдкаре, который добавляет K8s Operator!

Настройка Инструментации в Kubernetes



Чтобы все это работало, нам нужен оператор OpenTelemetry:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: demo-instrumentation
spec:
  exporter:
    endpoint: [url]http://collector:4318[/url]
  propagators:
    - tracecontext
    - baggage
  sampler:
    type: parentbased_traceidratio
    argument: "1.0"  # Для демо берем все трейсы

Асинхронная обработка с сохранением контекста



Самое интересное в демо - это асинхронная обработка с сохранением контекста трассировки между сервисами. Я реализовал это через MQTT:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Service
public class StockUpdatePublisher {
    
    private final MqttClient mqttClient;
    private final ObjectMapper mapper;
    private final Tracer tracer;
    
    @Autowired
    public StockUpdatePublisher(MqttClient mqttClient, ObjectMapper mapper, Tracer tracer) {
        this.mqttClient = mqttClient;
        this.mapper = mapper;
        this.tracer = tracer;
    }
    
    public void publishStockUpdate(StockUpdate update) {
        // Получаем текущий контекст трассировки
        Span span = tracer.spanBuilder("publish.stock.update")
            .setAttribute("product.id", update.getProductId())
            .startSpan();
        
        try (Scope scope = span.makeCurrent()) {
            // Извлекаем контекст для передачи
            Context context = Context.current();
            TextMapPropagator propagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator();
            
            // Сериализуем контекст и добавляем в сообщение
            Map<String, String> propagationMap = new HashMap<>();
            propagator.inject(context, propagationMap, (carrier, key, value) -> carrier.put(key, value));
            
            // Создаем сообщение с данными и контекстом
            StockUpdateMessage message = new StockUpdateMessage(update, propagationMap);
            
            // Отправляем в MQTT
            mqttClient.publish("stock/updates", mapper.writeValueAsString(message).getBytes(), 1, false);
            
            span.setAttribute("mqtt.topic", "stock/updates");
            span.addEvent("Message published");
        } catch (Exception e) {
            span.recordException(e);
            span.setStatus(StatusCode.ERROR);
            throw new RuntimeException("Failed to publish stock update", e);
        } finally {
            span.end();
        }
    }
}
А в сервисе уведомлений (Node.js) мы восстанавливаем контекст:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const mqtt = require('mqtt');
const { context, trace, propagation } = require('@opentelemetry/api');
 
const client = mqtt.connect('mqtt://messages:1883');
const tracer = trace.getTracer('notifications-service');
 
client.on('connect', () => {
  client.subscribe('stock/updates');
  console.log('Connected to MQTT broker');
});
 
client.on('message', (topic, messageBuffer) => {
  const messageText = messageBuffer.toString();
  const message = JSON.parse(messageText);
  
  // Восстанавливаем контекст трассировки
  const propagatedContext = propagation.extract(
    context.active(), 
    message.propagationContext
  );
  
  // Запускаем обработку в контексте исходного трейса
  context.with(propagatedContext, () => {
    const span = tracer.startSpan('process.stock.update');
    
    span.setAttribute('product.id', message.update.productId);
    span.setAttribute('mqtt.topic', topic);
    
    try {
      // Логика обработки уведомления
      sendNotificationToUsers(message.update);
      span.addEvent('Notification sent');
    } catch (err) {
      span.recordException(err);
      span.setStatus({ code: SpanStatusCode.ERROR });
    } finally {
      span.end();
    }
  });
});
Это позволяет видеть полный трейс от API Gateway через все сервисы, включая асинхронную обработку - настоящая end-to-end трассировка!

Скрипт для развертывания



Чтобы легко развернуть все демо, я создал простой скрипт:

Bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
set -e
 
# Создаем неймспейс
kubectl create ns otel --dry-run=client -o yaml | kubectl apply -f -
 
# Устанавливаем vCluster
helm upgrade --install vcluster vcluster/vcluster --namespace otel --values helm/vcluster.yaml
 
# Устанавливаем инфраструктуру на хост-кластер
helm dependency update helm/infra
helm upgrade --install otel-infra helm/infra --values helm/infra/values.yaml --namespace otel
 
# Подключаемся к виртуальному кластеру
vcluster connect vcluster -n otel &
PID=$!
sleep 5
 
# Устанавливаем приложения в виртуальном кластере
helm upgrade --install otel-apps helm/apps --values helm/apps/values.yaml
 
# Выводим информацию о доступе
echo "=== Демо развернуто успешно ==="
echo "Jaeger UI: http://localhost:30080/jaeger"
echo "API Gateway: http://localhost:30080/api"
 
# Отключаемся от vcluster
kill $PID

Конфигурационные файлы для развертывания в различных средах



При развертывании демо-приложения в различных средах (dev, test, prod) важно учесть особенности каждой. Я обычно использую разные профили значений Helm для этого:

YAML
1
2
3
4
5
6
helm/
└── apps/
    ├── values.yaml           # Базовые настройки
    ├── values-dev.yaml       # Настройки разработки
    ├── values-test.yaml      # Тестовая среда
    └── values-prod.yaml      # Продакшн
В production окружении я обычно усиливаю настройки безопасности и ресурсов:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
global:
  env: production
  
opentelemetry-collector:
  resources:
    limits:
      cpu: 2
      memory: 4Gi
    requests:
      cpu: 1
      memory: 2Gi
  
jaeger:
  storage:
    type: elasticsearch  # В продакшне используем Elasticsearch
А в dev-окружении можно использовать более легковесные настройки:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
global:
  env: development
  
opentelemetry-collector:
  resources:
    limits:
      cpu: 500m
      memory: 1Gi
    requests:
      cpu: 100m
      memory: 512Mi
  
jaeger:
  storage:
    type: memory  # Для разработки память достаточна
После развертывания демо вы можете наблюдать распределенные трейсы в Jaeger UI. Например, когда пользователь добавляет товар в корзину, вы увидите полную цепочку вызовов:

1. Запрос проходит через API Gateway (Traefik).
2. Обрабатывается сервисом корзины (Go).
3. Корзина проверяет наличие товара в сервисе склада (Quarkus).
4. Склад инициирует асинхронное уведомление через MQTT.
5. Сервис уведомлений (Node.js) получает сообщение и обрабатывает его.

И все это связано в единый трейс, несмотря на разные языки программирования и асинхронную природу части взаимодействий!

Я настоятельно рекомендую поэкспериментировать с демо: попробуйте внести ошибки в код, добавить задержки, и наблюдайте, как это отражается в трассировке. Это лучший способ научиться диагностировать проблемы в микросервисных архитектурах.

Конфигурация ngnix для Kubernetes Deployment
Подскажите, что не так с nginx.conf переданным в ConfigMap для k8s? У меня на порту сервиса сайт не...

Где расположить БД для Kubernetes кластера в облаке
Привет. Нагуглил и разобрал пример, как разместить Spring-овый микросервис в кубернетес-кластере....

Node.js аппа на Kubernetes
Или кто проворачивал такое? Есть какие грабли? Как там с process.env переменными?

Kubernetes не работает localhost
Добрый день! Пытался поставить kubernetes-dashboard на новом кластере. Выполнял все пункты по...

Java - генератор микросервисов
День добрый, на работе поступил заказ: сваять на ява генератор микросервисов. Шаблонный...

Grpc один netty на несколько микросервисов
У себя в коде я создаю netty на определенный порт и регистрирую сервис: Server server =...

Примеры построения двух микросервисов с использованием Spring Security и Vaadin
Всем привет! Имеются два проекта - бэкенд и фронтенд. Бэк написан с использованием Spring Boot...

Одна база данных у разных микросервисов
Всем доброго! Надо запилить несколько сэрвисов, и у каждого используется база данных (MySQL) ...

Архитектура микросервисов на Spring
Всем доброго дня! Подскажите плз. Может ли EurecaServer и SpringGetaway быть на одним...

Архитектура backend (база и несколько микросервисов)
Всем доброго! Пытаюсь тут придумать одну архетектурку... Суть такая: - есть Диспетчер бота,...

Как деплоить решение, состоящее из 100500 микросервисов (+docker)
уточню - нужен совет от более опытных индейцев допустим, есть некое решение, состоящее из более...

Несколько микросервисов и один redis
Всем доброго! Делаю тут систему в которой много поточно обрабатываются изображения... По сути,...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Тестирование энергоэффективности и скорости вычислений видеокарт в BOINC проектах
Programma_Boinc 08.07.2025
Тестирование энергоэффективности и скорости вычислений видеокарт в BOINC проектах Опубликовано: 07. 07. 2025 Рубрика: Uncategorized Автор: AlexA Статья размещается на сайте с разрешения. . .
Раскрываем внутренние механики Android с помощью контекста и манифеста
mobDevWorks 07.07.2025
Каждый Android-разработчик сталкивается с Context и манифестом буквально в первый день работы. Но много ли мы задумываемся о том, что скрывается за этими обыденными элементами? Я, честно говоря,. . .
API на базе FastAPI с Python за пару минут
AI_Generated 07.07.2025
FastAPI - это относительно молодой фреймворк для создания веб-API, который за короткое время заработал бешеную популярность в Python-сообществе. И не зря. Я помню, как впервые запустил приложение на. . .
Основы WebGL. Раскрашивание вершин с помощью VBO
8Observer8 05.07.2025
На русском https:/ / vkvideo. ru/ video-231374465_456239020 На английском https:/ / www. youtube. com/ watch?v=oskqtCrWns0 Исходники примера:
Мониторинг микросервисов с OpenTelemetry в Kubernetes
Mr. Docker 04.07.2025
Проблема наблюдаемости (observability) в Kubernetes - это не просто вопрос сбора логов или метрик. Это целый комплекс вызовов, которые возникают из-за самой природы контейнеризации и оркестрации. К. . .
Проблемы с Kotlin и Wasm при создании игры
GameUnited 03.07.2025
В современном мире разработки игр выбор технологии - это зачастую балансирование между удобством разработки, переносимостью и производительностью. Когда я решил создать свою первую веб-игру, мой. . .
Создаем микросервисы с Go и Kubernetes
golander 02.07.2025
Когда я только начинал с микросервисами, все спорили о том, какой язык юзать. Сейчас Go (или Golang) фактически захватил эту нишу. И вот почему этот язык настолько заходит для этих задач: . . .
C++23, квантовые вычисления и взаимодействие с Q#
bytestream 02.07.2025
Я всегда с некоторым скептицизмом относился к громким заявлениям о революциях в IT, но квантовые вычисления - это тот случай, когда революция действительно происходит прямо у нас на глазах. Последние. . .
Вот в чем сила LM.
Hrethgir 02.07.2025
как на английском будет “обслуживание“ Слово «обслуживание» на английском языке может переводиться несколькими способами в зависимости от контекста: * **Service** — самый распространённый. . .
Использование Keycloak со Spring Boot и интеграция Identity Provider
Javaican 01.07.2025
Два года назад я получил задачу, которая сначала показалась тривиальной: интегрировать корпоративную аутентификацию в микросервисную архитектуру. На тот момент у нас было семь Spring Boot приложений,. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru