Netpoll: пишем сервера, которые не умирают от нагрузки
Вы знаете, что обычные сетевые библиотеки Go начинают »тяжело дышать», если их нагрузить десятками тысяч соединений? Неважно, делали вы HTTP API или свой TCP сервер — дефолтные инструменты вроде net
всегда имеют свои лимиты. Тут-то хорош зайдет Netpoll — библиотека, которая позволяет серверам обрабатывать сотни тысяч соединений одновременно и при этом не терять в производительности.
Почему Netpoll?
Если вы уже успели поиграться со стандартной библиотекой net
, то знаете: она классная… до поры до времени. Как только подключений становится больше тысячи, начинается боль: блокировки, миллионы горутин и изматывающее профилирование.
А вот Netpoll решает всё это за счёт асинхронности и низкоуровневого доступа к системе. Она использует epoll
на Linux и kqueue
на macOS. Для нас это значит:
Асинхронная работа: никаких блокировок. Всё крутится на событиях.
Нагрузоустойчивость: сервер может держать сотни тысяч соединений и даже не вспотеть.
Без лишнего копирования: данные читаются напрямую из буфера. Память не страдает, производительность радует.
Гибкая настройка: можно тонко подогнать под конкретные нужды.
Но не всё так идеально. Если вы пишете простой REST API, Netpoll вам вряд ли нужен. Зато если у вас чаты, игровые серверы, вебсокеты или TCP-прокси — это для вас.
Сразу к делу: пишем сервер
Создаём Listener и EventLoop
Netpoll делит работу между Listener (точка входа для соединений) и EventLoop (мозг, который обрабатывает события). Настроим этот тандем:
package main
import (
"fmt"
"time"
"os"
"os/signal"
"syscall"
"github.com/cloudwego/netpoll"
)
// Обработчик запросов
func handleRequest(ctx netpoll.Context, conn netpoll.Connection) error {
reader := conn.Reader()
data, err := reader.Next(512) // Читаем до 512 байт
if err != nil {
fmt.Printf("Ошибка чтения: %v\n", err)
return err
}
defer reader.Release() // Освобождаем буфер
fmt.Printf("Получено: %s\n", string(data))
writer := conn.Writer()
writer.WriteString("Привет! Это сервер на Netpoll!\n")
return writer.Flush() // Отправляем ответ
}
func main() {
// Настраиваем Listener
listener, err := netpoll.CreateListener("tcp", ":8080")
if err != nil {
panic(fmt.Sprintf("Ошибка создания Listener: %v", err))
}
defer listener.Close()
// Настраиваем EventLoop
eventLoop, err := netpoll.NewEventLoop(
handleRequest,
netpoll.WithReadTimeout(10*time.Second),
netpoll.WithIdleTimeout(5*time.Minute),
)
if err != nil {
panic(fmt.Sprintf("Ошибка создания EventLoop: %v", err))
}
fmt.Println("Сервер запущен на порту 8080...")
go func() {
err := eventLoop.Serve(listener)
if err != nil {
fmt.Printf("Ошибка EventLoop: %v\n", err)
}
}()
// Грейсфул-шатдаун
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
<-c
fmt.Println("Завершаю работу...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
eventLoop.Shutdown(ctx)
}
handleRequest обрабатывает запросы, так же настроили тайм-ауты для чтения и простоя. Плюсом реализовали грейсфул-шатдаун, сервер корректно завершает работу, закрывая соединения.
nocopy API
Netpoll позволяет работать с памятью напрямую. Т.е можно лишнего копирования, но здесь есть нюансы. Например, забудете вызвать Release()
— получите утечку памяти.
Чтение данных:
func handleRequest(ctx netpoll.Context, conn netpoll.Connection) error {
reader := conn.Reader()
buf, err := reader.Next(512)
if err != nil {
return fmt.Errorf("Ошибка чтения: %v", err)
}
defer reader.Release() // Буфер обязательно освобождаем
fmt.Printf("Получено: %s\n", string(buf))
return nil
}
Запись данных:
func writeResponse(conn netpoll.Connection, message string) error {
writer := conn.Writer()
buf, err := writer.Malloc(len(message)) // Выделяем память
if err != nil {
return fmt.Errorf("Ошибка выделения памяти: %v", err)
}
copy(buf, message)
return writer.Flush() // Отправляем данные
}
Клиентская часть Netpoll
Серверы — это круто, но часто нужен еще и мощный клиент. Пример:
package main
import (
"fmt"
"time"
"github.com/cloudwego/netpoll"
)
func main() {
conn, err := netpoll.DialConnection("tcp", "127.0.0.1:8080", 5*time.Second)
if err != nil {
panic(fmt.Sprintf("Ошибка подключения: %v", err))
}
defer conn.Close()
writer := conn.Writer()
writer.WriteString("Привет, сервер!")
writer.Flush()
reader := conn.Reader()
response, _ := reader.Next(512)
fmt.Printf("Ответ сервера: %s\n", string(response))
}
Продвинутые настройки
Поллеры
Настраиваем количество поллеров:
package main
import (
"runtime"
"github.com/cloudwego/netpoll"
)
func init() {
runtime.GOMAXPROCS(runtime.NumCPU()) // Используем все ядра
netpoll.SetNumLoops(runtime.NumCPU()) // Поллер на каждое ядро
}\
Балансировка нагрузки
Netpoll поддерживает стратегии распределения:
package main
import "github.com/cloudwego/netpoll"
func init() {
netpoll.SetLoadBalance(netpoll.RoundRobin) // Равномерное распределение
}
Тайм-ауты
Тайм-ауты защищают сервер от зависших соединений:
package main
import (
"time"
"github.com/cloudwego/netpoll"
)
func main() {
var conn netpoll.Connection
conn.SetReadTimeout(10 * time.Second) // Тайм-аут чтения
conn.SetIdleTimeout(5 * time.Minute) // Тайм-аут простоя
}
Мониторинг: метрики и логи
Используем logrus
или zap
для структурированного логирования:
package main
import (
"github.com/sirupsen/logrus"
)
var log = logrus.New()
func handleRequest(ctx netpoll.Context, conn netpoll.Connection) error {
reader := conn.Reader()
data, err := reader.Next(512)
if err != nil {
log.WithError(err).Error("Ошибка чтения")
return err
}
defer reader.Release()
log.WithField("data", string(data)).Info("Получены данные")
return nil
}
Далее подключаем Prometheus для мониторинга соединений:
package main
import (
"github.com/prometheus/client_golang/prometheus"
"net/http"
)
var (
activeConnections = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "active_connections",
Help: "Количество активных соединений",
})
)
func init() {
prometheus.MustRegister(activeConnections)
}
func main() {
go func() {
http.Handle("/metrics", prometheus.Handler())
http.ListenAndServe(":9090", nil)
}()
}
Возможные проблемы
nocopy API
и утечки памяти. Не забываем (!!!) вызыватьRelease()
после чтения.Перегрузка. Если сервер перегружен, ограничиваем количество соединений через
WithMaxConnections
.Грейсфул-шатдаун. Всегда освобождайте ресурсы корректно.
Заключение
Netpoll — это идеальный инструмент для высоконагруженных серверов.
Попробуйте, внедряйте и делитесь своими успехами в комментариях. И помните: хороший сервер — это сервер, который не падает.
Больше инструментов и практических кейсов эксперты OTUS рассматривают в рамках практических онлайн курсов.Также напомню о том, что в календаре мероприятий вы можете зарегистрироваться на ряд интересных бесплатных вебинаров.