微服务dubbo+Nacos+k8s+shenyu解决本地联调困难问题,使用自定义负载均衡实现

286 阅读5分钟
  1. 前端支持

前后端联调需要前端在发送http请求时在header增加localIp参数,值为后端的ip地址。

  1. 后台配置修改

  1. ci脚本调整

改此配置目的是将k8s中的docket容器提供的dubbo接口通过nodeport映射成外部能访问的端口。并将nodeport端口注册到nacos上。

  1. dubbo配置调整

    dubbo:
      protocol:
        port: 20880 # 此端口固定。需要用noteport映射,  不固定应该也是20880
      consumer:
        loadbalance: localFirst  # 自定义负载均衡策略,此策略会优先获取localIp的服务。如果找不到获取权重最高的。
      provider:
        weight: 1 # 自己的配置文件中,一定不能把权重配置太高。 dev环境权重配置100。本地的配置不能超过100。否则dev环境调用的时候即使没有传localIp也会调用到你本地。
    
  1. 网关配置调整

  1. 本地注册到dev的网关上。
  2. 修改divide插件选择器的配置。weight配置成1。保证本地的权重比dev环境的低即可。
  3. 修改规则中负载均衡策略,设置成localFirst。网关的默认值已经调整,dev环境默认值已经调整成localFirst
  1. 启动项目

  1. Nacos dev命名空间增加配置文件 application-common-**.yml
  2. 修改2.2dubbo配置调整 章节提到的配置文件
  3. 本地application.yml中调整引用的配置文件

config:

import:

  • optional:nacos:application-common.yml

修改为

config:

import:

  • optional:nacos:application-common-**.yml
  1. 启动项目

  2. 代码

  1. 网关负载均衡策略

  1. LocalFirstLoadBalance

package com.**.shenyu.gateway.loadbalancer;


import cn.hutool.core.collection.CollUtil;
import com.**.shenyu.gateway.util.ExchangeContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.shenyu.loadbalancer.entity.Upstream;
import org.apache.shenyu.loadbalancer.spi.AbstractLoadBalancer;
import org.apache.shenyu.loadbalancer.spi.RandomLoadBalancer;
import org.apache.shenyu.spi.Join;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

/**
 *  <p>PURPOSE:
 *  <p>DESCRIPTION:
 *  <p>
 *
 *  </p>
 *  <p>CALLED  BY:
 *  <p>CREATE  DATE:  2024-09-10 15 :48
 *  <p>UPDATE  DATE:
 *  <p>UPDATE  USER:
 *  <p>HISTORY:
 *
 *  @author  chenzhaoheng
 *  @version  1.0
 *  @see
 *  @since  java  1.8
 */
@Service
@Slf4j
@Join
public class LocalFirstLoadBalance extends AbstractLoadBalancer {
    private final String HEADER_NAME = "localIp";
    private final String COLON = ":";
    // 使用已有的随机负载均衡
    private final RandomLoadBalancer randomLoadBalancer = new RandomLoadBalancer();

    @Override
    protected Upstream doSelect(List<Upstream> upstreamList, String ip) {
        String clientIp = getClientIp();
        // 优先选择本地 IP 匹配的实例
        Optional<Upstream> localUpstream = upstreamList.stream()
                .filter(upstream -> extractIpFromUrl(upstream).equals(clientIp))
                .findFirst();

        // 如果找到本地 IP 的实例,直接返回
        if (localUpstream.isPresent()) {
            log.info("Selected local instance with IP: {}", clientIp);
            return localUpstream.get();
        }

        // 如果没有找到本地 IP 匹配的实例,则选择权重最高的实例
        Optional<Upstream> highestWeightUpstream = upstreamList.stream()
                .max((u1, u2) -> Integer.compare(u1.getWeight(), u2.getWeight()));

        // 如果找到权重最高的实例,返回它
        if (highestWeightUpstream.isPresent()) {
            log.info("Selected instance with highest weight: {}", highestWeightUpstream.get().getUrl());
            return highestWeightUpstream.get();
        }

        // 如果没有找到权重最高的实例,使用随机负载均衡策略
        log.info("No local instance or weighted instance found, falling back to random load balancer");
        return randomLoadBalancer.doSelect(upstreamList, ip);
    }

    /**
 *  从  URL  中提取  IP  地址
 *  @param  upstream  Upstream  实例
 *  @return  IP  地址
 */
private String extractIpFromUrl(Upstream upstream) {
        return upstream.getUrl().split(COLON)[0];
    }

    /**
 *  从请求的  header  中获取本地  IP  地址
 *  @return  本地  IP  地址
 */
private String getClientIp() {
        ServerHttpRequest request = ExchangeContext.getExchange().getRequest();
        HttpHeaders headers = request.getHeaders();
        List<String> strings = headers.get(HEADER_NAME);

        if(CollUtil.isEmpty(strings)){
            return null;
        }

        return strings.get(0);
    }
}

2. ### org.apache.shenyu.loadbalancer.spi.LoadBalancer

localFirst=com.**.shenyu.gateway.loadbalancer.LocalFirstLoadBalance

3. ### 网关配置页面增加负载均衡字典值

  1. dubbo负载均衡

  1. CommonRequestContext

ThreadLocal中存储ip和用户信息使用。

package com.**.common.satoken.requestcontext;

import cn.hutool.core.collection.CollUtil;
import com.**.common.core.utils.JsonUtils;
import com.**.common.satoken.core.model.LoginUser;

import java.util.HashMap;
import java.util.Map;

