“先卖10000台再说!”2011年8月9日,小米网负责人黎万强在公司内部大会上这样说道。时间回到4年多前,在小米“标配”产品发布日8月16日前几天,小米公司100多号人沉浸在即将到来的第一款产品诞生的亢奋中,但是,没有人能告诉我们:未来,我们将能走多远\做多大。彼时,小米网仅有三位开发工程师,在经过两个多月的紧张开发后,小米网将要第一次面对公众在线销售产品,接受大考。由于工程师资源极度紧张,我们甚至考虑过使用ECSHOP之类的开源系统搭建小米网,不过幸好我们很快放弃了这一想法。因为在3个月之后,我们就发现不得不对系统进行一轮重构了。如果使用了第三方开源系统,为适应原系统架构,我们将长期被迫“迁就”原架构而放弃很多更优的设计,并为学习这个系统而付出时间成本。
第一代的小米网架构非常简单,如图1所示。
图1 第一代小米网架构
我们实现了一个最基本的电商网站基本组件:在线销售系统、订单处理系统、仓储和物流系统,其中物流只对接了兄弟公司凡客的子公司如风达(现已独立)。所有的业务系统共用一个数据库。这样运行了几个月,发现网站访问量越来越大,当有新品销售时,面对突然激增的大量访问,数据库压力陡增,造成后端业务系统几乎无法使用。
2012年上半年,在小米网运行半年多后,我们决定将业务系统进行拆分,首先将销售系统剥离,之后逐步将越多越多的子系统拆分。拆分后各业务系统相对独立,各自使用自己的数据库,这样就完美解决了不同系统抢占数据库资源的问题,也让模块更清晰,程序员也能专注于自己负责的业务系统的开发,如图2所示。
图2 拆分业务系统
这种结构随着小米网子系统的增多,只运行了几个月,我们就发现灾难开始显现了:我们需要维护的接口越来越多。系统间接口调用图变成了图3这样。
图3 系统间接口调用图
这张网越来越复杂,从而使系统越来越难以维护,问题层出不穷。为了让各子系统尽量解耦,我们开发了小米网异步消息服务系统(Notify),让它作为所有子系统异步通信的中间人,所有子系统只需与中间人通信,接口标准化,将网状结构变为星状结构,大大降低了系统间通信成本,提高了开发效率,如图4所示。
图4 小米网异步消息服务系统(Notify)
此时,我们各子系统的网络架构也进行了相应的升级,大体分三层:调度层、业务层、数据库。在调度层,我们主要使用LVS、HAProxy做流量转发和故障转移;业务层则五花八门,不同语言,不同框架百花齐放;数据层主要使用MySQL、NoSQL存储及缓存服务(Redis和Memcache)。
经过以上改造,我们从结构上,让整个流程更清晰了。然而,流量在继续增大,特别是小米网的爆品非常多,由于供应链及硬件产业特性,导致新品上市时供应量无法满足用户需求,用户的热情又远超我们的期望。大量请求导致前端销售系统的数据库开始告急。我们急需采取一种方案,将峰值抹平。我们是一家电商网站,在交易时会有大量在线联机事务处理,对数据一致性要求极高,所以经过讨论,我们决定采用淘宝开源的数据库中间件产品Cobar来实现数据库的水平切分。一共部署了32个实例,按用户ID实现数据的均匀读写,每个实例都做了MM双主高可用实现,如图5所示。
图5 采用淘宝开源的数据库中间件产品Cobar
这种架构保证了我们日常的在线销售稳定运行无压力,但是,每逢重大产品发布时,仍然要面对数百万QPS的抢购并发压力。不光是对数据库,对前端的应用程序服务器一样造成巨大的冲击。非抢购时间和抢购时间的流量差距可达几十倍到上百倍,如果我们按抢购峰值流量部署服务器,那将是一笔巨大的硬件资产浪费。在这个背景下,我们组织专门的人员开发了小米网的大型秒杀系统:BigTap。其实道理很简单,大家都去银行办过业务,在银行窗口排队的多,但是我们几乎很少看到有人在银行的取号机面前排过队。大秒系统其实就是充当银行的取号机的角色。它的业务逻辑非常简单:判定用户是否合法,合法则给这个用户购买资格,用户抢购成功;不合法则拒绝用户请求,抢购失败,如图6所示。
图6 小米网的大型秒杀系统BigTap
由于平时不抢购,大秒系统没有任何流量,所以,我们将大秒系统整体迁移至AWS云上,抢购前一天,将系统扩容,抢购后,将服务器再下架,实现完美伸缩,大大节约了成本。
除了大秒系统以外,我们还额外开发了众多小米网特色服务以支撑自己的业务。其中之一就是基于Redis和Twitter开源的Twemproxy开发的小米通用缓存服务(内部代号MCC),集群中单节点达到14万QPS,支持自动分片,热加载,全Redis协议支持。由于MCC支撑了小米网全业务线的缓存服务,所以我们还将此服务设计成双机房高可用架构,如图7所示。
图7 小米双机房架构(图中M表示主Redis实例,S表示从Redis实例)
常态下,双机房同时工作,读写机房1的主和从实例。机房2的Mi-Twemproxy也读写机房1的主从实例。当机房1故障时,只需修改机房2的Mi-Twemproxy读写机房2的从实例,并将此实例提升为主实例。当机房2出现故障时,不需要做任何改动。不足之处是当机房1出现故障时,机房2短期内只有一个主实例工作,无冗余。
在搭建电商网站中,我们还要时刻考虑的一个业务问题是:如何尽快地将货物售出,实现最快的库存周转,同时还要有好的购物体验,在这个问题上,库存系统的设计是一个很大的挑战。我们尝试考虑过很多电商的做法:按仓库库存卖商品。这种设计的好处是:仓与仓之间不用调拨,省去物流费用。缺点是,可能某个仓库存过高卖不出,某个仓又缺货,导致用户无法下单购买。最终导致库存周转周期太长,降低了整体效率。小米网结合自身实际情况,设计了一套虚拟库存分配系统,将各个仓作为库存渠道,可以自由合并,拆分供给不同的销售渠道,且可以自由调配。这种方法的缺点是订单可能会跨仓发货,增加物流成本,但是优点也显而易见:大大提高了库存周转率,用户也获得了较好的购物体验,在以用户为中心的小米网,这是我们首先会考虑的问题,设计如图8所示。
图8 小米虚拟库存分配系统
跨仓调拨既然无法避免,那我们能做的是尽量减少跨仓调拨频次,在多个仓库之间调拨时尽量合理规划。小米网的跨仓调拨问题实际上是一个多目标线性规划求最优解的问题,我们将各仓当前需求量,未来预测需求量,调拨线路和时间均考虑了进去。
在任何一家互联网公司中,都必须要重视的一件事就是:监控。服务器、应用程序、我们的业务,都存在监控需求。然而当今好的监控方案几乎是空白,每个公司业务不同,特点不同,通用的监控方案全都收效甚微。监控的意义在于出故障时,责任人应该第一时间知道并能采取措施。这要求监控系统做到一是及时,二是准确。当监控对象是一家大型公司时,还要求监控系统做到极其重要的一点是——有效。下面我会分析何为有效监控。小米网一路走过来,做过很多监控,当业务触发异常时就开始通过短信、邮件等告警。就算监控点很少时也不足以构成威胁,一旦触发告警,马上处理。然而,业务量激增后,监控点非常多,责任人往往会在短时间内收到大量重复的告警邮件和短信,时间一长,人都会疲劳,此时极容易忽略重大告警。所以,之前监控系统设计的最大问题在于没有区分异常和告警的关系,没有设定异常的策略和告警的策略。如果一条告警送到了,但是没有引起责任人的重视,那么这就是一条失败的、无效的告警。遇到异常马上触发告警是不科学的设计。我们新设计的监控系统的目的是大大提高有效告警量,其核心思想如下。
异常判断策略:
异常判定函数
val,监控结果取值
val(‘key’),结果若有多个字段,如Server各参数,取指定 key 的值
count,结果若有多条,取结果集条数
exist,监控结果是否存在
empty,监控结果是否为空
异常判定表达式
val>3 ,取值大于3时判定为异常
count>10,结果集条数大于10时判定为异常
exist,结果不为空即判定为异常
empty,结果为空即判定为异常
告警判断策略:
告警判定函数
times,当前连续异常次数 percent(num),最近n次监控结果中异常的百分比
limit(num),告警次数限制,num=0为不限制
snooze(minutes),不限制告警次数时,异常周期内每隔minutes分钟告警一次 告警
判定策表达式
times>3,连续异常3次则告警
percent(10)>0.8,最近10次监控结果中异常数大于80%则告警
times>3 && limit(0) && snooze(30),大于3次异常开始告警,异常周期内,每隔30分钟告警一次
通过这两个策略组合,能够实现最大限度不打扰人的有效告警。
最后,我在此稍提一下小米网的服务化。这是目前我们正在进行的技术架构大升级,采用Thrift+ETCD+Go+PHP实现。小米SOA框架完全自行开发,框架本身由Go语言实现。非Go语言的项目,我们通过两个小插件:服务发现助手和服务注册助手帮助接入到我们的服务平台。关于服务化的意义,网上有非常多的详细的分析,在此不再赘述。对于越来越大的技术架构体系,服务化将是未来的趋势。