多K8s集群纳管模块

2025-10-15T10:30:00+08:00 | 19分钟阅读 | 更新于 2025-10-15T10:30:00+08:00

@

学习目标

学完本章,你应该能够:

  1. 解释「物理隔离多集群」为何是金融/政企的合规底线,以及生产/预发布/测试/开发四环境如何划分。
  2. 描述统一 K8s API 网关(反向隧道 / 中心代理,参考 remotedialer 思路)如何收敛多集群 apiserver 访问。
  3. 完成一次集群注册:导入已有集群、托管其 kubeconfig(KMS 信封加密)、下发租户 Namespace 配额。
  4. 说清联邦 Prometheus Agent 如何随集群纳管自动接入,指标带 tenant/app/env/cluster 标签进中心。
  5. 处理故障集群的自动摘除与流量隔离,并基于联邦监控定位纳管异常根因。

前置知识

  • 了解 Kubernetes 集群基本组成(apiserver / etcd / kubelet / CNI / CSI)。
  • 知道 kubeconfig 文件的作用与 context/cluster/user 字段含义。
  • 看过本白皮书《〇、全局基础架构约束》与《模块 01 租户与组织权限中心》(Namespace 租户隔离、联邦监控)。
  • 了解反向隧道 / WebSocket 长连接的基本思路(无需会写)。

本章你会动手做的事

  • paas-cli cluster register 把一套测试 K8s 集群纳管进平台,观察 kubeconfig 被加密托管。
  • 在网关侧用 kubectl 等价命令跨集群查询 Namespace,验证你无需直连各集群 apiserver。
  • 在 Grafana 用 cluster= 标签切换,确认不同物理集群指标已汇入同一联邦视图。

一、模块概述与企业商用价值

类比:多集群纳管就像连锁品牌的总部调度中心。每家门店(集群)分布在不同的城市(物理隔离机房),各有自己的门禁和账本;总部不每家都派常驻经理,而是拉一条加密专线与每家店的收银系统(apiserver)对账。平时总部统一下发促销策略(Namespace 配额/部署),某家店断网(故障集群)时总部自动把它从调度名单摘掉,订单转给邻近门店。

金融、政企客户因等保与容灾要求,绝不允许把所有业务塞进一个 K8s 集群。生产、预发布、测试、开发往往分属不同机房甚至不同云。本模块是企业自研 PaaS 的神经中枢——它把分散、异构、物理隔离的 K8s 集群,收敛成「平台上一个统一入口」,让上层模块(权限、应用、监控)无需关心集群在哪。

适用角色:集群运维(注册/巡检/节点池)、平台管理员(环境划分/网关调优)、SRE(故障摘除/流量隔离)。

企业商用价值:

  • 统一入口:所有集群经统一 K8s API 网关纳管,上层模块零改造跨集群操作。
  • 物理隔离合规:四环境物理隔离,满足等保「生产与非生产分离」要求。
  • 连接安全:kubeconfig 永不落明文本地,KMS 信封加密托管,网关侧按需解密下发。
  • 可观测一致:纳管即接入联邦 Prometheus Agent,监控口径跨集群统一。

下图展示纳管模块在 PaaS 中的中枢定位:上接各业务模块,下收多套物理隔离集群,横向接监控与权限。

graph TD
    A[权限中心] --> G[统一 K8s API 网关]
    B[应用生命周期] --> G
    C[观测平台] --> G
    G --> D[生产集群 物理隔离]
    G --> E[预发布集群 物理隔离]
    G --> F[测试/开发集群 物理隔离]
    G --> H[联邦 Prometheus Agent 随纳管接入]
    H --> I[(中心 Thanos 长期存储)]

二、细分功能详解(商用生产级)

带「★禁止删减」项为金融/政企交付红线,缺失即不达标。

【基础能力】

① 集群注册(导入已有 / 平台纳管)

支持「导入已有集群」(客户提供 kubeconfig)与「平台纳管新建」(平台自动化初始化)。注册后平台与集群建立受控连接,apiserver 不再对外直暴露。集群在平台侧建模为自定义资源 Cluster(CRD clusters.paas.io),spec.env/spec.endpoint/spec.mode/spec.kubeconfigRef 与 Go 代码字段自洽。

注册方式适用场景连接模式
导入已有客户既有集群接入反向隧道(集群侧出连网关)
平台纳管新建平台统一供给中心代理(网关转发)

下面给出 Cluster CRD 定义(平台侧用 DynamicClient 操作,与 Go cluster.go 字段一致):

# file: deploy/crd-cluster.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: clusters.paas.io
spec:
  group: paas.io
  names:
    kind: Cluster
    plural: clusters
    singular: cluster
  scope: Cluster
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          required: ["spec"]
          properties:
            spec:
              type: object
              required: ["env", "endpoint"]
              properties:
                env:
                  type: string
                  description: "production / staging / test / dev"
                endpoint:
                  type: string
                  description: "apiserver 地址,仅网关可达,不直接对外暴露"
                mode:
                  type: string
                  enum: ["reverse-tunnel", "center-proxy"]
                kubeconfigRef:
                  type: string
                  description: "KMS 信封加密后的 kubeconfig Secret 名"
            status:
              type: object
              properties:
                phase:
                  type: string
                  enum: ["Connected", "Degraded", "Evicted"]
      subresources:
        status: {}

② 环境划分(★禁止删减:跨集群网络策略)——生产/预发布/测试/开发物理隔离

每套集群标注 env 标签,平台按 env 做调度域隔离;跨环境网络默认不通,确需互通走审批后的专用通道。

③ 集群健康巡检与纳管状态

定时探活 apiserver 健康、etcd 健康、节点 Ready 比例、核心组件(CNI/CSI)状态,纳管状态分:已连接 / degraded / 已摘除。

④ 节点池管理

按机型/可用区划分节点池,给租户 Namespace 绑定节点池亲和性与 ResourceQuota(★禁止删减:租户 Namespace 配额)。

