apollo客户端的长轮询机制的原理

工作原理

总体架构:

 

 

 

  1. 用户在Portal操作配置发布
  2. Portal调用Admin Service的接口操作发布
  3. Admin Service发布配置后,发送ReleaseMessage给各个Config Service
  4. Config Service收到ReleaseMessage后,通知对应的客户端

 

客户端:

 

 

上图简要描述了Apollo客户端的实现原理:

  1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
  2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
  • 这是一个fallback机制,为了防止推送机制失效导致配置不更新
  • 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
  • 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。
  1. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
  2. 客户端会把从服务端获取到的配置在本地文件系统缓存一份
  • 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
  1. 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知

 

 

这里重点分析下apollo客户端和后端服务的长轮询机制的原理是啥

Apollo为什么用长轮询而不是长连接?

1、长连采用HTTP而非TCP的主要考虑点是多语言的适配;

2、采用异步servlet,单机可以支撑1W个连接,也就是一万个客户端,一般10台服务器就能抗住一个中小型公司的连接数;

http://www.iocoder.cn/Apollo/config-service-notifications/?self

http://www.kailing.pub/article/index/arcid/163.html

apollo关于配置变更通知这块,主要分为了2个部分:
1.  如何发现配置变更?  apollo解耦了「配置变更」和「配置变更发布」, 配置变更就是一个数据库操作,除了保存本身的配置信息,还会新增保存一个「ReleaseMessage」到数据库, 表示有变更发生, config server中的一个线程,每隔1S扫描「ReleaseMessage」表, 以此发现变更的配置。
2. 如何通知客户端? 使用的是http的长轮询, 客户端在启动后,会连接cofig server, 连接90秒超时, config server收到请求会hang到服务端不返回, 如果90秒内没有配置变更,就自动返回, 如果在90秒内有配置变更,请求会立刻返回,并且携带namespace。 客户端收到请求后,会立刻重新请求服务端。

长连接 / 长轮询

长轮询实际上就是在一个类似死循环里,不停请求 ConfigServer 的配置变化通知接口 notifications/v2,如果配置有变更,就会返回变更信息,然后向定时任务线程池提交一个任务,任务内容是执行 sync 方法。

在请求 ConfigServer 的时候,ConfigServer 使用了 Servlet 3 的异步特性,将 hold 住连接 30 秒,等到有通知就立刻返回,这样能够实现一个基于 HTTP 的长连接。

关于为什么使用 HTTP 长连接,初次接触 Apollo 的人都会疑惑,为什么使用这种方式,而不是"那种"方式?

下面是作者宋顺的回复:
1240

总结一下:

  1. 为什么不使用消息系统?太复杂,杀鸡用牛刀。
  2. 为什么不用 TCP 长连接?对网络环境要求高,容易推送失败。且有双写问题。
  3. 为什么使用 HTTP 长轮询?性能足够,结合 Servlet3 的异步特性,能够维持万级连接(一个客户端只有一个长连接)。直接使用 Servlet 的 HTTP 协议,比单独用 TCP 连接方便。HTTP 请求/响应模式,保证了不会出现双写的情况。最主要还是简单,性能暂时不是瓶颈。

长连接 / 长轮询

长轮询实际上就是在一个类似死循环里,不停请求 ConfigServer 的配置变化通知接口 notifications/v2,如果配置有变更,就会返回变更信息,然后向定时任务线程池提交一个任务,任务内容是执行 sync 方法。

在请求 ConfigServer 的时候,ConfigServer 使用了 Servlet 3 的异步特性,将 hold 住连接 30 秒,等到有通知就立刻返回,这样能够实现一个基于 HTTP 的长连接。

关于为什么使用 HTTP 长连接,初次接触 Apollo 的人都会疑惑,为什么使用这种方式,而不是"那种"方式?

下面是作者宋顺的回复:
1240

