高效消息处理系统设计
大部分的后端系统依赖于大量的消息处理实现各种功能服务,架构高效的消息处理系统是非常有必要的。这里笔者简单记录下日常开发常用的高效消息处理系统的设计思路。
设计消息处理系统时,我们要明确三个角色:
- 消息生产者:产生消息的对象(接收到来自网络中的数据包,解析适配成系统内部消息,这种角色称为消息生产者)
- 消息消费者:消费消息的对象,也就是service。接受消息提供服务。
- 消息分发者:第三方存在,通常消息生产者和消息消费者在系统内不互相可见,所以需要第三方来进行消息的分发。这种第三方通常是全局存在的,全局唯一的,消费者和生产者都可以直接访问到其内存。
妥善处理这三个角色的动作是一个高效消息处理系统的基础,根据笔者的经验,将这三者的作用发挥好,设计出来的消息处理系统的性能不会差。
重点是消息的分发者,消息分发者的分发效率决定消息处理系统的性能。系统内存在大量的不同服务,比如登录服务、认证服务、场景服务等等,笔者建议对于每一个服务使用一个单独的消息分发者。这样消息分发者的职责可以不只只是转发消息,还可以对服务提供对象进行管理。消息分发着需要对外开放的接口是Publish。这样对于无状态的服务,可以向管理者注册多个冗余的服务,提高系统的承载能力。
1
2
3
type ServiceManager interface {
func Publish(msg Message) error
}
消息产生者调用Publish方法将消息发布到消息分发者,消息分发者会根据消息的类型将消息分发给对应的服务。服务进行消息处理。
消息消费者如何接受到消息呢?首先消息的执行和分发不能在同一个线程中,这样会导致消息的分发和执行效率低下。所以对于service,笔者的建议是每个service使用一个独立的任务队列,供消息分发者将消息投递到任务队列中。如何实现任务队列?可以查看笔者之前的博客。
看到这里,一个较为成熟的分布式消息处理系统的设计就完成了。service和dispatcher可以独立部署,所以可以是分布式的,也可以同时存在在一个内存中,看读者自己的需求。
这样设计的消息处理系统的好处是:
- 系统各部分的解耦合性高,独立工作,互不影响;
- 节点化,可以弹性伸缩;
提供单线程运行环境的高并发消息处理框架
首先解答第一个问题,为什么消息处理需要单线程环境。单线程环境意味着资源的访问是同步的,在开发上不需要考虑多线程访问资源带来的资源竞争问题,资源的访问不需要使用锁进行同步访问保护。这样做的好处:一是简化开发复杂度,无需考虑并发访问资源带来的数据访问错误的问题;二是减少了因锁竞争带来的性能损耗。游戏服务器对于响应性的要求很高,对于请求的响应不希望会因为锁竞争等待而导致延迟。
但是单线程环境下,如何保证系统的高并发呢?实现消息的并发处理的第一个重要的点是对消息进行分类,消息具备类型,需要访问固定的资源,这样访问相同资源的消息可以放在一个线程中处理。第二个重要的点是消息的分发,消息的处理对象需要具备接收不同线程分发过来的消息的能力,这个能力通过可休眠的单线程任务队列实现。
网络框架并发的处理数据流读取,解析成具备类型信息的消息,根据类型获取消息的分发者,分发者将消息投递当消息需要被处理的任务队列中。这个消息投递的过程其实就是订阅发布,消息处理对象向消息分发者注册自己感兴趣的消息类型,分发者接收到消息将消息发布到指定订阅者。
这样就实现了消息产生和消息处理的解耦,即保证了消息的并发处理能力,又保证了消息处理的同步性。
为什么说这样的消息处理系统是借鉴了Actor的设计模式呢?Actor是单独处理消息的单元,资源由Actor自己持有,不被其他Actor所共享。同时Actor是由消息驱动的,Actor是否启动线程处理消息是根据消息队列内是否具有待处理的消息。是不是和我们的消息处理框架的设计模式非常类似。
这个系统实现的根基是无锁的可休眠的单线程的消息处理队列,这个队列的实现我会单独写一篇博客记录。
为什么选择这样的模型,而不是常见的线程池模型?
传统的线程池只具备处理消息策略,而不持有消息处理所需资源,所以在并发场景下,资源具有竞争关系,可能会导致性能瓶颈。