Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 8 из 25 ~20 мин +50 XP

Tracing: OpenTracing

Логи из урока 06 рассказывают истории одного сервиса, метрики из урока 07 — темпы и хвосты, но ни то, ни другое не видит путь запроса сквозь несколько сервисов. Четвёртый слой наблюдаемости — распределённый трейсинг. Подход go kit tracing держится на паре пакетов: opentracing-go описывает модель спанов, а tracing/opentracing из go-kit превращает её в те же обёртки, к которым ты привык, — RequestFunc HTTPToContext в ServerBefore делает Span-инъекцию в контекст, TraceServer и TraceClient создают спаны на endpoint'ах, Jaeger собирает дерево трейса по trace id. Ты настроишь трейсер в main, разберёшь SpanContext и carrier, свяжешь спаны с Failer-ошибками из урока 02 и увидишь один запрос целиком в Jaeger UI.

Содержание урока · 13 разделов

Зачем трейсинг: слепая зона логов и метрик#

До сих пор stringsvc жил в одном процессе, и всех наблюдательных слоёв хватало: лог из урока 06 отвечал «что случилось с этим запросом», метрики из урока 07 — «сколько запросов и как быстро». Но микросервис по определению соседствует с другими сервисами: запрос идёт через балансировщик, транспорт, endpoint, потом улетает в базу или в соседний stringsvc, и уже там что-то ломается. Логи двух сервисов коррелировать нечем: у каждого свой request_id из ServerFinalizer, своя нумерация, свои таймстемпы — склеивать их руками после каждого инцидента невозможно. Метрики дают p90 каждого сервиса по отдельности, но на вопрос «где именно запрос провёл двести миллисекунд» честный ответ — «не знаю, в первом или во втором». Эту слепую зону закрывает распределённый трейсинг: вся операция называется трейсом и имеет один trace id на всю цепочку, а внутри трейса живут спаны — именованные отрезки времени с тегами и событиями: «транспорт принял», «endpoint отработал», «исходящий вызов к соседу занял 180 мс». Подход go kit tracing не изобретает свою модель: go-kit реализует стандарт OpenTracing теми же двумя механизмами, что и логи с метриками, — RequestFunc на транспорте и endpoint.Middleware на endpoint'е. Поэтому к концу урока трейсинг встанет в stringsvc рядом с loggingMiddleware и instrumentingMiddleware, как будто всегда там стоял.

Модель OpenTracing: Tracer, Span, SpanContext#

Открой github.com/opentracing/opentracing-go — весь пакет это горстка интерфейсов, и стоит выучить четыре имени. Tracer — фабрика и почтальон: StartSpan(operationName, opts...) создаёт спан, Inject и Extract пакуют и распаковывают контекст трассировки для передачи по сети. Span — отрезок времени: StartSpan возвращает его, span.Finish() фиксирует длительность, SetTag(key, value) вешает метки вроде span.kind=server, LogFields(...) записывает события внутри отрезка. SpanContext — самое хитрое и самое важное: это не сам спан, а минимальные данные для связи спанов между процессами — trace id, id этого спана, решение о сэмплинге; именно SpanContext, а не спан, путешествует по сети. Carrier — конверт, в который SpanContext упаковывается: opentracing.HTTPHeadersCarrier оборачивает http.Header, и Inject пишет туда заголовок uber-trace-id, Extract читает его обратно; помимо HTTPHeaders есть форматы TextMap и Binary, но для веб-сервисов нужен первый. Наконец, ссылки: StartSpan с опцией ChildOf(parent) объявляет, что новый спан — ребёнок родительского, а FollowsFrom — что работа параллельна родителю, например публикация в очередь, на которую мы не ждём ответа. Весь урок ты будешь встречать только ChildOf, но знать оба термина полезно. Правило разбиения простое: одна нога сетевого вызова — один спан; у входящего запроса — server-спан, у исходящего — client-спан, внутри процесса — сколько осмысленных отрезков, столько спанов.

Span-инъекция через ServerBefore: HTTPToContext

GO
import (
        kithttp "github.com/go-kit/kit/transport/http"
        kitot "github.com/go-kit/kit/tracing/opentracing"
        stdopentracing "github.com/opentracing/opentracing-go"
)

