一、从一顿饭说起

大家平时做饭,最怕的就是火候没控制好。火太小,菜半天熟不了;火太大,锅底糊了。运行 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 给函数设置合理的“饭量”

每个函数部署时,都可以指定两个参数:requestslimitsrequests 是系统保证给它的最低饭量,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 功能设计

我们把前面学的几个小工具合并成一个完整的脚本。这个脚本做三件事:

  1. 扫描命名空间下所有函数 Pod,收集它们的 CPU 和内存使用量。
  2. 计算平均使用量,如果发现某个函数的使用量超过我们预设的阈值,就自动调大它的 limits。
  3. 把结果打印出来,让你一目了然。

技术栈依然是 Python。我们会用到 kubernetesmetrics 和时间计算。

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 指标拉下来,算平均值,超阈值就自动改资源限制。整个思路简单直接,能帮助你把每台服务器的资源用在刀刃上。记住,优化的目标不是让函数吃得越少越好,而是让它在保证响应速度的前提下,不浪费资源。就像做饭一样,提前调好火候,才能少花时间多出菜。希望你能从这篇文章里找到一点启发,真正把自己的函数调到“不饿也不撑”的状态。

现在,打开你的终端,动手跑一下脚本吧。