Как один игровой ПК в гостиной тянет For You feed на 72 тысячи юзеров Bluesky — перевод

72 тыс. пользователей Bluesky получают For You feed с игрового ПК в гостиной его автора. spacecowboy держит ленту в одиночку: Go-бинарь, SQLite на 419 ГБ, VPS за $7 — $30 в месяц вместо $245.

Обложка: Как один игровой ПК в гостиной тянет For You feed на 72 тысячи юзеров Bluesky — перевод

72 тысячи пользователей Bluesky получают For You feed с игрового ПК, который стоит в гостиной у его автора. Разработчик под ником spacecowboy рассказал, как он в одиночку держит ленту рекомендаций For You для 72 тыс. пользователей Bluesky. Один Go-бинарь, SQLite на 419 ГБ, VPS за $7 в качестве прокси и домашняя батарея Ecoflow на случай отключений. Полный бюджет — $30 в месяц вместо $245, которые стоил бы аналогичный сервер в аренде.

Текст ниже — перевод гостевой публикации spacecowboy в блоге atproto.com, обзор которой сделал Саймон Уиллисон. Логика ленты, кстати, предельно простая: «сервис ищет людей, которые лайкнули те же посты, что и вы, и показывает вам, что ещё они недавно лайкнули».

Ключевые выводы

Вся инфраструктура For You feed — один Go-процесс на домашнем игровом ПК (AMD 9950X3D, 96 ГБ DDR5, два NVMe по 2 ТБ), прокси на VPS за $7 в месяц и Tailscale между ними.

Хранилище — SQLite на 419 ГБ (90 дней лайков) плюс отдельная БД на 200 ГБ для логов, которые выгружаются в parquet и анализируются через DuckDB.

Нагрузка — 15-25 QPS, 72 тысячи уникальных пользователей в день. CPU загружен только на 37%, в запасе ещё 3× по нагрузке и 10× по «дешёвому» алгоритму — теоретически хватит на всех активных юзеров Bluesky.

Итоговый счёт — $30 в месяц: $20 электричество, $7 VPS OVH, $3 два домена. Аренда аналогичной машины стоила бы $245 в месяц.

Один Go-бинарь делает всё

For You — это один Go-бинарь, который одновременно делает три дела:

  • читает firehose Bluesky (Jetstream) — все посты, лайки и репосты — и складывает их в SQLite
  • отдаёт саму ленту For You
  • отдаёт playground-страницу, где можно посмотреть, какие рекомендации ты получаешь

Один процесс проще менеджить: нет распределённой системы, которую надо мониторить, перезапускать и дебажить по частям. Вся память общая, вся логика в одном месте, при сбое падает всё и поднимается всё — не бывает частичных состояний.

Железо: игровой ПК в гостиной

Feed запущен дома в гостиной — на игровом ПК, подключённом к телевизору. Спецификации автор описывает так:

  • CPU: AMD 9950X3D — 16 ядер с увеличенным L3-кэшем. Апгрейд с 12-ядерного 7900 в декабре 2025 года. По словам spacecowboy, 3D-кэш даёт прирост около 25% на CCD, где он установлен
  • RAM: 96 ГБ DDR5 на 6000 MT/s. Апгрейд с 32 ГБ в июне 2025-го — успел до скачка цен
  • Хранилище: 2 ТБ NVMe под базу данных и ещё 2 ТБ NVMe под систему
  • Резервное питание: ПК, интернет-модем и роутер подключены к Ecoflow Delta 3, которая даёт 4-5 часов работы при отключении электричества

Апгрейд процессора в декабре 2025-го, по оценке автора, увеличил ёмкость сервиса на 50-100% — feed был недоступен всего пару часов, пока устанавливалось новое железо.

SQLite как единственное хранилище

Все данные лежат в SQLite. Ключевое преимущество для автора — тестируемость: можно создать базу в памяти для юнит-тестов, не мокать и не фейкить слой хранения, а сетап и тирдаун занимают 0 мс.

Запросы пишутся через sqlc.dev — инструмент, который даёт полный контроль над SQL и генерирует Go-код вокруг него.

