SpringCloudAlibaba源码(Nacos、Sentinel):nacos-discovery,Sentinel底层LongAdder的计数实现、时间窗口的实现,限流算法的实现

11 篇文章 2 订阅
2 篇文章 0 订阅

Nacos 服务注册

  • nacos-spring-boot-project 中有关服务注册的几个项目
    • nacos-discovery-spring-boot-actuator
      nacos-discovery-spring-boot-autoconfigure
      nacos-discovery-spring-boot-starter
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.alibaba.boot.nacos.discovery.autoconfigure.NacosDiscoveryAutoConfiguration

找到注解NacosDiscoveryAutoConfiguration

@ConditionalOnProperty(name = NacosDiscoveryConstants.ENABLED, matchIfMissing = true)
@ConditionalOnMissingBean(name = DISCOVERY_GLOBAL_NACOS_PROPERTIES_BEAN_NAME)
@EnableNacosDiscovery
@EnableConfigurationProperties(value = NacosDiscoveryProperties.class)
@ConditionalOnClass(name = "org.springframework.boot.context.properties.bind.Binder")
    public class NacosDiscoveryAutoConfiguration {

 @Bean
 public NacosDiscoveryAutoRegister discoveryAutoRegister() {
  return new NacosDiscoveryAutoRegister();
 }

}
  • 注解:EnableNacosDiscovery
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(NacosDiscoveryBeanDefinitionRegistrar.class)
public @interface EnableNacosDiscovery {}
  • import 类 :NacosDiscoveryBeanDefinitionRegistrar
public class NacosDiscoveryBeanDefinitionRegistrar
      implements ImportBeanDefinitionRegistrar, EnvironmentAware {

   private Environment environment;

   @Override
   public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
         BeanDefinitionRegistry registry) {
      AnnotationAttributes attributes = AnnotationAttributes
            .fromMap(importingClassMetadata
                  .getAnnotationAttributes(EnableNacosDiscovery.class.getName()));
      // Register Global Nacos Properties Bean
      registerGlobalNacosProperties(attributes, registry, environment,
            DISCOVERY_GLOBAL_NACOS_PROPERTIES_BEAN_NAME);
      registerGlobalNacosProperties(attributes, registry, environment,
            MAINTAIN_GLOBAL_NACOS_PROPERTIES_BEAN_NAME);
      // Register Nacos Common Beans
      registerNacosCommonBeans(registry);
      // Register Nacos Discovery Beans
      registerNacosDiscoveryBeans(registry);
   }

   @Override
   public void setEnvironment(Environment environment) {
      this.environment = environment;
   }
}
  • 两个流程

    1. 将注解EnableNacosDiscovery的属性读取,放入到 nacos 的全局属性配置中

    2. bean 注入

nacos 全局配置属性

  • com.alibaba.nacos.spring.context.annotation.discovery.NacosDiscoveryBeanDefinitionRegistrar#registerBeanDefinitions
    • com.alibaba.nacos.spring.util.NacosBeanUtils#registerGlobalNacosProperties(org.springframework.core.annotation.AnnotationAttributes, org.springframework.beans.factory.support.BeanDefinitionRegistry, org.springframework.core.env.PropertyResolver, java.lang.String)
public static void registerGlobalNacosProperties(AnnotationAttributes attributes,
      BeanDefinitionRegistry registry, PropertyResolver propertyResolver,
      String beanName) {
   if (attributes == null) {
      return; // Compatible with null
   }
   AnnotationAttributes globalPropertiesAttributes = attributes
         .getAnnotation("globalProperties");
   registerGlobalNacosProperties((Map<?, ?>) globalPropertiesAttributes, registry,
         propertyResolver, beanName);
}
  • 贴出注解上的信息
	NacosProperties globalProperties() default @NacosProperties(username = USERNAME_PLACEHOLDER, password = PASSWORD_PLACEHOLDER, endpoint = ENDPOINT_PLACEHOLDER, namespace = NAMESPACE_PLACEHOLDER, accessKey = ACCESS_KEY_PLACEHOLDER, secretKey = SECRET_KEY_PLACEHOLDER, serverAddr = SERVER_ADDR_PLACEHOLDER, contextPath = CONTEXT_PATH_PLACEHOLDER, clusterName = CLUSTER_NAME_PLACEHOLDER, encode = ENCODE_PLACEHOLDER);

  • 通过下面这段代码会将注解信息获取到对象AnnotationAttributes globalPropertiesAttributes

    AnnotationAttributes globalPropertiesAttributes = attributes
          .getAnnotation("globalProperties");
    
  • 下一段代码是将属性换算出来

    registerGlobalNacosProperties((Map<?, ?>) globalPropertiesAttributes, registry,
          propertyResolver, beanName)
    
    
        	public static void registerGlobalNacosProperties(Map<?, ?> globalPropertiesAttributes,
    			BeanDefinitionRegistry registry, PropertyResolver propertyResolver,
    			String beanName) {
    		// 占位符解析成具体的配置信息
    		Properties globalProperties = resolveProperties(globalPropertiesAttributes,
    				propertyResolver);
    		// 单例注册
    		registerSingleton(registry, beanName, globalProperties);
    	}
    
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HNr3tKMg-1613906128928)(/images/nacos/image-20200821111938485.png)]

