Сквозное наблюдение (observability) в микросервисах

98c2ee4532606e03f51a8ba215ea809c.jpg

Привет, Хабр!

Сегодня мы поговорим о чем-то, что является неотъемлемой частью современной микросервисной архитектуры, что-то, без чего трудно представить себе успешное и надежное приложение в мире распределенных систем. Да, вы правильно догадались, мы говорим о сквозном наблюдении, или, как его еще называют,»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, может значительно улучшить процесс мониторинга и отладки в микросервисной архитектуре.

Интеграция сквозного наблюдения

Интеграция сквозного наблюдения в ваши микросервисы требует использования специализированных инструментов и библиотек, которые позволят вам собирать и передавать данные о трассировке, метриках и логах. Несколько популярных инструментов и библиотек, которые мы рассматривали ранее для разных языков и фреймворков:

  1. OpenTelemetry: OpenTelemetry — это ведущий стандарт для сквозного наблюдения. Он поддерживает множество языков программирования и интеграцию с различными фреймворками. Например, вы можете интегрировать OpenTelemetry в ваше приложение на Python следующим образом:

from opentelemetry import trace

tracer = trace.get_tracer(__name__)

# Создание корневой трассы
with tracer.start_as_current_span("main-request"):
    # Обработка запроса
  1. 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. Подробнее о курсе.

© Habrahabr.ru