fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • claude-code 페이지로 이동
    • harness 페이지로 이동
    • llm 페이지로 이동
    • ops 페이지로 이동
    • practice 페이지로 이동
  • algorithm 페이지로 이동
    • Alias Method: 가중치 랜덤 선택을 전처리 O(n), 추출 O(1)로 바꾸기
    • Welford's Online Algorithm: 값을 저장하지 않고 평균과 분산 계산하기
  • architecture 페이지로 이동
    • distributed-systems 페이지로 이동
    • domain 페이지로 이동
    • evolution 페이지로 이동
    • patterns 페이지로 이동
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • postgresql 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • gaps and islands: SQL 창 함수로 이어진 구간 찾기
    • MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 정규화와 역정규화: 무결성과 조회 비용 사이의 선택
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
    • 종료 신호는 어디서 멈추는가
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS와 TLS: 핸드셰이크, 인증서, 종료 지점
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • Alpine 이미지의 Java 에서 링크 바꿔치기(TOCTOU)를 막지 못하는 이유
    • Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • 로그에 traceId 남기기 — MDC 부터 OpenTelemetry 까지
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 클러스터 아키텍처: Broker, KRaft Controller와 복제
    • Kafka 기본 개념: Topic, Partition, Offset과 Segment
    • Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • Kafka 파티션·리밸런스·컨슈머 지연 운영
    • Kafka를 로그로 이해하기: 복제, 재처리, 상태와 데이터 통합
    • Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • SSH forced command 로 배포 키가 실행할 수 있는 명령 제한하기
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • model-router 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • observability 페이지로 이동
    • Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • K8s 위 Spring Boot 앱의 메트릭 수집
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS가 줄지 않는 이유: CPython, gc.collect(), malloc_trim
  • rabbitmq 페이지로 이동
    • RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • resume 페이지로 이동
    • 경험 및 경력기술서
    • 김병태 경력기술서
    • 김병태 포트폴리오
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • personal-projects 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • thinking 페이지로 이동
    • 자기주도 학습: 질문에서 시작해 글로 검증하기
    • 좋은 일을 넘어 중요한 일을 하는 법
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • Glossary↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/observability/K8s 위 Spring Boot 앱의 메트릭…
observability

K8s 위 Spring Boot 앱의 메트릭 수집

K8s 클러스터에서 Spring Boot 애플리케이션을 운영하다 보면 JVM 힙 사용량, HTTP 요청 수, 응답 시간 같은 지표를 모니터링하고 싶어진다. 팀에 중앙 Grafana가 이미 있다면 클러스터 내에서 Prometheus로 긁어다가 remotewrite로 쏴주기만 하면 된다. 이번에 실제로 적용하면서 삽질한 내용들을 정리해봤다. 클러스터 안에 Pr...

2026.09.03·5 min read·86 views

K8s 클러스터에서 Spring Boot 애플리케이션을 운영하다 보면 JVM 힙 사용량, HTTP 요청 수, 응답 시간 같은 지표를 모니터링하고 싶어진다. 팀에 중앙 Grafana가 이미 있다면 클러스터 내에서 Prometheus로 긁어다가 remote_write로 쏴주기만 하면 된다.

이번에 실제로 적용하면서 삽질한 내용들을 정리해봤다.

전체 구조

plaintext
Spring Boot Pod (/actuator/prometheus)
        ↓  scrape (15s)
  Prometheus (K8s 내, Agent 모드)
        ↓  remote_write
  중앙 Grafana (사내 공통 모니터링 플랫폼)

클러스터 안에 Prometheus를 띄우되, 로컬에 데이터를 쌓지 않고 중앙 플랫폼으로만 전달하는 구조다. 이걸 Agent 모드라고 한다.

Helm Chart 구성

prometheus-community/prometheus 차트를 Wrapper 형태로 감싸서 사용했다.

yaml
# Chart.yaml
apiVersion: v2
name: prometheus
type: application
version: 0.1.0
dependencies:
  - name: prometheus
    repository: https://prometheus-community.github.io/helm-charts
    version: "25.27.0"

최종 values.yaml