registerNacosCommonBeans

public static void registerNacosCommonBeans(BeanDefinitionRegistry registry) {
   // Register NacosApplicationContextHolder Bean
   registerNacosApplicationContextHolder(registry);
   // Register AnnotationNacosInjectedBeanPostProcessor Bean
   registerAnnotationNacosInjectedBeanPostProcessor(registry);
}
  • 主要方法: registerInfrastructureBean
    1. 定义出 bean
    2. 设置构造参数
    3. 注册对象
public static void registerInfrastructureBean(BeanDefinitionRegistry registry,
      String beanName, Class<?> beanClass, Object... constructorArgs) {
   // Build a BeanDefinition for NacosServiceFactory class
   // 定义出 bean 根据类型
   BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
         .rootBeanDefinition(beanClass);
   for (Object constructorArg : constructorArgs) {
      beanDefinitionBuilder.addConstructorArgValue(constructorArg);
   }
   // ROLE_INFRASTRUCTURE
   beanDefinitionBuilder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
   // Register
   registry.registerBeanDefinition(beanName,
         beanDefinitionBuilder.getBeanDefinition());
}

@EnableConfigurationProperties(value = NacosDiscoveryProperties.class)

属性读取,从 application 配置文件中读取数据转换成 java 对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZFL01L6B-1613906128932)(/images/nacos/image-20200821132413628.png)]

NacosDiscoveryAutoRegister

public class NacosDiscoveryAutoRegister
        implements ApplicationListener<WebServerInitializedEvent> {}
  • 处理一个WebServerInitializedEvent 事件的方法

  • 重写方法如下,主要工作内容

    1. 把服务发现配置读取出来
    2. 设置一些数据值
    3. 调用服务注册接口
@Override
public void onApplicationEvent(WebServerInitializedEvent event) {

    if (!discoveryProperties.isAutoRegister()) {
        return;
    }

    Register register = discoveryProperties.getRegister();

    if (StringUtils.isEmpty(register.getIp())) {
        register.setIp(NetUtils.localIP());
    }

    if (register.getPort() == 0) {
        register.setPort(event.getWebServer().getPort());
    }

    register.getMetadata().put("preserved.register.source", "SPRING_BOOT");

    register.setInstanceId("");

    String serviceName = register.getServiceName();

    if (StringUtils.isEmpty(serviceName)){
        if (StringUtils.isEmpty(applicationName)){
            throw new AutoRegisterException("serviceName notNull");
        }
        serviceName = applicationName;
    }

    try {
        namingService.registerInstance(serviceName, register.getGroupName(),
                register);
        logger.info("Finished auto register service : {}, ip : {}, port : {}",
                serviceName, register.getIp(), register.getPort());
    } catch (NacosException e) {
        throw new AutoRegisterException(e);
    }
}
  • 注册的参数

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUI5S4eA-1613906128933)(/images/nacos/image-20200821133350982.png)]

服务注册

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1bV1ZpaB-1613906128933)(/images/nacos/image-20200821133445090.png)]

  • 注册一个实例
    1. 将 instance 对象转换成 BeatInfo 对象
    2. 注册实例
    @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {

        if (instance.isEphemeral()) {
            // 实例信息转换
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setCluster(instance.getClusterName());
            beatInfo.setWeight(instance.getWeight());
            beatInfo.setMetadata(instance.getMetadata());
            beatInfo.setScheduled(false);
            beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
            // 插入这条实例的信息
            beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName),
                    beatInfo);
        }

        serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }

  • addBeatInfo
    • 创建了一个定时任务 BeatTask
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
    NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
    String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
    BeatInfo existBeat = null;
    //fix #1733
    if ((existBeat = dom2Beat.remove(key)) != null) {
        existBeat.setStopped(true);
    }
    dom2Beat.put(key, beatInfo);
    executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
    MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
}

