Irbisa · Шпаргалка

Go в проде

Docker multi-stage, конфиг и секреты, graceful shutdown, health и readiness, лимиты ресурсов и GOMAXPROCS в контейнере, миграции

Middle-разработчик · 21 вопрос

  1. 01

    Что такое контейнер, чем он отличается от виртуальной машины и что дают ему namespaces и cgroups?

    Контейнер представляет собой обычный процесс Linux, изолированный ядром: namespaces дают ему собственный взгляд на систему, а cgroups ограничивают, сколько CPU и памяти он может занять.

  2. 02

    Что делают Pod, Deployment, Service и ConfigMap и как Go-сервис проходит путь от образа до приёма трафика в Kubernetes?

    Pod запускает один или несколько контейнеров вместе; Deployment держит N подов одной версии и выкатывает их на следующую; Service даёт этим подам одно стабильное имя и адрес и шлёт трафик только в Ready-поды; ConfigMap хранит конфигурацию, которая становится переменными окружения или файлами.

    apiVersion: apps/v1kind: Deploymentmetadata:  name: ordersspec:  replicas: 3 # keep three Pods of this version running…
  3. 03

    Как Go-сервис должен получать конфигурацию, что говорит о ней 12-factor app и что происходит, если обязательного значения нет?

    Прочитать её из окружения один раз при старте в одну типизированную структуру Config: с дефолтами для безопасных значений и валидацией для остальных.

    type Config struct {	Addr            string	DatabaseURL     string // a secret: never log it	ShutdownTimeout time.Duration}…
  4. 04

    Чем отличаются SIGTERM, SIGINT и SIGKILL и какие из них Go-программа может перехватить?

    SIGTERM означает вежливое «пожалуйста, остановись», которое шлют docker stop и Kubernetes, SIGINT приходит от Ctrl+C, и оба перехватываются через signal.NotifyContext.

  5. 05

    Почему Go-контейнер должен работать не от root и как это настроить на scratch или distroless?

    Без user namespaces UID 0 в контейнере остаётся UID 0 на хосте, которого сдерживают только урезанные capabilities и seccomp, так что взломанный процесс под root отделяет от ноды один баг в ядре или рантайме.

    # Dockerfile: USER 65532:65532 (numeric, so runAsNonRoot can verify it)spec:  securityContext:    runAsNonRoot: true    runAsUser: 65532    runAsGroup: 65532…
  6. 06

    Контейнер на базе Alpine стартует через entrypoint.sh, который читает секрет и запускает /app/server. Теперь поды останавливаются все 30 с и не пишут лог shutdown. Почему?

    PID 1 занимает скрипт, а Go-бинарник работает как его дочерний процесс, поэтому SIGTERM от kubelet получает sh, который его не пересылает и, будучи PID 1 без обработчика, даже не умирает от него.

    #!/bin/sh# entrypoint.shset -euDB_PASSWORD="$(cat /run/secrets/db-password)"export DB_PASSWORD…
  7. 07

    Почему секреты нельзя зашивать в образ контейнера и как Go-сервис должен получать их вместо этого: при сборке и в рантайме?

    Любой слой образа может прочитать каждый, кто может его скачать, а удаление файла или переопределение переменной в следующем слое не убирает их из предыдущего.

    # syntax=docker/dockerfile:1FROM golang:1.25 AS buildWORKDIR /srcENV GOPRIVATE=github.com/acme/*COPY go.mod go.sum ./# wrong: ARG GITHUB_TOKEN, then RUN git config ... $GITHUB_TOKEN…
  8. 08

    Что -ldflags="-s -w" вырезает из Go-бинарника и что после этого продолжает работать: стектрейсы паник, pprof, Delve?

    -s убирает таблицу символов ELF, а -w убирает отладочную информацию DWARF, что обычно уменьшает бинарник примерно на 30%.

  9. 09

    Найдите баг в этом graceful shutdown: при SIGTERM запросы, которые сейчас обрабатываются, всё равно обрываются.

    func run(h http.Handler, db *sql.DB) error {	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM)	defer stop()	srv := &http.Server{Addr: ":8080", Handler: h, ReadHeaderTimeout: 5 * time.Second}	go func() {		<-ctx.Done()		sctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)		defer cancel()		srv.Shutdown(sctx)	}() 	if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {		return err	}	return db.Close()}

    ListenAndServe возвращает ErrServerClosed в тот момент, когда Shutdown закрывает листенер, а не когда заканчивается дренаж.

  10. 10

    Найдите баг: при раскатке поды убиваются с кодом 137 посреди дренажа, и Shutdown так и не завершается.

    spec:  terminationGracePeriodSeconds: 30  containers:    - name: api      image: registry.example.com/api:1.5.0      env:        - name: SHUTDOWN_TIMEOUT # the context deadline passed to srv.Shutdown          value: "25s"      lifecycle:        preStop:          sleep:            seconds: 30 # wait until the load balancer stops sending traffic

    У пода всего 30 с, но один только sleep в preStop занимает 30 с, а Shutdown разрешено ещё 25 с.

  11. 11

    Как безопасно открыть pprof на продовом сервисе и что атакующий может сделать с открытым /debug/pprof/?

    Отдавать его на отдельном admin-листенере, который не выставлен ни через Service, ни через Ingress, ни через балансировщик; проще всего использовать loopback и kubectl port-forward.

    import "net/http/pprof" // its init also registers on http.DefaultServeMux func adminServer() *http.Server {	mux := http.NewServeMux()	mux.HandleFunc("/debug/pprof/", pprof.Index)	mux.HandleFunc("/debug/pprof/profile", pprof.Profile)…
  12. 12

    Под перезапускается с OOMKilled и кодом выхода 137, а в логах Go пусто. Что произошло и чем это отличается от падения Go с out of memory?

    Память контейнера упёрлась в лимит cgroup, и OOM killer ядра отправил SIGKILL: ни паники, ни отложенных вызовов, ни последней строки в логе.

  13. 13

    Как Horizontal Pod Autoscaler решает, сколько подов запустить, и почему CPU служит плохим сигналом для масштабирования I/O-bound Go API или Kafka-консьюмера?

    Каждые 15 с HPA считает ceil(replicas × current / target) по каждой метрике и масштабирует к наибольшему результату в пределах minReplicas и maxReplicas.

    apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata:  name: apispec:  scaleTargetRef:…
  14. 14

    Что должен запускать CI-пайплайн Go-сервиса, в каком порядке и что делает его быстрым (кэши модулей и сборки, шардирование тестов)?

    Сначала дешёвые проверки, потом тесты, потом артефакт: go mod tidy -diff, go vet и линтер за секунды; затем параллельно go test -race, интеграционные тесты и govulncheck; затем один образ на коммит с тегом по SHA.

    on:  pull_request:  push:    branches: [main]jobs:  checks: # seconds: fail before anything slow starts…
  15. 15

    Чем отличаются rolling, blue-green и canary деплой и что должен поддерживать Go-сервис, чтобы любой из них проходил без простоя?

    Rolling заменяет поды по несколько штук, blue-green поднимает полную вторую копию и переключает весь трафик разом, а canary отправляет на новую версию малую долю трафика и расширяет её, пока метрики в порядке.

  16. 16

    Запускать миграции БД при старте сервиса или отдельным шагом деплоя, и что ломается в каждом варианте?

    В проде запускайте отдельным шагом до раскатки (CI-джоба или Kubernetes Job из того же образа), чтобы их применил один процесс один раз, со своим таймаутом и правами на DDL.

  17. 17

    Десять реплик запускают одну и ту же часовую задачу на time.Ticker, и отчёт уходит десять раз. Как выполнять периодическую работу ровно один раз?

    Вынесите расписание из реплик или заставьте их договориться, кто запускает: Kubernetes CronJob стартует один под на запуск, либо каждая реплика пытается захватить запуск в базе, и удаётся это только одной.

    // Every replica ticks once a minute and calls hourly. The first one to claim// the hour runs the job; the other nine see the claim and skip.func hourly(ctx context.Context, db *sql.DB, job string, run func(context.Context, time.Time) error) error {	period := time.Now().UTC().Truncate(time.Hour) // the run's identity: "the 13:00 report"	res, err := db.ExecContext(ctx, `		INSERT INTO job_runs (job, period) VALUES ($1, $2)…
  18. 18

    Как readiness управляет rolling update и как сделать так, чтобы сломанный релиз останавливался на первых подах, а не заменял весь парк?

    Rolling update убирает очередные старые поды только после того, как новые стали Ready и продержались в этом состоянии minReadySeconds, так что readiness служит тормозом раскатки.

    apiVersion: apps/v1kind: Deploymentmetadata:  name: apispec:  replicas: 10…
  19. 19

    За балансировщиком Go-сервис вызывает редкие 502 при любой нагрузке, и ни один не виден в его собственных логах. Как к этому приводит IdleTimeout и как это исправить?

    Балансировщик держит простаивающие keep-alive соединения к бэкенду и переиспользует их.

  20. 20

    Как задать requests и limits по CPU и памяти для Go-сервиса в Kubernetes и что происходит с GOMAXPROCS и GOMEMLIMIT, если убрать лимит по CPU?

    Память: лимит равен request, а GOMEMLIMIT составляет около 90% от него.

    containers:  - name: api    image: registry.example.com/api:1.5.0    resources:      requests:        cpu: "2"…
  21. 21

    Сервис держит HTTP API, Kafka-консьюмер, фоновые воркеры и пул БД. В каком порядке их останавливать по SIGTERM и почему?

    Перестать брать новую работу, доделать уже взятую и последним закрыть то, от чего эта работа зависит: readiness в 503, подождать обновления эндпоинтов, параллельно дренировать HTTP-сервер и консьюмер, затем воркеры, затем продюсеры, затем пул БД и в самом конце телеметрию.

    // Shutdown(ctx) stops taking new work and waits for work already taken.type drainer interface{ Shutdown(ctx context.Context) error } func drain(srv *http.Server, consumer, workers, producer, telemetry drainer, db *sql.DB) error {	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)	defer cancel()…