Архітектурні рішення

10 хв читання

Архітектурні проєктні рішення

Ця сторінка пояснює обґрунтування ключових архітектурних та технологічних рішень у PgArachne. Ці рішення були прийняті, щоб надати пріоритет продуктивності, безпеці та продуктивності розробників, водночас забезпечуючи, що система залишається високо сумісною з сучасними AI та LLM-агентами.

1. Чому PostgreSQL

PgArachne свідомо побудований виключно на PostgreSQL і не прагне бути незалежним від бази даних. Більшість того, що пропонує шлюз, — це не власна логіка на Go, а безпосереднє використання можливостей PostgreSQL.

Чому PostgreSQL:

  • Вбудована модель прав доступу: Ролі, GRANT/REVOKE та право EXECUTE на функції є частиною бази даних. Тому PgArachne не потребує власного шару авторизації — він лише перемикає роль за допомогою SET LOCAL ROLE, а решту бере на себе PostgreSQL. Правила доступу однаково діють для API, psql та будь-якого іншого інструмента.
  • Row-Level Security: Політики на рівні рядків обчислюються залежно від ролі, від імені якої виконується виклик. Ізоляція даних між користувачами чи тенантами міститься в базі даних, а не в коді шлюзу.
  • Нативний JSON (jsonb): Контракт функція(jsonb) → json точно відповідає тілу запиту та відповіді JSON-RPC. Параметри не потрібно відображати на типи чи генерувати обгортки — JSON проходить крізь шлюз без змін.
  • Транзакційні гарантії: Кожен виклик виконується в одній транзакції з повними гарантіями ACID. Функція може атомарно записувати в кілька таблиць, а у разі помилки все відкочується, і шлюзу не потрібно нічого про це знати.
  • Потужна модель програмування: PL/pgSQL, SQL-функції та інші процедурні мови дозволяють писати бізнес-логіку там, де зберігаються дані, без передавання проміжних результатів мережею.
  • LISTEN/NOTIFY: Сповіщення в реальному часі (рішення 5) ґрунтуються безпосередньо на механізмі PostgreSQL. Зовнішній брокер повідомлень не потрібен.
  • Інтроспекція системного каталогу: Список методів, які можна викликати, їхні описи та схеми параметрів читаються з pg_proc і коментарів до функцій. Із єдиного джерела істини утворюються capabilities, MCP tools/list та специфікація OpenAPI, тож вони не можуть розійтися з реальністю.
  • Екосистема розширень: PostGIS, pgvector, TimescaleDB, pg_trgm та інші розширення одразу доступні як звичайні SQL-функції, а отже й як методи API та інструменти для AI-агентів — без жодного рядка коду на Go.
  • Відкритість і зрілість: Вільна ліцензія без прив’язки до постачальника, десятиліття перевіреної експлуатації, активна розробка та доступність у всіх великих хмарних провайдерів, а також як самостійно розгорнутий екземпляр.

Чому не інша база даних чи незалежний від БД шар:

  • Найменший спільний знаменник: Підтримка кількох баз даних означала б відмову саме від тих можливостей, на яких тримається PgArachne, — моделі безпеки на основі ролей, jsonb, LISTEN/NOTIFY та інтроспекції функцій. Залишився б тонкий і менш безпечний шар над універсальним SQL.
  • MySQL/MariaDB: Не мають нативного Row-Level Security та аналога LISTEN/NOTIFY, а їхня підтримка JSON і процедурної логіки більш обмежена.
  • SQL Server та Oracle: Пропрієтарне ліцензування та експлуатаційні витрати суперечать меті простого безкоштовного розгортання одним бінарним файлом.
  • NoSQL-бази даних (документні, key-value, колонкові): Масштабування та гнучкість схеми, заради яких обирають NoSQL, не є для PgArachne головною перевагою. Потрібне документне сховище покриває jsonb з індексацією та запитами — поряд із реляційними даними й у тій самій транзакції. Натомість бракує того, на чому стоїть PgArachne: серверних функцій, які можна викликати з гранулярним правом EXECUTE для конкретної ролі, Row-Level Security, інтроспекції функцій із каталогу та єдиної мови запитів. Без них шлюзу довелося б самому вирішувати авторизацію, валідацію й опис API — і він перестав би бути тонким шаром.
  • SQLite: Це вбудована база даних без серверної моделі ролей і прав доступу, на якій побудовано всю безпеку PgArachne.

