前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >性能提升30%,陌陌应用性能持续剖析产品化实践

性能提升30%,陌陌应用性能持续剖析产品化实践

原创
作者头像
童子龙
修改2025-03-06 12:09:17
修改2025-03-06 12:09:17
4500
代码可运行
举报
运行总次数:0
代码可运行

Continous Profiling的概念起源于 Google 的论文,是一种在应用运行时收集应用程序相关信息的动态分析手段,让性能分析贯穿应用的整个生命周期,广泛地用于性能巡检、问题定位等场景。当应用程序在运行过程中,正在处理计算或者执行syscall的时候,应用自身会产生大量有价值的运行时信息,我们可以进行实时的数据采集,这个采集过程是read only的,我们要确保我们的采集数据动作不会给应用带来任何Debuff。通过对这些运行时的一手信息的聚合分析,帮助用户做一些疑难杂症的root cause analysis和应用性能的退化原因分析。

1、背景

当前陌陌已经建设了比较成熟的Trace、Metric、Log可观测平台,可以很快的定位服务上下游调用问题,但是对于服务自身的内部问题(某个私有方法cpu占用高或者内存申请太多导致fullgc等)通常缺少有效的手段去定位问题的根因,运行时的jvm对于用户来说是一个黑盒,而仅仅通过trace、metric、log等数据还不足以反应jvm内部真实的执行情况,排查此类问题通常需要借助一些第三方工具如jstack、arthus,工具的使用门槛导致一部分开发经验较少的开发通常会止步于此,交由问题经验更丰富的资深开发去解决,无形中也就拖慢了问题定位的效率。基于以上的背景我们建设了服务性能持续剖析能力,并将相关剖析工具进行产品化和平台化,补足当前陌陌apm监控在自身问题定位上的短板。

产品目标:

  1. 降低定位门槛,提升排查效率,新手也能动手分析
  2. 支持问题现场回溯,故障root cause analysis
  3. 每天定时生成应用性能分析报告,提供优化建议
  4. 采集探针不影响应用的安全和性能

2、产品全景

我们建设了性能剖析诊断平台,产品定位是服务自身疑难杂症的root cause analysis,发现和优化应用性能的退化点。覆盖陌陌所有容器化部署的java类型服务,并提供4种基础性能剖析能力:

  • cpu持续分析,提供方法维度的性能趋势分析;
  • alloc(内存申请)持续分析,提供方法维度的内存占用分析;
  • 线程分析则提供了线程和线程组维度的性能分析;
  • 内存dump分析,实现了对jvm堆内存使用情况和内部活跃对象的分析。

另外我们通过持续、固定频率的收集的服务的cpu、alloc性能数据,将方法维度的时序数据存储到Clickhouse,生成方法性能趋势图。结合服务运维、代码变更记录等信息建立了服务性能巡检机制,用于发现服务发布过程中的性能退化事件,避免微小的性能退化日积月累后导致服务整体性能的恶化。

产品架构概览

系统整体大致可以分为Agent、Server、产品console、性能巡检4个模块。

Agent部分

  1. 通过javaagent探针技术实现了业务无感知接入profile,只需要在发布平台一键勾选profile开关,重新发布后即生效。
  2. 基于AsyncProfiler、JMX等技术实现了低开销的性能诊断能力,剖析期间对服务性能影响在1%左右,不开启剖析的时间则没有任何性能开销。

Server端主要功能有两个

  1. 对Agent下发剖析任务,我们除支持常规的单次下发、定时下发任务外,还和报警平台进行了联动,实现了基于报警触发profile采集的功能,即使服务不开启定时剖析功也不会丢失问题现场;
  2. 接收来自Agent上传的快照,解析后的数据(火焰图、分析结果等)会存储到oss中供前端ui展示使用。对于cpu、alloc类型的快照会额外的生成方法维度的性能时序数据并存储到clickhosue,供后续性能巡检模块分析使用。

3、profile技术原理

在java领域主流的profiling功能包括cpu、memory allocation、thread、class等,其中以cpu profling最为常用,这里我们主要介绍下cpu profling的实现。基本上主流的cpu profling都是基于sampling实现的,也有少部分方案如jprofiler提供了基于Instrument字节码增强技术实现的cpu采样,缺点就是资源消耗巨大,通常不会在生产环境使用。cpu采样的基本原理为定期对对线程的堆栈进行dump,统计堆栈中出现的方法频次,近而估算出每个方法占用的cpu时间。

