ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

TCP拥塞控制深度解析:从慢启动到BBR的算法演进与调优实践

TCP拥塞控制深度解析:从慢启动到BBR的算法演进与调优实践 说实话搞了这么多年网络相关的项目我越来越觉得TCP拥塞控制是被低估的一个知识点。很多人能背出三次握手、四次挥手但一问到为什么文件传输会忽快忽慢为什么带宽明明很大下载速度却不达标为什么有时候延迟会突然飙到几百毫秒就开始含糊了。这些问题的背后几乎都能追溯到拥塞控制。这次研究的初衷就是想把这些看似分散的现象串联起来从底层逻辑把拥塞控制彻底捋清楚。1. 先搞定基础认知拥塞控制到底在解决什么问题1.1 网络拥塞的本质是资源竞争要理解拥塞控制得先接受一个事实网络不是一条专属管道而是无数数据流共享的交换网络。你在家看电影时邻居也在刷视频楼下还在开视频会议所有流量都在争抢同一台路由器、同一条出口带宽。这个场景放在生活中就很好理解一条高速公路的收费站只有四个窗口但高峰期来了四百辆车结果就是所有车都堵在收费站前面谁也动不了。网络也一样当大量数据包同时涌入一台路由器路由器的缓冲区Buffer装不下只能把多余的数据包丢掉。问题来了TCP是可靠传输协议丢包就必须重传。如果发送方不停地发送、接收方不停重传网络压力会越来越大丢包越来越严重最终形成拥塞崩溃——整个网络瘫在原地谁的数据都发不出去。1.2 拥塞控制是端到端的妥协方案解决网络拥塞有两种思路一种是让路由器参与告诉发送方我现在很忙你慢点发这叫显式拥塞通知ECN另一种是发送方自己猜根据丢包和延迟的变化推断网络状态这叫端到端拥塞控制。TCP实际采用的主要是后者。这背后有个很现实的理由TCP设计之初是给互联网用的互联网由无数异构网络组成你不可能要求每一台路由器都配合你。最稳妥的方案就是发送方自己管好自己——通过观察丢包和往返时间RTT推测网络有多忙然后动态调整发送速率。这就像你在高速上开车没人告诉你前面堵不堵你只能通过踩刹车的频率相当于丢包和到达时间相当于RTT来判断路况。踩刹车的频率越高说明前面越堵你就得主动减速。1.3 拥塞控制和流量控制的界限初学者最容易混淆的两个概念是拥塞控制和流量控制。这里我习惯用一个类比区分流量控制是接收方怕你发太快我处理不过来所以告诉你窗口大小它管的是发送方和接收方之间的供需关系。拥塞控制是网络怕你发太快我转发不过来它管的是发送方和整条路径上的承载能力。接收方处理不过来消息是透明的——接收窗口rwnd直接写在TCP头里发送方随时能读到。网络处理不过来的信号是隐性的——只能靠丢包和延迟去猜。我见过不少人调优TCP一上来就改接收缓冲区大小结果该卡还是卡原因就是没有意识到瓶颈可能在中间链路而不是在接收端。你只有同时看懂这两个窗口才能真正定位问题。2. 四大核心机制拆解慢启动、拥塞避免、快速重传、快速恢复拥塞控制的实现机制说到底是围绕两个核心状态变量的博弈拥塞窗口cwndCongestion Window发送方自己维护的一个变量表示当前网络允许我一次性发送多少数据。慢启动阈值ssthreshSlow Start Threshold慢启动和拥塞避免阶段的分界线。发送方实际能发送的数据量是min(cwnd, rwnd)也就是看网络的脸色也看接收方的脸色取两者较小值。2.1 慢启动为什么叫慢实际却是指数增长慢启动是TCP刚建立连接时的第一阶段。名字里带着慢很多人以为它是慢慢往上涨的——恰恰相反它涨得飞快。刚建立连接时发送方不知道这条路径的承载能力是多少。如果一上来就火力全开很可能直接把中间路由器打爆。所以TCP的做法是先发一小部分数据试探确认网络没问题再翻倍增加。初始cwnd 10个MSS约14KBMSS通常为1460字节 每收到一个ACKcwnd增加1个MSS 第一个RTT后cwnd 20 第二个RTT后cwnd 40 第三个RTT后cwnd 80看出来了吗每经过一个往返时间cwnd就翻一倍是指数增长。这个速度非常夸张。那为什么叫慢启动呢因为和传统的线性增长相比它从一个很小的窗口开始试探所以叫slow start。这个设计的意义在于用指数增长快速探测网络的可用带宽但又留了缓冲时间让发送方不至于瞬间淹没网络。我实测过在一个RTT为100ms的链路上传输大文件从初始窗口涨到几百KB只需要几个RTT确实非常快。问题也恰恰出在快上如果不设上限指数增长很快就会打爆网络所以有了ssthresh和拥塞避免。2.2 拥塞避免从指数到线性的急刹车当cwnd增长到ssthresh时TCP进入拥塞避免阶段。这一阶段的策略完全变了每收到一个ACKcwnd增加 (1/cwnd) 个MSS 等价效果是每经过一个RTTcwnd只增加1个MSS从指数增长切换到线性增长目的很明确在接近网络容量上限时放慢逼近速度避免突然拥塞。这里有个很容易栽坑的点很多人以为ssthresh是固定值实际上它会在发生拥塞时被动态调整。传统Reno算法中检测到丢包后ssthresh会被设置为当前cwnd的一半然后重新进入慢启动或快速恢复。到底应该从哪里开始减、减多少各个算法有不同思路这也就引出了后面提到的算法演进。2.3 丢包检测手段超时重传和快速重传TCP识别拥塞的最核心信号是丢包。而丢包的发现方式有两条路超时重传。发送方发了一个数据段启动定时器。如果在规定时间内没收到ACK就认为包丢了或者接收方异常了。RTO重传超时时间不是固定的它是根据历史RTT采样动态计算的通常要略大于平均RTT加上一定的抖动余量。超时重传是很重的惩罚机制因为超时意味着链路可能严重拥塞反应非常剧烈。快速重传。另一种更优雅的丢包发现方式。收到乱序数据时TCP接收方会立即回复一个重复ACK告诉发送方我还在等某个序号的数据。当发送方连续收到3个重复ACK时说明某个包大概率丢了——但传输链路还是通的因为后续数据还在源源不断回来。快速重传的精髓是不等超时靠重复ACK提前触发重传降低等待时间。我把这两种机制做成了表格对比方便你理解对比维度超时重传快速重传触发条件RTO超时未收到ACK连续收到3个重复ACK链路状态推断可能严重拥塞仍有数据在传输拥塞较轻反应强度重传且大降cwnd重传且将cwnd减半适用场景网络异常、丢包严重单包乱序、轻度丢包2.4 快速恢复丢包后的快速降档快速恢复和快速重传是配对的。当快速重传触发后TCP不会笨拙地回到慢启动重新探测而是直接进入快速恢复状态。快速恢复的核心逻辑是既然链路还能传数据说明拥塞程度有限不需要全盘归零。做法是ssthresh cwnd / 2 cwnd ssthresh 3 (已收到的3个重复ACK释放的报文段数) 后续每收到一个重复ACKcwnd cwnd 1 收到新ACK确认了新数据cwnd回落到ssthresh这里体现的设计哲学很有意思把网络上还在飞行的数据包估算出来不因为一次丢包就全面撤退而是通过降档的方式平滑过渡。这就像开车遇到前方拥堵你减速到60而不是直接刹停因为你知道拥堵路段马上就可以匀速通过了。3. 算法演进内幕从Tahoe到BBR为什么不能一套算法打天下3.1 早期算法以丢包为拥塞信号的时代先理一下TCP拥塞控制的演进路线你会发现每个算法都是对前一个算法的补丁。TCP Tahoe1988年。最早的拥塞控制算法只有慢启动、拥塞避免、快速重传。问题是太保守无论遇到超时还是快速重传一律回到慢启动重新来。这在当时网络带宽低、链路质量差的环境下没问题但随着带宽增加它的效率问题暴露了——动不动就归零传输速率波动极大。TCP Reno1990年。在Tahoe基础上增加了快速恢复解决了快速重传导致的全盘归零问题。Reno成为很长一段时间的默认算法但它在多条丢包同时发生时表现很差——当同一个窗口内的多个数据包丢失快速恢复会反复触发性能急剧劣化。TCP NewReno1996年。改进了对多丢包场景的处理进入快速恢复后只有当所有丢失的数据都被确认后才退出恢复状态避免Reno在恢复过程中的误判。这个改进在丢包率高的链路上效果显著。3.2 丢包判拥塞的致命弱点上面这些算法的共同思路都是丢包 拥塞这在早期的低速网络上是成立的——缓冲区满了网络扛不住了包才会丢。但到了高带宽大缓冲时代这个假设开始失效。现代路由器的缓冲区越来越大可以吸收大量突发流量而不丢包但代价是延迟飙升。这个现象叫缓冲区膨胀Bufferbloat。数据包在路由器缓冲区里排队等了好几百毫秒延迟就上去了但网络没有丢包发送方也感知不到拥塞。问题就在这里基于丢包的算法在高缓冲设备面前会持续增长cwnd直到缓冲区被塞满然后才开始丢包并减半。整个过程伴随着延迟剧烈抖动和带宽利用不足。3.3 BIC/CUBIC高带宽长距离网络的破局针对高带宽长距离网络业界俗称长肥网络研究者提出了BIC二分增长和CUBIC算法。BIC的思路是二分查找最优窗口值如果cwnd在某个位置丢包了就把这个位置标记为上限然后在上限和下限之间折中试探。CUBIC则是把窗口增长函数换成了一个三次函数曲线在接近上次丢包点时增长缓慢远离丢包点时增长加快。CUBIC的巧妙之处在于它的增长速率只依赖距离上次拥塞事件的时间而不依赖RTT。这就解决了RTT不公平问题——以前的算法中RTT短的连接增长快会抢占更多带宽CUBIC让不同RTT的流在竞争带宽时更加公平。Linux内核从2.6.19开始把CUBIC设为默认拥塞控制算法现在大多数Linux发行版的默认设置依旧是它这背后是有充分理由的——它在各种网络环境下的表现都非常均衡。3.4 BBR不再依赖丢包的模型化方案2016年Google团队发布了BBR拥塞控制算法。它彻底颠覆了此前的思路不再把丢包当作拥塞信号而是通过测量网络带宽BtlBw和最小RTT来建立一个网络模型。BBR的核心是三步实时测量带宽的极大值和RTT的极小值然后根据带宽延迟积BDP确定目标发送速率定期探测是否有更多带宽可用。这里的计算公式很关键BDP 带宽(Mbps) × RTT(ms) / 8举个例子一条100Mbps的链路RTT是20ms那么BDP 100 × 20 / 8 250KB。这意味着发送方只要维持250KB的在途数据就能占满这条链路再往上加数据只会增加排队延迟不会提升吞吐。BBR的厉害之处在于它适应了真实世界的网络特征——丢包不一定是拥塞可能是无线链路的瞬时干扰也可能是中间设备的限速策略。在链路丢包率高但不太拥塞的环境下BBR能比CUBIC保持更高的吞吐量。我在真实网络环境测试过这两个算法的对比。在丢包率2%的链路上CUBIC的吞吐量明显下降而BBR几乎不受影响。但在完全无丢包的实验室环境中两者差距不大。这说明算法选型不能盲目追新得看场景。3.5 算法对比速查表我把主流算法的核心差异整理成了一张表方便后续选型参考算法拥塞信号核心策略优势/适用场景劣势Tahoe丢包重传后回到慢启动稳健适合极度不可靠链路吞吐波动大效率低Reno丢包快速重传快速恢复经典实现基础教科书必讲多丢包性能差NewReno丢包改进快速恢复多丢包场景更稳定带宽增长较慢CUBIC丢包三次函数增长链路质量好时吞吐高Linux默认大缓冲时延迟高BBR带宽延迟模型驱动周期性探测高丢包、高延迟链路吞吐更好对纯丢包信号不敏感实现在不断演进4. 实战观察与分析用Linux和Wireshark看拥塞控制的行为研究拥塞控制纸上谈兵是不够的。我在Linux环境里搭配Wireshark实录了一套观测方法你可以照着操作看到cwnd和ssthresh的变化过程。4.1 查看当前系统的拥塞控制算法Linux系统里查看和修改拥塞控制算法很简单。先看看当前系统支持哪些算法当前用的哪个# 查看当前生效的拥塞控制算法 sysctl net.ipv4.tcp_congestion_control # 查看内核支持的拥塞控制算法 sysctl net.ipv4.tcp_available_congestion_control # 查看模块挂了哪些算法 lsmod | grep tcp我这台Linux服务器的输出是net.ipv4.tcp_congestion_control cubic net.ipv4.tcp_available_congestion_control reno cubic如果想用BBR需要先确认内核支持然后加载模块并设置# 开启BBR内核4.9才支持 modprobe tcp_bbr sysctl net.ipv4.tcp_congestion_controlbbr # 持久化配置 echo net.ipv4.tcp_congestion_controlbbr /etc/sysctl.conf sysctl -p设置后再次查看确认已切换到BBR。在我的环境中BBR在两台相距1500公里的服务器之间传文件时带宽利用率比默认CUBIC高了不少尤其是在有一定丢包率的跨网链路上。4.2 实时观察cwnd和ssthresh想直接看到cwnd和ssthresh的变化ss命令是最方便的工具# 查看所有TCP连接的拥塞状态 ss -tin # 过滤特定端口的连接 ss -tin sport :8080 # 查看某个IP连接状态 ss -tin dst 192.168.1.100输出中能看到类似这样的关键字段cubic rto:204 rtt:21.5/7.5 ato:40 mss:1448 pmtu:1500 rcvmss:536 advmss:1448 cwnd:64 ssthresh:50 bytes_acked:1048389 bytes_received:16442 segs_out:764 segs_in:88cwnd:64表示当前拥塞窗口是64个MSS约92KBssthresh:50表示慢启动阈值在50个MSS附近。rtt表示当前平滑RTT是21.5ms。我通常会在传输大文件时每隔一秒执行一次这个命令观察cwnd的变化曲线判断当前处于慢启动阶段还是拥塞避免阶段。如果cwnd在快速翻倍增长说明链路还有大量余量如果cwnd在25附近徘徊且频繁出现重传大概率链路质量不佳。4.3 用Wireshark抓包还原拥塞行为Wireshark是更直观地观察拥塞控制行为的利器而且不用修改任何内核参数。抓包时重点看几个指标分析丢包和重传。Wireshark的着色规则会自动标出TCP重传包黑色或红色背景配合tcp.analysis.retransmission过滤条件可以快速定位重传事件。在TCP协议详情里的[TCP Retransmission]标注会明确告诉你这是第几次重传。分析RTT变化。Wireshark的TCP Stream Graph里面有一个Round Trip Time Graph能把每条流的RTT变化实时画出来。当网络开始拥塞RTT曲线会像呼吸一样起伏——逐步升高然后因为重传而回落。分析接收窗口。TCP头里的Window字段代表接收窗口。注意区分这是接收方声明的可用空间和发送方的cwnd不是一回事。我建议抓包时带上-w参数保存文件等传完文件后再慢慢分析不要实时盯着终端tcpdump -i eth0 host 192.168.1.100 and port 8080 -w /tmp/tcp-congestion.pcap4.4 制造拥塞观察算法响应想深入理解拥塞控制最好亲自动手制造一次拥塞事件观察算法的响应过程。方法很简单开两个大流量传输同时争抢带宽# 终端一大文件下载 wget http://大文件服务器/大文件.iso -O /dev/null # 终端二并发压测 for i in $(seq 1 10); do wget http://大文件服务器/大文件.iso -O /dev/null done # 同时观察流量特征 ss -tin | grep cwnd多路并发传输开始后链路很快进入拥塞状态你会看到cwnd从高位骤降同时ss输出的retrans重传计数开始增长。这个过程的实时变化比任何教科书描述都直观。我录制过一次完整过程初始时单条流cwnd稳定在500左右叠加十条并发流后单流cwnd直接掉到20以下同时RTT从15ms飙到120ms典型的拥塞饱和状态。等并发任务结束后cwnd又缓慢爬回高位。这一整个爬升-饱和-崩溃-恢复的过程就是TCP拥塞控制算法工作的全貌。5. 常见网络故障排查那些年和TCP相关的疑难杂症研究拥塞控制之后再看日常运维中遇到的各种TCP报错思路会清晰很多。很多问题表面上是连接失败追根溯源都和拥塞窗口、网络状态有关。我挑了实际工作中最常遇到的几类问题整理了排查思路。5.1 bind报错端口被占用怎么破error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket addre这个报错常出现在本地开发调试时。根本原因是端口已被其他进程占用系统不允许同一地址和端口组合被绑定两次。但在拥塞控制语境下这类问题容易发生在大量短连接快速重启的服务上——某个连接还处于TIME_WAIT状态时新进程立刻去bind同一端口就会触发这个错误。排查和解决办法# 查看端口被哪个进程占用 netstat -tulpn | grep 11434 # 查看TIME_WAIT状态连接 ss -tan state time-wait | wc -l # 如果确认需要快速回收TIME_WAIT可考虑调整内核参数 sysctl net.ipv4.tcp_tw_reuse1注意tcp_tw_reuse只对主动连接方客户端有效服务端监听端口复用并不可靠。开发调试时最稳妥的解决办法是换一个端口或者等TIME_WAIT计时结束。生产中如果频繁出现TIME_WAIT堆积说明短连接过多建议考虑连接池化。5.2 connection reset by peer为什么连接会被重置场景一服务端主动关闭。后端代码里调用abort或未正常关闭socket内核会发RST包。curl报错信息就是curl: (35) tcp connection reset by peer。排查思路是先看服务端日志确认是否有异常退出其次确认是否有防火墙/安全组策略限制。场景二客户端发送了非法TCP报文。比如发送了不属于当前连接状态机的数据内核直接以RST响应。场景三监听队列已满。服务端accept队列被占满新增连接请求无法进入队列内核可能会直接丢弃SYN包而长时间的SYN重传也可能触发连接失败。排查这类问题时带上抓包工具最有效tcpdump -i eth0 tcp port 8080 -w reset.pcap重点看RST包出现时的TCP头标志位和窗口大小。RST包通常会携带触发原因——虽然里面没有文字说明但通过比对同一时间段的连接状态往往能还原出因果链。5.3 TCP connect超时三次握手的假死连接超时的本质是SYN包发出后一直没收到SYNACK响应。原因常见有几类防火墙丢弃SYN最常见的静默丢包发出去的SYN石沉大海。网络层连通正常但TCP层不通。服务器accept队列溢出服务器繁忙SYN调度队列填满内核没有精力回复新连接。中间链路拥塞严重导致SYN重传失效SYN重传次数达到上限后连接建立失败。排查时先确认网络连通性然后抓包看SYN是否到达服务器# 抓取所有到目标端口的SYN包 tcpdump -i eth0 tcp port 8080 and tcp[tcpflags] tcp-syn ! 0这里有一个实际经验某次线上故障应用报connect超时但ping延迟正常。抓包后发现SYN包被防火墙静默丢弃服务器根本没有收到SYN。排查路径从服务器负载转到防火墙规则问题五分钟内解决。5.4 拥塞引发的性能问题带宽占不满和延迟抖动除了连接建立失败拥塞控制导致的性能问题更加隐蔽也更常见。我总结了两种典型带宽占不满。链路明明是千兆但传输速率只有几十兆。此时优先检查链路RTT和丢包率。如果丢包率为0但RTT很高接近100ms以上很可能是链路处于长肥网络场景cwnd增长到BDP所需的大小还需要时间而应用本身的连接时长又太短。解决办法包括调大初始窗口、使用连接池复用连接或改用BBR。延迟抖动严重。传输过程中RTT从20ms震荡到200ms伴随轻微丢包。这是经典的缓冲区膨胀表现。优先考虑在路由设备和终端设备上启用主动队列管理AQM如CoDel或PIE主动丢一些包来告诉发送端减少发送量避免缓冲区被填满。这类优化一定要先量化再动手。我习惯在调整前后各记录一组数据指标调整前调整后说明平均吞吐量35 Mbps97 Mbps链路100Mbps改善明显平均RTT68 ms21 ms排队延迟消失丢包率1.8%0.1%主动队列管理起效6. 参数调优实录与内核参数速查6.1 最值得关注的几个内核参数Linux内核TCP协议栈有几十个可调参数但实际调优时不是每个都值得碰。我梳理了几个优先级最高、效果最直接的参数# 拥塞控制算法选择 net.ipv4.tcp_congestion_control cubic # 启用窗口缩放大BDP链路必须开启 net.ipv4.tcp_window_scaling 1 # 接收缓冲区和发送缓冲区自动调节 net.ipv4.tcp_moderate_rcvbuf 1 net.ipv4.tcp_rmem 4096 65536 16777216 net.ipv4.tcp_wmem 4096 65536 16777216 # 慢启动空闲重置建议关闭避免空闲后带宽探测退化 net.ipv4.tcp_slow_start_after_idle 0关于tcp_slow_start_after_idle多说两句。默认情况下一条TCP连接空闲一段时间后内核会把cwnd重置为初始值连接要重新经历慢启动过程才能恢复到原有带宽。对大量短连接和间歇性请求的服务这个重置非常吃亏。我通常建议在长连接服务中关闭它吞吐量能提升20%~40%。6.2 BDP计算和缓冲区设置的实操方法带宽延迟积BDP是网络调优中避不开的核心指标。计算方法是BDP 带宽 × 往返时间。它代表一条链路上最多同时处于传输中的数据量如果你设置的缓冲区小于BDP发送窗口就会受限于缓冲区大小带宽再大也跑不满。举个例子链路带宽100Mbps RTT20ms BDP 100 × 10^6 × 0.02 / 8 ≈ 250KB假设发送方设置的socket发送缓冲区只有64KB那么无论拥塞控制算法多优秀实际吞吐量上限也只有约25Mbps64KB×8/0.02s。这就是为什么大带宽场景必须同步调整系统缓冲区参数的原因。Linux的自动调节机制在4.0以后的内核中已经做得相当好大多数情况下不需要手工干预tcp_rmem和tcp_wmem。但如果你的业务非常依赖高吞吐长连接手工把最大值调大是必要的sysctl -w net.ipv4.tcp_rmem4096 87380 25165824 sysctl -w net.ipv4.tcp_wmem4096 65536 2516582425165824即24MB足以支撑约1Gbps带宽、200ms RTT的链路BDP 25MB。调整完记得用sysctl -p刷新并检查。6.3 调整TCP初始窗口Linux内核从3.2版本开始支持通过ip route命令调整初始窗口大小这个技巧在短连接场景特别好用。比如一个Web服务平均请求只传几十KB数据初始窗口越大建连后需要经历慢启动的RTT数就越少# 查看现有路由的初始窗口设置 ip route show # 将初始窗口设为10个MSS约14KB ip route change default via 192.168.1.1 dev eth0 initcwnd 10 initrwnd 10调大初始窗口不是没有代价的。如果服务端网络带宽很低过大的初始窗口会在建连瞬间产生突发流量。一般建议局域网环境可以设到10跨公网链路保守设到6~8即可。6.4 关于接收窗口缩放和TCP timestampstcp_window_scaling默认开启但要注意它依赖TCP选项协商。两边都不支持时发送窗口最大只能到64KB。这意味着高带宽长延迟链路如果不支持窗口缩放性能会被压得死死的。TCP timestamps时间戳选项平时不起眼但对丢包重传很有帮助——它能更精确地计算RTT还能用来区分乱序包和重传包。代价是每包增加12字节头开销。对低速链路可以接受对极高速链路需要权衡。检测连接是否启用了timestamps和窗口缩放用ss -tin可以看到ws: cwnd:64 ssthresh:50 ... ts: 返回的时间戳选项信息7. 最后的工程建议研究拥塞控制给我最大的收获是看网络问题的视角变了。以前遇到性能问题第一反应是查带宽够不够、配置对不对现在会先评估链路本身的RTT、丢包特征和瓶颈位置再决定选什么算法、调什么参数。这种从链路特征出发的思路远比盲目套用某套调优参数可靠。针对不同的业务场景我的选型经验是多花费几行代码验证而不是照搬别人的优化配置内网低延迟环境默认CUBIC已经够好没必要折腾跨运营商、无线链路等有丢包和延迟波动的场景BBR改善显著数据中心内部短流极多时关注DCTCP这类专门为数据中心设计的方案会更对路。最后再留一个自测题目如果你服务器返回给客户端的带宽是100MbpsRTT是40ms那么理论上客户端至少要设置多大的接收缓冲区才能跑满带宽按BDP公式算一算你会发现答案和直觉可能不太一样。网络调优的乐趣往往就藏在这些看似简单的计算里。
返回列表