点击上方卡片,关注「CloudPilot AI」
回复关键词【案例】
查看多邻国、Canva等名企的云端降本实践
01 / 背景
对于使用大镜像的 Kubernetes 工作负载,冷启动分为两段:先是节点准备镜像,然后应用才开始启动。在服务能够加载代码、插件、配置、模型或索引之前,containerd 必须先让镜像的文件系统就绪。
在传统的 containerd overlayfs 路径下,节点必须先下载并解压整个镜像,容器才能启动。懒加载(lazy loading)改变了这个顺序:它不再要求整个文件系统在进程启动前全部落到本地,而是通过索引挂载镜像,文件在被访问时才按需拉取。
Hermes 让这套流程变成策略驱动(policy-driven)。应用团队继续使用原始的 OCI 镜像,无需重建镜像、无需发布转换后的镜像标签、无需修改 Dockerfile,也无需改动 Pod 的镜像引用。平台团队只需定义一个 HermesPolicy,Hermes 便会在集群中准备好懒加载所需的制品(artifacts)。
上一篇 Hermes 基准测试主要关注 Pod Ready 和镜像拉取的表现。这次我们更进一步:测量从 Pod 被调度到一台全新 EKS 节点,直到服务返回第一个成功的 HTTP 200 响应的整条路径。
这样的口径让测量贴近 Hermes 真正能影响的环节:镜像拉取、挂载、容器启动,以及随后的应用启动。需要说明的是,首个 HTTP 200 仍然包含运行时初始化、库与配置读取、插件加载、模型或索引加载以及服务自举(bootstrap),因此它的百分比提升会小于纯镜像拉取的提升。
02 / 测试对象
我们测试了三个使用大体积公共镜像的 HTTP 工作负载。它们恰好都是基于 Java 的,但 Hermes 工作在应用运行时之下:它改变的是镜像文件系统的准备和读取方式,而不是 Java 的启动方式。
🔹 Solr:docker.io/library/solr:10.0.0
🔹 OpenSearch:docker.io/opensearchproject/opensearch:2.19.1
🔹 Spark master:docker.io/apache/spark:python3-java17
这些 Pod 使用上游公共镜像,并以 digest 固定版本。没有引入任何 Hermes 专用的转换镜像标签。
我们对比了两条路径:
🔹 overlay:标准的 containerd overlayfs 镜像拉取与解压。
🔹 Hermes:相同的工作负载镜像,在目标节点上走 Hermes 懒加载路径。
每个服务、每种路径各运行三次,共计 18 次目标测试。
03 / 测试环境
基准测试运行在 EKS 上:
🔹 Kubernetes:v1.34.9-eks-93b80c6
🔹 节点操作系统:Amazon Linux 2023
🔹 运行时:containerd 2.2.4
🔹 目标实例类型:m6i.large
🔹 平台:linux/amd64
🔹 节点隔离:每次目标测试都使用一台全新的目标节点
每次测试开始前,基准测试都会先移除上一次的目标节点,以避免测到上一个 Pod 遗留的本地镜像缓存。
04 / 实验步骤
步骤一:安装 Hermes Controller 与 CRD
Controller 会监听 HermesPolicy 资源和 Pod。当它发现一个匹配的镜像时,就会构建并缓存懒加载制品。
kubectl apply -f deploy/hermespolicy-crd.yaml
kubectl apply -f deploy/hermes-controller-eks.yaml
kubectl -n hermes-system rollout status deploy/hermes-controller
步骤二:配置两个目标 NodePool
测试使用了两组 NodePool/NodeClass:
🔹 一个 overlay NodePool:标准的 AL2023/containerd overlay 路径。
🔹 一个 Hermes NodePool:AL2023/containerd,并通过 EC2NodeClass 的 userData 安装 Hermes 守护进程。
启用 Hermes 的 NodeClass 会在节点引导(bootstrap)阶段安装该守护进程:
export HERMES_INSTALLER_URL="..."
export HERMES_DAEMON_URL="..."
export HERMES_DAEMON_SHA256="..."
curl -fsSL "${HERMES_INSTALLER_URL}" | \
HERMES_DAEMON_URL="${HERMES_DAEMON_URL}" \
HERMES_DAEMON_SHA256="${HERMES_DAEMON_SHA256}" \
bash -s --
两个 NodePool 使用相同的实例类型和容量类型。
步骤三:创建 HermesPolicy
该策略选中了三个基准测试的工作负载镜像:
apiVersion: hermes.cloudpilot.ai/v1alpha1
kind:HermesPolicy
metadata:
name:benchmark-images
spec:
imageSelectors:
-imageRegex:'.*(solr:10\.0\.0|opensearchproject/opensearch:2\.19\.1|apache/spark:python3-java17).*'
platforms:
- linux/amd64
步骤四:等待制品就绪
Hermes 的制品准备发生在目标 Pod 启动计时窗口之外,它是 controller 侧针对匹配镜像的一次性准备步骤。
本次运行中,准备耗时为:
🔹 Solr:58.918s
🔹 OpenSearch:2m44.999s
🔹 Spark:1m4.493s
目标工作负载的基准测试在制品达到 Ready 后才开始。
步骤五:在全新节点上运行目标 Pod
对每个服务和每种路径,基准测试会:
1. 删除上一次的目标 Pod 和 Service。
2. 移除上一次的目标节点。
3. 等待目标节点完全消失。
4. 在一台稳定节点上创建一个 watcher Pod。
5. 在一台全新的目标节点上创建目标工作负载 Pod。
6. 记录镜像拉取、容器启动、Pod Ready 以及首个 HTTP 200 的时间。
首个 HTTP 200 的时间戳是在 watcher 收到成功的 HTTP 响应之后记录的,而不是在发出请求之前。
步骤六:工作负载 YAML 与 HTTP 200 探测
两种路径使用完全相同的应用 YAML。唯一的调度差异是目标节点池:overlay 运行使用 hermes-overlay,Hermes 运行使用 hermes-startuplocal。
每个目标 Pod 由一个 headless Service 暴露。该 Service 通过 app、variant 和 e2e-run 标签选中某一次运行特定的 Pod:
apiVersion: v1
kind:Service
metadata:
name:opensearch-overlay-r1-20260622025902-ope
namespace:hermes-e2e
spec:
clusterIP:None
publishNotReadyAddresses:true
selector:
app:opensearch-e2e
variant:overlay
e2e-run:20260622025902-opensearch-overlay-r1
ports:
-name:http
port:9200
targetPort: http
publishNotReadyAddresses: true 让 watcher 能在 Pod 被标记为 Ready 之前,就通过 Service DNS 解析并调用该 Pod。否则 Service 通常会在就绪成功之前隐藏该端点,使得测量首个成功的 HTTP 200 变得更困难。
下面是基准测试中使用的 OpenSearch 目标 Pod。Hermes 运行使用相同的容器规格,仅改动了运行标签/名称以及 karpenter.sh/nodepool 选择器:
apiVersion: v1
kind:Pod
metadata:
name:opensearch-overlay-r1-20260622025902-ope
namespace:hermes-e2e
labels:
app:opensearch-e2e
variant:overlay
e2e-run:20260622025902-opensearch-overlay-r1
spec:
restartPolicy:Never
terminationGracePeriodSeconds:1
nodeSelector:
kubernetes.io/os:linux
kubernetes.io/arch:amd64
karpenter.sh/nodepool:hermes-overlay
containers:
-name:opensearch
image:docker.io/opensearchproject/opensearch:2.19.1@sha256:7ad3c515e43fb1642ddf2181dfd03402e42e85a16030e098ed1f3fc1404d7e89
imagePullPolicy:IfNotPresent
args:
-opensearch
--Ediscovery.type=single-node
--Ehttp.host=0.0.0.0
--Etransport.host=127.0.0.1
env:
-name:OPENSEARCH_JAVA_OPTS
value:"-Xms512m -Xmx512m"
-name:DISABLE_SECURITY_PLUGIN
value:"true"
ports:
-name:http
containerPort:9200
readinessProbe:
httpGet:
path:/_cluster/health?local=true
port:http
periodSeconds:1
timeoutSeconds:1
failureThreshold:900
resources:
requests:
cpu:"1"
memory:"2Gi"
ephemeral-storage:"8Gi"
limits:
memory: "3Gi"
Solr 和 Spark 使用相同的 Pod 结构,它们的工作负载容器配置如下:
# Solr
-name:solr
image:docker.io/library/solr:10.0.0@sha256:c5d3e51740f81612dac200c91908c253bf8302dca330874d6dcef23dacafc723
imagePullPolicy:IfNotPresent
env:
-name:SOLR_HEAP
value:"512m"
ports:
-name:http
containerPort:8983
readinessProbe:
httpGet:
path:/solr/admin/info/system
port:http
periodSeconds:1
timeoutSeconds:1
failureThreshold:900
resources:
requests:
cpu:"500m"
memory:"1Gi"
ephemeral-storage:"4Gi"
limits:
memory:"3Gi"
# Spark master
-name:spark-master
image:docker.io/apache/spark:python3-java17@sha256:6fb854a580e552290a21d7b9c6214f2d8840733e63a90ff687a2ffce80f45ef9
imagePullPolicy:IfNotPresent
command:
-/opt/spark/bin/spark-class
args:
-org.apache.spark.deploy.master.Master
---host
-0.0.0.0
---port
-"7077"
---webui-port
-"8080"
ports:
-name:web
containerPort:8080
-name:master
containerPort:7077
readinessProbe:
httpGet:
path:/
port:web
periodSeconds:1
timeoutSeconds:1
failureThreshold:900
resources:
requests:
cpu:"500m"
memory:"1Gi"
ephemeral-storage:"4Gi"
limits:
memory: "4Gi"
HTTP 200 的时间戳来自一个独立的 watcher Pod,它运行在一台稳定的托管节点上,反复调用 Service 的 DNS 名称,并且只有在收到 HTTP 200 状态码后才打印 first_http_200_ns:
apiVersion: v1
kind:Pod
metadata:
name:watch-opensearch-overlay-r1-20260622025902-ope
namespace:hermes-e2e
labels:
app:opensearch-first-200
variant:overlay
e2e-run:20260622025902-opensearch-overlay-r1
spec:
restartPolicy:Never
nodeSelector:
eks.amazonaws.com/nodegroup:eks-spot-20260620152041180300000015
containers:
-name:watcher
image:python:3.12-alpine
imagePullPolicy:IfNotPresent
env:
-name:TARGET_URL
value:http://opensearch-overlay-r1-20260622025902-ope.hermes-e2e.svc.cluster.local:9200/_cluster/health?local=true
command:
-sh
--lc
-|
python - <<'PY'
import os
import time
import urllib.request
url=os.environ["TARGET_URL"]
print(f"watcher_start_ns={time.time_ns()}",flush=True)
while True:
try:
withurllib.request.urlopen(url,timeout=0.5)as response:
ifresponse.status==200:
print(f"first_http_200_ns={time.time_ns()}",flush=True)
break
except Exception:
pass
time.sleep(0.25)
PY
05 / 测试结果
|
|
|
|
|
|
|
|
|
|
|---|---|---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OpenSearch 的拉取时间变化最大,从 20.371s 降到 2.998s。Spark 的首个 HTTP 200 变化最大,从 20.191s 降到 13.304s。
06 / 这些数字意味着什么
Hermes 对镜像拉取的影响最为明显。在三个工作负载上,镜像拉取时间下降了 71% 到 85%。
首个 HTTP 200 提升了 20% 到 34%。这个数字低于拉取时间的提升,因为首个 HTTP 200 包含的不只是镜像拉取:
🔹 容器创建
🔹 进程启动
🔹 运行时初始化
🔹 代码、库、配置或模型的读取
🔹 插件加载
🔹 服务就绪检查
以 OpenSearch 为例,overlay 基线从 Pod 被调度到首个 HTTP 200 约为 38 秒。Hermes 把拉取时间从约 20 秒降到约 3 秒,服务到达首个 HTTP 200 的时间提前了约 7.6 秒。剩下的时间是容器启动之后 OpenSearch 自身的启动工作。
这正是表格同时给出镜像拉取和首个 HTTP 200 的原因:拉取时间反映镜像路径的直接收益,首个 HTTP 200 则反映最终到达服务边界的效果。
07 / 结论
这次基准测试表明,在不改动应用镜像引用、也不重建镜像的前提下,镜像拉取时间得到了明显下降。
在这次 EKS 测试中,OpenSearch 的拉取时间从 20.371s 降到 2.998s,Spark 的首个 HTTP 200 从 20.191s 降到 13.304s。
这一结果并不局限于 Java。Hermes 工作在镜像文件系统层,因此同样的机制适用于任何 OCI 镜像。这些基于 Java 的工作负载只是一组具有真实 HTTP 就绪行为的具体测试样本。
在运维层面,整个流程依然简单:平台准备好懒加载制品,而 Pod 保持原有的镜像和规格不变。下一步的优化空间在容器启动之后的路径——让应用关键文件的表现更接近本地 overlay 文件系统。
关注项目:https://github.com/cloudpilot-ai/hermes
推荐阅读
全球抢 GPU,Kubernetes 却闲置?看 DRA 如何让算力按需飞
别了,EC2 Auto Scaling!AWS 2025 变革信号背后的行业真相
公司 GPU 还在 “摸鱼” 吗?这项Kubernetes 技术或许能帮你节省百万算力成本
公司介绍
CloudPilot AI 是一家总部位于旧金山硅谷的科技公司。致力于为全世界最严苛的团队提供弹性扩缩容解决方案。无需任何手动调优即可消除云浪费、提升应用性能并降低运维风险。已为数百家全球顶尖科技公司提供服务,累计为客户节省超过5亿美金,平均节省67%。
免费试用,2步5分钟,降低50%云成本:
cloudpilot.ai

