Service Mesh Tracing: автоматическая трассировка с Istio
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: 8080Deploy:
# 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=100Production 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% для productionRecommendation:
- 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: 256Mi3. 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 для /health4. 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
| Аспект | Istio | OpenTelemetry SDK |
|---|---|---|
| Setup complexity | High (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 usage | High (sidecar per pod) | Low (SDK in app) |
| Kubernetes required | ✅ Yes | ❌ No |
| Multi-language support | ✅ Агностик к языку | ⚠️ Нужна поддержка в SDK |
| Best for | Large K8s deployments | Any environment |
Рекомендация: Hybrid approach - Istio + OTel SDK для best results! 🎯
Практические задания
Задание 1: Deploy приложения в Istio
- Установите minikube + Istio
- Включите sidecar injection для namespace
- Deploy 3-tier приложение (frontend → backend → database)
- Проверьте в Jaeger единый trace через все сервисы
Задание 2: Добавить OTel SDK в один сервис
- Обновите backend service добавив OpenTelemetry SDK
- Создайте manual span для business logic
- Verify в Jaeger что видны оба типа spans (Istio + OTel)
Задание 3: Troubleshoot Missing Spans
- Намеренно сломайте trace propagation (не передавайте headers)
- В Jaeger найдите orphan spans
- Исправьте propagation
- Verify что trace теперь complete
Что дальше
В следующих уроках вернемся к OpenTelemetry Collector и рассмотрим advanced темы: tail-based sampling, performance tuning, security.
Поздравляем! Вы овладели Service Mesh трассировкой! Теперь вы можете получать distributed traces даже из legacy приложений без изменения кода.