TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

gRPC Debugging and Observability: grpcurl, Reflection, channelz, and Metric Pitfalls

Status: Draft (2026-10-17), partially measured. Reference versions: grpc/grpc-go v1.83.2, grpc/grpc v1.84.0 documentation, kubernetes v1.34.10, go-grpc-prometheus v1.2.0. Metrics were obtained solely by reading Prometheus around 03:00 KST on 2026-10-17. grpcurl and channelz were not executed(unmeasured). What is required is the grpcurl binary (or downloading the module to build with Go) and a test server with reflection and channelz enabled. Therefore, the commands in sections 3 and 4 are usages verified through documentation and code, and their outputs are not included.

Table of Contents

1. Intuition: Four Ways to Read Binary Letters

JSON APIs can be read by humans in curl a single line. Because gRPC has no field names in its bytes (Part 2), you need a contract to read it. Debugging tools ultimately solve the problems of where to get the contractand where to view the connection state.

서버 하나를 들여다보는 네 가지 창 gRPC 서버 애플리케이션 서비스 서버 리플렉션 reflection.v1 grpcurl 로 조회 헬스 체크 health.v1 kubelet 프로브 channelz channelz.v1 채널·소켓 통계 메트릭 grpc_server_* Prometheus·OTel 위 셋은 같은 포트의 gRPC 서비스로 붙고, 메트릭은 별도 HTTP 로 긁는다.
Figure: Positions of reflection, health, channelz, and metrics
Window What it reveals How it attaches
Server Reflection List of services and methods, message definitions gRPC service on the same port grpc.reflection.v1.ServerReflection
Health Check SERVING status grpc.health.v1.Health(Part 5)
channelz Call counts, failure counts, last call time, stream and message counts per channel, subchannel, and socket grpc.channelz.v1.Channelz
Metrics Counts and latency per method and status code Separate HTTP (/metrics) or OTel export

2. Reflection and grpcurl

Reflection is an optional feature where the server exposes its own proto definitions so thatserver-reflection.md). A representative use case is command-line debugging tools, and grpcurl is such a tool.

In grpc-go, it is enabled with a single line. reflection.Register registers both v1 and the legacy v1alpha. This is because many clients still only understand v1alpha, as explained by a comment in RegisterV1 of the same file.

What you do with grpcurl is roughly as follows (not executed, unmeasured).

grpcurl -plaintext 서버:포트 list                                   # 서비스 목록(리플렉션 필요)
grpcurl -plaintext 서버:포트 describe tt.v1.Scoreboard              # 메서드와 메시지 정의
grpcurl -plaintext -d '{"rallyId":150}' 서버:포트 tt.v1.Scoreboard/Hit   # JSON 으로 요청, JSON 으로 응답
grpcurl -plaintext -import-path . -proto rally.proto 서버:포트 list   # 리플렉션 없이 .proto 를 직접 줌

3. channelz: Peering Inside Channels

channelz (gRFC A14) presents the layers of Part 7 directly as statistics. From the top-down tree of channel to subchannel to socket, it provides the status of each node (such as READY), number of started, successful, and failed calls, last call time, and the number of streams, messages, and keepalives for sockets.

In grpc-go, you enable it directly with channelz/service.RegisterChannelzServiceToServer , or enable channelz and CSDS (if using xDS) together with admin.Register . Questions like "247 etcd connections in apiserver" from Part 6 are normally answered using this window. kube-apiserver does not expose channelz, so this time we used ss instead (channelz registration could not be found in kube-apiserver source, unconfirmed).

Logs can also serve as a window. grpc-go enables internal logs via environment variables GRPC_GO_LOG_SEVERITY_LEVEL(info, warning, error) and GRPC_GO_LOG_VERBOSITY_LEVEL(number) (grpclog/loggerv2.go:65). Resolver updates, picker replacements, and connection failures are logged here.

4. Metrics: Two Families

Family Example Names Used by
Interceptor approach (go-grpc-prometheus family) grpc_server_started_total, grpc_server_handled_total{grpc_code}, grpc_client_msg_received_total, optional grpc_server_handling_seconds etcd, kube-apiserver (etcd client), Argo CD, istiod
gRPC Native OpenTelemetry Metrics (gRFC A66) grpc.client.attempt.duration, grpc.client.call.duration, grpc.server.call.duration grpc-go stats/opentelemetry(client_metrics.go:291)
respective names kubelet_runtime_operations_total, tempo_request_duration_seconds{route}, otelcol_* kubelet, Tempo, Collector

Three useful combinations:

