普通视图

发现新文章,点击刷新页面。
昨天以前谭新宇的博客

2025 年终总结:从时序数据库到 AI Infra 的转身

作者 谭新宇
2026年2月12日 23:29

前言

2025 年转瞬即逝。回望这一年的经历,许多事都是撰写 2024 年年终总结时未曾预料的,这份不确定性也让复盘过程更具意义。

首先依然是自我介绍环节:我叫谭新宇,清华本硕,是一名开源技术爱好者,主要关注分布式存储、共识算法、时序数据库、可观测性与性能优化等领域,过去六年也一直在这些方向做一些工作。今年开始,我把重心从偏存储扩展到计算调度与机器学习平台基础设施。尽管在新领域仍是新人,但也算正式以 AI Infra 工程师的视角开始做事了。

2025

介绍完背景,下面按时间线回顾一下 2025。

一开年回来,同事告诉我:我们系统组 2024 年为 IoTDB 做的 TPCx-IoT 性能与性价比双登顶工作,榜单成绩被刷新了。这件事也让我很快进入状态。

TPCx-IoT 的核心指标是吞吐与性价比。吞吐可以通过横向扩展提升;性价比更依赖架构与工程细节——能否把资源利用率做到极致。我们 2024 年的上榜方案在集群多副本架构上已经做到相对 SOTA,且相关工作也在投稿路上,因此对于 “短期被赶超” 的结果确实有些意外。

我随后认真读了新榜一的公开报告。先说结论:其吞吐提升约 40%,其中相当一部分来自硬件资源增加约 7 倍(这条路我们也能走,甚至用 7 倍硬件资源的话吞吐还能更高);但奇怪的是,其性价比也提升约 15%。继续拆解后发现,差异点并不在软件方案本身,而在硬件计价口径:我们 2024 年是在云厂商环境测试,硬件报价相比其私有部署高出接近 10 倍。因此榜单上的“性价比优势”,更多来自硬件成本模型差异,而非软件层面的突破。

找到根因后,动作也很直接:我们将报价测试切换到线下部署。在软件方案不变、硬件成本下降一个数量级的前提下,相比新榜一,我们用更少硬件跑出了更高吞吐:吞吐提升约 50%,性价比进一步提升一倍。这次结果也再次确认了我们对软件方案本身的信心。

当时我一度得出“线下更便宜”的直觉结论。但谁能想到 2025 年年底,同款机器受内存条整体溢价影响,价格上涨接近 6 倍,几乎把这条结论拉平。站在 2026 年初再看,上云还是下云又变成一个需要随时间与供给波动不断重算的选择题。

上半年基于这些积累,我们也在继续积极推进论文投稿:一是“通用 Raft + LSM 的状态同步”。在强一致性前提下,尝试做一些类似 ECRaft 的工作,在特定场景里给出强一致性推导,并带来一定的实际性能收益。二是“异步复制 + LSM 的状态同步”。在几乎实时同步的前提下,实现三节点三副本集群性能接近单机三倍这一反直觉表现。我觉得这两个方向都很有价值。更幸运的是,其中一篇已被 SIGMOD 2026 接收,算是圆了我在 DB + Raft 领域的一个长期心愿。也期待后续能在这块有更多交流。

其实在春节期间那波 DeepSeek 带来的技术浪潮里,我就暗下决心:年后一定要去市场上拿一些“真实反馈”,验证自己过去两年的成长。由于年后工作依然很满,我的跳槽决心也没那么强;加上我也不太愿意把大量精力投入纯面试训练,所以准备和投递都相对克制:刷了二三十道 LeetCode,梳理了一遍简历与项目,然后就开面。前后两个月里,我断断续续面了六七家,主要是大厂、LLM 公司和量化,结果大致是一半挂一半过。

挂掉的那些机会,有的是薪资预期没对齐,沟通不久就结束;也有的是对 C++ 熟练度要求非常高,本质上是技术栈不匹配。刚开始因为 C++ 原因被挂时,确实会有挫败感:虽然我也能借助 Cursor 相对高效地写一些 C++ 项目,但毕竟写了 7 年 Java,缺少 C++ 系统开发经验;面试一旦深入到语言特性与工程细节,短期补课难免露出短板。

事后回想,这段经历让我更清楚地看到两类岗位的差异:一类岗位在面试中对某项具体技能要求较高,且不太接受转行。入职后通常希望你立刻产出,工作范围更聚焦,职责边界也更早确定。另一类团队更关注“你解决过什么问题、如何思考与协作”,对特定技能的硬性要求相对弱,但对综合能力要求更高。这类机会往往业务处于高速发展期,入职后更容易覆盖多个方向。这两种选择没有绝对好坏,但适配偏好和职业路径确实不同。对于跨行业跳槽的同学来说,显然后者更容易让你把既有能力迁移过去,拿到更合理的定价甚至溢价。

最后我通过的几个 offer,基本都是后者:要么需要跨语言,要么需要跨行业,但所有 offer 的薪资和发展方向都还不错。某种意义上,这也算是市场对我过去几年能力积累的一次确认——这份正反馈对我很重要,也让我踏实了不少。

到了 5 月,综合所有考虑后我选择离职,正式离开自己待了六年的环境,去探索学习 AI Infra 的挑战。在这里也想对同样在考虑跨行业跳槽但又担心面不过的朋友说一句:先准备起来面试看看,即使被拒几次也别太焦虑。面试本来就是双向选择,多尝试总会找到合适的。

回头看,这应该是我 2025 年最重要的决定。离开熟悉且擅长的领域需要勇气,但我最终还是倾向于在合适阶段主动拓宽技术与认知边界。比较幸运的是,目前入职 landing 比较顺利:新工作里既有技术挑战,也能产生业务价值;过往的技术积累和软素质依然派得上用场。方向上也不再只聚焦存储,而是更多接触计算调度、训练/推理平台等新领域,在新的问题域里从头补课并参与建设。

由于新工作不再维护开源产品,我的大部分精力投入在闭源产品上。但工作中依然深度使用了很多开源组件,因此我仍能在喜欢的开源社区里交流并贡献。比如我接着 Anyscale CTO Moritz 的遗留 issue,为 Ray 社区贡献了 K8s 下的 Ray Debug 方案,并被他合入成为 Ray 官方推荐的解决方案。同时,我业余时间持续贡献的 Ratis 社区也提名我成为 PMC,非常感谢社区的认可。

我认为工作中最理想的状态,是在一个节奏相对可持续但仍保持专注的环境里,自驱地做一些自己感兴趣、有技术挑战、对业务有价值且有成长空间的事情。这种状态可遇不可求,我也很庆幸目前正处于这样的状态。

以上是 2025 的主要经历。下面整理一些阶段性思考。

一些感悟

Alpha、Beta 与选择

今年换了赛道之后,我对 Alpha 和 Beta 这两个词有了更切身的体会。借用投资语言:Beta 是跟着大盘走拿到的收益;Alpha 是你能跑赢大盘的那部分。

回想 2021 年,国内 AI 四小龙纷纷陷入困境,数据库赛道如日中天、深受资本宠爱;短短四年后,AI 浪潮已经被称为“第四次工业革命”。身边那些做出巨大 impact 的同龄 AI 大佬们,个人实力当然重要,但很多时候真正起决定作用的,是 Alpha 与 Beta 的共振:能力是一回事,时代把天花板抬高又是另一回事。