tracer := stdopentracing.GlobalTracer() // NoopTracer, пока не вызван SetGlobalTracer

mux.Handle("/uppercase", kithttp.NewServer(
        upper,
        decodeUppercaseRequest,
        encodeResponse,
        kithttp.ServerBefore(kitot.HTTPToContext(tracer, "HTTP /uppercase", logger)),
))

HTTPToContext возвращает kithttp.RequestFunc — тот же тип, что PopulateRequestContext из урока 04, поэтому она честно встаёт в ServerBefore. Внутри — tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header)): из заголовков входящего запроса достаётся SpanContext, затем tracer.StartSpan(operationName, ext.RPCServerOption(wireContext)) создаёт спан с тегом span.kind=server и кладёт его в контекст через opentracing.ContextWithSpan. Это и есть Span-инъекция: дальше любой endpoint-middleware достанет спан из ctx.

Как работает инъекция: Extract, ErrSpanContextNotFound и имя операции#

Разберём функцию по строчкам, потому что здесь три решения, которые определяют весь дальнейший трейсинг. Первое — Extract вернёт ошибку opentracing.ErrSpanContextNotFound, если заголовков нет: так будет с самым первым запросом цепочки, от клиента, который ещё не знает о трейсинге. Это не сбой: ext.RPCServerOption(wireContext) спроектирован под оба случая — есть wireContext, и спан станет ChildOf удалённого родителя; нет — и спан станет корнем нового трейса. Транспорт продолжает работать без трейсинга, ничего не падает и не деградирует. Второе — имя операции, второй аргумент HTTPToContext: «HTTP /uppercase» — распространённая конвенция «транспорт + маршрут», по которой в Jaeger UI удобно группировать входящие вызовы; ниже TraceServer создаст второй спан с именем «uppercase» — и дерево сразу читается: транспорт снаружи, endpoint внутри. Третье — почему спан кладётся именно в context.Context, а не в поле request-структуры: endpoint из урока 02 — функция от ctx и request, декодер уже съел http.Request, и единственный канал, который доживает до endpoint-middleware, — это ctx. Тот же контракт ты увидишь в gRPC-транспорте: там инъекцией занимается ContextToGRPC через метаданные gRPC, но идея идентична. Заметь и порядок ServerBefore: HTTPToContext можно ставить первой в списке — ей всё равно, что делают соседи, она читает только http.Request.

TraceServer: спан на endpoint middleware

GO
upper = loggingMiddleware(logger)(upper)                             // урок 06: снаружи
upper = instrumentingMiddleware(requestCount, requestLatency)(upper) // урок 07: внутри
upper = kitot.TraceServer(tracer, "uppercase")(upper)                // урок 08: глубже всех

// внутри пакета tracing/opentracing это выглядит так:
// parentSpan := opentracing.SpanFromContext(ctx)
// span := tracer.StartSpan("uppercase", opentracing.ChildOf(parentSpan.Context()))
// defer span.Finish()
// ctx = opentracing.ContextWithSpan(ctx, span)
// return next(ctx, request)

TraceServer(tracer, operationName, opts ...EndpointOption) — обычный endpoint.Middleware: ищет родителя через opentracing.SpanFromContext, заводит дочерний спан, defer span.Finish() гарантирует закрытие при любом исходе, обновлённый ctx передаётся дальше. Тег span.kind=server добавляется автоматически — потому и Server, а рядом есть TraceClient для исходящих вызовов. Опции EndpointOption позволяют добавить теги через WithTags, динамическое имя через WithOperationNameFunc и игнорировать бизнес-ошибки через WithIgnoreBusinessError.

Бизнес-ошибки тоже видны: Failer в спане#

