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