March 1

Каждый баг, который я когда-либо исправлял, начинал иметь смысл только после того, как я понял эти 7 уровней

Это перевод оригинальной статьи Every Bug I Ever Fixed Made Sense Only After I Understood These 7 Layers.

Подписывайтесь на телеграм-канал usr_bin, где я публикую много полезного по Linux, в том числе ссылки на статьи в этом блоге.

Спустя три года работы я потратил две недели на отладку причин, по которым наш API случайным образом возвращал ошибки 502. Логи были чистыми. Приложение работало исправно. Все указывало на то, что проблем нет.

Затем мимо проходил сетевой инженер и спросил: «А ты проверял, совпадает ли таймаут keepalive у балансировщика нагрузки с таймаутом сервера приложения?»

Изображение, сгенерированное искусственным интеллектом
Тогда меня осенило. Я занимался отладкой не на том уровне.

Семь уровней, которые действительно имеют значение

Модель OSI — это не какое-то академическое упражнение, которое нужно заучивать наизусть для собеседований. Это ментальная модель, которая позволяет не тратить дни, копаясь не там, где нужно.

Layer 7: Application    [HTTP, DNS, SSH]
Layer 6: Presentation   [TLS, Compression]
Layer 5: Session        [Auth, Connections]
Layer 4: Transport      [TCP, UDP]
Layer 3: Network        [IP, Routing]
Layer 2: Data Link      [Ethernet, MAC]
Layer 1: Physical       [Cables, Signals]

Вот что на самом деле происходит при обращении к API:

User Request
    |
    v
[Application Layer] - Parse HTTP request
    |
    v
[Transport Layer]   - Establish TCP connection
    |
    v
[Network Layer]     - Route packets via IP
    |
    v
[Data Link Layer]   - Frame data for physical transmission
    |
    v
[Physical Layer]    - Transmit bits over wire

Уровень 7: Прикладной уровень

Здесь работает большинство разработчиков. Коды состояния HTTP, ответы API, запросы к базе данных. Но вот чего они не говорят: большинство «ошибок в приложениях» на самом деле не являются ошибками приложения.

Реальная ошибка: Пользователи жаловались, что эндпоинт загрузки файлов «не работает». API возвращал 200 OK, но файлы не отображались.
// The problematic code
func uploadHandler(w http.ResponseWriter, r *http.Request) {
    file, _, err := r.FormFile("file")
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    defer file.Close()
    
    // Processing happens here
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}

В чём проблема? Большие файлы (свыше 50 МБ) тихо отклонялись nginx ещё до того, как запрос доходил до приложения. Код 200 приходил от другого запроса, который успешно обработался.

Исправление находилось на три уровня ниже:

# /etc/nginx/nginx.conf
client_max_body_size 100M;

Уровень 4: Транспортный уровень

TCP против UDP. Таймауты соединения. Исчерпание портов. Этот уровень разрушил бесчисленное количество карьер.

Классический сценарий: приложение перестаёт отвечать под нагрузкой, но показатели процессора и памяти в норме.
# Check current connections
ss -s

# Output showing the problem
TCP: 28547 (estab 1034, closed 27500, orphaned 12, timewait 27488)

27 488 соединений находятся в состоянии TIME_WAIT. Ядру не хватило доступных портов.

// Before (bad) - Creates new connection every time
func fetchUsers(userIDs []string) error {
    for _, id := range userIDs {
        resp, err := http.Get(fmt.Sprintf("https://api.example.com/user/%s", id))
        if err != nil {
            return err
        }
        defer resp.Body.Close()
        // Process response
    }
    return nil
}

// After (good) - Reuses connections
var client = &http.Client{
    Timeout: 10 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    },
}

func fetchUsers(userIDs []string) error {
    for _, id := range userIDs {
        resp, err := client.Get(fmt.Sprintf("https://api.example.com/user/%s", id))
        if err != nil {
            return err
        }
        defer resp.Body.Close()
        // Process response
    }
    return nil
}
Результаты теста производительности: использование пула соединений снизило время запроса с 340 мс до 12 мс под нагрузкой (1000 запросов).