但本质上,Beta 终究是外部变量:你能感知、能选择,但很难完全掌控。换到一个全新领域后,我最大的感受反而是:过去积累的很多东西并没有“归零”。比如入职后做性能优化、迭代流程、分布式计算框架学习时,以前沉淀的定位瓶颈和迭代效率的方法论,几乎可以直接复用。

这让我逐渐明确:真正属于自己的 Alpha,未必是某个领域的具体知识点,而是端到端解决问题的方法论、工程直觉和协作能力——它们跨领域依然成立,不会因为行业冷热就失效,也不会因为一次跳槽就消失。尤其在如今有了 LLM 之后,很多特定领域知识更容易在一段时间的投入中补上来。

所以我对 Beta 的态度不是不追求,而是不把它当成唯一变量。Beta 天然风险与收益并存:红利来得快,退潮也快。在若干个“风险与收益都能接受”的 Beta 环境里,我更愿意选一个能持续积累自己 Alpha 的方向,跟公司和团队形成双赢——对我来说这更舒服。毕竟投资自己的 Alpha,更像一笔没有回撤的复利。

职业生涯里的很多选择,大概永远无法从全局视角判断是不是最优解。我们能做的,往往只是用当下的认知做局部最优,结果好坏还夹杂不少运气。刚毕业那会我问过一位在互联网摸爬滚打十余年的前辈,他说这十年最大的感悟:乐观点叫“选择大于努力”,悲观点叫“看命”。

既然全局最优不可得,那我现在做选择时,会更关注下限而不是上限。上限当然诱人,但它很大程度取决于 Beta 的走势,不完全可控;下限反而更容易想清楚:最差情况能不能接受?不满足预期时有没有退路?把下限想清楚之后,选择就会简单很多——选那条即使结果不如上限预期,自己也不会后悔的路。

Impact 本质上与你在解决什么问题息息相关

今年经历社招,跟不少朋友和猎头深聊之后,我的一个感受越来越强:职业发展真的不是一条平滑曲线,更像爬台阶——平时看不出什么变化,但总会遇到一两个关键的坎。能不能迈过去,靠的往往不是临时抱佛脚,而是之前在做人做事上积累的那些“不起眼的复利”。

这次社招面到终面时,我能明显感觉到:老板们看的已经不只是技术能力了——更多是你做事的风格、你在意什么、你过去解决过什么问题、未来想解决什么问题。技术当然是入场券,但真正决定你能走多远的,可能是这些更贴近“人本身”的东西。

换了工作之后也更能体会到:从 junior 到 senior 的成长,不只是技术深度的精进,更重要的是视角的拓宽——从解决一个具体技术问题,到能把质量、效率、迭代这些维度串起来;再到能站在业务视角,找到技术的端到端生态位,形成闭环。这个过程其实不需要等到某个职级才开始。反过来讲,日常工作里多想一层,本身就是在练这件事。

今年换工作的过程中我也反复想过一个问题:职级、薪资和 impact 到底由什么决定?综合来看,它们大概率是对“一个人能解决问题的价值”的均值回归——短期可能有高估或低估,但拉长来看,市场总会给出相对公允的定价。想明白这一点之后,我反而没那么焦虑:与其盯着职级和薪资纠结,不如把注意力放在让自己能解决的问题越来越难、越来越有价值上。

从开源参与者到开源用户

以前在开源公司做开源产品时,我对开源的感受更多是“参与者”视角:怎么把自己负责的模块做好、怎么跟社区协作、怎么把 patch 合进去。今年换到一个以使用开源组件为主的环境后,我反而从“用户”视角重新理解了开源社区的价值。

大多数业务团队的目标很明确:用技术尽快创造业务价值。人力有限时,选对一个开源框架往往能较快做到 60–80 分;想做到 90 分以上,通常需要持续跟进社区动态,甚至参与共建。

从零到一把底层框架打磨到通用且可靠,成本极高。开源社区的价值在于:有一群人愿意长期专注,把通用能力持续打磨,并在大量不同场景中反复验证。

还记得刚毕业时,我跟好朋友苏总聊过:选工作时最好能选一个在 GitHub 上保持活跃的环境。当时我认为去开源公司做开源产品是最直接的路径。今年换了公司之后发现,即使不再全职做开源产品,只要工作中深度使用开源组件、需要跟社区交流甚至贡献 patch,再加上自己在社区承担的角色,GitHub 的活跃度依然可以保持。我对这种状态比较满意,也希望来年继续坚持。

做 SOTA 和用好 SOTA

以前我对技术价值的理解有点“理想主义”,总觉得只有在 SOTA 的基础上继续往前推,才算真正有价值。今年的感受让我意识到:SOTA 的诞生,和它真正落地之间,往往存在很大的鸿沟。

现实里,大多数团队对新技术的采用,确实会落后于 SOTA 很多。在这样的现状下,把 SOTA 结合具体业务场景真正落地,本身就能创造巨大的价值——你不一定非得站在最前沿把天花板再顶高一层;把前沿的东西用好、用对,本身也是一种稀缺能力。

比如现在 LLM 的能力已经很强,但如果每个人都能把其中哪怕一小部分能力真正用到自己的业务里,往往就会有意想不到的效果。

过去我更容易被“突破天花板”的工作吸引;但现在更能体会到,把好东西带到更多人面前并落地到真实场景里,同样很有价值。

先做到极致,再优化效率

今年用 AI Agent 越多,我越能感受到它能力进化的速度。印象很深的是:有些需求年初怎么描述都做不对;到了年末,同样的需求可能一句话就能搞定。

我观察到这个规律放到人身上也类似:能不能把事做到极致,本质上是一个 0/1 的信任问题(是否敢把这件事完全交给你);而在“已经做到极致”的前提下,如何进一步提升效率,更像一个可以稳步迭代的工程问题(如何更快、更省心、更可持续)。

回到自己身上也是一样:很多工作都希望尽可能做到最好,但资源有限时总会不断妥协,最后容易变成“每件事都做了,但每件事都差一口气”。这时候最让人不甘心的,往往不是没做,而是明明知道理想状态是什么,却没能把它做出来。

我经常会问自己一个问题:如果不考虑历史包袱,也不考虑眼前阻力,这件事的理想状态应该是什么样?如果真给我充足时间,我会怎么做?

这几年下来,我的一个体会是:如果一开始因为阻力没做到极致,后面即便阻力消失,也很容易因为惯性而不再补齐;但如果一开始就把它做到极致并且做成了,后续“怎么优化效率”往往是可解的——可以逐步拆解、逐步迭代,总能变快。这样单位时间里的成长也会更多,长期积累下来会形成更好的工作习惯与技术复利。

希望以后自己还能在更多事情上继续保持认真,把“先做到极致”尽量坚持住。

Infra 之路:从 Disk 到 Memory 到 GPU

