tcpKeepalive 필드 노트 — time / interval / probes는 각각 무엇을 제어하는가
DR connectionPool.tcp.tcpKeepalive의 세 필드는 Envoy가 만든 개념이 아니라 리눅스 커널의
TCP keepalive 소켓 옵션 3개에 1:1 매핑된다. Envoy는 upstream 소켓에 옵션을 설정만 하고,
probe를 보내는 주체는 커널이다. 이 한 설정이 서로 다른 두 역할(중간장비 세션 유지 / 죽은 상대 감지)을
겸한다는 것, 그리고 각 역할을 결정하는 필드가 다르다는 것이 이 노트의 본체다.
선행 문서: TCP 병목 정본 §04(병목 4: 중간장비 idle timeout)·§06-1(권장값 YAML).
01. 커널 소켓 옵션 매핑
| DR 필드 | 소켓 옵션 | 의미 | 커널 기본값 |
|---|---|---|---|
time |
TCP_KEEPIDLE |
마지막 데이터 송수신 이후 이만큼 유휴하면 첫 probe 발사 | 7200s (2시간) |
interval |
TCP_KEEPINTVL |
probe에 응답이 없을 때 다음 probe까지의 재시도 간격 | 75s |
probes |
TCP_KEEPCNT |
연속 무응답 probe가 이 횟수에 도달하면 연결을 죽은 것으로 판정·폐기 | 9 |
probe의 정체는 데이터가 아니라 빈 ACK 세그먼트다(시퀀스 번호를 일부러 1 뒤로 물려 보내 상대의 ACK를 유도). 상대가 살아 있으면 즉시 ACK가 돌아오고, 그 왕복이 경로상 중간장비의 세션 타이머를 갱신한다.
02. 타임라인 — 권장값 (time=300, interval=30, probes=3)의 두 시나리오
flowchart LR
subgraph A["scenario A: peer alive"]
D1["last data"] -- "300s idle" --> P1["probe"] -- "ACK" --> K1["session refreshed, repeat every 300s"]
end
subgraph B["scenario B: peer dead (silent)"]
D2["last data"] -- "300s" --> Q1["probe 1"] -- "30s" --> Q2["probe 2"] -- "30s" --> Q3["probe 3"] -- "fail x3" --> X["closed at t=390s"]
end
역할 분리가 핵심이다:
- 중간장비 세션 유지 (병목 4의 처방) — 상대가 살아있는 정상 케이스. probe/ACK 왕복이 FW 세션 idle 타이머를 갱신한다. 결정 규칙은
time < FW idle timeout(예: 300s < 1800s)이며,interval/probes는 이 역할과 무관하다 — 살아있는 상대는 첫 probe에 즉시 ACK하므로 재시도가 발생하지 않는다. - 죽은 상대 감지 — 상대 서버가 소리 없이 사라진 경우(전원 단절, 경로 단절).
interval × probes가 감지 지연을 결정한다. 커널 기본(75s × 9 ≈ 11분)은 너무 느려서, 30s × 3 = 90초 안에 죽은 연결을 정리하도록 줄인다. 이게 없으면 죽은 연결이 Envoy 풀에 유령으로 남아maxConnections슬롯을 점유한다.
한 줄 요약: time은 “얼마나 자주 안부를 묻나”(FW 대응), interval+probes는 “무응답을 얼마나 참고 사망 선고하나”(유령 연결 정리).
03. 값 결정 규칙
| 필드 | 규칙 | 근거 |
|---|---|---|
time |
FW idle timeout의 1/3 이하 (예: FW 1800s → 300) | probe 유실·지연 1~2회를 견디고도 세션 갱신 보장 |
interval |
수십 초 (예: 30) | 짧을수록 감지 빠르지만 일시적 경로 flap에 과민해짐 |
probes |
3 안팎 | interval × probes = 사망 판정 지연이자 오판 방어 횟수 |
04. 적용 검증 — 설정은 sysctl이 아니라 소켓에 박힌다
DR tcpKeepalive는 커널 파라미터(net.ipv4.tcp_keepalive_*)를 바꾸지 않는다.
istiod가 cluster 설정으로 컴파일하고, Envoy가 연결을 새로 열 때마다 그 소켓에
setsockopt(TCP_KEEPIDLE/KEEPINTVL/KEEPCNT)를 호출하는 방식이다. 소켓 단위 옵션은
netns 기본값(sysctl)을 소켓별로 덮어쓸 뿐, 기본값 자체는 영원히 그대로다.
sysctl로 확인하는 것은 검증 지점 자체가 틀린 것이지 미적용의 증거가 아니다.
[sysctl 경로] net.ipv4.tcp_keepalive_time = 7200 (netns-wide DEFAULT)
|
v applies only when socket has NO explicit option
[DR 경로] Envoy --- setsockopt(fd, TCP_KEEPIDLE=300) ---> per-SOCKET option
(overrides the netns default, socket by socket)
설정은 DR → istiod(xDS) → Envoy cluster → 소켓 옵션 → 와이어 probe 순으로 흐른다.
단계 순서대로 확인하면 실패 지점을 이분탐색할 수 있다.
확인할 pod에 주의: 외부 호스트 DR(레이어 1)은 gw→외부 연결이므로 egress gateway pod에서,
sidecar→gw subset DR(레이어 2)은 앱 sidecar에서 확인한다.
① xDS — Envoy에 설정이 도달했는가 (default + subset cluster 나란히)
istioctl proxy-config cluster deploy/istio-egressgateway -n istio-egress \
--fqdn api.partner-a.example.com -o json | \
jq '.[] | {name, dr: .metadata.filterMetadata.istio.config,
max: .circuitBreakers.thresholds[0].maxConnections,
ka: .upstreamConnectionOptions.tcpKeepalive}'
기대 출력 — DR host의 모든 cluster가 한 줄씩 나온다. name의 세 번째 칸이 subset 이름(비어 있으면 default):
{ "name": "outbound|443||api.partner-a.example.com", "dr": ".../destination-rule/partner-a-external", "max": 4096, "ka": { "keepaliveTime": 300, ... } }
{ "name": "outbound|443|partner-a|api.partner-a.example.com", "dr": ".../destination-rule/partner-a-external", "max": 4096, "ka": { "keepaliveTime": 300, ... } }
dr= 이 cluster에 결부된 DR의 정체.null이면 값 이전의 문제 — DR이 그 cluster에 아예 붙지 않은 것(host 불일치·exportTo·중복 DR·잘못된 pod).max가 4294967295(uint32 최대) 로 나오면 connectionPool이 그 cluster에 안 닿았다는 시그니처다 — istiod가 미설정 한도에 넣는 기본값.- subset 줄만
ka: null이면 통째-교체 함정(아래 warning) — subset connectionPool에 tcpKeepalive 재기재 누락. - 특정 subset만 좁히려면:
--subset partner-a --port 443 - 필드마다 컴파일 칸이 다르다는 점 주의: 한도 4종(
maxConnections등)→circuitBreakers.thresholds,tcpKeepalive→upstreamConnectionOptions,connectTimeout→connectTimeout, HTTP 공통(idleTimeout 등)→typedExtensionProtocolOptions[...].commonHttpProtocolOptions. “max는 보이는데 ka가 null"을 한 칸만 보고 놓치지 말 것.
①-b (subset 전용) — 트래픽이 실제 그 subset cluster를 타는가
istiod는 VS 라우팅과 무관하게 DR의 subset마다 cluster를 만들어 두므로, cluster의 존재와 값은 “트래픽이 그걸 쓴다"의 증거가 아니다. VS가 subset으로 보내지 않으면 트래픽은 default cluster를 탄다:
kubectl exec <pod> -c istio-proxy -- pilot-agent request GET stats | \
grep "outbound|443|partner-a|" | grep -E "cx_active|cx_total|cx_overflow"
- 요청을 보낸 뒤
upstream_cx_total이 증가하면 subset cluster 사용 확정. - 정지 상태면 VS
destination.subset라우팅 미스 — 아래 미적용 원인 표. upstream_cx_overflow가 움직이면 subset의 maxConnections가 발동까지 된 것(access log flagUO와 1:1).
② 소켓 — 커널 소켓에 실제로 박혔는가
ss -o의 timer 필드가 소켓 단위 keepalive 상태를 직접 보여준다:
kubectl exec deploy/istio-egressgateway -n istio-egress -- \
ss -tno state established 'dst 203.0.113.10'
# Recv-Q Send-Q Local:Port Peer:Port
# 0 0 10.244.1.5:53210 203.0.113.10:443 timer:(keepalive,4min32sec,0)
timer:(keepalive, 남은시간, 무응답횟수)— 남은 시간이 300s(=5min)에서 카운트다운하면time: 300적용 확정. 미적용 소켓은 timer가 없거나(SO_KEEPALIVEoff) 기본 7200s 기준(2시간 근처)으로 표시된다.- 이미지에
ss가 없으면(distroless 계열) 노드에서nsenter -t <pause-or-proxy pid> -n ss -tno ...로 같은 netns를 본다.
③ 와이어 — probe가 실제로 나가는가 (최종 확인, 선택)
probe = payload 0바이트, 시퀀스를 1 물린 빈 ACK(§01). 유휴 상태에서 time 간격마다 보이면 끝까지 검증된 것:
# 노드에서
tcpdump -ni any host 203.0.113.10 and port 443
# 유휴 300s 후: length 0 ACK -> 상대의 즉시 ACK, 이후 300s 주기 반복
랩에서 빨리 보려면 time: 10으로 임시 설정해 10초 주기를 눈으로 확인한다 — TCP 장애 재현 랩 병목 4 절차와 같은 요령.
①이 비어 있다면 — 미적용의 흔한 원인
| 원인 | 확인 |
|---|---|
| 0순위: 값이 애초에 클러스터에 저장 안 됨 — apply 누락, 다른 kubeconfig 컨텍스트/클러스터에 적용, 로컬 YAML만 수정 | kubectl config current-context + kubectl get dr <name> -o yaml이 검증 0단계. 저장본에 connectionPool이 없으면 아래 전부 무의미 |
DR이 cluster에 아예 결부 안 됨 (dr: null + 전 항목 기본값) |
①의 dr 필드. null이면 host 불일치·exportTo·중복 DR·잘못된 pod 중 하나 |
| subset의 trafficPolicy가 상위를 통째로 교체 (아래 callout) | subset cluster의 upstreamConnectionOptions 직접 확인 |
DR host ↔ 트래픽 목적지 FQDN 불일치 (ServiceEntry hosts와 대조) |
proxy-config cluster에 해당 cluster 이름 존재 여부 |
| 같은 host에 DR 여러 벌 — 하나만 선택되고 나머지는 조용히 무시 | kubectl get dr -A | grep <host> + istioctl analyze (IST0101류 경고) |
portLevelSettings 포트 불일치 / VS가 subset으로 라우팅 안 함 |
VS의 destination.subset·포트 확인 + ①-b stats |
DR namespace/exportTo 스코프 밖 |
istioctl analyze |
| 확인하는 pod가 틀림 (레이어 1 설정을 sidecar에서 찾는 경우) | 위 “확인할 pod” 기준 |
| 기존 연결에는 소급 적용 안 됨 — 소켓 옵션은 연결 생성 시 1회 | ②에서 일부 소켓만 이상하면 설정 변경 전 연결. idleTimeout/maxConnectionDuration으로 자연 교체 또는 rollout restart |
subset 병합 규칙 — deep-merge가 아니라 최상위 필드 단위 통째 교체.
subset의 trafficPolicy는 상위 trafficPolicy와 필드 단위로 합쳐지지 않는다. istiod는
connectionPool·outlierDetection·loadBalancer·tls 각각을 통째로 subset 것으로
교체한다(subset에 없으면 상위 것 상속). 따라서 subset에 connectionPool.tcp.maxConnections
하나만 적어도 그 subset cluster에서는 상위의 tcpKeepalive가 사라진다.
subset을 타는 트래픽에 keepalive를 적용하려면 subset(또는 그 portLevelSettings) 안에
다시 전부 적어야 한다 — TCP 병목 정본 §06-1/06-2 YAML이
레이어 1과 레이어 2 양쪽에 tcpKeepalive를 중복 기재하는 이유가 이것이다.
portLevelSettings도 같은 규칙이 포트 단위로 한 번 더 적용된다.
05. 일괄 적용 — 채널별 DR 없이 통째로 하려면 (meshConfig vs sysctl)
“컨테이너 커널 파라미터로 통째 적용"은 절반만 맞다. 커널 keepalive는 SO_KEEPALIVE가
켜진 소켓에서만 동작하고, net.ipv4.tcp_keepalive_* sysctl은 on/off가 아니라 값의
기본값만 제공한다. 전 소켓 활성화를 강제하는 sysctl 스위치는 커널에 없다. Envoy는
설정 없이는 upstream 소켓에 SO_KEEPALIVE를 켜지 않으므로, pod sysctl만으로는
Envoy 경유 연결에 keepalive가 아예 발동하지 않는다 — 값이 300이든 7200이든 무관하게.
keepalive는 “스위치 + 다이얼” 2층 구조다:
+-----------------------------------------------------------+
| per-socket SWITCH: SO_KEEPALIVE default: OFF | <- setsockopt로만 켜짐
+-----------------------------------------------------------+
| DIALS (values): time / interval / probes |
| per-socket option (TCP_KEEPIDLE..) > netns sysctl | <- 스위치 ON일 때만 의미
+-----------------------------------------------------------+
스위치 기본 OFF는 리눅스의 선택이 아니라 TCP 표준(RFC 1122)의 요구다 — probe도 대역폭을 쓰고, 일시적 경로 장애 중 멀쩡히 복구될 연결을 keepalive가 먼저 죽일 수 있어, 켤지 말지는 소켓을 소유한 애플리케이션의 opt-in으로만 허용된다. 커널은 스위치가 켜진 소켓에만 probe를 보낸다. 그래서 “이 연결에 keepalive가 도는가?“의 답은 언제나 sysctl이 아니라 그 소켓을 만든 주체가 뭘 설정했는가다 — Envoy 소켓이면 DR/meshConfig가 그 답이다.
| 방법 | 동작 | 판단 |
|---|---|---|
meshConfig.tcpKeepalive |
istiod가 mesh 전역 모든 cluster에 keepalive를 컴파일. DR에 tcpKeepalive가 있는 목적지는 DR이 우선 |
권장 — “전역 기본 + 채널별 예외” 구조 |
pod sysctl(값) + DR/mesh에 tcpKeepalive: {} 빈 블록 |
빈 블록이 SO_KEEPALIVE만 켜고, 세 값은 netns sysctl을 따름 |
값 관리가 sysctl과 Istio 설정 두 곳으로 갈라짐 — 비추천 |
| pod sysctl 단독 | Envoy 소켓엔 무효(스위치가 안 켜짐). 앱 자체 발신 소켓 중 앱이 SO_KEEPALIVE를 켠 것에만 값이 반영 |
DR/mesh가 못 미치는 소켓용 보조 수단 |
# IstioOperator 또는 istio configmap (meshConfig)
meshConfig:
tcpKeepalive:
time: 300s
interval: 30s
probes: 3
적용 우선순위: DR tcpKeepalive > meshConfig.tcpKeepalive > (SO_KEEPALIVE가 켜진 소켓에 한해) netns sysctl 기본값.
빈 블록(tcpKeepalive: {}) + pod sysctl 조합의 정확한 동작
스위치만 켜고 다이얼을 sysctl에 맡기는 조합은 동작하지만, 네 가지를 정확히 알아야 한다:
- 스코프 — DR은 pod가 아니라 목적지(host) 단위다. DR 하나에
{}를 넣으면 그 host로 가는 연결만 켜진다. “이 pod에서 나가는 모든 목적지"를 원하면meshConfig.tcpKeepalive: {}— 스위치는 mesh 전역에서 켜고 다이얼은 각 pod netns sysctl이 정하는 분업이 성립한다. 특정 pod에서만 전 목적지를 켜는 스코프의 리소스는 없다. - netns 함정 — 노드 sysctl은 상속되지 않는다. 새 netns는 호스트 값이 아니라 커널 기본(7200/75/9)으로 초기화된다. 노드에서
sysctl -w를 해도 pod 소켓에는 무효 — 값은 반드시 **podsecurityContext.sysctls**로 그 pod netns에 넣어야 한다 (Pod 커널 파라미터 정본의 초기화 규칙). - 필드 혼합 가능 —
{time: 300}처럼 일부만 적으면 time은 소켓 옵션, interval/probes는 sysctl을 따르는 혼합 상태가 된다. Envoy는 명시된 필드만 setsockopt한다. - 소급 비대칭 — 소켓 옵션은 연결 생성 시 1회라 비소급(§04)이지만, sysctl 폴백 상태의 소켓은 probe를 스케줄할 때마다 현재 sysctl을 읽으므로 sysctl 변경은 기존 연결에도 동적 반영된다.
전역화의 대가 두 가지:
time의 전역 제약 — 채널별 튜닝 여지가 사라지므로 §03 규칙의 전역판이 된다: 모든 채널 FW idle timeout의 최솟값 기준 1/3 이하. probe 오버헤드는 time마다 빈 ACK 1개라 무시할 수준이므로 보수적으로(짧게) 잡아도 부담 없다.- 적용 범위가 mesh 전체 — gw→외부만이 아니라 sidecar→sidecar 등 모든 proxy에 걸린다. 정확히는 각 Envoy가 클라이언트로서 여는 upstream 소켓에 걸리는 것이고, 들어오는(downstream) 연결의 keepalive는 그쪽 클라이언트 소켓 주인의 문제다. 동작상 무해하지만 “여기도 probe가?“류 관측 노이즈가 생기니 도입 시 팀 공유 필요.
앱 런타임 층의 함정 하나: Go처럼 keepalive를 소켓 옵션으로 명시 설정(기본 15s)하는 런타임은 sysctl 값을 다시 덮어쓴다. “sysctl = 전 소켓 일괄 통제"라는 기대는 소켓 옵션 층(Envoy든 앱이든)에서 깨진다는 원리가 여기서도 반복된다.
결국 세 층의 분업으로 정리된다:
| 층 | 리소스 | 역할 |
|---|---|---|
| 전역 스위치·기본값 | meshConfig.tcpKeepalive |
mesh 모든 proxy의 upstream 연결에 일괄 |
| 채널별 튜닝 | DR tcpKeepalive (subset 재기재 주의 — §04) |
특정 목적지만 override |
| pod별 값 | pod securityContext.sysctls |
빈 블록·미명시 필드의 다이얼 공급 |
What you might be missing
- probe는 Envoy
idleTimeout을 리셋하지 못한다. probe는 데이터가 아니므로 Envoy의 idle 타이머(tcp_proxy 기본 1h)는 계속 흐른다. keepalive는 방화벽을 깨우는 장치이고, Envoy 자신의idleTimeout은 채널의 최장 유휴 간격보다 길게 직접 설정해야 한다 — “keepalive 넣었는데 1시간마다 끊겨요"의 정체 (TCP 병목 정본 §04). - 커널 기본값(
net.ipv4.tcp_keepalive_*)도 netns 스코프 sysctl이라 Pod 커널 파라미터 정본의 초기화 규칙이 그대로 적용되지만, DR로 설정하면 소켓 단위 옵션이 netns 기본값을 override하므로 pod sysctl을 건드릴 필요가 없다. k8s 1.29+에서tcp_keepalive_*가 safe sysctl로 승격된 것은 DR이 못 미치는 소켓(앱 자체 발신 등)까지 일괄 조정할 때의 대안 — 단 sysctl은 값만 정하고SO_KEEPALIVE스위치는 못 켠다. 일괄 적용의 올바른 경로는 §05(meshConfig). - keepalive ACK를 세션 갱신으로 안 쳐주는 중간장비(데이터 바이트만 카운트하는 일부 L7 장비)가 드물게 있다. FW 타입이 미확인인 채널은 “유휴 30분 후 첫 요청” 시나리오를 랩에서 실측할 것 — TCP 장애 재현 랩의 병목 4 절차 참조.
참조
아카이브 내부
- TCP 병목 정본 — §04 병목 4(half-open), §06-1 권장값이 놓이는 전체 YAML 맥락
- Pod 커널 파라미터 정본 — netns 초기화 규칙, safe/unsafe sysctl
- TCP 장애 재현 랩 — 병목 4 재현 절차
작업 파일 (다운로드)
- /files/istio-egress/tcp-keepalive/ — §04 검증 절차의 실행 스크립트(
verify-tcp-keepalive.sh)와 이 노트 md 사본
외부
- man 7 tcp — TCP_KEEPIDLE / TCP_KEEPINTVL / TCP_KEEPCNT 정의
- Envoy TcpKeepalive proto — DR 필드가 컴파일되는 대상