开源项目timingwheel

项目地址:RussellLuo/timingwheel: Golang implementation of Hierarchical Timing Wheels. (github.com)

时间轮的原理,我这里就不班门弄斧了,这个项目的作者写了一篇博文,作了详细的说明。层级时间轮的 Golang 实现 | RussellLuo 

我这篇文章只是简单的记录一下使用以及部分源码的注解。

源码阅读

// TimingWheel is an implementation of Hierarchical Timing Wheels.
type TimingWheel struct {
	tick      int64 // in milliseconds
	wheelSize int64

	interval    int64 // in milliseconds
	currentTime int64 // in milliseconds
	buckets     []*bucket
	queue       *delayqueue.DelayQueue

	// The higher-level overflow wheel.
	//
	// NOTE: This field may be updated and read concurrently, through Add().
	overflowWheel unsafe.Pointer // type: *TimingWheel

	exitC     chan struct{}
	waitGroup waitGroupWrapper
}

时间轮结构体概念说明:

tick:时间轮一格时间。

wheelSize:一层时间轮有多少格。

interval:一层时间轮的时长。比如,tick设置为s,wheelSize设置为60,那么,interval=60,即表示一层时间为60s。

buckets:任务列表

queue:延时队列,目的是为了优化时间轮空推问题。

overflowWheel:溢出时间轮。比如,第一层时间轮的时间为60s,现在有一个2分钟的任务,超出了第一层时间轮的时间,此时,就会创建高一层的时间轮,就像时钟一样,秒针转一圈推动分针走一格。

// add inserts the timer t into the current timing wheel.
func (tw *TimingWheel) add(t *Timer) bool {
	currentTime := atomic.LoadInt64(&tw.currentTime)
	if t.expiration < currentTime+tw.tick {
		// Already expired
		return false
	} else if t.expiration < currentTime+tw.interval {
		// Put it into its own bucket
		virtualID := t.expiration / tw.tick
		b := tw.buckets[virtualID%tw.wheelSize]
		b.Add(t)

		// Set the bucket expiration time
		if b.SetExpiration(virtualID * tw.tick) {
			// The bucket needs to be enqueued since it was an expired bucket.
			// We only need to enqueue the bucket when its expiration time has changed,
			// i.e. the wheel has advanced and this bucket get reused with a new expiration.
			// Any further calls to set the expiration within the same wheel cycle will
			// pass in the same value and hence return false, thus the bucket with the
			// same expiration will not be enqueued multiple times.
			tw.queue.Offer(b, b.Expiration())
		}

		return true
	} else {
		// Out of the interval. Put it into the overflow wheel
		overflowWheel := atomic.LoadPointer(&tw.overflowWheel)
		if overflowWheel == nil {
			atomic.CompareAndSwapPointer(
				&tw.overflowWheel,
				nil,
				unsafe.Pointer(newTimingWheel(
					tw.interval,
					tw.wheelSize,
					currentTime,
					tw.queue,
				)),
			)
			overflowWheel = atomic.LoadPointer(&tw.overflowWheel)
		}
		return (*TimingWheel)(overflowWheel).add(t)
	}
}

 从这一段逻辑,我们可以看出,高一层时间轮的槽数和低层时间轮的槽数是一致的,一格时间则是第一层时间轮一圈的时长。

tw.queue.Offer(b, b.Expiration()),把任务丢到了延时队列中。

b := tw.buckets[virtualID%tw.wheelSize]这一段逻辑没有看懂,不知道是什么目的,看起来就是从桶中获取一个桶,仅此而已。有明白这一段逻辑的,帮忙指点一二。

这个地方的设计逻辑,我搞懂了,这个是为了提升性能,避免,频繁的初始化桶,频繁的初始化桶,还会造成频繁的垃圾回收。

我实际测试了一下,把这个地方改成这样初始化,逻辑是正常的,即验证了我上面的推理。 