如果把过去二十年的数据基础设施按“主要瓶颈”粗暴分段,我大概会这么理解:Disk 时代,瓶颈更多在磁盘与跨节点 IO,很多系统走 shared-nothing 路线,Hadoop + MapReduce 先把分布式存储与分布式计算的基本盘搭起来;Memory 时代,内存更便宜、网络更快,Spark 把更多中间状态留在内存里,把分布式计算效率推到更高水平,瓶颈也更常从磁盘转移到网络与 CPU。

现在到了 GPU 时代,我观察到新一轮的数据架构都在围绕 GPU 去重新设计。原因也很直接:GPU 把单位时间内的可用算力抬得太高了,系统瓶颈更频繁地从“算不动”变成“喂不饱”——数据搬运、访存、落盘、网络、调度与资源切分都会变成主要矛盾。这个视角下,NVMe、RDMA 这些更像“算力的供给链路”;围绕 GPU 的数据布局、IO 路径、缓存策略、batch 组织、任务切分与调度,才是新的主战场。很多我最近接触到的技术栈(例如分布式计算框架、训练/推理引擎与 Kubernetes 生态),本质上也都在解决同一个问题:当计算形态变了,怎样把工程系统重新组织起来,让算力稳定、可控、可规模化地转化成业务价值。

每一段技术栈的更迭,往往会带来至少十年的技术周期。从这个尺度看,GPU 这波的 Beta 收益还有很长空间,所以我并不太担心“2025 年入场算不算晚”。种一棵树最好的时间是十年前,其次是现在。至少对我自己而言,既然决定往这个方向走,那现在开始做一些围绕 GPU 的基础设施工作从新人重新学起,长期依然值得期待。

LLM 是消除信息差的有力工具

今年我对 LLM 的感受越来越具体:它最有价值的地方,可能不是“直接写出一个完美答案”,而是帮助缩小很多原本需要靠人脉、经验和踩坑才能补齐的信息差。

比如换到新领域后,一些概念、术语和最佳实践,以前往往要在博客、论文、issue、内部文档里检索一两周,才能拼出一个大致轮廓。现在只要把问题描述清楚,让它先给出一个“地图”(有哪些选项、trade-off 是什么、该看哪些关键词/资料、怎么验证),进入状态会快很多。

当然,LLM 也不是万能的。我现在更倾向于把它当成“高级搜索 + 快速补课 + 帮你写第一版草稿”的工具,而不是最终裁判。真正关键的两件事仍然在自己手上:第一,提问要具体,最好带上下文、约束和你期望的输出形式;第二,永远要有验证意识——不管它给你的是代码、结论还是方案,都得用实验、数据或一手资料去过一遍。

总之,LLM 让我更确信一件事:在信息密度越来越高的时代,能否快速学习、快速定位关键矛盾、快速验证假设会越来越重要。而 LLM 恰好把“学习—验证—迭代”的闭环速度往上抬了一档。

总结

2025 对我来说是认知变化很大的一年:从熟悉的方向出发,切换到新的赛道,也在这个过程中重新审视了许多过去的经历与积累。回头看,我很庆幸自己在合适的节点做了一次职业发展的改变——不是因为结果有多好,而是这段经历迫使我更认真地思考了几个长期问题:什么东西是真正属于自己的?自己的追求是什么?希望怎样去过完这一生?

这些问题也许没有标准答案,但至少现在的我比一年前更坚定,更坦然。我感谢上一段工作经历里一起并肩的人,也感谢 🍊 让我在工作之余更热爱这个世界并感受到生活的很多美好。

2026 年,希望自己能在新的领域继续扎根,继续把事情做到极致。

最后,感谢一路上帮助过我的领导、同事、朋友和家人。

预祝大家新年快乐,万事如意!

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

作者 谭新宇
2025年12月6日 14:47

背景

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 git@github.com: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 <pcmoritz@gmail.com>
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 <pcmoritz@gmail.com>
Co-authored-by: pcmoritz <pcmoritz@anyscale.com>
Co-authored-by: srinathk10 <68668616+srinathk10@users.noreply.github.com>
Co-authored-by: Edward Oakes <ed.nmi.oakes@gmail.com>

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 <dhyey2019@gmail.com>
Date: Mon Apr 7 12:41:34 2025 -0400

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

Signed-off-by: dayshah <dhyey2019@gmail.com>

.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年8月17日 00:39

背景

在软件开发过程中,具备单步调试能力的 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年5月14日 15:14

前言

过去五年半里,我在 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 提供如此强大的技术支持。

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

作者 谭新宇
2024年2月7日 15:24

前言

兜兜转转又是一年,不知不觉 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 的知名度和技术影响力。

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

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

分布式事务概述和对应代码框架介绍

作者 谭新宇
2022年4月21日 17:44

背景

分享一下前两天在 Talent Plan Community 做的有关分布式事务和 Distributed-Txn 代码框架的介绍。

这次分享除了对 2021 VLDB summer school 中讲授的若干重要主题进行了概述,还着重介绍了事件排序这一很本质的问题,此外也参考了不少优质资料,现在 share 出来希望能对这块知识感兴趣的同学有帮助。由于本人水平有限,如有原理错误欢迎与我沟通~

注:以下仅为图片,可以在 此处 在线浏览 PPT 原件和录屏。

内容
































Raft 算法和对应代码框架介绍

作者 谭新宇
2022年3月2日 10:10

背景

分享一下前不久在 Talent Plan Community 做的有关 Raft 算法和 Etcd/TinyKV 代码框架的介绍。

这次分享对 Raft 算法和对应实现做了较为系统的调研整理,不仅对若干经典问题做了介绍,也提供了不少优质参考资料,现在 share 出来希望能对这块知识感兴趣的同学有帮助。

注:以下仅为图片,可以在 此处 在线浏览 PPT 原件。

内容

































2021 Talent Plan KV 学习营结营总结

作者 谭新宇
2022年1月14日 17:23

背景

2021 年 11 月 ~ 2022 年 1 月 ,PingCAP 举办了第一届 Talent Plan KV 学习营,相关介绍可参考 推送

在本次比赛中,由于我们小组的两位成员之前都刷过 MIT 6.824,已经对教学级别的 raft 有一定的了解,所以参加此次比赛的目的就是去感受一下生产级别分布式 KV 的代码实现,学习实践一下 lsm, etcd, raftstore 和 percolator 的理论知识和 codebase。

u1s1,刷 lab 的过程十分曲折,我们俩所在的实验室到年底的时候都非常忙,前几周基本每周都只能抽出顶多一两天的时间来写代码,而理解 lab2b/lab3b raftstore 的难度是非常大的,我们用了一周多的时间才勉强看懂 raftstore 的代码。这使得到还剩两周时间的时候,我们才刷到 lab2c。最后两周我们利用中午午休时间和晚上睡觉时间疯狂加班,在 lab 上花了更多的时间,最后才堪堪刷完。

在刷 lab 的过程中,由于时间有限,我们始终秉持着学习优先,成绩第二的原则。即以 了解 codebase,学习知识,做最容易做且最有用的优化 为主,并没有去卷很多功能点。在处理 bug 的态度上,对于 safety 的问题比如错误读写的 bug 等,我们对这类问题进行了重点关注和解决;对于 liveness 的问题比如 request timeout 等,我们则是在有限的时间内尽力做了优化,但并没有投入太多精力,因为这种工作没有上限,tikv 的 raftstore 也一定在持续做这些工作,时间不够的情况下去卷这些就没有太大意义了。

