Rpc 行为优化 - 自动选择调用目标为 dev 或 test 背景

Rpc 自动选择调用目标

背景

随着系统的不断细化完善,服务的数量也水涨船高,为了在起服务的时候不用一次性把所有服务都启动,Feign 的调用目标默认为测试环境,但有些需求会涉及多个服务,这时需要把这部分服务的 Rpc 调用改成本地服务

目前是方式是手动修改启动参数 -Dfeign.client.serviceName.url=http://127.0.0.1:8080 来解决,十分不优雅,而且切换时非常麻烦,于是需要一个自动切换调用目标的解决方案

除了修改手动参数以外,也可以直接修改代码,但这很容易出问题,一旦不小心提 Merge Request 到测试环境,而且 Code Review 的同学没有仔细看的话,会导致测试环境出现部分不可用,影响到其他同学正在进行的验证/对接工作

预期效果

  • Rpc 调用源自动选择
    • 如果本地服务可用,那么 Rpc 的目标为本地服务
    • 如果本地服务不可用,那么 Rpc 的目标为测试环境
  • 本地环境开箱即用,不需要其他同学的学习成本

技术选型

  • 鉴于我们团队使用的是 SpringCloud 框架,在 Rpc 这块使用 Open-Feign,因此通过 Feign 拦截器的方式处理这个问题

  • 如何判断调用源是否可用,这个问题有两个解决方案

    • 利用 K8s 的存活探针接口(语言无关,但验证时如果目标不可用,需要等待一定的超时时间)
    • 利用 JDK 提供的 jps 工具(与 Java 绑定,不那么可靠,服务在启动过程中也会被验证为可用,但可以相对直接地验证)

    经过思考后,由于项目使用 SpringCloud,整体上已经选择 Feign 拦截器来实现,很难跳出 Java 语言,因此利用 jps 来实现并不会造成太大的困扰

    同时,服务启动也不会花费太多时间,即使在服务启动的过程中被验证为可用,稍等几秒即可

  • 缓存

    不可能每次 Rpc 的时候都判断调用源是否可用,如果一个接口在调用过程中疯狂验证同一个目标是否可用,未免太浪费资源,缓存主要有两方面的方案:本地缓存或中间件缓存

    由于这个方案主要为本地环境服务,如果使用中间件缓存的话,需要对不同机器的缓存根据 MAC 地址等内容进行隔离,因此直接使用 Guava Cache / Caffine 等本地缓存即可,不需要使用 Redis 等中间件进行缓存

上代码

@Profile("dev")
@Configuration
public class FeignAutoLocalConfiguration implements RequestInterceptor {

    /**
     * feign 的优先目标, 为 dev 时会优先走本地, 本地未启动时走测试, 其他值会直接走测试
     */
    @Value("${feign.client.auto.local.primary:dev}")
    private String primary;

    @Data
    private static class RpcAutoLocalConfig {
        /**
         * 服务名, 通过它来建立 <服务名, 判断本地服务是否可用需要的其他参数> 之间的联系
         */
        private String serviceName;

        /**
         * 服务的启动类名, 比如 MyServiceApplication
         */
        private String appName;

        /**
         * 服务在本地启动时的端口
         */
        private Integer localServicePort;
    }

    /**
     * Rpc Local 配置信息, 这里不想依赖 apollo, 因此没有直接使用 @ApolloJsonValue, 而是自行序列化
     */
    @Value("${feign.client.auto.local.config:[]}")
    private String feignAutoLocalConfigListStr;

    /**
     * 服务名 => Rpc Local 配置信息
     */
    private Map<String, RpcAutoLocalConfig> serviceName2RpcAutoLocalConfig;

    /**
     * 本地服务是否启动的缓存, 启动类名 => 是否启动
     * 10s 足够完成一个复杂请求的多次 rpc, 也足够启动一个本地服务使缓存值失去意义
     */
    private final Cache<String, Boolean> APP_NAME_TO_HAS_LOCAL_RUN_CACHE = Caffeine.newBuilder()
            .expireAfterWrite(10, TimeUnit.SECONDS)
            .build();

    /**
     * 反序列化 rpcAutoLocalConfigListStr, 并转为 Map
     */
    @PostConstruct
    private void initServiceName2RpcLocalConfig() {
        serviceName2RpcAutoLocalConfig = JSON.parseArray(feignAutoLocalConfigListStr, RpcAutoLocalConfig.class)
                .stream()
                .collect(Collectors.toMap(RpcAutoLocalConfig::getServiceName, Function.identity()));
    }

    /**
     * 判断本地服务是否已启动
     *
     * @param appName 服务启动类名
     */
    private boolean hasLocalRun(String appName) {
        var jpsResult = RuntimeUtil.execForStr("jps");
        var appNameUpperCase = StringUtils.upperCase(appName);
        return StrUtil.split(jpsResult, "\n")
                .stream()
                .map(String::toUpperCase)
                .anyMatch(item -> item.contains(appNameUpperCase));
    }

    /**
     * 把 rpc 请求的 target 修改为本地环境的服务
     */
    private void tryChangeTargetToLocal(RequestTemplate template) {
        var serviceName = template.feignTarget().name();

        if (!serviceName2RpcAutoLocalConfig.containsKey(serviceName)) {
            return;
        }
        var rpcInfo = serviceName2RpcAutoLocalConfig.get(serviceName);

        var localHasRun = APP_NAME_TO_HAS_LOCAL_RUN_CACHE.get(rpcInfo.getAppName(), this::hasLocalRun);
        if (BooleanUtils.isFalse(localHasRun)) {
            return;
        }

        template.target("http://127.0.0.1:" + rpcInfo.getLocalServicePort());
    }

    @Override
    public void apply(RequestTemplate template) {
        if (Objects.equals(primary, "dev")) {
            tryChangeTargetToLocal(template);
        }
    }
}

具体使用

  • 确保 apollo 上面 dev 调用的的 feign 地址为测试环境

  • 在 dev 环境的 feign.client.auto.local.config 中追加各系统相关的配置,格式为:

    [
      ...,
      {
        "serviceName":"服务名, 比如 service1",
        "appName":"服务的启动类名, 比如 XXXApplication",
        "localServicePort":"服务在本地启动时的端口, 比如 8080"
      }
    ]
    
  • 如果希望一直调用测试环境,提供相关参数 feign.client.auto.local.primary 供使用,支持在启动时使用 -Dfeign.client.auto.local.primary=dev 的方式指定优先级

    • 值为 dev 时,优先走本地,如果本地服务不可用,则走测试环境,这也是默认策略

    • 值为其他内容时,会直接走测试环境,不考虑本地服务,有其他需要的话可以自行扩展

为避免重复判断,对于调用源是否可用的判断会缓存 10s,调用源从本地切到测试环境时需要小等一会儿

Tip

如果当前这个拦截器不是放在对应项目里,而是放在 Common 项目里,需要往 spring.factories 里面把这个 Bean 配进去

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  ......,\
  org.divine.place.common.feign.FeignAutoLocalConfiguration
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值