Kubernetes 基础体系 · 第 55/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kustomize 配置管理:Base、Overlay、Patch、Generator 和边界
Kustomize 是 Kubernetes 原生配置定制工具。它读取一组 Kubernetes YAML,按照声明式规则进行组合、变换和生成,最后输出一组可以交给 kubectl 或 GitOps 控制器应用的 YAML。
它与 Helm 的核心区别是:Kustomize 通常不把 YAML 当作模板执行,而是把 YAML 解析成 Kubernetes 对象,再对对象进行结构化修改。它没有 Helm 那样的 Chart、Values、Release 和模板函数体系,因此更适合“已有 YAML 的环境差异管理”,但不适合所有类型的配置复用。
一、先建立模型:Kustomize 处理的是什么
Kustomize 的输入通常是一个目录,其中包含:
kustomization.yaml:描述资源、补丁、生成器和变换规则;- Kubernetes 资源文件;
- 其他被引用的 Base 或 Overlay;
- 生成器所需的文件、字面量或环境变量文件。
最小的 kustomization.yaml 可以是:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
这里的 apiVersion 是 Kustomize 配置文件的 API 版本,不是 Kubernetes 工作负载的 API 版本。它与资源中的:
apiVersion: apps/v1
kind: Deployment
属于两套不同的 API 体系。
Kustomize 的基本处理模型可以抽象为:
其中:
- 是读取到的原始资源集合;
- 是资源累加过程,例如加载
resources引用的 Base; - 是某一个变换器,例如补丁、名称前缀、镜像替换或命名空间变换;
- 是最终输出的 Kubernetes YAML。
这个模型表达了三个重要事实:
- Kustomize 不是把字符串直接替换后输出;
- 资源之间的引用可能随名称变换一起更新;
- 最终结果取决于多个变换的组合,而不是单个文件的内容。
实际变换顺序由 Kustomize 内部的资源加载器和变换器决定,不应把它当成可以任意控制的脚本执行顺序。对于有顺序依赖的复杂修改,应通过明确的资源结构和补丁目标消除歧义,而不是依赖某个版本的内部排序。
二、Base 和 Overlay:复用与环境差异的结构
1. Base 是可复用的共同配置
Base 表示一组与环境无关,或者至少可以被多个环境共同使用的资源。
目录示例:
app/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
└── overlays/
├── staging/
│ ├── kustomization.yaml
│ └── replicas.yaml
└── production/
├── kustomization.yaml
└── replicas.yaml
base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
base/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app.kubernetes.io/name: web
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: web
template:
metadata:
labels:
app.kubernetes.io/name: web
spec:
containers:
- name: web
image: nginx:1.27
ports:
- name: http
containerPort: 80
base/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app.kubernetes.io/name: web
ports:
- name: http
port: 80
targetPort: http
Base 本身也可以直接构建:
kubectl kustomize base
或者:
kustomize build base
前者使用当前 kubectl 内置的 Kustomize 版本,后者使用独立安装的 Kustomize。两者的支持能力可能不同,因此生产构建应固定工具版本,并在 CI 中验证生成结果。
2. Overlay 是针对某个环境或部署场景的入口
Overlay 通过 resources 引用 Base,然后叠加环境差异。
overlays/staging/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: staging
namePrefix: staging-
resources:
- ../../base
patches:
- path: replicas.yaml
target:
group: apps
version: v1
kind: Deployment
name: web
overlays/staging/replicas.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
构建:
kubectl kustomize overlays/staging
输出中的 Deployment 会具有类似结果:
apiVersion: apps/v1
kind: Deployment
metadata:
name: staging-web
namespace: staging
spec:
replicas: 2
Service 也会得到 staging- 前缀和 staging 命名空间。
这里的 metadata.name: web 是补丁匹配用的原始资源名;补丁匹配发生在资源名称变换的语义范围内,不能简单地把目标写成最终输出的 staging-web。如果补丁同时指定了 target,应让 target 明确描述原始对象的 Group、Version、Kind 和 Name。
3. Base 与 Overlay 的边界
Base 适合放:
- 工作负载结构;
- Service;
- 通用标签;
- 通用探针;
- 默认容器端口;
- 所有环境都需要的 RBAC 或 ServiceAccount。
Overlay 适合放:
- 副本数;
- 镜像版本;
- 命名空间;
- 环境变量;
- 资源请求和限制;
- Ingress 域名;
- 生产环境才需要的补丁。
不应把某个环境的绝对路径、云厂商专用注解或生产域名放进 Base,否则 Base 的复用性会下降,Overlay 也会被迫通过反向覆盖来修正错误默认值。
一个 Overlay 引用另一个 Overlay 通常会使层级关系难以推断。更稳妥的结构是:多个环境共同引用同一个 Base,环境之间共享的差异再抽成 Component 或新的中间 Base,而不是让 production 继承 staging。
三、Patch:对结构化 Kubernetes 对象进行修改
Patch 是 Kustomize 修改已有资源的机制。现代写法统一使用 patches 字段,它可以承载不同类型的补丁。旧字段 patchesStrategicMerge 和 patchesJson6902 仍可能在旧项目中出现,但应注意版本兼容和弃用状态,新的配置优先使用 patches。
1. Strategic Merge Patch
Strategic Merge Patch 按 Kubernetes 对象结构合并 YAML。例如:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
template:
spec:
containers:
- name: web
resources:
requests:
cpu: 100m
memory: 128Mi
如果它被应用到原始 Deployment:
spec:
replicas: 1
template:
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
合并结果的核心部分是:
spec:
replicas: 3
template:
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests:
cpu: 100m
memory: 128Mi
containers 这类列表通常依据 Kubernetes 的结构化合并键进行合并,Deployment 容器列表的键通常是 name。因此 name: web 的容器被修改,而不是简单地把整个列表追加一遍。
可以使用显式的 $patch 指令:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
template:
spec:
containers:
- name: web
$patch: replace
image: nginx:1.27
ports:
- name: http
containerPort: 80
这表示用补丁中的容器列表替换原有列表,而不是按容器名合并。
2. JSON Patch
JSON Patch 使用 RFC 6902 风格的操作,适合表达精确修改:
- op: replace
path: /spec/replicas
value: 3
对应 Kustomization:
patches:
- target:
group: apps
version: v1
kind: Deployment
name: web
patch: |-
- op: replace
path: /spec/replicas
value: 3
JSON Patch 的路径必须真实存在。以下操作的失败条件不同:
- op: replace
path: /spec/template/spec/containers/0/image
value: nginx:1.27
要求数组中确实存在第一个容器以及 image 字段;如果资源结构不同,构建会失败。
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: LOG_LEVEL
value: info
要求 env 数组已存在。若 env 不存在,不能假设 /- 会自动创建父数组,通常需要先添加 /env,或使用更合适的结构化补丁。
JSON Patch 的优点是路径精确,缺点是依赖数组索引。容器顺序变化后,/containers/0 可能不再指向原来的容器。
3. CRD 是 Patch 的重要边界
Strategic Merge Patch 依赖 Kubernetes 类型的合并语义。对于许多内置资源,Kustomize 知道列表如何按键合并;对于自定义资源,Kustomize 通常不能自动获得完整的 Kubernetes OpenAPI 合并信息。
因此对 CRD 的列表字段使用 Strategic Merge Patch 可能得到与预期不同的结果,例如列表整体替换、重复追加或字段合并不符合业务语义。修改 CRD 实例时,应优先:
- 使用 JSON Patch;
- 使用 CRD 明确支持的结构;
- 在目标集群和固定 Kustomize 版本上执行构建测试;
- 不把“看起来像 Kubernetes YAML”误认为“具有内置资源的合并语义”。
4. Patch 的匹配必须唯一
如果一个补丁匹配多个对象,结果可能不是预期的单对象修改;如果匹配不到对象,构建通常会报错。诊断时先执行:
kubectl kustomize overlays/staging > rendered.yaml
kubectl get -f rendered.yaml --dry-run=client -o name
也可以直接检查目标对象:
kubectl kustomize overlays/staging | grep -n -A8 -B3 'name: staging-web'
生产流水线不应只检查 kubectl kustomize 命令成功,还应检查输出中关键字段,例如副本数、镜像、命名空间和 Service 选择器。
四、Generator:从输入生成 ConfigMap 和 Secret
Generator 不是运行时读取配置,而是在构建阶段生成 Kubernetes 资源。最常见的两种生成器是:
configMapGenerator;secretGenerator。
1. ConfigMapGenerator
示例:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
configMapGenerator:
- name: web-config
literals:
- LOG_LEVEL=info
- FEATURE_X_ENABLED=false
它会生成一个名字类似下面的 ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config-7k5m6d8f4c
data:
FEATURE_X_ENABLED: "false"
LOG_LEVEL: info
默认情况下,生成器会根据内容计算哈希后缀。只要 data 内容变化,名称就会变化。
Deployment 可以这样引用:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
selector:
matchLabels:
app.kubernetes.io/name: web
template:
metadata:
labels:
app.kubernetes.io/name: web
spec:
containers:
- name: web
image: nginx:1.27
envFrom:
- configMapRef:
name: web-config
Kustomize 会识别这个引用,并把它改写为带哈希的实际名称,例如:
envFrom:
- configMapRef:
name: web-config-7k5m6d8f4c
因此配置变化同时导致:
- 新 ConfigMap 名称生成;
- Deployment 引用名称改变;
- Pod Template 的内容改变;
- Deployment 触发滚动更新。
这是一条由内容哈希驱动的发布链路,而不是 Kubernetes 自动比较 ConfigMap 内容后重启 Pod。
2. SecretGenerator
secretGenerator:
- name: web-secret
literals:
- username=admin
- password=change-me
生成结果中的 data 通常是 Base64 编码:
apiVersion: v1
kind: Secret
metadata:
name: web-secret-6g7h8k9m2b
type: Opaque
data:
password: Y2hhbmdlLW1l
username: YWRtaW4=
Base64 不是加密。任何能够读取该 Secret 的人都可以解码;如果把明文凭据放进 Git,再由 secretGenerator 生成 Secret,Git 历史中仍然存在明文风险。
更安全的方案包括:
- 使用外部 Secret 管理系统;
- 使用 Git 加密方案;
- 让 GitOps 控制器在集群侧解密;
- 严格限制仓库、构建日志和生成产物的访问权限。
secretGenerator 只解决“如何生成 Kubernetes Secret YAML”,不解决密钥保管、轮换、审计和访问控制。
3. Generator 的行为和名称冲突
生成器可以配置:
configMapGenerator:
- name: shared-config
behavior: merge
literals:
- LOG_LEVEL=debug
常见的 behavior 语义是:
create:创建;replace:替换同名生成结果;merge:与已有生成结果合并。
这里的“已有”通常指 Kustomize 当前资源累加范围中已存在的生成对象,不是指向 Kubernetes API 查询一个线上对象。Kustomize build 默认不读取集群状态,因此不能把集群里已有的 ConfigMap 当作输入进行合并。
如果多个资源最终产生相同的资源身份,Kustomize 可能报告资源 ID 冲突。不要用 behavior: merge 掩盖两个环境无意中复用了同一个名称的问题,应先确认名称、层级和数据来源是否合理。
4. 是否关闭哈希后缀
可以通过:
generatorOptions:
disableNameSuffixHash: true
关闭生成资源的哈希后缀,但这会失去“配置变更自动改变 Pod Template”的机制。此时更新 ConfigMap 后,已运行的 Pod 是否重新读取配置取决于应用本身:
- 有些应用只在启动时读取;
- 有些应用会监听文件变化;
- 通过环境变量注入时,进程通常不会自动获得新环境变量。
关闭哈希不是单纯的命名偏好,而是改变了配置发布和进程重启语义。
五、名称、命名空间和引用变换
Kustomize 支持对资源名称添加前缀和后缀:
namePrefix: prod-
nameSuffix: -v2
资源名称会变成类似:
prod-web-v2
Kustomize 还会尝试更新已知的名称引用,例如:
- Deployment 的
configMapRef; - Pod 的
secretRef; - ServiceAccount 引用;
- 某些卷引用;
- Service 相关引用。
这种引用识别依赖 Kustomize 已知的字段路径。自定义 CRD 中的任意字符串字段不会自动被当作资源引用。例如:
spec:
configName: web-config
如果这是某个 CRD 的业务字段,Kustomize 不一定知道它需要跟随 namePrefix 或生成器哈希变化。此时需要使用补丁、替换机制或由控制器自身处理。
namespace 变换也不是简单的文本替换:
namespace: production
它通常会作用于命名空间级资源,但不会把集群级资源,例如 ClusterRole,变成命名空间资源。某些 CRD 字段中的命名空间字符串也不会因为“看起来像 namespace”就自动改变。
常见错误是把 metadata.namespace、Service DNS 名称和应用配置中的任意字符串混为一谈。Kustomize 只能变换它理解的 Kubernetes 结构和已知引用。
六、一个完整可运行的构建示例
目录:
demo/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── kustomization.yaml
│ └── app.env
└── overlays/
└── production/
├── kustomization.yaml
└── replicas.yaml
base/app.env:
LOG_LEVEL=info
FEATURE_X_ENABLED=false
base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
configMapGenerator:
- name: web-config
envs:
- app.env
base/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app.kubernetes.io/name: web
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: web
template:
metadata:
labels:
app.kubernetes.io/name: web
spec:
containers:
- name: web
image: nginx:1.27
envFrom:
- configMapRef:
name: web-config
ports:
- name: http
containerPort: 80
base/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app.kubernetes.io/name: web
ports:
- name: http
port: 80
targetPort: http
overlays/production/replicas.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
overlays/production/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
namePrefix: prod-
resources:
- ../../base
images:
- name: nginx
newTag: "1.27"
patches:
- path: replicas.yaml
target:
group: apps
version: v1
kind: Deployment
name: web
构建:
kubectl kustomize demo/overlays/production
预期结果包括:
metadata:
name: prod-web
namespace: production
Deployment 的副本数为 3,并且引用类似:
envFrom:
- configMapRef:
name: prod-web-config-<hash>
其中 <hash> 只是说明位置,实际值由文件内容计算,不能在脚本中硬编码。
应用前先查看差异:
kubectl diff -k demo/overlays/production
应用:
kubectl apply -k demo/overlays/production
验证:
kubectl -n production get deployment,service,configmap,pods
kubectl -n production rollout status deployment/prod-web
这里必须区分三个阶段:
kustomize build或kubectl kustomize:只在本地构建 YAML,不访问 API Server;kubectl diff -k:使用构建结果与集群对象比较;kubectl apply -k:把构建结果提交给 API Server。
如果 Deployment 已存在但镜像拉取失败,Kustomize 构建仍可能成功;失败发生在 Kubernetes 调度、拉取镜像或容器启动阶段。因此“构建成功”不等于“部署成功”。
删除时:
kubectl delete -k demo/overlays/production
它会根据当前构建结果删除当前清单中的资源。配置内容变化导致生成 ConfigMap 名称变化后,旧 ConfigMap 可能不再出现在新清单中,普通 apply 不一定自动删除它。长期运行的环境需要单独设计旧生成资源的清理策略,并避免未经限定的 prune 删除其他系统或团队的资源。
七、常见变换:镜像、标签和副本数
镜像替换可以写成:
images:
- name: nginx
newName: registry.example.com/platform/nginx
newTag: "1.27.3"
它不是全局字符串替换,而是针对 Kubernetes 已知的镜像字段进行处理。镜像名称、仓库路径和标签的匹配应与资源中实际使用的镜像引用一致。
副本数也可以使用专门字段:
replicas:
- name: web
count: 3
但当修改同时涉及容器、探针或资源限制时,patches 通常更清晰,因为所有变更可以在一个目标明确的补丁中表达。
标签变更尤其需要谨慎。Deployment 的:
spec:
selector:
matchLabels:
app.kubernetes.io/name: web
一旦创建,通常不能随意修改。若给 Pod Template 添加标签却没有同步 Service selector,Service 可能找不到 Endpoint;若修改 Deployment selector,API Server 可能拒绝更新。Kustomize 能生成语法正确的 YAML,但不能保证标签语义和控制器关系正确。
八、Kustomize 的执行状态与并发边界
Kustomize build 本质上是一个本地构建过程:
文件与目录
│
├── 读取 Base
├── 累加资源
├── 执行 Generator
├── 执行 Patch 和字段变换
└── 输出 YAML
它通常不维护类似 Helm Release 的发布历史,也不负责持续调谐。多个构建进程可以同时读取同一目录;真正的并发控制发生在应用阶段,由 API Server 处理资源更新、冲突和字段所有权。
应用到 Kubernetes 后,状态路径是:
本地 Git 清单
│ kubectl apply -k
▼
API Server 接收对象
▼
控制器创建或更新实际资源
▼
Scheduler、Kubelet、Ingress Controller 等继续处理
▼
集群实际状态
因此:
- Kustomize 生成的是期望配置;
- API Server 负责校验和持久化;
- 控制器负责把对象推进到运行状态;
- Kustomize 本身不会检测配置漂移;
- Kustomize 本身不会在一段时间后自动恢复被手工修改的对象。
如果另一个管理员直接修改了线上 Deployment,只有再次执行 apply,或者交给 GitOps 控制器持续调谐,期望配置才可能重新覆盖该修改。若多人或多个控制器同时管理同一字段,还会出现字段所有权冲突或反复覆盖。
使用 Server-Side Apply 时可以显式指定字段管理者,例如:
kubectl apply \
--server-side \
--field-manager=platform-kustomize \
-k demo/overlays/production
这不会改变 Kustomize 的构建语义,只改变 API Server 处理字段所有权和冲突的方式。生产环境必须明确哪些字段由 Kustomize、Helm、Operator 或人工管理,不能让多个系统无约定地写同一对象。
九、失败表现和诊断路径
1. 资源重复
错误通常类似“已存在相同资源身份”。常见原因是两个 Base 都定义了相同的:
group + version + kind + namespace + name
诊断方法:
kubectl kustomize overlays/production > rendered.yaml
kubectl apply --dry-run=client -f rendered.yaml
然后搜索重复的 kind 和 metadata.name。修复方式通常是删除重复引用,而不是盲目增加前缀。
2. 补丁找不到目标
表现为构建阶段报找不到匹配资源。检查:
group是否正确;version是否写成v1或v1beta1;kind大小写是否正确;name是否是 Base 中的原始名称;- 目标资源是否被另一个条件分支排除。
可以先只构建 Base,再构建 Overlay,定位目标在哪一层消失或被重命名。
3. JSON Patch 路径错误
表现为 /spec/... 路径不存在。不要凭印象填写路径,应先查看实际输出:
kubectl kustomize demo/overlays/production
然后确认字段层级、数组索引和父对象是否存在。对于结构变化频繁的资源,战略合并补丁通常比数组索引 JSON Patch 更耐维护,但 CRD 场景又可能需要 JSON Patch,这取决于对象的 schema 和变更目标。
4. Service 没有流量
构建成功、Deployment 也正常,但 Service 没有 Endpoint,常见原因是:
Service.spec.selector
与:
Pod.metadata.labels
不一致。
名称前缀不会自动修复业务标签选择器错误。验证:
kubectl -n production get pods --show-labels
kubectl -n production get endpointslice
kubectl -n production describe service prod-web
5. 配置更新但 Pod 没有重启
如果使用了生成器默认哈希,检查 Deployment 的 Pod Template 是否引用了新名称:
kubectl -n production get deployment prod-web -o yaml
kubectl -n production get configmap
如果关闭了哈希后缀,则配置更新本身通常不会改变 Pod Template。此时需要应用主动热加载,或通过显式的 Pod Template 变更触发滚动更新。
十、Kustomize 与 Helm、GitOps 的边界
Kustomize 和 Helm 都可以产生 Kubernetes YAML,但抽象层不同。
Helm 的核心流程是:
Chart + Values + Template
│
▼
渲染 YAML + Release 管理
Kustomize 的核心流程更接近:
已有 YAML + Patch + Generator + Transformer
│
▼
变换后的 YAML
因此:
- 需要循环、条件、模板函数、Chart 分发和参数化安装时,Helm 更自然;
- 已有一套 Kubernetes YAML,只需按环境修改少量字段时,Kustomize 更直接;
- Helm 模板过于复杂时,Kustomize 可以作为渲染后的二次定制层,但这样会增加调试链路;
- Kustomize 不提供 Helm Release 的原生历史、Hook 和回滚模型。
与 GitOps 的关系也需要区分。GitOps 控制器可以执行 Kustomize 构建,并持续将 Git 中的期望状态调谐到集群。此时:
- Kustomize 负责生成清单;
- Git 仓库负责期望状态来源;
- 控制器负责持续调谐;
- API Server 负责对象校验和持久化;
- Kubernetes 控制器负责实际运行状态。
没有 GitOps 控制器时,kubectl apply -k 只是一次性操作;它不会自行监控漂移,也不会自动晋级或回滚。
十一、生产使用时必须明确的边界
Kustomize 能保证的是构建规则和输出结构,不保证:
- 镜像一定存在;
- Pod 一定能调度;
- PVC 一定绑定成功;
- Ingress Controller 一定支持所写注解;
- 云厂商 LoadBalancer 一定按预期分配;
- CRD 一定已经安装;
- 应用一定能理解生成的配置;
- Secret 一定安全;
- 变更一定可回滚。
在不同 Kubernetes 发行版、不同 kubectl 内置版本和不同独立 Kustomize 版本之间,字段支持、弃用行为和变换细节可能存在差异。应在 CI 中固定构建工具,执行:
kubectl kustomize path/to/overlay > rendered.yaml
kubectl apply --dry-run=server -f rendered.yaml
kubectl diff -f rendered.yaml
其中 --dry-run=server 需要访问 API Server,并会使用目标集群的 schema、准入控制和 API 能力进行校验,比纯本地构建更接近真实应用条件。
真正适合 Kustomize 的配置管理边界可以概括为:
它不是:
理解这个边界后,Base 负责稳定的共同结构,Overlay 负责环境差异,Patch 负责精确修改,Generator 负责构建期资源生成,而 Kubernetes API Server 和 GitOps 控制器分别承担校验、持久化与持续交付职责。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Helm 完整指南:Chart、Template、Values、Release、Hook 和回滚
- 下一篇:Kubernetes GitOps:期望状态、调谐、Secret、漂移、晋级和回滚
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论