【高级企业增值能力】

⑤ kubeconfig 加密托管(★禁止删减:集群连接加密)

客户 kubeconfig 上传后,平台用 KMS 信封加密(数据密钥 DEK 由 KMS 加密为密文,DEK 明文仅在网关内存使用),明文永不落盘。网关向目标集群发请求时内存解密、用完即焚。

⑥ 集群版本与 CNI/CSI 抽象

屏蔽底层 K8s 小版本差异与不同 CNI(Calico/Cilium)/CSI(云盘/自建存储)差异,向上提供一致接口。

⑦ 故障集群自动摘除与流量隔离

探测到集群连续不可用,网关将其标记为「已摘除」,调度器停止向该集群投放新负载,存量流量按策略隔离/转移,并触发 Alertmanager 告警。

下面功能架构图区分基础与高级能力:

graph LR
    A[集群注册
导入/纳管新建] --> B[环境划分
生产/预发布/测试/开发] B --> C[健康巡检
apiserver/etcd/节点] B --> D[节点池管理
亲和/配额] A --> E[kubeconfig 加密托管
KMS 信封加密] B --> F[版本与 CNI/CSI 抽象] C --> G[故障自动摘除
流量隔离] H[联邦 Prometheus Agent 自动接入] --> A

三、底层架构联动设计

1. 与多 K8s 集群交互:统一 K8s API 网关收敛 apiserver

平台不把各集群 apiserver 直接暴露给上层模块,而是全部经统一 K8s API 网关。网关采用「反向隧道 / 中心代理」模式(参考 Rancher remotedialer 思路):集群侧 agent 经 WebSocket 长连接主动连回网关,网关作为唯一入口转发所有 K8s API 请求。这样客户防火墙只需放通网关方向,且所有请求在网关层做统一鉴权、限流、审计。

项目结构与文件清单(本章起逐个文件完整展示,全部端到端自洽):

paas-multicluster/
├── deploy/
│   ├── crd-cluster.yaml          # Cluster CRD(DynamicClient 操作对象)
│   ├── gateway-deployment.yaml   # 统一 K8s API 网关 Deployment
│   ├── networkpolicy.yaml        # 跨租户/跨环境默认 Deny 网络策略
│   └── tenant-quota.yaml         # 注册时随之下发的租户 ResourceQuota
├── platform/
│   ├── main.go                   # gin 网关:注册 / 多集群代理 / metrics
│   ├── go.mod                    # 模块与依赖
│   └── k8s/
│       ├── registry.go           # rest.Config + 多集群路由(网关下发)
│       ├── kms.go                # KMS 信封加密 kubeconfig(明文不落盘)
│       ├── cluster.go            # 集群注册:加密托管+Cluster CR+部署Agent
│       ├── health.go             # 探活 + 故障自动摘除
│       ├── metrics.go            # Prometheus 指标(与规则名一致)
│       └── audit.go              # 审计落库(≥90 天)
├── observability/
│   ├── prometheus-agent.yaml     # 集群内 Prometheus Agent(relabel 注入标签)
│   ├── prometheus-federation.yaml# 中心联邦拉取
│   ├── recording-rules.yaml
│   ├── alert-rules.yaml
│   └── alertmanager.yaml
└── runbook/
    └── cluster-ops.sh            # kubectl/helm/shell 运维命令

registry.go 是「Go 调用 K8s」的核心:展示 rest.Config 的三种来源(in-cluster / kubeconfig / 网关下发的多集群配置)、kubernetes.NewForConfigdynamic.NewForConfig,并演示网关按集群 ID 路由到对应 apiserver 连接池。

// file: platform/k8s/registry.go
package k8s

import (
	"fmt"
	"sync"

	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
)

// ClusterEntry 网关侧保存的单集群连接(来自注册时 KMS 解密到内存的 kubeconfig)。
type ClusterEntry struct {
	ClusterID string
	Env       string
	Endpoint  string
	Config    *rest.Config
	Client    kubernetes.Interface
	Dyn       dynamic.Interface
	Phase     string // Connected / Degraded / Evicted
}

type ClusterRegistry struct {
	mu       sync.RWMutex
	clusters map[string]*ClusterEntry
}

func NewClusterRegistry() *ClusterRegistry {
	return &ClusterRegistry{clusters: make(map[string]*ClusterEntry)}
}

// LoadGatewayConfig 加载网关自身(中心控制面)rest.Config。
func LoadGatewayConfig(path string) (*rest.Config, error) {
	if path == "" {
		return rest.InClusterConfig()
	}
	return clientcmd.BuildConfigFromFlags("", path)
}

// RegisterCluster 用「KMS 信封解密后的 kubeconfig 字节」构建 rest.Config 并接入网关连接池。
// 明文 kubeconfig 不落盘:调用方从 Secret/内存解密后传入,函数返回后即被 GC。
func (r *ClusterRegistry) RegisterCluster(clusterID, env, endpoint string, kubeconfigBytes []byte) error {
	cfg, err := clientcmd.RESTConfigFromKubeConfig(kubeconfigBytes)
	if err != nil {
		return fmt.Errorf("build rest.Config from decrypted kubeconfig: %w", err)
	}
	cli, err := kubernetes.NewForConfig(cfg)
	if err != nil {
		return fmt.Errorf("new clientset: %w", err)
	}
	dyn, err := dynamic.NewForConfig(cfg)
	if err != nil {
		return fmt.Errorf("new dynamic client: %w", err)
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	r.clusters[clusterID] = &ClusterEntry{
		ClusterID: clusterID, Env: env, Endpoint: endpoint,
		Config: cfg, Client: cli, Dyn: dyn, Phase: "Connected",
	}
	return nil
}

// ClientForCluster 网关按集群 ID 路由(反向隧道模式下实际转发到内存解密的 apiserver 连接)。
func (r *ClusterRegistry) ClientForCluster(id string) (*ClusterEntry, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	c, ok := r.clusters[id]
	if !ok {
		return nil, fmt.Errorf("cluster %s not registered", id)
	}
	return c, nil
}

func (r *ClusterRegistry) SetPhase(id, phase string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if c, ok := r.clusters[id]; ok {
		c.Phase = phase
	}
}

// IDs 返回当前已纳管的所有集群 ID(健康巡检循环遍历用)。
func (r *ClusterRegistry) IDs() []string {
	r.mu.RLock()
	defer r.mu.RUnlock()
	ids := make([]string, 0, len(r.clusters))
	for id := range r.clusters {
		ids = append(ids, id)
	}
	return ids
}

kms.go 演示 KMS 信封加密:随机 DEK 加密明文,主密钥加密 DEK,密文落盘、DEK 明文仅内存使用(生产替换为云 KMS,接口一致)。

// file: platform/k8s/kms.go
package k8s

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"io"
)

