Graceful shutdown, кэширование и мониторинг
Блок вопросов «а как вы это эксплуатируете»: деплой без потерянных запросов, кэш, логи и метрики. Здесь проверяют опыт, а не знание API.
Graceful shutdown — завершение процесса, при котором сервис перестаёт принимать новые запросы, доводит до конца уже начатые, закрывает соединения с базой и только после этого выходит с кодом 0.
Что проверяет интервьюер
Понимаете ли вы, что происходит при деплое. Оркестратор посылает контейнеру SIGTERM и ждёт (обычно 30 секунд), потом присылает SIGKILL. Если приложение не обрабатывает сигнал, оно умирает мгновенно — вместе с запросами, которые в этот момент были в работе. Пользователь видит 502, а вы — «мистические» ошибки ровно во время каждого релиза.
Корректное завершение
const server = app.listen(3000);
let shuttingDown = false;
// health-проба должна начать отвечать «не готов» ДО закрытия сервера
app.get('/readyz', (req, res) => {
if (shuttingDown) return res.status(503).json({ status: 'shutting_down' });
res.json({ status: 'ok' });
});
async function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
logger.info({ signal }, 'начинаем graceful shutdown');
// страховка: если за 15 с не уложились — выходим принудительно
const force = setTimeout(() => {
logger.error('shutdown timeout, выходим принудительно');
process.exit(1);
}, 15000).unref();
server.close(async () => { // перестали принимать новые соединения
try {
await queue.close(); // дорабатываем фоновые задачи
await db.end(); // закрываем пул соединений
await redis.quit();
clearTimeout(force);
logger.info('shutdown завершён');
process.exit(0);
} catch (err) {
logger.error({ err }, 'ошибка при завершении');
process.exit(1);
}
});
server.closeIdleConnections?.(); // keep-alive соединения не держат нас вечно
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
Три детали, которые ценятся отдельно. Первая: сначала переключаем readiness-пробу в «не готов», чтобы балансировщик убрал инстанс из ротации, и только потом закрываем сервер — иначе часть запросов успеет прилететь в закрывающийся процесс. Вторая: keep-alive соединения не закрываются сами, и server.close() без closeIdleConnections может ждать очень долго. Третья: принудительный таймаут обязателен, иначе зависший запрос не даст процессу выйти вовсе.
Кэширование
Стандартный ответ — слоями, от дешёвого к дорогому: HTTP-кэш и CDN на краю, общий Redis для данных, разделяемых между инстансами, и небольшой кэш в памяти процесса для «горячих» справочников. У каждого слоя обязателен TTL — кэш без срока жизни неизбежно превращается в источник устаревших данных и в утечку памяти.
Два вопроса, которые задают следом. Инвалидация: надёжнее всего короткий TTL плюс явное удаление ключа при записи; сложные схемы инвалидации по зависимостям ломаются первыми. Cache stampede — когда популярный ключ протухает и сотня запросов одновременно идёт в базу; лечится блокировкой на пересчёт (только один запрос идёт в базу, остальные ждут) или фоновым обновлением до истечения срока.
Логирование и мониторинг
const pino = require('pino');
const { AsyncLocalStorage } = require('node:async_hooks');
const als = new AsyncLocalStorage();
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
redact: ['req.headers.authorization', 'password', '*.token'], // секреты не логируем
});
// сквозной идентификатор запроса без передачи его через все функции
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] || crypto.randomUUID();
als.run({ requestId }, next);
});
function log(obj, msg) {
logger.info({ ...obj, ...als.getStore() }, msg);
}
Логи в проде — структурный JSON в stdout, а не отформатированные строки: их читает не человек, а система сбора. Сквозной requestId позволяет собрать все записи одного запроса, а AsyncLocalStorage хранит контекст через всю цепочку асинхронных вызовов, не протаскивая его аргументом.
Из метрик минимально нужны RED (Rate, Errors, Duration) по эндпоинтам и — специфичная для Node вещь — event loop lag. Растущий lag прямо означает, что цикл занят синхронной работой и все запросы начинают тормозить, даже если CPU далёк от 100%. Ещё смотрят на RSS/heapUsed, число открытых дескрипторов, размер очередей и время ответа зависимостей.
Масштабирование: короткие тезисы
- Stateless-процессы: состояние в Redis и базе, тогда добавление реплики — тривиальная операция.
- Тяжёлое — в очередь: отчёты, письма, обработка файлов уходят воркерам, HTTP отвечает быстро.
- Вертикально сначала: часто дешевле убрать N+1 запрос и добавить индекс, чем удваивать число подов.
- Ограничения на зависимости: таймауты, ретраи с экспоненциальной задержкой и circuit breaker, иначе один медленный сервис положит всю цепочку.
- Пулы соединений: 20 реплик по 20 соединений — это 400 подключений к базе, и она этого может не пережить.
Типичные ошибки кандидатов
- Не знают, что деплой начинается с
SIGTERM, и не обрабатывают сигналы вообще. - Закрывают сервер, но не закрывают пул БД — процесс не завершается.
- Забывают принудительный таймаут выхода.
- Кэшируют без TTL и без ограничения размера.
- Логируют строками с интерполяцией и складывают в лог токены и пароли.
- Не знают про event loop lag как метрику здоровья Node-сервиса.
Как ответить кратко
По
SIGTERMсначала переключаю readiness-пробу в «не готов», чтобы балансировщик убрал инстанс из ротации, потом закрываю сервер, дорабатываю активные запросы, закрываю пул БД, Redis и очереди — и всё это под принудительным таймаутом, иначе процесс может не выйти. Кэш строю слоями с обязательным TTL и защитой от stampede. Логи — структурный JSON в stdout со сквознымrequestIdчерезAsyncLocalStorageи с маскированием секретов. Из метрик обязательно RED по эндпоинтам плюс event loop lag: это главный ранний признак того, что Node-сервису плохо.