首页
/ 基于 k8s.io/apiserver 构建扩展 API Server:sample-apiserver(Wardle)从源码到部署全解析

基于 k8s.io/apiserver 构建扩展 API Server:sample-apiserver(Wardle)从源码到部署全解析

2026-09-07 21:53:58作者:伍霜盼Ellen

sample-apiserver 是 Kubernetes 官方提供的一个"最小可运行"的演示性 API Server,它演示了如何使用 k8s.io/apiserver 通用库从头构建一个符合 Kubernetes 约定(REST 语义、版本化、验证、鉴权、etcd 存储、OpenAPI)的独立 API 服务器。本文将以其在 Kubernetes 仓库中的源码位置(staging/src/k8s.io/sample-apiserver)与官方 README 为核心骨架,结合仓库内真实源码与 artifacts/ 部署清单,系统讲解它的设计动机、API 类型定义、代码生成、集群内聚合部署(API Aggregation)与单机自签名证书运行(X.509 认证)的完整流程。读完本文,你将能够:判断"CRD vs 扩展 API Server"的技术选型、fork 并定制这套样板代码为自己的 API 类型、看懂它从 main.goregistry/strategy 的启动链路,并能在本地把它跑起来。

本文所有路径均相对于 Kubernetes 仓库根目录;代码副本位于 staging/src/k8s.io/sample-apiserver

sample-apiserver 解决什么问题

README.md 所述,sample-apiserver 的核心价值是:

Demonstration of how to use the k8s.io/apiserver library to build a functional API server.

即:演示如何用 k8s.io/apiserver 库构建一个"功能完整"的 API Server。它的目标读者是两类开发者:

  • 想构建 Extension API Server 并通过 API Aggregation(API 聚合) 接入 Kubernetes 集群的人;
  • 想构建 独立运行的、Kubernetes 风格的 API Server 的人。

示例中的 API 分组名为 wardle.example.com,因此它也常被称为 "wardle server"。仓库中注册了两类演示资源:

  • Flunder:一个带 spec/status 的命名空间级(namespaced)示例资源,还支持 flunder 与 fischer 两种引用;
  • Fischer:一个**非命名空间级(cluster-scoped)**的示例资源,持有"禁止创建的 Flunder 名称列表"。

这些资源类型在 pkg/apis/wardle/types.go(内部版本)与 pkg/apis/wardle/v1alpha1/types.gov1beta1 对应文件中定义。

先想清楚:CRD、apiserver-builder 还是 sample-apiserver

README 开篇非常坦诚地提醒使用者,在动手之前先评估另外两条"少走弯路"的路线:

  1. CRD(Custom Resource Definition):如果只是想往 Kubernetes 集群里"加一种资源",优先考虑 CRD。它的编码量与后续 rebase 维护成本都远低于扩展 API Server。关于两者差异,可参考 Kubernetes 官方文档中 Custom Resources 一节对 CRD 与 API 聚合(aggregation)的阐述。
  2. apiserver-builder:如果确定要构建扩展 API Server,官方曾孵化过 apiserver-builder 框架,它可以自动生成 apiserver、client 库与安装程序,比"手工 fork 本仓库"更省事。注意该框架曾托管于 kubernetes-incubator 组织。

如果最终仍选择基于 sample-apiserver 开发,README 推荐的模式是:fork 本仓库 → 修改 pkg/apis/.../types.go 加入自己的类型 → 定期 rebase 到上游,从而持续获取 apiserver 库的改进与 bug 修复。

兼容性与来源

README 明确了两个重要约束:

  • 本仓库 HEAD 与 k8s.io/apiserverk8s.io/apimachineryk8s.io/client-go 的 HEAD 保持对齐,因此当你升级这些依赖版本时,需要同步考虑与 sample-apiserver 的配套关系;
  • 它本质上是 Kubernetes 主仓库 staging/src/k8s.io/sample-apiserver 目录被同步到独立外部仓库的产物(staged repository),代码的修改入口在主仓库的 staging 目录,先合入 Kubernetes,再同步发布。因此本仓库只读,不用于直接贡献。贡献相关约定见 staging/src/k8s.io/sample-apiserver/CONTRIBUTING.md

从当前仓库看,该目录的 go module 名为 k8s.io/sample-apiserver(见 go.mod),因此 README 特别提醒:go-get 或以 vendor 方式引入此包时,路径一律写作 k8s.io/sample-apiserver

获取代码与准备依赖

如果你已经在按 Kubernetes 社区标准的 fork-and-branch 流程开发,那么本演示代码其实就躺在主仓库的 staging/src/k8s.io/sample-apiserver,其依赖(包括 code-generator)也都处于可用的 GOPATH 布局中,无需额外 clone。

