自研RPC理论

讲师-B站孙帅老师

基础 RPC 组成

  1. 网络传输 :既然要调用远程的方法,就要发送网络请求来传递目标类和方法的信息以及方法的参数等数据到服务提供端

  2. 传输协议 :这个协议是客户端和服务端交流的基础

  3. 序列化和反序列化 :要在网络传输数据就要涉及到序列化(统一格式)

  4. 动态代理 :屏蔽远程方法调用的底层细节

  5. 注册中心 :注册中心负责服务地址的注册与查找,相当于目录服务

  6. 负载均衡 : 避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题

    1. 随机
    2. 轮训
    3. 一致性hash
    4. weigth...
  7. 集群容错 :

    • FailFast 快速失败
    • FailOver 失效转移
    • FailBack 失效自动恢复, 后台记录失败请求,定时重发。通常用于消息通知操作
    • FailSafe:失效安全, 出现异常时,直接忽略。通常用于写入审计日志等操作

网络通信

  1. BIO Socket

    1. BIO(Blocking IO)是Java最原始的网络编程模型,使用阻塞IO方式进行通信。
    2. 适用于连接数较少、并发要求不高的场景。
    3. 编程模型相对简单,但在高并发环境下性能较差
  2. NIO Selector+Channel

    1. NIO(Non-blocking IO)是Java提供的一种基于事件驱动的IO模型
    2. 使用Selector和Channel实现非阻塞通信,可以管理多个连接。
    3. 适用于高并发、连接数较多的场景。
    4. 编程模型相对复杂,需要手动处理事件分发和缓冲区管理
  3. Netty (选择)

    1. Netty是一个高性能、异步事件驱动的网络应用框架。
    2. 基于NIO实现 ,提供了简单而强大的API,简化了网络编程的复杂性
    3. 提供了高度可定制的线程模型和协议支持,适用于构建各种网络应用。
    4. 高并发、高性能的网络应用中广泛应用。
  4. Mina

    1. Mina(Apache MINA)是一个Java网络应用框架
    2. 采用NIO实现 ,提供了高度抽象的API和事件驱动的编程模型。
    3. 支持多种网络协议,可用于构建各种网络应用。
    4. 具有良好的可扩展性和灵活性。

传输协议

网络传输数据过程中的,数据格式,一种约定

协议是分层的,

  • 应用层 (http, ftp, pop3 ..)
  • 传输层 (tcp, udp)

应用层的协议会经过传输层, 并在数据帧的头部加上传输层头部信息

端口占用情况说明

以 80 端口为例

  • tcp 80 ✅
  • udp 80 ✅
  • http 80 ❎
  • Pop3 80 ❎
  • tomcat 80 ❎

协议分类

  • 公有协议

    • 全世界都认可的协议格式 ,由国际组织定义,并颁布。
    • 绝大多数应用层协议都是公有协议 grpc (http2), web (http)
  • 私有协议

    • 基于自己软件定义的一种数据传输的格式,只限定于自己的软件网络通信过程中使用
    • Netty聊天室私有协议
    • Dubbodubbo协议私有
    • 自研的RPC 私有的协议
    • jdbc 私有的协议
    • 传输层应用较多
  • 通信组合

    • 应用层通信 + 应用层协议 ---> 应用层通信 【Http协议+文本】

      • SpringCloud体系 传输效率低, 应用广泛性高
    • 传输层通信 + 私有协议 ----> 传输层通信 【私有协议+二进制】

      • Dobbo 传输效率高, 应用广泛性 低
    • grpc 应用层协议(Http2+二进制)

      • 趋势

序列化

序列化协议属于 TCP/IP 协议应用层的一部分

  • 序列化: 将对象转换成固定的数据格式
  • 反序列化: 将固定的数据格式转换成对象

序列化两大类

  • 像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差(体量大),一般不会选择
  • 常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议

JDK 自带的序列化方式

前提:实现java.io.Serializable接口, Serializable真的只是一个标志,一个序列化标志

不推荐使用 JDK 自带的序列化

  • 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了
  • 性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大
  • 存在安全问题 :序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码

serialVersionUID 有什么作用?

  1. 序列化号 serialVersionUID 属于版本控制的作用

    1. 反序列化时,会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException 异常。
    2. 强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的 serialVersionUID
      serialVersionUID 不是被 static 变量修饰了吗?为什么还会被"序列化"?
  2. static 修饰的变量是静态变量,位于方法区,本身是不会被序列化的

    1. static 变量是属于类的而不是对象。
    2. 反序列之后,static 变量的值就像是默认赋予给了对象一样,看着就像是 static 变量被序列化,实际只是假象罢了。
      如果有些字段不想进行序列化怎么办?
  3. 对于不想进行序列化的变量,可以使用 transient 关键字修饰, 或者 static 修饰变量

    1. transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复, transient 只能修饰变量,不能修饰类和方法。transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型, 那么反序列后结果就是 0
    2. static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化

