QUIC之拥塞控制和0-RTT连接建立

翻译、编辑:Alex

技术审校:刘连响

本文来自_Smashing Magazine_,原文链接:

https://www.smashingmagazine.com/2021/08/http3-performance-improvements-part2/

Robin 讲 HTTP/3#003#

速览: 在经过五年的开发之后,新的 HTTP/3 协议终于接近尾声。让我们一起深入了解 HTTP/3 的性能提升、拥塞控制、队头阻塞和 0-RTT 连接建立。

欢迎回到 HTTP/3 协议系列文章。在第一部分,我们了解了为什么我们需要 HTTP/3(从 0 到 1 讲解 HTTP/3)、底层 QUIC 协议以及它的主要新特性。

在第二部分,我们将聚焦 QUIC 和 HTTP/3 为网页加载带来的性能提升。不过,我们对这些新性能实际产生的预期影响持保留意见。

正如我们将看到的,QUIC 和 HTTP/3 确实具备提升网络性能的巨大潜力,但主要针对网速较慢的用户。如果你的平均访客使用的是较快的有线或者蜂窝网络,那他们很可能就不会从新的协议中获益太多。不过请注意,通常即使在上行链路很快的国家或地区,网速最慢的 1% 甚至是 10% 用户(即所谓的第 99 或者第 90 个百分位)依然很可能受益良多。这是因为 HTTP/3 和 QUIC 主要帮助处理当今互联网上那些并不常见却潜在影响很大的问题。

虽然第二部分将真正深入的技术内容交给了外部资源,主要解释这些协议对于普通 Web 开发者至关重要的原因,但与第一部分相比,它的技术性还是比较强。

网速入门课

讨论性能和 “速度” 是一件很复杂的事,因为导致网页加载速度 “缓慢” 的潜在原因有很多。我们今天涉及的是网络协议,所以我们将从网络的角度来了解这些原因,而其中最重要的两个便是延迟和带宽。

延迟可简略定义为从 A 点(如客户端)发送数据到 B 点(如服务器)所需的时间。它在物理上受限于光速,或(实际)信号在电线中或者室外的传播速度。这意味着延迟常常取决于物理世界中两点 A 与 B 之间的距离。

在地球上 [1],这意味着通常延迟(概念上)会很小,约在 10~200 毫秒之间。不过这只是单向延迟:对数据包的响应也需要返回。双向延迟通常被称为往返时间(RTT,round-trip time)。

由于拥塞控制等特性(见下文),我们将经常需要相当多的 RTT 来加载一个文件。因此,低于 50ms 的低延迟不断叠加,就会造成很大的延迟。这就是 CDN(content delivery network,内容分发网络)存在的主要原因:为了尽量减少延迟,它们将服务器放在靠近终端用户的物理位置。

带宽可以简略定义为同时发送的数据包数量。这里不太好解释,因为它取决于介质的物理属性(比如,所使用的无线电波的频率)、网络上的用户数量,以及连接不同子网(因为它们通常每秒只能处理一定数量的数据包)的设备。

输送水的管道是常用到的一个比喻:管道的长度是延迟,而宽度就是带宽。然而,在互联网上,我们通常会有很多由多个管道连接起来的长管道,而其中一些要更宽(这就形成了最窄连接处的瓶颈)。因此,A 点与 B 点之间的端到端带宽常常受限于最慢的子网。

虽然在接下来的讲解中并不需要充分理解这些概念,但是如果你想要了解这方面的更多信息,我推荐你阅读 Ilya Grigorik 的_High Performance Browser Networking_一书中介绍延迟和带宽的章节 [2]。

拥塞控制

传输协议如何高效利用网络的全部(物理)带宽(大致来说,就是每秒发送和接收的数据包数量)是性能很重要的一方面。它反过来会影响网页下载资源的速度。有人声称 QUIC 在这方面比 TCP 做得好,但事实并非如此。

“你知道吗?

比如,TCP 连接不会以满带宽的方式发送数据,因为很可能会导致网络过载(或者拥塞)。正如我们之前所说,这是因为每个网络链接每秒只能(物理)处理特定数量的数据。如果给它的数据包太多,它除了丢掉多出的数据包之外别无选择,这就导致了丢包。

