下載Zookeeper
筆者這里不對(duì)下載部分做過(guò)多贅述评凝,從官網(wǎng)下載何時(shí)版本的包解壓即可。其中bin目錄中含有zk的啟動(dòng)腳本腺律,conf中則是啟動(dòng)所需的配置文件奕短,lib目錄則是java的jar文件。
第一個(gè)zookeeper會(huì)話
初學(xué)zookeeper匀钧,我們使用bin目錄下的zkServer和zkClient工具進(jìn)行簡(jiǎn)單的調(diào)試和管理翎碑。
筆者使用的版本是3.4.10,conf目錄下的zoo.cfg是zk默認(rèn)的配置文件之斯,zoo_sample則是包含了更多的配置及其含義的注釋日杈,為了簡(jiǎn)便,我們直接使用zoo.cfg來(lái)啟動(dòng)zk服務(wù)佑刷。
localhost:bin wz$ sudo ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /Users/wz/Developement/zookeeper-3.4.10/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
這個(gè)指令可以讓zk服務(wù)在后臺(tái)運(yùn)行莉擒,如果需要在前臺(tái)運(yùn)行以便查看服務(wù)器輸出,可以通過(guò)以下命令瘫絮。
sudo ./zkServer.sh start-foreground
好了涨冀,啟動(dòng)好了服務(wù)端,接著我們啟動(dòng)一個(gè)客戶端麦萤。
./zkCli.sh
......
2018-06-05 21:11:46,487 [myid:] - INFO [main:ZooKeeper@438] - Initiating client connection, connectString=localhost:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@67424e82 ①
Welcome to ZooKeeper!
2018-06-05 21:11:46,511 [myid:] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@1032] - Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
JLine support is enabled ②
2018-06-05 21:11:46,566 [myid:] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@876] - Socket connection established to localhost/127.0.0.1:2181, initiating session ③
2018-06-05 21:11:46,595 [myid:] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@1299] - Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x163d00dddef0000, negotiated timeout = 30000 ④
WATCHER::
WatchedEvent state:SyncConnected type:None path:null ⑤
下面我們逐個(gè)分析這幾行日志鹿鳖,其實(shí)就是會(huì)話建立的過(guò)程信息。
① 客戶端啟動(dòng)壮莹,開始建立會(huì)話
② 客戶端嘗試連接到localhost/127.0.0.1:2181
③ 連接成功建立翅帜,開始初始化會(huì)話
④ 會(huì)話初始化完成
⑤ 服務(wù)端向客戶端發(fā)送一個(gè)state:SyncConnected事件,會(huì)話建立完成命满,id為 0x163d00dddef0000
客戶端需要實(shí)現(xiàn)Watcher對(duì)象來(lái)處理這個(gè)事件涝滴。
接下來(lái)我們劣列出根節(jié)點(diǎn)下的所有znode,然后嘗試創(chuàng)建一個(gè)znode胶台。
[zk: localhost:2181(CONNECTED) 3] ls /
[zookeeper]
只有zookeeper節(jié)點(diǎn)狭莱,其中包含了zk服務(wù)所需要的元數(shù)據(jù)樹,這里不多贅述概作,下面我們新建一個(gè)workers znode腋妙。
[zk: localhost:2181(CONNECTED) 3] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 4] create /workers ""
Created /workers
[zk: localhost:2181(CONNECTED) 5] ls /
[zookeeper, workers]
這里創(chuàng)建時(shí)我們指定了一個(gè)空串,代表此時(shí)這個(gè)znode中不保存數(shù)據(jù)讯榕,當(dāng)然你也可以把""替換為"workers"或是任意內(nèi)容骤素。
然后我們刪除znode匙睹,停止這個(gè)會(huì)話,這樣就完成了這第一個(gè)小實(shí)驗(yàn)济竹。
[zk: localhost:2181(CONNECTED) 6] delete /workers
[zk: localhost:2181(CONNECTED) 7] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 8] quit
Quitting...
2018-06-05 21:45:24,904 [myid:] - INFO [main:ZooKeeper@684] - Session: 0x163d00dddef0000 closed
2018-06-05 21:45:24,906 [myid:] - INFO [main-EventThread:ClientCnxn$EventThread@519] - EventThread shut down for session: 0x163d00dddef0000
會(huì)話的狀態(tài)和生命周期
一個(gè)zookeeper會(huì)話的狀態(tài)轉(zhuǎn)換大致如下圖所示
一個(gè)會(huì)話從NOT_CONNECTED開始痕檬,當(dāng)客戶端初始化連接完成時(shí)轉(zhuǎn)到CONNECTING狀態(tài),連接成功建立后會(huì)轉(zhuǎn)到CONNECTED狀態(tài)送浊。倘若此時(shí)服務(wù)器斷開連接或者無(wú)法收到服務(wù)器的響應(yīng)時(shí)梦谜,就會(huì)轉(zhuǎn)會(huì)CONNECTING狀態(tài)(箭頭3),并嘗試重新連接或發(fā)現(xiàn)其他zk服務(wù)器袭景,如果發(fā)現(xiàn)一個(gè)服務(wù)器或重連成功唁桩,狀態(tài)就會(huì)重新回到CONNECTED,否則耸棒,會(huì)話過(guò)期荒澡,轉(zhuǎn)到CLOSED狀態(tài)(箭頭4)。當(dāng)然与殃,應(yīng)用也可以顯示關(guān)閉會(huì)話(箭頭5)单山。
注意:
如果一個(gè)客戶端因超時(shí)與服務(wù)端斷開連接,客戶端仍然保持CONNECTING狀態(tài)幅疼,此時(shí)倘若因?yàn)榫W(wǎng)絡(luò)分區(qū)錯(cuò)誤導(dǎo)致客戶端與服務(wù)端之間連接不可達(dá)米奸,那么其狀態(tài)會(huì)一直保持,直到顯示的關(guān)閉這個(gè)會(huì)話或者問(wèn)題修復(fù)后客戶端悉知會(huì)話已過(guò)期爽篷。這是因?yàn)闀?huì)話的超時(shí)由服務(wù)集群來(lái)控制悴晰,客戶端無(wú)法控制。直到客戶端獲悉會(huì)話超時(shí)狼忱,否則不能聲明自己的回話過(guò)期膨疏,但是客戶端可以顯示關(guān)閉會(huì)話一睁。
因此钻弄,我們需要設(shè)置會(huì)話過(guò)期時(shí)間這個(gè)參數(shù),如果經(jīng)過(guò)t時(shí)間服務(wù)接收不到會(huì)話的消息者吁,就會(huì)聲明這個(gè)會(huì)話過(guò)期窘俺。在客戶端側(cè),如果經(jīng)過(guò)t/3時(shí)間后沒(méi)有收到消息复凳,就會(huì)向服務(wù)器發(fā)送心跳消息瘤泪。經(jīng)過(guò)2t/3時(shí)間后會(huì)開始尋找其他服務(wù)器,如果在剩下t/3時(shí)間內(nèi)無(wú)法找到育八,就會(huì)被聲明會(huì)話過(guò)期对途。
當(dāng)客戶端嘗試連接到一個(gè)不同的服務(wù)器時(shí),需要保證這個(gè)服務(wù)的狀態(tài)要與最后連接的服務(wù)器狀態(tài)一致髓棋,如果某個(gè)服務(wù)獲悉狀態(tài)變更的時(shí)間點(diǎn)延遲于客戶端实檀,那就要保證這個(gè)服務(wù)不會(huì)被鏈接惶洲。zk通過(guò)在服務(wù)中排序更新操作發(fā)發(fā)生的事件來(lái)確保這種情況,如果客戶端再位置i觀察到一個(gè)更新膳犹,那他就不能連接到只觀察到i之前狀態(tài)的服務(wù)恬吕。這個(gè)過(guò)程如下圖所示。
zookeeper與仲裁模式
上面我們都是基于獨(dú)立模式進(jìn)行的實(shí)驗(yàn)须床,這在實(shí)際環(huán)境中肯定是非常不靠譜的铐料,如果服務(wù)器故障那整個(gè)zk服務(wù)都將關(guān)閉。下面我們通過(guò)在一臺(tái)機(jī)器上運(yùn)行多個(gè)zk服務(wù)器來(lái)演示仲裁模式豺旬。
首先將配置文件修改如下:
tickTime=2000
initLimit=10
syncLimit=5
dataDir=./data
clientPort=2181
server.1=127.0.0.1:2222:2223
server.2=127.0.0.1:3333:3334
server.3=127.0.0.1:4444:4445
其中server.n指定了編號(hào)為n的服務(wù)器所使用的地址和端口钠惩,每個(gè)server.n通過(guò)冒號(hào)分隔為三部分,第一部分為服務(wù)器主機(jī)名哈垢,第二部分和第三部分為TCP端口號(hào)妻柒,分別用于仲裁和群首選舉。
接著我們還需要為每個(gè)服務(wù)分別設(shè)置data目錄
localhost:bin wz$ mkdir z1
localhost:bin wz$ mkdir z2
localhost:bin wz$ mkdir z3
localhost:bin wz$ mkdir z1/data
localhost:bin wz$ mkdir z2/data
localhost:bin wz$ mkdir z3/data
當(dāng)一個(gè)服務(wù)器啟動(dòng)時(shí)耘分,需要知道啟動(dòng)的是哪個(gè)服務(wù)器举塔,通過(guò)讀取data目錄下一個(gè)名為myid的文件來(lái)獲取服務(wù)器ID信息,我們可以通過(guò)以下命令創(chuàng)建這些文件:
localhost:bin wz$ echo 1 > z1/data/myid
localhost:bin wz$ echo 2 > z2/data/myid
localhost:bin wz$ echo 3 > z3/data/myid
接著我們分別創(chuàng)建每臺(tái)服務(wù)器的配置文件求泰,根據(jù)上面的配置新建z1.cfg央渣,然后修改端口為2181和2183創(chuàng)建z2/z3.cfg。
現(xiàn)在我們可以啟動(dòng)服務(wù)器渴频,先從z1開始
sudo ./zkServer.sh start-foreground ./z1/z1.cfg
此時(shí)服務(wù)器瘋狂嘗試連接到其他服務(wù)器芽丹,報(bào)錯(cuò)如下:
018-06-07 22:51:26,169 [myid:1] - WARN [WorkerSender[myid=1]:QuorumCnxManager@588] - Cannot open channel to 2 at election address /127.0.0.1:3334
java.net.ConnectException: Connection refused (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:562)
at org.apache.zookeeper.server.quorum.QuorumCnxManager.toSend(QuorumCnxManager.java:538)
at org.apache.zookeeper.server.quorum.FastLeaderElection$Messenger$WorkerSender.process(FastLeaderElection.java:452)
接著我們啟動(dòng)第二臺(tái)服務(wù)器,我們會(huì)看到以下日志
2018-06-07 22:51:42,699 [myid:2] - INFO [QuorumPeer[myid=2]/0:0:0:0:0:0:0:0:2182:Leader@371] - LEADING - LEADER ELECTION TOOK - 250
該日志指出服務(wù)器2已被選舉為群首卜朗,再看服務(wù)器1的日志
2018-06-07 22:51:42,683 [myid:1] - INFO [QuorumPeer[myid=1]/0:0:0:0:0:0:0:0:2181:Follower@64] - FOLLOWING - LEADER ELECTION TOOK - 16517
該服務(wù)器作為服務(wù)器2的追隨者被激活拔第,現(xiàn)在我們并沒(méi)有啟動(dòng)服務(wù)器3,也就是說(shuō)此時(shí)構(gòu)成了允許執(zhí)行的最小數(shù)目(參見一篇博客)场钉。
此刻服務(wù)已經(jīng)可用蚊俺,現(xiàn)在我們啟動(dòng)一個(gè)客戶端來(lái)連接到服務(wù)上,連接字符串需要列出所有組成服務(wù)的服務(wù)器host:port對(duì)逛万。這個(gè)例子中泳猬,這個(gè)連接串為
"127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183"(這里我們包含了第三臺(tái)服務(wù)器的信息,但我們并沒(méi)有啟動(dòng)它宇植,放在這里僅為了說(shuō)明zk的一些屬性)
接下來(lái)使用zkCli來(lái)訪問(wèn)這個(gè)集群:
./zkCli.sh -server 127.0.0.1:2181,127.0.0.2:2182,127.0.0.1:2183
連接成功后得封,你會(huì)看到如下日志
2018-06-07 23:09:16,507 [myid:] - INFO [main-SendThread(127.0.0.1:2181):ClientCnxn$SendThread@1299] - Session establishment complete on server 127.0.0.1/127.0.0.1:2181, sessionid = 0x163dabb62e60001, negotiated timeout = 30000
如果你通過(guò)多次ctrl+c停止并重連,就會(huì)發(fā)現(xiàn)端口號(hào)在2181和2182之間變化指郁,也會(huì)看到因?yàn)閲L試連接2183而報(bào)錯(cuò)的日志忙上,之后又成功連接到某一個(gè)服務(wù)。
一個(gè)主從模式例子的實(shí)現(xiàn)
主從模式的模型包括三個(gè)角色:
- 主節(jié)點(diǎn)闲坎,主要負(fù)責(zé)監(jiān)視新的從節(jié)點(diǎn)和任務(wù)疫粥,進(jìn)行任務(wù)的分配洋腮。
- 從節(jié)點(diǎn), 通過(guò)系統(tǒng)注冊(cè)自己以便可以被主節(jié)點(diǎn)所監(jiān)控手形,然后開始監(jiān)視新的任務(wù)啥供。
- 客戶端,創(chuàng)建新任務(wù)并等待服務(wù)端響應(yīng)库糠。
因?yàn)橹挥幸粋€(gè)進(jìn)程會(huì)成為主節(jié)點(diǎn)伙狐,所以一旦有一個(gè)進(jìn)程成為主節(jié)點(diǎn)后就必須鎖定管理權(quán),為此瞬欧,我們先創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn)/master贷屎。
[zk: localhost:2181(CONNECTED) 0] create -e /master "master1.example.com:2223"
Created /master
[zk: localhost:2181(CONNECTED) 1] ls /
[zookeeper, master]
[zk: localhost:2181(CONNECTED) 2] get /master
master1.example.com:2223
cZxid = 0xf0a
ctime = Tue Jun 12 22:35:20 CST 2018
mZxid = 0xf0a
mtime = Tue Jun 12 22:35:20 CST 2018
pZxid = 0xf0a
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x163f1627cc3000e
dataLength = 24
numChildren = 0
以上我們創(chuàng)建主節(jié)點(diǎn)znode,使用 -e標(biāo)注創(chuàng)建的znode為臨時(shí)性的艘虎。之后列出了zookeeper樹的根唉侄。最后獲取了/master這個(gè)znode的數(shù)據(jù)和元數(shù)據(jù)。
主節(jié)點(diǎn)建立后野建,倘若其他進(jìn)程不知道一個(gè)主節(jié)點(diǎn)已被選舉出來(lái)属划,嘗試重復(fù)創(chuàng)建/master,zk會(huì)告訴我們一個(gè)/master節(jié)點(diǎn)已經(jīng)存在候生。但是主節(jié)點(diǎn)隨時(shí)可能崩潰同眯,為了讓其他節(jié)點(diǎn)能夠接替主節(jié)點(diǎn)的角色,需要在/master上設(shè)置一個(gè)監(jiān)視點(diǎn)唯鸭。
[zk: localhost:2181(CONNECTED) 4] stat /master true
cZxid = 0xf0a
ctime = Tue Jun 12 22:35:20 CST 2018
mZxid = 0xf0a
mtime = Tue Jun 12 22:35:20 CST 2018
pZxid = 0xf0a
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x163f1627cc3000e
dataLength = 24
numChildren = 0
stat命令可以得到一個(gè)znode的屬性须蜗,并允許我們?cè)O(shè)置一個(gè)監(jiān)視點(diǎn)。通過(guò)路徑后添加true來(lái)添加監(jiān)視點(diǎn)目溉,這樣以來(lái)明肮,當(dāng)主節(jié)點(diǎn)會(huì)話結(jié)束過(guò)崩潰時(shí),我們就可以收到一個(gè)NodeDeleted事件缭付。此時(shí)備份節(jié)點(diǎn)就可以創(chuàng)建/master成為新的活動(dòng)主節(jié)點(diǎn)柿估。
從節(jié)點(diǎn)的任務(wù)和分配
開始之前,我們先創(chuàng)建3個(gè)持久性父znode, /workers, /tasks以及/assign蛉腌。他們不包含任何數(shù)據(jù)官份,用來(lái)告訴主節(jié)點(diǎn)有哪些節(jié)點(diǎn)可以接受任務(wù)只厘,哪些任務(wù)要分配烙丛,并向從節(jié)點(diǎn)分配任務(wù)。
[zk: localhost:2181(CONNECTED) 5] create /workers ""
Created /workers
[zk: localhost:2181(CONNECTED) 6] create /tasks ""
Created /tasks
[zk: localhost:2181(CONNECTED) 7] create /assign ""
Created /assign
[zk: localhost:2181(CONNECTED) 8] ls /workers true
[]
[zk: localhost:2181(CONNECTED) 9] ls /tasks true
[]
通過(guò)ls的可選參數(shù)true羔味,來(lái)設(shè)置對(duì)這個(gè)znode子節(jié)點(diǎn)變化的監(jiān)視點(diǎn)河咽。
好了,從節(jié)點(diǎn)現(xiàn)在首先要通知主節(jié)點(diǎn)赋元,自己可以執(zhí)行任務(wù)忘蟹。通過(guò)在/workers子節(jié)點(diǎn)下創(chuàng)建臨時(shí)性的znode來(lái)進(jìn)行通知飒房,并用主機(jī)名表示自己。
create -e /workers/worker1.example.com "worker1.example.com:2224"
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/workers
Created /workers/worker1.example.com
一單創(chuàng)建成功媚值,主節(jié)點(diǎn)就會(huì)收到type:NodeChildrenChanged的通知狠毯。
下一步,從節(jié)點(diǎn)需要?jiǎng)?chuàng)建一個(gè)znode /assign/worker1.example.com來(lái)接受任務(wù)分配褥芒,
并監(jiān)視這個(gè)節(jié)點(diǎn)的變化:
[zk: localhost:2181(CONNECTED) 11] create -e /assign/worker1.example.com ""
Created /assign/worker1.example.com
[zk: localhost:2181(CONNECTED) 12] ls /assign/worker1.example.com true
[]
這樣從節(jié)點(diǎn)就已經(jīng)準(zhǔn)備就緒嚼松,可以接受任務(wù)分配。接下來(lái)我們通過(guò)討論客戶端角色來(lái)看一下任務(wù)分配的問(wèn)題锰扶。
假設(shè)客戶端提交了一個(gè)請(qǐng)求主從系統(tǒng)來(lái)運(yùn)行cmd的任務(wù)献酗,客戶端執(zhí)行一下操作
create -s /tasks/task- "cmd"
因?yàn)樾枰WC任務(wù)執(zhí)行順序,所以這里是一個(gè)有序隊(duì)列坷牛,使用-s創(chuàng)建順序節(jié)點(diǎn)罕偎。負(fù)責(zé)執(zhí)行該任務(wù)的節(jié)點(diǎn)執(zhí)行完成后會(huì)在此節(jié)點(diǎn)下創(chuàng)建一個(gè)新的節(jié)點(diǎn)表示任務(wù)的狀態(tài),因此客戶端需要監(jiān)視這個(gè)節(jié)點(diǎn)京闰,同樣的颜及,使用ls的參數(shù)true來(lái)設(shè)置監(jiān)視點(diǎn)
ls /tasks/task-0000000000 true
之前我們已經(jīng)設(shè)置了主節(jié)點(diǎn)對(duì)于/task節(jié)點(diǎn)的監(jiān)視,所以這里一單創(chuàng)建成功蹂楣,我們就會(huì)監(jiān)視到以下事件:
[zk: localhost:2181(CONNECTED) 6]
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/tasks
主節(jié)點(diǎn)接著會(huì)檢查這個(gè)新任務(wù)器予,獲取可處理任務(wù)的節(jié)點(diǎn)列表,之后分配給work1.example.com
[zk: 6] ls /tasks
[task-0000000000]
[zk: 7] ls /workers
[worker1.example.com]
[zk: 8] create /assign/worker1.example.com/task-0000000000 ""
Created /assign/worker1.example.com/task-0000000000
[zk: 9]
接下來(lái)從節(jié)點(diǎn)會(huì)獲取到新增任務(wù)的通知:
[zk: localhost:2181(CONNECTED) 3]
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged
path:/assign/worker1.example.com
之后從節(jié)點(diǎn)會(huì)再次檢查任務(wù)是否分配給自己
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged
path:/assign/worker1.example.com
[zk: localhost:2181(CONNECTED) 3] ls /assign/worker1.example.com
[task-0000000000]
[zk: localhost:2181(CONNECTED) 4]
一旦從節(jié)點(diǎn)完成任務(wù)捐迫,就會(huì)向/task/task-0000000000中添加一個(gè)狀態(tài)節(jié)點(diǎn)status
[zk: localhost:2181(CONNECTED) 4] create /tasks/task-0000000000/status "done"
Created /tasks/task-0000000000/status
[zk: localhost:2181(CONNECTED) 5]
之后客戶端收到通知乾翔,檢查結(jié)果:
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged
path:/tasks/task-0000000000
[zk: localhost:2181(CONNECTED) 2] get /tasks/task-0000000000
"cmd"
cZxid = 0x7c
ctime = Tue Dec 11 10:30:18 CET 2012
mZxid = 0x7c
mtime = Tue Dec 11 10:30:18 CET 2012
pZxid = 0x7e
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 1
[zk: localhost:2181(CONNECTED) 3] get /tasks/task-0000000000/status
"done"
cZxid = 0x7e
ctime = Tue Dec 11 10:42:41 CET 2012
mZxid = 0x7e
mtime = Tue Dec 11 10:42:41 CET 2012
pZxid = 0x7e
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 0
[zk: localhost:2181(CONNECTED) 4]
小結(jié)
這次我們通過(guò)例子了解了很多zk的基礎(chǔ)以及api的使用,盡管實(shí)際在分布式系統(tǒng)中可能復(fù)雜數(shù)倍施戴,但本質(zhì)上是相似的反浓。通過(guò)對(duì)仲裁模式中主從節(jié)點(diǎn)通訊過(guò)程的演示,相信你對(duì)zk的基本原理已經(jīng)有一些理解赞哗,本文中的演示主要使用的都是zkcli這個(gè)命令行工具雷则,它更多的是為了學(xué)習(xí)和演示,下一章我們將直接使用JAVA來(lái)實(shí)現(xiàn)一些例子肪笋。