序列化对象

  • 序列化 ObjectOutputStream
  • 反序列化 ObjectInputStream
java 复制代码
public class JDKSerializar implements Serializar {
    @Override
    public byte[] serializar(Object obj) throws Exception {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(obj);
        return outputStream.toByteArray();
    }
​
    @Override
    public Object deserializar(byte[] bytes) throws Exception {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        return objectInputStream.readObject();
    }
}

JSON 序列化

可以采用 gson/jackson

xml 复制代码
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.8</version>
        </dependency>
java 复制代码
public class JSONSerilizar implements Serializar {
    private ObjectMapper objectMapper = new ObjectMapper();
​
    @Override
    public byte[] serializar(Object obj) throws Exception {
        return objectMapper.writeValueAsBytes(obj);
    }
​
    @Override
    public Object deserializar(byte[] bytes) throws Exception {
        return objectMapper.readValue(bytes, User.class);
    }
}

Hessian 序列化

Hessian 是一个轻量级的,自定义描述的二进制 RPC 协议。Hessian 是一个比较老的序列化实现了,并且同样也是跨语言的。

  • Dubbo2.x 默认启用的序列化方式是 Hessian2 ,但是,Dubbo 对 Hessian2 进行了修改,不过大体结构还是差不多

序列化对象

xml 复制代码
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>4.0.38</version>
        </dependency>
  • 序列化 Hessian2Output
  • 反序列化 Hessian2Input
java 复制代码
public class HessianSerializar implements Serializar {
    @Override
    public byte[] serializar(Object obj) throws Exception {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(outputStream);
        hessian2Output.writeObject(obj);
        hessian2Output.flush();
        return outputStream.toByteArray();
    }
​
    @Override
    public Object deserializar(byte[] bytes) throws Exception {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        Hessian2Input hessian2Input = new Hessian2Input(inputStream);
        return hessian2Input.readObject();
    }
}

Thrift 序列化

Thrift 网络传输的时候不认可自定义实体对象, 只认可在IDL创建结构。

  • 需要进行 IDL定义

    • 一. 通过Thrift命令(maven plugin)生成代码
    • 二. 也可以通过 maven 插件 maven-thrift-plugin

Demo

xml 复制代码
        <dependency>
            <groupId>org.apache.thrift</groupId>
            <artifactId>libthrift</artifactId>
            <version>0.13.0</version>
        </dependency>

maven 插件 maven-thrift-plugin

xml 复制代码
<plugin>
    <groupId>org.apache.thrift.tools</groupId>
    <artifactId>maven-thrift-plugin</artifactId>
    <version>0.1.11</version>
    <configuration>
       <thriftSourceRoot>${project.basedir}/src/main/thrift</thriftSourceRoot>
       <outputDirectory>${project.build.directory}/generated-sources/thrift</outputDirectory>
       <generator>java</generator>
       </configuration>
</plugin>

src/main/java同级目录,创建 thrift文件夹下创建idl文件, 比如 test.thrift

arduino 复制代码
namespace java com.suns.thrift
​
struct User{
  1: string name
}
​
/*
  thrift --gen java test.thrift
  maven插件 maven-thrift-plugin 不是官方
*/

序列化工具类

java 复制代码
public class ThriftSerializar implements Serializar {
    @Override
    public byte[] serializar(Object obj) throws Exception {
        User user = (User) obj;
        //数据的copy 手工copy
        com.suns.thrift.User u = new com.suns.thrift.User();
        u.setName(user.getName());

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        TBinaryProtocol outProtocol = new TBinaryProtocol(new TIOStreamTransport(outputStream));
        u.write(outProtocol);
        return outputStream.toByteArray();
    }

    @Override
    public Object deserializar(byte[] bytes) throws Exception {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        TBinaryProtocol inProtocol = new TBinaryProtocol(new TIOStreamTransport(inputStream));
        com.suns.thrift.User newUser = new com.suns.thrift.User();
        newUser.read(inProtocol);

        //数据的copy
        User user = new User();
        user.setName(newUser.getName());
        return user;
    }
}

Kryo 序列化

Kryo 是一个高性能的序列化/反序列化工具,由于其变长存储特性 并使用了字节码生成机制,拥有较高的运行速度较小的字节码体积

java 复制代码
@Slf4j
public class KryoSerializer implements Serializer {