出人意料的是,我们得了第二名的好成绩,具体可参考 官宣。事后反省一下,在 safety 上我们遇到的问题都解决了;在 liveness 上我们没投入太多精力;在文档上,我们简单介绍了代码实现,但将重点放在了我们对相关知识的理解和思考上;在性能上,我们重点做了最容易做的 batching 优化,其本质上是使用 raft 的优化而不是 raft 自身的优化,但对性能的提升却异常关键,比如 tidb 对于一个事务打包的一堆写请求,到 tikv 的 region 之后,这些写请求同步成一条还是多条 raftlog 对于性能的影响是巨大的。

从结果来看,我们的策略是正确的,我们在很有限的时间内拿到了很高的收益。

最后,出于对课程的保护,也出于跟大家分享一些刷 lab 的经验,让大家少踩坑,在此处我仅将文档公开,希望能为大家提供一些思路,欢迎一起交流。

文档

lab1

解题思路

Part 1 : Implement a standalone storage engine

本部分是对底层 badger api 的包装,主要涉及修改的代码文件是 standalone_storage.go, 需要实现 Storage 接口的 Write 和 Reader 方法,来实现对底层 badger 数据库的读写。

1.Write 部分实现思路

Write 部分涉及到 Put 和 Delete 两种操作。

因为 write_batch.go 中已经实现了对 badger 中 entry 的 put 和 delete 操作,我们只需要判断 batch 中的每一个 Modify 的操作类型,然后直接调用 write_batch.go 中相对应的方法即可。

2.Reader 部分实现思路

Reader 部分会涉及到 point read 和 scan read 两种不同读方式。

因为提示到应该使用 badger.Txn 来实现 Reader 函数,所以我们声明了一个 badgerReader 结构体来实现 StorageReader 接口,badgerReader 结构体内部包含对 badger.Txn 的引用。

针对 point read,
我们直接调用 util.go 中的 GetCF 等函数,对 cf 中指定 key 进行读取。

针对 scan read,
直接调用 cf_iterator.go 中的 NewCFIterator 函数,返回一个迭代器,供 part2 中调用。

Part 2 : Implement raw key/value service handlers

本部分需要实现 RawGet/ RawScan/ RawPut/ RawDelete 四个 handlers,主要涉及修改的代码文件是 raw_api.go

针对 RawGet,
我们调用 storage 的 Reader 函数返回一个 Reader,然后调用其 GetCF 函数进行点读取即可,读取之后需要判断对应 key 是否存在。

针对 RawScan,
同样地调用 storage 的 Reader 函数返回一个 Reader,然后调用其 IterCF 函数返回一个迭代器,然后使用迭代器读取即可。

针对 RawPut 和 RawDelete,
声明对应的 Modify 后,调用 storage.Write 函数即可。

相关知识学习

LSM 是一个伴随 NoSQL 运动一起流行的存储引擎,相比 B+ 树以牺牲读性能的代价在写入性能上获得了较大的提升。

近年来,工业界和学术界均对 LSM 树进行了一定的研究,具体可以阅读 VLDB2018 有关 LSM 的综述:LSM-based Storage Techniques: A Survey, 也可直接阅读针对该论文我认为还不错的一篇 中文概要总结

介绍完了 LSM 综述,可以简单聊聊 badger,这是一个纯 go 实现的 LSM 存储引擎,参照了 FAST2016 有关 KV 分离 LSM 的设计: WiscKey 。有关其项目的动机和一些 benchmark 结果可以参照其创始人的 博客

对于 Wisckey 这篇论文,除了阅读论文以外,也可以参考此 阅读笔记 和此 总结博客。这两篇资料较为系统地介绍了现在学术界和工业界对于 KV 分离 LSM 的一些设计和实现。

实际上对于目前的 NewSQL 数据库,其底层大多数都是一个分布式 KV 存储系统。对于 OLTP 业务,其往往采用行存的方式,即 key 对应的 value 便是一个 tuple。在这样的架构下,value 往往很大,因而采用 KV 分离的设计往往能够减少大量的写放大,从而提升性能。

之前和腾讯云的一个大佬聊过,他有说 TiKV 的社区版和商业版存储引擎性能差异很大。目前想一下,KV 分离可能便是 RocksDB 和 Titan 的最大区别吧。

lab2

解题思路

lab2a

Leader election

本部分是对 raft 模块 leader 选举功能的实现,主要涉及修改的代码文件是 raft.go、log.go

raft 模块 leader 选举流程如下:

第一步,我们首先实现对 raft 的初始化。

实现 log.go 中的 newLog 方法,调用 storage 的 InitialState 等方法对 RaftLog 进行初始化,读取持久化在 storage 中 term、commit、vote 和 entries,为后面的 lab 做准备。完成 RaftLog 的初始化后,再填充 Raft 中的相应字段,即完成 Raft 对象的初始化。

第二步,我们实现 Raft 对象的 tick() 函数

上层应用会调用 tick() 函数,作为逻辑时钟控制 Raft 模块的选举功能和心跳功能。因此我们实现 tick() 函数,当 Raft 状态是 Follower 时,检查自上次接收心跳之后,间隔时间是否超过了 election timeout,如果超过了,将发送 MessageType_MsgHup;当 Raft 状态时 Leader 时,检查自上次发送心跳之后,间隔时间是否超过了 heartbeat timeout,如果超过了,将发送 MessageType_MsgBeat。

第三步,我们实现 raft.Raft.becomeXXX 等基本函数

实现了 becomeFollower(),becomeCandidate(),becomeLeader() 等 stub 函数,对不同状态下的属性进行赋值。

第四步,我们实现 Step() 函数对不同 Message 的处理

主要涉及到的 Message 有

  • MessageType_MsgHup

  • MessageType_MsgRequestVote

  • MessageType_MsgRequestVoteResponse

接下来分情况实现:

(1)MessageType_Msgup

当 Raft 状态为 Follower 和 Candidate 时,会先调用 becomeCandidate() 方法,将自己的状态转变为 Candidate,然后向所有 peer 发送 MessageType_MsgRequestVote 消息,请求他们的投票

(2)MessageType_MsgRequestVote

当 Raft 接收到此消息时,会在以下情况拒绝投票:

  • 当 Candidate 的 term 小于当前 raft 的 term 时拒绝投票

  • 如果当前 raft 的 term 与 candidate 的 term 相等,但是它之前已经投票给其他 Candidate 时,会拒绝投票

  • 如果当前 raft 发现 candidate 的日志不如自己的日志更 up-to-date 时,也会拒绝投票

(3)MessageType_MsgRequestVoteResponse

Candidate 接收到此消息时,就会根据消息的 reject 属性来确定自己的得票,当自己的得票数大于一半以上,就会调用 becomeLeader() 函数,将状态转变为 Leader;当拒绝票数也大于一半以上时,就会转回到 Follower 状态。

Log replication

本部分是对 raft 模块日志复制功能的实现,主要涉及修改的代码文件是 raft.go、log.go

日志复制的流程如下:

Log Replication