我们知道方法的调用栈是由一个个栈帧(stack frame)组成,当发生发生函数调用会开辟新的栈空间,将函数参数、局部变量、返回地址等入栈,栈帧遵循后进先出(LIFO)的原则,最近被调用的函数的栈帧位于栈顶,而先前调用的函数的栈帧位于栈中,调用链起始处的函数则位于栈底。因此当我们在某个时刻对一个正在运行的线程进行dump后,此刻位于栈顶的函数即代表了当前时刻正在执行函数,我们就可以根据一个方法在栈顶出现的次数除以总采样次数来估算它占进程cpu执行时间的比例,即算出方法自身占用的cpu占比

业界主流的实现cpu profling有三种技术方案: JMX 、JFR 和 AsyncProfiler:

JMX

全称为 Java Management Extensions,是一个为java应用程序植入管理功能的框架,提供了一种简单、标准的监控和管理资源的方式,允许用户通过MBeans来监控应用程序的性能指标,例如内存使用、线程、垃圾回收等。其中JMX内置的ThredMXBean管理接口中的dumpAllThreads方法可以对当前jvm所有的线程进行dump,返回结果中就包括了线程的栈帧(stacktrace)。通常做法是通过javaagent探针技术在premain方法中启动一个异步线程定时地执行dumpAllThread方法收集方法堆栈。听起来很简单对吧,但是这个方案有一个致命的缺点,即 SafePoint bias问题。简单来说JMX的固有机制导致在dump某个线程的时候只能在目标线程运行到“安全点”(SafePoint)的时候才能执行,这会导致我们采样到的堆栈都是在安全点附近执行的代码,采样结果缺乏了公平性,可能使得某些执行时间极短也真实占用了大量的CPU Time的方法得不到采样的机会,进而导致最终结果无法反映真实的CPU热点。 想了解更多Safepoint bias的细节见 Why (Most) Sampling Java Profilers Are Fxxking Terrible

JFR

是 Java Flight Record 的缩写,是 JVM 内置的基于事件的JDK监控记录框架,与飞机的黑盒子功能相似JFR开启后会持续地记录JVM内部的一系列事件。 JFR支持100多种JVM事件,包括 Class Load Event(类加载)、Garbage collect Event(垃圾回收)等,包括开启JFR自身也是一个Event。JFR Event可以分为三类:

  • Instant Event:瞬时事件,例如Throw Execption Event。
  • Duration Event: 持续时间,例如 Garbage collect Event。
  • Sample Event: 采样事件,通过一定得频率采样到的事件,比如 Method sampling Event,方法调用事件的元信息中就包括了方法堆栈信息,可以用来实现cpu采样功能。

JFR的性能开销很低,官方宣称在默认的采集配置下性能影响在1%左右,并且对方法执行的采样事件是完全异步的,没有JMX方案的Safepoint bias问题。听起来是似乎是很完美的方案,然而不幸的是JFR在jdk11之前是收费的,而openjdk8需要在292版本后才可以使用,并且由于是从jdk11 backport回去的没有专门的优化,性能上有很大的问题。当前陌陌还有不少的服务泡在jdk8上,因此也pass了该方案。

AsyncProfiler

是一个c/c++开发的没有 Safepoint bias 问题的低开销 java 性能分析工具,利用了 hostspot jvm 特殊的 api 收集线程堆栈信息来实现准确的 cpu 性能剖析,除了 cpu 还支持 alloc、lock、wall等类型的剖析,甚至可以收集机器硬件事件,例如缓存未命中、页面错误等。作为我们最终采用的方案,AsyncProfiler也同样拥有极低的性能开销,根据我们的压测在普通的负载下性能影响在1%左右,极端负载下在3%左右,在网上也看到过一些其他公司的分享中有提到性能影响这一块,基本上测试结果是可以互相印证的。同时为了降低业务接入的复杂度,我们采用了javaagent的集成方案,在业务服务进程加载profile agent后会在 premain 函数中开启一个后台线程,通过System.loadLibrary函数加载 AsyncProfiler 动态连接库,并在后收到server下发的 profile 任务后通过 JNI 接口实时调用 AsyncProfiler 执行剖析。

聊聊关于AysncProfiler实现的一些技术细节:

上面有介绍到AsyncProfiler使用了 jvm 内部的接口,即 AsyncGetCallTrace 实现的cpu堆栈采样,从名字可以看出 AsyncGetCallTrace 是异步的,因此不会像 JMX 方案会受安全点的影响,采样准确性也就得到了保障。由于AsyncGetCallTrace非标准JVMTI函数,因此需要采用一些 trick 的方法才能拿到方法的地址,AsyncProfiler 通过在 Agent_OnLoad 和 Agent_Attach 阶段通过 glibc 提供的 dlsym 函数拿到了 AsyncGetCallTrace 在 libjvm.so 中的符号地址,经过转换后就可以当做普通函数一样使用了,这也意味着 AsyncGetCallTrace 函数只能在 hotspot 及衍生的 jvm 中运行。

