본문 바로가기

전체 글181

화면은 뜨는데 API만 503 — Ingress가 Istio Gateway를 몰래 우회하고 있었다 TL;DR- 증상: 관리자 콘솔 도메인이 전 환경 503 ("Backend service does not exist")- 원인: ALB Ingress backend가 frontend-admin-svc를 직접 가리켜 Istio Gateway를 우회 → VirtualService의 path 분기가 아예 평가되지 않음 → /v1/admin/* API 경로가 SPA pod에 도달 → 503/500- 해결: Ingress backend를 istio-ingressgateway-private로 교체 + pathType: Prefix → ImplementationSpecific이런 분께: 하나의 도메인에서 path 기준으로 SPA와 Backend API를 함께 서빙하는 구조를 .. 2026. 7. 17.
GHES를 ACM으로 옮기면서 만난 두 함정 — NLB SG와 babeld TL;DR- 배경: 사설망 자체 호스팅 GitHub Enterprise Server(GHES)가 Let's Encrypt 인증서. 90일마다 6단계 수동 갱신 — 라우팅 테이블 변경 / SG 인바운드 0.0.0.0/0 오픈 / EIP 연결 / DNS 변경 / 콘솔 클릭 / 6단계 원복. 휴먼 에러 시한폭탄.- 해결: ACM (DNS-01 검증) + NLB로 전환. DNS 검증이라 외부 노출 불필요, 자동 갱신. Cross-account 제약 (NLB가 GHES와 같은 계정, Route53은 다른 계정) 때문에 Route53 Alias 불가 → CNAME 사용.- 마이그레이션 후 만난 두 함정: - 함정 1: NLB 보안그룹 인바운드에 run.. 2026. 6. 10.
같은 워치리스트 캐시에 다른 두 함정 — 활성 사용자 cold start + 장마감 miss 폭주 TL;DR- 워치리스트 차트 캐시에 서로 다른 두 시나리오가 있었음: - 시나리오 1 (장중·활성 사용자): 10s TTL 캐시인데 사용자가 매 10초마다 cold start 3~4s 체감. 활성 세션 내내 반복. - 시나리오 2 (장마감·전체 사용자): 캐시 키가 `ric_from_to_dayType` 인데 Non-Regular(AFTER/CLOSED/PRE) 동안 from·to 고정 → 1분 TTL이 매분 동일 키를 miss 처리 → Hit Rate 80% → 8%, miss 23k → 6.18M/day.- 두 문제는 같은 워치리스트 캐시인데 증상·시기·원인 모두 다름. 하나의 TTL로는 해결 불가.- 해결: - 시나리오 1: 논리/물리 TTL 분리 (stale-while-re.. 2026. 6. 9.
쓰지도 않는 APM에 매달 청구되고 있었다 — Datadog 청구서 58% 줄인 audit TL;DR- 증상: Datadog 월 청구서 $1,376 (약 210만원). 무심코 결제되고 있었는데 팀에서 APM 대시보드는 2개월째 아무도 안 열고 있었고, Alert는 다섯 개나 켜져있는데 *전부 알림 채널 미설정*.- 처음 본 단서: '있긴 한데 안 쓰는' 기능들이 청구서 대부분을 차지. APM 25호스트 ($659), NDM 2장치 ($20), Synthetics 5개 paused 9개월째.- 진짜 원인: SaaS 모니터링은 *기능을 활성화*하는 순간 청구가 시작되고, 활용도가 0이어도 청구는 계속된다. 정기 audit 없으면 청구서가 서서히 부풀어진다. 그리고 청소만으론 절반에서 멈춘다 — 나머지는 *약정(Committ.. 2026. 6. 9.
?: ""가 만든 3시간 장애 — null을 빈 문자열로 갈음하는 관행이 위험한 이유 TL;DR- 증상: 어느 평범한 오후, 멀쩡하던 두 개의 API가 갑자기 StringIndexOutOfBoundsException: begin 0, end 2, length 0 으로 무너짐. 13:45 ~ 16:52, 약 3시간 장애.- 처음 본 단서: 발생 7분 전 새로운 데이터 레코드 3건이 처음 유입됨. 해당 레코드가 처음으로 응답 대상에 포함되는 순간 폭발.- 진짜 원인: upstream 서비스가 일부 메타 데이터의 null을 ""로 갈음해 내려보냄 (?: ""). downstream은 ?.substring(0, 2)로 null-safety를 걸어두었지만, 값이 null이 아닌 ""이라 safe call(?.)을 그대로 통과 → .. 2026. 5. 26.
DaemonSet인데 N배 중복 수집 — alloy 로그 파이프라인이 OOM 무한 루프에 빠진 이유 TL;DR- 증상: 노드 25대짜리 클러스터에서 alloy DaemonSet pod 25개 전부가 OOMKilled (exit 137) → 재생성 무한 루프. memory limit 512Mi인데 일부 pod은 1.7GB+ 사용. 동시에 kubelet API 폭주로 client-side throttling (delay=1m32s) 발생.- 처음 본 단서: 'alloy 메모리 한 번 늘려보자' 시도하고 잠시 안정 → 또 폭발 반복. 메모리 증설로는 해결 안 됨 = 입력 자체가 비정상적으로 많다.- 진짜 원인: alloy의 loki.source.kubernetes 컴포넌트는 K8s API로 '클러스터 전체 pod 로그'를 streaming. Da.. 2026. 5. 26.
주말 트래픽 0인데 노드는 계속 켜져있다 — nodeSelector 빠진 모니터링 파드가 막은 scale-in TL;DR- 증상: WebSocket 워크로드용 노드그룹(r7i.8xlarge)이 평일 6.7대로 운영되다가 트래픽 0인 주말에도 2대가 종료되지 않고 상주.- 처음 본 단서: 노드그룹 minSize는 0인데도 desired/running이 0으로 안 내려감. kubectl describe로 노드 안을 들여다보니 워크로드 파드는 없고 모니터링 관련 파드(otel-collector, cost-forecasting)만 남아있음.- 진짜 원인: 두 파드 모두 nodeSelector 미설정 → 스케줄러가 가장 여유 있는 WebSocket 노드에 배치. cluster-autoscaler는 '이동시킬 수 없는' 파드가 노드에 남아있으면 scale-in을.. 2026. 5. 26.
@Transactional 안에서 다른 DB를 호출했더니 — idle-in-transaction 7.54분 TL;DR- 증상: Grafana DB Connection Hold Time Max가 1시간 안에 8번 튐. 최대 7.54분. 반면 avg는 5~20ms 정상.- 처음 본 단서: avg는 정상이고 max만 튀는 패턴 — 전체가 느려진 게 아니라 '1개 트랜잭션이 커넥션을 독점'하는 형태.- 진짜 원인: @Transactional이 메인 DB(HikariPool-1) 커넥션을 잡은 상태에서, 메서드 안에서 *다른 DataSource*(서브 DB, HikariPool-2)를 10스레드 × 14배치로 병렬 조회. 그동안 메인 DB 커넥션은 쿼리 없이 트랜잭션만 열어두고 대기 → PostgreSQL 입장에서 'idle in transac.. 2026. 5. 26.
Hibernate LockAcquisitionException인데 lock 경합이 아니었다 — PostgreSQL 40001과 슬로우 쿼리의 합작 TL;DR- 증상: 뉴스 목록 API가 평소 2~3초 슬로우, 가끔 SQLSTATE 40001로 실패- 처음 의심: Hibernate가 LockAcquisitionException으로 매핑 → 트랜잭션 lock 경합- 진짜 원인: lock 경합 아님. PostgreSQL Replica의 recovery conflict → 조건 1: Replica에서 슬로우 쿼리 실행 중 (수 초 이상) → 조건 2: Main DB에서 동일 테이블 대량 INSERT/UPDATE + VACUUM → 두 조건이 겹치면 max_standby_streaming_delay(30s) 초과 → replica가 쿼리 강제 종료 → 40001- 해결: (1) 파티션 pruning 활성화로 .. 2026. 5. 13.
RDS를 교체했는데 앱은 모른다 — JVM DNS 캐싱과 HikariCP의 함정 TL;DR- 증상: RDS 7개 재생성 후 DNS 엔드포인트를 새 RDS로 교체했는데, 앱이 계속 old RDS에 연결 시도- 처음 본 단서: nslookup으로 도메인 확인하면 새 IP인데, 앱 로그엔 old IP 연결 오류- 진짜 원인: JVM이 DNS 조회 결과를 프로세스 수명 동안 영구 캐싱 (networkaddress.cache.ttl = -1) HikariCP는 커넥션 풀 생성 시점의 IP를 재사용, 새 커넥션도 JVM DNS 캐시에서 IP 가져옴 → 앱 재시작 전까지 DNS 변경이 완전히 반영 안 됨- 해결: networkaddress.cache.ttl=60 설정으로 JVM DNS TTL을 유한하게 변경이런 분께: RDS 교체·마이그레이션·재생성 후 앱이 ol.. 2026. 5. 13.
k8s 이관 후 WebSocket이 3~4초 늦어졌다 — CPU throttling + Throttle 버그의 합작 TL;DR- 증상: WebSocket connect + subscribe 후 첫 데이터 수신까지 3~4초 딜레이 EC2에선 멀쩡, k8s 이관 후 발생- 1차 대응: CPU 사용량 확인 → CPU limit 150m에 막혀 CFS throttling 발생 → CPU limit 1000m 상향 → spring.kafka.listener AVG 5.19s → 29.6ms (178배 개선)- 그런데 의문이 남음: "왜 CPU 부하가 그렇게 컸지? throttle 로직이 있는데"- 2차 분석: 코드 점검 → throttle 비교식이 epoch ms(1.74조)를 200ms와 비교 → 항상 false → throttle이 한 번도 동작 안 했음 .. 2026. 5. 10.
Airflow DAG가 돌 때마다 죽는 K8s 파드 — JVM Full GC + CPU throttle 함정 TL;DR- 증상: 백엔드 파드가 19시간 동안 2번 재시작 — 둘 다 Airflow DAG 실행 시점, exit 137- 처음 의심: exit 137은 OOMKilled — JVM 메모리 부족- 진짜 원인: reason이 'OOMKilled' 아닌 'Error' → liveness probe 6회 연속 실패로 인한 SIGKILL → 대용량 처리 burst → JVM Full GC (8G heap, Stop-The-World) → CPU throttle(limit 3000m)과 겹쳐 GC가 CPU 못 받아 더 길어짐 → /actuator/health/liveness 응답 불가 60초 (= failureThreshold 6 × period 10s) .. 2026. 5. 10.
Spring Boot가 기동에 220초 걸린 이유 — k8s CPU limit + JIT의 함정 TL;DR- 증상: Spring Boot 백엔드 서비스가 기동에 60~180초, 최악의 경우 220초까지- 처음 의심: 무거운 의존성 / Hibernate scan / JPA proxy 생성 등 코드 측- 진짜 원인: K8s CPU limit이 정상 운영 기준(500m 등)에 맞춰져 있었음 → 기동 시 JIT 컴파일/클래스 로딩/Hibernate burst가 CPU 1코어 가까이 사용 → CFS throttling: 100ms 주기당 50ms만 실행 → 기동이 절반 속도로 진행- 해결: CPU limits만 2000m으로 상향 (requests는 유지 → 스케줄링 영향 없음)- 결과: 모든 서비스 기동 시간 평균 10초 내외로 단축 (최소 4초까지)이런 분께: K8s에서 .. 2026. 5. 10.
API는 504, replica DB는 EOF — PostgreSQL Hang 쿼리의 후폭풍 TL;DR- 증상: 한 서비스에선 504 Gateway Timeout, 다른 서비스에선 replica DB 커넥션 강제 종료(EOF)- 처음 본 단서: pg_stat_activity에 13시간 이상 active 상태인 SELECT 쿼리 2개- 진짜 원인: 13시간 hang 쿼리가 오래된 XID를 잡고 있어서 autovacuum 무력화 → dead tuple 누적 (일부 테이블 dead_pct 5,704%) → 테이블 bloat → 일반 쿼리 성능 급락 → 504 → primary가 vacuum WAL을 replica에 보내면 lock conflict → max_standby_streaming_delay 초과 → replica가 backend.. 2026. 5. 7.
Heap을 늘렸는데 OOM이 떨어졌다 — Spring + K8s의 JAVA_OPTS 함정 TL;DR- 증상: Spring Boot 백엔드 파드 2개가 거의 동시에 OOM으로 재시작 (exit code 143)- 처음 본 단서: exit 143은 OOMKilled가 아닌 SIGTERM — kubelet이 liveness probe 실패로 보낸 신호- 진짜 원인: values.yaml에 JAVA_OPTS=-Xms256m -Xmx768m 을 줬는데 JVM이 못 받음 → 배포 템플릿이 env 키를 --key=value Spring 커맨드라인 인자로 변환 → --java.opts=-Xms256m -Xmx768m 으로 넘어가서 JVM은 무시 → Xmx가 JVM 기본값(컨테이너 메모리의 25%, ~256Mi)으로 동작 중이었음- 해결: JAVA_OPTS →.. 2026. 5. 7.