gdb (ptrace) plus gdb_bthread_stack.py is slow and blocks the process. We need a cheaper way to trace a bthread call stack.
brpc's cooperative userland threads cannot do an efficient STW (Stop The World) the way Go's preemptive goroutines can, and the framework cannot interrupt user logic. Tracing a bthread stack is therefore hard.
Online tracing has to solve:
- Tracing a suspended bthread's call stack.
- Tracing a running bthread's call stack.
The current bthread status model:
To solve those two problems, this design implements STB (Stop The Bthread). While a bthread stack is being traced, the status must not move into a state that the current tracing method does not support. STB has two modes: context tracing and signal tracing.
Context tracing covers suspended bthreads. A suspended stack is stable. Using the context saved in TaskMeta.stack (on x86_64 the important registers are mainly RIP, RSP, RBP), a library that can unwind a given context walks the stack. A suspended bthread may wake at any time, run (including jump_stack), and then the stack keeps changing. An unstable context cannot be unwound, so scheduling is intercepted before jump_stack and the bthread only continues after tracing finishes. Context tracing therefore supports the ready and suspended states.
Signal tracing covers running bthreads. A running bthread is unstable, so TaskMeta.stack cannot be used. Instead a signal interrupts the bthread and the signal handler unwinds the stack. Signals bring two issues:
- Async-signal-safety.
- Signal tracing does not support
jump_stack. Unwinding needs register state, andjump_stackmutates registers, so interruptingjump_stackis unsafe. Scheduling is intercepted beforejump_stackand the bthread only suspends after tracing finishes.
So this mode only supports the running state.
jump_stack is on every path that suspends or runs a bthread, and it is the STB intercept point. STB splits states into three groups:
- Context-tracing states: ready, suspended.
- Signal-tracing states: running.
- Unsupported states. Neither method can unwind during
jump_stack. Scheduling is intercepted beforejump_stackand continues only after tracing finishes.
After STB, two intercept states are added on top of the original model: about-to-run and about-to-suspend.
STB flow:
- When TaskTracer (the STB module) receives a trace request, it marks tracing in progress. When tracing finishes, it marks completion and signals bthreads that may be in about-to-run or about-to-suspend. TaskTracer then branches on status:
- created, ready but no stack yet, destroyed: finish immediately.
- suspended, ready: context tracing.
- running: signal tracing.
- about-to-run, about-to-suspend: spin until the bthread moves to the next state (suspended or running), then continue.
- While TaskTracer is tracing, the bthread also branches on status:
- created, ready but no stack yet, ready: nothing extra.
- suspended, running: notify TaskTracer to continue.
- about-to-run, about-to-suspend, destroyed: wait on a condition variable until TaskTracer finishes. After that, TaskTracer wakes the bthread to continue
jump_stack.
- Install libunwind and abseil-cpp. Note: libunwind must be built from source. Do not use the distro package
libunwind-dev/libunwind-devel, or you will hit the crash in Known issue: libunwind and libgcc_s_Unwind_*symbol conflict. Bazel builds can skip this step and use the libunwind version maintained in the brpc repo. - Pass
--with-bthread-tracertoconfig_brpc.sh, or-DWITH_BTHREAD_TRACER=ONto cmake, or--define with_bthread_tracer=trueto bazel (Bzlmod). - Hit the builtin service
http://ip:port/bthreads/<bthread_id>?st=1, or callbthread::stack_trace()in code. - To trace a pthread, call
bthread::init_for_pthread_stack_trace()on that pthread to get a fakebthread_t, then use step 3.
Example output:
#0 0x00007fdbbed500b5 __clock_gettime_2
#1 0x000000000041f2b6 butil::cpuwide_time_ns()
#2 0x000000000041f289 butil::cpuwide_time_us()
#3 0x000000000041f1b9 butil::EveryManyUS::operator bool()
#4 0x0000000000413289 (anonymous namespace)::spin_and_log()
#5 0x00007fdbbfa58dc0 bthread::TaskGroup::task_runner()With bthread tracer enabled, you may see an occasional segfault on bthread_exit / pthread_exit or on a C++ exception path, with a stack like:
#0 0x0000000000000000 in ?? ()
#1 0x00007fa2b5d6458a in _ULx86_64_dwarf_find_proc_info ()
from /root/.cache/bazel/_bazel_root/743b333b2429a1dbd390ef66b59c771d/execroot/_main/bazel-out/k8-fastbuild/bin/test/../_solib_k8/libexternal_Slibunwind~_Slibunwind.so
#2 0x00007fa2b5d6668d in fetch_proc_info ()
from /root/.cache/bazel/_bazel_root/743b333b2429a1dbd390ef66b59c771d/execroot/_main/bazel-out/k8-fastbuild/bin/test/../_solib_k8/libexternal_Slibunwind~_Slibunwind.so
#3 0x00007fa2b5d681a1 in _ULx86_64_dwarf_make_proc_info ()
from /root/.cache/bazel/_bazel_root/743b333b2429a1dbd390ef66b59c771d/execroot/_main/bazel-out/k8-fastbuild/bin/test/../_solib_k8/libexternal_Slibunwind~_Slibunwind.so
#4 0x00007fa2b5d70cfd in _ULx86_64_get_proc_info ()
from /root/.cache/bazel/_bazel_root/743b333b2429a1dbd390ef66b59c771d/execroot/_main/bazel-out/k8-fastbuild/bin/test/../_solib_k8/libexternal_Slibunwind~_Slibunwind.so
#5 0x00007fa2b5d6c775 in __libunwind_Unwind_GetLanguageSpecificData ()
from /root/.cache/bazel/_bazel_root/743b333b2429a1dbd390ef66b59c771d/execroot/_main/bazel-out/k8-fastbuild/bin/test/../_solib_k8/libexternal_Slibunwind~_Slibunwind.so
#6 0x00007fa2b503c6df in __gxx_personality_v0 () from /lib/x86_64-linux-gnu/libstdc++.so.6
#7 0x00007fa2b5452ce5 in ?? () from /lib/x86_64-linux-gnu/libgcc_s.so.1
#8 0x00007fa2b54533c0 in _Unwind_ForcedUnwind () from /lib/x86_64-linux-gnu/libgcc_s.so.1
#9 0x00007fa2b4ca57a4 in __GI___pthread_unwind (buf=<optimized out>) at ./nptl/unwind.c:130
#10 0x00007fa2b4c9dd22 in __do_cancel () at ../sysdeps/nptl/pthreadP.h:271
#11 __GI___pthread_exit (value=0x0) at ./nptl/pthread_exit.c:36
#12 0x0000000000000000 in ?? ()
libunwind's src/unwind/*.c implements GCC's _Unwind_* ABI compatibility layer (_Unwind_GetLanguageSpecificData, _Unwind_ForcedUnwind, _Unwind_Resume, and so on), which exports the same global symbols as libgcc_s.so.1. When libunwind is linked as a shared library and appears before libgcc_s.so.1 in the final binary's DT_NEEDED list, the runtime linker ld.so resolves _Unwind_* calls from pthread_exit / exception handling to libunwind's DWARF implementation. That implementation needs an internal context that bRPC has not initialized on the pthread_exit path, so you get a null-pointer access.
This is an ELF runtime symbol-resolution-order issue. It is independent of the compiler (GCC / Clang). Clang's default runtime is also libstdc++ + libgcc_s, and it hits the same crash.
Important: do not use the distro libunwind (for example
apt install libunwind-dev,yum install libunwind-devel). Most distrolibunwind.sobuilds still export_Unwind_*in the dynamic symbol table, which triggers the crash in this section.You must use libunwind built from source. Upstream
./configure+makehides_Unwind_*as local via-Wl,--version-scriptby default, so they are not exported and the conflict goes away.
Recommended approach per build system:
| Build | Recommendation |
|---|---|
config_brpc.sh + make |
Build and install libunwind from source, then pass its include and lib dirs to config_brpc.sh |
cmake |
Build and install libunwind from source, then pass its include and lib dirs to cmake |
bazel (Bzlmod) |
Use the libunwind version maintained in the brpc repo |
Build and install libunwind into a private prefix (do not pollute the system), then point config_brpc.sh at that prefix.
# 1) Build libunwind from source (v1.8.1 or newer recommended)
git clone https://github.com/libunwind/libunwind.git
cd libunwind && git checkout tags/v1.8.1
mkdir -p /opt/libunwind
autoreconf -i
./configure --prefix=/opt/libunwind
make -j$(nproc) && make install
cd ..
# 2) Make config_brpc.sh use headers and libs under /opt/libunwind
# (do not let it pick up the system libunwind-dev)
cd brpc
sh config_brpc.sh \
--with-bthread-tracer \
--headers="/opt/libunwind/include /usr/include" \
--libs="/opt/libunwind/lib /usr/lib /usr/lib64"
make -j$(nproc)After the build, confirm libunwind.so does not export _Unwind_*:
nm -D /opt/libunwind/lib/libunwind.so | grep ' T _Unwind_' \
&& echo "WARN: _Unwind_* exported" \
|| echo "OK: _Unwind_* hidden"CMakeLists.txt looks up libunwind with find_library(... NAMES unwind unwind-x86_64). Build libunwind from source into a private prefix as in the make section, then prefer that prefix with CMAKE_PREFIX_PATH:
# 1) Build libunwind from source (same as the make section)
# 2) Make cmake search headers and libs under /opt/libunwind first
cd brpc
mkdir build && cd build
cmake -DWITH_BTHREAD_TRACER=ON \
-DCMAKE_PREFIX_PATH=/opt/libunwind \
..
make -j$(nproc)Tip: if
libunwind-devis already installed,find_librarymay still prefer/usr/lib. Pass-DLIBUNWIND_LIB=/opt/libunwind/lib/libunwind.so -DLIBUNWIND_X86_64_LIB=/opt/libunwind/lib/libunwind-x86_64.so -DLIBUNWIND_INCLUDE_PATH=/opt/libunwind/includeto force the self-built copy.
The brpc repo already maintains a Bzlmod overlay for libunwind under registry/modules/libunwind/, used via --registry=https://github.com/apache/brpc/registry in .bazelrc. The version uses a <base>.brpc-no-unwind suffix (for example 1.8.3.brpc-no-unwind) to distinguish it from the same base version on BCR. The overlay adds a switch:
--define libunwind_hide_unwind_symbols=true
When on, libunwind's src/unwind/*.c (the GCC _Unwind_* compatibility layer) is not compiled, matching upstream autoconf's default. bRPC only uses libunwind's native unw_* API (unw_getcontext, unw_init_local, unw_step, and so on) and does not need the _Unwind_* layer, so the switch is safe.
.bazelrc already turns this on for the build:test / test configs:
build:test --define libunwind_hide_unwind_symbols=true
test --define libunwind_hide_unwind_symbols=true
Add --define=with_bthread_tracer=true as in usage step 2:
# Tests: the test config in .bazelrc already includes the hide switch
bazel test //test:bthread_unittest
# Non-test (production) builds need both defines
bazel build --define=with_bthread_tracer=true \
--define=libunwind_hide_unwind_symbols=true \
//...Note: a production build that sets only
--define=with_bthread_tracer=trueand omits--define=libunwind_hide_unwind_symbols=truecan crash onpthread_exit/ exception paths.
After the build, confirm the libunwind shared library does not export _Unwind_*:
nm -D bazel-bin/external/_solib_*/libexternal*libunwind*.so 2>/dev/null \
| grep ' T _Unwind_' || echo "OK: no _Unwind_* exported by libunwind.so"signal_trace_timeout_ms: timeout for signal tracing, default 50ms.