prompts.chatprompts.chatprompts.chat
BotPromptKỹ năngQuy trìnhDanh mụcThẻCao thủ PromptPhong cách
SáchCho trẻ emNhà phát triển
Đăng nhập
CC0 2026 prompts.chat
DeepWikiHướng dẫn...Tài liệuAPIQuyền riêng tưĐiều khoảnHỗ trợGiới thiệuGitHub
Tất cả thẻ

Prompt Cost Optimization

1 prompt
Công cụ Tối ưu hóa Định mức Tài nguyên Kubernetes (Rightsizer)
Kỹ năng

Tối ưu hóa đúng định mức (rightsize) các thông số requests và limits cho CPU và bộ nhớ trong Kubernetes dựa trên mức sử dụng thực tế (p50, p95 và max trên từng container). Đề xuất mức requests tại ngưỡng p95 cộng thêm khoảng dự phòng (headroom), giới hạn bộ nhớ (memory limits) cao hơn mức đỉnh và loại bỏ giới hạn CPU (CPU limits), đồng thời ước tính số lõi (cores), dung lượng GiB và chi phí tiết kiệm được. Cảnh báo các lỗi OOM kill, mức dùng bộ nhớ vượt quá request, hiện tượng bóp băng thông CPU (CPU throttling), thiếu thông số requests hoặc memory limits, cấp phát dư thừa (overprovisioning) và khoảng thời gian thu thập dữ liệu quá ngắn. Bao gồm một công cụ tính toán bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.

---
name: kubernetes-resource-requests-rightsizer
description: Tối ưu hóa đúng định mức (rightsize) các thông số requests và limits cho CPU và bộ nhớ trong Kubernetes dựa trên mức sử dụng thực tế (p50, p95 và max trên từng container). Đề xuất mức requests tại ngưỡng p95 cộng thêm khoảng dự phòng (headroom), giới hạn bộ nhớ (memory limits) cao hơn mức đỉnh và loại bỏ giới hạn CPU (CPU limits), đồng thời ước tính số lõi (cores), dung lượng GiB và chi phí tiết kiệm được. Cảnh báo các lỗi OOM kill, mức dùng bộ nhớ vượt quá request, hiện tượng bóp băng thông CPU (CPU throttling), thiếu thông số requests hoặc memory limits, cấp phát dư thừa (overprovisioning) và khoảng thời gian thu thập dữ liệu quá ngắn. Bao gồm một công cụ tính toán bằng thư viện chuẩn Python có hỗ trợ cờ --self-test.
---

# Công cụ Tối ưu hóa Định mức Tài nguyên Kubernetes (Rightsizer)

Bạn giúp các kỹ sư nền tảng (platform engineers), kỹ sư SRE và nhà phát triển thiết lập thông số requests và limits cho các container Kubernetes dựa trên bằng chứng số liệu thực tế thay vì phỏng đoán. Từ các phân vị mức sử dụng (usage percentiles) được xuất ra từ Prometheus, Datadog hoặc trình đề xuất VPA, bạn đề xuất thông số requests và limits mới cho từng container, giải thích các rủi ro nguy hiểm trước tiên (lỗi OOM kill, nghẽn CPU do throttling, nguy cơ bị trục xuất pod - eviction), ước tính lượng tài nguyên mà cụm (cluster) tiết kiệm được, và lập kế hoạch triển khai an toàn.

## Các tệp trong kỹ năng này

- `scripts/rightsize.py` - đọc tệp CSV chứa các thiết lập hiện tại và phân vị mức sử dụng của từng container, sau đó in ra thông số requests và limits được khuyến nghị, các phát hiện rủi ro và lượng tài nguyên requests được giải phóng (chỉ dùng thư viện chuẩn của Python 3)
- `references/sizing-method.md` - vai trò của requests và limits, quy tắc tính toán định mức, các chỉ số cần thu thập (kèm câu truy vấn PromQL), và cách đọc số liệu của các container đang bị bóp CPU (throttled) hoặc bị OOM kill
- `references/rollout-and-guardrails.md` - thứ tự áp dụng thay đổi, quy trình triển khai (rollout) và hoàn tác (rollback) an toàn, LimitRange, ResourceQuota, VPA, cảnh báo (alerts), và những trường hợp không nên cắt giảm tài nguyên
- `templates/rightsizing-plan.md` - mẫu kế hoạch tối ưu hóa định mức tài nguyên để chia sẻ với nhóm hoặc đính kèm vào phiếu yêu cầu thay đổi (change ticket)
- `examples/shop-cluster-usage.csv` - sáu container của một hệ thống cửa hàng trực tuyến: một API được cấp phát quá dư thừa, container sidecar đi kèm của nó, một dịch vụ tìm kiếm vừa bị OOM kill vừa bị bóp CPU, một worker không đặt giới hạn bộ nhớ, Redis và một trình đẩy log (log shipper) không khai báo requests

## Quy trình làm việc

### 1. Thu thập số liệu sử dụng
Hãy yêu cầu dữ liệu thống kê theo từng container trong ít nhất 7 ngày (tốt nhất là 14-28 ngày): số lượng bản sao (replicas), thông số requests và limits hiện tại, phân vị p50/p95/max của CPU tính bằng millicores, phân vị p50/p95/max của bộ nhớ làm việc (memory working set) tính bằng MiB, tỷ lệ phần trăm bị bóp CPU (throttled percentage) và số lần bị OOM kill. Tệp `references/sizing-method.md` có sẵn câu truy vấn PromQL cho từng chỉ số này. Hãy hỏi thêm về nhiệm vụ của từng workload, đặc điểm biến thiên lưu lượng truy cập (đỉnh điểm hàng tuần, đợt khuyến mãi, tác vụ chạy theo lô batch jobs) và liệu có đang dùng HPA để tự động co giãn cho nó hay không.

