Learning Platform
Глоссарий Troubleshooting
Урок 16.04 · 20 мин
Начальный
netstatsslsofSocketsTroubleshooting

netstat, ss, lsof — смотрим текущие соединения и открытые порты

Когда «порт занят» или «сервис не отвечает на 8080», вам нужен инструмент, который показывает текущее состояние сетевого стека: какие сокеты открыты, какие процессы их слушают, какие соединения установлены. В Linux/macOS таких инструментов три: netstat (классика, deprecated на Linux), ss (современная замена), lsof (универсальный list open files).

В этом уроке разберём, как ими пользоваться, какой когда, и как читать их output. Это базовый инструментарий sysadmin/DevOps.


netstat — классика, но deprecated

netstat — из эпохи UNIX 80-х. До сих пор повсеместен (macOS, Windows, BSD), но в современном Linux считается deprecated — его заменили на ss (из пакета iproute2). На большинстве Linux-дистрибутивов 2020-х netstat не предустановлен, нужно ставить отдельно.

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

# Все TCP-соединения и слушающие порты:
netstat -an

# tcp4       0      0  127.0.0.1.5432         *.*                    LISTEN
# tcp4       0      0  10.0.0.42.443          203.0.113.5.45678      ESTABLISHED
# tcp4       0      0  10.0.0.42.443          198.51.100.8.32145     ESTABLISHED
# udp4       0      0  *.53                   *.*

# Только TCP (без UDP):
netstat -an -p tcp

# С процессами (Linux):
sudo netstat -anp

# С процессами (macOS):
sudo netstat -anv  # macOS немного другие флаги

Поля:

  • Proto — tcp4 / tcp6 / udp4 / udp6.
  • Recv-Q / Send-Q — размер receive и send очередей. >0 в Recv-Q может означать, что процесс не успевает читать.
  • Local Address — IP:port на нашей стороне.
  • Foreign Address — IP:port удалённой стороны.
  • State — состояние TCP (LISTEN, ESTABLISHED, TIME_WAIT, CLOSE_WAIT, …).
# Только слушающие порты:
netstat -anlt -p tcp  # Linux
netstat -anlv | grep LISTEN  # macOS

# Найти, кто слушает порт 8080:
netstat -anv | grep ':8080.*LISTEN'

# Открытые UDP-сокеты:
netstat -an -p udp

ss — современный, быстрый

ss (Socket Statistics) — стандарт в Linux с 2010-х. Получает информацию напрямую из ядра через netlink, поэтому быстрее netstat (особенно при тысячах соединений).

Базовое:

# Все слушающие TCP:
ss -tln
# State       Recv-Q   Send-Q     Local Address:Port     Peer Address:Port
# LISTEN      0        128        127.0.0.1:5432         0.0.0.0:*
# LISTEN      0        128        0.0.0.0:80             0.0.0.0:*
# LISTEN      0        128        [::]:443               [::]:*

# Все TCP-соединения (включая ESTABLISHED):
ss -tn

# С процессами (нужен sudo):
sudo ss -tlnp

# Только UDP:
ss -uln

# IPv4 / IPv6:
ss -4tln  # только IPv4
ss -6tln  # только IPv6

# Подробная статистика по каждому соединению:
ss -tni

Флаги:

  • -t — TCP
  • -u — UDP
  • -l — listening only
  • -n — numeric (не резолвить имена)
  • -p — show processes
  • -i — internal info (RTT, retransmits, cwnd)
  • -a — all (listening + established)
  • -4 / -6 — IPv4 / IPv6

Особенно полезный -i — показывает TCP internal state:

ss -tni
State    Recv-Q   Send-Q       Local Address:Port     Peer Address:Port
ESTAB    0        0            10.0.0.42:443         203.0.113.5:45678
         cubic wscale:7,7 rto:204 rtt:1.234/0.678 ato:40 mss:1448
         cwnd:10 ssthresh:7 bytes_sent:12345 bytes_acked:12345 segs_out:18 segs_in:20
         data_segs_out:5 send 93.7Mbps lastsnd:12 lastrcv:14 pacing_rate 187.4Mbps
         delivery_rate 47.8Mbps app_limited rcvmss:536 advmss:1448 rcvspace:14600

Это глубокая TCP-диагностика:

  • rtt: 1.234/0.678 — средний RTT и его jitter. Полезно для понимания качества соединения.
  • cwnd: 10 — congestion window. Маленький cwnd = TCP считает сеть перегруженной.
  • retrans: N — сколько retransmits. Большой — потеря пакетов.
  • bytes_sent / bytes_acked — если разница большая, есть unacked данные.

Состояния TCP — что они значат

При просмотре соединений вы видите разные states. Каждое имеет конкретный смысл:

