JavaRush /Курси /ChatGPT Apps /Масштабування та розгортання: балансування, кластери беке...

Масштабування та розгортання: балансування, кластери бекенд‑сервісів, blue/green і canary

ChatGPT Apps
Рівень 16 , Лекція 3
Відкрита

1. Про що ця лекція і чому це важливо

Уявіть, що ви зупинилися на етапі, коли GiftGenius самотньо живе на Vercel: один інстанс MCP Gateway (який одночасно реалізує MCP назовні й звертається до ваших REST‑сервісів), один бекенд для агентів — і все «якось працює». Це ще терпимо для пет‑проєкту й перших 100 користувачів.

Але щойно OpenAI додасть ваш застосунок у Store і він раптово потрапить у головну підбірку перед Різдвом, «один gateway на 3000‑му порту» перетвориться на дуже сумну історію. Почнуться черги tool‑викликів, тайм‑аути, помилки 500, падіння рейтингу в Store і листи від маркетингу в стилі: «А чому все лежало в пік продажів?».

Наше завдання в цій лекції — навчитися мислити про GiftGenius (і будь‑який застосунок ChatGPT) як про систему з багатьма однаковими інстансами за балансувальником. Також розберемо акуратні стратегії релізів і зрозуміємо, як швидко відкочуватися, якщо щось піде не так.

2. Горизонтальне масштабування і stateless‑дизайн

Почнемо з базової ідеї: якщо ваш MCP Gateway або внутрішній бекенд‑сервіс зберігає важливий стан у памʼяті конкретного процесу, нормально масштабувати його горизонтально майже неможливо.

Вертикальне vs горизонтальне масштабування

Спершу розберімося з термінами.

Вертикальне масштабування — це коли ви просто «накручуєте мʼязи» одному серверу: більше CPU, більше RAM. Це швидко (інколи й недорого на старті), але має жорстку межу. До того ж один інстанс стає single point of failure: якщо цей потужний монстр падає, падає все.

Горизонтальне масштабування — це коли ви запускаєте кілька екземплярів сервісу за балансувальником. Кожен інстанс відносно невеликий, не тримає нічого критичного в памʼяті, а стан живе у зовнішніх сховищах (Postgres, Redis, object storage). Ви можете вільно додавати й прибирати інстанси під навантаження.

Для MCP Gateway і бекенд‑сервісів (Gift REST API, Commerce REST API, Analytics Service / REST API тощо) горизонтальне масштабування фактично обовʼязкове. ChatGPT може раптово надіслати вам у рази більше трафіку (сезон, промо в Store, якийсь вірусний TikTok). Тож ви маєте просто додати інстанси, а не «молитися, щоб один сервер витримав».

Що таке stateless‑сервіс у контексті MCP Gateway і бекендів

Щоб горизонтальне масштабування працювало, сервіс має бути максимально stateless.

Stateless у нашому контексті означає:

  • сервіс не зберігає в памʼяті унікальний, довготривалий користувацький стан, від якого залежить бізнес‑логіка;
  • будь‑який важливий стан зберігається у зовнішній БД, черзі, кеші, S3‑подібному сховищі;
  • якщо конкретний інстанс упав, інший інстанс може продовжити обслуговувати користувача, просто «підхопивши» контекст із зовнішнього сховища.

Для GiftGenius це означає, що:

  • історія підборів подарунків користувача, його лайки/дизлайки й кошик лежать, наприклад, у Postgres;
  • черги тривалих задач (масова генерація підбірок, розсилка підбірок електронною поштою) лежать у брокері на кшталт Redis/Cloud Queue;
  • якщо є окремий сервіс для складних агентних workflow, він зберігає чекпойнти та довготривалу памʼять у своєму сховищі, а не в RAM одного процесу.

Інстанс MCP Gateway або будь‑якого бекенд‑сервісу перетворюється на «корову, а не домашнього улюбленця»: його можна безболісно зупинити й розгорнути знову, не втративши бізнес‑дані.

Міні‑приклад: перенесення стану з памʼяті у зовнішнє сховище

Уявімо, що ви колись зробили дуже простий MCP‑tool add_to_cart, який через gateway звертається до внутрішньої логіки. А та зберігає кошик у памʼяті процесу (так‑так, інколи так роблять у демках — і це нормально, доки ви розумієте, що в продакшні так не можна):

