Jaeger Opentracing и Microservices в реальном проекте на PHP и Golang

в 7:00, , рубрики: Go, golang, jaeger, Microservices, open source, opentracing, Блог компании CarPrice, отладка, Разработка веб-сайтов

Всем привет, меня зовут Юрий Буйлов, я руковожу разработкой в CarPrice. Коротко расскажу как и почему мы пришли к микросервисам на PHP и Golang. Что используем, как инструментируем и мониторим наши приложения в production. Далее расскажу о распределенном трейсинге, который обеспечивает нам прозрачность работы сервисов.

Jaeger Opentracing и Microservices в реальном проекте на PHP и Golang - 1

Почему микросервисы

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

Изначально CarPrice был собран как монолитное приложение на Битриксе с привлечением аутсорс разработчиков и упором на скорость разработки. В определённый период это сыграло одну из важных ролей в успешном выходе проекта на рынок.

Со временем поддерживать стабильную работу монолита стало невозможно — каждый релиз превращался в испытание для тестировщиков, разработчиков и админов. Разные процессы мешали нормальной работе друг друга. К примеру, девочка из документооборота могла запустить генерацию документов на завершенные аукционы и в этот момент дилеры не могли нормально торговаться из-за тормозов на бекенде.

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

На чем пишем

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

В основном это небольшие приложения на Laravel (php) решающие определенную бизнес задачу. Такие сервисы предоставляют HTTP API и могут иметь административный Web UI (Vue.js)
Общие компоненты мы стараемся оформлять в библиотеки, которые доставляет composer. Кроме того сервисы наследуют общий php-fpm docker-образ. Это снимает головную боль при обновлении. Например у нас почти везде php7.1.

Критичные по скорости сервисы мы пишем на Golang.
Например, сервис jwt-аутентификации выписывает и проверяет токены, а также умеет разлогинить недобросовестного дилера, которого руководство за грехи отключает от аукционной платформы.
Сервис приёма ставок обрабатывает ставки дилеров, сохраняет их в базу и отложено отправляет события в rabbitmq и сервис RT-нотификаций.

Для сервисов на Golang мы используем go-kit и gin/chi.
go-kit привлек своими абстракциями, возможностью использовать различные транспорты и обертки для метрик, но немного утомляет любовью к функциональщине и многословностью, поэтому его используем в капитальных постройках с богатой бизнес логикой.
На gin и chi удобно собирать простые http-сервисы. Это идеальный вариант быстро и с минимальными усилиями запустить мелкий сервис в production. Если у нас появляются сложные сущности, то мы стараемся перевести сервис на go-kit.

Эволюция мониторинга

Во времена монолита нам хватало newrelic. Заскочив на ступеньку микросервисов число серверов увеличилось и мы отказались от него по финансовым соображениям и бросились во все тяжкие: Zabbix – для железа, ELK, Grafana и Prometheus – для APM.

Jaeger Opentracing и Microservices в реальном проекте на PHP и Golang - 2

Первым делом мы сложили логи nginx со всех сервисов в ELK, построили графики в Grafana, а за запросами, что портили нам 99-й перцентиль ходили в Kibana.

И вот тут начинался квест — понять что происходило с запросом.
В монолитном приложении было все просто — если это php, то раньше был xhprof, вооружившись которым можно было понять что же там происходило. При микросервисах, где запрос проходит через несколько сервисов да ещё и на разных технологиях — такой трюк не пройдет. Где-то сеть, где-то синхронные запросы или протухший кеш.

Допустим, мы нашли медленный запрос нашего API. По коду установили, что запрос обратился к трем сервисам, собрал и вернул результат. Теперь надо по косвенным признакам (timestamp, параметры запроса) найти нижестоящие запросы чтоб понять какой из сервисов был причиной медленного запроса. Даже если мы нашли тот сервис — надо идти в метрики или логи сервиса и искать причину там, а ведь нередко бывает так, что нижние сервисы работают быстро, а результирующий запрос тормозит. Вобщем это так себе удовольствие.

И мы поняли, что пора – нужен распределенный трейсинг.

Jaeger, добро пожаловать!

Мотивация:

  • Поиск аномалий – почему зафейлен 99-й перцентиль, например таймауты сети, падения сервисов или блокировки в базе.
  • Диагностика массовых проблем (50 или 75-й перцентиль) после деплоя, изменения конфигурации сервиса или количества инстансов
  • Распределенный профайлинг – найти медленные сервисы, компоненты или функции.
    Визуализация (Gantt) этапов запроса – можно понять что происходит внутри

Вспомнив про Google’s Dapper мы первым делом пришли к Opentracing – универсальный стандарт для распределенного трейсинга. Его поддерживают несколько трейсеров. Самые известные – это Zipkin (Java) и Appdash (Golang).
Однако недавно среди трейсеров-старожилов, поддерживающих стандарт появился новый и многообещающий Jaeger от Uber Technologies. О нем мы и поговорим.