### 2. Chạy công cụ tối ưu hóa định mức
```
python3 scripts/rightsize.py examples/shop-cluster-usage.csv --core-month 24 --gib-month 3.2
python3 scripts/rightsize.py usage.csv --headroom 25 --keep-cpu-limits --json
python3 scripts/rightsize.py --self-test
```
Thông số đơn giá là tùy chọn và được tính trên mỗi lõi (core) hoặc mỗi GiB được yêu cầu (requested) mỗi tháng. Mã thoát (exit code) bằng 1 nghĩa là có phát hiện rủi ro mức CRITICAL hoặc HIGH, bằng 2 nghĩa là dữ liệu đầu vào không hợp lệ.

### 3. Giải thích và lập kế hoạch
Bắt đầu với các container có mức cảnh báo CRITICAL và HIGH: nêu rõ vấn đề gì đang xảy ra hiện nay, bằng chứng số liệu cụ thể và thay đổi cần áp dụng. Đối với các container đang bị bóp CPU hoặc bị OOM kill, hãy lưu ý rằng các con số sử dụng hiện tại đã bị chạm trần giới hạn và cần lên kế hoạch đo đạc lại lần thứ hai sau khi khắc phục. Tiếp theo, liệt kê các khoản tiết kiệm lớn nhất (số lượng replicas sẽ nhân bội mức tiết kiệm này lên). Kiểm tra các trường hợp ngoại lệ trong `references/rollout-and-guardrails.md` mà việc cắt giảm tài nguyên là không an toàn, đồng thời nhắc nhở việc tinh chỉnh lại ngưỡng HPA khi thay đổi thông số requests.

### 4. Bàn giao kết quả
Điền vào mẫu `templates/rightsizing-plan.md` với bảng tổng hợp, bản vá YAML (YAML patch) cho từng workload, thứ tự triển khai, các chỉ số cần theo dõi và ngày hẹn đo đạc lại.

## Ví dụ kết quả
Đối với tệp `examples/shop-cluster-usage.csv`: container shop/catalog-search/app ở mức CRITICAL (bị OOM kill 3 lần tại ngưỡng giới hạn 1536Mi, mức bộ nhớ p95 là 1180Mi vượt quá mức request 1024Mi, bị bóp CPU 31% tại ngưỡng giới hạn 500m) và được đề xuất mức requests mới là 570m / 1360Mi, giới hạn bộ nhớ 1872Mi và gỡ bỏ hoàn toàn giới hạn CPU; ops/log-shipper hoàn toàn không có thông số requests nào (mức HIGH, chạy 8 replicas); shop/image-resizer cần bổ sung giới hạn bộ nhớ và tăng thêm bộ nhớ; shop/checkout-api/app giảm từ 1000m / 2048Mi xuống còn 485m / 912Mi trên 6 replicas. Tổng cộng giải phóng được 1.76 cores và 4.06 GiB tài nguyên requests, tương đương tiết kiệm khoảng 55 mỗi tháng nếu tính theo đơn giá 24 trên mỗi core và 3.2 trên mỗi GiB.

## Quy tắc

- Luôn khắc phục các rủi ro về độ tin cậy trước khi tối ưu tiết kiệm chi phí, và tuyệt đối không bao giờ khuyến nghị gỡ bỏ giới hạn bộ nhớ (memory limit).
- Luôn nêu rõ mỗi con số đề xuất được tính từ phân vị nào, khoảng thời gian dữ liệu bao lâu và tỷ lệ dự phòng (headroom) là bao nhiêu, đồng thời lưu ý rằng các đợt tăng vọt hiếm gặp nằm ngoài khoảng thời gian thu thập dữ liệu sẽ chưa được tính tới.
- Coi mức sử dụng của các container đang bị bóp CPU (throttled) hoặc bị OOM kill chỉ là mức sàn tối thiểu (floor), chứ chưa phải nhu cầu thực tế của chúng.
- Cung cấp các thay đổi dưới dạng bản vá tệp manifest hoặc Helm values để đội ngũ kỹ thuật xem xét; không tự ý chạy các lệnh `kubectl` trực tiếp lên cụm đang hoạt động.
- Báo cáo mức tiết kiệm dưới dạng "tài nguyên requests được giải phóng" cho tới khi các máy chủ nút (nodes) thực sự được thu hồi giảm bớt.
FILE:references/sizing-method.md
# Phương pháp Tính toán Định mức (Sizing Method)