Самая ценная деталь TraceEndpoint — работа с ошибками, и она снова про два канала, к которым ты привык с уроков 02 и 05. Если endpoint вернул err != nil — дедлайн, недоступная база, сломанный декодер — middleware записывает в спан стандартное событие ошибки через ext.LogError: в Jaeger такой спан покрасится как ошибочный, и его можно найти фильтром по error-тэгу. Но бизнес-ошибки, как ты помнишь, едут другим путём: адаптер кладёт их в Err-поле response и возвращает err = nil. TraceEndpoint проверяет response на endpoint.Failer: если Failed() вернул не-nil, в спан пишется событие с полем gokit.business.error и текстом ошибки, затем LogError помечает спан как сбойный. Значит, ErrEmpty — честная «пустая строка на входе» — будет видна в Jaeger так же ярко, как отказ базы, хотя транспорт вернул аккуратный 400 из уроков 05. Если для твоего сервиса бизнес-ошибки — норма жизни и шуметь не хочется, включи опцию WithIgnoreBusinessError(true): событие запишется, но спан не покрасится. Отдельная ветка — lb.RetryError из пакета sd/lb: когда endpoint внутри балансировщика, каждая неудачная попытка попадает в спан отдельным полем gokit.retry.error.N — пригодится, когда в уроке 11 будем собирать прокси. Итого: TraceServer уже умеет всё, что ты писал руками в логах, — и сразу привязывает это к trace id.

Jaeger в main: конфигурация трейсера

GO
import (
        jaegercfg "github.com/jaegertracing/jaeger-client-go/config"
)

cfg := jaegercfg.Configuration{
        ServiceName: "stringsvc",
        Sampler:  &jaegercfg.SamplerConfig{Type: "const", Param: 1},
        Reporter: &jaegercfg.ReporterConfig{LocalAgentHostPort: "localhost:6831"},
}
tracer, closer, err := cfg.NewTracer()
if err != nil {
        log.Fatal(err)
}
defer closer.Close()

stdopentracing.SetGlobalTracer(tracer) // теперь GlobalTracer() вернёт jaeger

logger.Log("msg", "HTTP", "addr", ":8080")
log.Fatal(http.ListenAndServe(":8080", mux))

NewTracer возвращает opentracing.Tracer и io.Closer: closer.Close() в defer аккуратно досылает хвостовой батч спанов при выключении. Sampler const с Param 1 трассирует каждый запрос — для разработки; Reporter отправляет спаны батчами по UDP на jaeger-agent (LocalAgentHostPort), Jaeger UI живёт на localhost:16686. SetGlobalTracer делает трейсер глобальным — именно его теперь вернёт GlobalTracer(), который мы записали в tracer в начале main. Короткий путь — cfg.InitGlobalTracer(serviceName), он сам зовёт SetGlobalTracer и возвращает только closer.

Сэмплинг: const, probabilistic, remote#

Параметр Sampler из конфигурации — это решение «трассировать ли вообще», и оно принимается в момент StartSpan: недосэмплированный запрос не создаёт спанов ни у себя, ни у соседей — трейс обрывается тихо, без ошибок. Типов сэмплера в Jaeger четыре, и выбор — вопрос трафика и денег. const с Param 1 или 0 — всё или ничего: единственно правильный вариант для разработки и для малонагруженных внутренних сервисов. probabilistic с Param 0.01 — случайный один процент: стандартная продовая настройка для высоконагруженных сервисов, потому что стоимость растёт линейно с трафиком, а польза от тысяч одинаковых успешных трейсов сомнительна. ratelimiting — не больше N трейсов в секунду: честный потолок нагрузки на инфраструктуру трейсинга. remote — самое взрослое: стратегию присылает агент или collector, и ты меняешь процент выборки в консоли Jaeger, не передеплоивая ни одного сервиса; так и настраивают продовые кластеры. Стоит знать и термин head-based sampling: решение принимается в голове трейса и наследуется всеми спанами — поэтому недосэмплированный запрос нельзя «доотрассировать» потом; противоположность — tail-based sampling, где решение принимают после завершения трейса и хранят только интересные хвосты с ошибками, но это уже работа collector-пайплайна, а не клиента. Начни с const 1, поставь probabilistic, когда трейсинг начнёт жрать заметную долю бюджета — и переведи на remote, когда сервисов станет больше пяти.

Анатомия одного запроса в Jaeger#

