Практическая работа №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 “Получение исходного кода проекта”- Клонирование с GitHub:
Terminal window git clone https://github.com/ghostITwe/optimization-web-apps.gitcd optimize-web-apps/lab19-blocking-io
Установите зависимости:
npm install1. Изучение и анализ текущей реализации
Section titled “1. Изучение и анализ текущей реализации”- Откройте
starter/server.js— обратите внимание на вызовfs.readFileSyncвнутри обработчика запроса/report/:id. - Запустите стартовый сервер:
Terminal window node starter/server.js - В другом терминале проведите нагрузочный тест:
Terminal window npx autocannon -c 20 -d 10 http://127.0.0.1:3000/report/1 - Зафиксируйте текущий показатель RPS (среднее число запросов в секунду) и p99 latency.
2. Устранение блокирующего чтения
Section titled “2. Устранение блокирующего чтения”- В
starter/server.jsвынесите чтение файла шаблона (fs.readFileSync) из обработчика запроса — прочитайте его один раз при старте процесса и сохраните в переменной модуля. - Убедитесь, что обработчик запроса теперь использует только эту переменную, не обращаясь к диску повторно.
- Сохраните изменения.
3. Повторное нагрузочное тестирование
Section titled “3. Повторное нагрузочное тестирование”- Перезапустите сервер и повторите нагрузочный тест той же командой
autocannon. - Сравните новые показатели RPS и p99 latency с результатами из пункта 1.
- Запустите автоматическое сравнение:
Скрипт
Terminal window npm testtests/check-throughput.jsподнимет по очередиstarterиsolutionверсии и напечатает сравнение RPS/latency между ними.
4. Финальный отчёт
Section titled “4. Финальный отчёт”Подготовьте краткий отчёт с указанием:
- RPS и p99 latency “до” и “после” изменений;
- объяснением, почему именно
fs.readFileSyncограничивал пропускную способность всего сервиса, а не только одного запроса; - ответом на вопрос, что изменилось бы, если бы шаблон менялся динамически на каждый запрос (можно ли было бы просто убрать чтение полностью).
Контрольные вопросы
Section titled “Контрольные вопросы”- Почему
fs.readFileSyncв обработчике одного запроса замедляет обработку вообще всех запросов к серверу, а не только текущего? - Чем
fs.promises.readFileотличается отfs.readFileSyncс точки зрения влияния на event loop? - Почему в данном случае оптимальным решением стало не асинхронное чтение на каждый запрос, а полный отказ от чтения в hot path?
- Что показывает p99 latency и почему для оценки качества сервиса он важнее среднего значения?
- В модели PHP-FPM (пул воркеров, а не единый поток) — насколько критичной была бы та же проблема с синхронным чтением файла по сравнению с Node.js?