目录

一、同步复制

二、Galera复制架构

1. wsrep api

2. 全局事务ID(global transaction id,GTID)

3. Galera复制插件

4. 组通信插件

三、Galera复制工作原理

四、状态转移

    1. 状态快照传输

    2. 增量状态转移

    3. 写集缓存(gcache)

五、流控

    1. 流控原理

    2. 理解节点状态

    3. 节点状态与流控

    六、单节点故障与恢复

七、仲裁

    1. 加权法定票数(Weighted Quorum)

    2. 裂脑(Split-Brain)

    3. 法定票数计算

    4. 加权仲裁示例

参考:


        Galera Cluster是由Codership开发的MySQL多主集群,包含在MariaDB中,同时支持Percona xtradb、MySQL,是一个易于使用的高可用解决方案,在数据完整性、可扩展性及高性能方面都有可接受的表现。图1所示为一个三节点Galera 集群,三个MySQL实例是对等的,互为主从,这被称为多主(multi-master)架构。当客户端读写数据时,可连接任一MySQL实例。对于读操作,从每个节点读取到的数据都是相同的。对于写操作,当数据写入某一节点后,集群会将其同步到其它节点。这种架构不共享任何数据,是一种高冗余架构。

Galera Cluster for MySQL——基本原理_第1张图片


图1 三节点Galera集群

 

Galera集群具有以下特点:

    多主架构:真正的多主多活群集,可随时对任何节点进行读写。

    同步复制:集群不同节点之间数据同步,某节点崩溃时没有数据丢失。

    数据一致:所有节点保持相同状态,节点之间无数据分歧。

    并行复制:重放支持多线程并行执行以获得更好的性能。

    故障转移:故障节点本身对集群的影响非常小,某节点出现问题时无需切换操作,因此不需要使用VIP,也不会中断服务。

    自动克隆:新增节点会自动拉取在线节点的数据,最终集群所有节点数据一致,而不需要手动备份恢复。

    应用透明:提供透明的客户端访问,不需要对应用程序进行更改。

Galera集群复制要求数据库系统支持事务,因此仅支持MySQL的Innodb存储引擎,并且多主模式下只能使用可重复读隔离级别。

一、同步复制

        不同于MySQL原生的主从异步复制,Galera采用的是多主同步复制,如图2所示。

Galera Cluster for MySQL——基本原理_第2张图片

图2 多主同步复制

        异步复制中,主库将数据更新传播给从库后立即提交事务,而不论从库是否成功读取或重放数据变化。这种情况下,在主库事务提交后的短时间内,主从库数据并不一致。同步复制时,主库的单个更新事务需要在所有从库上同步更新。换句话说,当主库提交事务时,集群中所有节点的数据保持一致。

           

        相对于异步复制,同步复制的优点主要体现在以下几方面:

数据一致:同步复制保证了整个集群的数据一致性,无论何时在任何节点执行相同的select查询,结果都一样。

高可用性:由于所有节点数据一致,单个节点崩溃不需要执行复杂耗时的故障切换,也不会造成丢失数据或停止服务。

性能改进:同步复制允许在集群中的所有节点上并行执行事务,从而提高读写性能。

        当然,同步复制的缺点也显而易见,这主要源于其实现方式。同步复制协议通常使用两阶段提交或分布式锁协调不同节点的操作。假设集群有n个节点,每秒处理o个操作,每个操作中包含t个事务,则每秒将在网络中产生 n*o*t 条消息。这意味着随着节点数量的增加,事务冲突和死锁的概率将呈指数级增加。这也是MySQL缺省使用异步复制的主要原因。


        为解决传统同步复制的问题,现已提出多种数据库同步复制的替代方法。除理论外,一些原型实现也显示出了很大的希望,如以下重要改进:

组通信(Group Communication):定义了数据库节点间的通信模式,保证复制数据的一致性。

写集(Write-sets):将多个并发数据库写操作更新的数据,绑定到单个写集消息中,提高节点并行性。关于写集的概念,参见https://wxy0327.blog.csdn.net/article/details/94614149#3.%20%E5%9F%BA%E4%BA%8EWriteSet%E7%9A%84%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%A4%8D%E5%88%B6。

数据库状态机:数据库站点本地处理只读事务。更新事务首先在本地的“影子拷贝(shallow copies)”上执行,然后作为读集广播到其它数据库站点进行验证并提交。

