先聊点实在的。“K8S集群 - 四大组件工作原理”这个话题,我见过太多人栽在“会用kubectl但不懂内部机制”这个阶段。你问他Pod是怎么调度到节点上的,他能给你喊出kube-scheduler;你问他etcd什么时候会丢数据,他支支吾吾,答不上来。不是大家不努力,是Kubernetes的组件确实抽象,官方文档写得又分散,新手光靠翻文档很难建立起一个整体的心智模型。这篇文章我就把控制面最核心的四大组件——kube-apiserver、etcd、kube-scheduler、kube-controller-manager——挨个拆开,讲清楚它们各自管什么、怎么配合、踩过哪些坑,以及日常排障时应该盯哪些关键点。无论是准备K8S面试,还是刚接手集群的运维同学,这篇都能帮你把“原理”这块短板补上。
1. 先搞清楚总体框架:四大组件在集群里各自扮演什么角色
1.1 两层结构:控制面与工作节点的分工逻辑
Kubernetes集群从物理和逻辑上都分成两半:一半是控制面(Control Plane),一半是工作节点(Worker Node)。控制面就是集群的大脑,负责决策和状态管理;工作节点是肌肉,负责真正跑容器。
四大组件指的全部是控制面组件:kube-apiserver、etcd、kube-scheduler、kube-controller-manager。注意,这里说的“四大”是控制面里的四个核心组件,不要跟节点上的kubelet、kube-proxy混淆。节点组件职责很单纯:kubelet负责拉起和销毁容器,kube-proxy负责维护网络规则。真正决定“集群要变成什么样子”的,是控制面这四位。
整个设计最核心的哲学是“声明式状态协调”。用户提交的不是“帮我把nginx跑起来”这种命令式请求,而是一份YAML描述:“我要3个nginx副本,每个副本1核500M”。集群收到这个描述后,控制面组件各司其职,协同把当前实际状态慢慢“拉”向这个描述的状态。谁跟谁之间怎么协作?答案是一切通信都以kube-apiserver为中心,其他组件之间不直接通信。
1.2 四大组件的“角色扮演”与关键设计思想
如果类比一家公司,四大组件可以这样理解:
- kube-apiserver:前台行政。所有指令的入口,所有消息的集散地。没有它,其他组件谁也联系不上谁。
- etcd:档案室。存储所有最终状态、配置信息、集群元数据的地方。公司能不能恢复运转,全看档案在不在。
- kube-scheduler:排班经理。决定新来的Pod去哪台机器上跑,看资源、看约束、看亲和性。
- kube-controller-manager:质检员兼执行大队。里面有几十个“小控制器”,各自盯着一类资源,发现有偏差就拉回来。
这四个组件的设计有一个隐藏的共通点:状态分离。etcd只存数据,不做业务判断;apiserver只做请求转发和校验,不主动发起变更;scheduler和controller只做决策和执行,不保存状态。这种单职责设计让整个系统容易扩展、容易替换。比如你觉得默认调度器不够聪明,可以部署一个自定义调度器,只要把Pod的spec.schedulerName改一下就行,apiserver根本不管你用的是哪个调度器。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. kube-apiserver:一切的入口,也是最容易被低估的组件
2.1 为什么所有交互都必须经过apiserver
Kubernetes有一条铁律:所有组件只能跟kube-apiserver通信,组件之间禁止直接通信。kubectl命令发给apiserver,kubelet上报节点状态给apiserver,controller-manager和scheduler也通过apiserver去读取和写入状态。这样设计至少有三个好处:
- 统一校验:所有请求先在apiserver做认证、鉴权、准入校验,不符合规范的请求直接拒绝,防止绕过门槛。
- 并发控制:所有写入都汇聚到同一个入口,apiserver配合etcd的事务机制,才能保证状态一致性,避免两个人同时改同一份数据。
- 可观测性:所有操作行为都有审计日志,出了问题可以回溯是谁在什么时间做了什么变更。
apiserver本身是“无状态”的,所以可以横向扩容。你可以起三个apiserver实例,前面挂一个负载均衡器,所有客户端都访问这个VIP。但是这里有一个踩坑点:如果直接让kubelet和kubectl访问多个apiserver而前面没有负载均衡,某些请求可能打到不同的实例上。好消息是apiserver之间不共享状态,它们都读同一个etcd,所以只要每个apiserver连的是同一个etcd集群,打到哪个都行,没有脏数据的问题。
2.2 watch机制与声明式API
apiserver最容易被忽略的一个功能是watch接口。任何客户端不光是能“查一下当前状态”,还可以建立一条长连接,持续获取某个资源的变更事件。这个能力是整个集群得以运转的动力来源。
我们常说K8S是“声明式”的,这种模式之所以能成立,靠的就是watch。你提交一个期望状态的YAML给apiserver,apiserver把这份期望状态写进etcd。然后呢?scheduler在watch未调度的Pod,controller-manager在watch Deployment、Service、Node等各种资源。它们一发现“当前状态”和“期望状态”有偏差,马上动手纠正。纠正完把最新状态写回apiserver,再由apiserver广播给所有订阅者。
这个模式听起来复杂,实际用起来非常优美。比如kubectl get pods -w这个命令,实现原理就是发了一个watch请求,apiserver收到后不立即断连接,而是持续推送Pod状态变化。生产中如果你想实时感知集群的异常Pod,也可以自己写一个小脚本去watch Pod变化,然后对接告警系统。
2.3 实操经验:apiserver是集群的生命线
我踩过最狠的一次坑,是etcd节点故障导致apiserver全部只读不写。当时症状是:kubectl get pods能通,但kubectl apply会卡住,甚至超时。排查到最后发现是etcd的leader选举出了问题。这里想强调的是:apiserver自身不保存任何数据,它的可用性完全依赖etcd。所以我们平时监控不能只看apiserver进程活着没有,一定要把etcd集群的健康状态一起纳管。
另外,apiserver的--max-requests-inflight参数默认是400个请求,写入请求上限是200。如果集群规模大,很容易出现并发请求超过限制的情况。症状是kubectl返回“server has asked for the client to provide credentials”或者直接超时。调参数不是首选,先想想是不是有哪个controller或者客户端在疯狂调用apiserver,比如把日志采集Agent的--kube-api-qps设得太高。先降客户端负载,再考虑调服务端参数,这个顺序别搞反。
3. etcd:集群的“最终状态库”
3.1 为什么是etcd而不是别的存储
etcd是Kubernetes的唯一数据源,集群里所有状态信息都存在这里:Pod、Service、ConfigMap、Secret、Namespace、RBAC策略,甚至集群自身的元数据。你可能会问,为什么不直接用MySQL或者Redis?
两个关键点让etcd成为最合适的选择:第一,分布式一致性。K8S集群必须保证多个apiserver读到的是同一份数据,不允许出现节点A看到Pod存在、节点B看到Pod不存在这样的分裂脑问题。etcd内置raft共识算法,任何写入必须经过多数节点确认,数据天然一致。第二,watch能力。etcd原生支持key前缀的watch接口,这正好对接到apiserver的watch功能。MySQL要做一个持续变更推送,得自己造轮子,难度不小。
3.2 raft共识、版本控制与compact机制
etcd的raft共识机制有一个基本概念,你可能面试时会碰到:quorum。一个3节点etcd集群,写入必须得到至少2个节点确认((3/2)+1=2);5节点集群需要3个节点确认。所以etcd集群节点数建议是奇数,且3节点只能容忍挂1个,5节点只能容忍挂2个。
etcd的key-value存储还带版本号机制。每次数据变更,key的版本号自增,同时旧版本数据会被保留一段时间。为什么保留旧版本?因为K8S的watch机制需要支持从任意历史版本开始监听。比如某个客户端断开连接了,重新连上时apiserver会拿着客户端最后看到的资源版本号,去etcd继续向前读变更。但如果这个版本号的旧数据已经被清理掉了,客户端就只能从最新状态重新同步,也就是全量重推。
所以etcd引入了一个机制叫compact:定期把历史版本压缩掉,只保留最近N个版本。K8S控制面组件(controller-manager、scheduler)会定期做一次“资源同步”,也就是全量重新列出资源,更新自己的本地缓存。这就是为什么即使etcd做了compact,kubelet和controller也能正常干活,因为它们是周期性全量同步的,不完全是流式增量。
3.3 实操经验:etcd备份、性能与容量规划
给etcd做备份是集群运维最基本的底线。有两种常见方式:
- 快照方式,也是最推荐的方式。使用etcd内置的
etcdctl snapshot save命令导出快照文件。恢复时用etcdctl snapshot restore恢复到一个新目录,再启动etcd进程。 - 在K8S的etcd运维实践中,最好同时定期把快照文件异地保存,因为本地磁盘故障可能连快照一起丢。
bash复制# 备份快照
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /backup/etcd-snapshot-$(date +%Y%m%d).db
# 恢复快照
ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd-snapshot-20250101.db \
--data-dir=/var/lib/etcd-restore --name=etcd-restore
关于性能,etcd对磁盘IO极其敏感。官方建议使用SSD盘,延迟一旦飙高,所有写入都受影响,apiserver表现就是“卡顿”。监控etcd的时候,重点关注etcd_server_leader_changes_seen_total这个指标。如果这个指标频繁增长,说明leader在频繁切换,通常由磁盘延迟高或网络抖动引起。还有一个隐藏坑:etcd默认的--quota-backend-bytes是2GB,如果集群里面有大量历史事件数据,很容易把etcd撑爆,导致写入被拒。生产环境一般调到8GB,但一定要配合历史Event定期清理策略。
4. kube-scheduler:把Pod放到最合适的节点上
4.1 调度流程:从Pod创建到出现在节点上
scheduler的工作看起来简单:找一个合适的节点运行这个Pod。但里面的流程分得很细。Pod一旦被创建到集群里,apiserver会在Pod的spec里发现一个关键字段:nodeName还是空的。scheduler监听的就是这类“需要调度的Pod”。
调度的核心链路是:
- 客户端提交Pod,apiserver写入etcd,Pod的
nodeName为空。 - scheduler通过watch发现这个Pod,拉取Pod定义、相关Node信息和集群资源情况。
- scheduler先做过滤(Filtering),找出一批“可以运行这个Pod”的节点。
- 再对通过的节点做打分(Scoring),按得分排序,分数最高的节点胜出。
- scheduler把选中的节点名填到Pod的
nodeName字段,再通过apiserver更新回etcd。 - 节点上的kubelet watch到这个Pod有了
nodeName,并且写的就是自己,就开始拉镜像、创建容器。
这个流程看起来轻松,但过滤和打分背后的规则非常多。过滤阶段会看节点的内存、CPU够不够,有没有端口冲突,有没有磁盘压力或者内存压力,Pod有没有指定nodeSelector或nodeAffinity,有没有tolerations可以容忍节点上的taint等等。打分阶段则会综合考虑资源余量、地域分布均衡性、镜像是否已缓存在本地等维度。
4.2 过滤与打分:调度策略设计的演进
K8S默认调度器的策略经历了几个版本演进,如果你面试K8S岗位,这经常是高频考点。
早期版本用predicates和priorities两套策略文件配置,运维想自定义规则需要改调度器代码或者写配置文件,很不灵活。从K8S v1.15开始引入Scheduling Framework,把调度的各个环节变成插件化的扩展点:你可以在过滤插件、打分插件、甚至绑定插件上做自定义。默认调度器内部也是插件化实现,比如NodeName、NodeResourcesFit、NodeAffinity等都是一个个独立的插件。
这个设计最大的好处是解耦。比如某业务要求“Pod运行在GPU节点,且节点上最好已经缓存了镜像”,你不需要推翻默认调度器,只需要写一个自定义插件,注册到打分阶段,在默认打分分数之上叠加你的加分项,然后和默认调度器一起跑。
但有一个经验之谈:别一上来就想做自定义调度器。大多数调度需求,用默认调度器配合nodeSelector、nodeAffinity、taints/tolerations、Pod Topology Spread Constraints就能解决七八成。自定义插件意味着你要维护一个调度器代码库,升级K8S版本时插件API还可能变,运维成本不低。我用过一个非常复杂的自定义调度器,最后发现它解决的“资源碎片问题”,其实在节点池设计上规划好就能避免。
4.3 实操经验:调度失败的排查思路
调度失败是大家最常见的困扰。典型症状:
bash复制kubectl get pods -n <namespace>
# 状态一直Pending,describe可以看到scheduler的报错
kubectl describe pod <pod-name>
kubectl describe输出的末尾,Events区域会列出调度器给出的Reason。常见的几种:
0/4 nodes are available: 1 node(s) had untolerated taint, 2 node(s) didn't match pod anti-affinity rules, 1 node(s) didn't match node selector.这条信息很直白,按提示逐项核对taint、亲和性、selector就行。0/4 nodes are available: 1 Insufficient memory, 1 NodeDiskPressure, 2 node(s) had untolerated taint.说明资源不够或者节点有压力。0/4 nodes are available: 1 node(s) had taint {node.kubernetes.io/not-ready: }, ...说明节点处于not-ready状态,检查kubelet是否存活。
有一个排障技巧非常重要:查看调度器日志时,不能光看Warn和Error。默认调度器很多节点的日志级别是Info,如果集群里Pod很多,日志量大会把真实原因冲掉。建议把scheduler的-v调到4或5,然后看带Attempting to schedule pod和Failed to schedule pod关键字的日志,效率远高于大海捞针。
5. kube-controller-manager:把“期望状态”拉向“当前状态”
5.1 控制循环的本质:reconcile
如果说scheduler是“排班经理”,那controller-manager就是“纠正大队”。里面包含几十个控制器:DeploymentController、ReplicaSetController、StatefulSetController、NodeController、ServiceAccountController、EndpointController等等。每一个控制器的基本逻辑都遵循一个模式——reconcile loop(调谐循环)。
调谐循环可以写成伪代码:
text复制for {
currentState = 获取当前状态(从apiserver/watch缓存)
expectedState = 获取期望状态(从资源的spec)
if currentState == expectedState {
// 无需操作
} else {
// 拉向期望状态
}
}
这个循环不是一次性执行,而是永不停止的循环。某些控制器会定期跑一遍,某些则靠事件触发。这种“有事件处理事件,没事件定时兜底”的设计,保证了即使之前漏掉了一些事件,等定时周期一到,控制器会重新计算一遍全部差异,把漏下的补上。
5.2 各控制器的分工与协作方式
拿一个Deployment举例子。你提交了一个Deployment,声明副本数为3。谁来完成这件事?
- DeploymentController创建一个ReplicaSet,副本数为3。
- ReplicaSetController看到ReplicaSet期望3副本,但目前Pod数量是0,就创建3个Pod。
- scheduler调度这3个Pod到具体节点。
- kubelet拉镜像、启动容器,把Pod状态回传给apiserver。
- ReplicaSetController持续watch,发现有Pod被删了,就再补一个新Pod。
注意,这里每个控制器只处理自己负责的一小环,不越过边界。DeploymentController不直接创建Pod,它只创建ReplicaSet。ReplicaSetController不直接调度Pod,它只负责创建Pod对象。这种严格的职责划分,使得系统可以灵活扩展。比如你要做一个“金丝雀发布”的控制器,只需创建一个新的ReplicaSet,而不用管Pod怎么创建和调度,底层的东西全部复用。
controller-manager本身是无状态的,但它需要选主。它启动时会尝试抢占一个系统里的lease资源,抢到的实例才是真正干活的,没抢到的实例处于standby状态。这样设计允许你跑多个controller-manager副本,实现高可用。检查谁是主实例,可以看这个lease:
bash复制kubectl get lease kube-controller-manager -n kube-system -o yaml
holderIdentity字段会显示持有者的IP和hostname。
5.3 实操经验:控制器卡住的排查与预防
controller-manager有一个常见问题是队列积压。每个控制器内部都有一个工作队列,接收来自apiserver的事件。如果某些事件的处理卡住了,比如访问apiserver超时、网络抖动,整个控制器的处理速度会变慢,表现出来就是Deployment发布了新版本,Pod状态久久不变。
排查时先看controller-manager的健康状态和指标:
bash复制kubectl get --raw /healthz -n kube-system
# 输出 ok 或者错误信息
再通过Prometheus监控workqueue_depth指标,这个指标表示队列深度。如果某个controller的队列深度长时间居高不下,说明这个controller处理不过来或陷入了循环。多数情况下重启controller-manager确实能暂时缓解,但要治本就得看是哪个controller、处理什么关键对象时卡住的。常见原因包括:某个CRD资源的watch事件风暴、apiserver响应变慢、RBAC配置错误导致客户端访问被拒。
还有一个小经验和新手说:不要试图用kubectl edit直接修改controller-manager产生的对象。比如你手动改了ReplicaSet生成的Pod标签,ReplicaSetController立刻会发现Pod数量偏差,马上把Pod删掉重建。你手动改的东西分分钟被“纠偏”掉,这是声明式系统设计里的“大石搬不走,只能碎掉了重新砌”。改配置要改源头Deployment,而不是改被它管着的Pod。
6. 一条Pod的完整生命周期:四大组件如何串起来
6.1 从kubectl apply到Pod运行,中间经历了什么
前面分别讲了单个组件,现在把它们串成一个完整的故事。你可以想象自己执行了:
bash复制kubectl apply -f nginx-deployment.yaml
nginx-deployment.yaml内容大概是:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.26
ports:
- containerPort: 80
第1步:kubectl把这份YAML发给apiserver。apiserver先做认证(你是谁)、鉴权(你是否有权创建Deployment)、准入校验(字段是否合法)。
第2步:apiserver把Deployment对象写入etcd。注意,这会触发watch事件,DeploymentController收到新Deployment创建事件。
第3步:DeploymentController核对发现当前ReplicaSet数量是0,创建一个ReplicaSet(副本数=3)。
第4步:ReplicaSetController监控到新的ReplicaSet,创建3个Pod。这3个Pod的nodeName都为空。
第5步:scheduler watch到3个未调度的Pod。依次经过过滤和打分,把Pod1调度到node-a,Pod2调度到node-b,Pod3调度到node-c,然后通过apiserver更新Pod的nodeName字段。
第6步:三个节点上的kubelet watch到属于自己的Pod被写入了nodeName,开始执行创建流程:先创建Sandbox容器(pause),再拉取业务镜像,再启动业务容器。容器启动成功后,kubelet把Pod状态上报给apiserver。
第7步:apiserver把最新状态写会etcd,同时广播给所有watch该Pod的客户端。ReplicaSetController确认3个Pod都Running了,任务完成,状态收敛。
整个过程在集群健康时可能只要几秒。你可以在终端执行kubectl get pods -w亲眼观察Pod状态从Pending变为ContainerCreating再变为Running的变化。
6.2 故障场景下的联动:节点宕机时发生了什么
理解了正常流转,再看故障场景。
假设node-b突然宕机。kubelet失联后,node对象的心跳无法更新,NodeController会周期性检查节点状态。默认配置下,如果节点超过40秒(node-monitor-grace-period)没有上报心跳,节点会被标记为NotReady,然后过5分钟(pod-eviction-timeout)如果仍未恢复,NodeController会把该节点上运行的Pod标记为异常,并触发驱逐。
驱逐说白了就是:在其他健康节点上重新创建Pod。Pod的调度规则实际上绕过了scheduler吗?不是,驱逐是controller-manager通过删除Pod对象实现的。Pod被删后,ReplicaSetController发现副本数不足,会创建新Pod,然后scheduler再调度,如此新Pod落到健康节点上。整个过程就是一套“期望状态被破坏,控制器自动纠正”的机制。
这里有一个老生常谈的坑:如果Pod是裸Pod(没有Deployment/StatefulSet管理),节点宕机后它不会自动重建,因为它没有上层控制器去“补副本”。所以生产环境几乎不要创建裸Pod,必须通过Deployment、StatefulSet、DaemonSet等控制器来管理,否则节点一挂,服务就永久少了一台。
6.3 排障时应该看什么:四大组件视角的检查清单
遇到集群异常,我建议按照下面顺序逐层排查,不要一上来就怀疑scheduler或者controller-manager:
- 先看apiserver:
kubectl get pods -A能不能正常返回?如果不能,检查apiserver进程状态和etcd健康。 - 再看etcd:
etcdctl endpoint health是否正常?如果etcd的leader频繁切换,先看磁盘IOPS和网络抖动。 - 再看调度:Pod是否一直Pending?用
kubectl describe看scheduler的Events。 - 再看控制器:Deployment的副本数有没有变化?ReplicaSet和Pod的期望数量对不对?用
kubectl get rs -A看DESIRED和CURRENT列。
很少有人会告诉你一个隐藏的点:controller-manager里某个controller卡住,可能不会直接体现在“进程挂了”,而是体现为“某个类型的资源持续无法收敛”。比如ServiceAccountController卡住,新创建的Namespace无法自动创建默认的sa和secret令牌,最终可能导致Pod一直ContainerCreating。排查这种问题,多练几次“事件链追踪”就熟了:从异常资源出发,找出管理它的控制器,再看控制器的日志和指标,八成能定位。
7. 常见问题速查与几个加分知识点
7.1 典型故障与处理速查表
整理一份我平时排障常用的速查表,遇到类似问题可以直接照着手动:
| 现象 | 可能原因 | 排查与处理 |
|---|---|---|
| kubectl命令卡住或超时 | etcd故障或网络分区 | 先检查etcd健康状态,再检查apiserver到etcd的网络 |
| Pod一直Pending | 没有满足条件的节点、资源不足、taint未容忍 | kubectl describe pod看Events,再核对节点资源 |
| 集群节点NotReady | kubelet故障、网络异常、docker/cri异常 | 登录节点看kubelet服务状态和日志 |
| 发布Deployment后Pod不更新 | controller-manager工作队列积压 | 看controller-manager日志和workqueue_depth指标 |
| 部分节点上Pod反复重启 | kubelet版本不一致、镜像拉取失败、存储挂载异常 | 用kubectl describe pod看容器的Last State和Events |
| 访问Service不稳定 | kube-proxy规则异常、Endpoint未更新 | 检查Pod的Labels与Service的Selector是否匹配 |
这是我反复用过无数次的表格,你可以存下来。但是注意,速查表是“指方向”,不是“最终答案”,实际排障还是要看具体日志和事件。
7.2 对面试和实际设计都有用的几个关键点
第一个:kubectl exec是怎么工作的? 表面看是“进入容器”,实际是apiserver收到你的exec请求后,在kubelet上建立了一条升级通道,kubelet再与容器运行时交互。这一连串过程不再涉及scheduler或controller-manager,只有apiserver和kubelet参与。理解这个能帮你明白为什么apiserver挂了就连exec都失效了。
第二个:为什么说controller-manager的多个controller本质上是同一个模式? 不管你是DeploymentController还是NodeController,底层都一样:informers监听资源、workqueue传递事件、reconcile函数处理。你甚至可以用client-go自己写一个类似PodCountController的小控制器,体验一把“为自己定义资源写自定义控制器”的感觉。
第三个:高可用集群里,控制面组件都怎么部署? apiserver可以多副本横向扩展,前面套LB;etcd集群3节点或5节点,采用raft做主从;scheduler和controller-manager用lease机制选主,只有主节点实例在工作。这套架构设计你现在再看,会发现思路非常统一:有状态的部分靠共识算法(etcd)保证一致性,无状态的部分靠多副本和选主实现高可用。
7.3 最后一个实操心得
我个人这些年使用K8S,最深的感受是:千万别把四大组件当成四个孤立的进程,它们是同一条状态流水线。apiserver是入口,etcd是存储,scheduler负责“放在哪”,controller-manager负责“状态对不对”。任何一个环节出了毛病,都会在上层表现为“Pod起不来”或者“服务不稳定”。排查时从流水线视角去分析,比单点去猜要高效十倍。
如果你正准备K8S面试,四个组件的职责和主要工作机制吃透,再能手绘出上面那条完整的Pod创建链路,就已经比一大半候选人强了。如果是在生产环境运维,日常抓metrics、做etcd备份和灾难恢复演练,这三件事做到位,安全隐患能防掉一大半。
这内容后续还能往深挖的方向太多了——比如apiserver的RBAC设计、scheduler的插件源码阅读、etcd的压缩参数调优、controller-manager里的源码级调谐细节。哪一块你感兴趣,可以顺着往下钻研。总之先把这个“四大组件工作原理”的整体框架立起来,后面所有细节就都有了落脚点。
