PThread OOM 终局之战 | 吹爆Matrix

3,838 阅读5分钟

终局之战

之前已经和大家聊过两次pthread oom问题了,去年下半年我也抄袭完成了最后一部分了,今天就和大家简单的聊一下看看吧。

我们观察过线上的实际pthread oom的崩溃问题,发现有些情况下,设备的java的线程数量其实也就200-300之间,并没有到崩溃的边缘。但是实际情况就是还是崩了,所以我们怀疑是native的线程构造导致的崩溃。

既然有了想法,我们也就去网上开始找寻相关的资料以及别的大厂的方案了,对于native层面的线程hook能力。

无限宝石

这次就让我们像灭霸一样来收集无限宝石,虽然可能就只有几颗,但是力求能打个响指,解决这个蛋疼的问题。

我去年年中的时候发现matrix更新了2.0版本,我发现了他更新了一个叫matrix-hooksmodule,看到名字觉得很有意思也就点进去看了下,就发现了其中的pthread hook的能力。

xhook

因为之前也简单的介绍过了,就不过多展开了,作者蔡克伦现在去了字节更新了bhook,大佬牛逼(破音)。

这部分原理可以参考文章 字节跳动开源AndroidPLThook方案bhook

小见解

我个人认为哦,这部分其实就是 ELF(Executable and Linkable Format)格式的文件,虚拟机在加载so的时候,会根据ELF来定位到具体的so内的函数调用,那么通过这种机制,我们就可以用这套机制来将so内的方法替换成我们的中间方法就可以做到代码的hook了。

pthread hook

这部分我还没开始写方案的时候就考虑过用xhook的方式。类似之前做的iocanary的功能,当时是通过xhook替换io的打开读写关闭方法来做到监控的,而这次则是要找到线程创建的点就行了,然后获取到对应的堆栈,之后在收集一些别的相关的我们就可以完成我们想要的功能了。

恰巧这次在matrix-hooks中也是用这种方式去实现的,白嫖一时爽,一直白嫖一直爽,哈哈哈。

源代码地址

void InstallHooks(bool enable_debug) {
    LOGI(LOG_TAG, "[+] Calling InstallHooks, sThreadTraceEnabled: %d, sThreadStackShinkEnabled: %d",
         sThreadTraceEnabled, sThreadStackShrinkEnabled);
    if (!sThreadTraceEnabled && !sThreadStackShrinkEnabled) {
        LOGD(LOG_TAG, "[*] InstallHooks was ignored.");
        return;
    }

    FETCH_ORIGIN_FUNC(pthread_create)
    FETCH_ORIGIN_FUNC(pthread_setname_np)

    if (sThreadTraceEnabled) {
        thread_trace::thread_trace_init();
    }

    matrix::PauseLoadSo();
    {
        int ret = xhook_export_symtable_hook("libc.so", "pthread_create",
                                             (void *) HANDLER_FUNC_NAME(pthread_create), nullptr);
        LOGD(LOG_TAG, "export table hook sym: pthread_create, ret: %d", ret);
        ret = xhook_export_symtable_hook("libc.so", "pthread_setname_np",
                                         (void *) HANDLER_FUNC_NAME(pthread_setname_np), nullptr);
        LOGD(LOG_TAG, "export table hook sym: pthread_setname_np, ret: %d", ret);
        xhook_register(".*/.*\\.so$", "pthread_create",
                       (void *) HANDLER_FUNC_NAME(pthread_create), nullptr);
        xhook_register(".*/.*\\.so$", "pthread_setname_np",
                       (void *) HANDLER_FUNC_NAME(pthread_setname_np), nullptr);
        xhook_enable_debug(enable_debug ? 1 : 0);
        xhook_enable_sigsegv_protection(enable_debug ? 0 : 1);
        xhook_refresh(0);
    }
    matrix::ResumeLoadSo();
}

上述代码就是讲native的线程构造的函数进行替换,当pthread_create还有pthread_setname_np方法被触发的情况下,替换成另外一本地的native的代码。

因为代码被hook了所以接下来就能转换很多自己想要的操作完成之后再调用原始的代码了。

Wechat-Backtrace

如果当前线程构造是java,我们通过当前线程的堆栈来进行打印,那么native的如何获取堆栈呢?

这部分微信大佬们在matrix内开源了一套高性能,基于 Wechat-Backtrace进行快速 unwind 堆栈。

这部分代码我其实没看(主要也是看不懂,这部分我是真的菜),但是我在使用过程中发现对这部分堆栈数据进行信任的。

matrix-backtrace 仓库地址

struct pthread_meta_t {
    pid_t tid;
    char *thread_name;
//    char  *parent_name;
    wechat_backtrace::BacktraceMode unwind_mode;

    uint64_t hash;

    wechat_backtrace::Backtrace native_backtrace;

    std::atomic<char *> java_stacktrace;

    pthread_meta_t() : tid(0),
                       thread_name(nullptr),
//                       parent_name(nullptr),
                       unwind_mode(wechat_backtrace::FramePointer),
                       hash(0),
                       native_backtrace(BACKTRACE_INITIALIZER(m_pthread_backtrace_max_frames)),
                       java_stacktrace(nullptr) {
    }

    ~pthread_meta_t() = default;

    pthread_meta_t(const pthread_meta_t &src) {
        tid = src.tid;
        thread_name = src.thread_name;
//        parent_name       = src.parent_name;
        unwind_mode = src.unwind_mode;
        hash = src.hash;
        native_backtrace = src.native_backtrace;
        java_stacktrace.store(src.java_stacktrace.load(std::memory_order_acquire),
                              std::memory_order_release);
    }
};

但是从最终获取到的堆栈数据来看,大概也能看出来一部分线程创建的信息。

