字节跳动云原生

抖音解决 Top 1 Java 崩溃的通用优化实践

Image

火山引擎应用性能监控全链路版 APMPlus 通过先进的数据采集与监控技术,为企业提供端到端的全链路 APM 服务。

来源 | 抖音 Android 质量团队

体验 | www.volcengine.com/product/apmplus
最近三个月,抖音 Android 版面临一个多次触发线上报警的崩溃问题,全量版本和灰度版本的异常数据激增,该问题不仅容易触发报警,更成为了 Java Top 1 崩溃问题,带来巨大困扰,亟需攻坚解决。
本文展示了具体的分析过程、优化思路和解决方案,同时提供了已集成该方案的实用工具。

初步分析

多维特征

我们以某发版期间数据为例进行分析:

  • 机型方面:比较分散,有聚集部分 samsung sm-s9180 占比 top1。

  • 渠道方面:比较分散,华为、小米、三星占比 top3。

  • ROM方面:比较分散。

  • OS 版本方面:排除古老的 5.1.1 系统,只有 10 以上版本,12/13/10 占比 top3。

  • App 小版本方面:排除古老的版本,基本跟日活排序一致。

通过对多维特征分析,我们得出结论是:存在 OS 高版本和三星机型聚集特征。

Image
崩溃堆栈

从下图我们看到,崩溃类型 TransactionTooLargeException,崩溃现场在 BinderProxy.transactNative 方法附近,崩溃提示消息 data parcel size xxx bytes,崩溃时 Activity 在向 AMS 传递 stop 信息。

初步分析这些信息,我们可以得出以下结论:

  • 这是由 Binder 传输数据量超过阈值触发的崩溃

  • 排除其中无关的动态代理 ProxyInvocationHandler 的业务堆栈,其它都在系统堆栈中,无法直接定位到具体业务方解决

Image
崩溃日志

除了崩溃堆栈,我们也分别从聚合 logcat 日志和单次 logcat 日志进行分析,希望能够从中获得一些有价值的线索。

通过火山引擎应用性能监控全链路版(APMPlus)App 端监控的 logcat 词云,我们看到聚合日志 top1 是 JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = **。这是系统 Binder 传输数据失败打印的日志。

Image

我们从源码里找到打印日志的地方,发现是在 Native 层 Binder.cpp 里,接收到 Binder 驱动的错误码 FAILED_TRANSACTION 之后,先打印错误日志,然后通过 jniThrowException 函数从 JNI 层进入 Java 层抛出异常。

Image

通过对聚合 logcat 日志进行分析,我们发现:只有系统 Binder 传输失败日志,没有业务相关日志,无法确定是哪个业务不合理导致的崩溃。

根据前面提到的聚合 logcat 日志关键字,我们翻看了几十个上报日志,发现在 JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = ** 的后面紧接着出现 Tag 为 ActivityStopInfo 的 Bundle stats: 日志,其中一个如上图所示。因此,我们知道 Binder transaction 传输的 parcel 内容很大可能跟 Bundle 有关。

从打印的 Bundle stats: 树形结构看,由于只打印了系统的 view 和 fragment 占用 size,没有 App 业务相关占用 size,我们无法细分是哪里不合理。另外在日志 JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = ** 的前面,我们终于发现了跟业务相关的 Activity 是 com.ss.android.ugc.aweme.detail.ui.DetailActivity。翻看几十个上报日志,并不是每个日志都是 com.ss.android.ugc.aweme.detail.ui.DetailActivity,还有 com.ss.android.ugc.aweme.shortvideo.ui.VideoPublishActivity 等。

经过分析,我们能够从单次 logcat 日志中知道是哪个 Activity stop 时出错,但是目前分析不出这些处于 stop 状态的 Activity 占比情况。

Image

综合聚合和单次 logcat 日志看,没有发现明显 App 业务逻辑聚合,只有系统日志,因此无法简单定位到某个业务团队解决。

初步结论

经过前面的分析,我们得出初步结论:

  • 问题在于 Activity stop 时给 AMS 传输 Bundle 超过 Binder 驱动限制的大小,触发了崩溃。

  • 调用栈和日志无明显业务聚合,无法简单确定是哪个业务引入的问题。

因此,我们需要进行深入分析,一方面要从监控归因着手排查其中不合理的因素,另一方面也要从底层通用解决方案尝试突破限制彻底解决问题。

深入分析

