面试 · 原理追问型
底层机制。考察对工具/概念背后原理的深度理解,而非只会用。每题需讲清楚"数据流/控制流是什么样的"、"关键机制怎么实现的"。
子类 1:Git 原理#
题目1:Git 的"三区模型"是什么?git add 和 git commit 在底层分别做了什么?#
问题: Git 的工作区、暂存区、版本库被称为"三区模型"。请从底层 object 存储的角度解释 git add 和 git commit 分别做了什么。
工作流程/原理说明:
Git 三区模型:① 工作区(Working Directory)——你看到的文件目录;② 暂存区(Staging Area / Index)——git add 后文件进入的区域,是一个二进制 index 文件(.git/index);③ 版本库(Repository)——git commit 后文件进入的区域,存储在 .git/objects/。
Git 的底层是 content-addressable storage(内容寻址存储)——所有内容都用 SHA-1 哈希作为唯一标识。Git 有四种 object:① blob——文件内容(不含文件名);② tree——目录结构(包含文件名 + blob SHA + 权限);③ commit——提交记录(指向 root tree SHA + parent commit SHA + author/committer/message);④ tag—— annotated tag。
git add file.txt 的底层流程:① Git 读取 file.txt 内容 → 计算 SHA-1 → 压缩存为 blob object(.git/objects/ab/cdef...);② Git 更新 .git/index 文件——记录"file.txt → blob SHA"的映射。注意 blob 只存内容不存文件名,文件名在 tree 里。
git commit 的底层流程:① Git 根据 .git/index 当前内容生成 tree object(递归构建子目录的 tree);② Git 创建 commit object——包含 root tree SHA、parent commit SHA(HEAD 指向的 commit)、author/committer/timestamp/message;③ Git 移动当前分支引用(如 refs/heads/main)指向新 commit。
关键理解:① 文件内容相同则 blob SHA 相同(Git 自动去重)——改一个文件不会重新存其他文件;② commit 是不可变的——修改 commit 只能创建新 commit(rebase/amend 都是创建新 commit);③ 分支只是指向某个 commit 的指针(refs/heads/main 文件内容就是 commit SHA)——创建/删除分支几乎零成本;④ git diff 是比较两个 tree object 的差异,git log 是沿 commit 的 parent 链遍历。
题目2:Git 的 object 存储如何实现去重和压缩?#
问题: Git 仓库越来越大但磁盘占用增长缓慢,背后的去重和压缩机制是什么?
工作流程/原理说明:
Git object 存储的两个核心机制:content-addressable 去重 + packfile 压缩。
去重机制:① 每个 object 的 SHA-1 由内容决定(content-addressable),相同内容必然产生相同 SHA → Git 检测到 SHA 已存在就不再存储;② blob 只存文件内容不含文件名——两个文件内容相同就共享一个 blob;③ tree object 引用 blob SHA——目录结构变化但文件内容没变时,新 tree 引用旧 blob;④ commit 间的相似文件自动去重——改一个文件的新 commit 只多存一个 blob + 一个 tree + 一个 commit,其他文件复用旧 blob。
packfile 压缩:① 松散 object(loose objects)——.git/objects/ab/cdef... 每个对象单独压缩(zlib),适合小文件但浪费空间;② packfile——git gc 时把多个 object 打包成单个 .git/objects/pack/pack-xxx.pack,使用 delta compression(增量压缩);③ delta compression——相似的 object 只存"与前一个版本的差异"(delta),而非完整内容。例如 100 个 commit 改同一个文件,packfile 只存"每次改了哪几行"而非 100 份完整文件;④ packfile 的 .idx 索引文件记录每个 object 在 pack 中的偏移量,支持随机访问。
git gc 的触发:① 用户手动 git gc;② 自动触发——loose object 数量超过阈值(gc.auto,默认 6700)或 packfile 数量超过阈值(gc.autoPackLimit,默认 50);③ git repack -ad 强制重新打包。
实战观察:① git clone 后仓库比原仓库小——clone 时自动 gc + pack;② 大文件改一个字节也会产生新 blob(Git 不做文件级去重)——所以二进制大文件(如镜像)不适合存 Git,应该用 Git LFS;③ git filter-repo 可以清理历史中的大文件,但会重写所有 commit SHA(破坏性操作)。
题目3:git rebase 和 git merge 在底层有什么区别?为什么 rebase 会"重写历史"?#
问题: rebase 和 merge 都能整合分支,但底层机制完全不同。请从 commit object 的角度解释差异。
工作流程/原理说明:
git merge(保留历史):① 假设 main 在 C1,feature 在 C2(从 C1 分叉);② main 上有新 commit C3,feature 上有新 commit C4;③ git merge feature 创建一个 merge commit C5——parent 是 C3 和 C4,tree 是 C3 和 C4 的合并结果;④ C1/C2/C3/C4 都保留,C5 是新的 merge commit。历史图是"分叉再汇合"的形状。
git rebase(重写历史):① 同样 main 在 C3,feature 在 C4(parent 是 C2,C2 的 parent 是 C1);② git rebase main 的本质是"把 feature 的 commit 重新应用到 main 最新位置";③ Git 依次对 feature 的每个 commit(C4)创建新 commit C4'——C4' 的 parent 是 C3(main 最新),内容是 C4 的 patch 应用到 C3 上;④ 原来的 C4 变成 dangling object(没有引用指向,最终被 gc 清理);⑤ feature 分支指针移动到 C4'。历史图是"线性"的。
为什么 rebase "重写历史":rebase 创建了全新的 commit(C4' 的 SHA 与 C4 不同),原来的 C4 不再被引用。这是"重写历史"的本质——commit 是不可变的,rebase 不是"修改"commit 而是创建新 commit 替代旧 commit。
rebase 的风险:① 已 push 到远端的 commit 被 rebase 后,其他人 pull 会冲突——因为本地和远端的 commit SHA 不同;② rebase 公共分支(如 main)会导致所有人历史错乱——黄金法则是"不要 rebase 已 push 的公共分支";③ rebase 丢失 merge commit——默认 rebase 会把 merge commit 展平,--rebase-merges 保留 merge 结构。
选择建议:① 个人 feature 分支整合 main 用 rebase(保持线性历史,PR review 容易);② 多人协作的分支用 merge(保留协作历史,不重写);③ 已 push 的分支不要 rebase(除非是个人分支且协调好);④ squash merge 是 GitHub Flow 的主流——把 feature 分支的多个 commit 压成一个新 commit 合到 main,既线性又保留 main 干净。
题目4:HEAD、branch、ref、commit 之间是什么关系?#
问题: Git 的 HEAD、branch、ref、commit 概念容易混淆,请理清它们的关系。
工作流程/原理说明:
四者关系从底层到上层:commit(实体)→ ref(指针)→ branch(一种 ref)→ HEAD(特殊 ref)。
commit:Git 的基本实体,包含 tree SHA + parent SHA + author/committer/message。每个 commit 有唯一的 SHA-1 哈希(如 a1b2c3d...)。commit 是不可变的。
ref(引用):指向某个 commit SHA 的命名指针。存储在 .git/refs/ 目录下,文件内容就是 commit SHA(40 字符)。例如 .git/refs/heads/main 文件内容是 a1b2c3d...。ref 是可变的——可以移动到任意 commit。
branch(分支):一种 ref,存储在 .git/refs/heads/。例如 refs/heads/main 就是 main 分支。branch 的特殊性在于:① 创建分支 = 创建一个 ref 文件(几乎零成本);② 切换分支 = 移动 HEAD 指向该 ref;③ commit 时当前分支的 ref 自动前进到新 commit。tag 是另一种 ref(refs/tags/v1.0),通常不移动(annotated tag 指向 tag object,lightweight tag 直接指向 commit)。
HEAD:特殊的 ref,指向"当前所在的分支"。存储在 .git/HEAD 文件里,内容是 ref: refs/heads/main(指向 main 分支的 ref)。git checkout feature 时 HEAD 内容变成 ref: refs/heads/feature。detached HEAD 状态是 HEAD 直接指向某个 commit SHA(不指向任何 branch ref),例如 git checkout a1b2c3d 后 HEAD 内容是 a1b2c3d...。
关键操作的本质:① git commit——创建新 commit → 当前 branch ref 前进到新 commit → HEAD 跟随 branch ref 前进;② git checkout main——HEAD 内容改为 ref: refs/heads/main + 工作区更新为 main 指向的 commit 的 tree;③ git branch feature——创建 .git/refs/heads/feature 文件指向当前 commit(HEAD 不变);④ git reset --hard a1b2c3d——当前 branch ref 移动到 a1b2c3d + 工作区更新(不创建新 commit);⑤ git merge feature——创建 merge commit(parent 是当前 HEAD 和 feature 的 ref)+ 当前 branch ref 前进。
理解这层关系后,很多 Git 操作就不再是黑盒:rebase 是"创建新 commit + 移动 branch ref"、cherry-pick 是"把某 commit 的 patch 创建为新 commit"、tag 是"创建一个不移动的 ref"。
子类 2:CI/CD 原理#
题目1:Docker 多阶段构建为什么能"瘦身"镜像?底层原理是什么?#
问题: 多阶段构建(multi-stage build)是减小镜像体积的标准做法。请解释底层原理。
工作流程/原理说明:
Docker 镜像本质是"分层文件系统"——每条 RUN/COPY/ADD 指令产生一个 layer(层),layer 是文件系统的差异(diff)。镜像 = 所有 layer 叠加 + 元数据。
单阶段构建的问题:① 编译工具残留——RUN go build 需要 Go 工具链(~500MB),编译后的二进制只需要几 MB,但 Go 工具链留在镜像里;② 中间产物残留——/tmp/build-cache、node_modules、.git 等中间文件留在镜像里;③ layer 叠加不能"删除"——即使后面 RUN rm -rf /tmp/build-cache,前面的 layer 仍包含这些文件,镜像体积不会减小。
多阶段构建原理:① 定义多个 FROM 阶段(builder/runtime),每个阶段是独立的镜像构建;② builder 阶段安装编译工具链 + 编译产物;③ runtime 阶段只 COPY --from=builder /app/binary /app/binary 拷贝编译产物;④ 最终镜像只包含 runtime 阶段的 layer,builder 阶段的工具链和中间产物完全不进最终镜像。
示例:
FROM golang:1.22 AS builder # builder 阶段,~800MB
WORKDIR /app
COPY . .
RUN go build -o server . # 生成二进制
FROM gcr.io/distroless/static-debian12 # runtime 阶段,~20MB
COPY --from=builder /app/server /server # 只拷贝二进制
ENTRYPOINT ["/server"]最终镜像只有 ~25MB(distroless + 二进制),而不是 ~825MB(golang + 二进制)。
进一步瘦身技巧:① 用 distroless/alpine 代替 ubuntu/debian(减少 OS 层体积);② COPY --from=builder 只拷贝必要文件(不要 COPY . .);③ 合并 RUN 指令(RUN apt update && apt install -y curl && rm -rf /var/lib/apt/lists/*)减少 layer 数;④ 用 .dockerignore 排除不需要的文件(如 .git/node_modules);⑤ 用 BuildKit cache mount(RUN --mount=type=cache,target=/root/.cache/go-build)加速构建但不进镜像。
题目2:Terraform State 为什么要加锁?State 文件里存了什么?#
问题: Terraform State 文件是 IaC 的核心,为什么必须加锁?State 里存了什么?
工作流程/原理说明:
State 文件存了什么:① 资源映射——Terraform 资源地址(如 aws_vpc.main)到云资源 ID(如 vpc-abc123)的映射;② 资源属性——每个资源的当前完整属性(CIDR/子网/安全组规则等);③ 依赖关系——资源间的依赖图(VPC → 子网 → 路由表);④ metadata——Terraform 版本、provider 版本、serial 序号(每次 apply 递增)。
为什么要加锁:① 并发 apply 冲突——两人同时 terraform apply,A 创建 VPC,B 也创建 VPC,结果创建两个 VPC(预期一个);② State 文件覆盖——两人同时 apply 后各自 push state 到 S3,后 push 的覆盖先 push 的,资源映射丢失;③ plan 不准——A 在 apply 时 B 改了 state,A 的 plan 基于过期 state 做决策。
加锁机制:① S3 backend + DynamoDB lock——terraform init 时配置 dynamodb_table = "tf-locks",apply 前 Terraform 在 DynamoDB 写一条锁记录,apply 完删除锁;② HCP Terraform(托管)——内置锁机制;③ 自建 HTTP backend——自己实现锁 API。
State 管理最佳实践:① 远端存储——S3/Azure/GCS,不要存本地(团队协作 + 灾备);② 加锁——DynamoDB/Consul/PostgreSQL;③ 加密——S3 SSE-KMS 加密 state(state 里可能有敏感数据如密码);④ 分环境隔离——prod/qa/pre 各一个 state 文件(不要混在一个 state);⑤ 不要手动改 state——terraform state mv/rm 命令操作,不要直接编辑 JSON;⑥ state 文件不入 Git——可能含密码且每次 apply 都变;⑦ 定期备份——S3 versioning 开启,误删可恢复。
OpenTofu 的增强:原生 state encryption——用 KMS/AES 加密 state 内容,即使 S3 被攻陷也无法解密。Terraform 只能依赖 S3 SSE(存储层加密),颗粒度更粗。
题目3:ArgoCD 的 Sync 流程是什么样的?Sync 失败的常见原因有哪些?#
问题: ArgoCD 的 Sync 是核心操作,请详细描述 Sync 流程的每个步骤。
工作流程/原理说明:
ArgoCD Sync 流程:① 触发——三种触发方式:自动(syncPolicy.automated 检测到 OutOfSync)、手动(UI/API 触发)、webhook(Git push 触发立即 reconcile);② 拉取 Git 期望状态——application-controller 通知 repo-server 渲染 manifest,repo-server git clone + kustomize build/helm template 生成最终 yaml;③ 对比 live state——application-controller 通过 K8s API 查询集群当前实际状态;④ 计算 diff——对比 desired(Git)和 live(集群),标记 OutOfSync 的资源;⑤ apply——按 sync-wave annotation 顺序 apply 资源(sync-wave -10 的先 apply,10 的最后);⑥ 等待 health——每个资源 apply 后等待健康检查通过(Deployment 等 Ready、Pod 等 Running);⑦ 状态更新——更新 Application status(Synced/Failed/OutOfSync + Healthy/Degraded/Progressing)。
Sync 失败的常见原因:① CRD 不存在——apply CustomResource 前没有对应 CRD,需要先 apply CRD(配 sync-wave -10);② namespace 不存在——CreateNamespace=true 与 ApplyOutOfSyncOnly=true 组合时不生效,需要显式 namespace.yaml + sync-wave -10;③ 资源冲突——两个 Application 管同一个资源(如两个 ArgoCD 竞管 HTTPRoute),ComparisonError;④ CRD 太大超 etcd 限制——client-side apply 把整个 manifest 塞进 kubectl.kubernetes.io/last-applied-configuration annotation 超 256KB,需要 ServerSideApply=true;⑤ selector immutable——K8s 不允许修改 Deployment 的 selector,PR 隔离 overlay 模板升级时存量 Deployment 报错,需要先 delete 重建;⑥ manual sync 模式没触发——syncPolicy.automated 关闭时 git push 后 ArgoCD 只标记 OutOfSync 不会自动 sync,需要手动触发或调 sync API;⑦ wait 逻辑死等——deploy.py 等 App health=Healthy,但 baseline OutOfSync 导致 App 整体 Degraded,业务 Pod 实际 Ready 却死等 timeout。正确做法是等 Pod 状态而非 App 整体 health;⑧ Helm chart 拉不下来——CN 集群访问不了境外 Helm 仓库(charts.external-secrets.io),需要本地 helm template 渲染成 manifests 入 GitOps。
题目4:FluxCD 的 Reconcile 机制和 ArgoCD 有什么不同?#
问题: FluxCD 和 ArgoCD 都是 GitOps 工具,但 Reconcile 机制有本质差异。请解释。
工作流程/原理说明:
FluxCD Reconcile 机制:① per-resource interval——每个 Kustomization/HelmRelease CR 有独立的 spec.interval(如 5m),controller 每 interval 秒主动 apply 一次,不看 diff(即使 Git 没变也 apply);② source-controller 拉源——source-controller 拉 Git/OCI/Helm 仓库内容生成 artifact(tar.gz),通过内置 HTTP 暴露给其他 controller;③ kustomize-controller 消费 artifact——从 source-controller 拉 artifact + kustomize build + server-side apply;④ 持续收敛——每 interval 秒全量 apply,天然持续 reconcile(漂移自动纠正),不需要 selfHeal 开关;⑤ healthChecks——apply 完等待显式列出的资源进入 Ready;⑥ dependsOn——Kustomization 间显式依赖(CRDs → Namespaces → Apps),拓扑排序。
ArgoCD Reconcile 机制:① 全局 resync——--app-resync 默认 180s,所有 Application 共享,不能 per-app 配 interval;② diff-based sync——application-controller 对比 desired(Git)和 live(集群),只在 diff 变化时 sync(默认不主动 apply);③ selfHeal 开关——漂移纠正需要显式 syncPolicy.automated.selfHeal: true,否则只标记 OutOfSync 不纠正;④ repo-server 渲染——repo-server 是无状态 worker,每次被 application-controller 请求时 git clone + 渲染,有 LRU 缓存但每副本一份;⑤ sync-wave——annotation 隐式排序(-10 先 apply,10 后 apply),不是显式依赖。
核心差异:① interval 粒度——Flux 每个 CR 独立 interval(核心基础设施 1m,业务 10m),Argo 全局 180s 共享。大规模集群(2000+ Application)Flux 能把 reconcile 压力摊平到不同时间段,Argo 的 application-controller CPU 水位一直很高;② 收敛模式——Flux 天然持续 apply(每 interval 全量),Argo 默认 diff-based(只在变化时 sync),selfHeal 是开关;③ 源模型——Flux 的 source-controller artifact 缓存模型(pull-once-use-many),Argo 的 repo-server 每副本独立 cache;④ 依赖表达——Flux 的 dependsOn 显式拓扑,Argo 的 sync-wave annotation 隐式排序。
实战影响:① 大规模场景 Flux 的 per-resource interval 更优雅——可以把 CRD bundle 设 1h reconcile(很少变),核心 infra 设 1m,业务设 10m;② ArgoCD 的 UI 和手动 sync 按钮更适合开发同学自助;③ Flux 的持续 apply 模式对"防止手动改集群"更彻底——即使不开 selfHeal,下一次 interval 也会把手动改的拉回。
子类 3:可观测性原理#
题目1:Prometheus 的 Service Discovery(SD)机制是怎么工作的?#
问题: Prometheus 在 K8s 场景能自动发现新 Pod/Service,背后的 Service Discovery 机制是什么?
工作流程/原理说明:
Prometheus SD 流程:① 配置 SD job——在 Prometheus 配置里定义 scrape_configs,指定 SD 类型(kubernetes/node/file/consul/etc);② SD 查询——Prometheus 按 sd_interval(默认 30s)调用 SD API 获取目标列表。K8s SD 调用 K8s API(如 list pods/services/endpoints);③ relabel——对每个目标用 relabel_configs 改写 label(如从 __meta_kubernetes_pod_label_app 提取 app label、过滤特定 namespace、改写 address);④ scrape——按 scrape_interval(默认 15-30s)对每个目标发 HTTP GET /metrics 拉取指标;⑤ 目标健康检查——拉取失败标记 target 为 down,up=0。
K8s SD 的三种角色:① node——发现 K8s 节点(适合 node-exporter);② pod——发现所有 Pod(适合业务应用,可按 label 过滤);③ service——发现 Service(适合 ingress 后端,但 service IP 可能负载均衡到多个 Pod 难定位);④ endpoints——发现 Endpoints(适合 K8s Service 后端的 Pod 列表)。
ServiceMonitor 抽象:Prometheus Operator 引入 ServiceMonitor CRD,声明式定义"抓哪些 Service 的指标"。Prometheus 通过 serviceMonitorSelector 选取 ServiceMonitor(如 release=monitoring label),自动生成 scrape_configs。注意:① label 必须匹配 ruleSelector,否则 PrometheusRule 不被加载(曾有团队 12 条规则因 label 不匹配 75 天没生效);② kubectl get prometheusrule 看到的规则不一定真的被加载,必须从 /api/v1/rules 接口验证。
常见问题:① target down——Pod 没暴露 /metrics、端口不对、网络策略阻挡;② 指标延迟——scrape_interval 30s 意味着指标最多延迟 30s+;③ 高 cardinality——把 user_id 当 label 会导致 index 爆炸,Prometheus OOM;④ ruleSelector 不匹配——PrometheusRule 的 metadata.labels 必须匹配 Prometheus.spec.ruleSelector,否则规则不生效(静默失效,不报错)。
题目2:Alertmanager 的告警路由链路是什么样的?#
问题: 从 Prometheus 规则触发到钉钉收到消息,Alertmanager 的路由链路是怎样的?
工作流程/原理说明:
完整链路:PrometheusRule → Prometheus 评估 → Alertmanager → webhook adapter → 钉钉群。
① PrometheusRule 定义规则——expr + for + labels(severity/channel/env)+ annotations(summary/description);
② Prometheus 评估——按 evaluation_interval(默认 30s)评估每条规则。expr 满足时进入 pending 状态;持续 for 时间后进入 firing 状态;条件不满足时进入 inactive;
③ 发送到 Alertmanager——firing 的告警通过 POST /api/v2/alerts 发到 Alertmanager。每次评估都发(重复发送靠 Alertmanager 的 repeat_interval 控制);
④ Alertmanager 接收——Alertmanager 收到告警后放入内部队列,等待 group_wait(默认 30s)聚合同组告警;
⑤ 路由匹配——Alertmanager 按配置的 route 树匹配告警 labels。顶层 route 的 receiver 是兜底,子 routes 按 matchers(如 severity="P0")匹配。continue: true 表示继续匹配下一条 route(一个告警可发多个 receiver),continue: false(默认)表示匹配即停;
⑥ 抑制检查(Inhibit)——检查是否有更高 severity 的告警正在 firing,如果有则抑制当前告警。如 KubeAPIDown 时抑制该集群所有 Pod 告警;
⑦ 静默检查(Silence)——检查是否命中 active silence。silence 用 matchers 定义(如 alertname=Watchdog),命中的告警不发;
⑧ 发送到 receiver——按 repeat_interval(默认 30m-4h)控制重复发送频率。webhook_configs 发到 adapter(如 prometheus-webhook-dingtalk),adapter 用模板渲染成钉钉 markdown 消息发到群。
实战坑点:① silence 用 .+ 误伤 Watchdog——alertname=~".+" 会 silence 所有告警包括心跳,反向告警失效。必须用负匹配 alertname!=Watchdog 排除;② 钉钉关键词白名单——群启用"自定义关键词"安全策略,消息体必须含"告警/运维/恢复"之一,否则 300005 keywords not in content。模板要写死【告警】前缀;③ resolved 通知被拒——firing 渲染含"告警"放行,resolved 渲染"恢复"不含告警/运维 → 每 5min 一波 310000,业务方以为告警没了实际从没收到恢复消息。模板要改 【告警恢复】;④ configmap 热更新延迟——改模板后 /-/reload 只重载 config.yml,模板文件要 rollout restart;⑤ URL encode 吞 token——webhook URL 内嵌 ddurl=...?access_token=XXX,query parser 遇内层 ? 截断,token 丢。换 timonwong/prometheus-webhook-dingtalk(config-as-data,无此坑)。
题目3:OpenTelemetry Collector 的架构是什么样的?#
问题: OpenTelemetry Collector 是可观测性数据的中枢,请解释其架构和数据流。
工作流程/原理说明:
OTel Collector 架构:receiver → processor → exporter 三段式 pipeline,配 extension 辅助。
① Receiver(接收器)——接收数据的入口。支持多协议:OTLP(gRPC/HTTP)、Jaeger、Zipkin、Prometheus、Fluentd Forward 等。一个 collector 可配置多个 receiver 同时监听不同端口;
② Processor(处理器)——对数据做转换/过滤/增强。常用 processor:① batch——批量发送减少 API 调用;② memory_limiter——内存限制防 OOM;③ attributes——添加/修改/删除 span attributes;④ resource——添加 resource attributes(如 cluster name、region);⑤ filter——过滤不需要的 span(如健康检查);⑥ probabilistic_sampler——按比例采样降低数据量;
③ Exporter(导出器)——发送数据到后端。OTLP(到 Tempo/Mimir/Loki)、Prometheus(到 Prometheus)、Jaeger、Zipkin、Logging(调试用);
④ Extension(扩展)——辅助功能。health_check(健康检查 endpoint)、pprof(性能分析)、zpages(调试页面)、file_storage(文件存储缓冲);
⑤ Pipeline——配置 receiver → processor → exporter 的组合。可以有多条 pipeline(traces/metrics/logs 各一条,或多条同类型 pipeline 分流)。
数据流:① 应用用 OTel SDK 发数据到 collector(OTLP 协议);② collector 的 receiver 接收;③ processor 链式处理(batch + filter + attributes);④ exporter 发到后端(Tempo/Jaeger/Prometheus/Loki)。
部署模式:① Agent 模式——每个节点一个 collector(DaemonSet),接收本节点应用数据,转发到 gateway;② Gateway 模式——集群级 collector(Deployment 多副本),接收 Agent 数据,处理后发后端;③ Gateway + Agent——大规模场景组合,Agent 做轻量转发,Gateway 做重处理(enrichment/sampling)。
实战要点:① 同一组件在不同集群 Service 名不同——us-qa 的 otel-collector 叫 otel-collector,sandbox 集群叫 otel-collector-opentelemetry-collector(带 Helm release 前缀)。写代码连集群内服务前先 kubectl get svc 实证;② buffer 配置——collector 重启时数据可能丢失,配 file_storage extension 持久化缓冲;③ 采样策略——全量 trace 数据量大,用 tail_based_sampling 按错误率/延迟采样,保留有意义的 trace;④ resource attributes——务必添加 cluster、namespace、service.name 等 attributes,否则跨集群/跨服务无法关联。
题目4:Loki 的写入路径是什么样的?为什么它比 Elasticsearch 成本低?#
问题: Loki 的低成本源于其特殊的存储模型,请解释写入路径和成本优势的原理。
工作流程/原理说明:
Loki 写入路径:① Promtail/Fluent Bit 采集日志——从容器 stdout/文件采集日志行;② 添加 label——添加 K8s metadata 作为 label(app/namespace/pod_name);③ 发送到 Loki distributor——HTTP POST /loki/api/v1/push;④ distributor 分流——按 tenant + label hash 分配到 ingester;⑤ ingester 写入内存——日志行追加到内存中的 chunk(压缩块),label 建立"stream"索引(stream = label 集合的唯一组合);⑥ chunk 刷盘——内存 chunk 达到阈值(大小/时间)后刷到对象存储(S3/OSS);⑦ index 存储——label → stream ID → chunk 位置的索引存到 index store(BoltDB/Cassandra/TSDB)。
为什么比 ES 成本低:① 只索引 label 不索引内容——ES 对日志每个词建倒排索引,索引可能比日志原文还大;Loki 只索引 label(app=backend, namespace=prod),索引体积小 10-100 倍;② 日志原文压缩存对象存储——S3/OSS 比 ES 的本地磁盘便宜 5-10 倍,且日志原文用 gzip/zstd 压缩;③ 无分片副本——ES 要分片+副本(默认 1 副本,存储翻倍),Loki 的 ingester 做 RF(replication factor)但对象存储本身有多副本;④ 冷热分离——ES 所有数据在 SSD 上(贵),Loki 热数据在 ingester 内存,冷数据在对象存储(S3 Glacier 更便宜)。
Loki 查询的代价:① 按 label + 时间范围扫描——{app="backend"} |= "error" 先按 label 找到 stream,再扫描时间范围内的所有 chunk 找含 "error" 的行;② 全文搜索慢——不像 ES 有倒排索引能秒级返回,Loki 要扫描所有 chunk,时间范围大时慢;③ 高 cardinality 灾难——把 user_id 当 label 会导致 stream 数爆炸(每个 user_id 一个 stream),ingester 内存爆 + index 爆。Loki 严格限制 label 基数。
实战要点:① label 控制——只用低基数 label(app/namespace/env),不要用 user_id/request_id;② retention 配置——Loki 默认永久保留,要配 retention_period(如 30 天)+ compactor 自动清理;③ PVC 容量——filesystem 后端的 Loki PVC 会满(某团队 Loki PVC 50Gi 满 → CrashLoop 5737 次/20 天无人知)。生产用对象存储(S3/OSS)而非 filesystem;④ 查询优化——LogQL 先按 label + 时间范围缩小,再用 |= 过滤内容,不要全表扫描。
子类 4:安全原理#
题目1:Cosign Keyless 签名的原理是什么?#
问题: Cosign 的 Keyless 签名不需要管理长期密钥,这是怎么实现的?
工作流程/原理说明:
Keyless 签名依赖 Sigstore 项目的三个组件:Fulcio(证书签发)+ Rekor(透明日志)+ OIDC(身份证明)。
完整流程:① CI Job 获取 OIDC token——GitHub Actions 配 id-token: write 权限,运行时自动获取 OIDC token。token 内容包含 repo、commit、workflow、actor 等信息,由 GitHub 签名;
② Cosign 用 OIDC token 换证书——cosign sign --keyless 把 OIDC token 发给 Fulcio;
③ Fulcio 验证 OIDC token——Fulcio 验证 token 签名(GitHub 的 OIDC 公钥)+ 检查 issuer(如 https://token.actions.githubusercontent.com);
④ Fulcio 签发短期证书——Fulcio 用自己的根 CA 私钥签发一个短期证书(10 分钟过期),证书里绑定 OIDC identity(如 repo:example-org/myapp:ref:main:workflow:build)。这个证书是 X.509 格式,包含身份信息;
⑤ Cosign 用证书私钥签名镜像——Cosign 生成一个临时密钥对,用私钥签名镜像 digest,签名 + 证书 + OIDC token 一起提交到 Rekor;
⑥ Rekor 记录透明日志——Rekor 是 append-only 透明日志(类似区块链),把签名记录写入日志,返回一个 index。任何人都可以查询 Rekor 验证某镜像是否被签过、签名者是谁;
⑦ 验证流程——部署方 cosign verify --certificate-identity-regexp "example-org/myapp" --certificate-oidc-issuer "https://token.actions.githubusercontent.com":① 从 Rekor 拉签名 + 证书;② 验证证书链(Fulcio 根 CA 签发);③ 检查证书未过期;④ 检查证书 identity 符合预期(如必须是 example-org/myapp repo);⑤ 验证签名匹配镜像 digest。
比传统密钥签名的优势:① 无长期私钥——私钥是短期的(10 分钟),用完即弃,不存在泄露风险;② 身份可追溯——证书里绑定 CI Job 的 identity(repo/commit/workflow),出问题能定位到具体构建;③ 透明日志——所有签名记录在 Rekor,攻击者无法偷偷签恶意镜像而不被发现;④ 跨组织验证——只要信任 Fulcio 根 CA,任何组织都能验证对方签名,不需要预先交换公钥。
落地注意:① 依赖 Fulcio/Rekor 服务可用性(Sigstore 公共服务可能被墙,可自部署);② 验证时要联网查 Rekor,离线场景不适用;③ GitHub Actions 的 OIDC token 必须配 id-token: write 权限;④ Kyverno 验证时配 attestors 为 Fulcio 证书 + identity 规则(certificate-identity-regexp + certificate-oidc-issuer)。
题目2:External Secrets Operator(ESO)的同步机制是什么?#
问题: ESO 把 Vault/AWS Secrets Manager 的密钥同步成 K8s Secret,背后的机制是什么?
工作流程/原理说明:
ESO 架构:SecretStore + ExternalSecret + Controller 三层。
① SecretStore——定义"去哪里取密钥"的连接配置。包含后端类型(vault/aws/secretsmanager/gcp/azure)、认证方式(K8s ServiceAccount/IRSA/Vault AppRole)、endpoint。ClusterSecretStore 是集群级(跨 namespace 共享);
② ExternalSecret——定义"取哪些 key + 同步成什么 K8s Secret"。包含 secretStoreRef(指向 SecretStore)、target(生成的 K8s Secret 名字)、data(key 映射)、refreshInterval(同步频率,默认 1h);
③ Controller 同步循环——按 refreshInterval 周期性执行:① 读取 ExternalSecret CR;② 通过 SecretStore 配置连接后端(Vault/ASM);③ 用 K8s ServiceAccount 的 token 做 Kubernetes Auth(Vault)或 IRSA(AWS)认证;④ 调用后端 API 拉取密钥值;⑤ 用拉取的值创建/更新 K8s Secret;⑥ 更新 ExternalSecret status(Synced/Failed + lastSyncedTime)。
Vault Kubernetes Auth 流程:① ESO controller 用自己的 ServiceAccount token(JWT);② 把 JWT 发给 Vault 的 auth/kubernetes/login endpoint;③ Vault 用 TokenReview API 验证 JWT 有效(确认是 K8s 颁发的);④ Vault 检查 JWT 对应的 ServiceAccount 是否绑定到某个 role(bound_service_account_names + bound_service_account_namespaces);⑤ 匹配则返回 Vault token(有 TTL + Policy 限制);⑥ ESO 用 Vault token 读 secret/data/myapp/prod(KV v2 路径);⑦ 创建 K8s Secret。
关键配置:① Vault Policy——必须给 role 绑定的 Policy 配 read 权限(path "secret/data/myapp/prod/*" { capabilities = ["read"] });② KV v2 路径混淆——API 路径是 secret/data/myapp/prod,CLI 和 Policy 写 secret/myapp/prod,ESO 的 remoteRef.key 填 CLI 风格(不带 /data/),ESO 内部自动处理;③ refreshInterval——Vault 里的值改了,K8s Secret 不会立即更新,要等下一次 refresh。紧急情况可手动 kubectl annotate externalsecret xxx force-sync=$(date +%s) 触发;④ Secret 轮换不触发 Pod 重启——ESO 更新 K8s Secret 后,用 env 引用 Secret 的 Pod 不会自动重启。要装 Reloader(reloader.stakater.com/auto: "true" annotation)监听 Secret 变化自动重启。
动态密钥模式:Database Engine 提供临时数据库账号——ESO 每次刷新都调 Vault database/creds/app-role 获取新账号,旧账号 TTL 到期自动吊销。refreshInterval 要小于 Vault role 的 default_ttl,否则凭证过期前没刷新会断连。
题目3:Vault 动态密钥的生命周期是怎样的?#
问题: Vault 的 Database Engine 能动态生成数据库临时账号,请描述完整生命周期。
工作流程/原理说明:
Vault 动态密钥生命周期:配置 → 请求 → 使用 → TTL 到期 → 自动吊销。
① 配置 Database Engine:① vault secrets enable database 启用引擎;② 配置数据库连接(database/config/my-postgres)——指定 plugin(postgresql/mysql/etc)、连接串、vault-admin 凭据(Vault 用这个 admin 账号去创建/删除临时账号);③ 创建 role(database/roles/app-role)——指定 creation_statements(SQL 创建账号语句模板)、default_ttl(默认 1h)、max_ttl(最大 24h)。
② 请求动态密钥:① 应用通过 ESO 调 vault read database/creds/app-role;② Vault 用 vault-admin 凭据连接数据库,执行 creation_statements——CREATE ROLE '{{name}}' WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES...;③ Vault 生成随机用户名(如 v-token-app-role-abc123)和密码;④ Vault 返回凭据 + lease_id + lease_duration(默认_ttl);⑤ Vault 记录 lease(用于后续续约和吊销)。
③ 使用密钥:① ESO 把凭据同步成 K8s Secret;② 应用通过 envFrom 使用;③ 应用用这个账号连接数据库。
④ 续约(Renewal):① lease 到期前,ESO 的 refreshInterval(如 45m)触发刷新;② ESO 再次调 vault read database/creds/app-role——Vault 生成全新账号(不是续约旧账号);③ ESO 更新 K8s Secret;④ Reloader 触发 Pod 滚动重启使用新账号;⑤ 旧账号在原 lease_duration 到期后被 Vault 自动吊销(DROP ROLE)。
⑤ 吊销(Revoke):① 自动吊销——lease_duration 到期,Vault 自动执行 revocation_statements(DROP ROLE);② 手动吊销——vault lease revoke <lease_id> 立即吊销(如发现泄露);③ 应用 Pod 重启前旧账号仍可用(lease 未过期),重启后用新账号。
关键设计:① 每次请求生成新账号——不是续约旧账号,而是创建新账号,旧账号 TTL 到期自动吊销;② ESO refreshInterval < default_ttl——如 TTL=1h,refreshInterval=45m,确保新账号在旧账号过期前生成;③ Pod 滚动重启——Secret 更新后 Reloader 触发重启,否则应用还在用旧账号(已过期会连接失败);④ vault-admin 权限——Vault 需要数据库 admin 权限来创建/删除账号,这个 admin 凭据要妥善保管(建议用云 KMS 加密存 Secret Manager)。
实战价值:① 凭证泄露窗口小——即使 K8s Secret 被窃取,攻击者也只有不到 1 小时的窗口;② 紧急吊销——发现泄露立即 vault lease revoke 吊销当前账号,无需改密码重启所有应用;③ 审计完整——Vault 记录每次凭据请求的 caller(哪个 K8s SA)、时间、lease_id;④ 数据库里无长期应用账号——减少密码管理负担。代价:① Pod 频繁重启(每小时一次)影响长连接;② Vault 依赖——Vault 挂了应用无法获取新凭据(已有 Secret 仍可用到过期);③ creation_statements 要正确(错误会导致账号创建失败但 Vault 认为已成功创建,凭证发出去却连不上)。