一、先说说这两个家伙到底在争什么

最近跟几个朋友聊天,发现大家一提到部署应用就开始纠结:到底是上Kubernetes(后面我简称K8s)还是直接用Serverless?其实这俩不是同一层面的东西,但放在一起比较也挺正常,因为对于很多业务来说,它们都是“把代码跑起来”的选项。你可以把K8s想象成自己租了一块地,想怎么盖房子、种什么菜、用多少水,全由你说了算。而Serverless更像是去菜市场点菜,你说要一盘鱼香肉丝,人家做完了端给你,你根本不用关心厨房在哪儿、锅有多大、厨师有几个。

Pod是K8s里最小的调度单元,你可以把它理解成一个“鸡蛋盒”,里面可能装着一个或多个容器。很多人习惯了用K8s管理一切,但有些场景其实用函数(Function)更省心。这篇文章是想帮你看清楚,什么情况下该让Pod退居二线,什么情况下把业务交给函数处理反而更香。

二、先搞清楚两者的核心差异

2.1 Pod(Kubernetes)是什么感觉

用K8s跑一个应用,你得先写一堆YAML文件。比如部署一个简单的Web服务,至少要有Deployment、Service,可能还有ConfigMap、Ingress。Deployment负责管理Pod副本,Service负责流量接入。然后你还得关注节点资源、自动扩缩容策略、网络策略、存储卷等等。K8s给了你极大的自由度,但这份自由的背后是你得自己承担运维成本。

举个例子,你有一个Node.js的HTTP服务,部署到K8s里大概长这样:


# 这是一个Deployment配置,负责管理Pod副本数
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service          # deployment名称
spec:
  replicas: 3                 # 先跑3个副本,保证高可用
  selector:
    matchLabels:
      app: user-service       # 标签选择器,用来关联Pod
  template:
    metadata:
      labels:
        app: user-service     # 给Pod打上标签
    spec:
      containers:
        - name: user-api      # 容器名称
          image: node:18-alpine  # 基础镜像
          ports:
            - containerPort: 3000  # 容器暴露的端口
          env:
            - name: DB_URL        # 数据库连接串,实际会用ConfigMap或Secret管理
              value: "mysql://user:pass@localhost:3306/users"
          resources:
            requests:            # 容器运行时最少需要多少资源
              cpu: 250m
              memory: 512Mi
            limits:              # 容器最多能用多少资源,防止“吃撑”
              cpu: "1"
              memory: 1Gi

然后你还需要一个Service来暴露访问入口:


# Service负责把流量负载均衡到各个Pod上
apiVersion: v1
kind: Service
metadata:
  name: user-service-svc
spec:
  selector:
    app: user-service       # 选择带这个标签的Pod
  ports:
    - port: 80              # Service对外端口
      targetPort: 3000      # 转发到Pod的3000端口
  type: ClusterIP           # 集群内部访问方式

这只是最最简单的例子。如果你要上线,还得配Ingress、HPA(自动扩缩容)、RBAC权限、PV/PVC存储。这一套下来,没个两天搞不定,而且你得时刻盯着集群的运行状态。但好处很明显:所有组件都在你眼皮底下,出问题你能直接进去看日志、调试、修改配置,完全可控。

2.2 函数(Function)是什么感觉

Serverless函数就简单粗暴了。你只需要关心你的业务代码,比如同样实现一个用户查询接口,如果用腾讯云函数或者AWS Lambda,你只需要写一个函数处理事件:


// 技术栈:Node.js 18 + Express风格的事件处理
// 这是一个云函数入口,接收API网关传来的HTTP事件
exports.handler = async (event, context) => {
  // 从event里解析出路径参数和请求体
  const userId = event.pathParameters && event.pathParameters.id;
  
  // 模拟从数据库查询用户信息
  const user = {
    id: userId,
    name: "张三",
    age: 30,
    // 这里本来要查DB,为了演示直接返回一个对象
    email: "zhangsan@example.com"
  };

  // 返回一个标准的HTTP响应对象
  return {
    statusCode: 200,
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(user)
  };
};