## Vai trò của requests và limits
- **CPU request:** lượng tài nguyên mà bộ lập lịch (scheduler) đặt chỗ trước trên một node và là tỷ trọng chia sẻ CPU của container đó khi node rơi vào trạng thái bận. Đặt quá thấp: pod bị xếp vào các node chật chội và chạy chậm đi khi tải cao. Đặt quá cao: các node trông có vẻ đã đầy trên sổ sách trong khi thực tế lại đang nhàn rỗi, khiến bạn phải trả tiền cho những lõi CPU bỏ không.
- **CPU limit:** mức trần cứng được thực thi bởi hạn ngạch CFS (CFS quota) theo từng chu kỳ 100 ms. Một container chạm tới ngưỡng này sẽ bị bóp băng thông (throttled), ngay cả khi node đó đang dư thừa CPU nhàn rỗi. Các dịch vụ nhạy cảm với độ trễ (API, proxy, ứng dụng JVM và chương trình Go sử dụng nhiều luồng) sẽ chịu ảnh hưởng nặng nề nhất.
- **Memory request:** lượng bộ nhớ mà bộ lập lịch đặt chỗ trước. Các pod sử dụng nhiều bộ nhớ hơn mức request của chúng sẽ là đối tượng đầu tiên bị trục xuất (evicted) khi một node cạn kiệt bộ nhớ.
- **Memory limit:** mức trần cứng; vượt quá ngưỡng này đồng nghĩa với việc bị OOM kill ngay lập tức (mã thoát 137, lý do `OOMKilled`). Bộ nhớ không thể bị làm chậm (throttled) như CPU.
- **Các hạng QoS (QoS classes):** Guaranteed (requests = limits cho mọi container trong pod), Burstable (có thiết lập một số thông số requests), BestEffort (không thiết lập gì cả, sẽ bị trục xuất đầu tiên khi thiếu tài nguyên).

## Các quy tắc được sử dụng bởi `scripts/rightsize.py`
| Thông số | Quy tắc tính | Lý do |
|---|---|---|
| CPU request | p95 x (1 + headroom), làm tròn lên theo bậc 5m, tối thiểu 10m | đáp ứng tốt các giai đoạn bận rộn thông thường; phần nhu cầu đột biến còn lại sẽ tận dụng nguồn CPU nhàn rỗi dùng chung |
| CPU limit | gỡ bỏ (hoặc nếu dùng cờ `--keep-cpu-limits`: max(max x 1.2, 2 x request)) | giới hạn CPU gây nghẽn các đợt xử lý đột biến ngay cả trên các node đang rảnh |
| Memory request | p95 x (1 + headroom), làm tròn lên theo bậc 16Mi, tối thiểu 32Mi | giữ cho pod nằm ngoài danh sách ưu tiên bị trục xuất (eviction) |
| Memory limit | max(max x 1.25, request), làm tròn lên theo bậc 16Mi | tạo khoảng trống cho các đỉnh tải; không bao giờ được gỡ bỏ vì một lỗi rò rỉ bộ nhớ (memory leak) không được phép đánh sập cả node |

Tỷ lệ dự phòng (headroom) mặc định là 15%. Hãy nâng lên mức 25-30% đối với các lưu lượng có tính chất tăng vọt đột ngột (spiky traffic), các tác vụ chạy theo lô (batch jobs) hoặc khi khoảng thời gian thu thập dữ liệu quá ngắn.

## Các chỉ số cần thu thập
Trên từng container, trong khoảng thời gian ít nhất 7 ngày (tốt nhất là 14-28 ngày để bắt được các đỉnh tải hàng tuần và cuối tháng):
- CPU tính bằng millicores: giá trị p50, p95 và max của `rate(container_cpu_usage_seconds_total[5m]) * 1000`
- Bộ nhớ tính bằng MiB: giá trị p50, p95 và max của `container_memory_working_set_bytes / 2^20` (working set chính là chỉ số mà trình OOM killer và cơ chế trục xuất eviction theo dõi, chứ không phải RSS hay cache)
- Tỷ lệ bóp CPU (Throttling): `container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total * 100`
- Số lần OOM kill: `kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}` hoặc số lần khởi động lại (restarts) với mã thoát 137
- Thiết lập hiện tại: dùng lệnh `kubectl get deploy -o jsonpath` hoặc chỉ số `kube_pod_container_resource_requests` / `_limits`

## Cách đọc kết quả
- **Các container bị bóp CPU (Throttled containers)** sẽ báo cáo mức sử dụng bị chặn đứng tại ngưỡng limit, do đó chỉ số p95 thu được thấp hơn nhu cầu thực tế. Hãy nâng cao hoặc gỡ bỏ giới hạn CPU trước, đo đạc lại trong vài ngày, rồi mới tính toán định mức chuẩn.
- **Các lỗi OOM kill** có nghĩa là mức đỉnh thực sự vẫn chưa xác định được (vì container đã bị giết ngay khi chạm ngưỡng limit). Hãy tăng giới hạn bộ nhớ lên thêm 50% hoặc nhiều hơn, tìm nguyên nhân rò rỉ bộ nhớ hoặc truy vấn nặng gây tốn RAM, sau đó đo đạc lại.
- **Ứng dụng JVM, Node.js và Go** đọc kích thước vùng nhớ heap từ cấu hình khởi chạy: hãy thay đổi các cờ `-Xmx`, `--max-old-space-size` hoặc `GOMEMLIMIT` đồng bộ với giới hạn bộ nhớ của container (giữ vùng nhớ heap ở mức khoảng 70-75% so với memory limit).
- **Các container phụ trợ (Sidecars)** (như istio-proxy, trình đẩy log) cần được tính toán định mức riêng biệt; thông số của chúng sẽ được nhân lên theo từng bản sao (replica) của ứng dụng.
- **HPA dựa trên CPU** thực hiện co giãn dựa trên tỷ lệ giữa mức sử dụng thực tế chia cho thông số request. Việc hạ thấp thông số request sẽ khiến HPA kích hoạt nhân bản (scale out) sớm hơn, vì vậy bạn có thể cần hạ mục tiêu `averageUtilization` hoặc số lượng bản sao sẽ thay đổi. Hãy tối ưu hóa định mức tài nguyên trước, sau đó tinh chỉnh lại HPA.

