상태: 초안 (2026-10-17), 일부 실측. 기준 판 grpc/grpc-go v1.83.2, grpc/grpc v1.84.0 문서, kubernetes v1.34.10, go-grpc-prometheus v1.2.0. 메트릭은 2026-10-17 03시대(KST)에 Prometheus 를 읽기만 해서 얻었습니다. grpcurl·channelz 는 실행하지 않았습니다(미실측). 필요한 것은 grpcurl 바이너리 (또는 Go 로 빌드할 모듈 내려받기)와, 리플렉션·channelz 를 켠 시험 서버입니다. 그래서 3·4절의 명령은 문서와 코드로 확인한 사용법이고 출력은 싣지 않았습니다.
목차
- 1. 직관: 이진 편지를 읽는 네 가지 방법
- 2. 리플렉션과 grpcurl
- 3. channelz: 채널 안을 들여다보기
- 4. 메트릭: 두 가족
- 5. 실측: 함정 둘
- 6. 한계
- 7. 기여 후보: 문서와 코드가 어긋난 곳
- 8. 작은 클러스터와의 연결
- 참고
1. 직관: 이진 편지를 읽는 네 가지 방법
JSON API 는 curl 한 줄이면 사람이 읽을 수 있습니다. gRPC 는 바이트에 칸 이름이 없으므로(2편) 계약서가 있어야 읽습니다. 디버깅 도구들은 결국
계약서를 어디서 얻느냐와 연결 상태를 어디서 보느냐의 문제를 풉니다.
| 창 | 무엇을 알려 주나 | 어떻게 붙나 |
|---|---|---|
| 서버 리플렉션 | 서비스·메서드 목록, 메시지 정의 | 같은 포트의 gRPC 서비스 grpc.reflection.v1.ServerReflection |
| 헬스 체크 | SERVING 여부 | grpc.health.v1.Health(5편) |
| channelz | 채널·서브채널·소켓별 호출 수, 실패 수, 마지막 호출 시각, 스트림·메시지 수 | grpc.channelz.v1.Channelz |
| 메트릭 | 메서드·상태 코드별 횟수, 지연 | 별도 HTTP(/metrics) 또는 OTel 내보내기 |
2. 리플렉션과 grpcurl
리플렉션은 「클라이언트에 스텁이 없어도 실행 중에 요청을 만들 수 있게」 서버가 자기 proto 정의를 내주는 선택 기능입니다 (server-reflection.md). 대표 사용처가 명령줄 디버깅 도구이고, grpcurl 이 그런 도구입니다.
grpc-go 에서는 한 줄로 켭니다. reflection.Register 는 v1 과 옛 v1alpha 를 둘 다 등록합니다.
아직 v1alpha 만 아는 클라이언트가 많기 때문이라고 같은 파일의 RegisterV1 주석이 설명합니다.
grpcurl 로 하는 일은 대략 이렇습니다(실행하지 않음, 미실측).
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 를 직접 줌
- 요청을 JSON 으로 적으면 도구가 계약서를 보고 2편의 이진으로 바꿔 보냅니다. 응답도 반대로 바꿔 보여 줍니다.
- 리플렉션은 서비스 구조를 그대로 내보냅니다. 외부에 열린 포트에서는 끄고, 내부·관리 포트에만 켜는 것이 보통입니다(5편 6절의 hubble-relay 가 헬스용 4222 와 본 서비스 4245 를 나눈 것과 같은 발상).
- 헬스 체크는 리플렉션 없이도 됩니다. 헬스 proto 는 grpcurl 이 내장하지 않으므로 grpc 저장소의
health.proto를-proto로 주면 됩니다(문서상 사용법, 미실측).
3. channelz: 채널 안을 들여다보기
channelz(gRFC A14)는 7편의 계층을 그대로 통계로 보여 줍니다. 최상위 채널 → 서브채널 → 소켓의 나무에서 각 노드의 상태(READY 등), 시작·성공·실패한 호출 수, 마지막 호출 시각, 소켓의 스트림·메시지·keepalive 수를 줍니다.
grpc-go 에서는 channelz/service.RegisterChannelzServiceToServer 로 직접 켜거나,
admin.Register 로 channelz 와 (xDS 를 쓰면) CSDS 를 한꺼번에 켭니다. 6편의 「apiserver 의 etcd 연결
247개」 같은 질문은 원래 이 창으로 답하는 것이 정석입니다. kube-apiserver 는 channelz 를 내놓지 않아 이번에는 ss 로 대신했습니다(kube-apiserver 소스에서
channelz 등록을 찾지 못함, 미확인).
로그도 창이 됩니다. grpc-go 는 환경 변수 GRPC_GO_LOG_SEVERITY_LEVEL(info·warning·error)과 GRPC_GO_LOG_VERBOSITY_LEVEL(숫자)로 내부 로그를 켭니다
(grpclog/loggerv2.go:65). 리졸버 갱신, 픽커 교체, 연결 실패가 여기에 남습니다.
4. 메트릭: 두 가족
| 가족 | 이름 예 | 누가 씀 |
|---|---|---|
| 인터셉터 방식(go-grpc-prometheus 계열) | grpc_server_started_total, grpc_server_handled_total{grpc_code}, grpc_client_msg_received_total, 선택적 grpc_server_handling_seconds |
etcd, kube-apiserver(etcd 클라이언트), Argo CD, istiod |
| gRPC 자체 OpenTelemetry 지표(gRFC A66) | grpc.client.attempt.duration, grpc.client.call.duration, grpc.server.call.duration |
grpc-go stats/opentelemetry(client_metrics.go:291) |
| 각자 이름 | kubelet_runtime_operations_total, tempo_request_duration_seconds{route}, otelcol_* |
kubelet, Tempo, Collector |
쓸모 있는 조합 셋:
started_total − handled_total은 지금 열려 있는 스트림 수입니다(4편 6.2절의 Watch 242개).handled_total을grpc_code로 나누면 3편의 표가 됩니다. 단, Watch 의 CANCELLED 처럼 정상 종료가 오류 코드로 남는 경우를 걸러야 합니다.- 지연 히스토그램은 대개 기본으로 꺼져 있습니다. 작은 클러스터에서
grpc_server_handling_seconds를 내는 것은 istiod 뿐이었고, 그래서 이 시리즈의 지연 표는 apiserver 의etcd_request_duration_seconds, kubelet·Tempo 의 자체 히스토그램으로 대신했습니다.
5. 실측: 함정 둘
5.1 kube-apiserver 1.34 의 수신 메시지 지표는 「실패」를 센다
apiserver 의 etcd 클라이언트 지표를 보다가 이상한 숫자를 만났습니다. 기동 이후 Range 를 1억 6천만 번 넘게 불렀는데 받은 메시지는 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"})
| 메서드 | 받은 메시지(msg_received) |
보낸 메시지(msg_sent) |
OK 가 아닌 결과의 합 |
|---|---|---|---|
| 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 | 시계열 없음(한 번도 세지 않음) | 각각 1.7만~155만 | 0 |
받은 메시지 수가 오류 수와 정확히 같습니다. apiserver 1.34 는 etcd 클라이언트에 github.com/grpc-ecosystem/go-grpc-prometheus 를 붙이고
(etcd3.go:316), go.mod 의 판은 v1.2.0 입니다. 그 판의 단항 클라이언트 인터셉터는 이렇습니다.
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
}
}
출처: grpc-ecosystem/go-grpc-prometheus v1.2.0 client_metrics.go 108~120행, Apache-2.0
err != nil 일 때 「받았다」고 셉니다. 조건이 뒤집혀 있습니다. 같은 저장소의 master 는 err == nil 로 고쳐져 있지만 그 뒤로 릴리스가 없고, 저장소는 보관(archived) 상태입니다.
스트림(Watch)은 다른 경로(RecvMsg 포장)로 세므로 정상입니다. 같은 클러스터의 etcd 서버 쪽 grpc_server_msg_received_total 은 다른 라이브러리라 정상이었습니다(Compact 10,268 = 시작 10,268).
- 쿠버네티스는 이 의존성을
go-grpc-middleware/providers/prometheus로 바꿨고(PR #136227, 2026-01-20 병합), v1.36.0 부터 들어 있습니다(v1.35.0 의 같은 파일은 아직 옛 라이브러리). 새 라이브러리의 단항 인터셉터는 오류와 상관없이 응답 수신을 한 번 셉니다 (interceptors/client.go의UnaryClientInterceptor,providers/prometheus/reporter.go의PostMsgReceive). - 그래서 1.35 → 1.36 업그레이드 때 이 지표의 값이 몇 자리 뛰어오릅니다(수백에서 수억으로). 이 지표로 대시보드나 경보를 만든 곳은 놀랄 수 있습니다. PR 본문은 의존성 갱신만 적고 지표 의미 변화는 언급하지 않습니다.
5.2 Argo CD 서버: 시계열 1,938개 중 13개
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개 메서드 × 상태 코드 17개 = 1,938 입니다. 인터셉터 방식 라이브러리는 서버에 등록된 모든 메서드와 모든 코드 조합을 0으로 미리 만들어 두는 기능이 있고
(「아직 한 번도 안 불린 메서드」도 비율 계산에 나오게 하려는 것), Argo CD 서버가 이를 켠 것으로 보입니다(추정, Argo CD 소스는 열지 않음). 그 결과 이 job 하나의
grpc_* 시계열이 2,280개로 작은 클러스터 전체 grpc_* 시계열 2,402개의 95% 를 차지했습니다. 값이 있는 것은 13개뿐입니다. 시계열 예산이 빠듯하면
metric_relabel_configs 로 0인 조합을 버리는 것을 생각할 만합니다.
6. 한계
- grpcurl·channelz·리플렉션은 실행하지 않았습니다(미실측). 필요한 내려받기: grpcurl 릴리스 바이너리, 또는 grpc-go 예제 서버를 빌드할 Go 모듈.
- 5.1절의 판단은 지표 숫자와 라이브러리 소스의 대조입니다. apiserver 가 다른 경로로
ReceivedMessage를 더 부르지 않는지는 인터셉터 체인을 끝까지 따라가지 않았습니다. 다만 132 = 132, 39 = 39 가 정확히 맞아 다른 경로는 없다고 봅니다(추정). - 5.2절의 미리 만들기 기능이 Argo CD 의 어느 설정에서 켜지는지는 확인하지 않았습니다(미확인).
7. 기여 후보: 문서와 코드가 어긋난 곳
이 시리즈를 쓰며 찾은 것입니다. 아직 어디에도 게시하지 않았고, 게시 전에 최신 main 에서 다시 확인해야 합니다.
| # | 저장소 | 어긋남 | 근거 | 크기 |
|---|---|---|---|---|
| 1 | grpc/grpc | doc/health-checking.md 의 서비스 정의에 List RPC 가 없음 |
gRFC A90 과 grpc-proto 의 health.proto 에는 List·HealthListRequest·HealthListResponse 가 있고, grpc-go 는 v1.83.2 에 구현(health/server.go:73, grpc-go#8155). 문서는 master 에서도 Check·Watch 둘뿐(2026-10-17) |
문서 PR 하나 |
| 2 | grpc/grpc | doc/statuscodes.md 는 INVALID_ARGUMENT 를 라이브러리가 만들지 않는 코드로 적음 |
grpc-go 서버가 잘못된 content-type 요청에 HTTP 415 와 grpc-status: 3 을 보냄(internal/transport/http2_server.go:504). 다른 언어 구현도 그런지 먼저 확인 필요 |
이슈로 먼저 질문 |
| 3 | kubernetes | 1.36 의 의존성 교체로 grpc_client_msg_received_total(apiserver→etcd, 단항)의 의미가 「실패 수」에서 「응답 수」로 바뀜 |
5.1절 실측(132 = 132), go-grpc-prometheus v1.2.0 client_metrics.go:113, PR #136227 |
릴리스 노트 보충 또는 알려진 문제 기록(효용 낮음, 이미 지난 릴리스) |
| 4 | kubernetes | apiserver 가 자원(CRD)마다 etcd 클라이언트를 만들어 연결이 자원 수에 비례(노드 A 에서 TCP 247) | etcd3.go:74 의 TODO, 이슈 #111476 |
이미 알려진 이슈. 실측 수치를 이슈에 보태는 정도 |
- 1번이 가장 작고 확실합니다. 문서의 proto 블록에
List와 두 메시지를 더하고, 서버 동작 문단(전체 목록, 많은 서비스면 RESOURCE_EXHAUSTED)을 A90 에 맞춰 요약하면 됩니다. - grpc-go 와 grpc 조직은 생성형 AI 정책이 있습니다. 이슈·PR·댓글은 사람이 직접 쓰고, AI 가 만든 내용은 제출자가 모두 이해·검토해야 합니다(7편 7절). 쿠버네티스도 PR 본문 고지 등 별도 규칙이 있습니다.
8. 작은 클러스터와의 연결
- 직접 만든 gRPC 서버라면 헬스와 리플렉션을 관리용 포트에 두고, channelz 는
admin.Register로 같은 관리 포트에 붙여 두면 6편 같은 조사를ss없이 할 수 있습니다. - 대시보드에
grpc_client_msg_received_total을 쓰고 있다면 쿠버네티스 1.36 으로 올리기 전에 5.1절을 확인합니다. - 시계열 예산을 볼 때는
count by (job) ({__name__=~"grpc_.*"})부터 봅니다. 작은 클러스터에서는 한 job 이 95% 였습니다.
티티 한마디. 「받은 공 132개」라는 기록이 사실은 「놓친 공 132개」였어요. 점수판도 가끔은 거꾸로 적혀 있으니, 숫자가 이상하면 계산식부터 열어 보세요!
참고
모든 자료는 2026-10-17 에 열람했습니다.
- grpc/grpc v1.84.0:
doc/server-reflection.md,doc/health-checking.md,doc/statuscodes.md; grpc-protogrpc/health/v1/health.proto - gRFC: A14 channelz, A66 OpenTelemetry Metrics, A90 Health Service List Method
- grpc/grpc-go v1.83.2:
reflection/serverreflection.go,channelz/service/service.go,admin/admin.go,grpclog/loggerv2.go,stats/opentelemetry/client_metrics.go,health/server.go,internal/transport/http2_server.go; PR grpc-go#8155 - go-grpc-prometheus: v1.2.0
client_metrics.go, masterclient_metrics.go - kubernetes: v1.34.10
etcd3.go, v1.36.0 같은 파일, PR #136227, 이슈 #111476 - 인용 코드: go-grpc-prometheus v1.2.0, Apache License 2.0.