RPC框架中客户端的实现


RPC客户端实现的难点在于客户端一般都不是单线程的,需要考虑在多线程的情况下如何流畅的使用客

户端而不会出现并发的问题,交互过程的模型图如下:

3weuJU.jpg

就如上面的模型图所示,在多线程的客户端中,客户端与数据库之间会维护一个连接池。当线程中的代码

需要访问数据库时,先从数据库中获取一个连接,与数据库交互完成后再将这个连接归还给线程池。所以对

于业务线程来说,拿到的连接不会同时被其他的线程所共享,这样就可以避免并发的问题

此外,服务器的性能往往随着并发连接数量的增加而下降,所以必须严格控制有效连接的数量;连接池的

数量上限也是数据库的一层壁垒,下面将介绍一些客户端设计中的主要机制

一、安全锁

3wlqbQ.png

连接池是为多线程而设计的,每个线程都会访问线程池的对象,所以线程池需要使用锁来控制数据结构

的安全。安全锁会使线程安全,但是也会导致性能受损。锁的临界区代码要尽量避免耗时的计算和I/O操作,

而且锁的粒度还要尽可能的细,但是代码实现不易

增大锁的粒度可能在某些程度上使代码实现更为容易,因为连接都是用来进行相对缓慢的I/O操作的,锁

是基于内存操作的,相比于I/O操作可以忽略不记

二、懒惰连接

连接池的连接多为懒惰连接,在需要的时候才会向数据库中申请。因为一个系统非常闲置,以前开辟了

太多的连接是对资源的浪费;懒惰的连接池可以保证只会对单线程的程序开辟一个连接

当然懒惰连接也有不好的地方,比如服务器的代码需要经过一个热身的过程,早来的请求需要额外付出

一次建立连接的耗时代价、如果数据库连接参数不正确,需要在收到用户的请求进行显式数据访问时才能发现

三、健康检查和性能追踪

连接池中管理的连接可能或因为网络原因而损坏断开连接,连接池需要保持内部管理的连接是健康可用

具体实现的机制如下:

  • 线程从连接池中申请连接返回之前,线程池需要对连接进行检查,确定连接是通常的

  • 线程将连接归还给连接池时,线程池对连接进行检查,确定连接没有被搞坏

  • 线程池定时对管理的连接进行检查

如果检查发现连接有问题时,一般的做法两种:

  • 抛弃当前的连接连接池的连接数量减1,如果是在线程的borrow方法中,那就再重新去连接池申请一个

  • 修复当前连接,一般也就是执行重连

此外,好的连接池还应该考虑到性能的可追踪性,当用户通过线程池分配的连接去访问数据库时,

它的消息执行时间应该是可以被统计和追踪的。所以连接池往往还需要对原生的连接进行一定程度的

包装,在关键的函数代码调用前后增加性能统计代码,并对外提供监听端口,以便将统计信息传递给

外部的监控模块

四、超时策略

当业务线程繁忙的时,连接池内部的连接可能会出现不够用的场景,一个线程请求的borrow方法

在长时间的等待后仍然等不到空闲的连接,这就是超时问题,主要有以下是3种解决方案:

  • 永不超时,等不到就一直接着等
  • 一定的时间拿不到后,就像外部抛出超时异常,中断业务逻辑
  • 如果发现连接池没有空闲连接,就去申请一个新的连接给调用方。当调用方归还连接的时候,连接池计算当前

缓存的连接数量,如果超过了最大的连接数,就将当前的连接销毁,否则就保存

五、多路复用

传统的RPC客户端都是基于一问一答的,同一个连接上连续的两个请求必须按先后顺序排队获取结果。高级

RPC客户端往往是同一个连接上可以同时进行多个请求,并且可以乱序的执行。通过在请求中增加一个唯一的ID

进行标识,服务器的响应消息携带请求ID到客户端,客户端就可以将响应和请求进行关联

上述实现多个请求的方式就是多路复用,HTTP1.X协议是一问一答的,但是HTTP2.0就具备了多路复用的

特性,Google开源的gRPC就是基于HTTP2.0的多用复用连接实现的高性能RPC框架

而且多路复用的连接往往都是线程安全的,它支持多个线程同时写入请求而不会出现并发问题,性能很好,

但是实现起来比较难

六、单向请求

对一些不是特别重要的请求可以不需要服务器进行响应,客户端在发送完请求后不需要等待结果直接返回

的就是单向请求,它一般适应于允许少量丢失的请求,比如日志信息

七、心跳

3wKlh6.png

当客户端长期空闲时,服务器往往会自动关闭连接以减轻资源的消耗,当客户端再次请求时,就会遇到连接

已断开的错误;为了避免这种错误,一般有两种方法,一是当请求遇到连接错误时进行重连测试,另一种就是

通过心跳告知服务器不要关闭连接