## Giới hạn của phương pháp này
- Các chỉ số phân vị trong một khung thời gian sẽ bỏ sót các sự kiện hiếm gặp: tác vụ tổng hợp dữ liệu hàng tháng, đợt siêu khuyến mãi, hoặc quá trình dựng lại bộ nhớ đệm (cache rebuild) ngay sau khi triển khai bản mới.
- Mức sử dụng trên mỗi container thường được tính trung bình qua các bản sao (replicas); một phân mảnh nóng (hot shard) hoặc một pod đóng vai trò nút trưởng (leader pod) cần được tách thành một dòng dữ liệu riêng.
- Khả năng xếp khối trên node (node bin-packing), phần tài nguyên tiêu hao cho các DaemonSet và dung lượng bộ nhớ thực tế có thể cấp phát (allocatable memory) mới là yếu tố quyết định mức tiết kiệm thực sự; lượng tài nguyên requests được giải phóng chỉ thực sự chuyển hóa thành tiền tiết kiệm được khi số lượng node giảm xuống.
FILE:references/rollout-and-guardrails.md
# Triển khai và Các Rào chắn An toàn (Rollout and Guardrails)

## Thứ tự thực hiện thay đổi
1. **Khắc phục rủi ro trước tiên:** xử lý các trường hợp bị OOM kill, mức bộ nhớ p95 vượt quá request, bị bóp CPU nặng, hoặc thiếu khai báo requests. Những lỗi này trực tiếp gây ra sự cố hệ thống; việc tiết kiệm chi phí có thể đợi sau.
2. **Sau đó mới cắt giảm lãng phí:** xử lý các container bị cấp phát dư thừa (`OVERPROVISIONED`), ưu tiên những dịch vụ có số lượng bản sao (replica) lớn nhất trước vì mức tiết kiệm sẽ được nhân lên theo số bản sao.
3. **Thực hiện từng workload một,** áp dụng trên môi trường thử nghiệm (staging) trước nếu nơi đó có lưu lượng truy cập sát thực tế, sau đó mới đưa lên môi trường chính thức (production) vào khung giờ thấp điểm.

## Các bước triển khai an toàn
1. Thay đổi thông số requests và limits thông qua Git (trong tệp Helm values, bản vá Kustomize hoặc tệp manifest), tuyệt đối không dùng lệnh `kubectl edit` trực tiếp trên môi trường production.
2. Duy trì tốc độ triển khai cuốn chiếu từ từ: thiết lập `maxUnavailable: 0`, `maxSurge: 25%` kèm theo một `PodDisruptionBudget`, để các pod mới với kích thước tài nguyên mới khởi chạy thành công trước khi các pod cũ bị hủy.
3. Theo dõi sát sao trong 24-48 giờ: độ trễ p95, tỷ lệ lỗi, tỷ lệ bóp CPU (throttling), số lần OOM kill, số lần khởi động lại, số lần bị trục xuất (evictions) và số lượng bản sao do HPA điều chỉnh.
4. Hoàn tác (Rollback) = đảo ngược lại bản commit (revert commit). Việc hạ thấp thông số request không bao giờ tự giết chết các pod đang chạy, nhưng các pod sẽ được tạo lại mới trong quá trình triển khai (rollout).

## Ví dụ bản vá cấu hình (patch)
```yaml
resources:
  requests:
    cpu: 485m
    memory: 912Mi
  limits:
    memory: 1136Mi   # no cpu limit on purpose: avoid CFS throttling
```

## Các rào chắn bảo vệ cụm (Cluster guardrails)
- **LimitRange** trên từng namespace: áp đặt mức requests mặc định và giới hạn bộ nhớ mặc định, nhằm ngăn chặn sự xuất hiện của các pod thuộc hạng BestEffort.
- **ResourceQuota** trên từng namespace: khống chế tổng lượng tài nguyên requests tối đa để một nhóm đơn lẻ không thể chiếm hết toàn bộ tài nguyên của cụm.
- **Vertical Pod Autoscaler (VPA)** ở chế độ `updateMode: "Off"` giúp đưa ra các con số khuyến nghị để đối chiếu với kết quả của kỹ năng này mà không tự động khởi động lại các pod. Không được để VPA (chế độ Auto) và HPA theo dõi CPU hoặc bộ nhớ cùng lúc điều khiển chung một workload.
- **Cảnh báo (Alerts):** có sự kiện OOMKilled trong vòng một giờ qua, tỷ lệ bóp CPU vượt quá 25% kéo dài trong 15 phút, bộ nhớ làm việc (working set) vượt quá 90% ngưỡng limit, hoặc có các pod bị treo ở trạng thái Pending do thiếu tài nguyên.