BeatTask

class BeatTask implements Runnable {

    BeatInfo beatInfo;

    public BeatTask(BeatInfo beatInfo) {
        this.beatInfo = beatInfo;
    }

    @Override
    public void run() {
        if (beatInfo.isStopped()) {
            return;
        }
        long nextTime = beatInfo.getPeriod();
        try {
        	// 与nacos进行一次rest请求交互
            JSONObject result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled);
            long interval = result.getIntValue("clientBeatInterval");
            boolean lightBeatEnabled = false;
            if (result.containsKey(CommonParams.LIGHT_BEAT_ENABLED)) {
                lightBeatEnabled = result.getBooleanValue(CommonParams.LIGHT_BEAT_ENABLED);
            }
            BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
            if (interval > 0) {
                nextTime = interval;
            }
            int code = NamingResponseCode.OK;
            if (result.containsKey(CommonParams.CODE)) {
                code = result.getIntValue(CommonParams.CODE);
            }
            // 如果nacos找不到当前实例,
            if (code == NamingResponseCode.RESOURCE_NOT_FOUND) {
                Instance instance = new Instance();
                instance.setPort(beatInfo.getPort());
                instance.setIp(beatInfo.getIp());
                instance.setWeight(beatInfo.getWeight());
                instance.setMetadata(beatInfo.getMetadata());
                instance.setClusterName(beatInfo.getCluster());
                instance.setServiceName(beatInfo.getServiceName());
                instance.setInstanceId(instance.getInstanceId());
                instance.setEphemeral(true);
                try {
                	// 执行注册服务
                    serverProxy.registerService(beatInfo.getServiceName(),
                        NamingUtils.getGroupName(beatInfo.getServiceName()), instance);
                } catch (Exception ignore) {
                }
            }
        } catch (NacosException ne) {
            NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}",
                JSON.toJSONString(beatInfo), ne.getErrCode(), ne.getErrMsg());

        }
        executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS);
    }
}
  • 定时任务说明

    1. 和 nacos 进行一次交互,根据交互结果的 code 判断,如果不在 nacos 会执行注册.
  • 发送请求的方法

    public String reqAPI(String api, Map<String, String> params, String body, List<String> servers, String method) throws NacosException {
    
        params.put(CommonParams.NAMESPACE_ID, getNamespaceId());
    
        if (CollectionUtils.isEmpty(servers) && StringUtils.isEmpty(nacosDomain)) {
            throw new NacosException(NacosException.INVALID_PARAM, "no server available");
        }
    
        NacosException exception = new NacosException();
    
        if (servers != null && !servers.isEmpty()) {
    
            Random random = new Random(System.currentTimeMillis());
            int index = random.nextInt(servers.size());
    
            for (int i = 0; i < servers.size(); i++) {
                // 获取nacos所在的ip+port地址
                String server = servers.get(index);
                try {
                    // 进行请求
                    return callServer(api, params, body, server, method);
                } catch (NacosException e) {
                    exception = e;
                    if (NAMING_LOGGER.isDebugEnabled()) {
                        NAMING_LOGGER.debug("request {} failed.", server, e);
                    }
                }
                index = (index + 1) % servers.size();
            }
        }
    
        if (StringUtils.isNotBlank(nacosDomain)) {
            for (int i = 0; i < UtilAndComs.REQUEST_DOMAIN_RETRY_COUNT; i++) {
                try {
                    return callServer(api, params, body, nacosDomain, method);
                } catch (NacosException e) {
                    exception = e;
                    if (NAMING_LOGGER.isDebugEnabled()) {
                        NAMING_LOGGER.debug("request {} failed.", nacosDomain, e);
                    }
                }
            }
        }
    
        NAMING_LOGGER.error("request: {} failed, servers: {}, code: {}, msg: {}",
            api, servers, exception.getErrCode(), exception.getErrMsg());
    
        throw new NacosException(exception.getErrCode(), "failed to req API:/api/" + api + " after all servers(" + servers + ") tried: "
            + exception.getMessage());
    
    }
    

