聊聊RPC之Consumer

本文详细解析RPC框架中的Consumer角色,包括如何找到Provider地址、通信方式、异常处理等,并探讨Consumer的开发实践,如面向异常编程、错误日志与监控、模型适配和测试策略。着重介绍了Dubbo的Consumer行为及其与Provider的交互机制。

上一篇我们介绍了RPC的Provider,包括它的发布过程和在设计一个Provider时需要考虑的问题,本文将介绍做一个调用方,在这个Consumer调用过程中,RPC又帮我们做了哪些事情和在使用别人提供的Provider时需要注意哪些问题。(和上篇一样,我们仍以Dubbo为例)

Consumer的调用过程

作为Consumer调用别人提供的一个服务,一般需要如下工作

  1. 引入client包

          <dependency>
               <groupId>info.yywang.demo</groupId>
               <artifactId>hello-client</artifactId>
               <version>1.0.0-SNAPSHOT</version>
           </dependency>
  2. Spring中引用远程服务

       <dubbo:registry address="zookeeper://192.168.0.122:2181" />
    
       <dubbo:reference id="helloService" interface="info.yywang.service.HelloService" />
  3. 调用远程服务

    public class HelloTest{
    
     @Resources
     private HelloService helloService;
    
     @Test
     public void testSay(){
       helloService.say('yywang');
     }
    }

通过以上代码,我们可以看出,只需要我们在spring配置中,增加一些配置,其他的就像我们使用本地接口一样,来使用远程接口。那么在这个背后,RPC框架帮我们做了什么呢?(今天不在赘述过程,也不在针对Dubbo框架,我们从几个问题思考)

Consumer端怎么找到Provider的地址

答:从注册中心获取到Provider的地址,并且根据路由规则,比如随机、轮训等方式,最后获取到一个将要方式的Provider地址,一般Consumer端会把Provider的地址缓存到本地,当请求来的时候,从本地获取。

Consumer端怎么和Provider进行通信

答:Consumer端和Provider的通信有两种方式,一种是同步调用,一种是异步调用。

  1. 同步调用

    一个工作线程在调用远程服务时,在得到结果之前,一直处于阻塞状态。在启动后,建立一个连接池,当一个请求来了之后,首先会经过对象序列化,然后从连接池里拿到一个连接,发送请求包,请求远程的服务,当远程服务响应结果之后,把连接放回连接池。然后工作线程继续执行以下代码

  2. 异步调用

    一个工作线程,在发起调用之后,在拿到结果之前,不会阻塞。在Dubbo的dubbo协议底层,使用dubbo协议就是基于Netty的底层通信模型。过程太长暂时不说了。

请求包里有哪些内容

答:请求包里有接口名,方法名字,参数类型等,参数值,请求ID。

异步情况下,怎么把请求、响应对应起来

答:在远程调用时,首先会生成一个ID用于标记本次请求,并且把这个ID发送给服务端,同时使用这个ID作为Key,将调用信息放在一个map里。在服务端响应的时候,把这个ID带过来,然后通过这个ID直接从map里找到对应的信息

Consumer怎么知道Provider挂了

答:在Consumer端缓存了Provider的服务地址之后,Consumer会和Provider保持心跳,同时在Provider挂掉之后,ConfigServer也会通知Consumer,Consumer在本地的地址列表里清除该地址

Consumer怎么知道Provider端增加了机器

答:同上ConfigServer会通知Consumer端增加服务地址

以上可以看出ConfigServer在这里扮演了很重要的角色,下篇文章我们将揭秘ConfigServer具体怎么实现通知的

Consumer的开发

以上是对于原理的讲解,我们回到作为RPC的使用者来说,我们探讨一些几个问题

面向异常编程

在使用了RPC之后,我们不得不面对一个问题,就是网络通信。在网络通信过程中,就会有各种不确定的问题,所以在使用远程接口调用时,我们要考虑以下异常场景:

  1. 超时了怎么办

    超时一般有几种情况,一种是在调用时机器负载比较高,响应比较慢。第二种,方法本身存在性能问题。第二种,可能在调用的过程中,网络出现了抖动,可能调用方的逻辑已经执行完成了,但是由于网络原因,没有及时的响应。

    对于超时情况,通常的处理办法是重试,一旦选择了重试,就需要在实现具体的业务时,分析需不需要做幂等。需要注意对于写的操作,如果重复执行,可能出现两次写入相同的值。关于如何做幂等又是一个话题,大家可以思考

    对于方法本身的性能问题,首先可以通过提高超时时间来解决当前问题,同时需要催促服务提供方做性能改进。

  2. 调用返回了失败

    返回失败的可能性有几种,第一种是业务错误,第二种是程序错误。对于业务错误很简单,根据业务逻辑处理即可,但是当对于业务错误,需要做监控时,需要记录好用于监控的错误日志。对于程序错误,可能出现在几点,一是程序bug,对于程序bug已经要记录好错误日志。第二是网络原因等错误。这种错误可以通过重试机制来保证。

