From 7a470aad44ea909515e31dcdcc28f9ec3b7082c1 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 18 May 2026 10:31:02 +0900 Subject: [PATCH] =?UTF-8?q?perf(infra):=20NAS=20CPU=20=ED=8F=AD=EC=A3=BC?= =?UTF-8?q?=205=EA=B1=B4=20=EC=9D=BC=EA=B4=84=20fix=20(CHECK=5FPOINT=20?= =?UTF-8?q?=F0=9F=94=B4=20=EC=A6=89=EC=8B=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit J4025 Celeron 2C/2.0GHz에서 oversaturation을 일으키던 5개 패턴 해소. 1) 09:00 cron 스태거링 — agent-office insta_trends 09:00 / lotto 09:05 / youtube 09:10, realestate-lab collect 09:15. 동시 실행 4개가 직렬 분산되어 1분 단위로 분산됨. 2) lotto Monte Carlo 08:05 → 08:30 — stock 08:00 cron과 25분 분리. 3) insta-lab card_renderer.render_slate를 asyncio.Semaphore(1)로 감쌈. 동시 슬레이트 렌더 요청이 와도 Chromium 인스턴스 1개만 직렬 launch. 4) docker-compose healthcheck interval 30s → 60s (9 백엔드 + frontend 총 10개). 30초마다 동시 healthcheck로 인한 CPU 잡음 절반으로. 5) 9개 백엔드 Dockerfile CMD에 --workers 1 명시. 기본값 의존 제거. CHECK_POINT.md 갱신 — 즉시 5건 체크 + 변경 이력 한 줄. 적용 효과 검증: NAS 재기동 후 `docker stats` 비교. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHECK_POINT.md | 211 +++++++++++++++++++++++++++++++++ agent-office/Dockerfile | 2 +- agent-office/app/scheduler.py | 7 +- docker-compose.yml | 20 ++-- insta-lab/Dockerfile | 2 +- insta-lab/app/card_renderer.py | 17 +++ lotto/Dockerfile | 2 +- lotto/app/main.py | 3 +- music-lab/Dockerfile | 2 +- packs-lab/Dockerfile | 2 +- personal/Dockerfile | 2 +- realestate-lab/Dockerfile | 2 +- realestate-lab/app/main.py | 3 +- stock/Dockerfile | 2 +- travel-proxy/Dockerfile | 2 +- 15 files changed, 255 insertions(+), 24 deletions(-) create mode 100644 CHECK_POINT.md diff --git a/CHECK_POINT.md b/CHECK_POINT.md new file mode 100644 index 0000000..57738cb --- /dev/null +++ b/CHECK_POINT.md @@ -0,0 +1,211 @@ +# web-backend CHECK_POINT + +> NAS Docker 11 컨테이너(9 백엔드 + frontend + deployer). Synology Celeron J4025 (2C 2.0GHz) 18GB. +> 2026-05-18 작성 — uvicorn CPU 폭주 진단 결과 정리. + +## 🔴 즉시 (오늘, 총 1시간 5분) + +### 1. 09:00 cron 5분 스태거링 ⭐ 가장 큰 효과 + +**파일**: `agent-office/app/scheduler.py:72-76` +```python +# 변경 전 — 09:00 동시 실행 (CPU 폭주 원인 #1) +scheduler.add_job(_run_insta_trends_collect, "cron", hour=9, minute=0) +scheduler.add_job(_run_lotto_schedule, "cron", day_of_week="mon", hour=9, minute=0) +scheduler.add_job(_run_youtube_research, "cron", hour=9, minute=0) + +# 변경 후 — 5분 스태거링 +scheduler.add_job(_run_insta_trends_collect, "cron", hour=9, minute=0, id="insta_trends") +scheduler.add_job(_run_lotto_schedule, "cron", day_of_week="mon", hour=9, minute=5, id="lotto_curate") +scheduler.add_job(_run_youtube_research, "cron", hour=9, minute=10, id="youtube_research") +``` + +**파일**: `realestate-lab/app/main.py:51` +```python +# 변경 전 +scheduler.add_job(scheduled_collect, "cron", hour=9, minute=0, id="collect") + +# 변경 후 +scheduler.add_job(scheduled_collect, "cron", hour=9, minute=15, id="collect") +``` + +- [x] agent-office scheduler.py 수정 (2026-05-18) +- [x] realestate-lab main.py 수정 (2026-05-18) +- [ ] git commit + push (Gitea Webhook 자동 빌드) + +--- + +### 2. insta-lab Playwright Semaphore(1) ⭐ + +**파일**: `insta-lab/app/main.py` (모듈 레벨 추가) +```python +import asyncio + +# 모듈 레벨에 한 번만 선언 +RENDER_SEMAPHORE = asyncio.Semaphore(1) # Chromium 동시 실행 1개로 제한 + +# 카드 렌더 백그라운드 함수에 감싸기 +async def _bg_render(task_id: str, slate_id: int): + async with RENDER_SEMAPHORE: + await card_renderer.render_slate(slate_id, ...) +``` + +- [x] card_renderer.render_slate를 Semaphore(1)로 감쌈 (2026-05-18, lazy init) +- [ ] 동시 2개 요청 테스트 (curl 동시 2회 → 순차 처리되는지 확인) + +--- + +### 3. healthcheck interval 60s + +**파일**: `docker-compose.yml` (모든 9 컨테이너) +```yaml +# 변경 전 +healthcheck: + interval: 30s + +# 변경 후 +healthcheck: + interval: 60s +``` + +- [x] docker-compose.yml 10개 healthcheck 일괄 변경 (9 백엔드 + frontend, 2026-05-18) +- [ ] `docker compose up -d` 재기동 +- [ ] `docker stats` 로 CPU 5% 정도 감소 확인 + +--- + +### 4. uvicorn --workers 1 명시 + +**모든 Dockerfile CMD**: +```dockerfile +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] +``` + +영향 9 파일 (모두 2026-05-18 적용): +- [x] lotto/Dockerfile +- [x] stock/Dockerfile +- [x] music-lab/Dockerfile +- [x] insta-lab/Dockerfile +- [x] realestate-lab/Dockerfile +- [x] agent-office/Dockerfile +- [x] personal/Dockerfile +- [x] packs-lab/Dockerfile +- [x] travel-proxy/Dockerfile + +→ `docker compose build --no-cache` 후 재기동. + +--- + +### 5. lotto Monte Carlo 08:05 → 08:30 + +**파일**: `lotto/app/main.py:86` +```python +# 변경 전 — stock 08:00과 5분 차이로 겹침 +scheduler.add_job(_run_simulation_job, "cron", hour="0,4,8,12,16,20", minute=5) + +# 변경 후 — 25분 분리 +scheduler.add_job(_run_simulation_job, "cron", hour="0,4,8,12,16,20", minute=30) +``` + +- [x] lotto/app/main.py 수정 (2026-05-18) + +--- + +## 🟡 중기 (1~2주) + +### 6. Chromium Browser Pool 재설계 (insta-lab) +- 매번 launch X → 1개 인스턴스 재사용 +- 카드 10장 렌더 시간 30% 단축 기대 +- [ ] `insta-lab/app/browser_pool.py` 신규 모듈 +- [ ] card_renderer에서 풀 사용 + +### 7. stock 뉴스 스크랩 비동기화 +- **파일**: `stock/app/main.py:104-149` +- `run_scraping_job()` 동기 → BackgroundTask 또는 asyncio +- 08:00 APScheduler 블로킹 해소 +- [ ] async/await 변환 + +### 8. realestate 수집 병렬화 +- **파일**: `realestate-lab/app/main.py:28-37` +- `collect_all()` + `delete_old()` 병렬 → `asyncio.gather` +- 매칭은 순차 유지 (DB 일관성) +- [ ] async 함수 변환 + gather 적용 + +### 9. lotto Monte Carlo 시뮬레이션 빈도 검토 +- 현재 6회/일 (00·04·08·12·16·20) +- 실제 필요 빈도 박재오 결정 — 3회/일(아침·점심·저녁)로 줄이면 CPU 50% 감소 +- [ ] 박재오 의사결정 후 cron 변경 + +--- + +## 🟢 장기 (1개월+) + +### 10. 무거운 작업 Windows AI 서버로 이전 +- 현재 stock Ollama qwen3:14b → NAS 로컬 호출 가능성 (확인 필요) +- 모든 LLM 호출을 Windows AI 192.168.45.59로 통일 +- NAS CPU ~20% 감소 기대 + +### 11. 컨테이너 리소스 제한 +```yaml +services: + insta-lab: + deploy: + resources: + limits: + cpus: "0.5" + memory: "512M" +``` +- [ ] insta-lab·music-lab·stock 등 무거운 컨테이너부터 적용 +- 효과: OOM killer로부터 다른 컨테이너 보호 + +### 12. NAS 업그레이드 검토 +- 현재: Celeron J4025 (2C 2.0GHz) +- 대안: Ryzen N5105 (4C 2.0GHz) NAS — 4코어로 병렬성 2배 +- 박재오 자금·우선순위 결정 + +--- + +## ✅ 최근 완료 (참고) + +- 2026-05-15: insta-lab 신설 (포트 18700, Jinja2 + Playwright + Claude Sonnet) +- 2026-05-16: insta-lab Playwright 1080×1350 PNG 렌더 완성 +- 2026-05-17: agent-office random idle 제거, ADMIN_API_KEY 강화 (stock) +- 2026-05-17: insta-lab minimal theme + design_importer 추가 +- 2026-05-17: blog-lab 트랙 완전 폐기 (docker-compose에 없음, 위키 정정 완료) +- 2026-05-18: 🔴 즉시 5건 일괄 적용 — 09:00 cron 스태거링(insta/lotto/youtube/realestate), lotto Monte Carlo 08:30, insta-lab Semaphore(1), healthcheck 60s, uvicorn --workers 1 명시 (사용자 push + NAS deployer 재기동 대기) + +--- + +## 🔧 진단 커맨드 (NAS bash) + +```bash +# 실시간 CPU 사용 (상위 15) +top -b -n 1 | head -25 + +# 프로세스별 CPU 정렬 +ps aux --sort=-%cpu | head -15 + +# uvicorn·chromium·python 프로세스만 +ps aux | grep -E "uvicorn|chromium|python" | grep -v grep + +# 스케줄러 실행 로그 (최근 50) +docker logs agent-office 2>&1 | grep -E "APScheduler|executing" | tail -50 + +# insta-lab Chromium 프로세스 개수 +docker exec insta-lab ps aux | grep chromium | wc -l + +# 컨테이너별 CPU/메모리 실시간 +docker stats --no-stream +``` + +--- + +## 📚 참고 + +- 진단 풀 보고서: `C:\Users\jaeoh\Documents\Obsidian Vault\raw\2026-05-18-NAS-uvicorn-CPU-진단-개선안.md` +- 위키 페이지: [[사업-개인-웹-플랫폼]] (CPU 부하 진단 섹션 + 컨테이너 표) +- docker-compose.yml: 본 디렉토리 루트 + +## 변경 이력 + +- 2026-05-18: 페이지 신설. 즉시 5건 + 중기 4건 + 장기 3건. 진단 커맨드. diff --git a/agent-office/Dockerfile b/agent-office/Dockerfile index c05ee7c..a599f32 100644 --- a/agent-office/Dockerfile +++ b/agent-office/Dockerfile @@ -7,4 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] diff --git a/agent-office/app/scheduler.py b/agent-office/app/scheduler.py index b694f7c..f9c3285 100644 --- a/agent-office/app/scheduler.py +++ b/agent-office/app/scheduler.py @@ -70,9 +70,10 @@ def init_scheduler(): id="stock_ai_news_sentiment", ) scheduler.add_job(_run_insta_schedule, "cron", hour=9, minute=30, id="insta_pipeline") - scheduler.add_job(_run_insta_trends_collect, "cron", hour=9, minute=0, id="insta_trends_collect") - scheduler.add_job(_run_lotto_schedule, "cron", day_of_week="mon", hour=9, minute=0, id="lotto_curate") - scheduler.add_job(_run_youtube_research, "cron", hour=9, minute=0, id="youtube_research") + # 09:00 cron 스태거링 — Celeron 2C/2.0GHz에서 동시 실행 시 CPU 폭주 (CHECK_POINT FU-A) + scheduler.add_job(_run_insta_trends_collect, "cron", hour=9, minute=0, id="insta_trends_collect") + scheduler.add_job(_run_lotto_schedule, "cron", day_of_week="mon", hour=9, minute=5, id="lotto_curate") + scheduler.add_job(_run_youtube_research, "cron", hour=9, minute=10, id="youtube_research") scheduler.add_job(_send_youtube_weekly_report, "cron", day_of_week="mon", hour=8, minute=0, id="youtube_weekly_report") scheduler.add_job(_poll_pipelines, "interval", seconds=30, id="pipeline_poll") scheduler.start() diff --git a/docker-compose.yml b/docker-compose.yml index 2b94689..204ff46 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: - ${RUNTIME_PATH}/data:/app/data healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s + interval: 60s timeout: 5s retries: 3 @@ -48,7 +48,7 @@ services: - ${RUNTIME_PATH}/data/stock:/app/data healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s + interval: 60s timeout: 5s retries: 3 @@ -82,7 +82,7 @@ services: - ${RUNTIME_PATH:-.}/data/videos:/app/data/videos healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s + interval: 60s timeout: 5s retries: 3 @@ -109,7 +109,7 @@ services: - ${RUNTIME_PATH}/data/insta:/app/data healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s + interval: 60s timeout: 5s retries: 3 @@ -129,7 +129,7 @@ services: - ${RUNTIME_PATH}/data/realestate:/app/data healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s + interval: 60s timeout: 5s retries: 3 @@ -170,7 +170,7 @@ services: - realestate-lab healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s + interval: 60s timeout: 5s retries: 3 @@ -189,7 +189,7 @@ services: - ${RUNTIME_PATH:-.}/data/personal:/app/data healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s + interval: 60s timeout: 5s retries: 3 @@ -216,7 +216,7 @@ services: - ${PACK_DATA_PATH:-./data/packs}:${PACK_BASE_DIR:-/app/data/packs} healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s + interval: 60s timeout: 5s retries: 3 @@ -239,7 +239,7 @@ services: - ${RUNTIME_PATH}/travel-thumbs:/data/thumbs:rw healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s + interval: 60s timeout: 5s retries: 3 @@ -270,7 +270,7 @@ services: - "host.docker.internal:host-gateway" healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"] - interval: 30s + interval: 60s timeout: 5s retries: 3 diff --git a/insta-lab/Dockerfile b/insta-lab/Dockerfile index 2bdafaf..8f5fb7a 100644 --- a/insta-lab/Dockerfile +++ b/insta-lab/Dockerfile @@ -23,4 +23,4 @@ RUN playwright install chromium COPY . . EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] diff --git a/insta-lab/app/card_renderer.py b/insta-lab/app/card_renderer.py index 11055d8..1547d45 100644 --- a/insta-lab/app/card_renderer.py +++ b/insta-lab/app/card_renderer.py @@ -17,6 +17,18 @@ from . import db logger = logging.getLogger(__name__) +# NAS Celeron 2C 환경에서 Chromium을 동시에 여러 인스턴스로 띄우면 CPU/메모리 폭주. +# 슬레이트 렌더는 디스크 I/O와 Chromium launch가 직렬화되어도 충분히 빠르므로 +# 단일 슬롯으로 직렬화한다. (CHECK_POINT FU-C) +_RENDER_SEMAPHORE: asyncio.Semaphore | None = None + + +def _render_semaphore() -> asyncio.Semaphore: + global _RENDER_SEMAPHORE + if _RENDER_SEMAPHORE is None: + _RENDER_SEMAPHORE = asyncio.Semaphore(1) + return _RENDER_SEMAPHORE + def _resolve_template_dir() -> str: """Prefer config CARD_TEMPLATE_DIR if it exists; else fall back to in-repo templates/.""" @@ -64,6 +76,11 @@ def _build_pages(slate: dict) -> List[dict]: async def render_slate(slate_id: int, template: str = "default/card.html.j2") -> List[str]: + async with _render_semaphore(): + return await _render_slate_locked(slate_id, template) + + +async def _render_slate_locked(slate_id: int, template: str) -> List[str]: slate = db.get_card_slate(slate_id) if not slate: raise ValueError(f"slate {slate_id} not found") diff --git a/lotto/Dockerfile b/lotto/Dockerfile index b198bd9..810ec76 100644 --- a/lotto/Dockerfile +++ b/lotto/Dockerfile @@ -15,7 +15,7 @@ ENV PYTHONUNBUFFERED=1 EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] ARG APP_VERSION=dev ENV APP_VERSION=$APP_VERSION diff --git a/lotto/app/main.py b/lotto/app/main.py index c8aa3e0..4d8505d 100644 --- a/lotto/app/main.py +++ b/lotto/app/main.py @@ -83,7 +83,8 @@ def on_startup(): def _run_simulation_job(): run_simulation(n_candidates=20000, top_k=100, best_n=20) - scheduler.add_job(_run_simulation_job, "cron", hour="0,4,8,12,16,20", minute=5) + # stock 08:00 cron과 분리하기 위해 minute=5 → 30 (CHECK_POINT FU-B) + scheduler.add_job(_run_simulation_job, "cron", hour="0,4,8,12,16,20", minute=30) # 3. 토요일 오전 9시 — 다음 회차 공략 리포트 자동 캐싱 def _save_weekly_report_job(): diff --git a/music-lab/Dockerfile b/music-lab/Dockerfile index de71eaa..e74c4b8 100644 --- a/music-lab/Dockerfile +++ b/music-lab/Dockerfile @@ -15,4 +15,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] diff --git a/packs-lab/Dockerfile b/packs-lab/Dockerfile index 98f817e..0276b4c 100644 --- a/packs-lab/Dockerfile +++ b/packs-lab/Dockerfile @@ -15,4 +15,4 @@ ENV PYTHONUNBUFFERED=1 EXPOSE 8000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] diff --git a/personal/Dockerfile b/personal/Dockerfile index c05ee7c..a599f32 100644 --- a/personal/Dockerfile +++ b/personal/Dockerfile @@ -7,4 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] diff --git a/realestate-lab/Dockerfile b/realestate-lab/Dockerfile index c05ee7c..a599f32 100644 --- a/realestate-lab/Dockerfile +++ b/realestate-lab/Dockerfile @@ -7,4 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] diff --git a/realestate-lab/app/main.py b/realestate-lab/app/main.py index b7e6105..2a24790 100644 --- a/realestate-lab/app/main.py +++ b/realestate-lab/app/main.py @@ -48,7 +48,8 @@ def scheduled_status_update(): @asynccontextmanager async def lifespan(app: FastAPI): init_db() - scheduler.add_job(scheduled_collect, "cron", hour=9, minute=0, id="collect") + # 09:00 cron 스태거링 — agent-office 09:00/05/10 이후 (CHECK_POINT FU-A) + scheduler.add_job(scheduled_collect, "cron", hour=9, minute=15, id="collect") scheduler.add_job(scheduled_status_update, "cron", hour=0, minute=0, id="status_update") scheduler.start() logger.info("realestate-lab 시작") diff --git a/stock/Dockerfile b/stock/Dockerfile index 4345e2e..179d622 100644 --- a/stock/Dockerfile +++ b/stock/Dockerfile @@ -6,4 +6,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] diff --git a/travel-proxy/Dockerfile b/travel-proxy/Dockerfile index b307412..0c27f3e 100644 --- a/travel-proxy/Dockerfile +++ b/travel-proxy/Dockerfile @@ -19,7 +19,7 @@ EXPOSE 8000 ENV PYTHONUNBUFFERED=1 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] ARG APP_VERSION=dev ENV APP_VERSION=$APP_VERSION