Jiwon Kim

GPU Cloud 플랫폼 백엔드 (MSA)

2024.08 – 현재 · 백엔드 개발 (선임) · 엔쓰리엔(주) 직지랩스

  • Java 21
  • Spring Cloud Gateway
  • Spring WebFlux
  • Reactor-Kafka
  • R2DBC
  • PostgreSQL
  • Redis
  • fabric8 kubernetes-client
  • Prometheus / Micrometer
  • Gradle
  • Docker
  • GitLab CI

Overview

개요

GPU 클라우드 웹 서비스를 여러 개의 작은 서버(마이크로서비스)로 나눠 만든 백엔드입니다. 모든 요청이 거쳐 가는 입구 서버(게이트웨이), 서버 간 메시지 큐(Kafka) 통신, 쿠버네티스 자원 상태 실시간 감시를 설계하고 구현했습니다.

  • Spring Cloud Gateway 단일 진입점 설계 — 라우트를 프록시 전달이 아닌 필터 체인에서 종결하는 Filter-centric 구조, AuthFilter(우선순위 -900)로 WebClient 논블로킹 JWT 인가 위임
  • Kafka↔HTTP 비동기 브리지: Redis 시퀀스로 유니크 Key 생성 후 MonoSink 상관처리로 비동기 메시징을 동기 REST 응답으로 변환
  • fabric8 Informer로 Deployment/PVC 상태 실시간 감지(AbstractInformer<T> 추상화) + Ceph PV 사용량 10분 주기 수집·토큰 자동 재발급
  • 4토픽 리액티브 Kafka 컨슈머 → 유형별 핸들러 → R2DBC 멱등 영속(existsBy), onErrorContinue 장애 격리
  • Redis Write-Back + Pub/Sub PK ack 기반 분산 정합성 패턴으로 리소스 명령 오케스트레이션, STOMP WebSocket 게이트웨이로 상태 실시간 push
  • MSA 공통 인프라(reactor-kafka 설정·공유 메시지 계약·prefix Redis 직렬화) + GitLab CI 기반 NonWeb 워커 다수 운영

Architecture

시스템 아키텍처

GPU Cloud 플랫폼 아키텍처: 사용자 → BFF → API Router → Resource Server·K8s Master → GPU Node·Kafka → Logger/Metering·Log DB의 도메인별 흐름
시스템 데이터 흐름

Metrics

성과 지표

MSA Gateway

단일 진입점

Filter-centric

실시간 이벤트 파이프라인

K8s 모니터링

다수 리액티브 설계

NonWeb 워커

Troubleshooting

트러블슈팅

Kafka 비동기 ↔ HTTP 동기 브리지

문제 상황

게이트웨이는 HTTP 요청을 받지만 백엔드 서비스들과는 Kafka 비동기 메시징으로 통신해야 했다. 응답이 별도 토픽으로 비동기 도착하므로, 원래의 HTTP 요청과 비동기 응답을 어떻게 상관(correlate)시켜 동기 REST 응답으로 되돌릴지가 핵심이었다. WebFlux 논블로킹 환경이라 스레드 블로킹 없이 처리해야 했다.

원인 가설

요청마다 유니크한 Key를 부여해 발행하고, 그 Key로 보류 중인 응답 핸들러를 등록해 두면, 응답 도착 시 Key 매칭으로 정확히 짝지을 수 있다. Mono.create로 만든 sink를 보류시켰다가 응답 도착 시 완료하면 논블로킹으로 동기 응답을 만들 수 있다.

검증

엔드포인트+메서드별 시퀀스를 Redis Hash에 INCR해 유니크 Key(endpoint-method-seq)를 생성하고, Mono.create의 sink를 응답 핸들러의 in-flight 맵(ConcurrentHashMap)에 Key로 등록한 뒤 Kafka로 발행했다. 응답 컨슈머가 동일 Key의 레코드를 받으면 보류 중인 sink를 완료시키는지, 30초 타임아웃 시 504로 폴백하는지 확인했다.

해결

  • Redis INCR 시퀀스로 유니크 Key를 만들고 Mono.create sink를 등록 → Kafka 응답 도착 시 Key 매칭으로 sink 완료하는 요청-응답 상관 처리를 구현했다.
  • 라우트를 프록시 전달이 아닌 필터 체인 내부에서 Kafka로 종결하는 Filter-centric 구조로 설계했다.
  • AuthFilter(우선순위 -900)는 외부 인가 서비스에 WebClient 논블로킹으로 JWT 인가를 위임하고, 응답 클레임을 다운스트림에 주입했다.
  • 일부 응답(예: SSH 키)은 octet-stream 파일 다운로드(Content-Disposition)로 분기 처리했다.

Kubernetes 실시간 모니터링 + 외부 API 토큰 자동 갱신

문제 상황

클러스터의 Deployment/PVC 상태 변화를 실시간으로 감지하고, 스토리지 사용량은 외부 대시보드 API에서 주기 수집해야 했다. 외부 API 토큰은 만료되면 401을 반환했고, 만료 시점에 수집이 중단되면 사용량 데이터에 공백이 생겼다.

원인 가설

