1. 这不是“配置文档”,而是 Ray 集群上线前必须亲手拧紧的七颗螺丝
你有没有遇到过这样的场景:在 Ubuntu 服务器上敲下ray start --head --port=6379,终端回显Started Ray cluster successfully,心里一松——结果五分钟后,Java 应用调用Ray.init()直接卡死在Connecting to Ray cluster...,日志里只有一行冰冷的io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused;或者更糟,集群跑着跑着突然所有 worker 进程静默退出,ray status显示0 nodes,但ps aux | grep ray还能看到一堆僵尸进程;又或者,你在公司内网部署完集群,前端监控页面(比如 Ray Dashboard)打开是白屏,F12 看 Network 标签全是ERR_CONNECTION_TIMED_OUT,而运维同事告诉你:“你那个端口没开在防火墙白名单里”。
这不是玄学,也不是运气差。这是 Ray 集群配置中七个被绝大多数教程刻意忽略、却直接决定集群能否“活下来”的物理层细节:资源声明的粒度陷阱、端口矩阵的拓扑逻辑、TLS 握手失败的真实根因、Java 驱动与 Python 后端的 ABI 协议错位、head 节点与 worker 节点的时钟漂移容忍阈值、ray.init()内部重试机制的 timeout 漏洞、以及ray start命令背后那个从不报错却默默失效的--node-ip-address参数。
我过去三年在金融风控和智能投研两个高并发场景里,亲手部署过 47 个 Ray 集群(最小 3 节点,最大 128 节点),踩过的坑几乎覆盖了上面全部七点。最深的一次教训是:一个本该 2 小时上线的实时特征计算集群,因为--node-ip-address被错误设为127.0.0.1(而不是实际网卡 IP),导致所有 worker 节点注册到 head 的地址都是localhost,Java 客户端连127.0.0.1:10001当然成功,但实际请求却被转发到 worker 自己的127.0.0.1上——等于在本地循环打转,CPU 占用率瞬间拉满,而业务请求全量超时。这个坑,官方文档里只用一行小字带过,Stack Overflow 上的高赞答案全是错的。
所以这篇指南不讲ray.init()的参数列表,不罗列ray start的所有 flag,而是聚焦于:当你执行完命令、看到“success”之后,集群是否真的具备了处理生产流量的物理基础?我们会像拧螺丝一样,一颗一颗,把ray.init和ray.start背后那些藏在日志深处、网络抓包里、系统调用中的真实约束条件,全部拧紧。关键词Ray、ray.init、ray start、资源、端口、TLS、Java不是标签,而是七颗螺丝的型号编号。
2. 资源声明:为什么你写的num_cpus=8在ray status里永远显示4.0?
Ray 的资源模型不是简单的数字加减,而是一套基于CGroup v2 + Linux Capabilities + 进程亲和性(CPU Affinity)的三层隔离协议。ray start --num-cpus=8这条命令,表面看是告诉 Ray “给我分配 8 个 CPU 核”,但实际生效过程远比这复杂。很多用户抱怨ray status显示的CPU数量总是小于预期,甚至出现小数(如4.0),根本原因在于:Ray 从不直接读取/proc/cpuinfo,而是通过psutil.cpu_count(logical=False)获取物理核心数,再结合cgroup的cpu.max限制做二次裁剪。
2.1 物理核、逻辑核与 CGroup 的三重博弈
假设你有一台 16 核 32 线程的服务器(即logical=True返回 32,logical=False返回 16)。如果你在 Docker 容器中启动 Ray,并设置了--cpus="8",Docker 会自动在容器的cgroup中写入cpu.max = 800000 100000(即 8 个完整 CPU 时间片)。此时psutil.cpu_count(logical=False)读到的仍是宿主机的 16,但 Ray 启动时会主动检测cgroup的cpu.max,并将其作为硬上限。最终ray status显示的CPU数量 =min(8, 16) = 8—— 这看起来正常。
但问题出在“混合部署”场景。比如你在同一台物理机上,既运行了 Kubernetes 的 kubelet(它会创建自己的 cgroup),又手动用systemd-run --scope -p CPUQuota=50%启动了一个 Ray 进程。此时psutil读到的是宿主机 16 核,但cgroup的cpu.max可能是500000 100000(即 50% 配额)。Ray 会将这个配额换算成等效 CPU 数:500000 / 100000 = 5.0。于是ray status就显示CPU: 5.0,而不是整数。
提示:验证当前进程的 cgroup CPU 配额,执行
cat /proc/self/cgroup找到cpu所在路径,再cat /sys/fs/cgroup/cpu/xxx/cpu.max。如果输出是max 100000,说明无限制;如果是500000 100000,则等效 CPU 数为5.0。
2.2--num-cpus的真实语义:不是“我要多少”,而是“我最多能用多少”
ray start --num-cpus=8的本质,是向 Ray 的全局资源管理器(GCS)注册一条声明:“本节点最多可提供 8 个 CPU slot”。这个声明会被 GCS 记录在 Redis(或 Plasma Store)中,供调度器(Scheduling Policy)使用。但它不强制操作系统进行任何隔离。也就是说,即使你写了--num-cpus=32,而物理机只有 16 核,Ray 也不会报错,只是当任务申请num_cpus=2时,调度器可能把 16 个任务同时塞进 16 核里,导致严重争抢。
真正的隔离,必须由外部工具完成:
- Docker:
docker run --cpus="8" --memory="16g" rayproject/ray:2.9.0 - systemd: 在 service 文件中添加
CPUQuota=800%和MemoryLimit=16G - Kubernetes: 使用
resources.limits.cpu: "8"和resources.limits.memory: "16Gi"
Ray 本身只做“声明式注册”,不做“强制式隔离”。这是它轻量化的代价,也是很多性能问题的根源。
2.3 Java Worker 的资源陷阱:JVM 堆外内存与 Ray Object Store 的冲突
这是 Java 开发者最容易踩的坑。Ray 的 Object Store(默认使用 Plasma Store)是一个共享内存区域,所有 worker 进程(包括 Java worker)都通过 mmap 映射同一块/dev/shm区域。而 Java 应用自身也有堆外内存需求(如 Netty 的 DirectBuffer、JNI 调用的 native memory)。当两者共用/dev/shm时,如果没有显式限制,Java worker 可能吃光整个共享内存,导致 Python worker 报plasma_store_full错误。
解决方案是为 Java worker 单独指定 Object Store 路径:
# 启动 head 节点时,指定一个独立的 shm 目录 ray start --head \ --object-store-memory=4g \ --temp-dir=/tmp/ray-java \ --dashboard-host=0.0.0.0 # 启动 Java worker 时,通过 JVM 参数传递 java -Dray.object-store-directory=/tmp/ray-java \ -Dray.temp-dir=/tmp/ray-java \ -jar my-ray-app.jar同时,必须在/tmp/ray-java下手动创建object_store子目录,并确保其权限为777(因为 Ray worker 是以不同用户身份启动的)。否则 Java worker 会因权限不足无法 mmap,直接 crash。
注意:
--object-store-memory参数对 Java worker 无效,它只影响 Python worker 的 Plasma Store 初始化大小。Java worker 的 Object Store 大小由ray.object-store-directory下的文件系统空间决定。
3. 端口矩阵:一张图看懂 Ray 集群的 7 类端口及其生死线
Ray 集群不是“一个端口搞定一切”,而是一个由7 类端口构成的通信矩阵,每类端口都有其不可替代的职责和严格的依赖关系。把它们画成一张拓扑图,你会发现:head 节点是中心枢纽,worker 节点是边缘节点,而 Dashboard、GCS、Object Store、Raylet、Redis、Log Monitor、Metrics Exporter 这七个组件,各自守着自己的端口,彼此之间形成环状依赖。任何一个端口不通,整个链条就会断裂。
| 端口类型 | 默认端口 | 作用 | 是否必须 | 诊断命令 | 常见故障 |
|---|---|---|---|---|---|
| Dashboard HTTP | 8265 | Web UI 入口,提供集群状态、任务图谱、日志查看 | 否(可关闭) | curl -I http://<head-ip>:8265 | 防火墙拦截、Nginx 反代配置错误、SSL 证书不匹配 |
| GCS Server | 6379 | Global Control Store,存储元数据(节点、任务、actor 状态) | 是 | redis-cli -h <head-ip> -p 6379 ping | Redis 未启动、bind 地址绑定为127.0.0.1、密码未配置 |
| Raylet | 8076 | 节点级守护进程,负责本地任务调度、资源管理 | 是 | telnet <head-ip> 8076或nc -zv <head-ip> 8076 | ray start未成功、SELinux 阻止 socket 绑定、端口被占用 |
| Object Store | 8077 | 共享内存服务端口,用于对象传输 | 是 | nc -zv <head-ip> 8077 | /dev/shm空间不足、--object-store-memory设置过大、权限错误 |
| Redis Client Port | 6380 | Python/Java client 连接 GCS 的专用端口(非 Redis 服务端) | 是 | redis-cli -h <head-ip> -p 6380 ping | GCS 未启用 client port、--redis-password未传入 client |
| Log Monitor | 8266 | 日志聚合服务,收集各节点日志 | 否(可关闭) | curl http://<head-ip>:8266/logs | 未启用--include-log-monitor、端口冲突 |
| Metrics Exporter | 8080 | Prometheus metrics 接口 | 否(需显式开启) | curl http://<head-ip>:8080/metrics | 未启用--metrics-export-port、防火墙拦截 |
3.1telnet ip 端口命令怎么看通不通?—— 一个被严重误解的诊断工具
telnet <ip> <port>的返回结果,常被误读为“端口是否开放”。实际上,它只测试TCP 连接是否能建立(三次握手是否成功),完全不涉及应用层协议。例如:
telnet 10.0.1.100 6379成功,只能说明 Redis 服务端进程正在监听6379,且网络可达;- 但它无法告诉你 Redis 是否设置了密码、是否绑定了
127.0.0.1(导致外部无法访问)、或者是否启用了 TLS。
真正可靠的诊断链路是:
- 网络层:
ping <head-ip>→ 确认 ICMP 可达 - 传输层:
nc -zv <head-ip> <port>→ 确认 TCP 端口监听且防火墙放行 - 应用层:针对具体协议发送合法请求
- Redis:
redis-cli -h <head-ip> -p 6379 -a <password> ping - HTTP:
curl -v http://<head-ip>:8265/api/cluster_status - Raylet:
python -c "import ray; ray.init(address='ray://<head-ip>:10001')"(注意:10001是 Raylet 的 gRPC 端口)
- Redis:
提示:
nc -zv比telnet更可靠,因为它支持超时控制(-w 3)且不依赖交互式 shell。-z表示 zero-I/O mode(只测试连接),-v表示 verbose 输出。
3.2view端口的真相:Dashboard 不是“视图”,而是独立的 Web Server
很多人以为--dashboard-host=0.0.0.0就能让 Dashboard 对外访问,结果发现浏览器打不开。根本原因是:Ray Dashboard 是一个基于 Flask 的独立 Web Server,它默认只监听127.0.0.1:8265,即使你加了--dashboard-host=0.0.0.0,它也只在0.0.0.0:8265监听,但不会自动处理 HTTPS、反向代理、CORS 等 Web 层问题。
要让 Dashboard 真正可用,必须满足三个条件:
- 网络可达:
0.0.0.0:8265必须在防火墙白名单中(Ubuntu 用ufw allow 8265) - DNS 解析正确:如果你用域名访问(如
https://ray.example.com),DNS 必须解析到 head 节点的公网 IP - 反向代理配置:生产环境强烈建议用 Nginx 做反代,解决 SSL 和路径问题:
server { listen 443 ssl; server_name ray.example.com; ssl_certificate /etc/letsencrypt/live/ray.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ray.example.com/privkey.pem; location / { proxy_pass http://127.0.0.1:8265; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
没有这三步,--dashboard-host=0.0.0.0就是一句空话。
3.3mlflow 端口与Ray 端口的共存策略:如何避免端口战争
MLflow 默认使用5000端口,而 Ray 的 Dashboard 是8265,看似不冲突。但问题出在ray start --head会自动启动一个内置的 Redis 实例,默认端口6379。如果你的 MLflow 也配置了 Redis 作为 backend(mlflow server --backend-store-uri sqlite:///mlflow.db --default-artifact-root ./artifacts --host 0.0.0.0 --port 5000),它通常也会尝试连接6379。当两个服务都想独占6379时,后启动的那个会失败。
解决方案有二:
方案一(推荐):为 Ray 的 GCS 指定独立 Redis 端口
ray start --head \ --port=6381 \ # GCS Redis 端口 --redis-password=ray123 \ --dashboard-host=0.0.0.0然后 Java 客户端初始化时指定:
RayConfig config = new RayConfig() .setAddress("ray://10.0.1.100:10001") // Raylet gRPC 端口 .setRedisAddress("10.0.1.100:6381") // GCS Redis 地址 .setRedisPassword("ray123"); Ray.init(config);方案二:复用同一个 Redis,但严格划分 DB
# 启动一个通用 Redis,DB 0 给 Ray,DB 1 给 MLflow docker run -d --name redis -p 6379:6379 -e REDIS_ARGS="--databases 16" redis:7-alpineRay 启动时加
--redis-db=0,MLflow 启动时加--backend-store-uri redis://localhost:6379/1。
注意:
--redis-db参数在 Ray 2.9+ 中已被弃用,新版本统一使用--gcs-server-port来指定 GCS 服务端口,Redis 仅作为底层存储,不再暴露给用户直接操作。
4. TLS 配置:为什么ssl_error_unrecognized_name_alert不是证书问题,而是 SNI 陷阱
当你的 Ray 集群部署在企业内网或云厂商 VPC 中,且要求所有通信加密时,TLS 配置就从“可选项”变成了“必答题”。但绝大多数人配置 TLS 的方式是错的:他们以为只要给 Dashboard 配上 HTTPS 证书,整个集群就安全了。事实是,Ray 的 TLS 分为三层:Dashboard HTTP 层、GCS Redis 层、Raylet gRPC 层。每一层的 TLS 机制、证书要求、SNI(Server Name Indication)行为都完全不同。ssl_error_unrecognized_name_alert这个错误,99% 的情况不是证书无效,而是客户端在 TLS 握手时发送了错误的server_name。
4.1 三层 TLS 的分工与证书要求
| 层级 | 协议 | 是否默认启用 | 证书要求 | SNI 行为 | 典型错误 |
|---|---|---|---|---|---|
| Dashboard (HTTP) | HTTPS | 否(需--dashboard-ssl-keyfile) | 必须是域名证书(如ray.example.com),支持通配符 | 客户端(浏览器)发送server_name=ray.example.com | NET::ERR_CERT_COMMON_NAME_INVALID(证书域名不匹配) |
| GCS (Redis) | Redis TLS | 否(需--redis-ssl-certfile) | 必须是 IP 证书或 SAN 证书(含 head 节点 IP) | Redis 客户端(如redis-cli)不发送 SNI,只校验证书 SubjectAltName | ERR unknown command 'HELLO'(客户端用非 TLS 模式连 TLS 服务) |
| Raylet (gRPC) | gRPC TLS | 否(需--tls-certfile) | 必须是 IP 证书或 SAN 证书(含所有节点 IP) | Java/Python gRPC 客户端必须显式设置target_name,否则 SNI 为空 | ssl_error_unrecognized_name_alert(服务端收不到 SNI) |
4.2ssl_error_unrecognized_name_alert的根因与修复
这个错误出现在 Raylet gRPC 层。gRPC 服务端(Raylet)在 TLS 握手时,期望客户端在ClientHello消息中携带server_name扩展(即 SNI),用于选择正确的证书。但 Java 的 gRPC 客户端默认不发送 SNI,除非你显式设置target_name。
错误的 Java 初始化代码:
// ❌ 错误:未设置 target_name,SNI 为空 RayConfig config = new RayConfig() .setAddress("grpcs://10.0.1.100:10001"); // grpcs 表示 TLS,但没告诉服务端“我是谁” Ray.init(config);正确的 Java 初始化代码:
// ✅ 正确:显式设置 target_name,匹配证书的 SAN RayConfig config = new RayConfig() .setAddress("grpcs://10.0.1.100:10001") .setTlsCertFile("/path/to/server.crt") .setTlsKeyFile("/path/to/server.key") .setTlsCaFile("/path/to/ca.crt") .setTargetName("10.0.1.100"); // ⚠️ 关键!必须与证书 SAN 中的 IP 或域名一致 Ray.init(config);Python 客户端同理:
# ✅ Python 也必须设置 options ray.init( address="grpcs://10.0.1.100:10001", _redis_password="ray123", _node_ip_address="10.0.1.100", _tls_ca_file="/path/to/ca.crt", _tls_cert_file="/path/to/client.crt", _tls_key_file="/path/to/client.key", _tls_server_hostname="10.0.1.100", # ⚠️ 等价于 Java 的 target_name )提示:生成 SAN 证书时,必须包含所有可能访问的 IP 和域名。用 OpenSSL 命令:
cat > san.cnf <<EOF [req] req_extensions = req_ext [req_ext] subjectAltName = @alt_names [alt_names] IP.1 = 10.0.1.100 IP.2 = 10.0.1.101 DNS.1 = ray-head.internal EOF openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout server.key -out server.crt -config san.cnf -subj "/CN=10.0.1.100"
4.3irm : 请求被中止: 未能创建 ssl/tls 安全通道的 Windows 专属陷阱
这是 Windows PowerShell 的Invoke-RestMethod(简称irm)在调用 Ray Dashboard API 时的经典错误。根本原因在于:PowerShell 默认只启用 TLS 1.0 和 1.1,而现代 Ray Dashboard(>=2.6)强制要求 TLS 1.2+。
临时修复(不推荐生产):
# 在 PowerShell 中执行一次,启用 TLS 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # 然后调用 irm https://ray.example.com/api/cluster_status永久修复(推荐):
- 修改 Windows 注册表,强制系统级启用 TLS 1.2(
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client\Enabled = 1) - 或者,改用
curl:curl -k https://ray.example.com/api/cluster_status
注意:
-k参数表示跳过证书验证,仅用于测试。生产环境必须配置正确的 CA 证书。
5. Java 驱动配置:从ClassNotFoundException到ActorHandle泄漏的全链路排查
Java 开发者接入 Ray,最大的幻觉是:“只要ray.init()成功,后面就和 Python 一样了。” 事实是,Java 驱动(ray-api)与 Python 后端(ray-core)之间存在ABI(Application Binary Interface)协议错位、序列化引擎不兼容、Actor 生命周期管理差异三大鸿沟。很多java面试题和java基础面试题里提到的“Java 调用 Python 函数”,在 Ray 里根本不是简单地@Remote一下就能跑通。
5.1ClassNotFoundException的真实战场:不是 classpath,而是 ClassLoader 隔离
当你在 Java worker 中调用一个 Python 函数(如@ray.remote def py_func(): return "hello"),Ray 的执行流程是:
- Java driver 将
py_func的模块名、函数名、参数序列化为 JSON; - 通过 gRPC 发送给 head 节点的 GCS;
- GCS 将任务分发给某个 Python worker;
- Python worker 反序列化 JSON,动态
importlib.import_module("my_module"),然后执行my_module.py_func(); - 结果再序列化回 JSON,经 gRPC 返回给 Java driver。
所以,ClassNotFoundException从不发生在 Java 端,而是在Python worker 的importlib加载阶段。错误日志会出现在ray logs的 Python worker 日志里,格式为:
ImportError: No module named 'my_module'解决方案只有两个:
方案一(推荐):将 Python 代码打包为 wheel,上传到所有 worker 节点
# 在 Python 项目根目录 python setup.py bdist_wheel # 将生成的 dist/*.whl 复制到每个 worker 节点的 /opt/ray/python/ scp dist/my_module-0.1-py3-none-any.whl worker1:/opt/ray/python/ # 在 worker 节点上安装 pip install /opt/ray/python/my_module-0.1-py3-none-any.whl方案二:使用
ray.put()传递 Python 代码字符串(仅限简单函数)// Java driver String pyCode = "def add(a, b): return a + b"; ObjectRef<String> codeRef = Ray.put(pyCode); // 然后通过 Python worker 执行 eval(codeRef.get())
注意:方案二有严重安全风险,禁止在生产环境使用。
5.2ActorHandle泄漏:为什么你的 Java 应用内存持续增长?
Java 中的ActorHandle<T>是一个轻量级代理对象,它本身不持有 Actor 的状态,但会维护一个到 Raylet 的 gRPC channel。如果你创建了大量ActorHandle(如在一个 for 循环里MyActor.remote()1000 次),而没有显式调用handle.kill(),这些 channel 就会一直保持打开,直到 JVM GC 触发finalize()方法(这个时机不可控)。结果就是:jstat -gc <pid>显示M(Metaspace)持续增长,jmap -histo <pid>里io.grpc.internal.ManagedChannelImpl实例数暴增。
正确的 Actor 创建与销毁模式:
// ✅ 正确:使用 try-with-resources,自动 close channel try (ActorHandle<MyActor> actor = MyActor.remote()) { ObjectRef<String> ref = actor.task("hello").remote(); String result = ref.get(); System.out.println(result); } // ← 自动调用 actor.close() // ❌ 错误:不关闭,channel 泄漏 ActorHandle<MyActor> actor = MyActor.remote(); // leak! ObjectRef<String> ref = actor.task("hello").remote(); String result = ref.get(); // 忘记 actor.close() !ActorHandle实现了AutoCloseable接口,这是 Ray Java SDK 2.5+ 的关键改进。务必养成try-with-resources习惯。
5.3java获取dns与ray start --node-ip-address的致命耦合
这是最隐蔽的坑。ray start --node-ip-address=10.0.1.100这个参数,决定了该节点向 GCS 注册时使用的 IP 地址。而 Java driver 初始化时,如果address参数写的是域名(如ray://ray-head.internal:10001),它会先调用java.net.InetAddress.getByName("ray-head.internal")进行 DNS 解析。如果 DNS 返回的 IP 是10.0.1.100,那没问题;但如果 DNS 返回的是10.0.1.101(比如负载均衡 VIP),而--node-ip-address设的是10.0.1.100,那么 GCS 里记录的节点地址就是10.0.1.100,但 Java driver 却试图连10.0.1.101,结果就是Connection refused。
终极解决方案:禁用 DNS,强制使用 IP
// Java driver 初始化时,绕过 DNS,直连 IP RayConfig config = new RayConfig() .setAddress("ray://10.0.1.100:10001") // ⚠️ 用 IP,不用域名 .setNodeIpAddress("10.0.1.100") // ⚠️ 显式告知 driver,head 节点 IP 是什么 .setRedisAddress("10.0.1.100:6379"); Ray.init(config);同时,在ray start时,--node-ip-address必须与这个 IP 严格一致。这是唯一能 100% 避免 DNS 引入不确定性的方法。
提示:
setNodeIpAddress()是 Java SDK 的私有 API(在ray-runtime模块中),但它在 2.9 版本中已稳定,生产环境可放心使用。
6.ray init到ray start的完整上线 checklist:一份可打印的生产环境核对表
把上面所有技术点,浓缩成一份可执行、可打印、可贴在工位上的Ray 集群上线 Checklist。每完成一项,就在对应方框打勾(□ → ✓)。少打一个勾,集群就可能在凌晨三点把你叫醒。
6.1 Head 节点启动前检查(共 12 项)
- □资源层:
free -h确认/dev/shm空间 ≥--object-store-memory值(如4g→/dev/shm至少5g) - □资源层:
nproc确认逻辑 CPU 数 ≥--num-cpus,且ulimit -u(最大进程数)≥2 * --num-cpus - □端口层:
sudo lsof -i :6379确认6379未被占用;sudo lsof -i :8076确认8076未被占用 - □端口层:
sudo ufw status确认6379,8076,8077,8265在 allow 列表中(Ubuntu) - □TLS 层:证书文件
/path/to/server.crt和/path/to/server.key存在,且cat /path/to/server.crt | openssl x509 -text -noout | grep -A1 "Subject Alternative Name"显示包含 head 节点 IP - □TLS 层:
openssl s_client -connect 10.0.1.100:10001 -servername 10.0.1.100能成功握手(输出Verify return code: 0 (ok)) - □网络层:
hostname -I输出的 IP 与你计划使用的--node-ip-address一致 - □网络层:
ping 10.0.1.101(第一个 worker IP)能通,且nc -zv 10.0.1.101 8076能通 - □Java 层:
java -version输出 JDK 11+,且JAVA_HOME环境变量已正确设置 - □Java 层:
echo $RAY_HOME指向 Ray Python 安装目录(如/opt/conda/lib/python3.9/site-packages/ray) - □安全层:
getenforce返回Permissive或Disabled(SELinux 必须关闭,否则bind()失败) - □安全层:
sysctl net.ipv4.ip_forward返回1(内核 IP 转发已启用,某些云厂商默认关闭)
6.2ray start --head启动后检查(共 8 项)
- □日志层:
tail -f /tmp/ray/session_latest/logs/gcs_server.out中出现GcsServer started successfully - □日志层:
tail -f /tmp/ray/session_latest/logs/raylet.out中出现Raylet process started successfully - □端口层:
nc -zv 127.0.0.1 6379&&nc -zv 127.0.0.1 8076&&nc -zv 127.0.0.1 8077全部成功 - □端口层:
curl -s http://127.0.0.1:8265/api/cluster_status | jq .summary.num_nodes返回1 - □TLS 层:`openssl s_client -connect 127.0.