// MockKMS 模拟云 KMS:真实场景调用 AWS KMS / 腾讯云 KMS 的 Encrypt/Decrypt。
// 这里用固定主密钥演示「数据密钥 DEK 由 KMS 加密为密文,DEK 明文仅内存使用」。
type MockKMS struct {
	masterKey []byte
}

func NewMockKMS(masterKey string) *MockKMS {
	return &MockKMS{masterKey: []byte(masterKey)}
}

func splitDot(s string) []string {
	for i := 0; i < len(s); i++ {
		if s[i] == '.' {
			return []string{s[:i], s[i+1:]}
		}
	}
	return []string{s}
}

// EnvelopeEncrypt 信封加密:生成随机 DEK,用 DEK 加密明文,再用主密钥加密 DEK。
// 返回格式:base64(主IV+encDEK).base64(数据IV+数据密文)
func (k *MockKMS) EnvelopeEncrypt(plaintext []byte) (string, error) {
	dek := make([]byte, 32)
	if _, err := io.ReadFull(rand.Reader, dek); err != nil {
		return "", err
	}
	block, err := aes.NewCipher(dek)
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}
	iv := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return "", err
	}
	ct := gcm.Seal(nil, iv, plaintext, nil)

	mblock, err := aes.NewCipher(k.masterKey)
	if err != nil {
		return "", err
	}
	mgcm, err := cipher.NewGCM(mblock)
	if err != nil {
		return "", err
	}
	miv := make([]byte, mgcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, miv); err != nil {
		return "", err
	}
	encDEK := mgcm.Seal(nil, miv, dek, nil)

	outer := append(miv, encDEK...)
	inner := append(iv, ct...)
	return base64.StdEncoding.EncodeToString(outer) + "." + base64.StdEncoding.EncodeToString(inner), nil
}

// EnvelopeDecrypt 解密:主密钥解出 DEK,DEK 解出明文;DEK 明文仅函数内使用,用完即焚。
func (k *MockKMS) EnvelopeDecrypt(payload string) ([]byte, error) {
	parts := splitDot(payload)
	if len(parts) != 2 {
		return nil, errors.New("bad envelope payload")
	}
	outer, err := base64.StdEncoding.DecodeString(parts[0])
	if err != nil {
		return nil, err
	}
	inner, err := base64.StdEncoding.DecodeString(parts[1])
	if err != nil {
		return nil, err
	}
	mblock, err := aes.NewCipher(k.masterKey)
	if err != nil {
		return nil, err
	}
	mgcm, err := cipher.NewGCM(mblock)
	if err != nil {
		return nil, err
	}
	ns := mgcm.NonceSize()
	if len(outer) < ns {
		return nil, errors.New("short outer")
	}
	miv, encDEK := outer[:ns], outer[ns:]
	dek, err := mgcm.Open(nil, miv, encDEK, nil)
	if err != nil {
		return nil, err
	}
	block, err := aes.NewCipher(dek)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	if len(inner) < gcm.NonceSize() {
		return nil, errors.New("short inner")
	}
	iv, ct := inner[:gcm.NonceSize()], inner[gcm.NonceSize():]
	return gcm.Open(nil, iv, ct, nil)
}

cluster.go 演示完整纳管流程(KMS 加密托管 + Cluster CR + 部署 Prometheus Agent),字段与 crd-cluster.yaml 自洽:

// file: platform/k8s/cluster.go
package k8s

import (
	"context"
	"fmt"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	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"
	"k8s.io/client-go/kubernetes"
)

var clusterGVR = schema.GroupVersionResource{Group: "paas.io", Version: "v1", Resource: "clusters"}

// RegisterCluster 完整纳管:加密托管 kubeconfig -> 建 Cluster CR -> 部署 Agent -> 接入连接池。
func RegisterCluster(ctx context.Context, reg *ClusterRegistry, kms *MockKMS,
	gwCli kubernetes.Interface, gwDyn dynamic.Interface,
	clusterID, env, endpoint, mode string, kubeconfigPlain []byte) error {

	// 1) KMS 信封加密 kubeconfig,仅密文落 Secret(明文不落盘)
	cipherText, err := kms.EnvelopeEncrypt(kubeconfigPlain)
	if err != nil {
		return fmt.Errorf("envelope encrypt kubeconfig: %w", err)
	}
	sec := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name: "kubeconfig-" + clusterID, Namespace: "paas-system",
			Labels: map[string]string{"paas.io/cluster": clusterID},
		},
		StringData: map[string]string{"kubeconfig": cipherText},
	}
	if _, err := gwCli.CoreV1().Secrets("paas-system").Create(ctx, sec, metav1.CreateOptions{}); err != nil {
		return fmt.Errorf("store encrypted kubeconfig: %w", err)
	}

	// 2) 在网关侧建 Cluster CR(DynamicClient)
	c := &unstructured.Unstructured{Object: map[string]interface{}{
		"apiVersion": "paas.io/v1", "kind": "Cluster",
		"metadata": map[string]interface{}{"name": clusterID, "labels": map[string]interface{}{"env": env}},
		"spec": map[string]interface{}{
			"env": env, "endpoint": endpoint, "mode": mode,
			"kubeconfigRef": "kubeconfig-" + clusterID,
		},
		"status": map[string]interface{}{"phase": "Connected"},
	}}
	if _, err := gwDyn.Resource(clusterGVR).Create(ctx, c, metav1.CreateOptions{}); err != nil {
		return fmt.Errorf("create Cluster CR: %w", err)
	}

	// 3) 解密后的 kubeconfig 接入网关连接池(明文仅内存)
	if err := reg.RegisterCluster(clusterID, env, endpoint, kubeconfigPlain); err != nil {
		return err
	}

	// 4) 在该集群部署 Prometheus Agent(用目标集群 client)
	entry, err := reg.ClientForCluster(clusterID)
	if err != nil {
		return err
	}
	if err := DeployPrometheusAgent(ctx, entry.Client, clusterID, env); err != nil {
		return err
	}
	IncClusterRegister(env, mode)
	return nil
}

