Мониторинг и трассировка в Go: от Prometheus до Jaeger

aa0a50f47636b38fb6fb33bd83b0b532.jpg

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

Когда приложения становятся все более сложными и распределенными, и эффективное управление ими требует глубокого понимания происходящего внутри. Мониторинг позволяет нам наблюдать за состоянием приложения, собирать ценные метрики о его работе и предоставлять нам обратную связь в реальном времени. Это помогает в обнаружении и устранении проблем до того, как они станут серьезными. Мониторинг также дает возможность планировать масштабирование и оптимизацию, основываясь на данных, а не на предположениях.

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

Prometheus — это система мониторинга с открытым исходным кодом, разработанная для наблюдения за распределенными системами. Он предоставляет инструменты для сбора и хранения временных рядов данных, а также для создания пользовательских запросов и алертинга на основе этих данных. Prometheus предлагает нативную поддержку для сбора метрик от приложений, что делает его идеальным выбором для мониторинга Go-приложений.

Трассировка, с другой стороны, позволяет нам отслеживать путь выполнения запросов и выявлять проблемы производительности и узкие места в наших сервисах. Jaeger — это инструмент для трассировки с открытым исходным кодом, который обеспечивает сбор и анализ трассировочных данных. Он позволяет нам визуализировать путь выполнения запросов и идентифицировать бутылочные горлышки в наших приложениях.

Мониторинг с Prometheus

Prometheus — это инструмент мониторинга с открытым исходным кодом, созданный для эффективного сбора и хранения временных рядов данных о состоянии системы. Он обладает модульной архитектурой, которая позволяет настраивать и расширять его функциональность под ваши потребности.

  1. Архитектура и компоненты

Архитектура Prometheus представляет собой серверное приложение, которое собирает, хранит и предоставляет метрики. Основные компоненты архитектуры включают:

  • Prometheus Server: Это центральное звено системы, ответственное за сбор данных. Он выполняет периодические запросы к приложениям и устройствам для получения метрик.

  • Storage: Prometheus хранит метрики во временных рядах (time series). Это позволяет анализировать данные на различных временных интервалах.

  • Exporter: Экспортеры — это приложения, которые обеспечивают доступ к метрикам из различных источников. Например, Go-приложение может использовать Prometheus Go-клиент для экспорта метрик в формате, понятном Prometheus.

  • Query Language: Prometheus предоставляет мощный язык запросов PromQL, который позволяет фильтровать и агрегировать данные для создания пользовательских запросов и алертинга.

Пример кода (на Go) для экспорта метрик с использованием Prometheus Go-клиента:

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "net/http"
)

func main() {
    // Создаем счетчик метрик
    metric := prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "my_custom_metric",
            Help: "An example custom metric",
        },
        []string{"label_name"},
    )
    prometheus.MustRegister(metric)

    // Пример инкрементирования счетчика
    metric.WithLabelValues("example_label").Inc()

    // Запускаем HTTP-сервер для экспорта метрик
    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(":9090", nil)
}
  1. Метрики и экспортеры

Прометеус собирает метрики с помощью экспортеров, которые могут быть как встроенными, так и созданными вами. Например, Go-приложение может использовать Prometheus Go-клиент для создания и экспорта метрик. Метрики в Prometheus имеют следующие характеристики:

  • Имя: Уникальное имя метрики, которое используется для идентификации.

  • Метки (Labels): Дополнительные метаданные, позволяющие категоризировать метрику. Метки позволяют группировать и агрегировать метрики.

  • Значение: Числовое значение, представляющее метрику.

  • Таймстэмп: Время, когда метрика была собрана.

Пример создания метрики и экспорта её с использованием Prometheus Go-клиента:

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "net/http"
)

func main() {
    metric := prometheus.NewGauge(
        prometheus.GaugeOpts{
            Name: "example_gauge",
            Help: "An example gauge metric",
        },
    )
    prometheus.MustRegister(metric)

    metric.Set(42.0) // Установка значения метрики

    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(":9090", nil)
}