## Khi nào không nên cắt giảm
- Các workload có những đợt tăng tải đột biến hiếm gặp đã biết trước nhưng chưa xuất hiện trong khoảng thời gian lấy mẫu dữ liệu (chốt sổ cuối tháng, Black Friday, chạy báo cáo quý).
- Các pod có giai đoạn khởi động tiêu tốn nhiều CPU hơn hẳn so với trạng thái chạy ổn định (ví dụ quá trình khởi động làm nóng của JVM): hãy giữ mức request cao hơn hoặc sử dụng cơ chế kiểm tra khởi động (startup probe) với độ trễ rộng rãi.
- Các hệ quản trị cơ sở dữ liệu và bộ nhớ đệm đã được cấu hình cứng kích thước bộ nhớ bên trong (như `maxmemory` của Redis, `shared_buffers` của PostgreSQL): giới hạn bộ nhớ (memory limit) của container bắt buộc phải luôn cao hơn kích thước đã cấu hình cộng với phần bộ nhớ phụ trợ (overhead).
- Các trường hợp chủ đích cần hạng chất lượng dịch vụ Guaranteed QoS (ứng dụng đòi hỏi độ trễ cực thấp, ghim cố định lõi CPU với chính sách static CPU manager): hãy giữ nguyên thông số requests bằng đúng với limits.

## Chuyển hóa lượng requests giải phóng thành tiền tiết kiệm thực tế
Lượng tài nguyên requests được giải phóng chỉ thực sự làm giảm hóa đơn chi phí khi bộ tự động co giãn cụm (cluster autoscaler hoặc Karpenter) có thể gom pod và gỡ bỏ bớt các node dư thừa. Hãy kiểm tra hiệu suất sử dụng node theo tỷ lệ requests sau khi áp dụng thay đổi và chờ khoảng một ngày để quá trình dồn nén (consolidation) diễn ra. Hãy luôn báo cáo mức tiết kiệm là "lượng requests được giải phóng" cho đến khi số lượng node thực sự giảm xuống.
FILE:templates/rightsizing-plan.md
# Kế hoạch Tối ưu hóa Định mức Tài nguyên (Rightsizing Plan) - <cluster / namespace> - <ngày>

**Nguồn dữ liệu:** <Prometheus / Datadog / khác>, từ <ngày đầu> đến <ngày cuối> (<n> ngày)  **Dự phòng (Headroom):** <15>%  **Người phụ trách:** <tên>

## Các rủi ro cần khắc phục trước tiên
| Container | Phát hiện | Bằng chứng | Thay đổi | Hạn hoàn thành |
|---|---|---|---|---|
| | | | | |

## Thông số Requests và Limits
| Container | Replicas | CPU req (hiện tại -> mới) | CPU limit | Mem req (hiện tại -> mới) | Mem limit (hiện tại -> mới) | Trạng thái |
|---|---|---|---|---|---|---|
| | | | | | | |

## Mức tiết kiệm ước tính
- Tài nguyên requests giải phóng được: <cores> cores, <GiB> GiB
- Mức tiết kiệm hàng tháng ước tính: <số tiền> (tính theo đơn giá <giá mỗi core> / <giá mỗi GiB>); chỉ thành tiền thực tế sau khi cụm thu giảm số lượng node

## Lộ trình triển khai (Rollout)
| Bước | Workload | Môi trường | Ngày | Chỉ số theo dõi | Phương án hoàn tác (Rollback) |
|---|---|---|---|---|---|
| 1 | | staging | | độ trễ, OOM, throttling | revert commit |

## Các bước theo dõi tiếp theo
- Đo đạc lại vào ngày <ngày> với số liệu phân vị mới
- Rào chắn an toàn (Guardrails): thiết lập mặc định LimitRange <...>, cảnh báo <...>
FILE:examples/shop-cluster-usage.csv
namespace,workload,container,replicas,cpu_request_m,cpu_limit_m,mem_request_mi,mem_limit_mi,cpu_p50_m,cpu_p95_m,cpu_max_m,mem_p50_mi,mem_p95_mi,mem_max_mi,throttled_pct,oom_kills,days
shop,checkout-api,app,6,1000,2000,2048,2048,180,420,1350,610,780,905,0,0,14
shop,checkout-api,istio-proxy,6,100,2000,128,1024,35,90,240,70,95,120,0,0,14
shop,catalog-search,app,4,500,500,1024,1536,310,495,500,900,1180,1490,31,3,14
shop,image-resizer,worker,3,250,1000,512,,120,380,950,340,610,1720,4,0,14
shop,cart-redis,redis,1,200,,768,1024,40,75,160,690,712,730,0,0,14
ops,log-shipper,fluent-bit,8,,,,,25,60,180,45,70,95,0,0,5
FILE:scripts/rightsize.py
#!/usr/bin/env python3
"""Kubernetes resource requests rightsizer (Python 3 standard library only).

Input: CSV with a header row, one row per container, columns
  namespace,workload,container,replicas,
  cpu_request_m,cpu_limit_m,mem_request_mi,mem_limit_mi,
  cpu_p50_m,cpu_p95_m,cpu_max_m,mem_p50_mi,mem_p95_mi,mem_max_mi,
  throttled_pct,oom_kills,days
CPU in millicores (m), memory in MiB (Mi). Empty request/limit cells mean "not set".
throttled_pct = share of CFS periods throttled (0-100); oom_kills and days may be empty (0 / unknown).

Recommendation per container (headroom default 15%, --headroom):
  cpu request  = p95 x (1 + headroom), rounded up to 5m, at least 10m
  cpu limit    = none (recommended), unless --keep-cpu-limits: max(cpu max x 1.2, 2 x request), 50m steps
  mem request  = p95 x (1 + headroom), rounded up to 16Mi, at least 32Mi
  mem limit    = max(mem max x 1.25, request), rounded up to 16Mi (never removed)
Findings (severity):
  CRITICAL OOM_RISK           OOM kills seen, or memory max >= 95% of the memory limit
  HIGH     NO_REQUESTS        cpu or memory request missing (BestEffort / poor scheduling)
  HIGH     MEM_UNDER_REQUEST  memory p95 above the memory request (eviction risk under node pressure)
  HIGH     CPU_THROTTLED      throttled_pct >= 25
  MEDIUM   CPU_THROTTLED      throttled_pct 10-25
  MEDIUM   CPU_UNDER_REQUEST  cpu p95 above 1.2 x cpu request (noisy neighbours, slow under load)
  MEDIUM   NO_MEMORY_LIMIT    memory limit missing (one leak can take the node down)
  LOW      OVERPROVISIONED    recommended cpu or memory request < 50% of current
  LOW      SHORT_WINDOW       under 7 days of data (misses weekly peaks; trust less)
Savings: (current - recommended requests) x replicas, totals in cores and GiB, and a monthly
estimate when --core-month and --gib-month prices are given.
Usage:
  python3 rightsize.py usage.csv
  python3 rightsize.py usage.csv --headroom 25 --core-month 24 --gib-month 3.2 --json
  python3 rightsize.py --self-test
Exit code: 0 no CRITICAL/HIGH, 1 CRITICAL or HIGH findings, 2 bad input.
"""
import argparse
import csv
import io
import json
import math
import sys

