声明:本文为《从程序员》7月期原创投稿文章,未经许可禁止任何形式的转载。
作者:王晓宇,小米网平台研发部软件研发工程师。2015年入职小米,主要负责电商后端仓储物流相关的业务系统开发。曾在西门子中国研究院,从事软件研发工作,拥有两年以上的软件开发相关经验。曾使用过的编程语言主要有Java与PHP,拥有多年的服务器开发经验以及MySQL优化经验,对电商相关业务与系统架构具有一定的了解以及自己的见解。
责编:钱曙光,关注架构和算法领域,寻求报道或者投稿请发邮件qianshg@csdn.net,另有「CSDN 高级架构师群」,内有诸多知名互联网公司的大牛架构师,欢迎架构师加微信qshuguang2008申请入群,备注姓名+公司+职位。
摘要:本文首先介绍小米网系统架构的发展变化,然后介绍Notify系统的设计,最后介绍Notify系统的演化与升级变迁。希望能给各位的工作带来一些启发与指导。
为了适应业务的高速发展,小米网的系统架构经历了很多次变更。在此过程中,为了给各个子系统解耦合,同时保证最终一致性原则的实现,我们建立了自己的异步消息系统——
Notify异步消息系统。
小米网的发展大致可以分为三个阶段:初创阶段、发展阶段、完善阶段。
当小米推出自己的第一部手机时,为了减少渠道成本,我们开始推行电商直销的商业模式,与此同时,开始建设小米电商网站。最开始,小米的业务特点是:
后两点是在最初设计系统时完全没有想到,因为当前我们并没有预料到小米手机会如此受欢迎和供不应求。
在这个阶段,快速上线是第一目标,因为团队需要快速配合公司的手机销售计划。所以一开始小米网的架构设计比较简单,并没有考虑高并发和大数据的情况。当时系统从立项到上线仅两个多月时间,并且只由三名工程师开发完成。
从图中可以看出,系统架构只有两个Web服务器与一个DB服务器,两台Web服务器互为主备,所有的业务功能集成在一个系统中。当时的架构设计仅能支持简单的电商功能,我们预测每年的手机销量能到30万就已经很好了。但是计划永远赶不上变化,很快小米电商就遇到了第一个大问题:系统耦合度很高,导致当抢购活动开始时,其他业务都会受到影响。
为了解决上面的问题,需要对小米网的架构进行修改,把各种业务系统拆成独立的子系统。这一时期小米电商的系统架构发展的特点是:
这一阶段小米网的架构如下图所示:
从图2可以看出,前端与后端系统完全独立出来,当前端进行抢购活动时,后端的客服、售后、物流三大服务系统不会受到任何影响。图2中所列举的子系统只是小米网中的一小部分较为主要的系统,还有很多业务没有列举出来。
这种系统架构可以确保系统之间不会受到影响,但是接口的稳定性就成了一个至关重要的问题。这种系统架构几乎所有的接口都是同步接口,意思就是说一个业务调用这些接口如果不成功,业务也无法成功。如果出现网络问题或者接口BUG,就会导致大量业务失败。但事实上,并非所有的接口都必须做成同步性的,有些业务比如订单系统把订单信息发给仓储系统生产,就对同步性的要求不是很高,可以考虑使用异步方式来解决。为了解决这个问题,小米网架构下一步的演化就是建立一个异步消息队列系统。
经过异步消息系统的引入,小米网的系统架构最终发生了变化,如图3所示:
从图3可以看出,异步消息队列成了一个中心节点,几乎所有的子系统都与它有单向或者双向的交互。当一个业务需要调用一个接口时,如果他对实时性的要求不是特别高,就可以把消息发到我们的异步消息队列系统,然后他就可以完成这项业务,而之后的消息投递过程就完全交给消息队列系统来实现。经过这次调整之后,小米网基本实现了主要业务的异步化,大大增加了系统的容错性,因为所有投递不成功的消息都可以保存在消息队列系统中等待下次投递。
基于对上面业务变化的分析,小米网内部开始计划建立自己的异步消息系统。我们对比了市面的几款MQ软件,最后决定以Redis队列为基础,开发自己的异步消息队列系统,取名叫做Notify异步消息系统。
Notify消息系统的设计需要解决以下几个问题:
我们采用接口的方式来接收业务系统的消息,采用MySQL来存储消息,在消息发送时使用Redis队列来存储。
为了实现以上主要功能,为Notify系统设计了以下的数据结构。下面为五个最主要的数据表,以及重要的字段:
biz - 业务(生产者);
receive - 接收者(消费者);
biz_receive - 订阅关系;
状态字段,表示订阅关系的运行状态,分为正常、暂停(接收消息,但不发送)、废弃(不接收消息)三种。
接口地址字段。
biz_msg - 业务消息;
消息体字段。
receive_msg - 投递消息;
发送状态字段,分为四种状态,待处理、待投递、已投递、丢弃。
发送次数字段。
这里需要提一下Notify系统的消息分裂机制。考虑到有可能在一项业务执行过程中需要把消息发给多个接口,Notify消息在设计的时候引入了一个消息分裂的概念。
图4分别列举了消息分裂的三种情况。首先第一种,在没有消息队列时直接调用接口的情况,一个业务执行时如果要将一个消息传递给不同的系统时,就需要调用不同的接口,并且这些接口还必须都返回成功,才能算这个业务执行成功。第二种情况是引入消息队列来处理这个问题的话,如是不进行分裂处理的话,S1需要把同一个消息塞到不同的消息队列里去。也就是要多次将消息发送给Notify系统。这样设计虽然可以确保业务执行成功,但是却不具备扩展性。假设我们新建立一个系统,也需要同样的消息,那么就不得不回过头来修改代码,关闭一个系统也是同理。所以第三种情况中我们设立了一个消息分裂与订阅的机制,业务执行时只需要把消息投递到Notify系统一次,而其他系统如果需要这个消息,就可以在我们的Notify系统中设置订阅关系,同样的消息就被复制成多个副本,然后被塞到多个不同的消息队列来投递。这样做既可以进一步提高业务执行的成功性,又使得业务具备可扩展性与可配置性。
基于上面的一系列设计思想,最终形成了图5中的结构设计图,即Notify系统的最初设计图。我们通过Api.notify接口,来收集业务系统的消息,并存放在DB中。发送消息时,我们设立一个Maker组件,这个组件采用多进程执行方式,对每一个订阅关系开启一个进程,把消息复制一个副本并放到对应的RedisMQ中。MQ的名称就以biz-receive的对应id组合而成,方便查找。然后,我们设立了一个Sender组件,Sender主要完成两样工作:一是把消息发送给对应的业务系统;二是把消息放到Marker Queue中,来回写消息的状态。如果消息发送成功了,就把消息的状态回写成已投递,如果发送失败,就把消息状态重新回写成待处理,以便下一个周期再次投递。然后我们又设立了一个Marker程序,来异步的读取Marker Queue里的消息来回写状态。这样就完成了一个投递周期,整个Notify系统就是通过这种方式源源不断的将消息投递给各业务系统。
除了上面的主要结构外,在实现Notify时,还引入了以下几个特性:
消息分裂,如上文介绍过的一样。
冷库备份功能。随着业务的扩展,DB中的消息数也增长的很快,如果不对DB中的消息做备份,会影响Notify本身的性能,以及统计功能的可用性。对于已经投递成功的消息来说,大部分情况下不会被用到,所以需要定期对消息做迁移冷库的操作。
为了保证发送消息的时效性,对Maker与Sender进行了多进程编程,每个进程负责一个订阅关系的处理,可以独占一个MQ的控制权,我们通过这种方式来提高消息发送的时效性,确保关键业务消息不会被阻塞。
消息重发功能。如果消息发送失败,会被Marker重新标记成待处理状态,以进入下一次的投递周期,同时消息的发次数会加1。每次投递都间隔一定的时间,当投递次数超过一个阈值时,就不再投递了。因为这个时候可能是由于业务系统的接口出了什么问题,再尝试投递没有任何意义,还会造成网络流量的浪费,影响其他系统的业务,所以停止继续投递。待接口问题修复后,我们再手动批量重推消息。
采用异步方式,在投递周期中,也用到了异步思想,这样做也是为了加快消息投递的时效。
消息可查询。Notify为小米网的其他工程师提供了一个可以查询消息体及返回的地方,方便我们工程师调试系统,定位bug。
第一版Notify消息系统设计的时候还存在着很多不足与考虑不周之处。随着业务的发展,逐渐暴露出很多问题。小米网每年有两大促销活动,一个是天猫的双十一活动,另外一个是小米自己的米粉节。这两个节日,小米网的订单量是呈爆发式的增长,而且每年的订单量峰值都会有所增加。这对业务系统,尤其是作为中心节点的Notify系统来说,是一次巨大的冲击。所以Notify消息系统在最初设计的基础经过了很多修改,并且于去年完成了一次大的重构,才达到了现在的处理能力,以目前的性能来看,小米网可以轻松经受住米粉节或双十一的订单量。
目前Notify系统主要的不足有以下几点:
针对上面的这些不足,我们对Notify进行了一些重构。首先,对于接收消息的功能,我们改为采用Agent代理的方式,来收集消息。如图6所示:
业务系统由原来的直接将消息发送给Notify,改为将消息存在本地数据库,然后由常驻内存的Agent代理来收集消息,并将消息发送给Notify系统。如此以来,大大增加了业务的成功率,因为DB操作的可靠性远大于网络操作。
对于PHP问题,我们则是采用Golang将关键的模块进行了重构。经过这次重构,主要模块的性能都有了很大的提升。与老版本对比,Api.Notify系统每台Web服务器接收消息的能力提升了21倍,Marker服务处理能力提升了4倍,Sender服务处理能力提升了4倍。最终投递周期中的各环节性能达到了图7中所示:
针对单一MySQL实例问题,我们引入了MyCAT,MyCAT是阿里开发并维护的一款开源数据库中间件,实现了MySQL的分布式存储,提升了数据库性能,并且MySQL实例数量动态可扩展,最重要的是MyCAT可运行MySQL语句,因此与MySQL几乎无缝切换,开发成本小。
本文主要介绍了小米网在实现异步消息队列系统时所进行的实践与探索,介绍了异步消息系统的设计经验,为其他公司的实践提供保贵经验。