Сквозное наблюдение (observability) в микросервисах
Привет, Хабр!
Сегодня мы поговорим о чем-то, что является неотъемлемой частью современной микросервисной архитектуры, что-то, без чего трудно представить себе успешное и надежное приложение в мире распределенных систем. Да, вы правильно догадались, мы говорим о сквозном наблюдении, или, как его еще называют,»observability.»
Если вы читаете эту статью, то, вероятно, уже являетесь опытным разработчиком и хорошо знакомы с микросервисами. Вы, возможно, создали и поддерживаете множество микросервисов, и, скорее всего, столкнулись с ситуацией, когда отладка и мониторинг становятся настоящей головной болью.
Именно здесь на сцену выходит сквозное наблюдение. Это не просто новомодное словечко или набор инструментов, это фундаментальный компонент, который позволяет нам видеть и понимать, что происходит в наших микросервисах в реальном времени. С его помощью мы можем следить за запросами, анализировать производительность, идентифицировать проблемы и, конечно же, обеспечивать бесперебойную работу наших приложений.
Разберемся подробнее с первым из ключевых компонентов сквозного наблюдения — трассировкой.
Трассировка
1. Контекстное распространение запросов
Контекстное распространение запросов является фундаментальной частью трассировки. Оно позволяет отслеживать путь запроса от момента поступления в систему до его завершения. Для этого контекст (или специальная информация о запросе) передается между различными компонентами системы, такими как микросервисы, базы данных, брокеры сообщений и другие.
Код Python с использованием библиотеки OpenTelemetry:
import opentelemetry.trace as trace
from opentelemetry import propagate, trace
tracer = trace.get_tracer(__name__)
# Создание корневой трассы
with tracer.start_as_current_span("main-request"):
# Отправка запроса к микросервису A
with tracer.start_as_current_span("microservice-A"):
# Здесь можно вставить логику для обработки запроса к микросервису A
pass
# Отправка запроса к микросервису B
with tracer.start_as_current_span("microservice-B"):
# Здесь можно вставить логику для обработки запроса к микросервису B
pass
2. Генерация и агрегирование трасс
После того как мы создали трассу запроса, необходимо собрать информацию о каждом ее сегменте и агрегировать данные для анализа. Это может включать в себя информацию о времени выполнения, статусе запроса, идентификаторах запроса и многое другое.
Пример кода JavaScript с использованием OpenTelemetry:
const { trace, context } = require('@opentelemetry/api');
const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin');
const tracer = trace.getTracer('example-tracer');
// Экспортер для отправки данных трассировки в Zipkin
const exporter = new ZipkinExporter({ serviceName: 'my-service' });
// Установка экспортера
tracer.addSpanProcessor(new SimpleSpanProcessor(exporter));
async function handleRequest(request) {
const span = tracer.startSpan('handleRequest');
// Добавление контекста к запросу
context.with(trace.setSpan(context.active(), span), () => {
// Здесь происходит обработка запроса
});
span.end();
}
3. Применение стандартов, таких как OpenTelemetry и Jaeger
Для обеспечения совместимости и эффективной работы трассировки в микросервисной среде, стоит использовать стандарты и библиотеки, такие как OpenTelemetry и Jaeger.
OpenTelemetry предоставляет API для трассировки, метрик и журналов, что делает его мощным инструментом для сквозного наблюдения.
Jaeger, с другой стороны, является популярным инструментом для сбора и визуализации данных трассировки. Он предоставляет дружественный интерфейс для анализа трассировок и поиска проблем.
Пример кода для настройки Jaeger:
from jaeger_client import Config
config = Config(
config={
'sampler': {
'type': 'const',
'param': 1,
},
'logging': True,
},
service_name='my-service',
)
tracer = config.initialize_tracer()
Помимо вышеуказанных компонентов, трассировка также может включать в себя передачу данных о запросах между микросервисами с помощью HTTP заголовков или метаданных сообщений в системе сообщений.
Основные компоненты сквозного наблюдения включают в себя не только трассировку, как было описано ранее, но и метрики. Метрики — это ценный источник информации о производительности, доступности и общем состоянии ваших микросервисов.
Метрики
1. Сбор, агрегирование и визуализация метрик
Сбор метрик — это процесс собирания данных о производительности и состоянии приложения. Эти данные могут включать в себя информацию о загрузке CPU, использовании памяти, количестве обработанных запросов, времени ответа и многом другом. Метрики могут быть собраны внутри кода приложения и отправлены в инструменты мониторинга.
Код на Python с использованием библиотеки Prometheus:
from prometheus_client import start_http_server, Summary
# Создаем объект для сбора метрик
request_latency = Summary('request_latency_seconds', 'Request latency in seconds')
# Ваш код обработки запросов
@request_latency.time()
def process_request():
# Здесь происходит обработка запроса
pass
if __name__ == '__main__':
# Запускаем HTTP сервер для сбора метрик Prometheus
start_http_server(8000)
2. Инструменты для мониторинга производительности
Для эффективного сбора и анализа метрик, необходимо использовать специализированные инструменты мониторинга производительности. Один из самых популярных инструментов — Prometheus. Это open-source система мониторинга и тревожной системы, спроектированная для сбора, агрегации, запросов и визуализации данных.
Grafana — еще один мощный инструмент для визуализации метрик и создания информативных дашбордов. Он может интегрироваться с Prometheus и позволяет создавать красочные графики и диаграммы, что делает мониторинг производительности более наглядным.
3. Применение Prometheus и Grafana
Пример кода для запуска Prometheus и Grafana в Docker-контейнерах:
# Запуск Prometheus
docker run -d -p 9090:9090 --name prometheus prom/prometheus
# Запуск Grafana
docker run -d -p 3000:3000 --name grafana grafana/grafana
Затем вы можете настроить Prometheus для сбора метрик из ваших микросервисов и настроить Grafana для создания красочных дашбордов.
Примечание: Приведенные выше примеры кода и команды для Docker предоставляют только базовое представление о том, как можно начать использовать Prometheus и Grafana. Реальная настройка и интеграция могут потребовать дополнительных шагов и настроек, в зависимости от вашей конкретной архитектуры и потребностей.
Журналирование (Logging)
Журналирование — это еще один важный компонент сквозного наблюдения, который позволяет вам получать ценные данные о работе ваших микросервисов. Систематически собирая и анализируя журналы, вы можете выявлять проблемы, идентифицировать ошибки и отслеживать поток выполнения ваших приложений.
1. Структурированные логи и их хранение
Хороший подход к журналированию включает использование структурированных логов. Вместо простых строк вы можете сохранять логи в формате JSON или других структурированных форматах. Это позволяет вам легко фильтровать, анализировать и сопоставлять логи. Например:
{
"timestamp": "2023-09-13T12:00:00.000Z",
"level": "INFO",
"message": "User 'john.doe' logged in",
"application": "auth-service",
"component": "authentication",
"user_id": 123
}
Такие структурированные логи бывают особенно полезными при работе с большим количеством микросервисов и несколькими экземплярами каждого из них.
2. Централизованный сбор и анализ журналов
Чтобы обеспечить эффективное журналирование в микросервисной архитектуре, централизованный сбор и анализ журналов является жизненно важным. Это позволяет вам собирать все журналы в одном месте, где вы можете легко их анализировать.
Один из популярных подходов — использование ELK Stack (Elasticsearch, Logstash и Kibana):
Elasticsearch: Это мощный поисковый и аналитический движок, который может использоваться для хранения журналов в распределенном хранилище с возможностью быстрого поиска и анализа.
Logstash: Этот компонент занимается сбором и фильтрацией журналов из различных источников, а затем передает их в Elasticsearch для хранения.
Kibana: Интерфейс для визуализации и анализа данных, хранящихся в Elasticsearch. С его помощью вы можете создавать интерактивные дашборды и графики для мониторинга приложений.
Примеры кода
Python с использованием библиотеки Loguru:
from loguru import logger
# Настройка логгера
logger.add("app.log", rotation="500 MB")
# Пример журналирования
def process_request(request):
logger.info("Request received: {}", request)
# Ваш код обработки запроса
Node.js с использованием библиотеки Winston:
const winston = require('winston');
// Настройка логгера
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'app.log' })
]
});
// Пример журналирования
function processRequest(request) {
logger.info('Request received: %s', request);
// Ваш код обработки запроса
}
Java с использованием библиотеки SLF4J и Logback:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// Настройка логгера
Logger logger = LoggerFactory.getLogger(MyClass.class);
// Пример журналирования
public void processRequest(Request request) {
logger.info("Request received: {}", request);
// Ваш код обработки запроса
}
Журналирование — важнейший компонент сквозного наблюдения, который помогает вам не только выявлять проблемы и ошибки в приложениях, но и понимать, как ваше приложение работает в реальном времени. Управление журналами с использованием структурированных логов и централизованных инструментов анализа, таких как ELK Stack, может значительно улучшить процесс мониторинга и отладки в микросервисной архитектуре.
Интеграция сквозного наблюдения
Интеграция сквозного наблюдения в ваши микросервисы требует использования специализированных инструментов и библиотек, которые позволят вам собирать и передавать данные о трассировке, метриках и логах. Несколько популярных инструментов и библиотек, которые мы рассматривали ранее для разных языков и фреймворков:
OpenTelemetry: OpenTelemetry — это ведущий стандарт для сквозного наблюдения. Он поддерживает множество языков программирования и интеграцию с различными фреймворками. Например, вы можете интегрировать OpenTelemetry в ваше приложение на Python следующим образом:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
# Создание корневой трассы
with tracer.start_as_current_span("main-request"):
# Обработка запроса
Jaeger: Jaeger — это популярная реализация системы сбора и анализа трассировок, которая также предоставляет библиотеки для интеграции с различными языками. Вот пример интеграции с Node.js:
const { initTracer } = require('jaeger-client');
const { Tags, FORMAT_HTTP_HEADERS } = require('opentracing');
const config = {
serviceName: 'my-service',
sampler: {
type: 'const',
param: 1,
},
reporter: {
logSpans: true,
},
};
const options = {
tags: { [Tags.SPAN_KIND]: Tags.SPAN_KIND_RPC_SERVER },
format: FORMAT_HTTP_HEADERS,
};
const tracer = initTracer(config, options);
// Создание и отправка трассы
const span = tracer.startSpan('my-operation');
span.setTag(Tags.HTTP_METHOD, 'GET');
span.finish();
Чтобы упростить процесс создания трассировки, метрик и логов в ваших микросервисах, вы можете воспользоваться автоматизированными инструментами. Эти инструменты автоматически инструментируют ваше приложение, добавляя трассировку и сбор метрик без необходимости вручную вставлять код в каждый сервис.
Пример инструмента для автоматической интеграции — OpenTelemetry Auto-Instrumentation. Этот инструмент поддерживает автоматическую интеграцию с различными фреймворками и библиотеками. Например, для Python вы можете использовать opentelemetry-instrumentation
:
from opentelemetry.instrumentation.auto_instrumentation import install_all_patches
install_all_patches()
Настройка сквозного наблюдения может сильно различаться в зависимости от языка программирования и фреймворка. Вот несколько примеров настройки сквозного наблюдения для разных языков:
Java с использованием Spring Boot и OpenTelemetry:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import io.opentelemetry.exporter.trace.jaeger.JaegerGrpcSpanExporter;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.trace.Tracer;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
// Инициализация и настройка трассировки
Tracer tracer = TracerProvider.createTracer();
JaegerGrpcSpanExporter exporter = JaegerExporterProvider.createExporter();
SimpleSpanProcessor spanProcessor = SimpleSpanProcessor.create(exporter);
SpringApplication.run(MyApplication.class, args);
}
}
Node.js с использованием Express и Jaeger:
const express = require('express');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { LogLevel, BasicTracerProvider } = require('@opentelemetry/tracing');
const app = express();
const tracerProvider = new BasicTracerProvider({
logger: console,
logLevel: LogLevel.ERROR,
});
tracerProvider.addSpanProcessor(new BatchSpanProcessor(new JaegerExporter()));
tracerProvider.register();
app.get('/', (req, res) => {
const span = tracer.startSpan('my-operation');
// Ваша обработка запроса
span.end();
res.send('Hello, World!');
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
Интеграция сквозного наблюдения с вашими микросервисами — это безусловная необходимость, которя обеспечить мониторинг и отладку в микросервисной архитектуре.
Применение на практике
Распределенные отладочные сценарии могут быть сложной задачей в микросервисной архитектуре, но они необходимы для выявления и устранения проблем в системе:
from opentelemetry import trace
import time
tracer = trace.get_tracer(__name__)
def perform_distributed_debugging():
with tracer.start_as_current_span("distributed-debugging"):
# Логика для отладки
try:
# Вызов микросервиса A
with tracer.start_as_current_span("microservice-A"):
# Логика для отладки микросервиса A
pass
# Вызов микросервиса B
with tracer.start_as_current_span("microservice-B"):
# Логика для отладки микросервиса B
pass
except Exception as e:
# Обработка ошибок и логирование
tracer.get_current_span().set_status(trace.Status(StatusCode.ERROR, str(e)))
finally:
# Завершение трассы
tracer.get_current_span().end()
if __name__ == "__main__":
perform_distributed_debugging()
Профилирование производительности помогает выявить узкие места в вашем коде и оптимизировать его для повышения эффективности:
import cProfile
import pstats
def slow_function():
for _ in range(1000000):
pass
if __name__ == "__main__":
profiler = cProfile.Profile()
profiler.enable()
# Вызываем функцию для профилирования
slow_function()
profiler.disable()
# Сохраняем статистику профилирования
stats = pstats.Stats(profiler)
stats.sort_stats(pstats.SortKey.TIME)
stats.print_stats()
Обнаружение и решение проблем в реальном времени — это важная часть поддержки микросервисов в рабочем состоянии:
import logging
def main():
try:
# Ваша логика выполнения микросервиса
result = perform_operation()
logging.info("Operation successful: %s", result)
except Exception as e:
# Логирование ошибки и отправка уведомления
logging.error("An error occurred: %s", str(e))
send_notification("Error in microservice", str(e))
def perform_operation():
# Логика операции
return 42
def send_notification(subject, message):
# Логика отправки уведомления
pass
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main()
Мониторинг и управление версиями микросервисов играют ключевую роль в устойчивости системы:
from flask import Flask, request
from prometheus_client import Counter, generate_latest, CONTENT_TYPE_LATEST
app = Flask(__name__)
# Счетчик для мониторинга версий микросервиса
version_counter = Counter("microservice_version", "Microservice version information")
@app.route("/version", methods=["GET"])
def get_version():
version = request.args.get("version")
version_counter.labels(version).inc()
return f"Microservice version: {version}"
@app.route("/metrics", methods=["GET"])
def metrics():
return generate_latest(), 200, {"Content-Type": CONTENT_TYPE_LATEST}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
Обеспечение безопасности и управление доступом к микросервисам — это неотъемлемая часть их разработки:
const express = require('express');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const app = express();
// Конфигурация Passport
passport.use(new LocalStrategy(
(username, password, done) => {
// Здесь происходит проверка логина и пароля в базе данных
if (isValidUser(username, password)) {
return done(null, { username });
} else {
return done(null, false, { message: 'Неверный логин или пароль' });
}
}
));
passport.serializeUser((user, done) => {
done(null, user.username);
});
passport.deserializeUser((username, done) => {
// Здесь можно получить пользователя из базы данных
done(null, { username });
});
// Маршруты для аутентификации и доступа
app.post('/login',
passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/login',
failureFlash: true
})
);
app.get('/dashboard', (req, res) => {
if (req.isAuthenticated()) {
// Защищенная страница
res.send('Добро пожаловать на защищенную страницу');
} else {
// Перенаправление на страницу входа
res.redirect('/login');
}
});
// Запуск сервера
app.listen(3000, () => {
console.log('Сервер запущен на порту 3000');
});
function isValidUser(username, password) {
// Здесь можно реализовать проверку логина и пароля в базе данных
return username === 'user' && password === 'password';
}
Масштабирование микросервисов и управление нагрузкой важно для обеспечения высокой производительности системы:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
Этот микросервис может быть легко масштабирован с использованием Kubernetes, позволяя управлять высокой нагрузкой и обеспечивая высокую доступность.
Заключение
Применение сквозного наблюдения и лучших практик разработки микросервисов может значительно улучшить управление и мониторинг вашей микросервисной архитектуры. Профессиональные примеры кода и понимание основных концепций позволят вам разрабатывать и поддерживать надежные и высокопроизводительные микросервисы.
А подробнее про микросервисную архитектуру вы можете узнать в рамках одноименного курса от моих коллег из OTUS. Подробнее о курсе.