Zookeeper学习大纲

发布时间 2023-05-25 09:29:21作者: 似懂非懂视为不懂

ZooKeeper学习大纲

ZooKeeper应用及原理解析

  1. Zookeeper介绍
    • 什么是Zookeeper
    • Zookeeper的应用场景
  2. 搭建Zookeeper服务器
    • zoo.cfg配置文件说明
    • Zookeeper服务器的操作命令
  3. Zookeeper内部的数据模型
    • zk是如何保存数据
    • zk中的znode是什么样的结构
    • zk中节点znode的类型
    • zk的数据持久化
  4. Zookeeper客户端(zkCli)的使用
    • 多节点类型创建
    • 查询节点
    • 删除节点
    • 权限设置
  5. Curator客户端的使用
    • Curator介绍
    • 引入Curator
    • 创建节点
    • 获得节点数据
    • 修改节点数据
    • 创建节点同时创建父节点
    • 删除节点
  6. zk实现分布式锁
    • zk中锁的种类
    • zk如何上读锁
    • zk如何上写锁
    • 羊群效应
    • curator实现读写锁
      • 获取读锁
      • 获取写锁
  7. zk的watch机制
    • watch机制介绍
    • zkCli客户端使用watch
    • curator客户端使用watch
  8. Zookeeper集群实战
    • Zookeeper集群角色
    • 集群搭建
      • 创建4个节点的myid,并设值
      • 编写4个zoo.cfg
      • 启动4台Zookeeper
    • 连接Zookeeper集群
  9. ZAB协议
    • 什么是ZAB协议
    • ZAB协议定义的四种节点状态
    • 集群上线时的Leader选举过程
    • 崩溃恢复时的Leader选举
    • 主从服务器之间的数据同步
    • Zookeeper中的NIO与BIO的应用
  10. CAP理论
    • CAP定理
    • CAP权衡
    • BASE理论
    • Zookeeper追求的一致性

一、Zookeeper介绍

1.什么是Zookeeper

Zookeeper是一种分布式协调服务,用于管理大型主机。在分布式环境中协调和管理服务是一个复杂的过程。Zookeeper通过其简单的架构和API解决了这个问题。Zookeeper允许开发人员专注于核心应用程序逻辑,而不必担心应用程序的分布式特性。

2.Zookeeper的应用场景

  • 分布式协调组件

在分布式系统中,需要有Zookeeper作为分布式协调组件,协调分布式系统中的状态。

  • 分布式锁

zk在实现分布式锁上,可以做到强一致性,关于分布式锁相关的知识,在之后的ZAB协议中介绍。

  • 无状态化的实现

二、搭建Zookeeper服务器

1.zoo.cfg配置文件说明

# zookeeper时间配置中的基本单位(毫秒)
tickTime=2000
# 允许follower初始化连接到leader最大时长,它表示tickTIme时间倍数 即:initLimit*tickTime
initLimit=10
# 允许follower与leader数据同步最大时长,它表示tickTime时间倍数
syncLimit=5
# zookeeper数据存储目录及日志保存目录(如果没有指明dataLogDir,则日志也保存在这个文件中)
dataDir=/tmp/zookeeper
# 对客户端提供的端口号
clientPort=2181
# 单个客户端与zookeeper最大并发连接数
maxClientCnxns=60
# 保存的数据快照数量,之外的将会被清除
autopurge.snapRetainCount=3
# 自动触发清除任务时间间隔,小时为单位,默认为0,表示不自动清除。
autopurge.purgeInterval=1

2.Zookeeper服务器的操作命令

  • 重命名conf中的文件zoo_sample.cfg-->zoo.cfg

  • 启动zk服务器:

    ./bin/zkServer.sh start ./conf.zoo.cfg
    
  • 查看zk服务器状态:

    ./bin/zkServer.sh status ./conf/zoo.cfg
    
  • 停止zk服务器:

    ./bin/zkServer.sh stop ./conf/zoo.cfg
    

三、Zookeeper内部的数据模型

1.zk是如何保存数据的

zk中的数据是保存在节点上的,节点就是znode,多个znode之间构成一棵树的目录结构。Zookeeper的数据模型是什么样子呢?它很像数据结构当中的树,他很像文件系统的目录。

树是由节点所组成,Zookeeper的数据存储也同样是基于节点,这种节点叫做Znode

但是,不同于树的节点,Znode的引用方式是路径引用,类似于文件路径:

/动物/猫
/汽车/宝马

