From 422c8933948c46554a9b12b459868ba3ef80a33d Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:45:07 +0100 Subject: [PATCH 01/17] docs/ folder: add all missing content files from gh-pages branch Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/api.md | 101 +++++++ docs/architecture.md | 80 ++++++ docs/demo.md | 144 ++++++++++ docs/design.md | 72 +++++ docs/design/architecture.md | 80 ++++++ docs/design/database_interfaces.md | 187 +++++++++++++ docs/design/diagrams/relationships/README.md | 25 ++ .../diagrams/relationships/armada_system.png | Bin 0 -> 178826 bytes .../design/diagrams/relationships/generate.py | 147 ++++++++++ .../diagrams/relationships/images/armada.png | Bin 0 -> 16551 bytes .../diagrams/relationships/images/browser.png | Bin 0 -> 39639 bytes .../diagrams/relationships/images/pulsar.png | Bin 0 -> 3356 bytes docs/design/index.md | 76 ++++++ docs/design/jobservice/airflow-sequence.pml | 8 + docs/design/jobservice/airflow-sequence.svg | 18 ++ docs/design/jobservice/job-service.md | 114 ++++++++ docs/design/jobservice/job-service.svg | 15 ++ docs/design/jobservice/jobservice.pml | 5 + docs/design/priority.md | 52 ++++ docs/design/relationships_diagram.md | 43 +++ docs/design/scheduler.md | 154 +++++++++++ docs/developer/aws-ec2.md | 236 ++++++++++++++++ docs/developer/manual-localdev.md | 75 ++++++ docs/developer/ubuntu-setup.md | 164 ++++++++++++ docs/development.md | 6 + docs/development_guide.md | 119 +++++++++ docs/priority.md | 52 ++++ docs/production-install.md | 252 ++++++++++++++++++ docs/production.md | 6 + docs/quickstart.md | 7 + docs/quickstart/index.md | 7 + 31 files changed, 2245 insertions(+) create mode 100644 docs/api.md create mode 100644 docs/architecture.md create mode 100644 docs/demo.md create mode 100644 docs/design.md create mode 100644 docs/design/architecture.md create mode 100644 docs/design/database_interfaces.md create mode 100644 docs/design/diagrams/relationships/README.md create mode 100644 docs/design/diagrams/relationships/armada_system.png create mode 100644 docs/design/diagrams/relationships/generate.py create mode 100644 docs/design/diagrams/relationships/images/armada.png create mode 100644 docs/design/diagrams/relationships/images/browser.png create mode 100644 docs/design/diagrams/relationships/images/pulsar.png create mode 100644 docs/design/index.md create mode 100644 docs/design/jobservice/airflow-sequence.pml create mode 100644 docs/design/jobservice/airflow-sequence.svg create mode 100644 docs/design/jobservice/job-service.md create mode 100644 docs/design/jobservice/job-service.svg create mode 100644 docs/design/jobservice/jobservice.pml create mode 100644 docs/design/priority.md create mode 100644 docs/design/relationships_diagram.md create mode 100644 docs/design/scheduler.md create mode 100644 docs/developer/aws-ec2.md create mode 100644 docs/developer/manual-localdev.md create mode 100644 docs/developer/ubuntu-setup.md create mode 100644 docs/development.md create mode 100644 docs/development_guide.md create mode 100644 docs/priority.md create mode 100644 docs/production-install.md create mode 100644 docs/production.md create mode 100644 docs/quickstart.md create mode 100644 docs/quickstart/index.md diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 00000000000..28191663cb3 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,101 @@ +# Armada API + +Armada exposes an API via gRPC or REST. + +## gRPC +The API is defined in `/pkg/api` folder with `*.proto` files as source for all generated code. + +Folder `/pkg/api` also contains generated clients and together with helper methods from `/pkg/client` provides a convenient way to call Armada API from go code. See armadactl code for +[examples](https://github.com/armadaproject/armada/blob/master/cmd/armadactl/cmd/submit.go). + +Following subset of API defined in `/pkg/api` is intended for public use. + +### api.Submit ([definition](https://github.com/armadaproject/armada/blob/master/pkg/api/submit.proto)) + +__/api.Submit/SubmitJobs__ - submitting jobs to be run + +__/api.Submit/CancelJobs__ - cancel jobs + +__/api.Submit/CreateQueue__ - create a new queue + +__/api.Submit/UpdateQueue__ - update an existing queue + +__/api.Submit/DeleteQueue__ - remove queue + +__/api.Submit/GetQueue__ - get information about queue (name, permissions) + +__/api.Submit/GetQueueInfo__ - get information about queued (active jobs, including those currently running) + +### api.Event ([definition](https://github.com/armadaproject/armada/blob/master/pkg/api/submit.proto)) + +__/api.Event/GetJobSetEvents__ - read events of jobs running under particular JobSet + + +### Internal +There are additional API methods defined in proto specifications, which are used by Armada executor and not intended to be used by external users. This API can change in any version. + +- [event.proto](https://github.com/armadaproject/armada/blob/master/pkg/api/event.proto) - methods for event reporting +- [queue.proto](https://github.com/armadaproject/armada/blob/master/pkg/api/queue.proto) - methods related to job leasing by executor +- [usage.proto](https://github.com/armadaproject/armada/blob/master/pkg/api/usage.proto) - methods for reporting of resources usage + +## REST +The REST API only exposes the public part of the gRPC API and it is implemented using [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway). + +Swagger json specification can be found [here](https://github.com/armadaproject/armada/blob/master/pkg/api/api.swagger.json) and is also served by Armada under `my.armada.deployment/api/swagger.json` + +## Authentication + +Both gRPC and REST API support the same set of authentication methods. In the case of gRPC all authentication methods uses `authorization` key in grpc metadata. The REST API use standard http Authorization header (which is translated by grpc-gateway to `authorization` metadata). + +See helm chart [documentation](https://armadaproject.io/helm#Authentication) for different server authentication schemes setup. + +### No Auth +For testing, Armada can be configured to accept no authentication. All operations will use user `anonymous` in this case. + +### OpenId Authentication +When server is configured with OpenID, it will accept authorization header or metadata in the form `Bearer {oauth_token}`. + +### Basic Authentication +For basic authentication API accepts standard authorization header or metadata in the form `basic {base64(user:password)}`. + +### Kerberos +For Kerberos authentication API accepts the same authorization metadata for gRPC as standard Kerberos http SPNEGO authorization headers, the API responds with `WWW-Authenticate` header or metadata. + + +## Permissions + +Armada will determine which actions you are able to perform based on your user's permissions. +These are defined as global or on a per queue basis. + +Below is the list of global Armada permissions (defined [here](https://github.com/armadaproject/armada/blob/master/internal/armada/permissions/permissions.go)): +* `submit_jobs` +* `submit_any_jobs` +* `create_queue` +* `delete_queue` +* `cancel_jobs` +* `cancel_any_jobs` +* `reprioritize_jobs` +* `reprioritize_any_jobs` +* `watch_events` +* `watch_all_events` + +In addition, the following queue-specific permission verbs control what actions can be taken per individual queues (defined [here](https://github.com/armadaproject/armada/blob/master/pkg/client/queue/permission_verb.go)): +* `submit` +* `cancel` +* `reprioritize` +* `watch` + +The table below shows which permissions are required for a user to access each API endpoint (either directly or via a group). +Note queue-specific permission require a user to be bound to a global permission as well (shown as tuples in the table below). + +| Endpoint | Global Permissions | Queue Permissions | +|--------------------|-------------------------|---------------------------------------| +| `SubmitJobs` | `submit_any_jobs` | (`submit_jobs`, `submit`) | +| `CancelJobs` | `cancel_any_jobs` | (`cancel_jobs`, `cancel`) | +| `ReprioritizeJobs` | `reprioritize_any_jobs` | (`reprioritize_jobs`, `reprioritize`) | +| `CreateQueue` | `create_queue` | | +| `UpdateQueue` | `create_queue` | | +| `DeleteQueue` | `delete_queue` | | +| `GetQueue` | | | +| `GetQueueInfo` | `watch_all_events` | (`watch_events`, `watch`) | +| `GetJobSetEvents` | `watch_all_events` | (`watch_events`, `watch`) | diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000000..eacfcad41a8 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,80 @@ +# Architecture + +Armada is designed to manage millions of batch jobs across compute clusters made up of potentially hundreds of thousands of nodes, while providing near-constant uptime. Hence, the Architecture of Armada must be highly resilient and scalable. The current architecture was chosen in early 2022 to achieve these goals while also ensuring new features, e.g., advanced scheduling techniques, can be delivered. + +At a high level, Armada is a so-called data stream system (sometimes referred to as an event sourcing system), for which there are two components responsible for tracking the state of the system: + +* A log-based message broker that stores state transitions durably in order, referred to throughout this document simply as "the log". +* A set of databases, each deriving its state from the log (but are otherwise mutually independent) and storing a different so-called materialised view of the state of the system. + +The log is a publish-subscribe system consisting of multiple topics to which messages can be published. Those messages are eventually delivered to all subscribers of the topic. Important properties of the log are: + +* Durability: Messages published to the log are stored in a durable manner, i.e., they are not lost in case of up to x node failures, where x is a tuneable parameter. +* Ordering: All subscribers of a topic see messages in the same order as they were published in, and replaying messages on a topic always results in the same message order. Further, all messages on the same topic are annotated with a message id that is monotonically increasing within each topic. + +In Armada, the log is implemented using Apache Pulsar. + +In a data stream system, the log is the source of truth and the databases an optimisation to simplify querying – since the databases can be re-constructed by replaying messages from the log, if the log was replayed for each query, although highly unpractical, the databases could be omitted. For example, in Armada there are separate PostgreSQL databases for storing jobs to be scheduled and the jobs to be shown in the UI, Lookout. Both of these derive their state from the log but are otherwise independent. + +To change the state of the system, a message (e.g., corresponding to a job being submitted) is published to the log. Later, that message is picked up by a log processor, which updates some database accordingly (in the case of a job being submitted, by storing the new job in the database). Hence, the log serialises state transitions and the database is a materialised view of part of the state of the system, as derived from the state transitions submitted to the log. In effect, a data stream system is a bespoke distributed database with the log acting as the transaction log. + +This approach has several benefits: + +* Resiliency towards bursts of high load: Because the log buffers state transitions, the components reading from the log and acting on those transitions are not directly affected by incoming requests. +* Simplicity and extensibility: Adding new materialised views (e.g., for a new dashboard) can be accomplished by adding a new subscriber to the log. This new subscriber has the same source of truth as all others (i.e., the log) but is loosely coupled to those components; adding or removing views does not affect other components of the system. +* Consistency: When storing state across several independent databases, those databases are guaranteed to eventually be consistent; there is no failure scenario where the different databases become permanently inconsistent, thus requiring a human to manually reconcile them (assuming acting on state transitions is idempotent). + +However, the approach also has some drawbacks: + +* Eventual consistency: Because each database is updated from the log independently, they do not necessarily represent the state of the system at the same point of time. For example, a job may be written to the scheduler database (thus making it eligible for scheduling) before it shows up in the UI. +* Timeliness: Because databases are updated from the log asynchronously, there may be a lag between a message being published and the system being updated to reflect the change (e.g., a submitted job may not show up in the UI immediately). + +## System overview + +Besides the log, Armada consists of the following components: + +* Submit API: Clients (i.e., users) connect to this API to request state transitions (e.g., submitting jobs or updating job priorities) and each such state transition is communicated to the rest of the system by writing to the log (more detail on this below). +* Streams API: Clients connect to this API to subscribe to log messages for a particular set of jobs. Armada components can receive messages either via this API or directly from the log, but users have to go via the streams API to isolate them from internal messages. +* Scheduler: A log processor responsible for maintaining a global view of the system and preempting and scheduling jobs. Preemption and scheduling decisions are communicated to he rest of the system by writing to the log. +* Executors: Each executor is responsible for one Kubernetes worker cluster and is the component that communicates between the Armada scheduler and the Kubernetes API of the cluster it is responsible for. +* Lookout: The web UI showing the current state of the system. Lookout maintains its views by reading log messages to populate its database. + +### Job submission logic + +Here, we outline the sequence of actions resulting from submitting a job. + +1. A client submits a job to the submit-query API, which is composed of a Kubernetes podspec and some Armada-specific metadata (e.g., the priority of the job). +2. The submit API authenticates and authorizes the user, validates the submitted job, and, if valid, submits the job spec. to the log. The submit API annotates each job with a randomly generated UUID that uniquely identifies the job. This UUID is returned to the user. +3. The scheduler receives the job spec. and stores it in-memory (discarding any data it doesn't need, such as the pod spec.). The scheduler runs periodically, at which point it schedules queued jobs. At the start of each scheduling run, the scheduler queries each executor for its available resources. The scheduler uses this information in making scheduling decisions. When the scheduler assigns a job to an executor, it submits a message to the log indicating this state transition. It also updates its in-memory storage immediately to reflect the change (to avoid scheduling the same job twice). +4. A log processor receives the message indicating the job was scheduled, and writes this decision to a database acting as the interface between the scheduler and the executor. +5. Periodically, each executor queries the database for the list of jobs it should be running. It compares that list with the list of jobs it is actually running and makes changes necessary to reconcile any differences. +6. When a job has finished, the executor responsible for running the job informs the scheduler, which on its behalf submits a "job finished" message to the log. The same log processor as in step 4. updates its database to reflect that the job has finished. + +### Streams API + +Armada does not maintain a user-queryable database of the current state of the system. This is by design to avoid overloading the system with connections. For example, say there is one million active jobs in the system and that there are clients who want to track the state of all of those jobs. With a current-state-of-the-world database, those client would need to resort to polling that database to catch any updates, thus opening a total of one million connections to the database, which, while not impossible to manage, would pose significant challenges. + +Instead, users are expected to be notified of updates to their jobs via an event stream (i.e., the streams API), where a client opens a single connection for all jobs in a so-called job set over which all state transitions are streamed as they happen. This approach is highly scalable since data is only sent when something happens and since a single connection that contain updates for thousands of jobs. Users who want to maintain a view of their jobs are thus responsible for maintaining that view themselves by subscribing to events. + +## Notes on consistency + +The data stream approach taken by Armada is not the only way to maintain consistency across views. Here, we compare this approach with the two other possible solutions. + +Armada stores its state across several databases. Whenever Armada receives an API call to update its state, all those databases need to be updated. However, if each database were to be updated independently it is possible for some of those updates to succeed while others fail, leading to an inconsistent application state. It would require complex logic to detect and correct for such partial failures. However, even with such logic we could not guarantee that the application state is consistent; if Armada crashes before it has had time to correct for the partial failure the application may remain in an inconsistent state. + +There are three commonly used approaches to address this issue: + +* Store all state in a single database with support for transactions. Changes are submitted atomically and are rolled back in case of failure; there are no partial failures. +* Distributed transaction frameworks (e.g., X/Open XA), which extend the notation of transactions to operations involving several databases. +* Ordered idempotent updates. + +The first approach results in tight coupling between components and would limit us to a single database technology. Adding a new component (e.g., a new dashboard) could break existing component since all operations part of the transaction are rolled back if one fails. The second approach allows us to use multiple databases (as long as they support the distributed transaction framework), but components are still tightly coupled since they have to be part of the same transaction. Further, there are performance concerns associated with these options, since transactions may not be easily scalable. Hence, we use the third approach, which we explain next. + +First, note that if we can replay the sequence of state transitions that led to the current state, in case of a crash we can recover the correct state by truncating the database and replaying all transitions from the beginning of time. Because operations are ordered, this always results in the same end state. If we also, for each database, store the id of the most recent transition successfully applied to that database, we only need to replay transitions more recent than that. This saves us from having to start over from a clean database; because we know where we left off we can keep going from there. For this to work, we need transactions but not distributed transactions. Essentially, applying a transition already written to the database results in a no-op, i.e., the updates are idempotent (meaning that applying the same update twice has the same effect as applying it once). + +The two principal drawbacks of this approach are: + +* Eventual consistency: Whereas the first two approaches result in a system that is always consistent, with the third approach, because databases are updated independently, there will be some replication lag during which some part of the state may be inconsistent. +* Timeliness: There is some delay between submitting a change and that change being reflected in the application state. + +Working around eventual consistency requires some care, but is not impossible. For example, it is fine for the UI to show the a job as "running" for a few seconds after the job has finished before showing "completed". Regarding timeliness, it is not a problem if there is a few seconds delay between a job being submitted and the job being considered for queueing. However, poor timeliness may lead to clients (i.e., the entity submitting jobs to the system) not being able to read their own writes for some time, which may lead to confusion (i.e., there may be some delay between a client submitting a job a that job showing as "pending"). This issue can be worked around by storing the set of submitted jobs in-memory either at the client or at the API endpoint. diff --git a/docs/demo.md b/docs/demo.md new file mode 100644 index 00000000000..424dc80bab0 --- /dev/null +++ b/docs/demo.md @@ -0,0 +1,144 @@ +# Armada Demo + +
+ +
+ +> This video demonstrates the use of Armadactl, Armada Lookout UI, and Apache Airflow. + +This guide will show you how to take a quick test drive of an Armada +instance already deployed to AWS EKS. + +## EKS + +The Armada UI (lookout) can be found at this URL: + +- [https://ui.demo.armadaproject.io](https://ui.demo.armadaproject.io) + +## Local prerequisites + +- Git +- Go 1.20 + +## Obtain the armada source +Clone [this](https://github.com/armadaproject/armada) repository: + +```bash +git clone https://github.com/armadaproject/armada.git +cd armada +``` + +All commands are intended to be run from the root of the repository. + +## Setup an easy-to-use alias +If you are on a Windows System, use a linux-supported terminal to run this command, for example [Git Bash](https://git-scm.com/downloads) or [Hyper](https://hyper.is/) +```bash +alias armadactl='go run cmd/armadactl/main.go --armadaUrl armada.demo.armadaproject.io:443' +``` + +## Create queues and jobs +Create queues, submit some jobs, and monitor progress: + +### Queue Creation +Use a unique name for the queue. Make sure you remember it for the next steps. +```bash +armadactl create queue $QUEUE_NAME --priorityFactor 1 +armadactl create queue $QUEUE_NAME --priorityFactor 2 +``` + +For queues created in this way, user and group owners of the queue have permissions to: +- submit jobs +- cancel jobs +- reprioritize jobs +- watch queue + +For more control, queues can be created via `armadactl create`, which allows for setting specific permission; see the following example. + +```bash +armadactl create -f ./docs/quickstart/queue-a.yaml +armadactl create -f ./docs/quickstart/queue-b.yaml +``` + +Make sure to manually edit both of these `yaml` files using a code or text editor before running the commands above. + +``` +name: $QUEUE_NAME +``` + +### Job Submission +``` +armadactl submit ./docs/quickstart/job-queue-a.yaml +armadactl submit ./docs/quickstart/job-queue-b.yaml +``` + +Make sure to manually edit both of these `yaml` files using a code or text editor before running the commands above. +``` +queue: $QUEUE_NAME +``` + +### Monitor Job Progress + +```bash +armadactl watch $QUEUE_NAME job-set-1 +``` +```bash +armadactl watch $QUEUE_NAME job-set-1 +``` + +Try submitting lots of jobs and see queues get built and processed: + +#### Windows (using Git Bash): + +Use a text editor of your choice. +Copy and paste the following lines into the text editor: +``` +#!/bin/bash + +for i in {1..50} +do + armadactl submit ./docs/quickstart/job-queue-a.yaml + armadactl submit ./docs/quickstart/job-queue-b.yaml +done +``` +Save the file with a ".sh" extension (e.g., myscript.sh) in the root directory of the project. +Open Git Bash, navigate to the project's directory using the 'cd' command, and then run the script by typing ./myscript.sh and pressing Enter. + +#### Linux: + +Open a text editor (e.g., Nano or Vim) in the terminal and create a new file by running: nano myscript.sh (replace "nano" with your preferred text editor if needed). +Copy and paste the script content from above into the text editor. +Save the file and exit the text editor. +Make the script file executable by running: chmod +x myscript.sh. +Run the script by typing ./myscript.sh in the terminal and pressing Enter. + +#### macOS: + +Follow the same steps as for Linux, as macOS uses the Bash shell by default. +With this approach, you create a shell script file that contains your multi-line script, and you can run it as a whole by executing the script file in the terminal. + +## Observing job progress + +CLI: + +```bash +$ armadactl watch queue-a job-set-1 +Watching job set job-set-1 +Nov 4 11:43:36 | Queued: 0, Leased: 0, Pending: 0, Running: 0, Succeeded: 0, Failed: 0, Cancelled: 0 | event: *api.JobSubmittedEvent, job id: 01drv3mey2mzmayf50631tzp9m +Nov 4 11:43:36 | Queued: 1, Leased: 0, Pending: 0, Running: 0, Succeeded: 0, Failed: 0, Cancelled: 0 | event: *api.JobQueuedEvent, job id: 01drv3mey2mzmayf50631tzp9m +Nov 4 11:43:36 | Queued: 1, Leased: 0, Pending: 0, Running: 0, Succeeded: 0, Failed: 0, Cancelled: 0 | event: *api.JobSubmittedEvent, job id: 01drv3mf7b6fd1rraeq1f554fn +Nov 4 11:43:36 | Queued: 2, Leased: 0, Pending: 0, Running: 0, Succeeded: 0, Failed: 0, Cancelled: 0 | event: *api.JobQueuedEvent, job id: 01drv3mf7b6fd1rraeq1f554fn +Nov 4 11:43:38 | Queued: 1, Leased: 1, Pending: 0, Running: 0, Succeeded: 0, Failed: 0, Cancelled: 0 | event: *api.JobLeasedEvent, job id: 01drv3mey2mzmayf50631tzp9m +Nov 4 11:43:38 | Queued: 0, Leased: 2, Pending: 0, Running: 0, Succeeded: 0, Failed: 0, Cancelled: 0 | event: *api.JobLeasedEvent, job id: 01drv3mf7b6fd1rraeq1f554fn +Nov 4 11:43:38 | Queued: 0, Leased: 1, Pending: 1, Running: 0, Succeeded: 0, Failed: 0, Cancelled: 0 | event: *api.JobPendingEvent, job id: 01drv3mey2mzmayf50631tzp9m +Nov 4 11:43:38 | Queued: 0, Leased: 0, Pending: 2, Running: 0, Succeeded: 0, Failed: 0, Cancelled: 0 | event: *api.JobPendingEvent, job id: 01drv3mf7b6fd1rraeq1f554fn +Nov 4 11:43:41 | Queued: 0, Leased: 0, Pending: 1, Running: 1, Succeeded: 0, Failed: 0, Cancelled: 0 | event: *api.JobRunningEvent, job id: 01drv3mf7b6fd1rraeq1f554fn +Nov 4 11:43:41 | Queued: 0, Leased: 0, Pending: 0, Running: 2, Succeeded: 0, Failed: 0, Cancelled: 0 | event: *api.JobRunningEvent, job id: 01drv3mey2mzmayf50631tzp9m +Nov 4 11:44:17 | Queued: 0, Leased: 0, Pending: 0, Running: 1, Succeeded: 1, Failed: 0, Cancelled: 0 | event: *api.JobSucceededEvent, job id: 01drv3mf7b6fd1rraeq1f554fn +Nov 4 11:44:26 | Queued: 0, Leased: 0, Pending: 0, Running: 0, Succeeded: 2, Failed: 0, Cancelled: 0 | event: *api.JobSucceededEvent, job id: 01drv3mey2mzmayf50631tzp9m +``` + +Web UI: + +Open [https://ui.demo.armadaproject.io](https://ui.demo.armadaproject.io) in your browser. + +![Lookout UI](./quickstart/img/lookout.png "Lookout UI") diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 00000000000..afd8210019e --- /dev/null +++ b/docs/design.md @@ -0,0 +1,72 @@ +# System overview + +This document is meant to be an overview of Armada for new users. We cover the architecture of Armada, show how jobs are represented, and explain how jobs are queued and scheduled. + +If you just want to learn how to submit jobs to Armada, see: + +- [User guide](./user.md) + +## Architecture + +Armada consists of two main components: +- The Armada server, which is responsible for accepting jobs from users and deciding in what order, and on which Kubernetes cluster, jobs should run. Users submit jobs to the Armada server through the `armadactl` command-line utility or via a gRPC or REST API. +- The Armada executor, of which there is one instance running in each Kubernetes cluster that Armada is connected to. Each Armada executor instance regularly notifies the server of how much spare capacity it has available and requests jobs to run. Users of Armada never interact with the executor directly. + +All state relating to the Armada server is stored in [Redis](https://redis.io/), which may use replication combined with failover for redundancy. Hence, the Armada server is itself stateless and is easily replicated by running multiple independent instances. Both the server and the executors are intended to be run in Kubernetes pods. We show a diagram of the architecture below. + +![How Armada works](./assets/img/batch-api.svg) + +### Job leasing + +To avoid jobs being lost if a cluster (or cluster executor) becomes unavailable, each job assigned to an executor has an associated timeout. Armada executors are required to check in with the server regularly and, if an executor responsible for running a particular job fails to check in within that timeout, the server will re-schedule the job over another executor. + +## Jobs and job sets + +A job is the most basic unit of work in Armada, and is represented by a Kubernetes pod specification (podspec) with additional metadata specific to Armada. Armada handles creating, running, and removing containers as necessary for each job. Hence, Armada is essentially a system for managing the life cycle of a set of containerised applications representing a batch job. + +The Armada workflow is: + +1. Create a job specification, which is a Kubernetes podspec with a few additional metadata fields. +2. Submit the job specification to one of Armada's job queues using the `armadactl` CLI utility or through the Armada gRPC or REST API. + +For example, a job that sleeps for 60 seconds could be represented by the following yaml file. + +```yaml +queue: test +jobSetId: set1 +jobs: + - priority: 0 + podSpecs: + - terminationGracePeriodSeconds: 0 + restartPolicy: Never + containers: + - name: sleep + imagePullPolicy: IfNotPresent + image: busybox:latest + args: + - sleep + - 60s + resources: + limits: + memory: 64Mi + cpu: 150m + requests: + memory: 64Mi + cpu: 150m +``` + +In the above yaml snippet, `podSpec` is a Kubernetes podspec, which consists of one or more containers that contain the user code to be run. In addition, the job specification (jobspec) contains metadata fields specific to Armada: + +- `queue`: which of the available job queues the job should be submitted to. +- `priority`: the job priority (lower values indicate higher priority). +- `jobSetId`: jobs with the same `jobSetId` can be followed and cancelled in a single operation. The `jobSetId` has no impact on scheduling. + +Queues and scheduling is explained in more detail below. + +For more examples, including of jobs spanning multiple nodes, see the [user guide](./user.md). + +### Job events + +A job event is generated whenever the state of a job changes (e.g., when changing from submitted to running or from running to completed) and is a timestamped message containing event-specific information (e.g., an exit code for a completed job). All events generated by jobs part of the same job set are grouped together and published via a [Redis stream](https://redis.io/topics/streams-intro). There are unique streams for each job set to facilitate subscribing only to events generated by jobs in a particular set, which can be done via the Armada API. + +Armada records all events necessary to reconstruct the state of each job and, after a job has been completed, the only information retained about the job is the events generated by it. diff --git a/docs/design/architecture.md b/docs/design/architecture.md new file mode 100644 index 00000000000..eacfcad41a8 --- /dev/null +++ b/docs/design/architecture.md @@ -0,0 +1,80 @@ +# Architecture + +Armada is designed to manage millions of batch jobs across compute clusters made up of potentially hundreds of thousands of nodes, while providing near-constant uptime. Hence, the Architecture of Armada must be highly resilient and scalable. The current architecture was chosen in early 2022 to achieve these goals while also ensuring new features, e.g., advanced scheduling techniques, can be delivered. + +At a high level, Armada is a so-called data stream system (sometimes referred to as an event sourcing system), for which there are two components responsible for tracking the state of the system: + +* A log-based message broker that stores state transitions durably in order, referred to throughout this document simply as "the log". +* A set of databases, each deriving its state from the log (but are otherwise mutually independent) and storing a different so-called materialised view of the state of the system. + +The log is a publish-subscribe system consisting of multiple topics to which messages can be published. Those messages are eventually delivered to all subscribers of the topic. Important properties of the log are: + +* Durability: Messages published to the log are stored in a durable manner, i.e., they are not lost in case of up to x node failures, where x is a tuneable parameter. +* Ordering: All subscribers of a topic see messages in the same order as they were published in, and replaying messages on a topic always results in the same message order. Further, all messages on the same topic are annotated with a message id that is monotonically increasing within each topic. + +In Armada, the log is implemented using Apache Pulsar. + +In a data stream system, the log is the source of truth and the databases an optimisation to simplify querying – since the databases can be re-constructed by replaying messages from the log, if the log was replayed for each query, although highly unpractical, the databases could be omitted. For example, in Armada there are separate PostgreSQL databases for storing jobs to be scheduled and the jobs to be shown in the UI, Lookout. Both of these derive their state from the log but are otherwise independent. + +To change the state of the system, a message (e.g., corresponding to a job being submitted) is published to the log. Later, that message is picked up by a log processor, which updates some database accordingly (in the case of a job being submitted, by storing the new job in the database). Hence, the log serialises state transitions and the database is a materialised view of part of the state of the system, as derived from the state transitions submitted to the log. In effect, a data stream system is a bespoke distributed database with the log acting as the transaction log. + +This approach has several benefits: + +* Resiliency towards bursts of high load: Because the log buffers state transitions, the components reading from the log and acting on those transitions are not directly affected by incoming requests. +* Simplicity and extensibility: Adding new materialised views (e.g., for a new dashboard) can be accomplished by adding a new subscriber to the log. This new subscriber has the same source of truth as all others (i.e., the log) but is loosely coupled to those components; adding or removing views does not affect other components of the system. +* Consistency: When storing state across several independent databases, those databases are guaranteed to eventually be consistent; there is no failure scenario where the different databases become permanently inconsistent, thus requiring a human to manually reconcile them (assuming acting on state transitions is idempotent). + +However, the approach also has some drawbacks: + +* Eventual consistency: Because each database is updated from the log independently, they do not necessarily represent the state of the system at the same point of time. For example, a job may be written to the scheduler database (thus making it eligible for scheduling) before it shows up in the UI. +* Timeliness: Because databases are updated from the log asynchronously, there may be a lag between a message being published and the system being updated to reflect the change (e.g., a submitted job may not show up in the UI immediately). + +## System overview + +Besides the log, Armada consists of the following components: + +* Submit API: Clients (i.e., users) connect to this API to request state transitions (e.g., submitting jobs or updating job priorities) and each such state transition is communicated to the rest of the system by writing to the log (more detail on this below). +* Streams API: Clients connect to this API to subscribe to log messages for a particular set of jobs. Armada components can receive messages either via this API or directly from the log, but users have to go via the streams API to isolate them from internal messages. +* Scheduler: A log processor responsible for maintaining a global view of the system and preempting and scheduling jobs. Preemption and scheduling decisions are communicated to he rest of the system by writing to the log. +* Executors: Each executor is responsible for one Kubernetes worker cluster and is the component that communicates between the Armada scheduler and the Kubernetes API of the cluster it is responsible for. +* Lookout: The web UI showing the current state of the system. Lookout maintains its views by reading log messages to populate its database. + +### Job submission logic + +Here, we outline the sequence of actions resulting from submitting a job. + +1. A client submits a job to the submit-query API, which is composed of a Kubernetes podspec and some Armada-specific metadata (e.g., the priority of the job). +2. The submit API authenticates and authorizes the user, validates the submitted job, and, if valid, submits the job spec. to the log. The submit API annotates each job with a randomly generated UUID that uniquely identifies the job. This UUID is returned to the user. +3. The scheduler receives the job spec. and stores it in-memory (discarding any data it doesn't need, such as the pod spec.). The scheduler runs periodically, at which point it schedules queued jobs. At the start of each scheduling run, the scheduler queries each executor for its available resources. The scheduler uses this information in making scheduling decisions. When the scheduler assigns a job to an executor, it submits a message to the log indicating this state transition. It also updates its in-memory storage immediately to reflect the change (to avoid scheduling the same job twice). +4. A log processor receives the message indicating the job was scheduled, and writes this decision to a database acting as the interface between the scheduler and the executor. +5. Periodically, each executor queries the database for the list of jobs it should be running. It compares that list with the list of jobs it is actually running and makes changes necessary to reconcile any differences. +6. When a job has finished, the executor responsible for running the job informs the scheduler, which on its behalf submits a "job finished" message to the log. The same log processor as in step 4. updates its database to reflect that the job has finished. + +### Streams API + +Armada does not maintain a user-queryable database of the current state of the system. This is by design to avoid overloading the system with connections. For example, say there is one million active jobs in the system and that there are clients who want to track the state of all of those jobs. With a current-state-of-the-world database, those client would need to resort to polling that database to catch any updates, thus opening a total of one million connections to the database, which, while not impossible to manage, would pose significant challenges. + +Instead, users are expected to be notified of updates to their jobs via an event stream (i.e., the streams API), where a client opens a single connection for all jobs in a so-called job set over which all state transitions are streamed as they happen. This approach is highly scalable since data is only sent when something happens and since a single connection that contain updates for thousands of jobs. Users who want to maintain a view of their jobs are thus responsible for maintaining that view themselves by subscribing to events. + +## Notes on consistency + +The data stream approach taken by Armada is not the only way to maintain consistency across views. Here, we compare this approach with the two other possible solutions. + +Armada stores its state across several databases. Whenever Armada receives an API call to update its state, all those databases need to be updated. However, if each database were to be updated independently it is possible for some of those updates to succeed while others fail, leading to an inconsistent application state. It would require complex logic to detect and correct for such partial failures. However, even with such logic we could not guarantee that the application state is consistent; if Armada crashes before it has had time to correct for the partial failure the application may remain in an inconsistent state. + +There are three commonly used approaches to address this issue: + +* Store all state in a single database with support for transactions. Changes are submitted atomically and are rolled back in case of failure; there are no partial failures. +* Distributed transaction frameworks (e.g., X/Open XA), which extend the notation of transactions to operations involving several databases. +* Ordered idempotent updates. + +The first approach results in tight coupling between components and would limit us to a single database technology. Adding a new component (e.g., a new dashboard) could break existing component since all operations part of the transaction are rolled back if one fails. The second approach allows us to use multiple databases (as long as they support the distributed transaction framework), but components are still tightly coupled since they have to be part of the same transaction. Further, there are performance concerns associated with these options, since transactions may not be easily scalable. Hence, we use the third approach, which we explain next. + +First, note that if we can replay the sequence of state transitions that led to the current state, in case of a crash we can recover the correct state by truncating the database and replaying all transitions from the beginning of time. Because operations are ordered, this always results in the same end state. If we also, for each database, store the id of the most recent transition successfully applied to that database, we only need to replay transitions more recent than that. This saves us from having to start over from a clean database; because we know where we left off we can keep going from there. For this to work, we need transactions but not distributed transactions. Essentially, applying a transition already written to the database results in a no-op, i.e., the updates are idempotent (meaning that applying the same update twice has the same effect as applying it once). + +The two principal drawbacks of this approach are: + +* Eventual consistency: Whereas the first two approaches result in a system that is always consistent, with the third approach, because databases are updated independently, there will be some replication lag during which some part of the state may be inconsistent. +* Timeliness: There is some delay between submitting a change and that change being reflected in the application state. + +Working around eventual consistency requires some care, but is not impossible. For example, it is fine for the UI to show the a job as "running" for a few seconds after the job has finished before showing "completed". Regarding timeliness, it is not a problem if there is a few seconds delay between a job being submitted and the job being considered for queueing. However, poor timeliness may lead to clients (i.e., the entity submitting jobs to the system) not being able to read their own writes for some time, which may lead to confusion (i.e., there may be some delay between a client submitting a job a that job showing as "pending"). This issue can be worked around by storing the set of submitted jobs in-memory either at the client or at the API endpoint. diff --git a/docs/design/database_interfaces.md b/docs/design/database_interfaces.md new file mode 100644 index 00000000000..0003e7b00e7 --- /dev/null +++ b/docs/design/database_interfaces.md @@ -0,0 +1,187 @@ +# Armada Database Interfaces + +## Problem Description + +Open source projects should not be hard coded to a particular Database. Armada currently only allows users to use Postgres. This project is to build interfaces around our connections to Postgres so we can allow other databases. + +## Solution + +1. Introduce base common database interfaces that can be shared reused by all components (Lookout, Scheduler, Scheduler Ingester). +2. Add interfaces that abstracts the hardcoded Postgres configuration. +3. Add interfaces around `pgx` structs. + +### Functional Specification (API Description) + +#### Database Connection + +Most of the components (Lookout, Scheduler, Scheduler Ingester) rely on [PostgresConfig](https://github.com/armadaproject/armada/blob/master/internal/armada/configuration/types.go#L294) to connect to external databases, we can avoid hardcoding the configuration of those components to use `PostgresConfig` but defining a generic `DatabaseConfig` interface that's when implemented will provide those components with the necessary details to connect to databases. + + /** + Components configuration (e.g. LookoutConfiguration) can now make use of this interface instead of hardcoding PostgresConfig. + */ + type DatabaseConfig interface { + GetMaxOpenConns() int + GetMaxIdleConns() int + GetConnMaxLifetime() time.Duration + GetConnectionString() string + } + + type DatabaseConnection interface { + GetConnection() (*sql.DB, error) + GetConfig() DatabaseConfig + } + +The existing configurations can then be tweaked to use the new generic `DatabaseConfig` interface instead of hardcoding `PostgresConfig` + + type LookoutConfiguration struct { + Postgres PostgresConfig // this can be replaced with the new Database property + Database DatabaseConfig // new property + } + +#### Database Communication + +Currently, most of the Armada components make use of the `github.com/jackc/pgx` Postgres client which provides APIs to interact exclusively with Postgres databases, this makes Armada tightly coupled with Postgres and makes it impossible to use other SQL dialects (e.g. MySQL). + +A way to fix this would be to design database-agnostic interfaces that can abstract away the existing Postgres core implementation (pgx), and then implement adapters around `pgx` that implement those interfaces. This will allow for having a high level abstraction API for interacting with databases while maintaining the existing Postgres core implementation. +To accomplish this, we will need to define interfaces for the following features: + +1. Connection Handler + + // DatabaseConn represents a connection handler interface that provides methods for managing the open connection, executing queries, and starting transactions. + type DatabaseConn interface { + // Close closes the database connection. It returns any error encountered during the closing operation. + Close(context.Context) error + + // Ping pings the database to check the connection. It returns any error encountered during the ping operation. + Ping(context.Context) error + + // Exec executes a query that doesn't return rows. It returns any error encountered. + Exec(context.Context, string, ...any) (any, error) + + // Query executes a query that returns multiple rows. It returns a DatabaseRows interface that allows you to iterate over the result set, and any error encountered. + Query(context.Context, string, ...any) (DatabaseRows, error) + + // QueryRow executes a query that returns one row. It returns a DatabaseRow interface representing the result row, and any error encountered. + QueryRow(context.Context, string, ...any) DatabaseRow + + // BeginTx starts a transcation with the given DatabaseTxOptions, or returns an error if any occured. + BeginTx(context.Context, DatabaseTxOptions) (DatabaseTx, error) + + // BeginTxFunc starts a transaction and executes the given function within the transaction. It the function runs successfuly, BeginTxFunc commits the transaction, otherwise it rolls back and return an errorr. + BeginTxFunc(context.Context, DatabaseTxOptions, func(DatabaseTx) error) error + } + +2. Connection Pool + + // DatabasePool represents a database connection pool interface that provides methods for acquiring and managing database connections. + type DatabasePool interface { + // Acquire acquires a database connection from the pool. It takes a context and returns a DatabaseConn representing the acquired connection and any encountered error. + Acquire(context.Context) (DatabaseConn, error) + + // Ping pings the database to check the connection. It returns any error encountered during the ping operation. + Ping(context.Context) error + + // Close closes the database connection. It returns any error encountered during the closing operation. + Close() + + // Exec executes a query that doesn't return rows. It returns any error encountered. + Exec(context.Context, string, ...any) (any, error) + + // Query executes a query that returns multiple rows. It returns a DatabaseRows interface that allows you to iterate over the result set, and any error encountered. + Query(context.Context, string, ...any) (DatabaseRows, error) + + // BeginTx starts a transcation with the given DatabaseTxOptions, or returns an error if any occured. + BeginTx(context.Context, DatabaseTxOptions) (DatabaseTx, error) + + // BeginTxFunc starts a transaction and executes the given function within the transaction. It the function runs successfuly, BeginTxFunc commits the transaction, otherwise it rolls back and return an errorr. + BeginTxFunc(context.Context, DatabaseTxOptions, func(DatabaseTx) error) error + } + +3. Transaction + + // DatabaseTx represents a database transaction interface that provides methods for executing queries, managing transactions, and performing bulk insertions. + type DatabaseTx interface { + // Exec executes a query that doesn't return rows. It returns any error encountered. + Exec(context.Context, string, ...any) (any, error) + + // Query executes a query that returns multiple rows. It returns a DatabaseRows interface that allows you to iterate over the result set, and any error encountered. + Query(context.Context, string, ...any) (DatabaseRows, error) + + // QueryRow executes a query that returns one row. It returns a DatabaseRow interface representing the result row, and any error encountered. + QueryRow(context.Context, string, ...any) DatabaseRow + + // CopyFrom performs a bulk insertion of data into a specified table. It accepts the table name, column names, and a slice of rows representing the data to be inserted. It returns the number of rows inserted and any error encountered. + CopyFrom(ctx context.Context, tableName string, columnNames []string, rows [][]any) (int64, error) + + // Commit commits the transaction. It returns any error encountered during the commit operation. + Commit(context.Context) error + + // Rollback rolls back the transaction. It returns any error encountered during the rollback operation. + Rollback(context.Context) error + } + +4. Result Row + + // DatabaseRow represents a single row in a result set. + type DatabaseRow interface { + // Scan reads the values from the current row into dest values positionally. It returns an error if any occured during the read operation. + Scan(dest ...any) error + } + +5. Resultset + + // DatabaseRows represents an interator over a result set. + type DatabaseRows interface { + // Close closes the result set. + Close() error + + // Next moves the iterator to the next row in the result set, it returns false if the result set is exhausted, otherwise true. + Next() bool + + // Err returns the error, if any, encountered during iteration over the result set. + Err() error + + // Scan reads the values from the current row into dest values positionally. It returns an error if any occured during the read operation. + Scan(dest ...any) error + } + +### Implementation Plan + +Designing interfaces that can remove the coupling between Armada and Postgres while maintaining the existing core Postgres implementation is a requirement. + +To fullfill this requirement, we can implement adapters around the `pgx` client so that it also implements the interfaces defined above. + +For example, an adapter can be implemented for `pgxpool.Pool` so that it can be used with `DatabasePool`: + + type PostgresPoolAdapter struct { + *pgxpool.Pool + } + + func (p PostgresPoolAdapter) Exec(ctx context.Context, sql string, args ...any) (any, error) { + return p.Pool.Exec(ctx, sql, args) + } + + func (p PostgresPoolAdapter) BeginTxFunc(ctx context.Context, opts dbtypes.DatabaseTxOptions, action func(dbtypes.DatabaseTx) error) error { + tx, err := p.Pool.BeginTx(ctx, pgx.TxOptions{ + IsoLevel: pgx.TxIsoLevel(opts.Isolation), + DeferrableMode: opts.DeferrableMode, + AccessMode: pgx.TxAccessMode(opts.AccessMode), + }) + + if err != nil { + return err + } + + // PostgresTrxAdapter is the Postgres adapter for DatabaseTx interface + if err := action(PostgresTrxAdapter{Tx: tx}); err != nil { + return tx.Rollback(ctx) + } + + return tx.Commit(ctx) + } + +The example above showcases the implementation of a Postgres connection pool adapter, this example implements the `DatabasePool` interface (the rest of the methods can be implemented similarly to `Exec` and `BeginTxFunc`). + +This allows the components that make use `pgxpool.Pool` (e.g. Lookout) to switch to using `DatabasePool` which underneath can make use of `pgxpool.Pool` (or any other `DatabasePool` implementation) without making any changes to the core Postgres implementation. + +To support new SQL dialects, we can simply introduce adapters that implement the interfaces, as well as introduce some level of flexibility into the configuration of components to allow choosing which dialect we want to use. diff --git a/docs/design/diagrams/relationships/README.md b/docs/design/diagrams/relationships/README.md new file mode 100644 index 00000000000..1360c3f904a --- /dev/null +++ b/docs/design/diagrams/relationships/README.md @@ -0,0 +1,25 @@ +# Diagrams of Armada Architecture + +## Generating the Diagram + +To generate this diagram, you can use the following command: + +```bash +# install graphviz +sudo apt-get install graphviz + +# then install diagrams from pip +pip install diagrams + +# then run the following command to generate the diagram +python3 generate.py +``` + +To find out more about the diagrams library, see https://diagrams.mingrammer.com/ +To find out more about the graphviz library, see https://graphviz.org/ + + + + + + diff --git a/docs/design/diagrams/relationships/armada_system.png b/docs/design/diagrams/relationships/armada_system.png new file mode 100644 index 0000000000000000000000000000000000000000..f6c953403610d69451b02868e4a204f5f8e58dad GIT binary patch literal 178826 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
IzR8tb);i7j~?z3{46h9jjUmTEQ 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

dmn;XJkwT4><|vtv0+S6mnoSI(jK? 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

cV8P3yK&>f%0%`RTo{}0^5 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 Edge(color="black") >> server + + # submit api talks to Pulsar + server >> Edge(color="red") >> pulsar + + # Pulsar talks to each of the ingesters + pulsar >> Edge(color="red") >> lookout_ingester + pulsar >> Edge(color="red") >> scheduler_ingester + pulsar >> Edge(color="red") >> event_ingerster + + # make Postgres blue, redis orange + # lookout and scheduler ingesters talk to postgres + # the other ingesters talk to redis + lookout_ingester >> Edge(color="blue") >> postgres_lookout + scheduler_ingester >> Edge(color="blue") >> postgres_scheduler + + event_ingerster >> Edge(color="orange") >> redis_events + + # the Postgres scheduler talks to the scheduler and executor api + postgres_scheduler >> Edge(color="blue") >> scheduler + + # the scheduler talks to Pulsar + scheduler >> Edge(color="red") >> pulsar + + executor >> Edge(color="blue") >> k8s_api + k8s_api >> Edge(color="blue") >> executor + + executor2 >> Edge(color="blue") >> k8s_api2 + k8s_api2 >> Edge(color="blue") >> executor2 + + # The binoculars in every cluster talks to k8s, and + # then talks directly to the lookout UI + k8s_api >> Edge(color="blue") >> binoculars + binoculars >> Edge(color="black") >> lookoutUI + + k8s_api2 >> Edge(color="blue") >> binoculars2 + binoculars2 >> Edge(color="black") >> lookoutUI + + # Lookout API gets its data from postgres + # and passes it to the lookout UI + postgres_lookout >> Edge(color="blue") >> lookout_api + lookout_api >> Edge(color="black") >> lookoutUI + + # The scheduler talks to the executor api + scheduler >> Edge(color="blue") >> executor + scheduler >> Edge(color="blue") >> executor2 + + # Pulsar talks to the server + pulsar >> Edge(color="red") >> server + + # redis events are given back to the server + redis_events >> Edge(color="orange") >> server + + # and passed to the client + server >> Edge(color="black") >> client diff --git a/docs/design/diagrams/relationships/images/armada.png b/docs/design/diagrams/relationships/images/armada.png new file mode 100644 index 0000000000000000000000000000000000000000..f4c86ed5cbe4094312858bd784087a725eb33d97 GIT binary patch literal 16551 zcmcIrWm_CgupQhbNU-3+-4k4cYhZDf;I;&JCxHOL-QC^E65QP#LV~-y-hJ==4VQiR zFw?c&T~%kQ>Qv8$sj0|fp_8Hm0DvVgC#?Yha1F5kNmOLmogww?{|1mv6lJA>*Z)2_ zZN>2bAZ{QpEurbLe4L5WVkk2g!q5<-CUx2-3r{?aj|OiNlV^pi7E3Q9QGiAxHwhM( zP=HAnV8HE9!y{8)slm&meoI(+RJQjDep-j_zQg=Sf4k~DvE*%Wdh45Y3il%RcZtV# zEUnHW(iu>*c~ID$!~_6bZd>_N#mfRiD{KHjO?)$5SJ5wLXoUy6h-@pA8A0NmDdoig zfN#Av(<3a1JTt7;WN9b>Ao=f!KS)KdJ; zf+#q{0Rhz+NwS(Bm^!b=S;fyJ%M`&r8R&Fq9{*IsjNoR}*%l{`$ z;FkQh7beXx`2@SdWc|2Vm=cs)D!-gz4*&tL7ADs*#RdSAZ}<~Qg)yzN6~Ou406+z%ID;+7Yp9pZ4i5y} zeCXbUY3AyJ4*(V6SOvoXe42SmOCD0P_Z=ob8M`0MyW+K$WKZ>H=Jl)?ij1 znK69$P-ASNM{_KD3kQft9U@>_d~7@BR$YN%Zg`)84eLWrDtitY956kmH~pmfh%xfZ{LdvAQ3K*|fX2|D={!UjAgom;0n^Ti62)6kI35Imk&R3)5iuj~5Z{_}2N_Tke37ULQ;IKa z-V_k=Tw;aRCYezZUZCO<+I@Zrgxj!Dwn zS?(!X*RSmZ;Ks&*+unC@0Q#;#Al6e-gOiWYdP5?{5&==;(Aj`5*glTU55`rW6K1{N+iwCeSZ#M3ZZuF&9#r_7dp{ z4{QxoO+N)7-+i*)h3}XfFL)s_RuiJ&K?1D&1N_-@0@udxu#dI`+~bXKSaWo7*D;-u z0G*GPP#1c6J0>l}o8w!PDgc0(?jP|WkWRkpY@IgaeRf8`uvzc5g9F;-$K!bRrex1&S=RWMR0R2=zqX*yY9l_H7Q zxf!EeaJVal3lOLOMU|kPffHS#eDq~Z8$rLZDwqjDJnbWEj>O~Bj};4yQr4WCdyySX zz}nomR5+&VVLWBo3T&++&aw!X!Uq(@>cf+ifE`mU?KTYhLM|v)y{Gk3_<@2j1Owvl zp5+CVB39KAh9C5!JGy_vQL*Wi7Bj(Nj)GL{qGgZWY^mTNv9tI*C;_|-IP&G5t5Mzx z_3&1*-m%`6qZB}#94cMrAJ;DixT3S)WAE=MlS_Tzff(YOn71CHcgtj8A*9)&-Cvwd z6srACa8yi-H)}c*D2Co8mjS@a%?>O;L89#=$pCS@J6$07#5%C4(p*%OQv6Xg1a@=J z?(~Uc&BgF96@fEPV96O8@cbezH?ng03~Da2$6F+c`#*p9VM!d>LOaR$|Zh)~kh| ziuQar%U%=#P}#E%ndB7*7p#5XWUG4p;@~S9RNAbHiI2KGO2N(oQJ~+_9GMp#67x*%V^7!|`8`a?DTSp$?hY9XebtuB$8TQW!0{jFe zZj-4?vu?1=5K)jXvNv%uolbFMncwj71Q{sY8lb@Tqiji`JkX?Ck%BAZo`8jajr~xH zt9_70Vd%3L<$qG|?kv3c_xR*%i+Wj7j*B}r*Slb14AZ3)SCxs`X}e$49^cxd0EK_m zcfsVH!5ellfe0u!>f$!c|0VWPmn@WL+tbB+kO5ThAn*`Qoa??lwEi}`_cynYc%YRV zL-5iCbUE1Xr7`r`hXt0F`b_Y5!LDH|8+N$nOcX@`Z^sXqrt5vH3n1pB?C}oxnb5x> zu-f3{(k{STUXP{U*ewBmq8krpHhZ`8q<{nZwiDfK)#Uj;Lbx0rpPrsT$V>~VRM(F8 zbQ~Vc0VkmS3O) z{$j~YGTVx;{xn#X92tjbyb@F9wd<5M@kwvsctRRZARpI$U_b#omYf0Xf!tSZctUvWsd-&cK0X*Aq{ zQvn}!*KukOXu%n4UBxB|`j<)D;kzPWQi}SxB1Nx=U|4+g%}HFI=UNIO#9+TR#rt1bMAz&VHT20#fh}wC$6VGbQZrIOQ_Jf8 zXOazXb$_4JKW(inI>OtYoFj!-+PC{N#gVdk(#EI z!FnM?gBg@9QRQb>61Hn9QSW9`p^&3(z}1(DU|w%d;F$|1n7Y(XdUl9UI_mVGDJX=T zF1QenHXYj{AP)5y3=Z7wVDAnUOx$SiKz``|hEhKfl-5K!5#3c~f74mTY}h*_1K?5) zcyR71#q)+^YKcJ zSdB_!Hz@L%Dyj*1(zGW8>yR{eEfcSPOjrCk@)6;EM7jxuPQOg!bH1?~h~G66m~YC` zBy&}aJ#IVX9%2S!0OXd@B`pI4g+zj(`~=bp4jSWa*PZikG z%^~2X!f&9*(f#!(5ANYpkfwwLB#6*DYLT98WqQ2vNV&0={W8av;O4$8f`0}d2>&7S z($ew`;ai)Youw&l9fIZ-2MW%APY>2;^P3F8#^(>Z;g=${V+HsH>Qz@VQ4=OQ0BST} zU02Z}f~2Y2-&(j5*$m3X|DK_SZjXQ*O@c8s{gaKbHSDA+_Som~L^l%;05t+k;E0=& zLT-#bj{Tw5v~-s=fRY-QP>YZG%?;he(PGeqHTz}2Za2-+rL*G^Js|$&h<@z!ThGJ@ zHWh!2$8CdewNcyr)L)@AKof6nDFQKT+4c6Q;NVl~?|6x0#1J&#wN~ns2*cs5bQ)Yh zTTs>Rm%(gkS8|!9jVeqh@;SXUho1`2Ki%gJqsuoOmFCy+s54>Yf?{!gRCm$$C%%#B?fuXUjhu z2C-0=s(b}RJC|6tWH0s$#|GsP2cNn_H{6f&``)_c=n zR2p2EDWP_dK+zrkzkH>V)95`Xq1&z!q?wN%5%`Pd`*^daj&NlM<2cQvo_lz((xzeg zj!U9U%8Q^J4rVR(guY>YtTf=vga-&1C~tu9>Il54SzfbwdOHsfsMoVAPjcDS&|9g? zQ-TrCA}S4b-gIMGhhtaI+EY6;mWF}&F){Wz4;AqXAlB#s-8E9)-s-m#yfOpCf-ZZ@ zq`wF7J4K7X4KW$fA7*HK8o=d9T0+ql#QA81o06^?kDDNkgU*d-)ZF zupseeJA*3eBJ&_D5_b36`ocU%#JUv0>@4-F!VrIr2pd(S~SQeNfc z;0iZ0Dujvz{T0WY3_tnLuJ5qUf>~!cWZ;hRyxNS}$7YCLB|pRY#O9t{rfQZ?OO`Sy zgVfX>zbFJPJwL3nUwKFzv9k@%5RF;l6b%6{p@uG*am5JRS7{A`$_tJSShtVgk{i7~ z)6h0?g5r%1ayV4t<)CnbP7oAw#^UU0m+x45AZyWFI0&*#s8HWWq=yYYUh)8E--1FU z!6{*DhP0Z^Mf6X>uZ!Q1>OWbldNmuvm@2_uM0Seq)VCN5*7$>Cc-Tz4xW~ zu+LA$C!a;N=-Z^EyXkxVz#T)~m7!;XGQ6*r26f(3f`E~lY}V^YuG3X=+{3%I4POd( z1oJ;z*~ro8D~!ECY9N`0@_O#70ZS4Oo$0aI2NukY@EwVXCmqB<8+eKet_ zdoEGC1_`k-5HKDeBi6eP2b8sJZfsN0q=jX z!?PBH=Kb&nJwF_m1zZ}11?fhB%-0JNBUCu79Y@=(9s~{Hs7-KQl&?Djl}_bsr|D$g zk$Ye3$#(5l(V)~j{uec3=h*~7AqUE;KDA1E(d>bfcliw12{Yea|RPP^Jm6CE*RT?JEnV+O$Yu)Dv7GE@sIlUK{<0r%EOtr1s zE&ua5Vz9Y}^3^B|7+vb&#b$eU$DBwd;ZYUCUbiP>AcuWUegE;#;P(h5L4OrgVSwe? z%bSiB(Qh4$69UC{eNZTDWHiwC8oyd4@>e7ZoH*Ki8Z6LD)GL7Omrds-F3>M|+hQbR zSmYOS%m~KTB=XKUgBvi-m^%ZKE2twcR|B0M1l9<#hV@pJ^<$eEW5|_krH5$)?>1Va zFwM?lz=#_#JgO#3NZuP6w|j6IB8%3hUvk8kiSQ|;Pr1f$^2Z*)U&@S#|B0_cpscx& z=0M&N|F|Y~E79E|pa%BoQ$N5I{>u;udx&JGzQ^EVW2_Q+hP^W}7qWKytD;Eipm`ugBt^Bzf~1Id6uq7ok(*#grS*Oh=6?C#BS`J0g*$&#S~x(8B6~KBg#OC^ zZHL4VE9fvWIHMY3_Z77A($T@NVLN@` zBH|H}7)qMX4bEV%R9uj%y^|V#$5`yV=XLfoyKt7My6`ZU_{uf)sDl?4ufmcOyNF-` zs$iB#EEd@Wev*WB<@7lLX0{1MLr$c#085RNi1>o46gYz69|=ORkn{x|mxp09v=aL6 zbAV(G%I(_?@yh@DbMoR!cGP^Eq1onC{%G+5QO2p79Gk+~Go~p>y}cc09TWSmvdG*n z0hf+x{MY142s9QS{1%GFdw8iR-5mDE3T#yQraz^iN`Dg(*8jd<$&27gq=X3iZhk2e zQD33)+NnfEV?VGCD&>OVLkCH$TmE7?Hw8T*r5bvbkaqY}rB5@5t;xkqW4oMW+A9S! z0oB{ejoB~B9r=Pk1+!vM>Vs^k3oShYoyq%`Ickxpq0S2*qGHApf(c_KlyD{gQwO0%Y$IO_i(f>!w*u$SNBT}l!P+oQJ7nr7Ysi; zN}#B)yT6EEnab?D3#R*krNSC#?UplPzmY~}SeQiC6R|&2X;p7;+?Z&TQkh1%%<|jM!ETER zX<;1+!U(?vijtHD3&K@YP3$MJqc<2}f%Kn2kfUtb=@+LOy!L2+=`ygf(~ya~wM0OO z)qXP$;(>I+Mqtw-uPs^bQhg<3HO?#%k=cN_9FQb`Z4&UwY6)!nOQ~r3Fi>z>j7v-> z@DaYjbR_%_l~9uK+Y*krzzp1R?8&=|pdtDZ-{7`6(3=ukUwr&;tm~+geq@E2$&bk8 z-CUw-U||e=%=F`er9hI*=RX{c=WLJi&Im4fHl-*A))aS92N;HHg9{j(>o+=SuH~`beZ+;)6FQb}{lfi4k{A59*u9mH}+0Y5EZrQZFv!8-H2I22l_6q4R(}=UN zP$Yf~{%7>h%EHMg7V*18|I#aQ&wh8dKUSxdWxw0&9xi~To^?NUW0i)%M&AYOxGoCz z3Jqs>g7cLQ{KXGu9oM!g6<`=d2jEDiq^j=Btt4A_O}Br823+t%m##zTgfrf1g6M-b>L3CCKBR;e#>`P9-QT5QurOKR(!b+KViTao4X%Ycph!>B zdqZoC;9sKi2~_cFM1AH9S*XiJDoQ*&#oO-!M^+ubr=VI_KQy4OY^x=E`iUeoeMI_< zo%I=^uKCTICPgcmK3U`8^P1)nvyob{HU3@XE$MoLFUccaICgZ?N7L!Bm8l5{!EspK zxYQOgxe0!CkQ6=>o8oi?bem|?`K}8!D*l_Oi0Z7pLb&@pEfH#jkCN=Dy`1!lWc`aO zuuU6?NAbR!!HzP8udPJ39w1&B8YezQA0(O>XvqIEAbwL&`F_rbqG93Fv@9H!V7kdh zmvNKUq1@nba!h!*KJo>7ipRd?$9x1%Bo3`5d!}BkzVQ6+ zs6eG+HnC9=I^-Qe2U5c5jLLF6r!(8;p+?Q6`bM5<7d%9i9g48$Q~qUP?$YfyQ%uf{ ztP%y8(?5E%?Vj(jWQ(8TX2GDvNItSK%D9>#EH(;PO=^F_Y-@t(^A@ zT}+M}YZ3Teh3J>e-Ast?~l)uuHf(qw%-;PCD}94bvMS6@qj7APX$nd8^Hztz@i&t6bA@ot)sX#r zBT+kffS6TWioq}=$;fQ~brAAH(IrF7`%mrH*1Lml!gMi2+*$U1I+IPJsBaY|9~c1e z6F&PCm(N5&S!|QCnzw%K?trj+-goxPc{1q;bByuZMfM|!DI=vF+6nO&^r~3OCJ5)y z=C}dXwW@LJipAQeayN^k%wPR;?=Q14 z&?0B{)a$I@^1chwq0Vo0xB8;JNmFhC-gT#+JN-O~f>6KW-F_3;)=)!RrQy^RZ{cl+ zg441^G=AMy(n42r6kXmBX|B<>?YJv)Jbk&hh9jI!A{y43@Gg5ola0FETj8N=yv(Ov zkLOQp;3~an$KFF=gT(N|Ci6t_GLA!PoMX_X5H$;=&&ijA`9;xSCqnTMDa#EMuedAa z-jwOwF`zQb2#Go*Aos2wSwQEOR~{S=jU7M5+-h^k#DvBAxB>58l){#;tP%TI(d7H0 ztHpbF!eJjj_C1>nkVkv33c%9uyP=A>Y(ee=pZ-6&hfK)yu_1UC^SY1E zWV!u>1XgWJAzU9WeAQNcI1plYj-4BLfhIau%{?^YNE50{6eeK_xGSoWd&rs{EMRMhS?s!s}0Y%qomg|pYy6*snDu7mlF+!XyAx&%1jrO zx*kKy&z-WrQgFE0QmoD0WQDM2nV)~98$6*@KmJkz#YbxyLItgG6umCEke+0S{3Ss| z_e+iUcX=*gSdVv;{n>13qmkmKP|H=`1^EI(!4I8_{@KWGFp(v)5N9f!0OQIomulj1kB5ZD@i z7(0QEkYw!}!}{IX6rhKTnij;>OsRYIxK+Ee@djr z4_N*12P_nV)1JN691iWF&n{n9Ti**N`$6T}@8)Bur8a&K*x-*o3LkAA#&3u}XZWXT zc0@KxJY26BguEKXzGG~Te$9JnJsh~xb-Ur3tM_YiGPL$#`eJ_cm)o7q_aI+r(+)Qs zb=Wt_jn~^E`z+RNoc&_f#?X?$ z-t#&5hCf~ZJ=`3(=974Z!AL-uU`Ets(hDb!_Es_8#A>wgVLUTala`WO^41#Oqe-fd zUo-LJ)5v&7-iG$K1M#*DuxjW^aE#Ev95MHaNMRWsi~vyDxV1oj`ugWus+g7cH?%qZ z7Iu5nzLfux!f_o|&eaf6TiAriAiWjQh@RDEa^$@p9=<{r|M-1Jys7ZENH)^S^&j?6 zNB%|qkTu*`1YdrkpT7j}V0EC4YQcvbk)tPtK|%KZux;$=fj5sP^*&tV$xX5zI}tFB z9I_=Yc3T0b8qJl0l_b$D4eMCUS<#bf*P8PzzIs@otSf)v6q>IWy>m6dq6RNYiK>&t zbBs^jm>TpY9Y0-B$XeJ+EF!pwCQ6f%Y}z@LkT@w}Uo9P}MZH__ZH>$XC$5rZFsLHP zEWW@*>Jm3>U_FE9EsSff#|>d+hfS!N-4L-TAz?FQ*U;CUDjiLDS3N&6Y5vDhAa*hw z$B(5zr?kR#eC??}A_hAj(WZOz2Z}LHPoXL73)t^I)I|H_4B>+f-uG!WEMy&Dl$P9K zj9+4I@d(>gEFN@Qzjdql=~nw6zc+ZUYN6;7o;Ak~_N|Si{fs*r^05Q2&P4vpg7gWn z^kO&R<5)_tK%4UD)fDUot;TLhl;v?O$5`MgGQJ5u8fh94o|XJ~Ctz9^?JVp><9+@m z7!if1oJnWrNq&%&G@gTkK0W)ilw&zK*U^ySKIyW!#3Q1iGFs*82LqNFxvK}vDw~8x zP1ov*tb6@twEj1YC2I+P18x|im+u=skczwc{S0k=&oWGb9a%2CLPxPQ*x~D(vngVe zacSz56Cv!LH39LoDs*$g95dTZZO^D(?zNPI4_*SIE!GzXwj$OqJ(7EiWxY?YtX4(G ziBRg4$^=zOx1&-(7TSng!r5ckgtLBcrJR?7{VBanIBtZXbc`uWJw@@t)+?5Vm}t6n zhdl6$bk>R;5nVNyfjj3vbuQbNTS3g?x`wm9+byTSl*Ak^(`K^U?b+Ho7gV1EkZ!BHhIvul@nejL)<#ce$lzi363Z0# zZ*??SYx0Gl=je<;HN)yRMg@T?nPvP}&&eoI!ULK5Y22V&wPGB{0ps7;FFheLZ7W-f z>W=yucEq9hYa3ky`i3p0w8yR_X&?(62G*2q7j4Y?p4cqJMn!mRtB$JzELxHHMnk-% zBQX9!#4E@+;=$(@UiD~ir-XbD*p)@veWUK@^53CBY_3SU8mw6+svr1iu>v&s;D}>q z{l)E&ju-Z-mB*xC^FYnEzihYfIg$ucqfzh8mXx|<1RILXbr-+w-C%IEALB3^cclS; zLSS2l1IjSOQ#kiD@`75_dHGhPrc~QdsSz2Y*Y8k~qG4r(z5825mWX#1ozSy3sgJss zluV>R)MSwl6SI9{)_mATu>Wb-Ks(vo04eETfw(ViS$MF}_ESWaGMDz4YR=nO-x7&n za{MNz&7uW$rn1p{((vU@Y1rrdm|7}CT36GXc2`#YJPvGLBWUbssi6yXH*@`WrMB~y zyW~~_)^BMmfO|iTryg9d@9((0U_kVFtqZ#@b0ond0#^OhVXCJaGo^@RYk;4`*D1C! z)

jo8KosD_#dCv7Wh&Qnq~X_9H?on{I2vv{q1nHM^P#X7gAteDY9uq{=O(`z`}+ z5>R&E4M_9PkO;Sg+MPEhJZ`u|$_vR_ncmDQugdf(CI$MW!p_^G^A6<6`%l;rm{orv zZ1c(=zu6Y)uobRp#23c45WC9pWsf*8O?;~TW#=I5x9HVwSZSEgczXXli=f}yut>Qr zML$PL6Cam>ga+g;*ieL_uK_?_kTHnSepYBaO%76a1;=HQ_RHK&)VOV)YeSOS0 zNMNi}{l-t^uI?xlvZp)RMR%f9W@#Yp`j24LYs8q;A&pSkFDsW$o<9rj3GKE$4vzUh=h z&7Ji2oke0N{)|coT1%`g(^c73rsd2Y_D3PaztpC61Bh*r|1MX_w7g7xO3!o87}uN$Wz^?0RL zCfGmNFm7CInI)4vpTkYxp26G+4Kkkv-Jhhp^iMS25nyjsIzAR|X@GUA<166)qROO@ z+Y}y-v{hy;5r$MgMEEjYL5bUUA87o07<9o65(CWjy+*AXHu=b(K z@JC%R>&;4N^!6id`{#n!t`OK}MKWaH?{SH*2Q4#=bMOzHz_02SkfUHA2h%(~KN~py zDM?FT%WAC5t@Jsd!fIbnK4$Im$2+$=Ez`05m6>2dC(!6^-y*@#457EM^lLgY=<)o4 zyaqNsqFcN8uy8M5i(M}^NMD*js`w-AFT6q(v$2UMz5WYbTltOm?pL(tRh8kzBk9^n z+xKLZ2mV`>nJM>>vY*~kcvHr4-nR=D%N8NVF; z0==aARTf~(5C8f@^S1X4&)3pT1KHBo(I89Wc#`mycC9|JtgYce#4x}r$qDZhxgzyk zutCa}bINlFiw08`Va?JN3wz8~8GO?dPPc%GZfPb#3CBqbOpjr7)=>u*GpcBVe>k%i z$^QJ8`s+w4|3zo@;(6o6*aH!w`NN6t?<|sO(vRSqFSg3C@F%m%hxq^WiRH0bR4j1M zD+U~IxoI5Pwb&tv{W`BF2a-K4S@%-G6g)jMTt{-+mSFcGv-=)9A*nxwgZ1in9JRaNak*Jo41$ni%I-mFgM# zl+~+Bvt;8l;DoAStRZ56IS1JgRTRRd437AhhO8qBA z>?KdWM_Pe-foWrq&s2FbFpL+~2Caz7impwT=4tIs;=U|Oe z!Af4`! z-=6@!LiblcUlIwrsLmVYUVltu_oB06l~Q49LWvxFo}PanB>#QQvBo`!B$SBtsjXfj z@Y6b@JIJr0si_=LhxLH5UFhZ!WN!Mz{^}#`+gG6IO(iYSbN|rG$dgJdf#fyTQZUm0 z_Zmc5;xgh>xm1}(I^^4R=LJs>t>e$%jo&IiG3IjkrPk??FEf3_EDvnDIXmr@xR@@~ z!AST}@{BAtqS$Q0Hc7uMsjrQE@kXUip)DcqU=6pLLvMbQjV(4*H zqCI4^K=hroOw)-CM*Bk1TYV5C2>#yAUG1>;IKYi{9u9W+#dSx~hneGCQM5F1`C)AL zN_!OE1l^{2(;r-tzKk>y@O#R}%(?7`PZg=0djIXH=9_ykAB@|C*oA9}ht|?$ z-@rRBPmIlC3z=f!ULrbcFKt#dWT(pq*C6kWx()nda?4TW6MA_I0$>$M{wV$Dqi9PL zB(E_y;I}map=_h?;=bp}Xc$)hdD_$(tO#oLB`A0s;vM<2)Mq(vhOon* z?1zlGJ@p?M{}nWe0R$VZ?_B>;`9Q6b|3!5+lVmp&weBnYz*790%+*d$N8% zH;H&z3Y%D~^;CgF@@NoOAVH%2O$vfY>3SP~8wwhxO*G0*#3nKh#cJ>$1XyG}%thpB z;-~s2am*D&B7PA}+J%W~gUAzb!-a6|`luedgOi>=qZos1w_(92FM?&kp^wJlh4#Bx zyV~f`V^6L*7J#>=^={kYXs0t(wV8y2E9_M2sZ&0|+S~86h^DXBl-|pW!BYBAo>8C>kpK+q-gdORlh#ScP9BW@W z%>Rxc!JkBEX;2=->!;eRTm$%h9R=>4R5A zRBi|#9u?k=_774D6gN`W>eR{I1U$sk2t{XN8AxLwY(-~sCI9`HO^{8c#oUl=ENo+Lll$111kEPTBQR1VbyCy2P7*=_Ewa$&y0Qbx#JM>fkd~I1rk0j7SWEeMQ)_NcRa`j{>(^86D_yIXzkYp2eoQ)8 zp1Oflu3N3!tXF((=C5h0;h%yvl>A@n8?|iHOOGqCD%tc`T>5_o&i5}h{TW*R^8hF! z25545UTj_SQrUlWJ(x9wsLihXyLx%vJCgEFjALdI`d3s`v?~T5`jWa(mQ|JdN(Nsz zlNJmpRL{-LMbA))a^*i*Uq>p>9FnCmlcigL1jaM8)`iQ>=SOwxcSr3~FRe#1?nf*p z3~TIeej|!$0j=#uH_kEe-zO>L}ftZi&;^pyJS z+;|;${rx&D==}X*wi0-*t;kq#{$cV#L*g1d-#kAszpxbG3G(#3ueHFiz_%c?ptWG} zzyuN!!tfcY$|Wp?#wKs4Mtw>w_82s^wZR7Jx}L!<$;g!`1q==F9_>v{C);Vzhi0B& zgXae5L;r9{QEH{w z6;3iP{UM}55E-|AS*7o|-H4r%#JNiS$9j>azc{0B$LPgg?lvJ~pUyP{u8*eV2gN3wF@EXt76!uu`svBt74I|9tgb7U}Q88uSJ zQjdM#;+n@})hXAj*K6(j)*IG;sW-2;Ub?BQyHtc&cG?-iIS|291q`t2H`z5F&Hs6E zT0KqD2EBaZSzg(mjXuEKCP_cG5wiL{ZX{}BW!D7E6;u}V@pIN_NLuTVq*bPmr61dl zG<5q&z?~UFV%rT2^&fxUhu{AM`FHSB4%^3mWm-SzMnzD2U!;^eJqOZUHu-4mrMt9` z4q8|S!9YK@$;a1d+8}b7l;KBN8w-DJ~0Oz9VkfHl40mL#W^?1TnNy~32PXBz66g^ z4X_+SLf(y-%uOrR=*}i9*^TqW10fqSY*T zIE;TID+UcB7T5ZbKgpfp9kzI&qkC%=Ns{kU;9z}o0>=5I`2(>K+dX*-i?O`qF7f9XICZoW8U{`O zTsm4V!!Ok1_(qak>u^}b^hYF}Tda}k2r-$dT}ACs9ms8sI55|^f;ni-bBe!9i<`Ce z?lJ8+Re*m?FGgra5;a$27~XJZbm@|eG(Dj}L%>KIFRQ*@uyU(Czj9=Zq%?OdF&T5q znCnchtt zGsDAC>cC89Wb?_1;EH%htN)IgB8S+hR0@kDxcIq``)C zj_>P%hSO(7TS7NX66$YB$&ryz(aAi!;oIY=)vNt&*?pS31L?TC`ZteSnwoz~qdcoi zJ#XNHh$T^7SzGw_hXX+NpSdl+@*v~t(u^z2dM?T^f4qc(OxO^)zTq;fILxF7Dw-bJ z3OcvOR1D$eJy;!<2g)9}{=qp`8YQKq{-$i|>DH3K(c z+?hJaElT5Lgp>Xo?uC>W)9<=G=)*f2O_WLUFpPtAi}4Kei7Hyy*r)5rv9TK{YAkby z;#8X}T+Q1a2~=t^^W+9shz*zqBvg`i^_dt`67`-%@$Ho7a=diK%}MIqtV*vjuH@iX z>xwM#UK@`3TBt>kjQ=-W$(LaT%Qv;lT}icLY|dQ7uoX1+t{IahU#Le!NUs2xj{uPewdS%!jJGH8!4?u z+uSyK!w&HR6&kTV`aUf4SR`bFb+s;SAikRwMXB}I8wc-Tie^sRv4{%S*VP@&U0e)1 zLn`Ptq|>o}`RmH`WfCUNowBev`O$V^QGdHgicB5WGbor_EV#G`_l79Z)%E^7K32&T z+$2M{ci*pDC*y0xQpOr%BW9IEtt9T{ zd2G@Qa;iT}x7g4@%|#7bVAb~=D=#b6nkqP0ApD)BLBFw{0Ng?%)zi5(q%5_a9`)bVCuP;c_`BTz%FTxtw)NGQ1T_vw#v!cmER^hMiJPZQvU`ykK@Nx2crPK)J8L%o zIzOdl0Rrx3f298B`d{R{={&n1bX6{d#?DTXHib3Sb*~LP&FA#p*2Eu=x4p7UbnClS z4;yg4s)XVmq>Q?6Nuh(TBO2>$622zdy7H;F-_-e>Fp<*=r>vQO^j>bT26L^OB_gSr ziNZ>au`T0A#xr02z*BBQNP8u$V5QzC*_ZiPU=gE5xs=sbhPW91^-JWtX8zW(Ff|Qx zexn)E0@Al>$LTmJ94V@ddTk}*=Asud_doBW?qlxb?g?ly0tR4(lY4T#{a2KUheLN4 z@_*g13dTQe_R=z3as?GvCy6>fSx>%~t; z#;ZTeY?HKP!C$*UTd!Nc=A>9tQsU8Tw``{m?F>p-`uzgUv}-4$g%d{~5}x8QRfN=E zH9%B_M(4#Hjeh<4#LU9dA*Iiee{~xzpU)Tbvyp9bo-M7+Mo4?|jWHxL=u19tbCavg z;%wP`S6EI;m9g21=hBOSeo2*WifPjqev>8>e$ysX{)Ae(dZs0i=k~LOExOV|+VK!_ zhi#J;zjf2l3_XB5WHZ?WsSs~i%dN#YfGJxr)7a*`f3{Y6TUY}KE&1{y{h+nGop zQ&L~iYp>)k&+cD+yT)y+Z)<9MIM1)WqEFdyBU7YG-*gjuop_ykZCHepT_dW+{2)?^ zpIui+Rxwd|+9>(pHviTu9g-56Qu&t)h+(#|wOyE^TI^|3b`I3=U)Scj31-vLTNB*( zArt1S?@&QFpybQ>!@iVs05M+3eqNwbkeM~>HyCU?R(0LrYSve*<(mzT+S~(&+);~@^UKts#hvf3t zHr4XiH4QiD-rvuM%){Ofo^Sfq84tOnm`Bl}xf_2)==if1>y(n*^&iiZmr;?fk~9wb EA3=xUwEzGB literal 0 HcmV?d00001 diff --git a/docs/design/diagrams/relationships/images/browser.png b/docs/design/diagrams/relationships/images/browser.png new file mode 100644 index 0000000000000000000000000000000000000000..a02b3a98eedee2d502a237bf5e0cc6f7d0fe7f6f GIT binary patch literal 39639 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

nQDwH4tSIf`a7vo=TcVgXu*Z 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 LSm%M3L-_v#ICB#2 literal 0 HcmV?d00001 diff --git a/docs/design/diagrams/relationships/images/pulsar.png b/docs/design/diagrams/relationships/images/pulsar.png new file mode 100644 index 0000000000000000000000000000000000000000..36de6bb18ffe663cb755b078436b4c61973b8b31 GIT binary patch literal 3356 zcmV+%4de2OP)J3ma47QYU@G4s-em5W=Uo?iQbu{ z$SJjzqW0KxXe(Ado|I!P53{==+9MYFvRVpOindUzeOPFK0My$ypkuYK zs)v4BSWk!$uVH4AMgd5BT>@`JcT^|oWMM@iLcDpr{T_ig3c$)9`Uzo8AwnEV-4Z$; z3M=ne0BToNirzggO)silN>4oN2Xt&rntly`Z*lx96I1kbVO1eQ?4*m10l@s(0!V+W z?WPlWr@`MQg1%^R-b!IuAwp~fTM0Y>fw$XGc|SclXO4)i2=H{g^T9h_C#)+(h=o-2 z(pmt@9u9c6RWG5HPR6LYm!1vBZ|8t_uxc?KC#)<)2;b}dhy%Ps<9g^+H{;YKX`6-j zTq>+AL*gpe z$Mytg?AIXwYxJVEJ@oT-myO8E6%kn_PBfNK8e@|B*?K}wcJpYF1Zgvl1LE0MQAuev zj#Xb@e+=)qm`s|D$Hn9E<3pTUI4pjaxj24Vqu)ys6kbx`;A zmfbaDva$vy2z9ALpf3V&Z*6F6JJS|0va;8B{uDw!>KS~pvdcJ@EX$`@9M@+&u3qY_ z2m^IsgX=)III!7=v4r~LXtNp!k5?Njd;E2N#>1DIhk0O}4;#=HRxiVFz?Y`$0`<-* zy5=YVpHcU~7<2ILpC|AQ76bV38Njox_F%8W=oRZ*TTk~6_>Ni_B;|)z04z1|b#--(iOOWHPn&JSaMT-6)bDdY z1Mr4L06x@%QHgwo&H0EUP0}w{CFyMtPClH2he_eM-cWMW6kTBn_9T52?(G|(9k$Pr zqRn>KYD_3Gm`Jnj6unzCL2g5k;oHr^=y&D;d^-ybKFt>9XMYNEKDnv2VC~102 zsFm3iUBwL}m(pX4rFtAqbW8vP4`dl!Qtu8Ge3G)Gkl<@fkPF!y2g6Y3FAsGFobv}S z)mQYW4Db&@RksO#4jX>A(`_}{FZhmBwUz0t2aG3(x3o-WvK-)m;H9uZfb15F%IBOS;9Vc%Tz{+Yo??K4{UJ8=EM@RP-g_-G(O2i~MJFrIGq%1; zk3gm}zkkDRFEuFe!3$x2b`ePVp38C;9b+-^QRnB&*t+LE%pqNOmKBebv&959>o2QZ^(7F#WwCVJDo&)BtXB zbYjIqdJ3*2xA1tcJ6)$2aOHEd#r<4c6h#+FZDA(jD`AG6FbTI8y43&^wa0?xu}Y)i z5pqV5Hg1y1d01RAT&s^|lHmV2)#t173BFi+dkviXL6+xhQ9t9Pz!3p*^s82z8^PZ` zS*QsPVdj$@BKIcbgBB8apPN1VSTOKL=8c;yb@0XGGfu^qOpYe=FnS)W{6<&ZeieAZi30YJ_k$W&7fH<9ttmV zv8{MOdKBatzh;bms7da;DAa{TlF+uJu)ojU$acZD1e|vctK*Y2(C`OY8&Fxh3?|WcP5nLXO{=CY zfKYPa(>_Az@4+PfDtM+>T~M!{jkJ=BotN%4TN`cMojWp6yR)ySF7*Pw%nL((-K^w4 zTC|f;Hp9<;>*pljW{Ve2Igtl&%fCrWXkg1wG}a1$48Zarf*N#RSgR}yy3j$+_OQ+x zvC|K8ayt{`X)90#48C`ZX;s4r2ytl*R`zhUhfbc(1;G8#vQX{DMLRKJ_R#d{?PupY z2mj7gpZB5n^Z-7ux(in~@G6IPlXWjtG?!KN(vSK5K+x-8Lma&s{2a&w&^&=J0Ae*v zG<8`n#s-^&TLH#P&S$kwE}!7zg<>&@%)|yfFGA4uK&+Kawt3b$OyzKe!*<9onQL|B z0et_(9$!38Dy_=)rd`7HZw*?GL}kBUT7ugJ!kE{?c)xS-MJY!+_=eyEd%UTIOmX0? z`m!6-9TR+bzJrFb_By?0KEcOGd8Ri=p5WND{eyF$a}xR>B=V=&o22xZyQkz93~*79(wz)ST2zJDS^pQGzC z#9(!i)>Cp2${e`{`3D2Osb1`i0AlX7?K$o)3Aq0LGJc(`mdmfD7%%O_E@Y2OvD13pxW^9^i#9tVq!sEXsua3?}eF-!6n} ztaMT;w{PH@h`Z~KCh(PMY#{aJ>}EN?0_viY>J)ria}mJZfKUHSr!jv3xIIKES7!qV4-XcK4cDOK7A}%_{`>fH!%Z*K0Xv4x;*q+29(u)=sg zGKsAK;s!@<#%PqtS$e;0bPQfkF>LT@xftYL_#*HUX5n%oI~Iz*_%^F--`xDgv2NAG zvxFvikl@QydCAi(*A+kS;6T?y8(et0?cm+g7&?|61(FZ&WhRuV2rz&POdaoFIZtfi$WX!=AtjV!pv= zA}{L|SEP|4yTlfFlJXMc(bk&nr1mo2YN4-}o|%WbDoF5U!cV1<;htiM`+)14y`YO8 zhoohvphHFqcsZ%iJj-GSpFVl#S>z*|G7V~V2JR`7*%wl|6}_A3Lv`A>V!(&biG?4x z3g`Z0wAzZ4I%uz0Q^?3#M|$W zHkf*gJ{TLOie-Y_!B+a*6BEddrhCqe8~7@D80Rs&s-$1=;o@C(6+8C-M!BT{^xO^s zP@e!6%%x=|B6$Yi$TjD1*2LNF59(%ngb=}ZBpl9c-*DLFc!TKS+3~P=!|npsD0f^E zh`qrBv!OD(?8z_q^wgr3rT>Bc8*_F);2QwYEvih?No6G_g#aH478-D;tqfT==VW~o z8~c{Dr2>I()P4{;0tgrDgyUS}Ah7PSY^ux3^~~{sen}3ld}l(!+k*z57UpVOQJPYP zVy4hJ8^D`s@ zXxr!^n+5xuxaW&>&H1+Ah|WC8cwN7109nEDvYw0C>I2ti=K8x|+w;ITY@ltehr-E2 m8q*|6uf%*%z(R Airflow : Creates a dag +Airflow -> AirflowOperator : Specify ArmadaPythonClient and JobServiceClient and pod definitions +AirflowOperator -> ArmadaPythonClient : Submits pod spec to Armada +AirflowOperator -> JobServiceClient : Polls GetJobStatus rpc call for given job id +AirflowOperator <- JobServiceClient : Wait for finished event and returns state, message +Airflow <- AirflowOperator : Airflow moves on to new task in schedule +@enduml \ No newline at end of file diff --git a/docs/design/jobservice/airflow-sequence.svg b/docs/design/jobservice/airflow-sequence.svg new file mode 100644 index 00000000000..894e02b708a --- /dev/null +++ b/docs/design/jobservice/airflow-sequence.svg @@ -0,0 +1,18 @@ +UserUserAirflowAirflowAirflowOperatorAirflowOperatorArmadaPythonClientArmadaPythonClientJobServiceClientJobServiceClientCreates a dagSpecify ArmadaPythonClient and JobServiceClient and pod definitionsSubmits pod spec to ArmadaPolls GetJobStatus rpc call for given job idWait for finished event and returns state, messageAirflow moves on to new task in schedule \ No newline at end of file diff --git a/docs/design/jobservice/job-service.md b/docs/design/jobservice/job-service.md new file mode 100644 index 00000000000..737d6670427 --- /dev/null +++ b/docs/design/jobservice/job-service.md @@ -0,0 +1,114 @@ +# Armada Job Service + +## Deprecation Warning + +The Job Service is being deprecated in favor of the new Query API as of May 2024. Users are encouraged to use the new API, and this component will be removed from future versions of Armada. + +## Problem Description +Armada’s API is event driven, preventing it from integrating with tools, such as Apache Airflow, written with the expectation that it can easily fetch status of a running job. It is not scalable to have Airflow subscribe to the event stream to observe status, so we must implement a caching layer which will expose a friendlier API for individual job querying. + +## Proposed Change +### Notes +- Add an optional caching API and service to Armada +- Caches job_id:(job_status, message) relationship for subscribed (queue,job_set) tuples +- Service is written in Go for performance and to reuse code from armadactl + +### Proposed Airflow Operator flow +1. Create the job_set +2. [do the work to schedule the job] +3. Status polling loop that talks to job service + +## Alternative Options + +### Change Armada API +Armada could expose a direct endpoint allowing access to status of a running job. +A previous iteration of Armada did provide an endpoint to get status of a running job. This was found to be a bottleneck for scaling to large number of jobs and/or users. The switch to an event API was used to alleviate this performance issue. + +### Change Airflow DAG API +Airflow could be modified to allow alternate forms of integration which work better with event-based systems. +This is impractical because we do not have Airflow contributors on staff, and the timeline required to get such a change proposed, approved, and merged upstream is much too long and includes lots of risk. + +## Data Access +- Service will need to insert job-id and state for a given queue and job-set +- Service will need to delete all jobs for a given queue and job-set. +- Service will access by job-id for polling loop. +- We will use in an in memory cache while doing subscription and then write to a persistent DB periodically. +- We will delete data after a configuration amount of time without an update. +## Data Model + - The Job Service will contain a table of queue, job-set, job-id, state and timestamp. + - What database should store this? + - We will use SQLLite. + - A in memory database will be used to get job-sets and then we will write in batches to our database for persistence. +### SQLLite +Pros + - Lightweight + - In memory db + - Part of service + - Persists database to a file + - SQL operations for inserting and deleting are simple. + +Cons + - Writing is sequential and blocks. + - Meant for small amount of concurrent users. + - Difficult to scale with Kubernetes. + - Scaling is only possible by increasing the number of job services + - Logic for deleting is more complicated. + - Writing to virtualized file volume will be slow in Kubernetes. + +## API (impact/changes?) +- What should be the API between Armada cache <-> Airflow? + - The proto file above will generate a python client where they can call get_job_status with job_id, job_set_id and queue specified. All of these are known by the Airflow Operator. + - [API Definition](https://github.com/armadaproject/armada/blob/master/pkg/api/jobservice/jobservice.proto) +- JobSet subscription will happen automatically for all tasks in a dag. + +## Security Impact + +The cache should use the same security as our armadactl. Airflow does not currently support multitenancy. + +## Documentation Impact +- Update dev and quickstart guides +- Update production deployment guides + +## Use Cases + +### Airflow Operator +1) User creates a dag and assigns a job-set. +2) Dag setup includes ArmadaPythonClient and JobServiceClient +3) Airflow operator takes both ArmadaPythonClient and JobServiceClient +4) Airflow operator submits job via ArmadaPythonClient +5) Airflow operator polls JobServiceClient via GetJobStatus +6) Once Armada has a terminal event, the airflow task is complete. + +### Implementation Plan + +I have a PR that implements this [plan](https://github.com/armadaproject/armada/pull/1122). +- Created a jobservice proto definition +- Generated GRPC service for the correspond proto definition +- Created a jobservice cmd that takes an configuration object +- JobService starts a GRPC server +- Added ApiConnection and GRPC configuration parameters + + +### Subscription + +The logic for this service is as follows: + +- When a request comes in, check if we already have a subscription to that jobset. +If we don't have a subscription, create one using the armada go client (grpc api). +- Have a function connected to this subscription that updates the jobId key in the local cache with the new state for all jobs in the jobset (even those nobody has asked for yet). +- The local redis should just store jobId -> state mappings. Any messages you get that don't correspond to states we care about (ingresses, unableToSchedule) just ignore. +- Return the latest state. If we just subscribed then it's probably "not found" +The armada operator just polls for the job state. The first poll for a given jobset will cause a subscription to be made for that jobset. + +### Airflow Sequence Diagram + +![AirflowSequence](./airflow-sequence.svg) + + +### JobService Server Diagram + +![JobService](./job-service.svg) + +- EventClient is the GRPC public GRPC client for watching events + +- The JobService deployment consists of a GRPC Go Server and a database. diff --git a/docs/design/jobservice/job-service.svg b/docs/design/jobservice/job-service.svg new file mode 100644 index 00000000000..795a12f733e --- /dev/null +++ b/docs/design/jobservice/job-service.svg @@ -0,0 +1,15 @@ +JobServiceJobServiceEventClientEventClientDatabaseDatabaseJobServiceClientJobServiceClientCalls Event client with job-set and queueStores all job status and message for a given job-set in the databaseCalls Database via rpc call to retrieve status and message for given id \ No newline at end of file diff --git a/docs/design/jobservice/jobservice.pml b/docs/design/jobservice/jobservice.pml new file mode 100644 index 00000000000..4d45e346bec --- /dev/null +++ b/docs/design/jobservice/jobservice.pml @@ -0,0 +1,5 @@ +@startuml +JobService -> EventClient : Calls Event client with job-set and queue +EventClient -> JobService : Stores all job status and message for a given job-set in the database +Database -> JobServiceClient : Calls Database via rpc call to retrieve status and message for given id +@enduml \ No newline at end of file diff --git a/docs/design/priority.md b/docs/design/priority.md new file mode 100644 index 00000000000..f7a8bfab7e9 --- /dev/null +++ b/docs/design/priority.md @@ -0,0 +1,52 @@ +# Armada priority + +This document describes priority calculation algorithm in detail. + +## How is priority calculated + +### Resource usage +Armada schedules jobs which can use multiple types of resources (CPU, memory, GPU, ...). +To get one number which represents the share of a resource by a particular queue, Armada firstly calculates how much of particular +resource is available for one CPU `resource factor`. +Then queue usage can be calculated as `usage = # of CPU + # GPU / GPU factor + # memory / memory factor + ...` + +In example: +If our cluster has 10 CPUs, 20Gb of memory and 5 GPUs.
+GPU factor will be `0.5` and memory factor `2`.
+Queue using 5 CPUs, 2 Gb memory and 1 GPU will have usage `5 + 2 / 2 + 1 / 0.5 = 8` . + +### Queue priority +Queue priority is calculated based on current resource usage; if a particular queue usage is constant, the queue priority will approach this number and eventually stabilize on this value. +Armada allows configuration of `priorityHalftime` which influences how quickly queue priority approaches resource usage. + +The formula for priority update is as follows (inspired by Condor priority calculation): + +`priority = priority (1 - beta) + resourceUsage * beta` + +`beta = 0.5 ^ (timeChange / priorityHalftime)` + +### Priority factor +Each queue has a priority factor, this is a multiplicative constant which is applied to the priority. The lower this number is the more resources a queue will be allocated in scheduling. + +`effectivePriority = priority * priorityFactor` + +## Scheduling resources +Available resources are divided between non empty queues based on queue priority. The share allocated to the queue is proportional to inverse of its priority. + +For example if queue `A` has priority `1` and queue `B` priority `2`, `A` will get `2/3` and `B` `1/3` of the resources. + +There are 2 approaches Armada uses to schedule jobs: + +### Slices of resources +When the Executor requests new jobs with information about available resources, resources are divided into slices according to the inverse priority. + +Armada iterates through queues and allocates jobs up to the slice size for each queue. + +Whatever resources remain after this round are scheduled using probabilistic slicing. + +This round is skipped if Armada Server is configured with the option `scheduling.useProbabilisticSchedulingForAllResources = true`. + +### Probabilistic scheduling +To schedule any remaining resources Armada randomly selects a non-empty queue with probability distribution corresponding to the remainders of queue slices. One job from this queue is scheduled, and the queue slice is reduced. This continues until there is no resource available, queues are empty or the scheduling time is up. + +This way there is a chance than one queue will get allocated more than it is entitled to in the scheduling round. However as we are concerned with fair share over the time, rather than in a moment, this does not matter much. Queue priority will compensate for this in the future. diff --git a/docs/design/relationships_diagram.md b/docs/design/relationships_diagram.md new file mode 100644 index 00000000000..b58cfe82cf8 --- /dev/null +++ b/docs/design/relationships_diagram.md @@ -0,0 +1,43 @@ +## Relationships Diagram + +![Systems Diagram](./diagrams/relationships/armada_system.png) + +This diagram shows the high-level relationships between components of Armada and third-party softwares. + +For a more detailed view of Armada, see the [Scheduler Architecture Doc](./architecture.md). + +### Armada Client + +This is the comonent that is used by users to submit jobs to Armada, using gRPC. Current languages supported are: +- Go +- Python +- C# + +### Ingester Loops + +All data-flows in armada are controlled by Pulsar. This means that all data is first written to Pulsar, and then ingested into the appropriate database. The ingester loops are the components that read data from Pulsar and write it to the appropriate database. + +There are 3 ingester loops: +- **Event Ingester**: This ingests data from Pulsar into Redis. +- **Lookout Ingester**: This ingests data from Pulsar into Postgres. +- **Scheduler Ingester**: This ingests data from Pulsar into Postgres. + +### Scheduler + +The [scheduler](./scheduler.md) is the component that is responsible for scheduling jobs. + +It receives data from the ingester loops, and then uses that data to schedule jobs. Its decisions are then fed back to Pulsar, allowing the process to repeat. + +### Armada Executor Components + +These are the components that run on each k8s cluster that executes jobs. + +It includes: +- **Armada Executor**: The main component of the executor. It is responsible for the execution of jobs on the cluster. +- **Binoculars**: A component that reads logs from the k8s API. + +### Lookout + +Lookout is made of 2 components: +- **Lookout API**: This is the component that acts as a gateway to the lookout database. It is a gRPC API. +- **Lookout UI**: This is the component that is used by users to query the state of jobs. It is a web UI. diff --git a/docs/design/scheduler.md b/docs/design/scheduler.md new file mode 100644 index 00000000000..e8e6afff538 --- /dev/null +++ b/docs/design/scheduler.md @@ -0,0 +1,154 @@ +# Scheduler + +The scheduler is the component of Armada responsible for determining when and where each job should run. Here, we describe how it works, including its support for + +* fair resource allocation, +* bin-packing, +* gang-scheduling, and +* preemption (urgency-based and to fair share). + +## Jobs + +An Armada job represents a computational task to be carried out and consists of a bag of Kubernetes objects (e.g., pods, services, and ingresses) with a shared life cycle, i.e., all objects are created at the start of the job (within the same cluster) and are destroyed at its end. Each job is a member of a queue (e.g., corresponding to a particular user) and a job set (a per-queue logical grouping of jobs that can be managed as a unit), and has a priority associated with it, i.e., each job has associated with it a tuple (queue, jobSet, priority). Each job also has a priority class (PC) associated with it; see the section on preemption. + +Each job is submitted to a specific queue and is stored in that queue while waiting to be scheduled. Job sets typically represent workflows consisting of multiple jobs. For example, if fitting a machine learning model against several different data sets, there may be a separate job for each data set, and these jobs could all be members of the same job set. Job sets can be monitored and, e.g., cancelled as a unit, which simplifies managing large numbers of jobs. + +Armada jobs are created by submitting a job specification to the Armada server via either the gRPC API, client libraries, or the armadactl command-line-utility. + +## Resource usage and fairness + +Each job has a cost associated with it that is the basis of fair resource allocation. In particular, Armada tries to balance the aggregate cost of jobs between queues. Specifically, the cost of each job is a weighted sum of all resources requested by that job. For example, a job requesting CPU, GPU, and RAM, has cost + +CPU + w_GPU * GPU + w_RAM * RAM, + +where w_GPU and w_RAM is the cost of GPU and RAM relative to 1 CPU. Currently, w_GPU=1 and w_RAM=0. + +Further, the cost associated with each queue is the sum of the cost of all jobs in the pending or running state associated with the queue; this per-queue cost is what the Armada scheduler tries to balance. (This notion of fairness is sometimes referred to as asset fairness.) Specifically, each queue has a weight associated with it that determines the size of its fair share. If we denote these per-queue weights by + +w_1, ..., w_N, + +where N is the number of active queues (see below) and by + +w = sum(w_1, ..., w_N) + +their sum, then the fair share of each of the N queues is + +w_1 / w, ..., w_N / w. + +This computation only includes active queues, i.e., queues for which there are jobs in the queued, pending, or running state. Hence, the fair share of a queue may vary over time as other queues transition between active and inactive. Armada considers the cost associated with each queue (more specifically, the fraction of its fair share each queue is currently assigned) when selecting which job to schedule next; see the following section. + +## Job scheduling order + +Armada schedules one job at a time, and choosing the order in which jobs are attempted to be scheduled is the mechanism by which Armada ensures resources are divided fairly between queues. In particular, jobs within each queue are ordered by per-job priorities set by the user, but there is no inherent ordering between jobs associated with different queues; the scheduler is responsible for establishing such a global ordering. To divide resources fairly, Armada establishes such a global ordering as follows: + +1. For each queue, find the next schedulable job in that queue, where jobs are considered in order of per-job priority first and submission time second. +2. For each queue, compute what fraction of its fair share the queue would have if the next schedulable job were to be scheduled. +3. Select for scheduling the next schedulable job from the queue for which this computation resulted in the smallest fraction of fair share. + +Including the next schedulable job in the computation in step 2. is important since the next job may be a gang job requesting thousands of nodes. + +This approach is sometimes referred to as progressive filling and is known to achieve max-min fairness, i.e., for an allocation computed in this way, an attempt to increase the allocation of one queue necessarily results in decreasing the allocation of some other queue with equal or smaller fraction of its fair share, under certain conditions, e.g., when the increments are sufficiently small. + +Note that when finding the next schedulable job, there is a limit to the number of jobs considered, i.e., there may be schedulable job in a queue blocked behind a long sequence of unschedulable jobs. + +## Bin-packing +When assigning jobs to nodes, Armada adheres to the following principles: + +* When choosing a node to assign a job to, Armada first considers the set of nodes on which the queue the job is associated with is the only user. If not schedulable on any of those nodes, Armada considers the set of unused nodes, and, only if not schedulable there either, considers nodes with multiple users. This principle is important to reduce interference between jobs associated with different queues during the scheduling cycle and serves to reduce the number of preemptions necessary when preempting to fair share (see the section on preemption). +* When choosing among the nodes in such a set, Armada attempts to schedule the job onto the node with the smallest amount of available resources on which the job fits. This principle is important to increase the amount of contiguous resources available, thus facilitating scheduling large jobs later – Armada is optimised for large jobs in this way since the difficulty of scheduling a job increases with its size; very small jobs are typically easy to schedule regardless. + +Together, these principles result in jobs being bin-packed on a per-queue basis – the second step above can be seen as greedy bin-packing solver. + +This approach comes with an important trade-off compared global bin-packing in that it reduces cross-queue job contention at the expense of potentially increasing inter-queue job contention. I.e., each user has a greater level of control of how the resources on a node are utilised – since a user submitting a large number of jobs is likely to be the only user on most of the nodes assigned to those jobs. However, this approach also results in jobs that are likely to have similar resource usage profiles being clustered together – since jobs originating from the same queue are more likely to, e.g., consume large amounts of network bandwidth at the same time, than jobs originating from different queues. We opt for giving users the greater level of control since it can allow for overall more performant applications (hence, this is also the approach typically taken in the high-performance computing community). + +## Gang scheduling +Armada supports gang scheduling of jobs, i.e., all-or-nothing scheduling of a set of jobs, such that all jobs in the gang are scheduled onto the same cluster at the same time or not at all. Specifically, Armada implicitly groups jobs using a special annotation set on the pod spec embedded in the job. A set of jobs (not necessarily a "job set") for which the value of this annotation is the same across all jobs in the set is referred to as a gang. All jobs in a gang are gang-scheduled onto the same cluster at the same time. The cluster is chosen dynamically by the scheduler and does not need to be pre-specified. + +Details related to the gang-scheduling algorithm: + +* Jobs are grouped using the armadaproject.io/gangId annotation. The value of the armadaproject.io/gangId annotation must not be re-used. For example, a unique UUID generated for each gang is a good choice. +* All jobs in a gang must also specify the total number of jobs in the gang using another annotation armadaproject.io/gangCardinality. It's the responsibility of the submitter to ensure this value is set correctly on each job that makes up a gang. +* All jobs in a gang must be submitted within the same request to Armada. This is to ensure that Armada can validate at submit-time that all jobs in the gang are present. +* During scheduling, Armada iterates over jobs. Whenever the Armada scheduler find a job that sets the armadaproject.io/gangId annotation, it stores that job in a separate place. Armada only considers these jobs for scheduling once it has found all of the jobs that make up the gang. Note that the scheduler object already supports several pods. + +## Preemption + +Armada supports two forms of preemption: + +1. Urgency-based preemption, i.e., making room for a job by preempting less urgent jobs. This form of preemption works in the same way as Kubernetes priority classes (PCs). +2. Preemption to fair share, i.e., preempting jobs belonging to users with more than their fair share of resources, such that those resources can be re-allocated to improve fairness. + +These forms of preemption are driven by separate processes and operate independently of each other. Incidentally, preemption also makes it easier to schedule large jobs, since currently running jobs can be preempted to make room for them. + +Both forms of preemption are based on Armada job PCs, each of which is represented by a tuple (name, priority, isPreemptible). Armada comes with two PCs by default: + +* (armada-default, 30000, false) +* (armada-preemptible, 20000, true) + +Job priority classes are set by setting the priorityClassName field of the embedded podspec. Jobs with no PC are automatically assigned the armada-default PC (i.e., preemption is opt-in). We describe both forms of preemption in more detail below. + +### Urgency-based preemption + +When scheduling a job, Armada may preempt other jobs to make room for it. Specifically, Armada may preempt running preemptible jobs with a lower-priority PC. Hence, the PC priority of a job expresses how urgent a job is. In this way, PCs support the following use cases: + +* A user wants to run, e.g., a speculative job and doesn't want to occupy the farm if there are more urgent jobs to run. To that end, the user chooses a low-priority preemptible PC when submitting it. If there are more urgent jobs to run (i.e., with a higher-priority PC), those jobs may preempt the submitted job. +* A user has an urgent job they want to be run immediately. To that end, they choose a high-priority PC, such that it may preempt currently running preemptible jobs to make room for itself. + +Hence, this is a cooperative form of preemption that requires users to coordinate among themselves to set appropriate PCs. + +Urgency-based preemption is implemented in Armada via tracking the allocatable resources at different PC priorities. For example, if a 32-core node has running on it + +* an armada-default job requesting 10 cores and +* an armada-preemptible job requesting 20 cores, + +then Armada understands that up to 22 cores can be allocated to armada-default jobs (allocating more than 2 cores is possible by preempting the armada-preemptible job) and up to 2 cores can be allocated to armada-preemptible jobs. If during scheduling a job needs to be preempted, kube-scheduler (a Kubernetes component) makes the decision on which job should be preempted. + +### Preemption to fair share + +Whereas urgency-based preemption is triggered by a job being submitted that can not be scheduled without preempting a lower-urgency job, preemption to fair share is triggered whenever a queue A has been allocated more than their fair share of resources – and at least some of those jobs are preemptible – and another queue B has jobs of equal PC priority queued. Recall that the fair share of each queue is computed from the set of currently active queues, i.e., queue A may be exceeding its fair share because a previously inactive queue B has become active as a result of jobs being submitted to it, thus reducing the size of the fair share of queue A. + +For example, if jobs of PC armada-preemptible are submitted to queue A and are allocated all available resources, and later jobs of PC armada-preemptible are submitted to queue B, Armada would preempt some of the jobs of user A to make room for the jobs submitted by user B (the fraction of resources reclaimed from queue A depends on the relative weights of the two user's queues). + +At a high level, preemption to fair share works as follows: + +1. Some preemptible jobs are evicted from the nodes to which they are assigned and are placed at the front of their respective queues. Specifically, for each node, all preemptible jobs on that node are evicted with a configurable probability. If a job part of gang is evicted, all other jobs in that gang that are still running are also evicted, regardless of which node they are assigned to. This happens only within the scheduler, i.e., no running jobs are preempted at this stage. The cost of the evicted jobs are subtracted from the cost of their respective queues. +2. Armada performs a scheduling cycle, thus computing a mapping from jobs to nodes as if the evicted jobs had never started running and were still queued. Recall that Armada selects which queue to schedule from next based on what fraction of its fair share the queue is currently allocated. Hence, any queue above its fair share is unlikely to have all of its evicted jobs re-scheduled as part of the cycle (unless other jobs for some reason can not be scheduled). +3. Any jobs that were evicted and not re-scheduled as part of step 2. are preempted. Any jobs scheduled in step 2. that were not previously evicted are new jobs that should be allowed to start running. Because evicted jobs are placed at the front of each queue, Armada will always, for each queue, try to re-schedule evicted jobs before trying to schedule now jobs. + +In this way Armada makes a unified of which jobs should be preempted and scheduled. It is then the responsibility of the rest of the system to reconcile against this new desired state. + +For this process to be effective, i.e., not cause large numbers of avoidable preemptions, the mapping from jobs to nodes must be stable across invocations of the scheduler. We use two strategies to increase stability: + +* Evicted jobs may only be scheduled onto the node they were evicted from, i.e., no moving jobs between nodes. +* Jobs are bin-packed on a per-queue basis. This reduces interference between queues during the scheduling cycle. + +For example, consider a scenario with two queues A and B of equal weight and two nodes – node 1 and node 2 – with 32 cores each. Say that initially 40 1-core preemptible jobs are submitted to queue A, 32 of which are scheduled onto node 1 and 8 on node 2. Later, 50 1-core preemptible jobs are submitted to queue B. During the next scheduler invocation, Armada evicts all jobs from nodes 1 and 2 and places these at the front of queue A. At this point, there are 40 jobs in queue A and 50 jobs in queue B. Then, Armada starts scheduling from both queue A and B. Because Armada selects which queue to schedule from next based on which queue is currently assigned the smallest fraction of its fair share, Armada will alternate between scheduling from queue A and B, such that at the end of the scheduling cycle, 32 jobs are scheduled from each queue; the scheduling cycle will have terminated because both node 1 and 2 are full at this point. For queue A, all these jobs are re-scheduled evicted jobs, whereas for queue B the 32 scheduled jobs are new jobs. For queue A, 8 evicted jobs were not re-scheduled and are thus preempted. For queue B, 18 jobs could not be scheduled and are still queued. + +The above example also illustrates why per-queue bin-packing (as opposed to bin-packing in a non-queue-aware manner) is important; because Armada alternates between scheduling from queue A and B, non-queue-aware bin-packing would fill up node 1 with 16 jobs from each queue before scheduling onto node 2, thus unnecessarily preempting 16 jobs from queue A. Per-queue bin-packing, on the other hand, avoids scheduling jobs from queue B onto node 1 when possible, which results in the 32 jobs of queue B all being scheduled onto node 2. + +To control the rate of preemptions, the expected fraction of currently running jobs considered for preemption to fair share is configurable. Specifically, for each node, the preemptible jobs on that node are evicted with a configurable probability. + +## Graceful termination + +Armada will sometimes kill pods, e.g., because the pod is being preempted or because the corresponding job has been cancelled. Pods can optionally specify a graceful termination period, i.e., an amount of time that the pod is given to exit gracefully before being terminated. Graceful termination works as follows: + +1. The pod is sent a SIGTERM. Kubernetes stores the time at which the signal was sent. +2. After the amount of time specified in the graceful termination period field of the pod has elapsed, the job is sent a SIGKILL if still running. + +Armada implements graceful termination periods as follows: + +* Jobs submitted to Armada with no graceful termination period set, are assigned a 1s period. +* Armada validates that all submitted jobs have a termination period between 1s and a configurable maximum. It is important that the minimum is positive, since a 0s grace period is interpreted by Kubernetes as a "force delete", which may result in inconsistencies due to pods being deleted from etcd before they have stopped running. + +Jobs that set a termination period of 0s will be updated in-place at submission to have a 1s termination period. This is to ensure that current workflows, some of which unnecessarily set a 0s termination period, are not disrupted. + +Jobs that explicitly set a termination period higher than the limit will be rejected at submission. Jobs that set a termination period greater than 0s but less than 1s will also be rejected at submission. + +## Job deadlines + +All Armada jobs can be assigned default job deadlines, i.e., jobs have a default maximum runtime after which the job will be killed. These defaults are: + +* CPU jobs: 3 days +* GPU jobs: 14 days + +Default deadlines are only added to jobs that do not already specify one. To manually specify a deadline, set the ActiveDeadlineSeconds field of the pod spec embedded in the job; see https://github.com/kubernetes/api/blob/master/core/v1/types.go#L3182 diff --git a/docs/developer/aws-ec2.md b/docs/developer/aws-ec2.md new file mode 100644 index 00000000000..b3909aa4e36 --- /dev/null +++ b/docs/developer/aws-ec2.md @@ -0,0 +1,236 @@ +# EC2 Developer Setup + +## Background + +For development, you might want to set up an Amazon EC2 instance as the resource requirements for Armada are substantial. A typical Armada installation requires a system with at least 16GB of memory to perform well. Running Armada on a laptop made before ~2017 will typically eat battery life and result in a slower UI. + +Note: As of June 2022, not all Armada dependencies reliably build on a Mac M1 using standard package management. So if you have an M1 Mac, working on EC2 or another external server is your best bet. + +## Instructions + +- We suggest a t3.xlarge instance from aws ec2 with AmazonLinux as the OS. 16 GB of memory is suggested. +- During selection of instance, Add a large volume to your ec2 instance. 100 gb of storage is recommended. +- When selecting the instance, you will have the opportunity to choose a security group. You may need to make a new one. Be sure to add a rule allowing inbound communication on port 22 so that you can access your server via SSH. We recommend that you restrict access to the IP address from which you access the Internet, or a small CIDR block containing it. + +If you want to use your browser to access Armada Lookout UI or other web-based interfaces, you will also need to grant access to their respective ports. For added security, consider using an [SSH tunnel](https://www.ssh.com/academy/ssh/tunneling/example) from your local machine to your development server instead of opening those ports. You can add LocalForward to your ssh config: `LocalForward 4000 localhost:3000` + +- ### Install [Docker](https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/) + +The procedure to install Docker on AMI 2 (Amazon Linux 2) running on either EC2 or Lightsail instance is as follows: + +1. Login into remote AWS server using the ssh command: + +``` +ssh ec2-user@ec2-ip-address-dns-name-here +``` + +2. Apply pending updates using the yum command: + +``` +sudo yum update +``` + +3. Search for Docker package: + +``` +sudo yum search docker +``` + +4. Get version information: + +``` +sudo yum info docker +``` +

+ +

+ +5. Install docker, run: + +``` +sudo yum install docker +``` + +6. Add group membership for the default ec2-user so you can run all docker commands without using the sudo command: + +``` +sudo usermod -a -G docker ec2-user +id ec2-user +# Reload a Linux user's group assignments to docker w/o logout +newgrp docker +``` + + +- ### Install [docker-compose](https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/) + +```bash +$ cd $HOME/.docker +$ mkdir cli-plugins +$ cd cli-plugins +$ curl -SL https://github.com/docker/compose/releases/download/v2.17.3/docker-compose-linux-x86_64 -o docker-compose +$ chmod 755 docker-compose +``` + +Then verify it with: + +```bash +docker-compose version +``` + +- ### Getting the [Docker Compose Plugin](https://docs.docker.com/compose/install/linux/#install-the-plugin-manually) + +Armadas setup assumes You have the docker compose plugin installed. If you do not have it installed, you can use the following guide: + +* https://docs.docker.com/compose/install/linux/#install-the-plugin-manually + +Then test it with: + +```bash +docker compose version +``` + + +- ### Install [Go](https://go.dev/doc/install) + +ssh into your EC2 instance, become root and download the go package from [golang.org](https://go.dev/doc/install). + +1. Extract the archive you downloaded into /usr/local, creating a Go tree in /usr/local/go with the following command: + +``` +rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.1.linux-amd64.tar.gz +``` + +2. Configure .bashrc + +Switch back to ec2-user and add the following line to your ~/.bashrc file + +``` +export PATH=$PATH:/usr/local/go/bin +``` + +3. Configure go Environment + +Add the following lines to your ~/.bashrc file as well, also create a golang folder under /home/ec2-user. + +``` +# Go envs +export GOVERSION=go1.20.1 +export GO_INSTALL_DIR=/usr/local/go +export GOROOT=$GO_INSTALL_DIR +export GOPATH=/home/ec2-user/golang +export PATH=$GOROOT/bin:$GOPATH/bin:$PATH +export GO111MODULE="on" +export GOSUMDB=off +``` + +4. Test go + +Verify that you’ve installed Go by opening a command prompt and typing the following command: + +``` +go version +go version go1.20.1 linux/amd64 +``` + +- ### Install [Kind](https://dev.to/rajitpaul_savesoil/setup-kind-kubernetes-in-docker-on-linux-3kbd) + +1. Install Kind + +``` +go install sigs.k8s.io/kind@v0.11.1 +# You can replace v0.11.1 with the latest stable kind version +``` + +2. Move the KinD Binary to /usr/local/bin + +``` +- You can find the kind binary inside the directory go/bin +- Move it to /usr/local/bin - mv go/bin/kind /usr/local/bin +- Make sure you have a path setup for /usr/local/bin +``` + +- ### Install [kubectl](https://dev.to/rajitpaul_savesoil/setup-kind-kubernetes-in-docker-on-linux-3kbd) + +1. Install Latest Version of Kubectl: + +``` +curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +chmod +x kubectl +mv kubectl /usr/local/bin +``` + +- ### Install [helm](https://helm.sh/docs/intro/install/) + +1. Install helm: + +``` +curl -sSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash +``` + +2. We can verify the version + +``` +helm version --short +``` + +- ### Install [python3 (>= 3.7)](https://www.geeksforgeeks.org/how-to-install-python3-on-aws-ec2/) + +1. Check if Python is already installed or not on our AWS EC2. + +``` +python --version +``` + +

+ +

+ +2. At first update, Ubuntu packages by using the following command. + +``` +sudo apt update +``` + +

+ +

+ +3. If Python3 is not installed on your AWS EC2, then install Python3 using the following command. + +``` +sudo apt-get install python3.7 +``` + +4. We have successfully installed Python3 on AWS EC2, to check if Python3 is successfully installed or not, verify using the following command. + +``` +python3 --version +``` + +- ### Install .NET for [linux](https://docs.microsoft.com/en-us/dotnet/core/install/linux-centos) + +1. Before you install .NET, run the following commands to add the Microsoft package signing key to your list of trusted keys and add the Microsoft package repository. Open a terminal and run the following commands: + +``` +sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm +``` + +2. Install the SDK + +``` +sudo yum install dotnet-sdk-7.0 +``` + +3. Install the runtime + +``` +sudo yum install aspnetcore-runtime-7.0 +``` + +- ### We suggest using the [remote code extension](https://code.visualstudio.com/docs/remote/ssh) for VS Code if that is your IDE of choice. + +

+ +

+ +- ### Please see [Our Developer Docs](../developer.md) for more information on how to get started with the codebase. diff --git a/docs/developer/manual-localdev.md b/docs/developer/manual-localdev.md new file mode 100644 index 00000000000..236995857c7 --- /dev/null +++ b/docs/developer/manual-localdev.md @@ -0,0 +1,75 @@ +# Manual Local Development + +Here, we give an overview of a development setup for Armada that gives users full control over the Armada components and dependencies. + +Before starting, please ensure you have installed [Go](https://go.dev/doc/install) (version 1.20 or later), gcc (for Windows, see, e.g., [tdm-gcc](https://jmeubank.github.io/tdm-gcc/)), [mage](https://magefile.org/), [docker](https://docs.docker.com/get-docker/), [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl), and, if you need to compile `.proto` files, [protoc](https://github.com/protocolbuffers/protobuf/releases). + +For a full lust of mage commands, run `mage -l`. + +## Setup + +### Note for Arm/M1 Mac Users + +You will need to set the `PULSAR_IMAGE` enviromental variable to an arm64 image. + +We provide an optimised image for this purpose: + +```bash +export PULSAR_IMAGE=richgross/pulsar:2.11.0 +``` + +```bash +# Download Go dependencies. +go mod tidy + +# Install necessary tooling. +mage BootstrapTools + +# Compile .pb.go files from .proto files +# (only necessary after changing a .proto file). +mage proto +make dotnet + +# Build the Docker images containing all Armada components. +# Only the main "bundle" is needed for quickly testing Armada. +mage buildDockers "bundle,lookout-bundle,jobservice" + +# Setup up a kind (i.e., Kubernetes-in-Docker) cluster; see +# https://kind.sigs.k8s.io/ for details. +mage Kind + +# Start necessary dependencies. +# Verify that dependencies started successfully +# (check that Pulsar has fully started as it is quite slow (~ 1min )). +mage StartDependencies && mage checkForPulsarRunning + +# Start the Armada server and executor. +# Alternatively, run the Armada server and executor directly on the host, +# e.g., through your IDE; see below for details. +docker compose up -d server executor + +# Wait for Armada to come online +mage checkForArmadaRunning +``` + +Run the Armada test suite against the local environment to verify that it is working correctly. +```bash +# Create an Armada queue to submit jobs to. +go run cmd/armadactl/main.go create queue e2e-test-queue + +# To allow Ingress tests to pass +export ARMADA_EXECUTOR_INGRESS_URL="http://localhost" +export ARMADA_EXECUTOR_INGRESS_PORT=5001 + +# Run the Armada test suite against the local environment. +go run cmd/testsuite/main.go test --tests "testsuite/testcases/basic/*" --junit junit.xml +``` + +Tear down the local environment using the following: +```bash +# Stop Armada components and dependencies. +docker compose down + +# Tear down the kind cluster. +mage KindTeardown +``` diff --git a/docs/developer/ubuntu-setup.md b/docs/developer/ubuntu-setup.md new file mode 100644 index 00000000000..6aa02e39a7c --- /dev/null +++ b/docs/developer/ubuntu-setup.md @@ -0,0 +1,164 @@ +# Setting up an Ubuntu Linux instance for Armada development + +## Introduction + +This document is a list of the steps, packages, and tweaks that need to be done to get an Ubuntu Linux +instance running, with all the tools needed for Armada development and testing. + +The packages and steps were verified on an AWS EC2 instance (type t3.xlarge, 4 vcpu, 16GB RAM, +150GB EBS disk), but should be essentially the same on any comparable hardware system. + +### Install Ubuntu Linux + +Install Ubuntu Linux 22.04 (later versions may work as well). The default package set should +work. If you are setting up a new AWS EC2 instance, the default Ubuntu 22.04 image works well. + +When installing, ensure that the network configuration allows: +- SSH traffic from your client IP(s) +- HTTP traffic +- HTTPS traffic + +Apply all recent updates: +``` +$ sudo apt update +$ sudo apt upgrade +``` +You will likely need to reboot after applying the updates: +``` +$ sudo shutdown -r now +``` +After logging in, clean up any old, unused packages: +``` +$ sudo apt autoremove +``` + +AWS usually creates new EC2 instances with a very small root partion (8GB), which will quickly +fill up when using containers, or doing any serious development. Creating a new, large EBS volume, and +attaching it to the instance, will give a system usable for container work. + +First, provision an EBS volume in the AWS Console - of at least 150GB, or more - and attach it to +the instance. You will need to create the EBS volume in the same availability zone as the EC2 +instance - you can find the latter's AZ by clicking on the 'Networking' tab in the details page +for the instance, and you should see the Availabilty Zone listed in that panel. Once you've created +the volume, attach it to the instance. + +Then, format a filesystem on the volume and mount it. First, determine what block device the +parition is on, by running the `lsblk` comand. There should be a line where the TYPE is 'disk' +and the size matches the size you specified when creating the volume - e.g. +``` +nvme1n1 259:4 0 150G 0 disk +``` +Create a filesystem on that device by running `mkfs`: +``` +$ sudo mkfs -t ext4 /dev/nvme1n1 +``` +Then set a label on the partition - here, we will give it a label of 'VOL1': +``` +$ sudo e2label /dev/nvme1n1 VOL1 +``` +Create the mount-point directory: +``` +$ sudo mkdir /vol1 +``` +Add the following line to the end of `/etc/fstab`, so it will be mounted upon reboot: +``` +LABEL=VOL1 /vol1 ext4 defaults 0 2 +``` +Then mount it by doing `sudo mount -a`, and confirm the available space by running `df -h` - the `/vol1` +filesystem should be listed. + +### Install Language/Tool Packages + +Install several development packages that aren't installed by default in the base system: +``` +$ sudo apt install gcc make unzip +``` + +### Install Go, Protobuffers, and kubectl tools +Install the Go compiler and associated tools. Currently, the latest version is 1.20.5, but there may +be newer versions: + +``` +$ curl --location -O https://go.dev/dl/go1.20.5.linux-amd64.tar.gz +$ sudo tar -C /usr/local -xzvf go1.20.5.linux-amd64.tar.gl +$ echo 'export PATH=$PATH:/usr/local/go/bin' > go.sh +$ sudo cp go.sh /etc/profile.d/ +``` +Then, log out and back in again, then run `go version` to verify your path is now correct. + +Install protoc: +``` +$ curl -O --location https://github.com/protocolbuffers/protobuf/releases/download/v23.3/protoc-23.3-linux-x86_64.zip +$ cd /usr/local && sudo unzip ~/protoc-23.3-linux-x86_64.zip +$ cd ~ +$ type protoc +``` + +Install kubectl: +``` +$ curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +$ sudo cp kubectl /usr/local/bin +$ sudo chmod 755 /usr/local/bin/kubectl +$ kubectl version +``` + +### Install Docker + +Warning: do not install Docker as provided by the `docker.io` and other packages in the Ubuntu base +packages repository - the version of Docker they provide is out-of-date. + +Instead, follow the instructions for installing Docker on Ubuntu at https://docs.docker.com/engine/install/ubuntu/ . +Specifically, follow the listed steps for installing using an apt repository, and install the latest Docker version. + +### Relocate Docker storage directory to secondary volume + +Since Docker can use a lot of filesystem space, the directory where it stores container images, logs, +and other datafiles should be relocated to the separate, larger non-root volume on the system, so that +the root filesystem does not fill up. + +Stop the Docker daemon(s) and copy the existing data directory to the new location: +``` +$ sudo systemctl stop docker +$ ps ax | grep -i docker # no Docker processes should be shown + +$ sudo rsync -av /var/lib/docker /vol1/ +$ sudo rm -rf /var/lib/docker +$ sudo ln -s /vol1/docker /var/lib/docker +``` +Then restart Docker and verify that it's working again: +``` +$ sudo systemctl start docker +$ sudo docker ps +$ sudo docker run hello-world +``` + +### Create user accounts, verify docker access + +First, make a home directory parent in the new larger filesystem: +``` +$ sudo mkdir /vol1/home +``` +Then, for each user to be added, run the following steps - we will be using the account named 'testuser' here. +First, create the account and their home directory. +``` +$ sudo adduser --shell /bin/bash --gecos 'Test User' --home /vol1/home/testuser testuser +``` +Set up their $HOME/.ssh directory and add their SSH public-key: +``` +$ sudo mkdir /vol1/home/testuser/.ssh +$ sudo vim /vol1/home/testuser/.ssh/authorized_keys +# In the editor, add the SSH public key string that the user has given you, save the file and exit +$ sudo chmod 600 /vol1/home/testuser/.ssh/authorized_keys +$ sudo chmod 700 /vol1/home/testuser/.ssh +$ sudo chown -R testuser:testuser /vol1/home/testuser/.ssh +``` +Finally, add them to the `docker` group so they can run Docker commands without `sudo` access: +``` +$ sudo gpasswd -a testuser docker +``` +**sudo Access (OPTIONAL)** + +If you want to give the new user `sudo` privileges, run the following command: +``` +$ sudo gpasswd -a testuser sudo +``` diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 00000000000..5f8e06b62f6 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,6 @@ +--- +layout: redirect +sitemap: false +permalink: /development +redirect_to: /developer +--- \ No newline at end of file diff --git a/docs/development_guide.md b/docs/development_guide.md new file mode 100644 index 00000000000..b76a6194282 --- /dev/null +++ b/docs/development_guide.md @@ -0,0 +1,119 @@ +# Development guide + +Here, we give an overview of a development setup for Armada that is closely aligned with how Armada is built and tested in CI. + +Before starting, please ensure you have installed [Go](https://go.dev/doc/install) (version 1.20 or later), gcc (for Windows, see, e.g., [tdm-gcc](https://jmeubank.github.io/tdm-gcc/)), [mage](https://magefile.org/), [docker](https://docs.docker.com/get-docker/), [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl), and, if you need to compile `.proto` files, [protoc](https://github.com/protocolbuffers/protobuf/releases). + +Then, use the following commands to setup a local Armada system. +```bash +# Download Go dependencies. +go mod tidy + +# Install necessary tooling. +mage BootstrapTools + +# Compile .pb.go files from .proto files +# (only necessary after changing a .proto file). +mage proto +make dotnet + +# Build a Docker image containing all Armada components. +mage buildDockers "bundle" + +# Setup up a kind (i.e., Kubernetes-in-Docker) cluster; see +# https://kind.sigs.k8s.io/ for details. +mage Kind + +# Start necessary dependencies. +# Verify that dependencies started successfully +# (check that Pulsar has fully started as it is quite slow (~ 1min )). +mage StartDependencies && mage checkForPulsarRunning + +# Start the Armada server and executor. +# Alternatively, run the Armada server and executor directly on the host, +# e.g., through your IDE; see below for details. +docker-compose up -d server executor +``` + +**Note: the components take ~15 seconds to start up.** + +Run the Armada test suite against the local environment to verify that it is working correctly. +```bash +# Create an Armada queue to submit jobs to. +go run cmd/armadactl/main.go create queue e2e-test-queue + +# To allow Ingress tests to pass +export ARMADA_EXECUTOR_INGRESS_URL="http://localhost" +export ARMADA_EXECUTOR_INGRESS_PORT=5001 + +# Run the Armada test suite against the local environment. +go run cmd/testsuite/main.go test --tests "testsuite/testcases/basic/*" --junit junit.xml +``` + +Tear down the local environment using the following: +```bash +# Stop Armada components and dependencies. +docker-compose down + +# Tear down the kind cluster. +mage KindTeardown +``` + + +## Running the Armada server and executor in Visual Studio Code + +To run the Armada server and executor from Visual Studio Code for debugging purposes, add, e.g., the following config to `.vscode/launch.json` and start both from the "Run and Debug" menu (see the Visual Studio Code [documentation](https://code.visualstudio.com/docs/editor/debugging) for more information). + +```json +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "server", + "type": "go", + "request": "launch", + "mode": "auto", + "env": { + "CGO_ENABLED": "0", + "ARMADA_REDIS_ADDRS": "localhost:6379", + "ARMADA_EVENTSAPIREDIS_ADDRS": "localhost:6379", + "ARMADA_EVENTAPI_POSTGRES_CONNECTION_HOST": "localhost", + "ARMADA_POSTGRES_CONNECTION_HOST": "localhost", + "ARMADA_PULSAR_URL": "pulsar://localhost:6650" + }, + "cwd": "${workspaceFolder}/", + "program": "${workspaceFolder}/cmd/armada/main.go", + "args": [ + "--config", "${workspaceFolder}/localdev/config/armada/config.yaml" + ] + }, + { + "name": "executor", + "type": "go", + "request": "launch", + "mode": "auto", + "env": { + "CGO_ENABLED": "0", + "ARMADA_HTTPPORT": "8081", + "ARMADA_APICONNECTION_ARMADAURL": "localhost:50051", + "KUBECONFIG": "${workspaceFolder}/.kube/external/config" + }, + "cwd": "${workspaceFolder}/", + "program": "${workspaceFolder}/cmd/executor/main.go", + "args": [ + "--config", "${workspaceFolder}/localdev/config/executor/config.yaml" + ] + } + ], + "compounds": [ + { + "name": "server/executor", + "configurations": ["server", "executor"], + "stopAll": true + } + ] +} +``` \ No newline at end of file diff --git a/docs/priority.md b/docs/priority.md new file mode 100644 index 00000000000..74da7596e9b --- /dev/null +++ b/docs/priority.md @@ -0,0 +1,52 @@ +# Armada priority + +This document describes priority calculation algorithm in detail. + +## How is priority calculated + +### Resource usage +Armada schedules jobs which can use multiple types of resources (CPU, memory, GPU, ...). +To get one number which represents the share of a resource by a particular queue, Armada firstly calculates how much of particular +resource is available for one CPU `resource factor`. +Then queue usage can be calculated as `usage = # of cpu + # gpu / gpu factor + # memory / memory factor + ...` + +In example: +If our cluster has 10 CPUs, 20Gb of memory and 5 GPUs.
+GPU factor will be `0.5` and memory factor `2`.
+Queue using 5 CPU, 2 Gb memory and 1 GPU will have usage `5 + 2 / 2 + 1 / 0.5 = 8` . + +### Queue priority +Queue priority is calculated based on current resource usage; if a particular queue usage is constant, the queue priority will approach this number and eventually stabilize on this value. +Armada allows configuration of `priorityHalftime` which influences how quickly queue priority approaches resource usage. + +The formula for priority update is as follows (inspired by Condor priority calculation): + +`priority = priority (1 - beta) + resourceUsage * beta` + +`beta = 0.5 ^ (timeChange / priorityHalftime)` + +### Priority factor +Each queue has a priority factor, this is a multiplicative constant which is applied to the priority. The lower this number is the more resources a queue will be allocated in scheduling. + +`effectivePriority = priority * priorityFactor` + +## Scheduling resources +Available resources are divided between non empty queues based on queue priority. The share allocated to the queue is proportional to inverse of its priority. + +For example if queue `A` has priority `1` and queue `B` priority `2`, `A` will get `2/3` and `B` `1/3` of the resources. + +There are 2 approaches Armada uses to schedule jobs: + +### Slices of resources +When the Executor requests new jobs with information about available resources, resources are divided into slices according to the inverse priority. + +Armada iterates through queues and allocates jobs up to the slice size for each queue. + +Whatever resources remain after this round are scheduled using probabilistic slicing. + +This round is skipped if Armada Server is configured with the option `scheduling.useProbabilisticSchedulingForAllResources = true`. + +### Probabilistic scheduling +To schedule any remaining resources Armada randomly selects a non-empty queue with probability distribution corresponding to the remainders of queue slices. One job from this queue is scheduled, and the queue slice is reduced. This continues until there is no resource available, queues are empty or the scheduling time is up. + +This way there is a chance than one queue will get allocated more than it is entitled to in the scheduling round. However as we are concerned with fair share over the time, rather than in a moment, this does not matter much. Queue priority will compensate for this in the future. diff --git a/docs/production-install.md b/docs/production-install.md new file mode 100644 index 00000000000..3bd8352c1b7 --- /dev/null +++ b/docs/production-install.md @@ -0,0 +1,252 @@ +# Production Installation + +### Prerequisites + +* At least one running Kubernetes cluster + +### Installing Armada Server + +For production it is assumed that the server component runs inside a Kubernetes cluster. + +The below sections will cover how to install the component into Kubernetes. + +#### Recommended prerequisites + + +* Cert manager installed [https://cert-manager.io/docs/installation/helm/#installing-with-helm](https://cert-manager.io/docs/installation/helm/#installing-with-helm) +* gRPC compatible ingress controller installed for gRPC ingress such as [https://github.com/kubernetes/ingress-nginx](https://github.com/kubernetes/ingress-nginx) +* Redis installed [https://github.com/helm/charts/tree/master/stable/redis-ha](https://github.com/helm/charts/tree/master/stable/redis-ha) +* Optionally install NATS streaming server helm chart:[https://github.com/nats-io/k8s/tree/main/helm/charts/stan](https://github.com/nats-io/k8s/tree/main/helm/charts/stan), additional docs: [https://docs.nats.io/running-a-nats-service/nats-kubernetes](https://docs.nats.io/running-a-nats-service/nats-kubernetes) + + +Set `ARMADA_VERSION` environment variable and clone [this repository](https://github.com/armadaproject/armada.git) with the same version tag as you are installing. For example to install version `v1.2.3`: +```bash +export ARMADA_VERSION=v1.2.3 +git clone https://github.com/armadaproject/armada.git --branch $ARMADA_VERSION +``` + +#### Installing server component + +To install the server component, we will use Helm. + +You'll need to provide custom config via the values file, below is a minimal template that you can fill in: + +```yaml +ingressClass: "nginx" +clusterIssuer: "letsencrypt-prod" +hostnames: + - "server.component.url.com" +replicas: 3 + +applicationConfig: + redis: + masterName: "mymaster" + addrs: + - "redis-ha-announce-0.default.svc.cluster.local:26379" + - "redis-ha-announce-1.default.svc.cluster.local:26379" + - "redis-ha-announce-2.default.svc.cluster.local:26379" + poolSize: 1000 + eventsRedis: + masterName: "mymaster" + addrs: + - "redis-ha-announce-0.default.svc.cluster.local:26379" + - "redis-ha-announce-1.default.svc.cluster.local:26379" + - "redis-ha-announce-2.default.svc.cluster.local:26379" + poolSize: 1000 + +basicAuth: + users: + "user1": "password1" +``` + +For all configuration options you can specify in your values file, see [server Helm docs](https://armadaproject.io/helm#server-helm-chat). + +Fill in the appropriate values in the above template and save it as `server-values.yaml` + +Then run: + +```bash +helm install ./deployment/armada --set image.tag=$ARMADA_VERSION -f ./server-values.yaml +``` + +#### Using NATS Streaming +You can optionally setup Armada to route all job events through persistent NATS Streaming subject before saving them to Redis. This is useful if additional application needs to consume events from Armada as NATS subject contains job events from all job sets. + +Required additional server configuration is: + +```yaml +eventsNats: + servers: + - "armada-nats-0.default.svc.cluster.local:4222" + - "armada-nats-1.default.svc.cluster.local:4222" + - "armada-nats-2.default.svc.cluster.local:4222" + clusterID: "nats-cluster-ID" + subject: "ArmadaEvents" + queueGroup: "ArmadaEventsRedisProcessor" +``` + +### Installing Armada Executor + +For production the executor component should run inside the cluster it is "managing". + +To install the executor into a cluster, we will use Helm. + +You'll need to provide custom config via the values file, below is a minimal template that you can fill in: + +```yaml +applicationConfig: + application: + clusterId : "clustername" + apiConnection: + armadaUrl : "server.component.url.com:443" + basicAuth: + username: "user1" + password: "password1" +``` + +
+ +##### Moving Executor off the control plane + +By default, the executor runs on the control plane. + +When that isn't an option, maybe because you are using a managed kubernetes service where you cannot access the master nodes. + +Add the following to your values file: + ```yaml +nodeSelector: null +tolerations: [] +``` +
+ + +For other node configurations and all other executor options you can specify in your values file, see [executor Helm docs](https://armadaproject.io/helm#Executor-helm-chart). + +Fill in the appropriate values in the above template and save it as `executor-values.yaml`. + +Then run: + +```bash +helm install ./deployment/armada-executor --set image.tag=$ARMADA_VERSION -f ./executor-values.yaml +``` +# Interacting with Armada + +Once you have the Armada components running, you can interact with them via the command-line tool called `armadactl`. + +## Setting up armadactl + +`armadactl` connects to `localhost:50051` by default with no authentication. + +For authentication please create a config file described below. + +#### Config file + +By default config is loaded from `$HOME/.armadactl.yaml`. + +You can also set location of the config file using command line argument: + +```bash +armada command --config=/config/location/config.yaml +``` + +The format of this file is a simple yaml file: + +```yaml +armadaUrl: "server.component.url.com:443" +basicAuth: + username: "user1" + password: "password1" +``` + +For Open Id protected server armadactl will perform PKCE flow opening web browser. +Config file should look like this: +```yaml +armadaUrl: "server.component.url.com:443" +openIdConnect: + providerUrl: "https://myproviderurl.com" + clientId: "***" + localPort: 26354 + useAccessToken: true + scopes: [] +``` + +To Invoke an external program to generate an access token, config file should be as follows: +```yaml +armadaUrl: "server.component.url.com:443" +execAuth: + # Command to run. Needs to be on the path and should write only a token to stdout. Required. + cmd: some-command + # Environment variables to set when executing the command. Optional. + args: + - "arg1" + # Arguments to pass when executing the command. Optional. + env: + - name: "FOO" + value: "bar" + # Whether the command requires user input. Optional + interactive: true +``` + +For Kerberos authentication, config file should contain this: +``` +KerberosAuth: + enabled: true +``` + +#### Environment variables + + --- TBC --- + +## Submitting Test Jobs + +For more information about usage please see the [User Guide](./user.md) + +Specify the jobs to be submitted in a yaml file: +```yaml +queue: test +jobSetId: job-set-1 +jobs: + - priority: 0 + podSpec: + terminationGracePeriodSeconds: 0 + restartPolicy: Never + containers: + ... any Kubernetes pod spec ... + +``` + +Use the `armadactl` command line utility to submit jobs to the Armada server +```bash +# create a queue: +armadactl create queue test --priorityFactor 1 + +# submit jobs in yaml file: +armadactl submit ./example/jobs.yaml + +# watch jobs events: +armadactl watch test job-set-1 + +``` + +**Note: Job resource request and limit should be equal. Armada does not support limit > request currently.** + +## Metrics + +All Armada components provide a `/metrics` endpoint providing relevant metrics to the running of the system. + +We actively support Prometheus through our Helm charts (see below for details), however any metrics solution that can scrape an endpoint will work. + +### Component metrics + +#### Server + +The server component provides metrics on the `:9000/metrics` endpoint. + +You can enable Prometheus components when installing with Helm by setting `prometheus.enabled=true`. + +#### Executor + +The executor component provides metrics on the `:9001/metrics` endpoint. + +You can enable Prometheus components when installing with Helm by setting `prometheus.enabled=true`. + diff --git a/docs/production.md b/docs/production.md new file mode 100644 index 00000000000..91a03832f06 --- /dev/null +++ b/docs/production.md @@ -0,0 +1,6 @@ +--- +layout: redirect +sitemap: false +permalink: /production +redirect_to: /production-install +--- \ No newline at end of file diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000000..3419c9816db --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,7 @@ +# Quickstart + +Easiest way to install **Armada** is by using the **Armada Operator**, a Kubernetes operator that manages the lifecycle of **Armada components**. + +The operator is available in the [Armada Operator repository](https://github.com/armadaproject/armada-operator). + +Follow the [Quickstart](https://github.com/armadaproject/armada-operator?tab=readme-ov-file#quickstart) to create your first Armada cluster. \ No newline at end of file diff --git a/docs/quickstart/index.md b/docs/quickstart/index.md new file mode 100644 index 00000000000..bb07ad08308 --- /dev/null +++ b/docs/quickstart/index.md @@ -0,0 +1,7 @@ +# Armada Quickstart + +Easiest way to install **Armada** is by using the **Armada Operator**, a Kubernetes operator that manages the lifecycle of **Armada components**. + +The operator is available in the [Armada Operator repository](https://github.com/armadaproject/armada-operator). + +Follow the [Quickstart](https://github.com/armadaproject/armada-operator?tab=readme-ov-file#quickstart) to create your first Armada cluster. \ No newline at end of file From 119109e1a81f658d1607a87079fae8b3af50fdd3 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:46:34 +0100 Subject: [PATCH 02/17] docs/ folder: remove duplicated quickstart page Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/quickstart/index.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docs/quickstart/index.md diff --git a/docs/quickstart/index.md b/docs/quickstart/index.md deleted file mode 100644 index bb07ad08308..00000000000 --- a/docs/quickstart/index.md +++ /dev/null @@ -1,7 +0,0 @@ -# Armada Quickstart - -Easiest way to install **Armada** is by using the **Armada Operator**, a Kubernetes operator that manages the lifecycle of **Armada components**. - -The operator is available in the [Armada Operator repository](https://github.com/armadaproject/armada-operator). - -Follow the [Quickstart](https://github.com/armadaproject/armada-operator?tab=readme-ov-file#quickstart) to create your first Armada cluster. \ No newline at end of file From 921572fd48bdb01c782dd1a56f84ac9a62ec0ce2 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:48:12 +0100 Subject: [PATCH 03/17] docs/ folder: remove duplicated api page + add permalink Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/api.md | 101 ------------------------------------------ docs/developer/api.md | 4 ++ 2 files changed, 4 insertions(+), 101 deletions(-) delete mode 100644 docs/api.md diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 28191663cb3..00000000000 --- a/docs/api.md +++ /dev/null @@ -1,101 +0,0 @@ -# Armada API - -Armada exposes an API via gRPC or REST. - -## gRPC -The API is defined in `/pkg/api` folder with `*.proto` files as source for all generated code. - -Folder `/pkg/api` also contains generated clients and together with helper methods from `/pkg/client` provides a convenient way to call Armada API from go code. See armadactl code for -[examples](https://github.com/armadaproject/armada/blob/master/cmd/armadactl/cmd/submit.go). - -Following subset of API defined in `/pkg/api` is intended for public use. - -### api.Submit ([definition](https://github.com/armadaproject/armada/blob/master/pkg/api/submit.proto)) - -__/api.Submit/SubmitJobs__ - submitting jobs to be run - -__/api.Submit/CancelJobs__ - cancel jobs - -__/api.Submit/CreateQueue__ - create a new queue - -__/api.Submit/UpdateQueue__ - update an existing queue - -__/api.Submit/DeleteQueue__ - remove queue - -__/api.Submit/GetQueue__ - get information about queue (name, permissions) - -__/api.Submit/GetQueueInfo__ - get information about queued (active jobs, including those currently running) - -### api.Event ([definition](https://github.com/armadaproject/armada/blob/master/pkg/api/submit.proto)) - -__/api.Event/GetJobSetEvents__ - read events of jobs running under particular JobSet - - -### Internal -There are additional API methods defined in proto specifications, which are used by Armada executor and not intended to be used by external users. This API can change in any version. - -- [event.proto](https://github.com/armadaproject/armada/blob/master/pkg/api/event.proto) - methods for event reporting -- [queue.proto](https://github.com/armadaproject/armada/blob/master/pkg/api/queue.proto) - methods related to job leasing by executor -- [usage.proto](https://github.com/armadaproject/armada/blob/master/pkg/api/usage.proto) - methods for reporting of resources usage - -## REST -The REST API only exposes the public part of the gRPC API and it is implemented using [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway). - -Swagger json specification can be found [here](https://github.com/armadaproject/armada/blob/master/pkg/api/api.swagger.json) and is also served by Armada under `my.armada.deployment/api/swagger.json` - -## Authentication - -Both gRPC and REST API support the same set of authentication methods. In the case of gRPC all authentication methods uses `authorization` key in grpc metadata. The REST API use standard http Authorization header (which is translated by grpc-gateway to `authorization` metadata). - -See helm chart [documentation](https://armadaproject.io/helm#Authentication) for different server authentication schemes setup. - -### No Auth -For testing, Armada can be configured to accept no authentication. All operations will use user `anonymous` in this case. - -### OpenId Authentication -When server is configured with OpenID, it will accept authorization header or metadata in the form `Bearer {oauth_token}`. - -### Basic Authentication -For basic authentication API accepts standard authorization header or metadata in the form `basic {base64(user:password)}`. - -### Kerberos -For Kerberos authentication API accepts the same authorization metadata for gRPC as standard Kerberos http SPNEGO authorization headers, the API responds with `WWW-Authenticate` header or metadata. - - -## Permissions - -Armada will determine which actions you are able to perform based on your user's permissions. -These are defined as global or on a per queue basis. - -Below is the list of global Armada permissions (defined [here](https://github.com/armadaproject/armada/blob/master/internal/armada/permissions/permissions.go)): -* `submit_jobs` -* `submit_any_jobs` -* `create_queue` -* `delete_queue` -* `cancel_jobs` -* `cancel_any_jobs` -* `reprioritize_jobs` -* `reprioritize_any_jobs` -* `watch_events` -* `watch_all_events` - -In addition, the following queue-specific permission verbs control what actions can be taken per individual queues (defined [here](https://github.com/armadaproject/armada/blob/master/pkg/client/queue/permission_verb.go)): -* `submit` -* `cancel` -* `reprioritize` -* `watch` - -The table below shows which permissions are required for a user to access each API endpoint (either directly or via a group). -Note queue-specific permission require a user to be bound to a global permission as well (shown as tuples in the table below). - -| Endpoint | Global Permissions | Queue Permissions | -|--------------------|-------------------------|---------------------------------------| -| `SubmitJobs` | `submit_any_jobs` | (`submit_jobs`, `submit`) | -| `CancelJobs` | `cancel_any_jobs` | (`cancel_jobs`, `cancel`) | -| `ReprioritizeJobs` | `reprioritize_any_jobs` | (`reprioritize_jobs`, `reprioritize`) | -| `CreateQueue` | `create_queue` | | -| `UpdateQueue` | `create_queue` | | -| `DeleteQueue` | `delete_queue` | | -| `GetQueue` | | | -| `GetQueueInfo` | `watch_all_events` | (`watch_events`, `watch`) | -| `GetJobSetEvents` | `watch_all_events` | (`watch_events`, `watch`) | diff --git a/docs/developer/api.md b/docs/developer/api.md index 94dea8ec631..f35a19a5d53 100644 --- a/docs/developer/api.md +++ b/docs/developer/api.md @@ -1,3 +1,7 @@ +--- +permalink: /api +--- + # Armada API Armada exposes an API via gRPC or REST. From fc0f0dc0cd69c932e09bb645f4bba53e38deb93e Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:50:24 +0100 Subject: [PATCH 04/17] docs/ folder: remove duplicated production.md + add permalink to production-install.md Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/production-install.md | 4 ++++ docs/production.md | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 docs/production.md diff --git a/docs/production-install.md b/docs/production-install.md index 3bd8352c1b7..2719f46b9f6 100644 --- a/docs/production-install.md +++ b/docs/production-install.md @@ -1,3 +1,7 @@ +--- +permalink: /production +--- + # Production Installation ### Prerequisites diff --git a/docs/production.md b/docs/production.md deleted file mode 100644 index 91a03832f06..00000000000 --- a/docs/production.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: redirect -sitemap: false -permalink: /production -redirect_to: /production-install ---- \ No newline at end of file From 2edd841c7bc6aae7f59071ab33dc7626e4153065 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:54:09 +0100 Subject: [PATCH 05/17] docs/ folder: remove duplicated architecture.md (existing design/architecture.md) Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/architecture.md | 80 -------------------------------------------- 1 file changed, 80 deletions(-) delete mode 100644 docs/architecture.md diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index eacfcad41a8..00000000000 --- a/docs/architecture.md +++ /dev/null @@ -1,80 +0,0 @@ -# Architecture - -Armada is designed to manage millions of batch jobs across compute clusters made up of potentially hundreds of thousands of nodes, while providing near-constant uptime. Hence, the Architecture of Armada must be highly resilient and scalable. The current architecture was chosen in early 2022 to achieve these goals while also ensuring new features, e.g., advanced scheduling techniques, can be delivered. - -At a high level, Armada is a so-called data stream system (sometimes referred to as an event sourcing system), for which there are two components responsible for tracking the state of the system: - -* A log-based message broker that stores state transitions durably in order, referred to throughout this document simply as "the log". -* A set of databases, each deriving its state from the log (but are otherwise mutually independent) and storing a different so-called materialised view of the state of the system. - -The log is a publish-subscribe system consisting of multiple topics to which messages can be published. Those messages are eventually delivered to all subscribers of the topic. Important properties of the log are: - -* Durability: Messages published to the log are stored in a durable manner, i.e., they are not lost in case of up to x node failures, where x is a tuneable parameter. -* Ordering: All subscribers of a topic see messages in the same order as they were published in, and replaying messages on a topic always results in the same message order. Further, all messages on the same topic are annotated with a message id that is monotonically increasing within each topic. - -In Armada, the log is implemented using Apache Pulsar. - -In a data stream system, the log is the source of truth and the databases an optimisation to simplify querying – since the databases can be re-constructed by replaying messages from the log, if the log was replayed for each query, although highly unpractical, the databases could be omitted. For example, in Armada there are separate PostgreSQL databases for storing jobs to be scheduled and the jobs to be shown in the UI, Lookout. Both of these derive their state from the log but are otherwise independent. - -To change the state of the system, a message (e.g., corresponding to a job being submitted) is published to the log. Later, that message is picked up by a log processor, which updates some database accordingly (in the case of a job being submitted, by storing the new job in the database). Hence, the log serialises state transitions and the database is a materialised view of part of the state of the system, as derived from the state transitions submitted to the log. In effect, a data stream system is a bespoke distributed database with the log acting as the transaction log. - -This approach has several benefits: - -* Resiliency towards bursts of high load: Because the log buffers state transitions, the components reading from the log and acting on those transitions are not directly affected by incoming requests. -* Simplicity and extensibility: Adding new materialised views (e.g., for a new dashboard) can be accomplished by adding a new subscriber to the log. This new subscriber has the same source of truth as all others (i.e., the log) but is loosely coupled to those components; adding or removing views does not affect other components of the system. -* Consistency: When storing state across several independent databases, those databases are guaranteed to eventually be consistent; there is no failure scenario where the different databases become permanently inconsistent, thus requiring a human to manually reconcile them (assuming acting on state transitions is idempotent). - -However, the approach also has some drawbacks: - -* Eventual consistency: Because each database is updated from the log independently, they do not necessarily represent the state of the system at the same point of time. For example, a job may be written to the scheduler database (thus making it eligible for scheduling) before it shows up in the UI. -* Timeliness: Because databases are updated from the log asynchronously, there may be a lag between a message being published and the system being updated to reflect the change (e.g., a submitted job may not show up in the UI immediately). - -## System overview - -Besides the log, Armada consists of the following components: - -* Submit API: Clients (i.e., users) connect to this API to request state transitions (e.g., submitting jobs or updating job priorities) and each such state transition is communicated to the rest of the system by writing to the log (more detail on this below). -* Streams API: Clients connect to this API to subscribe to log messages for a particular set of jobs. Armada components can receive messages either via this API or directly from the log, but users have to go via the streams API to isolate them from internal messages. -* Scheduler: A log processor responsible for maintaining a global view of the system and preempting and scheduling jobs. Preemption and scheduling decisions are communicated to he rest of the system by writing to the log. -* Executors: Each executor is responsible for one Kubernetes worker cluster and is the component that communicates between the Armada scheduler and the Kubernetes API of the cluster it is responsible for. -* Lookout: The web UI showing the current state of the system. Lookout maintains its views by reading log messages to populate its database. - -### Job submission logic - -Here, we outline the sequence of actions resulting from submitting a job. - -1. A client submits a job to the submit-query API, which is composed of a Kubernetes podspec and some Armada-specific metadata (e.g., the priority of the job). -2. The submit API authenticates and authorizes the user, validates the submitted job, and, if valid, submits the job spec. to the log. The submit API annotates each job with a randomly generated UUID that uniquely identifies the job. This UUID is returned to the user. -3. The scheduler receives the job spec. and stores it in-memory (discarding any data it doesn't need, such as the pod spec.). The scheduler runs periodically, at which point it schedules queued jobs. At the start of each scheduling run, the scheduler queries each executor for its available resources. The scheduler uses this information in making scheduling decisions. When the scheduler assigns a job to an executor, it submits a message to the log indicating this state transition. It also updates its in-memory storage immediately to reflect the change (to avoid scheduling the same job twice). -4. A log processor receives the message indicating the job was scheduled, and writes this decision to a database acting as the interface between the scheduler and the executor. -5. Periodically, each executor queries the database for the list of jobs it should be running. It compares that list with the list of jobs it is actually running and makes changes necessary to reconcile any differences. -6. When a job has finished, the executor responsible for running the job informs the scheduler, which on its behalf submits a "job finished" message to the log. The same log processor as in step 4. updates its database to reflect that the job has finished. - -### Streams API - -Armada does not maintain a user-queryable database of the current state of the system. This is by design to avoid overloading the system with connections. For example, say there is one million active jobs in the system and that there are clients who want to track the state of all of those jobs. With a current-state-of-the-world database, those client would need to resort to polling that database to catch any updates, thus opening a total of one million connections to the database, which, while not impossible to manage, would pose significant challenges. - -Instead, users are expected to be notified of updates to their jobs via an event stream (i.e., the streams API), where a client opens a single connection for all jobs in a so-called job set over which all state transitions are streamed as they happen. This approach is highly scalable since data is only sent when something happens and since a single connection that contain updates for thousands of jobs. Users who want to maintain a view of their jobs are thus responsible for maintaining that view themselves by subscribing to events. - -## Notes on consistency - -The data stream approach taken by Armada is not the only way to maintain consistency across views. Here, we compare this approach with the two other possible solutions. - -Armada stores its state across several databases. Whenever Armada receives an API call to update its state, all those databases need to be updated. However, if each database were to be updated independently it is possible for some of those updates to succeed while others fail, leading to an inconsistent application state. It would require complex logic to detect and correct for such partial failures. However, even with such logic we could not guarantee that the application state is consistent; if Armada crashes before it has had time to correct for the partial failure the application may remain in an inconsistent state. - -There are three commonly used approaches to address this issue: - -* Store all state in a single database with support for transactions. Changes are submitted atomically and are rolled back in case of failure; there are no partial failures. -* Distributed transaction frameworks (e.g., X/Open XA), which extend the notation of transactions to operations involving several databases. -* Ordered idempotent updates. - -The first approach results in tight coupling between components and would limit us to a single database technology. Adding a new component (e.g., a new dashboard) could break existing component since all operations part of the transaction are rolled back if one fails. The second approach allows us to use multiple databases (as long as they support the distributed transaction framework), but components are still tightly coupled since they have to be part of the same transaction. Further, there are performance concerns associated with these options, since transactions may not be easily scalable. Hence, we use the third approach, which we explain next. - -First, note that if we can replay the sequence of state transitions that led to the current state, in case of a crash we can recover the correct state by truncating the database and replaying all transitions from the beginning of time. Because operations are ordered, this always results in the same end state. If we also, for each database, store the id of the most recent transition successfully applied to that database, we only need to replay transitions more recent than that. This saves us from having to start over from a clean database; because we know where we left off we can keep going from there. For this to work, we need transactions but not distributed transactions. Essentially, applying a transition already written to the database results in a no-op, i.e., the updates are idempotent (meaning that applying the same update twice has the same effect as applying it once). - -The two principal drawbacks of this approach are: - -* Eventual consistency: Whereas the first two approaches result in a system that is always consistent, with the third approach, because databases are updated independently, there will be some replication lag during which some part of the state may be inconsistent. -* Timeliness: There is some delay between submitting a change and that change being reflected in the application state. - -Working around eventual consistency requires some care, but is not impossible. For example, it is fine for the UI to show the a job as "running" for a few seconds after the job has finished before showing "completed". Regarding timeliness, it is not a problem if there is a few seconds delay between a job being submitted and the job being considered for queueing. However, poor timeliness may lead to clients (i.e., the entity submitting jobs to the system) not being able to read their own writes for some time, which may lead to confusion (i.e., there may be some delay between a client submitting a job a that job showing as "pending"). This issue can be worked around by storing the set of submitted jobs in-memory either at the client or at the API endpoint. From 40a8b6db375e9eae2fc4b7e382efa9234bda24a8 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:58:39 +0100 Subject: [PATCH 06/17] docs/ folder: remove duplicated design.md (existing design/index.md) Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/design.md | 72 -------------------------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 docs/design.md diff --git a/docs/design.md b/docs/design.md deleted file mode 100644 index afd8210019e..00000000000 --- a/docs/design.md +++ /dev/null @@ -1,72 +0,0 @@ -# System overview - -This document is meant to be an overview of Armada for new users. We cover the architecture of Armada, show how jobs are represented, and explain how jobs are queued and scheduled. - -If you just want to learn how to submit jobs to Armada, see: - -- [User guide](./user.md) - -## Architecture - -Armada consists of two main components: -- The Armada server, which is responsible for accepting jobs from users and deciding in what order, and on which Kubernetes cluster, jobs should run. Users submit jobs to the Armada server through the `armadactl` command-line utility or via a gRPC or REST API. -- The Armada executor, of which there is one instance running in each Kubernetes cluster that Armada is connected to. Each Armada executor instance regularly notifies the server of how much spare capacity it has available and requests jobs to run. Users of Armada never interact with the executor directly. - -All state relating to the Armada server is stored in [Redis](https://redis.io/), which may use replication combined with failover for redundancy. Hence, the Armada server is itself stateless and is easily replicated by running multiple independent instances. Both the server and the executors are intended to be run in Kubernetes pods. We show a diagram of the architecture below. - -![How Armada works](./assets/img/batch-api.svg) - -### Job leasing - -To avoid jobs being lost if a cluster (or cluster executor) becomes unavailable, each job assigned to an executor has an associated timeout. Armada executors are required to check in with the server regularly and, if an executor responsible for running a particular job fails to check in within that timeout, the server will re-schedule the job over another executor. - -## Jobs and job sets - -A job is the most basic unit of work in Armada, and is represented by a Kubernetes pod specification (podspec) with additional metadata specific to Armada. Armada handles creating, running, and removing containers as necessary for each job. Hence, Armada is essentially a system for managing the life cycle of a set of containerised applications representing a batch job. - -The Armada workflow is: - -1. Create a job specification, which is a Kubernetes podspec with a few additional metadata fields. -2. Submit the job specification to one of Armada's job queues using the `armadactl` CLI utility or through the Armada gRPC or REST API. - -For example, a job that sleeps for 60 seconds could be represented by the following yaml file. - -```yaml -queue: test -jobSetId: set1 -jobs: - - priority: 0 - podSpecs: - - terminationGracePeriodSeconds: 0 - restartPolicy: Never - containers: - - name: sleep - imagePullPolicy: IfNotPresent - image: busybox:latest - args: - - sleep - - 60s - resources: - limits: - memory: 64Mi - cpu: 150m - requests: - memory: 64Mi - cpu: 150m -``` - -In the above yaml snippet, `podSpec` is a Kubernetes podspec, which consists of one or more containers that contain the user code to be run. In addition, the job specification (jobspec) contains metadata fields specific to Armada: - -- `queue`: which of the available job queues the job should be submitted to. -- `priority`: the job priority (lower values indicate higher priority). -- `jobSetId`: jobs with the same `jobSetId` can be followed and cancelled in a single operation. The `jobSetId` has no impact on scheduling. - -Queues and scheduling is explained in more detail below. - -For more examples, including of jobs spanning multiple nodes, see the [user guide](./user.md). - -### Job events - -A job event is generated whenever the state of a job changes (e.g., when changing from submitted to running or from running to completed) and is a timestamped message containing event-specific information (e.g., an exit code for a completed job). All events generated by jobs part of the same job set are grouped together and published via a [Redis stream](https://redis.io/topics/streams-intro). There are unique streams for each job set to facilitate subscribing only to events generated by jobs in a particular set, which can be done via the Armada API. - -Armada records all events necessary to reconstruct the state of each job and, after a job has been completed, the only information retained about the job is the events generated by it. From aa61c44815253e33441b1511fc72ec51d022d0e6 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:01:59 +0100 Subject: [PATCH 07/17] docs/ folder: mv developer.md to developer/index.md Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/{developer.md => developer/index.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{developer.md => developer/index.md} (100%) diff --git a/docs/developer.md b/docs/developer/index.md similarity index 100% rename from docs/developer.md rename to docs/developer/index.md From 58e40a23817e3b4f6666a3f1e7024bf6622ef54f Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:05:34 +0100 Subject: [PATCH 08/17] docs/ folder: rm development.md (redirect only file, should be added to website/ folder) Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/development.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 docs/development.md diff --git a/docs/development.md b/docs/development.md deleted file mode 100644 index 5f8e06b62f6..00000000000 --- a/docs/development.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: redirect -sitemap: false -permalink: /development -redirect_to: /developer ---- \ No newline at end of file From 6430f8cb89c3d162ee05d517fa148d9bad253ef4 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:12:52 +0100 Subject: [PATCH 09/17] fix broken links in developer/index.md Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/developer/index.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/developer/index.md b/docs/developer/index.md index 315f7e5bfa7..f5c03d832a1 100644 --- a/docs/developer/index.md +++ b/docs/developer/index.md @@ -32,22 +32,22 @@ Feel free to create a ticket if you encounter any issues, and link them to the r Please see these documents for more information about Armadas Design: -* [Armada Components Diagram](./design/relationships_diagram.md) -* [Armada Architecture](./design/architecture.md) -* [Armada Design](./design/index.md) -* [How Priority Functions](./design/priority.md) -* [Armada Scheduler Design](./design/scheduler.md) +* [Armada Components Diagram](../design/relationships_diagram.md) +* [Armada Architecture](../design/architecture.md) +* [Armada Design](../design/index.md) +* [How Priority Functions](../design/priority.md) +* [Armada Scheduler Design](../design/scheduler.md) ## Other Useful Developer Docs -* [Armada API](./developer/api.md) -* [Running Armada in an EC2 Instance](./developer/aws-ec2.md) -* [Armada UI](./developer/ui.md) -* [Usage Metrics](./developer/usage_metrics.md) -* [Using OIDC with Armada](./developer/oidc.md) -* [Building the Website](./developer/website.md) -* [Using Localdev Manually](./developer/manual-localdev.md) -* [Inspecting and Debugging etcd in Localdev setup](./developer/etc-localdev.md) +* [Armada API](./api.md) +* [Running Armada in an EC2 Instance](./aws-ec2.md) +* [Armada UI](./ui.md) +* [Usage Metrics](./usage_metrics.md) +* [Using OIDC with Armada](./oidc.md) +* [Building the Website](./website.md) +* [Using Localdev Manually](./manual-localdev.md) +* [Inspecting and Debugging etcd in Localdev setup](./etc-localdev.md) ## Pre-requisites @@ -129,7 +129,7 @@ go run cmd/testsuite/main.go test --tests "testsuite/testcases/basic/*" --junit In LocalDev, the UI is built seperately with `mage ui`. To access it, open http://localhost:8089 in your browser. -For more information see the [UI Developer Guide](./developer/ui.md). +For more information see the [UI Developer Guide](./ui.md). ### Choosing components to run @@ -172,7 +172,7 @@ It supports the following commands: ### VSCode Debugging After running `mage debug vscode`, you can attach to the running processes using VSCode. -The launch.json file can be found [Here](../developer/debug/launch.json) +The launch.json file can be found [Here](../../developer/debug/launch.json) For using VSCode debugging, see the [VSCode Debugging Guide](https://code.visualstudio.com/docs/editor/debugging). @@ -256,4 +256,4 @@ For required enviromental variables, please see [The Enviromental Variables Guid ## Finer-Grain Control If you would like to run the individual mage targets yourself, you can do so. -See the [Manually Running LocalDev](./developer/manual-localdev.md) guide for more information. +See the [Manually Running LocalDev](./manual-localdev.md) guide for more information. From 1355c534cea89651dd0fd675464ecb4772f8160d Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:16:19 +0100 Subject: [PATCH 10/17] move scheduler.md to design/scheduler.md Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/design/scheduler.md | 309 +++++++++++++++++++++++++++------------ docs/scheduler.md | 279 ----------------------------------- 2 files changed, 217 insertions(+), 371 deletions(-) delete mode 100644 docs/scheduler.md diff --git a/docs/design/scheduler.md b/docs/design/scheduler.md index e8e6afff538..90c55d501db 100644 --- a/docs/design/scheduler.md +++ b/docs/design/scheduler.md @@ -1,132 +1,108 @@ # Scheduler -The scheduler is the component of Armada responsible for determining when and where each job should run. Here, we describe how it works, including its support for +Here, we give an overview of the algorithm used by Armada to determine which jobs to schedule and preempt. This algorithm runs within the scheduling subsystem of the Armada control plane. Note that Armada does not rely on kube-scheduler (or other in-cluster schedulers) for scheduling and preemption. -* fair resource allocation, -* bin-packing, -* gang-scheduling, and -* preemption (urgency-based and to fair share). +Scheduling requires balancing throughput, timeliness, and fairness. The Armada scheduler operates according to the following principles: -## Jobs +- Throughput: Maximise resource utilisation by scheduling jobs onto available nodes whenever possible. +- Timeliness: Schedule more urgent jobs before less urgent jobs. Always preempt less urgent jobs to make room for more urgent jobs, but never the opposite. +- Fairness: Schedule jobs from queues allocated a smaller fraction of their fair share of resources before those allocated a larger fraction. Always preempt jobs from queues with a larger fraction of their fair share if doing so helps schedule jobs from queues with a smaller fraction. -An Armada job represents a computational task to be carried out and consists of a bag of Kubernetes objects (e.g., pods, services, and ingresses) with a shared life cycle, i.e., all objects are created at the start of the job (within the same cluster) and are destroyed at its end. Each job is a member of a queue (e.g., corresponding to a particular user) and a job set (a per-queue logical grouping of jobs that can be managed as a unit), and has a priority associated with it, i.e., each job has associated with it a tuple (queue, jobSet, priority). Each job also has a priority class (PC) associated with it; see the section on preemption. +The Armada scheduler also satisfies (with some exceptions as noted throughout the documentation) the following properties: -Each job is submitted to a specific queue and is stored in that queue while waiting to be scheduled. Job sets typically represent workflows consisting of multiple jobs. For example, if fitting a machine learning model against several different data sets, there may be a separate job for each data set, and these jobs could all be members of the same job set. Job sets can be monitored and, e.g., cancelled as a unit, which simplifies managing large numbers of jobs. +- Sharing incentive: Each user should be better off sharing the cluster than exclusively using their own partition of the cluster. +- Strategy-proofness: Users should not benefit from lying about their resource requirements. +- Envy-freeness: A user should not prefer the allocation of another. This property embodies the notion of fairness. +- Pareto efficiency: It should not be possible to increase the allocation of a user without decreasing the allocation of another. +- Preemption stability: Whenever a job is preempted and subsequently re-submitted before any other jobs have completed, the re-submitted job should not trigger further preemptions. -Armada jobs are created by submitting a job specification to the Armada server via either the gRPC API, client libraries, or the armadactl command-line-utility. +Next, we cover some specific features of the scheduler. -## Resource usage and fairness - -Each job has a cost associated with it that is the basis of fair resource allocation. In particular, Armada tries to balance the aggregate cost of jobs between queues. Specifically, the cost of each job is a weighted sum of all resources requested by that job. For example, a job requesting CPU, GPU, and RAM, has cost - -CPU + w_GPU * GPU + w_RAM * RAM, - -where w_GPU and w_RAM is the cost of GPU and RAM relative to 1 CPU. Currently, w_GPU=1 and w_RAM=0. - -Further, the cost associated with each queue is the sum of the cost of all jobs in the pending or running state associated with the queue; this per-queue cost is what the Armada scheduler tries to balance. (This notion of fairness is sometimes referred to as asset fairness.) Specifically, each queue has a weight associated with it that determines the size of its fair share. If we denote these per-queue weights by - -w_1, ..., w_N, +## Jobs and queues -where N is the number of active queues (see below) and by +Each Armada job represents a computational task to be carried out and consists of: -w = sum(w_1, ..., w_N) +- A Kubernetes podspec representing the workload to be run. +- Auxiliary Kubernetes objects, e.g., services and ingresses. -their sum, then the fair share of each of the N queues is +All objects that make up the job are created at job startup and are deleted when the job terminates. -w_1 / w, ..., w_N / w. +Jobs are annotated with the following Armada-specific metadata: -This computation only includes active queues, i.e., queues for which there are jobs in the queued, pending, or running state. Hence, the fair share of a queue may vary over time as other queues transition between active and inactive. Armada considers the cost associated with each queue (more specifically, the fraction of its fair share each queue is currently assigned) when selecting which job to schedule next; see the following section. +- Queue: Each job belongs to a queue, which is the basis for fair share in Armada. +- Job set: A per-queue logical grouping of jobs meant to make it easier to manage large number of jobs. Jobs within the same job set can be managed as a unit. +- Priority: Controls the order in which jobs appear in the queue. +- Armada priority class, which itself contains a priority that controls preemption. -## Job scheduling order +Jobs are totally ordered within each queue by: -Armada schedules one job at a time, and choosing the order in which jobs are attempted to be scheduled is the mechanism by which Armada ensures resources are divided fairly between queues. In particular, jobs within each queue are ordered by per-job priorities set by the user, but there is no inherent ordering between jobs associated with different queues; the scheduler is responsible for establishing such a global ordering. To divide resources fairly, Armada establishes such a global ordering as follows: +1. Priority class priority. +2. Job priority. +3. Time submitted. -1. For each queue, find the next schedulable job in that queue, where jobs are considered in order of per-job priority first and submission time second. -2. For each queue, compute what fraction of its fair share the queue would have if the next schedulable job were to be scheduled. -3. Select for scheduling the next schedulable job from the queue for which this computation resulted in the smallest fraction of fair share. +Armada attempts to schedule one job at a time. When scheduling from a particular queue, Armada chooses the next job to schedule according to this order. -Including the next schedulable job in the computation in step 2. is important since the next job may be a gang job requesting thousands of nodes. - -This approach is sometimes referred to as progressive filling and is known to achieve max-min fairness, i.e., for an allocation computed in this way, an attempt to increase the allocation of one queue necessarily results in decreasing the allocation of some other queue with equal or smaller fraction of its fair share, under certain conditions, e.g., when the increments are sufficiently small. +## Resource usage and fairness -Note that when finding the next schedulable job, there is a limit to the number of jobs considered, i.e., there may be schedulable job in a queue blocked behind a long sequence of unschedulable jobs. +Armada divides resources fairly between queues. In particular, Armada will schedule and preempt jobs to balance the vector -## Bin-packing -When assigning jobs to nodes, Armada adheres to the following principles: +``` +c_1/w_1, c_2/w_2, ..., c_n/w_n +``` -* When choosing a node to assign a job to, Armada first considers the set of nodes on which the queue the job is associated with is the only user. If not schedulable on any of those nodes, Armada considers the set of unused nodes, and, only if not schedulable there either, considers nodes with multiple users. This principle is important to reduce interference between jobs associated with different queues during the scheduling cycle and serves to reduce the number of preemptions necessary when preempting to fair share (see the section on preemption). -* When choosing among the nodes in such a set, Armada attempts to schedule the job onto the node with the smallest amount of available resources on which the job fits. This principle is important to increase the amount of contiguous resources available, thus facilitating scheduling large jobs later – Armada is optimised for large jobs in this way since the difficulty of scheduling a job increases with its size; very small jobs are typically easy to schedule regardless. +where `c_i` and `w_i`, is the cost and weight associated with the `i`-th active queue, respectively, and `n` is the number of active queues. Only active queues, i.e., queues with jobs either queued or running, are considered when computing fairness. Hence, the fair share of a queue may change over time as other queues become active or inactive. -Together, these principles result in jobs being bin-packed on a per-queue basis – the second step above can be seen as greedy bin-packing solver. +The cost of each queue is computed from the resource requests of running jobs originating from the queue. In particular, Armada relies on dominant resource fairness to compute cost, such that -This approach comes with an important trade-off compared global bin-packing in that it reduces cross-queue job contention at the expense of potentially increasing inter-queue job contention. I.e., each user has a greater level of control of how the resources on a node are utilised – since a user submitting a large number of jobs is likely to be the only user on most of the nodes assigned to those jobs. However, this approach also results in jobs that are likely to have similar resource usage profiles being clustered together – since jobs originating from the same queue are more likely to, e.g., consume large amounts of network bandwidth at the same time, than jobs originating from different queues. We opt for giving users the greater level of control since it can allow for overall more performant applications (hence, this is also the approach typically taken in the high-performance computing community). +``` +c_i = max(cpu_i/cpu_total, memory_i/memory_total, ...) +``` -## Gang scheduling -Armada supports gang scheduling of jobs, i.e., all-or-nothing scheduling of a set of jobs, such that all jobs in the gang are scheduled onto the same cluster at the same time or not at all. Specifically, Armada implicitly groups jobs using a special annotation set on the pod spec embedded in the job. A set of jobs (not necessarily a "job set") for which the value of this annotation is the same across all jobs in the set is referred to as a gang. All jobs in a gang are gang-scheduled onto the same cluster at the same time. The cluster is chosen dynamically by the scheduler and does not need to be pre-specified. +where `cpu_i` is the total CPU allocated to jobs from the `i`-th queue and so on, and the totals is the total amount of resources available for scheduling. This fairness model has several desirable proprties; see -Details related to the gang-scheduling algorithm: +- [Dominant Resource Fairness: Fair Allocation of Multiple Resource Types](https://amplab.cs.berkeley.edu/wp-content/uploads/2011/06/Dominant-Resource-Fairness-Fair-Allocation-of-Multiple-Resource-Types.pdf) -* Jobs are grouped using the armadaproject.io/gangId annotation. The value of the armadaproject.io/gangId annotation must not be re-used. For example, a unique UUID generated for each gang is a good choice. -* All jobs in a gang must also specify the total number of jobs in the gang using another annotation armadaproject.io/gangCardinality. It's the responsibility of the submitter to ensure this value is set correctly on each job that makes up a gang. -* All jobs in a gang must be submitted within the same request to Armada. This is to ensure that Armada can validate at submit-time that all jobs in the gang are present. -* During scheduling, Armada iterates over jobs. Whenever the Armada scheduler find a job that sets the armadaproject.io/gangId annotation, it stores that job in a separate place. Armada only considers these jobs for scheduling once it has found all of the jobs that make up the gang. Note that the scheduler object already supports several pods. +The weight of each queue is the reciprocal of its priority factor, which is configured on a per-queue basis. -## Preemption +## Priority classes and preemption Armada supports two forms of preemption: -1. Urgency-based preemption, i.e., making room for a job by preempting less urgent jobs. This form of preemption works in the same way as Kubernetes priority classes (PCs). +1. Urgency-based preemption, i.e., making room for a job by preempting less urgent jobs. This form of preemption works in the same way as the normal Kubernetes preemption. 2. Preemption to fair share, i.e., preempting jobs belonging to users with more than their fair share of resources, such that those resources can be re-allocated to improve fairness. -These forms of preemption are driven by separate processes and operate independently of each other. Incidentally, preemption also makes it easier to schedule large jobs, since currently running jobs can be preempted to make room for them. - -Both forms of preemption are based on Armada job PCs, each of which is represented by a tuple (name, priority, isPreemptible). Armada comes with two PCs by default: - -* (armada-default, 30000, false) -* (armada-preemptible, 20000, true) +Both forms of preemption are based on Armada priority classes (PCs). These are similar to but distinct from Kubernetes PCs. All Armada jobs have an Armada PC associated with them. Each Armada PC is represented by the following fields: -Job priority classes are set by setting the priorityClassName field of the embedded podspec. Jobs with no PC are automatically assigned the armada-default PC (i.e., preemption is opt-in). We describe both forms of preemption in more detail below. +- name: A unique name associated with each Armada PC. +- priority: An integer encoding the urgency of jobs with this PC. Jobs with a PC with higher priority can always preempt jobs with a PC with lower priority. +- isFairSharePreemptible: A boolean indicating whether jobs with this PC can be preempted via preemption to fair share. Note that all jobs can be preempted via urgency-based preemption, unless there is no other job with a higher PC priority. -### Urgency-based preemption +Job priority classes are set by setting the `priorityClassName` field of the podspec embedded in the job. Jobs with no PC are automatically assigned one. We describe both forms of preemption in more detail below. -When scheduling a job, Armada may preempt other jobs to make room for it. Specifically, Armada may preempt running preemptible jobs with a lower-priority PC. Hence, the PC priority of a job expresses how urgent a job is. In this way, PCs support the following use cases: - -* A user wants to run, e.g., a speculative job and doesn't want to occupy the farm if there are more urgent jobs to run. To that end, the user chooses a low-priority preemptible PC when submitting it. If there are more urgent jobs to run (i.e., with a higher-priority PC), those jobs may preempt the submitted job. -* A user has an urgent job they want to be run immediately. To that end, they choose a high-priority PC, such that it may preempt currently running preemptible jobs to make room for itself. - -Hence, this is a cooperative form of preemption that requires users to coordinate among themselves to set appropriate PCs. - -Urgency-based preemption is implemented in Armada via tracking the allocatable resources at different PC priorities. For example, if a 32-core node has running on it - -* an armada-default job requesting 10 cores and -* an armada-preemptible job requesting 20 cores, - -then Armada understands that up to 22 cores can be allocated to armada-default jobs (allocating more than 2 cores is possible by preempting the armada-preemptible job) and up to 2 cores can be allocated to armada-preemptible jobs. If during scheduling a job needs to be preempted, kube-scheduler (a Kubernetes component) makes the decision on which job should be preempted. - -### Preemption to fair share - -Whereas urgency-based preemption is triggered by a job being submitted that can not be scheduled without preempting a lower-urgency job, preemption to fair share is triggered whenever a queue A has been allocated more than their fair share of resources – and at least some of those jobs are preemptible – and another queue B has jobs of equal PC priority queued. Recall that the fair share of each queue is computed from the set of currently active queues, i.e., queue A may be exceeding its fair share because a previously inactive queue B has become active as a result of jobs being submitted to it, thus reducing the size of the fair share of queue A. - -For example, if jobs of PC armada-preemptible are submitted to queue A and are allocated all available resources, and later jobs of PC armada-preemptible are submitted to queue B, Armada would preempt some of the jobs of user A to make room for the jobs submitted by user B (the fraction of resources reclaimed from queue A depends on the relative weights of the two user's queues). +## Gang scheduling -At a high level, preemption to fair share works as follows: +Armada supports gang scheduling of jobs, i.e., all-or-nothing scheduling of a set of jobs. Gang scheduling is controlled by the following annotations set on individual jobs submitted to Armada: -1. Some preemptible jobs are evicted from the nodes to which they are assigned and are placed at the front of their respective queues. Specifically, for each node, all preemptible jobs on that node are evicted with a configurable probability. If a job part of gang is evicted, all other jobs in that gang that are still running are also evicted, regardless of which node they are assigned to. This happens only within the scheduler, i.e., no running jobs are preempted at this stage. The cost of the evicted jobs are subtracted from the cost of their respective queues. -2. Armada performs a scheduling cycle, thus computing a mapping from jobs to nodes as if the evicted jobs had never started running and were still queued. Recall that Armada selects which queue to schedule from next based on what fraction of its fair share the queue is currently allocated. Hence, any queue above its fair share is unlikely to have all of its evicted jobs re-scheduled as part of the cycle (unless other jobs for some reason can not be scheduled). -3. Any jobs that were evicted and not re-scheduled as part of step 2. are preempted. Any jobs scheduled in step 2. that were not previously evicted are new jobs that should be allowed to start running. Because evicted jobs are placed at the front of each queue, Armada will always, for each queue, try to re-schedule evicted jobs before trying to schedule now jobs. +* `armadaproject.io/gangId`: Jobs with the same value for this annotation are considered part of the same gang. The value of this annotation should not be re-used. For example, a unique UUID generated for each gang is a good choice. For jobs that do not set this annotation, a randomly generated value is filled in, i.e., single jobs are considered gangs of cardinality one. +* `armadaproject.io/gangCardinality`: Total number of jobs in the gang. The Armada scheduler relies on this value to know when it has collected all jobs that make up the gang. It is the responsibility of the submitter to ensure this value is set correctly for gangs. +* `armadaproject.io/gangNodeUniformityLabel`: Constrains the jobs that make up a gang to be scheduled across a uniform set of nodes. Specifically, if set, all gang jobs are scheduled onto nodes for which the value of the provided label is equal. This can be used to ensure, e.g., that all gang jobs are scheduled onto the same cluster or rack. -In this way Armada makes a unified of which jobs should be preempted and scheduled. It is then the responsibility of the rest of the system to reconcile against this new desired state. +## Node selection and bin-packing -For this process to be effective, i.e., not cause large numbers of avoidable preemptions, the mapping from jobs to nodes must be stable across invocations of the scheduler. We use two strategies to increase stability: +Armada schedules one job at a time. This process consists of: -* Evicted jobs may only be scheduled onto the node they were evicted from, i.e., no moving jobs between nodes. -* Jobs are bin-packed on a per-queue basis. This reduces interference between queues during the scheduling cycle. +1. Selecting a job to schedule. +2. Assigning the selected job to a node. -For example, consider a scenario with two queues A and B of equal weight and two nodes – node 1 and node 2 – with 32 cores each. Say that initially 40 1-core preemptible jobs are submitted to queue A, 32 of which are scheduled onto node 1 and 8 on node 2. Later, 50 1-core preemptible jobs are submitted to queue B. During the next scheduler invocation, Armada evicts all jobs from nodes 1 and 2 and places these at the front of queue A. At this point, there are 40 jobs in queue A and 50 jobs in queue B. Then, Armada starts scheduling from both queue A and B. Because Armada selects which queue to schedule from next based on which queue is currently assigned the smallest fraction of its fair share, Armada will alternate between scheduling from queue A and B, such that at the end of the scheduling cycle, 32 jobs are scheduled from each queue; the scheduling cycle will have terminated because both node 1 and 2 are full at this point. For queue A, all these jobs are re-scheduled evicted jobs, whereas for queue B the 32 scheduled jobs are new jobs. For queue A, 8 evicted jobs were not re-scheduled and are thus preempted. For queue B, 18 jobs could not be scheduled and are still queued. +Here, we explain the second step. Armada adheres to the following principles in the order listed: -The above example also illustrates why per-queue bin-packing (as opposed to bin-packing in a non-queue-aware manner) is important; because Armada alternates between scheduling from queue A and B, non-queue-aware bin-packing would fill up node 1 with 16 jobs from each queue before scheduling onto node 2, thus unnecessarily preempting 16 jobs from queue A. Per-queue bin-packing, on the other hand, avoids scheduling jobs from queue B onto node 1 when possible, which results in the 32 jobs of queue B all being scheduled onto node 2. +1. Avoid preempting running jobs if possible. +2. If necessary, preempt according to the following principles: + 1. Preempt jobs of as low PC priority as possible. + 2. For jobs of equal PC priority, preempt jobs from queues allocated the largest fraction of fair share possible. +3. Assign to a node with the smallest amount of resources possible. -To control the rate of preemptions, the expected fraction of currently running jobs considered for preemption to fair share is configurable. Specifically, for each node, the preemptible jobs on that node are evicted with a configurable probability. +These principles result in Armada doing the best it can to avoid preemptions, or at least preempt fairly, and then greedily bin-packing jobs. ## Graceful termination @@ -146,9 +122,158 @@ Jobs that explicitly set a termination period higher than the limit will be reje ## Job deadlines -All Armada jobs can be assigned default job deadlines, i.e., jobs have a default maximum runtime after which the job will be killed. These defaults are: +All Armada jobs can be assigned default job deadlines, i.e., jobs have a default maximum runtime after which the job will be killed. Default deadlines are only added to jobs that do not already specify one. To manually specify a deadline, set the `ActiveDeadlineSeconds` field of the podspec embedded in the job. + +## Scheduler: implementation + +Each scheduling cycle can be seen as a pure function that takes the current state as its input and returns a new desired state. We could express this in code as the following function: + +```go +// Schedule determines which queued jobs to schedule and which running jobs to preempt. +func Schedule( + // Map from queues to the jobs in that queue. + queues map[Queue][]Job + // Nodes over which to schedule jobs. + nodes []Node + // Map from jobs to nodes. Queued jobs are not assigned to any node. + nodeByJob map[Job]Node +) map[Job]Node { + // Scheduling logic + ... + + // Return an updated mapping from jobs to nodes. + // - nodeByJob[job] == nil && updatedNodeByJob != nil: job was scheduled. + // - nodeByJob[job] != nil && updatedNodeByJob == nil: job was preempted. + // - nodeByJob[job] == updatedNodeByJob: no change for job. + return updatedNodeByJob +} +``` + +Each scheduling cycle thus produces a mapping from jobs to nodes that is the desired state of the system. It is the responsibility of the rest of the system to reconcile any differences between the actual and desired state. Note that in actuality the scheduler does maintain state between iterations for efficiency and for certain features (e.g., rate-limiting). + +Each scheduling cycle in turn consists of the following steps: + +1. Eviction +2. Queue ordering +3. Job scheduling: + 1. Job selection + 2. Node selection + +Which we express in pseudocode as (a more detailed version of the above snippet): + +```go +func Schedule(queues map[Queue][]Job, nodes []Node, nodeByJob map[Job]Node) map[Job]Node { + queues, nodeByJob := evict(queues, nodeByJob) + queues = sortQueues(queues) + for { + gang := selectNextQueuedGangToSchedule(queues) + if gang == nil { + // No more jobs to schedule. + break + } + nodeByJobForGang := selectNodesForGang(gang, nodes) + copy(nodeByJob, nodeByJobForGang) + } + return nodeByJob +} + +func evict(queues map[Queue][]Job, nodeByJob map[Job]Node) (map[Queue][]Job, map[Job]Node) { + updatedNodeByJob := make(map[Job]Node) + for job, node := range nodeByJob { + if isFairSharePreemptible(job) { + queue := queueFromJob(job) + queues[queue] = append(queues[queue], job) + } else { + updatedNodeByJob[job] = node + } + } + return updatedNodeByJob +} + +func sortQueues(queues map[Queue][]Job) map[Queue][]Job { + updatedQueues := clone(queues) + for queue, jobs := range queues { + updatedQueues[queue] = sort(jobs, sortFunc) + } + return updatedQueues +} + +func selectNextQueuedGangToSchedule(queues map[Queue][]Job) Gang { + var Gang selectedGang + var Queue selectedQueue + for queue, jobs := range queues { + // Consider jobs in the order they appear in the queue. + gang := firstGangFromJobs(jobs) + // Select the queue with smallest fraction of its fair share. + if fractionOfFairShare(queue, gang) < fractionOfFairShare(selectedQueue, selectedGang) { + selectedJob = job + selectedQueue = queue + } + } + // Remove the selected gang from its queue. + popFirstGang(queues[selectedQueue]) + return gang +} + +func selectNodesForGang(gang Gang, nodes []Node) map[Job]Node { + nodes = clone(nodes) + nodeByJob := make(map[Job]Node) + for _, job := range jobsFromGang(gang) { + node = selectNodeForJob(job, nodes) + nodeByJob[job] = node + } + return nodeByJob +} + +func selectNodeForJob(job Job, nodes []Node) Node { + var Node selectedNode + for _, node := range nodes { + if jobFitsOnNode(job, node) { + if fitScore(job, node) > fitScore(job, selectedNode) { + selectedNode = node + } + } + } + return selectedNode +} +``` + +Next, we explain eviction and job ordering in more detail. + +### Eviction + +Eviction is part of the preemption strategy used by Armada. It consists of, at the start of each cycle, moving all currently running preemptible jobs from the nodes to which they are assigned back to their respective queues. As a result, those jobs appear to Armada as if they had never been scheduled and are still queued. We refer to such jobs moved back to the queue as *evicted*. + +Whether a job is evicted or not and whether it is assigned to a node in the job scheduling stage or not determines which jobs are scheduled, preempted, or neither. Specifically: + +- Not evicted and assigned a node: Queued jobs that should be scheduled. +- Not evicted and not assigned a node: Queued jobs that remain queued. +- Evicted and assigned a node: Running jobs that should remain running. +- Evicted and not assigned a node: Running jobs that should be preempted. + +Eviction and (re-)scheduling thus provides a unified mechanism for scheduling and preemption. This approach comes with several benefits: + +- No need to maintain separate scheduling and preemption algorithms. Improvements to scheduling also improves preemption. +- We are guaranteed there are no preemptions that do not help scheduling new jobs without needing to check specifically that is the case. +- Preemption and scheduling is consistent in the sense that a job that was preempted will if re-submitted not be scheduled. +- Many-to-many preemption, i.e., one preemption may facilitate scheduling several other jobs. + +There are two caveats for which some care needs to be taken: + +1. Evicted jobs may only be re-scheduled onto the node from which they were evicted. +2. We should avoid preventing re-scheduling evicted jobs when scheduling new jobs. + +To address these issues, Armada maintains a record of which node each job was evicted from that is used when assigning jobs to nodes. + + +### Job scheduling order + +Armada schedules one job at a time, and choosing the order in which jobs are attempted to be scheduled is the mechanism by which Armada ensures resources are divided fairly between queues. In particular, jobs within each queue are totally ordered, but there is no inherent ordering between jobs associated with different queues; the scheduler is responsible for establishing such a global ordering. To divide resources fairly, Armada establishes such a global ordering as follows: + +1. Get the topmost gang (which may be a single job) in each queue. +2. For each queue, compute what fraction of its fair share the queue would have if the topmost gang were to be scheduled. +3. Select for scheduling the next gang from the queue for which this computation resulted in the smallest fraction of fair share. + +Including the topmost gang in the computation in step 2. is important since it may request thousands of nodes. -* CPU jobs: 3 days -* GPU jobs: 14 days - -Default deadlines are only added to jobs that do not already specify one. To manually specify a deadline, set the ActiveDeadlineSeconds field of the pod spec embedded in the job; see https://github.com/kubernetes/api/blob/master/core/v1/types.go#L3182 +This approach is sometimes referred to as progressive filling and is known to achieve max-min fairness, i.e., for an allocation computed in this way, an attempt to increase the allocation of one queue necessarily results in decreasing the allocation of some other queue with equal or smaller fraction of its fair share, under certain conditions, e.g., when the increments are sufficiently small. diff --git a/docs/scheduler.md b/docs/scheduler.md deleted file mode 100644 index 90c55d501db..00000000000 --- a/docs/scheduler.md +++ /dev/null @@ -1,279 +0,0 @@ -# Scheduler - -Here, we give an overview of the algorithm used by Armada to determine which jobs to schedule and preempt. This algorithm runs within the scheduling subsystem of the Armada control plane. Note that Armada does not rely on kube-scheduler (or other in-cluster schedulers) for scheduling and preemption. - -Scheduling requires balancing throughput, timeliness, and fairness. The Armada scheduler operates according to the following principles: - -- Throughput: Maximise resource utilisation by scheduling jobs onto available nodes whenever possible. -- Timeliness: Schedule more urgent jobs before less urgent jobs. Always preempt less urgent jobs to make room for more urgent jobs, but never the opposite. -- Fairness: Schedule jobs from queues allocated a smaller fraction of their fair share of resources before those allocated a larger fraction. Always preempt jobs from queues with a larger fraction of their fair share if doing so helps schedule jobs from queues with a smaller fraction. - -The Armada scheduler also satisfies (with some exceptions as noted throughout the documentation) the following properties: - -- Sharing incentive: Each user should be better off sharing the cluster than exclusively using their own partition of the cluster. -- Strategy-proofness: Users should not benefit from lying about their resource requirements. -- Envy-freeness: A user should not prefer the allocation of another. This property embodies the notion of fairness. -- Pareto efficiency: It should not be possible to increase the allocation of a user without decreasing the allocation of another. -- Preemption stability: Whenever a job is preempted and subsequently re-submitted before any other jobs have completed, the re-submitted job should not trigger further preemptions. - -Next, we cover some specific features of the scheduler. - -## Jobs and queues - -Each Armada job represents a computational task to be carried out and consists of: - -- A Kubernetes podspec representing the workload to be run. -- Auxiliary Kubernetes objects, e.g., services and ingresses. - -All objects that make up the job are created at job startup and are deleted when the job terminates. - -Jobs are annotated with the following Armada-specific metadata: - -- Queue: Each job belongs to a queue, which is the basis for fair share in Armada. -- Job set: A per-queue logical grouping of jobs meant to make it easier to manage large number of jobs. Jobs within the same job set can be managed as a unit. -- Priority: Controls the order in which jobs appear in the queue. -- Armada priority class, which itself contains a priority that controls preemption. - -Jobs are totally ordered within each queue by: - -1. Priority class priority. -2. Job priority. -3. Time submitted. - -Armada attempts to schedule one job at a time. When scheduling from a particular queue, Armada chooses the next job to schedule according to this order. - -## Resource usage and fairness - -Armada divides resources fairly between queues. In particular, Armada will schedule and preempt jobs to balance the vector - -``` -c_1/w_1, c_2/w_2, ..., c_n/w_n -``` - -where `c_i` and `w_i`, is the cost and weight associated with the `i`-th active queue, respectively, and `n` is the number of active queues. Only active queues, i.e., queues with jobs either queued or running, are considered when computing fairness. Hence, the fair share of a queue may change over time as other queues become active or inactive. - -The cost of each queue is computed from the resource requests of running jobs originating from the queue. In particular, Armada relies on dominant resource fairness to compute cost, such that - -``` -c_i = max(cpu_i/cpu_total, memory_i/memory_total, ...) -``` - -where `cpu_i` is the total CPU allocated to jobs from the `i`-th queue and so on, and the totals is the total amount of resources available for scheduling. This fairness model has several desirable proprties; see - -- [Dominant Resource Fairness: Fair Allocation of Multiple Resource Types](https://amplab.cs.berkeley.edu/wp-content/uploads/2011/06/Dominant-Resource-Fairness-Fair-Allocation-of-Multiple-Resource-Types.pdf) - -The weight of each queue is the reciprocal of its priority factor, which is configured on a per-queue basis. - -## Priority classes and preemption - -Armada supports two forms of preemption: - -1. Urgency-based preemption, i.e., making room for a job by preempting less urgent jobs. This form of preemption works in the same way as the normal Kubernetes preemption. -2. Preemption to fair share, i.e., preempting jobs belonging to users with more than their fair share of resources, such that those resources can be re-allocated to improve fairness. - -Both forms of preemption are based on Armada priority classes (PCs). These are similar to but distinct from Kubernetes PCs. All Armada jobs have an Armada PC associated with them. Each Armada PC is represented by the following fields: - -- name: A unique name associated with each Armada PC. -- priority: An integer encoding the urgency of jobs with this PC. Jobs with a PC with higher priority can always preempt jobs with a PC with lower priority. -- isFairSharePreemptible: A boolean indicating whether jobs with this PC can be preempted via preemption to fair share. Note that all jobs can be preempted via urgency-based preemption, unless there is no other job with a higher PC priority. - -Job priority classes are set by setting the `priorityClassName` field of the podspec embedded in the job. Jobs with no PC are automatically assigned one. We describe both forms of preemption in more detail below. - -## Gang scheduling - -Armada supports gang scheduling of jobs, i.e., all-or-nothing scheduling of a set of jobs. Gang scheduling is controlled by the following annotations set on individual jobs submitted to Armada: - -* `armadaproject.io/gangId`: Jobs with the same value for this annotation are considered part of the same gang. The value of this annotation should not be re-used. For example, a unique UUID generated for each gang is a good choice. For jobs that do not set this annotation, a randomly generated value is filled in, i.e., single jobs are considered gangs of cardinality one. -* `armadaproject.io/gangCardinality`: Total number of jobs in the gang. The Armada scheduler relies on this value to know when it has collected all jobs that make up the gang. It is the responsibility of the submitter to ensure this value is set correctly for gangs. -* `armadaproject.io/gangNodeUniformityLabel`: Constrains the jobs that make up a gang to be scheduled across a uniform set of nodes. Specifically, if set, all gang jobs are scheduled onto nodes for which the value of the provided label is equal. This can be used to ensure, e.g., that all gang jobs are scheduled onto the same cluster or rack. - -## Node selection and bin-packing - -Armada schedules one job at a time. This process consists of: - -1. Selecting a job to schedule. -2. Assigning the selected job to a node. - -Here, we explain the second step. Armada adheres to the following principles in the order listed: - -1. Avoid preempting running jobs if possible. -2. If necessary, preempt according to the following principles: - 1. Preempt jobs of as low PC priority as possible. - 2. For jobs of equal PC priority, preempt jobs from queues allocated the largest fraction of fair share possible. -3. Assign to a node with the smallest amount of resources possible. - -These principles result in Armada doing the best it can to avoid preemptions, or at least preempt fairly, and then greedily bin-packing jobs. - -## Graceful termination - -Armada will sometimes kill pods, e.g., because the pod is being preempted or because the corresponding job has been cancelled. Pods can optionally specify a graceful termination period, i.e., an amount of time that the pod is given to exit gracefully before being terminated. Graceful termination works as follows: - -1. The pod is sent a SIGTERM. Kubernetes stores the time at which the signal was sent. -2. After the amount of time specified in the graceful termination period field of the pod has elapsed, the job is sent a SIGKILL if still running. - -Armada implements graceful termination periods as follows: - -* Jobs submitted to Armada with no graceful termination period set, are assigned a 1s period. -* Armada validates that all submitted jobs have a termination period between 1s and a configurable maximum. It is important that the minimum is positive, since a 0s grace period is interpreted by Kubernetes as a "force delete", which may result in inconsistencies due to pods being deleted from etcd before they have stopped running. - -Jobs that set a termination period of 0s will be updated in-place at submission to have a 1s termination period. This is to ensure that current workflows, some of which unnecessarily set a 0s termination period, are not disrupted. - -Jobs that explicitly set a termination period higher than the limit will be rejected at submission. Jobs that set a termination period greater than 0s but less than 1s will also be rejected at submission. - -## Job deadlines - -All Armada jobs can be assigned default job deadlines, i.e., jobs have a default maximum runtime after which the job will be killed. Default deadlines are only added to jobs that do not already specify one. To manually specify a deadline, set the `ActiveDeadlineSeconds` field of the podspec embedded in the job. - -## Scheduler: implementation - -Each scheduling cycle can be seen as a pure function that takes the current state as its input and returns a new desired state. We could express this in code as the following function: - -```go -// Schedule determines which queued jobs to schedule and which running jobs to preempt. -func Schedule( - // Map from queues to the jobs in that queue. - queues map[Queue][]Job - // Nodes over which to schedule jobs. - nodes []Node - // Map from jobs to nodes. Queued jobs are not assigned to any node. - nodeByJob map[Job]Node -) map[Job]Node { - // Scheduling logic - ... - - // Return an updated mapping from jobs to nodes. - // - nodeByJob[job] == nil && updatedNodeByJob != nil: job was scheduled. - // - nodeByJob[job] != nil && updatedNodeByJob == nil: job was preempted. - // - nodeByJob[job] == updatedNodeByJob: no change for job. - return updatedNodeByJob -} -``` - -Each scheduling cycle thus produces a mapping from jobs to nodes that is the desired state of the system. It is the responsibility of the rest of the system to reconcile any differences between the actual and desired state. Note that in actuality the scheduler does maintain state between iterations for efficiency and for certain features (e.g., rate-limiting). - -Each scheduling cycle in turn consists of the following steps: - -1. Eviction -2. Queue ordering -3. Job scheduling: - 1. Job selection - 2. Node selection - -Which we express in pseudocode as (a more detailed version of the above snippet): - -```go -func Schedule(queues map[Queue][]Job, nodes []Node, nodeByJob map[Job]Node) map[Job]Node { - queues, nodeByJob := evict(queues, nodeByJob) - queues = sortQueues(queues) - for { - gang := selectNextQueuedGangToSchedule(queues) - if gang == nil { - // No more jobs to schedule. - break - } - nodeByJobForGang := selectNodesForGang(gang, nodes) - copy(nodeByJob, nodeByJobForGang) - } - return nodeByJob -} - -func evict(queues map[Queue][]Job, nodeByJob map[Job]Node) (map[Queue][]Job, map[Job]Node) { - updatedNodeByJob := make(map[Job]Node) - for job, node := range nodeByJob { - if isFairSharePreemptible(job) { - queue := queueFromJob(job) - queues[queue] = append(queues[queue], job) - } else { - updatedNodeByJob[job] = node - } - } - return updatedNodeByJob -} - -func sortQueues(queues map[Queue][]Job) map[Queue][]Job { - updatedQueues := clone(queues) - for queue, jobs := range queues { - updatedQueues[queue] = sort(jobs, sortFunc) - } - return updatedQueues -} - -func selectNextQueuedGangToSchedule(queues map[Queue][]Job) Gang { - var Gang selectedGang - var Queue selectedQueue - for queue, jobs := range queues { - // Consider jobs in the order they appear in the queue. - gang := firstGangFromJobs(jobs) - // Select the queue with smallest fraction of its fair share. - if fractionOfFairShare(queue, gang) < fractionOfFairShare(selectedQueue, selectedGang) { - selectedJob = job - selectedQueue = queue - } - } - // Remove the selected gang from its queue. - popFirstGang(queues[selectedQueue]) - return gang -} - -func selectNodesForGang(gang Gang, nodes []Node) map[Job]Node { - nodes = clone(nodes) - nodeByJob := make(map[Job]Node) - for _, job := range jobsFromGang(gang) { - node = selectNodeForJob(job, nodes) - nodeByJob[job] = node - } - return nodeByJob -} - -func selectNodeForJob(job Job, nodes []Node) Node { - var Node selectedNode - for _, node := range nodes { - if jobFitsOnNode(job, node) { - if fitScore(job, node) > fitScore(job, selectedNode) { - selectedNode = node - } - } - } - return selectedNode -} -``` - -Next, we explain eviction and job ordering in more detail. - -### Eviction - -Eviction is part of the preemption strategy used by Armada. It consists of, at the start of each cycle, moving all currently running preemptible jobs from the nodes to which they are assigned back to their respective queues. As a result, those jobs appear to Armada as if they had never been scheduled and are still queued. We refer to such jobs moved back to the queue as *evicted*. - -Whether a job is evicted or not and whether it is assigned to a node in the job scheduling stage or not determines which jobs are scheduled, preempted, or neither. Specifically: - -- Not evicted and assigned a node: Queued jobs that should be scheduled. -- Not evicted and not assigned a node: Queued jobs that remain queued. -- Evicted and assigned a node: Running jobs that should remain running. -- Evicted and not assigned a node: Running jobs that should be preempted. - -Eviction and (re-)scheduling thus provides a unified mechanism for scheduling and preemption. This approach comes with several benefits: - -- No need to maintain separate scheduling and preemption algorithms. Improvements to scheduling also improves preemption. -- We are guaranteed there are no preemptions that do not help scheduling new jobs without needing to check specifically that is the case. -- Preemption and scheduling is consistent in the sense that a job that was preempted will if re-submitted not be scheduled. -- Many-to-many preemption, i.e., one preemption may facilitate scheduling several other jobs. - -There are two caveats for which some care needs to be taken: - -1. Evicted jobs may only be re-scheduled onto the node from which they were evicted. -2. We should avoid preventing re-scheduling evicted jobs when scheduling new jobs. - -To address these issues, Armada maintains a record of which node each job was evicted from that is used when assigning jobs to nodes. - - -### Job scheduling order - -Armada schedules one job at a time, and choosing the order in which jobs are attempted to be scheduled is the mechanism by which Armada ensures resources are divided fairly between queues. In particular, jobs within each queue are totally ordered, but there is no inherent ordering between jobs associated with different queues; the scheduler is responsible for establishing such a global ordering. To divide resources fairly, Armada establishes such a global ordering as follows: - -1. Get the topmost gang (which may be a single job) in each queue. -2. For each queue, compute what fraction of its fair share the queue would have if the topmost gang were to be scheduled. -3. Select for scheduling the next gang from the queue for which this computation resulted in the smallest fraction of fair share. - -Including the topmost gang in the computation in step 2. is important since it may request thousands of nodes. - -This approach is sometimes referred to as progressive filling and is known to achieve max-min fairness, i.e., for an allocation computed in this way, an attempt to increase the allocation of one queue necessarily results in decreasing the allocation of some other queue with equal or smaller fraction of its fair share, under certain conditions, e.g., when the increments are sufficiently small. From 59972015c892fed0adf4c7c5ad840e49da10a525 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:18:34 +0100 Subject: [PATCH 11/17] docs/ folder: rm duplicated priority.md (existing design/priority.md) Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/priority.md | 52 ------------------------------------------------ 1 file changed, 52 deletions(-) delete mode 100644 docs/priority.md diff --git a/docs/priority.md b/docs/priority.md deleted file mode 100644 index 74da7596e9b..00000000000 --- a/docs/priority.md +++ /dev/null @@ -1,52 +0,0 @@ -# Armada priority - -This document describes priority calculation algorithm in detail. - -## How is priority calculated - -### Resource usage -Armada schedules jobs which can use multiple types of resources (CPU, memory, GPU, ...). -To get one number which represents the share of a resource by a particular queue, Armada firstly calculates how much of particular -resource is available for one CPU `resource factor`. -Then queue usage can be calculated as `usage = # of cpu + # gpu / gpu factor + # memory / memory factor + ...` - -In example: -If our cluster has 10 CPUs, 20Gb of memory and 5 GPUs.
-GPU factor will be `0.5` and memory factor `2`.
-Queue using 5 CPU, 2 Gb memory and 1 GPU will have usage `5 + 2 / 2 + 1 / 0.5 = 8` . - -### Queue priority -Queue priority is calculated based on current resource usage; if a particular queue usage is constant, the queue priority will approach this number and eventually stabilize on this value. -Armada allows configuration of `priorityHalftime` which influences how quickly queue priority approaches resource usage. - -The formula for priority update is as follows (inspired by Condor priority calculation): - -`priority = priority (1 - beta) + resourceUsage * beta` - -`beta = 0.5 ^ (timeChange / priorityHalftime)` - -### Priority factor -Each queue has a priority factor, this is a multiplicative constant which is applied to the priority. The lower this number is the more resources a queue will be allocated in scheduling. - -`effectivePriority = priority * priorityFactor` - -## Scheduling resources -Available resources are divided between non empty queues based on queue priority. The share allocated to the queue is proportional to inverse of its priority. - -For example if queue `A` has priority `1` and queue `B` priority `2`, `A` will get `2/3` and `B` `1/3` of the resources. - -There are 2 approaches Armada uses to schedule jobs: - -### Slices of resources -When the Executor requests new jobs with information about available resources, resources are divided into slices according to the inverse priority. - -Armada iterates through queues and allocates jobs up to the slice size for each queue. - -Whatever resources remain after this round are scheduled using probabilistic slicing. - -This round is skipped if Armada Server is configured with the option `scheduling.useProbabilisticSchedulingForAllResources = true`. - -### Probabilistic scheduling -To schedule any remaining resources Armada randomly selects a non-empty queue with probability distribution corresponding to the remainders of queue slices. One job from this queue is scheduled, and the queue slice is reduced. This continues until there is no resource available, queues are empty or the scheduling time is up. - -This way there is a chance than one queue will get allocated more than it is entitled to in the scheduling round. However as we are concerned with fair share over the time, rather than in a moment, this does not matter much. Queue priority will compensate for this in the future. From dbfd1bb212657ef69149d65a8318e896867b2b10 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:25:51 +0100 Subject: [PATCH 12/17] docs/ folder: fix names (use README.md instead of index.md to enable browsing on GitHub) Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/{docs-readme.md => README.md} | 0 docs/design/{index.md => README.md} | 0 docs/developer/{index.md => README.md} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename docs/{docs-readme.md => README.md} (100%) rename docs/design/{index.md => README.md} (100%) rename docs/developer/{index.md => README.md} (100%) diff --git a/docs/docs-readme.md b/docs/README.md similarity index 100% rename from docs/docs-readme.md rename to docs/README.md diff --git a/docs/design/index.md b/docs/design/README.md similarity index 100% rename from docs/design/index.md rename to docs/design/README.md diff --git a/docs/developer/index.md b/docs/developer/README.md similarity index 100% rename from docs/developer/index.md rename to docs/developer/README.md From fb1089745756618e37689d19e04d37b73b56b4cf Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:27:36 +0100 Subject: [PATCH 13/17] docs/ folder: mv batch-api.svg to design/ Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/design/README.md | 2 +- docs/{ => design}/batch-api.svg | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/{ => design}/batch-api.svg (100%) diff --git a/docs/design/README.md b/docs/design/README.md index 77d263275da..590bfb4c8e7 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -18,7 +18,7 @@ Armada consists of two main components: All state relating to the Armada server is stored in [Redis](https://redis.io/), which may use replication combined with failover for redundancy. Hence, the Armada server is itself stateless and is easily replicated by running multiple independent instances. Both the server and the executors are intended to be run in Kubernetes pods. We show a diagram of the architecture below. -![How Armada works](../assets/img/batch-api.svg) +![How Armada works](./batch-api.svg) ### Job leasing diff --git a/docs/batch-api.svg b/docs/design/batch-api.svg similarity index 100% rename from docs/batch-api.svg rename to docs/design/batch-api.svg From ad5319d49f31659114b813948329a2bed5ecac9b Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:43:30 +0100 Subject: [PATCH 14/17] docs/ folder: fix broken links Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/developer/README.md | 2 +- docs/developer/aws-ec2.md | 2 +- docs/user.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/developer/README.md b/docs/developer/README.md index f5c03d832a1..1573cc18f07 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -34,7 +34,7 @@ Please see these documents for more information about Armadas Design: * [Armada Components Diagram](../design/relationships_diagram.md) * [Armada Architecture](../design/architecture.md) -* [Armada Design](../design/index.md) +* [Armada Design](../design/README.md) * [How Priority Functions](../design/priority.md) * [Armada Scheduler Design](../design/scheduler.md) diff --git a/docs/developer/aws-ec2.md b/docs/developer/aws-ec2.md index b3909aa4e36..bbe4be3ed07 100644 --- a/docs/developer/aws-ec2.md +++ b/docs/developer/aws-ec2.md @@ -233,4 +233,4 @@ sudo yum install aspnetcore-runtime-7.0

-- ### Please see [Our Developer Docs](../developer.md) for more information on how to get started with the codebase. +- ### Please see [Our Developer Docs](./README.md) for more information on how to get started with the codebase. diff --git a/docs/user.md b/docs/user.md index 1c6d6763fb6..6eb505f75f0 100644 --- a/docs/user.md +++ b/docs/user.md @@ -7,7 +7,7 @@ This document is meant to be a guide for new users of how to create and submit J For more information about the design of Armada (e.g., how jobs are prioritised), see: -- [System overview](./design.md) +- [System overview](./design/README.md) The Armada workflow is: From a51c924222d7264880dffd0ca7f67c3ba8b45e86 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:47:24 +0100 Subject: [PATCH 15/17] docs/ folder: replace NBSP with space in Markdown files Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/consistency.md | 2 +- docs/design/architecture.md | 2 +- docs/python_armada_client.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/consistency.md b/docs/consistency.md index 7b1644e7568..5d39cb7157d 100644 --- a/docs/consistency.md +++ b/docs/consistency.md @@ -7,7 +7,7 @@ Armada stores its state across several databases. Whenever Armada receives an AP There are three commonly used approaches to address this issue: * Store all state in a single database with support for transactions. Changes are submitted atomically and are rolled back in case of failure; there are no partial failures. -* Distributed transaction frameworks (e.g., X/Open XA), which extend the notation of transactions to operations involving several databases. +* Distributed transaction frameworks (e.g., X/Open XA), which extend the notation of transactions to operations involving several databases. * Ordered idempotent updates. The first approach results in tight coupling between components and would limit us to a single database technology. Adding a new component (e.g., a new dashboard) could break existing component since all operations part of the transaction are rolled back if one fails. The second approach allows us to use multiple databases (as long as they support the distributed transaction framework), but components are still tightly coupled since they have to be part of the same transaction. Further, there are performance concerns associated with these options, since transactions may not be easily scalable. Hence, we use the third approach, which we explain next. diff --git a/docs/design/architecture.md b/docs/design/architecture.md index eacfcad41a8..ecc0f02af22 100644 --- a/docs/design/architecture.md +++ b/docs/design/architecture.md @@ -65,7 +65,7 @@ Armada stores its state across several databases. Whenever Armada receives an AP There are three commonly used approaches to address this issue: * Store all state in a single database with support for transactions. Changes are submitted atomically and are rolled back in case of failure; there are no partial failures. -* Distributed transaction frameworks (e.g., X/Open XA), which extend the notation of transactions to operations involving several databases. +* Distributed transaction frameworks (e.g., X/Open XA), which extend the notation of transactions to operations involving several databases. * Ordered idempotent updates. The first approach results in tight coupling between components and would limit us to a single database technology. Adding a new component (e.g., a new dashboard) could break existing component since all operations part of the transaction are rolled back if one fails. The second approach allows us to use multiple databases (as long as they support the distributed transaction framework), but components are still tightly coupled since they have to be part of the same transaction. Further, there are performance concerns associated with these options, since transactions may not be easily scalable. Hence, we use the third approach, which we explain next. diff --git a/docs/python_armada_client.md b/docs/python_armada_client.md index 44cc6ff9abd..a6d55bd5541 100644 --- a/docs/python_armada_client.md +++ b/docs/python_armada_client.md @@ -691,10 +691,10 @@ Subject is a NamedTuple that represents a subject in the permission system. * **Fields** - 1.  **kind** (`str`) – Alias for field number 0 + 1. **kind** (`str`) – Alias for field number 0 - 2.  **name** (`str`) – Alias for field number 1 + 2. **name** (`str`) – Alias for field number 1 From f265bbf21515da4d76ff4eab41cc4561d0a5b736 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:56:52 +0100 Subject: [PATCH 16/17] update documentation for docs/ Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/README.md | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/README.md b/docs/README.md index 43c5808bfcd..a5649d90d0d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,31 +1,34 @@ -# Docs Readme +# Docs + +This folder contains the documentation for the Armada project. The documentation is written in markdown and is rendered as webpages on [armadaproject.io](https://armadaproject.io). + +It's accessible from the IDE, GitHub, and the website. ## For Developers See [website.md](./developer/website.md) ## Overview -Docs added to this directory are automatically copied into armadaproject.io. + +Docs added to this the `docs/` folder are automatically copied into [armadaproject.io](https://armadaproject.io). For example, if you wanted to document bananas, and you added `bananas.md`, once committed to master that would be published at `https://armadaproject.io/bananas/`. -## Complex pages with assets -If you'd like to add a more complex page, such as one with images or other -linked assets, you have to be very careful to ensure links will work both -for people viewing in github and for those viewing via armadaproject.io. +> [!NOTE] +> All files in `docs/` folder are rendered as webpage except this `README.md` file. -The easiest way to accomplish this is by using page bundles. See quickstart -as as example: quickstart/index.md is the actual content, with links to -various images using relative pathing; e.g. `./my-image.png`. This is -considered a page bundle by jekyll (github pages) and are rendered as a -single page at `quickstart/`. +## Pages with assets -In order to get this page bundle pushed to gh-pages branch, you'll need -to adjust the github workflow in `.github/workflows/pages.yml` to add your -new page bundle as well. +If you'd like to add a more complex page, such as one with images or other +linked assets, you have to be careful to ensure links will work both +for people viewing in GitHub and for those viewing via [armadaproject.io](https://armadaproject.io). + +The easiest way to accomplish this is by using page bundles. Assets should be located inside the `docs/` folder and +used in the markdown file with relative paths. ## Removing pages -If you put a commit here to remove a page, you will need to also commit -to the gh-pages branch to remove that page. + +Any page that is removed from the `docs/` folder will be removed from the website automatically. The `docs/` folder is +the source of truth for the website's content. From 6321298ae804372b09d8323f8f1194e7681256c9 Mon Sep 17 00:00:00 2001 From: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:37:47 +0100 Subject: [PATCH 17/17] fix: use generated file python_armada_client.md Signed-off-by: Mehdi Nassim KHODJA <18899702+naskio@users.noreply.github.com> --- docs/python_armada_client.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/python_armada_client.md b/docs/python_armada_client.md index a6d55bd5541..44cc6ff9abd 100644 --- a/docs/python_armada_client.md +++ b/docs/python_armada_client.md @@ -691,10 +691,10 @@ Subject is a NamedTuple that represents a subject in the permission system. * **Fields** - 1. **kind** (`str`) – Alias for field number 0 + 1.  **kind** (`str`) – Alias for field number 0 - 2. **name** (`str`) – Alias for field number 1 + 2.  **name** (`str`) – Alias for field number 1