Ray整体架构
本文主要是对Ray Docs做的一点翻译
应用概念:
- Task:进程上执行的单个函数调用。可以是无状态的(function.remote()),也可以是有状态的(Class.remote(),即actor)。Task是通过.remote()调用,是异步执行的,会返回objectRefs(结果数据的引用)。
- Actor:有状态的worker进程,被@ray.remote()所装饰的类的实例。
- Driver:根程序,运行ray.init()的程序。
- Job:同一个Driver内的tasks, objects和actors的集合。
Ownership
每个worker进程管理并拥有它所提交的task和返回的objectRef(也就是结果数据不一定在执行函数的worker内,而是在提交task的worker内)
组件
Ray实例可以有多个worker节点,每个节点内部运行的进程如下:
- 一个或多个worker进程:用于提交和执行task,当然也可以执行actor的方法,前者worker进程是无状态,后者worker进程是有状态。每一个worker进程都会和一个具体的job绑定。默认初始化的worker进程数量等于CPU核心数量。每个worker会存储:
- ownership table:worker所引用的object的系统元数据(e.g. 引用数)
- in-process table:用于存储较小的object
- 一个raylet进程:raylet被同一集群上的所有job共享。raylet有两个线程:
- scheduler:负责分布式object store上的资源管理和满足task参数。集群中的各个scheduler共同构成了ray的distributed scheduler。
- shared-memory object store(即Plasma Object Store):负责存储和传输较大的object。集群中的各个object store共同构成了ray的distributed object store。
有一个特殊的worker节点也可以当作head节点,它除了开启上面的就进程,还会开启以下进程:
- **Global Control Store (GCS):**GCS是一个k/v存储服务器(用来存储系统级别的元数据,比如objects和actors的位置)。正在优化以使GCS运行多个节点上。
- **driver process:**driver是特殊的worker进程(执行最顶层应用的worker进程)。它可以提交task但不能执行。driver可以在任意节点上运行,但当运行了Ray autoscaler时是在head节点上
Task生命周期
task提交时,owner会等待依赖(比如task function的参数)。依赖可以在来自集群的任意节点。依赖一旦准备好,owner会从scheduler请求资源。资源如果可用,scheduler会同意请求,并会携带worker地址信息响应给owner,这个worker用来执行task。
owner通过gRPC将task发送给worker,worker执行完task后会存储结果:
- 如果结果很小(默认是小于100KiB),就会返回给owner,owner会存储到in-process table。
- 如果结果比较大,worker会将结果存储在它本地的Plasma Object Store,并告诉owner:object已经在分布式内存中了,允许owner引用。
如果task提交时,参数是一个objectRef,那在执行该task前必须先处理参数:
- 如果参数很小,直接从owner的in-process object store复制到task description,worker可以直接引用。
- 如果参数很大,那就必须从Plasma Object Store中取,scheduler会辅助object的传输:查找object的位置并在不同节点中复制。
Actor生命周期
actor存活时间和元数据都是存储在GCS中,每个actor客户端可以缓存这些元数据在本地,然后通过gRPC根据元数据将task发送给其它actor。
actor的创建:创建actor的worker进程首先会在GCS中同步注册acotr。一旦GCS响应,那么woker进程注册actor的过程就是异步的,这和function.remote()是一样的。如果创建actor的依赖都被满足,创建者就会将task规范(?)发送给GCS,GCS就会通过分布式调度协议(distributed scheduling protocol)调度actor creation task(类似于task的调度,GCS可以比作actor creation task的owner)。与此同时,Class.remote()会立刻返回actor handle。
因为actor在创建时已经调度了资源,在执行actor内的方法时,就不需要再通过scheduler调度资源
Task任务调度和Object存储的实例
以下面的代码为例:
@ray.remote
def A():
y_id = C.remote(B.remote())
y = ray.get(y_id)
task A会提交task B和task C,task C是依赖task B的结果Object作为参数的。这里假设B的结果为large object X,也就是采用plasma store(共享内存存储);而C的结果为small object Y,采用进程内存储(in-process table)。
task scheduling

Node1执行task A,那么B,C由它提交,Node1的ownership table就会保存B C的结果X Y的条目(并不是具体的值)。然后开始看task B的调度过程:
- worker1(Node1)会向local scheduler(in the raylet process) 请求资源执行B;
- local scheduler会对该请求响应(这里假设的是worker1本地资源不足),本地调度器告诉worker1再向Node2的调度器发起请求;
- worker1就会更新自己的ownership table表明B()由Node2执行;
- worker1向Node2的调度器(Scheduler 2)发起请求;
- Scheduler 2同意分配资源,向Node1返回Node2的地址。Scheduler 2会保证worker 1占用这些资源时,不会被分配给其它tasks;
- worker 1发送task B给worker 2执行。
task execution

这部分展示worker 2执行任务并返回给owner(worker 1)存储值的过程:
- worker 2执行完task B之后,会将X存储在自己的shared-memory object store(即Plasma Object Store),这是因为X为large;
- Node 2异步更新object table表示X再自己这(虚线箭头);
- Node 2会一直保存X知道Node 1通知它删除(图中没展示)。
- Worker 2响应给worker 1告诉它B已经执行完成了;
- Worker 1更新自己的ownership table中的Val为引用,表示X存储再共享内存中;
- Worker 1返回资源给scheduler 2(就是放弃占用的资源)。
Distributed task scheduling and argument resolution

B执行完了后,参数就准备好了。worker 1现在就要准备调度C
- 同调度task B
- 同调度task B
- 同调度task B
- 同调度task B
- scheduler 3看到task C需要参数X,但是本地没有X的副本,就会将task C放进队列,并向object table询问X的定位。

接上,该图是Node 3获取X的过程:
- Object table响应scheduler 3 请求,告诉它X在N2;
- scheduler 3向N2的object store请求X的副本;
- X被复制到N3;
- N3也会异步更新Object Table,表明X的副本也在N3上;
- N3的X会被缓存但不会保证不被删除。因为X现在有多个副本保存在各个节点上,N3如果内存有压力,可以根据LRU算法淘汰本地的X副本。
- N3现在满足了task C的依赖,可以分配资源给worker 1执行task C,返回N3的地址给N1;

- worker 1发送task C给worker 3执行;
- worker 3从本地object store获取X,执行C(X);
- worker 3执行完毕,返回Y给worker 1的ownership table
- 因为Y所占内存比较小,会存储在worker 1的in-process table中;并且ownership table会删除task C的描述和位置,因为该任务已经执行完毕。此时
y = ray.get(y_id)
就可以获得Y的值 - worker 1将N3的资源放弃。
Garbage collection

这里展示垃圾回收机制:
-
worker 1会擦除X,这是因为只有C对X有引用,此时C已经结束了。但是会保存Y,因为应用还在引用Y的ID;
最终所有X的副本都将删除。