Faas核心技术

71 阅读16分钟

Serverless

概念

狭义的 Serverless,就是开发人员的 “懒人部署工具”  —— 你只需要把写好的代码(比如一个处理订单的函数、一个图片压缩接口)直接上传到平台,不用管服务器怎么买、怎么配,也不用操心代码跑在哪个容器里、要不要扩容。平台会自动帮你运行代码、应对流量高峰(比如秒杀时自动加资源)、流量低时缩容,而且只在代码真正运行的时候收费,空闲时一分钱不花。

广义的 Serverless,则是一种 “按需用资源” 的架构思想 —— 它不只是部署工具,而是指导你怎么设计整个应用。就像微服务把大应用拆成小服务一样,Serverless 鼓励把应用拆成一个个独立的函数,每个函数只干一件事(比如 “生成订单”“发送短信”“解析日志”);同时把计算和存储彻底分开(函数只管计算,数据存在专门的存储服务里),再用 “事件” 驱动函数运行(比如用户下单这个事件,自动触发 “生成订单” 函数)。整个架构里,你看不到一台需要维护的服务器,只关心 “函数做什么” 和 “什么事件触发它”。

架构

Serverless 的本质是基于 FaaS 运行函数、依托 BaaS 调用能力,无需关心底层服务器。

  • Backend-as-a-Service(BaaS): “后端能力的‘即插即用’”

    • BaaS 是云端提供的 “现成后端功能包” —— 它把应用开发中通用的核心能力(比如存数据、用户登录、发消息)做成标准化的 API 服务,你不用自己写数据库逻辑、搭认证系统,直接调用这些云端 API 就能用。这些服务自带 “自动扩容” 和 “免运维” 属性:数据存满了自动加空间,用户登录请求多了自动扛住,你只用管 “怎么调用”,不用管 “服务怎么跑”。
    • 简单说:BaaS 是 “别人做好的后端轮子,你直接装到自己车上用”,核心是用云端 API 替代自研后端功能
  • Functions-as-a-Service(FaaS): “代码的‘按需运行’”

    • FaaS 是 “只跑代码、不管机器” 的事件驱动计算服务 —— 你把业务逻辑写成 “一个函数”(比如 “用户下单后发通知”“上传图片后压缩”),上传到平台;当特定事件发生时(比如用户下了单、有图片上传),平台会自动启动一个 “临时容器” 运行这个函数,跑完就销毁容器。整个过程你不用买服务器、不用配环境,函数要处理的请求多了,平台自动多开容器;没请求时,一个容器都不跑,而且只按函数实际运行的时间 / 次数收费

    • 简单说:FaaS 是 “把代码丢给平台,让它在事件发生时帮你跑,跑完就撤”,核心是用 “函数 + 事件” 替代传统的 “服务 + 服务器”

image.png


事件源(HTTP/消息队列/...)
↓ 产生事件
触发器
↓ 转发事件
FaaS控制器
↓ 调度函数
函数实例(容器)
↓ 执行handler(event,ctx)
BaaS服务(云存储/数据库/...)

优势

一、 成本优势

  1. 按需付费,精准控本采用按执行时间 + 调用次数的计费模式,仅在函数实际运行时产生费用,无请求流入时零计费,大幅削减业务低峰期的闲置资源成本。
  2. 实例零缩容,成本完全归零当函数长期无触发事件时,运行实例会自动缩减至零,彻底杜绝空跑资源的成本消耗,相比传统服务器 / 容器的 “闲置仍计费” 模式,实现极致的成本优化。

二、 弹性扩缩容能力

  1. 极致弹性,轻松应对潮汐流量支持从 0 到数千并发的极速扩展,可在毫秒级响应流量峰值(如秒杀、活动促销场景);流量回落时自动缩容,确保资源利用率始终处于最优状态。
  2. 免运维扩缩,无需资源规划扩缩容逻辑由平台完全托管,无需开发 / 运维人员手动配置扩缩容阈值、预留资源容量,彻底摆脱传统架构中 “资源预估不准” 导致的浪费或过载问题。

