Observability & Testing
curl ดู binary protocol ไม่ได้
หัวข้อที่มีชื่อว่า “curl ดู binary protocol ไม่ได้”ความเร็วของ protobuf แลกมาด้วยการอ่านด้วยตาไม่ออก คุณมอง gRPC call ด้วยตาแบบที่มอง REST request ไม่ได้ ecosystem จึงแทน “อ่าน traffic” ด้วย reflection, telemetry ที่อยู่บน interceptor และ in-process test
Reflection: สำรวจ server ที่รันอยู่
หัวข้อที่มีชื่อว่า “Reflection: สำรวจ server ที่รันอยู่”Server reflection ให้ server อธิบาย service และ message ของตัวเองตอน runtime ได้ ทำให้เครื่องมือเรียก method ได้โดยไม่ต้องมี .proto ในมือ เมื่อเปิดใช้ grpcurl จะกลายเป็น curl ของ gRPC:
# List services on a running server, then call one — no local .proto neededgrpcurl -plaintext localhost:50051 listgrpcurl -plaintext -d '{"id": 42}' localhost:50051 user.v1.UserService/GetUserเปิด reflection ใน non-production หรือหลัง auth — reflection คือสิ่งที่ทำให้ server ที่รันอยู่สำรวจได้
telemetry อยู่บน interceptor
หัวข้อที่มีชื่อว่า “telemetry อยู่บน interceptor”คุณเจอ interceptor มาแล้วในฐานะที่ ๆ ใส่ cross-cutting concern observability คือการใช้งานที่ใหญ่ที่สุดของตัวเอง: interceptor chain เดียวเพิ่ม metrics (latency, จำนวน request, status code), traces (span ที่ตาม call ข้าม service) และ log แบบมีโครงสร้าง — ให้ทุก method โดยไม่แตะ handler code
flowchart LR call["incoming RPC"] --> mw["interceptor chain"] mw --> m["metrics (latency, count, code)"] mw --> t["trace span (propagate context)"] mw --> l["structured log"] mw --> handler["your handler"]
เส้นทางมาตรฐานคือ instrumentation แบบ OpenTelemetry ซึ่งมาเป็น gRPC interceptor สำเร็จรูปในทุกภาษาหลัก — register ครั้งเดียวแล้วได้ metrics และ distributed trace ทั่วทั้ง fleet
test แบบ in-process ไม่ใช่ผ่าน network
หัวข้อที่มีชื่อว่า “test แบบ in-process ไม่ใช่ผ่าน network”การเปิด port จริงทำให้ test ช้าและ flaky gRPC ให้คุณรัน server และ client จริง ใน memory ได้ โดยผ่านทั้งเส้นทาง serialization และ interceptor เต็ม ๆ โดยไม่มี network:
// bufconn: a real gRPC server over an in-memory listenerlis := bufconn.Listen(1024 * 1024)s := grpc.NewServer()userv1.RegisterUserServiceServer(s, &server{})go s.Serve(lis)
conn, _ := grpc.NewClient("passthrough:///bufnet", grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }), grpc.WithTransportCredentials(insecure.NewCredentials()))client := userv1.NewUserServiceClient(conn)got, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 42})# Start a real server on an ephemeral port inside the testserver = grpc.server(futures.ThreadPoolExecutor(max_workers=1))user_pb2_grpc.add_UserServiceServicer_to_server(Servicer(), server)port = server.add_insecure_port("localhost:0")server.start()
channel = grpc.insecure_channel(f"localhost:{port}")client = user_pb2_grpc.UserServiceStub(channel)got = client.GetUser(user_pb2.GetUserRequest(id=42))server.stop(None)// Bind to port 0, let the OS pick a free port, connect to itconst server = new Server();server.addService(UserServiceService, new UserServiceImpl());server.bindAsync('localhost:0', ServerCredentials.createInsecure(), (_e, port) => { const client = new UserServiceClient(`localhost:${port}`, credentials.createInsecure()); client.getUser({ id: 42 }, (err, user) => { /* assert */ });});benchmark อย่างตรงไปตรงมา
หัวข้อที่มีชื่อว่า “benchmark อย่างตรงไปตรงมา”gRPC มักเร็วกว่า REST/JSON สำหรับ call ที่ปริมาณสูง ขนาดเล็ก และมีโครงสร้าง — byte น้อยกว่า, decode ถูกกว่า, connection แบบ multiplex แต่ต้องวัดกับ workload ของคุณเอง: สำหรับ call นาน ๆ ที ครั้งหรือ blob ใหญ่ที่ถูก compress มาแล้ว ความต่างอาจแทบไม่มี และ tooling ของ REST อาจชนะโดยรวม benchmark ด้วย payload และ concurrency ที่สมจริง ไม่ใช่ loop ของเล่น ก่อนจะเอาไปเป็นจุดขาย