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 |