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 "/".
}