Resource Management

4 posts

aws2 min readCurated summary

Announcing managed daemon support for Amazon ECS Managed Instances | Amazon Web Services

Amazon ECS Managed Daemons let platform teams independently deploy and maintain monitoring, logging, and tracing agents across ECS Managed Instances. This decouples operational tooling from application task definitions, reducing coordination and redeployment work. ECS ensures daemons are available before applications start and maintains coverage during rolling updates. ## Decoupled Daemon Management - Platform teams can centrally deploy and update agents without changing application services or rebuilding AMIs. - Daemons can target multiple or specific capacity providers. - Each instance runs exactly one daemon copy shared by its application tasks. - CPU and memory settings are managed separately from application configurations. - Daemons start before application tasks and are drained after them. ## Deployment and Updates - Daemon task definitions are created separately in the ECS console. - A daemon can be associated with a cluster and an ECS Managed Instances capacity provider. - ECS automatically launches the daemon on every applicable instance. - Rolling updates use a “start before stop” process: - New instances launch with the updated daemon. - The daemon starts before application tasks migrate. - Old instances are terminated afterward. - Configurable drain percentages control replacement speed, while automatic rollback improves update safety. ## Technical Capabilities - Managed daemons use a dedicated `daemon_bridge` network mode to communicate with application tasks while remaining isolated from application networking. - They support privileged containers, additional Linux capabilities, and host filesystem mounts. - These features enable host-level monitoring of metrics, processes, and system calls. - ECS validates and manages daemon-specific task definitions independently from standard application tasks. ## Availability and Cost - Managed daemon support is available in all AWS Regions. - There is no additional managed-daemon fee; users pay only for the compute resources consumed by daemon tasks. - The feature can be configured through the ECS console, APIs, and documentation. For organizations running many ECS services, managed daemons provide a simpler and more reliable way to operate shared infrastructure agents without involving application teams in every update.

Read original(opens in new tab)
google3 min readCurated summary

Scheduling in a changing world: Maximizing throughput with time-varying capacity

The post presents scheduling algorithms for non-preemptive jobs when cloud capacity changes over time because of failures, maintenance, power limits, or higher-priority workloads. The goal is to maximize completed job value while respecting release times, deadlines, processing durations, and fluctuating parallel capacity. The research establishes the first constant-factor guarantees for several offline and online variants, including a 1/11 competitive ratio for a demanding common-deadline model. ## Scheduling with Time-Varying Capacity - A capacity profile specifies how many jobs can run simultaneously at each point in time. - Each job has: - A release time - A hard deadline - A processing duration - A weight or profit - Jobs must run continuously once started in the non-preemptive setting. - If capacity drops during execution, an interrupted job loses its progress. - The objective is to select and schedule jobs maximizing total completed weight. - The study considers: - **Offline scheduling**, where future jobs and capacity changes are known. - **Online scheduling**, where jobs arrive dynamically and decisions cannot be reversed. ## Offline Scheduling Results - The optimal problem is NP-hard, so the work focuses on approximation guarantees. - For unit-profit jobs, an earliest-finish-time Greedy algorithm achieves a **1/2-approximation**. - It completes at least half as many jobs as an optimal schedule. - This matches the classic guarantee for single-capacity scheduling. - For jobs with different weights, a primal-dual algorithm achieves a **1/4-approximation**. ## Why Online Non-Preemptive Scheduling Is Difficult - Online schedulers must commit without knowing future jobs. - Starting a long job can block many shorter jobs that arrive later. - Because each completed job may have equal value regardless of duration, one poor decision can sharply reduce throughput. - Consequently, standard non-preemptive online algorithms have competitive ratios approaching zero. ## Interruption with Restarts - An active job may be interrupted, but its completed work is discarded and the job can be retried later. - A modified earliest-finish-time Greedy algorithm achieves a **1/2 competitive ratio**. - This means it can guarantee at least half the throughput of an optimal schedule with complete knowledge of future arrivals. ## Interruption Without Restarts - If an interrupted job is permanently discarded, online scheduling becomes substantially harder. - In general, every online algorithm can be forced into decisions that prevent it from completing much future work. - The competitive ratio again approaches zero. - The authors therefore study a practical special case in which all jobs share a common deadline. ## A Common-Deadline Algorithm For a unit-capacity system, the algorithm maintains a tentative schedule of jobs in disjoint time intervals. When a new job arrives, it applies the first suitable action: 1. Place the job in an empty interval. 2. Replace a scheduled future job if the new job is significantly shorter. 3. Interrupt the current job if the new job is shorter than its remaining processing time. 4. Discard the new job. - The approach balances immediate execution against preserving capacity for shorter future jobs. - A generalized version works with arbitrary capacity profiles. - The resulting algorithm achieves the first constant competitive guarantee for this setting: **1/11**. The results suggest that schedulers for volatile cloud environments need controlled interruption and carefully designed replacement policies. Allowing restarts offers strong guarantees, while stricter interruption rules require additional structure—such as a shared deadline—to achieve predictable performance.

Read original(opens in new tab)
lineOriginal article

Code Quality Improvement Techniques Part (opens in new tab)

