Производительный NodeJS

Александр Завьялов, Евгений Мокеев

Пример

Просмотрщик домашних заданий

  • Навигация по домашним задачам
  • Должно быть быстро
  • Держать высокую нагрузку
Example

getTasks(category)


function getTasks(category) {
    return github.getRepos('urfu-2015')
        .then(tasks => {
            return filterTasks(tasks, category)
        })
        .then(getTasksInfo);
};
    

Пример. Главная страница


Promise
    .all([
        getTasks('javascript'),
        getTasks('verstka'),
        getTasks('webdev')
    ])
    .then(results => {
        res.render(...);
    });
    

Promise
    .all([
        getTasks('javascript'),
        getTasks('verstka'),
        getTasks('webdev')
    ]);
    

function getTasks(category) {
    return github.getRepos('urfu-2015')
        .then(...)
        .then(...);
};
    

Три одновременных пользователя


require('debug-http')();
    

GET https://api.github.com/orgs/urfu-2015/repos 891ms
GET https://api.github.com/orgs/urfu-2015/repos 889ms
GET https://api.github.com/orgs/urfu-2015/repos 902ms
GET https://api.github.com/orgs/urfu-2015/repos 933ms
GET https://api.github.com/orgs/urfu-2015/repos 953ms
GET https://api.github.com/orgs/urfu-2015/repos 1,003ms
GET https://api.github.com/orgs/urfu-2015/repos 1,018ms
GET https://api.github.com/orgs/urfu-2015/repos 1,067ms
GET https://api.github.com/orgs/urfu-2015/repos 1,072ms
    

Много одинаковых запросов

Объединение запросов

Объединение запросов

Over requests

Объединение запросов

Batching

Объединяем запросы за репозиториями

let reposQuery;
function getReposBatch() {
    if (!reposQuery) {
        reposQuery = github.getRepos('urfu-2015')
            .then(res => {
                reposQuery = null;
                return res;
            });
    }
    return reposQuery;
}

Объединяем запросы за репозиториями


function getTasks(category) {
    return getReposBatch()
        .then(tasks => {
            return filterTasks(tasks, category)
        })
        .then(getTasksInfo);
};
    

Объединение запросов

Уменьшили количество запросов

Быстрее отвечаем некоторым пользователям

Если запрос завершился ошибкой, то для всех

Кэширование

Кэширование

Данные ближе к месту использования

Хранение результатов вычислений

Оптимизация скорости получения данных

Кэширование. Решаемые проблемы

Низкая производительность

Избыточность запросов

Высокая нагрузка на внешний источник

Узкий сетевой канал

Высокие сетевые задержки

Кэш

  • Промежуточное хранилище c быстрым доступом
  • Ограничен по размеру
  • Наиболее часто запрашиваемые данные
  • Данные не всегда актуальные

Эффективность кэша

Количество попаданий

Hit Rate = Попадание в кэш / Количество запросов

Скорость получения данных

“Разогретый” кэш

Инвалидация кэша

Данные устарели (“протухли”) и их нужно убрать из кэша

Данные удаляются вручную

Данные заменяются новыми

Данные вытесняются автоматически по алгоритму

Одна из самых сложных задач в программировании

Алгортимы кэширования

Алгортим вытеснения из кэша

Time period - Вытеснение по времени

LFU - Вытеснение редко используемых

LRU - Вытеснение давно неиспользуемых

Segmented LRU - Многоуровневый LRU

От алгортима зависит быстродействие кэша

Когда не кэшировать

  • Большая вариация данных
  • Персонализированные данные
  • Есть другие оптимизации

Мемоизация

Сохранение результата выполнения функции


const cache = {};
function memoize(key, fn) {
    if (!cache.hasOwnProperty(key)) {
        cache[key] = fn();
    }
    return cache[key];
}
    

Кэшируем задачи


const LRU = require('lru-cache');
const cache = new LRU();
    
cache.set(key, value, maxAge);
cache.get(key);
cache.del(key);
cache.has(key);

class Cache {
    constructor() {
        this._cache = new LRU();
    }

    memoize(key, maxAge, fn) {
        // ...
    }
}
    
memoize(key, maxAge, fn) {
    const cache = this._cache;
    const value = cache.get(key);
    if (value) {
        return Promise.resolve(value);
    }
 
    return Promise.resolve()
        .then(fn)
        .then(results => {
            cache.set(key, result, maxAge * 1000);
            return result;
        });
}

function getTasks(category) {
    return getReposBatch()
        .then(tasks => {
            return filterTasks(tasks, category)
        })
        .then(getTasksInfo);
};
    
