排查 duckdb-node-neo 并发查询时的随机段错误

问题的说明跟复现请看这里,这篇文章主要记录一下如何排查 node addon 代码引发的问题。

保存进程崩溃的翻车现场

在 Linux/KDE 环境下,当应用进程崩溃的时候,systemd 会记录下该进程的翻车现场(aka,coredump)。我们可以使用 coredumpctl 这个工具查看当前机器上发生过的 coredump。

  List all captured core dumps:

      coredumpctl

  List captured core dumps for a program:

      coredumpctl list program

  Show information about the core dumps matching a program with `PID`:

      coredumpctl info PID

  Invoke debugger using the last core dump:

      coredumpctl debug

  Invoke debugger using the last core dump of a program:

      coredumpctl debug program

  Extract the last core dump of a program to a file:

      coredumpctl --output path/to/file dump program

  Skip debuginfod and pagination prompts and then print the backtrace when using `gdb`:

      coredumpctl debug --debugger-arguments "-iex 'set debuginfod enabled on' -iex 'set pagination off' -ex bt"

还原翻车现场

通过上面的步骤获取到 coredump 后,可以搭配 debugger 工具跟 symbol file 定位出问题的代码。我选择的 debugger 工具是 gdb 搭配 CLion 作 UI。这样只要将 node addon 项目用 CLion 打开,并导入 coredump 后,就可以获得一个功能齐全的 GUI Debugger。

用 CLion 打开 node addon 项目

以 duckdb-node-neo 为例,这个项目使用 node-gyp 作为构建工具,而 CLion 只支持 CMAKE 工程,所以需要手动创建一份 CMakefile 文件,让 IDE 的智能提示正常工具,方便在 debug 的过程中查看定义、搜索引用等。

下面的这份 CMake 文件是我跟 Claude 一起编写的,不一定能够通过 IDE 编译 node addon,但是可以提供智能补全。

cmake_minimum_required(VERSION 3.10)
project(duckdb_node_neo)

set(CMAKE_CXX_STANDARD 17)

# include node headers
include_directories(/usr/include/node)

# Node.js addon API headers
include_directories(${CMAKE_SOURCE_DIR}/bindings/node_modules/node-addon-api)

# DuckDB headers
include_directories(${CMAKE_SOURCE_DIR}/bindings/libduckdb)

# Add source files
file(GLOB SOURCE_FILES
    "bindings/src/*.cpp"
)

# This is just for CLion's code intelligence - not for actual building
add_library(duckdb_node_bindings SHARED ${SOURCE_FILES})

# Define preprocessor macros used in the code
add_definitions(-DNODE_ADDON_API_DISABLE_DEPRECATED)
add_definitions(-DNODE_ADDON_API_REQUIRE_BASIC_FINALIZERS)
add_definitions(-DNODE_API_NO_EXTERNAL_BUFFERS_ALLOWED)

# Add Node.js specific definitions to help CLion's IntelliSense
add_definitions(-DNAPI_VERSION=8)

# Tell CLion to treat this as a Node.js addon project
set_target_properties(duckdb_node_bindings PROPERTIES
    CXX_STANDARD 17
    PREFIX ""
    SUFFIX ".node"
)

生成带有 symbol 的二进制

设置后 debugger 后,还需要提供一个 symbol 文件,帮助 debugger 定位代码位置。对于 node-gyp 而言,只需要在编译时设置 --debug 参数即可生成带有 symbol 的二进制。

cross-replace node-gyp configure --verbose --arch=$TARGET_ARCH --debug && node-gyp build --verbose --arch=$TARGET_ARCH --debug

接下来我们只要用带有 symbol 的二进制复现进程崩溃的问题,就可以得到一个方便 debug 的 coredump。

定位问题

通过 debug coredump 文件,可以很容易找到抛出错误的地方,但这次很不走运的是, error 变量是 NULL,没有提供任何有价值的信息。另外,可以发现 result_ptr_ 似乎处于一个没有被初始化的状态。

查看 duckdb 的源码后发现,虽然 duckdb 对外提供的是 C API,但是其本身是用 C++ 开发的,在将 C++ 操作以 C API 的形式暴露时,duckdb 会捕获内部的异常,然后将其中的错误信息写入到 duckdb_result 中。

目前 coredump 表明 result_ptr_ 未被 duckdb 正常初始化,而异常对象很可能已经在跳出 catch 作用域时被销毁,现在只能在运行时 debug 才能捕获到具体的错误信息了。

附加到进程上 debug

CLion 提供了非常好用的附加到未启动进程上 debug 的功能,只需要向 CLion 提供进程的 commandline,它就可以自动将 debugger 附加到该进程上。

在开始 debug 之前,还需要设置一下 C++ 异常断点:

当启动问题进程,并复现崩溃后,就可以在 debugger 视图查看到实时的异常对象信息:

这里的 obj 就是异常对象,但是由于 CLion 没有识别出异常的类型,我们得手动用 gdb 指令查看 obj 的信息。

通过 gdb 的输出,可以看到异常对象的类型是 duckdb::InvalidInputException

这时在变量页面输入下面的指令就行:

*(duckdb::InvalidInputException*)obj

终于,更加具体的错误信息被挖掘出来了~

后续

这个问题的根因是 duckdb 不支持在同一个 connection 对象上执行高并发查询,如果需要高并发,则应该为每个查询单独创建 connection,否则将会随机抛出异常 InvalidInputException

@duckdb/node-api 没有正确初始化异常捕获对象,造成了野指针问题,这才让整个进程在异常报错出后产生段错误崩溃。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Index