// ПОГАНО: кошик у памʼяті процесу бекенд-сервісу
const inMemoryCarts = new Map<string, string[]>();

export async function addToCart(userId: string, sku: string) {
  const cart = inMemoryCarts.get(userId) ?? [];
  cart.push(sku);
  inMemoryCarts.set(userId, cart);
  return cart;
}

Горизонтальне масштабування тут неможливе: один запит потрапить на інстанс A, інший — на інстанс B, і кошики в користувача будуть різні.

Правильний варіант — винести кошик у зовнішню БД або кеш. Умовно (сильно спрощено):

// ДОБРЕ: кошик у зовнішньому сховищі
import { db } from "./db";

export async function addToCart(userId: string, sku: string) {
  await db.cartItems.insert({ userId, sku }); // спрощено
  const cart = await db.cartItems.findMany({ where: { userId } });
  return cart;
}

Тепер неважливо, який саме інстанс бекенд‑сервісу обробляє запит, що прийшов через gateway: кошик єдиний для всіх.

3. Балансування навантаження: як трафік потрапляє в кластери бекенд‑сервісів

Щойно у вас зʼявляється більше одного інстанса сервісу, потрібен хтось, хто розподілятиме запити між ними. Це як диспетчер замовлень у популярній піцерії: курʼєрів багато, клієнтів багато — без чіткої логіки буде хаос.

L4 vs L7, і чому нас переважно цікавить L7

Балансувальник може працювати на різних рівнях:

  • L4 (TCP/UDP) просто перекидає байти від клієнта на один із бекендів, не надто розуміючи, який там протокол;
  • L7 (HTTP) розуміє, що перед ним HTTP‑запит, уміє дивитися на шлях, заголовки, куки, інколи навіть на тіло.

Для архітектури застосунку ChatGPT із MCP Gateway і REST‑сервісами нам майже завжди потрібен L7‑балансувальник: усе спілкується по HTTP/SSE, і хочеться вміти маршрутизувати за шляхом, доменом, заголовками (наприклад, для canary‑релізів), а також робити перевірки стану.

Health checks і виведення «хворих» інстансів із ротації

Балансувальник має періодично перевіряти, що інстанси «живі». Найпростіший спосіб — мати GET /health або /readyz ендпойнт, який повертає 200 OK, якщо все гаразд.

У Node/TypeScript‑сервісі, який працює як MCP Gateway або бекенд, health check може виглядати так:

// apps/gateway/src/http/health.ts
import { type Request, type Response } from "express";

export function healthHandler(req: Request, res: Response) {
  res.json({
    status: "ok",
    version: process.env.RELEASE_ID ?? "dev",
  });
}

Балансувальник звертається кожні N секунд до /health. Якщо відповіді починають приходити з 5xx або за тайм‑аутом, цей інстанс прибирається з ротації, і нові запити туди більше не потрапляють.

Особливості для streaming / SSE

MCP Gateway доволі часто працює через SSE (Server‑Sent Events), особливо якщо ви використовуєте стримінг часткових результатів. Балансувальник має:

  • підтримувати довготривалі HTTP‑зʼєднання;
  • уміти враховувати такі зʼєднання під час вибору інстанса (деякі балансувальники зважають на кількість активних зʼєднань, а не лише на RPS).

Це важливо, тому що один «балакучий» tool‑виклик, який передає текст потоком 2 хвилини, висить як активне зʼєднання. Якщо таких зʼєднань забагато на одному інстансі, його потрібно тимчасово «розвантажувати» — надсилати нові зʼєднання на інші.

4. Кластери бекенд‑сервісів: розділяємо за задачами, а не звалюємо все в одну купу

Логічний наступний крок — перестати думати про один «великий бекенд‑сервіс» і розбити систему на кілька кластерів залежно від характеру навантаження та критичності.

Приклад архітектури GiftGenius за кластерами

Усі зібрані дані за модулем 16 рекомендують нам таку схему для GiftGenius:

Кластер Що робить Характер навантаження Особливості масштабування
A: Gift REST API / легкі інструменти Пошук товарів, форматування списків, прості обчислення Високий RPS, короткі відповіді (< 500 мс), мало CPU Масштабуємо за CPU/RPS: багато дрібних інстансів
B: Agents / Heavy Jobs REST‑сервіс Виклики LLM, складні workflow, генерація привітань Низький RPS, довгі відповіді (10 с–2 хв), IO‑heavy Масштабуємо за довжиною черги завдань: можна використовувати воркери
C: Commerce REST API / ACP Checkout, інтеграція з платіжним провайдером, ACP Критична надійність, жорсткі SLO Окреме розгортання, повільні й обережні зміни

По суті, це реалізація патерна bulkheads (відсіки): якщо кластер B раптово починає «палити CPU токенами» під час генерації складних текстів, кластер C з оплатою продовжить працювати. У нього свій пул ресурсів і своє масштабування.

Як це виглядає через Gateway

MCP Gateway, описаний у першій лекції модуля, бачить увесь вхідний MCP‑трафік і маршрутизує його за бекенд‑кластерами. Приблизно так:

  • tool‑виклики list_gifts, suggest_gifts → кластер A (Gift REST API);
  • tool‑виклики generate_greeting_card або складні agent‑workflow → кластер B (Agents REST‑сервіс або воркери);
  • інструменти create_order, confirm_payment → кластер C (Commerce REST API).

За цим уже може стояти один спільний балансувальник або кілька балансувальників (наприклад, окремий L7‑LB перед commerce, щоб ще сильніше все ізолювати).

Можна зобразити загальну картинку:

flowchart LR
    ChatGPT((ChatGPT))
    GW[MCP Gateway]
    LBA[LB Gift API Cluster A]
    LBB[LB Agents/Workers Cluster B]
    LBC[LB Commerce API Cluster C]

    A1[Gift REST API A-1]
    A2[Gift REST API A-2]
    B1[Agents Service B-1]
    B2[Agents Service B-2]
    C1[Commerce REST API C-1]
    C2[Commerce REST API C-2]

    ChatGPT --> GW
    GW -->|tools: gifts| LBA
    GW -->|agents workflows| LBB
    GW -->|commerce| LBC

    LBA --> A1
    LBA --> A2
    LBB --> B1
    LBB --> B2
    LBC --> C1
    LBC --> C2

Схема дещо ідеалізована, але показує головний принцип: різні типи навантаження — різні бекенд‑кластери за одним MCP Gateway.

5. Стратегії розгортання: навіщо потрібні blue/green і canary

Тепер перейдемо до того, як оновлювати все це господарство так, щоб користувачі нічого не помічали, а ви могли спокійно спати вночі.

Антиприклад: розгортання «поверх продакшена»

Найпростіша й водночас найнебезпечніша стратегія: ви берете чинний кластер (наприклад, кластер Gift REST API A), запускаєте новий образ поверх старого, підміняєте контейнери або перезапускаєте процеси.

У чому проблеми:

  • поки частина інстансів уже нова, а частина — стара, система може поводитися непередбачувано (особливо якщо змінювалася схема БД);
  • якщо щось піде не так, відкат — це нове розгортання «як було», яке може займати хвилини;
  • у момент деплою цілком можливий короткий простій, коли жоден інстанс ще не запустився.

У Kubernetes і PaaS це трохи помʼякшується rolling‑оновленнями, але загальна ідея та сама: без чіткої стратегії у вас багато «сірої зони», де різні версії коду одночасно обробляють трафік.

Blue/Green‑розгортання: два середовища і миттєве перемикання

Blue/Green — це підхід, за якого у вас одночасно існують два майже ідентичні оточення: Blue (поточний продакшн) і Green (нова версія).

Схематично процес виглядає так:

  1. Розгортаєте нову версію (v2) у Green‑оточенні: це такий самий набір gateway + бекенд‑кластерів, тільки поки без реального трафіку.
  2. Проганяєте на Green усі потрібні тести: автотести, smoke‑сценарії, ручні перевірки через ChatGPT Dev Mode.
  3. У момент релізу перемикаєте балансувальник/маршрутизацію так, щоб 100 % бойового трафіку йшло в Green.
  4. Blue продовжує жити поруч як «запасний аеродром». Якщо щось піде не так, перемикаєте трафік назад за лічені секунди.

Для GiftGenius це може виглядати так: у вас є mcp-gateway-blue.example.com і mcp-gateway-green.example.com. У продакшні застосунок ChatGPT «дивиться» на офіційний MCP‑endpoint (gateway), а під час релізу ви змінюєте конфіг DNS/LB так, щоб доменне імʼя mcp-gateway.example.com вказувало вже на green.