COLUMNS = ("namespace", "workload", "container", "replicas", "cpu_request_m", "cpu_limit_m",
           "mem_request_mi", "mem_limit_mi", "cpu_p50_m", "cpu_p95_m", "cpu_max_m",
           "mem_p50_mi", "mem_p95_mi", "mem_max_mi", "throttled_pct", "oom_kills", "days")
OPTIONAL = {"cpu_request_m", "cpu_limit_m", "mem_request_mi", "mem_limit_mi", "throttled_pct", "oom_kills", "days"}
SEV_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}


class DataError(ValueError):
    pass


def up(x, step):
    return int(math.ceil(x / step - 1e-9) * step)


def num(row, col, line):
    v = row.get(col, "")
    if v == "":
        if col in OPTIONAL:
            return None
        raise DataError(f"line {line}: {col} is required")
    try:
        x = float(v)
    except ValueError:
        raise DataError(f"line {line}: {col} must be a number, got {v!r}")
    if x < 0:
        raise DataError(f"line {line}: {col} must be >= 0")
    return x


def read_rows(text):
    rd = csv.DictReader(io.StringIO(text))
    if not rd.fieldnames:
        raise DataError("empty CSV")
    missing = [c for c in COLUMNS if c not in {f.strip() for f in rd.fieldnames}]
    if missing:
        raise DataError("CSV header is missing: " + ", ".join(missing))
    rows = []
    for n, raw in enumerate(rd, start=2):
        row = {k.strip(): (v or "").strip() for k, v in raw.items() if k}
        if not any(row.values()):
            continue
        r = {c: row[c] for c in ("namespace", "workload", "container")}
        for c in COLUMNS[3:]:
            r[c] = num(row, c, n)
        if r["replicas"] < 1:
            raise DataError(f"line {n}: replicas must be >= 1")
        if r["cpu_p95_m"] > r["cpu_max_m"] or r["mem_p95_mi"] > r["mem_max_mi"]:
            raise DataError(f"line {n}: p95 cannot be above max")
        rows.append(r)
    if not rows:
        raise DataError("no data rows")
    return rows


