RPC框架中客户端的实现
RPC框架中客户端的实现
RPC客户端实现的难点在于客户端一般都不是单线程的,需要考虑在多线程的情况下如何流畅的使用客
户端而不会出现并发的问题,交互过程的模型图如下:
就如上面的模型图所示,在多线程的客户端中,客户端与数据库之间会维护一个连接池。当线程中的代码
需要访问数据库时,先从数据库中获取一个连接,与数据库交互完成后再将这个连接归还给线程池。所以对
于业务线程来说,拿到的连接不会同时被其他的线程所共享,这样就可以避免并发的问题
此外,服务器的性能往往随着并发连接数量的增加而下降,所以必须严格控制有效连接的数量;连接池的
数量上限也是数据库的一层壁垒,下面将介绍一些客户端设计中的主要机制
一、安全锁
连接池是为多线程而设计的,每个线程都会访问线程池的对象,所以线程池需要使用锁来控制数据结构
的安全。安全锁会使线程安全,但是也会导致性能受损。锁的临界区代码要尽量避免耗时的计算和I/O操作,
而且锁的粒度还要尽可能的细,但是代码实现不易
增大锁的粒度可能在某些程度上使代码实现更为容易,因为连接都是用来进行相对缓慢的I/O操作的,锁
是基于内存操作的,相比于I/O操作可以忽略不记
二、懒惰连接
连接池的连接多为懒惰连接,在需要的时候才会向数据库中申请。因为一个系统非常闲置,以前开辟了
太多的连接是对资源的浪费;懒惰的连接池可以保证只会对单线程的程序开辟一个连接
当然懒惰连接也有不好的地方,比如服务器的代码需要经过一个热身的过程,早来的请求需要额外付出
一次建立连接的耗时代价、如果数据库连接参数不正确,需要在收到用户的请求进行显式数据访问时才能发现
三、健康检查和性能追踪
连接池中管理的连接可能或因为网络原因而损坏断开连接,连接池需要保持内部管理的连接是健康可用
具体实现的机制如下:
线程从连接池中申请连接返回之前,线程池需要对连接进行检查,确定连接是通常的
线程将连接归还给连接池时,线程池对连接进行检查,确定连接没有被搞坏
线程池定时对管理的连接进行检查
如果检查发现连接有问题时,一般的做法两种:
抛弃当前的连接连接池的连接数量减1,如果是在线程的borrow方法中,那就再重新去连接池申请一个
修复当前连接,一般也就是执行重连
此外,好的连接池还应该考虑到性能的可追踪性,当用户通过线程池分配的连接去访问数据库时,
它的消息执行时间应该是可以被统计和追踪的。所以连接池往往还需要对原生的连接进行一定程度的
包装,在关键的函数代码调用前后增加性能统计代码,并对外提供监听端口,以便将统计信息传递给
外部的监控模块
四、超时策略
当业务线程繁忙的时,连接池内部的连接可能会出现不够用的场景,一个线程请求的borrow方法
在长时间的等待后仍然等不到空闲的连接,这就是超时问题,主要有以下是3种解决方案:
- 永不超时,等不到就一直接着等
- 一定的时间拿不到后,就像外部抛出超时异常,中断业务逻辑
- 如果发现连接池没有空闲连接,就去申请一个新的连接给调用方。当调用方归还连接的时候,连接池计算当前
缓存的连接数量,如果超过了最大的连接数,就将当前的连接销毁,否则就保存
五、多路复用
传统的RPC客户端都是基于一问一答的,同一个连接上连续的两个请求必须按先后顺序排队获取结果。高级
RPC客户端往往是同一个连接上可以同时进行多个请求,并且可以乱序的执行。通过在请求中增加一个唯一的ID
进行标识,服务器的响应消息携带请求ID到客户端,客户端就可以将响应和请求进行关联
上述实现多个请求的方式就是多路复用,HTTP1.X协议是一问一答的,但是HTTP2.0就具备了多路复用的
特性,Google开源的gRPC就是基于HTTP2.0的多用复用连接实现的高性能RPC框架
而且多路复用的连接往往都是线程安全的,它支持多个线程同时写入请求而不会出现并发问题,性能很好,
但是实现起来比较难
六、单向请求
对一些不是特别重要的请求可以不需要服务器进行响应,客户端在发送完请求后不需要等待结果直接返回
的就是单向请求,它一般适应于允许少量丢失的请求,比如日志信息
七、心跳
当客户端长期空闲时,服务器往往会自动关闭连接以减轻资源的消耗,当客户端再次请求时,就会遇到连接
已断开的错误;为了避免这种错误,一般有两种方法,一是当请求遇到连接错误时进行重连测试,另一种就是
通过心跳告知服务器不要关闭连接