ЭлементЧто этоВ stringsvc
Traceвся операция: дерево спанов с одним trace idPOST /uppercase сквозь балансировщик и два сервиса
Spanименованный отрезок с таймингами и тегами«HTTP /uppercase» на транспорте
Дочерний spanChildOf-спан, заведённый внутри endpoint-обёртки«uppercase» от TraceServer
SpanContexttravel-данные для связи процессов: trace id + span idуехал в заголовке uber-trace-id
Tagпара ключ-значение на спанеspan.kind=server, http.method=POST
Log-событиесобытие внутри отрезка спанаgokit.business.error: empty string
Baggageпары ключ-значение, едущие через всю цепочкуне для user_id — это дорога в заголовки

Исходящие вызовы: ContextToHTTP и TraceClient#

Входящая половина готова, но трейс живёт, только когда уезжает вместе с запросом к соседу. Для исходящих у go-kit есть зеркальная пара. ContextToHTTP(tracer, logger) возвращает ту же kithttp.RequestFunc, но для исходящих: она достаёт спан из контекста, проставляет стандартные теги http.method и http.url, достаёт хост с портом в тег peer.hostname и вызывает tracer.Inject(span.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header)) — SpanContext упаковывается в заголовки исходящего http.Request, в тот самый uber-trace-id. Подключается она в клиентском транспорте: httptransport.NewClient(method, target, encodeRequest, decodeResponse, httptransport.ClientBefore(kitot.ContextToHTTP(tracer, logger))) — симметрично ServerBefore из урока 04. А TraceClient(tracer, operationName) — это TraceServer с тегом span.kind=client: оберни ею endpoint, который делает исходящий вызов, и у ноги появится свой отрезок с длительностью. Правило читается само: каждая нога сетевого вызова — client-спан у отправителя и server-спан у получателя; получатель через свой HTTPToContext делает Extract и джойнится к трейсу — Jaeger склеит два сервиса в одно дерево автоматически. Отдельная любопытная деталь: в коде TraceEndpoint есть ветка lb.RetryError — go-kit логирует каждую попытку балансировщика отдельным полем, так что даже ретраи видны в трейсе как события, а не как один серый отрезок.

Trace id в логах: склейка трёх слоёв

GO
import (
        jaeger "github.com/jaegertracing/jaeger-client-go"
        stdopentracing "github.com/opentracing/opentracing-go"
)

func traceIDMiddleware(logger log.Logger) endpoint.Middleware {
        return func(next endpoint.Endpoint) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (interface{}, error) {
                        if span := stdopentracing.SpanFromContext(ctx); span != nil {
                                if sc, ok := span.Context().(jaeger.SpanContext); ok {
                                        logger.Log("trace_id", sc.TraceID().String())
                                }
                        }
                        return next(ctx, request)
                }
        }
}

Простейший мост между слоями: middleware достаёт спан из ctx, кастует SpanContext к jaeger-типу и пишет trace id в лог тем самым kitlog из урока 06. Теперь grep по trace_id находит все строки запроса во всех сервисах, а по нему же трейс открывается в Jaeger UI одним кликом. Цена — импорт jaeger-пакета в middleware: жёсткая зависимость, которую принято изолировать на транспортном уровне; альтернатива — отдавать trace id в заголовке ответа и собирать корреляцию на клиентах.

Читаем трейс: дерево, длительности и хвосты#

Открой Jaeger UI на localhost:16686, выбери сервис stringsvc, операцию HTTP /uppercase — и получи список трейсов с гистограммой длительностей. Внутри трейса — дерево: server-спан «HTTP /uppercase» шириной во весь запрос, внутри него дочерний «uppercase» от TraceServer, рядом — client-спаны исходящих вызовов, а у соседа — его server-спан, джойнутый по uber-trace-id. Читать стоит сверху вниз: разница между началом родителя и началом ребёнка — время до endpoint'а, включая декодер и все middleware; ширина ребёнка — время сервиса; промежуток после Finish до конца родителя — время энкодера и сети. Пустой длинный хвост при коротком ребёнке — спрашивай, что происходило в обёртках: ретраи, лимитеры, дедлайны. Фильтр tags=error=true отберёт сбойные трейсы — сюда попадают и инфраструктурные ошибки, и gokit.business.error от Failer, о которых шла речь выше. Практика с логами из урока 06 замыкает петлю: trace_id в логе и в спане — одна и та же строка, поэтому от строки лога ты за один клик попадаешь в трейс, а из трейса — обратно в конкретные строки логов каждого сервиса. Метрики из урока 07 остаются слоем суммарного ответа: p90 говорит, что хвост существует, трейс — где именно он образовался. Три инструмента не дублируют, а дополняют друг друга: метрики — «есть ли проблема», логи — «что случилось», трейс — «где именно по пути».