学习点

  • 这里采用随机值作为第一个 server 的获取,主要目的是为了将请求随机分配给不同的 nacos 服务

    如果直接使用 for 循环的索引那第一台 nacos 服务会收到所有的请求,直到这台服务坏了才会请求第二台

      Random random = new Random(System.currentTimeMillis());
      int index = random.nextInt(servers.size());
    
      for (int i = 0; i < servers.size(); i++) {
          // 获取nacos所在的ip+port地址
          String server = servers.get(index);
          try {
              // 进行请求
              return callServer(api, params, body, server, method);
          } catch (NacosException e) {
              exception = e;
              if (NAMING_LOGGER.isDebugEnabled()) {
                  NAMING_LOGGER.debug("request {} failed.", server, e);
              }
          }
          index = (index + 1) % servers.size();
      }
    

    }

registerService

  • 注册方法就是请求一次接口,将数据发送给 nacos 就完成了
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {

    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}",
        namespaceId, serviceName, instance);

    final Map<String, String> params = new HashMap<String, String>(9);
    params.put(CommonParams.NAMESPACE_ID, namespaceId);
    params.put(CommonParams.SERVICE_NAME, serviceName);
    params.put(CommonParams.GROUP_NAME, groupName);
    params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
    params.put("ip", instance.getIp());
    params.put("port", String.valueOf(instance.getPort()));
    params.put("weight", String.valueOf(instance.getWeight()));
    params.put("enable", String.valueOf(instance.isEnabled()));
    params.put("healthy", String.valueOf(instance.isHealthy()));
    params.put("ephemeral", String.valueOf(instance.isEphemeral()));
    params.put("metadata", JSON.toJSONString(instance.getMetadata()));

    reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, HttpMethod.POST);

}
  • 服务注册的接口

    • /nacos/v1/ns/instance
    • /nacos/v1/ns/instance/beat
  • 接下来去寻找这两个接口的实现

    com.alibaba.nacos.naming.controllers.InstanceController

nacos 服务端

实例注册

public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
	// 创建空服务
    createEmptyService(namespaceId, serviceName, instance.isEphemeral());

    Service service = getService(namespaceId, serviceName);

    if (service == null) {
        throw new NacosException(NacosException.INVALID_PARAM,
            "service not found, namespace: " + namespaceId + ", service: " + serviceName);
    }

    addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
}
  • 创建空服务的流程

    • 获取服务对象

      nacos 的服务信息存储在

      com.alibaba.nacos.naming.core.ServiceManager#serviceMap

      private Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();
      
public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster) throws NacosException {
    // 获取服务信息
    Service service = getService(namespaceId, serviceName);
    if (service == null) {

        Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
        service = new Service();
        service.setName(serviceName);
        service.setNamespaceId(namespaceId);
        service.setGroupName(NamingUtils.getGroupName(serviceName));
        // now validate the service. if failed, exception will be thrown
        service.setLastModifiedMillis(System.currentTimeMillis());
        service.recalculateChecksum();
        if (cluster != null) {
            cluster.setService(service);
            service.getClusterMap().put(cluster.getName(), cluster);
        }
        service.validate();

        putServiceAndInit(service);
        if (!local) {
            addOrReplaceService(service);
        }
    }
}
  • 在了解 map 结构后不难理解下面这个获取 Service 的方法了
public Service getService(String namespaceId, String serviceName) {
    if (serviceMap.get(namespaceId) == null) {
        return null;
    }
    return chooseServiceMap(namespaceId).get(serviceName);
}
private void putServiceAndInit(Service service) throws NacosException {
    putService(service);
    service.init();
    consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
    consistencyService.listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
    Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJSON());
}
  • 把服务加入 map 对象
public void putService(Service service) {
    if (!serviceMap.containsKey(service.getNamespaceId())) {
        synchronized (putServiceLock) {
            if (!serviceMap.containsKey(service.getNamespaceId())) {
                serviceMap.put(service.getNamespaceId(), new ConcurrentHashMap<>(16));
            }
        }
    }
    serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
}
  • init 方法设置了一个数据验证的任务 , 并且在集群中设置 service 信息
public void init() {

    HealthCheckReactor.scheduleCheck(clientBeatCheckTask);

    for (Map.Entry<String, Cluster> entry : clusterMap.entrySet()) {
        entry.getValue().setService(this);
        entry.getValue().init();
    }
}
  • 再往后添加两个 key 的监听

  • addInstance 方法

public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips) throws NacosException {

    String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);

    Service service = getService(namespaceId, serviceName);

    synchronized (service) {
        List<Instance> instanceList = addIpAddresses(service, ephemeral, ips);

        Instances instances = new Instances();
        instances.setInstanceList(instanceList);

        consistencyService.put(key, instances);
    }
}
  • 简单理解 consistencyService 结构信息
    • key: 定义的一个名字
    • value : 实例的列表

