第24篇-Go-gRPC推理服务-高性能跨语言通信

【AIaaS 全栈架构师】第 24 篇:Go gRPC 推理服务------高性能跨语言通信

系列定位:AIaaS 全栈架构师教程,技术栈以 Go 为主。本篇深入高性能推理通信层------gRPC。


本篇你将学到

  • 理解 gRPC 相对 HTTP/JSON 的性能优势与适用场景
  • 用 Protocol Buffers 定义推理服务接口(.proto 文件)
  • 用 Go 实现 gRPC 推理服务端和客户端,包括双向流式推理
  • 掌握 gRPC 拦截器(Interceptor)实现认证、日志和指标采集

前两篇我们用 HTTP/JSON 构建了 LLM API 网关。HTTP/JSON 对外部用户友好(curl 即可调用),但在平台内部------网关到推理引擎、推理引擎到推理引擎------我们追求的是极致性能。这正是 gRPC 的主战场。


一、为什么推理服务需要 gRPC

1.1 gRPC vs HTTP/JSON

先看一个直观的对比。同一个推理请求,用 HTTP/JSON 和 gRPC 传输的差异:

维度 HTTP/JSON gRPC
序列化格式 JSON(文本) Protocol Buffers(二进制)
传输协议 HTTP/1.1 或 HTTP/2 HTTP/2
序列化速度 慢(字符串解析) 快 3-10 倍
消息体积 大(字段名占空间) 小 30-70%
连接复用 HTTP/1.1 有限 HTTP/2 多路复用
流式支持 SSE(单向) 原生双向流
代码生成 手写 struct proto 自动生成
浏览器支持 原生 需要 grpc-web

1.2 性能差距实测

以一个典型的推理请求(messages 含 2000 token,响应 500 token)为例:

复制代码
HTTP/JSON:
  请求序列化:  2.1 ms    响应反序列化:  1.8 ms
  请求体积:    18.2 KB   响应体积:      4.5 KB
  网络传输:    0.8 ms    (内网)

gRPC:
  请求序列化:  0.3 ms    响应反序列化:  0.2 ms
  请求体积:    6.1 KB    响应体积:      1.8 KB
  网络传输:    0.3 ms    (内网)

单次请求省约 4ms,在高 QPS 场景下累积显著。更关键的是序列化 CPU 开销------JSON 的 encoding/json 反射开销大,而 Protobuf 用预生成代码直接内存操作。

1.3 gRPC 在 AIaaS 中的典型应用

#mermaid-svg-zivG6ycuT1YEVIU3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zivG6ycuT1YEVIU3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zivG6ycuT1YEVIU3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zivG6ycuT1YEVIU3 .error-icon{fill:#552222;}#mermaid-svg-zivG6ycuT1YEVIU3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zivG6ycuT1YEVIU3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zivG6ycuT1YEVIU3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zivG6ycuT1YEVIU3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zivG6ycuT1YEVIU3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zivG6ycuT1YEVIU3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zivG6ycuT1YEVIU3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zivG6ycuT1YEVIU3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zivG6ycuT1YEVIU3 .marker.cross{stroke:#333333;}#mermaid-svg-zivG6ycuT1YEVIU3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zivG6ycuT1YEVIU3 p{margin:0;}#mermaid-svg-zivG6ycuT1YEVIU3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zivG6ycuT1YEVIU3 .cluster-label text{fill:#333;}#mermaid-svg-zivG6ycuT1YEVIU3 .cluster-label span{color:#333;}#mermaid-svg-zivG6ycuT1YEVIU3 .cluster-label span p{background-color:transparent;}#mermaid-svg-zivG6ycuT1YEVIU3 .label text,#mermaid-svg-zivG6ycuT1YEVIU3 span{fill:#333;color:#333;}#mermaid-svg-zivG6ycuT1YEVIU3 .node rect,#mermaid-svg-zivG6ycuT1YEVIU3 .node circle,#mermaid-svg-zivG6ycuT1YEVIU3 .node ellipse,#mermaid-svg-zivG6ycuT1YEVIU3 .node polygon,#mermaid-svg-zivG6ycuT1YEVIU3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zivG6ycuT1YEVIU3 .rough-node .label text,#mermaid-svg-zivG6ycuT1YEVIU3 .node .label text,#mermaid-svg-zivG6ycuT1YEVIU3 .image-shape .label,#mermaid-svg-zivG6ycuT1YEVIU3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zivG6ycuT1YEVIU3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zivG6ycuT1YEVIU3 .rough-node .label,#mermaid-svg-zivG6ycuT1YEVIU3 .node .label,#mermaid-svg-zivG6ycuT1YEVIU3 .image-shape .label,#mermaid-svg-zivG6ycuT1YEVIU3 .icon-shape .label{text-align:center;}#mermaid-svg-zivG6ycuT1YEVIU3 .node.clickable{cursor:pointer;}#mermaid-svg-zivG6ycuT1YEVIU3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zivG6ycuT1YEVIU3 .arrowheadPath{fill:#333333;}#mermaid-svg-zivG6ycuT1YEVIU3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zivG6ycuT1YEVIU3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zivG6ycuT1YEVIU3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zivG6ycuT1YEVIU3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zivG6ycuT1YEVIU3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zivG6ycuT1YEVIU3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zivG6ycuT1YEVIU3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zivG6ycuT1YEVIU3 .cluster text{fill:#333;}#mermaid-svg-zivG6ycuT1YEVIU3 .cluster span{color:#333;}#mermaid-svg-zivG6ycuT1YEVIU3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zivG6ycuT1YEVIU3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zivG6ycuT1YEVIU3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zivG6ycuT1YEVIU3 .icon-shape,#mermaid-svg-zivG6ycuT1YEVIU3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zivG6ycuT1YEVIU3 .icon-shape p,#mermaid-svg-zivG6ycuT1YEVIU3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zivG6ycuT1YEVIU3 .icon-shape .label rect,#mermaid-svg-zivG6ycuT1YEVIU3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zivG6ycuT1YEVIU3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zivG6ycuT1YEVIU3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zivG6ycuT1YEVIU3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 推理后端
AIaaS 平台内部(gRPC)
HTTP/JSON
gRPC
gRPC
gRPC
gRPC
gRPC
gRPC 控制指令
外部客户端