Пять правил трейсинга в go-kit#

  1. Корневой спан создаёт транспорт: HTTPToContext в ServerBefore, до декодера; имя — «транспорт + маршрут».
  2. TraceServer оборачивает endpoint и заводит дочерний спан; TraceClient — исходящие вызовы; каждая нога сети — пара client/server.
  3. SpanContext путешествует в заголовках через HTTPHeadersCarrier: uber-trace-id у Jaeger, traceparent в мире W3C.
  4. Ошибки попадают в спан сами: TraceEndpoint пишет ext.LogError для err и gokit.business.error для Failer.
  5. Сэмплинг настраивается в main: const 1 для разработки, probabilistic или remote для прода; решение наследуется всем трейсом.

Первый запрос цепочки приходит вовсе без заголовков трейсинга. Что сделает HTTPToContext?

Сервис вернул UppercaseResponse с Err: "empty string" и err = nil. Что запишет TraceServer в спан?

main не вызвал SetGlobalTracer, и код трассирует через stdopentracing.GlobalTracer(). Что будет?

Двойной спан: что окажется в Jaeger?

Прочитайте код и выберите, что он выведет:

mux.Handle("/uppercase", kithttp.NewServer(
        upper,
        decodeUppercaseRequest,
        encodeResponse,
        kithttp.ServerBefore(kitot.HTTPToContext(tracer, "HTTP /uppercase", logger)),
))

upper = kitot.TraceServer(tracer, "uppercase")(upper)

Порядок обёрток: появится ли trace id в логе?

Прочитайте код и выберите, что он выведет:

upper = loggingMiddleware(logger)(upper)
upper = kitot.TraceServer(tracer, "uppercase")(upper)

// loggingMiddleware из урока 06 пишет только:
// logger.Log("method", "uppercase", "err", err, "took", time.Since(begin))

Собери инъекцию спана

Восстанови монтаж трейсинга. Пропуски — по порядку: опция транспорта для pre-декодерных функций, RequestFunc инъекции из пакета go-kit, установка глобального трейсера и endpoint-обёртка со span.kind=server.

mux.Handle("/uppercase", kithttp.NewServer( upper, decodeUppercaseRequest, encodeResponse, kithttp.(kitot.(tracer, "HTTP /uppercase", logger)), )) stdopentracing.(tracer) upper = kitot.(tracer, "uppercase")(upper)

Путь одного трейса

Расставь этапы трассированного запроса POST /uppercase в правильном порядке: от заголовка клиента до дерева в Jaeger UI.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Термины урока

Термин

opentracing.Tracer

нажмите, чтобы перевернуть
Определение

Интерфейс бэкенда: StartSpan, Inject, Extract. Реализации — NoopTracer по умолчанию, Jaeger, Zipkin; подменяется одной строкой в main.

1 / 6

Что дальше#

