Архітектура однопоточного сервера з готовністю до масштабування
Концепція та принципи дизайну
Ця архітектура базується на принципах, що забезпечують легкий перехід від однопоточної до багатопоточної обробки:
- Абстракція процесів обробки - відділення логіки обробки від механізму її виконання
- Асинхронність операцій - використання неблокуючих операцій навіть в однопоточному режимі
- Статлес-дизайн - мінімізація стану, що зберігається в пам’яті одного процесу
- Шаблон публікації/підписки - використання системи подій для комунікації між компонентами
Структура однопоточного сервера
Ключові компоненти
/server
server.js // Головний файл сервера
/core
task-processor.js // Процесор завдань (абстракція виконавця)
event-bus.js // Внутрішня шина подій
processing-manager.js // Менеджер обробки завдань
/services
/openai
/wordpress
/google-sheets
/data
database.service.js // Сервіс доступу до бази даних
template.service.js // Сервіс роботи з шаблонами промптів
/api
/routes
/controllers
/middlewares
/utils
result-store.js // Сховище результатів (абстракція)
progress-tracker.js // Відстеження прогресу
/models // Моделі даних для роботи з БД
prompt-template.model.js // Модель шаблону промптів
processing-task.model.js // Модель завдання на обробку
api-key.model.js // Модель для зберігання API ключів
generation-result.model.js // Модель результату генерації
Дизайн компонентів для масштабування
1. Абстракція процесора завдань
// task-processor.js - концептуальний код
class TaskProcessor {
constructor() {
this.processing = false
this.taskQueue = []
}
async addTask(task) {
this.taskQueue.push(task)
if (!this.processing) {
this.processQueue()
}
return task.id // Повертаємо ID для відстеження
}
async processQueue() {
this.processing = true
while (this.taskQueue.length > 0) {
const task = this.taskQueue.shift()
try {
await this.executeTask(task)
} catch (error) {
EventBus.emit("task:error", { taskId: task.id, error })
}
}
this.processing = false
}
async executeTask(task) {
// В однопоточній версії - пряме виконання завдання
// При багатопоточності - відправка в чергу завдань
return await task.execute()
}
}
2. Абстракція сховища результатів
// result-store.js - концептуальний код
class ResultStore {
constructor() {
this.results = new Map()
// В багатопоточній версії буде використовуватись Redis/MongoDB
}
async saveResult(taskId, result) {
this.results.set(taskId, result)
// В однопоточній версії зберігаємо в пам'яті
// У багатопоточній - у зовнішньому сховищі
EventBus.emit("result:saved", { taskId, status: "completed" })
}
async getResult(taskId) {
return this.results.get(taskId)
// У багатопоточній версії - читання з зовнішнього сховища
}
}
3. Система подій для координації
// event-bus.js - концептуальний код
class EventBus {
static listeners = {}
static on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event].push(callback)
}
static emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach((callback) => callback(data))
}
}
}
4. Абстракція доступу до бази даних
// database.service.js - концептуальний код
class DatabaseService {
constructor(config) {
this.connection = null
this.config = config
}
async connect() {
// В однопоточній версії - проста локальна БД, SQLite або локальний MongoDB
// У багатопоточній - підключення до зовнішньої MongoDB/PostgreSQL
// Реалізація залежить від вибору БД
}
async findOne(collection, query) {
// Універсальний метод пошуку, який може працювати з різними БД
}
async find(collection, query, options) {
// Універсальний метод пошуку колекції документів
}
async insert(collection, data) {
// Універсальний метод вставки даних
}
async update(collection, query, data) {
// Універсальний метод оновлення даних
}
async delete(collection, query) {
// Універсальний метод видалення даних
}
}
5. Сервіс шаблонів промптів
// template.service.js - концептуальний код
class TemplateService {
constructor(databaseService) {
this.db = databaseService
}
async getTemplateById(id) {
// Отримання шаблону промпту за ID
return this.db.findOne("promptTemplates", { id })
}
async getTemplateByType(type) {
// Отримання шаблону за типом (стаття, QA, коментарі)
return this.db.findOne("promptTemplates", { type })
}
async saveTemplate(template) {
// Збереження нового шаблону
return this.db.insert("promptTemplates", template)
}
async updateTemplate(id, template) {
// Оновлення існуючого шаблону
return this.db.update("promptTemplates", { id }, template)
}
async compileTemplate(templateId, variables) {
// Компіляція шаблону з підстановкою змінних
const template = await this.getTemplateById(templateId)
if (!template) throw new Error("Template not found")
return Object.entries(variables).reduce(
(text, [key, value]) =>
text.replace(new RegExp(`{{${key}}}`, "g"), value),
template.content
)
}
}
Моделі даних
Модель шаблону промптів
// prompt-template.model.js - концептуальний код
class PromptTemplate {
constructor(data) {
this.id = data.id || generateUuid()
this.name = data.name
this.type = data.type // 'article', 'qa', 'comment', etc.
this.content = data.content
this.variables = data.variables || []
this.createdAt = data.createdAt || new Date()
this.updatedAt = data.updatedAt || new Date()
}
toJSON() {
return {
id: this.id,
name: this.name,
type: this.type,
content: this.content,
variables: this.variables,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
}
}
}
Модель завдання на обробку
// processing-task.model.js - концептуальний код
class ProcessingTask {
constructor(data) {
this.id = data.id || generateUuid()
this.type = data.type // 'generate_article', 'generate_qa', etc.
this.sourceType = data.sourceType // 'google_sheet', 'direct_input'
this.sourceData = data.sourceData // URL or direct data
this.templateId = data.templateId
this.options = data.options || {}
this.status = data.status || "pending" // 'pending', 'processing', 'completed', 'failed'
this.progress = data.progress || 0
this.result = data.result || null
this.error = data.error || null
this.createdAt = data.createdAt || new Date()
this.updatedAt = data.updatedAt || new Date()
}
updateStatus(status, options = {}) {
this.status = status
if (options.progress) this.progress = options.progress
if (options.result) this.result = options.result
if (options.error) this.error = options.error
this.updatedAt = new Date()
}
toJSON() {
return {
id: this.id,
type: this.type,
sourceType: this.sourceType,
sourceData: this.sourceData,
templateId: this.templateId,
options: this.options,
status: this.status,
progress: this.progress,
result: this.result,
error: this.error,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
}
}
}
Процес обробки в однопоточній архітектурі
- Запит від клієнта → API контролер приймає запит
- Створення завдання → Контролер формує об’єкт завдання
- Планування виконання → Завдання додається до TaskProcessor
- Повернення ID → Клієнт отримує ID завдання для відстеження
- Асинхронне виконання → TaskProcessor виконує завдання у фоні
- Збереження результату → Результат зберігається в ResultStore
- Сповіщення про завершення → EventBus сповіщає про завершення
- Запит результату → Клієнт отримує результат за ID
Діаграма потоку даних для однопоточної версії
+-------------+ +-------------------+ +-----------------+
| Client | | Server (Single | | External APIs |
| | | Thread Process) | | |
+-------------+ +-------------------+ +-----------------+
| | |
| 1. API Request | |
|--------------------->| |
| | |
| |--+ |
| | | 2. Create Task |
| |<-+ Object |
| | |
| |--+ |
| | | 3. Add Task to |
| |<-+ Internal Queue |
| | |
| 4. Return Task ID | |
|<---------------------| |
| | |
| |--+ |
| | | 5. Process Task |
| |<-+ Asynchronously |
| | |
| | 6. API Call |
| |----------------------->|
| | |
| | 7. API Response |
| |<-----------------------|
| | |
| |--+ |
| | | 8. Save Results |
| |<-+ to Memory Store |
| | |
| |--+ |
| | | 9. Emit |
| |<-+ Completion Event |
| | |
| 10. Status Request | |
|--------------------->| |
| | |
| 11. Return Results | |
|<---------------------| |
Готовність до масштабування
Що залишається незмінним при переході до багатопоточності
- API інтерфейс - всі ендпоінти залишаються незмінними
- Формат завдань - структура об’єктів завдань зберігається
- Система подій - принцип комунікації через події
- Бізнес-логіка - вся логіка сервісів залишається такою ж
Що змінюється при масштабуванні
-
TaskProcessor:
- Замість прямого виконання - відправка в чергу завдань
- Інтеграція з Bull/Bee-Queue для розподілу завдань
-
ResultStore:
- Перехід від зберігання в пам’яті до Redis/MongoDB
- Додавання TTL для результатів
-
EventBus:
- Заміна на Redis Pub/Sub або подібне рішення
- Додавання підтримки кластерних подій
-
Сервер:
- Запуск кількох воркерів через PM2/Docker
- Додавання балансувальника навантаження
Діаграма потоку даних для багатопоточної версії
+-----------+ +-------------+ +-----------------+
| API Layer | | Task Queue | | Worker Process |
+-----------+ +-------------+ +-----------------+
| | |
| Create Task | |
|---------------->| |
| | |
| Return Task ID | |
|<----------------| |
| | |
| | Process Task |
| |------------------->|
| | |
| | | Save Result
| |<-------------------|
| | |
| Get Result | |
|---------------->| |
| | |
| Return Result | |
|<----------------| |
Стратегія впровадження масштабування
Фаза 1: Однопоточне виконання
- Реалізація всієї логіки в одному процесі
- Використання абстракцій для сховищ і процесорів
- Підготовка інфраструктури для тестування
Фаза 2: Підготовка до масштабування
- Інтеграція з Redis для зберігання результатів
- Впровадження системи черг (Bull/Bee-Queue)
- Розділення API та обробників запитів
Фаза 3: Повне масштабування
- Розгортання кластеру воркерів через PM2/Docker
- Впровадження моніторингу навантаження
- Автоматичне масштабування за потребою
Технічні деталі реалізації
Однопоточна версія
- Мова програмування: Node.js/Express.js
- Зберігання стану: В пам’яті процесу (Maps, Arrays)
- Асинхронність: async/await з обробкою Promise
- Відстеження: Локальний EventEmitter
Багатопоточна версія
- Черги завдань: Bull на базі Redis
- Збереження даних: Redis для кешу, MongoDB для персистентності
- Обмін подіями: Redis Pub/Sub
- Масштабування: Docker Swarm або Kubernetes
- Балансування: HAProxy або Nginx
Висновки
Запропонована архітектура дозволяє почати з простого однопоточного рішення, але має всі необхідні абстракції для легкого переходу до багатопоточної обробки без значного рефакторингу коду. Ключовим є чітке відділення обробки завдань від API та використання системи подій для комунікації між компонентами.
Процес отримання та обробки даних
Джерела промптів та даних
-
Шаблони промптів:
- Зберігаються в базі даних
- Містять змінні частини для підстановки
- Можуть оновлюватись адміністратором через API
-
Вхідні дані для генерації:
- Google Sheets:
- URL таблиці передається з фронтенда
- Сервер отримує доступ та читає дані
- Обробляє рядки згідно з шаблоном
- Пряме введення:
- Дані передаються безпосередньо з фронтенда
- Використовуються для одноразової генерації
- Google Sheets:
-
Конфігурації генерації:
- Параметри генерації для OpenAI API
- Налаштування форматування результатів
- Збережені у БД з можливістю вибору при генерації
Потік роботи з даними
- Запит від клієнта:
{
"type": "generate_article",
"sourceType": "google_sheet",
"sourceData": "https://docs.google.com/spreadsheets/d/...",
"templateId": "article_template_1",
"options": {
"model": "gpt-4",
"maxTokens": 4000,
"temperature": 0.7
}
}
-
Підготовка даних на сервері:
- Отримання шаблону промпту з БД
- Отримання даних з Google Sheets
- Валідація даних і перевірка необхідних колонок
-
Створення завдання:
- Формування моделі ProcessingTask
- Збереження в базі даних для відстеження
- Повернення ID завдання клієнту
-
Обробка даних:
- Для кожного рядка з таблиці:
- Підстановка даних у шаблон промпту
- Запит до OpenAI API
- Обробка результатів
- Публікація у WordPress (за потреби)
- Оновлення прогресу у базі даних
- Для кожного рядка з таблиці:
-
Збереження результатів:
- Повні результати зберігаються в БД
- Оновлення статусу завдання
- Сповіщення клієнта про завершення
Діаграма потоку даних (розширена)
+-------------+ +---------------+ +----------------+
| Client | | API Server | | Database |
+-------------+ +---------------+ +----------------+
| | |
| Request Task | |
| (template, data) | |
|-------------------->| |
| | Get Template |
| |---------------------->|
| | |
| | Template Data |
| |<----------------------|
| | |
| | Create Task |
| |---------------------->|
| | |
| Task ID | |
|<--------------------| |
| | |
| | Process Data |
| |--------+ |
| | | |
| | v |
| | +------------+ |
| | | Google | |
| | | Sheets API | |
| | +------------+ |
| | | |
| |<-------+ |
| | |
| | +------------+ |
| | | OpenAI API | |
| | +------------+ |
| | | |
| |<-------+ |
| | |
| | +------------+ |
| | | WordPress | |
| | | API | |
| | +------------+ |
| | | |
| |<-------+ |
| | |
| | Update Task Status |
| |---------------------->|
| | |
| Get Task Status | |
|-------------------->| |
| | Get Task |
| |---------------------->|
| | |
| | Task Data |
| |<----------------------|
| | |
| Task Result | |
|<--------------------| |
| | |
Стратегія розвитку бази даних при масштабуванні
Фаза 1: Мінімальна база даних (однопоточне виконання)
- Технологія: SQLite або вбудована MongoDB (через mongodb-memory-server)
- Структура: Базові таблиці для шаблонів, завдань і результатів
- Перевага: Проста установка, не потребує зовнішніх сервісів
Фаза 2: Централізована база даних
- Технологія: PostgreSQL або MongoDB
- Структура: Розширена схема з індексами та зв’язками
- Перевага: Надійне зберігання, підтримка транзакцій
Фаза 3: Розподілена база даних
- Технологія: MongoDB Cluster або PostgreSQL з шардингом
- Структура: Оптимізована для розподілених запитів
- Перевага: Горизонтальне масштабування, висока доступність
Висновки
Запропонована архітектура включає всі необхідні компоненти для повноцінної роботи з даними та базою даних:
- Абстрактний доступ до БД - єдиний інтерфейс, незалежно від типу бази даних
- Система моделей - чітке представлення даних у системі
- Шаблони промптів - гнучке налаштування логіки генерації
- Джерела даних - підтримка як Google Sheets, так і прямого введення
Ця архітектура дозволяє починати з простої однопоточної системи з мінімальними вимогами до інфраструктури, але має всі необхідні компоненти для легкого масштабування при зростанні навантаження.