// DeployPrometheusAgent 在目标集群部署 Agent(Agent 模式只 remote_write,本地不长周期存)。
func DeployPrometheusAgent(ctx context.Context, cli kubernetes.Interface, clusterID, env string) error {
	cm := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name: "prom-agent-" + clusterID, Namespace: "paas-monitoring",
			Labels: map[string]string{"paas.io/cluster": clusterID},
		},
		Data: map[string]string{
			"prometheus.yml": fmt.Sprintf(`global:
  scrape_interval: 30s
  external_labels:
    cluster: %s
    env: %s
remote_write:
  - url: http://thanos-receiver.paas-monitoring.svc:19291/api/v1/receive
scrape_configs:
  - job_name: kube-pods
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_namespace]
        target_label: tenant
      - source_labels: [__meta_kubernetes_pod_label_app]
        target_label: app
`, clusterID, env),
		},
	}
	if _, err := cli.CoreV1().ConfigMaps("paas-monitoring").Create(ctx, cm, metav1.CreateOptions{}); err != nil {
		return err
	}
	replicas := int32(1)
	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name: "prom-agent-" + clusterID, Namespace: "paas-monitoring",
			Labels: map[string]string{"paas.io/cluster": clusterID},
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "prom-agent", "paas.io/cluster": clusterID}},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "prom-agent", "paas.io/cluster": clusterID}},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{{
						Name:  "agent",
						Image: "prom/prometheus:v2.51.0",
						Args:  []string{"--config.file=/etc/prometheus/prometheus.yml", "--enable-feature=agent"},
						VolumeMounts: []corev1.VolumeMount{
							{Name: "cfg", MountPath: "/etc/prometheus"},
						},
					}},
					Volumes: []corev1.Volume{{
						Name: "cfg",
						VolumeSource: corev1.VolumeSource{
							ConfigMap: &corev1.ConfigMapVolumeSource{
								LocalObjectReference: corev1.LocalObjectReference{Name: "prom-agent-" + clusterID},
							},
						},
					}},
				},
			},
		},
	}
	if _, err := cli.AppsV1().Deployments("paas-monitoring").Create(ctx, dep, metav1.CreateOptions{}); err != nil {
		return err
	}
	return nil
}

health.go 探活与故障自动摘除:

// file: platform/k8s/health.go
package k8s

import (
	"context"
	"fmt"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
)

// ProbeCluster 探活:apiserver 健康 + 节点 Ready 比例,返回 phase。
func ProbeCluster(ctx context.Context, cli kubernetes.Interface) (string, error) {
	nodes, err := cli.CoreV1().Nodes().List(ctx, metav1.ListOptions{Limit: 100})
	if err != nil {
		return "Degraded", fmt.Errorf("apiserver probe failed: %w", err)
	}
	ready := 0
	for _, n := range nodes.Items {
		for _, c := range n.Status.Conditions {
			if c.Type == "Ready" && c.Status == "True" {
				ready++
			}
		}
	}
	if ready == 0 && len(nodes.Items) > 0 {
		return "Degraded", nil
	}
	return "Connected", nil
}

// EvictIfUnhealthy 连续不可用达阈值则标记为已摘除并隔离流量;否则标记 Degraded。
func EvictIfUnhealthy(reg *ClusterRegistry, id string, failCount int) (evicted bool) {
	if failCount >= 3 {
		reg.SetPhase(id, "Evicted")
		return true
	}
	reg.SetPhase(id, "Degraded")
	return false
}

metrics.go 暴露指标(paas_cluster_health / paas_cluster_agent_up / paas_cluster_register_total),与第六章规则名一致:

// file: platform/k8s/metrics.go
package k8s

import (
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
)

var (
	clusterHealthGauge = promauto.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "paas_cluster_health",
			Help: "集群健康状态:1=Connected 0=Degraded -1=Evicted",
		},
		[]string{"cluster", "env"},
	)
	agentUpGauge = promauto.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "paas_cluster_agent_up",
			Help: "Prometheus Agent 是否在线",
		},
		[]string{"cluster", "env"},
	)
	clusterRegisterTotal = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "paas_cluster_register_total",
			Help: "集群纳管总数",
		},
		[]string{"env", "mode"},
	)
)

func SetClusterHealth(cluster, env, phase string) {
	v := 1.0
	if phase == "Degraded" {
		v = 0
	} else if phase == "Evicted" {
		v = -1
	}
	clusterHealthGauge.WithLabelValues(cluster, env).Set(v)
}
func SetAgentUp(cluster, env string, up bool) {
	if up {
		agentUpGauge.WithLabelValues(cluster, env).Set(1)
	} else {
		agentUpGauge.WithLabelValues(cluster, env).Set(0)
	}
}
func IncClusterRegister(env, mode string) {
	clusterRegisterTotal.WithLabelValues(env, mode).Inc()
}