Интеграция Prometheus в Go-приложения

  1. Использование Prometheus клиента для экспорта метрик

Прометеус предоставляет набор клиентских библиотек для разных языков, включая Go. Эти клиенты упрощают сбор и экспорт метрик из ваших приложений:

import (
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"net/http"
)

func main() {
	// Создаем счетчик метрик
	counter := prometheus.NewCounter(prometheus.CounterOpts{
		Name: "example_counter",
		Help: "An example counter metric",
	})

	// Регистрируем счетчик в реестре метрик
	prometheus.MustRegister(counter)

	// Инкрементируем счетчик
	counter.Inc()

	// Запускаем HTTP-сервер для экспорта метрик
	http.Handle("/metrics", promhttp.Handler())
	http.ListenAndServe(":9090", nil)
}

В этом примере мы создаем счетчик метрик с именем «example_counter» и регистрируем его с помощью prometheus.MustRegister(). Затем мы инкрементируем счетчик на единицу. По умолчанию, метрики экспортируются через HTTP-сервер, который слушает на порту 9090.

  1. Метрики собственного приложения

Представим, у нас есть веб-сервер на Go, и мы хотим собирать метрики о количестве обработанных запросов и времени ответа:

package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	requestsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "http_requests_total",
			Help: "Total number of HTTP requests.",
		},
		[]string{"method"},
	)
	requestDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name: "http_request_duration_seconds",
			Help: "Duration of HTTP requests.",
		},
		[]string{"method"},
	)
)

func main() {
	prometheus.MustRegister(requestsTotal, requestDuration)

	http.Handle("/metrics", promhttp.Handler())
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
	start := time.Now()
	defer func() {
		method := r.Method
		elapsed := time.Since(start).Seconds()
		requestsTotal.WithLabelValues(method).Inc()
		requestDuration.WithLabelValues(method).Observe(elapsed)
	}()

	// Ваш код обработки запроса здесь
	fmt.Fprintf(w, "Hello, World!")
}

В этом примере мы создаем счетчик http_requests_total для подсчета обработанных запросов и гистограмму http_request_duration_seconds для измерения времени ответа. Мы используем метки для категоризации метрик по методу HTTP (например, GET или POST).

Алертинг и оповещения в Prometheus

  1. Конфигурация правил оповещения

Алерты в Prometheus основаны на конфигурационных файлах, в которых определены правила оповещений. Эти правила определяют условия, при которых следует срабатывать алертам, и какие действия следует предпринимать при срабатывании.

Пример конфигурации правила оповещения:

groups:
  - name: example
    rules:
      - alert: HighRequestLatency
        expr: http_request_duration_seconds > 1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "High request latency on {{ $labels.instance }}"
          description: "{{ $labels.instance }} is experiencing high request latency."

      - alert: ServiceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Service {{ $labels.instance }} is down"
          description: "Service {{ $labels.instance }} is not responding to requests."

В этом примере мы определяем два правила оповещения: «HighRequestLatency» и «ServiceDown». Первое правило срабатывает, если среднее время ответа на запросы превышает 1 секунду в течение 5 минут. Второе правило срабатывает, если целевой сервис недоступен в течение 1 минуты.

  1. Интеграция с системами оповещения

Prometheus не предоставляет функциональность отправки оповещений напрямую, но он интегрируется с различными системами оповещения, такими как Alertmanager. Alertmanager — это отдельный компонент, предназначенный для управления оповещениями и их дальнейшей рассылки.

Пример конфигурации Alertmanager:

route:
  group_by: ['alertname']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 12h

receivers:
  - name: 'email-notifications'
    email_configs:
      - to: 'admin@example.com'
        from: 'alertmanager@example.com'
        smarthost: 'smtp.example.com:25'
        auth_username: 'your_username'
        auth_password: 'your_password'