HTTP/JSON
API 网关

HTTP→gRPC 转换
调度器

gRPC
推理引擎

gRPC
模型管理

gRPC
vLLM

gRPC
Triton

gRPC
自研推理

gRPC

核心原则:外部用 HTTP/JSON(兼容性优先),内部用 gRPC(性能优先)。API 网关承担协议转换的职责。


二、Protocol Buffers 定义推理服务

gRPC 的第一步是写 .proto 文件定义服务接口和数据结构。Protobuf 是语言无关的,同一份 .proto 可以生成 Go、Python、C++、Java 等多种语言的代码。

2.1 推理服务的 proto 定义

创建 proto/inference.proto

protobuf 复制代码
syntax = "proto3";

package aias.inference.v1;

option go_package = "aias-gateway/proto/inference;inferencepb";

// InferenceService 推理服务定义
service InferenceService {
  // ChatCompletion 非流式推理(一元 RPC)
  rpc ChatCompletion(CompletionRequest) returns (CompletionResponse);

  // StreamChat 流式推理(服务端流式 RPC)
  rpc StreamChat(CompletionRequest) returns (stream CompletionChunk);

  // Embedding 文本嵌入
  rpc Embedding(EmbeddingRequest) returns (EmbeddingResponse);

  // HealthCheck 健康检查
  rpc HealthCheck(HealthRequest) returns (HealthResponse);
}

// ---- 消息定义 ----

message CompletionRequest {
  string model = 1;
  repeated Message messages = 2;
  float temperature = 3;
  int32 max_tokens = 4;
  float top_p = 5;
  repeated string stop = 6;
}

message Message {
  string role = 1;    // system / user / assistant
  string content = 2;
}

message CompletionResponse {
  string id = 1;
  string model = 2;
  int64 created = 3;  // Unix 时间戳
  Choice choice = 4;
  Usage usage = 5;
}

message Choice {
  int32 index = 1;
  Message message = 2;
  string finish_reason = 3;  // stop / length / content_filter
}

message Usage {
  int32 prompt_tokens = 1;
  int32 completion_tokens = 2;
  int32 total_tokens = 3;
}

// 流式推理的 chunk
message CompletionChunk {
  string id = 1;
  string model = 2;
  int64 created = 3;
  ChunkChoice choice = 4;
}

message ChunkChoice {
  int32 index = 1;
  Delta delta = 2;
  string finish_reason = 3;
}

message Delta {
  string role = 1;
  string content = 2;
}

// Embedding 相关
message EmbeddingRequest {
  string model = 1;
  repeated string inputs = 2;
}

message EmbeddingResponse {
  string model = 1;
  repeated EmbeddingData data = 2;
  Usage usage = 3;
}

message EmbeddingData {
  int32 index = 1;
  repeated float embedding = 2;  // 嵌入向量
}

// 健康检查
message HealthRequest {}

message HealthResponse {
  enum Status {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
  }
  Status status = 1;
  string model = 2;
  int32 loaded_models = 3;
}

2.2 proto 设计要点

要点 说明
syntax = "proto3" 使用 proto3 语法,简洁且支持更多语言
option go_package 指定生成 Go 代码的包路径
字段编号 1-15 占用 1 字节,16-2047 占 2 字节。高频字段用小编号
repeated 对应 Go 的 slice,JSON 的 array
stream 定义流式 RPC,stream 在返回值前表示服务端流
enum 从 0 开始 proto3 的第一个 enum 值必须是 0,作为默认值

2.3 生成 Go 代码

安装 protoc 工具链:

bash 复制代码
# 安装 protoc 编译器(以 Ubuntu 为例)
apt install -y protobuf-compiler

# 安装 Go 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# 生成代码
protoc --go_out=. --go-grpc_out=. \
  proto/inference.proto