事务重排序:此操作在数据库提交事务并将其广播到其它站点之前重新排序事务,增加成功通过验证的事务数。

        Galera集群就是基于这些方法构建的。可以看到Galera复制的原理与实现与MySQL组复制有很多相似之处。为了更好地理解Galera,在深入细节之前,先将它和MySQL组复制作一类比,如下表所示。

对比项

Galera Cluster for MySQL——基本原理_第3张图片

二、Galera复制架构

        同步复制系统中的节点将通过单个事务更新副本,从而与所有其它节点同步。这意味着当事务提交时,所有节点都将具有相同的值。此过程通过组通信使用写集复制进行。

        Galera集群的内部架构包含四个组件,如图3所示:

        

数据库管理系统(DBMS):在单个节点上运行的数据库服务器。Galera群集可以使用MySQL、Mariadb或Percona xtradb。

wsrep api:Galera与数据库服务器的接口,为上层提供了丰富的状态信息和回调函数。wsrep api由wsrep hooks、dlopen函数两部分组成。wsrep hooks钩子程序用于与数据库服务器引擎集成。dlopen函数使Galera插件中的复制程序对wsrep hooks可用。

Galera复制插件:实现写集复制功能的核心模块。

组通信插件:Galera集群的组通信系统(Group Communication System,GCS),如GComm。

Galera Cluster for MySQL——基本原理_第4张图片

1. wsrep api

        wsrep api是数据库的通用复制插件接口,定义了一组应用程序回调和复制插件调用函数。wsrep api将数据库中的数据改变视为一种状态变化,当客户端修改数据库内容时,其状态将更改。wsrep api将数据库状态更改表示为一系列事务。集群中的所有节点始终具有相同状态,它们通过以相同的顺序复制和应用状态更改来相互同步。从更技术角度看,Galera集群使用以下方式处理状态更改:

        一个节点的数据库中发生状态更改。

        wsrep钩子将更改转换为写集。

        dlopen函数连接wsrep钩子与Galera复制插件。

        Galera复制插件处理写集验证,并将更改复制到集群中的其它节点。

2. 全局事务ID(global transaction id,GTID)

        在MySQL社区中,GTID的概念并不新鲜,MySQL中的GTID由Master生成,是用于标记唯一事务并通过ID定位binlog位置的一种手段,从而有效解决了级联复制等场景中的各种问题。

        对Galera Cluster而言,复制不基于binlog,而是通过Galera复制插件来保障。Galera的GTID同样也标记事务唯一性,wsrep api使用GTID识别状态更改。Galera的GTID格式如下:

45eec521-2f34-11e0-0800-2a36050b826b:94530586304

GTID由两部分组成:

        状态UUID:表示当前状态的唯一ID,可以简单认为是集群的一个唯一标识符。

        顺序号:一个64位有符号整数,表示事务在Galera Cluster所有节点中的序号。

3. Galera复制插件

        Galera复制插件实现wsrep api,作为wsrep provider运行。Galera复制插件由以下组件构成:

         验证层:该层准备写集,并检测本机事务,以及从其它节点同步来的事务是否可以提交。

         复制层:该层的工作包含组通信和并行复制两方面。组通信负责与其它节点同步写集,并为事务分配全局唯一的GTID。并行复制实现Galera事务乐观并行控制。

4. 组通信插件

        组通信框架为各种gcomm系统提供了一个插件体系结构。Galera集群建立在专有的组通信系统层之上,实现虚拟同步。所谓虚拟同步,简单说是指一个事务在一个节点上执行成功后,保证它在其它节点也一定会被成功执行,但并不能保证实时同步。为了解决实时性问题,Galera集群实现了自己的运行时可配置的时态流控。

        组通信框架还使用GTID提供来自多个源的消息总序(Total Order)。在传输层上,Galera集群是一个对称的无向图,所有节点都通过TCP相互连接。默认情况下,TCP用于消息复制和群集成员资格服务,但也可以使用udp多播在LAN中进行复制。


