Порядок вывода легаси: почему VOG переносит игры по одной

Почему перезапуск VOG не стал «переписать всё с нуля»: откуда в проекте привычка к осторожности, что пришлось сделать в 2021 году, чтобы старый C++-сервер вообще можно было собрать и запустить, и как в 2026-м выбрали порядок — сначала общее хранилище для всех игр, потом по одной игре, на каждом шаге меняется что-то одно

В первой статье серии я обещал рассказать, в каком порядке отключать легаси-серверы, чтобы на каждом шаге менялось только что-то одно. В предыдущей была частная задача: вернуть старому серверу адреса игроков после переезда за HAProxy. Эта статья — про общий план: почему мы не стали переписывать VOG разом, а переносим игры по одной, и почему первым шагом был вообще не перенос игры.

Коротко для тех, кто не читал первую статью. VOG — клуб настольных игр онлайн, который работает с 1997 года: нарды, шахматы, шашки, реверси, червы и «Слова». Почти тридцать лет в них играли из Windows-клиента, который разговаривает с C++-сервером по собственному бинарному протоколу. В 2026 году рядом появились веб-клиент и новые службы на Go, и задача перезапуска — посадить оба клиента за одни столы, не потеряв ни игроков, ни их рейтинги и историю.

Чтобы объяснить порядок переезда, придётся начать издалека. Осторожность в VOG не взялась из учебника, она накопилась за двадцать с лишним лет.

Схема в стиле журнала «Полдень»: сортировочная станция, старый паровоз «C++» с вагонами-играми Shell, реверси, шашки, шахматы, нарды, червы и «Слова», через стрелку под сигналом «по одной» на путь электровоза «Go» переезжает вагон «Реверси», за электровозом уже стоят шашки и шахматы; под обоими путями проходит общий кабельный коллектор «общее хранилище», внизу журнал дежурного по станции от 31.08.2026

В статье

Откуда осторожность

После Windows-серверов первых лет VOG переехал на Solaris, причём на x86, а примерно к 2001 году — на Linux. Задача C10K, то есть как одному серверу держать десять тысяч одновременных соединений, была нам понятна не понаслышке.

Самым опасным моментом была не нагрузка как таковая, а возвращение игроков. Перезапустили сервер после обновления — и все разом переподключаются. Этот шторм переподключений гарантированно ронял сервер, хотя игроки сидели на модемах 14 400 бит/с и трафика каждый давал немного. Поэтому выкатка обновления была ритуалом: выложили, затем дежурство, пока не убедимся, что сервер пережил возвращение всех.

Отсюда и привычка: резкие движения не приветствовались. У неё была и обратная сторона. Ставка на Internet Explorer и ActiveX, о которой я писал в первой статье, сама по себе была разумной: установка в один клик, обновления прозрачно для игрока. Ошибкой стала не она, а то, что проект не смог с неё сойти, когда обстоятельства изменились. Команда к тому времени почти разошлась, а оставшиеся не могли переделать всё сами. Были мысли перейти на плагины для Firefox, но дальше мыслей дело не пошло.

Та же цена изменений была и у новых игр. Начинал VOG с четырёх: нарды, шашки, шахматы и реверси. После перехода на VOG2 игроков спрашивали в опросах, какие игры добавить, но каждая новая игра означала свой C++-сервер, свой модуль клиента и месяцы работы. Успели добавить только две — червы и «Слова».

Получается, осторожность VOG знает с двух сторон. Резкое движение роняет сервер. Отсутствие движения медленно теряет игроков.

2021: сначала научиться собирать

Когда в 2021 году я забрал проект, первой задачей было не развитие, а возможность вообще что-то менять. Серверов было семь: оболочка VOG Shell и шесть игр. Общие библиотеки у них были, но собирался каждый независимо, и результат сильно зависел от того, какие библиотеки стояли на машине сборки.

Первые шаги были такими.

С Oracle на PostgreSQL. Схемы данных были близки, и сама миграция прошла за пару часов. Сложнее было с хранимыми процедурами: их было порядка двадцати, и от них пришлось по максимуму отказаться, перенеся логику в код. В базе осталось несколько штук.

