HengBus——一款轻量级的跨进程事件总线


项目前瞻

借助网络程序设计这门课的契机,我们实现了一个基于go语言的跨进程事件总线SDK,命名为HengBus(基于我的名字)。HengBus提供了单进程内发布和订阅的功能,也提供了跨进程的发布和订阅功能。经过测试,HengBus能够胜任多功能的业务场景,并轻松处理万级别的qps量级。

背景知识

什么是EventBus?

在这里插入图片描述
EventBus是一个基于发布者/订阅者模式的事件总线框架。发布者/订阅者模式,也就是观察者模式(定义了对象之间的一种一对多的依赖关系,当一个对象状态发生改变时,它的所有依赖者都会收到通知并自动更新)。在EventBus中,当发布者发布事件时,所有订阅该事件的事件处理方法将被调用。
在这里插入图片描述
举个例子来说,在某个网页中你有如下的一个需求:

当用户点击某个按钮时,系统需要在网页渲染一个酷炫的界面并且给数据库发送相关的请求。

用eventbus来实现上述需求的操作如下:

  1. 为函数渲染界面和更新数据库订阅事件"ClickButton"
  2. 当用户点击按钮时,发布事件"ClickButton"

eventbus的使用

原先的eventbus项目主要用于前端和客户端框架中,针对安卓和java语言进行了适配。
具体使用详情请参考 https://github.com/greenrobot/EventBus

项目设计

动机

经个人调研发现,现有的EventBus框架主要存在以下两大局限先:

  • Java等热门语言的框架实现居多
  • 单进程内的EventBus实现居多

受到上述两个限制因素的推动,我开发了 HengBus——一款基于 Golang 语言的跨进程事件总线 SDK。HengBus 符合了当前 Golang 语言日益流行和多服务协同通信日益普遍的趋势。

单进程设计

尽管HengBus的特点是跨进程事件总线,但是其依然支持单进程内的使用,且在单进程内的表现依然出色。
首先,给出数据结构的设计如下:

type EventBus struct {
    handlers map[string][]*eventHandler
    lock     sync.Mutex 
    wg       sync.WaitGroup
}

type eventHandler struct {
    callBack      reflect.Value
    flagOnce      bool
    async         bool
    transactional bool
    sync.Mutex    
}

在单进程中,我们用Map[string][]*eventHandler 来存储topic和handler的对应关系。订阅和发布操作,本质上就转化为了对Map[string][]*eventHandler 结构的增删改查。由于不论是订阅操作,还是发布操作,当进程内的多个线程同时操作时,需要有一定的并发控制操作,所以我们引入了互斥锁sync.Mutex ,来保证订阅和发布操作的原子性。

接着,我们来分析如何存储订阅者订阅时所指定的函数handler。在golang中,我们利用反射机制来存储函数handler,具体地,所有的函数变量都存为interface{},后续执行时,利用反射机制映射为函数执行。同样地,为保证多线程情况下,handler的一致性,我们加入一个互斥锁sync.Mutex。此外,HengBus支持了handler只执行一次、异步执行、事务性保证等功能,反映在数据结构上分别对应于flagOnce、async、transactional 变量。HengBus在单进程内的具体架构图如下
在这里插入图片描述

跨进程设计

为了避免和单进程中的HengBus产生冲突,下面我们将单进程的事件总线称为EventBus,HengBus用于指代我们实现的完整的事件总线。在本部分,我们将整个项目分为三部分:Subcriber、Server、Publisher,和之前单进程中不同,这里的订阅者、发布者不再是一些简单的订阅、发布操作,而是一个个独立的服务,服务间采用http协议通信。HengBus的架构图如下,Subscriber在进程内维护一个EventBus的同时,将进程内的信息同步到Server,Server维护多个Subscriber间的订阅信息。当有Publisher发布信息时,Server告知相关的Subscriber执行相应的topic。
在这里插入图片描述

Subscriber

Subscriber维护了一个进程内的EventBus,数据结构如下:

type Subscriber struct {
    eventBus Bus 
    address  string
    path     string
    serverPath string
    serverAddr string
    service  *SubscriberService
}

其中Bus为前面介绍的单进程事件总线,address和path用于描述订阅者的进程信息,serverPath和serverAddr用于描述Server的进程信息。和之前不同的是,Subscriber的订阅操作除了要在进程内的eventbus变量中执行外,还应同步到Server端。此外,Subcriber也需要能够接收来自Server端的发布信息,因此Subcriber需要暴露一个接收发布信息的接口,如下。

type HandleArgs struct {
    Args  []interface{}
    Topic string
}

func (*SubscriberService) HandleEvent(*HandleArgs, *bool) error 

Publisher

Publisher是一个比较简单的部分,其主要作用是向Server发布事件,没有复杂的数据结构。

type Publisher struct {
    serverAddr string
    serverPath string
}

Server

Server在HengBus中起到一个中心化管理的作用。同样,我们先给出数据结构的设计。


type SubscribeArg struct {
    ClientAddr    string
    ClientPath    string
    ServiceMethod string
    Topic         string
}

type SubscriberHandler struct {
    subArg SubscribeArg
    sync.Mutex
}

type Server struct {
    eventBus    Bus
    address     string
    path        string
    subscribers map[string][]*SubscriberHandler
    service     *ServerService
    lock        sync.Mutex
}

在Server中,我用map[string][]*SubscriberHandler维护topic和Subcriber之间的对应关系。同样地,考虑到并发关系,我们需要加入互斥锁sync.Mutex来保证数据在多进程间的一致性。其次,除了用subscribers变量维护订阅信息外,我们需要暴露两个接口,一个用于接收Publisher发布的信息,一个用于接收Subscriber的订阅变更信息。

type SubscribeArg struct {
    ClientAddr    string
    ClientPath    string
    ServiceMethod string
    Topic         string
}

type HandleArgs struct {
    Args  []interface{}
    Topic string
}

func (*ServerService) Register(*SubscribeArg,*bool) error 

func (*ServerService) PushEvent(*HandleArgs, *bool) error 

实现&&使用

实现

具体的代码实现,我们不再一一详细介绍,有任何疑问请访问我的代码仓库https://github.com/baigeiguai/HengBus提交Issue。

使用

导入包

  1. 通过 go get github.com/baigeiguai/HengBus 命令获取包
  2. 通过 import "github.com/baigeiguai/HengBus" 命令引入包

接口

  1. 对于单进程内的事件总线,可参考如下Demo快速开始。
func MyPrint(a int) {
    fmt.Printf("MyPrint:%d\n", a)
}

func main() {
    bus := HengBus.New()
    bus.Subscribe("main:Print", MyPrint)
    bus.Publish("main:Print", 20)
    bus.Unsubscribe("main:Print", MyPrint)
}

单进程的场景下,我们还提供了如下接口:

  • Subscribe()
  • SubscribeOnce()
  • Unsubscribe()
  • Publish()
  • SubscribeAsync()
  • SubscribeOnceAsync()
  1. 对于跨进程场景下,我们可以参考如下Demo快速开始,分别在文件 subscriber_test.gopublisher_test.goserver_test.go 中。
//订阅者相关进程
func main(t *testing.T){
    subscriber := HengBus.NewSubscriber(":2026", "/_subscriber_", ":2024", "/_server_", HengBus.New())
    defer subscriber.Stop()
    err := subscriber.Start()
    if err != nil {
        panic(err)
    }
    subscriber.Subscribe("main:add", add)
    for true{
        ;//正常服务进程
    }
}

//发布者相关进程
func main(t *testing.T){
    publisher := HengBus.NewPublisher(":2024","/_server_")
    publisher.Publish("main:add",1,2)
}

//Server相关进程
func TestServerDemo(t *testing.T) {
    server := HengBus.NewServer(":2024", "/_server_")
    server.Start()
    defer server.Stop()
}

项目测试

业务测试

本部分,我们编写了如下的四个测试函数。

  1. TestSubscriberAM:订阅add和multiply方法
  2. TestSubscriberMM:订阅minus和multiply方法
  3. TestPublisher:依次发布add、minus、multiply,每次发布之间间隔三秒
  4. TestServerDemo:正常的Server服务部署
    执行上述函数,预期测试结果是:
  5. TestSubscriberAM先执行add函数
  6. TestSubscriberMM执行minus函数
  7. TestSubscriberMM和TestSubscriberAM同时执行multiply
    测试过程展示如下(有服务log打印,未删除)

HengBus业务测试展示

性能测试

单进程场景

本部分我们采用多个协程进行订阅的方式测试HengBus在单进程内的性能。

  • 当qps小于10000时,表现正常,当qps大于10000时,有明显的等待耗时,考虑是现有的锁策略粒度过粗。

多进程场景

本部分我们仅开启了10个进程,每个进程开启高频的订阅发布操作。

  • 当总qps小于5000时,表现正常,当总qps大于5000时,有明显的等待耗时,考虑是http请求的低效性进一步拉低了性能。

致谢&&展望

展望

  • 目前的HengBus在高并发场景下具有性能瓶颈,其主要是受苛刻的加锁策略和并不高效的http通信方式的影响,未来我将继续优化HengBus,进一步提高其在高并发场景下的性能。
  • 目前的HengBus在跨进程场景下,由于时间问题仍未实现异步、事务等功能,未来我将持续完善。
  • 本学期的网络程序设计课程中,另外三个实验WebSocket、RPC、内核网络协议也十分有趣。受最近实验室工作忙碌的影响,未能来得及整理相关内容,后续我会将剩余三个实验整理后也一并发表在我的博客上。

致谢

特别感谢asaskevich在单进程事件总线上的探索和孟宁老师在网络程序设计课上的指导(还欠我一朵小红花哦),两者皆为本文的工作提供了先导的指引。最后,感谢每一位为互联网事业做出贡献的人。
在这里插入图片描述

参考

  1. https://gitee.com/mengning997/net/blob/master/ppt/%E7%BD%91%E7%BB%9C%E5%B9%B6%E5%8F%91%E5%A4%84%E7%90%86%E6%A6%82%E8%BF%B0.pptx
  2. https://greenrobot.org/eventbus/documentation/
  3. https://www.topgoer.com/go%E5%9F%BA%E7%A1%80/
  4. https://github.com/asaskevich/EventBus
方案是为解决特定问题或达成特定目标而制定的一系列计划或步骤。它的作用是提供一种系统性的方法,以有效地应对挑战、优化流程或实现目标。以下是方案的主要作用: 问题解决: 方案的核心目标是解决问题。通过系统性的规划和执行,方案能够分析问题的根本原因,提供可行的解决方案,并引导实施过程,确保问题得到合理解决。 目标达成: 方案通常与明确的目标相关联,它提供了一种达成这些目标的计划。无论是企业战略、项目管理还是个人发展,方案的制定都有助于明确目标并提供达成目标的路径。 资源优化: 方案在设计时考虑了可用资源,以最大化其效用。通过明智的资源分配,方案可以在有限的资源条件下实现最大的效益,提高效率并减少浪费。 风险管理: 方案通常会对潜在的风险进行评估,并制定相应的风险管理策略。这有助于减轻潜在问题的影响,提高方案的可行性和可持续性。 决策支持: 方案提供了决策者所需的信息和数据,以便做出明智的决策。这种数据驱动的方法有助于减少不确定性,提高决策的准确性。 团队协作: 复杂的问题通常需要多个人的协同努力。方案提供了一个共同的框架,帮助团队成员理解各自的职责和任务,促进协作并确保整个团队朝着共同的目标努力。 监控与评估: 方案通常包括监控和评估的机制,以确保实施的有效性。通过定期的评估,可以及时调整方案,以适应变化的环境或新的挑战。 总体而言,方案的作用在于提供一种有序、有计划的方法,以解决问题、实现目标,并在实施过程中最大化资源利用和风险管理。 方案是为解决特定问题或达成特定目标而制定的一系列计划或步骤。它的作用是提供一种系统性的方法,以有效地应对挑战、优化流程或实现目标。以下是方案的主要作用: 问题解决: 方案的核心目标是解决问题。通过系统性的规划和执行,方案能够分析问题的根本原因,提供可行的解决方案,并引导实施过程,确保问题得到合理解决。 目标达成: 方案通常与明确的目标相关联,它提供了一种达成这些目标的计划。无论是企业战略、项目管理还是个人发展,方案的制定都有助于明确目标并提供达成目标的路径。 资源优化: 方案在设计时考虑了可用资源,以最大化其效用。通过明智的资源分配,方案可以在有限的资源条件下实现最大的效益,提高效率并减少浪费。 风险管理: 方案通常会对潜在的风险进行评估,并制定相应的风险管理策略。这有助于减轻潜在问题的影响,提高方案的可行性和可持续性。 决策支持: 方案提供了决策者所需的信息和数据,以便做出明智的决策。这种数据驱动的方法有助于减少不确定性,提高决策的准确性。 团队协作: 复杂的问题通常需要多个人的协同努力。方案提供了一个共同的框架,帮助团队成员理解各自的职责和任务,促进协作并确保整个团队朝着共同的目标努力。 监控与评估: 方案通常包括监控和评估的机制,以确保实施的有效性。通过定期的评估,可以及时调整方案,以适应变化的环境或新的挑战。 总体而言,方案的作用在于提供一种有序、有计划的方法,以解决问题、实现目标,并在实施过程中最大化资源利用和风险管理。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值