Nacos集群(二)阿里自研弱一致性Distro协议核心实现

Nacos中有CP和AP两种模式,而这两种模式在实现数据一致性方案上面是完全不一样的,对于CP模式而言,使用的是raft这种强一致性协议,对于AP模式而言,则是使用阿里自创的Distro协议,那么这里我们就来看看这个Distro协议在Nacos中是如何实现的

一.@CanDistro注解

在Nacos服务中会发现有很多接口上面加了@CanDistro这个注解,例如实例注册接口:

 而@CanDistro这个注解有什么用呢?通过名字大概知道它应该是跟Distro协议有关的,所以我们通过idea去看一下这个注解哪里有被引用到,经过一番寻找之后,发现了一个很重要的核心类DistroFilter

二.DistroFilter

com.alibaba.nacos.naming.web.DistroFilter#doFilter

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
    ReuseHttpRequest req = new ReuseHttpServletRequest((HttpServletRequest) servletRequest);
    HttpServletResponse resp = (HttpServletResponse) servletResponse;

    String urlString = req.getRequestURI();

    if (StringUtils.isNotBlank(req.getQueryString())) {
        urlString += "?" + req.getQueryString();
    }

    try {
        String path = new URI(req.getRequestURI()).getPath();
        String serviceName = req.getParameter(CommonParams.SERVICE_NAME);
        // For client under 0.8.0:
        if (StringUtils.isBlank(serviceName)) {
            serviceName = req.getParameter("dom");
        }

        if (StringUtils.isNotBlank(serviceName)) {
            serviceName = serviceName.trim();
        }

        // 获取到要请求的接口方法
        Method method = controllerMethodsCache.getMethod(req);

        // 请求方法为空,抛出异常
        if (method == null) {
            throw new NoSuchMethodException(req.getMethod() + " " + path);
        }

        String groupName = req.getParameter(CommonParams.GROUP_NAME);
        if (StringUtils.isBlank(groupName)) {
            groupName = Constants.DEFAULT_GROUP;
        }

        // use groupName@@serviceName as new service name.
        // in naming controller, will use com.alibaba.nacos.api.naming.utils.NamingUtils.checkServiceNameFormat to check it's format.
        String groupedServiceName = serviceName;
        if (StringUtils.isNotBlank(serviceName) && !serviceName.contains(Constants.SERVICE_INFO_SPLITER)) {
            groupedServiceName = groupName + Constants.SERVICE_INFO_SPLITER + serviceName;
        }

        // 条件成立:该接口方法上有@CanDistro注解,并且当前节点不负责处理该服务
        // 说明当前节点不处理该请求,需要向其他服务器发出代理请求
        if (method.isAnnotationPresent(CanDistro.class) && !distroMapper.responsible(groupedServiceName)) {

            String userAgent = req.getHeader(HttpHeaderConsts.USER_AGENT_HEADER);

            if (StringUtils.isNotBlank(userAgent) && userAgent.contains(UtilsAndCommons.NACOS_SERVER_HEADER)) {
                // This request is sent from peer server, should not be redirected again:
                Loggers.SRV_LOG.error("receive invalid redirect request from peer {}", req.getRemoteAddr());
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
                        "receive invalid redirect request from peer " + req.getRemoteAddr());
                return;
            }

            // 获取负责处理该服务的节点地址
            final String targetServer = distroMapper.mapSrv(groupedServiceName);

            // 封装请求参数
            List<String> headerList = new ArrayList<>(16);
            Enumeration<String> headers = req.getHeaderNames();
            while (headers.hasMoreElements()) {
                String headerName = headers.nextElement();
                headerList.add(headerName);
                headerList.add(req.getHeader(headerName));
            }
            final String body = IoUtils.toString(req.getInputStream(), Charsets.UTF_8.name());
            final Map<String, String> paramsValue = HttpClient.translateParameterMap(req.getParameterMap());

            // 转发请求到其他节点
            RestResult<String> result = HttpClient
                    .request("http://" + targetServer + req.getRequestURI(), headerList, paramsValue, body,
                            PROXY_CONNECT_TIMEOUT, PROXY_READ_TIMEOUT, Charsets.UTF_8.name(), req.getMethod());
            String data = result.ok() ? result.getData() : result.getMessage();
            try {
                WebUtils.response(resp, data, result.getCode());
            } catch (Exception ignore) {
                Loggers.SRV_LOG.warn("[DISTRO-FILTER] request failed: " + distroMapper.mapSrv(groupedServiceName)
                        + urlString);
            }
        }
        // 条件成立:当前节点需要处理这个请求
        else {
            OverrideParameterRequestWrapper requestWrapper = OverrideParameterRequestWrapper.buildRequest(req);
            requestWrapper.addParameter(CommonParams.SERVICE_NAME, groupedServiceName);
            // 放行该请求,后面就会执行具体的接口
            filterChain.doFilter(requestWrapper, resp);
        }
    } catch (AccessControlException e) {
        resp.sendError(HttpServletResponse.SC_FORBIDDEN, "access denied: " + ExceptionUtil.getAllExceptionMsg(e));
    } catch (NoSuchMethodException e) {
        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED,
                "no such api:" + req.getMethod() + ":" + req.getRequestURI());
    } catch (Exception e) {
        resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                "Server failed," + ExceptionUtil.getAllExceptionMsg(e));
    }

}

 DistroFilter这个类是实现了Filter接口,所以表明了它是一个过滤器,在请求来的时候,会经过doFilter方法,在doFilter方法中大概有下面4个过程:

1.根据请求路径从controllerMethodsCache中获取到对应的controller方法

2.判断这个controller方法是否有@CanDistro注解,如果有的话再调用distroMapper.responsible()方法去判断当前nacos节点是否需要处理这个请求

3.如果controller方法没有@CanDistro注解,或者有@CanDistro注解并且当前nacos节点需要处理这个请求,那么就直接放行这个请求到controller端

4.反之如果controller方法有@CanDistro注解并且当前nacos节点不需要处理这个请求,那么就会把这个请求转发到对应的其他节点去处理


其中第一点中从controllerMethodsCache中获取对应的controller方法,那么是怎么获取的呢?所以我们要看下ControllerMethodsCache中的getMethod方法

public Method getMethod(HttpServletRequest request) {
    // 获取到请求路径
    String path = getPath(request);
    // 获取到请求方法类型
    String httpMethod = request.getMethod();
    // 构造一个urlKey
    String urlKey = httpMethod + REQUEST_PATH_SEPARATOR + path.replaceFirst(EnvUtil.getContextPath(), "");
    // 根据这个urlKey找到对应的RequestMappingInfo
    List<RequestMappingInfo> requestMappingInfos = urlLookup.get(urlKey);
    // 条件成立:说明并没有这个请求路径对应的controller方法,直接返回null
    if (CollectionUtils.isEmpty(requestMappingInfos)) {
        return null;
    }
    // 根据@RequestMapping注解中指定的params参数校验这个请求的参数是否合理
    List<RequestMappingInfo> matchedInfo = findMatchedInfo(requestMappingInfos, request);
    // 条件成立:说明这个请求的参数不合理,直接返回null
    if (CollectionUtils.isEmpty(matchedInfo)) {
        return null;
    }
    RequestMappingInfo bestMatch = matchedInfo.get(0);
    if (matchedInfo.size() > 1) {
        RequestMappingInfoComparator comparator = new RequestMappingInfoComparator();
        matchedInfo.sort(comparator);
        bestMatch = matchedInfo.get(0);
        RequestMappingInfo secondBestMatch = matchedInfo.get(1);
        if (comparator.compare(bestMatch, secondBestMatch) == 0) {
            throw new IllegalStateException(
                    "Ambiguous methods mapped for '" + request.getRequestURI() + "': {" + bestMatch + ", "
                            + secondBestMatch + "}");
        }
    }
    // 最终返回对应的controller方法
    return methods.get(bestMatch);
}

 从getMethod方法中可以看到这个方法主要是根据构造出来的一个urlKey(请求方式 + “-->” + 类上的请求路径 + 方法上的请求路径)去找到对应的RequestMappingInfo,RequestMappingInfo这个对象就封装了@RequestMapping注解的一些匹配信息,然后通过这个RequestMappingInfo对象去校验请求的参数是否被@RequestMapping注解中指定的params参数值所匹配,如果匹配的话就再根据这个RequestMappingInfo对象去找到对应的controller方法。但是获取controller方法是从methods这个map中获取到的,那么这个map中的数据又是从哪里来的呢?


我们通过idea一步步地去找看是在什么时候给上面的methods这个map填充数据的 

 

 

