Перейти к содержимому
К программе курса
Распределенная трассировка: от основ до production
13 / 1872%

Service Mesh Tracing: автоматическая трассировка с Istio

75 минут

Service Mesh Tracing: автоматическая трассировка с Istio

Цель урока

Научиться использовать Service Mesh для автоматической трассировки БЕЗ изменения кода. Вы узнаете:

  • Что такое Service Mesh и sidecar pattern
  • Как Istio/Envoy генерируют traces автоматически
  • Setup Istio + Jaeger в Kubernetes
  • Automatic vs Manual instrumentation - когда что использовать
  • Hybrid approach: Istio для infra + OTel для business logic
  • Troubleshooting missing spans в mesh

Готовые примеры кода

Полный Istio + Kubernetes setup с тремя микросервисами БЕЗ кода трассировки.

Готовые примеры: Istio Service Mesh

См. директорию: 12-istio-tracing/ в примерах курса

Быстрый старт:

cd 12-istio-tracing
 
# Запустить Kubernetes кластер
minikube start --memory=8192 --cpus=4
 
# Установить Istio
istioctl install --set profile=demo -y
 
# Включить sidecar injection
kubectl label namespace default istio-injection=enabled
 
# Задеплоить приложения
kubectl apply -f k8s-manifests/
 
# Установить Jaeger addon
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.20/samples/addons/jaeger.yaml
 
# Port-forward Jaeger UI
kubectl port-forward -n istio-system svc/tracing 16686:80
 
# Открыть Jaeger UI
open http://localhost:16686

Что включено:

  • 3 микросервиса (Node.js, Python, Go) БЕЗ OTel кода
  • Kubernetes manifests с Istio labels
  • Istio configuration files
  • Hybrid example (Istio + OTel SDK)
  • Автоматическая трассировка через Envoy sidecar

Требования: minikube или kind, istioctl, kubectl

Подробные инструкции: README.md


Что такое Service Mesh?

Service Mesh - это инфраструктурный слой для управления service-to-service коммуникацией в microservices.

Sidecar Pattern

Ключевая идея: Каждый pod в Kubernetes получает sidecar container (Envoy proxy), который:

  • Перехватывает весь входящий/исходящий traffic
  • Автоматически добавляет trace headers
  • Генерирует spans для каждого HTTP/gRPC call
  • Отправляет traces в Jaeger

Результат: Трассировка работает без изменения кода приложения! 🎉


Istio + Jaeger: Setup

Требования

  • Kubernetes cluster (minikube, kind, GKE, EKS, AKS)
  • kubectl установлен
  • Helm (опционально, но рекомендуется)

Шаг 1: Установка Kubernetes cluster (minikube)

# Установка minikube (если еще нет)
# macOS:
brew install minikube
 
# Linux:
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
 
# Запуск cluster
minikube start --cpus=4 --memory=8192 --kubernetes-version=v1.28.0
 
# Проверка
kubectl get nodes

Альтернатива: kind (Kubernetes in Docker)

# Установка kind
brew install kind  # macOS
# или
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
 
# Создание cluster
kind create cluster --name istio-demo
 
# Проверка
kubectl cluster-info

Шаг 2: Установка Istio

# Скачивание Istio
curl -L https://istio.io/downloadIstio | sh -
cd istio-1.20.0  # версия может отличаться
 
# Добавление istioctl в PATH
export PATH=$PWD/bin:$PATH
 
# Установка Istio с demo profile (включает Jaeger!)
istioctl install --set profile=demo -y
 
# Проверка установки
kubectl get pods -n istio-system

Что установилось:

NAME                                    READY   STATUS
istiod-xxx                              1/1     Running   # Control plane
istio-ingressgateway-xxx                1/1     Running   # Ingress
istio-egressgateway-xxx                 1/1     Running   # Egress
jaeger-xxx                              1/1     Running   # Jaeger! ✅

Шаг 3: Включение Sidecar Injection

# Включаем автоматический sidecar injection для namespace
kubectl label namespace default istio-injection=enabled
 
# Проверка
kubectl get namespace -L istio-injection

Что происходит: Все новые pods в namespace default автоматически получат Envoy sidecar!


Шаг 4: Настройка Tracing

Istio уже настроен отправлять traces в Jaeger! Проверим конфигурацию:

