Netpoll: пишем сервера, которые не умирают от нагрузки

10a059399d332935ab5dbd141aebabb6.jpg

Вы знаете, что обычные сетевые библиотеки 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)
	}()
}

Возможные проблемы

  1. nocopy API и утечки памяти. Не забываем (!!!) вызывать Release() после чтения.

  2. Перегрузка. Если сервер перегружен, ограничиваем количество соединений через WithMaxConnections.

  3. Грейсфул-шатдаун. Всегда освобождайте ресурсы корректно.

Заключение

Netpoll — это идеальный инструмент для высоконагруженных серверов.

Попробуйте, внедряйте и делитесь своими успехами в комментариях. И помните: хороший сервер — это сервер, который не падает.

Больше инструментов и практических кейсов эксперты OTUS рассматривают в рамках практических онлайн курсов.Также напомню о том, что в календаре мероприятий вы можете зарегистрироваться на ряд интересных бесплатных вебинаров.

© Habrahabr.ru