Status: Draft (2026-10-17), Code Reading. Baseline version: grpc/grpc-go v1.83.2 (commit 030ee8becb20, read from local Go module cache). Since the latest release is v1.84.0, line numbers may differ slightly. The
파일:줄in the text refers to the function declaration line in v1.83.2. There are no new measurements in this part, only comparisons with the numbers from Part 6.
Table of Contents
- 1. One-line Summary: Company Org Chart
- 2. Architecture Diagram
- 3. Code Map
- 4. Following the Core Path
- 5. Comparison with Part 6 Measurements
- 6. Where to Start Reading
- 7. Good Places for Your First Contribution
- 8. Limitations
- References
1. One-line Summary: Company Organizational Chart
The channel is a team leader. When a task (call) is received, instead of doing it directly, the address book manager (resolver) hands over the given list to the assignment manager (balancer), and the assignment manager picks employees (subchannels) in advance and selects one using the daily assignment sheet (picker) whenever a task arrives. Each employee has their own telephone line (HTTP/2 connection). Since the assignment sheet is replaced entirely as a single paper, it does not contend for locks with anyone at the moment of picking a task.
2. Architecture Diagram
3. Code Map
| Path | What | Where to read first |
|---|---|---|
clientconn.go |
Channel, subchannel (addrConn), idle mode, retry control |
NewClient(183), addrConn(1240) |
call.go, stream.go |
Unary and streaming calls, retry attempts (csAttempt) |
invoke(65), newClientStream(213) |
resolver_wrapper.go, resolver/, internal/resolver/ |
Resolver wrapping and dns, passthrough, and unix implementations | ccResolverWrapper.UpdateState(129) |
balancer_wrapper.go, balancer/ |
Balancer wrapping, pick_first, round_robin, ringhash, weightedroundrobin, etc. | ccBalancerWrapper.NewSubConn(180) |
picker_wrapper.go |
Selecting a subchannel per call, waiting | pickerWrapper.pick(105) |
internal/transport/ |
HTTP/2 client and server, flow control, keepalive, control buffer | http2_client.go, http2_server.go |
rpc_util.go |
5-byte prefix, compression, receive limit, call options | msgHeader(879), parser.recvMsg(785) |
server.go |
Server, service registration, dispatch | Serve(888), handleStream(1800) |
codes/, status/ |
Status codes and status objects | codes.go |
health/, reflection/, channelz/, admin/ |
Additional services (Parts 5 and 8) | health/server.go |
xds/, internal/xds/ |
xDS resolver and balancer (Part 5) | Large scale, later |
4. Following the Core Path
4.1 Creating a Channel: Nothing is Connected Yet
NewClient applies options, parses the target string, selects the resolver builder, and (initParsedTargetAndResolverBuilder, default scheme dns), prepares the idle state, and finishes. Preparing the idle state (initIdleStateLocked) creates a resolver wrapper
(newCCResolverWrapper) and a balancer wrapper (newCCBalancerWrapper), but the resolver does not run yet. When the first call arrives, exitIdleMode changes the channel to CONNECTING and starts the resolver. If there are no calls for 30 minutes, enterIdleMode tears down the connections with the resolver and balancer and returns to idle.
4.2 When addresses arrive: Resolver → Balancer → Subchannel → Picker
- When the resolver finds addresses, it calls
ccResolverWrapper.UpdateState, and the channel'supdateResolverStateAndUnlockapplies the service config and passes it to (applyServiceConfigAndBalancer) the balancer (updateClientConnState). - The balancer (e.g., round_robin) calls
NewSubConnfor each address, which createsnewAddrConnLockedwithaddrConn. ThisaddrConnis the actual entity of the subchannel. addrConnconnects in the order ofresetTransportAndUnlock→tryAllAddrs→createTransport, and if it fails, retries after a backoff. If it fails or gets disconnected, it requestsResolveNowfrom the resolver (Part 5, Section 4.1).- Whenever the subchannel state changes, the balancer creates a new picker, sends it to
ccBalancerWrapper.UpdateState, andpickerWrapper.updatePickerupdates the picker and swaps it atomically. Calls that were waiting are awakened as the channel attached to the previous picker closes.
Why was it divided like this? By keeping address resolution (DNS, xDS, Kubernetes APIs, etc.) and selection (round-robin, weighted, hash, etc.) completely decoupled, any resolver can be combined with any balancer. Balancer updates are processed sequentially by a serializer (see the comment ingrpcsync.CallbackSerializer, balancer_wrapper.go ), and the hot path where calls pass through (pick) only reads a single atomic value, ensuring that the two worlds never wait for each other's locks.
4.3 Single Call: From Invoke to Stream
- The generated stub calls
ClientConn.Invoke, passes through the interceptor, andinvokegoes tonewClientStream→newClientStreamWithParams. Even unary calls are internally implemented as a "stream of sending one message and receiving one." - Creating a single attempt (
newAttemptLocked),getTransportselects a subchannel from the picker. For the transport of the chosen subchannel, it sends HEADERS viacsAttempt.newStream→http2Client.NewStream(Part 3'screateHeaderFields). SendMsgserializes, compresses, adds a 5-byte prefix, writes tocsAttempt.sendMsg, and reads the response throughRecvMsg→recv→parser.recvMsg). If it fails,withRetrylooks at Part 4'sshouldRetryto create a new attempt and replays the buffered sent messages.
4.4 Picker: To Wait or Fail Fast
What happens if a call arrives but there are no READY subchannels? A branch in pickerWrapper.pick provides the answer.
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())
}
Source: grpc/grpc-go v1.83.2 picker_wrapper.go Excerpt from lines 157-176, Apache-2.0
- If still connecting (
ErrNoSubConnAvailable), wait until a new picker arrives is waited for. This is why calls do not fail on a connecting channel. - If all subchannels are TRANSIENT_FAILURE, the picker returns an error, and the default (fail fast) is immediately UNAVAILABLE .
grpc.WaitForReady(true)Only calls with wait for the deadline (wait-for-ready.md as defined in). - If it returns a status error (drop), it does not even retry. This is the path used when the control plane says "discard this call."
In production, this difference looks like this: for a few seconds while restarting the server, fail fast clients spew UNAVAILABLE (like logs 10-13 of Collector in Part 6), and wait-for-ready clients quietly wait and succeed if the server comes back within the deadline.
4.5 Server: From Serve to Handler
Server.ServeWhenAcceptdoeshandleRawConn, it spawns- as a goroutine. After the credential handshake,
NewServerTransportexchanges HTTP/2 settings, andserveStreamsreads frames. The default maximum concurrent streams is unlimited (math.MaxUint32,server.go:189). - When HEADERS arrive,
operateHeadersreads:path,content-type,grpc-timeoutto create a stream (invalid content-type is 415), andhandleStreamsplits the path into service name and method name to look up in the table registered asRegisterService(ServiceDescfrom Part 1). If not found, UNIMPLEMENTED. - If unary,
processUnaryRPC, if stream,processStreamingRPCreads messages, calls interceptors and handlers, and then writes trailers withwriteStatus. GracefulStopsends GOAWAY to block new streams and waits for ongoing ones to finish. MaxConnectionAge in Part 5 uses the same GOAWAY.
5. Comparison with Part 6 Measurements
| What was measured in Part 6 | In this map |
|---|---|
| apiserver → etcd 247 TCP connections | 247 etcd clients = ClientConn 247, each with one READY addrConn (since the target is a single 127.0.0.1) |
| 242 open Watch streams on Node A | Each ClientConn has one http2Client long-lived stream above it |
| 2 kubelet → containerd channels | Runtime and image ClientConn two, passthrough Unix address, not a |
| hubble-relay connecting to 8 nodes | per node ClientConn(managed by relay's peer pool, estimated) |
| kubelet gRPC probes | per probe ClientConn creation and termination (Part 5, Section 6) |
Even with the same grpc-go, how many channels are created and how they are usedis determined by the client code, and that choice dictates the connection count, memory usage, and load balancing.
6. Where to Read First
Documentation/ofkeepalive.md·concurrency.md·anti-patterns.mdis read first.anti-patterns.mdexplains why the oldDial·WithBlockshould be avoided.call.go→stream.goofnewClientStreamWithParams·csAttemptis used to follow a single call.picker_wrapper.go→balancer_wrapper.go→balancer/roundrobinis used for the selection side, andresolver_wrapper.go→internal/resolver/dnsfor the lookup side.internal/transport/http2_client.goofNewStream·reader·keepalive,controlbuf.goleads down to the write loop.- For the server, reading only
server.goofhandleStream→processUnaryRPCreveals the dispatch structure.
7. Good First Contribution Areas
- grpc-go recommends "Status: Help Wanted" issues with no assignee for first-time contributors (11 open as of 2026-10-17, CONTRIBUTING). Changing behavior or APIs requires prior agreement via an issue, and features involving multiple languages must start with a gRFC.
- Generative AI Policyis in place. All repository communication (issues, PR bodies, and comments) must be written directly by humans (with translation and phrasing assistance as exceptions), automated agents must not post directly to GitHub, and AI-generated code must be fully understood and taken responsibility for by the submitter (CONTRIBUTING under master).
- Three TODOs found while reading:
stream.go:634hedging,clientconn.goline 1364 "Move re-interpretation request into pick_first" (#7534), andcreateHeaderFieldsbenchmark for pre-counting header counts. All three require design discussions and are too large for a first PR, but serve as great entry points for reading the issues. - Documentation candidates are gathered in Part 8, Section 7.
8. Limitations
- xDS(
xds/·internal/xds/) and advanced balancers like ringhash, weightedroundrobin, and RLS were not read. controlbuf.gowrite scheduling and memory pools(mem/), and stats handlers (OpenTelemetry integration) were not covered.- Line numbers are based on v1.83.2. Differences with v1.84.0 were not verified (unverified).
Titi's note. Since the assignment table (picker) is swapped out entirely, there is no need to fight with the assignment manager for a spot even when customers rush in. Instead, if all employees fall ill, whether to wait (WaitForReady) or turn them away (fail fast) is decided by the customer!
References
All materials were accessed on 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 - Cited code: grpc-go v1.83.2, Apache License 2.0.