Плюси:

  • миттєвий перемикач «туди‑сюди»;
  • будь‑яку проблему можна розбирати вже після відкату;
  • немає стану «пів кластера нова, пів кластера стара».

Мінуси:

На час релізу потрібно тримати два повних оточення, тобто оплачувати ресурси ×2. Тому таку стратегію найчастіше застосовують для критичних бекенд‑сервісів — наприклад, commerce‑кластера C і самого MCP Gateway, де ламати checkout і вхідну точку не можна за жодних обставин.

Canary‑релізи: маленька «канарка» у вугільній шахті

Canary‑реліз — економніший варіант: ви не піднімаєте два повних продакшни, а викочуєте нову версію поступово, на невелику частку трафіку, й уважно за нею спостерігаєте.

Приблизний сценарій:

  1. Деплоїте версію v2 кластера Gift REST API A у той самий пул або в окремий невеликий канарковий пул.
  2. Налаштовуєте балансувальник або MCP Gateway так, щоб, скажімо, 1 % tool‑викликів, повʼязаних із подарунками, йшло на v2, а 99 % — на v1.
  3. Дивитеся на метрики: частку помилок (error rate), затримку (latency), специфічні бізнес‑метрики (конверсія, успішні checkout‑и).
  4. Якщо все добре — поступово збільшуєте частку: 1 % → 5 % → 10 % → 50 % → 100 %. Якщо погано — терміново відкочуєте.

У контексті застосунків ChatGPT canary особливо корисний не лише для коду, а й для експериментів із промптами: нова версія system‑promptʼа для agent‑сервісу може радикально змінити поведінку, тож краще спочатку перевірити її на невеликій вибірці користувачів.

Gateway або LB можуть визначати, який запит вважати «канарковим», за різними ознаками:

  • випадково (наприклад, 1 % усіх запитів);
  • за userId (частина користувачів потрапляє в експеримент назавжди);
  • за спеціальним заголовком або cookie (для внутрішнього тестування).

Невеликий приклад логіки маршрутизації в псевдо‑TypeScript (для ілюстрації ідеї в gateway):

// Псевдокод у Gateway: simple random canary 5%
function routeToGiftBackendCluster(ctx: { userId?: string | null }) {
  const rnd = Math.random();
  if (rnd < 0.05) {
    return "gift-api-v2"; // canary
  }
  return "gift-api-v1";   // stable
}

На практиці ви, звісно, не робитимете це через Math.random() у runtime‑коді, а винесете правила в конфіг/feature flags. Але логіка дуже схожа: частина трафіку йде на canary‑версію бекенд‑сервісу, решта — на стабільну.

6. Rollback як обовʼязкова частина стратегії

Колись давно я засвоїв хороше правило: відкат має бути швидшим за фікс.

Це означає, що якщо після релізу посипалися помилки й користувачі пишуть «все ламається», не треба героїчно лагодити баг у продакшні. Треба натиснути велику червону кнопку «відкотитися».

У контексті платформ на кшталт Vercel (на яких ми вже розгортали Next.js‑частину GiftGenius) це дуже природно: кожен деплой — immutable артефакт, і Vercel дозволяє швидко відкотитися до попереднього.

Для MCP Gateway і бекенд‑кластерів, розгорнутих у Kubernetes або іншому оркестраторі, цю роль виконує kubectl rollout undo: ви відкочуєтеся до попереднього набору podʼів і образів.

Головне — логувати й показувати версію, яка зараз обслуговує трафік. Наприклад, можна:

  • додавати version у /health та інші діагностичні ендпойнти (ми вже це робили вище);
  • прокидати ідентифікатор релізу через заголовки в логи (наприклад, X-Release-Id).

Міні‑приклад: Next.js‑API‑route, який віддає версію збірки для інспекції застосунку ChatGPT всередині віджета:

// apps/web/app/api/version/route.ts
export async function GET() {
  return Response.json({
    version: process.env.RELEASE_ID ?? "dev",
    builtAt: process.env.BUILT_AT ?? "unknown",
  });
}

Такий ендпойнт корисний і для відладки: ви можете запитувати в прод‑інстанса, яка саме версія зараз працює, і не гадати: «А чи точно викотився останній білд?».