生成的代码包含两部分:

  • inference.pb.go:消息结构体(CompletionRequestMessage 等)
  • inference_grpc.pb.go:gRPC 服务端接口和客户端 stub

不要手写这些代码。每次 .proto 变更后重新生成。生成的代码应该提交到版本控制(或通过 CI 生成),避免开发者本地 protoc 版本不一致。


三、gRPC 服务端实现

3.1 实现推理服务

go 复制代码
package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"log"
	"net"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	inferencepb "aias-gateway/proto/inference"
)

// inferenceServer 实现 InferenceService 接口
type inferenceServer struct {
	inferencepb.UnimplementedInferenceServiceServer
	models map[string]bool // 已加载的模型
}

func newInferenceServer() *inferenceServer {
	return &inferenceServer{
		models: map[string]bool{
			"llama-3.1-70b": true,
			"qwen-2.5-72b":  true,
		},
	}
}

// generateID 生成唯一的 completion ID
func generateID() string {
	b := make([]byte, 12)
	rand.Read(b)
	return "inference-" + hex.EncodeToString(b)
}

// ChatCompletion 非流式推理
func (s *inferenceServer) ChatCompletion(
	ctx context.Context, req *inferencepb.CompletionRequest,
) (*inferencepb.CompletionResponse, error) {

	// 验证模型是否可用
	if !s.models[req.GetModel()] {
		return nil, status.Errorf(codes.NotFound,
			"model '%s' not found", req.GetModel())
	}

	// 模拟推理过程(实际调用推理引擎)
	start := time.Now()
	content := s.mockInference(req)
	promptTokens := countTokens(req.GetMessages())
	completionTokens := countTokensString(content)

	log.Printf("ChatCompletion model=%s prompt_tokens=%d completion_tokens=%d duration=%v",
		req.GetModel(), promptTokens, completionTokens, time.Since(start))

	return &inferencepb.CompletionResponse{
		Id:      generateID(),
		Model:   req.GetModel(),
		Created: time.Now().Unix(),
		Choice: &inferencepb.Choice{
			Index: 0,
			Message: &inferencepb.Message{
				Role:    "assistant",
				Content: content,
			},
			FinishReason: "stop",
		},
		Usage: &inferencepb.Usage{
			PromptTokens:     int32(promptTokens),
			CompletionTokens: int32(completionTokens),
			TotalTokens:      int32(promptTokens + completionTokens),
		},
	}, nil
}

// StreamChat 服务端流式推理
func (s *inferenceServer) StreamChat(
	req *inferencepb.CompletionRequest,
	stream inferencepb.InferenceService_StreamChatServer,
) error {

	if !s.models[req.GetModel()] {
		return status.Errorf(codes.NotFound,
			"model '%s' not found", req.GetModel())
	}

	completionID := generateID()
	created := time.Now().Unix()

	// 模拟逐 token 生成
	content := s.mockInference(req)
	tokens := tokenize(content)

	// 第一个 chunk:发送 role
	if err := stream.Send(&inferencepb.CompletionChunk{
		Id:      completionID,
		Model:   req.GetModel(),
		Created: created,
		Choice: &inferencepb.ChunkChoice{
			Index: 0,
			Delta: &inferencepb.Delta{Role: "assistant"},
		},
	}); err != nil {
		return err
	}

	// 逐 token 发送
	for i, token := range tokens {
		// 检查客户端是否断开
		if stream.Context().Err() != nil {
			log.Printf("client disconnected at token %d", i)
			return stream.Context().Err()
		}

		// 模拟生成延迟
		time.Sleep(20 * time.Millisecond)

		if err := stream.Send(&inferencepb.CompletionChunk{
			Id:      completionID,
			Model:   req.GetModel(),
			Created: created,
			Choice: &inferencepb.ChunkChoice{
				Index: 0,
				Delta: &inferencepb.Delta{Content: token},
			},
		}); err != nil {
			return err
		}
	}

	// 最后一个 chunk:finish_reason
	return stream.Send(&inferencepb.CompletionChunk{
		Id:      completionID,
		Model:   req.GetModel(),
		Created: created,
		Choice: &inferencepb.ChunkChoice{
			Index:        0,
			Delta:        &inferencepb.Delta{},
			FinishReason: "stop",
		},
	})
}

// Embedding 文本嵌入
func (s *inferenceServer) Embedding(
	ctx context.Context, req *inferencepb.EmbeddingRequest,
) (*inferencepb.EmbeddingResponse, error) {

	data := make([]*inferencepb.EmbeddingData, 0, len(req.GetInputs()))
	for i, input := range req.GetInputs() {
		// 模拟生成 768 维向量
		vec := mockEmbedding(input, 768)
		data = append(data, &inferencepb.EmbeddingData{
			Index:     int32(i),
			Embedding: vec,
		})
	}

	totalTokens := 0
	for _, input := range req.GetInputs() {
		totalTokens += countTokensString(input)
	}

	return &inferencepb.EmbeddingResponse{
		Model: req.GetModel(),
		Data:  data,
		Usage: &inferencepb.Usage{
			PromptTokens: int32(totalTokens),
			TotalTokens:  int32(totalTokens),
		},
	}, nil
}