代码语言:javascript
代码运行次数:0
复制
// AGCT函数签名
void AsyncGetCallTrace(ASGCT_CallTrace *trace,
                       jint depth,
                       void* ucontext);

AsyncProfiler实现低开销的关键就是通过注册SIGPROF系统信号实现定时地采集,SIGPROF 是一个操作系统信号,可以向操作系统注册一个回调函数和指定触发的回调事件的间隔(例如10ms),操作系统就会每隔10ms随机从当前进程运行的线程中挑选一个,触发系统中断并执行回调函数,函数参数中就包括了 AsyncGetCallTrace 需要的 ucontext 。 由于不需要轮询所有的线程,因此采样的整体开销是非常低的,唯一的开销就是解析栈帧,对于栈帧比较深的线程AsyncProfiler默认最多爬取2000层,多种机制共同保障了低性能开销。

代码语言:javascript
代码运行次数:0
复制
void PerfEvents::signalHandler(int signo, siginfo_t* siginfo, void* ucontext) {
    ......
    ExecutionEvent event;
    Profiler::instance()->recordSample(ucontext, counter, PERF_SAMPLE, &event);
    ......
}

4、产品功能形态

1.火焰图分析

CPU、内存Alloc聚合火焰图

CPU和内存申请分析功能我们采用了火焰图(Flame Graph)的展现形式,界面功能上两者完全一致,区别在于一个是统计的方法cpu消耗,一个统计的是方法内存申请量。火焰图是由 Linux 性能优化大师 Brendan Gregg 发明的,名字由图形看起来就像一个跳动的火焰而得名,每个格子代表一个独立的方法,格子宽度代表方法消耗的性能多少,这种展现形式能够更加直观的展示函数之间的调用关系和方法的资源占用情况,能够以全局的视野发现所有可能出现潜在性能问题的代码路径。

在下图左边排名表格中的每个方法都有 “自身”“总计” 两个统计维度, “自身”列展示了方法自身消耗的资源(cpu、内存),即不包括调用其他方法消耗的资源;总计列展示了方法栈自身和调用其他方法消耗的总资源。 通常来说我们需要重点关注“自身”资源消耗排名靠前的方法,它们往往是造成服务性能瓶颈的“元凶”。为了进一步提升定位效率,我们还支持了将一段时间的火焰图进行聚合分析的功能,这样我们便能排除单次采集导致的误差,火焰图的结果能够更加真实的反应服务的运行情况。

cpu聚合火焰图
cpu聚合火焰图

我们还将方法列表和火焰图实现了联动,在选中表格中的单行方法后,火焰图会自动展示仅和该方法关联的所有执行路径,这样做的好处就是即便是某个第三方类库的方法消耗了大量的性能,我们也能快速的定位到调用源头的业务代码。

方法联动的火焰图
方法联动的火焰图
性能差分火焰图

对于一些性能巡检的使用场景,我们可能需要了解一个方法在过去和现在的性能是否发生了变化,趋势如何等,因此我们还设计了差分火焰图的功能,支持对两个时间段的火焰图进行差异对比分析,并用不同的颜色来标记、突出方法性能退化点,颜色越红退化的程度越大,颜色越绿则方法的优化效果越好,方便我们评估优化效果。

性能差分火焰图
性能差分火焰图

2.线程分析

线程分析被设计为一个轻量级的剖析能力,提供线程粒度的CPU使用率和内存申请量统计,可以真实还原线程执行过程。我们还设计了线程组、状态、锁对象等多个维度的统计饼图,可以快速定位进程cpu_load高、锁争用等问题场景。页面也支持查看单个异常线程的方法栈,方方便我们快速的定位问题代码。

线程状态分组统计
  • 线程状态分组统计,快速分析线程状态比例是否合理
  • 线程组分组统计,快速找到线程数高的线程组
  • 锁对象分组统计,快速找到当前阻塞在该锁对象的线程列表
  • 线程组cpu占比分组统计,快速找到cpu占用高的线程组
线程状态图
线程状态图
线程状态、线程组、锁对象排序分析
  • 线程状态所处状态,是否存在死锁
  • 等待的锁对象(如果状态处于等待、阻塞状态)
  • cpu使用率(采集期间的cpu使用率)
  • 申请内存大小(采集期间申请的内存)
  • 线程进入wait状态的总次数(从进程启动到采集时刻)
  • 线程进入block状态的总次数(从进程启动到采集时刻)
