티스토리 뷰
temp 폴더 20GB가
노드 하나를 멈췄습니다.
첨부 최대 크기는 100MB로 걸려 있었습니다. 그런데 temp에는
upload_*.tmp와 *.part가 20GB 쌓여 있었고,
마운트가 100%가 되자 1번 노드는 로그인도 첨부 업로드도 응답하지 않았습니다.
제한이 있는데 왜 못 막았는지, 무엇을 고쳐야 재발하지 않는지 정리했습니다.
죽은 건 노드 하나, 체감은 전사 장애
2노드 active-active 구성이면 한 노드가 빠져도 서비스는 남은 노드로 계속돼야 합니다. 그런데 실제로는 사용자 절반이 아무것도 못 했습니다. 증상은 세 가지로 나타났습니다.
로그인이 안 된다
세션 정보를 쓰려는 순간 디스크 write가 실패합니다. 1번 노드로 배정된 사용자는 화면이 그냥 멈춥니다.
첨부 업로드 무응답
첨부는 반드시 temp를 경유합니다. temp가 꽉 차면 업로드는 100% 실패하고, 에러가 아니라 무응답으로 보입니다.
프로세스는 살아있다
JVM은 죽지 않았고 8090 포트도 열려 있습니다. 그래서 TCP 헬스체크는 계속 정상으로 판정합니다 — 이게 핵심입니다.
왜 한 노드 장애가 전체로 번졌나
| 번짐 경로 | 무슨 일이 벌어지는가 |
|---|---|
| TCP 헬스체크 | 디스크가 꽉 찬 Confluence는 프로세스도 포트도 살아 있습니다. L4 체크만 하는 LB는 좀비 노드에 계속 트래픽을 보냅니다. |
| Sticky session | Confluence DC는 세션 어피니티가 필수입니다. 1번 노드에 붙어 있던 사용자는 노드가 퇴출되지 않는 한 계속 그쪽으로 갑니다. |
| 스레드 고갈 | 로그·temp write가 블록되면 Tomcat 스레드가 점유된 채 쌓입니다. thread pool이 마르면 모든 요청이 hang합니다. |
| Hazelcast 판정 지연 | 노드가 죽은 게 아니라 느려진 상태(long GC pause와 구분되지 않음)라 클러스터에서 늦게 퇴출됩니다. |
| 2노드의 한계 | 한 노드를 빼면 남는 건 50% 용량입니다. 남은 노드가 2배 부하를 못 버티면 연쇄 장애로 갑니다. |
temp에 남은 건 중단된 업로드의 잔해였습니다
파일명이 답을 말해줍니다. upload_*.tmp는 멀티파트 업로드의
스테이징 파일이고, *.part는 전송이 완결되지 않은 부분 파일입니다.
정상 종료된 업로드라면 둘 다 남지 않습니다.
첨부 업로드가 지나가는 길
│
├─▶ 크기·확장자 검증
├─▶ shared home/attachments 로 이동
└─▶ temp 파일 삭제
↑ 요청이 정상 종료돼야 실행된다
커넥션이 끊기면 마지막 단계가 실행되지 않는다 → 고아 파일
고아 파일은 사실상 재시작 때까지 회수되지 않는다
즉 재시도 1회 = temp에 파일 크기만큼 영구 적립입니다. 500MB 파일을 40번 재시도하면 그것만으로 20GB가 됩니다.
먼저 확인할 것 — 진단 4단계
어느 마운트가 찼고, 무엇이 먹었는지
df는 100%인데 du는 작게 나오면
삭제됐지만 프로세스가 붙잡고 있는 파일입니다.
du -sh /var/atlassian/application-data/confluence/temp/* | sort -rh | head -20
# df 와 du 가 안 맞을 때 — 열린 채 삭제된 파일
lsof +L1 | grep -i confluence
mtime이 전부 같다면 두 가지 해석이 가능하다
이 구분을 건너뛰면 엉뚱한 대책으로 갑니다. 한 번의 버스트일 수도 있고, 디스크가 꽉 찬 순간 모든 in-flight 쓰기가 동시에 멈춘 흔적일 수도 있습니다.
stat --printf='%s\t%y\t%z\t%w\t%n\n' /var/conf-temp/upload_*.tmp | sort -n | head -40
# birth 도 같은 시각에 몰려 있다 → 한 번의 버스트
# birth 는 흩어져 있고 mtime 만 같다 → 디스크 full 의 증상
파일 개수를 세면 시나리오가 좁혀진다
20GB를 채우려면 최대 크기(100MB)로 꽉 채워도 최소 205건, 평균 20MB라면 약 1,000건이 필요합니다.
ls -1 /var/conf-temp/*.part | wc -l
# 크기 분포 — 특정 값에 몰려 있으면 잘린 것, 제각각이면 실제 업로드
find /var/conf-temp -maxdepth 1 -type f \
\( -name 'upload_*.tmp' -o -name '*.part' \) \
-printf '%s\n' | sort -n | uniq -c | tail -20
| 파일 개수 | 유력 원인 |
|---|---|
| 200~300개 각각 100MB 근처 | 100MB를 넘는 파일을 반복 업로드 → 매번 거부 → 재시도 (다음 장의 함정) |
| 1,000개 이상 크기 제각각 | WebDAV·Office 연동 동기화 루프, 마이그레이션 앱, REST API 봇의 재시도 루프 |
| 수십 개 | temp 외에 다른 것도 같이 찬 것 — heap dump나 일일 XML 백업을 함께 확인 |
access log에 사용자명까지 남아 있다
Confluence의 Tomcat access log는 X-AUSERNAME을 기록합니다.
누가, 어떤 경로로, 몇 번 시도했는지 그대로 나옵니다.
소요시간이 특정 값에 딱 붙어 있으면 그 값이 프록시 타임아웃입니다.
grep -E '(doattachfile|drag-and-drop/upload|child/attachment)' "$LOG" \
| awk '{print $2, $(NF-2), $(NF-1)}' | sort | uniq -c | sort -rn | head -20
# ↑사용자 ↑status ↑bytes
# 결정적 증거 — 업로드가 완료 전에 끊겼는가
CLOG=/var/atlassian/application-data/confluence/logs/atlassian-confluence.log
grep -cE 'ClientAbortException|SocketTimeoutException|Broken pipe' "$CLOG"
ClientAbortException이 대량이면 확정입니다.
업로드가 서버 처리 완료 전에 클라이언트/프록시 쪽에서 끊겼다는 뜻이고,
그게 곧 회수되지 않은 스테이징 파일의 개수입니다.첨부 100MB 제한은 temp 소비를 막지 못합니다
이 장애에서 가장 반직관적인 부분입니다. "첨부 최대 크기를 걸어놨는데 왜 디스크가 차죠?"의 답은 검증 시점에 있습니다. 크기 검증은 파일을 temp에 다 받은 뒤에 일어납니다.
│
▼
2. temp 에 파일 전체를 기록한다
│ └─ 여기서 이미 디스크를 다 먹는다
▼
3. "첨부 최대 크기 초과" 로 거부한다
│ └─ 크기 검증은 이 시점에 처음 일어난다
▼
4. 요청이 비정상 종료되면 temp 파일은 회수되지 않는다
│
▼
5. 사용자가 재시도한다 ──▶ 1. 로 되돌아간다
재시도 1회 = temp 에 파일 크기만큼 영구 적립
그래서 제한은 앱이 아니라 앞단에 걸어야 합니다
nginx는 Content-Length가
client_max_body_size를 넘으면 body를 읽지 않고 즉시 413을 반환합니다.
Confluence의 temp는 손도 대지 않습니다.
| 제한을 두는 위치 | 초과 파일 업로드 시 temp 소비 | 사용자 피드백 |
|---|---|---|
| Confluence 앱 설정만 | 파일 크기만큼 소비 — 다 받은 뒤 거부 | 느림 (전송 완료 후) |
| 프록시 + 앱 | 0 — body 를 받기 전에 413 | 즉시 |
Content-Length가 없는 chunked 전송은 이 검사를 우회합니다.
일부 WebDAV 클라이언트나 curl -T가 그렇습니다.
그래서 뒤에 나오는 reaper가 여전히 필요합니다.우선순위는 앞단에서 끊기 → 회수 → 격리
순서가 중요합니다. 1번은 원인을 제거하고, 2번은 새는 것을 주워 담고, 3번은 그래도 터졌을 때 피해 범위를 업로드 기능 하나로 묶습니다.
프록시에서 초과 요청을 차단한다
값은 Confluence 첨부 최대 크기 + 5MB 정도로 맞춥니다. 너무 크게 잡으면 초과 파일이 Confluence까지 도달해 temp를 먹으니, 넉넉하게 잡는 것이 오히려 위험합니다.
client_body_timeout 300s;
proxy_request_buffering off; # ★ nginx 임시파일 이중 스테이징 방지
proxy_read_timeout 300s;
proxy_send_timeout 300s;
send_timeout 300s;
HAProxy는 body 크기 제한이 없으니 헤더 기반 룰로 처리합니다.
http-request deny deny_status 413 if { req.hdr_val(content-length) -m int gt 110100480 }
timeout client 300s
timeout server 300s
timeout tunnel 3600s # WebSocket(Synchrony) 용
proxy_request_buffering on(기본값)이면 nginx도 body를 임시파일로 받습니다.
nginx가 Confluence와 같은 호스트에 있으면 /var/lib/nginx/body가
또 하나의 디스크 소진 벡터입니다.고아 스테이징 파일을 자동 회수한다
핵심 논리는 하나입니다 — 업로드 중인 파일은 mtime이 계속 갱신됩니다.
따라서 "mtime이 2시간 이상 멈춤 + 프로세스가 열고 있지 않음"이면 확정 고아입니다.
lsof 확인 없이 지우면 진행 중인 업로드를 깨뜨립니다.
set -u
TEMP_DIRS=(/var/conf-temp /var/atlassian/application-data/confluence/temp)
STALE_MIN=120 # 2시간 이상 mtime 이 멈춘 것만
FREED=0
for d in "${TEMP_DIRS[@]}"; do
[ -d "$d" ] || continue
while IFS= read -r -d '' f; do
# JVM 이 아직 열고 있는 파일은 절대 건드리지 않는다
if ! lsof -- "$f" >/dev/null 2>&1; then
sz=$(stat -c %s "$f" 2>/dev/null || echo 0)
rm -f -- "$f" && FREED=$((FREED + sz))
fi
done < <(find "$d" -maxdepth 1 -type f \
\( -name 'upload_*.tmp' -o -name '*.part' \) \
-mmin +$STALE_MIN -print0)
done
[ "$FREED" -gt 0 ] && logger -t conf-temp-reaper "reclaimed $((FREED/1024/1024)) MB"
*/15 * * * * /usr/local/bin/conf-temp-reaper.sh
# /etc/systemd/system/confluence.service.d/override.conf
# temp 는 기동 시 재생성되므로 시작 전 초기화가 안전하다
[Service]
ExecStartPre=/bin/bash -c 'find /var/conf-temp -mindepth 1 -delete || true'
temp와 heap dump를 별도 볼륨으로 격리한다
이번 장애의 본질은 "temp가 local home과 같은 볼륨이라, temp가 차면 Confluence 전체가 죽는다"입니다. 20GB가 차더라도 업로드만 실패하고 로그인·조회는 정상이어야 합니다.
heap dump는 더 중요합니다. OOM 한 번에 힙 크기만큼(수십 GB) 쓰기 때문에, local home과 같은 볼륨에 두면 같은 장애가 반드시 재발합니다.
CATALINA_OPTS="${CATALINA_OPTS} -XX:+HeapDumpOnOutOfMemoryError"
CATALINA_OPTS="${CATALINA_OPTS} -XX:HeapDumpPath=/var/dumps"
export CATALINA_OPTS
| 마운트 | 담는 것 | 사이징 기준 |
|---|---|---|
/var/conf-temp | 업로드 스테이징, export 산출물 | 첨부최대 × 동시업로드 × 3 |
/var/dumps | heap dump (*.hprof) | 힙 크기 × 1.5 |
| local home | Lucene 인덱스, 설정, 앱 | 로컬 디스크 — NFS 금지 |
| logs | catalina.out 등 | logrotate 필수 |
일일 XML 백업을 끈다
기본으로 켜져 있고, Atlassian이 프로덕션에서 명시적으로 비권장하는 기능인데 매일 temp와 backups를 GB 단위로 채웁니다. 직접 원인이 아니어도 같은 볼륨을 함께 좁히고 있던 공범입니다.
⚙ 관리 → 백업 관리 → "매일 백업 수행" 해제 후 DB dump로 대체합니다.
rsync -a --delete /nfs/confluence-shared/attachments/ /backup/attachments/
find /backup -name 'conf_*.dump' -mtime +14 -delete
노드 하나가 죽어도 서비스는 살아 있어야 합니다
디스크 대책을 다 세워도 좀비 노드를 걸러내지 못하면 다음 장애도 똑같이 전사 장애가 됩니다. Confluence DC 구조를 한 장으로 보고, 헬스체크를 고칩니다.
│
┌────────▼─────────┐
│ Load Balancer │
└───┬──────────┬───┘
┌─────┘ └────┐
┌────▼─────┐ ┌────▼─────┐
│ Node 1 │ │ Node 2 │
│ JVM │<── HZ ──>│ JVM │
│local home│ │local home│
└────┬─────┘ └────┬─────┘
└─────┐ ┌────┘
┌───▼──────────▼───┐
│ Shared Home (NFS)│
├──────────────────┤
│ Shared Database │
└──────────────────┘
LB sticky + WebSocket + /status 체크
HZ Hazelcast 5801 / Synchrony 5701
local home 인덱스·로그·temp (노드별 로컬 디스크)
Shared Home 첨부·아바타·export (NFS)
Database 본문·권한·사용자
| 저장 위치 | 담는 것 | 이번 장애와의 관계 |
|---|---|---|
| Local home (노드별) | Lucene 인덱스, 로그, 캐시, temp | 여기가 찼습니다. 노드마다 독립이라 1번 노드만 죽었습니다 |
| Shared home (NFS) | 첨부파일, 아바타, export/import | 여기가 차면 두 노드가 동시에 죽습니다 |
| Database | 페이지 본문·버전, 권한, 사용자 | 정상 — 그래서 데이터 손실은 없었습니다 |
| Hazelcast | 분산 캐시, 클러스터 락, 멤버십 | 느려진 노드를 죽은 노드로 즉시 판정하지 못합니다 |
헬스체크를 /status 로 바꾸는 것만으로 등급이 내려갑니다
Confluence는 노드 상태를 HTTP로 노출합니다. 이걸 보면 "프로세스는 살아 있지만 서비스는 못 하는" 상태를 정확히 잡아냅니다.
{"state":"RUNNING"} # 200 — LB 에 투입
{"state":"ERROR"} # 503 — 즉시 빼야 한다
{"state":"STARTING"} # 503 — 아직 준비 안 됨
balance roundrobin
cookie CONFLUENCE_LB insert indirect nocache # sticky + failover 허용
option httpchk GET /status
http-check expect string "RUNNING"
default-server inter 5s fall 3 rise 2
server node1 10.0.0.11:8090 check cookie n1
server node2 10.0.0.12:8090 check cookie n2
cookie ... indirect, F5는 fallback persistence로 처리합니다.20GB가 차기 전에 알아야 합니다
df 80% 알람은 몇 분 만에 20GB가 차는 상황에서는 늦습니다.
감시 지표를 용량이 아니라 증가율과 파일 개수로 바꿔야 개입할 시간이 생깁니다.
TEMP=/var/conf-temp
STATE=/var/tmp/.conf_temp_state
CNT=$(find "$TEMP" -maxdepth 1 -type f \
\( -name 'upload_*.tmp' -o -name '*.part' \) | wc -l)
SZ=$(du -sm "$TEMP" | cut -f1)
PREV=$(cut -d' ' -f2 "$STATE" 2>/dev/null || echo 0)
echo "$CNT $SZ" > "$STATE"
DELTA=$((SZ - PREV))
# 파일 100개 초과 또는 5분간 2GB 이상 증가 → 즉시 알림
if [ "$CNT" -gt 100 ] || [ "$DELTA" -gt 2048 ]; then
curl -s -X POST "$ALERT_HOOK" -H 'Content-Type: application/json' \
-d "{\"text\":\"temp 급증 $CNT개 / ${SZ}MB (+${DELTA}MB/5m)\"}"
fi
터졌을 때 — 복구 절차
temp는 Confluence 정지 상태에서 전체 삭제해도 안전합니다(기동 시 재생성).
단 index, attachments,
confluence.cfg.xml은 절대 건드리면 안 됩니다.
echo "disable server confluence/node1" | socat /var/run/haproxy.sock stdio
# ② 정지 후 공간 확보
systemctl stop confluence
find /var/conf-temp -mindepth 1 -delete
find /var/dumps -name '*.hprof' -mtime +0 -delete # 분석 필요하면 먼저 이관
df -h
# ③ 기동 → RUNNING 확인 후 LB 재투입
systemctl start confluence
curl -s localhost:8090/status
체크리스트
client_max_body_size = 첨부 최대 크기 + 5MBproxy_request_buffering off — nginx 이중 스테이징 차단lsof 확인 포함)/status + RUNNING 문자열 검사ExecStartPre로 기동 시 temp 초기화catalina.out logrotate 등록 (없으면 무한 증가)삽질 노트
lsof 없이 지우면 안 된다
진행 중인 업로드의 스테이징 파일을 지우면 사용자 업로드가 깨집니다. mtime 정지 + lsof 미점유 두 조건을 모두 봐야 합니다.
df 는 100%, du 는 작다
삭제됐지만 프로세스가 붙잡고 있는 파일입니다.
lsof +L1로 찾고, 결국 재시작해야 반환됩니다.
chunked 는 프록시 차단을 우회
Content-Length가 없으면 헤더 기반 룰이 통과됩니다.
앞단 차단만으로 끝나지 않고 reaper가 같이 있어야 합니다.
nginx 도 임시폴더를 쓴다
/var/lib/nginx/body를 확인하세요.
Confluence와 같은 호스트면 여기도 디스크를 채우는 벡터입니다.
local home 을 NFS 에 두지 말 것
인덱스 손상 위험으로 Atlassian이 금지합니다. NFS는 shared home 전용입니다.
자체 스크립트의 재시도 루프
첨부를 올리는 사내 자동화에 지수 백오프와 재시도 상한이 없으면 정확히 이 형태의 장애를 만듭니다.
제한은 앱에만 걸면 절반만 걸린 것입니다
거부될 업로드가 디스크를 먹지 않게 앞단에서 끊고,
남는 잔해는 회수하고, 그래도 터지면 업로드 하나만 아프게 격리하세요.
'기타' 카테고리의 다른 글
| 스마트폰 15년, 뇌는 무엇을 잃었나 — 도파민 중독 설계 분석과 대응 전략 (0) | 2026.09.13 |
|---|---|
| 9.11 테러, 그날의 102분 — 배경부터 진행 과정, 그 이후까지 (0) | 2026.09.12 |
| 옥수수 산업 완전정리 — 12억 톤 곡물이 식탁으로 오는 길 (1) | 2026.08.11 |
| 모두의 카드 모바일 티머니 혜택 비교 — 실물카드·후불카드와 뭐가 다를까 (2026) (1) | 2026.08.03 |
| AI 엔지니어링이란 (1) | 2026.07.19 |
- Total
- Today
- Yesterday
- 배드민턴팁
- UA93
- 모두의카드
- PostgreSQL
- Ai
- 가변보상
- AI Engineer
- 석유 독점
- claudecode
- 럭비 #노사이드게임
- 911타임라인
- DBHub
- PowerShell
- 배드민턴신입
- 킹우의 수
- 사내DB
- Gemma사용법
- MCP
- 내향인운동
- 스탠다드 오일
- 국제곡물시장
- OracleDatabase
- 동호회적응
- 적극적 자유 #소극적 자유
- AI AGENT #CLAUDE CODE #개발자 #AI 자동화 #CODEX CLI #GEMINI CLI #OPENHANDS
- 폐쇄망AI
- SQLcl
- 우르비에트오르비
- nginx
- 맥주 #YEBISU
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