// Poll starts an infinite loop, in which it continually waits for an element
// to expire and then send the expired element to the channel C.
func (dq *DelayQueue) Poll(exitC chan struct{}, nowF func() int64) {
	for {
		now := nowF()

		dq.mu.Lock()
		item, delta := dq.pq.PeekAndShift(now)
		if item == nil {
			// No items left or at least one item is pending.

			// We must ensure the atomicity of the whole operation, which is
			// composed of the above PeekAndShift and the following StoreInt32,
			// to avoid possible race conditions between Offer and Poll.
			atomic.StoreInt32(&dq.sleeping, 1)
		}
		dq.mu.Unlock()

		if item == nil {
			if delta == 0 {
				// No items left.
				select {
				case <-dq.wakeupC:
					// Wait until a new item is added.
					continue
				case <-exitC:
					goto exit
				}
			} else if delta > 0 {
				// At least one item is pending.
				select {
				case <-dq.wakeupC:
					// A new item with an "earlier" expiration than the current "earliest" one is added.
					continue
				case <-time.After(time.Duration(delta) * time.Millisecond):
					// The current "earliest" item expires.

					// Reset the sleeping state since there's no need to receive from wakeupC.
					if atomic.SwapInt32(&dq.sleeping, 0) == 0 {
						// A caller of Offer() is being blocked on sending to wakeupC,
						// drain wakeupC to unblock the caller.
						<-dq.wakeupC
					}
					continue
				case <-exitC:
					goto exit
				}
			}
		}

		select {
		case dq.C <- item.Value:
			// The expired element has been sent out successfully.
		case <-exitC:
			goto exit
		}
	}

exit:
	// Reset the states
	atomic.StoreInt32(&dq.sleeping, 0)
}
  1. 锁定队列并检查任务
    • 使用互斥锁dq.mu保护对队列的访问。
    • 调用dq.pq.PeekAndShift(now)方法查看并移除队列中最早到期的任务(如果存在且已到期)。
    • item变量存储了到期的任务(如果有的话),delta变量表示当前时间与最早到期任务之间的时间差(以毫秒为单位)。
  2. 处理没有任务的情况
    • 如果itemnil,表示队列中没有到期任务。
      • 如果delta为0,表示队列为空,没有任务等待处理。此时,Poll方法会等待dq.wakeupC通道的信号,该信号在新任务被添加到队列时触发。
      • 如果delta大于0,表示队列中有任务,但尚未到期。此时,Poll方法会等待time.After(time.Duration(delta) * time.Millisecond)返回的定时器超时,即等待任务到期,或者等待dq.wakeupC通道的信号(如果在新任务被添加且其到期时间早于当前等待的任务时触发)。
  3. 处理到期任务
    • 如果item不为nil,表示有任务到期。
      • 使用select语句尝试将到期任务的值发送到dq.C通道。
      • 如果发送成功,则继续下一次循环。
      • 如果发送失败(因为exitC通道关闭),则跳转到exit标签退出循环。
  4. 退出处理
    • 在退出循环时(无论是因为exitC通道关闭还是因为其他原因),重置dq.sleeping状态为0,表示队列不再处于睡眠状态。
  5. 避免忙等待
    • 通过使用time.Afterselect语句等待任务到期或新任务添加的信号,避免了在没有任务时不停地轮询队列,从而有效地解决了空推问题。

使用案例

package main

import (
	"fmt"
	"time"

	"github.com/RussellLuo/timingwheel"
)

func main() {
	tw := timingwheel.NewTimingWheel(time.Millisecond, 20)
	tw.Start()
	defer tw.Stop()
	fmt.Println("TimingWheel start", time.Now())
    
    //1s后的任务
	t := tw.AfterFunc(time.Second, func() {
		fmt.Println("The timer fires", time.Now())
	})
    //1min后的任务
	tw.AfterFunc(time.Minute, func() {
		fmt.Println("The timer fires after 1 Minute", time.Now())
	})

	<-time.After(1 * time.Hour)
	// Stop the timer before it fires
	t.Stop()
}

时间轮实例初始化选择

  1. 对于小规模或单一场景的应用
    • 推荐全局初始化一个时间轮实例,以简化系统架构和提高资源利用率。
  2. 对于大规模或多场景的应用
    • 可以考虑根据不同的任务类型或需求实例化不同的时间轮实例。例如,将延迟敏感的任务和延迟不敏感的任务分别放在不同的时间轮实例中处理。
  3. 动态调整
    • 在实际应用中,可以根据系统负载和任务需求的变化动态地调整时间轮实例的数量和配置。例如,在系统负载较低时合并时间轮实例以减少资源消耗;在系统负载较高时增加时间轮实例以提高并发处理能力。

时间轮监控和调优

最后一个部分尚未实现,待考虑。

1.定义性能指标

首先,需要明确要监控的性能指标。这些指标应该能够反映时间轮实例的性能状况,如:

  • 处理吞吐量:每秒处理的任务数量。
  • 延迟:任务从添加到执行之间的时间差。
  • 内存使用:时间轮实例占用的内存量。
  • CPU使用:时间轮实例消耗的CPU资源。
  • 队列长度:等待执行的任务数量。

2.收集与分析数据

使用选定的监控工具收集性能数据,并进行定期分析。分析应关注以下几个方面:

  • 趋势分析:观察性能指标随时间的变化趋势,识别可能的性能瓶颈或异常。
  • 峰值分析:找出性能指标的高峰时段,分析原因并采取相应措施。
  • 关联分析:分析不同性能指标之间的关联关系,如内存使用与CPU使用之间的相关性。

3. 调优操作

基于分析结果,进行调优操作以改善时间轮实例的性能。这些操作可能包括:

  • 调整时间轮大小:根据任务的数量和分布,调整时间轮的大小(即桶的数量)以优化性能。
  • 优化任务调度:改进任务调度算法,减少延迟和提高吞吐量。
  • 资源分配:为时间轮实例分配更多的CPU或内存资源,以缓解性能瓶颈。
  • 并发控制:通过限流、分片等方式控制并发任务的数量,避免系统过载。

4. 持续监控与反馈

性能监控和调优是一个持续的过程。在调优后,需要继续监控性能指标,并根据反馈进行进一步的调整。此外,随着业务的发展和系统环境的变化,可能需要定期重新评估和优化时间轮实例的性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值