Введение в fasthttp

При создании высокопроизводительных HTTP-серверов на Go пакет fasthttp часто становится предпочтительным выбором для разработчиков, которым необходимо обрабатывать тысячи запросов в секунду с минимальной задержкой. В этой статье мы подробно рассмотрим fasthttp, его особенности, сравним его со стандартным пакетом net/http и узнаем, как использовать его для создания сверхбыстрого HTTP-сервера.

Зачем нужен fasthttp?

Пакет fasthttp предназначен для высоконагруженных сценариев, где стандартного пакета net/http может быть недостаточно. Вот несколько ключевых причин, по которым вы можете выбрать fasthttp:

  • Скорость: fasthttp оптимизирован для скорости и может обрабатывать более 100 тыс. запросов в секунду (QPS) и более 1 млн одновременных постоянных соединений на современном оборудовании.
  • Низкое выделение памяти: в отличие от net/http, который использует значительное выделение кучи, fasthttp минимизирует выделение памяти в горячих путях, делая его намного быстрее в плане сырой производительности.
  • Нулевое выделение памяти: во многих тестах fasthttp демонстрирует нулевое выделение памяти в своих горячих путях, что значительно повышает производительность.

Настройка окружения

Перед тем как приступить к коду, убедитесь, что у вас установлена последняя версия Go. fasthttp работает без проблем с Go версии 1.22 или выше.

go get -u github.com/valyala/fasthttp

Базовый сервер fasthttp

Давайте начнём с простого примера сервера fasthttp. Вот как можно настроить базовый сервер, который будет отвечать на запросы GET:

package main

import (
    "log"
    "github.com/valyala/fasthttp"
)

func main() {
    handler := func(ctx *fasthttp.RequestCtx) {
        switch string(ctx.Path()) {
        case "/":
            ctx.SetContentType("text/plain; charset=utf-8")
            ctx.SetBody([]byte("Welcome to the fasthttp server"))
        default:
            ctx.Error("Unsupported path", fasthttp.StatusNotFound)
        }
    }

    if err := fasthttp.ListenAndServe(":8080", handler); err != nil {
        log.Fatalf("Error in ListenAndServe: %s", err)
    }
}

Этот пример настраивает сервер, прослушивающий порт 8080 и отвечающий простым текстовым сообщением при обращении к корневому пути.

Обработка запросов и ответов

Одно из ключевых отличий между fasthttp и net/http заключается в способе обработки запросов и ответов. Вот сравнение того, как вы можете обработать простой запрос в обоих пакетах:

Пример net/http

package main

import (
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/":
        w.Write([]byte("Welcome to the net/http server"))
    default:
        http.Error(w, "Unsupported path", http.StatusNotFound)
    }
}

func main() {
    http.HandleFunc("/", handler)
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

Пример fasthttp

package main

import (
    "github.com/valyala/fasthttp"
)

func handler(ctx *fasthttp.RequestCtx) {
    switch string(ctx.Path()) {
    case "/":
        ctx.SetContentType("text/plain; charset=utf-8")
        ctx.SetBody([]byte("Welcome to the fasthttp server"))
    default:
        ctx.Error("Unsupported path", fasthttp.StatusNotFound)
    }
}

func main() {
    if err := fasthttp.ListenAndServe(":8080", handler); err != nil {
        panic(err)
    }
}

Обратите внимание на разницу в том, как обрабатывается контекст запроса. В fasthttp объект RequestCtx предоставляет всю необходимую функциональность для обработки запросов и записи ответов.

Тестирование вашего сервера fasthttp

Тестирование — важная часть любого процесса разработки. Вот как вы можете протестировать свой сервер fasthttp, используя слушатели в памяти, которые значительно улучшают производительность тестов.

Использование fasthttputil.NewInmemoryListener

package main

import (
    "testing"
    "github.com/valyala/fasthttp"
    "github.com/valyala/fasthttp/fasthttputil"
    "github.com/valyala/fasthttp/testing"
)

func TestHandler(t *testing.T) {
    ln := fasthttputil.NewInmemoryListener()
    defer ln.Close()

    go func() {
        if err := fasthttp.ListenAndServe(ln, handler); err != nil {
            t.Errorf("ListenAndServe error: %v", err)
        }
    }()

    req := testing.DoRequest(ln, "GET", "/", nil)
    if req.StatusCode != fasthttp.StatusOK {
        t.Errorf("Expected status code %d but got %d", fasthttp.StatusOK, req.StatusCode)
    }
}

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

Сравнение производительности

Чтобы понять разницу в производительности между net/http и fasthttp, давайте посмотрим на некоторые результаты тестов.

Результаты тестов net/http

Вот некоторые результаты тестов для net/http:

BenchmarkNetHTTPServerGet10ReqPerConn-4 500000 2751 ns/op 2118 B/op 19 allocs/op
BenchmarkNetHTTPServerGet100ReqPerConn-4 500000 2398 ns/op 2037 B/op 18 allocs/op

Результаты тестов fasthttp

BenchmarkServerGet10ReqPerConn-4 3000000 417 ns/op 0 B/op 0 allocs/op
BenchmarkServerGet100ReqPerConn-4 5000000 351 ns/op 0 B/op 0 allocs/op

Как видите, fasthttp значительно превосходит net/http, особенно с точки зрения выделения памяти и времени выполнения.

Диаграмма последовательности для обработки запроса fasthttp

Вот диаграмма последовательности, иллюстрирующая, как запрос обрабатывается на сервере fasthttp:

sequenceDiagram participant Client participant Server participant Handler Note over Client,Server: Client sends a GET request Client->>Server: GET / HTTP/1.1 Server->>Handler: Call handler function Handler->>Server: Process request and set response Server->>Client: HTTP/1.1 200 OK

Заключение

Создание высокопроизводительного HTTP-сервера на Go с использованием fasthttp — это простой и полезный процесс. Благодаря своей оптимизированной конструкции для скорости и минимального выделения памяти, fasthtml является идеальным выбором для приложений, требующих обработки тысяч запросов в секунду.