리소스 종류별로 동일한 Informer 부트스트랩/콜백 패턴이 반복되므로 제네릭 추상 베이스로 공통화할 수 있다. 외부 API 호출은 401을 감지하면 토큰을 재발급한 뒤 1회 재시도하면 만료로 인한 수집 공백을 막을 수 있다.

검증

AbstractInformer<T extends HasMetadata> 추상 베이스로 Deployment/PVC 워처를 공통화하고, 각 콜백을 try/catch로 격리해 한 이벤트의 예외가 Informer 전체를 죽이지 않는지 확인했다. 라벨 셀렉터로 대상만 필터링되는지, 외부 사용량 API가 401을 반환하면 토큰 재발급 → 재호출(onErrorResume) 1회로 복구되는지 점검했다.

해결

  • fabric8 Informer 기반 AbstractInformer<T>로 Deployment/PVC 실시간 워처를 추상화하고, 콜백 예외를 개별 격리했다.
  • PVC 볼륨 지연 바인딩에 대응해 재시도 큐(ConcurrentHashMap + 15초 스케줄 + TTL)로 eventual consistency를 처리했다.
  • 외부 사용량 API는 10분 스케줄로 수집하고, 401 감지 시 토큰을 자동 재발급한 뒤 1회 재시도(AtomicReference 토큰 캐시)하도록 했다.
  • 수집/감지 결과는 리액티브 Kafka로 비동기 발행했다(app/log 클러스터 분리).

4토픽 이벤트 파이프라인과 멱등 영속화

문제 상황

모니터링 워커가 발행한 리소스/사용량/인증 이벤트를 여러 토픽에서 소비해 DB에 영구 기록해야 했다. at-least-once 컨슈머 특성상 같은 메시지가 중복 수신될 수 있어, 그대로 저장하면 데이터가 중복되었다. 한 메시지의 처리 실패가 전체 스트림을 중단시켜서도 안 됐다.

원인 가설

토픽/컨슈머/핸들러를 데이터 주도(table-driven) 바인딩으로 묶으면 다수 토픽 구독을 일괄 구성할 수 있다. 저장 전에 existsBy...로 선조회하면 중복 CREATE를 1회만 영속해 멱등성을 확보할 수 있고, onErrorContinue로 개별 메시지 실패를 격리하면 스트림을 유지할 수 있다.

검증

TopicBinding record로 4개 토픽을 함수형 핸들러에 매핑해 receiveAutoAck로 일괄 구독하고, flatMap 병렬 처리 + 핸들러 예외 격리(safeApply) + onErrorContinue로 스트림이 유지되는지 확인했다. 중복 CREATE 메시지가 existsBy... 선조회로 1회만 영속되는지, CREATE/DELETE 생명주기가 start/end 컬럼으로 구간화되는지 점검했다.

해결

  • TopicBinding record + 함수형 핸들러 매핑으로 4토픽 리액티브 컨슈머를 일괄 구독하는 확장 용이 구조를 구현했다.
  • 메시지 단위 예외를 격리(onErrorContinue)해 개별 실패가 전체 스트림을 중단시키지 않도록 했다.
  • existsBy... 선조회로 중복 메시지를 1회만 R2DBC 영속(멱등 처리)하고, 리소스 생명주기를 startTime/endTime으로 구간화했다.

Redis Write-Back + Pub/Sub 분산 정합성

문제 상황

리소스 명령 오케스트레이터는 DB에 직접 쓰지 않고 캐시(Redis)에 먼저 반영한 뒤 외부 워커가 DB에 영속하는 구조였다. 그런데 후속 작업에는 DB가 생성한 PK가 필요했고, 명령 발행과 PK 회수 사이의 정합성을 비동기로 맞춰야 했다.

원인 가설

Redis에 명령(SET)을 발행하고, 외부 워커가 DB 반영 후 Pub/Sub 채널로 생성된 PK를 회신하게 하면, 캐시 선반영의 응답성과 DB PK의 정확성을 동시에 얻을 수 있다. 단, PK 콜백을 놓치지 않으려면 ack 구독을 먼저 시작한 뒤 명령을 발행하는 순서가 중요하다.

검증

wb:done:\{table\}:* Pub/Sub 채널을 먼저 구독(hot Flux로 핀 고정)한 다음 SET을 발행하도록 순서를 보장하고, 외부 워커가 DB 반영 후 보내는 PK 이벤트를 타임아웃(15초) 내 수신하는지 확인했다. PK는 메타 또는 채널 토큰에서 numeric/text로 추출되도록 점검했다.

해결

  • Redis를 명령 버스 + DB PK 콜백 채널로 사용하는 Write-Back 비동기 영속 패턴을 설계했다 — "ack 구독 먼저, 그다음 SET 발행" 순서로 콜백 유실을 방지했다.
  • 생성된 PK는 Pub/Sub 이벤트로 비동기 회수(타임아웃·ok 검증·PK 파싱 폴백 포함)했다.
  • 상태 이벤트(status-event)는 미완료(temp) 가드 + 재시도로 캐시에 반영했고, STOMP WebSocket 게이트웨이가 이 상태를 사용자 세션으로 실시간 push하도록 연결했다(Redis 단발 티켓 핸드셰이크 + scale-out 세션 라우팅).