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消息。

  1. @RestController

  2. public class NotificationControllerV2 implements ReleaseMessageListener {

  3. // 模拟配置更新,往里插入数据表示有更新

  4. public static Queue<String> queue = new LinkedBlockingDeque<>();

  5. @GetMapping("/addMsg")

  6. public String addMsg() {

  7. queue.add("xxx");

  8. return "success";

  9. }

  10. }

消息发送之后,前面我们有讲过Config Service会启动一个线程定时扫描ReleaseMessage表,去查看是否有新的消息记录,然后取通知客户端,这边我们也启动一个线程去扫描:

  1. @Component

  2. public class ReleaseMessageScanner implements InitializingBean {

  3. @Autowired

  4. private NotificationControllerV2 configController;

  5. @Override

  6. public void afterPropertiesSet() throws Exception {

  7. // 定时任务从数据库扫描有没有新的配置发布

  8. new Thread(() -> {

  9. for (;;) {

  10. String result = NotificationControllerV2.queue.poll();

  11. if (result != null) {

  12. ReleaseMessage message = new ReleaseMessage();

  13. message.setMessage(result);

  14. configController.handleMessage(message);

  15. }

  16. }

  17. }).start();;

  18. }

  19. }

循环去读取NotificationControllerV2中的队列,如果有消息的话就构造一个ReleaseMessage的对象,然后调用NotificationControllerV2中的handleMessage()方法进行消息的处理。

ReleaseMessage就一个字段,模拟消息内容:

  1. public class ReleaseMessage {

  2. private String message;

  3. public void setMessage(String message) {

  4. this.message = message;

  5. }

  6. public String getMessage() {

  7. return message;

  8. }

  9. }

接下来,我们看handleMessage做了什么样的工作

NotificationControllerV2实现了ReleaseMessageListener接口,ReleaseMessageListener中定义了handleMessage()方法。

  1. public interface ReleaseMessageListener {

  2. void handleMessage(ReleaseMessage message);

  3. }

handleMessage就是当配置发生变化的时候,通知的消息监听器,消息监听器得到配置发布的信息后,则会通知对应的客户端:

  1. @RestController

  2. public class NotificationControllerV2 implements ReleaseMessageListener {

  3. private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps

  4. .synchronizedSetMultimap(HashMultimap.create());

  5. @Override

  6. public void handleMessage(ReleaseMessage message) {

  7. System.err.println("handleMessage:"+ message);

  8. List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get("xxxx"));

  9. for (DeferredResultWrapper deferredResultWrapper : results) {

  10. List<ApolloConfigNotification> list = new ArrayList<>();

  11. list.add(new ApolloConfigNotification("application", 1));

  12. deferredResultWrapper.setResult(list);

  13. }

  14. }

  15. }

Apollo的实时推送是基于Spring DeferredResult实现的,在handleMessage()方法中可以看到是通过deferredResults获取DeferredResult,deferredResults就是第一行的Multimap,Key其实就是消息内容,Value就是DeferredResult的业务包装类DeferredResultWrapper,我们来看下DeferredResultWrapper的代码:

  1. public class DeferredResultWrapper {

  2. private static final long TIMEOUT = 60 * 1000;// 60 seconds

  3. private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST =

  4. new ResponseEntity<>(HttpStatus.NOT_MODIFIED);

  5. private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;

  6. public DeferredResultWrapper() {

  7. result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);

  8. }

  9. public void onTimeout(Runnable timeoutCallback) {

  10. result.onTimeout(timeoutCallback);

  11. }

  12. public void onCompletion(Runnable completionCallback) {

  13. result.onCompletion(completionCallback);

  14. }

  15. public void setResult(ApolloConfigNotification notification) {

  16. setResult(Lists.newArrayList(notification));

  17. }

  18. public void setResult(List<ApolloConfigNotification> notifications) {

  19. result.setResult(new ResponseEntity<>(notifications, HttpStatus.OK));

  20. }

  21. public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getResult() {

  22. return result;

  23. }

  24. }

通过setResult()方法设置返回结果给客户端,以上就是当配置发生变化,然后通过消息监听器通知客户端的原理,那么客户端是在什么时候接入的呢?

  1. @RestController

  2. public class NotificationControllerV2 implements ReleaseMessageListener {

  3. // 模拟配置更新,往里插入数据表示有更新

  4. public static Queue<String> queue = new LinkedBlockingDeque<>();

  5. private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps

  6. .synchronizedSetMultimap(HashMultimap.create());

  7. @GetMapping("/getConfig")

  8. public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getConfig() {

  9. DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper();

  10. List<ApolloConfigNotification> newNotifications = getApolloConfigNotifications();

  11. if (!CollectionUtils.isEmpty(newNotifications)) {

  12. deferredResultWrapper.setResult(newNotifications);

  13. } else {

  14. deferredResultWrapper.onTimeout(() -> {

  15. System.err.println("onTimeout");

  16. });

  17. deferredResultWrapper.onCompletion(() -> {

  18. System.err.println("onCompletion");

  19. });

  20. deferredResults.put("xxxx", deferredResultWrapper);

  21. }

  22. return deferredResultWrapper.getResult();

  23. }

  24. private List<ApolloConfigNotification> getApolloConfigNotifications() {

  25. List<ApolloConfigNotification> list = new ArrayList<>();

  26. String result = queue.poll();

  27. if (result != null) {

  28. list.add(new ApolloConfigNotification("application", 1));

  29. }

  30. return list;

  31. }

  32. }

