Efficient live access to container logs » History » Revision 3
Revision 2 (Tom Clegg, 08/08/2023 06:44 PM) → Revision 3/4 (Tom Clegg, 08/08/2023 06:48 PM)
h1. Efficient live access to container logs
{{toc}}
h2. Goals
* Low latency: deliver live logs to clients (workbench/cli) as quickly as possible
* Scalable: don't generate significant load in RailsAPI
* Scalable: don't generate significant load in PostgreSQL
* Efficient: don't waste resources on event streams that nobody is listening to
* Robust: Don't let a badly behaved container (writing many gigabytes of logs) impede others
* Dependable: Retain complete logs (container/crunch-run/crunchstat) _in the log collection_ even in the event that crunch-run or the compute instance itself dies unexpectedly (_e.g.,_ bug, out of memory, hardware failure)
h2. Proposed architecture
* crunch-run offers a “container logs” API at its container gateway server
* controller offers a “container logs” API that works whether the container is finished or not
** proxies to crunch-run _or_ keep-web (depends on whether the container is running)
* “container logs” API has two parts:
** webdav: read file content from the log collection
** server-sent-events stream: get notifications as logs appear (see below)
* dispatcher provides “plan B” to save logs when crunch-run crashes, instance dies, etc.
** dispatcher routinely retrieves live logs from container gateway, and buffers content to local disk
** dispatcher discards buffered logs when crunch-run commits to log collection
** when crunch-run exits, dispatcher checks whether crunch-run managed to save the full log collection and, if needed, appends the latest buffered logs
* command-line client can “tail” and “tail -f” logs from a running container
* workbench2 uses a single code path to retrieve logs (instead of choosing webdav or logs table based on whether container is running)
h2. API details
@GET /arvados/v1/container_requests/{uuid}/log/{container_uuid}/stderr.txt@
** client will use Range headers to request partial content
** client will add the @{container_uuid}/@ part explicitly using the @container_uuid@ field from the container request record -- @/arvados/v1/container_requests/{uuid}/log/@ is a valid WebDAV endpoint but the client cannot rely on that directory listing to return the @{container_uuid}/@ subdirectory
@GET /arvados/v1/container_requests/{uuid}/log_events@
** response is a "server-sent events":https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events stream, data is JSON-encoded
** a “file_sizes” event indicates the current size of new/updated files in the log collection
@event: file_sizes@
@data: {"{container_uuid}/stderr.txt":1234,"{container_uuid}/stdout.txt":0,"{container_uuid}/crunch-run.txt":2345}@
** after the first message, unchanged files may be omitted:
@event: file_sizes@
@data: {"{container_uuid}/stderr.txt":1444,"{container_uuid}/stdout.txt":72}@
** a “final” message indicates that the log collection is finalized and no further file_sizes events will be sent:
@event: final@
** a “retry” message indicates how long, in milliseconds, the client should wait before reconnecting after a disconnect (per SSE spec):
@retry: 2000@
** empty comments are sent periodically to keep the connection alive during idle periods
@:@
** after the “final” message, the connection either remains open and silent, or closes after specifying a long reconnect delay in a “retry” message (the idea is to avoid sending an inattentive client into a reconnect cycle - this doesn't affect well-behaved clients because they will hang up after receiving the “final” message)
** @?maxInterval=X@ sets the maximum interval between messages (i.e., keepalive) to X seconds, default 15
** @?minInterval=X@ increases the minimum interval between messages (i.e., throttle) to X seconds, default 1
h2. Authentication
The browser's EventSource API allows the caller to enable existing cached credentials/cookies, but not to explicitly set an Authorization header or another custom header.
* @?api_token=v2/...@ in the URL is easy to support on the client and server
** this is not ideal: the token is likely to be logged by HTTP servers/proxies
** we already accept this situation in our websocket usage
Server also accepts the usual Authorization header, and this should be used by clients that have the capability (e.g., arvados-client).
Future version may also support a “copy Authorization header into cookie” endpoint
** client requests @GET /arvados/v1/cookie@ request with the token in the Authorization request header
** response sets a cookie with the provided token
** client sets up EventSource using @{withCredentials: true}@
** this approach is inherently flawed in that applications in different tabs (e.g., wb2.clusterA and wb2.clusterB, where A and B are in a federation) will unknowingly fight over which cookie should be sent to cluster A.
Future version may also support a “provide short-lived token” endpoint
** client requests @GET /arvados/v1/api_client_authorizations/:uuid/slt@ with the real API token in an Authorization request header
** response contains a new token that will only be recognized for the next 15 seconds
** client sets up EventSource using @?slt={newtoken}@
** event stream sends a message with the real token in the “id” field
** when reconnecting, the browser automatically sends the real token in the Last-Event-Id header
** server uses the token in the Last-Event-Id field, if provided
h2. Client pseudocode
to reliably display logs for container with uuid C:
<pre>
connect to server-sent-events endpoint /arvados/v1/container_requests/{CR}/log_events
on unexpected disconnect:
delay for duration indicated in last “retry” message
reconnect
on final:
disconnect
on file_sizes:
decode message data as json
compare each entry to last size received for corresponding file, if any
if desired, retrieve file data via webdav endpoint and present to user
</pre>
h2. Client (workbench2 / CLI) implementation notes
* optionally, if container state is complete/cancelled or CR state is final, client _may_ skip the log_events steps and just retrieve file data via webdav endpoint
* the client can rely on the server-sent-events stream to return a message, with sizes of all available log files, upon first connection, even if there are no available sizes yet (i.e., message.files may have zero keys) or the container is already finished
* message.files may have keys for newly added files that have not appeared in any previous message
* client should have logic to skip some content if the log file size grows faster than expected
h2. Crunch-run implementation notes
* buffer logs and periodically flush to keep (as before)
* container gateway has a new webdav endpoint that reads the (live) log collection
** stdlib webdav server implements range requests for us
* container gateway has a new endpoint that returns a server-sent-events stream
** upon connection, return one event per log file: [filename, current-size, last-saved-size]
** when a file is updated, wait until the last returned event is more than 1s old, then return an event [filename, current-size, last-saved-size]
*** “current-size” enables client to construct a suitable range request to retrieve recent data
*** “current-size” enables client to skip ahead if appropriate
*** “last-saved-size” enables dispatcher to discard buffered data once it's safely written to storage and accessible via log collection
* crunch-run updates gateway_address as soon as the gateway service is up (instead of waiting until state=Running)
** railsapi should allow gateway_address to be set when state=Locked
** this allows clients to access early logs (e.g., while preparing the docker image)
h2. Controller implementation notes for log_events
* additional logic is needed here beyond a simple proxy, in order that a single client-to-controller connection can receive all log updates through the entire container lifecycle, including one with the final file sizes, even though
** crunch-run is not running yet when state=Queued
** crunch-run may exit/disconnect before returning the final update (many race possibilities, including container finishing while controller is trying to reconnect)
** additional logs may be added by dispatcher if crunch-run exits before finalizing the container
* if state=Queued or container gateway_address is empty, send an empty file_sizes message, then send keepalive messages while waiting/polling for the gateway to come up
* if state=Complete or Cancelled, get the final log file sizes, send a file_sizes message, a long retry message, and a “final” message; delay a few seconds; and hang up
* connect to crunch-run's log_events endpoint (through controller tunnel if applicable), propagate client's last-id and min/maxInterval params, and relay messages (including keepalive) to client
* when crunch-run disconnects (or does not answer), wait a second, re-fetch the container record, and repeat from top
h2. Controller implementation notes for log data
* if container log is not empty, a specific range is requested, and the size of the requested file is compatible with the requested range, proxy to keep-web
* if state=Complete/Cancelled, proxy to keep-web
* if gateway_address is empty, return 404
* if state=Locked/Running and gateway_address is not empty, proxy to gateway_address
h2. Dispatcher implementation (for retaining the last log entries when a worker instance crashes)
* routinely connect/reconnect to crunch-run gateway server of containers that are being supervised
* when unflushed logs appear, save them in a temporary buffer on local disk, up to a configurable size limit
* when crunch-run exits unexpectedly, check the latest saved log collection, and (if needed) append the buffered logs to the respective truncated files before finalizing the container
* in HPC environment where dispatcher→worker connections are not supported, connect through the worker→controller tunnel (see #19166)