Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

支持在线追踪bthread调用栈——STB(Stop The Bthread) #2797

Open
chenBright opened this issue Oct 26, 2024 · 5 comments
Open

支持在线追踪bthread调用栈——STB(Stop The Bthread) #2797

chenBright opened this issue Oct 26, 2024 · 5 comments
Labels
feature new feature

Comments

@chenBright
Copy link
Contributor

chenBright commented Oct 26, 2024

Is your feature request related to a problem? (你需要的功能是否与某个问题有关?)

相关isue:#2389

gdb(ptrace)+gdb_bthread_stack.py主要的缺点是要慢和阻塞进程,需要一种高效的追踪bthread调用栈的方法。

bRPC框架的协作式用户态协程无法像Golang内建的抢占式协程一样实现高效的STW(Stop the World),框架也无法干预用户逻辑的执行,所以要追踪bthread调用栈是比较困难的。

在线追踪bthread调用栈需要解决以下问题:

  1. 追踪挂起bthread的调用栈。
  2. 追踪运行中bthread的调用栈。

Describe the solution you'd like (描述你期望的解决方法)

bthread状态模型

以下是目前的bthread状态模型。

bthread状态模型

设计方案

核心思路

为了解决上述两个问题,该方案实现了STB(Stop The Bthread),核心思路可以简单总结为,在追踪bthread调用栈的过程中,状态不能流转到当前追踪方法不支持的状态。STB包含了两种追踪模式:上下文(context)追踪模式和信号追踪模式。

上下文(context)追踪模式

上下文追踪模式可以追踪挂起bthread的调用栈。挂起的bthread栈是稳定的,利用TaskMeta.stack中保存的上下文信息(x86_64下关键的寄存器主要是RIP、RSP、RBP),通过一些可以回溯指定上下文调用栈的库来追踪bthread调用栈。但是挂起的bthread随时可能会被唤醒,执行逻辑(包括jump_stack),则bthread栈会一直变化。不稳定的上下文是不能用来追踪调用栈的,需要在jump_stack前拦截bthread的调度,等到调用栈追踪完成后才继续运行bthread。所以,上下文追踪模式支持就绪、挂起这两个状态。

目前调研到,支持回溯指定上下文调用栈的库有gpertools、abseil-cpp和libunwind,其中:

  1. gpertools:对于已经使用cpu/heap profiler的用户,可以不引入额外的库满足回溯指定上下文调用栈的需求。但是在x86_64上测试验证,该场景下,gpertools 2.7版本的GetStackTraceWithContext/GetStackFramesWithContext的回溯结果还是当前调用位置的调用栈,不符合预期。此外,用户不一定使用tcmalloc(gpertools),可能使用jemalloc。
  2. abseil-cpp:最新版本要求C++14以上,早期版本支持C++11。实际效果未验证。
  3. libunwind:在x86_64上测试验证符合预期。没有其他依赖。提供了unw_set_reg函数用于设置寄存器,屏蔽了不同CPU架构下unw_context_t的差异。

综合考虑,选择libunwind

信号追踪模式

信号追踪模式可以追踪运行中bthread的调用栈。运行中bthread是不稳定的,不能使用TaskMeta.stack来追踪bthread调用栈。只能另辟蹊径,使用信号中断bthread运行逻辑,在信号处理函数中回溯bthread调用栈。使用信号有两个问题:

  1. 异步信号安全问题。libunwind回溯调用栈的函数是异步信号安全,可以满足需求。
  2. 信号追踪模式不支持jump_stack。调用栈回溯需要寄存器信息,但jump_stack会操作寄存器,这个过程是不安全的,所以jump_stack不能被信号中断,需要在jump_stack前拦截bthread的调度,等到bthread调用栈追踪完成后才继续挂起bthread。

所以,追踪模式只支持运行状态。

小结

jump_stack是bthread挂起或者运行的必经之路,也是STB的拦截点。STB将状态分成三类:

  1. 上下文追踪模式的状态:就绪、挂起。
  2. 支持信号追踪模式的状态:运行。
  3. 不支持追踪的状态。jump_stack的过程是不允许使用以上两种调用栈追踪方法,需要在jump_stack前拦截bthread的调度,等到调用栈追踪完成后才继续调度bthread。

