В этой статье я расскажу, как построить production-ready сервис коротких ссылок на чистом PHP с базой SQLite. Без фреймворков, без микросервисов, без Kubernetes. Мы запустили Сократи.Онлайн именно так - и я поделюсь архитектурными решениями, которые сработали, и граблями, на которые мы наступили.

Почему PHP без фреймворка

Для MVP фреймворк - это оверхед. Laravel тянет за собой десятки зависимостей, Symfony требует времени на конфигурацию. Когда нужно запустить сервис за месяц, а не за полгода, чистый PHP - лучший выбор.

Плюсы:

  • Мгновенная загрузка страниц (нет автозагрузки классов фреймворка)

  • Полный контроль над каждым запросом

  • Минимальное потребление памяти (важно на VDS с 1 ГБ RAM)

Минусы:

  • Всю безопасность нужно писать руками

  • Нет ORM - только чистые SQL-запросы

  • Нет миграций - схему БД обновляешь скриптами

Минусы решаются дисциплиной. Расскажу как.

Почему SQLite, а не PostgreSQL

Да, SQLite - нестандартный выбор для веб-сервиса. Но для сервиса коротких ссылок на старте он идеален:

  1. Ноль настройки. База данных - это один файл database.db. Не нужно устанавливать и конфигурировать PostgreSQL, создавать пользователей, настраивать доступ.

  2. Бэкап - копирование файла. Раз в день cron-скрипт копирует файл базы в облако. Восстановление - скопировать обратно.

  3. Достаточная производительность. SQLite обрабатывает до 1000 конкурентных чтений в секунду. Для сервиса на старте с трафиком до 10 000 переходов в день - более чем достаточно.

  4. Простая миграция. Когда нагрузки вырастут, можно переехать на PostgreSQL, сохранив ту же схему таблиц.

Важный нюанс: SQLite блокирует всю базу при записи. Поэтому мы оптимизировали запись статистики переходов - собираем данные в оперативной памяти и сбрасываем в базу пачками раз в 10 секунд.

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

Сервис состоит из трёх ключевых компонентов:

1. Создание короткой ссылки (index.php)

Пользователь вводит длинный URL, опционально - короткий код, UTM-метки и ID пикселей. Сервис:

  • Проверяет, не занят ли короткий код

  • Если код не указан - генерирует случайный из 3 символов (латиница + цифры = 36³ = 46 656 комбинаций)

  • Сохраняет в таблицу link: source, destination, user_id, domain, utm-метки, пиксели

function generateRandomCode($length = 3) {
    $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
    $result = '';
    for ($i = 0; $i < $length; $i++) {
        $result .= $characters[rand(0, strlen($characters) - 1)];
    }
    return $result;
}

// Проверка уникальности
do {
    $source = generateRandomCode(3);
    $stmt = $db->prepare("SELECT id FROM link WHERE source = :source");
    $stmt->bindValue(':source', $source, SQLITE3_TEXT);
    $exists = $stmt->execute()->fetchArray();
} while ($exists);

Почему 3 символа, а не 6-8 как у bit.ly? Потому что мы используем брендированные поддомены. На одном поддомене 46 тысяч комбинаций хватит надолго. Когда доменов станет много, можно увеличить длину до 4-5 символов.

2. Редирект и сбор статистики (link.php)

Это самая нагруженная часть. Когда пользователь переходит по короткой ссылке:

  1. Ищем ссылку в базе по коду и домену

  2. Если не нашли - 404

  3. Если нашли - собираем данные о переходе (IP, User-Agent, гео, время)

  4. Вставляем трекинг-пиксели (VK, Яндекс.Метрика)

  5. Делаем 301-редирект на целевой URL

Ключевой момент - скорость. Мы оптимизировали редирект до 0.3 секунды:

  • Индекс на поле source в таблице link

  • Кэширование самых популярных ссылок в файл (top-100 по переходам)

  • Асинхронная запись статистики

$stat_data = [
    'source' => $link_id,
    'ip' => $_SERVER['REMOTE_ADDR'],
    'browser' => $browser,
    'os' => $os,
    'time' => date('Y-m-d H:i:s'),
    // ...
];

file_put_contents(
    'data/stats_buffer.txt', 
    json_encode($stat_data) . "\n", 
    FILE_APPEND
);

3. Личный кабинет (cabinet.php)

Здесь пользователь видит свои ссылки, статистику, управляет доменами. Основные функции:

  • Список ссылок с пагинацией

  • Детальная статистика по каждой ссылке (переходы по дням, часам, гео, устройствам)

  • Редактирование целевого URL без изменения короткого кода

  • Экспорт в CSV

  • Управление брендированными поддоменами через API REG.RU

Подводные камни

Камень 1: Кэш браузера

Пользователи часто жалуются: «Я изменил целевую страницу, а ссылка ведёт на старую». Проблема не в сервисе, а в кэше браузера — тот запомнил 301-редирект. Решение: используем 302 (временный) редирект для ссылок, которые редактировались за последние 24 часа. Для остальных - 301.

if ($link['edit_count'] > 0 && strtotime($link['last_edit']) > time() - 86400) {
    header("Location: " . $destination, true, 302);
} else {
    header("Location: " . $destination, true, 301);
}

Камень 2: Боты и накрутка

Боты кликают по ссылкам, искажая статистику. Мы отсеиваем их по User-Agent:

$bot_patterns = ['bot', 'crawler', 'spider', 'scanner', 'curl', 'wget'];
$is_bot = false;
foreach ($bot_patterns as $pattern) {
    if (stripos($user_agent, $pattern) !== false) {
        $is_bot = true;
        break;
    }
}

Ботам отдаём редирект без записи в статистику. Заодно экономим место в базе.

Камень 3: Конкурентная запись в SQLite

SQLite блокирует файл базы при записи. Если одновременно 100 пользователей переходят по ссылкам, возникают задержки. Решение - буферизация статистики:

  1. Каждый переход пишется не в БД, а в текстовый файл stats_buffer.txt

  2. Cron-скрипт раз в минуту читает файл, сбрасывает данные в БД и очищает буфер

  3. Для чтения статистики используем отдельное подключение к БД в режиме WAL (Write-Ahead Logging)

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;

Режим WAL позволяет одновременно читать и писать - то, что нужно для сервиса коротких ссылок.

Метрики и производительность

После месяца работы на VDS с 1 ГБ RAM и 1 ядром CPU:

  • Аптайм: 99.9%

  • Среднее время редиректа: 0.28 секунды

  • Пиковая нагрузка: 200 одновременных переходов без задержек

  • Размер базы: 15 МБ (50 000 ссылок, 500 000 записей статистики)

  • Потребление RAM: 120 МБ (PHP-FPM + SQLite)

В планах - переход на PostgreSQL при достижении 1 млн ссылок и интеграция Redis для кэширования популярных URL.

Выводы

Запустить сервис коротких ссылок на PHP + SQLite - реально. Это быстрее и дешевле, чем строить микросервисную архитектуру с Kubernetes. Главное — понимать ограничения SQLite и закладывать пути отступления (миграция на PostgreSQL, кэширование).

P.S. Если интересно посмотреть на результат — сервис называется Сократи.Онлайн. Там как раз реализовано всё, о чём я рассказал: от брендированных поддоменов до трекинг-пикселей.