← Все кейсы

Финансовый регулятор

API сравнения банковских продуктов

Год
2023
Роль
Единственный бэкенд-инженер
Результаты
p95 82 мс на shared-хостинге

Контекст

Национальный банк Таджикистана публикует условия вкладов всех банков страны. До этого сравнение собирал аналитик руками: открывал четырнадцать сайтов, выписывал ставки в таблицу и раз в неделю обновлял PDF. Бизнес-задача была не «сделать API», а снять с человека еженедельную ручную работу и дать банкам единый контракт вместо четырнадцати разных выгрузок.

Ограничения

  • Shared-хостинг без доступа к конфигурации PHP и без долгоживущих процессов
  • PHP 7.4 без фреймворка и без composer — весь код в репозитории
  • Cron не чаще одного запуска в пятнадцать минут
  • Источники данных — четырнадцать банков с разными форматами выгрузки

Решение

Ключевое решение — развести чтение и запись во времени. Пользовательский запрос никогда не ходит в банки: он читает заранее собранный снимок.

Файловый кэш здесь не оптимизация, а способ уложиться в отведённое: на shared-хостинге открытие соединения с MySQL стоит дороже, чем чтение файла, и именно это определило форму ответа.

cache/file.php
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);
}

Нормализацию я написал как четырнадцать отдельных парсеров с общим интерфейсом. Это честно отражало реальность, но каждое изменение формата у банка означало правку кода и релиз. Правильнее было бы описать разбор декларативно — таблицей соответствий полей — и хранить её рядом с данными.

Архитектура

Два контура не пересекаются: медленный собирает, быстрый отдаёт. 14 банков → collector; collector → normalize; normalize → mysql; клиент → router; router → cache.file; cache.file → mysql (промах)промах14 банковcollectornormalizemysqlклиентroutercache.file
Два контура не пересекаются: медленный собирает, быстрый отдаёт

Ключевые решения

  • Где держать собранный снимок

    Что выбрал

    Файловый кэш в JSON, инвалидация по mtime. MySQL — источник правды и запасной путь при промахе.

    Что отверг

    Redis или memcached: на shared-тарифе их нет и не будет, а просить — значит менять контур закупки.

    Почему

    Единственное, чем я управлял, — файловая система и код. Решение, которое требует нового сервиса, здесь не решение, а заявка на согласование. Файловый кэш дал те же 82 мс без единого письма.

  • Как обновлять данные без долгоживущих процессов

    Что выбрал

    Сборка по cron раз в пятнадцать минут, полностью отдельно от пользовательского запроса.

    Что отверг

    Обновление по требованию, когда кэш протух: первый невезучий пользователь ждал бы обхода четырнадцати банков.

    Почему

    Разведение чтения и записи во времени — единственный способ держать p95, когда источники отвечают за секунды. Свежесть данных при этом теряется максимум на пятнадцать минут, и для ставок по вкладам это ничто.

Что было сложно

  • Проблема

    Четыре банка отдавали HTML вместо выгрузки, и вёрстка у них менялась без предупреждения.

    Решение

    Парсер для таких источников проверяет структуру перед разбором и при несовпадении не падает, а помечает источник устаревшим. Ответ отдаётся с прошлыми данными и флагом, а не пятисоткой.

  • Проблема

    Хостинг убивал процесс дольше тридцати секунд, а полный обход всех банков в это не укладывался.

    Решение

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

Результаты

p95
82
запросов в секунду
140
uptime за 12 месяцев
99,95

Стек