Сборка в Docker. В теории это просто, на практике каждая сборка превращалась в полную пересборку всего, и шла она очень долго. Решилось «золотым» образом: в нём заранее собрано всё окружение и зависимости, поверх него собирается только бинарник, а затем он копируется в маленький итоговый контейнер. Сборка C++-сервера и сейчас занимает 10–15 минут против прежних 30–40. Сервисы на Go для сравнения собираются гораздо быстрее.

Один бинарник вместо семи. Раньше различия между серверами задавались макросами сборки: общий код компилировался для каждого сервера отдельно, со своим SERVERID. В коде на конец 2020 года таких ветвлений было 118. Вот как выглядела функция, которая решает, ждать ли хода игрока:

int DLGame::WaitOrAdjorned(CGameTranscript& GT,uint8_t UserIndex)
{
#if SERVERID == 2
	return GT.GetNextMove()==UserIndex?0:1;
#endif
#if SERVERID == 3
	_tagHeartsGame*				HG = (_tagHeartsGame*)GT.m_GameSpecific;
	_tagHeartsSubgame*			HS = (_tagHeartsSubgame*)GT.m_GameInfoExt.back();
	if(HS->Reason==0 && HS->IsNeedPass(HG->Rules.Pass,GT.m_Header->PlayerCount) && HS->PassValid(GT.m_Header->PlayerCount))
		return GT.GetNextMove()==UserIndex?0:1;
	else
		return 0;
#endif
#if SERVERID == 4
	return GT.GetNextMove()==UserIndex?0:1;
#endif
#if SERVERID == 5
	return GT.GetNextMove()==UserIndex?0:1;
#endif
#if SERVERID == 7
	_tagGammonSubgame*			GS = (_tagGammonSubgame*)GT.m_GameInfoExt.back();
	if(GS->nReason==0)
		return GT.GetNextMove()==UserIndex?0:1;
	else
		return 0;
#endif
	// … и ещё два варианта
}

Чтобы собрать один бинарник, каждое такое ветвление пришлось развести по классам. Общее поведение осталось в базовом классе, особое переехало в наследника своей игры:

// Общее поведение — в базовом классе
int DLGame::WaitOrAdjourned(CGameTranscript &GT, uint8_t UserIndex) {
    return GT.GetNextMove() == UserIndex ? 0 : 1;
}

// Нарды переопределяют его у себя
int vog::game::backgammon::backgammon_game::WaitOrAdjourned(CGameTranscript &GT, uint8_t UserIndex) {
    auto pGammonSubgame = (_tagGammonSubgame *) GT.m_GameInfoExt.back();
    if (pGammonSubgame->nReason == 0)
        return GT.GetNextMove() == UserIndex ? 0 : 1;
    else
        return 0;
}

Какой сервер запускать, теперь решает переменная окружения при старте, а не набор флагов при сборке:

switch (const auto server_id = std::stoi(vgoServerId)) {
    case 1:
        vgos = new vog::common::VGOShellServer();
        break;
    case 5:
        vgos = new vog::game::reversi::VGOReversiServer();
        break;
    // … остальные игры так же
    default:
        console->error("Server not found - {}", server_id);
        return -3;
}
vgos->prepare();
status = vgos->run();

Что не влезло — закомментировать. Денежные игры и турниры при переезде проще было отключить, чем переносить. Их код закомментировали и отложили рефакторинг до лучших времён.

Наблюдаемость — никакой. Ни метрик, ни тестов. Только логи и копящиеся core dump.

Установщик вместо ActiveX. Раньше клиент ставился из браузера: удобно, прозрачно и понятно для игрока. Теперь пришлось собирать MSI на WiX, и бандл выходил на 20–30 МБ. Чтобы его уменьшить, пришлось отказаться от части ботов, в основном от языковых ботов «Слов»: остались русский, английский и французский.

Цена этого периода была видна игрокам. Обновления выпускать было сложно, и отдельные исправления подолгу не доходили до людей. Например, звуки сломались при переезде и долго не работали. Зарегистрироваться или восстановить доступ к аккаунту можно было только ручным скриптом в базе. Уже тогда появилась первая идея следующего шага: связать локальное приложение с браузером через WebSocket. Но до неё было ещё далеко.

2026: латать дальше нельзя

Спустя несколько лет выяснилось, что текущий дистрибутив больше не ставится. Установщик сам скачивает распространяемый пакет библиотек времени выполнения (redist), а новая его версия оказалась несовместимой. Снова пришлось восстанавливать сборку установщика, и стало понятно, что так дальше нельзя: подход к обновлениям нужно менять радикально.

