如何使用Golang进行RPC压测
在微服务架构中,RPC(远程过程调用)服务的性能直接影响整个系统的稳定性与响应速度。当业务规模扩大时,我们需要提前评估RPC服务的承载能力,这时候压测就成为必不可少的环节。Golang本身标准库和生态中提供了丰富的工具支持,我们可以用它快速实现RPC压测逻辑,灵活模拟不同场景的调用压力。
压测前的准备工作
首先我们需要明确压测的目标RPC服务信息,包括服务地址、接口定义、请求参数格式等。如果是基于Golang标准库net/rpc开发的服务,需要先确保客户端能够正常调用服务端接口,再在此基础上扩展压测逻辑。如果是其他RPC框架(如gRPC),只需要替换对应的客户端调用逻辑即可,整体压测框架可以复用。
我们需要引入几个核心依赖:
- 标准库net/rpc:如果是对标准库RPC服务压测,直接引入即可
- 标准库time:用于统计请求耗时、控制压测节奏
- 标准库sync:用于协调多个并发协程的执行
- 第三方库github.com/streadway/quantile:用于统计请求耗时的分位数指标,可选引入
基础RPC压测实现
以下是一个针对标准库net/rpc服务的压测示例,支持设置并发数、总请求数,统计成功率、平均耗时、最大最小耗时等核心指标。
package main
import (
"fmt"
"net/rpc"
"sync"
"time"
"github.com/streadway/quantile"
)
// 定义RPC请求参数结构,需要和实际服务的参数一致
type RpcRequest struct {
UserId int64
Name string
}
// 定义RPC响应结果结构,需要和实际服务的返回一致
type RpcResponse struct {
Code int
Message string
Data string
}
func main() {
// 压测配置参数
serverAddr := "127.0.0.1:8080" // RPC服务地址
concurrency := 100 // 并发数
totalRequests := 10000 // 总请求数
timeout := 3 * time.Second // 单个请求超时时间
// 初始化统计指标
var successCount, failCount int64
var totalDuration time.Duration
var maxDuration, minDuration time.Duration
maxDuration = 0
minDuration = time.Hour // 初始化为很大的值,方便后续比较
// 初始化分位数统计器,设置精度和分位数区间
quantileConfig := quantile.Config{
Epsilon: 0.01, // 误差范围
}
quantileEstimator := quantile.New(quantileConfig)
targetQuantiles := []float64{0.5, 0.9, 0.95, 0.99} // 需要统计的分位数
// 用于控制并发的等待组
var wg sync.WaitGroup
// 请求计数器,控制总请求数
requestChan := make(chan struct{}, concurrency)
// 启动并发协程
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func(workerId int) {
defer wg.Done()
// 每个协程循环发送请求,直到达到总请求数
for {
// 从通道获取请求令牌,没有令牌则退出
_, ok := <-requestChan
if !ok {
return
}
// 建立RPC连接,这里每次请求新建连接模拟真实场景,也可以复用连接优化
client, err := rpc.DialTimeout("tcp", serverAddr, timeout)
if err != nil {
failCount++
continue
}
// 构造请求参数
req := &RpcRequest{
UserId: int64(workerId),
Name: fmt.Sprintf("test-user-%d", workerId),
}
var resp RpcResponse
// 记录请求开始时间
startTime := time.Now()
// 调用RPC接口,假设服务端的接口名为"UserService.GetUserInfo"
callErr := client.Call("UserService.GetUserInfo", req, &resp)
// 计算请求耗时
elapsed := time.Since(startTime)
// 关闭连接
client.Close()
// 统计结果
if callErr != nil {
failCount++
} else {
successCount++
totalDuration += elapsed
// 更新最大最小耗时
if elapsed > maxDuration {
maxDuration = elapsed
}
if elapsed < minDuration {
minDuration = elapsed
}
// 记录耗时到分位数统计器
quantileEstimator.Add(float64(elapsed.Milliseconds()))
}
}
}(i)
}
// 向通道发送总请求数的令牌,触发请求发送
for i := 0; i < totalRequests; i++ {
requestChan <- struct{}{}
}
close(requestChan)
// 等待所有协程执行完成
wg.Wait()
// 计算并输出压测结果
fmt.Println("====== RPC压测结果 ======")
fmt.Printf("服务地址:%s\n", serverAddr)
fmt.Printf("并发数:%d\n", concurrency)
fmt.Printf("总请求数:%d\n", totalRequests)
fmt.Printf("成功请求数:%d\n", successCount)
fmt.Printf("失败请求数:%d\n", failCount)
if successCount > 0 {
fmt.Printf("平均耗时:%.2f ms\n", float64(totalDuration.Milliseconds())/float64(successCount))
fmt.Printf("最大耗时:%.2f ms\n", float64(maxDuration.Milliseconds()))
fmt.Printf("最小耗时:%.2f ms\n", float64(minDuration.Milliseconds()))
// 输出分位数耗时
fmt.Println("分位数耗时:")
for _, q := range targetQuantiles {
qValue := quantileEstimator.Query(q)
fmt.Printf(" P%.0f:%.2f ms\n", q*100, qValue)
}
}
fmt.Printf("成功率:%.2f%%\n", float64(successCount)/float64(totalRequests)*100)
}上面的代码实现了基础的压测逻辑:首先配置压测的并发数、总请求数、服务地址等参数,然后通过多个goroutine并发发送RPC请求,每个请求都会统计耗时和调用结果,最后汇总输出所有核心指标。如果需要压测gRPC等其他RPC服务,只需要把建立连接和调用接口的代码片段替换成对应框架的客户端逻辑即可。
压测结果分析要点
拿到压测结果后,我们需要重点关注几个维度:
- 成功率:如果成功率低于99%,说明服务在高压力下已经出现了稳定性问题,需要先排查服务本身的问题,再继续加大压力测试
- 平均耗时与分位数耗时:平均耗时容易受极端值影响,建议重点看P90、P99等分位数耗时,它们更能反映大部分用户的实际体验
- 最大耗时:如果最大耗时远高于平均耗时,说明服务存在偶发的性能抖动,可能需要排查资源竞争、GC等问题
压测的注意事项
在实际执行压测时,有几个点需要特别注意:
- 压测客户端和服务端尽量部署在不同的机器上,避免客户端自身资源消耗影响压测结果准确性
- 压测前先对服务做一次基准测试,确认单请求耗时在正常范围,避免无效压测
- 逐步提升并发数和请求量,不要一次性打满压力,观察服务在不同压力下的表现,找到服务的性能拐点
- 如果是线上服务压测,要选择业务低峰期,并且提前做好服务降级、限流等预案,避免压测影响正常业务
除了自己编写压测代码,也可以使用现有的压测工具进行扩展,比如k6、wrk等工具支持自定义脚本,也可以结合Golang的RPC客户端逻辑实现更复杂的压测场景。如果是团队长期使用,还可以把上面的压测逻辑封装成通用的压测工具,支持通过配置文件调整压测参数,降低使用门槛。