你看,不需要写Deployment,不需要写Service,不需要关心容器资源。你只要把代码传上去,平台帮你把一切都搞定。甚至平台会自动扩缩容:没人访问的时候,你的函数实例数为0,不花一分钱;来了1000个请求,平台瞬间拉起来1000个实例,每个请求独立执行。这就是云函数的魅力。

三、什么场景下你该抛弃Pod,大胆用函数

3.1 短时任务与突发流量

如果你的业务是典型的“偶尔忙一下”型,比如一个秒杀活动、一个报表生成任务、一个视频转码任务,K8s特别容易让你头疼。为什么?因为你得提前准备好足够的节点资源。比如你预计活动持续10分钟,峰值QPS达到5000,按照K8s的思路,你得在活动开始前把Pod副本数扩容到100个,并且确保集群有足够的Node节点承载这些Pod。活动结束后,你还得手动缩容,否则资源一直空着,白白花钱。

用函数就轻松得多。云函数平台是自动伸缩的,请求来了就启动实例,请求处理完就销毁。你根本不用关心底层有多少台机器。比如下面的场景:一个图片压缩功能,用户上传一张图片,触发一个函数去压缩,然后传回结果。这个函数可能每次只运行几百毫秒,但你不需要为了这几百毫秒专门起一个常驻Pod。


// 技术栈:Node.js 18 + 阿里云函数计算
// 使用图片处理库sharp进行压缩
const sharp = require("sharp");

exports.handler = async (event, context) => {
  // 从event里拿到上传的图片Buffer
  const imgBuffer = Buffer.from(event.body, "base64");
  
  try {
    // 用sharp对图片进行缩放,宽度限制为800像素,保持比例
    const resizedBuffer = await sharp(imgBuffer)
      .resize({ width: 800 })
      .jpeg({ quality: 80 })   // 压缩为JPEG,质量80
      .toBuffer();
    
    // 返回压缩后的图片数据
    return {
      statusCode: 200,
      headers: { "Content-Type": "image/jpeg" },
      body: resizedBuffer.toString("base64")
    };
  } catch (err) {
    console.error("压缩失败", err);
    return {
      statusCode: 500,
      body: "图片处理出错"
    };
  }
};

如果把这个功能放到K8s里,你得写一个常驻服务,还得考虑并发处理,为了偶尔的调用量去维护一个服务,实在不划算。

3.2 事件驱动型业务

你有没有听说过“事件驱动架构”?其实就是说系统里各个组件之间通过事件来触发动作。比如用户下单后,发送一条消息到消息队列,然后有消费者去处理这个消息。这在传统的K8s里怎么做?你得部署一个消息消费者服务,它不停地从队列里拉消息。这个服务哪怕没有消息,也得一直运行着,一直在拉取。

用函数就非常舒服。你可以让消息队列直接触发函数。当队列里来了一条消息,云平台自动执行一次函数,处理完就结束。没有消息的时候,函数不运行,也不花钱。这就好比你去食堂吃饭,平时食堂没菜,你也就不去;一旦有菜了,厨师马上给你炒一份,炒完就休息。而在K8s里,相当于你雇了一个厨师,让他从早到晚坐在厨房里,没菜的时候他也坐着,你还得给他发工资。

来看一个典型的订单超时关单场景:用户下单后15分钟未支付,需要自动关闭订单。这个逻辑在K8s里通常要写一个定时任务,或者用延时消息。用函数加定时触发器,几行代码就搞定:


// 技术栈:Node.js 18 + 腾讯云函数定时触发器
// 这个函数每5分钟执行一次,扫描未支付的过期订单
exports.handler = async (event, context) => {
  // 假设这是从数据库查询出来的过期订单列表
  const expiredOrders = [
    { orderId: "20250101001", userId: 123, createTime: "2025-01-01 10:00:00" },
    { orderId: "20250101002", userId: 456, createTime: "2025-01-01 10:05:00" }
  ];
  
  const closeOrder = async (order) => {
    // 模拟调用订单服务关闭订单
    console.log(`正在关闭订单 ${order.orderId}`);
    // 这里本来要更新数据库状态,省略
    return true;
  };

  for (const order of expiredOrders) {
    await closeOrder(order);   // 逐个关闭超时订单
  }

  return {
    statusCode: 200,
    body: `本次处理了 ${expiredOrders.length} 个过期订单`
  };
};

