Apollo服务端设计原理剖析
本文摘自于《Spring Cloud微服务 入门 实战与进阶》一书。
1 配置发布后的实时推送设计
配置中心最重要的一个特性就是实时推送了,正因为有这个特性,我们可以依赖配置中心做很多事情。在我自己开发的Smconf这个配置中心,Smconf是依赖于Zookeeper的Watch机制来实现实时推送。

上图简要描述了配置发布的大致过程:
- 用户在Portal中进行配置的编辑和发布
- Portal会调用Admin Service提供的接口进行发布操作
- Admin Service收到请求后,发送ReleaseMessage给各个Config Service,通知Config Service配置发生变化
- Config Service收到ReleaseMessage后,通知对应的客户端,基于Http长连接实现
2 发送ReleaseMessage的实现方式
ReleaseMessage消息是通过Mysql实现了一个简单的消息队列。之所有没有采用消息中间件,是为了让Apollo在部署的时候尽量简单,尽可能减少外部依赖。

上图简要描述了发送ReleaseMessage的大致过程:
- Admin Service在配置发布后会往ReleaseMessage表插入一条消息记录
- Config Service会启动一个线程定时扫描ReleaseMessage表,去查看是否有新的消息记录
- Config Service发现有新的消息记录,那么就会通知到所有的消息监听器
- 消息监听器得到配置发布的信息后,则会通知对应的客户端
3 Config Service通知客户端的实现方式
通知是采用基于Http长连接实现,主要分为下面几个步骤:
- 客户端会发起一个Http请求到Config Service的notifications/v2接口
- v2接口通过Spring DeferredResult把请求挂起,不会立即返回
- 如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端
- 如果发现配置有修改,则会调用DeferredResult的setResult方法,传入有配置变化的namespace信息,同时该请求会立即返回
- 客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置
4 源码解析实时推送设计
Apollo推送这块代码比较多,就不在本书中详细分析了,我把推送这块的代码稍微简化了下,给大家进行讲解,这样理解起来会更容易。当然我这边会比较简单,很多细节就不做考虑了,只是为了能够让大家明白Apollo推送的核心原理。
发送ReleaseMessage的逻辑我们就写一个简单的接口,用队列存储,测试的时候就调用这个接口模拟配置有更新,发送ReleaseMessage消息。
@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
// 模拟配置更新,往里插入数据表示有更新
public static Queue<String> queue = new LinkedBlockingDeque<>();
@GetMapping("/addMsg")
public String addMsg() {
queue.add("xxx");
return "success";
}
}
消息发送之后,前面我们有讲过Config Service会启动一个线程定时扫描ReleaseMessage表,去查看是否有新的消息记录,然后取通知客户端,这边我们也启动一个线程去扫描:
@Component
public class ReleaseMessageScanner implements InitializingBean {
@Autowired
private NotificationControllerV2 configController;
@Override
public void afterPropertiesSet() throws Exception {
// 定时任务从数据库扫描有没有新的配置发布
new Thread(() -> {
for (;;) {
String result = NotificationControllerV2.queue.poll();
if (result != null) {
ReleaseMessage message = new ReleaseMessage();
message.setMessage(result);
configController.handleMessage(message);
}
}
}).start();;
}
}
循环去读取NotificationControllerV2中的队列,如果有消息的话就构造一个ReleaseMessage的对象,然后调用NotificationControllerV2中的handleMessage()方法进行消息的处理。
ReleaseMessage就一个字段,模拟消息内容:
public class ReleaseMessage {
private String message;
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
接下来,我们看handleMessage做了什么样的工作
NotificationControllerV2实现了ReleaseMessageListener接口,ReleaseMessageListener中定义了handleMessage()方法。
public interface ReleaseMessageListener {
void handleMessage(ReleaseMessage message);
}
handleMessage就是当配置发生变化的时候,通知的消息监听器,消息监听器得到配置发布的信息后,则会通知对应的客户端:
@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps
.synchronizedSetMultimap(HashMultimap.create());
@Override
public void handleMessage(ReleaseMessage message) {
System.err.println("handleMessage:"+ message);
List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get("xxxx"));
for (DeferredResultWrapper deferredResultWrapper : results) {
List<ApolloConfigNotification> list = new ArrayList<>();
list.add(new ApolloConfigNotification("application", 1));
deferredResultWrapper.setResult(list);
}
}
}
Apollo的实时推送是基于Spring DeferredResult实现的,在handleMessage()方法中可以看到是通过deferredResults获取DeferredResult,deferredResults就是第一行的Multimap,Key其实就是消息内容,Value就是DeferredResult的业务包装类DeferredResultWrapper,我们来看下DeferredResultWrapper的代码:
public class DeferredResultWrapper {
private static final long TIMEOUT = 60 * 1000;// 60 seconds
private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST =
new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;
public DeferredResultWrapper() {
result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);
}
public void onTimeout(Runnable timeoutCallback) {
result.onTimeout(timeoutCallback);
}
public void onCompletion(Runnable completionCallback) {
result.onCompletion(completionCallback);
}
public void setResult(ApolloConfigNotification notification) {
setResult(Lists.newArrayList(notification));
}
public void setResult(List<ApolloConfigNotification> notifications) {
result.setResult(new ResponseEntity<>(notifications, HttpStatus.OK));
}
public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getResult() {
return result;
}
}
通过setResult()方法设置返回结果给客户端,以上就是当配置发生变化,然后通过消息监听器通知客户端的原理,那么客户端是在什么时候接入的呢?
@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
// 模拟配置更新,往里插入数据表示有更新
public static Queue<String> queue = new LinkedBlockingDeque<>();
private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps
.synchronizedSetMultimap(HashMultimap.create());
@GetMapping("/getConfig")
public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getConfig() {
DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper();
List<ApolloConfigNotification> newNotifications = getApolloConfigNotifications();
if (!CollectionUtils.isEmpty(newNotifications)) {
deferredResultWrapper.setResult(newNotifications);
} else {
deferredResultWrapper.onTimeout(() -> {
System.err.println("onTimeout");
});
deferredResultWrapper.onCompletion(() -> {
System.err.println("onCompletion");
});
deferredResults.put("xxxx", deferredResultWrapper);
}
return deferredResultWrapper.getResult();
}
private List<ApolloConfigNotification> getApolloConfigNotifications() {
List<ApolloConfigNotification> list = new ArrayList<>();
String result = queue.poll();
if (result != null) {
list.add(new ApolloConfigNotification("application", 1));
}
return list;
}
}
NotificationControllerV2中提供了一个/getConfig的接口,客户端在启动的时候会调用这个接口,这个时候会执行getApolloConfigNotifications()方法去获取有没有配置的变更信息,如果有的话证明配置修改过,直接就通过deferredResultWrapper.setResult(newNotifications);返回结果给客户端了,客户端收到结果后重新拉取配置的信息进行覆盖本地的配置。
如果getApolloConfigNotifications()方法没有返回配置修改的信息,证明配置没有发生修改,就将DeferredResultWrapper对象添加到deferredResults中,等待后续配置发生变化时消息监听器进行通知。
同时这个请求就会挂起,不会立即返回,挂起是通过DeferredResultWrapper中的下面的代码实现的:
private static final long TIMEOUT = 60 * 1000;// 60 seconds
private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST =
new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;
public DeferredResultWrapper() {
result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);
}
在创建DeferredResult对象的时候指定了超时的时间和超时后返回的响应码,如果60秒内没有消息监听器进行通知,那么这个请求就会超时,超时后客户端就收到的响应码就是304。
整个Config Service的流程就走完了,接下来我们看客户端是怎么实现的,我们简单的写个测试类模拟客户端注册:
public class ClientTest {
public static void main(String[] args) {
reg();
}
private static void reg() {
System.err.println("注册");
String result = request("http://localhost:8081/getConfig");
if (result != null) {
// 配置有更新,重新拉取配置
// ......
}
// 重新注册
reg();
}
private static String request(String url) {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL getUrl = new URL(url);
connection = (HttpURLConnection) getUrl.openConnection();
connection.setReadTimeout(90000);
connection.setConnectTimeout(3000);
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept-Charset", "utf-8");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Charset", "UTF-8");
System.out.println(connection.getResponseCode());
if (200 == connection.getResponseCode()) {
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
StringBuilder result = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
result.append(line);
}
System.out.println("结果 " + result);
return result.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.disconnect();
}
}
return null;
}
}
首先启动/getConfig接口所在的服务,然后启动客户端,客户端就会发起注册请求,如果有修改直接获取到结果,进行配置的更新操作。如果无修改,请求会挂起,这边客户端设置的读取超时时间是90秒,大于服务端的60秒超时时间。
每次收到结果后,无论是有修改还是没修改,都必须重新进行注册,通过这样的方式就可以达到配置实时推送的效果。
我们可以调用之前写的/addMsg接口来模拟配置发生变化,调用之后客户端就能马上得到返回结果。
本文摘自于《Spring Cloud微服务 入门 实战与进阶》一书。