错误日志,监控日志
  1. 错误日志,是排查线上问题时的最有利的武器,没有之一。线上处理问题,所以在调用远程服务的时候,一定需要记录好错误日志,但是日志也不是越多越好,日志过多也会代码排查问题的难度。并且在记录错误日志的同时,需要记录好关键的业务字段,比如订单编号、商品Code等等
  2. 监控日志,对于线上的出现的业务错误,我们通常需要通过监控来获取,有的时候监控日志和错误日志在一块,有的专门记录一个监控日志用于监控,个人推荐对于监控专门记录日志,用于监控。另外监控日志同样需要记录好关键的监控参数。
外部系统模型和本系统模型的适配

这个问题是在系统设计层面上的问题。就是在设计时,如何做到外部系统模型和本系统模型的适配。解决这个问题很简单,增加防腐层,就是增加一层防腐层,来将其他系统的模型适配到本系统模型,这样本系统的代码将不会充斥着其他系统模型的代码,并且在防腐层还能在提供方的系统发生变化时帮助本系统保持稳定。

如何测试你的代码

当你调用了远程接口之后,你想编写单元测试代码来测试自己的代码。如果你直接调用远程接口,有可能远程接口还调用了其他的远程接口。所以你需要准备满足这些系统的测试数据,这是非常麻烦的过程,通常这时就需要使用mock的方式来模拟远程接口。这样你只需要关心自己的逻辑即可。在集成测试时,在使用真实接口。

欢迎关注我的公众号MyArtNote

MyArtNote

### RPC 架构中的 Consumer、Provider 和注册中心 #### 1. Provider 的作用与实现方式 在RPC架构中,Provider是指服务提供者。其主要职责在于对外暴露可被其他应用调用的服务接口[^2]。为了使这些服务能够被远端消费者发现并调用,通常会经历如下几个阶段: - **服务初始化**:启动时完成业务逻辑的加载以及必要的资源准备。 - **服务发布**:向注册中心上报自身的存在信息(如主机名、端口号等),以便于后续消费者的查找。 ```java public class ServiceProvider { public void publishService(Object service, String interfaceName){ // 将service绑定到指定interfaceName,并将其加入本地缓存 services.put(interfaceName, service); // 向注册中心报告本机提供的服务列表 registerCenter.register(interfaceName, localAddress()); } } ``` #### 2. Consumer 的角色及其工作原理 Consumer即服务消费者,负责发起对远程服务实例的具体请求操作。当一个应用程序想要调用另一个程序所提供的功能时,它实际上是在扮演着Consumer的角色。具体来说,这涉及到以下几个方面的工作: - **获取目标服务位置**:通过查询注册中心来定位所需服务的实际部署地点; - **建立连接并发送消息**:依据获得的信息构建通往对应节点之间的链路,之后按照既定协议格式打包参数传递给对方; - **接收响应数据**:等待来自服务器端返回的结果,并据此执行下一步动作; ```java public class ServiceConsumer<T> implements InvocationHandler{ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 查询注册中心得到可用的服务提供者地址集合 List<String> providers = registry.lookup(serviceInterface.getName()); // 根据负载均衡策略选取其中一个作为实际调用对象 String selectedProviderUrl = loadBalancer.select(providers); // 创建代理客户端并向选定的目标发出同步/异步调用... RpcClient client = new RpcClient(selectedProviderUrl); return client.call(method, args); } } ``` #### 3. 注册中心的功能描述及其实现手段 注册中心在整个分布式环境中充当了至关重要的枢纽角色,用于维护各个成员间的关系图谱——特别是关于哪台机器正在运行什么类型的后台进程这类元数据记录[^4]。除此之外,还承担着诸如健康监测、流量调度等多项附加职能。常见的技术选型包括但不限于Zookeeper、Consul或是简单的键值存储方案像Redis也可以胜任此任[^3]。 对于如何搭建这样一个设施,则需考量多方面的因素,比如性能需求、可靠性保障措施等等。下面给出了一种基于内存哈希表模拟简易版注册中心的核心代码片段: ```java import java.util.HashMap; import java.util.List; class SimpleRegistry { private final HashMap<String,List<ServiceInstance>> serviceMap=new HashMap<>(); /** * 记录新的服务实例或更新已有条目 */ synchronized void register(String serviceName,String address,int port){ var instances=serviceMap.computeIfAbsent(serviceName,k->new ArrayList<>()); instances.add(new ServiceInstance(address,port)); } /** * 查找特定名称下的所有活跃实例 */ synchronized List<ServiceInstance> lookup(String serviceName){ return Collections.unmodifiableList(serviceMap.getOrDefault(serviceName,Collections.emptyList())); } } record ServiceInstance(String host,int port){} ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值