如果用K8s,你得部署一个CronJob,再配一个常驻Pod来跑关单逻辑,平时Pod闲着也是闲着,纯属浪费。

3.3 集成外部API的轻量胶水层

很多时候我们需要把不同的SaaS服务串起来。比如接收CRM的Webhook,解析数据,写入数据仓库。这种场景业务逻辑不重,但涉及很多第三方调用。用函数做胶水层非常方便,因为你不关心Webhook接收服务器的配置,也不关心数据写入的并发瓶颈。函数天然就是一个HTTP端点,别人往这个URL发POST请求,你的函数就被唤醒。

比如下面这个例子:当用户在小程序里提交一个反馈,我们需要把反馈内容转发到企业微信群里。这个功能用K8s部署一个服务就有点重了,但用函数就很顺手:


// 技术栈:Node.js 18 + AWS Lambda
// 用axios库发送HTTP请求到企业微信机器人
const axios = require("axios");

exports.handler = async (event) => {
  // 解析小程序传来的反馈内容
  const body = JSON.parse(event.body);
  const { nickname, content } = body;

  // 企业微信机器人的Webhook地址(敏感信息一般放环境变量里)
  const webhookUrl = process.env.WECHAT_WEBHOOK_URL;

  // 构造消息内容
  const message = {
    msgtype: "text",
    text: {
      content: `用户${nickname}反馈:${content}`
    }
  };

  try {
    // 发送到企业微信
    const resp = await axios.post(webhookUrl, message);
    console.log("企业微信响应状态码", resp.status);
    return {
      statusCode: 200,
      body: "发送成功"
    };
  } catch (err) {
    console.error("发送失败", err);
    // 这里可以改成重试或者转人工
    return {
      statusCode: 500,
      body: "发送失败"
    };
  }
};

这种胶水层代码没有状态,也不关心请求从哪来,函数模型简直是量身定做。

3.4 开发测试以及快速原型验证

作为开发者,你一定有过这种经历:刚写完一个功能,想马上让同事看效果。如果在K8s环境里,你得先构建镜像,推到镜像仓库,然后更新Deployment,等滚动升级完成。这一套流程最少也要几分钟。如果你用Serverless,本地写好代码,直接推送,几十秒后就能通过公网URL访问到。而且你可以随时改,随时重新部署,完全不用污染现有的K8s集群。

比如你临时要做一个API给前端联调,返回一份假数据。用函数写个接口,几分钟搞定:


// 技术栈:Node.js 18 + Vercel Serverless Function
// 这是Vercel风格的函数写法,导出default函数接收请求
export default async function handler(req, res) {
  // 模拟一个用户列表返回
  const users = [
    { id: 1, name: "李四", role: "admin" },
    { id: 2, name: "王五", role: "editor" },
    { id: 3, name: "赵六", role: "viewer" }
  ];

  // 返回JSON响应
  res.status(200).json({
    code: 0,
    data: users,
    message: "mock数据返回成功"
  });
}

相比之下,在K8s里搞这个,你得先写Dockerfile,再写Deployment YAML,还要配置Ingress域名,折腾半天,不值当。

四、但有些场景,Pod还是那个“值得托付的人”

4.1 长时间运行的业务

如果你的服务需要7x24小时不间断运行,比如一个在线游戏服务器,或者一个实时消息推送网关,这种场景用函数就不合适。因为函数有超时时间限制,比如AWS Lambda最长跑15分钟,腾讯云函数最长跑24小时(某些版本)。但K8s里的Pod可以永远跑下去,直到你主动杀掉它。而且Pod里的进程可以一直保持连接,比如WebSocket长连接,你的服务端需要和客户端保持TCP连接,这种情况下函数很难做到,因为函数每次执行都是无状态的,新启动的实例在请求结束就会被回收。

