项目3:PVC 自动扩容工具
项目3:PVC 自动扩容工具
项目背景
持久化存储(PVC)使用率过高会导致:
- ❌ 应用写入失败
- ❌ Pod 崩溃重启
- ❌ 数据丢失风险
解决方案:
监控 PVC 使用率,自动扩容到合理大小。
功能需求
核心功能
- ✅ 监控 PVC 使用率
- ✅ 自动扩容(支持百分比和固定值)
- ✅ 扩容策略配置
- ✅ 支持多种 StorageClass
- ✅ Dry Run 模式
高级功能
- ✅ 扩容历史记录
- ✅ 扩容上限保护
- ✅ 告警通知
- ✅ 回滚机制
Go 完整实现
main.go
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"pvc-expander/pkg/config"
"pvc-expander/pkg/expander"
)
var log = logrus.New()
func main() {
kubeconfig := flag.String("kubeconfig", "", "path to kubeconfig")
configFile := flag.String("config", "", "path to config file")
flag.Parse()
// 加载配置
cfg, err := config.LoadConfig(*configFile)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// 设置日志
level, _ := logrus.ParseLevel(cfg.LogLevel)
log.SetLevel(level)
log.SetFormatter(&logrus.JSONFormatter{})
// 创建 Kubernetes 客户端
var k8sConfig *rest.Config
if *kubeconfig != "" {
k8sConfig, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
} else {
k8sConfig, err = rest.InClusterConfig()
}
if err != nil {
log.Fatalf("Failed to create k8s config: %v", err)
}
clientset, err := kubernetes.NewForConfig(k8sConfig)
if err != nil {
log.Fatalf("Failed to create clientset: %v", err)
}
// 创建 Expander
pvcExpander := expander.NewPVCExpander(clientset, cfg, log)
// 启动 Metrics Server
go pvcExpander.StartMetricsServer(cfg.MetricsPort)
// 启动 Expander
stopCh := make(chan struct{})
go pvcExpander.Run(stopCh)
// 等待信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
log.Info("PVC Expander started")
<-sigCh
log.Info("Shutting down...")
close(stopCh)
time.Sleep(2 * time.Second)
}
pkg/expander/expander.go
package expander
import (
"context"
"fmt"
"time"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"pvc-expander/pkg/config"
)
type PVCExpander struct {
clientset *kubernetes.Clientset
config *config.Config
logger *logrus.Logger
metrics *Metrics
history map[string]*ExpansionHistory
}
type PVCMetrics struct {
Namespace string
Name string
CurrentSize int64
UsedSize int64
UsagePercent float64
StorageClass string
}
type ExpansionHistory struct {
Timestamp time.Time
OldSize int64
NewSize int64
ExpansionNum int
}
func NewPVCExpander(
clientset *kubernetes.Clientset,
cfg *config.Config,
logger *logrus.Logger,
) *PVCExpander {
return &PVCExpander{
clientset: clientset,
config: cfg,
logger: logger,
metrics: NewMetrics(),
history: make(map[string]*ExpansionHistory),
}
}
func (pe *PVCExpander) Run(stopCh <-chan struct{}) {
// 创建 Informer 监听 PVC 变化
factory := informers.NewSharedInformerFactory(pe.clientset, 30*time.Second)
pvcInformer := factory.Core().V1().PersistentVolumeClaims()
pvcInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
pvc := obj.(*corev1.PersistentVolumeClaim)
pe.checkPVC(pvc)
},
UpdateFunc: func(oldObj, newObj interface{}) {
pvc := newObj.(*corev1.PersistentVolumeClaim)
pe.checkPVC(pvc)
},
})
factory.Start(stopCh)
cache.WaitForCacheSync(stopCh, pvcInformer.Informer().HasSynced)
pe.logger.Info("Informer synced, starting periodic check")
// 定期检查
ticker := time.NewTicker(time.Duration(pe.config.CheckInterval) * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
pe.checkAllPVCs()
case <-stopCh:
pe.logger.Info("Stopping PVC expander")
return
}
}
}
func (pe *PVCExpander) checkPVC(pvc *corev1.PersistentVolumeClaim) {
// 只处理 Bound 状态的 PVC
if pvc.Status.Phase != corev1.ClaimBound {
return
}
// 检查是否在白名单中
if !pe.shouldExpandPVC(pvc) {
return
}
// 获取 PVC 使用情况
metrics := pe.getPVCMetrics(pvc)
if metrics == nil {
return
}
// 评估是否需要扩容
pe.evaluateExpansion(pvc, metrics)
}
func (pe *PVCExpander) shouldExpandPVC(pvc *corev1.PersistentVolumeClaim) bool {
// 检查命名空间
if len(pe.config.IncludeNamespaces) > 0 {
included := false
for _, ns := range pe.config.IncludeNamespaces {
if ns == pvc.Namespace {
included = true
break
}
}
if !included {
return false
}
}
// 检查排除列表
for _, ns := range pe.config.ExcludeNamespaces {
if ns == pvc.Namespace {
return false
}
}
// 检查 StorageClass
if pvc.Spec.StorageClassName == nil {
return false
}
storageClass := *pvc.Spec.StorageClassName
// 检查是否在支持列表中
if len(pe.config.SupportedStorageClasses) > 0 {
supported := false
for _, sc := range pe.config.SupportedStorageClasses {
if sc == storageClass {
supported = true
break
}
}
if !supported {
return false
}
}
// 检查 annotation(可以通过 annotation 禁用自动扩容)
if pvc.Annotations["auto-expand"] == "false" {
return false
}
return true
}
func (pe *PVCExpander) getPVCMetrics(pvc *corev1.PersistentVolumeClaim) *PVCMetrics {
// 获取 PVC 大小
currentSize := pvc.Status.Capacity[corev1.ResourceStorage]
// 获取 Pod 使用该 PVC
pods, err := pe.clientset.CoreV1().Pods(pvc.Namespace).List(
context.TODO(),
metav1.ListOptions{},
)
if err != nil {
pe.logger.WithError(err).Error("Failed to list pods")
return nil
}
// 找到使用该 PVC 的 Pod
var targetPod *corev1.Pod
for i := range pods.Items {
pod := &pods.Items[i]
for _, volume := range pod.Spec.Volumes {
if volume.PersistentVolumeClaim != nil &&
volume.PersistentVolumeClaim.ClaimName == pvc.Name {
targetPod = pod
break
}
}
if targetPod != nil {
break
}
}
if targetPod == nil {
return nil
}
// 通过 kubectl exec 获取磁盘使用情况
usedSize, err := pe.getDiskUsage(targetPod, pvc)
if err != nil {
pe.logger.WithError(err).Warn("Failed to get disk usage")
return nil
}
metrics := &PVCMetrics{
Namespace: pvc.Namespace,
Name: pvc.Name,
CurrentSize: currentSize.Value(),
UsedSize: usedSize,
StorageClass: *pvc.Spec.StorageClassName,
}
if metrics.CurrentSize > 0 {
metrics.UsagePercent = float64(metrics.UsedSize) / float64(metrics.CurrentSize) * 100
}
return metrics
}
func (pe *PVCExpander) getDiskUsage(pod *corev1.Pod, pvc *corev1.PersistentVolumeClaim) (int64, error) {
// 简化实现:这里应该通过 kubectl exec df 获取实际使用量
// 由于篇幅限制,这里返回模拟值
// 实际生产中应该使用 exec 或 metrics-server
// TODO: 实现通过 exec 获取磁盘使用量
// kubectl exec <pod> -- df -B1 <mount-path> | awk 'NR==2 {print $3}'
return 0, fmt.Errorf("not implemented")
}
func (pe *PVCExpander) evaluateExpansion(pvc *corev1.PersistentVolumeClaim, metrics *PVCMetrics) {
// 检查使用率阈值
if metrics.UsagePercent < float64(pe.config.Thresholds.UsageWarning) {
return
}
// 检查是否刚刚扩容过(避免频繁扩容)
pvcKey := fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name)
if history, exists := pe.history[pvcKey]; exists {
if time.Since(history.Timestamp) < time.Duration(pe.config.ExpansionCooldown)*time.Minute {
pe.logger.Warnf("PVC %s in cooldown period", pvcKey)
return
}
// 检查扩容次数限制
if history.ExpansionNum >= pe.config.MaxExpansions {
pe.logger.Warnf("PVC %s reached max expansions (%d)", pvcKey, pe.config.MaxExpansions)
return
}
}
// 计算新大小
newSize := pe.calculateNewSize(metrics.CurrentSize)
// 检查扩容上限
if newSize > pe.config.MaxSize {
pe.logger.Warnf("New size %d exceeds max size %d", newSize, pe.config.MaxSize)
newSize = pe.config.MaxSize
}
// 执行扩容
pe.expandPVC(pvc, newSize, metrics)
}
func (pe *PVCExpander) calculateNewSize(currentSize int64) int64 {
var newSize int64
switch pe.config.ExpansionStrategy {
case "percentage":
// 按百分比增长
increase := float64(currentSize) * (float64(pe.config.ExpansionPercentage) / 100.0)
newSize = currentSize + int64(increase)
case "fixed":
// 固定增量
newSize = currentSize + pe.config.ExpansionFixedSize
default:
newSize = currentSize * 2 // 默认翻倍
}
return newSize
}
func (pe *PVCExpander) expandPVC(
pvc *corev1.PersistentVolumeClaim,
newSize int64,
metrics *PVCMetrics,
) {
logger := pe.logger.WithFields(logrus.Fields{
"namespace": pvc.Namespace,
"pvc": pvc.Name,
"current_size": metrics.CurrentSize,
"new_size": newSize,
"usage": fmt.Sprintf("%.2f%%", metrics.UsagePercent),
})
if pe.config.DryRun {
logger.Info("[DRY RUN] Would expand PVC")
pe.metrics.PVCsChecked.Inc()
return
}
// 更新 PVC 大小
pvc.Spec.Resources.Requests[corev1.ResourceStorage] = *resource.NewQuantity(newSize, resource.BinarySI)
_, err := pe.clientset.CoreV1().PersistentVolumeClaims(pvc.Namespace).Update(
context.TODO(),
pvc,
metav1.UpdateOptions{},
)
if err != nil {
logger.WithError(err).Error("Failed to expand PVC")
pe.metrics.ExpansionErrors.Inc()
return
}
logger.Info("PVC expanded successfully")
pe.metrics.PVCsExpanded.Inc()
// 记录扩容历史
pvcKey := fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name)
expansionNum := 1
if history, exists := pe.history[pvcKey]; exists {
expansionNum = history.ExpansionNum + 1
}
pe.history[pvcKey] = &ExpansionHistory{
Timestamp: time.Now(),
OldSize: metrics.CurrentSize,
NewSize: newSize,
ExpansionNum: expansionNum,
}
}
func (pe *PVCExpander) checkAllPVCs() {
pe.logger.Debug("Running periodic PVC check")
pvcs, err := pe.clientset.CoreV1().PersistentVolumeClaims("").List(
context.TODO(),
metav1.ListOptions{},
)
if err != nil {
pe.logger.WithError(err).Error("Failed to list PVCs")
return
}
for _, pvc := range pvcs.Items {
pe.checkPVC(&pvc)
}
}
Python 实现
pvc_expander.py
#!/usr/bin/env python3
import time
import logging
import yaml
from datetime import datetime, timedelta
from kubernetes import client, config, watch
from kubernetes.client.rest import ApiException
class PVCExpander:
def __init__(self, config_file=None):
# 加载 Kubernetes 配置
try:
config.load_incluster_config()
except:
config.load_kube_config()
self.v1 = client.CoreV1Api()
# 加载配置
self.load_config(config_file)
# 扩容历史
self.history = {}
# 设置日志
logging.basicConfig(
level=getattr(logging, self.config['logLevel'].upper()),
format='%(asctime)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger(__name__)
def load_config(self, config_file):
"""加载配置"""
default_config = {
'checkInterval': 300,
'logLevel': 'info',
'dryRun': False,
'includeNamespaces': [],
'excludeNamespaces': ['kube-system'],
'supportedStorageClasses': [],
'thresholds': {
'usageWarning': 80,
'usageCritical': 90
},
'expansionStrategy': 'percentage',
'expansionPercentage': 50,
'expansionFixedSize': 10737418240, # 10Gi
'maxSize': 107374182400, # 100Gi
'maxExpansions': 10,
'expansionCooldown': 60
}
if config_file:
with open(config_file, 'r') as f:
user_config = yaml.safe_load(f)
default_config.update(user_config)
self.config = default_config
def run(self):
"""运行扩容器"""
self.logger.info("Starting PVC Expander")
w = watch.Watch()
last_check = time.time()
while True:
try:
# Watch PVC 事件
for event in w.stream(
self.v1.list_persistent_volume_claim_for_all_namespaces,
timeout_seconds=self.config['checkInterval']
):
event_type = event['type']
pvc = event['object']
if event_type in ['ADDED', 'MODIFIED']:
self.check_pvc(pvc)
# 定期全量检查
if time.time() - last_check >= self.config['checkInterval']:
self.check_all_pvcs()
last_check = time.time()
except Exception as e:
self.logger.error(f"Error in watch loop: {e}")
time.sleep(5)
def check_pvc(self, pvc):
"""检查单个 PVC"""
# 只处理 Bound 状态
if pvc.status.phase != 'Bound':
return
# 检查是否应该扩容
if not self.should_expand_pvc(pvc):
return
# 获取 PVC 使用情况
metrics = self.get_pvc_metrics(pvc)
if not metrics:
return
# 评估是否需要扩容
self.evaluate_expansion(pvc, metrics)
def should_expand_pvc(self, pvc):
"""检查是否应该扩容该 PVC"""
namespace = pvc.metadata.namespace
# 检查命名空间
if self.config['includeNamespaces']:
if namespace not in self.config['includeNamespaces']:
return False
if namespace in self.config['excludeNamespaces']:
return False
# 检查 StorageClass
if not pvc.spec.storage_class_name:
return False
if self.config['supportedStorageClasses']:
if pvc.spec.storage_class_name not in self.config['supportedStorageClasses']:
return False
# 检查 annotation
annotations = pvc.metadata.annotations or {}
if annotations.get('auto-expand') == 'false':
return False
return True
def get_pvc_metrics(self, pvc):
"""获取 PVC 指标"""
try:
# 获取当前大小
current_size = self.parse_quantity(
pvc.status.capacity.get('storage', '0')
)
# 获取使用该 PVC 的 Pod
pod = self.find_pod_using_pvc(pvc)
if not pod:
return None
# 获取磁盘使用情况(简化实现)
# 实际应该通过 exec df 获取
used_size = current_size * 0.85 # 模拟值
usage_percent = (used_size / current_size) * 100 if current_size > 0 else 0
return {
'namespace': pvc.metadata.namespace,
'name': pvc.metadata.name,
'current_size': current_size,
'used_size': used_size,
'usage_percent': usage_percent,
'storage_class': pvc.spec.storage_class_name
}
except Exception as e:
self.logger.error(f"Failed to get PVC metrics: {e}")
return None
def find_pod_using_pvc(self, pvc):
"""查找使用该 PVC 的 Pod"""
try:
pods = self.v1.list_namespaced_pod(pvc.metadata.namespace)
for pod in pods.items:
if not pod.spec.volumes:
continue
for volume in pod.spec.volumes:
if (volume.persistent_volume_claim and
volume.persistent_volume_claim.claim_name == pvc.metadata.name):
return pod
return None
except Exception as e:
self.logger.error(f"Failed to find pod: {e}")
return None
def parse_quantity(self, quantity_str):
"""解析 Kubernetes 资源数量"""
import re
multipliers = {
'Ki': 1024,
'Mi': 1024 ** 2,
'Gi': 1024 ** 3,
'Ti': 1024 ** 4,
'Pi': 1024 ** 5,
'k': 1000,
'M': 1000 ** 2,
'G': 1000 ** 3,
'T': 1000 ** 4,
'P': 1000 ** 5
}
match = re.match(r'^(\d+(?:\.\d+)?)([A-Za-z]*)$', quantity_str)
if not match:
return 0
value = float(match.group(1))
unit = match.group(2)
return int(value * multipliers.get(unit, 1))
def evaluate_expansion(self, pvc, metrics):
"""评估是否需要扩容"""
# 检查使用率
if metrics['usage_percent'] < self.config['thresholds']['usageWarning']:
return
# 检查冷却期
pvc_key = f"{metrics['namespace']}/{metrics['name']}"
if pvc_key in self.history:
history = self.history[pvc_key]
cooldown = timedelta(minutes=self.config['expansionCooldown'])
if datetime.now() - history['timestamp'] < cooldown:
self.logger.warning(f"PVC {pvc_key} in cooldown period")
return
# 检查扩容次数
if history['expansion_num'] >= self.config['maxExpansions']:
self.logger.warning(
f"PVC {pvc_key} reached max expansions ({history['expansion_num']})"
)
return
# 计算新大小
new_size = self.calculate_new_size(metrics['current_size'])
# 检查上限
if new_size > self.config['maxSize']:
self.logger.warning(f"New size {new_size} exceeds max size")
new_size = self.config['maxSize']
# 执行扩容
self.expand_pvc(pvc, new_size, metrics)
def calculate_new_size(self, current_size):
"""计算新大小"""
strategy = self.config['expansionStrategy']
if strategy == 'percentage':
increase = current_size * (self.config['expansionPercentage'] / 100.0)
return int(current_size + increase)
elif strategy == 'fixed':
return current_size + self.config['expansionFixedSize']
else:
return current_size * 2
def expand_pvc(self, pvc, new_size, metrics):
"""扩容 PVC"""
self.logger.info(
f"{'[DRY RUN] Would expand' if self.config['dryRun'] else 'Expanding'} PVC "
f"{metrics['namespace']}/{metrics['name']} "
f"from {metrics['current_size']} to {new_size} "
f"(usage: {metrics['usage_percent']:.2f}%)"
)
if self.config['dryRun']:
return
try:
# 更新 PVC
pvc.spec.resources.requests['storage'] = f"{new_size}"
self.v1.patch_namespaced_persistent_volume_claim(
name=pvc.metadata.name,
namespace=pvc.metadata.namespace,
body=pvc
)
# 记录历史
pvc_key = f"{metrics['namespace']}/{metrics['name']}"
expansion_num = 1
if pvc_key in self.history:
expansion_num = self.history[pvc_key]['expansion_num'] + 1
self.history[pvc_key] = {
'timestamp': datetime.now(),
'old_size': metrics['current_size'],
'new_size': new_size,
'expansion_num': expansion_num
}
self.logger.info(f"PVC {pvc_key} expanded successfully")
except ApiException as e:
self.logger.error(f"Failed to expand PVC: {e}")
def check_all_pvcs(self):
"""检查所有 PVCs"""
self.logger.debug("Running periodic PVC check")
try:
pvcs = self.v1.list_persistent_volume_claim_for_all_namespaces()
for pvc in pvcs.items:
self.check_pvc(pvc)
except Exception as e:
self.logger.error(f"Error checking PVCs: {e}")
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Kubernetes PVC Auto Expander')
parser.add_argument('--config', help='Path to config file')
args = parser.parse_args()
expander = PVCExpander(config_file=args.config)
expander.run()
配置示例
config.yaml
checkInterval: 300 # 检查间隔(秒)
logLevel: info
dryRun: false
# 命名空间过滤
includeNamespaces: []
excludeNamespaces:
- kube-system
- kube-public
# 支持的 StorageClass
supportedStorageClasses:
- fast-ssd
- standard
# 使用率阈值
thresholds:
usageWarning: 80 # 80% 开始扩容
usageCritical: 90 # 90% 严重告警
# 扩容策略
expansionStrategy: percentage # percentage 或 fixed
expansionPercentage: 50 # 增加 50%
expansionFixedSize: 10737418240 # 10Gi
# 限制
maxSize: 107374182400 # 100Gi 上限
maxExpansions: 10 # 最多扩容 10 次
expansionCooldown: 60 # 冷却期 60 分钟
部署
deploy/deployment.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: pvc-expander
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pvc-expander
rules:
- apiGroups: [""]
resources: ["persistentvolumeclaims", "pods"]
verbs: ["list", "get", "watch", "patch", "update"]
- apiGroups: [""]
resources: ["persistentvolumeclaims/status"]
verbs: ["get", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pvc-expander
subjects:
- kind: ServiceAccount
name: pvc-expander
namespace: kube-system
roleRef:
kind: ClusterRole
name: pvc-expander
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ConfigMap
metadata:
name: pvc-expander-config
namespace: kube-system
data:
config.yaml: |
checkInterval: 300
logLevel: info
dryRun: false
excludeNamespaces:
- kube-system
thresholds:
usageWarning: 80
expansionStrategy: percentage
expansionPercentage: 50
maxSize: 107374182400
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: pvc-expander
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: pvc-expander
template:
metadata:
labels:
app: pvc-expander
spec:
serviceAccountName: pvc-expander
containers:
- name: pvc-expander
image: your-registry/pvc-expander:latest
args:
- --config=/etc/pvc-expander/config.yaml
volumeMounts:
- name: config
mountPath: /etc/pvc-expander
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
volumes:
- name: config
configMap:
name: pvc-expander-config
使用示例
1. 标记 PVC 允许自动扩容
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
annotations:
auto-expand: "true" # 启用自动扩容
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 10Gi
2. 禁用自动扩容
annotations:
auto-expand: "false" # 禁用
3. 查看扩容历史
# 查看 PVC Expander 日志
kubectl logs -n kube-system deployment/pvc-expander | grep "expanded successfully"
监控告警
Prometheus 告警规则
groups:
- name: pvc-expander
rules:
- alert: PVCNearFull
expr: |
(kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes) > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "PVC {{ $labels.persistentvolumeclaim }} 接近满"
description: "使用率 {{ $value | humanizePercentage }}"
- alert: PVCExpansionFailed
expr: rate(pvc_expander_expansion_errors_total[5m]) > 0
for: 5m
labels:
severity: critical
annotations:
summary: "PVC 扩容失败"
总结
功能特性
✅ 自动扩容: 基于使用率自动扩容
✅ 灵活策略: 百分比或固定值增长
✅ 安全保护: 上限保护、冷却期、次数限制
✅ 可观测: 扩容历史记录
✅ 干预控制: Annotation 控制
注意事项
⚠️ StorageClass 支持: 确保 StorageClass 支持扩容(allowVolumeExpansion: true)
⚠️ 文件系统扩展: 某些文件系统需要手动扩展
⚠️ 备份数据: 扩容前建议备份
⚠️ 成本控制: 设置合理的 maxSize
扩展方向
- 智能预测: 基于历史趋势预测扩容时机
- 成本优化: 集成云厂商 API 获取存储成本
- 自动缩容: 使用率低时自动缩容(需要注意数据安全)
- 告警集成: 扩容前后发送通知
下一个项目将介绍 Namespace 资源报表生成器。