boomer 基於 gRPC 壓測併發方案及效能測評
前言
boomer 可使用 go 指令碼定製開發 slave 的特點,使得 boomer 不再侷限於做 http/https 的壓測,對於 tcp/udp/gRPC 等協議的壓測,也有著很大的擴充套件性。本次主要是對 gRPC 壓測方案設計及探索測評。
什麼是 gRPC
優點
簡單來說,gRPC 使用 HTTP2.0 通訊,並使用 protobuf 來定義介面和資料型別。
gRPC 主要應用場景:
需要對介面進行嚴格約束的情況,比如我們提供了一個公共的服務,很多人,甚至公司外部的人也可以訪問這個服務,這時對於介面我們希望有更加嚴格的約束,我們不希望客戶端給我們傳遞任意的資料,尤其是考慮到安全性的因素,我們通常需要對介面進行更加嚴格的約束。這時 gRPC 就可以通過 protobuf 來提供嚴格的介面約束。
對於效能有更高的要求時。有時我們的服務需要傳遞大量的資料,而又希望不影響我們的效能,這個時候也可以考慮 gRPC 服務,因為通過 protobuf 我們可以將資料壓縮編碼轉化為二進位制格式,通常傳遞的資料量要小得多,而且通過 http2 我們可以實現非同步的請求,從而大大提高了通訊效率。
缺點
暫不支援分散式,負載均衡等等。
一個 gRPC 服務,就算開多個 pod,流量也只會集中在一個 pod 裡面。
因為 http2 具備更低的延遲的特點,其實是通過利用單個長期存在的 TCP 連線並在其上多路複用請求 / 響應。這會給第 4 層(L4)負載均衡器帶來問題,因為它們的級別太低,無法根據接收到的流量型別做出路由決策。這樣,嘗試對 http2 流量進行負載平衡的 L4 負載平衡器將開啟一個 TCP 連線,並將所有連續的流量路由到該相同的長期連線,從而實際上取消了負載平衡。而 k8s 的 kube-proxy 本質上是一個 L4 負載平衡器,因此我們不能依靠它來平衡微服務之間的 gRPC 呼叫。
不過不要慌,問題不大。因為 service mesh(istio)天生就支援 gRPC 負載均衡。這個暫不在本話題討論範圍內,後續大家感興趣我可以寫一下。
boomer 基於 gRPC 壓測設計
問題: gRPC client 有沒有必要用連線池?
gRPC 的 http2 元件是自己實現的,沒有采用 golang 標準庫裡的 net/http。那要不要給 gRPC 加連線池呢?
不著急盲目加池,先測一下。
先寫個非池連線的 boomer slave。
package main
import (
"flag"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
"log"
"time"
"github.com/myzhan/boomer"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
var verbose bool
var targetUrl string
var conn *grpc.ClientConn
const defaultName = "world"
func NewConn() *grpc.ClientConn {
conn, err := grpc.Dial(targetUrl, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
return conn
}
func worker() {
startTime := time.Now()
c := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
name := defaultName
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
elapsed := time.Since(startTime)
if err != nil {
if verbose {
log.Fatalf("could not greet: %v", err)
}
boomer.RecordFailure("grpc", "helloworld.proto", 0.0, err.Error())
} else {
log.Printf("Greeting: %s", r.GetMessage())
boomer.RecordSuccess("grpc", "helloworld.proto",
elapsed.Nanoseconds()/int64(time.Millisecond), 10)
}
}
func main() {
defer func() {
_ = conn.Close()
}()
flag.StringVar(&targetUrl, "url", "", "URL")
flag.Parse()
if targetUrl == "" {
log.Println("Boomer target url is null")
return
}
log.Println("Boomer target url is", string(targetUrl))
conn = NewConn()
task := &boomer.Task{
Name: "worker",
Weight: 10,
Fn: worker,
}
boomer.Run(task)
}
打包好以後,正常容器排程測試,配置 500 壓力執行緒數併發。
rps 能發出 3700+/s 的效果。
現在試一下加池設定,使用 pool 作為連線池。
//建立一個連線池: 初始化5,最大空閒連線是20,最大併發連線30
poolConfig := &pool.Config{
InitialCap: 100,//資源池初始連線數
MaxIdle: 400,//最大空閒連線數
MaxCap: 500,//最大併發連線數
Factory: factory,
Close: close,
//Ping: ping,
//連線最大空閒時間,超過該時間的連線 將會關閉,可避免空閒時連線EOF,自動失效的問題
IdleTimeout: 15 * time.Second,
}
p, err := pool.NewChannelPool(poolConfig)
if err != nil {
fmt.Println("err=", err)
}
//從連線池中取得一個連線
v, err := p.Get()
//將連線放回連線池中
p.Put(v)
//釋放連線池中的所有連線
p.Release()
池有了,再改下 invoke 的方式,採用 bugVanisher/grequester 發起請求。
// getRealMethodName
func (r *Requester) getRealMethodName() string {
return fmt.Sprintf("/%s/%s", r.service, r.method)
}
// Call 發起請求
func (r *Requester) Call(req interface{}, resp interface{}) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(r.timeoutMs)*time.Millisecond)
defer cancel()
cc, err := r.pool.Get()
if err != nil {
log.Printf("get rpc ClientConn error")
return err
}
// 必須要把連線放回連線池否則一直建立新連線
defer r.pool.Put(cc)
if err = cc.(*grpc.ClientConn).Invoke(ctx, r.getRealMethodName(), req, resp, grpc.ForceCodec(&ProtoCodec{})); err != nil {
fmt.Fprintf(os.Stderr, err.Error())
return err
}
return nil
}
打包好以後,正常容器排程測試,配置 500 壓力執行緒數併發。
rps 能發出 2900+ 的效果。
說實話問題有點大,理想當中加池應該會有更高的效能,為什麼發出的 rps 反而減低了呢?
先來對比一下火焰圖。
nonpool:
pool:
可以看到,加了 pool,其實就是把原有的 helloworld.(*greeterClient).SayHello 拆開成 grpc.(*ClientConn).Invoke,pool.(*channelPool).Get,以及 pool.(*channelPool).Put。
加 pool 的初衷是為了減少 grpc.invoke 裡面的 grpc.newClientStream 的耗時,因為建立一個新的連線,理論上是比複用分配池裡的一個連結要久的。因為 grpc.newClientStream 裡面有個 grpc.(*clientStream).withRetry,看下程式碼。
func (cs *clientStream) withRetry(op func(a *csAttempt) error, onSuccess func()) error {
cs.mu.Lock()
for {
if cs.committed {
cs.mu.Unlock()
return op(cs.attempt)
}
a := cs.attempt
cs.mu.Unlock()
err := op(a)
cs.mu.Lock()
if a != cs.attempt {
// We started another attempt already.
continue
}
if err == io.EOF {
<-a.s.Done()
}
if err == nil || (err == io.EOF && a.s.Status().Code() == codes.OK) {
onSuccess()
cs.mu.Unlock()
return err
}
if err := cs.retryLocked(err); err != nil {
cs.mu.Unlock()
return err
}
}
}
可以看到這裡加了鎖,而且粒度還不小。所以加 pool 主要是為了節省 grpc.newClientStream 的時間。
再檢視一下時間。
nonpool:
pool:
這裡看確實優化到了,但,pool 裡面還有 pool.Get 和 pool.Put 操作。
由此可見,加池主要是提高一個服務系統的吞吐量,讓它在系統可容納連線的範圍內,使用池減少產生連線的時間,更快給出響應。但是對於一個併發工具來說,能不能更快響應不重要,請求能發出去就行,要的就是產生壓力,所以它也不需要回池的操作,它只需要一股腦建立大量 goroutine 來發起連線請求產生壓力即可,所以 pool.Put 在一系列高併發操作裡面,有點浪費時間,產生的壓力,也比沒有池的要低。
所以這樣一看,還是不能盲目加池。除非 pool.Get 和 pool.Put 加起來的收益比 grpc.newClientStream 高的多,比如一些複雜業務複雜資料之類的,這裡就是簡單的 helloworld,體現不出來效果。
方案
grpc-demo:
image: shaonian/grpc-demo:latest
locust-master:
image: shaonian/locust-master:latest
ports:
- "8089:8089"
locust-slave1:
image: shaonian/locust-slave-rpc:nonpool
command:
- ./main
- --master-host=locust-master
- --master-port=5557
- --url=grpc-demo:50051
- --cpu-profile=cpu.pprof
- --cpu-profile-duration=60s
links:
- locust-master
- grpc-demo
這裡內嵌了 pprof ,剛剛上面的資料也是從這裡產生的,然後直接從容器複製出來就可以檢視分析了,不用專門改程式碼內建來看具體耗時了。
net/http 與 gRPC 效能測評
都說 gRPC 好,快,那比普通的 http 要快多少呢?
同樣都是 hello world。
net/http:
grpc:
可以看到,http 大部分時間都用來系統排程了,都浪費在 tcp 握手裡面了。
總結
至此,boomer 基於 gRPC 的壓測改造已實現,以上即為 demo 以及各種框架效能測評。
專案在這裡: shaonian/boomer_locust
快來 star 吧~
相關話題: docker && k8s 分散式壓測 locust_boomer 方案
PS:技術交流 QQ 群 552643038
相關文章
- 真刀真槍壓測:基於TCPCopy的模擬壓測方案TCP
- docker && k8s 分散式壓測 locust_boomer 方案DockerK8S分散式OOM
- 介面效能測試 —— Jmeter併發與持續性壓測JMeter
- 記一次 Boomer 壓測 MQTT 過程OOMMQQT
- 基於python+ffmpeg的視訊併發直播壓力測試Python
- Redis Primer(1)基於JedisPool的Redis hset併發效能測試Redis
- 想要完成系統效能評估? 試試【雲壓力測試 + APM】的端到端壓測解決方案
- Jmeter效能測試:高併發分散式效能測試JMeter分散式
- 魔改 locust:基於 locust 和 boomer 核心,構建一個簡單 http 介面壓測共享平臺OOMHTTP
- mysqlslap 效能壓測MySql
- mysqlslap效能壓測MySql
- 併發網站壓力測試工具網站
- 介面測試,負載測試,併發測試,壓力測試區別負載
- 軟體測評中心▏效能測試、壓力測試、負載測試有什麼區別?負載
- 效能壓測工具 —— wrk
- 介面高併發壓測入門實戰
- 關於壓力測試中 TPS 和併發數的思考
- 智慧駕駛數採及測評解決方案
- 效能測試:主流壓測工具介紹
- 使用AB對Nginx壓測和併發預估Nginx
- 壓縮工具效能測試
- (一)效能測試(壓力測試、負載測試)負載
- 基於 JMeter的壓測工具的實現JMeter
- 一次效能壓測及分析調優實踐
- 為 java 開發者設計的效能測試框架,用於壓測+測試報告生成Java框架測試報告
- Taurus.MVC 效能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET 版本MVCLinux
- 從 0 到 1 開發壓力測試框架: Python 基礎,壓測框架開發框架Python
- Taurus.MVC 效能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本MVCLinux
- 基於jmeter的效能全流程測試JMeter
- 壓測和效能分析方法論
- 容量預估/效能壓測思考
- Jmeter效能測試 —— 壓力模式JMeter模式
- 關於SignalR併發量測試SignalR
- 安全測評基礎-安全測評常用測試工具講解
- 效能測試 —— 什麼是全鏈路壓測?
- gRPC-web現狀及測試RPCWeb
- jmeter介面效能測試-高併發分散式部署JMeter分散式
- 我編的併發執行緒壓力測試工具執行緒