可以看到源头是在一个叫ConsoleConfig的类的init方法开始的,并且这个init方法加了@PostConstruct注解,表示在spring容器启动的时候就能够被调用该方法,在init方法中,调用了4次ControllerMethodsCache的initClassMethod方法,分别传了不同的包名,在initClassMethod方法中会根据传入的包名然后找到加了@RequestMapping注解的类,然后寻找每一个类中加了@RequestMapping注解的方法,然后构造出一个RequestMappingInfo对象,其中给这个RequestMappingInfo对象设置两个校验,一个是请求路径的校验,一个是请求参数的校验,然后把urlKey和RequestMappingInfo对象放到urlLookup这个map中,再把RequestMappingInfo对象和controller方法放到methods这个map中。所以经过上面的分析,我们可以做一个小总结,在spring容器启动的时候,nacos就会在指定的几个包名下找到所有加了@RequestMapping注解的controller类,然后再找到这些类下面加了@RequestMapping注解的方法,再构造出一个RequestMappingInfo校验对象用来对请求路径和请求参数进行校验匹配,而请求路径的检验是根据@RequestMapping注解指定的请求方式以及请求路径去构造出一个urlKey作为校验匹配的条件,请求参数校验则是根据@RequestMapping注解中的params属性作为检验匹配的条件,最终就会把这个RequestMappingInfo校验对象和对应的controller方法放到methods这个map中了。所以当有请求过来的时候,DistroFilter会进行拦截,首先会根据请求路径构造出urlKey,再根据urlKey找到对应的RequestMappingInfo检验对象,然后使用这个RequestMappingInfo校验对象对这个请求参数进行校验,如果校验不通过则返回null,校验通过则再根据这个RequestMappingInfo对象找到对应的controller方法

三.Distro弱一致性协议实现原理

 通过上面我们知道在DistroFilter中会根据请求找到对应的controller方法,然后会去判断这个controller方法上是否有@CanDistro注解,如果有的话会再判断当前的nacos节点是否需要对这个请求进行处理,而这个判断就是通过distroMapper.responsible()这个方法去判断的,那么这个方法具体是干什么的呢?其实这个方法就是实现distro弱一致性协议的核心,我们看下这个方法

/**
 * 判断当前nacos服务是否需要负责响应指定的service(比如是否需要心跳检查)
 *
 * @param serviceName 实例服务名称
 * @return true表示当前nacos服务需要响应指定的service,反之不需要响应
 */
public boolean responsible(String serviceName) {
    final List<String> servers = healthyList;

    // 条件成立:没有开启distro协议,或者是nacos服务是单机模式
    if (!switchDomain.isDistroEnabled() || EnvUtil.getStandaloneMode()) {
        // 返回true表示需要响应处理这个service
        return true;
    }

    if (CollectionUtils.isEmpty(servers)) {
        // means distro config is not ready yet
        return false;
    }

    // 获取到当前nacos服务在集群中的位置索引
    // index和lastIndex通常都会相等
    int index = servers.indexOf(EnvUtil.getLocalAddress());
    int lastIndex = servers.lastIndexOf(EnvUtil.getLocalAddress());
    if (lastIndex < 0 || index < 0) {
        return true;
    }

    // target变量的范围:0 <= target <= servers.size() -1
    // 对于同一个service来说,distroHash(serviceName)得到的结果都是相同的
    int target = distroHash(serviceName) % servers.size();
    // 所以在nacos集群中,只会有一个节点这里会返回true
    return target >= index && target <= lastIndex;
}
private int distroHash(String serviceName) {
    return Math.abs(serviceName.hashCode() % Integer.MAX_VALUE);
}

首先这个方法的作用是判断当前nacos节点是否需要负责处理指定的服务,如果不负责处理就返回true,反之就返回false。在开始的时候会去判断当前是否开启了distro协议,如果没有开启就返回true,以及会去判断这个nacos节点是否是单机模式,如果是单机模式就返回true,也就是说在单机模式下,distro协议是不起作用的,很好理解,因为distro协议就是解决了集群之间数据同步一致性的一种方案,而单机模式也没有所谓的数据同步,自然distro协议是不需要的。然后就是会去获取到当前nacos节点在整个nacos集群中的索引位置,并且对指定的服务名通过distroHash方法获取到一个值,把这个值与整个nacos集群节点数进行取模得到一个target值,如果这个target值是等于当前nacos节点所在集群的索引位置值,那么就返回true,反之就返回false。所以对于每一个服务,它都会通过上面这种方式分配到具体的nacos节点,也就是说每一个nacos节点都会负责一部分的服务,那么这这难道nacos集群是分布式集群吗 ?很显然不是的,虽然说每一个nacos节点只会负责一部分的服务请求,但是nacos之间会进行数据的同步,也就是nacos集群的每一个节点数据是最终一致性的,所以这也就是什么说distro协议是一个弱一致性的协议了。而如果这个服务请求根据distro协议的规则判断之后发现不归当前这个nacos节点负责处理怎么办呢?这时候就需要对这个服务请求进行转发了,此时会通过distro协议的规则重新计算找出负责处理这个服务请求的nacos节点,然后当前nacos节点就把这个请求重转发到指定的nacos节点,这样整个distro协议的实现流程就完成了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值