1장. 트래픽 대응 전략 개요
DXCMS는 공유호스팅부터 멀티서버 클라우드 환경까지 단계적으로 스케일 가능하도록 설계되어 있습니다. 모든 트래픽 대응 기능은 기본으로 활성화되어 있으며, Redis 등 추가 인프라를 도입할수록 처리 능력이 비례해서 향상됩니다.
1.1 트래픽 대응 5대 레이어
요청 수신
↓
① WAF + IP 차단 (Secure.php)
→ 공격 트래픽 즉시 차단 (Redis 30분 IP 차단)
→ 정상 트래픽만 통과
↓
② Rate Limit (Secure::rateLimit)
→ 동일 IP/키의 과다 요청 제한
→ 로그인 무차별 대입, 스팸 등록 방어
↓
③ 세션 최적화 ($_dxNeedSession)
→ 비로그인 GET 요청: 세션 시작 안 함
→ 세션 파일 잠금 제거 → 병렬 처리 가능
↓
④ 캐시 레이어 (DxCache)
→ 게시판 목록: 비로그인 사용자 60초 캐시
→ DB 쿼리 80% 절감
↓
⑤ 출력 버퍼링 + 비동기 DB 기록
→ ob_start() → 응답 먼저 → 통계 나중에
→ fastcgi_finish_request(): PHP-FPM 응답 즉시 완료
↓
응답 완료 (사용자 화면 표시)
→ 방문자 로그 DB INSERT (응답 후 처리)
1.2 트래픽 규모별 권장 설정
| 규모 | 환경 | 핵심 설정 |
| 소규모 | 일 방문자 ~1천명 공유호스팅 | 파일 캐시 활성화, 세션 최적화, 봇 차단 (기본 설정으로 충분) |
| 중규모 | 일 방문자 ~1만명 VPS/클라우드 단일 | Redis 도입 (캐시+Rate Limit+세션), PHP-FPM 최적화, MySQL 쿼리 캐시 |
| 대규모 | 일 방문자 ~10만명 멀티서버 | Redis 클러스터, 로드밸런서, CDN, MySQL 레플리케이션, 읽기/쓰기 분리 |
| 초대규모 | 일 방문자 ~100만+ マルチ리전 | Kubernetes, 오토스케일링, 분산 캐시, 글로벌 CDN, 데이터 샤딩 |
2장. Rate Limiting — 요청 속도 제한
Rate Limit은 같은 IP 또는 같은 키에서 단시간에 너무 많은 요청이 오는 것을 제한합니다. 로그인 무차별 대입 공격, 댓글 스팸, API 남용 등을 방어합니다.
2.1 기본 설정값
// Secure.php 기본 설정
private $rateWindow = 10; // 측정 윈도우: 10초
private $rateLimit = 60; // 10초 내 최대 요청 수
private $ipRateLimit = 200; // IP별 1분 최대 요청 수
// rateLimit($key, $limit, $window, $ipLimit)
// $key: 기능별 식별 키 (문자열)
// $limit: $window 초 내 최대 요청 수
// $window: 측정 시간 윈도우 (초)
// $ipLimit: IP별 추가 제한 (0 = IP 제한 없음)
// 반환: true = 허용, false = 초과 (차단)
2.2 Redis 슬롯 방식 (원자적 카운터)
// Redis 기반 Rate Limit 동작 원리
// 시간을 윈도우 크기($window)로 분할한 슬롯 번호
$slot = (int)floor(time() / $window);
// 예: window=30, time=1746957750
// → slot = 1746957750 / 30 = 58231925
// → 30초마다 새 슬롯 → 카운터 자동 초기화
// Redis 원자적 증가
$sKey = "rl:{$key}:{$slot}"; // 예: "rl:login:58231925"
$count = $redis->incr($sKey); // 원자적 증가 (동시성 안전)
if ($count === 1) {
$redis->expire($sKey, $window + 5); // 슬롯 만료 설정
}
if ($count > $limit) return false; // 초과 → 차단
// IP별 추가 제한 ($ipLimit > 0)
$iKey = "rl:ip:{$key}:" . substr(md5($ip), 0, 8) . ":{$slot}";
$iCnt = $redis->incr($iKey);
if ($iCnt > $ipLimit) return false;
// Redis 오류 시 → 허용 (가용성 우선)
// → Redis가 죽어도 서비스 중단 없음
💡 원자적 연산이 중요한 이유
동시에 100개의 요청이 들어올 때 일반 카운터(read → +1 → write)는 경쟁 조건 발생.
99개가 동시에 read(0) → 모두 1로 증가 → 100개 모두 허용되는 버그.
Redis INCR은 단일 원자적 연산 → 동시 요청도 정확하게 카운팅.
100개 동시 요청 → 1,2,3...100 순서대로 카운팅 → 제한 초과 시 정확히 차단.
2.3 파일 기반 폴백
// Redis 없는 환경의 파일 기반 Rate Limit
// 저장 위치: data/cache/rl_{md5(key+ip)}.tmp
$file = DX_ROOT . "/data/cache/rl_" . md5($key . $ip) . ".tmp";
// 파일 내용: serialize(["count"=>N, "start"=>timestamp])
$data = array("count" => 0, "start" => time());
// 기존 데이터 읽기
if (is_file($file)) {
$saved = unserialize(file_get_contents($file));
if (is_array($saved) && ($now - $saved["start"]) < $window) {
$data = $saved; // 윈도우 내 데이터 재사용
}
// 윈도우 초과 → $data 초기화 (카운터 리셋)
}
$data["count"]++;
file_put_contents($file, serialize($data), LOCK_EX); // 잠금 쓰기
return $data["count"] <= $limit;
// 주의: 트래픽이 많으면 파일 I/O 병목 발생 가능
// 고트래픽 환경에서는 반드시 Redis 사용
2.4 API별 Rate Limit 적용 예시
| 적용 위치 | limit | window | 설명 |
| 로그인 시도 | 5 | 30초 | 30초에 5회. 무차별 대입 공격 차단 |
| 회원가입 | 3 | 60초 | 60초에 3회. 대량 계정 자동 생성 방어 |
| 비밀번호 찾기 | 3 | 300초 | 5분에 3회. SMS/이메일 발송 남용 방지 |
| 댓글 작성 | 10 | 60초 | 60초에 10회 + IP 20회 제한 |
| 게시글 작성 | 5 | 60초 | 60초에 5회. 스팸 게시글 방어 |
| 파일 업로드 | 20 | 600초 | 10분에 20회. 스토리지 남용 방지 |
| 소셜 로그인 시작 | 10 | 60초 | 60초에 10회. OAuth CSRF 방어 |
| API 일반 | 30 | 60초 | 60초에 30회. 기본 API 보호 |
2.5 Rate Limit 적용 방법
// core/api/login.php (로그인 Rate Limit 예시)
$secure = Secure::getInstance();
// 30초에 5회 초과 시 429 반환
if (!$secure->rateLimit("login_attempt", 5, 30)) {
dx_json(array(
"success" => false,
"message" => "로그인 시도가 너무 많습니다. 잠시 후 다시 시도해주세요.",
), 429); // HTTP 429 Too Many Requests
}
// 플러그인에서 Rate Limit 추가
// plugins/my-plugin/api/submit.php
if (!Secure::getInstance()->rateLimit("my_plugin_submit", 10, 60, 30)) {
dx_json(array("success"=>false, "message"=>"너무 많은 요청입니다."), 429);
}
// extend/top/에서 전역 Rate Limit (모든 요청에 적용)
// extend/top/99_global_rate_limit.php
$ip = Secure::getInstance()->clientIp();
if (!Secure::getInstance()->rateLimit("global_" . md5($ip), 200, 60)) {
http_response_code(429);
exit("Too Many Requests");
}
3장. 봇 트래픽 차단
봇 트래픽은 실제 방문자 통계를 왜곡하고 DB와 서버 자원을 낭비합니다. DXCMS는 두 단계의 봇 차단 로직으로 정상 크롤러는 허용하고 악성 봇은 걸러냅니다.
3.1 방문자 트래커 봇 차단 (01_visit_tracker.php)
extend/middle/01_visit_tracker.php에서 모든 요청의 User-Agent를 검사합니다. 봇으로 판별되면 방문자 로그를 아예 기록하지 않습니다.
// 봇 판별 패턴 (visit_tracker.php)
// 아래 패턴 중 하나라도 UA에 포함되면 봇 처리
// 검색엔진 및 크롤러
"googlebot","bingbot","slurp","duckduckbot","baiduspider",
"yandexbot","sogou","exabot","facebot","ia_archiver",
"ahrefsbot","semrushbot","dotbot","rogerbot","mj12bot",
"blexbot","seznambot","linkdexbot","petalbot","bytespider",
// 일반 봇 패턴
"bot","spider","crawl","scraper","checker","monitor",
"fetcher","scanner","analyzer","collector","harvester",
// HTTP 클라이언트 라이브러리
"curl","wget","python-requests","python-urllib","python",
"java","httpclient","go-http-client","okhttp","axios",
"libwww-perl","lwp-trivial","php","ruby","scrapy",
// 모니터링 서비스
"uptimerobot","pingdom","newrelic","datadog","statuspage",
"site24x7","zabbix","nagios","gomez",
// 헤드리스 브라우저 / 자동화 도구
"headlesschrome","phantomjs","selenium","puppeteer","playwright"
// User-Agent 비어있으면 → 무조건 봇으로 처리
if ($_vt_ua === "") { $_vt_isBot = true; }
// 봇이면 → 함수 즉시 return → visit_logs INSERT 없음
if ($_vt_isBot) return;
💡 봇 차단이 DB 성능에 미치는 영향
실제 운영 환경에서 봇 트래픽은 전체 요청의 30~60%를 차지합니다.
봇 차단 없이 visit_logs에 모두 기록하면:
→ 하루 10만 요청 중 5만 건이 봇 → 90일 × 5만 = 450만 봇 로그
→ 테이블 비대화 → 인덱스 효율 저하 → 통계 쿼리 속도 저하
DXCMS는 봇을 visit_logs에 전혀 기록하지 않아 테이블 크기를 최소화합니다.
3.2 WAF 봇 감지 (Secure.php — detectSuspiciousBot)
// Secure.php::detectSuspiciousBot()
// visit_tracker와 별도로 WAF 레벨에서 동작
// 허용 봇 화이트리스트 (차단 없이 통과)
Googlebot, Yeti (네이버), bingbot, DuckDuckBot,
Baiduspider, kakaotalk-scrap, facebookexternalhit,
Twitterbot, LinkedInBot, AhrefsBot, SemrushBot
// 의심 봇 패턴 (차단 아님 — security.log에만 기록)
curl, wget, python, scrapy, headless, selenium, phantomjs
// 차단하지 않는 이유:
// 실제 Googlebot을 잘못 차단하면 검색 노출 손실
// → 로그만 기록하고 관리자가 판단하도록 설계
3.3 추가 봇 차단 방법 (extend/top/ 활용)
// extend/top/02_bot_block.php — 커스텀 봇 차단
if (!defined("DX_CMS")) exit;
$ua = strtolower(isset($_SERVER["HTTP_USER_AGENT"]) ? $_SERVER["HTTP_USER_AGENT"] : "");
// User-Agent 없는 요청 즉시 차단 (봇/스크래퍼 특성)
if (empty($ua)) {
http_response_code(403);
exit("403 Forbidden");
}
// 특정 IP 대역 차단 (예: 알려진 악성 봇 대역)
$ip = Secure::getInstance()->clientIp();
$blockedRanges = array("185.220.", "193.32.", "171.25.");
foreach ($blockedRanges as $range) {
if (strpos($ip, $range) === 0) {
http_response_code(403); exit();
}
}
// Referer 없는 POST 요청 차단 (CSRF 추가 방어)
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$referer = isset($_SERVER["HTTP_REFERER"]) ? $_SERVER["HTTP_REFERER"] : "";
$siteUrl = dx_config("site_url", "");
if ($siteUrl && $referer && strpos($referer, $siteUrl) !== 0) {
http_response_code(403); exit("Cross-Origin Request Denied");
}
}
4장. 출력 버퍼링 및 비동기 처리
사용자에게 응답을 먼저 보내고 DB 기록 등 부가 작업은 그 이후에 처리합니다. 사용자가 느끼는 응답 시간을 최소화하는 핵심 기법입니다.
4.1 ob_start() 출력 버퍼링
// index.php 최상단 — 출력 버퍼 시작
ob_start();
// ob_start() 효과:
// - PHP가 출력하는 모든 내용을 메모리 버퍼에 쌓음
// - 최종적으로 ob_end_flush()가 호출될 때 한 번에 전송
// - 중간에 header() 함수를 어디서든 호출 가능 (HEADER_SENT 방지)
// - 출력 전에 응답 크기 계산 가능 (Content-Length 헤더 설정)
// index.php 마지막 — 출력 버퍼 플러시
if (ob_get_level() > 0) {
ob_end_flush();
}
// IIS/CGI 환경에서도 정상 동작하도록 조건부 flush
4.2 register_shutdown_function — 응답 후 처리
register_shutdown_function은 PHP 스크립트가 종료될 때(응답 완료 후) 실행됩니다. 통계 기록, 비동기 이메일 발송, GC 등 사용자 응답에 영향을 주지 않아야 하는 작업에 적합합니다.
// extend/middle/01_visit_tracker.php
// ① 사용자에게 응답 먼저 완료
register_shutdown_function(function() use ($_vt_data) {
// PHP-FPM 환경: fastcgi_finish_request()가 가장 빠름
// → 응답을 즉시 클라이언트에게 전송하고 프로세스는 계속 실행
if (function_exists("fastcgi_finish_request")) {
fastcgi_finish_request();
} elseif (ob_get_level() > 0) {
ob_end_flush(); // 일반 Apache 환경
}
if (function_exists("flush")) flush();
// ② 응답 완료 후 DB 기록 (사용자 대기 없음)
$db = Database::getInstance();
$pdo = $db->pdo();
// dx_visits UPSERT (1회 쿼리)
$pdo->prepare(
"INSERT INTO dx_visits (visit_date, visit_count, unique_count)",
" VALUES (?, 1, ?)",
" ON DUPLICATE KEY UPDATE",
" visit_count = visit_count + 1,",
" unique_count = unique_count + ?"
)->execute([$date, $uniqueInc, $uniqueInc]);
// dx_visit_logs INSERT
$pdo->prepare(
"INSERT INTO dx_visit_logs (visit_date, ip, page_url, ...) VALUES (?,...)"
)->execute([...]);
// 1/200 확률 GC: 90일 이전 로그 삭제
if (mt_rand(1, 200) === 1) {
$pdo->prepare(
"DELETE FROM dx_visit_logs WHERE visit_date < ?"
)->execute([date("Y-m-d", strtotime("-90 days"))]);
}
});
💡 fastcgi_finish_request()의 성능 효과
PHP-FPM 환경에서만 동작합니다. Apache mod_php는 효과 없음.
동작 방식:
1. fastcgi_finish_request() 호출 시 브라우저로 응답 즉시 전송
2. PHP 프로세스는 계속 실행 (DB 기록, 이메일 발송 등)
3. 사용자는 이미 응답을 받아 페이지를 보는 중
4. PHP가 백그라운드에서 나머지 작업 처리
효과: 방문자 로그 DB INSERT가 응답 시간에 포함되지 않음
예시: 응답 시간 80ms → fastcgi_finish_request 사용 시 50ms (38% 단축)
4.3 순방문자 판단 — DxCache 활용 (DB 조회 없음)
// visit_tracker.php — 순방문자 판단 최적화
// 기존 방식 (DB 조회 필요)
// SELECT COUNT(*) FROM dx_visit_logs WHERE ip=? AND visit_date=?
// → 매 요청마다 DB 쿼리 실행
// DXCMS 방식 (DxCache 활용 — DB 조회 없음)
$ttl = strtotime("tomorrow") - time(); // 자정까지 남은 시간
$cacheKey = "vt_u:" . $date . ":" . substr(md5($ip . $browser), 0, 12);
// 예: "vt_u:2026-05-12:a1b2c3d4e5f6"
if (!DxCache::get($cacheKey, false)) {
// 캐시 없음 → 오늘 첫 방문 → unique
DxCache::set($cacheKey, 1, $ttl); // 자정까지 유지
$isUnique = true;
} else {
// 캐시 있음 → 오늘 재방문 → not unique
$isUnique = false;
}
// ttl = strtotime("tomorrow") - time():
// 자정이 지나면 캐시 자동 만료 → 다음날 다시 unique 판정
// → 일별 순방문자 집계 정확성 유지
5장. DB 쿼리 최적화 — 인덱스 전략
트래픽이 늘어날수록 DB 쿼리 속도가 병목이 됩니다. DXCMS는 실제 쿼리 패턴을 분석하여 복합 인덱스를 설계했습니다.
5.1 dx_posts 복합 인덱스
-- dx_posts 인덱스 구성
-- 게시판 목록 쿼리: WHERE board_id=? AND status=1 ORDER BY id DESC
KEY idx_board_status_id (board_id, status, id)
-- → board_id + status 필터 후 id DESC 정렬을 인덱스만으로 처리
-- → filesort 없음 → 빠른 목록 조회
-- 인기글 정렬: WHERE board_id=? AND status=1 ORDER BY popular_score DESC
KEY idx_popular_score (board_id, status, popular_score)
-- → 인기 정렬도 인덱스 사용 → 테이블 전체 스캔 없음
-- FULLTEXT 전문 검색: MATCH(title, content) AGAINST(?)
FULLTEXT KEY ft_title_content (title, content)
-- MySQL 5.6+ InnoDB FULLTEXT 지원
-- LIKE "%검색어%" 대비 성능 수십 배 향상
-- 작성자별 게시글 조회
KEY idx_member_id (member_id)
-- 날짜 범위 조회 (최신글 위젯, 인기글 7일 필터)
KEY idx_created (created_at)
5.2 dx_visit_logs 복합 인덱스
-- dx_visit_logs 인덱스 구성 (v8.1.0)
-- 일별 순방문자 집계: WHERE visit_date=? AND is_bot=0
KEY idx_date_bot (visit_date, is_bot)
KEY idx_bot_date (is_bot, visit_date)
-- 현재 접속자 수: WHERE created_at >= ? AND is_bot=0
KEY idx_created_bot (created_at, is_bot)
-- IP별 중복 방문 확인
KEY idx_ip_date (ip, visit_date)
-- 브라우저별 통계
KEY idx_browser (browser)
-- 유입 도메인 통계
KEY idx_referer_domain (referer_domain(100))
-- 최적화된 통계 쿼리 (main.php 위젯)
-- 기존: COUNT(*) FROM visit_logs (풀스캔)
-- 개선: SUM(unique_count) FROM dx_visits (집계 테이블)
SELECT SUM(unique_count) FROM dx_visits; -- 행 수 적음 → 빠름
5.3 인기글 점수 계산 (popular_score)
// 인기글 점수 = (조회×1 + 좋아요×5 + 댓글×3) × 시간감쇠
// 조회수가 증가할 때마다 점수 갱신
$daysPassed = max(0, (time() - strtotime($post["created_at"])) / 86400);
$decay = max(0.1, 1.0 - floor($daysPassed / 7) * 0.1);
// 시간감쇠 계산:
// 0~7일: decay = 1.0 (감쇠 없음)
// 7~14일: decay = 0.9 (10% 감쇠)
// 14~21일:decay = 0.8 (20% 감쇠)
// ...
// 63일+: decay = 0.1 (90% 감쇠, 최소값)
$raw = (int)$post["view_count"] * 1
+ (int)$post["like_count"] * 5
+ (int)$post["comment_count"] * 3;
$newScore = (int)round($raw * $decay);
UPDATE dx_posts SET popular_score=? WHERE id=?
// 장점: popular_score가 DB에 저장됨
// → ORDER BY popular_score DESC 로 인덱스 활용 가능
// → 런타임에 복잡한 계산 없이 단순 정렬
5.4 비정규화 캐시 컬럼
like_count, comment_count는 실시간으로 집계 쿼리를 실행하면 부하가 크므로, 변경 시마다 dx_posts 테이블에 직접 업데이트합니다.
// dx_posts 비정규화 컬럼 (집계 캐시)
like_count INT DEFAULT 0 -- 좋아요 수 캐시
comment_count INT DEFAULT 0 -- 댓글 수 캐시
view_count INT DEFAULT 0 -- 조회수
// 좋아요 추가 시 즉시 업데이트
INSERT INTO dx_likes (target_type, target_id, member_id) VALUES (...)
UPDATE dx_posts SET like_count = like_count + 1 WHERE id=?
// 댓글 작성 시 즉시 업데이트
INSERT INTO dx_comments (...)
UPDATE dx_posts SET comment_count = comment_count + 1 WHERE id=?
// 목록 쿼리에서 서브쿼리 없이 바로 읽기
// 기존 방식 (느림):
// SELECT *, (SELECT COUNT(*) FROM dx_comments WHERE post_id=p.id) AS cmt_cnt
// DXCMS 방식 (빠름):
// SELECT *, p.comment_count
5.5 방문자 통계 — visits 집계 테이블
-- dx_visits 일별 집계 테이블
-- visit_logs 풀스캔 대신 집계 테이블 사용 → 극적 성능 향상
-- 매 요청마다 UPSERT (1회 쿼리)
INSERT INTO dx_visits (visit_date, visit_count, unique_count)
VALUES (?, 1, ?)
ON DUPLICATE KEY UPDATE
visit_count = visit_count + 1,
unique_count = unique_count + ?
-- 전체 방문자 합계 (빠름)
SELECT SUM(unique_count) FROM dx_visits;
-- 행 수: 날짜 수 = 수년치 해도 1825행 이하
-- vs 풀스캔: COUNT(DISTINCT ip) FROM visit_logs
-- 수백만 행 → 수십 초 소요
-- 오늘 방문자 (인덱스 사용)
SELECT unique_count FROM dx_visits WHERE visit_date = ?
-- 단 1행 조회 → 즉시 반환
6장. PHP-FPM 최적화
PHP-FPM은 PHP 요청을 처리하는 프로세스 관리자입니다. 트래픽 증가에 따라 워커 프로세스 수와 메모리를 최적화하면 응답 속도와 동시 처리 수가 크게 향상됩니다.
6.1 권장 PHP-FPM 설정
; /etc/php/8.x/fpm/pool.d/www.conf
; 프로세스 관리 방식: dynamic (트래픽에 따라 자동 조정)
pm = dynamic
; 최대 자식 프로세스 수 (= 동시 처리 가능 요청 수)
; 공식: (서버 메모리 - OS용 메모리) / PHP 프로세스 메모리
; 예: 4GB RAM, PHP 50MB/프로세스 → (4096-512) / 50 = 71
pm.max_children = 50
; 항상 유지할 최소 대기 프로세스 수 (트래픽 급증 시 즉시 대응)
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
; 프로세스당 최대 요청 처리 수 (메모리 누수 방지)
pm.max_requests = 500
; 요청 처리 제한 시간
request_terminate_timeout = 60s
; 느린 요청 로깅 (10초 이상 처리되는 요청 추적)
slowlog = /var/log/php-fpm-slow.log
request_slowlog_timeout = 10s
6.2 PHP.ini 성능 설정
; /etc/php/8.x/fpm/php.ini
; OPcache: PHP 코드 컴파일 결과 캐싱 (핵심 성능 최적화)
opcache.enable = 1
opcache.memory_consumption = 128 ; MB
opcache.interned_strings_buffer = 16 ; MB
opcache.max_accelerated_files = 10000 ; 캐싱할 최대 파일 수
opcache.revalidate_freq = 60 ; 파일 변경 체크 주기 (초)
opcache.fast_shutdown = 1 ; 빠른 종료
; 메모리 제한 (대용량 파일 업로드, 복잡한 페이지 처리)
memory_limit = 256M
; 업로드 제한
upload_max_filesize = 50M
post_max_size = 55M
; 실행 시간 제한
max_execution_time = 60
max_input_time = 60
; 세션 파일 잠금 최소화
session.lazy_write = 1 ; 세션 데이터가 변경된 경우만 파일 쓰기
💡 OPcache 효과
OPcache 없이: 매 요청마다 PHP 파일 읽기 → 파싱 → 컴파일 → 실행
OPcache 있을 때: 첫 요청만 컴파일 → 이후 메모리에서 바이트코드 직접 실행
효과: PHP 처리 속도 3~5배 향상
DXCMS 기준: 50ms → 15ms (코드 처리 시간 70% 단축)
주의: 코드 수정 후 OPcache를 비워야 변경사항 반영
opcache_reset() 또는 PHP-FPM 재시작
7장. MySQL 최적화
7.1 권장 MySQL 설정 (my.cnf)
# /etc/mysql/mysql.conf.d/mysqld.cnf
# InnoDB 버퍼 풀: 서버 RAM의 70~80% 권장
# (자주 사용되는 데이터와 인덱스를 메모리에 캐싱)
innodb_buffer_pool_size = 2G # 4GB RAM 서버 기준
# 버퍼 풀 인스턴스 수 (innodb_buffer_pool_size / 1GB 권장)
innodb_buffer_pool_instances = 2
# 로그 파일 크기 (큰 트랜잭션 성능 향상)
innodb_log_file_size = 256M
# 쓰기 성능: O_DIRECT로 OS 캐시 이중화 방지
innodb_flush_method = O_DIRECT
# 동시 I/O 스레드 수 (SSD: CPU 코어 수)
innodb_read_io_threads = 4
innodb_write_io_threads = 4
# 최대 연결 수 (PHP-FPM pm.max_children과 맞춤)
max_connections = 200
# 느린 쿼리 로깅 (1초 이상 쿼리 추적)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
# 쿼리 캐시 (MySQL 5.7 이하, 8.0에서 제거됨)
# query_cache_size = 64M
# query_cache_type = 1
7.2 EXPLAIN으로 쿼리 분석
-- 게시판 목록 쿼리 분석 예시
EXPLAIN
SELECT p.*, m.name
FROM dx_posts p
LEFT JOIN dx_members m ON m.id = p.member_id
WHERE p.board_id = 1 AND p.status = 1 AND p.is_notice = 0
ORDER BY p.id DESC
LIMIT 20;
-- EXPLAIN 결과 해석
-- type:
-- ref → 인덱스 사용 (좋음)
-- range → 인덱스 범위 스캔 (양호)
-- ALL → 풀스캔 (나쁨, 인덱스 추가 필요)
-- key: 사용된 인덱스명 (NULL이면 인덱스 미사용)
-- rows: 예상 스캔 행 수 (적을수록 좋음)
-- Extra:
-- Using index → 커버링 인덱스 (가장 빠름)
-- Using where → WHERE 조건 있음
-- Using filesort → 정렬에 파일 사용 (인덱스 추가 검토)
-- Using temporary → 임시 테이블 사용 (개선 필요)
7.3 읽기/쓰기 분리 (레플리케이션)
트래픽이 많아지면 MySQL 레플리케이션으로 읽기(SELECT)와 쓰기(INSERT/UPDATE)를 다른 서버로 분리할 수 있습니다.
// data/config.php — 읽기/쓰기 분리 설정 (확장 구성)
// 마스터 (쓰기용)
$db->connect("master-db.host", "dxcms", "user", "pass", "utf8mb4", "dx_");
// 슬레이브 (읽기용) — 별도 설정 필요
// DXCMS 기본은 단일 DB 연결이므로
// 읽기/쓰기 분리는 커스텀 미들웨어 또는 ProxySQL 사용 권장
// ProxySQL 설정 예시 (MySQL 앞단에서 쿼리 라우팅)
// SELECT → 슬레이브로 자동 라우팅
// INSERT/UPDATE/DELETE → 마스터로 라우팅
// → PHP 코드 변경 없이 DB 레이어에서 분리
8장. 세션 병목 최적화
PHP 파일 세션의 가장 큰 문제는 동일 세션 ID의 요청이 직렬화된다는 것입니다. 한 탭이 처리되는 동안 다른 탭은 대기해야 합니다. DXCMS는 이 문제를 두 가지 방법으로 해결합니다.
8.1 조건부 세션 시작 ($_dxNeedSession)
// index.php — 세션 필요 여부 판단
// 세션 스킵 조건 (모두 충족 시 세션 없이 처리)
$_dxNeedSession = true;
if (
$_SERVER["REQUEST_METHOD"] === "GET" && // GET 요청
empty($_COOKIE[session_name()]) && // 세션 쿠키 없음
!isset($_SERVER["HTTP_X_REQUESTED_WITH"]) // AJAX 아님
) {
// 세션 필수 경로 확인
$uri = $_SERVER["REQUEST_URI"];
$needSession = strpos($uri, "/admin") !== false
|| strpos($uri, "/auth") !== false
|| strpos($uri, "/view/") !== false
|| strpos($uri, "/api/") !== false
|| strpos($uri, "/write") !== false
|| strpos($uri, "/edit") !== false
|| strpos($uri, "/reply") !== false;
if (!$needSession) {
$_dxNeedSession = false; // 세션 시작 안 함 → 파일락 없음
}
}
// 효과:
// 비로그인 게시판 목록 접근 → 세션 없음 → 병렬 처리 가능
// 100명 동시 접속도 서로 대기 없이 처리
8.2 Redis 세션 (멀티서버 공유)
// data/config.php — Redis 세션 설정
define('REDIS_SESSION_URL', 'tcp://127.0.0.1:6379?auth=비밀번호');
// Redis 세션의 장점
// 1. 멀티서버: 모든 서버가 동일 세션 공유
// 서버 A에서 로그인 → 서버 B에서 요청 → 세션 유지
// 2. 파일 잠금 없음
// Redis는 원자적 연산으로 동시 읽기/쓰기 지원
// → 동일 세션의 여러 탭도 병렬 처리 가능
// 3. 자동 TTL
// Redis TTL로 세션 자동 만료 → 파일 GC 불필요
// 4. 속도
// 파일 I/O 없음 → 세션 읽기/쓰기 10배 빠름
// session.lazy_write = 1 (php.ini)
// 세션 데이터 변경 없으면 Redis 쓰기 안 함 → 성능 향상
9장. 멀티서버 및 로드밸런서 구성
9.1 로드밸런서 구성 요소
// 멀티서버 아키텍처 (고트래픽 구성)
사용자 브라우저
↓
Cloudflare / AWS CloudFront (CDN)
↓ (정적 파일: CSS, JS, 이미지는 CDN에서 반환)
Load Balancer (Nginx / AWS ALB)
↓ (라운드로빈 또는 IP 해시)
┌──────────────────────────────────┐
│ WAS 1: Apache+PHP-FPM │
│ WAS 2: Apache+PHP-FPM │
│ WAS 3: Apache+PHP-FPM │
└──────────────────────────────────┘
↓ (모든 서버가 공유)
┌─────────────┬─────────────────────┐
│ MySQL Master │ Redis Cluster │
│ MySQL Slave │ (세션+캐시+Rate Limit) │
└─────────────┴─────────────────────┘
↓
NFS / S3 / 분산 스토리지
(data/uploads/ 공유 스토리지)
9.2 멀티서버 필수 설정
| 항목 | 설정 방법 |
| 세션 공유 | REDIS_SESSION_URL → Redis 세션 저장소 사용. 모든 서버가 동일 Redis 가리킴 |
| 캐시 공유 | REDIS_SESSION_URL 설정 시 DxCache도 Redis 사용 → 캐시 자동 공유 |
| 업로드 파일 공유 | data/uploads/ → NFS, AWS S3, GCS 마운트. 모든 서버에서 접근 가능해야 함 |
| Rate Limit 공유 | Redis 사용 시 자동으로 전체 서버 합산 카운팅. 서버별 독립 동작 불가 |
| IP 차단 공유 | Redis "dx:ban:{IP}" 키는 모든 서버에서 공유. 한 서버 차단 → 전체 차단 |
| 코드 동기화 | rsync, Ansible, Docker 이미지. 모든 서버 코드 동일 버전 유지 필수 |
9.3 Nginx 로드밸런서 설정
# /etc/nginx/nginx.conf
# 업스트림 서버 정의
upstream dxcms_backend {
# 라운드로빈 (기본)
server 10.0.0.1:80 weight=3; # 고성능 서버 가중치 3
server 10.0.0.2:80 weight=2;
server 10.0.0.3:80 weight=1;
# 또는 IP 해시 (같은 IP는 항상 같은 서버로)
# ip_hash;
# 헬스 체크
# server 10.0.0.4:80 backup; # 장애 시 백업 서버
}
server {
listen 80;
server_name dxcms.example.com;
# 정적 파일: 로드밸런서에서 직접 서빙 (WAS 부하 없음)
location ~* ^/(assets|themes)/ {
root /var/www/dxcms;
expires 1y;
add_header Cache-Control "public, immutable";
}
# 동적 요청: 백엔드로 전달
location / {
proxy_pass http://dxcms_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
}
}
9.4 Cloudflare 연동
// Cloudflare CDN 사용 시 실제 클라이언트 IP 처리
// Secure::clientIp()는 이미 CF-Connecting-IP 헤더를 우선 처리
// Cloudflare IP 대역 (신뢰 프록시 범위 — Secure.php 내장)
173.245.48.0/20
103.21.244.0/22
103.22.200.0/22
103.31.4.0/22
141.101.64.0/18
108.162.192.0/18
190.93.240.0/20
// ... 14개 대역
// Cloudflare에서 보내는 헤더
// CF-Connecting-IP: 실제 클라이언트 IP
// X-Forwarded-For: 프록시 체인 전체
// CF-Ray: Cloudflare 요청 ID (디버깅용)
// Cloudflare 레이어에서 차단 가능한 공격
// DDoS: 자동 흡수 (Cloudflare Magic Transit)
// 봇: Cloudflare Bot Management
// Rate Limit: Cloudflare Rules (CMS Rate Limit 보완)
// WAF: Cloudflare WAF (CMS WAF 보완)
10장. 트래픽 급증 대응 체크리스트
10.1 급증 전 사전 준비
- [ ] Redis 설치 및 REDIS_SESSION_URL 설정 → 세션 + 캐시 + Rate Limit Redis 전환
- [ ] PHP OPcache 활성화 확인 → phpinfo()에서 "Zend OPcache" 활성화 여부
- [ ] MySQL innodb_buffer_pool_size → RAM의 70%로 설정
- [ ] PHP-FPM pm.max_children → 서버 RAM에 맞게 설정
- [ ] 게시판 캐시 TTL 확인 → 비로그인 목록 60초 캐시 정상 동작 여부
- [ ] 봇 차단 로직 확인 → visit_tracker.php 정상 동작 여부
- [ ] DB 인덱스 점검 → SHOW INDEX FROM dx_posts; idx_board_status_id 확인
- [ ] 모니터링 설정 → CPU, RAM, DB 커넥션, Redis 메모리 실시간 모니터링
10.2 급증 중 즉각 대응
// 1. DxCache 전체 초기화 → 새 캐시로 새 출발
DxCache::flush();
// 2. 특정 게시판만 캐시 초기화
DxCache::deletePrefix("board_list_free_");
// 3. extend/top/ 에 유지보수 모드 임시 활성화
// extend/top/00_maintenance.php
$allowedIPs = array("1.2.3.4"); // 관리자 IP
$ip = Secure::getInstance()->clientIp();
if (!in_array($ip, $allowedIPs)) {
http_response_code(503);
exit("서비스 점검 중입니다. 잠시 후 다시 시도해주세요.");
}
// 4. Redis IP 임시 차단 (공격 IP)
$redis = Secure::getRedis();
if ($redis) {
$redis->setex("dx:ban:공격자IP", 86400, "MANUAL"); // 24시간
}
// 5. MySQL 슬로우 쿼리 확인
SHOW FULL PROCESSLIST; -- 현재 실행 중인 쿼리
SHOW STATUS LIKE "Threads_%"; -- 연결 스레드 현황
SHOW STATUS LIKE "Innodb_row_lock%"; -- 락 현황
10.3 주요 지표 모니터링
| 지표 | 확인 명령 | 정상 기준 |
| PHP-FPM 활성 워커 | php-fpm status (pm.status_path) | max_children 80% 미만 |
| MySQL 연결 수 | SHOW STATUS LIKE "Threads_connected" | max_connections 70% 미만 |
| MySQL 슬로우 쿼리 | tail -f /var/log/mysql/slow.log | 1초 이상 쿼리 없음 |
| Redis 메모리 | redis-cli info memory | maxmemory 80% 미만 |
| 캐시 히트율 | redis-cli info stats (keyspace_hits/misses) | 80% 이상 |
| 서버 CPU | top / htop | 50% 미만 (버스트 기준) |
| PHP OPcache 히트율 | opcache_get_status()["opcache_statistics"] | 95% 이상 |
| DX_START 처리 시간 | extend/bottom/ 로깅 | 200ms 미만 |
10.4 extend를 이용한 성능 모니터링
// extend/bottom/99_perf_monitor.php.disabled
// 활성화: .disabled 제거
if (!defined("DX_CMS")) exit;
if (!dx_is_admin()) return; // 관리자만
$elapsed = round((microtime(true) - DX_START) * 1000, 2);
$memPeak = round(memory_get_peak_usage(true) / 1024 / 1024, 2);
$driver = DxCache::getDriver();
$info = array(
"time" => $elapsed . "ms",
"mem" => $memPeak . "MB",
"cache" => $driver,
);
if ($driver === "redis") {
$redis = Secure::getRedis();
if ($redis) {
$mem = $redis->info("memory");
$info["redis_mem"] = $mem["used_memory_human"];
$stats = $redis->info("stats");
$h = (int)$stats["keyspace_hits"];
$m = (int)$stats["keyspace_misses"];
if ($h + $m > 0) {
$info["hit_rate"] = round($h / ($h + $m) * 100, 1) . "%";
}
}
}
echo "<!-- PERF: " . json_encode($info) . " -->";