Использование Nginx в качестве прокси-сервера для нескольких сокетов Unix

TL;DR Порт прослушивания может быть спорным ресурсом на занятой общей машине, сокеты Unix практически не ограничены. Nginx может открывать их с помощью одного порта и префикса URL.

В некоторых ситуациях вы можете захотеть запустить множество (экземпляров) приложений на одной машине. Каждый экземпляр может нуждаться в предоставлении внутренней информации (например, Prometheus /metrics, обработчики профилирования/отладки) по ограниченному HTTP.

Когда количество экземпляров растет, становится трудно обеспечить бесконфликтное использование портов прослушивания. Напротив, использование сокетов Unix позволяет добиться большей прозрачности (читаемые имена файлов) и масштабируемости (легко придумать уникальное имя).

Вот небольшая демонстрационная программа, написанная на Go, которая будет обслуживать тривиальный HTTP-сервис с помощью Unix-сокета.

package main

import (
    "context"
    "flag"
    "io/fs"
    "log"
    "net"
    "net/http"
    "os"
    "os/signal"
)

func main() {
    var socketPath string

    flag.StringVar(&socketPath, "socket", "./soc1", "Path to unix socket.")
    flag.Parse()

    if socketPath == "" {
        flag.Usage()
        return
    }

    listener, err := net.Listen("unix", socketPath)
    if err != nil {
        log.Println(err.Error())
        return
    }

    // By default, unix socket would only be available to same user.
    // If we want access it from Nginx, we need to loosen permissions.
    err = os.Chmod(socketPath, fs.ModePerm)
    if err != nil {
        log.Println(err)
        return
    }

    httpServer := http.Server{
        Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
            log.Println(request.URL.String())
            if _, err := writer.Write([]byte(request.URL.String())); err != nil {
                log.Println(err.Error())
            }
        }),
    }

    // Setting up graceful shutdown to clean up Unix socket.
    go func() {
        sigint := make(chan os.Signal, 1)
        signal.Notify(sigint, os.Interrupt)
        <-sigint
        if err := httpServer.Shutdown(context.Background()); err != nil {
            log.Printf("HTTP Server Shutdown Error: %v", err)
        }
    }()

    log.Printf("Service is listening on socket file %s", socketPath)
    err = httpServer.Serve(listener)
    if err != nil {
        log.Println(err.Error())
        return
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

./soc -socket /home/ubuntu/soc1
Вход в полноэкранный режим Выход из полноэкранного режима
./soc -socket /home/ubuntu/soc2
Войти в полноэкранный режим Выход из полноэкранного режима

Вот минимальная конфигурация Nginx для обслуживания этих экземпляров с префиксами URL. Он получает http://my-host/soc1/foo/bar, удаляет префикс пути /soc1 и передает /foo/bar в soc1.

server {
    listen 80 default;

    location /soc1/ {
        proxy_pass http://soc1/;
    }
    location /soc2/ {
        proxy_pass http://soc2/;
    }
}

upstream soc1 {
    server unix:/home/ubuntu/soc1;
}

upstream soc2 {
    server unix:/home/ubuntu/soc2;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Каждый сокет Unix определяется как upstream и имеет оператор /location в server.

Также можно использовать сокеты Unix непосредственно в /location, как в примере

    location /soc1/ {
        proxy_pass http://unix:/home/ubuntu/soc1;
    }
Войти в полноэкранный режим Выйти из полноэкранного режима

однако это имеет нежелательное ограничение — вы не можете добавить трейлинг / к proxy_pass. Это означает, что URL будет передан как есть, например, soc1 получит /soc1/foo вместо /foo.

Чтобы избежать такого ограничения, мы можем использовать именованный upstream и добавить трейлинг / к proxy_pass.

    location /soc1/ {
        proxy_pass http://soc1/; # Mind trailing "/".
    }
Вход в полноэкранный режим Выход из полноэкранного режима

Оцените статью
devanswers.ru
Добавить комментарий