Четвёртый слой наблюдаемости встал на место: корневой спан инъецируется на транспорте через ServerBefore, TraceServer заводит дочерние спаны на endpoint'ах, ошибки из обоих каналов попадают в трейс, а Jaeger собирает дерево по uber-trace-id. Обещание урока 07 выполнено: спаны связали логи, метрики и теперь ещё и путь запроса одним trace id. Освежить обёртки endpoint — в уроке 02-endpoint, опции транспорта и ServerBefore — в уроке 04-http-transport, структурный лог — в уроке 06-logirovanie, метрики — в уроке 07-metriki; карта курса — на странице курса. Дальше — урок 09-rate-limit: расширенная механика поверх тех же обёрток. Пока весь трафик проходит в сервис свободно, но под нагрузкой или атакой это роскошь: поставим token bucket на endpoint и научимся отвечать честным 429 Too Many Requests с Retry-After.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • OpenTracing — модель: Tracer (StartSpan/Inject/Extract), Span (Finish/SetTag/LogFields), SpanContext — travel-данные, carrier — конверт для сети.
  • Span-инъекция: kithttp.ServerBefore(kitot.HTTPToContext(tracer, имя, logger)) — Extract из заголовков, корневой спан span.kind=server, спан кладётся в ctx.
  • TraceServer(tracer, имя) — endpoint.Middleware: дочерний спан по ChildOf, defer span.Finish, ошибки err и Failer в спане событиями.
  • Jaeger в main: jaegercfg.Configuration + NewTracer + closer.Close() в defer + SetGlobalTracer; агент UDP 6831, UI 16686, заголовок uber-trace-id.
  • Исходящие вызовы: ContextToHTTP в ClientBefore и TraceClient — каждая нога сети даёт пару client/server спанов, Jaeger склеивает трейс.
  • Сэмплинг: const для разработки, probabilistic/ratelimiting/remote для прода; решение принимается в голове трейса и наследуется всем спанам.
  • Opentracing-go и jaeger-client-go в режиме сопровождения, OpenTelemetry — будущее; модель переезжает 1-в-1, навык переносится.

Домашнее задание

Собери трейс-контур stringsvc целиком. Первое: подними Jaeger (docker run -p 16686:16686 -p 6831:6831/udp jaegertracing/all-in-one) и объяви jaegercfg.Configuration с Sampler const/Param 1 в main; поставь SetGlobalTracer и closer.Close() в defer. Второе: подключи kitot.HTTPToContext в kithttp.ServerBefore для всех трёх endpoint'ов с именами «HTTP /uppercase», «HTTP /count», «HTTP /concat». Третье: оберни каждый endpoint в kitot.TraceServer и убедись, что в Jaeger появились деревья из двух спанов на запрос. Четвёртое: добавь traceIDMiddleware и сравни trace_id в логе с trace id в UI — они обязаны совпасть. Пятое: экспериментируй с заголовками — пришли запрос с чужим uber-trace-id (из другого трейса UI) и убедись, что твой спан джойнулся к чужому дереву; потом переключи Sampler на probabilistic с Param 0.5 и объясни по UI, что изменилось. Шестое: добавь на один endpoint WithIgnoreBusinessError(true) и опиши, чем его спаны стали отличаться от соседних при ErrEmpty.

Личные заметкиПусто
Хранятся только в вашем браузере (go-kit · Tracing: OpenTracing)

Вопросы и ответы по уроку

Opentracing-go архивирован — может, сразу учить OpenTelemetry?

OpenTelemetry — правильный выбор для новых проектов, но go-kit поставляет обёртки именно под opentracing, а модель (Tracer, Span, SpanContext, Inject/Extract, carrier) в OTel практически идентична. Этот урок даёт инструменты, которые работают с текущим go-kit, и понятия, которые переезжают в otel без переучивания: инъекция в ServerBefore, спаны на endpoint, сэмплинг в main.

Трассировать всё или сэмплировать?

Для разработки и внутренних сервисов с малым трафиком — const 1: полный трейс стоит копейки и даёт полную картину. Для нагруженного прода — probabilistic (процент от трафика) или remote (управление из UI Jaeger без передеплоя). Трюк с tail-based sampling, когда хранятся только ошибочные или медленные трейсы, требует пайплайна на стороне collector и не решается настройками клиента.

Куда класть user_id: в baggage, в теги или в лог?

В лог из урока 06 — там поле ничего не стоит. Baggage едёт со SpanContext в каждом заголовке всей цепочки — большие или персональные значения раздут трафик и создают проблемы приватности. Теги спана полезны, но учти урок метрик: идентификаторы в тегах дробят выборку — искать по конкретному trace id или по времени с фильтром тегов низкой кардинальности (метод, исход) практичнее.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

gRPC25 уроков~14 чпродвинутый25 уроков уже доступны

gRPC-Go: высокопроизводительный RPC

Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.

  • Писать .proto-схемы и генерировать код
  • Реализовывать unary и streaming сервисы
  • Управлять метаданными, дедлайнами и отменами
  • Строить interceptors для логов и аутентификации