学习目标
学完本篇,你应该能够:
- 说清平台元数据中心与多套物理隔离 K8s 集群、统一 K8s API 网关之间的关系,知道主数据如何驱动租户 Namespace 强隔离。
- 在后台独立完成网关与平台组件的状态巡检、License 与全局配额管理、特性开关配置,并理解每一项操作背后的审计留痕要求。
- 独立规划并执行一次平台组件升级 + 回滚演练,走通多级审批流,且全过程指标进入 Prometheus 联邦。
- 配置并验证数据备份与容灾策略,确保全局审计视图可检索 90 天以上且不可篡改。
- 当后台出现"组件失联 / 网关抖动 / 审计缺失"类故障时,依据排查流程图快速定位根因。
前置知识:Kubernetes 核心概念(Namespace / Secret / Deployment / Operator);Prometheus 联邦与 Thanos 基础;RBAC 与 OAuth2 基本概念;企业 ITIL 变更与审批流程。
本章你会动手做的事:
- 在测试环境通过后台将某租户全局配额从 100 核调整为 120 核,并确认审计记录可查。
- 触发一次平台组件小版本升级,观察审批流、联邦指标、Grafana 大盘联动。
- 模拟一次备份恢复,验证备份对象存储与恢复 RTO/RPO 达标。
一、模块概述与企业商用价值
类比:平台系统后台管理模块就像一栋写字楼的"总控室"——前台物业(统一 K8s API 网关)负责带人进出各楼层(集群),而总控室掌握着整栋楼的房本(元数据中心)、各楼层的电表底数(全局配额)、监控录像(全局审计)和电梯检修计划(组件升级)。没有总控室,物业只能被动响应,出了事谁都查不清。
适用角色与业务痛点
平台系统后台面向平台管理员(Platform Admin)与SRE / 运维负责人,解决以下真实痛点:
- 主数据散落:租户、应用、环境、集群信息分散在多个系统,配置漂移导致 Namespace 隔离失效。
- 黑盒运维:组件异常无法从全局视角定位,只能逐集群登录排查。
- 合规无据:金融 / 政企客户要求操作可审计、可回溯 90 天以上,缺一个统一审计视图便无法满足等保 / 审计要求。
- 升级风险高:平台自身组件升级无审批、无回滚、无监控,影响全部租户。
平台不可替代性
本模块是平台"管理平面的管理平面"——它不托管业务应用,却管理着托管一切的前置元数据与基础设施状态。任何一个商用 PaaS 如果缺少统一后台,就无法在物理隔离多集群之上提供一致的租户视图、审计视图与升级视图。
模块在平台中的定位
graph TD A[企业自研 PaaS 平台] --> B[统一 K8s API 网关] A --> C[平台系统后台管理模块] A --> D[联邦 Prometheus + Thanos 长期存储] A --> E[(业务应用 Pod 租户 Namespace)] C --> B C --> D C --> F[(平台元数据中心 租户/应用/环境/集群)] C --> G[全局审计视图 90天+] B --> E
这张图说明:后台管理模块通过统一 K8s API 网关触达各物理隔离集群,并把自身状态与操作事件写入元数据中心与联邦监控;它和被纳管的业务 Pod 处于不同的管理层次。
项目结构与文件清单
本篇所有代码落在一个独立服务 platform-admin 中,Go module 路径 github.com/example/paas/admin。下述目录树即本章逐节完整展示的文件清单:
platform-admin/
├── go.mod
├── cmd/admin/main.go # 进程入口:装配 DB / 网关客户端 / gin 路由
├── deploy/
│ ├── admin-configmap.yaml # 配置(网关地址 / DB / 留存天数)
│ ├── admin-rbac.yaml # ServiceAccount + Role + RoleBinding
│ ├── admin-deployment.yaml # 管理后台 Deployment + 探针
│ ├── db-backup-cronjob.yaml # 元数据中心 DB 每日异地加密备份
│ └── admin-prometheus.yaml # ServiceMonitor + 告警规则
└── internal/
├── model/
│ ├── audit.go # GORM 审计表(90天+ 不可篡改)
│ └── config.go # 全局配额 / 特性开关模型
├── store/
│ └── postgres.go # GORM 连接与自动迁移
├── k8s/
│ ├── client.go # client-go 经统一网关(GatewayRoundTripper)
│ └── tenant.go # 租户 Namespace / ResourceQuota reconcile
└── admin/
├── metrics.go # Prometheus 指标 + Metrics 中间件
├── middleware.go # RBAC + 审计落库
├── handler.go # gin 管理 API(审计查询/配额管理)
└── backup.go # 备份 Job 经网关触发
下面是 go.mod,所有源码编译依赖以此为基准:
// file: go.mod
module github.com/example/paas/admin
go 1.22
require (
github.com/gin-gonic/gin v1.10.0
github.com/prometheus/client_golang v1.19.1
gorm.io/driver/postgres v1.5.7
gorm.io/gorm v1.25.10
k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2
k8s.io/client-go v0.30.2
)
二、细分功能详解(商用生产级)
下表区分【基础能力】与【高级企业增值能力】,并标注「★禁止删减」项。删除任何 Demo 简化功能,仅保留商用生产级能力。
| 编号 | 功能 | 分级 | 说明 | 禁删标记 |
|---|---|---|---|---|
| ① | 平台元数据中心 | 基础能力 | 租户 / 应用 / 环境 / 集群主数据统一管理,驱动 Namespace 强隔离与配额下发 | ★禁止删减(权限) |
| ② | 网关与组件状态管理 | 基础能力 | 统一 K8s API 网关连接态、平台各组件健康度巡检与纳管状态看板 | — |
| ③ | License 与全局配额管理 | 基础能力 | 按租户 / 环境下发 CPU、内存、存储、中间件实例等全局配额 | ★禁止删减(权限) |
| ④ | 系统配置与特性开关 | 基础能力 | 平台级特性开关(Feature Flag)、系统参数集中配置、灰度生效 | — |
| ⑤ | 数据备份与容灾 | 高级企业增值能力 | 元数据中心 + etcd + 配置全量备份,异地对象存储、定期演练恢复 | ★禁止删减(备份) |
| ⑥ | 平台组件升级与回滚 | 高级企业增值能力 | 组件版本治理、滚动升级、一键回滚、升级审批绑定 | — |
| ⑦ | 全局审计视图 | 高级企业增值能力 | 跨模块操作日志聚合,留存 90 天以上,不可篡改、可按租户检索 | ★禁止删减(审计) |
功能架构分层
graph TD L1[接入层 平台管理员 SSO/OAuth2 登录] --> L2 L2[控制层 后台管理服务 Admin Console] --> L3 L3[能力层
元数据中心
配额管理
特性开关
备份容灾
升级回滚
全局审计] --> L4 L4[支撑层
统一 K8s API 网关
联邦 Prometheus + Thanos
审批工作流引擎] --> L5 L5[资源层 多套物理隔离 K8s 集群 租户 Namespace]
这张功能架构图把后台拆成接入、控制、能力、支撑、资源五层,强调所有底层动作都经由统一 K8s API 网关和联邦监控支撑层下发到物理隔离集群。
⚠️ 新手必踩的坑:把"全局配额"和"K8s ResourceQuota"混为一谈。全局配额在元数据中心做逻辑管控与成本计量,真正生效的 ResourceQuota 由平台控制器经网关下发给租户 Namespace,二者必须保持一致,否则会出现"后台显示已用满、集群实际还能调度"的漂移。
2.1 基础设施即代码(IaC):ConfigMap 与 RBAC
平台后台以 SA platform-admin 运行,仅持有 paas-system 命名空间内的受限权限(命名空间、配额、配置、备份 Job)。绝不直接持有集群管理员凭据。
# file: deploy/admin-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: platform-admin-config
namespace: paas-system
labels:
app: platform-admin
paas.io/managed: "true"
data:
LOG_LEVEL: "info"
GATEWAY_ADDR: "https://k8s-gateway.paas.internal" # 统一 K8s API 网关地址
DB_HOST: "pg-admin.paas-system.svc.cluster.local"
DB_PORT: "5432"
DB_NAME: "paas_admin"
DB_SSLMODE: "require"
AUDIT_RETENTION_DAYS: "90" # 审计留存 ≥90 天
BACKUP_BUCKET: "s3://paas-backup-prod/metadata" # 异地对象存储
# file: deploy/admin-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: platform-admin
namespace: paas-system
labels:
app: platform-admin
paas.io/managed: "true"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: platform-admin-role
namespace: paas-system
rules:
- apiGroups: [""]
resources: ["namespaces", "resourcequotas", "configmaps", "secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch", "create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: platform-admin-binding
namespace: paas-system
subjects:
- kind: ServiceAccount
name: platform-admin
namespace: paas-system
roleRef:
kind: Role
name: platform-admin-role
apiGroup: rbac.authorization.k8s.io
2.2 数据模型:审计表与配额 / 特性开关(GORM)
审计表 audit_logs 带 tenant/app/env/cluster 标签,对应 Prometheus 联邦指标维度;敏感字段(前后值)在写入前由上层加密,落库后仅只读副本可查,配合 Thanos 实现 90 天不可篡改留存。
// file: internal/model/audit.go
package model
import "time"
// AuditLog 全局审计视图,留存 90 天以上,不可篡改(数据库只读副本 + Thanos 备份)。
// 标签 tenant/app/env/cluster 与 Prometheus 联邦指标对齐。
type AuditLog struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
TraceID string `gorm:"size:64;index" json:"trace_id"`
Actor string `gorm:"size:128;index" json:"actor"` // 操作人
Role string `gorm:"size:64" json:"role"` // 平台管理员/审计员/审批人
Tenant string `gorm:"size:128;index" json:"tenant"` // 租户
App string `gorm:"size:128;index" json:"app"` // 应用
Env string `gorm:"size:32;index" json:"env"` // prod/staging/test/dev
Cluster string `gorm:"size:128;index" json:"cluster"` // 物理隔离集群标识
Resource string `gorm:"size:256" json:"resource"` // 资源类型
Action string `gorm:"size:128" json:"action"` // create/update/delete/quota
BeforeValue string `gorm:"type:text" json:"before_value"` // 变更前值(敏感字段已加密)
AfterValue string `gorm:"type:text" json:"after_value"` // 变更后值
ApprovalID string `gorm:"size:64;index" json:"approval_id"` // 关联审批单
Result string `gorm:"size:16" json:"result"` // success/fail
ClientIP string `gorm:"size:64" json:"client_ip"`
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
func (AuditLog) TableName() string { return "audit_logs" }
// file: internal/model/config.go
package model
import "time"
// GlobalQuota 全局配额(元数据中心逻辑管控 + 成本计量)。
// 真正生效的 ResourceQuota 由平台控制器经统一 K8s API 网关下发给租户 Namespace。
type GlobalQuota struct {
ID uint64 `gorm:"primaryKey" json:"id"`
Tenant string `gorm:"size:128;uniqueIndex:uniq_tenant_env" json:"tenant"`
Env string `gorm:"size:32;uniqueIndex:uniq_tenant_env" json:"env"`
CPUQuota int64 `json:"cpu_quota"` // 单位:毫核
MemQuota int64 `json:"mem_quota"` // 单位:Mi
StorageQuota int64 `json:"storage_quota"` // 单位:Gi
MiddlewareInstances int `json:"middleware_instances"`
LicenseEdition string `gorm:"size:32" json:"license_edition"` // standard/enterprise/finance
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (GlobalQuota) TableName() string { return "global_quotas" }
// FeatureFlag 平台级特性开关,支持按租户灰度。
type FeatureFlag struct {
ID uint64 `gorm:"primaryKey" json:"id"`
Key string `gorm:"size:128;uniqueIndex" json:"key"`
Enabled bool `json:"enabled"`
GrayTenant string `gorm:"size:128" json:"gray_tenant"` // 灰度租户,空表示全量
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (FeatureFlag) TableName() string { return "feature_flags" }
三、底层架构联动设计
1. 与多 K8s 集群交互逻辑、API 调用链路
后台管理服务绝不直连任何集群 apiserver。所有读写都走统一 K8s API 网关(反向隧道 / 中心代理模式,参考 remotedialer 思路):kubeconfig 只在网关侧解密下发,后台持有限定作用域的网关令牌。
graph TD
A[后台管理服务 Admin Console] --> B[统一 K8s API 网关]
B --> C{按 cluster 标签路由}
C --> D[生产集群 apiserver]
C --> E[预发布集群 apiserver]
C --> F[测试集群 apiserver]
B --> G[(网关侧解密 kubeconfig 信封加密)]
A --> H[联邦 Prometheus 查询 中心 + Thanos]这张调用链路图表明:后台对多集群的管控是"单入口、多出口",网关负责物理隔离集群的连接收敛与解密,避免凭据散落。
3.1 client-go 经统一网关执行(Go 调用 K8s 演示)
client.go 用 GatewayRoundTripper 在每次请求注入网关令牌与目标集群标签(X-PaaS-Cluster / X-PaaS-Env)。后台只持有网关令牌,网关按集群标签路由到对应物理隔离 apiserver,apiserver 凭据只在网关侧存在。
// file: internal/k8s/client.go
package k8s
import (
"context"
"net/http"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// GatewayRoundTripper 在每次请求注入网关令牌与目标集群标签,
// 后台管理服务绝不直连 apiserver,所有请求经统一 K8s API 网关收敛。
type GatewayRoundTripper struct {
GatewayToken string
Cluster string
Env string
Next http.RoundTripper
}
func (rt *GatewayRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", "Bearer "+rt.GatewayToken)
req.Header.Set("X-PaaS-Cluster", rt.Cluster) // 网关按此标签路由到物理隔离集群
req.Header.Set("X-PaaS-Env", rt.Env)
return rt.Next.RoundTrip(req)
}
// Client 封装经网关访问某集群的 clientset。
type Client struct {
Clientset *kubernetes.Clientset
Cluster string
Env string
}
// Dial 构造指向统一 K8s API 网关的 client-go 客户端。
func Dial(gatewayAddr, gatewayToken, cluster, env string) (*Client, error) {
cfg := &rest.Config{
Host: gatewayAddr,
TLSClientConfig: rest.TLSClientConfig{
Insecure: false, // 生产使用网关 CA,不跳过校验
},
}
cfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
return &GatewayRoundTripper{
GatewayToken: gatewayToken,
Cluster: cluster,
Env: env,
Next: rt,
}
}
cs, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, err
}
return &Client{Clientset: cs, Cluster: cluster, Env: env}, nil
}
// Health 探测网关到目标集群的连接态。
func (c *Client) Health(ctx context.Context) error {
_, err := c.Clientset.Discovery().ServerVersion()
return err
}
tenant.go 演示"主数据如何驱动租户 Namespace 强隔离":根据 tenant/env 标签 reconcile Namespace 与 ResourceQuota,直接消除第二章提到的配额漂移。
// file: internal/k8s/tenant.go
package k8s
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/api/resource"
"github.com/example/paas/admin/internal/model"
)
// ReconcileNamespace 确保租户 Namespace 存在且带 tenant/env 标签(资源强隔离)。
func (c *Client) ReconcileNamespace(ctx context.Context, tenant, env string) error {
nsName := fmt.Sprintf("%s-%s", tenant, env)
cs := c.Clientset
ns, err := cs.CoreV1().Namespaces().Get(ctx, nsName, metav1.GetOptions{})
if err != nil {
ns = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: nsName,
Labels: map[string]string{
"tenant": tenant,
"env": env,
"paas.io/managed": "true",
},
},
}
if _, err := cs.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("create namespace %s: %w", nsName, err)
}
return nil
}
if ns.Labels == nil {
ns.Labels = map[string]string{}
}
ns.Labels["tenant"] = tenant
ns.Labels["env"] = env
_, err = cs.CoreV1().Namespaces().Update(ctx, ns, metav1.UpdateOptions{})
return err
}
// ReconcileResourceQuota 将全局配额下发给租户 Namespace 的 K8s ResourceQuota,
// 避免"后台已满、集群仍可调度"的配额漂移。
func (c *Client) ReconcileResourceQuota(ctx context.Context, q model.GlobalQuota) error {
nsName := fmt.Sprintf("%s-%s", q.Tenant, q.Env)
rqName := fmt.Sprintf("quota-%s-%s", q.Tenant, q.Env)
hard := corev1.ResourceList{
corev1.ResourceCPU: *resource.NewMilliQuantity(q.CPUQuota, resource.DecimalSI),
corev1.ResourceMemory: *resource.NewQuantity(q.MemQuota*1024*1024, resource.BinarySI),
corev1.ResourceStorage: *resource.NewQuantity(q.StorageQuota*1024*1024*1024, resource.BinarySI),
}
rq := &corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{
Name: rqName,
Namespace: nsName,
Labels: map[string]string{
"tenant": q.Tenant,
"env": q.Env,
"paas.io/managed": "true",
},
},
Spec: corev1.ResourceQuotaSpec{Hard: hard},
}
cs := c.Clientset
if _, err := cs.CoreV1().ResourceQuotas(nsName).Get(ctx, rqName, metav1.GetOptions{}); err != nil {
if _, err := cs.CoreV1().ResourceQuotas(nsName).Create(ctx, rq, metav1.CreateOptions{}); err != nil {
return fmt.Errorf("create resourcequota %s: %w", rqName, err)
}
return nil
}
if _, err := cs.CoreV1().ResourceQuotas(nsName).Update(ctx, rq, metav1.UpdateOptions{}); err != nil {
return fmt.Errorf("update resourcequota %s: %w", rqName, err)
}
return nil
}
2. 与联邦 Prometheus 监控、Grafana、Alertmanager 联动
后台自身的健康指标(组件存活、网关连接数、升级任务耗时、配额变更次数)与所有操作事件指标,都带 tenant/app/env/cluster 标签进入联邦:各集群部署 Prometheus Agent 采集本地,汇总至中心 Prometheus + Thanos 长期存储。后台组件异常会通过 Alertmanager 分级告警。
graph LR A[各集群 Prometheus Agent] -->|federation 拉取| B[中心 Prometheus] B --> C[Thanos Query + Object Storage] C --> D[Grafana 后台专属大盘] B --> E[Alertmanager 分级路由] E --> F[平台管理员 告警 按 env/severity] H[后台管理服务 指标] --> A
这张联邦架构图说明:后台指标与业务指标共用同一套联邦管道,按 cluster/env 标签隔离,管理员可在 Grafana 查看"后台专属大盘",异常经 Alertmanager 按环境 / 严重度路由。
3. 与 RBAC / 审批 / 审计联动
- RBAC:后台操作按"平台管理员 / 只读审计员 / 配额审批人"等角色细分,所有接口级权限经 RBAC 校验。
- 审批:配额变更、组件升级、特性开关批量生效等敏感操作必须走多级审批流,审批通过才放行网关调用。
- 审计:每一次后台读写都落审计日志,带操作人、租户、资源、前后值、网关集群标签,留存 90 天以上且只读不可篡改。
下面的
middleware.go把 RBAC 与审计落库落到代码层:接口级角色校验 + 每次敏感写操作写入audit_logs。
// file: internal/admin/middleware.go
package admin
import (
"context"
"crypto/rand"
"encoding/hex"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/example/paas/admin/internal/model"
)
// RequireRole 后台 RBAC:仅允许持对应角色的管理员执行敏感操作。
// 角色由接入层 SSO/OAuth2 注入到 X-Admin-Role 头,绝不下放集群凭据。
func RequireRole(roles ...string) gin.HandlerFunc {
allowed := make(map[string]bool)
for _, r := range roles {
allowed[r] = true
}
return func(c *gin.Context) {
role := c.GetHeader("X-Admin-Role")
actor := c.GetHeader("X-Admin-User")
if role == "" || !allowed[role] {
c.AbortWithStatusJSON(403, gin.H{"error": "forbidden: insufficient platform role"})
return
}
c.Set("actor", actor)
c.Set("role", role)
c.Next()
}
}
// AuditContext 审计落库上下文。
type AuditContext struct {
Ctx context.Context
Actor string
Role string
Tenant string
App string
Env string
Cluster string
Resource string
Action string
Before string
After string
ApprovalID string
Result string
ClientIP string
}
// writeAudit 写入一条审计记录(90天+ 不可篡改,配合 Thanos 只读层)。
func (s *Server) writeAudit(a AuditContext) {
_ = s.db.WithContext(a.Ctx).Create(&model.AuditLog{
TraceID: newTraceID(),
Actor: a.Actor,
Role: a.Role,
Tenant: a.Tenant,
App: a.App,
Env: a.Env,
Cluster: a.Cluster,
Resource: a.Resource,
Action: a.Action,
BeforeValue: a.Before,
AfterValue: a.After,
ApprovalID: a.ApprovalID,
Result: a.Result,
ClientIP: a.ClientIP,
CreatedAt: time.Now(),
}).Error
}
func newTraceID() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
func strPtr(v interface{}) string {
if v == nil {
return ""
}
if s, ok := v.(string); ok {
return s
}
return ""
}
// ensure gorm 被引用(Server.db 使用),避免未使用导入告警
var _ = gorm.ErrRecordNotFound
四、端到端标准操作流程
以下流程区分【测试环境】与【生产环境】差异步骤。以"平台组件升级 + 配额调整"为例,分角色说明。
角色:平台管理员(发起)、配额审批人(审批)、SRE(执行升级)、审计员(核查)。
步骤(测试环境)
# 步骤 1:管理员登录后台,确认目标组件在测试集群状态正常
kubectl --context=paas-gateway get deploy platform-admin -n paas-system
# 步骤 2:在后台提交"配额调整 + 组件升级"草稿,标记 env=测试
curl -X PUT "https://admin.paas.internal/api/v1/quotas/acme/test" \
-H "X-Admin-Role: platform-admin" -H "X-Admin-User: alice" \
-H "X-Approval-Id: TEST-APR-0001" \
-d '{"cluster":"test-cluster","cpu_quota":120000,"mem_quota":262144,"storage_quota":500,"middleware_instances":8}'
# 步骤 3:测试环境审批可自动通过(策略配置),网关下发变更
# 步骤 4:观察联邦 Grafana 后台大盘,确认组件版本与配额生效
步骤(生产环境)
# 步骤 1:管理员登录后台提交变更,env=生产,触发多级审批
curl -X PUT "https://admin.paas.internal/api/v1/quotas/acme/prod" \
-H "X-Admin-Role: platform-admin" -H "X-Admin-User: alice" \
-H "X-Approval-Id: PROD-APR-8842" \
-d '{"cluster":"prod-cluster","cpu_quota":120000,"mem_quota":262144,"storage_quota":500,"middleware_instances":8}'
# 步骤 2:配额审批人 + 变更委员会两级审批,审批事件进审计
# 步骤 3:审批通过后网关下发;升级先对生产集群做 pre-flight 检查
# 步骤 4:滚动升级,分批灰度;每批经联邦健康检查,异常自动暂停
# 步骤 5:升级完成后审计员核验审计视图,确认 90 天可查
操作时序图
sequenceDiagram participant A as 平台管理员 participant B as 后台管理服务 participant C as 审批工作流引擎 participant D as 统一 K8s API 网关 participant E as 联邦 Prometheus A->>B: 提交配额调整+组件升级(env=生产) B->>C: 触发多级审批 C->>A: 审批人逐级审核 C->>B: 审批通过 B->>D: 经网关下发变更到目标集群 D->>E: 上报变更指标 tenant/env/cluster E->>B: 健康检查结果回传 B->>A: 升级完成 审计记录已落库
这张时序图覆盖了"提交→审批→网关下发→联邦回传→审计落库"的完整链路,是后台标准操作的培训蓝本。
4.1 部署与备份 IaC
# file: deploy/admin-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: platform-admin
namespace: paas-system
labels:
app: platform-admin
tenant: platform
env: prod
cluster: prod-cluster
paas.io/managed: "true"
spec:
replicas: 2
selector:
matchLabels:
app: platform-admin
template:
metadata:
labels:
app: platform-admin
tenant: platform
env: prod
cluster: prod-cluster
spec:
serviceAccountName: platform-admin
containers:
- name: admin
image: registry.paas.internal/platform-admin:v1.8.2
ports:
- containerPort: 8080
name: http
- containerPort: 9090
name: metrics
envFrom:
- configMapRef:
name: platform-admin-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: platform-admin-secret
key: db-password
- name: GATEWAY_TOKEN
valueFrom:
secretKeyRef:
name: platform-admin-secret
key: gateway-token
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
livenessProbe:
httpGet:
path: /livez
port: 8080
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
# file: deploy/db-backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: platform-admin-db-backup
namespace: paas-system
labels:
app: platform-admin
paas.io/managed: "true"
spec:
schedule: "0 2 * * *" # 每日 02:00 全量备份
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 7
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
serviceAccountName: platform-admin
restartPolicy: Never
containers:
- name: backup
image: registry.paas.internal/pg-backup:1.4.0
env:
- name: PGHOST
valueFrom:
configMapKeyRef:
name: platform-admin-config
key: DB_HOST
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: platform-admin-secret
key: db-password
- name: BACKUP_BUCKET
valueFrom:
configMapKeyRef:
name: platform-admin-config
key: BACKUP_BUCKET
command:
- /bin/sh
- -c
- |
ts=$(date +%Y%m%d-%H%M%S)
pg_dump -U paas_admin paas_admin | gzip > /tmp/meta-$ts.sql.gz
# 异地对象存储 + KMS 服务端加密上传
rclone copy /tmp/meta-$ts.sql.gz ${BACKUP_BUCKET}/ --s3-server-side-encryption aws:kms
echo "backup $ts done"
4.2 存储层与 gin 管理 API(Go 完整实现)
store/postgres.go 用 GORM 连接 Postgres 并自动迁移三张表(生产由独立迁移任务执行,此处仅兜底):
// file: internal/store/postgres.go
package store
import (
"fmt"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/example/paas/admin/internal/model"
)
// Config 数据库连接配置,从 ConfigMap / 环境变量注入。
type Config struct {
Host string
Port int
User string
Password string
DBName string
SSLMode string
}
func NewPostgres(cfg Config) (*gorm.DB, error) {
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode,
)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
})
if err != nil {
return nil, fmt.Errorf("open postgres: %w", err)
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(50)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(30 * time.Minute)
// 生产环境由独立迁移任务执行;此处兜底确保本地/测试可用
if err := db.AutoMigrate(&model.AuditLog{}, &model.GlobalQuota{}, &model.FeatureFlag{}); err != nil {
return nil, fmt.Errorf("migrate: %w", err)
}
return db, nil
}
metrics.go 定义带 tenant/app/env/cluster 标签的 Prometheus 指标与采集中间件:
// file: internal/admin/metrics.go
package admin
import (
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
apiRequests = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "paas_admin_api_requests_total",
Help: "后台管理 API 调用量",
}, []string{"tenant", "app", "env", "cluster", "action", "result"})
apiLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "paas_admin_api_request_duration_seconds",
Help: "后台管理 API 耗时",
Buckets: prometheus.DefBuckets,
}, []string{"tenant", "app", "env", "cluster", "action"})
backupRuns = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "paas_admin_backup_runs_total",
Help: "DB 备份触发次数",
}, []string{"tenant", "env", "cluster", "result"})
)
// MetricsMiddleware 记录后台管理 API 调用量(带 tenant/app/env/cluster 标签)。
func MetricsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
tenant := pick(c.Query("tenant"), c.Param("tenant"), "platform")
env := pick(c.Query("env"), "prod")
cluster := pick(c.Query("cluster"), "prod-cluster")
result := "success"
if c.Writer.Status() >= 400 {
result = "fail"
}
apiRequests.WithLabelValues(tenant, "platform-admin", env, cluster, c.Request.Method, result).Inc()
}
}
func pick(vals ...string) string {
for _, v := range vals {
if v != "" {
return v
}
}
return ""
}
handler.go 实现审计日志查询、配额查询 / 更新(更新后经网关 reconcile,消除漂移并落审计):
// file: internal/admin/handler.go
package admin
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/example/paas/admin/internal/k8s"
"github.com/example/paas/admin/internal/model"
)
// Server 后台管理服务核心,持有 DB 与按集群缓存的网关客户端。
type Server struct {
db *gorm.DB
gatewayAddr string
gatewayToken string
clients map[string]*k8s.Client
}
func NewServer(db *gorm.DB, gatewayAddr, gatewayToken string) *Server {
return &Server{
db: db,
gatewayAddr: gatewayAddr,
gatewayToken: gatewayToken,
clients: make(map[string]*k8s.Client),
}
}
// k8sFor 按 cluster/env 返回经统一网关的客户端(带缓存,避免重复 Dial)。
func (s *Server) k8sFor(cluster, env string) (*k8s.Client, error) {
key := cluster + "/" + env
if c, ok := s.clients[key]; ok {
return c, nil
}
c, err := k8s.Dial(s.gatewayAddr, s.gatewayToken, cluster, env)
if err != nil {
return nil, err
}
s.clients[key] = c
return c, nil
}
// QueryAuditLogs GET /api/v1/audit/logs 全局审计视图检索(≥90天)。
func (s *Server) QueryAuditLogs(c *gin.Context) {
var logs []model.AuditLog
q := s.db.WithContext(c.Request.Context()).Order("created_at DESC")
if v := c.Query("tenant"); v != "" {
q = q.Where("tenant = ?", v)
}
if v := c.Query("env"); v != "" {
q = q.Where("env = ?", v)
}
if v := c.Query("cluster"); v != "" {
q = q.Where("cluster = ?", v)
}
if v := c.Query("actor"); v != "" {
q = q.Where("actor = ?", v)
}
if v := c.Query("action"); v != "" {
q = q.Where("action = ?", v)
}
if err := q.Limit(200).Find(&logs).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"total": len(logs), "items": logs})
}
// GetQuota GET /api/v1/quotas/:tenant/:env 查询元数据中心全局配额。
func (s *Server) GetQuota(c *gin.Context) {
tenant := c.Param("tenant")
env := c.Param("env")
var q model.GlobalQuota
if err := s.db.WithContext(c.Request.Context()).
Where("tenant = ? AND env = ?", tenant, env).First(&q).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "quota not found"})
return
}
c.JSON(http.StatusOK, q)
}
// UpdateQuota PUT /api/v1/quotas/:tenant/:env
// 更新元数据中心全局配额,并经统一 K8s API 网关重下发 ResourceQuota 消除漂移。
func (s *Server) UpdateQuota(c *gin.Context) {
tenant := c.Param("tenant")
env := c.Param("env")
var req struct {
Cluster string `json:"cluster"`
CPUQuota int64 `json:"cpu_quota"`
MemQuota int64 `json:"mem_quota"`
StorageQuota int64 `json:"storage_quota"`
MiddlewareInstances int `json:"middleware_instances"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
var existing model.GlobalQuota
if err := s.db.WithContext(ctx).Where("tenant = ? AND env = ?", tenant, env).First(&existing).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "quota not found"})
return
}
before, _ := json.Marshal(existing)
existing.CPUQuota = req.CPUQuota
existing.MemQuota = req.MemQuota
existing.StorageQuota = req.StorageQuota
existing.MiddlewareInstances = req.MiddlewareInstances
if err := s.db.WithContext(ctx).Save(&existing).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
after, _ := json.Marshal(existing)
// 经网关重下发 ResourceQuota 与 Namespace(消除"后台已满、集群可调度"漂移)
client, err := s.k8sFor(req.Cluster, env)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "dial gateway: " + err.Error()})
return
}
if err := client.ReconcileNamespace(ctx, tenant, env); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "reconcile ns: " + err.Error()})
return
}
if err := client.ReconcileResourceQuota(ctx, existing); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "reconcile rq: " + err.Error()})
return
}
s.writeAudit(AuditContext{
Ctx: ctx,
Actor: strPtr(c.Get("actor")),
Role: strPtr(c.Get("role")),
Tenant: tenant,
App: "platform-admin",
Env: env,
Cluster: req.Cluster,
Resource: "global_quota",
Action: "update",
Before: string(before),
After: string(after),
ApprovalID: c.GetHeader("X-Approval-Id"),
Result: "success",
ClientIP: c.ClientIP(),
})
c.JSON(http.StatusOK, existing)
}
backup.go 演示"Go 如何经网关触发备份 Job"——这是「★禁止删减(备份)」的代码落地:
// file: internal/admin/backup.go
package admin
import (
"fmt"
"time"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/gin-gonic/gin"
)
func int32Ptr(i int32) *int32 { return &i }
// TriggerBackup POST /api/v1/backup/trigger
// 经统一 K8s API 网关在目标集群 paas-system 创建一次性备份 Job。
func (s *Server) TriggerBackup(c *gin.Context) {
var req struct {
Tenant string `json:"tenant"`
Env string `json:"env"`
Cluster string `json:"cluster"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
client, err := s.k8sFor(req.Cluster, req.Env)
if err != nil {
backupRuns.WithLabelValues(req.Tenant, req.Env, req.Cluster, "fail").Inc()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
jobName := fmt.Sprintf("db-backup-%s-%s-%d", req.Tenant, req.Env, time.Now().Unix())
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: jobName,
Namespace: "paas-system",
Labels: map[string]string{"tenant": req.Tenant, "env": req.Env, "paas.io/managed": "true"},
},
Spec: batchv1.JobSpec{
BackoffLimit: int32Ptr(2),
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
Containers: []corev1.Container{{
Name: "backup",
Image: "registry.paas.internal/pg-backup:1.4.0",
Env: []corev1.EnvVar{
{Name: "PGHOST", Value: "pg-admin.paas-system.svc.cluster.local"},
{Name: "PGPASSWORD", ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: "platform-admin-secret"},
Key: "db-password",
},
}},
{Name: "BACKUP_BUCKET", Value: "s3://paas-backup-prod/metadata"},
},
Command: []string{"/bin/sh", "-c",
"ts=$(date +%Y%m%d-%H%M%S); pg_dump -U paas_admin paas_admin | gzip > /tmp/meta-$ts.sql.gz; " +
"rclone copy /tmp/meta-$ts.sql.gz ${BACKUP_BUCKET}/ --s3-server-side-encryption aws:kms; echo backup $ts done"},
}},
},
},
},
}
if _, err := client.Clientset.BatchV1().Jobs("paas-system").Create(c.Request.Context(), job, metav1.CreateOptions{}); err != nil {
backupRuns.WithLabelValues(req.Tenant, req.Env, req.Cluster, "fail").Inc()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
backupRuns.WithLabelValues(req.Tenant, req.Env, req.Cluster, "success").Inc()
c.JSON(http.StatusAccepted, gin.H{"job": jobName, "status": "triggered"})
}
4.3 进程入口 main.go
cmd/admin/main.go 把 DB、网关客户端、gin 路由与 Prometheus 指标装配为可运行服务。注意路由级 RBAC(RequireRole)与指标中间件(MetricsMiddleware)的装配。
// file: cmd/admin/main.go
package main
import (
"fmt"
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/example/paas/admin/internal/admin"
"github.com/example/paas/admin/internal/store"
)
func atoiDefault(s string, def int) int {
if s == "" {
return def
}
var n int
if _, err := fmt.Sscanf(s, "%d", &n); err != nil {
return def
}
return n
}
func main() {
dbCfg := store.Config{
Host: os.Getenv("DB_HOST"),
Port: atoiDefault(os.Getenv("DB_PORT"), 5432),
User: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASSWORD"),
DBName: os.Getenv("DB_NAME"),
SSLMode: os.Getenv("DB_SSLMODE"),
}
db, err := store.NewPostgres(dbCfg)
if err != nil {
log.Fatalf("init postgres: %v", err)
}
srv := admin.NewServer(db, os.Getenv("GATEWAY_ADDR"), os.Getenv("GATEWAY_TOKEN"))
r := gin.New()
r.Use(gin.Recovery())
r.Use(admin.MetricsMiddleware())
r.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) })
r.GET("/livez", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) })
api := r.Group("/api/v1")
api.Use(admin.RequireRole("platform-admin", "quota-approver", "audit-viewer"))
{
api.GET("/audit/logs", srv.QueryAuditLogs)
api.GET("/quotas/:tenant/:env", srv.GetQuota)
api.PUT("/quotas/:tenant/:env", admin.RequireRole("platform-admin", "quota-approver"), srv.UpdateQuota)
api.POST("/backup/trigger", admin.RequireRole("platform-admin"), srv.TriggerBackup)
}
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
addr = ":8080"
}
log.Printf("platform-admin listening on %s", addr)
if err := r.Run(addr); err != nil {
log.Fatal(err)
}
}
4.4 运维 Runbook(真实命令)
# ── 备份 / 恢复元数据中心 DB ──────────────────────────────
# 触发一次手动备份(经后台 API,走统一网关)
curl -X POST "https://admin.paas.internal/api/v1/backup/trigger" \
-H "X-Admin-Role: platform-admin" -H "X-Admin-User: sre" \
-d '{"tenant":"acme","env":"prod","cluster":"prod-cluster"}'
# 从异地对象存储恢复到新库(灾难演练)
kubectl --context=paas-gateway -n paas-system exec -it pg-admin-0 -- bash
# 容器内:rclone copy s3://paas-backup-prod/metadata/meta-20251015-020000.sql.gz /tmp/ && \
# gunzip -c /tmp/meta-*.sql.gz | psql -U paas_admin paas_admin_restore
# ── 配额调整并核验审计 ────────────────────────────────────
curl -X PUT "https://admin.paas.internal/api/v1/quotas/acme/prod" \
-H "X-Admin-Role: platform-admin" -H "X-Approval-Id: PROD-APR-8842" \
-d '{"cluster":"prod-cluster","cpu_quota":120000,"mem_quota":262144,"storage_quota":500,"middleware_instances":8}'
# 在联邦 Grafana 后台大盘确认指标带 tenant/env/cluster 标签
# 检索审计是否可查(≥90天)
curl "https://admin.paas.internal/api/v1/audit/logs?tenant=acme&action=update&env=prod" \
-H "X-Admin-Role: audit-viewer"
# ── 网关连接态巡检 ────────────────────────────────────────
kubectl --context=paas-gateway -n paas-system get deploy platform-admin -o wide
kubectl --context=paas-gateway -n paas-system logs deploy/platform-admin -l app=platform-admin --tail=100
五、生产环境管控与安全约束
核心约束
- 权限隔离:后台采用平台级 RBAC,生产操作仅限持对应角色令牌的管理员;租户 Namespace 强隔离,后台不能越权读取租户业务数据。
- 敏感操作审批:所有生产变更(配额、升级、特性开关、备份恢复策略)强制多级审批,不可绕过。
- 审计规则:审计日志留存 90 天以上,写入 Thanos 长期存储只读层;任何删除审计的行为被实时告警。
- 故障熔断:网关连接异常或联邦采集失败时,后台进入"只读降级"模式,禁止任何写操作下发,避免脑裂。
Prometheus 观测配置
后台组件健康指标、API 调用量、备份次数都带 tenant/app/env/cluster 标签。下面 ServiceMonitor 被联邦 Prometheus 抓取,PrometheusRule 在错误率过高 / 网关失联时分级告警(告警经 Alertmanager 路由到 on-call)。
# file: deploy/admin-prometheus.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: platform-admin
namespace: paas-system
labels:
app: platform-admin
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app: platform-admin
endpoints:
- port: metrics
interval: 15s
path: /metrics
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: platform-admin-rules
namespace: paas-system
labels:
app: platform-admin
release: kube-prometheus-stack
spec:
groups:
- name: platform-admin
rules:
- alert: AdminAPIHighErrorRate
expr: |
sum(rate(paas_admin_api_requests_total{result="fail"}[5m])) by (tenant,env,cluster)
/
sum(rate(paas_admin_api_requests_total[5m])) by (tenant,env,cluster) > 0.05
for: 10m
labels:
severity: warning
env: "{{ $labels.env }}"
annotations:
summary: "后台API错误率超5% (tenant={{ $labels.tenant }} cluster={{ $labels.cluster }})"
- alert: AdminBackupFailed
expr: |
increase(paas_admin_backup_runs_total{result="fail"}[1h]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "元数据中心备份失败,容灾 RPO 受影响"
- alert: AdminGatewayUnreachable
expr: |
probe_success{job="platform-admin-gateway-health"} == 0
for: 5m
labels:
severity: critical
annotations:
summary: "统一K8s API网关连接失败,后台应进入只读降级"
测试环境 vs 生产环境 差异化管控对照表
| 管控项 | 测试环境 | 生产环境 |
|---|---|---|
| 审批流 | 单级 / 可自动通过 | 多级审批 + 变更委员会,不可绕过 |
| 配额调整 | 即时生效,可超配 | 需审批,受全局 License 上限约束 |
| 组件升级 | 直接滚动,窗口宽松 | pre-flight 检查 + 分批灰度 + 暂停阈值 |
| 备份策略 | 每日快照,可丢弃 | 异地对象存储 + 加密 + 定期演练恢复 |
| 审计留存 | 本地 30 天 | Thanos 只读层 90 天以上,不可篡改 |
| 网关令牌 | 长有效期开发令牌 | 短有效期 + 审计绑定 + 设备绑定 |
| 故障熔断 | 允许写,提示即可 | 只读降级,禁止下发写操作 |
| 监控告警 | 弱告警,仅通知 | 分级路由,P1 直呼 on-call |
⚠️ 注意:生产环境"只读降级"是保命开关。曾经有案例因网关短暂抖动时后台仍允许并发下发,导致两个集群配额同时超限、调度锁死。必须确认熔断优先于可用性。
六、常见生产故障与解决方案
结合多集群与联邦监控场景,列举高频问题。
| 故障 | 现象 | 根因 | 解决 |
|---|---|---|---|
| 网关连接抖动 | 后台组件状态闪烁、部分集群失联 | 网关反向隧道心跳超时 / 网络分区 | 网关自动摘除故障集群,后台只读降级,恢复后重连 |
| 联邦采集缺口 | Grafana 后台大盘某集群断点 | Agent 从目标集群采集失败 / 网关限流 | 检查 Agent Pod 与网关链路,补 Thanos 历史补齐 |
| 审计缺失 | 操作无记录或 90 天后不可查 | 审计写入 Thanos 失败 / 留存策略错配 | 校验 Thanos Object Storage 写权限与 retention 规则 |
| 配额漂移 | 后台已满、集群仍可调度 | 全局配额与 ResourceQuota 不一致 | 后台触发 reconcile,经网关重新下发 ResourceQuota |
| 升级卡住 | 滚动升级某批不就绪 | 新版本依赖未满足 / 探针失败 | 联邦健康检查暂停批次,一键回滚至上一稳定版本 |
配额漂移自愈(代码级 reconcile 入口)
当"后台已满、集群仍可调度"时,直接调用 Server.UpdateQuota 即可触发 ReconcileResourceQuota 重新下发。下面给出一行式运维入口,等价于后台"触发 reconcile"按钮:
# 重新下发 acme/prod 的 ResourceQuota(消除漂移)
curl -X PUT "https://admin.paas.internal/api/v1/quotas/acme/prod" \
-H "X-Admin-Role: platform-admin" -H "X-Approval-Id: PROD-APR-8842" \
-d '{"cluster":"prod-cluster","cpu_quota":120000,"mem_quota":262144,"storage_quota":500,"middleware_instances":8}'
故障排查流程图
flowchart TD
S[后台告警 组件异常/审计缺失] --> Q1{是否网关失联?}
Q1 -->|是| R1[网关只读降级 摘除故障集群]
Q1 -->|否| Q2{是否联邦采集缺口?}
Q2 -->|是| R2[检查 Agent 与网关链路 补 Thanos 历史]
Q2 -->|否| Q3{是否审计写入失败?}
Q3 -->|是| R3[校验 Thanos 写权限与 retention]
Q3 -->|否| R4[查配额漂移 触发 reconcile 重下发]
R1 --> E[恢复后重连 复核审计]
R2 --> E
R3 --> E
R4 --> E这张流程图给出"网关→联邦→审计→配额"的逐层排查路径,对应上方高频故障表,可直接用于 on-call 手册。
自测题与动手练习
5 道自测
- 平台系统后台管理模块为什么必须经由统一 K8s API 网关访问各集群,而不直连 apiserver?
- 全局配额与 K8s ResourceQuota 的关系是什么?出现"后台已满、集群可调度"漂移应如何 reconcile?
- 模块 09 的「★禁止删减」三项是什么?各自对应哪种商用合规要求?
- 生产环境网关连接抖动时,后台为何要进入"只读降级"而非继续允许写操作?
- 后台操作审计日志需要满足哪些留存与防篡改要求,技术上如何落地?
3 个动手
- 在测试环境通过后台将某租户全局配额上调,提交后到联邦 Grafana 后台大盘确认指标带
tenant/env/cluster标签。 - 发起一次平台组件升级,故意让审批流停留在"配额审批人"节点,验证无审批不向下游网关下发。
- 模拟审计写入失败(断开 Thanos 写权限),观察 Alertmanager 是否分级告警,并确认后台进入只读降级。
本章小结
- 平台系统后台管理模块是"管理平面的管理平面",统一管控元数据中心、网关与组件状态、License 与配额、特性开关、备份容灾、升级回滚与全局审计。
- 所有跨集群动作都经统一 K8s API 网关单入口收敛(
GatewayRoundTripper注入网关令牌 + 集群标签),kubeconfig 网关侧解密,杜绝凭据散落;指标与操作事件带标签进入 Prometheus 联邦 + Thanos。 - 「★禁止删减」三项——审计(
audit_logs表 + Thanos 90 天不可篡改)、备份(每日异地加密 CronJob + 手动触发 Job)、权限(RBACRequireRole+ 多级审批)——是金融 / 政企商用的合规底线。 - 测试与生产在审批、升级、备份、审计、熔断上差异显著,生产以"只读降级保命"优先。
下一篇我们将进入开放 API 与第三方集成模块,看平台如何把上述能力以受控、可审计的方式开放给外部系统。