本部分主要实现不同状态的 raft 对以下 Message 的处理:

  • MessageType_MsgBeat
  • MessageType_MsgHeartbeat
  • MessageType_MsgHeartbeatResponse
  • MessageType_MsgPropose
  • MessageType_MsgAppend
  • MessageType_MsgAppendResponse

接下来分情况实现:

(1)MessageType_MsgBeat

当上层应用调用 tick() 函数时,Leader 需要检查是否到了该发送心跳的时候,如果到了,那么就发送 MessageType_MsgHeartbeat。

leader 会将自己的 commit 值赋给在 MsgHeartbeat 消息中响应值,以让 Follower 能够及时 commit 安全的 entries

(2)MessageType_MsgHeartbeat

当 Follower 接收到心跳时,会更新自己的 electionTimeout,并会将自己的 lastIndex 与 leader 的 commit 值比较,让自己能够及时 commit entry。

(3)MessageType_MsgHeartbeatResponse

当 Leader 接收到心跳回复时,会比较对应 Follower 的 Pr.Match, 如果发现 Follower 滞后,就会向其发送缺少的 entries

(4)MessageType_MsgPropose

当 Leader 要添加 data 到自己的 log entries 中时,会发送一个 local message—MsgPropose 来让自己向所有 follower 同步 log entries,发送 MessageType_MsgAppend

(5)MessageType_MsgAppend

当 Follower 接收到此消息时,会在以下情况拒绝 append:

  • 当 Leader 的 term 小于当前 raft 的 term 时拒绝 append
  • 当 Follower 在对应 Index 处不含 entry,说明 Follower 滞后比较严重
  • 当 Follower 在对应 Index 处含有 entry,但是 term 不相等,说明产生了冲突

其他情况,Follower 会接收新的 entries,并更新自己的相关属性。

(6)MessageType_MsgAppendResponse

当 Leader 发现 Follower 拒绝 append 后,会更新 raft.Prs 中对应 Follower 的进度信息,并根据新的进度,重新发送 entries。

Implement the raw node interface

本部分主要实现 raw node 的接口,涉及修改的代码文件为 rawnode.go

RawNode 对象中的属性除了 Raft 对象,还增加了 prevSoftState 和 preHardState 两个属性,用于在 HasReady() 函数中判断 node 是否 pending

此外还实现了 Advance() 函数,主要是对 Raft 内部属性进行更新。

lab2b

Implement peer storage

本部分主要实现 peer_storage.go 中 SaveReadyState() 方法和 Append() 方法,涉及修改的代码文件为 peer_storage.go

peer storage 除了管理持久化 raft log 外,也会管理持久化其他元数据(RaftLocalState、RaftApplyState 和 RegionLocalState),因此我们需要实现 SaveReadyState() 方法,将 raft.Ready 中修改过的状态和数据保存到 badger 中。

首先我们通过实现 Append() 方法,保存需要持久化的 raft log。遍历 Ready 中 Entries,调用 SetMeta() 方法将他们保存到 raftWB,并删除可能未提交的 raft log,最后更新 raftState。

在处理完 raft log 后,我们还需要保存 Ready 中的 hardState,并在最后调用 WriteToDB() 方法保证之前的修改落盘。

Implement raft ready process

本部分主要实现 peer_storage_handler.go 中的 proposeRaftCommand() 和 HandleRaftReady() 方法,涉及修改的代码文件为 peer_storage_handler.go

proposeRaftCommand() 方法使得系统有能力将接收到的 client 请求通过 raft 模块进行同步,以实现分布式环境下的一致性。在本方法中,我们直接调用 raft 模块的 Propose 方法,将 client 请求进行同步,并为该请求初始化对应的 proposal,以便该请求 committed 后将结果返回给 client

当 msg 被 raft 模块处理后,会导致 raft 模块的一些状态变化,这时候需要 HandleRaftReady() 方法进行一些操作来处理这些变化:

  1. 需要调用 peer_storage.go() 中的 SaveReadyState() 方法,将 log entries 和一些元数据变化进行持久化。
  2. 需要调用 peer_storage_handler 中的 send() 方法,将一些需要发送的消息,发送给同一个 region 中的 peer
  3. 我们需要处理一些 committed entries,将他们应用到状态机中,并把结果通过 callback 反馈给 client
  4. 在上述处理完后,需要调用 advance() 方法,将 raft 模块整体推进到下一个状态

lab2c

因为 raft entries 不可能一直无限增长下去,所以本部分我们需要实现 snapshot 功能,清理之前的 raft entries。

整个 lab2c 的执行流程如下:

  1. gc log 的流程:

gc raftLog

  1. 发送和应用 snapshot 的流程:

send and apply snapshot

Implement in raft

当 leader 发现 follower 落后太多时,会主动向 follower 发送 snapshot,对其进行同步。在 Raft 模块内部,需要增加对 MessageType_MsgSnapshot 消息的处理,主要对以下两点进行处理:

  1. 当 leader 需要向 follower 同步日志时,如果同步的日志已经被 compact 了,那么直接发送 snapshot 给 follower 进行同步,否则发送 MessageType_MsgAppend 消息,向 follower 添加 entries。通过调用 peer storage 的 Snapshot() 方法,我们可以得到已经制作完成的 snapshot
  2. 实现 handleSnapshot() 方法,当 follower 接收到 MessageType_MsgSnapshot 时,需要进行相应处理。

在第二步中,follower 需要判断 leader 发送的 snapshot 是否会与自己的 entries 产生冲突,如果发送的 snapshot 是目前现有 entries 的子集,说明 snapshot 是 stale 的,那么要返回目前 follower 的进度,更新 leader 中相应的 Match 和 Next,以便再下一次发送正确的日志;如果没有发生冲突,那么 follower 就根据 snapshot 中的信息进行相应的更新,更新自身的 committed 等 index,如果 confstate 也产生变化,有新的 node 加入或者已有的 node 被移除,需要更新本节点的 confState,为 lab3 做准备。

Implement in raftstore

在本部分中,当日志增长超过 RaftLogGcCountLimit 的限制时,会要求本节点整理和删除已经应用到状态机的旧日志。节点会接收到类似于 Get/Put/Delete/Snap 命令的 CompactLogRequest,因此我们需要在 lab2b 的基础上,当包含 CompactLogRequest 的 entry 提交后,增加 processAdminRequest() 方法来对这类 adminRequest 的处理。

在 processAdminRequest() 方法中,我们需要更新 RaftApplyState 中 RaftTruncatedState 中的相关元数据,记录最新截断的最后一个日志的 index 和 term,然后调用 ScheduleCompactLog() 方法,异步让 RaftLog-gc worker 能够进行旧日志删除的工作。

另外,因为 raft 模块在处理 snapshot 相关的 msg 时,也会对一些状态进行修改,所以在 peer_storage.go 方法中,我们需要在 SaveReadyState() 方法中,调用 ApplySnapshot() 方法中,对相应的元数据进行保存。

在 ApplySnapshot() 方法中,如果当前节点已经处理过的 entries 只是 snapshot 的一个子集,那么需要对 raftLocalState 中的 commit、lastIndex 以及 raftApplyState 中的 appliedIndex 等元数据进行更新,并调用 ClearData() 和 ClearMetaData() 方法,对现有的 stale 元数据以及日志进行清空整理。同时,也对 regionLocalState 进行相应更新。最后,我们需要通过 regionSched 这个 channel,将 snapshot 应用于对应的状态机

