Архитектура сервиса коротких ссылок на PHP + SQLite: подводные камни и решения
В этой статье я расскажу, как построить production-ready сервис коротких ссылок на чистом PHP с базой SQLite. Без фреймворков, без микросервисов, без Kubernetes. Мы запустили Сократи.Онлайн именно так - и я поделюсь архитектурными решениями, которые сработали, и граблями, на которые мы наступили.
Почему PHP без фреймворка
Для MVP фреймворк - это оверхед. Laravel тянет за собой десятки зависимостей, Symfony требует времени на конфигурацию. Когда нужно запустить сервис за месяц, а не за полгода, чистый PHP - лучший выбор.
Плюсы:
Мгновенная загрузка страниц (нет автозагрузки классов фреймворка)
Полный контроль над каждым запросом
Минимальное потребление памяти (важно на VDS с 1 ГБ RAM)
Минусы:
Всю безопасность нужно писать руками
Нет ORM - только чистые SQL-запросы
Нет миграций - схему БД обновляешь скриптами
Минусы решаются дисциплиной. Расскажу как.
Почему SQLite, а не PostgreSQL
Да, SQLite - нестандартный выбор для веб-сервиса. Но для сервиса коротких ссылок на старте он идеален:
Ноль настройки. База данных - это один файл
database.db. Не нужно устанавливать и конфигурировать PostgreSQL, создавать пользователей, настраивать доступ.Бэкап - копирование файла. Раз в день cron-скрипт копирует файл базы в облако. Восстановление - скопировать обратно.
Достаточная производительность. SQLite обрабатывает до 1000 конкурентных чтений в секунду. Для сервиса на старте с трафиком до 10 000 переходов в день - более чем достаточно.
Простая миграция. Когда нагрузки вырастут, можно переехать на 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)
Это самая нагруженная часть. Когда пользователь переходит по короткой ссылке:
Ищем ссылку в базе по коду и домену
Если не нашли - 404
Если нашли - собираем данные о переходе (IP, User-Agent, гео, время)
Вставляем трекинг-пиксели (VK, Яндекс.Метрика)
Делаем 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 пользователей переходят по ссылкам, возникают задержки. Решение - буферизация статистики:
Каждый переход пишется не в БД, а в текстовый файл
stats_buffer.txtCron-скрипт раз в минуту читает файл, сбрасывает данные в БД и очищает буфер
Для чтения статистики используем отдельное подключение к БД в режиме
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. Если интересно посмотреть на результат — сервис называется Сократи.Онлайн. Там как раз реализовано всё, о чём я рассказал: от брендированных поддоменов до трекинг-пикселей.