实例健康检查

  • 获取实例独享, 从 service 中根据集群名称获取实例列表 , 再根据 ip + 端口 返回实例对象
Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port);
public Instance getInstance(String namespaceId, String serviceName, String cluster, String ip, int port) {
    Service service = getService(namespaceId, serviceName);
    if (service == null) {
        return null;
    }

    List<String> clusters = new ArrayList<>();
    clusters.add(cluster);

    List<Instance> ips = service.allIPs(clusters);
    if (ips == null || ips.isEmpty()) {
        return null;
    }

    for (Instance instance : ips) {
        if (instance.getIp().equals(ip) && instance.getPort() == port) {
            return instance;
        }
    }

    return null;
}
  • 实例健康检查接口做的事件
    1. 获取实例
      1. 实例不存在注册实例
    2. 获取服务
      1. 服务不存在抛出异常
      2. 服务存在执行一个心跳方法
    3. 组装结果返回
@CanDistro
@PutMapping("/beat")
@Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
public JSONObject beat(HttpServletRequest request) throws Exception {

    JSONObject result = new JSONObject();

    result.put("clientBeatInterval", switchDomain.getClientBeatInterval());
    String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
    String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID,
        Constants.DEFAULT_NAMESPACE_ID);
    String clusterName = WebUtils.optional(request, CommonParams.CLUSTER_NAME,
        UtilsAndCommons.DEFAULT_CLUSTER_NAME);
    String ip = WebUtils.optional(request, "ip", StringUtils.EMPTY);
    int port = Integer.parseInt(WebUtils.optional(request, "port", "0"));
    String beat = WebUtils.optional(request, "beat", StringUtils.EMPTY);

    RsInfo clientBeat = null;
    if (StringUtils.isNotBlank(beat)) {
        clientBeat = JSON.parseObject(beat, RsInfo.class);
    }

    if (clientBeat != null) {
        if (StringUtils.isNotBlank(clientBeat.getCluster())) {
            clusterName = clientBeat.getCluster();
        }
        ip = clientBeat.getIp();
        port = clientBeat.getPort();
    }

    if (Loggers.SRV_LOG.isDebugEnabled()) {
        Loggers.SRV_LOG.debug("[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}", clientBeat, serviceName);
    }
    // 获取实例
    Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port);

    if (instance == null) {
        if (clientBeat == null) {
            result.put(CommonParams.CODE, NamingResponseCode.RESOURCE_NOT_FOUND);
            return result;
        }
        instance = new Instance();
        instance.setPort(clientBeat.getPort());
        instance.setIp(clientBeat.getIp());
        instance.setWeight(clientBeat.getWeight());
        instance.setMetadata(clientBeat.getMetadata());
        instance.setClusterName(clusterName);
        instance.setServiceName(serviceName);
        instance.setInstanceId(instance.getInstanceId());
        instance.setEphemeral(clientBeat.isEphemeral());

        serviceManager.registerInstance(namespaceId, serviceName, instance);
    }

    Service service = serviceManager.getService(namespaceId, serviceName);

    if (service == null) {
        throw new NacosException(NacosException.SERVER_ERROR,
            "service not found: " + serviceName + "@" + namespaceId);
    }
    if (clientBeat == null) {
        clientBeat = new RsInfo();
        clientBeat.setIp(ip);
        clientBeat.setPort(port);
        clientBeat.setCluster(clusterName);
    }
    // 处理心跳方法
    service.processClientBeat(clientBeat);

    result.put(CommonParams.CODE, NamingResponseCode.OK);
    result.put("clientBeatInterval", instance.getInstanceHeartBeatInterval());
    result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled());
    return result;
}

Sentinel底层LongAdder的计数实现

LongAdder 的原理

在 LongAdder 中,底层通过多个数值进行累加来得到最后的结果。当多个线程对同一个 LongAdder 进行更新的时候,将会对这一些列的集合进行动态更新,以避免多线程之间的资源竞争。当需要得到 LongAdder 的具体的值的时候,将会将一系列的值进行求和作为最后的结果。
在高并发的竞争下进行类似指标数据的收集的时候,LongAdder 通常会和 AtomicLong 进行比较,在低竞争的场景下,两者有着相似的性能表现。而当在高并发竞争的场景下,LongAdder 将会表现更高的性能,但是也会伴随更高的内存消耗。

