책의 색인처럼 DB 인덱스도 원하는 행의 후보를 찾는 데 도움을 줍니다. 하지만 책의 대부분을 읽어야 한다면 색인을 왕복하는 일이 더 편하지 않을 수 있습니다. 이번에는 인덱스가 하는 일과 DB가 그것을 선택하는 일 을 구분해보겠습니다. 행을 찾는 보조 구조 PostgreSQL의 기본 B-tree 인덱스는 정렬 가능한 값의 동등·범위 조건을 지원합니다. 실제로 사용할지는 플래너가 비용을 비교해 선택합니다. 모든 조건과 모든 인덱스 종류가 같은 방식으로 동작하는 것은 아닙니다. PostgreSQL 18 인덱스 종류 그림은 조회 경로를 설명하는 모델입니다. 실제 B-tree 높이나 페이지 접근 횟수, 측정 시간을 나타내지 않습니다. EXPLAIN에서 달라지는 표시 보기 독립 SQLite 메모리 DB 에 합성 데이터 1,000행을 넣고 다음 SQL을 실행했습니다. PostgreSQL의 실행 계획을 테스트한 결과는 아닙니다. CREATE TABLE item (id INTEGER PRIMARY KEY, category INTEGER); WITH RECURSIVE seq(n) AS ( SELECT 0 UNION ALL SELECT n + 1 FROM seq WHERE n < 999 ) INSERT INTO item (id, category) SELECT n, n % 10 FROM seq; EXPLAIN QUERY PLAN SELECT id FROM item WHERE category = 7; CREATE INDEX item_category_idx ON item(category); EXPLAIN QUERY PLAN SELECT id FROM item WHERE category = 7; 이 실험에서는 인덱스 전 계획에 SCAN item , 생성 후 계획에 SEARCH item USING COVERING INDEX item_category_idx 가 표시됐습니다. SQLite 공식 EXPLAIN QUERY PLAN 설명 계획이 바뀐 사실을 확인한 것이지, 실제 실행 시간이 몇 배 줄었음을 측정한 것은 아닙니다. 다른 데이터 분포와 엔진 버전에서는 다른 선택이 가능합니다. 인덱스를 적용하기 전에는 조회 결과가 같은지도 함께 확인해야 합니다. 읽기만 있는 자료구조가 아니다 인덱스는 공간을 사용하고 데이터 변경 때 함께 관리됩니다. 많은 행을 읽는 조건이라면 전체 스캔이 유리할 수 있습니다. 인덱스가 있다고 항상 쓰는 것도, 계획에 인덱스가 나오면 전체 요청이 빨라지는 것도 아닙니다. 반환 행 수, 정렬, 조인과 다른 비용이 남습니다. 여러 열을 묶은 인덱스는 열의 순서와 조건 조합도 검토해야 합니다. 이번 글에서는 단일 열의 후보 검색까지만 다룹니다. 실서비스 인덱스를 만들기 전에 실제 쿼리와 데이터 분포를 기준으로 판단합니다. 확인 문제 인덱스가 있다는 사실과 플래너가 선택했다는 사실은 같을까요? 전체 행의 90%가 필요한 조회에서도 인덱스가 무조건 유리할까요? EXPLAIN 표시가 바뀌었다면 실행 시간이 줄었다고 확정할 수 있을까요? 답과 해설 아닙니다. 실제 실행 계획을 확인합니다. 아닙니다. 전체 스캔과 후보 조회의 비용을 비교해야 합니다. 안 됩니다. 동일한 조건에서 결과와 실행 비용을 별도로 측정해야 합니다. 오늘은 후보 찾기와 실제 행 확인을 나눠 설명해보겠습니다. 내일은 선택되는 행 비율을 바꾸고, 일주일 뒤에는 복합 인덱스의 열 순서와 연결해보면 좋겠습니다. 자료 확인 기준: PostgreSQL 18과 SQLite 공식 문서, 2026-10-04. 메모리 DB 계획·결과 검증이며 운영 DB의 성능 시험은 아닙니다. 예제 검증 환경: Python 3.13.13의 SQLite 3.51.2, 독립 :memory: DB.
음악을 들으면서 파일을 내려받고 글을 쓰면 컴퓨터가 여러 일을 동시에 하는 것처럼 보입니다. 이번에는 여러 작업이 진행 중이라는 말과, 같은 순간 계산한다는 말 을 구분해보겠습니다. 프로세스와 스레드의 기본 개념을 알고 있으면 읽기 편합니다. 한 코어의 시간표부터 보기 동시성은 여러 작업의 진행을 함께 다루는 성질입니다. 병렬성은 여러 작업이 같은 시간에 실제로 실행되는 성질입니다. 한 코어가 A와 B를 번갈아 실행해도 두 작업을 함께 진행할 수 있습니다. 여러 코어가 A와 B를 각각 실행하면 실제 실행 구간이 겹칠 수 있습니다. 스레드의 병렬 실행과 입출력 대기를 겹치는 이유는 OSTEP의 동시성 장 에서 확인할 수 있습니다. 그림의 칸은 설명용 구간입니다. 실제 밀리초나 측정한 성능을 뜻하지 않습니다. 상황 관찰할 것 한 코어에서 A, B 번갈아 실행 여러 작업이 진행되지만 그 코어의 실행은 번갈아 일어남 두 코어에서 A, B 실행 실제 실행 구간이 겹칠 수 있음 A가 파일 읽기를 기다리는 동안 B 실행 대기를 다른 유용한 작업과 겹침 비동기라면 계산도 빨라질까? 다음은 Node.js에서 비동기 콜백이 언제 실행되는지 보는 작은 예제입니다. const order = []; order.push("A:start"); Promise.resolve().then(() => order.push("B:callback")); order.push("A:end"); queueMicrotask(() => console.log(order.join(" -> "))); 검증한 출력은 A:start -> A:end -> B:callback 입니다. 이 예제는 마이크로태스크의 순서를 확인합니다. 코어 두 개를 사용하거나 CPU 계산을 병렬화한 실험은 아닙니다. Promise를 만들었다는 사실만으로 계산이 다른 코어로 이동하지 않습니다. Node.js의 queueMicrotask 설명 작업이 CPU 계산 때문에 오래 걸리는지, 응답을 기다리느라 오래 걸리는지부터 구분해야 합니다. 기다림을 겹칠 수 있는 작업과 계산을 나눌 수 있는 작업은 해결 방법이 달라집니다. 헷갈리지 않기 스레드 수, 비동기 함수 수, 코어 수는 서로 다른 숫자입니다. 스레드를 늘렸다고 실행 시간이 그 비율대로 줄어들지는 않습니다. 공유 상태를 보호하는 비용과 작업을 나누는 비용도 생깁니다. 이번 시간표는 실행 관계를 설명하는 모델이고 성능 예측표는 아닙니다. 확인 문제 한 코어에서 A와 B를 번갈아 실행하면 동시성인가요? 위 코드에서 Promise 콜백을 등록하면 A:end 보다 먼저 실행될까요? CPU 계산을 두 배 빨리 끝내려면 비동기 함수 두 개만 만들면 충분할까요? 답과 해설 네. 여러 작업을 함께 진행하지만 해당 코어에서 같은 순간 실행하는 것은 아닙니다. 아닙니다. 현재 동기 실행을 마친 뒤 등록한 콜백을 실행합니다. 아닙니다. 실제 병렬 실행 구조, 분할 가능한 계산, 전달 비용과 공유 자원 등을 확인해야 합니다. 복습과 다음 단계 오늘은 한 코어와 두 코어의 시간표를 직접 그려보겠습니다. 내일은 파일 읽기 대기 구간을 추가하고, 일주일 뒤에는 CPU 작업과 입출력 작업의 개선 방법을 각각 설명해보면 좋겠습니다. 다음에는 작업을 바꿀 때 실행 상태를 어떻게 이어 가는지 살펴봅니다. 자료 확인 기준: OSTEP 동시성 장과 Node.js 공식 API 문서, 2026-10-04. 예제 검증 환경: Node.js v24.13.1. 이전 기초 개념: 프로세스와 스레드 정리
화면을 새로고침했는데 개발자 도구에 304 가 보입니다. 본문을 다시 받지 않았다고 무조건 서버에 요청하지 않은 것은 아닙니다. 이번에는 저장, 재사용, 재검증 을 각각 나눠보겠습니다. 세 동작을 구분하기 저장은 응답을 캐시에 보관하는 일입니다. 재사용은 그 응답으로 다음 요청을 처리하는 일입니다. 재검증은 서버에 기존 응답이 여전히 유효한지 확인하는 일입니다. 이 구분과 지시자의 의미는 HTTP Caching RFC 9111 에서 확인할 수 있습니다. 그림은 저장 가능한 GET 응답의 기본 흐름입니다. 모든 상태 코드와 공유 캐시의 예외를 나타내지는 않습니다. 응답 지시자 핵심 의미 max-age=60 신선도를 판단하는 수명 기준 60초 no-cache 보관할 수 있지만 성공적인 재검증 없이 재사용하지 않음 no-store 해당 요청·응답에 대해 캐시가 저장하지 않도록 지시 private 공유 캐시는 저장하지 않음. 개인 캐시와 구분 ETag로 다시 확인하는 예시 서버가 응답에 ETag: "v1" 을 붙였다고 가정하겠습니다. 다음 요청은 If-None-Match: "v1" 로 현재 표현이 같은지 물을 수 있습니다. 조건이 맞으면 304 Not Modified 를 받아 저장된 본문을 재사용하고, 응답 헤더도 규칙에 맞게 갱신합니다. RFC 9111의 검증 const decide = (clientTag, serverTag) => clientTag === serverTag ? 304 : 200; console.log(decide('"v1"', '"v1"')); console.log(decide('"v1"', '"v2"')); 출력은 304 , 200 입니다. 태그 비교 분기만 검증한 단순 모형입니다. 실제 HTTP 구현은 메서드, 여러 태그, 약한 비교와 다른 조건부 요청 규칙도 처리해야 합니다. 네트워크 서버를 시험한 코드는 아닙니다. 캐시는 응답 하나만 보고 끝나지 않는다 요청의 인증 정보, Vary , 공유 캐시 여부와 저장 가능 조건을 함께 확인해야 합니다. 사용자별 응답을 공용으로 재사용하면 문제가 됩니다. 또 no-store 를 붙였다고 이미 모든 곳에 저장된 과거 복사본이 삭제되는 것은 아닙니다. “캐시를 껐다”보다 어느 계층에서 무엇을 금지했는지 설명하는 편이 정확합니다. 확인 문제 no-cache 는 저장 금지와 같은 말일까요? 304 라면 서버와 통신하지 않았다는 뜻일까요? 서버의 태그가 "v3" 로 바뀌면 위 모형의 결과는 무엇일까요? 답과 해설 아닙니다. 성공적인 재검증 없이 재사용하지 말라는 의미입니다. 아닙니다. 조건부 요청에 대한 서버 응답일 수 있습니다. 기존 클라이언트 태그와 다르므로 200입니다. 실제 응답 본문과 헤더 처리도 필요합니다. 오늘은 저장·재사용·재검증을 각각 한 문장으로 설명해보겠습니다. 내일은 개인 캐시와 공유 캐시를 나누고, 일주일 뒤에는 DNS TTL과 HTTP 신선도가 관리하는 대상을 비교해보면 좋겠습니다. 자료 확인 기준: RFC 9111, 2026-10-04. 코드의 출력은 모형 검증이며 실제 캐시 서버의 보안·성능 시험이 아닙니다. 예제 검증 환경: Node.js v24.13.1.
네트워크를 공부하면 “TCP는 안전하고 UDP는 빠르다”는 말을 자주 만납니다. 그런데 안전이 암호화를 뜻하는지, 전송 순서를 뜻하는지부터 모호합니다. 이번에는 프로그램이 받는 데이터의 형태와 전송 계층이 제공하는 성질 을 나눠보겠습니다. 바이트 흐름과 데이터그램 TCP는 애플리케이션에 신뢰성 있는 순서 있는 바이트 스트림을 제공합니다. UDP는 데이터그램 방식이며 전달, 중복 방지, 순서 보장을 기본으로 제공하지 않습니다. TCP RFC 9293 §2.2 , UDP RFC 768 그림의 “메시지 경계”는 프로그램이 정한 구분입니다. IP 패킷 크기나 실제 캡처 결과를 나타내지 않습니다. 확인할 것 TCP UDP 애플리케이션이 보는 기본 형태 바이트 스트림 데이터그램 보낸 메시지 단위의 유지 별도 프레이밍 필요 데이터그램 단위 누락·중복·순서 처리 TCP의 신뢰성 메커니즘 필요한 기능을 상위 계층에서 설계 두 번 보냈으면 두 번 받을까? TCP에 HELLO 와 WORLD 를 각각 보냈다고 가정하겠습니다. 수신 프로그램은 HELLOWORLD 를 한 번에 읽거나, 여러 조각으로 읽을 수 있습니다. 한 번의 send 와 한 번의 read 가 같은 업무 메시지라는 가정을 버려야 합니다. 줄바꿈을 메시지 구분자로 정한 단순 모형은 다음처럼 나눌 수 있습니다. const chunks = ["HEL", "LO\nWOR", "LD\n"]; const messages = chunks.join("").split("\n"); messages.pop(); console.log(messages.join(" / ")); 출력은 HELLO / WORLD 입니다. 합성 조각으로 경계 복원을 검증한 코드이며 실제 소켓 테스트는 아닙니다. 지속 연결에서는 남은 조각을 보관하고, 최대 길이와 문자 인코딩, 종료 상황도 처리해야 합니다. 신뢰성은 암호화와 다르다 TCP를 사용했다는 사실만으로 내용이 암호화되거나 상대를 인증하는 것은 아닙니다. TLS 같은 별도 계층을 확인해야 합니다. 또 UDP 위에 신뢰성 기능을 만들 수도 있으므로, UDP를 쓴다는 사실만으로 서비스 전체에 재전송이 없다고 단정할 수 없습니다. “항상 빠르다”는 결론도 실제 메시지 크기, 손실, 구현과 요구 조건 없이 내릴 수 없습니다. 확인 문제 TCP에서 읽은 한 조각이 항상 한 메시지일까요? TCP를 선택하면 자동으로 암호화될까요? UDP를 쓰는 서비스가 재전송을 구현할 수 있을까요? 답과 해설 아닙니다. 길이 헤더나 구분자 등 업무 메시지를 나누는 규칙이 필요합니다. 아닙니다. 전송 신뢰성과 암호화·인증은 다른 기능입니다. 가능합니다. 상위 프로토콜이나 애플리케이션이 기능을 추가할 수 있습니다. 오늘은 임의의 위치에서 문자열을 잘라 다시 합쳐보겠습니다. 내일은 구분자를 포함한 본문을 어떻게 처리할지 생각하고, 일주일 뒤에는 길이 헤더 방식과 비교해보면 좋겠습니다. 다음은 주소를 찾는 DNS입니다. 자료 확인 기준: RFC 9293의 핵심 TCP 성질, RFC 768의 기본 UDP 정의, 2026-10-04. 최신 확장 전체를 다루는 글은 아닙니다. 예제 검증 환경: Node.js v24.13.1.
작업 A는 락 L1을 잡고 L2를 기다립니다. 작업 B는 L2를 잡고 L1을 기다립니다. 둘 중 하나가 진행해야 다른 쪽도 진행할 수 있는데, 두 작업이 모두 멈췄습니다. 이번에는 이 작은 상황으로 교착 상태가 만들어지는 조건과 예방 방법 을 정리해보겠습니다. 기다림이 고리가 될 때 교착 상태를 설명하는 네 조건은 상호 배제, 점유하며 대기, 비선점, 순환 대기입니다. 락 예시에서는 각각 한 작업만 락을 가지며, 이미 잡은 락을 놓지 않고 다른 락을 기다리고, 다른 작업이 강제로 빼앗지 못하며, 대기가 고리로 이어집니다. OSTEP의 교착 상태 조건 이 그림은 각 락을 한 작업만 소유하는 모델입니다. 자원에 여러 인스턴스가 있는 일반적인 상황에서는 고리만 보고 결론을 내리면 안 됩니다. 작업 이미 소유 기다리는 자원 A L1 L2 B L2 L1 둘 다 상대가 놓아야 하는 락을 기다립니다. 단순히 실행 속도가 느린 상황과 구분해야 합니다. 공통 순서를 정해보기 모든 경로가 L1 → L2 순서로 락을 잡도록 정하면 이 두 락의 순환 대기를 피할 수 있습니다. “함수 인자 순서대로 잡는다”는 규칙은 충분하지 않습니다. 호출자가 인자를 뒤집으면 전체 프로그램의 순서는 다시 달라집니다. OSTEP의 락 순서 예방 설명 const order = (locks) => [...locks].sort((a, b) => a - b); console.log(order([2, 1]).join(" -> ")); console.log(order([1, 2]).join(" -> ")); 두 출력 모두 1 -> 2 입니다. 이 코드는 순서 규칙만 검증합니다. 실제 락을 획득하거나 운영체제의 교착 상태를 발생시킨 실험은 아닙니다. 실제 구현에서는 같은 락을 중복 획득하는지, 예외 때 해제하는지, 다른 경로도 규칙을 따르는지 확인해야 합니다. 타임아웃만 넣으면 해결일까? 대기를 중단할 수는 있지만, 잡아 둔 자원을 해제하고 부분 작업을 복구하는 절차가 필요합니다. 두 작업이 같은 패턴으로 계속 재시도하면 진행하지 못하는 다른 문제가 생길 수도 있습니다. 예방, 탐지, 복구를 한 단어로 묶지 말고 각각 어떤 규칙을 제공하는지 확인합니다. 확인 문제 A와 B가 모두 L1부터 잡으면 위 두 락의 순환 대기가 만들어질까요? 함수마다 인자 순서대로 잡는 규칙은 전체 순서를 보장할까요? 대기가 길다는 사실만으로 교착 상태를 확정할 수 있을까요? 답과 해설 이 모델에서는 아닙니다. 먼저 L1을 잡은 작업이 L2로 진행하고, 다른 작업은 L1을 잡기 전에 기다립니다. 아닙니다. 모든 호출 경로가 공유하는 자원 순서가 필요합니다. 아닙니다. 자원 소유자, 대기 관계와 진행 가능성을 확인해야 합니다. 오늘은 소유와 대기 화살표를 따로 그려보겠습니다. 내일은 락 세 개의 순서를 정하고, 일주일 뒤에는 타임아웃 뒤 복구할 상태를 설명해보면 좋겠습니다. 자료 확인 기준: OSTEP 교착 상태 절의 2026-10-04 확인본. 코드와 그림은 설명용 모형입니다. 예제 검증 환경: Node.js v24.13.1.
동영상을 보면서 편집기를 사용하는 상황을 생각해보겠습니다. CPU가 실행할 작업을 바꿨다가 돌아왔을 때, 편집기는 처음부터 시작하지 않습니다. 멈춘 위치와 실행 상태를 보관하고 복원하기 때문 입니다. 선택하는 일과 바꾸는 일 스케줄링은 다음에 실행할 작업을 선택하는 일입니다. 문맥 전환은 현재 실행 상태를 저장하고 다른 작업의 상태를 복원하는 일입니다. 두 용어를 같은 뜻으로 외우면 “누구를 선택했나”와 “어떻게 이어 갔나”가 섞입니다. 스레드의 문맥에는 실행 위치와 레지스터 상태 등이 포함됩니다. 같은 프로세스의 스레드 사이에서는 주소 공간을 공유하지만 실행 상태는 각각 관리합니다. OSTEP의 스레드 문맥 설명 그림은 핵심 절차를 줄인 모델입니다. 실제 커널 함수 호출 순서나 모든 저장 항목을 나타내지는 않습니다. 시간 조각을 나눠 주는 예시 라운드 로빈은 실행 가능한 작업을 시간 조각 단위로 번갈아 실행하는 대표적인 설명 모델입니다. OSTEP의 스케줄링 장 A와 B가 각각 계산 3단위가 필요하고, 시간 조각을 1단위라고 가정하겠습니다. 도착 시각은 같고, 입출력 대기와 전환 비용은 이번 계산에서 제외합니다. const remaining = { A: 3, B: 3 }; const trace = []; while (remaining.A + remaining.B > 0) { for (const job of ["A", "B"]) { if (remaining[job] > 0) { remaining[job] -= 1; trace.push(job); } } } console.log(trace.join(" ")); 검증한 출력은 A B A B A B 입니다. 이것은 JavaScript로 작성한 스케줄링 모형 이며, 운영체제의 실제 스케줄러를 실행하거나 측정한 결과는 아닙니다. 조각을 작게 만들면 무조건 좋을까? 작은 시간 조각은 다른 작업이 첫 실행 기회를 받기까지의 시간을 줄일 수 있습니다. 하지만 전환이 잦아지면 상태 저장·복원과 캐시 등에 영향을 주는 비용도 고려해야 합니다. 응답성과 총 처리량은 같은 목표가 아닙니다. OSTEP의 시간 조각과 전환 비용 설명 모든 인터럽트가 작업 교체로 이어지는 것도 아닙니다. 이벤트가 발생했다는 사실과 실제로 다른 실행 흐름으로 전환했는지는 구분해서 봐야 합니다. 확인 문제 다음 작업을 고르는 것은 스케줄링일까요, 상태 복원일까요? B가 이미 끝났다면 위 모형은 B를 계속 실행할까요? 전환 비용을 무시한 시간표로 실제 처리량을 단정해도 될까요? 답과 해설 스케줄링입니다. 선택한 작업으로 상태를 바꾸는 과정과 구분합니다. 아닙니다. 남은 계산이 0이면 실행하지 않습니다. 안 됩니다. 실제 작업과 전환 비용, 캐시, 입출력 등을 측정해야 합니다. 복습 오늘은 “선택 → 저장·복원 → 실행”을 말로 설명해보겠습니다. 내일은 A가 4단위, B가 2단위인 시간표를 그리고, 일주일 뒤에는 응답 시간과 완료 시간을 구분해보면 좋겠습니다. 다음에는 여러 작업이 서로 자원을 기다리는 문제를 살펴봅니다. 자료 확인 기준: OSTEP 스레드·스케줄링 장, 2026-10-04. 예제의 시간 단위는 설명용이며 실제 성능 수치가 아닙니다. 예제 검증 환경: Node.js v24.13.1.
“이 코드는 O(n)이라 빠르다”는 설명만으로 실제 시간을 알 수는 없습니다. 이번에는 입력 개수와 반복 횟수를 따로 세면서 증가율을 설명하는 표기와 측정 시간을 구분 해보겠습니다. 무엇을 세는지 먼저 정하기 Big-O는 입력 크기에 따른 비용의 점근적 상한을 설명합니다. 상수와 작은 항을 생략하므로 같은 O(n)인 코드라도 실제 실행 시간은 달라질 수 있습니다. 또 최악의 경우를 분석하는지, 평균 경우를 분석하는지는 별도 선택입니다. Open Data Structures의 수학적 배경 그림의 단위는 연산 횟수 모형 입니다. 초나 밀리초를 측정한 그래프가 아닙니다. 두 반복문을 실제로 세기 for (const n of [4, 8, 16]) { let linear = 0; let quadratic = 0; for (let i = 0; i < n; i += 1) linear += 1; for (let i = 0; i < n; i += 1) { for (let j = 0; j < n; j += 1) quadratic += 1; } console.log(`${n}: ${linear}, ${quadratic}`); } 검증한 출력은 다음과 같습니다. n 첫 반복문 중첩 반복문 4 4 16 8 8 64 16 16 256 여기서는 표시한 증가 연산을 셌습니다. 모든 CPU 명령어 수나 전체 실행 시간을 센 것은 아닙니다. n을 두 배로 늘리면 첫 값은 두 배, 두 번째 값은 네 배가 됩니다. 중첩이면 모두 O(n2)일까? 안쪽 반복문이 항상 n번 실행하는지 봐야 합니다. 안쪽이 고정 3번이면 전체 반복은 3n입니다. 안쪽 변수가 매번 두 배가 되는 경우는 증가 방식이 다릅니다. 중첩된 모양보다 각 반복 횟수의 합을 확인하는 편이 좋습니다. O(n)은 “정확히 n번”이라는 뜻도 아닙니다. 이 예제의 정확한 횟수와 Big-O 표기를 구분합니다. 작은 입력에서의 상수 비용, 캐시와 입출력은 실제 실행 측정으로 보완해야 합니다. 확인 문제 n을 32로 바꾸면 위 두 값은 무엇일까요? 바깥 n번, 안쪽 3번이면 O(n2)일까요? 같은 O(n)이면 실제 실행 시간도 같을까요? 답과 해설 32와 1024입니다. 각 반복문의 횟수를 대입합니다. 아닙니다. 3n이므로 O(n)입니다. 아닙니다. 상수 비용과 구현, 입력 특성, 실행 환경이 다릅니다. 오늘은 연산을 무엇으로 정의했는지 적어보겠습니다. 내일은 고정 3번 반복을 추가하고, 일주일 뒤에는 이진 탐색의 입력 절반 줄이기와 비교해보면 좋겠습니다. 자료 확인 기준: Open Data Structures의 Big-Oh 설명, 2026-10-04. 표는 코드로 확인한 증가 연산 횟수이며 실행 시간 벤치마크가 아닙니다. 예제 검증 환경: Node.js v24.13.1.
해시 테이블은 키로 값을 빠르게 찾는 데 자주 쓰입니다. 그런데 해시값만 같으면 같은 키라고 봐도 될까요? 이번에는 서로 다른 키가 같은 위치에 들어가는 충돌 을 작은 숫자로 확인해보겠습니다. 해시는 후보 위치를 정한다 해시 함수는 키를 저장할 위치를 계산하는 데 사용됩니다. 서로 다른 키가 같은 버킷으로 갈 수 있으므로 충돌을 처리해야 합니다. 체이닝은 버킷에 여러 항목을 보관하고, 조회할 때 실제 키를 비교하는 방법입니다. Open Data Structures의 체이닝 해시 테이블 그림의 key % 4 는 충돌을 쉽게 보여 주기 위한 함수입니다. 실제 문자열 해시나 보안용 해시 함수가 아닙니다. 충돌해도 두 값을 보관하기 const buckets = Array.from({ length: 4 }, () => []); for (const [key, value] of [[1, "one"], [5, "five"]]) { buckets[key % 4].push([key, value]); } const found = buckets[5 % 4].find(([key]) => key === 5); console.log(buckets[1].length); console.log(found[1]); 검증한 출력은 2 , five 입니다. 두 키가 같은 버킷에 있지만 실제 키 비교로 값을 구분했습니다. 이 예제는 양의 정수 키의 삽입·조회만 다루며, 삭제와 재삽입, 갱신, 크기 확장은 구현하지 않았습니다. O(1)에는 조건이 있다 해시가 항목을 적절히 분산하고 적재율을 관리하면 기대 조회 비용을 작게 유지할 수 있습니다. 하지만 충돌이 한곳에 몰리면 체이닝 버킷의 선형 탐색이 길어집니다. 크기를 늘릴 때 재배치 비용도 생깁니다. 평균·기대 비용, 최악 비용, 여러 삽입에 나눠 보는 상각 비용을 구분해야 합니다. 같은 자료의 성능 조건 이 모형만으로 JavaScript Map 이나 특정 언어의 딕셔너리가 같은 방식으로 구현되어 있다고 주장하지 않습니다. 자료구조의 개념과 실제 런타임 구현은 별도로 확인합니다. 확인 문제 해시값이 같으면 키도 같을까요? 위 코드에 키 9를 추가하면 어느 버킷에 들어갈까요? 한 버킷에 모든 항목이 들어가면 조회 비용은 어떻게 될까요? 답과 해설 아닙니다. 충돌할 수 있으므로 키를 확인합니다. 9 % 4 = 1 이므로 버킷 1입니다. 이 체이닝 모형에서는 해당 리스트를 순서대로 찾아야 해서 항목 수에 따라 비용이 커집니다. 오늘은 키 1, 5, 9를 넣어 그림을 다시 그려보겠습니다. 내일은 버킷 개수를 8로 바꾸고, 일주일 뒤에는 열린 주소 방식과 체이닝의 차이를 찾아보면 좋겠습니다. 다음은 입력 크기에 따라 연산 수가 어떻게 늘어나는지입니다. 자료 확인 기준: Open Data Structures의 ChainedHashTable 절, 2026-10-04. 실행 결과는 교육용 구현의 출력이며 상용 런타임 벤치마크가 아닙니다. 예제 검증 환경: Node.js v24.13.1.
도메인의 주소를 바꿨는데 일부 사용자는 예전 서버로 접속합니다. “DNS가 전파되는 중”이라고만 말하면 무엇을 기다리는지 알기 어렵습니다. 이번에는 누가 답을 가지고 있고, 언제까지 재사용할 수 있는지 를 중심으로 보겠습니다. 이름을 묻는 곳과 답을 관리하는 곳 일반적인 이름 조회에서 클라이언트는 재귀 리졸버에 질문합니다. 리졸버가 유효한 캐시를 가지고 있으면 재사용하고, 필요하면 이름 서버를 찾아 답을 구합니다. 권한 있는 이름 서버는 자신이 담당하는 영역의 정보를 제공합니다. DNS의 리졸버·이름 서버·캐시 역할은 RFC 1034 에서 설명합니다. 그림은 중간의 루트·최상위 도메인 서버 조회를 접은 개념 흐름입니다. 모든 요청이 권한 서버까지 가는 것은 아닙니다. TTL은 재사용 시간의 기준 TTL은 캐시한 레코드를 재사용할 수 있는 시간의 기준입니다. TTL=60 인 답을 시각 0에 받았다면, 다음 표처럼 생각할 수 있습니다. 여기서는 전송 시간, 시계 차이와 별도 캐시 정책은 제외합니다. 경과 시간 이 모형에서의 판단 20초 40초 남음 59초 1초 남음 60초 만료, 새 조회가 필요함 const ttl = 60; for (const elapsed of [20, 59, 60]) { const remaining = Math.max(0, ttl - elapsed); console.log(`${elapsed}:remaining=${remaining}`); } 출력은 20:remaining=40 , 59:remaining=1 , 60:remaining=0 입니다. TTL 계산 모형을 검증했고 실제 도메인 설정은 변경하지 않았습니다. TTL을 낮췄는데 바로 안 바뀌는 이유 이전에 더 긴 TTL로 받은 캐시는 그 이전 답의 시간을 기준으로 남아 있을 수 있습니다. 지금 권한 서버의 TTL을 낮췄다고 이미 배포된 캐시의 시간을 소급해서 줄이는 것은 아닙니다. 브라우저, 운영체제, 리졸버 등 어느 계층의 답인지도 구분해야 합니다. TTL을 “전체 인터넷이 이 시각에 바뀐다”는 약속으로 보지 않는 편이 좋습니다. 오류 응답 캐시나 만료된 답의 제한적인 제공처럼 추가 규칙도 있습니다. 이 글은 기본 정상 조회만 설명합니다. 확인 문제 캐시가 유효한데 매번 권한 서버에 물어야 할까요? 기존 답의 TTL이 600초였는데 새 TTL을 60초로 바꾸면 기존 캐시도 바로 줄어들까요? TTL 90초인 답을 받은 뒤 35초가 지나면 이 모형에서 얼마나 남을까요? 답과 해설 기본적으로 유효한 캐시를 재사용할 수 있습니다. 아닙니다. 이미 받은 답의 TTL과 새로 받은 답을 구분해야 합니다. 55초입니다. 실제 운영에서는 조회 계층과 추가 정책도 확인합니다. 오늘은 “내가 보는 답은 어느 계층의 캐시인가?”를 적어보겠습니다. 내일은 TTL을 바꾸기 전후의 시간표를 그리고, 일주일 뒤에는 HTTP 캐시와 무엇이 다른지 비교해보면 좋겠습니다. 자료 확인 기준: RFC 1034의 정상 조회와 캐시 개념, 2026-10-04. 합성 TTL 계산을 제외한 실제 DNS 질의·변경 실험은 하지 않았습니다. 예제 검증 환경: Node.js v24.13.1.
1. 메모리 계층 CPU는 결국 메모리에 올라와 있는 명령어를 실행할 뿐이다. 그래서 메모리를 어떻게 구성하고 관리하느냐가 중요하다. (그림 3-8) 메모리 계층은 위에서부터 레지스터, 캐시, 주기억장치, 보조기억장치로 구성된다. 레지스터: CPU 안에 있는 작은 메모리. 휘발성이며 속도가 가장 빠르지만 용량은 가장 적다. 캐시: L1, L2(+L3) 캐시를 말한다. 휘발성이며 빠르고 용량이 적다. 주기억장치: RAM을 말한다. 휘발성이며 속도와 용량 모두 보통이다. 보조기억장치: HDD, SSD를 말한다. 비휘발성이며 느리지만 용량이 크다. 위로 갈수록 빠르고 비싸고 작아지며, 아래로 갈수록 느리고 싸고 커진다. 전부 빠른 메모리로 채우면 좋겠지만 너무 비싸기 때문에 경제성을 이유로 계층을 나눠서 관리하는 것이다. 게임을 켰을 때 뜨는 "로딩 중"도 이 계층 구조 때문이다. 하드디스크(또는 인터넷)에 있는 데이터를 RAM으로 옮기는 작업이 아직 끝나지 않았다는 의미이다. 2. 캐시 캐시는 데이터를 미리 복사해두는 임시 저장소이자, 빠른 장치와 느린 장치 사이의 속도 차이로 생기는 병목을 줄이기 위한 메모리이다. 이렇게 속도 차이를 메우려고 계층과 계층 사이에 두는 계층을 캐싱 계층 이라고 한다. 예를 들어 캐시 메모리와 보조기억장치 사이에 있는 주기억장치는 보조기억장치의 캐싱 계층이라고 볼 수 있다. 쉽게 말해 CPU라는 일꾼이 매번 창고(디스크)까지 가지 않게 책상 위(캐시)에 자주 쓰는 공구를 올려두는 느낌이다. 지역성의 원리 캐시를 직접 설정할 때는 자주 사용하는 데이터를 기준으로 해야 하는데, 그 근거가 되는 것이 바로 지역성이다. 시간 지역성(temporal locality): 최근에 사용한 데이터에 다시 접근하려는 특성 공간 지역성(spatial locality): 최근 접근한 데이터의 주변 공간에 접근하려는 특성 int[] arr = new int[10]; for (int i = 0; i < 10; i++) { arr[i] = i; } 위 코드에서 변수 i 는 반복할 때마다 계속 접근되므로 시간 지역성, 배열 arr 의 요소들은 연속된 공간에 순서대로 접근되므로 공간 지역성의 예시라고 볼 수 있다. 캐시히트와 캐시미스 (그림 3-9) 캐시에서 원하는 데이터를 찾으면 캐시히트 , 캐시에 없어서 주 메모리까지 가서 찾아와야 하면 캐시미스 라고 한다. 캐시히트는 위치도 가깝고 CPU 내부 버스를 통해 동작해서 빠르지만, 캐시미스는 시스템 버스를 거쳐 메모리에서 가져오기 때문에 느리다. 캐시매핑 캐시는 메모리에 비해 매우 작기 때문에 메모리의 데이터를 캐시의 어느 위치에 둘지, 즉 매핑을 어떻게 하느냐가 캐시히트율에 큰 영향을 준다. 직접 매핑(direct mapping): 메모리의 각 블록이 캐시의 정해진 한 위치에만 들어가는 방식. 처리가 빠르지만 같은 위치를 두고 충돌이 자주 발생한다. 연관 매핑(associative mapping): 위치를 정해두지 않고 캐시의 빈 곳 아무 데나 넣는 방식. 충돌은 적지만 찾을 때 모든 블록을 탐색해야 해서 느리다. 집합 연관 매핑(set associative mapping): 위 둘을 합친 방식. 캐시를 여러 집합으로 나누고 메모리 블록이 들어갈 집합은 정해두되, 집합 안에서는 자유롭게 저장한다. 충돌과 탐색 비용 사이에서 균형을 잡은 방식이다. 웹 브라우저의 캐시 소프트웨어적인 캐시의 대표적인 예로 웹 브라우저의 쿠키, 로컬 스토리지, 세션 스토리지가 있다. 주로 사용자 정보나 인증 관련 정보를 저장해두고 서버에 요청할 때 자신을 식별하거나 중복 요청을 막는 용도로 쓰인다. 쿠키: 만료기한이 있는 키-값 저장소. 최대 4KB. document.cookie 로 접근하지 못하도록 httpOnly 옵션을 거는 것이 중요하고, 만료기한은 보통 서버에서 정한다. 로컬 스토리지: 만료기한이 없는 키-값 저장소. 최대 10MB. 브라우저를 닫아도 유지되며 도메인 단위로 저장된다. 클라이언트에서만 수정 가능하다. 세션 스토리지: 만료기한이 없는 키-값 저장소. 최대 5MB. 탭 단위로 생성되며 탭을 닫으면 삭제된다. 클라이언트에서만 수정 가능하다. 로컬/세션 스토리지는 둘 다 HTML5를 지원하지 않는 브라우저에서는 사용할 수 없다. 데이터베이스의 캐싱 계층 (그림 3-10) DB 시스템을 구축할 때도 메인 DB 앞에 레디스(Redis) 같은 인메모리 DB를 캐싱 계층으로 두어 성능을 높이기도 한다. 3. 메모리 관리 운영체제의 핵심 역할 중 하나가 한정된 메모리를 최대한 효율적으로 쓰도록 관리하는 것이다. 가상 메모리 (그림 3-11) 가상 메모리는 실제 사용 가능한 메모리 자원을 추상화해서 사용자에게 매우 큰 메모리처럼 보이게 만드는 기법이다. 가상적으로 주어진 주소를 가상 주소(logical address) , 실제 메모리상의 주소를 실제 주소(physical address) 라고 한다. 가상 주소는 MMU(메모리 관리 장치)에 의해 실제 주소로 변환되기 때문에 개발자는 실제 주소를 신경 쓰지 않고 프로그램을 만들 수 있다. 가상 주소와 실제 주소의 매핑 정보는 페이지 테이블 로 관리하며, 이 변환 속도를 높이기 위해 TLB를 사용한다. TLB: 메모리와 CPU 사이에 있는 주소 변환용 캐시. 페이지 테이블의 일부를 보관해서 CPU가 매번 페이지 테이블까지 가지 않아도 되게 해준다. 페이지 폴트와 스와핑 페이지 폴트(page fault)는 프로세스의 주소 공간에는 있지만 현재 RAM에는 없는 데이터에 접근할 때 발생한다. 이때 당장 쓰지 않는 메모리 영역을 하드디스크로 내보내고, 필요한 데이터를 디스크에서 불러와 메모리처럼 쓰는 것을 스와핑(swapping) 이라고 한다. 스와핑 덕분에 마치 페이지 폴트가 없었던 것처럼 동작할 수 있다. 페이지 폴트가 발생하면 다음 순서로 처리된다. CPU가 물리 메모리에 해당 페이지가 없음을 확인하면 트랩을 발생시켜 운영체제에 알린다. 운영체제는 CPU 동작을 잠시 멈춘다. 운영체제가 페이지 테이블을 확인하고 물리 메모리에 비어 있는 프레임이 있는지 찾는다. 빈 프레임이 없으면 스와핑이 발동된다. 빈 프레임에 해당 페이지를 로드하고 페이지 테이블을 최신화한다. 멈췄던 CPU를 다시 시작한다. 페이지(page): 가상 메모리를 사용하는 최소 크기 단위 프레임(frame): 실제 메모리를 사용하는 최소 크기 단위 스레싱 (그림 3-12) 스레싱(thrashing)은 페이지 폴트율이 너무 높아져 컴퓨터 성능이 심각하게 떨어지는 현상이다. 메모리에 너무 많은 프로세스가 올라가면 스와핑이 계속 일어나고, CPU는 스와핑을 기다리느라 놀게 된다. 그러면 운영체제는 "CPU가 한가하네?"라고 착각해서 프로세스를 더 올리고, 그 결과 페이지 폴트가 더 늘어나는 악순환이 반복된다. 일꾼이 바쁜 게 아니라 짐 나르느라 바쁜 건데 일을 더 주는 셈이다 하드웨어적으로는 메모리를 늘리거나 HDD를 SSD로 바꾸는 방법이 있고, 운영체제 차원에서는 다음 두 가지 방법이 있다. 작업 세트(working set): 프로세스의 과거 사용 이력(지역성)을 바탕으로 자주 쓰는 페이지 집합을 만들어 미리 메모리에 올려두는 방법. 탐색 비용과 스와핑을 줄일 수 있다. PFF(Page Fault Frequency): 페이지 폴트 빈도에 상한선과 하한선을 두는 방법. 상한선에 도달하면 프레임을 늘리고, 하한선에 도달하면 프레임을 줄인다. 4. 메모리 할당 메모리에 프로그램을 할당할 때는 시작 위치와 할당 크기를 기준으로 하며, 크게 연속 할당과 불연속 할당으로 나뉜다. 연속 할당 (그림 3-13) 메모리에 공간을 연속적으로 할당하는 방식으로, 고정 분할 방식과 가변 분할 방식이 있다. 고정 분할 방식(fixed partition allocation): 메모리를 미리 나눠두고 관리하는 방식. 융통성이 없고 내부 단편화가 발생한다. 가변 분할 방식(variable partition allocation): 프로그램 크기에 맞게 그때그때 동적으로 메모리를 나누는 방식. 내부 단편화는 없지만 외부 단편화가 발생할 수 있다. 가변 분할 방식은 다시 세 가지로 나뉜다. 최초적합(first fit): 위나 아래부터 탐색하다가 들어갈 수 있는 홀을 찾으면 바로 할당 최적적합(best fit): 프로세스 크기 이상인 홀 중 가장 작은 홀에 할당 최악적합(worst fit): 프로세스 크기와 가장 많이 차이 나는(가장 큰) 홀에 할당 내부 단편화: 나눠진 공간보다 프로그램이 작아서 공간 안에 남는 부분이 낭비되는 현상 외부 단편화: 남은 공간을 다 합치면 충분한데, 조각조각 나뉘어 있어서 프로그램이 들어가지 못하는 현상 (예: 100MB를 55MB, 45MB로 나눴는데 70MB 프로그램이 들어가지 못함) 홀(hole): 할당할 수 있는 비어 있는 메모리 공간 불연속 할당 현대 운영체제가 사용하는 방식으로, 메모리를 연속적으로 할당하지 않는다. 페이징(paging): 메모리를 동일한 크기의 페이지(보통 4KB)로 나누고, 프로그램마다 페이지 테이블을 두어 서로 다른 위치에 할당하는 방식. 홀 크기가 제각각인 문제는 없어지지만 주소 변환이 복잡해진다. 세그멘테이션(segmentation): 페이지라는 크기 단위가 아닌 코드, 데이터, 스택, 힙이나 함수 같은 의미 단위(세그먼트)로 나누는 방식. 공유와 보안 측면에서 유리하지만 홀 크기가 균일하지 않은 문제가 생긴다. 페이지드 세그멘테이션(paged segmentation): 공유나 보안은 의미 단위인 세그먼트로 나누고, 물리 메모리는 페이지로 나누는 방식. 둘의 장점을 합친 형태이다. 5. 페이지 교체 알고리즘 메모리는 한정되어 있어서 스와핑은 피할 수 없지만, 최대한 적게 일어나도록 설계해야 한다. 이때 어떤 페이지를 내보낼지 정하는 것이 페이지 교체 알고리즘이다. 오프라인 알고리즘 앞으로 가장 먼 미래에 참조될 페이지를 교체하는 알고리즘으로, 이론상 가장 좋은 방법이다. 하지만 미래에 어떤 페이지가 쓰일지는 알 수 없기 때문에 실제로는 사용할 수 없고, 다른 알고리즘의 성능을 비교하는 기준으로 쓰인다. FIFO(First In First Out) 가장 먼저 들어온 페이지를 가장 먼저 교체하는 방식이다. LRU(Least Recently Used) (그림 3-14) 참조된 지 가장 오래된 페이지를 교체하는 방식이다. '오래됨'을 판단하기 위해 페이지마다 계수기나 스택을 둬야 한다는 단점이 있다. LRU를 코드로 구현할 때는 보통 해시 테이블 + 이중 연결 리스트 를 사용한다. 이중 연결 리스트는 한정된 메모리(캐시) 자체를 나타내고, 해시 테이블은 리스트에서 원하는 노드를 O(1)로 빠르게 찾기 위해 쓴다. 참고로 Java에서는 LinkedHashMap 이 내부적으로 이 구조를 갖고 있어서 간단하게 구현할 수 있다. import java.util.LinkedHashMap; import java.util.Map; public class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int capacity; public LRUCache(int capacity) { // accessOrder = true → 접근할 때마다 해당 요소를 맨 뒤로 이동 super(capacity, 0.75f, true); this.capacity = capacity; } // 크기를 넘으면 가장 오래 참조되지 않은 요소(맨 앞)를 제거 @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } public static void main(String[] args) { LRUCache<Integer, Integer> cache = new LRUCache<>(3); int[] refs = {1, 3, 0, 3, 5, 6, 3}; for (int r : refs) { cache.put(r, r); System.out.println(cache.keySet()); } } } /* [1] [1, 3] [1, 3, 0] [1, 0, 3] [0, 3, 5] [3, 5, 6] [5, 6, 3] */ 출력 결과에서 오른쪽일수록 최근에 참조된 페이지이며, 4개째가 들어올 때마다 가장 왼쪽(가장 오래된) 페이지가 빠지는 것을 볼 수 있다. NUR(Not Used Recently) (그림 3-15) LRU를 발전시킨 알고리즘으로 clock 알고리즘이라고도 부른다. 각 페이지에 참조 비트를 두고 1은 최근에 참조됨, 0은 참조되지 않음을 의미한다. 시계 방향으로 돌면서 0인 페이지를 찾으면 그 페이지를 교체하고 해당 비트를 1로 바꾼다. LFU(Least Frequently Used) 참조 횟수가 가장 적은 페이지, 즉 가장 덜 사용된 페이지를 교체하는 방식이다.
이 글에서 다룰 주제 DNS·TCP·Service·Gateway·메시·애플리케이션을 나눠 실패 지점 찾기 404·502·503·504와 Envoy response flag를 함께 읽기 MTU·conntrack·SNAT·종료 중 연결, 가설을 검증하는 장애 실습 주요 단어 · EndpointSlice · SNI · Host · response flag · MTU · conntrack · draining · readiness 목표 — 명령을 많이 외우는 대신, 각 명령이 어떤 가설을 확인하는지 설명한다. 아래 사례는 실제 사고 회고가 아니라 원리 학습을 위해 구성한 가상 장애다. “쿠버네티스 네트워크가 안 됩니다”라는 보고에는 너무 많은 가능성이 들어 있다. 이름을 못 찾은 것인지, TCP 연결을 못 만든 것인지, 프록시가 거절했는지, 애플리케이션이 오류를 반환했는지 먼저 나눠야 한다. 무작정 Pod를 재시작하면 증거가 사라지고 잠깐 회복했다가 같은 문제가 반복될 수 있다. 1. 첫 기록: 출발점과 실패한 단계를 고정하기 요청을 보낸 위치, 목적지 이름과 포트, 프로토콜, 발생 시각, 영향받는 버전·노드·사용자 범위를 적는다. localhost , 클러스터 내부 Pod, 외부 브라우저는 서로 다른 네트워크 경로다. 같은 URL이라도 출발점에 따라 DNS와 인증 정책이 달라질 수 있다. 진단 지도는 반드시 한 방향으로만 따라야 하는 실행 절차가 아니다. 확인된 증거에 따라 해당 계층으로 돌아가 가설을 수정한다. 최소 시작 명령은 조회 위주로 구성한다. <pod-name> 등은 실제 조회 결과로 바꾼다. 이 글의 명령은 공식 문서 기반이며 클러스터에서 실행한 결과는 아니다. kubectl config current-context kubectl -n bookshop get pods -o wide kubectl -n bookshop get svc kubectl -n bookshop get endpointslices kubectl -n bookshop get events --sort-by=.metadata.creationTimestamp Pod가 Running 인 것과 요청을 받을 준비가 된 것은 다르다. READY , 재시작 횟수, Pod IP, 어느 노드에 있는지까지 함께 본다. Service가 존재한다는 사실도 정상 endpoint가 있다는 보장은 아니다. 2. DNS 실패: 이름 문제와 연결 문제를 분리하기 Could not resolve host 라면 아직 목적지 TCP 연결을 시작하지 못했을 가능성이 크다. 실제 호출 Pod에서 /etc/resolv.conf 를 확인하고 짧은 이름, Namespace를 포함한 이름, 전체 도메인을 비교한다. kubectl -n bookshop exec <client-pod> -- cat /etc/resolv.conf # 아래 nslookup은 해당 도구가 설치된 진단 Pod에서 실행 kubectl -n bookshop exec <dns-tools-pod> -- \ nslookup productpage.bookshop.svc.cluster.local kubectl -n kube-system get pods -l k8s-app=kube-dns kubectl -n kube-system get svc kube-dns cluster.local 은 흔한 기본값이므로 실제 cluster domain이 다르면 바꿔야 한다. kube-dns 라벨과 Service 이름도 배포 구성에 맞는지 확인한다. 도구가 없는 distroless 컨테이너에서 nslookup: not found 가 나온 것은 DNS 장애 증거가 아니다. 승인된 진단 이미지를 사용하되 원래 호출자와 동일한 정책 조건인지 구분한다. Kubernetes DNS 진단 NXDOMAIN 은 질의한 이름이 없다는 DNS 응답이고, SERVFAIL 은 resolver가 처리를 완료하지 못했다는 응답이다. timeout은 응답을 받지 못한 상황이다. 셋을 모두 “DNS 서버가 죽음”으로 묶으면 잘못된 조치를 한다. 특정 이름만 실패하는지, 클러스터 내부 이름과 외부 이름이 함께 실패하는지로 문제 범위를 좁힌다. 가상 사례: NetworkPolicy를 배포한 뒤 Service 이름만 실패하고 같은 Pod IP로는 연결된다. 이때 CoreDNS를 재시작하기 전에 호출자의 DNS egress를 확인한다. 데이터 요청 목적지 허용과 DNS 허용은 별개였음을 4편에서 배웠다. 3. Pod IP는 되는데 Service는 안 된다면 서로 다른 요청 경로의 성공·실패를 비교한다. 직접 Pod IP 호출이 메시 정책 등을 완전히 우회한다고 가정하지 않는다. kubectl -n bookshop get service productpage -o yaml kubectl -n bookshop get pods -l app=productpage --show-labels kubectl -n bookshop get endpointslices \ -l kubernetes.io/service-name=productpage -o yaml 읽는 순서는 Service selector → 선택된 Pod → EndpointSlice 주소·포트·condition → 실제 수신 포트 다. port: 80 , targetPort: http 라면 선택된 Pod에 name: http 인 컨테이너 포트가 있는지 확인한다. 애플리케이션 프로세스가 그 포트에서 실제로 수신하는지도 따로 확인한다. containerPort 선언 자체가 프로세스를 열어 주지는 않는다. Endpoint가 비어 있다면 selector·라벨 불일치를 본다. 주소가 있지만 ready: false 라면 readiness 실패 이유를 본다. 개별 Pod IP는 되지만 Service VIP가 안 된다면 서비스 데이터 평면(kube-proxy 또는 대체 구현), 포트 매핑, 정책, 세션 상태로 범위를 좁힌다. Kubernetes Service 진단 kubectl port-forward 가 성공해도 일반 Service 네트워크 경로가 정상이라고 보장하지 않는다. API 서버·kubelet을 통한 터널은 사용자 → LB → Gateway → Service 경로와 다르다. 어떤 경로를 검증했는지 정확히 기록해야 한다. 4. Gateway는 받았는데 404가 난다면 외부 요청은 DNS와 TCP 연결 이후에도 Host·경로·TLS SNI로 나뉜다. HTTPRoute의 hostname이 shop.example.com 인데 IP 주소로 요청하면 다른 Host가 전달되어 경로가 선택되지 않을 수 있다. kubectl -n bookshop get gateways.gateway.networking.k8s.io kubectl -n bookshop get httproutes.gateway.networking.k8s.io -o yaml kubectl -n bookshop describe httproute productpage 객체의 이름은 실제 배포에서 확인한다. Accepted , ResolvedRefs , Gateway의 Programmed 같은 상태와 reason을 읽고, parentRefs·listener·allowedRoutes·backendRefs를 차례로 확인한다. 다른 Namespace를 참조했다면 해당 연결에 필요한 ReferenceGrant 조건도 검토한다. Gateway API HTTPRoute HTTPS 진단에서 -H 'Host: ...' 만 바꿔도 TLS SNI까지 바뀌는 것은 아니다. 이름과 IP를 분리해 테스트하려면 다음처럼 URL의 호스트 이름을 유지하고 접속 IP를 지정할 수 있다. # 203.0.113.10은 문서용 주소다. 실제 Gateway IP로 교체한다. curl --resolve shop.example.com:443:203.0.113.10 \ https://shop.example.com/ -v 이 명령은 정상 인증서 검증을 유지한다. curl -k 로 검증을 끈 결과만 보고 TLS를 고쳤다고 기록하지 않는다. 인증서 SAN에 이름이 포함되는지, 신뢰 체인이 연결되는지, 만료 시간과 실제 제공되는 인증서가 무엇인지 확인한다. curl --resolve 5. 503 하나에도 여러 원인이 있다 HTTP 상태 코드는 출발점이다. 503은 사용 가능한 upstream이 없거나 연결에 실패했거나, 프록시의 처리 한도에 걸렸거나, 앱이 직접 응답했을 수 있다. 어디서 생성한 응답인지 모르면 잘못된 컴포넌트를 수정하게 된다. 관찰 먼저 세울 가설 다음 증거 HTTP 404 + NR 요청에 맞는 HTTP route 없음 Host·경로·listener·route 503 + UH 건강한 upstream 없음 readiness·endpoint·outlier 배제 503 + UF upstream 연결 실패 수신 포트·mTLS·정책·연결 오류 detail 503 + UO upstream overflow connectionPool·pending request·실제 부하 504 + UT upstream 요청 timeout 앱 지연·의존성·timeout budget 이는 Envoy의 대표적인 response flag이며, 프록시 버전과 프로토콜에 따라 세부 결과가 달라질 수 있다. TCP 연결의 NR 는 맞는 filter chain이 없다는 뜻일 수도 있으며, 이 경우 HTTP 404가 반환된다고 가정하지 않는다. 상태 코드만으로 flag를 추정하지 말고 access log의 response_flags , response_code_details , upstream 주소·연결 오류 정보를 함께 읽는다. 애플리케이션이 만든 503은 같은 표의 원인이라고 단정하지 않는다. Envoy access log 공식 설명 sidecar 구성에서는 다음 도구로 선언한 설정과 실제 프록시 상태를 비교할 수 있다. istioctl analyze -n bookshop istioctl proxy-status istioctl proxy-config routes <productpage-pod> -n bookshop istioctl proxy-config clusters <productpage-pod> -n bookshop istioctl proxy-config endpoints <productpage-pod> -n bookshop kubectl -n bookshop logs <productpage-pod> -c istio-proxy --since=10m analyze 는 설정의 문제를 찾는 출발점이고 모든 런타임 연결을 보장하는 테스트가 아니다. proxy-status 로 동기화 상태를 보고, route가 실제로 어느 cluster를 선택하는지, 그 cluster에 어느 endpoint가 들어 있는지 이어 확인한다. ambient에서는 sidecar 컨테이너가 없으므로 ztunnel·waypoint 진단 경로를 사용한다. 없는 istio-proxy 로그를 찾는 것으로 시간을 쓰지 않는다. Istio 프록시 진단 6. 작은 요청은 되는데 큰 응답이 멈춘다면 이때는 MTU와 경로상의 패킷 처리도 후보에 올린다. MTU 는 한 링크에서 보낼 수 있는 패킷 크기의 상한이다. overlay 캡슐화가 헤더를 더하면 내부 패킷에 쓸 수 있는 크기가 줄어든다. ICMP 오류 전달이 막혀 Path MTU Discovery가 제대로 동작하지 않는 상황도 가설이 될 수 있다. 증상만으로 MTU를 확정하지 않는다. 큰 응답의 서버 처리 지연, 프록시 body limit, 업로드 제한도 비슷한 현상을 만든다. 동일 노드/서로 다른 노드, 작은/큰 payload, overlay/외부 egress 경로의 차이를 비교하고 네트워크 담당자와 인터페이스 MTU·재전송을 확인한다. 모든 노드의 MTU를 임의 값으로 낮추는 조치부터 하지 않는다. Calico MTU 구성 원리 같은 노드에서는 성공하고 다른 노드에서만 실패한다면 노드 간 라우팅·캡슐화·방화벽과 해당 노드 CNI 상태를 우선 조사할 이유가 생긴다. 이 역시 조사 우선순위이지 결론은 아니다. 특정 버전 Pod가 특정 노드에만 배치된 상황처럼 애플리케이션 변수가 섞일 수 있다. 7. 부하가 올라갈 때만 실패한다면: 연결도 유한한 자원이다 conntrack 은 Linux에서 연결 상태를 추적하는 기능이고, SNAT 은 출발지 주소·포트를 변환하는 처리다. 노드·NAT 장치의 연결 추적과 포트 자원이 포화되면 애플리케이션 CPU가 여유로워도 새 연결이 실패할 수 있다. 연결 수뿐 아니라 짧은 연결 생성률, keep-alive, 유휴 timeout, 동일 목적지 집중도를 함께 본다. 여기에 애플리케이션 connection pool과 프록시 connection pool도 있다. “DB 연결 한도”와 “Envoy upstream 연결 한도”는 서로 다른 자원이다. 부하가 오를 때 UO 가 보이면 무조건 한도를 올리기 전에 하류 서비스가 더 받을 능력이 있는지 확인한다. 한도를 늘려 overload를 하류로 밀어낼 수 있기 때문이다. 설명용 가정으로 호출이 초당 100개이고 장애 시 매번 총 3회 시도한다면 하류 시도량은 최대 초당 300개까지 늘 수 있다. 여러 계층이 중첩 재시도하면 증폭은 더 커진다. 6편의 timeout·retry budget과 함께 읽어야 한다. 큐 길이, 실제 active connection, 재시도, 오류와 지연이 같은 시점에 어떻게 변했는지 비교한다. Istio circuit breaking 8. 배포할 때만 끊긴다면: 시작보다 종료를 살펴보기 새 Pod가 Ready가 되고 옛 Pod가 종료되는 동안에는 endpoint 상태 변화와 앱 종료, 프록시 설정 전파가 동시에 일어난다. 긴 HTTP/2·WebSocket 연결은 기존 Pod와 계속 연결되어 있을 수 있다. Service의 endpoint 목록이 바뀌었다고 이미 열린 연결이 다른 Pod로 자동 이동하는 것은 아니다. readiness는 신규 요청 대상으로 삼아도 되는지 알려 준다. 종료 중 endpoint는 terminating 상태와 ready · serving 조건을 함께 고려한다. 앱은 SIGTERM 이후 신규 작업을 멈추고 진행 중 작업을 정리하는 graceful shutdown을 구현해야 한다. Pod 종료와 endpoint 흐름 preStop 과 terminationGracePeriodSeconds , 애플리케이션 종료 처리, 프록시 draining 시간을 같이 설계한다. preStop 실행 시간도 종료 유예 시간 안에 포함되므로 긴 sleep만 넣는다고 모든 연결 문제가 해결되는 것은 아니다. 실제 전파 지연과 요청의 최대 지속 시간을 측정해 선택해야 한다. 컨테이너 lifecycle hook 가상 사례: v2 배포 직후 10초 동안 reset이 늘어난다. 새 버전의 코드 오류뿐 아니라 옛 Pod가 진행 중 요청을 기다리지 않고 종료하는지도 조사한다. 오류가 어느 Pod·노드·종료 이벤트와 겹치는지 trace·로그·배포 기록의 시계를 맞춘다. 9. 학습용 장애 실습: 한 번에 변수 하나를 바꾸기 운영이 아닌 전용 클러스터에서 다음 실험을 할 수 있다. 변경 전 manifest를 보관하고, 실험 하나를 복구한 뒤 다음 실험으로 넘어간다. 아래는 실제 실행 결과가 아닌 실험 설계 다. 바꾸는 변수 관찰할 것 복구 확인 Service selector의 라벨 EndpointSlice의 선택 결과 원래 selector 복원 후 실제 요청 NetworkPolicy의 DNS 허용 이름 호출과 직접 IP 호출의 차이 DNS·앱 연결 각각 성공 HTTPRoute hostname Gateway 응답·route 상태 원래 이름으로 경로 일치 reviews의 의도적인 지연 timeout·retry·trace span 지연 제거 후 부하·오류율 회복 앱 종료 처리 배포 중 reset과 종료 시각 반복 배포에서도 연결 정리 실험 기록은 가설 → 변경 → 관찰 → 해석 → 복구 로 남긴다. “503이 났다”보다 “reviews endpoint가 비어 있고 gateway에 UH가 기록되며 selector를 복구한 뒤 요청이 회복됐다”가 더 강한 설명이다. 다만 한 번의 재현이 모든 장애의 원인을 증명하지는 않는다. 10. 운영 설계로 되돌아가기 진단에서 얻은 지식은 다음 배포 설계를 바꿔야 한다. 서비스 간 허용 경로는 정책으로, 기대 지연은 timeout budget으로, 배포 안전성은 readiness와 종료 처리로, 실패 판단은 메트릭과 trace 기준으로 남긴다. 버전 업그레이드 전에는 Kubernetes·CNI·Gateway 구현·Istio·cert-manager의 호환 범위를 각 공식 문서에서 확인한다. 10편을 읽었다면 서점 화면 하나의 요청에 대해 다음 질문을 이어 답할 수 있어야 한다. 이름은 누가 해석하는가, 대상 Pod는 누가 선택하는가, 네트워크와 사용자 권한은 어디서 검사하는가, 인증서는 누가 갱신하는가, 실패 시 어느 신호를 볼 것인가. 이 연결을 설명할 수 있는 것이 도구 이름을 많이 아는 것보다 실제 장애 대응에 도움이 된다. 자료·예제 기준 — 2026-10-04에 링크한 공식 진단 문서를 확인했다. 명령은 미실행, 장애 사례·수치는 설명용이다. Envoy flag 표는 명시한 버전 문서의 대표 의미를 인용했으며 배포 버전의 문서·실제 로그를 우선한다. 모든 그림은 자체 제작 개념도다. 쿠버네티스 네트워크 10편 시리즈 TCP/IP부터 CNI까지, Pod가 통신하는 원리 Service·EndpointSlice·DNS Ingress와 Gateway API, 외부 진입과 연결 NetworkPolicy와 연결 허용 설계 Istio 구조: Sidecar·Ambient·CNI 카나리·재시도·서킷 브레이커 mTLS·JWT·인증과 인가 TLS와 cert-manager의 발급·갱신 Kiali·Jaeger·OpenTelemetry DNS부터 TLS까지 장애 진단 그림과 아이콘 출처 · 도식은 이 시리즈를 위해 직접 제작했다. Kubernetes 리소스 아이콘은 Kubernetes Icons Set (Kubernetes Authors / contributors, CC BY 4.0 )의 원본을 비율대로 축소해 배치했다. 제품 로고는 CNCF Artwork 와 Kiali 공식 자산 을 식별 목적으로 사용했다. 각 이름과 로고의 상표권은 해당 권리자에게 있으며, 공식 후원이나 인증을 뜻하지 않는다. 확인일: 2026-10-04.
이 글에서 다룰 주제 Kiali·Prometheus·Jaeger의 역할과 수집·조회 경로 ServiceMonitor·PodMonitor, Istio 메트릭과 오류율·지연 쿼리 Trace·Span·Context propagation·Sampling, 관측 데이터가 비는 이유 주요 단어 · Kiali · Jaeger v2 · OpenTelemetry · OTLP · RED · p95 · Trace ID · Sampling 선수 지식과 목표 — Service와 Istio의 데이터 평면을 이해했다면, 이번에는 “어느 연결에서 문제가 시작되었는가”를 관측 신호로 좁힌다. 읽고 나면 서비스 지도·메트릭·트레이스를 서로 다른 근거로 해석할 수 있다. 서점 화면이 느리다는 알림이 왔다. productpage Pod의 CPU는 정상이다. 이 정보만으로 productpage가 정상이라고 결론 낼 수 있을까? reviews의 응답을 기다리거나, ratings 호출을 여러 번 재시도하거나, 연결 풀에서 순서를 기다릴 수도 있다. CPU 한 개의 그래프로는 요청이 지나간 경로를 알 수 없다. 1. 관측 지도: 요청과 관측 데이터는 다른 길로 이동한다 앞선 전체도와 같은 배치에서 관측 대상을 강조한 개념도다. 애플리케이션 요청이 Kiali나 Jaeger를 거쳐 전달되는 것은 아니다. Observability — 외부로 내보낸 신호를 통해 시스템 내부에서 무엇이 일어났는지 질문하고 설명하는 능력이다. Kiali는 서비스 메시의 연결 관계와 상태를 보는 콘솔이다. Prometheus의 메트릭과 Kubernetes·Istio 설정을 함께 해석한다. Jaeger는 분산 요청의 트레이스를 저장·조회하는 도구다. Grafana는 메트릭 등 여러 데이터 소스를 조회하는 대시보드로 연결할 수 있다. Kiali 자체를 메트릭 저장소로 생각하면 “Kiali가 비었으니 요청이 없었다”는 잘못된 결론에 도달한다. Kiali 아키텍처 화살표는 범례에 적힌 요청·전송 방향이다. Prometheus가 메트릭 endpoint를 조회하는 것과 앱이 트레이스를 보내는 것을 구분한다. Kiali 화면의 연결이 비었다면 먼저 조회 Namespace와 시간 구간을 확인한다. 그 구간에 트래픽을 발생시켰는지, Istio 메트릭이 수집되는지, Prometheus endpoint에 Kiali가 접근할 수 있는지 순서대로 본다. “선이 없다”는 관찰에는 요청 없음과 수집 실패라는 서로 다른 가능성이 포함된다. 1.1 제품 로고는 역할을 찾는 표식이다 이 시리즈의 제품별 표식이다. 같은 로고를 썼다는 이유만으로 동일 프로세스나 필수 구성요소라는 뜻은 아니다. 아래 그림에서는 수집·저장·조회 화살표를 따라 실제 역할을 구분한다. Kubernetes 리소스 아이콘과 제품 로고도 서로 다른 의미로 사용한다. 2. 메트릭: 어떤 신호를 어느 위치에서 계산하는가 RED — 요청량(Rate), 오류(Errors), 지연(Duration)을 함께 보는 서비스 관측 관점이다. Istio HTTP 메트릭 중 istio_requests_total 은 요청 수 counter이고, istio_request_duration_milliseconds 는 요청 지연 분포다. source 와 destination reporter는 관측한 프록시 위치를 나타낸다. 두 reporter를 구분 없이 더하면 같은 호출을 중복 집계할 수 있다. TLS가 해독되지 않는 통과 트래픽이나 L4만 관측하는 구간에서는 같은 HTTP 지표가 나오리라고 가정하면 안 된다. Istio 표준 메트릭 다음은 Prometheus가 Istio HTTP 메트릭을 수집 중일 때 사용하는 미실행 쿼리 예제 다. 대상은 bookshop Namespace의 reviews Service로 들어온 요청이고 reporter는 수신 측으로 고정한다. Bookinfo처럼 Deployment가 버전별로 나뉘면 destination_workload 는 reviews-v1 , reviews-v2 등이 될 수 있으므로 Service 이름과 workload 이름을 혼동하지 않는다. 다음처럼 실제 라벨 조합부터 조회하고, 아래 식의 Service 이름이 수집값과 맞는지 확인한다. count by (destination_service_name, destination_service_namespace, destination_workload) ( istio_requests_total{ reporter="destination", destination_workload_namespace="bookshop" } ) 이 탐색식의 값은 요청 수가 아니라 해당 라벨 조합의 시계열 개수다. 목적은 이름 확인이다. 버전별 Service를 별도로 사용한다면 실제 Service 이름에 맞춰 필터를 바꾼다. sum(rate(istio_requests_total{ reporter="destination", destination_service_namespace="bookshop", destination_service_name="reviews" }[5m])) 결과는 최근 5분 표본으로 계산한 평균 요청/초 다. counter의 누적값을 그대로 그리면 재시작과 시간 경과가 섞이므로 rate 를 먼저 계산한 뒤 합친다. 그래프를 30분 구간·30초 step으로 표시하면 각 점은 그 시점의 직전 5분을 요약한 값이다. 순간 최대 요청량과 같은 뜻은 아니다. HTTP 5xx 비율은 같은 조건의 전체 요청률을 분모로 사용한다. 100 * sum(rate(istio_requests_total{ reporter="destination", destination_service_namespace="bookshop", destination_service_name="reviews", response_code=~"5.." }[5m])) / sum(rate(istio_requests_total{ reporter="destination", destination_service_namespace="bookshop", destination_service_name="reviews" }[5m])) 결과 단위는 %. 설명용 가정으로 초당 200개 중 4개가 5xx라면 2%다. 이는 실측이 아니다. 요청이 전혀 없으면 분모가 0이거나 시계열이 없을 수 있다. 전체 요청이 있어도 5xx 시계열이 아직 생성되지 않았다면 분자가 비어 식 전체가 빈 결과일 수 있다. 이런 경우 전체 요청의 수집이 확인된 구간에 한해서만 오류 0으로 해석하거나 보정한다. 이를 무조건 0% 성공으로 채우지 않는다. 트래픽 부재·수집 실패·표본 부족을 구분해야 한다. 또한 수신 프록시에 도달하기 전에 실패한 연결은 이 분모에 안 들어갈 수 있어, 호출자와 게이트웨이 지표도 함께 본다. gRPC는 별도 주의가 필요하다. HTTP 상태가 200이어도 gRPC status가 실패일 수 있다. 위 쿼리는 HTTP 5xx 질문에 답하며 모든 업무 실패율을 대표하지 않는다. 업무 오류·gRPC 상태 기준은 서비스의 성공 정의에 맞게 따로 설계한다. 3. p95: 평균이 숨기는 느린 요청을 읽기 classic histogram이 노출된 환경에서는 다음처럼 95번째 백분위수를 추정한다. histogram_quantile( 0.95, sum by (le) ( rate(istio_request_duration_milliseconds_bucket{ reporter="destination", destination_service_namespace="bookshop", destination_service_name="reviews" }[5m]) ) ) 단위는 메트릭 이름 그대로 밀리초 다. le 는 bucket 상한을 나타내므로 집계에서 보존한다. workload별 결과가 필요하면 sum by (le, destination_workload) 처럼 목적에 맞는 그룹도 남긴다. 현재 식은 지정한 reviews Service의 버전들을 합친 지연 분포를 계산한다. p95가 800ms라는 값은 관측 분포상 약 95%가 그보다 빠르다는 의미이지 모든 요청이 800ms 걸렸다는 뜻이 아니다. histogram bucket 경계에 따른 추정 오차도 있다. Pod별 p95를 평균내는 계산은 전체 요청의 p95와 다르다. native histogram을 사용하면 식과 저장 형태가 달라질 수 있으므로 이 예제는 _bucket 이 있는 classic histogram에 한정한다. Prometheus histogram 함수 실무에서는 p95 하나만으로 배포를 판단하기보다 요청량·오류율·p99·버전별 분포를 같이 본다. 예를 들어 표본 10개인 v2와 표본 10만 개인 v1의 p95를 같은 확신으로 비교할 수 없다. 비교 구간의 부하와 요청 종류가 바뀌면 지연 변화의 원인도 달라진다. 4. ServiceMonitor·PodMonitor: 대상을 찾는 선언과 수집 결과 구분 ServiceMonitor / PodMonitor — Prometheus Operator가 읽어 수집 대상을 구성하는 CRD다. 전자는 Service의 라벨을, 후자는 Pod의 라벨을 기준으로 대상을 찾는다. 선택은 보통 두 번 일어난다. Prometheus 리소스가 Monitor 객체를 고르고, 그 Monitor가 Service 또는 Pod를 고른다. 그래서 ServiceMonitor 파일을 만들었는데 Targets에 나타나지 않는다면 serviceMonitorSelector , serviceMonitorNamespaceSelector , Monitor의 대상 selector와 Namespace 범위를 차례로 확인한다. ServiceMonitor endpoint의 port 는 일반적으로 Service 포트의 이름 이다. 앱 컨테이너 포트 숫자나 라벨과 혼동하지 않는다. Prometheus Operator 시작 안내 또한 애플리케이션 메트릭, Envoy 데이터 평면 메트릭, istiod 제어 평면 메트릭은 관측 대상이 다르다. istiod만 잘 수집한다고 reviews의 HTTP 오류율이 생기지 않는다. sidecar의 메트릭 병합 기능을 사용하는지, 프록시 endpoint를 직접 scrape하는지에 따라 포트와 경로도 달라진다. 기존 수집 설정과 새 Monitor를 중복 적용하면 중복 시계열·부하가 생길 수 있다. Istio Prometheus 통합 “Targets가 UP”은 수집 endpoint 응답 성공이라는 좁은 사실이다. 필요한 istio_requests_total 이 존재하고 시간 구간에 샘플이 들어오는지까지 확인해야 원하는 관측이 완성된다. 5. Trace와 Span: 느린 한 요청을 분해하기 Trace — 하나의 작업이 여러 서비스를 거치는 흐름을 연결한 기록이다. Span 은 그 안의 개별 작업 구간으로 시작·종료 시각과 속성을 가진다. 가상의 trace가 다음처럼 보인다고 하자. productpage의 전체 처리는 900ms, reviews 호출은 820ms, 그 안의 ratings 호출은 700ms다. 이 세 시간을 더해 2,420ms라고 해석하면 안 된다. 부모 span은 자식이 실행된 시간을 포함할 수 있기 때문이다. 병렬 호출이 있다면 단순 합이 아니라 전체 완료를 늦춘 경로를 살펴야 한다. 이 경우 ratings가 유력한 조사 대상이지만 “ratings CPU가 원인”이라는 결론까지는 나오지 않는다. ratings 내부 DB 호출, 연결 대기, 스레드 대기처럼 더 세부적인 span이나 로그·프로파일이 필요하다. 네트워크 프록시가 만든 span만으로 함수 내부의 시간을 모두 설명할 수는 없다. 동일 trace의 관계를 이어 주는 것은 시간의 근접성이 아니라 전파된 context다. 프록시가 서로 다른 앱의 업무 인과관계를 자동 추론하지 않는다. Istio 프록시는 span을 보낼 수 있지만, 애플리케이션은 incoming request와 그로 인해 발생한 outgoing request 사이에 context를 전달해야 한다. 설정한 전파 형식에 맞게 W3C의 traceparent · tracestate 또는 B3 헤더와 SDK instrumentation을 확인한다. Istio가 요구하는 x-request-id 전파도 확인하되, 이 값만 전달하는 것으로 trace context 전파를 대신할 수는 없다. 중간 서비스가 새 Trace ID를 만들면 화면에 여러 개의 끊어진 trace가 나타날 수 있다. Istio 추적 개요 , OpenTelemetry context propagation 6. Jaeger v2: 옛 Agent 그림과 현재 수집 경로 구분 Jaeger v2는 OpenTelemetry Collector 프레임워크를 기반으로 한다. collector는 span을 받고 저장하며, query는 저장된 trace를 UI와 API로 조회한다. all-in-one은 역할을 한 프로세스에 모은 배포 형태다. all-in-one이라는 이름과 메모리 저장은 같은 개념이 아니다. 개발용 메모리 저장 구성을 쓰면 재시작 시 데이터가 사라질 수 있으므로 운영의 보존 기간·저장 용량·복구 요구에 맞는 저장소 구성이 필요하다. Jaeger v2 아키텍처 구성 예를 말로 읽으면 앱/프록시 → OTLP 수신 → 처리 → 저장 → query 조회 다. OTLP gRPC와 HTTP는 다른 전송 방식이며 흔히 각각 4317과 4318을 쓰지만, Service의 실제 노출 포트를 확인해야 한다. 16686 같은 UI 포트로 span을 보내는 것은 목적지가 잘못된 것이다. Jaeger API 별도의 OpenTelemetry Collector는 여러 신호를 모으거나 공통 속성 추가·필터링·샘플링을 집중할 때 유용하다. Jaeger 앞에 반드시 추가해야 하는 필수 부품은 아니다. Kafka 버퍼도 모든 배포의 필수 단계가 아니라 순간 유입량과 저장소 처리량 사이를 완충할 필요가 있을 때 검토한다. 구성요소를 늘리면 그만큼 대기·유실·재시도·저장 공간을 관측해야 할 곳도 늘어난다. 구버전 강의의 Jaeger Agent·Thrift·환경변수 설정을 현재 v2 설정에 그대로 섞지 않는다. 제품 버전과 함께 사용 중인 receiver·exporter·storage 설정을 확인한다. Kiali에서 Jaeger를 연결할 때도 ingestion endpoint가 아니라 해당 연동이 요구하는 query endpoint와 API 지원 을 확인한다. Jaeger 구성 , Kiali Jaeger 연동 7. Sampling: trace가 없다는 사실의 해석 Sampling — 모든 요청의 trace를 저장하는 대신 정책에 따라 일부를 선택하는 과정이다. Head sampling은 요청 초기에 선택한다. 비용을 줄이기 쉽지만 뒤늦게 발생할 오류를 미리 알 수 없다. Tail sampling은 여러 span을 받은 뒤 결과를 보고 고를 수 있어 느린 요청이나 오류 보존에 유리하지만 버퍼 메모리·판정 대기·동일 trace의 span을 모으는 설계가 필요하다. OpenTelemetry sampling 설명용으로 독립적인 1% 확률 샘플링을 가정하면, 요청 100개 중 정확히 하나가 남는다고 보장되지 않는다. 희귀 장애는 샘플에서 빠질 수 있다. 그러므로 오류율 메트릭은 상승했지만 Jaeger에 사례가 없다고 오류가 없었다고 결론 내리지 않는다. trace가 비는 이유는 sampling만이 아니다. exporter endpoint·TLS·권한 문제, Collector drop, queue 포화, 저장소 장애, 잘못된 시간 범위와 서비스명도 확인한다. “앱이 span을 생성했다 → Collector가 받았다 → exporter가 전송했다 → 저장됐다 → query가 조회했다”를 단계별로 검증한다. 8. 하나의 장애를 세 화면으로 좁히기 가상의 상황은 “reviews v2를 배포한 뒤 화면 지연 상승”이다. Kiali에서는 어느 edge와 버전에 오류가 집중되는지 본다. 메트릭에서는 같은 시간·workload·reporter로 요청량과 오류율, 지연을 비교한다. Jaeger에서는 해당 구간의 느린 trace를 선택해 reviews 자체 처리와 ratings 대기를 구분한다. 여기서 reviews v2의 ratings client가 context를 전달하지 않으면 가장 필요한 호출이 trace에서 끊겨 보일 수 있다. 이는 네트워크 장애가 아니라 관측 계측의 누락일 수도 있다. 데이터가 뒷받침하는 범위 안에서 가설을 세우고, 다음 확인 명령을 선택한다. 10편에서는 이 관측을 DNS·TCP·Service·Gateway·메시 정책의 진단 순서와 연결한다. 도구를 설치했다는 사실보다 중요한 것은 각 화면이 답할 수 있는 질문과 답할 수 없는 질문을 아는 것이다. 자료·예제 기준 — 2026-10-04에 링크한 공식 문서의 역할·메트릭·설정 의미를 확인했다. Jaeger는 v2 구조를 설명한다. PromQL은 공식 지표·문법 기반·실제 데이터 미실행 예제이며 수치는 설명용이다. 기존 Observability 시리즈 와 함께 읽으면 신호별 역할을 복습할 수 있다. 쿠버네티스 네트워크 10편 시리즈 TCP/IP부터 CNI까지, Pod가 통신하는 원리 Service·EndpointSlice·DNS Ingress와 Gateway API, 외부 진입과 연결 NetworkPolicy와 연결 허용 설계 Istio 구조: Sidecar·Ambient·CNI 카나리·재시도·서킷 브레이커 mTLS·JWT·인증과 인가 TLS와 cert-manager의 발급·갱신 Kiali·Jaeger·OpenTelemetry DNS부터 TLS까지 장애 진단 그림과 아이콘 출처 · 도식은 이 시리즈를 위해 직접 제작했다. Kubernetes 리소스 아이콘은 Kubernetes Icons Set (Kubernetes Authors / contributors, CC BY 4.0 )의 원본을 비율대로 축소해 배치했다. 제품 로고는 CNCF Artwork 와 Kiali 공식 자산 을 식별 목적으로 사용했다. 각 이름과 로고의 상표권은 해당 권리자에게 있으며, 공식 후원이나 인증을 뜻하지 않는다. 확인일: 2026-10-04.