总结一下:

  1. 为什么不使用消息系统?太复杂,杀鸡用牛刀。
  2. 为什么不用 TCP 长连接?对网络环境要求高,容易推送失败。且有双写问题。
  3. 为什么使用 HTTP 长轮询?性能足够,结合 Servlet3 的异步特性,能够维持万级连接(一个客户端只有一个长连接)。直接使用 Servlet 的 HTTP 协议,比单独用 TCP 连接方便。HTTP 请求/响应模式,保证了不会出现双写的情况。最主要还是简单,性能暂时不是瓶颈。

总结

本文没有贴很多的代码。因为不是一篇源码分析的文章。

总之,Apollo 的更新配置设计就是通过定时轮询和长轮询进行组合而来。

定时轮询负责调用获取配置接口,长轮询负责调用配置更新通知接口,长轮询得到结果后,将提交一个任务到定时轮询线程池里,执行同步操作——也就是调用获取配置接口。

为什么使用 HTTP 长轮询? 简单!简单!简单!

转载于:https://www.cnblogs.com/stateis0/p/9255966.html

我们来看下这个对应的代码http://www.iocoder.cn/Apollo/config-service-notifications/?self

 

 

https://juejin.cn/post/6844903733990703118

http://www.iocoder.cn/Apollo/config-service-notifications/?self

apollo关于配置变更通知这块,主要分为了2个部分:
1.  如何发现配置变更?  apollo解耦了「配置变更」和「配置变更发布」, 配置变更就是一个数据库操作,除了保存本身的配置信息,还会新增保存一个「ReleaseMessage」到数据库, 表示有变更发生, config server中的一个线程,每隔1S扫描「ReleaseMessage」表, 以此发现变更的配置。
2. 如何通知客户端? 使用的是http的长轮询, 客户端在启动后,会连接cofig server, 连接90秒超时, config server收到请求会hang到服务端不返回, 如果90秒内没有配置变更,就自动返回, 如果在90秒内有配置变更,请求会立刻返回,并且携带namespace。 客户端收到请求后,会立刻重新请求服务端。
http://www.kailing.pub/article/index/arcid/163.html
https://www.jianshu.com/p/da11fcc4e8e6

Apollo 3 — 定时/长轮询拉取配置的设计

2018.06.30 12:21:28字数 1,435阅读 989
 
Apollo 基础模型

前言

如上图所示,Apollo portal 更新配置后,进行轮询的客户端获取更新通知,然后再调用接口获取最新配置。不仅仅只有轮询,还有定时更新(默认 5 分钟一次)。目的就是让客户端能够稳定的获取到最新的配置。

一起来看看他的设计。

核心代码

具体的类是 RemoteConfigRepository,每一个 Config —— 也就是 namespace 都有一个 RemoteConfigRepository 对象,表示这个 Config 的远程配置仓库,可以利用这个仓库请求远程服务,得到配置。

RemoteConfigRepository 的构造方法需要一个 namespace 字符串,表明这个 Repository 所属的 Config 名称。

下面是该类的构造方法。

public RemoteConfigRepository(String namespace) {
    m_namespace = namespace;// Config 名称
    m_configCache = new AtomicReference<>(); //  Config 引用
    m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);// 单例的 config 配置,存放 application.properties
    m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);// HTTP 工具
    m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);// 远程服务 URL 更新类
    remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);// 长轮询服务
    m_longPollServiceDto = new AtomicReference<>();// 长轮询发现的当前配置发生变化的服务
    m_remoteMessages = new AtomicReference<>();
    m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS());// 限流器
    m_configNeedForceRefresh = new AtomicBoolean(true);// 是否强制刷新
    m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(),//1
        m_configUtil.getOnErrorRetryInterval() * 8);// 1 * 8;失败定时重试策略: 最小一秒,最大 8 秒.
    gson = new Gson();// json 序列化
    this.trySync(); // 第一次同步
    this.schedulePeriodicRefresh();// 定时刷新
    this.scheduleLongPollingRefresh();// 长轮询刷新
}

可以看到,在构造方法中,就执行了 3 个本地方法,其中就包括定时刷新和长轮询刷新。这两个功能在 apollo 的 github 文档中也有介绍:

1.客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
2.客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
3.这是一个fallback机制,为了防止推送机制失效导致配置不更新。
4.客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified。
5.定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。