详细流程

以下是引入STB后的bthread状态模型,在原来bthread状态模型的基础上,加入两个状态(拦截点):将运行、挂起中。

bthread STB状态模型

为了实现状态流转,在TaskMeta中增加一个butil::atomic<BthreadStatus> status,表示bthread状态,状态流转是原子的(CAS)。开始追踪的时候,status=-status,变成负值;结束的时候,status=-status,变会正值,即用status的最高位作为追踪的标识。

enum BthreadStatus {
    BTHREAD_STATUS_CREATED = 0,  // 创建
    BTHREAD_STATUS_READY,        // 就绪
    BTHREAD_STATUS_PRE_RUN,      // 将运行
    BTHREAD_STATUS_RUNNING,      // 运行
    BTHREAD_STATUS_SUSPENDING,   // 挂起中
    BTHREAD_STATUS_SUSPENDED,    // 挂起
    BTHREAD_STATUS_END,          // 销毁
};

经过上述分析,总结出STB的流程:

  1. stb(实现STB的一个模块)收到追踪bthread调用栈的请求时,标识正在追踪。追踪完成后,标识追踪完成,并stb调signal()通知可能处于将运行或者挂起中状态的bthread。根据bthread状态,stb执行不同的逻辑:
  • 创建、就绪但还没分配栈、销毁:直接结束追踪。
  • 挂起、就绪:使用上下文追踪模式追踪bthread的调用栈。
  • 运行:使用信号追踪模式追踪bthread的调用栈。
  • 将运行、挂起中:stb调wait()等到bthread状态流转到下一个状态(挂起或者运行),bthread调signal()通知stb继续追踪。注:这里只会阻塞stb线程,所以wait/signal使用pthread条件变量实现即可。
  1. stb追踪时,bthread根据状态也会执行不同的逻辑:
  • 创建、就绪但还没分配栈、销毁、就绪:不需要额外处理。
  • 挂起、运行:唤醒stb继续追踪。
  • 将运行、挂起中:bthread调wait()等到stb追踪完成并signal()后才继续执行jump_stack。注:这里竞争的概率比较小,wait/signal使用pthread条件变量实现即可。

性能

正常情况下,不会追踪bthread调用栈,该特性只是增加了记录状态流转的原子操作(CAS)。所以,可以先测试一下状态流转CAS的性能消耗:

  1. 纯框架调度场景:32个worker,起100个bthread,循环调bthread_yield,并且计数和计时,cpu没有明细变化,都是接近跑满32个核,CAS也没有导致耗时增加。注:bthread_yield会调度下一个bthread,并将当前bthread放到调度队列,消除挂起操作的影响,基本上可以认为CPU都是在执行框架调度逻辑了。
  2. rpc场景:使用multi_threaded_echo_c++,client、server的cpu核数和耗时都基本没有变化。

其他

  1. 同一时刻只能追踪一个bthread,即追踪的并发数最大为1。
  2. 目前只支持x86_64架构。后续逐渐支持其他CPU架构。

Describe alternatives you've considered (描述你想到的折衷方案)

Additional context/screenshots (更多上下文/截图)

@chenBright
Copy link
Contributor Author

@wwbmmm 有空帮忙评估一下

@wwbmmm
Copy link
Contributor

wwbmmm commented Oct 29, 2024

赞,这个功能打算怎么使用呢?通过内置服务查看调用栈?还是将栈打印到日志中?

@wwbmmm
Copy link
Contributor

wwbmmm commented Oct 29, 2024

综合考虑,选择libunwind

这个可以考虑做成编译开关,如果不用的话就不需要引入新的编译依赖

@chenBright
Copy link
Contributor Author

赞,这个功能打算怎么使用呢?通过内置服务查看调用栈?还是将栈打印到日志中?

通过BthreadsService查看调用栈。对于不适用内置服务的场景,可以调接口获取调用栈。

@chenBright
Copy link
Contributor Author

chenBright commented Oct 29, 2024

综合考虑,选择libunwind

这个可以考虑做成编译开关,如果不用的话就不需要引入新的编译依赖

嗯,是打算搞个编译开关,按需打开。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature new feature
Projects
None yet
Development

No branches or pull requests

2 participants