LongAdder 的代码实现

transient volatile Cell[] cells;
transient volatile long base;

cells 是一个简单的 Cell 数组,当比如通过 LongAdder 的 add()方法进行 LongAdder 内部的数据的更新的时候,将会根据每个线程的一个 hash 值与 cells 数组的长度进行取模而定位,并在定位上的位置进行数据更新。而 base 则是当针对 LongAdder 的数据的更新时,并没有线程竞争的时候,将会直接更新在 base 上,而不需要前面提到的 hash 再定位过程,当 LongAdder 的 sum()方法被调用的时候,将会对 cells 的所有数据进行累加在加上 sum 的值进行返回。

public long sum() {
    long sum = base;
    Cell[] as = cells;
    if (as != null) {
        int n = as.length;
        for (int i = 0; i < n; ++i) {
            Cell a = as[i];
            if (a != null) { sum += a.value; }
        }
    }
    return sum;
}

相比 sum()方法,LongAdder 的 add()方法要复杂得多。

public void add(long x) {
    Cell[] as;
    long b, v;
    HashCode hc;
    Cell a;
    int n;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        int h = (hc = threadHashCode.get()).code;
        if (as == null || (n = as.length) < 1 ||
            (a = as[(n - 1) & h]) == null ||
            !(uncontended = a.cas(v = a.value, v + x))) { retryUpdate(x, hc, uncontended); }
    }
}

在 add()方法的一开始,将会观察 cells 数组是否存在,如果不存在,将会尝试直接通过 casBase()方法在 base 上通过 cas 更新,这是在低并发竞争下的 add()流程,这一流程的前提是对于 LongAdder 的更新并没有遭遇别的线程的并发修改。
在当 cells 已经存在,而或者对于 base 的 cas 更新失败,都将会将数据的更新落在 cells 数组之上。首先,每个线程都会在其 ThreadLocal 中生成一个线程专有的随机数,并根据这个随机数与 cells 进行取模,定位到的位置进行 cas 修改。在这个流程下,由于根据线程专有的随机数进行 hash 而定位的流程,尽可能的避免了线程间的资源竞争。但是仍旧可能存在 hash 碰撞而导致两个线程定位到了同一个 cells 槽位的情况,这里就需要通过 retryUpdate()方法进行进一步的解决。
retryUpdate()方法的代码很长,但是逻辑很清晰,主要分为一下几个流程,其中的主流程是一个死循环,进入 retryUpdate()方法后,将会不断尝试执行主要逻辑,直到对应的逻辑执行完毕:
1.当进入 retryUpdate()的时候,cells 数组还没有创建,将会尝试获取锁并初始化 cells 数组并直接在 cells 数组上进行修改,而别的线程在没创建的情况下进入并获取锁失败,将会直接尝试在 base 上进行更行。 2.当进入 retryUpdate()的时候,cells 数组已经创建,但是分配给其的数组槽位的 Cells 还没有进行初始化,那么将会尝试获取锁并对该槽位进行初始化。 3.当进入 retryUpdate()的时候,cells 数组已经创建,分配给其的槽位的 Cell 也已经完成了初始化,而是因为所定位到的槽位与别的线程发生了 hash 碰撞,那么将会加锁并扩容 cells 数组,之后对该线程持有的 hash 进行 rehash,在下一轮循环中对新定位的槽位数据进行更新。而别的线程在尝试扩容并获取锁失败的时候,将会直接对自己 rehash 并在下一轮的循环中重新在新的 cells 数组中进行定位更新。

Cell 本身的内存填充

最后,提一下 cells 数组中的 Cell 对象。

volatile long p0, p1, p2, p3, p4, p5, p6;
volatile long value;
volatile long q0, q1, q2, q3, q4, q5, q6;

每个 Cell 对象中具体存放的 value 前后都由 7 个 long 类型的字段进行内存填充以避免缓存行伪共享而导致的缓存失效。

Sentinel时间窗口的实现

获取时间窗口的主要流程

