Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > go请求超时控制

一文带你搞懂go中的请求超时控制

作者:小范真是一把好手

在日常开发中,对于RPC、HTTP调用设置超时时间是非常重要的,这就需要我们设置超时控制,本文将通过相关示例为大家深入介绍一下go中的请求超时控制,希望对大家有所帮助

一、为什么需要超时控制

在日常开发中,对于RPC、HTTP调用设置超时时间是非常重要的。那为什么需要超时控制呢?我们可以从用户、系统两个角度进行考虑;

现在,我们知道要设置超时时间了,那就有个问题,超时时间设置为多少呢?设置太小,可能会出现大面积超时的情况,不符合业务需求。设置太长,可能会有以上两个缺点。

二、超时时间设置为多少

超时时间的设置可以从这四个角度考虑:

上面四个方法,只要有一个凑效就行,但是,我们要秉承数据来源要有依据这条原则,优先考虑历史数据、压测,其次结合业务需求,决定是否需要优化代码等等。

三、超时控制的种类

在微服务框架中,我们一个请求可能需要经历多个服务,那么在生产环境下,咱们应该得两手抓:

【注:我们公司大概是这样的】

上面,服务时间的控制里头,有包含两方面,客户端超时控制与服务端超时控制,我们通过一个例子来表述这两者之间的差异。如果A服务请求B服务,这个请求设置的超时时间为3s,但是B服务处理数据的需要话费两分钟,那么:

接下来,我们来看几个例子。

四、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 退出以后,没有接受者,会导致协程阻塞
    go func() {
        done <- hardWork(job)
    }()

    select {
        case err := <-done:
        return err
        case <-ctx.Done():   // 这一部分提前退出
        return ctx.Err()
    }
}

// 可以做到超时控制,但是会出现协程泄露的情况
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秒后打印这条语句,说明协程只执行了两秒

    time.Sleep(time.Minute * 2)
    fmt.Println("number of goroutines:", runtime.NumGoroutine())  // number of goroutines: 1002
}

执行上述代码:我们会发现协程执行2秒就退出了 【满足我们超时控制需求】 ,但是第2个打印语句显示协程泄漏了,当前有1002个协程;

原因:select中的协程提前退出,从而导致无缓存chan没有接受者,从而导致协程泄漏。只需要将无缓存chan改为有缓存chan即可。

五、GRPC中如何做超时控制

接着,我们在看看在GRPC中,我们如何做超时控制。

首先,我们看下这个小Demo的目录结构:

.
├── 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;
}

执行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)
}

在客户端代码中,我们只需要设置ctx即可。grpc客户端框架就会帮我们监控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)
    }
}

服务端,grpc框架就没有替我们监控了,需要我们自己写逻辑,上述代码可以通过注释不同部分,验证以下几点:

六、GRPC框架如何监控超时的呢

代码在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替我们起了一个协程,监控ctx.Done

以上就是一文带你搞懂go中的请求超时控制的详细内容,更多关于go请求超时控制的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文