15 вопросов
Процесс - изолированный экземпляр программы с собственным адресным пространством. Поток - единица выполнения внутри процесса; потоки делят память процесса. В Go одна программа - один процесс; множество горутин планируются на несколько потоков ОС (M:N). GOMAXPROCS задает макс. число потоков для пользовательского кода. Горутины легче потоков ОС.
runtime.GOMAXPROCS(runtime.NumCPU())Переключение контекста - сохранение состояния потока и загрузка другого. Частые переключения - накладные расходы. В Go переключение между горутинами дешевле переключения потоков ОС: планировщик Go переключает горутины на одних и тех же потоках. Блокирующий syscall (например, чтение с диска) может занять поток ОС; тогда создается новый поток для других горутин. Не блокировать горутины долгими syscall без ограничения их числа.
Сигналы ОС можно перехватить через signal.Notify. SIGINT (Ctrl+C) и SIGTERM (kube stop) - типичные для graceful shutdown. В Go перехватывают в канал, в select с контекстом или каналом остановки; при получении отменяют контекст и завершают сервер (Shutdown), закрывают соединения.
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)Способы: пайпы (os.Pipe), сокеты (Unix domain: net.Listen("unix", path)), разделяемая память (редко в Go), очереди сообщений. В Go чаще сокеты (TCP или unix) и HTTP/gRPC поверх них. Для локального взаимодействия unix socket быстрее TCP. Пример: сервис слушает unix socket, другой процесс подключается через net.Dial("unix", path).
ln, _ := net.Listen("unix", "/tmp/app.sock")
conn, _ := ln.Accept()Каждое сетевое соединение, открытый файл - файловый дескриптор. Лимит процесса - ulimit -n. В Go при утечке (не закрытые соединения, не закрытые файлы) дескрипторы заканчиваются. Всегда закрывать: defer conn.Close(), defer file.Close(), после использования response body: defer resp.Body.Close(). При пуле соединений контролировать размер пула и закрытие при shutdown.
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()Локальные переменные обычно на стеке; при "уходе" за пределы функции (возврат указателя, замыкание, сохранение в глобальной переменной) компилятор делает escape - выделение на куче. Escape analysis: go build -gcflags="-m" показывает, что уходит на кучу. Цель - минимизировать аллокации на куче для снижения нагрузки на GC. Указатели на стеке не должны сохраняться после возврата.
// escape: return &x
// no escape: return xВиртуальная память - каждому процессу свое адресное пространство; страницы маппятся на физическую память или swap. В Go приложение использует виртуальную память через runtime: куча горутин, стеки, куча аллокаций. Рост потребления - куча и стеки. OOM Killer убивает процесс при нехватке памяти; лимиты в контейнерах (cgroup) ограничивают использование.
Порядок байт: little-endian (младший байт по младшему адресу, x86) и big-endian (сетевой порядок). В Go encoding/binary: binary.BigEndian.PutUint32, binary.LittleEndian.Uint32; при чтении из сети обычно big-endian. Пакет binary.Read/binary.Write с порядком. Важно при реализации бинарных протоколов и чтении форматов файлов.
var val uint32
binary.Read(r, binary.BigEndian, &val)
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, val)COW - копирование при записи: общие данные до изменения, при записи создается копия. В ОС - fork процесса (страницы общие до записи). В приложениях - неизменяемые структуры, при "изменении" создается копия с изменением. В Go копирование слайсов и мап не COW; для иммутабельных данных делают копии при обновлении или используют структуры с версионированием.
epoll (Linux) и kqueue (BSD/macOS) - механизмы мультиплексирования I/O: один поток ждет события на множестве дескрипторов. В Go netpoller абстрагирует это: горутина блокируется на Read/Write, runtime регистрирует дескриптор в netpoller и переключает горутину на другую; при готовности I/O горутина возобновляется. Пользовательский код синхронный (блокирующий вызов), под капотом асинхронность.
net.Listen("tcp", ":8080") - слушать TCP; Accept() возвращает net.Conn (Read, Write, Close). net.Dial("tcp", "host:port") - клиентское соединение. UDP: net.ListenPacket("udp", ":8080"), ReadFrom/WriteTo (без установленного соединения). В Go сокеты абстрагированы через net.Conn и net.PacketConn; таймауты через SetDeadline. HTTP и gRPC строятся поверх TCP.
ln, _ := net.Listen("tcp", ":8080")
conn, _ := ln.Accept()
defer conn.Close()
io.Copy(conn, conn)Горутины - пользовательские легкие потоки (зеленые); планировщик Go (runtime) распределяет их по потокам ОС (M). G - горутина, M - поток ОС, P - контекст процессора (до GOMAXPROCS). При блокировке горутины (канал, syscall) M может отвязываться и выполнять другую горутину. Старт горутины дешевый (несколько КБ стека, растет при необходимости).
go func() { ... }()Steal time - время, когда виртуальный CPU готов выполнять код, но гипервизор отдал квант другому виртуал-хосту. Высокий steal - контейнер/ВМ недополучает CPU. В Go приложение видит это как замедление; метрики (например, node_cpu_seconds_total с mode=steal в Prometheus) помогают диагностировать. Решение - на уровне инфраструктуры (выделенные ядра, другой хост).
cgroups ограничивают ресурсы (CPU, память, I/O); namespaces изолируют видимость (PID, сеть, mount). Контейнер - процесс(ы) в своих namespaces с лимитами cgroups. Go-приложение в контейнере видит лимиты через /sys/fs/cgroup; GOMEMLIMIT можно выставить под cgroup memory limit. runtime.NumCPU() учитывает лимиты CPU; дефолтный размер стека горутины не зависит от cgroup.
// чтение лимита памяти cgroup для GOMEMLIMIT
// /sys/fs/cgroup/memory/memory.limit_in_bytesПри нехватке памяти ядро может убить процесс (OOM Killer). В Go при росте кучи GC пытается освободить память; при невозможности процесс может быть убит. В контейнерах лимит памяти задает cgroup; превышение ведет к OOM kill. GOMEMLIMIT (Go 1.19+) ограничивает кучу ниже лимита cgroup, чтобы GC активнее работал и снижал пиковое потребление, уменьшая риск OOM.
// GOMEMLIMIT=400Mi при лимите контейнера 512Mi
// оставляет запас для стека и прочего