一文帶你搞懂go中的請求超時控制
一、為什么需要超時控制
在日常開發(fā)中,對于RPC、HTTP調(diào)用設(shè)置超時時間是非常重要的。那為什么需要超時控制呢?我們可以從用戶、系統(tǒng)兩個角度進行考慮;
- 用戶角度:在這個快節(jié)奏的時代,如果一個接口耗時太長,用戶可能已經(jīng)離開頁面了。這種請求下,后續(xù)的計算任務(wù)就沒用了。比如說,最近的AIGC,我們有個需求需要用到微軟的ChatGPT,這類接口有個特點,耗時不受控制,可能30s,可能1min,我們和產(chǎn)品討論以后,這個接口最后的超時時間設(shè)置為9s。(說實在,有點短,很多超時的情況)
- 系統(tǒng)角度:因為HTTP、RPC請求均會占用資源,比如鏈接數(shù)、計算資源等等,盡快返回,可能防止資源被耗盡的請求;
現(xiàn)在,我們知道要設(shè)置超時時間了,那就有個問題,超時時間設(shè)置為多少呢?設(shè)置太小,可能會出現(xiàn)大面積超時的情況,不符合業(yè)務(wù)需求。設(shè)置太長,可能會有以上兩個缺點。
二、超時時間設(shè)置為多少
超時時間的設(shè)置可以從這四個角度考慮:
- 問產(chǎn)品;產(chǎn)品從業(yè)務(wù)、用戶的角度,行為考慮,這個頁面他們能夠接受的時間是多少。
- 看歷史數(shù)據(jù);我們可以看這個接口歷史數(shù)據(jù)的99線,也就是99%的接口耗時是多少。
- 壓測;如果這是個新接口,沒有歷史數(shù)據(jù)可查,那么我們可以考慮進行壓測,觀察99%接口耗時是多少;
- 計算代碼邏輯;通過巴拉代碼,看有多少次MySQL、redis查找與插入;
上面四個方法,只要有一個湊效就行,但是,我們要秉承數(shù)據(jù)來源要有依據(jù)這條原則,優(yōu)先考慮歷史數(shù)據(jù)、壓測,其次結(jié)合業(yè)務(wù)需求,決定是否需要優(yōu)化代碼等等。
三、超時控制的種類
在微服務(wù)框架中,我們一個請求可能需要經(jīng)歷多個服務(wù),那么在生產(chǎn)環(huán)境下,咱們應(yīng)該得兩手抓:
- 鏈路超時:也就是在服務(wù)進入gate-api的時候,應(yīng)該設(shè)置一個鏈路時間。(我們服務(wù)設(shè)置的是10s)
- 服務(wù)時間:每個微服務(wù)請求其他服務(wù)的超時時間。(我們設(shè)置的是3s)
【注:我們公司大概是這樣的】
上面,服務(wù)時間的控制里頭,有包含兩方面,客戶端超時控制與服務(wù)端超時控制,我們通過一個例子來表述這兩者之間的差異。如果A服務(wù)請求B服務(wù),這個請求設(shè)置的超時時間為3s,但是B服務(wù)處理數(shù)據(jù)的需要話費兩分鐘,那么:
- 對于A客戶端,rpc框架大部分都設(shè)置了客戶端超時時間,3s就會返回了。
- 對于B服務(wù)端,當客戶端3s超時了,那是否還需要執(zhí)行兩分鐘呢?這個一般都會繼續(xù)執(zhí)行了(我們公司就會執(zhí)行),如果你在代碼里頭有明確的校驗超時時間,也能做到只執(zhí)行3s的。
接下來,我們來看幾個例子。
四、Golang超時控制實操
案例一
func hardWork(job interface{}) error { time.Sleep(time.Minute) return nil } func requestWorkV1(ctx context.Context, job interface{}) error { ctx, cancel := context.WithTimeout(ctx, time.Second*2) defer cancel() // 僅需要改這里即可 // done := make(chan error, 1) done := make(chan error) // done 退出以后,沒有接受者,會導致協(xié)程阻塞 go func() { done <- hardWork(job) }() select { case err := <-done: return err case <-ctx.Done(): // 這一部分提前退出 return ctx.Err() } } // 可以做到超時控制,但是會出現(xiàn)協(xié)程泄露的情況 func TestV1(t *testing.T) { const total = 1000 var wg sync.WaitGroup wg.Add(total) now := time.Now() for i := 0; i < total; i++ { go func() { defer wg.Done() requestWorkV1(context.Background(), "any") }() } wg.Wait() fmt.Println("elapsed:", time.Since(now)) // 2秒后打印這條語句,說明協(xié)程只執(zhí)行了兩秒 time.Sleep(time.Minute * 2) fmt.Println("number of goroutines:", runtime.NumGoroutine()) // number of goroutines: 1002 }
執(zhí)行上述代碼:我們會發(fā)現(xiàn)協(xié)程執(zhí)行2秒就退出了 【滿足我們超時控制需求】 ,但是第2個打印語句顯示協(xié)程泄漏了,當前有1002個協(xié)程;
原因:select
中的協(xié)程提前退出,從而導致無緩存chan
沒有接受者,從而導致協(xié)程泄漏。只需要將無緩存chan
改為有緩存chan
即可。
五、GRPC中如何做超時控制
接著,我們在看看在GRPC中,我們?nèi)绾巫龀瑫r控制。
首先,我們看下這個小Demo的目錄結(jié)構(gòu):
.
├── client_test.go
├── proto
│ ├── hello.pb.go
│ ├── hello.proto
│ └── hello_grpc.pb.go
└── server_test.go
定義接口IDL文件
syntax = "proto3"; package helloworld; option go_package = "."; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
執(zhí)行protoc工具
hello git:(master) ? protoc -I proto/ proto/hello.proto --go_out=./proto --go-grpc_out=./proto
寫client代碼
const ( address = "localhost:50051" defaultName = "world" ) func TestClient(t *testing.T) { conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewGreeterClient(conn) name := defaultName ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.Message) }
在客戶端代碼中,我們只需要設(shè)置ctx
即可。grpc客戶端框架就會幫我們監(jiān)控ctx
,只要超時了就會返回。
寫server代碼
func (s *server) SayHello(ctx context.Context, request *pb.HelloRequest) (*pb.HelloReply, error) { logrus.Info("request in") time.Sleep(5 * time.Second) //select { //case <-ctx.Done(): // fmt.Println("time out Done") //} logrus.Info("requst out") if ctx.Err() == context.DeadlineExceeded { log.Printf("RPC has reached deadline exceeded state: %s", ctx.Err()) return nil, ctx.Err() } return &pb.HelloReply{Message: "Hello, " + request.Name}, nil } func TestServer(t *testing.T) { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("Failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("Failed to serve: %v", err) } }
服務(wù)端,grpc框架就沒有替我們監(jiān)控了,需要我們自己寫邏輯,上述代碼可以通過注釋不同部分,驗證以下幾點:
- grpc框架沒有替我們監(jiān)控
ctx
,需要我們自己監(jiān)控; - 通過
select
監(jiān)控ctx
; - 通過
context.DeadlineExceeded
來監(jiān)控ctx
,從而提前返回;
六、GRPC框架如何監(jiān)控超時的呢
代碼在grpc/stream.go
文件:
func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, mc serviceconfig.MethodConfig, onCommit, doneFunc func(), opts ...CallOption) (_ iresolver.ClientStream, err error) { // ..... if desc != unaryStreamDesc { // Listen on cc and stream contexts to cleanup when the user closes the // ClientConn or cancels the stream context. In all other cases, an error // should already be injected into the recv buffer by the transport, which // the client will eventually receive, and then we will cancel the stream's // context in clientStream.finish. go func() { select { case <-cc.ctx.Done(): cs.finish(ErrClientConnClosing) case <-ctx.Done(): cs.finish(toRPCErr(ctx.Err())) } }() } }
可以看到,在newClientStreamWithParams
中,GRPC替我們起了一個協(xié)程,監(jiān)控ctx.Done
。
以上就是一文帶你搞懂go中的請求超時控制的詳細內(nèi)容,更多關(guān)于go請求超時控制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
pytorch中的transforms.ToTensor和transforms.Normalize的實現(xiàn)
本文主要介紹了pytorch中的transforms.ToTensor和transforms.Normalize的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-04-04vscode 通過Go:Install/Update Tools命令安裝失敗的問題解決
本文介紹了在VSCode開發(fā)環(huán)境中通過Go:Install/UpdateTools命令安裝工具時遇到網(wǎng)絡(luò)問題的解決方法,具有一定的參考價值,感興趣的可以了解一下2024-12-12