标签: C++

  • 排查 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 没有正确初始化异常捕获对象,造成了野指针问题,这才让整个进程在异常报错出后产生段错误崩溃。