Какие знаешь типы масштабирования баз данных?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Типы масштабирования баз данных
По мере роста объёма данных и количества пользователей нужно масштабировать БД. Существует несколько стратегий масштабирования, каждая с плюсами и минусами.
1. Vertical Scaling (вертикальное масштабирование)
Увеличение мощности одного сервера: больше CPU, памяти, диска:
Шаг 1: Сервер 4 ядра, 8 GB RAM
↓
Шаг 2: Сервер 8 ядер, 32 GB RAM
↓
Шаг 3: Сервер 16 ядер, 64 GB RAM
Преимущества:
- Просто реализовать
- Нет изменения кода
- ACID транзакции сохраняются
Проблемы:
- Дороговизна (экспоненциальный рост стоимости)
- Есть предел (физический лимит сервера)
- Downtime при обновлении
- Single point of failure
Когда использовать: маленькие и средние приложения.
2. Horizontal Scaling (горизонтальное масштабирование)
Добавление новых серверов БД и распределение данных:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ User 1-333 │ │ User 334-666│ │ User 667-999│
└─────────────┘ └─────────────┘ └─────────────┘
↑ ↑ ↑
└────────┬───────────┬────────┬──────────┘
│ │ │
┌────────────────────────────┐
│ Load Balancer / Router │
└────────────────────────────┘
↑
Приложение
Типы:
3. Sharding (разделение по ключу)
Распределение данных по разным серверам на основе ключа:
// Функция шардирования
public class ShardRouter {
private final int NUM_SHARDS = 3;
public int getShard(Long userId) {
return (int) (userId % NUM_SHARDS); // 0, 1 или 2
}
public Database getShardDatabase(Long userId) {
int shard = getShard(userId);
switch (shard) {
case 0: return db1; // Shard 1
case 1: return db2; // Shard 2
case 2: return db3; // Shard 3
default: throw new IllegalArgumentException();
}
}
}
// SQL запрос
User user = shardRouter.getShardDatabase(123).query(
"SELECT * FROM users WHERE id = ?",
123
);
Преимущества:
- Линейное масштабирование
- Каждый shard содержит часть данных
- Высокая производительность
Проблемы:
- Сложность
- Cross-shard запросы медленные (JOIN между шардами)
- Rebalancing сложный при добавлении нового шарда
- Hotspot проблема (какой-то шард получает больше нагрузки)
4. Replication (репликация)
Копирование данных на несколько серверов для надёжности и чтения:
┌─────────────────────────────────────┐
│ Primary (Master) DB │
│ Принимает write запросы │
│ user_id=1, name="Alice" │
│ Отправляет изменения на replicas │
└────────────┬────────────────────────┘
│ replication stream
┌────┴─────────┬──────────────┐
↓ ↓ ↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│Replica 1 (RO)│ │Replica 2 (RO)│ │Replica 3 (RO)│
│user_id=1... │ │user_id=1... │ │user_id=1... │
│(Read-only) │ │(Read-only) │ │(Read-only) │
└─────────────┘ └─────────────┘ └─────────────┘
В Java с Read Replicas:
@Configuration
public class DatabaseConfig {
@Bean
public DataSource primaryDataSource() { // Master
// host: db-master.example.com
return DataSourceBuilder.create()
.url("jdbc:postgresql://db-master:5432/mydb")
.build();
}
@Bean
public DataSource replicaDataSource() { // Read replica
// host: db-replica-1.example.com
return DataSourceBuilder.create()
.url("jdbc:postgresql://db-replica-1:5432/mydb")
.build();
}
}
@Service
public class UserService {
@Autowired
private DataSource primaryDataSource; // Для write
@Autowired
private DataSource replicaDataSource; // Для read
public void createUser(User user) {
// Write на master
JdbcTemplate write = new JdbcTemplate(primaryDataSource);
write.update("INSERT INTO users ...", ...);
}
public User getUserById(Long id) {
// Read с replica
JdbcTemplate read = new JdbcTemplate(replicaDataSource);
return read.query("SELECT * FROM users WHERE id = ?", ...);
}
}
Преимущества:
- Высокая доступность (если master упадёт)
- Масштабирование чтения
- Можно читать синхронно
Проблемы:
- Replication lag: replica отстаёт на несколько ms/sec
- Consistency issues: читаем старые данные после write
- Storage дублируется (нужно больше диска)
5. Multi-Master Replication
Несколько master'ов, все могут писать:
┌──────────────┐ ┌──────────────┐
│ Master 1 │ ←---→ │ Master 2 │
│ (write + read)│ sync │ (write + read)│
└──────────────┘ └──────────────┘
↑ ↑
write write
│ │
App Instance 1 App Instance 2
Проблемы:
- Конфликты: если оба master'а пишут в одну запись одновременно
- Complexity: нужна сложная логика разрешения конфликтов
- Consistency: не гарантирует ACID
Редко используется в SQL БД.
6. Partitioning (разделение на партиции)
Разделение одной таблицы на части по диапазону значений:
-- Разделяем таблицу заказов по дате
CREATE TABLE orders (
id BIGINT,
user_id BIGINT,
created_at TIMESTAMPTZ,
amount DECIMAL
) PARTITION BY RANGE (created_at);
-- Партиции
CREATE TABLE orders_2024_q1 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE orders_2024_q2 PARTITION OF orders
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
-- Со своим индексом
CREATE INDEX idx_orders_2024_q1 ON orders_2024_q1(user_id);
CREATE INDEX idx_orders_2024_q2 ON orders_2024_q2(user_id);
Преимущества:
- Быстрые запросы к старым данным (partition pruning)
- Проще архивирование
- Улучшена производительность индексов
Проблемы:
- Сложнее управлять
- Cross-partition запросы медленнее
7. Read-Write Splitting
Направление write на master, read на replica:
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
? "replica" // Читаем с replica
: "primary"; // Пишем на primary
}
}
@Configuration
public class DataSourceConfig {
@Bean
public DataSource routingDataSource(
@Qualifier("primaryDataSource") DataSource primary,
@Qualifier("replicaDataSource") DataSource replica) {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> targets = new HashMap<>();
targets.put("primary", primary);
targets.put("replica", replica);
routingDataSource.setTargetDataSources(targets);
routingDataSource.setDefaultTargetDataSource(primary);
return routingDataSource;
}
}
// Использование
@Service
public class UserService {
@Transactional(readOnly = false) // → primary
public void createUser(User user) {
userRepository.save(user);
}
@Transactional(readOnly = true) // → replica
public User getUserById(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
8. NoSQL для масштабирования
Перемещение определённых данных в NoSQL (MongoDB, Cassandra):
┌──────────────────┐
│ PostgreSQL │ ← Структурированные данные
│ (Users, Orders) │
└──────────────────┘
┌──────────────────┐
│ MongoDB │ ← JSON documents (logs, events)
│ (Event logs) │
└──────────────────┘
┌──────────────────┐
│ Cassandra │ ← Time series (metrics)
│ (Metrics, Logs) │
└──────────────────┘
┌──────────────────┐
│ Redis │ ← Cache (sessions, real-time)
│ (Cache) │
└──────────────────┘
NoSQL преимущества:
- Горизонтальное масштабирование из коробки
- Высокая пропускная способность
- Гибкая схема
Проблемы:
- Нет ACID транзакций
- Eventual consistency
- Слабый query язык
9. Database Federation
Разделение данных по доменам (каждый домен свою БД):
┌─────────────────────┐
│ User Service │
│ PostgreSQL │
│ (users, profiles) │
└─────────────────────┘
┌─────────────────────┐
│ Order Service │ ← Microservices
│ PostgreSQL │ каждый со своей БД
│ (orders, items) │
└─────────────────────┘
┌─────────────────────┐
│ Payment Service │
│ PostgreSQL │
│ (transactions) │
└─────────────────────┘
Каждый микросервис управляет своей БД.
10. Caching Layer
Добавляем кэш перед БД:
┌─────────────┐
│ Application│
└──────┬──────┘
│
↓
┌─────────────────────────┐
│ Redis / Memcached │ ← Cache L1 (in-memory)
│ (hot data, sessions) │
└──────┬──────────────────┘
│ (на cache miss)
↓
┌─────────────────────────┐
│ PostgreSQL + Read │ ← Primary storage
│ Replicas │
└─────────────────────────┘
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, User> redisTemplate;
public User getUserById(Long id) {
String key = "user:" + id;
// Проверяем кэш
User cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return cached; // Cache hit
}
// Cache miss → идём в БД
User user = userRepository.findById(id)
.orElseThrow();
// Сохраняем в кэш на 1 час
redisTemplate.opsForValue().set(key, user, Duration.ofHours(1));
return user;
}
}
Стратегия выбора масштабирования
Время: Пользователи: Стратегия:
────────────────────────────────────────────────
0-3 месяца < 10k Vertical scaling
3-12 месяцев 10k - 100k Replication + cache
1-2 года 100k - 1M Sharding
2+ года 1M+ Federation + Sharding
Best Practices
- Начни с vertical scaling (дешевле и проще)
- Добавь caching (Redis для горячих данных)
- Replicate reads (master-slave для read-heavy workloads)
- Sharding (только когда data не влезает на один сервер)
- Federation/Microservices (когда разные домены независимы)
- NoSQL (для времени/логов когда не нужна strict consistency)