下面将主要从崩溃堆栈进行深入分析。
我们翻看了几十个崩溃堆栈,发现并不是只有 Activity stop 调用栈,还有 Activity start 调用栈,主要包含两类,一个是 activityStopped 关键字的崩溃,占比约 97.25%;另一个是 handleStartActivity 关键字的崩溃,占比约 2.75%,这些崩溃主要与三星兼容性问题有关。

在时间和精力都有限的情况下,我们决定先解决 activityStopped 关键字的崩溃。如图所示,我们从底层向上层逐步推进,对核心逻辑崩溃堆栈进行分析。

Image

Caused by: android.os.TransactionTooLargeException: data parcel size 541868 bytes

根据关键字 data parcel size 和相关源码分析,我们得出 data parcel size 附近的数据处理流程:

Image

1. JNI 层 BinderProxy_transact 函数先调用 IBinder->transact 函数进行 Binder 数据传输。

2. IBinder->transact 函数的返回值 err 用于识别 Binder 传输数据错误码。

3. 当 err != NO_ERROR && err != UNKNOWN_TRANSACTION,说明 Binder 数据传输出现异常。

4. signalExceptionForError 函数对异常情况多种错误码进行分发处理。

5. 当错误码 err = FAILED_TRANSACTION 时,说明是在传输的过程中失败,接下来主要根据 parcelSize 和 200*1024 的大小关系处理。

6. 当 parcelSize > 200*1024 时,后续通过 jniThrowException 函数在 Java 层抛出 TransactionTooLargeException,并提示消息 "data parcel size %d bytes"。

7. 当 parcelSize <= 200*1024 时,后续通过 jniThrowException 函数在 Java 层抛出 DeadObjectException 或者 RuntimeException,并提示消息 "Transaction failed on small parcel; remote process probably died, but this could also be caused by running out of binder buffer space",意思是 Binder 传输小的数据量情况下也可能失败,原因有两个:第一是远程进程可能已经死亡,Binder 驱动找不到远程进程对应的 mmap 虚拟内存,也就无法完成 Binder 数据传输;第二是远程进程还存活,但是它的 Binder mmap 接收缓冲区已经被用完,无法分配空闲的 mmap 虚拟内存,也就无法完成 Binder 数据传输。

通过对错误消息分析,我们得出结论:

  • IBinder transact 函数返回 FAILED_TRANSACTION 错误码,且 parcelSize > 200*1024 的情况下,会抛出 TransactionTooLargeException。

BinderProxy.transactNative 流程

前面我们在 signalExceptionForError 函数中读取这个 FAILED_TRANSACTION 错误码,接下来我们要知道其写入的地方在哪里,以便理顺数据处理流程。

通过关键字查找,我们发现这个 FAILED_TRANSACTION 错误码只在 IPCThreadState::waitForResponse 函数中写入。对应有两种情况会写入:

  • BR_FAILED_REPLY 回复失败

  • BR_FROZEN_REPLY 冻结回复

这两个 cmd 对应的值是从 mIn.readInt32 读取出来,也就是 Parcel 中读取出来的命令值。

Image

我们依然循着错误码传递路径来到 Binder 驱动层。

BR_FAILED_REPLY 错误码在 binder 驱动中有 40 个引用,过于宽泛,没有唯一性,导致继续使用这个排查方向的 ROI 不高,因此暂停这个排查方向。

Image

BR_FROZEN_REPLY 错误码在 binder 驱动中有 1 个引用,指进程/线程被冻结情况,主要用于区分进程死亡时 Binder 无法回复 BR_DEAD_REPLY。我们这个 Activity stop 场景用户大概率还在前台,App 和 System Server 不太可能出现进程被冻结,因此暂停这个排查方向。

Image

除了错误码之外,我们结合源码得出 BinderProxy.transactNative 执行流程如下:

Image
  • Java 层 BinderProxy.transactNative 调用 Native 层 android_os_BinderProxy_transact。

  • 数据经过 IBinder::transact、BpBinder::transact 函数调用到达 IPCThreadState::transact。

  • IPCThreadState::writeTransactionData 先把待传输数据写入 Parcel 。

  • IPCThreadState::waitForResponse 负责和 Binder 驱动通讯进行数据发送和接收,并处理接收的命令。

  • IPCThreadState::talkWithDriver 负责和 Binder 驱动通讯,关键是通过 ioctl 系统调用进入内核处理。

PendingTransactionActions$StopInfo.run 流程

