Архітектурні рішення
Архітектурні проєктні рішення
Ця сторінка пояснює обґрунтування ключових архітектурних та технологічних рішень у 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, MCPtools/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()бачив цю роль, так само як уже працює MCPtools/list.