第一章:从单体到Serverless:Go在FaaS平台开发中的演进全景
传统单体架构正加速让位于事件驱动、按需伸缩的Serverless范式,而Go语言凭借其轻量二进制、快速启动、高并发原生支持与极低内存开销,已成为主流FaaS平台(如AWS Lambda、Google Cloud Functions、OpenFaaS)首选的后端语言之一。这一演进不仅是部署形态的转变,更是开发思维的重构——从“管理服务器”转向“专注业务逻辑”,从“长时运行进程”转向“毫秒级冷启动响应”。
Go函数模型的核心优势
- 启动性能卓越:编译为静态链接二进制,无运行时依赖,Lambda冷启动通常低于100ms;
- 资源效率突出:单函数实例内存占用常低于20MB,远低于Node.js或Java;
- 并发模型天然适配:goroutine轻量调度完美匹配高并发事件流(如Kafka消息、API网关请求)。
在OpenFaaS中部署Go函数的典型流程
- 初始化函数模板:
faas-cli new --lang go hello-world; - 编写业务逻辑(
handler.go):package function
import ( “fmt” “io/ioutil” “net/http” )
// Handle接受HTTP请求并返回响应 func Handle(w http.ResponseWriter, r *http.Request) { body, _ := ioutil.ReadAll(r.Body) fmt.Fprintf(w, “Hello, received %d bytes: %s”, len(body), string(body)) }
3. 构建并部署:`faas-cli build -f hello-world.yml && faas-cli deploy -f hello-world.yml`。
### 主流平台能力对比
| 平台 | Go支持方式 | 最小内存配置 | 冷启动典型耗时 | 自定义Dockerfile支持 |
|------------------|--------------------|--------------|----------------|----------------------|
| AWS Lambda | 官方Runtime API | 128MB | 80–150ms | ✅ |
| Google Cloud Functions | Go 1.19+原生支持 | 128MB | <60ms | ✅(via Buildpacks) |
| OpenFaaS | 模板化Docker构建 | 可设至16MB | 40–90ms | ✅ |
随着Cloudflare Workers对Go WASM的支持成熟,以及Knative Serving对Go Knative Service的深度优化,Go在FaaS生态中的角色正从“可用”迈向“首选”。开发者不再需要为语言特性妥协架构弹性,而是以更少的胶水代码、更高的资源密度,交付可瞬时扩缩的业务能力。
## 第二章:Go在FaaS平台的3种主流形态深度解析
### 2.1 基于HTTP Handler的无状态API服务——Lambda Go Runtime API实践
AWS Lambda 的 Go 运行时通过 `http.Handler` 暴露标准化的 Runtime API 接口,实现函数生命周期与执行上下文的解耦。
#### 核心交互流程
```go
func main() {
http.HandleFunc("/runtime/invocation/next", handleInvocation)
http.HandleFunc("/runtime/invocation/<id>/response", handleResponse)
http.ListenAndServe(":8080", nil) // Lambda Runtime API 端点
}
该服务监听 8080 端口,响应 /runtime/invocation/next 获取待执行事件,返回 JSON 结构含 awsRequestId 和 payload;/runtime/invocation/{id}/response 用于回传结果。所有路径均无状态,依赖外部存储管理上下文。
关键端点语义
| 端点 | 方法 | 用途 |
|---|---|---|
/runtime/invocation/next |
GET | 拉取下一个待处理事件 |
/runtime/invocation/{id}/response |
POST | 提交执行结果 |
/runtime/invocation/{id}/error |
POST | 上报运行时错误 |
执行模型示意
graph TD
A[Lambda Runtime] -->|GET /next| B[Go Handler]
B -->|return event| C[用户函数]
C -->|POST /response| B
B -->|HTTP 200| A
2.2 事件驱动型微服务架构——SNS/SQS/EventBridge触发器与Go SDK集成实测
核心触发器对比
| 服务 | 触发场景 | Go SDK 关键包 | 重试机制 |
|---|---|---|---|
| SNS | 发布-订阅广播 | github.com/aws/aws-sdk-go-v2/service/sns |
无(需下游处理) |
| SQS | 点对点队列消费 | github.com/aws/aws-sdk-go-v2/service/sqs |
内置可见性超时+死信队列 |
| EventBridge | 基于事件总线的路由 | github.com/aws/aws-sdk-go-v2/service/eventbridge |
可配置最大重试次数与指数退避 |
Go SDK 消费 SQS 消息示例
msg, err := sqsClient.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
QueueUrl: aws.String(queueURL),
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20,
VisibilityTimeout: 30, // 处理失败后重新入队前的锁定时间
})
if err != nil {
log.Fatal(err)
}
VisibilityTimeout 决定消息在被成功删除前对其他消费者不可见的时长;WaitTimeSeconds 启用长轮询降低空响应开销。
事件流协同拓扑
graph TD
A[API Gateway] -->|HTTP POST| B[SNS Topic]
B --> C[EventBridge Rule]
C --> D[Lambda A]
C --> E[SQS Queue]
E --> F[Go Worker]
2.3 长时任务协程化封装方案——利用Lambda Extension + Go goroutine管理后台作业
在 AWS Lambda 环境中,长时任务(如文件处理、ETL)需绕过 15 分钟执行限制。本方案通过 Lambda Extension 拦截 Runtime API,将耗时逻辑卸载至独立 Go 进程,并由 goroutine 池统一调度。
数据同步机制
Extension 启动后监听 /2023-09-28/telemetry 流式日志,提取 INVOKE_START 事件并触发异步作业:
func spawnJob(ctx context.Context, payload []byte) {
go func() {
defer wg.Done()
job := parsePayload(payload) // 解析函数调用参数(含 S3 key、timeoutSec)
processFile(job) // 实际业务逻辑(含重试、进度上报)
}()
}
payload 包含原始 Lambda event 序列化数据;processFile 内部使用 context.WithTimeout 控制单次 goroutine 生命周期,避免泄漏。
协程治理策略
| 维度 | 策略 |
|---|---|
| 并发控制 | 固定大小 worker pool(默认 5) |
| 异常隔离 | panic recover + 日志透传 |
| 资源回收 | defer runtime.GC() 触发内存清理 |
graph TD
A[Extension 启动] --> B{接收 INVOKE_START}
B --> C[解析 event & spawn goroutine]
C --> D[Worker Pool 分发]
D --> E[执行 + 上报状态到 CloudWatch]
2.4 构建可复用FaaS组件库——Go Module设计、版本语义化与跨环境兼容性验证
模块化结构设计
采用 go mod init github.com/org/faas-kit 初始化模块,明确声明主路径。依赖收敛至 faas-core、faas-http、faas-event 三个子模块,避免循环引用。
语义化版本实践
// go.mod 中关键声明
module github.com/org/faas-kit
go 1.21
require (
github.com/org/faas-core v1.3.0 // 主功能抽象
github.com/org/faas-http v0.8.2 // 兼容性预发布
)
v1.3.0 表示向后兼容的功能增强;v0.8.2 属于预发布阶段,允许破坏性变更,适用于灰度环境验证。
跨环境兼容性验证矩阵
| 环境 | Go 版本 | OS | 验证项 |
|---|---|---|---|
| 本地开发 | 1.21 | macOS | 单元测试覆盖率 ≥92% |
| CI/CD | 1.21/1.22 | Linux AMD64 | 构建+集成测试通过 |
| 生产沙箱 | 1.21 | Linux ARM64 | 函数冷启动耗时 ≤120ms |
自动化验证流程
graph TD
A[Git Tag v1.3.0] --> B[触发多平台构建]
B --> C{Linux AMD64}
B --> D{Linux ARM64}
B --> E{macOS}
C --> F[运行基准测试]
D --> F
E --> F
F --> G[生成兼容性报告]
2.5 多运行时协同编排——Go Lambda与Python/Node.js函数通过Step Functions协同调用案例
在微服务化无服务器架构中,不同语言运行时的职责解耦日益普遍。Go Lambda 以高并发与低延迟处理核心事务(如订单校验),Python 函数执行数据清洗与ML推理,Node.js 负责实时通知发送。
架构概览
graph TD
A[Step Functions State Machine] --> B[Go Lambda: validateOrder]
B --> C{Validation Pass?}
C -->|Yes| D[Python Lambda: enrichAndScore]
C -->|No| E[Fail State]
D --> F[Node.js Lambda: sendNotification]
F --> G[Success State]
关键集成点
- Step Functions 使用
InputPath/OutputPath精确裁剪跨函数数据载荷 - Go 函数返回结构体需 JSON 序列化兼容 Python/Node.js 的
event解析 - 所有 Lambda 均启用
Publish: true以支持 Step Functions 的同步等待
Go Lambda 核心逻辑(简化)
func handler(ctx context.Context, event map[string]interface{}) (map[string]interface{}, error) {
orderID := event["order_id"].(string)
// 参数说明:event 为 Step Functions 传入的 JSON 映射;order_id 必须存在且非空
if len(orderID) == 0 {
return nil, errors.New("invalid order_id")
}
return map[string]interface{}{
"order_id": orderID,
"validated": true,
"timestamp": time.Now().Unix(),
}, nil
}
该函数仅校验基础字段并注入时间戳,输出结构被下游 Python 函数直接消费,无需反序列化适配。
第三章:Go函数冷启动机理与性能瓶颈定位
3.1 Go二进制加载、GC初始化与runtime.init耗时拆解(perf + pprof实测)
Go程序启动时,runtime.main前的阶段包含:ELF加载 → .init_array执行 → runtime·rt0_go → schedinit → GC初始化 → runtime.init(用户包级init())。
perf采样关键路径
perf record -e 'cycles,instructions,page-faults' -g ./myapp
perf script | grep -A5 -B5 'runtime\.mallocgc\|runtime\.mspanInit'
该命令捕获CPU周期、指令数及缺页中断,聚焦GC内存管理初始化热点。
启动阶段耗时分布(典型Web服务)
| 阶段 | 平均耗时(ms) | 主要开销来源 |
|---|---|---|
| ELF加载与TLS初始化 | 0.8 | mmap、动态符号解析 |
| GC堆结构初始化 | 2.3 | mheap_.spanalloc预分配 |
runtime.init |
4.1 | 包依赖拓扑排序+串行执行 |
GC初始化核心流程
// src/runtime/mgc.go: markinit()
func markinit() {
work.markrootStacks = true // 启用栈扫描标记
work.nproc = uint32(GOMAXPROCS(0)) // 绑定P数量
}
此函数在mallocinit()后立即调用,为首次GC准备标记工作队列与并发协程数,nproc直接受GOMAXPROCS环境变量影响。
graph TD A[main → rt0_go] –> B[sysmon启动] B –> C[schedinit → mallocinit] C –> D[gcinit → markinit] D –> E[runtime.init → user init]
3.2 Lambda执行环境预热机制与Go init()阶段优化黄金法则
Lambda冷启动时,Go runtime 的 init() 阶段执行顺序直接影响预热效率。合理编排初始化逻辑,可显著缩短首次调用延迟。
init() 执行顺序约束
Go 按包依赖拓扑排序执行 init() 函数,不可跨包控制时序,但可通过以下方式优化:
- 将耗时操作(如HTTP客户端配置、数据库连接池预热)延迟至
handler首次调用前完成 - 避免在
init()中执行阻塞I/O或未缓存的密钥获取 - 使用
sync.Once包裹幂等初始化逻辑,兼顾并发安全与懒加载
预热触发时机对比
| 触发方式 | 初始化阶段 | 是否复用 init() |
典型延迟影响 |
|---|---|---|---|
| Lambda warm-up 请求 | Runtime 启动后 | ✅ 复用已执行的 init() |
低(仅 handler 层) |
| 正常 HTTP 调用 | init() + handler |
✅ 复用 | 中(若 init() 含重操作) |
| 并发首次调用 | init() 并发竞争 |
❌ 可能重复执行(无 sync.Once) | 高(资源争抢) |
var dbOnce sync.Once
var db *sql.DB
func init() {
// ✅ 安全:init() 仅注册,不执行重操作
// 实际连接池构建推迟到首次使用
}
func getDB() *sql.DB {
dbOnce.Do(func() {
db = sql.Open("postgres", os.Getenv("DB_URI"))
db.SetMaxOpenConns(10)
})
return db
}
该写法将连接池构建从 init() 延迟到 getDB() 首次调用,避免冷启动时阻塞;sync.Once 确保线程安全且仅执行一次。Lambda 容器复用时,dbOnce 状态保留,后续调用直接返回已初始化实例。
graph TD
A[Lambda Runtime 启动] --> B[执行所有 init\\n(轻量级注册)]
B --> C[收到首个请求]
C --> D{调用 getDB\\n?}
D -->|是| E[dbOnce.Do\\n构建连接池]
D -->|否| F[跳过初始化]
E --> G[返回就绪 DB 实例]
3.3 并发模型适配分析:Goroutine调度器在短生命周期函数中的行为偏差与规避策略
Goroutine 调度器对微秒级函数(如 http.HandlerFunc 中的简单校验逻辑)存在调度开销放大效应:M-P-G 协作模型中,goroutine 创建/唤醒/抢占的固定成本可能远超其实际执行时间。
调度偏差根源
- 新 goroutine 默认通过
newproc分配,触发栈分配(2KB起)与 G 结构体初始化; - 短任务常在
runtime.mcall前即完成,导致 P 处于空转等待状态; - GC 扫描时仍需遍历所有 G 的栈指针,增加 STW 压力。
规避策略对比
| 策略 | 适用场景 | 开销降低 | 注意事项 |
|---|---|---|---|
| 同步执行 | ~92% | 需确保无 channel/send、time.Sleep | |
| sync.Pool 复用 goroutine | 高频中短任务(10–100μs) | ~65% | 需自定义 G 封装与安全回收 |
goless 模式(runtime.LockOSThread + 循环复用) |
极端低延迟服务 | ~88% | 丧失并发弹性,仅限单 P 绑定 |
// 同步执行替代方案(推荐用于校验类短函数)
func validateUserSync(u *User) error {
if u.ID == 0 { return errors.New("id required") }
if len(u.Email) > 254 { return errors.New("email too long") }
return nil // 执行耗时 ≈ 300ns,远低于 goroutine 启动开销(≈2.1μs)
}
该函数避免 go validateUser(u) 调用,消除 g0 → g 栈切换、G 状态机更新(_Grunnable → _Grunning)及后续 schedule() 查找空闲 P 的三重开销。基准测试显示 QPS 提升 3.2×,P99 延迟下降至 87ns。
graph TD
A[调用 go f()] --> B[allocg: 分配 G 结构体]
B --> C[stackalloc: 分配 2KB 栈]
C --> D[schedule(): 寻找空闲 P]
D --> E[execute(): 切换至 g0 栈执行]
E --> F[f() 实际执行]
F -->|若 f<1μs| G[调度器尚未响应已返回]
关键参数说明:GOMAXPROCS 不影响单 goroutine 启动延迟;GODEBUG=schedtrace=1000 可观测 schedlt 字段确认调度滞留。
第四章:面向生产的Go函数冷启动优化四大工程方案
4.1 静态链接与UPX压缩——减小部署包体积与冷启动延迟的量化对比(12MB→3.2MB,RT↓41%)
静态链接消除动态依赖,UPX进一步对可执行文件进行LZMA熵编码压缩。二者叠加显著降低磁盘IO与内存映射开销。
压缩前后关键指标对比
| 指标 | 原始动态链接 | 静态链接 + UPX |
|---|---|---|
| 二进制体积 | 12.0 MB | 3.2 MB |
| 冷启动RT(P95) | 862 ms | 509 ms |
mmap()页缺页数 |
1,247 | 413 |
构建流程示意
# 启用静态链接(Go示例)
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o svc-static main.go
# UPX压缩(需UPX 4.2+以支持现代ELF重定位)
upx --lzma --best --compress-strings svc-static
逻辑分析:
-a强制静态编译所有依赖;-ldflags '-extldflags "-static"'确保C标准库亦静态链接;UPX--lzma --best启用最高压缩比,但会增加解压CPU开销(实测仅增3.2ms,远低于IO节省)。
性能影响路径
graph TD
A[加载ELF] --> B[解析动态符号表]
A --> C[静态映射段]
C --> D[UPX stub解压]
D --> E[跳转至原始入口]
E --> F[执行初始化]
4.2 初始化延迟加载(Lazy Initialization)——DB连接池、配置中心客户端、TLS证书缓存的按需加载模式
延迟加载并非简单推迟 new,而是将资源绑定与首次使用解耦,避免冷启动阻塞与无效初始化。
为何选择 lazy?
- DB 连接池:避免应用启动时连接未就绪或数据库不可达导致失败
- 配置中心客户端:仅当首次
getConfig("db.url")时才建立长连接与监听 - TLS 证书缓存:证书解析开销大,且多数请求不涉及双向认证
典型实现模式(Java + Spring Boot)
@Component
public class LazyDataSource {
private volatile HikariDataSource dataSource;
public Connection getConnection() {
if (dataSource == null) {
synchronized (this) {
if (dataSource == null) {
dataSource = buildHikariPool(); // 含 connection-test-query、max-lifetime 等关键参数
}
}
}
return dataSource.getConnection();
}
}
逻辑分析:双重检查锁(DCL)保障线程安全;
buildHikariPool()中connectionTimeout=3000防止无限等待,leakDetectionThreshold=60000主动捕获连接泄漏。
加载时机对比表
| 组件 | 饿汉式启动耗时 | 首次调用延迟 | 内存占用峰值 |
|---|---|---|---|
| DB 连接池 | 850ms | +0ms | 12MB |
| 配置中心客户端 | 420ms | +180ms | 3.2MB |
| TLS 证书缓存 | 310ms | +95ms | 1.8MB |
初始化依赖关系(mermaid)
graph TD
A[应用启动] --> B{首次 DB 查询?}
B -- 是 --> C[初始化连接池]
C --> D[触发 TLS 证书校验]
D --> E[按需加载证书缓存]
B -- 否 --> F[跳过全部]
4.3 预置并发(Provisioned Concurrency)+ Go sync.Once协同机制实现零抖动热实例复用
Lambda 预置并发确保函数实例常驻内存,但冷启动后的初始化逻辑(如数据库连接池构建、配置加载)仍可能引入毫秒级抖动。sync.Once 可精准保障初始化仅执行一次,与预置并发形成“热态固化”闭环。
初始化时机对齐策略
- 预置并发实例启动时触发
sync.Once.Do(init) - 所有后续调用直接复用已初始化的全局资源
关键代码实现
var (
dbOnce sync.Once
db *sql.DB
)
func initDB() {
// 预置并发实例启动时执行:连接池创建、迁移校验、健康探针注册
db = sql.Open("postgres", os.Getenv("DB_URL"))
db.SetMaxOpenConns(20)
}
func Handler(ctx context.Context, event Event) (Response, error) {
dbOnce.Do(initDB) // ✅ 仅首次调用触发,零重复开销
return query(db, event.ID)
}
dbOnce.Do(initDB)确保在预置并发生命周期内仅初始化一次;SetMaxOpenConns(20)匹配预置并发数,避免连接争抢。
性能对比(ms,P99 延迟)
| 场景 | 初始化方式 | P99 延迟 |
|---|---|---|
| 无预置 + 每次初始化 | initDB() in handler |
182 ms |
预置并发 + sync.Once |
如上代码 | 12 ms |
graph TD
A[预置并发实例启动] --> B[sync.Once.Do(initDB)]
B --> C[DB连接池就绪]
C --> D[后续请求直连复用]
4.4 自定义Runtime Bootstrap优化——替换默认Go bootstrap,内联初始化逻辑并绕过冗余反射调用
Go 默认 runtime bootstrap 在程序启动时执行 runtime.main → runtime.doInit → 反射驱动的包级 init 调用链,带来可观开销(尤其在嵌入式或冷启动敏感场景)。
核心优化路径
- 替换
runtime/proc.go中main_init入口为静态初始化序列 - 将
init()函数地址直接硬编码进.init_array,跳过types.InitMap查找 - 移除
reflect.Value.Call对 init 函数的间接调用,改用CALL rel32
关键代码改造示例
// 替换原 runtime.doInit 的反射调用:
// fn := unsafe.Pointer(&initFuncs[i])
// reflect.ValueOf(fn).Call(nil)
// → 改为直接跳转:
asm volatile ("call *%0" : : "r"(initFuncs[i].fnptr) : "ax", "dx", "cx");
initFuncs[i].fnptr 是编译期生成的函数指针数组,避免运行时 unsafe 转换与反射元数据查找;"ax","dx","cx" 明确声明被修改寄存器,保障 ABI 兼容性。
性能对比(ARM64,100个init函数)
| 指标 | 默认 bootstrap | 自定义 bootstrap |
|---|---|---|
| 启动延迟 | 842 μs | 217 μs |
| 初始化栈帧数 | 32+ | 1(内联单帧) |
graph TD
A[main] --> B[custom_bootstrap_start]
B --> C[inline_init_1]
C --> D[inline_init_2]
D --> E[...]
E --> F[runtime.main]
第五章:未来展望:WasmEdge、Spin与Go Serverless生态的新边界
WasmEdge 在边缘 AI 推理中的真实落地场景
某智能安防 SaaS 企业将 YOLOv5 模型量化为 ONNX 格式,通过 wasmedge-tensorflow-lite 插件部署至 WasmEdge 运行时。在 200 台边缘网关(ARM64 架构,2GB RAM)上,单次人脸检测平均耗时 83ms,较传统 Docker 容器方案降低 62% 内存占用。其 Go SDK 直接调用 wasi-nn 接口,无需修改模型推理逻辑:
import "github.com/second-state/wasmedge-go/wasmedge"
// 初始化 WASI-NN 实例并加载模型二进制
nnCtx, _ := wasmedge.NewWasiNNContext()
nnCtx.LoadModel(modelBytes, wasmedge.WasiNNFormatOnnx)
Spin 应用在多云 Serverless 环境的灰度发布实践
一家电商中台团队使用 Spin 构建订单履约微服务,通过 spin up --listen 0.0.0.0:3000 启动本地调试,再一键部署至 AWS Lambda(via spin deploy --target aws-lambda)和阿里云函数计算(通过自定义 spin-cloud-aliyun 插件)。关键配置如下表所示:
| 环境 | 冷启动时间 | 并发上限 | Go Runtime 版本 | 自动扩缩策略 |
|---|---|---|---|---|
| AWS Lambda | 210ms | 1000 | go1.21 | 请求速率 >50/s 触发 |
| 阿里云 FC | 140ms | 3000 | go1.22 | CPU 使用率 >70% 持续1min |
Go 语言与 WebAssembly 的协同演进路径
Go 1.22 原生支持 GOOS=wasip1 GOARCH=wasm 编译目标,开发者可直接复用 net/http 和 encoding/json 标准库。某物联网平台将设备协议解析模块(含 Modbus/TCP 解包逻辑)编译为 .wasm 文件,嵌入前端控制台,实现浏览器端实时解析原始 TCP 报文:
$ GOOS=wasip1 GOARCH=wasm go build -o protocol.wasm ./cmd/parser
$ wasmtime run --dir=. protocol.wasm -- --input hex:000100000006010300010001
{"function_code":3,"register":1,"value":42}
生态工具链的互操作性验证
团队构建了跨运行时兼容性测试矩阵,覆盖 7 类典型 Serverless 场景(HTTP 触发、定时任务、消息队列消费等),结果表明:
- WasmEdge v0.14.0 与 Spin v2.10 兼容性达 100%,所有 HTTP 路由与中间件正常工作;
- Go 编译的 Wasm 模块在 Spin 中可通过
spin_sdk::http::send_request()调用外部 API,但需显式声明wasi-httpcapability; tinygo编译的轻量模块在内存受限设备(如 ESP32-WROVER)上成功运行,但无法使用reflect包。
graph LR
A[Go 源码] -->|go build -o main.wasm| B(WASI 兼容 Wasm 模块)
B --> C{运行时选择}
C --> D[WasmEdge<br>支持 GPU 加速]
C --> E[Spin<br>内置 HTTP 服务器]
C --> F[Wasmer<br>企业级监控集成]
D --> G[边缘视频分析]
E --> H[多租户 API 网关]
F --> I[金融风控实时计算]
开源社区驱动的标准化进程
Bytecode Alliance 正在推进 WASI Preview2 标准,其中 wasi:http 和 wasi:io 接口已获 WasmEdge、Spin、Wasmer 三方实现。Go 团队提交的 x/sys/wasi 包 PR#59217 已合并,使标准库能直接调用 wasi_snapshot_preview1::args_get 等底层接口。某开源项目 wasm-go-sqlite 利用该能力,在浏览器中运行完整 SQLite 引擎,执行 SELECT * FROM orders WHERE status='pending' 返回 127 条记录,全程无服务端参与。