所以,长连接是更新配置的主要手段,然后用定时任务辅助长连接,防止长连接失败。

那就看看长连接和定时任务的具体代码。

定时任务

定时任务主要由一个单 core 的线程池维护这定时任务。

static {
    // 定时任务,单个 core. 后台线程
    m_executorService = Executors.newScheduledThreadPool(1,
        ApolloThreadFactory.create("RemoteConfigRepository", true));
}

private void schedulePeriodicRefresh() {
    // 默认 5 分钟同步一次.
    m_executorService.scheduleAtFixedRate(
        new Runnable() {
          @Override
          public void run() {
            trySync();
          }
        }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),// 5
        m_configUtil.getRefreshIntervalTimeUnit());//单位:分钟
}

具体就是每 5 分钟执行 sync 方法。我简化了一下 sync 方法,一起看看:

protected synchronized void sync() {
  ApolloConfig previous = m_configCache.get();
  // 加载远程配置
  ApolloConfig current = loadApolloConfig();

  //reference equals means HTTP 304
  if (previous != current) {
    m_configCache.set(current);
    // 触发监听器
    this.fireRepositoryChange(m_namespace, this.getConfig());
  }
}

首先,拿到上一个 config 对象的引用,然后,加载远程配置,判断是否相等,如果不相等,更新引用缓存,触发监听器。

可以看出,关键是加载远程配置和触发监听器,这两个操作。

loadApolloConfig 方法主要逻辑就是通过 HTTP 请求从 configService 服务里获取到配置。大概步骤如下:

  1. 首先限流。获取服务列表。然后根据是否有更新通知,决定此次重试几次,如果有更新,重试2次,反之一次。
  2. 优先请求通知自己的 configService,如果失败了,就要进行休息,休息策略要看是否得到更新通知了,如果是,就休息一秒,否则按照 SchedulePolicy 策略来。
  3. 拿到数据后,重置强制刷新状态和失败休息状态,返回配置。

触发监听器步骤:

  1. 循环远程仓库的监听器,调用他们的 onRepositoryChange 方法。其实就是 Config。
  2. 然后,更新 Config 内部的引用,循环向线程池提交任务—— 执行 Config 监听器的 onChange 方法。

好,到这里,定时任务就算处理完了,总之就是调用 sync 方法,请求远程 configServer 服务,得到结果后,更新 Config 对象里的配置,并通知监听器。

再来说说长轮询。

长连接 / 长轮询

长轮询实际上就是在一个类似死循环里,不停请求 ConfigServer 的配置变化通知接口 notifications/v2,如果配置有变更,就会返回变更信息,然后向定时任务线程池提交一个任务,任务内容是执行 sync 方法。

在请求 ConfigServer 的时候,ConfigServer 使用了 Servlet 3 的异步特性,将 hold 住连接 30 秒,等到有通知就立刻返回,这样能够实现一个基于 HTTP 的长连接。

关于为什么使用 HTTP 长连接,初次接触 Apollo 的人都会疑惑,为什么使用这种方式,而不是"那种"方式?

下面是作者宋顺的回复:


 

总结一下:

  1. 为什么不使用消息系统?太复杂,杀鸡用牛刀。
  2. 为什么不用 TCP 长连接?对网络环境要求高,容易推送失败。且有双写问题。
  3. 为什么使用 HTTP 长轮询?性能足够,结合 Servlet3 的异步特性,能够维持万级连接(一个客户端只有一个长连接)。直接使用 Servlet 的 HTTP 协议,比单独用 TCP 连接方便。HTTP 请求/响应模式,保证了不会出现双写的情况。最主要还是简单,性能暂时不是瓶颈。

总结

本文没有贴很多的代码。因为不是一篇源码分析的文章。

总之,Apollo 的更新配置设计就是通过定时轮询和长轮询进行组合而来。

定时轮询负责调用获取配置接口,长轮询负责调用配置更新通知接口,长轮询得到结果后,将提交一个任务到定时轮询线程池里,执行同步操作——也就是调用获取配置接口。

为什么使用 HTTP 长轮询? 简单!简单!简单!

 

