Зачем keepalive
TCP без трафика не знает, жив ли peer. Если клиент молча выдернул шнур или NAT закрыл маппинг - сервер увидит это только при попытке послать данные. На long-lived соединениях (БД-пулы, WebSocket'ы, очереди) это плохо: тред занят «зомби»-соединением.
Keepalive - системный механизм: после N секунд тишины ядро шлёт «пробный» ACK с устаревшим sequence-number. Если peer жив - вернёт ACK; если мёртв - ничего, и после M проб ядро закрывает сокет с ETIMEDOUT.
Три тюнинга на хост
| sysctl | дефолт | что |
|---|---|---|
net.ipv4.tcp_keepalive_time | 7200 (2 ч) | сколько секунд тишины до первой пробы |
net.ipv4.tcp_keepalive_intvl | 75 | интервал между пробами |
net.ipv4.tcp_keepalive_probes | 9 | сколько проб до объявления мёртвым |
Дефолт = 2 часа простоя + 9×75с = ~2 часа 11 минут до закрытия. В 99% сценариев это безумно долго.
Включить keepalive в приложении
Параметр сокета SO_KEEPALIVE - по дефолту выключен. Нужно явно:
import socket
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# Per-socket override (Linux):
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60) # 60s простоя
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10) # 10s между пробами
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) # 3 пробы
Получится: 60 + 3×10 = 90 секунд от тишины до закрытия.
Когда нужно тюнить
- БД-пулы (PostgreSQL/MySQL): дефолт 2 часа значит что после рестарта БД пул держит «мёртвые» сокеты до первого SQL-запроса. Поставить keepalive 30-60 секунд.
- WebSocket / gRPC с прокси посередине: NAT/LB закрывают idle обычно через 5-10 минут. Keepalive каждые 30-60 сек спасает.
- VPN / SSH туннели: то же.
- API behind cloud LB: AWS NLB закрывает idle через 350с по дефолту.
Keepalive vs application ping
| Подход | Плюсы | Минусы |
|---|---|---|
| TCP keepalive | бесплатно, в ядре | не проверяет, что приложение живо - только сокет; не работает через прокси, проксирующий L7 |
| Application ping | проверяет всю цепочку до handler'а | надо реализовать, лишний traffic |
Для [[websocket|WebSocket]] правильно делать обе - keepalive ловит оборванный TCP, application-ping (frame opcode 0x9) ловит зависший сервер.
Что видно в tcpdump
Keepalive-проба = пакет с seq = current_seq - 1, без payload, флаг ACK.
В tcpdump смотри пустой ACK через интервал tcp_keepalive_intvl от
предыдущего трафика.
IP 10.0.0.1.443 > 10.0.0.5.34521: Flags [.], ack 100, win 1024, length 0
Заметки
- Keepalive не сохраняет соединение «живым» в смысле NAT - он шлёт
пакеты, и NAT ровно поэтому не таймаутится. То есть меньше
tcp_keepalive_time, чем NAT-timeout - и NAT не закроет. - На современных Linux есть
TCP_USER_TIMEOUT- альтернатива: «закрыть соединение если за N миллисекунд нет ACK на отправленные данные». Часто полезнее keepalive'а, потому что работает и под нагрузкой.
Когда что-то пошло не так
- Соединение «висит» через 5 минут idle и не закрывается - keepalive выключен, либо tcp_keepalive_time > NAT-timeout
- Слишком частые пробы шумят - tcp_keepalive_intvl слишком мал, либо TCP_KEEPIDLE = 5 секунд (избыточно)
- Соединение закрылось за 5 минут хотя трафик идёт - не keepalive, смотри NAT/LB конфиг (idle timeout != keepalive)
error: ETIMEDOUTна send() - keepalive отработал, peer мёртв