去年出版的《Spring Cloud微服务:全栈技术与案例解析》一书,得到了大家的支持以及反馈,基于大家的反馈,重新进行了更正和改进。
基于比较稳定的 Spring Cloud Finchley.SR2 版本和 Spring Boot 2.0.6.RELEASE 版本编写。
同时将示列代码进行标准的归档,之前的都在一起,不方便读者参考和运行。


同时还增加了像Apollo,Spring Cloud Gateway,生产实践经验等新的内容。
Apollo服务端设计原理剖析的更多相关文章
- TYPESDK手游聚合SDK服务端设计思路与架构之一:应用场景分析
TYPESDK 服务端设计思路与架构之一:应用场景分析 作为一个渠道SDK统一接入框架,TYPESDK从一开始,所面对的需求场景就是多款游戏,通过一个统一的SDK服务端,能够同时接入几十个甚至几百个各 ...
- TYPESDK手游聚合SDK服务端设计思路与架构之二:服务端设计
在前一篇文中,我们对一个聚合SDK服务端所需要实现的功能作了简单的分析.通过两个主要场景的功能流程图,我们可以看到,作为多款游戏要适配多个渠道的统一请求转发中心,TYPESDK服务端主要需要实现的功能 ...
- TYPESDK手游聚合SDK服务端设计思路与架构之四:流程优化之信息安全与订单校验
有了前文几个步骤的分析和设计,TYPESDK的信息交互流程已经可以正常工作了,但是,这个流程还没有考虑到支付这样的过程中,至关重要的信息安全问题. 在整个交互过程中,游戏服务端,SDK服务端,渠道服务 ...
- TYPESDK手游聚合SDK服务端设计思路与架构之三:流程优化之订单保存与通知
经过前两篇文字的分析与设计,我们已经可以搭建出一个能够支持多游戏多渠道的聚合SDK服务端,但这只是理想化状态下的一个简化模型.如果接入渠道的逻辑都是按照理想化的简化过程来构建,那么对于支付的请求,我们 ...
- 移动APP服务端设计开发注意要点
2014年,移动APP的热度丝毫没有减退,怎么为您的移动端app设计良好的服务器端接口(API)呢? 下面谈谈我个人的一些想法. 2014年,移动APP的热度丝毫没有减退,并没有像桌面软件被WEB网站 ...
- cas sso单点登录系列2:cas客户端和cas服务端交互原理动画图解,cas协议终极分析
转:http://blog.csdn.net/ae6623/article/details/8848107 1)PPT流程图:ppt下载:http://pan.baidu.com/s/1o7KIlom ...
- SSO单点登录系列2:cas客户端和cas服务端交互原理动画图解,cas协议终极分析
落雨 cas 单点登录 一.用户第一次访问web1应用. ps:上图少画了一条线,那一条线,应该再返回来一条,然后再到server端,画少了一步...谢谢提醒.而且,重定向肯定是从浏览器过去的.我写的 ...
- nextjs服务端渲染原理
1. 简单的介绍一下 nextjs是react进行服务端渲染的一个工具,默认以根目录下的pages为渲染路由 比如我在pages目录下创建一个index.js文件,然后export default一个 ...
- 携程开源分布式配置系统Apollo服务端是如何实时更新配置的?
引言 前面有写过一篇<分布式配置中心apollo是如何实时感知配置被修改>,也就是客户端client是如何知道配置被修改了,有不少读者私信我你既然说了client端是如何感知的,那服务端又 ...
随机推荐
- dedecmsV5.7 任意文件上传漏洞修复
问题文件:在/include/dialog/select_soft_post.php文件 解决方法: 先找到这个文件/include/dialog/select_soft_post.php,找到大概7 ...
- Hive 时间函数总结【转】
1.日期函数UNIX时间戳转日期函数: from_unixtime语法:from_unixtime(bigint unixtime[, stringformat]) 返回值: string说明: 转化 ...
- Linux中crontab定时任务
crontab安装(centOS) yum -y install vixie-cron crontab语法(计划任务) crontab [-u user] file crontab [-u user] ...
- 用Loading 加载中的整页加载来做蒙层
总结:遇见的bug 如何写一个蒙层 最初我打算的是自己写一个蒙层,但是写出来后, 不能够将整个屏幕全部覆盖.只能够覆盖 除[顶部导航] 和[左侧菜单栏] 于是我就使用了element-ui中的 [Lo ...
- [PHP]关于连接MySQL的问题
概述 PHP中无论使用MySQL函数抑或PDO连接MySQL服务器,都允许有两种方式,一是通过TCP网络层,一是通过unix socket: PHP并没有给出指明用何种方式去连接数据库,决定使用何种方 ...
- Codeforces Round #607 (Div. 1)
A. Cut and Paste 题解 在计算答案的时候,我们发现只需要知道这个字符串前\(l\) 个具体是啥就行了.所以对于每一组询问,我们暴力把这个字符串前\(l\) 的位都算出来,然后剩下的就推 ...
- 你真的会用搜索吗?—— google 搜索技巧
鄙人用了那么多年 google ,却只会简单的空格. 虽然空格已经很强大了.google 对此做了非常多的优化,原则是让你只用最基础的输入搭配空格就能达到跟下面介绍的方法几乎一样的效果,但是还有知道一 ...
- 在python操作数据库中游标的使用方法
cursor就是一个Cursor对象,这个cursor是一个实现了迭代器(def__iter__())和生成器(yield)的MySQLdb对象,这个时候cursor中还没有数据,只有等到fetcho ...
- Java生鲜电商平台-交易对账以及跟商家对账的思考
Java生鲜电商平台-交易对账以及跟商家对账的思考 说明:对于任何一家电商而言,资金的安全尤为重要,在资金管理过程中,涉及到交易订单的对账以及商家的对账,那i么如何来保证对账的高效与准确呢? 公司在搭 ...
- Java - java概述
简介: JAVA是一门面向对象的编程语言 1995有sun公司发布 java程序执行流程: xxxjava源文件, 经过编译器编译 产生字节码文件 字节码交给解释器 解释成当前平台的本地机器指令 名词 ...