const cache = new Cache();
function getTasksCached(category) {
    const cacheKey = `tasks.${category}`;
 
    return cache.memoize(cacheKey, 5 * 60, () => {
        return getTasks(category);
    });
}

Результаты кэширования


GET https://api.github.com/orgs/urfu-2015/repos 990ms
GET / 200 1249.771 ms
GET / 200 1254.626 ms
GET / 200 1236.532 ms
GET / 200 11.003 ms
GET / 200 18.849 ms
GET / 200 25.639 ms
    

Еще раз о кэшировнии

  • Не решение всех проблем
  • Кэшировать только после всех оптимизаций
  • Кэш должен быть вспомогательной компонентой
  • Кэш должен легко включаться и отключаться
  • Все должно работать и без кэша
  • Кэш не постоянное хранилище!

Масштабирование

Масштабирование

Процесс увеличения производительности и отказоустойчивости системы

Распределение нагрузки между несколькими процессами и машинами

Масштабирование

Увеличивает доступность

Увеличивает производительность

Увеличивает отказоустойчивость

Увеличивает сложность

Масштабируемость

Сильная. Система “легко” масштабируется

Слабая. Очень тяжело масштабируется

Масштабируемость ~ Монолитность

Масштабирование

Вертикальное. Добавление мощностей

Горизонтальное. Разбиение приложения на несколько частей

Scale cube

scale cube

ось X: Клонирование приложения

ось Y: Декомпозиция приложения

ось Z: Разделение в зависимости от данных

The Art of Scalability. Martin L. Abbott and Michael T. Fisher

Клонирование NodeJS-приложения

  • Запуск в разных процессах
  • Запуск на разных машинах
  • Решить вопрос балансировки запросов

Cluster

cluster

Round-robin алгоритм балансировки

Cluster

const cluster = require('cluster');
const os = require('os');
 
if (cluster.isMaster) {
    const cpus = os.cpus().length;
    for (let i = 0; i < cpus; i++) {
        cluster.fork();
    }
} else {
    require('./app.js');
}

Cluster

Распараллелена нагрузка

При ошибке приложение не доступно


siege -t20S http://localhost:8080

Availability:              10.89 %
Successful transactions:   61
Failed transactions:       499
    

Cluster


if (cluster.isMaster) {
    // ...
    cluster.on('exit', (worker, code) => {
        if (code !== 0 && !worker.suicide) {
            console.log('Worker crashed');
            cluster.fork();
        }
    });
}
    

Availability:              87.10 %
Successful transactions:   243
Failed transactions:       36
    

Масштабирование

Высокая доступность

Отказоустойчивость

Изолированная память

cache. first request cache. second request
shared cache. first request shared cache. second request

Единый кэш

Shared cache

Единый кэш

Redis

Memcached

Redis

Хорошая документация

Данные в памяти

Транзакции

Пакетная обработка команд

Механизм pub/sub из коробки

Поддержка LRU алгортима

Встроенный мониторинг команд

Redis

Try redis

The Little Redis Book

Using Redis as an LRU cache

Установка Redis

Redis on Windows

Nuget

Chocolatey

Командная строка

redis-cli
telnet localhost 6379

Список команд

Работа с ключами

Установка значения:

SET tasks.javascript "[{ name: ... }]"

Проверка существования ключа:

EXISTS tasks.javascript

Получение ключа:

GET tasks.javascript

Удаление ключа:

DEL tasks.javascript

Временные ключи

Установка времени жизни ключа:

EXPIRE tasks.javascript 30

Получение времени жизни ключа:

TTL tasks.javascript

Установка значения вместе с временем жизни:

SETEX tasks.javascript 30 "[{ name: ... }]"

const Redis = require("ioredis");
class Cache {
    constructor() {
        this._cache = new Redis(6379, '127.0.0.1');
    }
}
    

memoize(key, maxAge, fn) {
    const cache = this._cache;
    const value = cache.get(key)
    if (value) {
        return Promise.resolve(value)
    }

    return Promise.resolve()
        .then(fn)
        .then(results => {
            cache.set(key, result, maxAge * 1000)
            return result;
        }
}
    
memoize(key, maxAge, fn) {
    const cache = this._cache;
    return cache.get(key)
        .then(value => {
            if (value) {
                return JSON.parse(value);
            }
 
            return Promise.resolve()
                .then(fn)
                .then(result => {
                    cache.setex(
                        key, maxAge,
                        JSON.stringify(result));
                    return result;
                });
        });
}

Итоги

Объединение запросов

Кэширование

Масштабирование

Всегда нужно отталкиваться от задачи

Вопросы?