>백엔드 개발 >Golang >Go 시간 초과 제어 구현 방법에 대한 자세한 설명

Go 시간 초과 제어 구현 방법에 대한 자세한 설명

藏色散人
藏色散人앞으로
2021-04-06 11:18:293876검색

튜토리얼 칼럼에서 시간 관리가 필요한 친구들에게 도움이 되기를 바랍니다.

Go 시간 초과 제어 구현 방법에 대한 자세한 설명타임아웃 제어가 왜 필요한가요?

요청 시간이 너무 길면 사용자가 이 페이지를 떠났을 수 있으며 서버는 여전히 처리에 리소스를 소비하고 있으며 얻은 결과는 의미가 없습니다.

서버에서 너무 오랫동안 처리하면 너무 많은 리소스를 차지하게 됩니다. , 이로 인해 동시성이 감소합니다. 심지어 비가용 사고가 발생합니다
  • Go 시간 초과 제어의 필요성

Go는 일반적으로 백엔드 서비스를 작성하는 데 사용됩니다. 일반적으로 요청은 여러 개의 직렬 또는 병렬 하위 작업으로 완료됩니다. 또 다른 내부 요청일 수 있으므로 이 요청 시간이 초과되면 신속하게 반환하여 고루틴, 파일 설명자 등과 같이 점유된 리소스를 해제해야 합니다.

서버 측 공통 시간 제한 제어

프로세스 내 논리적 처리

HTTP 또는 RPC 요청과 같은 클라이언트 요청 읽기 및 쓰기
  • RPC 호출 또는 DB 액세스를 포함한 다른 서버 요청 호출, etc.
  • 타임아웃 제어가 없으면 어떻게 될까요?

이 기사를 단순화하기 위해 hardWork 요청 기능을 예로 들어 보겠습니다. 이름에서 알 수 있듯이 처리 속도가 느려질 수 있습니다.
func hardWork(job interface{}) error {
    time.Sleep(time.Minute)
    return nil}func requestWork(ctx context.Context, job interface{}) error {
  return hardWork(job)}
이때 클라이언트는 항상 익숙한 화면을 보게 됩니다

Go 시간 초과 제어 구현 방법에 대한 자세한 설명hardWork 为例,用来做啥的不重要,顾名思义,可能处理起来比较慢。