如前文所述,我们分析了崩溃类型 TransactionTooLargeException、崩溃消息 data parcel size xx bytes 和堆栈最底层函数 BinderProxy.transactNative 数据处理流程,得出的结论是:该崩溃是 Binder 传输数据超过 200 * 1024 时出现的崩溃。

由于 BinderProxy.transactNative 是底层通用函数,它的调用方有很多,我们需要快速确定这个崩溃场景最大的 Context 是在哪个里面,所以我们直接跳到分析主线程消息处理里面业务根消息 PendingTransactionActions$StopInfo.run 的分析。

Image

通过下图源码,我们发现 App 进程通过 Binder IPC 通知 System Server 进程的 AMS Activity Stop 信息时,有个 catch RemoteException 异常捕获逻辑。

通过先打印 Bundle 统计数据,后进行异化逻辑处理,我们看到 targetSdk < 24 情况下,程序打印了一行日志 App sent too much data in instance state, so it was ignored,然后 return 返回,并没有执行 throw 异常。所以推测系统在这种情况下其实不崩溃。

Image

使用 APMPlus 的 App 监控统计,我们分析到 6/7/10 系统没有崩溃,这个和源码逻辑可以匹配上。在 targetSdk >= 24 之后,把捕获的异常重新通过 throw 关键字抛出,这种情况下会出现崩溃。经过回溯 StopInfo 中的 Bundle 来自 Activity.onSaveInstanceState :

Image

因此,我们得出崩溃场景是:

Image

  • ActivityThread.handleStopActivity 过程中保存 callActivityOnSaveInstanceState 产生的 Bundle 数据。

  • 在主线程异步消息执行 StopInfo.run 告诉 AMS activityStopped 信息过程中,传递 Bundle 数据用于未来恢复 Activity 状态。

Activity.onRestoreInstanceState 流程

前面我们发现崩溃场景 Binder 传输数据和 Activity.onSaveInstance 的 Bundle 数据有关,接下来我们分析这个 Bundle 数据恢复处理流程。

Image

  • Activity 被销毁重建场景,在 ActivityThread.performLaunchActivity 函数中,Bundle 会跟随新 Activity 实例创建从 AMS 传递到 App 进程内。

  • ActivityThread.performLaunchActivity 中,Bundle 是从 AMS 进程序列化传递到 App 进程,所以其中的类需要在 App 进程进行反序列化,重新进程类加载和类初始化,所以 Bundle.setClassLoader 用于 App 进程内 Bundle 数据反序列化。

  • 接下来通过 Instrumentation 传递 Bundle 到 Activity.onCreate 函数,在这里是 Activity 接收到 Bundle 最早的地方,Activity 可以从这里开始访问被恢复的 Bundle 数据。

  • 在 ActivityThread.handleStartActivity 处理 start 时,也会通过 Activity.onRestoreInstanceState 函数用于恢复 Activity 状态。

TransactionTooLargeException 直接原因

经过前面分析,我们知道 Binder 传输数据量超限是由于 Activity.onSaveInstance 保存的 Bundle 过大,但多大的 Bundle 会导致崩溃呢?200K、500K、1M……?

为了分析崩溃执行过程,我们在 demo App 中 Activity.onSaveInstanceState 保存大数据,并使用 root 机器分析崩溃场景日志:

Image
// 4692 是发送方 app 进程 pid,1838 是接收方 system server 进程 pid。接收方接收数据时,遇到分配 4198824 虚拟内存失败,没有足够的虚拟地址空间4692  4692 E binder_alloc: 1838: binder_alloc_buf size 4198824 failed, no address space4692  4692 E binder_alloc: allocated: 2240 (num: 8 largest: 2080), free: 1038144 (num: 4 largest: 1036672)4692  4692 I binder  : 4692:4692 transaction failed 29201/-28, size 4198812-8 line 33174692  4692 E JavaBinder: !!! FAILED BINDER TRANSACTION !!!  (parcel size = 4198812)

结合源码,可知 BinderProxy.transactNative 执行流程是这样:

Image

  • Java 层 BinderProxy.transactNative 调用 Native 层 android_os_BinderProxy_transact 。

  • 数据经过 IBinder::transact、BpBinder::transact 函数调用到达 IPCThreadState::transact 。

  • IPCThreadState::writeTransactionData 先把待传输数据写入 Parcel 。

  • IPCThreadState::waitForResponse 负责和 Binder 驱动通讯进行数据发送和接收,并处理接收的命令。

  • IPCThreadState::talkWithDriver 负责和 Binder 驱动通讯,关键是通过 ioctl 系统调用进入内核 binder_ioctl 处理。

  • binder_thread_write 负责向目标进程写入 Binder 传输数据,binder_thread_read 负责读取其它进程发送给当前进程的 Binder 传输数据。

  • 进程间传输数据走到公用函数 binder_transaction,通过函数参数 intreply识别区分是否是发送数据还是接收数据。

  • 在往一个进程写入传输数据之前,需要通过 binder_alloc_new_buf_locked 函数先申请合适大小的 Binder 接收缓冲区,如果分配失败,则无法完成数据传输。

Image

因此,TransactionTooLargeException 直接原因是接收方进程 Binder mmap 接收缓冲区无法分配足够大的空闲内存。

Binder 传输数据最大值

前面我们只传输了约 500K 大小的数据就遇到了 TransactionTooLargeException 问题。那么

  • 是否 Binder mmap 接收缓冲区大小就是小于 500K 呢?

  • 是否最大缓冲区其实超过 100M,只是被其它 Binder 调用分配用完了呢?

通过 Binder 驱动底层源码分析,单进程 Binder mmap 最大值 4MB,但是还受到参数 vma 控制。

Image

而从整个 Binder 架构初始化流程分析,这个参数来自 ProcessState 里面,最大值是 1M - 8K。

这两个值里面取最小值作为 Binder 传输数据最大值。

Image

因此,一个进程 Binder 接收缓冲区最大是 1M-8K。

问题总结

经过前面的深入分析,我们对这个问题得出更加清晰的结论:

  • 问题是在 App 进程向 system_server 进程发送  Activity.onSaveInstanceState 保存的 Bundle 数据时,驱动层在 system_server 的 Binder 接收缓冲区(大小上限 1M-8K)中分配不出用于接收 Bundle 大小的内存,导致 Binder 发送失败

  • Bundle 数据用于 Activity 页面销毁重建时恢复页面状态,其来源包含 framework 和 App 层保存的数据。当页面过于复杂或者很重场景下可能会出现超限问题

  • 从日志和调用栈无法定位/拆解出 Bundle 中什么数据过大导致的崩溃,需要有归因和解决工具

解决方案

优化思路

前面我们分析得知崩溃场景是 Activity stop 和 start 时和 AMS 传递的 Bundle 过大流程:

Image

  • App 进程内通过 Activity.onSaveInstance 方法把数据放到 Bundle 中

  • 通过 Binder Driver 提供的接口 App 把 Bundle 写入 System Server 进程的 mmap 内存

  • 接下来 Activity 重建/启动时,System Server 再把保存的 Bundle 数据转发给 App 进程内 Activity

按照这个数据处理流程,一旦 Bundle 超过进程 Binder 接收缓冲区大小,就会触发 Binder 传输失败。

