如果你管过一台几十个节点、同时跑着几十个作业的 Hadoop 集群,你一定对 YARN 调度器的脾气有深刻体会:大作业占住所有容器,小作业在 ACCEPTED 状态排队半天;某个队列的作业一飚起来,把别人家的资源吃得干干净净;你打开 ResourceManager 的 Web UI,看着一排黄黄绿绿的队列百分比,根本不知道是哪个参数让作业卡住。这篇文章想聊的,就是 YARN 调度器算法背后的那套逻辑:FIFO、容量调度器、公平调度器各自是怎么算的,DRF 是怎么把 CPU 和内存这两种没法直接比较的资源摆到同一个天平上的,以及作业从提交到容器启动的完整链路里,调度器到底在哪些环节做了决策。可能你平时不需要天天改调度器,但把算法框架理解了,排查“任务不跑、队列饿死、资源被抢”这类问题时,你会一下子知道该去看哪里。
1. 为什么需要一个专门的“调度器”:问题域与核心角色
1.1 资源不是“抢”出来的,是“算”出来的
先想一个极端情况:没有调度器会怎样。两个作业同时启动,各自认为自己应该占满整个集群,任务往每个 NodeManager 上发,容器一到就被对方挤掉,两边都在反复失败重试,最后谁也跑不完。这就是共享资源集群最原始的冲突:每个应用都只关心自己,没人关心全局。
调度器解决的,本质上是一个多目标优化问题:
- 公平性:多个用户、多个队列之间,每个人大概能拿到和其权重匹配的资源。
- 利用率:空闲资源要能被临时借用,不能因为“这是别人的队列”就空转。
- SLA/优先级:某些生产队列必须有保障,某些临时队列只能在别人不用的时候捡漏。
- 本地性:尽量把任务分到数据所在的节点或机架,减少网络IO。
这些目标互相打架。要保障利用率就得允许队列互相借用,要保障 SLA 就得在关键时刻从别的队列“抢”资源回来,也就是抢占。YARN 的三种原生调度器,其实就是对这几个目标做了不同的优先级排序。
1.2 调度器在 YARN 架构里的位置
先复习一下 YARN 里几个角色,后面聊算法都要用到:
- ResourceManager(RM):集群的中央大脑,调度器就住在 RM 里,它是一个可插拔的
ResourceScheduler实现。 - NodeManager(NM):每个节点上的代理,管理本节点的资源,通过心跳把节点状态汇报给 RM。
- ApplicationMaster(AM):每个作业的“项目经理”,负责申请容器、分配任务、监控进度。YARN 调度器的直接客户其实是 AM,不是用户代码。
- Container:资源分配的最小单位,本质是“多少内存 + 多少 vcore”的一个组合。
调度器干的事情可以压缩成一句话:接收 AM 发来的资源请求,在某个时刻把某个节点上的一部分资源,分配给某个应用的某个容器,并保证整体不违反队列容量和公平性约束。
这个“某个时刻”很关键。调度不是集中式地把所有待分配任务排成一个大队列慢慢算,而是由 NodeManager 的心跳驱动:RM 每次收到节点心跳,就看看哪些应用在这个节点上能放下请求,然后决定要不要分配。后面讲容量调度器的算法时你会发现,它的主循环就是处理 NodeUpdate 事件。
2. 三种原生调度器的算法骨架:FIFO、容量型与公平型
2.1 FIFO:只解决“谁先来”,解决不了“谁该让步”
FIFO 是 YARN 里最早也最简单的调度器,逻辑和它的名字一样:一个全局队列,按作业提交时间排序,先提交先跑。作业来了就排在队尾,前面的作业全部完成后才开始下一个。
它的算法本质是单队列、无抢占、无权重。好处是零配置、行为可预测、几乎没有性能开销;坏处是被一句老话概括了:队头阻塞。一个跑 10 小时的离线大作业排在队首,后面几十个小作业全部干等,哪怕集群资源明明够跑好几个小作业,也只能眼睁睁看着。所以现在几乎没人把 FIFO 用在生产环境,顶多单用户学习实验时用一用。它最大的价值反而是作为参照系:你看到其他调度器的复杂度,就能理解它们到底在解决 FIFO 留下的什么坑。
2.2 容量调度器:用百分比划分势力范围
Capacity Scheduler 的思路是“分蛋糕”:把集群资源按百分比切成多个队列,可以一级一级往下切,形成树结构。每个队列有一个保证容量(capacity),还有一个可以设定的最大容量(maximum-capacity)。
它的算法核心是:只要有队列没用到自己的保证容量,其他队列可以临时借用;一旦被借用的队列有需求,资源要归还。归还的强制手段就是抢占。在 Apache Hadoop 3.x 里,Capacity Scheduler 是默认调度器,也是我见过的大多数生产集群实际在用的那一个。它天生适合多部门、多业务共用一个集群的场景:每个部门一个队列,写死“离线分析占 50%、实时任务占 30%、临时查询占 20%”,互不干扰。
2.3 公平调度器:一切向“份额”看齐
Fair Scheduler 的思路不是划百分比,而是算“每个人应得的份额”。默认情况下,所有活跃的作业平分工集群资源:两个作业就是各 50%,三个就是各 33%;如果某个作业很早提交、占了 70%,新作业一进来,老的就要吐出来让它俩重回 50/50。
当然实际不会裸奔,它支持给队列配置weight(权重)、minResources(最少资源)和maxResources(最多资源),也支持抢占。所以公平调度器的算法难点就变成了:份额怎么算、什么时候可以夺回资源、夺回时先杀哪个容器。CDH 历史上把 Fair Scheduler 作为默认,和 Capacity Scheduler 形成了两大流派;现在新出的集群里 Capacity 更多见,但 Fair 在需要“所有作业平等竞争”的场景里仍然顺手。
2.4 一张表说清三者的区别
| 维度 | FIFO | Capacity Scheduler | Fair Scheduler |
|---|---|---|---|
| 队列结构 | 单队列 | 层级树队列 | 层级队列(可嵌套) |
| 分配依据 | 提交时间 | 各队列保证容量百分比 | 队列/作业的公平份额与权重 |
| 资源隔离 | 无 | 队列容量隔离,可临时借用 | 按份额动态调整 |
| 抢占 | 不支持 | 支持,需显式开启监控策略 | 支持,需显式开启 |
| 典型场景 | 单用户开发集群 | 多租户、有SLA的离线仓库 | 作业类型混杂、强调全局公平 |
| 配置复杂度 | 低 | 中高 | 中高 |
这里有个容易混淆的点:Capacity 和 Fair 的差别不在“有没有队列”,而在“资源怎么分”。Capacity 是绝对比例,写进去就要长期遵守;Fair 是动态目标,随着活跃作业数量变化不断重算。理解这一点,后面看它们的算法代码时就不会绕晕。
3. 多资源公平的核心算法:最大最小公平与 DRF
3.1 最大最小公平:从一盆水分给一堆花说起
先讲一个很朴素的公平概念:假设有一池子水,要分给几盆土质不同、缺水程度不同的花。最大最小公平的做法是:把所有花按需求排好,大家先均匀加水,直到哪一盆先浇满,就把它移出队伍,剩下的水继续在其余花之间均匀分配。反复进行,直到水用完或全部浇满。
这个“水盆”算法用一句话概括就是:让分到最少的一方尽可能多拿,不让任何一方被饿死。YARN 里的份额计算、抢占触发条件,本质上都是这个思想的变体。但现实有个麻烦——YARN 里的“水”不是一种,是两种:内存和 CPU vcore。如果只按内存算公平,一个 CPU 密集型的作业会说“我内存需求小,凭啥只能拿一样的份额?”
3.2 DRF与主导份额:一个两步计算的例子
DRF(Dominant Resource Fairness,主导资源公平)就是来解决多资源公平问题的。它的核心定义只有一句话:一个应用在某个资源维度上占用集群总量的比例,叫这个维度上的份额;所有维度份额里最大的那个,叫主导份额。调度时,永远给当前主导份额最小的应用分配资源。
听起来抽象,直接看例子。假设集群总共 10 个 vcore、10GB 内存,两个应用:
- 应用 A:每个容器要 2 vcore + 1GB,属于 CPU 型。
- 应用 B:每个容器要 1 vcore + 2GB,属于内存型。
按主导份额分配的过程如下(记 A 用 (CPU, 内存) 表示):
- 初始都是 (0,0),A 拿一个容器,变成 (2,1),主导份额 max(2/10, 1/10)=0.2。
- B 的主导份额还是 0,拿一个容器,变成 (1,2),主导份额挂到 0.2。
- 两者都是 0.2,随便选一个,比如 A 再拿,变 (4,2),主导份额 0.4;B 还是 0.2。
- B 主导份额小,B 再拿,变 (2,4),主导份额 0.4。
- 交替进行,直到 A 拿满 3 个容器 (6,3),B 拿满 3 个容器 (3,6)。
最终两者主导份额都是 0.6,剩余 1 vcore + 1GB 不够任何一方再开一个容器,只能空着。这个结果就是“两个应用在各自最缺的资源维度上达到了平等”,而纯内存公平计算根本做不到这一点——按内存看 A 只用了 30%、B 用了 60%,会错误地把资源继续倾向 A,最后 CPU 被 A 吃光,B 饿死。
| 轮次 | 判给谁 | A 已用(CPU,内存) | A 主导份额 | B 已用(CPU,内存) | B 主导份额 |
|---|---|---|---|---|---|
| 1 | A | (2,1) | 0.2 | (0,0) | 0 |
| 2 | B | (2,1) | 0.2 | (1,2) | 0.2 |
| 3 | A | (4,2) | 0.4 | (1,2) | 0.2 |
| 4 | B | (4,2) | 0.4 | (2,4) | 0.4 |
| 5 | A | (6,3) | 0.6 | (2,4) | 0.4 |
| 6 | B | (6,3) | 0.6 | (3,6) | 0.6 |
3.3 YARN里的DRF开关与资源计算器
YARN 不是默认就按 DRF 跑的。Capacity Scheduler 里有两个资源计算器:
DefaultResourceCalculator:只认内存,忽略 vcore。早期 Hadoop 的默认值,简单但有明显缺陷。DominantResourceCalculator:内存和 vcore 都参与计算,也就是 DRF。
生产集群如果 CPU 密集型作业多,我建议别用默认,显式在capacity-scheduler.xml里配置:
<property> <name>yarn.scheduler.capacity.resource-calculator</name> <value>org.apache.hadoop.yarn.util.resource.DominantResourceCalculator</value> </property>Fair Scheduler 则直接在队列上指定策略,fair是经典的内存公平,drf是多资源公平。队列里schedulingPolicy填drf就能打开。要注意的是,DRF 不是银弹:它解决的是“多维资源如何公平比较”的问题,但队列容量、用户限制、抢占门限这些约束仍然要靠调度器其他部分配合。
4. 容量调度器是怎么“算”的:心跳驱动、队列选择与本地性放宽
4.1 一次心跳引发的资源再分配
Capacity Scheduler 的分配动作不是定时跑一遍全局规划,而是被 NodeManager 的心跳牵着走。每个节点心跳到达 RM 后,调度器会进入 NodeUpdate 流程,大概以下几步:
- 拿到该节点空闲资源(空闲内存、空闲 vcore)。
- 从根队列开始,深度优先地扫描所有队列,找出“有 pending 请求、未达到最大容量、父队列也没超容量”的叶子队列。
- 在每个符合条件的叶子队列里,按该队列配置的作业排序策略挑一个应用。
- 用应用当前的资源请求去匹配节点资源:请求大小不能超过节点空闲资源,也不能超过单容器最大分配。
- 匹配成功就生成一个 Container,交给 AM。
这个流程的复杂点不在“扫描”,而在“队列怎么选、作业怎么排序、本地性怎么放宽”这三件事上,每个都是一套独立算法。
4.2 队列内部怎么挑作业:有序策略与用户限制
一个叶子队列里往往排着几十个作业,内部用ordering-policy决定谁优先。历史上默认是 FIFO,新版本提供了多种:fifo(先来先得)、fair(队列内再按份额平分)、drf(队列内按主导资源公平),还有按优先级、按容量等组合方式。我见过不少生产集群把叶子队列的ordering-policy配成fair,避免同一个部门内部总是大作业插队。
光有作业排序还不够,还得防“一个用户把整个队列吃光”。Capacity Scheduler 有用户限制机制:每个队列里单个用户最多使用的资源,由user-limit-factor乘以队列容量决定。比如队列容量是 60%,user-limit-factor默认是 1,那么单个用户最多占用 60%;如果两个活跃用户瓜分,每个还会进一步收缩。这个限制是为了保证“队列内部也能对多个用户公平”,很多刚上手的人只配了队列容量,忘了看 user 维度,结果一个数据团队的用户把整个部门的队列占满,其他团队投诉都找不到原因。
4.3 本地性延迟调度:宁可等一拍,也别跨机架
MapReduce 这类作业的数据放在 HDFS 上,任务放到数据所在节点跑就是“节点本地”,性能最好。但调度器不可能期望每个请求立刻就有节点本地可放:如果为了本地性死等,节点空闲浪费;如果立刻给任意节点,又会有大量网络传输。
YARN 采用的是延迟调度(Delay Scheduling):一个任务的请求首先只能匹配节点本地;等错过一定次数的心跳调度窗口后,允许放宽到机架本地;再往后才允许放到任意节点。这样既给了短作业“等等就能本地化”的机会,又不会让长尾任务无限等下去。这个参数在 Capacity Scheduler 里对应yarn.scheduler.capacity.node-locality-delay,含义大概是可以容忍错过多少个心跳,默认值在不同版本里不一样,生产上要看实际文档确认。公平调度器也有类似的yarn.scheduler.fair.locality.delay.node和yarn.scheduler.fair.locality.delay.rack。
还有一点容易被忽视:当一个应用已经有很多任务分散在各个节点后,继续等本地性意义不大。YARN 会动态计算“本地性收益下降”的临界点,所以延迟调度只在应用启动早期作用明显。你如果监视过 Spark/Flink 在 YARN 上的运行日志,会发现前几批任务分配慢,后面越来越快,跟这个机制直接相关。
4.4 一份可以抄的容量调度配置
假设集群要分两个一级队列:batch(离线生产)和 adhoc(临时分析),各占 60% 和 40%,batch 内按 fair 策略选作业:
<!-- capacity-scheduler.xml --> <configuration> <property> <name>yarn.scheduler.capacity.root.queues</name> <value>batch,adhoc</value> </property> <property> <name>yarn.scheduler.capacity.root.batch.capacity</name> <value>60</value> </property> <property> <name>yarn.scheduler.capacity.root.batch.maximum-capacity</name> <value>80</value> </property> <property> <name>yarn.scheduler.capacity.root.batch.ordering-policy</name> <value>fair</value> </property> <property> <name>yarn.scheduler.capacity.root.adhoc.capacity</name> <value>40</value> </property> <property> <name>yarn.scheduler.capacity.root.adhoc.maximum-capacity</name> <value>60</value> </property> </configuration>注意maximum-capacity是“上限”,含义是即便有空闲资源,这条队列最多也只能用到 80%。如果你希望队列之间可以充分借力,就不要把上限设得太低;如果业务上有硬隔离要求,再把上限打到 100%、90% 这样去“封顶”。capacity之和最好等于 100,不然未被分配的部分就真的闲置了;大于 100 则 RM 启动校验会直接报错。
5. 公平调度器的份额计算与抢占博弈
5.1 公平份额是怎么从权重里长出来的
Fair Scheduler 的分配单位是队列,队列套队列形成层级。对每个队列,调度器会算两个份额:
- steadyFairShare(稳态份额):把所有队列(包括暂时没有作业的空队列)都算进去,按权重和 min/max 资源算出来的长期均衡值。
- instantaneousFairShare(瞬时份额):只算当前活跃的队列,资源在这些活跃队列之间按权重分。
为什么分两个?因为调度器需要区分“长期该这样”和“现在立刻该这样”。比如一个高权重队列暂时没作业,它的份额就应当被其他队列借用;一旦它提交作业,调度器不会要求其他队列立刻归还,而是逐步收敛回去,这个收敛过程就是看瞬时份额和稳态份额之间的差距。
权重计算很简单:某队列的公平份额 = 集群可用资源 × 该队列权重 / 所有活跃队列权重之和。如果再加上minResources,就先给每个队列填到最少资源,再对剩余部分按权重分配。maxResources相当于硬顶,超过之后即使权重再高也不能拿更多。
5.2 当“公平”违约:抢占线程的完整决策链
公平调度器的抢占是一个独立线程定期跑的,不是每一次分配时实时判断。它大致做这几件事:
- 定时检查每个队列的使用量与公平份额、最小资源之间的差距。
- 只有“差距超过阈值且持续了一段时间”才动手,避免震荡。
- 决定抢占后,选出那些“超出自身份额最多的队列”里的容器作为牺牲品。
- 先发警告,给一段时间让超额队列自己释放(比如作业自己降并发、或者等容器自然结束),过了等待期仍不释放才真正 kill 容器。
关键时刻的配置是yarn.scheduler.fair.preemption.enabled。配套的还有触发前等待时间、集群总使用率阈值等参数,不同 Hadoop 版本命名略有差异,建议以官方 Configuration 文档为准。我的实践体会是:抢占的参数必须成套调,只开一个开关、其他全默认,很容易出现“杀过来的容器刚算出结果前一刻被抢走”的惨案。
5.3 抢占的代价与关闭时机
这里要给两个泼冷水的观点。第一,抢占不是免费的。被 kill 的容器里如果正在跑 Map 任务,作业会重新执行,浪费的是整个计算过程;抢占频繁时集群的整体吞吐反而下降。第二,并非所有集群都需要抢占。如果队列之间容量边界清晰、作业类型差异不大,关掉抢占、靠队列容量的自然流动就够了。
一个相对稳妥的开启策略是:只对“有 SLA 的队列”开启抢占保证,让它们低于 minResources 时能抢回来;对一般队列,主要用maximum-capacity和权重来控制,避免全局抢占。我踩过的坑是某个流批一体的集群开了全局抢占,结果每小时都在杀任务,最后查日志发现全是 preemption kill,而不是资源不够。
6. 从作业提交到容器启动:调度器在整条链路上的作用
6.1 提交阶段:客户端、HDFS暂存目录与RM的第一次接触
用户执行一条hadoop jar xxx.jar MainClass之后,发生的第一件事不是直接进调度器,而是先准备“作业描述文件”。客户端会做三件事:
- 对输入数据计算分片(split),生成作业计划。
- 把 jar 包、配置、分片信息上传到 HDFS 的暂存目录(通常是
/user/<用户名>/application_<时间戳>这种路径)。 - 通过 RPC 向 ResourceManager 提交一个
ApplicationSubmissionContext,里面包含了 jar 路径、AM 需要的资源大小、队列名、优先级等。
RM 收到提交请求后,会先把这条记录写入状态存储(用于故障恢复),再交给内部的 AppManager 创建应用对象。到这一步为止,调度器还没参与,它看到的只是一个“新应用等待启动”的事件。
6.2 调度器第一次分配:给ApplicationMaster找个窝
每个 YARN 应用要跑起来,第一步是先启动一个 ApplicationMaster。这个 AM 本身也需要一台节点、一份资源,所以 RM 要向调度器申请一个“AM 容器”。这个申请和普通任务申请走的是同一个调度流程,只不过优先级特殊、请求次数少。
刚好这就是很多人定位问题卡住的点:如果 AM 容器要求的资源超过yarn.scheduler.maximum-allocation-mb,调度器永远找不到能放下的节点,作业就一直停在 ACCEPTED。这种问题在日志里反而不容易看到具体原因,因为 RM 端只显示“等待资源”。我排查过不少次,最后都是把 AM 的资源要求降到单容器上限之内,或者把上限调大。
6.3 AM的循环心跳:ResourceRequest与Allocation的往返
AM 启动后,会向 RM 的ApplicationMasterProtocol注册。它向调度器提交的不是“我要跑 100 个 Map”,而是一批批ResourceRequest对象。每个请求包含三件关键信息:
- 资源量:需要多少内存和 vcore。
- 本地性偏好:指定节点、指定机架,还是任意节点。
- 优先级:先满足哪些任务。
调度器把请求吃进去,不停产出Allocation回应,里面有被分配的Container列表。AM 拿到这些 Container 后,再通过 NodeManager 启动具体任务。这个过程不是一次性的:AM 通过周期心跳反复“申请 - 拿容器 - 启动任务 - 汇报进度”,直到作业完成。
6.4 调度器的决策点到底在哪里
把整条链路摊开,调度器的决策点其实就两个:
- 第一次:决定 AM 容器放哪个节点,这是“作业能不能启动”的闸门。
- 之后无数次:决定各种资源的容器分配给哪些作业的哪些任务,这是“作业跑得快不快”的闸门。
需要刷新认知的是,YARN 调度器从来不会替 AM 决定“任务在容器里怎么执行”。任务调度(Map 阶段、Reduce 阶段内部的任务编排)是 MRAppMaster 自己干的,YARN 调度器只负责“给资源”。很多新手把两者混在一起,出了问题以为调度器算法有 bug,其实调度器早就把容器发出去了,是 AM 自己没用好。
7. 生产环境里的选型、调参与踩坑实录
7.1 选型原则:先问你需要SLA还是需要和谐
如果集群要承载多个业务线、每个业务线有明确的资源预算和响应时间要求,选 Capacity Scheduler。它的百分比模型和用户限制,天然适合“按部门/业务划分势力范围”。如果集群是一群临时分析作业、每个人谈好的共享池,希望所有作业尽量平等地分资源,选 Fair Scheduler,再配合权重点出重要作业。
FIFO 我基本只在单机实验环境用。还有一个真实情况是:很多发行版和大数据中间件已经在yarn-site.xml里写好了默认调度器,比如阿里云 EMR 默认就是 Capacity 系,你要改之前先确认到底用的是哪个,免得改了一套配置但没生效。
7.2 三个我踩过的坑
坑一:队列容量加起来超过 100%。有一次同事手滑把两个队列都配成了 70%,RM 启动直接拒绝,集群怎么也起不来。这也是 Capacity Scheduler 的一个“好处”:配置校验做得很严格,提前暴露问题。所以上线前养成习惯,用yarn scheduler相关命令或者直接看启动日志确认一下Queues的容量总和。
坑二:maximum-allocation-mb 太小,导致所有作业的 AM 都卡在 ACCEPTED。这个我前面提过,是生产环境最高频的“集群没坏,作业就是不动”的原因。默认的单容器上限在不同发行版里不一样,有 8GB、16GB、甚至 32GB,如果你的作业 AM 要求 8GB,而上限刚好也是 8GB,看似相等其实可能因为一点点 overhead 就被拒。建议把最大分配值放宽到集群单节点内存的一半或更高,避免这种无谓的边界问题。
坑三:用户限制害死人。某个部门二十个人共用一个队列,队列容量 70%,user-limit-factor 默认 1,表面看没问题。但实际某个同事一次提交了 20 个作业,因为“单个用户不能超过队列容量”,这些作业加起来只能吃到 70%,每个作业分到的资源非常有限,全部慢悠悠地跑。最后把user-limit-factor调到 2、3 才让多作业用户能充分用资源。这种问题在 RM 队列页面上能看到“单用户已用资源明显高于/低于预期”的信号,关键是要有意识去查用户维度。
7.3 能救命的监控命令与日志线索
最后列几个我排查调度问题时必定会用的命令,都是 YARN 自带能力:
yarn application -list -appStates RUNNING,ACCEPTED yarn applicationattempt -list <application_id> yarn queue -status <queue_name> yarn logs -applicationId <application_id> yarn scheduler # 通过 RMActive 进程的管理命令,具体以发行版为准yarn queue -status能直接看到队列的 usedCapacity、absoluteCapacity、numPendingApplications 这些指标,用来确认“是队列满了还是调度器没分配”非常快。RM 的 Web UI(默认 8088 端口/cluster/scheduler页面)里,每个队列的颜色块能直观反映容量占用,看到某队列长期是深色、另一个队列几乎全灰,基本可以断定是 maximum-capacity 限制了弹性共享。
我自己在新集群上线前的调参顺序通常是这样的:先把minimum-allocation和maximum-allocation设好,让容器规格符合绝大多数作业的请求;再配队列容量和 user-limit-factor,先保证硬隔离能立起来;最后才考虑抢占,而且一定从小阈值开始放,观察一天日志再决定要不要加大力度。调度器这东西没有一口吃成胖子的方案,它更像一个动态的平衡器,你给它多一点现场数据,它就给你少一点玄学故障。
写到这里刚好想起一个经验:很多时候用户在群里喊“集群死了”,其实不是节点挂掉,更不是调度器算法出 bug,而是某个队列的maximum-capacity设成了 100%,另一个关键队列又设了很低的capacity,两边凑不出弹性。看懂了调度器怎么算的之后,这种“你以为的资源空闲”和“实际上的资源被锁死”的落差,就会变成一眼能识破的配置问题。