三、 事件驱动架构优势

  1. 原生事件集成,无缝对接上下游与云产品生态深度联动,可直接监听对象存储(TOS)、消息队列、数据库变更等多种事件源,无需额外开发适配逻辑,是构建事件驱动架构的原生首选方案。
  2. 简化开发链路,降低架构复杂度函数由事件直接触发执行,省去传统架构中 “事件监听 - 消息转发 - 服务部署” 的多层适配环节,大幅缩短业务开发链路,提升迭代效率。

劣势

。。。冷启动

产品

开源自部署 FaaS(适合私有化场景)

  1. OpenFaaS轻量开源 FaaS,支持容器镜像部署,可运行在 Kubernetes 上,兼容多语言,适合中小团队在私有集群中快速搭建 FaaS 能力。
  2. Knative由谷歌主导的开源 Serverless 框架,基于 Kubernetes,核心包含 “服务部署” 和 “事件触发” 能力,可看作 Kubernetes 原生的 FaaS 扩展,适合已有 K8s 集群的团队。

Faas

控制面

在 FaaS(函数即服务)架构中,控制面(Control Plane)  是 FaaS 平台的大脑中枢,负责管理、调度和协调函数的全生命周期,不直接处理函数的业务请求,只做 “决策和管控”

  1. Regional Server:单地区控制面入口,管理本地区元数据存储;对接 K8s 集群,负责任务发布及 Deployment 的创建、扩缩容。
  2. GlobalHub:全局控制面与各地区的流量转发入口;管理函数代码下载、函数实例初始化与状态上报,收集实例日志和监控数据。
  3. EdgeHub:单地区控制面流量接入层;与 GlobalHub 维持 WebSocket 连接,序列化 / 反序列化 HTTP 请求,转发全局与地区间的消息。
  4. FaaS Build:函数构建组件;负责代码压缩、打包、缓存预置,编译阶段安装用户依赖。

image.png

image.png

数据面

与控制面对应的是数据面(Data Plane) ,负责实际运行函数代码、处理用户请求。

image.png

存在的问题?

用户请求 → Gateway 组件 → Gateway/Dispatcher 发现:

  • ❌ 实例数为 0(流量长期为 0 被缩容);
  • ❌ 现有实例并发已满,无法承接新请求;→ 触发冷启动流程:Dispatcher 通知 K8s 集群 → K8s 创建新的函数容器实例 → 实例启动完成 → Gateway 转发请求到新实例 → 执行并返回。👉 时延 = “实例启动耗时 + 请求转发 + 函数执行耗时”,核心额外开销就是 “容器实例启动”。
优化手段核心动作优化目标典型耗时优化效果
镜像代码分离拆分镜像,缓存基础镜像,仅下载代码砍掉大镜像拉取的耗时300ms → 50ms
函数实例预热提前启动实例,放入预热池彻底消除实例启动耗时500ms → 0ms(无启动)
冷启动实例调度智能选节点,并行调度,预留资源减少调度 / 资源竞争耗时200ms → 50ms

触发器

FaaS 触发器就是能触发函数运行的各类事件源,包括 HTTP 请求、定时任务、消息队列消息、存储 / 数据库变更等。

HTTP

这里给出HTTP 触发器最核心的极简版实现,只保留「请求接收→参数封装→函数调用→响应返回」的核心链路,剔除所有非必要的细节(如可信 IP、复杂错误处理、多事件类型等)

package main

import (
    "context"
    "encoding/json"
    "net/http"
    "time"
)

// 1. 定义核心结构体:HTTP请求参数 + 函数响应
type HTTPRequest struct {
    Method  string            `json:"method"`
    Path    string            `json:"path"`
    Query   map[string]string `json:"query"`
    Body    []byte            `json:"body"`
    Headers map[string]string `json:"headers"`
}

type FunctionResponse struct {
    StatusCode int               `json:"status_code"`
    Headers    map[string]string `json:"headers"`
    Body       []byte            `json:"body"`
}

// 2. 定义函数处理器签名(核心:统一入口)
type FunctionHandler func(ctx context.Context, req *HTTPRequest) (*FunctionResponse, error)