NotificationControllerV2中提供了一个/getConfig的接口,客户端在启动的时候会调用这个接口,这个时候会执行getApolloConfigNotifications()方法去获取有没有配置的变更信息,如果有的话证明配置修改过,直接就通过deferredResultWrapper.setResult(newNotifications);返回结果给客户端了,客户端收到结果后重新拉取配置的信息进行覆盖本地的配置。

如果getApolloConfigNotifications()方法没有返回配置修改的信息,证明配置没有发生修改,就将DeferredResultWrapper对象添加到deferredResults中,等待后续配置发生变化时消息监听器进行通知。

同时这个请求就会挂起,不会立即返回,挂起是通过DeferredResultWrapper中的下面的代码实现的:

  1. private static final long TIMEOUT = 60 * 1000;// 60 seconds

  2. private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST =

  3. new ResponseEntity<>(HttpStatus.NOT_MODIFIED);

  4. private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;

  5. public DeferredResultWrapper() {

  6. result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);

  7. }

在创建DeferredResult对象的时候指定了超时的时间和超时后返回的响应码,如果60秒内没有消息监听器进行通知,那么这个请求就会超时,超时后客户端就收到的响应码就是304。

整个Config Service的流程就走完了,接下来我们看客户端是怎么实现的,我们简单的写个测试类模拟客户端注册:

  1. public class ClientTest {

  2. public static void main(String[] args) {

  3. reg();

  4. }

  5. private static void reg() {

  6. System.err.println("注册");

  7. String result = request("http://localhost:8081/getConfig");

  8. if (result != null) {

  9. // 配置有更新,重新拉取配置

  10. // ......

  11. }

  12. // 重新注册

  13. reg();

  14. }

  15. private static String request(String url) {

  16. HttpURLConnection connection = null;

  17. BufferedReader reader = null;

  18. try {

  19. URL getUrl = new URL(url);

  20. connection = (HttpURLConnection) getUrl.openConnection();

  21. connection.setReadTimeout(90000);

  22. connection.setConnectTimeout(3000);

  23. connection.setRequestMethod("GET");

  24. connection.setRequestProperty("Accept-Charset", "utf-8");

  25. connection.setRequestProperty("Content-Type", "application/json");

  26. connection.setRequestProperty("Charset", "UTF-8");

  27. System.out.println(connection.getResponseCode());

  28. if (200 == connection.getResponseCode()) {

  29. reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));

  30. StringBuilder result = new StringBuilder();

  31. String line = null;

  32. while ((line = reader.readLine()) != null) {

  33. result.append(line);

  34. }

  35. System.out.println("结果 " + result);

  36. return result.toString();

  37. }

  38. } catch (IOException e) {

  39. e.printStackTrace();

  40. } finally {

  41. if (connection != null) {

  42. connection.disconnect();

  43. }

  44. }

  45. return null;

  46. }

  47. }

首先启动/getConfig接口所在的服务,然后启动客户端,客户端就会发起注册请求,如果有修改直接获取到结果,进行配置的更新操作。如果无修改,请求会挂起,这边客户端设置的读取超时时间是90秒,大于服务端的60秒超时时间。

每次收到结果后,无论是有修改还是没修改,都必须重新进行注册,通过这样的方式就可以达到配置实时推送的效果。

我们可以调用之前写的/addMsg接口来模拟配置发生变化,调用之后客户端就能马上得到返回结果。

本文摘自于《Spring Cloud微服务 入门 实战与进阶》一书。

去年出版的《Spring Cloud微服务:全栈技术与案例解析》一书,得到了大家的支持以及反馈,基于大家的反馈,重新进行了更正和改进。

基于比较稳定的 Spring Cloud Finchley.SR2 版本和 Spring Boot 2.0.6.RELEASE 版本编写。

同时将示列代码进行标准的归档,之前的都在一起,不方便读者参考和运行。

同时还增加了像Apollo,Spring Cloud Gateway,生产实践经验等新的内容。

发布了250 篇原创文章 · 获赞 91 · 访问量 44万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览