MoreRSS

site iconTXY | 谭新宇修改

清华本硕,对分布式系统和性能优化感兴趣。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

TXY | 谭新宇的 RSS 预览

Ray 编译踩坑记:老版本在老系统上的编译之路

2025-12-06 14:47:04

背景

Ray 官方提供了安装文档编译文档,涵盖了多种预构建方案——从稳定的发行版本、到日常构建版本,甚至支持主分支上任意 commit 的构建版本。在多平台、多 Python 版本和多芯片架构的组合下,这些预构建方案在大多数情况下都能满足需求。

但当涉及到在生产环境中维护 HotFix 版本时,从源码编译就成了必不可少的技能。特别是在一些企业环境中,你可能面临的是在 CentOS 8 这样的老系统上,基于 Ray 2.40.0 这样的较早版本进行代码改动和编译。官方的编译文档看起来步骤清晰,但当版本和系统都比较旧的时候,往往会踩到文档没有提及的坑。这些坑有的是版本兼容性问题,有的是系统环境的特殊性导致的,网络上也很难找到现成的解决方案。

面对这些问题,我在 macOS 和 CentOS 8 上编译 Ray 2.40.0 的实践中逐个排查和解决了这些问题,本文记录了其中的关键坑点和解法,希望能给未来有类似编译需求且遇到坑的同学一些帮助。

在 MacBook 上编译 Ray

我的开发环境是 MacBook M4 Pro。由于本地开发方便,因此我优先在 MacBook 上尝试编译了 Ray,优先探索老版本编译可能遇到的问题。

环境配置

参考 Ray 官方的编译文档即可。

  1. 克隆 Ray 仓库
    1
    2
    git clone [email protected]:ray-project/ray.git
    cd ray
  2. 安装 Bazel 编译环境。执行以下命令会通过 bazelisk 安装 v6.5.0 版本的 bazel 到 ~/bin/ 目录。需要手动将 ~/bin/ 加入 PATH 才能访问 bazel 命令。
    1
    2
    3
    4
    5
    6
    brew update
    brew install wget

    ci/env/install-bazel.sh
    echo 'export PATH="$PATH:~/bin"' >> ~/.bashrc
    exec bash
  3. 官网下载安装 Node.js,安装后确保 npm 和 node 命令可用即可。实测较高版本并不会导致编译失败,如果后续编译 dashboard 时遇到问题可回退到官方指定的 Node 版本。
  4. 官网下载安装 Anaconda。
  5. 使用 Anaconda 创建 Python 环境。为避免潜在的依赖不一致问题,建议 Python 版本与目标环境保持一致。
    1
    2
    conda create -n ray-compile python=3.11.9
    conda activate ray-compile

编译 2.52.1

为保证可复现性,我选择基于当前最新正式版本 2.52.1 而非 master 分支的最新 commit 进行编译。

注意:如果仅修改了 Python 文件,可参考官方文档直接替换 pip 中的 Python 文件即可,无需进行以下复杂的 C++ 编译。

  1. 切换到 2.52.1 版本
    1
    git checkout ray-2.52.1
  2. 编译 dashboard(约 3 分钟)
    1
    2
    3
    cd python/ray/dashboard/client
    npm ci
    npm run build
  3. 编译 Ray
    1
    2
    3
    4
    cd -
    cd python/
    pip install -r requirements.txt
    pip install -e . --verbose
  4. 编译成功,输出如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    > python git:(ray-2.52.1) pip install -e . --verbose
    Using pip 25.3 from /opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip (python 3.11)
    Obtaining file:///Users/xytan/Desktop/study/ray/python
    Running command installing build dependencies
    Using pip 25.3 from /opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip (python 3.11)
    Collecting setuptools>=40.8.0
    Obtaining dependency information for setuptools>=40.8.0 from https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl.metadata
    Using cached setuptools-80.9.0-py3-none-any.whl.metadata (6.6 kB)
    Using cached setuptools-80.9.0-py3-none-any.whl (1.2 MB)
    Installing collected packages: setuptools
    Successfully installed setuptools-80.9.0
    Installing build dependencies ... done
    Running command Checking if build backend supports build_editable
    Checking if build backend supports build_editable ... done
    Running command Getting requirements to build editable
    Getting requirements to build editable ... done
    Running command installing backend dependencies
    Using pip 25.3 from /opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip (python 3.11)
    ...
    ...
    ...
    Building editable for ray (pyproject.toml) ... done
    Created wheel for ray: filename=ray-2.52.1-0.editable-cp311-cp311-macosx_11_0_arm64.whl size=7592 sha256=95a5cacd0ec290dbca09851988ac9bb0de54c9ddedbee169e7fa8a84428b5e21
    Stored in directory: /private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-ephem-wheel-cache-6wzkmjte/wheels/3b/4a/f0/6edffb2ad8c786ba8990ff9495668d930965bc91921b146ea6
    Successfully built ray
    Installing collected packages: ray
    changing mode of /opt/anaconda3/envs/ray-compile/bin/ray to 755
    changing mode of /opt/anaconda3/envs/ray-compile/bin/serve to 755
    changing mode of /opt/anaconda3/envs/ray-compile/bin/tune to 755
    Successfully installed ray-2.52.1

编译 2.40.0

成功编译 2.52.1 后,下一步尝试编译 2.40.0 版本。

首先执行以下命令,预期编译能够顺利完成:

1
2
3
git checkout ray-2.40.0
pip install -r requirements.txt
pip install -e . --verbose