https://blog.csdn.net/xiao_jun_0820/article/details/82956593

通过spring提供的DeferredResult实现长轮询服务端推送消息

 

 

DeferredResult字面意思就是推迟结果,是在servlet3.0以后引入了异步请求之后,spring封装了一下提供了相应的支持,也是一个很老的特性了。DeferredResult可以允许容器线程快速释放以便可以接受更多的请求提升吞吐量,让真正的业务逻辑在其他的工作线程中去完成。

最近再看apollo配置中心的实现原理,apollo的发布配置推送变更消息就是用DeferredResult实现的,apollo客户端会像服务端发送长轮训http请求,超时时间60秒,当超时后返回客户端一个304 httpstatus,表明配置没有变更,客户端继续这个步骤重复发起请求,当有发布配置的时候,服务端会调用DeferredResult.setResult返回200状态码,然后轮训请求会立即返回(不会超时),客户端收到响应结果后,会发起请求获取变更后的配置信息。

下面我们自己写一个简单的demo来演示这个过程

springboot启动类:

 

 

/**
 * 功能说明:
 * 功能作者:
 * 创建日期:
 * 版权归属:每特教育|蚂蚁课堂所有 www.itmayiedu.com
 */
package com.itmayiedu.api.controller;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;

/**
 * 功能说明: <br>
 * 创建作者:每特教育-余胜军<br>
 * 创建时间:2018年8月28日 下午9:09:14<br>
 * 教育机构:每特教育|蚂蚁课堂<br>
 * 版权说明:上海每特教育科技有限公司版权所有<br>
 * 官方网站:www.itmayiedu.com|www.meitedu.com<br>
 * 联系方式:qq644064779<br>
 * 注意:本内容有每特教育学员共同研发,请尊重原创版权
 */
//@EnableApolloConfig({"application", "spring-rocketmq","spring-redis"})
@SpringBootApplication
public class App implements WebMvcConfigurer {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
    
    
    @Bean
    public ThreadPoolTaskExecutor mvcTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setQueueCapacity(100);
        executor.setMaxPoolSize(25);
        return executor;
 
    }
    
    //配置异步支持,设置了一个用来异步执行业务逻辑的工作线程池,设置了默认的超时时间是60秒
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.setTaskExecutor(mvcTaskExecutor());
        configurer.setDefaultTimeout(60000L);
    }

}

首先我来看下启动类实现implements WebMvcConfigurer,在里面模拟创建了一个线程池,这个线程池用来模拟apollo的configserver的进程,在线程池中设置超时时间为60秒,等价于apollo的客户端长轮询confisevice,configserverhold住线程60秒

我们来看下controller类

package com.itmayiedu.api.controller;
 
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
 
import java.util.Collection;
 
@RestController
public class ApolloController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
    //guava中的Multimap,多值map,对map的增强,一个key可以保持多个value
    private Multimap<String, DeferredResult<String>> watchRequests = Multimaps.synchronizedSetMultimap(HashMultimap.create());
 
 
    //模拟长轮询
    @RequestMapping(value = "/watch/{namespace}", method = RequestMethod.GET, produces = "text/html")
    public DeferredResult<String> watch(@PathVariable("namespace") String namespace) {
        logger.info("Request received");
        DeferredResult<String> deferredResult = new DeferredResult<>();
        //当deferredResult完成时(不论是超时还是异常还是正常完成),移除watchRequests中相应的watch key
        deferredResult.onCompletion(new Runnable() {
            @Override
            public void run() {
                System.out.println("remove key:" + namespace);
                watchRequests.remove(namespace, deferredResult);
            }
        });
        watchRequests.put(namespace, deferredResult);
        logger.info("Servlet thread released");
        return deferredResult;
 
 
    }
 
    //模拟发布namespace配置
    @RequestMapping(value = "/publish/{namespace}", method = RequestMethod.GET, produces = "text/html")
    public Object publishConfig(@PathVariable("namespace") String namespace) {
        if (watchRequests.containsKey(namespace)) {
            Collection<DeferredResult<String>> deferredResults = watchRequests.get(namespace);
            Long time = System.currentTimeMillis();
            //通知所有watch这个namespace变更的长轮训配置变更结果
            for (DeferredResult<String> deferredResult : deferredResults) {
                deferredResult.setResult(namespace + " changed:" + time);
            }
        }
        return "success";
 
    }
    
    
    //模拟发布namespace配置
    @RequestMapping(value = "/hello", method = RequestMethod.GET, produces = "text/html")
    public Object publishConfig2() {
        
        return "success";
 
    }
}

