Golang context 實現服務端超時控制記錄

golang 官方博文context的主要設計目的和實踐作闡述, 本文結合該博文對服務器端實現超時的方法再作彙總。


  • 通過設置nginx或後端web server配置實現請求超時
    router.HandleFunc("/lazy", lazyHandler)
    server := &http.Server{
        Addr:"0.0.0.0:8080",
        Handler: router,
        ReadTimeout: time.Second * 3,
        WriteTimeout: time.Second *2,
    }

web serverhandler處理時間是5s, 此時WriteTimeout2s,此時客戶端會接收到err_empty_response響應。

func lazyHandler(resp http.ResponseWriter, req *http.Request){
	time.Sleep(time.Duration(5) * time.Second)
	resp.Write([]byte("Lazy reply after 5 sec"))
}

在複雜的業務背景下,若處理不當該線程可能會在服務端後臺形成內存泄露,可通過/debug/pprof查看服務端goroutine數目,顯示結果是約5s後服務端對應的goroutine會結束,且被回收。



  • 通過golang context實現請求超時控制和goroutine生命週期控制

下例中:lazyHander負責請求參數處理和context的初始化:

  • 客戶端發送超時參數,採用WithTimeout來控制context在指定時間內關閉其管道,若所有子進程中均聲明瞭正確的context信號接收處理,則lazyHandler派生的所有子進程會返回並被回收。

  • 若客戶端不發送超時參數,則採用WithCancel函數來控制所有子線程的生命週期。若所有子進程中均聲明瞭正確的context信號接收處理,一旦lazyHandlerdefer cancel語句被執行,則由lazyHandler派生的所有子進程會返回並被回收。

func lazyHandler(resp http.ResponseWriter, req *http.Request) {
    	var (
    		ctx context.Context
    		cancel context.CancelFunc
    	)
    	query, err := url.ParseQuery(req.URL.RawQuery)
    	if err != nil{
    		log.Fatalln("Http error")
    	}
    	keys, ok := query["timeout"]
    	if !ok  {
    		ctx, cancel = context.WithCancel(context.Background())
    	} else {
    		timeout, err := time.ParseDuration(keys[0])
    		if err != nil {
    			ctx, cancel = context.WithCancel(context.Background())
    		} else {
    			ctx, cancel = context.WithTimeout(context.Background(), timeout)
    		}
    	}
    	defer cancel()
    	s := funcA(ctx)
    	resp.Write([]byte(s))
    }

funcAhandler的主要業務邏輯,包含異步操作。

func funcA(ctx context.Context) string{
	c := make(chan error, 1)
	go func() {
		c <- funcB(ctx, func()error {
			time.Sleep(time.Duration(5) * time.Second)
			return nil
		})
	}()
	select {
	case <-ctx.Done():
		err:= <-c
		return ctx.Err().Error() +"; " + err.Error()
	case <-c:
		return "Lazy reply after 5 sec"
	}
}

這裏注意,雖然ctx.Done()信號已被接收,並不意味這該函數能馬上返回,因爲若該函數涉及到子線程funcB的調用,則需要等待子線程返回,否則子線程會失去控制且可能引起內存泄露。

func funcB(ctx context.Context, f func() error) error {
	c := make(chan error ,1)
	go func(){
		c <- f()
	}()
	select {
	case <-ctx.Done():
		return errors.New("Interrupt by context")
	case err:= <-c:
		return err
	}
}

子線程funcB同樣能接收parent contextctx.Done(),能在timeout指定時間內強制返回,或正常執行到程序段結束。另外,lazyHandlerdefer cancel()也能確保funcB總能結束。

funcA中涉及到服務之間的調用,即調用某apiendpoint, 也可以將context存於request中,利用request接口來實現請求超時。

func funcA(ctx context.Context) string {
	c := make(chan string, 1)
	go func() {
		c <- func(ctx context.Context) string{
			req, err := http.NewRequest("GET", "http://localhost:8079/", nil)
			if err != nil {
				return err.Error()
			}
			req = req.WithContext(ctx)
			resp, err := http.DefaultClient.Do(req)
			if err != nil {
				return err.Error()
			}else{
				data, _ := ioutil.ReadAll(resp.Body)
				resp.Body.Close()
				return string(data)
			}
		}(ctx)
	}()
	select {
	case <-ctx.Done():
		err := <-c
		return ctx.Err().Error() + "; " + err
	case str:= <-c:
		return str
	}
}

注意, 這裏使用了req = req.WithContext(ctx), 使得context傳到req對象中,實現類似funcB中的子線程控制。


  • 通過中間件Middleware傳入帶有timeoutcontext參見

開發者可以更優雅地通過中間件的形式設置timeout, 另外必須在handler實現中使用select監聽ctx.Done()信號, 或將該ctx交由支持ctx作爲參數的接口方法處理, 如:

rpcResponse, err := grpcFuncFoo(ctx, ...)

此方法與上方法原理上相同。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章