func requestWork(ctx context.Context, job interface{}) error {
    ctx, cancel := context.WithTimeout(ctx, time.Second*2)
    defer cancel()

    done := make(chan error)
    go func() {
        done <p>这时客户端看到的就一直是大家熟悉的画面</p><p><img src="https://img.php.cn/upload/article/000/000/020/ea4492ddee56842b8d2dd9e34d636408-1.jpg" alt="Go 시간 초과 제어 구현 방법에 대한 자세한 설명"></p><p>绝大部分用户都不会看一分钟菊花,早早弃你而去,空留了整个调用链路上一堆资源的占用,本文不究其它细节,只聚焦超时实现。</p><p>下面我们看看该怎么来实现超时,其中会有哪些坑。</p><h2>
<span class="header-link octicon octicon-link"></span>第一版实现</h2><p>大家可以先不往下看,自己试着想想该怎么实现这个函数的超时,第一次尝试:</p><pre class="brush:php;toolbar:false">func main() {
    const total = 1000
    var wg sync.WaitGroup
    wg.Add(total)
    now := time.Now()
    for i := 0; i <p>我们写个 main 函数测试一下</p><pre class="brush:php;toolbar:false">➜ go run timeout.go
elapsed: 2.005725931s

跑一下试试效果

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

超时已经生效。但这样就搞定了吗?

goroutine 泄露

让我们在main函数末尾加一行代码看看执行完有多少goroutine

➜ go run timeout.go
elapsed: 2.005725931s
number of goroutines: 1001

sleep 2分钟是为了等待所有任务结束,然后我们打印一下当前goroutine数量。让我们执行一下看看结果

done := make(chan error, 1)

goroutine泄露了,让我们看看为啥会这样呢?首先,requestWork 函数在2秒钟超时后就退出了,一旦 requestWork 函数退出,那么 done channel 就没有goroutine接收了,等到执行 done 这行代码的时候就会一直卡着写不进去,导致每个超时的请求都会一直占用掉一个goroutine,这是一个很大的bug,等到资源耗尽的时候整个服务就失去响应了。

那么怎么fix呢?其实也很简单,只要 make chan 的时候把 buffer size 设为1,如下:

➜ go run timeout.go
elapsed: 2.005655146s
number of goroutines: 1

这样就可以让 done 不管在是否超时都能写入而不卡住goroutine。此时可能有人会问如果这时写入一个已经没goroutine接收的channel会不会有问题,在Go里面channel不像我们常见的文件描述符一样,不是必须关闭的,只是个对象而已,<code>close(channel) 只是用来告诉接收者没有东西要写了,没有其它用途。

改完这一行代码我们再测试一遍:

panic("oops")

goroutine泄露问题解决了!

panic 无法捕获

让我们把 hardWork 函数实现改成

go func() {
  defer func() {
    if p := recover(); p != nil {
      fmt.Println("oops, panic")
    }
  }()

  defer wg.Done()
  requestWork(context.Background(), "any")}()

修改 main 函数加上捕获异常的代码如下:

func requestWork(ctx context.Context, job interface{}) error {
    ctx, cancel := context.WithTimeout(ctx, time.Second*2)
    defer cancel()

    done := make(chan error, 1)
    panicChan := make(chan interface{}, 1)
    go func() {
        defer func() {
            if p := recover(); p != nil {
                panicChan <p>此时执行一下就会发现panic是无法被捕获的,原因是因为在 <code>requestWork</code> 内部起的goroutine里产生的panic其它goroutine无法捕获。</p><p>解决方法是在 <code>requestWork</code> 里加上 <code>panicChan</code> 来处理,同样,需要 <code>panicChan</code> 的 <code>buffer size</code></p>대부분의 사용자는 국화를 잠시도 보지 않고 일찍 포기할 것이며 전체 통화 링크에 많은 리소스를 차지할 것입니다. 이 기사에서는 다른 세부 사항을 다루지 않을 것입니다. , 포커스 시간 초과 구현만 가능합니다. 🎜🎜타임아웃을 구현하는 방법과 어떤 함정이 있는지 살펴보겠습니다. 🎜🎜🎜🎜첫 번째 버전 구현🎜🎜아래 내용을 읽지 말고 이 함수의 시간 초과를 구현하는 방법에 대해 생각해 보세요. 먼저 시도해 보세요. 🎜<pre class="brush:php;toolbar:false">ctx, cancel := contextx.ShrinkDeadline(ctx, time.Second*2)
🎜테스트할 주요 함수를 작성해 봅시다🎜
package mainimport (
    "context"
    "fmt"
    "runtime"
    "sync"
    "time"

    "github.com/tal-tech/go-zero/core/contextx")func hardWork(job interface{}) error {
    time.Sleep(time.Second * 10)
    return nil}func requestWork(ctx context.Context, job interface{}) error {
    ctx, cancel := contextx.ShrinkDeadline(ctx, time.Second*2)
    defer cancel()

    done := make(chan error, 1)
    panicChan := make(chan interface{}, 1)
    go func() {
        defer func() {
            if p := recover(); p != nil {
                panicChan 🎜실행해 보세요. 🎜rrreee🎜타임아웃이 적용되었습니다. 그런데 이게 다 됐나요? 🎜🎜🎜🎜고루틴 유출🎜🎜주 함수 끝에 코드 한 줄을 추가하여 얼마나 많은 고루틴이 실행되었는지 확인하겠습니다.🎜rrreee🎜잠자기 2분은 모든 작업이 끝날 때까지 기다린 후 다음을 인쇄합니다. 현재 고루틴의 수. 실행해서 결과를 볼까요🎜rrreee🎜고루틴이 유출됐는데 왜 이런 일이 일어나는지 살펴볼까요? 우선, <code>requestWork</code> 함수는 2초의 시간 초과 후에 종료됩니다. <code>requestWork</code> 함수가 종료되면 <code>done 채널</code>에는 수신할 고루틴이 없습니다. <code>done 을 실행할 때 이 코드 줄이 멈춰서 작성할 수 없게 되어 각 시간 초과 요청이 고루틴을 차지하게 됩니다. 리소스가 소진되면 전체 서비스가 응답하지 않게 됩니다. 🎜🎜그럼 어떻게 고치나요? 사실, <code>make chan</code>을 할 때 다음과 같이 <code>버퍼 크기</code>를 1로 설정하면 됩니다. 🎜rrreee🎜이 방법으로 <code>done  시간 초과 여부에 관계없이 고루틴을 차단하지 않고 쓸 수 있습니다. 이때 누군가는 고루틴이 없는 채널에 글을 써서 수신하면 문제가 없느냐고 물을 수 있는데, Go에서는 채널이 일반적인 파일 디스크립터와는 달리 닫힐 필요가 없습니다. 단지 객체일 뿐입니다. <code>close(channel)</code>는 수신자에게 쓸 내용이 없으며 다른 목적이 없음을 알리는 데에만 사용됩니다. 🎜🎜이 코드 줄을 변경한 후 다시 테스트해 보겠습니다. 🎜rrreee🎜고루틴 누출 문제가 해결되었습니다! 🎜🎜🎜🎜패닉은 포착할 수 없습니다🎜🎜 <code>hardWork</code> 함수 구현을 🎜rrreee🎜로 변경하겠습니다. <code>main</code> 함수를 수정하고 다음과 같이 예외를 캡처하는 코드를 추가합니다. 🎜rrreee🎜이때 실행해보면 패닉을 캡쳐할 수 없다는 것을 알 수 있는데, 그 이유는 <code>requestWork</code> 내부 고루틴에서 발생한 패닉은 다른 고루틴에서 캡쳐할 수 없기 때문입니다. 🎜🎜해결책은 <code>requestWork</code>에 <code>panicChan</code>을 추가하여 처리하는 것입니다. 마찬가지로 <code>panicChan</code>의 <code>버퍼 크기</code>도 필요합니다. 다음과 같이 1이 됩니다: 🎜<pre class="brush:php;toolbar:false">func requestWork(ctx context.Context, job interface{}) error {
    ctx, cancel := context.WithTimeout(ctx, time.Second*2)
    defer cancel()

    done := make(chan error, 1)
    panicChan := make(chan interface{}, 1)
    go func() {
        defer func() {
            if p := recover(); p != nil {
                panicChan <p>改完就可以在 <code>requestWork</code> 的调用方处理 <code>panic</code> 了。</p><h2>
<span class="header-link octicon octicon-link"></span>超时时长一定对吗?</h2><p>上面的 <code>requestWork</code> 实现忽略了传入的 <code>ctx</code> 参数,如果 <code>ctx</code> 已有超时设置,我们一定要关注此传入的超时是不是小于这里给的2秒,如果小于,就需要用传入的超时,<code>go-zero/core/contextx</code> 已经提供了方法帮我们一行代码搞定,只需修改如下:</p><pre class="brush:php;toolbar:false">ctx, cancel := contextx.ShrinkDeadline(ctx, time.Second*2)

Data race

这里 requestWork 只是返回了一个 error 参数,如果需要返回多个参数,那么我们就需要注意 data race,此时可以通过锁来解决,具体实现参考 go-zero/zrpc/internal/serverinterceptors/timeoutinterceptor.go,这里不做赘述。

完整示例

package mainimport (
    "context"
    "fmt"
    "runtime"
    "sync"
    "time"

    "github.com/tal-tech/go-zero/core/contextx")func hardWork(job interface{}) error {
    time.Sleep(time.Second * 10)
    return nil}func requestWork(ctx context.Context, job interface{}) error {
    ctx, cancel := contextx.ShrinkDeadline(ctx, time.Second*2)
    defer cancel()

    done := make(chan error, 1)
    panicChan := make(chan interface{}, 1)
    go func() {
        defer func() {
            if p := recover(); p != nil {
                panicChan <h2>
<span class="header-link octicon octicon-link"></span>更多细节</h2><p>请参考 <code>go-zero</code> 源码:</p>
  • go-zero/core/fx/timeout.go
  • go-zero/zrpc/internal/clientinterceptors/timeoutinterceptor.go
  • go-zero/zrpc/internal/serverinterceptors/timeoutinterceptor.go

项目地址

github.com/tal-tech/go-zero

欢迎使用 go-zerostar 支持我们!

위 내용은 Go 시간 초과 제어 구현 방법에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 learnku.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제