日均5000万订单点餐系统中间件选型和关键技术实现方案
本系统假定的应用场景为:面向中等以上规模餐厅提供二维码扫码点餐服务,客户可以通过微信或支付宝扫码点单。系统规模为:
1) 商家数量:
10万家活跃商家。
2) 菜单条目总数
1000万条。
3) 日均订单量
5000万单(10万店 x 500单/店)
平均每个订单的明细条目数6条左右。如果每条订单明细用一条独立的数据库记录保存,每天将生成3亿条订单明细记录。
和一般的B2C、C2C电商系统不同,基于餐饮行业的经营规律,点餐系统有一些自己特有的特点。深入分析这些特点,对系统的实现方案设计和优化至关重要。
1) 客户的点餐流量主要集中在午餐(11:30~1:30)和晚餐(5:30~7:30)总共4个小时左右的时间段。这两个高峰时间段之外几乎没有什么点餐流量。
2) 流量平稳,可预见性强,不会出现特定时间点(双11,双12)流量暴增的现象。
1) 每个商家的商品SKU数量不多(一般不超过百数量级),商品信息更新频度不高。
2) 订单的时效性比较强,订单一旦结束(用户完成消费和付款行为),不会发生退货、换货等后期变更行为,因此不会对订单进行任何写操作。
3) 一个订单对应的明细条目比较多,四人一桌的点菜数量,包括主食,饮料会在8~10个条目左右。
1) 一次成功的点餐下单操作对应的菜单浏览操作次数比较多。
2) 客户对历史订单的查询需求不强,不需要支持复杂条件查询。基本上够列举“按照时间排序的我曾经在这个商家点过的菜”列表就足够。
3) 商家需要查看经营情况分析数据,该行为发生频度不高,数据的实时性和及时性要求不太高。
注:下文中搭建的原型系统使用的硬件环境为:
阿里云ECS服务器,4核2.6GHz主频CPU, 16G内存, 千兆网卡,500GSSD硬盘
[难点]
5000万的日订单量,每个订单对应多次菜单浏览操作,获取菜单接口访问量很大,给系统带来一定的压力。缓存机制的优化是解决这一问题的关键。
[解决方案]
>采用Redis3.0分片集群作为缓存系统。
>一个商家的所有菜单条目组成一个数据包,一次性返回给客户端,客户端在本地缓存菜单条目信息。在一个完整的点菜过程中,客户端只需要通过Ajax调用从服务器端获取一次菜单条目信息。
>菜单条目信息采用lz4压缩算法压缩后再保存到Redis服务器。
[中间件选型,原型系统及其性能评测]
根据估算,大多数餐馆的菜单信息不会超过50KB。我们使用50KB大小的数据作为原型系统性能测试的菜单模拟数据。
为了评估优化后的性能提升幅度,我们先搭建一个没有进行优化的基准环境,配置如下:
1台ECS服务器服务器用作Redis服务器
1台ECS服务器用作Redis客户端
先后运行编写的两个测试程序:
1)数据上传工具:将10万条50K大小的模拟菜单数据上传到Redis服务器。
2)数据读取测试程序:创建20个线程,每个线程循环5千次,共发出10万次读取模拟菜单数据包的请求。
测试结果:
测试用时34.715秒
计算得到的TPS为:2.88K
(100K /34.715 = 2.88K)
2.88K的TPS,折算到4个小时能够响应的调用次数为:4147.2万。这个性能无法支撑每日5000万订单量。
经过简单的分析和计算,就能够得知瓶颈在Redis服务器的网络流量上。因为千兆网卡的极限速率会在100MB左右(理论上的极限速率:1000 bit/ 8 = 125M Byte,实际能够达到100MB已经很不错了)。如果一个包的大小是50K,Redis服务器每秒能响应的包的数量(TPS)的最大值为:100M / 50K = 2K。
我们可以通过观察Redis服务器的CPU状态验证这一点:
CPU占用:
4核CPU如果全部跑满,%CPU能够达到400%,目前只达到20%。
根据上面的分析和测试结果,在公有云服务器使用千兆网卡的情况下,即使升级更高配置的服务器也无法提高缓存服务的性能。
优化方案:数据压缩保存 + Redis分片集群
我们先看看单独采用数据压缩的优化效果。测试方法为:
先后运行编写的两个测试程序:
1)数据上传工具:将10万条50K大小的模拟菜单数据经过lz4算法压缩后,(压缩后的尺寸为9~10K左右)上传到Redis服务器。
2)数据读取测试程序:创建20个线程,每个线程循环5千次,得到压缩数据包后,将数据解压,共发出10万次读取模拟菜单数据包的请求。
测试结果:
测试用时6.969秒
计算得到的TPS为:1.43万,比基准环境的性能提高了5倍左右。
(100K /6.969 = 14.349K)
接下来要考虑的问题是:性能还能再提高吗?如何继续提高性能?
使用Redis的分片集群功能能够进一步提高缓存系统性能。
从Redis3.0版本开始,已经支持分片集群功能,能够自动将数据均匀分布在各个服务器上。多个客户端和组成集群的各服务器之间通过P2P的方式通信,不存在中心汇聚和转发节点,可以有效解决网络带宽瓶颈问题。
下面,我们按照如下图示搭建一个Redis分片集群测试环境:
使用6台ECS服务器配置成Redis服务器分片集群,5台ECS服务器作为客户端访问Redis集群。
先后运行编写的两个测试程序:
1)数据上传工具:将10万条50K大小的模拟菜单数据经过lz4算法压缩后,(压缩后的大小为9~10K左右)上传到Redis服务器集群。
2)数据读取测试程序:同时启动5个客户端测试程序,在每一台客户端创建20个线程,每个线程循环5万次,得到压缩数据包后,将数据解压。共发出500万次读取模拟菜单数据包的请求。
测试结果:
客户端1测试用时68.152秒
客户端2测试用时68.152秒
客户端3测试用时67.776秒
客户端4测试用时70.712秒
客户端5测试用时69.506秒
按照平均用时68.860秒计算,得到的TPS为:7.26万
(5000K /68.860 = 72.6K)
相对于基准环境,进行了压缩和分片集群优化后的性能提高到了25倍。
(72.6 / 2.88 = 25.208)
一天4小时工作时间段内,能够响应的调用次数为10.45亿次,完全能够满足缓存系统的性能需求:
(72,600 x 3,600 x 4 = 1,045,440,000, 即:10.45亿。)
[难点]
5000万的日订单量,每日的订单明细3亿条左右。每天的订单数据量已经突破了Mysql关系数据库的单表容量极限。
[解决方案]
>采用分布式关系数据库中间件Mycat进行数据分片
>对Mycat源代码进行定制扩展,支持(日期 + 商户ID)组合分片键,支持数据库平滑扩容,提高系统的可扩展性
>订单对应的订单明细条目不用单独使用一张订单明细表保存,在订单表中使用一个字段保存对应的明细信息,订单表不会用于进行面向客户的信息查询。查询功能的实现方案请见“3 海量数据统计分析”和“4 历史订单查询”中的介绍。
>数据冷热分离。订单表中只保存最近7天的订单信息,历史订单每天定时归档到基于Hbase的历史订单数据仓库中保存。(详情请见“4 历史订单查询”)
>采用分时处理机制,分别在午餐和晚餐的两个小时营业高峰结束后,定时启动订单后处理任务,完成以下工作:
1) 对订单信息进行累进式的统计和分析,更新商家的营业报表信息。
2) 将本次营业时间段内完成的订单归档到历史订单数据仓库。
可以使用消息队列,将需要处理的订单发送到消息队列中,在订单后处理阶段,大部分应用服务器从接单状态切换到订单后处理状态,分别从消息队列取出消息,进行订单后处理。
[中间件选型,原型系统及其性能评测]
关于Mycat
在开源的关系数据库分库分表中间件中,Mycat是最佳选择。Mycat的前身是阿里的Cobar,并在其基础上进行了大量优化。关于Mycat和其它分库分表中间件的对比介绍比较多,下面这一篇比较详细:
http://blog.csdn.net/lichangzhen2008/article/details/44708227
分片方案
点餐订单表需要分片保存。
在原型系统中,使用5台ECS服务器作为数据库分片服务器,每台分片服务器上安装一个MySql实例(DataHost),1台服务器作为MyCat服务器。在每个Mysql实例上,创建14个分片数据库(DataNode),其中每天的订单对应两个分片数据库,1周7天对应14个分片数据库。5台分片服务器总共包含70个分片数据库。每个数据库中,包含一个结构完全相同的订单表,每个分片数据库中的订单表最多容纳500万条订单记录。
该分片方案需要支持“按照日期”和“按照范围求模”组合分片:
1) 每周的数据滚动覆盖到周一~周日对应的分片中(需要数据自动清理处理模块配合)
2) 随着务量的扩展,注册的商家增加时,只需要添加分片服务器,就能够平滑扩容,原有的数据不需要进行迁移。每增加一台分片服务器,就能够支持2万个新增的商家。
在Mycat现有的分片规则中,还没有一个分片规则能够满足上面的需求,其中功能最接近的规则是PartitionByRangeMod(范围求模)分片规则,该规则不包含“按照日期分片”功能。
解决的方法是:参考“范围求模”分片规则代码,创建一个新的类PartitionByRangeModDate(范围求模-日期组合)分片规则。采用ID_DATE组合字段作为分片键。该字段由SHOP_ID和订单日期组合而成,的格式类似于:35003-20161219,其中,350003是商户的ID,20161219是订单的日期(yyyyMMdd格式)。分片算法获取根据分隔符”-“获取商户的ID和订单日期,然后根据前面描述的分片逻辑,计算出分片的序号(0~69)。
例如,参照下文中的分片方案示意图,对于分片键35003-20161219的订单记录,其SHOP_ID在20001~40000之间,应该落在DH2数据库分片服务器上,20161219是周一,数据应该落在DN15或DN16数据库分片上,其中,DN15负责的SHOP_ID范围为(20001~30000),DN16负责的SHOP_ID范围为(30001~40000),因此最终确定该记录落在DN16上,分片函数返回15。
性能测试结果
使用Mycat的性能测试工具模拟插入40万条订单记录,测试得到的数据插入TPS为:33,450 TPS
按照这个性能,一天4小时营业时间能够保存的订单数量为:
481,680,000,即4.8亿
(33,450 x 3,600 x 4 = 481,680,000)
分片方案的示意图和配置信息请见下文:
其中,DH代表DataHost, DN代表DataNode。
Mycat配置文件
1)schema.xml
2)Rule.xml
3)partition-range-mod-date.txt
数据插入性能测试Mycat测试脚本
mycat-create.sql
[难点]
商家需要查看经营情况报表。在海量数据的情况下,根据历史订单实时生成报表所需信息几乎是不可能完成的任务。
[解决方案]
>采用分时处理机制,分别在午餐和晚餐的两个小时营业高峰结束后,定时启动订单后处理任务,对订单信息进行累进式的统计和分析,更新商家的营业报表信息。
[难点]
对于三年以内的订单,需要支持客户查询历史订单详情,如:客户在一家餐馆点餐时,可以浏览“我过去在这家餐馆点过的菜”。在数百亿条历史订单中需要快速检索出某个客户在某个商家下过的历史订单。
[解决方案]
>采用Hbase保存历史订单。简单的查询功能,即使是海量数据Hbase也能够保证足够快的查询速度。
[中间件选型,原型系统及其性能评测]
原型系统使用3台ECS服务器作为Hbase的DataNode服务器(同时作为Hbase运行所需的Zookeeper服务器),1台ECS服务器作为NameNode服务器,1台ECS服务器作为测试客户端。
先后运行编写的两个测试程序:
1)模拟订单数据生成工具:批量写入1亿条模拟订单,模拟的订单数据确保平均每个客户在每个商家有10个左右的历史订单,历史订单的时间跨度为1年,日期随机。采用SHOP_ID+CUSTOMER_ID+ORDER_TIME作为行键。(+表示字符串拼接)
2)数据读取测试程序:该测试程序写成Jmeter的Java测试模块插件,配合Jmeter以命令行方式运行。创建40个线程,每个线程循环5万次,共发出200万次查询历史订单调用,每次调用的客户ID和商家ID的组合为随机组合。
测试结果:
查询操作的QPS为:600左右
平均响应时间:22毫秒
查询历史订单操作是一个低频操作,600QPS, 22毫秒的平均响应速度,能够满足应用性能需求。