Files
chocomae/doc/plan/db/260916-load-and-capacity-check/00-check-methods.md
T

908 lines
42 KiB
Markdown
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.
# 운영 서버 부하·용량 확인 방법 (260916)
> 서비스 중인 초코마에 운영 서버(AWS EC2 + MariaDB)에서 아래 3가지를 확인하기 위한 **방법 모음**입니다.
> 각 항목마다 여러 방법을 나열했으니, 권한·설치 가능 여부·측정 기간에 맞춰 골라서 쓰세요.
>
> | 확인 항목 | 문서 위치 |
> |---|---|
> | ① EC2 인스턴스에서 MariaDB에 걸리는 성능 부하 | [A. MariaDB 성능 부하 측정](#a-mariadb-성능-부하-측정) |
> | ② 접속 서비스(chocomae / chocoadmin)별 pool 할당·부하 | [B. 접속 서비스별 연결 pool·부하 측정](#b-접속-서비스별-연결-pool부하-측정) |
> | ③ `best_record` 등 기록 테이블에 데이터가 몇 개까지 들어가는지 | [C. 테이블 최대 저장 가능 행 수](#c-테이블-최대-저장-가능-행-수) |
>
> ⚠️ 이 문서는 **측정 방법만** 다룹니다. 실제 값은 `result/`에 저장하고, 결론은 [90-결과-기록-양식](#90-결과-기록-양식)에 채웁니다.
---
## 0. 사전 준비
### 0-1. 용어 정리
- 질문의 **`best_score` 테이블**은 실제 스키마에 없습니다. 점수/기록을 담는 테이블은 아래 4개입니다. 이 문서에서는 이 4개를 "기록 테이블"로 부릅니다.
| 테이블 | 내용 | PK |
|---|---|---|
| `best_record` | 게임 플레이 기록 (가장 큼, 약 121만 행) | `BestRecordID INT UNSIGNED AUTO_INCREMENT` |
| `app_highest_record` | 앱별 최고 기록 | `AppHighestRecordID INT UNSIGNED AUTO_INCREMENT` |
| `typing_exam_record` | 타자 시험 기록 (약 13만 행) | `TypingExamRecordID INT UNSIGNED AUTO_INCREMENT` |
| `typing_exam_highest_record` | 타자 시험 최고 기록 | `TypingExamHighestRecordID INT UNSIGNED AUTO_INCREMENT` |
### 0-2. 접속 경로 3가지
측정 방법마다 필요한 접속 경로가 다릅니다.
| 경로 | 접속 방법 | 할 수 있는 것 |
|---|---|---|
| **(가) EC2 SSH** | `ssh ubuntu@13.124.33.235` (chocomae.jisangs.com) | OS 지표(CPU/메모리/디스크 I/O), 설정 파일, 로그 파일, 로컬 mariadb 접속 |
| **(나) 원격 DB 클라이언트** | `mariadb -h chocomae.jinaju.com -P 3306 -u <계정> -p chocomae` | SQL로 가능한 모든 측정 (상태 변수, PROCESSLIST, EXPLAIN) |
| **(다) 시놀로지 SSH** | `ssh happyhome@jisangs.synology.me` | chocoadmin 컨테이너 설정·로그, 시놀로지→운영 DB 연결 확인 |
> ⚠️ 2026-09-15 작업에 쓰던 임시 계정 `jisangs`는 작업 후 **삭제**했습니다. 이번 측정에도 조회 전용 계정이 필요하니, 아래 중 하나를 먼저 정하세요.
>
> 1. 관리자(root) 계정으로 직접 측정 (가장 간단, 권한 문제 없음)
> 2. 측정 전용 임시 계정을 다시 만들고 끝나면 삭제
> ```sql
> -- 관리자 계정으로 실행
> CREATE USER 'perfcheck'@'182.217.174.221' IDENTIFIED BY '<임시 비밀번호>';
> GRANT SELECT, PROCESS, REPLICATION CLIENT ON *.* TO 'perfcheck'@'182.217.174.221';
> GRANT SELECT ON mysql.slow_log TO 'perfcheck'@'182.217.174.221';
> -- 측정 종료 후
> -- DROP USER 'perfcheck'@'182.217.174.221';
> ```
> 3. EC2에 SSH로 들어가 `localhost` 소켓으로 접속 (원격 계정을 새로 만들지 않아도 됨)
### 0-3. 측정에 필요한 권한
| 측정 내용 | 필요 권한 |
|---|---|
| `SHOW GLOBAL STATUS`, `SHOW VARIABLES`, `information_schema.TABLES` | 기본 (일반 계정 가능) |
| `SHOW FULL PROCESSLIST` (다른 계정 세션까지 보기), `SHOW ENGINE INNODB STATUS` | `PROCESS` |
| `mysql.slow_log` 조회 | `mysql.slow_log` SELECT |
| `SET GLOBAL ...` (slow log 켜기, `userstat` 켜기) | `SUPER` |
| OS 지표, 설정 파일, 로그 파일 | EC2 SSH (`ubuntu` 계정, 일부는 `sudo`) |
### 0-4. 결과 저장 위치
```bash
cd doc/plan/db/260916-load-and-capacity-check/result
```
> 개인정보·비밀번호 해시가 섞일 수 있는 파일(`*grants*`, 덤프)은 커밋하지 마세요. 2026-09-15 작업의 관례를 따릅니다.
### 0-5. MariaDB가 도커인지 호스트 설치인지 먼저 확인
문서마다 명령어가 달라지므로 **가장 먼저** 확인합니다. (EC2 SSH)
```bash
sudo docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
systemctl status mariadb --no-pager | head -5
ls -l /var/lib/mysql | head -5
```
- 도커면: 이후 모든 `mariadb ...` 명령 앞에 `sudo docker exec -i <컨테이너명> ` 을 붙입니다.
- 호스트 설치면: 명령을 그대로 씁니다. (README 기준 설정은 `/etc/mysql`, 로그는 `/var/log/mysql`)
---
## A. MariaDB 성능 부하 측정
> "지금 이 EC2에서 MariaDB가 얼마나 힘들어하는가"를 보는 방법들입니다.
> **OS 레벨(A-1)** 과 **DB 레벨(A-2~A-5)** 을 같이 봐야 원인을 가릴 수 있습니다.
### A-0. 방법 비교
| 방법 | 보는 것 | 설치 필요 | 측정 부하 | 지속 관찰 | 추천도 |
|---|---|---|---|---|---|
| A-1 OS 지표 | CPU·메모리·디스크 I/O·스왑 | 없음(일부 `sysstat`) | 매우 낮음 | 스냅샷~단기 | ★★★ |
| A-2 상태 변수 델타 | QPS, 버퍼 풀 적중률, 임시 테이블, 잠금 | 없음 | 매우 낮음 | 단기 | ★★★ |
| A-3 PROCESSLIST·INNODB STATUS | 지금 실행 중인 쿼리, 대기·잠금 | 없음 | 매우 낮음 | 스냅샷 | ★★★ |
| A-4 slow query log | 느린 쿼리 목록·빈도 | 없음 | 낮음(설정 주의) | 장기 | ★★★ |
| A-5 userstat / performance_schema | 쿼리 다이제스트별·계정별 누적 부하 | 없음(설정 변경) | 낮음~중간 | 장기 | ★★☆ |
| A-6 모니터링 도구 | 시계열 그래프, 알림 | 있음 | 낮음 | 상시 | ★★☆ |
**추천 조합:** 먼저 A-1 + A-2 + A-3으로 현재 상태를 잡고, A-4를 24시간 돌려 실제 서비스 쿼리를 확인합니다. 상시 감시가 필요해지면 A-6.
---
### 방법 A-1. OS 레벨 지표 (EC2 SSH)
**A-1-1. 한 번에 훑기**
```bash
# 인스턴스 사양·부하 평균
nproc; free -h; uptime
# 프로세스별 CPU·메모리 상위 10개 (mysqld가 몇 위인지)
ps -eo pid,comm,pcpu,pmem,rss --sort=-pcpu | head -11
```
**A-1-2. 실시간 관찰**
```bash
top -b -n 3 -d 5 | grep -E 'Cpu|Mem|mysqld' > os_top.txt
cat os_top.txt
```
**A-1-3. CPU·디스크 I/O 시계열 (sysstat 필요)**
```bash
sudo apt-get install -y sysstat # 이미 있으면 생략
# 5초 간격 12회 = 1분치 CPU·메모리·스왑
vmstat 5 12 > os_vmstat.txt
# 5초 간격 12회 디스크 I/O (await, %util 확인)
iostat -dx 5 12 > os_iostat.txt
# mysqld 프로세스만 따로
pidstat -p $(pgrep -o mysqld) 5 12 > os_pidstat_mysqld.txt
```
**판단 기준**
| 지표 | 정상 | 주의 |
|---|---|---|
| `vmstat` `r`(실행 대기) | vCPU 수 이하 | vCPU 수보다 계속 큼 → CPU 포화 |
| `vmstat` `si/so`(스왑) | 0 | 0보다 큼 → 메모리 부족, DB 성능 급락 |
| `iostat` `%util` | 50% 이하 | 90% 이상 지속 → 디스크 I/O 병목 |
| `iostat` `await` | 10ms 이하(EBS gp3) | 수십 ms 이상 → I/O 대기 |
| `free -h` `available` | 여유 있음 | 0에 가까움 → 버퍼 풀 축소 검토 |
**A-1-4. 도커로 돌고 있다면**
```bash
sudo docker stats --no-stream
```
**A-1-5. AWS CloudWatch (SSH 없이 확인)**
EC2 콘솔 → 대상 인스턴스 → **모니터링** 탭에서 `CPUUtilization`, `EBSReadOps`/`EBSWriteOps`, `NetworkIn/Out`을 기간별로 봅니다. 기본 지표는 5분 간격이며, 메모리·디스크 사용률은 CloudWatch Agent를 설치해야 나옵니다. **과거 추이**를 보는 데 가장 편합니다.
---
### 방법 A-2. MariaDB 상태 변수 델타 측정
절대 누적값은 서버 기동 이후 합계라 의미가 약합니다. **일정 시간 동안의 증가량(델타)** 을 봐야 합니다.
**A-2-1. 가장 간단한 방법 — `mariadb-admin`의 상대값 출력**
```bash
# 10초 간격 6회, 이전 값 대비 증가량만 출력
mariadb-admin -h chocomae.jinaju.com -u <계정> -p extended-status -i 10 -c 6 -r \
| grep -vE '\|\s+0\s+\|' > db_status_delta.txt
cat db_status_delta.txt
```
**A-2-2. 요약 한 줄 보기**
```bash
# 5초 간격으로 Queries/초, Threads, Opens, Slow queries 표시 (Ctrl+C로 종료)
mariadb-admin -h chocomae.jinaju.com -u <계정> -p status -i 5
```
**A-2-3. 직접 SQL로 60초 델타 뽑기**
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p chocomae -N -B -e "
SHOW GLOBAL STATUS;" > st1.txt
sleep 60
mariadb -h chocomae.jinaju.com -u <계정> -p chocomae -N -B -e "
SHOW GLOBAL STATUS;" > st2.txt
join st1.txt st2.txt | awk '{d=$3-$2; if (d>0) printf "%-40s %12d\n", $1, d}' \
| sort -k2 -nr | head -40 > db_status_delta_60s.txt
cat db_status_delta_60s.txt
```
**A-2-4. 꼭 봐야 할 값**
| 상태 변수 | 의미 | 해석 |
|---|---|---|
| `Questions`, `Com_select`, `Com_insert` | 처리한 쿼리 수 | 60초 델타 ÷ 60 = QPS |
| `Threads_connected` / `Threads_running` | 연결 수 / 실제 실행 중 | `Threads_running`이 지속적으로 vCPU 수 이상이면 과부하 |
| `Innodb_buffer_pool_read_requests` vs `Innodb_buffer_pool_reads` | 논리 읽기 vs 디스크 읽기 | 적중률 = (1 - reads/read_requests) × 100, **99% 미만이면 버퍼 풀 부족 의심** |
| `Created_tmp_disk_tables` | 디스크 임시 테이블 | 증가하면 `GROUP BY`/`ORDER BY`가 디스크로 넘어감 |
| `Select_scan`, `Select_full_join` | 풀 스캔 쿼리 수 | 증가하면 인덱스 미사용 쿼리 존재 |
| `Slow_queries` | 느린 쿼리 수 | A-4와 함께 확인 |
| `Table_locks_waited`, `Innodb_row_lock_waits`, `Innodb_row_lock_time_avg` | 잠금 대기 | 0에 가까워야 정상 |
| `Aborted_connects`, `Connection_errors_max_connections` | 연결 실패 | 0보다 크면 B 항목에서 원인 추적 |
**A-2-5. 버퍼 풀 적중률·크기 한 번에 계산**
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p -e "
SELECT
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_VARIABLES
WHERE VARIABLE_NAME='innodb_buffer_pool_size')/1024/1024 AS buffer_pool_mb,
(SELECT ROUND(SUM(DATA_LENGTH+INDEX_LENGTH)/1024/1024,1) FROM information_schema.TABLES
WHERE TABLE_SCHEMA='chocomae') AS chocomae_total_mb,
ROUND(100 - (
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='Innodb_buffer_pool_reads') /
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='Innodb_buffer_pool_read_requests')
) * 100, 3) AS hit_rate_pct;" > db_buffer_pool.txt
cat db_buffer_pool.txt
```
> `chocomae_total_mb`가 `buffer_pool_mb`보다 작으면 데이터 전체가 메모리에 올라가므로 디스크 I/O 부하는 거의 없습니다. 이 값은 C-3(실용적 상한 계산)에서 다시 씁니다.
---
### 방법 A-3. 실행 중인 쿼리·잠금 관찰
**A-3-1. 지금 무엇이 돌고 있는가**
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p -e "
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, LEFT(INFO,120) AS query
FROM information_schema.PROCESSLIST
WHERE COMMAND <> 'Sleep'
ORDER BY TIME DESC;"
```
**A-3-2. 1초 간격으로 반복 관찰 (부하가 걸리는 시간대에)**
```bash
for i in $(seq 1 60); do
date +%FT%T
mariadb -h chocomae.jinaju.com -u <계정> -p<비밀번호> -N -B -e "
SELECT COUNT(*) AS running FROM information_schema.PROCESSLIST WHERE COMMAND<>'Sleep';"
sleep 1
done > db_running_1min.txt
```
> 비밀번호를 명령줄에 쓰면 셸 히스토리에 남습니다. `~/.my.cnf`(`[client]` 섹션)에 적어두고 `-p`를 빼는 편이 안전합니다.
**A-3-3. InnoDB 내부 상태 (잠금·대기·I/O)**
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p -e "SHOW ENGINE INNODB STATUS\G" > db_innodb_status.txt
grep -A12 'TRANSACTIONS' db_innodb_status.txt
grep -A10 'BUFFER POOL AND MEMORY' db_innodb_status.txt
grep -A8 'LATEST DETECTED DEADLOCK' db_innodb_status.txt
```
**A-3-4. 대기 중인 잠금만 콕 집어 보기**
```sql
-- MariaDB 10.5+ (information_schema.INNODB_TRX + metadata lock 정보)
SELECT trx_id, trx_state, trx_started, trx_wait_started, trx_mysql_thread_id, LEFT(trx_query,100)
FROM information_schema.INNODB_TRX
ORDER BY trx_started;
SELECT * FROM information_schema.METADATA_LOCK_INFO; -- 플러그인 설치 시
```
---
### 방법 A-4. Slow query log 분석
2026-09-15 작업 때 이미 켰던 경로라 재사용이 쉽습니다.
**A-4-1. 현재 설정 확인**
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p -e "
SHOW GLOBAL VARIABLES WHERE Variable_name IN
('slow_query_log','slow_query_log_file','long_query_time','log_output',
'log_queries_not_using_indexes','log_slow_admin_statements','min_examined_row_limit');" > slow_log_settings.txt
cat slow_log_settings.txt
```
**A-4-2. 측정 기간에만 임계값 낮추기 (`SUPER` 필요)**
```sql
-- 원래 값을 먼저 기록해 두고 실행
SET GLOBAL long_query_time = 0.5; -- 0.5초 이상 쿼리를 기록
SET GLOBAL slow_query_log = ON;
-- 측정 종료 후 원래 값으로 복원 (예: 3초)
-- SET GLOBAL long_query_time = 3;
```
> ⚠️ `log_queries_not_using_indexes`는 켜지 마세요. 인덱스를 안 쓰는 모든 쿼리가 기록돼 로그가 폭증합니다. `log_output`에 `TABLE`이 포함돼 있으면 `mysql.slow_log`가 계속 커지고 자동으로 비워지지 않습니다.
**A-4-3. 파일 로그 분석 (EC2 SSH)**
```bash
# 요약: 실행 시간 합계가 큰 순서로 정렬
sudo mariadb-dumpslow -s t -t 20 /var/log/mysql/mariadb-slow.log > slow_top20.txt
cat slow_top20.txt
# 더 정밀한 분석이 필요하면 (percona-toolkit 설치 필요)
sudo apt-get install -y percona-toolkit
sudo pt-query-digest /var/log/mysql/mariadb-slow.log > slow_digest.txt
```
**A-4-4. 테이블 로그 분석 (`log_output`에 `TABLE`이 있을 때)**
```sql
-- 일자별 건수
SELECT DATE(start_time) AS d, COUNT(*) AS cnt, ROUND(MAX(query_time_sec),1) AS max_sec
FROM (SELECT start_time, TIME_TO_SEC(query_time) AS query_time_sec FROM mysql.slow_log) t
GROUP BY d ORDER BY d;
-- 계정·호스트별 느린 쿼리 (B 항목에서 서비스 구분에 활용)
SELECT user_host, COUNT(*) AS cnt, ROUND(AVG(TIME_TO_SEC(query_time)),2) AS avg_sec
FROM mysql.slow_log GROUP BY user_host ORDER BY cnt DESC;
```
> 2026-09-15 측정에서는 slow log에 **애플리케이션 쿼리가 한 건도 없었고**, 매일 04:00 백업 덤프의 `best_record` 전체 SELECT(약 5초)만 기록됐습니다. 이번에도 같은 항목이 계속 잡히니 성능 저하로 오해하지 마세요.
---
### 방법 A-5. 누적 통계 (userstat / performance_schema)
**A-5-1. MariaDB `userstat` (가볍고 설치 불필요, `SUPER` 필요)**
```sql
SET GLOBAL userstat = 1; -- 켠 시점부터 누적 (재시작하면 꺼짐)
-- 잠시 서비스 트래픽을 받은 뒤
SELECT * FROM information_schema.CLIENT_STATISTICS\G -- 접속 호스트별
SELECT * FROM information_schema.USER_STATISTICS\G -- 계정별
SELECT * FROM information_schema.TABLE_STATISTICS
WHERE TABLE_SCHEMA='chocomae' ORDER BY ROWS_READ DESC;
SELECT * FROM information_schema.INDEX_STATISTICS
WHERE TABLE_SCHEMA='chocomae' ORDER BY ROWS_READ DESC;
FLUSH CLIENT_STATISTICS; -- 구간 측정을 하려면 초기화 후 다시 수집
```
- **B 항목(서비스별 부하)에 가장 직접적인 방법**입니다. `CLIENT_STATISTICS`는 접속 호스트별 `CONCURRENT_CONNECTIONS`, `CONNECTED_TIME`, `BUSY_TIME`, `ROWS_READ`, `SELECT_COMMANDS` 등을 보여줍니다.
- `INDEX_STATISTICS`에 안 나오는 인덱스는 **한 번도 안 쓰인 인덱스**입니다. 불필요한 인덱스 정리 판단에 쓰세요.
- 영구 적용하려면 설정 파일에 `userstat=1`을 넣고 재시작해야 합니다.
**A-5-2. performance_schema 다이제스트 (기본 OFF)**
```sql
SHOW VARIABLES LIKE 'performance_schema'; -- OFF면 아래는 못 씀
```
OFF인 경우 `/etc/mysql/my.cnf`(또는 `conf.d/*.cnf`)에 `performance_schema=ON`을 넣고 **재시작**해야 합니다. 서비스 중단이 생기므로 급하지 않으면 A-4/A-5-1로 대체하세요.
```sql
-- 총 실행 시간이 큰 쿼리 유형 순
SELECT SCHEMA_NAME, DIGEST_TEXT, COUNT_STAR,
ROUND(SUM_TIMER_WAIT/1e12,2) AS total_sec,
ROUND(AVG_TIMER_WAIT/1e9,2) AS avg_ms,
SUM_ROWS_EXAMINED, SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME='chocomae'
ORDER BY SUM_TIMER_WAIT DESC LIMIT 20;
-- 계정별 부하 (B 항목에도 사용)
SELECT * FROM performance_schema.events_statements_summary_by_account_by_event_name
WHERE EVENT_NAME='statement/sql/select' ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;
```
---
### 방법 A-6. 상시 모니터링 도구
| 도구 | 설치 위치 | 특징 |
|---|---|---|
| `mytop` / `innotop` | EC2 (`apt-get install mytop percona-toolkit`) | 터미널 실시간 대시보드. QPS·슬로우·연결·실행 쿼리를 한 화면에서 봄. 가장 가볍게 시작 가능 |
| Netdata | EC2 (에이전트 1개) | 설치 5분, MySQL 플러그인 자동 인식, 웹 UI로 시계열 확인 |
| Prometheus + `mysqld_exporter` + Grafana | 수집기는 시놀로지, exporter는 EC2 | 장기 보관·대시보드·알림. 이미 시놀로지 도커가 있으니 구성 부담이 적음 |
| Percona PMM | 도커 1개 + 에이전트 | MySQL 전용 대시보드·쿼리 분석기(QAN)가 강력. 리소스는 좀 더 씀 |
| AWS CloudWatch (+Agent) | EC2 | 인스턴스 지표·알림. DB 내부 지표는 안 보임 |
**mytop 예시**
```bash
sudo apt-get install -y mytop
mytop -h 127.0.0.1 -u root -p --dbname chocomae
```
> **선택 가이드:** 이번처럼 "한 번 확인"이면 A-1~A-5로 충분합니다. "앞으로 계속 보겠다"면 `mysqld_exporter` + 시놀로지 Grafana 조합이 운영 부담 대비 효과가 가장 좋습니다.
---
## B. 접속 서비스별 연결 pool·부하 측정
### B-0. 먼저 알아야 할 구조
| 서비스 | 위치 | 접속 방식 | pool 존재 여부 |
|---|---|---|---|
| **chocomae** (PHP) | 같은 EC2 | `new mysqli($db_host, ...)` — [connect_db.php:17](../../../../src/web/server/setup/connect_db.php#L17), `localhost`(또는 도커 호스트명) | **pool 없음.** PHP 요청마다 새 연결을 맺고 요청이 끝나면 끊습니다. `p:` 접두사(persistent)를 쓰지 않으므로, 동시 연결 수의 상한은 사실상 **Apache의 동시 프로세스 수(MaxRequestWorkers)** 입니다 |
| **chocoadmin** (Next.js/Node) | 시놀로지 도커 | 인터넷 경유로 `chocomae.jinaju.com:3306` | Node의 MySQL 드라이버(`mysql2` 등)를 쓰면 **커넥션 풀 존재**. `connectionLimit` 기본값은 보통 10. 컨테이너/프로세스 수만큼 배수가 됨 |
| **배치/백업** | EC2 cron, 시놀로지 cron | 단발 연결 | 매일 04:00 풀백업이 대표적 |
**따라서 확인할 것은 3가지입니다.**
1. DB가 허용하는 상한 (`max_connections`)과 실제 최대 사용치 (`Max_used_connections`)
2. 출처별 현재·최대 연결 수 (EC2 로컬 vs 시놀로지 공인 IP)
3. 각 애플리케이션 쪽 설정값 (Apache 워커 수, Node pool `connectionLimit`)
### B-0-1. 방법 비교
| 방법 | 보는 것 | 서비스 구분 가능? | 필요 권한 |
|---|---|---|---|
| B-1 연결 상한·사용치 | 전체 연결 수, 한계 근접 여부 | ✗ | 기본 |
| B-2 PROCESSLIST 집계 | 출처 IP·계정별 현재 연결 수 | ○ (IP로) | `PROCESS` |
| B-3 userstat | 출처별 누적 연결 시간·쿼리 수·읽은 행 수 | ◎ (가장 정확) | `SUPER` + 조회 |
| B-4 계정 분리 | 서비스별 완전 분리 + 상한 제한 | ◎ | `CREATE USER`, `GRANT` |
| B-5 애플리케이션 설정 | pool 할당값 자체 | ◎ | 서버 접근 |
| B-6 네트워크 소켓 | TCP 연결 수·상태 | ○ (IP로) | EC2 SSH |
| B-7 주기 샘플링 | 시간대별 추이 | ○ | `PROCESS` + cron |
---
### 방법 B-1. 연결 상한과 실제 사용치
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p -e "
SHOW GLOBAL VARIABLES WHERE Variable_name IN
('max_connections','max_user_connections','thread_handling','thread_pool_size',
'wait_timeout','interactive_timeout','back_log','table_open_cache','open_files_limit');
SHOW GLOBAL STATUS WHERE Variable_name IN
('Threads_connected','Threads_running','Threads_created','Threads_cached',
'Max_used_connections','Max_used_connections_time','Connections',
'Aborted_connects','Aborted_clients','Connection_errors_max_connections');" > conn_limits.txt
cat conn_limits.txt
```
**해석**
| 값 | 의미 |
|---|---|
| `Max_used_connections` / `max_connections` | **연결 여유율.** 80%를 넘으면 상한을 올리거나 pool을 줄여야 합니다 |
| `Max_used_connections_time` | 최고치를 찍은 시각 → 그 시간대에 무슨 작업이 있었는지 추적 |
| `Connection_errors_max_connections` | 0보다 크면 **연결 거부가 실제로 발생**한 것 |
| `Aborted_connects` | 인증 실패·타임아웃. 시놀로지↔AWS 인터넷 구간 문제의 신호일 수 있음 |
| `Threads_created``Connections`의 일부 | 스레드 캐시 부족 (`thread_cache_size` 조정 대상) |
| `wait_timeout` | pool의 유휴 연결이 이 시간을 넘기면 서버가 끊습니다. chocoadmin에서 간헐적 `ECONNRESET`/`Connection lost`가 난다면 1순위 원인 |
---
### 방법 B-2. PROCESSLIST로 출처별 집계
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p -e "
SELECT USER,
SUBSTRING_INDEX(HOST, ':', 1) AS client_ip,
DB,
COUNT(*) AS conns,
SUM(COMMAND <> 'Sleep') AS active,
SUM(COMMAND = 'Sleep') AS idle,
MAX(TIME) AS max_idle_or_run_sec
FROM information_schema.PROCESSLIST
GROUP BY USER, client_ip, DB
ORDER BY conns DESC;" > conn_by_source.txt
cat conn_by_source.txt
```
**읽는 법**
- `client_ip``localhost`/`127.0.0.1`/도커 내부 IP(`172.x`) → **chocomae(PHP)**
- `client_ip`가 시놀로지 공인 IP → **chocoadmin**
- `idle`이 크고 `max_idle_or_run_sec`이 수백 초 → pool이 유휴 연결을 계속 붙잡고 있는 상태 (정상이지만 `max_connections` 여유를 잡아먹음)
- `active`가 계속 큼 → 실제 쿼리 부하
> 시놀로지 공인 IP를 모른다면 시놀로지에서 `curl -s ifconfig.me` 로 확인하세요. README 기준 출발지 IP는 `182.217.174.221`입니다.
---
### 방법 B-3. userstat으로 출처별 누적 부하 (가장 정확)
```sql
SET GLOBAL userstat = 1;
FLUSH CLIENT_STATISTICS; -- 구간 측정 시작점
-- (서비스 트래픽을 받는 동안 대기: 예를 들어 1시간)
SELECT CLIENT,
TOTAL_CONNECTIONS, CONCURRENT_CONNECTIONS,
ROUND(CONNECTED_TIME/60,1) AS connected_min,
ROUND(BUSY_TIME,1) AS busy_sec,
ROUND(CPU_TIME,1) AS cpu_sec,
ROWS_READ, ROWS_SENT, SELECT_COMMANDS, UPDATE_COMMANDS, OTHER_COMMANDS
FROM information_schema.CLIENT_STATISTICS
ORDER BY BUSY_TIME DESC;
```
- `BUSY_TIME`·`CPU_TIME` = **그 출처가 DB를 실제로 점유한 시간**입니다. "chocomae와 chocoadmin 중 어느 쪽이 DB를 더 쓰는가"에 대한 가장 직접적인 답입니다.
- `USER_STATISTICS`는 같은 형식을 **계정 단위**로 보여줍니다. B-4(계정 분리)를 먼저 하면 IP가 아니라 계정만으로 깔끔하게 구분됩니다.
---
### 방법 B-4. 서비스별 DB 계정 분리 + 연결 상한 지정 (구조 개선)
지금은 chocomae와 chocoadmin이 **같은 `chocomae` 계정**을 쓸 가능성이 큽니다(README 기준 계정은 `root`/`chocomae`/`backup` 3개). 계정을 나누면 측정과 제어가 동시에 쉬워집니다.
```sql
-- 관리자 계정으로 실행
CREATE USER 'chocoadmin'@'182.217.174.221' IDENTIFIED BY '<새 비밀번호>';
GRANT SELECT, INSERT, UPDATE, DELETE ON chocomae.* TO 'chocoadmin'@'182.217.174.221';
-- 이 계정 하나가 쓸 수 있는 동시 연결 수 상한 (폭주 시 서비스 보호)
ALTER USER 'chocoadmin'@'182.217.174.221' WITH MAX_USER_CONNECTIONS 20;
FLUSH PRIVILEGES;
SHOW GRANTS FOR 'chocoadmin'@'182.217.174.221';
```
- 계정을 나눈 뒤에는 B-2/B-3의 집계가 `USER` 기준으로 바로 읽힙니다.
- `MAX_USER_CONNECTIONS`를 걸어두면 chocoadmin 쪽 pool 설정 실수로 운영 서비스가 연결을 못 맺는 사고를 막습니다.
- ⚠️ 계정 변경은 chocoadmin 배포와 함께 진행해야 하니, 측정만이 목적이라면 B-2/B-3(IP 기준)으로 충분합니다.
---
### 방법 B-5. 애플리케이션 쪽 설정 확인 (pool 할당값 자체)
**B-5-1. chocomae (PHP / Apache) — EC2 SSH**
```bash
# 동시 처리 가능한 요청 수 = 사실상 최대 동시 DB 연결 수
apache2ctl -V | grep -i 'Server MPM'
grep -RnE 'MaxRequestWorkers|ServerLimit|StartServers|MaxConnectionsPerChild' /etc/apache2/mods-available/mpm_*.conf /etc/apache2/apache2.conf
# php-fpm을 쓴다면
grep -RnE '^(pm|pm\.max_children|pm\.start_servers|pm\.max_spare_servers)' /etc/php/*/fpm/pool.d/*.conf 2>/dev/null
# persistent connection 관련 설정 (현재 코드는 사용 안 함)
php -i | grep -iE 'mysqli.allow_persistent|mysqli.max_persistent|mysqli.max_links|default_socket_timeout'
```
> **판단:** `MaxRequestWorkers`(prefork 기본 150) ≤ `max_connections`여야 합니다. 그렇지 않으면 트래픽 급증 시 PHP가 "Too many connections"를 만납니다.
>
> 현재 코드는 요청마다 연결을 새로 맺습니다(`new mysqli`). 연결 비용이 부담되면 `p:localhost` 형태의 persistent 연결을 검토할 수 있지만, 연결이 프로세스에 남아 `max_connections`를 오래 점유하므로 **먼저 B-1의 여유율을 확인한 뒤** 결정하세요.
**B-5-2. chocoadmin (Next.js / Node) — 시놀로지 SSH**
```bash
cd /volume1/docker/service/jinaju/chocoadmin
# pool 설정값 찾기
grep -RniE 'createPool|connectionLimit|pool|queueLimit|connectTimeout|enableKeepAlive' \
--include='*.ts' --include='*.js' --include='*.mjs' . | grep -v node_modules | head -40
# 환경변수 (비밀번호는 출력하지 말 것)
grep -vE 'PASS|PWD|SECRET|TOKEN' .env.production
# 실행 중인 컨테이너·프로세스 수 (pool이 몇 배로 늘어나는지)
sudo docker ps --filter name=chocoadmin
sudo docker exec chocoadmin sh -c 'ps -eo pid,comm | head'
```
**확인 포인트**
| 항목 | 확인 내용 |
|---|---|
| `connectionLimit` | 명시하지 않았다면 `mysql2` 기본값 10 |
| 컨테이너/워커 수 | pool은 **프로세스마다 하나**입니다. Next.js를 여러 워커로 띄우면 `connectionLimit × 워커 수`가 실제 상한 |
| 모듈 스코프 재사용 | pool을 요청마다 만들면 연결이 계속 늘어납니다. 모듈 최상단에서 한 번만 생성하는지 확인 |
| `connectTimeout`, `enableKeepAlive` | 시놀로지→AWS 인터넷 구간이라 지연·끊김 대비 필요 |
| `wait_timeout`(B-1) 대비 유휴 시간 | 서버가 먼저 끊으면 죽은 연결을 재사용하다 오류가 납니다 |
**B-5-3. chocoadmin 쪽 실제 연결 확인 (시놀로지 SSH)**
```bash
# 컨테이너에서 운영 DB로 맺은 연결 수
sudo docker exec chocoadmin sh -c "ss -tn state established '( dport = :3306 )' 2>/dev/null | wc -l" \
|| sudo netstat -tn | grep ':3306' | wc -l
```
---
### 방법 B-6. 네트워크 소켓 레벨 확인 (EC2 SSH)
DB에 로그인하지 않고도 **누가 3306에 붙어 있는지** 셀 수 있습니다.
```bash
# 출처 IP별 3306 연결 수
sudo ss -tn state established '( sport = :3306 )' \
| awk 'NR>1 {split($5,a,":"); print a[1]}' | sort | uniq -c | sort -nr > conn_by_ip_ss.txt
cat conn_by_ip_ss.txt
# 프로세스까지 같이 보기
sudo ss -tnp state established '( sport = :3306 )' | head -30
# TIME_WAIT 등 상태별 집계 (연결을 자주 맺고 끊는지 확인 — PHP 특성상 많을 수 있음)
sudo ss -tan '( sport = :3306 )' | awk 'NR>1 {print $1}' | sort | uniq -c
```
> `TIME_WAIT`가 수천 개면 "요청마다 연결/해제"의 비용이 눈에 보이는 수준이라는 뜻입니다. persistent 연결이나 pool 도입을 검토할 근거가 됩니다.
---
### 방법 B-7. 시간대별 추이 샘플링 (권장: 24시간)
한 번의 스냅샷으로는 피크를 놓칩니다. cron으로 1분마다 찍어 CSV로 모읍니다.
```bash
# EC2에 저장: /home/ubuntu/conn_sample.sh
cat > /home/ubuntu/conn_sample.sh <<'EOS'
#!/bin/bash
TS=$(date +%FT%T)
mariadb -N -B -e "
SELECT '$TS', USER, SUBSTRING_INDEX(HOST,':',1), COUNT(*), SUM(COMMAND<>'Sleep')
FROM information_schema.PROCESSLIST GROUP BY USER, 3;" >> /home/ubuntu/conn_sample.tsv
mariadb -N -B -e "
SELECT '$TS','__global__','-',
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='Threads_connected'),
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME='Threads_running');" \
>> /home/ubuntu/conn_sample.tsv
EOS
chmod +x /home/ubuntu/conn_sample.sh
# 1분마다 실행
( crontab -l 2>/dev/null; echo "* * * * * /home/ubuntu/conn_sample.sh" ) | crontab -
```
수집 후 정리:
```bash
# 시간대별 최대 연결 수
awk -F'\t' '$2=="__global__" {split($1,t,"T"); split(t[2],h,":"); \
key=t[1]" "h[1]"시"; if ($4>m[key]) m[key]=$4} END {for (k in m) print k, m[k]}' \
/home/ubuntu/conn_sample.tsv | sort > conn_hourly_peak.txt
cat conn_hourly_peak.txt
# 측정 끝나면 cron 제거
crontab -l | grep -v conn_sample.sh | crontab -
```
---
## C. 테이블 최대 저장 가능 행 수
> "`best_record`에 데이터가 몇 개까지 들어갈 수 있나"는 상한이 **세 겹**입니다. 셋 중 **가장 먼저 걸리는 것**이 실제 한계입니다.
>
> | 구분 | 상한 요인 | 대략적인 값 |
> |---|---|---|
> | C-1 논리적 상한 | PK `INT UNSIGNED AUTO_INCREMENT` 최대값 | **4,294,967,295 (약 42.9억)** |
> | C-2 물리적 상한 | InnoDB 테이블스페이스 / 디스크 여유 공간 | 디스크 여유 ÷ 행당 바이트 |
> | C-3 실용적 상한 | 버퍼 풀·인덱스 크기 대비 성능이 버티는 지점 | 보통 **위 둘보다 훨씬 먼저** 도달 |
### 방법 C-1. 논리적 상한 — AUTO_INCREMENT 소진까지
기록 테이블 4개 모두 PK가 `INT UNSIGNED AUTO_INCREMENT`이므로 **42.9억**이 상한입니다. 중요한 점은 **행을 삭제해도 AUTO_INCREMENT 값은 되돌아가지 않는다**는 것입니다. 즉 "현재 행 수"가 아니라 "지금까지 발급된 ID 값"이 기준입니다.
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p -e "
SELECT TABLE_NAME,
TABLE_ROWS AS approx_rows,
AUTO_INCREMENT AS next_id,
4294967295 - AUTO_INCREMENT AS remaining_ids,
ROUND(AUTO_INCREMENT / 4294967295 * 100, 4) AS used_pct
FROM information_schema.TABLES
WHERE TABLE_SCHEMA='chocomae' AND AUTO_INCREMENT IS NOT NULL
ORDER BY used_pct DESC;" > capacity_auto_increment.txt
cat capacity_auto_increment.txt
```
**남은 기간 계산**
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p chocomae -e "
SELECT DATE_FORMAT(RecordDateTime,'%Y-%m') AS ym, COUNT(*) AS cnt
FROM best_record
WHERE RecordDateTime >= DATE_SUB(CURDATE(), INTERVAL 13 MONTH)
GROUP BY ym ORDER BY ym;" > capacity_monthly_load.txt
cat capacity_monthly_load.txt
```
> 남은 기간 = `remaining_ids` ÷ (월 적재량 × 12)
>
> **2026-09-15 기준 개략 계산:** 월 평균 약 57,968행 → 연 약 69.6만 행. 42.9억 ÷ 69.6만 ≈ **6,000년 이상**.
> 지금 증가 속도가 100배가 되어도 60년이 넘습니다. → **AUTO_INCREMENT는 실질적인 제약이 아닙니다.**
>
> 단, 아래 경우는 소진이 빨라지니 `used_pct`를 연 1회 확인하세요.
> - `INSERT ... ON DUPLICATE KEY UPDATE` / `REPLACE`를 많이 쓰면 실패한 INSERT도 ID를 소비합니다
> - 대량 삭제 후 재적재를 반복하는 경우
>
> **넘어갈 조짐이 보이면:** `ALTER TABLE best_record MODIFY BestRecordID BIGINT UNSIGNED NOT NULL AUTO_INCREMENT;` (테이블 재구성이 필요해 시간과 디스크 여유가 많이 듭니다. 미리 계획하세요)
### 방법 C-2. 물리적 상한 — 디스크 여유 공간
**C-2-1. 현재 테이블 크기와 행당 바이트**
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p -e "
SELECT TABLE_NAME,
TABLE_ROWS AS approx_rows,
ROUND(DATA_LENGTH/1024/1024,1) AS data_mb,
ROUND(INDEX_LENGTH/1024/1024,1) AS index_mb,
ROUND((DATA_LENGTH+INDEX_LENGTH)/1024/1024,1) AS total_mb,
ROUND(DATA_FREE/1024/1024,1) AS free_mb,
ROUND((DATA_LENGTH+INDEX_LENGTH)/NULLIF(TABLE_ROWS,0), 1) AS bytes_per_row
FROM information_schema.TABLES
WHERE TABLE_SCHEMA='chocomae'
ORDER BY (DATA_LENGTH+INDEX_LENGTH) DESC;" > capacity_table_sizes.txt
cat capacity_table_sizes.txt
```
> `TABLE_ROWS`는 InnoDB 추정치입니다. 정확한 값이 필요하면 `SELECT COUNT(*) FROM best_record;`를 쓰되, 운영 시간대에는 부하를 고려하세요.
**C-2-2. 디스크 여유 (EC2 SSH)**
```bash
df -h /var/lib/mysql
sudo du -sh /var/lib/mysql
sudo du -sh /var/lib/mysql/chocomae/*.ibd 2>/dev/null | sort -h | tail -10
```
**C-2-3. 계산**
```
추가 가능 행 수 ≈ (디스크 여유 바이트 × 0.8) ÷ bytes_per_row
남은 기간(년) ≈ 추가 가능 행 수 ÷ (월 적재량 × 12)
```
> 0.8은 안전 계수입니다. 백업·바이너리 로그·임시 파일·`ALTER` 시 임시 테이블 공간을 남겨둬야 합니다. 특히 **인덱스 추가나 `BIGINT` 변경 같은 온라인 DDL은 테이블 크기만큼의 여유 공간을 추가로 요구**합니다.
>
> **2026-09-15 기준 개략 계산:** `best_record`는 약 121만 행에 data 72.5MB + index 72.1MB ≈ 145MB → **행당 약 125바이트**. 42.9억 행을 다 채우면 약 537GB가 필요합니다. 즉 **디스크가 AUTO_INCREMENT보다 훨씬 먼저 한계**입니다. 현재 `df -h` 결과로 실제 여유를 확인하세요.
**C-2-4. InnoDB 자체 상한(참고)**
- 테이블스페이스 최대 크기: 페이지 16KB 기준 **64TB** (`SHOW VARIABLES LIKE 'innodb_page_size'`로 확인)
- `innodb_file_per_table`이 ON이면 테이블마다 `.ibd` 파일 → 파일시스템 최대 파일 크기도 확인 대상 (ext4는 16TB)
- 실무에서 여기에 닿는 일은 없습니다. 디스크 여유(C-2-2)가 먼저 걸립니다.
### 방법 C-3. 실용적 상한 — 성능이 버티는 지점 (가장 중요)
디스크가 남아도 **인덱스가 버퍼 풀을 넘어가는 순간** 조회 성능이 급격히 나빠집니다. "몇 개까지 넣을 수 있나"의 현실적인 답은 대개 여기입니다.
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p -e "
SELECT
ROUND((SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_VARIABLES
WHERE VARIABLE_NAME='innodb_buffer_pool_size')/1024/1024, 0) AS buffer_pool_mb,
ROUND((SELECT SUM(DATA_LENGTH+INDEX_LENGTH) FROM information_schema.TABLES
WHERE TABLE_SCHEMA='chocomae')/1024/1024, 1) AS chocomae_total_mb,
ROUND((SELECT SUM(INDEX_LENGTH) FROM information_schema.TABLES
WHERE TABLE_SCHEMA='chocomae')/1024/1024, 1) AS chocomae_index_mb;" \
> capacity_buffer_pool.txt
cat capacity_buffer_pool.txt
```
**판단 기준**
| 상태 | 의미 | 조치 |
|---|---|---|
| `chocomae_total_mb` < `buffer_pool_mb` | 데이터 전체가 메모리에 상주. 디스크 읽기 거의 없음 | 여유 있음 |
| `chocomae_index_mb` < `buffer_pool_mb` < `chocomae_total_mb` | 인덱스는 메모리, 데이터는 일부만 | 정상 범위. 버퍼 풀 적중률(A-2-5) 감시 |
| `buffer_pool_mb` < `chocomae_index_mb` | 인덱스조차 다 못 올림 | **성능 급락 구간.** 버퍼 풀 증설 / 아카이빙 / 파티셔닝 필요 |
**한계 도달 시점 추정**
```
현재 인덱스 크기 ÷ 현재 행 수 = 행당 인덱스 바이트
(버퍼 풀 크기 × 0.8 - 현재 인덱스 크기) ÷ 행당 인덱스 바이트 = 여유 행 수
여유 행 수 ÷ (월 적재량 × 12) = 남은 기간(년)
```
> 인덱스 크기는 2026-09-15 작업에서 인덱스 5개 + UNIQUE 2개를 추가한 뒤의 값으로 다시 재야 정확합니다. 인덱스가 늘면 행당 바이트도 늘어납니다.
**추가 확인: 실제 메모리 여유 (EC2 SSH)**
```bash
free -h
# 버퍼 풀을 키울 때는 (버퍼 풀 + 연결당 버퍼 × max_connections + OS) < 물리 메모리 여야 합니다
mariadb -e "SHOW VARIABLES WHERE Variable_name IN
('innodb_buffer_pool_size','sort_buffer_size','join_buffer_size','read_buffer_size',
'read_rnd_buffer_size','tmp_table_size','max_heap_table_size','max_connections');"
```
### 방법 C-4. 증가 속도 기반 예측 (한 번에 정리)
아래 쿼리 하나로 세 상한을 같이 뽑을 수 있습니다.
```bash
mariadb -h chocomae.jinaju.com -u <계정> -p chocomae -e "
SELECT
t.TABLE_NAME,
t.TABLE_ROWS AS approx_rows,
t.AUTO_INCREMENT AS next_id,
ROUND((t.DATA_LENGTH+t.INDEX_LENGTH)/1024/1024,1) AS total_mb,
ROUND((t.DATA_LENGTH+t.INDEX_LENGTH)/NULLIF(t.TABLE_ROWS,0),1) AS bytes_per_row,
ROUND((4294967295 - t.AUTO_INCREMENT) / (:monthly_rows * 12), 1) AS years_to_autoinc_limit,
ROUND((4294967295 - t.AUTO_INCREMENT)
* (t.DATA_LENGTH+t.INDEX_LENGTH)/NULLIF(t.TABLE_ROWS,0)
/1024/1024/1024, 1) AS gb_needed_for_autoinc_limit
FROM information_schema.TABLES t
WHERE t.TABLE_SCHEMA='chocomae'
AND t.TABLE_NAME IN ('best_record','app_highest_record',
'typing_exam_record','typing_exam_highest_record');" > capacity_projection.txt
cat capacity_projection.txt
```
> `:monthly_rows` 자리에는 C-1에서 구한 **월 평균 적재량**(2026-09 기준 약 57968)을 숫자로 직접 넣으세요. MariaDB CLI에는 바인드 변수가 없습니다.
### 방법 C-5. 상한에 가까워졌을 때의 대응 (기존 문서 연결)
| 대응 | 효과 | 문서 |
|---|---|---|
| 인덱스·쿼리 최적화 | 같은 데이터로 더 오래 버팀 | [02-option1-index-and-query-rewrite.md](../02-option1-index-and-query-rewrite.md) |
| 일별 요약 테이블 | 랭킹 조회가 원본 테이블을 안 읽음 | [03-option2-daily-summary-tables.md](../03-option2-daily-summary-tables.md) |
| 아카이빙 (오래된 기록 분리) | 활성 테이블 크기 감소 → 버퍼 풀 여유 | [04-option3-archiving.md](../04-option3-archiving.md) |
| 파티셔닝 (날짜 기준) | 조회 시 읽는 파티션만 접근 | [05-option4-partitioning.md](../05-option4-partitioning.md) |
| DB 분리 (RDS 등) | 웹/DB 리소스 경합 해소 | [09-aws-db-separation.md](../09-aws-db-separation.md) |
| 시놀로지 복제 | 읽기 부하(chocoadmin) 분산 | [08-synology-replication-setup.md](../08-synology-replication-setup.md) |
> chocoadmin이 DB를 많이 읽는 것으로 B-3에서 확인된다면, **복제본을 두고 chocoadmin만 복제본을 보게 하는 것**이 가장 효과가 큰 조치입니다.
---
## 90. 결과 기록 양식
측정 후 아래 표를 채워 넣으세요.
### A. MariaDB 부하
| 항목 | 측정값 | 측정 방법 | 판단 |
|---|---|---|---|
| vCPU / 메모리 | | A-1-1 | |
| CPU 사용률 (peak) | | A-1-3 / A-1-5 | |
| 스왑 사용 | | A-1-3 | 0이어야 정상 |
| 디스크 %util / await | | A-1-3 | |
| QPS (평균 / peak) | | A-2-1 | |
| `Threads_running` (peak) | | A-2-4 | vCPU 이하면 정상 |
| 버퍼 풀 적중률 | | A-2-5 | 99% 이상 |
| 디스크 임시 테이블 (60초) | | A-2-3 | |
| 잠금 대기 | | A-2-4 / A-3-3 | |
| 느린 쿼리 (24시간) | | A-4 | 백업 SELECT 제외 |
### B. 서비스별 연결
| 항목 | chocomae (EC2) | chocoadmin (시놀로지) | 측정 방법 |
|---|---|---|---|
| 접속 방식 | `new mysqli` (pool 없음) | Node pool | B-0 |
| 설정상 최대 연결 | Apache MaxRequestWorkers = | connectionLimit × 워커 수 = | B-5 |
| 현재 연결 수 | | | B-2 |
| 최대 연결 수 (24h) | | | B-7 |
| 유휴 연결 수 | | | B-2 |
| 누적 BUSY_TIME | | | B-3 |
| 읽은 행 수 | | | B-3 |
| 전체 `max_connections` | | 여유율: | B-1 |
| 연결 거부 발생 | | | B-1 |
### C. 테이블 용량
| 테이블 | 현재 행 수 | next_id | AUTO_INCREMENT 사용률 | total_mb | 행당 바이트 | 디스크 기준 남은 행 | 남은 기간 |
|---|---|---|---|---|---|---|---|
| best_record | | | | | | | |
| app_highest_record | | | | | | | |
| typing_exam_record | | | | | | | |
| typing_exam_highest_record | | | | | | | |
| 항목 | 값 |
|---|---|
| 월 평균 적재량 | |
| `innodb_buffer_pool_size` | |
| chocomae 전체 크기 (data+index) | |
| 디스크 여유 (`/var/lib/mysql`) | |
| **가장 먼저 걸리는 상한** | (AUTO_INCREMENT / 디스크 / 버퍼 풀) |
| **예상 도달 시점** | |
---
## 91. 주의사항
1. **측정 자체가 부하입니다.** `SELECT COUNT(*)``SHOW ENGINE INNODB STATUS`를 반복 실행하지 마세요. 정확한 행 수가 꼭 필요할 때만 한 번 실행합니다.
2. **`SET GLOBAL`로 바꾼 값은 반드시 복원합니다.** 변경 전 값을 파일로 먼저 저장하세요 (`long_query_time`, `slow_query_log`, `userstat`).
3. **`log_queries_not_using_indexes`는 켜지 않습니다.** 로그가 폭증합니다.
4. **cron 샘플링(B-7)은 측정이 끝나면 지웁니다.** 지우지 않으면 TSV가 무한히 커집니다.
5. **매일 04:00 풀백업**이 돌고 있습니다. 이 시간대 측정값은 백업 부하가 섞이니 따로 표시하세요.
6. **비밀번호를 명령줄에 쓰지 마세요.** 셸 히스토리와 `ps` 출력에 남습니다. `~/.my.cnf`(권한 600)를 쓰세요.
```ini
[client]
host=chocomae.jinaju.com
user=perfcheck
password=<비밀번호>
```
7. **임시 계정·임시 권한은 측정 후 회수합니다.** (`DROP USER`, `REVOKE`)
8. **README에 평문으로 남아 있는 DB 비밀번호**(`root`/`chocomae`/`backup`이 모두 같은 값)는 이 작업과 별개로 교체가 필요합니다.