4.2 对基础设施的高度定制需求

有些业务需要特殊的内核参数、特定的Linux用户、特殊的网络配置,甚至需要挂载宿主机上的物理设备。这些需求函数平台往往不支持或者限制很多。在K8s里,你可以随便指定特权容器,可以挂载hostPath,可以设置nodeSelector让Pod跑到特定的机器上。比如你的服务需要用到GPU做模型推理,虽然有些云函数也支持GPU,但远不如K8s里调度GPU灵活。你可以限制GPU型号、显存大小,还可以在一个Pod里同时跑多个GPU任务。

4.3 已经有完整的微服务体系,且重度绑定K8s

如果你们的团队已经围绕K8s建立了完整的DevOps流程,比如有丰富的Helm Chart,有自建的监控告警系统,有日志采集管道,那么你把所有服务都换成函数反而会增加复杂度。因为函数是平台绑定的,不同云厂商的函数接口不一样,部署方式不一样,迁移成本很高。而K8s是开源的,几乎各个云厂商都能提供一样的K8s环境,迁移到哪家都一样。你可以在K8s里做更细粒度的A/B测试、流量染色、故障注入,这些都是生产级应用非常需要的能力。

比如你有下面一个服务在K8s里运行,它需要从Kafka持续消费消息,并且把处理结果写入Elasticsearch。这种流式处理任务往往需要长时间运行,而且需要支持分区分配、自动提交offset等机制。用K8s部署一个消费者组里的Pod是最自然的选择:


# 这是一个消费者服务的Deployment,保持运行状态
apiVersion: apps/v1
kind: Deployment
metadata:
  name: kafka-consumer
spec:
  replicas: 3  # 三个消费者组成一个组
  selector:
    matchLabels:
      app: kafka-consumer
  template:
    metadata:
      labels:
        app: kafka-consumer
    spec:
      containers:
        - name: consumer
          image: my-registry/consumer:v1.2
          env:
            - name: KAFKA_BROKERS
              value: "kafka:9092"
            - name: KAFKA_GROUP_ID
              value: "order-group"
            - name: ES_URL
              value: "elasticsearch:9200"
          # 假设这个容器内部有一个常驻进程,不断拉取消息并写入ES
          command: ["node", "consumer.js"]

像这种常驻消费者,用函数很难模拟。你可以用消息触发函数,但函数每次执行只能处理一条或者一批消息,无法维护本地状态(比如聚合多个消息再写入ES),处理逻辑会比较别扭。

4.4 需要高密度的架构组件

K8s里不仅仅可以跑你业务的应用,你还可以把Redis、MySQL、RabbitMQ、Nginx Ingress这些组件都部署到集群里,用Operator进行管理。这种自托管的方式在函数世界里是完全不存在的。函数只适合纯计算,不适合存储数据,更不适合承担基础设施角色。

五、各平台上的具体选择指南

5.1 阿里云:ACK和函数计算FC

阿里云的容器服务Kubernetes版(ACK)和函数计算(FC)是很多国内企业的首选。如果你要跑Spring Cloud全家桶,或者需要和阿里云VPC内部的其他服务进行复杂网络交互,那ACK更合适。如果你的业务是数据处理、API后端、定时任务,而且你想省掉运维成本,FC就很好。FC还支持自定义容器镜像,你可以把已有的镜像直接部署成函数,体验更平滑。

5.2 AWS:EKS和Lambda

AWS EKS是托管K8s服务,Lambda是业界最成熟的Serverless平台。Lambda的亮点在于事件源极其丰富,S3上传事件、DynamoDB流、SQS消息都能直接触发函数。但注意Lambda有冷启动延迟,大约几百毫秒到几秒不等,如果你对延迟非常敏感,比如交易系统,那EKS里常驻Pod更好。你可以用EKS的Karpenter自动扩缩容来减少闲置成本,但仍然需要自己管理节点组。

