Проблема наблюдаемости (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 Всем доброго!
Делаю тут систему в которой много поточно обрабатываются изображения...
По сути,...
|