我们在第一部分(从 0 到 1 讲解 HTTP/3)讨论过,对于 TCP 这样的可靠协议,恢复丢包的唯一方法就是重传一份新的数据备份(需要一个 RTT)。特别是在高延迟的网络上(比如,超过 50ms RTT 的网络),丢包会严重影响网络性能。

另一个问题是,我们无法提前知道最大带宽将是多少。它通常取决于端到端连接中的瓶颈,但是我们无法预测或知道它的所在。目前互联网也没有任何向端点发送链路容量信号的机制。

除此之外,即使我们知道可用的物理带宽,也并不意味着我们就都能使用。通常几个用户同时在网络上活跃,他们每一个都需要公平共享可用带宽。

因此,连接并不知道它可以预先(安全且公平)使用多少带宽,以及带宽会随着其他用户的加入、离开和使用而发生变化。为了解决这个问题,TCP 通过使用一种被称为 “拥塞控制(congestion control)” 的机制来不断尝试发现可用带宽。

在连接开始处,它发送一些数据包(实际为 10~100 个数据包,或者约 14~140KB 的数据)并等待一次往返,直到接受方发送回这些数据包的确认。如果所有数据包都被确认,便意味着网络可以应对此发送速率,我们可以尝试使用更多数据来重复这个过程(实际情况是,通常发送速率每次迭代都会翻倍)。

这样一来,发送速率就会持续增加直到一些数据包无法确认(指丢包和网络拥塞)。第一阶段通常被称为 “慢启动(slow start)”。一旦检测到丢包,TCP 就会降低发送速率,(一段时间之后)再增加发送速率,尽管增量要小得多。这种先减再增的逻辑会在每次丢包后不断重复。最终,这意味着 TCP 会一直努力达到它理想且公平的带宽份额。图 1 说明了这种机制。

图 1 TCP 拥塞控制的简化示例:从 10 个数据包的发送速率开始(改编自 hpbn.co)

上面是对拥塞控制的一个极其简化的解释。在实际中,还要受到很多其他因素的影响,比如 bufferbloat [3]、拥塞导致的 RTT 波动 [4],以及多个并发发送方需要获得公平的带宽份额这一事实 [5]。因此,存在许多不同的拥塞控制算法,今天依然有很多算法被开发出来,但没有一种算法可以在所有场景中都获得最佳表现。

虽然 TCP 的拥塞控制使其更强健,但也意味着需要时间达到最佳发送速率(取决于 RTT 和实际可用带宽)。对网页加载来说,这种慢启动方法也会影响 FCP(first contentful paint,第一次内容绘制)等指标,因为只有少量数据(几十 KB 到几百 KB)可以在最初的几次往返中被传输(你也许听说过将你的关键数据保持在 14KB 以内的建议 [6])。

选择一种更加激进的方式也可以在高带宽和高延迟的网络上获得更好的效果,特别是当你不在意偶尔的丢包时。这里我再次看到了许多关于 QUIC 工作原理的误解。

我们在第一部分(HTTP/3 核心概念之 QUIC)讨论过,理论上丢包(以及相关的队头阻塞)对 QUIC 影响较小,这是因为 QUIC 可以独立处理每个资源的字节流的丢包。此外,QUIC 运行在 UDP 之上。UDP 与 TCP 不同,没有内置拥塞控制特性;同时它允许你以任何速率传输你想传输的数据且不会重传丢失数据。

因此很多相关文章声称 QUIC 也不会使用拥塞控制,而是以比 UDP(依靠队头阻塞消除来处理丢包)高得多的速率开始重新发送数据,这就是 QUIC 比 TCP 快很多的原因。

实际上,事实并非如此 [7]:QUIC 其实使用了与 TCP 非常相似的带宽管理技术。它同样以较低的速率开始,随着时间推移不断提高速率,并将确认作为测量网络容量的关键机制。原因是(除其他原因外):一、为了对 HTTP 等有用,QUIC 需要可靠;二、QUIC 需要对其他 QUIC(和 TCP!)连接公平;三、消除队头阻塞实际上并不能很好地防止丢包(我们将在下文了解)。

不过,这并不表示 QUIC 不能在管理带宽方面比 TCP 更智能(一点)。主要原因是 QUIC 比 TCP 更加灵活,且更易进化。正如我们前文所述,如今仍然在大力开发拥塞控制算法,我们将很有可能(比如)需要调整这些算法使其充分利用 5G [8]。