7. Capacity planning: скільки інстансів потрібно під GiftGenius

Ми вже обговорили, як безпечно викочувати нові версії (blue/green, canary) і швидко відкочуватися у разі проблем. Залишилося практичне питання: скільки взагалі інстансів і яких кластерів тримати в продакшні, щоб усе це витримало реальний трафік і не розорило вас?

Без фанатизму у формули, але трохи порахувати доведеться. Масштабування слід повʼязувати з навантаженням і економікою: скільки запитів на день/секунду, скільки важких LLM‑викликів — і скільки все це коштує.

Для простоти можна мислити порядками:

  • за 10 тис. запитів на день до GiftGenius (приблизно 0,1 RPS у середньому) ви легко проживете на одному‑двох інстансах MCP Gateway і парі інстансів Gift REST API/Agents‑воркерів;
  • за 100 тис. запитів на день (12 RPS у середньому, а в піку — більше) уже варто мати 35 інстансів gateway + кластера Gift REST API, окремий кластер B для важких агентів і виділений commerce‑кластер;
  • за 1 млн запитів на день (десятки RPS, пікові навантаження у свята) вам точно знадобляться кластери, виділені ресурси під LLM‑агентів, агресивний кеш і edge‑шар (про нього — в окремій лекції).

Це не суворі числа, а спосіб привчити себе оцінювати порядок навантаження й думати наперед: де вузькі місця, як ви масштабуватиметеся і скільки це коштуватиме.

Для GiftGenius особливо важливо готуватися до свят: Новий рік, Різдво, День святого Валентина, Чорна пʼятниця. Навантаження може зрости в рази, і вам хотілося б, щоб система це витримала.

8. Практичний міні‑приклад: еволюція розгортання GiftGenius

Щоб зібрати все докупи, намалюймо просту еволюцію розгортання GiftGenius.
Тут ми послідовно застосуємо все, про що говорили вище: stateless‑дизайн gateway і бекенд‑сервісів, балансування навантаження, окремі кластери та стратегії релізів (blue/green, canary).

Базовий рівень: один gateway + бекенд на Vercel/Kubernetes

У якийсь момент курсу ви вже це зробили: один Next.js‑додаток з Apps SDK на Vercel, у якому живе і MCP‑endpoint, і проста бекенд‑логіка (Gift/Commerce) в одному сервісі. Усе доволі монолітно.

Плюси зрозумілі: просто, дешево, мало місць, де можна помилитися.

Мінус рівно один, але критичний: це ніяк не масштабується під серйозний трафік і погано переносить оновлення.

Рівень 2: окремий MCP Gateway + кілька бекенд‑кластерів

Наступний крок:

  • виносите MCP Gateway в окремий сервіс (Node/Go/NGINX+Lua — не має значення);
  • запускаєте кілька інстансів Gift REST API (кластер A) і кілька воркерів/сервісів для агентів (кластер B);
  • для commerce виділяєте окремий сервіс (кластер C), можливо — на окремій базі/інфраструктурі.

Уже тут вмикається класичне L7‑балансування, перевірки стану і, за можливості, горизонтальне масштабування.

Рівень 3: стратегії релізів

На цьому рівні ви додаєте:

  • Blue/Green для commerce‑кластера C (і, за бажання, для MCP Gateway), щоб checkout і авторизація були максимально стабільними;
  • Canary‑релізи для кластерів Gift REST API й agent‑сервісу, щоб спокійно експериментувати з новими версіями tool‑ів і агентів без ризику «покласти» весь продакшн.

Схематично:

flowchart LR
    ChatGPT((ChatGPT))
    GWBlue[Gateway Blue]
    GWGreen[Gateway Green]
    LB[Traffic Switch]

    subgraph Prod
      LB --> GWBlue
      LB -.canary,% .-> GWGreen
    end

    ChatGPT --> LB

У реальності може бути трохи складніше (окремий Blue/Green лише для commerce, canary — тільки для gift‑кластерів), але ідею це ілюструє: ви завжди знаєте, яка версія куди йде. При цьому для ChatGPT усе, як і раніше, виглядає як одна MCP‑точка входу (gateway).

9. Невеликі фрагменти коду для версіонування і діагностики

