Spring cloud中的服务自定义路由

很多情况下我们需要服务自定义路由,比如需要灰度发布时线上验证环境、生产环境的服务实例路由是需要区分的,还有在SAAS化应用中,经常会把租户分成一个个组,每组分配几个服务实例,就是说组内服务实例共享,组间是隔离的。

  本文在Spring Cloud的基础上,给出了一个服务分组和自定义路由的方案,并提供了范例代码,代码开源地址为:

https://github.com/tangaiyun/custom-routing-for-Spring-Cloud-service

 

方案的基本思路为:

  • 服务发布时注册到Eureka上,服务发布时必须指定appGroupName,常用的指定方法为:
    1. 在java程序启动参数中添加
    --eureka.instance.app-group-name=group_1
    2. docker 启动的话,在docker运行环境参数中添加:
    -e "eureka.instance.app-group-name=group_1"
    以上都是要指定服务实例的appGroupName为group_1。
  • 服务访问时必须通过ZUUL网关按服务名字访问
    比如http://localhost:8060/microservice-provider-user/1
    8086为ZUUL的端口,microservice-provider-user为服务的名字
  • 服务访问时必须在HTTP header 或cookie中提供一个路由码,本例中它的名字为“ROUTECODE”
  • 自定义ZUUL,实现一个父类为AbstractLoadBalancerRule的类,本案中名字为“MyCustomRule”
  • 重点定义MyCustomRule的 public Server choose(ILoadBalancer lb, Object key)方法
    1. 读取ZK中的配置,初始化对象,并监控ZK中配置的变化
    2. 获取request中cookie或header中名为"ROUTECODE"属性值
    3. 根据ROUTECODE值映射到一个服务分组或者映射失败则使用默认分组
    4. 获取所有服务实例,按按照appGroupName过滤并排序
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.List;
    import java.util.Map.Entry;
    import java.util.Properties;
    import java.util.Set;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.stream.Collectors;
     
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
     
    import org.apache.curator.framework.CuratorFramework;
    import org.apache.curator.framework.CuratorFrameworkFactory;
    import org.apache.curator.framework.recipes.cache.ChildData;
    import org.apache.curator.framework.recipes.cache.NodeCache;
    import org.apache.curator.framework.recipes.cache.NodeCacheListener;
    import org.apache.curator.retry.ExponentialBackoffRetry;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
     
    import com.netflix.client.config.IClientConfig;
    import com.netflix.loadbalancer.AbstractLoadBalancerRule;
    import com.netflix.loadbalancer.ILoadBalancer;
    import com.netflix.loadbalancer.Server;
    import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
    import com.netflix.zuul.context.RequestContext;
     
    public class MyCustomRule extends AbstractLoadBalancerRule {
     
    	private ConcurrentHashMap nextServerCyclicCounterMap;
     
    	private static Logger log = LoggerFactory.getLogger(MyCustomRule.class);
     
    	private static final String ROUTE_HEAD_COOKIE_NAME = "ROUTECODE";
    	private static final String ZK_CONN_STR = "192.168.0.106:2181,192.168.0.135:2181,192.168.0.143:2181";
    	private static final String ROUTE_CONFIG_ZK_PATH = "/config/zuul/route";
    	private static final String GROUP_PREFIX = "group_";
    	private static final String ALL_AS_ONE_GROUP = "onegroup";
    	private static final String DEFAULT_ROUTE_KEY = "default_route";
    	private static final String ENABLE_KEY = "enable";
    	private volatile boolean isAcmInited = false;
    	private Properties routeConfigProperties = new Properties();
    	private boolean isCustomRouteEnable = false;
    	private ConcurrentHashMap> groupMemberMap;
    	private String defaultRoute;
     
    	public MyCustomRule() {
    		nextServerCyclicCounterMap = new ConcurrentHashMap();
    		nextServerCyclicCounterMap.put(ALL_AS_ONE_GROUP, new AtomicInteger(0));
    		groupMemberMap = new ConcurrentHashMap>();
    	}
     
    	public MyCustomRule(ILoadBalancer lb) {
    		this();
    		setLoadBalancer(lb);
    	}
     
    	private synchronized void configZKInit() {
     
    		System.out.println(" loading route config from ZK*****************************************************");
    		CuratorFramework client = CuratorFrameworkFactory.newClient(ZK_CONN_STR, new ExponentialBackoffRetry(1000, 3));
    		client.start();
    		try {
    			final CountDownLatch cdl = new CountDownLatch(1);
    			@SuppressWarnings("resource")
    			final NodeCache cache = new NodeCache(client, ROUTE_CONFIG_ZK_PATH);
    			NodeCacheListener listener = () -> {
    				System.out.println("enter listener...");
    				ChildData data = cache.getCurrentData();
    				if (null != data) {
    					String strRouteConfig = new String(cache.getCurrentData().getData());
    					System.out.println("节点数据:" + strRouteConfig);
    					loadConfig(strRouteConfig);
     
    				} else {
    					System.out.println("节点被删除!");
    				}
    				cdl.countDown();
    				System.out.println("countdownlatch count!");
    			};
    			cache.getListenable().addListener(listener);
     
    			cache.start();
    			isAcmInited = true;
    			cdl.await();
    			System.out.println("countdownlatch await finish!");
    		} catch (Exception e) {
    			log.error("路由配置节点监控器启动错误", e);
    		}
    	}
     
    	private void loadConfig(String content) {
    		try {
    			routeConfigProperties.load(new ByteArrayInputStream(content.getBytes()));
    			isCustomRouteEnable = Boolean.parseBoolean(routeConfigProperties.getProperty(ENABLE_KEY).trim());
    			Set keySet = routeConfigProperties.keySet();
    			for(Object key : keySet) {
    				String strKey = (String)key;
    				if(strKey.startsWith(GROUP_PREFIX)) {
    					ArrayList groupMemberList = new ArrayList();
    					groupMemberList.addAll(Arrays.asList(routeConfigProperties.getProperty(strKey).split(",")));
    					groupMemberMap.put(strKey, groupMemberList);
    					nextServerCyclicCounterMap.put(strKey, new AtomicInteger(0));
    				}
    			}
    			defaultRoute = routeConfigProperties.getProperty(DEFAULT_ROUTE_KEY);
    		} catch (IOException e) {
    			log.error("配置读入到Properties对象失败", e);
    		}
    	}
     
    	public Server choose(ILoadBalancer lb, Object key) {
    		if (lb == null) {
    			log.warn("no load balancer");
    			return null;
    		}
     
    		if (!isAcmInited) {
    			configZKInit();
    		}
     
    		Server server = null;
    		if (!isCustomRouteEnable) {
    			int count = 0;
    			while (server == null && count++ < 10) {
    				List reachableServers = lb.getReachableServers();
    				List allServers = lb.getAllServers();
    				int upCount = reachableServers.size();
    				int serverCount = allServers.size();
     
    				if ((upCount == 0) || (serverCount == 0)) {
    					log.warn("No up servers available from load balancer: " + lb);
    					return null;
    				}
     
    				int nextServerIndex = incrementAndGetModulo(ALL_AS_ONE_GROUP, serverCount);
    				List sortedAllServers = allServers.stream().sorted((s1, s2) -> s1.getId().compareTo(s2.getId()))
    						.collect(Collectors.toList());
    				System.out.println("all servers info:");
    				printServersInfo(sortedAllServers);
    				server = sortedAllServers.get(nextServerIndex);
     
    				if (server == null) {
    					/* Transient. */
    					Thread.yield();
    					continue;
    				}
     
    				if (server.isAlive() && (server.isReadyToServe())) {
    					System.out.println("this request will be served by the following server:");
    					printServerInfo(server);
    					return (server);
    				}
    				// Next.
    				server = null;
    			}
     
    			if (count >= 10) {
    				log.warn("No available alive servers after 10 tries from load balancer: " + lb);
    			}
    		} else {
    			int count = 0;
    			while (server == null && count++ < 10) {
    				List reachableServers = lb.getReachableServers();
    				List allServers = lb.getAllServers();
    				int upCount = reachableServers.size();
    				int serverCount = allServers.size();
     
    				if ((upCount == 0) || (serverCount == 0)) {
    					log.warn("No up servers available from load balancer: " + lb);
    					return null;
    				}
    				String routeKey = getRouteKey();
    				String routeGroup = getGroupbyRouteKey(routeKey);
    				if(routeGroup == null) {
    					routeGroup = defaultRoute;
    				}
    				final String appFilter = routeGroup;
    				List serverCandidates = allServers.stream()
    						.filter(s -> appFilter
    								.equalsIgnoreCase(((DiscoveryEnabledServer) s).getInstanceInfo().getAppGroupName()))
    						.sorted((s1, s2) -> s1.getId().compareTo(s2.getId())).collect(Collectors.toList());
    				
    				int nextServerIndex = incrementAndGetModulo(routeGroup,serverCandidates.size());
    				server = serverCandidates.get(nextServerIndex);
    			
    				
    				if (server == null) {
    					/* Transient. */
    					Thread.yield();
    					continue;
    				}
    				if (server.isAlive() && (server.isReadyToServe())) {
    					System.out.println("this request will be served by the following server:");
    					printServerInfo(server);
    					return (server);
    				}
    				// Next.
    				server = null;
    			}
    			if (count >= 10) {
    				log.warn("No available alive servers after 10 tries from load balancer: " + lb);
    			}
    		}
    		return server;
    	}
     
    	private void printServersInfo(Collection servers) {
    		for (Server s : servers) {
    			printServerInfo(s);
    		}
    	}
     
    	private void printServerInfo(Server server) {
    		System.out.print("appName: " + ((DiscoveryEnabledServer) server).getInstanceInfo().getAppName() + " ");
    		System.out.print("appGroup: " + ((DiscoveryEnabledServer) server).getInstanceInfo().getAppGroupName() + " ");
    		System.out.print("id: " + server.getId() + " isAlive: " + server.isAlive() + " ");
    		System.out.print("id: " + server.getId() + " isReadyToServe: " + server.isReadyToServe() + " ");
    		System.out.println();
    	}
     
    	/**
    	 * first find
    	 * 
    	 * @return
    	 */
    	private String getRouteKey() {
    		RequestContext ctx = RequestContext.getCurrentContext();
    		HttpServletRequest request = ctx.getRequest();
    		String orgCode = "";
    		Cookie[] cookies = request.getCookies();
    		if (cookies != null) {
    			for (Cookie cookie : cookies) {
    				if (ROUTE_HEAD_COOKIE_NAME.equals(cookie.getName())) {
    					orgCode = cookie.getValue();
    					break;
    				}
    			}
    		}
    		if ("".equals(orgCode)) {
    			orgCode = request.getHeader(ROUTE_HEAD_COOKIE_NAME);
    		}
     
    		return orgCode;
    	}
    	
    	private String getGroupbyRouteKey(String routeKey) {
    		String group = null;
    		Set>> entrySet = groupMemberMap.entrySet();
    		for(Entry> entry : entrySet) {
    			List memberList = entry.getValue();
    			if(memberList.contains(routeKey)) {
    				group = entry.getKey();
    				break;
    			}
    		}
    		return group;
    	}
     
    	/**
    	 * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
    	 *
    	 * @param modulo
    	 *            The modulo to bound the value of the counter.
    	 * @return The next value.
    	 */
    	private int incrementAndGetModulo(String group, int modulo) {
    		AtomicInteger groupCurrent = nextServerCyclicCounterMap.get(group);
    		for (;;) {
    			int current = groupCurrent.get();
    			int next = (current + 1) % modulo;
    			if (groupCurrent.compareAndSet(current, next))
    				return next;
    		}
    	}
     
    	@Override
    	public Server choose(Object key) {
    		return choose(getLoadBalancer(), key);
    	}
     
    	@Override
    	public void initWithNiwsConfig(IClientConfig clientConfig) {
    	}
    } 

     

  • ZUUL中的配置

    server:
      port: 8060
    spring:
      application:
        name: microservice-gateway-zuul
    eureka:
      client:
        service-url:
          defaultZone: http://peer1:8761/eureka/,http://peer2:8762/eureka/
      instance:
        prefer-ip-address: true
                
    microservice-provider-user:
      ribbon:
        NFLoadBalancerRuleClassName: com.tay.customroute.MyCustomRule  
    microservice-provider-user2:
      ribbon:
        NFLoadBalancerRuleClassName: com.tay.customroute.MyCustomRule    

     

  • 特别注意下面的
        microservice-provider-user:
          ribbon:
            NFLoadBalancerRuleClassName: com.tay.customroute.MyCustomRule

     

  • 这个配置的意思就是名字为

    microservice-provider-user

    的服务将使用com.tay.customroute.MyCustomRule 作为负载均衡器的路由规则,即我们自义定的路由规则。

  • ZK中配置范例

        enable=true
        group_1=org1,org2,org3,org4,org5,org6,org7,org8,org9,org10
        group_2=org11,org12,org13,org15
        group_3=org20,org21,org22,org23
        default_route=group_1

    这个配置的含义为:
    自定义路由配置当前生效。
    当前有三个组group_1,group_2,group_3,每个组包含一组租户机构码(tenaut code),有多少个组完全由用户自己定义,但每个组组名必须以“group_”开头。
    如果一个tenaut code没有包含在以上3个组中,则默认它归属为group_1。

  • 组件关系图
    Spring cloud中的服务自定义路由_第1张图片

  • 转:https://blog.csdn.net/suncold/article/details/79388168

  • 你可能感兴趣的:(Spring,cloud)