Наслідок такий: PgArachne залишається невеликим — безпеку, транзакції, сповіщення та виявлення API він делегує PostgreSQL і сам відповідає лише за трансляцію протоколів та автентифікацію.

2. Функції PostgreSQL як поверхня API

PgArachne свідомо надає доступ до функцій бази даних, а не до необроблених таблиць.

Чому функції:

  • Інкапсуляція: Бізнес-логіка розташована разом з даними в базі даних — одне місце для аудиту, версіювання та захисту.
  • Явна безпека: Через API доступні лише функції, яким явно надано права EXECUTE для конкретної ролі.
  • Абстракція: Валідація вхідних даних, обчислювані поля та складні операції з кількома таблицями приховані від клієнта, що забезпечує чистий інтерфейс.

Чому не CRUD на рівні таблиць:

  • Тісне зв’язування: Прямий доступ до таблиць прив’язує ваш API до внутрішньої схеми бази даних, що ускладнює рефакторинг бази даних без порушення роботи клієнтів.
  • Фрагментація бізнес-правил: Бізнес-логіка в кінцевому підсумку розділяється між обмеженнями бази даних та будь-яким middleware, що використовується для фільтрації HTTP-запитів.

3. Go проти альтернатив

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

Чому Go:

  • Статичні бінарні файли: Компілюється в один бінарний файл без зовнішніх залежностей. Розгортання настільки просте, як копіювання файлу на сервер.
  • Конкурентність (concurrency): Горутини Go роблять обробку тисяч одночасних з’єднань SSE та бази даних легкою та прямолінійною.
  • Надійна стандартна бібліотека: Вбудовані бібліотеки для HTTP, TLS та JSON мають промисловий рівень якості і не вимагають жодних «node_modules» чи зовнішніх середовищ виконання.
  • Крос-компіляція: Легко націлюється на Linux, macOS та Windows (amd64 та arm64) з будь-якої машини розробника.

Чому не Node.js, PHP чи Ruby:

  • Середовища виконання: Вони вимагають встановлення специфічного середовища виконання на кожній цільовій машині.
  • Ефективність: Однопотоковий цикл Node.js або модель «процес на запит» PHP менш ефективні для підтримки тисяч неактивних з’єднань SSE.
  • Обсяг пам’яті: Go використовує значно менше пам’яті на з’єднання, ніж скриптові мови.

Чому не Rust:

  • Швидкість розробки: Хоча Rust пропонує надзвичайну продуктивність, його складність (borrow checker) сповільнює ітерації для інструменту, орієнтованого на I/O, де продуктивності Go вже більш ніж достатньо.

Чому не C/C++:

  • Безпека: Ручне керування пам’яттю додає суттєві ризики безпеки (переповнення буфера) без відчутного приросту продуктивності в застосунку типу шлюзу.

4. JSON-RPC 2.0 проти REST

PgArachne використовує JSON-RPC 2.0 як основний протокол комунікації замість традиційного REST.

Чому JSON-RPC 2.0:

  • Єдина точка входу: Уся комунікація відбувається через POST /{prefix}/{database}/jsonrpc. Немає потреби проєктувати складні структури URL або сперечатися про семантику HTTP-методів.
  • Самодостатні виклики: Кожен запит — це повний JSON-об’єкт (метод + параметри + id). Цей формат тривіально генерується та парситься LLM та AI-агентами з високою надійністю.
  • Стандартизована обробка помилок: Коди та повідомлення про помилки є частиною специфікації, що усуває потребу «вигадувати» конвенції HTTP-кодів статусу для бізнес-помилок.
  • Пакетна обробка (batching): Протокол нативно підтримує пакетні запити, дозволяючи виконати кілька операцій (наприклад, декілька викликів функцій) за один HTTP-раундтрип без додаткової роботи.
  • Виявлення (discovery): Ендпоінт можливостей (capabilities) надає повний опис API у форматі, який AI-агенти можуть споживати, щоб зрозуміти доступні інструменти без галюцинацій.

