ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

共享Ray集群多用户资源隔离实战:四层方案与踩坑记录

共享Ray集群多用户资源隔离实战:四层方案与踩坑记录 我们组上个月把一台 16 个节点、总共 128 核、256GB 内存、8 卡 GPU 的 Ray 集群正式开放给三个算法小组共用。开放第二天下午训练平台的告警群就炸了A 组同学跑了一个num_cpus32的 Actor 挂在集群里忘了删B 组所有任务瞬间全部卡死到了晚上另一个组的人把 50GB 的 NumPy 数组一次性put进 object store连 C 组的调度日志都开始疯狂超时。也就是从那天起我才真正开始认真做 Ray 集群的多用户资源隔离。这篇不是官方文档搬运而是我把 namespace、max_resources、Placement Group、Autoscaler 节点出口这四层方案逐个试了一遍之后的完整记录。我会讲清楚每一层解决什么问题、怎么配、为什么这样配以及哪些方案看起来像隔离、实际根本不限制资源。想把共享 Ray 集群管起来的平台工程师、负责大数据集群的 SRE还有被队友坑过的算法同学都能在里面找到对应的答案。1. 为什么共享集群必须做隔离先把痛点摆出来1.1 一个人霸占全员排队到底是怎么发生的很多团队把 Ray 集群搭起来之后第一反应都是先让同事用起来再说资源隔离这事儿往往排到后面。但我可以负责任地说只要集群超过 10 个节点、用户超过 3 个组用不了两周就会出乱子。问题一般来自三类典型操作。第一类是单个任务声明过大的资源。有些同学的习惯是反正机器多num_cpus直接写 32但他实际用到的只有两个核。问题在于 Ray 的调度器是看数字记账的不会去猜你的真实负载。一个 32 核的节点被这个任务占掉 32 核之后其他组的哪怕num_cpus1的小任务也只能排队等待看起来就是集群明明很空任务就是起不来。第二类是长驻 Actor 没有释放。训练服务或者模型热加载的 Actor 一旦挂在集群里除非显式ray.kill或者进程结束否则它占用的 CPU、GPU 会一直被 raylet 记在账上。我见过一个num_gpus1的 Actor 在集群里活了两周期间有三个组的任务一直在等这块 GPU。第三类是 object store 内存被打爆。Ray 的任务间通信和参数传递走的是共享内存对象存储object store默认会吃掉节点内存的相当大一部分。有人一次性往里面丢几十 GB 的大数组老对象会被强制驱逐正在读取这些对象的任务就会开始重试或者长时间等待。这三种情况叠加在一起最难受的不是慢而是不可预测——没人知道下一个任务到底要排多久。更严重的还有 Placement Group 互相等待导致的死锁式停顿后面我会专门讲。1.2 隔离需求怎么翻译成 Ray 的语言做隔离之前先要把目标拆清楚。我给共享集群定的目标是三个词故障爆炸半径可控、资源分配相对公平、用量可审计。故障爆炸半径可控某个用户 OOM 或者把 object store 打满不能拖垮整个 raylet更不能让其他组的 Driver 全部断连。资源分配相对公平关键是相对。每个团队至少要有一个保障性的资源下限同时也要有总上限防止一个人把集群吃光。用量可审计出了问题能回答这块 GPU 被谁占了、从什么时候开始占的。这三个目标翻译成 Ray 的术语分别对应四层手段namespace 做逻辑隔离max_resources 做单 Job 累计配额Placement Group 做团队级资源池Autoscaler 节点出口做集群总量控制。它们不是替代关系而是叠加关系。下面我从地基开始把这四层逐一讲透。2. 隔离的地基Ray 资源模型先讲清楚2.1 raylet 如何登记和分配资源想理解隔离先得理解 Ray 的资源记账机制。每个节点上有一个 raylet 进程节点一共有多少资源是在启动时登记的。手动ray start的话可以通过--num-cpus、--num-gpus、--memory和--resources指定默认情况下它会按机器实际配置自动探测。所有节点的资源情况和调度决策集中在 head 节点的 GCSGlobal Control Service里维护形成一个集群资源视图。当你提交一个任务或者创建 Actor 时调度器会在集群范围内找一个当前可用资源足够的节点然后由该节点的 raylet 把资源划拨出去。任务跑完资源归还Actor 不删资源就一直在账上。整个过程特别像酒店前台登记房间每个房间是否空闲、被谁占用前台都记得清清楚楚。但注意Ray 默认只是个记账系统不是限额系统如果不做任何限制任何用户都可以申请任意多的资源直到把集群压垮或者彼此饿死。这就是多用户场景必须自己做配额的根本原因。2.2 任务与 Actor 的资源声明差异Ray 里资源声明的核心参数是num_cpus、num_gpus、memory和自定义资源。这里有两个新手常踩的点。第一GPU 资源声明不代表显存管理。你写num_gpus1只表示我要占用一块 GPU 的调度名额Ray 不会帮你管显存更不会做类似 GPU 显存的隔离。如果两个任务被调度到同一块 GPU 上比如num_gpus0.5的比例调度显存冲突只能靠你自己的框架或者监控去发现。第二Task 和 Actor 的生命周期完全不同。普通 Task 是跑完即释放而 Actor 是常驻进程创建之后会一直占着资源直到显式删除或者进程崩溃。多用户集群里绝大多数持续性资源泄漏都是 Actor 引起的后面踩坑实录会专门讲。2.3 Placement Group 是怎么占坑的Placement Group 是 Ray 提供的一种资源打包占位机制。你可以把一组确定量的资源定义成 bundle比如{CPU: 8, memory: 32GB}就是一个 bundle然后把这些 bundle 按一定策略放到一个或多个节点上。常见策略有PACK尽量打包到同一节点、STRICT_PACK强制同一节点、SPREAD尽量分散和STRICT_SPREAD必须分散。为什么要提它因为多用户隔离里Placement Group 是团队资源池方案的基础——先占坑再让任务往坑里调度。但它的代价也很明显占着坑不用资源就浪费了而且多个用户同时等坑的时候可能出现 A 等 B、B 等 A 的死锁。所以它不是一个拿来就用的功能配套的生命周期管理必须跟上。3. 四层隔离方案怎么选场景、代价与对比3.1 namespace最容易被忽略的逻辑隔离Ray 的 namespace 是一个逻辑命名空间每个任务和 Actor 都在某个 namespace 下。同一 namespace 内的 Actor 可以互相直接调用不同 namespace 之间默认不可见、不能直接访问对象。如果你的用户直接ray.init()而不指定 namespace系统会分配一个默认 namespace所有人混在一起同名 Actor 冲突、对象互相覆盖的情况就很难避免。用 Ray Jobs API 提交任务时每个 Job 默认会跑在独立的私有 namespace 下这本身就是一道逻辑隔离。但请记住namespace 隔离的是名字空间和可见性它不限制任何物理资源。一个用户照样可以申请 100 个 CPU 把你的集群占满。所以 namespace 是必要但不充分的一层它主要解决起名冲突和误访问问题资源限制要靠下一层。3.2 Jobs API max_resources真正的用户级配额Ray 从 2.7 版本开始在 Jobs API 里支持了max_resources参数。它可以在 Ray Job 这一层直接给整个作业设置 CPU、GPU、内存的累计上限。举个实际例子如果给某用户的 Job 设置max_resources{CPU: 16, GPU: 2}那么这个 Job 内所有 Task 和 Actor 的 CPU/GPU 声明加在一起不能超过这个数超过的部分调度器会排队等待而不是无限抢占。这是目前做多用户隔离最实用的一层控制因为它的语义正好落在用户和作业上。相比让每个用户自觉约束自己的num_cpus直接把配额写进提交接口约束力强很多。它依赖的是 Ray 的吞吐调度器throughput scheduler来做限额记账细节我放在实操章节。3.3 Placement Group 预留给团队一个保底资源池配额能管上限但管不了保证。如果集群繁忙配额内的小任务也可能一直抢不到资源。这时候 Placement Group 的价值就出来了你可以提前为团队 A 预留一组 bundle相当于这几块资源已经被团队 A 预定了其他用户调度时会自动避开。打个比方max_resources 像是给每个人发了张最多能花这么多的卡Placement Group 则是直接给团队开一个专属包间。包间的缺点是空着也得付钱——预留资源在没跑任务时依然被占用集群整体利用率会下降。所以这个手段适合资源紧张、核心团队需要稳定保障的场景不适合所有用户都来一层。3.4 KubeRay 多集群隔离最彻底运维最重如果你已经在 Kubernetes 上跑 Ray还可以走更彻底的路线每个团队一个独立 RayCluster配合 K8s 的 ResourceQuota、LimitRange 做物理限额。这种方式隔离效果最好故障、网络、资源全部独立但代价是集群数量变多、运维复杂度成倍上升镜像、依赖、监控、日志都要跟着多份。我的判断是除非有强合规要求或者团队之间有严重的相互干扰否则不需要一上来就上多集群。大多数场景下一个共享集群加上 namespace、max_resources、Placement Group 三层就够了把 KubeRay 多集群当作战术性的最后手段而不是默认方案。3.5 四者的组合方式与选型表隔离手段隔离粒度是否限制总资源部署成本典型场景namespace逻辑命名空间否极低防止 Actor 同名冲突、对象误访问max_resourcesJob 级配额是低给每个用户/作业设 CPU、GPU、内存上限Placement Group团队级资源池是预留中核心团队保底资源、避免资源饿死KubeRay 多集群集群级是K8s 限额高强隔离、强合规、多环境故障隔离我们最终的方案是所有任务强制通过 Ray Jobs 提交每个用户在提交层配max_resources对两个核心算法组各建一个 Placement Group 做保底namespace 随 Job 自动隔离不需要额外干预多集群暂时不上留到规模再大一倍再说。4. 实操照着配置一套用户级资源隔离4.1 先确认版本和调度器第一步是确认 Ray 版本。max_resources是 2.7 引入的建议直接上 2.8 之后的版本调度器行为更稳定。还有一点容易被忽略max_resources依赖吞吐调度器throughput scheduler的资源限额机制。如果你还在用旧版本的调度器哪怕代码里写了max_resources也可能不生效。可以通过环境变量RAY_SCHEDULER_TASKS_USE_THROUGHPUT_SCHEDULER1显式开启2.8 开始这个调度器默认启用。启动 head 节点的时候我建议顺手确认两件事一是给节点起了稳定的名字方便 Dashboard 识别二是确认ray status -v能正常显示各节点的资源总量。这个命令后面排障时天天要用。4.2 用 JobSubmissionClient 给每个用户设总配额给用户开放集群的第一步是把提交入口统一收编到 Ray Jobs API不要让用户自己ray.init()。我写了一个简单的提交脚本交给各组使用from ray.job_submission import JobSubmissionClient client JobSubmissionClient(http://ray-head:8265) submission_id client.submit_job( entrypointpython train.py --config configs/base.yaml, runtime_env{ working_dir: /data/team_a/project, env_vars: {PYTHONPATH: .}, }, max_resources{ CPU: 16, GPU: 2, memory: 64 * 1024**3, # 单位是字节 }, ) print(submission_id)这里三个参数值得你留意。memory的单位是字节不是 GB写错会导致配额语义完全不对。max_resources限制的是整个 Job 内所有 Task 和 Actor 的累计声明值不是某个单任务的限制。另外如果用户的代码在 Job 内部又尝试自己连一个本地 Ray 集群那这份配额就管不到他了我在排障章节会教你怎么发现这种绕过。提交之后可以定期用client.get_job_status(submission_id)查状态。我们内部还做了一层封装每个用户绑定固定的配额模板比如算法组 A 默认 CPU 16 / GPU 2 / 内存 64GB避免用户自己乱填。4.3 用 Placement Group 做团队级资源池对于核心团队我会额外创建一个名为team-a-pool的 Placement Group。下面的代码示例演示了如何创建并等待它就绪import ray ray.init(addressray://ray-head:10001, namespaceteam-a) pg ray.util.placement_group( bundles[ {CPU: 8, memory: 32 * 1024**3}, {CPU: 8, memory: 32 * 1024**3}, {GPU: 4}, ], strategyPACK, nameteam-a-pool, ) ray.get(pg.ready())创建之后团队成员在装饰器里指定placement_group和placement_group_bundle_index任务就会被优先调度到这个资源池里ray.remote(num_cpus8, placement_grouppg, placement_group_bundle_index0) def train_worker(): ...用完一定要显式释放否则资源会一直被占着ray.util.remove_placement_group(pg)这里提醒一个常见问题多个团队同时创建多个STRICT_PACK或STRICT_SPREAD类型的 Placement Group在资源不够的节点上会出现互相等待也就是调度死锁。我们的做法是给每个 Placement Group 的ray.get(pg.ready())加上超时和告警一旦超过 5 分钟没就绪就会触发人工介入。4.4 Autoscaler 节点类型从集群出口控制总量最后一道闸门是 Autoscaler 层面的节点出口。如果用户提交的 Job 数量很多、每个 Job 配额都不小光靠 Job 级配额还不够因为集群总量是有限的。常规做法是把不同团队的资源分成不同的节点类型node type比如 CPU 节点池、GPU 节点池再分别设置节点数和资源总量。以 Ray Cluster Launcher 的配置为例你可以为团队 A 单独定义一个节点类型available_node_types: team_a_cpu: resources: CPU: 32 memory: 128000000000 node_config: InstanceType: c5.4xlarge team_a_gpu: resources: CPU: 16 GPU: 4 memory: 256000000000 node_config: InstanceType: g4dn.2xlarge max_workers: 10这样做的好处是即使某个用户代码里把num_cpus写得再大他最多也只能用到一个节点类型内的资源总量配合集群级max_workers就能把整个集群的出口规模卡死。如果你用 KubeRay逻辑类似只是把限制写在了 RayCluster 的 WorkerGroup 里minReplicas/maxReplicas。这一步不是给每个用户做精细隔离而是给集群封顶避免资源被无限扩出来。5. 隔离效果怎么看监控指标与排障链路5.1 Dashboard、ray status、ray memory 怎么配合看配完隔离不代表万事大吉你得能随时回答现在谁占了多少资源。我日常监控就靠三个工具。第一个是 Ray Dashboardhead 节点的 8265 端口打开 Jobs 页面可以直接看到每个 Job 的 CPU、GPU、内存使用曲线如果发现某个 Job 长期贴着max_resources上限跑基本能断定他在抢资源。第二个是命令行ray status -v可以看到每个节点的资源总量、已用、可用以及正在排队的 Task 数量ray memory则是专门看 object store 内存占用谁 put 了大对象一目了然。第三个是 Prometheus 指标。Ray 会暴露一套调度和资源相关的 metrics包括ray_resources、ray_placement_groups等。我们把指标接进了 Grafana专门做了一个各团队资源使用排行面板按 Job 的 metadata 打标聚合。有了这个面板再有人来问我的任务为什么排队可以直接甩图给他省去大量沟通成本。5.2 配了上限却不生效排查链路如果你设置了max_resources但发现用户作业还是能无限拿资源按下面的顺序排查基本能定位确认 Ray 版本在 2.7 以上。版本不到max_resources这个参数会被直接忽略。确认任务真的是通过 Ray Jobs API 提交的。很多人把 Jobs API 用成了只是套一层壳代码里还是ray.init(address...)那配额就不归这个 Job 管。看调度器日志里有没有吞吐调度器相关的类名或 token bucket 字样。如果日志显示还在用旧的调度队列就需要设置RAY_SCHEDULER_TASKS_USE_THROUGHPUT_SCHEDULER1后重启集群。检查用户代码里是否在 Job 内部又起了新的ray.init()本地集群。这种情况经常出现在 Notebook 用户身上他们习惯性地在代码开头ray.init()导致任务根本没跑到你的集群里。最后打开 Dashboard 的 Jobs 页面看那个 Job 的资源曲线是否到了你设的上限附近。如果曲线远超上限说明配额确实没生效如果刚好顶在上限实际上调度器已经帮他排队了表现是任务等待而不是失败。这套排查链路我们走了不止一次百分之九十的问题都出在第 2 和第 4 步也就是看似通过 Jobs API实际绕过了配额。5.3 一个伪报错别把 CDN 的 Ray ID 当成 Ray 框架报错做 Ray 相关排障时网上搜索会遇到一类奇怪的报错比如error 1033 ray id: a41b20262cb6b9aa。这里我必须提醒一句如果这个错误出现在浏览器页面或者某个 API 网关的响应体里那个大写的Ray ID是 CDN 产品Cloudflare 等用来追踪请求的标识符和开源的分布式计算框架 Ray 没有任何关系。我们团队就有人拿着这种报错来问我Ray 集群是不是出问题了查了半天才发现是网关层的缓存/范围问题。区分方法很简单Ray 框架本身的报错几乎都会出现在 Dashboard、日志或者 Python 的 traceback 里内容通常是Failed to allocate ...、ObjectStore ...、RayTaskError之类而 Ray ID 这种格式一般是一串十六进制字符串跟着CF-Ray或类似响应头出现。以后再看到这种报错先分清它是网关随手给你的追踪号还是Ray 调度器的真实错误。6. 踩坑实录多用户隔离最容易翻车的三个点6.1 用户声明资源远大于真实需求结果调度瘫痪有一个组的算法同学写num_cpus32并没有恶意只是沿用了以前单机多进程的习惯。但问题在于Ray 的调度器只看声明值不看真实负载。在 32 核节点上一个num_cpus32的任务会让其他所有任务在这个节点上完全无法调度哪怕这个任务实际只用了 2 个核。我们的对策分两层。第一层是技术兜底给每个用户的 Job 设置max_resources上限再差的声明也翻不出天第二层是使用规范在提交脚本里预检查num_cpus和节点规格的匹配关系超标的直接打回。效果比单纯开会宣讲好很多。6.2 Actor 泄漏导致 GPU 永久占坑这是多用户 GPU 集群最头疼的问题。某次训练任务因为异常退出了但它的训练 Actor 还活着继续霸占着num_gpus1。后续任务只能排队等这块不存在的已占用 GPU。更隐蔽的是 detached Actor它不依赖任何 Driver 存活Driver 退出了它还活着时间一长GPU 资源会一块一块消失。遇到这种情况先用ray list actors把所有 ALIVE 状态的 Actor 拉出来看 owner 和创建时间再用 dashboard 的 Actor 页面确认占用资源。治理上我们做了三件事一是规范 Actor 命名必须包含团队名和业务名二是在共享集群上默认禁止用户创建 detached Actor确需长期常驻的走申请流程三是写了一个巡检脚本每 20 分钟扫描一次超过 24 小时未更新的常驻 Actor自动告警。6.3 object store 内存被忽略任务忽快忽慢Job 级max_resources里配的 memory主要管的是任务和 Actor 的资源声明object store 是另一套账。用户一次put几十 GB 的对象会直接影响所有任务的对象访问速度副作用就是整个集群任务执行时间变得极不稳定。我们的处理方式是双管齐下。一是在 node 启动参数里控制单个节点的--object-store-memory防止它无限吃内存二是明确要求大数组不要直接进 object store优先写共享文件系统或者用ray.data做流式处理。这个习惯养成之后集群稳定性提升非常明显。6.4 一点个人经验如果让我重来一次我不会一上来就上多集群而是先做好两件事把所有提交入口统一到 Ray Jobs API并给每个用户配上max_resources再把 Dashboard 和 Prometheus 指标接好让资源使用透明化。这两件事加起来能解决共享集群 80% 的打架问题。剩下的 20%用 Placement Group 给核心团队保底、用 Autoscaler 封顶基本就够了。多集群隔离不是不能上而是要等业务规模和合规要求真正需要的时候再上否则运维成本会让你怀疑人生。另外任何技术配置都抵不过一份使用公约。我们和技术组长对齐了一份《共享 Ray 集群资源使用规范》里面只写了三条硬规则num_cpus必须按真实并行度填写、Actor 必须设置生命周期或申请常驻、超过 10GB 的对象不允许直接 put。这三条配合上面的技术隔离才是集群到现在还能稳定运行的真正原因。
返回列表