(null) (+0);(null) (+0);_ZN7android6Thread3runEPKcim (+224);_ZN7android12ProcessState17spawnPooledThreadEb (+220);(null) (+0);(null) (+0);(null) (+0);

可以看得出来,native的堆栈的回溯能力还是很不错的,而且速度也足够的快。到这里我们大概已经能完成数据收集的操作了。

PthreadHook 简单的使用

这部分我参考了下官方的demo,但是不同的地方在于,我在启动完成之后就进行了dump操作,开始线程数据收集。

等到java线程堆栈数量超过一定阈值的时候,开始分析pthread的json文件,然后将数据分段上报,然后观察阈值情况下的线程状况是怎么样的。

这部分代码上传了dokit,作为debug组件的一部分提供给dokit了。

try {
          val config = ThreadStackShrinkConfig()
              .setEnabled(true)
              .addIgnoreCreatorSoPatterns(".*/app_tbs/.*")
              .addIgnoreCreatorSoPatterns(".*/libany\\.so$")
          PthreadHook.INSTANCE
              .addHookThread(".*")
              .setThreadStackShrinkConfig(config)
              .setThreadTraceEnabled(true)
              .enableQuicken(true)
          PthreadHook.INSTANCE.hook()
      } catch (e: HookFailedException) {
          e.printStackTrace()
      }

这部分就是pthread hook的初始化逻辑,因为考虑到plthook的原因,所以以debug组件能力提供给测试包使用,虽然测试环境下设备量较小,但是也能分析出一定量的问题。

class AutoDumpListener : Application.ActivityLifecycleCallbacks {

    init {
        DoKit.APPLICATION.registerActivityLifecycleCallbacks(this)
    }


    private fun getThreadCount(): Int {
        return getAllThreads().size
    }

    private var count = 0

    /**
     * ps -p `self` -t
     * See http://man7.org/linux/man-pages/man1/ps.1.html
     */
    private fun getAllThreads(): MutableList<Thread> {
        var group = Thread.currentThread().threadGroup
        var system: ThreadGroup?
        do {
            system = group
            group = group?.parent
        } while (group != null)
        val count = system?.activeCount() ?: 0
        val threads = arrayOfNulls<Thread>(count)
        system?.enumerate(threads)
        val list = mutableListOf<Thread>()
        threads.forEach {
            it?.let { it1 -> list.add(it1) }
        }
        return list
    }

    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {

    }

    override fun onActivityStarted(activity: Activity) {

    }

    override fun onActivityResumed(activity: Activity) {

    }

    override fun onActivityPaused(activity: Activity) {

    }

    override fun onActivityStopped(activity: Activity) {
    }

    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {

    }

    override fun onActivityDestroyed(activity: Activity) {
        if (count > 3) {
            return
        }
        if (getThreadCount() > Threshold) {
            DoKit.APPLICATION.dump {
                forEach {
                  // 数据上报
                }

            }
        }
    }


    companion object {
        const val Threshold = 200
    }
}

然后注册一个全局的ActivityLifecycleCallbacks,当页面切换的时候获取当前java堆栈数量,当超过阈值的情况下,开始dump生成当前完整的堆栈状况,然后进行数据上报。

pthread hook 是在native 内存中先收集好每一次线程创建的情况,以及使用的线程数量信息以及调用堆栈等,当触发dump之后会开始基于native内存数据,生成对应的json文件,之后写入到文件中。我们可以通过反序列化json文件,之后进行一场情况回溯。


// 当前没有删除这部分文件 后续需要考虑的
fun Context.dump(invoke: MutableList<PThreadEntity>.() -> Unit = {}) {
    try {
        val parent = "$cacheDir/pthread"
        val output = "$parent/pthread_hook_${System.currentTimeMillis() / 1000L}.log"
        parent.createDirectory()
        PthreadHook.INSTANCE.dump(output)
        fdLimit()
        val pthreads = getJson(output).parserPThread()
        pthreads.forEach {
            Log.i(TAG, "pthread error :$it\n")
        }
        invoke.invoke(pthreads)
    } catch (e: Exception) {

    }
}

fun String.createDirectory() {
    val file = File(this)
    if (!file.exists()) {
        file.mkdir()
    }
}

fun fdLimit() {
    Log.i(TAG, "FD limit = " + FDDumpBridge.getFDLimit())
}

fun getJson(path: String): String {
    val stream = File(path).bufferedReader().readText()
    return stream.apply {
        Log.i(TAG, "pThread:$this")
    }
}

const val TAG = "PThreadDumpHelper"

这部分就是dump的代码块,比较简单传入文件路径,之后调用jni方法,之后基本就会生成对应的json文件,完成反序列化操作。

配合Dokit

因为这个是个调试组件的功能,一部分波动的数据我们使用的是数据上报。另外我们也会提供一个主动的入口,让同学们可以自行查看下这部分异常数据。

这部分我认为Dokit还是非常非常好用的,毕竟可以自由的组合调试代码,方便自行对其进行拓展操作,而且不会发布到线上去。

Screenshot_20220210_104118.png

在下ui大湿岂是浪的虚名。

打个响指

其实这个东西吧,这几年断断续续的都在想如何长治久安的治理这个问题,毕竟当应用一旦变大之后,什么奇奇怪怪的现象都有可能会发生的。

我个人看法是这样的,解决问题其实并不是特别困难,真的难的事情是如何发现问题,定位问题。

更多的时候当我们看到线上崩溃的时候其实两眼一黑,满脑子应该都是我是谁,我在哪,我要干什么,宇宙的边界是哪里。如果有一个有效的工具或者别的手段能让我们快速的去定位问题的话,这个才是问题的关键。

这就是我的终局之战了。