Чому не REST:

  • Складність для AI: Семантика REST (GET/POST/PATCH/DELETE + параметри URL + тіло запиту) розкидана по кількох місцях, що ускладнює AI-агентам надійне формування викликів.
  • Витік схеми: CRUD над таблицями (як у PostgREST) часто напряму розкриває внутрішню структуру бази даних в API. PgArachne свідомо надає доступ до функцій, зберігаючи бізнес-логіку інкапсульованою в SQL.
  • Відсутність стандартів: REST не пропонує універсального стандарту для пакетних операцій, уніфікованих обгорток помилок або автоматизованого виявлення API.

5. SSE (Server-Sent Events) проти WebSocket

Для сповіщень у реальному часі PgArachne реалізує Server-Sent Events (SSE).

Чому SSE:

  • Звичайний HTTP: SSE — це стандартний HTTP. Він працює через проксі, балансувальники навантаження та CDN без спеціальної конфігурації «оновлення протоколу».
  • Нативна підтримка в браузерах: API EventSource вбудований в усі сучасні браузери та обробляє автоматичне повторне підключення без будь-яких клієнтських бібліотек.
  • Відповідає семантиці NOTIFY: NOTIFY у PostgreSQL є односпрямованим (сервер до клієнта), що ідеально відображається на SSE.
  • Мультиплексування: Через HTTP/2 сотні потоків SSE можуть спільно використовувати одне TCP-з’єднання, що робить це надзвичайно ефективним.
  • Операційна простота: З’єднання SSE виглядають як звичайні HTTP-запити в логах та інструментах моніторингу, що полегшує їх діагностику та обмеження частоти запитів (rate limiting).

Чому не WebSocket:

  • Непотрібна двоспрямованість: Оскільки клієнту ніколи не потрібно надсилати дані назад через канал сповіщень, складність WebSocket не дає жодної переваги.
  • Проблеми з підключенням: WebSocket часто блокується або передчасно закривається корпоративними фаєрволами та деякими хмарними балансувальниками навантаження.
  • Вищі накладні витрати: Додає складність протоколу (handshake, ping/pong фрейми), яка не потрібна для простого потокового передавання подій.

6. Структура URL: /{prefix}/{database}/{endpoint}

PgArachne маршрутизує всі ендпоінти під конфігурованим сегментом префікса: /db/{database}/jsonrpc, /db/{database}/file, /db/{database}/sse, /db/{database}/mcp. За умовчанням префікс — db, і його можна змінити через API_PREFIX.

Чому саме така структура:

  • Маршрутизація через зворотний проксі: Один екземпляр PgArachne може обслуговувати кілька баз даних. Зворотний проксі (Nginx, Caddy, Traefik) може маршрутизувати за префіксом або назвою бази даних без перевірки тіла запиту, що є критично важливим для балансування навантаження та правил маршрутизації на основі шляхів.
  • Горизонтальна масштабованість: З назвою бази даних у шляху URL можна запускати декілька екземплярів PgArachne та спрямовувати трафік до конкретних екземплярів по базі даних, використовуючи стандартні правила проксі — без sticky sessions чи перевірки тіла запиту.
  • Мультиплексування протоколів на базу даних: Групування /jsonrpc, /file, /sse та /mcp під одним простором імен /{prefix}/{database}/ робить природним застосування автентифікації, обмеження частоти запитів та контролю доступу для кожної бази даних на рівні проксі.
  • Налаштовуваний префікс: Розгортання, що вже використовують /api/ як префікс у своїй інфраструктурі, можуть встановити API_PREFIX=api, щоб відповідати своїм конвенціям.
  • Спостережуваність: Агрегатори логів та системи метрик можуть групувати та фільтрувати трафік за назвою бази даних безпосередньо з URL без парсингу тіл JSON.

Чому не плоска структура, як /api/{database}:

  • Неоднозначність протоколу: Один плоский ендпоінт не може розрізнити трафік JSON-RPC, SSE та MCP на рівні маршрутизації — це рішення потрапляє в логіку застосунку або перевірку заголовків.
  • Складніше розширювати: Додавання нових протоколів (наприклад, GraphQL, gRPC-gateway) все одно вимагатиме введення нових шляхів верхнього рівня, тож структурований простір імен убезпечує дизайн на майбутнє.

7. MCP як рівень трансляції, а не протокол бази даних