public class CommonRequestContext {
    private static final InheritableThreadLocal<Map<String, Object>> CONTEXT = new InheritableThreadLocal<>();

    public static void clear() {
        CONTEXT.remove();
    }

    private CommonRequestContext() {
    }

    public static void setLocalIp(String value){
        Map<String, Object> map = CONTEXT.get();
        if (CollUtil.isEmpty(map)) {
            map = new HashMap<>(INIT_SIZE);
            CONTEXT.set(map);
        }
        map.put("localIp", value);
    }

    public static Object getLocalIp(){
        Map<String, Object> map = CONTEXT.get();
        return (CollUtil.isNotEmpty(map)) ? map.get("localIp") : null;
    }
}

2. ### HttpInterceptor

http拦截器,负责从header中获取ip地址,存入ThreadLocal

package com.**.common.web.requestcontext;

import com.**.common.core.utils.StringUtils;
import com.**.common.satoken.requestcontext.CommonRequestContext;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 *  http拦截器,
 *  负责存储操作用户信息到RequestContext
 */
@Slf4j
public class HttpInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        String localIp = request.getHeader("localIp");
        if (StringUtils.isNotBlank(localIp)) {
            log.debug("preHandle,localIp:{}", localIp, localIp);
            CommonRequestContext.setLocalIp(localIp);
        }

        return true;
    }


    /**
* 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
*
* @param request  请求
* @param response 响应
* @param handler  处理器
* @param ex       异常
*/
@Override
    public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler, Exception ex) {
        CommonRequestContext.clear();
    }
}

3. ### ConsumerContextFilter

dubbo消费者拦截器,调用dubbo接口时将threadLocal中的ip传到RpcContext中,让生产者可以获取到ip信息。

package com.**.common.dubbo.filter;

import cn.hutool.core.util.ObjectUtil;
import com.**.common.core.utils.JsonUtils;
import com.**.common.satoken.core.model.LoginUser;
import com.**.common.satoken.requestcontext.CommonRequestContext;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;

@Activate(group = {CommonConstants.CONSUMER})
public class ConsumerContextFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        Object loginIpObj = CommonRequestContext.getLocalIp();
        if (ObjectUtil.isNotNull(loginIpObj)) {
            String loginIp = loginIpObj.toString();
            RpcContext.getServiceContext()
                    .setAttachment("loginIp", loginIp);
        }
        return invoker.invoke(invocation);
    }
}

4. ### ProviderContextFilter

dubbo生产者拦截器,从RpcContext中获取

package com.**.common.dubbo.filter;

import com.**.common.satoken.requestcontext.CommonRequestContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;

@Activate(group = {CommonConstants.PROVIDER})
public class ProviderContextFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String loginIp = RpcContext.getServiceContext().getAttachment("loginIp");
        if (StringUtils.isNotBlank(loginIp)) {
            CommonRequestContext.setLocalIp(loginIp);
        }

        try {
            return invoker.invoke(invocation);
        } finally {
            CommonRequestContext.clear();
        }
    }
}

5. ### LocalFirstLoadBalance

package com.**.common.dubbo.loadBalance;

import cn.hutool.core.util.RandomUtil;
import com.**.common.satoken.requestcontext.CommonRequestContext;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance;
import org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance;

import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Random;

/**
 *  <p>PURPOSE:
 *  <p>DESCRIPTION:
 *  <p>
 *
 *  </p>
 *  <p>CALLED  BY:
 *  <p>CREATE  DATE:  2024-09-10 18 :36
 *  <p>UPDATE  DATE:
 *  <p>UPDATE  USER:
 *  <p>HISTORY:
 *
 *  @author  chenzhaoheng
 *  @version  1.0
 *  @see
 *  @since  java  1.8
 */
public class LocalFirstLoadBalance extends AbstractLoadBalance {

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        // 从 RpcContext 中获取请求的 IP
        Object clientIpObj = CommonRequestContext.getLocalIp();

        if (null == clientIpObj){
           return invokers.get(RandomUtil.randomInt(invokers.size()));
        }
        String clientIp = clientIpObj.toString();
        // 优先选择本地 IP 匹配的服务实例
        Optional<Invoker<T>> localInvoker = invokers.stream()
                .filter(invoker -> invoker.getUrl().getHost().equals(clientIp))
                .findFirst();

        if (localInvoker.isPresent()) {
            return localInvoker.get();
        }

        // 如果找不到本地 IP 的实例,选择权重最高的实例
        Optional<Invoker<T>> highestWeightInvoker = invokers.stream()
                .max(Comparator.comparingInt(invoker -> this.getWeight(invoker, invocation)));

        // 如果找不到权重最高的实例,使用随机负载均衡
        // 使用随机负载均衡
        return highestWeightInvoker.orElseGet(() -> invokers.get(RandomUtil.randomInt(invokers.size())));

    }
}

6. ### com.alibaba.dubbo.rpc.cluster.LoadBalance

localFirst=com.**.common.dubbo.loadBalance.LocalFirstLoadBalance

4. # 使用方式

  1. 后端调试

请求dev环境网关地址,在header中增加localIp参数,值设置成本地的ip地址

  1. 前后端联调

前端所有请求统一加上localIp请求头。值设置成后端的ip地址。如上图所示。

  1. 注意事项

  1. dubbo负载均衡不生效

只有一个生产者时dubbo不会走负载均衡策略。直接用获取到的生产者调用。

场景:

dev环境挂了,只有本地一个服务注册到nacos。此时负载均衡不生效。

  1. 本地的application.yml文件不能往git上提交

后续在.gitignore文件中增加配置

src/resource/application.yml