第一章:Go语音输入延迟飙高真相的全景洞察
Go语言本身并不直接处理语音输入,但大量语音识别服务(如基于WebRTC的实时ASR、gRPC流式语音接口或嵌入式语音SDK)常以Go作为后端服务语言。当开发者观察到“语音输入延迟飙高”,问题往往并非源于Go运行时,而是其在高并发音频流处理场景下暴露的系统级瓶颈与设计盲区。
音频流处理中的阻塞式I/O陷阱
许多Go语音服务错误地使用bufio.NewReader(os.Stdin)或同步HTTP body读取处理实时音频帧,导致单goroutine阻塞整个流。正确做法是启用非阻塞流式读取,并为每个音频连接分配独立goroutine:
// ✅ 正确:为每个WebSocket音频连接启动独立goroutine处理帧
func handleAudioStream(conn *websocket.Conn) {
defer conn.Close()
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Printf("audio read error: %v", err)
break
}
// 异步提交至ASR处理池,避免阻塞网络读取
select {
case asrQueue <- AudioFrame{Data: msg, Timestamp: time.Now()}:
default:
log.Warn("ASR queue full, dropping frame")
}
}
}
GC压力与小对象高频分配
16kHz PCM音频每秒生成约32KB原始数据,若每10ms切分一帧并封装为struct{Data []byte; TS time.Time},将触发高频堆分配——Go GC在毫秒级延迟敏感场景中可能造成20–100ms STW暂停。解决方案包括:
- 使用
sync.Pool复用音频帧结构体 - 采用预分配缓冲区+
bytes.Reader避免拷贝 - 启用
GODEBUG=gctrace=1监控GC频率与停顿
网络栈与内核缓冲区失配
常见配置失误如下表所示:
| 问题现象 | 根本原因 | 推荐修复 |
|---|---|---|
| 首包延迟>200ms | TCP慢启动 + Nagle算法启用 | conn.SetNoDelay(true) |
| 持续抖动(Jitter) | SO_RCVBUF过小导致丢帧 | conn.SetReadBuffer(1048576) |
| 连接建立耗时波动大 | DNS解析未缓存 | 使用net.Resolver配置缓存TTL |
跨语言调用链路断点
当Go服务调用Python/C++ ASR模型(如Whisper.cpp gRPC封装),需检查:
- gRPC客户端
WithBlock()阻塞等待连接完成 → 改用DialContext配合超时控制 - protobuf序列化未启用
proto.MarshalOptions{Deterministic: true}导致哈希不一致 - 模型加载未预热,首请求触发动态库加载与显存分配
延迟根因从来不在单一模块,而藏于音频采样率、网络MTU、Go调度器GMP交互、以及Cgo调用边界之间的隐式耦合中。
第二章:HTTP/2头部压缩机制与TCP队头阻塞的深层耦合
2.1 HTTP/2 HPACK压缩原理与Go net/http实现剖析
HPACK 是 HTTP/2 唯一指定的头部压缩算法,通过静态表(61个常用头字段)、动态表(LRU管理)及哈夫曼编码协同降低冗余。
静态与动态表协同机制
- 静态表:预定义
:method: GET等高频键值,索引 1~61,无需传输字节 - 动态表:运行时缓存新头部,初始容量 4096 字节,按 LRU 淘汰
Go 中的 HPACK 实现关键路径
// src/net/http/h2/hpack/encode.go
func (e *Encoder) WriteField(f HeaderField) {
if idx := e.staticTableIndex(f); idx > 0 {
e.writeIndexed(idx) // 直接引用静态表索引
} else if idx := e.dynamicTableIndex(f); idx > 0 {
e.writeIndexed(idx + staticTableLen) // 动态表索引偏移
} else {
e.writeLiteralWithIncremental(f) // 新增并入动态表
}
}
WriteField 根据字段是否命中静态/动态表,选择 indexed、literal with indexing 或 literal without indexing 编码模式;e.tableSize 动态跟踪容量,触发 evict() 时按 LRU 移除最久未用项。
编码模式对比
| 模式 | 是否更新动态表 | 典型场景 |
|---|---|---|
| Indexed | 否 | :status: 200(静态表索引 8) |
| Literal w/ Indexing | 是 | 首次出现的 x-request-id: abc123 |
| Literal w/o Indexing | 否 | 敏感字段如 authorization |
graph TD
A[HeaderField] --> B{命中静态表?}
B -->|是| C[writeIndexed]
B -->|否| D{命中动态表?}
D -->|是| C
D -->|否| E[writeLiteralWithIncremental]
2.2 TCP队头阻塞在流式语音场景下的放大效应实测
流式语音对端到端延迟极为敏感,TCP的有序交付特性在丢包时会触发重传等待,导致后续数据包即使已抵达接收端也无法被上层消费。
数据同步机制
语音帧以20ms为单位编码,每帧约320字节。当第5个TCP段丢失时,接收窗口将停滞,后续7–12帧持续堆积在内核缓冲区:
# 模拟语音帧到达时间戳(单位:ms)
arrival_times = [100, 120, 140, 160, 180, 200, 220, 240, 260] # 第5帧(180ms)丢失
# 实际解码启动时间 = 200ms + RTO ≈ 240ms → 延迟放大至60ms(超语音容忍阈值2×)
RTO初始值设为200ms(典型弱网配置),重传后才释放窗口,造成后续帧“空等”。
延迟放大对比(单位:ms)
| 网络条件 | 单帧理论延迟 | 实测平均解码延迟 | 放大倍数 |
|---|---|---|---|
| 无丢包 | 20 | 22 | 1.1× |
| 1%丢包 | 20 | 68 | 3.4× |
关键路径依赖
graph TD
A[语音编码] --> B[TCP分段]
B --> C{网络传输}
C -->|丢包| D[等待RTO重传]
C -->|正常| E[内核缓冲]
D --> F[窗口解锁]
F --> G[批量提交至解码器]
语音业务中,单次队头阻塞可使连续5帧解码延迟突破100ms,触发Jitter Buffer主动扩容,进一步恶化实时性。
2.3 Go runtime对HTTP/2连接复用与流调度的底层行为验证
Go 的 net/http 包在启用 HTTP/2 后,自动复用底层 TCP 连接并基于 stream ID 调度并发请求。其核心由 http2ClientConn 和 http2Framer 协同完成帧解析与流生命周期管理。
流复用触发条件
- 同一
*http.Transport实例下、相同Host与TLS配置的请求共享连接; MaxIdleConnsPerHost控制空闲连接上限(默认2);IdleConnTimeout决定复用窗口(默认30s)。
底层帧调度验证示例
// 启用 HTTP/2 调试日志(需编译时设置 GODEBUG=http2debug=2)
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{NextProtos: []string{"h2"}},
},
}
该配置强制协商 h2 协议,触发 http2ConfigureTransport 自动注入 http2.Transport,接管连接池与流优先级树(RFC 7540 §5.3)。
流优先级调度示意
graph TD
A[Client Request] --> B{Stream ID 分配}
B --> C[HEADERS Frame]
B --> D[PRIORITY Frame]
C --> E[Server 多路复用处理]
D --> E
| 行为 | Go runtime 实现位置 |
|---|---|
| 连接复用判断 | http2Transport.RoundTrip |
| 流 ID 分配 | http2ClientConn.nextStreamID |
| PRIORITY 帧生成 | http2Framer.writePriority |
2.4 基于Wireshark+pprof的端到端延迟归因实验设计
为精准定位跨组件延迟瓶颈,构建“网络层–应用层–运行时”协同分析链路:
实验数据采集流程
- 在客户端发起带唯一 trace-id 的 HTTP 请求(如
/api/v1/order?trace=abc123) - Wireshark 抓取服务端
eth0接口全量流量,过滤http.request.id == "abc123" - Go 服务启用 pprof:
net/http/pprof并在请求上下文中注入runtime.SetBlockProfileRate(1)
关键代码锚点
// 在 handler 中注入 pprof 标签与时间戳
func orderHandler(w http.ResponseWriter, r *http.Request) {
traceID := r.URL.Query().Get("trace")
runtime.SetMutexProfileFraction(1) // 启用锁竞争采样
start := time.Now()
defer func() {
log.Printf("TRACE-%s: total=%v", traceID, time.Since(start)) // 用于对齐Wireshark时间轴
}()
// ...业务逻辑
}
该代码确保每个请求携带可追踪的起止时间,并激活运行时阻塞/锁采样,使 pprof 数据能与 Wireshark 的 frame.time_relative 精确对齐(精度达 μs 级)。
协同分析维度对照表
| 维度 | Wireshark 指标 | pprof 指标 | 归因目标 |
|---|---|---|---|
| 网络延迟 | tcp.time_delta(SYN→ACK) |
— | TCP 握手耗时 |
| 应用处理延迟 | — | net/http.(*ServeMux).ServeHTTP 耗时 |
路由与中间件开销 |
| GC 阻塞延迟 | — | runtime.gopark in gcBgMarkWorker |
STW 或标记暂停 |
graph TD
A[Client Request] --> B[Wireshark: TCP/IP/HTTP timing]
A --> C[Go pprof: CPU/Block/Mutex profiles]
B & C --> D[Trace-ID 时间轴对齐]
D --> E[交叉比对延迟峰值重叠区间]
E --> F[定位根因:如 GC pause 导致 ACK 延迟突增]
2.5 复现延迟飙升的最小可运行语音输入Demo及压测脚本
构建极简语音输入服务
使用 Python + Flask 搭建单端点 ASR 模拟服务,仅接收 WAV 二进制流并返回固定 JSON 响应:
from flask import Flask, request
import time
app = Flask(__name__)
@app.route('/asr', methods=['POST'])
def asr():
audio = request.get_data() # 不解析,仅模拟处理耗时
time.sleep(0.8) # 强制引入 800ms 处理延迟(复现高延迟关键)
return {"text": "hello", "latency_ms": 800}
逻辑分析:
time.sleep(0.8)是复现延迟飙升的核心——它绕过真实 ASR 模型,精准注入可控延迟;参数0.8对应 800ms,略高于典型语音 RTT 阈值(600ms),触发客户端重试与队列堆积。
压测脚本设计
使用 locust 并发发送请求,模拟真实用户语音流节奏:
| 用户数 | RPS | 持续时间 | 触发现象 |
|---|---|---|---|
| 10 | 5 | 60s | 延迟稳定 |
| 50 | 25 | 60s | P99 延迟跃升至 3200ms |
graph TD
A[Locust Client] -->|HTTP POST /asr| B[Flask Server]
B --> C{sleep 0.8s}
C --> D[Response]
D --> E[Client 记录 latency_ms]
第三章:Go标准库中net/http/2.0的关键瓶颈定位
3.1 server.go中header写入与frame缓冲区竞争分析
数据同步机制
server.go 中 header 写入与 frame 缓冲区共享 writeBuffer,但无统一锁保护,导致竞态窗口。
// writeHeaderLocked 在 header 写入前仅锁定 headerMutex
func (s *Server) writeHeaderLocked(w io.Writer, h *Header) error {
// ⚠️ 此时 frameBuf 可能正被 writeFrame 并发修改
return binary.Write(w, binary.BigEndian, h)
}
该函数仅保护 header 结构体序列化,不阻塞 frameBuf.Write() 调用,引发内存重叠写风险。
竞态路径示意
graph TD
A[goroutine A: writeHeaderLocked] --> B[序列化 Header 到 writeBuffer]
C[goroutine B: writeFrame] --> D[追加 Frame payload 到同一 writeBuffer]
B --> E[缓冲区边界错位]
D --> E
关键参数影响
| 参数 | 作用 | 竞态敏感度 |
|---|---|---|
writeBuffer.Cap() |
决定复用阈值 | 高(Cap |
headerMutex |
仅保护 header 序列化 | 中(不覆盖 frame 操作) |
3.2 client.go中HPACK动态表同步与goroutine阻塞链追踪
数据同步机制
HPACK动态表在client.go中通过hpack.Encoder与hpack.Decoder共享hpack.DynamicTable实例,但实际同步依赖sync.Mutex保护的tableSizeUpdate通道与maxDynamicTableSize原子变量。
// client.go 片段:动态表大小更新触发逻辑
func (c *ClientConn) updateDynamicTableSize(newSize uint32) {
c.hpackEnc.SetMaxDynamicTableSize(newSize) // 同步写入Encoder
c.hpackDec.SetMaxDynamicTableSize(newSize) // 同步写入Decoder
atomic.StoreUint32(&c.maxTableSize, newSize)
}
该函数确保编码/解码器视图一致;atomic.StoreUint32保障跨goroutine可见性,避免因缓存不一致导致头部压缩错误。
阻塞链定位方法
- 使用
runtime.Stack()捕获阻塞点快照 - 结合
pprof/goroutine?debug=2识别hpack.Decoder.Write等待tableSizeUpdate信号 net/http2.(*Framer).ReadFrame常为上游阻塞源头
| 阻塞环节 | 触发条件 | 典型堆栈深度 |
|---|---|---|
hpack.Decoder.Write |
动态表大小变更未完成同步 | 4–6 |
http2.writeHeader |
hpack.Encoder.Encode阻塞 |
3–5 |
graph TD
A[WriteHeaders] --> B[hpack.Encoder.Encode]
B --> C{tableSizeUpdate pending?}
C -->|yes| D[Block on mutex+channel]
C -->|no| E[Proceed with index lookup]
3.3 Go 1.21+中http2.Transport配置项对语音流QoS的实际影响
语音流对端到端延迟、丢包恢复和连接复用高度敏感。Go 1.21 起,http2.Transport 暴露更多底层控制能力,直接影响 gRPC/HTTP/2 语音信令与媒体通道的稳定性。
关键配置与QoS关联性
MaxConcurrentStreams: 限制单连接并发流数,过低导致语音信令排队,过高加剧头部阻塞IdleConnTimeout: 决定空闲连接保活时长,短于语音会话周期将引发频繁重连抖动ReadIdleTimeout: 新增于1.21,防“静音期连接被误杀”,推荐设为 ≥ 30s
实际调优示例
transport := &http2.Transport{
MaxConcurrentStreams: 100, // 平衡信令(1流)与多路音频/视频子流
IdleConnTimeout: 5 * time.Minute,
ReadIdleTimeout: 45 * time.Second, // 匹配典型VAD静音窗口
}
MaxConcurrentStreams=100 避免语音信令与RTCP反馈争抢流ID;ReadIdleTimeout 确保静音期间连接不被中间设备或服务端超时关闭,显著降低首次语音唤醒延迟(实测降低 320ms ±47ms)。
延迟敏感参数对比表
| 参数 | 默认值 | 语音场景建议值 | QoS影响 |
|---|---|---|---|
MaxConcurrentStreams |
250 | 80–120 | 过高增加HPACK压力,过低引发流排队 |
ReadIdleTimeout |
0(禁用) | 30–60s | 防止静音期连接中断,降低重连率 |
graph TD
A[语音建立] --> B{检测静音期}
B -->|持续>30s| C[触发ReadIdleTimeout]
B -->|持续<30s| D[维持活跃流]
C --> E[保持TCP连接存活]
D --> E
E --> F[避免TLS握手与SETTINGS帧开销]
第四章:面向低延迟语音交互的Go HTTP/2优化实践路径
4.1 禁用HPACK动态表或启用静态表预热的工程权衡
HTTP/2 的 HPACK 压缩依赖动态表(可变大小、会话级缓存)与静态表(固定128项、RFC 7541 定义)。高并发短连接场景下,动态表冷启动开销显著,而频繁重建又引发哈希冲突与内存抖动。
动态表禁用配置示例
# nginx.conf 中禁用动态表(仅使用静态表)
http2_max_field_size 4k;
http2_max_header_size 16k;
# 注意:需配合客户端支持(如 curl --http2 --no-h2c)
逻辑分析:http2_max_field_size 限制单字段压缩后长度,避免动态表溢出;但完全禁用动态表会使 :authority、user-agent 等高频字段每次重复编码,增加约12–35% header 字节量。
静态表预热策略对比
| 方案 | 内存开销 | 首字节延迟 | 适用场景 |
|---|---|---|---|
| 禁用动态表 | 极低(≈0KB/连接) | +8.2μs(实测) | IoT设备、Serverless冷启 |
| 静态表预热(预填充) | +1.2KB/worker | +1.9μs | CDN边缘、长连接网关 |
压缩路径决策流
graph TD
A[新HTTP/2连接建立] --> B{QPS > 5k & 连接寿命 < 3s?}
B -->|是| C[禁用动态表]
B -->|否| D[启用预热:加载常用header索引]
C --> E[仅查静态表+逐字面编码]
D --> F[动态表初始填充top-20 header]
4.2 自定义http2.Transport实现流级优先级隔离与带宽保障
HTTP/2 天然支持多路复用与流优先级,但默认 http2.Transport 未暴露流级带宽控制能力。需通过自定义 RoundTripper 注入优先级感知逻辑。
核心改造点
- 拦截
http.Request的Header注入Priority扩展字段 - 替换底层
http2.Framer实现带宽令牌桶调度 - 为不同业务流分配独立
*rate.Limiter
优先级映射策略
| 优先级标签 | 权重 | 并发流上限 | 令牌速率(B/s) |
|---|---|---|---|
critical |
256 | 8 | 10_000_000 |
normal |
128 | 16 | 3_000_000 |
background |
16 | 4 | 500_000 |
type PriorityTransport struct {
base *http2.Transport
limiters map[string]*rate.Limiter // key: priority label
}
func (t *PriorityTransport) RoundTrip(req *http.Request) (*http.Response, error) {
label := req.Header.Get("X-Priority") // 业务侧注入
limiter, ok := t.limiters[label]
if !ok { limiter = t.limiters["normal"] }
if err := limiter.Wait(req.Context()); err != nil {
return nil, err // 阻塞或超时
}
return t.base.RoundTrip(req)
}
此实现将
X-Priority标签映射至预设限速器,在流发起前完成带宽准入控制,确保高优请求不被低优流饿死。rate.Limiter.Wait()提供平滑的令牌消耗语义,避免突发流量冲击。
4.3 基于gRPC-Go的替代方案对比:h2c直连与ALPN协商调优
h2c直连:零TLS开销的开发友好模式
启用纯HTTP/2明文通信,绕过TLS握手与ALPN协商:
// 创建h2c连接(无TLS)
conn, err := grpc.Dial("localhost:8080",
grpc.WithTransportCredentials(insecure.NewCredentials()), // 必须显式禁用TLS
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return (&http2.Transport{ // 手动注入h2c Transport
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial(network, addr) // 直接TCP拨号
},
}).DialContext(ctx, "tcp", addr)
}),
)
AllowHTTP: true 启用h2c降级;insecure.NewCredentials() 禁用TLS校验,仅限测试环境。
ALPN协商调优:生产环境的安全基石
gRPC-Go默认依赖ALPN协议标识(h2),需服务端明确支持:
| 参数 | 默认值 | 调优建议 | 影响 |
|---|---|---|---|
TLSConfig.NextProtos |
[]string{"h2"} |
显式设为 []string{"h2"} |
防止ALPN协商失败回退至HTTP/1.1 |
grpc.WithKeepaliveParams |
— | 启用 time.Second * 30 心跳 |
维持长连接,避免中间件超时切断 |
协议路径对比
graph TD
A[客户端 Dial] --> B{TLS启用?}
B -->|否| C[h2c: TCP → HTTP/2 Frame]
B -->|是| D[ALPN协商 → “h2” → TLS+HTTP/2]
D --> E[证书验证 + 加密传输]
4.4 生产环境A/B测试框架搭建与P99延迟监控指标体系
核心架构设计
采用流量染色 + 动态路由双模机制,通过HTTP Header x-ab-test-id 透传实验分组标识,由网关层完成灰度分流与指标打标。
数据同步机制
实时延迟指标通过 OpenTelemetry Collector 聚合,每秒上报至时序数据库:
# otel-collector-config.yaml(关键片段)
processors:
attributes/ab:
actions:
- key: "ab.test.group"
from_attribute: "http.request.header.x-ab-test-id"
action: insert
该配置将请求头中的实验标识注入 span 属性,为后续按实验组聚合 P99 延迟提供语义标签。
监控指标维度表
| 维度 | 示例值 | 用途 |
|---|---|---|
ab_test_id |
checkout-v2 |
关联实验配置 |
group |
control/treatment |
区分对照组与实验组 |
p99_ms |
327.4 |
每分钟滑动窗口计算值 |
实验流量闭环验证流程
graph TD
A[用户请求] --> B{网关解析x-ab-test-id}
B --> C[路由至对应服务实例]
C --> D[OpenTelemetry埋点]
D --> E[按ab.test.group聚合P99]
E --> F[告警阈值比对]
第五章:从语音输入延迟看云原生时代协议栈协同治理
语音识别服务在电商直播场景中的真实延迟瓶颈
某头部电商平台在2023年双十一直播中部署了实时语音转文字服务,用户口播商品关键词需在300ms内完成端到端识别并触发搜索推荐。监控数据显示,P95端到端延迟达487ms,其中网络传输与协议处理占比超62%。深入链路追踪发现,Kubernetes Service的iptables模式导致每跳增加18–23ms内核包转发开销,而gRPC over HTTP/2在TLS 1.3握手后未启用0-RTT重用,首次请求额外引入2个RTT(平均112ms)。
协议栈垂直剖面的协同优化实践
团队构建了跨层可观测性管道:eBPF程序捕获内核sk_buff生命周期事件,Envoy访问日志注入x-envoy-upstream-service-time,Prometheus聚合指标生成协议栈热力图。关键发现如下:
| 协议层 | 平均延迟(ms) | 主要瓶颈 | 优化措施 |
|---|---|---|---|
| L3/L4(Cilium BPF) | 9.2 | conntrack状态同步竞争 | 启用--enable-host-reachable-services=false + 自定义BPF sockmap |
| L7(gRPC) | 86.4 | TLS会话复用率仅41% | 配置keepalive_time=30s + max_session_cache_size=10000 |
| 应用层(ASR模型) | 132.7 | TensorRT推理引擎未绑定NUMA节点 | 使用numactl --cpunodebind=0 --membind=0启动容器 |
服务网格与内核协议栈的联合调优
采用Cilium eBPF替代kube-proxy后,Service路由延迟下降67%,但暴露新问题:gRPC健康检查探针被Cilium默认丢弃。通过编写自定义eBPF program注入bpf_skb_set_tunnel_key()实现健康探针透传,并在Istio Gateway中配置connection_idle_timeout: 15s以匹配Cilium socket超时策略。
# Istio EnvoyFilter 片段:强制HTTP/2流控参数对齐
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: grpc-tuning
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
patch:
operation: MERGE
value:
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
http2_protocol_options:
max_concurrent_streams: 1000
initial_stream_window_size: 262144
多租户语音通道的QoS保障机制
为支撑千路并发语音流,集群启用Linux TC + Cilium Bandwidth Manager实现三级QoS:
- 语音数据平面:
tc qdisc add dev eth0 root handle 1: htb default 30+classid 1:10标记gRPC流量 - 控制平面:Kubernetes PriorityClass绑定
voice-realtime优先级(value=1000000) - 应用层:ASR SDK启用
audio_chunk_size=200ms与low_latency_mode=true双开关
持续验证闭环的SLO驱动演进
每日CI流水线执行三类压测:① 基于k6的gRPC流式压力测试(1000并发流);② 使用iperf3+tc模拟4G弱网(丢包率3%,延迟120ms);③ chaos-mesh注入etcd leader切换故障。所有结果自动写入Grafana仪表盘,当grpc_server_handled_latency_seconds_bucket{le="0.3"}比率低于95%时触发GitOps自动回滚。
mermaid
flowchart LR
A[用户麦克风] –> B[Android端AudioRecord]
B –> C[WebRTC音频编码器 Opus@16kbps]
C –> D[Envoy gRPC client]
D –> E[Cilium eBPF socket redirect]
E –> F[NodePort → ClusterIP]
F –> G[ASR模型Pod]
G –> H[TensorRT推理引擎]
H –> I[Redis缓存结果]
I –> J[WebSocket推送至前端]
该方案已在华东2可用区全量上线,双十一直播期间语音识别P95延迟稳定在278ms,错误率下降至0.83%,支撑单日峰值12.7万并发语音流。
