zoukankan      html  css  js  c++  java
  • 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个连接是没有问题的。

  • 相关阅读:
    sqlserver tips
    mysql tips
    小知识点集锦
    设计模式
    将微博或者qq空间的说说同步至博客园 wcf+js(ajax)跨域请求(1)
    WCF服务寄宿IIS与Windows服务
    C# 基础小知识之yield 关键字
    WPF命令绑定 自定义命令
    KnockOut 绑定之foreach绑定(mvc+knockout)
    P5019 铺设道路
  • 原文地址:https://www.cnblogs.com/kebibuluan/p/14435592.html
Copyright © 2011-2022 走看看