WebMonitor采集端优化之路
一、前言
WebMonitor 作为一个前端监控系统,服务于众多业务的上报需求,包括:微信小程序
、H5
、京喜 App
和部分 PC 页
。作为京喜业务流量最大的服务,WebMonitor 过去在超大流量下的表现并不理想,例如 2020 年年初疫情抢口罩时,几乎是每天的抢购高峰就会有分钟级的服务不可用,后果就是红绿灯面板的一半红灯都亮起来了?。因此 2020 年彻底优化了 WebMonitor 的采集端性能,此文借以回顾过去两年内对于 WebMonitor 采集端的优化升级之路
二、WebMonitor基本架构
WebMonitor基本架构
上图是目前的 WebMonitor 整体架构,称之为WebMonitor Stack。整体架构是三层,分别是采集端、管理端和数据端。三层结构通过 WebMonitor 的数据流结合在一起,下面简单介绍以下各层的主要职责:
采集端
采集端面向的是 Nginx,即直面用户设备上的前端上报请求,采集端的整体架构是两级 Flume Agent 组成的。
- 第一级 Flume Agent,用于解析前端上报的请求,根据 biz 类型的上报和 badJs 类型上报的接口协议,将请求解析。根据解析后的数据上报 Athena 和 UMP。然后将数据传递至下游 Flume Agent。
- 第二级 Flume Agent,用于将数据持久化,目前数据持久化的通道有两个,HDFS 和 Kafka(MQ),一方面可以通过 Impala+HDFS 的形式查询持久化的数据。另一方面也可以通过 Kafka 落 ElasticSearch(后简称
ES
),通过搜索引擎查询数据。
管理端
相信很多前端同学都体验过 WebMonitor 的 Old School 式的管理端。管理端主要的功能包括以下内容:
- 维护 biz 类型上报和 badJs 类型上报的埋点信息
- 提供查询业务错误和 badJs 错误的界面
- 提供配置内容接口,方便采集端定时更新上报配置
数据端
数据端的数据承载形式在过去的若干年进行了多次优化升级,毕竟在现有的上报量级下(日均4TB+),当传统的DB已经不能承载这种数据量级时,需要提供一些高效的查询手段。
- 黑铁时代。上报数据在本地磁盘进行
休克式
采样,仅保留不足1%的原始日志,通过rsync
汇总至一台机器进行查询。无索引、无并行。 - 白银时代。上报数据通过采集端落入分布式文件系统
HDFS
,使用大数据查询引擎Impala
进行并行查找。无索引、并行。 - 黄金时代。上报数据通过采集端落入 Kafka,通过一个 Agent 将数据写入
ES
,利用搜索引擎进行查找。有索引、并行。
数据端的优化往往最能改变用户的体验感受,查询的性能终于有着火箭般的提升:
- 黑铁时代:1-2 min
- 白银时代:30s
- 黄金时代:不足5s
三、WebMonitor采集端架构的整体优化
WebMonitor采集端旧流程
上图是 WebMonitor 采集端旧流程的示意图。采集端的核心流程包括:
- 接收数据
- 解析数据(计算)
- 上报数据( IO )
- 存储数据(高 IO)
这四个流程依次顺序的同步执行,完成数据存储后将 Response
返回给前端。但是由于上述流程中涉及到 IO 操作,在海量请求下,哪怕是仅仅保留较小比例的数据,IO压力也会非常大,会直接拖累采集端的性能。因此针对上述情况,WebMonitor 采集端做了一些调整,结果如下图所示:
WebMonitor采集端新流程
新的流程通过以下手段规避了旧流程的问题:
- 在同步处理流程中丢弃高 IO 负载的操作,例如数据落地。保证主流程中尽可能少的IO操作,避免IO操作降低接口响应的整体性能。
-
通过内存Event Bus Channel解耦监控上报和数据落地,使接受请求、上报监控和数据落地三个操作完全
异步化
。 - 数据落地使用分布式文件系统HDFS,解决了单一机器存储不足需要定时 rsync 到管理端机器的问题。
通过上述的优化过程,大量减轻了采集端接口的 IO 压力,使得接口性能有了显著的提升,如下表所示。
流程类型 |
Avg |
TP99 |
MAX |
---|---|---|---|
旧流程 |
2.1ms |
300ms |
2000ms |
新流程 |
0.7ms |
200ms |
2000ms |
四、监控上报的优化
量变引发质变,谁能想到简单的监控上报也会成为业务的性能瓶颈?
在海量上报请求(日常 300W QPM)的前提下,UMP上报存在一些问题
- UMP 对于标记为失败的方法监控,不做聚合操作。
- UMP 对于相同 key 的业务上报,不做聚合操作。
由于存在上述的问题,那么过去直接将上报能力放入采集端接口处可能引发性能问题。因此我们曾经进行了一次失败的尝试,其修改如下图所示:
WebMonitor上报异步化流程
我将监控上报的能力从采集端的两级 Flume Agent 中完全拆解,通过消费下游的 Kafka 消息,解析后进行上报,这个其实是很“很常规
”的异步操作,看似完美的解决了上报的性能问题,但是完全异步化又会引入新的问题?
- 中间链条过长,可用率受到中间节点的影响
- 由于 WebMonitor 的上报请求过大,需要单独维护一组机器进行上报,资源利用率较低
曾经出现过一次故障,因为 Kafka 的部分节点故障,导致 Kafka 短时间内不可用,然后红绿灯面板的一半红灯就亮起来了......
痛定思痛,我将采集端+上报的结构再次进行了调整,结构如下图所示:
WebMonitor上报优化流程
同样是异步上报,和之前的架构相比,我做了以下调整:
- 将监控上报通过 Sink 的方式加在采集端的 Channel 后面,在采集端的进程内完成上报。
- 业务监控、方法监控,成功的、失败的上报通通聚合,避免单条上报引发的IO性能瓶颈。
前面介绍WebMonitor采集端的架构调整,接下来我们讲讲枯燥的性能优化,虽然枯燥,但是优化完毕的效果着实喜人
五、Jetty Server 的线程优化
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));
}
简单解读一下上述代码:
- 默认的线程数是200
- 最低线程数不能少于8
其实对于大部分“典型”的 Http 服务而言,确实是比较合理的,毕竟动辄调用一个外部服务10+ms,写一次 DB 10+ms,那么大量的线程能够明显改善性能问题。但是 WebMonitor 它不是个“典型的” Http 服务呀?
默认线程模型的不足:
- 更适合数据密集型的应用。
- 过多的 Worker 线程在高负载的情况下,大幅度抵消多线程的性能“红利”,甚至会影响 Connect 线程。
针对 WebMonitor 的业务特点,我做了一些关于线程的调整:
- Worker 线程数调整至 32 甚至更低(8 Core的机器),这是避免在超高的请求下(例如大促零点),即使所有 Worker 线程都满负荷,也不能影响前端的 Nginx 。做一个好服务的必备条件是:既要保护好自己的服务不被打垮,也要保护好别人的服务不受影响。
- connect 线程可以通过线程池的方式进行连接,避免单一连接线程有问题引发的 HttpServer 整体的低响应。
六、Jetty Server 的 Timeout 优化
Jetty请求响应示意图
Timeout 是一个非常关键设置,还是那句话“量变引发质变
”,当请求量过大时,连 TCP 的连接可能都值得优化。设置 Timeout 是一个妥协
的过程,没有完全合适的设置,只有妥协上下游的设置。我在设置 Timeout 的过程中走了很多弯路:
No Timeout
- ?:不需要和前端 Nginx 维持长连接,内存负载压力小。
- ?:需要额外的资源不断的创建连接、关闭连接。(TCP的三次握手和四次挥手,一个都不能少!)
Short Timeout(100ms)
- ?:自身服务异常时不会影响前端 Nginx。(就算自己爆炸也要保护兄弟)
- ?:Nginx 容易使用已经被服务端关闭的连接,造成出错。
- ?:连接较少可以复用,增加创建连接的成本。
Long Timeout(8000ms = Nginx KeepLive Timeout)
- ?:连接基本可以复用,不论是 Nginx 还是服务端都能节省创建连接的开销。
- ?:服务异常时 Nginx 性能可能受到影响。因为这条连接在 Timeout 时间内都是可用的,但是服务异常时,Nginx 就不得不创建更多的连接,而创建的连接可能响应依然很慢,所以整体降低 Nginx 的性能。
- ?:连接过大时,由于 TCP 的 TIME_WAIT 阶段需要一定时间,可能引发 connect timeout 的问题,因为没有 socket 可以使用了。
前面提到,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居然识别的是容器所在宿主机的核心数,这就会对各种框架对于最佳并行度的设置带来非常大的影响。
针对计算密集型应用:
- 最佳线程数应等于可用 CPU 的核数。
- 过多的线程会引发 CPU 的上下文切换,导致性能下降。
- JDK1.8.0_190 版本优化了 Docker 容器中 CPU 可用资源的识别。
- 不论是 JDK 内部,还是应用的第三方库,都会大量使用上述语句得到预期的最佳并发度,因此如果该语句不能返回真实的数据,影响是非常巨大的!
至此,WebMonitor 的各项优化,从架构到性能的各项优化措施已经介绍完了,最后看看优化完成以后的性能指标
流程类型 |
单机QPM |
Avg |
TP99 |
MAX |
---|---|---|---|---|
旧流程 |
30k |
2.1ms |
300ms |
2000ms |
新流程 |
41k |
0.7ms |
200ms |
2000ms |
性能优化后 |
81k |
0.8ms |
3ms |
25ms |
八、一个 Java 后端程序员的经验之谈~
- 80%的性能问题与 GC 有关。但是 GC 往往只是表象,产生不合理 GC 的原因才是问题的根本原因。
- 线程资源要合理利用,过多的线程非但不能带来预期的性能提升,反而会拖你的后腿。
- 永远要根据自己应用的实际情况来分析,Google 出来的答案未必适合你。
- 定位问题善用 Arthas (一个开源的性能分析工具),各种硬件资源监控,找到不合理的地方,也许就能找到问题的突破口。
- 浅谈DNS
- silverlight寻奇 - Graphite
- 程序运算性能测量
- 2018年比特币的真正瓶颈在这里
- 玩转 React 服务器端渲染
- WCF版的PetShop之二:模块中的层次划分[提供源代码下载]
- 我的WCF之旅(3):在WCF中实现双工通信
- 我的WCF之旅 (11): 再谈WCF的双向通讯-基于Http的双向通讯 V.S. 基于TCP的双向通讯
- 更新弹幕系统的心得体会
- 我的WCF之旅(6):在Winform Application中调用Duplex Service出现TimeoutException的原因和解决方案
- 我的WCF之旅 (11): 再谈WCF的双向通讯-基于Http的双向通讯 V.S. 基于TCP的双向通讯
- 我的WCF之旅(6):在Winform Application中调用Duplex Service出现TimeoutException的原因和解决方案
- 扩展mysql - 手把手教你写udf
- scrapy初体验 - 安装遇到的坑及第一个范例
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Linux查看进程的所有信息的方法示例
- 新版VPS主机管理面板WDCP安装及使用体验-国产简单易用型VPS面板
- linux中普通用户的定时任务详解
- 详解在Linux中清空或删除大文件内容的5种方法
- 教你一招实现Linux中的文本比对
- 怎么禁用 Ubuntu 服务器中终端欢迎消息中的广告
- Linux系统下部署项目的设置方法
- Linux中设置路由以及虚拟机联网图文详解
- 在Linux中如何一次重命名多个文件详解
- Vim自定义高亮分组以及一些实用技巧小结
- Linux redis-Sentinel配置详解
- 使用 Apache Web 服务器配置两个或多个站点的方法
- Linux下命令行cURL的10种常见用法示例
- Apache Web 服务器的安装配置方法
- Linux(Ubuntu 18.04)上安装Anaconda步骤详解