net/rpc 不支持优先级调度,需在网络层或协议解析层通过自定义 listener、分离的 high/low 两级 channel 队列及独立 goroutine 消费者实现,核心是收发解耦。

Go 的 net/rpc 本身不支持优先级调度
直接说结论:net/rpc 是同步阻塞式设计,没有内置队列、更无优先级概念。所有请求按接收顺序串行处理(除非你手动启多 goroutine 跑服务端方法,但那会破坏 RPC 协议语义)。想实现优先级,必须在它外面套一层任务分发逻辑。
常见错误现象是:高优先级心跳请求被低优先级日志上报卡住,响应延迟突增;或者用 rpc.ServeConn 直接处理连接,误以为加个 select 就能切优先级 —— 实际上底层 DecodeRequestHeader 已经阻塞在读流上了。
- 优先级必须在请求解码前介入,即在网络层或协议解析层做分流
- 不要试图 patch
Server.ServeCodec来插入调度逻辑 —— 它假设每个 codec 对应单个有序请求流 - 推荐路径:用自定义 listener + 分离的优先级队列 + 独立 goroutine 池消费
用 channel + select 实现两级优先队列最轻量
不需要引入完整任务队列库(如 asynq、machinery),Go 原生并发原语就能搞定核心逻辑。关键是把“接收”和“执行”彻底拆开,中间用带缓冲的 channel 隔离。
使用场景:内部微服务间调用,QPS 不超过 5k,优先级维度简单(比如 high/low 两级)。
立即学习“go语言免费学习笔记(深入)”;
- 声明两个 channel:
highQ和lowQ,类型为chan *rpc.Request - 自定义
net.Listener的Accept方法,在读取完完整 RPC 帧后,根据请求 header 中的自定义字段(如X-Priority: high)决定投递到哪个 channel - 启动两个消费者 goroutine,
highQ消费者永远用select优先尝试收,lowQ消费者只在highQ空时才取 - 注意:channel 缓冲区大小要设合理(如 1024),避免突发高优请求打爆内存
select {
case req := <-highQ:
handle(req)
default:
select {
case req := <-lowQ:
handle(req)
default:
}
}
context.Context 不能用于跨 RPC 调用传递优先级
很多人想在 client 端用 ctx.WithValue 塞优先级,再在 server 端从 context 里取 —— 这行不通。net/rpc 不透传 context,server 端收到的始终是空 context 或你硬塞的默认值。
真正能落地的方式只有两种:
- 在 RPC 请求体里显式加字段,比如扩展 struct:
type PriorityArgs struct { Priority string; Payload interface{} } - 复用 HTTP transport 时,走
http.Header注入(X-Priority),然后在自定义 listener 解析阶段提取 - 避免用 gRPC 的 metadata 方式去类比 ——
net/rpc没有 metadata 抽象层 - 如果用 JSON-RPC over HTTP,header 方式最干净;如果是 TCP 自定义协议,必须改编码格式,否则无法在不解包 payload 的前提下识别优先级
goroutine 泄漏比性能瓶颈更常出问题
优先级队列容易让人忽略资源回收。典型泄漏点:client 断连后,server 还在往已关闭的 channel 发请求;或 handler panic 后没 recover,导致消费者 goroutine 退出,高优请求堆积。
- 所有写 channel 操作必须加
select+default或用len(ch) 判断是否可写 - 每个 handler 外层包
defer func(){if r:=recover();r!=nil{log.Printf("panic: %v", r)}}() - 不要用
for range ch消费 channel —— 如果 channel 永远不 close,goroutine 永不退出 - 监控项建议加:各队列当前长度、消费者 goroutine 数、平均等待时间(用
time.Since(req.Timestamp)记录)
优先级真正的复杂点不在调度算法,而在于如何让“高优”不变成“永远优”——得给低优任务留活口,否则系统吞吐会隐性归零。这点最容易被忽略。










