Go问答 Golang 后台守护进程队列处理方式小结

pathbox · 2018年03月18日 · 725 次阅读

在写一个 Golang Server的时候,比如 http 接口,最简单的就是使用 net/http 包,每个请求就会起一个 goroutine 来进行操作。很方便,但是,当并发量大的时候,就会起了成千上万的 goroutine,当 goroutine 的量达到一个很大的数量,服务性能也就出现了瓶颈。

我们可以手动构建简单的 goroutine 池,再借助 channel 做队列,后台起守护 goroutine,来处理 channel 中的数据。模型大概是这样的:

var myChan = make(chan int, 1000) // 全局channel队列

for i := 0; i < poolSize; i++{
  go initWorker()
}

func ProcessWorker() {
  for {
    select {
    case value := <-myChan:
      fmt.Println(value)
      // ... do something with value
    case done = <-done: // 关闭机制
      break
    default: // 这里不用超时处理,就是希望goroutine一直在后台执行
    }
  }
}

func indexHandler() {
  //...
  myChan <- value
}

这样我们就创建了 poolSize 数量的 goroutine,在不断的从 myChan 中获取数据,Golang 的调度会帮我们做这一切。

很容易就可以发现,这里的一个瓶颈就是 myChan 的 size 大小。当 myChan 很快就满了,后续的请求就会阻塞了。在我的项目的尝试中,我将 size 设为 10000, 进行 ab 测试,发现测试结果比用普通的多 goroutine 处理方式要好。我加到了 10w,性能反而下降了。所以,myChan Size 的代销不是越多越好,而是根据你实际情况来测试出一个合适的值。

在我的另一个项目中,是对文件的格式转换,对核心代码的执行,希望不要支持高并发,在高并发下核心的命令代码会执行报错。所以,我只创建了一个守护 goroutine,并且 make myChan 的 Size 为 200.这样,用 ab 测试时,能支持 200 并发,并且几乎没有执行失败的报错。

我似乎喜欢上了使用上述的方式来处理请求,Golang select 和 channel 的设计让我可以很愉悦的使用上面的方式,并且达到预期的效果。

我并没有发现程序执行的异常,直到今天,我忽然发现服务在没有请求的时候,CPU 占用率达到了 100%(4 核心的机器)。我十分诧异,第一时间想到了可能是 ProcessWorker 的问题。

这里涉及到了 select 调度的方法:

  • 当某个 case 的 channel 数据可以取到了就执行它;

  • 当多个 case 同时取到数据了,会随机执行一个;

  • 当没有 case 取到数据,都阻塞时,会执行 default。

当服务没有接收请求的时候,ProcessWorker 方法中会执行的应该是 default,这个没错。而在外层我用了 for 的死循环,以保证 goroutine 一直执行。这个问题就来了,ProcessWorker在没有请求的时候会一直执行 default,而不会阻塞在 case。如果你在 default 打印日志

func ProcessWorker() {
  for {
    select {
    case value := <-myChan:
      fmt.Println(value)
      // ... do something with value
    case done = <-done: // 关闭机制
      break
    default: // 这里不用超时处理,就是希望goroutine一直在后台执行
      log.Println("default")
    }
  }
}

你会发现后台在不断的输出日志。

一个简单的代码例子
// example.go
package main

import (
    "fmt"
    "log"
    "net/http"
)
var c = make(chan int, 100)

func main() {
    go worker()
    addr := ":9090"
    http.HandleFunc("/", index)
    log.Fatal(http.ListenAndServe(addr, nil))
}

func worker() {
    for {
        select {
        case d := <-c:
            fmt.Println(d)
            default:
        }
    }
}

func index(w http.ResponseWriter, r *http.Request) {
    c <- 1
    w.Write([]byte("OK"))
}

当你 go run example.go 的时候,打开 htop 或 top,查看这个服务,在我的 Ubuntu 14.04 环境下 (Mac 下也是),CPU 占用到了 100%(用了一个核心线程).

当把 default 注释了变成

func worker() {
    for {
        select {
        case d := <-c:
            fmt.Println(d)
            // default:
        }
    }
}

CPU 占用率就恢复了正常。这时候,worker()是阻塞在了 case d:= <-c: 上,这种阻塞并不会占用 CPU 的调度处理,CPU 会闲置或去处理别的任务。直到 channel 中有数据了,会唤醒该 channel 的数据结构中对应的 goroutine,设置为 runnable 的状态,该 goroutine 的调度才会继续进行。

解决方法: 将default: 删除即可
default 并非是罪恶之人

只是在上述的使用情况下,default 变成了不必要的了。 当你的使用方式并非如此时,比如下面的方法:

func TryRun() {
  select {
  case a := <-c:
    //...
  // case t := time.After(10 * time.Second):
  //   return
  default:
    return
  }
}

外层没有 for 死循环,当 <-c 阻塞时,default 会里执行 return,而起到立即释放 goroutine 的效果,或者你可以加一定的超时机制。要不然,在大量使用 goroutine 的时候,极有可能造成 goroutine 泄露或僵死。

Golang 设计了便捷的 goroutine 的创建方式: go 一下,方便的进行并发处理。并且设计了使用select方法来进行调度处理。让并发编程变得简单。

不过,我们还是需要对其原理和底层结构有所掌握,这样才能写出合适的代码。

关于 channel 的参考:

https://about.sourcegraph.com/go/understanding-channels-kavya-joshi/<span class="embed-responsive embed-responsive-16by9"><iframe class="embed-responsive-item" src="//www.youtube.com/embed/KBZlN0izeiY" allowfullscreen></iframe></span>
更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册