StateЗначение
LISTENСлушает входящие соединения
SYN_SENTПослали SYN, ждём SYN-ACK (клиент в начале handshake)
SYN_RECVПолучили SYN, послали SYN-ACK, ждём ACK (сервер)
ESTABLISHEDHandshake завершён, передача данных
FIN_WAIT_1Послали FIN, ждём подтверждения
FIN_WAIT_2Получили ACK на наш FIN, ждём FIN от другой стороны
TIME_WAITСоединение закрыто, ждём кратное 2*MSL (~60 сек) для предотвращения duplicate packets
CLOSE_WAITПолучили FIN от другой стороны, ждём app закрыть свою сторону
LAST_ACKПослали FIN после CLOSE_WAIT, ждём ACK
CLOSINGОбе стороны послали FIN одновременно
CLOSEDСоединение полностью закрыто
Основной жизненный цикл TCP-соединения
CLOSEDНачальное / конечное состояние
SYN
SYN_SENTПослали SYN, ждём SYN-ACK
SYN-ACK + ACK
ESTABLISHEDHandshake завершён, данные летают
ESTABLISHED
FIN
FIN_WAIT_1Послали FIN, начали закрывать
ACK
FIN_WAIT_2ACK получен, ждём FIN от другой стороны
FIN_WAIT_2
FIN
TIME_WAIT60 секунд для cleanup duplicate packets, потом CLOSED

Самые «подозрительные» состояния в production:

  • Много TIME_WAIT. Нормально для активного сервера, но если >10k — может быть проблема (исчерпание local ports). Решение: net.ipv4.tcp_tw_reuse=1.
  • Много CLOSE_WAIT. Означает: удалённая сторона послала FIN, но наш app не закрыл свою сторону. Это утечка соединений в коде — забыли close() или Connection.close().
  • Много SYN_SENT висящих. Не доходим до сервера, скорее всего firewall.
  • Много SYN_RECV висящих. Возможно, под SYN flood (см. урок про DDoS).
# Сколько соединений в каждом состоянии:
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
#    234 ESTABLISHED
#     89 TIME_WAIT
#     12 CLOSE_WAIT
#      5 LISTEN
#      1 SYN_SENT

lsof — list open files (и сокеты)

В UNIX «всё файл», включая сетевые сокеты. lsof показывает, какие файлы открыты каждым процессом. С нужными флагами это эквивалентно netstat + ps.

# Все открытые TCP/UDP-соединения:
sudo lsof -i

# Только TCP:
sudo lsof -iTCP

# Только UDP:
sudo lsof -iUDP

# Только LISTEN (слушающие порты):
sudo lsof -iTCP -sTCP:LISTEN

# Кто слушает конкретный порт:
sudo lsof -iTCP:8080

# Кто слушает 8080 на состояние LISTEN:
sudo lsof -iTCP:8080 -sTCP:LISTEN

# Все сетевые соединения процесса по PID:
sudo lsof -i -p 12345

# Все сетевые соединения по имени процесса:
sudo lsof -i -c nginx

# Без номеров (резолвить имена):
sudo lsof -i

# С номерами:
sudo lsof -i -n -P

Пример output:

COMMAND   PID USER   FD   TYPE   DEVICE SIZE/OFF NODE NAME
nginx    1234 root   6u   IPv4 0x123456      0t0  TCP *:80 (LISTEN)
nginx    1234 root   7u   IPv4 0x123457      0t0  TCP *:443 (LISTEN)
python3  5678 www   12u   IPv4 0x123458      0t0  TCP 127.0.0.1:8000 (LISTEN)
postgres 9012 postg  4u   IPv4 0x123459      0t0  TCP 127.0.0.1:5432 (LISTEN)

Главная фишка lsof — видно PROCESS+PID на каждом сокете. Это сразу отвечает на вопрос «кто занимает порт».


Кто слушает порт — стандартные сценарии

«Сервер не запускается — Address already in use»

python3 -m http.server 8000
# OSError: [Errno 48] Address already in use

Кто занял порт?

# Linux ss:
sudo ss -tlnp | grep ':8000'
# LISTEN  0  5  0.0.0.0:8000  0.0.0.0:*  users:(("python3",pid=12345,fd=3))

# Linux netstat:
sudo netstat -anp | grep ':8000.*LISTEN'

# macOS lsof:
sudo lsof -iTCP:8000 -sTCP:LISTEN
# python3   12345 user    3u  IPv4 ...  TCP *:8000 (LISTEN)

# Универсальный:
sudo lsof -i:8000

Видим: процесс python3 с PID 12345 уже слушает. Если хотим освободить порт:

sudo kill 12345
# или принудительно:
sudo kill -9 12345

«На каком порту мой сервер?»