// 3. 核心:HTTP触发器实现(仅保留核心链路)
func HTTPTrigger(handler FunctionHandler) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
       // 核心步骤1:封装HTTP请求为结构化参数
       req := &HTTPRequest{
          Method:  r.Method,
          Path:    r.URL.Path,
          Query:   make(map[string]string),
          Headers: make(map[string]string),
       }

       // 简化版:解析Query参数
       for k, v := range r.URL.Query() {
          if len(v) > 0 {
             req.Query[k] = v[0]
          }
       }

       // 简化版:读取Body
       body := make([]byte, r.ContentLength)
       if r.ContentLength > 0 {
          r.Body.Read(body)
       }
       req.Body = body

       // 核心步骤2:调用用户函数(带超时控制)
       ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
       defer cancel()
       res, err := handler(ctx, req)

       // 核心步骤3:封装并返回响应
       if err != nil {
          http.Error(w, err.Error(), http.StatusInternalServerError)
          return
       }

       // 设置响应Header + 状态码 + Body
       for k, v := range res.Headers {
          w.Header().Set(k, v)
       }
       w.WriteHeader(res.StatusCode)
       w.Write(res.Body)
    }
}

// 4. 示例用户函数(业务逻辑)
func HelloHandler(ctx context.Context, req *HTTPRequest) (*FunctionResponse, error) {
    name := req.Query["name"]
    if name == "" {
       name = "world"
    }

    respBody, _ := json.Marshal(map[string]string{
       "message": "hello " + name,
       "path":    req.Path,
    })

    return &FunctionResponse{
       StatusCode: http.StatusOK,
       Headers:    map[string]string{"Content-Type": "application/json"},
       Body:       respBody,
    }, nil
}

// 5. 启动服务(核心入口)
func main() {
    // 核心:将用户函数绑定到HTTP路径
    http.HandleFunc("/hello", HTTPTrigger(HelloHandler))
    // 启动HTTP服务器
    http.ListenAndServe(":8080", nil)
}

MQ

MQ 触发器核心版本


package main

import (
    "context"
    "encoding/json"
    "fmt"
    "time"

    // 模拟MQ客户端(实际用对应MQ的SDK:如Kafka/RocketMQ/Pulsar)
    "github.com/segmentio/kafka-go"
)

// 1. 定义核心结构体:MQ消息参数 + 函数响应
type MQMessage struct {
    Topic     string            `json:"topic"`    // MQ主题
    Partition int               `json:"partition"`// 分区
    Offset    int64             `json:"offset"`   // 偏移量
    Key       string            `json:"key"`      // 消息Key
    Body      []byte            `json:"body"`     // 消息体
    Headers   map[string]string `json:"headers"`  // 消息头
}

// 复用HTTP触发器的响应结构体(核心逻辑通用)
type FunctionResponse struct {
    StatusCode int               `json:"status_code"` // 仅标记处理结果(成功/失败)
    Message    string            `json:"message"`     // 处理结果描述
}

// 2. 定义MQ函数处理器签名(核心:统一入口)
type MQFunctionHandler func(ctx context.Context, msg *MQMessage) (*FunctionResponse, error)

// 3. 核心:MQ触发器实现(仅保留核心链路)
func MQTrigger(handler MQFunctionHandler, brokerAddr, topic string) {
    // 核心步骤1:创建MQ消费者(连接MQ)
    reader := kafka.NewReader(kafka.ReaderConfig{
       Brokers:  []string{brokerAddr},
       Topic:    topic,
       GroupID:  "faas-mq-group", // FaaS消费组
       MinBytes: 10e3,            // 10KB
       MaxBytes: 10e6,            // 10MB
    })
    defer reader.Close()

    fmt.Printf("MQ触发器启动:监听主题 %s\n", topic)

    // 核心步骤2:循环消费MQ消息
    for {
       // 拉取MQ消息
       m, err := reader.ReadMessage(context.Background())
       if err != nil {
          fmt.Printf("拉取MQ消息失败:%v\n", err)
          time.Sleep(1 * time.Second)
          continue
       }

       // 封装MQ消息为结构化参数(核心:和HTTP触发器的"解析请求"逻辑等价)
       msg := &MQMessage{
          Topic:     m.Topic,
          Partition: m.Partition,
          Offset:    m.Offset,
          Key:       string(m.Key),
          Body:      m.Value,
          Headers:   make(map[string]string),
       }
       // 解析消息头
       for _, h := range m.Headers {
          msg.Headers[h.Key] = string(h.Value)
       }

       // 核心步骤3:调用用户函数(带超时控制)
       ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
       defer cancel()
       res, err := handler(ctx, msg)

       // 核心步骤4:处理函数执行结果(MQ触发器核心差异:需确认/重试/死信)
       if err != nil || (res != nil && res.StatusCode != 200) {
          // 处理失败:记录日志 + 重试/发死信队列(简化版仅打印)
          errMsg := fmt.Sprintf("处理消息失败:offset=%d, err=%v", m.Offset, err)
          fmt.Println(errMsg)
          continue
       }

       // 处理成功:打印结果(实际无需手动提交offset,kafka-go自动提交)
       fmt.Printf("处理消息成功:offset=%d, result=%s\n", m.Offset, res.Message)
    }
}

