在阅读VictoriaMetrics的源码的时候,读到了那么平平无奇的一段:

// AddRows adds the given mrs to s.
func (s *Storage) AddRows(mrs []MetricRow, precisionBits uint8) error {
if len(mrs) == 0 {
return nil
} // Limit the number of concurrent goroutines that may add rows to the storage.
// This should prevent from out of memory errors and CPU trashing when too many
// goroutines call AddRows.
select {
case addRowsConcurrencyCh <- struct{}{}:
default:
// Sleep for a while until giving up
atomic.AddUint64(&s.addRowsConcurrencyLimitReached, 1)
t := timerpool.Get(addRowsTimeout) // Prioritize data ingestion over concurrent searches.
storagepacelimiter.Search.Inc() select {
case addRowsConcurrencyCh <- struct{}{}:
timerpool.Put(t)
storagepacelimiter.Search.Dec()
case <-t.C:
timerpool.Put(t)
storagepacelimiter.Search.Dec()
atomic.AddUint64(&s.addRowsConcurrencyLimitTimeout, 1)
atomic.AddUint64(&s.addRowsConcurrencyDroppedRows, uint64(len(mrs)))
return fmt.Errorf("cannot add %d rows to storage in %s, since it is overloaded with %d concurrent writers; add more CPUs or reduce load",
len(mrs), addRowsTimeout, cap(addRowsConcurrencyCh))
}
}

仔细看了以后,真是不得了。这感觉就像——

1.背景

在vm-storage这个组件中,作为存储节点,它一边要负责数据的写入,一边要负责数据的查询。很明显,数据写入的工作很重要,而查询的优先级相比写入就要低一些。

遇到这种问题,我的第一反应就是:把写入的协程数设置得比查询的协程数多不就行了吗?想要多高的优先级就设置为多大的比例。

太天真了!

  1. 物理核是性能真正的限制。无论你有多少协程,理论上N个核就最多只有N个协程处于执行状态。
  2. 协程调度并非没有成本,协程越多,就会有越多的CPU时间花在协程调度上。对于CPU密集型的业务,计算的协程数超过物理核的个数的部分都是白瞎。
  3. 假设写的协程数是读的协程数的2倍,概率上看调度到写的次数是读的次数的2倍;但是读和写的计算量并不是对等的,假设某个查询的数据量较大,就会导致读协程总体的CPU时间多于写协程,最终可能会导致写入超时失败。正确的办法是通过机制来让读协程主动让出CPU资源。

所以这里我直接总结vm-storage在协程控制的处理思路,再逐段分析源码:

  1. 区分IO协程和计算协程。

​ IO协程收到数据后,通过channel转给计算协程。

  1. 计算协程的数量与核的数量相等。
  • 处理insert操作的协程数等于CPU的核数,且接收任务的channel的长度也等于CPU核数。
  • 处理query_range等查询操作的协程数是CPU核数的2倍,猜测这里是因为部分读操作可能导致mmap区域内存产生缺页中断,继而引发IO阻塞。但不管怎么样,对协程数仍然是很克制。
  1. insert协程执行业务逻辑前,在一个排队channel里面写入一个struct{},这个排队channel的长度与CPU核数相等。写入成功,证明同时进行的写操作小于核数,允许继续写入。

    写入队列失败,就证明某个insert协程没有被及时调度,就需要通知select协程主动让出CPU资源。

  2. 每当有一个insert操作被阻塞,就会通过原子操作累加计数。这个计数代表了有多少个insert操作处于等待。

    如果insert操作排队成功,计数器就会减一。当计数器为0时,通过条件变量来发起 broadcast(),唤醒在等待的select操作。

  3. select协程中,每扫描4095个block就会检查一次是否有insert操作在等待。如果有,调用条件变量 cond.Wait()进入等待,让出协程调度。

(源码位于:https://github.com/VictoriaMetrics/VictoriaMetrics)

2. insert操作源码分析

2.1 工作协程的创建

lib/protoparser/common/unmarshal_work.go:24

// StartUnmarshalWorkers starts unmarshal workers.
func StartUnmarshalWorkers() {
if unmarshalWorkCh != nil {
logger.Panicf("BUG: it looks like startUnmarshalWorkers() has been alread called without stopUnmarshalWorkers()")
}
gomaxprocs := cgroup.AvailableCPUs() //获取物理核的个数
unmarshalWorkCh = make(chan UnmarshalWork, gomaxprocs) //创建一个channel,长度与核数相等
unmarshalWorkersWG.Add(gomaxprocs)
for i := 0; i < gomaxprocs; i++ {
go func() { // 启动N个协程,数量与核数相等
defer unmarshalWorkersWG.Done()
for uw := range unmarshalWorkCh {
uw.Unmarshal() // 这里调用具体的业务处理函数
}
}()
}
}

IO协程获取数据后,把请求丢到unmarshalWorkCh中:

// ScheduleUnmarshalWork schedules uw to run in the worker pool.
//
// It is expected that StartUnmarshalWorkers is already called.
func ScheduleUnmarshalWork(uw UnmarshalWork) {
unmarshalWorkCh <- uw
}

2.2 insert协程的并发检查

lib/storage/storage.go:1617

首先创建了一个用于管理写入并发的channel:

var (
// Limit the concurrency for data ingestion to GOMAXPROCS, since this operation
// is CPU bound, so there is no sense in running more than GOMAXPROCS concurrent
// goroutines on data ingestion path.
addRowsConcurrencyCh = make(chan struct{}, cgroup.AvailableCPUs())
addRowsTimeout = 30 * time.Second
)

队列的长度是CPU核数。假设有10个核,则写入操作最多10个并发。

下面是对于写入并发的处理:lib/storage/storage.go:1529

// AddRows adds the given mrs to s.
func (s *Storage) AddRows(mrs []MetricRow, precisionBits uint8) error {
if len(mrs) == 0 {
return nil
} // Limit the number of concurrent goroutines that may add rows to the storage.
// This should prevent from out of memory errors and CPU trashing when too many
// goroutines call AddRows.
select {
case addRowsConcurrencyCh <- struct{}{}: //如果写入channel成功,说明并发小于最大核数。然后走到插入逻辑去。
default: //如果插入channel失败,说明某个insert操作的协程被阻塞。这时需要通知select协程去让出。
// Sleep for a while until giving up
atomic.AddUint64(&s.addRowsConcurrencyLimitReached, 1)
t := timerpool.Get(addRowsTimeout) // Prioritize data ingestion over concurrent searches.
storagepacelimiter.Search.Inc() // pacelimiter(步长限制器)中有个原子累加的变量,说明有多少个insert操作在等待 select {
case addRowsConcurrencyCh <- struct{}{}: //在超时的时间内,等待入队成功的事件。
timerpool.Put(t) //把timer放回对象池,减少GC
storagepacelimiter.Search.Dec() // insert操作可以顺利调度了,等待的数量原子减一。
// 等待数量为0的时候,调用 cond.Broadcast() 来通知select协程开始工作。
case <-t.C: //等待30秒
timerpool.Put(t)
storagepacelimiter.Search.Dec()
atomic.AddUint64(&s.addRowsConcurrencyLimitTimeout, 1)
atomic.AddUint64(&s.addRowsConcurrencyDroppedRows, uint64(len(mrs)))
return fmt.Errorf("cannot add %d rows to storage in %s, since it is overloaded with %d concurrent writers; add more CPUs or reduce load",
len(mrs), addRowsTimeout, cap(addRowsConcurrencyCh))
// 等待了30秒仍然没有CPU资源,只能报错
}
}
// 这里以下是具体的插入逻辑...
<-addRowsConcurrencyCh // insert逻辑执行完成后,出队 return firstErr
}

3. select操作源码分析

select请求没有区分IO协程和计算协程,因为查询请求通常不多且包很小。

3.1 用于查询并发限制的channel

lib/storage/storage.go:1097

var (
// Limit the concurrency for TSID searches to GOMAXPROCS*2, since this operation
// is CPU bound and sometimes disk IO bound, so there is no sense in running more
// than GOMAXPROCS*2 concurrent goroutines for TSID searches.
searchTSIDsConcurrencyCh = make(chan struct{}, cgroup.AvailableCPUs()*2)
)

查询的并发数限制为CPU核的两倍。

查询限制的处理代码如下:lib/storage/storage.go:1056

// searchTSIDs returns sorted TSIDs for the given tfss and the given tr.
func (s *Storage) searchTSIDs(tfss []*TagFilters, tr TimeRange, maxMetrics int, deadline uint64) ([]TSID, error) {
// Do not cache tfss -> tsids here, since the caching is performed
// on idb level. // Limit the number of concurrent goroutines that may search TSIDS in the storage.
// This should prevent from out of memory errors and CPU trashing when too many
// goroutines call searchTSIDs.
select {
case searchTSIDsConcurrencyCh <- struct{}{}: //处理思路上与insert并发限制一样。入队成功才允许进入查询逻辑
default:
// Sleep for a while until giving up
atomic.AddUint64(&s.searchTSIDsConcurrencyLimitReached, 1)
currentTime := fasttime.UnixTimestamp()
timeoutSecs := uint64(0)
if currentTime < deadline {
timeoutSecs = deadline - currentTime //与insert的超时处理不同,每个查询可能与不同的查询超时时间
}
timeout := time.Second * time.Duration(timeoutSecs)
t := timerpool.Get(timeout)
select {
case searchTSIDsConcurrencyCh <- struct{}{}:
timerpool.Put(t)
case <-t.C:
timerpool.Put(t)
atomic.AddUint64(&s.searchTSIDsConcurrencyLimitTimeout, 1)
return nil, fmt.Errorf("cannot search for tsids, since more than %d concurrent searches are performed during %.3f secs; add more CPUs or reduce query load",
cap(searchTSIDsConcurrencyCh), timeout.Seconds())
}
}

3.2 select协程主动让出的实现

lib/storage/search.go:188

// NextMetricBlock proceeds to the next MetricBlockRef.
func (s *Search) NextMetricBlock() bool {
if s.err != nil {
return false
}
for s.ts.NextBlock() {
if s.loops&paceLimiterSlowIterationsMask == 0 { //每执行4095次后,检查是否有insert协程在等待
if err := checkSearchDeadlineAndPace(s.deadline); err != nil {
// 如果有insert协程等待,在WaitIfNeeded()方法中用条件变量阻塞: cond.Wait()
s.err = err
return false
}
}
s.loops++
//...
}
//...
}

WaitIfNeeded()方法的实现细节:lib/pacelimiter/pacelimiter.go:43

// WaitIfNeeded blocks while the number of Inc calls is bigger than the number of Dec calls.
func (pl *PaceLimiter) WaitIfNeeded() {
if atomic.LoadInt32(&pl.n) <= 0 {
// Fast path - there is no need in lock.
return
}
// Slow path - wait until Dec is called.
pl.mu.Lock()
for atomic.LoadInt32(&pl.n) > 0 { // n代表了高优先级协程等到的个数
pl.delaysTotal++
pl.cond.Wait() // 当n==0时,触发 pl.cond.Broadcast(),让低优先级的协程重新调度
}
pl.mu.Unlock()
}

4. 总结

  1. 关键的计算协程的数量,围绕可用的物理CPU核的数量展开。超过物理核数的协程,CPU资源只会白白浪费在协程调度器上。
  2. 区分高优先级和低优先级的协程,低优先级的协程要能够主动让出。
  3. 用一个队列来代表被调度的关键协程的数量,队列被阻塞就证明有关键协程处于未被调度的状态,这是就需要触发对应的协调机制。感觉就像在golang调度器的基础上又封装了部分能力。

不管怎么样,感谢valyala大神,后面我们就可以直接import这些代码来抄作业了。

golang源码阅读:VictoriaMetrics中的协程优先级的处理方式的更多相关文章

  1. 详细讲解Hadoop源码阅读工程(以hadoop-2.6.0-src.tar.gz和hadoop-2.6.0-cdh5.4.5-src.tar.gz为代表)

    首先,说的是,本人到现在为止,已经玩过.                   对于,这样的软件,博友,可以去看我博客的相关博文.在此,不一一赘述! Eclipse *版本 Eclipse *下载 Jd ...

  2. boost.asio源码阅读(2) - task_io_service

    1.0 task_io_service 在boost.asio源码阅读(1)中,代码已经查看到task_io_service中. 具体的操作调用void task_io_service::init_t ...

  3. JDK源码阅读-DirectByteBuffer

    本文转载自JDK源码阅读-DirectByteBuffer 导语 在文章JDK源码阅读-ByteBuffer中,我们学习了ByteBuffer的设计.但是他是一个抽象类,真正的实现分为两类:HeapB ...

  4. Golang源码探索(二) 协程的实现原理(转)

    Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱,虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻底 ...

  5. go 中 select 源码阅读

    深入了解下 go 中的 select 前言 1.栗子一 2.栗子二 3.栗子三 看下源码实现 1.不存在 case 2.select 中仅存在一个 case 3.select 中存在两个 case,其 ...

  6. 源码阅读笔记 - 1 MSVC2015中的std::sort

    大约寒假开始的时候我就已经把std::sort的源码阅读完毕并理解其中的做法了,到了寒假结尾,姑且把它写出来 这是我的第一篇源码阅读笔记,以后会发更多的,包括算法和库实现,源码会按照我自己的代码风格格 ...

  7. Golang源码探索(二) 协程的实现原理

    Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱, 虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻 ...

  8. SpringMVC源码阅读:Controller中参数解析

    1.前言 SpringMVC是目前J2EE平台的主流Web框架,不熟悉的园友可以看SpringMVC源码阅读入门,它交代了SpringMVC的基础知识和源码阅读的技巧 本文将通过源码(基于Spring ...

  9. caffe-windows中classification.cpp的源码阅读

    caffe-windows中classification.cpp的源码阅读 命令格式: usage: classification string(模型描述文件net.prototxt) string( ...

随机推荐

  1. 【LeetCode】42. Trapping Rain Water 接雨水 (C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 暴力求解 保存左右最大值 单调栈 日期 题目地址:ht ...

  2. HTML网页设计基础笔记 • 【目录】

    持续更新中- 我的大学笔记>>> 章节 内容 第1章 HTML网页设计基础笔记 • [第1章 HTML5基础] 第2章 HTML网页设计基础笔记 • [第2章 排列页面内容] 第3章 ...

  3. 编写Java程序,模拟教练员和运动员出国比赛场景,其中运动员包括乒乓球运动员和篮球运动员。教练员包括乒乓球教练和篮球教练。为了方便出国交流,根乒乓球相关的人员都需要学习英语。

    需求说明: 模拟教练员和运动员出国比赛场景,其中运动员包括乒乓球运动员和篮球运动员.教练员包括乒乓球教练和篮球教练.为了方便出国交流,根乒乓球相关的人员都需要学习英语.具体分析如下: (1)共同的属性 ...

  4. 编写Java程序,使用抽象类和抽象方法构建不同动物的扑食行为,抽象类设计

    返回本章节 返回作业目录 需求说明: 不同的动物都有进食的行为,但是在进食前需要捕获到食物后才可以进食. 要求定义一个抽象的动物类,该类中有一个抽象的捕食方法,和一个非抽象的进食方法.在进食方法中调用 ...

  5. Hadoop Jobhistory配置启用

    Hadoop Jobhistory记录下已运行完的MapReduce作业信息并存放在指定的HDFS目录下, 默认情况下是没有启动的,需要配置完后手工启动服务. 1.修改hadoop的配置文件mapre ...

  6. Pytest_定制allure报告(12)

    定制报告需要先导入allure模块,再使用以下装饰器方法: feature: 标注主要功能模块. story: 标注feature功能模块下的分支功能. description:在报告中显示用例描述. ...

  7. Launch agent by connecting it to the master

    Jenkins Node 是 Windows, Jenkins Server 在 Linux C:\JenkinsAgent\start_jenkins_agent.bat java -DSoftKi ...

  8. Centos7 暂时记录

    chown 修改属主和属组信息 chown -R 对目录所有子的子目录和文件进行修改属主信息 w命令 USER TTY FROM    LOGIN@ IDLE JCPU PCPU WHATroot   ...

  9. react中使用styled-component

    styled-component的使用地址(https://www.cnblogs.com/aichenxy/p/8672752.html)

  10. HBase环境搭建(hbase1.2.5+zookeeper3.4.6)

    注:图片如果损坏,点击文章链接:https://www.toutiao.com/i6627857018461880836/ 系统版本,Hadoop已安装完成 Mysql安装完成 Hive版本 Sqoo ...