openlang

hostprobe 队列上限 8、4 个 worker,但 100 个请求全部返回 202,goroutine 数涨到约 100

我在写 hostprobe,一个内部的小服务,cron 和几个脚本会以 /probe?host=NAME 的形式 POST 过来。4 个 worker,队列长度 8,探测目前就是 sleep 20 秒(ssh 检查以后再加)。我希望脚本失控的时候它还能保持小体量。

测试用的是 hey -n 100 -c 100。100 个请求几毫秒内全部返回 202。我本来预期从第 9 个开始会被拖慢或者被拒绝。/stats 显示队列 len=8 cap=8,4 个在运行,所以 channel 和 worker 看起来没问题。但 runtime.NumGoroutine() 大约是 100,/debug/pprof/goroutine?debug=1 里有一大组 goroutine 停在同一个位置。请求 1000 个的时候 goroutine 数涨到约 1000,而队列还是 8。Activity Monitor 里的内存每次突发之后也会涨一点。

handler 和池子我好久没动过了,基本是在盯着自己忘掉的代码看。handler 代码如下

job := Job{Host: host, Enqueued: time.Now()}

go func() {
	q.jobs <- job
}()

atomic.AddInt64(&q.accepted, 1)
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, "queued %s\n", host)

池子就是常见的写法,4 个 goroutine 里 for job := range q.jobs,channel 是 make(chan Job, cfg.QueueDepth)。

多出来的 goroutine 是哪来的?我怀疑是 worker 或者 channel,但数字上两边都没问题。

从英语翻译查看原文

后续讨论 3

顺着彼此的依据读下去
  1. 固定链接

    我觉得 go func() { q.jobs <- job }() 就是你的全部答案。channel 本身在正常工作,8 个在缓冲区里,4 个被 worker 拿着,其余的发送都会阻塞。但每个被阻塞的发送都是它自己的一个 goroutine,停在 channel 发送上,而 handler 早就往下走去返回 202 了。所以队列从来没有对任何人施加背压,只是在它后面多了一个由 goroutine 组成的等候室。这就是你在 pprof 里看到的那一大组,也是 1000 个请求会有大约 1000 个 goroutine 的原因。每个 goroutine 都占着一个栈,还拿着 job,所以每次突发之后内存往上爬也对得上。

    如果你想让第 9 个开始被拒绝,就在 handler 里用 select 来做发送。

    select {
    case q.jobs <- job:
    	atomic.AddInt64(&q.accepted, 1)
    	w.WriteHeader(http.StatusAccepted)
    	fmt.Fprintf(w, "queued %s\n", host)
    case <-r.Context().Done():
    	return
    default:
    	http.Error(w, "queue full", http.StatusServiceUnavailable)
    }

    加上 default 就会立刻失败。如果你更希望调用方等着,就去掉 default,context 那个分支会让它在客户端断开时放弃。对 cron 脚本来说,通常的做法是返回 503(想用 429 也行)并带上 Retry-After。

    另外我觉得,对旧代码的行为来说,"queued" 这个词有点太大方了。它更像是“答应了会排队,迟早的事”。

    从英语翻译查看原文

同一个问题的其他角度

  1. 已取消 context 但出错后 worker goroutine 仍然存活(总是 workers + 2)

    大家好, 我在写一个小型 Go CLI,叫 shotcheck,用来检查文件夹里每张 PNG 是否符合 App Store 对应位置要求的精确尺寸。一个 worker 池(默认 8 个)从 jobs 里读文件名,检查后把 Result 发到无缓冲的…

    回复 4最近活动 go
  2. 把过滤函数标成 async,卡顿一点没变

    ledgerscan 在 400k 条记录上做搜索。我把 run 改成了 async,在 main 里 await 它,本以为 100 ms 的心跳 Timer 会一直跳。结果先是几下 tick,然后打出 searching "rent"...,接着…

    回复 2最近活动 dart
  3. isolate 都结束了,对 ReceivePort 的 await for 却一直不结束

    大家好,我写了个小的 dart 工具,用来汇总温度 CSV,每个文件起一个 isolate,这样大文件不用互相等。我在 WSL 里这样跑 dart run bin/summarize.dart data/north.csv data/south.cs…

    回复 2最近活动 dart