本文咱们开始介绍一个新的畛域 APM,也是字节码技术胜利利用的典型案例。将分为两大部分:APM 的根底概念和分布式跟踪的实践根底。
什么是 APM
APM 是 Application Performance Managment 的缩写,字面意思很容易了解,” 利用性能治理 ”, 它是由 Gartner 演绎形象出的一个治理模型。近年来 APM 行业被越来越多的企业所关注,尤其是在 2014 年末,NewRelic 的胜利上市,更加激发了人们对这个行业前景的有限遥想。国内崛起的听云、OneAPM,以及最近微信和 360 团队刚开源的安卓端 APM,使 APM 遍地开花。上面是赫赫有名的 skywalking APM 监控采集到的数据
咱们为什么须要 APM
影响用户体验的三大环节:
- 前端渲染
- 页面加载工夫
- DOM 解决工夫
- 页面渲染工夫
- 首屏加载工夫
- 网络传输
- DNS 解析工夫
- TCP 建连工夫
- 网络传输工夫
- SSL 握手工夫
- 后端服务
- SQL 执行工夫
- 缓存读写工夫
- 内部服务调用工夫
每一个环节都有可能有性能的问题,咱们须要做的是把性能做到可度量、指标化
咱们须要做什么
针对咱们刚提到的三大环节,咱们能够针对性的来看下每个环节咱们能够做些什么
浏览器端
浏览器的页面加载过程如下
咱们能够通过这些过程拿到十分要害的业务指标:页面加载工夫、首屏工夫、页面渲染工夫 咱们在 chrome console 里输出 window.performance.timing
就能够拿到具体的各阶段工夫
服务端 APM
假如有这样一个函数,咱们须要进行监控
public void saveUser() {doDbOperation();
flushCache();}
咱们须要对它的字节码进行改写,主动注入一些代码达到监控的性能,一个最简略的模型如上面的代码所示
public void _saveUser() {
// 获取开始工夫
long start = System.currentTimeMillis();
// 记录未捕捉异样
Throwable uncaughtException = null;
try {doDbOperation();
flushCache();} catch (Throwable e) {
uncaughtException = e;
throw e;
} finally {
// 记录完结工夫
long end = System.currentTimeMillis();
// 上报 spanName、开始工夫、完结工夫、是否有未捕捉的异样
APMUtil.report("UserService.saveUser", start, end, uncaughtException);
}
}
怎么样做嵌码?
- Java 服务端:应用咱们之前介绍过的 javaagent 字节码 instrument 技术进行字节码改写
- Node.js 阿里有开源 pandora.js 能够参考借鉴
- 安卓:用 gradle 插件在编译期进行 hook
- iOS:Hook(Method Swizzling)
咱们前面会着重介绍 Java 服务端 APM 如何来实现跨过程的调用链路跟踪监控
接下来咱们将解说分布式跟踪相干的内容,将从单 JVM 到扩过程如何实现链路调用来解说原理与实现。
分布式跟踪实践根底
参考 Google Dapper 论文实现,每个申请都生成全局惟一的 Trace ID/Span ID,端到端透传到上下游所有的节点,通过 Trace ID 将不同零碎的孤立的调用日志和异样日志串联在一起,同时通过 Span ID、ParentId 表白节点的父子关系,如下图所示
单 JVM 调用链路跟踪实现原理
在 Java 中,咱们能够很不便的用 ThreadLocal 的 Stack 的实现调用栈的跟踪,比方有如下的调用关系
void A() {B();
}
void B() {C();
}
void C(){}
咱们约定:spanId 做为以后调用 id,parentId 做为父调用 id,traceId 做为整条链路 id
那么咱们调用上报的 trace 信息大略如下
[
{"spanName": "A()",
"traceId": "1001",
"spanId": "A001",
"parentId": null,
"timeCost": 1000,
"startTime": 10001,
"endTime": 11001,
"uncaughtException": null
},
{"spanName": "B()",
"traceId": "1001",
"spanId": "B001",
"parentId": "A001",
"timeCost": 900,
"startTime": 10001,
"endTime": 11001,
"uncaughtException": null
},
{"spanName": "C()",
"traceId": "1001",
"spanId": "C001",
"parentId": "B001",
"timeCost": 800,
"startTime": 10001,
"endTime": 11001,
"uncaughtException": "java.lang.RuntimeException"
}
]
通过 traceId、spanId、parentId 三者的数据,咱们能够很不便的构建出一个残缺的调用栈
跨过程、异构零碎的调用链路跟踪如何解决?
只须要把 traceId 和 spanId 传递到下一层调用就好了。比方咱们采纳 HTTP 调用的形式调用另外一个 JVM 的服务。在 JVM 1 中在 HTTP 调用前调用相应 setHeader 函数新增 X-APM-TraceId 和 X-APM-SpanId 两个 header。JVM 2 收到申请当前,会先去查看是否有这两个 header,如果没有这两个 header,阐明它本人是最顶层的调用。如果有这两个 header 的状况下,会把 header 中的 traceId 当做后续调用的 traceId,header 中的 spanId 做为以后调用的 parentId。如下图所示
Duboo 等 RPC 调用同理,只是参数传递的形式有所不同
APM 架构
架构概览
以数据流向的角度看整个后端 APM 的架构如下
Agent 上报
Agent 上报端采纳一个大小为 N 的内存队列缓存产生的调用 trace,N 个别为几千,业务代码写完队列立刻 return,如果此时因为队列生产太慢,则容许丢数据,免得造成内存暴涨影响失常业务。设置一个心跳包定时上报以后队列的大小,如果超过 N 的 80%,则进行告警人工干预。
因为 APM 会产生调用次数放大,一次调用可能产生几十次上百次的的链路调用 trace。因而数据肯定要合并上报,缩小网络的开销。这个场景就是 合并上报,指定工夫还没达到批量的阈值,有多少条报多少条,针对此场景我写了一个非常简单的工具类,用 BlockingQueue 实现了带超时的批量取
-
Add 办法
// 如果队列已满,须要超时期待一段时间,应用此办法 queue.offer(logItem, 10, TimeUnit.MILLISECONDS) // 如果队列已满,间接须要返回 add 失败,应用此办法 queue.offer(logItem)
-
批量获取办法 BlockingQueue 的批量取办法 drainTo() 不反对超时个性,然而留神到 poll() 反对,联合这两者的个性咱们做了如下的改变(参考局部 Guava 库的源码)
public static <E> int batchGet(BlockingQueue<E> q,Collection<? super E> buffer, int numElements, long timeout, TimeUnit unit) throws InterruptedException {long deadline = System.nanoTime() + unit.toNanos(timeout); int added = 0; while (added < numElements) { // drainTo 十分高效,咱们先尝试批量取,能取多少是多少,不够的 poll 来凑 added += q.drainTo(buffer, numElements - added); if (added < numElements) {E e = q.poll(deadline - System.nanoTime(), TimeUnit.NANOSECONDS); if (e == null) {break;} buffer.add(e); added++; } } return added; }
残缺代码如下:
private static final int SIZE = 5000;
private static final int BATCH_FETCH_ITEM_COUNT = 50;
private static final int MAX_WAIT_TIMEOUT = 30;
private BlockingQueue<String> queue = new LinkedBlockingQueue<>(SIZE);
public boolean add(final String logItem) {return queue.offer(logItem);
}
public List<String> batchGet() {List<String> bulkData = new ArrayList<>();
batchGet(queue, bulkData, BATCH_FETCH_ITEM_COUNT, MAX_WAIT_TIMEOUT, TimeUnit.SECONDS);
return bulkData;
}
public static <E> int batchGet(BlockingQueue<E> q,Collection<? super E> buffer, int numElements, long timeout, TimeUnit unit) throws InterruptedException {long deadline = System.nanoTime() + unit.toNanos(timeout);
int added = 0;
while (added < numElements) {added += q.drainTo(buffer, numElements - added);
if (added < numElements) {E e = q.poll(deadline - System.nanoTime(), TimeUnit.NANOSECONDS);
if (e == null) {break;}
buffer.add(e);
added++;
}
}
return added;
}
数据收集服务
数据处理能够分为三大部分:流水查问、实时告警、离线报表剖析
- 流水查问 因为咱们常常依据一些用户 id 或者特定的字符串来检索整条链路调用,咱们在技术选型上抉择了 ElasticSearch 来做存储和含糊查问
ElasticSearch 在海量数据存储和文本检索方面的微小劣势和,联合 ELK 工具套件,能够十分轻松的实现数据可视化、运维、监控、告警。比方咱们查问特定调用的昨天一整天响应工夫百分位,能够十分不便的统计进去
也能够不便统计某个我的项目整体的响应工夫百分位,能够用来掂量服务 SLA 程度
- 实时告警 实时数据处理能够用时序数据库,也能够用 Redis + Lua 的形式,有告警的状况下能够达到分钟级别的微信、邮件告诉,也能够在业务上做告警收敛,防止告警风暴
- 离线解决 离线解决次要产生一些实时性要求没那么高、ELK 无奈生成的简单报表,技术栈上有很多可供选择的,咱们这里抉择的是最传统的阿里云 ODPS
总结
这篇文章咱们解说了 APM 的基本概念,次要内容小结如下:第一,APM 的含意是 ” 利用性能治理 ”,近年来 APM 行业被越来越多的企业所关注。第二,谈到影响用户体验的三大环节:前端渲染、网络传输、后端解决,以及为了进步用户体验每一步咱们能够做什么使得性能能够做到可度量、指标化。第三,介绍了常见的嵌码技术,帮忙以最小的接入老本进行性能监控治理。第四,讲了基于 Google dapper 实践的分布式系统跟踪的原理和简略实现,次要分了两块:第一,单过程内调用链路跟踪如何实现,第二,跨过程、异构零碎的调用链路如何实现。
本文由 mdnice 多平台公布