--- title: tcpKeepalive 필드 노트 — time / interval / probes는 각각 무엇을 제어하는가 date: 2026-07-03 type: note domain: istio tags: [istio, egress, tcp, keepalive] --- > [!abstract] > DR `connectionPool.tcp.tcpKeepalive`의 세 필드는 Envoy가 만든 개념이 아니라 **리눅스 커널의 > TCP keepalive 소켓 옵션 3개에 1:1 매핑**된다. Envoy는 upstream 소켓에 옵션을 설정만 하고, > probe를 보내는 주체는 커널이다. 이 한 설정이 **서로 다른 두 역할**(중간장비 세션 유지 / 죽은 상대 감지)을 > 겸한다는 것, 그리고 각 역할을 결정하는 필드가 다르다는 것이 이 노트의 본체다. **선행 문서**: [TCP 병목 정본](/public/istio-egress/ref__src-tcp-bottlenecks.html) §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)의 두 시나리오 ```mermaid 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 ``` **역할 분리가 핵심이다:** 1. **중간장비 세션 유지** ([병목 4](/public/istio-egress/ref__src-tcp-bottlenecks.html)의 처방) — 상대가 살아있는 정상 케이스. probe/ACK 왕복이 FW 세션 idle 타이머를 갱신한다. 결정 규칙은 `time < FW idle timeout`(예: 300s < 1800s)이며, **`interval`/`probes`는 이 역할과 무관**하다 — 살아있는 상대는 첫 probe에 즉시 ACK하므로 재시도가 발생하지 않는다. 2. **죽은 상대 감지** — 상대 서버가 소리 없이 사라진 경우(전원 단절, 경로 단절). `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이 아니라 소켓에 박힌다 > [!key] > 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 나란히) ```bash 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): ```json { "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를 탄다: ```bash kubectl exec -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 flag `UO`와 1:1). ### ② 소켓 — 커널 소켓에 실제로 박혔는가 `ss -o`의 timer 필드가 소켓 단위 keepalive 상태를 직접 보여준다: ```bash 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_KEEPALIVE` off) 기본 7200s 기준(2시간 근처)으로 표시된다. - 이미지에 `ss`가 없으면(distroless 계열) 노드에서 `nsenter -t -n ss -tno ...`로 같은 netns를 본다. ### ③ 와이어 — probe가 실제로 나가는가 (최종 확인, 선택) probe = payload 0바이트, 시퀀스를 1 물린 빈 ACK(§01). 유휴 상태에서 `time` 간격마다 보이면 끝까지 검증된 것: ```bash # 노드에서 tcpdump -ni any host 203.0.113.10 and port 443 # 유휴 300s 후: length 0 ACK -> 상대의 즉시 ACK, 이후 300s 주기 반복 ``` 랩에서 빨리 보려면 `time: 10`으로 임시 설정해 10초 주기를 눈으로 확인한다 — [TCP 장애 재현 랩](/public/istio-egress/cfg__guide-tcp-failure-reproduction.html) 병목 4 절차와 같은 요령. ### ①이 비어 있다면 — 미적용의 흔한 원인 | 원인 | 확인 | |---|---| | **0순위: 값이 애초에 클러스터에 저장 안 됨** — apply 누락, 다른 kubeconfig 컨텍스트/클러스터에 적용, 로컬 YAML만 수정 | `kubectl config current-context` + `kubectl get dr -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 ` + `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 | > [!warning] > **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 병목 정본](/public/istio-egress/ref__src-tcp-bottlenecks.html) §06-1/06-2 YAML이 > 레이어 1과 레이어 2 양쪽에 `tcpKeepalive`를 중복 기재하는 이유가 이것이다. > `portLevelSettings`도 같은 규칙이 포트 단위로 한 번 더 적용된다. --- ## 05. 일괄 적용 — 채널별 DR 없이 통째로 하려면 (meshConfig vs sysctl) > [!warning] > "컨테이너 커널 파라미터로 통째 적용"은 절반만 맞다. 커널 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가 못 미치는 소켓용 보조 수단 | ```yaml # 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 소켓에는 무효 — 값은 반드시 **pod `securityContext.sysctls`**로 그 pod netns에 넣어야 한다 ([Pod 커널 파라미터 정본](/public/istio-egress/cfg__guide-pod-sysctl-netns.html)의 초기화 규칙). - **필드 혼합 가능** — `{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](/public/istio-egress/ref__src-tcp-bottlenecks.html)). - **커널 기본값(`net.ipv4.tcp_keepalive_*`)도 netns 스코프 sysctl**이라 [Pod 커널 파라미터 정본](/public/istio-egress/cfg__guide-pod-sysctl-netns.html)의 초기화 규칙이 그대로 적용되지만, DR로 설정하면 **소켓 단위 옵션이 netns 기본값을 override**하므로 pod sysctl을 건드릴 필요가 없다. k8s 1.29+에서 `tcp_keepalive_*`가 safe sysctl로 승격된 것은 DR이 못 미치는 소켓(앱 자체 발신 등)까지 일괄 조정할 때의 대안 — 단 sysctl은 **값만** 정하고 `SO_KEEPALIVE` 스위치는 못 켠다. 일괄 적용의 올바른 경로는 §05(meshConfig). - **keepalive ACK를 세션 갱신으로 안 쳐주는 중간장비**(데이터 바이트만 카운트하는 일부 L7 장비)가 드물게 있다. FW 타입이 미확인인 채널은 "유휴 30분 후 첫 요청" 시나리오를 랩에서 실측할 것 — [TCP 장애 재현 랩](/public/istio-egress/cfg__guide-tcp-failure-reproduction.html)의 병목 4 절차 참조. --- ## 참조 **아카이브 내부** - [TCP 병목 정본](/public/istio-egress/ref__src-tcp-bottlenecks.html) — §04 병목 4(half-open), §06-1 권장값이 놓이는 전체 YAML 맥락 - [Pod 커널 파라미터 정본](/public/istio-egress/cfg__guide-pod-sysctl-netns.html) — netns 초기화 규칙, safe/unsafe sysctl - [TCP 장애 재현 랩](/public/istio-egress/cfg__guide-tcp-failure-reproduction.html) — 병목 4 재현 절차 **작업 파일 (다운로드)** - [/files/istio-egress/tcp-keepalive/](/files/istio-egress/tcp-keepalive/) — §04 검증 절차의 실행 스크립트(`verify-tcp-keepalive.sh`)와 이 노트 md 사본 **외부** - [man 7 tcp](https://man7.org/linux/man-pages/man7/tcp.7.html) — TCP_KEEPIDLE / TCP_KEEPINTVL / TCP_KEEPCNT 정의 - [Envoy TcpKeepalive proto](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-tcpkeepalive) — DR 필드가 컴파일되는 대상