如其他大中型互联网应用一样,微博平台由众多的分布式组件构成,用户通过浏览器或移动客户端的每一个HTTP请求到达应用服务器后,会经过很多个业务系统或系统组件,并留下足迹(footprint)。但是这些分散的数据对于问题排查,或是流程优化都帮助有限。对于这样一种典型的跨进程/跨线程的场景,汇总收集并分析这类日志就显得尤为重要。另一方面,收集每一处足迹(footprint)的性能数据,并根据策略对各子系统做流控或降级也是确保微博平台高可用的重要因素。要能做到追踪每个请求的完整调用链路;收集调用链路上每个服务的性能数据;通过计算性能数据和比对性能指标(SLA)再回馈到控制流程(control flow)中,基于这些目标就诞生了微博的Watchman系统。在业界,Twitter的Zipkin和淘宝的鹰眼系统也是类似的系统。
这样的系统通常有几个设计目标:
- 低侵入性(non-invasivenss):作为非业务组件,应当尽可能少侵入或者不侵入其他业务系统,保持对使用方的透明性,可以大大减少开发人员的负担和接入门槛。
- 灵活的应用策略(application-policy):可以决定所收集数据的范围和粒度。
- 时效性(time-efficient):从数据的收集和产生,到数据计算/处理,再到展现或反馈控制,都要求尽可能得快速。
- 决策支持(decision-support):这些数据数据是否能在决策支持层面发挥作用,特别是从DevOps的角度。
Watchman系统架构图
对于这些设计目标,Watchman系统是怎么样做的呢?
-
既然要追踪调用链路要收集数据,通常的做法就是通过代码埋点来记录日志。这样一方面要求在所有需要收集数据的地方侵入代码进行修改,并且(可能)引入新的依赖。比如淘宝的鹰眼系统在跨进程的远程调用两侧(stub和skeleton)通过埋点记录数据并传递请求上下文(request-context)。
watchman-runtime组件利用字节码增强的方式在载入期织入增强逻辑(load-time weaving),为了跨进程/线程传递请求上下文,对于跨线程watchman-enhance组件通过javaagent的方式在应用启动并载入class时修改了JDK自身的几种线程池(ThreadPool或几类Executor)实现,在客户代码提交(execute或submit)时对传入的runnable/callable对象包上具有追踪能力的实现(proxy-pattern),并且从父线程上去继承或初始化请求上下文(request-context);如下图所示:
而对于跨进程的RPC场景,则动态增强传输层的客户端和服务端的逻辑。微博平台使用的Motan RPC框架有着类似filter-chain的流程,watchman-aspect会插入自己的filter实现;实现的逻辑就是在RPC请求前获取请求方的请求上下文,序列化后装配近请求体中,服务方获取请求后,再从请求体中反序列化请求上下文,同时设置到线程上下文中(ThreadLocal)。如下图所示:
这类增强或修改都在运行期完成,对于开发人员完全透明,对于运维人员也很友好;
普通Java调用的处理方式(埋点/追踪)则是通过AspectJ的静态织入,相信广大读者对AspectJ都不陌生,它提供非常强大的AOP的能力,我们使用AspectJ来定义几类切面,分别针对WeiboAuth、HTTP接口、资源客户端的下行方法等。再利用AspectJ的语法定义各个切点,形如:
@AfterReturning(value="execution(public $type $signature($param))",returning="$return"
之后在目标项目的maven构建过程中依靠ajc进行编译期的织入。之所以选择编译期织入方式是因为我们的业务场景是十分performance-sensitive的。每一个生效的切点也会在运行时向configserver注册SLA数据。(这个后面会讲到)
-
watchman-core组件内置几类策略,分别用来控制收集数据的范围、收集数据的采样率、以及几种控制策略。
每个请求进入Watchman系统的边界后(在这里是微博平台Auth系统),通过这些策略来决定哪些足迹需要记录,比如REST API、RPC调用、存储/缓存的操作等;同时也通过策略决定本次请求是否需要采样,采样率可以动态修改;之后创建请求上下文并向后传递。在每一个控制点,又会根据控制策略来确定对本次请求是否丢弃,或是对整个方法以什么样的方式来gracefully degrade等;
我们先来看下请求上下问(request-context)的简单定义:
RequestContext类关联一个阀门策略接口(ThrottleStrategy)和采样策略接口(SampleStrategy),每个req-ctx实例被构造时会传入两个策略的具体实现。在记录(trace())时,会根据当前采样策略来决定是否采集数据,并且策略可以动态更新,包括本地配置文件的方式,或者同步configserver的方式。从完全关闭、百万分之一到全量采集几个粒度可以选择。
阀门策略,顾名思义,就像一个阀门,用来控制流量的大小,或是开启/关闭。默认是全开的,因为认为业务99.999%是可用的,同时源源不断的性能数据会被收集,在watchman-stream进行汇总计算后会产生与注册在configserver中的SLA数据的比对结果。比如A服务的性能统计结果低于SLA水平,那么就会通知到阀门策略,并通过随机丢弃请求的方式来做流控,当性能结果严重低于SLA时就关闭,达到降级的效果。
互联网运维中对降级还有一个指标是,是否能优雅的降级,也就是不损害用户体验的情况下进行降级。这一点watchman-aspect会根据代码上的注解(annotation)来实现,@Degradable可以标注在方法上,可以指定returnType(required)和returnValue(optional),降级时根据returnType来生成伪造的结果并返回,如果使用方有指定returnValue就直接用后者返回,如果默认提供的returnType不满足需要也可以进行扩展。
-
watchman-aspect组件通过异步日志(async-logger)会在各个节点上输出日志文件,如何将这些分散的日志源源不断的收集汇总并计算?
通过watchman-prism组件(基于Scribe),将日志推送到watchman-stream组件(基于Storm),利用这两个业界成熟的系统以流式的方式处理数据,stream中bolt会根据需求进行聚合、统计等计算(针对性能数据),规范化、排序(针对调用链数据),之后写入HBase中。这个过程通过benchmark反映出的结果来看,完全能达到准实时的要求(30s左右)。 对于日志数据推送:首先应用要以一致的方式输出日志,理所当然就是通过Log框架的logger来输出,每个节点产生日志后需要再依赖scribe推送到日志中心,所以我们实现了自己的AsyncScribeAppender,如下:
由于Scribe是基于Thrift进行通信,所以我们的Appender扩展于Log4j的AppenderSkeleton,以普通Logger的API形式供上层使用(异步),同时再作为一个ThriftClient直接将数据写到节点上的ScribeClient,之后再通过网络把日志推到远程。watchman-prism在这里作为远程接受方,它是扩展于Scribe的一个ThriftServer。 而Storm这一侧,众所周知,spout组件作为数据的入口,分发到各个bolt进行流式计算,spout是拉(pull)的模式,它从watchman-prism中不断取数据,经过简单的过滤后发射(emit)到其他bolt,不同的bolt有着不同的计算任务,之后将不同纬度的计算数据写入HBase中。
-
服务质量保障是Watchman系统的另一特点,在面向服务的架构(SOA)中,各个服务的提供方需要给出SLA(service level agreement)数据,量化服务的各种指标(如吞吐、承载)和服务质量(如99.99% <50ms)。这里的服务包括http形式的REST API,RPC服务,DB或Cache的接口,以及网络IO层面等; 微博平台的各业务方的每一层服务都会在Vintage(微博的类Zookeeper系统)中注册自己的SLA数据,运行时watchman-stream将不断计算得出的性能数据与通过watchman-registry获得的各服务的SLA数据进行比对。结果会反映到Dashboard上,这里与运维的告警系统等集成,可以及时将状况推送出去,除此之外也会更新registry中的指标,ConfigService根据指标的变化判断是否通知各个注册的客户方,也就是watchman-aspect,阀门策略就会根据通知调整阈值进行干预。流程如下:
比如某个服务由于瞬时访问高峰,造成底层资源压力变大从而服务响应时间变长,控制策略可以根据设定随机丢弃后续的请求,如果情况加剧就会自动降级该服务,保证核心服务路径。整个过程可以自动完成也可以人工通过Dashboard控制。
Watchman系统的下一步
之后的迭代会进一步增强watchman-stream的计算/分析能力,争取在更多的维度上挖掘出有价值的数据;同时依靠watchman-prism来汇总更丰富的业务日志,力图在一个请求链路上展现更丰富的上下文相关数据。
总之,为构建更健壮、更可靠的微博平台,Watchman系统会继续演进。