这样的层级结构,让每一个Znode节点拥有唯一的路径,就像命名空间一样对不同信息作出清晰的隔离。

# 创建节点
create /name
# 删除节点[如果该节点下存在子节点即无法删除,想实现删除需要使用deleteall]
delete /name
# 创建节点数据
create /name luoyu
# 获取节点数据
get /name

2.zk中的znode是什么样的结构

zk中的znode,包含了四个部分:

  • data:保存数据
  • acl:权限,定义 了什么样的用户能够操作这个节点,且能够进行怎样的操作。
    • c:create创建权限,允许在该节点下创建子节点
    • w:write更新权限,允许更新该节点的数据
    • r:read读取权限,允许读取该节点的内容以及子节点的列表信息
    • d:delete删除权限,允许删除该节点的子节点
    • a:admin管理着权限,允许对该节点进行acl权限设置
  • stat:描述当前znode的元数据
  • child:当前节点的子节点

3.zk中节点znode的类型

  • 持久节点:创建出的节点,在会话结束后依然存在。保存数据【create /luoyu】

    zk客户端----->zk服务器 1.创建连接请求

    zk服务器----->zk客户端 2.返回session ID

  • 持久序号节点:创建出的节点,根据先后顺序,会在节点之后带上一个数值,越后执行数值越大,适用于分布式锁的应用场景-单调递增【create -s /luoyu】

  • 临时节点:

    临时节点是在会话结束后,自动被删除的,通过这个特性,zk可以实现服务注册与发现的效果,那么临时节点是如何维持心跳呢?【create -e /luoyu1】

    zk客户端----->zk服务器 1.创建连接请求

    zk服务器----->zk客户端 2.返回session ID

    zk客户端----->zk服务器 3.持续会话,续约session ID的时间

    zk客户端 4.会话断开

    zk服务器 5.删除没有续约的session ID对应的临时节点

  • 临时序列号节点:跟持久序列节点相同,适用于临时的分布式锁【create -s -e /luoyu】

  • Container节点(3.5.3版本新增):Container容器节点,当容器中没有任何子节点,该容器节点会被zk定期删除(60s其他地方解释为某个时间会被定期删除,未说明具体的时间)。【create -c /mycontainer】

  • TTL(time to live)节点:可以指定节点的到期时间,到期后被zk定时删除,只能通过系统配置zookeeper.extendedTypesEnabled=true开启【先了解】

4.zk的数据持久化

zk的数据是运行在内存中,zk提供了两种持久化机制:

  • 事务日志

    zk把执行的命令以日志形式保存在dataLogDir指定的路径中的文件中(如果没有指定dataLogDir,则按dataDir指定的路径)。

  • 数据快照

    zk会在一定的时间间隔内做一次内存数据的快照,把该时刻的内存数据保存在快照文件中

zk通过两种形式的持久化,在恢复时先恢复快照文件中的数据到内存中,再用日志文件中的数据做增量恢复,这样的恢复速度更快。

四、Zookeeper客户端(zkCli)的使用

1.多节点类型创建

  • 创建持久节点【create /test;创建子节点create /test/sub】
  • 创建持久序号节点【create -s /test1】
  • 创建临时节点【create -e /test2】
  • 创建临时序号节点【create -e -s /test3】【如果直接关闭客户端,该节点会等待一段时间才被销毁。因此通过quit或者close命令关闭,才会立即被销毁】
  • 创建容器节点【create -c /test4】【如果该容器节点下没有子节点,在60s之后该节点会被自动删除】

2.查询节点

  • 普通查询【ls /;查询当前节点下的所有子节点ls -R /】
  • 查询节点详细信息【get /test1;查询节点详细的信息get -s /test1】
    • cZxid:创建节点的事务ID
    • mZxid:修改节点的事务ID
    • pZxid:添加和删除子节点的事务ID
    • ctime:节点创建的时间
    • mtime:节点最近修改的时间
    • dataVersion:节点内数据的版本,每更新一次数据,版本会+1
    • aclVersion:此节点的权限版本
    • ephemeralOwner:如果当前节点是临时节点,该值是当前节点所有者的session id。如果节点不是临时节点,则该值为零
    • dataLength:节点内数据的长度
    • numChildren:该节点的子节点个数

3.删除节点

  • 普通删除【delete /luoyu;如果该节点下有子节点deleteall /luoyu】
  • 乐观锁删除【delete -v 1 /luoyu】

4.权限设置

  • 注册当前会话的账号和密码:

    addauth digest xiaoluo:123456
    
  • 创建节点并设置权限

    create /test-node abcd auth:xiaoluo:123456:cdwra
    
  • 在另一个会话中必须先使用账号密码,才能拥有操作该节点的权限