def analyze(r, headroom=0.15, keep_cpu_limits=False):
    h = 1 + headroom
    rec = {
        "cpu_request_m": max(10, up(r["cpu_p95_m"] * h, 5)),
        "mem_request_mi": max(32, up(r["mem_p95_mi"] * h, 16)),
    }
    rec["cpu_limit_m"] = max(up(r["cpu_max_m"] * 1.2, 50), up(2 * rec["cpu_request_m"], 50)) if keep_cpu_limits else None
    rec["mem_limit_mi"] = max(rec["mem_request_mi"], up(r["mem_max_mi"] * 1.25, 16))
    findings = []

    def f(sev, code, msg):
        findings.append({"severity": sev, "code": code, "message": msg})

    cr, cl, mr, ml = r["cpu_request_m"], r["cpu_limit_m"], r["mem_request_mi"], r["mem_limit_mi"]
    oom = r["oom_kills"] or 0
    if oom > 0:
        f("CRITICAL", "OOM_RISK", f"{oom:g} OOM kill(s); memory max {r['mem_max_mi']:g}Mi vs limit {fmt(ml, 'Mi')}")
    elif ml and r["mem_max_mi"] >= 0.95 * ml:
        f("CRITICAL", "OOM_RISK", f"memory max {r['mem_max_mi']:g}Mi is {r['mem_max_mi'] / ml:.0%} of the {ml:g}Mi limit")
    if cr is None or mr is None:
        f("HIGH", "NO_REQUESTS", "missing " + " and ".join(x for x, v in (("cpu request", cr), ("memory request", mr)) if v is None))
    if mr and r["mem_p95_mi"] > mr:
        f("HIGH", "MEM_UNDER_REQUEST", f"memory p95 {r['mem_p95_mi']:g}Mi > request {mr:g}Mi")
    th = r["throttled_pct"] or 0
    if th >= 25:
        f("HIGH", "CPU_THROTTLED", f"{th:g}% of CPU periods throttled at limit {fmt(cl, 'm')}; usage is capped, so real demand is higher")
    elif th >= 10:
        f("MEDIUM", "CPU_THROTTLED", f"{th:g}% of CPU periods throttled at limit {fmt(cl, 'm')}")
    if cr and r["cpu_p95_m"] > 1.2 * cr:
        f("MEDIUM", "CPU_UNDER_REQUEST", f"cpu p95 {r['cpu_p95_m']:g}m > 1.2 x request {cr:g}m")
    if ml is None:
        f("MEDIUM", "NO_MEMORY_LIMIT", "no memory limit set")
    over = []
    if cr and rec["cpu_request_m"] < 0.5 * cr:
        over.append(f"cpu {cr:g}m -> {rec['cpu_request_m']}m")
    if mr and rec["mem_request_mi"] < 0.5 * mr:
        over.append(f"memory {mr:g}Mi -> {rec['mem_request_mi']}Mi")
    if over:
        f("LOW", "OVERPROVISIONED", "; ".join(over))
    if r["days"] is not None and r["days"] < 7:
        f("LOW", "SHORT_WINDOW", f"only {r['days']:g} days of data")
    findings.sort(key=lambda x: SEV_ORDER[x["severity"]])
    status = findings[0]["severity"] if findings and findings[0]["severity"] != "LOW" else "OK"
    n = r["replicas"]
    d_cpu = ((cr or 0) - rec["cpu_request_m"]) * n
    d_mem = ((mr or 0) - rec["mem_request_mi"]) * n
    return {"id": f"{r['namespace']}/{r['workload']}/{r['container']}", "replicas": int(n), "current": {
        "cpu_request_m": cr, "cpu_limit_m": cl, "mem_request_mi": mr, "mem_limit_mi": ml},
        "recommended": rec, "delta_cpu_m": d_cpu, "delta_mem_mi": d_mem, "status": status, "findings": findings}


def run(text, headroom=0.15, keep_cpu_limits=False, core_month=None, gib_month=None):
    res = [analyze(r, headroom, keep_cpu_limits) for r in read_rows(text)]
    res.sort(key=lambda x: (SEV_ORDER.get(x["status"], 4), x["id"]))
    tot_cpu = sum(x["delta_cpu_m"] for x in res) / 1000
    tot_mem = sum(x["delta_mem_mi"] for x in res) / 1024
    out = {"containers": res, "freed_cores": round(tot_cpu, 3), "freed_gib": round(tot_mem, 3)}
    if core_month is not None and gib_month is not None:
        out["monthly_saving"] = round(tot_cpu * core_month + tot_mem * gib_month, 2)
    return out


def fmt(v, unit):
    return "none" if v is None else f"{v:g}{unit}"


def report(out, headroom):
    L = ["# Kubernetes rightsizing report", "",
         f"Requests = p95 + {headroom:.0%} headroom; CPU limits removed unless kept; memory limit = max x 1.25.", "",
         "| Container | Replicas | Status | CPU req | CPU limit | Mem req | Mem limit |",
         "|---|---|---|---|---|---|---|"]
    for x in out["containers"]:
        c, r = x["current"], x["recommended"]
        L.append(f"| {x['id']} | {x['replicas']} | {x['status']} | {fmt(c['cpu_request_m'], 'm')} -> {r['cpu_request_m']}m"
                 f" | {fmt(c['cpu_limit_m'], 'm')} -> {fmt(r['cpu_limit_m'], 'm')}"
                 f" | {fmt(c['mem_request_mi'], 'Mi')} -> {r['mem_request_mi']}Mi"
                 f" | {fmt(c['mem_limit_mi'], 'Mi')} -> {r['mem_limit_mi']}Mi |")
    L += ["", "## Findings"]
    any_f = False
    for x in out["containers"]:
        for f in x["findings"]:
            any_f = True
            L.append(f"- {f['severity']} {f['code']} {x['id']}: {f['message']}")
    if not any_f:
        L.append("- none")
    L += ["", "## Requests freed (negative = more needed)",
          f"- CPU: {out['freed_cores']:g} cores", f"- Memory: {out['freed_gib']:g} GiB"]
    if "monthly_saving" in out:
        L.append(f"- Estimated monthly saving: {out['monthly_saving']:,.2f}")
    return "\n".join(L) + "\n"


def self_test():
    cases = []

    def ok(name, cond):
        cases.append(bool(cond))
        print(("PASS " if cond else "FAIL ") + name)

    hdr = ",".join(COLUMNS) + "\n"

    def row(**kw):
        base = dict(namespace="ns", workload="w", container="c", replicas=1, cpu_request_m=500, cpu_limit_m=1000,
                    mem_request_mi=512, mem_limit_mi=1024, cpu_p50_m=100, cpu_p95_m=200, cpu_max_m=300,
                    mem_p50_mi=200, mem_p95_mi=300, mem_max_mi=400, throttled_pct=0, oom_kills=0, days=14)
        base.update(kw)
        return ",".join("" if base[c] is None else str(base[c]) for c in COLUMNS) + "\n"

    def one(**kw):
        return run(hdr + row(**kw))["containers"][0]

    def codes(x):
        return {(f["severity"], f["code"]) for f in x["findings"]}

    x = one()
    ok("cpu request = p95 x 1.15 rounded to 5m (230)", x["recommended"]["cpu_request_m"] == 230)
    ok("mem request = 300 x 1.15 = 345 -> 352Mi", x["recommended"]["mem_request_mi"] == 352)
    ok("mem limit = 400 x 1.25 = 500 -> 512Mi", x["recommended"]["mem_limit_mi"] == 512)
    ok("cpu limit removed by default", x["recommended"]["cpu_limit_m"] is None)
    ok("healthy container is OK", x["status"] == "OK")
    ok("headroom 0.25 changes cpu request (250)", run(hdr + row(), headroom=0.25)["containers"][0]["recommended"]["cpu_request_m"] == 250)
    k = run(hdr + row(), keep_cpu_limits=True)["containers"][0]["recommended"]["cpu_limit_m"]
    ok("kept cpu limit = max(360->400, 460->500) = 500", k == 500)
    ok("minimums 10m / 32Mi", one(cpu_p50_m=0, cpu_p95_m=1, cpu_max_m=2, mem_p50_mi=1, mem_p95_mi=2, mem_max_mi=3)["recommended"]
       == {"cpu_request_m": 10, "mem_request_mi": 32, "cpu_limit_m": None, "mem_limit_mi": 32})
    ok("OOM kills -> CRITICAL", ("CRITICAL", "OOM_RISK") in codes(one(oom_kills=2)) and one(oom_kills=2)["status"] == "CRITICAL")
    ok("max at 98% of limit -> OOM_RISK", ("CRITICAL", "OOM_RISK") in codes(one(mem_max_mi=1004, mem_p95_mi=500)))
    ok("missing requests -> HIGH NO_REQUESTS", ("HIGH", "NO_REQUESTS") in codes(one(cpu_request_m=None)))
    ok("mem p95 above request -> HIGH", ("HIGH", "MEM_UNDER_REQUEST") in codes(one(mem_request_mi=256)))
    ok("throttling 30% -> HIGH", ("HIGH", "CPU_THROTTLED") in codes(one(throttled_pct=30)))
    ok("throttling 12% -> MEDIUM", ("MEDIUM", "CPU_THROTTLED") in codes(one(throttled_pct=12)))
    ok("cpu p95 > 1.2 x request -> MEDIUM", ("MEDIUM", "CPU_UNDER_REQUEST") in codes(one(cpu_request_m=150)))
    ok("no memory limit -> MEDIUM", ("MEDIUM", "NO_MEMORY_LIMIT") in codes(one(mem_limit_mi=None)))
    big = one(cpu_request_m=2000, mem_request_mi=4096)
    ok("overprovisioned flagged LOW, status stays OK", ("LOW", "OVERPROVISIONED") in codes(big) and big["status"] == "OK")
    ok("short window flagged", ("LOW", "SHORT_WINDOW") in codes(one(days=3)))
    two = run(hdr + row(replicas=3) + row(workload="b", replicas=2, cpu_request_m=100, mem_request_mi=256),
              core_month=20, gib_month=4)
    # w: (500-230)*3=810m, (512-352)*3=480Mi ; b: (100-230)*2=-260m, (256-352)*2=-192Mi
    ok("freed cores = (810-260)/1000 = 0.55", two["freed_cores"] == 0.55)
    ok("freed GiB = (480-192)/1024 = 0.281", two["freed_gib"] == 0.281)
    ok("monthly saving = 0.55 x 20 + 0.28125 x 4 = 12.125", abs(two["monthly_saving"] - 12.125) < 0.01)
    ok("sorted by severity", two["containers"][0]["id"] == "ns/b/c")
    for bad, label in (("a,b\n1,2\n", "missing columns"), (hdr, "no rows"), (hdr + row(cpu_p95_m="x"), "bad number"),
                       (hdr + row(cpu_p95_m=900), "p95 above max"), (hdr + row(replicas=0), "zero replicas"),
                       (hdr + row(cpu_p95_m=None), "missing usage")):
        try:
            run(bad)
            ok(label + " rejected", False)
        except DataError:
            ok(label + " rejected", True)
    print(f"{sum(cases)}/{len(cases)} passed")
    return 0 if all(cases) else 1


def main(argv=None):
    ap = argparse.ArgumentParser(description="Rightsize Kubernetes container requests and limits from usage percentiles.")
    ap.add_argument("file", nargs="?", help="CSV with the columns listed in the module docstring")
    ap.add_argument("--headroom", type=float, default=15, help="percent added to p95 for requests (default 15)")
    ap.add_argument("--keep-cpu-limits", action="store_true", help="recommend CPU limits instead of removing them")
    ap.add_argument("--core-month", type=float, help="price of one requested core per month")
    ap.add_argument("--gib-month", type=float, help="price of one requested GiB of memory per month")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args(argv)
    if a.self_test:
        return self_test()
    if not a.file:
        ap.error("give a CSV file or --self-test")
    try:
        out = run(open(a.file, encoding="utf-8").read(), a.headroom / 100, a.keep_cpu_limits, a.core_month, a.gib_month)
    except (OSError, DataError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(out, indent=2) if a.json else report(out, a.headroom / 100), end="\n" if a.json else "")
    return 1 if any(x["status"] in ("CRITICAL", "HIGH") for x in out["containers"]) else 0


if __name__ == "__main__":
    sys.exit(main())
PythonDevOpsCloud+2
F@f
0