    /**
     * Because Kryo is not thread safe. So, use ThreadLocal to store Kryo objects
     */
    private final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        kryo.register(RpcResponse.class);
        kryo.register(RpcRequest.class);
        return kryo;
    });

    @Override
    public byte[] serialize(Object obj) {
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             Output output = new Output(byteArrayOutputStream)) {
            Kryo kryo = kryoThreadLocal.get();
            // Object->byte:将对象序列化为byte数组
            kryo.writeObject(output, obj);
            kryoThreadLocal.remove();
            return output.toBytes();
        } catch (Exception e) {
            throw new SerializeException("Serialization failed");
        }
    }

    @Override
    public <T> T deserialize(byte[] bytes, Class<T> clazz) {
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
             Input input = new Input(byteArrayInputStream)) {
            Kryo kryo = kryoThreadLocal.get();
            // byte->Object:从byte数组中反序列化出对象
            Object o = kryo.readObject(input, clazz);
            kryoThreadLocal.remove();
            return clazz.cast(o);
        } catch (Exception e) {
            throw new SerializeException("Deserialization failed");
        }
    }
}

Protobuf 序列化

Protobuf 出自于 Google,性能还比较优秀,也支持多种语言,同时还是跨平台的。

  • 就是在使用中过于繁琐,因为你需要自己定义 IDL 文件和生成对应的序列化代码。
  • 这样虽然不灵活,但是,另一方面导致 protobuf 没有序列化漏洞的风险
xml 复制代码
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.18.1</version>
</dependency>
xml 复制代码
    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.7.1</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.52.1:exe:${os.detected.classifier}</pluginArtifact>
                    <outputDirectory>${basedir}/src/main/java</outputDirectory>
                    <clearOutputDirectory>false</clearOutputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

src/main/java同级目录,创建 proto文件夹下创建idl文件, 比如 hello.thrift

ini 复制代码
syntax = "proto3";

package com.suns;

option java_multiple_files = false;
option java_package = "com.suns";
option java_outer_classname = "HelloProto";

message HelloRequest{
  string name = 1;
}

序列化工具类

java 复制代码
public class ProtoBufSerializar implements Serializar {
    @Override
    public byte[] serializar(Object obj) throws Exception {
        //数据的copy
        User user = (User) obj;
        HelloProto.HelloRequest helloRequest = HelloProto.HelloRequest.newBuilder().setName(user.getName()).build();

        return helloRequest.toByteArray();
    }

    @Override
    public Object deserializar(byte[] bytes) throws Exception {
        HelloProto.HelloRequest helloRequest = HelloProto.HelloRequest.parseFrom(bytes);

        //数据的copy
        User user = new User();
        user.setName(helloRequest.getName());
        return user;
    }
}

序列化与网络传输集成

  • 序列化 --> 编码
  • 反序列化 --> 解码
  1. Netty 编解码 是通过 Handler 进行处理的 (byte 系列)

    • ByteToMessageDecoder 解码
    • MessageToByteEncoder 编码
  1. 把编解码的内容 序列化的内容书写在MessageToMessage中 (message 序列, 不解决封帧问题)

    • MessageToMessageDecoder

    • MessageToMessageEncoder

    • MessageToMessageCodec

    • 要使用封帧解码器 解决半包 粘包的问题

      • LengthFieldBaseFrameDecoder
ini 复制代码
// ByteBuf 解码所需参数
// User 编码所需参数
@Slf4j
public class RPCMessageToMessageCodec extends MessageToMessageCodec<ByteBuf, User> {

    /*
        网络传输的格式 ByteBuf ---------->  JavaObject
        反序列化
     */
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        //1. ByteBuf msg ---> byte[]
        //byte[]长度
        int protocolLength = msg.readInt();
        byte[] bytes = new byte[protocolLength];
        msg.readBytes(bytes);

        //2. 反序列化操作
        Serializar serializar = new JDKSerializar();
        User user = (User) serializar.deserializar(bytes);

        //3 解码器如何把封装转换的对象 交给后面的操作呢?
        out.add(user);
    }

    @Override
    /*
        java object ---------------> 网络传输的格式
        1. java object msg参数已经获取
        2. 序列化 自己创建
        3. 如何把序列化后的byte[] 交给netty进行传输
              byte[] ---> ByteBuf ---> out.add();
     */
    protected void encode(ChannelHandlerContext ctx, User msg, List<Object> out) throws Exception {
        log.debug("编码器运行了....");
        ByteBufAllocator alloc = ctx.alloc();
        ByteBuf byteBuf = alloc.buffer();

        try {
            Serializar serializar = new JDKSerializar();
            byte[] bytes = serializar.serializar(msg);

            //封帧的解码器 数据大小是多少
            byteBuf.writeInt(bytes.length);
            byteBuf.writeBytes(bytes);

            out.add(byteBuf);
        } catch (Exception e) {
            log.error("编码器出现了一场", e);
        }
    }
}

Netty与自定义编解码器的整合 (Client, Server)