Как теперь устроены установка и обновление Windows-клиента, я расскажу в статье про установщик на Go: там есть довольно необычное применение Go.

Установщик стал последней каплей, но не единственной причиной. Остальные были уже не про сборку, а про устройство самой системы, и заплатками они не чинились:

  • Архив партий на файловой системе. Профили, рейтинги и список партий жили в базе, а сами записи партий — в файлах. Идущая партия ведётся в отображённом в память файле, завершённая дописывается в файловую библиотеку на диске игрового сервера. К 2026 году там лежало 24 ГБ. Процесс привязан к своему диску, и в Kubernetes его не переселить на другой узел и не запустить вторым экземпляром. Оркестрация такого сервера превращается в уход за конкретной машиной. Как это решено, рассказано ниже, в разделе про общее хранилище.
  • Боты «Слов» на машине игрока. Клиентский бот считает ходы на компьютере игрока, причём ход за место бота присылает клиент человека, и сервер его принимает. Для «Слов» этого бота нужен словарь рядом с клиентом, от 5 до 52 МБ на язык. Обход по нему за микросекунды выдаёт все слова, которые складываются на поле, вместе с очками. По сути это готовый подсказчик, который мы сами кладём на диск каждому, кто поставил клиент. Заметить его использование со стороны сервера невозможно: ходы приходят обычные, просто всегда лучшие. Таких ботов нужно переносить на сервер. Как они туда переезжают — отдельная статья серии.
  • Бинарный протокол плохо маршрутизируется. Балансировщик не может по содержимому соединения понять, к какой игре оно идёт. Поэтому у каждого игрового сервера свой порт, и каждый порт отдельно пробрасывается снаружи — это видно по конфигурации HAProxy в предыдущей статье. Каждая новая игра по этой схеме означала бы новый сервер и новый порт. Новый клиент ходит через один вход по WebSocket.
  • Архитектура практически не масштабируется. Каждая игра — один процесс с состоянием в памяти и на своём диске. Второй экземпляр рядом не поставить, нагрузку между ними не разделить. Сейчас трафика немного, но его рост в такой архитектуре привёл бы не к замедлению, а к коллапсу. Как это выглядит, мы видели ещё в 90-х, на шторме переподключений.

В начале 2026 года стало ясно: латать старую систему по частям уже недостаточно. Нужен перезапуск.

Соблазн переписать всё разом

Самый очевидный план звучит так: новый сервер, новый протокол, новый клиент, и в один день переключиться. Я, честно говоря, к нему и тянулся. Как я писал в первой статье, консерватором в нашей паре с ИИ оказался именно ИИ: раз за разом он возвращал разговор к переносу по одной игре.

Аргументы против большого переключения у VOG очень конкретные:

  • Пришлось бы одновременно менять клиент и сервер. При первой же ошибке непонятно, чья она: нового сервера, нового клиента или их разговора.
  • Откатывать пришлось бы обе стороны сразу. А у Windows-клиента обновление доходит до игроков не мгновенно.
  • Данные пришлось бы переносить в момент переключения. Почти тридцать лет партий, рейтингов и профилей за одну ночь, без права на ошибку.
  • Шторм переподключений никуда не делся. Игроки так же вернутся все разом, только теперь на сервер, который живых игроков ещё не видел.

Правило: на каждом шаге меняется что-то одно

Ответ записан в ADR — журнале архитектурных решений проекта — под говорящим названием «Порядок вывода легаси — по одной игре, а не сразу». Его суть — одно правило: на каждом шаге меняется что-то одно. Клиент и сервер не правятся одновременно. Сломавшийся шаг откатывает одну игру, а не платформу.

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

Дальше в статье часто встречается слово «тракт». Так в проекте называют путь, по которому идёт партия. Старый тракт — C++-сервер, его база и файловая библиотека. Новый тракт — шина NATS, служба партий на Go, ScyllaDB для идущих партий и Parquet в S3 для архива. На первом шаге старый сервер отдаёт в новый тракт только копию своих партий. Позже по признаку в каталоге туда уходит и сама игра: вход в комнату через новое лобби, ходы через службу партий.