audit.go 审计落库(≥90 天):

// file: platform/k8s/audit.go
package k8s

import (
	"context"
	"encoding/json"
	"os"
	"sync"
	"time"
)

// AuditRecord 审计记录(含操作人/对象/动作/集群/时间/来源IP/审批单号)。
type AuditRecord struct {
	Timestamp time.Time `json:"ts"`
	Operator  string    `json:"operator"`
	Action    string    `json:"action"`
	Cluster   string    `json:"cluster"`
	Target    string    `json:"target"`
	SrcIP     string    `json:"src_ip"`
	Ticket    string    `json:"ticket"`
}

type AuditSink struct {
	mu   sync.Mutex
	path string
}

func NewAuditSink(path string) *AuditSink { return &AuditSink{path: path} }

func (a *AuditSink) Write(ctx context.Context, rec AuditRecord) error {
	rec.Timestamp = time.Now()
	b, err := json.Marshal(rec)
	if err != nil {
		return err
	}
	a.mu.Lock()
	defer a.mu.Unlock()
	f, err := os.OpenFile(a.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer f.Close()
	_, err = f.Write(append(b, '\n'))
	return err
}
// file: platform/go.mod
module paas.example.com

go 1.22

require (
	github.com/gin-gonic/gin v1.9.1
	github.com/prometheus/client_golang v1.18.0
	k8s.io/api v0.29.0
	k8s.io/apimachinery v0.29.0
	k8s.io/client-go v0.29.0
)

网关 HTTP 服务(gin):注册集群 + 多集群代理路由 + metrics:

// file: platform/main.go
package main

import (
	"context"
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"

	"paas.example.com/platform/k8s"
)

func main() {
	reg := k8s.NewClusterRegistry()
	kms := k8s.NewMockKMS("0123456789abcdef0123456789abcdef") // 真实为 KMS 主密钥,永不落盘

	// 网关自身连接(中心控制面:存 Cluster CR / 加密 kubeconfig Secret / 部署 Agent)
	gwCfg, err := k8s.LoadGatewayConfig("/etc/paas/gateway-kubeconfig")
	if err != nil {
		log.Fatalf("load gateway config: %v", err)
	}
	gwCli, err := kubernetes.NewForConfig(gwCfg)
	if err != nil {
		log.Fatalf("gateway clientset: %v", err)
	}
	gwDyn, err := dynamic.NewForConfig(gwCfg)
	if err != nil {
		log.Fatalf("gateway dynamic client: %v", err)
	}
	audit := k8s.NewAuditSink("/var/log/paas/cluster-audit.jsonl")

	r := gin.Default()
	r.GET("/metrics", gin.WrapH(promhttp.Handler()))

	// 集群注册(平台管理员 / 集群运维)
	r.POST("/api/v1/clusters", func(c *gin.Context) {
		var req struct {
			ID         string `json:"id"`
			Env        string `json:"env"`
			Endpoint   string `json:"endpoint"`
			Mode       string `json:"mode"`
			Kubeconfig string `json:"kubeconfig"` // 明文仅请求内存中,立即 KMS 加密
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		// 生产环境注册强制二级审批:header 携带审批单号
		if req.Env == "production" && c.GetHeader("X-Approval-Ticket") == "" {
			c.JSON(http.StatusForbidden, gin.H{"error": "production register requires approval ticket"})
			return
		}
		if err := k8s.RegisterCluster(c.Request.Context(), reg, kms, gwCli, gwDyn,
			req.ID, req.Env, req.Endpoint, req.Mode, []byte(req.Kubeconfig)); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		_ = audit.Write(c.Request.Context(), k8s.AuditRecord{
			Operator: c.GetHeader("X-User"), Action: "register-cluster",
			Cluster: req.ID, Target: "cluster/" + req.ID,
			SrcIP: c.ClientIP(), Ticket: c.GetHeader("X-Approval-Ticket"),
		})
		c.JSON(http.StatusOK, gin.H{"status": "registered"})
	})

	// 网关多集群代理:按集群 ID 路由到对应 apiserver(反向隧道/中心代理)
	r.Any("/proxy/:cluster/*path", func(c *gin.Context) {
		id := c.Param("cluster")
		entry, err := reg.ClientForCluster(id)
		if err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
			return
		}
		// 真实实现:用 entry.Config 构造 transport 转发 K8s API;此处回显路由命中结果
		c.JSON(http.StatusOK, gin.H{
			"cluster": entry.ClusterID, "endpoint": entry.Endpoint, "phase": entry.Phase,
		})
	})

	// 健康检查与故障摘除循环(SRE):每 30s 探活,连续失败达阈值则摘除
	probeFails := map[string]int{}
	go func() {
		ticker := time.NewTicker(30 * time.Second)
		defer ticker.Stop()
		for range ticker.C {
			for _, id := range reg.IDs() {
				entry, err := reg.ClientForCluster(id)
				if err != nil {
					continue
				}
				phase, perr := k8s.ProbeCluster(context.Background(), entry.Client)
				if perr != nil {
					probeFails[id]++
					if k8s.EvictIfUnhealthy(reg, id, probeFails[id]) {
						phase = "Evicted"
					} else {
						phase = "Degraded"
					}
				} else {
					probeFails[id] = 0
				}
				k8s.SetClusterHealth(id, entry.Env, phase)
				if phase == "Degraded" || phase == "Evicted" {
					_ = audit.Write(context.Background(), k8s.AuditRecord{
						Operator: "health-probe", Action: "cluster-probe",
						Cluster: id, Target: "cluster/" + id, SrcIP: "gateway", Ticket: "AUTO",
					})
				}
			}
		}
	}()

	log.Println("multi-cluster gateway listening on :8080")
	if err := r.Run(":8080"); err != nil {
		log.Fatal(err)
	}
}

网关自身部署清单:

# file: deploy/gateway-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-api-gateway
  namespace: paas-system
  labels:
    app: k8s-api-gateway
spec:
  replicas: 2
  selector:
    matchLabels:
      app: k8s-api-gateway
  template:
    metadata:
      labels:
        app: k8s-api-gateway
    spec:
      containers:
      - name: gateway
        image: paas/k8s-api-gateway:v1.0.0
        args: ["--kubeconfig=/etc/paas/gateway-kubeconfig", "--listen=:8080"]
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: kc
          mountPath: /etc/paas
          readOnly: true
      volumes:
      - name: kc
        secret:
          secretName: gateway-kubeconfig   # 由 KMS 信封加密托管,挂载即内存解密

网关收敛多集群的架构:

graph TD
    A[应用模块] --> G[统一 K8s API 网关]
    B[权限中心] --> G
    G -->|反向隧道 WebSocket| C[生产集群 agent]
    G -->|反向隧道 WebSocket| D[测试集群 agent]
    C --> E[生产 apiserver]
    D --> F[测试 apiserver]
    G -->|内存解密 kubeconfig| E
    G -->|内存解密 kubeconfig| F

2. 与联邦 Prometheus 监控联动:纳管即接入

集群注册成功的同时,平台在该集群部署 Prometheus Agent(Agent 模式只远程写、本地不存长周期),指标统一带 tenant/app/env/cluster 标签,经中心 Prometheus federation 拉取 + Thanos 长期存储。Grafana 用 cluster= 标签跨集群统一视图,Alertmanager 按 env/cluster 分级路由。Go DeployPrometheusAgent 注入的 cluster/env external_labels 与下列 Agent 清单一致。

graph TD
    A[集群A Prometheus Agent] -->|remote_write / federation| C[中心 Prometheus]
    B[集群B Prometheus Agent] -->|remote_write / federation| C
    C --> D[Thanos Query + Store]
    D --> E[(对象存储 长期)]
    D --> F[Grafana 按 cluster/env 过滤]
    D --> G[Alertmanager 分级告警]
    H[纳管时自动注入 cluster/env 标签] --> A
    H --> B

3. 与 RBAC / 审批 / 审计的联动

  • 网关层鉴权复用权限中心的租户/角色模型:请求带 tenant 标签,网关校验「该租户是否有权操作目标集群的该 Namespace」。
  • 集群摘除、kubeconfig 轮换等高危操作触发审批流,执行后写审计(含审批单号、操作人、集群 ID)。

四、端到端标准操作流程

分角色给出分步操作,并显式区分【测试环境】与【生产环境】。

角色一:集群运维 —— 注册一套测试集群

注册由平台 REST API 触发(main.goPOST /api/v1/clusters),后端经 RegisterCluster 完成 KMS 加密托管 + Cluster CR + Agent 部署:

# 步骤 1:准备最小权限 kubeconfig(禁止 cluster-admin)
# 步骤 2:注册,平台自动 KMS 信封加密托管,并部署 Prometheus Agent
curl -X POST https://paas-gateway/api/v1/clusters \
  -H "Content-Type: application/json" \
  -d '{
    "id": "test-cluster-01",
    "env": "test",
    "endpoint": "https://10.10.10.10:6443",
    "mode": "reverse-tunnel",
    "kubeconfig": "<base64-or-plaintext-only-in-memory>"
  }'