然而出现了以下报错:No module named pip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
...
...
...
running build_py
running build_ext
/opt/anaconda3/envs/ray-compile/bin/python3.11: No module named pip
Traceback (most recent call last):
File "/opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 389, in <module>
main()
File "/opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 373, in main
json_out["return_val"] = hook(**hook_input["kwargs"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 303, in build_editable
return hook(wheel_directory, config_settings, metadata_directory)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 468, in build_editable
return self._build_with_temp_dir(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 404, in _build_with_temp_dir
self.run_setup()
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 512, in run_setup
super().run_setup(setup_script=setup_script)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 317, in run_setup
exec(code, locals())
File "<string>", line 784, in <module>
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/__init__.py", line 115, in setup
return distutils.core.setup(**attrs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/_distutils/core.py", line 186, in setup
return run_commands(dist)
^^^^^^^^^^^^^^^^^^
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/_distutils/core.py", line 202, in run_commands
dist.run_commands()
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 1002, in run_commands
self.run_command(cmd)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/dist.py", line 1102, in run_command
super().run_command(command)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 1021, in run_command
cmd_obj.run()
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 139, in run
self._create_wheel_file(bdist_wheel)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 349, in _create_wheel_file
files, mapping = self._run_build_commands(dist_name, unpacked, lib, tmp)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 272, in _run_build_commands
self._run_build_subcommands()
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 299, in _run_build_subcommands
self.run_command(name)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/_distutils/cmd.py", line 357, in run_command
self.distribution.run_command(command)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/dist.py", line 1102, in run_command
super().run_command(command)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-dszuoqoi/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 1021, in run_command
cmd_obj.run()
File "<string>", line 772, in run
File "<string>", line 674, in pip_run
File "<string>", line 542, in build
File "/opt/anaconda3/envs/ray-compile/lib/python3.11/subprocess.py", line 413, in check_call
raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['/opt/anaconda3/envs/ray-compile/bin/python3.11', '-m', 'pip', 'install', '-q', '--target=/Users/xytan/Desktop/study/ray/python/ray/thirdparty_files', 'psutil', 'setproctitle==1.2.2', 'colorama']' returned non-zero exit status 1.
An error occurred when building editable wheel for ray.
See debugging tips in: https://setuptools.pypa.io/en/latest/userguide/development_mode.html#debugging-tips
error: subprocess-exited-with-error

× Building editable for ray (pyproject.toml) did not run successfully.
exit code: 1
╰─> No available output.

note: This error originates from a subprocess, and is likely not a problem with pip.
full command: /opt/anaconda3/envs/ray-compile/bin/python3.11 /opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py build_editable /var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/tmp2ifs64vi
cwd: /Users/xytan/Desktop/study/ray/python
Building editable for ray (pyproject.toml) ... error
ERROR: Failed building editable for ray
Failed to build ray
error: failed-wheel-build-for-install

× Failed to build installable wheels for some pyproject.toml based projects
╰─> ray

遇到该报错后,我检查了 2.40.0 版本的官方编译文档,确认流程完全符合文档步骤。

按理说,当前 conda 环境应该能找到 python3 和 pip,但调用 pip install -e . 时却报错。查看相关代码后发现,Ray 是通过子进程来安装这些 pip 包的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Note: We are passing in sys.executable so that we use the same
# version of Python to build packages inside the build.sh script. Note
# that certain flags will not be passed along such as --user or sudo.
# TODO(rkn): Fix this.
if not os.getenv("SKIP_THIRDPARTY_INSTALL"):
pip_packages = ["psutil", "setproctitle==1.2.2", "colorama"]
subprocess.check_call(
[
sys.executable,
"-m",
"pip",
"install",
"-q",
"--target=" + os.path.join(ROOT_DIR, THIRDPARTY_SUBDIR),
]
+ pip_packages,
env=dict(os.environ, CC="gcc"),
)

# runtime env agent dependenceis
runtime_env_agent_pip_packages = ["aiohttp"]
subprocess.check_call(
[
sys.executable,
"-m",
"pip",
"install",
"-q",
"--target=" + os.path.join(ROOT_DIR, RUNTIME_ENV_AGENT_THIRDPARTY_SUBDIR),
]
+ runtime_env_agent_pip_packages
)

我尝试直接在命令行执行 /opt/anaconda3/envs/ray-compile/bin/python3.11 -m pip install -q --target=/Users/xytan/Desktop/study/ray/python/ray/thirdparty_files psutil setproctitle==1.2.2 colorama,发现可以成功。这说明问题出在子进程执行环境中,可能是子进程初始化时未包含完整的 conda 环境。带着这些上下文,我咨询了 ChatGPT、DeepSeek、Qwen 等大模型,给出的方案包括修改 ~/.bazelrc、将 python 和 pip 加入 /etc/profile 的 PATH 等,但均未能解决问题。

由于对 Ray 编译的复杂度有所顾虑,担心白盒分析耗时不可控,我转而去 Ray 的 issue 区寻找线索。幸运的是找到了一个相同报错的 issue,遗憾的是该 issue 自 2024 年初创建至今近两年仍未关闭,官方的回复也未给出直接的解决方案,看起来是个棘手的环境问题。

这个问题说来也有些离谱:按照 Ray 官方文档竟然无法从源码编译,这算是个挺严重的问题。不知道 Ray 官方当时构建 2.40.0 版本时是如何操作的,也许是在一个包含所有依赖的沙箱环境中进行,因而未发现此问题。

既然官方也没有提供解决方案,白盒分析又耗时不可控,那有没有高效的黑盒方法来定位问题呢?

我灵机一动:既然 2.52.1 版本可以编译,2.40.0 版本不行,虽然两者相隔近五千个 commit,但可以用 git bisect 二分查找第一个能编译的 commit。由于不可编译的版本只需执行 pip install -e . --verbose 十秒内就能复现错误,理论上最多 13 次、耗时不到 3 分钟即可定位。

按照这个思路,我先通过 git merge-base ray-2.52.1 ray-2.40.0 获取两个分支的公共祖先 02ac0cdc7adf5e611134840c73fa47dd7866140d

经测试,ray-2.52.1 可以编译,公共祖先版本不可编译,满足二分条件。

执行 git bisect start ray-2.52.1 02ac0cdc7adf5e611134840c73fa47dd7866140d 开始二分查找。需要注意的是,bisect 默认假定新版本为 bad、旧版本为 good,用于寻找第一个引入 bad 的 commit。而我们的情况恰好相反——新版本可编译、旧版本不可编译,因此在判断 good/bad 时需反向操作。

以下是二分的详细过程,总耗时不超过 5 分钟即定位到第一个使编译成功的 commit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 只需 11 次即可定位,二分效率惊人
git bisect start ray-2.52.1 02ac0cdc7adf5e611134840c73fa47dd7866140d
# Bisecting: 2469 revisions left to test after this (roughly 11 steps)
# [07f509670a9857d3507fcc9defdc5487d8083758] [data] Refactor interface for actor_pool_map_operator (#53752)

# git bisect 必须在项目根目录执行,因此退回上级目录,pip install 命令中的路径也相应调整
cd ..
pip install -e python --verbose

git bisect bad
pip install -e python --verbose

git bisect bad
pip install -e python --verbose

git bisect good
pip install -e python --verbose

git bisect bad
pip install -e python --verbose

git bisect good
pip install -e python --verbose

git bisect bad
pip install -e python --verbose

git bisect good
pip install -e python --verbose

git bisect bad
pip install -e python --verbose

git bisect bad
pip install -e python --verbose

git bisect good
pip install -e python --verbose

git bisect bad
pip install -e python --verbose

git bisect bad

出现结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
d2004b6353e131bb67e1bc7f771a09780ee32d2a is the first bad commit
commit d2004b6353e131bb67e1bc7f771a09780ee32d2a
Author: Philipp Moritz <[email protected]>
Date: Thu Feb 13 00:08:30 2025 -0800

[Core] Initial port of Ray to Python 3.13 (#47984)

<!-- Thank you for your contribution! Please review
https://github.com/ray-project/ray/blob/master/CONTRIBUTING.rst before
opening a pull request. -->

<!-- Please add a reviewer to the assignee section when you create a PR.
If you don't have the access to it, we will shortly find a reviewer and
assign them to your PR. -->

## Why are these changes needed?

This is the first step towards
https://github.com/ray-project/ray/issues/47933

It is not very tested at the moment (on Python 3.13), but it compiles
locally (with `pip install -e . --verbose`) and can execute a simple
workload like

>>> import ray
>>> ray.init()
2024-10-10 16:03:31,857 INFO worker.py:1799 -- Started a local Ray instance.
RayContext(dashboard_url='', python_version='3.13.0', ray_version='3.0.0.dev0', ray_commit='{{RAY_COMMIT_SHA}}')
>>> @ray.remote
... def f():
... return 42
...
>>> ray.get(f.remote())
42
>>>

(and similar for actors).

The main thing that needed to change to make Ray work on Python 3.13 was
to upgrade Cython to 3.0.11 which seems to be the first version of
Cython to support Python 3.13. Unfortunately it has a compiler bug
https://github.com/cython/cython/pull/3235 (the fix is not released yet)
that I had to work around.

I also had to work around https://github.com/cython/cython/issues/5750
by changing some typing from `float` to `int | float`.

## Related issue number

<!-- For example: "Closes #1234" -->

## Checks

- [ ] I've signed off every commit(by using the -s flag, i.e., `git
commit -s`) in this PR.
- [ ] I've run `scripts/format.sh` to lint the changes in this PR.
- [ ] I've included any doc changes needed for
https://docs.ray.io/en/master/.
- [ ] I've added any new APIs to the API Reference. For example, if I
added a
method in Tune, I've added it in `doc/source/tune/api/` under the
corresponding `.rst` file.
- [ ] I've made sure the tests are passing. Note that there might be a
few flaky tests, see the recent failures at https://flakey-tests.ray.io/
- Testing Strategy
- [ ] Unit tests
- [ ] Release tests
- [ ] This PR is not tested :(

---------

Signed-off-by: Philipp Moritz <[email protected]>
Co-authored-by: pcmoritz <[email protected]>
Co-authored-by: srinathk10 <[email protected]>
Co-authored-by: Edward Oakes <[email protected]>

bazel/ray_deps_setup.bzl | 4 +-
python/ray/_raylet.pxd | 3 +-
python/ray/_raylet.pyx | 20 ++++++----
python/ray/includes/gcs_client.pxi | 28 +++++++-------
python/ray/includes/global_state_accessor.pxi | 8 ++--
python/ray/includes/object_ref.pxi | 2 +-
python/ray/includes/unique_ids.pxd | 53 +++++++--------------------
python/ray/includes/unique_ids.pxi | 10 ++---
python/setup.py | 4 +-
9 files changed, 55 insertions(+), 77 deletions(-)

分析该 commit 的代码,发现这是 AnyScale CTO 为 Ray 添加 Python 3.13 支持的改动,遗憾的是 PR 描述中并未提及任何编译问题的修复,应该是无意间修复了此问题。

因此只能深入代码寻找原因。幸运的是这个 PR 只修改了 9 个文件、不到 100 行代码,可以较直观地分析为何这个 commit 使编译得以成功。

经排查,发现该 commit 在 setup.py 中只修改了两行代码,其中关键的一行是为 setup_requires 增加了 pip 依赖。这个改动与之前的报错高度吻合:setup_requires 正是用于在子进程中初始化构建依赖的。

1
2
3
4
5
6
7
8
@@ -807,7 +807,7 @@ setuptools.setup(
# The BinaryDistribution argument triggers build_ext.
distclass=BinaryDistribution,
install_requires=setup_spec.install_requires,
- setup_requires=["cython >= 0.29.32", "wheel"],
+ setup_requires=["cython >= 3.0.12", "pip", "wheel"],
extras_require=setup_spec.extras,
entry_points={

找到原因后,我立即切换到 ray-2.40.0 分支并在 setup.py 中添加 pip 依赖,改动如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/python/setup.py b/python/setup.py
index 16017fa544..28ffef2503 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -807,7 +807,7 @@ setuptools.setup(
# The BinaryDistribution argument triggers build_ext.
distclass=BinaryDistribution,
install_requires=setup_spec.install_requires,
- setup_requires=["cython >= 0.29.32", "wheel"],
+ setup_requires=["cython >= 0.29.32", "pip", "wheel"],
extras_require=setup_spec.extras,
entry_points={
"console_scripts": [

执行 pip install -e python --verbose 后,之前的报错消失了,说明改动生效。但不幸的是又出现了新的报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
[2,597 / 5,261] Executing genrule @com_github_antirez_redis//:bin; 3s local ... (12 actions, 10 running)
ERROR: /private/var/tmp/_bazel_xytan/13505f911ec68d8fcfe382f9a26054b3/external/zlib/BUILD.bazel:1:11: Compiling zutil.c failed: (Exit 1): cc_wrapper.sh failed: error executing command (from target @zlib//:zlib)
(cd /private/var/tmp/_bazel_xytan/13505f911ec68d8fcfe382f9a26054b3/sandbox/darwin-sandbox/12269/execroot/com_github_ray_project_ray && \
exec env - \
PATH=/Users/xytan/Library/Caches/bazelisk/downloads/bazelbuild/bazel-6.5.0-darwin-arm64/bin:/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/bin:/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/normal/bin:/Users/xytan/.local/bin:/opt/anaconda3/envs/ray-compile/bin:/opt/anaconda3/condabin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pmk/env/global/bin:/Applications/iTerm.app/Contents/Resources/utilities:/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin:/usr/local/maven/bin:/Users/xytan/bin:/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin:/usr/local/maven/bin:/Users/xytan/bin \
PWD=/proc/self/cwd \
external/local_config_cc/cc_wrapper.sh -U_FORTIFY_SOURCE -fstack-protector -Wall -Wthread-safety -Wself-assign -Wunused-but-set-parameter -Wno-free-nonheap-object -fcolor-diagnostics -fno-omit-frame-pointer -g0 -O2 '-D_FORTIFY_SOURCE=1' -DNDEBUG -ffunction-sections -fdata-sections -MD -MF bazel-out/darwin_arm64-opt/bin/external/zlib/_objs/zlib/zutil.pic.d '-frandom-seed=bazel-out/darwin_arm64-opt/bin/external/zlib/_objs/zlib/zutil.pic.o' -fPIC '-DBAZEL_CURRENT_REPOSITORY="zlib"' -iquote external/zlib -iquote bazel-out/darwin_arm64-opt/bin/external/zlib -isystem external/zlib -isystem bazel-out/darwin_arm64-opt/bin/external/zlib -fPIC -Werror -w '-Wno-error=implicit-function-declaration' -no-canonical-prefixes -Wno-builtin-macro-redefined '-D__DATE__="redacted"' '-D__TIMESTAMP__="redacted"' '-D__TIME__="redacted"' -c external/zlib/zutil.c -o bazel-out/darwin_arm64-opt/bin/external/zlib/_objs/zlib/zutil.pic.o)
# Configuration: 5f13e584be259b429338435560124496342d10ebccdd9918322724af70f69ddb
# Execution platform: @local_config_platform//:host

Use --sandbox_debug to see verbose messages from the sandbox and retain the sandbox build root for debugging
In file included from external/zlib/zutil.c:10:
In file included from external/zlib/gzguts.h:21:
In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/stdio.h:61:
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_stdio.h:318:7: error: expected identifier or '('
318 | FILE *fdopen(int, const char *) __DARWIN_ALIAS_STARTING(__MAC_10_6, __IPHONE_2_0, __DARWIN_ALIAS(fdopen));
| ^
external/zlib/zutil.h:147:33: note: expanded from macro 'fdopen'
147 | # define fdopen(fd,mode) NULL /* No fdopen() */
| ^
/Library/Developer/CommandLineTools/usr/lib/clang/17/include/__stddef_null.h:26:16: note: expanded from macro 'NULL'
26 | #define NULL ((void*)0)
| ^
In file included from external/zlib/zutil.c:10:
In file included from external/zlib/gzguts.h:21:
In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/stdio.h:61:
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_stdio.h:318:7: error: expected ')'
external/zlib/zutil.h:147:33: note: expanded from macro 'fdopen'
147 | # define fdopen(fd,mode) NULL /* No fdopen() */
| ^
/Library/Developer/CommandLineTools/usr/lib/clang/17/include/__stddef_null.h:26:16: note: expanded from macro 'NULL'
26 | #define NULL ((void*)0)
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_stdio.h:318:7: note: to match this '('
external/zlib/zutil.h:147:33: note: expanded from macro 'fdopen'
147 | # define fdopen(fd,mode) NULL /* No fdopen() */
| ^
/Library/Developer/CommandLineTools/usr/lib/clang/17/include/__stddef_null.h:26:15: note: expanded from macro 'NULL'
26 | #define NULL ((void*)0)
| ^
In file included from external/zlib/zutil.c:10:
In file included from external/zlib/gzguts.h:21:
In file included from /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/stdio.h:61:
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_stdio.h:318:7: error: expected ')'
318 | FILE *fdopen(int, const char *) __DARWIN_ALIAS_STARTING(__MAC_10_6, __IPHONE_2_0, __DARWIN_ALIAS(fdopen));
| ^
external/zlib/zutil.h:147:33: note: expanded from macro 'fdopen'
147 | # define fdopen(fd,mode) NULL /* No fdopen() */
| ^
/Library/Developer/CommandLineTools/usr/lib/clang/17/include/__stddef_null.h:26:22: note: expanded from macro 'NULL'
26 | #define NULL ((void*)0)
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_stdio.h:318:7: note: to match this '('
external/zlib/zutil.h:147:33: note: expanded from macro 'fdopen'
147 | # define fdopen(fd,mode) NULL /* No fdopen() */
| ^
/Library/Developer/CommandLineTools/usr/lib/clang/17/include/__stddef_null.h:26:14: note: expanded from macro 'NULL'
26 | #define NULL ((void*)0)
| ^
3 errors generated.
INFO: Elapsed time: 6.329s, Critical Path: 4.04s
INFO: 512 processes: 360 internal, 152 darwin-sandbox.
FAILED: Build did NOT complete successfully
Traceback (most recent call last):
File "/opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 389, in <module>
main()
File "/opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 373, in main
json_out["return_val"] = hook(**hook_input["kwargs"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 303, in build_editable
return hook(wheel_directory, config_settings, metadata_directory)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 468, in build_editable
return self._build_with_temp_dir(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 404, in _build_with_temp_dir
self.run_setup()
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 512, in run_setup
super().run_setup(setup_script=setup_script)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/build_meta.py", line 317, in run_setup
exec(code, locals())
File "<string>", line 784, in <module>
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/__init__.py", line 115, in setup
return distutils.core.setup(**attrs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/_distutils/core.py", line 186, in setup
return run_commands(dist)
^^^^^^^^^^^^^^^^^^
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/_distutils/core.py", line 202, in run_commands
dist.run_commands()
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 1002, in run_commands
self.run_command(cmd)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/dist.py", line 1102, in run_command
super().run_command(command)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 1021, in run_command
cmd_obj.run()
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 139, in run
self._create_wheel_file(bdist_wheel)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 349, in _create_wheel_file
files, mapping = self._run_build_commands(dist_name, unpacked, lib, tmp)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 272, in _run_build_commands
self._run_build_subcommands()
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 299, in _run_build_subcommands
self.run_command(name)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/_distutils/cmd.py", line 357, in run_command
self.distribution.run_command(command)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/dist.py", line 1102, in run_command
super().run_command(command)
File "/private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-build-env-v2znngb3/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 1021, in run_command
cmd_obj.run()
File "<string>", line 772, in run
File "<string>", line 674, in pip_run
File "<string>", line 617, in build
File "<string>", line 397, in bazel_invoke
File "/opt/anaconda3/envs/ray-compile/lib/python3.11/subprocess.py", line 413, in check_call
raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['bazel', 'build', '--verbose_failures', '--', '//:ray_pkg', '//cpp:ray_cpp_pkg']' returned non-zero exit status 1.
An error occurred when building editable wheel for ray.
See debugging tips in: https://setuptools.pypa.io/en/latest/userguide/development_mode.html#debugging-tips
error: subprocess-exited-with-error

× Building editable for ray (pyproject.toml) did not run successfully.
exit code: 1
╰─> No available output.

note: This error originates from a subprocess, and is likely not a problem with pip.
full command: /opt/anaconda3/envs/ray-compile/bin/python3.11 /opt/anaconda3/envs/ray-compile/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py build_editable /var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/tmpq2o3yp9s
cwd: /Users/xytan/Desktop/study/ray/python
Building editable for ray (pyproject.toml) ... error
ERROR: Failed building editable for ray
Failed to build ray
error: failed-wheel-build-for-install

× Failed to build installable wheels for some pyproject.toml based projects
╰─> ray

通过搜索,找到了 Bazel 仓库中一个相同报错的 issue。原因是新版本 macOS SDK 与 Bazel 依赖的 zlib 1.3 不兼容,需升级到 zlib 1.3.1 版本。

于是我按照 issue 中的描述在 WORKSPACE 文件中添加了以下配置,遗憾的是仍然报错:

1
2
3
4
5
6
7
8
9
10
11
zlib_version = "1.3.1"

zlib_sha256 = "9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23"

http_archive(
name = "zlib",
build_file = "@com_google_protobuf//:third_party/zlib.BUILD",
sha256 = zlib_sha256,
strip_prefix = "zlib-%s" % zlib_version,
urls = ["https://github.com/madler/zlib/releases/download/v{v}/zlib-{v}.tar.gz".format(v = zlib_version)],
)

白盒分析暂时没有头绪。既然如此,只能继续用二分方案黑盒查找。这次由于需要编译数分钟才能复现,二分过程会稍慢一些,但仍可行。

经过 11 轮二分,定位到了使编译通过的 commit。从 PR 标题来看,该 commit 正是为了解决 macOS 编译问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
65ae6076f25325528dabf1432d1ff1bedb1c70b3 is the first bad commit
commit 65ae6076f25325528dabf1432d1ff1bedb1c70b3
Author: Dhyey Shah <[email protected]>
Date: Mon Apr 7 12:41:34 2025 -0400

[core] Patch zlib and clang 17 compliant for mac update (#52020)

Signed-off-by: dayshah <[email protected]>

.bazelrc | 2 +-
bazel/ray.bzl | 4 ++++
bazel/ray_deps_setup.bzl | 2 ++
src/ray/core_worker/core_worker.h | 9 +++++++--
thirdparty/patches/grpc-zlib-fdopen.patch | 13 +++++++++++++
thirdparty/patches/prometheus-zlib-fdopen.patch | 11 +++++++++++
thirdparty/patches/zlib-fdopen.patch | 19 +++++++++++++++++++
7 files changed, 57 insertions(+), 3 deletions(-)
create mode 100644 thirdparty/patches/grpc-zlib-fdopen.patch
create mode 100644 thirdparty/patches/prometheus-zlib-fdopen.patch
create mode 100644 thirdparty/patches/zlib-fdopen.patch

切回 ray-2.40.0 版本,执行 git cherry-pick 65ae6076f25325528dabf1432d1ff1bedb1c70b3 将该 commit cherry-pick 过来(需处理小范围冲突,可参考我个人维护的 release/2.40.0 版本),再补充 setup.py 中的 pip 依赖,即可在新版本 macOS 上成功编译 ray-2.40.0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
ray git:(6e726cac4f) ✗ pip install -e python --verbose
Using pip 25.3 from /opt/anaconda3/envs/ray-compile-1/lib/python3.11/site-packages/pip (python 3.11)
Obtaining file:///Users/xytan/Desktop/study/ray/python
Running command installing build dependencies
Using pip 25.3 from /opt/anaconda3/envs/ray-compile-1/lib/python3.11/site-packages/pip (python 3.11)
Collecting setuptools>=40.8.0
Obtaining dependency information for setuptools>=40.8.0 from https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl.metadata
Using cached setuptools-80.9.0-py3-none-any.whl.metadata (6.6 kB)
Using cached setuptools-80.9.0-py3-none-any.whl (1.2 MB)
Installing collected packages: setuptools
Successfully installed setuptools-80.9.0
Installing build dependencies ... done
Running command Checking if build backend supports build_editable
Checking if build backend supports build_editable ... done
Running command Getting requirements to build editable
Getting requirements to build editable ... done
Running command installing backend dependencies
Using pip 25.3 from /opt/anaconda3/envs/ray-compile-1/lib/python3.11/site-packages/pip (python 3.11)
Collecting pip
Obtaining dependency information for pip from https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl.metadata
Using cached pip-25.3-py3-none-any.whl.metadata (4.7 kB)
Collecting wheel
Obtaining dependency information for wheel from https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl.metadata
Using cached wheel-0.45.1-py3-none-any.whl.metadata (2.3 kB)
Collecting cython>=0.29.32
Obtaining dependency information for cython>=0.29.32 from https://files.pythonhosted.org/packages/e0/ba/d785f60564a43bddbb7316134252a55d67ff6f164f0be90c4bf31482da82/cython-3.2.2-cp311-cp311-macosx_11_0_arm64.whl.metadata
Using cached cython-3.2.2-cp311-cp311-macosx_11_0_arm64.whl.metadata (5.0 kB)
Using cached pip-25.3-py3-none-any.whl (1.8 MB)
Using cached wheel-0.45.1-py3-none-any.whl (72 kB)
Using cached cython-3.2.2-cp311-cp311-macosx_11_0_arm64.whl (3.0 MB)
...
...
...
Building editable for ray (pyproject.toml) ... done
Created wheel for ray: filename=ray-2.40.0-0.editable-cp311-cp311-macosx_11_0_arm64.whl size=7304 sha256=5b09461aeadadc13af4d10af9d5c78e4a55a52718113de72f0b02bbeb485c5c3
Stored in directory: /private/var/folders/xx/j9ztcfr55_d_3p24y1v_4mzw0000gn/T/pip-ephem-wheel-cache-njomx5v1/wheels/3b/4a/f0/6edffb2ad8c786ba8990ff9495668d930965bc91921b146ea6
Successfully built ray
Installing collected packages: ray
Attempting uninstall: ray
Found existing installation: ray 2.52.1
Uninstalling ray-2.52.1:
Removing file or directory /opt/anaconda3/envs/ray-compile-1/bin/ray
Removing file or directory /opt/anaconda3/envs/ray-compile-1/bin/serve
Removing file or directory /opt/anaconda3/envs/ray-compile-1/bin/tune
Removing file or directory /opt/anaconda3/envs/ray-compile-1/lib/python3.11/site-packages/__editable__.ray-2.52.1.pth
Removing file or directory /opt/anaconda3/envs/ray-compile-1/lib/python3.11/site-packages/__editable___ray_2_52_1_finder.py
Removing file or directory /opt/anaconda3/envs/ray-compile-1/lib/python3.11/site-packages/__pycache__/__editable___ray_2_52_1_finder.cpython-311.pyc
Removing file or directory /opt/anaconda3/envs/ray-compile-1/lib/python3.11/site-packages/ray-2.52.1.dist-info/
Successfully uninstalled ray-2.52.1
changing mode of /opt/anaconda3/envs/ray-compile-1/bin/ray to 755
changing mode of /opt/anaconda3/envs/ray-compile-1/bin/rllib to 755
changing mode of /opt/anaconda3/envs/ray-compile-1/bin/serve to 755
changing mode of /opt/anaconda3/envs/ray-compile-1/bin/tune to 755
Successfully installed ray-2.40.0

小结

在 macOS 上编译 ray-2.52.1 和 ray-2.40.0 的过程中,遇到了两个棘手问题:第一个是找不到 pip 的问题,官方 issue、PR 和网络资料均无解决方案;第二个是 zlib 版本兼容问题,虽然在 issue 中找到了疑似方案,但尝试后未能奏效。

在白盒分析无果的情况下,我决定使用 git bisect 黑盒定位。得益于 O(log n) 相比 O(n) 的效率优势,成功在近五千个 commit 中高效找到了使 ray-2.40.0 能够编译的两个关键 commit。

通过这次排查,我将基于 release/2.40.0 版本新增的两个修复 commit 推送到了 GitHub,同时也将本文的发现回复在了近两年未关闭的 issue 中并使得 issue 被 resolve 关闭,希望后来遇到这些坑的朋友能从中受益。

在 CentOS 8 上编译 Ray

完成 MacBook 上的编译探索后,接下来在 CentOS 8 上编译 Ray。相比之前遇到的代码层面问题,这部分更多是环境配置的挑战。

由于 CentOS 8 已于 2021 年底停止维护,主流云厂商的官方镜像中已不再提供该版本,最低可选版本为 CentOS Stream 9:

Ubuntu 同样如此,最低可选版本为 Ubuntu 22.04,无法直接获取 Ubuntu 16 等老版本镜像:

虽然基于这些新版本镜像也能编译 Ray,但由于其 glibc 等核心库版本较高,编译产物往往无法在老版本系统上运行。

因此,若需为老版本操作系统编译 HotFix,推荐的做法是:在云厂商处租用相同 CPU 架构的较新版本机器,然后通过 Docker 拉取 CentOS 或 Ubuntu 官方提供的老版本镜像进行编译,以此确保编译环境与生产环境的一致性。

基于以上分析,我在 Google Cloud 上租用了一台 x86 架构的 CentOS Stream 9 机器进行后续编译。

环境配置

  1. 按上述要求从云厂商处申请机器,通过 SSH 登录
  2. 安装 Docker
    1
    sudo yum install docker
  3. 拉取目标版本的 CentOS 镜像并进入容器
    1
    docker run -it centos:8.1.1911 /bin/bash
  4. 由于 CentOS 8 官方源已停止服务,需在容器内配置可用的 yum 源

    1
    2
    3
    4
    5
    sed -i 's|mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-*.repo
    sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*.repo

    yum clean all
    yum makecache
  5. 安装 Node.js

    1
    2
    3
    4
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
    exec bash
    nvm install 14
    nvm use 14
  6. 克隆 Ray 仓库

    1
    2
    3
    yum install -y git
    git clone https://github.com/onesizefitsquorum/ray.git
    cd ray
  7. 安装 C++ 编译工具链

    1
    2
    3
    4
    5
    yum groupinstall 'Development Tools'
    yum install psmisc
    ci/env/install-bazel.sh
    echo 'export PATH="$PATH:~/bin"' >> ~/.bashrc
    exec bash
  8. 安装 Anaconda

    1
    2
    3
    4
    yum install wget
    wget https://repo.anaconda.com/archive/Anaconda3-2025.06-1-Linux-x86_64.sh
    sh Anaconda3-2025.06-1-Linux-x86_64.sh
    exec bash
  9. 创建并激活 Python 环境

    1
    2
    conda create -n ray-compile python=3.11.9
    conda activate ray-compile

编译 HotFix 分支

  1. 切换到维护的 HotFix 分支
    1
    git checkout release/2.40.0
  2. 编译 Dashboard
    1
    2
    3
    cd python/ray/dashboard/client
    npm ci
    npm run build
  3. 编译 Ray
    1
    2
    3
    4
    cd -
    cd python/
    pip install -r requirements.txt
    pip install -e . --verbose
  4. 编译失败。需要继续探索原因。

这期间的若干尝试主要有以下三类报错:

  1. GLIBC 安全检查与外部依赖冲突(Fortify 越界)

    • 这个问题是由于 Ray 的外部依赖库 (@upb) 采取的内存操作方式,与系统的高版本 GLIBC 引入的严格安全检查(_FORTIFY_SOURCE)冲突导致的。
    • 报错日志片段(关键信息):
      1
      error: '__builtin_memcpy(...)' forming offset [9, 16] is out of the bounds [0, 8] of object 'value' ... [-Werror=array-bounds]
    • 问题根源与修改原因: 该错误是 upb 库在进行内存复制时,触发了 GLIBC 的 _FORTIFY_SOURCE 机制的 array-bounds 警告,并且该警告被 Bazel 的编译选项 -Werror 升级为错误。由于 upb 库的编译规则(特别是针对 Bazel Host 工具时)强制定义了 -D_FORTIFY_SOURCE=1,同时又强制使用了 -Werror,覆盖了我们的外部参数。 因此,需要指定 BAZEL_ARGS 来强制覆盖这些编译选项:
      • —host_copt=-U_FORTIFY_SOURCE —copt=-U_FORTIFY_SOURCE:取消定义 _FORTIFY_SOURCE 宏,绕过严格的安全检查。
      • —host_copt=-Wno-error —copt=-Wno-error:禁用将警告升级为错误的行为,防止 upb 编译失败。
  2. Bazel 配置文件强制执行 -Werror 导致的编译失败

    • 即使我们通过 BAZEL_ARGS 传递了 -Wno-error,Ray 源码中的 Bazel 配置文件(.bazelrc)仍有更高级别的规则强制应用 -Werror,这导致了像 implicit-fallthrough 这样的 C++ 警告升级为错误。

    • 报错日志片段(关键信息):

      1
      2
      3
      4
      src/ray/common/id.cc: In function 'uint64_t ray::MurmurHash64A(const void*, int, unsigned int)':
      src/ray/common/id.cc:106:7: error: this statement may fall through [-Werror=implicit-fallthrough=]
      ...
      cc1plus: all warnings being treated as errors
    • 问题根源与修改原因: Ray 的 C++ 代码在 MurmurHash64A 等函数中使用了 switch 语句的 Fall-through(自然落入) 结构,这种结构在 GCC 中会触发 -Wimplicit-fallthrough 警告。由于 Ray 源码根目录下的 .bazelrc 文件中存在一条高优先级的配置规则,例如 build:linux —per_file_copt=”…”-Werror,这条规则将所有警告都升级为了错误。命令行参数无法覆盖这条规则。因此,需要手动进入 .bazelrc 文件,将该行配置(即强制添加 -Werror 的项)注释掉,才能允许这些警告存在,从而使核心代码编译通过。
  3. GCC 版本不兼容导致的 C++ 歧义错误

    • 这个问题发生在尝试使用 Ray 2.40.0 版本的源代码时。Ray 的 C++ 代码库是基于较新的 C++ 标准(如 C++17)编写的,而系统默认 GCC 版本(可能是 GCC 8.x 或更早)在处理新标准的一些特性时存在缺陷。
    • 报错日志片段(关键信息):
      1
      error: ambiguous overload for 'operator<<' (operand types are 'std::ostringstream' {aka 'std::__cxx11::basic_ostringstream<char>'} and 'std::nullptr_t')
    • 问题根源与修改原因: 该错误是经典的 nullptr_t 歧义问题。在 Ray 的日志宏(RayLog)中,尝试将 nullptr(类型为 std::nullptr_t)输出到 std::ostringstream。旧版 GCC(如 GCC 8.x 的标准库)对 std::nullptr_t 没有明确的 operator<< 重载,导致编译器无法区分它是应该被当作 bool 还是 const void*,因此报告歧义错误。将 GCC 版本升级到 11.2.1 能够解决此问题,因为新版本的 GCC 标准库完善了对 C++17 特性的支持,消除了这种类型转换的歧义。

通过在 GitHub 和 Ray 问答社区中进行搜索,并结合 Gemini 和 ChatGPT 的多轮问答结果,在踩坑十余次、折腾许久后,最终一一解决。

解决方案有三:

  1. 升级 gcc 版本到 11.2.1:可以看到在官网编译文档中 Ubuntu 上推荐的编译器版本为 clang12,但却没有说明推荐的 gcc 版本。实测升级到 11.2.1 版本的 GCC 能够编译通过。
    1
    2
    3
    4
    yum install gcc-toolset-11
    scl enable gcc-toolset-11 bash
    # 注意需要重新切换 conda 环境
    conda activate ray-compile
  2. 设置 BAZEL_ARGS 环境变量
    • -U_FORTIFY_SOURCE: 禁用 Fortify 检查,解决 upb/memcpy 越界问题。
    • -Wno-error: 禁用将警告升级为错误,避免外部依赖因严格的警告而失败。
    • --host_copt / --host_cxxopt: 确保这些豁免规则应用于 Bazel 编译工具链(即 Host 平台)。
      1
      export BAZEL_ARGS="--host_copt=-U_FORTIFY_SOURCE --copt=-U_FORTIFY_SOURCE --host_copt=-Wno-error --copt=-Wno-error --host_cxxopt=-Wno-error --cxxopt=-Wno-error"
  3. 修改 Ray .bazelrc 代码中的编译选项:尽管我们设置了 BAZEL_ARGS,但 Ray 源码目录下的 .bazelrc 文件中包含的 build:linux —per_file_copt=”…-Werror” 规则具有极高的优先级,强制将 implicit-fallthrough 等警告升级为错误。需要手动将其注释。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    diff --git a/.bazelrc b/.bazelrc
    index 3c84ce36a7..84a5b3fa7a 100644
    --- a/.bazelrc
    +++ b/.bazelrc
    @@ -43,10 +43,10 @@ build:windows --enable_runfiles
    # for compiling assembly files is fixed on Windows:
    # https://github.com/bazelbuild/bazel/issues/8924
    # Warnings should be errors
    -build:linux --per_file_copt="-\\.(asm|S)$@-Werror"
    -build:macos --per_file_copt="-\\.(asm|S)$@-Werror"
    -build:clang-cl --per_file_copt="-\\.(asm|S)$@-Werror"
    -build:msvc-cl --per_file_copt="-\\.(asm|S)$@-WX"
    +# build:linux --per_file_copt="-\\.(asm|S)$@-Werror"
    +# build:macos --per_file_copt="-\\.(asm|S)$@-Werror"
    +# build:clang-cl --per_file_copt="-\\.(asm|S)$@-Werror"
    +# build:msvc-cl --per_file_copt="-\\.(asm|S)$@-WX"
    # Ignore warnings for protobuf generated files and external projects.
    build --per_file_copt="\\.pb\\.cc$@-w"
    build:linux --per_file_copt="-\\.(asm|S)$,external/.*@-w,-Wno-error=implicit-function-declaration,-Wno-error=unused-function"

完成以上修改后,即可成功执行 pip install -e . --verbose 完成编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...
...
...
Options like `package-data`, `include/exclude-package-data` or
`packages.find.exclude/include` may have no effect.

adding '__editable___ray_2_40_0_finder.py'
adding '__editable__.ray-2.40.0.pth'
creating '/tmp/pip-ephem-wheel-cache-rq0i6oso/wheels/3b/a3/3e/5871189f4113432e73b7e4659ab9a4d2edef3998a6dcfea06f/tmp5xknhp72/.tmp-_9ctuqe5/ray-2.40.0-0.editable-cp311-cp311-linux_x86_64.whl' and adding '/tmp/tmpr9lwou7pray-2.40.0-0.editable-cp311-cp311-linux_x86_64.whl' to it
adding 'ray-2.40.0.dist-info/METADATA'
adding 'ray-2.40.0.dist-info/WHEEL'
adding 'ray-2.40.0.dist-info/entry_points.txt'
adding 'ray-2.40.0.dist-info/top_level.txt'
adding 'ray-2.40.0.dist-info/RECORD'
/tmp/pip-build-env-mlv1uqa4/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py:351: InformationOnly: Editable installation.
!!

********************************************************************************
Please be careful with folders in your working directory with the same
name as your package as they may take precedence during imports.
********************************************************************************

!!
with strategy, WheelFile(wheel_path, "w") as wheel_obj:
Building editable for ray (pyproject.toml) ... done
Created wheel for ray: filename=ray-2.40.0-0.editable-cp311-cp311-linux_x86_64.whl size=7272 sha256=18b317c847a6088a316df5f5c98bda8e245fb62cd7acb720a374447e4b94646c
Stored in directory: /tmp/pip-ephem-wheel-cache-rq0i6oso/wheels/3b/a3/3e/5871189f4113432e73b7e4659ab9a4d2edef3998a6dcfea06f
Successfully built ray
Installing collected packages: ray
changing mode of /root/anaconda3/envs/ray-compile/bin/ray to 755
changing mode of /root/anaconda3/envs/ray-compile/bin/rllib to 755
changing mode of /root/anaconda3/envs/ray-compile/bin/serve to 755
changing mode of /root/anaconda3/envs/ray-compile/bin/tune to 755
Successfully installed ray-2.40.0

下一步也可以使用 pip wheel . --verbose 来打包成 wheel 供其它环境安装使用。

小结

与 macOS 上的编译经历类似,CentOS 8 上的编译同样遇到了不少坑。除了需要通过 Docker 确保编译环境与生产环境一致外,还需解决三个编译问题:GCC 版本过低导致的 nullptr_t 歧义错误、GLIBC Fortify 安全检查与外部依赖冲突、以及 .bazelrc 强制启用 -Werror 导致的编译失败。最终通过升级 GCC 到 11.2.1、设置 BAZEL_ARGS 环境变量、以及注释 .bazelrc 中的 -Werror 配置,成功完成编译。

总结

本文记录了在 macOS 和 CentOS 8 上编译 Ray 2.40.0 的完整踩坑过程,共解决了五个关键问题:

macOS 编译问题:

  1. pip 模块缺失:Ray 2.43 之前的版本编译时报 No module named pip,需在 setup.pysetup_requires 中添加 pip 依赖,详见 commit
  2. zlib 兼容性问题:Ray 2.45 之前的版本在新版 macOS 上因 zlib 版本不兼容而编译失败,需 cherry-pick 此 commit 修复。

CentOS 8 编译问题:

  1. GCC 版本过低:CentOS 8 默认的 GCC 8.x 在处理 C++17 特性时存在 nullptr_t 歧义问题,需升级到 GCC 11.2.1。
  2. GLIBC Fortify 冲突:外部依赖库 upb 的内存操作与 GLIBC 的 _FORTIFY_SOURCE 安全检查冲突,需通过 BAZEL_ARGS 禁用相关检查。
  3. -Werror 强制启用.bazelrc 中的 -Werror 配置将警告升级为错误,需手动注释相关配置行。详见 commit

以上修复已合并到我维护的 release/2.40.0 分支,同时也已将解决方案回复到社区 issue 中并使得 issue 被 resolve 掉,希望能帮助后来者少走弯路。

至此,本文所有内容均已结束,感谢您的阅读和关注!

让 Ray Distributed Debugger 在 Kuberay 下可用

2025-08-17 00:39:26

背景

在软件开发过程中,具备单步调试能力的 Debugger 是提升开发效率的关键工具。对于复杂的分布式系统而言,单步调试能力尤为重要,它能帮助开发者在纷繁复杂的同步/异步代码链路中快速定位问题,从而缩短问题诊断周期。

以分布式存储系统为例,2021 年我曾通过 IDEA 配置 Apache IoTDB 3C3D 集群的单步调试能力(可参考 博客)。在随后的几年里,这套方案帮助我解决了 IoTDB 分布式开发过程中的不少疑难问题,提升了开发效率。

最近,我开始学习并研究分布式计算框架 Ray,首先从其调试功能入手。Ray 官方目前支持两种 Debugger,具体使用方式可参考官方文档,这里简要介绍:

  • Ray Debugger:通过 Ray debug 命令复用 pdb session 命令行进行单步调试。从 2.39 版本开始已被标记为废弃,不推荐使用。
  • Ray Distributed Debugger:通过 VSCode 插件复用 pydebug 图形界面进行单步调试,体验更佳。目前是 Ray 官方社区推荐的默认调试工具。

注意:Ray Cluster 启动时需配置相应的 Debugger 参数,且上述两种 Debugger 不支持同时使用。

Ray Distributed Debugger 的核心原理是基于 Ray 内核中默认开启的 RAY_DEBUG 环境变量。当触发断点时,所有 Worker 会周期性地将断点信息汇总到 Head 节点。VSCode 插件通过连接 Ray Head 节点获取断点列表,用户可进一步点击 Start Debugging,attach 到对应 Worker 上进行单步调试。其官方文档大纲如下:

Ray Distributed Debugger Architecture

Ray Distributed Debugger 在 Kuberay 环境下的问题

如上所述,Ray Distributed Debugger 需要能够网络连接到触发断点的 Worker,才能实现单步调试。在裸机部署场景下,只需配置好防火墙规则即可满足需求。然而,随着云原生技术的普及,目前大多数分布式计算框架都基于 Kubernetes(K8S)进行资源管理。此时,用户通常会选择安装 Kuberay,并通过 RayCluster/RayJob/RayServe 等自定义资源进行 Ray 集群的生命周期和资源控制。

在 K8S 环境下,由于其网络隔离机制,Ray 集群实际运行在集群内部的隔离网络空间中,外部默认无法直接访问 Ray Cluster 的各个组件。Ray Distributed Debugger 需要连接 Ray Head 节点的 dashboard 端口(8265)才能获取所有断点信息,此时我们可以将 Ray Head 的 8265 端口暴露出来,使 Ray Distributed Debugger 能够获取到集群中触发的断点列表。

以下是一个在 Kuberay 环境下测试 Ray Distributed Debugger 的例子:

  1. 首先安装好 K8S 集群和 kuberay-operator,然后使用 RayJob 模式提交一个会触发断点的任务。
Submit RayJob with breakpoint
  1. 当代码中触发断点时,会在 job submitter 侧打印日志,表明 debugger 正在等待 attach:
Debugger waiting for attach
  1. 我们使用 kubectl port-forward 命令将 Head 节点的 8265 端口转发到本地的 8265 端口,并通过 Ray Distributed Debugger 连接。此时可以看到集群中触发的所有断点:
Ray Distributed Debugger showing breakpoints
  1. 然而,当尝试连接任意一个断点进行调试时,系统显示无法 attach 到断点,报错如下:
Connection error to breakpoint
  1. 分析错误信息后发现,问题在于 Ray Distributed Debugger 插件尝试连接的是 Kubernetes 集群内部的 IP 和端口。这些 IP 和端口在集群外部无法直接访问,且端口是随机分配的,无法提前进行端口映射,因此导致连接失败。

以上示例表明,在 Kuberay 环境下使用 Ray Distributed Debugger 存在实际困难。

值得一提的是,在官方文档中我们还发现一个 PR,提出了通过在 Ray Head 镜像中安装 SSH,并利用 VSCode Remote 进行连接的方案。虽然理论上可行,但这种方式操作较为复杂,涉及密钥管理、生命周期管理等问题,因此被用户诟病。

User complaint about SSH approach
More complaints about SSH approach

通过分析,我们发现 Ray 官方目前对于 Ray Distributed Debugger 在 Kuberay 环境下的支持不够完善,需要一个更便捷的解决方案。

技术探索

在 Kubernetes 环境下,是否有办法方便地使用 Ray Distributed Debugger?带着这个问题,我进行了一些技术调研和尝试。

请求代理方案的探索与局限

首先查阅了 Ray 官方 GitHub 仓库中的相关 issue:[Ray debugger] Unable to use debugger on Ray Cluster on k8s。从讨论中看出,Ray 官方最初的解决思路是让 Worker 在暴露等待 attach 的端口时使用固定的端口范围,这样用户就可以预先将这些端口暴露到外部进行 attach:

GitHub issue discussion about port ranges

有开发者甚至提交了相关 PR 尝试将这一功能集成到 Ray 内核中,但该 PR 最终未被推进,被自动关闭:

Closed PR for port range feature

推测这种方案未能推进主要是因为存在几个明显的问题:

  1. 端口范围设定难题:如何确定合适的端口范围?范围太小可能无法覆盖所有断点,范围太大可能占用过多集群资源,甚至与 Kubernetes API Server 等系统组件的端口冲突。

  2. 操作复杂度高:即使确定了端口范围,用户仍需手动暴露大量端口,操作繁琐且容易出错,不符合云原生环境下自动化的设计理念。

  3. 网络连接障碍:最关键的问题是,即使端口被成功暴露,Ray Distributed Debugger 的 VSCode 插件仍然会尝试连接 Kubernetes 集群内部的 IP 地址,而这些 IP 在集群外部不可达。由于 VSCode 插件已被 Anyscale 公司闭源管理,我们无法修改其连接逻辑。

理论上,可以通过为每个断点设置 kubectl port-forward,然后配合 iptables 规则将本地向 Kubernetes 内部 IP 发送的请求重定向到对应的本地端口,但这种方法操作繁琐、难以自动化,且需要较深的网络知识,在断点数量较多时几乎不可维护。

考虑到这些因素,特别是第三点的根本限制,我放弃了这条技术路径,转而寻找更简单的解决方案。

Code-Server:浏览器端 VSCode 的解决方案

在前述 issue 的讨论末尾,有用户反馈他们在 Kubernetes 集群中部署 Code Server 后成功解决了该问题:

User suggesting Code Server solution

这一思路得到了 Ray 官方的认可,但由于缺乏具体实现细节和完整解决方案,该方案一直停留在概念阶段:

Ray team acknowledging the potential of Code Server

受此启发,我决定沿着这个思路进行探索。Code Server 是一个在浏览器中运行的 VSCode 服务,提供与桌面版 VSCode 几乎完全一致的开发体验:

Code Server in browser

这一特性为解决问题提供了思路:如果将 VSCode 部署在 Kubernetes 集群内部并通过浏览器访问,就可以规避网络隔离问题,使 VSCode 能够直接访问 Ray 集群内部网络。这种方案不需要管理 SSH 密钥或配置复杂的 VSCode Remote 连接,操作流程简单明了。

为了优化体验并解决不同 RayJob 之间的潜在冲突,我设计了将 Code Server 作为 Ray Head 的 Sidecar 容器部署的方案。这样不仅确保 Code Server 与 Ray 集群共享生命周期,还能直接访问 Ray 的工作目录,实现无缝集成。

基于这一思路,我开发了一个专用镜像并将其放到了 Dockerhub 上:onesizefitsquorum/code-server-with-ray-distributed-debugger。该镜像基于 linuxserver/code-server:4.101.2,预装了 Python、Ray、debugpy 等必要依赖,以及 VSCode 的 Python Run/Debug 和 Ray Distributed Debugger 插件。

以下是镜像的核心 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
FROM linuxserver/code-server:4.101.2

RUN sudo apt-get update && apt-get install -y software-properties-common && sudo add-apt-repository ppa:deadsnakes/ppa && apt-get install -y python3 python3-pip && pip3 install ray[default] debugpy --break-system-packages

RUN mkdir -p /config/extensions \
&& curl -L -o /config/extensions/ms-python.python.vsix https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/python/2025.10.0/vspackage \
&& curl -L -o /config/extensions/ms-python.debugpy.vsix https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-python/vsextensions/debugpy/2025.10.0/vspackage \
&& curl -L -o /config/extensions/anyscalecompute.ray-distributed-debugger.vsix https://marketplace.visualstudio.com/_apis/public/gallery/publishers/anyscalecompute/vsextensions/ray-distributed-debugger/0.1.4/vspackage \
&& /app/code-server/bin/code-server --extensions-dir /config/extensions --install-extension ms-python.python \
&& /app/code-server/bin/code-server --extensions-dir /config/extensions --install-extension ms-python.debugpy \
&& /app/code-server/bin/code-server --extensions-dir /config/extensions --install-extension anyscalecompute.ray-distributed-debugger

接下来,配置 Code Server 作为 Ray Head 所在 Pod 的 Sidecar 容器,并确保它与 Ray 共享工作目录。注意 Code Server 需要使用前文上传至 DockerHub 的自定义镜像。关键的 Kubernetes 配置片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
containers:
- image: rayproject/ray:2.46.0
name: ray-head
ports:
- containerPort: 6379
name: gcs-server
- containerPort: 8265
name: dashboard
- containerPort: 10001
name: client
resources:
limits:
cpu: "500m"
requests:
cpu: "200m"
volumeMounts:
- mountPath: /tmp/ray
name: shared-ray-volume
- name: vscode-debugger
image: docker.io/onesizefitsquorum/code-server-with-ray-distributed-debugger:4.101.2
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8443
volumeMounts:
- mountPath: /tmp/ray
name: shared-ray-volume
env:
- name: PUID
value: "1000"
- name: PGID
value: "1000"
- name: TZ
value: "Asia/Shanghai"
- name: DEFAULT_WORKSPACE
value: "/tmp/ray/session_latest/runtime_resources"
- name: SUDO_PASSWORD
value: "root"
volumes:
- name: shared-ray-volume # Shared volume for /tmp/ray
emptyDir: {}

部署示例

通过以上技术探索,我们成功让 Ray Distributed Debugger 在 Kuberay 环境下可用。下面给出一个结合本文工作在 Kuberay 集群中使用 Ray Distributed Debugger 的完整示例,所有相关代码和配置文件均已上传至 GitHub 仓库,方便读者参考和使用。

对于有特定业务需求的开发者,只需理解示例代码的核心逻辑,即可轻松扩展实现自定义的 Debugger 管理功能,无需重复开发基础组件和镜像。

开发环境选择

在进行开发调试时,你可以选择本地环境或云端开发环境。对于云端开发,GitHub Codespaces 提供了一个便捷的选项:

  • 每个 GitHub 账户每月有 60 小时的免费使用额度
  • 免费版配置为 2 核 CPU、4GB 内存和 32GB 存储空间的 Linux 环境
  • 预装了 Docker、Kubernetes 工具链等开发必备工具
  • 可以直接在浏览器中进行开发,无需本地环境配置

这些资源足以运行本文中的示例代码和小型 Kubernetes 集群(如 kind、k3d 等),非常适合学习和测试 Ray 的调试功能。

部署步骤

具体步骤如下:

  1. 确保已安装 Kubernetes、Kuberay Operator 和 Kubectl ray 插件。如果使用 GitHub Codespaces,可以直接在终端中安装这些工具。

  2. 进入示例目录,执行以下命令启动一个包含 Ray Head、Code Server 和 Ray Worker 的集群:

1
kubectl ray job submit -f ray-job.interactive-mode.yaml --working-dir ./working_dir --runtime-env-json="{\"pip\": [\"debugpy\"], \"py_modules\": [\"./dependency\"]}" -- python sample_code.py
  1. 集群启动后,会自动安装 debugpy 并将工作目录和模块文件传入 Ray Cluster。当代码执行到 breakpoint() 语句时,会等待调试器 attach。

  2. 使用以下命令转发 Code Server 端口:

1
kubectl port-forward pod/the-name-of-ray-head 8443:8443
  1. 打开浏览器访问 http://127.0.0.1:8443,进入 Code Server 界面。如果在 GitHub Codespaces 中运行,可以利用其端口转发功能,系统会自动创建可访问的 URL。

  2. 在 Code Server 中,使用 Ray Distributed Debugger 插件连接到 127.0.0.1:8265(Ray Head 的 Dashboard 地址),即可看到并连接所有断点。

部署成功后的界面如下:

Code Server with Ray Distributed Debugger in action
Debugging a Ray worker in Code Server

总结与思考

通过这次探索,我们找到了一种在 Kuberay 环境下使用 Ray Distributed Debugger 的方法。这种方案通过 Code Server 作为中间层,解决了 Kubernetes 网络隔离导致的连接问题。主要有以下几点收获:

  1. 解决了实际问题:通过 Code Server 作为桥梁,成功解决了 Kubernetes 网络隔离机制导致的 Ray Distributed Debugger 连接障碍。

  2. 提供了实用方案:方案包括完整的镜像构建、配置模板和使用指南,可以直接应用于实际开发环境。

  3. 简化了操作流程:采用 Sidecar 容器模式,确保了与 Ray 集群共享生命周期,通过共享卷实现了资源无缝访问。

  4. 启发性思考:这种解决方案不仅适用于 Ray Distributed Debugger,也可能适用于其他在 Kubernetes 环境中进行开发调试的场景。

从更广的角度看,这次尝试也引发了一些思考:

  • 云原生环境中的开发体验:随着云原生技术普及,如何在保持隔离性的同时提供良好的开发体验,是一个值得关注的问题。无论是本文提到的 Code Server,还是 GitHub Codespaces 这样的云端开发环境,都在朝着简化开发者体验的方向发展。

  • 浏览器 IDE 的应用前景:基于浏览器的 VSCode 让开发者能够在不同设备上获得一致的开发体验,这种模式在云开发环境中很有潜力。Code Server 和 Codespaces 都采用了这种模式,降低了环境配置的门槛。

  • 开源社区协作的价值:这个问题的解决思路源于社区讨论,也会回馈给社区,体现了开源协作的价值。

我计划将这个解决方案分享给 Ray 社区,希望能帮助到有类似需求的开发者。同时,也欢迎社区成员对方案进行改进和完善。

数据库内核开发 5 年,我从无数坑中学到的 14 个宝贵教训

2025-05-14 15:14:35

前言

过去五年半里,我在 Apache IoTDB 社区担任核心开发者,亲历了老分布式版本的迭代、新分布式架构的设计、盲测性能优化和系统可观测性搭建。这些年来,我在调试各种疑难杂症、修复线上事故以及优化系统架构的过程中,踩过无数坑,也积累了宝贵经验。

这篇文章记录了我在实战中总结出的 14 个重要教训,不是纸上谈兵,而是用血泪换来的经验。希望能帮助正在或即将从事数据库内核开发的朋友们少走弯路。

14 条教训

预见性设计集群扩展,消除性能瓶颈

集群扩展性是确保系统长期可持续发展的关键。在设计初期,应尽量避免集群中的单点瓶颈,合理地将用户负载分配到集群中的所有节点上,并且要控制分片数量。

这样,不仅可以保证集群在负载增加时能够平稳扩展,还能避免在实际运行过程中出现性能瓶颈,从而提高系统的整体可用性。

抽象共识算法接口,实现无缝迭代

共识算法是分布式存储系统核心中的核心,其设计决策直接影响系统性能上限和可靠性保障。如果确信系统只需使用一种共识算法,可以集中精力将其优化到极致;但如果预见到未来可能需要支持多种算法,就应当提前设计一个抽象的通用接口。

通用共识框架的设计不仅能支持当前的共识算法,还为未来算法的演进和优化创造了可能性。良好的抽象接口使新算法的引入变得简单,避免了整体架构的大规模重构,极大地减少了技术债务。

构建完善可观测性,实现透明可控

可观测性是系统设计中的核心部分之一。随着系统的迭代,良好的可观测性设计不仅能帮助你快速定位问题根源,避免因找不到问题所在而浪费大量时间,还能够在不同业务负载和硬件环境下,提供详细数据来量化评估各项优化工作的投入产出比。

投入构建完善的可观测性体系是极其有价值的工作,它不仅能够构建可扩展的工程服务体系,还能够支撑可持续的架构演进。

稳定性优先于性能,打造可靠基础

当系统出现不稳定性问题时,稳定性应始终作为首要解决目标。系统的不稳定性问题通常比性能问题更为紧急,只有系统足够稳定,才能进行进一步的性能优化。

因此,如果系统本身仍存在严重稳定性问题,可以考虑暂停性能优化工作,集中精力解决系统的稳定性问题。

精细化模块设计,控制复杂度增长

代码量每增加一个数量级,维护的复杂度都会呈指数级增长。大型系统的可维护性直接影响到产品的长期生命力和演进能力。在系统不断扩展的过程中,良好的模块化设计是控制复杂度的关键武器。

通过清晰的责任边界、松耦合的接口设计和合理的抽象层次,可以将复杂系统分解为多个可独立理解和维护的模块。这种”分而治之”的策略不仅能够降低团队协作的成本,还能够使系统在面对不断变化的需求时保持足够的灵活性和可扩展性。

隐藏系统复杂性,打造友好接口

在功能迭代过程中,很容易为追求极致性能而向用户暴露底层实现细节或复杂概念。然而,这种”优化”往往会带来用户理解成本的急剧上升、使用门槛的提高以及后期维护的困难。

系统设计的艺术在于在保证性能的同时,尽可能对用户隐藏内部复杂性。一个优秀的系统应当既能提供强大的功能和性能,又能通过简洁直观的抽象概念让用户轻松上手。用户关心的是解决问题的简易程度,而非系统的内部构造。过早的性能优化和不必要的复杂性往往会得不偿失。

自动化代码规范检查,统一团队风格

从项目一开始,就应该引入代码自动化规范检查工具,避免在后续迭代中频繁出现代码风格的变化,或者通过大型 PR 改变代码风格而破坏原有的 git blame 历史。通过自动化检查,不仅能够确保团队成员的代码风格一致,还能减少不必要的沟通和协调,提高团队的协作效率。

构建分层 CI/CD,平衡效率与质量

CI/CD 流程是高效开发的基石。它不仅能够帮助团队保持高效的开发节奏,还能确保系统的稳定性和可靠性。在设置 CI 时,建议将其拆分为 commit、daily 和 weekly 级别,分别执行不同优先级的测试用例,从而在开发效率和代码质量之间找到最佳平衡点。

坚持持续检测,防止质量问题积累

性能和功能的持续检测是保持系统质量的关键。尤其在长期的迭代过程中,确保开发主分支持续接受充分检测,可以有效避免”问题积累”。随着时间推移,未及时发现的问题修复成本会大幅增加,因此及时发现并修复问题,才能确保系统质量持续得到保障。

借助 AI 编程工具,提升开发效率

随着 AI 技术的发展,像 Cursor 这样的 AI 工具可以大幅提高开发效率。尤其是在你已经具备扎实的开发能力时,借助 AI 工具生成代码并进行细致的 review 和微调,能够显著提升代码产出速度。

利用 AI 辅助编程,可以将每日有效代码产出从 100 行提升到 500 行,这不仅节省了时间,也能够提高团队的整体生产力。

选择成熟 IDL 工具,奠定扩展基础

在系统设计初期,选择成熟的 IDL 工具(例如 Protobuf 或 Thrift IDL)来管理网络接口的字段和持久化对象的非压缩磁盘存储(例如 WAL)是明智的选择。不要为了短期性能而放弃可演进性,否则日后很可能会产生难以消除的技术债。

在滚动升级集群时,或者在添加、删除持久化对象字段时,如果最初没考虑可演进性,往往会涉及非常复杂的处理过程和额外的维护成本。提前做出决策,选择合适的工具,可以为未来的扩展和维护打下坚实的基础。

掌握高效调试工具,缩短排障时间

开发初期,学习并掌握先进的线上调试工具是非常必要的。掌握高效的调试工具,能够极大提高问题解决效率。例如,Java 系统开发者至少应当熟悉 JDK 自带命令、JProfile 和 Arthas 等工具,它们可以帮助你快速诊断系统问题,特别是在复杂的线上环境中,能节省大量排查时间。

熟练掌握这些工具可以将复杂问题的解决时间从数天缩短到数小时甚至数分钟。

选择高效流程工具,降低沟通成本

软件开发不仅仅是写代码,管理好软件的迭代流程同样至关重要。结合需求分析、功能设计、技术研究、开发、测试等环节,选择合适的工具来管理文档和迭代任务,能够显著降低团队沟通成本。

高效的流程管理工具能够提高团队的协作效率,确保信息透明和流畅,在团队规模扩大后尤其重要。在这方面,我强烈推荐飞书文档和飞书多维表格等协作工具。

定期小版本发布,降低发版风险

定期发版的计划需要提前制定,避免将所有功能集中在大版本发布中,这样做会带来潜在的延期风险。通过定期发布小版本,不仅能够帮助团队及时应对问题,还能减轻技术负担,避免大版本发布时出现复杂情况。

建立每季度甚至每月定时发布功能版本的节奏,既能让用户及时获得新特性,也能有效降低每次发版的风险。

写在最后

五年多的数据库内核开发之路,既有成功的喜悦,也有踩坑的痛苦。这些教训都是在实际项目中一点一滴积累的,希望能对你的工作有所启发。

在数据库这个相对成熟的领域,虽然具体实现会随着业务需求不断演进,但这些经过实践检验的工程智慧和方法论却是经得起时间考验的。即使技术栈更迭,底层架构变化,这些原则依然适用。从项目伊始就重视这些关键点,不仅能够减少技术债务,还将为你的系统打下坚实的基础,让团队能够持续、稳健地迭代和创新。

本文借助 Cursor IDE 和 Claude 3.7 辅助创作完成,AI 工具极大提高了内容的整理和润色效率,感谢 Anthropic 提供如此强大的技术支持。

2024 年终总结:在清华 IoTDB 创业公司中构建起摩尔定律成长节奏

2025-01-23 11:17:22

前言

忙忙碌碌又是一年,2024 匆匆结束。回想这一年的成长和收获,除了个人能力的提升,在做人做事做选择等方面也有了更多的认识。可以说,自己并未虚度时光,过得十分充实。

临近除夕,总算抽出时间坚持自己之前的习惯来继续写年终总结。希望今年的总结不仅能继续鞭策自己寻找并实践摩尔定律的成长节奏,也能获得更多反馈来修正自己。

首先依然是自我介绍环节,我叫谭新宇,清华本硕,师从软件学院王建民/黄向东老师。目前在时序数据库 Apache IoTDB 的商业化公司天谋科技系统组担任内核开发工程师。我对分布式系统、性能优化等技术驱动的系统设计感兴趣,2024 年也一直致力于提升 Apache IoTDB 的集群易用性 & 鲁棒性、共识能力和写入性能等,并接手完成了若干具有挑战性的大项目。

接下来介绍一下我司:

天谋科技的物联网时序数据库 IoTDB 是一款低成本、高性能的时序数据库,技术原型发源于清华大学,自研完整的存储引擎、查询计算引擎、流处理引擎、智能分析引擎,并拓展集群管理、系统监控、可视化控制台等多项配套工具,可实现单平台采存算管用的横向一站式解决方案,与跨平台端边云协同的纵向一站式解决方案,可方便地满足用户在工业物联网场景多测点、多副本、多环境,达到灵活、高效的时序数据管理。

天谋科技由全球性开源项目、Apache Top-Level 项目 IoTDB 核心团队创立。公司围绕开源版持续进行产品性能打磨,提供更加全面的企业级服务与行业特色功能,并开发易用性工具,使得 IoTDB 的读写、压缩、处理速度、分布式高可用、部署运维等技术维度领先多家数据库厂商。目前,IoTDB 可达到单节点每秒千万级数据写入、10X 倍无损压缩、TB 数据毫秒级查询响应、两节点高可用、秒级扩容等性能表现,实现单设备万级点位、多设备亿级点位管理。

目前,IoTDB 能够为我国关键行业提供一个国产的、更加安全的、性能更加优异的选择。据不完全统计,IoTDB 已服务超 1000 家以上工业企业,在能源电力、钢铁冶炼、航空航天、石油石化、智慧工厂、车联网等行业均成功部署数十至数百套,并扩展至期货、基金等金融行业。目前已投入使用的企业包括华润电力、中核集团、国家电网、宝武钢铁、中冶赛迪、中航成飞、中国中车、长安汽车等。

值得一提的是,2024 年 IoTDB 的商业公司天谋科技营收同比增长近 300%,正式进入了指数增长的摩尔定律节奏。

2024

介绍完背景后,在这里回顾下 2024 年我们系统组的主要工作,可分为 TPCx-IoT 双版本登顶、共识遥遥领先、性能优化摩尔定律新时代、集群易用性 & 鲁棒性显著提升、海量项目支持和海量技术沉淀 6 个方面。

在 TPCx-IoT 双版本登顶方面,国际事务处理性能委员会(TPC)是全球最权威的数据库性能测评基准组织之一。TPCx-IoT(TPC Express Benchmark IoT)是业界首个能直接对比物联网场景下不同软件和硬件性能的基准,涵盖了性能和性价比两个维度。今年,TimechoDB 1.3.2.2 版本在开启和关闭 WAL 测试的两种配置下,分别在 TPCx-IoT 的性能和性价比两个维度均登顶。值得一提的是,原本性能第一的系统是开启 WAL 的,而性价比第一的系统则关闭了 WAL。做数据库的人都知道,是否开启 WAL 对写入性能和资源消耗有着巨大的影响。尽管如此,IoTDB 最终实现了即使开启 WAL,仍能在性能和性价比两个维度同时登顶。如果关闭 WAL,性能和性价比还能够进一步提升约 20%,这充分彰显了 IoTDB 在应对物联网高吞吐场景中的极致性能。如今回想今年与新豪一起完成的这一工作,个人感触颇深,这对我来说也是四年磨一剑的过程。还记得 2020 年下半年研究生刚入学时,我就第一次尝试用 TPCx-IoT 测试过 0.12 版本的 IoTDB 老分布式集群。当时的分布式架构存在较大问题,性能始终难以提升,那年我的外号也成了 “tpc”,因为我一个学期几乎都在做 TPCx-IoT 测试,遗憾的是最终没有得到理想结果。经过四年的彻底重构与迭代,IoTDB 自 1.0 版本推出了全新的分布式架构,并在分区、共识和写入性能等方面做了诸多创新与改进。这些进展不仅使 IoTDB 能够支撑更多的用户场景,也最终帮助我们在 TPCx-IoT 榜单上登顶,性能达到了老版本的近 6 倍,充分证明了技术创新始终是第一生产力。此外,随着商业化公司成立,本次登顶过程中,我们不再是单打独斗,获得了许多同事的全力支持,特别是在与 TPC 委员会沟通、撰写报告等方面。这里特别感谢鹏程、Chris、昊男和苏总等同事对我们组的支持与帮助。没有团队的默默付出与协作,我们也不可能在今年完成这一目标,让这把磨了四年的剑最终得以出鞘。

在共识遥遥领先方面,IoTDB 的 1.x 分布式版本参照 2020 OSDI 最佳论文 Facebook Delos 的思路抽象了一个支持不同共识算法的统一共识框架,允许用户在一致性、可用性、性能和存储成本等若干维度进行权衡。今年我们不仅将现有共识算法迭代的几乎稳定,更是创新的提出了一个新的性能遥遥领先的共识算法

  • 在强一致性算法 Ratis 方面,过去一年里我们组三位同学共为 Ratis 社区贡献了 35 个 commit,使得 Apache IoTDB 社区成为 Apache Ratis 社区仅次于原创团队的最大贡献社区。目前,Ratis 在 IoTDB 的通用场景下已经基本稳定,成员变更的稳定性也得到了显著提升。今年,宋哥成为了 Ratis 的 PMC,我成为了 Ratis 的 Committer,邓超也成为了 Ratis 的 Active Contributor。由于 IoTDB 每次发布正式版本不能依赖 Ratis 的快照版本,今年我们主动承担了多次 Ratis 社区的 Release Manager 角色,确保 IoTDB 每次发布正式版本时不依赖快照版本,从而符合 Apache 基金会的规范。今年,Ratis 共发布了 5 个版本,其中 4 次由我们组担任 Release Manager,Ratis-ThirdParty 发布了 3 个版本,其中 2 次由我们组担任 Release Manager。这标志着我们在 Ratis 维护方面已经积累了相当的能力和影响力。
  • 在基于操作复制的最终一致性算法 IoTV1 方面,过去一年湘鹏,宇衡和我已经将其打磨至几乎彻底稳定,成员变更的稳定性也得到了显著提升。近半年以来,IoTV1 相关的工单几乎为零,这标志着 IoTDB 共识算法稳定性的显著提升。今年,我们在理论上也迈出了重要一步,针对多主异步复制共识算法在成员变更时的一致性机制和保证边界进行了详细分析和完善,最终效果是在上万次 Region 迁移中副本数据依然保持一致。甚至测试组的同学也发出了疑问:“IoTV1 已经这么稳定了,为什么还要开发新的共识算法?”。这进一步说明了 IoTV1 的稳定性已经得到了团队的广泛认可。
  • 在基于状态复制的最终一致性算法 IoTV2 方面,过去一年我们结合物联网场景中写写冲突少的特点,与思成和俊植从 0 到 1 设计并实现了业界首个多节点多副本性能超越单节点单副本的共识算法。该算法不仅在鲁棒性上优于 IoTV1,解决了 WAL 堆积问题,还能在性能上超越现有常用的 Raft 算法 5-10 倍。在实现这一新共识算法的过程中,我们复用了流处理框架,推动了团队合力迭代,与苏总、哲涵、宇辰和振羽一起逐步提升了流处理框架的鲁棒性和稳定性。这一工作是我们在物联网场景下对共识算法的重要创新,也是我们在工程实践中发现的关键方向。我也首次完全自驱地联系伙伴(十篇 A 在投的张先生,以及我们组的新豪和俊植)共同撰写论文来沉淀我们组的工作成果,并最终得到了导师们的认可与支持。虽然没读博士,但也写了论文的两个章节过了一把博士的瘾。经过两个多月的努力,第一版论文已提交至系统方向的 A 类会议,现正等待评审。我非常期待 2025 年,IoTV2 能在学术与工程上同时与大家见面,这也是我们组今年最令人振奋、最反直觉的技术突破。

在性能优化摩尔定律新时代方面,今年我跟旭鑫、昊男、雨峰、湘鹏、荣钊、钰铭、江天学长,田原学长和振宇师兄等团队成员一起进行了多项盲测写入性能优化工作并取得了显著进展。我们不仅在很多特定场景下实现了性能提升数十倍的效果,还在通用场景下实现了写入性能翻倍的成就,这是 IoTDB 写入性能提升最大的一年。通过分布式架构、存储引擎和系统优化的组合拳,我们成功让 IoTDB 在通用场景下的盲测写入性能进入了摩尔定律的成长节奏(每 18 个月性能翻一倍或资源利用率减少一半)。以 2023 年为基准,2024 年我们已经实现了这一目标,2025 和 2026 年现有的技术储备也已经为继续沿着摩尔定律节奏提升奠定好了基础。在具体优化方面,我们做了很多关键工作,仅列举已做的开源部分如下:

  • 行列接口自动转换:优化场景是用户使用行式接口写入列式数据,系统能够自动检测并转换为列式向量化执行,从而提升写入性能。
  • WAL 压缩:优化场景是通用 I/O 瓶颈场景,通过 WAL 压缩,显著节约实时磁盘 I/O 带宽。
  • WAL 批量化:优化场景是设备多测点少的行式批量写入接口,能够显著降低 CPU 利用率和写入延迟。
  • 第一条写入调优:优化场景是空集群第一条写入耗时过长,通过优化显著提升了用户体验。
  • 表模型写入性能优化:优化目标是使 IoTDB 即将发布的表模型写入性能与树模型相似,将树模型优化思路接入表模型,从而提升写入效率。
  • 重启加速:优化场景是加速重启速度,使得重启时间不再与节点数据量挂钩,进而提升集群可用性。
  • 默认 DataRegion 数与硬件资源绑定:优化场景是针对更强硬件(如 16 核以上机器),自动根据硬件资源配置合理的共识组个数,从而高效利用 CPU 资源。
  • Memtable State of the Art 数组:优化场景是解决 Memtable 读写删并发互相影响的性能问题,我们通过设计符合时序场景的 State of the Art 数组结构,显著提升了存储引擎 Memtable 的并发性能。

在集群易用性 & 鲁棒性显著提升方面,我们也做了非常多的工作

  • Region 迁移和 DataNode 缩容是 IoTDB 动态扩缩容能力的基石,针对在迭代过程中遇到的分布式状态维护难(多个节点的内存和磁盘均维护状态)、沟通成本高(牵扯模块多)和复杂场景多(考虑若干故障场景)的问题,宇衡、湘鹏、珍姐和我在考虑众多因素后进行了详细设计和开发。为了保证最终交付给用户的功能质量,我们进入了测试的 Bug 拉锯战,这个过程堪比系统组的诺曼底登陆,无数精力都投入在鲁棒性的打磨上。经过近一年的打磨,现阶段的 bug 已经基本收敛,功能也逐步从不可用到基本可用,我们已在若干实际用户场景中实践了该功能,2025 年我们已经不再畏惧上万次迁移和 TB 级别迁移的场景。
  • 在易用性 & 鲁棒性方面,我也和宇衡、雨峰、文炜、湘鹏和荣钊一起进行了大量的完善,包括但不限于配置文件三合一、热更新加载参数缺失配置项恢复默认值、Set Configuration 集群更新配置语句、Region 重建/增删副本、Verify connection 检测网络连通性、CLI 激活 & 机器码缩短、Procedure 维护(10+ commit)、WAL 阻写默认阈值与磁盘大小绑定、CN 脑裂问题修复、节点启停流程双军问题工程完善、RTO/RPO 优化、多数据库负载均衡、多数据库创建保护机制、激活代码同步冲突处理等。这些工作显著提升了 IoTDB 的稳定性和用户黏性。

在 IoT-Benchmark 方面,今年我们梳理了其项目结构和 README,使其逐步向更通用的时序基准测试工具演进。过去一年钰铭作为主力带领我们迭代了近 100+ commit,包括 50+ 稳定性修复和易用性提升、以及 10+ 性能优化。

在持续集成与迭代体系方面,今年在王老师的指引下,我们引入了对第三方库的 SBOM 管理,并开始使用 NVD 扫描并持续追踪开源项目中的 CVE 问题,从而逐步提升了对第三方依赖漏洞安全问题的重视。此外,我们还开始统计 IoTDB 的代码量,以评估代码复用效果和项目的复杂度。与此同时,我们也意识到,随着产品功能和复杂度的不断增加,测试用例的指数级增长与产品迭代效率之间存在一定的 trade-off。结合每天晚上和周末 CI 机器几乎都在空闲的现状——每周 168 个小时中,只有大约 1/3 的时间 CI 机器在工作,其余 2/3 的时间处于闲置状态——我与钰铭开始探索将 CI 拆分为不同级别的测试体系,包括 commit、daily 和 weekly 级别的测试。我们在 commit 级别保留最为关键的 CI 测试,在 daily 和 weekly 测试中充分利用闲置的机器资源,补充更多的测试用例。同时,我们也将引入智能化策略,自动识别并追踪有问题的 commit,从而在开发效率与质量保障之间找到更好的平衡点。

在海量项目支持这块,我则个人负责了若干探索性项目并参与了很多实际项目

  • 在某一关键领域的大客户项目中,我担任了技术架构师和部分项目经理的角色,与乔老师、祥志、洪胤和高飞学长从需求调研到业务建模,再到推动内核迭代优化,几乎将自己毕生所学都倾注其中。庆幸的是,今年我们的工作得到了双方的认可。我们的工作包括但不限于:特定领域数据时序数据库一体化建模与应用的可行性验证,较原生系统现有资料数据性能提升超过 10 倍,下一代负载更大 10 倍的资料数据从不可写转为可写;我们还实现了该领域首个针对多维查询的异构多副本高可用方案,性能提升了一个数量级;结合具体场景,我们对 IoTDB 的 LSM 引擎进行了架构优化,使 0 层 TsFile 文件大小提升了 300 倍,系统从不可用变为可用;此外,我们还针对大文本数据进行了关键技术演进,包括零拷贝和内存池优化等。最终,我们还预留了若干内核优化,期待在 2025 年继续打磨完善。这个极具性能挑战的业务场景,不仅让我在高压下不断突破自己,也让我更加深刻地体会到系统设计的乐趣。
  • 除了预研项目外,我还通过项目工时表统计了自己今年 6-12 月共 7 个月参与的项目耗时,共计 257 小时,平均每个工作日 2 小时。具体支持的内容包括但不限于与竞品 PK 并取得胜利、撰写各种报告和文档、售前(包括纯英文售前)、oncall、写标书、评奖答辩和技术分享等。这些持续的项目和业务侧投入,使我能够始终接触产品一线,从一个更全局、具备发展眼光的视角去平衡不同工作的优先级,并理解一个 2B 创业产品需要关注的方方面面。在此,也特别感谢佳哥、红岩以及其他项目组的同事,帮助内核团队承担了大量的线上压力。

在海量技术沉淀这块,则基本是我们出于技术 & 业务双驱动完成的很多探索

  • 在技术推送方面,今年我们组协调产研团队共发布了 19 篇技术推送,其中我们组独立完成了 10 篇,包括《分布式三部曲》系列、监控系列、与 HBase/InfluxDB 的对比系列、TPC 系列等。我们公众号的推送视角也逐渐从纯技术视角转向了用户视角,开始更加关注公众号目标用户的实际需求。现在,我也在跟随旋哥一起审核并整理一些 FAQ 问题,期望能够解决更多开源用户的问题。此外,我还在知乎上宣传了 IoTDB 在分布式架构下的细致考虑,收获了不少关注和反馈。
  • 在学术成果方面,今年我们组产出了 1 篇软著、4 篇专利和 3 篇论文(2 篇在投),涵盖了我们组负责的负载均衡、共识算法、时序基准测试工具和 TPC 登顶等方面的工作。这些成果得益于去年王老师和东哥的要求,促使我们组不断沉淀并输出成果,在与同行交流的过程中激发了更多的创新点。
  • 在 JVM GC 探究方面,今年俊植和我一起举办了 GC 讨论班,我们对 JDK8/11/17 的默认 GC 算法 PS 和 G1 的原理和所有可调优参数都进行了研究和分享,我们也整理了相关 Cook Book 便于更多的同事能够参照流程图进行 GC 调优。我们也完善了 IoTDB 启动相关的 GC 参数,使得默认的 GC 参数是我们实践得到的最优选择。此外关于默认 GC 算法到底应该选择 PS 还是 G1 的问题,团队内部有很多争论和质疑,由于默认总需要选择一个 GC 算法,而不可能有任何一个 GC 算法在所有维度(例如吞吐,最大暂停延迟,稳定性和内存占用等)都能够超过其它 GC 算法。为了避免大家在这里耗费太多精力(例如“我发现某个场景 PS 更好”,“我发现某个场景 G1 更好”,“我觉得应该 xxx”),我们组结合过去两年对 GC 算法的研究和所有的对比案例整理了一个文档,得出了在我们所接触的所有场景里默认使用 G1 对于盲测更优,如有特定需要可调整为 PS 的方案。通过这种方式,我们平息了大家对这块的时间投入,能够让大家抽出更多的精力去专注于其他更重要的事情。
  • 在默认推荐 JDK 版本升级方面,23 年俊植和我曾尝试将 IoTDB 的默认推荐 JDK 版本从 JDK8 升级为 JDK17。然而,升级后冯老师发现写入、合并和导入导出等功能的耗时均有增加,经过近一个月的排查未果,我们暂时搁置了该问题。今年在调整 GC 算法默认参数时,我们意外发现,JDK17 性能下降的原因是 JDK15 之后默认关闭了偏向锁。我们在 JDK8 环境下关闭偏向锁也能复现类似的耗时增加现象。进一步排查后发现,IoTDB 内部的某些文件 IO 基础类过多使用了 synchronized,导致偏向锁取消时性能回退。通过优化这些基础类,我们解决了性能下降的问题,使 IoTDB 从 1.3.2 版本起默认推荐 JDK 17 部署。此举不仅让 IoTDB 的默认推荐 GC 算法从 PS 改为 G1,还为我们未来利用如 Vector API 等 JDK 高阶功能奠定了基础。
  • 在 JVM 非堆内存上涨问题方面,23 年我们团队已将 JVM 内存划分为堆内内存、堆外内存和非堆内存。今年,在某用户环境中,我们发现配置好 IoTDB 的堆内和堆外内存后,整个进程占用内存依然不断上涨,最终被 OOM-Killer 杀掉,说明非堆内存出现泄漏。通过结合 NMT 工具和 Oracle 官网文档对非堆内存进行分析,俊植和我提出了 IoTDB 内存配置的安全部署公式,虽然解决了线上内存不断增长的问题,但由于用户不愿进一步在生产环境中帮助我们确认原因,问题的根因排查暂时被搁置。幸运的是,由于我们开源了我们沉淀的 JVM 内存管理文档,一家广州创业公司的程序员联系到了我们,他们的 Java 服务也复现了该问题。在进一步沟通后,我们定位到这个场景的问题是由于 JVM 的默认内存分配器 glibc 缓存机制引起的,经过更换为 jemalloc 后,内存 RSS 稳定不再出现泄漏。这一经历也让我们更加深刻地认识到沉淀和分享技术的重要性。
  • 在访存瓶颈零拷贝优化方面,23 年我们发现,在一些大文本场景中,IoTDB 的 CPU、磁盘和网络均已经不再是瓶颈,反而是访存成为了瓶颈。今年,思屹和我系统整理了 IoTDB 的写入 RPC 请求从网卡到磁盘的端到端拷贝次数和访存次数,发现了 Thrift 框架中可优化的零拷贝部分。通过该优化,IoTDB 在 44KB 大文本场景下的写入吞吐提升了 35%。由于零拷贝技术需要完善控制对象生命周期,我们尽量平衡了性能收益与代码侵入性,首先优化了性能提升最大且生命周期控制最容易的客户端 Server 和共识 Server 部分。我们也为未来的扩展预留了接口,目前流处理组的宇辰已经开始尝试进一步引入客户端零拷贝来优化性能。
  • 在 JVM 内存池优化方面,针对大文本场景中的 GC 问题,思屹和我仔细梳理了 GC 触发的根本原因,并分析了 Java 与 C++ 在内存管理上的差异。C++ 允许开发者手动管理对象生命周期,并显式申请和释放内存,而 Java 则通过后台的可达性分析机制异步回收内存。由于这一机制,在处理大对象(如 byte[]、long[] 数组等)时,Java 在频繁的大内存申请与释放过程中容易引发较多的 GC,消耗大量 CPU 资源并影响程序性能。这本质上也是 C++ 和 Java 在开发者心智负担与性能之间的 Trade-off。因此,我们设计并实现了一个支持变长 byte[] 的内存池,提供了手动和自动两种接口,允许调用者选择是否手动管理生命周期来池化 byte[]。手动接口需要牺牲一定的开发者体验,要求实现引用计数机制来显式归还,但能获得更好的性能;而自动接口则基于虚引用机制实现内存的自动归还,性能相对较差,但仍优于不做池化的方式。该内存池还引入了基于 EMA 算法的主动驱逐策略和基于 JMX 的 GC 感知被动驱逐策略,确保性能提升的同时,不会引入新的稳定性问题。根据我们的测试,在 4KB 至 1MB 的大文本场景下,写入性能提升了 6% 到 71%,GC 从很严重降低到几乎没有。未来,我们计划将所有 JVM 大对象数组接入该对象池,从而消除大多数 GC 开销。
  • 在多 NUMA Node 机器性能优化方面,今年思屹和我在一台 192C 768G 内存的 4 NUMA Node 机器上进行了 IoTDB 性能提升的探索。我们首先尝试了单进程优化,发现 JDK 17 之后的 G1 垃圾回收算法已支持 NUMA 感知,但仅限于新生代,对于老年代的内存访问仍然存在较多的跨 NUMA Node 访问。对于 Java 来说,老年代内存可以通过第三方库如 Thread Affinity 进行核绑定,这要求访存和管存线程在同一个核上运行。然而,这种方式对内核侵入较大,因此我们推荐直接使用 JDK 17 以上版本自身的能力进行单进程优化。尽管单进程优化的空间有限,但我们发现多进程优化具有较大突破潜力。通过将每个 IoTDB 进程使用 numactl 命令绑定到一个 NUMA Node,我们能够以极小的代价显著提升性能。在该物理机上进行的 1 进程与 4 进程绑核的性能对比测试表明,读写性能最高可提升 1 倍,使用 Intel Vtune 工具观测到的跨 NUMA 带宽也显著降低。通过这一探索,IoTDB 在多 NUMA Node 机器上的高性能部署方案得到了进一步的优化和完善。
  • 在外部技术输出方面,今年宇衡在持续追踪一个影响 IoTDB 运行的问题时,发现了一个 GraalVM 编译器的 bug,在将其提报到 Oracle 社区后,得到了认可并快速修复。此外我在解决一个线上 WAL 堆积问题时,发现根因是 Thrift AsyncServer 的 Epoll 存在 bug,通过深入研究代码,我明确了具体原因并推动 Thrift 社区最终解决了该问题。这些反馈也引发了我的深思:对于一个追求普适性的复杂产品,稳定性和兼容性往往需要最优先考虑,那么那些出于性能优化驱动的新技术(如异步 server、异步 I/O、direct I/O、虚拟线程等)便需要谨慎使用。即便使用了,也应提供开关。因为只要延迟够用,吞吐基本可以通过横向扩展来提升,不必过度依赖那些看起来非常新颖但可能不够稳定的技术。特别是在当前国产化的背景下,如果这些技术没有在各种硬件和操作系统环境中做足够的测试和完善,线上问题一旦发生,oncall 的体验将非常痛苦,且会对用户和产研团队带来更大的负担。
  • 在更多的探索尝试方面,今年我们还有 6-7 个尚未合并到主分支的硬核技术尝试工作,具体细节暂不展开。期望这些工作能在 2025 年不断完善,为 IoTDB 2026 年通用场景盲测性能的摩尔定律节奏奠定基础。

今年,我在 Apache IoTDB 社区提交并被合并了 84 个 PR(去年 119 个),Review 了 509 个 PR(去年 387 个)。相比去年,今年我的大部分精力都集中在贴近业务和技术管理上,也对个人和团队如何最大化产出有了更多思考和感悟。今年 8 月,我受邀成为 Ratis 社区的 Committer,并成功拥有了 1k Github Follower,这让我更加认可自己在开源领域的专注。回顾过去一年,我觉得我们成功将团队的许多工作通过各种方式沉淀下来,并影响了许多人。希望我们能始终在这段青春年华中保持对技术的热情,专注于我们的工作继续前行。在这里,我特别感谢我的女朋友🍊 始终支持我的工作并帮助我疏解情绪,让我感受到生活的美好与幸福。她还带我见识了许多新事物,让我对人生的很多方面有了新的认识和思考。

一些感悟

介绍完充实的 2024,回顾 2023 年终总结,可以发现今年我们在去年四个维度的展望上都取得了不错的成绩

  • 做深:我们组输出了大量工程技术沉淀,也产出了 4 项专利、3 篇论文(2 篇在投)和 1 项软著。
  • 做广:上半年与存储引擎合作,下半年与流处理引擎合作。
  • 做好:我们显著提升了系统稳定性,降低了纯分布式模块的 oncall;通过若干内核功能优化,提升了易用性;同时,我们也通过分布式、存储引擎和系统优化组合拳打造了 IoTDB 通用场景盲测写入性能的摩尔定律节奏,并预留了三年的余量。
  • 做响:我们成功完成了 TPCx-IoT 登顶工作,并获得了央视报道;此外,我们小组的技术输出总阅读量达 2w+,提升了团队的知名度和影响力。

下面分享一下我今年的很多成长感悟,欢迎大家批评指正。

稳定性打磨工作如何评估时间

对于一个稳定性打磨的功能,如何评估其完善时间?在打磨 Region 迁移的稳定性时,我今年思考了很久。如果考虑无数硬件环境(如 4C16G 和 192C768G)、测试负载(实时写入、读写混合),再加上注入各种异常(如节点重启、网络分区、断电等),以及多个模块功能的组合(如多级存储、存储引擎、共识层、流处理引擎等),可以看出它们的组合是指数级扩展的。即使研发进行了完善的设计与实现,但提测后仅测试完善的开销就几乎永无止境。但如果一开始就定下工作周期为半年或一年,也难以做出可靠的过程管理来向上汇报。

在这种困境下,我们必须意识到场景是无限的,在精力有限的情况下,我们的目标是用最小的研发和测试代价解决尽可能多的 bug。最初,我们按照研发视角将功能的稳定性迭代分为 V1、V2、V3,期望逐步打磨到稳定状态。然而在实际测试中,我们发现测试视角与研发视角并不同频,导致测出的 bug 比较分散,尽管测试与研发一同打磨很久,仍难以向上汇报阶段性成果,因为每个模块都有不少 bug 被修复。这使得这个工作看起来像是无底洞,且容易受到质疑。其实,问题的根本在于缺乏多方共识的客观评价标准。

回顾整体流程,我认为可以在以下两个方面做得更好

  • 避免测试开销的指数级扩展:对功能中的核心模块,尽量做好抽象并补充完备的测试。在功能从 UT、IT、研发自测、测试自测、用户 POC 到用户线上等各个环节中,越早发现问题,整体时间成本就越低。更有趣的是,这种流程优化能潜移默化提升整个团队的效率。同样工作 8 个小时,高效与低效的差距,对于软件开发团队的影响会非常的大,这也是我未来需要持续反思与提升的地方。
  • 多方对齐:对于稳定性迭代工作,需要研发、测试和产品在功能、性能和测试场景等多个维度上提前对齐优先级,并将工作分配到 V1、V2、V3 版本中。对于每个小工作项,能够精确预估时间;对于大工作项,提供概要预估时间即可。这样即便出现延期,团队也能清楚了解目前功能的进展,哪些场景可以交付给用户,哪些还需改进。这会大大提高工作的透明度和效率。

软件工程没有银弹

今年有件让我深受感触的事,那就是发现大家对 IoTV2 共识算法的价值产生了质疑。从纯研发的视角来看,IoTV2 显然在创新性和性能上都显著超越了 IoTV1,是我们组过去几年最具创新性的工作之一,其他共识算法也都花了好几年才稳定,IoTV2 毕竟才诞生一年。但如果想在开发团队中获得更广泛的认可,就需要考虑大家关注的不同方面,包括稳定性、创新性、问题收敛程度、潜在收益与投入的平衡等。可以看出,这些维度之间往往存在矛盾,而且很难得出绝对客观的结论,很多东西也完全看未来的事在人为,因此很难在所有人中达成共识。这让我意识到,当一个软件项目和团队发展到一定阶段后,是否落实创新工作,往往会面临保守和激进的分歧,二者需要不断博弈与制衡,才能走向一个可行的方向。完全激进或者完全保守都会带来不可预知的风险。

回到 IoTV2,我们能够平息质疑的一个重要原因是我们做了共识层的抽象,能在一套接口下支持不同的共识算法,从而使得各个共识算法可以单独迭代。如果没有这个统一的接口,不管 IoTV2 作为业界第一个多副本超越单副本的共识算法有多么创新,仍然会面临无数质疑,甚至可能导致无法迭代。而对于竞品来说,除非照抄 IoTDB 整体共识层的设计,否则也很难平息内部质疑,全力推进这项工作。这为我们未来的扩展性设计提供了指导——良好的接口抽象能够使得系统的关键迭代从“不可能”变为“可能”。当然,软件工程没有银弹,即使我们通过共识层的抽象让 IoTV2 的迭代得以顺利进行,但代价就是翻倍的测试和打磨开销。总体而言,抽象得越好,复杂度封装得越到位,测试和打磨的开销也就越低。

个人的管理成本 ROI

在今年参与更多技术管理工作后,我渐渐关注到个人管理成本 ROI 这一概念,其本质是消耗尽可能低的 +1 管理成本(包括时间和资源等)完成更复杂的工作,并在有风险时及时汇报并提供辅助决策的数据。

总体而言,不同的人有不同的管理风格,同一个人对于不同的事情也会采取不同的管理方式。有时像《大明王朝 1566》中的嘉靖一样,只关注结果,不拘过程;有时又像《大决战》中的 101 一样,会关注每一个细节。其实,不论是哪种管理风格,最终目标都是完成工作,并没有绝对的好坏之分。

对于我们个人而言,我们控制不了别人,唯一能够不断改善的就是提升自己的管理成本 ROI。通过这种方式,我发现能够使得自己与他人的合作变得更加高效和愉快。类似的例子包括但不限于

  • 完成一个 PR 后,补充详细描述和充足的测试用例,并至少自己 Review 一遍,再让 +1 Review,这样 +1 只需花费极小代价即可合并 PR。
  • 对于自己负责的工作方向,如果涉及到非常多的琐碎事项,主动周期性汇总关键点并屏蔽细节与 +1 沟通,让其在最短时间内了解现状并能进一步向上汇报,不要让他消耗很多时间去整理细节。
  • 在几乎不消耗 +1 时间的情况下,完成复杂架构设计并获得原本 +1 需要沟通的人员认可,让 +1 仅需做最终决策。
  • 在发现项目潜在风险时,及时整理现状和信息与 +1 沟通,评估是否需要更多资源,确保项目整体风险可控。

这些经验很多是在与我们组俊植一起进步的过程中学到的。希望自己能像俊植一样,不断提升自己的管理成本 ROI,进而锻炼出更好的职业素养。

十倍程序员如何进一步提升

今年年中,我读了润基哥哥的十倍程序员文章,受益良多。对于十倍程序员的成长,本质上有两个方面:

  • 基于延迟的纵向扩展:这既包括以前做不到的事现在能做了,即延迟从无穷大变成了可量化的数字,也包括以前能做的事现在能更高效完成,即延迟更低。
  • 基于吞吐的横向扩展:这包含了能够带领团队在单位时间内并行产出更多成果,这中间需要避免单点瓶颈和负载不均衡现象,才能发挥出团队最大的力量。

从纯个人能力上来看,可以按照上述思路进行纵向和横向扩展,但今年我也意识到人力始终有限,战略上选择一个正确的方向才是事半功倍的关键。所以今年除了个人业务能力的提升,我还积累了很多做人做事的经验。对于很多事情的可行性,我不用再依赖他人的意见,而是能够自己进行主观判断。希望自己能在这个方向上继续沉淀,用靠谱的战略指引自己不断成长。

决定不做什么往往比决定做什么更难

其实这个感悟与战略类似,说一个应该做的事情只需要十秒钟,然而将这个事情具体落地可能需要十周甚至十个月的时间。人力始终有限,尤其对于一个软件团队来说,面对无数的输入和决策指引,客观上这些工作不可能面面俱到,必须进行战略性取舍。

决定做什么往往没有太多压力,因为人性中总有一种“即使失败了,没有功劳也有苦劳”的自我安慰。但如果要决定不做什么,则必须对自己的业务和竞争力有深刻理解,出于提高人效的角度思考且愿意承担政治责任,才能最终说服别人。这种决策十分困难且珍贵,但也正是许多高效团队能够成功的关键。

今年,我有幸在博士生组会中跟随王老师龙老师带领的实验室团队学习时序 AI 大模型的落地思路。虽然王老师龙老师一直强调我们现在已经做好了“存数”,接下来要把“用数”做好,但他们也明确通过案例分析告诉我们,哪些 AI 项目是靠谱的能够最终产生实际价值,哪些 AI 项目是不靠谱的做了也只是白白耗费精力不创造实际价值。这种战略定力和担当,让我深受触动。

人性的惯性是根据情绪和结果来评价过程

尽管我一直对历史很感兴趣,很多大道理早已听过,但今年在工作中,我从切身实践中获得了一个深刻的感悟:任何事、任何人都会有正负面的影响和评价,不存在一个完人,也不存在一个能够得到所有人认可的方案。

人性大多数时候是非常真实的,大家常常根据结果来判断过程。如果事情没有做成,就会有人列举一堆负面评价来解释为什么失败;如果事情做成了,又会有人说一堆正面评价来证明“早就看他行”。但实际上,成与不成,除了人的因素,还很大程度上取决于天时地利,而这些天时地利,作为非人为因素,反而会深刻影响最终大家的评价。此外,尽管我们都在强调要客观理智,但根据我的观察,大多数人,包括我自己,也曾在情绪驱动下做出一些非预期的行为,并且不断强调自己并没有被情绪左右。

意识到这些后,我明白了很多事情,要么不做,要做就尽己所能做到最好,不必过多顾虑他人的评价。只有这样,才能避免不必要的内耗和沟通成本,将精力集中在更重要的事情上,这样反而更有可能将事情做成。

刚柔并济才能实现可持续发展

在今年的工作中,我逐渐有了一些中庸之道的感悟。每个人都有不同的特点,要凝聚一个多样化的团队,需要更多的包容性和开放性。过于从自己的视角偏重某一维度,反而可能导致适得其反,产生“过刚易折”的效果。在做人做事时,面对大的目标和原则性问题时,我们要坚持“刚”;而对于那些不影响最终目标的小细节,则可以选择“柔”。此外,在与许多跨行业朋友的沟通中,我逐渐意识到,尽管我们都在努力工作,但很多事情还是需要天时地利。有时候,顺势而为、蛰伏等待也许是成功的关键。

因此,保持良好的工作心态,营造融洽的工作氛围,在自驱保证自我成长的基础上,不必过于纠结于远方的目标,而应专注当下刚柔并济,才能实现可持续发展,并与团队一起走得更远。

如何平衡个人输出和团队输出

今年对我个人的时间管理和抗压能力来说是极具挑战的一年。上半年临危受命接管存储组,scope 显著扩大,团队人数相比去年接近翻倍。由于一些原因,无法进一步进行分级管理,这对我个人精力提出了极大挑战。基本上,我每天都在不断地线程切换,盯着十几二十个事项。尽管我已经转变为“没有深入参与时间,只略微沟通过程便要结果”的最低投入策略,但由于我依然是单点瓶颈,很多进展缓慢的事情和无人处理的 bug 需要我来当“救火队员”。我的时间依然远远不够用,一旦我在个人处理的某个问题上阻塞了一两个小时,那基本上会造成四五个问题的连锁阻塞。高压状态下,这种情况对我个人的心态和情绪也产生了一定的影响。幸好,下半年江天学长挺身而出,接过了存储组的压力,帮助我们组的人数恢复到了一个相对合理的规模,让我有更多精力去探索和深度参与我们组的很多工作。

现在回想这段经历,我意识到一个人的合理管理半径不应超过 10 个人。在这个范围内,能够在个人输出和团队输出之间取得一个良好的平衡。此外,只有与团队一起成长、大家自驱地去做事,才能在不线性增加时间和精力投入的情况下,扩展管理半径。

与指数增长团队一起指数增长

即使是同一个人,在不同的年纪,对于金钱、工作氛围、健康和工作生活平衡等方面的追求都会有所不同。但一直以来,驱动我前进并屏蔽这些外部欲望的动力,始终是如何在单位时间内获得更多的成长。随着时间的推移,我逐渐意识到,能够承担越来越大的责任,并创造更多的价值,才是个人成长的核心所在。

固然我们可以在任何地方按照这个思路去追求自我成长,但只有在一个增量团队中,团队和个人的双指数增长才更容易实现。希望大家可以找到与自己 match 的指数增长团队。

来年展望

通过这一年,我们为 IoTDB 在技术上构建了摩尔定律的成长节奏。幸运的是,这些技术积累也立即在影响力和营收上得到了体现。希望在新的一年,无论是我个人还是团队,都能继续保持这种摩尔定律般的成长节奏,推动更多的技术创新和业务突破。

最后,在除夕之际,预祝大家新年万事如意,心想事成!愿每个人在新的一年中都能够事业有成,收获满满!

2023 年终总结:从清华 Apache IoTDB 组到创业公司天谋科技

2024-02-07 15:24:52

前言

兜兜转转又是一年,不知不觉 2023 已经结束。回想自己过去一年的成长与感悟,依然觉得是收获满满。今年工作之后闲余时间相比学生时代少了许多,到了除夕才有时间来写今年的年终总结。好在自己还是下定决心将这个习惯坚持下去,希望这些年终总结不仅能够在未来的时光里鞭策自己,也能够获得更多大家的反馈来修正自己。

首先依然是自我介绍环节,我叫谭新宇,清华本硕,师从软件学院王建民/黄向东老师。目前在时序数据库 Apache IoTDB 的商业化公司天谋科技担任内核开发工程师。我对分布式系统、可观测性和性能优化都比较感兴趣,2023 年也一直致力于提升 Apache IoTDB 的分布式能力、可观测性和写入性能。

接下来介绍一下我司:

天谋科技的物联网时序数据库 IoTDB 是一款低成本、高性能的时序数据库,技术原型发源于清华大学,自研完整的存储引擎、查询计算引擎、流处理引擎、智能分析引擎,并拓展集群管理、系统监控、可视化控制台等多项配套工具,可实现单平台采存算管用的横向一站式解决方案,与跨平台端边云协同的纵向一站式解决方案,可方便地满足用户在工业物联网场景多测点、多副本、多环境,达到灵活、高效的时序数据管理。

天谋科技由全球性开源项目、Apache Top-Level 项目 IoTDB 核心团队创立。公司围绕开源版持续进行产品性能打磨,提供更加全面的企业级服务与行业特色功能,并开发易用性工具,使得 IoTDB 的读写、压缩、处理速度、分布式高可用、部署运维等技术维度领先多家数据库厂商。目前,IoTDB 可达到单节点每秒千万级数据写入、10X 倍无损压缩、TB 数据毫秒级查询响应、两节点高可用、秒级扩容等性能表现,实现单设备万级点位、多设备亿级点位管理。

目前,IoTDB 能够为我国关键行业提供一个国产的、更加安全的、性能更加优异的选择。据不完全统计,IoTDB 已服务超 1000 家以上工业企业,在能源电力、钢铁冶炼、航空航天、石油石化、智慧工厂、车联网等行业均成功部署数十至数百套,并扩展至期货、基金等金融行业。目前已投入使用的企业包括华润电力、中核集团、国家电网、宝武钢铁、中冶赛迪、中航成飞、中国中车、长安汽车等。

2023

介绍完背景后,在这里回顾下 2023 年我们系统组的主要工作,可分为高扩展性、高可用性、可观测性、性能优化、技术支持和技术沉淀 6 个方面。

在高扩展性方面,我们主要做了以下工作:

  • 计算负载均衡: Share Nothing 架构面临的主要挑战之一是扩展性问题,扩缩容过程中需要迁移大量数据,这不可避免地消耗系统资源,进而影响现有的读写性能。为了解决这个问题,Snowflake 带头在业界推广了存算分离的架构设计,近年来的 Serverless 架构则进一步追求了更极致的弹性。尽管存算分离架构能够避免在扩缩容时迁移大量数据的问题,但它仍面临着冷启动问题。也就是说,当一个计算节点宕机后,从对象存储服务恢复宕机节点数据的过程可能会比较耗时,这对于对 SLA 要求极高的应用场景构成了挑战。那要如何解决这一问题呢?在 VLDB 2019 的论文中,ADB 介绍了其架构解决方案,其中一个值得注意的点是,它对本应无状态的 ReadNode 实施了热备份。虽然论文没有解释为何采取这种做法,但很明显,这种方案可以通过增加机器资源消耗来确保 SLA 指标,从而进一步说明了面对不同业务场景和问题时,不同架构可以找到更加适合的 trade-off。针对 Share Nothing 架构的扩展性问题,乔老师今年引导我们探讨了在时序场景中是否可能避免扩缩容时的数据迁移。我们发现,相比传统的 TP/AP 场景,时序场景有几个不同之处:首先,读写负载相对更加稳定可预测;其次,大部分情况下数据的时间戳会呈现正态分布,并随着时间不断递增。这为我们提供了结合场景进行优化的可能性。我们通过将数据划分为不同的时间分区,并在新的时间分区到来时进行实时负载均衡分配,从而实现了无需迁移数据即可达到计算资源均衡的效果,甚至在运行 TTL 时间后,还能进一步实现存储和计算资源的双均衡。回顾我们的设计,通过牺牲新节点立即提供服务的能力,我们避免了扩容时的数据迁移,这在大多数负载可预测的时序场景下取得了良好的效果。当然,对于一些特殊场景,我们也提供了手动 Region 迁移的指令,以便运维人员根据业务需求,在存储和计算资源的平衡时间上进行手动调整。
  • 分片分配算法:在今年上半年针对某用户的 12 节点 2 副本场景进行高可用性测试时,我们遇到了一个问题:当我们故意使一个节点宕机后,发现另一个节点出现了 OOM 现象。深入分析后,我们明白了问题所在:由于整个集群仅有 6 个副本集合,一个节点的宕机导致约 1/6 的 Region Leader 被迫迁移到了同一节点上,这导致了该节点过载,进而出现 OOM。其实这一问题是一个典型的分片分配问题。我们调研学习了来自 Stanford 的 ATC 2013 Best Paper Copysets 论文以及该作者两年后在扩缩容场景对 Copysets 算法的补充,并决定将该算法应用到 IoTDB 中。通过这一改动,客户场景中的节点散度从 1 增加到了 5.11,这意味着当单个节点宕机时,多个节点能够分摊待迁移 Leader 的压力,有效避免了 OOM 现象的发生。此外,集成 Copysets 算法还带来了其论文提到的对于数据丢失概率和副本恢复速度的提升。回顾这项工作,最让人印象深刻的是陈老师的深厚算法功底。在我们努力理解论文理论证明的过程中,陈老师补充了论文中遗漏的公式证明。当陈老师引入泊松过程的概念时,我们尚能跟上步伐;然而当陈老师引入指数型随机变量和连续马尔可夫链的概念时,我们只能赞叹:天不生陈老师,飞书 Latex 公式功能万古如长夜了。
  • 企业版激活:对于企业版软件实现可信授权,我们面临多项挑战:如何在不依赖网络的情况下部署,同时通过硬件绑定来防止许可证的滥用?如何设计一个系统,让激活次数不再受节点数量的限制,以提高整个集群的激活效率?我们还需要引入一系列的使用限制,包括许可证的有效期、节点数、CPU 核心数、序列号和设备数等等。此外,还需考虑如何防止各种潜在的破解尝试,比如回调系统时间、复制文件目录、在使用相同机器码的云平台上部署等,同时保证这些安全措施不会影响到商业用户的使用体验,例如支持非 root 用户激活、提供一键激活功能等。面对这些问题,我们逐一制定了解决方案并加以设计实现。在这个过程中,我和宇衡对各种 Corner Case 进行了深入的分析和讨论,这段经历让我受益匪浅。

在高可用性方面,我们主要做了以下工作:

  • IoTConsensus:在过去的一年里,我们针对基于异步复制思路的 IoTConsensus 共识算法,在性能、稳定性、鲁棒性和可观测性方面做出了显著提升。如今,在线上的大部分场景中,该共识算法已经被优化至接近实时同步的效果,基本上不会再出现因为同步速度跟不上写入速度而导致的 WAL 堆积现象。接着我们开始思考一个命题:在异步复制系统中,不考虑节点宕机等异常情况,是否能在任何写入负载下都保持同步速度与写入速度同步?通过对 MySQL binlog 异步复制等类似场景的观察,湘鹏和我通过排队论的论证和性能实测发现,这个假设是错误的。这一发现促使我们开始进一步探索和设计基于操作变更到状态变更的共识算法。尽管理论上 Leader 侧的 WAL 堆积问题似乎无解,但在实际工程应用中,我们找到了解决办法。我们不仅在多个方面迭代优化以减少 WAL 堆积的可能性,还特别总结了导致 WAL 堆积的八大潜在原因及其解决策略。目前,我们团队已有许多成员能够独立地诊断并解决这一问题,有效地消除了这一单点瓶颈。
  • RatisConsensus:今年,我们对 Apache Ratis 社区做出了显著贡献,包括引入了基于 Read-Index 和 Lease Read 的线性一致性读功能,以及若干状态机易用的 API。我们还提高了 Snapshot 传输的稳定性,并提交了超过 30 个 patch,涵盖了各种 bug 修复。除此之外,宋哥不仅多次担任 Ratis 社区的 Release Manager,近期还荣幸被邀请成为 Apache Ratis 社区的 PMC 成员。宋哥作为目前 Ratis 社区 Top3 活跃的开发者,已经时常被我们开玩笑称为 Ratis 社区 Vice PMC Chair 了。
  • 共识层:去年,IoTDB 共识层参考了 OSDI 2020 Best Paper Delos 的思路进行了设计和实现,支持了多种具有不同一致性和性能特性的共识算法。今年,我们在性能与一致性级别这两个维度上对其支持的不同共识算法进行了深入的对比分析,为 IoTDB 的实施及用户在选择共识算法时提供了重要参考。我们还广泛调研了多种数据库的共识算法实现,通过文档阅读、代码走读和性能实测等多种方法,从共识算法的功能和性能开销等多个角度进行了细致地对比,并取其精华,去其糟粕。此外,今年我们在一些内存紧张的特殊场景下,发现 IoTConsensus 可能会出现副本不一致的问题。经过排查,我们认识到问题并非出在共识算法本身,而是由于状态机执行的不确定性导致的。虽然理论上根据 RSM 模型,所有副本应当达到一致状态,但在实际工程实践中,许多问题都可能使得 RSM 模型不完全适用,比如 Leader 的磁盘写满而 Follower 的磁盘未写满,可能引发执行的不确定性。针对这一问题,我们咨询了曾在 OB 工作的剑神,并在知乎上发问探寻大佬们的解决思路。收到的许多反馈都倾向于“Fail Fast”的处理原则,这可能是因为对许多 TP 系统而言,一致性比可用性更为重要。然而,对于时序场景,可用性往往比短暂的不一致性更加重要。因此,我们认为在遇到此类问题时直接退出进程并不是一个合适的解决方案。为此,我们通过在共识层捕获此类异常并采取有限重试的策略,以避免让业务感知到这种现象,从而保证了系统的高可用性和一致性。

在可观测性方面,我们主要做了以下工作:

  • 监控面板:今年,我们借鉴了火焰图作者在《性能之巅》中的思路,从用户视角和资源视角出发,构建并完善了四个监控面板,共计近四百个 panel。这些面板的建设旨在提供全面的性能监控和分析能力,帮助我们更有效地诊断和解决性能问题。首先,我们设立了 Performance Overview 面板,该面板汇总了集群信息,不仅能帮助我们判断性能瓶颈是否存在于 IoTDB 中,还能进一步拆解并统计不同类型请求的延迟分布,从而精确定位到 IoTDB 内部读写流程的具体瓶颈环节。其次是 System 面板,它聚焦于系统资源,包括网络、磁盘、CPU、线程池利用率、JVM 内存和 GC 等多个维度的监控数据。这个面板为系统资源瓶颈的分析提供了丰富的数据支持,使我们能够从资源层面进行深入分析。接下来,我们还有包含集群节点状态、分区信息等的 ConfigNode 面板,以及涵盖存储、查询、元数据、共识和流计算等引擎监控的 DataNode 面板。这两个面板从不同角度提供了 IoTDB 集群的详尽状态和性能信息,为我们提供了全面的监控视图。在这个过程中,我们团队中也涌现出了包括吾皇,彦桑在内的多位 Grafana 艺术家。他们运用 Grafana 的高级功能,创造了许多既美观又实用的监控面板,所有这些都是各位艺术家精心设计的作品。
  • 监控模块:在过去一年中,随着 IoTDB 各模块可观测性的显著提升,监控指标数量从 100 多个增加到了 900 多个。尽管监控指标数量增加了近 10 倍,但监控模块在火焰图中的 CPU 开销却从 11.34% 下降到了 5.81%,实现了显著的开销节省。这一成就主要归功于俊植、洪胤和我对监控模块的持续迭代和优化。我们不仅对 IoTDB 自身的监控框架进行了大量优化,还结合了 Micrometer 和 Dropwizard 这两个 Metric 库,通过白盒调参或自研选择了对写入操作最友好的实现方式,并针对不同监控指标类型进行了精细化管理。此外,今年雨峰、洪胤和我还持续完善了线上 IoTDB 的巡检文档、告警文档以及面板快照的导出方法等,进一步提升了运维工作的效率和便捷性。通过整个团队一年的共同努力,我们的监控模块不仅大幅提高了问题排查和性能调优的效率,而且已经成为运维 IoTDB 不可或缺的工具。现在我也可以非常自豪地说,IoTDB 现在的可观测性水平已经接近 2022 年暑假我在 PingCAP 实习时体验到的 TiDB 的可观测性水平,在时序数据库中也处于领先地位,这对于我个人和我们组来说是一个巨大的成就。
  • 日志精简:今年,我们注意到 IoTDB 线上环境中日志打印量较大,这在一定程度上影响了问题排查的效率。随着监控面板的日益完善,许多原本需要通过日志记录的性能统计信息已经能够通过监控模块以更高的信息密度进行记录,这使得部分日志变得不再必要。因此,吾皇和我针对 36 个用户和测试场景进行了深入的日志挖掘分析,筛选出了 62 条出现频率较高的日志记录。经过与各模块负责人的逐一讨论,我们对其中 23 条日志进行了降级(例如从 info 降至 debug)或直接删除的优化处理。此外,团队内部就如何打印性能调优、系统关键行为、SQL 执行错误等异常情况的日志达成了共识。通过这次日志精简工作,在不同场景下我们总共减少了约 37% 到 74% 的日志打印量,取得了明显的效果。其实这项工作可大做可小做,但我们还是非常认真地编写了日志分析脚本进行分析,并进行了量化的数据统计和效果预估。完成这项工作后,有一次我和在北大读博做可观测性研究的张先生闲聊,居然发现我们的工作思路与他们领域内腾讯和中山大学在 2023 ICSE 上发表的顶会论文 LogReducer 非常相似。这种巧合让我感到非常有成就感。我们的工作不仅提升了 IoTDB 的运维效率,还与学术前沿领域的研究工作不谋而合,证明了我们的方向和方法是具有前瞻性和实际应用价值的。

在性能优化方案,我们主要做了以下工作:

  • 某知名测试场景性能调优及打磨:今年后半年,我和刚上博一对 IoTDB 几乎 0 基础的谷博共同投入到了某知名测试场景的瓶颈分析、性能调优和内核迭代中。在短短三个月的时间里,谷博迅速成长为一个具备系统思维和深度 IoTDB 调优能力的专家。我们的努力最终获得了显著成果,不仅在该测试场景中取得了第一名的成绩,还通过了第三方的评测。这一成就不仅证明了 IoTDB 1.x 架构的出色性能,也让我们对于 2024 年能够实现更进一步的成绩充满期待。
  • 写入性能优化预研:IoTDB 之前主要集中在列式写入接口的性能迭代,而对行式写入接口的关注不足。鉴于今年许多用户由于各种原因必须使用行式接口,我们迫切需要对行式写入接口进行深入的瓶颈分析和性能优化。借助于我们目前的可观测性能力,以及对各种性能分析工具(如 JProfile、Arthas)的熟练使用,旭鑫和我对可能的性能提升方案进行了大量的 demo 级别预研。针对典型场景,我们已经找到了 5 个主要的优化点,预计完成这些优化后性能将提升一倍以上。当然,性能优化是一项需要持续投入的工作。当把目前发现的主要优化点做进去后,我们也会基于新的 codebase,继续探索新的瓶颈和优化方案。在这个过程中,我们意识到最重要的是积累理论建模能力和系统思维。如何针对任何系统分析当前的瓶颈并提出有效的优化方案,成为了我们在这项工作中积累的最宝贵财富。

在技术支持方面,我们主要做了以下工作:

  • IoT-Benchmark 基准测试工具的发展:IoT-Benchmark 在过去一年中实现了显著的功能提升,特别是在写入能力(跨设备写入)、查询能力(align by device/desc/limit 查询)和元数据建模能力(支持不同 TagKey 层级设置 TagValue 个数)方面。通过持续的迭代更新(50+ commits),我们不仅增强了工具的功能和稳定性,还吸引了其他时序数据库社区的贡献者,如 CnosDB 的开发者就在最近为我们贡献了 CnosDB Client Driver 的代码。我们期待 IoT-Benchmark 能够成为时序数据库领域内公认的基准测试工具,为不同的时序数据库提供一个公平竞技的平台。
  • POC:今年我们组参与了 10+ POC 项目,覆盖了海、陆、空、天等多个领域,并成功部署上线了 95 节点的 IoTDB 集群,实现了 62.6 GB/s 的最大吞吐量和 0.8 以上的集群线性比。参与这些带有挑战性的项目并最终成功落地还是非常让人有成就感的。
  • DBA 宝典:在乔老师的带领下,我们逐步构建了面向 IoTDB 的 DBA 宝典。通过梳理异常排查方案和问题导图,我们为 33 个常见问题提供了原因分析和解决策略。DBA 宝典的存在大大降低了实施团队处理异常的难度,有效减轻了产研团队的 Oncall 负担。
  • Oncall:今年,我个人承担了组内 80% 以上的 Oncall 工作,这不仅是一次对个人能力的极大考验,也是一次成长和学习的机会。通过不断地思考和解决问题,我对 IoTDB 的各个模块有了更深入的了解,并明确了可观测性建设的推进思路。值得一提的是,尽管项目数量还在增加,我的 Oncall 效率已经显著提升,感受到的压力也在逐渐减轻,这与 DBA 宝典的不断完善和实施团队技术支持团队的建立息息相关。

在技术沉淀方面,我们主要做了以下工作:

  • 技术工具:今年我们梳理了常用的 JDK 和 Linux 命令,也用熟了问题排查工具 JProfile 和 Arthas。回想之前看一个 Runtime 的值还需要使用 UDF 去 hack,现在我们直接用 Arthas 就可以了,技术工具的进步极大地提升了我们的生产力。在性能调优方面,除了常见的 JProfile 线程耗时分析和 Arthas 火焰图,权博带领我们探索了 Intel vTune 工具,用于观测高性能机器上的跨 NUMA 访问比例和 CPU 前后端执行效率等。随着 IoTDB 性能优化进入深水区,需要不断将硬件性能进一步压榨,学会使用这些原本 HPC 才可能需要的工具也就非常重要了。
  • 论文讨论班:今年我们组组织了 6 次工程讨论班和 6 次论文讨论班,对 6 个方向的共 15 篇论文进行了分享介绍,其中一些论文已经提供了写入性能的优化思路并 demo 实测有效。这中间最让我印象深刻的还是旭鑫的存算分离讨论班,我们对若干友商的云服务版本进行了计价统计,发现某些号称云原生时序数据库的系统定价显著高于其他时序数据库,我猜测是因为系统架构用了 EBS 而非对象服务吧,那么高成本就只能让用户买单了。
  • JVM:今年我们对 JVM 有了一些深入的探索和技术沉淀。俊植和我细致调研了 Java 的内存分类和观测手段,通过使用 NMT 等工具,我们发现堆外内存分类居然有 19 种之多,这是我在外面的八股中从没看到的结论。在 GC 方面,俊植和我不仅完善了 GC 的可观测性指标,例如不同 GC cause 的次数和耗时以及 GC 占据 Runtime 的比例等等。我们还针对 JDK 8/11/17 的默认 GC 算法 PS 和 G1,分析学习其原理并列举其所有可调参数,搜索优质 GC 调优博客并积累 GC 调优经验。目前我们已经基本具备了对 GC 深度调优的能力,在 GC 严重场景通过调优甚至能带来 60%+ 吞吐的提升,今年我们也会不断细化沉淀这里的方法论并择机分享。在向量化 API 方面,今年旭鑫实测了 JDK21 的 Vector API,在部分场景下能够带来最大 13.5 倍的性能提升,这也是 IoTDB 未来进行性能演进的技术储备之一。
  • IoTDB 磁盘文件地图:今年我们参照 Oracle/IBM 等数据库绘制了 IoTDB 的磁盘文件地图。通过该地图,我们不仅发现了一些可以潜在优化的点,还理顺了不同模块落盘文件的逻辑关系。
  • 压缩算法性能测试:今年我们针对若干用户场景的真实数据进行了压缩算法的对比测试,发现大多数场景下 LZ4 相比 Snappy 有更好的压缩效果,这也促使了 IoTDB 默认压缩算法的更改。
  • 难点预研:今年我们组还针对多个复杂问题,如共识组数与集群性能、线程模型、集群滚动升级方案和大 Text 值类型访存瓶颈优化方案等进行了深入的调研和测试,虽然部分工作尚未得出最终结论,但已经为未来的深入研究奠定了基础。

今年我在 Apache IoTDB 社区提交并被合并了 119 个 PR, Review 了 387 个 PR。从 PR 数量上来说相比去年和前年有了显著提升,可能是由于更加专注于工作,并且 scope 也在不断扩大吧。此外我也于今年 9 月受邀成为了 Apache IoTDB 社区的 PMC 成员,感谢社区对我的认可。

因时间所限,我今年在知乎等社交平台的活跃度有所下降。但回顾这一年,我觉得我们团队完成了许多既有趣又深入的工作,并且几乎都有相应的文档沉淀下来。这些宝贵的积累完全可以与业界分享以交流学习。我期待在 2024 年,我们团队能够更频繁地分享我们的技术沉淀,并吸引更多对技术有兴趣的同学加入 IoTDB 社区或我们的实验室进行交流!

一些感悟

性能优化:体系结构和操作系统是基本功

在深入研究和优化数据库系统在各种硬件环境及业务负载下的性能过程中,我越发认识到掌握体系结构和操作系统知识是进行性能优化的基础。今年,我在这两方面补充了许多知识,并阅读了《性能之巅》的部分章节。然而,令人感到有些沮丧的是,随着知识的增加,我反而越来越感觉到自己的无知。但我仍然希望,在 2024 年能够跨越这段充满挑战的绝望之谷,登上开悟之坡。

对于有意向学习 CMU 15-418 课程的朋友,我非常期待能够一同学习和进步!如果有经验丰富的大佬愿意指导,我将不胜感激!

GC 算法:追求吞吐还是延迟?

今年,我们组深入研究了 JDK 的垃圾回收(GC)算法,包括但不限于 Parallel Scavenge(PS)、Concurrent Mark Sweep(CMS)、Garbage-First(G1)和 Z Garbage Collector(ZGC)。我们还对 IoTDB 在相同业务负载下采用不同 GC 算法的吞吐量和延迟性能进行了比较测试,结果表明在不同的负载条件下,各 GC 算法的性能表现排序也有所不同。

在 GC 算法的选择上,我们面临着内存占用(footprint)、吞吐量(throughput)和延迟(latency)三者之间的取舍,类似于 CAP 定理,这三者不可能同时被完全满足,最多只能满足其中的两项。通常情况下,高吞吐量的 GC 算法会伴随较长的单次 STW 时间;而 STW 时间较短的 GC 算法往往会频繁触发 GC,占用更多的线程资源,导致吞吐量下降。例如,PS GC 虽然只有一次 STW,但可能耗时较长;G1 的 Mixed GC 在三次 STW 中的 Copying 阶段可能造成几百毫秒的延迟;而 ZGC 的三次 STW 时间都与 GC Roots 数量有关,因此 STW 延迟可以控制在毫秒级别。

JDK GC 算法的发展趋势似乎是在尽量减少 GC 对业务延迟的影响,但这种优化的代价是消耗更多的 CPU 资源(JDK 21 引入的分代 ZGC 有望大幅降低 ZGC 的 CPU 开销)。在 CPU 资源本身成为瓶颈的场景下,使用 ZGC 和 G1 等 GC 算法的吞吐量可能会低于 PS。GC 算法目前的演进具有两面性,例如 Go 语言就由于其默认 GC 与 Java 相比 STW 时间较短而被赞扬,但其 CPU 资源消耗大也会被批评,我们需要根据不同的目标选择合适的 GC 算法。

然而,GC 算法朝低延迟方向的不断演进仍具有重要意义,因为吞吐问题可以通过增加机器进行横向扩展来解决,而延迟问题则只能依赖于 GC 算法的改进。因此,在调优时应该有针对性,分别针对吞吐和延迟进行优化,而不是同时追求两者。如果追求吞吐量,可以优先考虑使用 PS;如果追求低延迟,可以考虑使用 G1/ZGC,并为之准备额外的机器资源以支付低延迟的代价。

全局成本:C/C++ 相比 Java 性能更好?

今年,我参与了许多问题修复和优先级排序的工作,同时深入思考了编程语言对软件开发总成本的影响。

在 PingCAP 实习期间的一次闲聊中,有些同事提出 TiDB 应该用 Rust 或 C++重写,理由是用 Go 语言编写的性能较差。然而,我的 mentor 徐总认为,采用 Go 语言后显著减少了大家的 OnCall 次数,从而节约了大量研发成本。

从纯技术的角度看,C/C++ 在极限优化下确实能比 Java 更好地发挥硬件特性。但工程开发,尤其是内核开发,不仅仅是技术问题,它更多涉及到软件工程的广泛议题。现实中,我们经常面临着无休止的问题修复和需求实现,性能优化往往未能充分利用硬件能力。我认为,尽管开发团队采用的编程语言可能影响理论上的性能上限,但在大多数工程实践中,项目成功的关键并不仅仅在于将性能优化到极致。更重要的是,在有限资源下如何优先追求满足用户需求的产品特性、如何持续保证产品的稳定性和可维护性、如何提升系统的横向扩展能力、以及如何在现有代码基础上持续进行性能优化。我相信,这些因素比起编程语言的选择所带来的潜在收益要重要得多。

因此,除了少数极特别的场景(例如追求超低延迟 or 边缘端等),选择一个团队熟悉且学习成本较低的编程语言就足够了。

工程难题:不是所有技术问题都能够立即找到解决方案

今年,我们面对并快速解决了许多棘手的问题,但同时也遇到了一些难以快速找到原因的疑难杂症。这些问题涵盖了多个方面,例如 DataNode 进程在 OOM 后仍能响应心跳但无法处理新的读写请求(这是因为 JVM 在 OOM 后随机终止了一些线程,导致监听线程被终止无法响应新连接而心跳服务线程仍在运行),以及 Ratis consensusGroupID 编码错误导致的 GroupNotFound 错误(使用 Arthas 监控后问题消失,我们怀疑这是 JVM JIT 的 bug)等。

解决这些问题的过程加深了我们对于设计新功能时对各种异常场景的考虑,有效避免了许多未来可能发生的 Oncall 问题。

在面对问题和解决问题的过程中,我深刻体会到人的认知可以分为四个象限:已知的已知、已知的未知、未知的已知以及未知的未知。其中,最难以应对的是“未知的未知”。我一直在思考工程经验这四个字究竟意味着什么?现在我认为,工程经验的积累不仅意味着将更多的“已知的未知”转化为“已知的已知”,还需要将更多的“未知的未知”变成“已知的未知”,这样才能具有可持续性。

流程体系:软件开发团队的重中之重

今年,我深刻体会到了流程体系在构建一个可持续发展的软件开发团队中的重要性。我认识到只有拥有一流的团队,才能够开发出一流的软件。

在王老师软件工程理念的统筹指导和 Apache 基金会的支持下,我认为我们的产品流程体系已经相对健全,包括但不限于以下几个方面:

  • CI/CD:对不稳定的 UT 和 IT 进行持续的修复,确保代码质量和功能稳定性。
  • 代码质量静态检测:利用 Sonar 等工具持续提升代码质量,确保软件的健壮性。
  • Commit 级别的监控:针对不同的用户和测试场景,实现性能和资源使用量的监测,防止出现非预期的产品回退。
  • 定期封版和发版:对每一项 Release Note 进行逐项测试验证,通过多轮的 RC 版本,不断收敛测试范围,确保成功发布。
  • 定期的功能和技术评审会议:各模块的核心开发者共同参与,评估产品的功能和技术实现。
  • 发版问题同步会:确保团队成员对 RC 验证中发现的问题能够快速响应。
  • P0 项目支持任务同步会:对重要项目的支持任务进行同步和讨论。
  • 多层级技术支持团队(L0/L1/L2):根据问题的复杂度,提供分层次的技术支持。
  • 敏捷开发的支持工具:使用多维表格等工具,支持敏捷开发流程。
  • 论文讨论班:持续学习和探索行业内的最新研究成果。
  • 竞品功能和技术分析:分析竞争对手的产品,从而不断优化自身产品。
  • 安全漏洞感知和修复:及时发现和修补安全漏洞,保证产品的安全性。

通过在这样的团队中工作,我对如何打造一个可持续的软件工程体系有了更深地理解。

工作管理:一键生成总结是好是坏?

随着我们组负责的模块和同学数量的增加,我逐渐发现,仅仅通过飞书文档记录工作内容的做法,虽然实现了工作的“记录”,却缺乏了有效的“管理”。例如,我们组面临的任务琐碎而多样,大家都经常会忘记一些计划中的任务;同时,我们的业务需求变化迅速,虽然大家都在同时推进多项任务,但仍然跟不上需求的变化速度。这就要求我们能够及时调整任务的优先级,以便灵活应对并优先完成 ROI 最高的任务。此外,我们以前的月度总结并没有持续进行,我分析的原因是任务汇总本身就是一种成本,导致月度总结难以持续,从而失去了很多总结沟通的机会。

为了解决这些问题,我开始学习并使用飞书的多维表格来管理团队的任务。通过多维表格,我们不仅可以清晰地看到每位成员当前的工作任务,还可以在团队会议上根据业务需要灵活调整任务优先级,甚至能够一键生成甘特图来明确不同优先级任务的时间线。在进行每周和每月总结时,我们也能够通过筛选日期快速生成任务汇总。

一开始,多维表格似乎完美地解决了我们之前的问题。然而,随着时间的推移,我发现这种方式也存在缺陷。由于总结能够一键生成,我不再每周花费一小时来统计和规划我们的周报和下周计划,甚至我们的月度总结也鲜少举办。这反而导致我们的日常开发缺乏规划,显得有些随波逐流。在东哥的点拨下,我重新开始在飞书文档中记录周报,并且连续三个月组织了月度总结会。通过定时的每周和每月汇总与沟通,团队的工作变得更加有序和明确。现在如果让我去说上半年做了什么主要工作,我可能还需要看多维表格筛选半天,但如果问我后 3 个月做了什么,我只需要看每月的月度总结就可以了。

现在,我们通过多维表格来管理任务的优先级,同时利用飞书文档来汇总周报和月报。通过对我们组流程管理的持续迭代和优化,我意识到有时候追求速度反而会拖慢进度,而适当地放慢脚步思考反而能够使我们更加高效。

团队协作:分布式系统的高扩展性和高可用性

在技术方面,我最开始深入了解的就是分布式系统,我一直在学习如何实现系统的高扩展性和高可用性。随着时间的推移,我发现这些分布式系统的理念同样适用于团队协作中。

为了实现高扩展性,关键在于让所有团队成员并行工作,而不是仅依赖于“主节点”或关键个体,这要求每个成员都能独自完成任务并持续提高自己的工作效率,这样才能提升整个团队的整体性能。同时,团队还需要能够支持成员的动态调整,如新成员的加入和旧成员的离开,确保团队结构的灵活性和适应性。

为了满足高可用性,就需要在关键任务或数据上实施冗余策略,以防止暂时的不可用状态对团队工作造成影响。这可能意味着需要在某些区域投入额外的资源,确保信息、知识或工作负载能够在多个成员之间共享,保持一致性。

这一年来,我们团队负责的模块不断增加,但每个模块都至少有 3 位以上的成员熟悉,上半年我的感受是每天从早忙到晚,连半天假都请不了。但到后半年我感觉偶尔请一两天假也不会对外产生可感知的影响了,这代表了我们组的高可用性出现了显著提升。针对我们组负责的模块,我们维护了详尽的功能和技术设计文档,以及改进措施的追踪记录,这不仅加速了新成员的融入,也保持了团队知识的一致性。此外,我们通过引入自动化工具,如飞书激活解密机器人、各类测试脚本、木马清理脚本等,有效提升了团队的工作效率,体现了我们组在高扩展性方面的进步。

希望 24 年我们组能在高扩展性和高可用性方面继续取得显著进步,为实现更加高效和稳定的团队协作模式而不断努力。

时间管理:可观测性

今年我们组的主要工作之一便是打造 IoTDB 的可观测性,目前已经显著提升了问题排查和性能调优的效率,成为线上运维 IoTDB 的必备工具。回到时间管理上,我发现可观测性的很多理念也同样适用。

随着组内同学越来越多,scope 越来越大,沟通协调的成本已经不容忽视,我自己的时间越来越不够用,逐渐成为了单点瓶颈。在向东哥请教后,我开始按照半小时为单位记录自己每天的工作内容,并定期反思每半小时的工作是否满足了高效率。

通过整理自己工作日一天 24 小时的时间分配,我发现自己实际可用于工作的时间并不超过 11 小时,因为每天基本要包括睡眠 8 小时、起床和就寝的准备及洗漱时间 1 小时、通勤 1 小时、餐饮和午休 2 小时以及运动 1 小时(有时会被娱乐消遣取代)。11 月份的数据显示,我的平均工作时间约为 10 小时(没有摸鱼时间),已经接近饱和每天都十分充实。这促使我思考如何提升自己和团队单位时间的工作效率,比如在协调任务时明确目标和截止日期,实行更细致的分工以解决我作为单点瓶颈的问题等。通过这些措施,到了 12 月份,我的平均每日工作时间减少到了 9.5 小时,而感觉团队的整体产出反而有所提升。不过,到了 1 月份,由于一些新的工作安排尚未完全理顺,我的平均工作时间又回升到了 10 小时,这需要我持续进行优化。

总的来说,定期统计和评估自己的时间分配及其 ROI,我觉得对于提高工作效率具有重大意义。

心态变化:职业发展和生活的关系

在经历了半年学生生活和半年职场生活后,我对职业发展与生活的关系有了新的认识和感悟。之前我是那种职业动机极强以至于生活显得相对单调的人。对我来说,除了那些能带给我快乐的少数娱乐活动外,生活中的许多琐碎事务如做饭洗碗,都被视为时间的浪费,不如将这些时间用于创造更多的价值。在地铁和高铁上不学习,我也会感到是对时间的浪费。我认为既然职业发展对我而言十分重要且能从中获得快乐,那么我应该将所有可用的时间都投入其中。

然而今年我的心态发生了显著的变化。我逐渐意识到,即使职业发展很重要,即使我能从中获得快乐,它也只是生活的一部分。我开始挤出更多的时间来陪伴家人,也开始与各行各业的老朋友新朋友进行交流。我不再认为生活中的全部琐事是对时间的浪费。我更加注重如何在有限的工作时间内提升效率完成超出预期的工作,而不是简单地用更多的时间去完成这些工作。

这种心态的转变对个人来说不一定是坏事。如果我的心态没有这些变化,可能会投入全部可用的时间于职业发展中,但这样的状态不确定能够持续多久。如果我的心态发生了变化,那我可能会更加注重工作效率和生活体验感,也许能达到职业发展和生活的双赢。

总的来说,每个人在不同的年龄阶段对这种平衡的感悟都会有所不同。我目前的想法是,顺应我们不断成熟的心态,选择让我们感到最舒适的状态,这不仅能让我们的心理状态更加健康,也能更好地平衡职业发展和生活的关系。

任务分配:兴趣驱动,效率优先

马克思指出社会分工是生产力发展的结果和需要,这种分工具有历史的必然性。对于创业公司而言,追求指数型增长是生存和发展的关键,因为即使是线性增长,在激烈的市场竞争中也可能面临被淘汰的风险。如何实现这种增长,是一个复杂且多维的问题,我在这里只从任务分配的角度分享一些个人理解。

在创业团队中,自上而下的任务繁多,而自下而上每个成员的兴趣和专长也各不相同。如何最大化团队的价值?关键在于沟通和了解每个成员的兴趣点和擅长点,尽可能让他们大部分时间都在做自己感兴趣和擅长的工作。虽然总有一些额外的任务需要团队共同承担,但是优先保证成员大部分时间能够从事自己感兴趣且擅长的工作是非常重要的。只有这样,每个人才会带着兴趣和专长去挖掘提升效率的可能,从而可能产生指数级的复利效应,并最终影响整个团队的产出。在现有的权力结构体系下,无论是企业还是更广泛的社会,我觉得自上而下的人员任命也基本遵循这一原则。

基于这样的理解,我在分配我们组的任务时,尽可能根据我对团队成员的了解,分配给每个人感兴趣和擅长的任务,并与大家一起探索提升效率和价值的途径。这一年里,我一直在寻求任务分配的全局最优解,并坚信找到合适的人做他们感兴趣的工作,能够产生的复利远远超过随机或平均分配工作所能带来的效益。

个人发展:更广还是更深?

在创业团队的初期阶段,各方面的需求和缺口(技术,市场,运行,销售等等)很多。从公司的角度看,这就非常需要大家能够主动承担额外的职责。从个人的角度看,我们不论是承担更多的职责还是在自己所做的工作上做得更突出,都是对公司的贡献,也都能收获成长。然而人的精力总是有限的,一个人不可能完美地做完所有事情,总是要把有限的精力投入到有限的事情上。面对这样的环境,每个人都面临着如何在工作的广度和深度之间做出选择的问题。

对于这个问题,我今年有了一番思考和探索。个人觉得对于职场新人来说,寻找一个自己擅长且能从中获得乐趣和成就感的领域至关重要,并且需要与领导进行积极的沟通,以获得相应的支持和资源。每个人的选择可能不同,领导的任务就是在团队成员之间找到一个平衡点,不仅能够完成所有任务,还要尽量让每个人能在其擅长的领域内发挥最大的复利效应。

就我个人而言,我目前更倾向于追求工作的深度,希望能够深入学习并掌握我目前尚不擅长但团队需要的技术知识。通过专注于深度,我希望能够在专业领域内取得更大的进步,并为团队带来更具影响力的贡献。当然,这也并不意味着就完全抛弃广度,随着时间不断推移,我在广度上投入的精力也会越来越多。

协作理念:以人为本,真诚坦率

今年,通过阅读《跳出盒子——领导与自欺的管理寓言》和李玉琢老师的《办中国最出色企业:我的职业经理人生涯》,我对管理有了初步的理解和感悟。这两本书分别代表了不同的管理理念,一种强调以人为本,另一种则是以结果为导向的雷厉风行。对于我目前的心态而言,我认同后者的评价体系,但从个人性格上我自己的风格更像前者。

在日常的产品迭代和团队管理中,我始终认为把人放在第一位是非常重要的。通过团结所有可以团结的力量,关注每个成员的工作态度、能力、心理状态以及需求和期望,找到大家适合的方向,往往能比反复推动大家完成不情愿的工作更加高效。

当然,在工作过程中难免会遇到与某些人的争执和冲突。面对这些情况,我常采取的做法是换位思考。我会设身处地地想,如果我是对方,我是否也会做出同样的选择?如果答案是肯定的,那么这往往是角色之间的冲突,而非个人情感的问题,我就不会在情感层面上过多消耗精力。如果答案是否定的,我则会进一步探索解决分歧的方法。我是一个性格相对温和的人,我通常不倾向于与人争执,而是尽可能地通过和平的方式解决问题。今年,我几乎都是这样处理冲突的。

然而,我也逐渐意识到,过分的忍让并不会赢得他人的尊重和理解,反而会被得过且过。有些原则和理念是需要坚持的底线,绝不能妥协。希望在未来的一年里,我能够在保持真诚坦率的同时,也能够坚持自己的原则和底线。

人生成就:小赢靠智,大赢靠势

今年在工作之余也读了《新程序员》杂志,深入了解了很多大佬的成长经历,也获得了不少启发。一个很深刻的感悟还是江同志的一句话:一个人的命运啊,当然要靠自我奋斗,但是也要考虑到历史的进程。

自从 ChatGPT 爆火以来,周围已经涌现出许多彻底成功的案例,这些故事不仅激励着我,也让我对未来充满了好奇和期待。尽管对于自己未来的方向,我目前还没有一个清晰的规划,甚至只能对未来 1 到 2 年内的工作做出一些预测,3 年后会做什么我还没有确切的答案。

但在这样的不确定性中,我坚信的一点是,只要相信自己当前的工作富有意义和前景,并且能够在其中找到快乐,那么就值得坚持下去,全力以赴。关于未来命运将我们带往何方,或许可以交给时间和命运去安排。在这个快速变化的时代,保持学习和成长的心态,积极面对每一次机遇和挑战,可能就是我们能做的最好的准备了。

来年展望

经过一天多的思考,我终于完成了今年的年终总结。回顾这一年,我在技术和管理方面取得了一些进步,但同时也深刻意识到,在让企业成功的方方面面,我还有太多不了解不擅长需要学习的地方。

展望新的一年,我为自己和我们组设定了以下几点期望:

  • 做深:希望能够系统地学习体系结构、操作系统以及《性能之巅》中的相关知识,并将这些知识应用到实践中,不断提升 IoTDB 的技术水平和性能表现。
  • 做广:除了在分布式和可观测性方面的投入之外,希望能深入学习时序数据存储引擎和流处理引擎的知识,向优秀的同事和业界前辈学习。
  • 做好:持续努力提高 IoTDB 的稳定性、鲁棒性和易用性,确保它成为用户信赖的时序数据库。
  • 做响:寻找机会将我们团队的工作成果和经验分享给外部,与更多的同行进行技术交流,不断增强 IoTDB 的知名度和技术影响力。

最后,感谢您的阅读。欢迎各位读者批评指正。

在新的一年里,祝愿大家身体健康、家庭幸福、梦想成真。希望我们都能在新的一年中取得更大的进步!

2023 IoTDB 用户大会分享:如何用 IoTDB 监控工具进行深度系统调优

2023-12-07 18:03:00

背景

2023 年 12 月 3 日,IoTDB 一年一度的 用户大会 成功举办。

在本次大会中,我有幸作为讲师之一做了《优其效:如何用 IoTDB 监控工具进行深度系统调优》的分享,系统介绍了 IoTDB 这一年来在可观测性方面的进展,并展示了它如何显著提升我们的性能调优和问题排查效率。

本博客将通过文字和图片的方式展示我的分享内容。

我们在可观测性方面做的工作后续也会有更多的博客输出出来,敬请期待!

内容

大家好,我是来自天谋科技的谭新宇,接下来我为大家分享的主题是”如何用 IoTDB 监控工具进行深度系统调优”。

本次分享分为 5 个方面,首先我们将介绍数据库系统的用户服务和架构演进挑战,这些挑战的本质都是如何去提升效率;接着我们会对 IoTDB 可观测性的发展进行概览,主要包括 Logging,Metrics 和 Tracing 三个方面;然后我们会介绍一下 IoTDB 的监控模块,其构建主要参考了火焰图作者著作《性能之巅》的思路,即从负载视角和资源视角两个维度对系统进行观测;最后我们会概述一下 IoTDB 的 4 个监控面板并着重做一些性能调优和问题排查的典型案例分享。

首先先来分析一下数据库系统的用户服务和架构演进有着怎样的挑战。

对于用户服务,主要存在以下三个挑战:

第一是如何快速找到业务场景的瓶颈点?系统的性能存在木板效应,会受限于系统最慢的模块,比如某节点的 CPU 和磁盘还没有打满,但网卡已经被打满了,此时增加写入负载也不会获得任何性能上的提升。

第二是如何对业务场景进行针对性调优?不同硬件环境和业务负载的排列组合会使得很多默认参数并不是当下最优的值,针对这个问题,一种理想流派是像 ottertune 一样使用机器学习的方式去找到最优的参数组合,另一种更为实际的流派则是能够对系统进行白盒调优。

第三是如何形成可扩展的调优体系?对于性能调优这个工作,其实非常容易形成马太效应,即越会调优的人越容易被分配更多性能调优的工作,虽然他会越来越能调优,但这也容易形成单点瓶颈,导致调优工作横向扩展不起来,这样其实是不利于整个产研团队和实施团队的共同成长的。因此需要针对调优这项工作形成可复制的调优方法论,大家共享互补调优的知识,一起成长。

对于架构演进,也主要存在以下三个挑战:

第一是如何确定典型业务场景?性能优化需要结合场景谈论才有意义,而一个系统往往也会有很多用户场景,这就需要从中抽象出来通用普适的典型场景并总结他们的典型特征。比如硬件环境到底是 4 核 8G 还是 64 核 128G ,业务需求到底是追求低延迟还是高吞吐等等。

第二是如何进一步演进典型业务场景下的性能?任何系统在特定业务场景下都存在进一步性能演进的可能,我们需要在寻找瓶颈的过程中区分出来哪些是工程问题(比如 GC 参数调优,代码写的冗余),哪些是学术问题(比如针对 IoTDB 特定的时序场景,有些数据库原理的 trade-off 发生了变化,这个时候就可以结合场景做一些更针对的设计,IoTDB 近几年在 Fast 和 ICDE 等顶会上发表的论文都是沿着这个思路去设计的),区分出这两个问题之后就可以利用不同的思路去并行协作优化性能了。

第三是如何确保性能优化的 ROI 最大?对于一个系统怎么优化,收集一圈能够得到一大堆思路,到底哪些效果会好,哪些效果会差?我们需要能够精确评估一个优化的正向作用和负面影响,并能够量化排列优先级,这样才可能将有限的资源持续投入到 ROI 最大的性能优化上,坚持做最正确的选择。

分析完了挑战,其实我们也都清楚了可观测性对于解决这些挑战的重要性。那么接下来我们介绍一下 IoTDB 的可观测性发展概览。

随着分布式架构成为主流,可观测性这一名词逐渐被大家频繁提及。学术界一般会将可观测性分为三个更具体的方向进行研究,分别是 Logging,Metrics 和 Tracing。

Logging 的职责是记录离散事件,从而使得事后可以通过这些记录来分析程序的行为。

Metrics 的职责是将不同类型的消息分别进行统计聚合处理,从而能够对系统进行持续的监控和预警。

Tracing 的职责是记录完整的调用轨迹,这就包含了服务间的网络传输信息与各个服务内部的调用堆栈信息。

IoTDB 自诞生时就使用了 Logback 框架来管理日志,随着版本的不断迭代,目前已经将不同级别和模块的日志拆分成了不同的文件便于检索。

虽然这些日志很重要,但它所有的信息都是离散的。如果要对某一类的信息进行一些汇总聚合统计,比如统计一段时间的平均刷盘点数,就需要首先 cat 文件,接着再 grep 过滤出同一类型的日志,然后还要写脚本来计算次数,平均值之类的,这就非常繁琐。

很自然的这就需要引入 Metrics 了。

IoTDB 在 0.12 版本就开始设计开发 Metrics,但从 1.0 版本之后才开始投入大量精力打磨 Metrics,到了 1.3 版本 Metrics 已经基本打磨的差不多了。

我们用了 Micrometer 和 DropWizard 的算法库来作为监控指标的类型支撑,具体的存储可以导出到 Prometheus 或者 IoTDB 中,可视化目前主要是在用 Grafana 工具。

右边贴了一张我们监控面板的图,还是非常漂亮的,后面会进一步介绍。

有了 Metric 之后,我们可以统计同一类请求的聚合信息,例如平均值,P99 值等等。这其实已经能够解决 90% 以上的问题,但对于剩下 10% 的问题,比如海量小查询和一个大查询并发执行时,大查询的执行耗时会被吞并,从而无法体现在 Metrics 中。此时我们就需要具备单独观测一条请求完整调用链路耗时的能力。

为了满足这种需求,今年我们也启动了对 Tracing 工作的研究,我们用 OpenTelemetry,ElasticSearch 和 Grafana 搭建了 Tracing 系统。

比如右图对于 show region 请求的调用链路,我们可以在 Grafana 中展示这个请求跨进程通信时不同进程内部调用栈的详细耗时信息,这对于慢查询等场景的性能排查效率会有显著提升。

总体而言,IoTDB 的可观测性能力在今年发生了质变。我们有信心也非常欢迎我们的用户朋友前来体验。

接下来我会着重介绍一下 IoTDB 的监控模块:

对于监控模块而言,它的灵魂就是他拥有哪些监控指标。

这里我们参照火焰图作者著作《性能之巅》的思路。从负载分析和资源分析这两个相反的角度去互补推进监控指标体系的建设。

对于自顶向下的负载视角:

我们对客户端写入 IoTDB 的流程进行了拆解。对于每个 IoTDB 的连接,当它将请求交给到 IoTDB 执行时,该连接被视为忙碌状态;当它在客户端攒批等待或者向服务端传输时,该连接被视为闲置状态。通过这种区分,我们能够对瓶颈是否在 IoTDB 内部有一个评估。比如每次连接繁忙都是 10ms,之后却要闲置 5 分钟,那基本瓶颈就不在 IoTDB 端了。

如果发现连接繁忙的时间要更大,要如何进一步去寻找瓶颈呢。我们将 IoTDB 的写入请求延迟进行了拆解,将写入流程分成了若干阶段,并对一般情况下更为耗时的阶段进行了更细粒度的拆分,从而能够确保发现瓶颈出现在哪个模块。比如调度执行阶段一直存在远程转发,那就需要去排查客户端的分区缓存是否失效。

总之,通过这种自顶向下的分析,我们能够找到系统当前的瓶颈是在哪些模块。

对于自底向上的资源视角:

我们主要从 4 个维度进行了考虑:

在磁盘方面,我们希望我们要比 Linux 的常用磁盘监控命令 iostat 更为丰富,比如除了磁盘利用率,吞吐 iops 之外,我们还想统计进程级别的磁盘读写情况和 page cache 的使用情况。

在网络方面,我们希望我们要比 Linux 的常用网络监控命令 sar 更为丰富,比如除了网络吞吐和收发包的速度之外,我们还想统计进程级别的连接个数等等。

在 CPU 方面,我们不仅要统计操作系统和进程的 CPU 利用率,还想统计 IoTDB 进程内部不同模块不同线程池的 CPU 利用率,也还想统计进程内部线程池的关键参数。

在 JVM 方面,我们不仅要对堆内堆外的内存大小做观测,对不同状态的线程个数做观测,还想对 GC 做更细致的观测。

总之,通过这种自底向上的分析,我们能为很多模块的瓶颈原因提供思路

那到了 1.3.0 版本,我们前面提到的监控指标都已经实现了,那么监控模块对于性能的影响到底大不大呢,线上敢不敢打开呢?

其实这块我们也在持续的做性能优化,尽管 IoTDB 监控指标的个数已经从 1.0.0 版本的 134 涨到了 1.3.0 版本的 905,增加了接近 8 倍,但监控模块 CPU 的开销反而从 11.34% 降低到了 5.81%,减少了近 50% 。其对于读写性能的影响也从 7% 以内降低到了 3% 以内。

因此,大家可以放心的开启监控模块,它对于系统运维的收益绝对远远大于这一点点性能损耗。

基于这些监控指标,接下来我们简单介绍一下 IoTDB 的监控面板:

主要分为四个监控面板:

分别 Performance Overview,System,ConfigNode 和 DataNode 面板。

下面将给出这些面板的示例:

对于 Performance Overview 面板:

它汇总统计了集群的基本信息,例如集群大小,总时间序列个数,总写入吞吐等等。

它还以延迟拆解的方式展示了客户端写入不同阶段的耗时统计,辅助定位瓶颈存在于哪个模块。

任何一个子面板我们都写了详细的注释,比如左图这个面板就展示了不同接口的耗时统计。

同时我们也可以在一个面板中同时查看多个节点的监控数据,便于定位相同时间不同节点的状态。

对于 System 面板,它提供了 CPU, JVM, 磁盘和网络维度的监控数据,在用于定位系统资源是否为瓶颈时非常管用。

对于 ConfigNode 面板,它也汇总统计了集群的基本信息,还提供了元数据及数据分区 Leader 分布等维度的监控。在用户定位集群扩展性能力时非常有用,比如是否所有的节点上都分配了读写流量,是否有节点宕机等等。

对于 DataNode 面板,它汇总了单节点引擎内部的细致监控,如存储,查询,元数据,共识和流处理引擎等等。在判断模块内部瓶颈原因时非常有用。

现在 IoTDB 有接近上千的监控指标,这些指标很难在今天短短的分享中介绍完,那接下来我就分享 5 个典型案例来展示一下 IoTDB 监控模块的能力:

第一个案例是在某高吞吐量场景下如何去确认瓶颈所在。

当业务链路较为复杂时,如果整体的性能不达标,用户其实是不太好去确认到底瓶颈是在业务上还是在 IoTDB 中。

那现在 IoTDB 的监控模块则是可以帮忙定位瓶颈是否在数据库中。

比如对于一个 Flink 实时消费 Kafka 数据来写入 IoTDB 的用户场景,业务链路上有 128 节点的 Kafka 集群,96 节点的 Flink 和 IoTDB 集群。

由于集群规模较大,部署测试调优运维的成本都较高。当时跑通整个链路后业务给我们的反馈就是 IoTDB 写入性能不够,IoTDB 集群总写入吞吐仅为 15GB/s,扩展性很差等等。

那当我们进行瓶颈排查之后发现锅并不在 IoTDB 而是在业务上层。

比如我们发现每个连接平均控制 4s 才会繁忙执行请求 20ms,每个节点平均每秒才接受 3 个请求且系统资源利用率都非常低。

因此我们推动了业务侧进行排查,他们发现即使把 Flink 的 Sink 侧置为空写整体吞吐也才 20GB/s,最终他们找到了问题所在并对 Flink 侧进行了优化。

在业务调整进行复测,我们发现 IoTDB 集群的整体吞吐可以达到 62.6GB/s,相比之前提升了 4 倍以上的性能,集群的线性比最高也达到了 0.89。

如果没有监控模块指导我们去推动业务侧改造,我们还一门心思的在数据库内部找瓶颈,那最终的结果一定是事倍功半。

第二个案例是某车联网场景的写入性能尖刺调优。

由于 IoTDB 是 Java 写的,很多用户也会询问我们 GC 对 IoTDB 性能的影响。由于我们在内存中也做了不少的池化来自己管理内存,所以大部分场景下用户其实感知不到 GC 对性能的影响。只有极少数个别场景才会观测到,比如这个案例就是 GC 导致了写入性能尖刺。

那现在 IoTDB 的监控模块内嵌了 GC 调优分析器,其实是具备对 GC 深度观测和调优的能力的,接下来让我们一探究竟。

该场景的整体架构是一个 3c 12D 的 IoTDB 集群,也是 Flink 实时消费 Kafka 的数据写入 IoTDB 集群。

在写入压测过程中,我们发现 IoTDB 的写入吞吐能力基本符合预期,但是存在尖刺,有时吞吐会接近 0。进一步排查原因我们发现这是由于 JVM 每 20 分钟会触发一次 Full GC,每次 Full GC 都耗时 1 min 以上,那这样的 GC 其实是非常不健康的。

那对于 GC 应该如何调优呢?常见的流程是启动 JVM 时打开 GC 日志,测试一段时间后上传 GC 日志到特定的网站进行分析,其会将不同原因导致的 GC 进行耗时和次数的汇总,然后我们可以基于这些聚合后的高密度 GC 信息再分析应该如何调整 GC 算法参数。

在建设好可观测性之后,现在的 IoTDB 如何去做 GC 调优分析呢?

我们首先提供了 GC 耗时比例的新手指标,它表示了 GC STW 耗时占整个 JVM RunTime 耗时的比例,如果这个比例小于 5-10%,则说明 GC 对系统整体的吞吐影响不大,如果在延迟上没有额外要求,那一般就不需要再调优了。

如果这个比例大于 10-15%,则一般说明可以对 GC 进行进一步调优。我们这时提供了若干专家指标,比如我们对不同 GC 原因导致的耗时和次数进行了统计,还对于种种 GC 前后的内存申请,内存大小都做了统计,这都能作为我们进行调优 GC 的数据支撑。

在该用户环境下,我们的调优思路首先是将 GC 算法从 PS 换成了对大内存更为友好的 G1,接着又结合负载和 IoTDB 的特点进行了 GC 参数的调优,其核心思路也是延缓并发标记阈值,提升 MixedGC 吞吐,控制单次 GC 耗时软上限等。

那最终调优的结果呢是写入吞吐稳定,不再又 Full GC,同时写入性能也提升了接近 50%,还是比较可观的。

这个案例主要是说明一下 IoTDB 对 GC 的观测能力和调优能力。

第三个案例是某测试场景下的硬件瓶颈原因探究。

在一些 POC 阶段,当系统出现瓶颈时,如果将系统视为黑盒,其实是不知道如何升级硬件的收益最高的。

结合 IoTDB 的监控模块,我们可以量化算出升级硬件带来的潜在性能收益,用以选择收益最高的硬件升级方案。

比如在该测试场景下,我们用 2 个客户端机器去压测高配机器的单节点集群,发现系统地性能仅为 1.2GB/s,不符合我们对如此高配机器的想象。

那接着我们对系统资源进行了分析,发现 CPU 和网络都没有达到瓶颈,但磁盘的繁忙程度达到了 100% 成为了瓶颈,从而限制了整体吞吐,此时就需要升级磁盘才能进一步提升性能了。

这就是一个典型的木桶效应,在理想状况下,所有资源应该同时达到瓶颈,这样硬件资源才没有浪费;然而在实际情况中,往往会有个别资源先到瓶颈,从而限制整体性能。

在升级磁盘之后,我们发现磁盘和网络不再是瓶颈,写入吞吐也提升到了原来的 2.5 倍,此时 CPU 又成为了新的瓶颈。

通过该案例,可以说明 IoTDB 的系统资源监控可以帮助我们快速找到硬件瓶颈,从而用最小的成本达到最大的收益。

案例 4 是某周测场景的写入性能波动变大问题排查。

对于服务器的 CPU 利用率出现波动这类问题,其实是比较难排查的,因为他不一定持续,等到我们去排查的时候可能已经不波动了。

对于这类问题,IoTDB 监控模块内置了操作系统,进程,线程池和模块 CPU 利用率监控,我们可以首先确认该波动是不是 IoTDB 引起的,如果是则可以一更进一步给出调优建议。

比如该问题就是我们在日常的周测场景中发现 1.2.0 rc5 版本的写入吞吐相比之前的一个版本波动更大,这其实属于很细致的观察了,不一定对业务有什么影响。

但我们没有放弃这一次机会进行了原因探究。

首先我们排查了操作系统及进程的 CPU 监控:发现新版本的 CPU 利用率波动更大,它应该是造成写入性能波动更大的原因。

那更进一步我们直接用了我们的大杀器-线程池 CPU 利用率监控,发现新版本中后台执行的合并线程池利用率在 0-18% 进行大幅波动,而老版本则稳定在 8% 左右。

更进一步我们去排查了存储引擎 TsFile 层级监控:发现新版本的存储引擎合并速度更快,文件状态也更健康。

因此我们就检查那段时间合入的代码,发现新版本修复了之前合并模块 IO 大小预估偏大的问题,可能导致之前受 IO 限速不能执行的合并任务受现在能够被执行。

至此我们已经明确了该现象的根因,考虑到改进后文件合并的更为健康,因此我们也没有进一步修改默认的限速参数。

但对于写入性能波动有要求的场景,我们也可以进一步降低合并模块的限速,从而达到与之前版本近似的效果。

该案例主要是说明 IoTDB 对 CPU 利用率的观测和掌控能力。

最后一个案例则是某钢厂场景的写入性能周期性下降 5% 排查。

随着我们可观测性做的不断深入,很多用户也开始对我们的监控面板越来越感兴趣,每天就上来翻一翻。

那在翻的过程中如果发现监控面板中有一些不影响业务的异样,是否有必要继续深挖?我们欢迎并鼓励这样的行为。

该案例就是由于用户的深挖反而促进了我们内核迭代的进一步演进,从而达到了双赢的效果。

该场景的架构是一个 3c3d 的集群,客户端会定期攒批写入 IoTDB,也会定期查询单设备过去 1 天的全量数据。

在用户日常巡检监控面板时,他发现了一个有趣的现象,即每 7 天会出现一次持续一天的 5% 性能下降。

这个问题其实可大可小,如果没有我们的监控面板,业务都不会感知到这件事情的存在,但该用户跟我们进行了反馈,于是我们也进行排查。

我们首先排查了系统及进程 CPU 监控:发现写入性能下降 5% 之后 CPU 占用率增加 5%,那依然还是怀疑 CPU 利用率的升高应该是写入性能下降 5% 的根因。

然后我们排查了写入延迟拆解监控,发现写入性能刚下降时存在跨节点转发,这代表客户端缓存失效,同时也观测到写入性能下降时调度执行阶段 P99 耗时增加。这基本可以辅助确定写入性能下降时 IoTDB 切换了时间分区,导致数据需要被写到新的节点。

接着我们对查询进行了分析,尽管查询的逻辑数据量始终是最近一天的数据,但跨分片查询时,由于涉及到更多的 operator 算子和跨节点序列化反序列化开销,这也会对 CPU 造成更大的消耗。

至此该问题的原因便找到了,它也催生了 IoTDB 内核的两个后续优化,第一个是尽量使得同一设备的数据保留在一个分片中,这样即可以避免该现象出现,第二个则是线程池 CPU 利用率监控,有了它我们则可以更直观的观察到增加的 5% CPU 都是在查询线程池导致的,排查效率就更高了。

该案例主要说明了 IoTDB 对这种很细微的业务感知不到的波动也具有诊断和迭代能力。

最后对以上 5 个案例做一个总结:

IoTDB 的可观测性目标是高效定位遇到的一切性能问题,虽然还有很长的路要走,但大家已经能够看到我们这一年的质变。

据我们的经验而言,针对硬件环境和业务负载调优一般可以获得 50% - 1000% 的性能提升。

所以我们非常欢迎大家来试用我们的商业版 IoTDB,也非常欢迎大家在使用监控模块过程中对想不通的性能问题和我们沟通。

我们期待与用户一起获得业务和技术上的成长。

我的分享就这么多,谢谢大家!