o2o-negosium-original/lps/config/server_configs.py
민헌 9c2432c818 feat(lps): process_count 기반 커넥션 풀 자동 산정 — 멀티코어 풀 오버서브스크립션 방지
connection_budget(기본 40)을 두고, 기동 시 process_count 에 맞춰
pool_size/max_overflow 를 역산: (pool+overflow)×2엔진×process_count ≤ budget.
워커를 늘려도 config 가 스스로 예산을 지켜 커넥션 고갈→요청 실패를 예방.

- config_models: MainDBConfig.connection_budget 추가
- server_configs: _autosize_pool(process_count 확정 후 산정) + _apply_pool_env_override
  (우선순위 = 명시 DB_POOL_SIZE > 자동 산정 > toml pool)
- web_main: 기동 로그에 실효 풀/총커넥션/예산 출력
- env: DB_CONNECTION_BUDGET override, docker-compose 에 knob 노출
- tests: test_pool_autosize 10케이스(예산 준수·분할비·비활성·infeasible 바닥)
- docs: operations 2-1 멀티코어/풀 섹션 + loadtest README 자동산정 표

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:59:05 +09:00

109 lines
5.1 KiB
Python
Raw 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.

import os
from config.config_loader import Configs
from config.config_models import (
WebServerConfig, LogConfig, MainDBConfig, NaverConfig, NaverKey, OpenAIConfig, DecodoConfig,
)
# 실행 환경 결정 (기본 local). 환경변수 APP_ENV 로 변경.
APP_ENV = os.environ.get("APP_ENV", "local")
_config_dir = os.path.dirname(__file__)
_config_file = os.path.join(_config_dir, f"config.{APP_ENV}.toml")
# 운영 전제: 항상 APP_ENV=local 로 띄운다 → config.local.toml 사용 (test/docker 도 local 로 실행).
if not os.path.exists(_config_file):
raise FileNotFoundError(f"설정 파일이 없습니다: {_config_file} (APP_ENV={APP_ENV}). APP_ENV=local 로 실행하세요.")
configs = Configs(_config_file)
web_server_config: WebServerConfig = configs.get(WebServerConfig)
log_config: LogConfig = configs.get(LogConfig)
main_db_config: MainDBConfig = configs.get(MainDBConfig)
# 시크릿 포함 설정도 TOML 로 통합. 섹션이 없으면 기본값(빈/비활성).
naver_config: NaverConfig = configs.get(NaverConfig) or NaverConfig()
openai_config: OpenAIConfig = configs.get(OpenAIConfig) or OpenAIConfig()
decodo_config: DecodoConfig = configs.get(DecodoConfig) or DecodoConfig()
# DB 접속 env override (config.local.toml 유지, 도커에서 host 만 교체). 로컬은 env 미설정 → toml 그대로.
def _apply_db_env_override(cfg: MainDBConfig):
h = os.environ.get("DB_HOST")
if h:
cfg.write_host = cfg.read_host = h
if os.environ.get("DB_PORT"):
cfg.write_port = cfg.read_port = int(os.environ["DB_PORT"])
if os.environ.get("DB_USER"):
cfg.write_id = cfg.read_id = os.environ["DB_USER"]
if os.environ.get("DB_PASSWORD"):
cfg.write_pw = cfg.read_pw = os.environ["DB_PASSWORD"]
if os.environ.get("DB_NAME"):
cfg.name = os.environ["DB_NAME"]
# 커넥션 예산 override (자동 산정용). env DB_CONNECTION_BUDGET.
if os.environ.get("DB_CONNECTION_BUDGET"):
cfg.connection_budget = int(os.environ["DB_CONNECTION_BUDGET"])
def _autosize_pool(cfg: MainDBConfig, process_count: int):
"""process_count 기반 커넥션 풀 자동 산정.
실제 동시 커넥션 = (pool_size + max_overflow) × 2엔진(R/W) × process_count.
이 값이 connection_budget 를 넘지 않도록 pool_size/max_overflow 를 역산한다.
budget<=0 이면 비활성(toml 의 pool_size/max_overflow 그대로 사용).
반환: 자동 산정을 수행했으면 (pool_size, max_overflow), 아니면 None.
"""
budget = cfg.connection_budget
if budget <= 0:
return None
pc = max(1, process_count)
# 워커·엔진당 커넥션 = 예산 / (2엔진 × process_count). 최소 1(pool_size>=1) 보장.
# (예산 준수가 최우선 — process_count 가 너무 크면 엔진당 1커넥션까지 줄여서라도 예산을 넘지 않는다)
per_engine = max(1, budget // (2 * pc))
# 정상(pool) 60% + 버스트(overflow) 40% 로 분할.
cfg.pool_size = max(1, round(per_engine * 0.6))
cfg.max_overflow = max(0, per_engine - cfg.pool_size)
return cfg.pool_size, cfg.max_overflow
def _apply_pool_env_override(cfg: MainDBConfig):
"""명시적 풀 override — 자동 산정보다 우선(테스트·특수 배포용)."""
if os.environ.get("DB_POOL_SIZE"):
cfg.pool_size = int(os.environ["DB_POOL_SIZE"])
if os.environ.get("DB_MAX_OVERFLOW"):
cfg.max_overflow = int(os.environ["DB_MAX_OVERFLOW"])
# 시크릿 env override — 프로덕션에선 API 키를 이미지에 굽지 않고 env(또는 시크릿매니저)로 주입한다.
# 로컬은 env 미설정 → config.local.toml 값 그대로. (배포 시 toml 의 시크릿은 비워두고 아래 env 로 주입 권장)
def _apply_secret_env_override():
if os.environ.get("OPENAI_API_KEY"):
openai_config.api_key = os.environ["OPENAI_API_KEY"]
if os.environ.get("OPENAI_MODEL"):
openai_config.model = os.environ["OPENAI_MODEL"]
for k in ("host", "username", "password"):
v = os.environ.get(f"DECODO_{k.upper()}")
if v:
setattr(decodo_config, k, v)
if os.environ.get("DECODO_COST_PER_GB"):
decodo_config.cost_per_gb = float(os.environ["DECODO_COST_PER_GB"])
# NAVER_KEYS="id1:secret1,id2:secret2" 형식으로 키 로테이션 주입
nk = os.environ.get("NAVER_KEYS")
if nk:
naver_config.keys = [NaverKey(id=i, secret=s)
for i, s in (p.split(":", 1) for p in nk.split(",") if ":" in p)]
_apply_db_env_override(main_db_config)
_apply_secret_env_override()
# uvicorn 워커 수(멀티코어) env override — 부하테스트에서 1↔N 비교용(코드/toml 수정 없이).
if os.environ.get("PROCESS_COUNT"):
web_server_config.process_count = int(os.environ["PROCESS_COUNT"])
# 커넥션 풀 자동 산정: process_count(위에서 확정) 기준으로 예산 안에 맞춘다.
# → 멀티워커 배포 시 풀 오버서브스크립션(→커넥션 고갈)을 config 가 스스로 방지.
_pool_autosized = _autosize_pool(main_db_config, web_server_config.process_count)
# 명시적 DB_POOL_SIZE/DB_MAX_OVERFLOW 는 자동 산정보다 우선(최종 override).
_apply_pool_env_override(main_db_config)