相关知识学习

Raft

Raft 是 2015 年以来最受人瞩目的共识算法,有关其前世今生可以参考我们总结的 博客,此处不再赘述。

etcd 是一个生产级别的 Raft 实现,我们在实现 lab2a 的时候大量参考了 etcd 的代码。这个过程不仅帮助我们进一步了解了 etcd 的 codebase,也让我们进一步意识到一个工程级别的 raft 实现需要考虑多少 corner case。整个学习过程收获还是很大的,这里贴一些 etcd 的优质博客以供学习。

KVRaft

在 Raft 层完成后,下一步需要做的便是基于 Raft 层搭建一个高可用的 KV 层。这里依然参考了 etcd KV 层驱动 Raft 层的方式。
即总体的思路如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for {
select {
case <-s.Ticker:
Node.Tick()
default:
if Node.HasReady() {
rd := Node.Ready()
saveToStorage(rd.State, rd.Entries, rd.Snapshot)
send(rd.Messages)
for _, entry := range rd.CommittedEntries {
process(entry)
}
s.Node.Advance(rd)
}
}

做过 tinykv 的同学应该都能够感觉到 lab2b 的难度与之前有一个大 gap,我认为主要原因是需要看的代码实现是太多了。

如今回首,建议分三个步骤来做,这样效率可能会高一些:

  • 了解读写流程的详细步骤。对于 client 的请求,其处理和回复均在 raft_server.go 中进行了处理,然而其在服务端内部的生命周期如何,这里需要知根知底。(注意在遇到 channel 打断同步的执行流程时不能瞎猜,一定要明确找到 channel 的接收端和发送端继续把生命周期理下去)
  • 仔细阅读 raft_server.go, router.go, raftstore.go, raft_worker.go, peer_storage.go, peer_msg_handle.go 等文件的代码。这会对了解整个系统的 codebase 十分有帮助。
  • 仔细阅读 tinykv 的 lab2 文档,了解编码,存储等细节后便可以动手实现了。

在实现 lab2b 中,由于时间有限,我们重点关注了 batching 的优化和 apply 时的 safety,以下进行简单的介绍:

  • batching 优化:客户端发来的一条 command 可能包含多个读写请求,服务端可以将其打包成一条或多条 raft 日志。显然,打包成一条 Raft 日志的性能会更高,因为这样能够节省大量 IO 资源的消耗。当然这也需要在 apply 时对所有的 request 均做相应的业务和容错处理。

  • apply 时的 safety:要想实现基于 Raft 的 KV 服务,一大难点便是如何保证 applyIndex 和状态机数据的原子性。比如在 6.824 的框架中,Raft 层对于上层状态机的假设是易失的,即重启后状态机为空,那么 applyIndex 便可以不被持久化记录,因为一旦发生重启 Raft 实例可以从 0 开始重新 apply 日志,对于状态机来说这个过程保证不会重复。然而这样的实现虽然保证了 safety,但却不是一个生产可用的实现。对于 tinykv,其状态机为非易失的 LSM 引擎,一旦要记录 applyIndex 就可能出现与状态机数据不一致的原子性问题,即重启后可能会存在日志被重复 apply 到状态机的现象。为了解决这一问题,我们将每个 Index 下 entry 的应用和对应 applyIndex 的更新放到了一个事务中来保证他们之间的原子性,巧妙地解决了该过程的 safety 问题。

Snapshot

tinykv 的 Snapshot 几乎是一个纯异步的方案,在架构上有很多讲究,这里可以仔细阅读文档和一位社区同学分享的 Snapshot 流程 后再开始编码。

一旦了解了以下两个流程,代码便可以自然而然地写出来了。

  • log gc 流程
  • snapshot 的异步生成,异步分批发送,异步分批接收和异步应用。

lab3

解题思路

lab3a

本部分主要涉及 Raft 算法 leader transfer 和 conf change 功能的两个工作,主要涉及修改的代码文件是 raft.go

对于 leader transfer,注意以下几点即可:

  • leader 在 transfer 时需要阻写。
  • 当 leader 发现 transferee 的 matchIndex 与本地的 lastIndex 相等时直接发送 timeout 请求让其快速选举即可,否则继续发送日志让其快速同步。
  • 当 follower 收到 leader transfer 请求时,直接发起选举即可

对于 conf change,注意以下几点即可:

  • 只对还在共识组配置中的 raftnode 进行 tick。
  • 新当选的 leader 需要保证之前任期的所有 log 都被 apply 后才能进行新的 conf change 变更,这有关 raft 单步配置变更的 safety,可以参照 邮件 和相关 博客
  • 只有当前共识组的最新配置变更日志被 apply 后才可以接收新的配置变更日志。
  • 增删节点时需要维护 PeerTracker。

lab3b

本部分主要是在 3a 的基础上,在 raft store 层面实现对 TransferLeader、ChangePeer 和 Split 三种 AdminRequest 的处理,涉及修改的文件主要是 peer_msg_handler.go 和 peer.go

对于 TransferLeader,比较简单:

TransferLeader request 因为不需要复制到 follower 节点,所以在 peer_msg_handler.go 的 pproposeRaftCommand() 方法中直接调用 raw_node.go 中的 TransferLeader() 方法即可

对于 ConfChange,分 addNode 和 removeNode 两种行为处理。

当 addNode 的命令 commit 之后,不需要我们手动调用 createPeer() 或者 maybeCreatePeer() 来显式创建 peer。我们只需要对 d.ctx 中的 storeMeta 进行修改即可,新 peer 会通过心跳机制进行创建。

当 removeNode 的命令 commit 之后,与 addNode 命令不同的是,我们需要显式调用 destroyPeer() 函数来停止相应的 raft 模块。这时需要注意的一个点时,当 Region 中只剩下两个节点,要从这两个节点中移除一个时,如果有一个节点挂了,会使整个集群不可用,特别是要移除的节点是 leader 本身。

在测试中会遇到这样的问题:当 Region 中只剩下节点 A(leader)和 节点 B(follower),当 removeNode A 的命令被 commit 之后,leader 就进行自我销毁,如果这个时候进入了 unreliable 的状态,那么 leader 就有可能无法在 destory 之前通过 heartbeat 去更新 follower 的 commitIndex。这样使得 follower B 不知道 leader A 已经被移除,就算发起选举也无法收到节点 A 的 vote,最终无法成功,导致 request timeout。

对于 split, 需要注意:

  1. 因为 Region 会进行分裂,所以需要对 lab2b 进行修改,当接收到 delete/put/get/snap 等命令时,需要检查他们的 key 是否还在该 region 中,因为在 raftCmd 同步过程中,可能会发生 region 的 split,也需要检查 RegionEpoch 是否匹配。
  2. 在比较 splitKey 和当前 region 的 endKey 时,需要使用 engine_util.ExceedEndKey(),因为 key range 逻辑上是一个环。
  3. split 时也需要对 d.ctx 中的 storeMeta 中 region 相关信息进行更新。
  4. 需要显式调用 createPeer() 来创建新 Region 中的 peer。
  5. 在 3b 的最后一个测试中,我们遇到以下问题:
    1. 达成共识需要的时间有时候比较长,这就会导致新 region 中无法产生 leade 与 Scheduler 进行心跳交互,来更新 Scheduler 中的 regions,产生 find no region 的错误。这一部分可能需要 pre-vote 来进行根本性地解决,但时间不够,希望以后有时间解决这个遗憾。
    2. 会有一定概率遇到“多数据”的问题,经排查发现 snap response 中会包含当前 peer 的 region 引用返回,但是这时可能会产生的一个问题时,当返回时 region 是正常的,但当 client 端要根据这个 region 来读的时候,刚好有一个 split 命令改变了 region 的 startKey 或者 endKey,最后导致 client 端多读。该问题有同学在群中反馈应该测试中对 region 进行复制。
    3. 会有一定概率遇到“少数据”的问题,这是因为当 peer 未初始化时,apply snapshot 时不能删除之前的元数据和数据。

lab3c

本部分主要涉及对收集到的心跳信息进行选择性维护和对 balance-region 策略的具体实现两个工作,主要涉及修改的代码文件是 cluster.go 和 balance_region.go

对于维护心跳信息,按照以下流程执行即可:

  • 判断是否存在 epoch,若不存在则返回 err
  • 判断是否存在对应 region,如存在则判断 epoch 是否陈旧,如陈旧则返回 err;若不存在则选择重叠的 regions,接着判断 epoch 是否陈旧。
  • 否则维护 region 并更新 store 的 status 即可。

对于 balance-region 策略的实现,按照以下步骤执行即可:

  • 获取健康的 store 列表:
    • store 必须状态是 up 且最近心跳的间隔小于集群判断宕机的时间阈值。
    • 如果列表长度小于等于 1 则不可调度,返回空即可。
    • 按照 regionSize 对 store 大小排序。
  • 寻找可调度的 store:
    • 按照大小在所有 store 上从大到小依次寻找可以调度的 region,优先级依次是 pending,follower,leader。
    • 如果能够获取到 region 且 region 的 peer 个数等于集群的副本数,则说明该 region 可能可以在该 store 上被调度走。
  • 寻找被调度的 store:
    • 按照大小在所有 store 上从小到达依次寻找不存在该 region 的 store。
    • 找到后判断迁移是否有价值,即两个 store 的大小差值是否大于 region 的两倍大小,这样迁移之后其大小关系依然不会发生改变。
  • 如果两个 store 都能够寻找到,则在新 store 上申请一个该 region 的 peer,创建对应的 MovePeerOperator 即可。

相关知识学习

Multi-Raft

Multi-Raft 是分布式 KV 可以 scale out 的基石。TiKV 对每个 region 的 conf change 和 transfer leader 功能能够将 region 动态的在所有 store 上进行负载均衡,对 region 的 split 和 merge 则是能够解决单 region 热点并无用工作损耗资源的问题。不得不说,后两者尽管道理上理解起来很简单,但工程实现上有太多细节要考虑了(据说贵司写了好几年才稳定),分析可能的异常情况实在是太痛苦了,为贵司能够啃下这块硬骨头点赞。

最近看到有一个基于 TiKV 的 hackathon 议题,其本质是想通过更改线程模型来优化 TiKV 的写入性能、性能稳定性和自适应能力。这里可以简单提提一些想法,其实就我们在时序数据库方向的一些经验来说,每个 TSM(TimeSeries Merge Tree)大概能够用满一个核的 CPU 资源。只要我们将 TSM 引擎额个数与 CPU 核数绑定,写入性能基本是能够随着核数增加而线性提升的。那么对于 KV 场景,是否开启 CPU 个数的 LSM 引擎能够更好的利用 CPU 资源呢?即对于 raftstore,是否启动 CPU 个数的 Rocksdb 实例能够更好的利用资源呢?感觉这里也可以做做测试尝试一下。

负载均衡

负载均衡是分布式系统中的一大难题,不同系统均有不同的策略实现,不同的策略可能在不同的 workload 中更有效。

相比 pd 的实现,我们在 lab3c 实现的策略实际上很 trivial,因此我们简单学习了 pd 调度 region 的 策略。尽管这些策略道理上理解起来都比较简单,但如何将所有统计信息准确的量化成一个动态模型却是一件很难尽善尽美的事,这中间的很多指标也只能是经验值,没有严谨的依据。

有关负载均衡我们对学术界的相关工作还不够了解,之后有时间会进行一些关注。

lab4

解题思路

本 Lab 整体相对简单,在基本了解 MVCC, 2PC 和 Percolator 后便可动手了,面向测试用例编程即可。

lab4a

本部分是对 mvcc 模块的实现,主要涉及修改的代码文件是 transaction.go。需要利用对 CFLock, CFDefault 和 CFWrite 三个 CF 的一些操作来实现 mvcc。

针对 Lock 相关的函数:

  • PutLock:将 PUT 添加到 Modify 即可。
  • DeleteLock:将 Delete 添加到 Modify 即可。
  • GetLock:在 CFLock 中查找即可。

针对 Value 相关的函数:

  • PutValue:将 PUT 添加到 Modify 即可。
  • DeleteValue:将 Delete 添加到 Modify 即可。
  • GetValue:首先从 CFWrite 中寻找在当前快照之前已经提交的版本。如果未找到则返回空,如果找到则正对不同的 Kind 有不同的行为:
    • Put:根据 value 中的 StartTS 去 CFDefault 寻找即可。
    • Delete:返回空即可。
    • Rollback:继续寻找之前的版本。

针对 Write 相关的函数:

  • PutWrite:将 PUT 添加到 Modify 即可。
  • CurrentWrite:从 CFWrite 当中寻找当前 key 对应且值的 StartTS 与当前事务 StartTS 相同的行。
  • MostRecentWrite:从 CFWrite 当中寻找当前 key 对应且值的 StartTS 最大的行。

lab4b

本部分是对 Percolator 算法 KVPreWrite, KVCommit 和 KVGet 三个方法的实现,主要涉及修改的代码文件是 server.go, query.go 和 nonquery.go。

  • KVPreWrite:针对每个 key,首先检验是否存在写写冲突,再检查是否存在行锁,如存在则需要根据所属事务是否一致来决定是否返回 KeyError,最后将 key 添加到 CFDefault 和 CFLock 即可。
  • KVCommit:针对每个 key,首先检查是否存在行锁,如不存在则已经 commit 或 rollback,如存在则需要根据 CFWrite 中的当前事务状态来判断是否返回 KeyError,最后将 key 添加到 CFWrite 中并在 CFLock 中删除即可。
  • KVGet:首先检查行锁,如为当前事务所锁,则返回 Error,否则调用 mvcc 模块的 GetValue 获得快照读即可。

lab4c

本部分是对 Percolator 算法 KvCheckTxnStatus, KvBatchRollback, KvResolveLock 和 KvScan 四个方法的实现,主要涉及修改的代码文件是 server.go, query.go 和 nonquery.go。

  • KvCheckTxnStatus:检查 PrimaryLock 的行锁,如果存在且被当前事务锁定,则根据 ttl 时间判断是否过期从而做出相应的动作;否则锁很已被 rollback 或者 commit,从 CFWrite 中获取相关信息即可。
  • KvBatchRollback:针对每个 key,首先检查是否存在行锁,如果存在则删除 key 在 CFLock 和 CFValue 中的数并且在 CFWrite 中写入一条 rollback 即可。如果不存在或者不归当前事务锁定,则从 CFWrite 中获取当前事务的提交信息,如果不存在则向 CFWrite 写入一条 rollback,如果存在则根据是否为 rollback 判断是否返回错误。
  • KvResolveLock:针对每个 key,根据请求中的参数决定来 commit 或者 rollback 即可。
  • KvScan:利用 Scanner 扫描到没有 key 或达到 limit 阈值即可。针对 scanner,需要注意不能读有锁的 key,不能读未来的版本,不能读已删除或者已 rollback 的 key。

代码结构

为了使得 server.go 逻辑代码清晰,在分别完成三个 lab 后对代码进行了进一步整理,针对读写请求分别抽象出来了接口,这样可以使得逻辑更为清晰。

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
type BaseCommand interface {
Context() *kvrpcpb.Context
StartTs() uint64
}

type Base struct {
context *kvrpcpb.Context
startTs uint64
}

type QueryCommand interface {
BaseCommand
Read(txn *mvcc.MvccTxn) (interface{}, error)
}

func ExecuteQuery(cmd QueryCommand, storage storage.Storage) (interface{}, error) {
ctx := cmd.Context()
reader, err := storage.Reader(ctx)
if err != nil {
return &kvrpcpb.ScanResponse{RegionError: util.RaftstoreErrToPbError(err)}, nil
}
defer reader.Close()
return cmd.Read(mvcc.NewMvccTxn(reader, cmd.StartTs()))
}

type NonQueryCommand interface {
BaseCommand
IsEmpty() bool
GetEmptyResponse() interface{}
WriteKeys(txn *mvcc.MvccTxn) ([][]byte, error)
Write(txn *mvcc.MvccTxn) (interface{}, error)
}

func ExecuteNonQuery(cmd NonQueryCommand, storage storage.Storage, latches *latches.Latches) (interface{}, error) {
if cmd.IsEmpty() {
return cmd.GetEmptyResponse(), nil
}

ctx := cmd.Context()
reader, err := storage.Reader(ctx)
if err != nil {
return &kvrpcpb.ScanResponse{RegionError: util.RaftstoreErrToPbError(err)}, nil
}
defer reader.Close()
txn := mvcc.NewMvccTxn(reader, cmd.StartTs())

keys, err := cmd.WriteKeys(txn)
if err != nil {
return nil, err
}

latches.WaitForLatches(keys)
defer latches.ReleaseLatches(keys)

response, err := cmd.Write(txn)
if err != nil {
return nil, err
}

err = storage.Write(ctx, txn.Writes())
if err != nil {
return nil, err
}

latches.Validation(txn, keys)

return response, nil
}

相关知识学习

有关分布式事务,我们之前有过简单的 学习,对 2PL, 2PC 均有简单的了解,因此此次在实现 Percolator 时只需要关注 2PC 与 MVCC 的结合即可,这里重点参考了以下博客:

实现完后,我们进一步被 Google 的聪明所折服,Percolator 基于单行事务实现了多行事务,基于 MVCC 实现了 SI 隔离级别。尽管其事务恢复流程相对复杂,但其本质上是在 CAP 定理中通过牺牲恢复时的 A 来优化了协调者正常写入时的 A,即协调者单点在 SQL 层不用高可用来保证最终执行 commit 或者 abort。因为一旦协调者节点挂掉,该事务在超过 TTL (TTL 的超时也是由 TSO 的时间戳来判断,对于各个 TiKV 节点来说均为逻辑时钟,这样的设计也避免了 Wall Clock 的同步难题)后会被其他事务 rollback,总体上来看 Percolator 比较优雅的解决了 2PC 的 safety 问题。

当然,分布式事务可以深究的地方还很多,并且很多思想都与 Lamport 那篇最著名的论文 Time, Clocks, and the Ordering of Events in a Distributed System 有关。除了 TiDB 外,Spanner,YugaByte,CockroachDB 等 NewSQL 数据库均有自己的大杀器,比如 TrueTime,HLC 等等。总之这块儿挺有意思的,虽然在这儿告一段落,但希望以后有机会能深入做一些相关工作。

总结

实现一个稳定的分布式系统实在是太有挑战太有意思啦。

感谢 PingCAP 社区提供如此优秀的课程!

15-445 数据库课程学习总结

作者 谭新宇
2021年9月12日 15:12

背景

众所周知,CMU 15-445/721 是数据库的入门神课,类似于 MIT 6.824 之于分布式系统一样。由于前半年学习了 MIT 6.824 课程后感觉个人收获很大,因此在今年暑假,我抽时间学习完了 CMU 15-445 的网课,现做一概要总结。

总结

15-445 可以当做数据库的入门课程,授课老师是著名网红教授 Andy Pavlo,以下是他的 Google Scholar 主页,还是非常厉害的。

本课程的组织方式采用了自底向上的方式,分别介绍了文件管理,缓冲池管理,索引管理,执行管理,查询优化,并发控制和容错恢复等内容,基本讲述了如何从 0 实现一个单机关系型数据库。由于时间有限,没来得及做课程笔记。因此在参考资料部分列出了课程所有的 PPT 资料以及一些从网上找到的优质课程笔记,以备日后温习之用。

当然,由于课程内容涉及的范围很广,所以每个章节都只是进行了相对简单的介绍。要想了解更多细节,建议结合大黑砖《数据库系统概念》来学习。2021 年 6 月,最新第七版的中文译版已经发行,赶紧买一本镇脑吧!

对于其作业 bustub,由于其需要基于 C++17 实现,而本人在目前没有太多的 C++ 知识储备,所以就暂时搁置了,毕竟想学的是数据库而不是 C++。不过我也注意到,MIT 6.830 数据库课程的作业 simple-db 是基于 Java 的,且其 6 个 lab 的内容基本覆盖了 CMU 15-445 lab 的内容,所以刷一刷 MIT 6.830 的 lab 也挺有意义的,希望自己后半年能抽出些时间吧。

此外,简单看了一下 15-721 的 课程主页,感觉其更多的是在讲 research 方向的工作,基本是在讲各个方向的 sota,那么这门课可以等到工作之后再说吧,目前来看优先级不是很高。

参考资料

6.824 分布式系统课程学习总结

作者 谭新宇
2021年6月30日 21:59

Lab

2021 年 6 月 30 日,本人总算刷完了 6.824 的 lab 并整理完了文档,发篇博客庆祝一下!!!

目前能够稳定通过 6.824 lab 所有的测试,并尽可能的提升了代码可读性。

不保证绝对的 bug-free,但每个 lab 均测试 500 次以上,无一 fail。

为了遵守课程代码开放协议,只开源了文档,具体可参考 repo

配合 raft 博士论文翻译raft 算法介绍 阅读效果更佳。

如有收获,希望点个 star 以表支持。十分感谢!

课程内容

基本上每篇论文都结合课程内容做了对应的论文阅读笔记。链接如下:

由于时间有限,以上博客中参考了大量其他优质博客,本人在此对于这些博客的作者表示真诚的感谢。

❌
❌