如果要在自己的工作区独立获取该库副本,流程是:

git clone <sample-apiserver 外部仓库地址>
cd sample-apiserver

Go modules 场景下的特殊提醒

README 指出:如果打算修改类型后重新生成代码(generate code),那么 code-generator 仓库必须存在于"旧式"位置(GOPATH 风格)。一个便捷做法是执行 go mod vendor 生成并填充 vendor 目录,让脚本能在传统路径下定位 code-generator。这一要求直接对应 hack/update-codegen.sh 中的探测逻辑:

CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}

脚本会优先在仓库自身 vendor/k8s.io/code-generator 下寻找 code-generator,找不到则退回相对路径 ../code-generator

理解并定制 API 类型:Flunder 与 Fischer

开发扩展 API Server 的第一步是定义你自己的资源类型。sample-apiserver 用一个"动物园命名"(Wardle)展示了两类典型资源形态。

内部版本类型定义

内部版本(internal version)定义在 pkg/apis/wardle/types.go,其中:

  • Flunder(命名空间级)含 SpecStatus,注释标记 // +genclient 表示需要生成 client,// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 表示需要生成 deepcopy;
  • Fischer(集群级)用 // +genclient:nonNamespaced 标记非命名空间语义,含 DisallowedFlunders []string
  • FlunderSpec 通过 FlunderReference / FischerReference 两个互斥字段与 ReferenceType 枚举,演示"一个 spec 里如何做引用建模"。
type FlunderSpec struct {
	FlunderReference string       // 另一个 flunder 的名字,与 FischerReference 互斥
	FischerReference string       // 一个 fischer 的名字,与 FlunderReference 互斥
	ReferenceType    ReferenceType
}

// +genclient
// +genclient:nonNamespaced
type Fischer struct {
	metav1.TypeMeta
	metav1.ObjectMeta
	DisallowedFlunders []string // 被禁止的 Flunder.Name 列表
}

对外版本(v1alpha1 / v1beta1)

对外可序列化版本放在 pkg/apis/wardle/v1alpha1pkg/apis/wardle/v1beta1 子目录。以 v1alpha1/types.go 为例,它给每个字段补充了 JSON / protobuf tag,并通过 // +k8s:prerelease-lifecycle-gen:introduced=1.0removed=1.10 注释声明 API 生命周期(引入版本 / 移除版本,配合 zz_generated.prerelease-lifecycle.go 供生命周期验证使用)。注意 v1alpha1 与内部版本在 spec 形状上有差异(v1alpha1 把引用收敛为单个 Reference string + 可空 ReferenceType *ReferenceType),这种差异正是通过 conversion(见 v1alpha1/conversion.go 与生成的 zz_generated.conversion.go)与 defaulting(defaults.go / zz_generated.defaults.go)来弥合的——这也说明了为什么要为每个版本维护一套独立的 types 与生成物。

类型注册

pkg/apis/wardle/register.go 定义了组名常量:

const GroupName = "wardle.example.com"

并通过 SchemeBuilderFlunder/FlunderList/Fischer/FischerList 注册进 runtime scheme。所有版本(v1alpha1、v1beta1)的 types 最终都要能被 install 包统一装入全局 Scheme。

何时需要重新生成代码

README 明确:只要修改了任意 pkg/apis/.../types.go 中的 API 类型定义,就必须重新生成派生文件(deepcopy、conversion、defaults、applyconfiguration、clientset、informers、listers、openapi 定义等)。操作步骤:

  1. 如尚未有 vendor 目录,先按上文创建(Go modules 场景);
  2. 以 sample-apiserver 作为当前工作目录,执行 hack/update-codegen.sh脚本不接受参数)。

该脚本内部(见 hack/update-codegen.sh)按顺序调用 kube::codegen::gen_helpers(deepcopy 等基础生成)、gen_openapi(输出到 pkg/generated/openapi,模型名文件为 zz_generated.model_name.go,并支持通过 API_KNOWN_VIOLATIONS_DIR / --update-report 维护已知违规清单)、gen_client--with-watch --with-applyconfig,生成 clientset / informers / listers / applyconfiguration,落到 pkg/generated 下)。也就是说,你改完类型后,pkg/generated/ 整棵树的产物都应视作"需要重新生成而非手改"。

当前仓库中的生成产物确实覆盖了这些品类,例如:

常规构建与部署(Normal Build and Deploy)

启动入口与参数体系:从 main.go 到 RecommendedOptions

程序入口 main.go 非常精简:

