Add k8s-limitrange-oom-silent-kill skill
This commit is contained in:
parent
73a1402533
commit
44883ab6a8
1 changed files with 145 additions and 0 deletions
145
dot_claude/skills/k8s-limitrange-oom-silent-kill/SKILL.md
Normal file
145
dot_claude/skills/k8s-limitrange-oom-silent-kill/SKILL.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
---
|
||||
name: k8s-limitrange-oom-silent-kill
|
||||
description: |
|
||||
Debug Kubernetes pods that suddenly start OOM-killing after a LimitRange is added
|
||||
to the namespace. Use when: (1) pods that previously worked fine start getting
|
||||
OOMKilled (exit code 137), (2) a LimitRange or ResourceQuota was recently added
|
||||
to the namespace, (3) deployments have `resources: {}` and inherit default limits,
|
||||
(4) periodic jobs or background workers fail silently with degraded results before
|
||||
dying. Covers diagnosing the timeline correlation between LimitRange creation and
|
||||
pod failures, and fixing by setting explicit resource requests/limits.
|
||||
author: Claude Code
|
||||
version: 1.0.0
|
||||
date: 2026-02-21
|
||||
---
|
||||
|
||||
# Kubernetes LimitRange Causing Silent OOM Kills
|
||||
|
||||
## Problem
|
||||
Pods that previously ran without issues suddenly start getting OOMKilled after a
|
||||
`LimitRange` is added to the namespace. Deployments with `resources: {}` silently
|
||||
inherit the LimitRange's default memory limit, which may be too low for the actual
|
||||
workload. The failures can be subtle: background workers may partially complete
|
||||
tasks before dying, making it look like the application is buggy rather than
|
||||
resource-starved.
|
||||
|
||||
## Context / Trigger Conditions
|
||||
- Pods suddenly restarting with `Reason: OOMKilled`, `Exit Code: 137`
|
||||
- A `LimitRange` was recently created in the namespace
|
||||
- Deployment manifests have `resources: {}` (no explicit limits)
|
||||
- Background workers (Celery, Sidekiq, etc.) process fewer items than expected
|
||||
- Periodic/cron jobs that used to succeed now fail or produce partial results
|
||||
- `kubectl describe pod` shows `Last State: Terminated, Reason: OOMKilled`
|
||||
|
||||
## Diagnosis
|
||||
|
||||
### Step 1: Check for OOM kills
|
||||
```bash
|
||||
kubectl -n <ns> describe pod <pod> | grep -A 5 "Last State"
|
||||
# Look for: Reason: OOMKilled, Exit Code: 137
|
||||
```
|
||||
|
||||
### Step 2: Check current memory usage vs limits
|
||||
```bash
|
||||
kubectl -n <ns> top pod <pod>
|
||||
kubectl -n <ns> describe pod <pod> | grep -A 5 "Limits:"
|
||||
```
|
||||
|
||||
### Step 3: Check for LimitRange in the namespace
|
||||
```bash
|
||||
kubectl -n <ns> describe limitrange
|
||||
# Look for Default Limit column — this is what pods without explicit
|
||||
# resources inherit
|
||||
```
|
||||
|
||||
### Step 4: Correlate LimitRange creation with failure timeline
|
||||
```bash
|
||||
kubectl -n <ns> get limitrange -o yaml | grep creationTimestamp
|
||||
# Compare this date with when pods started failing
|
||||
# Check application data (DB timestamps, logs) to find exact failure start
|
||||
```
|
||||
|
||||
### Step 5: Verify deployment has no explicit resources
|
||||
```bash
|
||||
kubectl -n <ns> get deployment <name> -o jsonpath='{.spec.template.spec.containers[0].resources}'
|
||||
# If output is {} — the pod inherits LimitRange defaults
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
Set explicit resource requests and limits on the deployment that match the
|
||||
workload's actual needs:
|
||||
|
||||
```bash
|
||||
kubectl -n <ns> patch deployment <name> --type='json' -p='[
|
||||
{"op": "replace", "path": "/spec/template/spec/containers/0/resources",
|
||||
"value": {
|
||||
"requests": {"memory": "512Mi", "cpu": "100m"},
|
||||
"limits": {"memory": "2Gi", "cpu": "2"}
|
||||
}}
|
||||
]'
|
||||
```
|
||||
|
||||
For multi-process workers (Celery prefork, Gunicorn), also reduce concurrency
|
||||
to lower idle memory:
|
||||
|
||||
```bash
|
||||
# Example: Celery worker with 16 prefork processes using ~60Mi each = ~960Mi idle
|
||||
# Reduce to 4 processes: ~240Mi idle, leaving headroom for actual work
|
||||
kubectl -n <ns> patch deployment <name> --type='json' -p='[
|
||||
{"op": "replace", "path": "/spec/template/spec/containers/0/command",
|
||||
"value": ["celery", "-A", "app", "worker", "--concurrency=4"]}
|
||||
]'
|
||||
```
|
||||
|
||||
### Choosing memory limits
|
||||
|
||||
1. Check idle memory: `kubectl top pod` with no active tasks
|
||||
2. Check peak memory: `kubectl top pod` during heaviest workload
|
||||
3. Set limit to ~2x peak to allow for spikes
|
||||
4. Set request to ~idle usage so scheduler places pods correctly
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
# Confirm new limits are applied
|
||||
kubectl -n <ns> describe pod <new-pod> | grep -A 5 "Limits:"
|
||||
|
||||
# Monitor memory during workload
|
||||
kubectl -n <ns> top pod <pod>
|
||||
|
||||
# Confirm no OOM kills after running a full workload cycle
|
||||
kubectl -n <ns> describe pod <pod> | grep "Restart Count"
|
||||
# Should show 0
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
**Scenario**: Celery workers processing periodic scrape jobs start failing after
|
||||
a `tier-defaults` LimitRange is added with 1Gi default memory limit.
|
||||
|
||||
- 16 prefork workers consume ~983Mi at idle (nearly 1Gi)
|
||||
- Any task execution pushes over 1Gi, triggering OOM kill
|
||||
- Scrape jobs process 1-8 items instead of thousands before dying
|
||||
- Eventually pods cycle between start and OOM-kill, effectively going offline
|
||||
|
||||
**Fix**: Set explicit 2Gi limit and reduce concurrency from 16 to 4:
|
||||
- Idle memory drops to ~296Mi
|
||||
- Peak during scrape: ~919Mi
|
||||
- Well within 2Gi limit, zero OOM kills
|
||||
|
||||
## Notes
|
||||
- LimitRange defaults apply at pod admission time. Existing running pods are NOT
|
||||
affected until they are recreated (e.g., by a deployment rollout)
|
||||
- The failure mode is insidious: pods may partially work, processing some items
|
||||
before getting killed, making it look like an application bug
|
||||
- Always set explicit `resources` on production deployments to avoid inheriting
|
||||
namespace defaults
|
||||
- `resources: {}` is NOT the same as "no limits" when a LimitRange exists
|
||||
- CI/CD pipelines that only patch the image tag (not resources) will preserve
|
||||
manually-set resource limits across deploys
|
||||
|
||||
See also: kubernetes-latest-tag-image-pull
|
||||
|
||||
## References
|
||||
- [Kubernetes LimitRange](https://kubernetes.io/docs/concepts/policy/limit-range/)
|
||||
- [Kubernetes Resource Management](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
|
||||
Loading…
Add table
Add a link
Reference in a new issue