Чтобы экономить память (и на диске, и в кэшах), каждому AT URI поста присваивается целочисленный id. То же самое для пользователей — в таблице raters они хранятся с маленьким integer id. Таблица ratings связывает их: id, item_id, rater_id, timestamp. Сырые did:plc- и at:// строки занимают десятки байт каждая, а integer id — 4-8 байт.

База открыта с 5 соединениями для конкурентного чтения. Чтобы не ловить классическую ошибку «database is busy» на записи, spacecowboy запрещает параллельные записи через Go mutex — одна пишущая горутина на все. В интернете обычно советуют db.SetMaxOpenConns(1), но такая настройка сериализует и чтения тоже — что автору не подходит.

Чтобы не раздувать размер базы, хранятся только последние 90 дней данных. Каждые 24 часа запускается горутина-клинап: удаляет все ratings старше 90 дней, потом items, на которые ни одна rating уже не ссылается. Файл базы всё равно весит 419 ГБ. Команду VACUUM автор не запускает — она шла бы пару часов и на это время положила бы feed.

Второй SQLite для логов — и DuckDB для аналитики

Есть ещё одна база — SQLite на 200 ГБ с логами ответов: какие посты были возвращены какому пользователю. Когда юзер лайкает пост, который был показан ему через For You, spacecowboy замечает это в firehose и апдейтит соответствующую запись в логе. То же с реакциями «show more» и «show less».

Периодически логи выгружаются в parquet-файлы — по одному на день, примерно 2,6 ГБ каждый. Для запросов к этим файлам автор использует DuckDB: можно строить графики, считать метрики, прогонять A/B-тесты.

Прокручивал этот тест больше месяца, сегодня закончил. Статистически значимый результат: юзеры на 2,6% реже нажимают «show less like this». 5,7% больше реакций «show more» (41 425 → 43 795, +2370). 5,2% меньше «show less» (169 848 → 160 938, −8910).
spacecowboyавтор и единственный разработчик For You feed

In-process кэши вместо Redis

Рекомендательный движок сильно опирается на кэши в оперативке — конкретно на hashicorp/golang-lru. Отдельный Redis не нужен: и писатели, и читатели живут в одном процессе, кэш физически общий между ними. Нет межпроцессной коммуникации, нет сериализации и десериализации — отсюда скорость.

По словам автора, почти 100% данных, нужных для генерации рекомендаций, берётся из кэша, а не из БД. Обратная сторона — при каждом рестарте feed какое-то время очень медленный, пока кэши не прогреются.

Публичный доступ через VPS и Tailscale

Локальный процесс слушает http://localhost:8090, и снаружи к нему не подключиться. Чтобы быть публично видимым, spacecowboy арендует маленький VPS на OVH за $7 в месяц.

На VPS стоит Nginx, который принимает запросы на /xrpc/app.bsky.feed.getFeedSkeleton и /xrpc/app.bsky.feed.sendInteractions и проксирует их в маленький Go-процесс под названием «dispatch». У него три задачи:

  • Валидирует JWT-токены. Для проверки подписи надо сходить за DID-документом пользователя, а адрес его сервера указывает сам клиент. spacecowboy не хочет, чтобы такие запросы светили его домашний IP — если подсунуть свой DID, хост узнал бы, где он живёт
  • Маршрутизирует запросы между лентами. У автора их несколько — For You на :8090 и Videos For You на :8093. Внешний эндпоинт общий, порт определяется на dispatch
  • Отдаёт пост-заглушку во время обслуживания. Если домашний ПК временно выключен, клиенты Bluesky не получают ошибку — видят заранее подготовленный пост-объявление

VPS и домашний ПК находятся в одной сети Tailscale, поэтому dispatch обращается к домашнему ПК по внутренним хостам вида http://gaming:8090 для For You или http://gaming:8093 для Videos For You. Для внешнего мира IP домашнего ПК скрыт, трафик идёт через шифрованный Tailscale-туннель. Все сервисы на VPS подняты через docker-compose.

Нагрузка и запас прочности

Каждый день feed хотя бы раз открывают 72 тысячи уникальных пользователей. В среднем один пользователь делает 22 запроса в день. Трафик колеблется от 15 до 25 QPS. При 25 QPS CPU загружен на 37% (12 из 32 потоков) — то есть даже при нынешнем железе запас по CPU ещё в 3 раза.