yaml
prometheus:
  server:
    global:
      scrape_interval: 15s
      evaluation_interval: 15s
      external_labels:
        cluster: ""
 
    defaultFlagsOverride:
      - --enable-feature=agent
      - --storage.agent.path=/data
      - --config.file=/etc/config/prometheus.yml
      - --web.console.libraries=/etc/prometheus/console_libraries
      - --web.console.templates=/etc/prometheus/consoles
      - --web.enable-lifecycle
 
    persistentVolume:
      enabled: false
 
    resources:
      requests:
        cpu: 100m
        memory: 256Mi
      limits:
        memory: 512Mi
 
    remoteWrite:
      - url: http://<중앙-grafana-수신-endpoint>/v1/prom/push
        headers:
          x-monitoring-service-code: "<서비스-식별-코드>"
 
  serverFiles:
    prometheus.yml:
      rule_files: []
      scrape_configs:
        - job_name: 'spring-apps'
          kubernetes_sd_configs:
            - role: endpoints
              namespaces:
                names:
                  - <앱-네임스페이스>
          relabel_configs:
            - source_labels: [__meta_kubernetes_endpoint_port_name]
              action: keep
              regex: metric
            - source_labels: [__meta_kubernetes_service_label_app]
              target_label: app
            - source_labels: [__meta_kubernetes_pod_node_name]
              target_label: node
            - source_labels: [__meta_kubernetes_pod_name]
              target_label: pod
          metrics_path: /actuator/prometheus
 
  alertmanager:
    enabled: false
  prometheus-pushgateway:
    enabled: false
  kube-state-metrics:
    enabled: false
  prometheus-node-exporter:
    enabled: false

설정 하나하나 짚어보자.

Agent 모드 활성화와 defaultFlagsOverride

Prometheus를 Agent 모드로 실행한다. 일반 모드와 다른 점은 로컬 TSDB가 없다는 것이다.

  • 일반 모드: 수집 → 로컬 저장 → PromQL 쿼리 가능
  • Agent 모드: 수집 → remote_write 전송 (WAL만 임시 사용)

Grafana가 사내 중앙 플랫폼에 있으니 클러스터 안에 데이터를 쌓을 필요가 없다. Agent 모드가 메모리도 훨씬 덜 먹는다. 실제로 일반 모드로 운영했을 때 519Mi를 쓰던 게 Agent 모드 전환 후 30Mi로 줄었다.

왜 defaultFlagsOverride를 써야 하나?

차트 deploy 템플릿의 args 블록이 이런 구조다.

plaintext
if defaultFlagsOverride 설정됨
  → defaultFlagsOverride 내용만 사용
else
  → --storage.tsdb.retention.time   (retention 설정 시)
  → --storage.tsdb.path             ← 조건 없이 항상 추가
  → extraFlags

Prometheus는 Agent 모드에서 --storage.tsdb.path가 있으면 시작을 거부한다. else 블록 안에서는 이 플래그를 피할 방법이 없다.

defaultFlagsOverride를 쓰면 else 블록 자체를 건너뛰기 때문에 tsdb 플래그가 아예 추가되지 않는다. 그래서 아래처럼 시도했다가 실패한 방법들이 있다.

yaml
# ❌ 이 차트에 없는 필드 — 그냥 무시됨
agentMode: true
 
# ❌ else 블록 안에서 실행됨 — tsdb 플래그와 함께 추가되어 충돌
extraFlags:
  - enable-feature=agent

persistentVolume.enabled: false

PVC를 끄고 emptyDir을 쓴다. Agent 모드에서는 로컬 저장이 없으니 영구 볼륨이 필요 없고, 클러스터에 StorageClass가 없는 경우에도 이 설정이 필요하다.

StorageClass 없이 PVC를 만들면 Pending 상태로 계속 머물러 Pod가 뜨지 않는다.

resources

yaml
resources:
  requests:
    cpu: 100m
    memory: 256Mi
  limits:
    memory: 512Mi

리소스 제한을 꼭 걸어야 한다. 처음에 limits 없이 배포했다가 Prometheus가 메모리를 무제한으로 써서 노드 전체가 NotReady가 됐다. 2core/4GB 노드에서 다른 앱들과 함께 돌아야 하니 512Mi로 잡았다.

CPU limit은 의도적으로 안 걸었다. 메모리 OOM은 프로세스 종료로 이어지지만 CPU 스로틀링은 그냥 느려지는 거라서, 수집 지연은 감수할 수 있어도 OOM은 막아야 하기 때문이다.

remoteWrite

yaml
remoteWrite:
  - url: http://<엔드포인트>/v1/prom/push
    headers:
      x-monitoring-service-code: "<코드>"

수집한 메트릭을 Prometheus remote_write 프로토콜로 중앙 플랫폼에 전송한다. 중앙 플랫폼마다 인증 방식이 다를 텐데, HTTP 헤더로 서비스 식별 코드를 넘기는 방식이었다.

serverFiles로 scrape job 정의

차트 기본값에는 kubernetes-apiservers, kubernetes-nodes 같은 k8s 시스템 scrape job이 10개 들어있다. OCR 앱 메트릭만 필요하므로 serverFiles로 완전히 덮어쓴다.

yaml
serverFiles:
  prometheus.yml:
    rule_files: []          # Agent 모드에서 rule_files 미지원
    scrape_configs:
      - job_name: 'spring-apps'
        ...

rule_files: []도 명시해야 한다. Agent 모드는 alerting/recording rule을 지원하지 않아서, 기본값으로 들어오는 rule_files 항목이 있으면 설정 로드 자체가 실패한다.