java 复制代码
public class RPCServer {
    public static void main(String[] args) {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.channel(NioServerSocketChannel.class);
        bootstrap.group(new NioEventLoopGroup());
        //handler       ServerSocketChannel
        //childHandler  SocketChannel
        bootstrap.childHandler(new ChannelInitializer<NioSocketChannel>() {

            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 0));
                ch.pipeline().addLast(new LoggingHandler());
                ch.pipeline().addLast(new RPCMessageToMessageCodec());
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        User user = (User) msg;
                        System.out.println("user = " + user);
                    }
                });
            }
        });
        bootstrap.bind(8000);
    }
}
java 复制代码
public class RPCClient {
    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.group(eventLoopGroup);
        bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 0));
                ch.pipeline().addLast(new LoggingHandler());
                ch.pipeline().addLast(new RPCMessageToMessageCodec());
                ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        // User对象 POJO
                        ctx.writeAndFlush(new User("xiaohei"));
                    }
                });
            }
        });

        ChannelFuture connect = bootstrap.connect(new InetSocketAddress(8000));
        connect.sync();
    }
}

动态代理

客户端代理可以使用

  • Proxy.newProxyInstance()
  • Cglib
  • Javasist
  • Spring AOP [BeanPostProcessor]
  • Spring AOP [FactoryBean]

注册中心 Zookeeper

开源分布式协调服务,

  • 分布式(集群) 协调服务(管理) = 用于集群管理的技术
  • 提供了强一致性的保证,基于Paxos算法的ZAB协议保证的数据的强一致性。(RAFT)

现在都在去 zk (dubbo Kafka RocketMQ 去zk的趋势)

  • 数据一致性的算法不够优秀 【RAFT】

ZK 的功能

注册中心,配置中心,负载均衡,故障转移,分布式锁, 命名服务, 健康检查

ZK 的逻辑结构

树状结构

  • 根节点 /
  • 后续节点都是 绝对路径 都必须以 /开头
  • 树的节点名叫 Znode

ZK 物理结构

  • 单机版 standalone

    • 单机版的 ZK 只能进行测试 而不用用于生产环境。

    • 原因:

        1. 单点故障
          1. 受限硬件资源 (CPU 内存 网络)
  • 集群版 cluster

    • 生产环境中,使用的zk的版本
    • 必须是集群数奇数
    • 更加可靠,更安全,资源更多

ZK集群注意事项

  1. ZK 主节点(leader) 从节点 (flower)

    1. ZK 为了避免与管理的Client集群命名歧义, 所以自己把对应的主 leader 从flower
  2. 如何确定ZK集群中节点的身份 (主 从)

    1. 选举算法(投票),过半数认可 就是主节点
  3. 主节点作用, 从节点作用

    1. 主:主节点对树状结构 增加 删除 ---> 节点 查
    2. 从:查询 (当主节点更新完树的结构后,把相应数据同步所有的从)
    3. 注意:如果过半数的从节点,更新到了最新的数据,那么ZK就认可这个操作成功了
  4. zk集群的容错性

    1. 过半节点出现问题,zk集群中断服务。
  5. zk集群 节点数 什么要求

    1. 任意个数节点都可以作为 zk集群

      1. ZK 集群个数为奇数个
      2. 偶数个节点在保证系统可靠性时,并没有比奇数节点更安全。但是硬件资源反而占多了

Znode 类型

  • 持久节点

    • 如果zk client在zk树状结构上创建了持久节点,那么client在与zk服务端断掉连接后,这个节点还会永久的放置在zk的树状结构上。
  • 临时节点

    • 如果zk client在zk树状结构上创建了临时节点,那么client在与zk服务端断掉连接后,这个节点就会从zk的树状结构上移除
  • 有序持久节点

    • 首先他是持久节点,同时这个节点的名字上还会有一个序号,且这个序号是zk集群中唯一(自增)
  • 有序临时节点

    • 首先他是持久节点,同时这个节点的名字上还会有一个序号,且这个序号是zk集群中唯一(自增)

正对于有序节点的序号,他是全局唯一,累加的,和节点是不是持久、临时没有关系。

ZK集群的选举过程

  1. 初始情况下,没有选举之前,zk集群中所有的节点,每一个节点的状态都叫做LOOKING.

  2. 在选举时,如果集群中过半节点,选举某一个节点作为主节点,那么他就是主节点,其他剩余节点就是从节点

  3. 主节点:Leader 从节点:Flower[跟随者] Observer[观察者]

    1. Flower与Observer的区别:只同步数据,不参与选举。
  4. 选举过程中的核心内容:投票依据

    1. id 服务器id ----> data--myid文件

      1. id小的服务器 会把票 投给id大
    2. zxid (zookeeper transaction id zk事务id)


      0. zxid:zk会对每一个写操作 都会记录一个zxid (在那个节点处理的,就会记录在那个节点中)

      1. zxid 64位整数 递增 唯一
      2. zxid小的节点 ---> zxid大的节点
      3. 优先级 zxid > id
    3. epoch 时代 纪元 (投票得轮数) Split Brains脑裂

      1. 投票轮数高的最终决定 投票依据
      2. 优先级 epoch >zxid > id

投票得2个维度

  1. 集群初始化 【初始选主】【图1】
  2. 集群运行中,主出现问题的情况 【从新选主】【图2】

Split-Brains 脑裂 在主从集群中,经常发生。2个大脑,2主节点(Leader,Master)

  1. zk集群中 怎么会产生脑裂?

    1. zk集群中 出现了多个分区,原有的主节点从在于一个分区内,而另外过半多个节点所构成的新分区又选举了新的主节点,那这个时候就产生脑裂。

    2. zk如何解决脑裂?

      1. 最终整合,如何在多个主节点中,确定最后的唯一主?通过epoch 进行判断 最终确定。

ZK 安装

linux 设置

  1. 关闭了防火墙

    1. systemctl stop firewalled

    2. systemctl disable firewalled

    3. 网卡进行配置

      1. /etc/sysconfig/network-scripts/xxx

      2. 修改主机名 以主机名替代 ip(ip 可能会变)

        1. vi /etc/hostname -> zookeeper1.suns.com
      3. 主机名映射

        1. vi /etc/hosts -> 172.16.79.132 zookeeper1.suns.com

网卡配置

  • onboot=yes每一次启动自动取获取网卡
  • bootproto=dhcp 动态 ip
  • device 要与文件名-后面的相同

单机版安装

  • 安装JDK jdk8

    • jdk归档路径

    • ftp工具上传至linux服务器

    • 安装rpm 软件 rpm -ivh 软件名

    • 配置环境变量

      • vi /etc/profile 全局
      • 或者 vi ~/.bash_profile
      • 或者 vi ~/.bashrc
    • source .bash_profile

bash 复制代码
	  JAVA_HOME=/usr/java/jdk1.8.0_351-amd64
      PATH=$PATH:$HOME/bin:$JAVA_HOME/bin

      export JAVA_HOME
      export PATH

安装 zookeeper

apache官网下载zk最新。如何下载历史版本的zk? - apache归档 - tar -zxvf apache-zookeeper-3.6.1-bin.tar.gz - mv apache-zookeeper-3.6.1-bin zookeeper

  • 配置zookeeper

    • 修改名字

      • mv conf/zoo_sample.cfg conf/zoo.cfg
    • 创建数据文件夹 mkdir /root/zookeeper/data

    • vi conf/zoo.cfg -> dataDir=/root/zookeeper/data

  • 启动ZK的服务

    • bin下面 ./zkServer.sh start | status | stop
  • 客户端访问

    • bin下面 ./zkCli.sh 连接到zk服务器

集群版安装

  • ssh登录 :linux远端登录操作的一种手段,对比其他的登录方式(telnet), ssh登录非常安全,可以防止黑客恶意的劫持

  • ssh登录 :在ssh登录过程中,需要提供用户名、密码

    • ssh 默认的使用方式 ssh root@zookeeper1.suns.com 提供密码
  • ssh免密登录 : 在集群环境下,个个节点间可能需要进行相互通信,设置会相互执行一些命令,那么在执行命令时,需要登录到另一台服务上,才可以执行。按照传统的ssh登录需要提供用户名密码,会造成通信的复杂度,设置需要人工干预,繁琐。所以ssh提供了免密登录,降低服务器之间登录通信的难度。所以在集群环境下搭建ssh免密登录是一种常见的运维手段

    • 生成公私钥对

      • ssh-keygen -t rsa
      • 公私钥对的放置的位置 ~/.ssh id_rsa id_rsa.pub
    • 公钥发送给远端的主机

      • ssh-copy-id 用户名@主机名
      • 公钥存储在远端主机 ~/.ssh authorized_keys

开始安装

  1. 奇数个节点 进行zk集群的安装。3台

  2. 主机名 克隆 规划 zookeeper1.suns.com 172.16.79.132 zookeeper2.suns.com 172.16.79.133 zookeeper3.suns.com 172.16.79.134

  3. vim /ect/hosts文件 172.16.79.132 zookeeper1.suns.com 172.16.79.133 zookeeper2.suns.com 172.16.79.134 zookeeper3.suns.com

    技巧: scp 个个节点之间数据的复制, scp 本机文件的位置 用户名@主机名:路径 scp /etc/hosts root@zookeeper2.suns.com:/etc

  4. ZK集群搭建

    1. 安装jdk

    2. 安装zk

      1. 解压缩 改名

      2. 准备数据目录 [区别]

        1. root/zookeeper-3.6.1/data
        2. 创建myid文件 ---> 1 (说明是第一台服务器节点)
      3. 修改配置文件 [区别] vi conf/zoo.cfg

ini 复制代码
	     dataDir=/root/zookeeper-3.6.1/data
	
	     server.1=zookeeper1.suns.com:2888:3888 
	     server.2=zookeeper2.suns.com:2888:3888
	     server.3=zookeeper3.suns.com:2888:3888
	
	   > [2888 是 lead 与 follow 通信端口]
	   > [3888 是 选举投票端口] 
	
	4. 集群的复制 
	    `scp -r /root/zookeeper-3.6.1/ root@zookeeper3.suns.com:/root`
	
	5. 修改myid文件
	    zookeeper2   --->   myid   --->   2
	    zookeeper3   --->   myid   --->   3
3. 每一个节点上面 启动zk服务
      `./zkServer.sh start`
      `./zkServer.sh status`

客户端与服务端通信

查询 : client 访问 ZK 服务端进行查询操作

  1. client是可以访问ZK 服务端 任意节点

    1. 查询过程中 client 访问ZK集群中的任意一个节点 ,都可以获得树的数据

      1. 问题:因为zk的过半机制,可能导致树的数据,没有及时的同步到client所访问的那个flower
      2. 通过client进行同步操作避免这件事的产生。

增删改:client 访问ZK服务端 进行增删改操作

  1. client是可以访问ZK服务端任意节点
  2. 如果client 访问的是ZK的 leader节点,leader就会完成增删改。会把最新的数据 同步到flower集群中,而且过半flower接受到最新的数据,就意味这个操作成功
  3. 如果client 访问的是ZK的flowder节点,flwoer会自动的找到leader节点,leader就会完成增删改。会把最新的数据 同步到flower集群中,而且过半flower接受到最新的数据,就意味这个操作成功

client 如何让他的访问 指定的服务端节点

  • 通过 ./zkCli.sh 进行服务端的连接 存在问题

    • 只能连接本机,不能连接zk集群中的其他节点
    • 如果本机没有zk的服务,报错
  • ./zkCli.sh -server ip|主机名(修改主机名 映射)

    • 通过这种方式,可以让client任意根据需要访问zk集群中的节点

ZK的使用

zk cli命令

  1. 客户端连接集群的命令 ./zkCli.sh -server ip|hostname

  2. 退出客户端 quit

  3. 创建

    1. 创建节点 create /cloudcreate create /cloudcreate/java 不能越级创建节点

    2. 创建有数据的节点 create /cloudcreate/java/teacher "suns" 注意:此时我们创建的节点 是持久节点

    3. 创建临时节点 create -e /cloudcreate/python

    4. 创建持久有序节点 create -s /cloudcreate/go/teacher 这种有序节点,可以重复创建的,因为他会自动的为节点编号

    5. 创建临时有序节点 create -e -s /cloudcreate/go/teacher

      注意:同一个路径下 有序节点的编号累加, 不同路径下的 有序节点的编号互不影响的。

  4. 修改 「znode的数据修改」 set /xxxx/xxxx "新的数据"

  5. 查看 znode数据 get /xxx/xxxx

  6. 删除 delete 删除某一个具体的路径,如果这个路径下面还有子路径,不能够删除 deleteall 删除某一个具体的路径,如果这个路径下面还有子路径,能够删除

  7. ls 看的是目录

  8. 客户端监听的操作 [重要] 监听的树 相关处理

    1. 树结构的变化 ls -w /createcloud 作用:监控 createcloud 目录下的节点的变化 ls -w /createcloud/teacher 注意:监听器 一次性的行为,如果需要反复监控,重复注册。
    2. 节点数据的变化 get -w /xxxx

Java代码 - 原生 ZK

  1. 准备 zk gui工具 (图形)

    1. PrettyZoo win mac linux
    2. IDEA 插件(zookeeper) 不建议IDEA
  2. 搭建开发环境的 【依赖jar】 zk的java代码工具 (zk的 java驱动)

xml 复制代码
     <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.6.2</version>
    </dependency>

    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>

还需引入 log4j.properties

typescript 复制代码
public class TestZKClient {
    private static ZooKeeper zooKeeper;

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        // zk集群中的每一个节点的ip:port
        // IDEA所在的机器中,进行主机映射
        // mac /etc/hosts
        // win c:\windows\system32\drivers\etc
        String connectionString = "192.168.74.128:2181";  // 多个 ip , 分割
        int sessionTimeout = 2000;
        zooKeeper = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
              /*  try {
                    List<String> children = zooKeeper.getChildren("/suns", true);
                    for (String child : children) {
                        System.out.println("child = " + child);
                    }
                } catch (KeeperException e) {
                    throw new RuntimeException(e);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }*/
            }
        });


        //使用6. 未来判断一个节点是否存在
        zooKeeper.exists("/xiaohei", new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                System.out.println("这个节点是存在的");
            }
        });
        System.in.read();

        //这种方式判断的是现在这个节点是不是存在
        Stat exists = zooKeeper.exists("/xiaohei", false);
        System.out.println("exists.toString() = " + exists.toString());


        // 使用5. 监听
        // 第一, 每一次监听到 都要在重复注册。
        // 第二, 只能监听当前路径的下一级,后续的子集无法监听
        zooKeeper.getChildren("/suns", true);

        System.in.read();


        //使用 1. 创建节点的操作 有数据
        zooKeeper.create("/suns", "xiaohei".getBytes(Charset.defaultCharset()), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        zooKeeper.create("/xiaohei", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        zooKeeper.create("/xiaojr",null, ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL);
        zooKeeper.create("/xiaojr",null, ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT_SEQUENTIAL);
        System.in.read();

        //使用2. 修改节点的数据
        // -1 版本的配对
        zooKeeper.setData("/suns", "xiaopp".getBytes(Charset.defaultCharset()), -1);

        //使用3. 查询
        List<String> children = zooKeeper.getChildren("/", false);
        for (String child : children) {
            System.out.println("child = " + child);
        }

        //使用4. 删除
        //1. 自己封装
        //2. curator zk high level api
        zooKeeper.delete("/a1", -1);
    }
}

Java 代码 - Curator

  • curator 是apache开源顶级项目

  • curator解决了原始zookeeper进行监听处理,繁琐不和理的问题。

  • curator解决原始zookeeper api 创建/删除多级目录的问题。

  • curator完善 client操作的可用性

  • curator解决原始zookeeper api 监听层级目录变化的问题。

  • curator封装了 zookeeper应用的场景。

    • master选举机制。
    • 分布式锁
    • 分布式计数器
    • ....
xml 复制代码
   <dependency>
      <groupId>org.apache.curator</groupId>
      <artifactId>curator-recipes</artifactId>
      <version>5.2.1</version>
   </dependency>

编码

  1. CuratorFramework 核心一个对象 CRUD操作 都是围绕着 这个对象完成。
  2. CuratorFramework 如何进行监听
typescript 复制代码
public class TestCuratorClient {
    public static void main(String[] args) throws Exception {
        //1 设置client重试
        ExponentialBackoffRetry backoffRetry = new ExponentialBackoffRetry(1000, 3, 3);
        //2 CuratorFramework
        String connectString = "192.168.74.128:2181";
        CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, 3000, 3000, backoffRetry);
        //客户端 开启
        client.start();

        //使用 4:数据的查询
        //测试 子路径的查询
        List<String> strings = client.getChildren().forPath("/z1");
        for (String string : strings) {
            System.out.println("string = " + string);
        }
        //测试 节点上面的数据
        byte[] dataBytes = client.getData().forPath("/z1/z2/z3");
        System.out.println("z3 node value " + new String(dataBytes));


        //使用 3:修改数据
        client.setData().forPath("/xiaojr", "buliangren".getBytes(StandardCharsets.UTF_8));


        //使用 2:删除
        client.delete().forPath("/xiaohua0000000013");
        //默认使用delete方法进行删除时,他只能删除单级目录
        client.delete().forPath("/z1");
        //删除多级目录的操作
        client.delete().deletingChildrenIfNeeded().forPath("/z1");


        //使用 1:创建
        //创建目录 在使用curator过程中,如果创建目录没有指定对应的数据,那么curator会把client的ip地址作为这个目录数据的默认值。
        client.create().forPath("/xiaohei");
        //创建目录 同时指定目录的数据
        client.create().forPath("/xiaojr", "sunshuai".getBytes(StandardCharsets.UTF_8));
        //创建多级目录
        client.create().creatingParentsIfNeeded()
            .forPath("/z1/z2/z3","xiaohei".getBytes(StandardCharsets.UTF_8));
        //创建不同类型节点 4种类型的节点
        client.create().creatingParentsIfNeeded()
            .withMode(CreateMode.PERSISTENT_SEQUENTIAL).forPath("/xiaohua");
    }
}
  1. curator 监听

    1. 监听节点数据变化
    2. 监听路径变化
  2. curator 监听核心对象

    1. CuratorCache ---> 缓存节点的原始数据 缓存节点的原始路径

    2. CuratorCacheListener ---> 监听器 ---> 业务处理

      1. NodeCacheListener 监听节点数据变化
      2. PathChildrenCacheListener 监听路径变化, /z1/z2/z3/z4 监听子(多级)路径
      3. TreeCacheListener 监听路径变化 不仅监听孩子,还能监听当前节点 /z1/z2
  1. Listener 注册到 Cache
ini 复制代码
//监听节点数据的变化
/*
   下面这段代码,我们并没有获得这个节点新的数据,只能监听变化
*/
public class TestNodeCacheListener1 {
    public static void main(String[] args) throws IOException {
        ExponentialBackoffRetry backoffRetry = new ExponentialBackoffRetry(1000, 3, 1000);
        CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.74.128:2181", 1000, 1000, backoffRetry);
        client.start();

        CuratorCache curatorCache = CuratorCache.build(client, "/xiaojr");
        // 监听节点变化
        CuratorCacheListener curatorCacheListener = CuratorCacheListener.builder().forNodeCache(new NodeCacheListener() {
            @Override
            public void nodeChanged() throws Exception {
                System.out.println("node value is change");
            }
        }).build();

        curatorCache.listenable().addListener(curatorCacheListener);
        curatorCache.start();

        System.in.read();

    }
}
/*
   下面这段代码,我们既可以监听节点数据变化,同时也可以获取节点新旧值
*/

