總結:分布式鎖、服務發(fā)現(xiàn)與配置中心落地指南)
接手一個需要多服務協(xié)作的項目時我踩過的第一個大坑就是所有節(jié)點各干各的沒有一套統(tǒng)一的協(xié)調(diào)機制。后來我把ZooKeeper引入整個架構先后解決了配置同步、服務上下線感知和分布式鎖這幾個最頭疼的問題。這篇文章就是我基于實際項目做的階段性總結寫給正在學Java分布式、并且準備把ZooKeeper用起來的開發(fā)者尤其是那些剛看完理論、想知道怎么落地的朋友。我會從ZooKeeper的定位講起帶著你理解它的數(shù)據(jù)模型和會話機制然后給出Java客戶端選型的關鍵對比再手把手拆解節(jié)點CRUD、Watch監(jiān)聽、分布式鎖和服務發(fā)現(xiàn)這些高頻實戰(zhàn)場景最后聊幾個我親身踩過的坑。整個內(nèi)容不求面面俱到但保證每一個步驟和代碼示例都能讓你直接抄作業(yè)改一改就能跑。1. 為什么分布式系統(tǒng)里需要ZooKeeper這個協(xié)調(diào)員分布式系統(tǒng)的難點往往不在功能實現(xiàn)而在多個進程之間怎么達成一致。我剛接手項目X的時候服務數(shù)量從幾臺擴展到幾十臺馬上就遇到三個問題配置文件散落在各節(jié)點上、改了配置要一臺臺重啟、服務掛了其他調(diào)用方感知不到。這些問題的共性就是缺少一個可以讓所有節(jié)點共同依賴的協(xié)調(diào)者。1.1 沒有協(xié)調(diào)框架時的混亂場景舉個例子項目X里有三個服務需要共享一份DB開關配置。我最初的做法是把配置寫在每個服務本地的properties文件里運維改配置時用腳本批量推送。聽起來簡單但實際會發(fā)生兩種情況推送時部分節(jié)點成功、部分節(jié)點失敗失敗節(jié)點繼續(xù)跑舊邏輯新邏輯和舊邏輯并行。服務擴容時新拉起來的機器如果配置文件不是最新版會帶著舊配置進入生產(chǎn)環(huán)境。更麻煩的是分布式鎖。早期我用數(shù)據(jù)庫的唯一索引做鎖通過insert一條記錄來占用鎖最后delete釋放。在高并發(fā)下數(shù)據(jù)庫連接會被打滿而且鎖的續(xù)租、異常釋放、死鎖檢測全部要自己寫。后來我決定徹底改造用ZooKeeper把這些分散的麻煩事統(tǒng)一收攏。1.2 ZooKeeper到底替我們解決了什么問題ZooKeeper在架構里扮演的角色簡單說就是一個帶強一致性的分布式文件系統(tǒng)同時具備通知機制。它讓你可以像操作目錄一樣存數(shù)據(jù)又能在數(shù)據(jù)變化時主動告訴訂閱者。如果把這個模型放到業(yè)務場景里可以看到幾個核心能力統(tǒng)一配置存儲配置放ZooKeeper節(jié)點上集中修改所有客戶端實時收到變更通知。服務注冊和發(fā)現(xiàn)服務啟動時注冊臨時節(jié)點服務掛掉后節(jié)點自動消失調(diào)用方通過Watch感知上下線。分布式鎖利用ZooKeeper的臨時順序節(jié)點多個客戶端可以排隊獲取鎖避免了單點故障。這三類能力基本覆蓋了中小團隊80%的協(xié)調(diào)需求。ZooKeeper之所以能承擔這些職責是因為它背后的Zab協(xié)議保證了寫入順序的一致性和高可用。集群內(nèi)部通過選舉產(chǎn)生Leader所有寫請求都交給Leader廣播給Follower超過半數(shù)確認后返回成功保證數(shù)據(jù)不會因為節(jié)點故障而分叉。1.3 它不是萬能存儲適用邊界要先劃清楚我在團隊內(nèi)部做技術分享時總強調(diào)一句話不要把ZooKeeper當數(shù)據(jù)庫用。它默認單個節(jié)點的數(shù)據(jù)大小限制通常是1MB不適合存業(yè)務大對象。它更適合存配置項、元數(shù)據(jù)、鎖標識這類小而關鍵的信息。如果你發(fā)現(xiàn)某個ZNode里的數(shù)據(jù)超過幾十KB就要停下來想想是不是設計有問題。ZooKeeper的強順序一致性也是有時機代價的寫請求需要多數(shù)派確認所以吞吐量跟Redis沒法比。業(yè)務向的高頻寫操作建議放RedisZooKeeper只做協(xié)調(diào)類數(shù)據(jù)。把邊界劃清楚后面用起來才不別扭。2. 數(shù)據(jù)模型與會話機制入門ZooKeeper前必須搞懂的四個核心概念跳過這些底層概念直接寫代碼很容易遇到各種摸不著頭腦的問題。我見過不少同事在寫節(jié)點操作時搞不清持久節(jié)點和臨時節(jié)點的區(qū)別結果服務重啟后舊節(jié)點還掛在線上服務注冊信息一直重復。所以我想先把四個核心概念講透。2.1 樹形數(shù)據(jù)模型ZNode不是傳統(tǒng)的文件ZooKeeper的命名空間是一棵類似文件系統(tǒng)的樹每個節(jié)點叫ZNode。路徑比如 /config、/config/db_switch寫法上和Linux目錄一樣訪問某個ZNode就是直接按路徑定位。與傳統(tǒng)文件系統(tǒng)不同ZNode既可以有子節(jié)點本身也可以存數(shù)據(jù)這一點要特別注意。每個ZNode的數(shù)據(jù)量很小實際操作時一般存JSON字符串或特定業(yè)務標識。ZNode上不僅有數(shù)據(jù)還附帶一系列元信息比如版本號、創(chuàng)建時間、數(shù)據(jù)長度。這里最關鍵的一個元信息是版本號因為ZooKeeper的并發(fā)改造成靠版本號來實現(xiàn)類似樂觀鎖的機制。2.2 節(jié)點類型持久、臨時、順序節(jié)點各自的使用場景ZooKeeper把ZNode分成幾種類型每個類型都對應一個典型業(yè)務場景節(jié)點類型特點典型用途持久節(jié)點客戶端斷開后節(jié)點依然存在保存配置數(shù)據(jù)、全局元信息持久順序節(jié)點持久節(jié)點基礎上帶全局自增序號記錄有序事件、任務編號臨時節(jié)點客戶端會話結束自動刪除服務注冊、分布式鎖占位臨時順序節(jié)點臨時節(jié)點基礎上帶全局自增序號分布式鎖的排隊隊列我最初用過持久節(jié)點做服務注冊結果服務宕機后節(jié)點不會消失手動清理又容易出錯。后來改成臨時節(jié)點服務進程退出、會話斷開節(jié)點馬上被刪除整個上下線流程就自動化了。順序節(jié)點雖然不常用但分布式鎖里它是核心因為鎖競爭退出時可以利用序號的先后關系確定獲取順序避免驚群效應。2.3 會話與心跳客戶端和服務器如何維持連接ZooKeeper客戶端和服務端之間的連接基于會話會話本質(zhì)是一個帶超時時間的TCP長連接??蛻舳送ㄟ^發(fā)送心跳請求來保活服務端在會話超時時間內(nèi)收到心跳會話就保持有效超過時間還沒收到會話過期臨時節(jié)點隨之被清理。Java操作時我一般配置會話超時時間為3000到5000毫秒這個值既不能太大也不能太小。太大會話過期識別慢臨時節(jié)點殘留時間久太小網(wǎng)絡抖動就會導致會話誤判超時服務頻繁掉線。再配合客戶端自動重連機制即使網(wǎng)絡短暫異常原來創(chuàng)建的臨時節(jié)點也會在重連后恢復但這個恢復并不自動需要代碼回滾業(yè)務邏輯。2.4 Watch通知機制一次性觸發(fā)循環(huán)注冊Watch是ZooKeeper最實用也最容易寫錯的機制??蛻舳丝梢詫δ硞€事務操作注冊監(jiān)聽比如節(jié)點數(shù)據(jù)變化、子節(jié)點列表變化、節(jié)點刪除只要事件發(fā)生客戶端會收到一次通知。注意是“一次”因為每次通知完之后監(jiān)聽就失效了想繼續(xù)監(jiān)聽必須重新注冊。這個一次性規(guī)則讓很多初學者困惑為什么第一次修改配置能收到通知第二次就不行原因就是沒做再次注冊。后面的代碼示例里我會專門封裝一個帶循環(huán)注冊的工具類把每次收到通知后重新設置Watch這個動作固化進去。3. 客戶端選型原生API、ZkClient與Curator的取舍Java操作ZooKeeper時選型直接影響開發(fā)效率和后期維護。我在這部分有比較明確的答案但為了讓你做技術決策時有依據(jù)我先把三種方案的實際體驗都列出來。3.1 三種客戶端方案的橫向對比維度原生ZooKeeper APIZkClientCurator依賴程度JDK自帶輕量封裝完整框架Watch處理手動循環(huán)注冊自動監(jiān)聽自動監(jiān)聽重連機制需自己實現(xiàn)簡單封裝完善分布式鎖需要手寫需要手寫內(nèi)置實現(xiàn)維護活躍度保持更新基本停滯活躍學習曲線較陡平滑中間原生API最大的問題不是功能不夠而是重復代碼太多。每次操作都要處理連接狀態(tài)、重連邏輯、Watch重新注冊這些都是極其容易出錯的邊角料。ZkClient曾經(jīng)流行過一段但封裝程度有限遇到復雜會話事件處理比較吃力。如果你問我現(xiàn)在的建議我會直接推薦Curator它在國內(nèi)項目里已經(jīng)積累了足夠的驗證文檔也相對齊全。3.2 為什么我建議直接上CuratorCurator把ZooKeeper客戶端做成了可以開箱即用的框架提供了連接狀態(tài)監(jiān)聽、自動重連、Watch管理器還內(nèi)置了InterProcessMutex這類分布式鎖實現(xiàn)。引入Curator之后代碼量對比原生API至少減少一半同時可讀性和健壯性明顯提升。當然這不意味著原生API不值得學。我建議你先把原生API的基本操作跑一遍理解它怎么建立連接、怎么注冊Watch、怎么處理回調(diào)然后切到Curator你會更清楚框架替你做了什么遇到問題時也能定位到ZooKeeper底層還是框架封裝層。這種學習路徑比直接上手Curator更扎實。3.3 環(huán)境準備單機部署和偽集群搭建本地開發(fā)時單機模式就夠用。下載解壓后修改配置文件zoo.cfg設置dataDir和clientPort默認2181端口然后執(zhí)行啟動腳本。確認啟動成功的命令是連接2181端口并輸入stat能看到ZooKeeper版本信息就說明通了。如果需要模擬生產(chǎn)環(huán)境的故障轉移我建議用偽集群模式也就是在同一臺機器上跑三個ZooKeeper實例。三個實例分別監(jiān)聽2181、2182、2183端口dataDir也分成三份并且在每個實例的data目錄中都要新建myid文件內(nèi)容分別是1、2、3。配置文件里通過server.1127.0.0.1:2888:3888這樣的格式聲明集群成員。偽集群能驗證選舉和會話轉移對理解ZooKeeper高可用機制幫助很大。4. 手寫Java操作ZooKeeper從連接、CRUD到Watch監(jiān)聽這一章是實操的重點。我會同時給出原生API和Curator兩套代碼方便你根據(jù)項目實際情況選用。所有代碼我都基于實際跑通的示例整理你復制到自己的項目里改幾個參數(shù)就能用。4.1 基礎依賴與連接建立如果用Curator需要在pom.xml中加入dependency groupIdorg.apache.curator/groupId artifactIdcurator-framework/artifactId version5.5.0/version /dependency dependency groupIdorg.apache.curator/groupId artifactIdcurator-recipes/artifactId version5.5.0/version /dependency如果只是想體驗原生API只需要引入ZooKeeper官方客戶端依賴。不過下文除了最基礎的連接示例用原生API后面的業(yè)務操作我都用Curator因為Curator的API設計更適合工程落地。原生API建立連接的方式ZooKeeper zooKeeper new ZooKeeper(127.0.0.1:2181, 3000, event - { System.out.println(收到事件 event.getType()); });原生API的構造函數(shù)中第一個參數(shù)是連接串第二個是會話超時時間第三個是Watcher回調(diào)。連接本身是異步的所以構造完成后要等待一下確認連接穩(wěn)定再繼續(xù)執(zhí)行后續(xù)操作。用Curator建立連接的代碼會更直觀RetryPolicy retryPolicy new ExponentialBackoffRetry(1000, 3); CuratorFramework client CuratorFramework.builder() .connectString(127.0.0.1:2181) .sessionTimeoutMs(3000) .connectionTimeoutMs(3000) .retryPolicy(retryPolicy) .build(); client.start();ExponentialBackoffRetry設置了初始重試間隔1000毫秒、最多重試3次。Curator啟動后不是馬上連上實際操作前最好調(diào)用blockUntilConnected等待連接建立避免在未連接狀態(tài)執(zhí)行命令拋異常。4.2 節(jié)點的增刪改查與版本控制用Curator操作節(jié)點非常方便。創(chuàng)建持久節(jié)點的代碼client.create().creatingParentContainersIfNeeded() .withMode(CreateMode.PERSISTENT) .forPath(/config/db_switch, on.getBytes(StandardCharsets.UTF_8));creatingParentContainersIfNeeded會自動創(chuàng)建父節(jié)點這個特性很實用避免手動判斷父路徑是否存在。withMode指定節(jié)點類型CreateMode.PERSISTENT是持久節(jié)點如果做服務注冊就換成EPHEMERAL。讀取節(jié)點數(shù)據(jù)byte[] data client.getData().forPath(/config/db_switch); String value new String(data, StandardCharsets.UTF_8);更新節(jié)點有兩種情況。第一種直接覆蓋用setData第二種是帶版本號的更新。ZooKeeper里的setData可以指定期望版本如果當前版本和期望版本不一致會拋出BadVersionException。這種機制適合保證多人同時改配置時不覆蓋別人的修改client.setData() .withVersion(0) .forPath(/config/db_switch, off.getBytes(StandardCharsets.UTF_8));刪除節(jié)點也有版本控制并且刪除有子節(jié)點的節(jié)點會失敗需要先遞歸刪除子節(jié)點。Curator的刪除方法支持guaranteed和deletingChildrenIfNeeded前者保證網(wǎng)絡異常時后臺重試刪除后者自動清理子節(jié)點client.delete().guaranteed().deletingChildrenIfNeeded().forPath(/config);4.3 一個能落地的Watch監(jiān)聽循環(huán)Watch是ZooKeeper最核心的機制代碼也最容易出錯。原生API的寫法是注冊Watcher但一次觸發(fā)后失效所以要在回調(diào)里再次注冊。下面是封裝好的循環(huán)監(jiān)聽工具public class ConfigWatcher { private final ZooKeeper zooKeeper; public ConfigWatcher(ZooKeeper zooKeeper) { this.zooKeeper zooKeeper; } public void watch(String path) throws Exception { // 用getData注冊監(jiān)聽監(jiān)聽的事件是節(jié)點數(shù)據(jù)變化 byte[] data zooKeeper.getData(path, event - { System.out.println(配置發(fā)生變化 event.getPath()); try { // 實際業(yè)務處理比如刷新本地緩存 handleConfigChange(path); // 重點重新注冊監(jiān)聽 watch(path); } catch (Exception ex) { ex.printStackTrace(); } }, null); System.out.println(當前配置值 new String(data)); } }這段代碼的關鍵點就在watch方法末尾再次調(diào)用自己注冊下一次監(jiān)聽。漏掉這一步監(jiān)聽只會觸發(fā)一次。我在項目里看到過很多次這個問題造成的現(xiàn)象是配置第一次修改生效以后再也收不到通知。用Curator時可以借助CuratorCache或者NodeCache來簡化監(jiān)聽邏輯。NodeCache在節(jié)點數(shù)據(jù)變化時觸發(fā)支持再次監(jiān)聽不需要手動循環(huán)NodeCache nodeCache new NodeCache(client, /config/db_switch); nodeCache.getListenable().addListener(() - { String newValue new String(nodeCache.getCurrentData().getData()); System.out.println(最新配置 newValue); }); nodeCache.start();NodeCache啟動后內(nèi)部自己處理了Watch重新注冊你只需要關心業(yè)務邏輯。這也是我更推薦Curator的原因把最容易錯的邊角料封住了。4.4 關于序列化傳數(shù)據(jù)到底傳什么操作ZNode時雖然接口接收的是byte[]但實際傳什么內(nèi)容我建議先約定好。寫項目X時我統(tǒng)一用JSON字符串把配置項封裝成對象后序列化存儲到節(jié)點里。讀取時再反序列化回對象。如果直接用Java原生序列化雖然類實現(xiàn)了Serializable就能存但跨語言調(diào)用會很痛苦而且序列化內(nèi)容體積大浪費ZooKeeper有限的存儲空間。所以我的建議是存量小字段直接用字符串比如開關狀態(tài)寫on/off。結構化成組配置用JSON字符串。切勿存大對象或大數(shù)組。深度思考一下ZooKeeper節(jié)點的數(shù)據(jù)是給所有客戶端看的用通用的JSON格式意味著不同語言的服務也能解析這比綁定Java序列化的方案通用性強很多。5. 實戰(zhàn)分布式鎖、服務發(fā)現(xiàn)、配置下發(fā)這三個場景怎么拆掌握基礎操作后關鍵是把它們組合成真正的業(yè)務解決方案。這一章我會挑三個最有代表性的實戰(zhàn)場景完整說明設計思路和實現(xiàn)步驟。5.1 用臨時順序節(jié)點實現(xiàn)分布式鎖項目X里有個定時任務需要保證集群里只有一個實例在執(zhí)行。最初用數(shù)據(jù)庫鎖后來切到ZooKeeper后穩(wěn)定很多。原理很簡單多個客戶端在同一個鎖路徑下創(chuàng)建臨時順序節(jié)點序號最小的獲得鎖其他客戶端監(jiān)聽序號比自己小的節(jié)點等它刪除后再嘗試獲得鎖。用Curator內(nèi)置的InterProcessMutex代碼幾乎沒有門檻InterProcessMutex lock new InterProcessMutex(client, /lock/order_task); boolean acquired lock.acquire(5, TimeUnit.SECONDS); if (acquired) { try { // 執(zhí)行定時任務 doTask(); } finally { lock.release(); } }acquire方法第二個參數(shù)是獲取鎖的超時時間超過則放棄避免線程長時間卡死。release方法必須在finally里調(diào)用否則鎖不會釋放其他實例會一直阻塞。還要注意如果進程崩潰鎖節(jié)點因為是臨時節(jié)點會自動被ZooKeeper清除這也是這個方案比數(shù)據(jù)庫鎖穩(wěn)的原因之一。如果不用Curator也可以基于原生API自己實現(xiàn)分布式鎖核心代碼邏輯是創(chuàng)建臨時順序節(jié)點然后獲取父節(jié)點下所有子節(jié)點列表判斷自己序號是否最小。不過這部分代碼要處理監(jiān)聽和重試細節(jié)我強烈建議直接用InterProcessMutex。5.2 服務注冊與發(fā)現(xiàn)沒有注冊中心時怎么辦服務注冊與發(fā)現(xiàn)是ZooKeeper非常經(jīng)典的場景。服務啟動時在 /services/服務名下創(chuàng)建臨時節(jié)點節(jié)點里存服務地址。調(diào)用方監(jiān)聽 /services/服務名 下的子節(jié)點列表變化就能實時感知服務的上下線。服務端注冊的示例String servicePath /services/order-service; String instancePath servicePath /instance- UUID.randomUUID(); String address 192.168.1.10:8080; client.create().creatingParentContainersIfNeeded() .withMode(CreateMode.EPHEMERAL) .forPath(instancePath, address.getBytes(StandardCharsets.UTF_8));客戶端訂閱服務列表的示例PathChildrenCache cache new PathChildrenCache(client, /services/order-service, true); cache.getListenable().addListener((curator, event) - { System.out.println(服務列表發(fā)生變化 event.getType()); cache.getCurrentData().forEach(data - { String address new String(data.getData()); System.out.println(可用實例 address); }); }); cache.start();PathChildrenCache是Curator提供的子節(jié)點監(jiān)聽工具啟動后監(jiān)聽子節(jié)點新增、刪除、數(shù)據(jù)修改。服務上下線時監(jiān)聽回調(diào)會收到事件客戶端就可以更新本地服務列表。配合臨時節(jié)點能夠實現(xiàn)服務宕機自動摘除整個過程不需要人工介入。5.3 配置中心動態(tài)下發(fā)與App內(nèi)熱更新配置中心是ZooKeeper比較容易上手的場景。我把項目X的數(shù)據(jù)庫連接池參數(shù)、功能開關、限流閾值全部放到了 /config 路徑下每個子節(jié)點對應一個配置項。服務啟動時讀取一次然后注冊監(jiān)聽配置更新后應用自動刷新對應組件。一個典型的配置模型// 啟動加載 byte[] data client.getData().forPath(/config/db_pool_max_active); String maxActive new String(data); // 修改數(shù)據(jù)庫連接池 dataSource.setMaxActive(Integer.parseInt(maxActive)); // 啟動監(jiān)聽后續(xù)變化實時更新 NodeCache cache new NodeCache(client, /config/db_pool_max_active); cache.getListenable().addListener(() - { String newMaxActive new String(cache.getCurrentData().getData()); dataSource.setMaxActive(Integer.parseInt(newMaxActive)); }); cache.start();這套配置中心實現(xiàn)上非常簡單比引入完整的配置中心中間件輕量得多。如果你只是需要十幾個配置項的熱更新ZooKeeper是性價比極高的方案。但要注意配置項應該集中在一個路徑下方便管理還要為沒有經(jīng)驗的運維同學準備好配置變更的操作文檔避免改錯數(shù)據(jù)格式導致應用啟動時解析失敗。5.4 三個場景的通用套路總結拆完三個場景你能發(fā)現(xiàn)它們共享同一套設計模式選定ZNode路徑、確定節(jié)點類型、注冊Watch。服務注冊用臨時節(jié)點是因為要自動清理配置存儲用持久節(jié)點是因為要長期保存分布式鎖用臨時順序節(jié)點是因為要排隊和自動釋放。所以學習ZooKeeper時不需要死記硬背API關鍵是能把業(yè)務需求翻譯成“節(jié)點類型 監(jiān)聽事件”的二元組。翻譯對了代碼只是時間問題。6. 生產(chǎn)環(huán)境必須面對的幾個硬骨頭把ZooKeeper放到生產(chǎn)環(huán)境之前有幾個問題必須提前想清楚。我在項目X推進的過程中就踩過不少坑這里挑影響最大的幾個說。6.1 會話超時與心跳假死ZooKeeper客戶端會話超時機制是它高可用的基礎但也容易造成假死?,F(xiàn)象是服務進程還在但ZooKeeper服務端判斷會話過期把臨時節(jié)點清掉了其他客戶端看到你的服務已經(jīng)下線實際你的進程還在運行。我遇到過一次比較典型的情況某服務因為Full GC暫停了幾秒心跳沒發(fā)出去ZooKeeper判定會話超時把臨時節(jié)點刪了。Full GC恢復后服務還在處理業(yè)務下游已經(jīng)把流量切走了。這個問題靠代碼很難完全規(guī)避但可以通過調(diào)大sessionTimeout來降低誤判概率。我線上一般設sessionTimeoutMs為10秒比本地開發(fā)時大不少。處理方式是在客戶端加上ConnectionStateListener監(jiān)聽會話的重連和過期事件。Curator里這個Listener會告訴你RECONNECTED或者LOST。收到LOST事件時業(yè)務側必須主動檢查自己的臨時節(jié)點是否還存在不存在就需要重新注冊。6.2 Watch風暴與重復注冊Watch機制雖然好用但用不好會造成嚴重問題。如果幾百個客戶端對同一個父節(jié)點注冊子節(jié)點監(jiān)聽那么這個節(jié)點一變化幾百個客戶端全部被打醒同時執(zhí)行后續(xù)邏輯這就是Watch風暴。我在項目X早期犯過這個錯誤所有服務實例都在監(jiān)聽同一個服務列表節(jié)點導致服務上下線時通知量爆炸。優(yōu)化方案是把監(jiān)聽改為客戶端主動輪詢或者說改變數(shù)據(jù)組織方式讓每個客戶端只關心自己需要的那部分節(jié)點。比如把子節(jié)點按租戶分隔客戶端只監(jiān)聽自己租戶的路徑。這個調(diào)整之后通知量降了一個數(shù)量級效果立竿見影。另外就是重復注冊的坑。在緩存框架里一個節(jié)點被多個模塊同時監(jiān)聽很容易產(chǎn)生重復的Watcher。釋放節(jié)點緩存時必須調(diào)用close清理監(jiān)聽否則舊監(jiān)聽會一直駐留不僅浪費內(nèi)存還會造成重復通知。6.3 單機模式的坑與集群容錯本地開發(fā)用單機模式?jīng)]問題但生產(chǎn)環(huán)境千萬不能只部署一個ZooKeeper實例。一旦這個單點宕機依賴它的配置中心和分布式鎖全部癱瘓整個系統(tǒng)都被卡住。我建議生產(chǎn)環(huán)境至少部署三個實例并且滿足奇數(shù)個。這是ZooKeeper選舉機制決定的3個實例允許掛1個5個允許掛2個。如果只部署2個掛1個就達不到多數(shù)派條件集群直接不可用還不如單機。部署時還要注意ZooKeeper和業(yè)務應用盡量分離部署不要圖省事混在同一個宿主機上否則宿主機資源被業(yè)務占滿時ZooKeeper的磁盤和端口也會受影響容易產(chǎn)生連鎖故障。6.4 ACL權限不要讓ZNode裸奔很多團隊搭建ZooKeeper后節(jié)點默認不設任何權限所有人都能讀寫。在開發(fā)環(huán)境可以容忍但生產(chǎn)環(huán)境這樣做風險很大。如果有誤操作或者在從節(jié)點路徑上亂寫數(shù)據(jù)可能導致線上配置被覆蓋。ZooKeeper的ACL不像Linux文件權限那么簡單它包含scheme、id和permission三層。我目前用的是digest認證給節(jié)點設置用戶名密碼客戶端訪問時必須攜帶認證信息。雖然MultiDocker內(nèi)部的網(wǎng)絡環(huán)境相對可控但加上認證至少能擋住誤操作和弱安全要求的臨時腳本訪問。設置認證的示例client.setACL().withACL(ZooDefs.Ids.CREATOR_ALL_ACL).forPath(/config);同時客戶端連接后要調(diào)用addAuthInfoclient.getZookeeperClient().getZooKeeper() .addAuthInfo(digest, admin:password.getBytes());ACL的好處是防止未授權客戶端亂寫數(shù)據(jù)代價是每次連接多一步認證但這對生產(chǎn)安全是有價值的。如果你所在團隊安全要求比較高建議一開始就按這套方式設計節(jié)點權限體系。作為Java開發(fā)者在項目中落地ZooKeeper我從最開始的只會寫CRUD到后來能獨立完成分布式鎖、服務發(fā)現(xiàn)和配置中心整個過程中最大的體會是ZooKeeper的API并不難難的是把“節(jié)點類型 Watch事件”這兩個概念靈活組合出正確的解決方案。我在項目X里真正穩(wěn)定運行之后才慢慢把之前踩過的坑變成團隊內(nèi)部的排查手冊。最后分享一個小技巧排查ZooKeeper相關問題時可以先用命令行客戶端連接上去手動執(zhí)行get和ls操作確認數(shù)據(jù)狀態(tài)是否和預期一致再回頭查代碼邏輯這樣能快速縮小問題范圍比直接翻代碼有效率得多。