- Authors

- Name
- Youngju Kim
- @fjvbn20031
들어가며 — "서비스 메시는 해답인가, 유행인가"
2017년 전후로 "마이크로서비스를 하려면 Service Mesh가 필수"라는 분위기가 있었다. 그러다 2021년쯤부터 "사이드카 하나당 메모리 100MB? 이거 비효율이지 않나?"라는 목소리가 나왔고, 2023~2024년엔 Istio Ambient Mesh와 Cilium의 Sidecar-less 메시가 본격 등장했다.
8년 동안 무슨 일이 일어났는가. 이 글에서는:
- 왜 Service Mesh가 필요했는가 — 마이크로서비스가 낳은 문제들
- Envoy가 어떻게 동작하는가 — listener, filter, cluster, xDS
- Istio의 컨트롤 플레인 — istiod가 하는 일
- mTLS가 자동으로 되는 원리
- Ambient Mesh — Sidecar 없는 Istio
- Cilium eBPF의 Mesh — 커널에서 처리하는 L7
- Linkerd의 Rust 사이드카 — 간결함의 철학
- 현장의 선택 기준 — 언제 뭘 써야 하나
이전 글 Kubernetes 내부 구조에서 Pod 네트워킹의 기본을 봤다면, 이번엔 그 위에서 Pod끼리 어떻게 안전하고 관측 가능하게 대화하는가의 이야기다.
1. 왜 Service Mesh가 태어났나
마이크로서비스의 숨겨진 비용
모놀리스를 100개 마이크로서비스로 쪼개면 생기는 문제들:
- 재시도(retry) — A가 B를 호출. B가 500 에러. 재시도? 얼마나? 지수 백오프?
- 타임아웃 — B가 느리면 A가 얼마나 기다려야 하나?
- 회로 차단(circuit breaker) — B가 계속 실패하면 A는 시도를 멈추고 빠르게 실패해야 한다
- 로드 밸런싱 — B가 10개 인스턴스일 때 어떻게 고르나? 라운드 로빈? Least request?
- 트래픽 분할 — B의 새 버전을 10% 트래픽만 받게 하려면?
- mTLS — A와 B 사이가 인증·암호화돼야 한다
- 관측성 — A→B 호출 하나하나에 trace ID, 지연 히스토그램
- 인증/인가 — 어떤 서비스가 어떤 서비스를 호출할 수 있나?
이걸 **각 언어의 라이브러리(Netflix Hystrix, Ribbon, Eureka)**로 해결했던 시대가 2015~2017년이었다. 하지만 Java 외에 Go, Python, Node까지 쓰게 되면 같은 기능을 각 언어로 구현해야 했다. 그리고 라이브러리를 업그레이드하려면 모든 서비스를 다시 배포해야 했다.
해답: 네트워크 프록시로 외주
"어차피 A가 B에게 보내는 건 HTTP/gRPC 호출이다. 이걸 프록시가 중간에 가로채면 언어와 무관하게 기능을 추가할 수 있지 않을까?" 이게 Service Mesh의 탄생 논리다.
Lyft가 2016년 Envoy를 오픈소스화하고, Google과 IBM이 2017년 Envoy를 데이터 플레인으로 하는 Istio를 만들면서 Service Mesh 시대가 열렸다.
2. Envoy — 모던 네트워크 프록시의 표준
왜 Envoy인가
Envoy가 선택된 이유:
- C++로 작성 — nginx처럼 빠르지만 더 현대적인 설계
- HTTP/2와 gRPC 네이티브 — 기본 가정이 HTTP/2
- 동적 설정 — 재시작 없이 설정 변경 (xDS)
- 강력한 관측성 — 통계, 로그, trace가 기본
- 필터 체인 — WASM과 Lua로 확장 가능
Envoy의 4대 개념
┌─────────────────────────────────────────────────┐
│ Envoy │
│ │
│ ┌─────────┐ ┌──────────────┐ ┌────────┐ │
│ │Listener │ -> │ Filter Chain │ -> │ Cluster│ │
│ └─────────┘ └──────────────┘ └────────┘ │
│ (수신 포트) (HTTP/TLS/필터) (업스트림) │
└─────────────────────────────────────────────────┘
- Listener — 어떤 포트에서 듣는가 (L4)
- Filter Chain — 그 포트로 들어온 연결에 어떤 처리를 하는가
- Cluster — 어디로 보낼 것인가 (업스트림 서비스의 IP:Port 목록)
- Endpoint — Cluster 안의 실제 IP 주소들
HTTP 요청 하나의 경로
클라이언트 → Listener(포트 15001) → TLS 필터 → HTTP Connection Manager → 라우팅 → 클러스터 선택 → 로드밸런서 → Endpoint → TLS로 암호화 → 실제 서비스
각 단계가 필터로 되어 있어, 필터를 추가해서 WAF, Rate Limit, JWT 검증 등을 껴넣을 수 있다.
xDS API — 동적 설정의 핵심
Envoy는 다음 5개의 디스커버리 서비스로 설정을 받는다:
- LDS (Listener Discovery Service)
- RDS (Route Discovery Service)
- CDS (Cluster Discovery Service)
- EDS (Endpoint Discovery Service)
- SDS (Secret Discovery Service — 인증서)
이들을 묶은 게 ADS (Aggregated Discovery Service). gRPC 스트림으로 Envoy가 컨트롤 플레인에 연결하면, 컨트롤 플레인이 설정 변경을 push한다.
Istiod (컨트롤 플레인)
│
│ gRPC stream (ADS)
▼
Envoy 사이드카 (데이터 플레인)
설정이 변하면 istiod가 새 config를 스트림으로 보내고, Envoy는 무중단으로 반영한다. 이게 Kubernetes Service나 Deployment가 바뀌면 즉시 반영되는 원리다.
3. Istio — 가장 널리 쓰이는 Service Mesh
Istio 1.0 시대의 복잡성
2018년 Istio 1.0은 컴포넌트가 4개였다:
- Pilot — xDS 서버
- Mixer — 정책 평가 + 텔레메트리 수집
- Citadel — 인증서 발급
- Galley — 설정 검증
Mixer가 모든 요청마다 별도 gRPC 호출을 받았고, 성능 병목이 됐다. 1.5에서 Mixer가 제거되고 모든 기능이 Envoy의 WASM 필터로 들어가면서 성능이 크게 개선됐다.
현재 Istio: istiod 하나
┌──────────────────────────────────┐
│ istiod │
│ │
│ Pilot (xDS) + Citadel + Galley │
│ 모두 한 바이너리 │
└─────────────┬────────────────────┘
│ xDS
▼
┌──────────────────────────────────┐
│ Envoy 사이드카 │
│ (각 Pod마다 하나씩) │
└──────────────────────────────────┘
Sidecar 주입의 원리
Pod에 Envoy를 어떻게 넣나? Mutating Admission Webhook이다.
- 사용자가
istio-injection=enabled라벨 붙은 namespace에 Pod 생성 - API Server가 Pod 생성 요청을 받으면 Istio의 Mutating Webhook 호출
- Webhook이 Pod spec에 **사이드카 컨테이너
istio-proxy**를 주입 - 동시에 **initContainer
istio-init**을 추가해서 iptables 룰 삽입
초기 컨테이너의 iptables 룰:
모든 아웃바운드 트래픽 → REDIRECT to 127.0.0.1:15001 (Envoy)
모든 인바운드 트래픽 → REDIRECT to 127.0.0.1:15006 (Envoy)
즉 Pod 안의 애플리케이션은 자기가 직접 외부와 통신한다고 생각하지만, iptables가 Envoy로 가로챈다. 이 투명한 가로채기가 Sidecar의 핵심이다.
VirtualService와 DestinationRule
Istio는 Kubernetes의 Service로는 부족한 고급 트래픽 제어를 두 CRD로 제공한다.
VirtualService — 라우팅 규칙:
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: reviews
spec:
hosts: [reviews]
http:
- match:
- headers:
user-agent: { regex: ".*Mobile.*" }
route:
- destination: { host: reviews, subset: v2 }
- route:
- destination: { host: reviews, subset: v1 }
weight: 90
- destination: { host: reviews, subset: v2 }
weight: 10
→ "모바일 UA는 v2로, 나머지는 v1 90% / v2 10%"
DestinationRule — 클러스터 설정(서브셋, 회로 차단):
kind: DestinationRule
spec:
host: reviews
trafficPolicy:
connectionPool:
tcp: { maxConnections: 100 }
http: { http1MaxPendingRequests: 10 }
outlierDetection:
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 30s
subsets:
- name: v1
labels: { version: v1 }
- name: v2
labels: { version: v2 }
→ "5번 연속 5xx 에러 난 Endpoint는 30초간 제외 (outlier detection)"
4. mTLS — "모든 통신이 자동 암호화"의 원리
문제
서비스 A가 서비스 B를 호출할 때, 네트워크가 암호화되지 않으면:
- 같은 클러스터 내부라도 악의적 Pod가 스니핑
- 인증 없으면 B가 "이게 정말 A인가?"를 알 수 없음
수동으로 TLS를 붙이려면 모든 서비스에 인증서 발급·갱신·검증 로직을 넣어야 한다. Istio는 이걸 자동화한다.
인증서 생성 흐름
- istiod가 자체 CA(Certificate Authority) 역할. 루트 키를 가짐
- 각 Envoy 사이드카는 시작 시 istiod에 CSR(Certificate Signing Request) 보냄
- CSR에 해당 Pod의 Service Account 정보가 담김
- istiod가 검증 후 서명된 인증서 반환 (보통 24시간 유효)
- Envoy는 SDS로 주기적으로 재발급
인증서의 SAN(Subject Alternative Name)에는 다음과 같은 SPIFFE ID가 들어간다:
spiffe://cluster.local/ns/default/sa/reviews
핸드셰이크와 검증
A의 Envoy가 B의 Envoy에게 연결:
- TLS ClientHello
- B의 Envoy가 자기 인증서 제시
- A의 Envoy가 istiod의 루트 CA로 검증
- B의 Envoy도 A에게 인증서 요구(mTLS의 "m"은 mutual)
- 양쪽 모두 SPIFFE ID로 **"이 연결의 반대편은 ns=default, sa=reviews"**라는 인증된 ID를 얻음
이 ID를 바탕으로 AuthorizationPolicy가 "이 SA에서 이 SA로는 허용"을 강제한다.
STRICT vs PERMISSIVE
kind: PeerAuthentication
spec:
mtls:
mode: STRICT # mTLS 아니면 거부
# mode: PERMISSIVE # 평문도 허용 (마이그레이션용)
# mode: DISABLE # mTLS 끄기
프로덕션은 STRICT가 목표. 하지만 마이그레이션 시에는 PERMISSIVE로 "기존 서비스 망가뜨리지 않고 점진 전환"을 한다.
5. Sidecar의 비용 — 왜 사람들이 싫어하기 시작했나
비용 1: 메모리
Envoy 하나당 50~150MB 메모리. Pod 1000개면 사이드카만 100GB. 작은 앱일수록 비율이 커진다 — "100MB 앱에 150MB 사이드카"라는 비효율.
비용 2: 시작 지연
initContainer → 사이드카 부팅 → xDS 설정 수신 → 애플리케이션 준비. 2~3초의 추가 지연. Job이나 CronJob에서는 특히 문제.
비용 3: 라이프사이클 불일치
애플리케이션 컨테이너는 종료됐는데 사이드카는 살아있음. 혹은 반대. preStop hook으로 순서를 맞춰야 하는데 복잡함.
1.28 쿠버네티스에서 **Sidecar Container (native)**가 Beta로 들어가면서 해결되는 중이지만, 메모리 문제는 여전하다.
비용 4: 두 번의 hop
A 앱 → A의 Envoy → B의 Envoy → B 앱. 매 요청마다 4번의 L7 파싱(HTTP 헤더 파싱). 지연이 추가된다.
비용 5: 디버깅의 지옥
503이 떴다. A의 앱인가, A의 Envoy인가, 네트워크인가, B의 Envoy인가, B의 앱인가? 로그 다섯 군데를 봐야 한다.
6. Ambient Mesh — Istio의 Sidecar-less 혁명
2022년 Istio 팀이 발표한 Ambient Mesh. 2024년 Beta, 2025년 기준 GA.
핵심 아이디어: L4와 L7 분리
Sidecar에 밀어 넣었던 기능을 두 레이어로 쪼갠다.
Layer 1: Ztunnel (per-node L4 proxy)
- Node마다 하나의 Rust 기반 프록시
- mTLS, 인증, L4 라우팅만 담당
- Pod들은 Node의 Ztunnel로 트래픽이 리다이렉트됨 (iptables/eBPF)
Layer 2: Waypoint Proxy (per-service L7 proxy)
- 필요한 서비스에만 배포하는 Envoy
- L7 라우팅, WAF, Rate Limit 등
- Deployment로 배포됨 (오토스케일링 가능)
┌──────────────────────────────┐
│ Node │
│ │
│ [App Pod] [App Pod] │
│ │ │ │
│ ▼ ▼ │
│ [Ztunnel] (L4 mTLS) │
│ │ │
└──────┼───────────────────────┘
│
▼ (L7 필요한 경우만)
[Waypoint Proxy Pod]
│
▼
[목적지 서비스]
장점
- 메모리 절약 — 사이드카가 없어지고 Ztunnel 하나로 압축
- 점진 적용 — 단순한 서비스는 L4만, 복잡한 건 Waypoint 추가
- 라이프사이클 분리 — 앱과 프록시가 독립
단점 / 우려
- 성숙도 — Beta 기간이 길었고, 아직 엣지 케이스가 있다
- Waypoint 추가 홉 — L7 정책 걸린 서비스는 결국 4 hop 유사
- 디버깅 — 새로운 모델이라 도구가 덜 성숙
7. Cilium Service Mesh — eBPF로 커널에서
eBPF란 무엇인가 (1분 복습)
eBPF (extended Berkeley Packet Filter)는 Linux 커널 안에서 안전한 가상 머신이 바이트코드를 실행하게 해주는 기술. 원래 패킷 필터링용이었으나 지금은:
- 네트워크 훅 (XDP, TC)
- 시스템 콜 추적
- 성능 프로파일링
- 보안 (seccomp 대체)
장점: 커널 모듈 없이 커널을 확장. 안전하게. 재컴파일·재부팅 없이.
Cilium의 베팅: 프록시를 커널로
Cilium은 "사이드카 프록시를 커널 eBPF 프로그램으로 대체"한다. 결과:
- Pod에서 나온 패킷이 userspace 프록시를 거치지 않고 커널 eBPF에서 라우팅됨
- kube-proxy도 대체 (iptables 없음)
- L7 라우팅은 여전히 Envoy를 쓰지만 Node에 하나만 배치
┌────────────────────────────────┐
│ Node │
│ │
│ [App Pod] --- (eBPF hook) │
│ │ │
│ ▼ │
│ Kernel eBPF Program │
│ (L3/L4 policy, LB) │
│ │ │
│ ▼ │
│ [Envoy (per-node, L7 only)] │
└────────────────────────────────┘
성능
Cilium 벤치마크(자체 발표)에 따르면:
- 사이드카 대비 P99 지연 2~3배 개선
- CPU 사용량 40% 이상 감소
- 커넥션당 메모리 오버헤드 거의 0
기능
- L3-L7 NetworkPolicy — HTTP 메서드/경로까지 검사
- Hubble — 관측성 (flow logs, service map)
- Tetragon — 런타임 보안 (프로세스 실행, 파일 접근 감시)
8. Linkerd — 간결함의 철학
Ultralight 접근
Linkerd는 2.0(2018)부터 **Rust로 작성된 경량 프록시 linkerd2-proxy**를 쓴다.
- Envoy가 아닌 자체 프록시
- 메모리: 10~20MB (Envoy의 1/10 수준)
- 기능은 적지만 핵심 5가지만 단단히:
- mTLS
- 재시도·타임아웃
- 메트릭
- 로드 밸런싱 (EWMA — 지연 가중 평균)
- 서비스 프로파일
철학
"Service Mesh에 너무 많은 걸 넣으면 운영 부담이 된다." 그래서 일부러 WASM, 동적 config reload, 복잡한 CRD를 줄였다.
대상
- 마이크로서비스가 100개 미만
- Istio의 복잡도가 부담
- "그냥 mTLS와 재시도만 자동으로 됐으면"
9. 선택 가이드 — 우리 팀은 뭘 써야 하나
의사결정 트리
Service Mesh가 필요한가?
├── 서비스 10개 미만 → 필요 없음. 라이브러리로 해결
├── 모든 서비스가 같은 언어 → 언어별 RPC 라이브러리로 충분
└── 언어 혼합 + 정책 복잡도 있음 → Service Mesh 후보
어떤 메시?
├── 기존 Istio 사용 중 → Ambient Mesh로 마이그레이션 검토
├── 대규모(1000+ 서비스) + 성능 중시 → Cilium
├── 간결함 선호 + Rust 신뢰 → Linkerd
└── 풍부한 기능 + 커뮤니티 → Istio (Sidecar 또는 Ambient)
실제 현장의 2025년 분위기
- Netflix, Uber, Airbnb — 자체 라이브러리/프록시 (내부 언어가 통제됨)
- 클라우드 네이티브 스타트업 — Cilium 급상승
- 엔터프라이즈(금융/통신) — Istio 안정성 선호
- 작은 팀 — Linkerd 또는 아예 안 씀
10. 실무 튜닝 — mTLS 이외에도 챙겨야 할 것
Connection Pool Tuning
Envoy/linkerd-proxy는 기본값이 보수적이다. 실제 트래픽에 맞게:
trafficPolicy:
connectionPool:
tcp:
maxConnections: 1000 # 서비스당
connectTimeout: 5s
http:
http1MaxPendingRequests: 1024
http2MaxRequests: 10000
maxRequestsPerConnection: 10000
Circuit Breaker
outlierDetection:
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 60s
maxEjectionPercent: 50 # 최대 50%까지만 배제
"50% 룰"이 중요. 안 그러면 모든 Endpoint가 배제돼 서비스 중단.
Retry 예산
retries:
attempts: 3
perTryTimeout: 2s
retryOn: 5xx,connect-failure,refused-stream
재시도는 부하를 증폭시킨다. 하류가 아프면 상류의 재시도가 쓰나미를 만든다. 회로 차단기와 함께 써야 한다.
11. 관측성 — 숨겨진 선물
Service Mesh의 숨은 가치는 관측성이 기본으로 붙어온다는 점이다.
메트릭 (Prometheus)
모든 요청에 대해 자동으로:
istio_requests_total— 카운터 (소스/대상/코드별)istio_request_duration_milliseconds— 히스토그램istio_tcp_sent_bytes_total/received_bytes_total
이걸 Grafana 대시보드로 보면 서비스 맵이 자동으로 그려진다.
분산 트레이싱 (Jaeger/Tempo/Zipkin)
Envoy가 B3 헤더 (x-request-id, x-b3-traceid, x-b3-spanid)를 자동 전파. 애플리케이션이 헤더만 통과시키면 trace가 이어진다.
주의: 생성은 자동이지만 전파는 애플리케이션 책임. 경로 중에 헤더를 버리면 trace가 끊김.
접근 로그
Envoy의 접근 로그는 JSON 또는 커스텀 포맷으로 매 요청을 기록. Loki/Elasticsearch에 보내면 "5xx 요청만 모아 보기" 같은 쿼리 가능.
12. 함정과 안티패턴
함정 1: mTLS ON/OFF 혼재
일부 서비스가 STRICT, 일부 PERMISSIVE면 디버깅 악몽. 정책 단위를 namespace로 통일하고 마이그레이션 때만 예외.
함정 2: Ingress Gateway를 Sidecar로 착각
Istio Gateway는 별도 Pod로 배치되는 외부 엔트리포인트. Sidecar와는 라이프사이클과 튜닝이 다르다. 메모리/CPU를 별도로 튜닝해야 한다.
함정 3: 모든 서비스에 Istio 주입
Redis, Kafka 같은 stateful 서비스에 사이드카를 넣으면:
- TCP 기반이라 L7 기능이 무의미
- 커넥션 재사용 튜닝이 꼬임
- 성능 저하 가능
→ Label로 제외하거나 namespace에서 빼라.
함정 4: 컨트롤 플레인 실수로 전체 장애
istiod에 잘못된 VirtualService를 push하면 모든 Envoy가 잘못된 설정을 받음. 카나리 컨트롤 플레인 (revision-based canary)으로 점진 배포 권장.
함정 5: WASM 필터의 비용
EnvoyFilter로 WASM 필터 붙이면 기능은 강력하지만 레이턴시 +수 ms. 프로덕션 시뮬레이션 필수.
13. 미래 — Service Mesh는 사라지나?
여러 예측이 있다:
- eBPF가 사이드카를 대체 — Cilium의 방향. 이미 현실
- 플랫폼 엔지니어링의 한 축으로 흡수 — 사용자는 메시를 모르고도 트래픽 정책을 얻음
- gRPC client-side LB 복귀 — xDS를 애플리케이션이 직접 받아 프록시 없이 로드 밸런싱 (Google 내부 방식)
- Gateway API + 표준화 — 메시별 CRD가 통합되는 중 (GAMMA 이니셔티브)
공통점: **"사이드카 프록시 패턴은 지난 5년의 과도기적 설계였을 수 있다"**는 인식.
하지만 Service Mesh가 해결한 문제 — 언어 중립 정책, mTLS 자동화, 관측성 — 은 사라지지 않는다. 구현 방식만 진화할 뿐이다.
14. 실무 체크리스트 12가지
- 설치 전에 꼭 필요한지 5번 자문 — 복잡도 비용이 크다
- Sidecar vs Ambient 결정 — 새 도입이면 Ambient 권장
- CNI와의 호환성 확인 — Cilium을 이미 쓰면 Cilium Mesh 검토
- mTLS는 PERMISSIVE로 시작 → STRICT 전환 — 한 번에 STRICT 가지 말기
- 리소스 requests/limits 반드시 설정 — 사이드카 OOM이 서비스를 죽인다
- Telemetry v2 활성화 — 메트릭 성능 저하 없음
- Envoy 버전과 Istio 버전 일치 — 미스매치 시 설정 실패
istioctl analyze로 설정 검증 — CI에 넣어라- Gateway는 별도 NodePool — 워크로드와 분리
- Access log sampling — 100%는 로그 저장 비용 폭탄
- Retry + Circuit Breaker 세트로 설정 — 재시도만 있으면 증폭
- 업그레이드는 리비전 기반 카나리 — 데이터 플레인을 한번에 재시작 X
다음 글 예고 — OpenTelemetry로 관측성의 끝을 보자
Service Mesh가 자동으로 메트릭과 trace를 뿌려주지만, 실제 운영에서는 애플리케이션 코드와 인프라의 trace가 하나로 연결돼야 한다. 다음 글에서는:
- OpenTelemetry의 탄생 — OpenTracing + OpenCensus의 통합
- Span, Trace, Context Propagation의 정확한 의미
- Collector 아키텍처 — Receiver, Processor, Exporter
- Sampling 전략 — Head vs Tail
- Logs + Metrics + Traces의 세 기둥을 하나로
- Pyroscope / Parca로 프로파일(네 번째 기둥) 통합
- eBPF auto-instrumentation — 코드 수정 없이 관측성
- OTLP 프로토콜 내부
Jaeger의 sparse trace를 봤지만 왜 이게 저장되는지를 설명하기 어려웠던 분, 수백만 req/s에서 trace를 어떻게 샘플링하는지 궁금한 분이라면 다음 글이 재미있을 것이다.
"관측성은 로그 수집이 아니다. 분산 시스템이 스스로 자기 상태를 설명할 수 있게 만드는 설계 철학이다."