- Published on
Service Discovery 완전 가이드 2025: Consul, Eureka, Kubernetes DNS, 로드 밸런싱 통합
- Authors

- Name
- Youngju Kim
- @fjvbn20031
TL;DR
- Service Discovery = 마이크로서비스의 필수: IP/포트 하드코딩 X, 동적 발견
- 2가지 모델: Client-side (Eureka, Ribbon), Server-side (K8s, AWS ALB)
- 3대 옵션: Consul (가장 인기), Kubernetes DNS (K8s 환경), etcd (인프라용)
- Health Check 필수: 죽은 인스턴스 자동 제거
- Service Mesh 통합: Istio, Linkerd가 디스커버리 + 로드 밸런싱 + observability
1. Service Discovery가 필요한 이유
1.1 모놀리스의 단순함
# 모놀리스
def get_user(user_id):
return database.query(...) # 같은 프로세스
함수 호출. 끝.
1.2 마이크로서비스의 복잡함
# 마이크로서비스
def get_user(user_id):
response = requests.get(f"http://???/users/{user_id}")
**???**에 무엇을 넣을까?
옵션 1: 하드코딩
USER_SERVICE = "http://10.0.1.5:8080"
문제:
- IP가 변경되면? (재배포, 스케일링)
- 여러 인스턴스 중 어디로?
- 인스턴스가 죽으면?
- 새 인스턴스가 추가되면?
해결: Service Discovery.
1.3 동적 환경의 현실
Kubernetes 환경:
- Pod이 죽고 재생성됨 (다른 IP)
- HPA로 스케일링 (인스턴스 수 변화)
- 롤링 업데이트 (옛 + 새 인스턴스 공존)
- 노드 장애 (다른 노드로 이동)
필요한 것:
- 동적 인스턴스 추적
- 자동 등록/제거
- 로드 밸런싱
- 헬스 체크
2. Service Discovery 모델
2.1 Client-Side Discovery
[Client]
↓ "users 서비스 어디?"
[Service Registry]
↓ "[10.0.1.5, 10.0.1.6, 10.0.1.7]"
[Client] → 직접 선택 → [Server]
예: Netflix Eureka + Ribbon
장점:
- 더 적은 hop (직접 호출)
- 클라이언트가 로드 밸런싱 전략 선택
- 인프라 단순
단점:
- 모든 언어에 클라이언트 라이브러리 필요
- 클라이언트가 디스커버리 로직 처리
- 결합도 증가
2.2 Server-Side Discovery
[Client]
↓ "/users"
[Load Balancer / Proxy]
↓ (디스커버리 + 로드 밸런싱)
[Service Registry]
[Server]
예: Kubernetes Service, AWS ALB
장점:
- 클라이언트는 단일 endpoint만 알면 됨
- 언어 무관
- 중앙 관리
단점:
- 추가 hop (load balancer)
- 인프라 복잡도
2.3 Self-Registration vs Third-Party Registration
Self-Registration:
- 서비스 시작 시 스스로 등록
- 종료 시 스스로 해제
- 예: Spring Cloud + Eureka
@SpringBootApplication
@EnableDiscoveryClient // 자동 등록
public class UserServiceApplication {
// ...
}
Third-Party Registration:
- 외부 시스템이 등록 관리
- 서비스는 모름
- 예: Kubernetes (kubelet이 등록), Registrator + Docker
3. 주요 도구 비교
3.1 Consul
HashiCorp의 service mesh + 디스커버리.
# 서비스 등록
service {
name = "users"
port = 8080
check {
http = "http://localhost:8080/health"
interval = "10s"
}
}
기능:
- Service discovery (DNS + HTTP API)
- Health checking
- KV store
- Multi-datacenter
- Service mesh (Connect)
장점:
- 풍부한 기능
- 멀티 DC 지원
- 강력한 health check
- 활발한 커뮤니티
단점:
- 운영 복잡 (Raft 클러스터)
- 학습 곡선
3.2 Eureka (Netflix)
Netflix가 만든 client-side 디스커버리.
# application.yml
eureka:
client:
serviceUrl:
defaultZone: http://eureka:8761/eureka/
장점:
- Spring Cloud와 완벽 통합
- 단순
- AP (가용성 우선)
단점:
- Java 중심
- Netflix가 유지보수 줄임 (2018~)
- 새 프로젝트는 Consul/K8s 추천
3.3 etcd
Kubernetes의 기반 데이터 저장소.
용도:
- Kubernetes 상태 저장
- 분산 락
- 설정 관리
- (덜 일반적) Service discovery
장점:
- Raft 합의로 강력한 일관성
- gRPC API
- Kubernetes 생태계
단점:
- 직접 service discovery로는 자주 사용 안 함
- K8s 위에서는 K8s API 사용
3.4 Apache ZooKeeper
가장 오래된 분산 코디네이션 시스템.
역사:
- Hadoop 생태계
- Kafka가 사용 (KRaft로 대체 중)
- HBase, Solr
장점:
- 매우 성숙
- 검증됨
단점:
- 운영 복잡
- 새 프로젝트는 etcd/Consul 선호
- Java 의존
3.5 Kubernetes DNS
K8s 내장 디스커버리. 추가 도구 불필요.
apiVersion: v1
kind: Service
metadata:
name: users
spec:
selector:
app: users
ports:
- port: 8080
클라이언트:
response = requests.get("http://users:8080/api/users/123")
# K8s DNS가 자동으로 IP로 해결
작동:
- CoreDNS가 K8s API 모니터링
- 서비스 생성/변경 시 DNS 레코드 자동 업데이트
users.namespace.svc.cluster.local형식
장점:
- 추가 도구 불필요
- 표준 DNS (모든 언어 지원)
- 자동 등록/제거
단점:
- K8s 안에서만
- DNS 캐싱 문제 (TTL 짧게)
3.6 비교표
| Consul | Eureka | etcd | ZooKeeper | K8s DNS | |
|---|---|---|---|---|---|
| 모델 | Both | Client-side | Server | Both | Server-side |
| 언어 | Any | Java 중심 | Any | Any | Any |
| CAP | CP | AP | CP | CP | CP |
| Health Check | 강력 | 단순 | 외부 | 외부 | 강력 |
| Multi-DC | ✅ | 제한적 | ❌ | ❌ | 외부 |
| 운영 난이도 | 보통 | 낮음 | 보통 | 높음 | 매우 낮음 |
4. Health Checking
4.1 왜 중요한가?
서비스 등록 ≠ 건강함. 등록되었지만 응답 못 하는 인스턴스를 거르기.
[Client] → [Load Balancer] → [Healthy Instance] ✅
→ [Dead Instance] ❌ ← 제거 필요!
4.2 Health Check 종류
1. HTTP:
GET /health
HTTP/1.1 200 OK
{"status": "healthy"}
가장 흔함. 단순 endpoint.
2. TCP:
TCP connection to port 8080
연결만 확인. HTTP보다 약함.
3. gRPC:
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}
gRPC 표준.
4. Script:
#!/bin/bash
# 커스텀 로직
if [ $(check_db) ]; then exit 0; fi
exit 1
복잡한 검증 로직.
4.3 Health Check 깊이
Liveness vs Readiness (Kubernetes 용어):
Liveness: "이 컨테이너가 살아있나?"
- 죽었으면 → 재시작
- 단순 체크
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
Readiness: "이 컨테이너가 트래픽을 받을 수 있나?"
- 아니면 → 로드 밸런서에서 제외
- DB 연결, 의존 서비스 확인
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Startup (K8s 1.16+): "초기화 완료했나?"
- 느린 시작 앱용 (Java)
- liveness/readiness보다 먼저
4.4 Health Check 설계
Shallow Check:
@app.route('/health')
def health():
return {'status': 'ok'}, 200
문제: 앱은 살아있지만 DB 연결 끊겼을 수 있음.
Deep Check:
@app.route('/health')
def health():
if not db.ping():
return {'status': 'db_down'}, 503
if not cache.ping():
return {'status': 'cache_down'}, 503
return {'status': 'ok'}, 200
문제: DB가 잠시 느리면 모든 인스턴스가 unhealthy → cascade failure.
균형:
- Liveness: shallow (앱 자체)
- Readiness: deep (의존성 포함)
- 의존성 장애 시 readiness만 fail → 트래픽 차단, 재시작 X
4.5 Cascade Failure 방지
@app.route('/ready')
def ready():
# DB 다운 → 503 → 트래픽 차단
# 모든 인스턴스가 동시에 503 → 모든 트래픽 차단 → 더 큰 장애!
pass
해결: Bulkhead, Circuit Breaker 패턴.
5. Kubernetes Service Discovery 깊이
5.1 Service 종류
ClusterIP (기본):
- 클러스터 내부에서만 접근
- 가상 IP (kube-proxy가 처리)
NodePort:
- 모든 노드의 특정 포트로 노출
- 외부 접근 가능
LoadBalancer:
- 클라우드 로드 밸런서 자동 생성
- AWS ALB, GCP LB, Azure LB
ExternalName:
- 외부 DNS 이름으로 매핑
- 외부 서비스 통합
Headless (clusterIP: None):
- 가상 IP 없음
- DNS는 직접 Pod IP 반환
- StatefulSet, 클라이언트 사이드 LB
5.2 DNS 형식
<service>.<namespace>.svc.cluster.local
예시:
users(같은 네임스페이스)users.production(다른 네임스페이스)users.production.svc.cluster.local(전체)
5.3 kube-proxy
각 노드에서 실행. Service IP → Pod IP 변환.
모드:
- iptables (기본): iptables 규칙 사용
- IPVS: 더 빠름, 큰 클러스터 권장
- eBPF (Cilium): 가장 빠름
5.4 Endpoint와 EndpointSlice
kubectl get endpoints users
# users 10.0.1.5:8080,10.0.1.6:8080,10.0.1.7:8080
Service가 가리키는 Pod들의 IP 목록. Pod 추가/제거 시 자동 업데이트.
EndpointSlice (K8s 1.21+):
- 큰 클러스터 (수천 endpoint)에서 더 효율적
- API 부하 감소
5.5 Service Mesh와 통합
Istio, Linkerd 같은 service mesh:
- K8s service discovery 위에 추가 기능
- mTLS
- 트래픽 분할 (canary)
- 분산 트레이싱
- 관찰성
# Istio VirtualService
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: users
spec:
hosts:
- users
http:
- route:
- destination:
host: users
subset: v1
weight: 90
- destination:
host: users
subset: v2
weight: 10 # 10% 카나리
6. 분산 환경의 일관성
6.1 CAP 정리 적용
Eureka (AP):
- 가용성 우선
- 일시적으로 stale data 허용
- "최선의 노력" 디스커버리
Consul (CP):
- 일관성 우선
- 네트워크 분할 시 일부 노드 거부
- Raft 합의
선택:
- AP: 디스커버리 가용성이 우선 (대부분의 경우)
- CP: 강한 일관성 필요 (드문 경우)
6.2 Eventually Consistent 디스커버리
대부분의 시스템:
- 새 인스턴스 등록 → 다른 클라이언트가 알기까지 약간의 지연 (수 초)
- 죽은 인스턴스 제거 → 동일
현실:
- 100% 즉시 알림은 불가능
- 클라이언트는 retry + circuit breaker로 보호
- Eventually consistency 받아들임
6.3 Split Brain 방지
네트워크 분할 시 두 그룹이 자기가 master라 생각.
해결: Quorum.
3노드 클러스터:
- 1+1 분할: 어느 쪽도 quorum 못 만듦
- 2+1 분할: 2 쪽이 quorum, 1 쪽은 거부
5노드 클러스터:
- 2+3 분할: 3 쪽이 quorum
→ 홀수 개 노드 권장.
7. 보안
7.1 mTLS
서비스 간 통신 암호화 + 신원 검증.
[Client] ←─ mTLS ─→ [Server]
(둘 다 인증서 제시)
Service Mesh가 제공:
- Istio: 자동 mTLS
- Linkerd: 자동 mTLS
- Consul Connect: 자동 mTLS
효과:
- 도청 방지
- MITM 공격 방지
- Zero-trust 네트워크
7.2 ACL (Access Control)
Consul ACL:
service "users" {
policy = "read"
}
서비스별 권한 정의. 어떤 클라이언트가 어떤 서비스를 호출 가능?
7.3 Service Identity
SPIFFE/SPIRE: 표준 service identity 프레임워크.
spiffe://example.com/services/users
각 서비스에 unique ID. 인증서에 포함.
8. 멀티 클러스터 / 멀티 리전
8.1 왜 필요한가?
- 글로벌 분산: 사용자에 가까이
- 재해 복구: 한 region 다운 시
- 격리: 환경별 (dev/staging/prod)
8.2 Consul Mesh Gateway
[DC1] [DC2]
[Client] ─→ [MGW] ─→ [MGW] ─→ [Server]
DC 간 트래픽이 mesh gateway를 통해. 보안 + 라우팅.
8.3 Istio Multi-Cluster
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
values:
global:
multiCluster:
clusterName: cluster-1
network: network-1
모드:
- Replicated control plane: 각 클러스터에 Istio
- Single control plane: 한 곳에서 모두 관리
8.4 Submariner
K8s 클러스터 간 네트워크 연결. 다른 service mesh와 결합.
9. 실전 패턴
9.1 Sidecar 패턴
Service mesh가 사용. 각 service 옆에 proxy:
[App] ←─→ [Envoy Proxy] ←─→ [Network]
(sidecar)
장점:
- 앱은 모름
- 모든 언어 지원
- 일관된 정책
단점:
- 추가 latency (~1ms)
- 리소스 사용
9.2 DNS 캐싱 문제
# 첫 호출
ip = dns.resolve("users") # 10.0.1.5
# IP 캐시
# 이후 user-service Pod 재시작 → 새 IP (10.0.1.6)
# 하지만 캐시는 옛 IP 그대로
# → 연결 실패
해결:
- TTL 짧게 (K8s DNS는 5초)
- HTTP 클라이언트가 매번 DNS 해결
- Service Mesh 사용 (Envoy가 자동)
9.3 Graceful Shutdown
잘못된 종료:
[Pod 종료 신호]
↓
[즉시 종료]
↓
[진행 중 요청 손실]
Graceful:
[Pod 종료 신호]
↓
[Readiness fail] → 새 트래픽 차단
↓
[진행 중 요청 완료 대기]
↓
[종료]
import signal
def handle_term(signum, frame):
app.shutdown_started = True
# Readiness가 503 반환
# 진행 중 요청 완료
sys.exit(0)
signal.signal(signal.SIGTERM, handle_term)
K8s에서 terminationGracePeriodSeconds 설정.
9.4 Connection Draining
로드 밸런서가 인스턴스 제거 시:
- 새 연결 차단
- 기존 연결 완료 대기 (보통 30초)
- 강제 종료
10. 도구 선택 가이드
10.1 의사결정 트리
Kubernetes 사용 중?
├─ YES
│ ├─ 단순 (basic discovery만 필요)
│ │ → K8s DNS (기본)
│ └─ Service Mesh 필요 (mTLS, traffic, observability)
│ → Istio, Linkerd
└─ NO
├─ HashiCorp 스택?
│ → Consul
├─ Java + Spring?
│ → Eureka (legacy) 또는 Consul
└─ 일반 솔루션
→ Consul
10.2 Consul vs K8s DNS vs Istio
Consul:
- ✅ K8s 외부 환경
- ✅ 멀티 데이터센터
- ✅ KV 스토어 등 추가 기능
- ❌ K8s만 사용한다면 과도
K8s DNS:
- ✅ K8s 환경, 단순
- ✅ 추가 도구 불필요
- ❌ K8s 밖 서비스 통합 어려움
Istio:
- ✅ K8s + service mesh 필요
- ✅ mTLS, 트래픽 분할, observability
- ❌ 복잡, 러닝 커브
퀴즈
1. Client-side와 Server-side discovery의 차이는?
답: Client-side: 클라이언트가 service registry에서 인스턴스 목록을 받아 직접 선택 (Eureka + Ribbon). 장점: hop 적음, 유연. 단점: 모든 언어에 라이브러리 필요. Server-side: 클라이언트는 단일 endpoint(load balancer)에 요청 → LB가 디스커버리 + 라우팅 (K8s, AWS ALB). 장점: 언어 무관, 클라이언트 단순. 단점: 추가 hop. 현대 시스템은 server-side가 우세 (K8s 표준).
2. Liveness, Readiness, Startup probe의 차이는?
답: Liveness: "컨테이너가 살아있나?" 실패 시 → 재시작. 단순 체크 (앱 프로세스 응답). Readiness: "트래픽을 받을 준비가 됐나?" 실패 시 → 로드 밸런서에서 제외 (재시작 X). 의존성 (DB, 캐시) 포함. Startup (K8s 1.16+): "초기화 완료?" 실패 시 → liveness/readiness 미실행. 느린 시작 앱(Java)에 적합. 잘못 구성하면 cascade failure 발생 — DB 다운 → 모든 readiness fail → 모든 트래픽 차단.
3. Eureka vs Consul vs K8s DNS — 언제 어떤 것?
답: Eureka: Spring Boot/Java + AP (가용성 우선). 새 프로젝트는 비추천 (Netflix 유지보수 감소). Consul: HashiCorp 스택, 멀티 DC, K8s 외부 환경, 풍부한 기능 (KV store, mesh). K8s DNS: K8s 환경의 기본. 추가 도구 불필요, 단순. 대부분의 K8s 워크로드에 충분. 결정: K8s 안 → DNS, K8s + 고급 → Istio, K8s 외부 → Consul, Java + Spring legacy → Eureka.
4. DNS 캐싱이 service discovery에 일으키는 문제는?
답: 클라이언트가 DNS를 캐시하면 → Pod이 재시작하여 IP가 바뀌어도 옛 IP로 계속 시도 → 연결 실패. 해결: (1) TTL 짧게 (K8s DNS는 5초), (2) HTTP 클라이언트가 매번 DNS 해결 (Java의 networkaddress.cache.ttl=0), (3) Service Mesh 사용 — Envoy가 endpoint를 직접 모니터링하여 캐싱 문제 회피, (4) Headless service + 클라이언트 LB. 가장 흔한 함정 중 하나입니다.
5. mTLS가 service mesh의 핵심 기능인 이유는?
답: 마이크로서비스는 수많은 서비스 간 통신이 발생합니다. 각 통신을 평문으로 두면 (1) 도청 위험, (2) MITM 공격, (3) 신원 위조 가능. mTLS (mutual TLS): 양쪽이 서로 인증서를 교환하여 신원 검증 + 트래픽 암호화. Zero-trust 네트워크의 기반. Istio, Linkerd, Consul Connect가 자동으로 mTLS 제공 — 앱 코드 변경 없이. SPIFFE/SPIRE는 표준 service identity를 정의합니다.
참고 자료
- Microservices.io: Service Discovery
- Consul — HashiCorp
- Eureka — Netflix
- Kubernetes Service
- CoreDNS
- Istio Service Discovery
- Linkerd
- SPIFFE/SPIRE — Service identity
- Designing Data-Intensive Applications — Martin Kleppmann
- Microservice Patterns — Chris Richardson
- Cloud Native DevOps with Kubernetes