三、Galera复制工作原理

        Galera复制是一种基于验证的复制,以这两篇论文为理论基础:Don’t be lazy, be consistent 和 Database State Machine Approach。基于验证的复制使用组通信和事务排序技术实现同步复制。它通过广播并发事务之间建立的全局总序来协调事务提交。简单说就是事务必须以相同的顺序应用于所有实例。事务在本节点乐观执行,然后在提交时运行一个验证过程以保证全局数据一致性。所谓乐观执行是指,事务在一个节点提交时,被认为与其它节点上的事务没有冲突,首先在本地执行,然后再发送到所有节点做冲突检测,无冲突时在所有节点提交,否则在所有节点回滚。Galera复制原理如图4所示:

Galera Cluster for MySQL——基本原理_第5张图片


        当客户端发出commit命令时,在实际提交之前,对数据库所做的更改都将被收集到一个写集中,写集中包含事务信息和所更改行的主键。然后,数据库将此写集发送到所有其它节点。节点用写集中的主键与当前节点中未完成事务的所有写集(不仅包括当前节点其它事务产生的写集,还包括其它节点传送过来的写集)的主键相比较,确定节点是否可以提交事务。同时满足以下三个条件则验证失败(存在冲突):

        两个事务来源于不同节点。

        两个事务包含相同的主键。


老事务对新事务不可见,即老事务未提交完成。新老事务的划定依赖于全局事务总序,即GTID。

        验证失败后,节点将删除写集,集群将回滚原始事务。对于所有的节点都是如此,每个节点单独进行验证。因为所有节点都以相同的顺序接收事务,它们对事务的结果都会做出相同的决定,要么全成功,要么都失败。成功后自然就提交了,所有的节点又会重新达到数据一致的状态。节点之间不交换“是否冲突”的信息,各个节点独立异步处理事务。由此可见,Galera本身的数据也不是严格同步的,很明显在每个节点上的验证是异步的,这也就是前面提到的“虚拟同步”。

        最后,启动事务的节点可以通知客户端应用程序是否提交了事务。


四、状态转移

        当一个新节点加入集群时,数据将从集群复制到这个节点,这是一个全自动的过程,Galera将此称为状态转移。前面介绍Galera架构时曾提到,wsrep api将集群中的数据改变视为状态改变,因此这里将数据同步称作状态转移也就不足为怪了。Galera集群中有两种状态转移方法:


    状态快照传输(State Snapshot Transfers,SST),也就是通常所说的全量数据同步。

    增量状态转移(Incremental State Transfers,IST),指增量数据同步。

当有新节点加入时,集群会选择出一个捐献者(Donor)节点为新节点提供数据,这点与MySQL组复制类似。

1. 状态快照传输

        新节点加入集群时会启动状态快照传输(SST),将其数据与集群同步。Galera支持rsync、rsync_-wan、xtrabackup、mysqldump四种状态快照传输方法,由系统变量wsrep_sst_method指定,缺省为rsync。

        rsync、rsync_-wan、xtrabackup三种方法是物理备份,将数据文件直接从捐献者服务器复制到新节点服务器,并在传输后初始化接收服务器,其中xtrabackup方式可实现捐赠者无阻塞数据同步。这些方法比mysqldump快很多。

        mysqldump方法是逻辑备份,要求用户手动初始化接收服务器,并在传输之前准备好接受连接。这是一种阻塞方法,在传输期间,捐赠节点变为只读。mysqldump是状态快照传输最慢的方法,不建议在生产环境使用。


2. 增量状态转移

        增量状态转移(IST)只向新节点发送它所缺失的事务。使用IST需要满足两个先决条件:

新加入节点的状态UUID与集群中的节点一致。

新加入节点所缺失的写集在捐助者的写集缓存中存在。这点很好理解,类比MySQL的binlog,如果所需的binlog文件缺失,是无法做增量备份恢复的。

        满足这些条件时,捐助节点单独传输缺失的事务,并按顺序重放它们,直到新节点赶上集群。例如,假设集群中有一个节点落后于集群。此节点携带的节点状态如下:

5a76ef62-30ec-11e1-0800-dba504cf2aab:197222

同时,集群上的捐助节点状态为:

5a76ef62-30ec-11e1-0800-dba504cf2aab:201913

        集群上的捐助节点从加入节点接收状态转移请求。它检查自身写集缓存中的序列号197223。如果该序号在写集缓存中不可用,则会启动SST。否则捐助节点将从197223到201913的提交事务发送到新加入节点。增量状态传输的优点是可以显著加快节点合并到集群的速度。另外,这个过程对捐赠者来说是非阻塞的。

        增量状态传输最重要的参数是捐助节点上的gcache.size,它控制分配多少系统内存用于缓存写集。可用空间越大,可以存储的写集越多。可以存储的写集越多,通过增量状态传输可以弥合的事务间隙就越大。另一方面,如果写集缓存远大于数据库大小,则增量状态传输开始时的效率低于发送状态快照。

3. 写集缓存(gcache)

        Galera群集将写集存储在一个称为gcache的特殊缓存中。gcache使用三种类型的存储:

永久内存存储(Permanent In-Memory Store):写集使用操作系统的默认内存分配器进行分配,永久存储于物理内存中。gcache.keep_pages_size参数指定保留的内存页总大小,缺省值为0。由于硬件的限制,默认情况下是禁用的。

永久环缓冲区文件(Permanent Ring-Buffer File):写集在缓存初始化期间预分配到磁盘,生成一个内存映射文件,用作写集存储。文件目录和文件名分别由gcache.dir和gcache.name参数指定。文件大小由gcache.size参数指定,缺省值为128MB。

按需页存储(On-Demand Page Store):根据需要在运行时将写集分配给内存映射页文件。大小由gcache.page_size参数指定,缺省值为128M,可随写集自动变大。页面存储的大小受可用磁盘空间的限制。默认情况下,Galera会在不使用时删除页面文件,用户可以设置要保留的页面文件总大小(gcache.size)。当所有其它存储被禁用时,磁盘上至少保留一个页面的文件。

        Galera集群使用一种分配算法,尝试按上述顺序存储写集。也就是说,它首先尝试使用永久内存存储,如果没有足够的空间用于写入集,它将尝试存储到永久环缓冲区文件。除非写入集大于可用磁盘空间,否则页面存储始终成功。

        注意,如果gcache.recover参数设置为yes,则在启动时将尝试恢复gcache,以便该节点可以继续向其它节点提供IST服务。如果设置为no(缺省),gcache将在启动时失效,节点将只能为SST提供服务。

五、流控

        Galera集群内部使用一种称为流控的反馈机制来管理复制过程。流控允许节点根据需要暂停和恢复复制,这可以有效防止任一节点在应用事务时落后其它节点太多。

1. 流控原理

        从Galera集群同步复制(虚拟同步)原理可知,事务的应用和提交在各个节点上异步发生。节点从集群接收但尚未应用和提交的事务将保留在接收队列中。由于不同节点之间执行事务的速度不一样,慢节点的接收队列会越积越长。当接收队列达到一定大小时,节点触发流控,作用就是协调各个节点,保证所有节点执行事务的速度大于队列增长速度。流控的实现原理很简单:整个Galera集群中,同时只有一个节点可以广播消息,每个节点都会获得广播消息的机会(获得机会后也可以不广播)。当慢节点的接收队列超过一定长度后,它会广播一个FC_PAUSE消息,所有节点收到消息后都会暂缓广播消息,直到该慢节点的接收队列长度减小到一定长度后再恢复复制。

        流控相关参数如下:

gcs.fc_limit:接收队列中积压事务的数量超过该值时,流控被触发,缺省值为16。对于Master-Slave模式(只在一个节点写)的Galera集群,可以配置一个较大的值,防止主从复制延迟。对启动多写的Galera集群,较小的值比较合适,因为较大的接收队列长度意味着更多冲突。

gcs.fc_factor:当接收队列长度开始小于 gcs.fc_factor * gcs.fc_limit 时恢复复制,缺省值为1。

gcs.fc_master_slave:Galera集群是否为Master-Slave模式,缺省为no。

2. 理解节点状态

        一个节点在Galera集群中可能经历的节点状态有Open、Primary、Joiner、Joined、Synced、Donor。可以通过wsrep_local_state和wsrep_local_state_comment系统变量查看节点的当前状态。节点状态更改如图5所示:

Galera Cluster for MySQL——基本原理_第6张图片

 

节点启动并建立到主组件( Primary Component,PC)的连接。由于网络问题群集可能被拆分为多个部分,为避免数据差异或脑裂,此时只能有一部分可以修改数据,这部分称为主组件。

当节点成功执行状态传输请求时,它将开始缓存写集。