When implementing resource management patterns similar to Kotlin's `use` or Java's try-with-resources, developers often face the challenge of handling exceptions that occur during both primary execution and resource cleanup. Simply wrapping these multiple failures in a custom exception container can inadvertently break the calling code's error-handling logic by masking the original exception type. To maintain code quality, developers should prioritize the primary execution exception and utilize the `addSuppressed` mechanism to preserve secondary errors without disrupting the expected flow. ### The Risks of Custom Exception Wrapping Creating a new exception class to consolidate multiple errors during resource management can lead to significant issues for the caller. * Wrapping an expected exception, such as an `IOException`, inside a custom `DisposableException` prevents specific `catch` blocks from identifying and handling the original error. * This pattern often results in unhandled exceptions or the loss of specific error context, especially when the wrapper is hidden inside utility functions. * While this approach aims to be "neat" by capturing all possible failures, it forces the caller to understand the internal wrapping logic of the utility rather than the business logic errors. ### Prioritizing Primary Logic over Cleanup When errors occur in both the main execution block and the cleanup (e.g., `dispose()` or `close()`), it is critical to determine which exception takes precedence. * The exception from the main execution block is typically the "primary" failure that reflects a business logic or IO error, whereas a cleanup failure is often secondary. * Throwing a cleanup exception while discarding the primary error makes debugging difficult, as the root cause of the initial failure is lost. * In a typical `try-finally` block, if the `finally` block throws an exception, it naturally suppresses any exception thrown in the `try` block unless handled manually. ### Implementing Better Suppression Logic A more robust implementation mimics the behavior of Kotlin’s `Closeable.use` by ensuring the most relevant error is thrown while keeping others accessible for debugging. * Instead of creating a wrapper class, use `Throwable.addSuppressed()` to attach the cleanup exception to the primary exception. * If only the primary block fails, throw that exception directly to satisfy the caller's `catch` requirements. * If both the primary block and the cleanup fail, throw the primary exception and add the cleanup exception as a suppressed error. * If only the cleanup fails, it is then appropriate to throw the cleanup exception as the standalone failure. ### Considerations for Checked and Unchecked Exceptions The impact of exception handling varies by language, particularly in Java where checked exceptions are enforced by the compiler. * Converting a checked exception into an unchecked `RuntimeException` inside a wrapper can cause the compiler to miss necessary error-handling requirements. * If exceptions have parent-child relationships, such as `IOException` and `Exception`, wrapping can cause a specific handler to be bypassed in favor of a more generic one. * It is generally recommended to only wrap checked exceptions in `RuntimeException` when the error is truly unrecoverable and the caller is not expected to handle it. When designing custom resource management utilities, always evaluate which exception is most critical for the caller to see. Prioritize the primary execution error and use suppression for auxiliary cleanup failures to ensure that your error-handling remains transparent and predictable for the rest of the application.

datadog3 min readCurated summary

How we minimized the overhead of Kubernetes in our job system

Kubernetes can improve machine management and scalability, but its scheduling and runtime overhead can significantly reduce job throughput if configured poorly. Datadog found its Kubernetes-based job system used more CPU and completed jobs 40–50% more slowly than the previous VM-based system. By designing a controlled experiment, choosing better metrics, and tuning pod resource requests, the team recovered performance to roughly VM parity while investigating the overhead of running one parent process per pod. ## Designing a Comparable Experiment - The initial comparison was difficult because the Kubernetes and VM deployments differed in: - Number of nodes - Number of worker-parent clusters - Workload and enqueue rate - A controlled experiment was created using: - Identical `c5.2xlarge` machines - The same kernel version, `3.13.0-141` - Both systems repeatedly running a simple Python job - Each Kubernetes pod contained one parent process and its worker processes, making pod count equivalent to parent-process count per node. - The older kernel did not include CPU mitigations, avoiding that variable in the comparison. ## Choosing Useful Performance Metrics ### Measuring node effort - Load average initially appeared useful for measuring machine utilization. - Kubernetes background processes—such as cluster polling and pod-state checks—artificially increased load average. - Load average counts runnable processes rather than the amount of CPU time they actually consume. - The team therefore used CPU idle time instead: - It measures unused CPU capacity. - It reflects actual CPU work rather than the number of active processes. ### Measuring system performance - The job system optimized for throughput rather than latency. - Throughput was measured by the number of jobs completed within 30 seconds. - Latency remained useful for detecting queueing problems, but throughput was the primary success metric. ## Tuning Kubernetes Resource Requests - The main performance gains came from improving pod scheduling. - The target was six pods per `c5.2xlarge` node. - Initially, each pod requested: - One full CPU core - More memory than necessary - Since the node had eight cores and approximately 1.5 GiB of memory consumed by Kubernetes and system services, only four pods could be scheduled. - Requests were reduced to: - `100m` CPU, or 100 millicores - `500 MB` memory - CPU tuning generally enabled six pods per node, although some nodes still scheduled only five. - Further memory reduction was needed because system daemons consumed enough memory to prevent six pods from fitting on some nodes. - Resource requests affect scheduling minimums, while limits constrain containers after they start. - These request changes did not slow jobs because the pods still received sufficient resources to operate. ## One Parent Process per Pod - The team considered placing multiple parent processes in each pod to reduce potential pod overhead. - One parent plus its workers was a natural application unit and simplified orchestration. - The decision depended on how much overhead each pod introduced: - High overhead would favor fewer, larger pods. - Low overhead would favor one parent per pod for simpler management. - Using `pstree`, the team identified six job-system instances per node and traced their process trees through components such as: - `containerd-shim` - `tini` - The application process - They estimated that each pod included overhead associated with three containers, particularly `containerd-shim`. - CPU overhead was then investigated using `perf sched`. The practical lesson is to compare equivalent workloads, measure actual CPU consumption rather than relying blindly on load average, and tune Kubernetes requests for the desired packing density. Resource requests should be large enough for reliable operation but not so large that they unnecessarily prevent pods from being scheduled together.

Read original(opens in new tab)