Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/docs/DATABASE_SCALABILITY_GUIDE.md
2026-04-14 19:34:55 +03:30

965 lines
31 KiB
Markdown
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# راهنمای جامع بهبود مقیاس‌پذیری پایگاه داده - Hesabix
## 📋 فهرست مطالب
1. [تحلیل مشکلات فعلی](#تحلیل-مشکلات-فعلی)
2. [راه‌حل‌های پیشنهادی](#راه‌حل‌های-پیشنهادی)
3. [بهینه‌سازی Connection Pool](#بهینه‌سازی-connection-pool)
4. [پیاده‌سازی Read Replicas](#پیاده‌سازی-read-replicas)
5. [Partitioning جداول بزرگ](#partitioning-جداول-بزرگ)
6. [بهینه‌سازی MySQL Configuration](#بهینه‌سازی-mysql-configuration)
7. [Monitoring و Metrics](#monitoring-و-metrics)
8. [Migration Plan](#migration-plan)
---
## 🔍 تحلیل مشکلات فعلی
### 1. Connection Pool محدود
#### وضعیت فعلی:
```python
# hesabixAPI/app/core/settings.py
db_pool_size: int = 20 # اتصالات پایه
db_max_overflow: int = 30 # اتصالات اضافی
# حداکثر: 50 اتصال per worker
```
#### مشکل:
- با 17 worker: `17 * 50 = 850` اتصال حداکثر
- برای میلیون‌ها کاربر، این عدد کافی نیست
- در Peak Times، احتمال Connection Exhaustion وجود دارد
### 2. Single Database Instance
#### مشکل:
- تمام Read و Write queries به یک دیتابیس می‌روند
- در Scale بالا، Write Operations باعث Lock می‌شوند
- Read Operations تحت تأثیر Write Operations قرار می‌گیرند
### 3. عدم استفاده از Replication
#### مشکل:
- Single Point of Failure
- عدم توزیع بار بین چند دیتابیس
- عدم امکان Horizontal Scaling
### 4. جداول بزرگ بدون Partitioning
#### جداول مشکل‌دار:
- `documents`: با رشد بالا
- `activity_logs`: با تعداد زیاد رکورد
- `file_storage`: با حجم بالا
#### مشکل:
- Query های کند روی جداول بزرگ
- Index Maintenance سنگین
- Backup و Restore زمان‌بر
### 5. MySQL Configuration بهینه نیست
#### مشکلات:
- `max_connections` پیش‌فرض (151) بسیار پایین است
- `innodb_buffer_pool_size` بهینه نشده
- Query Cache غیرفعال یا ناکافی
---
## ✅ راه‌حل‌های پیشنهادی
### راه‌حل 1: بهینه‌سازی Connection Pool (Quick Win)
#### 1.1 افزایش Pool Size
**فایل**: `hesabixAPI/app/core/settings.py`
```python
# تنظیمات پیشنهادی برای Production
class Settings(BaseSettings):
# Connection Pool برای Production
# محاسبه: (تعداد Worker ها * اتصالات مورد نیاز per Worker) + Buffer
# مثال: 5 Workers * 100 اتصال = 500 + 300 buffer = 800
db_pool_size: int = 100 # افزایش از 20
db_max_overflow: int = 100 # افزایش از 30
db_pool_timeout: int = 30 # افزایش از 10
db_pool_recycle: int = 3600 # Recycle هر ساعت
db_pool_pre_ping: bool = True # Health check قبل از استفاده
```
#### 1.2 بهبود Session Management
**فایل**: `hesabixAPI/adapters/db/session.py`
```python
from sqlalchemy import create_engine, event
from sqlalchemy.pool import QueuePool
settings = get_settings()
# Engine با تنظیمات بهینه شده
engine = create_engine(
settings.mysql_dsn,
echo=settings.sqlalchemy_echo,
poolclass=QueuePool, # استفاده از QueuePool برای بهتر control
pool_pre_ping=True, # بررسی سلامت اتصالات
pool_recycle=settings.db_pool_recycle, # Recycle اتصالات قدیمی
pool_size=settings.db_pool_size,
max_overflow=settings.db_max_overflow,
pool_timeout=settings.db_pool_timeout,
connect_args={
"connect_timeout": 10,
"read_timeout": 60, # افزایش برای Query های طولانی
"write_timeout": 60,
"charset": "utf8mb4",
"init_command": "SET sql_mode='STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO'",
},
# بهینه‌سازی برای Performance
pool_reset_on_return='commit', # Reset connection بعد از return
isolation_level="READ COMMITTED", # برای بهتر Concurrency
)
# Event Listener برای Logging Pool Statistics
@event.listens_for(engine, "connect")
def set_sqlite_pragma(dbapi_conn, connection_record):
"""تنظیمات MySQL برای هر Connection"""
try:
with dbapi_conn.cursor() as cursor:
# بهینه‌سازی Query Cache
cursor.execute("SET SESSION query_cache_type = ON")
# بهینه‌سازی برای InnoDB
cursor.execute("SET SESSION innodb_lock_wait_timeout = 50")
# بهینه‌سازی برای Read Performance
cursor.execute("SET SESSION transaction_isolation = 'READ-COMMITTED'")
except Exception as e:
logger.warning(f"Error setting MySQL session variables: {e}")
# Logging Pool Statistics
@event.listens_for(engine.pool, "connect")
def receive_connect(dbapi_conn, connection_record):
logger.debug(f"New database connection created. Pool size: {engine.pool.size()}, "
f"Checked out: {engine.pool.checkedout()}")
@event.listens_for(engine.pool, "checkout")
def receive_checkout(dbapi_conn, connection_record, connection_proxy):
logger.debug(f"Connection checked out. Pool size: {engine.pool.size()}, "
f"Checked out: {engine.pool.checkedout()}")
@event.listens_for(engine.pool, "checkin")
def receive_checkin(dbapi_conn, connection_record):
logger.debug(f"Connection checked in. Pool size: {engine.pool.size()}, "
f"Checked out: {engine.pool.checkedout()}")
```
#### 1.3 Connection Pool Monitoring
**فایل جدید**: `hesabixAPI/app/core/db_pool_monitor.py`
```python
"""
Connection Pool Monitoring
برای مانیتورینگ وضعیت Connection Pool
"""
from sqlalchemy import event, inspect
from adapters.db.session import engine
import logging
import time
from typing import Dict
logger = logging.getLogger(__name__)
class ConnectionPoolMonitor:
"""Monitor برای Connection Pool"""
@staticmethod
def get_pool_stats() -> Dict[str, int]:
"""دریافت آمار Connection Pool"""
pool = engine.pool
return {
"pool_size": pool.size(),
"checked_out": pool.checkedout(),
"overflow": pool.overflow(),
"checked_in": pool.size() - pool.checkedout(),
}
@staticmethod
def log_pool_stats():
"""Log آمار Connection Pool"""
stats = ConnectionPoolMonitor.get_pool_stats()
logger.info(f"Connection Pool Stats: {stats}")
# Alert در صورت Full Pool
if stats["checked_out"] >= (stats["pool_size"] + stats.get("overflow", 0)) * 0.9:
logger.warning(f"Connection Pool nearly exhausted! {stats}")
# Event Listener برای Monitoring
@event.listens_for(engine.pool, "checkout")
def on_checkout(dbapi_conn, connection_record, connection_proxy):
stats = ConnectionPoolMonitor.get_pool_stats()
if stats["checked_out"] >= stats["pool_size"] * 0.8:
logger.warning(f"High connection pool usage: {stats}")
# Periodic monitoring (می‌توان در Background Job اضافه کرد)
```
---
### راه‌حل 2: پیاده‌سازی Read Replicas (High Impact)
#### 2.1 ساختار Database Routing
**فایل جدید**: `hesabixAPI/adapters/db/routing.py`
```python
"""
Database Routing برای Read/Write Splitting
"""
from typing import Generator
from contextlib import contextmanager
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
import random
import logging
from app.core.settings import get_settings
logger = logging.getLogger(__name__)
class DatabaseRouter:
"""Router برای توزیع Read/Write Queries"""
def __init__(self):
self.settings = get_settings()
self._write_engine = None
self._read_engines = []
self._read_sessions = []
@property
def write_engine(self):
"""Engine برای Write Operations"""
if self._write_engine is None:
self._write_engine = self._create_engine(
host=self.settings.db_host,
port=self.settings.db_port,
user=self.settings.db_user,
password=self.settings.db_password,
database=self.settings.db_name,
pool_size=self.settings.db_pool_size,
max_overflow=self.settings.db_max_overflow,
)
return self._write_engine
@property
def read_engines(self):
"""Engines برای Read Operations"""
if not self._read_engines:
# خواندن لیست Read Replicas از Settings
read_hosts = self._get_read_replicas()
for host_config in read_hosts:
engine = self._create_engine(
host=host_config["host"],
port=host_config.get("port", self.settings.db_port),
user=host_config.get("user", self.settings.db_user),
password=host_config.get("password", self.settings.db_password),
database=self.settings.db_name,
pool_size=self.settings.db_pool_size // 2, # نصف Write Pool
max_overflow=self.settings.db_max_overflow // 2,
)
self._read_engines.append(engine)
return self._read_engines
def _create_engine(self, host, port, user, password, database, pool_size, max_overflow):
"""ایجاد Engine با تنظیمات مشخص"""
dsn = f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}"
return create_engine(
dsn,
pool_pre_ping=True,
pool_recycle=3600,
pool_size=pool_size,
max_overflow=max_overflow,
pool_timeout=30,
connect_args={
"connect_timeout": 10,
"read_timeout": 60,
"write_timeout": 60,
},
)
def _get_read_replicas(self) -> list:
"""دریافت لیست Read Replicas از Settings"""
# می‌توان از Environment Variables یا Settings استفاده کرد
read_replicas_str = getattr(self.settings, 'db_read_replicas', '')
if not read_replicas_str:
# اگر Read Replica تعریف نشده، از Write استفاده می‌کنیم
return [{"host": self.settings.db_host}]
replicas = []
for replica_str in read_replicas_str.split(','):
parts = replica_str.strip().split(':')
replica = {"host": parts[0]}
if len(parts) > 1:
replica["port"] = int(parts[1])
replicas.append(replica)
return replicas
def get_read_engine(self):
"""دریافت یک Read Engine به صورت Random (Load Balancing)"""
engines = self.read_engines
if not engines:
# Fallback به Write Engine اگر Read Replica نباشد
return self.write_engine
return random.choice(engines)
@contextmanager
def get_read_session(self) -> Generator[Session, None, None]:
"""Context Manager برای Read Session"""
engine = self.get_read_engine()
session = sessionmaker(bind=engine, autoflush=False, autocommit=False)()
try:
yield session
finally:
session.close()
@contextmanager
def get_write_session(self) -> Generator[Session, None, None]:
"""Context Manager برای Write Session"""
session = sessionmaker(bind=self.write_engine, autoflush=False, autocommit=False)()
try:
yield session
finally:
session.close()
# Global Router Instance
_router = None
def get_router() -> DatabaseRouter:
"""Get global database router"""
global _router
if _router is None:
_router = DatabaseRouter()
return _router
def get_read_db() -> Generator[Session, None, None]:
"""Dependency برای FastAPI - Read Database"""
router = get_router()
with router.get_read_session() as session:
yield session
def get_write_db() -> Generator[Session, None, None]:
"""Dependency برای FastAPI - Write Database"""
router = get_router()
with router.get_write_session() as session:
yield session
```
#### 2.2 افزودن Settings برای Read Replicas
**فایل**: `hesabixAPI/app/core/settings.py`
```python
class Settings(BaseSettings):
# ... existing settings ...
# Database Read Replicas
# فرمت: host1:port1,host2:port2,host3:port3
# مثال: replica1.example.com:3306,replica2.example.com:3306
db_read_replicas: str = "" # لیست Read Replicas (comma-separated)
# Separate credentials for read replicas (optional)
db_read_user: str | None = None
db_read_password: str | None = None
```
#### 2.3 استفاده از Read Replicas در Endpoints
**مثال**: `hesabixAPI/adapters/api/v1/products.py`
```python
from adapters.db.routing import get_read_db, get_write_db
from sqlalchemy.orm import Session
# برای Read Operations (GET)
@router.get("/products")
async def list_products(
db: Session = Depends(get_read_db), # استفاده از Read Replica
...
):
# Read operations
...
# برای Write Operations (POST, PUT, DELETE)
@router.post("/products")
async def create_product(
db: Session = Depends(get_write_db), # استفاده از Master
...
):
# Write operations
...
```
---
### راه‌حل 3: Partitioning جداول بزرگ
#### 3.1 Partitioning Strategy
**Migration File**: `hesabixAPI/migrations/versions/add_partitioning.py`
```python
"""Partitioning جداول بزرگ برای بهبود Performance"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
def upgrade():
"""
اضافه کردن Partitioning برای جداول بزرگ
"""
# 1. Partitioning برای documents بر اساس business_id
# استفاده از HASH Partitioning برای توزیع یکنواخت
try:
op.execute(text("""
ALTER TABLE documents
PARTITION BY HASH(business_id)
PARTITIONS 16
"""))
print("✅ Partitioning added to documents table")
except Exception as e:
print(f"⚠️ Warning: Could not partition documents: {e}")
# 2. Partitioning برای activity_logs بر اساس created_at
# استفاده از RANGE Partitioning برای جداول زمانی
try:
op.execute(text("""
ALTER TABLE activity_logs
PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p_2024_q1 VALUES LESS THAN (TO_DAYS('2024-04-01')),
PARTITION p_2024_q2 VALUES LESS THAN (TO_DAYS('2024-07-01')),
PARTITION p_2024_q3 VALUES LESS THAN (TO_DAYS('2024-10-01')),
PARTITION p_2024_q4 VALUES LESS THAN (TO_DAYS('2025-01-01')),
PARTITION p_future VALUES LESS THAN MAXVALUE
)
"""))
print("✅ Partitioning added to activity_logs table")
except Exception as e:
print(f"⚠️ Warning: Could not partition activity_logs: {e}")
# 3. Partitioning برای file_storage (اختیاری)
# اگر جدول خیلی بزرگ شد، می‌توان Partitioning اضافه کرد
def downgrade():
"""
حذف Partitioning (احتیاط: ممکن است زمان‌بر باشد)
"""
try:
op.execute(text("ALTER TABLE documents REMOVE PARTITIONING"))
print("✅ Partitioning removed from documents table")
except Exception as e:
print(f"⚠️ Warning: {e}")
try:
op.execute(text("ALTER TABLE activity_logs REMOVE PARTITIONING"))
print("✅ Partitioning removed from activity_logs table")
except Exception as e:
print(f"⚠️ Warning: {e}")
```
#### 3.2 Partition Maintenance Script
**فایل جدید**: `hesabixAPI/scripts/maintain_partitions.py`
```python
"""
Script برای نگهداری Partitions
- اضافه کردن Partitions جدید
- حذف Partitions قدیمی
"""
from sqlalchemy import create_engine, text
from app.core.settings import get_settings
from datetime import datetime, timedelta
def add_monthly_partitions(connection, table_name: str, months_ahead: int = 3):
"""
اضافه کردن Partitions ماهانه برای جداول زمانی
"""
for i in range(months_ahead):
month_date = datetime.now() + timedelta(days=30 * (i + 1))
month_start = month_date.replace(day=1)
next_month_start = (month_start + timedelta(days=32)).replace(day=1)
partition_name = f"p_{month_start.strftime('%Y_%m')}"
try:
connection.execute(text(f"""
ALTER TABLE {table_name}
REORGANIZE PARTITION p_future INTO (
PARTITION {partition_name} VALUES LESS THAN (TO_DAYS('{next_month_start.strftime('%Y-%m-%d')}')),
PARTITION p_future VALUES LESS THAN MAXVALUE
)
"""))
print(f"✅ Added partition {partition_name} to {table_name}")
except Exception as e:
print(f"⚠️ Warning: {e}")
def drop_old_partitions(connection, table_name: str, months_to_keep: int = 12):
"""
حذف Partitions قدیمی تر از X ماه
"""
cutoff_date = datetime.now() - timedelta(days=30 * months_to_keep)
# Query برای پیدا کردن Partitions قدیمی
result = connection.execute(text(f"""
SELECT PARTITION_NAME
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = '{table_name}'
AND PARTITION_NAME IS NOT NULL
AND PARTITION_NAME != 'p_future'
"""))
for row in result:
partition_name = row[0]
# Extract date from partition name (format: p_YYYY_MM)
try:
year_month = partition_name.split('_')[1:]
if len(year_month) >= 2:
year, month = int(year_month[0]), int(year_month[1])
partition_date = datetime(year, month, 1)
if partition_date < cutoff_date:
connection.execute(text(f"""
ALTER TABLE {table_name} DROP PARTITION {partition_name}
"""))
print(f"✅ Dropped old partition {partition_name}")
except Exception as e:
print(f"⚠️ Warning: Could not process partition {partition_name}: {e}")
if __name__ == "__main__":
settings = get_settings()
engine = create_engine(settings.mysql_dsn)
with engine.connect() as connection:
# اضافه کردن Partitions جدید
add_monthly_partitions(connection, "activity_logs", months_ahead=3)
# حذف Partitions قدیمی (نگه داشتن 12 ماه)
drop_old_partitions(connection, "activity_logs", months_to_keep=12)
print("✅ Partition maintenance completed")
```
---
### راه‌حل 4: بهینه‌سازی MySQL Configuration
#### 4.1 MySQL Configuration File
**فایل**: `mysql.conf` (برای Production)
```ini
[mysqld]
# Connection Settings
max_connections = 2000 # افزایش از پیش‌فرض 151
max_user_connections = 1000
connect_timeout = 10
wait_timeout = 600
interactive_timeout = 600
# InnoDB Settings
innodb_buffer_pool_size = 70% RAM # مثال: برای 32GB RAM = 22GB
innodb_buffer_pool_instances = 8 # برای RAM > 1GB
innodb_log_file_size = 512M
innodb_log_buffer_size = 64M
innodb_flush_log_at_trx_commit = 2 # بهتر برای Performance (trade-off با Durability)
innodb_flush_method = O_DIRECT
innodb_file_per_table = 1
innodb_read_io_threads = 8
innodb_write_io_threads = 8
# Query Cache (MySQL 5.7 - در 8.0 حذف شده)
# query_cache_type = 1
# query_cache_size = 256M
# query_cache_limit = 2M
# Table Cache
table_open_cache = 4000
table_definition_cache = 2000
# Temporary Tables
tmp_table_size = 256M
max_heap_table_size = 256M
# Sort and Join
sort_buffer_size = 4M
join_buffer_size = 4M
read_buffer_size = 2M
read_rnd_buffer_size = 4M
# Binary Logging (برای Replication)
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
expire_logs_days = 7
max_binlog_size = 500M
# Slow Query Log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-query.log
long_query_time = 2 # Log queries > 2 seconds
# Error Log
log_error = /var/log/mysql/error.log
# Character Set
character_set_server = utf8mb4
collation_server = utf8mb4_unicode_ci
# MyISAM (اگر استفاده نمی‌کنید)
key_buffer_size = 32M
# Threading
thread_cache_size = 50
thread_stack = 256K
# Network
max_allowed_packet = 64M
# Performance Schema (برای Monitoring)
performance_schema = ON
```
#### 4.2 Docker Compose برای Production
**فایل**: `docker-compose.prod.yml`
```yaml
version: "3.9"
services:
db-master:
image: mysql:8.4
container_name: hesabix-mysql-master
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db_master_data:/var/lib/mysql
- ./mysql.conf:/etc/mysql/conf.d/custom.cnf:ro
- ./mysql-init:/docker-entrypoint-initdb.d:ro
ports:
- "3306:3306"
command: --server-id=1 --log-bin=mysql-bin --binlog-format=ROW
restart: unless-stopped
db-replica-1:
image: mysql:8.4
container_name: hesabix-mysql-replica-1
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db_replica1_data:/var/lib/mysql
- ./mysql.conf:/etc/mysql/conf.d/custom.cnf:ro
ports:
- "3307:3306"
command: --server-id=2 --relay-log=mysql-relay-bin --read-only=1
depends_on:
- db-master
restart: unless-stopped
db-replica-2:
image: mysql:8.4
container_name: hesabix-mysql-replica-2
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
volumes:
- db_replica2_data:/var/lib/mysql
- ./mysql.conf:/etc/mysql/conf.d/custom.cnf:ro
ports:
- "3308:3306"
command: --server-id=3 --relay-log=mysql-relay-bin --read-only=1
depends_on:
- db-master
restart: unless-stopped
volumes:
db_master_data:
db_replica1_data:
db_replica2_data:
```
#### 4.3 Setup Replication Script
**فایل جدید**: `scripts/setup_replication.sh`
```bash
#!/bin/bash
# Script برای راه‌اندازی MySQL Replication
MASTER_HOST="${MASTER_HOST:-db-master}"
MASTER_PORT="${MASTER_PORT:-3306}"
REPLICA_HOST="${REPLICA_HOST:-db-replica-1}"
REPLICA_PORT="${REPLICA_PORT:-3306}"
REPLICA_USER="${REPLICA_USER:-replica}"
REPLICA_PASSWORD="${REPLICA_PASSWORD:-replica_password}"
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD}"
echo "🔧 Setting up MySQL Replication..."
# 1. Create replication user on Master
mysql -h ${MASTER_HOST} -P ${MASTER_PORT} -uroot -p${MYSQL_ROOT_PASSWORD} <<EOF
CREATE USER IF NOT EXISTS '${REPLICA_USER}'@'%' IDENTIFIED BY '${REPLICA_PASSWORD}';
GRANT REPLICATION SLAVE ON *.* TO '${REPLICA_USER}'@'%';
FLUSH PRIVILEGES;
SHOW MASTER STATUS;
EOF
# 2. Get Master Status
MASTER_STATUS=$(mysql -h ${MASTER_HOST} -P ${MASTER_PORT} -uroot -p${MYSQL_ROOT_PASSWORD} -e "SHOW MASTER STATUS\G")
BINLOG_FILE=$(echo "$MASTER_STATUS" | grep "File:" | awk '{print $2}')
BINLOG_POS=$(echo "$MASTER_STATUS" | grep "Position:" | awk '{print $2}')
echo "📊 Master Status:"
echo " Binlog File: ${BINLOG_FILE}"
echo " Binlog Position: ${BINLOG_POS}"
# 3. Configure Replica
mysql -h ${REPLICA_HOST} -P ${REPLICA_PORT} -uroot -p${MYSQL_ROOT_PASSWORD} <<EOF
CHANGE MASTER TO
MASTER_HOST='${MASTER_HOST}',
MASTER_PORT=${MASTER_PORT},
MASTER_USER='${REPLICA_USER}',
MASTER_PASSWORD='${REPLICA_PASSWORD}',
MASTER_LOG_FILE='${BINLOG_FILE}',
MASTER_LOG_POS=${BINLOG_POS};
START SLAVE;
SHOW SLAVE STATUS\G
EOF
echo "✅ Replication setup completed!"
```
---
### راه‌حل 5: Monitoring و Metrics
#### 5.1 Database Health Check Endpoint
**فایل**: `hesabixAPI/adapters/api/v1/health.py` (بهبود یافته)
```python
from sqlalchemy import text
from adapters.db.session import engine, get_db
from adapters.db.routing import get_router
import logging
logger = logging.getLogger(__name__)
@router.get("/health/database")
async def database_health(request: Request, db: Session = Depends(get_db)):
"""بررسی سلامت پایگاه داده و Connection Pool"""
health_status = {
"status": "healthy",
"checks": {}
}
# 1. Check Master Connection
try:
result = db.execute(text("SELECT 1")).scalar()
health_status["checks"]["master"] = {
"status": "ok" if result == 1 else "error",
"response_time_ms": 0 # می‌توان time اضافه کرد
}
except Exception as e:
health_status["checks"]["master"] = {
"status": "error",
"error": str(e)
}
health_status["status"] = "unhealthy"
# 2. Check Connection Pool
pool = engine.pool
pool_stats = {
"pool_size": pool.size(),
"checked_out": pool.checkedout(),
"overflow": pool.overflow(),
"available": pool.size() - pool.checkedout(),
"usage_percent": (pool.checkedout() / (pool.size() + pool.overflow())) * 100 if (pool.size() + pool.overflow()) > 0 else 0
}
health_status["checks"]["connection_pool"] = pool_stats
# Alert اگر Pool بیش از 90% استفاده شده
if pool_stats["usage_percent"] > 90:
logger.warning(f"Connection pool usage high: {pool_stats}")
health_status["status"] = "degraded"
# 3. Check Read Replicas
try:
router = get_router()
read_replicas_status = []
for i, replica_engine in enumerate(router.read_engines):
try:
with replica_engine.connect() as conn:
result = conn.execute(text("SELECT 1")).scalar()
read_replicas_status.append({
"replica_id": i + 1,
"status": "ok" if result == 1 else "error"
})
except Exception as e:
read_replicas_status.append({
"replica_id": i + 1,
"status": "error",
"error": str(e)
})
health_status["checks"]["read_replicas"] = read_replicas_status
# اگر هیچ Replica سالم نباشد، degraded
if all(r["status"] != "ok" for r in read_replicas_status):
health_status["status"] = "degraded"
except Exception as e:
health_status["checks"]["read_replicas"] = {
"error": "Not configured",
"message": str(e)
}
# 4. Check Database Size
try:
result = db.execute(text("""
SELECT
table_schema AS 'Database',
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'
FROM information_schema.TABLES
WHERE table_schema = DATABASE()
GROUP BY table_schema
"""))
size_info = result.fetchone()
health_status["checks"]["database_size"] = {
"size_mb": float(size_info[1]) if size_info else 0
}
except Exception as e:
health_status["checks"]["database_size"] = {
"error": str(e)
}
# 5. Check Slow Queries Count
try:
result = db.execute(text("""
SELECT COUNT(*)
FROM information_schema.processlist
WHERE command != 'Sleep'
AND time > 5
"""))
slow_queries = result.scalar()
health_status["checks"]["slow_queries"] = {
"count": slow_queries or 0
}
if slow_queries and slow_queries > 10:
health_status["status"] = "degraded"
except Exception:
pass # ممکن است دسترسی نداشته باشیم
status_code = 200 if health_status["status"] == "healthy" else 503
return JSONResponse(status_code=status_code, content=health_status)
```
---
## 📋 Migration Plan
### Phase 1: Quick Wins (1-2 هفته)
1. ✅ **افزایش Connection Pool**
- به‌روزرسانی `settings.py`
- افزایش `max_connections` در MySQL
- تست و Monitoring
2. ✅ **بهینه‌سازی MySQL Configuration**
- ایجاد `mysql.conf`
- Restart MySQL
- Monitoring Performance
3. ✅ **Connection Pool Monitoring**
- پیاده‌سازی `db_pool_monitor.py`
- اضافه کردن Metrics به Health Check
### Phase 2: Read Replicas (3-4 هفته)
1. ✅ **Setup Read Replicas**
- نصب و Configure MySQL Replicas
- Setup Replication
- تست Replication
2. ✅ **پیاده‌سازی Database Routing**
- ایجاد `routing.py`
- Update Settings
- Migration Endpoints به Read/Write
3. ✅ **Testing و Rollout**
- تست Read Replicas
- Monitoring Replication Lag
- Rollout تدریجی
### Phase 3: Partitioning (4-6 هفته)
1. ✅ **تحلیل جداول**
- شناسایی جداول بزرگ
- تحلیل Query Patterns
- انتخاب Partitioning Strategy
2. ✅ **پیاده‌سازی Partitioning**
- ایجاد Migration
- Test در Staging
- اجرا در Production (Off-peak hours)
3. ✅ **Partition Maintenance**
- ایجاد Maintenance Scripts
- Setup Cron Jobs
- Monitoring
---
## 📊 Metrics برای Monitoring
### Connection Pool Metrics
- `pool_size`: اندازه Pool
- `checked_out`: تعداد اتصالات در حال استفاده
- `overflow`: تعداد اتصالات اضافی
- `usage_percent`: درصد استفاده
### Database Metrics
- `connection_count`: تعداد کل اتصالات
- `slow_query_count`: تعداد Query های کند
- `replication_lag`: تأخیر Replication (ثانیه)
- `database_size_mb`: اندازه دیتابیس
### Query Metrics
- `avg_query_time`: میانگین زمان Query
- `p95_query_time`: P95 زمان Query
- `query_count_per_second`: تعداد Query در ثانیه
---
## ⚠️ نکات مهم
1. **Backup قبل از هر تغییری**: همیشه Backup کامل بگیرید
2. **Test در Staging**: هر تغییری ابتدا در Staging تست شود
3. **Rollout تدریجی**: تغییرات را به صورت تدریجی Rollout کنید
4. **Monitoring**: همیشه Metrics را Monitor کنید
5. **Documentation**: تمام تغییرات را Document کنید
---
**آماده برای پیاده‌سازی!** 🚀