Learning Platform
Глоссарий Troubleshooting
Урок 09.03 · 22 мин
Начальный
IPCUnix SocketsTCPNetworking

Unix domain sockets — быстрее TCP на одной машине

Когда два процесса на одной машине должны общаться по запрос-ответ протоколу — HTTP, JSON-RPC, бинарный custom — стандартный выбор это TCP-сокет на localhost. Работает, отлажено, привычно. Но есть лучший вариант: Unix domain socket (UDS). Тот же API сокетов, тот же send/recv, но без сетевого стека под капотом — kernel доставляет данные прямо в очередь получателя, без TCP/IP, без портов, без routing-а.

Unix sockets — любимый механизм Docker (/var/run/docker.sock), PostgreSQL (/var/run/postgresql/.s.PGSQL.5432), Nginx-uwsgi, gunicorn workers, systemd-journald. Везде, где сервисы живут на одной машине, UDS дают 30-50% выигрыш по latency и неплохой по throughput по сравнению с TCP localhost. Плюс безопасность: pid и uid отправителя видны, доступ контролируется файловыми permissions.

В этом уроке: чем UDS физически отличаются от TCP, два типа (stream vs datagram), как их использовать в коде, fd passing — уникальная фишка UDS, и реальные production-сценарии.


Откуда выигрыш в производительности

TCP-сокет на localhost проходит через весь сетевой стек: socket API -> TCP layer -> IP layer -> loopback driver -> обратно в IP -> TCP -> socket. Это десятки функций, упаковка в TCP-сегменты, ARP, checksums, congestion control. Всё это нужно для разных машин, но на одном хосте — бесполезный overhead.

Unix socket — shortcut. Kernel создаёт две очереди (send/recv) между двумя сокетами и просто перекидывает данные. Никакого TCP, никакого IP, никакого checksum. Просто memcpy в kernel space.

TCP localhost vs Unix socket -- стек прохождения
TCP socket on 127.0.0.1Полный путь: write() -> socket API -> TCP layer (segmenting, checksum, ack tracking) -> IP layer (routing, fragmenting) -> loopback driver -> IP -> TCP -> socket recv queue -> read(). Десятки kernel functions, есть TCP state machine с RTT и congestion
Unix socket /tmp/app.sockКороткий путь: write() -> socket API -> прямо в recv queue адресата -> read(). Никакого TCP/IP, никакого checksum, никакого routing. Просто memcpy в kernel buffers
Latency: ~10-20 usTCP loopback на современном Linux: типичная round-trip ~15 микросекунд + работа сетевого стека. Throughput может быть отличным, но per-message latency высокая
Latency: ~5-10 usUnix socket: ~5-7 микросекунд per round-trip. 2-3x быстрее TCP loopback. На больших данных throughput тоже выше (нет TCP segment overhead)

Цифры зависят от железа и kernel-версии, но порядок — TCP loopback в ~2-3 раза медленнее UDS на маленьких сообщениях. На больших разница меньше (overhead amortized over data size).


Файл-представитель в файловой системе

Unix socket существует как файл специального типа в файловой системе:

# Создаём socket с помощью netcat:
nc -l -U /tmp/my.sock &
NC_PID=$!

ls -la /tmp/my.sock
# srw-rw-r-- 1 myuser myuser 0 May 18 10:00 /tmp/my.sock
#  ^
#  's' -- socket

# Подключаемся:
echo "hello" | nc -U /tmp/my.sock
# в Terminal 1 nc -l выведет: hello

kill $NC_PID
rm /tmp/my.sock

Тип файла s — socket. Размер всегда 0 — это просто маркер. Permissions работают: если файл 600, никто кроме owner-а не подключится. Это безопасность file-system-level, чего TCP-сокет на localhost не даёт.

Альтернатива — abstract namespace sockets (Linux-специфика): имя начинается с \0, файл не создаётся, но сокет идентифицируется в kernel-space. Полезно для приложений в read-only filesystems:

import socket
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind('\0my_abstract_socket')  # имя в abstract namespace
# Никакого файла в /tmp/ или где-либо

Stream vs datagram sockets

Как и TCP/UDP, Unix sockets имеют два режима:

SOCK_STREAM — байтовый поток, гарантированная доставка, в порядке отправки. Аналог TCP. Соединение устанавливается через connect/accept. Используется чаще всего.

SOCK_DGRAM — сообщения с границами, без соединения. Аналог UDP, но локально доставка гарантирована (kernel не «теряет» сообщения, если буфер не переполнен). Используется реже — например, systemd-journald принимает логи через datagram socket.

# Server (stream):
cat > uds_server.py << 'EOF'
import socket
import os

SOCK = '/tmp/echo.sock'
if os.path.exists(SOCK):
    os.unlink(SOCK)

server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(SOCK)
os.chmod(SOCK, 0o600)
server.listen(5)
print(f"Listening on {SOCK}")

while True:
    conn, _ = server.accept()
    data = conn.recv(4096)
    print(f"Got: {data!r}")
    conn.sendall(b"echo: " + data)
    conn.close()
EOF

python3 uds_server.py &

cat > uds_client.py << 'EOF'
import socket
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.connect('/tmp/echo.sock')
client.sendall(b"hello UDS")
print(client.recv(4096).decode())
client.close()
EOF

python3 uds_client.py
# echo: hello UDS

kill %1
rm /tmp/echo.sock

API идентичен TCP. Разница только в AF_UNIX вместо AF_INET и в bind/connect с file path вместо (host, port).


File descriptor passing — уникальная фишка UDS

Одна возможность, которая есть только у Unix sockets и недоступна никаким другим IPC механизмам — передача file descriptor от процесса A к процессу B. Звучит магически, но это так: один процесс открыл файл (или socket, или pipe), передал fd через UDS, второй процесс получил тот же открытый ресурс.

Использование:

  • systemd socket activation: systemd слушает сокет, при первом соединении запускает сервис и передаёт ему fd сокета. Сервис продолжает accept’ить, не зная, что сокет был не им создан.
  • nginx upgrade: новый nginx-master запускается, получает от старого fd слушающего сокета через UDS, начинает обслуживать — без потери соединений.
  • Privilege separation: привилегированный процесс открывает /dev/* (требует root), передаёт fd не-root процессу — тот работает без привилегий.
# Минимальный пример на Python (sendmsg / recvmsg с SCM_RIGHTS):
cat > fd_pass_server.py << 'EOF'
import socket, os, array

server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try: os.unlink('/tmp/fdpass.sock')
except: pass
server.bind('/tmp/fdpass.sock')
server.listen(1)
conn, _ = server.accept()

# Открываем файл, передаём fd:
fd = os.open('/etc/hostname', os.O_RDONLY)
conn.sendmsg(
    [b'fd_passed'],
    [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array('i', [fd]))]
)
print("Sent fd")
os.close(fd)
conn.close()
EOF

cat > fd_pass_client.py << 'EOF'
import socket, os, array

client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.connect('/tmp/fdpass.sock')

fds = array.array('i')
msg, anc, flags, _ = client.recvmsg(4096, socket.CMSG_LEN(fds.itemsize))
for cmsg_level, cmsg_type, cmsg_data in anc:
    if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
        fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])

new_fd = fds[0]
print("Got fd", new_fd)
print("File content:", os.read(new_fd, 100).decode())
os.close(new_fd)
EOF

python3 fd_pass_server.py &
sleep 0.5
python3 fd_pass_client.py
# Got fd 4
# File content: my-hostname

Здесь sendmsg с auxiliary data типа SCM_RIGHTS — стандартный механизм передачи fd. Получатель должен принять через recvmsg с буфером для ancillary data.


Реальные сценарии использования

Где используются Unix sockets в production
Docker daemon/var/run/docker.sock -- API daemon-а Docker. CLI 'docker ps' подключается сюда. Permissions 660 + группа docker -- стандартный способ дать пользователю доступ к Docker без sudo
PostgreSQL/var/run/postgresql/.s.PGSQL.5432 -- по дефолту psql на localhost идёт через UDS. Быстрее TCP loopback. Можно явно: psql -h /var/run/postgresql
systemd / journald/run/systemd/notify -- сервисы шлют notify сюда. /run/systemd/journal/socket -- логи приходят через UDS datagram. Активация сервисов по сокетам также через UDS
MySQL / MariaDB/var/run/mysqld/mysqld.sock -- аналогично PostgreSQL. По умолчанию локальные подключения идут через UDS, а не TCP
gunicorn + nginxgunicorn слушает на /tmp/gunicorn.sock, nginx делает upstream к нему. Web reverse proxy -> app server -- идеально для UDS, нет нужды в TCP
X Window System/tmp/.X11-unix/X0 -- X-сервер слушает локальные подключения. X-клиенты (приложения) подключаются и шлют framebuffers через UDS

Пример nginx -> gunicorn через UDS:

# gunicorn слушает на UDS:
gunicorn -b unix:/tmp/gunicorn.sock myapp:app

# nginx конфиг:
# upstream myapp {
#     server unix:/tmp/gunicorn.sock fail_timeout=0;
# }
# server {
#     location / {
#         proxy_pass http://myapp;
#     }
# }

Когда client подключается к nginx, nginx форвардит запрос через UDS gunicorn-у. По сравнению с TCP localhost экономия 5-10% latency, плюс tighter security (файловые permissions ограничивают, кто может подключиться).


Permissions и безопасность

Файловые permissions — big win UDS:

# Создать сокет, доступный только текущему пользователю:
chmod 600 /tmp/my.sock

# Доступ группе:
chmod 660 /tmp/my.sock
chgrp myapp_group /tmp/my.sock

Дополнительно — SO_PEERCRED (Linux) или getpeereid (BSD): сервер может узнать pid/uid подключившегося клиента.

# Python: узнать UID клиента после accept
import socket, struct
conn, _ = server.accept()
SO_PEERCRED = 17
creds = conn.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, struct.calcsize('3i'))
pid, uid, gid = struct.unpack('3i', creds)
print(f"Client: PID={pid}, UID={uid}, GID={gid}")

Это решает аутентификацию в local context — проверять не «правильный ли пароль», а «правильный ли uid».

TIP

PostgreSQL по дефолту использует UDS authentication как ‘peer auth’: если ваш uid в системе совпадает с именем PG-юзера — пускает без пароля. Это работает только через UDS, потому что TCP-соединение не несёт credentials. Поэтому psql на localhost ‘просто работает’, а через TCP требует пароль.


Минусы и ограничения

UDS не панацея. Минусы:

  • Same machine only. Очевидно: файла нет на другой машине.
  • Truncated path names. На многих системах sun_path всего 108 байт. Длинные пути не помещаются. Используйте короткие пути типа /tmp/x.sock.
  • Cleanup. Если сервер не сделал unlink(path) при выходе — файл остался, при следующем bind будет EADDRINUSE. Лекарство — atexit-hook и/или unlink перед bind.
  • No transport-level encryption. TLS не работает напрямую на UDS (хотя теоретически можно надеть). Но в локальном context это редко нужно — доступ контролируется permissions.
TCP-сервер и клиент на Python: тот же API, другой протокол

Попробуй сам

Проверьте, что Docker daemon слушает UDS:

ls -la /var/run/docker.sock
# srw-rw---- 1 root docker 0 May 18 09:00 /var/run/docker.sock

# Подключиться напрямую через curl:
sudo curl --unix-socket /var/run/docker.sock http://localhost/version
# {"Version":"24.0.5", ...}

# Это и есть Docker API через UDS

Запустите PostgreSQL и подключитесь через UDS:

# Если есть postgres:
psql -h /var/run/postgresql -U myuser mydb
# или просто:
psql -U myuser mydb   # по умолчанию идёт через UDS на localhost

# Через TCP (медленнее, требует пароль):
psql -h 127.0.0.1 -U myuser mydb

Замерьте разницу:

# 100k простых запросов через TCP:
time pgbench -h 127.0.0.1 -U myuser -c 1 -j 1 -T 10 mydb

# Через UDS:
time pgbench -h /var/run/postgresql -U myuser -c 1 -j 1 -T 10 mydb

# Типично UDS на 10-30% быстрее по TPS

Сделайте echo-сервер на Python:

cat > echo_uds.py << 'EOF'
import socket, os, time

PATH = '/tmp/echo_test.sock'
try: os.unlink(PATH)
except FileNotFoundError: pass

server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(PATH)
os.chmod(PATH, 0o600)
server.listen(5)

start = time.time()
count = 0
while time.time() - start < 5:
    conn, _ = server.accept()
    while True:
        data = conn.recv(4096)
        if not data: break
        conn.sendall(data)
        count += 1
    conn.close()

print(f"{count} round-trips in 5 sec = {count/5:.0f} req/sec")
os.unlink(PATH)
EOF

# Запустить сервер на 5 сек и клиента параллельно:
python3 echo_uds.py &
sleep 0.5

python3 -c "
import socket
c = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
c.connect('/tmp/echo_test.sock')
import time
start = time.time()
count = 0
while time.time() - start < 4:
    c.sendall(b'x'*100)
    c.recv(4096)
    count += 1
print(f'Client did {count} round-trips')
c.close()
"

wait

На локальной машине типично 50-100k round-trips/sec через UDS. Сравните с TCP — получится меньше.


Проверка знанийKnowledge check
Архитектор решил перейти с TCP localhost на UDS для общения между API gateway и downstream сервисами на одной машине. Какие изменения нужно учесть в коде, security и operations?
ОтветAnswer
Переход на UDS даёт 10-30% выигрыш по latency, но требует адаптации: 1) Код подключения: socket.socket(AF_UNIX) вместо AF_INET, connect(path) вместо connect((host, port)). Большинство HTTP-клиентов поддерживают UDS через специальные адаптеры -- aiohttp UnixConnector, requests-unixsocket, hyper для Rust. 2) Server bind: путь должен быть менее 108 байт, директория должна существовать, при бинде сервер должен сделать unlink (или check + remove) старого файла -- иначе bind вернёт EADDRINUSE. Также явно chmod после bind (0660 + правильная группа). 3) systemd: можно использовать socket activation -- systemd создаёт сокет, сервис получает fd через LISTEN_FDS. Это даёт zero-downtime restart и lazy-start. 4) Health checks: load balancer должен уметь проверять UDS, не все умеют. Внутренние health checks через локальные curl --unix-socket работают. 5) Logs: при дебаге нельзя просто 'tcpdump -i lo' -- UDS не видны там. Использовать strace для отладки или специальные тулзы (sockdump, eBPF). 6) Backup connectivity: если оба процесса контейнеризованы -- UDS работают только при правильно настроенных volume-mount-ах. Docker compose: shared volume или host bind. K8s: emptyDir или hostPath. Для multi-host fallback -- держать опциональный TCP-listener. 7) Security: filesystem permissions заменяют network-уровень auth. Убедиться что директория сокета не world-writable. 8) Cleanup: stale UDS-файлы при креше -- ловить через systemd-tmpfiles или atexit. 9) Мониторинг: метрики per-socket надо собирать иначе -- /proc/net/unix вместо /proc/net/tcp.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Почему Unix domain socket быстрее TCP loopback (127.0.0.1) на одной машине?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4