// HealthCheck 健康检查
func (s *inferenceServer) HealthCheck(
	ctx context.Context, req *inferencepb.HealthRequest,
) (*inferencepb.HealthResponse, error) {
	return &inferencepb.HealthResponse{
		Status:       inferencepb.HealthResponse_SERVING,
		LoadedModels: int32(len(s.models)),
	}, nil
}

// ---- 辅助函数 ----

func (s *inferenceServer) mockInference(req *inferencepb.CompletionRequest) string {
	lastMsg := req.GetMessages()[len(req.GetMessages())-1]
	return fmt.Sprintf("收到你的问题:%s。这是一个模拟的推理结果。", lastMsg.GetContent())
}

func countTokens(messages []*inferencepb.Message) int {
	total := 0
	for _, msg := range messages {
		total += countTokensString(msg.GetContent()) + 4
	}
	return total
}

func countTokensString(s string) int {
	return len(s) / 4 // 粗略估算
}

func tokenize(s string) []string {
	// 简化:按空格分词
	var tokens []string
	current := ""
	for _, r := range s {
		if r == ' ' || r == ',' || r == '。' {
			if current != "" {
				tokens = append(tokens, current)
			}
			tokens = append(tokens, string(r))
			current = ""
		} else {
			current += string(r)
		}
	}
	if current != "" {
		tokens = append(tokens, current)
	}
	return tokens
}

func mockEmbedding(text string, dim int) []float32 {
	vec := make([]float32, dim)
	for i := range vec {
		vec[i] = float32(len(text)+i%100) / 100.0
	}
	return vec
}

3.2 启动 gRPC 服务端

go 复制代码
package main

import (
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	inferencepb "aias-gateway/proto/inference"
)

func main() {
	// 监听 TCP 端口
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// 创建 gRPC Server(注册拦截器,下一节详述)
	s := grpc.NewServer(
		grpc.UnaryInterceptor(unaryInterceptorChain(
			loggingUnaryInterceptor,
			metricsUnaryInterceptor,
		)),
		grpc.StreamInterceptor(streamInterceptorChain(
			loggingStreamInterceptor,
			metricsStreamInterceptor,
		)),
	)

	// 注册推理服务
	inferencepb.RegisterInferenceServiceServer(s, newInferenceServer())

	// 注册反射服务(方便 grpcurl 调试)
	reflection.Register(s)

	log.Println("gRPC inference server listening on :50051")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

3.3 用 grpcurl 调试

服务启动后,可以用 grpcurl 命令行工具调试(类似 curl for gRPC):

bash 复制代码
# 安装
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest

# 列出可用服务
grpcurl -plaintext localhost:50051 list

# 调用健康检查
grpcurl -plaintext -d '{}' localhost:50051 \
  aias.inference.v1.InferenceService/HealthCheck

# 调用非流式推理
grpcurl -plaintext -d '{
  "model": "llama-3.1-70b",
  "messages": [{"role":"user","content":"hello"}],
  "max_tokens": 100
}' localhost:50051 \
  aias.inference.v1.InferenceService/ChatCompletion

四、gRPC 拦截器

拦截器(Interceptor)是 gRPC 的中间件机制,类似 HTTP 中间件,但分为 Unary(一元)和 Stream(流式)两类。认证、日志、指标、重试等横切关注点都通过拦截器实现。

4.1 Unary 拦截器

go 复制代码
package main

