Содержание урока · 10 разделов
- Мультистейдж-образ: build-слой против runtime-слоя
- Окружение: 12-factor и лимиты рантайма
- Пробы и жизненный цикл: связка с graceful shutdown
- Ingress: TLS на границе, HTTP/2 внутрь
- Таймауты, лимиты и сжатие: выравнивание цепочки
- Чек-лист деплоя Connect-сервиса
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Сервис, который отлично работает на localhost, в проде встречает оркестратор с пробами, ingress с TLS, лимиты тела, таймауты прокси и SIGTERM среди ночи. Деплой connect собирает всё, что ты построил за курс: один mux с healthz и метриками из девятнадцатого урока, graceful shutdown, h2c для gRPC-жанров, лимиты из четырнадцатого, метрики из семнадцатого. Задача урока — выровнять всю цепочку: клиент, ingress, под, хендлер — так, чтобы ни одно звено не резало то, что нужно соседнему. Если собрать частые поисковые запросы — connect docker, grpc docker, connect деплой, — получится ровно чек-лист этого урока, по которому деплой connect проходит без сюрпризов: образ, окружение, пробы, остановка, TLS, HTTP/2, таймауты, лимиты. План: мультистейдж Dockerfile и почему CGO_ENABLED=0, окружение 12-factor и лимиты рантайма, пробы и жизненный цикл пода, ingress с TLS на границе и HTTP/2 внутрь, и финальное выравнивание таймаутов, лимитов тела и сжатия по метрикам p99.
Мультистейдж-образ: build-слой против runtime-слоя#
Docker-образ для Go-сервиса собирается двумя этажами. Первый — builder на полном golang-образе: go mod download выносится в отдельный слой после копирования только go.mod и go.sum — пока зависимости не менялись, слой берётся из кэша и никакие пакеты не перекачиваются; затем сборка с CGO_ENABLED=0: чисто Go-бинарник без линковки с libc работает на любом минимальном базовом образе, вплоть до scratch. Второй этаж — runtime: distroless/static с nonroot-пользователем или alpine, куда копируется только бинарник. Итог — десятки мегабайт вместо гигабайта тулчейна, меньше поверхность атаки, быстрее pull на нодах. connect-специфика тут минимальна, но одна привычка важна: кодогенерация buf generate выполняется в builder-слое или раньше в CI — в runtime-образе buf не нужен, сгенерированные файлы уже вкомпилированы. EXPOSE документирует порт 8080, ENTRYPOINT запускает бинарник; HEALTHCHECK в Dockerfile полезен для docker-compose и standalone Docker, но Kubernetes его игнорирует — там правят бал пробы манифеста, о них ниже.
Dockerfile: два этажа
GOFROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o /out/notes ./cmd/notes
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/notes /notes
EXPOSE 8080
USER nonroot
ENTRYPOINT ["/notes"]Кэш по слоям: пока go.mod не менялся, download берётся из кэша и сборка начинается с копирования кода. nonroot в distroless — процесс не root; порт 8080 больше 1024, привилегии не нужны. Флаг -trimpath убирает локальные пути из бинарника. В runtime-образе нет ни Go, ни buf, ни shell — только бинарник и минимум системных слоёв.
Зачем CGO_ENABLED=0 при сборке образа Connect-сервиса?
Окружение: 12-factor и лимиты рантайма#
Конфигурация живёт в окружении, не в образе: один и тот же артефакт катится через dev, staging и prod, различаясь переменными — PORT, адрес базы, уровень логов, базовый URL из пятого урока. Секреты приходят из хранилища оркестратора в переменные или смонтированные файлы, никогда — слоями образа. Два рантайм-лимитера, которые в контейнерах кусаются регулярно. GOMAXPROCS по умолчанию равен числу ядер ноды, а не CPU-лимиту пода: при лимите в полтора ядра планировщик Go гоняет десятки потоков, конкуренция и троттлинг пляшут, и p99 проседает без видимых причин — лечит uber-go/automaxprocs или явная установка переменной. GOMEMLIMIT ставь чуть ниже memory-лимита пода, чтобы GC начал убираться до OOMKill, а не после. Для connect-сервиса это не теория: метрики из семнадцатого урока — первый индикатор, что CPU-троттлинг или GC-давление начались, и связка го-профилей с платформенными лимитами экономит часы дебага. Логи пиши в stdout структурно — сборщик платформы заберёт их сам, файлы и ротации в контейнере не нужны. Флаги командной строки оставь для редких переопределений, env — для всего остального: так пайплайн деплоя остаётся одинаковым для всех сервисов платформы независимо от языка.
Пробы и жизненный цикл: связка с graceful shutdown#
Пробы из девятнадцатого урока становятся контрактом с оркестратором. Liveness на /healthz отвечает «процесс жив» без походов к зависимостям — ошибка здесь перезапускает под, и зависшая база не должна устраивать рестарт-шторм. Readiness на /readyz проверяет готовность принимать трафик — лёгкий пинг базы с таймаутом в полсекунды, потому что readiness дёргают часто. StartupProbe даёт долгую инициализацию, не убивая под до первого успеха. Жизненный цикл при выкатке: под исключается из эндпоинтов сервиса — readiness снимается, — затем получает SIGTERM; graceful shutdown из девятнадцатого урока перестаёт принимать новые вызовы и ждёт активные до grace-дедлайна. Правило выравнивания: terminationGracePeriodSeconds манифеста должен быть больше Shutdown-дедлайна с запасом на закрытие зависимостей, иначе оркестратор убьёт под посреди доезда unary-вызовов. Долгие bidi-стримы сами не закончатся — их гасит дедлайн Shutdown, а клиент обязан переживать обрыв ретраем или переподключением: деплой и клиентская вежливость из пятого урока — одна система, а не два отдельных дела. StartupProbe особенно полезен, если сервис на старте прогревает кэши или ждёт миграций: без него liveness убьёт под раньше, чем тот успеет встать.
Пробы и grace в манифесте
GOcontainers:
- name: notes
ports:
- containerPort: 8080
readinessProbe:
httpGet: { path: /readyz, port: 8080 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
lifecycle:
preStop:
exec: { command: ["sleep", "5"] }
terminationGracePeriodSeconds: 35preStop с паузой 5 секунд — время, чтобы исключение пода из эндпоинтов разъехалось по балансировщикам до SIGTERM; 35 секунд grace — больше Shutdown-дедлайна в 25-30 из девятнадцатого урока и больше p99 unary из метрик. Согласование трёх чисел — дедлайн Shutdown, grace пода, таймаут ingress — главная арифметика этого урока.
Что случится с вызовами при SIGTERM?
Прочитайте код и выберите, что он выведет:
<-ctx.Done() // SIGTERM от оркестратора
shutdownCtx, _ := context.WithTimeout(context.Background(), 30*time.Second)
srv.Shutdown(shutdownCtx)
// живы: пачка unary-вызовов и один bidi-стрим из восьмого урока
// terminationGracePeriodSeconds: 35Ingress: TLS на границе, HTTP/2 внутрь#
Топология TLS из тринадцатого урока в деплое принимает конкретные формы. Вариант по умолчанию: TLS терминируется на ingress — сертификаты выпускает cert-manager или платформенный секрет, — внутрь кластера идёт plaintext, и тогда для gRPC-жанров обязателен h2c из девятнадцатого урока плюс ingress, который проксирует HTTP/2, а не даунгрейдит до HTTP/1.1: в ingress-nginx это аннотация backend-protocol со значением GRPC, в рукописном nginx — grpc_pass вместо proxy_pass. Connect-жанру хватает HTTP/1.1, но держи h2c всегда: жанры не выключены, пока живы старые gRPC-клиенты из двадцатого урока. Второй вариант — end-to-end TLS до пода: http.Server с TLSConfig из тринадцатого урока, ingress в TCP-режиме или mesh; безопаснее для комплаенса, тяжелее в сертификатном хозяйстве. Трейлеры: чистый gRPC-жанр требует HTTP/2-трейлеров через всю цепочку — ещё одна причина не даунгрейдить; Connect-протокол обходится без трейлеров, и это его прод-плюс: промежуточные прокси ломают его реже. Проверка после выката: grpcurl через ingress на gRPC-жанр и curl на Connect-жанр — оба должны ответить на одном хосте. Адрес сервера внутри кластера — обычный DNS сервиса; баланс по подам делает платформа, но долгие gRPC-соединения требуют аккуратности: headless-сервис или клиентские ретраи, иначе все соединения залипнут на первом поде.
За ingress, который терминирует TLS и даунгрейдит до HTTP/1.1, живут оба жанра — Connect и gRPC. Что сломается?
Таймауты, лимиты и сжатие: выравнивание цепочки#
У каждой прослойки свои ручки, и их надо свести в одну арифметику. Таймаут ingress больше p99 unary из метрик семнадцатого урока, иначе прокси режет доезжающие вызовы раньше сервера. Shutdown-дедлайн меньше terminationGracePeriodSeconds — правило из блока о пробах. Лимит тела — два барьера: WithReadMaxBytes из четырнадцатого урока в хендлере и client_max_body_size в ingress; из двух сработает меньший, поэтому сделай их согласованными, чтобы клиент получал честный код resource_exhausted, а не загадочный 413 от прокси. Сжатие — классическая двойная работа: ingress с gzip на RPC-пути пережимает то, что хендлер уже сжал по порогу WithCompressMinBytes; выключи прокси-компрессию для RPC-путей и оставь решение хендлеру — порог считает размер сообщения, а не HTTP-тела. Метрики скрейпятся с /metrics того же mux — отдельный порт не нужен, но проверь, что ingress не публикует /metrics и /healthz наружу: служебные роуты — внутренние, публикуется только RPC-путь. Дедлайны per-call остаются в контексте из пятого урока — платформенные таймауты их не заменяют. Наконец, drainage: балансировщик должен переставать слать новые длинные соединения раньше, чем под умрёт, — иначе SIGTERM придёт соединению, которому ещё жить и жить.
Чек-лист деплоя Connect-сервиса#
| Слой | Настройка | Практика |
|---|---|---|
| Образ | мультистейдж, distroless, nonroot, CGO_ENABLED=0 | десятки МБ, без тулчейна и shell |
| Окружение | PORT, DSN, GOMAXPROCS, GOMEMLIMIT | конфиг в env, секреты из хранилища оркестратора |
| Пробы | liveness /healthz, readiness /readyz, startupProbe | без походов к зависимостям в liveness |
| Остановка | Shutdown 25-30 с меньше terminationGracePeriodSeconds | preStop sleep на разъезд эндпоинтов |
| TLS | терминация на ingress, cert-manager | h2c внутрь, HTTP/2-проксирование для gRPC-жанров |
| Таймауты | timeout ingress больше p99 unary | дедлайны per-call в контексте из пятого урока |
| Лимиты тела | WithReadMaxBytes и client_max_body_size согласованы | коды resource_exhausted вместо 413 от прокси |
| Сжатие | gzip ingress выключен на RPC-пути | WithCompressMinBytes решает в хендлере |
| Наблюдаемость | /metrics на mux, скрейп платформы | наружу публикуется только RPC-путь |
Собери пробы манифеста
Впиши пути проб: живость — без зависимостей, готовность — с лёгкой проверкой базы.
Путь релиза
Расставь путь релиза от коммита до трафика.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Даунгрейд HTTP/1.1 в ingress при живых gRPC-клиентах — жанр падает мгновенно, проверяй backend-protocol и grpc_pass. terminationGracePeriodSeconds меньше Shutdown-дедлайна — оркестратор прибивает поды посреди доезда unary-вызовов. Двойная компрессия ingress плюс хендлер — потраченный CPU на обоих этажах и прибавка латентности; компрессию решает хендлер порогом из четырнадцатого урока.
Сервис на одном порту с mux из девятнадцатого урока даёт один набор проб, одно ingress-правило, один скрейп-таргет: платформенная конфигурация линейна. Второй порт с админ-плоскостью оправдан только сетевой политикой, когда RPC-путь обязан быть физически недоступен с прод-балансировщика — иначе не усложняй: разделить роуты можно и на одном порту.
Код урока — настоящий Dockerfile, манифест и конфиги: собери образ учебного notes-сервиса из третьего урока и прокатись локально docker run с пробами curl-ом. Браузерная песочница сетевых вызовов не исполняет, поэтому исполняемых блоков в курсе нет — интерактивы работают на странице и дают XP, а деплой-цепочку проверяй docker build, docker run и вызовами grpcurl и curl через ingress.
Карточки: деплой
Что дальше по курсу#
Деплой замкнул дугу от proto до прода: образ, пробы, остановка, граница TLS и выровненные лимиты. Впереди — структура проекта: реальный шаблон с каталогами proto, gen и service в двадцать третьем уроке, типичные ошибки и отладка curl-режимом в двадцать четвёртом, и итоговый проект, соберущий сервис задач целиком. Для повторения: первый сервис и локальный запуск — в третьем уроке, TLS и сертификаты — в тринадцатом, лимиты и сжатие — в четырнадцатом, метрики и p99 — в семнадцатом, mux, h2c и graceful shutdown — в девятнадцатом, JSON-поверхность и GET-кэширование — в двадцать первом. Программа всех двадцати пяти уроков — на странице курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- деплой connect начинается с образа: мультистейдж Docker, CGO_ENABLED=0, distroless nonroot — без тулчейна и shell; buf generate живёт в CI.
- Конфиг в окружении по 12-factor; GOMAXPROCS через automaxprocs, GOMEMLIMIT ниже лимита пода — иначе троттлинг и OOMKill.
- liveness /healthz без зависимостей, readiness /readyz лёгкий, startupProbe для долгого старта; readiness связан с ротацией трафика.
- Shutdown-дедлайн меньше terminationGracePeriodSeconds; preStop sleep разъезжает эндпоинты; стримы гасит дедлайн Shutdown.
- TLS на границе с cert-manager, h2c внутрь, HTTP/2-проксирование ingress для gRPC-жанров; Connect-жанр живёт на HTTP/1.1.
- Таймаут ingress больше p99; лимиты тела согласованы с WithReadMaxBytes; компрессию решает хендлер, прокси-gzip выключен.
- Один порт и mux — один набор проб, одно ingress-правило, один скрейп; админ-порт только по сетевой политике.
- Проверка деплоя: docker run локально с curl-пробами, затем grpcurl и curl через ingress — оба жанра отвечают на одном хосте.
Домашнее задание
Упакуй notes-сервис и прогони его через всю цепочку. Шаг первый: мультистейдж Dockerfile из урока, docker build, docker run с публикацией 8080 — curl на healthz и JSON-вызов на процедуру из двадцать первого урока. Шаг второй: docker-compose с HEALTHCHECK и остановкой — запусти долгий bidi-стрим из восьмого урока, сделай stop контейнера и зафиксируй, что unary доезжают, а стрим гасится дедлайном Shutdown. Шаг третий: манифест с пробами и terminationGracePeriodSeconds 35, больше Shutdown-дедлайна; в kind или minikube сделай выкатку новой версии и понаблюдай за обрывами через метрики — их быть не должно для unary. Шаг четвёртый: ingress с TLS через cert-manager или self-signed и backend-protocol GRPC; grpcurl через ingress на gRPC-жанр и curl на Connect-жанр — оба должны ответить на одном хосте. Шаг пятый: выключи HTTP/2-проксирование и наблюдай падение grpcurl-жанра, верни обратно; затем проверь двойную компрессию, поставив gzip на ingress, и выпиши в заметки чек-лист выравнивания таймаутов и лимитов по p99 из семнадцатого урока.
Личные заметкиПусто
Вопросы и ответы по уроку
HEALTHCHECK в Dockerfile или пробы в манифесте?
HEALTHCHECK работает в docker-compose и standalone Docker, Kubernetes его игнорирует — там правят бал liveness, readiness и startup пробы манифеста. Если деплоишь и туда, и туда, держи один /healthz-эндпоинт для обеих систем: разница только в декларации, а не в коде сервиса.
Нужен ли service mesh для Connect?
Не обязателен: Connect живёт на обычном HTTP, и mesh-сайдкары проксируют его без специфики. Если нужен mTLS между подами, mesh даёт его без правок кода; без mesh — end-to-end TLS из тринадцатого урока на http.Server с TLSConfig.
Сколько реплик и какой autoscaler?
Начни с двух реплик и HPA по CPU после того, как GOMAXPROCS выровнен, иначе метрика врёт. Соединения gRPC длинные — scale-out новых реплик работает нормально, а scale-in рвёт стримы: держи graceful shutdown из девятнадцатого урока и drainage на балансировщике.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Fyne: десктопные и мобильные GUI на Go
Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.
- Создавать окна и интерфейсы из виджетов
- Управлять компоновкой: Box, Grid, Form
- Рисовать примитивами canvas и строить свои виджеты
- Связывать данные через binding