五、Curator客户端的使用

1.Curator介绍

Curator是NetFlix公司开源的一套zookeeper客户端框架,Curator是对Zookeeper支持最好的客户端框架。Curator封装了大部分Zookeeper的功能。比如Leader选举、分布式锁等,减少了技术人员在使用Zookeeper时的底层细节开发工作。

六、zk实现分布式锁

1.zk中锁的种类:

  • 读锁:大家都可以读,要想上读锁的前提:之前的锁没有写锁。
  • 写锁:只有得到写锁的才能写。要想上写锁的前提是,之前没有任何锁。

2.zk如何上读锁

  • 创建一个临时序号节点,节点的数据是read,表示是读锁
  • 获取当前zk中序号比自己小的所有节点
  • 判断最小节点是否是读锁:
    • 如果不是读锁的话,则上锁失败,为最小节点设置监听。阻塞等待,zk的watch机制会当最小节点发生变化时通知当前节点,于是再执行第二步的流程
    • 如果是读锁的话,则上锁成功

3.zk如何上写锁

  • 创建一个临时序号节点,节点的数据是write,表示是写锁
  • 获取zk中所有的子节点
  • 判断自己是否是最小的节点:
    • 如果是,则上写锁成功
    • 如果不是,说明前面还有锁,则上锁失败,监听最小的节点,如果最小节点有变化,则回到第二步。

4.羊群效应

如果用上述的上锁方式,只要有节点发生变化,就会触发其他节点的监听事件,这样的话对zk的压力非常大,——羊群效应,可以调整成链式监听。解决这个问题。

5.curator实现读写锁

  1. 获取读锁
@Test
void testGetReadLock() throws Exception{
    // 读写锁
    InterProcessReadWriteLock interProcessReadWriteLock=new InterProcessReadWriteLock(client,"/lock1");
    // 获取读锁对象
    InterProcessLock interProcessLock=interProcessReadWriteLock.readLock();
    System.out.println("等待获取读锁对象");
    // 获取锁
    interProcessLock.acquire();
    for(int i=1;i<=100;i++){
        Thread.sleep(3000);
        System.out.println(i);
    }
    //释放锁
    interProcessLock.release();
    System.out.println("等待释放锁");
}
  1. 获取写锁
@Test
void testGetWriteLock() throws Exception{
    //读写锁
    InterProcessReadWriteLock interProcessReadWriteLock= new InterProcessReadWriteLock(client,"/lock1");
    //获取写锁对象
    InterProcessLock interProcessLock=interProcessReadWriteLock。writeLock();
    System.out.println("等待获取写锁对象!");
    //获取锁
    interProcessLock.acquire();
    for(int i =1;i<=100;i++){
        Thread.sleep(3000);
        System.out.println(i);
    }
    //释放锁
    interProcessLock.release();
    System.out.println("等待释放锁!");
}

七、zk的watch机制

1.Watch机制介绍

我们可以把Watch理解成是注册在特定Znode上的触发器。当这个Znode发生改变,也就是调用了create,delete,setData方法的时候,将会触发Znode上注册的对应事件,请求Watch的客户端会接收到异步通知。

具体交互过程如下:

  • 客户端调用getData方法,watch参数是true。服务端接到请求,返回节点数据,并且在对应的哈希表里插入被Watch的Znode路径,以及Watcher列表。
  • 当被Watch的Znode已删除,服务端会查找哈希表,找到该Znode对应的所有Watcher,异步通知客户端,并且删除哈希表中的key-value

客户端使用NIO通信模式监听服务端的调用。

# 客户端1
create /test9
get -w /test9  #一次性监听。如果想持续触发监听,在监听后的命令行中get -w /test9

#客户端2
set /test9 abc #触发监听
create /test9/sub1 #在节点下创建子节点的时间不会被监听
delete /test9 #会触发监听

2.zkCli客户端使用watch

create /test xxx
get -w /test #一次性监听节点
ls -w /test #监听目录,创建和删除子节点会收到通知,子节点中新增节点不会收到通知
ls -R -w /test #对于子节点中子节点的变化会收到通知,但内容的变化不会收到通知

3.curator客户端使用watch

@Test
public void addNodeListener() throws Exception{
    NodeCache nodeCache = new NodeCache(curatorFramework,"/curator-node");
    nodeCache.getListenable().addListener(new NodeCacheListener(){
        @Override
        public void nodeChanged() throws Exception{
            log.info("{} path nodeChanged: ","/curator-node");
            printNodeData();
        }
    });
    nodeCache.start();
    System.in.read();
}

public void printNodeData() throws Exception{
    byte[] bytes = curatorFramework.getData().forPath("/curator-node");
    log.info("data: {}",new String(bytes));
}

八、Zookeeper集群实战

1.Zookeeper集群角色

zookeeper集群中的节点有三种角色

  • Leader:处理集群的所有事务请求,集群中只有一个Leader
  • Follower:只能处理读请求,参与Leader选举
  • Observer:只能处理读请求,提升集群读的性能,但不能参与Leader选举。

2.集群搭建(伪集群)

搭建4个节点,其中一个节点为Observer

1)创建4个节点的myid,并设值

在/usr/local/zookeeper中创建一下四个文件

mkdir -p /usr/local/zookeeper/zkdata/zk1 
echo 1 > /usr/local/zookeeper/zkdata/zk1/myid
mkdir -p /usr/local/zookeeper/zkdata/zk2
echo 2 > /usr/local/zookeeper/zkdata/zk2/myid
mkdir -p /usr/local/zookeeper/zkdata/zk3
echo 3 > /usr/local/zookeeper/zkdata/zk3/myid
mkdir -p /usr/local/zookeeper/zkdata/zk4
echo 4 > /usr/local/zookeeper/zkdata/zk4/myid

2)编写4个zoo.cfg

#在/usr/local/zookeeper/apache-zookeeper-3.7.0-bin/conf下创建4个zoo1.cfg,zoo2.cfg,zoo3.cfg,zoo4.cfg
#因为这里是伪集群因此使用这种方式
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial 
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between 
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just 
# example sakes.
#修改对应的zk1,zk2,zk3,zk4
dataDir=/usr/local/zookeeper/zkdata/zk1
# the port at which the clients will connect
#修改对应的zk1,zk2,zk3,zk4的客户端访问端口2181,2182,2813,2184
#如果此处搭建的是集群的话,端口可不做变动
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the 
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1

## Metrics Providers
#
# https://prometheus.io Metrics Exporter
#metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
#metricsProvider.httpPort=7000
#metricsProvider.exportJvmInfo=true

#2001为集群内部数据通信端口,3001为集群选举端口,observer表示不参与集群选举
server.1=192.168.147.144:2001:3001
server.2=192.168.147.144:2002:3002
server.3=192.168.147.144:2003:3003
server.4=192.168.147.144:2004:3004:observer

3)启动4台Zookeeper服务节点

cd /usr/local/zookeeper/apache-zookeeper-3.7.0-bin/bin
#启动所有节点
./zkServer.sh start ../conf/zoo1.cfg 
./zkServer.sh start ../conf/zoo2.cfg 
./zkServer.sh start ../conf/zoo3.cfg 
./zkServer.sh start ../conf/zoo4.cfg

#查看节点状态
./zkServer.sh status ../conf/zoo1.cfg 
./zkServer.sh status ../conf/zoo2.cfg 
./zkServer.sh status ../conf/zoo3.cfg 
./zkServer.sh status ../conf/zoo4.cfg

3.连接zookeeper集群

cd /usr/local/zookeeper/apache-zookeeper-3.7.0-bin/bin
./zkCli.sh -server 192.168.147.144:2181,192.168.147.144:2182,192.168.147.144:2183

九、ZAB协议

1.什么是ZAB协议

zookeeper作为非常重要的分布式协调组件,需要进行集群部署,集群中会以一主多从的形式进行部署。zookeeper为了保证数据的一致性,使用ZAB(Zookeeper Atomic Broadcast)协议,这个协议解决了Zookeeper的崩溃回复和主从数据同步的问题。

ZAB协议包括如下特点:

  • follower节点上所有的写请求都转发给leader
  • 写操作严格有序
  • ZooKeeper使用改编的两阶段提交协议来保证各个节点的事务一致性

二阶段提交(英语:Two-phase Commit)是指在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,二阶段提交也被称为是一种协议(Protocol)。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

整个提交过程分为4个步骤

  • 协调者询问所有的参与者是不是可以提交了
  • 参与者回复yes or no
  • 协调者收到所有的yes之后执行commit否则执行rollback
  • 参与者执行完成后回复ACK

zookeeper采用的是改编过的两阶段提交,就是在第三步的时候,不需要所有的参与者回复yes,只需要超过半数(刚好半数也不行)的参与者回复yes。之所以是超过半数而不是所有参与者回复yes,是为了避免少量的参与者出现单点故障或者网络波动导致协调者长时间收不到回复。

