透過自定義feignclient 的LoadBalancerFeignClient實現靈活的負載均衡策略

梦在旅途發表於2024-12-01

透過自定義feignclient 的LoadBalancerFeignClient 或IRule 能實現完全自定義的負載均衡策略,本文主要是透過實現自定義的LoadBalancerFeignClient而達到自定義的負載均衡策略

示例程式碼實現如下:

package cn.zuowenjun.demo;

import com.netflix.loadbalancer.Server;
import feign.Client;
import feign.Request;
import feign.Response;
import org.apache.commons.collections4.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.cloud.netflix.feign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.netflix.feign.ribbon.LoadBalancerFeignClient;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

/**
 * FeignClient 服務BEAN(含自定義負載均衡請求機制),注意這裡指定了configuration的型別
 */
@FeignClient(value ="zuowenjun-demo" , configuration = {DemoProviderDispatchService.Config.class})
public interface DemoProviderDispatchService {
    Logger LOGGER = LoggerFactory.getLogger(FileFmsDispatchService.class);

    //要RPC請求的介面,需要自定義負載均衡
    @RequestMapping(value = "/fileContent/compare", method = RequestMethod.POST)
    ResponseData<Integer> compare(@RequestBody FileBillCompareBO billCompareBO);

    /**
     * DemoProviderDispatchService 專用的配置類,在這個配置類裡面新增的BEAN均可替換全域性預設的BEAN
     * 注意:此處不能加上@Configuration,否則將變成全域性配置了
     */
    static class Config {
        /**
         * 為FileFmsDispatchClient 重新定義專用的Client,在裡面實現自定義的URL請求
         *
         * @param cachingFactory
         * @param clientFactory
         * @return
         */
        @Bean
        public Client requestClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) {

            //獲取當前節點的ID與埠
            String currentIpAndPort = CommonUtils.getCurrentIpAndPort();

            //構建返回一個完全自定義的LoadBalancerFeignClient
            return new LoadBalancerFeignClient(new Client.Default(null, null) {
                @Override
                public Response execute(Request request, Request.Options options) throws IOException {
                    //透過重新建立Request,將Request 中的url指向自定義負載均衡的選中的URL
                    Request newRequest = reCreateRequestForLoadBalance(request);
                    if (newRequest == null) {
                        return Response.builder().reason("伺服器繁忙,當前無可用服務節點").status(204).build();
                    }

                    return super.execute(newRequest, options);
                }

                private Request reCreateRequestForLoadBalance(Request request) throws IOException {
                    URL url = new URL(request.url());
                    //獲取可用的服務例項節點列表
                    List<Server> upServers = clientFactory.getLoadBalancer(“zuowenjun-demo”).getReachableServers();
                    Server bestServer = choose(url, upServers, url.getFile());
                    if (bestServer == null) {
                        //找不到最佳可用伺服器節點,說明所有服務節點都壓力山大
                        return null;
                    }

                    url = new URL(url.getProtocol(), bestServer.getHost(), bestServer.getPort(), url.getFile());
                    return Request.create(request.method(), url.toString(), request.headers(), request.body(), request.charset());
                }


                /**
                 * 選擇最優的服務節點
                 * @param url
                 * @param upServers
                 * @param apiPath
                 * @return
                 */
                private Server choose(URL url, List<Server> upServers, String apiPath) {

                    if (CollectionUtils.isEmpty(upServers)) {
                        throw new ApplicationException(500, "從註冊中心獲取不到可用的服務節點資訊");
                    }

                    apiPath=apiPath.startsWith("/")?apiPath.substring(1):apiPath;
                    String hashKey = Constants.LOADBALANCE_API_PREFIX + apiPath.replace("/", "_");
                    Boolean existRequest = RedisUtils.existHashKey(hashKey, String.format("%s:%s", url.getHost(), url.getPort()));
                    Server bestServer = null;
                    if (!Boolean.TRUE.equals(existRequest)) {
                        //如果當前即將請求的URL的節點之前沒有快取標記請求處理中時,則可直接複用返回
                        bestServer = upServers.stream().filter(s -> s.getHost().equals(url.getHost()) && s.getPort() == url.getPort()).findFirst().orElse(null);
                        if (bestServer != null) {
                            return bestServer;
                        }
                    }

                    //先從快取中找出當前API 的請求中的節點列表
                    Map<Object, Object> existRequestMap = RedisUtils.getHashEntries(hashKey);
                    Set<Object> existRequestIpAndPorts = new HashSet<>();
                    if (MapUtils.isNotEmpty(existRequestMap)) {
                        existRequestIpAndPorts.addAll(existRequestMap.keySet());
                    }

                    //排除API請求中的節點,保留空閒節點列表
                    upServers = upServers.stream().filter(s -> !existRequestIpAndPorts.contains(s.getHostPort()) && !s.getHostPort().equals(currentIpAndPort)).collect(Collectors.toList());
                    if (CollectionUtils.isEmpty(upServers)) {
                        //說明當前所有節點全部都有處理請求中,無空閒節點
                        return null;
                    }

                    //從空閒節點列表中隨機返回一個節點
                    int rndNo = new Random().nextInt(upServers.size());
                    bestServer = upServers.get(rndNo);
                    LOGGER.debug("DemoProviderDispatchService.Config.requestClient.Client#choose {}", bestServer.getHostPort());
                    return bestServer;
                }

            }, cachingFactory, clientFactory);
        }


    }

}

呼叫時就正常注入DemoProviderDispatchService BEAN,並使用:demoProviderDispatchService.compare(...) 即可實現在自定義負載均衡的策略下請求遠端服務API,這種自定義的負載均衡策略可以滿足特定的效能要求

相關文章