5. Measurements: Pitfall Two

5.1 kube-apiserver 1.34 Received Message Metric Counts "Failures"

While looking at apiserver's etcd client metrics, I encountered a strange number. It stated that Range was called over 160 million times since startup, but the number of received messages was 132.

sum by (grpc_service, grpc_method, grpc_type) (grpc_client_msg_received_total{job="apiserver"})
sum by (grpc_method, grpc_code) (grpc_client_handled_total{job="apiserver", grpc_type="unary"})
Method Received Messages (msg_received) Sent Messages (msg_sent) Sum of Non-OK Results
Range 132 162,817,697 CANCELLED 15 + DEADLINE_EXCEEDED 2 + OUT_OF_RANGE 94 + UNAVAILABLE 21 = 132
Txn 39 58,967,709 26 + 4 + 9 = 39
LeaseGrant·Status·Compact No time series (never counted) 17K to 1.55M each 0

The number of received messages is exactly equal to the error count. apiserver 1.34 attaches github.com/grpc-ecosystem/go-grpc-prometheus to its etcd client (etcd3.go:316), and the version in go.mod is v1.2.0. The unary client interceptor in that version looks like this:

func (m *ClientMetrics) UnaryClientInterceptor() func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		monitor := newClientReporter(m, Unary, method)
		monitor.SentMessage()
		err := invoker(ctx, method, req, reply, cc, opts...)
		if err != nil {
			monitor.ReceivedMessage()
		}
		st, _ := status.FromError(err)
		monitor.Handled(st.Code())
		return err
	}
}

Source: grpc-ecosystem/go-grpc-prometheus v1.2.0 client_metrics.go lines 108–120, Apache-2.0

err != nil It counts as "received" when . The condition is inverted. While master in the same repository has been fixed to err == nil , there have been no releases since, and the repository is archived. Streams (Watches) are counted via a different path (wrapped inRecvMsg ), so they are normal. On the etcd server side of the same cluster, grpc_server_msg_received_total was normal because it uses a different library (Compact 10,268 = Startup 10,268).

5.2 Argo CD Server: 13 out of 1,938 Time Series

count(grpc_server_handled_total{job="argocd-server-metrics"})          → 1,938
count(grpc_server_handled_total{job="argocd-server-metrics"} > 0)      → 13
count(count by (grpc_service,grpc_method) (grpc_server_started_total{job="argocd-server-metrics"}))  → 114

114 methods × 17 status codes = 1,938. Interceptor-based libraries have a feature that pre-creates all method and code combinations registered on the server as 0 (meant to make "methods never called yet" appear in ratio calculations), and it appears the Argo CD server enabled this (estimated, did not open Argo CD source). As a result, this single job's grpc_* time series reached 2,280, accounting for 95% of the small cluster's total grpc_* time series of 2,402 . Only 13 have values. If your time series budget is tight, metric_relabel_configs dropping zero-value combinations with is worth considering.

6. Limitations

7. Contribution Candidates: Discrepancies Between Documentation and Code

Found while writing this series. Not posted anywhere yet, and must be re-verified against the latest main before posting.

# Repository Discrepancy Rationale Size
1 grpc/grpc doc/health-checking.md service definition lacks List RPC gRFC A90 and grpc-proto's health.proto have List·HealthListRequest·HealthListResponse , and grpc-go implemented it in v1.83.2 (health/server.go:73, grpc-go#8155). Documentation still shows only Check and Watch even on master (2026-10-17) One documentation PR
2 grpc/grpc doc/statuscodes.md describes INVALID_ARGUMENT as a code not generated by the library grpc-go server sends HTTP 415 and grpc-status: 3 for invalid content-type requests (internal/transport/http2_server.go:504). Need to check if other language implementations do the same first Ask via issue first
3 kubernetes Due to dependency replacement in 1.36, the meaning of grpc_client_msg_received_total(apiserver→etcd, unary) changed from "failure count" to "response count" Section 5.1 measurement (132 = 132), go-grpc-prometheus v1.2.0 client_metrics.go:113, PR #136227 Supplement release notes or record known issue (low utility, already past release)
4 kubernetes apiserver creates an etcd client for each resource (CRD), making connections proportional to the number of resources (TCP 247 on Node A) etcd3.go:74 's TODO, issue #111476 Known issues. Adding measured figures to the issue context

8. Connection with small clusters

A word from TT. The record of "132 catches" was actually "132 dropped balls." Scoreboards are sometimes written backward, so if the numbers look strange, check the calculation formula first!

References

All materials were accessed on 2026-10-17.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment