Почему unix-сокет быстрее, чем подключение к localhost по TCP
Приложение и база данных стоят на одном сервере, но по умолчанию всё равно общаются через TCP и 127.0.0.1 — просто потому что так проще настроить и это работает «из коробки». Разница в накладных расходах между этим вариантом и unix-сокетом не мифическая: ядро реально делает больше работы для TCP, даже если пакет физически никуда не уходит с машины. Ниже — что именно происходит внутри ядра в обоих случаях и когда имеет смысл переключаться.
Содержание
- Что на самом деле происходит при подключении к 127.0.0.1 по TCP
- Что такое unix-домен-сокет и почему это не сеть вообще
- Где именно TCP тратит лишнюю работу даже на loopback
- Что происходит на уровне структур данных в ядре
- Как переключить приложение и локальную базу на unix-сокет
- Когда выигрыш заметен, а когда нет, и какие есть ограничения
Что на самом деле происходит при подключении к 127.0.0.1 по TCP
Интуитивно кажется, что раз пакет не покидает машину, сеть тут «условная» и почти бесплатная. На деле ядро не знает заранее, что адресат — тот же хост, пока не дойдёт до маршрутизации, и весь путь пакета устроен так же, как для любого другого TCP-соединения.
Когда процесс вызывает connect() на 127.0.0.1:5432, происходит следующее:
- Ядро ищет маршрут для адреса назначения в таблице маршрутизации. Для loopback-адресов находится интерфейс
lo, но сам поиск маршрута выполняется как обычно, по общему пути маршрутизации. - Формируется TCP-сегмент: заголовок TCP (порты, sequence/ack номера, окно, флаги, контрольная сумма), поверх него — заголовок IP (адреса, TTL, протокол, своя контрольная сумма).
- Выполняется трёхстороннее рукопожатие: SYN, SYN-ACK, ACK — три полноценных сегмента с обработкой состояния TCP на обеих сторонах, даже если «обе стороны» — это два потока одного и того же ядра.
- Пакет проходит через сетевую подсистему: netfilter/conntrack (если правила iptables/nftables заданы, они применяются и к трафику на
lo), обработку сетевых пространств имён, если процессы находятся в разных network namespace (например, в разных Docker-контейнерах). - Дальше пакет «спускается» до интерфейса
lo, разворачивается и «поднимается» обратно вверх по стеку приёма: разбор IP-заголовка, проверка контрольной суммы, разбор TCP-заголовка, поиск сокета-получателя, укладка данных в приёмный буфер. - Дальше — обычная жизнь TCP-соединения: подтверждения (ACK) на сегменты, работа алгоритма контроля перегрузки (даже на loopback у TCP-соединения есть окно перегрузки, slow start, состояние
tcp_sock), возможные задержки из-за алгоритма Нейгла и delayed ACK, если они не отключены явно.
Ничего из этого не бесплатно с точки зрения работы ядра. Пакет не летит по проводу, поэтому нет задержек физической среды и коммутаторов, но вся программная часть сетевого стека — формирование заголовков, проверка контрольных сумм, маршрутизация, netfilter, управление TCP-состоянием — выполняется полностью, как для «настоящего» сетевого соединения. Подробнее о том, как устроен путь пакета внутри ядра, разобрано в статье про путь пакета от сетевой карты до сокета — там те же механизмы, только со стороны физического интерфейса.
Что такое unix-домен-сокет и почему это не сеть вообще
Unix domain socket (AF_UNIX) — это механизм межпроцессного взаимодействия (IPC), который использует API сокетов (socket(), bind(), connect(), accept(), send(), recv()), но не является сетевым протоколом. Программисту он выглядит как обычный сокет — можно использовать SOCK_STREAM (аналог TCP по семантике: надёжный, потоковый, с сохранением порядка байт) или SOCK_DGRAM (аналог UDP). Но под капотом там нет ничего сетевого:
- Вместо IP-адреса и порта адресом служит путь в файловой системе — например,
/var/run/postgresql/.s.PGSQL.5432или/run/redis/redis.sock. Файл сокета — это специальный inode, используемый только как точка адресации. - Нет IP-заголовка и TCP/UDP-заголовка — их физически не существует, потому что нет сетевого уровня, через который бы шли данные.
- Нет контроля перегрузки, окна TCP, подтверждений на уровне протокола — потому что нет самого протокола передачи по сети. Данные копируются между буферами процессов через ядро.
- Нет маршрутизации и netfilter/conntrack в обычном смысле — правила iptables/nftables к unix-сокетам не применяются (кроме LSM-модулей вроде AppArmor/SELinux, которые контролируют доступ на уровне файловой системы, а не сети).
- Права доступа регулируются как у обычного файла: владелец, группа, права
rwx. Это одновременно удобство и источник частых ошибок (см. ниже).
По сути, unix-сокет — это способ дать двум процессам на одной машине прямой канал данных через ядро, используя файловую систему только как способ найти друг друга (адресация), а не как хранилище данных.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверГде именно TCP тратит лишнюю работу даже на loopback
Разница между TCP-к-localhost и unix-сокетом — не в одной точке, а в сумме нескольких слоёв, которые TCP обязан пройти, а unix-сокет — нет:
- Построение и разбор заголовков. Каждый TCP-сегмент оборачивается в TCP-заголовок (минимум 20 байт) и IP-заголовок (минимум 20 байт для IPv4), и на каждый заголовок считается контрольная сумма. Unix-сокету заголовки такого рода не нужны вообще — данные копируются как есть, с минимальной служебной обёрткой на уровне ядра.
- Состояние соединения TCP. У каждого TCP-сокета есть структура состояния — окно приёма и отправки, счётчики sequence/ack, состояние машины TCP (
ESTABLISHED, обработкаFIN/RST), состояние контроля перегрузки (окно перегрузки, slow start threshold). Всё это ядро обязано поддерживать и обновлять на каждый пакет, даже если оба конца соединения — процессы на одной машине без реальной сетевой задержки. У unix-сокета этого состояния попросту нет — там нечего разгонять и не от чего защищаться перегрузкой, потому что нет разделяемого сетевого канала с конкуренцией за пропускную способность. - Подтверждения и таймеры. TCP должен подтверждать доставку сегментов (ACK) и держать таймеры ретрансмиссии на случай потери пакета. На loopback пакеты почти никогда не теряются, но ядро всё равно ведёт таймеры и генерирует ACK-сегменты — это дополнительные проходы через стек в обе стороны.
- Маршрутизация и netfilter. Даже для
127.0.0.1ядро выполняет поиск маршрута и, если заданы правила iptables/nftables, прогоняет пакет через conntrack и цепочки правил. Для unix-сокета таблицы маршрутизации не существует в принципе. - Количество копирований данных. У TCP путь данных длиннее: буфер отправки → формирование сегментов → путь по стеку вниз → путь по стеку вверх на приёме → буфер приёма. У unix-сокета путь короче: ядро передаёт данные напрямую между буферами двух процессов без сборки/разбора сетевых заголовков на каждом шаге.
Важно: ни один из этих пунктов не означает, что TCP «плохой» или «медленный» в целом — контроль перегрузки, окна и подтверждения существуют не просто так, они нужны, когда данные реально идут по нестабильному каналу с конкуренцией за полосу. На loopback вся эта защита работает вхолостую — канал внутри одной машины не теряет пакеты и не имеет реальной задержки распространения, поэтому вы платите за механизмы, которые здесь ничего не защищают. Про то, зачем вообще нужен контроль перегрузки TCP и чем ограничена скорость реального соединения, подробнее — в статье про TCP-окно и почему канал через океан тормозит.
Что происходит на уровне структур данных в ядре
Для TCP-сокета ядро поддерживает структуру struct sock, а поверх неё — расширенную структуру struct tcp_sock, в которой хранится вся специфика протокола: окна отправки и приёма, состояние конечного автомата TCP, параметры контроля перегрузки, очереди ретрансмиссии, таймеры. Каждый сегмент так или иначе затрагивает эти поля — ядро должно их читать и обновлять, даже если оба процесса живут на одном хосте.
Для unix-сокета тоже используется struct sock как общая база (почти любой тип сокета в Linux так или иначе опирается на неё), но поверх неё лежит гораздо более простая структура unix_sock. В ней нет полей для окна перегрузки, таймеров ретрансмиссии или состояния маршрутизации — вместо этого там, по сути, очередь буферов (sk_buff), которые ядро передаёт от одного процесса к другому напрямую, без прохождения через IP- и TCP-уровни стека.
Разница в структурах — не абстракция ради абстракции: чем меньше полей нужно инициализировать и обновлять на каждую операцию, тем меньше работы ядру нужно проделать за то же самое действие «переслать байты от одного процесса к другому». Для программиста разница почти незаметна — POSIX API сокетов одинаковый что для AF_INET, что для AF_UNIX. Именно поэтому переключение приложения на unix-сокет обычно не требует переписывать логику — меняется только адрес подключения.
Как переключить приложение и локальную базу на unix-сокет
Если приложение и база данных (или кэш) стоят на одном сервере, переключение с TCP/localhost на unix-сокет обычно ограничивается конфигурацией, без изменений в коде.
PostgreSQL. По умолчанию PostgreSQL уже слушает unix-сокет — обычно в /var/run/postgresql/ или /tmp, в зависимости от дистрибутива. Проверить путь можно так:
SHOW unix_socket_directories;
Клиентские библиотеки (libpq и большинство ORM поверх неё) подключаются через unix-сокет автоматически, если в строке подключения не указан host, либо host явно указывает на директорию сокета:
# вместо
postgresql://user:pass@127.0.0.1:5432/dbname
# использовать
postgresql://user:pass@/dbname?host=/var/run/postgresql
MySQL/MariaDB. Сокет обычно уже поднят по пути /var/run/mysqld/mysqld.sock или /run/mysqld/mysqld.sock. Клиент переключается на него, если в конфиге указать socket вместо host:
[client]
socket = /run/mysqld/mysqld.sock
Стоит помнить конкретную особенность MySQL: если в качестве хоста указать буквально localhost, клиентская библиотека сама переключится на unix-сокет; а вот 127.0.0.1 — это явное указание использовать TCP, даже если сервер и клиент на одной машине. Это частый источник путаницы, когда «localhost» в конфиге меняют на IP «для ясности» и неожиданно теряют преимущество unix-сокета.
Redis. В redis.conf можно включить unix-сокет параллельно с TCP или вместо него:
unixsocket /run/redis/redis.sock
unixsocketperm 770
port 0
port 0 полностью отключает TCP-листенер — полезно, если Redis используется только локально и незачем держать открытый порт вообще.
Nginx + PHP-FPM / приложение на unix-сокете. Веб-сервер может проксировать запросы на бэкенд через unix-сокет вместо TCP:
upstream app_backend {
server unix:/run/app/app.sock;
}
и в конфиге пула PHP-FPM:
listen = /run/php-fpm/php-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
Во всех случаях логика одна: приложение и сервис должны быть на одном хосте, а путь к сокету — доступен процессу приложения по правам файловой системы. Это первое, что стоит проверить при ошибке Permission denied или No such file or directory. Про типичные проблемы с подключением к PostgreSQL разобрано отдельно в статье про то, почему PostgreSQL не принимает подключения, а базовая установка — в статье про установку и настройку PostgreSQL на VPS.
Когда выигрыш заметен, а когда нет, и какие есть ограничения
Разница в накладных расходах между TCP-к-localhost и unix-сокетом реально существует на уровне того, что делает ядро на каждый вызов — но насколько это ощутимо на практике, зависит от характера нагрузки:
- Заметнее всего — там, где очень много коротких запросов: веб-приложение с несколькими обращениями к базе или кэшу на каждый HTTP-запрос, PHP-FPM, чат-сервисы через локальный брокер. Здесь накладные расходы на TCP-состояние складываются на большом количестве операций.
- Менее заметно — там, где соединение долгоживущее и данные передаются большими порциями редко (потоковая передача файлов или бэкапов между процессами). На первый план выходят не накладные расходы протокола, а скорость самого копирования данных.
- Не работает вообще — если процессы на разных серверах. Unix-сокет не маршрутизируется по сети и не заменяет TCP, если база стоит на отдельном сервере. В такой конфигурации TCP — единственный вариант.
Ограничения, которые стоит держать в голове перед переключением:
- Права доступа. Сокет — это файл в файловой системе, значит на него действуют владелец, группа и права
rwx. Ошибка в правах (или в том, что процессы работают от разных пользователей без общей группы) — самая частая причина, почему «переключили на сокет — перестало работать». - Контейнеры и пространства имён. Если приложение и база работают в разных Docker-контейнерах, у них разные mount namespace, и путь к сокету одного контейнера не виден в другом без явного общего volume. Просто «указать путь к сокету» в такой конфигурации не сработает — нужно пробросить volume с директорией сокета в оба контейнера.
- Нет шифрования на уровне протокола. У unix-сокета нет аналога TLS — но это, как правило, не проблема, потому что данные не покидают ядро и одну машину; шифрование сетевого уровня тут просто неприменимо по смыслу.
- Наблюдаемость. Многие привычные инструменты мониторинга и диагностики заточены под TCP-порты —
ss -tlnp, метрики по портам в системах мониторинга. Для unix-сокетов нужна отдельная привычка смотретьss -xlnpили проверять файлы сокетов напрямую, иначе может казаться, что сервис «не слушает», хотя он просто слушает не то, что вы проверяете.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Даёт ли переключение на unix-сокет заметный прирост производительности сам по себе?
Зависит от нагрузки. Ядру приходится делать меньше работы на каждую операцию (нет заголовков TCP/IP, нет контроля перегрузки, короче путь копирования данных), и при большом количестве коротких запросов это складывается в экономию ресурсов процессора. При редких больших передачах данных или нагрузке, упирающейся в диск, разница малозаметна на общем фоне.
Можно ли использовать unix-сокет и TCP одновременно для одного и того же сервиса?
Да, большинство серверных приложений (PostgreSQL, MySQL, Redis) умеют слушать оба варианта одновременно — unix-сокет для локальных клиентов и TCP-порт для подключений с других серверов или для мониторинга.
Что будет, если приложение попытается подключиться к TCP-порту, а сервис слушает только unix-сокет (или наоборот)?
Клиент получит ошибку соединения — Connection refused для закрытого TCP-порта либо No such file or directory для отсутствующего файла сокета. Это разные механизмы адресации, клиентская библиотека должна явно знать, к какому из них подключаться.
Работает ли unix-сокет между процессами в разных Docker-контейнерах?
Только если контейнеры используют общий volume, в который смонтирован каталог с файлом сокета обеих сторон. Сетевая связность контейнеров (общая docker-сеть) тут не помогает — это не сетевой механизм.
Стоит ли переписывать уже работающую инфраструктуру ради unix-сокетов?
Если приложение и база стабильно работают на одном сервере и упираются в накладные расходы на большом количестве локальных запросов — да, это дешёвое изменение конфигурации без переписывания кода. Если сервисы разнесены по разным серверам или узкое место в медленных запросах к базе или в дисковом вводе-выводе, переключение на unix-сокет проблему не решит.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →