Northflank: два ONNX-бота — установка, эксплуатация и первые замеры
Summary
Состояние на 17.08.2026: оба webhook-сервиса —
northflank/kcp-one-ply-1иnorthflank/hybrid-kcp-1— остановлены,instances: 0. Они сняты с лестницы Play, и, в отличие от первоначального предположения, Developer Sandbox не делал их бесплатными: два бота стоили $13,07/мес (разбор в §5.1). Планnf-compute-20и образ сохранены, подъём — одной командой.Замеры за 78 часов работы 14–17.08 остаются в силе: каждый сервис получал 0,2 shared vCPU, 512 MiB RAM, один экземпляр и одну одновременную партию. KCP One Ply упирается в CPU, но имеет большой запас памяти. Hybrid KCP использует CPU мягче, зато дошёл примерно до 408 MiB / 512 MiB — в
nf-compute-10(256 MiB) он бы не поместился.Куда переносить ботов — открытый вопрос, решается отдельно.
Связанные заметки:
- Где запускать бота: варианты и ёмкость домашней лаборатории;
- Dexus: KCP One Ply ONNX-бот;
- Боты на платформах: рантаймы, параллельность и лимиты;
- Артефакты моделей: хранение, бэкап и деплой;
- Azure Container Apps: оценка альтернативы;
- Google Cloud: стоимость и лимиты.
1. Зачем сделаны два бота
Модель kcp_nodice_serving_20260809.onnx появилась как аналитическая модель оценки
позиции. При просмотре партий обнаружилось, что она иногда предпочитает ход, отличный
от хода существующего Hunter book. Это породило два разных эксперимента:
- KCP One Ply — применить KCP-модель напрямую ко всем легальным ходам и выбрать лучший результат без expectimax-поиска.
- Hybrid KCP — сохранить rich-feature 2-ply expectimax, но пересчитать небольшой шортлист корневых ходов KCP-моделью.
Это две отдельные игровые гипотезы, поэтому им нужны отдельные имена и рейтинговые истории. При этом транспорт, HMAC, часы, книга, HTTP-сервер и контейнер одинаковы. Поддерживать два репозитория или два Dockerfile не требуется.
Публичный репозиторий кода: rabestro/dicechess-bot-gcp-onnx. Подготовка сделана в PR #17, который:
- обновил движок до
dicechess-engine-scalav1.14.0; - добавил
SEARCH_MODE=expectimax|one-ply; - добавил выбор
TIME_POLICY=empirical-v1|legacy-linear-v1; - вынес production opening book во внешний
OPENING_BOOK_PATH; - оставил в публичном репозитории только пятистрочный пример TSV;
- сохранил synthetic ONNX-модель, чтобы открытый проект собирался и тестировался без закрытых весов;
- починил и проверил linux/amd64 Docker-сборку.
Почему версия находится в имени Play-бота
Play ранжирует идентичность, а не код. Суффикс
-1фиксирует первую итерацию алгоритма. Если существенно меняются модель, фичи, вес рескоринга или стратегия, корректнее зарегистрировать-2, чем смешивать две силы в одной рейтинговой истории.
2. Архитектура, которая работает сейчас
flowchart LR L["Play ladder\nplay.jc.id.lv"] -->|"signed HTTPS POST\n/api/webhook"| K["Northflank service\nkcp-one-ply\n0.2 vCPU / 512 MiB"] L -->|"signed HTTPS POST\n/api/webhook"| H["Northflank service\nhybrid-kcp\n0.2 vCPU / 512 MiB"] G["Private Google Artifact Registry\nlinux/amd64 image"] --> K G --> H P["Public GitHub repo\ncode + synthetic model\n+ 5 book rows"] --> G A["Private assets\n2 ONNX models\n+ 147-row book"] --> G
Northflank
| Параметр | Значение |
|---|---|
| Team | rabestros-team |
| Project | Dice Chess ONNX Bots (dice-chess-onnx-bots) |
| Регион | Europe West — Frankfurt |
| Архитектура | x86 / linux-amd64 |
| Сервисы | kcp-one-ply, hybrid-kcp |
| Compute plan | nf-compute-20 |
| На сервис | 0,2 shared vCPU, 512 MiB RAM, 1 GiB ephemeral storage |
| Экземпляры | 1 на сервис |
| Публичный порт | HTTP 8080, автоматически выданный HTTPS-домен |
| Persistent volumes / database | нет |
| Registry integration | Dice Chess GAR |
Публичные webhook URL:
https://http--kcp-one-ply--85xh4h7g8scd.code.run/api/webhook;https://http--hybrid-kcp--85xh4h7g8scd.code.run/api/webhook.
Play
| Сервис | Зарегистрированная идентичность | Capacity | Ladder | Human catalog |
|---|---|---|---|---|
kcp-one-ply | northflank/kcp-one-ply-1 | 1 | включена | выключен |
hybrid-kcp | northflank/hybrid-kcp-1 | 1 | включена | выключен |
maxConcurrentGames=1 выбран намеренно. Внутри JVM один Strategy сериализует
chooseMoves, потому что ONNX-сессия не считается thread-safe. Второй параллельный
webhook в тот же контейнер ждал бы первый и тратил часы другой партии. Ограничение Play
не позволяет посадить бота во вторую игру, пока первая не закончилась.
3. Где проходит публично-приватная граница
Публично доступны:
- Scala-код стратегии и webhook-рантайма;
- Dockerfile;
- synthetic signal-free ONNX-модель;
- пять строк
opening_book.tsv, показывающие формат; - все переменные конфигурации и описание алгоритмов.
Приватными остаются:
- реальные обученные ONNX-веса;
- полная production opening book;
- Play bearer tokens;
- per-bot HMAC webhook secrets;
- ключ или credential, дающий Northflank право читать приватный GAR.
Снимок приватных артефактов в образе
Хэши позволяют проверить идентичность файлов, но не раскрывают их содержимое:
| Файл | Размер | SHA-256 | Назначение |
|---|---|---|---|
kcp_nodice_serving_20260809.onnx | 1 149 338 B | 3154c6e4d35292063ebf55141a90d2d09cb70418ef9035d4bb6aae7a4679434d | KCP-13: one-ply и root rescore |
oracle-3.onnx | 1 149 338 B | 5e3cd9a1fbeba2b762d4b914d134298fcc7e33d83b59ae37577700a8ca06f702 | rich-9: 2-ply expectimax |
opening_book.tsv | 10 161 B, 147 строк | 2f00088525b2ca0485de4ae7b233a32caed9cff9f15330fa80eb2d484e675e68 | закрытая production book |
Файлы находятся в контейнере под /models. Это приватный производный образ:
публичная базовая сборка заканчивается JAR-файлом, а две верхние image-layer добавляют
/models/ и полную книгу. История текущего образа подтверждает слои размером примерно
2,3 MB и 10,2 kB.
Не пушить приватный производный образ в публичный registry
Репозиторий публичный, а trained weights и production book — нет. Тег с приватными слоями разрешено публиковать только в закрытый GAR/другой закрытый registry. Перед
docker pushвсегда проверять полное имя registry, project и repository.
4. Текущий контейнерный артефакт
| Поле | Значение |
|---|---|
| Registry | europe-west3-docker.pkg.dev |
| GCP project | dice-chess-lab |
| Repository | dicechess |
| Image | dicechess-bot-onnx:edccf9d |
| Manifest/image digest | sha256:7458a22d5b18ef993c0a76f52d393798a879a021a9a112295111fd3f7351ebfc |
| Platform | linux/amd64 |
| Локальный uncompressed size | 160 206 160 B (≈160,2 MB) |
| Исходный commit | edccf9d из PR #17 |
Тег — commit SHA, а не latest. Это даёт воспроизводимый rollback и не позволяет
Northflank неожиданно получить другой набор весов под старым именем.
Рекомендуемая двухступенчатая сборка
Публичную и приватную части лучше собирать раздельно:
# В публичном dicechess-bot-gcp-onnx.
GH_TOKEN="" gh auth status
sbt clean "testOnly *" assembly
PUBLIC_IMAGE="dicechess-bot-onnx-public:<commit-sha>"
docker build --platform linux/amd64 -t "$PUBLIC_IMAGE" .Затем в отдельном приватном, gitignored или временном build context:
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
COPY models/ /models/
COPY opening_book.tsv /models/opening_book.tsvPRIVATE_IMAGE="europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/dicechess-bot-onnx:<commit-sha>"
docker build --platform linux/amd64 \
--build-arg "BASE_IMAGE=$PUBLIC_IMAGE" \
-t "$PRIVATE_IMAGE" \
/path/to/private-build-context
docker run --rm --platform linux/amd64 --entrypoint /bin/sh "$PRIVATE_IMAGE" -c '
sha256sum /models/*.onnx /models/opening_book.tsv
wc -l /models/opening_book.tsv
'
gcloud auth configure-docker europe-west3-docker.pkg.dev
docker push "$PRIVATE_IMAGE"Нельзя копировать private build context внутрь публичного репозитория даже
«на минуту»: случайный git add, Docker build cache и архивы CI превращают временный
файл в долгоживущую копию. Исходные модели должны иметь отдельный бэкап согласно
runbook артефактов моделей.
5. Подготовка Northflank
5.1. Аккаунт и бесплатный план
Исправлено 17.08.2026: слоты Sandbox не защитили от списания
Первая редакция раздела утверждала, что два бота бесплатны, поскольку занимают два free-слота Developer Sandbox. На практике это не сработало: выделенные vCPU и RAM метрились по полным PAYG-ставкам с первой секунды жизни контейнера. За ~78 часов работы (деплой 14.08 07:51 UTC) два сервиса дали 13,07/мес. Оба заскейлены в
instances: 0.Должно ли было так быть — вопрос открытый: официально тариф Developer заявлен бесплатным. Разбор противоречия — ниже в этом же разделе.
Для создания ресурсов Northflank требует платёжную карту даже на Developer Sandbox, и это действительно проверка личности — формулировка официальной документации:
all users must add a payment method to start creating resources on Northflank, regardless of plan selection. This is to verify user identity, and prevent malicious usage of the platform.
То есть сам факт добавленной карты платного потребления не означает. Но и защитой от него не является: списание в нашем случае произошло (см. ниже).
На 14.08.2026 официальная документация Developer Sandbox даёт:
- два service-слота;
- два job/cron-слота;
- один addon/database-слот;
- один проект для обычного эксперимента.
Это лимиты на количество ресурсов, а не бесплатная вычислительная квота. Ни на странице прайса, ни в документации нет утверждения, что vCPU и RAM в Sandbox бесплатны, — но нет и обратного. Именно это молчание породило ошибку первой редакции. Формулировки прайса («2× free services», «Always-on-compute – no sleeping») ей активно способствуют; см. также лимиты бесплатных тарифов у платформ.
Как считают на самом деле
Тарифицируется выделено × время существования контейнера — не потребление и не число
запросов. Простой оплачивается полностью: снятие ботов с лестницы Play и отсутствие
живых игроков на счёт не влияют никак.
Ставки: 0,00833 / GiB-ч. Отсюда nf-compute-20
(0,2 vCPU / 512 MiB) = 10,80/мес,
с латвийским НДС 21% — $13,07/мес.
Три независимых подтверждения, что считают именно выделение:
- У обоих сервисов счёт совпал до цента (15,56 vCPU-ч / 37,11 GiB-ч), хотя профили разные: KCP One Ply упирается в CPU, Hybrid KCP — в память.
- 37,11 GiB-ч ÷ 74 ч = ровно 0,5 GiB, то есть выделенные 512 MiB. По фактическому RSS у Hybrid (408 MiB) вышло бы ≈29,7 GiB-ч.
- 15,56 vCPU-ч ÷ 0,2 vCPU = 77,8 ч — совпадает со временем от деплоя с точностью до часа (расхождение объясняется задержкой снимка биллинга). Значит метр не прерывался ни на час.
Бесплатного compute-плана в каталоге не существует
northflank list plans(и публичныйGET /v1/plans) отдаёт 20 планов, из них ни одного с нулевой ценой. Самый дешёвый —nf-compute-10(0,1 vCPU / 256 MiB) за $2,70/мес, и Hybrid KCP в 256 MiB не поместился бы: он уже на 408 MiB. Гипотезы про регион, внешний GAR-образ и купоны подтверждения не нашли.Важно не спутать два разных объекта: compute-план (
nf-compute-*, привязан к сервису) и биллинговый план аккаунта (Sandbox / Pay-as-you-go). Отсутствие compute-плана за $0 само по себе не доказывает, что Sandbox платный, — Sandbox мог бы покрывать выделение отдельным механизмом.
Нерешённое противоречие: официально Developer заявлен бесплатным
Это надо знать, прежде чем считать вопрос закрытым. Официальные источники Northflank противоречат факту нашего списания.
За бесплатность compute:
- матрица сравнения тарифов на странице цен: у колонки Developer строка Pricing = «Free», строка Compute = «Limited»;
- в той же матрице две строки биллинга — «To the second billing» и «Invoice history» — для Developer помечены как неприменимые, тогда как у Pay-as-you-go включены;
- карточка тарифа продаёт «free» именно применительно к ресурсам: «2× free services», «1× free database», плюс «Always-on-compute – no sleeping»;
- changelog от 22.11.2021 (старое имя «free developer projects») говорит прямо: «Everything is free within a developer project… no risk of incurring cloud costs», «2 services, 2 jobs and 1 database with generous resources».
Против:
- фактическое списание по полным PAYG-ставкам без единой скидочной строки;
- ни одна действующая страница документации не утверждает, что vCPU и RAM в Sandbox бесплатны, — и не утверждает обратного. Размер «Limited» нигде не выражен в числах;
- переименование «free developer projects» → «Developer Sandbox» в changelog не задокументировано, так что обещание 2021 года не переносится на текущий тариф автоматически. При этом нет и ни одной записи об урезании бесплатного тарифа.
Отсюда два возможных объяснения, различить которые изнутри не удалось:
- аккаунт фактически не на Developer/Sandbox, а на Pay-as-you-go — тогда всё корректно, и бесплатный тариф просто никогда не был активен;
- аккаунт на Developer, но
nf-compute-20выходит за необъявленный предел «Limited» — тогда заявленная «Free» и реальное поведение расходятся.
Что для этого сделать
Через API план аккаунта не виден: org-scoped эндпоинты требуют org-токена, а
list usageиlist invoicesотдают 403. Поэтому:
- посмотреть в UI, какой тариф показан у аккаунта (Billing → план);
- если там Developer/Sandbox — задать вопрос поддержке, приложив строки счёта: при заявленной «Free» списание по полным ставкам требует объяснения;
- параллельно любопытная деталь: 403 «Feature disabled for your account» пришёл именно на usage и invoices — те самые две строки, что в матрице помечены неприменимыми для Developer. Это согласуется с гипотезой «аккаунт на Developer», но не доказывает её: те же эндпоинты могут быть просто org-only.
Как останавливать расходы
Метр останавливает только instances: 0 либо Pause. Wake-on-HTTP для scale-from-zero
нет: автоскейл по RPS требует хотя бы одного живого инстанса. Даже будь он, холодный
старт JVM с ONNX (30–60 с) не уложился бы в таймаут вебхука — play-api рвёт клиента
на 45 с.
northflank scale service --project dice-chess-onnx-bots --service kcp-one-ply -i '{"instances": 0}'
northflank scale service --project dice-chess-onnx-bots --service hybrid-kcp -i '{"instances": 0}'deploymentPlan в теле запроса не указан намеренно: план и образ сохраняются, обратный
подъём — та же команда с "instances": 1.
По документации на нуле инстансов продолжает тарифицироваться только storage томов
(«When paused, you are only billed for volume storage»). У нас volumes в проекте нет —
northflank list volumes отдаёт [], — поэтому остаётся ноль.
Ловушка: CI на нуле инстансов продолжает билдить
Раздел Scale документации: «If you leave CI enabled the service will still build the latest commit to the linked repository». Билды тратят compute-план, то есть сервис на
instances: 0с включённым CI может продолжать начислять. У нас оба сервиса развёрнуты из внешнего GAR-образа, а не из связанного репозитория, поэтому build-пути нет — но при переходе на сборку внутри Northflank это надо проверить заново. Сам раздел Scale о биллинге не упоминает вообще, так что предупреждения там не будет.
Чем проверять, что метр встал
Не кодом выхода команды и не тем, что панель показывает ноль инстансов. Фактическая проверка —
Last 24 hoursв Billing должно стать **0,35).Почасовой API для этого не годится:
list usageиlist invoicesотдают 403 «Feature disabled for your account», так что следить за расходами через CLI на этом аккаунте нельзя — только панель и billing alerts. Отдельно:list teamsиget teamдают другой 403 («requires an organisation-scoped token»), это лишь скоуп токена и об аккаунте ничего не говорит. Узнать план аккаунта через API не удалось.Порог по умолчанию **13/мес счёт придёт молча в конце месяца. Billing alerts на аккаунте не настроены ни одного — незакрытый хвост.
Пригодность Sandbox по назначению
Официальное описание называет Sandbox средой для изучения платформы, hobby-проектов и проверки новых идей и отдельно говорит, что он не предназначен для production applications. Текущая работа — некоммерческий исследовательский эксперимент без SLA и без критической зависимости сайта от этих двух ботов — соответствует заявленному назначению по существу. Это эксплуатационная оценка, а не юридическая гарантия: если появятся коммерческая цель, SLA или production-зависимость, тариф и условия нужно пересмотреть.
Третий бот нельзя считать бесплатным продолжением этой схемы — но теперь по двум причинам: свободных service-слотов нет, и любой слот в любом случае платный.
5.2. CLI
На рабочей машине использовался Northflank CLI 0.12.1:
npm install --global @northflank/cli
northflank login
northflank --versionnorthflank login открывает одноразовую страницу авторизации. API token нельзя
печатать в заметке, shell history или логе CI. Для автоматизации нужен отдельный token
с минимальными правами на один team/project, а не личный admin-token.
5.3. Доступ к приватному GAR
В Northflank сохранена registry integration Dice Chess GAR и выбрана как credential
у обоих сервисов. Она должна иметь только право читать образы из
dice-chess-lab/dicechess.
Рекомендуемая долговечная схема:
- отдельный GCP service account для Northflank;
- роль
roles/artifactregistry.reader, по возможности на один repository; - JSON key загрузить только в Northflank registry credential;
- локальную временную копию key удалить после проверки;
- записать дату/владельца ключа и план ротации, но не само значение.
Команды для отдельного read-only service account:
GCP_PROJECT="dice-chess-lab"
GCP_REGION="europe-west3"
GAR_REPOSITORY="dicechess"
GAR_READER="northflank-gar-reader"
GAR_READER_EMAIL="${GAR_READER}@${GCP_PROJECT}.iam.gserviceaccount.com"
gcloud iam service-accounts create "$GAR_READER" \
--project "$GCP_PROJECT" \
--display-name "Northflank Artifact Registry reader"
gcloud artifacts repositories add-iam-policy-binding "$GAR_REPOSITORY" \
--project "$GCP_PROJECT" \
--location "$GCP_REGION" \
--member "serviceAccount:${GAR_READER_EMAIL}" \
--role roles/artifactregistry.reader
umask 077
gar_key_file=$(mktemp /tmp/northflank-gar-reader.XXXXXX.json)
gcloud iam service-accounts keys create "$gar_key_file" \
--project "$GCP_PROJECT" \
--iam-account "$GAR_READER_EMAIL"Файл gar_key_file загружается в Northflank как GCP registry credential и после
проверки удаляется с ноутбука. Это long-lived secret: его нужно ротировать и никогда
не добавлять в git/iCloud/заметки. Если Northflank предложит keyless federation для
обычного external image pull, она предпочтительнее JSON key.
Проверить credential до следующего обновления
При первом развёртывании нужно убедиться, что Dice Chess GAR использует долговечный read-only credential, а не часовой
gcloud auth print-access-token. Уже запущенный pod продолжит работать с локально скачанным образом, но новый pod или redeploy после истечения временного token не сможет pull-нуть image. Проверка: сделать контролируемый redeploy одного сервиса до следующего релиза и убедиться, что image загружается без обновления credential.
5.4. Проект и сервисы
Проект создан в регионе Frankfurt. Оба сервиса — Deployment → External image, источник — приватный GAR image, registry credential — Dice Chess GAR.
Путь в UI для повторения:
- Team → Integrations → Registries → Add registry: имя
Dice Chess GAR, тип Google, registryeurope-west3-docker.pkg.dev, read-only credential из §5.3. - Projects → Create project:
Dice Chess ONNX Bots, регион Europe West — Frankfurt, Developer Sandbox. - Project → Create → Service → Deployment → External image: вставить immutable
GAR tag, выбрать
Dice Chess GAR. - Выбрать
nf-compute-20, один instance и 1 GiB ephemeral storage. - Открыть порт 8080 как public HTTP и оставить автоматически выданный домен.
- Добавить две TCP-пробы из таблицы ниже.
- Добавить несекретные runtime variables из §6;
DICECHESS_WEBHOOK_SECRETпоявится только после регистрации webhook в §7.
GitHub/VCS к team намеренно не подключён. Northflank не собирает публичный
репозиторий и не имеет доступа к GitHub: он только pull-ит уже собранный private image
из GAR. Поэтому баннер team doesn't have a version control platform linked для этой
архитектуры нормален.
Общие параметры:
{
"billing": { "deploymentPlan": "nf-compute-20" },
"deployment": {
"instances": 1,
"external": {
"imagePath": "europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/dicechess-bot-onnx:edccf9d",
"credentials": "<Dice Chess GAR credential id>"
},
"storage": {
"ephemeralStorage": { "storageSize": 1024 }
}
},
"ports": [
{
"name": "http",
"internalPort": 8080,
"protocol": "HTTP",
"public": true
}
]
}Health checks:
| Probe | Protocol | Port | Initial delay | Period | Timeout | Failure threshold |
|---|---|---|---|---|---|---|
| startup | TCP | 8080 | 10 s | 10 s | 5 s | 18 |
| readiness | TCP | 8080 | 10 s | 30 s | 5 s | 3 |
Startup допускает до трёх минут на запуск JVM и загрузку ONNX. TCP readiness проверяет, что процесс слушает порт, но не доказывает, что модель выбирает легальный ход. Поэтому после каждого обновления обязательны Play handshake и реальный smoke turn.
6. Конфигурация двух стратегий
KCP One Ply
MODEL_PATH=/models/kcp_nodice_serving_20260809.onnx
OPENING_BOOK_PATH=/models/opening_book.tsv
ORACLE_FEATURES=kcp
SEARCH_MODE=one-ply
TIME_POLICY=empirical-v1
OVERHEAD_BUFFER_MS=1000
DEFAULT_THINK_MS=2000Hybrid KCP
MODEL_PATH=/models/oracle-3.onnx
OPENING_BOOK_PATH=/models/opening_book.tsv
ORACLE_FEATURES=rich
SEARCH_MODE=expectimax
TIME_POLICY=empirical-v1
OVERHEAD_BUFFER_MS=1000
DEFAULT_THINK_MS=2000
ORACLE_CANDIDATE_LIMIT=4
RESCORE_MODEL_PATH=/models/kcp_nodice_serving_20260809.onnx
RESCORE_WEIGHT=0.5Общее замечание: RESCORE_MODEL_PATH разрешён только при
SEARCH_MODE=expectimax. One-ply с rescore должен упасть при старте, а не молча
игнорировать ошибочную конфигурацию.
TIME_POLICY=empirical-v1 — новый алгоритм управления временем движка, полученный из
реального распределения длительности партий Dice Chess. Старый
legacy-linear-v1 сохранён для сравнения и rollback, но в этих двух deployments не
используется.
OVERHEAD_BUFFER_MS=1000 больше кодового default 300 ms. На маленьком shared CPU это
осознанный запас на последнюю неотменяемую ONNX-операцию, сериализацию и сетевой
round-trip. Уменьшать его стоит только по фактическим измерениям, а не ради ещё одной
кандидатной позиции.
Что должно быть в startup logs
KCP:
[bot] opening book: /models/opening_book.tsv (147 entries)
[bot] ONNX model: /models/kcp_nodice_serving_20260809.onnx (mode=one-ply, features=kcp, timePolicy=empirical-v1, ...)
[bot] ONNX custom handler listening on :8080/api/webhookHybrid:
[bot] opening book: /models/opening_book.tsv (147 entries)
[bot] ONNX model: /models/oracle-3.onnx (mode=expectimax, features=rich, timePolicy=empirical-v1, candidateLimit=4, rescore=/models/kcp_nodice_serving_20260809.onnx (weight=0.5))
[bot] ONNX custom handler listening on :8080/api/webhookЕсли вместо реального пути видно MODEL_PATH not set — using the bundled synthetic model, deployment формально healthy, но играет без шахматного сигнала. Это блокирующая
ошибка конфигурации.
7. Регистрация в Play без утечки секретов
7.1. Зарегистрировать долговечные identity
PLAY_BASE="https://play-api.jc.id.lv"
registration_json=$(curl --fail --silent --show-error \
-X POST "$PLAY_BASE/bot/register" \
-H "Content-Type: application/json" \
-d '{"team":"northflank","name":"kcp-one-ply-1"}')
bot_token_value=$(printf '%s' "$registration_json" | jq -r .token)
security add-generic-password \
-s dicechess-play-bot-token \
-a northflank/kcp-one-ply-1 \
-U -w "$bot_token_value"
unset bot_token_value registration_jsonПовторить для northflank/hybrid-kcp-1. Значение token сервер показывает один раз.
Команду нельзя запускать с set -x; переменную нельзя echo-ить. На общей машине
лучше использовать интерактивный ввод Keychain, чтобы token даже кратковременно не был
виден в аргументах процесса.
Проверка без вывода token:
security find-generic-password \
-s dicechess-play-bot-token \
-a northflank/kcp-one-ply-1 >/dev/null && echo OK7.2. Зарегистрировать webhook
Сначала Northflank endpoint должен быть публичным и healthy. Verification handshake не требует HMAC secret: runtime возвращает присланный nonce. После успешного handshake Play один раз возвращает новый per-bot secret.
bot_token_value=$(security find-generic-password \
-s dicechess-play-bot-token \
-a northflank/kcp-one-ply-1 -w)
webhook_registration=$(curl --fail --silent --show-error \
-X POST "$PLAY_BASE/bot/webhook" \
-H "Authorization: Bearer ${bot_token_value}" \
-H "Content-Type: application/json" \
-d '{"url":"https://http--kcp-one-ply--85xh4h7g8scd.code.run/api/webhook"}')
webhook_secret_value=$(printf '%s' "$webhook_registration" | jq -r .secret)webhook_secret_value нужно сразу записать в Northflank runtime variable
DICECHESS_WEBHOOK_SECRET, не печатая его и не добавляя в JSON внутри git. После
обновления сервиса:
unset webhook_secret_value webhook_registration bot_token_valueПовторить с отдельными token, URL и secret для Hybrid. Один secret нельзя разделять между identity.
Повторная регистрация ротирует secret
Каждый новый
POST /bot/webhookзаменяет URL и HMAC key. Старое значение перестаёт подходить. После re-register нужно сразу обновитьDICECHESS_WEBHOOK_SECRETи перезапустить deployment.
7.3. Capacity и ladder
Оставляем default capacity равным одному и проверяем:
bot_token_value=$(security find-generic-password \
-s dicechess-play-bot-token \
-a northflank/kcp-one-ply-1 -w)
curl --fail --silent --show-error "$PLAY_BASE/bot/capacity" \
-H "Authorization: Bearer ${bot_token_value}" | jq
curl --fail --silent --show-error -X POST "$PLAY_BASE/bot/ladder/join" \
-H "Authorization: Bearer ${bot_token_value}" | jq
unset bot_token_valueОжидаем maxConcurrentGames: 1, ladderAllowance: 1, onLadder: true в profile.
openToHumans пока оставлен false: текущая цель — чистый ladder experiment.
8. Проверка после развёртывания
Порядок важен:
- Northflank показывает
Running,1 / 1, startup/readiness passing. - В runtime logs загружены реальная модель и книга из 147 строк.
GET /bot/webhookпоказывает правильный URL иverifiedAt.GET /bot/capacityпоказывает одну разрешённую и не более одной активной партии.- Один контролируемый unrated game завершается реальными ходами.
- В
GET /bot/webhook/statsрастётapplied, нетtimed_out,unreachable,http_5xxилиgarbled. - Только после этого бот входит в ladder.
Текущий service status:
- оба deployments —
COMPLETED; - по одному pod,
0 restarts; - у обоих
2/2probes passing.
9. Первые реальные замеры
9.1. Northflank CPU и память
Окно: 14.08.2026 07:51:30–08:24:30 UTC (10:51–11:24 Europe/Riga), шаг 15 секунд. Метрика CPU — процент от выделенных 0,2 vCPU; память — процент от 512 MiB. В таблице приведён текущий pod каждого сервиса, а не старые pods от промежуточных deployments.
| Метрика | KCP One Ply | Hybrid KCP |
|---|---|---|
| CPU average | 50,97% = 0,102 vCPU | 27,74% = 0,055 vCPU |
| CPU p50 | 57,95% = 0,116 vCPU | 25,17% = 0,050 vCPU |
| CPU p90 | 99,95% = 0,200 vCPU | 63,38% = 0,127 vCPU |
| CPU max | 100% | 98,16% |
| Memory average | 27,98% = 143,2 MiB | 57,12% = 292,5 MiB |
| Memory p50 | 28,26% = 144,7 MiB | 76,35% = 390,9 MiB |
| Memory p90 | 32,76% = 167,7 MiB | 78,50% = 401,9 MiB |
| Memory latest / max | 35,26% = 180,5 MiB | 79,58% = 407,4 MiB |
| Запас на последней точке | ≈331,5 MiB | ≈104,6 MiB |
Интерпретация:
- KCP One Ply CPU-bound. На p90 он уже выбирает весь shared CPU. Памяти много, поэтому увеличение RAM ему не поможет; возможный будущий рычаг — 0,5 vCPU или более лёгкое вычисление KCP.
- Hybrid KCP memory-bound раньше KCP. У него две ONNX-сессии и тяжёлый поиск; вероятно, этим объясняется больший RSS. Это вывод по архитектуре и графику, а не доказанная декомпозиция памяти.
- Память Hybrid росла в первой половине окна, поэтому 407 MiB нельзя пока считать окончательным plateau. Нужно наблюдать несколько часов и после нескольких GC.
- Краткие пики CPU до 100% допустимы. Нежелательны устойчивые 100%, рост памяти выше 85–90%, рестарты/OOM и ухудшение webhook tail latency.
9.2. Доставка ходов Play
Снимок сделан сразу после ресурсного окна. Percentiles Play — приближения по логарифмическим latency buckets, а не точные миллисекунды.
| Метрика за 24 часа | KCP One Ply | Hybrid KCP |
|---|---|---|
| Deliveries | 88 | 183 |
| Applied | 88 | 182 |
| Refused | 0 | 1 |
| p50 | 5 s | 5 s |
| p90 | 60 s | 10 s |
| p99 | 60 s | 20 s |
| Последняя ошибка | нет | room refused the moves, 07:19 UTC |
Единственный refused Hybrid относится к контролируемому проверочному сценарию:
партия была завершена resign, пока вычисление уже шло, и вернувшийся ход попал в
закрытую room. Это не текущая недоступность endpoint. После него идут успешные
applied deliveries.
У KCP нет ошибок, но p90/p99 в 60-секундном bucket согласуется с CPU saturation. Это главный показатель для наблюдения: даже легальный ход, пришедший через минуту, съедает слишком много игровых часов. По 88 deliveries нельзя решать вопрос о новом тарифе окончательно, но сигнал уже есть.
9.3. Ранний ladder snapshot — не оценка силы
На 14.08.2026 около 11:25 Europe/Riga:
| Бот | Rated games | W-D-L | Rating | RD | Provisional |
|---|---|---|---|---|---|
| KCP One Ply | 11 | 9-0-2 | 1918,3 | 126,7 | да |
| Hybrid KCP | 18 | 7-0-11 | 1623,9 | 102,5 | нет по текущему profile |
Это только operational smoke: боты получают партии, ходы применяются и рейтинг обновляется. Выборка мала, соперники неодинаковы, Dice Chess содержит высокий шум. Сравнивать алгоритмы нужно после существенно большей серии и желательно через контролируемые mirrored pairings/SPRT, а не по этим первым W-L.
10. Как повторить замеры
Service configuration без вывода webhook secret
northflank get service \
--projectId dice-chess-onnx-bots \
--serviceId kcp-one-ply \
--output json --quiet |
jq 'del(.runtimeEnvironment.DICECHESS_WEBHOOK_SECRET)'Runtime logs
northflank get service logs \
--projectId dice-chess-onnx-bots \
--serviceId hybrid-kcp \
--types runtime \
--lineLimit 100root_search_stats появляется только у expectimax и только когда не сработала opening
book. Поля candidatesCompleted, deadlineTruncated и fellBackToPreRank показывают,
успевает ли Hybrid реально пересчитать свой candidate shortlist.
CPU/RAM за час, только текущий pod
northflank get service metrics \
--projectId dice-chess-onnx-bots \
--serviceId hybrid-kcp \
--duration 3600 \
--metricTypes cpu memory \
--output json --quiet |
jq '
def latest($m): ($m.values | max_by(.data[-1].ts));
def stats($s):
($s.data | map(.value)) as $v |
{
container: $s.metadata.containerId,
from: $s.data[0].ts,
to: $s.data[-1].ts,
samples: ($v | length),
latest: $v[-1],
average: ($v | add / length),
p50: ($v | sort | .[((length * 0.50) | floor)]),
p90: ($v | sort | .[((length * 0.90) | floor)]),
max: ($v | max)
};
{cpu_pct: stats(latest(.cpu)), memory_pct: stats(latest(.memory))}
'Play delivery stats
bot_token_value=$(security find-generic-password \
-s dicechess-play-bot-token \
-a northflank/hybrid-kcp-1 -w)
curl --fail --silent --show-error "$PLAY_BASE/bot/webhook/stats" \
-H "Authorization: Bearer ${bot_token_value}" | jq
curl --fail --silent --show-error "$PLAY_BASE/bot/capacity" \
-H "Authorization: Bearer ${bot_token_value}" | jq
unset bot_token_valueПубличный profile не требует token:
curl --fail --silent --show-error \
"$PLAY_BASE/bots/northflank/hybrid-kcp-1" | jq11. Стоимость и граница бесплатного использования
nf-compute-20 стоит 10,80/мес, с латвийским НДС 21% — $13,07/мес, плюс
возможные egress и storage.
Исправлено 17.08.2026: фактически это не $0
Прежняя редакция утверждала, что два service-слота покрываются бесплатным планом и «ожидаемая compute-стоимость этой конфигурации — **1,14 + НДС по полным PAYG-ставкам, без единой скидочной строки.
Соответствует ли это заявленным условиям тарифа Developer — открытый вопрос: официально он значится бесплатным. Разбор противоречия и что с ним делать — в §5.1. Планировать в любом случае надо от $13,07/мес, а не от нуля.
Форма лимита описана верно: это не пул «N CPU-часов», который незаметно выгорает от частой игры, а два разрешённых сервиса фиксированного малого размера, 0,2 shared vCPU и 512 MiB на контейнер. Неверным был вывод из этой формы. Слот ограничивает сколько сервисов можно создать — но в нашем случае не сделал их бесплатными, и оплачивалось всё время жизни контейнера, а не время игры. Простаивающий бот стоил столько же, сколько играющий.
Карта — не индикатор тарифа, следим отдельно
Карта требуется на любом плане и служит проверкой личности (§5.1), поэтому по её наличию нельзя судить, платный ли текущий тариф, — но и защиты от списания она не даёт: в нашем случае списание произошло. Дисциплина: не подтверждать upgrade вслепую, не создавать третий сервис, включить billing alert (сейчас нет ни одного) и опустить порог с дефолтных 13,07/мес**, а не ноль; нулём она становится только при
instances: 0.
Почему текущая работа подходит Sandbox
- проект некоммерческий;
- боты — исследовательские варианты модели, а не продаваемый сервис;
- нет SLA и обещания доступности;
- падение обоих ботов не ломает Play — перестают играть только две identity;
- цель запуска — измерить качество алгоритмов и пригодность маленького runtime;
- заняты ровно два предусмотренных service-слота (бесплатность которых опровергнута фактическим списанием — §5.1).
Сохраняем эту формулировку как контекст решения. Если статус проекта или назначение площадки изменятся, оценку условий нужно сделать заново.
12. Какие боты разумно размещать здесь дальше
Northflank Sandbox — дефицит из двух маленьких, но always-on слотов. Его лучше отдавать ботам, которые:
- укладываются в 512 MiB с запасом хотя бы 25–30%;
- отвечают быстро на 0,2 shared vCPU;
- не хранят локальное состояние и не требуют volume/database;
- играют только одну партию одновременно;
- имеют linux/amd64 образ и публичный HTTPS webhook;
- не являются критичной production-зависимостью.
Кандидаты на будущее — лёгкие book/greedy/raw-board/небольшие one-ply стратегии. Тяжёлый expectimax, полный KCP DFS и Monte Carlo логичнее держать там, где есть целое ядро или фиксированная машина: homelab, Cloud Run на время кредита, Azure/другой PaaS для canary либо дешёвый VPS.
Практические пороги пересмотра
Переносим или увеличиваем ресурс, если выполняется хотя бы одно:
- память устойчиво выше 85% или продолжает монотонно расти;
- есть OOM/restarts;
- CPU p90 постоянно около 100% и webhook p90 остаётся в 60-секундном bucket;
- появляются
timed_out,http_5xx,unreachable; - алгоритм нужно пустить более чем в одну одновременную партию;
- Developer Sandbox перестаёт соответствовать назначению проекта.
Текущая предварительная классификация:
- KCP One Ply: по памяти подходит отлично, по CPU — на границе;
- Hybrid KCP: по CPU подходит, по памяти — на границе;
- поэтому пока оставляем оба, но не считаем sizing закрытым вопросом.
13. Обновление, rollback и остановка
Обновление
- Сначала merge публичного кода и зелёный CI.
- Собрать новый public linux/amd64 image.
- Добавить private assets отдельным слоем и проверить SHA-256.
- Push нового immutable commit tag в private GAR.
- Обновить один Northflank сервис как canary.
- Проверить pull, startup logs, 147-entry book, handshake, smoke game и метрики.
- Только затем обновить второй сервис.
- Старый tag/digest сохранить до успешной серии партий.
Остановка без рейтингового ущерба
Сначала убрать identity с ladder, затем остановить compute:
bot_token_value=$(security find-generic-password \
-s dicechess-play-bot-token \
-a northflank/kcp-one-ply-1 -w)
curl --fail --silent --show-error -X POST "$PLAY_BASE/bot/ladder/leave" \
-H "Authorization: Bearer ${bot_token_value}" | jq
unset bot_token_value
northflank pause service \
--projectId dice-chess-onnx-bots \
--serviceId kcp-one-plyНе делать наоборот: ladder продолжит создавать партии для недоступного endpoint, бот получит timeout losses и может быть auto-parked только после нескольких поражений.
Rollback
ladder/leaveдля затронутой identity.- В deployment вернуть предыдущий известный tag или digest.
- Проверить, что GAR credential по-прежнему pull-ит image.
- Проверить startup logs и webhook stats.
ladder/joinтолько после smoke game.
Webhook не нужно регистрировать заново, если публичный URL и secret не менялись.
14. Что ещё нужно довести до зрелого runbook
- проверить долговечность Dice Chess GAR credential контролируемым redeploy;
- поставить billing alert на минимальную сумму;
- снять метрики после нескольких часов/суток, особенно memory plateau Hybrid;
- сохранить manifest происхождения
oracle-3.onnxрядом с моделью, как уже сделано для KCP; - через достаточную серию партий сравнить стратегии контролируемым методом;
- после эксперимента решить, какие две identity достойны занимать бесплатные слоты.
Источники и дата актуальности
Снимок конфигурации и метрик — 14.08.2026. Тарифы и условия могут измениться.