import (
	"context"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

// loggingUnaryInterceptor 日志拦截器
func loggingUnaryInterceptor(
	ctx context.Context, req interface{},
	info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (interface{}, error) {
	start := time.Now()

	// 调用实际 handler
	resp, err := handler(ctx, req)

	// 记录请求日志
	duration := time.Since(start)
	statusCode := "OK"
	if err != nil {
		statusCode = "ERROR"
	}
	log.Printf("[gRPC unary] method=%s duration=%s status=%s",
		info.FullMethod, duration, statusCode)

	return resp, err
}

// metricsUnaryInterceptor 指标采集拦截器
func metricsUnaryInterceptor(
	ctx context.Context, req interface{},
	info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (interface{}, error) {
	start := time.Now()
	resp, err := handler(ctx, req)
	duration := time.Since(start).Seconds()

	// 记录 Prometheus 指标(示例用日志替代)
	method := info.FullMethod
	status := "success"
	if err != nil {
		status = "error"
	}
	log.Printf("[METRIC] grpc_method=%q duration=%.4f status=%s",
		method, duration, status)

	return resp, err
}

// authUnaryInterceptor 认证拦截器(从 metadata 提取 Token)
func authUnaryInterceptor(validTokens map[string]string // token → tenantID
) grpc.UnaryServerInterceptor {
	return func(
		ctx context.Context, req interface{},
		info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
	) (interface{}, error) {
		// 健康检查跳过认证
		if info.FullMethod == "/aias.inference.v1.InferenceService/HealthCheck" {
			return handler(ctx, req)
		}

		// 从 metadata 提取 authorization
		md, ok := metadata.FromIncomingContext(ctx)
		if !ok {
			return nil, status.Error(codes.Unauthenticated, "metadata required")
		}
		tokens := md.Get("authorization")
		if len(tokens) == 0 {
			return nil, status.Error(codes.Unauthenticated, "authorization token required")
		}

		token := tokens[0]
		tenantID, ok := validTokens[token]
		if !ok {
			return nil, status.Error(codes.Unauthenticated, "invalid token")
		}

		// 将 tenantID 注入 context
		ctx = context.WithValue(ctx, tenantKey, tenantID)
		return handler(ctx, req)
	}
}

// unaryInterceptorChain 组合多个 Unary 拦截器
func unaryInterceptorChain(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor {
	return func(
		ctx context.Context, req interface{},
		info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
	) (interface{}, error) {
		// 从后往前包装
	 chain := handler
		for i := len(interceptors) - 1; i >= 0; i-- {
			chain = wrapUnary(interceptors[i], info, chain)
		}
		return chain(ctx, req)
	}
}

func wrapUnary(interceptor grpc.UnaryServerInterceptor,
	info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) grpc.UnaryHandler {
	return func(ctx context.Context, req interface{}) (interface{}, error) {
		return interceptor(ctx, req, info, handler)
	}
}

4.2 Stream 拦截器

流式 RPC 用 StreamServerInterceptor,签名不同:

go 复制代码
// loggingStreamInterceptor 流式日志拦截器
func loggingStreamInterceptor(
	srv interface{},
	ss grpc.ServerStream,
	info *grpc.StreamServerInfo,
	handler grpc.StreamHandler,
) error {
	start := time.Now()
	log.Printf("[gRPC stream START] method=%s client_stream=%v server_stream=%v",
		info.FullMethod, info.IsClientStream, info.IsServerStream)

	err := handler(srv, ss)

	log.Printf("[gRPC stream END] method=%s duration=%s err=%v",
		info.FullMethod, time.Since(start), err)
	return err
}

// metricsStreamInterceptor 流式指标拦截器
func metricsStreamInterceptor(
	srv interface{},
	ss grpc.ServerStream,
	info *grpc.StreamServerInfo,
	handler grpc.StreamHandler,
) error {
	start := time.Now()
	err := handler(srv, ss)
	duration := time.Since(start).Seconds()

	status := "success"
	if err != nil {
		status = "error"
	}
	log.Printf("[METRIC] grpc_stream_method=%q duration=%.4f status=%s",
		info.FullMethod, duration, status)
	return err
}

// streamInterceptorChain 组合多个 Stream 拦截器
func streamInterceptorChain(interceptors ...grpc.StreamServerInterceptor) grpc.StreamServerInterceptor {
	return func(
		srv interface{},
		ss grpc.ServerStream,
		info *grpc.StreamServerInfo,
		handler grpc.StreamHandler,
	) error {
		chain := handler
		for i := len(interceptors) - 1; i >= 0; i-- {
			chain = wrapStream(interceptors[i], info, chain)
		}
		return chain(srv, ss)
	}
}

func wrapStream(interceptor grpc.StreamServerInterceptor,
	info *grpc.StreamServerInfo, handler grpc.StreamHandler) grpc.StreamHandler {
	return func(srv interface{}, ss grpc.ServerStream) error {
		return interceptor(srv, ss, info, handler)
	}
}

4.3 拦截器执行时序

推理 Handler 指标拦截器 日志拦截器 认证拦截器 gRPC 服务端 gRPC 客户端 推理 Handler 指标拦截器 日志拦截器 认证拦截器 gRPC 服务端 gRPC 客户端 #mermaid-svg-yCQ5eo5w3h2yTvrg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yCQ5eo5w3h2yTvrg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yCQ5eo5w3h2yTvrg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yCQ5eo5w3h2yTvrg .error-icon{fill:#552222;}#mermaid-svg-yCQ5eo5w3h2yTvrg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yCQ5eo5w3h2yTvrg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yCQ5eo5w3h2yTvrg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yCQ5eo5w3h2yTvrg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yCQ5eo5w3h2yTvrg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yCQ5eo5w3h2yTvrg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yCQ5eo5w3h2yTvrg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yCQ5eo5w3h2yTvrg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yCQ5eo5w3h2yTvrg .marker.cross{stroke:#333333;}#mermaid-svg-yCQ5eo5w3h2yTvrg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yCQ5eo5w3h2yTvrg p{margin:0;}#mermaid-svg-yCQ5eo5w3h2yTvrg .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yCQ5eo5w3h2yTvrg text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-yCQ5eo5w3h2yTvrg .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yCQ5eo5w3h2yTvrg .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-yCQ5eo5w3h2yTvrg .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-yCQ5eo5w3h2yTvrg .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-yCQ5eo5w3h2yTvrg #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-yCQ5eo5w3h2yTvrg .sequenceNumber{fill:white;}#mermaid-svg-yCQ5eo5w3h2yTvrg #sequencenumber{fill:#333;}#mermaid-svg-yCQ5eo5w3h2yTvrg #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-yCQ5eo5w3h2yTvrg .messageText{fill:#333;stroke:none;}#mermaid-svg-yCQ5eo5w3h2yTvrg .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yCQ5eo5w3h2yTvrg .labelText,#mermaid-svg-yCQ5eo5w3h2yTvrg .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-yCQ5eo5w3h2yTvrg .loopText,#mermaid-svg-yCQ5eo5w3h2yTvrg .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-yCQ5eo5w3h2yTvrg .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-yCQ5eo5w3h2yTvrg .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-yCQ5eo5w3h2yTvrg .noteText,#mermaid-svg-yCQ5eo5w3h2yTvrg .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-yCQ5eo5w3h2yTvrg .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yCQ5eo5w3h2yTvrg .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yCQ5eo5w3h2yTvrg .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-yCQ5eo5w3h2yTvrg .actorPopupMenu{position:absolute;}#mermaid-svg-yCQ5eo5w3h2yTvrg .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-yCQ5eo5w3h2yTvrg .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-yCQ5eo5w3h2yTvrg .actor-man circle,#mermaid-svg-yCQ5eo5w3h2yTvrg line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-yCQ5eo5w3h2yTvrg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt Token 无效 Token 有效 gRPC 调用(metadata 含 authorization) 提取并验证 Token Unauthenticated 错误 注入 tenantID 到 ctx 记录开始时间 透传请求 执行推理 返回响应 记录延迟指标 返回响应 记录请求日志 返回最终响应


五、gRPC 客户端实现

5.1 基本客户端

go 复制代码
package main

import (
	"context"
	"io"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	inferencepb "aias-gateway/proto/inference"
)

type InferenceClient struct {
	conn   *grpc.ClientConn
	client inferencepb.InferenceServiceClient
}

func NewInferenceClient(addr string) (*InferenceClient, error) {
	// 建立连接(带重试和超时配置)
	conn, err := grpc.Dial(addr,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithDefaultCallOptions(
			grpc.MaxCallRecvMsgSize(64*1024*1024), // 64MB
		),
		grpc.WithUnaryInterceptor(clientRetryInterceptor),
	)
	if err != nil {
		return nil, err
	}
	return &InferenceClient{
		conn:   conn,
		client: inferencepb.NewInferenceServiceClient(conn),
	}, nil
}

func (c *InferenceClient) Close() error {
	return c.conn.Close()
}

// ChatCompletion 非流式推理
func (c *InferenceClient) ChatCompletion(
	ctx context.Context, model string, messages []*inferencepb.Message,
) (*inferencepb.CompletionResponse, error) {

	req := &inferencepb.CompletionRequest{
		Model:    model,
		Messages: messages,
		MaxTokens: 1000,
	}
	return c.client.ChatCompletion(ctx, req)
}

// StreamChat 流式推理(返回一个 chunk 通道,便于消费)
func (c *InferenceClient) StreamChat(
	ctx context.Context, model string, messages []*inferencepb.Message,
) (<-chan *inferencepb.CompletionChunk, error) {

	req := &inferencepb.CompletionRequest{
		Model:    model,
		Messages: messages,
		MaxTokens: 1000,
	}

	stream, err := c.client.StreamChat(ctx, req)
	if err != nil {
		return nil, err
	}

	ch := make(chan *inferencepb.CompletionChunk, 100)
	go func() {
		defer close(ch)
		for {
			chunk, err := stream.Recv()
			if err == io.EOF {
				return
			}
			if err != nil {
				log.Printf("stream recv error: %v", err)
				return
			}
			ch <- chunk
		}
	}()
	return ch, nil
}

// HealthCheck 健康检查
func (c *InferenceClient) HealthCheck(ctx context.Context) (*inferencepb.HealthResponse, error) {
	return c.client.HealthCheck(ctx, &inferencepb.HealthRequest{})
}

// clientRetryInterceptor 客户端重试拦截器
func clientRetryInterceptor(
	ctx context.Context, method string, req, reply interface{},
	cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption,
) error {
	var lastErr error
	for attempt := 0; attempt < 3; attempt++ {
		lastErr = invoker(ctx, method, req, reply, cc, opts...)
		if lastErr == nil {
			return nil
		}
		// 指数退避
		backoff := time.Duration(1<<attempt) * time.Second
		log.Printf("attempt %d failed: %v, retrying in %v", attempt+1, lastErr, backoff)
		select {
		case <-time.After(backoff):
		case <-ctx.Done():
			return ctx.Err()
		}
	}
	return lastErr
}

5.2 客户端使用示例

go 复制代码
func main() {
	client, err := NewInferenceClient("localhost:50051")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// 1. 非流式推理
	resp, err := client.ChatCompletion(ctx, "llama-3.1-70b", []*inferencepb.Message{
		{Role: "system", Content: "你是助手"},
		{Role: "user", Content: "用 Go 写 hello world"},
	})
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("response: %s", resp.Choice.Message.Content)
	log.Printf("usage: prompt=%d completion=%d total=%d",
		resp.Usage.PromptTokens, resp.Usage.CompletionTokens, resp.Usage.TotalTokens)

	// 2. 流式推理
	ch, err := client.StreamChat(ctx, "llama-3.1-70b", []*inferencepb.Message{
		{Role: "user", Content: "讲一个笑话"},
	})
	if err != nil {
		log.Fatal(err)
	}
	for chunk := range ch {
		if chunk.Choice.Delta.Content != "" {
			fmt.Print(chunk.Choice.Delta.Content)
		}
	}
	fmt.Println()
}

5.3 客户端→服务端完整调用时序

StreamChat Handler gRPC 服务端 ClientConn gRPC 客户端 StreamChat Handler gRPC 服务端 ClientConn gRPC 客户端 #mermaid-svg-ysNKReurF4pIwJQ1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ysNKReurF4pIwJQ1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ysNKReurF4pIwJQ1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ysNKReurF4pIwJQ1 .error-icon{fill:#552222;}#mermaid-svg-ysNKReurF4pIwJQ1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ysNKReurF4pIwJQ1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ysNKReurF4pIwJQ1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ysNKReurF4pIwJQ1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ysNKReurF4pIwJQ1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ysNKReurF4pIwJQ1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ysNKReurF4pIwJQ1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ysNKReurF4pIwJQ1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ysNKReurF4pIwJQ1 .marker.cross{stroke:#333333;}#mermaid-svg-ysNKReurF4pIwJQ1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ysNKReurF4pIwJQ1 p{margin:0;}#mermaid-svg-ysNKReurF4pIwJQ1 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ysNKReurF4pIwJQ1 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ysNKReurF4pIwJQ1 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ysNKReurF4pIwJQ1 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ysNKReurF4pIwJQ1 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ysNKReurF4pIwJQ1 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ysNKReurF4pIwJQ1 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ysNKReurF4pIwJQ1 .sequenceNumber{fill:white;}#mermaid-svg-ysNKReurF4pIwJQ1 #sequencenumber{fill:#333;}#mermaid-svg-ysNKReurF4pIwJQ1 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ysNKReurF4pIwJQ1 .messageText{fill:#333;stroke:none;}#mermaid-svg-ysNKReurF4pIwJQ1 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ysNKReurF4pIwJQ1 .labelText,#mermaid-svg-ysNKReurF4pIwJQ1 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ysNKReurF4pIwJQ1 .loopText,#mermaid-svg-ysNKReurF4pIwJQ1 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ysNKReurF4pIwJQ1 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ysNKReurF4pIwJQ1 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ysNKReurF4pIwJQ1 .noteText,#mermaid-svg-ysNKReurF4pIwJQ1 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ysNKReurF4pIwJQ1 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ysNKReurF4pIwJQ1 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ysNKReurF4pIwJQ1 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ysNKReurF4pIwJQ1 .actorPopupMenu{position:absolute;}#mermaid-svg-ysNKReurF4pIwJQ1 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ysNKReurF4pIwJQ1 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ysNKReurF4pIwJQ1 .actor-man circle,#mermaid-svg-ysNKReurF4pIwJQ1 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ysNKReurF4pIwJQ1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 逐 token 生成 StreamChat(req) HTTP/2 HEADERS + DATA(Protobuf 编码) 调用 StreamChat(req, stream) 生成 token stream.Send(chunk) HTTP/2 DATA frame chunk 到达 channel return nil(流结束) HTTP/2 DATA + END_STREAM channel 关闭(io.EOF)


六、gRPC 进阶配置

6.1 Keepalive 与连接保活

gRPC 基于 HTTP/2 长连接。如果中间有 NAT 或负载均衡器,空闲连接可能被静默断开。配置 Keepalive 保活:

go 复制代码
import (
	"google.golang.org/grpc/keepalive"
)

// 服务端 Keepalive 配置
s := grpc.NewServer(
	grpc.KeepaliveParams(keepalive.ServerParameters{
		MaxConnectionIdle:     5 * time.Minute,  // 空闲连接最大存活
		MaxConnectionAge:      30 * time.Minute,  // 连接最大年龄(强制重建)
		MaxConnectionAgeGrace: 5 * time.Second,   // 强制关闭前的宽限
		Time:                  30 * time.Second,  // 空闲多久后发 ping
		Timeout:               10 * time.Second,  // ping 超时
	}),
)

// 客户端 Keepalive 配置
conn, _ := grpc.Dial(addr,
	grpc.WithKeepaliveParams(keepalive.ClientParameters{
		Time:                30 * time.Second,
		Timeout:             10 * time.Second,
		PermitWithoutStream: true, // 无活跃流时也发 ping
	}),
)

6.2 健康检查协议

gRPC 有标准的健康检查协议(grpc.health.v1.Health),让负载均衡器能探测服务状态:

go 复制代码
import (
	healthpb "google.golang.org/grpc/health/proto"
	"google.golang.org/grpc/health"
)

// 服务端注册健康检查
s := grpc.NewServer()
healthSvc := health.NewServer()
healthSvc.SetServingStatus("aias.inference.v1.InferenceService",
	healthpb.HealthCheckResponse_SERVING)
healthpb.RegisterHealthServer(s, healthSvc)

6.3 错误处理与状态码

gRPC 使用标准状态码(codes 包),不要用字符串传递错误:

状态码 含义 HTTP 对应
OK 成功 200
InvalidArgument 参数错误 400
Unauthenticated 未认证 401
NotFound 资源不存在 404
ResourceExhausted 限流 429
Internal 内部错误 500
Unavailable 服务不可用 503
go 复制代码
// 返回带详细信息的错误
return nil, status.Errorf(codes.ResourceExhausted,
	"rate limit: %d/%d requests per minute", current, limit)

// 客户端判断错误类型
if status.Code(err) == codes.ResourceExhausted {
	// 限流了,退避重试
}

七、gRPC vs HTTP 性能对比实测

7.1 Benchmark

用 Go benchmark 对比 gRPC 和 HTTP/JSON 的性能:

go 复制代码
package main

import (
	"testing"
)

// 模拟推理请求体
var testMessages = []*inferencepb.Message{
	{Role: "user", Content: strings.Repeat("这是一个测试消息。", 100)},
}

func BenchmarkGRPC_ChatCompletion(b *testing.B) {
	client, _ := NewInferenceClient("localhost:50051")
	defer client.Close()
	ctx := context.Background()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := client.ChatCompletion(ctx, "llama-3.1-70b", testMessages)
		if err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkHTTP_JSON(b *testing.B) {
	body, _ := json.Marshal(map[string]interface{}{
		"model":    "llama-3.1-70b",
		"messages": []map[string]string{{"role": "user", "content": strings.Repeat("这是一个测试消息。", 100)}},
	})
	client := &http.Client{Timeout: 30 * time.Second}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		resp, err := client.Post("http://localhost:8080/v1/chat/completions",
			"application/json", bytes.NewReader(body))
		if err != nil {
			b.Fatal(err)
		}
		io.Copy(io.Discard, resp.Body)
		resp.Body.Close()
	}
}

7.2 典型结果

协议 平均延迟 (ms) P99 延迟 (ms) 序列化占比
gRPC 2.1 5.2 12%
HTTP/JSON 5.8 12.1 38%

gRPC 在序列化和传输上全面领先,尤其在高并发下差距更明显(HTTP/1.1 连接数限制 vs HTTP/2 多路复用)。

7.3 何时该用 gRPC

场景 推荐
平台内部服务间调用 ✅ gRPC
网关 → 推理引擎 ✅ gRPC
跨语言调用(Go ↔ Python) ✅ gRPC
面向外部开发者的 API ❌ HTTP/JSON(兼容性)
浏览器直接调用 ❌ HTTP/JSON 或 grpc-web
简单脚本/curl 调试 ❌ HTTP/JSON

本篇小结

知识点 核心内容
gRPC 优势 Protobuf 二进制序列化快 3-10x,HTTP/2 多路复用,原生双向流
proto 定义 服务用 service,流式用 stream 关键字,字段编号越小越省空间
服务端 实现 InferenceServiceServer 接口,stream.Send() 推送 chunk
拦截器 Unary(一元)和 Stream(流式)两类,链式组合
客户端 grpc.Dial 建连接,stream.Recv() 接收 chunk
性能优化 Keepalive 保活、健康检查协议、正确状态码

下篇预告

第 25 篇:Go 与 Python 推理引擎的协作架构

gRPC 只是 Go 和 Python 协作的方式之一。下一篇我们系统梳理 Go 网关与 Python 推理引擎的四种协作模式:gRPC 跨进程、CGO 直接调用 C/C++ 内核、共享内存传输大张量、Unix Domain Socket IPC,并对比各模式的延迟与复杂度。


如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。

相关推荐
码匠许师傅41 分钟前
【C++ 面试真题】聊聊 C++ 的关联容器
开发语言·c++·面试
hyunbar77742 分钟前
玩转LlamaIndex:流式输出与并发执行
人工智能
哈__42 分钟前
Windows部署Next AI Draw.io:用大模型生成并修改draw.io图表
人工智能·draw.io
2301_7803567042 分钟前
【技术解码】全视通“物联数据中台”如何打通医养结合全链路?
大数据·人工智能·物联网
乔氪智造43 分钟前
21 个免费大模型入口,实测只有 9 个能用
人工智能
正经教主43 分钟前
AI提示词工程(高阶)第15课:ReAct模式与工具调用
人工智能
不好听61343 分钟前
NestJS 是什么?一张图看懂企业级后端的骨架
后端·nestjs
SixHateSeven1 小时前
用 TRAE 自动生成股票分析报告,从敲命令到一句话搞定
人工智能
liuxiaocheng1 小时前
文本生成的进阶:generateText / streamText 里迟早会撞上的东西
前端·后端·ai编程