如何去设计一个消息队列
以下相关业务纯属假设,如有雷同,实属巧合,下面的设计案例仅仅描述一些关键的设计考虑点和决策点
1 需求
假想一个创业公司,叫胖批微博,胖批微博的业务发展很快,系统越来越多,系统之间的协调效率很低,如下:
- 用户发了一条微博后,微博子系统需要通知审核子系统进行审核,然后通知统计子系统进行统计,再通知广告子系统进行广告预测,接着通知消息子系统进行消息推送、一条微博就会有十几个通知,现在都是通用接口直接调用的,每通知一个新系统,就要设计接口,进行测试,效率低,且定位问题麻烦
- 用户等级达到VIP后,等级子系统要通知福利子系统进行奖品发放,通知客服子系统安排专属服务人员
新来的架构师在梳理这些问题时,结合自己的经验敏锐发现了问题在于架构上各业务子系统强耦合,正好消息队列又能完成子系统之间解耦,所以引入消息队列系统来解决这个问题很有必要
其他背景信息
- 中间件团队规模不大,约6人
- 中间件团队熟悉java语言,但有一个新同事C/C++很牛
- 开发平台是linux,数据库是MySQL
- 目前整个业务系统是单机房部署,没有双机房
2 设计流程
2.1 识别复杂度
- 是否需要高性能:假设胖批微博的每天有1000万条微博
- 那么微博子系统一天会产生1000万条数据,其他子系统总共会有1亿次的消息
- 但是这是消息总量,高性能更多的是看秒级,如TPS,QPS。计算下来,一天内的平均每秒写入115,每秒读取消息是1150,再考虑请求数并不会是完全平均,所以设计目标是峰值计算,一般取平均3倍,TPS:345,QPS:3450
- 这个量级不算很高,kafka都是万级,但是也会会增长,流量也会变得更多,为了考虑未来一两年,可以按照4倍峰值计算,最终的TPS:1380,QPS:13800,可以看到每秒1万多次的读取,其实也达到了高性能的要求的,但是使用kafka能应对
- 是否需要高可用:可以假想如果消息丢失了怎么办
- 首先当前流量很大,每天1000万条微博,所以如果对于审核子系统,没有审核到某个微博,可能导致触犯法律,或者社会性危害
- 如果用户达到VIP后,却发现别人有的奖品,自己没有,可能会不满意,且导致用户流失,但是相对来说没有审核那么严重
- 所以综合来看,消息队列需要高可用,包括数据写入、存储、消息读取都要高可用
- 是否需要高扩展性:消息队列使用的场景和明确,就是给各个子系统解耦,所以没有扩展需求,因此并不是复杂度关键
综合下来,消息队列的复杂性主要体现在:高性能消息读取、高可用消息写入、高可用消息存储、高可用消息读取
2.2 设计备选方案
- 备选方案1:采用开源Kafka
- 是一个成熟的开源消息队列方案、功能强大、性能高、且成熟
- 备选方案2:集群+MySQL存储
- 设计要求虽然要求高性能,C++等语言的性能优势是比java大的,但是C++更适合开发高性能中间件,但是为了语言的性能优势而去让团队切换语言是没必要的
- 所以使用java开发消息队列,例如采用Netty这种java中的高性能网络库来开发消息队列系统,但是QPS达到了13800,单个Netty来支撑还是可能存在问题,所以我们决定采用Netty加上集群的方式来分散QPS的压力,负载均衡就采用简单的轮询即可
- 这样高性能读取、高可用写入等都能满足,但是高可用存储和高可用读取确实比较复杂
- 高可用存储要保证:写入的消息在单台服务器宕机时不丢失
- 高可用读取要保证:写入的消息在单台服务器宕机后能继续读取
- 可以使用MySQL的主备方案,主备之间数据的复制能保证读取,主备结构可以保证存储
- 备选方案3:集群+自研存储方案
- 在2的基础上,将MySQL存储换为了自研的存储方案,因为MySQL的关系型数据特点本身就不是很契合消息队列的数据特点
- Kafka就是自己实现一套文件存储和复制方案
可以看出高性能消息读取单机系统设计并没有多少方案可选,很大原因是团队的背景约束,java为熟悉语言,适用于高性能中间件的C/C++的人员又少,虽然上面给出了3个方案,比较少,但是现实情况会更复杂,比如开源的MQ还可以选择ActiveMQ,RocketMQ,RabbitMQ,存储方案可以选HBase,Redis+Mysql等
2.3 评估和选择备选方案
参加备选方案的人组织了架构师、研发、测试、运维、几个核心业务主管,人之常情——踢皮球
- 备选方案1:采用开源的Kafka
- 支持
- Kafka比较成熟,且业务团队已经或多或少了解过Kafka,能节省大量的开发投入
- Kafka比较成熟,无须太多的测试投入
- 反对
- 但Kafka设计目的是支撑大容量的日志消息传输,但是当前要解决的问题是业务数据的可靠传输,并且能够保证高性能
- Kafka由Scala语言编写,运维团队没有维护Scala的经验,很难快速应对问题,且运维有一套运维体系,Kafka很难融入进来
- 支持
- 备选方案2:集群+MySQL存储
- 支持
- 能有效融入当前运维体系,且MySQL存储数据,可靠性有保障
- 反对
- MySQL存储消息数据性能肯定不如文件系统,且会影响团队声誉,毕竟用MySQL做消息队列看起来另类且土
- 运维又认为成本过高,一个数据分组就需要4台机器(2台Server+2台DB)
- 测试人力投入大,各方面都需要进行测试
- 支持
- 备选方案3:集群+自研存储系统
- 支持
- 部分研发人员任务很好,能够展现中间件团队的技术实力,性能相比MySQL要高
- 反对
- 一部分研发认为方案复杂度较高,想要达到稳定可靠需要长时间的迭代
- 运维认为存储系统在没成熟之前,存在各种问题,丢数据等,并且不认为研发能够一上来就解决这种问题
- 测试和运维一样的意见,测试不一定能够测出全部问题
环评表如下
- 支持
质量属性 | 引入Kafka | MySQL存储 | 自研存储 |
---|---|---|---|
性能 | 高 | 中 | 高 |
复杂度 | 低,开箱即用 | 中,MySQL存储和复制已有,只需要开发服务器集群即可 | 高,自研存储系统复杂度更高 |
硬件成本 | 低 | 高,一个分区就需要2S+2DB | 低,和Kafka一样 |
可运维性 | 低,无法融入现有的运维体系 | 高,MySQL运维很成熟,能融入现有体系 | 高,只需维护服务器即可,能融入现有体系 |
可靠性 | 高,成熟的开源方案 | 高,MySQL存储很成熟 | 低,最初时自研的存储系统的可靠性很难保证 |
人力投入 | 低,开箱即用 | 中,只需要开发服务器集群 | 高,需要开发服务器集群和存储系统 |
架构师最终选择了备选方案2
原因
- 排除备选1:运维性低,会导致新项目上线后,无法快速解决问题,也就意味着整个系统出现停滞的时间会很久,以及出了问题之后的数据丢失问题,且需求是一个可靠传输的MQ
- 排除备选3:复杂度过高,人又少,可能导致开发周期过长,且开发过程中有其他的开发任务以及中间件维护工作
- 备选2复杂度不高,可靠性有保障
- 缺点是性能,但是当前的性能要求不高,即使后续业务量提升了,备选2能支持平行扩展支撑来提高性能下限
- 成本过高也是一个缺点,备机本身就是为了应对突发故障和备份,可以和其他业务系统部署在同一个机器上
- 虽然技术上看起来不优越,但是设计的目的首先就是要满足业务需求,并不是证明自己(合适原则)
2.4 细化方案
细化设计1 数据库表如何设计
- 数据库设计两类表,一类是日志表,用于消息写入时快速存储到M小ySQL中;另一类是消息表,每个消息队列一张表。
- 业务系统发布消息时,首先写入日志表,日志表写入成功就代表消息写入成功;后台线程再从日志表中读取消息写入记录,将消息内容写入消息表中。
- 业务系统读取消息时,从消息表中读取。
- 日志表表名为MQ_LOG,包含的字段:日志IID、发布者信息、发布时间、队列名称、消息内容。
- 消息表表名就是队列名称,包含的字段:消息ID(递增生成人消息内容、消息发布时间、消息发布者。
- 日志表需要及时清除已经写入消息表的日志数据,消息表最多保存30天的消息数据。
细化设计2 数据如何复制
- 直接采用MySQL的主从复制,只复制消息存储表
细化设计3 主备如何倒换
- 使用ZK做主备决策:主备都在ZK建立节点,主服务器路径规则(/MQ/server/分区编号/master),备机(/MQ/server/分区编号/slave),备机去挂在监听主机节点,主机断开连接后,备机修改自己的状态,对外提供服务
细化设计4 业务服务器如何写入消息
- 消息队列系统设计两个角色:生产者和消费者,且都有唯一名称
- 消息队列系统通过SDK提供给其他业务系统调用
- SDK从配置中读取消息队列系统的服务器信息,使用轮询算法发起消息写入请求,如果发送后无响应(ack)或返回错误,则SDK再次轮询发送
细化设计5 业务服务器如何读取消息
- 消息队列系统通过SDK提供给其他业务系统调用
- SDK从配置中读取消息队列系统的服务器信息,轮流向所有服务发起读取请求
- 消息队列服务器,记录每个消费者状态,即读到哪条消息——index,当接收到读取请求时,就可以根据index返回下一条消息
细化设计6 业务系统和消息队列之间的通信协议
- 考虑消息队列系统后续可能会对接多种编程语言编写的系统,提高兼容性,就选择了TCP,数据格式为ProtocolBuffer
3 总结
- 识别复杂度对架构师来说是一项挑战,因为原始的需求中并没有哪个地方会明确地说复杂度在哪里,需要架构师在理解需求的基础上进行分析。
- 有经验的架构师可能一看需求就知道复杂度大概在哪里,如果经验不足,则只能采取“排查法”,从不同的角度逐一进行分析。
- 架构师关注的不是一天的数据,而是1秒的数据,即TPS和QPS。
- 备选方案的选择和很多因素相关,并不单单考虑性能高低、技术是否优越这些纯技术因素,业务的需求特点、运维团队的经验、已有的技术体系、团队人员的技术水平都会影响备选方案的选择。
- 架构设计目的不是证明自己(参考架构设计原则1:合适原则),而是更快更好地满足业务需求。