保障后端稳定性:探索一个高效且可维护的 Go 应用流控方案

10,719 阅读24分钟

我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。

有好久没有更新自己的文章了,最近一直在忙着学新东西,就把写文章的事情放了一边。今天我想写一篇关于 FlowControl 的相关文章,主要是想总结一下这个项目的设计思路和实现细节。

当然,在介绍项目之前,还是要从一个问题开始。这个问题使我重新审视了之前自己写的代码,同时也觉得这个问题基本上在用 Go 写业务的小伙伴们应该都会遇到。具体是什么问题呢?不妨我们先从这样一段代码开始:

在一段业务处理逻辑中,我们无可避免地需要对上游服务进行调用。不管这个调用是同步还是异步的,都需要发送请求到后端服务,然后等待后端服务的响应。我们假设下,当有很多的客户端都在请求后端服务的时候,后端服务可能为了保护自己的接口服务能力,会对请求进行限流。通常的手段是通过 QPS 限制(这个可以基于 HTTP Header 中任何有效资源或者是客户端的 IP)。我想绝大多数的小伙伴们都是想都不想,判断下服务端的返回码,如果是 429 的话,就直接返回给客户端一个 429 的状态码,告诉客户端请求太多了,稍后再试。

殊不知,此时服务器的 TCP 连接已经建立,客户端已经发送了请求,服务器端已经开始处理请求。频繁的请求过来,服务器端的处理能力是有限的。如果请求过多,服务器端的处理能力就会被耗尽,导致服务器端的服务能力下降,甚至是崩溃。这个时候我们就需要在客户端这边进行限流控制,以保护后端服务的稳定性。如果不对客户端限流,那么后端服务就活活被客户端给搞垮了。

用一句话大白话说:客户端限流是为了保护服务端,服务端限流是为了保护自己。

通过下面简单的代码片段来说明这个问题:

通过 http.Get 请求后端服务,如果返回的状态码是 429,那么就直接 Sleep 一段时间,然后再次请求后端服务。整个过程是一个死循环,直到请求成功为止。非常暴力,不推荐使用。我个人觉得这个稍微有点像 DDOS 攻击。

var ErrTooManyRequests = errors.New("Too many requests")

func doCallSomething() error{
    resp, err := http.Get("http://example.com")
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode == http.StatusTooManyRequests {
        return ErrTooManyRequests
    }

    // do something with resp
    ...

    return nil
}

func doRetry() {
    for {
        err := doCallSomething()
        if err != nil {
            log.Println(err)
            if !errors.Is(err, ErrTooManyRequests) {
                return
            }
            time.Sleep(time.Second)
        } else {
            return
        }
    }
}

那么到底有没有好办法呢?当然有,这个时候我们就可以使用 FlowControl 来解决这个问题。整体上可以设计思路是基于 Token Bucket 算法,通过 Token Bucket 算法来控制客户端的请求速率,以保护后端服务的稳定性。

有小伙伴一定会对我说:老李,你说的很好,你说得我都懂。有没有一个相对标准通用的 FlowControl 的实现呢?我不想自己去实现,因为我觉得这个东西应该是一个通用的组件,不应该每个人都去实现一遍。这样会浪费很多时间,而且很多人的实现可能都不太一样。我想要一个通用的 FlowControl 组件,这样我就可以直接拿来用,而不用自己去实现。

业务真实的痛点才是推动革新的源动力,这个时候我就开始思考,为什么不自己写一个 FlowControl 组件呢?这样就可以解决很多小伙伴们的痛点,而且也可以帮助大家提高开发效率。这个时候我的 Regula 项目就诞生了。

事件背景

在之前实际的业务场景中,因为客户端请求过多,导致后端服务触发了限速保护,并返回大量的 429 状态码,但是实在是客户端这边没有做好限流控制,导致后端服务在流向高峰期过去后,服务能力并没有恢复,TCP 连接而是一直处于一个比较高的状态。甚至多数的请求都出现了延迟很高的情况,一直到很长的一段时间过后,整个服务质量才恢复正常。

当初我和小伙伴一起排查问题,百思不得其解。尤其有些商户他们在做活动,比如每 5-10 分钟随机发福利,这个时候客户端请求量就会暴增,导致后端服务的稳定性受到了影响。然后后端服务在触发限流以后,平稳度过了高峰期,但是服务质量并没有恢复。下一次活动开始的时候,我们明显观测到了服务质量的下降,这个时候我们就意识到了问题的严重性。

后端的服务 TCP 连接数爆炸了,大量的 TCP 连接处于 TIME_WAIT 状态,导致服务的稳定性受到了影响。

regula-0.png

痛点分析

在之前提到的代码中,我们可以看到,我们的代码是一个死循环,直到请求成功为止。这个时候我们就需要在客户端这边进行限流控制,以保护后端服务的稳定性。如果不对客户端限流,那么后端服务就活活被客户端给搞垮了。

但是如果让我们手撸一个 FlowControl 组件,想想也没有那么简单。虽然原理很简单,就是根据 Token Bucket 算法来控制客户端的请求速率,但是内部逻辑应该如何设计呢?有没有可以参考的成熟且简单的模型参考呢,如果有那么我为什么要做呢?这些问题都紧紧围绕在我的心头。

通过与小伙伴们一段时间的沟通和思考,决定采用一个相对复杂的模型:workqueue + workpool + ratelimiter 模式。在设计这个模型时,我也参考了其他多种处理并发和数据流的方式,例如 channel、sync、context 等,但总觉得这些方式都不太适合这个场景,而且在很多方面都有些不足。如果要将这些模型强行绑定和实现,还不如自己写一个简单的模型来得更加直接和简单。那为什么要选择 workqueue + workpool + ratelimiter 模式呢?请耐心跟着我一起看下我是怎么设计的。

想要实现的效果:

var ErrTooManyRequests = errors.New("Too many requests")

func doCallSomething() error{
    resp, err := http.Get("http://example.com")
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode == http.StatusTooManyRequests {
        return ErrTooManyRequests
    }

    // do something with resp
    ...

    return nil
}

func main() {
    // 1. 创建一个限速器
    ratelimiter := NewRateLimiter(10, 1)

    // 2. 创建一个 FlowController
    flowcontroller := NewFlowController(ratelimiter)
    defer flowcontroller.Stop()

    // 3. 业务逻辑
    func doRetry() {
        for {
            // 投递一个任务,如果任务被限速器限制,那么任务会被延迟执行
            err := flowcontroller.Do(func() error {
                return doCallSomething()
            })

            if err != nil {
                log.Println(err)
                if !errors.Is(err, ErrTooManyRequests) {
                    return
                }
                time.Sleep(time.Second)
            } else {
                return
            }
        }
    }

    // 4. 运行逻辑
    doRetry()
}

看上面的代码是不是跟之前那个死循环的代码有点像呢?但是这段代码中使用了 flowcontroller.Do 方法,保证了请求的速度严格按照 ratelimiter 的速度进行,这样就可以实现对客户端的限流,保护后端服务的稳定性。

这个使用方式是不是很简单呢?NewFlowController 这个工厂函数创建了一个 FlowController 对象,然后与 ratelimiter 绑定在一起,这样就可以确保客户端的请求速率受到限制。然后通过 flowcontroller.Do 方法来投递一个任务,如果任务被限速器限制,那么任务会被延迟执行。

当然,doRetry 这个方法可以使用 Retry 库来实现。想了解更多关于 Retry 库的内容,可以参考我的文章:《一次不够,就再试一次:用 Retry,让 Golang 函数重试轻而易举的轻量级神器》

整体很美好,但其中存在许多细节问题需要考虑和解决:

  • 创建并调用 ratelimiter 对象

    • 描述 NewRateLimiter 函数的参数和初始化过程。
    • 详细说明 ratelimiterAllowWait 方法的调用机制。
    • 举例说明在网络请求或数据库操作中如何应用 ratelimiter
  • 处理任务执行的延迟

    • 讨论延迟机制的实现和 ratelimiter 的计算下一次执行时间的方式。
    • 分析延迟机制对系统性能和用户体验的影响。
    • 探讨并发处理问题和选择合适的数据结构来支持时间管理和请求排队。
  • flowcontrollerDo 方法确保任务执行顺序

    • 说明同步策略和队列管理对任务顺序的影响。
    • 讨论错误处理和异常安全的考虑。
    • 探讨在多线程环境下实现线程安全的技术和性能优化。
  • flowcontrollerDo 方法的线程安全性

    • 详述线程安全的实现技术和在高并发场景下的性能优化。
    • 提出测试线程安全性和进行压力测试的方法.

想法很美好,但是实现起来确实各种打脸。底层有大量细节和问题需要考虑,曾经我也是一脸懵逼,不知道从何下手。

通过不断尝试和探索,我终于找到了一种比较好的实现方式。利用 workqueue 作为最底层的数据队列,karta 作为数据处理的工具,ratelimiter 作为速率控制的工具,然后将这三个模块整合在一起,就可以实现一个完整的 FlowControl 组件了。 这样做的好处是,利用现有成熟的模块,可以大大减少开发的时间和成本,同时也可以提高代码的可维护性和可扩展性。

面对的挑战与应对思路

  • 参数配置与初始化复杂性

    • 复杂的参数需求:多样的应用场景要求参数配置具有高度的灵活性和精确性。
    • 错误配置的风险:配置错误可能导致性能瓶颈或资源浪费,需要增加配置验证机制。
    • 学习成本:用户需要花费时间理解各参数的功能和影响,增加了系统的使用难度。
  • 调用机制的适应性问题

    • 环境差异:不同系统环境对调用机制的适应性有不同要求,需要灵活调整。
    • 性能与延迟的权衡:调整调用机制时需平衡性能和延迟,确保系统效率。
    • 系统交互影响:调用策略需与系统其他部分良好交互,否则可能引起更广泛的系统问题。
  • 实际应用中的限制

    • 外部依赖影响:如网络和数据库的响应时间会影响 ratelimiter 的效果。
    • 动态环境适应:需要根据实际操作负载和外部环境的变化动态调整。
    • 持续性能监控:实时监控性能,以便及时调整配置和策略,保持系统性能最优化。
  • 延迟机制的精确性问题

    • 高负载环境的挑战:在高负载或多任务环境中保持延迟控制的精确性尤为困难。
    • 复杂的时间管理技术:需要精确计算和管理时间,通常涉及复杂的算法。
    • 优先级处理:实现优先级队列以确保重要任务能够优先得到处理。
  • 同步和顺序控制的复杂性

    • 避免死锁和竞态条件:设计必须确保没有死锁或竞态条件,这增加了同步的复杂度。
    • 效率与可扩展性的权衡:队列管理和同步策略设计需要兼顾效率和系统的可扩展性。
    • 顺序执行的需求:某些任务必须严格按顺序执行,这对同步机制的设计提出了高要求。
  • 线程安全与性能的平衡

    • 多线程环境的复杂性:确保线程安全通常会增加系统的复杂性和维护难度。
    • 性能考虑:线程安全措施可能牺牲一定的性能,尤其在高并发环境中更为明显。
    • 维护难度:增加的复杂性和维护需求可能导致系统难以迭代和扩展。

解决方案

实际将问题抽丝剥茧后,编写并不那么艰难,很简单,甚至可以用“见招拆招”来形容。下面我将详细介绍如何解决上述问题。

我的解题思路:

  1. 使用一个 Queue 作为连接存储的数据结构。
  2. 这个 Queue 具备 Delaying 排序的特性。
  3. 这个 Queue 拥有线程安全的特性。
  4. 拥有一个 RateLimiter 作为速率控制的工具。
  5. RateLimiter 拥有一条明确的能力分割线,当在控制阈值内,任务会被立即执行(不限制),否则会被延迟执行(限制)。
  6. RateLimiter 产生的时间戳作为 Delaying 的时间戳。
  7. 拥有一组协程组构成的 WorkPool 读取 Queue 中的数据,然后执行任务。
  8. WorkPool 并不是固定的,而是根据任务提交的速度动态调整。
  9. 支持自定义的 WorkPoolQueueRateLimiter 模块,以满足不同的业务需求。
  10. 考虑到 WorkPoolQueueRateLimiter 模块一起工作的复杂性,提供一个 "懒人" 模式,只需要调用一个函数就可以完成所有的初始化工作。

在有相关的解题思路后,在实现过程中,我遵循了以下设计原则:

  • 单一职责原则

    • 功能专一性:确保每个模块专注于一个具体功能,避免承担多重职责。
    • 易于理解和维护:模块的专一性使其更易于理解和维护。
    • 降低影响范围:修改单一职责的模块时,影响范围局限,减少对系统其他部分的潜在影响。
  • 开闭原则

    • 接口开放:模块应对扩展开放,使得新增功能可以通过扩展现有模块实现。
    • 实现封闭:一旦定义,模块内部的实现不应对修改开放,保证了模块的稳定性。
    • 促进模块可用性:通过允许扩展而非修改,降低了对原有代码的风险和成本。
  • 接口隔离原则

    • 精简接口:每个模块仅提供必要的接口功能,避免冗余。
    • 用户需求导向:接口设计基于用户实际需求,不被不相关功能负担。
    • 降低复杂性:减少不必要的交互,简化系统架构,提高模块独立性。
  • 最小化依赖原则

    • 降低耦合:尽量减少模块之间的直接关联,用最少的依赖关系维持功能。
    • 提高模块可重用性:独立性更高的模块更易于在其他项目或环境中重用。
    • 简化测试和维护:模块之间依赖最小化,使得测试和维护工作更为简单。
  • 简单易用原则

    • 用户友好的接口:接口设计简洁直观,容易被用户理解和使用。
    • 降低学习成本:简化的接口和文档减少了新用户的学习曲线。
    • 支持快速扩展:设计简单的接口使得新增功能或扩展现有功能更加方便。
  • 易于维护原则

    • 清晰的模块关系:模块之间的依赖关系明确,无隐蔽的依赖。
    • 支持快速问题定位:系统的透明度高,问题可以快速定位和解决。
    • 方便后续扩展:清晰的结构使得在现有系统上进行修改和扩展更为容易。

结构设计

LAW (1).png

结构阐述

Regula 从一开始设计之初,就是为了解决客户端限流的问题,保护后端服务的稳定性。整体上可以设计思路是基于 Token Bucket 算法,通过 Token Bucket 算法来控制客户端的请求速率,然后通过 FlowControl 组件来实现这个功能。整体上追求的是代码的简洁、高效、易用、可维护性和可扩展性。

所以整个模型就两个部分:数据队列、速率控制器和任务执行器。

数据队列

Queue 作为连接存储的数据结构,具备 Delaying 排序的特性,拥有线程安全的特性。这个 Queue 作为 FlowControl 的基础模块,用来存储任务数据,然后按照 Delaying 排序的特性来控制任务的执行顺序。

这个模块非常重要,几乎是整个 Regula 的核心。所有向 Regula 投递的工作任务都是异步的,使用 Queue 来存储这些任务,然后每次投递的时候都会从 RateLimiter 中获取一个 Token,然后根据 Token 的数量来决定任务的执行顺序。这个 Token 就是 Delaying 的时间戳,然后按照 Delaying 排序的特性来控制任务的执行顺序。

Workqueue 项目,同样可以参考我的文章:《解码队列精髓:Delaying Queue 与 Priority Queue 的深度剖析》

当然如果你对 Workqueue 项目不感兴趣,或者拥有更好的实现,那么你可以自己实现一个 Queue 模块,只要满足 Delaying 排序的特性,实现 DelayingQueueInterface 接口,然后具备线程安全的特性即可。然后将自定义的 Queue 模块与任务执行器和速率控制器整合在一起,就可以实现一个完整的 FlowControl 组件了。

Queue 模块的接口如下:

// QueueInterface 是一个接口,定义了队列的基本操作,如添加元素、获取元素、标记元素完成、停止队列和判断队列是否已经关闭
// QueueInterface is an interface that defines basic operations of a queue, such as adding elements, getting elements, marking elements as done, stopping the queue, and checking if the queue is closed
type QueueInterface interface {
	Add(element any) error         // 添加元素 (Add element)
	Get() (element any, err error) // 获取元素 (Get element)
	Done(element any)              // 标记元素完成 (Mark element as done)
	Stop()                         // 停止队列 (Stop the queue)
	IsClosed() bool                // 判断队列是否已经关闭 (Check if the queue is closed)
}

// DelayingQueueInterface 是一个接口,它继承了 QueueInterface,并添加了一个新的方法 AddAfter,用于在指定的延迟后添加元素到队列
// DelayingQueueInterface is an interface that inherits from QueueInterface and adds a new method AddAfter for adding elements to the queue after a specified delay
type DelayingQueueInterface interface {
	QueueInterface
	AddAfter(element any, delay time.Duration) error // 在指定的延迟后添加元素到队列 (Add an element to the queue after a specified delay)
}

Delaying Queue 架构图

regula-2.png

任务执行器

Karta 作为数据处理的工具,用来处理任务的执行。 KartaRegula 中作为主要工作执行单元,用来执行任务。Karta 会根据 Queue 中的任务数据的排序,逐一执行这些任务。 Karta 内部包含了一个动态的 WorkPool,它会根据任务的提交速度动态调整协程数量,当系统整体空闲下来时,它能回收这些工作协程,以保证任务的执行效率,所以不用担心过多的协程空闲导致资源浪费。

Karta 项目,可以参考我的文章:《Golang 批量和异步处理任务利器:Karta 异步和批量处理任务库》

在这里使用 Karta 中的 pipeline 模块。

异步模式 (pipeline):处理任务数量未知的情况。在异步模式下,Karta 将任务放入队列中,然后由一个或多个协程来执行这些任务。这种模式可以提高任务的执行效率,从而提高系统的并发能力。

刚才 数据队列 中提到的自定义 Queue 模块,实现 DelayingQueueInterface 接口,可以直接通过 KartaNewPipeline 方法,将自定义的 Queue 模块传入,就可以轻松将 QueueKarta 整合在一起,实现将任务投递到 Queue 中,然后由 Karta 来执行这些任务的过程了。

模式特征

  • 任务数量未知
  • 任务放入队列
  • 由一个或多个协程执行这些任务

流程图

karta-2.png

速率控制器

RateLimiter 作为速率控制的工具,用来控制客户端的请求速率。 RateLimiter 模块是单独设计的,基于 googlegolang.org/x/time 包进行二次封装。因此,没有什么特别的花头,主要是调用 ratelimite.Reserve().Delay() 方法。

regula-4.png

当然,这只是默认的 RateLimiter 模块。如果你有更好的实现,那么你可以自己实现一个满足 RateLimiterInterface 接口的 RateLimiter 模块。

// RateLimiterInterface 是一个接口,定义了一个方法,该方法返回下一个事件的延迟时间
// RateLimiterInterface is an interface that defines a method that returns the delay time of the next event
type RateLimiterInterface = interface {
	// When 返回下一个事件的延迟时间
	// When returns the delay time of the next event
	When() time.Duration
}

那么 RateLimiter 是如何与 QueueKarta 整合在一起的呢?其实很简单,只要在提交任务到 Karta 之前,调用 RateLimiterWhen 方法,然后根据返回的延迟时间来决定任务的执行顺序即可。如果 When 返回的延迟时间是 0,那么任务会被立即执行,否则会被延迟执行。

代码实现

// Do 是一个方法,它执行一个消息处理函数,如果有延迟,它会在延迟后提交函数,否则直接提交
// Do is a method that executes a message handle function, if there is a delay, it submits the function after the delay, otherwise it submits directly
func (fc *FlowController) Do(fn MessageHandleFunc, msg any) error {
	// 通过速率限制器获取下一个事件的延迟时间
	// Get the delay time of the next event through the rate limiter
	delay := fc.config.ratelimiter.When().Round(rl.DefaultEffectiveTimeSliceInterval)

	// 如果有延迟
	// If there is a delay
	if delay > 0 {
		// 调用回调函数,通知有延迟
		// Call the callback function to notify that there is a delay
		fc.config.callback.OnExecLimited(msg, delay)

		// 在延迟后提交函数
		// Submit the function after the delay
		return fc.pipline.SubmitAfterWithFunc(fn, msg, delay)
	}

	// 如果没有延返,直接提交函数
	// If there is no delay, submit the function directly
	return fc.pipline.SubmitWithFunc(fn, msg)
}

项目介绍

Regula: github.com/shengyanli1…

regula-1.png

Regula 是一个流控组件,旨在帮助 Golang 应用程序管理并发和数据流。它被设计为简单、高效和易于使用,并可用于各种应用程序,从简单的 Web 应用程序到复杂的分布式系统。

Regula 基于 workqueue + workpool + ratelimiter 模式。这使您可以将任何函数提交给 Regula 进行并发和速率限制的执行。因此,Regula 适用于需要同时具备并发和速率限制的场景。

Regula 是限制对任何资源(如数据库、API 或文件)请求速率的理想选择。

Regula 具有以下关键优势:

  • 简单性Regula 提供简洁的接口,使开发者能够轻松地提交函数,管理并发和数据流。
  • 高效性Regula 的设计注重轻量级结构,确保在多种应用场景下保持优异的性能。
  • 灵活性Regula 允许使用多种模式,如 workqueueworkpoolratelimiter,不受限于特定架构。
  • 可扩展性:用户可以根据具体需求自定义 pipelineratelimiter,增强系统的适应性。
  • 可靠性Regula 基于经过验证的技术构建,并已在多个应用程序中进行了广泛测试,确保其稳定性和可靠性。

Regula 提供两种使用模式,分别是 懒人 模式和 专家 模式。

  • 懒人模式: 只需要调用一个函数就可以完成所有的初始化工作。
  • 专家模式: 可以自定义 QueueKartaRateLimiter 模块,以满足不同的业务需求。

初始化

初始化部分主要是根据使用者对 FlowControl 的需求来初始化 Regula 的参数。

Regula 提供了一个配置对象,允许您自定义其行为。您可以使用以下方法来配置配置对象:

Controller 配置

设置 FlowController 的配置对象,可以通过以下方法来配置:

  • WithRateLimiter: 设置速率限制器。默认值:NewNopLimiter()(没有速率限制)
  • WithCallback: 设置回调函数。默认值:NewEmptyCallback()(没有回调函数)

Ratelimiter 配置

设置 RateLimiter 的配置对象,可以通过以下方法来配置:

  • WithRate: 设置速率限制。默认值:10.0(每秒 10 个事件)
  • WithBurst: 设置速率限制的突发值。默认值:5(每秒 5 个事件)

接口设计

接口设计本着最简单、最易用、最灵活、最可扩展的原则。

方法接口

Regula 方法接口也非常简洁,只有几个方法,非常容易上手。

  • NewFlowController: 创建一个新的 FlowController 对象。专家模式
  • NewSimpleFlowController: 创建一个新的 FlowController 对象。懒人模式
  • Stop: 停止 FlowController 对象。
  • Do: 执行一个消息处理函数。

回调接口

回调接口用于定义 Regula 的回调函数。它包括以下方法:

  • OnExecLimited: 仅当执行受限时调用的方法,参数:msg 是消息,delay 是延迟时间。

使用示例

我将通过简单示例来演示如何使用 Regula

懒人模式

懒人模式 下,您可以使用 NewSimpleFlowController 方法创建一个新的流控制器。该方法封装了 NewFlowController 方法,并使用默认的 pipelineratelimiter 模块。

NewSimpleFlowController 方法允许您指定 rateburst 参数,并提供一个可选的 callback 函数。

以下是使用 懒人模式 的示例:

  1. 创建一个速率为 2、突发为 1 的流控制器,并添加回调函数。
  2. 启动 10 个协程。
  3. 提交一个函数给流控制器。
  4. 等待所有协程完成。
package main

import (
	"fmt"
	"sync"
	"time"

	"github.com/shengyanli1982/regula/contrib/lazy"
)

// demoCallback 是一个空结构体,用于实现回调接口
// demoCallback is an empty structure used to implement the callback interface
type demoCallback struct{}

// OnExecLimited 是一个方法,当被限制时,它会打印出被限制的延迟时间
// OnExecLimited is a method that prints the limited delay time when being limited
func (demoCallback) OnExecLimited(msg any, delay time.Duration) {
	fmt.Printf("limited -> msg: %v, delay: %v\n", msg, delay.String())
}

// newCallback 是一个函数,它创建并返回一个新的demoCallback
// newCallback is a function that creates and returns a new demoCallback
func newCallback() *demoCallback {
	return &demoCallback{}
}

func main() {
	// 创建一个新的流控制器,设置速率为2,突发为1和回调函数
	// Create a new flow controller, set the rate to 2, burst to 1 and callback function
	fc := lazy.NewSimpleFlowController(float64(2), 1, newCallback())

	// 在函数结束时停止管道、队列和流控制器 (FlowController 会关闭 Pipeline,Pipeline 会关闭 Queue)
	// Stop the pipeline, queue and flow controller when the function ends (FlowController will close Pipeline, Pipeline will close Queue)
	defer fc.Stop()

	// 创建一个等待组
	// Create a wait group
	wg := sync.WaitGroup{}

	// 启动10个协程
	// Start 10 goroutines
	for i := 0; i < 10; i++ {
		v := i
		// 增加等待组的计数
		// Increase the count of the wait group
		wg.Add(1)
		go func() {
			// 在协程结束时减少等待组的计数
			// Decrease the count of the wait group when the goroutine ends
			defer wg.Done()
			// 执行一个消息处理函数,如果有错误,打印错误信息
			// Execute a message handle function, if there is an error, print the error message
			if err := fc.Do(func(msg any) (any, error) {
				fmt.Printf("msg: %v -> %v\n", msg, v)
				return msg, nil
			}, "test"); err != nil {
				fmt.Printf("fc.Do should not return error: %v\n", err)
			}
		}()
	}

	// 等待所有协程结束
	// Wait for all goroutines to end
	wg.Wait()

	// 等待5秒
	// Wait for 5 seconds
	time.Sleep(time.Second * 5)
}

执行结果

$ go run demo.go
limited -> msg: test, delay: 1s
limited -> msg: test, delay: 500ms
limited -> msg: test, delay: 3.5s
limited -> msg: test, delay: 1.5s
limited -> msg: test, delay: 3s
limited -> msg: test, delay: 2s
limited -> msg: test, delay: 2.5s
limited -> msg: test, delay: 4.5s
limited -> msg: test, delay: 4s
msg: test -> 9
msg: test -> 4
msg: test -> 5
msg: test -> 0
msg: test -> 8
msg: test -> 6
msg: test -> 3
msg: test -> 2
msg: test -> 7
msg: test -> 1

专家模式

专家模式 下,您可以使用 NewFlowController 方法创建自定义的流控制器。这样可以自定义 pipelineratelimiter 模块。

专家模式 下,您需要自定义 pipelineratelimiter 模块,并根据需要设置所需的参数。然后,将它们传递给 NewFlowController 方法。

以下是在 专家模式 下使用 Regula 库的示例:

  1. 使用自定义的延迟队列创建新的流水线。
  2. 使用自定义的流水线和速率限制器创建新的流控制器。
  3. 为流控制器设置回调函数。
  4. 启动 10 个 goroutine。
  5. 将函数提交给流控制器。
  6. 等待所有 goroutine 完成。
package main

import (
	"fmt"
	"sync"
	"time"

	"github.com/shengyanli1982/karta"
	"github.com/shengyanli1982/regula"
	rl "github.com/shengyanli1982/regula/ratelimiter"
	"github.com/shengyanli1982/workqueue"
)

// demoCallback 是一个空结构体,用于实现回调接口
// demoCallback is an empty structure used to implement the callback interface
type demoCallback struct{}

// OnExecLimited 是一个方法,当被限制时,它会打印出被限制的延迟时间
// OnExecLimited is a method that prints the limited delay time when being limited
func (demoCallback) OnExecLimited(msg any, delay time.Duration) {
	fmt.Printf("limited -> msg: %v, delay: %v\n", msg, delay.String())
}

// newCallback 是一个函数,它创建并返回一个新的demoCallback
// newCallback is a function that creates and returns a new demoCallback
func newCallback() *demoCallback {
	return &demoCallback{}
}

func main() {
	// 创建一个新的配置,并设置工作线程数为2
	// Create a new configuration and set the number of worker threads to 2
	kconf := karta.NewConfig().WithWorkerNumber(2)

	// 创建一个新的假延迟队列
	// Create a new fake delay queue
	queue := workqueue.NewDelayingQueueWithCustomQueue(nil, workqueue.NewSimpleQueue(nil))

	// 使用队列和配置创建一个新的管道
	// Create a new pipeline using the queue and configuration
	pl := karta.NewPipeline(queue, kconf)

	// 创建一个新的速率限制器,并设置速率为10,突发为1
	// Create a new rate limiter and set the rate to 10 and burst to 1
	rl := rl.NewRateLimiter(rl.NewConfig().WithRate(10).WithBurst(1))

	// 创建一个新的流控制器配置,并设置回调函数和速率限制器
	// Create a new flow controller configuration and set the callback function and rate limiter
	fconf := regula.NewConfig().WithCallback(newCallback()).WithRateLimiter(rl)

	// 使用管道和配置创建一个新的流控制器
	// Create a new flow controller using the pipeline and configuration
	fc := regula.NewFlowController(pl, fconf)

	// 在函数结束时停止管道、队列和流控制器 (FlowController 会关闭 Pipeline,Pipeline 会关闭 Queue)
	// Stop the pipeline, queue and flow controller when the function ends (FlowController will close Pipeline, Pipeline will close Queue)
	defer fc.Stop()

	// 创建一个等待组
	// Create a wait group
	wg := sync.WaitGroup{}

	// 启动10个协程
	// Start 10 goroutines
	for i := 0; i < 10; i++ {
		v := i
		// 增加等待组的计数
		// Increase the count of the wait group
		wg.Add(1)
		go func() {
			// 在协程结束时减少等待组的计数
			// Decrease the count of the wait group when the goroutine ends
			defer wg.Done()
			// 执行一个消息处理函数,如果有错误,打印错误信息
			// Execute a message handle function, if there is an error, print the error message
			if err := fc.Do(func(msg any) (any, error) {
				fmt.Printf("msg: %v -> %v\n", msg, v)
				return msg, nil
			}, "test"); err != nil {
				fmt.Printf("fc.Do should not return error: %v\n", err)
			}
		}()
	}

	// 等待所有协程结束
	// Wait for all goroutines to end
	wg.Wait()

	// 等待2秒
	// Wait for 2 seconds
	time.Sleep(time.Second * 2)
}

执行结果

$ go run demo.go
msg: test -> 9
limited -> msg: test, delay: 400ms
limited -> msg: test, delay: 700ms
limited -> msg: test, delay: 300ms
limited -> msg: test, delay: 200ms
limited -> msg: test, delay: 500ms
limited -> msg: test, delay: 600ms
limited -> msg: test, delay: 100ms
limited -> msg: test, delay: 800ms
limited -> msg: test, delay: 900ms
msg: test -> 5
msg: test -> 3
msg: test -> 0
msg: test -> 7
msg: test -> 2
msg: test -> 4
msg: test -> 6
msg: test -> 1
msg: test -> 8

总结

Regula 是一个流控组件,旨在帮助 Golang 应用程序管理并发和数据流。它提供了简洁、高效、易用、可维护和可扩展的解决方案,适用于各种应用程序,从简单的 Web 应用程序到复杂的分布式系统。

Regula 的设计基于 workqueue + workpool + ratelimiter 模式,这使得它具有很高的灵活性和可扩展性。您可以根据具体需求自定义 pipelineratelimiter,以满足不同的业务需求。

通过 Regula,我们可以避免重复造轮子,提高开发效率,减少代码冗余。希望 Regula 能够帮助更多的开发者,让他们的开发工作更加轻松和高效。

最后,如果您有任何问题或建议,请在 RegulaGitHub 上提出 issue。我将尽快回复您的问题。