Ми вже бачили health‑ендпойнт і /api/version. Додамо ще один приклад: як можна логувати версію і кластер в обробнику MCP‑toolʼа на боці gateway, щоб потім легко «зводити» метрики.

Уявімо tool suggest_gifts, який реалізований як REST‑ендпойнт у Gift REST API і викликається через gateway:

import { type McpToolHandler } from "@modelcontextprotocol/sdk";

export const suggestGifts: McpToolHandler<{
  occasion: string;
  budget: number;
}> = async ({ input, meta }) => {
  const releaseId = process.env.RELEASE_ID ?? "dev";
  const clusterId = process.env.CLUSTER_ID ?? "gift-api-A";

  console.log("[suggest_gifts]", {
    releaseId,
    clusterId,
    userId: meta.userId,
    occasion: input.occasion,
  });

  // тут MCP Gateway за таблицею маршрутизації викликає Gift REST API,
  // а сам інструмент лишається тонкою обгорткою над REST-викликом
  return {
    content: [{ type: "text", text: "Gift ideas..." }],
  };
};

Тут ми:

  • читаємо RELEASE_ID і CLUSTER_ID з env;
  • пишемо їх у структуровані логи;
  • далі їх легко використовувати для аналізу: «На якій версії/кластері в нас зараз сиплеться більше помилок?».

З погляду застосунку ChatGPT це взагалі прозоро, а для вас як розробника — великий плюс. Особливо в поєднанні з canary/blue‑green.

10. Типові помилки під час масштабування і розгортання застосунку ChatGPT

Помилка №1: зберігати стан сесії/користувача в памʼяті gateway або бекенд‑процесу.
Такий підхід убиває горизонтальне масштабування: щойно зʼявляється другий інстанс, стан «розшаровується» між ними. Особливо небезпечно зберігати в памʼяті кошик, результати пошуку або прогрес workflow. Усе це має жити у зовнішньому сховищі — БД, кеші або спеціалізованому сховищі для стану агента.

Помилка №2: думати, що «одного потужного сервера» достатньо.
Вертикальне масштабування зручне на старті, але погано працює за реального зростання: є фізична межа машини, один процес стає single point of failure, а ChatGPT може принести непередбачуваний сплеск трафіку. Для MCP Gateway і бекенд‑кластерів майже завжди потрібні stateless‑дизайн і кілька інстансів за балансувальником.

Помилка №3: викочувати нові версії «поверх продакшена» без чіткої стратегії.
Якщо ви просто оновлюєте контейнери/процеси в бойовому кластері, отримуєте проміжний стан, де частина трафіку йде на стару версію, а частина — на нову. А у випадку помилки відкат перетворюється на «перерозгорнути ще раз». Значно надійніше тримати або два оточення (blue/green), або принаймні окрему canary‑версію бекенд‑сервісу, куди йде мала частка трафіку.

Помилка №4: відсутність швидкого rollback‑плану.
Поганий сценарій: реліз пройшов, метрики «червоні», користувачі скаржаться, а ви лише починаєте думати, як відкотитися. Правильний сценарій: заздалегідь підготовлена можливість миттєвого відкату (blue/green‑перемикач, rollout undo, Vercel rollback), зрозумілі ідентифікатори версій у логах і health‑ендпойнтах, і жорстке правило «відкотитися спочатку, розбиратися потім».

Помилка №5: один спільний кластер «на все» без поділу за видами навантаження.
Якщо генерація вітальних текстів (LLM‑агенти) і checkout живуть в одному кластері, будь‑яка проблема на боці моделей (затримки, тайм‑аути, зростання токенів) може «покласти» й оплату. Поділ на кластери за типами завдань (Gift REST API / легкі інструменти, Agents‑heavy сервіс, Commerce REST API) та окремі ліміти/ресурси для кожного кластера — важливий крок до стійкості.

Помилка №6: відсутність звʼязку між архітектурою й економікою.
Легко захопитися ідеєю «А давайте ще піднімемо пару нод», забувши, що кожен LLM‑виклик і кожен інстанс коштують грошей. Без найпростішого capacity planningʼу (оцінки навантажень і вартості) можна або недомасштабуватися й «упустити» продакшн, або перемасштабуватися і втратити маржинальність. Тут корисно повʼязувати кількість запитів, відсоток важких LLM‑операцій і вартість хостингу з бізнес‑метриками застосунку.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