对 RPC 框架的内存优化—改变服务引用方式

近来总是会有服务遇到 OOM 的情况,简单定位后发现 rpc 框架内存占用较多,看来是时候需要优化一波了。

占用内存膨胀

首先我们需要简单了解一下目前 rpc 框架的层次结构。

先从服务注册中心 zookeeper 的数据结构看,一个命名空间对应数个服务,而每个服务对应数个实例信息,我们的 API 信息则是与实例配置一同放在实例信息的 body 里面的。当我们根据用户配置的订阅列表拉取服务信息的时候,会将所有的 API 信息一同拉下来,目前在不改变存储粒度的情况下这点是无法优化的。

根据在 serviceContainer 对象中配置的订阅列表从 zk 上拉取数据后,为每一个服务实例分别创建了 serviceProvider 对象,同时由于我们拉取了这个服务实例的所有 API 信息,所以还为每一个 API 都创建了一个 apiInvoker 对象。

serviceProvider 对象是根据配置的订阅列表中的 vip 而创建的,所以是”按需创建的“。但 apiInvoker 对象确是拉取 zk 上这个服务下所有的 API 信息一股脑创建起来的,而大部分场景下也许我们仅仅只需要依赖其中少数几个 API 接口而已。

不顾真实的依赖需求,而一直都创建所有的 apiInvoker 对象,在微服务应用变多、需求多样化、调用关系复杂化、API 接口持续增长的同时,占用内存也会持续膨胀,甚至出现 oom 情况。

寻址缓慢

寻址缓慢的问题对于普通微服务客户端和 API 网关都存在,而 API 网关尤为严重。

我们需要先了解一个真实的请求是怎么寻址,找到对应的内存中已创建好的 apiInvoker 的。

对于普通微服务客户端来说,我们往往使用动态代理创建 API 接口代理对象,而代理 API 上往往会带注解使用类似服务唯一标识码来标识出这个 API 是属于哪个依赖服务的。有了这个服务 id 信息后,我们可以直接从内存中精确的找到对应的 serviceProvider 对象。

有了 serviceProvider 对象后我们还需要寻找 apiInvoker 对象,这里就没有这么方便了。初始化时在构建好每个 apiInvoker 对象后会依次加入到前缀树 prefixTree 里去,而在寻址时候通过代理 API 上的 url 信息来对 prefixTree 里的所有节点进行匹配从而找到对应的 apiInvoker。抛开前缀树的一些实现细节来说,我们相当于是需要遍历这个 serviceProvider 下的所有 apiInvoker 来进行匹配。

至于 API 网关就更惨了,由于请求是直接来自于外部的,不可能会带有服务标识码,我们不知道这个请求是属于哪个服务的,大多数情况下我们需要遍历所有的 serviceProvider 对象,然后再遍历里面的所有 apiInvoker 对象去依次匹配,直到匹配到为止。当然,这边如果接口设计规范,同一个服务都是统一前缀的话就好办很多,这就是另一个问题了。

从上面得知,不管怎么样,我们都需要遍历一个实例里的所有 API 信息去寻址。但实际场景上,也许我们只需要依赖其中的一两个接口而已。

服务引用优化

了解上述原理之后,服务引用方面的优化是顺其自然的。之前在声明服务引用的时候,只是到服务级别,即配置了服务的订阅列表。但是其实更合理的是需要到 API 接口级别的,即描述我们到底需要依赖哪些接口。

那么 API 网关在配置订阅列表时,在配置服务订阅同时声明这个服务下面的接口依赖。网关在管理平台的界面上添加服务信息及服务下面的接口信息后存储至数据库,初始化时直接从数据库抓取依赖信息并创建改造过的的 serviceContainer 对象即可。

rpc:
  reference:
    com.qh.middleware.rpctest:
      - /test/testJson:POST
      - /test/testKryo:POST

对于普通微服务客户端,是不太可能去直接配置 API 订阅的。因为使用者使用的代理对象都是基于 class 粒度的,所以这里在配置服务订阅的同时还需要配置服务下订阅的 API 接口类的全限定类名,与 dubbo reference 类似。这样,在取到需要订阅的 class 后会去扫描里面的所有 API method 信息,并转换成相应的 API 订阅。

rpc:
  reference:
    com.qh.middleware.rpctest:
      - com.qh.mdw.api.testApi
      - com.qh.mdw.api.testApi2

这样,每个 serviceProvider 对象中只会按需创建描述过的 apiInvoker 对象,而不是 zk 上拉取的所有 API。这样 apiInvoker 对象的数量就会按实际情况减少,服务内存占用减少,网关和微服务客户端的寻址也都会快很多。

留下你的脚步
推荐阅读