5.3 腾讯云:TKE和SCF

腾讯云TKE和SCF都比较容易上手。SCF可以和腾讯云的API网关、消息队列、定时器无缝结合。如果你主要业务都在腾讯云生态内,而且是小团队或者个人开发者,SCF能让你以极低成本上线业务。但如果你的业务复杂度高,需要精细控制网络策略、安全组、服务网格,那TKE才是正路。

5.4 开源方案:Knative和OpenFaaS

你可能听说过Knative,它是Google开源的一个基于K8s的Serverless框架。它本质上还是用K8s来管理Pod,但你可以把它看作是“K8s上的函数”。Knative可以让你用类似函数的方式部署服务,但底层依然是K8s的Pod在跑。这样既保留了Pod的灵活性,又享受了Serverless的按需伸缩。OpenFaaS也是一个不错的选择,它把函数打包到容器里,运行在K8s或Docker Swarm上。这种方式适合不想被云厂商锁定,但又想要Serverless体验的团队。不过说实话,自建Knative的运维复杂度并不比纯K8s低多少,你得自己管理Istio(服务网格)等组件,对新手来说学习曲线比较陡峭。

六、选择之前你必须想清楚的几个坑

6.1 冷启动问题

函数不是“瞬间”执行的。当一个请求到来时,如果函数已经有一段时间没有调用,平台需要重新创建实例、加载代码、初始化运行环境,这个过程可能需要几百毫秒甚至几秒。对于用户无感的后台任务,这个延迟可以接受。但如果你在做面向用户的实时请求,比如查询商品详情、提交订单,冷启动会导致接口变慢,用户体验下降。减轻冷启动的方法包括:预留实例(提前把函数热起来)、使用较新的运行时(Node.js比Java冷启动快)、精简依赖包体积。但无论怎么优化,冷启动的物理存在还是无法完全消除。在K8s里,Pod是常驻的,没有冷启动问题。

6.2 供应商锁定

用函数开发,你的代码要遵循特定平台的接口格式。比如AWS Lambda的exports.handler入口跟腾讯云SCF的不一样,虽然大体相似,但事件对象的结构、环境变量方式、日志系统都有差异。如果你以后想换个云厂商,代码得改不少。而K8s是标准化的,几乎不存在锁定问题。如果你的公司有多云容灾需求,建议优先K8s;如果是单云没关系,可以大胆用函数。

6.3 调试与运维体验

K8s里的Pod出现问题,你可以kubectl exec进入容器,用bash或者ps查看进程、看网络、改文件,调试体验非常接近传统服务器。函数呢?你很难登录到运行时环境。你只能通过日志接口查看输出,或者用平台提供的远程调试工具(不一定支持)。对于复杂问题,函数排错效率低于Pod。这一条对于“喜欢打开黑盒一探究竟”的开发者来说特别重要。

6.4 成本模型差异

很多人的直觉是Serverless便宜,其实不一定。函数按请求次数和运行时长计费。如果你有一个服务每秒钟触发100次,每次运行200ms,那么你可能需要预置至少20个并发实例,而每个实例的单价可能比同规格的Pod要贵。而且函数平台存在并发配额限制,如果超过配额,请求会被拒绝。Pod则使用包月或按量计费,如果你能把资源利用率压得很高(比如把多个服务塞进同一个Pod),总成本反而更低。建议你细算一下自己的QPS、平均CPU时长、内存大小,再决定。

下面给一个粗略的成本对比示例(假设都是1G内存的Node.js服务,月请求量100万次,平均每次执行200ms):

方案 费用估算逻辑 大概月费用(人民币)
K8s Pod 1核1G的Pod常驻,每月约200元 200元
云函数 100万次请求(约0.0136元/次)+ CPU/内存时长,约0.5元/万次调用,合计142元 142元

但如果你每秒持续调用,函数会按并发实例数量累计运行时长,成本可能反超Pod。所以别只看宣传口号,得拿真实数据去算。

6.5 网络与安全