在这个control控制的业务类中,有两个接口,这里使用到了servlet 3.0 异步响应的DeferredResult技术

当访问/watch/{namespace}接口的时候,DeferredResult技术中会把当前的客户端访问的线程hold住,如果DeferredResult中保存的值有变化会通过http立刻返回给调用方,如果DeferredResult中保存的值没有发送变化,会一直hold住线程60秒,直到发生网络超时

当外部访问"/publish/{namespace}接口的时候,就是更新DeferredResult中保存的值,key为外部访问传入的namespace,value为当前对应的时间,如果外部更新了namespace对应的值,那么DeferredResult会立刻释放hold住的线程给客户端

apolloconfigserver设计的时候,当管理员更新了配置的时候,会更新releaseMessage这张表,configserver进程会每隔60秒扫描releaseMessage这张表是否有变化,一旦存在变化,就会触发DeferredResult中保存的对应的namespace,会立刻通过apollo的客户端,这也就是apollo的客户端能够1秒钟能够实时收到响应的原因

如果在hold住线程60秒内,配置的值没有发送变化,就会抛出网络异常,我们需要对整个网络异常进行拦截处理,返回给请求的客户端一个http状态码为304的状态码,表示整个配置项没有发生变化

package com.itmayiedu.api.controller;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
@ControllerAdvice
class GlobalControllerExceptionHandler {
 
    protected static final Logger logger = LoggerFactory.getLogger(GlobalControllerExceptionHandler.class);
 
    @ResponseStatus(HttpStatus.NOT_MODIFIED)//返回304状态码
    @ResponseBody
    @ExceptionHandler(AsyncRequestTimeoutException.class) //捕获特定异常
    public void handleAsyncRequestTimeoutException(AsyncRequestTimeoutException e, HttpServletRequest request) {
        System.out.println("handleAsyncRequestTimeoutException");
    }
}

然后我们通过postman工具发送请求http://localhost:8080/watch/mynamespace,请求会挂起,60秒后,DeferredResult超时,客户端正常收到了304状态码,表明在这个期间配置没有变更过。

然后我们在模拟配置变更的情况,再次发起请求http://localhost:8080/watch/mynamespace,等待个10秒钟(不要超过60秒),然后调用http://localhost:8080/publish/mynamespace,发布配置变更。这时postman会立刻收到response响应结果:

mynamespace changed:1538880050147

表明在轮训期间有配置变更过。

这里我们用了一个MultiMap来存放所有轮训的请求,Key对应的是namespace,value对应的是所有watch这个namespace变更的异步请求DeferredResult,需要注意的是:在DeferredResult完成的时候记得移除MultiMap中相应的key,避免内存溢出请求。

采用这种长轮询的好处是,相比一直循环请求服务器,实例一多的话会对服务器产生很大的压力,http长轮询的方式会在服务器变更的时候主动推送给客户端,其他时间客户端是挂起请求的,这样同时满足了性能和实时性。

整个代码相当的经典呀,整个代码的下载地址为如下

 

 https://blog.csdn.net/kouwoo/article/details/83898788

设置springboot自带tomcat的最大连接数和最大并发数

从源代码来看,最大连接数和最大并发数默认是10000和200

可以通过工程下的application.yml配置文件来改变这个值

server:
  tomcat:
    uri-encoding: UTF-8
    max-threads: 1000
    max-connections: 20000

默认情况下apollo的长轮询是基于http的异步响应的,一个tomcat的默认最大连接数是10000,所以一个configservice进程支持最大10000个连接是没有问题的。

 

posted on 2021-02-23 13:51  luzhouxiaoshuai  阅读(2374)  评论(0编辑  收藏  举报

导航