В этой конфигурации мы определяем правила маршрутизации оповещений, такие как интервалы повторений и адреса получателей. В приведенном примере оповещения отправляются на электронную почту.

Теперь, при срабатывании алерта, Alertmanager будет отправлять уведомления на указанные адреса электронной почты, интегрированные системы чата и другие системы оповещения, согласно настройкам.

Трассировка с Jaeger

В контексте мониторинга и отладки, трассировка предоставляет глубокое понимание того, как ваши сервисы взаимодействуют друг с другом и где возникают проблемы.

Преимущества трассировки:

  • Отслеживание запросов: Трассировка позволяет отслеживать, какой путь проходит запрос, начиная с его входа в систему и заканчивая ответом клиенту. Это особенно полезно в распределенных системах, где запрос может проходить через множество сервисов.

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

  • Диагностика ошибок: Трассировка упрощает диагностику ошибок. Вы можете увидеть, в каком сервисе или части приложения возникла проблема, и оперативно реагировать на нее.

Jaeger — это инструмент для сбора, хранения и анализа трассировочных данных в распределенных системах. Принцип работы Jaeger основан на концепции дерева трассировки, где каждый запрос представлен как дерево с узлами, представляющими различные компоненты системы.

Процесс трассировки включает следующие этапы:

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

  • Сбор данных: Собранные данные отправляются в Jaeger Agent, который агрегирует и передает их на центральный сервер Jaeger Collector.

  • Хранение данных: Данные хранятся в центральной базе данных Jaeger. Вы можете настроить различные хранилища данных, включая Elasticsearch, Cassandra и другие.

  • Анализ и визуализация: Данные анализируются и визуализируются с помощью Jaeger UI. Вы можете видеть путь выполнения запросов, статистику времени ответа и многое другое.

Пример инструментирования Go-приложения для сбора трассировочных данных с использованием Jaeger:

import (
    "github.com/opentracing/opentracing-go"
    "github.com/uber/jaeger-client-go"
    "github.com/uber/jaeger-client-go/config"
)

func main() {
    // Создание и настройка Jaeger Tracer
    cfg := config.Configuration{
        ServiceName: "my-service",
        Sampler: &config.SamplerConfig{
            Type:  "const",
            Param: 1,
        },
        Reporter: &config.ReporterConfig{
            LogSpans: true,
        },
    }
    tracer, closer, _ := cfg.NewTracer(config.Logger(jaeger.StdLogger))
    defer closer.Close()

    // Установка Jaeger Tracer как глобального
    opentracing.SetGlobalTracer(tracer)

    // Ваш код приложения
}

Этот код инициализирует Jaeger Tracer и инструментирует ваше приложение для сбора трассировочных данных.

Интеграция Jaeger в Go-приложения

Интеграция Jaeger в ваши Go-приложения позволяет отслеживать выполнение запросов и операций в распределенных системах.

  1. Использование OpenTracing API

OpenTracing — это стандартный API для создания и отправки трассировочных данных в различные системы, включая Jaeger. Используя OpenTracing API, вы можете инструментировать ваше приложение для сбора информации о трассировке.

Прежде всего, вам потребуется установить необходимые библиотеки. В Go, вы можете использовать пакет opentracing-go, который предоставляет реализацию OpenTracing API.

import (
    "log"
    "github.com/opentracing/opentracing-go"
    "github.com/opentracing/opentracing-go/ext"
    "github.com/opentracing/opentracing-go/log"
)

Далее, создайте Jaeger Tracer и настройте его для вашего приложения:

import (
    "github.com/uber/jaeger-client-go"
    "github.com/uber/jaeger-client-go/config"
)