func main() {
	ctx := genericapiserver.SetupSignalContext()
	options := server.NewWardleServerOptions(os.Stdout, os.Stderr)
	cmd := server.NewCommandStartWardleServer(ctx, options, false)
	code := cli.Run(cmd)
	os.Exit(code)
}

真正的组装在 pkg/cmd/server/start.go。这里有几个值得深入理解的设计点:

  • NewWardleServerOptions 通过 genericoptions.NewRecommendedOptions(defaultEtcdPathPrefix, ...) 一次性获得 kube-apiserver 同款的推荐命令行选项集(etcd 存储、安全端口、认证、授权、准入、审计等),etcd 键前缀为常量 defaultEtcdPathPrefix = "/registry/wardle.example.com"
  • NewCommandStartWardleServer 把这些选项注册成 cobra 命令的 flags,因此你的自定义 server 天然获得与官方组件一致的 --secure-port--etcd-servers--client-ca-file--kubeconfig--authentication-kubeconfig--authorization-kubeconfig--v(日志级别)等参数;
  • 它还演示了 KEP-4330 的组件版本与 FeatureGate 机制:注册 "Wardle" 组件的 effective version(二进制版本 1.2、emulation version、最低兼容版本),并声明了一个从 Alpha(1.0 默认关)→ Beta(1.1 默认开)→ GA(1.2 锁定为开)演进的示例 FeatureGate BanFlunder(见 start.goWardleVersionToKubeVersion 的映射逻辑与 BanFlunder 的版本化规格)。

BanFlunder 这个开关被 Complete() 使用:当它开启时才向准入插件链注册 banflunder 插件(见下)。换言之,你可以把这段样板当作"如何在自定义 server 里挂接版本化 feature gate + 准入插件"的活教材。

API 组装与存储注册

pkg/apiserver/apiserver.go 演示了"如何把一个自定义 API group 安装进通用 apiserver":

  1. init()install.Install(Scheme) 装入所有 wardle 版本类型,并补齐 metav1 与无版本类型;
  2. Config.Complete() 完成通用配置;
  3. New() 调用 c.GenericConfig.New("sample-apiserver", ...) 创建底层 GenericAPIServer
  4. 构造 apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(wardle.GroupName, Scheme, ...)
  5. v1alpha1v1beta1 分别建立"资源名 → REST storage"映射:flundersfischers 都对应各自的 etcd REST 实现,其中 fischerstorage.NewREST 是集群级资源、flunderstorage.NewREST 是命名空间级资源(注意 v1alpha1 暴露 flunders+fischers,而 v1beta1 只暴露 flunders);
  6. 最后 s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo) 把整个 API group 安装进 HTTP 路由树。

REST 存储的"策略"部分(validation、namespace-scoped、create/update 语义)在 pkg/registry/wardle/flunder/strategy.go 实现:flunderStrategy.NamespaceScoped() 返回 trueValidate 委托给 pkg/apis/wardle/validation/validation.go;底层的 etcd 存取由 pkg/registry/wardle/flunder/etcd.gogeneric.NewRegistry 把 strategy 接到通用存储后端,从而获得 list/watch/create/update/delete 全套 REST 能力。这即是"从结构上理解 k8s.io/apiserver:types → scheme → registry(strategy+storage) → InstallAPIGroup"这条主线。

认证插件(Authentication plugins)

默认构建只支持非常精简的认证方式集合;k8s.io/client-goplugin/pkg/client/auth 下还有大量认证插件(如 oidc)。如需启用其中某一种(例如 oidc),在 main.go 增加对应包的空导入即可:

import _ "k8s.io/client-go/plugin/pkg/client/auth/oidc"

如果想一次性启用全部认证插件:

import _ "k8s.io/client-go/plugin/pkg/client/auth"

这利用了 Go 包 init() 机制:空导入会触发插件向认证层注册。

构建二进制

以 sample-apiserver 为当前工作目录,执行(输出到容器构建目录):

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o artifacts/simple-image/kube-sample-apiserver

禁用 CGO、交叉编译到 linux/amd64,产出静态二进制 kube-sample-apiserver,可直接打进容器。本地纯测试也可以只执行 export GOOS=linux; go build . 得到当前目录下的 sample-apiserver 二进制(此方式在 docs/minikube-walkthrough.md 有配套说明)。

构建并推送容器镜像

artifacts/simple-image/Dockerfile 极简——基于 fedora 镜像,把二进制放到根目录作为 entrypoint:

FROM fedora
ADD kube-sample-apiserver /
ENTRYPOINT ["/kube-sample-apiserver"]

MYPREFIXMYTAG 替换为你的镜像仓库与标签后执行:

docker build -t MYPREFIX/kube-sample-apiserver:MYTAG ./artifacts/simple-image
docker push MYPREFIX/kube-sample-apiserver:MYTAG

部署进 Kubernetes 集群(API Aggregation)

编辑 artifacts/example/deployment.yaml,把 pod 模板中的 image 改成刚才推送的镜像、把 imagePullPolicy 设为合适值,然后一次性应用整个 artifacts/example 目录:

kubectl apply -f artifacts/example

artifacts/example/ 下各清单的职责(结合仓库文件逐一说明,其中大部分在 minikube walkthrough 中被依次创建):

文件 作用
ns.yaml 命名空间 wardle,隔离运行扩展 apiserver
sa.yaml ServiceAccount apiserver,pod 以其身份运行(serviceAccountName: apiserver
auth-delegator.yaml ClusterRoleBinding,把 system:auth-delegator ClusterRole 授予该 SA,允许它把鉴权委托回主 apiserver
auth-reader.yaml kube-system 下的 RoleBinding,授予 extension-apiserver-authentication-reader,让扩展 server 能读取主 apiserver 的认证配置(configmap)
rbac.yaml ClusterRole aggregated-apiserver-clusterrole:允许 list/watch namespacesadmissionregistration.k8s.io 相关资源以及 flowcontrol.apiserver.k8s.io 的 flowschemas/prioritylevelconfigurations(供共享 informer 与准入控制器使用)
rbac-bind.yaml ClusterRoleBinding,把上述 ClusterRole 绑定给 wardle 命名空间的 apiserver SA
deployment.yaml Deployment wardle-server:sidecar 方式同 Pod 运行 etcd(registry.k8s.io/etcd:v3.7.0)与 kube-sample-apiserver,参数 --etcd-servers=http://localhost:2379;这就是演示用的存储方案
service.yaml Service api(443→443),把扩展 apiserver 暴露给集群内聚合器(kube-apiserver)
apiservice.yaml APIService v1alpha1.wardle.example.com:告诉主 apiserver 该 API group 存在于哪个 Service,从而完成聚合注册

其中 apiservice.yaml 的关键字段为:

apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
  name: v1alpha1.wardle.example.com
spec:
  insecureSkipTLSVerify: true
  group: wardle.example.com
  groupPriorityMinimum: 1000
  versionPriority: 15
  service:
    name: api
    namespace: wardle
  version: v1alpha1

创建 APIService 后,kube-apiserver 就会把指向 /apis/wardle.example.com/v1alpha1/... 的请求代理到 wardle/api 这个 Service。

验证:创建并读取 Flunder

仓库在 artifacts/flunders/01-flunder.yaml 提供了一个现成的示例资源:

apiVersion: wardle.example.com/v1alpha1
kind: Flunder
metadata:
  name: my-first-flunder
  labels:
    sample-label: "true"

聚合生效后即可用 kubectl 直接操作新资源:

kubectl create -f artifacts/flunders/01-flunder.yaml
# flunder "my-first-flunder" created

kubectl get flunder my-first-flunder
# NAME               KIND
# my-first-flunder   Flunder.v1alpha1.wardle.example.com

完整的 minikube 演练(预置条件、创建 namespace/SA/RBAC/Service/APIService 的逐条命令)可参考 docs/minikube-walkthrough.md

准入控制演示:banflunder

为了演示扩展 server 如何注册自定义准入插件,仓库提供了 banflunder 插件(pkg/admission/plugin/banflunder/admission.go):当某个 FischerDisallowedFlunders 中列出了一个名字时,禁止创建同名 Flunder。插件通过 wardleinitializerpkg/admission/wardleinitializer/wardleinitializer.go)被注入 informer 以获取 Fischer 列表,注册与否受上文 BanFlunder FeatureGate 控制。配套的单元测试位于 admission_test.go,是理解"扩展 apiserver 里准入链如何工作"的极佳入口。

独立运行模式(Running it stand-alone)

开发阶段把扩展 server 独立跑起来(不依赖主 kube-apiserver 做认证/授权、不做聚合)会方便很多。README 给出了完整可操作的方案:放弃聚合器内嵌的认证链,改用本地客户端证书的 X.509 认证与授权

原理:客户端证书被一个自建 CA 信任,并且证书中携带了 system:masters 群组成员身份。由于用 --authorization-skip-lookup 禁用了委派授权查询,唯一被授权的就是 system:masters 这个超级用户组。该模式仍需要一份 kubeconfig(如 ~/.kube/config),但 Kubernetes 集群只被用于共享 informer 读取资源,不参与认证/授权;用 minikube 或主仓库 hack/local-up-cluster.sh 起一个集群即可满足。