// 4. 示例用户函数(MQ业务逻辑)
func MQHelloHandler(ctx context.Context, msg *MQMessage) (*FunctionResponse, error) {
    // 解析消息体(业务逻辑)
    var data map[string]string
    if err := json.Unmarshal(msg.Body, &data); err != nil {
       return nil, fmt.Errorf("解析消息体失败:%v", err)
    }

    name := data["name"]
    if name == "" {
       name = "world"
    }

    return &FunctionResponse{
       StatusCode: 200,
       Message:    fmt.Sprintf("处理MQ消息成功:hello %s (topic=%s)", name, msg.Topic),
    }, nil
}

// 5. 启动服务(核心入口)
func main() {
    // 核心:启动MQ触发器,绑定用户函数
    MQTrigger(MQHelloHandler, "localhost:9092", "faas-test-topic")
}

在实际生产环境,需要考虑更多复杂的问题。

核心维度具体问题典型风险场景生产级解决方案
消息可靠性消息丢失1. 拉取消息后进程崩溃2. 函数执行成功但 offset 未提交3. MQ 副本不足导致消息丢失1. 关闭自动提交 offset,函数执行成功后手动提交2. MQ 层面:Kafka 设 min.insync.replicas≥2+acks=all,RocketMQ 开启同步刷盘3. 触发器层面:处理前记录待办日志,崩溃后可恢复
重复消费1. offset 提交失败导致消息重推2. 业务无幂等性导致数据错乱(重复下单 / 扣款)1. 强制业务幂等:用消息 Key 作为幂等键,Redis/DB 标记处理状态2. 精准提交 offset:按最小成功 offset 批量提交3. 数据库层面:唯一索引 / 乐观锁兜底
消息堆积1. 函数执行慢 / 并发不足导致消息积压2. 堆积触发 MQ 消息过期 / 磁盘满1. 动态扩缩容:按堆积量 / 消费延迟自动扩容函数实例(设最大实例数)2. 消费限流:单实例 QPS 限制 + MQ max.poll.records 限制单次拉取数3. 优先级拆分:核心消息走高优先级队列
函数执行容错执行失败1. 函数 panic / 业务异常2. 下游服务不可用3. 网络抖动导致临时失败1. 分级重试:本地轻重试(1-2 次,100ms 间隔)+ 延时队列重重试(5s/10s/30s 阶梯)2. 死信队列(DLQ):重试失败消息打入 DLQ,人工兜底3. 失败隔离:单条失败不阻塞批量消费
执行超时1. 函数超时导致线程阻塞2. 超时后 MQ 重推,加剧堆积1. 严格超时控制:函数上下文设置超时(如 5s),超时立即终止执行2. 超时降级:超时消息直接打入 DLQ,不占用消费资源3. 监控告警:超时率超过阈值立即告警
资源过载1. 函数 CPU / 内存占用过高2. 并发消费导致下游数据库压垮1. 资源隔离:函数实例设置 CPU / 内存上限(如 1C2G)2. 并发控制:单实例消费并发数限制(如 10 并发)3. 下游限流:函数调用下游服务时加限流(如令牌桶)
性能优化消费效率1. 单条消费 QPS 低2. 网络往返次数多1. 批量消费:拉取 + 处理 + 提交均批量(如每次拉取 50 条)2. 异步处理:消息拉取和函数执行异步解耦(生产消费队列)3. 本地缓存:热点数据缓存,减少下游调用
冷启动延迟1. 函数实例冷启动导致消费延迟2. 低流量时实例缩容,突发流量触发冷启动1. 实例预热:保留最小空闲实例数(如 2 个)2. 流量预测:根据历史峰值提前扩容3. 快照加速:函数运行时内存快照,快速恢复实例
可观测性问题排查困难1. 消息消费失败无日志2. 无法定位函数执行耗时 / 错误类型1. 全链路日志:记录消息 Key/offset/ 函数执行耗时 / 错误栈2. 关键指标监控:消费 QPS / 堆积量 / 重试率 / 超时率 / 失败率3. 链路追踪:接入 Tracing(如 Jaeger),串联 MQ - 触发器 - 函数 - 下游
告警不及时1. 消息堆积到阈值未告警2. 函数执行失败率高未发现1. 多维度告警:堆积量 > 1w / 消费延迟 > 5min / 失败率 > 1% 触发告警2. 告警分级:P0(堆积超阈值)→ 电话,P1(失败率高)→ 短信,P2(超时率高)→ 邮件3. 告警降噪:相同告警 5 分钟聚合一次