# Запустили сервер с random port:
python3 -c "
from http.server import HTTPServer, BaseHTTPRequestHandler
server = HTTPServer(('', 0), BaseHTTPRequestHandler)  # 0 = random free port
print(f'Listening on {server.server_port}')
server.serve_forever()
"
# Listening on 49823

# Подтвердить через ss:
ss -tlnp | grep python

«Какие соединения активны?»

# Все ESTABLISHED:
ss -tn state established

# По конкретному порту:
ss -tn '( sport = :80 or dport = :80 )' state established

# С процессом:
sudo ss -tnp state established

Performance debugging через ss

ss -tni показывает TCP internal state — это бесценно для performance debug:

sudo ss -tni
ESTAB 0 0 10.0.0.42:443 203.0.113.5:45678
  cubic wscale:7,7 rto:204 rtt:1.234/0.678 ato:40 mss:1448
  cwnd:10 ssthresh:7 retrans:3
  bytes_sent:12345678 bytes_acked:12345600 segs_out:850 segs_in:870
  data_segs_out:850 data_segs_in:1
  send 12.5Mbps lastsnd:12 lastrcv:14 pacing_rate 25.0Mbps
  delivery_rate 8.4Mbps

Ключевые метрики:

  • rtt: avg/mdev — средний RTT и его медианное отклонение. Большой rtt — сеть тормозит.
  • cwnd — congestion window. Маленький при наличии данных = TCP замедлен из-за потерь.
  • retrans: N — ретрансмиты. Большой = packet loss.
  • send rate — текущая скорость. Если меньше ожидаемой — bottleneck.

Пример: ваш API клиент жалуется на медленные ответы. Запросите ss:

sudo ss -tnpi state established
ESTAB 0 0 10.0.0.42:443 203.0.113.5:45678 users:(("nginx",pid=1234,fd=10))
  cubic wscale:7,7 rto:1000 rtt:850.234/200.678 mss:1448
  cwnd:2 ssthresh:2 retrans:24

RTT 850ms — очень медленно для local DC. retrans:24 — много ретрансмитов. cwnd:2 — TCP сжался почти до минимума. Скорее всего проблема в качестве сети до клиента (Wi-Fi, плохой ISP).


Сравнение netstat / ss / lsof

Свойствоnetstatsslsof
Стандарт сегодняDeprecated (Linux)Yes (Linux)Yes (cross-platform)
СкоростьМедленный (читает /proc)Быстрый (netlink)Средний
Process info-p (Linux) или -v (macOS)-pПо умолчанию
Internal TCP infoНет-i (rtt, cwnd, retrans)Нет
Cross-platformДа (Linux, macOS, Windows)Только LinuxYes
Filter syntaxПростой grepБогатый ('( sport = :80 )')Простой
Network namespacesНетДа (-N)Нет

Когда что использовать:

  • Linux production — ss. Современный, быстрый, мощный.
  • macOS — lsof или netstat. ss не входит в macOS.
  • Кто слушает порт быстро — lsof. Лаконично.
  • TCP performance debug — ss -i. Internal state — только в ss.

Network namespaces (Docker, K8s)

В контейнерах ваши обычные netstat/ss/lsof показывают только хостовую namespace, не контейнерную. Чтобы заглянуть в контейнер:

# Список network namespaces:
ip netns list

# Войти в namespace конкретного контейнера и запустить ss:
sudo nsenter -t $(docker inspect -f '{{.State.Pid}}' my-container) -n ss -tlnp

# Или через docker exec:
docker exec my-container ss -tln
# (если в контейнере есть ss -- alpine может не иметь)

В Kubernetes:

# В пое:
kubectl exec my-pod -- ss -tln

# Или через kubectl debug (если включено):
kubectl debug -it my-pod --image=busybox -- netstat -an

Полезные one-liners

# Сколько соединений с конкретного IP:
ss -tn | awk -v ip="203.0.113.5" '$5 ~ ip {n++} END {print n}'

# Топ-10 IP по числу соединений:
ss -tn | awk 'NR>1 {split($5, a, ":"); print a[1]}' | sort | uniq -c | sort -rn | head

# Распределение по состояниям:
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn

# Соединения с конкретного порта (например, к нашему API):
ss -tnp dport = :443

# Watch соединений в реальном времени:
watch -n 1 'ss -tn state established | wc -l'  # счётчик ESTABLISHED

# Найти процессы с большим числом сокетов:
sudo lsof -i | awk '{print $1}' | sort | uniq -c | sort -rn | head

TIME_WAIT и почему его много

TIME_WAIT — это normal state, в котором соединение «висит» 60 секунд (2*MSL = Maximum Segment Lifetime) после закрытия. Это нужно, чтобы поздние duplicate packets не повлияли на новое соединение с тем же 4-tuple.