第一步:生成自建 CA

openssl req -nodes -new -x509 -keyout ca.key -out ca.crt

第二步:为超级用户签发客户端证书

为用户 development、超级用户组 system:masters 签发由上述 CA 签名的客户端证书:

openssl req -out client.csr -new -newkey rsa:4096 -nodes -keyout client.key -subj "/CN=development/O=system:masters"
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -sha256 -out client.crt

注意 -subj 中的 /O=system:masters 正是把用户划入超级用户组的关键。

第三步:导出 p12(供 curl 使用)

curl 需要带口令的 p12 格式客户端证书,因此做一次转换:

openssl pkcs12 -export -in ./client.crt -inkey ./client.key -out client.p12 -passout pass:password

第四步:启动 etcd 与 sample-apiserver

etcd &
sample-apiserver --secure-port 8443 --etcd-servers http://127.0.0.1:2379 --v=7 \
   --client-ca-file ca.crt \
   --kubeconfig ~/.kube/config \
   --authentication-kubeconfig ~/.kube/config \
   --authorization-kubeconfig ~/.kube/config

三个 kubeconfig 各司其职(README 特别说明):

  • 第一个(--kubeconfig):供共享 informer 访问 Kubernetes 资源;
  • 第二个(--authentication-kubeconfig):用于满足委派认证器的配置要求;
  • 第三个(--authorization-kubeconfig):用于满足委派授权器的配置要求。

但由于 --client-ca-file 的存在,你的开发用 X.509 证书会被直接接受并认证为 system:masters 成员;system:masters 是超级用户组,因此委派授权会被跳过——认证器与授权器实际上都不会被调用。

第五步:访问扩展 API

用 p12 客户端证书调用 curl:

curl -fv -k --cert-type P12 --cert client.p12:password \
   https://localhost:8443/apis/wardle.example.com/v1alpha1/namespaces/default/flunders

或改用 wget(直接用 PEM 证书与私钥):

wget -O- --no-check-certificate \
   --certificate client.crt --private-key client.key \
   https://localhost:8443/apis/wardle.example.com/v1alpha1/namespaces/default/flunders

备注:部分新版 macOS 的 curl 处理客户端证书存在问题。在 Mac 上可改用 httpie(brew install httpie):

http --verify=no --cert client.crt --cert-key client.key \
   https://localhost:8443/apis/wardle.example.com/v1alpha1/namespaces/default/flunders

把样板改造成你自己的 API Server:推荐路径小结

综合 README 与仓库源码,落地一个自定义扩展 API Server 的推荐路径是:

  1. 评估替代方案:仅需新资源 → CRD;完整 API 语义/版本化/准入/自定义子资源 → Extension API Server。
  2. fork 本仓库副本(当前仓库只读,作为 staging 镜像用于同步),以 k8s.io/sample-apiserver 作为 module 名引入。
  3. 修改类型:在 pkg/apis/<group>/types.go 定义内部类型与对外版本(v1alpha1/v1beta1/...),补全 json/protobuf tag 与 +genclient+k8s:prerelease-lifecycle-gen:* 等注释;需要的话实现自己的 strategy(命名空间作用域、校验、默认值、转换)与自定义准入插件。
  4. 重新生成代码:准备 vendor 后运行 hack/update-codegen.sh,刷新 deepcopy/conversion/clientset/informers/listers/applyconfiguration/openapi。
  5. 组装与部署:沿用 pkg/apiserver/apiserver.goInstallAPIGroup 模式安装你的 group;用 artifacts/example/ 的清单模板(namespace/SA/RBAC/auth-delegator/auth-reader/Deployment/Service/APIService)接入 API Aggregation,或用 README 的 X.509 独立模式做本地联调。
  6. 周期性 rebase 到上游,持续跟进 k8s.io/apiserver 的功能与安全修复。

总结

sample-apiserver 的价值不在于"能跑一个 wardle 服务",而在于它把 k8s.io/apiserver 的使用方法浓缩成一套可 fork、可改类型、可重新生成、可一键部署的最小工程。从 main.go 的信号上下文与 RecommendedOptions,到 apiserver.go 的 scheme/registry/storage 注册,再到 start.go 对版本兼容性与 FeatureGate 的示范,每一层都对应着你在生产级扩展 API Server 中会遇到的实际问题。无论你的目标是接入 API Aggregation 成为集群的"第二控制面",还是构建完全独立的 Kubernetes 风格 API 服务,这份样板代码连同 artifacts/ 清单、docs/minikube-walkthrough.md 演练,都是官方给出的最佳起点。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388