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, ...)

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

你可能感兴趣的:(Golang多线程,Golang)