# 步骤 3:注册成功后,平台在集群内生成租户 Namespace 与配额
kubectl --context test-cluster-01 get ns subcompany-a --show-labels
kubectl --context test-cluster-01 get resourcequota -n subcompany-a

对应静态清单(供 helm/排障对照):

# file: deploy/tenant-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-subcompany-a
  namespace: subcompany-a
  labels:
    tenant: subcompany-a
spec:
  hard:
    requests.cpu: "50"
    requests.memory: 200Gi
    pods: "100"

跨租户/跨环境默认拒绝网络策略:

# file: deploy/networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-cross-tenant
  namespace: subcompany-a
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  # 仅允许同租户 namespace 内通信,跨租户/跨环境默认拒绝
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          tenant: subcompany-a
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          tenant: subcompany-a

⚠️ 【测试环境】可用自签证书、宽松网络;【生产环境】kubeconfig 必须最小权限、走 TLS 双向认证,且注册动作需二级审批 + 合规基线检查通过后方可纳管(见 main.goX-Approval-Ticket 校验)。

角色二:平台管理员 —— 环境划分与节点池绑定

# 步骤 1:给集群打 env 标签(物理隔离域)
paas-cli cluster label --name prod-cluster-01 --set env=production

# 步骤 2:创建节点池并绑定租户配额
paas-cli nodepool create --cluster prod-cluster-01 --name np-gold --zone az1
paas-cli tenant bind-nodepool --tenant subcompany-a --nodepool np-gold

角色三:SRE —— 故障集群摘除与恢复

【测试环境】可手动 paas-cli cluster drain 演练;【生产环境】摘除由健康巡检自动触发,且摘除动作即时 Alertmanager 告警 + 企业微信通知,恢复需 SRE 确认健康后手动 uncordon

下面端到端时序图覆盖「注册 → Agent 接入 → 请求经网关 → 故障摘除」:

sequenceDiagram
    participant O as 集群运维
    participant P as 纳管模块
    participant G as K8s API 网关
    participant K as 目标集群
    participant M as 联邦监控
    O->>P: 提交注册申请
    P->>G: KMS 加密托管 kubeconfig
    G->>K: 建立反向隧道
    K-->>G: 隧道就绪
    G->>K: 部署 Prometheus Agent
    K-->>M: 指标带 cluster/env 标签
    P-->>O: 纳管成功
    Note over G,K: 巡检发现集群不可用
    G->>G: 标记为已摘除 + 流量隔离
    G->>M: 触发 Alertmanager 告警

