英文:
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 ....
}
有人遇到过类似的问题吗?这是预期的行为吗?你是如何处理这种情况的?
我是否做错了什么?欢迎提供建议
编辑:
我在原始代码中传递了上下文,但在这里忘记提及了,我刚刚添加了它。这意味着,我正在将相同的上下文传递给客户端,客户端在其中等待服务器在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
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
- 你应该将创建的上下文传递给
fireServerCall
和callToServer
函数。 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`通道
一些文档和文章:
- https://pkg.go.dev/context
- https://www.digitalocean.com/community/tutorials/how-to-use-contexts-in-go
英文:
- You should pass created context to
fireServerCall
andcallToServer
functions callToServer
should consider passed context and monitorctx.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 (
"context"
"fmt"
"time"
)
func callToServer(ctx context.Context) {
now := time.Now()
select {
case <-ctx.Done(): // context cancelled via cancel or deadline
case <-time.After(1 * time.Second): // emulate external call
}
fmt.Printf("callToServer: %v\n", time.Since(now))
}
func callToServerContextAgnostic(ctx context.Context) {
now := time.Now()
select {
case <-time.After(2 * time.Second): // emulate external call
}
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)
}
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 := &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, "http://google.com", nil)
if err != nil {
log.Fatal(err)
}
resp, err := c.Do(req) // will monitor `Done` channel for you
Some docs and articles:
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论