В production-сервере с большим RPS это нормально иметь много TIME_WAIT:

ss -tan | grep TIME-WAIT | wc -l
# 5230

Но если очень много — может исчерпать local source ports (limit ~30000) и новые исходящие соединения начнут failing с «port in use».

Решения:

# Включить TCP TIME_WAIT reuse (безопасно):
sysctl -w net.ipv4.tcp_tw_reuse=1

# Увеличить диапазон local ports:
sysctl -w net.ipv4.ip_local_port_range="1024 65535"

# Connection pooling в приложении -- лучшее решение, чем тюнить kernel:
# requests.Session() в Python, sql.DB connection pool в Go, etc
WARNING

‘tcp_tw_recycle’ — старая опция, удалена в Linux 4.12 из-за проблем с NAT. Не используйте ‘tcp_tw_recycle’, даже если найдёте в старых tutorials.


Попробуй сам

Серия упражнений для практики:

# 1. Что слушает на вашей машине прямо сейчас:
sudo lsof -iTCP -sTCP:LISTEN -n -P
# или Linux:
sudo ss -tlnp

# 2. Запустите простой сервер на 8000:
python3 -m http.server 8000 &
SERVER_PID=$!

# 3. Подтвердите, что слушает:
sudo ss -tlnp | grep 8000
sudo lsof -iTCP:8000

# 4. Сделайте к нему запросы и посмотрите ESTABLISHED:
curl http://localhost:8000/ &
ss -tn state established | grep 8000

# 5. Остановите сервер:
kill $SERVER_PID

# 6. Через несколько секунд TIME_WAIT:
ss -tan | grep 8000

# 7. Подождите ~60 секунд -- TIME_WAIT должен исчезнуть:
sleep 65
ss -tan | grep 8000  # больше нет

Анализ существующих соединений:

# Сколько соединений сейчас на вашем сервере к разным удалённым серверам:
ss -tn state established | awk 'NR>1 {split($5,a,":"); print a[1]}' | sort | uniq -c | sort -rn

# Какие процессы держат больше всего сокетов:
sudo lsof -i 2>/dev/null | awk '{print $1}' | sort | uniq -c | sort -rn | head

# Распределение по TCP-state:
ss -tan | awk 'NR>1 {state[$1]++} END {for(s in state) print state[s], s}' | sort -rn

# Activity на конкретный port:
sudo ss -tnp dport = :443 state established | head

ss, netstat и ip: полный арсенал диагностики соединений на Linux
Проверка знанийKnowledge check
Junior спрашивает: 'У меня на production сервере 15000 соединений в TIME_WAIT, при том что активных всего 200. Это утечка? Нужно ли паниковать?'
ОтветAnswer
Это нормально для активного веб-сервера, и не утечка. Но кое-что стоит понять. TIME_WAIT -- это намеренное состояние TCP, в котором сторона, которая закрыла соединение первой (обычно сервер при keep-alive timeout, или клиент при завершении запроса), 'удерживает' 4-tuple (src_ip, src_port, dst_ip, dst_port) на 60 секунд. Цель: не дать поздним duplicate packets (из-за retransmits в сети) повлиять на потенциальное новое соединение с тем же 4-tuple. Это RFC 793, фундаментальная часть TCP. 15000 TIME_WAIT при 200 active -- значит, у вас за последние 60 секунд закрылось ~15000 соединений, что соответствует ~250 RPS. Это совершенно нормальный объём для production-API. Утечка проявилась бы иначе: соединения накапливаются БЕЗ закрытия -- т.е. много ESTABLISHED или CLOSE_WAIT. Когда стоит беспокоиться. Local source ports -- 30000+ ports максимум (по default Linux). Если ваш сервер ИСХОДЯЩЕ инициирует много соединений (например, как клиент к другому сервису), и они все идут на один IP:port назначения (например, один upstream), то 4-tuple различается только source port -- ваши 30000 портов исчерпаются за минуту-две. Симптом: 'cannot assign requested address'. Это бывает в SSR с большим числом outbound API calls. Что делать. Если проблема в outbound: 1) Включите net.ipv4.tcp_tw_reuse=1 -- TIME_WAIT 4-tuples могут быть переиспользованы для новых соединений (безопасно). 2) Увеличьте net.ipv4.ip_local_port_range. 3) Используйте connection pooling в приложении -- requests.Session(), aiohttp ClientSession, HTTPS keep-alive. Pool из 20 переиспользуемых соединений лучше, чем 200 новых. Если 15000 TIME_WAIT на inbound port (server side) -- это нормально, никаких действий не требуется. Server side TIME_WAIT не блокируют новые входящие, потому что 4-tuple различается remote IP:port у каждого клиента.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 6. Какая команда быстро покажет, какой процесс слушает на порту 8080?

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

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

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

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