节点接收状态快照传输(SST)。它将拥有所有集群数据,并开始应用缓存的写集。

节点完成对群集的追赶。节点将mysql状态变量wsrep_ready设置为值1,现在允许该节点处理事务。

节点接收状态传输请求,成为捐赠者。节点缓存它无法应用的所有写集。

节点完成对新加入节点的状态传输。

3. 节点状态与流控

        Galera集群根据节点状态实现多种形式的流控以保证数据一致性。有四种主要流控类型:

无流控(No Flow Control):当节点处于Open或Primary状态时,此流控类型生效。此时节点还不被视为集群的一部分,不允许这些节点复制、应用或缓存任何写集。

写集缓存(Write-set Caching):当节点处于Joiner和Donor状态时,此流控类型生效。节点在此状态下不能应用任何写集,必须缓存它们以备以后使用。

赶上(Catching Up):此流控类型在节点处于Joined状态时生效。处于此状态的节点可以应用写集。这里的流控确保节点最终能够追赶上集群。由于应用写集通常比处理事务快几倍,处于这种状态的节点几乎不会影响集群性能。

集群同步(Cluster Sync):此流控类型在节点处于Synced状态时生效。当节点进入此状态时,流控将尝试将接收队列保持最小。

六、单节点故障与恢复

        当一个节点因为硬件、软件、网络等诸多原因与集群失去联系时,都被概括为节点故障。从集群的角度看,主组件看不到出问题的节点,它将会认为该节点失败。从故障节点本身的角度来看,假设它没有崩溃,那么唯一的迹象是它失去了与主组件的连接。可以通过轮询wsrep_local_state状态变量监控Galera群集节点的状态,值及其含义见上节流控中的描述。

        集群检查从节点最后一次接收到数据包的时间确定该节点是否连接到集群,检查的频率由evs.inactive_check_period参数指定,缺省值为每隔0.5秒检查一次。在检查期间,如果群集发现自上次从节点接收网络数据包以来的时间大于evs.keepalive_period参数的值(缺省值为1秒),则它将开始发出心跳信号。如果集群在evs.suspect_timeout参数(缺省值为5秒)期间没有继续从节点接收到网络数据包,则该节点被声明为suspect,表示怀疑该节点已下线。一旦主组件的所有成员都将该节点视为可疑节点,它就被声明为inactive,即节点失败。如果在大于evs.inactive_timeout(缺省值为15秒)的时间内未从节点接收到消息,则无论意见是否一致,都会声明该节点失败。在所有成员同意其成员资格之前,失败节点将保持非操作状态。如果成员无法就节点的活跃性达成一致,说明网络对于集群操作来说太不稳定。       

        这些选项值之间的关系为:

evs.inactive_check_period <= evs.keepalive_period <= evs.suspect_timeout <=    evs.inactive_timeout

        需要注意,如果网络过于繁忙,以至于无法按时发送消息或心跳信号无响应,也可能被宣布为节点失败,这可以防止集群其余部分的操作被锁。如果不希望这样处理,可以增加超时参数。如果用CAP原则来衡量,Galera集群强调的是数据一致性(Consistency),这就导致了集群需要在可用性(Availability)和分区容忍性(Partition tolerance)之间进行权衡。也就是说,当使用的网络不稳定时,低evs.suspect_timeout和evs.inactive_timeout值可能会导致错误的节点故障检测结果,而这些参数的较高值可能会导致在实际节点故障的情况下更长的发现时间。

        集群中的一个节点出现故障不会影响其它节点继续正常工作,单节点故障不会丢失任何数据。失败节点的恢复是自动的。当失败节点重新联机时,它会自动与其它节点同步数据,之后才允许它重新回到集群中。如果重新同步过程中状态快照传输(SST)失败,会导致接收节点不可用,因为接收节点在检测到状态传输故障时将中止。这种情况下若使用的是mysqldump方式的SST,需要手动还原。

七、仲裁

        除了单节点故障外,群集还可能由于网络故障而拆分为多个部分。每部分内的节点相互连接,但各部分之间的节点失去连接,这被称为网络分裂(network partitioning)。此情况下只有一部分可以继续修改数据库状态,以避免数据差异,这一部分即为主组件。正常情况下主组件就是整个集群。当发生网络分裂时,Galera集群调用一个仲裁算法选择一部分作为主组件,保证集群中只有一个主组件。