extraScrapeConfigs를 쓰지 않는 이유도 있다. 차트 템플릿이 serverFiles의 scrape_configs 뒤에 extraScrapeConfigs 내용을 이어붙이는 방식인데, scrape_configs: []로 비워두면 빈 배열 뒤에 리스트 아이템이 붙는 구조가 되어 YAML 파싱 오류가 난다.

yaml
# 이렇게 렌더링됨 → 파싱 에러
scrape_configs: []
- job_name: 'spring-apps'   ← invalid

scrape job을 직접 serverFiles.prometheus.yml.scrape_configs에 넣으면 이 문제가 없다.

kubernetes_sd_configs (서비스 디스커버리)

yaml
kubernetes_sd_configs:
  - role: endpoints
    namespaces:
      names:
        - <네임스페이스>

IP/포트를 직접 쓰는 게 아니라 K8s API를 통해 타겟을 자동으로 찾는다. role: endpoints는 Service에 연결된 Pod IP와 포트를 읽어온다. 네임스페이스를 지정하지 않으면 클러스터 전체를 뒤지니 필요한 곳만 지정하자.

relabel_configs

서비스 디스커버리로 찾은 타겟을 필터링하고 레이블을 붙이는 규칙들이다.

yaml
relabel_configs:
  # metric 포트만 수집
  - source_labels: [__meta_kubernetes_endpoint_port_name]
    action: keep
    regex: metric
 
  # 메트릭에 app, node, pod 레이블 추가
  - source_labels: [__meta_kubernetes_service_label_app]
    target_label: app
  - source_labels: [__meta_kubernetes_pod_node_name]
    target_label: node
  - source_labels: [__meta_kubernetes_pod_name]
    target_label: pod

Spring Boot 앱 Service에 포트가 두 개 있다. HTTP 트래픽용 포트와 메트릭용 포트. 이름으로 구분해서 메트릭 포트만 수집하도록 필터링한다.

Service에 포트 이름을 붙이는 방법:

yaml
spec:
  ports:
    - name: server    # 앱 트래픽용
      port: 80
      targetPort: 8080
    - name: metric    # 메트릭 수집용
      port: 8081
      targetPort: 8081

환경별 오버라이드

ArgoCD에서 values.yaml과 alpha-values.yaml을 순서대로 적용한다. 공통 설정은 values.yaml에, 환경별 차이는 오버라이드 파일에만 넣는다.

yaml
# alpha-values.yaml
prometheus:
  server:
    global:
      external_labels:
        cluster: my-cluster-alpha

cluster 레이블을 환경마다 다르게 찍어두면 Grafana에서 클러스터별로 필터링하기 편하다.

트러블슈팅 정리

문제원인해결
Pod가 Pending에서 안 뜸PVC Pending (StorageClass 없음)persistentVolume.enabled: false
노드 NotReady, 앱 전체 Terminatinglimits 없는 Prometheus가 노드 메모리 고갈memory limits: 512Mi 설정
CrashLoopBackOffextraFlags: enable-feature=agent + 차트 기본 --storage.tsdb.path 충돌defaultFlagsOverride로 전환
앱 메트릭이 Grafana에 안 들어옴extraScrapeConfigs를 server 하위에 잘못 배치차트 루트 레벨로 이동 후 serverFiles로 통합
OOMKilled 반복Agent 모드 미적용으로 TSDB 15일 보관 + 기본 scrape job 10개defaultFlagsOverride + serverFiles로 기본 job 제거
설정 로드 실패 (scrape_configs 파싱 오류)serverFiles.scrape_configs: [] + extraScrapeConfigs 병용 시 YAML 구조 깨짐scrape job을 serverFiles에 직접 정의
설정 로드 실패 (rule_files not allowed)Agent 모드에서 rule_files 미지원rule_files: [] 명시

삽질을 정말 많이 했다. 차트 소스를 직접 뜯어보지 않으면 알기 어려운 것들이 있었다. defaultFlagsOverride 같은 건 values.yaml 주석에 예시가 있어서 찾을 수 있었지만, deploy 템플릿의 분기 구조를 이해하지 못하면 왜 extraFlags로는 안 되는지 납득하기 어렵다.

최종적으로 Agent 모드가 정상 동작하면 메모리 사용량이 드라마틱하게 줄어든다. 519Mi → 30Mi. TSDB가 없으니 당연한 결과다.

on this page
  • 01전체 구조
  • 02Helm Chart 구성
  • 03최종 values.yaml
  • Agent 모드 활성화와 defaultFlagsOverride
  • persistentVolume.enabled: false
  • resources
  • remoteWrite
  • serverFiles로 scrape job 정의
  • kubernetes_sd_configs (서비스 디스커버리)
  • relabel_configs
  • 04환경별 오버라이드
  • 05트러블슈팅 정리
tags
#study

이런 글도

  • observability

    Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식

    2026.09.03

댓글 (0)