WebMonitor 作为一个前端监控系统,服务于众多业务的上报需求,包括:微信小程序、H5、京喜 App和部分 PC 页。作为京喜业务流量最大的服务,WebMonitor 过去在超大流量下的表现并不理想,例如 2020 年年初疫情抢口罩时,几乎是每天的抢购高峰就会有分钟级的服务不可用,后果就是红绿灯面板的一半红灯都亮起来了?。因此 2020 年彻底优化了 WebMonitor 的采集端性能,此文借以回顾过去两年内对于 WebMonitor 采集端的优化升级之路

WebMonitor基本架构
上图是目前的 WebMonitor 整体架构,称之为WebMonitor Stack。整体架构是三层,分别是采集端、管理端和数据端。三层结构通过 WebMonitor 的数据流结合在一起,下面简单介绍以下各层的主要职责:
采集端面向的是 Nginx,即直面用户设备上的前端上报请求,采集端的整体架构是两级 Flume Agent 组成的。
ES ),通过搜索引擎查询数据。相信很多前端同学都体验过 WebMonitor 的 Old School 式的管理端。管理端主要的功能包括以下内容:
数据端的数据承载形式在过去的若干年进行了多次优化升级,毕竟在现有的上报量级下(日均4TB+),当传统的DB已经不能承载这种数据量级时,需要提供一些高效的查询手段。
休克式 采样,仅保留不足1%的原始日志,通过 rsync 汇总至一台机器进行查询。无索引、无并行。HDFS ,使用大数据查询引擎 Impala 进行并行查找。无索引、并行。ES,利用搜索引擎进行查找。有索引、并行。数据端的优化往往最能改变用户的体验感受,查询的性能终于有着火箭般的提升:

WebMonitor采集端旧流程
上图是 WebMonitor 采集端旧流程的示意图。采集端的核心流程包括:
这四个流程依次顺序的同步执行,完成数据存储后将 Response 返回给前端。但是由于上述流程中涉及到 IO 操作,在海量请求下,哪怕是仅仅保留较小比例的数据,IO压力也会非常大,会直接拖累采集端的性能。因此针对上述情况,WebMonitor 采集端做了一些调整,结果如下图所示:

WebMonitor采集端新流程
新的流程通过以下手段规避了旧流程的问题:
异步化。通过上述的优化过程,大量减轻了采集端接口的 IO 压力,使得接口性能有了显著的提升,如下表所示。
流程类型 | Avg | TP99 | MAX |
|---|---|---|---|
旧流程 | 2.1ms | 300ms | 2000ms |
新流程 | 0.7ms | 200ms | 2000ms |
量变引发质变,谁能想到简单的监控上报也会成为业务的性能瓶颈?
在海量上报请求(日常 300W QPM)的前提下,UMP上报存在一些问题
由于存在上述的问题,那么过去直接将上报能力放入采集端接口处可能引发性能问题。因此我们曾经进行了一次失败的尝试,其修改如下图所示:

WebMonitor上报异步化流程
我将监控上报的能力从采集端的两级 Flume Agent 中完全拆解,通过消费下游的 Kafka 消息,解析后进行上报,这个其实是很“很常规”的异步操作,看似完美的解决了上报的性能问题,但是完全异步化又会引入新的问题?
曾经出现过一次故障,因为 Kafka 的部分节点故障,导致 Kafka 短时间内不可用,然后红绿灯面板的一半红灯就亮起来了......
痛定思痛,我将采集端+上报的结构再次进行了调整,结构如下图所示:

WebMonitor上报优化流程
同样是异步上报,和之前的架构相比,我做了以下调整:
Jetty 是一个轻量的 Java Servlet 容器,当然在 WebMoniter 中更多的是作为一个普通的 Http Server。但是 Jetty 本身是非常“可配置的”,所以 Jetty 的各种默认配置并不适合所有的情况,特别是 WebMonitor 这样一个非典型的Http服务。

Jetty结构简图
上图是 Jetty 的一个结构简图,也是比较典型的 Reactor 模式,较少的链接线程处理链接,大量的工作线程执行业务逻辑,例如调用服务、读写 DB 、存取缓存等等。其实大部分的 Http 服务的业务逻辑也是如此,但是 WebMonitor 作为一个非典型的Http服务,是一个零外部服务调用,零外部 IO 调用的 Http 服务,所以默认的 Http 服务器的设置并不能完美的适配 WebMonitor 场景。
默认的 Jetty 线程配置:
public QueuedThreadPool () {
this(200);
}
public QueuedThreadPool (@Name("maxThread") int maxThreads) {
this(maxThreads, Math.min(8, maxThreads));
}简单解读一下上述代码:
其实对于大部分“典型”的 Http 服务而言,确实是比较合理的,毕竟动辄调用一个外部服务10+ms,写一次 DB 10+ms,那么大量的线程能够明显改善性能问题。但是 WebMonitor 它不是个“典型的” Http 服务呀?
默认线程模型的不足:
针对 WebMonitor 的业务特点,我做了一些关于线程的调整:

Jetty请求响应示意图
Timeout 是一个非常关键设置,还是那句话“量变引发质变”,当请求量过大时,连 TCP 的连接可能都值得优化。设置 Timeout 是一个妥协的过程,没有完全合适的设置,只有妥协上下游的设置。我在设置 Timeout 的过程中走了很多弯路:
前面提到,Timeout 的设置是一个妥协的过程,上面三种 Timeout 的设置都是有优势、有劣势,我也进行了多次的尝试,最终,选择了 Long Timeout 的方案,原因是当自身服务的可用性极高时,Long Timeout 更具有性能的优势。
意料之外的优化源自于下面一行代码:
int cores = Runtime.getRuntime().availableProcessors();上面这行代码是 Java 多线程应用中最普遍使用的一句,其作用是获取系统可用的处理器核心数,因为通常来说不论是数据密集型任务还是计算密集型任务,其线程数的设置都需要充分考虑主机的理论最佳并行度,但是成也萧何、败也萧何,这条关键的语句居然在 Docker 容器使用中翻车了?。

JDK1.7

JDK1.8.0_91

JDK1.8.0_201
上面三幅图分别代表了在 Docker 容器中,使用 JDK1.7,JDK1.8.0_91 和 JDK1.8.0_201 三个 JDK 的版本,执行上述语句的结果。该容器是 8 核心 16G 内存,可以看到 JDK1.7 和 JDK1.8.0_91居然识别的是容器所在宿主机的核心数,这就会对各种框架对于最佳并行度的设置带来非常大的影响。
针对计算密集型应用:
流程类型 | 单机QPM | Avg | TP99 | MAX |
|---|---|---|---|---|
旧流程 | 30k | 2.1ms | 300ms | 2000ms |
新流程 | 41k | 0.7ms | 200ms | 2000ms |
性能优化后 | 81k | 0.8ms | 3ms | 25ms |