2.ZAB协议定义的四种节点状态

  • Looking:选举状态
  • Follower:Follower节点(从节点)所处的状态
  • Leading:Leader节点(主节点)所处状态
  • Observing:观察者节点所处的状态

3.集群上线时的Leader选举过程

Zookeeper集群中的节点在上线时,将会进入到Looking状态,也就是选举Leader的状态,这个状态具体会发生什么?

什么是zxid和myid?
zxid:zookeeper为了保证数据的有序性,会给每一个写操作的数据,编写一个全局唯一的zxid.zxid是一个64位的数字:前32位会是由当前节点参与的选举次数决定,后32位是存储数据的全局唯一id。因为先生成的节点值较小、后生成的节点值较大的特点,故而 后存储的数据的zxid 一定大于 先存储的数据的zxid。

myid:myid的值是zoo.cfg文件里定义的server.A项A的值,Zookeeper 启动时会读取这个文件,拿到里面的数据与 zoo.cfg 里面的配置信息,比较从而判断到底是那个server,只是一个标识作用。

三个核心选举原则:

  • zookeeper集群中只有超过了半数以上的服务器启动,此集群才能正常工作;

  • 在集群正常工作之前,myid小的服务器会给myid大的服务器投票,这种投票会一直持续到集群开始正常工作,即,选出了leader。

  • 选出leader之后,之前的服务器节点的状态要由looking转为following从节点,以后的服务器不管是不是新加进来的都会变成follower从节点。

4.崩溃恢复时的Leader选举

Leader建立完后,Leader周期性地不断向Follower发送心跳(ping命令,没有内容的socket)。当Leader崩溃后,Follower发现socket通道已关闭,于是Follower开始进入到Looking状态,重新回到上一节的Leader选举过程,此时集群不能对外提供服务。

5.主从服务器之后的数据同步

  • 此处的半数以上指的是所有主从节点,而不是单单的从节点
  • ACK (Acknowledge character)即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。
  • zookeeper的设计原理实际是介于强一致性和最终一致性之间的一种情况。如果在主从同步过程中,出现某一节点的网络动荡,其他节点已经完成并返回ACK给leader节点,那么该节点的数据将会出去丢失的情况。
  • 如果客户端访问从节点进行数据写入,实际写这个过程是leader节点完成的。

6.Zookeeper中的NIO和BIO的应用

  • NIO
    • 用于被客户端连接的2181端口,使用的是NIO模式与客户端建立连接
    • 客户端开启Watch时,也是用NIO,等待Zookeeper服务器的回调
  • BIO
    • 集群在选举时,多个节点之间的投票通信端口,使用BIO进行通信。

BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端发送请求服务器端就需要启动一个线程处理,若这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器(采用事件驱动思想实现)上,多路复用器轮询I/O请求时才启动一个线程进行处理。
AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

十、CAP理论

1.CAP定理

2000年7月,加州大学伯克利分校的Eric Brewer教授在ACM PODC会议上提出CAP猜想。2年后,麻省理工学院的Seth Glibert和Nancy Lynch从理论上证明了CAP。之后,CAP理论正式成为分布式计算领域的公认定理。

CAP理论为:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

  • 一致性(Consistency)

    一致性指“all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。

  • 可用性(Availability)

    可用性指“Reads and writes always succeed”,即服务一致可用,而且是正常相应时间。

  • 分区容错性(Partition tolerance)

    分区容错性指“the system continues to operate despite arbitray message loss or failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务。 --避免单点故障,就要进行冗余部署,冗余部署相当于是服务的分区,这样的分区就具备了容错性。

2.CAP权衡

通过CAP理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?

3.BASE理论

eBay的架构师Dan Pritchett源于对大规模分布式系统的时间总结,在ACM上发表文章提出BASE理论,BASE理论是对CAP理论的延伸,核心思想是几遍无法做到强一致性性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consistency)。

  • 基本可用(Basically Available)

    基本可用是指分布式系统在出现故障而定时候,允许损失部分可用性,即保证核心可用。

    电商大促时,为了应对访问量激增。部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。

  • 软状态(Soft State)

    软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现,mysql replication的异步复制也是一种体现。

  • 最终一致性(Eventual Consistency)

    最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态,弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

4.Zookeeper追求的一致性

Zookeeper在数据同步时,追求的并不是强一致性,而是顺序一致性(事务id的单调递增)。