Финансовый регулятор
API сравнения банковских продуктов
- Год
- 2023
- Роль
- Единственный бэкенд-инженер
- Результаты
- p95 82 мс на shared-хостинге
Контекст
Национальный банк Таджикистана публикует условия вкладов всех банков страны. До этого сравнение собирал аналитик руками: открывал четырнадцать сайтов, выписывал ставки в таблицу и раз в неделю обновлял PDF. Бизнес-задача была не «сделать API», а снять с человека еженедельную ручную работу и дать банкам единый контракт вместо четырнадцати разных выгрузок.
Ограничения
- Shared-хостинг без доступа к конфигурации PHP и без долгоживущих процессов
- PHP 7.4 без фреймворка и без composer — весь код в репозитории
- Cron не чаще одного запуска в пятнадцать минут
- Источники данных — четырнадцать банков с разными форматами выгрузки
Решение
Ключевое решение — развести чтение и запись во времени. Пользовательский запрос никогда не ходит в банки: он читает заранее собранный снимок.
Файловый кэш здесь не оптимизация, а способ уложиться в отведённое: на shared-хостинге открытие соединения с MySQL стоит дороже, чем чтение файла, и именно это определило форму ответа.
function cache_get(string $key, int $ttl): ?array
{
$path = CACHE_DIR . '/' . hash('xxh3', $key) . '.json';
$stat = @stat($path);
if ($stat === false || (time() - $stat['mtime']) > $ttl) {
return null;
}
$raw = @file_get_contents($path);
return $raw === false ? null : json_decode($raw, true);
}Нормализацию я написал как четырнадцать отдельных парсеров с общим интерфейсом. Это честно отражало реальность, но каждое изменение формата у банка означало правку кода и релиз. Правильнее было бы описать разбор декларативно — таблицей соответствий полей — и хранить её рядом с данными.
Архитектура
Ключевые решения
Где держать собранный снимок
Что выбрал
Файловый кэш в JSON, инвалидация по mtime. MySQL — источник правды и запасной путь при промахе.
Что отверг
Redis или memcached: на shared-тарифе их нет и не будет, а просить — значит менять контур закупки.
Почему
Единственное, чем я управлял, — файловая система и код. Решение, которое требует нового сервиса, здесь не решение, а заявка на согласование. Файловый кэш дал те же 82 мс без единого письма.
Как обновлять данные без долгоживущих процессов
Что выбрал
Сборка по cron раз в пятнадцать минут, полностью отдельно от пользовательского запроса.
Что отверг
Обновление по требованию, когда кэш протух: первый невезучий пользователь ждал бы обхода четырнадцати банков.
Почему
Разведение чтения и записи во времени — единственный способ держать p95, когда источники отвечают за секунды. Свежесть данных при этом теряется максимум на пятнадцать минут, и для ставок по вкладам это ничто.
Что было сложно
Проблема
Четыре банка отдавали HTML вместо выгрузки, и вёрстка у них менялась без предупреждения.
Решение
Парсер для таких источников проверяет структуру перед разбором и при несовпадении не падает, а помечает источник устаревшим. Ответ отдаётся с прошлыми данными и флагом, а не пятисоткой.
Проблема
Хостинг убивал процесс дольше тридцати секунд, а полный обход всех банков в это не укладывался.
Решение
Сборщик обходит по четыре банка за запуск и запоминает позицию. Полный круг занимает час, что укладывается в требование по свежести.
Результаты
- p95
- 82
- запросов в секунду
- 140
- uptime за 12 месяцев
- 99,95