Пошук уроків, статей та іншого контенту
Порівняння масштабування сервера за рахунок ресурсів та додавання нових серверів.
Коли навантаження на базу даних зростає, одного сервера може стати недостатньо. Наприклад:
запити виконуються довше;
не вистачає оперативної пам’яті;
процесор постійно завантажений;
закінчується місце на диску;
збільшується кількість одночасних підключень;
база не встигає обробляти всі операції запису.
Є два основні підходи:
вертикальне масштабування — збільшення ресурсів одного сервера;
горизонтальне масштабування — додавання нових серверів і розподіл навантаження між ними.
Жоден із підходів не є універсальним. Вибір залежить від типу навантаження, вимог до доступності, бюджету та складності системи.
Вертикальне масштабування, або scaling up, означає збільшення ресурсів сервера PostgreSQL:
більше оперативної пам’яті;
потужніший процесор;
швидший диск;
більше дискового простору;
швидша мережева підсистема.
У цьому випадку PostgreSQL продовжує працювати на одному основному сервері.
База даних працює на сервері з такими характеристиками:
4 ядра CPU;
16 ГБ RAM;
SSD-диск.
Після зростання навантаження сервер можна замінити на такий:
16 ядер CPU;
64 ГБ RAM;
швидший NVMe-диск.
Застосунок при цьому може продовжити використовувати той самий connection string. Архітектура змінюється мінімально.
Просте впровадження.
Не потрібно змінювати логіку застосунку.
Не потрібно розподіляти дані між серверами.
Транзакції залишаються централізованими.
Легше забезпечити узгодженість даних.
Простішими залишаються резервне копіювання та адміністрування.
Ресурси одного сервера обмежені.
Потужніше обладнання може бути значно дорожчим.
Сервер залишається єдиною критичною точкою відмови.
Заміна або обслуговування сервера може спричинити простій.
Збільшення ресурсів не завжди усуває проблему неефективних запитів.
Наприклад, додавання RAM не виправить запит, який виконує повне сканування великої таблиці через відсутність потрібного індексу.
PostgreSQL використовує пам’ять для кешування сторінок таблиць та індексів. Якщо потрібні дані часто залишаються в пам’яті, кількість повільних звернень до диска зменшується.
Збільшення RAM особливо корисне, коли:
робочий набір даних поміщається в пам’ять;
запити часто читають одні й ті самі дані;
на сервері працює багато одночасних запитів.
Однак пам’ять потрібно розподіляти обережно. PostgreSQL має загальні налаштування кешування, а окремі операції, наприклад сортування, можуть використовувати пам’ять для кожного з’єднання.
Додаткові ядра корисні, якщо:
одночасно виконується багато запитів;
запити містять складні обчислення;
є багато паралельних операцій;
вузьким місцем є CPU.
Більше ядер не означає, що один запит автоматично стане в стільки ж разів швидшим. Прискорення залежить від плану виконання та можливості PostgreSQL виконувати конкретну операцію паралельно.
Швидший диск зменшує затримку операцій читання і запису. Це важливо для:
великої кількості операцій запису;
інтенсивного використання WAL;
запитів, які не поміщаються в кеш;
створення індексів та обслуговування таблиць.
Збільшення обсягу диска вирішує проблему місця, але не обов’язково проблему швидкості.
Перед масштабуванням потрібно виміряти навантаження. PostgreSQL надає статистику про підключення, розмір бази та співвідношення читання з кешу і диска.
-- Поточна кількість активних і неактивних підключень
SELECT
state,
count(*) AS connection_count
FROM pg_stat_activity
GROUP BY state
ORDER BY state;
-- Загальний розмір поточної бази даних
SELECT
current_database() AS database_name,
pg_size_pretty(pg_database_size(current_database())) AS database_size;
-- Співвідношення читання з кешу та диска
SELECT
datname,
blks_hit,
blks_read,
round(
100.0 * blks_hit / NULLIF(blks_hit + blks_read, 0),
2
) AS cache_hit_ratio
FROM pg_stat_database
WHERE datname = current_database();
-- Поточні налаштування пам'яті PostgreSQL
SELECT
name,
setting,
unit
FROM pg_settings
WHERE name IN ('shared_buffers', 'work_mem', 'effective_cache_size');Результати не можна інтерпретувати ізольовано:
велика кількість підключень може вказувати на проблему з керуванням з’єднаннями, а не на нестачу CPU;
низьке співвідношення читання з кешу може означати недостатню пам’ять або робочий набір даних, який не поміщається в кеш;
великий розмір бази сам по собі не означає, що потрібно переходити до горизонтального масштабування.
Також потрібно аналізувати повільні запити та їхні плани виконання за допомогою EXPLAIN і EXPLAIN ANALYZE.
Горизонтальне масштабування, або scaling out, означає додавання нових серверів PostgreSQL і розподіл роботи між ними.
Найпоширеніші варіанти:
репліки для читання;
розподіл даних між серверами, тобто шардинг;
окремі сервери для різних незалежних баз або частин системи.
Горизонтальне масштабування складніше, оскільки потрібно вирішити:
куди направляти кожен запит;
як синхронізувати дані;
що робити у разі відмови сервера;
як обробляти транзакції між серверами;
як уникнути неузгодженості даних.
PostgreSQL підтримує потокову реплікацію. Основний сервер, який називають primary, передає зміни на один або кілька серверів standby.
Типова схема:
Застосунок
|
+-- операції запису --> Primary
|
+-- операції читання --> Read replica 1
--> Read replica 2У такій архітектурі:
записи виконуються на primary;
читання можна розподіляти між репліками;
додавання реплік збільшує сумарну пропускну здатність читання;
primary залишається відповідальним за приймання змін.
Репліка не є незалежною копією, на яку можна безпечно записувати дані за звичайної потокової реплікації. Вона отримує зміни від primary.
Тому репліки:
допомагають масштабувати читання;
можуть використовуватися для резервування;
не збільшують пропускну здатність запису в основну базу;
можуть мати затримку реплікації.
Якщо користувач одразу після запису читає дані з репліки, він може тимчасово не побачити щойно створений запис. Це називають проблемою read-after-write consistency.
Застосунок може використовувати два пули з’єднань:
пул для primary — для INSERT, UPDATE, DELETE і критичних читань;
пул для реплік — для читань, які допускають невелику затримку.
Логіка маршрутизації може виглядати так:
import pg from "pg";
const { Pool } = pg;
const primaryPool = new Pool({
connectionString: process.env.PRIMARY_DATABASE_URL
});
const replicaPool = new Pool({
connectionString: process.env.REPLICA_DATABASE_URL
});
export async function createUser(email) {
const result = await primaryPool.query(
`
INSERT INTO users (email)
VALUES ($1)
RETURNING id, email
`,
[email]
);
return result.rows[0];
}
export async function listUsers() {
const result = await replicaPool.query(
`
SELECT id, email
FROM users
ORDER BY id DESC
LIMIT 100
`
);
return result.rows;
}У цьому прикладі операція створення користувача завжди виконується на primary, а список користувачів — на репліці.
Такий підхід потребує додаткової логіки для випадків:
репліка тимчасово недоступна;
затримка реплікації перевищила допустиме значення;
після запису потрібно гарантовано прочитати актуальні дані;
потрібно виконати транзакцію, яка містить і читання, і запис.
Шардинг означає розподіл рядків або наборів даних між кількома серверами. Кожен сервер зберігає лише частину загального обсягу даних.
Наприклад, користувачів можна розподіляти за ідентифікатором:
Shard 1: user_id від 1 до 1 000 000
Shard 2: user_id від 1 000 001 до 2 000 000
Shard 3: user_id від 2 000 001 до 3 000 000Або використовувати хешування:
shard = hash(user_id) % кількість_шардівобсяг даних перевищує практичні можливості одного сервера;
записів настільки багато, що один primary не справляється;
дані природно поділяються на незалежні групи;
запити зазвичай працюють лише з одним сегментом даних.
потрібно визначити ключ розподілу;
запити до кількох шардів стають складнішими;
глобальні агрегати потребують збору результатів з різних серверів;
зміна кількості шардів може вимагати переміщення даних;
транзакції між шардами важче реалізувати;
нерівномірний розподіл даних може створити перевантажений шард.
Наприклад, якщо розподіляти дані за країною, одна дуже велика країна може отримати набагато більше навантаження, ніж інші. Це називають нерівномірним розподілом або hot shard.
Шардинг зазвичай не є першим кроком оптимізації. Спочатку перевіряють запити, індекси, схему даних, ресурси та реплікацію.
Підходить, коли:
основна проблема пов’язана з CPU, RAM або диском;
навантаження переважно записувальне;
потрібна проста архітектура;
база ще поміщається на одному сервері;
важливо зберегти повну підтримку звичайних транзакцій.
Основний компроміс: простота проти обмеженого максимального розміру одного сервера.
Підходить, коли:
потрібно збільшити пропускну здатність читання;
потрібна висока доступність;
дані можна розподілити між серверами;
один сервер уже наблизився до практичної межі;
команда готова підтримувати складнішу інфраструктуру.
Основний компроміс: більша пропускна здатність або доступність проти складнішої маршрутизації та узгодженості.
Перед зміною архітектури варто рухатися поетапно.
Виміряти проблему.
Визначити, що саме обмежує систему: CPU, RAM, диск, кількість з’єднань, читання або запис.
Перевірити запити.
Повільний запит або відсутній індекс часто створює більше проблем, ніж недостатня потужність сервера.
Збільшити ресурси, якщо це достатньо.
Вертикальне масштабування зазвичай має найменшу складність.
Додати репліки, якщо вузьке місце — читання.
Читання потрібно відокремити від запису на рівні застосунку або інфраструктури.
Розглянути шардинг лише за наявності чіткої потреби.
Він виправданий, коли один сервер уже не може обробляти обсяг даних або записів, а дані можна надійно розділити.
Передбачити відмови.
Для кожного додаткового сервера потрібно визначити, що відбувається у разі його недоступності або затримки реплікації.
На практиці часто використовують обидва підходи.
Наприклад:
Primary:
- потужний сервер;
- приймає всі записи.
Read replica 1:
- обробляє частину читань.
Read replica 2:
- обробляє іншу частину читань.
Резервний сервер:
- готовий замінити primary у разі відмови.Спочатку кожен сервер можна масштабувати вертикально, а потім додавати нові репліки горизонтально. Така комбінація дає змогу не переходити до шардингу доти, доки він справді не стане необхідним.
Репліки зазвичай масштабують читання, але не усувають обмеження primary на запис. Якщо проблема саме в INSERT або UPDATE, потрібно аналізувати primary і спосіб запису.
Збільшення RAM або CPU навмання може бути дорогим і не дати результату. Спочатку потрібно знайти фактичне вузьке місце.
Читання з репліки може повертати не найновіші дані через затримку реплікації. Критичні читання після запису потрібно виконувати на primary або використовувати іншу стратегію узгодженості.
Один потужний primary може мати багато ресурсів, але його відмова зупинить запис. Вертикальне масштабування саме по собі не забезпечує високої доступності.
Шардинг збільшує складність застосунку, тестування, моніторингу та операцій із даними. Його не варто впроваджувати лише тому, що база стала великою.
Невдалий ключ шардингу може перевантажити один сервер, тоді як інші залишаться майже без роботи. Ключ потрібно вибирати з урахуванням обсягу та характеру запитів.
Вертикальне масштабування збільшує ресурси одного сервера PostgreSQL.
Воно простіше, добре підтримує централізовані транзакції та часто є першим кроком.
Горизонтальне масштабування додає сервери й розподіляє між ними роботу.
Репліки насамперед масштабують читання та підвищують доступність.
Реплікація не робить усі сервери незалежними для запису.
Шардинг розподіляє дані між серверами, але значно ускладнює систему.
Вибір підходу потрібно робити після вимірювання навантаження, а не лише за розміром бази.
На практиці часто поєднують потужний primary, репліки для читання та поступове вертикальне масштабування.