PgArachne реалізує Model Context Protocol (MCP) як тонкий рівень трансляції в сервері на Go. Функції PostgreSQL ніколи не знають про MCP — вони залишаються простими функціями jsonb → json.

Чому транслювати MCP на сервері:

  • Нуль змін до існуючих функцій: Будь-яка функція, вже надана через JSON-RPC, миттєво стає доступною як інструмент MCP. Жодних змін SQL, жодного повторного розгортання об’єктів бази даних.
  • MCP — це більше, ніж просто інструменти: Протокол включає метод виявлення (server/discover), метадані версії протоколу та можливостей у кожному запиті, сповіщення та розширення (ресурси, промпти) поза межами простих викликів інструментів. Це проблеми на рівні протоколу, які належать до Go, а не до SQL-функцій.
  • Безпека залишається в одному місці: Автентифікація, перемикання ролей та валідація вхідних даних уже реалізовані в Go. Ендпоінт MCP повторно використовує цю логіку без змін.
  • Кілька протоколів, один backend: Ту саму функцію PostgreSQL можна викликати через JSON-RPC (зі звичайного клієнта), MCP (з Claude Desktop або Cursor) або SSE (для підписок на події). База даних не залежить від протоколу.
  • Простіший SQL: Обробка конвертів MCP (server/discover, tools/list, обробка сповіщень) всередині функцій PostgreSQL вимагала б парсингу складних структур JSON у PL/pgSQL, що ускладнило б написання, тестування та підтримку функцій.

Чому не переносити MCP в базу даних:

  • Валідація транспорту MCP не потребує бази даних: server/discover та перевірки версії протоколу/заголовків у кожному запиті — це чисті протокольні повідомлення. Відкриття з’єднання з базою даних для них марнує ресурси та додає латентність.
  • SQL — неправильний інструмент для протокольної логіки: Коди помилок JSON-RPC 2.0, маршрутизація сповіщень та керування ключами ідемпотентності є проблемами middleware, а не проблемами даних.

8. Експорт OpenAPI: віртуальні шляхи, а не новий протокол

GET /{prefix}/{database}/openapi.json (або .yaml) генерує документ OpenAPI 3.1, що описує кожен метод, який автентифікований викликач може виконати. Реальний виклик все ще відбувається виключно через єдиний ендпоінт POST /{prefix}/{database}/jsonrpc — специфікація додатково перелічує кожен метод під власним шляхом, таким як /{prefix}/{database}/rpc/api.hello_world, але цей шлях є лише документацією і не існує як HTTP-маршрут, який можна викликати напряму.

Чому віртуальні шляхи для кожного методу:

  • Сумісність з інструментами: Swagger UI, Postman, Insomnia та більшість генераторів коду OpenAPI очікують одну операцію на кожен запис у paths. Єдиний шлях JSON-RPC інакше не може представити N різних сигнатур методів у спосіб, зрозумілий цим інструментам.
  • Не потребує змін бекенду: Додавання реального маршруту для кожного методу означало б другий спосіб виклику кожної функції, з власною автентифікацією, обробкою помилок та версіюванням, які довелося б підтримувати синхронно з JSON-RPC. Генерування шляхів виключно з capabilities() зберігає поверхню протоколу точно такою, як описано в рішенні 4, водночас задовольняючи інструменти, яким потрібні схеми для кожної операції.
  • Самодокументування: Опис кожної віртуальної операції детально викладає точний запит JSON-RPC (метод + параметри), необхідний для його фактичного виклику, тож відсутність реального маршруту нічого не втрачає.

Чому специфікація автентифікована та відфільтрована за роллю:

  • Узгодженість з рештою API: Кожен інший ендпоінт (JSON-RPC, MCP tools/list) завжди розкриває лише методи, які роль викликача може виконувати. Неавтентифікований, нефільтрований документ OpenAPI розкрив би існування та форми параметрів функцій, які даний викликач насправді не може викликати.
  • Той самий механізм, що й скрізь: Обробник на Go автентифікує за тією ж логікою Basic/JWT/API-токена, що й /jsonrpc, а потім виконує SET LOCAL ROLE перед генеруванням специфікації — pgarachne.generate_openapi_spec() має атрибут SECURITY INVOKER саме для того, щоб її внутрішній виклик capabilities() бачив цю роль, так само як уже працює MCP tools/list.