在 Sentinel 中,主要是通过 LeapArray 类来实现滑动时间窗口的实现和选择。在 sentinel 的这个获取时间窗口并为时间窗口添加指标的过程中,主要的流程为:

  • 根据当前时间选择当前时间应该定位当前时间应该属于的时间窗口 id。
  • 根据时间窗口 id 获取时间窗口。这里可能会存在三种情况:
  1. 时间窗口还未建立,那么将会为此次流量的进入建立一个新的时间窗口返回,并且接下来这个时间窗口内的获取请求都将返回该窗口。
  2. 时间窗口已经建立的情况下,将会直接获取已经存在的符合条件的时间窗口。
  3. 时间窗口可能已经存在,但是当前获取的时间窗口已经过期,需要加锁,并重置当前时间窗口。
  4. 当前进入的时间已经远远落后当前的时间,目标时间窗口已经被 reset 更新成更新的时间窗口,那么将不会返回目标时间窗口,而是返回一个新的空的时间窗口进行统计,这个时间窗口不会再被重复利用。
    其中的第四个情况表明,sentinel 的滑动时间窗口是有时间范围的,这也是为了尽量减少 sentinel 的所占用的内存,默认情况下 sentinel 的采取的时间长度为 1 分钟和 1 秒钟。这里的实现与 LeapArray 类的结构非常有关系。
protected final AtomicReferenceArray<WindowWrap<T>> array;

在 LeapArray 中,时间窗口的存放通过一个由 AtomicReferenceArray 实现的 array 来实现。AtomicReferenceArray 支持原子读取和写入,并支持通过 cas 来为指定位置的成员进行更新。在时间窗口的创建并放回 array 的过程中,也就是上文的第一步,就是通过 AtomicReferenceArray 的 compareAndSet()方法来实现,保证并发下的线程安全。并发情况下,通过 cas 更新失败的线程将会回到就绪态,在下一次婚欢得到已经初始化完成的时间窗口。

private final ReentrantLock updateLock = new ReentrantLock();

此处的 updateLock 是专门在上述的第三个情况来进行加锁的,只有成功得到锁的线程才会对过期的时间窗口进行 reset 操作,其他没有成功获取的线程将不会挂起等待,而是通过 yield()方法回到就绪态在下一次的循环尝试重新获取该位置的时间窗口。在下一次获取该锁的线程可能已经完成了,那么将会执行上述第二步,否则继续回到就绪态等待下一次循环中再次获取该时间窗口。
以上两个数据结构是 LeapArray 类实现时间窗口在高并发下准确获取时间窗口并更新的关键。

以秒级别的时间窗口举个例子

在 sentinel 默认的秒级别时间窗口中,array 的大小为 2,也就是每 500ms 为一个时间窗口的大小。
因此当一个线程试图获取一个时间窗口来记录指标数据的时候,将会根据单个时间窗口的时间跨度进行取模,来得到 array 上对应的时间窗口的下标,在这个情况下,将为 0 或者 1,之后计算当前线程时间指标所属的时间窗口的起始时间,以此为依据来判断如果在后面如果获取到的时间窗口是过期还是正好所需要的。
最后,将会不断循环从 array 尝试获取之前计算得到下标位置处的时间窗口,可能发生的 4 种情况如上所示。在这个情况,如果 cas 失败或事没有尝试获取到更新锁,都不会阻塞或是挂起,而是通过 yield 重新回到就绪态等待下一次循环获取。

时间窗口本身的线程安全指标更新

在指标集合类的实现 MetricBucket 中,通过 LongAdder 类来记录单个指标的值而不是 AtomicLong,LongAdder 内部的核心思路是为各个线程分配一个专属变量进行更新,在需要总数的时候对这一系列进行累加,因此在更新值的时候相比 AtomicLong 会尽可能减少线程间的竞争,达到高效的 metric 更新。

Sentinel限流算法的实现

Sentinel 中漏桶算法的实现

Sentinel 中漏桶算法通过 RateLimiterController 来实现,在漏桶算法中,会记录上一个请求的到达时间,如果新到达的请求与上一次到达的请求之间的时间差小于限流配置所规定的最小时间,新到达的请求将会排队等待规定的最小间隔到达,或是直接失败。

@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    if (acquireCount <= 0) {
        return true;
    }

    if (count <= 0) {
        return false;
    }

    long currentTime = TimeUtil.currentTimeMillis();
    // 根据配置计算两次请求之间的最小时间
    long costTime = Math.round(1.0 * (acquireCount) / count * 1000);

    // 计算上一次请求之后,下一次允许通过的最小时间
    long expectedTime = costTime + latestPassedTime.get();

    if (expectedTime <= currentTime) {
        // 如果当前时间大于计算的时间,那么可以直接放行
        latestPassedTime.set(currentTime);
        return true;
    } else {
        // 如果没有,则计算相应需要等待的时间
        long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
        if (waitTime > maxQueueingTimeMs) {
            return false;
        } else {
            long oldTime = latestPassedTime.addAndGet(costTime);
            try {
                waitTime = oldTime - TimeUtil.currentTimeMillis();
            // 如果最大等待时间小于需要等待的时间,那么返回失败,当前请求被拒绝
                if (waitTime > maxQueueingTimeMs) {
                    latestPassedTime.addAndGet(-costTime);
                    return false;
                }
                // 在并发条件下等待时间可能会小于等于0
                if (waitTime > 0) {
                    Thread.sleep(waitTime);
                }
                return true;
            } catch (InterruptedException e) {
            }
        }
    }
    return false;
}

