在golang中,上下文超时不按预期工作。

huangapple go评论68阅读模式
英文:

Context timeout not working as expected in golang

问题

大家好,

我刚开始学习使用golang,并且在生产环境中调试超时问题。在调用服务器之前,我们在上下文中添加了50毫秒的超时时间,并发起了服务器调用。如果在50毫秒内没有收到响应,我们希望应用程序继续执行而不等待响应。

但是在调试过程中,我捕获了发起服务器调用和接收到响应(或错误)之间的持续时间,令我惊讶的是,这个时间远远超过了50毫秒。

客户端代码如下:

	ctx, cancel := context.WithTimeout(ctx, e.opts.Timeout)
	defer cancel()
    fireServerCall(ctx)
..
..

func fireServerCall(ctx context.Context){
  startTime:=time.Now()
  //调用服务器
  res, err:=callToServer(ctx)
  if err!=nil{
   //捕获失败的延迟时间
   return ....
  }
   //捕获成功的延迟时间
   return ....
}

有人遇到过类似的问题吗?这是预期的行为吗?你是如何处理这种情况的?

我是否做错了什么?欢迎提供建议 在golang中,上下文超时不按预期工作。

编辑:

我在原始代码中传递了上下文,但在这里忘记提及了,我刚刚添加了它。这意味着,我正在将相同的上下文传递给客户端,客户端在其中等待服务器在50毫秒内响应。

英文:

Helo All,

New to golang and was debugging timeout issues in a production environment. Before making a call to the server we add a timeout of 50ms to context and fire a server call. If the response is not received within 50 ms we expect the application to move on and not wait for the response.

But while debugging, I capture the duration between we fire a server call and the response received (or error out), to my surprise the value at the time is much higher than 50 ms.

Client syntax -

	ctx, cancel := context.WithTimeout(ctx, e.opts.Timeout)
	defer cancel()
    fireServerCall(ctx)
..
..

def fireServerCall(ctx context:Context){
  startTime:=time.Now()
  //call to the server
  res, err:=callToServer(ctx)
  if err!=nil{
   //capture failure latency
   return ....
  }
   //capture success latency
   return ....
}

Has anyone ever faced any similar issue? Is this expected behaviour? How did you handle such cases?

Am I doing something incorrectly? Suggestions are welcome 在golang中,上下文超时不按预期工作。

Edit:

I am passing context in my original code but forgot to mention it here, just added it. That mean, I am passing the same context on which my client is waiting for server to respond within 50 ms.

答案1

得分: 2

  1. 你应该将创建的上下文传递给fireServerCallcallToServer函数。
  2. callToServer函数应该考虑传递的上下文,并监视ctx.Done()通道以相应地停止执行。

回答@Bishnu的评论:

不认为这是必需的。进行了测试,即使不将ctx传递给callToServer(),它也可以工作。在高负载下,行为与预期不符。你能否分享一些关于你所指的文档/测试?

上下文超时如果没有传递上下文并检查其Done()通道是无法工作的。上下文不是某种魔法 - 简化来说,它只是一个带有done通道的结构体,通过调用取消函数或超时发生时关闭该通道。监视这个通道是接受它的最内层函数的责任。

示例:

package main

import (
	"context"
	"fmt"
	"time"
)

func callToServer(ctx context.Context) {
	now := time.Now()
	select {
	case <-ctx.Done(): // 通过取消或截止时间取消上下文
	case <-time.After(1 * time.Second): // 模拟外部调用
	}
	fmt.Printf("callToServer: %v\n", time.Since(now))
}

func callToServerContextAgnostic(ctx context.Context) {
	now := time.Now()
	select {
	case <-time.After(2 * time.Second): // 模拟外部调用
	}
	fmt.Printf("callToServerContextAgnostic: %v\n", time.Since(now))
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()
	callToServer(ctx)

	ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel2()
	callToServerContextAgnostic(ctx2)
}

结果:

callToServer: 100ms
callToServerContextAgnostic: 2s

你可以在Go Playground上运行它:https://go.dev/play/p/tIxjHxUzYfh

请注意,许多客户端(来自标准库或第三方库)会自行监视Done通道。

例如标准HTTP客户端:

	c := &http.Client{} // 用于所有请求的客户端

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*100))
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://google.com", nil)
	if err != nil {
		log.Fatal(err)
	}

	resp, err := c.Do(req) // 将为您监视`Done`通道

一些文档和文章:

英文:
  1. You should pass created context to fireServerCall and callToServer functions
  2. callToServer should consider passed context and monitor ctx.Done() channel to stop its execution accordingly

Answering to comment by @Bishnu:
> Don't think this is needed. Did a test and even without passing ctx to callToServer() it works. The behaviour is not as expected under high load. Can you kindly share some document/test what you have pointed here?

Context timeout just can't work without context passing and checking its Done() channel. Context is not some kind of magic — simplifying it is just a struct with done channel which is closed by calling cancel function or when timeout occurs. Monitoring this channel — is responsibility of the innermost function that accepts it.

Example:

package main

import (
	&quot;context&quot;
	&quot;fmt&quot;
	&quot;time&quot;
)

func callToServer(ctx context.Context) {
	now := time.Now()
	select {
	case &lt;-ctx.Done(): // context cancelled via cancel or deadline
	case &lt;-time.After(1 * time.Second): // emulate external call
	}
	fmt.Printf(&quot;callToServer: %v\n&quot;, time.Since(now))
}

func callToServerContextAgnostic(ctx context.Context) {
	now := time.Now()
	select {
	case &lt;-time.After(2 * time.Second): // emulate external call
	}
	fmt.Printf(&quot;callToServerContextAgnostic: %v\n&quot;, time.Since(now))
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()
	callToServer(ctx)

	ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel2()
	callToServerContextAgnostic(ctx2)
}

Results:

callToServer: 100ms
callToServerContextAgnostic: 2s

You can launch it on Go Playground: https://go.dev/play/p/tIxjHxUzYfh

Note that many of the clients (from standard or third party libraries) monitors Done channel by themselves.

For example standard HTTP client:

	c := &amp;http.Client{} // client for all requests

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*100))
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, &quot;http://google.com&quot;, nil)
	if err != nil {
		log.Fatal(err)
	}

	resp, err := c.Do(req) // will monitor `Done` channel for you

Some docs and articles:

huangapple
  • 本文由 发表于 2022年8月29日 18:34:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/73527504.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定