251 lines
8.9 KiB
Python
251 lines
8.9 KiB
Python
import json
|
|
import logging
|
|
import sys
|
|
from threading import RLock
|
|
from typing import Any, Dict, Optional
|
|
|
|
import requests
|
|
|
|
from ray._common.network_utils import build_address
|
|
from ray.autoscaler.node_launch_exception import NodeLaunchException
|
|
from ray.autoscaler.node_provider import NodeProvider
|
|
from ray.autoscaler.tags import (
|
|
NODE_KIND_HEAD,
|
|
NODE_KIND_WORKER,
|
|
STATUS_SETTING_UP,
|
|
STATUS_UP_TO_DATE,
|
|
TAG_RAY_NODE_KIND,
|
|
TAG_RAY_NODE_NAME,
|
|
TAG_RAY_NODE_STATUS,
|
|
TAG_RAY_USER_NODE_TYPE,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
HEAD_NODE_ID = 0
|
|
HEAD_NODE_TYPE = "ray.head.default"
|
|
|
|
|
|
class SparkNodeProvider(NodeProvider):
|
|
"""A node provider that implements provider for nodes of Ray on spark."""
|
|
|
|
def __init__(self, provider_config, cluster_name):
|
|
NodeProvider.__init__(self, provider_config, cluster_name)
|
|
self.lock = RLock()
|
|
|
|
self._nodes = {
|
|
str(HEAD_NODE_ID): {
|
|
"tags": {
|
|
TAG_RAY_NODE_KIND: NODE_KIND_HEAD,
|
|
TAG_RAY_USER_NODE_TYPE: HEAD_NODE_TYPE,
|
|
TAG_RAY_NODE_NAME: HEAD_NODE_ID,
|
|
TAG_RAY_NODE_STATUS: STATUS_UP_TO_DATE,
|
|
}
|
|
},
|
|
}
|
|
self._next_node_id = 0
|
|
|
|
self.ray_head_ip = self.provider_config["ray_head_ip"]
|
|
# The port of spark job server. We send http request to spark job server
|
|
# to launch spark jobs, ray worker nodes are launched by spark task in
|
|
# spark jobs.
|
|
spark_job_server_port = self.provider_config["spark_job_server_port"]
|
|
self.spark_job_server_url = (
|
|
f"http://{build_address(self.ray_head_ip, spark_job_server_port)}"
|
|
)
|
|
self.ray_head_port = self.provider_config["ray_head_port"]
|
|
# The unique id for the Ray on spark cluster.
|
|
self.cluster_id = self.provider_config["cluster_unique_id"]
|
|
|
|
def get_next_node_id(self):
|
|
with self.lock:
|
|
self._next_node_id += 1
|
|
return self._next_node_id
|
|
|
|
def non_terminated_nodes(self, tag_filters):
|
|
with self.lock:
|
|
nodes = []
|
|
|
|
died_nodes = []
|
|
for node_id in self._nodes:
|
|
if node_id == str(HEAD_NODE_ID):
|
|
status = "running"
|
|
else:
|
|
status = self._query_node_status(node_id)
|
|
|
|
if status == "running":
|
|
if (
|
|
self._nodes[node_id]["tags"][TAG_RAY_NODE_STATUS]
|
|
== STATUS_SETTING_UP
|
|
):
|
|
self._nodes[node_id]["tags"][
|
|
TAG_RAY_NODE_STATUS
|
|
] = STATUS_UP_TO_DATE
|
|
logger.info(
|
|
f"Spark node provider node {node_id} starts running."
|
|
)
|
|
|
|
if status == "terminated":
|
|
died_nodes.append(node_id)
|
|
else:
|
|
tags = self.node_tags(node_id)
|
|
ok = True
|
|
for k, v in tag_filters.items():
|
|
if tags.get(k) != v:
|
|
ok = False
|
|
if ok:
|
|
nodes.append(node_id)
|
|
|
|
for died_node_id in died_nodes:
|
|
self._nodes.pop(died_node_id)
|
|
|
|
return nodes
|
|
|
|
def _query_node_status(self, node_id):
|
|
spark_job_group_id = self._gen_spark_job_group_id(node_id)
|
|
|
|
response = requests.post(
|
|
url=self.spark_job_server_url + "/query_task_status",
|
|
json={"spark_job_group_id": spark_job_group_id},
|
|
)
|
|
response.raise_for_status()
|
|
|
|
decoded_resp = response.content.decode("utf-8")
|
|
json_res = json.loads(decoded_resp)
|
|
return json_res["status"]
|
|
|
|
def is_running(self, node_id):
|
|
with self.lock:
|
|
return (
|
|
node_id in self._nodes
|
|
and self._nodes[node_id]["tags"][TAG_RAY_NODE_STATUS]
|
|
== STATUS_UP_TO_DATE
|
|
)
|
|
|
|
def is_terminated(self, node_id):
|
|
with self.lock:
|
|
return node_id not in self._nodes
|
|
|
|
def node_tags(self, node_id):
|
|
with self.lock:
|
|
return self._nodes[node_id]["tags"]
|
|
|
|
def _get_ip(self, node_id: str) -> Optional[str]:
|
|
return node_id
|
|
|
|
def external_ip(self, node_id):
|
|
return self._get_ip(node_id)
|
|
|
|
def internal_ip(self, node_id):
|
|
return self._get_ip(node_id)
|
|
|
|
def set_node_tags(self, node_id, tags):
|
|
assert node_id in self._nodes
|
|
self._nodes[node_id]["tags"].update(tags)
|
|
|
|
def create_node(
|
|
self, node_config: Dict[str, Any], tags: Dict[str, str], count: int
|
|
) -> Optional[Dict[str, Any]]:
|
|
raise AssertionError("This method should not be called.")
|
|
|
|
def _gen_spark_job_group_id(self, node_id):
|
|
return (
|
|
f"ray-cluster-{self.ray_head_port}-{self.cluster_id}"
|
|
f"-worker-node-{node_id}"
|
|
)
|
|
|
|
def create_node_with_resources_and_labels(
|
|
self, node_config, tags, count, resources, labels
|
|
):
|
|
for _ in range(count):
|
|
self._create_node_with_resources_and_labels(
|
|
node_config, tags, resources, labels
|
|
)
|
|
|
|
def _create_node_with_resources_and_labels(
|
|
self, node_config, tags, resources, labels
|
|
):
|
|
from ray.util.spark.cluster_init import _append_resources_config
|
|
|
|
with self.lock:
|
|
resources = resources.copy()
|
|
node_type = tags[TAG_RAY_USER_NODE_TYPE]
|
|
# NOTE:
|
|
# "NODE_ID_AS_RESOURCE" value must be an integer,
|
|
# but `node_id` used by autoscaler must be a string.
|
|
node_id = str(self.get_next_node_id())
|
|
resources["NODE_ID_AS_RESOURCE"] = int(node_id)
|
|
|
|
conf = self.provider_config.copy()
|
|
|
|
num_cpus_per_node = resources.pop("CPU")
|
|
num_gpus_per_node = resources.pop("GPU")
|
|
heap_memory_per_node = resources.pop("memory")
|
|
object_store_memory_per_node = resources.pop("object_store_memory")
|
|
|
|
conf["worker_node_options"] = _append_resources_config(
|
|
conf["worker_node_options"], resources
|
|
)
|
|
response = requests.post(
|
|
url=self.spark_job_server_url + "/create_node",
|
|
json={
|
|
"spark_job_group_id": self._gen_spark_job_group_id(node_id),
|
|
"spark_job_group_desc": (
|
|
"This job group is for spark job which runs the Ray "
|
|
f"cluster worker node {node_id} connecting to ray "
|
|
f"head node {build_address(self.ray_head_ip, self.ray_head_port)}"
|
|
),
|
|
"using_stage_scheduling": conf["using_stage_scheduling"],
|
|
"ray_head_ip": self.ray_head_ip,
|
|
"ray_head_port": self.ray_head_port,
|
|
"ray_temp_dir": conf["ray_temp_dir"],
|
|
"num_cpus_per_node": num_cpus_per_node,
|
|
"num_gpus_per_node": num_gpus_per_node,
|
|
"heap_memory_per_node": heap_memory_per_node,
|
|
"object_store_memory_per_node": object_store_memory_per_node,
|
|
"worker_node_options": conf["worker_node_options"],
|
|
"collect_log_to_path": conf["collect_log_to_path"],
|
|
"node_id": resources["NODE_ID_AS_RESOURCE"],
|
|
},
|
|
)
|
|
|
|
try:
|
|
# Spark job server is locally launched, if spark job server request
|
|
# failed, it is unlikely network error but probably unrecoverable
|
|
# error, so we make it fast-fail.
|
|
response.raise_for_status()
|
|
except Exception:
|
|
raise NodeLaunchException(
|
|
"Node creation failure",
|
|
f"Starting ray worker node {node_id} failed",
|
|
sys.exc_info(),
|
|
)
|
|
|
|
self._nodes[node_id] = {
|
|
"tags": {
|
|
TAG_RAY_NODE_KIND: NODE_KIND_WORKER,
|
|
TAG_RAY_USER_NODE_TYPE: node_type,
|
|
TAG_RAY_NODE_NAME: node_id,
|
|
TAG_RAY_NODE_STATUS: STATUS_SETTING_UP,
|
|
},
|
|
}
|
|
logger.info(f"Spark node provider creates node {node_id}.")
|
|
|
|
def terminate_node(self, node_id):
|
|
if node_id in self._nodes:
|
|
response = requests.post(
|
|
url=self.spark_job_server_url + "/terminate_node",
|
|
json={"spark_job_group_id": self._gen_spark_job_group_id(node_id)},
|
|
)
|
|
response.raise_for_status()
|
|
|
|
with self.lock:
|
|
if node_id in self._nodes:
|
|
self._nodes.pop(node_id)
|
|
|
|
logger.info(f"Spark node provider terminates node {node_id}")
|
|
|
|
@staticmethod
|
|
def bootstrap_config(cluster_config):
|
|
return cluster_config
|