自定义一个简单的RPC框架(二)

前言:

    上一篇文章中,我们通过一个简单的示例来模拟了RPC的调用。后续通过Netty改造了服务端框架,适当的提高了服务端性能。

    但是改造后的可以作为一个底层RPC框架来使用吗?显然是不能,还缺了很多东西,性能还有很多需要优化的地方。

    本篇文章我们就一起来分析下如果想做成一个成熟点的RPC框架,还需要做哪些工作。

1.面临的挑战

    * 服务端是单机的,单机的应用永远都会有单点故障的问题,所以需要做成集群模式

    * 如果服务端是集群的,那么客户端在调用的时候需要知道具体应该调用哪个服务端

    * 客户端调用服务端集群中的A机器如果失败了(A服务挂了或者在规定时间内没有响应),应该支持自动调用集群中的其他机器

    * 客户端或者服务端服务应该需要支持扩展,可以无侵入的对服务进行扩展(比如在服务端记录请求的基本信息、对请求进行报表统计)

    

    * 服务端应该支持不同的语言(不只是java)

    * 可以提供更高效的序列化方式(或者允许用户自定义序列化方式)

    ...

以上都是当我们需要创建一个RPC框架所需要考虑的问题。当然这还只是一部分,如果想做成一个更大型的成熟的框架,还需要更多的功能支持。

2.序列化

    我们先来解决一个简单的,就是序列化方式的优化。

    序列化,就是将对象类型转变成字节数组的过程;反序列化,顾名思义,就是将字节数组转换为对象类型的过程。

    默认情况下我们使用的java中自带的序列化方式,这种序列化方式并不高效,而且生成的字节数组相对而言还是比较大的,不利于传输。

2.1 常用的序列化方式

    我们直接拿一张图来展示下常用的序列化方式效能及空间开销对比。数据图片来自 https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking 

解析性能对比:

解析后占用空间对比: 

 

可以看到,java自带的序列化方式确实在效率和占用空间两方面都不占优势,唯一的优势就是简单吧,毕竟是自带的。

而在效率和空间两方面都比较强的,就是protobuf了。

总结:

1)公司内部调用,都是java应用,性能要求不算高的话,可以考虑java序列化方式

2)如果有较高的调试要求(能通过日志等方式看到请求响应结果),可以考虑使用JSON、XML方式

3)当对性能有很高的要求,那么可以考虑使用protobuf、thrift、avro方式

4)当对跨语言支持有要求时,可以考虑JSON、XML、protobuf等,其中protobuf可以优先考虑

3.注册中心

    之前说过,单服务器容易产生单点故障问题,所以需要集群模式的提供方式。

    既然有那么多服务端,我们总要找一个地方来记录下这些服务端的信息,不然客户端是无法知道具体有哪些服务端可供调用的。

    这时就需要一个叫做注册中心的东东。一般来说,我们可以使用Zookeeper作为注册中心,服务端启动后都将服务信息注册到zookeeper上某一个节点下,如下

/server作为一个公共namespace,service_name就是具体的服务名称,下面的子节点server1_ip:port就是服务端信息,客户端在调用具体服务时可以先寻找到具体的service_name,然后根据其子节点再选择一个适合的服务端发起调用即可。

总结:

    作为注册中心本身需要有哪些问题要考虑呢?

    1)注册中心本身要是高可用的,否则注册中心挂掉了,那么客户端就无法获取最新的服务端信息

    2)客户端从注册中心获取到服务端的信息后,可以保存在本地,这样就不用每次都从注册中心获取服务端信息。如果这样设计的话,注册中心就需要有一个自动推送功能(当出现新的服务端或者有的服务端不可用时,需要及时将这些信息推送给客户端)

    

3.1 常用的注册中心

    除了Zookeeper,还有些其他常用的注册中心,具体见下表。表格来自 https://www.cnblogs.com/allennote/articles/12459814.html 

Nacos Eureka Consul zookeeper
数据一致性 AP/CP AP CP CP
健康检查 TCP/HTTP/MySql/ClientBeat ClientBeat TCP/HTTP/grpc/Cmd Keep Alive
负载均衡策略 权重/metadata/Selector Ribbon Fabio -
雪崩保护
容量 100w 5000 百万级 百万级
自动注销实例 ×
访问协议 HTTP/DNS HTTP HTTP/DNS TCP
监听支持
多数据中心 ×
跨注册中心同步 × ×
Spring Cloud集成 ×
Dubbo集成 × ×
K8s集成 × ×

4.服务路由

    当多个服务端都注册到注册中心后,客户端在调用时就有据可依。既然可以有这么多的服务端供调用,具体调用时应该选择哪一个呢?这就是服务路由需要考虑的事情。

常见的路由模式有以下几种:

1)随机(这个比较容易,随机调用即可)

2)轮询(客户端的轮询调用对服务端的压力而言会比较均匀,一般的RPC框架会默认选择这种方式)

3)基于权重轮询(服务端的性能可能不尽相同,则可以针对高性能的服务端配比高权重,让其承受更多的调用量)

4)粘滞调用(有点类似nginx的 ip_hash策略,粘滞调用常用于用状态服务,尽可能让客户端总是调用同一个服务端)

5)条件路由(配置白名单访问、通过ip条件表达式进行访问控制等,支持用户特定场景的自定义)

6)多机房路由(针对多机房的服务提供场景,尽量让客户端进行同机房调用,避免跨机房)

5.集群调用容错

    所谓调用容错,就是当客户端对集群中的某一个服务端发起调用时,若服务端异常或者在规定时间内没有返回结果值,则框架应该支持自动容错,可以根据其策略选择其他的服务端进行调用,或者记录下来,后续继续发起调用。

    常见的容错策略:

策略执行方式 适用场景 备注
Failover 调用异常后,重新选择一个服务端重新执行 幂等类的调用 需要对最大重试次数做限制
Failback 调用异常后,不再重试其他服务端,将异常抛给客户端 非幂等调用,需要客户端自行解决异常
Failcache 调用异常后,先将该调用信息缓存下来,等待T周期后,
重新调用,直到该服务端能够正常处理该消息
对消息延迟不敏感, 重试次数、等待时间都要有限制;
服务端异常不能是无法恢复的异常,
或者由于业务操作导致的异常都不能使用Failcache策略
Failfast 调用异常后,直接失败,不再重试 非核心业务,调用失败记录日记即可

用户可根据各自的业务特点选择适当的容错策略即可。

6.服务端扩展

    服务端除了正常提供功能支持之外,通常还有些其他需求。比如:

    1)期望对客户端的请求进行鉴权,没有权限的客户端直接拒绝访问

    2)服务端针对具体接口请求进行限流操作,过高流量则直接拒绝

    2)客户端的请求信息进行记录,后续方便统计回溯

    ...

    针对这一系列功能的扩展,我们可以使用类似Spring AOP的扩展,通过切面的方式对客户端请求进行拦截,拦截到之后,先进行这些扩展功能的操作,都成功后,再进行最后的服务调用。

总结:

    以上种种,只是我们实现RPC框架的一些扩展想法。想真正实现一个好的RPC框架,需要做的工作还远远不止上面列举的这些。

    所以,如果公司规模不大,无法自研一个RPC框架的话,建议还是选择市面上比较成熟的框架直接使用的好。比如Dubbo,gRPC、Thrift等。

    有了这些知识点的铺垫,后续我们就真正的进入到Dubbo的学习中,带着问题来学习,看看Dubbo作为一个成熟的框架是如何解决上述问题的。

猜你喜欢

转载自blog.csdn.net/qq_26323323/article/details/121407827