Совсем не трогать старый сервер при этом не получилось. Чтобы он мог жить рядом с новыми службами, с апреля 2026 года в него добавили поддержку шины NATS и Redis через Sentinel: он работает с тем же Redis, что и новые службы, и переживает переключение его мастера. Это не развитие легаси, а мосты на переходный период, и уйдут они вместе с ним.

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

Из правила получился такой порядок:

flowchart TB S1["1. Общее хранилище
все шесть игр пишут в новый тракт"] --> S2["2. Реверси
первая игра на новом сервисе"] S2 --> S3["3. Остальные игры
по одной, тем же способом"] S3 --> S4["4. Оболочка VOG Shell
лобби, чат, друзья"] S4 --> S5["5. Старый сервер выключен
старая база — только на чтение"] W["Веб-клиент
в те же комнаты"] -.-> S2 W -.-> S3

flowchart TB
    S1["1. Общее хранилище
все шесть игр пишут в новый тракт"] --> S2["2. Реверси
первая игра на новом сервисе"] S2 --> S3["3. Остальные игры
по одной, тем же способом"] S3 --> S4["4. Оболочка VOG Shell
лобби, чат, друзья"] S4 --> S5["5. Старый сервер выключен
старая база — только на чтение"] W["Веб-клиент
в те же комнаты"] -.-> S2 W -.-> S3
Порядок вывода легаси: сначала общее хранилище для всех игр, потом игры по одной, оболочка последней

Веб-клиент в этой схеме идёт отдельной линией: он подключается к игре по мере того, как она переезжает на новый сервис, и садится за те же столы, что и Windows-клиент.

Шаг первый: общее хранилище раньше новых серверов

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

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

Поэтому старый C++-сервер продолжил вести партии сам, но научился дублировать их в новый тракт: начало партии, ходы, откладывание, итог. Идущие партии легли в ScyllaDB, архив — в Parquet в S3, а не в файлы на диске игрового сервера. Новый тракт при этом только принимает: восстанавливать партию из него старый сервер не умеет и не будет. Это означало бы переписать его хранение состояния, то есть ровно ту работу, которой мы и избегаем.

Режим записи — теневой. Партия идёт через привычное хранение, а новый тракт получает копию. Если он не ответил, партия не остановится: в худшем случае ход задержится на секунду таймаута, а в журнале останется предупреждение.

// Primary появится, когда новый тракт станет основным: тогда отказ записи
// обязан отклонять ход, потому что состояние окажется неопределённым.
// Сейчас его нет намеренно — включать до готовности нечего.
enum class TractMode {
    Off,
    Shadow,
};
// Ждать ответа дольше секунды нельзя: в теневом режиме отправка идёт в том
// же потоке, что и обработка хода, и долгий таймаут превратился бы в
// задержку для играющих. Лучше не записать в новый тракт, чем задержать
// партию, которая и так идёт через привычное хранение.
constexpr int64_t REQUEST_TIMEOUT_MS = 1000;

// …

std::string reply;
if (!m_client.request(SUBJECT_CREATE, toJsonString(root), reply, REQUEST_TIMEOUT_MS)) {
    // Тракт вторичен: партия прекрасно идёт через привычное хранение.
    // Но молчать нельзя — без создания в новый тракт не уедут ни ходы,
    // ни результат этой партии.
    logger->warn("[TRACT] create failed for game #{} (no reply)", legacyGameID);
    return;
}

Включается запись одной настройкой сервера и так же выключается, без выката.

Чтобы отказавший тракт не тормозил игру, неудача создания выключает тракт для этой партии, но не для остальных: её ходы дальше никуда не отправляются и таймаута не ждут. А чтобы копия была полной, работают три механизма. Ход считается отправленным только после ответа службы, и точный повтор принимается без дубля. Недостающий хвост ходов досылается при откладывании и закрытии партии. Каждый час служба сверяет законченные партии за последние сутки со списком в базе старого сервера и добирает те, о которых не пришло событие. Это не формальность: при недоступной шине старый сервер молча не отправляет событие, и только 4 октября 2026 года сверка добрала четыре такие партии. Гарантия у неё ограничена окном: позиции она не хранит, и если сама сверка не работала дольше суток, более старые пропуски автоматически не найдутся. Окно задаётся настройкой, и после долгого простоя сверку перезапускают с окном шире.

Мало записать, нужно убедиться, что записано то же самое. При закрытии партии тракт читает её обратно, склеивает ходы по порядку и сравнивает с записью старого сервера байт в байт. На первой же партии это дало расхождение: заголовок блока ходов у реверси (MovesCount) переписывается на каждом ходу, а в тракт уехал со значением после первого. Заголовок исключили из сравнения как производное поле. С этой оговоркой сравнение байт в байт, и дальше оно ловило уже настоящие ошибки.

Живая сверка нашла то, чего не показал бы прогон архива. В архиве эти данные уже были записаны правильно, ошибались только мы, пока писали их заново:

  • Автор хода в «Словах». У игр с одновременным ходом признак «ходит» стоит сразу у обоих, и вся партия подписывалась одним игроком.
  • Итоговые места на четверых. Контракт допускал итог от −1 до 1, а старый формат записывает места выигравших положительными числами, проигравших отрицательными, и за столом червей встречаются 1, 2, −2 и −1. Ни одна партия червей не закрывалась.
  • Молчаливые отказы. Ответ тракта проверялся только при создании партии. У хода, откладывания и закрытия смотрели лишь на сам факт ответа, поэтому предыдущая ошибка два дня оставалась невидимой: в логе сервера не было ни строчки.

Фаза закрылась 31 августа 2026 года: на контрольных партиях каждой из шести игр, сыгранных через теневой тракт, сошлись все четыре проверки — состояние, состав архива, содержимое по смыслу и расчёт рейтинга, включая отложенные и продолженные партии. Рейтинг при этом по-прежнему считал и записывал старый сервер. Новая служба считала рядом своей формулой и только сравнивала, ничего не публикуя. Весь исторический архив — другая выборка. На нём формулы на Go сходятся со старым расчётом на 76–99 % партий в зависимости от игры. Это не допустимая доля ошибок, а необъяснённый остаток: командные, турнирные, с ботами и поправленные вручную партии, правила которых формулы пока не знают. Разбор этого остатка — открытая задача, и пока он не закончен, «сверено» значит «сверено на обычных партиях». На новом тракте случаев, которых формулы не знают, пока не встречали. Рейтинговые партии с ботами там тоже есть, в комнатах «Игра с ботом». Весь старый архив, 17,7 млн партий, к тому времени лежал в Parquet: 11,5 ГБ, упакованные тем же кодом, что пакует новые партии.

Заодно этот шаг стал первым шагом от одной базы к многим. Раньше всё, кроме самих записей партий, жило в одной базе, сначала Oracle, потом PostgreSQL: профили, рейтинги, список партий и их итоги. Записи лежали рядом, в файлах. Теперь у каждой службы своё хранилище, и далеко не все они PostgreSQL: идущие партии в ScyllaDB, архив в Parquet, аналитика в ClickHouse.

Первая игра — реверси

Первой игрой на новом сервисе стала реверси. Причины прозаичные:

  • Движок был уже написан и проверен живыми партиями: доска, ходы, тесты.
  • Простые правила и простая доска. Меньше мест, где новый движок может разойтись со старым.
  • Канал старого клиента к новым службам писался именно на ней. Перевод реверси проверял не только движок, но и весь путь старого клиента в новый тракт.

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

На деле вышло быстрее плана. 14 сентября 2026 года на новый тракт перевели первые две комнаты реверси, не дожидаясь, пока закроются все пункты. Старый Windows-клиент вошёл в них через новое лобби по WebSocket, а не к старому серверу, и первые входы прошли без ошибок. Рейтинг по этим партиям сразу начала считать и записывать новая служба, в ту же общую таблицу, что и старый сервер. В августе проверяли формулу, а теперь в бою оказался весь путь: рейтинг до партии, расчёт, запись. Сверку этого пути на живых партиях мы ещё не прогнали. Это осознанный риск, и он самый дорогой из всех, почему — ниже, в границах отката.

Каталог как переключатель

Клиенту нужно как-то узнать, какая комната обслуживается новым трактом. Отдельной инфраструктуры для этого не понадобилось: и Windows-клиент, и веб уже получали состав серверов и комнат из каталога. В каталог добавили признак тракта: у игрового сервера он говорит клиенту, идти к старому серверу или в новое лобби.

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

Но откат маршрута — не откат состояния, и границы у него такие:

  • Новые партии из Windows-клиента сразу пойдут на старый сервер.
  • Партии, начатые на новом сервисе, идущие и отложенные, на старый сервер не переедут: он поднимает только свои. Их можно доиграть из браузера, который ходит в новый тракт при любом значении признака. Старый клиент их не увидит до обратного переключения.
  • Архив не теряется: старый клиент читает его через новую службу, и записи партий нового тракта попадают туда же после суточной упаковки.
  • Рейтинг, начисленный новой службой, остаётся в общей таблице, и старый сервер продолжит считать от этих значений. Если ошибка была в самом расчёте, откат признака её не исправит: начисленное придётся пересчитывать отдельно, по записям партий. Поэтому рейтинг — самое дорогое место отката.

Для того, ради чего откат нужен, этого достаточно: быстро увести игроков с поломанного пути и чинить без спешки. Именно это и позволяет переводить игры по одной и не бояться ошибиться.

У каталога есть и второе измерение — канал. Комната может быть открыта старому клиенту, вебу или обоим, и для каждого канала свои правила доступа. Так новые игры, которых в старом клиенте нет, видны только в вебе, а комнату можно открыть вебу раньше, чем старому клиенту.

Что вылезло по дороге

Тестовый стенд ушёл вперёд прода. На тестовом стенде все игровые серверы уже работали через новый тракт, и клиент ходил в комнаты только через новое лобби. В версии клиента 2.8.22 была правка, которая считала любой пустой пакет разрывом связи. Но пустым пакетом приходит и удачное соединение со старым сервером. В итоге 2.8.22 не входил ни в одну комнату старого сервера, хотя на тестовом стенде всё работало: проверять там этот путь было просто нечем. Исправление вышло следующей версией. Вывод неприятный, но полезный: пока на проде живут оба тракта, тестовый стенд должен держать оба.

«Сыграл и сразу посмотрел». Старый клиент открывает партию из архива через новую службу, но в индекс архива партия попадает только при суточной упаковке. До неё служба отвечала «нет такой», и клиент уходил запасным путём к старому серверу. Пока тот жив, партия находится. После его выключения не нашлась бы нигде. Значит, перед выключением сервера показ свежей партии должен идти из ScyllaDB.

Отложенные партии. В реверси играют и по переписке, и отложенных партий накопилось много. Новая служба откладывать и продолжать умела, но старый клиент спрашивал список отложенных у старого сервера по TLV. Без перевода этих запросов игрок в переведённой комнате мог отложить партию, но не мог её доиграть. Эти запросы перевели, и теперь отложенные партии в переведённых играх видны в старом клиенте и доигрываются.

Что отпустили сознательно

Порядок «по одной игре» имеет цену, и её стоит назвать честно.

Турниры по переведённой игре на паузе. Турнирную машину старого сервера мы не дорабатываем и не переносим как код. Это источник правил, которые нужно извлечь и реализовать в отдельной службе. Пока службы нет, турниров по переехавшей игре не будет. Это прямое следствие отказа развивать TLV, и мы на это согласились заранее.

Старый сервер не улучшаем. Мосты к новому стеку — исключение, без них не было бы переходного периода. А вот устойчивость к обрыву связи с базой, задача вполне полезная, ушла в самый низкий приоритет: цель — выключить сервер, а не сделать его лучше. По той же причине старые серверы не переносили в виртуальные машины, хотя пара эпизодов недоступности к этому подталкивала. Вместо переноса — лимиты памяти контейнерам и правило «ничего тяжёлого на этой машине».

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

Не всё переехало. Планировалось переключать игры постепенно, с тестированием и проверкой на каждом шаге, но получилось гораздо быстрее. Платой за скорость стало то, что не весь функционал старого тракта переехал на новый. Чего-то нет пока, от чего-то, вроде денежных игр, мы отказались совсем.

Где мы сейчас

Состояние на начало октября 2026 года.

К концу августа все шесть игр писали партии в новый тракт. К середине сентября у каждой появился свой движок в новой службе. Дальше переключение пошло быстрее, чем мы планировали:

Игра Старый Windows-клиент Веб
Реверси, шашки, шахматы новый тракт новый тракт
Нарды, червы, «Слова» старый сервер новый тракт
Го, домино этих игр в нём нет новый тракт

В реверси, шашках и шахматах Windows-клиент и браузер теперь садятся за одни столы — ради этого всё и делалось. Старые серверы этих игр пока не погашены: пока они работают, маршрут любой из этих игр откатывается правкой признака в каталоге. Старый сервер игры гасим, когда выполнены все условия:

  1. Неделя на новом тракте без жалоб игроков на переход и без падений служб.
  2. Только что сыгранная партия открывается сразу, из ScyllaDB, а не запасным путём через старый сервер.
  3. Отложенные партии, созданные на новом тракте, видны и доигрываются в обоих клиентах. Это уже работает.
  4. Расчёт рейтинга новой службой сверен на живых партиях.
  5. По всему, что осталось только на старом сервере, — турнирам, платным комнатам — принято решение: перенести, отложить или отказаться. У реверси таких комнат нет.

В нардах, червах и «Словах» пока сохраняется промежуточное состояние: Windows-клиент играет на старом сервере, веб — через новый сервис. Следующий шаг для них тот же, что уже прошли реверси, шашки и шахматы. После них очередь оболочки VOG Shell: лобби, чат, друзья, присутствие. Когда переедет и она, старый сервер выключится целиком, а старая база останется только на чтение, для сверок.

А новые игры наконец перестали быть событием. Если после VOG2 успели добавить только две игры, то го и домино появились на новом стеке в сентябре 2026 года за считанные недели: нужен движок на Go и компонент стола в вебе, а не новый сервер.

Итог

  • Осторожность — не врождённое свойство, а опыт. Резкие движения в VOG роняли сервер, отсутствие движения теряло игроков. Нужен был способ двигаться без резкости.
  • Прежде чем переносить, нужно научиться собирать. Один бинарник вместо семи, сборка в контейнере, основная логика вынесена из хранимых процедур — без этого 2021 года не было бы 2026-го.
  • Починить сборку недостаточно: ограничения архитектуры остаются. Архив на диске сервера, боты с данными на машине игрока, порт на каждую игру и рост только вверх заплатками не лечатся.
  • На каждом шаге меняется что-то одно. Клиент и сервер не правятся одновременно, сломавшийся шаг откатывает одну игру.
  • Первым переносится хранение, а не игра. Когда партии всех игр уже лежат в новом хранилище, переезд игры перестаёт быть переносом данных.
  • Теневой режим и сверка байт в байт (за вычетом производных полей) ловят то, чего не покажет прогон архива: ошибки появляются, когда данные пишутся заново, а не когда они уже записаны.
  • Признак в каталоге дешевле выката, но откатывает маршрут, а не состояние. Партии нового тракта доигрываются в нём, а ошибку рейтинга признак не исправит.
  • У порядка есть цена: турниры на паузе, часть функционала не переехала. Её лучше назвать заранее, чем обнаружить по жалобам.

Что дальше в серии

В этой статье проблемы в основном названы. Решаются они в следующих статьях серии:

  • Установщик и обновления Windows-клиента на Go. Как клиент перестал зависеть от redist и MSI и стал обновляться сам, в том числе звуки и прочие исправления, которые раньше годами не доходили до игроков.
  • Боты на сервере. Как боты «Слов» и остальных игр переехали с машины игрока на сервер, и как Go держит старые 32-битные DLL ботов, исходников которых нет.
  • Один стол для браузера и Windows-клиента. Как старый бинарный протокол и JSON через WebSocket сошлись в одних комнатах, и почему портов на каждую игру больше не нужно.
  • Архив в Parquet на S3готовится, с 24 октября. Как перепаковывали 17,7 млн партий, подбирали подход, проверяли и приводили к одному формату.
  • Из монолита в службы. Как семь серверов с состоянием на диске разделились на отдельные решения, готовые к масштабированию.
  • От одной базы к многим. Почему вместо одной базы Oracle, потом PostgreSQL, теперь несколько хранилищ, и не все из них PostgreSQL.
  • Вход без пароля. Как регистрация и восстановление доступа перестали быть ручным скриптом в базе.
  • Рейтинги за тридцать лет. Почему ошибку рейтинга заметить труднее всего и как сверяли расчёт с историческим архивом.
  • Работа в паре с ИИ. Как выглядит перезапуск, где консерватор — ИИ.

Обсуждение в Telegram

Присоединиться →

Комментарии