Files
2026-07-13 13:17:40 +08:00

235 lines
9.1 KiB
Python

import logging
from queue import Queue
from typing import List, Optional
from urllib.parse import urlsplit
from ray._raylet import GcsClient
from ray.autoscaler._private.providers import _get_node_provider
from ray.autoscaler.v2.event_logger import AutoscalerEventLogger
from ray.autoscaler.v2.instance_manager.cloud_providers.kuberay.cloud_provider import (
KubeRayProvider,
)
from ray.autoscaler.v2.instance_manager.cloud_providers.read_only.cloud_provider import ( # noqa
ReadOnlyProvider,
)
from ray.autoscaler.v2.instance_manager.config import (
AutoscalingConfig,
IConfigReader,
Provider,
)
from ray.autoscaler.v2.instance_manager.instance_manager import (
InstanceManager,
InstanceUpdatedSubscriber,
)
from ray.autoscaler.v2.instance_manager.instance_storage import InstanceStorage
from ray.autoscaler.v2.instance_manager.node_provider import (
ICloudInstanceProvider,
NodeProviderAdapter,
)
from ray.autoscaler.v2.instance_manager.ray_installer import RayInstaller
from ray.autoscaler.v2.instance_manager.reconciler import Reconciler
from ray.autoscaler.v2.instance_manager.storage import InMemoryStorage
from ray.autoscaler.v2.instance_manager.subscribers.cloud_instance_updater import (
CloudInstanceUpdater,
)
from ray.autoscaler.v2.instance_manager.subscribers.cloud_resource_monitor import (
CloudResourceMonitor,
)
from ray.autoscaler.v2.instance_manager.subscribers.ray_stopper import RayStopper
from ray.autoscaler.v2.instance_manager.subscribers.threaded_ray_installer import (
ThreadedRayInstaller,
)
from ray.autoscaler.v2.metrics_reporter import AutoscalerMetricsReporter
from ray.autoscaler.v2.scheduler import ResourceDemandScheduler
from ray.autoscaler.v2.sdk import get_cluster_resource_state
from ray.core.generated.autoscaler_pb2 import AutoscalingState
from ray.exceptions import AuthenticationError
logger = logging.getLogger(__name__)
class Autoscaler:
def __init__(
self,
session_name: str,
config_reader: IConfigReader,
gcs_client: GcsClient,
event_logger: Optional[AutoscalerEventLogger] = None,
metrics_reporter: Optional[AutoscalerMetricsReporter] = None,
) -> None:
"""Initialize the autoscaler.
Args:
session_name: The current Ray session name.
config_reader: The config reader.
gcs_client: The GCS client.
event_logger: The event logger for emitting cluster events.
metrics_reporter: The metrics reporter for emitting cluster metrics.
"""
self._config_reader = config_reader
config = config_reader.get_cached_autoscaling_config()
logger.info(f"Using Autoscaling Config: \n{config.dump()}")
self._gcs_client = gcs_client
self._cloud_instance_provider = None
self._instance_manager = None
self._ray_stop_errors_queue = Queue()
self._ray_install_errors_queue = Queue()
self._event_logger = event_logger
self._metrics_reporter = metrics_reporter
self._init_cloud_instance_provider(config, config_reader)
self._cloud_resource_monitor = None
self._init_instance_manager(
session_name=session_name,
config=config,
cloud_provider=self._cloud_instance_provider,
gcs_client=self._gcs_client,
)
self._scheduler = ResourceDemandScheduler(self._event_logger)
def _init_cloud_instance_provider(
self, config: AutoscalingConfig, config_reader: IConfigReader
):
"""
Initialize the cloud provider, and its dependencies (the v1 node provider)
Args:
config: The autoscaling config.
config_reader: The config reader.
"""
provider_config = config.get_provider_config()
if provider_config["type"] == "kuberay":
provider_config["head_node_type"] = config.get_head_node_type()
self._cloud_instance_provider = KubeRayProvider(
config.get_config("cluster_name"),
provider_config,
gcs_client=self._gcs_client,
)
elif config.provider == Provider.READ_ONLY:
provider_config["gcs_address"] = self._gcs_client.address
self._cloud_instance_provider = ReadOnlyProvider(
provider_config=provider_config,
)
else:
node_provider_v1 = _get_node_provider(
provider_config,
config.get_config("cluster_name"),
)
self._cloud_instance_provider = NodeProviderAdapter(
v1_provider=node_provider_v1,
config_reader=config_reader,
)
def _init_instance_manager(
self,
session_name: str,
cloud_provider: ICloudInstanceProvider,
gcs_client: GcsClient,
config: AutoscalingConfig,
):
"""
Initialize the instance manager, and its dependencies.
"""
instance_storage = InstanceStorage(
cluster_id=session_name,
storage=InMemoryStorage(),
)
subscribers: List[InstanceUpdatedSubscriber] = []
subscribers.append(
CloudInstanceUpdater(
cloud_provider=cloud_provider,
metrics_reporter=self._metrics_reporter,
)
)
subscribers.append(
RayStopper(gcs_client=gcs_client, error_queue=self._ray_stop_errors_queue)
)
if not config.disable_node_updaters() and isinstance(
cloud_provider, NodeProviderAdapter
):
head_node_ip = urlsplit("//" + self._gcs_client.address).hostname
assert head_node_ip is not None, "Invalid GCS address format"
subscribers.append(
ThreadedRayInstaller(
head_node_ip=head_node_ip,
instance_storage=instance_storage,
ray_installer=RayInstaller(
provider=cloud_provider.v1_provider,
config=config,
),
error_queue=self._ray_install_errors_queue,
# TODO(rueian): Rewrite the ThreadedRayInstaller and its underlying
# NodeUpdater and CommandRunner to use the asyncio, so that we don't
# need to use so many threads. We use so many threads now because
# they are blocking and letting the new cloud machines to wait for
# previous machines to finish installing Ray is quite inefficient.
max_concurrent_installs=config.get_max_num_worker_nodes() or 50,
)
)
self._cloud_resource_monitor = CloudResourceMonitor()
subscribers.append(self._cloud_resource_monitor)
self._instance_manager = InstanceManager(
instance_storage=instance_storage,
instance_status_update_subscribers=subscribers,
)
def update_autoscaling_state(
self,
) -> Optional[AutoscalingState]:
"""Update the autoscaling state of the cluster by reconciling the current
state of the cluster resources, the cloud providers as well as instance
update subscribers with the desired state.
Returns:
AutoscalingState: The new autoscaling state of the cluster or None if
the state is not updated.
Raises:
None: No exception.
"""
try:
ray_stop_errors = []
while not self._ray_stop_errors_queue.empty():
ray_stop_errors.append(self._ray_stop_errors_queue.get())
ray_install_errors = []
while not self._ray_install_errors_queue.empty():
ray_install_errors.append(self._ray_install_errors_queue.get())
# Get the current state of the ray cluster resources.
ray_cluster_resource_state = get_cluster_resource_state(self._gcs_client)
# Refresh the config from the source
self._config_reader.refresh_cached_autoscaling_config()
autoscaling_config = self._config_reader.get_cached_autoscaling_config()
return Reconciler.reconcile(
instance_manager=self._instance_manager,
scheduler=self._scheduler,
cloud_provider=self._cloud_instance_provider,
cloud_resource_monitor=self._cloud_resource_monitor,
ray_cluster_resource_state=ray_cluster_resource_state,
non_terminated_cloud_instances=(
self._cloud_instance_provider.get_non_terminated()
),
cloud_provider_errors=self._cloud_instance_provider.poll_errors(),
ray_install_errors=ray_install_errors,
ray_stop_errors=ray_stop_errors,
autoscaling_config=autoscaling_config,
metrics_reporter=self._metrics_reporter,
)
except AuthenticationError as e:
logger.warning(f"AuthenticationError detected, restarting autoscaler: {e}")
raise
except Exception as e:
logger.exception(e)
return None