public class TestNodeCacheListener2 {
    public static void main(String[] args) throws IOException {
        ExponentialBackoffRetry backoffRetry = new ExponentialBackoffRetry(1000, 3, 1000);
        CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.74.128:2181", 1000, 1000, backoffRetry);
        client.start();

        CuratorCache curatorCache = CuratorCache.build(client, "/xiaojr");
        // 监听节点数据变化
        CuratorCacheListener curatorCacheListener = CuratorCacheListener.builder().forChanges(new CuratorCacheListenerBuilder.ChangeListener() {
            @Override
            public void event(ChildData oldNode, ChildData node) {
                byte[] oldNodeBytes = oldNode.getData();
                byte[] newNodeBytes = node.getData();

                System.out.println("oldNode value is " + new String(oldNodeBytes));
                System.out.println("newNode value is " + new String(newNodeBytes));

            }
        }).build();

        curatorCache.listenable().addListener(curatorCacheListener);
        curatorCache.start();


        System.in.read();

    }
}
java 复制代码
/*
  监听子目录的变化 (不包括父目录)
*/
public class TestPathChildrenCacheListener {
    public static void main(String[] args) throws IOException {
        ExponentialBackoffRetry backoffRetry = new ExponentialBackoffRetry(1000, 3, 300);
        CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.74.128:2181", 1000, 1000, backoffRetry);
        client.start();

        CuratorCache curatorCache = CuratorCache.build(client, "/z1/z2");
        // 监听子目录的变化
        CuratorCacheListener curatorCacheListener = CuratorCacheListener.builder().forPathChildrenCache("/z1/z2", client, new PathChildrenCacheListener() {
            @Override
            public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
                System.out.println("child path change");
            }
        }).build();

        curatorCache.listenable().addListener(curatorCacheListener);
        curatorCache.start();

        System.in.read();
    }
}
ini 复制代码
/*
   监听子目录的变化 (包括父目录)TreeCacheListener
   监听子路径 同时也可以监听 父路径,但是不能处理爷爷
 */
public class TestTreeCacheListener {
    public static void main(String[] args) throws IOException {
        ExponentialBackoffRetry backoffRetry = new ExponentialBackoffRetry(1000, 3, 300);
        CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.74.128:2181", 1000, 1000, backoffRetry);
        client.start();

        CuratorCache curatorCache = CuratorCache.build(client, "/z1/z2");
        CuratorCacheListener curatorCacheListener = CuratorCacheListener.builder().forTreeCache(client, new TreeCacheListener() {
            @Override
            public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception {
                System.out.println("---tree cache listener----");
            }
        }).build();

        curatorCache.listenable().addListener(curatorCacheListener);
        curatorCache.start();

        System.in.read();
    }
}

设计注册中心

  1. 注册

    1. 需要把 RPC 集群中的服务节点 注册到注册中心
  2. ZK 树规划

    1. 三级目录

      1. /zk-register 注册中心节点 [持久节点]
      2. /zk-register/服务类型 服务 Service [持久节点]
      3. /zk-register/服务类型/ip+port 集群 [临时节点]

把每个 RPC 服务都看做是 ZK 客户端,服务启动后,通过 ZKClient 或者 Curator, 在 ZK 树状结构中, 创建一个临时节点, 这样就完成了服务的注册, 由于是集群是临时节点,那服务的上线下线就完全能体现出来了

相关推荐
yx9o39 分钟前
Kafka 源码 KRaft 模式本地运行
分布式·kafka
Gemini19951 小时前
分布式和微服务的区别
分布式·微服务·架构
G丶AEOM1 小时前
分布式——BASE理论
java·分布式·八股
P.H. Infinity7 小时前
【RabbitMQ】03-交换机
分布式·rabbitmq
龙哥·三年风水9 小时前
群控系统服务端开发模式-应用开发-个人资料
分布式·php·群控系统
点点滴滴的记录10 小时前
RPC核心实现原理
网络·网络协议·rpc
徒步僧10 小时前
ThingsBoard规则链节点:RPC Call Reply节点详解
qt·microsoft·rpc
funnyZpC11 小时前
quartz集群增强版🎉
java·分布式·开源·集群·定时任务
明达技术12 小时前
工业4.0时代下的分布式IO模块
分布式
天冬忘忧13 小时前
Spark 程序开发与提交:本地与集群模式全解析
大数据·分布式·spark