工作原理
总体架构:
- 用户在Portal操作配置发布
- Portal调用Admin Service的接口操作发布
- Admin Service发布配置后,发送ReleaseMessage给各个Config Service
- Config Service收到ReleaseMessage后,通知对应的客户端
客户端:
上图简要描述了Apollo客户端的实现原理:
- 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
- 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
- 这是一个fallback机制,为了防止推送机制失效导致配置不更新
- 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
- 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。
- 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
- 客户端会把从服务端获取到的配置在本地文件系统缓存一份
- 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
- 应用程序可以从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 的人都会疑惑,为什么使用这种方式,而不是"那种"方式?
下面是作者宋顺的回复:
总结一下:
- 为什么不使用消息系统?太复杂,杀鸡用牛刀。
- 为什么不用 TCP 长连接?对网络环境要求高,容易推送失败。且有双写问题。
- 为什么使用 HTTP 长轮询?性能足够,结合 Servlet3 的异步特性,能够维持万级连接(一个客户端只有一个长连接)。直接使用 Servlet 的 HTTP 协议,比单独用 TCP 连接方便。HTTP 请求/响应模式,保证了不会出现双写的情况。最主要还是简单,性能暂时不是瓶颈。
长连接 / 长轮询
长轮询实际上就是在一个类似死循环里,不停请求 ConfigServer 的配置变化通知接口 notifications/v2,如果配置有变更,就会返回变更信息,然后向定时任务线程池提交一个任务,任务内容是执行 sync 方法。
在请求 ConfigServer 的时候,ConfigServer 使用了 Servlet 3 的异步特性,将 hold 住连接 30 秒,等到有通知就立刻返回,这样能够实现一个基于 HTTP 的长连接。
关于为什么使用 HTTP 长连接,初次接触 Apollo 的人都会疑惑,为什么使用这种方式,而不是"那种"方式?
下面是作者宋顺的回复:
总结一下:
- 为什么不使用消息系统?太复杂,杀鸡用牛刀。
- 为什么不用 TCP 长连接?对网络环境要求高,容易推送失败。且有双写问题。
- 为什么使用 HTTP 长轮询?性能足够,结合 Servlet3 的异步特性,能够维持万级连接(一个客户端只有一个长连接)。直接使用 Servlet 的 HTTP 协议,比单独用 TCP 连接方便。HTTP 请求/响应模式,保证了不会出现双写的情况。最主要还是简单,性能暂时不是瓶颈。
总结
本文没有贴很多的代码。因为不是一篇源码分析的文章。
总之,Apollo 的更新配置设计就是通过定时轮询和长轮询进行组合而来。
定时轮询负责调用获取配置接口,长轮询负责调用配置更新通知接口,长轮询得到结果后,将提交一个任务到定时轮询线程池里,执行同步操作——也就是调用获取配置接口。
为什么使用 HTTP 长轮询? 简单!简单!简单!
转载于:
我们来看下这个对应的代码http://www.iocoder.cn/Apollo/config-service-notifications/?self
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 服务里获取到配置。大概步骤如下:
- 首先限流。获取服务列表。然后根据是否有更新通知,决定此次重试几次,如果有更新,重试2次,反之一次。
- 优先请求通知自己的 configService,如果失败了,就要进行休息,休息策略要看是否得到更新通知了,如果是,就休息一秒,否则按照 SchedulePolicy 策略来。
- 拿到数据后,重置强制刷新状态和失败休息状态,返回配置。
触发监听器步骤:
- 循环远程仓库的监听器,调用他们的 onRepositoryChange 方法。其实就是 Config。
- 然后,更新 Config 内部的引用,循环向线程池提交任务—— 执行 Config 监听器的 onChange 方法。
好,到这里,定时任务就算处理完了,总之就是调用 sync 方法,请求远程 configServer 服务,得到结果后,更新 Config 对象里的配置,并通知监听器。
再来说说长轮询。
长连接 / 长轮询
长轮询实际上就是在一个类似死循环里,不停请求 ConfigServer 的配置变化通知接口 notifications/v2,如果配置有变更,就会返回变更信息,然后向定时任务线程池提交一个任务,任务内容是执行 sync 方法。
在请求 ConfigServer 的时候,ConfigServer 使用了 Servlet 3 的异步特性,将 hold 住连接 30 秒,等到有通知就立刻返回,这样能够实现一个基于 HTTP 的长连接。
关于为什么使用 HTTP 长连接,初次接触 Apollo 的人都会疑惑,为什么使用这种方式,而不是"那种"方式?
下面是作者宋顺的回复:
总结一下:
- 为什么不使用消息系统?太复杂,杀鸡用牛刀。
- 为什么不用 TCP 长连接?对网络环境要求高,容易推送失败。且有双写问题。
- 为什么使用 HTTP 长轮询?性能足够,结合 Servlet3 的异步特性,能够维持万级连接(一个客户端只有一个长连接)。直接使用 Servlet 的 HTTP 协议,比单独用 TCP 连接方便。HTTP 请求/响应模式,保证了不会出现双写的情况。最主要还是简单,性能暂时不是瓶颈。
总结
本文没有贴很多的代码。因为不是一篇源码分析的文章。
总之,Apollo 的更新配置设计就是通过定时轮询和长轮询进行组合而来。
定时轮询负责调用获取配置接口,长轮询负责调用配置更新通知接口,长轮询得到结果后,将提交一个任务到定时轮询线程池里,执行同步操作——也就是调用获取配置接口。
为什么使用 HTTP 长轮询? 简单!简单!简单!
通过spring提供的DeferredResult实现长轮询服务端推送消息
xiao_jun_0820 2018-10-07 11:28:59
7828
收藏 10
分类专栏: springboot apollo
版权
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长轮询的方式会在服务器变更的时候主动推送给客户端,其他时间客户端是挂起请求的,这样同时满足了性能和实时性。
整个代码相当的经典呀,整个代码的下载地址为如下
设置springboot自带tomcat的最大连接数和最大并发数
卡卡的喵 2018-11-09 15:48:24
89293
收藏 42
分类专栏: spring 文章标签: springboot tomcat 并发数 连接数
版权
从源代码来看,最大连接数和最大并发数默认是10000和200
可以通过工程下的application.yml配置文件来改变这个值
server:
tomcat:
uri-encoding: UTF-8
max-threads: 1000
max-connections: 20000
默认情况下apollo的长轮询是基于http的异步响应的,一个tomcat的默认最大连接数是10000,所以一个configservice进程支持最大10000个连接是没有问题的。