掌握zookeeper命令,看这篇文章就够了
共 15318字,需浏览 31分钟
·
2020-09-20 19:48
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
zookeeper使用
安装与配置
使用安装包安装
# 下载
$ wget https://downloads.apache.org/zookeeper/zookeeper-3.6.0/apache-zookeeper-3.6.0-bin.tar.gz
$ tar -zxvf apache-zookeeper-3.6.0-bin.tar.gz
$ cd apache-zookeeper-3.6.0-bin
# 配置
$ cp conf/zoo_sample.cfg conf/zoo.cfg
# 编辑配置文件,下面介绍
$ vi conf/zoo.cfg
# 启动服务端
$ bin/zkServer.sh start
# 查看状态
$ bin/zkServer.sh status
# 停止
$ bin/zkServer.sh stop
# 重启
$ bin/zkServer.sh restart
# 用 jps 查看状态
$ jps
46193 QuorumPeerMain
Homebrew 安装 zookeeper
$ brew install zookeeper
# 启动服务
$ brew services start zookeeper
# 停止服务
$ brew services stop zookeeper
# 配置文件位置
$ ls /usr/local/etc/zookeeper
defaults log4j.properties zoo.cfg zoo_sample.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.
dataDir=/tmp/zookeeper/data
dataLogDir=/tmp/zookeeper/log
# the port at which the clients will connect
clientPort=2181
tickTime:配置单元时间。单元时间是ZooKeeper的时间计算单元,其他的时间间隔都是使用tickTime的倍数来表示的。
initLimit:节点的初始化时间。该参数用于Follower(从节点)的启动,并完成与Leader(主节点)进行数据同步的时间。Follower节点在启动过程中,会与Leader节点建立连接并完成对数据的同步,从而确定自己的起始状态。Leader节点允许Follower节点在initLimit时间内完成这项工作。该参数默认值为10,表示是参数tickTime值的10倍。
syncLimit:心跳最大延迟周期。该参数用于配置Leader节点和Follower节点之间进行心跳检测的最大延时时间。在ZK集群运行的过程中,Leader节点会通过心跳检测来确定Follower节点是否存活。如果Leader节点在syncLimit时间内无法获取到Follower节点的心跳检测响应,那么Leader节点就会认为该Follower节点已经脱离了和自己的同步。该参数默认值为5,表示是参数tickTime值的5倍。
dataDir:是zookeeper持久化数据存放的目录。myid文件处于此目录下。
dataLogDir:日志目录选项,如果没有设置该参数,默认将使用和dataDir相同的设置。
clientPort:zookeeper监听客户端连接的端口,默认是2181。
zookeeper cli
使用 brew 安装,已经把 zookeeper bin 目录下的命令添加的系统中,因此在终端直接执行 zkCli
,就创建了一个 zk 客户端,连接 zk 服务。
使用压缩包安装,可以执行:
$ bin/zkCli.sh -server 127.0.0.1:2181
输入 help 命令(其实输入任何 zkCli 不能识别的命令,都会列出所有的命令),查看可用的命令:
对 znode 进行增删改查
创建节点 create
create [-s] [-e] [-c] [-t ttl] path [data] [acl]
-s
创建有序节点
如果在创建znode时,我们使用排序标志的话,ZooKeeper会在我们指定的 znode 名字后面增加一个数字。我们继续加入相同名字的znode时,这个数字会不断增加。这个序号的计数器是由这些排序znode的父节点来维护的。
-e
创建临时节点
znode有两种类型:ephemeral 和 persistent。在创建znode时,我们指定znode的类型,并且在之后不会再被修改。当创建znode的客户端的session结束后,ephemeral类型的znode将被删除。persistent类型的znode在创建以后,就与客户端没什么联系了,除非主动去删除它,否则他会一直存在。Ephemeral znode没有任何子节点。acl
在下面的《 ACL 操作》中详细介绍。
使用方法:
普通节点
[zk: localhost:2181(CONNECTED) 3] create /mynode hello
Created /mynode
[zk: localhost:2181(CONNECTED) 4] create /mynode/subnode world
Created /mynode/subnode
[zk: localhost:2181(CONNECTED) 9] get /mynode
hello
[zk: localhost:2181(CONNECTED) 10] get /mynode/subnode
world
有序节点
[zk: localhost:2181(CONNECTED) 4] create -s /mynode hello
Created /mynode0000000004
[zk: localhost:2181(CONNECTED) 6] create -s /mynode world
Created /mynode0000000005
临时节点
[zk: localhost:2181(CONNECTED) 7] create -e /temp hello
Created /temp
退出 zkCli,然后再重新打开它,/temp 节点已经被删除了。
列出节点 ls
ls [-s] [-w] [-R] path
-w
添加一个 watch(监视器),如果该节点发生变化,watch 可以使客户端得到通知。watch 只能被触发一次。如果要一直获得 znode 的创建和删除的通知,那么就需要不断的在znode上开启观察模式。如果在该 path 下节点发生变化,会产生 NodeChildrenChanged
事件,删除节点,会产生 NodeDeleted
事件。
使用方法:
[zk: localhost:2181(CONNECTED) 12] ls /
[mynode, mynode0000000003, mynode0000000004, test, zookeeper]
[zk: localhost:2181(CONNECTED) 13] ls -s /
[mynode, mynode0000000003, mynode0000000004, test, zookeeper]
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x300000053
cversion = 7
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 5
[zk: localhost:2181(CONNECTED) 2] ls /mynode
[subnode]
使用 -w
查看 /mynode 节点,然后在它下面添加(删除)子节点,就会触发该 watch。在其他节点下创建子节点,不会触发该 watch。
[zk: localhost:2181(CONNECTED) 20] ls -w /mynode
[subnode]
[zk: localhost:2181(CONNECTED) 21] create /mynode/subnode2
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/mynode
Created /mynode/subnode2
# 监听父节点,删除子节点,产生 NodeChildrenChanged事件
[zk: localhost:2181(CONNECTED) 22] ls -w /mynode
[subnode, subnode2]
[zk: localhost:2181(CONNECTED) 23] delete /mynode/subnode2
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/mynode
# 监听子节点,删除子节点,产生 NodeDeleted 事件
[zk: localhost:2181(CONNECTED) 51] create /mynode/subnode2
Created /mynode/subnode2
[zk: localhost:2181(CONNECTED) 52] ls -w /mynode/subnode2
[]
[zk: localhost:2181(CONNECTED) 53] delete /mynode/subnode2
WATCHER::
WatchedEvent state:SyncConnected type:NodeDeleted path:/mynode/subnode2
从上面的操作可以看到,在 /mynode 下添加了 subnode2 节点之后,触发了 watch,WatchedEvent 的类型是 NodeChildrenChanged
。之后再删除 subnode2 节点,也出发了 watch。
获取节点信息 get
get [-s] [-w] path
-w
添加一个 watch(监视器),如果节点内容发生改变,会产生 NodeDataChanged
事件;如果删除节点,会产生 NodeDeleted
事件。
使用方法
[zk: localhost:2181(CONNECTED) 20] ls -w /mynode
[subnode]
[zk: localhost:2181(CONNECTED) 21] create /mynode/subnode2
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/mynode
Created /mynode/subnode2
# 监听父节点,删除子节点,产生 NodeChildrenChanged事件
[zk: localhost:2181(CONNECTED) 22] ls -w /mynode
[subnode, subnode2]
[zk: localhost:2181(CONNECTED) 23] delete /mynode/subnode2
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/mynode
# 监听子节点,删除子节点,产生 NodeDeleted 事件
[zk: localhost:2181(CONNECTED) 51] create /mynode/subnode2
Created /mynode/subnode2
[zk: localhost:2181(CONNECTED) 52] ls -w /mynode/subnode2
[]
[zk: localhost:2181(CONNECTED) 53] delete /mynode/subnode2
WATCHER::
WatchedEvent state:SyncConnected type:NodeDeleted path:/mynode/subnode2
每一个对znode树的更新操作,都会被赋予一个全局唯一的ID,我们称之为zxid
(ZooKeeper Transaction ID)。更新操作的ID按照发生的时间顺序升序排序。例如,z1大于z2,那么z1的操作就早于z2操作。
每个 znode 的状态信息包含以下内容:
czxid,创建(create)该 znode 的 zxid
mzxid,最后一次修改(modify)该 znode 的 zxid
pzxid,最后一次修改该 znode 子节点的 zxid
ctime,创建该 znode 的时间
mtime,最后一次修改该 znode 的时间
dataVersion,该节点内容的版本,每次修改内容,版本都会增加
cversion,该节点子节点的版本
aclVersion,该节点的 ACL 版本
ephemeralOwner,如果该节点是临时节点(ephemeral node),会列出该节点所在客户端的 session id;如果不是临时节点,该值为 0
dataLength,该节点存储的数据长度
numChildren,该节点子节点的个数
检查状态 stat
stat [-w] path
-w
添加一个 watch(监视器),如果节点内容发生改变,会产生 NodeDataChanged
事件;如果删除节点,会产生 NodeDeleted
事件。
与 get 的区别是,不回列出 znode 的值。
使用方法
[zk: localhost:2181(CONNECTED) 56] stat /mynode
cZxid = 0x30000004c
ctime = Sun Apr 05 15:48:14 CST 2020
mZxid = 0x30000005e
mtime = Sun Apr 05 16:09:32 CST 2020
pZxid = 0x300000067
cversion = 16
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
修改节点 set
set [-s] [-v version] path data
修改已经存在的节点的值
使用方法
[zk: localhost:2181(CONNECTED) 57] set /mynode hello
[zk: localhost:2181(CONNECTED) 58] ls /mynode
[]
[zk: localhost:2181(CONNECTED) 59] stat /mynode
cZxid = 0x30000004c
ctime = Sun Apr 05 15:48:14 CST 2020
mZxid = 0x300000068
mtime = Sun Apr 05 16:20:34 CST 2020
pZxid = 0x300000067
cversion = 16
dataVersion = 3
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
可以看到,在修改节点值之后,mZxid、mtime、dataVersion 都发生了变化。
删除节点 deleteall
deleteall path [-b batch size]
使用方法
[zk: localhost:2181(CONNECTED) 34] delete /mynode
删除 /mynode,不会返回任何内容。如果有子节点的时候,都会删除。
删除节点 delete
delete [-v version] path
调用delete
和set
操作时,如果指定znode版本号,需要与当前的版本号匹配。如果版本号不匹配,操作将会失败。失败的原因可能是在我们提交之前,该znode已经被修改过了,版本号发生了增量变化。如果不指定版本号,就是直接操作最新版本的 znode。
使用方法
[zk: localhost:2181(CONNECTED) 15] create /mynode hello
Created /mynode
[zk: localhost:2181(CONNECTED) 16] delete /mynode
如果要删除的节点有子节点,不能删除
[zk: localhost:2181(CONNECTED) 33] create /mynode/sub sub
Created /mynode/sub
[zk: localhost:2181(CONNECTED) 34] delete /mynode
Node not empty: /mynode
其他指令
历史记录 history
history
列出最近的10条历史记录
[zk: localhost:2181(CONNECTED) 7] history
0 - history
1 - create /mynode hello
2 - ls /
3 - set /mynode worold
4 - get /mynode
5 - stat /mynode
6 - rmr /mynode
7 - history
重复之前的命令 redo
redo cmdno
根据 cmdno 重复之前的命令,cmdno 就是方括号里面最后的数字,每次执行命令都会自增。
[zk: localhost:2181(CONNECTED) 5] create /mynode hello
Created /mynode
[zk: localhost:2181(CONNECTED) 6] rmr /mynode
[zk: localhost:2181(CONNECTED) 7] redo 5
Created /mynode
是否输出 watch 事件(printwatches)
语法printwatches on|off
使用方法
[zk: localhost:2181(CONNECTED) 43] printwatches
printwatches is on
[zk: localhost:2181(CONNECTED) 44] ls /mynode 1
[sub]
[zk: localhost:2181(CONNECTED) 45] create /mynode/child child
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/mynode
Created /mynode/child
如果设置 printwatches off
,就看不到上面的 WATCHER 事件了。
关闭连接 close
close
[zk: localhost:2181(CONNECTED) 50] close
[zk: localhost:2181(CLOSED) 51]
[zk: localhost:2181(CLOSED) 52] ls /
Not connected
打开连接 connect
connect host:port
[zk: localhost:2181(CLOSED) 52] connect
[zk: localhost:2181(CONNECTING) 53]
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
[zk: localhost:2181(CONNECTED) 53]
指定 host:port 可以连接远程的 zk 服务。缺省的时候,会连接本地的 2181 端口。
退出连接 quit
quit
直接退出当前的 zkCli 命令行。
强制同步 sync
sync path
sync
方法会强制客户端所连接的服务器状态与leader的状态同步,这样在读取 path 的值就是最新的值了。
ACL 操作
一个znode中不仅包含了存储的数据,还有 ACL
(Access Control List)。znode的创建时,可以给它设置一个ACL(Access Control List),来决定谁可以对znode做哪些操作。
ACL 具有以下特点:
ZooKeeper的权限控制是基于每个znode节点的,需要对每个节点设置权限
每个znode支持设置多种权限控制方案和多个权限
子节点不会继承父节点的权限,客户端无权访问某节点,但可能可以访问它的子节点
ACL Permissions
ACL 权限 | ACL 简写 | 允许的操作 |
---|---|---|
CREATE | c | 创建子节点 |
READ | r | 获取节点的数据和它的子节点 |
WRITE | w | 设置节点的数据 |
DELETE | d | 删除子节点 (仅下一级节点) |
ADMIN | a | 设置 ACL 权限 |
权限相关命令
命令 | 语法 | 描述 |
---|---|---|
getAcl | getAcl [-s] path | 读取ACL权限 |
setAcl | setAcl [-s] [-v version] [-R] path acl | 设置ACL权限 |
addauth | addauth scheme auth | 添加认证用户 |
create | create [-s] [-e] path data acl | 创建节点时指明 ACL 权限 |
ACL Schemes
ZooKeeper内置了一些权限控制方案,可以用以下方案为每个节点设置权限:
方案 | 描述 |
---|---|
world | 只有一个用户:anyone,代表所有人(默认) |
ip | 使用IP地址认证 |
auth | 使用已添加认证的用户认证 |
digest | 使用“用户名:密码”方式认证 |
ACL是由鉴权方式、鉴权方式的ID和一个许可(permession)的集合组成。例如,我们想通过一个ip地址为10.0.0.1的客户端访问一个znode。那么,我们需要为znode设置一个ACL,鉴权方式使用IP鉴权方式,鉴权方式的ID为10.0.0.1,只允许读权限。那么 ACL 的格式就是:ip:10.0.0.1:w
world 方案
设置方式:setAcl
默认情况下时 world 方法,任何人有所有权限:
[zk: localhost:2181(CONNECTED) 6] getAcl /mynode
'world,'anyone
: cdrwa
[zk: localhost:2181(CONNECTED) 7] setAcl /mynode world:anyone:cdr
cZxid = 0x54a
ctime = Tue Apr 03 09:26:36 CST 2018
mZxid = 0x54a
mtime = Tue Apr 03 09:26:36 CST 2018
pZxid = 0x54a
cversion = 0
dataVersion = 0
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
[zk: localhost:2181(CONNECTED) 15] set /mynode hello
Authentication is not valid : /mynode
可以看出,在修改权限为 cdr 之后,不能再设置节点数据了。注意 aclVersion 也发生了变化。
IP 方案
设置方式:setAcl
:可以是具体IP也可以是IP/bit格式,即IP转换为二进制,匹配前bit位,如192.168.0.0/16
匹配192.168.*.*
[zk: localhost:2181(CONNECTED) 19] create /mynode hello
Created /mynode
[zk: localhost:2181(CONNECTED) 20] setAcl /mynode ip:192.168.1.250:cdrwa
cZxid = 0x552
ctime = Tue Apr 03 09:38:58 CST 2018
mZxid = 0x552
mtime = Tue Apr 03 09:38:58 CST 2018
pZxid = 0x552
cversion = 0
dataVersion = 0
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
[zk: localhost:2181(CONNECTED) 21] getAcl /mynode
'ip,'192.168.1.250
: cdrwa
使用其他电脑方法方法该节点:
#使用IP非 192.168.100.1 的机器
[zk: localhost:2181(CONNECTED) 0] get /node2
Authentication is not valid : /node2 #没有权限
[zk: localhost:2181(CONNECTED) 1] delete /node2 #删除成功(因为设置DELETE权限仅对下一级子节点有效,并不包含此节点)
auth 方案
设置方式
addauth digest : #添加认证用户
setAcl auth::
示例:
[zk: localhost:2181(CONNECTED) 22] create /mynode1 hello
Created /mynode1
[zk: localhost:2181(CONNECTED) 23] addauth digest admin:admin #添加认证用户
[zk: localhost:2181(CONNECTED) 24] setAcl /mynode1 auth:admin:cdrwa
cZxid = 0x554
ctime = Tue Apr 03 09:44:32 CST 2018
mZxid = 0x554
mtime = Tue Apr 03 09:44:32 CST 2018
pZxid = 0x554
cversion = 0
dataVersion = 0
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
[zk: localhost:2181(CONNECTED) 25] getAcl /mynode1
'digest,'admin:x1nq8J5GOJVPY6zgzhtTtA9izLc=
: cdrwa
[zk: localhost:2181(CONNECTED) 26] get /mynode1
hello #刚才已经添加认证用户,可以直接读取数据,断开会话重连需要重新addauth添加认证用户
cZxid = 0x554
ctime = Tue Apr 03 09:44:32 CST 2018
mZxid = 0x554
mtime = Tue Apr 03 09:44:32 CST 2018
pZxid = 0x554
cversion = 0
dataVersion = 0
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
digest 方案
设置方式
setAcl digest:::
这里的密码是经过SHA1及BASE64处理的密文,在SHELL中可以通过以下命令计算:
echo -n : | openssl dgst -binary -sha1 | openssl base64
先来算一个密文密码:
echo -n admin:admin | openssl dgst -binary -sha1 | openssl base64
x1nq8J5GOJVPY6zgzhtTtA9izLc=
示例:
[zk: localhost:2181(CONNECTED) 8] create /mynode2 hello
Created /mynode2
#使用是上面算好的密文密码添加权限:
[zk: localhost:2181(CONNECTED) 9] setAcl /mynode2
digest:admin:x1nq8J5GOJVPY6zgzhtTtA9izLc=:cdrwa
cZxid = 0x55a
ctime = Tue Apr 03 13:17:12 CST 2018
mZxid = 0x55a
mtime = Tue Apr 03 13:17:12 CST 2018
pZxid = 0x55a
cversion = 0
dataVersion = 0
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
[zk: localhost:2181(CONNECTED) 10] getAcl /mynode2
'digest,'admin:x1nq8J5GOJVPY6zgzhtTtA9izLc=
: cdrwa
[zk: localhost:2181(CONNECTED) 11] get /mynode2
#没有权限
Authentication is not valid : /mynode2
[zk: localhost:2181(CONNECTED) 12] addauth digest admin:admin #添加认证用户
[zk: localhost:2181(CONNECTED) 13] get /mynode2
hello #成功读取数据
cZxid = 0x55a
ctime = Tue Apr 03 13:17:12 CST 2018
mZxid = 0x55a
mtime = Tue Apr 03 13:17:12 CST 2018
pZxid = 0x55a
cversion = 0
dataVersion = 0
aclVersion = 1
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
创建节点时指定 ACL
#添加 admin 用户
[zk: localhost:2181(CONNECTED) 2] addauth digest admin:admin
#创建节点时赋予权限
[zk: localhost:2181(CONNECTED) 3] create /mynode hello auth:admin:cdrwa
Created /mynode
[zk: localhost:2181(CONNECTED) 4] getAcl /mynode
'digest,'admin:x1nq8J5GOJVPY6zgzhtTtA9izLc=
: cdrwa
[zk: localhost:2181(CONNECTED) 5] close
[zk: localhost:2181(CLOSED) 6] connect
[zk: localhost:2181(CONNECTING) 7]
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
#断开会话重连需要重新addauth添加认证用户
[zk: localhost:2181(CONNECTED) 7] get /mynode
Authentication is not valid : /mynode
[zk: localhost:2181(CONNECTED) 8] addauth digest admin:admin
[zk: localhost:2181(CONNECTED) 9] get /mynode
hello
cZxid = 0x56c
ctime = Tue Apr 03 15:00:27 CST 2018
mZxid = 0x56c
mtime = Tue Apr 03 15:00:27 CST 2018
pZxid = 0x56c
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
注意了!使用 rmr 删除节点没有权限时,竟然可以使用 delete
[zk: localhost:2181(CONNECTED) 25] rmr /mynode
Authentication is not valid : /mynode
[zk: localhost:2181(CONNECTED) 26] delete /mynode
zookeeper quota
zookeeper quota 机制支持节点个数(namespace)和空间大小(bytes)的设置。
zookeeper quota 保存在 /zookeeper/quota
节点下,可以设置该节点的 ACL 权限,以防其他人修改。
语法:listquota path
setquota -n|-b val path
delquota [-n|-b] path
使用方法:
# 目前还没有任何设置
[zk: localhost:2181(CONNECTED) 9] ls /zookeeper/quota
[]
# 还没有为 /mynode 设置 quota
[zk: localhost:2181(CONNECTED) 10] listquota /mynode
absolute path is /zookeeper/quota/mynode/zookeeper_limits
quota for /mynode does not exist.
# -n表示设置znode count限制,这里表示/mynode这个path下的znode count个数限制为3(包括/mynode本身)
[zk: localhost:2181(CONNECTED) 11] setquota -n 3 /mynode
Comment: the parts are option -n val 3 path /mynode
[zk: localhost:2181(CONNECTED) 12] create /mynode/sub1 hello
Created /mynode/sub1
[zk: localhost:2181(CONNECTED) 9] listquota /mynode
absolute path is /zookeeper/quota/mynode/zookeeper_limits
Output quota for /mynode count=3,bytes=-1
Output stat for /mynode count=2,bytes=6
注意,即使节点数超出了限制,也不会看到提示信息,zookeeper 只会在日志中提醒一下。
使用 listquota 列出了节点的设置的 quota,和节点实际的容量。
[zk: localhost:2181(CONNECTED) 20] delquota -n /mynode
[zk: localhost:2181(CONNECTED) 21] listquota /mynode
absolute path is /zookeeper/quota/mynode/zookeeper_limits
Output quota for /mynode count=-1,bytes=-1
Output stat for /mynode count=2,bytes=6
删除 quota 之后,count 也变成了 -1
至此,Zookeeper客户端所有的命令介绍完毕!
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:
http://blog.csdn.net/feixiang2039/article/details/79810102
感谢点赞支持下哈