而 TCP 通常在操作系统内核中实现,这是一个安全却更受限制的环境(对于大多数操作系统来说不是开源的)。因此,通常只有少数几个开发者会调整拥塞逻辑,且进化缓慢。

相比之下,大部分 QUIC 实现目前在 “用户层(我们通常运行 native 应用的地方)” 完成而且开源 [9]—— 明确鼓励更广泛的开发者进行试验(如 Facebook 所示 [10])。

另一个具体的例子是 QUIC 的延迟 ACK 频率(https://datatracker.ietf.org/doc/html/draft-iyengar-quic-delayed-ack-02)拓展提案。虽然默认情况下,QUIC 每收到两个数据包发送一个确认,但此扩展允许端点确认,比如,每 10 个数据包发送一个确认。目前显示在卫星和非常高带宽的网络上,这会带来很大的速度优势,因为降低了重传确认数据包的开销。将这一扩展添加在 TCP 上将耗费相当长的时间才能被采用,但对于 QUIC,部署起来就容易多了。

因此,我们希望随着时间的推移,QUIC 的灵活性能够带来更多的试验并促进更好的拥塞算法的开发,转而能够将它们向后移植(backport)到 TCP 上进而改进它。

“你知道吗?

官方的 QUIC Recovery RFC 9002 [11] 中规定了 NewReno 拥塞控制算法的使用。虽然这种方法很强大,但却有些过时,而且不再在实际中广泛应用。但它为什么会出现在 QUIC RFC 中呢?第一个原因就是,当启动 QUIC 时,NewReno 是最新的已标准化的拥塞控制算法,而其他高级算法(如 BBR 和 CUBIC),要么还没有被标准化,要么在最近 [12] 才成为 RFC。

第二个原因是,NewReno 的设置相对简单。因为需要调整算法来处理 QUIC 与 TCP 之间的差异,所以使用更简单的算法更容易解释这些变化。因此,RFC 9002 应解读为 “如何使拥塞控制算法适用于 QUIC”,而不是 “这些算法应该用于 QUIC”。确实,大部分生产级的 QUIC 实现已经自定义了 CUBIC [13] 和 BBR [14] 的实现。

值得强调的是,拥塞控制算法并不仅限定于 TCP 或 QUIC,任何协议都可以使用它们。希望 QUIC 的进步最终也能够体现在 TCP 协议栈。

“你知道吗?

注意,拥塞控制旁有一个相关概念被称为流量控制 [15](flow control),这两个特性在 TCP 中常被混淆,因为据说它们都使用 “TCP 窗口(TCP window)”,虽然实际上是有两个窗口:拥塞窗口和 TCP 接收窗口。流量控制在我们所感兴趣的页面加载场景中并不常用,所以这里我们略过不提。这里 [16] 提供 [17] 更多深入信息 [18]。

这一切意味着什么?

QUIC 仍然被物理定律所限制,而且需对互联网上的其他发送方友好。这意味着与 TCP 相比,它不会神奇般以快得多的速度下载网站资源。不过,QUIC 的灵活性意味着试验新的拥塞算法将会变得更容易,这将在未来同时改进 TCP 和 QUIC。

0-RTT 连接建立

QUIC 的第二个性能方面是关于在新的连接上发送有用的 HTTP 数据(比如网页资源)前需要多少个 RTT。有人说 QUIC 比 TCP+TLS 快两个甚至三个 RTT,但我们将看到实际上只快一个 RTT。

“你知道吗?

我们在第一部分说过,在 HTTP 请求和响应交换之前,连接通常进行一次(TCP)或者两次(TCP + TLS)握手。为了(比如)加密数据,客户端和服务器都需要知道这些握手交换的初始参数。

在下面图 2 中你可以看到,每个独立的握手至少需要一个 RTT 完成(TCP + TLS 1.3, (b)),有时是两个 RTT(TLS 1.2 和之前 (a))。这很低效,因为在发送第一个 HTTP 请求之前,我们至少需要两个握手往返等待时间(开销),这意味着我们至少要等待三个 RTT 才能收到第一个 HTTP 响应数据(返回红色箭头)。在较慢的网络上,这意味着 100ms~200ms 的开销。

图 2:TCP+TLS vs. QUIC 连接建立

你也许想知道为什么 TCP + TLS 握手无法简单地合并,并在同一个 RTT 中完成。这在概念上也许可以实现(QUIC 正是这么做的),但 TCP 最初并不是这样设计的,因为不管上方有没有 TLS [19],我们都需要能够使用 TCP。换句话说,TCP 在握手期间根本不支持发送非 TCP 内容。虽然曾经想通过 TCP Fast Open 扩展来添加这一特性,但正如我们在第一部分讨论过的,事实证明很难大规模部署 [20]。

幸好 QUIC 在一开始设计时就考虑到了 TLS,因此可以将传输和加密握手合并到一个机制中。这意味着 QUIC 握手仅需一个 RTT 就可以完成,要比 TCP + TLS 1.3 少一个 RTT(见上图 2c)。

你现在也许很困惑,因为你之前很可能读到过:QUIC 要比 TCP 快两个甚至三个 RTT,而不止一个。这是因为大部分文章只考虑了最差的情况(TCP + TLS 1.2, (a)),更不用说现代 TCP + TLS 1.3 也 “仅” 需要两个 RTT(很少显示 (b))。虽然一次往返对提升速度很有帮助,但也没那么厉害。尤其是在网速比较快的情况下(比如,小于 50ms 的 RTT),几乎很难发现其中的差别,显然较慢的网络和较远的服务器连接受益显著。

接下来,你也许想知道我们需要等待握手的原因。我们为什么不在第一次往返时就发送 HTTP 请求?这主要是因为,如果我们这么做了,那么第一个请求在发送时就无法加密,任何线路上的窃听者都会读取数据,这显然会危害到隐私和安全。因此,在发送第一个 HTTP 请求之前,我们需要等待加密握手完成。或者我们真的需要吗?

实际中这里用到了一个巧妙的技巧。我们知道用户经常会在短时间内重新访问网页(第一次访问时)。因此,我们可以在未来使用初始加密连接( initial encrypted connection)引导第二个连接。简单来说,在其生命周期的某个时刻,第一个连接用于在客户端和服务器间安全地传递新的密码参数,然后这些参数再被用于加密第二个连接(从一开始),而无需等待完整的 TLS 握手完成。这种方法被称为 “会话恢复(session resumption)”。

这使得我们有机会进行有效的优化:我们现在可以将第一个 HTTP 请求和 QUIC/TLS 握手一起安全地发送,节省了另一个 RTT!对于 TLS 1.3,这有效地去掉了 TLS 握手等待时间。这种方法也被称为 0-RTT(当然,虽然 HTTP 响应数据开始到达仍需要一次往返)。

会话恢复和 0-RTT 是我经常看到的被人错误解释的 QUIC 特定特性。实际上,它们都是 TLS 特性:已经在 TLS 1.2 中以某种形式存在,并在 TLS 1.3 [21] 中已完全成熟。

换言之,如下图 3 所示,我们也可以通过 TCP(HTTP/2 甚至是 HTTP/1)获得这些特性的性能优势!我们看到即使使用了 0-RTT,QUIC 依然只比运行最佳的 TCP + TLS 1.3 协议栈快一个 RTT。那些称 QUIC 比图 2(a)和图 3(f)快三个 RTT 的说法(如我们所见)有失公允。

图 3:TCP+TLS vs. QUIC 0-RTT 连接建立

最糟糕的地方是,当使用 0-RTT 时,由于安全性,QUIC 甚至无法很好地利用增加的这个 RTT。为了理解这点,我们需要理解 TCP 握手存在的原因之一。首先,它需要客户端在给服务端发送数据之前确定服务端是可用的(在向它发送高层数据之前)。

其次(很关键的一点),它允许服务器确保打开连接的客户端是它们在发送数据前所说的客户端及其位置。如果你回想我们在第一部分(HTTP/3 核心概念之 QUIC)定义连接所使用的四元组,你就会知道客户端主要由 IP 地址识别。这里就是问题所在:IP 地址具有欺骗性!

假设攻击者通过 HTTP over QUIC 0-RTT 请求了一个非常大的文件,但他们使用了虚假的 IP 地址,使它看上去像是来自受害者电脑的 0-RTT 请求。图 4 所示。QUIC 服务器无法检测到是否为虚假 IP 地址,因为这是它从那个客户端看到的第一个(批)数据包。

图 4:在向 QUIC 服务器发送 0-RTT 请求时攻击者可以使用虚假的 IP 地址,这会造成对受害者的放大攻击。

如果服务器随后只是开始将大文件发送回虚假的 IP 地址,那么很可能造成受害者网络带宽过载(尤其当攻击者并行发送许多这样的假请求时)。注意,受害者会丢掉 QUIC 响应,因为它并不期待传入的数据,但这不要紧:它们的网络仍然需要处理数据包!

这被称为反射(reflection)或者放大攻击(amplification attack)[22],它是黑客进行 DDoS 攻击的主要方式。注意,攻击并不会发生在使用 0-RTT over TCP + TLS 时,这是因为需要在发送 0-RTT 请求甚至与 TLS 握手一起发送前先完成 TCP 握手。

因此,QUIC 必须保守地回复 0-RTT 请求,限制发送的响应数据量,直到客户端被验证是真正的客户端而不是受害者。对于 QUIC 来说,这个数据量已被设置为从客户端接收到的数据量的三倍 [23]。

换言之,QUIC 的最大 “放大系数” 为 3,这一系数被认为在实用性能与安全风险间达到了可接受的平衡(尤其与某些放大系数超过 51000 倍的事件相比 [24])。由于客户端通常先发送一到两个数据包,QUIC 服务器的 0-RTT 回复将被限制在仅 4 到 6 KB(包括其他 QUIC 和 TLS 开销!),这里没什么特别之处。

除此之外,其他安全问题也会导致限制 HTTP 请求类型的重放攻击(replay attacks)。比如,Cloudflare 只允许 0-RTT 中不带查询参数的 HTTP GET 请求 [25]。这进一步限制了 0-RTT 的用途。

幸好,QUIC 具备改善这一情况的选项。比如,服务器可以检查 0-RTT 是否来自之前与其有效连接的 IP [26]。不过,只有客户端存在于同一网络时才有效(在某种程度上限制了 QUIC 的连接迁移特性 [27])。即使有效,QUIC 的响应也仍然被拥塞控制者的慢启动逻辑所限制(我们上文有讨论过)。所以,除了节省的一个 RTT 外,QUIC 并没有获得额外的大幅度速度提升。

“你知道吗?

有趣的是,QUIC 的三倍放大限制对正常的非 0-RTT 握手过程(图 2c)也有效。如果服务器的 TLS 证书 [28] 太大,导致 4 到 6 KB 无法容纳,就会出现问题。这种情况下,就不得不拆分证书,第二个数据块必须等待第二次往返被发送(在第一批传入的数据包确认以后,表明客户端的 IP 是真实的)。这时,QUIC 的握手可能仍需要两个 RTT,等同于 TCP + TLS!对于 QUIC 来说,这就是证书压缩 [29] 等技术会额外重要的原因。

“你知道吗?

某些特定高级设置足以缓解这些问题,以使 0-RTT 更有用处。比如,服务器可以记住客户端上次的可用带宽是多少,从而减少拥塞控制对重新连接(非虚假)客户端的慢启动的限制。学术界已经对此进行了研究 [30],QUIC 甚至已经有了扩展提案 [31]。几家公司也在进行相关工作来加速 TCP。

另一个选项会让客户端发送超过一到两个数据包(比如再发送七个带有填充的数据包),所以三倍的限制就会转换成一个更加有趣的 12 到 14KB 的响应(即使是在连接迁移之后)。我已经将其写入了我的一篇论文中 [32]。

最终,(行为不当的)QUIC 服务器如果认为安全或者不在意潜在的安全风险,那么它们也可以有意增加三倍限制(毕竟,并没有协议警察 [33] 来阻止)。

这一切意味着什么?

QUIC 通过 0-RTT 建立快速连接实际上更像是一种 “微优化”,而非革命性的新特性。与最先进的 TCP + TLS 1.3 设置相比,它最多可以节省一次往返。出于一系列安全考虑,在第一次往返时所实际发送的数据量受到很多限制。

因此,如果你的用户使用的是具备很高延迟的网络(比如,超过 200ms RTT 的卫星网络)或者你通常不会发送太多数据,这一特性将会非常有效。大量缓存的网站,以及通过 API 和其他协议(如 DNS-over-QUIC [34])定期获取少量更新的单页应用程序是后者的例子。谷歌十分看好 QUIC 的 0-RTT 的原因是 [35],它已经在其高度优化的搜索页面中测试了这一特性,其中查询响应非常少。

其他情况下,你最多只能再降低几十毫秒的延迟,即使已经使用了 CDN(如果你在意性能的话,就应该使用 CDN)。

注释:

[1] https://www.youtube.com/watch?v=6bbN48zCNl8

[2] https://hpbn.co/primer-on-latency-and-bandwidth/

[3] https://www.youtube.com/watch?v=ZeCIbCzGY6k

[4] https://blog.apnic.net/2017/05/09/bbr-new-kid-tcp-block/

[5] https://justinesherry.com/papers/ware-hotnets19.pdf

[6] https://www.tunetheweb.com/blog/critical-resources-and-the-first-14kb/

[7] https://www.rfc-editor.org/rfc/rfc9002.html

[8] https://dl.acm.org/doi/abs/10.1145/3387514.3405882

[9] https://github.com/quicwg/base-drafts/wiki/Implementations

[10] https://research.fb.com/wp-content/uploads/2019/12/MVFST-RL-An-Asynchronous-RL-Framework-for-Congestion-Control-with-Delayed-Actions.pdf

[11] https://www.rfc-editor.org/rfc/rfc9002.html

[12] https://datatracker.ietf.org/doc/html/rfc8312

[13] https://blog.cloudflare.com/cubic-and-hystart-support-in-quiche/

[14] https://qlog.edm.uhasselt.be/epiq/files/QUICImplementationDiversity_Marx_final_11jun2020.pdf

[15] https://www.rfc-editor.org/rfc/rfc9000.html#name-flow-control

[16] https://qlog.edm.uhasselt.be/epiq/files/QUICImplementationDiversity_Marx_final_11jun2020.pdf

[17] https://www.youtube.com/watch?v=HQ1uIClmzkU&t=603s

[18]

https://blog.cloudflare.com/delivering-http-2-upload-speed-improvements/

[19] https://www.smashingmagazine.com/2021/08/http3-core-concepts-part1/#there-is-no-quic-without-tls

[20] https://squeeze.isobar.com/2019/04/11/the-sad-story-of-tcp-fast-open/

[21] https://tools.ietf.org/html/rfc8446#section-2.3

[22] https://www.f5.com/labs/articles/education/what-is-a-dns-amplification-attack-

[23] https://www.rfc-editor.org/rfc/rfc9000.html#name-address-validation

[24] https://www.cloudflare.com/learning/ddos/memcached-ddos-attack/

[25] https://blog.cloudflare.com/introducing-0-rtt/#whatsthecatch

[26] https://www.rfc-editor.org/rfc/rfc9000.html#name-address-validation-for-futu

[27] https://www.smashingmagazine.com/2021/08/http3-performance-improvements-part2/#connection-migration

[28]https://hpbn.co/transport-layer-security-tls/#chain-of-trust-and-certificate-authorities

[29]https://www.fastly.com/blog/quic-handshake-tls-compression-certificates-extension-study

[30] https://arxiv.org/pdf/1905.03144.pdf

[31] https://datatracker.ietf.org/doc/html/draft-kuhn-quic-0rtt-bdp-08

[32] https://qlog.edm.uhasselt.be/epiq/files/QUICImplementationDiversity_Marx_final_11jun2020.pdf

[33] https://www.rfc-editor.org/rfc/rfc8962

[34] https://datatracker.ietf.org/doc/html/draft-ietf-dprive-dnsoquic

[35] https://storage.googleapis.com/pub-tools-public-publication-data/pdf/8b935debf13bd176a08326738f5f88ad115a071e.pdf

作者简介:

Robin Marx: IETF 贡献者、HTTP/3 和 QUIC 工作组成员。2015 年,作为 PhD 的一部分,Robin 开始研究 HTTP/2 的性能,这使他后来有机会在 IETF 中参与 HTTP/3 和 QUIC 的设计。在研究这些协议的过程中,Robin 开发了 QUIC 和 HTTP/3 的调试工具(被称为 qlog 和 qvis),目前这些工具已经使来自世界各地的许多工程师受益。

致谢:

本文已获得_Smashing Magazine_和作者 Robin Marx 的授权翻译和发布,特此感谢。