五、生产环境管控与安全约束

核心原则:连接加密、环境物理隔离、故障可隔离、纳管可审计

测试环境 vs 生产环境 差异化管控对照表

管控项测试环境生产环境
kubeconfig 权限可放宽便于调试强制最小权限 clusterrole
集群注册审批免审批,自助纳管二级审批 + 合规基线检查
网络通道可自签证书、宽松双向 TLS、专线/加密隧道
故障摘除可手动演练 drain自动摘除 + 即时告警 + 人工确认恢复
节点池变更随时调整变更需审批,灰度生效
纳管审计≥ 90 天≥ 90 天,可追溯导出
跨环境互通测试可临时放通默认不通,需安全审批专用通道
镜像运行可跳过扫描强制 Trivy 扫描阻断高危(联动安全模块)
  • 集群连接加密:kubeconfig 全程 KMS 信封加密(见 kms.go),网关内存解密、用完即焚,明文不落盘、不进日志。
  • 租户 Namespace 配额:注册即按租户 ResourceQuota 下发,杜绝单租户吃满集群。
  • 跨集群网络策略:默认 Deny-All,跨环境/跨租户互通走 NetworkPolicy + 审批(见 deploy/networkpolicy.yaml)。
  • 敏感操作审批:kubeconfig 轮换、集群摘除、env 变更触发审批流(见 main.goX-Approval-Ticket 校验)。
  • 故障熔断:集群 degraded 时网关限流降级,避免雪崩波及健康集群。

六、常见生产故障与解决方案

监控端配置自洽:prometheus-agent.yaml 的 relabel 注入 tenant/app/env/cluster 标签,中心 prometheus-federation.yaml 联邦拉取,与 Go metrics.go 暴露的 paas_cluster_health / paas_cluster_agent_up 组合形成告警。

# file: observability/prometheus-agent.yaml
# 集群内 Prometheus Agent:采集本集群指标,强制注入 tenant/app/env/cluster 标签
apiVersion: v1
kind: ConfigMap
metadata:
  name: prom-agent-prod-cluster-01
  namespace: paas-monitoring
data:
  prometheus.yml: |
    global:
      scrape_interval: 30s
      external_labels:
        cluster: prod-cluster-01
        env: production
    remote_write:
      - url: http://thanos-receiver.paas-monitoring.svc:19291/api/v1/receive
    scrape_configs:
      - job_name: kube-pods
        kubernetes_sd_configs:
          - role: pod
        relabel_configs:
          - source_labels: [__meta_kubernetes_namespace]
            target_label: tenant
          - source_labels: [__meta_kubernetes_pod_label_app]
            target_label: app
          - target_label: env
            replacement: production
          - target_label: cluster
            replacement: prod-cluster-01    
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prom-agent-prod-cluster-01
  namespace: paas-monitoring
  labels:
    app: prom-agent
    paas.io/cluster: prod-cluster-01
spec:
  replicas: 1
  selector:
    matchLabels:
      app: prom-agent
      paas.io/cluster: prod-cluster-01
  template:
    metadata:
      labels:
        app: prom-agent
        paas.io/cluster: prod-cluster-01
    spec:
      containers:
      - name: agent
        image: prom/prometheus:v2.51.0
        args: ["--config.file=/etc/prometheus/prometheus.yml", "--enable-feature=agent"]
        volumeMounts:
        - name: cfg
          mountPath: /etc/prometheus
      volumes:
      - name: cfg
        configMap:
          name: prom-agent-prod-cluster-01
# file: observability/prometheus-federation.yaml
# 中心 Prometheus 联邦拉取各集群 Agent,honor_labels 保留 tenant/env/cluster 源标签
global:
  scrape_interval: 30s
scrape_configs:
  - job_name: federate-prod
    honor_labels: true
    metrics_path: /federate
    params:
      match[]:
        - '{__name__=~"paas_.*|kube_.*"}'
    static_configs:
      - targets: ['prom-agent-prod.paas-monitoring.svc:9090']
  - job_name: federate-test
    honor_labels: true
    metrics_path: /federate
    params:
      match[]:
        - '{__name__=~"paas_.*|kube_.*"}'
    static_configs:
      - targets: ['prom-agent-test.paas-monitoring.svc:9090']
# file: observability/recording-rules.yaml
groups:
  - name: paas_cluster_recording
    rules:
      - record: paas:cluster:health_ratio
        expr: sum(paas_cluster_health) / count(paas_cluster_health)
      - record: paas:cluster:agent_up_count
        expr: sum by (env) (paas_cluster_agent_up)
# file: observability/alert-rules.yaml
groups:
  - name: paas_cluster_alerts
    rules:
      - alert: ClusterDegraded
        expr: paas_cluster_health == 0
        for: 5m
        labels:
          severity: warning
          team: paas-sre
        annotations:
          summary: "集群 {{ $labels.cluster }} 降级,请检查反向隧道/防火墙"
      - alert: ClusterEvicted
        expr: paas_cluster_health == -1
        for: 1m
        labels:
          severity: critical
          team: paas-sre
        annotations:
          summary: "集群 {{ $labels.cluster }} 已摘除,触发流量隔离"
      - alert: PrometheusAgentDown
        expr: paas_cluster_agent_up == 0
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "集群 {{ $labels.cluster }} Prometheus Agent 离线,联邦监控缺口"
# file: observability/alertmanager.yaml
global:
  resolve_timeout: 5m
route:
  receiver: default
  group_by: ['cluster', 'env']
  routes:
    - matchers: ['env="production"']
      receiver: prod-oncall
      continue: true
    - matchers: ['severity="critical"']
      receiver: crit-page
receivers:
  - name: default
    webhook_configs:
      - url: http://paas-notify.svc/api/v1/alert
  - name: prod-oncall
    webhook_configs:
      - url: http://paas-notify.svc/api/v1/wechat
  - name: crit-page
    pagerduty_configs:
      - routing_key: ${PAGERDUTY_KEY}