Процесс съедает около 50 ГБ RAM, и 99% из них — те самые кэши.

Если трафик вдруг вырастет больше чем в 3 раза, feed заметит, что запросы начали обрабатываться медленно, и автоматически переключится на облегчённый набор параметров алгоритма — он дешевле в вычислениях примерно в 10 раз при минимальной потере качества. По замерам автора (сделанным ещё на прежнем 12-ядерном 7900), с этими параметрами загрузка CPU падает с 22/24 до 7/24 — то есть с почти полной до трети. В сумме получается 30×-й запас по нагрузке: 72 000 × 30 = 2,1 миллиона пользователей. Для сравнения, по данным независимого дашборда bsky.jazco.dev, суточное число уникальных лайкеров (лучший доступный прокси для активных юзеров Bluesky) стабильно держится в районе 1 миллиона. То есть текущая домашняя конфигурация теоретически может обслуживать всех активных пользователей сети целиком.

Мониторинг и простои

После нескольких незамеченных вовремя сбоев автор подключил hetrixtools.com — бесплатный uptime-мониторинг. Каждую минуту он ходит на https://foryou.club/playground, и если страница недоступна больше 5 минут, отправляет письмо, звонит на телефон и шлёт SMS. Uptime с января — 99,77%.

Итоговый счёт — $30 в месяц

Ежемесячные затраты spacecowboy раскладывает так:

  • около $20 за электричество (~200 Вт в режиме 24/7)
  • около $7 за VPS
  • около $3 за два домена

Аренда аналогичного выделенного сервера стоила бы $245 в месяц. Но, как пишет автор, «какой тогда в этом кайф». For You он держит как хобби-проект без монетизации: предложения сброситься на инфраструктуру отклоняет и предлагает помогать Graze, SkyFeed и другим сервисам-лентам, которым деньги действительно нужны.

72 тыс. пользователей, $30 в месяц, игровой ПК в гостиной — и при этом запас по нагрузке ещё в 30 раз. Для read-heavy сервисов с понятными паттернами доступа один Go-бинарь и SQLite с in-memory-кэшами — рабочая архитектура даже на десятках тысяч юзеров. Вопрос не «как это держится на SQLite», вопрос — почему в большинстве pet-проектов вместо этого сразу три пода в Kubernetes, Redis, Postgres и ежемесячный счёт на сотни долларов за сотню пользователей. Главное ограничение тут — не железо, а готовность владельца самому чинить сервер, когда он упадёт в три часа ночи. Подробнее про SQLite как очередь и pub/sub без Redis — в свежем разборе honker, а сравнение PostgreSQL/ClickHouse/DuckDB для аналитики — здесь.

Часто задаваемые вопросы
1
Можно ли подписаться на For You feed из России?

Сама лента For You открывается через веб-интерфейс Bluesky — регистрация в сети не требует российского или иностранного номера, но с российских IP часть фич Bluesky периодически работает нестабильно. Если с подключением возникают проблемы, обычно помогает смена сети.

2
Почему автор выбрал SQLite, а не PostgreSQL или MySQL?

Главный аргумент — простота и тестируемость. SQLite можно инициализировать прямо в памяти процесса для юнит-тестов, не поднимая отдельный сервер и не мокая слой хранения. Плюс single-process архитектура не требует отдельной СУБД — сам feed и его БД живут в одном бинаре.

3
Что такое Tailscale и зачем он тут нужен?

Tailscale — mesh-VPN поверх WireGuard, который поднимает шифрованную сеть между любыми устройствами владельца. В случае spacecowboy она нужна по двум причинам: VPS может достучаться до домашнего ПК без проброса портов и публичного IP, а внешний трафик из интернета никогда не видит реальный адрес дома.

4
Можно ли запустить свою feed-ленту для Bluesky?

Да, протокол atproto это позволяет. Любой может написать свой feed generator — процесс, который читает firehose и возвращает список постов в ответ на запрос от клиента Bluesky. Есть официальные гайды atproto, а открытые инструменты вроде SkyFeed или Graze дают визуальный редактор без написания кода.