摘要
本文详细介绍了阿里数据服务架构的演进历程,从DWSOA到OneService的四个阶段,每个阶段都有其独特特点及面临的问题。同时,文章还探讨了技术架构、数据服务最佳实践等多方面内容,包括性能优化、稳定性保障、资源分配、缓存优化等关键要素,为数据服务架构的发展提供了全面的参考。
1. 数据服务架构演进
阿里数据服务架构演进过程如图6.1示。基于性能、扩展性和稳定性等方面的要求,我们不断升级数据服务的架构,依次经历了内部代号为DWSOA、OpenAPI、SmartDQ和OneService的四个阶段,下面将详细介绍各个阶段的特点及问题。
1.1. DWSOA架构
DWSOA是数据服务的第一个阶段,也就是将业务方对数据的需求通过SOA服务的方式暴露出去。由需求驱动,一个需求开发一个或者几个接口,编写接口文档,开放给业务方调用。
这种架构实现起来比较简单,但是其缺陷也是特别明显的。一方面,接口粒度比较粗,灵活性不高,扩展性差,复用率低。随着业务方对数据服务的需求增加,接口的数量也会很快从一位数增加到两位数,从两位数增加到三位数,其维护成本可想而知。另一方面,开发效率不高,无法快速响应业务。一个接口从需求开发、测试到最终的上线,整个流程走完至少需要1天的时间,即使有时候仅仅是增加一、两个返回字段,也要走一整套流程,所以开发效率比较低,投人的人力成本较高。
1.2. OpenAPI架构
DWSOA阶段存在的明显问题,就是烟囱式开发,导致接口众多不好维护,因此需要想办法降低接口的数量。当时我们对这些需求做了调研分析,发现实现逻辑基本上就是从DB取数,然后封装结果暴露服务,并且很多接口其实是可以合并的。
OpenAPI就是数据服务的第二个阶段。具体的做法就是将数据按照其统计粒度进行聚合,同样维度的数据,形成一张逻辑表,采用同样的接口描述。以会员维度为例:把所有以会员为中心的数据做成一张逻辑宽表,只要是查询会员粒度的数据,仅需要调用会员接口即可。通过一段时间的实施,结果表明这种方式有效地收敛了接口数量。
1.3. SmartDQ架构
然而,数据的维度并没有我们想象的那么可控,随着时间的推移,大家对数据的深度使用,分析数据的维度也越来越多,当时OpenAPI生产已有近100个接口;同时也带来大量对象关系映射的维护工作量。于是,在OpenAPI的基础上,再抽象一层,用DSL(DomainSpecificLanguage,领域专用语言)来描述取数需求。新做一套DSL必然有一定的学习成本,因此采用标准的SQL语法,在此基础上做了一些限制和特殊增强,以降低学习成本。同时也封装了标准DataSource,可以使用ORM(ObjectRelationMapping,对象关系映射)框架(目前比较主流的框架有Hibernate、MyBatis等)来解决对象关系映射问题。至此,所有的简单查询服务减少到只有一个接口,这大大降低了数据服务的维护成本。传统的方式查问题需要翻源码,确认逻辑;而SmartDQ只需要检查SQL的工作量,并且可以开放给业务方通过写SQL的方式对外提供服务,由服务提供者自己来维护SQL,也算是服务走向DevOps的一个里程碑吧。逻辑表虽然在OpenAPI阶段就已经存在,但是在SmartDQ阶段讲更合适,因为SmartDQ把逻辑表的作用真正发挥出来了。SQL提供者只需关心逻辑表的结构,不需要关心底层由多少物理表组成,甚至不需要关心这些物理表是HBase还是MySQL的,是单表还是分库分表,因为SmartDQ已经封装了跨异构数据源和分布式查询功能。此外,数据部门字段的变更相对比较频繁,这种底层变更对应用层来说应该算是最糟糕的变更之一了。而逻辑表层的设计很好地规避了这个痛点,只变更逻辑表中物理字段的映射关系,并且即刻生效,对调用方来说完全无感知。
小结:接口易上难下,即使一个接口也会绑定一批人(业务方、接口开发维护人员、调用方)。所以对外提供的数据服务接口一定要尽可能抽象,接口的数量要尽可能收敛,最后在保障服务质量的情况下,尽可能减少维护工作量。现在SmartDQ提供300多个SQL模板,每条SQL承担多个接口的需求,而我们只用1位同学来维护SmartDQ。
1.3.1. 统一的数据服务层
第四个阶段是统一的数据服务层(即OneService)。大家心里可能会有疑问:SQL并不能解决复杂的业务逻辑啊。确实,SmartDQ其实只满足了简单的查询服务需求。我们遇到的场景还有这么几类:个性化的垂直业务场景、实时数据推送服务、定时任务服务。所以OneService主要是提供多种服务类型来满足用户需求,分别是OneService-SmartDQ、OneService-Lego、OneService-iPush、OneService-uTiming。上面提到过,SmartDQ不能满足个性化的取数业务场景,可以使用Lego。Lego采用插件化方式开发服务,一类需求开发一个插件,目前一共生产5个插件。为了避免插件之间相互影响,我们将插件做成微服务,使用Docker做隔离。实时数据服务iPush主要提供WebSocket和longpolling两种方式,其应用场景主要是商家端实时直播。在“双11”当天,商家会迫不及待地去刷新页面,在这种情况下long polling会给服务器带来成倍的压力。而WebSocket方式,可以在这种场景下,有效地缓解服务器的压力,给用户带来最实时的体验。
uTiming主要提供即时任务和定时任务两种模式,其主要应用场景是满足用户运行大数据量任务的需求。在OneService阶段,开始真正走向平台化。我们提供数据服务的核心引擎、开发配置平台以及门户网站。数据生产者将数据入库之后,服务提供者可以根据标准规范快速创建服务、发布服务、监控服务、下线服务,服务调用者可以在门户网站中快速检索服务,申请权限和调用服务。
2. 技术架构
2.1. SmartDQ架构
2.1.1. 元数据模型
SmartDQ的元数据模型,简单来说,就是逻辑表到物理表的映射。自底向上分别是:
- 数据源:SmartDQ支持跨数据源查询,底层支持接入多种数据源,比如MySQL、HBase、OpenSearch等。
- 物理表:物理表是具体某个数据源中的一张表。每张物理表都需要指明主键由哪些列组成,主键确定后即可得知该表的统计粒度。
- 逻辑表:逻辑表可以理解为数据库中的视图,是一张虚拟表,也可以看作是由若干主键相同的物理表构成的大宽表。SmartDQ对用户展现的只是逻辑表,从而屏蔽了底层物理表的存储细节。
- 主题:逻辑表一般会挂载在某个主题下,以便进行管理与查找。
2.1.2. 架构图
- 查询数据库
SmartDQ底层支持多种数据源,数据的来源主要有两种:①实时公共层的计算作业直接将计算结果写入HBase;②通过同步作业将公共层的离线数据同步到对应的查询库。
- 服务层
- 元数据配置。数据发布者需要到元数据中心进行元数据配置,建立好物理表与逻辑表的映射关系,服务层会将元数据加载到本地缓存中,以便进行后续的模型解析。
- 主处理模块。一次查询从开始到结果返回,一般会经过如下几步。
-
- DSL解析:对用户的查询DSL进行语法解析,构建完整的查询树。
- 逻辑Query构建:遍历查询树,通过查找元数据模型,转变为逻辑Query。
- 物理Query构建:通过查找元数据模型中的逻辑表与物理表的映射关系,将逻辑Query转变为物理Query。
- Query拆分:如果该次查询涉及多张物理表,并且在该查询场景下允许拆分,则将Query拆分为多个SubQuery。
- SQL执行:将拆分后的SubQuery组装成SQL语句,交给对应的DB执行。
- 结果合并:将DB执行的返回结果进行合并,返回给调用者。
- ·其他模块。除了一些必要的功能(比如日志记录、权限校验等),服务层中的一些模块是专门用于性能及稳定性方面的优化的,具体介绍请见的内容。
2.2. Ipsuh架构
iPush应用产品是一个面向TT、MetaQ等不同消息源,通过定制过滤规则,向Wb、无线等终端推送消息的中间件平台。iPush核心服务器端基于高性能异步事件驱动模型的网络通信框架Netty4实现,结合使用Guava缓存实现本地注册信息的存储,Filter与Server之间的通信采用Thrift异步调用高效服务实现,消息基于Disruptor高性能的异步处理框架(可以认为是最快的消息框架)的消息队列,在服务器运行中Zookeeper实时监控服务器状态,以及通过Diamond作为统一的控制触发中心。
2.3. Lego架构
Lego被设计成一个面向中度和高度定制化数据查询需求、支持插件机制的服务容器。它本身只提供日志、服务注册、Diamond配置监听鉴权、数据源管理等一系列基础设施,具体的数据服务则由服务插件提供。基于Lego的插件框架可以快速实现个性化需求并发布上线。Lego采用轻量级的Node.JS技术栈实现,适合处理高并发、低延迟的IO密集型场景,目前主要支撑用户识别发码、用户识别、用户画像、人群透视和人群圈选等在线服务。底层根据需求特点分别选用Tair、HBase、ADS存储数据。
2.4. uTiming
uTiming是基于在云端的任务调度应用,提供批量数据处理服务,支撑用户识别、用户画像、人群圈选三类服务的离线计算,以及用户识别、用户画像、人群透视的服务数据预处理、人库。
uTiming-scheduler负责调度执行SQL或特定配置的离线任务,但并不直接对用户暴露任务调度接口。用户使用数据超市工具或LegoAPI建立任务。
3. 数据服务最佳实践
3.1. 性能最优实践
3.1.1. 资源分配
系统的资源是有限的,如果能合理分配资源,使资源利用最大化,那么系统的整体性能就会上一个台阶。下面讲述合理的资源分配是如何提高性能的。
- 剥离计算资源
调用者调用接口获取的数据,有些指标需要多天数据的聚合,比如最近7天访客浏览量、最近365天商品最低价格等;有些指标还包含一些复杂的计算逻辑,比如成交回头率,其定义为在统计时间周期内,有两笔及以上成交父订单的买家数除以有成交父订单的买家数。如此复杂的计算逻辑,如果放在每次调用接口时进行处理,其成本是非常高的。因此剥离复杂的计算统计逻辑,将其全部交由底层的数据公共层进行处理,只保留核心的业务处理逻辑。
- 查询资源分配
查询接口分为两种:Get接口,只返回一条数据;List接口,会返回多条数据。一般来说,Gt查询基本都转换为KV查询,响应时间比较短,或者说查询代价比较小。而Lst查询的响应时间相对较长,且返回记录数比较多,这就增加了序列化以及网络传输的成本,查询代价肯定会更高一些。
假如将Gt、List请求都放在同一个线程池中进行查询,那么查询效率会怎么样?想象一下如图6.11所示的场景,在高速公路上,行车道以及超车道全部都有大卡车在慢速行驶,后面的小轿车只能慢慢等待,并祈祷前方路段能少一些大卡车。这样整个路段的行车速度就降了下来,车流量也会下降许多。同理,虽然Gt请求的真正查询耗时很短,但是会在队列等待上消耗大量的时间,这样整体的QPS会很不理想。
为此,我们设计了两个独立的线程池:Gt线程池和List线程池,分别处理Get请求和List请求,这样就不会因为某些List慢查询,而影响到Gt快查询。系统的QPS比之前提升许多。回到上文的类比中,在高速公路上大卡车只行驶在最右车道上,小轿车行驶在其他车道上,这样整个路段也会畅通许多。
List查询的响应时间相对较长,所以List线程池设置的最大运行任务数就稍微多一些。另外,由于超时的限制,Lst线程池的等待队列不宜过长。具体的参数设置,可以根据压力测试的结果评估出来。后期,也可以根据线上调用日志的统计,比如List请求与Gt请求的比例来进行优化调整。
- 执行计划优化
查询拆分。举个例子,顾客去肯德基点餐,需要一个汉堡、一包薯条,再加一杯饮料。他可以先点个汉堡,拿到后再点包薯条,最后再点杯饮料,是不是很浪费时间?为了节约时间,他可以叫上朋友来帮忙,每个人负责一样,同时去点餐。这样是快了很多,但是需要顾客付出额外的成本。那么现实中应该是怎么样的呢?顾客直接跟服务员说需要这些,服务员可以分工协作,最后统一放在餐盘中,告知顾客可以取餐了。查询接口同样如此,接口暴露给调用者的指标都是逻辑字段,调用者不用关注这些逻辑字段对应的是哪张物理表的哪个物理字段。比如调用者调用了A,B,C三个指标,这些指标分别在三张物理表中,引擎层会将调用者的请求拆分成三个独立的查询,分别去三张物理表中查询,且这些查询是并发执行的。查询结束后,引擎层会将三个查询的结果汇总到一起返回给调用者,这样最大程度地降低了调用者的调用成本,并能保证查询性能(见图6.13)。
查询优化。上文提到Gt请求与List请求分别有独立的线程池进行查询,但是一个请求具体是Gt还是List,则依赖调用者具体调用哪个方法。在很多情况下,调用者调用的方法不一定是最合适的。比如,为了使代码更简洁,所有的调用全用Lst方法,这样就会造成很多本可以快速返回的查询,也在List线程池中进行排队。查询优化,就是分析用户请求中的SQL语句,将符合条件的List查询转换为Get查询,从而提高性能。具体的步骤是:解析SQL语句中的WHERE子句,提取出筛选字段以及筛选条件。
假如筛选字段中包含了该逻辑表的所有主键,且筛选条件都为equal,则说明主键都已经确定为固定值,返回记录数肯定为1条。在这种场景中,List查询就转换为Get查询。
3.1.2. 缓存优化
- 元数据缓存
在接口查询的过程中,查询引擎需要频繁地调用元数据信息。举例来说:
- 查询解析,需要从元数据中得出逻辑表与物理表的映射关系,从而将逻辑Query解析为物理Query。
- SQL安全检查,这里要根据元数据中的逻辑表配置信息来检查调用者的调用参数是否合法。比如LIMIT是否超过上限、必传字段是否遗漏等。
- 字段权限检查,需要通过权限元数据来判断调用者是否有权限进行本次访问。
这些元数据的总量不大,因此在服务启动时就已经将全量数据加载到本地缓存中,以最大程度地减少元数据调用的性能损耗。后台对数据生产者的发布信息进行监听,一旦有新的发布,就重新加载一次元数据。不过,这时候的加载与初始化时不同,是一次增量更新,只会加载刚刚修改的元数据。
- 模型缓存
接口查询的输入其实是DSL,而最终提交给DB执行的是物理SQL。在从DSL到物理SQL的转换过程中,经过了多步解析处理,如图6.14所示是SmartDQ架构图(见图6.7)的一部分,展示了主处理模块的处理步骤。
模型缓存,就是将解析后的模型(包括逻辑模型、物理模型)缓存在本地。下次再遇到相似的SQL时,直接从缓存中得到解析结果,直接省略了图6.14中虚线框中的步骤,因而节省了DSL->SQL的解析时间。具体做法如下:
- 对DSL进行语法、词法分析,并替换WHERE中的常量。比如将where user id=I23替换为where user id=?
- 以替换后的语句做ky,去本地缓存中进行查找。如果命中,则提取出缓存中的模型,直接将SQL提交给DB查询。如果上一步没有命中,则进行正常的解析处理,并缓存解析后的结果。
需要注意的是,由于模型缓存在本地,为了避免占用太多的内存,需要定期将过期的模型淘汰掉。假如元数据有变更,则缓存中的模型有可能已经失效或者是错误的,因此需要全部清理掉。
- 结果缓存
在某些场景下,会对查询结果进行缓存,以提高查询性能。例如:
- 某些查询可能比较复杂,直接查询DB响应时间较长。这时可以将结果进行缓存,下次执行相同的查询时,即可直接从缓存中获取结果,省去了DB查询这一步耗时操作。
- 还有一种场景,比如获取某个卖家所属类目的统计指标,一个类目下可能会有十几万个卖家,这些卖家请求的结果肯定是完全一致的。因此,这时将结果放在缓存中,大部分请求都会直接从缓存中得到结果,缓存命中率会非常高。另外,类目的记录数不会太多,这样不会增加太多的额外开销。
当然,并不是所有场景都适合走缓存。为了保证数据的一致性,使用缓存的流程一般如图6.15所示。
假设有这样的场景:获取某个卖家对应的指标。由于每个卖家只能请求自己的指标,因此就会导致绝大部分请求都需要从DB查询,再写入缓存中。这样不仅使得单次请求的成本会提升,而且缓存的记录数会非常大,利用率也非常低。所以,这种场景其实是不太适合走缓存的,直接走DB查询是比较合适的。
3.1.3. 查询能力
- 合并查询
数据产品的有些场景,虽然表面上看只是展现几个数字而已,但是后台的处理逻辑其实并不简单。举例来说,展现某一日卖家的支付金额,有个日期选择框可以任意选择日期。日期为今天时,展现的是实时数据(从零点截至当前的成交金额);日期为昨天时,展现的就是离线数据(最近1天的成交金额)。其背后的复杂性在于:
- 在数据公共层中,实时数据是在流计算平台Galaxy上进行计算的,结果保存在HBase中;而离线数据的计算和存储都是在MaxCompute中进行的。这就造成了实时数据与离线数据存储在两个数据源中,调用者的查询方式完全不同。
- 离线数据的产出时间,取决于上游任务的执行时间,以及当前平台的资源情况。所以其产出时间是无法估算的,有可能3:00产出,也有可能延迟到6:00。在昨天的离线任务产出之前,其前台展现的数字只能来源于实时数据。
- 出于对性能和成本的考虑,实时作业做了一些折中,去重时,视情况可能使用一些不精准的去重算法,这就导致实时数据的计算结果与离线数据存在一些差异。
综上所述,离线数据最准确,需要优先使用离线数据。如果离线数据还未产出,则改用实时数据。所以在简单的数字背后,需要使用者清晰地了解上述三点。
为了降低这种场景的复杂性,我们设计了一种新的语法REPLACE,如图所示。REPLACE的效果就是用上边SQL的结果,根据replace_key去替换下边SQL的结果。比如上述SQL,上边的查询是取离线数据,下边的查询是取实时数据,那么结果就是优先取离线数据,如果没有再去取实时数据。
调用者使用这样的语法,就可以实现离线数据替换实时数据的功能,不再需要考虑离线数据未产出等问题。
- 推送服务
有些数据产品需要展现实时指标,为了追求数据的实时性,都是轮询请求最新数据。轮询的间隔时间设置很重要,如果设置间隔时间较长,用户体验会不太好;如果设置很短,对服务器的请求压力会非常大,从而影响整体性能。另外,这种轮询请求的方式,其实很大部分时间是在浪费资源,因为有可能后台的数据根本没有更新,而前端却一直在请求。那能不能换种方式呢?监听数据提供者,新数据产生时能够及时知道,并且告知用户,为此“推送”应运而生。推送服务很好地解决了数据更新的实时性问题,同时也减少了对服务器的请求压力。其主要从网络、内存、资源等方面做了如下设计:
- 对消息生产者进行监听。比如监听消息源TT,一天的消息量可能有几百亿,但实际在线用户关心的可能就几亿甚至更少,所以并不是所有的消息都需要关心,做好消息过滤是非常必要的。
- 过滤后的消息量也是可观的,推送服务无法满足高效的响应需求,这就需要考虑将符合条件的消息放置在临时队列中,但对于有锁的队列,存在竞争则意味着性能或多或少会有些下降,所以采用无锁的队列Disruptor来存放消息是最佳的选择。在采用Disruptor的情况下,推送应用也考虑到可以对重要的消息配置单独的队列单线程运转,以提高性能。
- 消息的推送必须基于Socket来实现,Netty在性能表现上比较优秀,采用基于高性能异步事件的网络通信框架Nety是我们的最终选择。不同事件采用不同的监听处理,职责分明也是提高性能的基础。
- 推送应用是典型的O密集型系统,在采用多线程解决性能问题的同时,也带来了上下文切换的损耗。在注册消息向Filter广播时,采用协程方式可以大大减少上下文切换,为性能的提高做出相应的贡献。
- 从业务角度出发,主题也会存在重要级别或者优先级,适当地控制线程数以及流量,为某些重要的业务消息节约服务器资源也是备选方案。
- 缓存的利用在推送应用中多处体现。例如对注册的在线用户信息做本地缓存,可以极大地提高读性能。
- 对突发事件的推送也有针对性地做了很多工作。比如过滤服务器异常重启时,在线用户信息需要重新向该过滤服务器投递,但每条用户信息才几百字节,如果逐条投递,则会造成高流量带宽的浪费,所以批量投递甚至打包投递会大大降低网络开销。
3.2. 稳定性
3.2.1. 发布系统
上文中提到,服务启动时会将元数据全量加载到本地缓存中。数据生产者会对元数据做一些修改,并发布到线上。那么,如何保证用户的变更是安全的,不会导致线上故障呢?如何保证不同用户发布的变更不会相互影响呢?下面将会阐述发布系统在稳定性保障方面的作用。
- 元数据隔离
一般的应用都会有三个环境:日常环境、预发环境和线上环境。日常环境用于线下开发测试。预发环境隔离了外部用户的访问,用于在正式发布前校验即将上线的代码。为了保障系统的稳定性,根据应用环境设计了三套元数据:日常元数据、预发元数据和线上元数据,如图所示。
三套元数据分别对应着三个应用环境,每个环境的应用只会访问对应的元数据。从用户修改元数据到最终正式上线,经过如下以下几步:
- 用户在元数据管理平台上进行操作,修改元数据。此时,DB中的预发元数据发生了变更,但是还没有加载到本地缓存中。用户点击“预发布”,此时预发元数据就会被加载到引擎的本地缓存中,在预发环境中就可以看到用户的最新修改了。此时,可以验证用户的修改是否会影响线上已有的功能。
- 如果验证通过,则表明此次用户的修改是安全的。用户点击“正式发布”,预发元数据会将变更同步到线上元数据,并加载到引擎的本地缓存中。此时,在线上环境中也可以看到用户的变更。至此,发布的整个流程就结束了。
此外,会有一个定时任务,定期将预发元数据同步到日常环境。通过元数据的隔离,使得用户的变更可以在预发环境中进行充分的验证,验证通过后再发布到线上环境中,避免了因用户误操作而导致线上故障,保障了系统的稳定性。
- 隔离发布
发布系统还需要考虑到一点,就是隔离发布,即不同用户的发布不会相互影响。要实现这一点,需要做到:资源划分。为了做到隔离发布,首先需要确定隔离的最小单元。由于调用者的查询请求最终都会转换成对某张逻辑表的查询,因此我们决定将隔离的粒度控制在逻辑表层面上。
资源独占。当用户开始修改的时候,系统会锁定其正在修改的逻辑表及其下挂的物理表等资源,禁止其他用户修改。当用户正式发布变更后,就会释放锁定的资源,这时其他用户才可以对相关元数据进行修改。
增量更新。用户每次只会修改某张逻辑表的对应元数据,因此发布时引擎是不需要重新加载全量元数据的,只需要加载所发布的逻辑表元数据即可。同理,预发元数据与线上元数据之间的数据同步,也仅仅需要同步用户修改的部分。
3.2.2. 隔离
隔离的一个作用是将系统划分为若干个独立模块,当某个模块出现问题时,整体功能仍然能保证可用。隔离的另一个作用是可以对系统资源进行有效的管理,从而提高系统的可用性。
- 机房隔离
将服务器部署在两个机房中,每个机房独立部署一个集群,且机器数量尽量保持均衡,以实现双机房容灾。当一个机房发生故障时,另一个机房中的应用仍然可以对外服务。同时,需要保障内部调用优先,服务调用同机房优先,最大程度地减少双机房部署带来的网络开销。
- 分组隔离
不同调用者的优先级不尽相同,且查询场景也存在一定的差异。所以,可以根据某些条件将调用者进行分层,然后将服务端的机器划分为若干个分组,每个分组都有明确的服务对象和保障等级。即使某个分组出现性能较差的查询,或者有突发大流量涌入,也不会影响其他分组的正常使用。另外,可以动态地调整分组规则,以重新分配每个分组的机器数量,在总体机器数量不变的情况下,实现资源的最大化利用。
3.2.3. 安全限制
对调用者的调用做了诸多安全限制,以防止查询消耗大量的资源,或者返回太多的记录。主要体现在以下几点:
- 最大返回记录数。数据库的查询强制带上LIMT限制,具体的数值以用户配置为准。
- 必传字段。每张逻辑表都会配置主键,并标识哪些字段是调用者必须传入的。这样最终的SQL肯定会带上这些字段的限制条件,防止对表做全表扫描。
- 超时时间。设置合适的超时时间,以使得超时的查询能及时终止并释放资源,保障系统不会被偶发的超时拖垮。
3.2.4. 监控
- 调用日志采集
如果要对调用做监控,首先要保证调用日志的完整性。对于每次调用都进行了采集,采集的信息包括:
- 基础信息,包括调用时间、接口名、方法名、返回记录数等。
- 调用者信息,包括调用者应用名、来源P地址等。
- 调用信息,包括调用指标、查询筛选条件等。
- 性能指标,包括响应时间、是否走缓存等。
- 错误信息,包括出错原因、错误类型、数据源、错误堆栈等。
- 调用监控
有了调用日志,就可以监控系统的健康状况,及时发现问题。监控可以从以下几个方面展开:
性能趋势。总体的QPS趋势图、RT趋势图、响应时长区间分布。
- 分组性能统计、单机QPS统计,以对当前系统容量做评估。
- 零调用统计。找出最近N天无调用的表,进行下线处理,节约成本。
- 慢SQL查找。找出响应时间较长的SQL,及时进行优化。
- 错误排查。当系统的调用错误数突增时,能从错误日志中及时发现出错原因、出错的数据源等。
3.2.5. 限流、降级
系统的总体容量,主要是根据平日的性能监控,以及定期的全链路压测评估得出,但是难免会遇到突发流量涌入的情况。此时,系统需要有合适的方式来应对突增流量,以免系统被压垮。
- 限流
限流有很多种方法,我们采用的是应用内的QPS保护。针对调用者以及数据源等关键角色做了QPS阈值控制。也就是说,如果某个调用者的调用量突增,或者对某个数据源的查询流量突增,超过了预设的QPS阈值,则后续的请求立即失败返回,不再继续处理。通过快速失败,将超出系统处理能力的流量直接过滤掉,保障了系统的可用性。
- 降级
查询引擎底层是支持多种数据源接入的,但是接入的数据源越多,系统就越复杂,出问题的概率也就越大。假设某个数据源突然出现问题,或者某个数据源中的某张表访问超时,那么该如何处理才能保障整体的可用性呢?
理想的做法肯定是将这些数据源、表全部隔离成独立的模块,单个模块的故障不会引起整体不可用。但是,实际中隔离带来的成本也是比较大的,且有可能造成资源的浪费。假如没有隔离措施,所有数据源共享资源,这时候就需要通过降级将故障影响降到最低。降级主要有两种做法:
- 通过限流措施,将QPS置为0,则对应的所有访问全部立即失败,防止了故障的扩散。
- 通过修改元数据,将存在问题的资源置为失效状态,则重新加载元数据后,对应的访问就全部失败了,不会再消耗系统资源。
4. 博文参考
- 《阿里巴巴大数据实战》