zookeeper
介紹
分佈式是指多臺不同的服務器中部署不同的服務模塊,通過遠程調用協同工作,對外提供服務,由於遠程調用會出現網絡故障等問題,如何保持數據一致性和可用性則成爲要解決的問題。而zookeeper是一個分佈式服務協調組件,是一個高性能的分佈式數據一致性的解決方案。
特性
一致性:數據一致性,數據按照順序分批入庫。
原子性:事務要麼成功要麼失敗,不會局部化
單一視圖:客戶端連接集羣中的任一zk節點,數據都是一致的
可靠性:每次對zk的操作狀態都會保存在服務端
實時性:客戶端可以讀取到zk服務端的最新數據
安裝及簡單運行
去官網下載壓縮包到本地解壓,然後把zoo_sample.cfg改爲zoo.cfg並按實際情況填寫配置文件,在bin目錄下運行啓動腳本
zookeeper主要目錄結構
bin:主要的一些運行命令
conf:存放配置文件
contrib:附加的一些功能
dist-maven:mvn編譯後的目錄
docs:文檔
lib:需要依賴的jar包
recipes:案例demo代碼
src:源碼
zoo.cfg配置
tickTime:用於計算的時間單位。比如session超時:N*tickTime
initLimit:用於集羣,允許從節點連接並同步到master節點的初始化連接時間,以tickTime的倍數表示
syncLimit:用於集羣,master主節點與從節點之間發送消息,請求和應答時間長度。(心跳機制)
dataDir:必須配置,數據目錄
dataLogDir:日誌目錄,如果不配置會和dataDir同一個公用目錄
clientPort:連接服務器的端口,默認2181
zookeeper基本數據模型
是一個樹形結構,類似linux的文件目錄結構
每個節點稱爲znode,它可以有子節點,也可以有數據
節點分爲臨時節點和永久節點,臨時節點在客戶端斷開後失效
每個zk節點都有各自的版本號,可以通過命令行來顯示節點信息
每當節點數據發生變化,那麼該節點的版本會累加(樂觀鎖)
刪除/修改過時節點,版本號不匹配會報錯
每個zk節點存儲的數據不宜過大,幾k即可
節點可以設置權限acl,可以通過權限來限制用戶的訪問、區分環境等等,acl這裏就不做介紹了
master節點選舉,主節點掛了以後,從節點就會接受工作,並且保證這個節點是唯一的,這也是所謂首腦模式,從而保證我們的集羣是高可用的
常見運用
統一配置文件管理,即只需要部署一臺服務器,則可以把相同的配置文件同步更新到其他所有服務器,此操作在雲計算中用的特別多。
發佈與訂閱,類似於消息隊列,dubbo發佈者把數據存在znode上,訂閱者讀取這個數據
提供分佈式鎖,分佈式環境中不同進程之間爭奪資源,類似於多線程中的鎖
集羣管理,保證集羣中數據的強一致性
命令
ls 子節點
ls2 ls+stat
stat 狀態信息
czxid:節點被創建的事務ID
ctime: 節點創建時間
mzxid: 最後一次被更新的事務ID
mtime: 節點修改時間
pzxid:子節點列表最後一次被更新的事務ID
cversion:子節點的版本號
dataversion:數據版本號
aclversion:權限版本號
ephemeralOwner:用於臨時節點,代表臨時節點的事務ID,如果爲持久節點則爲0
dataLength:節點存儲的數據的長度 numChildren:當前節點的子節點個數
get 數據+stat
session基本原理
客戶端與服務端之間的鏈接存在會話
每個會話都會可以設置一個超時時間
心跳結束,session則過期
session過期,則臨時節點znode會被拋棄
心跳機制:客戶端向服務端的ping包請求
create:創建 -e臨時節點 -s 有序節點
set:修改
delete:刪除
watcher機制:
針對每個節點的操作,都有要給監督者->watcher
當監控的某個對象(znode)發生了變換,則觸發了watcher事件
zk中的watcher是一次性的,觸發後立即銷燬
父節點,子節點 增刪改都能夠觸發其watcher
針對不同類型的操作,觸發的watcher事件也不同:
節點創建、刪除、數據變化事件
注意watcher只可以使用一次,stat、get、ls、ls2後面均可以加watcher進行監聽。ls爲父節點設置watcher,創建、刪除子節點觸發:NodeChildrenChanged,修改不會觸發
關於zookeeper更詳細介紹可以看看這篇文章https://www.cnblogs.com/luxiaoxun/p/4887452.html
下面開始編碼的環節
這裏的格式也是像前一篇文章一樣 {interface:{url:impl}},而在zookeeper上面的格式爲 /interface/url:data(具體實現類的名字),
而心跳機制的實現,是通過服務提供者定時向註冊中心發送本機地址(心跳數據包),而註冊中心的監控則維持一個channelId和具體地址的map,並且通過IdleHandler監聽空閒事件,到達一定的空閒次數則認爲不活躍,當不活躍時(這裏的不活躍條件是5分鐘內3次以上沒有發送心跳包),zookeeper刪除相應的url節點,但後續的邏輯沒有繼續做,比如:服務提供方在網絡穩定後嘗試重新發送心跳包,註冊中心通過一定的計算(比如在一定時間內的心跳發送率達到一定的值)認爲該ip可用了,就嘗試重新向zookeeper註冊該ip,而且也可以在本地維持一個map存放接口信息,並添加監聽事件去更新可用列表,可以優化的點還很多,這裏暫時先接入zookeeper並簡單地演示通過心跳來移除不穩定服務
這裏採用curator作爲zookeeper的客戶端
首先編寫關於zookeeper的業務操作
/**
* @author lulu
* @Date 2019/11/18 21:17
* 負責實現註冊中心具體的業務功能
*/
public class ZkRegister {
//{接口:{URL:實現類名}},這裏可以爲每個接口建立子節點,節點名爲url地址,值爲className
private static CuratorFramework client = null;
//通過靜態代碼塊初始化
static{
init();
}
//初始化鏈接客戶端
private static void init() {
RetryPolicy retryPolicy = new RetryNTimes(ZKConsts.RETRYTIME, ZKConsts.SLEEP_MS_BEWTEENR_RETRY);
client = CuratorFrameworkFactory.builder()
.connectString(ZKConsts.ZK_SERVER_PATH)
.sessionTimeoutMs(ZKConsts.SESSION_TIMEOUT_MS).retryPolicy(retryPolicy)
.namespace(ZKConsts.WORK_SPACE).build();
client.start();
}
//註冊接口、對應服務ip及其實現類
public static void register(String interfaceName, URL url, Class implClass) {
try {
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath(getPath(interfaceName, url.toString()), implClass.getCanonicalName().getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
//hostname:port,遍歷所有interface節點,把對應的url節點去掉
public static void remove(String url) {
try {
List<String> interfaces = client.getChildren().forPath("/");
for (String anInterface : interfaces) {
List<String> urlList = client.getChildren().forPath(getPath(anInterface));
for (String s : urlList) {
if (s.equals(url)) {
client.delete().forPath(getPath(anInterface, url));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
//獲取具體實現類的類名,這裏還可以添加一個內部緩存,不用每次都去訪問,
public static String get(String interfaceName, URL url) {
String res = null;
try {
byte[] bytes = client.getData().forPath(getPath(interfaceName, url.toString()));
res = new String(bytes);
} catch (Exception e) {
e.printStackTrace();
}
return res;
}
//通過接口名獲取具體的實現類
public static URL random(String interfaceName) {
try {
List<String> urlList = client.getChildren().forPath(getPath(interfaceName));
String[] url = urlList.get(0).split(":");
return new URL(url[0], Integer.valueOf(url[1]));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//生成節點路徑
private static String getPath(String... args) {
StringBuilder builder = new StringBuilder();
for (String arg : args) {
builder.append("/").append(arg);
}
return builder.toString();
}
public static void closeZKClient() {
if (client != null) {
client.close();
}
}
}
心跳監聽,在提供服務方使用
package com.gdut.rpcstudy.demo.register.zk.heartbeat;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.util.Random;
import java.util.concurrent.*;
/**
* @author lulu
* @Date 2019/11/18 23:30
*/
public class BeatDataSender {
private BeatDataSender() {
}
public static void send(String url, String hostName, Integer port) {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
try {
Bootstrap bootstrap = new Bootstrap();
ChannelFuture connect = bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new StringEncoder())
.addLast(new StringEncoder())
.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("由於不活躍次數在5分鐘內超過2次,鏈接被關閉");
}
});
}
})
.connect(hostName, port).sync();
System.out.println("心跳客戶端綁定" + "hostname:" + hostName + "port:" + port);
//這裏只是演示心跳機制不活躍的情況下重連,普通的做法只需要定時發送本機地址即可
service.scheduleAtFixedRate(() -> {
if (connect.channel().isActive()) {
int time = new Random().nextInt(5);
System.out.println(time);
if(time >3){
System.out.println("發送本機地址:" + url);
connect.channel().writeAndFlush(url);
}
}
}, 60, 60, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
}
註冊中心檢查心跳
/**
* @author lulu
* @Date 2019/11/18 22:17
* 註冊中心心跳檢查服務器,通過查看心跳來查看各server是否存活
*/
public class ZkServer {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
ConcurrentHashMap<String,String> ChannalIdUrlMap=new ConcurrentHashMap();
try {
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
//存放已完成三次握手的請求的隊列的最大長度
.option(ChannelOption.SO_BACKLOG, 128)
//啓用心跳保活
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//string編碼器
ch.pipeline().addLast(new StringEncoder())
//string解碼器
.addLast(new StringDecoder())
//監聽鏈接空閒時間
.addLast(new IdleStateHandler(0,0,60))
//hearbeat處理器
.addLast(new HeartbeatHandler(ChannalIdUrlMap));
}
});
//bind初始化端口是異步的,但調用sync則會同步阻塞等待端口綁定成功
ChannelFuture future = bootstrap.bind("127.0.0.1",8888).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
在nettyserver的start方法添加發送心跳
BeatDataSender.send(hostName + ":" + port, "127.0.0.1", 8888);
心跳監聽處理邏輯,用於監聽心跳服務器的處理,需要和監聽鏈接空閒時間的IdleHandler一起使用,複寫eventTriger方法當鏈接符合給的空閒條件時,對其進行邏輯處理
/**
* @author lulu
* @Date 2019/11/18 22:29
*/
public class HeartbeatHandler extends ChannelInboundHandlerAdapter {
//維護channelId和具體地址的map,當發生變化時對其進行刪除
private static ConcurrentHashMap<String, String> channelUrlMap;
//活躍次數
private int inActiveCount = 0;
//開始計數時間
private long start;
public HeartbeatHandler(ConcurrentHashMap<String, String> map) {
HeartbeatHandler.channelUrlMap = map;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String url = msg.toString();
String id = ctx.channel().id().asShortText();
System.out.println("收到channelId:" + id + "發來信息:" + url);
if (channelUrlMap.get(id) == null) {
channelUrlMap.put(id, url);
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent state = (IdleStateEvent) evt;
if (state.state().equals(IdleState.READER_IDLE)) {
System.out.println("讀空閒");
} else if (state.state().equals(IdleState.WRITER_IDLE)) {
System.out.println("寫空閒");
}
//在一定時間內讀寫空閒纔會關閉鏈接
else if (state.state().equals(IdleState.ALL_IDLE)) {
if (++inActiveCount == 1) {
start = System.currentTimeMillis();
}
int minute = (int) ((System.currentTimeMillis() - start) / (60 * 1000))+1;
System.out.printf("第%d次讀寫都空閒,計時分鐘數%d%n", inActiveCount,minute);
//5分鐘內出現2次以上不活躍現象,有的話就把它去掉
if (inActiveCount > 2 && minute <= 5) {
System.out.println("移除不活躍的ip");
removeAndClose(ctx);
} else {
//重新計算
if (minute >= 5) {
System.out.println("新週期開始");
start = 0;
inActiveCount = 0;
}
}
}
}
}
//通過ID獲取地址,並刪除zk上相關的
private void removeAndClose(ChannelHandlerContext ctx) {
String id = ctx.channel().id().asShortText();
String url = channelUrlMap.get(id);
//移除不活躍的節點
ZkRegister.remove(url);
channelUrlMap.remove(id);
ctx.channel().close();
}
//當出現異常時關閉鏈接
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
removeAndClose(ctx);
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().id().asShortText() + "註冊");
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().id().asShortText() + "註銷");
}
}
由於存放的是classImplName,所以要在handler處理邏輯里加載該類,後面接入spring後可以從一個服務註冊的類上獲取相應的實現類
String serviceImplName= ZkRegister.get(invocation.getInterfaceName(),new URL(hostAddress,8080));
Class<?> serviceImpl = Class.forName(serviceImplName);
Method method=serviceImpl.getMethod(invocation.getMethodName(),invocation.getParamsTypes());
客戶端獲取url
URL url= ZkRegister.random(interfaceClass.getName());
此致更改完畢,地址爲https://github.com/97lele/rpcstudy/tree/withzk,接下來是把服務端和客戶端整合到spring