func initJaeger(serviceName string) (opentracing.Tracer, io.Closer) {
    cfg := config.Configuration{
        ServiceName: serviceName,
        Sampler: &config.SamplerConfig{
            Type:  "const",
            Param: 1,
        },
        Reporter: &config.ReporterConfig{
            LogSpans: true,
        },
    }

    tracer, closer, err := cfg.NewTracer(config.Logger(jaeger.StdLogger))
    if err != nil {
        log.Fatalf("Could not initialize jaeger tracer: %s", err.Error())
    }
    return tracer, closer
}

Этот код создает Jaeger Tracer и настраивает его с параметрами по умолчанию. Вы можете настроить его в соответствии с вашими потребностями, включая выбор сэмплера, хранилища и адаптеров.

  1. Создание и отправка трассировочных данных

Теперь, когда Jaeger Tracer настроен, вы можете начать создавать и отправлять трассировочные данные в вашем приложении. Это делается с помощью методов OpenTracing API.

Пример создания и отправки трассировочных данных:

func main() {
    // Инициализация Jaeger Tracer
    tracer, closer := initJaeger("my-service")
    defer closer.Close()

    // Начало новой трассировки
    span := tracer.StartSpan("my-operation")
    defer span.Finish()

    // Определение тегов и журналирование
    ext.SpanKindRPCClient.Set(span)
    span.SetTag("custom-tag", "tag-value")
    span.LogFields(log.String("event", "my-event"))

    // Ваш код выполнения операции
}

В этом примере мы создаем новую трассировку с именем «my-operation» и указываем ее завершение с помощью defer span.Finish(). Мы также добавляем теги и логируем события, чтобы обогатить трассировку информацией.

Интеграция Jaeger в Go-приложения помогает вам отслеживать выполнение операций в распределенных системах и быстро находить и решать проблемы. Процесс интеграции не только улучшает мониторинг и отладку ваших приложений, но и предоставляет ценную информацию для оптимизации и улучшения производительности.

Анализ и визуализация трассировочных данных в Jaeger

  1. Интерфейс Jaeger UI

Jaeger предоставляет удобный веб-интерфейс, известный как Jaeger UI, для анализа трассировочных данных. Этот интерфейс позволяет вам визуализировать пути выполнения запросов, анализировать производительность и находить проблемы в вашем приложении.

Когда Jaeger запущен и интегрирован с вашим приложением, вы можете получить доступ к Jaeger UI, обычно по адресу http://localhost:16686 (если запущен на локальной машине).

Jaeger UI предоставляет следующие ключевые возможности:

  • Поиск трассировок: Вы можете использовать различные фильтры, такие как сервис, теги и диапазоны времени, чтобы найти трассировки, соответствующие вашим критериям.

  • Визуализация трассировок: Jaeger показывает дерево выполнения запроса, где каждый узел представляет собой компонент системы, через который прошел запрос.

  • Статистика времени выполнения: Вы можете видеть, сколько времени заняло выполнение каждой операции в запросе и определить, где требуется оптимизация.

  • Детальные сведения: Для каждой трассировки вы можете просматривать теги, логи и другие сведения о запросе.

  1. Отладка проблем с помощью трассировки

Jaeger не только позволяет вам визуализировать пути выполнения запросов, но и помогает вам отлаживать проблемы в распределенных системах.

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

  • Диагностика ошибок: При возникновении ошибок или исключений, Jaeger может показать вам, где именно ошибка произошла. Вы сможете увидеть, как запрос прошел через систему, и точно определить, где возникла проблема.

  • Изучение производительности системы: Вы можете анализировать производительность вашей системы в реальном времени или в исторической перспективе, что помогает в идентификации трендов и улучшении производительности.

Пример Jaeger UI с визуализацией трассировки:

c6d28b1f9cd9378f40337c4d0f87c123.png

Интеграция Prometheus и Jaeger

Мониторинг и трассировка — два важных аспекта обеспечения стабильной работы и эффективной отладки распределенных систем. Переплетение этих двух аспектов позволяет разработчикам получить более полное представление о работе своего приложения.