故障 1:纳管状态显示 degraded,apiserver 探活失败

  • 排查:先查网关到集群的反向隧道是否中断(agent 进程/网络);再查客户侧防火墙是否放通网关出连。对应 paas_cluster_health == 0
  • 优化:agent 心跳超时阈值调优 + 多活网关,避免单点。

故障 2:联邦监控看不到某集群指标

  • 排查:查该集群 Prometheus Agent 是否部署成功、remote_write 地址是否指向中心、标签 cluster 是否注入;查 Thanos Store 是否挂载对应对象存储。对应 paas_cluster_agent_up == 0
  • 优化:纳管流程把「Agent 就绪 + 首条指标到达」作为注册完成的验收项。

故障 3:某租户误占满集群资源,影响其他租户

  • 排查:查 ResourceQuota 是否真的下发;查是否有 Pod 未设 requests 导致配额失效。
  • 优化:准入控制(OPA/Gatekeeper)强制所有 Pod 设 requests/limits,超配额直接拒绝。

完整运维 runbook(runbook/cluster-ops.sh)覆盖章节四 SOP 与本章排查:

#!/usr/bin/env bash
# file: runbook/cluster-ops.sh
set -euo pipefail

# ---- 章节四 SOP:注册测试集群 ----
curl -X POST https://paas-gateway/api/v1/clusters \
  -H "Content-Type: application/json" \
  -d '{"id":"test-cluster-01","env":"test","endpoint":"https://10.10.10.10:6443","mode":"reverse-tunnel","kubeconfig":"<in-memory-only>"}'
# 校验 Namespace 与配额
kubectl --context test-cluster-01 get ns subcompany-a --show-labels
kubectl --context test-cluster-01 get resourcequota -n subcompany-a

# ---- 环境划分与节点池 ----
paas-cli cluster label --name prod-cluster-01 --set env=production
paas-cli nodepool create --cluster prod-cluster-01 --name np-gold --zone az1

# ---- 跨集群查询(经网关,无需直连 apiserver)----
curl -s https://paas-gateway/proxy/prod-cluster-01/api/v1/namespaces

# ---- 故障1:查反向隧道/防火墙/agent ----
kubectl --context prod-cluster-01 -n paas-system get pods -l app=cluster-agent -o wide
curl -s 'http://prom-center:9090/api/v1/query?query=paas_cluster_health' | jq '.data.result[] | {cluster:.metric.cluster, health:.value[1]}'

# ---- 故障2:查 Agent 在线情况 ----
curl -s 'http://prom-center:9090/api/v1/query?query=paas_cluster_agent_up' | jq '.data.result[] | {cluster:.metric.cluster, up:.value[1]}'
kubectl --context prod-cluster-01 -n paas-monitoring get deploy -l app=prom-agent

# ---- 故障3:查租户配额与 Pod requests ----
kubectl --context prod-cluster-01 -n subcompany-a get resourcequota -o yaml
kubectl --context prod-cluster-01 -n subcompany-a get pods -o json \
  | jq '.items[] | select(.spec.containers[].resources.requests==null) | .metadata.name'

# ---- 查询审计(≥90 天)----
grep '"action":"register-cluster"' /var/log/paas/cluster-audit.jsonl | tail -5

故障排查统一流程:

flowchart TD
    A[纳管异常告警] --> B{网关到集群连通?}
    B -->|否| C[查反向隧道/防火墙/agent]
    B -->|是| D{Agent 指标到达?}
    D -->|否| E[查 Agent 部署与 remote_write]
    D -->|是| F[查 RBAC/配额/标签]
    C --> G[恢复隧道并重试]
    E --> G
    F --> G
    G --> H[回归验证 + 写故障单]

自测题与动手练习

5 道自测题

  1. 统一 K8s API 网关采用什么模式收敛多集群 apiserver?为何客户防火墙只需放通网关方向?
  2. kubeconfig 加密托管用了哪种加密方式?明文是否会落盘?
  3. 集群注册时,生产环境与测试环境在审批与证书上有何差异?
  4. 联邦 Prometheus 下,如何保证不同物理集群的指标汇入同一视图并按 cluster 过滤?
  5. 故障集群自动摘除后,存量流量如何处理?恢复需要什么动作?

3 个动手练习

  1. paas-cli cluster register 纳管一套测试集群,验证 kubeconfig 上传后本地文件被标记为「已加密托管」(对照 kms.go 信封加密)。
  2. 通过网关(而非直连 apiserver)跨集群 get namespace,确认你无需知道各集群地址(/proxy/:cluster/*path)。
  3. 在 Grafana 用 cluster=test-cluster-01 过滤,确认该集群指标已进联邦视图;手动停掉 Agent,观察告警是否触发(PrometheusAgentDown)。

本章小结

  • 多集群纳管是 PaaS 的神经中枢:把物理隔离的 K8s 集群收敛为统一入口。
  • 统一 K8s API 网关用反向隧道/中心代理(remotedialer 思路)收敛 apiserver,统一鉴权/限流/审计;Go 用 rest.Config + Clientset/DynamicClient 按集群 ID 路由。
  • 四环境(生产/预发布/测试/开发)物理隔离,是等保合规底线。
  • kubeconfig KMS 信封加密托管(明文仅内存、用完即焚),租户 Namespace 配额随纳管下发,跨集群网络默认隔离。
  • 纳管即接入联邦 Prometheus Agent,指标带 tenant/app/env/cluster 标签进中心;Go 暴露 paas_cluster_health/paas_cluster_agent_up 与告警规则名一致;故障集群自动摘除隔离。

下一章《应用全生命周期管理模块》将基于本模块的集群与网关,讲解应用如何被建模、安全发布与弹性伸缩——而这一切都运行在你刚刚纳管的物理隔离集群之上。

About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!