一、从一顿饭说起
大家平时做饭,最怕的就是火候没控制好。火太小,菜半天熟不了;火太大,锅底糊了。运行 OpenFaaS 函数其实也是一回事。你写了一个函数,它跑在一堆容器里,容器要吃东西——这个东西就是 CPU 和内存。吃少了,函数反应慢;吃多了,机器受不了,甚至要花钱加节点。今天咱们不聊高深的理论,就聊聊怎么看住这些“饭量”,以及怎么让它吃得刚刚好。
二、先看看你的函数吃了多少资源
2.1 用什么工具看
OpenFaaS 本身跑在 Kubernetes 上,所以最简单的方法是直接问 Kubernetes:我的函数 Pod 现在用了几核 CPU?占了多少内存?Kubernetes 提供了一个叫 Metrics API 的东西,你只要敲一条命令,就能看到每个 Pod 的实时消耗。不过,直接敲命令看一次两次还行,想持续记录下来,还得写点小脚本。
在动手之前,你得先装好 Python 环境,因为接下来我们所有的示例都用 Python。Python 里有一个非常好用的库叫 kubernetes,它能帮你连上集群,把资源数据拉回来。如果你还没有这个库,先安装一下:
# 这里用 pip 安装 kubernetes 这个 Python 包
# 这个包能让我们在 Python 里调用 Kubernetes 的接口
pip install kubernetes
2.2 一个实际的监控示例
假设你已经在本地配好了 Kubernetes 的访问证书。下面这段代码会列出命名空间 openfaas-fn 下所有函数的 Pod,并打印出它们当前的 CPU 和内存使用量。请注意,这里的技术栈是 Python,后续所有代码示例都是 Python。
# 文件名:check_usage.py
# 这个脚本用来查看 OpenFaaS 函数 Pod 的资源消耗
# 导入必要的库
from kubernetes import client, config
from kubernetes.metrics import MetricsAPI
def main():
# 第一步:从默认位置加载 Kubernetes 配置
# 如果你在本地用 kubectl 连接集群,这里会自动读取 ~/.kube/config
config.load_kube_config()
# 第二步:创建两个客户端
# 一个用于获取 Pod 元数据,一个用于获取实时指标
v1 = client.CoreV1Api()
metrics_api = MetricsAPI()
# 指定我们要查看的命名空间,OpenFaaS 的函数都跑在 openfaas-fn 里
namespace = "openfaas-fn"
# 第三步:获取该命名空间下的所有 Pod 列表
pod_list = v1.list_namespaced_pod(namespace)
# 第四步:遍历每个 Pod,打印它的名字和资源消耗
for pod in pod_list.items:
pod_name = pod.metadata.name
# 获取这个 Pod 的实时指标
try:
m = metrics_api.get_pod_metrics(namespace, pod_name)
# 指标里可能有多个容器,我们只拿第一个(一般来说一个 Pod 一个容器)
container = m.containers[0]
cpu_cores = container.usage["cpu"]
memory_bytes = container.usage["memory"]
print(f"Pod: {pod_name}")
print(f" CPU 使用量: {cpu_cores}")
print(f" 内存使用量: {memory_bytes}")
print("---")
except Exception as e:
# 如果 Pod 还在启动,可能暂时拿不到指标,就跳过
print(f"Pod {pod_name} 没有指标: {e}")
if __name__ == "__main__":
main()
跑起来之后,你会看到类似这样的输出:
Pod: echo-b6f7d8c9d-4k2pz
CPU 使用量: 2m
内存使用量: 3145728Ki
---
Pod: face-detect-5f6b7c8d9-7qwsd
CPU 使用量: 15m
内存使用量: 12884902Ki
---
这里 CPU 的 2m 表示 2 毫核,也就是 0.002 个 CPU 核心。内存的 3145728Ki 是 3GB 左右(Ki 是 1024 字节)。看到这些数字,你就能大概清楚每个函数是“小鸟胃”还是“大胃王”了。
三、资源消耗的常见瓶颈
3.1 CPU 饥饿
一个函数如果被分配到的 CPU 太少,而代码里又有个死循环,或者做了太多的计算,那么它就会像一个人被捆住手脚跑步——使不上劲。表现就是函数响应特别慢,甚至超时。你可以在 Pod 上看到 CPU Throttling(CPU 节流)的情况,意思是 CPU 配额用完了,系统强制让它歇一会儿。
3.2 内存泄漏
内存泄漏是更隐蔽的问题。函数每次被调用,都会创建一些临时对象,如果这些对象没被正确释放,就像家里堆垃圾,越堆越多。最终内存用完,系统会直接杀掉这个进程,函数就会出现莫名其妙的崩溃。监控内存的曲线,如果呈现一个台阶式上升,那就要小心了。
3.3 冷启动与并发
OpenFaaS 的函数在空闲时会被缩容到零,来省资源。当请求来临时,它要从零启动一个容器,这个启动过程也需要 CPU 和内存。如果同时来很多请求,函数会同时启动多个副本,瞬间的资源消耗会冲得很高。这个时候,如果集群没有足够的空闲资源,新的副本就会一直排队,请求就会超时。
四、怎么优化
4.1 给函数设置合理的“饭量”
每个函数部署时,都可以指定两个参数:requests 和 limits。requests 是系统保证给它的最低饭量,limits 是它最多能吃多少。设置太小,函数饿肚子;设置太大,浪费资源,还可能因为占用过多导致其他函数没得吃。
怎么判断合理不合理?我们可以用 Python 脚本持续采集一段时间的用量,然后取一个中位数,再加上一点余量,作为新的 requests 和 limits。下面这个示例演示了如何修改某个函数的资源配置。
# 文件名:update_limit.py
# 这个脚本用来给 OpenFaaS 函数动态调整 CPU 和内存的配额
from kubernetes import client, config
def update_function_limit(namespace, deployment_name, cpu_limit, memory_limit):
# 加载 Kubernetes 配置
config.load_kube_config()
# 创建 AppsV1 客户端,用来操作 Deployment
apps_v1 = client.AppsV1Api()
# 读取目标 Deployment
deploy = apps_v1.read_namespaced_deployment(deployment_name, namespace)
# 我们只修改第一个容器(通常一个函数只有一个容器)
container = deploy.spec.template.spec.containers[0]
# 构造新的 resource 配置
# 这里为了简化,只改 limits,requests 保持不变
# 如果你也想改 requests,可以一并修改
container.resources.limits = {
"cpu": cpu_limit, # 比如 "100m" 代表 0.1 核
"memory": memory_limit # 比如 "256Mi" 代表 256 兆
}
# 应用修改
apps_v1.replace_namespaced_deployment(deployment_name, namespace, deploy)
print(f"已更新 {deployment_name} 的限制为 CPU={cpu_limit}, 内存={memory_limit}")
# 调用示例
if __name__ == "__main__":
# 将 echo 函数的 CPU 限制改为 100 毫核,内存限制改为 256Mi
update_function_limit(namespace="openfaas-fn", deployment_name="echo", cpu_limit="100m", memory_limit="256Mi")
注意,这里我们用的是 Python 的客户端去修改 Deployment,其实 OpenFaaS 也支持用 YAML 文件部署。但为了保持示例统一技术栈,我们就不在代码块里出现 YAML 了。你只要知道原理就行:修改的是同一个东西。
4.2 让函数“少吃多餐”
减少资源消耗的另一个思路是优化代码本身。比如你的函数要读一个大文件,能不能只读需要的部分?能不能用更高效的算法?Python 虽然写起来方便,但某些计算密集型的任务性能并不好。你可以把函数里的热点部分用 C 扩展或者 NumPy 来加速,这样 CPU 的耗时就大大缩短了。
下面是一个小例子,比较一下用普通循环和用列表推导式求平方和的差距。这个差距虽然不大,但能说明代码风格对 CPU 消耗的影响。
# 文件名:cpu_example.py
# 这个脚本展示了两种写法对 CPU 消耗的影响
import time
def slow_sum(n):
# 慢做法:for 循环逐个累加
total = 0
for i in range(n):
total += i * i
return total
def fast_sum(n):
# 快做法:用生成器表达式配合 sum 函数
# 底层循环用 C 实现,速度更快
return sum(i * i for i in range(n))
# 测试一个比较大的数
n = 5000000
start = time.time()
slow_sum(n)
end = time.time()
print(f"慢做法耗时: {end - start:.3f} 秒")
start = time.time()
fast_sum(n)
end = time.time()
print(f"快做法耗时: {end - start:.3f} 秒")
这段代码会告诉你,同样的结果,不同的写法能差出一倍的时间。时间少了,占用的 CPU 资源自然就少了。
4.3 控制并发和缩容策略
OpenFaaS 默认会在函数空闲时缩容到零,但这会带来冷启动的代价。如果你的函数对延迟很敏感,你可以设置 minimum replicas(最小副本数),让一两个副本常驻。这样请求来了马上就能处理,不用等容器启动。
调整副本数同样可以用 Python 完成。下面这段代码会把指定函数的最小副本数调整为 2。
# 文件名:scale_function.py
# 这个脚本用来设置 OpenFaaS 函数的最小副本数
from kubernetes import client, config
def set_min_replicas(namespace, deployment_name, min_replicas):
# 加载配置并获取 AppsV1 客户端
config.load_kube_config()
apps_v1 = client.AppsV1Api()
# 读取 Deployment
deploy = apps_v1.read_namespaced_deployment(deployment_name, namespace)
# 设置副本数
deploy.spec.replicas = min_replicas
# 应用修改
apps_v1.replace_namespaced_deployment(deployment_name, namespace, deploy)
print(f"{deployment_name} 的副本数已设置为 {min_replicas}")
# 设置 echo 函数至少跑 2 个副本
set_min_replicas(namespace="openfaas-fn", deployment_name="echo", min_replicas=2)
注意,OpenFaaS 的自动伸缩可能会覆盖这个值。如果你希望这个值不被自动伸缩改掉,可能需要去 OpenFaaS 的配置文件里调整 minReplicas,但原理是一样的。
五、实战:写一个监控优化小助手
5.1 功能设计
我们把前面学的几个小工具合并成一个完整的脚本。这个脚本做三件事:
- 扫描命名空间下所有函数 Pod,收集它们的 CPU 和内存使用量。
- 计算平均使用量,如果发现某个函数的使用量超过我们预设的阈值,就自动调大它的 limits。
- 把结果打印出来,让你一目了然。
技术栈依然是 Python。我们会用到 kubernetes、metrics 和时间计算。
5.2 完整代码
# 文件名:smart_optimizer.py
# 这个脚本会自动检查 OpenFaaS 函数资源使用情况,并在必要时调整配额
from kubernetes import client, config
from kubernetes.metrics import MetricsAPI
import time
# 我们设定的阈值:当 CPU 使用量超过 limits 的 80% 时,就增加限制
CPU_LIMIT_RATE = 0.8
MEMORY_LIMIT_RATE = 0.8
def get_all_deployments(namespace):
""" 获取命名空间下所有 Deployment 的名字和当前资源限制 """
config.load_kube_config()
apps_v1 = client.AppsV1Api()
v1 = client.CoreV1Api()
# 获取所有 Deployment
deps = apps_v1.list_namespaced_deployment(namespace)
result = []
for dep in deps.items:
# 获取第一个容器
container = dep.spec.template.spec.containers[0]
# 如果容器没有设置 limits,我们给一个默认值
if container.resources and container.resources.limits:
cpu_limit = container.resources.limits.get("cpu", "100m")
mem_limit = container.resources.limits.get("memory", "256Mi")
else:
cpu_limit = "100m"
mem_limit = "256Mi"
result.append({
"name": dep.metadata.name,
"cpu_limit": cpu_limit,
"mem_limit": mem_limit,
"namespace": namespace
})
return result
def get_current_usage(namespace, pod_name):
""" 获取某个 Pod 的实时使用量(单位:m 核 和 MiB) """
metrics_api = MetricsAPI()
try:
m = metrics_api.get_pod_metrics(namespace, pod_name)
container = m.containers[0]
cpu_usage = container.usage["cpu"] # 格式如 '2m'
mem_usage = container.usage["memory"] # 格式如 '3145728Ki'
# 把字符串转换成数字单位
# CPU 从 'm' 转换成核的千分之一
if cpu_usage.endswith("m"):
cpu_millicore = float(cpu_usage[:-1])
else:
# 没有后缀,就是多少核,转换成毫核
cpu_millicore = float(cpu_usage) * 1000
# 内存从 'Ki' 转换成 MiB
if mem_usage.endswith("Ki"):
mem_mib = float(mem_usage[:-2]) / 1024
elif mem_usage.endswith("Mi"):
mem_mib = float(mem_usage[:-2])
else:
mem_mib = float(mem_usage) / (1024 * 1024)
return cpu_millicore, mem_mib
except Exception:
# 如果没有指标,返回0,避免出错
return 0, 0
def adjust_limits(deployment_name, namespace, new_cpu_m, new_mem_mi):
""" 修改 Deployment 的资源限制 """
apps_v1 = client.AppsV1Api()
dep = apps_v1.read_namespaced_deployment(deployment_name, namespace)
container = dep.spec.template.spec.containers[0]
# 将新值格式化成 Kubernetes 格式
container.resources.limits["cpu"] = f"{new_cpu_m}m"
container.resources.limits["memory"] = f"{new_mem_mi}Mi"
apps_v1.replace_namespaced_deployment(deployment_name, namespace, dep)
print(f"调整 {deployment_name} 的 limits 为 CPU={new_cpu_m}m, 内存={new_mem_mi}Mi")
def main():
namespace = "openfaas-fn"
# 获取所有 Deployment 的设置
deployments = get_all_deployments(namespace)
print("开始扫描 OpenFaaS 函数资源使用情况...")
for dep in deployments:
name = dep["name"]
cpu_limit = dep["cpu_limit"]
mem_limit = dep["mem_limit"]
# 将限制字符串转换成数字(毫核和 MiB)
# 这里简化处理,假设限制都是类似 '100m' '256Mi'
cpu_limit_m = float(cpu_limit.rstrip("m"))
mem_limit_mi = float(mem_limit.rstrip("Mi"))
# 找到这个 Deployment 下的所有 Pod
config.load_kube_config()
v1 = client.CoreV1Api()
pods = v1.list_namespaced_pod(namespace, label_selector=f"faas_function={name}")
total_cpu = 0
total_mem = 0
pod_count = 0
for pod in pods.items:
cpu_usage, mem_usage = get_current_usage(namespace, pod.metadata.name)
total_cpu += cpu_usage
total_mem += mem_usage
pod_count += 1
if pod_count == 0:
print(f"函数 {name} 没有运行中的 Pod,跳过")
continue
avg_cpu = total_cpu / pod_count
avg_mem = total_mem / pod_count
# 检查是否超过阈值
if avg_cpu > cpu_limit_m * CPU_LIMIT_RATE or avg_mem > mem_limit_mi * MEMORY_LIMIT_RATE:
print(f"函数 {name} 吃得太多了!平均 CPU: {avg_cpu:.0f}m, 内存: {avg_mem:.0f}Mi")
# 用原来 1.5 倍的新限制
new_cpu = int(cpu_limit_m * 1.5)
new_mem = int(mem_limit_mi * 1.5)
adjust_limits(name, namespace, new_cpu, new_mem)
else:
print(f"函数 {name} 状态良好,平均 CPU: {avg_cpu:.0f}m, 内存: {avg_mem:.0f}Mi")
if __name__ == "__main__":
main()
5.3 运行效果
假设你的集群里只有一个叫 echo 的函数。运行 python smart_optimizer.py,你可能会看到:
开始扫描 OpenFaaS 函数资源使用情况...
函数 echo 状态良好,平均 CPU: 3m, 内存: 45Mi
如果它某段时间突然流量变大,第二次运行可能就会触发调整:
开始扫描 OpenFaaS 函数资源使用情况...
函数 echo 吃得太多了!平均 CPU: 120m, 内存: 200Mi
调整 echo 的 limits 为 CPU=150m, 内存=384Mi
这个脚本很简单,但已经覆盖了监控、判断、调整三个步骤。你可以把它放到计划任务里,每小时跑一次,就实现了最基本的自动优化。
六、应用场景与优缺点
应用场景:如果你的函数是给内部业务接口用的,比如图像识别、数据处理、报表生成,这类函数通常比较吃资源,而且流量波动大,用手动调参数很累。用这种自动化监控就特别合适。另外,如果你们公司针对资源使用有成本考核,也需要精确了解每个函数花了多少钱,这套监控系统能给出数据支撑。
优点:用 Python 写监控和优化逻辑非常轻量,不需要额外部署 Grafana 和 Prometheus 全家桶,对于小集群足够用。而且代码非常直白,大家都能看懂,改起来也方便。
缺点:它只能做“事后诸葛”,也就是资源已经用超了才去调整,不能预测未来的峰值。另外,脚本本身也需要消耗一点资源,虽然不大,但如果你有成千上万个函数,每个循环去调 API 可能会比较慢。还有,直接修改 Deployment 可能会导致 OpenFaaS 的自动伸缩策略重置,你需要小心测试。
七、注意事项
第一,别让脚本和函数抢资源。这个脚本是跑在集群外的,如果用 Python 频繁调用 Kubernetes API,会在本地产生大量请求。建议设置合理的间隔时间,比如每分钟跑一次就够了,别写成死循环。
第二,修改 limits 的时候要小心。如果把限制调得太大,节点可能放不下,导致 Pod 无法被调度。最好在脚本里加一个判断:如果新的限制超过节点剩余资源,就不调整,或者发送一个告警。
第三,内存的清理是个大学问。如果你的函数长期运行,内存使用率会缓慢上升,这不一定是泄漏,有可能是因为 Python 的 GC 没来得及回收。你可以在函数代码里手动调用 gc.collect(),但不推荐过度依赖。
第四,监控数据要留痕。现在这个脚本只是打印,最好能把数据写入数据库或者日志文件。这样将来出了什么问题,你能回过头来看历史曲线,而不是只有当下这一刻快照。
八、总结
OpenFaaS 函数的资源消耗监控与优化,核心就是“先看到数据,再调整配置”。我们用纯 Python 的方式,把 Kubernetes 指标拉下来,算平均值,超阈值就自动改资源限制。整个思路简单直接,能帮助你把每台服务器的资源用在刀刃上。记住,优化的目标不是让函数吃得越少越好,而是让它在保证响应速度的前提下,不浪费资源。就像做饭一样,提前调好火候,才能少花时间多出菜。希望你能从这篇文章里找到一点启发,真正把自己的函数调到“不饿也不撑”的状态。
现在,打开你的终端,动手跑一下脚本吧。
评论
围绕“OpenFaaS函数运行时资源消耗的监控与优化”参与讨论