1. 加权法定票数(Weighted Quorum)

        集群中的当前节点数量定义了当前集群的大小,群集大小决定达到仲裁所需的票数。Galera集群在节点不响应并且被怀疑不再是集群的一部分时进行仲裁投票。可以使用evs.suspect_timeout参数微调此无响应的超时时间,默认为5秒。

        发生网络分裂时,断开连接的两侧都有活动节点。主组件要求获得仲裁的多数票,因此具有较多存活节点的部分将成为主组件,而另一部分将进入非主状态并开始尝试与主组件连接,如图6所示。

Galera Cluster for MySQL——基本原理_第7张图片

 

        仲裁要求多数,这意味着不能在双节点群集中进行自动故障转移,因为一个节点的故障会导致另一节点自动进入非主状态。而具有偶数个节点的集群则有脑裂风险。如果在网络分裂导致节点的数量正好分成两半,则两个分区都不能成为主组件,并且都进入非主状态,如图7所示。要启用Galera集群自动故障切换,至少需要使用三个节点。

Galera Cluster for MySQL——基本原理_第8张图片

2. 裂脑(Split-Brain)

        导致数据库节点彼此独立运行的集群故障称为“脑裂”。这种情况可能导致数据不一致,并且无法修复,例如当两个数据库节点独立更新同一表上的同一行时。与任何基于仲裁的系统一样,当仲裁算法无法选择主组件时,Galera集群会受到脑裂影响。

        Galera设计为避免进入分裂脑状态,如果失败导致将集群分割为两个大小相等的部分,则两部分都不会成为主组件。在节点数为偶数的集群中,为把脑裂风险降到最低,可以人为分区将一部分始终划分为集群主组件,如:

4 node cluster -> 3 (Primary) + 1 (Non-primary)

6 node cluster -> 4 (Primary) + 2 (Non-primary)

6 node cluster -> 5 (Primary) + 1 (Non-primary)

以上分区示例中,任何中断或失败都很难导致节点完全分成两半。

3. 法定票数计算

        Galera群集支持加权仲裁,其中每个节点可以被分配0到255范围内的权重参与计算。法定票数计算公式为:

其中:

pi:最后可见的主组件的成员;

li:已知正常离开集群的成员;

mi:当前组件成员;

wi:成员权重。

        这个公式的含义是:当且仅当当前节点权重总和大于最后一个主组件节点权重和减去正常离开集群节点权重和的一半时,才会被选为新的主组件。

        消息传递时带有权重信息。缺省的节点权重为1,此时公式被转换为单纯的节点计数比较。通过设置pc.weight参数,可以在运行时更改节点权重,例如:

set global wsrep_provider_options="pc.weight=3";

4. 加权仲裁示例

        在了解了加权仲裁的工作原理后,下面是一些部署模式的示例。

(1)三个节点的加权仲裁

        三个节点配置仲裁权重如下:

node1: pc.weight = 2

node2: pc.weight = 1

node3: pc.weight = 0

此时如果node2和node3失效,node1会成为主组件,而如果node1失效,node2和node3都将成为非主组件。

(2)一主一从方案的加权仲裁

        主、从节点配置仲裁权重如下:

node1: pc.weight = 1

node2: pc.weight = 0

如果主节点失效,node2将成为非主组件,如果node2失效,node1将继续作为主组件。

(3)一主多从方案的加权仲裁

        为具有多个从节点的主从方案配置仲裁权重:

node1: pc.weight = 1

node2: pc.weight = 0

node3: pc.weight = 0

...

noden: pc.weight = 0

如果node1失效,所有剩余的节点都将作为非主组件,如果任何其它节点失效,则保留主组件。在网络分裂的情况下,node1始终作为主组件。(4)主站点和从站点方案的加权仲裁

        为主站点和从站点配置仲裁权重:

Primary Site:

  node1: pc.weight = 2

  node2: pc.weight = 2

Secondary Site:

  node3: pc.weight = 1

  node4: pc.weight = 1

这种模式下,一些节点位于主站点,而其它节点位于从站点。如果从站点关闭或站点之间的网络连接丢失,则主站点上的节点仍然是主组件。此外,node1或node2崩溃不会让其它剩余节点成为非主组件。