Кеширование на сервере

Роль кеширования в интернационализации

При использовании Globalize на серверной стороне ключевая стоимость формируется не только за счёт форматирования дат, чисел и сообщений, но и за счёт подготовки локализационных данных CLDR (Unicode Common Locale Data Repository), компиляции форматтеров и разборщиков, а также построения структур, ускоряющих повторные операции.

Серверное кеширование в таком контексте решает сразу несколько задач:

  • снижение времени инициализации локали;
  • уменьшение нагрузки на CPU при массовых запросах;
  • устранение повторной загрузки CLDR-данных;
  • стабилизация латентности при высоком RPS;
  • унификация результатов форматирования между инстансами приложения.

Что именно подлежит кешированию

В Globalize можно выделить несколько уровней данных, которые имеют смысл сохранять в памяти или внешних кешах.

CLDR-данные

CLDR представляет собой базовый слой локализации. Он включает:

  • правила форматирования чисел;
  • наборы календарей;
  • данные о валютах;
  • правила pluralization;
  • наборы символов и локализованных строк.

Загрузка CLDR — одна из самых дорогих операций при старте приложения. При серверной архитектуре с несколькими воркерами важно избегать повторного парсинга этих данных в каждом процессе.

Компилированные форматтеры

Globalize создаёт специализированные функции форматирования:

  • форматирование дат с учётом локали;
  • форматирование валют;
  • форматирование процентов;
  • правила plural forms.

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

Парсеры

Разбор строк обратно в числа или даты также требует предварительной подготовки. Парсеры для каждой локали включают:

  • шаблоны дат;
  • правила разделителей;
  • правила интерпретации чисел.

Сообщения (messages)

Если используется система сообщений Globalize, то интерполяция и plural rules могут быть заранее обработаны и сохранены в виде готовых функций.

Архитектура серверного кеша

Локальный in-memory кеш

Самый быстрый уровень кеширования — память процесса Node.js. Он подходит для:

  • форматтеров;
  • CLDR-объектов;
  • локальных инстансов Globalize по локали.

Ключевая идея — избегать повторной инициализации:

import Globalize from "globalize";
import cldrData from "./cldr-data";

const cache = new Map();

function getGlobalize(locale) {
  if (cache.has(locale)) {
    return cache.get(locale);
  }

  Globalize.load(cldrData);

  const instance = new Globalize(locale);
  cache.set(locale, instance);

  return instance;
}

Основная проблема такого подхода — дублирование памяти при масштабировании на несколько процессов.

Кеш на уровне процесса (singleton)

При использовании кластеризации Node.js важно разделять:

  • инициализацию CLDR (один раз на процесс);
  • локальные экземпляры Globalize (на каждую локаль).

Правильная стратегия:

  • CLDR грузится при старте воркера;
  • Globalize инстансы создаются лениво.

Redis как распределённый кеш

Redis используется для хранения:

  • сериализованных форматтеров;
  • precomputed message functions;
  • результатов сложных локализованных вычислений.

Однако Globalize-объекты нельзя напрямую сериализовать. Поэтому кешируются:

  • результаты форматирования;
  • промежуточные структуры (например, конфигурации локали).

Пример подхода:

async function formatCurrencyCached(redis, locale, value, currency) {
  const key = `currency:${locale}:${currency}:${value}`;

  const cached = await redis.get(key);
  if (cached) return cached;

  const g = getGlobalize(locale);
  const result = g.formatCurrency(value, currency);

  await redis.set(key, result, "EX", 60 * 10);

  return result;
}

CDN и edge-кеширование

На уровне CDN можно кешировать уже сформированные ответы API, особенно если:

  • локаль фиксирована в URL;
  • данные не меняются часто;
  • форматирование полностью детерминировано.

Пример сценария:

/api/products?locale=ru-RU

Ответ может кешироваться на edge-уровне, включая:

  • форматированные цены;
  • локализованные названия;
  • даты обновления.

Стоимость инициализации Globalize

Инициализация включает:

  • загрузку CLDR;
  • построение внутренних индексов;
  • компиляцию локализованных правил;
  • создание функций форматирования.

При большом количестве локалей стоимость растёт линейно по числу языков, но нелинейно по объёму CLDR.

Оптимизация:

  • загрузка только необходимых сегментов CLDR;
  • lazy-loading локалей;
  • разделение runtime и build-time инициализации.

Стратегия кеширования по локалям

Разделение по ключу locale

Базовый ключ кеша:

locale + feature + parameters

Примеры:

  • ru-RU:number:currency:USD
  • en-GB:date:long
  • fr-FR:plural:messages

Такое разделение позволяет:

  • минимизировать коллизии;
  • точно контролировать инвалидирование.

Изоляция локалей

Каждая локаль должна быть изолирована, чтобы избежать:

  • смешивания правил pluralization;
  • конфликтов календарей;
  • некорректного форматирования чисел.

Проблемы сериализации

Globalize-инстансы и форматтеры содержат:

  • функции;
  • замыкания;
  • внутренние структуры CLDR.

Они не сериализуются напрямую в JSON.

Решение:

  • кешировать только примитивные результаты;
  • или хранить конфигурации, а не экземпляры.

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

Основная сложность — обновление CLDR или правил локали.

Стратегии:

Версионирование CLDR

cacheKey = locale + cldrVersion

При обновлении CLDR старая версия автоматически игнорируется.

TTL-кеширование

Используется для:

  • валютных курсов;
  • сообщений;
  • форматированных ответов.

Явная инвалидция

При деплое:

  • очищается in-memory кеш;
  • обновляется Redis namespace.

Многопроцессная архитектура

В Node.js часто используется:

  • cluster;
  • PM2;
  • Docker replicas.

Проблема: каждый процесс содержит свой кеш.

Решения:

  • разделение read-heavy и compute-heavy операций;
  • вынос предрасчитанных значений в Redis;
  • прогрев кеша при старте.

Прогрев кеша

При запуске сервиса выполняется:

  • загрузка всех используемых локалей;
  • инициализация форматтеров;
  • предварительный рендер популярных значений.

Пример:

async function warmup(locales) {
  for (const locale of locales) {
    const g = getGlobalize(locale);

    g.formatNumber(1000);
    g.formatDate(new Date());
    g.formatCurrency(10, "USD");
  }
}

Производительность под нагрузкой

Основные метрики:

  • latency formatNumber;
  • latency formatDate;
  • memory per locale;
  • cache hit ratio.

Оптимизационные эффекты кеширования:

  • снижение CPU до 60–80% при высоком RPS;
  • уменьшение GC pressure;
  • стабилизация p95 latency.

Многоуровневая модель кеширования

Эффективная архитектура включает:

  1. L1 — in-memory Globalize instances
  2. L2 — Redis для готовых строк
  3. L3 — CDN для HTTP-ответов

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