线程状态、线程组、锁对象排序分析
线程状态、线程组、锁对象排序分析
采集时刻的执行堆栈
采集时刻的执行堆栈

3.堆栈内存dump分析

首先简单介绍下JVM的内存结构是怎样的,JVM 进程可用的内存大致可分为以下5类(JDK11版本以上)

  • 堆内存: JVM 存储对象或动态数据的地方。这是最大的内存区域,也是垃圾收集(GC)发生的地方。堆内存的大小可以使用Xms(初始)和Xmx(最大)标志来控制,堆进一步分为年轻和老年代空间。
  • 年轻代:年轻代进一步分为“Eden”和“Survivor”,该空间由“Minor GC”管理。
  • 老年代:在 Minor GC 期间达到最大保留阈值的对象所在的位置,该空间由“Major GC”管理。
  • 线程堆栈:存储线程的静态数据的位置,包括方法/函数帧和对象指针。可以使用Xss设置堆栈内存限制。
  • 元空间:类加载器用来存储类定义。元空间是动态的,可以用-XX:MetaspaceSize-XX:MaxMetaspaceSize来限制元空间大小。
  • 代码缓存JIT编译器存储经常访问的已编译代码块的位置,一般情况JVM 必须将字节码解释为机器码,而 JIT 编译的代码不需要解释,因为它已经是机器码并缓存在这里。
  • 共享库:存储所使用的任何共享库的机器码,操作系统每个进程仅加载一次。

内存dump分析核心功能(不同JDK版本dump协议内容有差异)

  • 直接输出jvm进程当前的总/活跃对象统计信息
  • 输出堆的汇总信息,如年轻代、年老代堆使用情况等。
  • 打印类加载信息
  • 输出堆配置信息
  • 输出finalize队列排队情况
堆配置信息
堆配置信息
堆瞬时使用情况
堆瞬时使用情况

4. profile持续分析报告

业务应用在持续迭代过程中,可能会发生性能恶化,比如热循环代码、数据资源的IO瓶颈等场景。

针对核心服务每日自动开启性能巡检,支持cpu、内存申请、线程三个维度进行分析:

支持配置定时采集(每一小时采集一次) 支持报警事件触发采集(订阅告警事件采集profile信息)

上文架构介绍提到,我们将方法函数维度的性能时序数据存储到Clickhouse中,通过方法函数堆栈级别的性能时序数据的diff分析,计算出函数方法级别的性能趋势图,利用统计算法,判断出现性能退化函数,根据专家经验,并给出合理的优化建议。

cpu性能时序分析

1.方法性能退化分析,与前日数据进行对比分析,找出cpu大幅增加的方法。

  • 业务方法(以公司组织命名的包)退化分析
  • 第三方包方法(非公司组织命名的包)退化分析

2.方法性能时序图,展示当日的方法性能趋势

cpu分析体检报告
cpu分析体检报告
内存申请时序分析

1.方法性能退化分析,与前日数据进行对比分析,找出Alloc内存大幅增加的方法。

  • 业务方法退化分析
  • 第三方包方法退化分析

2.方法申请内存时序图,展示报告当日的内存使用趋势

内存分析体检报告
内存分析体检报告
线程分析
  1. 线程状态分析:分析各个状态的线程数量是否合理
  2. 线程数量分析:分析线程组的数量是否合理
  3. 线程死锁分析:是否发生死锁
  4. 线程性能分析:分析cpu、内存申请占比最高的线程组
  5. 线程组cpu、线程状态数量时序图
线程分析报告
线程分析报告

5、最后

在应用性能监控领域,问题根因定位是一个非常重要的特性。结合profile、trace和metric,将不同类型的数据关联起来,以获得更全面的上下文信息。例如,将profile数据与trace数据关联,可以根据请求的响应时间和错误指标找到对应的堆栈,profile关联服务内部的调用链,trace关联服务间的调用链,进一步分析具体的方法真正意义上的全链路调用堆栈。这样可以更准确地定位性能瓶颈和问题所在,将分析结果可视化和报告化,使得根因分析的结果更易于理解和分享。使用图表、图形和可视化工具,将分析结果以易于理解的方式展示给开发人员、运维团队和决策者,帮助他们更好地理解性能问题的根本原因。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、背景
  • 2、产品全景
    • 产品架构概览
  • 3、profile技术原理
    • JMX
    • JFR
    • AsyncProfiler
  • 4、产品功能形态
    • 1.火焰图分析
    • 2.线程分析
    • 3.堆栈内存dump分析
    • 4. profile持续分析报告
  • 5、最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档