Netpoll: пишем сервера, которые не умирают от нагрузки
- четверг, 28 ноября 2024 г. в 00:00:11
Вы знаете, что обычные сетевые библиотеки Go начинают «тяжело дышать», если их нагрузить десятками тысяч соединений? Неважно, делали вы HTTP API или свой TCP сервер — дефолтные инструменты вроде net
всегда имеют свои лимиты. Тут-то хорош зайдет Netpoll — библиотека, которая позволяет серверам обрабатывать сотни тысяч соединений одновременно и при этом не терять в производительности.
Если вы уже успели поиграться со стандартной библиотекой net
, то знаете: она классная... до поры до времени. Как только подключений становится больше тысячи, начинается боль: блокировки, миллионы горутин и изматывающее профилирование.
А вот Netpoll решает всё это за счёт асинхронности и низкоуровневого доступа к системе. Она использует epoll
на Linux и kqueue
на macOS. Для нас это значит:
Асинхронная работа: никаких блокировок. Всё крутится на событиях.
Нагрузоустойчивость: сервер может держать сотни тысяч соединений и даже не вспотеть.
Без лишнего копирования: данные читаются напрямую из буфера. Память не страдает, производительность радует.
Гибкая настройка: можно тонко подогнать под конкретные нужды.
Но не всё так идеально. Если вы пишете простой REST API, Netpoll вам вряд ли нужен. Зато если у вас чаты, игровые серверы, вебсокеты или TCP-прокси — это для вас.
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 обрабатывает запросы, так же настроили тайм-ауты для чтения и простоя. Плюсом реализовали грейсфул-шатдаун, сервер корректно завершает работу, закрывая соединения.
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() // Отправляем данные
}
Серверы — это круто, но часто нужен еще и мощный клиент. Пример:
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 рассматривают в рамках практических онлайн курсов.Также напомню о том, что в календаре мероприятий вы можете зарегистрироваться на ряд интересных бесплатных вебинаров.