Split View: Service Mesh 완전 해부 — Envoy, Istio, Linkerd, Cilium eBPF, Ambient Mesh, xDS/mTLS까지
Service Mesh 완전 해부 — Envoy, Istio, Linkerd, Cilium eBPF, Ambient Mesh, xDS/mTLS까지
들어가며 — "서비스 메시는 해답인가, 유행인가"
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를 어떻게 샘플링하는지 궁금한 분이라면 다음 글이 재미있을 것이다.
"관측성은 로그 수집이 아니다. 분산 시스템이 스스로 자기 상태를 설명할 수 있게 만드는 설계 철학이다."
Service Mesh Deep Dive — Envoy, Istio, Linkerd, Cilium eBPF, Ambient Mesh, xDS/mTLS
Intro — "Answer or Fad?"
Around 2017 the consensus was "microservices require a Service Mesh." By 2021 people asked "100MB per sidecar — is this really efficient?" By 2023-2024, Istio Ambient Mesh and Cilium's sidecar-less mesh arrived in force.
What happened in eight years? This post covers:
- Why a Service Mesh was needed — pains born of microservices
- How Envoy works — listener, filter, cluster, xDS
- Istio's control plane — what istiod actually does
- How mTLS becomes automatic
- Ambient Mesh — Istio without sidecars
- Cilium eBPF — L7 handled in the kernel
- Linkerd's Rust sidecar — the minimalist school
- Selection criteria in the field
1. Why Service Meshes Were Born
Splitting a monolith into 100 microservices creates:
- Retries — how many, with what backoff?
- Timeouts — how long to wait?
- Circuit breaking — fail fast when downstream keeps erroring
- Load balancing — round robin? Least request?
- Traffic splitting — route 10% to the new version
- mTLS — encrypt and authenticate between services
- Observability — trace IDs, latency histograms per call
- AuthN/AuthZ — which service can call which?
From 2015-2017 people solved this with per-language libraries (Netflix Hystrix, Ribbon, Eureka). But once Go, Python, Node arrived, every language needed its own implementation, and upgrades required redeploying every service.
The Answer: Outsource to a Network Proxy
If A calls B over HTTP/gRPC, a proxy in the middle can add features regardless of language. Lyft open-sourced Envoy in 2016, Google and IBM built Istio on top in 2017, and the Service Mesh era began.
2. Envoy — The Modern Network Proxy Standard
Why Envoy
- Written in C++ — fast like nginx, but modern design
- Native HTTP/2 and gRPC
- Dynamic config (xDS) — no restart needed
- Strong observability — stats, logs, traces built-in
- Filter chains — extensible via WASM and Lua
The Four Core Concepts
┌─────────────────────────────────────────────────┐
│ Envoy │
│ │
│ ┌─────────┐ ┌──────────────┐ ┌────────┐ │
│ │Listener │ -> │ Filter Chain │ -> │ Cluster│ │
│ └─────────┘ └──────────────┘ └────────┘ │
└─────────────────────────────────────────────────┘
- Listener — which port to listen on (L4)
- Filter Chain — how to process incoming connections
- Cluster — where to send traffic (upstream IP:Port list)
- Endpoint — actual IPs within a Cluster
Filters are pluggable, so WAF, Rate Limit, and JWT validation can be inserted.
xDS API — The Heart of Dynamic Config
Envoy receives config from five discovery services:
- LDS (Listener), RDS (Route), CDS (Cluster), EDS (Endpoint), SDS (Secret)
These are aggregated into ADS (Aggregated Discovery Service). Envoy connects to the control plane via a gRPC stream, and the control plane pushes config changes. Updates apply without downtime — this is how changes to a Kubernetes Service or Deployment propagate instantly.
3. Istio — The Most Widely Used Mesh
Istio 1.0's Complexity
Istio 1.0 (2018) had four components: Pilot, Mixer, Citadel, Galley. Mixer received a separate gRPC call per request and became a bottleneck. In 1.5, Mixer was removed and all functionality moved into Envoy WASM filters.
Today's Istio: Just istiod
┌──────────────────────────────────┐
│ istiod │
│ Pilot (xDS) + Citadel + Galley │
└─────────────┬────────────────────┘
│ xDS
▼
Envoy Sidecar
(one per Pod)
How Sidecar Injection Works
A Mutating Admission Webhook.
- User creates a Pod in a namespace labeled
istio-injection=enabled - API Server calls Istio's Mutating Webhook
- Webhook injects the
istio-proxysidecar container - An
istio-initinitContainer is also added to insert iptables rules
The iptables rules:
All outbound traffic → REDIRECT to 127.0.0.1:15001 (Envoy)
All inbound traffic → REDIRECT to 127.0.0.1:15006 (Envoy)
The app thinks it talks directly to the network, but iptables transparently intercepts into Envoy.
VirtualService and DestinationRule
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
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 }
4. mTLS — How "Automatic Encryption" Actually Works
Certificate Flow
- istiod acts as its own CA with a root key
- Each Envoy sidecar sends a CSR to istiod on startup
- The CSR carries the Pod's Service Account identity
- istiod validates and returns a signed cert (typically 24h validity)
- Envoy re-rotates via SDS
The cert SAN contains a SPIFFE ID:
spiffe://cluster.local/ns/default/sa/reviews
Handshake
Envoy A connects to Envoy B: TLS ClientHello, B presents its cert, A validates against istiod's root CA. B also demands A's cert (the "m" in mTLS). Both sides get an authenticated SPIFFE identity, which AuthorizationPolicy uses to enforce "SA X may call SA Y."
STRICT vs PERMISSIVE
kind: PeerAuthentication
spec:
mtls:
mode: STRICT
Production target is STRICT. During migration, PERMISSIVE allows both plaintext and mTLS.
5. The Cost of Sidecars
Memory
50-150MB per Envoy. 1000 Pods = 100GB just in sidecars. Tiny apps suffer the most — "150MB sidecar for a 100MB app."
Startup Latency
initContainer, sidecar boot, xDS config fetch, app ready. 2-3s extra. Painful for Jobs and CronJobs.
Lifecycle Mismatch
App container exits but sidecar still runs, or vice versa. Kubernetes 1.28 introduced native Sidecar Containers (Beta), which helps, but the memory issue remains.
Double Hop
App A -> Envoy A -> Envoy B -> App B. Four L7 parses per request.
Debugging Pain
A 503 appeared. App A? Envoy A? Network? Envoy B? App B? Five places to check.
6. Ambient Mesh — Istio's Sidecar-less Revolution
Announced 2022, Beta 2024, GA as of 2025.
Core Idea: Split L4 and L7
Ztunnel (per-node L4 proxy) — one Rust proxy per Node; handles mTLS, AuthN, and L4 routing. Pod traffic is redirected via iptables/eBPF.
Waypoint Proxy (per-service L7 proxy) — deployed only for services that need L7 features; a regular Deployment that can autoscale.
┌──────────────────────────────┐
│ Node │
│ [App Pod] [App Pod] │
│ │ │ │
│ ▼ ▼ │
│ [Ztunnel] (L4 mTLS) │
└──────┼───────────────────────┘
│
▼ (only when L7 needed)
[Waypoint Proxy Pod]
▼
[Destination Service]
Pros and Cons
Pros: less memory, gradual adoption (L4 only or add Waypoint), separated lifecycle.
Cons: younger, extra hop when Waypoint is used, tooling less mature.
7. Cilium Service Mesh — eBPF in the Kernel
eBPF in One Minute
eBPF is a Linux kernel virtual machine that safely runs bytecode at kernel hooks — networking (XDP, TC), syscall tracing, profiling, security. Extends the kernel without modules or reboots.
Cilium's Bet: Move the Proxy into the Kernel
Packets from a Pod are routed in-kernel without passing through a userspace proxy. kube-proxy is replaced (no iptables). L7 still uses Envoy, but only one per Node.
┌────────────────────────────────┐
│ Node │
│ [App Pod] --- (eBPF hook) │
│ │ │
│ ▼ │
│ Kernel eBPF Program │
│ (L3/L4 policy, LB) │
│ │ │
│ ▼ │
│ [Envoy (per-node, L7 only)] │
└────────────────────────────────┘
Performance (per Cilium benchmarks)
- P99 latency 2-3x better vs sidecars
- CPU usage reduced 40%+
- Near-zero memory overhead per connection
Features
- L3-L7 NetworkPolicy (HTTP method/path)
- Hubble — flow logs, service map
- Tetragon — runtime security
8. Linkerd — The Minimalist Philosophy
Linkerd 2.0 (2018) uses a Rust proxy, linkerd2-proxy.
- Not Envoy — a custom proxy
- Memory: 10-20MB (roughly 1/10 of Envoy)
- Five solid features: mTLS, retry/timeout, metrics, load balancing (EWMA), service profiles
Philosophy: "Don't bloat the mesh — less ops burden." No WASM, no fancy CRDs.
Target: under ~100 microservices, Istio feels heavy, "I just want mTLS and retries for free."
9. Selection Guide
Do you need a Service Mesh?
├── < 10 services → No. Libraries are fine.
├── Single language → Use a language-native RPC lib.
└── Mixed langs + policy complexity → candidate.
Which mesh?
├── Already on Istio → consider Ambient migration
├── Large (1000+) + perf → Cilium
├── Minimalism + Rust trust → Linkerd
└── Rich features + community → Istio (Sidecar or Ambient)
2025 Landscape
- Netflix, Uber, Airbnb — own libraries/proxies (controlled language surface)
- Cloud-native startups — Cilium rising fast
- Enterprises (finance/telecom) — Istio for stability
- Small teams — Linkerd or nothing
10. Practical Tuning
Connection Pool
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
The 50% rule matters — otherwise all endpoints get ejected and the service dies.
Retry Budget
retries:
attempts: 3
perTryTimeout: 2s
retryOn: 5xx,connect-failure,refused-stream
Retries amplify load. Combine with circuit breakers.
11. Observability — The Hidden Gift
Service Mesh automatically provides:
istio_requests_total— counter by source/destination/codeistio_request_duration_milliseconds— histogramistio_tcp_sent_bytes_total/received_bytes_total
Plus B3 headers (x-request-id, x-b3-traceid, x-b3-spanid) for distributed tracing. The mesh emits, but the app must propagate headers or the trace breaks.
Access logs: Envoy writes JSON/custom format per request — Loki or Elasticsearch gives you "show me all 5xx" queries.
12. Pitfalls and Anti-Patterns
- Mixed mTLS modes — STRICT and PERMISSIVE mixed is debugging hell. Unify by namespace.
- Confusing Ingress Gateway with Sidecar — different lifecycle and tuning.
- Injecting into all services — Redis/Kafka sidecars hurt more than they help (L7 features meaningless for TCP, connection reuse broken).
- Control plane fat-fingers — a bad VirtualService breaks every Envoy. Use revision-based canary control planes.
- WASM filter cost —
EnvoyFilterwith WASM adds several ms latency. Simulate in prod-like load.
13. The Future — Will Service Mesh Disappear?
Predictions:
- eBPF supplants sidecars — Cilium's direction, already reality
- Absorbed into platform engineering — users get policies without knowing about "mesh"
- gRPC client-side LB returns — apps consume xDS directly (Google's internal way)
- Gateway API standardization — GAMMA initiative unifies mesh CRDs
The common theme: "the sidecar pattern was a five-year transitional design." But the problems it solved — language-neutral policy, automatic mTLS, observability — aren't going away. Only the implementation evolves.
14. 12-Point Field Checklist
- Ask five times if you really need a mesh
- New adoption? Pick Ambient over Sidecar
- Check CNI compatibility — already on Cilium? Consider Cilium Mesh
- mTLS: start PERMISSIVE, graduate to STRICT
- Set resource requests/limits — sidecar OOM kills the service
- Enable Telemetry v2 — no perf regression
- Match Envoy and Istio versions
- Run
istioctl analyzein CI - Gateway on a separate NodePool
- Sample access logs — 100% is a log-cost bomb
- Configure retries + circuit breaker as a pair
- Upgrade via revision-based canary — no big-bang data plane restart
Next — OpenTelemetry: Closing the Observability Loop
Service Mesh sprays metrics and traces automatically, but in real operations, application and infrastructure traces must form one chain. Next post: OpenTelemetry's birth (OpenTracing + OpenCensus), Span/Trace/Context Propagation semantics, the Collector architecture (Receiver/Processor/Exporter), head vs tail sampling, three pillars unified (logs + metrics + traces), profiles as a fourth pillar (Pyroscope/Parca), eBPF auto-instrumentation, and OTLP internals.
"Observability isn't log collection. It's the design philosophy that lets a distributed system explain its own state."