Почему это так важно:

  1. Понимание производительности: Мониторинг с Prometheus предоставляет вам информацию о производительности системы, включая метрики, такие как скорость запросов, использование ресурсов и динамику изменения данных. Это позволяет выявлять узкие места и оптимизировать код.

  2. Отслеживание запросов: Трассировка с Jaeger дает вам возможность следить за тем, как запросы перемещаются через вашу систему. Вы можете видеть путь выполнения запросов и определить, где возникают задержки и проблемы.

  3. Диагностика ошибок: Совмещение мониторинга и трассировки упрощает выявление и решение проблем. Вы можете увидеть, какие метрики связаны с определенными трассировками, и быстро найти причины ошибок.

  4. Улучшенное планирование и оптимизация: Используя данные из мониторинга и трассировки, вы можете принимать более обоснованные решения о масштабировании, оптимизации кода и улучшении производительности вашей системы.

Prometheus и Jaeger — это два разных инструмента, но они могут интегрироваться вместе для обеспечения полного мониторинга и отладки ваших приложений:

  1. Сбор метрик: Prometheus отвечает за сбор и хранение метрик, таких как скорость запросов, загрузка ЦП, использование памяти и другие. Эти метрики могут предоставить вам широкий обзор состояния вашей системы.

  2. Создание трассировочных данных: В вашем Go-приложении вы можете инструментировать код с использованием OpenTracing API для создания и отправки трассировочных данных в Jaeger. Эти данные описывают путь выполнения запросов через вашу систему.

  3. Интеграция с Jaeger Collector: Jaeger может быть настроен для отправки трассировочных данных в Jaeger Collector, который агрегирует и сохраняет эти данные.

  4. Визуализация и анализ: Jaeger UI позволяет вам визуализировать трассировки и анализировать их в сочетании с данными мониторинга из Prometheus. Вы можете легко найти связь между задержками в трассировке и изменениями в метриках.

Примеры совместной конфигурации

Процесс интеграции Prometheus и Jaeger начинается с настройки обоих инструментов. Примеры конфигурации для обоих инструментов:

Пример конфигурации Prometheus для сбора метрик:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'my-app'
    static_configs:
      - targets: ['localhost:9090']  # Адрес вашего приложения

Пример интеграции Jaeger с Prometheus для сбора трассировочных данных:

agent:
  collectd:
    listen-addr: "localhost:8080"

collector:
  zipkin:
    http-port: 9411
  pprof-http:
    http-port: 5778

Эти примеры позволяют Prometheus собирать метрики из вашего приложения и Jaeger агрегировать трассировочные данные. Важно подстраивать конфигурацию под ваши потребности и требования.

Примеры

Пример 1: Мониторинг HTTP-сервиса с Prometheus и отладка с Jaeger

Предположим, у вас есть Go-приложение, предоставляющее HTTP-сервис. Вы хотите мониторить его производительность и отслеживать выполнение запросов.

  1. Настройка Prometheus: Создайте конфигурацию Prometheus для сбора метрик вашего HTTP-сервиса. Например, вы можете использовать promhttp пакет для экспорта метрик. Здесь вы настраиваете job_name, target, и выбираете метрики для мониторинга.

scrape_configs:
  - job_name: 'my-http-service'
    static_configs:
      - targets: ['localhost:8080']
    metrics_path: /metrics
  1. Инструментирование кода: Ваше приложение должно быть инструментировано с использованием Prometheus клиента для регистрации метрик. Например, вы можете измерять время выполнения HTTP-обработчиков и собирать информацию о запросах.

import (
    "net/http"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/promhttp"
)

func main() {
    http.Handle("/metrics", promhttp.Handler())
    http.HandleFunc("/api", myHandler)
    http.ListenAndServe(":8080", nil)
}

