상태: 초안 (2026-10-17), 코드 읽기. 기준 판 grpc/grpc-go v1.83.2(커밋 030ee8becb20, 로컬 Go 모듈 캐시에서 읽음). 최신 릴리스는 v1.84.0 이라 줄 번호가 조금 다를 수 있습니다. 본문의
파일:줄은 v1.83.2 의 함수 선언 줄입니다. 이 편의 새 실측은 없고, 6편 수치와 대조만 합니다.
목차
1. 한 줄 요약: 회사 조직도
채널은 팀장입니다. 일(호출)을 받으면 직접 하지 않고, 주소록 담당(리졸버)이 준 명단을 배정 담당(밸런서)에게 넘기고, 배정 담당은 직원(서브채널)을 뽑아 두었다가 일이 올 때마다 그날의 배정표(픽커)로 한 명을 고릅니다. 직원은 자기 전화선(HTTP/2 연결) 하나를 갖고 있습니다. 배정표는 통째로 갈아 끼우는 종이라서, 일을 고르는 순간에는 누구와도 잠금을 다투지 않습니다.
2. 구조도
3. 코드 지도
| 경로 | 무엇 | 처음 읽을 곳 |
|---|---|---|
clientconn.go |
채널, 서브채널(addrConn), 유휴 모드, 재시도 조절 |
NewClient(183), addrConn(1240) |
call.go, stream.go |
단항·스트림 호출, 재시도 시도(csAttempt) |
invoke(65), newClientStream(213) |
resolver_wrapper.go, resolver/, internal/resolver/ |
리졸버 포장과 dns·passthrough·unix 구현 | ccResolverWrapper.UpdateState(129) |
balancer_wrapper.go, balancer/ |
밸런서 포장, pick_first·round_robin·ringhash·weightedroundrobin 등 | ccBalancerWrapper.NewSubConn(180) |
picker_wrapper.go |
호출마다 서브채널 고르기, 대기 | pickerWrapper.pick(105) |
internal/transport/ |
HTTP/2 클라이언트·서버, 흐름 제어, keepalive, 제어 버퍼 | http2_client.go, http2_server.go |
rpc_util.go |
5바이트 접두, 압축, 수신 상한, 호출 옵션 | msgHeader(879), parser.recvMsg(785) |
server.go |
서버, 서비스 등록, 디스패치 | Serve(888), handleStream(1800) |
codes/, status/ |
상태 코드와 상태 객체 | codes.go |
health/, reflection/, channelz/, admin/ |
부가 서비스(5·8편) | health/server.go |
xds/, internal/xds/ |
xDS 리졸버·밸런서(5편) | 규모가 커서 나중에 |
4. 핵심 경로 따라가기
4.1 채널 만들기: 아직 아무것도 연결하지 않는다
NewClient 는 옵션을 적용하고 대상 문자열을 해석해 리졸버 빌더를 고른 뒤
(initParsedTargetAndResolverBuilder, 기본 스킴 dns), 유휴 상태를 준비하고 끝납니다.
유휴 상태 준비(initIdleStateLocked)가 리졸버 포장
(newCCResolverWrapper)과 밸런서 포장(newCCBalancerWrapper)을
만들어 두지만, 리졸버는 아직 돌지 않습니다. 첫 호출이 오면 exitIdleMode 가 채널을 CONNECTING 으로 바꾸고
리졸버를 시작합니다. 30분 동안 호출이 없으면 enterIdleMode 가 리졸버·밸런서와 연결을 걷어 내고 다시 유휴로 돌아갑니다.
4.2 주소가 들어오면: 리졸버 → 밸런서 → 서브채널 → 픽커
- 리졸버가 주소를 찾으면
ccResolverWrapper.UpdateState를 부르고, 채널의updateResolverStateAndUnlock이 서비스 설정을 적용한 뒤 (applyServiceConfigAndBalancer) 밸런서에 넘깁니다 (updateClientConnState). - 밸런서(예: round_robin)는 주소마다
NewSubConn을 부르고, 이것이newAddrConnLocked로addrConn을 만듭니다. 이addrConn이 서브채널의 실체입니다. addrConn은resetTransportAndUnlock→tryAllAddrs→createTransport순서로 연결하고, 실패하면 백오프 뒤 다시 시도합니다. 실패하거나 연결이 끊기면 리졸버에ResolveNow를 요청합니다(5편 4.1절).- 서브채널 상태가 바뀔 때마다 밸런서는 새 픽커를 만들어
ccBalancerWrapper.UpdateState로 올리고,pickerWrapper.updatePicker가 픽커를 원자적으로 갈아 끼웁니다. 기다리던 호출들은 이전 픽커에 딸린 채널이 닫히는 것으로 깨어납니다.
왜 이렇게 나눴을까요. 주소를 찾는 일(DNS·xDS·쿠버네티스 API 등)과 고르는 일(첫 번째·돌아가며·가중치·해시)을 서로 모르게 해서 아무 리졸버와 아무 밸런서를
조합할 수 있게 하려는 것입니다. 밸런서 쪽 갱신은 직렬 실행기(grpcsync.CallbackSerializer, balancer_wrapper.go 의 주석)로 차례대로 처리하고, 호출이 지나는 뜨거운 길(pick)은 원자 값 하나만 읽게 해서
두 세계가 서로의 잠금을 기다리지 않게 합니다.
4.3 호출 하나: Invoke 에서 스트림까지
- 생성된 스텁이
ClientConn.Invoke를 부르고, 인터셉터를 거쳐invoke가newClientStream→newClientStreamWithParams로 갑니다. 단항 호출도 내부적으로는 「메시지 하나 보내고 하나 받는 스트림」입니다. - 시도 하나를 만들고(
newAttemptLocked),getTransport가 픽커에서 서브채널을 고릅니다. 고른 서브채널의 전송에csAttempt.newStream→http2Client.NewStream으로 HEADERS 를 보냅니다(3편의createHeaderFields). SendMsg가 직렬화·압축·5바이트 접두를 붙여csAttempt.sendMsg로 쓰고,RecvMsg→recv→parser.recvMsg로 응답을 읽습니다. 실패하면withRetry가 4편의shouldRetry를 보고 새 시도를 만들고, 버퍼에 쌓아 둔 보낸 메시지를 다시 재생합니다.
4.4 픽커: 기다릴까, 바로 실패할까
호출이 왔는데 READY 인 서브채널이 없으면 어떻게 될까요. pickerWrapper.pick 의 한 분기가 답입니다.
pickResult, err := p.Pick(info)
if err != nil {
if err == balancer.ErrNoSubConnAvailable {
continue
}
if st, ok := status.FromError(err); ok {
...
return pick{}, dropError{error: err}
}
// For all other errors, wait for ready RPCs should block and other
// RPCs should fail with unavailable.
if !failfast {
lastPickErr = err
continue
}
return pick{}, status.Error(codes.Unavailable, err.Error())
}
출처: grpc/grpc-go v1.83.2 picker_wrapper.go 157~176행 발췌, Apache-2.0
- 아직 연결 중이면(
ErrNoSubConnAvailable) 새 픽커가 올 때까지 기다립니다. 연결 중인 채널에서 호출이 실패하지 않는 이유입니다. - 모든 서브채널이 TRANSIENT_FAILURE 면 픽커가 오류를 돌려주고, 기본(fail fast)은 즉시 UNAVAILABLE 입니다.
grpc.WaitForReady(true)를 준 호출만 deadline 까지 기다립니다 (wait-for-ready.md 의 정의 그대로). - 상태 오류를 돌려주면(드롭) 재시도도 하지 않습니다. 제어 평면이 「이 호출은 버려라」 할 때 쓰는 길입니다.
운영에서 이 차이는 이렇게 보입니다. 서버를 재시작하는 몇 초 동안 fail fast 클라이언트는 UNAVAILABLE 을 쏟아 내고(6편 Collector 의 10-13 기록처럼), wait-for-ready 클라이언트는 조용히 기다렸다가 deadline 안에 서버가 돌아오면 성공합니다.
4.5 서버: Serve 에서 핸들러까지
Server.Serve가Accept할 때마다handleRawConn을 고루틴으로 띄웁니다.- 자격 증명 핸드셰이크 뒤
NewServerTransport가 HTTP/2 설정을 교환하고,serveStreams가 프레임을 읽습니다. 동시 스트림 수 기본값은 무제한(math.MaxUint32,server.go:189)입니다. - HEADERS 가 오면
operateHeaders가:path,content-type,grpc-timeout을 읽어 스트림을 만들고(잘못된 content-type 은 415),handleStream이 경로를 서비스 이름과 메서드 이름으로 나눠RegisterService로 등록된 표(1편의ServiceDesc)에서 찾습니다. 없으면 UNIMPLEMENTED. - 단항이면
processUnaryRPC, 스트림이면processStreamingRPC가 메시지를 읽고 인터셉터와 핸들러를 부른 뒤,writeStatus로 트레일러를 씁니다. GracefulStop은 GOAWAY 를 보내 새 스트림을 막고 진행 중인 것이 끝나길 기다립니다. 5편의 MaxConnectionAge 도 같은 GOAWAY 를 씁니다.
5. 6편 실측과의 대조
| 6편에서 잰 것 | 이 지도에서 |
|---|---|
| apiserver → etcd TCP 247개 | etcd 클라이언트 247개 = ClientConn 247개, 각자 READY addrConn 하나(대상이 127.0.0.1 하나라서) |
| 노드 A 의 열린 Watch 스트림 242 | 각 ClientConn 의 http2Client 위 장수 스트림 하나 |
| kubelet → containerd 채널 2개 | 런타임·이미지 ClientConn 둘, passthrough 가 아닌 유닉스 주소 |
| hubble-relay 의 노드 8개 연결 | 노드마다 ClientConn(릴레이의 피어 풀이 관리, 추정) |
| kubelet gRPC 프로브 | 프로브마다 ClientConn 생성·종료(5편 6절) |
같은 grpc-go 라도 채널을 몇 개 만들어 어떻게 쓰느냐는 쓰는 쪽 코드가 정하고, 그 선택이 연결 수·메모리·부하 분산을 정합니다.
6. 어디부터 읽을까
Documentation/의keepalive.md·concurrency.md·anti-patterns.md를 먼저 읽습니다.anti-patterns.md는 옛Dial·WithBlock을 왜 피하라는지 설명합니다.call.go→stream.go의newClientStreamWithParams·csAttempt로 호출 하나를 따라갑니다.picker_wrapper.go→balancer_wrapper.go→balancer/roundrobin으로 고르는 쪽을,resolver_wrapper.go→internal/resolver/dns로 찾는 쪽을 봅니다.internal/transport/http2_client.go의NewStream·reader·keepalive,controlbuf.go의 쓰기 루프로 내려갑니다.- 서버는
server.go의handleStream→processUnaryRPC만 읽어도 디스패치 구조가 보입니다.
7. 첫 기여하기 좋은 곳
- grpc-go 는 처음 기여하는 사람에게 담당자가 없는 「Status: Help Wanted」 이슈를 권합니다(2026-10-17 열린 것 11개, CONTRIBUTING). 동작이나 API 를 바꾸는 일은 먼저 이슈로 합의하고, 여러 언어가 함께 바뀌어야 하는 기능은 gRFC 부터입니다.
- 생성형 AI 정책이 있습니다. 저장소의 모든 소통(이슈·PR 본문·댓글)은 사람이 직접 써야 하고(번역·문장 다듬기 도움은 예외), 자동 에이전트가 GitHub 에 직접 게시하면 안 되며, AI 가 만든 코드는 제출자가 전부 이해하고 책임져야 합니다(master 의 CONTRIBUTING 「Generative AI Policy」).
- 읽다가 본 TODO 셋:
stream.go:634의 hedging,clientconn.go1364행의 「재해석 요청을 pick_first 안으로 옮기기」(#7534),createHeaderFields의 머리말 개수 미리 세기 벤치마크. 셋 다 설계 논의가 필요해 첫 PR 로는 크지만 이슈를 읽는 입구로 좋습니다. - 문서 쪽 후보는 8편 7절에 모았습니다.
8. 한계
- xDS(
xds/·internal/xds/)와 ringhash·weightedroundrobin·RLS 같은 고급 밸런서는 읽지 않았습니다. controlbuf.go의 쓰기 스케줄링, 메모리 풀(mem/), 통계 핸들러(OpenTelemetry 연동)는 다루지 않았습니다.- 줄 번호는 v1.83.2 기준입니다. v1.84.0 과의 차이는 확인하지 않았습니다(미확인).
티티 한마디. 배정표(픽커)를 통째로 바꿔 끼우니까, 손님이 몰려도 배정 담당과 줄 서서 싸울 일이 없어요. 대신 직원이 다 아프면 기다릴지(WaitForReady) 돌려보낼지(fail fast)는 손님이 정해요!
참고
모든 자료는 2026-10-17 에 열람했습니다.
- grpc/grpc-go v1.83.2:
clientconn.go,call.go,stream.go,picker_wrapper.go,balancer_wrapper.go,resolver_wrapper.go,server.go,internal/transport/http2_client.go,internal/transport/http2_server.go,rpc_util.go,Documentation/, CONTRIBUTING.md(master) - grpc/grpc v1.84.0:
doc/wait-for-ready.md,doc/load-balancing.md - 인용 코드: grpc-go v1.83.2, Apache License 2.0.