mirror of
https://github.com/wassname/ray.git
synced 2026-07-13 10:19:30 +08:00
Stream logs to driver by default. (#3892)
* Stream logs to driver by default. * Fix from rebase * Redirect raylet output independently of worker output. * Fix. * Create redis client with services.create_redis_client. * Suppress Redis connection error at exit. * Remove thread_safe_client from redis. * Shutdown driver threads in ray.shutdown(). * Add warning for too many log messages. * Only stop threads if worker is connected. * Only stop threads if they exist. * Remove unnecessary try/excepts. * Fix * Only add new logging handler once. * Increase timeout. * Fix tempfile test. * Fix logging in cluster_utils. * Revert "Increase timeout." This reverts commit b3846b89040bcd8e583b2e18cb513cb040e71d95. * Retry longer when connecting to plasma store from node manager and object manager. * Close pubsub channels to avoid leaking file descriptors. * Limit log monitor open files to 200. * Increase plasma connect retries. * Add comment.
This commit is contained in:
committed by
Philipp Moritz
parent
0aa74fb1fd
commit
ef527f84ab
+186
-85
@@ -3,14 +3,14 @@ from __future__ import division
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import redis
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import colorama
|
||||
|
||||
import ray.ray_constants as ray_constants
|
||||
from ray.services import get_ip_address
|
||||
from ray.services import get_port
|
||||
import ray.utils
|
||||
|
||||
# Logger for this module. It should be configured at the entry point
|
||||
@@ -19,91 +19,185 @@ import ray.utils
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LogFileInfo(object):
|
||||
def __init__(self,
|
||||
filename=None,
|
||||
size_when_last_opened=None,
|
||||
file_position=None,
|
||||
file_handle=None):
|
||||
assert (filename is not None and size_when_last_opened is not None
|
||||
and file_position is not None)
|
||||
self.filename = filename
|
||||
self.size_when_last_opened = size_when_last_opened
|
||||
self.file_position = file_position
|
||||
self.file_handle = file_handle
|
||||
self.worker_pid = None
|
||||
|
||||
|
||||
class LogMonitor(object):
|
||||
"""A monitor process for monitoring Ray log files.
|
||||
|
||||
This class mantains a list of open files and a list of closed log files. We
|
||||
can't simply leave all files open because we'll run out of file
|
||||
descriptors.
|
||||
|
||||
The "run" method of this class will cycle between doing several things:
|
||||
1. First, it will check if any new files have appeared in the log
|
||||
directory. If so, they will be added to the list of closed files.
|
||||
2. Then, if we are unable to open any new files, we will close all of the
|
||||
files.
|
||||
3. Then, we will open as many closed files as we can that may have new
|
||||
lines (judged by an increase in file size since the last time the file
|
||||
was opened).
|
||||
4. Then we will loop through the open files and see if there are any new
|
||||
lines in the file. If so, we will publish them to Redis.
|
||||
|
||||
Attributes:
|
||||
node_ip_address: The IP address of the node that the log monitor
|
||||
process is running on. This will be used to determine which log
|
||||
files to track.
|
||||
host (str): The hostname of this machine. Used to improve the log
|
||||
messages published to Redis.
|
||||
logs_dir (str): The directory that the log files are in.
|
||||
redis_client: A client used to communicate with the Redis server.
|
||||
log_files: A dictionary mapping the name of a log file to a list of
|
||||
strings representing its contents.
|
||||
log_file_handles: A dictionary mapping the name of a log file to a file
|
||||
handle for that file.
|
||||
log_filenames (set): This is the set of filenames of all files in
|
||||
open_file_infos and closed_file_infos.
|
||||
open_file_infos (list[LogFileInfo]): Info for all of the open files.
|
||||
closed_file_infos (list[LogFileInfo]): Info for all of the closed
|
||||
files.
|
||||
can_open_more_files (bool): True if we can still open more files and
|
||||
false otherwise.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
redis_ip_address,
|
||||
redis_port,
|
||||
node_ip_address,
|
||||
redis_password=None):
|
||||
def __init__(self, logs_dir, redis_address, redis_password=None):
|
||||
"""Initialize the log monitor object."""
|
||||
self.node_ip_address = node_ip_address
|
||||
self.redis_client = redis.StrictRedis(
|
||||
host=redis_ip_address, port=redis_port, password=redis_password)
|
||||
self.log_files = {}
|
||||
self.log_file_handles = {}
|
||||
self.files_to_ignore = set()
|
||||
self.host = os.uname()[1]
|
||||
self.logs_dir = logs_dir
|
||||
self.redis_client = ray.services.create_redis_client(
|
||||
redis_address, password=redis_password)
|
||||
self.log_filenames = set()
|
||||
self.open_file_infos = []
|
||||
self.closed_file_infos = []
|
||||
self.can_open_more_files = True
|
||||
|
||||
def close_all_files(self):
|
||||
"""Close all open files (so that we can open more)."""
|
||||
while len(self.open_file_infos) > 0:
|
||||
file_info = self.open_file_infos.pop(0)
|
||||
file_info.file_handle.close()
|
||||
file_info.file_handle = None
|
||||
self.closed_file_infos.append(file_info)
|
||||
self.can_open_more_files = True
|
||||
|
||||
def update_log_filenames(self):
|
||||
"""Get the most up-to-date list of log files to monitor from Redis."""
|
||||
num_current_log_files = len(self.log_files)
|
||||
new_log_filenames = self.redis_client.lrange(
|
||||
"LOG_FILENAMES:{}".format(self.node_ip_address),
|
||||
num_current_log_files, -1)
|
||||
for log_filename in new_log_filenames:
|
||||
logger.info("Beginning to track file {}".format(log_filename))
|
||||
assert log_filename not in self.log_files
|
||||
self.log_files[log_filename] = []
|
||||
"""Update the list of log files to monitor."""
|
||||
log_filenames = os.listdir(self.logs_dir)
|
||||
|
||||
def check_log_files_and_push_updates(self):
|
||||
"""Get any changes to the log files and push updates to Redis."""
|
||||
for log_filename in self.log_files:
|
||||
if log_filename in self.log_file_handles:
|
||||
# Get any updates to the file.
|
||||
new_lines = []
|
||||
while True:
|
||||
current_position = (
|
||||
self.log_file_handles[log_filename].tell())
|
||||
next_line = self.log_file_handles[log_filename].readline()
|
||||
if next_line != "":
|
||||
new_lines.append(next_line)
|
||||
else:
|
||||
self.log_file_handles[log_filename].seek(
|
||||
current_position)
|
||||
break
|
||||
for log_filename in log_filenames:
|
||||
full_path = os.path.join(self.logs_dir, log_filename)
|
||||
if full_path not in self.log_filenames:
|
||||
self.log_filenames.add(full_path)
|
||||
self.closed_file_infos.append(
|
||||
LogFileInfo(
|
||||
filename=full_path,
|
||||
size_when_last_opened=0,
|
||||
file_position=0,
|
||||
file_handle=None))
|
||||
logger.info("Beginning to track file {}".format(log_filename))
|
||||
|
||||
# If there are any new lines, cache them and also push them to
|
||||
# Redis.
|
||||
if len(new_lines) > 0:
|
||||
self.log_files[log_filename] += new_lines
|
||||
redis_key = "LOGFILE:{}:{}".format(
|
||||
self.node_ip_address, ray.utils.decode(log_filename))
|
||||
self.redis_client.rpush(redis_key, *new_lines)
|
||||
def open_closed_files(self):
|
||||
"""Open some closed files if they may have new lines.
|
||||
|
||||
# Pass if we already failed to open the log file.
|
||||
elif log_filename in self.files_to_ignore:
|
||||
pass
|
||||
Opening more files may require us to close some of the already open
|
||||
files.
|
||||
"""
|
||||
if not self.can_open_more_files:
|
||||
# If we can't open any more files. Close all of the files.
|
||||
self.close_all_files()
|
||||
|
||||
# Try to open this file for the first time.
|
||||
else:
|
||||
files_with_no_updates = []
|
||||
while len(self.closed_file_infos) > 0:
|
||||
if (len(self.open_file_infos) >=
|
||||
ray_constants.LOG_MONITOR_MAX_OPEN_FILES):
|
||||
self.can_open_more_files = False
|
||||
break
|
||||
|
||||
file_info = self.closed_file_infos.pop(0)
|
||||
assert file_info.file_handle is None
|
||||
# Get the file size to see if it has gotten bigger since we last
|
||||
# opened it.
|
||||
try:
|
||||
file_size = os.path.getsize(file_info.filename)
|
||||
except (IOError, OSError) as e:
|
||||
# Catch "file not found" errors.
|
||||
if e.errno == errno.ENOENT:
|
||||
logger.warning("Warning: The file {} was not "
|
||||
"found.".format(file_info.filename))
|
||||
self.log_filenames.remove(file_info.filename)
|
||||
continue
|
||||
raise e
|
||||
|
||||
# If some new lines have been added to this file, try to reopen the
|
||||
# file.
|
||||
if file_size > file_info.size_when_last_opened:
|
||||
try:
|
||||
self.log_file_handles[log_filename] = open(
|
||||
log_filename, "r")
|
||||
except IOError as e:
|
||||
if e.errno == os.errno.EMFILE:
|
||||
logger.warning(
|
||||
"Warning: Ignoring {} because there are too "
|
||||
"many open files.".format(log_filename))
|
||||
elif e.errno == os.errno.ENOENT:
|
||||
f = open(file_info.filename, "r")
|
||||
except (IOError, OSError) as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
logger.warning("Warning: The file {} was not "
|
||||
"found.".format(log_filename))
|
||||
"found.".format(file_info.filename))
|
||||
self.log_filenames.remove(file_info.filename)
|
||||
continue
|
||||
else:
|
||||
raise e
|
||||
|
||||
# Don't try to open this file any more.
|
||||
self.files_to_ignore.add(log_filename)
|
||||
f.seek(file_info.file_position)
|
||||
file_info.filesize_when_last_opened = file_size
|
||||
file_info.file_handle = f
|
||||
self.open_file_infos.append(file_info)
|
||||
else:
|
||||
files_with_no_updates.append(file_info)
|
||||
|
||||
# Add the files with no changes back to the list of closed files.
|
||||
self.closed_file_infos += files_with_no_updates
|
||||
|
||||
def check_log_files_and_publish_updates(self):
|
||||
"""Get any changes to the log files and push updates to Redis."""
|
||||
for file_info in self.open_file_infos:
|
||||
assert not file_info.file_handle.closed
|
||||
|
||||
lines_to_publish = []
|
||||
max_num_lines_to_read = 100
|
||||
for _ in range(max_num_lines_to_read):
|
||||
next_line = file_info.file_handle.readline()
|
||||
if next_line == "":
|
||||
break
|
||||
if next_line[-1] == "\n":
|
||||
next_line = next_line[:-1]
|
||||
lines_to_publish.append(next_line)
|
||||
|
||||
# Publish the lines if this is a worker process.
|
||||
filename = file_info.filename.split("/")[-1]
|
||||
is_worker = (filename.startswith("worker")
|
||||
and (filename.endswith("out")
|
||||
or filename.endswith("err")))
|
||||
output_type = "stdout" if filename.endswith("out") else "stderr"
|
||||
|
||||
if is_worker and file_info.file_position == 0:
|
||||
if (len(lines_to_publish) > 0 and
|
||||
lines_to_publish[0].startswith("Ray worker pid: ")):
|
||||
file_info.worker_pid = int(
|
||||
lines_to_publish[0].split(" ")[-1])
|
||||
lines_to_publish = lines_to_publish[1:]
|
||||
|
||||
# Record the current position in the file.
|
||||
file_info.file_position = file_info.file_handle.tell()
|
||||
|
||||
if len(lines_to_publish) > 0 and is_worker:
|
||||
lines_to_publish.insert(
|
||||
0, "{}{}{} (pid={}, host={})".format(
|
||||
colorama.Fore.CYAN, "worker ({})".format(output_type),
|
||||
colorama.Fore.RESET, file_info.worker_pid, self.host))
|
||||
|
||||
self.redis_client.publish(ray.gcs_utils.LOG_FILE_CHANNEL,
|
||||
"\n".join(lines_to_publish))
|
||||
|
||||
def run(self):
|
||||
"""Run the log monitor.
|
||||
@@ -113,8 +207,8 @@ class LogMonitor(object):
|
||||
"""
|
||||
while True:
|
||||
self.update_log_filenames()
|
||||
self.check_log_files_and_push_updates()
|
||||
time.sleep(1)
|
||||
self.open_closed_files()
|
||||
self.check_log_files_and_publish_updates()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -127,11 +221,6 @@ if __name__ == "__main__":
|
||||
required=True,
|
||||
type=str,
|
||||
help="The address to use for Redis.")
|
||||
parser.add_argument(
|
||||
"--node-ip-address",
|
||||
required=True,
|
||||
type=str,
|
||||
help="The IP address of the node this process is on.")
|
||||
parser.add_argument(
|
||||
"--redis-password",
|
||||
required=False,
|
||||
@@ -151,15 +240,27 @@ if __name__ == "__main__":
|
||||
type=str,
|
||||
default=ray_constants.LOGGER_FORMAT,
|
||||
help=ray_constants.LOGGER_FORMAT_HELP)
|
||||
parser.add_argument(
|
||||
"--logs-dir",
|
||||
required=True,
|
||||
type=str,
|
||||
help="Specify the path of the temporary directory used by Ray "
|
||||
"processes.")
|
||||
args = parser.parse_args()
|
||||
ray.utils.setup_logger(args.logging_level, args.logging_format)
|
||||
|
||||
redis_ip_address = get_ip_address(args.redis_address)
|
||||
redis_port = get_port(args.redis_address)
|
||||
|
||||
log_monitor = LogMonitor(
|
||||
redis_ip_address,
|
||||
redis_port,
|
||||
args.node_ip_address,
|
||||
redis_password=args.redis_password)
|
||||
log_monitor.run()
|
||||
args.logs_dir, args.redis_address, redis_password=args.redis_password)
|
||||
|
||||
try:
|
||||
log_monitor.run()
|
||||
except Exception as e:
|
||||
# Something went wrong, so push an error to all drivers.
|
||||
redis_client = ray.services.create_redis_client(
|
||||
args.redis_address, password=args.redis_password)
|
||||
traceback_str = ray.utils.format_error_message(traceback.format_exc())
|
||||
message = ("The log monitor on node {} failed with the following "
|
||||
"error:\n{}".format(os.uname()[1], traceback_str))
|
||||
ray.utils.push_error_to_driver_through_redis(
|
||||
redis_client, ray_constants.LOG_MONITOR_DIED_ERROR, message)
|
||||
raise e
|
||||
|
||||
Reference in New Issue
Block a user