# Посмотреть настройки tracing
kubectl get configmap istio -n istio-system -o yaml | grep -A 10 tracing

Дефолтная конфигурация:

tracing:
  zipkin:
    address: jaeger-collector.istio-system:9411 # Jaeger endpoint
  sampling: 1.0 # 100% sampling (для demo)

Production: Измените sampling на 1-10% для уменьшения overhead!


Практический пример: 3 микросервиса

Развернем простую систему:

  • Frontend (Node.js) → Backend (Python) → Database Service (Go)

Создание приложений (БЕЗ трассировки в коде!)

Frontend Service (Node.js):

frontend/app.js:

const express = require("express");
const axios = require("axios");
 
const app = express();
 
app.get("/", async (req, res) => {
  try {
    // ⭐ Обычный HTTP call - БЕЗ трассировки в коде!
    const response = await axios.get("http://backend:8080/api/data");
    res.json({
      message: "Frontend response",
      backend_data: response.data,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
 
app.listen(3000, () => {
  console.log("Frontend running on port 3000");
});

frontend/Dockerfile:

FROM node:18-alpine
WORKDIR /app
COPY package.json .
RUN npm install
COPY app.js .
CMD ["node", "app.js"]

Backend Service (Python):

backend/app.py:

from flask import Flask, jsonify
import requests
 
app = Flask(__name__)
 
@app.route('/api/data')
def get_data():
    # ⭐ Обычный HTTP call - БЕЗ трассировки!
    response = requests.get('http://database:8080/query')
    return jsonify({
        'message': 'Backend response',
        'database_data': response.json()
    })
 
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

Database Service (Go):

database/main.go:

package main
 
import (
    "encoding/json"
    "net/http"
)
 
func queryHandler(w http.ResponseWriter, r *http.Request) {
    data := map[string]string{
        "message": "Data from database",
        "records": "42",
    }
    json.NewEncoder(w).Encode(data)
}
 
func main() {
    http.HandleFunc("/query", queryHandler)
    http.ListenAndServe(":8080", nil)
}

Обратите внимание: НИ В ОДНОМ сервисе нет OpenTelemetry SDK! ✨


Kubernetes Manifests

deployments.yaml:

apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  selector:
    app: frontend
  ports:
    - port: 3000
      targetPort: 3000
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
        version: v1
    spec:
      containers:
        - name: frontend
          image: your-registry/frontend:v1
          ports:
            - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    app: backend
  ports:
    - port: 8080
      targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
        version: v1
    spec:
      containers:
        - name: backend
          image: your-registry/backend:v1
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: database
spec:
  selector:
    app: database
  ports:
    - port: 8080
      targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: database
spec:
  replicas: 1
  selector:
    matchLabels:
      app: database
  template:
    metadata:
      labels:
        app: database
        version: v1
    spec:
      containers:
        - name: database
          image: your-registry/database:v1
          ports:
            - containerPort: 8080

Deploy:

# Build и push Docker images (замените на свой registry)
docker build -t your-registry/frontend:v1 ./frontend
docker build -t your-registry/backend:v1 ./backend
docker build -t your-registry/database:v1 ./database
 
docker push your-registry/frontend:v1
docker push your-registry/backend:v1
docker push your-registry/database:v1
 
# Deploy в Kubernetes
kubectl apply -f deployments.yaml
 
# Проверка - каждый pod должен иметь 2 контейнера (app + envoy)!
kubectl get pods

Ожидаемый результат:

NAME                        READY   STATUS
frontend-xxx                2/2     Running  # ⭐ 2 containers!
backend-xxx                 2/2     Running  # ⭐ 2 containers!
database-xxx                2/2     Running  # ⭐ 2 containers!

Видите 2/2? Это app container + Envoy sidecar! 🎉


Проверка Sidecar Injection

# Посмотреть детали pod
kubectl describe pod frontend-xxx
 
# Вывод покажет 2 containers:
# - frontend (ваше приложение)
# - istio-proxy (Envoy sidecar)

Генерация Traffic

# Получить URL frontend service
minikube service frontend --url
# Или для kind:
kubectl port-forward svc/frontend 3000:3000
 
# Отправить несколько запросов
for i in {1..10}; do
  curl http://localhost:3000/
  sleep 1
done

Открытие Jaeger UI

# Port-forward Jaeger
kubectl port-forward -n istio-system svc/jaeger-query 16686:16686
 
# Откройте в браузере
open http://localhost:16686

Что увидите в Jaeger:

ЕДИНЫЙ TRACE через все 3 сервиса - и мы НЕ ПИСАЛИ НИ СТРОЧКИ трассировочного кода! 🏆


Automatic vs Manual Instrumentation

Что Istio делает АВТОМАТИЧЕСКИ

Infrastructure-level spans:

  • HTTP request/response
  • gRPC calls
  • Request/response headers
  • HTTP status codes
  • Latency measurement
  • Retry attempts
  • Circuit breaker events
  • Timeout enforcement

Что Istio НЕ видит

Business-level spans:

  • Database queries (внутри app)
  • Business logic operations
  • External API calls (если не через Envoy)
  • Custom events (user.created, payment.processed)
  • Function-level tracing

Hybrid Approach: Istio + OpenTelemetry

Лучшее решение - комбинировать:

  • Istio для infrastructure spans
  • OpenTelemetry SDK для business spans

Пример: Backend с OTel SDK

backend/app.py (обновленный):

# Добавляем OpenTelemetry SDK
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
 
# Setup OTel
provider = TracerProvider()
processor = BatchSpanProcessor(
    OTLPSpanExporter(endpoint="localhost:4317", insecure=True)
)
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
 
# Auto-instrumentation
FlaskInstrumentor().instrument()
RequestsInstrumentor().instrument()
 
tracer = trace.get_tracer(__name__)
 
app = Flask(__name__)
 
@app.route('/api/data')
def get_data():
    # ⭐ Istio создает span для HTTP request
 
    # ⭐ Мы создаем manual span для business logic
    with tracer.start_as_current_span("processBusinessLogic"):
        # Simulate business logic
        result = expensive_calculation()
 
        # Еще один manual span
        with tracer.start_as_current_span("fetchFromDatabase"):
            # Istio НЕ видит этот internal call
            db_data = internal_db_query()
 
    # ⭐ Istio создает span для outgoing HTTP
    response = requests.get('http://database:8080/query')
 
    return jsonify({
        'message': 'Backend response',
        'database_data': response.json(),
        'processed': result
    })

Результат в Jaeger:

Best of both worlds!


Troubleshooting Missing Spans

Проблема 1: Не вижу traces вообще

Checklist:

# 1. Проверить что sidecar injection включен
kubectl get namespace default --show-labels
# Должно быть: istio-injection=enabled
 
# 2. Проверить что pods имеют 2 контейнера
kubectl get pods
# READY должно быть 2/2
 
# 3. Проверить Jaeger работает
kubectl get pods -n istio-system | grep jaeger
# STATUS должен быть Running
 
# 4. Проверить Envoy logs
kubectl logs <pod-name> -c istio-proxy
# Не должно быть errors
 
# 5. Проверить sampling rate
kubectl get configmap istio -n istio-system -o yaml | grep sampling
# Должно быть > 0

Проблема 2: Trace "сломан" (orphan spans)

Причина: Приложение НЕ передает trace headers дальше.

Решение: Propagate headers в outgoing requests:

// Node.js - ПРАВИЛЬНО
const response = await axios.get("http://backend:8080/api/data", {
  headers: {
    // ⭐ Передаем все trace headers из входящего request
    "x-request-id": req.headers["x-request-id"],
    "x-b3-traceid": req.headers["x-b3-traceid"],
    "x-b3-spanid": req.headers["x-b3-spanid"],
    "x-b3-parentspanid": req.headers["x-b3-parentspanid"],
    "x-b3-sampled": req.headers["x-b3-sampled"],
    "x-b3-flags": req.headers["x-b3-flags"],
  },
});

Или используйте OTel SDK - он делает это автоматически! ✅

Проблема 3: High Latency от Sidecar

Причина: Envoy добавляет overhead (~1-5ms per hop)

Mitigation:

# Увеличить CPU limits для istio-proxy
spec:
  containers:
    - name: istio-proxy
      resources:
        limits:
          cpu: "500m" # Больше CPU = меньше latency
          memory: "256Mi"

Проблема 4: Traces не связываются между services

Причина: Разные trace context formats.

Решение: Настроить Istio на W3C Trace Context:

istioctl install --set meshConfig.defaultConfig.tracing.zipkin.address=jaeger-collector.istio-system:9411 \
  --set meshConfig.enableTracing=true \
  --set values.pilot.traceSampling=100

Production Considerations

1. Sampling Rate

# Istio config для production
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  meshConfig:
    defaultConfig:
      tracing:
        sampling: 1.0 # ❌ 100% для dev
        # sampling: 0.01  # ✅ 1% для production

Recommendation:

  • Development: 100%
  • Staging: 10%
  • Production: 1-5%

2. Resource Limits

# Per-pod sidecar resource limits
apiVersion: v1
kind: ConfigMap
metadata:
  name: istio-sidecar-injector
  namespace: istio-system
data:
  values: |
    global:
      proxy:
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi

3. Exclude Health Checks

# Не трассируем health checks
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: frontend
spec:
  hosts:
    - frontend
  http:
    - match:
        - uri:
            prefix: /health
      route:
        - destination:
            host: frontend
      headers:
        request:
          remove:
            - x-b3-traceid # Не создаем traces для /health

4. Jaeger Storage

Дефолтный Jaeger использует in-memory storage - НЕ для production!

Production setup:

# Установить Jaeger Operator
kubectl create namespace observability
kubectl apply -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.51.0/jaeger-operator.yaml -n observability
 
# Jaeger с Elasticsearch backend
kubectl apply -f - <<EOF
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
  name: jaeger-prod
  namespace: observability
spec:
  strategy: production
  storage:
    type: elasticsearch
    options:
      es:
        server-urls: http://elasticsearch:9200
        index-prefix: jaeger
  collector:
    maxReplicas: 5
  query:
    replicas: 2
EOF

Когда использовать Istio для Tracing?

✅ Используйте Istio когда:

  • Много legacy сервисов без инструментации
  • Kubernetes environment (Istio требует K8s)
  • Нужна infrastructure-level visibility (retries, timeouts, circuit breakers)
  • Команда не хочет добавлять трассировку в код
  • Poly-language environment (Java, Go, Python, Node.js в одном кластере)

❌ НЕ используйте Istio когда:

  • Non-Kubernetes environment (VM, serverless, edge)
  • Нужны business-level spans (Istio не видит внутри app)
  • Small microservices (<5 сервисов) - overhead не оправдан
  • High-performance requirements - sidecar добавляет latency
  • Сложность не нужна - simple app = simple OTel SDK

Сравнение: Istio vs OTel SDK

АспектIstioOpenTelemetry SDK
Setup complexityHigh (Kubernetes, Istio, Envoy)Low (add SDK to app)
Code changes❌ None✅ Minimal
Infrastructure spans✅ Automatic⚠️ Semi-automatic
Business logic spans❌ No visibility✅ Full visibility
Latency overhead~1-5ms per hop~0.1-1ms
Resource usageHigh (sidecar per pod)Low (SDK in app)
Kubernetes required✅ Yes❌ No
Multi-language support✅ Агностик к языку⚠️ Нужна поддержка в SDK
Best forLarge K8s deploymentsAny environment

Рекомендация: Hybrid approach - Istio + OTel SDK для best results! 🎯


Практические задания

Задание 1: Deploy приложения в Istio

  1. Установите minikube + Istio
  2. Включите sidecar injection для namespace
  3. Deploy 3-tier приложение (frontend → backend → database)
  4. Проверьте в Jaeger единый trace через все сервисы

Задание 2: Добавить OTel SDK в один сервис

  1. Обновите backend service добавив OpenTelemetry SDK
  2. Создайте manual span для business logic
  3. Verify в Jaeger что видны оба типа spans (Istio + OTel)

Задание 3: Troubleshoot Missing Spans

  1. Намеренно сломайте trace propagation (не передавайте headers)
  2. В Jaeger найдите orphan spans
  3. Исправьте propagation
  4. Verify что trace теперь complete

Что дальше

В следующих уроках вернемся к OpenTelemetry Collector и рассмотрим advanced темы: tail-based sampling, performance tuning, security.

Поздравляем! Вы овладели Service Mesh трассировкой! Теперь вы можете получать distributed traces даже из legacy приложений без изменения кода.


Дополнительные материалы

Service Mesh Tracing: автоматическая трассировка с Istio — Распределенная трассировка: от основ до production — Potapov.me