文章目录
项目前瞻
借助网络程序设计这门课的契机,我们实现了一个基于go语言的跨进程事件总线SDK,命名为HengBus(基于我的名字)。HengBus提供了单进程内发布和订阅的功能,也提供了跨进程的发布和订阅功能。经过测试,HengBus能够胜任多功能的业务场景,并轻松处理万级别的qps量级。
背景知识
什么是EventBus?
EventBus是一个基于发布者/订阅者模式的事件总线框架。发布者/订阅者模式,也就是观察者模式(定义了对象之间的一种一对多的依赖关系,当一个对象状态发生改变时,它的所有依赖者都会收到通知并自动更新)。在EventBus中,当发布者发布事件时,所有订阅该事件的事件处理方法将被调用。
举个例子来说,在某个网页中你有如下的一个需求:
当用户点击某个按钮时,系统需要在网页渲染一个酷炫的界面并且给数据库发送相关的请求。
用eventbus来实现上述需求的操作如下:
- 为函数渲染界面和更新数据库订阅事件"ClickButton"
- 当用户点击按钮时,发布事件"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。
使用
导入包
- 通过
go get github.com/baigeiguai/HengBus
命令获取包 - 通过
import "github.com/baigeiguai/HengBus"
命令引入包
接口
- 对于单进程内的事件总线,可参考如下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()
- 对于跨进程场景下,我们可以参考如下Demo快速开始,分别在文件 subscriber_test.go、publisher_test.go 、server_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()
}
项目测试
业务测试
本部分,我们编写了如下的四个测试函数。
- TestSubscriberAM:订阅add和multiply方法
- TestSubscriberMM:订阅minus和multiply方法
- TestPublisher:依次发布add、minus、multiply,每次发布之间间隔三秒
- TestServerDemo:正常的Server服务部署
执行上述函数,预期测试结果是: - TestSubscriberAM先执行add函数
- TestSubscriberMM执行minus函数
- TestSubscriberMM和TestSubscriberAM同时执行multiply
测试过程展示如下(有服务log打印,未删除)
HengBus业务测试展示
性能测试
单进程场景
本部分我们采用多个协程进行订阅的方式测试HengBus在单进程内的性能。
- 当qps小于10000时,表现正常,当qps大于10000时,有明显的等待耗时,考虑是现有的锁策略粒度过粗。
多进程场景
本部分我们仅开启了10个进程,每个进程开启高频的订阅发布操作。
- 当总qps小于5000时,表现正常,当总qps大于5000时,有明显的等待耗时,考虑是http请求的低效性进一步拉低了性能。
致谢&&展望
展望
- 目前的HengBus在高并发场景下具有性能瓶颈,其主要是受苛刻的加锁策略和并不高效的http通信方式的影响,未来我将继续优化HengBus,进一步提高其在高并发场景下的性能。
- 目前的HengBus在跨进程场景下,由于时间问题仍未实现异步、事务等功能,未来我将持续完善。
- 本学期的网络程序设计课程中,另外三个实验WebSocket、RPC、内核网络协议也十分有趣。受最近实验室工作忙碌的影响,未能来得及整理相关内容,后续我会将剩余三个实验整理后也一并发表在我的博客上。
致谢
特别感谢asaskevich在单进程事件总线上的探索和孟宁老师在网络程序设计课上的指导(还欠我一朵小红花哦),两者皆为本文的工作提供了先导的指引。最后,感谢每一位为互联网事业做出贡献的人。