Sentinel 中令牌桶算法的实现

在 Sentinel 中,令牌桶算法通过 WarmUpController 类实现。在这个情况下,当配置每秒能通过多少请求后,那么在这里 sentinel 也会每秒往桶内添加多少的令牌。当一个请求进入的时候,将会从中移除一个令牌。由此可以得出,桶内的令牌越多,也说明当前的系统利用率越低。因此,当桶内的令牌数量超过某个阈值后,那么当前的系统可以称之为处于饱和状态。
当系统处于 饱和状态的时候,当前允许的最大 qps 将会随着剩余的令牌数量减少而缓慢增加,达到为系统预热热身的目的。

this.count = count;

this.coldFactor = coldFactor;

warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);

maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));

slope = (coldFactor - 1.0) / count / (maxToken - warningToken);

其中 count 是当前 qps 的阈值。coldFactor 则为冷却因子,warningToken 则为警戒的令牌数量,warningToken 的值为(热身时间长度 _ 每秒令牌的数量) / (冷却因子 - 1)。maxToken 则是最大令牌数量,具体的值为 warningToken 的值加上 (2 _ 热身时间长度 _ 每秒令牌数量) / (冷却因子 + 1)。当当前系统处于热身时间内,其允许通过的最大 qps 为 1 / (超过警戒数的令牌数 _ 斜率 slope + 1 / count),而斜率的值为(冷却因子 - 1) / count / (最大令牌数 - 警戒令牌数)。
举个例子: count = 3, coldFactor = 3,热身时间为 4 的时候,警戒令牌数为 6,最大令牌数为 12,当剩余令牌处于 6 和 12 之间的时候,其 slope 斜率为 1 / 9。 那么当剩余令牌数为 9 的时候的允许 qps 为 1.5。其 qps 将会随着剩余令牌数的不断减少而直到增加到 count 的值。

@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    long passQps = (long) node.passQps();

    long previousQps = (long) node.previousPassQps();
    // 首先重新计算其桶内剩余的数量
    syncToken(previousQps);

    // 开始计算它的斜率
    // 如果进入了警戒线,开始调整他的qps
    long restToken = storedTokens.get();
    if (restToken >= warningToken) {
        long aboveToken = restToken - warningToken;
        // 如果当前剩余的令牌数大于警戒数,那么需要根据准备的计算公式重新计算qps,这个qps小于设定的阈值
        double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
        if (passQps + acquireCount <= warningQps) {
            return true;
        }
    } else {
        if (passQps + acquireCount <= count) {
            return true;
        }
    }

    return false;
}

protected void syncToken(long passQps) {
    long currentTime = TimeUtil.currentTimeMillis();
    currentTime = currentTime - currentTime % 1000;
    long oldLastFillTime = lastFilledTime.get();
    if (currentTime <= oldLastFillTime) {
        return;
    }

    long oldValue = storedTokens.get();
    long newValue = coolDownTokens(currentTime, passQps);

    if (storedTokens.compareAndSet(oldValue, newValue)) {
        // 从桶内移除相应数量的令牌,并更新最后更新时间
        long currentValue = storedTokens.addAndGet(0 - passQps);
        if (currentValue < 0) {
            storedTokens.set(0L);
        }
        lastFilledTime.set(currentTime);
    }

}

private long coolDownTokens(long currentTime, long passQps) {
    long oldValue = storedTokens.get();
    long newValue = oldValue;

    // 当令牌的消耗程度远远低于警戒线的时候,将会补充令牌数
    if (oldValue < warningToken) {
        newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
    } else if (oldValue > warningToken) {
        if (passQps < (int)count / coldFactor) {
            // qps小于阈值 / 冷却因子的时候,说明此时还不需要根据剩余令牌数调整qps的阈值,所以也会补充
            newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
        }
    }
    return Math.min(newValue, maxToken);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值