淘宝开放平台(open.taobao.com)是阿里系统与外部系统通讯的最重要平台,每天承载百亿级的API调用,百亿级的消息推送,十亿级的数据同步,经历了8年双11成倍流量增长的洗礼。本文将为您揭开淘宝开放平台的高性能API网关、高可靠消息服务、零漏单数据同步的技术内幕。
阿里巴巴内部的数据分布在各个独立的业务系统中,如:商品中心、交易平台、用户中心,各个独立系统间通过HSF(High-speed Service Framework)进行数据交换。如何将这些数据安全可控的开放给外部商家和ISV,共建繁荣电商数据生态,在这个背景下API网关诞生。
API网关采用管道设计模式,处理业务、安全、服务路由和调用等逻辑。为了满足双11高并发请求(近百万的峰值QPS)下的应用场景,网关在架构上做了一些针对性的优化:
元数据读取采用富客户端多级缓存架构,并异步刷新缓存过期数据,该架构能支持千万级QPS请求,并能良好的控制机房网络拥塞。
同步调用受限于线程数量,而线程资源宝贵,在API网关这类高并发应用场景下,一定比例的API超时就会让所有调用的RT升高,异步化的引入彻底的隔离API之间的影响。网关在Servlet线程在进行完API调用前置校验后,使用HSF或HTTP NIO client发起远程服务调用,并结束和回收到该线程。待HSF或者HTTP请求得到响应后,以事件驱动的方式将远程调用响应结果和API请求上下文信息,提交到TOP工作线程池,由TOP工作线程完成后续的数据处理。最后使用Jetty Continuation特性唤起请求将响应结果输出给ISV,实现请求的全异步化处理。线程模型如图所示。
在API调用链路中会依赖对元数据的获取,比如需要获取API的流控信息、字段等级、类目信息、APP的密钥、IP白名单、权限包信息,用户授权信息等等。在双11场景下,元数据获取QPS高达上千万,如何优化元数据获取的性能是API网关的关键点。
千万级QPS全部打到DB是不可取的,尽管DB有做分库分表处理,所以我们在DB前面加了一层分布式缓存;然而千万级QPS需要近百台缓存服务器,为了节约缓存服务器开销以及减少过多的网络请求,我们在分布式缓存前面加了一层LRU规则的本地缓存;为了防止缓存被击穿,我们在本地缓存前面加了一层BloomFilter。一套基于漏斗模型的元数据读取架构产生。缓存控制中心可以动态推送缓存规则,如数据是否进行缓存、缓存时长、本地缓存大小。为了解决缓存数据过期时在极端情况下可能出现的并发请求问题,网关会容忍拿到过期的元数据(多数情况对数据时效性要求不高),并提交异步任务更新数据信息。
在双11高并发的场景下,对商家和ISV的系统同样是一个考验,如何提高ISV请求API的性能,降低请求RT和网络消耗同样是一个重要的事情。在ISV开发的系统中通常存在这样的逻辑单元,需要调用多个API才能完成某项业务,在这种串行调用模式下RT较长同时多次调用发送较多重复的报文导致网络消耗过多,在弱网环境下表现更加明显。
API网关提供批量API调用模式缓解ISV在调用RT过高和网络消耗上的痛点。ISV发起的批量请求会在TOP SDK进行合并,并发送到指定的网关;网关接收到请求后在单线程模式下进行公共逻辑计算,计算通过后将调用安装API维度拆分,并分别发起异步化远程调用,至此该线程结束并被回收;每个子API的远程请求结果返回时会拿到一个线程进行私有逻辑处理,处理结束时会将处理结果缓存并将完成计数器加一;最后完成处理的线程,会将结果进行排序合并和输出。
TOP API网关暴露在互联网环境,日调用量达几百亿。特别是在双11场景中,API调用基数大、调用者众多以及各个API的服务能力不一致,为了保证各个API能够稳定提供服务,不会被暴涨的请求流量击垮,那么多维度流量控制是API网关的一个重要环节。API网关提供一系列通用的流量控制规则,如API每秒流控、API单日调用量控制、APPKEY单日调用量控制等。
在双11场景中,也会有一些特殊的流量控制场景,比如单个API提供的能力有限,例如只能提供20万QPS的能力而实际的调用需求可能会有40万QPS。在这种场景下怎么去做好流量分配,保证核心业务调用不被限流。
TOP API网关提供了流量分组的策略,比如我们可以把20万QPS的能力分为3个组别,并可以动态去配置和调整每个组别的比例,如:分组1占比50%、如分组2占比40%、分组3占比10%。我们将核心重要的调用放到分组1,将实时性要求高的调用放到分组2,将一些实时性要求不高的调用放到分组3。通过该模式我们能够让一些核心或者实时性要求高的调用能够较高概率通过流量限制获取到相应的数据。同时TOP API网关是一个插件化的网关,我们可以编写流控插件并动态部署到网关,在流控插件中我们可以获取到调用上下文信息,通过Groovy脚本或简单表达式编写自定义流控规则,以满足双11场景中丰富的流控场景。
使用集群流控还是单机流控?单机流控的优势是系统开销较小,但是存在如下短板:
集群单机流量分配不均。
单日流控计数器在某台服务器挂掉或者重启时比较难处理。
API QPS限制小于网关集群机器数量时,单机流控无法配置。
基于这些问题,API网关最开始统一使用集群流控方案,但在双11前压测中发现如下一些问题:
单KEY热点问题,当单KEY QPS超过几十万时,单台缓存服务器RT明显增加。
缓存集群QPS达到数百万时,服务器投入较高。
针对第一个问题的解法是,将缓存KEY进行分片可将请求离散多台缓存服务器。针对第二个问题,API网关采取了单机+集群流控相结合的解决方案,对于高QPS API流控采取单机流控方案,服务端使用Google ConcurrentLinkedHashMap缓存计数器,在并发安全的前提下保持了较高的性能,同时能做到LRU策略淘汰过期数据。
有了API网关,服务商可以很方便获取淘系数据,但是如何实时获取数据呢?轮询 !数据的实时性依赖于应用轮询间隔时间,这种模式,API调用效率低且浪费机器资源。基于这样的场景,开放平台推出了消息服务技术,提供一个实时的、可靠的、异步双向数据交换通道,大大提高API调用效率。目前,整个系统日均处理百亿级消息,可支撑百万级瞬时流量,如丝般顺滑。
消息系统从部署上分为三个子系统,路由系统、存储系统以及推送系统。消息数据先存储再推送,保证每条消息至少推送一次。写入与推送分离,发送方不同步等待接收方应答,客户端的任何异常不会影响发送方系统的稳定性。系统模块交互如图所示。
路由系统,各个处理模块管道化,扩展性强。系统监听主站的交易、商品、物流等变更事件,针对不同业务进行消息过滤、鉴权、转换、存储、日志打点等。系统运行过程记录各个消息的处理状况,通过日志采集器输出给JStorm分析集群处理并记录消息轨迹,做到每条消息有迹可循。
存储系统,主要用于削峰填谷,基于BitCask存储结构和内存映射文件,磁盘完全顺序写入,速度极佳。数据读取基于FileRegion零拷贝技术,减少内存拷贝消耗,数据读取速度极快。存储系统部署在多个机房,有一定容灾能力。
推送系统,基于Disputor构建事件驱动模型,使用Netty作为网络层框架,构建海量连接模型,根据连接吞吐量智能控制流量,降低慢连接对系统的压力;使用WebSocket构建 长连接通道,延时更低;使用对象池技术,有效降低系统GC频率;从消息的触发,到拉取,到发送,到确认,整个过程完全异步,性能极佳。
在消息系统中,一般有两种消费模式:服务端推送和客户端拉取。本系统主要面向公网的服务器,采用推送模式,有如下优点 :
实时性高。从消息的产生到推送,总体平均延时100毫秒,最大不超过200毫秒。
服务器压力小。相比于拉取模式,每次推送都有数据,避免空轮询消耗资源。
使用简便。使用拉取模式,客户端需要维护消费队列的位置,以及处理多客户端同时消费的并发问题。而在推送模式中,这些事情全部由服务器完成,客户端仅需要启动SDK监听消息即可,几乎没有使用门槛。
当然,系统也支持客户端拉取,推送系统会将客户端的拉取请求转换为推送请求,直接返回。推送服务器会据此请求推送相应数据到客户端。即拉取异步化,如果客户端没有新产生的数据,不会返回任何数据,减少客户端的网络消耗。
在采用推送模式的分布式消息系统中,最核心的指标之一就是推送延时。各个长连接位于不同的推送机器上,那么当消息产生时,该连接所在的机器如何快速感知这个事件?
在本系统中,所有推送机器彼此连接(如图所示),构成一个通知网,其中任意一台机器感知到消息产生事件后,会迅速通知此消息归属的长连接的推送机器,进而将数据快速推送给客户端。而路由系统每收到一条消息,都会通知下游推送系统。上下游系统协调一致,确保消息一触即达。
评估消息系统另外一个核心指标是消息丢失问题。由于面向广大开发者,因此系统必须兼顾各种各样的网络环境问题,开发者能力问题等。为了保证不丢任何一条消息,针对每条推送的消息,都会开启一个事务,从推送开始,到确认结束,如果超时未确认就会重发这条消息,这就是消息确认。
由于公网环境复杂,消息超时时间注定不能太短,如果是内网环境,5秒足矣,消息事务在内存就能完成。然后在公网环境中,5秒远远不够,因此需要持久化消息事务。在推送量不大的时候,可以使用数据库记录每条消息的发送记录,使用起来也简单方便。但是当每秒推送量在百万级的时候,使用数据库记录的方式就显得捉襟见肘,即便是分库分表也难以承受如此大的流量。
对于消息推送事务数据,有一个明显特征,99%的数据会在几秒内读写各一次,两次操作完成这条数据就失去了意义。在这种场景,使用数据库本身就不合理,就像是在数据库中插入一条几乎不会去读的数据。这样没意义的数据放在数据库中,不仅资源浪费,也造成数据库成为系统瓶颈。
如上图所示,针对这种场景,本系统在存储子系统使用HeapMemory、DirectMemory、FileSystem三级存储结构。为了保护存储系统内存使用情况,HeapMemory存储最近10秒发送记录,其余的数据会异步写入内存映射文件中,并写入磁盘。HeapMemory基于时间维度划分成三个HashMap,随着时钟滴答可无锁切换,DirectMemory基于消息队列和时间维度划分成多个链表,形成链表环,最新数据写入指针头链表,末端指针指向的是已经超时的事务所在链表。这里,基于消息队列维护,可以有效隔离各个队列之间的影响;基于时间分片不仅能控制链表长度,也便于扫描超时的事务。
在这种模式下,95%的消息事务会在HeapMemory内完成,5%的消息会在DirectMemory完成,极少的消息会涉及磁盘读写,绝大部分消息事务均在内存完成,节省大量服务器资源。
我们已经有了API网关以及可靠的消息服务,但是对外提供服务时,用户在订单数据获取中常常因为经验不足和代码缺陷导致延迟和漏单的现象,于是我们对外提供数据同步的服务。
传统的数据同步技术一般是基于数据库的主备复制完成的。在简单的业务场景下这种方法是可行的,并且已经很多数据库都自带了同步工具。 但是在业务复杂度较高或者数据是对外同步的场景下,传统的数据同步工具就很难满足灵活性、安全性的要求了,基于数据的同步技术无法契合复杂的业务场景。
双11场景下,数据同步的流量是平常的数十倍,在峰值期间是百倍,而数据同步机器资源不可能逐年成倍增加。保证数据同步写入的平稳的关键在于流量调控及变更合并。
在数据同步服务中,我们使用了消息 + 对账任务双重保障机制,消息保障数据同步的实时性,对账任务保障数据同步一致性。以订单数据同步为例,订单在创建及变更过程中都会产生该订单的消息,消息中夹带着订单号。接受到该消息后,对短时间内同一订单的消息做合并,数据同步客户端会拿消息中的订单号请求订单详情,然后写入DB。消息处理过程保证了订单在创建或者发生了任意变更之后都能在极短的延迟下更新到用户的DB中。
对账任务调度体系会同步运行。初始化时每个用户都会生成一个或同步任务,每个任务具有自己的唯一ID。数据同步客户端存活时每30秒发出一次心跳数据,针对同一分组任务的机器的心跳信息将会进行汇总排序,排序结果一般使用IP顺序。每台客户端在获取需执行的同步任务列表时,将会根据自身机器在存活机器总和x中的顺序y,取得任务ID % x = y - 1的任务列表作为当前客户端的执行任务。执行同步任务时,会从订单中心取出在过去一段时间内发生过变更的订单列表及变更时间,并与用户DB中的订单进行一一对比,如果发现订单不存在或者与存储的订单变更时间不一致,则对DB中的数据进行更新。
在双11场景下如何保证数据同步的高可用,资源调配是重点。最先面临的问题是,如果每台机器都是幂等的对应全体用户,那么光是这些用户身后的DB连接数消耗就是很大问题;其次,在淘宝的生态下,卖家用户存在热点,一个热点卖家的订单量可能会是一个普通卖家的数万倍,如果用户之间直接共享机器资源,那么大流量用户将会占用几乎全部的机器资源,小流量用户的数据同步实效会受到很大的影响。
为了解决以上问题,我们引入了分组隔离。数据同步机器自身是一个超大集群,在此之上,我们将机器和用户进行了逻辑集群的划分,同一逻辑集群的机器只服务同一个逻辑集群的用户。在划分逻辑集群时,我们将热点用户从用户池中取出,划分到一批热点用户专属集群中。分组隔离解决了DB连接数的问题,在此场景下固定的用户只会有固定的一批机器为他服务,只需要对这批机器分配连接数即可,而另一个好处是,我们可以进行指定逻辑集群的资源倾斜保障大促场景下重点用户的数据同步体验。
数据同步服务大集群的机器来源于三个机房, 在划分逻辑集群时,每个逻辑分组集群都是至少由两个以上机房的机器组成,在单个机房宕机的场景下,逻辑集群还会有存活机器,此时消息和任务都会向存活的机器列表进行重新分配,保证该逻辑集群所服务的用户不受影响。 在机器发生宕机或者单个逻辑集群的压力增大时,调度程序将会检测到这一情况并且对冗余及空闲机器再次进行逻辑集群划分,以保证数据同步的正常运行。在集群压力降低或宕机机器恢复一段时间后,调度程序会自动将二次划分的机器回收,或用于其他压力较大的集群。
订单上存储的数据结构随着业务的发展也在频繁的发生的变化,进行订单数据的同步,需要在上游结构发生变化时,避免对数据同步服务产生影响,同时兼顾用户的读取需求。对此我们设计了应对结构易变数据的大字段存储模型。在订单数据的存储模型中,我们将订单号、卖家昵称、更新时间等需要被当做查询/索引条件的字段抽出独立字段存储,将整个的订单数据结构当成json串存入一个大字段中。
这样的好处是通过大字段存储做到对上游业务的变化无感知,同时,为了在进行增量数据同步时避免对大字段中的订单详情进行对比,在进行数据同步写入的同时将当前数据的hashcode记录存储,这样就将订单数据对比转换成了hashcode与modified时间对比,提高了更新效率。
在双11场景下,数据同步的瓶颈一般不在淘宝内部服务,而在外部用户的DB性能上。数据同步是以消息的方式保证实时性。在处理非创建消息的时候,我们会使用直接update + modified时间判断的更新方式,替换传统的先select进行判断之后再进行update的做法。这一优化降低了90%的DB访问量。
传统写法:
SELECT * FROM jdp_tb_trade WHERE tid = #tid#;
UPDATE jdp_tb_trade SET jdp_response = #jdpResponse#, jdp_modified = now() WHERE tid = #tid#
优化写法:
UPDATE jdp_tb_trade SET jdp_response = #jdpResponse#, jdp_modified = now() WHERE tid = #tid# AND modified
订单数据存在明显的时间段分布不均的现象,在白天订单成交量较高,对DB的访问量增大,此时不适合做频繁的删除。采用逻辑删除的方式批量更新失效数据,在晚上零点后交易低峰的时候再批量对数据错峰删除,可以有效提升数据同步体验。
今日荐文
点击下方图片即可阅读
谷歌新发布的分布式数据库服务,是要打破CAP定理了吗?
QCon北京将邀请来自Google、Facebook、阿里巴巴、腾讯、百度、美团点评、爱奇艺等典型互联网公司的技术专家,分享他们在相关技术领域最新成果。具体详戳 「 阅读原文 」惊喜不停!