Ray分布式计算入门:解构任务调度与机器学习应用场景

发布时间:2026/10/16 4:50:55
Ray分布式计算入门:解构任务调度与机器学习应用场景
如果你最近在技术社区、招聘网站或者组会汇报里频繁看到一个叫 Ray 的词那不需要怀疑分布式计算这个方向绕来绕去总归会绕到它头上。尤其是机器学习模型的分布式训练、并行数据处理、在线推理服务化这几条链路里Ray 已经是很多人默认的底座。我计划用一个连载系列把 Ray 讲透这篇是整个系列的起点也就是“00-前言与导读”先聊清楚 Ray 到底解决什么问题哪些人适合学前置知识要准备到什么程度以及整个系列十讲会怎么走。看完这篇你不需要写出任何代码但你会对这个框架的全貌、适用边界和后续学习路径有一个非常清楚的地图。我见过太多人学分布式框架是直接冲 API 去的结果看两三个晚上就放弃了。问题往往不是代码难而是没搞懂它到底设计出来是干嘛的、跟别的东西有什么区别、我的场景到底用不用得上。所以这一篇不讲语法先把我这几年的理解沉淀下来。文章最后也会带一节完整的安装和第一个示例保证你想动手的时候能立刻跑起来。1. 为什么讲 Ray它到底解决了什么问题1.1 从单机到分布式你能扛住多大的计算量先想一想一个朴素的问题你的 Python 程序处理数据单机跑不动了通常怎么办最直接的办法是多进程。multiprocessing.Pool用起来不算复杂把任务切成片段丢给多个 worker 执行确实能吃到多核红利。但再往前走一步就难受了任务之间如果有依赖怎么办要共享一个大对象怎么办程序跑在五台机器上而不是一台机器上任务怎么调度worker 挂了要不要重试每台机器的资源使用情况谁来看这些问题会像滚雪球一样冒出来。你可以自己用消息队列、数据库、共享文件系统把一套流程拼出来但那是真正的“分布式开发”而不是“用分布式能力去解决业务问题”。多数业务方只想做一件事就是把已经写好的 Python 逻辑尽可能并行地跑在多台机器上并且不要我操心底层调度、通信、失败恢复。Ray 就是在这样一个位置出现的它给了你一套足够简单的编程模型让你的普通 Python 函数能像单机调用一样自然地变成分布式任务。拿生活类比一下。单机程序就像一家小店自己炒菜、自己端盘、自己收银人少的时候没问题。客流量大了你最简单的做法是雇几个伙计每个人负责一张桌子这就是多进程。但万一客人要点跨桌拼单、后厨要统一备料、某个伙计请假了还要有人顶上你一个人管不过来这时候就需要一套标准的“餐厅管理体系”排班表、备料台账、职责分工。Ray 就是给 Python 程序提供的这套管理体系它替你把调度、通信、故障转移这些脏活全接走。1.2 Ray 的定位与优势不只是 MPI也不只是 MapReduce很多第一次接触 Ray 的人会把它归类到“大数据框架”里觉得它和老的 MapReduce、Spark 是一类东西。这个理解对了一半。它们确实都在解决分布式计算但抽象层次和侧重点完全不同。看一下 Ray 的官方表述它是一个分布式执行引擎而不是一个纯批处理框架。它的核心能力是让你能编排任意依赖关系的任务图而不是只有 Map 和 Reduce 两种固定模式。这意味着你可以拿它来跑机器学习训练、做强化学习环境并行、搭建在线推理服务、跑大规模数值仿真也能做传统的数据 ETL。只要你描述得清楚“每个函数在做什么”Ray 就有办法把这些函数调度起来。另外一个容易混淆的概念是 MPI。从底层机制上看MPI 面向的是高性能计算场景通信模型很底层程序员要自己设计进程拓扑和数据收发逻辑写起来像在盖房子一砖一瓦都要自己码。Ray 面向的是业务型分布式应用通信和调度都藏在里层用户面对的还是普通的 Python 函数和对象。你可以把 Ray 理解为“带着自动挡的 MPI”它牺牲了一部分极端性能可控性换来的是极高的开发效率。Ray 最显著的差异化优势有三个统一的编程接口分布式任务在写法上跟普通 Python 函数几乎没区别学习成本非常低。动态的任务图任务不是一次性静态规划好的你可以在运行过程中根据中间结果动态决定下一步调度谁这对机器学习训练和仿真类场景特别重要。有状态的计算通过 Actor 抽象你可以很方便地保存中间状态不用每次把状态写回数据库或文件。正因为这些特性Ray 在 AI 基础设施领域的渗透速度非常快。尤其是当你需要把训练、调参、数据处理、模型部署放在同一套分布式底座上的时候Ray 是少数能把这条链路完整串起来的方案。1.3 它适合哪些真实场景我不建议你用 Ray 来做的事情是单纯的 MapReduce 离线统计。如果数据量已经到了 TB 级且计算模式非常规整那些专门为批处理优化的引擎仍然有优势。但下面这些场景Ray 是真的顺手并行化 Python 计算比如几百个实验参数组合需要跑同一个函数每组合一次独立运行。Ray 可以把这些组合并行分布到多核或多机上。机器学习超参数搜索配合各种调参库把一组组超参丢给集群并行训练实时汇总结果。分布式强化学习仿真环境和策略训练解耦Ray 的 Actor 非常适合并行模拟环境。在线模型推理服务模型多、流量波动大需要动态扩缩容和模型版本管理Ray 的 Serve 模块专门做这个。大规模数据处理和特征工程它提供了一套类 Dataset 的接口能用 pandas 的直觉处理分布在不同节点上的数据分组。需要“脚本化编排”的复杂流水线多个步骤有依赖关系某些步骤内部又要并行Ray 的动态任务图能自然表达。一句话总结场景边界如果只是跑一个能塞进内存的 Python 函数并行化Ray 有点大材小用如果这个函数只靠单机怎么都跑不动或者你的系统里既有计算又有服务又需要分布式扩展Ray 就很对味。2. 本系列教程的目标读者与学习路径2.1 你需要会什么前置知识清单很多教程不敢写前置条件怕把人吓跑。但作为导读把话说清楚反而省时间。我自己判断学这个系列有三个层次的要求第一层是硬要求也就是不会就没法动手的内容。Python 基础语法必须过关函数、类、装饰器、with语法要能看懂。Ray 的编程模型大量依赖装饰器ray.remote这种东西你写的时候不一定要自己发明但至少要知道它大概干了什么。第二层是软要求会了更好不会也能跟。比如进程和线程的区别、GIL 的基本含义、内存和 CPU 的基本关系。你不需要真正写过并发程序但至少要知道“多线程在 Python 里经常会因为 GIL 跑不满 CPU”这句话在说什么。我们在分布式环境里谈并行很多概念跟操作系统的进程调度紧密相关。第三层是常识层用于判断场景而不是写代码。比如你知道训练一个模型需要 GPU、数据处理是 CPU 密集、服务上线要处理多个请求并发。这些属于工程直觉短期补不上但会随着项目经验慢慢积累。我不建议你在学 Ray 之前先去啃分布式系统理论。相反我建议顺序反过来先学会用 Ray 把一个简单的函数并行跑起来感受一下分布式调度是怎么回事再回头去读那些理论和底层设计。先会骑自行车再研究机械原理这样不会劝退自己。2.2 整体目录规划从 0 到 1 的十讲安排我把整个系列规划成十个章节每一章都尽量围绕“能跑起来的例子”展开而不是单纯堆概念。已经确定的大致路线如下第 00 讲前言与导读也就是你正在看的这篇负责建立全局认知。第 01 讲核心抽象之 Task讲远程函数、对象引用、并发控制这是 Ray 一切功能的基础。第 02 讲Actor 模型与状态管理讲如何在分布式环境里维护有状态的对象。第 03 讲任务依赖与并行模式讲怎么组合出有依赖关系的执行流以及常见的并行范式。第 04 讲Ray Data 数据处理讲怎么用 DataFrame 直觉处理分布式数据集。第 05 讲Ray Train 分布式训练讲数据并行训练的原理和实现。第 06 讲Ray Serve 模型服务化讲怎么把模型发布成 HTTP 接口并弹性扩缩容。第 07 讲集群部署与资源管理讲从单机到多机的部署方式、资源配额。第 08 讲任务调试与性能分析讲日志、仪表盘、瓶颈定位相关的方法。第 09 讲生产环境实战复盘从项目初期踩坑到最终落地的完整过程。整个路线的设计逻辑是先解决“怎么写分布式代码”的问题再解决“怎么跑在更大规模的集群上”的问题最后解决“怎么把系统稳定跑在真实环境”的问题。前三讲是地基中间四讲是各个子模块的使用最后三讲是升维和实战。2.3 每讲的难度与产出预期我按难度把十讲分成了三个梯队。第一梯队是第 01 到第 03 讲偏基础概念难度一颗星你需要做到的是能独立写出并运行并行函数。如果你学完这三讲实际上你已经能解决大量常规的 Python 并行需求了很多“用 Ray 加速程序”的场景到这一步就够用。第二梯队是第 04 到第 06 讲进入机器学习领域难度三颗星。每讲结束你应当完成一个小型 Demo比如用 Ray Data 处理一份几十万行的日志用 Ray Train 训练一个简单的分类模型用 Ray Serve 把训练好的模型封装成 API。第三梯队是第 07 到第 09 讲面向部署和调优难度四颗星。这一梯队的产出不只是一个 Demo而是一套能在多台机器上稳定运行的集群和一套排查问题的思路。对普通学习者来说这一部分可以作为工具书按需查阅不要求一次啃完。我想特别提醒的是不要试图从第 07 讲倒着学。我见过不少同学直接问“怎么搭一个 Ray 集群”却写不出第一个远程函数。门都还没摸到就想把整栋楼盖起来这是分布式学习里最大的坑。3. 先建立核心概念地图Ray 的四个支柱3.1 Task把函数变成分布式可调度单元学 Ray 之前你只需要记住一个最核心的操作给你要并行执行的函数加一个装饰器它就从“普通函数”变成“可被 Ray 调度的任务”。这个函数在 Ray 的世界里叫 Task。普通函数是你在本进程里同步调用的调用方要等它返回结果。加了ray.remote之后你不再直接调用函数而是用.remote()方法提交它。这一步提交操作不会真的执行逻辑而是立刻返回一个“凭据”Ray 内部叫 ObjectRef。之后你可以继续进行其他计算等真正需要结果的时候再调用ray.get去取。这个“先拿凭据、后取结果”的设计是整个框架的灵魂。因为调用方不用阻塞等待所以你可以一口气提交几百个独立任务让调度器自己决定怎么排队、怎么分配资源。你甚至可以先把某个任务的结果当作另一个任务的参数Ray 会自动把依赖关系理顺再决定执行顺序。对入门来说Task 是你需要花最多时间体会的概念。它解决的其实是“把一个大程序切碎成可以独立运行的小块并安全地拼回来”这件事。切碎靠装饰器拼回来靠 ObjectRef。3.2 Actor有状态的远程对象Task 有个天然限制每次执行都是无状态的。你调用两次函数它们之间不共享任何东西所有中间数据要么存在内存对象里要么写到外部存储没有“留在进程内部”这种说法。但很多场景要求状态是持续存在的。比如一个模型推理服务希望模型常驻在某个进程里每个请求过来直接调用模型对象的方法而不是每次请求都重新加载一遍模型。再比如一个模拟环境的对象它内部维护着当前状态每一步动作都在这个状态上迭代。Ray 的 Actor 就是用来表达这种需求的。给一个类加上ray.remote实例化的时候就创建了一个远程 Actor你可以在不同任务里反复调用这个 Actor 的方法它的内部属性会一直保留在创建它的那个进程里。用生活化的方式理解Task 像你点外卖每次下单都是独立的商家不会记得你昨天吃了什么。Actor 像你雇了一个固定管家他有自己的冰箱和记事本你每次吩咐他办事他都能结合之前的情况来应对。3.3 Object Store跨进程共享数据的内存层Task 和 Actor 解决了计算单元的隔离但计算过程中必然要传输数据。Ray 设计了一个分布式对象存储层用来在多个进程之间共享对象这个存储层在入门阶段你不太需要感知它的存在但必须理解它的几个直觉规则。你用ray.put可以把一个 Python 对象放进对象存储得到 ObjectRef这个引用可以被传递到任意远程函数用ray.get可以把对象重新取回本地。理论上对象在内存中是共享的但如果一个很大的对象同时被很多任务需要Ray 会自动按需复制分发而不是每次都在网络上传输一份拷贝。关于对象存储入门阶段你先记住两句话第一小对象直接在函数间传递没问题第二大对象反复传递可能会成为瓶颈这时候需要显式用ray.put把它提前放好让各任务通过引用获取。这个优化在后面的分布式训练章节里会大量用到现在只要有个印象。3.4 Cluster从单机到多机的扩展方式Ray 的编程接口在单机和多机上几乎是同一套这是它设计上非常聪明的一点。你在一台电脑上写的代码迁移到集群上通常只需要把初始化方式从默认的本地模式改成连接已有集群的模式。在单机环境下ray.init()会创建一个本地版的“假集群”把本机的 CPU、内存自动划分成资源池多个 worker 进程共用这些资源。这个模式对学习和调试非常友好因为所有仪表盘、调度逻辑都能正常工作只是规模被限制在一台机器上。在多机环境下你需要在一台主节点上执行启动命令创建集群然后在其他机器上执行加入命令把这些节点的资源注册进来。之后你的远程函数就有机会被调度到这些节点上执行了。你不需要修改业务代码这背后是 Ray 的分布式调度器在自动协商。对于导读来说你只需要知道“单机能写代码多机是同一个代码换个环境跑”这个事实就够了。真正部署时遇到的地址、端口、防火墙、资源可见性等细节会在第 07 讲专门展开。4. 环境准备与第一次运行把 Ray 装起来4.1 安装与版本选择先解决能不能跑的问题。安装 Ray 比你想象的简单它就是一个标准的 Python 包用 pip 安装即可。我建议你在正式使用前先建一个独立的虚拟环境避免给系统 Python 环境制造一堆依赖冲突。推荐用 Python 3.9 以上的版本这是目前体验最稳定的区间。执行下面这条命令pip install -U ray-U的作用是升级到最新版本。如果你只是想先体验最新版没问题。但如果你要在生产环境用我建议固定大版本号。比如pip install ray[default]2.9.0方括号里的default表示安装默认依赖组这样能确保一些附加组件如仪表盘、客户端库都装好。不同的子模块还有不同的扩展依赖比如ray[train]、ray[serve]、ray[data]这些在后续章节用到时再安装也可以现在不用一次装齐。一个常见的疑问是“我机器上没 GPU 能不能学”。答案是完全可以。这个系列的入门部分绝大部分都是 CPU 计算等你跑到分布式训练那一讲如果实在没有 GPU也可以用小模型把流程跑通关键是理解数据并行和任务调度的机制而不是硬啃算力。4.2 多节点集群的搭建思路前期学习阶段你完全不用急着搭多机集群。我先说清楚集群的结构再告诉你最简单的启动方式。一个 Ray 集群由两类节点组成一个主节点和任意数量的工作节点。主节点负责运行调度器同时维护全局状态和对象存储元数据工作节点负责实际执行你提交的远程函数。主节点启动时会占用一个 Redis 兼容的地址和端口其他节点拿到这个地址后加入就算成为集群的一部分。在本地机器上模拟集群很简单打开终端输入ray start --head --port6379然后你可以通过某个特定端口查看仪表盘直观看到当前节点的 CPU、内存使用情况以及任务的调度过程。要停止本地集群执行ray stop这里有个很容易踩的细节很多人以为必须先执行ray start才能跑 Python 脚本其实不是。你在脚本里直接调用ray.init()Ray 会自动启动一个临时本地集群这个模式对写代码和调试最方便因为不需要额外维护后台进程。当你需要模拟真实集群时再考虑先启动集群再连接。4.3 运行第一个分布式函数来点能直接上手的。新建一个 Python 文件比如first_ray.py然后写入以下内容import ray ray.init() ray.remote def say_hello(name): return fHello, {name}! Im running on {ray.get_runtime_context().get_node_id()} futures [say_hello.remote(ffriend-{i}) for i in range(6)] results ray.get(futures) for r in results: print(r) ray.shutdown()这段代码干了几件事初始化 Ray 本地集群定义一个远程函数连续提交 6 个任务用ray.get一次性等所有结果返回并打印。每个任务输出的节点 ID 在单机环境下都是一样的但它会让你真实感受到每个函数都运行在独立的 worker 进程里。运行方式python first_ray.py如果你能看到 6 行打印输出说明 Ray 环境完全正常。到这一步你已经正式跑通了自己的第一个 Ray 分布式程序。这 6 个任务是否真的并行执行取决于你机器有几个 CPU 核以及集群可用的资源量。你可以在ray.init()里传一个参数num_cpus4来限制本地集群可用核数其他没被用到的核不影响你的其他程序运行。5. 入门阶段最容易踩的五个坑5.1 序列化与闭包带来的“幽灵变量”分布式环境最大的特点就是进程隔离。你在本地定义的全局变量、环境对象、数据库连接不会自动传送给远程函数。Ray 在提交任务时会对函数和参数做序列化如果某个对象无法被序列化任务会直接抛错。我见过最多的情况是项目里有人用了自定义类里面包了一个数据库连接或者线程锁。作为函数参数传进远程任务时就报PicklingError。解决办法不是强行修序列化而是重新设计代码结构把连接对象的创建放到任务内部去完成或者通过 Actor 来持有这种无法序列化的资源。另一个隐蔽问题是闭包。你在函数内部引用了一个外部变量Ray 只能序列化函数定义无法保证外部变量在你想象中的“最新状态”。所以写远程函数的时候尽量把需要的数据都通过参数显式传入。5.2 误以为远程函数天然共享状态有人写完第一个远程函数后发现函数里面读了某个全局列表两次调用结果不一样。这是因为每次任务都是在一个全新的进程中执行的全局变量在每次调用时都会被重新初始化你期望里的“共享”根本不存在。正确做法分两种如果共享的数据是只读的比如一个大的配置字典可以在提交任务前用ray.put放进对象存储再把 ObjectRef 传给所有任务如果共享的数据需要被修改比如一个计数器那应该用 Actor 来承载它让多个任务通过调用同一个 Actor 的方法来修改状态。刚入门时把这两个方向记清楚后面很多并发问题就不会发生。5.3 初始化时机不对导致的神秘超时Ray 的初始化是一个重量级操作不只在你本地启动进程还要建立调度器和对象存储。我看到有人把ray.init()放在每个远程函数内部结果每次任务提交都尝试重新初始化一个集群轻则报错重则把机器搞卡。正确的初始化位置应该是在 driver 进程也就是你运行主脚本的进程中执行一次并且只执行一次。后续提交的所有任务都会复用这个集群。获取完结果后再按需调用ray.shutdown()。在脚本退出前不显式关闭也能接受因为进程退出后相关资源会被系统回收但写代码养成好习惯显式关闭总没错。5.4 大规模任务一次性全部提交ray.get一次获取大量结果的开销可能比你想象中大。如果你在循环里放ray.get那是同步等待毫无并行可言。正确做法是先生成所有 ObjectRef在全部提交完成后再统一ray.get。不过一次性提交几十万个任务也不是好主意调度器和对象存储都有压力。更合适的方式是使用分块处理或者用 Ray 提供的一些高级接口比如ray.util.iter来分批调度。入门阶段你只要记住一个总结尽量批量化提交批量获取结果别让同步等待卡死并行度。5.5 本地模式与集群模式的行为差异本地开发时ray.init()默认使用的资源可能是你本机所有 CPU这会导致你机器上的其他程序变慢。调试时最好显式指定资源数量ray.init(num_cpus2)这样本地集群最多只用两个核其他程序不受影响。另一个差异是网络的可见性。本地模式下所有 worker 进程只管跟本机调度器通信感受不到网络拓扑到了真实集群机器之间会出现延迟、断连、节点宕机等情况。你在本地调通的代码并不代表集群上也一定稳定。所以越早接触真实多节点环境越能尽早发现这类问题。我在实际使用中最深的体会是Ray 入门最大的障碍不是 API 本身而是对你现有代码的“分布式改造思路”。很多同学总想着找一个函数库把原来大段的处理逻辑一股脑丢进去就能自动并行这是不现实的。真正高效的做法是先画清楚逻辑和数据流哪些步骤可以独立执行哪些状态必须共享哪些数据需要频繁传递然后再用 Task 和 Actor 把图画出来。我见过最成功的上手方式都是先拿一个非常小的真实任务比如批量处理几百个文件完整跑一遍调度、查看仪表盘、观察资源占用这一套流程走下来Ray 的核心感觉基本就有了。后续我不知道你会用这个系列走多远但如果你能坚持在前三讲里把每一个示例都亲手跑出来这个框架的底子就算打扎实了。