基於netty、zookeeper手寫RPC框架之二——接入zookeeper作爲註冊中心,添加心跳機制

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章