在K8s里,你可以用NetworkPolicy精细控制Pod之间谁能访问谁,也可以把Pod加入特定的安全组。函数则通常运行在平台的基础网络里,你虽然可以配置VPC,但无法直接看到底层弹性网卡。有些函数平台对出网IP的管理也不够灵活,如果你需要固定公网IP访问外部服务(比如对接第三方API白名单),函数平台往往不能直接提供,你需要额外绑定一个NAT网关或者代理服务。这一点在K8s里很容易做到,你只需要规划好EIP和Service的externalIP即可。

七、混搭才是聪明玩法

其实你不需要把自己逼进非此即彼的死胡同。一个成熟的系统完全可以同时使用Pod和函数。比如核心业务(用户鉴权、支付、订单状态机)放在K8s里,因为它们需要低延迟、复杂状态、可控扩容。而一些边角功能,比如发送短信通知、生成图片缩略图、清洗日志、定时报表,用函数来做,省心又省钱。

如何实现混搭?最简单的方式是让K8s里的Pod调用函数的HTTP端点,或者反过来让函数调用K8s里的Service。比如你的用户Profile服务在K8s里,当用户更新头像时,K8s服务往消息队列发一条事件,事件触发一个函数去做图片压缩,然后结果回调K8s服务。这样分工明确,互不干扰。

下面是一个直接调用函数API的Node.js示例(在K8s Pod内):


// 技术栈:Node.js 18 + axios
// 这段代码运行在K8s里的一个Pod中,用于触发一个云函数
const axios = require("axios");

async function triggerAvatarCompress(userId, imageUrl) {
  // 云函数的公网地址或者VPC内网地址
  const functionUrl = process.env.FUNC_COMPRESS_URL;

  // 构造事件参数
  const payload = {
    userId: userId,
    imageUrl: imageUrl
  };

  try {
    // 调用云函数并期望函数异步处理(假设函数是异步调用)
    const resp = await axios.post(functionUrl, payload, {
      timeout: 5000  // 5秒超时,防止长时间等待
    });
    console.log(`函数触发成功, 状态码: ${resp.status}`);
  } catch (err) {
    console.error("函数触发失败,走降级方案", err);
    // 降级方案:直接用K8s内部的服务进行同步压缩
    // ... 省略
  }
}

// 调用示例
triggerAvatarCompress(12345, "https://cdn.example.com/avatar/12345.jpg");

这种混搭架构有很多好处:核心链路稳定可控,边缘逻辑灵活轻量。你可以逐步把那些“一次性”的业务迁移到函数上,而不用大动干戈重写整个系统。

八、给不同背景开发者的最终建议

如果你是刚入门的小白,没接触过K8s,建议先学函数。因为它能让你快速实现业务逻辑,不需要被底层基础设施分心。当你做出一个像样的项目之后,再回头去啃K8s也不迟。如果你是一个运维工程师,或者负责一个长期运行、对稳定性要求极高的后台系统,那K8s还是你的主战场。你可以把函数看作一种补充工具,在特定场景下帮你减负。

如果你是架构师,请综合考虑团队的技术能力、现有资产、业务形态。如果团队里每个人都是K8s老手,那你引入函数反而增加学习成本。如果团队本身就喜欢敏捷开发,不想管服务器,那函数能极大解放生产力。另外,别忘了云厂商的生态:如果你的所有业务都逃不开某家云厂商的数据库、消息队列,那直接用它的函数是最省事的。

九、总结

用一句话来说:如果你需要的是“操作一台永远在跑着的机器”,选Pod;如果你需要的是“响应一个事件然后自动消失”,选函数。K8s是给那些喜欢掌控全局、追求可迁移性的人准备的,Serverless是给那些想把精力集中在业务上、追求极致效率的人准备的。两者没有绝对的好坏,只有合不合适。希望你能结合自己业务的流量特点、状态复杂度、运维能力和成本预算,做出理性的判断。千万别因为“大家都在用”而盲目选择,也别因为“新潮”而放弃沉稳。技术没有银弹,适配才是王道。