func myHandler(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    // Обработка запроса
    elapsed := time.Since(start)
    httpDuration.WithLabelValues("myHandler").Observe(float64(elapsed.Nanoseconds()))
}
  1. Интеграция Jaeger: Теперь вы интегрируете Jaeger, чтобы отслеживать выполнение запросов через ваш HTTP-сервис. Инструментируйте код, создавайте и отправляйте трассировочные данные.

import (
    "github.com/opentracing/opentracing-go"
    "github.com/uber/jaeger-client-go/config"
)

func main() {
    tracer, closer := initJaeger("my-service")
    defer closer.Close()

    http.Handle("/metrics", promhttp.Handler())
    http.HandleFunc("/api", myHandler)

    http.ListenAndServe(":8080", nil)
}

func myHandler(w http.ResponseWriter, r *http.Request) {
    span := tracer.StartSpan("myHandler")
    defer span.Finish()

    // Обработка запроса
}
  1. Анализ и визуализация: Теперь вы можете использовать Jaeger UI для анализа трассировочных данных и Prometheus для мониторинга. Смотрите на данные вместе, чтобы выявить связь между метриками и трассировкой, и решать проблемы.

Пример 2: Мониторинг микросервисной архитектуры с Prometheus и Jaeger

Предположим, у вас есть микросервисная архитектура, и вы хотите мониторить каждый сервис и отслеживать выполнение запросов между ними.

  1. Настройка Prometheus: Создайте конфигурацию Prometheus для каждого сервиса, собирая метрики из экспортеров, таких как node_exporter и blackbox_exporter. Учтите, что каждый сервис может иметь свой собственный порт для экспорта метрик.

scrape_configs:
  - job_name: 'my-service-1'
    static_configs:
      - targets: ['service-1:9100']  # node_exporter
  - job_name: 'my-service-2'
    static_configs:
      - targets: ['service-2:9100']  # node_exporter
  1. Инструментирование кода: Инструментируйте код каждого сервиса с использованием Prometheus клиента для регистрации метрик. Экспортируйте метрики, связанные с выполнением запросов между сервисами.

func main() {
    http.Handle("/metrics", promhttp.Handler())
    http.HandleFunc("/api", myHandler)
    http.ListenAndServe(":8080", nil)
}

func myHandler(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    // Обработка запроса
    elapsed := time.Since(start)
    httpDuration.WithLabelValues("myHandler").Observe(float64(elapsed.Nanoseconds()))
}
  1. Интеграция Jaeger: Добавьте интеграцию Jaeger для каждого сервиса, чтобы отслеживать выполнение запросов и взаимодействие между сервисами.

  2. Анализ и визуализация: Используйте Jaeger UI для анализа трассировочных данных, чтобы видеть, как запросы перемещаются между сервисами. Проанализируйте метрики Prometheus, чтобы мониторить производительность каждого сервиса и отлавливать аномалии.

Пример 3: Оповещения и автоматизация

Используя Prometheus Alertmanager, вы можете создавать оповещения на основе метрик и трассировки. Например, вы можете настроить оповещение, если среднее время выполнения запросов в вашем приложении превышает определенный порог.

route:
  group_by: ['alertname']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 3h

receivers:
  - name: 'slack'
    slack_configs:
      - send_resolved: true
        username: 'Prometheus'
        channel: '#alerts'
        api_url: 'https://slack.com/api/chat.postMessage'

Это позволяет автоматизировать процесс оповещения при возникновении проблем.

Заключение

Интеграция мониторинга и трассировки в ваши приложения позволяет вам легко отслеживать производительность, находить и устранять проблемы и обеспечивать стабильную работу распределенных систем. Помните, что каждый проект уникален, и вы должны настраивать и использовать Prometheus и Jaeger в соответствии с конкретными требованиями вашего приложения.

И напоследок хочу порекомендовать бесплатный урок курса Golang Developer. Professional, на котором вы узнаете что такое кодогенерация в Go, зачем она нужна, и какие задачи может решить.

© Habrahabr.ru