Бекенд – Go
UI – React
Storage – Cassandra/Elasticsearch

Изначально разрабатывался под стандарт OpenTracing.
В отличие от того же Zipkin, модель Jaeger нативно поддерживает key-value logging и трейсы представлены как направленный ациклический граф (DAG) а не просто дерево спанов.

Кроме того совсем недавно на Open Source саммите в LA Jaeger был поставлен на одну полку с такими почетными проектами как Kubernetes и Prometheus.

Jaeger Opentracing и Microservices в реальном проекте на PHP и Golang - 3

Архитектура

Каждый сервис собирает тайминги и доп.инфу в спаны и скидывает их в рядом стоящий jaeger-agent по udp. Тот, в свою очередь, отправляет их в jaeger-collector. После этого трейсы доступны в jaeger-ui. На оф.сайте архитектура изображена так:

Jaeger Opentracing и Microservices в реальном проекте на PHP и Golang - 4

Jaeger в production

Большинство наших сервисов развернуты в Docker-контейнерах. Собирает их Drone, а деплоит Ansible. К сожалению (нет), мы еще не перешли на системы оркестрации типа k8s, nomad или openshift и контейнеры работают под управлением Docker Compose.
Типичные наши сервисы в связке с jaeger выглядит так:

Jaeger Opentracing и Microservices в реальном проекте на PHP и Golang - 5

Инсталляция Jaeger в production представляет собой набор нескольких сервисов и storage.
→ collector – принимает спаны от сервисов и записывает их в storage
→ query – Web UI и API для чтения спанов из storage
→ storage – хранит все спаны. Можно использовать либо cassandra либо elasticsearch

Для девов и локальной разработки удобно использовать билд Jaeger “все в одном” с in-memory storage под трейсы jaegertracing/all-in-one:latest

Как это работает

Сервис собирает информацию по таймингам и мета информацию запроса в спаны. Спан передается между методами через контекст, а нижестоящим сервисам через инъекцию контекста в хедер.

Jaeger Opentracing и Microservices в реальном проекте на PHP и Golang - 6

Для демонстрации команда uber подготовила хороший пример, иллюстрирующий трейсинг в сервисе поиска водителя: Hotrod

Как в коде

Для начала нам нужно создать сам трейсер

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

jcfg := config.Configuration{
	Disabled: false, // Nop tracer if True
	Sampler: &config.SamplerConfig{
		Type:  "const",
		Param: 1,
	},
	Reporter: &config.ReporterConfig{
		LogSpans:            true,
		BufferFlushInterval: 1 * time.Second,
		// Адрес рядом стоящего jaeger-agent, который будет репортить спаны
		LocalAgentHostPort: cfg.Jaeger.ReporterHostPort,
	},
}

tracer, closer, err := jcfg.New(
	cfg.Jaeger.ServiceName,
	config.Logger(jaeger.StdLogger),
)

Добавляем middleware (opentracing.TraceServer) – создает корневой спан для метода api. Все вложенные спаны будут привязаны к нему.

endpoint := CreateEndpoint(svc)
// Middleware создает корневой спан для метода api
endpoint = opentracing.TraceServer(tracer, opName)(endpoint)

Кроме того мы извлекаем (Extract) контекст трейса из заголовка входящего запроса (opentracing.FromHTTPRequest). Таким образом наш сервис будет связан с вышестоящим сервисом при условии, что тот передал контекст трейса в запросе (Inject).

r.Handle(path, kithttp.NewServer(
	endpoint,
	decodeRequestFn,
	encodeResponseFn,
	// Извлекает контекст трейса из хедера и помещает в context.Context
	append(opts, kithttp.ServerBefore(opentracing.FromHTTPRequest(tracer, opName, logger)))...,
)).Methods("POST")

Далее мы инструментируем наши методы:

func (s Service) DoSmth() error {
    span := s.Tracing.StartSpan("DoSmth", ctx)
    defer span.Finish()
    // do smth
    return nil
}

Ну и сам старт спана выглядит так

func (t AppTracing) StartSpan(name string, ctx context.Context) opentracing.Span {
	span := opentracing.SpanFromContext(ctx);
	if span != nil {
		span = t.Tracer.StartSpan(name, opentracing.ChildOf(span.Context()))
	} else {
		span = t.Tracer.StartSpan(name)
	}

	return span
}

Это все. Теперь мы можем в реальном времени наблюдать за работой нашего сервиса.
(Большая картинка)

Jaeger Opentracing и Microservices в реальном проекте на PHP и Golang - 7

Например, на картинке выше мы нашли тормозящий запрос и увидели, что половину времени отъела сеть между сервисами, а вторую половину – update в базу. С этим уже можно работать.

Спасибо за внимание. Надеюсь, данная заметка будет полезна, а Jaeger кому-то поможет привнести прозрачность в работу сервисов.

Полезные ссылки

Оф сайт проекта
Репозиторий
Сайт Opentracing
Пример

Автор: yubuylov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js