Ray RocksDB GCS 后端 `session_name` 恢复机制:marker 文件如何弥合 GCS 启动前的引导缺口

发布时间:2026/9/21 1:43:49
Ray RocksDB GCS 后端 `session_name` 恢复机制:marker 文件如何弥合 GCS 启动前的引导缺口
Ray RocksDB GCS 后端session_name恢复机制marker 文件如何弥合 GCS 启动前的引导缺口【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray本篇文章聚焦 Ray 核心分布式运行时中一个精妙的工程细节当 GCSGlobal Control Service采用 RocksDB 持久化后端时重启的 head 进程如何在 GCS 尚未启动、internal_kv完全不可用的窗口期内恢复集群上一次的session_name。文章将以 rocksdb_session_name_recovery.md 为主线结合 node.py 中读写路径的源码实现、gcs_server.cc 的 C 侧校验与 conftest.py 中的测试夹具完整还原引导bootstrap→ 标记文件marker file→ 恢复recovery的闭环帮助读者理解 Ray 在容错重启场景下如何保证状态一致、如何选择正确的持久化原语。什么是session_name为什么重启时必须恢复它session_name是一段标识单次 Ray 会话的字符串形如session_2026-06-19_15-30-45_123456_12345。它在 Ray 中有两个重要载体目录名head 节点临时目录下所有session_*子目录日志、运行时环境、sockets 等都以它为前缀即 node.py 中的self._session_dir os.path.join(self.temp_dir, self._session_name)GCS 持久化键它以session_name为 key、以KV_NAMESPACE_SESSION为命名空间写入 GCS 的internal_kv内部 KV 存储。问题的关键在于重启语义当 head 进程崩溃后重启而非首次创建集群时head 必须恢复出与之前完全相同的session_name而不是重新生成一个新名字。原因很直接——GCS 的持久化存储里仍然保留着旧值如果 head 新铸造了一个名字随后 node.py 中internal_kv_putoverwriteFalse返回失败后触发的一致性断言就会命中assert curr_val self._session_name.encode(utf-8), ( fSession name {self._session_name} does not match fpersisted value {curr_val}. Perhaps there was an ferror connecting to the GCS storage backend. )也就是说能用旧名字不是可选项而是 head 重启能否成功的前提。RocksDB 后端引入的引导问题bootstrap problem恢复session_name的读取动作发生在GCS 进程启动之前。那么旧名字存在哪里、此时能不能读到完全取决于 GCS 的存储后端。原文档用一张对照表精确概括了两者的本质差异后端旧名字保存在哪GCS 启动前可读吗Redis一个独立的Redis 进程且一直在运行可以——直接连接 Redis 即可RocksDB内嵌在 GCS 服务进程内部的数据库文件不可以——只能经由GCS 访问而 GCS 还没起来这正是引导问题的核心悖论在 RocksDB 后端下唯一能读出已持久化名字的组件GCS恰好是那个尚未启动的组件。此时internal_kv完全不可用head 无法向 GCS 发起任何 KV 查询。从 C 侧可以印证这种内嵌关系GCS 服务端根据RayConfig::instance().gcs_storage()决定存储类型见 gcs_server.cc 中的GetStorageType()其中 RocksDB 分支要求gcs_storage_path非空且仅支持 Linux而真正的 KV 读写则由 rocksdb_store_client.cc 直接打开数据库文件完成RAY_CHECK(!db_path.empty()) RAY_gcs_storage_path must be set when RAY_gcs_storagerocksdb. (RAY_CONFIG env ;数据库文件、KV 表都位于gcs_storage_path指定的持久卷上且读写只发生在 GCS 进程内——这决定了任何从外部读 RocksDB的思路都行不通。marker 文件如何弥合缺口解决方案出奇地朴素在gcs_storage_path/session_name位置放置一个普通文件与 RocksDB 数据库文件位于同一块持久卷上。上一个 head 进程负责写入它下一个 head 进程在 GCS 存在之前负责读取它。由于存储路径本身是可持久化的它承载着 RocksDB 自己的数据这个文件自然能跨越本次重启存续下来。原文档中的时序图完整描述了这一闭环为什么是文件而不是更聪明的方案不能查询internal_kv如前所述GCS 未启动KV 服务不可达不能由 Python head 直接打开 RocksDB 数据库RocksDB 是**单写者single-writer**数据库head 进程若作为第二写者打开它会破坏数据库一致性。GCS 进程是唯一的合法写者一个与数据同地共存的普通文件是最简单且正确的选择它的持久化域与它所描述的数据完全一致——同一卷同生共死。文件跟着数据库一起存活或一起丢失不会出现文件在但库没了或反之的错位。读取路径源码剖析check_persisted_session_name读取逻辑的入口在 node.py 的Node初始化过程中当session_name未显式指定时head 节点调用check_persisted_session_name()尝试恢复旧名字恢复不到首次创建集群才铸造新名字maybe_key self.check_persisted_session_name() if maybe_key is None: date_str datetime.datetime.today().strftime(%Y-%m-%d_%H-%M-%S_%f) self._session_name fsession_{date_str}_{os.getpid()} else: self._session_name ray._common.utils.decode(maybe_key)注意新名字的格式session_日期时间微秒_pid——因此每次新铸造的名字必然不同这正是重启时必须走恢复路径的原因。check_persisted_session_name()是一个按后端分派的分发器node.pyRocksDB 后端走_check_persisted_rocksdb_session_name()其余含外部 Redis走 Redis 直连路径get_session_key_from_storage。RocksDB 路径的实现如下def _check_persisted_rocksdb_session_name(self): rocksdb_storage_path self._resolve_ray_config(gcs_storage_path, ) if not rocksdb_storage_path: raise ValueError( RAY_gcs_storagerocksdb requires RAY_gcs_storage_path to be set to a writable directory. ) session_name_file os.path.join(rocksdb_storage_path, session_name) try: with open(session_name_file, rb) as f: persisted f.read().strip() return persisted if persisted else None except FileNotFoundError: return None几个值得注意的细节配置通过_resolve_ray_config(gcs_storage_path, )解析它会同时考虑_system_config与RAY_gcs_storage_path环境变量见 node.py 的注释确保 Python 侧与 GCS 进程实际选择的后端一致路径缺失时直接抛异常而非静默跳过源码注释明确指出这镜像了 C 侧gcs_server.cc::GetStorageType()中的RAY_CHECK行为——宁可响亮地失败也不能静默跳过恢复否则新铸造的名字会在稍后的持久化值一致性断言处引爆文件不存在FileNotFoundError被视作没有历史记录返回None与首次创建集群的语义对齐读取时做了strip()容忍文件末尾的换行符。写入路径源码剖析_persist_rocksdb_session_name_file与两条不变式写入侧由 node.py 的_write_cluster_info_to_kv()触发——该函数在 head 启动进程的流程start_head_processes()node.py中于 GCS 启动后被调用。其中 RocksDB 分支先写 marker 文件再做internal_kv_putif self._is_rocksdb_gcs(): self._persist_rocksdb_session_name_file() added self.get_gcs_client().internal_kv_put( bsession_name, self._session_name.encode(), False, # overwriteFalse ray_constants.KV_NAMESPACE_SESSION, )原文档强调写路径必须恪守两条不变式这正是本方案正确性的基石。不变式一顺序——marker 文件必须先于internal_kv_put保持不变量文件存在 ⇒ RocksDB 已经或即将拥有该名字。若两步之间发生崩溃顺序是有利的下一次重启读到文件随后的internal_kv_putoverwriteFalse会干净地插入两条路径重新收敛。反过来则危险RocksDB 里存了名字但文件缺失下一次 head 无法恢复、铸造新名字最终触发断言。不变式二持久性——原子写tmp fsync rename dir fsyncinternal_kv_put经由 RocksDB 的 WAL fsync 保证持久性因此文件必须拥有同等的持久性否则一次断电崩溃可能让 RocksDB 的状态落盘、而文件仍停留在页缓存中丢失。_persist_rocksdb_session_name_file()的实现完整展示了这一教科书级原子持久化序列node.pyos.makedirs(rocksdb_storage_path, exist_okTrue) tmp_fd, tmp_path tempfile.mkstemp( dirrocksdb_storage_path, prefixsession_name., suffix.tmp, ) try: with os.fdopen(tmp_fd, wb) as f: f.write(self._session_name.encode(utf-8)) f.flush() os.fsync(f.fileno()) os.replace(tmp_path, session_name_file) dir_fd os.open(rocksdb_storage_path, os.O_DIRECTORY) try: os.fsync(dir_fd) finally: os.close(dir_fd) except BaseException: if os.path.exists(tmp_path): os.unlink(tmp_path) raise拆解这四个阶段tmp在同目录下用mkstemp创建临时文件保证与目标文件位于同一文件系统跨文件系统rename不保证原子fsync文件f.flush()之后显式os.fsync(f.fileno())把文件内容与元数据刷入磁盘renameos.replace原子替换目标文件读者永远看到完整内容或旧内容不会看到半截写入dir fsyncos.open(..., os.O_DIRECTORY)后对目录做 fsync确保重命名本身也持久化——这是许多实现容易遗漏、但对断电恢复至关重要的一步。失败即致命_write_cluster_info_to_kv()的注释明确写道——marker 写入失败按致命错误处理。理由同样朴素存储路径上同时躺着 RocksDB 自己的文件路径不可写意味着 GCS 本就无法工作响亮地失败远比瘸着腿走进断言要好。C 侧配套校验与配置前提marker 机制并非孤立存在于 Python 侧它在 C 侧有对称的强校验ray_config_def.h 定义了两个关键配置项gcs_storage默认memory与gcs_storage_path默认空字符串均可通过RAY_gcs_storage、RAY_gcs_storage_path环境变量覆盖gcs_server.cc 的 RocksDB 分支RAY_CHECK存储路径非空并且在非 Linux 平台直接RAY_LOG(FATAL)——RocksDB 后端目前仅支持 Linuxrocksdb_store_client.cc 在打开数据库前再次校验db_path非空。因此启用该机制的环境前提可归纳为Linux 平台、RAY_gcs_storagerocksdb、RAY_gcs_storage_path指向持久卷上的可写目录。这也是为什么 head 重启后 marker 文件必然还在——它与 RocksDB 数据库共享同一持久卷的存亡。测试与验证如何在本地复现仓库的测试基础设施提供了现成的验证路径。conftest.py 中的_setup_rocksdb_gcs夹具展示了最小化启动配置——把 DB 放进 fixture 作用域的临时目录并通过环境变量完成后端切换os.environ[RAY_gcs_storage] rocksdb os.environ[RAY_gcs_storage_path] tmpdirname感兴趣的同学可以在本地Linux做如下实验验证完整闭环设置RAY_gcs_storagerocksdb与RAY_gcs_storage_path持久目录后启动一个 head 节点观察持久目录/session_name文件中出现本次会话的名字与ray start输出或日志中的 session 目录名一致强杀 head 相关进程后重启重启后的会话应复用同一个session_name——目录名一致、无断言报错若删除 marker 文件后再重启则会触发session_name一致性断言。总结一句话概括整个机制marker 文件是一枚微小、持久、与数据同地共存的面包屑让重启中的 head 在唯一真相源RocksDB-via-GCS被证明不可用的时间窗口内仍能得知先前的session_name。这个设计之所以值得品味在于它的取舍哲学面对GCS 未启动、KV 不可查、数据库只允许单写者的三重约束作者没有引入任何新组件、新协议或新分布式原语而是选择了一个与数据同卷共存的普通文件并用两条硬不变式先文件后 KV、原子持久化写入把正确性钉死。它同时示范了 Ray 工程中一条可复用的经验当持久化状态与引导时序相互纠缠时最简单的、与数据共享持久域的原语往往就是最正确的答案。相关的完整设计推演记录在 rocksdb_session_name_recovery.md读写两侧的实现均在 python/ray/_private/node.pyC 侧约束见 gcs_server.cc 与 rocksdb_store_client.cc可供继续深入阅读。【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考