Уровень 3: Сетевой уровень

Маршрутизация IP-адресов, маски подсети, разрешение DNS. Если ваш сервис не может связаться с другим сервисом, именно здесь следует искать причину.

# Trace the actual route packets take
traceroute api.internal.company.com

# Check if DNS is lying to you
nslookup api.internal.company.com
История из практики: соединения с базой данных периодически прерывались из-за таймаута. База данных работала исправно. Приложение работало исправно. Сетевая команда настаивала, что всё в порядке.
# This revealed the truth
mtr --report --report-cycles 100 db.internal.company.com

# Packet loss at hop 4: 23%

Неправильно настроенный маршрутизатор между двумя дата-центрами терял пакеты. Приложение считало, что проблема в базе данных, и продолжало попытки подключения, что только усугубляло ситуацию.

Уровень 2: Канальный уровень

MAC-адреса, коммутаторы, VLAN. Отладка здесь, вероятно, будет нечастой, но когда она всё-таки потребуется, всё остальное перестанет иметь значение.

В облачных средах это проявляется как «перегрузка сетевого интерфейса». Для экземпляра EC2 существует жесткое ограничение на количество пакетов в секунду, зависящее от типа экземпляра.

# Check interface stats
ip -s link show eth0

# Look for TX/RX errors or drops

Процесс отладки, который действительно работает

Перестаньте гадать. Начните мыслить уровнями.

1. Application logs clean?        -> Go down one layer
2. TCP connections normal?         -> Go down one layer  
3. Can you ping the destination?   -> Go down one layer
4. Is the cable plugged in?        -> Go outside and touch grass

Реальная сессия отладки:

# Layer 7: Application
curl https://api.example.com/health
# Returns 503

# Layer 4: Transport
telnet api.example.com 443
# Connection refused

# Layer 3: Network
ping api.example.com
# Host unreachable

# Layer 2: (Skipped in cloud)
# Resolution: DNS was returning wrong IP
nslookup api.example.com

# Pointed to decommissioned server

Паттерн, который вы увидите повсюду.

После того, как вы поймете, что такое уровни, вы начнете замечать их повсюду:

Сетевое взаимодействие в Kubernetes:

  • Взаимодействие между модулями: уровень 3 (IP)
  • Сервис для пода: Уровень 4 (TCP/UDP + порты)
  • Входящий трафик: Уровень 7 (маршрутизация HTTP)

Проблемы с балансировщиком нагрузки:

  • Connection refused: Уровень 4 (неверный порт/протокол)
  • 502 Bad Gateway: Layer 7 (upstream timeout)
  • Connection timeout: Уровень 3 (маршрутизация/брандмауэр)

Что изменилось для меня

Я перестал говорить «сеть не работает» и начал спрашивать «на каком уровне произошла поломка?». Я перестал отлаживать систему интуитивно и начал отлаживать методом исключения.

Та ошибка 502, что была с самого начала? У балансировщика нагрузки (уровень 7) был keepalive 60 секунд. У сервера приложений (уровень 4) — 65 секунд. Когда балансировщик нагрузки закрывал соединение, приложение всё ещё считало, что оно открыто. Следующий запрос попадал в неактивный сокет.

// The fix
server := &http.Server{
    Addr:              ":8080",
    Handler:           router,
    ReadTimeout:       15 * time.Second,
    WriteTimeout:      15 * time.Second,
    IdleTimeout:       50 * time.Second, // Less than LB's 60s
    ReadHeaderTimeout: 5 * time.Second,
}

Семь уровней предназначены не для запоминания расположения SMTP. Они о том, чтобы знать, куда смотреть, когда всё ломается. А всё всегда ломается.

Как только вы научитесь видеть сквозь слои, отладка перестанет быть археологией и превратится в инженерное дело.

На этом все! Спасибо за внимание! Если статья была интересна, подпишитесь на телеграм-канал usr_bin, где будет еще больше полезной информации.