9.1 给机器学习推理的自适应分配(adaptive allocations)加了一个开关:空闲多久之后把分配数缩到 0。默认 24 小时。 这条改动直接关系到成本——推理端点空转是在烧钱的。
说明
来源和边界先交代:本文的依据出自 v9.4.2 标签下的官方文件(9.1.0.yml、MachineLearning.java、AdaptiveAllocationsScalerService.java),文中引用的原文均来自这些文件。示例取自官方示例;哪些是文档写明、哪些是我的推断,正文里逐处标注。
实测环境:本地另起一个 Elasticsearch 9.4.2 单节点实例(官方 tar 包,端口 9200,独立 path.data),安全特性与机器学习均关闭;实验产生的索引与视图都在这台实例上。文末「在 9.4.2 上跑一遍」里的命令与响应均取自该实例。
图 1:官方文件 9.1.0.yml(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 9.1.0.yml
官方条目
9.1 的 changelog 标为 enhancement(9.1.0.yml 第 1785–1788 行 @ v9.4.2),area: Machine Learning:
Make Adaptive Allocations Scale to Zero configurable and set default to 24h
只有一行 summary,但信息量够——可调、默认 24h。实质细节来自源码。
图 2:官方文件 MachineLearning.java(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 MachineLearning.java
先说自适应分配是什么
机器学习推理(比如跑一个嵌入模型)需要计算资源。ES 里的做法是为每个部署的模型分配若干个分配(allocation),可以理解为"处理推理请求的工作单元"。
自适应分配让这些分配数随负载自动变化:
- 请求多 → 增加分配;
- 请求少 → 减少分配。
目的是省资源:不用一直按峰值容量占着。
图 3:官方文件 AdaptiveAllocationsScalerService.java(v9.4.2 标签)。来源:GitHub elastic/elasticsearch v9.4.2 AdaptiveAllocationsScalerService.java
"缩到 0" 是什么
自适应分配有个下限问题:最少保留几个?
如果下限是 1,那么即使完全没有请求,也始终占着一份资源。
缩到 0(scale to zero)就是把下限降到 0——长时间没请求就完全释放。
好处:彻底不占资源,省钱。 代价:下次来请求时要重新分配,有个冷启动延迟。后面会讲这个代价怎么估。
源码里的设置定义
设置在 MachineLearning.java 第 811–817 行(源码 @ v9.4.2):
public static final Setting<TimeValue> SCALE_TO_ZERO_AFTER_NO_REQUESTS_TIME = Setting.timeSetting(
"xpack.ml.trained_models.adaptive_allocations.scale_to_zero_time",
TimeValue.timeValueHours(24),
TimeValue.timeValueMinutes(1),
Property.Dynamic,
Setting.Property.NodeScope
);
逐项看:
- 设置名:
xpack.ml.trained_models.adaptive_allocations.scale_to_zero_time - 默认值:
TimeValue.timeValueHours(24)—— 24 小时,和 changelog 说的 default 24h 对得上 - 最小值:
TimeValue.timeValueMinutes(1)—— 1 分钟。想设得比这更小会被拒 Property.Dynamic—— 可以动态改,不用重启节点Setting.Property.NodeScope—— 节点级设置
三个实用信息:默认 24h、最小 1 分钟、可动态改。
上面那段注释说了什么
定义之前有几行注释(第 805–810 行):
* interval hasn't passed, the minimum number of allocations will always be
* larger than zero.
大意是:在这个时间间隔还没过去之前,最小分配数始终大于零。
这句话把机制说清楚了:是要等这个间隔过去,不是"一看没请求就归零"。在这个间隔内,下限保持大于 0。
所以这个设置名的字面意思(scale_to_zero_time)可以更准确地理解为:"空闲多久之后才允许缩到 0"。
服务侧怎么用
具体执行在 AdaptiveAllocationsScalerService.java(第 240 行、第 245 行,源码 @ v9.4.2):
new AtomicLong(MachineLearning.SCALE_TO_ZERO_AFTER_NO_REQUESTS_TIME.get(settings).getSeconds()),
...
MachineLearning.SCALE_TO_ZERO_AFTER_NO_REQUESTS_TIME,
第二处(第 245 行)看起来是注册了设置更新监听——配合前面的 Property.Dynamic,意味着改了设置之后服务会拿到新值,不用重启。
(说明:"第 245 行是更新监听"是读法,从 Dynamic 属性和这段代码的写法推断,未逐个跟踪调用链确认。)
怎么配
节点级、可动态改,所以两种配法都行。
静态配(配置文件):
xpack.ml.trained_models.adaptive_allocations.scale_to_zero_time: 1h
动态改(不用重启):
curl -X PUT 'localhost:9200/_cluster/settings' -H 'Content-Type: application/json' -d '{
"persistent": {
"xpack.ml.trained_models.adaptive_allocations.scale_to_zero_time": "1h"
}
}'
注意最小值是 1 分钟,写 30s 会被拒。
(说明:这条命令已在文末实测——集群设置 API 接受 persistent 方式,改动即时生效,下限也在请求校验阶段拦住。)
怎么判断自己有没有在空转
上面反复说"空转浪费",但你得先知道自己有没有这个问题。这里给个可操作的判断方法。
推理端点的分配情况可以看(具体 API 名称以你版本的文档为准,未核对准确路径)。核心是看两个东西:
- 当前有多少个分配;
- 这些分配的实际使用率。
判断标准:如果某个端点长时间保持 1 个分配,但请求量接近 0,那就是典型的空转——它一直占着资源,却几乎没干活。
更简单的办法:看监控里的推理请求量曲线。有没有连续几小时为 0 的区间?
- 有 → 适合缩到 0,把这个时间设得比"空闲区间"短一些;
- 没有(请求始终有,哪怕很少)→ 缩到 0 收益有限。
倾向是用请求量曲线判断,因为它直观,不用理解分配的内部机制。而且这个曲线多半已经有了——ML 相关的监控指标通常默认采集。
(说明:具体的 API 路径和指标名未核对,上面是判断思路,不是操作步骤。)
该设成多少
这是本篇唯一需要判断的地方。思路:
我的判断是:核心权衡是省下的资源 vs 冷启动的频率,两边都要算,只算一边会得出错误结论。
- 设得短(比如 10 分钟)→ 省得多,但稍有空闲就释放,下次请求要重新分配;
- 设得长(比如 24 小时,默认)→ 冷启动少,但空转时间长。
判断顺序:
- 先看推理请求是不是有规律性空闲。比如业务只在白天有流量,夜里完全没请求——那夜里就该缩到 0,设个 1–2 小时很合理;
- 如果请求是稀疏但持续的(比如每分钟都有几条),那缩到 0 意义不大——很快又要起来,反而不如保持 1 个;
- 如果对延迟极其敏感,冷启动不可接受,那就别让它归零(设很长,或者不用自适应分配)。
第 2 点最容易判断错:稀疏请求看起来"资源浪费",但缩到 0 再起来的开销可能更大。判断依据是请求间隔和冷启动耗时的比值——如果间隔接近冷启动时间,缩到 0 就是纯粹的额外开销。
一个要提醒的点
冷启动的代价不是固定的。
加载一个模型要多久,取决于模型大小、节点状态、有没有缓存。小模型可能秒级,大模型可能分钟级。
所以"缩到 0 省下的资源"和"冷启动带来的延迟"不能简单比较。 建议是:先测一次冷启动耗时,再决定这个时间设多长。
具体做法:手动把某个端点的分配数降到 0,然后发一个请求,计时。拿到这个数之后,判断才有依据。
(说明:未实测任何模型的冷启动时间,上面是方法建议。)
为什么默认给的是 24 小时
默认值是个值得琢磨的东西——官方为什么选 24h,而不是 1h 或者 30 分钟?
这是推断,官方没解释(changelog 只说 "set default to 24h")。推测的理由:
一,24 小时对应"一天"这个自然的业务周期。 多数业务的请求都有日内波动——白天多、夜里少。24 小时意味着"只要一天内还有请求,就不会归零"。这个语义很清晰。
二,它足够保守。 冷启动是有代价的,默认设得保守可以避免"用户升级后发现推理变慢了"这种抱怨。先保证不出问题,需要省资源的用户自己去调——这个取向在 ES 的很多默认值上都常见。
三,它过滤掉了"短暂空闲"。 午休一小时没请求,不该触发归零;放一整天的假才应该。24 小时正好把这两类分开。
这三条合起来说明一个设计取向:默认值面向"不出问题",激进选项留给有明确需求的人。
这个取向合理。尤其对"缩到 0"这种可能带来性能回退的开关——默认保守是对的。
(再说明一次:以上是推断,官方没解释过为什么是 24h。)
和别的 ML 资源改动放一起看
9.x 在 ML 资源使用上做了不少调整,前面写过几篇:
| 篇 | 改动 | 解决什么 |
|---|---|---|
| RERANK 命令 | RERANK 的 1000 行护栏 | 限制调用量(费用) |
| macOS x86_64 上的机器学习 | macOS x86_64 禁用 ML | 平台限制 |
| 本篇 | 自适应分配缩到 0 | 减少空转占用(资源) |
前两条和本篇的关注点不一样:
- 本篇和 RERANK 命令 都跟成本有关,但层面不同——一个是"每次调用要花钱",一个是"占着资源要花钱";
- 判断这类"省资源"的改动会持续增加,因为 ML 推理是 ES 里资源占用最重的部分之一。
这条改动怎么看
这条改动的价值很直接:给了一个能省钱、且成本可控的开关。
说它"成本可控"是因为:
- 可动态改 —— 试错不用重启;
- 有默认值 —— 不改也不会变差;
- 有最小值限制 —— 不会因为设得过激而出问题。
建议:
- 如果你在用自适应分配且有明显空闲时段 → 值得调,能省资源;
- 如果请求持续且密集 → 不用管,默认值挺合理;
- 不确定 → 先看监控里推理请求的分布,有没有长时间为 0 的区间。没有就不动,有就调。
不要做的事:一上来就设成最小值(1 分钟)。那等于"稍不留神就释放",冷启动会非常频繁,省下的资源可能还不够抵消性能损失。
在 9.4.2 上跑一遍
设置名字、默认值、下限、能不能动态改,四条命令全验得出来。
图 4:9.4.2 单节点实例的实测输出。默认 scale_to_zero_time=24h、scale_up_cooldown_time=5m;改成 1h 后读回 1h;30s 被拒(must be >= [1m]),2m 通过;清掉覆盖后默认仍是 24h。
curl -s 'http://localhost:9200/_cluster/settings?include_defaults=true&filter_path=defaults.xpack.ml.trained_models.adaptive_allocations' \
| jq -c '.defaults.xpack.ml.trained_models.adaptive_allocations'
# {"scale_to_zero_time":"24h","scale_up_cooldown_time":"5m"}
curl -s -X PUT 'http://localhost:9200/_cluster/settings' -H 'Content-Type: application/json' \
-d '{"persistent":{"xpack.ml.trained_models.adaptive_allocations.scale_to_zero_time":"1h"}}' \
| jq -c '.acknowledged'
# true
curl -s 'http://localhost:9200/_cluster/settings' \
| jq -c '.persistent.xpack.ml.trained_models.adaptive_allocations.scale_to_zero_time'
# "1h"
curl -s -X PUT 'http://localhost:9200/_cluster/settings' -H 'Content-Type: application/json' \
-d '{"persistent":{"xpack.ml.trained_models.adaptive_allocations.scale_to_zero_time":"30s"}}' \
| jq -r '.error.root_cause[0].reason'
# failed to parse value [30s] for setting [...scale_to_zero_time], must be >= [1m]
curl -s -X PUT 'http://localhost:9200/_cluster/settings' -H 'Content-Type: application/json' \
-d '{"persistent":{"xpack.ml.trained_models.adaptive_allocations.scale_to_zero_time":null}}' >/dev/null
curl -s 'http://localhost:9200/_cluster/settings?include_defaults=true&filter_path=defaults.xpack.ml.trained_models.adaptive_allocations' \
| jq -c '.defaults.xpack.ml.trained_models.adaptive_allocations.scale_to_zero_time'
# "24h"
图 4 把正文标注「配置命令未执行」的那处补上了:设置确实可以用 persistent 动态改,1h 读回还是 1h;源码里的最小 1 分钟也在请求校验阶段生效(30s 直接 400,2m 通过)。清掉覆盖后默认仍是 24h。
边界:
- 图 4 出自 9.4.2 单节点实例,没有部署推理模型,实际缩到 0 的行为与冷启动耗时未测;
- 「第 245 行是更新监听」仍是源码读法,未跟踪调用链;
- 推理端点分配状态的 API 路径未核对。
写在最后
一条很实用的改动:把"空闲多久缩到 0"变成可配项,默认 24 小时。
一,设置是 xpack.ml.trained_models.adaptive_allocations.scale_to_zero_time,默认 24 小时。 含义是"空闲多久之后才允许缩到 0",不是"空闲多久就归零"。
二,最小值 1 分钟,Dynamic 可动态改,NodeScope。 结论是:试错成本低,不用重启——改完立刻能观察效果,这种设置值得按自己的负载调一次,而不是一直用 24 小时。
三,该设多少取决于请求分布和冷启动耗时。 稀疏但持续的请求不适合缩到 0;有明显空闲时段的适合。决定前先测一次冷启动时间,不然就是在猜。
另外诚实标注三处:"第 245 行是更新监听"是读法(依据 Dynamic 属性和代码写法,未跟踪调用链);配置命令已在文末执行,但冷启动耗时未实测,只给了测量方法;推理端点分配状态的 API 路径未核对。
参考
- changelog 9.1.0:Make Adaptive Allocations Scale to Zero configurable and set default to 24h(第 1785–1788 行)@ v9.4.2
MachineLearning.java:设置定义(第 811–817 行)与机制注释(第 805–810 行)@ v9.4.2AdaptiveAllocationsScalerService.java:服务侧的取值与更新(第 240、245 行)@ v9.4.2
本文对照 Elasticsearch
v9.4.2标签的 changelog 与源码核对,引文与代码逐字取自原文。「在 9.4.2 上跑一遍」里已实际读到默认scale_to_zero_time=24h、scale_up_cooldown_time=5m,用persistent改成1h后读回1h,30s被must be >= [1m]拒绝、2m通过,清掉覆盖后默认仍是 24h。**"第 245 行为设置更新监听"**仍是源码读法,未跟踪调用链确认;冷启动耗时未实测,仅给出测量方法;推理端点分配状态的 API 路径未核对。
