Skip to content

Практическая работа №19: Устранение блокирующего ввода-вывода

Цель работы: научиться находить синхронные блокирующие операции в обработчике запроса и устранять их, подтверждая результат нагрузочным тестированием.

Основные теоретические положения

Section titled “Основные теоретические положения”

1. Event loop и единственный поток Node.js

Section titled “1. Event loop и единственный поток Node.js”

Node.js обрабатывает запросы в одном потоке через событийный цикл (event loop). Пока выполняется синхронный код — в том числе синхронные операции ввода-вывода — поток занят целиком и не может обслуживать никакие другие запросы, даже если они пришли одновременно. Это принципиально отличает модель Node.js от воркер-пулов (PHP-FPM, Python WSGI), где каждый запрос обычно получает отдельный процесс/поток.

2. Блокирующие и неблокирующие версии API

Section titled “2. Блокирующие и неблокирующие версии API”

Многие модули Node.js (в частности, fs) предоставляют два варианта одной операции: синхронный (fs.readFileSync) и асинхронный (fs.promises.readFile или колбэк-версия). Синхронная версия блокирует event loop на всё время выполнения операции; асинхронная — освобождает поток на время ожидания и не мешает обработке остальных запросов.

3. Кэширование неизменяемых данных в памяти

Section titled “3. Кэширование неизменяемых данных в памяти”

Если данные, читаемые с диска (например, HTML-шаблон), не меняются между запросами, нет смысла читать их заново при каждом запросе — ни синхронно, ни асинхронно. Правильное решение — прочитать их один раз при старте процесса и хранить в памяти, обращаясь к hot path только за самими данными.

4. Нагрузочное тестирование и метрики RPS/latency

Section titled “4. Нагрузочное тестирование и метрики RPS/latency”

RPS (Requests Per Second) — число запросов, которое сервис успевает обработать в секунду при заданном уровне конкурентности (числе одновременных соединений). Latency (в частности p99 — 99-й перцентиль) показывает задержку для подавляющего большинства запросов. Инструменты вроде autocannon позволяют нагрузить сервис заданным числом соединений в течение фиксированного времени и получить обе метрики.

Алгоритм выполнения работы

Section titled “Алгоритм выполнения работы”

Получение исходного кода проекта

Section titled “Получение исходного кода проекта”
  1. Клонирование с GitHub:
    Terminal window
    git clone https://github.com/ghostITwe/optimization-web-apps.git
    cd optimize-web-apps/lab19-blocking-io

Установите зависимости:

Terminal window
npm install

1. Изучение и анализ текущей реализации

Section titled “1. Изучение и анализ текущей реализации”
  1. Откройте starter/server.js — обратите внимание на вызов fs.readFileSync внутри обработчика запроса /report/:id.
  2. Запустите стартовый сервер:
    Terminal window
    node starter/server.js
  3. В другом терминале проведите нагрузочный тест:
    Terminal window
    npx autocannon -c 20 -d 10 http://127.0.0.1:3000/report/1
  4. Зафиксируйте текущий показатель RPS (среднее число запросов в секунду) и p99 latency.

2. Устранение блокирующего чтения

Section titled “2. Устранение блокирующего чтения”
  1. В starter/server.js вынесите чтение файла шаблона (fs.readFileSync) из обработчика запроса — прочитайте его один раз при старте процесса и сохраните в переменной модуля.
  2. Убедитесь, что обработчик запроса теперь использует только эту переменную, не обращаясь к диску повторно.
  3. Сохраните изменения.

3. Повторное нагрузочное тестирование

Section titled “3. Повторное нагрузочное тестирование”
  1. Перезапустите сервер и повторите нагрузочный тест той же командой autocannon.
  2. Сравните новые показатели RPS и p99 latency с результатами из пункта 1.
  3. Запустите автоматическое сравнение:
    Terminal window
    npm test
    Скрипт tests/check-throughput.js поднимет по очереди starter и solution версии и напечатает сравнение RPS/latency между ними.

Подготовьте краткий отчёт с указанием:

  • RPS и p99 latency “до” и “после” изменений;
  • объяснением, почему именно fs.readFileSync ограничивал пропускную способность всего сервиса, а не только одного запроса;
  • ответом на вопрос, что изменилось бы, если бы шаблон менялся динамически на каждый запрос (можно ли было бы просто убрать чтение полностью).
  1. Почему fs.readFileSync в обработчике одного запроса замедляет обработку вообще всех запросов к серверу, а не только текущего?
  2. Чем fs.promises.readFile отличается от fs.readFileSync с точки зрения влияния на event loop?
  3. Почему в данном случае оптимальным решением стало не асинхронное чтение на каждый запрос, а полный отказ от чтения в hot path?
  4. Что показывает p99 latency и почему для оценки качества сервиса он важнее среднего значения?
  5. В модели PHP-FPM (пул воркеров, а не единый поток) — насколько критичной была бы та же проблема с синхронным чтением файла по сравнению с Node.js?