-
前端支持
前后端联调需要前端在发送http请求时在header增加localIp参数,值为后端的ip地址。
-
后台配置修改
-
ci脚本调整
改此配置目的是将k8s中的docket容器提供的dubbo接口通过nodeport映射成外部能访问的端口。并将nodeport端口注册到nacos上。
-
dubbo配置调整
dubbo: protocol: port: 20880 # 此端口固定。需要用noteport映射, 不固定应该也是20880 consumer: loadbalance: localFirst # 自定义负载均衡策略,此策略会优先获取localIp的服务。如果找不到获取权重最高的。 provider: weight: 1 # 自己的配置文件中,一定不能把权重配置太高。 dev环境权重配置100。本地的配置不能超过100。否则dev环境调用的时候即使没有传localIp也会调用到你本地。
-
网关配置调整
- 本地注册到dev的网关上。
- 修改divide插件选择器的配置。weight配置成1。保证本地的权重比dev环境的低即可。
- 修改规则中负载均衡策略,设置成localFirst。网关的默认值已经调整,dev环境默认值已经调整成localFirst
-
启动项目
- Nacos dev命名空间增加配置文件 application-common-**.yml
- 修改2.2dubbo配置调整 章节提到的配置文件
- 本地application.yml中调整引用的配置文件
config:
import:
- optional:nacos:application-common.yml
修改为
config:
import:
- optional:nacos:application-common-**.yml
-
启动项目
-
代码
-
网关负载均衡策略
-
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. ### 网关配置页面增加负载均衡字典值
-
dubbo负载均衡
-
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. # 使用方式
-
后端调试
请求dev环境网关地址,在header中增加localIp参数,值设置成本地的ip地址
-
前后端联调
前端所有请求统一加上localIp请求头。值设置成后端的ip地址。如上图所示。
-
注意事项
-
dubbo负载均衡不生效
只有一个生产者时dubbo不会走负载均衡策略。直接用获取到的生产者调用。
场景:
dev环境挂了,只有本地一个服务注册到nacos。此时负载均衡不生效。
-
本地的application.yml文件不能往git上提交
后续在.gitignore文件中增加配置
src/resource/application.yml