31 KiB
Executable file
راهنمای جامع بهبود مقیاسپذیری پایگاه داده - Hesabix
📋 فهرست مطالب
- تحلیل مشکلات فعلی
- راهحلهای پیشنهادی
- بهینهسازی Connection Pool
- پیادهسازی Read Replicas
- Partitioning جداول بزرگ
- بهینهسازی MySQL Configuration
- Monitoring و Metrics
- Migration Plan
🔍 تحلیل مشکلات فعلی
1. Connection Pool محدود
وضعیت فعلی:
# 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
# تنظیمات پیشنهادی برای 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
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
"""
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
"""
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
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
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
"""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
"""
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)
[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
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
#!/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 (بهبود یافته)
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 هفته)
-
✅ افزایش Connection Pool
- بهروزرسانی
settings.py - افزایش
max_connectionsدر MySQL - تست و Monitoring
- بهروزرسانی
-
✅ بهینهسازی MySQL Configuration
- ایجاد
mysql.conf - Restart MySQL
- Monitoring Performance
- ایجاد
-
✅ Connection Pool Monitoring
- پیادهسازی
db_pool_monitor.py - اضافه کردن Metrics به Health Check
- پیادهسازی
Phase 2: Read Replicas (3-4 هفته)
-
✅ Setup Read Replicas
- نصب و Configure MySQL Replicas
- Setup Replication
- تست Replication
-
✅ پیادهسازی Database Routing
- ایجاد
routing.py - Update Settings
- Migration Endpoints به Read/Write
- ایجاد
-
✅ Testing و Rollout
- تست Read Replicas
- Monitoring Replication Lag
- Rollout تدریجی
Phase 3: Partitioning (4-6 هفته)
-
✅ تحلیل جداول
- شناسایی جداول بزرگ
- تحلیل Query Patterns
- انتخاب Partitioning Strategy
-
✅ پیادهسازی Partitioning
- ایجاد Migration
- Test در Staging
- اجرا در Production (Off-peak hours)
-
✅ Partition Maintenance
- ایجاد Maintenance Scripts
- Setup Cron Jobs
- Monitoring
📊 Metrics برای Monitoring
Connection Pool Metrics
pool_size: اندازه Poolchecked_out: تعداد اتصالات در حال استفادهoverflow: تعداد اتصالات اضافیusage_percent: درصد استفاده
Database Metrics
connection_count: تعداد کل اتصالاتslow_query_count: تعداد Query های کندreplication_lag: تأخیر Replication (ثانیه)database_size_mb: اندازه دیتابیس
Query Metrics
avg_query_time: میانگین زمان Queryp95_query_time: P95 زمان Queryquery_count_per_second: تعداد Query در ثانیه
⚠️ نکات مهم
- Backup قبل از هر تغییری: همیشه Backup کامل بگیرید
- Test در Staging: هر تغییری ابتدا در Staging تست شود
- Rollout تدریجی: تغییرات را به صورت تدریجی Rollout کنید
- Monitoring: همیشه Metrics را Monitor کنید
- Documentation: تمام تغییرات را Document کنید
آماده برای پیادهسازی! 🚀