问题1:faas存在消息丢失问题吗?

  • 不会FaaS 中的 MQ 触发器核心采用 “消费处理完再发送 ACK” 的模式(推模式为主) ,而非 “拉到消息就发 ACK”—— 本质是 FaaS 平台作为 MQ 的消费者,先把消息推送给函数实例,等函数处理完成(且无异常)后,才向 MQ 发送 ACK 确认;若函数处理失败,触发器会触发重试,且不会发送 ACK,确保消息不丢失。

问题2:faas存在重复消费问题吗?

  • 会的,FaaS 中的 MQ 触发器依然存在重复消费问题,这是由 MQ 的消费机制和 FaaS 的无状态特性共同决定的,并非触发器本身的缺陷,而是分布式系统的共性问题。
    • 具体导致重复消费的核心场景有 3 类:
      • 函数执行成功但 ACK 发送失败函数处理完消息后,向 MQ 发送 ACK 的过程中出现网络抖动 / 平台故障,MQ 未收到 ACK,会判定消息未消费成功,将消息重新入队并再次推送给 FaaS 函数。
      • 函数执行超时触发重试FaaS 函数有执行超时限制(比如 30 秒),若业务处理耗时超过阈值,触发器会判定执行失败,触发重试逻辑,而此时函数可能已经处理完消息,导致重复消费。
      • 实例扩缩容导致的重复推送流量突增时 FaaS 会快速扩容多个函数实例,MQ 可能将同一条消息推送给多个实例;或者缩容时,正在处理消息的实例被强制销毁,MQ 会重新推送该消息。

其他

FaaS 与 IaaS、PaaS、Serverless 的关系及区别

核心结论:Serverless 是一种 “无服务器” 的架构理念,而 FaaS 是 Serverless 理念的核心落地形态;IaaS、PaaS 是传统云服务分层模型,与 FaaS 属于不同维度的云服务分类,且 PaaS 与 Serverless 存在部分重叠。

概念全称核心定义
IaaSInfrastructure as a Service(基础设施即服务)云厂商提供底层基础设施(服务器、虚拟机、存储、网络),用户自己部署操作系统、中间件、应用程序,需运维底层资源。
PaaSPlatform as a Service(平台即服务)云厂商提供应用运行平台(如数据库、应用服务器、开发工具),用户只需上传应用代码,无需关注操作系统和中间件的运维。
FaaSFunction as a Service(函数即服务)函数为最小部署单元,用户上传函数代码,平台负责调度、运行、弹性扩缩容,事件驱动触发执行,按实际运行时长计费。
Serverless无服务器架构一种架构理念:用户无需关注服务器 / 基础设施的管理,仅聚焦业务逻辑;核心特征是事件驱动、按需计费、弹性伸缩、无状态,FaaS + BaaS(后端即服务)是其典型落地方式。

学习自《Serverless核心技术和大规模实践》