学习目标
学完本模块,你应该能够:
- 在平台申请并创建一个生产级 MySQL / Redis / Kafka / Elasticsearch 托管实例,理解其运行在租户 Namespace 下的隔离边界。
- 解释中间件实例的高可用形态(主从 / 集群 / 多副本)及故障切换原理。
- 配置自动备份策略,并演示一次基于 PITR(时间点恢复)的故障恢复。
- 区分透明加密、审计、网络隔离三种安全能力,说明它们如何满足合规。
- 结合联邦监控识别慢查询、热 Key、消费堆积等中间件典型异常,并触发分级告警。
前置知识
- 了解至少一种中间件(MySQL 主从复制、Redis 主从 / 集群、Kafka 分区与副本、ES 分片)的基本工作原理。
- 已阅读本白皮书「模块 02 多 K8s 集群纳管」「模块 03 应用全生命周期管理」,理解 API 网关、Namespace 隔离与联邦监控。
- 了解 KMS 信封加密与备份存储(对象存储)基本概念。
本章你会动手做的事
- 在测试租户 Namespace 下申请一个 1 主 2 从的 MySQL 实例,观察 Pod 与 Service 落地。
- 配置每日全量 + 增量备份,并手动触发一次 PITR 恢复到指定时间点。
- 用 redis-benchmark 制造热 Key,验证联邦大盘出现热 Key 指标并触发 Alertmanager 告警。
项目结构与文件清单
本模块提供「平台侧 Go 控制面(调 operator/helm SDK)+ 目标集群 K8s IaC + 联邦观测配置」三件套:
paas-middleware/ # 中间件托管平台源码(Go + gin + client-go)
├── go.mod
├── main.go # gin 入口 + /metrics(暴露 paas_middleware_* 指标)
├── pkg/
│ ├── gateway/
│ │ └── client.go # 统一 K8s API 网关多集群客户端
│ ├── middleware/
│ │ ├── instance.go # 经 dynamic client 创建 MiddlewareInstance(调 operator)
│ │ ├── backup.go # 触发备份 Job(PITR 基点)
│ │ └── status.go # 读取实例状态
│ ├── metrics/
│ │ └── metrics.go # prometheus 业务指标
│ └── handler/
│ └── middleware.go # gin 创建/备份/状态 Handler
├── manifests/ # K8s IaC(kubectl apply 即用)
│ ├── mysql-instance.yaml # 平台下发的中介件 CRD(MiddlewareInstance)
│ ├── mysql-statefulset.yaml # MySQL StatefulSet + PVC + Service(group replication)
│ ├── redis-statefulset.yaml # Redis Cluster StatefulSet + PVC + Service
│ ├── mysql-backup-cronjob.yaml # 每日全量 + 增量备份 CronJob
│ ├── middleware-networkpolicy.yaml # NetworkPolicy 严格隔离
│ └── rbac-prod.yaml # 生产实例 RBAC 约束
└── observability/ # Prometheus / 联邦观测配置
├── mysql-servicemonitor.yaml # MySQL/Redis ServiceMonitor
├── prometheus-mw-relabel.yaml # 联邦抓取 + tenant/app/env/cluster 注入
├── mysql-recording-rules.yaml # 慢查询/命中率/堆积 recording rules
└── mysql-alert-rules.yaml # 主库宕机/慢查询/热Key/堆积/备份失败告警
下面各章逐文件完整展示,且所有文件相互自洽:Go 创建的 MiddlewareInstance 字段 = manifests/mysql-instance.yaml,Go 暴露的 paas_middleware_* / middleware_backup_* 指标 = 观测配置中的 recording/alert 输入。
一、模块概述与企业商用价值
中间件托管平台模块(Managed Middleware Platform)把企业最常用的有状态组件——MySQL、Redis、Kafka、Elasticsearch、对象存储等——从"各团队自己裸装"升级为"平台统一托管、按需申请、自动护航"。它解决的核心矛盾是:有状态组件最怕数据丢、最怕被邻居挤爆、最怕运维口径不一;而业务团队往往既不懂调优,也不愿承担 7×24 值守。
类比:这套模块像写字楼的"中央机房 + 专业电工班"。每个租户(公司)在自家楼层(Namespace)里租用一个带锁的机柜(实例),物业(平台)负责通电(高可用)、定期体检(备份)、配独立电表(指标标签)、上锁防窥(加密与隔离)。租户只管用,不用自己当运维。
适用角色与业务痛点
| 角色 | 痛点 | 本模块价值 |
|---|---|---|
| 业务研发 | 自建中间件运维成本高、易出错 | 按需申请,平台托管运维 |
| DBA / 中间件运维 | 实例散落、备份靠人肉 | 统一生命周期 + 自动备份 |
| 平台管理员 | 资源抢占、合规难证明 | 资源隔离 + 加密 + 审计留痕 |
合规与不可替代性
金融 / 政企要求:数据有备份且可恢复(容灾合规)、敏感数据静态加密(数据合规)、实例间网络隔离(等保合规)、操作可审计(监管留存)。本模块把以上固化为能力,而非依赖个人自觉。
下面这张图给出本模块在整体 PaaS 中的定位。
graph TB
subgraph 使用方
Biz[业务应用/研发]
end
subgraph PaaS平台
MW[中间件托管平台]
Ops[生命周期控制器]
BK[(备份与PITR)]
ENC[透明加密 KMS]
SEC[网络隔离/审计]
end
subgraph 基础设施
GW[统一 K8s API 网关]
NS[(租户 Namespace
MySQL/Redis/Kafka/ES Pod)]
end
Biz --> MW
MW --> Ops
MW --> BK
MW --> ENC
MW --> SEC
Ops --> GW
GW --> NS这张图说明:业务只面向托管平台申请实例,平台经控制器与 API 网关把有状态 Pod 落到租户 Namespace,并叠加备份、加密、隔离三种护航能力。
二、细分功能详解(商用生产级)
下面区分【基础能力】与【高级企业增值能力】,带 ★ 为禁止删减底线能力。
功能分级总览
| 级别 | 功能项 | 商用说明 |
|---|---|---|
| 【基础能力】 | ① 托管类型 | MySQL / Redis / Kafka / ES / 对象存储 |
| 【基础能力】 | ② 实例生命周期 | 申请 / 创建 / 扩容 / 缩容 / 销毁 |
| 【基础能力】 | ③ 高可用 | 主从 / 集群 / 多副本 + 故障切换 |
| 【基础能力】 | ⑤ 参数模板与版本管理 | 统一参数基线 + 大版本升级 |
| 【基础能力】 | ⑥ 容量规划与配额 | 规格档位 + 租户配额 |
| 【高级企业增值能力】 | ④ 自动备份与 PITR | 全量 + 增量 + 时间点恢复 |
| 【高级企业增值能力】 | ⑦ 监控告警 | 慢查询 / 热 Key / 堆积 等专属指标 |
| 【高级企业增值能力】 | ⑧ 安全(加密/审计/隔离) | 透明加密 + 操作审计 + 网络隔离 |
| ★禁止删减 | ★ 备份 | 自动备份 + PITR,数据可恢复底线 |
| ★禁止删减 | ★ 加密 | 静态数据 KMS 信封加密 |
| ★禁止删减 | ★ 资源隔离 | 租户 Namespace + 配额 + 网络策略 |
① 托管类型(基础能力)
平台内置 Operator 支持:MySQL(主从 / Group Replication)、Redis(主从 / Cluster)、Kafka(多 broker + 副本)、Elasticsearch(多节点分片)、对象存储兼容(MinIO / 自建)。统一抽象为 MiddlewareInstance CRD。平台侧 Go(pkg/middleware/instance.go)经 dynamic client 把该 CRD 下发到集群,由集群内 Operator 调谐出 StatefulSet + Service + PVC。
② 实例生命周期(基础能力)
从申请到销毁五态:申请 → 创建(调谐出 StatefulSet + Service + PVC)→ 扩容 / 缩容(调整规格或副本)→ 维护(参数变更)→ 销毁(安全擦除 + PVC 回收 + 配额释放)。
③ 高可用(基础能力)
- MySQL:1 主 2 从,主库宕机由控制器选新主,VIP / Service 不变。
- Redis:Cluster 模式跨节点分片,副本自动顶替。
- Kafka:多分区多副本(replication.factor≥3),Leader 重选举。
- ES:多主多数据节点,分片自动再平衡。
④ 自动备份与 PITR(★禁止删减,高级能力)
每日全量 + 持续增量(binlog / WAL / segment)备份至对象存储;恢复时先恢复全量再重放增量到指定时间戳(Point-In-Time Recovery)。这是数据零丢失的底线能力,不可关闭。
⚠️ 备份存储与目标实例必须跨故障域(不同集群或对象存储桶),否则"集群没了备份也没了"等于没备份。
平台下发的 MiddlewareInstance(manifests/mysql-instance.yaml)即把备份策略固化为期望状态:
# file: manifests/mysql-instance.yaml
apiVersion: paas.example.com/v1
kind: MiddlewareInstance
metadata:
name: mysql-order
namespace: tenant-a-prod # 租户生产 Namespace,强隔离
labels:
app: mysql-order
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
type: mysql
version: "8.0"
topology:
mode: group-replication
replicas: 3 # 1主2从
storage:
class: ssd-encrypted # ★ 加密存储类
size: 200Gi
backup:
full: "0 2 * * *" # 每日全量
incremental: binlog # 增量
pitr: true # ★ 时间点恢复
retention: 30d
⑤ 参数模板与版本管理(基础能力)
内置合规参数基线(如 MySQL innodb_flush_log_at_trx_commit=1、Redis 禁用危险命令),大版本升级走灰度校验。
⑥ 容量规划与配额(基础能力)
实例规格分档(小 / 中 / 大),每租户设存储与连接数配额,防止单实例吃满节点。生产配额约束见第五章 manifests/rbac-prod.yaml 配套 ResourceQuota。
⑦ 监控告警(高级能力)
专属指标:MySQL 慢查询数、连接数;Redis 热 Key / 大 Key / 命中率;Kafka 消费堆积(Lag);ES 分片未分配。经联邦进 Grafana 租户大盘,异常触发 Alertmanager。
⑧ 安全(★禁止删减:加密 / 隔离,高级能力)
- 透明加密:PVC 或上层存储经 KMS 信封加密,静态数据不可读(
storageClassName: ssd-encrypted)。 - 审计:实例的创建、扩缩、参数变更、备份恢复均留痕 ≥ 90 天。
- 网络隔离:NetworkPolicy 限制实例仅能被授权应用访问(见
manifests/middleware-networkpolicy.yaml)。
下面这张图给出托管平台功能组件关系。
graph LR A[托管类型] --> B[实例生命周期] B --> C[高可用主从/集群] C --> D[自动备份与PITR] C --> E[参数模板与版本] B --> F[容量规划与配额] C --> G[监控告警] G --> H[透明加密] G --> I[审计留痕] G --> J[网络隔离] D -.★禁止删减.-> D H -.★禁止删减.-> H J -.★禁止删减.-> J
这张图按"类型 → 生命周期 → 高可用 → 备份/参数/容量/监控/安全"呈现模块功能树,并标出三项禁止删减能力。
三、底层架构联动设计
本模块的有状态实例运行在租户 Namespace,通过统一 API 网关纳管的多集群承载,并把运行指标汇入联邦监控。
1. 与多 K8s 集群的交互链路
中间件控制器(平台侧)监听 MiddlewareInstance 对象,把期望状态(副本数、规格、存储类)经统一 K8s API 网关下发给目标集群,由集群内 Operator 调谐出 StatefulSet / PVC / Service。租户永远只能落在被授权的 Namespace,跨集群数据同步(如容灾副本)也由网关统一编排。
平台侧多集群客户端 pkg/gateway/client.go 与模块 03 同构(统一经 API 网关访问各集群):
// file: pkg/gateway/client.go
package gateway
import (
"context"
"fmt"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// ClusterGateway 统一 K8s API 网关客户端管理器。
type ClusterGateway struct {
clients map[string]kubernetes.Interface
gatewayREST *rest.Config
}
// NewClusterGateway 从网关 kubeconfig 初始化。
func NewClusterGateway(kubeconfigPath string) (*ClusterGateway, error) {
cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
if err != nil {
return nil, fmt.Errorf("build gateway config: %w", err)
}
cfg.Impersonate = rest.ImpersonationConfig{
UserName: "paas-platform",
Groups: []string{"paas:platform"},
}
clientset, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, fmt.Errorf("new gateway clientset: %w", err)
}
return &ClusterGateway{
clients: map[string]kubernetes.Interface{"gateway": clientset},
gatewayREST: cfg,
}, nil
}
// RESTConfig 暴露底层 *rest.Config,供 dynamic client 复用。
func (g *ClusterGateway) RESTConfig() *rest.Config { return g.gatewayREST }
// ClientFor 返回指定集群(经网关纳管)的客户端。
func (g *ClusterGateway) ClientFor(ctx context.Context, clusterID string) (kubernetes.Interface, error) {
if c, ok := g.clients[clusterID]; ok {
return c, nil
}
cs, err := kubernetes.NewForConfig(g.gatewayREST)
if err != nil {
return nil, fmt.Errorf("client for cluster %s: %w", clusterID, err)
}
g.clients[clusterID] = cs
return cs, nil
}
平台侧经 dynamic client 创建 MiddlewareInstance(调 operator 托管中间件) 的核心实现 pkg/middleware/instance.go:
// file: pkg/middleware/instance.go
package middleware
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)
var middlewareGVR = schema.GroupVersionResource{
Group: "paas.example.com", Version: "v1", Resource: "middlewareinstances",
}
// MiddlewareClient 通过动态客户端操作中间件 CRD(由集群内 Operator 调谐)。
type MiddlewareClient struct {
dyn dynamic.Interface
}
func NewMiddlewareClient(dyn dynamic.Interface) *MiddlewareClient {
return &MiddlewareClient{dyn: dyn}
}
// CreateInstance 创建 MiddlewareInstance(client-go 调 operator 托管中间件的核心示例)。
func (m *MiddlewareClient) CreateInstance(ctx context.Context, ns string, inst *unstructured.Unstructured) error {
_, err := m.dyn.Resource(middlewareGVR).Namespace(ns).Create(ctx, inst, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("create middlewareinstance: %w", err)
}
return nil
}
// BuildMySQLInstance 构造与 manifests/mysql-instance.yaml 完全一致的 MiddlewareInstance。
func BuildMySQLInstance(name, ns string, replicas int, size string) *unstructured.Unstructured {
return &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": "paas.example.com/v1",
"kind": "MiddlewareInstance",
"metadata": map[string]interface{}{
"name": name,
"namespace": ns,
"labels": map[string]interface{}{
"app": name,
"tenant": "tenant-a",
"env": "prod",
"cluster": "cluster-prod",
},
},
"spec": map[string]interface{}{
"type": "mysql",
"version": "8.0",
"topology": map[string]interface{}{
"mode": "group-replication",
"replicas": int64(replicas),
},
"storage": map[string]interface{}{
"class": "ssd-encrypted",
"size": size,
},
"backup": map[string]interface{}{
"full": "0 2 * * *",
"incremental": "binlog",
"pitr": true,
"retention": "30d",
},
},
}}
}
2. 与联邦 Prometheus 监控的联动(必绑定)
实例产生的指标——QPS、慢查询、连接数、热 Key、消费 Lag、分片状态——由集群内 Prometheus Agent 采集,并强制注入 tenant/app/env/cluster 标签(其中 app 取中间件类型与实例名),经联邦汇总进中心 Prometheus + Thanos。Grafana 按租户过滤出实例专属大盘;异常(如 Lag 暴涨)触发 Alertmanager 按严重度分级路由。
实例侧 ServiceMonitor(observability/mysql-servicemonitor.yaml)与中心抓取 + relabel(observability/prometheus-mw-relabel.yaml)如下:
# file: observability/mysql-servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: mysql-order
namespace: monitoring
labels:
tenant: tenant-a
env: prod
release: paas
spec:
selector:
matchLabels:
app: mysql-order
namespaceSelector:
matchNames:
- tenant-a-prod
endpoints:
- port: mysql
path: /metrics
interval: 15s
metricRelabelings:
- sourceLabels: [__name__]
regex: "mysql_up|mysql_global_status_slow_queries|mysql_global_status_threads_connected|mysql_global_status_max_connections"
action: keep
# file: observability/prometheus-mw-relabel.yaml
# 中心 Prometheus 抓取配置片段:把 Namespace/Pod 标签注入为 tenant/app/env/cluster 维度
scrape_configs:
- job_name: paas-middleware
kubernetes_sd_configs:
- role: endpoints
namespaces:
names:
- tenant-a-prod
- tenant-a-test
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_tenant]
target_label: tenant
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app
- source_labels: [__meta_kubernetes_pod_label_env]
target_label: env
- source_labels: [__meta_kubernetes_pod_label_cluster]
target_label: cluster
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__address__]
target_label: instance
# 平台控制面暴露的 paas_middleware_* 业务指标(已自带 tenant/app/env/cluster 标签)
- job_name: paas-middleware-platform
static_configs:
- targets: ["paas-middleware.platform.svc:8081"]
relabel_configs:
- source_labels: [__address__]
target_label: instance
下面这张图是中间件与联邦监控的联动拓扑。
graph TB
subgraph 生产集群[生产集群 租户NS]
OP[中间件 Operator]
MY[MySQL Pod]
RD[Redis Pod]
KF[Kafka Pod]
PA[Prometheus Agent]
end
MY -->|指标 tenant/app/env/cluster| PA
RD -->|指标 tenant/app/env/cluster| PA
KF -->|指标 tenant/app/env/cluster| PA
OP --> MY
OP --> RD
OP --> KF
PA -->|联邦拉取| CENTER[中心 Prometheus]
CENTER --> THANOS[Thanos 长期存储]
CENTER --> GRAF[Grafana 租户大盘]
CENTER --> AM[Alertmanager 分级告警]
AM -->|堆积/慢查询| MW[中间件模块自动处置]这张图说明:有状态实例指标带四维标签进联邦,Grafana/Alertmanager 按租户过滤与分级,异常可反哺中间件模块做自动处置(如扩副本、告警收敛)。
3. 与密钥管理、审批流、审计的联动
- 密钥管理:实例账号密码、TLS 证书经 KMS 信封加密托管,注入时不落明文。
- 审批流:生产实例的创建、规格变更、备份恢复、销毁均须走审批引擎多级审批。
- 审计:上述操作全留痕 ≥ 90 天,与模块 01 RBAC、模块 09 全局审计打通。
- 成本计量:实例规格与存储用量驱动模块 08 成本分摊。
四、端到端标准操作流程
以"DBA 申请一个生产 MySQL 主从实例并配置备份"为主线,分角色给出可培训步骤。
角色与职责
- 业务/DBA:提交实例申请、定义规格与备份策略。
- 集群运维:复核目标集群资源、确认高可用拓扑。
- 平台管理员:审批生产实例创建与备份恢复。
操作流程时序
sequenceDiagram participant DBA as DBA/业务 participant MW as 中间件模块 participant Appr as 审批引擎 participant Admin as 平台管理员 participant GW as K8s API网关 participant Op as 集群Operator participant BK as 备份服务 participant Mon as 联邦监控 DBA->>MW: 提交MySQL主从实例申请 MW->>Appr: 发起生产创建审批 Appr->>Admin: 通知待审批 Admin->>Appr: 多级审批通过 Appr->>MW: 放行 MW->>GW: 下发 MiddlewareInstance GW->>Op: 调谐 StatefulSet/PVC Op->>GW: 实例就绪 GW->>MW: 状态回传 MW->>BK: 注册自动备份策略 Op->>Mon: 指标带标签进联邦 Mon->>DBA: 大盘可见实例指标
这段时序展示从申请到实例就绪、备份注册、指标上联邦的完整链路,以及审批闸门的串联。
平台侧 Go 实现(gin + client-go 调用 operator)
pkg/middleware/backup.go 用 client-go 触发备份 Job(PITR 基点):
// file: pkg/middleware/backup.go
package middleware
import (
"context"
"fmt"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// BackupClient 触发一次性备份 Job。
type BackupClient struct {
cs kubernetes.Interface
}
func NewBackupClient(cs kubernetes.Interface) *BackupClient {
return &BackupClient{cs: cs}
}
// TriggerBackup 立即触发一次备份 Job(等效 cron 之外的手动 PITR 基点)。
func (b *BackupClient) TriggerBackup(ctx context.Context, ns, instance, img string) (string, error) {
jobName := fmt.Sprintf("%s-backup-%d", instance, metav1.Now().Unix())
job := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: jobName,
Namespace: ns,
Labels: map[string]string{
"app": instance, "paas.example.com/type": "backup",
},
},
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": instance, "paas.example.com/type": "backup",
},
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyOnFailure,
Containers: []corev1.Container{{
Name: "backup",
Image: img,
Args: []string{"backup", "--instance", instance, "--pitr"},
}},
},
},
},
}
if _, err := b.cs.BatchV1().Jobs(ns).Create(ctx, job, metav1.CreateOptions{}); err != nil {
return "", fmt.Errorf("trigger backup: %w", err)
}
return jobName, nil
}
pkg/middleware/status.go 用 dynamic client 读取实例状态(phase / 就绪副本):
// file: pkg/middleware/status.go
package middleware
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
)
// Status 读取 MiddlewareInstance 的 phase 与就绪副本数(Go 调用 K8s 读取状态)。
func (m *MiddlewareClient) Status(ctx context.Context, ns, name string) (phase string, replicas int64, err error) {
obj, err := m.dyn.Resource(middlewareGVR).Namespace(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return "", 0, fmt.Errorf("get instance: %w", err)
}
status, found, _ := unstructured.NestedMap(obj.Object, "status")
if !found {
return "Unknown", 0, nil
}
phase, _, _ = unstructured.NestedString(status, "phase")
replicas, _, _ = unstructured.NestedInt64(status, "readyReplicas")
return phase, replicas, nil
}
pkg/metrics/metrics.go 定义平台暴露的 paas_middleware_* / middleware_backup_* 指标:
// file: pkg/metrics/metrics.go
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
InstanceProvisionTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "paas_middleware_instance_total",
Help: "Total middleware instances provisioned",
},
[]string{"tenant", "app", "env", "cluster", "type", "result"},
)
InstanceReady = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "paas_middleware_instance_ready",
Help: "1 if instance ready",
},
[]string{"tenant", "app", "env", "cluster", "type"},
)
BackupLastSuccess = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "middleware_backup_last_success_timestamp",
Help: "Timestamp of last successful backup",
},
[]string{"tenant", "app", "env", "cluster"},
)
BackupDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "middleware_backup_duration_seconds",
Help: "Backup duration seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"tenant", "app", "env", "cluster"},
)
)
func init() {
prometheus.MustRegister(InstanceProvisionTotal, InstanceReady, BackupLastSuccess, BackupDuration)
}
pkg/handler/middleware.go 把以上收敛为 gin Handler:
// file: pkg/handler/middleware.go
package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"k8s.io/client-go/dynamic"
"github.com/example/paas-middleware/pkg/gateway"
"github.com/example/paas-middleware/pkg/metrics"
"github.com/example/paas-middleware/pkg/middleware"
)
// CreateInstanceRequest 创建实例请求体。
type CreateInstanceRequest struct {
Tenant string `json:"tenant" binding:"required"`
App string `json:"app" binding:"required"`
Env string `json:"env" binding:"required"`
Cluster string `json:"cluster" binding:"required"`
Type string `json:"type" binding:"required"` // mysql | redis | kafka
Replicas int `json:"replicas"`
Size string `json:"size"`
}
// CreateInstanceHandler 经 API 网关创建中间件实例(调 operator)。
func CreateInstanceHandler(gw *gateway.ClusterGateway, dyn dynamic.Interface) gin.HandlerFunc {
return func(c *gin.Context) {
var req CreateInstanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
if _, err := gw.ClientFor(ctx, req.Cluster); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ns := req.Tenant + "-" + req.Env
mw := middleware.NewMiddlewareClient(dyn)
inst := middleware.BuildMySQLInstance(req.App, ns, req.Replicas, req.Size)
if err := mw.CreateInstance(ctx, ns, inst); err != nil {
metrics.InstanceProvisionTotal.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, req.Type, "failed").Inc()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
metrics.InstanceProvisionTotal.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, req.Type, "success").Inc()
c.JSON(http.StatusOK, gin.H{"status": "provisioning", "namespace": ns})
}
}
// BackupHandler 触发备份(Go 调用 K8s 创建 Job)。
func BackupHandler(gw *gateway.ClusterGateway) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
Tenant string `json:"tenant" binding:"required"`
App string `json:"app" binding:"required"`
Env string `json:"env" binding:"required"`
Cluster string `json:"cluster" binding:"required"`
Image string `json:"image"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
cs, err := gw.ClientFor(ctx, req.Cluster)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ns := req.Tenant + "-" + req.Env
img := req.Image
if img == "" {
img = "harbor.example.com/paas-mysql-backup:latest"
}
bc := middleware.NewBackupClient(cs)
job, err := bc.TriggerBackup(ctx, ns, req.App, img)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
metrics.BackupLastSuccess.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster).SetToCurrentTime()
c.JSON(http.StatusOK, gin.H{"job": job, "status": "triggered"})
}
}
// StatusHandler 读取实例状态(Go 调用 K8s 读取状态)。
func StatusHandler(gw *gateway.ClusterGateway, dyn dynamic.Interface) gin.HandlerFunc {
return func(c *gin.Context) {
tenant := c.Param("tenant")
env := c.Param("env")
app := c.Param("app")
cluster := c.Query("cluster")
if cluster == "" {
cluster = "cluster-prod"
}
ctx := c.Request.Context()
if _, err := gw.ClientFor(ctx, cluster); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
mw := middleware.NewMiddlewareClient(dyn)
phase, ready, err := mw.Status(ctx, tenant+"-"+env, app)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
readyVal := 0.0
if phase == "Ready" {
readyVal = 1
}
metrics.InstanceReady.WithLabelValues(tenant, app, env, cluster, "mysql").Set(readyVal)
c.JSON(http.StatusOK, gin.H{"phase": phase, "readyReplicas": ready})
}
}
var _ = time.Now
go.mod 与 main.go:
// file: go.mod
module github.com/example/paas-middleware
go 1.22
require (
github.com/gin-gonic/gin v1.10.0
github.com/prometheus/client_golang v1.19.0
k8s.io/apimachinery v0.30.0
k8s.io/client-go v0.30.0
)
// file: main.go
package main
import (
"log"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"k8s.io/client-go/dynamic"
"github.com/example/paas-middleware/pkg/gateway"
"github.com/example/paas-middleware/pkg/handler"
)
func main() {
gw, err := gateway.NewClusterGateway("/etc/paas/gateway.kubeconfig")
if err != nil {
log.Fatalf("init gateway: %v", err)
}
dyn, err := dynamic.NewForConfig(gw.RESTConfig())
if err != nil {
log.Fatalf("init dynamic: %v", err)
}
r := gin.Default()
r.POST("/api/v1/middleware", handler.CreateInstanceHandler(gw, dyn))
r.POST("/api/v1/middleware/backup", handler.BackupHandler(gw))
r.GET("/api/v1/middleware/:tenant/:env/:app", handler.StatusHandler(gw, dyn))
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
if err := r.Run(":8081"); err != nil {
log.Fatalf("server: %v", err)
}
}
分步操作(K8s IaC 完整可 apply)
MySQL StatefulSet + PVC + Service(group replication,1 主 2 从,manifests/mysql-statefulset.yaml):
# file: manifests/mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql-order
namespace: tenant-a-prod
labels:
app: mysql-order
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
serviceName: mysql-order-headless
replicas: 3
selector:
matchLabels:
app: mysql-order
template:
metadata:
labels:
app: mysql-order
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
containers:
- name: mysql
image: harbor.example.com/mysql:8.0
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-order-secret
key: ROOT_PASSWORD
volumeMounts:
- name: data
mountPath: /var/lib/mysql
resources:
requests:
cpu: "1"
memory: 2Gi
limits:
cpu: "2"
memory: 4Gi
volumeClaimTemplates:
- metadata:
name: data
labels:
app: mysql-order
tenant: tenant-a
env: prod
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-encrypted # ★ KMS 信封加密存储类
resources:
requests:
storage: 200Gi
---
apiVersion: v1
kind: Service
metadata:
name: mysql-order
namespace: tenant-a-prod
labels:
app: mysql-order
tenant: tenant-a
env: prod
spec:
selector:
app: mysql-order
ports:
- name: mysql
port: 3306
targetPort: 3306
---
apiVersion: v1
kind: Service
metadata:
name: mysql-order-headless
namespace: tenant-a-prod
labels:
app: mysql-order
tenant: tenant-a
env: prod
spec:
clusterIP: None
selector:
app: mysql-order
ports:
- name: mysql
port: 3306
targetPort: 3306
Redis Cluster StatefulSet + PVC + Service(manifests/redis-statefulset.yaml):
# file: manifests/redis-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cache
namespace: tenant-a-prod
labels:
app: redis-cache
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
serviceName: redis-cache-headless
replicas: 6
selector:
matchLabels:
app: redis-cache
template:
metadata:
labels:
app: redis-cache
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
containers:
- name: redis
image: harbor.example.com/redis:7.2
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
cpu: "500m"
memory: 1Gi
limits:
cpu: "1"
memory: 2Gi
volumeClaimTemplates:
- metadata:
name: data
labels:
app: redis-cache
tenant: tenant-a
env: prod
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-encrypted
resources:
requests:
storage: 20Gi
---
apiVersion: v1
kind: Service
metadata:
name: redis-cache
namespace: tenant-a-prod
labels:
app: redis-cache
tenant: tenant-a
env: prod
spec:
selector:
app: redis-cache
ports:
- name: redis
port: 6379
targetPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis-cache-headless
namespace: tenant-a-prod
labels:
app: redis-cache
tenant: tenant-a
env: prod
spec:
clusterIP: None
selector:
app: redis-cache
ports:
- name: redis
port: 6379
targetPort: 6379
每日全量 + 增量备份 CronJob(manifests/mysql-backup-cronjob.yaml):
# file: manifests/mysql-backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: mysql-order-backup
namespace: tenant-a-prod
labels:
app: mysql-order
tenant: tenant-a
env: prod
cluster: cluster-prod
spec:
schedule: "0 2 * * *" # 每日全量
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
metadata:
labels:
app: mysql-order
paas.example.com/type: backup
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: harbor.example.com/paas-mysql-backup:latest
args: ["backup", "--instance", "mysql-order", "--pitr", "--upload", "s3://paas-backup/tenant-a/"]
env:
- name: S3_ENDPOINT
value: "https://obs.example.com"
- name: BACKUP_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-order-secret
key: BACKUP_PASSWORD
NetworkPolicy 严格隔离(manifests/middleware-networkpolicy.yaml):
# file: manifests/middleware-networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: mysql-order-netpol
namespace: tenant-a-prod
labels:
app: mysql-order
tenant: tenant-a
env: prod
spec:
podSelector:
matchLabels:
app: mysql-order
policyTypes:
- Ingress
- Egress
ingress:
# 仅允许同租户 order-service 访问 3306
- from:
- podSelector:
matchLabels:
app: order-service
namespaceSelector:
matchLabels:
tenant: tenant-a
ports:
- protocol: TCP
port: 3306
egress:
# 允许访问备份对象存储与 DNS
- to:
- ipBlock:
cidr: 10.20.0.0/16 # 备份存储网段
ports:
- protocol: TCP
port: 443
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
运维 runbook(端到端真实命令)
# 1) 经平台 API 申请生产 MySQL 主从实例(Go CreateInstanceHandler -> 下发 MiddlewareInstance CRD)
curl -XPOST http://paas-gateway/api/v1/middleware -H 'Content-Type: application/json' -d '{
"tenant":"tenant-a","app":"mysql-order","env":"prod","cluster":"cluster-prod",
"type":"mysql","replicas":3,"size":"200Gi"}'
# 2) 直接使用 IaC 落地(与平台下发等价)
kubectl apply -f manifests/mysql-instance.yaml
kubectl apply -f manifests/mysql-statefulset.yaml
kubectl apply -f manifests/mysql-backup-cronjob.yaml
kubectl apply -f manifests/middleware-networkpolicy.yaml
# 3) 查看实例状态(Pod/STS/PVC)
kubectl -n tenant-a-prod get middlewareinstance mysql-order
kubectl -n tenant-a-prod get sts,pods,pvc -l app=mysql-order
# 4) 手动触发一次 PITR 备份基点
kubectl -n tenant-a-prod create job mysql-order-backup-manual --from=cronjob/mysql-order-backup
# 5) 扩缩容(增加只读副本)
kubectl -n tenant-a-prod scale sts/mysql-order --replicas=5
# 6) 镜像安全(中间件基础镜像同样扫描)
trivy image --severity CRITICAL,HIGH --exit-code 1 harbor.example.com/mysql:8.0
【测试环境】与【生产环境】差异化步骤
| 环节 | 测试环境 | 生产环境 |
|---|---|---|
| 创建审批 | 免审 / 单级 | ★ 多级审批 |
| 加密存储 | 可选 | ★ 强制 KMS 信封加密 |
| 备份策略 | 可关闭或低频 | ★ 自动备份 + PITR 不可关 |
| 高可用拓扑 | 单副本即可 | 多副本 + 跨故障域 |
| 网络隔离 | 宽松 | NetworkPolicy 严格限制 |
| 监控告警 | 仅大盘 | 大盘 + Alertmanager 分级 |
| 销毁 | 直接删 | 审批 + 安全擦除 + 审计 |
⚠️ 即便测试环境,也建议开启加密与备份,保证压测数据、恢复演练与生产一致,避免"测试不备份、生产不敢恢复"。
五、生产环境管控与安全约束
生产环境对中间件实例的管控显著严于测试。下面是「测试 vs 生产 差异化管控」对照表。
| 管控维度 | 测试环境 | 生产环境 |
|---|---|---|
| 实例创建 | 免审 / 单级 | ★ 多级审批 |
| 静态加密 | 可选 | ★ KMS 信封加密,强制 |
| 资源隔离 | 逻辑 Namespace | ★ Namespace + 配额 + NetworkPolicy |
| 备份 | 可关闭 | ★ 自动备份 + PITR,不可关 |
| 高可用 | 单副本 | 多副本跨故障域 |
| 参数基线 | 默认 | 合规基线 + 变更审批 |
| 销毁 | 直接删 | 审批 + 安全擦除 + 审计归档 |
| 审计留存 | 90 天 | 90 天以上,不可篡改 |
| 监控 | 仅大盘 | 大盘 + 分级告警 + 自动处置 |
关键约束说明
- 资源隔离:生产实例独占节点池或强绑配额,防止与 noisy neighbor 争抢 IO;NetworkPolicy 仅放行授权应用网段(见
manifests/middleware-networkpolicy.yaml)。以下 RBAC 把"生产实例变更"收敛给运维角色:
# file: manifests/rbac-prod.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tenant-a-prod-mw-ops
namespace: tenant-a-prod
rules:
- apiGroups: ["paas.example.com"]
resources: ["middlewareinstances"]
verbs: ["get", "list", "watch", "create", "update", "patch"] # 仅运维可写
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["get", "list", "watch", "scale"]
- apiGroups: ["batch"]
resources: ["cronjobs", "jobs"]
verbs: ["get", "list", "watch", "create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tenant-a-prod-mw-ops
namespace: tenant-a-prod
subjects:
- kind: User
name: mw-ops@tenant-a
roleRef:
kind: Role
name: tenant-a-prod-mw-ops
apiGroup: rbac.authorization.k8s.io
- 敏感操作审批:创建、扩缩、参数变更、恢复、销毁均须审批,记录不可绕过。
- 审计规则:操作含 操作人 / 租户 / 实例 / 时间 / 前后值,留存 ≥ 90 天。
- 故障熔断:实例不可用触发自动选主 / 副本顶替;备份连续失败升级 P1 告警(见第六章
BackupFailed)。
六、常见生产故障与解决方案
结合多集群与联邦监控场景列举高频问题。下方 observability/mysql-recording-rules.yaml 与 observability/mysql-alert-rules.yaml 把"可观测"固化为规则。
recording rules(慢查询 / 命中率 / 堆积)
# file: observability/mysql-recording-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: mysql-recording-rules
namespace: monitoring
labels:
role: recording-rules
spec:
groups:
- name: middleware_slo
interval: 30s
rules:
- record: redis_hit_rate
expr: |
sum(rate(redis_keyspace_hits_total[5m])) by (tenant,app,env,cluster)
/
(sum(rate(redis_keyspace_hits_total[5m])) by (tenant,app,env,cluster)
+ sum(rate(redis_keyspace_misses_total[5m])) by (tenant,app,env,cluster))
- record: mysql_slow_query_rate
expr: rate(mysql_global_status_slow_queries[5m])
- record: mysql_connection_ratio
expr: mysql_global_status_threads_connected / mysql_global_status_max_connections
- record: kafka_consumer_lag_sum
expr: sum(kafka_consumer_group_lag) by (tenant,app,env,cluster)
# 平台控制面指标(与 pkg/metrics/metrics.go 上报一致)
- record: middleware_instance_available
expr: paas_middleware_instance_ready
告警规则(主库宕机 / 慢查询 / 热Key / 堆积 / 备份失败)
# file: observability/mysql-alert-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: mysql-alert-rules
namespace: monitoring
labels:
role: alert-rules
spec:
groups:
- name: middleware_alerts
rules:
- alert: MySQLDown
expr: mysql_up == 0
for: 2m
labels:
severity: critical
tenant: "{{ $labels.tenant }}"
annotations:
summary: "MySQL 实例 {{ $labels.instance }} 不可达"
- alert: MySQLSlowQuerySpike
expr: mysql_slow_query_rate > 1
for: 5m
labels:
severity: warning
annotations:
summary: "慢查询率升高"
- alert: RedisHitRateLow
expr: redis_hit_rate < 0.9
for: 10m
labels:
severity: warning
annotations:
summary: "Redis 命中率低于 90%"
- alert: KafkaConsumerLagHigh
expr: kafka_consumer_lag_sum > 100000
for: 10m
labels:
severity: warning
annotations:
summary: "Kafka 消费堆积超过 10 万"
- alert: BackupFailed
expr: (time() - middleware_backup_last_success_timestamp) > 86400
for: 5m
labels:
severity: critical
annotations:
summary: "实例 {{ $labels.app }} 超过 24h 无成功备份"
- alert: BackupJobFailed
expr: kube_job_status_failed{job_name=~".*backup.*"} == 1
for: 5m
labels:
severity: critical
annotations:
summary: "备份 Job 执行失败"
故障 1:MySQL 主库宕机,写入中断
现象:Grafana 显示主库 Pod 非就绪,应用报连接拒绝,Alertmanager P1(MySQLDown)。
排查:① 联邦大盘确认主从状态;② 查节点是否 NotReady(资源 / 网络);③ 查 PVC 是否绑定。
优化:控制器自动选新主,Service 端点不变;事后复盘是否跨故障域,必要时迁移副本。
故障 2:Kafka 消费堆积(Lag 暴涨)
现象:联邦指标 kafka_consumer_lag_sum 持续上升,触发 KafkaConsumerLagHigh。
排查:① 消费端是否宕机;② 分区数是否过少;③ 下游依赖是否变慢。
优化:临时扩消费副本 + 增加分区;长期做消费端限流与背压。
故障 3:误删数据需恢复
现象:运维误执行 DROP,业务要求找回。
排查:① 确认备份存在且未过期;② 取最近全量 + 重放 binlog 到删除前时间点。
优化:走 PITR 恢复到临时实例校验,再切流量;全程审批 + 审计。
故障处置 runbook
# 主库故障自动选主验证:删主库 Pod,观察副本顶替
kubectl -n tenant-a-prod delete pod mysql-order-0
kubectl -n tenant-a-prod get pods -l app=mysql-order -w
# 手动触发一次备份(PITR 基点)
kubectl -n tenant-a-prod create job mysql-order-backup-manual --from=cronjob/mysql-order-backup
# PITR 恢复到指定时间点(先恢复到临时实例校验,再切流)
kubectl -n tenant-a-prod run mysql-restore --image=harbor.example.com/paas-mysql-backup \
--restart=Never -- backup --pitr --target-time="2025-10-15T02:00:00+08:00" \
--src=s3://paas-backup/tenant-a/mysql-order/ --dst=/restore
kubectl -n tenant-a-prod exec mysql-restore -- mysql -h mysql-order < /restore/export.sql
# 确认备份成功时间戳指标(配合 BackupFailed 告警)
kubectl -n tenant-a-prod get pods -l paas.example.com/type=backup
# redis-benchmark 制造热 Key,验证联邦大盘热 Key 指标 + Alertmanager 告警
redis-benchmark -h redis-cache.tenant-a-prod.svc -t set -n 100000 -r 10
下面这张图给出中间件故障排查通用流程。
flowchart TD
S[联邦告警触发] --> A{实例是否不可用?}
A -->|是| B[确认主从/副本状态]
B --> C[控制器自动选主/顶替]
A -->|否| D{指标异常类型?}
D -->|慢查询| E[查索引/参数基线]
D -->|热Key/堆积| F[扩副本/加分区]
D -->|数据误删| G[走PITR恢复到临时实例]
G --> H[校验后切流]
C --> I[恢复后审计归档]
E --> I
F --> I
H --> I这张流程图把"告警 → 不可用/指标异常二分 → 选主/索引/扩副本/PITR → 审计归档"串成可复用的处置路径。
自测题与动手练习
自测题
- 中间件实例运行在何处?其隔离边界由什么保证?
- PITR 恢复的原理是什么?为什么备份必须与实例跨故障域?
- 中间件指标进入联邦必须携带哪四个标签?
app维度在中间件场景取什么? - 生产环境为何强制 KMS 信封加密与 NetworkPolicy 隔离?它们分别防什么?
- Kafka 消费堆积时应从哪几个方向排查与优化?
动手练习
- 在
tenant-a-test下申请 1 主 2 从 MySQL 实例,确认 StatefulSet / PVC / Service 落地到目标集群 Namespace。 - 配置每日全量 + binlog 增量备份,并手动触发一次 PITR 恢复到指定时间戳,校验数据完整。
- 用 redis-benchmark 制造热 Key,在联邦 Grafana 租户大盘观察热 Key 指标并确认 Alertmanager 触发分级告警。
本章小结
- 中间件托管平台把 MySQL / Redis / Kafka / ES 等从"各团队裸装"升级为"按需申请、平台护航"的标准托管。
- ★ 备份、加密、资源隔离三项不可删减底线,分别守住"数据可恢复"“静态不可读"“邻居不互扰”。
- 平台侧 Go 控制面(
pkg/gateway+pkg/middleware+pkg/handler)用 client-go(dynamic client)完成MiddlewareInstance创建(调 operator)、备份 Job 触发、实例状态读取,并通过paas_middleware_*/middleware_backup_*指标与联邦观测配置一一对齐。 - 实例运行在租户 Namespace,指标带
tenant/app/env/cluster进联邦,Grafana 隔离 + Alertmanager 分级联动。 - 生产管控显著严于测试:强制审批、加密、自动备份与严格网络隔离,差异贯穿创建到销毁全生命周期。
下一模块我们将进入「统一观测平台(Prometheus 联邦对接)」,深入联邦架构本身如何支撑以上所有模块的多租户监控与长期存储。