@Overrideprotected void onSaveInstanceState(Bundle outState) {    super.onSaveInstanceState(outState);    // 需要 hook 拦截处理 outState}
@Overrideprotected void onRestoreInstanceState(Bundle savedInstanceState) { // 需要 hook 拦截处理 savedInstanceState super.onRestoreInstanceState(savedInstanceState);}

因此,结合问题场景和现状,我们的优化思路是:

  • 由于目前没有太好的办法在运行时应用态直接修改 Binder 驱动层内核态的执行指令,因此主要从上层 App 应用态入手解决问题

  • 在 onSaveInstanceState 之后把 outState 缩小到小于底层 Binder 传输的阈值

  • 在 onRestoreInstanceState 之前把 savedInstanceState 扩大到原始大小给业务层使用

Hook 点

通过阅读源码和断点调试,我们发现一个合适的 hook 类 ActivityLifecycleCallbacks,其能满足我们的诉求:

  • onActivityPostSaveInstanceState 正好是在 onSaveInstanceState 之后执行,而且能拿到 outState 参数

    Image

  • onActivityPreCreated 正好是在 onRestoreInstanceState 之前执行,而且能拿到 savedInstanceState 参数

    Image

所以我们通过 Application.registerActivityLifecycleCallbacks 能够拿到合适的执行时机。

缩小 Binder 传输 Bundle

为了让 Binder 读取到的 Bundle 数据量够小,从而避免崩溃,我们需要知道它具体要小到什么程度?比如:

  • 完全不执行 onSaveInstanceState 函数,这样也就不会超过 Binder 阈值

  • onSaveInstanceState 中主动调用 clear 函数清理 outState 数据,这样也不会超过 Binder 阈值

  • 把 outState 数据替换为一个小的 KEY 数据,而且建立 KEY-outState 一一对应的 Map 结构,方便查找还原 Bundle

为了尽量不影响系统本身的 save 和 restore 逻辑,我们选择传输 KEY 数据缩小 Bundle 数据。其中依赖的 KEY 我们使用 UUID 生成方式。

public void onActivityPostSaveInstanceState(Activity activity, Bundle outState)     outState.clear();    String uuid = UUID.randomUUID().toString();    sKey2ContentMap.put(uuid, outState);    outState.putString("TransactionTooLargeOptKey", uuid);}
还原 Binder 传输 Bundle

前面我们有了 KEY 和 Map 结构,现在我们只需要根据 KEY 查找对应的 Bundle 数据进行还原:

public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {    if (savedInstanceState != null) {        Object uuid = savedInstanceState.get("TransactionTooLargeOptKey");        Bundle bundle = sKey2ContentMap.get(uuid);        if (bundle != null) {            savedInstanceState.clear();            savedInstanceState.putAll(bundle);        }    }}
功能、性能和稳定性折中

有了整体稳定性优化数据处理流程之后,我们发现维护的 Map 结构需要一定的空间开销,可能包括内存或者外存。

一方面,假如我们需要跟系统 save 和 restore 行为保持一致,即冷启动之后还可以恢复 Activity 状态,那么需要把 Bundle 信息保存在外存。而外存一般只能保存文本/二进制数据,这就需要把 Bundle 进行转换成二进制,并且涉及到一些 io 操作,很可能影响性能。

另一方面,我们对抖音主要 Activity 进行销毁重建,发现 Activity 并不能很完美的支持恢复能力。

所以,基于以上两点,我们决定 Map 结构不涉及的 io,只做内存级别。

前面我们的流程中针对所有 Bundle 都进行了优化,可能它就没有超过 Binder 的阈值。因此,为了精准识别问题场景,我们希望对超过一定大小的 Bundle 才进行优化。所以我们使用 Parcel 对 Bundle 进行了序列化量化:

private static byte[] bundle2Bytes(Bundle outState) {    Parcel parcel = Parcel.obtain();    try {        parcel.writeBundle(outState);        return parcel.marshall();    } catch (Throwable t) {        t.printStackTrace();    } finally {        parcel.recycle();    }    return null;}

把 Bundle 序列化为 byte[] 之后,我们限制大小超过 1024 Bytes 才进行优化。

假如用户不断触发 save 操作,那么很可能会挤爆 Map 内存。所以我们需要有个 Map 容量上限和清理逻辑。这里初步设置容量 64,超限之后清理掉最久不用的记录:

if (sKey2ContentMap.size() > 64) {    Set<Map.Entry<String, byte[]>> entries = sKey2ContentMap.entrySet();    List<String> removeKeys = new ArrayList<>();    // 清理掉前 20%,留下后面 80%    final int removeCount = (int) (64 * 0.2);    for (Map.Entry<String, byte[]> entry : entries) {        removeKeys.add(entry.getKey());        if (removeKeys.size() >= removeCount) {            break;        }    }    for (String firstKey : removeKeys) {        sKey2ContentMap.remove(firstKey);    }}
Demo 验证

在 demo App 中,我们尝试往 Bundle 中写入超过 Binder 传输阈值的 16 MB 数据,预期在 restore 中能读取到相同数据:

@Overrideprotected void onSaveInstanceState(Bundle outState) {    super.onSaveInstanceState(outState);    byte[] bytes = new byte[16 * 1024 * 1024]; // 16MB    outState.putByteArray("key", bytes);    System.out.println("onSaveInstanceState bytes.length = " + bytes.length);}
@Overrideprotected void onRestoreInstanceState(Bundle savedInstanceState) {    super.onRestoreInstanceState(savedInstanceState);    byte[] bytes = savedInstanceState.getByteArray("key");    System.out.println("onRestoreInstanceState bytes.length = " + bytes.length);}

在机器中开启开发者选项中的【不保留活动】选项,测试结果显示两行日志 bytes 数组长度一致,说明方案可行。

Image
最终方案

经过前面层层分析和逐步优化,我们的最终方案是:

  • 启动之后通过 Application#registerActivityLifecycleCallbacks 注册 Hook 点函数,从而拿到 save 和 restore 执行时机和参数。

  • 在 ActivityLifecycleCallbacks#onActivityPostSaveInstanceState 时把 Bundle 序列化为二进制,当大小超过 1024 Bytes 时创建 UUID 并在内存记录 UUID 和 Bundle 映射关系,替换 Bundle 数据为 ID,从而减小 Binder 看到的数据量。

  • 在 ActivityLifecycleCallbacks#onActivityPreCreated 时从 Bundle 中读取出 UUID,并从内存 Map 中查找出映射的 Bundle,还原 ID 为 Bundle 数据。

Image

上线效果

单个 issue 修复效果:优化方案已经解决 97% 的问题。

剩余三星 startActivity 兼容性问题,已经不是 Top 问题。由于涉及启动 Activity 多进程数据共享问题,故此次未做优化

接入方式

目前该方案已经在抖音全量上线,并集成到火山引擎 APMPlus。为了方便更多企业和用户快速接入,该能力开箱即用,用户只需在初始化时打开优化开关即可,具体接入方式如下,详见文档:www.volcengine.com/docs/6431/68852。
MonitorCrash.Config config = MonitorCrash.Config.app(APM_AID)        .enableOptimizer(true);//打开稳定性优化开关        .build();

总结和思考

系统层

对于系统服务端 system_server 进程,其会接收众多 App 进程发送的 Binder 数据,Binder 传输数据量上限限制和 App 进程一样,可能是不太合理的。随着手机内存发展,这个数值在内存充足的机器上或存在优化空间,例如扩容到 2M 之后,传输失败率下降,App 使用时长增加。

业务层

跨进程场景 Bundle 和 Intent 中不适合传递大数据,可以经过外存中转,通过维护外存 Map 和传递 Key 解决;另外 Bundle 中不建议放受到后台 Server 活动影响较大的序列化数据结构,这种情况平时没有问题,一到线上活动就容易崩溃,在活动上线前需要多加演练。

关于 APMPlus

应用性能监控全链路版 APMPlus 是火山引擎旗下的云原生可观测产品,通过先进的数据采集与监控技术,为企业提供全链路的应用性能监控服务,助力企业提升异常问题排查与解决的效率。

基于海量数据的聚合分析,平台可帮助客户发现多类异常问题,并及时报警,做分配处理。目前,APMPlus 已服务了抖音、今日头条等多个大规模移动 App,以及作业帮、Keep、德邦等众多外部业务方。

Image

APMPlus APP 端监控方案提供了 Java 崩溃、Native 崩溃、ANR 等不同的异常类别监控,以及启动、页面、卡顿等流畅性监控,还包括内存、CPU、电量、流量、磁盘等资源消耗问题的监控。此外,APMPlus 提供网络耗时和异常监控,拥有强大的单点分析和日志回捞能力。在数据采集方面,提供采样和开关配置,以满足客户对数据量和成本控制的需求,并支持实时报警能力。针对跨平台方案,其提供了 WebView 页面的监控。丰富的能力满足客户对 App 全面性能监控的诉求。

方案亮点
  • Java OOM 监控提供全流程自动分析能力,准确定位 Java 内存问题,泄漏链、泄漏大小、大对象一目了然。

  • ANR 使用基于信号的捕获方案,更节省系统资源,准确度高,提供现场消息调度图,高度还原现场主线程阻塞情况。

  • 真正解决 Native(C/C++)崩溃的现场还原能力,提供了最有价值的 Tombstone,精细还原现场。完整展示崩溃线程的进程信息、信号信息、寄存器信息,还原崩溃现场汇编指令,详细的 maps、fd 和内存信息。

  • 高性能日志库,做到数据稳定性强、性能好,保障了现场业务信息的高度还原。

Image
Image

扫码咨询

- END -
相关链接
[1] 火山引擎: www.volcengine.com
[2] 火山引擎 APMPlus: www.volcengine.com/product/apmplus
火山引擎云原生团队

火山引擎云原生团队主要负责火山引擎公有云及私有化场景中 PaaS 类产品体系的构建,结合字节跳动多年的云原生技术栈经验和最佳实践沉淀,帮助企业加速数字化转型和创新。产品包括容器服务、镜像仓库、分布式云原生平台、函数服务、服务网格、持续交付、可观测服务等。

Image
Image

Image