-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 505 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 505 KB
1
{"meta":{"title":"Spring Cloud中国社区博客","subtitle":"Spring Cloud中国社区博客","description":"Spring Cloud中国社区官方博客,投稿请联系QQ:2508203324,邮箱:Software_King@qq.com。","author":"Spring Cloud中国社区","url":"http://blog.springcloud.cn"},"pages":[{"title":"404 找不到该页面!","date":"2017-06-17T02:42:04.000Z","updated":"2017-04-03T06:03:42.000Z","comments":false,"path":"/404.html","permalink":"http://blog.springcloud.cn//404.html","excerpt":"","text":"对不起,您要找的内容本站不存在,可以在本站听一会音乐,休息一下!无法访本页的原因是:你使用的URL可能拼写错误或者它只是临时脱机所访问的页面不存在或被管理员已删除 请尝试以下操作: 1、尝试按F5进行页面刷新 2、重新键入URL地址进入访问 3、或返回 网站首页"},{"title":"加入Spring Cloud中国社区","date":"2016-10-03T06:00:00.000Z","updated":"2017-06-17T04:10:24.000Z","comments":true,"path":"joinus/index.html","permalink":"http://blog.springcloud.cn/joinus/index.html","excerpt":"","text":"一.加入QQ群或微信群1.1 QQ群 Spring Cloud中国社区QQ群①:415028731 Spring cloud中国社区QQ群②:530321604 1.2 微信群加微信Software_King,或者扫二维码入群 二.捐赠社区发展2.1 捐赠社区 如果你觉得,Spring Cloud中国社区还可以,为了更好的发展,你可以捐赠社区,点击下面的打赏捐赠,捐赠的钱将用于社区发展和线下meeting up。"},{"title":"关于SpringCloud中国社区以及国内使用情况","date":"2016-10-03T06:00:00.000Z","updated":"2017-06-17T03:28:09.000Z","comments":true,"path":"about/index.html","permalink":"http://blog.springcloud.cn/about/index.html","excerpt":"Spring Cloud中国社区起源 其实当Spring Cloud项目刚在github上出现的时候,我就一直在关注其项目发展,到了2015年8月,由于个人兴趣研究Spring Cloud项目,由于国内相关文档较少,当时就想建立一个中国社区,于是就先把域名注册了,选中域名为springcloud.cn。 为什么要发起Spring Cloud中国社区 Spring Cloud发展到2016年,国内关注的人越来越多,但是相应学习交流的平台和材料比较分散,不利于学习交流,因此Spring Cloud中国社区应运而生。 Spring Cloud中国社区是国内首个Spring Cloud构建微服务架构的交流社区。我们致力于为Spring Boot或Spring Cloud技术人员提供分享和交流的平台,推动Spring Cloud在中国的普及和应用。 欢迎CTO、架构师、开发者等,在这里学习与交流使用Spring Cloud的实战经验。 目前QQ群人数:7000+,微信群:2000+. 扫描下面二维码或者微信搜索SpringCloud,关注社区公众号 Spring Cloud中国社区QQ群①:415028731 Spring cloud中国社区QQ群②:530321604 Spring Cloud中国社区官网:http://springcloud.cn Spring Cloud中国社区论坛:http://springcloud.cn Spring Cloud中国社区文档:http://docs.springcloud.cn spring cloud目前国内使用情况 中国联通子公司http://flp.baidu.com/feedland/video/?entry=box_searchbox_feed&id=144115189637730162&from=timeline&isappinstalled=0","text":"Spring Cloud中国社区起源 其实当Spring Cloud项目刚在github上出现的时候,我就一直在关注其项目发展,到了2015年8月,由于个人兴趣研究Spring Cloud项目,由于国内相关文档较少,当时就想建立一个中国社区,于是就先把域名注册了,选中域名为springcloud.cn。 为什么要发起Spring Cloud中国社区 Spring Cloud发展到2016年,国内关注的人越来越多,但是相应学习交流的平台和材料比较分散,不利于学习交流,因此Spring Cloud中国社区应运而生。 Spring Cloud中国社区是国内首个Spring Cloud构建微服务架构的交流社区。我们致力于为Spring Boot或Spring Cloud技术人员提供分享和交流的平台,推动Spring Cloud在中国的普及和应用。 欢迎CTO、架构师、开发者等,在这里学习与交流使用Spring Cloud的实战经验。 目前QQ群人数:7000+,微信群:2000+. 扫描下面二维码或者微信搜索SpringCloud,关注社区公众号 Spring Cloud中国社区QQ群①:415028731 Spring cloud中国社区QQ群②:530321604 Spring Cloud中国社区官网:http://springcloud.cn Spring Cloud中国社区论坛:http://springcloud.cn Spring Cloud中国社区文档:http://docs.springcloud.cn spring cloud目前国内使用情况 中国联通子公司http://flp.baidu.com/feedland/video/?entry=box_searchbox_feed&id=144115189637730162&from=timeline&isappinstalled=0 上海米么金服 指点无限(北京)科技有限公司 易保软件 目前在定制开发中 http://www.ebaotech.com/cn/ 广州简法网络 深圳睿云智合科技有限公司 持续交付产品基于Spring Cloud研发 http://www.wise2c.com 猪八戒网 上海云首科技有限公司 华为 整合netty进来用rpc 包括nerflix那套东西 需要注意的是sleuth traceid的传递需要自己写。tps在物理机上能突破20w 东软 南京云帐房网络科技有限公司 四众互联(北京)网络科技有限公司 深圳摩令技术科技有限公司 广州万表网 视觉中国 上海秦苍信息科技有限公司-买单侠 爱油科技(大连)有限公司爱油科技基于SpringCloud的微服务实践 广发银行 卖货郎(http://www.51mhl.com/) 拍拍贷 甘肃电信 新浪商品部 春秋航空 冰鉴科技 万达网络科技集团-共享商业平台-共享供应链中心 网易乐得技术团队 饿了么某技术团队 高阳捷迅信息科技–话费中心业务平台–凭证查询及收单系统数据在统计之中,会一直持续更新,敬请期待! 捐赠社区发展捐赠社区 如果你觉得,Spring Cloud中国社区还可以,为了更好的发展,你可以捐赠社区,点击下面的打赏捐赠,捐赠的钱将用于社区发展和线下meeting up。"},{"title":"标签","date":"2017-06-17T03:33:08.000Z","updated":"2017-06-17T03:33:08.000Z","comments":false,"path":"tags/index.html","permalink":"http://blog.springcloud.cn/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"Spring Cloud微服务升级总结","slug":"sc/sc-lx","date":"2017-11-24T09:00:00.000Z","updated":"2017-11-24T00:54:48.000Z","comments":true,"path":"sc/sc-lx/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-lx/","excerpt":"","text":"一.应用系统的架构历史 二.什么是微服务?2.1 微服务概述 起源:微服务的概念源于 2014 年 3 月 Martin Fowler 所写的一篇文章“Microservices”。文中内容提到:微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。 通信方式:每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通(通常是基于 HTTP 的 RESTful API)。 微服务的常规定义:微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务。 把原来的一个完整的进程服务,拆分成两个或两个以上的进程服务,且互相之间存在调用关系,与原先单一的进程服务相比,就是“微服务”。(微服务是一个比较级的概念,而不是单一的概念) 2.2 微服务架构的优势 可扩展性:在增加业务功能时,单一应用架构需要在原先架构的代码基础上做比较大的调整,而微服务架构只需要增加新的微服务节点,并调整与之有关联的微服务节点即可。在增加业务响应能力时,单一架构需要进行整体扩容,而微服务架构仅需要扩容响应能力不足的微服务节点。 容错性:在系统发生故障时,单一应用架构需要进行整个系统的修复,涉及到代码的变更和应用的启停,而微服务架构仅仅需要针对有问题的服务进行代码的变更和服务的启停。其他服务可通过重试、熔断等机制实现应用层面的容错。 技术选型灵活:微服务架构下,每个微服务节点可以根据完成需求功能的不同,自由选择最适合的技术栈,即使对单一的微服务节点进行重构,成本也非常低。 开发运维效率更高:每个微服务节点都是一个单一进程,都专注于单一功能,并通过定义良好的接口清晰表述服务边界。由于体积小、复杂度低,每个微服务可由一个小规模团队或者个人完全掌控,易于保持高可维护性和开发效率。 Spring Cloud作为今年最流行的微服务开发框架,不是采用了Spring Cloud框架就实现了微服务架构,具备了微服务架构的优势。正确的理解是使用Spring Cloud框架开发微服务架构的系统,使系统具备微服务架构的优势(Spring Cloud就像工具,还需要“做”的过程)。 三.Spring Boot/Cloud3.1 什么是Spring Boot? Spring Boot框架是由Pivotal团队提供的全新框架,其设计目的是用来简化基于Spring应用的初始搭建以及开发过程。SpringBoot框架使用了特定的方式来进行应用系统的配置,从而使开发人 员不再需要耗费大量精力去定义模板化的配置文件。 3.2 什么是Spring Cloud? Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中的配置管理、服务注册,服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。 3.3 微服务,Spring Boot,Spring Cloud三者之间的关系 思想:微服务是一种架构的理念,提出了微服务的设计原则,从理论为具体的技术落地提供了指导思想。 脚手架:Spring Boot是一套快速配置脚手架,可以基于Spring Boot快速开发单个微服务。 多个组件的集合:Spring Cloud是一个基于Spring Boot实现的服务治理工具包;Spring Boot专注于快速、方便集成的单个微服务个体;Spring Cloud关注全局的服务治理框架。 3.4 Everything is jar, Everything is http Spring Boot通过@SpringBootApplication注解标识为Spring Boot应用程序。所有的应用都通过jar包方式编译,部署和运行. 12345678@SpringBootApplicationpublic class Application { private static final Logger LOGGER = LoggerFactory.getLogger(Application.class); public static void main(String[] args) { SpringApplication.run(Application.class, args); LOGGER.info(”启动成功!\"); } } 每个Spring Boot的应用都可以通过内嵌web容器的方式提供http服务,仅仅需要在pom文件中依赖spring-boot-start-web即可,原则上微服务架构希望每个独立节点都提供http服务。 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency> 3.5 Spring boot Task 任务启动和定时任务在Spring Boot需要启动任务时,只要继承CommandLineRunner接口实现其run方法即可。 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency> 在Spring Boot需要执行定时任务时,只需要在定时任务方法上增加@Scheduled(cron = “0 15 0 ?”)注解(支持标准cron表达式),并且在服务启动类上增加@EnableScheduling的注解即可。 12345678910111213141516@SpringBootApplication@EnableSchedulingpublic class Application { private static final Logger LOGGER = LoggerFactory.getLogger(Application.class); public static void main(String[] args) { SpringApplication.run(Application.class, args); LOGGER.info(”启动成功!\"); }}// some class@Scheduled(cron = \"0 15 0 * * ?\")public void someTimeTask() { //}} 3.6 Spring boot Actuator 监控Actuator是spring boot提供的对应用系统自身进行监控的组件,在引入spring-boot-start-web基础上引入spring-boot-starter-actuator即可。 1234<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> 3.7 Spring cloud Config 配置中心 在我们实现微服务架构时,每个微服务节点都需要自身的相关配置数据项,当节点众多,维护就变得非常困难,因此需要建立一个中心配置服务。 Spring Cloud Config分为两部分。Spring Cloud Config server作为一个服务进程,Spring Cloud Config File为配置文件存放位置。 3.8 Spring cloud Eureka 服务注册中心 服务注册的概念早在微服务架构之前就出现了,微服务架构更是把原先的单一应用节点拆分成非常多的微服务节点。互相之间的调用关系会非常复杂,Spring Cloud Eureka作为注册中心, 所有的微服务都可以将自身注册到Spring Cloud Eureka进行统一的管理和访问(Eureka和Zookeeper不同,在AOP原则中选择了OP,更强调服务的有效性) 3.9 Spring cloud Zuul 服务端智能路由 当我们把所有的服务都注册到Eureka(服务注册中心)以后,就涉及到如何调用的问题。Spring Cloud Zuul是Spring Cloud提供的服务端代理组件,可以看做是网关,Zuul通过Eureka获取到可用的服务,通过映射配置,客户端通过访问Zuul来访问实际需要需要访问的服务。所有的服务通spring.application.name做标识,不同IP地址,相同spring.application.name就是一个服务集群。当我们增加一个相同spring.application.name的节点,Zuul通过和Eureka通信获取新增节点的信息实现智能路由,增加该类型服务的响应能力。 3.10 Spring cloud Ribbon 客户端智能路由与Spring Cloud Zuul的服务端代理相对应,Spring Cloud Ribbon提供了客户端代理。在服务端代理中,客户端并不需要知道最终是哪个微服务节点为之提供服务,而客户端代理获取实质提供服务的节点,并选择一个进行服务调用。Ribbon和Zuul相似,也是通过和Eureka(服务注册中心)进行通信来实现客户端智能路由。 3.11 Spring cloud Sleuth 分布式追踪 2.12 Spring cloud Zipkin 调用链 3.13 Spring cloud Feign http客户端Spring Cloud Feign是一种声明式、模板化的http客户端。 使用Spring Cloud Feign请求远程服务时能够像调用本地方法一样,让开发者感觉不到这是远程方法(Feign集成了Ribbon做负载均衡)。 1.把远程服务和本地服务做映射 12345@FeignClient(name = \"rabbitmq-http\", url = \"${SKYTRAIN_RABBITMQ_HTTP}\") public interface TaskService { @RequestMapping(value = \"/api/queues\", method = RequestMethod.GET) public String query(@RequestHeader(\"Authorization\") String token); } 以调用本地服务的方式调用远程服务 1234567@Autowired private TaskService taskService; private String queryRabbitmqStringInfo() { byte[] credentials = Base64 .encodeBase64((rabbitmqHttpUserName + \":\" + rabbitmqHttpPassword).getBytes(StandardCharsets.UTF_8)); String token = \"Basic \" + new String(credentials, StandardCharsets.UTF_8); return taskService.query(token); } 3.13 Spring cloud Hystrix 断路器 四 自研组件4.1 我们开发的几个微服务组件—应用管理中心应用管理中心可以对每个已经注册的微服务节点进行停止,编译,打包,部署,启动的完整的上线操作。 4.2 我们开发的几个微服务组件—zookeeper数据查询中心zookeeper数据查询中心根据zookeeper地址,端口,命令获取zookeeper数据信息。 4.3 我们开发的几个微服务组件—微服务健康检测中心健康检测中心周期性检查每个微服务的状态,当发现有微服务状态处于DOWN或连接超时时,触发报警 4.4 我们开发的几个微服务组件—定时任务查询中心123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566// 在BeanPostProcessor子类中拦截 @Component public class SkytrainBeanPostProcessor implements BeanPostProcessor, Ordered { *** /** * Bean 实例化之后进行的处理 */ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { beanPostProcessor.postProcessAfter(bean, beanName); return bean; } } // 拦截后获取定时任务注解 public Object postProcessAfter(Object bean, String beanName) { Class<?> targetClass = AopUtils.getTargetClass(bean); Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass, new MethodIntrospector.MetadataLookup<Set<Scheduled>>() { public Set<Scheduled> inspect(Method method) { Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, Scheduled.class, Schedules.class); return (!scheduledMethods.isEmpty() ? scheduledMethods : null); } }); if (!annotatedMethods.isEmpty()) { String className = targetClass.getName(); for (Map.Entry<Method, Set<Scheduled>> entry : annotatedMethods.entrySet()) { Method method = entry.getKey(); for (Scheduled scheduled : entry.getValue()) { String key = className + \":\" + method.getName(); String value = scheduled.toString(); taskInfos.put(key, value); } } } return null; } // 获取定时任务后注册 public void taskRegister() { String nodeInfo = ipAddress + \":\" + serverPort + \":\"; try { /** * 定时任务 */ Map<String, String> infos = taskInfos; for (Entry<String, String> item : infos.entrySet()) { String taskId = nodeInfo + item.getKey(); String taskParameter = item.getValue(); JSONObject info = new JSONObject(); info.put(\"taskId\", taskId); info.put(\"taskParameter\", taskParameter); info.put(\"applicationName\", applicationName); info.put(\"taskType\", \"schedule\"); LOGGER.info(info.toString()); zooKeeperExecutor.createZKNode(SKYTRAIN_TASK_ZKNODE_PREFIX + taskId, info.toString()); } } catch (Exception ex) { LOGGER.error(\"\", ex); } } 4.5 微服务的分类 微服务平台组件 公共服务组件 基础服务组件/业务服务组件 4.6 整体微服务架构图 作者:宜信-技术研发中心-高级架构师-梁鑫","categories":[{"name":"微服务","slug":"微服务","permalink":"http://blog.springcloud.cn/categories/微服务/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"spring boot / cloud 分布式ID生成服务","slug":"sc/wk5","date":"2017-09-17T06:00:00.000Z","updated":"2017-09-17T13:49:41.000Z","comments":true,"path":"sc/wk5/","link":"","permalink":"http://blog.springcloud.cn/sc/wk5/","excerpt":"","text":"在几乎所有的分布式系统或者采用了分库/分表设计的系统中,几乎都会需要生成数据的唯一标识ID的需求, 常规做法,是使用数据库中的自动增长列来做系统主键,但是这样的做法无法保证ID全局唯一. 那么一个分布式ID生成器应该满足那些需求呢 : 全局唯一性 趋势递增 能够融入分库基因 本文将基于snowflake的算法来进行以下的讨论,当然,分布式ID的生成方案有很多, 不过在本文并不会分散开来讨论/比对,因为网上相关的文章实在太多,如果有需要了解的同学,请自行百度. 同时,也不会讨论snowflake算法,同样也是因为网上相关的文章实在太多,如果有需要了解的同学,请自行百度. 本文期望解决什么问题?先看两段代码: 12345678910public void id() { Map<Long, Long> map = new HashMap<>(); int maxCount = 100; IdWorker idWorker = new IdWorker(1, 1); for (int i = 0; i < maxCount; i++) { long id = idWorker.nextId(); map.put(id, id); } log.info(\"{} , {}\", maxCount, map.size()); } 输出为 : 100 , 100 12345678910public void id() { Map<Long, Long> map = new HashMap<>(); int maxCount = 100; for (int i = 0; i < maxCount; i++) { IdWorker idWorker = new IdWorker(1, 1); long id = idWorker.nextId(); map.put(id, id); } log.info(\"{} , {}\", maxCount, map.size()); } 输出为 : 100 , 10 这两段代码的区别,相信大家一眼就能看出,但是那为什么会出现这样的情况呢?了解snowflake的同学也都知道,这个算法是基于时间的,如下组成 : 0 | 时间(41位) | 数据中心ID(5位) | 机器ID(5位) | 序号(12位) 而生成ID的算法逻辑,简单点说,在相同数据中心ID和机器ID的情况下,如果时间的毫秒数是一致的,那么就通过递增序列号来保证ID不重复. 也就是说在1毫秒内最大生成的ID个数是二进制12bit的最大值,也就是4096(0-4095)个 那么如果序列号超过了这个最大值,则会将程序阻塞到下一毫秒,然后序列号归零,继续生成ID. 好知道了生成ID的逻辑后,上面两个程序判断的现象也就不难解释了. 程序一 : 没有重复,是因为在整个循环中,ID生成器只实例化过一次,在循环的过程中,能正常的递增序列号,所以不会有重复的ID出现 程序二 : 有重复,是因为ID生成器是在循环中循环实例化的,每次生成ID的时候序列号都是0,但是程序执行很快,得到的时间毫秒数又是一样的,那么,就必然会有重复值了. 所以从以上的程序片段和分析中可以得出一个结论 : 要想snowflake生成全局唯一的ID,那么ID生成器必须也是全局单例的 那申明一个全局静态的ID生成器不就行了?两个点要主注意一下 : 分布式系统下全局静态变量也是多份的,因为系统可能运行在不同的JVM下,并不能保证变量的全局单例 前面提到了在同一毫秒下,最多只能生成4096个ID,对于那些并发量很大是系统来说,显然是不够的,那么这个时候就是通过datacenterId和workerId来做区分,这两个ID,分别是5bit,共10bit,最大值是1024(0-1023)个,在这种情况下,snowflake一毫秒理论上最大能够生成的ID数量是约42W个,这是一个非常大的基数了,理论上能够满足绝大多数系统的并发量 所以得出一个结论 : snowflake可以通过datacenterId和workerId来区分ID的归属(可以是业务线,可以是机房,等等,按需定义)来达到更大的ID生成数量 那么有那些方法来分配atacenterId和workerId呢? 写死 : 正如上面说的一样,单机部署,然后写死两个值 读配置文件 : 将值放在配置中心,应用启动的时候读取,然后初始化 动态分配 : 本文主旨 所以本文主要讨论的是如何动态分配snowflake的datacenterId和workerId,以及如何做到高可用所以大家先看一下架构图 : 分布式ID-逻辑架构示意 分布式ID-发号流程示意 相关源码可在本文末尾的配套代码仓库中获得,工程是 : udf-starter-id架构设计构建独立的ID生成服务,提供如下服务:1234567891011121314151617#生成分布式ID(按时间戳区分datacenterId和workerId)/service/id#生成分布式ID(按dwId[0-1023])/service/id/{dwId}#生成分布式ID(按datacenterId[0-31]和workerId[0-31])/service/id/{datacenterId}/{workerId}#批量生成分布式ID(按时间戳区分datacenterId和workerId)/service/id/batch/{count}#批量生成分布式ID(按dwId[0-1023])/service/id/batch/{dwId}/{count}#批量生成分布式ID(按datacenterId[0-31]和workerId[0-31])/service/id/batch/{datacenterId}/{workerId}/{count} 融入分库基因在提供出来的rest服务中,提供了datacenterId和workerId的参数(dwId就是两者的融合,10bit), 总共预留了10个bit的空余来支持分库分表,最大支持1024个节点. 反解析分布式IDsnowflake生成的ID是可以被反解析的,这样更进一步的支持了分库的相关炒作,相关实现如下 : 1234567 Id reverseId = new Id();reverseId.setSequence((id) & ~(-1L << 12)); // sequencereverseId.setDwId((id >> (12)) & ~(-1L << (10))); // dwIdreverseId.setWorkerId((id >> 12) & ~(-1L << 5)); // workerIdreverseId.setDatacenterId((id >> 17) & ~(-1L << 5)); // datacenterIdreverseId.setTimestamp((id >> 22) + TWEPOCH); // timestampreturn reverseId; 集群部署 和 懒实例化ID生成器本方案是可以支持ID生成服务有多个实例,最多1024个,能并且能保证每个实例内,相同datacenterId和workerId的ID生成器只有一个,做到全局单例. 主要是通过redis原子锁的来实现的.详情可看上面的流程图,主要分为本地ID生成和跨实例ID生成两种模式 : 本地生成这种情况比较简单,就是生成ID的请求刚刚落到ID生成器所在的实例上,然后就可以直接拿到ID生成器,然后生成ID. 跨实例ID生成这种情况简单点说就是,比如你要生成3-3的ID,这个ID生成器在实例A上,但是负载均衡器将请求发到实例B上去了, 这个时候实例B上并没有对应的ID生成器,这个时候,就会从缓存中拿到对应的缓存值,拿到用用这个ID生成器的HOST和PORT, 然后在做一个RMS请求,调用远程的rest服务,生成ID,然后返回 高可用 和 故障转移上面提到了,ID生成器现在是全网单例的了,那么其中一个节点有故障,挂掉了怎么办呢? 在跨实例ID生成的场景下,会有RMS请求失败的情况,远程节点有可能会故障,这个时候,一旦RMS请求失败,则会触发故障转移, 具体操作就是将redis中的对应缓存删除掉,然后走一个实例化ID生成器的流程,这个时候,当前处理请求的节点就会将故障节点拥有的ID生成器转移过来,转为本地生成模式,从而做到的故障转移 性能如果是本地ID生成的话,那基本没有性能损耗,直接操作本地变量. 跨实例ID生成的情况会多出来一个RMS请求的耗时,但是一次ID生成的请求最多触发一次RMS请求,消耗是可控的 在有节点故障的时候,触发故障转移会额外的产生一次ID实例化的流程,会造成轻微波动,但紧当前的这一次请求,下次的请求就会转为本地ID生成的模式 结束今天跟大家分享了如何动态分配snowflake的datacenterId和workerId,以及如何做到高可用的设计和思路,环境大家提出意见和建议 代码仓库 (博客配套代码) udf-starter : 基础项目,脚手架,框架 udf-sample : 集成样例 想获得最快更新,请关注公众号","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"spring boot / cloud分布式调度中心进阶","slug":"sc/wk4","date":"2017-09-16T06:00:00.000Z","updated":"2017-09-17T13:47:57.000Z","comments":true,"path":"sc/wk4/","link":"","permalink":"http://blog.springcloud.cn/sc/wk4/","excerpt":"","text":"在这篇文章中介绍了如何在spring boot项目中集成quartz. 今天这篇文章则会进一步跟大家讨论一下设计和搭建分布式调度中心所需要关注的事情. 下面先看一下,总体的逻辑架构图: 分布式调度-逻辑架构示意 架构设计总体思路是,将调度和执行两个概念分离开来,形成调度中心和执行节点两个模块: 调度中心是一个公共的平台,负责所有任务的调度,以及任务的管理,不涉及任何业务逻辑,从上图可以看到,它主要包括如下模块: 核心调度器quartz : 调度中心的核心,按照jobDetail和trigger的设定发起作业调度,并且提供底层的管理api 管理功能 : 可通过restful和web页面的方式动态的管理作业,触发器的CURD操作,并且实时生效,而且还可以记录调度日志,以及可以以图表,表格,等各种可视化的方式展现调度中心的各个维度的指标信息 RmsJob和RmsJobDisallowConcurrent : 基于http远程调用(RMS)的作业和禁止并发执行的作业 Callback : 用于接收”执行节点”异步执行完成后的信息 执行节点是嵌入在各个微服务中的一个执行模块,负责接收调度中心的调度,专注于执行业务逻辑,无需关系调度细节,并且理论上来说,它主要包括如下模块: 同步执行器 : 同步执行并且返回调度中心触发的任务 异步执行器 : 异步执行调度中心触发的任务,并且通过callback将执行结果反馈给调度中心 作业链 : 可任意组合不同任务的执行顺序和依赖关系,满足更复杂的业务需求 业务bean : 业务逻辑的载体 架构优点这样一来,调度中心只负责调度,执行节点只负责业务,相互通过http协议进行沟通,两部分可以完全解耦合,增强系统整体的扩展性 并且引入了异步执行器的概念,这一样一来,调度中心就能以非阻塞的形式触发执行器,可以不受任务业务逻辑带来的性能影响,进一步提高了系统的性能 然后理论上来说执行节点是不局限于任何的语言或者平台的,并且与调度中心采用的是通用的http协议,真正的可以做到跨平台 特点集群,高可用,故障转移整体的解决方案是建立在spring cloud基础上的,依赖于服务发现eureka,可使所有的服务去中心化,来实现集群和高可用 调度中心的核心依赖于quartz,而quartz是原生支持集群的,它通过将作业和触发器的细节持久化到数据库中,然后在通过db锁的方式,与集群中的各个节点通讯,从而实现了去中心化 而执行节点和调度中心都是注册在eureka上的,通过ribbon的客户端负载均衡的特性,自动屏蔽坏掉的节点,自动发现新增加的节点,可使双方的http通信都做到高可用. 如下是quartz集群配置的片段: 12345678910111213#Configure schedulerorg.quartz.scheduler.instanceName=clusterQuartzScheduler #实例名称org.quartz.scheduler.instanceId=AUTO #自动设定实例IDorg.quartz.scheduler.skipUpdateCheck=true#Configure JobStore and Clusterorg.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX #使用jdbc持久化到数据中org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate #sql代理,mysqlorg.quartz.jobStore.useProperties=trueorg.quartz.jobStore.tablePrefix=QRTZ_ #表前缀org.quartz.jobStore.isClustered=true #开启集群模式org.quartz.jobStore.clusterCheckinInterval=20000org.quartz.jobStore.misfireThreshold=60000 线程池调优quartz的默认配置,可根据实际情况进行调整. 1234#Configure ThreadPoolorg.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool #线程池类型org.quartz.threadPool.threadCount=5 #线程池数量org.quartz.threadPool.threadPriority=5 #优先级 这里就体现出了分离调度的业务逻辑的好处,在传统的做法中,调度器承载着业务逻辑,必然会占用执行线程更长时间,并发能力受业务逻辑限制. 将业务逻辑分离出去后,并且采用异步任务的方式,调度器触发某个任务后,将立即返回,这时占用执行线程的时间会大幅缩短. 所以在相同的线程池数量下,采用这种架构,是可以大幅度的提高调度中心的并发能力的. 集中化配置管理同样,整个解决方案也依赖于spring cloud config server. 我们在系统中抽象出了一系列的元数据用于做系统配置,这些元数据在org.itkk.udf.scheduler.meta包下,大家可以查看,这些元数据基本囊括了所有作业和触发器的属性,通过@ConfigurationProperties特性,可轻松的将这些元数据类转化为配置文件. 并且设计上简化了后续管理api的复杂度,我们某个作业或者某个触发器的一套属性归纳到一个CODE中,然后后续通过这个CODE就能操作所对应的作业或者触发器. 配置片段如下: 123456789101112131415161718192021222324252627282930313233#jobGrouporg.itkk.scheduler.properties.jobGroup.general=通用#triggerGrouporg.itkk.scheduler.properties.triggerGroup.general=通用#rmsJoborg.itkk.scheduler.properties.jobDetail.rmsJob.name=generalJoborg.itkk.scheduler.properties.jobDetail.rmsJob.group=generalorg.itkk.scheduler.properties.jobDetail.rmsJob.className=org.itkk.udf.scheduler.job.RmsJoborg.itkk.scheduler.properties.jobDetail.rmsJob.description=通用作业org.itkk.scheduler.properties.jobDetail.rmsJob.recovery=falseorg.itkk.scheduler.properties.jobDetail.rmsJob.durability=trueorg.itkk.scheduler.properties.jobDetail.rmsJob.autoInit=true#rmsJobDisallowConcurrentorg.itkk.scheduler.properties.jobDetail.rmsJobDisallowConcurrent.name=generalJobDisallowConcurrentorg.itkk.scheduler.properties.jobDetail.rmsJobDisallowConcurrent.group=generalorg.itkk.scheduler.properties.jobDetail.rmsJobDisallowConcurrent.className=org.itkk.udf.scheduler.job.RmsJobDisallowConcurrentorg.itkk.scheduler.properties.jobDetail.rmsJobDisallowConcurrent.description=通用作业(禁止并发)org.itkk.scheduler.properties.jobDetail.rmsJobDisallowConcurrent.recovery=falseorg.itkk.scheduler.properties.jobDetail.rmsJobDisallowConcurrent.durability=trueorg.itkk.scheduler.properties.jobDetail.rmsJobDisallowConcurrent.autoInit=true#simpleTriggerorg.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.jobCode=rmsJoborg.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.name=testSimpleTriggerorg.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.group=generalorg.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.intervalInMilliseconds=10000org.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.autoInit=trueorg.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.description=测试简单触发器org.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.dataMap.serviceCode=SCH_CLIENT_UDF_SERVICE_A_DEMOorg.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.dataMap.beanName=testBeanorg.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.dataMap.async=trueorg.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.dataMap.param1=aorg.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.dataMap.param2=borg.itkk.scheduler.properties.simpleTrigger.testSimpleTrigger.dataMap.param3=123 以上可以看,我们可以通过properties配置文件设定作业和触发器的任何属性,并且通过如:simpleTrigger这个code,就能随意的通过管理api进行curd操作. 基于rms的JobDetail从上面的配置可以看到,解决方案中内置了两个默认的jobDetail,一个是rmsJob另一个是rmsJobDisallowConcurrent. 想要使用它们很简单,为它们配置一个触发器即可,rmsjob通过以下属性来确定自己将要调用那个任务: 123456789101112#配置simple或者corn触发器的dataMap属性,并且添加如下值:#指定要调用那个rms,这里设定的是rmscode,不太清楚的话可以回看第八篇文章省略.serviceCode=SCH_CLIENT_UDF_SERVICE_A_DEMO #指定要调用哪一个bean省略.beanName=testBean #是否采用异步方式省略.async=true #业务参数省略.param1=a 省略.param2=b省略.param3=123 如下方式可以在执行节点中定义一个执行器 1234567891011@Component(\"testBean\")public class TestSch extends AbstractExecutor { @Override public void handle(String id, Map<String, Object> jobDataMap) { try { LOGGER.info(\"任务执行了------id:{}, jobDataMap:{}\", id, jobDataMap); } catch (JsonProcessingException e) { throw new SchException(e); } }} 这样就能为某一个执行器设定触发器,从而做到调度的功能. 而rmsJob是可以并发的触发执行器的. 禁止并发的基于rms的JobDetail在这个解决方案中禁止并发有两个层次 第一个层次就是默认实现的rmsJobDisallowConcurrent,大家看源码就知道,这个类上标注了@DisallowConcurrentExecution,这个注解的含义是禁止作业并发执行. 在传统的做法中jobdetail中包含了业务逻辑,没有异步的远程操作,所以说在类上标注这个注解能做到禁止并发. 但是现在有了异步任务的概念,触发器触发执行器后立即就返回结束了,如果这个时候,触发器的触发间隔小于执行器的执行时间,那么依然还是会有任务并发执行的. 这显然是不希望发生的,既然禁止并发,那么就一定要完全的做到禁止并发,如下设定保证了这一点: 12345678910protected void disallowConcurrentExecute(RmsJobParam rmsJobParam) throws JobExecutionException { if (!this.hasRunning(rmsJobParam)) { //没有正在运行的任务才能运行 this.execute(rmsJobParam); } else { //跳过执行,并且记录 RmsJobResult result = new RmsJobResult(); result.setId(rmsJobParam.getId()); result.setStats(RmsJobStats.SKIP.value()); save(rmsJobParam, result); }} 在禁止并发的异步任务触发前,会校验当前这个任务是否正在执行,如果正在执行的话,跳过并且记录. 异步任务,异步回调执行节点中的任务即可同步执行也可异步执行,通过配置触发器的async属性来控制的, 同步执行 : 的任务适合执行时间短,执行时间稳定,并且有必要立即知道返回结果的任务 异步执行 : 高并发,高性能的执行方式,没有特别的限制,推荐使用 如下实现片段: 123456789101112131415161718192021//SchClientController中public RestResponse<RmsJobResult> execute(@RequestBody RmsJobParam param) { //记录来接收时间 Date receiveTime = new Date(); //定义返回值 RmsJobResult result = new RmsJobResult(); result.setClientReceiveTime(receiveTime); result.setId(param.getId()); result.setClientStartExecuteTime(new Date()); //执行(区分同步跟异步) if (param.getAsync()) { schClientHandle.asyncHandle(param, result); result.setStats(RmsJobStats.EXECUTING.value()); } else { schClientHandle.handle(param); result.setClientEndExecuteTime(new Date()); result.setStats(RmsJobStats.COMPLETE.value()); } //返回 return new RestResponse<>(result);} 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647//SchClientHandle中//异步执行@Asyncpublic void asyncHandle(RmsJobParam param, RmsJobResult result) { try { //执行 this.handle(param); result.setClientEndExecuteTime(new Date()); result.setStats(RmsJobStats.COMPLETE.value()); //回调 this.callback(result); } catch (Exception e) { result.setClientEndExecuteTime(new Date()); result.setStats(RmsJobStats.ERROR.value()); result.setErrorMsg(ExceptionUtils.getStackTrace(e)); //回调 this.callback(result); //抛出异常 log.error(\"asyncHandle error:\", e); throw new SchException(e); }}//同步执行public void handle(RmsJobParam param) { //判断bean是否存在 if (!applicationContext.containsBean(param.getBeanName())) { throw new SchException(param.getBeanName() + \" not definition\"); } //获得bean AbstractExecutor bean = applicationContext.getBean(param.getBeanName(), AbstractExecutor.class); //执行 bean.handle(param);}//异步回调(重处理)@Retryable(maxAttempts = 3, value = Exception.class)private void callback(RmsJobResult result) { log.info(\"try to callback\"); final String serviceCode = \"SCH_CLIENT_CALLBACK_1\"; rms.call(serviceCode, result, null, new ParameterizedTypeReference<RestResponse<String>>() { }, null);}//回调失败后的处理@Recoverpublic void recover(Exception e) { log.error(\"try to callback failed:\", e);} 任务链在执行器父类中提供如下方法,可在执行节点触发其他执行器: 123//调用链 (允许并发,异步调用)protected String chain(boolean isConcurrent, String parentId, String serviceCode, String beanName, boolean async, Map<String, String> param) 而在执行器中的使用样例: 123456789101112131415161718@Component(\"testBean\")public class TestSch extends AbstractExecutor { @Override public void handle(String id, Map<String, Object> jobDataMap) { try { LOGGER.info(\"任务执行了------id:{}, jobDataMap:{}\", id, xssObjectMapper.writeValueAsString(jobDataMap)); //NOSONAR if (!jobDataMap.containsKey(TriggerDataMapKey.PARENT_TRIGGER_ID.value())) { LOGGER.info(\"job链---->\"); //NOSONAR Map<String, String> param = new HashMap<>(); param.put(\"chain1\", \"1\"); param.put(\"chain2\", \"2\"); this.chain(id, \"SCH_CLIENT_UDF_SERVICE_A_DEMO\", \"testBean\", param); } } catch (JsonProcessingException e) { throw new SchException(e); } }} 这样可以使得执行器更加灵活,可以随意组合 管理api依赖于quartz的底层管理api,我们可以抽象出一系列restFul的api,目前实现的功能如下: 作业管理 : 保存作业 , 保存作业(覆盖) , 移除作业 , 立即触发作业 触发器管理 : 保存简单触发器 , 保存简单触发器(覆盖) , 保存CRON触发器 , 保存CRON触发器(覆盖) , 删除触发器 计划任务管理 : 清理数据 misfire设定quartz原生的设定,表示那些错过了触发时间的触发器,后续处理的规则,可能是因为 : 服务不可用 , 线程阻塞,线程池耗尽 , 等.. simple触发器MISFIRE_INSTRUCTION_FIRE_NOW 以当前时间为触发频率立即触发执行 MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT 不触发立即执行等待下次触发频率周期时刻执行以总次数-已执行次数作为剩余周期次数,重新计算FinalTime调整后的FinalTime会略大于根据starttime计算的到的FinalTime值 MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT 不触发立即执行等待下次触发频率周期时刻,执行至FinalTime的剩余周期次数保持FinalTime不变,重新计算剩余周期次数(相当于错过的当做已执行) MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT 以当前时间为触发频率立即触发执行以总次数-已执行次数作为剩余周期次数,重新计算FinalTime调整后的FinalTime会略大于根据starttime计算的到的FinalTime值 MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT 以当前时间为触发频率立即触发执行保持FinalTime不变,重新计算剩余周期次数(相当于错过的当做已执行) MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY 以错过的第一个频率时间立刻开始执行 MISFIRE_INSTRUCTION_SMART_POLICY(默认) 智能根据trigger属性选择策略:repeatCount为0,则策略同MISFIRE_INSTRUCTION_FIRE_NOWrepeatCount为REPEAT_INDEFINITELY,则策略同MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT否则策略同MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT cron触发器MISFIRE_INSTRUCTION_DO_NOTHING 是什么都不做,继续等下一次预定时间再触发 MISFIRE_INSTRUCTION_FIRE_ONCE_NOW 是立即触发一次,触发后恢复正常的频率 MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY 以错过的第一个频率时间立刻开始执行 MISFIRE_INSTRUCTION_SMART_POLICY(默认) 根据创建CronTrigger时选择的MISFIRE_INSTRUCTION_XXX更新CronTrigger的状态。如果misfire指令设置为MISFIRE_INSTRUCTION_SMART_POLICY,则将使用以下方案:指令将解释为MISFIRE_INSTRUCTION_FIRE_ONCE_NOW 大家可根据自身情况进行设定 结束今天跟大家分享了分布式调度的设计思路和想法,由于个人时间问题,这个设计的核心部分虽然已经完成,但是比如web界面,restful api,都还没有完成,后续有空就会把这些东西都弄上去的. 不过总体来说,把核心的思想讲出来了,也欢迎大家提出意见和建议 代码仓库 (博客配套代码) udf-starter : 基础项目,脚手架,框架 udf-sample : 集成样例 想获得最快更新,请关注公众号","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"Spring Cloud在国内中小型公司能用起来吗?","slug":"sc/sc-why","date":"2017-09-16T06:00:00.000Z","updated":"2017-09-16T03:06:47.000Z","comments":true,"path":"sc/sc-why/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-why/","excerpt":"","text":"好问题好问题必须配认真的回答,仔细的看了题主的问题,发现这个问题非常具有代表性,可能是广大网友想使用Spring Cloud却又对Spring Cloud不太了解的共同想法,题主对Spring Cloud使用的方方面面都进行过了思考,包括市场,学习、前后端、测试、配置、部署、开发以及运维,下面就是题主原本的问题: 想在公司推广Spring Cloud,但我对这项技术还缺乏了解,画了一张脑图,总结了种种问题。 微服务是这样一个结构吗? 1前端或二方 - > ng集群 -> zuul集群 -> eureka-server集群 -> service provider集群 (二方指其他业务部门) 想要明白这个问题,首先需要知道什么是Spring Boot,什么是Spring Cloud,以及两者之间有什么关系? 什么是Spring BootSpring Boot简化了基于Spring的应用开发,通过少量的代码就能创建一个独立的、产品级别的Spring应用。 Spring Boot为Spring平台及第三方库提供开箱即用的设置,这样你就可以有条不紊地开始。多数Spring Boot应用只需要很少的Spring配置。 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。用我的话来理解,就是Spring Boot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,Spring Boot整合了所有的框架(不知道这样比喻是否合适)。 Spring Boot的核心思想就是约定大于配置,一切自动完成。采用Spring Boot可以大大的简化你的开发模式,所有你想集成的常用框架,它都有对应的组件支持。如果你对Spring Boot完全不了解,可以参考我的这篇文章:Springboot(一):入门篇-%E5%85%A5%E9%97%A8%E7%AF%87.html) 什么是Spring CloudSpring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。 微服务是可以独立部署、水平扩展、独立访问(或者有独立的数据库)的服务单元,Spring Cloud就是这些微服务的大管家,采用了微服务这种架构之后,项目的数量会非常多,Spring Cloud做为大管家就需要提供各种方案来维护整个生态。 Spring Cloud就是一套分布式服务治理的框架,既然它是一套服务治理的框架,那么它本身不会提供具体功能性的操作,更专注于服务之间的通讯、熔断、监控等。因此就需要很多的组件来支持一套功能,如果你对Spring Cloud组件不是特别了解的话,可以参考我的这篇文章:springcloud(一):大话Spring Cloud Spring Boot和Spring Cloud的关系Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的云应用开发工具;Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架;Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,可以不基于Spring Boot吗?不可以。 Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系。 Spring -> Spring Boot > Spring Cloud 这样的关系。 回答 以下为我在知乎的回答。 首先楼主问的这些问题都挺好的,算是经过了自己的一番思考,我恰好经历了你所说的中小公司,且都使用Spring Cloud并且已经投产上线。第一家公司技术开发人员15人左右,项目实例 30多,第二家公司开发人员100人左右,项目实例达160多。 实话说Spring Boot、Spring Cloud仍在高速发展,技术生态不断的完善和扩张,不免也会有一些小的bug,但对于中小公司的使用来将,完全可以忽略,基本都可以找到解决方案,接下来回到你的问题。 1、市场 据我所知有很多知名互联网公司都已经使用了Spring Cloud,比如阿里、美团但都是小规模,没有像我经历的这俩家公司,业务线全部拥抱Spring Cloud;另外Spring Cloud并不是一套高深的技术,普通的Java程序员经过一到俩个月完全就可以上手,但前期需要一个比较精通人的来带队。 2、学习 有很多种方式,现在Spring Cloud越来越火的情况下,各种资源也越来越丰富,查看官方文档和示例,现在很多优秀的博客在写spirng cloud的相关教程,我这里收集了一些Spring Boot和Spring Cloud的相关资源可以参考,找到博客也就找到人和组织了。 Spring Boot学习资料汇总: Spring Cloud学习资料汇总 : 3、前后职责划分 其实这个问题是每个系统架构都应该考虑的问题,Spring Cloud只是后端服务治理的一套框架,唯一和前端有关系的是thymeleaf,Spring推荐使用它做模板引擎。一般情况下,前端app或者网页通过zuul来调用后端的服务,如果包含静态资源也可以使用nginx做一下代理转发。 4、测试 Spring-boot-starter-test支持项目中各层方法的测试,也支持controller层的各种属性。所以一般测试的步奏是这样,首先开发人员覆盖自己的所有方法,然后测试微服务内所有对外接口保证微服务内的正确性,再进行微服务之间集成测试,最后交付测试。 5、配置 session共享有很多种方式,比如使用tomcat sesion共享机制,但我比较推荐使用redis缓存来做session共享。完全可以分批引入,我在上一家公司就是分批过渡上线,新旧项目通过zuul进行交互,分批引入的时候,最好是新业务线先使用Spring Cloud,老业务做过渡,当完全掌握之后在全部替换。如果只是请求转发,zuul的性能不一定比nginx低,但是如果涉及到静态资源,还是建议在前端使用nginx做一下代理。另外Spring Cloud有配置中心,可以非常灵活的做所有配置的事情。 6、部署 多环境不同配置,Spring Boot最擅长做这个事情了,使用不同的配置文件来配置不同环境的参数,在服务启动的时候指明某个配置文件即可,例如:java -jar app.jar --spring.profiles.active=dev就是启动测试环境的配置文件;Spring Cloud 没有提供发布平台,因为jenkins已经足够完善了,推荐使用jenkins来部署Spring Boot项目,会省非常多的事情;灰度暂时不支持,可能需要自己来做,如果有多个实例,可以一个一个来更新;支持混合部署,一台机子部署多个是常见的事情。 7、开发 你说的包含html接口就是前端页面吧,Spring Boot可以支持,但其实也是Spring Mvc在做这个事情,Spring Cloud只做服务治理,其它具体的功能都是集成了各种框架来解决而已;excel报表可以,其实除过swing项目外,其它Java项目都可以想象;Spring Cloud和老项目可以混合使用,通过zuul来支持。是否支持callback,可以通过MQ来实现,还是强调Spring Cloud只是服务治理。 8、运维 Turbine、zipkin可以用来做熔断和性能监控;动态上下线某个节点可以通过jenkins来实现;provider下线后,会有其它相同的实例来提供服务,Eureka会间隔一段时间来检测服务的可用性;不同节点配置不同的流量权值目前还不支持。注册中心必须做高可用集群,注册中心挂掉之后,服务实例会全部停止。 总结,中小企业是否能用的起来Spring Cloud,完全取决于自己公司的环境,如果是一个技术活跃型的团队就大胆的去尝试吧,目前Spring Cloud是所有微服务治理中最优秀的方案,也是一个趋势,未来一两年可能就会像Spring一样流行,早接触早学习岂不更好。 希望能解答了你的疑问。 Spring Cloud 架构我们从整体来看一下Spring Cloud主要的组件,以及它的访问流程 1、外部或者内部的非Spring Cloud项目都统一通过API网关(Zuul)来访问内部服务. 2、网关接收到请求后,从注册中心(Eureka)获取可用服务 3、由Ribbon进行均衡负载后,分发到后端的具体实例 4、微服务之间通过Feign进行通信处理业务 5、Hystrix负责处理服务超时熔断 6、Turbine监控服务间的调用和熔断相关指标 图中没有画出配置中心,配置中心管理各微服务不同环境下的配置文件。 以上就是一个完整的Spring Cloud生态图。 最后送一个完整示例的Spirng Cloud开源项目等你去spring-cloud-examples 作者:纯洁的微笑出处:http://www.ityouknow.com/版权归作者所有,转载请注明出处","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"spring boot / cloud 认证鉴权设计与思考","slug":"sc/wk3","date":"2017-09-15T06:00:00.000Z","updated":"2017-09-17T13:46:55.000Z","comments":true,"path":"sc/wk3/","link":"","permalink":"http://blog.springcloud.cn/sc/wk3/","excerpt":"","text":"前言本篇接着这篇博客来继续讨论微服务间接口调用的认证和鉴权的思考和设计 在上一篇文章中主要是偏实现方面,具体的实现思想没有过多讨论,本篇文章则是主要讨论一下设计的思路. 我们都知道,在微服务的架构设计中,一个大的系统会被按照不同的领域拆分成一个个小的微服务,而这些微服务之间不可避免的会有业务数据的交互, 那么我们会用一些远程服务调用的方式来连接各个微服务( 比如:RPC,RestFul,等 ). 不过,就像前面说的一样,期初,应用不大,随便弄弄无所谓,但是等应用规模起来以后,你会发现,有成百上千的服务在运行,这些服务相互依赖,仿佛一团乱麻. 更可怕的事情是如果没有有效的权限控制,我们很有可能都不清楚是谁调用了你的服务…… 所以说,我认为,在微服务架构设计中,内部服务调用的权限控制是非常必要的(至少我参与的项目都有这种需求),它应该满足如下几个主要的功能: 防止越权行为 管理服务的依赖关系 规范服务调用行为 能够在运行时修改权限配置 下面我们来看看具体的分析: 场景分析防止越权行为在系统中添加权限相关的控制,主要是为了增加系统的安全性,总结下来主要是为了防止如下的两种越权行为: 横向越权 (指的是攻击者尝试访问与他拥有相同权限的用户的资源) 纵向越权 (指的是一个低级别攻击者尝试访问高级别用户的资源) 所以说通常,在系统中的权限校验也按照以上划分会分为两个步骤: 校验访问者身份 校验是否拥有被访问资源的权限 校验访问者身份先说校验访问者身份,这个主要目的就是确定A的确是A,不是其他的阿猫阿狗. 要做到这一点并不难,并且有很多安全框架都能支持,比如apache shiro,spring security,jwt,等等. 这些框架主要思路还是使用token签名的方式,也就是说, 要么调用方和服务方约定一个私钥,然后调用方自行通过算法生成token, 或者服务方提供一个获取token的接口(OAuth2),调用方主动调用接口获取token, 最后调用方在调用服务的时候,都把这个token给带上,便于服务方认证身份. 那么那种方式更好呢? 个人经验我会按场景做如下架构原则定义: 如果服务是给系统用的,则采用私钥的方式 如果服务是给用户(人)使用的,则采用获取token的方式(通过用户名和密码来获取token) 那么,还有一种情况,如果这个接口既是给人使用的,也是给第三方系统使用的,怎么办呢? 这个其实也不复杂,不过不会在今天这篇文章中讨论,这里只提一点,通过入口区分,也就是网关,大家可先自行脑洞. 那么,回过头来看,我们今天讨论的场景显然是属于是给系统使用的服务,所以在我设计的RMS组件中,是采用的私钥的方式. 采用这种身份认证方式需要注意如下几点: 私钥的安全性 token的过期策略 token的计算算法 在RMS组件中,我并没有引入第三方的依赖,因为我希望,这个身份认证是轻量级的,灵活的,这些第三方认证框架大而全,很优秀,但我们只会用到其中的一小块,会造成一些没必要的依赖. 从实现方面,首先,所有的私钥,都会配置到远端的配置中心里面,本地不做任何存储,由专门的人员管理和维护,系统只有在运行的时候,才能获取到私钥. 同时依赖于spring cloud config server的特性,可以在运行时更换私钥,更加灵活,也保证了的私钥的安全, 如下的sign(token)的算法,通过应用名称和私钥,要有当前时间(精确到小时),拼接起来然后进行md5,得到最终的sign,因为加入了时间的这个因子,所以计算出来的sign是每小时过期的 算法方面大家可以随意设计,但是切记,不要过度设计,满足需求即可 1234567public static String sign(String rmsApplicationName, String secret) { final String split = \"_\"; StringBuilder sb = new StringBuilder(); sb.append(rmsApplicationName).append(split).append(secret).append(split) .append(new SimpleDateFormat(DATA_FORMAT).format(new Date())); return DigestUtils.md5Hex(sb.toString());} 校验是否拥有被访问资源的权限然后我们再聊校验是否拥有被访问资源的权限,这个点说简单也简单,说复杂也非常复杂. 在前面一步的校验中,已经确定了身份,现在是要确定,A是否有访问B的/user服务的权限. 其他的不说,我这边只提两点: uri匹配 性能 在没有RestFul风格的url的时候,一切其实都还蛮美好的,因为,url就是唯一值,是整个系统的最小颗粒度的权限点. 大家以前可能是这样做的,有张表,记录这系统的url,以及其他的角色,岗位等的关联,然后,如何校验呢,非常简单, 直接的sql语句select count(1) form xxx where url=’/aaa/getUserByName’就行了,能查到值就代表有权限. 在稍微进阶一点的,会考虑性能问题,会将某个用户的一些权限缓存起来,然后在内存中进行判断. 但是,当RestFul风格的url到来的时候,这一切变得不那么美好了,先看如下几个url的例子: 12345678910111213GET /users -- 查询用户列表GET /user/{id} -- 查询用户详情POST /user -- 新增用户PUT /user --更新用户DELETE /user/{id} --删除用户GET /user/{id}/scores -- 查询某个用户的所有成绩GET /user/{id}/score/{sid} -- 查询某个用户的某门课程的成绩 我们可以看到,按照原有的方式已经不那么使用了,url的定义从原来的平面化的,变成了立体化的, 按照原来的方式,那么就变成了,如果拥有查询用户详情接口权限的系统,同时也就拥有了更新用户和删除用户的权限,这是非常严重的越权行为.这显然不是我们期望看到的. 那么如何优化呢? 首先,我们的权限判断中应该加入httpmethod的判断,这样,就能很简单的避免以上的情况. 但是更严重的问题来了,url不再是固定不变的了,而是动态的,怎么办呢?先拍脑子想想,处理方案可能有如下几种: 正则匹配 将所有url解析成树形结构,将动态部分用星号表示,然后进行最短匹配 以上两种方案我都试过,不过方案都过于复杂,甚至存在性能问题,因为以上两种方式都不可不免会进行循环匹配. 我们当然不想因为一个url校验,而引入一个性能问题的风险,那么如何解决这个问题呢? 其实我们回过头来想想,spring mvc为什么就能准确的定位到每个url对应的handler呢? 其实还是那句话,最复杂的部分,spring已经帮我们完成了,在spring 上下文初始化的时候,容器就会记录所有的mapping,如下: 1234Mapped \"{[/health || /health.json],methods=[GET],produces=[application/json || application/json]}\" onto HealthMvcEndpoint.invoke(HttpServletRequest,Principal)Mapped \"{[/env/{name:.*}],methods=[GET],produces=[application/json || application/json]}\" onto EnvironmentMvcEndpoint.value(String)Mapped \"{[/env || /env.json],methods=[GET],produces=[application/json || application/json]}\" onto EndpointMvcAdapter.invoke()Mapped \"{[/features || /features.json],methods=[GET],produces=[application/json || application/json]}\" onto EndpointMvcAdapter.invoke() 以上日志大家随便启动一个spring boot应用都能看到,其实这类输出就是我们controller定义的requestMapping 1@RequestMapping(value = \"/user/{id}/score/{sid}\", method = RequestMethod.GET) 而我们平时获取url的方式是这样的: 1String url = request.getRequestURI(); 这样获取到的url 大概是,也就是我们实际请求的url: 1/user/1/score/5 那么如何改变呢?以上这个url我们还是不知道如何匹配? 换个思路想想,能进入拦击器,则表示spring将这个地址已经匹配到了对应的handler,我们只需找到这个url对应的那个handler就行了. 在request的作用域中(Attribute),存放这很多spring的信息,debug一下,打个断点,看看都有啥?,最终我定位到了bestMatchingPattern这个属性,大致含义就是最佳匹配模式,也里面的值是requesMapping里的value 这不正是我们想要的结果吗?spring已经帮我们做了最佳的替换,如下代码: 12String url = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE).toString();//url的值为 : /user/{id}/score/{sid} 那么后续我们就可以调整我们的设计了,如果你的权限配置是配置在数据库中的那么,最简单的鉴权sql语句就是: 1select count(1) form xxx where url='/user/{id}/score/{sid}' method='GET' 这样做的好处就是可以做到最精确的匹配,颗粒度达到最细,并且毫无性能损耗,你无需做任何的循环匹配.详细代码可以在项目的RmsAuthHandlerInterceptor类中找到 其他在上面还提到了 管理服务的依赖关系 , 规范服务调用行为 , 能够在运行时修改权限配置 这几点 , 在上一篇讲解RMS代码的时候也都提及到了,会通过远程配置文件的方式,来进行管理,如下: 12345678910111213# applicationorg.itkk.rms.properties.application.udf-service-a-demo.serviceId=UDF-SERVICE-A-DEMOorg.itkk.rms.properties.application.udf-service-a-demo.secret=ASD5S2SDF6ASD2S2SD32Sorg.itkk.rms.properties.application.udf-service-a-demo.purview=ID_2,SCHEDULER_JOB_4,SCH_CLIENT_CALLBACK_1org.itkk.rms.properties.application.udf-service-a-demo.all=falseorg.itkk.rms.properties.application.udf-service-a-demo.disabled=falseorg.itkk.rms.properties.application.udf-service-a-demo.description=测试服务A# serviceorg.itkk.rms.properties.service.ID_1.owner=udf-general-server-demoorg.itkk.rms.properties.service.ID_1.uri=/service/idorg.itkk.rms.properties.service.ID_1.method=GETorg.itkk.rms.properties.service.ID_1.isHttps=falseorg.itkk.rms.properties.service.ID_1.description=获得分布式ID 会分为application和service两类,application主要描述身份认证和权限,service主要描述服务详情 . 通过这种结构来管理服务间的依赖关系 然后在项目中,大家可以看Rms这个类,里面抽象出了一个公共的方法,用于规范调用行为,最终调用的方式如下: 123ResponseEntity<RestResponse<FileInfo>> fileInfo = rms.call(\"FILE_4\", fileParam, null, new ParameterizedTypeReference<RestResponse<FileInfo>>() { }, null); 在系统中,开发人员都无需关心任何接口的定义,只需通过接口编号就可以进行调用. 最后,所有的RMS相关的配置都会放在配置中心,同一管理. 结束今天代码层面的东西讲的比较少,主要是跟大家介绍一下设计的思路,还是一个原则,使代码更健壮,更灵活,更合理,同时,也切记不要重复造轮子,也不要过度玩技术. 在下一篇文章中,我会介绍一下分布式任务调度的思考和设计,敬请期待. 代码仓库 (博客配套代码) udf-starter : 基础项目,脚手架,框架 udf-sample : 集成样例 想获得最快更新,请关注公众号","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"spring boot / cloud 404错误处理进阶","slug":"sc/wk2","date":"2017-09-14T06:00:00.000Z","updated":"2017-09-17T13:45:41.000Z","comments":true,"path":"sc/wk2/","link":"","permalink":"http://blog.springcloud.cn/sc/wk2/","excerpt":"","text":"前言在上一篇文章中介绍了spring boot 官方文档推荐的异常处理方式.承接上一篇文章,我们来了一下如何更好的处理404错误. 在spring boot / cloud (二) 规范响应格式以及统一异常处理这篇文章的最后跟大家提到了如下的配置 12spring.mvc.throw-exception-if-no-handler-found=truespring.resources.add-mappings=false 这两行配置的主要意思是告诉spring,如果你请求的地址,没有找到对应的handler,则抛出异常,默认配置是fase,同时也要关掉静态资源的映射,不然并不会起作用. 所以我们知道了,在spring中,404并不是是一个异常,而是一个错误,所走的处理方式也是不一样的. 场景分析按照以上的配置,我们可以轻易的将404错误也变成一个异常,然后走统一的异常处理方式,但是这样做是有代价的,就是也会关闭掉spring boot的经验资源mapping功能. (大家可查阅spring boot官方文档,来了解如何处理静态资源) 显然,这样做不太友好,因为不可避免的会有在后端服务中包加入一些静态界面情况.所以说,这里提到一个原则 不管是封装也好,增强也好,不能丢失原有框架原本支持的特性. 那么.如果去掉上面两行的配置,大家运行项目,请求一个不而存在的地址,错误信息会变成如下情况: 1234567{ \"timestamp\": 1503626878668, \"status\": 404, \"error\": \"Not Found\", \"message\": \"No message available\", \"path\": \"/a/a/a\"} 而我们希望的错误输出格式是这样的: 12345678910111213{ \"id\": \"e7593f09-2898-478e-a3bd-0280b93ac77f\", \"code\": \"404\", \"message\": \"Not Found\", \"result\": null, \"error\": { \"date\": 1503626920897, \"type\": \"......\", \"message\": \".........\", \"stackTrace\": null, \"child\": null }} 如何调整呢? 源码解读在spring中,专门处理error的类是BasicErrorController,如下是这个类的两个核心方法, 一个方法处理html请求的返回,会默认返回一个错误页面 另外一个则是处理json请求的,会返回json格式的错误信息,我们看到的默认输出结果,就是这个方法生成的 12345678910111213141516171819@RequestMapping(produces = \"text/html\")public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView == null ? new ModelAndView(\"error\", model) : modelAndView);}@RequestMapping@ResponseBodypublic ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<Map<String, Object>>(body, status);} 我们现在知道了我们要关注的地方,但是我们如何改造它呢? 细心的spring早就想到了可能存在的客制化需求,所以已经为我们留好了口子. 在源文件里ErrorMvcAutoConfiguration是用来配置BasicErrorController的,如下是核心方法, 这个方法上面标记了@ConditionalOnMissingBean,也就是说我们只需要实现一个ErrorController接口,注入到上下文中就可以了 @ConditionalOnMissingBean的意思 : 当上下文中存在某一个bean,则不初始化当前被标记的bean 123456@Bean@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);} 知道了方法,下面就开干吧! 实现创建一个类ExceptionController继承至AbstractErrorController(此基类实现了ErrorController) 1234567@ApiIgnore@Controller@RequestMapping(\"${server.error.path:${error.path:/error}}\")@Slf4jpublic class ExceptionController extends AbstractErrorController { ....省略} 编写如下两个方法,其实就是跟默认实现的方法一样,只是内容可以定制 然后类中其他的方法,就按照默认的实现照抄就行了. 123456789101112131415161718192021@RequestMapping(produces = \"text/html\")public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)); RestResponse<String> restResponse = this.getRestResponse(request, status, model); model.put(\"restResponse\", restResponse); model.put(KEY_EXCEPTION, restResponse.getError().getType()); model.put(KEY_MESSAGE, restResponse.getError().getMessage()); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView == null ? new ModelAndView(\"error\", model) : modelAndView);}@RequestMapping@ResponseBodypublic ResponseEntity<RestResponse<String>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); //这里构建自己的输出格式,详细代码就不贴出,如有需要可以到代码仓库查看 return new ResponseEntity<>(getRestResponse(request, status, body), status); } 然后在创建ExceptionControllerConfig类,注意上面一堆注解照抄默认实现即可 12345678@Configuration@ConditionalOnWebApplication@ConditionalOnClass({Servlet.class, DispatcherServlet.class})@AutoConfigureBefore(WebMvcAutoConfiguration.class)@EnableConfigurationProperties(ResourceProperties.class)public class ExceptionControllerConfig { ....省略} 创建exceptionController的bean,然后到这里,我们就已经替换了spring boot默认的错误处理controller,后续的错误处理都会走ExceptionController类 1234@Beanpublic ExceptionController exceptionController(ErrorAttributes errorAttributes) { return new ExceptionController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);} 结束在上面我们优化了404错误的处理流程,替换掉了原来不是很健壮的处理方式,使得代码更加灵活,也更加合理了. 在下一篇文章中,我会介绍一下服务间接口调用的认证和鉴权的思考和设计. 代码仓库 (博客配套代码) udf-starter : 基础项目,脚手架,框架 udf-sample : 集成样例 想获得最快更新,请关注公众号","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"spring boot / cloud异常统一处理进阶","slug":"sc/wk1","date":"2017-09-13T06:00:00.000Z","updated":"2017-09-17T13:45:51.000Z","comments":true,"path":"sc/wk1/","link":"","permalink":"http://blog.springcloud.cn/sc/wk1/","excerpt":"","text":"spring boot / cloud (十二) 异常统一处理进阶前言在spring boot / cloud (二) 规范响应格式以及统一异常处理这篇博客中已经提到了使用@ExceptionHandler来处理各种类型的异常,这种方式也是互联网上广泛的方式 今天这篇博客,将介绍一种spring boot官方文档上的统一处理异常的方式.大家可以在spring boot 官方文档查看介绍 在开始介绍新的方法之前 , 我们先来分析一下 , 以前的做法有那些地方是需要优化的 场景分析通常我们需要做统一异常处理的需求,大概都是要规范异常输出,以及处理,通同一套抽象出来的逻辑来处理所有异常. 但是在当前流行RestFul风格接口的环境下,对异常的输出还做了额外的一个要求,就是针对不同的错误需要输出对应的http状态. 在前面的实现中,我们大可以指定一个处理Exception的@ExceptionHandler,这样所有异常都能囊括了,但是却无法很好的将http状态区分开来. 如果要实现不同的异常输出不同的http状态,在原来的做法里就要将每个异常都穷举出来,然后做不同的设定. 显然,我们是不希望这样做的,显得太不聪明,不过还好,spring已经帮我们把这一步已经做掉了,我们只需处理自己关心的异常即可 12345678910111213@ExceptionHandler(value = 要拦截的异常.class)@ResponseStatus(响应状态)@ResponseBodypublic RestResponse<String> exception(要拦截的异常 exception) { return new RestResponse<>(ErrorCode.ERROR, buildError(exception));}@ExceptionHandler(value = Exception.class)@ResponseStatus(500)@ResponseBodypublic RestResponse<String> exception(Exception exception) { return new RestResponse<>(ErrorCode.ERROR, buildError(exception));} 源码解读在官方文档中指出,你需要实现一个类,使用@ControllerAdvice标注,然后继承至ResponseEntityExceptionHandler类. 这个ResponseEntityExceptionHandler类是一个抽象类,如下是它的核心方法 123456789101112131415161718192021222324@ExceptionHandler({ NoSuchRequestHandlingMethodException.class, HttpRequestMethodNotSupportedException.class, .....省略 })public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) { HttpHeaders headers = new HttpHeaders(); if (ex instanceof NoSuchRequestHandlingMethodException) { HttpStatus status = HttpStatus.NOT_FOUND; return handleNoSuchRequestHandlingMethod( (NoSuchRequestHandlingMethodException) ex, headers, status, request); } else if (ex instanceof HttpRequestMethodNotSupportedException) { HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED; return handleHttpRequestMethodNotSupported( (HttpRequestMethodNotSupportedException) ex, headers, status, request); } else if (..............){ .....省略 } else { .....省略 return handleExceptionInternal(ex, null, headers, status, request); }} 在以上的代码片段中,我们可以看到handleException方法已经把常见的异常都拦截掉了,并且做出了适当的处理,并且在最后,else分支里,调用了handleExceptionInternal方法, 这个方法就是处理没有被拦截到的异常,然后这也是我们要进行扩展的地方 实现实现ExceptionHandle类,继承至ResponseEntityExceptionHandler,并且注解@ControllerAdvice 12345@ControllerAdvice@Slf4jpublic class ExceptionHandle extends ResponseEntityExceptionHandler { .....} 实现exception方法,使用@ExceptionHandler拦截Exception,那么在这里,所有的异常都会进入这个方法进行处理. 然后调用父类的handleException方法(上面提到的),让spring默认的异常处理先处理一遍,如果当前的异常恰巧是被spring拦截的,那么就用spring的默认实现处理,就无需在写额外的代码了,http状态码也一并的会设置好. 最后在调用我们即将要重写的方法handleExceptionInternal,来处理自定义异常以及规范异常输出 12345@ExceptionHandler(value = Exception.class)public ResponseEntity<Object> exception(Exception ex, WebRequest request) { ResponseEntity<Object> objectResponseEntity = this.handleException(ex, request); return this.handleExceptionInternal(ex, null, objectResponseEntity.getHeaders(), objectResponseEntity.getStatusCode(), request);} 重写handleExceptionInternal方法, 在这个方法里面,可以向如下实现一样,去处理项目中自定义的异常,将其规范为想要的输出格式, 最后再调用父类的handleExceptionInternal方法,将控制权交还给spring, 这样就完成了整个异常处理的流程 12345678910111213141516171819202122232425@Overrideprotected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { HttpStatus localHttpStatus = status; ErrorResult errorResult = buildError(applicationConfig, ex); if (ex instanceof PermissionException) { //权限异常 localHttpStatus = HttpStatus.FORBIDDEN; } else if (ex instanceof AuthException) { //认证异常 localHttpStatus = HttpStatus.UNAUTHORIZED; } else if (ex instanceof ParameterValidException) { //参数校验异常 localHttpStatus = HttpStatus.BAD_REQUEST; } else if (ex instanceof RestClientResponseException) { //rest请求异常 try { RestClientResponseException restClientResponseException = (RestClientResponseException) ex; String data = restClientResponseException.getResponseBodyAsString(); if (StringUtils.isNotBlank(data)) { RestResponse<String> child = objectMapper.readValue(data, objectMapper.getTypeFactory().constructParametricType(RestResponse.class, String.class)); errorResult.setChild(child); } } catch (IOException e) { throw new SystemRuntimeException(e); } } log.error(ex.getClass().getName(), ex); return super.handleExceptionInternal(ex, new RestResponse<>(localHttpStatus, errorResult), headers, localHttpStatus, request);} 结束在上面我们优化了统一异常处理的代码,做到了只关心系统自定义异常的处理,框架和容器的异常处理,交由spring处理,简化了代码,避免了重复造轮子,同时代码也更加健壮了. 在下一篇文章中,我会介绍另外一个更合里的处理404错误的方式,敬请期待 代码仓库 (博客配套代码) udf-starter : 基础项目,脚手架,框架 udf-sample : 集成样例 想获得最快更新,请关注公众号","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"基于微服务API级权限的技术架构","slug":"sc/mdx-rhj","date":"2017-09-09T04:23:31.000Z","updated":"2017-09-09T12:57:12.000Z","comments":true,"path":"sc/mdx-rhj/","link":"","permalink":"http://blog.springcloud.cn/sc/mdx-rhj/","excerpt":"","text":"1.概念权限是根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。例如,对于一个文件系统的权限来说,用户A和B只具有查看和拷贝该文件系统下某些文件的权限,而用户C和D不仅有查看和拷贝文件的权限,也具有修改和删除文件的权限,这些权限的划分和授权需要事先通过专门管理员进行操作。在实际的生产系统中,用户数量是庞大的,权限的划分需要结合具体的业务场景,一旦把控不住粒度,工作也是繁重的,那么如何解决这个问题? 业界专门提出了一套权限模型和方法,RBAC(Role Based Access Control),即基于角色(Role)的访问控制方法。它的核心概念 角色(Role)与权限(Permission)相关联,一个角色对应多个权限 用户(User)与角色(Role)相关联,一个用户对应多个角色 权限(Permission)包含资源,或者与操作组合方式相结合 这样,我们就实现了让用户通过成为适当角色的成员而得到这些角色的权限,最终实现权限控制的目的 权限模型图o 结合上述的文件系统例子,我们可以去用RBAC去刻画和描述: 文件系统中的文件是权限概念中的“资源”,对文件的删除是“操作”,那么我们可以定义出一个“文件删除”的权限来 访问文件系统的用户A、B、C和D即上述模型中的用户,当然如果用户很多,我们可以划分出“用户组”概念来 对于角色,我们可以划分出两个角色,第一个是“文件普通用户”角色,它包含“文件查看”和“文件拷贝”两个权限;第二个是“文件管理员用户”角色,它包含“文件修改”和“文件删除”两个权限 一言以蔽之,基于角色的访问控制方法的访问逻辑表达式为“Who对What(Which)进行How的操作”,它的由内到外的逻辑结构为权限->角色->用户,即一个角色对应绑定多个权限,一个用户对应绑定多个角色. 从图中可以看到,用户A和B赋予了“文件普通用户”角色,即他们拥有了“文件查看”和“文件拷贝”的权限;用户C和D同时赋予了“文件普通用户”和“文件管理员用户”的两个角色,即他们拥有了“文件查看”、“文件拷贝”、“文件修改”和“文件删除”。如果后面,我们觉得“文件拷贝”有文件泄密的安全问题,那么只需要从它从“文件普通用户”角色移除就可以了,上述4个用户自然无法实行对文件拷贝的这个操作了,所以RBAC模型对于权限扩展和收缩非常方便. 阐述完权限系统的基本概念后,我们来讲讲,权限系统在互联网时代的分布式系统中,尤其是微服务架构的体系下,有什么样的挑战?它又必须解决哪些问题,最适合采用什么框架和技术去解决这些问题? 2.现状及挑战 服务实例数量庞大。目前,组成买单侠业务线系统,有将近400多个微服务,我们知道微服务的优点是可以清晰的划分出业务逻辑来,让每个微服务承担职责单一的功能,毕竟越简单的东西越稳定。但是,微服务也带来了很多的问题,完成一个业务操作,需要跨很多个微服务的调用,那么如何用权限系统去控制用户对不同微服务的调用,对我们来说,是个挑战。 用户系统数量多类型复杂。目前,接入买单侠业务线的用户系统数量多类型复杂,且数据分散,比如有公司的员工系统(Ldap系统),公司的销售人员系统,公司的外包人员系统,外部互联网用户系统(使用APP的客户),不同类型的用户系统都有可能接入某些微服务,那么如何用权限系统去控制不通用户对同一个微服务的调用,对我们来说,又是一个挑战。 微服务吞吐量大、可用性要求高。当业务微服务的调用接入权限系统后,不能拖累它们的吞吐量,当权限系统出现问题后,不能阻塞它们的业务调用进度,当然更不能改变业务逻辑。 已有业务系统快速接入权限系统。新的业务微服务快速接入权限系统相对容易把控,那么对于公司已有的微服务,如何能不改动它们的架构方式的前提下,快速接入,对我们来说,也是一大挑战。 3.技术方案及系统架构经过不断的业界框架的选型对比,原本想采用Spring Security框架来做我们的权限框架,但是经过研究,它有很多优势,但也有明显的两个不足:框架笨重,权限数据持久化层次结构不好,最重要的是无法做数据库持久化。于是我们决定自研开发,那么技术方案有几个特点: 权限系统微服务化 既然有那么多微服务,那么我们把权限系统也微服务化,通过微服务来控制其他微服务的权限,保证整体系统架构的一致性。 微服务的统一性和独立性 未来买单侠所有的业务微服务都将接入到权限微服务中,做统一控制。权限微服务即为独立的公共服务,作为众多微服务中的一员,它必定将遵循买单侠微服务架构的线路,即业务微服务的权限验证,要走阿里云SLB->Zuul Api Gateway,如果是基于Web的权限验证,还需要套入Ngnix Rest请求代理。如下图所示 权限服务与其它公共服务的关系图 业务微服务的代码微侵入 我们将采用自定义权限的注解(Annotation),尽可能增加新的代码到业务代码层面,减轻业务线的负担。 高可用,分布式的权限缓存 基本权限/角色数据我们通过MySql的数据中,权限验证数据,则通过Redis集群缓存。那么意味着,对于足够多的权限验证数据缓存Redis集群后,权限微服务全部崩溃也没关系;反之,当Redis集群崩溃,只要权限微服务运行正常,也不影响权限验证,只是性能会稍差而已。 支持多类型权限,多调用方式 我们将支持业务服务通过RPC方式进行权限验证,支持其他系统(例如WEB)通过REST方式进行权限验证。对于业务服务的,主要是支持接口加注解进行权限拦截验证,即API权限;对于其他系统,一般主要是体现在界面元素的权限校验(例如Web页面上按钮的Enabled/Disabled,通过权限系统来控制),即界面权限 丰富、友好、多维度的权限/角色/用户录入和绑定界面 权限数据的导入导出 结合Spring OAuth单点登录,Spring Session等,实现安全体系范畴的权限扩展 接下去,我们具体来阐述,权限微服务的核心技术方案,基于界面的权限控制相对容易,就略过了,主要讲一下基于API权限的实现 4. API权限定义、入库和拦截对于API权限,我们实现基于注解(Annotation)的扫描入库和拦截,不需要业务服务自行在权限Web界面上录入. (1)权限定义API权限以每个接口或者实现类中的方法作为权限资源,每个权限和微服务名(Service Name)挂钩 我们通过在业务服务的API上添加注解的方式,进行权限定义。基础架构部会提供一个权限组件(Permission Component)Jar给业务服务部门,里面包含了自定义的注解,这样的实现方式,对业务服务的影响非常小,增加权限机制只是在代码层面加几个注解而已。具体使用方式如下 对于一个普通的接口类,我们可以这样定义: 12345@Group(name = \"User Permission Group\", label = \"用户权限组\", description = \"用户权限组\")public interface UserService { @Permission(name = \"Add User\", label = \"添加用户\") boolean addUser(@UserId String userId, @UserType String userType, User user);} 对于通过Swagger方式暴露出去的API,我们可以这样定义: 1234567891011@Path(\"/user\")@Consumes(MediaType.APPLICATION_JSON)@Produces(MediaType.APPLICATION_JSON)@Api(value = \"User resource operations\")@Group(name = \"User Permission Group\", label = \"用户权限组\", description = \"用户权限组\")public interface UserService { @POST @Path(\"/addUser/{userId}/{userType}\") @Permission(name = \"Add User\", label = \"添加用户\") boolean addUser(@PathParam(\"userId\") @UserId String userId, @PathParam(\"userType\") @UserType String userType, User user);} 在上述简短的代码中,我们可以发现有四个自定义的注解,@Group、@Permission、@UserId和@UserType。 @Permission,即为每个API(接口方法)定义一个权限,要求有name(英文格式),label(中文格式)和description(权限描述) @Group,即定义的权限归属哪个权限组,考虑到一个接口中包含很多个API,接口数目又比较多,那么我们可以为每个接口下的所有方法归为一个组。业务服务可自行定义权限组,也可以选择不定义,那么会归属到默认预定义的权限组中 @UserId,即业务服务需要在他们的API上加入用户ID的参数,当AOP切面拦截做权限验证时候,用户ID是需要传入的必要参数 @UserType,即业务服务需要在他们的API上加入用户类型的参数,当AOP切面拦截做权限验证时候,用户类型是需要传入的必要参数。用户类型可以让同一个业务服务支持多个用户系统 (2)权限入库和拦截当API权限定义好以后,我们在权限组件里面加入扫描权限入库和拦截的算法。采用Spring AutoProxy自动代理的框架来实现我们的扫描算法: 2.1 创建PermissionInterceptor.java继承org.aopalliance.intercept.MethodInterceptor,步骤如下: 实现Object invoke(MethodInvocation invocation)方法,获取注解值 根据不同注解进行不同的切面拦截,实现对@Group,@Permission、@UserId和@UserType四个注解的权限拦截逻辑 2.2 创建PermissionAutoProxy.java继承Spring的org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator类,步骤如下: 在构造方法里设置好Interceptor通用代理器(即实现了MethodInterceptor接口的拦截类PermissionInterceptor.java) shouldProxyTargetClass用来决定是接口代理,还是类代理。在权限定义的时候,其实我们还支持把注解加在实现类上,而不仅仅在接口上,这样灵活运用注解放置的方式 getAdvicesAndAdvisorsForBean是最核心的方法,用来决定哪个类、哪个方法上的注解要被扫描入库,也决定哪个类、哪个方法要被代理。如果我们做的更加通用一点,那么可以抽象出三个方法,供getAdvicesAndAdvisorsForBean调用。 12345678// 返回拦截类,拦截类必须实现MethodInterceptor接口,即PermissionInterceptorprotected abstract Class<? extends MethodInterceptor> getInterceptorClass();// 返回接口或者类的方法名上的注解,如果接口或者类中方法名上存在该注解,即认为该接口或者类需要被代理protected abstract Class<? extends Annotation> getMethodAnnotationClass();// 扫描到接口或者类的方法名上的注解后,所要做的处理protected abstract void methodAnnotationScanned(Class<?> targetClass); 2.3 创建PermissionScanListener.java实现Spring的org.springframework.context.ApplicationListener.ApplicationListener接口,步骤如下: 在onApplicationEvent(ContextRefreshedEvent event)方法里实现入库代码 在业务服务的Spring容器启动的时候,将自动触发权限数据入库的事件 通过上述阐述,我们就实现了权限的扫描入库和拦截,可以参照下面的流程图: 5.API权限所对应的角色(Role)管理角色是一组API权限的汇总,每个角色也将和微服务名挂钩。角色组的作用是为了汇总和管理众多的角色 角色管理需要人工在界面上进行操作,角色管理分为角色组增删改查,以及每个角色组下的角色增删改查 角色组管理页面 角色管理界面 6. API权限所属的角色和用户(User)的绑定权限不能直接和用户绑定,必须通过角色作为中间桥梁进行关联。那么我们要实现 角色与权限的绑定,即一个角色和多个权限的关联 用户与角色的绑定,即一个用户和多个角色的关联 角色和权限的绑定界面 用户和角色的绑定界面 6. 权限系统验证方式 API接入的验证方式 通过远程RPC方式的调用,即通过权限API的方式注入,进行远程调用。 Rest调用的验证方式 http://host:port/authorization/authorize/{userId}/{userType}/{permissionName}/{PermissionType}/{serviceName} 通过User ID、User Type、Permission Name(权限名,映射于对应的方法名)、Permission Type(区别是API权限还是界面权限),Service Name(应用名)来判断是否被授权,返回结果是true或者false。 7. 权限服务和用户服务的整合用户服务即整合了Ldap系统的用户和桥接业务用户系统 权限服务接入用户服务后,可以在权限授权页面上选取相应的用户进行权限授权。 8.限服务和Redis分布式缓存系统的整合由于权限服务属于公共服务,它提供面向买单侠所有业务服务的权限接入,所以承受的性能压力会很大,我们通过运用Redis分布式缓存系统缓存已经验证过的权限。但其中需要注意一个策略,当跟某个用户有关的角色,权限添加删除,或者所属的绑定关系发生变更的时候,需要让缓存中的权限数据失效和删除。\u0005\\ 9.目前存在的问题以及未来规划存在的问题: 由于接入权限服务的业务微服务数量还不够多,随着后期接入数量的增加,可能会有更多问题暴露出来,比如高并发要求、低延迟要求等等。 业务权限的API上都要加User ID和User Type两个参数,给他们带来些许的不适,期望未来的版本能通过前置埋点的方式解决。 未来的规划: 我们会考虑通过多种机制实现服务级别的访问控制: 黑/白IP名单机制。当A服务调用B服务的时候,B服务会实现维护一个黑/白IP列表,表示B服务只允许在某个IP网段的A服务才能有权限调用B服务。 服务间约定SecretKey实现安全访问。当A服务调用B服务的时候,两个服务之间实现约定API访问密钥,此密钥不能轻易泄密。这样就规避了B服务被模拟Rest请求调用(例如通过PostMan调用)。 服务的API签名。当A服务调用B服务的时候,A服务需要获得正确的B服务API的签名,才有权限去调用。 ","categories":[{"name":"微服务","slug":"微服务","permalink":"http://blog.springcloud.cn/categories/微服务/"}],"tags":[{"name":"服务鉴权","slug":"服务鉴权","permalink":"http://blog.springcloud.cn/tags/服务鉴权/"}]},{"title":"自研网关纳管Spring Cloud(一)","slug":"sc/janus-01","date":"2017-08-16T06:00:00.000Z","updated":"2017-08-19T11:06:18.000Z","comments":true,"path":"sc/janus-01/","link":"http://xujin.org/janus/janus-01/","permalink":"http://blog.springcloud.cn/sc/janus-01/","excerpt":"","text":"","categories":[{"name":"Janus网关","slug":"Janus网关","permalink":"http://blog.springcloud.cn/categories/Janus网关/"}],"tags":[{"name":"网关中间件","slug":"网关中间件","permalink":"http://blog.springcloud.cn/tags/网关中间件/"}]},{"title":"Spring Cloud Zuul遗失的世界(三)","slug":"sc/sc-zuul-s3","date":"2017-08-15T06:00:00.000Z","updated":"2017-08-19T11:06:34.000Z","comments":true,"path":"sc/sc-zuul-s3/","link":"http://xujin.org/sc/sc-zuul-s3/","permalink":"http://blog.springcloud.cn/sc/sc-zuul-s3/","excerpt":"","text":"","categories":[{"name":"Spring Cloud Zuul","slug":"Spring-Cloud-Zuul","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Zuul/"}],"tags":[{"name":"Spring Cloud Zuul","slug":"Spring-Cloud-Zuul","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Zuul/"}]},{"title":"Spring Cloud Zuul遗失的世界(二)","slug":"sc/sc-zuul-s2","date":"2017-08-14T06:00:00.000Z","updated":"2017-08-19T11:06:27.000Z","comments":true,"path":"sc/sc-zuul-s2/","link":"http://xujin.org/sc/sc-zuul-s2/","permalink":"http://blog.springcloud.cn/sc/sc-zuul-s2/","excerpt":"","text":"","categories":[{"name":"Spring Cloud Zuul","slug":"Spring-Cloud-Zuul","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Zuul/"}],"tags":[{"name":"Spring Cloud Zuul","slug":"Spring-Cloud-Zuul","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Zuul/"}]},{"title":"Spring Cloud Zuul遗失的世界(一)","slug":"sc/sc-zuul-s1","date":"2017-08-13T06:00:00.000Z","updated":"2017-08-19T11:06:24.000Z","comments":true,"path":"sc/sc-zuul-s1/","link":"http://xujin.org/sc/sc-zuul-s1/","permalink":"http://blog.springcloud.cn/sc/sc-zuul-s1/","excerpt":"","text":"","categories":[{"name":"Spring Cloud Zuul","slug":"Spring-Cloud-Zuul","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Zuul/"}],"tags":[{"name":"Spring Cloud Zuul","slug":"Spring-Cloud-Zuul","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Zuul/"}]},{"title":"基于微服务API级权限的技术架构","slug":"sc/mfw-jq","date":"2017-08-12T04:23:31.000Z","updated":"2017-08-12T13:57:02.000Z","comments":true,"path":"sc/mfw-jq/","link":"","permalink":"http://blog.springcloud.cn/sc/mfw-jq/","excerpt":"","text":"背景权限系统是根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。 一般而言,企业内部一套成熟的权限系统,都是基于角色(Role)的访问控制方法(RBAC – Role Based Access Control),即权限(Permission)与角色相关联,用户(User)通过成为适当角色的成员而得到这些角色的权限,权限包含资源(或者与操作组合方式相结合),最终实现权限控制的目的。 一言以蔽之,基于角色的访问控制方法的访问逻辑表达式为“Who对What(Which)进行How的操作”,它的由内到外的逻辑结构为权限->角色->用户,即一个角色对应绑定多个权限,一个用户对应绑定多个角色,这也是秦苍基础架构部对于公共权限服务实现的基本指导思想。 权限服务与基础架构部其他公共服务的关系图 一.API权限定义、入库和拦截对于API权限,我们实行基于注解(Annotation)的扫描入库和拦截,不需要业务服务自行在界面上录入 1、权限定义API权限以每个接口或者实现类中的方法作为权限资源,每个权限和微服务名(Service Name)挂钩。 我们通过在业务服务的API上添加注解的方式,进行权限定义。基础架构部会提供一个权限组件(Permission Component)Jar给业务服务部门,里面包含了自定义的注解,这样的实现方式,对业务服务的影响非常小,增加权限机制只是在代码层面加几个注解而已。具体使用方式如下 对于一个普通的接口类,我们可以这样定义: 12345678@Group(name = \"User Permission Group\", label = \"用户权限组\", description = \"用户权限组\")public interface UserService { @Permission(name = \"Add User\", label = \"添加用户\") boolean addUser(@UserId String userId, User user); @Permission(name = \"Delete User\", label = \"删除用户\", description = \"删除用户\") boolean deleteUser(@UserId String userId, User user);} 对于通过Swagger方式暴露出去的API,我们可以这样定义: 12345678910111213141516@Path(\"/user\")@Consumes(MediaType.APPLICATION_JSON)@Produces(MediaType.APPLICATION_JSON)@Api(value = \"User resource operations\")@Group(name = \"User Permission Group\", label = \"用户权限组\", description = \"用户权限组\")public interface UserService { @GET @Path(\"/addUser/{userId}\") @Permission(name = \"Add User\", label = \"添加用户\") boolean addUser(@PathParam(\"userId\") @UserId String userId, User user); @POST @Path(\"/deleteUser/{userId}\") @Permission(name = \"Delete User\", label = \"删除用户\", description = \"删除用户\") boolean deleteUser(@PathParam(\"userId\") @UserId String userId, User user);} 在上述简短的代码中,我们可以发现有三个自定义的注解,@Group、@Permission和@UserId。 @Permission,即为每个API(接口方法)定义一个权限,要求有name(英文格式),label(中文格式)和description(权限描述) @Group,即定义的权限归属哪个权限组,考虑到一个接口中包含很多个API,接口数目又比较多,那么我们可以为每个接口下的所有方法归为一个组。业务服务可自行定义权限组,也可以选择不定义,那么会归属到默认预定义的权限组中 l@UserId,即业务服务需要在他们的API上加入用户ID的参数,当AOP切面拦截做权限验证时候,用户ID是需要传入的必要参数 2、权限入库和拦截当API权限定义好以后,我们在权限组件里面加入扫描权限入库和拦截的算法。采用Spring AutoProxy自动代理的框架来实现我们的扫描算法。 2.1 创建PermissionInterceptor.java继承org.aopalliance.intercept.MethodInterceptor,步骤如下 实现Object invoke(MethodInvocation invocation)方法,获取注解值 根据不同注解进行不同的切面拦截,实现对@Group,@Permission和@UserId三个注解的权限拦截逻辑 2.2 创建PermissionAutoProxy.java继承Spring的org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator类,步骤如下 在构造方法里设置好Interceptor通用代理器(即实现了MethodInterceptor接口的拦截类PermissionInterceptor.java) shouldProxyTargetClass用来决定是接口代理,还是类代理。在权限定义的时候,其实我们还支持把注解加在实现类上,而不仅仅在接口上,这样灵活运用注解放置的方式- getAdvicesAndAdvisorsForBean是最核心的方法,用来决定哪个类、哪个方法上的注解要被扫描入库,也决定哪个类、哪个方法要被代理。如果我们做的更加通用一点,那么可以抽象出三个方法,供getAdvicesAndAdvisorsForBean调用 12345678// 返回拦截类,拦截类必须实现MethodInterceptor接口,即PermissionInterceptorprotected abstract Class<? extends MethodInterceptor> getInterceptorClass();// 返回接口或者类的方法名上的注解,如果接口或者类中方法名上存在该注解,即认为该接口或者类需要被代理protected abstract Class<? extends Annotation> getMethodAnnotationClass();// 扫描到接口或者类的方法名上的注解后,所要做的处理protected abstract void methodAnnotationScanned(Class<?> targetClass); 2.3 创建PermissionScanListener.java实现Spring的org.springframework.context.ApplicationListener.ApplicationListener接口,步骤如下 在onApplicationEvent(ContextRefreshedEvent event)方法里实现入库代码 在微服务的Spring容器启动的时候,将自动触发权限数据入库的事件 通过上述阐述,我们就实现了权限的扫描入库和拦截 二、API权限所对应的角色(Role)管理角色是一组API权限的汇总,每个角色也将和微服务名挂钩。角色组的作用是为了汇总和管理众多的角色 角色管理需要人工在界面上进行操作,角色管理分为角色组增删改查,以及每个角色组下的角色增删改查 三、 API权限所属的角色和用户(User)的绑定权限不能直接和用户绑定,必须通过角色作为中间桥梁进行关联。那么我们要实现 角色与权限的绑定,即一个角色和多个权限的关联 用户与角色的绑定,即一个用户和多个角色的关联 角色和权限的绑定页面 用户和角色的绑定页面 </center 四、权限系统验证方式1、API接入的验证方式通过远程RPC方式的调用 1.1 扫描接入Permission组件的API Resource1234567@Configuration@ComponentScan(basePackages = { \"com.omniprimeinc.service.myservice \" }) @Import({ com.omniprimeinc.commonservice.permission.api.config.Config.class, com.omniprimeinc.commonservice.permission.annotations.config.Config.class })public class Config { } 1.2 通过API Resource去调用RPC接口获取验证结果123456789@Path(\"/authorization\")@Consumes(MediaType.APPLICATION_JSON)@Produces(MediaType.APPLICATION_JSON)@Api(value = \"Authorization resource operations\")public interface AuthorizationResource {@GET@Path(\"/authorize/{userId}/{permissionName}/{serviceName}\")Boolean authorize(@PathParam(\"userId\") String userId, @PathParam(\"permissionName\") String permissionName, @PathParam(\"serviceName\") String serviceName);} 2、Rest调用的验证方式http://host:port/authorization/authorize/{userId}/{permissionName}/{serviceName} 通过User ID、Permission Name(权限名,映射于对应的方法名)、Service Name(应用名)来判断是否被授权,返回结果是true或者false 五、权限服务和用户服务的整合用户服务即整合了Ldap系统的用户和桥接业务用户系统 权限服务接入用户服务后,可以在权限授权页面上选取相应的用户进行权限授权 六、权限服务的安全控制未来规划,服务之间的调用增加如下机制 黑/白IP名单机制。当A服务调用B服务的时候,B服务会实现维护一个黑/白IP列表,表示B服务只允许在某个IP网段的A服务才能有权限调用B服务 服务间约定的SecretKey。当A服务调用B服务的时候,两个服务之间实现实现约定API访问密钥,此密钥不能轻易泄密。这样就规避了B服务被模拟Rest请求调用(例如通过PostMan调用) 服务的API签名。当A服务调用B服务的时候,A服务需要获得正确的B服务API的签名,才有权限去调用","categories":[{"name":"微服务","slug":"微服务","permalink":"http://blog.springcloud.cn/categories/微服务/"}],"tags":[{"name":"服务鉴权","slug":"服务鉴权","permalink":"http://blog.springcloud.cn/tags/服务鉴权/"}]},{"title":"Spring Cloud Sleuth进阶实战","slug":"sc/sc-fzp-sleuth","date":"2017-08-07T04:23:31.000Z","updated":"2017-08-07T12:59:31.000Z","comments":true,"path":"sc/sc-fzp-sleuth/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-fzp-sleuth/","excerpt":"","text":"为什么需要Spring Cloud Sleuth微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。 举个例子,在微服务系统中,一个来自用户的请求,请求先达到前端A(如前端界面),然后通过远程调用,达到系统的中间件B、C(如负载均衡、网关等),最后达到后端服务D、E,后端经过一系列的业务逻辑计算最后将数据返回给用户。对于这样一个请求,经历了这么多个服务,怎么样将它的请求过程的数据记录下来呢?这就需要用到服务链路追踪。 Google开源的 Dapper链路追踪组件,并在2010年发表了论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,这篇文章是业内实现链路追踪的标杆和理论基础,具有非常大的参考价值。目前,链路追踪组件有Google的Dapper,Twitter 的Zipkin,以及阿里的Eagleeye (鹰眼)等,它们都是非常优秀的链路追踪开源组件。 本文主要讲述如何在Spring Cloud Sleuth中集成Zipkin。在Spring Cloud Sleuth中集成Zipkin非常的简单,只需要引入相应的依赖和做相关的配置即可。 基本术语 Spring Cloud Sleuth采用的是Google的开源项目Dapper的专业术语。 Span:基本工作单元,发送一个远程调度任务 就会产生一个Span,Span是一个64位ID唯一标识的,Trace是用另一个64位ID唯一标识的,Span还有其他数据信息,比如摘要、时间戳事件、Span的ID、以及进度ID。 Trace:一系列Span组成的一个树状结构。请求一个微服务系统的API接口,这个API接口,需要调用多个微服务,调用每个微服务都会产生一个新的Span,所有由这个请求产生的Span组成了这个Trace。 Annotation:用来及时记录一个事件的,一些核心注解用来定义一个请求的开始和结束 。这些注解包括以下: cs - Client Sent -客户端发送一个请求,这个注解描述了这个Span的开始 sr - Server Received -服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳便可得到网络传输的时间。 ss - Server Sent (服务端发送响应)–该注解表明请求处理的完成(当请求返回客户端),如果ss的时间戳减去sr时间戳,就可以得到服务器请求的时间。 cr - Client Received (客户端接收响应)-此时Span的结束,如果cr的时间戳减去cs时间戳便可以得到整个请求所消耗的时间。 案例实战本文案例一共四个工程采用多Module形式。需要新建一个主Maven工程,主要指定了Spring Boot的版本为1.5.3,Spring Cloud版本为Dalston.RELEASE。包含了eureka-server工程,作为服务注册中心,eureka-server的创建过程这里不重复;zipkin-server作为链路追踪服务中心,负责存储链路数据;gateway-service作为服务网关工程,负责请求的转发,同时它也作为链路追踪客户端,负责产生数据,并上传给zipkin-service;user-service为一个应用服务,对外暴露API接口,同时它也作为链路追踪客户端,负责产生数据。 构建zipkin-server工程新建一个Module工程,取名为zipkin-server,其pom文件继承了主Maven工程的pom文件;作为Eureka Client,引入Eureka的起步依赖spring-cloud-starter-eureka,引入zipkin-server依赖,以及zipkin-autoconfigure-ui依赖,后两个依赖提供了Zipkin的功能和Zipkin界面展示的功能。代码如下: ```xml com.forezp sleuth 0.0.1-SNAPSHOT <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-server</artifactId> </dependency> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-ui</artifactId> </dependency> </dependencies> 12 在程序的启动类ZipkinServiceApplication加上@EnableZipkinServer开启ZipkinServer的功能,加上@EnableEurekaClient注解,启动Eureka Client。代码如下: ```java @SpringBootApplication @EnableEurekaClient @EnableZipkinServer public class ZipkinServerApplication { public static void main(String[] args) { SpringApplication.run(ZipkinServerApplication.class, args); } } ``` 在配置文件application.yml文件,指定程序名为zipkin-server,端口为9411,服务注册地址为http://localhost:8761/eureka/。 ``` eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ server: port: 9411 spring: application: name: zipkin-server ``` ### 构建user-service 在主Maven工程下建一个Module工程,取名为user-service,作为应用服务,对外暴露API接口。pom文件继承了主Maven工程的pom文件,并引入了Eureka的起步依赖spring-cloud-starter-eureka,Web起步依赖spring-boot-starter-web,Zipkin的起步依赖spring-cloud-starter-zipkin,代码如下: ```xml <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> <version>RELEASE</version> </dependency> </dependencies> ``` 在配置文件applicatiom.yml,指定了程序名为user-service,端口为8762,服务注册地址为http://localhost:8761/eureka/,Zipkin Server地址为http://localhost:9411。spring.sleuth.sampler.percentage为1.0,即100%的概率将链路的数据上传给Zipkin Server,在默认的情况下,该值为0.1,代码如下: eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/server: port: 8762spring: application: name: user-service zipkin: base-url: http://localhost:9411 sleuth: sampler: percentage: 1.012345-------在UserController类建一个“/user/hi”的API接口,对外提供服务,代码如下: ```java @RestController @RequestMapping("/user") public class UserController { @GetMapping("/hi") public String hi(){ return "I'm forezp"; } } ``` 最后作为Eureka Client,需要在程序的启动类UserServiceApplication加上@EnableEurekaClient注解。 ### 构建gateway-service 新建一个名为gateway-service工程,这个工程作为服务网关,将请求转发到user-service,作为Zipkin客户端,需要将链路数据上传给Zipkin Server,同时它也作为Eureka Client。它在pom文件除了需要继承主Maven工程的 pom,还需引入的依赖如下: ```xml <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> <version>RELEASE</version> </dependency> </dependencies> ``` 在application.yml文件,配置程序名为gateway-service,端口为5000,服务注册地址为http://localhost:8761/eureka/,Zipkin Server地址为http://localhost:9411,以“/user-api/**”开头的Uri请求,转发到服务名为 user-service的服务。配置代码如下: ``` eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ server: port: 5000 spring: application: name: gateway-service sleuth: sampler: percentage: 1.0 zipkin: base-url: http://localhost:9411 zuul: routes: api-a: path: /user-api/** serviceId: user-service ``` 在程序的启动类GatewayServiceApplication,加上@EnableEurekaClient注解开启Eureka Client,加上@EnableZuulProxy注解,开启Zuul代理功能。代码如下: ``` @SpringBootApplication @EnableZuulProxy @EnableEurekaClient public class GatewayServiceApplication { public static void main(String[] args) { SpringApplication.run(GatewayServiceApplication.class, args); } } ``` ### 项目演示 完整的项目搭建完毕,依次启动eureka-server、zipkin-server、user-service、gateway-service。在浏览器上访问http://localhost:5000/user-api/user/hi,浏览器显示: >I'm forezp 访问http://localhost:9411,即访问Zipkin的展示界面,界面显示如图1所示:  这个界面主要用来查找服务的调用情况,可以根据服务名、开始时间、结束时间、请求消耗的时间等条件来查找。点击“Find Trackes”按钮,界面如图所示。从图可知服务的调用情况,比如服务调用时间、服务的消耗时间,服务调用的链路情况。  点击Dependences按钮,可以查看服务的依赖关系,在本案例中,gateway-service将请求转发到了user-service,它们的依赖关系如图:  ## 怎么在链路数据中添加自定义数据 现在需要实现这样一个功能,需要在链路数据中加上操作人。这需要在gateway-service上实现。建一个ZuulFilter过滤器,它的类型为“post”,order为900,开启拦截。在拦截逻辑方法里,通过Tracer的addTag方法加上自定义的数据,比如本案例中加入了链路的操作人。另外也可以在这个过滤器中获取当前链路的traceId信息,traceId作为链路数据的唯一标识,可以存储在log日志中,方便后续查找。 ``` @Component public class LoggerFilter extends ZuulFilter { @Autowired Tracer tracer; @Override public String filterType() { return FilterConstants.POST_TYPE; } @Override public int filterOrder() { return 900; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { tracer.addTag("operator","forezp"); System.out.print(tracer.getCurrentSpan().traceIdString()); return null; } } 使用spring-cloud-starter-stream-rabbit进行链路通讯在上述的案例中,最终gateway-service收集的数据,是通过Http上传给zip-server的,在Spring Cloud Sleuth中支持消息组件来通讯的,在这一小节使用RabbitMQ来通讯。首先来改造zipkin-server,在pom文件将zipkin-server的依赖去掉,加上spring-cloud-sleuth-zipkin-stream和spring-cloud-starter-stream-rabbit,代码如下: 123456789<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> 在application.yml配置上RabbitMQ的配置,包括host、端口、用户名、密码,如下: 123456spring: rabbitmq: host: localhost port: 5672 username: guest password: guest 在程序的启动类ZipkinServerApplication上@EnableZipkinStreamServer注解,开启ZipkinStreamServer。代码如下: 123456789@SpringBootApplication@EnableEurekaClient@EnableZipkinStreamServerpublic class ZipkinServerApplication { public static void main(String[] args) { SpringApplication.run(ZipkinServerApplication.class, args); }} 现在来改造下Zipkin Client(包括gateway-service、user-service),在pom文件中将spring-cloud-starter-zipkin以来改为spring-cloud-sleuth-zipkin-stream和spring-cloud-starter-stream-rabbit,代码如下: 12345678910<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> 同时在applicayion.yml文件加上RabbitMQ的配置,同zipkin-server工程。 这样,就将链路的上传数据从Http改了为用消息代组件RabbitMQ。 将链路数据存储在Mysql数据库在上述的例子中,Zipkin Server是将数据存储在内存中,一旦程序重启,之前的链路数据全部丢失,那么怎么将链路数据存储起来呢?Zipkin支持Mysql、Elasticsearch、Cassandra存储。这一小节讲述用Mysql存储,下一节讲述用Elasticsearch存储。 首先,在zipkin-server工程加上Mysql的连接依赖mysql-connector-java,JDBC的起步依赖spring-boot-starter-jdbc,代码如下: 12345678<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> 在配置文件application.yml加上数据源的配置,包括数据库的Url、用户名、密码、连接驱动,另外需要配置zipkin.storage.type为mysql,代码如下: 123456789spring: datasource: url: jdbc:mysql://localhost:3306/spring-cloud-zipkin?useUnicode=true&characterEncoding=utf8&useSSL=false username: root password: 123456 driver-class-name: com.mysql.jdbc.Driverzipkin: storage: type: mysql 另外需要在Mysql数据库中初始化数据库脚本,数据库脚本地址:https://github.com/openzipkin/zipkin/blob/master/zipkin-storage/mysql/src/main/resources/mysql.sql 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748CREATE TABLE IF NOT EXISTS zipkin_spans ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL, `id` BIGINT NOT NULL, `name` VARCHAR(255) NOT NULL, `parent_id` BIGINT, `debug` BIT(1), `start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL', `duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query') ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`) COMMENT 'ignore insert on duplicate';ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`) COMMENT 'for joining with zipkin_annotations';ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';CREATE TABLE IF NOT EXISTS zipkin_annotations ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id', `span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id', `a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1', `a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB', `a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation', `a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp', `endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null', `endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address', `endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null', `endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null') ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces';ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces';ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';CREATE TABLE IF NOT EXISTS zipkin_dependencies ( `day` DATE NOT NULL, `parent` VARCHAR(255) NOT NULL, `child` VARCHAR(255) NOT NULL, `call_count` BIGINT, `error_count` BIGINT) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`); 将链路数据存储在ElasticSearch使用Mysql存储链路数据,在并发高的情况下,显然不合理,这时可以选择使用ElasticSearch存储。读者需要自行安装ElasticSearch、Kibana(下一小节使用),下载地址为https://www.elastic.co/products/elasticsearch。安装完成后并启动它们,其中ElasticSearch的默认端口为9200,Kibana的端口为5601。 安装的过程可以参考我的这篇文章:http://blog.csdn.net/forezp/article/details/71189836 本小节的案例在上上小节的案例的基础上进行改造。首先在pom文件,加上zipkin的依赖和zipkin-autoconfigure-storage-elasticsearch-http的依赖,代码如下: 1234567891011<dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin</artifactId> <version>1.28.0</version></dependency><dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-storage-elasticsearch-http</artifactId> <version>1.28.0</version></dependency> 在application.yml文件加上Zipkin的配置,配置了zipkin的存储类型为elasticsearch,使用的StorageComponent为elasticsearch。然后需要配置elasticsearch,包括hosts,可以配置多个,用“,”隔开;index为zipkin等,具体配置如下: 123456789101112zipkin: storage: type: elasticsearch StorageComponent: elasticsearch elasticsearch: cluster: elasticsearch max-requests: 30 index: zipkin index-shards: 3 index-replicas: 1 hosts: localhost:9200 在kibana上展示上一小节讲述了如何将链路数据存储在ElasticSearch,ElasticSearch可以和Kibana结合,将链路数据展示在 Kibana上。安装完Kibana,并启动,它默认会向本地的9200端口的ElasticSearch读取数据,它默认的端口为5601。访问http://localhost:5601,显示的界面如下: 在上述的界面点击”Management”按钮,然后点击“Add New”,添加一个index,在上节我们在ElasticSearch中写入链路数据的index配置为“zipkin”,那么在界面填写为“zipkin-*”,点击“Create”按钮。 创建完index之后,点击Discover,就可以在界面上展示链路数据了。 源码下载最原始的工程: https://github.com/forezp/SpringCloudLearning/tree/master/chapter-sleuth 采用RabbitMq通讯的工程: https://github.com/forezp/SpringCloudLearning/tree/master/chapter-sleuth-stream 采用Mysql存储的工程: https://github.com/forezp/SpringCloudLearning/tree/master/chapter-sleuth-stream-mysql 采用ES存储的工程: https://github.com/forezp/SpringCloudLearning/tree/master/chapter-sleuth-stream-elasticsearch 参考资料http://cloud.spring.io/spring-cloud-sleuth/spring-cloud-sleuth.html https://github.com/openzipkin/zipkin 转载请标明出处:http://blog.csdn.net/forezp/article/details/76795269本文出自方志朋的博客","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Sleuth","slug":"Spring-Cloud-Sleuth","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Sleuth/"}]},{"title":"《Spring Cloud中国社区上海网关专题会议》","slug":"sc/sc-gw","date":"2017-08-05T06:00:00.000Z","updated":"2017-08-07T12:52:06.000Z","comments":true,"path":"sc/sc-gw/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-gw/","excerpt":"网关在微服务中的地位尤其重要,如果网关挂了或者出现任何抖动,用户请求的流量将会损耗,将会造成巨大的损失。因此Spring Cloud中国社区联合上海秦苍科技,走进企业畅聊Spring Cloud实战经验,以及网关经验。 一.会议内容1.主题《Spring Cloud中国社区上海网关专题》2.时间:2017年8月20,会议方式:闭门会议3.地点:上海市浦东新区陆家嘴软件园4号楼4楼4.主办方Spring Cloud中国社区+上海秦苍科技5.预计参会人数: 40-50人之间,先到先得,名额满关闭报名通道。6.分享方式(分享人分享+讨论+头脑风暴)","text":"网关在微服务中的地位尤其重要,如果网关挂了或者出现任何抖动,用户请求的流量将会损耗,将会造成巨大的损失。因此Spring Cloud中国社区联合上海秦苍科技,走进企业畅聊Spring Cloud实战经验,以及网关经验。 一.会议内容1.主题《Spring Cloud中国社区上海网关专题》2.时间:2017年8月20,会议方式:闭门会议3.地点:上海市浦东新区陆家嘴软件园4号楼4楼4.主办方Spring Cloud中国社区+上海秦苍科技5.预计参会人数: 40-50人之间,先到先得,名额满关闭报名通道。6.分享方式(分享人分享+讨论+头脑风暴) 二.分享内容 Spring Cloud Zuul与GPRC服务治理体系整合,来源于Spring Cloud中国社区开源项目saluki。 基于Netty自研网关中间件纳管Spring Cloud。 如何压测和自动化测试网关 上海秦苍科技(买单侠)Spring Cloud生产实战分享(包括Zuul) PS:分享嘉宾保密,闭门会议,您懂得。 三.报名方式扫支付宝二维码,支付完毕之后,加管理员微信Software_King,进入微信群,同时会进入Spring Cloud中国社区VIP会员群。 报名费->用于茶歇+请分享嘉宾喝咖啡","categories":[{"name":"Spring Cloud中国社区专题会议","slug":"Spring-Cloud中国社区专题会议","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud中国社区专题会议/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"微服务网关解决方案调研和使用总结","slug":"sc/gw-solution","date":"2017-07-29T06:00:00.000Z","updated":"2017-07-30T00:47:56.000Z","comments":true,"path":"sc/gw-solution/","link":"http://xujin.org/janus/gw-solution/","permalink":"http://blog.springcloud.cn/sc/gw-solution/","excerpt":"","text":"","categories":[{"name":"微服务网关","slug":"微服务网关","permalink":"http://blog.springcloud.cn/categories/微服务网关/"}],"tags":[{"name":"网关中间件","slug":"网关中间件","permalink":"http://blog.springcloud.cn/tags/网关中间件/"}]},{"title":"深入理解Zuul","slug":"sc/sc-fzp-zuul","date":"2017-07-28T04:23:31.000Z","updated":"2017-07-28T13:56:53.000Z","comments":true,"path":"sc/sc-fzp-zuul/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-fzp-zuul/","excerpt":"","text":"Zuul 架构图 在zuul中, 整个请求的过程是这样的,首先将请求给zuulservlet处理,zuulservlet中有一个zuulRunner对象,该对象中初始化了RequestContext:作为存储整个请求的一些数据,并被所有的zuulfilter共享。zuulRunner中还有 FilterProcessor,FilterProcessor作为执行所有的zuulfilter的管理器。FilterProcessor从filterloader 中获取zuulfilter,而zuulfilter是被filterFileManager所加载,并支持groovy热加载,采用了轮询的方式热加载。有了这些filter之后,zuulservelet首先执行的Pre类型的过滤器,再执行route类型的过滤器,最后执行的是post 类型的过滤器,如果在执行这些过滤器有错误的时候则会执行error类型的过滤器。执行完这些过滤器,最终将请求的结果返回给客户端。 zuul工作原理源码分析在之前已经讲过,如何使用zuul,其中不可缺少的一个步骤就是在程序的启动类加上@EnableZuulProxy,该EnableZuulProxy类代码如下: 1234567@EnableCircuitBreaker@EnableDiscoveryClient@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Import(ZuulProxyConfiguration.class)public @interface EnableZuulProxy {} 其中,引用了ZuulProxyConfiguration,跟踪ZuulProxyConfiguration,该类注入了DiscoveryClient、RibbonCommandFactoryConfiguration用作负载均衡相关的。注入了一些列的filters,比如PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter,代码如如下: 123456789101112131415161718@Beanpublic PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper) { return new PreDecorationFilter(routeLocator, this.server.getServletPrefix(), this.zuulProperties, proxyRequestHelper);}// route filters@Beanpublic RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper, RibbonCommandFactory<?> ribbonCommandFactory) { RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory, this.requestCustomizers); return filter;}@Beanpublic SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties zuulProperties) { return new SimpleHostRoutingFilter(helper, zuulProperties);} 它的父类ZuulConfiguration ,引用了一些相关的配置。在缺失zuulServlet bean的情况下注入了ZuulServlet,该类是zuul的核心类。 12345678910 @Bean@ConditionalOnMissingBean(name = "zuulServlet")public ServletRegistrationBean zuulServlet() { ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(), this.zuulProperties.getServletPattern()); // The whole point of exposing this servlet is to provide a route that doesn't // buffer requests. servlet.addInitParameter("buffer-requests", "false"); return servlet;} 同时也注入了其他的过滤器,比如ServletDetectionFilter、DebugFilter、Servlet30WrapperFilter,这些过滤器都是pre类型的。 12345678910111213141516171819@Beanpublic ServletDetectionFilter servletDetectionFilter() { return new ServletDetectionFilter();}@Beanpublic FormBodyWrapperFilter formBodyWrapperFilter() { return new FormBodyWrapperFilter();}@Beanpublic DebugFilter debugFilter() { return new DebugFilter();}@Beanpublic Servlet30WrapperFilter servlet30WrapperFilter() { return new Servlet30WrapperFilter();} 它也注入了post类型的,比如 SendResponseFilter,error类型,比如 SendErrorFilter,route类型比如SendForwardFilter,代码如下: 123456789101112131415@Beanpublic SendResponseFilter sendResponseFilter() { return new SendResponseFilter();}@Beanpublic SendErrorFilter sendErrorFilter() { return new SendErrorFilter();}@Beanpublic SendForwardFilter sendForwardFilter() { return new SendForwardFilter();} 初始化ZuulFilterInitializer类,将所有的filter 向FilterRegistry注册。 123456789101112131415 @Configurationprotected static class ZuulFilterConfiguration { @Autowired private Map<String, ZuulFilter> filters; @Bean public ZuulFilterInitializer zuulFilterInitializer( CounterFactory counterFactory, TracerFactory tracerFactory) { FilterLoader filterLoader = FilterLoader.getInstance(); FilterRegistry filterRegistry = FilterRegistry.instance(); return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry); }} 而FilterRegistry管理了一个ConcurrentHashMap,用作存储过滤器的,并有一些基本的CURD过滤器的方法,代码如下: 12345678910111213141516171819202122232425262728293031323334 public class FilterRegistry { private static final FilterRegistry INSTANCE = new FilterRegistry(); public static final FilterRegistry instance() { return INSTANCE; } private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>(); private FilterRegistry() { } public ZuulFilter remove(String key) { return this.filters.remove(key); } public ZuulFilter get(String key) { return this.filters.get(key); } public void put(String key, ZuulFilter filter) { this.filters.putIfAbsent(key, filter); } public int size() { return this.filters.size(); } public Collection<ZuulFilter> getAllFilters() { return this.filters.values(); }} FilterLoader类持有FilterRegistry,FilterFileManager类持有FilterLoader,所以最终是由FilterFileManager注入 filterFilterRegistry的ConcurrentHashMap的。FilterFileManager到开启了轮询机制,定时的去加载过滤器,代码如下: 12345678910111213141516void startPoller() { poller = new Thread("GroovyFilterFileManagerPoller") { public void run() { while (bRunning) { try { sleep(pollingIntervalSeconds * 1000); manageFiles(); } catch (Exception e) { e.printStackTrace(); } } } }; poller.setDaemon(true); poller.start(); } Zuulservlet作为类似于Spring MVC中的DispatchServlet,起到了前端控制器的作用,所有的请求都由它接管。它的核心代码如下: 1234567891011121314151617181920212223242526272829303132333435363738 @Override public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { try { init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); // Marks this request as having passed through the "Zuul engine", as opposed to servlets // explicitly bound in web.xml, for which requests will not have the same data attached RequestContext context = RequestContext.getCurrentContext(); context.setZuulEngineRan(); try { preRoute(); } catch (ZuulException e) { error(e); postRoute(); return; } try { route(); } catch (ZuulException e) { error(e); postRoute(); return; } try { postRoute(); } catch (ZuulException e) { error(e); return; } } catch (Throwable e) { error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName())); } finally { RequestContext.getCurrentContext().unset(); } } 跟踪init(),可以发现这个方法为每个请求生成了RequestContext,RequestContext继承了ConcurrentHashMap,在请求结束时销毁掉该RequestContext,RequestContext的生命周期为请求到zuulServlet开始处理,直到请求结束返回结果。RequestContext类在存储了很多重要的信息,包括HttpServletRequest、HttpServletRespons、ResponseDataStream、ResponseStatusCode等。 RequestContext对象在处理请求的过程中,一直存在,所以这个对象为所有Filter共享。 从ZuulServlet的service()方法可知,它是先处理pre()类型的处理器,然后在处理route()类型的处理器,最后再处理post类型的处理器。 首先来看一看pre()的处理过程,它会进入到ZuulRunner,该类的作用是将请求的HttpServletRequest、HttpServletRespons放在RequestContext类中,并包装了一个FilterProcessor,代码如下: 1234567891011121314151617 public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) { RequestContext ctx = RequestContext.getCurrentContext(); if (bufferRequests) { ctx.setRequest(new HttpServletRequestWrapper(servletRequest)); } else { ctx.setRequest(servletRequest); } ctx.setResponse(new HttpServletResponseWrapper(servletResponse)); } public void preRoute() throws ZuulException { FilterProcessor.getInstance().preRoute();} 而FilterProcessor类为调用filters的类,比如调用pre类型所有的过滤器: 123456789public void preRoute() throws ZuulException { try { runFilters("pre"); } catch (ZuulException e) { throw e; } catch (Throwable e) { throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName()); } } 跟踪runFilters()方法,可以发现,它最终调用了FilterLoader的getFiltersByType(sType)方法来获取同一类的过滤器,然后用for循环遍历所有的ZuulFilter,执行了 processZuulFilter()方法,跟踪该方法可以发现最终是执行了ZuulFilter的方法,最终返回了该方法返回的Object对象。 1234567891011121314151617public Object runFilters(String sType) throws Throwable { if (RequestContext.getCurrentContext().debugRouting()) { Debug.addRoutingDebug("Invoking {" + sType + "} type filters"); } boolean bResult = false; List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType); if (list != null) { for (int i = 0; i < list.size(); i++) { ZuulFilter zuulFilter = list.get(i); Object result = processZuulFilter(zuulFilter); if (result != null && result instanceof Boolean) { bResult |= ((Boolean) result); } } } return bResult; } route、post类型的过滤器的执行过程和pre执行过程类似。 Zuul默认过滤器默认的核心过滤器一览表Zuul默认注入的过滤器,它们的执行顺序在FilterConstants类,我们可以先定位在这个类,然后再看这个类的过滤器的执行顺序以及相关的注释,可以很轻松定位到相关的过滤器,也可以直接打开spring-cloud-netflix-core.jar的 zuul.filters包,可以看到一些列的filter,现在我以表格的形式,列出默认注入的filter. 过滤器 order 描述 类型 ServletDetectionFilter -3 检测请求是用 DispatcherServlet还是 ZuulServlet pre Servlet30WrapperFilter -2 在Servlet 3.0 下,包装 requests pre FormBodyWrapperFilter -1 解析表单数据 pre SendErrorFilter 0 如果中途出现错误 error DebugFilter 1 设置请求过程是否开启debug pre PreDecorationFilter 5 根据uri决定调用哪一个route过滤器 pre RibbonRoutingFilter 10 如果写配置的时候用ServiceId则用这个route过滤器,该过滤器可以用Ribbon 做负载均衡,用hystrix做熔断 route SimpleHostRoutingFilter 100 如果写配置的时候用url则用这个route过滤 route SendForwardFilter 500 用RequestDispatcher请求转发 route SendResponseFilter 1000 用RequestDispatcher请求转发 post 过滤器的order值越小,就越先执行,并且在执行过滤器的过程中,它们共享了一个RequestContext对象,该对象的生命周期贯穿于请求,可以看出优先执行了pre类型的过滤器,并将执行后的结果放在RequestContext中,供后续的filter使用,比如在执行PreDecorationFilter的时候,决定使用哪一个route,它的结果的是放在RequestContext对象中,后续会执行所有的route的过滤器,如果不满足条件就不执行该过滤器的run方法。最终达到了就执行一个route过滤器的run()方法。 而error类型的过滤器,是在程序发生异常的时候执行的。 post类型的过滤,在默认的情况下,只注入了SendResponseFilter,该类型的过滤器是将最终的请求结果以流的形式输出给客户单。 现在来看SimpleHostRoutingFilter是如何工作?进入到SimpleHostRoutingFilter类的方法的run()方法,核心代码如下: 123456789101112131415161718@Overridepublic Object run() { RequestContext context = RequestContext.getCurrentContext(); //省略代码 String uri = this.helper.buildZuulRequestURI(request); this.helper.addIgnoredHeaders(); try { CloseableHttpResponse response = forward(this.httpClient, verb, uri, request, headers, params, requestEntity); setResponse(response); } catch (Exception ex) { throw new ZuulRuntimeException(ex); } return null;} 查阅这个类的全部代码可知,该类创建了一个HttpClient作为请求类,并重构了url,请求到了具体的服务,得到的一个CloseableHttpResponse对象,并将CloseableHttpResponse对象的保存到RequestContext对象中。并调用了ProxyRequestHelper的setResponse方法,将请求状态码,流等信息保存在RequestContext对象中。 123456private void setResponse(HttpResponse response) throws IOException { RequestContext.getCurrentContext().set("zuulResponse", response); this.helper.setResponse(response.getStatusLine().getStatusCode(), response.getEntity() == null ? null : response.getEntity().getContent(), revertHeaders(response.getAllHeaders())); } 现在来看SendResponseFilter是如何工作?这个过滤器的order为1000,在默认且正常的情况下,是最后一个执行的过滤器,该过滤器是最终将得到的数据返回给客户端的请求。 在它的run()方法里,有两个方法:addResponseHeaders()和writeResponse(),即添加响应头和写入响应数据流。 1234567891011public Object run() { try { addResponseHeaders(); writeResponse(); } catch (Exception ex) { ReflectionUtils.rethrowRuntimeException(ex); } return null;} 其中writeResponse()方法是通过从RequestContext中获取ResponseBody获或者ResponseDataStream来写入到HttpServletResponse中的,但是在默认的情况下ResponseBody为null,而ResponseDataStream在route类型过滤器中已经设置进去了。具体代码如下: 12345678910111213141516171819202122232425262728private void writeResponse() throws Exception { RequestContext context = RequestContext.getCurrentContext(); HttpServletResponse servletResponse = context.getResponse(); //代码省略 OutputStream outStream = servletResponse.getOutputStream(); InputStream is = null; try { if (RequestContext.getCurrentContext().getResponseBody() != null) { String body = RequestContext.getCurrentContext().getResponseBody(); writeResponse( new ByteArrayInputStream( body.getBytes(servletResponse.getCharacterEncoding())), outStream); return; } //代码省略 is = context.getResponseDataStream(); InputStream inputStream = is; //代码省略 writeResponse(inputStream, outStream); //代码省略 } } ..//代码省略 } 如何在zuul上做日志处理由于zuul作为api网关,所有的请求都经过这里,所以在网关上,可以做请求相关的日志处理。我的需求是这样的,需要记录请求的 url,ip地址,参数,请求发生的时间,整个请求的耗时,请求的响应状态,甚至请求响应的结果等。很显然,需要实现这样的一个功能,需要写一个ZuulFliter,它应该是在请求发送给客户端之前做处理,并且在route过滤器路由之后,在默认的情况下,这个过滤器的order应该为500-1000之间。那么如何获取这些我需要的日志信息呢?找RequestContext,在请求的生命周期里这个对象里,存储了整个请求的所有信息。 现在编码,在代码的注释中,做了详细的说明,代码如下: 12345678910111213141516171819202122232425262728293031323334353637@Componentpublic class LoggerFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.POST_TYPE; } @Override public int filterOrder() { return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); String method = request.getMethod();//氢气的类型,post get .. Map<String, String> params = HttpUtils.getParams(request); String paramsStr = params.toString();//请求的参数 long statrtTime = (long) context.get("startTime");//请求的开始时间 Throwable throwable = context.getThrowable();//请求的异常,如果有的话 request.getRequestURI();//请求的uri HttpUtils.getIpAddress(request);//请求的iP地址 context.getResponseStatusCode();//请求的状态 long duration=System.currentTimeMillis() - statrtTime);//请求耗时 return null; }} 现在读者也许有疑问,如何得到的statrtTime,即请求开始的时间,其实这需要另外一个过滤器,在网络请求route之前(大部分耗时都在route这一步),在过滤器中,在RequestContext存储一个时间即可,另写一个过滤器,代码如下: 1234567891011121314151617181920212223242526@Componentpublic class AccessFilter extends ZuulFilter { @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); ctx.set("startTime",System.currentTimeMillis()); return null; }} 可能还有这样的需求,我需要将响应结果,也要存储在log中,在之前已经分析了,在route结束后,将从具体服务获取的响应流存储在RequestContext中,在SendResponseFilter过滤器写入在HttpServletResponse中,最终返回给客户端。那么我只需要在SendResponseFilter写入响应流之前把响应流写入到 log日志中即可,那么会引发另外一个问题,因为响应流写入到 log后,RequestContext就没有响应流了,在SendResponseFilter就没有流输入到HttpServletResponse中,导致客户端没有任何的返回数据,那么解决的办法是这样的: 1234InputStream inputStream =RequestContext.getCurrentContext().getResponseDataStream();InputStream newInputStream= copy(inputStream);transerferTolog(inputStream);RequestContext.getCurrentContext().setResponseDataStream(newInputStream); 从RequestContext获取到流之后,首先将流 copy一份,将流转化下字符串,存在日志中,再set到RequestContext中,这样SendResponseFilter就可以将响应返回给客户端。这样的做法有点影响性能,如果不是字符流,可能需要做更多的处理工作。 转载请标明出处:http://blog.csdn.net/forezp/article/details/76211680 本文出自方志朋的博客","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Zuul","slug":"Spring-Cloud-Zuul","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Zuul/"}]},{"title":"Eureka2.0的开发动机和改进点","slug":"sc/sc-eureka2","date":"2017-07-17T04:23:31.000Z","updated":"2017-07-17T10:49:01.000Z","comments":true,"path":"sc/sc-eureka2/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-eureka2/","excerpt":"","text":"一.为什么开发Eureka 2.0?(Why Eureka 2.0?) 目前的Eureka是一个非常稳定的系统,在具有数万个节点的大型云环境下的部署中进行了测试。但是,它有以下主要限制:只支持单一的客户端获取注册信息视图:Eureka服务器只支持客户端获取全量的注册信息,不支持获取特定的应用程序或 VIP地址的注册信息。这就对在Eureka注册的所有客户机造成了内存限制,即使它们只需要Eureka注册信息的一小部分。仅支持按计划更新:Eureka客户端基于轮询机制从服务器中获取更新。即使服务器没有变更也需要客户端不停的轮询,从而在客户端会有一定的开销,同时按照一定时间间隔的轮询会导致变更之后反馈给客户端有延时。复制算法限制了可扩展性:Eureka基于广播复制模型,即所有服务器将数据和心跳复制到所有对等体。对于Eureka包含的数据集来说,这是简单有效的,但是复制是通过变更服务器通过Http请求通知到所有对等节点来实现的。因为每个节点都必须承受eureka上的整个写入负载,限制了可扩展性。 虽然Eureka 2.0提供了更加丰富的功能集合,但上述限制是本版本中改进的主要驱动因素。 二.Eureka2.0的改进点(Eureka 2.0 Improvements) 基于上述动机,Eureka 2.0实现了以下改进:注册数据的按需订阅模式:Eureka的一个客户端可以选择它关注的注册信息(Eureka服务器全部注册信息的一部分),而Eureka服务器仅发送其关注的注册信息。例如:一个客户端可以说我只关注应用程序“WebFarm”,然后服务器将只发送WebFarm实例相关的信息。Eureka服务器提供多种注册信息选择的标准,同时提供动态更新客户端注册信息的方法;从服务端所有变更都推送改进为只推送客户端关注内容:不同于当前的客户端拉模式,Eureka服务器只推送客户端关注的变更信息;优化复制:与Eureka1.0一样,Eureka 2.0依然遵循广播复制模型,即每个节点将数据复制到所有其他节点。然而,复制算法更加优化,去掉了在注册中心每个实例发送心跳的需求,大大减少了复制流量,实现了更高的可伸缩性;自动扩展的Eureka服务器:Eureka2.0将读取(数据发现)和写入(注册)关注点划分为独立的集群。由于写入负载是可预测的(与区域中的实例数成比例),所以写入集群是提前计算好扩展能力进行部署。另一方面,读取负载是不可预测的(与客户端的订阅成比例),因此读取集群是自动扩展的;审计日志:Eureka2.0为注册信息所做的任何更改提供了详细的审计日志。这有助于Eureka的所有者和用户洞察了解Eureka中各个应用程序实例的状态。审计日志默认是保存在日志文件中,同时支持基于插件的方式适配不同的存储类型;仪表盘:Eureka2.0提供了一个丰富的仪表板(相对于Eureka 1.0的非常基本的仪表盘),可以查看注册信息视图、服务器运行状况、订阅状态和审核日志等信息 《Eureka 2.0 Motivations》的英文版本:https://github.com/Netflix/eureka/wiki/Eureka-2.0-Motivations","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"}]},{"title":"深入理解Ribbon","slug":"sc/sc-fzp-ribbon","date":"2017-07-08T04:23:31.000Z","updated":"2017-07-08T07:01:49.000Z","comments":true,"path":"sc/sc-fzp-ribbon/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-fzp-ribbon/","excerpt":"","text":"什么是RibbonRibbon是Netflix公司开源的一个负载均衡的项目,它属于上述的第二种,是一个客户端负载均衡器,运行在客户端上。它是一个经过了云端测试的IPC库,可以很好地控制HTTP和TCP客户端的一些行为。 Feign已经默认使用了Ribbon。 负载均衡 容错 多协议(HTTP,TCP,UDP)支持异步和反应模型 缓存和批处理 RestTemplate和Ribbon相结合Ribbon在Netflix组件是非常重要的一个组件,在Zuul中使用Ribbon做负载均衡,以及Feign组件的结合等。在Spring Cloud 中,作为开发中,做的最多的可能是将RestTemplate和Ribbon相结合,你可能会这样写: 12345678@Configurationpublic class RibbonConfig { @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); }} 消费另外一个的服务的接口,差不多是这样的: 123456789@Servicepublic class RibbonService { @Autowired RestTemplate restTemplate; public String hi(String name) { return restTemplate.getForObject("http://eureka-client/hi?name="+name,String.class); }} 深入理解RibbonLoadBalancerClient在Riibon中一个非常重要的组件为LoadBalancerClient,它作为负载均衡的一个客户端。它在spring-cloud-commons包下:的LoadBalancerClient是一个接口,它继承ServiceInstanceChooser,它的实现类是RibbonLoadBalancerClient,这三者之间的关系如下图: 其中LoadBalancerClient接口,有如下三个方法,其中excute()为执行请求,reconstructURI()用来重构url: 123456public interface LoadBalancerClient extends ServiceInstanceChooser { <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException; <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException; URI reconstructURI(ServiceInstance instance, URI original);} ServiceInstanceChooser接口,主要有一个方法,用来根据serviceId来获取ServiceInstance,代码如下: 1234public interface ServiceInstanceChooser { ServiceInstance choose(String serviceId);} LoadBalancerClient的实现类为RibbonLoadBalancerClient,这个类是非常重要的一个类,最终的负载均衡的请求处理,由它来执行。它的部分源码如下: 12345678910111213141516171819202122232425262728293031323334public class RibbonLoadBalancerClient implements LoadBalancerClient {...//省略代码@Override public ServiceInstance choose(String serviceId) { Server server = getServer(serviceId); if (server == null) { return null; } return new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server)); }protected Server getServer(String serviceId) { return getServer(getLoadBalancer(serviceId)); }protected Server getServer(ILoadBalancer loadBalancer) { if (loadBalancer == null) { return null; } return loadBalancer.chooseServer("default"); // TODO: better handling of key }protected ILoadBalancer getLoadBalancer(String serviceId) { return this.clientFactory.getLoadBalancer(serviceId); } ...//省略代码 在RibbonLoadBalancerClient的源码中,其中choose()方法是选择具体服务实例的一个方法。该方法通过getServer()方法去获取实例,经过源码跟踪,最终交给了ILoadBalancer类去选择服务实例。 ILoadBalancer在ribbon-loadbalancer的jar包下,它是定义了实现软件负载均衡的一个接口,它需要一组可供选择的服务注册列表信息,以及根据特定方法去选择服务,它的源码如下 : 12345678public interface ILoadBalancer { public void addServers(List<Server> newServers); public Server chooseServer(Object key); public void markServerDown(Server server); public List<Server> getReachableServers(); public List<Server> getAllServers();} 其中,addServers()方法是添加一个Server集合;chooseServer()方法是根据key去获取Server;markServerDown()方法用来标记某个服务下线;getReachableServers()获取可用的Server集合;getAllServers()获取所有的Server集合。 DynamicServerListLoadBalancer它的继承类为BaseLoadBalancer,它的实现类为DynamicServerListLoadBalancer,这三者之间的关系如下: 查看上述三个类的源码,可用发现,配置以下信息,IClientConfig、IRule、IPing、ServerList、ServerListFilter和ILoadBalancer,查看BaseLoadBalancer类,它默认的情况下,实现了以下配置: IClientConfig ribbonClientConfig: DefaultClientConfigImpl配置 IRule ribbonRule: RoundRobinRule 路由策略 IPing ribbonPing: DummyPing ServerList ribbonServerList: ConfigurationBasedServerList ServerListFilter ribbonServerListFilter: ZonePreferenceServerListFilter ILoadBalancer ribbonLoadBalancer: ZoneAwareLoadBalancer IClientConfig 用于对客户端或者负载均衡的配置,它的默认实现类为DefaultClientConfigImpl。 IRule用于复杂均衡的策略,它有三个方法,其中choose()是根据key 来获取server,setLoadBalancer()和getLoadBalancer()是用来设置和获取ILoadBalancer的,它的源码如下: 12345678public interface IRule{ public Server choose(Object key); public void setLoadBalancer(ILoadBalancer lb); public ILoadBalancer getLoadBalancer(); } IRule有很多默认的实现类,这些实现类根据不同的算法和逻辑来处理负载均衡。Ribbon实现的IRule有一下。在大多数情况下,这些默认的实现类是可以满足需求的,如果有特性的需求,可以自己实现。 BestAvailableRule 选择最小请求数 ClientConfigEnabledRoundRobinRule 轮询 RandomRule 随机选择一个server RoundRobinRule 轮询选择server RetryRule 根据轮询的方式重试 WeightedResponseTimeRule 根据响应时间去分配一个weight ,weight越低,被选择的可能性就越低 ZoneAvoidanceRule 根据server的zone区域和可用性来轮询选择 IPing是用来想server发生”ping”,来判断该server是否有响应,从而判断该server是否可用。它有一个isAlive()方法,它的源码如下: 123public interface IPing { public boolean isAlive(Server server);} IPing的实现类有PingUrl、PingConstant、NoOpPing、DummyPing和NIWSDiscoveryPing。它门之间的关系如下: PingUrl 真实的去ping 某个url,判断其是否alive PingConstant 固定返回某服务是否可用,默认返回true,即可用 NoOpPing 不去ping,直接返回true,即可用。 DummyPing 直接返回true,并实现了initWithNiwsConfig方法。 NIWSDiscoveryPing,根据DiscoveryEnabledServer的InstanceInfo的InstanceStatus去判断,如果为InstanceStatus.UP,则为可用,否则不可用。 ServerList是定义获取所有的server的注册列表信息的接口,它的代码如下: 123456public interface ServerList<T extends Server> { public List<T> getInitialListOfServers(); public List<T> getUpdatedListOfServers(); } ServerListFilter接口,定于了可根据配置去过滤或者根据特性动态获取符合条件的server列表的方法,代码如下: 12345public interface ServerListFilter<T extends Server> { public List<T> getFilteredListOfServers(List<T> servers);} 阅读DynamicServerListLoadBalancer的源码,DynamicServerListLoadBalancer的构造函数中有个initWithNiwsConfig()方法。在改方法中,经过一系列的初始化配置,最终执行了restOfInit()方法。其代码如下: 12345678910111213141516171819202122232425262728293031323334353637383940public DynamicServerListLoadBalancer(IClientConfig clientConfig) { initWithNiwsConfig(clientConfig); } @Override public void initWithNiwsConfig(IClientConfig clientConfig) { try { super.initWithNiwsConfig(clientConfig); String niwsServerListClassName = clientConfig.getPropertyAsString( CommonClientConfigKey.NIWSServerListClassName, DefaultClientConfigImpl.DEFAULT_SEVER_LIST_CLASS); ServerList<T> niwsServerListImpl = (ServerList<T>) ClientFactory .instantiateInstanceWithClientConfig(niwsServerListClassName, clientConfig); this.serverListImpl = niwsServerListImpl; if (niwsServerListImpl instanceof AbstractServerList) { AbstractServerListFilter<T> niwsFilter = ((AbstractServerList) niwsServerListImpl) .getFilterImpl(clientConfig); niwsFilter.setLoadBalancerStats(getLoadBalancerStats()); this.filter = niwsFilter; } String serverListUpdaterClassName = clientConfig.getPropertyAsString( CommonClientConfigKey.ServerListUpdaterClassName, DefaultClientConfigImpl.DEFAULT_SERVER_LIST_UPDATER_CLASS ); this.serverListUpdater = (ServerListUpdater) ClientFactory .instantiateInstanceWithClientConfig(serverListUpdaterClassName, clientConfig); restOfInit(clientConfig); } catch (Exception e) { throw new RuntimeException( "Exception while initializing NIWSDiscoveryLoadBalancer:" + clientConfig.getClientName() + ", niwsClientConfig:" + clientConfig, e); } } 在restOfInit()方法上,有一个 updateListOfServers()的方法,该方法是用来获取所有的ServerList的。 1234567891011121314void restOfInit(IClientConfig clientConfig) { boolean primeConnection = this.isEnablePrimingConnections(); // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList() this.setEnablePrimingConnections(false); enableAndInitLearnNewServersFeature(); updateListOfServers(); if (primeConnection && this.getPrimeConnections() != null) { this.getPrimeConnections() .primeConnections(getReachableServers()); } this.setEnablePrimingConnections(primeConnection); LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString()); } 进一步跟踪updateListOfServers()方法的源码,最终由serverListImpl.getUpdatedListOfServers()获取所有的服务列表的,代码如下: 12345678910111213141516@VisibleForTesting public void updateListOfServers() { List<T> servers = new ArrayList<T>(); if (serverListImpl != null) { servers = serverListImpl.getUpdatedListOfServers(); LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", getIdentifier(), servers); if (filter != null) { servers = filter.getFilteredListOfServers(servers); LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", getIdentifier(), servers); } } updateAllServerList(servers); } 而serverListImpl是ServerList接口的具体实现类。跟踪代码,ServerList的实现类为DiscoveryEnabledNIWSServerList,在ribbon-eureka.jar的com.netflix.niws.loadbalancer下。其中DiscoveryEnabledNIWSServerList有 getInitialListOfServers()和getUpdatedListOfServers()方法,具体代码如下: 123456789@Override public List<DiscoveryEnabledServer> getInitialListOfServers(){ return obtainServersViaDiscovery(); } @Override public List<DiscoveryEnabledServer> getUpdatedListOfServers(){ return obtainServersViaDiscovery(); } 继续跟踪源码,obtainServersViaDiscovery(),是根据eurekaClientProvider.get()来回去EurekaClient,再根据EurekaClient来获取注册列表信息,代码如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546private List<DiscoveryEnabledServer> obtainServersViaDiscovery() { List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>(); if (eurekaClientProvider == null || eurekaClientProvider.get() == null) { logger.warn("EurekaClient has not been initialized yet, returning an empty list"); return new ArrayList<DiscoveryEnabledServer>(); } EurekaClient eurekaClient = eurekaClientProvider.get(); if (vipAddresses!=null){ for (String vipAddress : vipAddresses.split(",")) { // if targetRegion is null, it will be interpreted as the same region of client List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion); for (InstanceInfo ii : listOfInstanceInfo) { if (ii.getStatus().equals(InstanceStatus.UP)) { if(shouldUseOverridePort){ if(logger.isDebugEnabled()){ logger.debug("Overriding port on client name: " + clientName + " to " + overridePort); } // copy is necessary since the InstanceInfo builder just uses the original reference, // and we don't want to corrupt the global eureka copy of the object which may be // used by other clients in our system InstanceInfo copy = new InstanceInfo(ii); if(isSecure){ ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build(); }else{ ii = new InstanceInfo.Builder(copy).setPort(overridePort).build(); } } DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr); des.setZone(DiscoveryClient.getZone(ii)); serverList.add(des); } } if (serverList.size()>0 && prioritizeVipAddressBasedServers){ break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers } } } return serverList; } 其中eurekaClientProvider的实现类是LegacyEurekaClientProvider,它是一个获取eurekaClient类,通过静态的方法去获取eurekaClient,其代码如下: 12345678910111213class LegacyEurekaClientProvider implements Provider<EurekaClient> { private volatile EurekaClient eurekaClient; @Override public synchronized EurekaClient get() { if (eurekaClient == null) { eurekaClient = DiscoveryManager.getInstance().getDiscoveryClient(); } return eurekaClient; }} EurekaClient的实现类为DiscoveryClient,在之前已经分析了它具有服务注册、获取服务注册列表等的全部功能。 由此可见,负载均衡器是从EurekaClient获取服务信息,并根据IRule去路由,并且根据IPing去判断服务的可用性。 那么现在还有个问题,负载均衡器多久一次去获取一次从Eureka Client获取注册信息呢。 在BaseLoadBalancer类下,BaseLoadBalancer的构造函数,该构造函数开启了一个PingTask任务,代码如下: 123456public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats, IPing ping, IPingStrategy pingStrategy) { ...//代码省略 setupPingTask(); ...//代码省略 } setupPingTask()的具体代码逻辑,它开启了ShutdownEnabledTimer执行PingTask任务,在默认情况下pingIntervalSeconds为10,即每10秒钟,想EurekaClient发送一次”ping”。 123456789101112void setupPingTask() { if (canSkipPing()) { return; } if (lbTimer != null) { lbTimer.cancel(); } lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name, true); lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000); forceQuickPing();} PingTask源码,即new一个Pinger对象,并执行runPinger()方法。 123456789class PingTask extends TimerTask { public void run() { try { new Pinger(pingStrategy).runPinger(); } catch (Exception e) { logger.error("LoadBalancer [{}]: Error pinging", name, e); } } } 查看Pinger的runPinger()方法,最终根据 pingerStrategy.pingServers(ping, allServers)来获取服务的可用性,如果该返回结果,如之前相同,则不去向EurekaClient获取注册列表,如果不同则通知ServerStatusChangeListener或者changeListeners发生了改变,进行更新或者重新拉取。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556public void runPinger() throws Exception { if (!pingInProgress.compareAndSet(false, true)) { return; // Ping in progress - nothing to do } // we are "in" - we get to Ping Server[] allServers = null; boolean[] results = null; Lock allLock = null; Lock upLock = null; try { /* * The readLock should be free unless an addServer operation is * going on... */ allLock = allServerLock.readLock(); allLock.lock(); allServers = allServerList.toArray(new Server[allServerList.size()]); allLock.unlock(); int numCandidates = allServers.length; results = pingerStrategy.pingServers(ping, allServers); final List<Server> newUpList = new ArrayList<Server>(); final List<Server> changedServers = new ArrayList<Server>(); for (int i = 0; i < numCandidates; i++) { boolean isAlive = results[i]; Server svr = allServers[i]; boolean oldIsAlive = svr.isAlive(); svr.setAlive(isAlive); if (oldIsAlive != isAlive) { changedServers.add(svr); logger.debug("LoadBalancer [{}]: Server [{}] status changed to {}", name, svr.getId(), (isAlive ? "ALIVE" : "DEAD")); } if (isAlive) { newUpList.add(svr); } } upLock = upServerLock.writeLock(); upLock.lock(); upServerList = newUpList; upLock.unlock(); notifyServerStatusChangeListener(changedServers); } finally { pingInProgress.set(false); } } 由此可见,LoadBalancerClient是在初始化的时候,会向Eureka回去服务注册列表,并且向通过10s一次向EurekaClient发送“ping”,来判断服务的可用性,如果服务的可用性发生了改变或者服务数量和之前的不一致,则更新或者重新拉取。LoadBalancerClient有了这些服务注册列表,就可以根据具体的IRule来进行负载均衡。 RestTemplate是如何和Ribbon结合的最后,回答问题的本质,为什么在RestTemplate加一个@LoadBalance注解就可可以开启负载均衡呢? 1234@LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } 全局搜索ctr+shift+f @LoadBalanced有哪些类用到了LoadBalanced有哪些类用到了, 发现LoadBalancerAutoConfiguration类,即LoadBalancer自动配置类。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253@Configuration@ConditionalOnClass(RestTemplate.class)@ConditionalOnBean(LoadBalancerClient.class)@EnableConfigurationProperties(LoadBalancerRetryProperties.class)public class LoadBalancerAutoConfiguration {@LoadBalanced @Autowired(required = false) private List<RestTemplate> restTemplates = Collections.emptyList();} @Bean public SmartInitializingSingleton loadBalancedRestTemplateInitializer( final List<RestTemplateCustomizer> customizers) { return new SmartInitializingSingleton() { @Override public void afterSingletonsInstantiated() { for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { for (RestTemplateCustomizer customizer : customizers) { customizer.customize(restTemplate); } } } }; } @Configuration @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") static class LoadBalancerInterceptorConfig { @Bean public LoadBalancerInterceptor ribbonInterceptor( LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) { return new LoadBalancerInterceptor(loadBalancerClient, requestFactory); } @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); } }; } }} 在该类中,首先维护了一个被@LoadBalanced修饰的RestTemplate对象的List,在初始化的过程中,通过调用customizer.customize(restTemplate)方法来给RestTemplate增加拦截器LoadBalancerInterceptor。 而LoadBalancerInterceptor,用于实时拦截,在LoadBalancerInterceptor这里实现来负载均衡。LoadBalancerInterceptor的拦截方法如下: 12345678@Override public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { final URI originalUri = request.getURI(); String serviceName = originalUri.getHost(); Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution)); } 总结综上所述,Ribbon的负载均衡,主要通过LoadBalancerClient来实现的,而LoadBalancerClient具体交给了ILoadBalancer来处理,ILoadBalancer通过配置IRule、IPing等信息,并向EurekaClient获取注册列表的信息,并默认10秒一次向EurekaClient发送“ping”,进而检查是否更新服务列表,最后,得到注册列表后,ILoadBalancer根据IRule的策略进行负载均衡。 而RestTemplate 被@LoadBalance注解后,能过用负载均衡,主要是维护了一个被@LoadBalance注解的RestTemplate列表,并给列表中的RestTemplate添加拦截器,进而交给负载均衡器去处理。 转载请标明出处:http://blog.csdn.net/forezp/article/details/74820899本文出自方志朋的博客","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Ribbon","slug":"Spring-Cloud-Ribbon","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Ribbon/"}]},{"title":"Spring Cloud Gateway离开孵化器的变化","slug":"sc/sc-gateway","date":"2017-07-08T01:21:26.000Z","updated":"2017-07-08T04:20:17.000Z","comments":true,"path":"sc/sc-gateway/","link":"http://xujin.org/sc/sc-gateway/","permalink":"http://blog.springcloud.cn/sc/sc-gateway/","excerpt":"","text":"","categories":[{"name":"Spring Cloud Gateway","slug":"Spring-Cloud-Gateway","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Gateway/"}],"tags":[{"name":"Spring Cloud Gateway","slug":"Spring-Cloud-Gateway","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Gateway/"}]},{"title":"如何管理数百个微服务并避免踩坑?","slug":"sc/sc-mdx","date":"2017-07-05T01:21:26.000Z","updated":"2017-07-05T10:46:13.000Z","comments":true,"path":"sc/sc-mdx/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-mdx/","excerpt":"","text":"过去两年中,微服务架构是一个非常热门的技术名词。秦苍科技也在微服务方面做了大量的投资和实践,我们有开发、测试、准生产、生产四套环境,每套环境有230+个微服务,总共有近1000个微服务。 秦苍科技为什么要采用微服务的架构?如何管理这么多微服务?本文将对这些问题进行阐述,希望对正在踩坑路上和即将踩坑的朋友们有所帮助。 作者介绍:秦苍科技高级技术总监兼首席架构师。复旦大学计算机博士。曾在Autodesk、SAP、Blackboard等公司担任首席工程师、架构师、高级经理等职位。在国内、国际权威杂志和会议上发表十余篇论文,出版过一本著作。十几年来在云计算、计算机辅助设计、自然语言处理、搜索引擎、数据挖掘、人工智能、机器学习、微服务架构等领域均有涉猎。 1.为什么要使用微服务关于微服务架构优点有很多讨论。但是,个人认为许多优点都可以算作一些“伪优点”。例如: 从单个服务的角度而言,微服务的每个服务都很简单,只关注于一个业务功能,降低了单个服务的复杂性。但是,从整体而言,作为一种分布式系统,微服务引入额外的复杂性和问题,比如说网络延迟、容错性、异步、分布式事务等。 从单个服务的角度而言,每个微服务可以通过不同的编程语言与工具进行开发,针对不同的服务采用更加合适的技术,也可以快速地尝试一些新技术。 但是,从整个公司的角度来说,往往希望能够尽量统一技术栈,避免重复投资某些技术。假设某公司主要用Spring Boot和Spring Cloud来构建微服务,使用Netflix Hystrix作为服务熔断的解决方案。后来,一些微服务开始使用Node.js来实现。很快,该公司就发觉使用Node.js构建的服务无法使用已有的服务熔断解决方案,需要为Node.js技术栈重新开发。 …… 我的观点是微服务架构的核心就是解决扩展性的问题。从组织结构的角度来看,微服务架构使得研发部门可以快速扩张,因为每个微服务都不是特别复杂,工作在这个服务上的研发人员不是必须对整个系统都充分了解,很多新人可以快速上手。 从技术的角度来看,微服务架构使得每个微服务可以独立部署、独立扩展,可以根据每个服务的规模来部署满足需求的规模,选择更适合于服务资源需求的硬件。 秦苍科技正处在一个人员规模和业务规模快速扩张的阶段,微服务的扩展性非常贴切地满足了我们现阶段的需求,所以使用微服务架构对秦苍科技来说也变成了一件顺理成章的事情了。 2.如何进行服务管理随着服务数量的增多,我们发觉微服务间的依赖关系越来越复杂,一个服务的改变将会波及多个服务,错误排查也相当困难。当系统有几百个服务时,这种依赖简直就是一个噩梦。 所以,秦苍科技启动了服务治理的项目,使用服务注册和发现技术简化服务的管理,对服务进行了分组、分层,降低系统的复杂性和耦合性。 其实,服务的管理和人员组织结构的管理非常类似。当一个组织中成员增多时,我们会将人员分为若干个小的团队,每个团队由较少的人员组成,负责某个比较独立的业务,并且会有一个团队负责人负责和其他团队的沟通。 当组织中的成员进一步增多时,我们会将若干个团队合并为一个部门,每个部门负责某个独立的职能。 对于微服务的管理,我们采用与组织结构管理类似的方法,把彼此紧密相关的服务构建成逻辑上的一个组。类似于组织结构中的团队负责人,该组有一个API网关,向外暴露了组中所有服务的功能。对于该组中服务的使用方来说,都通过这个API网关进行访问,仿佛这个组就是一个服务一样,无需关心该组是由多少个服务组成。 通过分组的方式,秦苍230+个微服务变为了25个组,从而大大降低了系统逻辑上的复杂性。然后,我们把系统分为了若干层,每一层由若干个组组成。上层只可以调用下层的服务,下层不可以调用上层服务。通过分层的方式,我们降低了系统的耦合性。 图1服务分层的组织方式 3.如何让服务管理自动化?在人员组织结构管理中,为了高效地管理人员的信息,通常会引入一套系统管理这些信息,例如:微软的Active Directory。 当一个新员工入职的时候,我们会在这个系统中添加该成员的基本信息,例如:姓名、电话、email等信息。 在一个员工离职的时候,我们会在这个系统中删除该员工。当一个员工A要和员工B沟通交流的时候,员工A可以根据员工B的姓名在这个系统中查询出员工B的电话、email等信息,然后使用电话或email和员工B进行沟通。 对于微服务的管理,我们也希望有这样一个系统,能够注册和查询微服务的所有信息。微服务架构中把这个系统叫做服务注册中心。 秦苍科技采用了Netflix Eureka作为我们的服务注册中心,所有的微服务都基于Spring Boot和Spring Cloud进行构建。 系统中的每个服务都非常“聪明”,在启动后都会跑到服务注册中心“自报家门”,告诉服务注册中心自己的名字、IP地址、版本、状态、所属组、所属层、所属层的级别、依赖的微服务等信息,服务注册中心会将这些服务保存到它的“花名册”上。 通过服务注册中心的“花名册”,我们可以对系统一览无遗,轻松了解系统的每一层,每一层中所有组,每个组中所有服务的信息。 当服务A依赖于服务B时,它只要知道服务B的名称,就可以从服务注册中心的“花名册”中查询到服务B的所有实例,及其相关的所有信息,例如IP地址、所属组、所属层等信息。 这样,服务A不再需要“死记硬背”服务B的IP地址。当服务A调用服务B的时候,服务A首先从服务注册中心获取到服务B的所有实例,然后服务A采用某种策略从服务B的实例中选择其中一个实例将请求发送给该实例,从而实现了客户端负载均衡。 这个客户端负载均衡的功能就是由图2中的Netflix Ribbon模块来完成的。 图2服务注册和发现机制 4.如何自动控制服务依赖?使用前述的方法后,我们可以使用分组分层的方式对服务进行管理。但是,我们仍然需要一定的技术手段来保证开发人员在开发某个微服务的时候一定会遵守下层服务不能调用上层服务的原则,保证开发人员不会引入不该引入的微服务依赖。 秦苍实现自动控制服务依赖的核心是注册服务依赖信息到服务注册中心,扩展Netflix Ribbon限制服务调用。 使用前述的方法后,我们可以使用分组分层的方式对服务进行管理。但是,我们仍然需要一定的技术手段来保证开发人员在开发某个微服务的时候一定会遵守下层服务不能调用上层服务的原则,保证开发人员不会引入不该引入的微服务依赖。 秦苍实现自动控制服务依赖的核心是注册服务依赖信息到服务注册中心,扩展Netflix Ribbon限制服务调用。 1.服务注册依赖信息到注册中心 每个微服务都会注册该服务所属层、所属层级、依赖的微服务等信息到服务注册中心。这些服务依赖信息作为服务的配置项,保存在配置文件中,统一由运维人员管理。开发人员在开发环境中可以修改这些服务依赖信息,进行开发调试。 但是,在测试、准生产和生产环境中,运维人员会使用自己管理的配置项覆盖掉这些信息,运维只有在经过架构师同意后才会修改这些服务依赖信息。 这就意味着开发无法绕过架构师自行引入新的依赖,否则在测试、准生产和生产环境中服务是调不通的,代码无法正常工作,这样就从技术手段上保证了无法随意地引入微服务依赖。 2.扩展Netflix Ribbon限制服务调用 有了服务依赖信息后,服务调用时我们需要使用这些信息限制不允许的服务调用。只要对Ribbon进行少许扩展就可以满足这样的需求。 本质来讲,Ribbon就是一个服务调用的“路由器”。只要在这个“路由器”上定义一些新的规则,我们就可以控制服务的调用关系。例如: 白名单规则:对于一个微服务,只能调用在该服务白名单列表中的服务,否则调用失败。 层级调用规则:对于一个微服务,只能调用层级比自己低的服务,否则调用失败。 …… 目前,秦苍科技对Spring Boot Admin进行了扩展来构建自己的服务治理中心,用户可以按照组的方式浏览服务,查看每个服务的健康状态、配置信息、日志等。 图3服务治理中心 将来,我们打算通过读取服务注册中心中每个服务注册的元信息,在服务治理中心中自动画出整个系统的架构图。关于自动管控服务依赖方面,我们的工作还在进行中。 希望将来有一天,我们在微服务治理方面的积累足够成熟,可以将这些经验回馈给开源社区。","categories":[{"name":"微服务","slug":"微服务","permalink":"http://blog.springcloud.cn/categories/微服务/"}],"tags":[{"name":"微服务","slug":"微服务","permalink":"http://blog.springcloud.cn/tags/微服务/"}]},{"title":"Feign使用性能优化","slug":"sc/sc-tt-feign","date":"2017-07-02T06:16:20.000Z","updated":"2017-07-01T14:31:59.000Z","comments":true,"path":"sc/sc-tt-feign/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-tt-feign/","excerpt":"","text":"Feign正确的使用方法和性能优化注意事项1. feign自定义Configuration和root 容器有效隔离。 用@Configuration注解 不能在主@ComponentScan (or @SpringBootApplication)范围内,从其包名上分离 注意避免包扫描重叠,最好的方法是明确的指定包名 2. Spring Cloud Netflix 提供了默认的Bean类型: Decoder feignDecoder: ResponseEntityDecoder (which wraps a SpringDecoder) Encoder feignEncoder: SpringEncoder Logger feignLogger: Slf4jLogger Contract feignContract: SpringMvcContract Feign.Builder feignBuilder: HystrixFeign.Builder 3. Spring Cloud Netflix没有提供默认值,但仍然可以在feign上下文配置中创建: Logger.Level Retryer ErrorDecoder Request.Options Collection 4. 自定义feign的消息编码解码器:不要在如下代码中getObject方法内new 对象,外部会频繁调用getObject方法。 123456ObjectFactory<HttpMessageConverters> messageConvertersObjectFactory = new ObjectFactory<HttpMessageConverters>() {@Overridepublic HttpMessageConverters getObject() throws BeansException { return httpMessageConverters;}}; 5. 注意测试环境和生产环境,注意正确使用feign日志级别。6. apacheHttpclient或者其他client的正确配置: apacheHttpclient自定义配置放在spring root context,不要在FeignContext,否则不会起作用。 apacheHttpclient 连接池配置合理地连接和其他参数 7. Feign配置12345678910111213#Hystrix支持,如果为true,hystrix库必须在classpath中feign.hystrix.enabled=false #请求和响应GZIP压缩支持feign.compression.request.enabled=truefeign.compression.response.enabled=true#支持压缩的mime typesfeign.compression.request.enabled=truefeign.compression.request.mime-types=text/xml,application/xml,application/jsonfeign.compression.request.min-request-size=2048# 日志支持logging.level.project.user.UserClient: DEBUG 8. Logger.Level支持必须为每一个Feign Client配置来告诉Feign如何输出日志,可选: NONE, No logging (DEFAULT). BASIC, Log only the request method and URL and the response status code and execution time. HEADERS, Log the basic information along with request and response headers. FULL, Log the headers, body, and metadata for both requests and responses. 9. FeignClient.fallback 正确的使用方法配置的fallback class也必须在FeignClient Configuration中实例化,否则会报 java.lang.IllegalStateException: No fallback instance of type class异常。 例子: 12345678910111213141516171819202122232425@FeignClient(name = \"hello\", fallback = HystrixClientFallback.class)public interface HystrixClient { @RequestMapping(method = RequestMethod.GET, value = \"/hello\") Hello iFailSometimes();}public class HystrixClientFallback implements HystrixClient { @Override public Hello iFailSometimes() { return new Hello(\"fallback\"); }}@Configurationpublic class FooConfiguration { @Bean @Scope(\"prototype\") public Feign.Builder feignBuilder() { return Feign.builder(); } @Bean public HystrixClientFallback fb(){ return new HystrixClientFallback(); } } 10. 使用Feign Client 和@RequestMapping时,注意事项当前工程中有和Feign Client中一样的Endpoint时,Feign Client的类上不能用@RequestMapping注解否则,当前工程该endpoint http请求且使用accpet时会报404. 下面的例子: 有一个 Controller 12345678910111213@RestController@RequestMapping(\"/v1/card\")public class IndexApi { @PostMapping(\"balance\") @ResponseBody public Info index() { Info.Builder builder = new Info.Builder(); builder.withDetail(\"x\", 2); builder.withDetail(\"y\", 2); return builder.build(); }} 有一个Feign Client 12345678910111213@FeignClient( name = \"card\", url = \"http://localhost:7913\", fallback = CardFeignClientFallback.class, configuration = FeignClientConfiguration.class)@RequestMapping(value = \"/v1/card\")public interface CardFeignClient { @RequestMapping(value = \"/balance\", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) Info info();} if @RequestMapping is used on class, when invoke http /v1/card/balance, like this : 如果 @RequestMapping注解被用在FeignClient类上,当像如下代码请求/v1/card/balance时,注意有Accept header: 1234Content-Type: application/jsonAccept: application/jsonPOST http://localhost:7913/v1/card/balance 那么会返回 404。 如果不包含Accept header时请求,则是OK: 12Content-Type:application/jsonPOST http://localhost:7913/v1/card/balance 或者像下面不在Feign Client上使用@RequestMapping注解,请求也是ok,无论是否包含Accept: 123456789101112@FeignClient( name = \"card\", url = \"http://localhost:7913\", fallback = CardFeignClientFallback.class, configuration = FeignClientConfiguration.class)public interface CardFeignClient { @RequestMapping(value = \"/v1/card/balance\", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) Info info();} 转载请标明出处:版权归铁汤作者本文出自Spring Cloud中国社区会员-铁汤博客","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Feign使用","slug":"Spring-Cloud-Feign使用","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Feign使用/"}]},{"title":"微服务实施Spring Cloud中踩过的坑","slug":"sc/sc-tt1","date":"2017-07-01T09:06:02.000Z","updated":"2017-07-01T14:31:45.000Z","comments":true,"path":"sc/sc-tt1/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-tt1/","excerpt":"","text":"1.注册IP问题早期的Spring Cloud Eureka在注册获取网卡IP时,不能区分外网网卡和内网网卡,如果安装了虚拟机和docker也不能区分虚拟网卡,每次启动注册的IP都有可能不一样,如果要注册为外网网卡IP,那运行带宽就不够,这个bug应该说是比较严重的问题,因此重写了网卡IP获取的逻辑来解决,同时也反馈给了spring cloud团队,再后期的版本中添加了网卡接口排序和通过名称过滤的功能来得到解决。 2.HealthCheck的问题在一些极小概率的情况下,会导致Eureka Server 下线微服务实例,出现“Remote status from Eureka server is down”的问题,即便是重启微服务也无济于事,不过已经有码友在spring cloud 官方github贴出了解决方法的issue。 3.Zookeeper版本带来的性能问题现象是一个团队在实施微服务时,发现部署到服务器上的微服务,在没有任何请求时,仍然CPU占用20~30%;匪夷所思的是,同样的微服务包在本地开发机器、本地物理机、本地虚拟机运行并未出现。通过jvisualvm观察,如下图: 可以看到和Zookeeper有关,同时我的另一个Demo微服务在任何环境下却未出现该问题。比较jar中依赖包之后发现,2个包中唯一不一样的是Apache Curator版本,一个是2.8.0, spring cloud默认依赖的版本;没问题的是Apache Curator 2.9.1。Apache Curator 2.8.0 BUG!!!在Apache Curator 2.9.1 Release Notes 12332392>也发现了对此bug的描述。升级到2.9.1问题解决,皆大欢喜。同时上报此bug给Spring Cloud团队,升级Apache Curator版本。 4.Feign使用不当带来的性能问题Feign在Spring容器中使用了独立应用上下文,要注意命名空间上隔离,详情参考:Feign正确的使用方法和性能优化注意事项 |http://www.jianshu.com/p/191d45210d16来绕过坑。 这几个坑是不能容忍的,踩过其他小坑就不计其数了,但都不伤大雅,可以替代。 转载请标明出处:版权归铁汤作者本文出自Spring Cloud中国社区会员-铁汤博客","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"微服务之Spring Cloud分布式外部化和中心化配置管理","slug":"sc/sc-tt-config","date":"2017-07-01T09:00:00.000Z","updated":"2017-07-01T14:30:33.000Z","comments":true,"path":"sc/sc-tt-config/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-tt-config/","excerpt":"","text":"1.背景微服务化之后,应用的数量剧增,零时需要调整配置参数时,无论是运维直接在服务器上修改还是工程中修改配置后重新打包部署,对运维来说工作量是巨大的,而且人为的操作会加大出错的几率,那么外化和中心化配置可以更好的解决分布式环境的配置问题。 2.Spring Cloud提供了2种方式的外化配置 Spring Cloud Config 通过本地文件系统,git/svn仓库来管理配置文件,可以满足基本外化需求,但不能精细的管理配置项。 Spring Cloud Zookeeper Config 通过Zookeeper分级命名空间来储存配置项数据,并且支持基础上下文和profile命名空间,另外Zookeeper可以实时监听节点变化和通知机制,应该是首选。 3.Spring Cloud Zookeeper Config提供的功能 和Spring Boot无缝集成,能完全无缝替代properties或yml文件的配置。 支持默认上下文命名空间。 支持profile命名空间。 支持应用名称命名空间。 命名空间上支持配置的继承。 支持更改实时通知和endpoint /refresh被动刷新,该特性不太好用。 Spring Cloud Zookeeper Config 基本能满足要求了,但其实时通知机制会造成应用暂停体验不好,需要初始化操作的配置(比如数据库连接池,http连接池等)实时更新的意义不大。需要实时更新的是那些在运行时从配置缓存中实时获取的参数,因此在此基础上增加基于Spring Cloud Zookeeper Config和CuratorFramework的实时通知组件,基本设计思路是这样的: 每一个需要动态更新的节点下增加push_status节点,任意值。 监听push_status节点的值变事件(NodeDataChanged)。 当修改push_status节点值时,会通知所有监听该节点的应用端。 应用端收到NodeDataChanged事件,递归获取push_status节点下所有的节点数据,并更新配置缓存。 转载请标明出处:版权归铁汤作者本文出自Spring Cloud中国社区会员-铁汤博客","categories":[{"name":"微服务","slug":"微服务","permalink":"http://blog.springcloud.cn/categories/微服务/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"微服务之Eureka服务发现","slug":"sc/sc-tt-eureka","date":"2017-07-01T08:51:48.000Z","updated":"2017-07-01T14:31:20.000Z","comments":true,"path":"sc/sc-tt-eureka/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-tt-eureka/","excerpt":"","text":"1.背景当调用API或者发起网络通信的时候,无论如何我们都要知道被调用方的IP和服务端口,大部分情况是通过域名和服务端口,事实上基于DNS的服务发现,因为DNS缓存、无法自治和其他不利因素的存在,有很多局限。传统的DNS方式,都是通过nginx或者其他代理软件来实现,物理机器的ip和port都是固定的,那么nginx中配置的服务ip和port也是固定的,服务列表的更新只能通过手动来做,但如果后端服务很多时,手动更新容易出错,效率也很低,这在后端服务发生故障时,不可用时间就可能会加长。在微服务中,尤其是使用了Docker等虚拟化技术的微服务,其IP和port都是动态分配的,服务实例数也是动态变化的,那么就需要精细而准确的服务发现机制。当微服务app启动后,告诉其他服务自己的ip和端口,这里的其他服务就是Eureka Server和Eureka Client,这样其他服务就知道这个服务有多少实例在线,都在哪些地方,方便去负载均衡和调用。 2.Eureka简介Eureka属于客户端发现模式,客户端负责决定相应服务实例的网络位置,并且对请求实现负载均衡。客户端从一个服务注册服务中查询所有可用服务实例的库,并缓存到本地。服务调用时,客户端使用负载均衡算法从多个后端服务实例中选择出一个,然后发出请求。Eureka分为Eureka Server和Eureka client, Eureka Server是一个服务注册中心,为服务实例注册管理和查询可用实例提供了REST API,并可以用其定位、负载均衡、故障恢复后端服务的中间层服务。在服务启动后,Eureka Client向服务注册中心注册服务同时会拉去注册中心注册表副本;在服务停止的时候,Eureka Client向服务注册中心注销服务;服务注册后,Eureka Client会定时的发送心跳来刷新服务的最新状态。 客户端发现模式的优点是服务调用、负载均衡不需要和Eureka Server通信,直接使用本地注册表副本,因此Eureka Server不可用时是不会影响正常的服务调用,性能也不会因为网络延迟和服务端延迟受到影响。但其缺点也很明显,但某个服务不可用时,各个Eureka Client不能及时的知道,需要1~3个心跳周期才能感知,但是,由于基于Netflix的服务调用端都会使用Hystrix来容错和降级,当服务调用不可用时Hystrix也能及时感知到,通过熔断机制来降级服务调用,因此弥补了基于客户端服务发现的时效性的缺点。 Eureka Server采用的是对等通信(P2P),无中心化的架构,无master/slave区分,每一个server都是对等的,既是Server又是Client,所以其集群方式可以自由发挥,可以各点互连,也可以接力互连。Eureka Server通过运行多个实例以及彼此之间互相注册来提高可用性,每个节点需要添加一个或多个有效的serviceUrl指向另一个节点。利用Eureka Server这种架构特性, 我在Eureka Server Cluster的部署时采用了三角形通信模型,三角形是一个很好的均衡模型,既是各点互连,又是接力互连,三角形本身就是一个稳定性几何形状,有着稳固、坚定搜索、耐压的特点,家具、建筑、交通等各种行业都有应用。如下图所示,Eureka Cluster的每个实例都和另外2个实例通信交互。 转载请标明出处:版权归铁汤作者本文出自Spring Cloud中国社区会员-铁汤博客","categories":[{"name":"微服务","slug":"微服务","permalink":"http://blog.springcloud.cn/categories/微服务/"}],"tags":[{"name":"Eureka Spring Cloud","slug":"Eureka-Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Eureka-Spring-Cloud/"}]},{"title":"微服务之Eureka Server原理","slug":"sc/sc-tt-eureka-server","date":"2017-06-30T09:23:06.000Z","updated":"2017-07-01T09:24:11.000Z","comments":true,"path":"sc/sc-tt-eureka-server/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-tt-eureka-server/","excerpt":"","text":"1.背景介绍Eureka的相关知识,在之前的《微服务之Eureka服务发现》中已经讲了很多,这里不再重复,本文主要通过Eureka Server源码和配置来阐述Eureka Server的工作原理。 Eureka提供了一系列REST的API,供Eureka Client来调用,实现服务注册,注销,心跳,状态更新等等操作,参考官网EurekaREst操作。 123456REST API <Jersey>Response Cache <com.google.common.cache.LoadingCache>InstanceRegistry <ConcurrentHashMap>EvictionTimer<java.util.Timer>CacheUpdateTask<java.util.Timer> 2.REST API基于Jersey实现,主要以appId[appname]和instanceId为操作维度,内容可以是xml或者json。相关的实现可以在com.netflix.eureka.resources包中找到。基于appId和instanceId和各种操作组合的实现,可以认为是InstanceRegistry的操作入口。 3.InstanceRegistryRegistry是Eureka Server的核心,服务发现就是围绕Registry来实现。以下提到的类都可以在com.netflix.eureka.registry包中找到。 整个Registry由4个接口组成: LeaseManager: register,cacel,renew,evict等基本操作 LookupService: 这个抽象是要是client和server端共用,EurekaClient也继承了该接口,主要用来查找服务和服务实例。 InstanceRegistry: 提供了实例相关的丰富的操作。 PeerAwareInstanceRegistry: eureka server之间的注册信息复制 如下是类关系图: 4.几个重要的定时任务心跳补偿任务EvictionTask1[com.netflix.eureka.registry.AbstractInstanceRegistry.EvictionTask] 主要是用来做心跳补偿,目的是用来取消或清理过期的注册信息,通常是eureka client在停止前未成功发送cacel请求。例如eureka client停止时网络不通了、eureka client进程奔溃了等等。 这个定时任务是通过eureka.server.eviction-interval-timer-in-ms参数来配置处理间隔,默认是60s。 补偿时间=当前时间-该任务最后执行时间-执行间隔 其中该任务最后执行时间-执行间隔主要是计算出实际执行时间的细微差别,也为后面的补偿时间>0埋下伏笔。 补偿时间>0或者最后更新时间 + leaseDuration[默认是90s,客户端durationInSecs]+补偿时间<当前时间 时会执行evict清理过期实例。 补偿时间>0, 即就是当前任务执行和上次执行的时间间隔大于配置的时间间隔,正常情况补偿时间应该很小。 最后更新时间+leaseDuration+补偿时间<当前时间,即就是最后一次成功心跳到当前时间的间隔比eureka client配置的间隔大。 其中leaseDuration: 在eureka client中通过eureka.instance.lease-expiration-duration-in-seconds[leaseExpirationDurationInSeconds]参数来配置,默认是90s。 下面是源代码: 计算补偿时间: 1234567891011long getCompensationTimeMs() { long currNanos = getCurrentTimeNano(); long lastNanos = lastExecutionNanosRef.getAndSet(currNanos); if (lastNanos == 0l) { return 0l; } long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos); long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs(); return compensationTime <= 0l ? 0l : compensationTime; } 这个是补偿清理逻辑: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public void evict(long additionalLeaseMs) { logger.debug(\"Running the evict task\"); if (!isLeaseExpirationEnabled()) { logger.debug(\"DS: lease expiration is currently disabled.\"); return; } // We collect first all expired items, to evict them in random order. For large eviction sets, // if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it, // the impact should be evenly distributed across all applications. List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>(); for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) { Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue(); if (leaseMap != null) { for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) { Lease<InstanceInfo> lease = leaseEntry.getValue(); if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) { expiredLeases.add(lease); } } } } // To compensate for GC pauses or drifting local time, we need to use current registry size as a base for // triggering self-preservation. Without that we would wipe out full registry. int registrySize = (int) getLocalRegistrySize(); int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold()); int evictionLimit = registrySize - registrySizeThreshold; int toEvict = Math.min(expiredLeases.size(), evictionLimit); if (toEvict > 0) { logger.info(\"Evicting {} items (expired={}, evictionLimit={})\", toEvict, expiredLeases.size(), evictionLimit); Random random = new Random(System.currentTimeMillis()); for (int i = 0; i < toEvict; i++) { // Pick a random item (Knuth shuffle algorithm) int next = i + random.nextInt(expiredLeases.size() - i); Collections.swap(expiredLeases, i, next); Lease<InstanceInfo> lease = expiredLeases.get(i); String appName = lease.getHolder().getAppName(); String id = lease.getHolder().getId(); EXPIRED.increment(); logger.warn(\"DS: Registry: expired lease for {}/{}\", appName, id); internalCancel(appName, id, false); } } } 从上面的逻辑上看,配置的时间间隔有2个: eureka server端:eviction-interval-timer-in-ms eureka client端:lease-expiration-duration-in-seconds 这2个参数同时作用着清理逻辑,配置时就要注意,eviction-interval-timer-in-ms要比lease-expiration-duration-in-seconds配置的要小,产生的结果就完全不一样。 5.ResponseCache通过readOnlyCache和readWriteCache实现: readOnlyCache: java.util.concurrent.ConcurrentMap readWriteCache: com.google.common.cache.LoadingCache readWriteCache会自动从registry中更新。 这个缓存只作用于获取整个注册表和app的实例注册信息。 缓存更新CacheUpdateTask这个任务用来定期更新只读缓存,逻辑上比较简单,定期从读写缓存中取出K/V,比较是否一致,不一致更新。 更新间隔通过参数eureka.server.response-cache-update-interval-ms来配置,默认是30s。 readWriteCache过期时间通过参数eureka.server.response-cache-auto-expiration-in-seconds来配置,默认是180s。 6.eureka Server几个重要的配置eureka.server.enable-self-preservation 是否开启自我保护模式,默认为true。 在开启状态下,Eureka Server会保持 默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。 Eureka通过“自我保护模式”来解决这个问题——当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。 综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。 123456789101112#是否开启自我保护模式,默认为true。enableSelfPreservation: true#默认是85%renewal-percent-threshold: 0.85#默认是15分钟renewal-threshold-update-interval-ms: 15#缓存更新时间,默认30sresponse-cache-update-interval-ms: 10#缓存过期时间,默认180sresponse-cache-auto-expiration-in-seconds: 30# 实例过期清理时间间隔,默认60秒eviction-interval-timer-in-ms: 10 7.eureka client几个重要的配置eureka.client.registry-fetch-interval-seconds 表示eureka client间隔多久去拉取服务注册信息,默认为30秒,对于api-gateway,如果要迅速获取服务注册状态,可以缩小该值,比如5秒 eureka.instance.lease-expiration-duration-in-seconds leaseExpirationDurationInSeconds,表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance。 默认为90秒 如果该值太大,则很可能将流量转发过去的时候,该instance已经不存活了。 如果该值设置太小了,则instance则很可能因为临时的网络抖动而被摘除掉。 该值至少应该大于leaseRenewalIntervalInSeconds eureka.instance.lease-renewal-interval-in-seconds leaseRenewalIntervalInSeconds,表示eureka client发送心跳给server端的频率。如果在leaseExpirationDurationInSeconds后,server端没有收到client的心跳,则将摘除该instance。除此之外,如果该instance实现了HealthCheckCallback,并决定让自己unavailable的话,则该instance也不会接收到流量。","categories":[{"name":"微服务","slug":"微服务","permalink":"http://blog.springcloud.cn/categories/微服务/"}],"tags":[{"name":"微服务 EurekaServer SpringCloud","slug":"微服务-EurekaServer-SpringCloud","permalink":"http://blog.springcloud.cn/tags/微服务-EurekaServer-SpringCloud/"}]},{"title":"微服务之API网关设计","slug":"sc/sc-tt-zuul","date":"2017-06-30T09:08:45.000Z","updated":"2017-07-01T14:31:32.000Z","comments":true,"path":"sc/sc-tt-zuul/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-tt-zuul/","excerpt":"","text":"1.简介微服务除了互相之间调用,还需要将API提供给外部应用访问,像浏览器,移动app,第三合作方等等,这就需要前段路由来管理后端微服务提供的服务。提供类似功能的应用有着多样化的名称,比如前置服务器/前置机、路由服务器、(反向)代理服务器,API网关服务器也是其中的一个叫法,只是场景和侧重点不一样。 API网关,顾名思义,就是外部到内部的一道门,其主要功能: 服务路由:将前段应用的调用请求路由定位并负载均衡到具体的后端微服务实例,对于前端应用看起来就是1个应用提供的服务,微服务对于前段应用来说就是黑盒,前段应用也不需要关心内部如何分布,由哪个微服务提供。主要有静态路由和动态路由。 静态路由:有时候需要通过域名或者其他固定方式提供和配置路由表 动态路由:通过服务发现服务,动态调整后端微服务的运行实例和路由表,为路由和负载均衡提供动态变化的服务注册信息。 安全:统一集中的身份认证,安全控制。比如登录,签名,黑名单等等,还可以挖掘和开发更高级的安全策略。 弹性:限流和容错,也是另一个层面的安全防护,防止突发的流量或者高峰流量冲击后端微服务而导致其服务不可用,另一方面可以在高峰期通过容错和降级保证核心服务的运行。 监控:实时观察后端微服务的TPS、响应时间,失败数量等准确的信息。 日志:记录所有请求的访问日志数据,可以为日志分析和查询提供统一支持。 其他,当然还有很多需要统一集中管理的都可以在网关层解决。 2.Zuul 作为API Gateway在Spring Cloud Netflix中使用Zuul来作为API Gateway组件,并结合Undertow,可以满足大部分性能和网关功能需求了(更高要求可以参考:https://github.com/tietang/ngx-lua-zuul),Zuul%EF%BC%8CZuul) + Undertow一般性能延迟,包括JVM和网络延迟在10%~20%,JVM延迟可以控制到10ms以内。另一方面,Zuul有强大的可定制化,通过ZuulFilter可以定制开发更多的网关功能。如下图所示:在Spring Cloud技术上,Zuul集成了Hystrix,Ribbon,Eureka Client等强大的技术栈,提供了开箱即用的Spring Cloud微服务网关功能。Zuul的强大之处是自由定制,这样对于很多老项目微服务化后,就不能按照Spring Cloud默认的动态路由规则运行,因此在其基础上定制了一些路由规则功能,更好的适应老项目微服务化。 定制的路由规则的主要功能: 路由表中包含源路径,微服务名称,目标路径。 Endpoint粒度配置支持。 路由支持1对1精确路由。 源路径可以前缀/**格式来模糊路由。 目标路径可以使用前缀/**格式来装配目标路径。 保留默认动态路由规则:服务名称/** –> 是否截去前缀 –> 目标路径。 保留默认动态路由规则是否支持截去前缀的配置参数stripPrefix特性。 路由规则可以在不重启服务动态更新,这个功能通过外化配置来支持。 匹配股则采取谁先匹配路由谁,也就是说在路由表中有2个或以上的路由规则可能被匹配到时,匹配最先查询到的规则。 路由规则格式采用properties格式: 源路径 = 微服务名称, 目标路径 启动时读取配置并解析,放入路由表。请求时通过查询匹配到合适的路由转发。 例如: 123/api/v1/trade=trade,/v1/trade/api/customer/**=customer,/api/v1/**/api/user/**=user 在上面的例子中: /api/v1/trade会精确的路由到trade微服务的/v1/trade; /api/customer/开头的api会路由转发到customer微服务的/api/v1/,其中后面的会被前面的部分替换,比如/api/customer/card->/api/v1/card的转换。 /api/user/开头的api会路由转发到user微服务的/api/user/,endppoint不变。 如果在Eureka Server中已经注册了微服务payment,那么在zuul启动后会自动添加路由规则,如果stripPrefix=false: 1/payment/**=payment,/payment/** 如果stripPrefix=true: 1/payment/**=payment,/payment/** API网关通过部署多个实例来保证可用性,前端通过Nginx来负载均衡。 转载请标明出处:版权归铁汤作者本文出自Spring Cloud中国社区会员-铁汤博客","categories":[{"name":"微服务","slug":"微服务","permalink":"http://blog.springcloud.cn/categories/微服务/"}],"tags":[{"name":"API GateWay Spring Cloud","slug":"API-GateWay-Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/API-GateWay-Spring-Cloud/"}]},{"title":"探讨通过Feign配合Hystrix进行调用时异常的处理","slug":"sc/sc-feign-hystrix","date":"2017-06-28T04:23:31.000Z","updated":"2017-07-01T09:28:21.000Z","comments":true,"path":"sc/sc-feign-hystrix/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-feign-hystrix/","excerpt":"","text":"场景及痛点 单个项目是通过Jersey来实现restful风格的架构 发生异常时异常信息总是没有回调方法,不能显示基础服务抛出的异常信息 暂时没有考虑发生异常之后进行回调返回特定内容 业务系统通过feign调用基础服务,基础服务是会根据请求抛出各种请求异常的(采用标准http状态码),现在我的想法是如果调用基础服务时发生请求异常,业务系统返回的能够返回基础服务抛出的状态码 当然基础服务抛出的请求异常不能触发hystrix的熔断机制 问题解决方案分析解决思路 通过网上一些资料的查询,看到很多文章会说HystrixBadRequestException不会触发hystrix的熔断–>但是并没有介绍该异常的实践方案 感觉要解决项目的痛点,切入点应该就在HystrixBadRequestException了。于是先看源码,一方面对Hystrix加深理解,尝试理解作者设计的初衷与想法,另一方面看看是否能找到其他方案达到较高的实践标准 对应源码解释对应方案主要类对象简介 interface UserRemoteCall 定义feign的接口其上会有@FeignClient,FeignClient定义了自己的Configuration–> FeignConfiguration class FeignConfiguration这里是定义了指定Feign接口使用的自定义配置,如果不想该配置成为全局配置,不要让该类被自动扫描到 ‘class UserErrorDecoder implements ErrorDecoder’该类会处理响应状态码 (![200,300) || !404) 不使用Hystrix源码分析 Feign的默认配置在org.springframework.cloud.netflix.feign.FeignClientsConfiguration类中,如果不自定义Feign.Builder,会优先配置feign.hystrix.HystrixFeign.Builder extends Feign.Builder,该类会让Feign的内部调用受到Hystrix的控制 12345678910111213//省略部分代码 @Configuration @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) protected static class HystrixFeignConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true) public Feign.Builder feignHystrixBuilder() { return HystrixFeign.builder(); } }//省略部分代码 解决方案 当然不使用Hystrix就不会有熔断等问题出现,处理好ErrorDecoder.decode()即可。 不开启Hystrix的方式: 配置增加feign.hystrix.enabled=false,这会在全局生效不推荐。 FeignConfiguration增加:(推荐)12345@Bean@Scope("prototype")public Feign.Builder feignBuilder() { return Feign.builder();} 使用Hystrix解决内部调用抛出异常问题源码分析 Hystrix的设计方案是通过命令模式加RxJava实现的观察者模式来开发的,想完全熟悉Hystrix的运作流程需要熟练掌握RxJava,本文只对源码进行简单介绍,后面有时间有机会再详细介绍 Hystrix如何处理异常的:代码位置:com.netflix.hystrix.AbstractCommand#executeCommandAndObserve1234567891011121314151617181920212223242526272829 //省略部分代码 private Observable<R> executeCommandAndObserve(final AbstractCommand<R> _cmd) {//省略部分代码 final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() { @Override public Observable<R> call(Throwable t) { Exception e = getExceptionFromThrowable(t); executionResult = executionResult.setExecutionException(e); if (e instanceof RejectedExecutionException) { return handleThreadPoolRejectionViaFallback(e); } else if (t instanceof HystrixTimeoutException) { return handleTimeoutViaFallback(); } else if (t instanceof HystrixBadRequestException) { return handleBadRequestByEmittingError(e); } else { /* * Treat HystrixBadRequestException from ExecutionHook like a plain HystrixBadRequestException. */ if (e instanceof HystrixBadRequestException) { eventNotifier.markEvent(HystrixEventType.BAD_REQUEST, commandKey); return Observable.error(e); } return handleFailureViaFallback(e); } } };//省略部分代码 } 该类中该方法为发生异常的回调方法,由此可以看出如果抛出异常如果是HystrixBadRequestException是直接处理异常之后进行抛出(这里不会触发熔断机制),而不是进入回调方法。 解决方案 那么我们对与异常的解决方案就需要通过HystrixBadRequestException来解决了,根据返回响应创建对应异常并将异常封装进HystrixBadRequestException,业务系统调用中取出HystrixBadRequestException中的自定义异常进行处理 封装异常说明:12345678910111213141516171819202122public class UserErrorDecoder implements ErrorDecoder{ private Logger logger = LoggerFactory.getLogger(getClass()); public Exception decode(String methodKey, Response response) { ObjectMapper om = new JiaJianJacksonObjectMapper(); JiaJianResponse resEntity; Exception exception = null; try { resEntity = om.readValue(Util.toString(response.body().asReader()), JiaJianResponse.class);//为了说明我使用的WebApplicationException基类,去掉了封装 exception = new WebApplicationException(javax.ws.rs.core.Response.status(response.status()).entity(resEntity).type(MediaType.APPLICATION_JSON).build()); } catch (IOException ex) { logger.error(ex.getMessage(), ex); } // 这里只封装4开头的请求异常 if (400 <= response.status() || response.status() < 500){ exception = new HystrixBadRequestException("request exception wrapper", exception); } return exception; }} 业务系统处理异常说明:123456789101112131415 @Override public UserSigninResEntity signIn(UserSigninReqEntity param) throws Exception { try {//省略部分代码 UserSigninResEntity entity = userRemoteCall.signin(secretConfiguration.getKeys().get("user-service"), param);//省略部分代码 } catch (Exception ex) { logger.error(ex.getMessage(), ex);//这里进行异常处理 if(ex.getCause() instanceof WebApplicationException){ throw (WebApplicationException) ex.getCause(); } throw ex; } } WebApplicationException是javax.ws.rs包中异常,通过Jersey抛出该异常能够将返回的HttpCode封装进该异常中(上述代码中展示了如何封装HttpCode),抛出该异常,调用端就能得到返回的HttpCode。 总结 本文主要出发点在于如何解决在Feign中使用Hystrix时被调用端抛出请求异常的问题。 本项目使用Jersey,封装WebApplicationException即可满足需求,其他架构也是大同小异了。 该解决方案我不确定是否为最佳实践方案,特别希望和欢迎有不同想法或意见的朋友来与我交流,包括但不限于解决方案、项目痛点是否合理等等。 转载请标明出处:版权归任聪作者和Spring Cloud中国社区所有本文出自任聪的博客","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Feign Hystrix","slug":"Spring-Cloud-Feign-Hystrix","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Feign-Hystrix/"}]},{"title":"JWT在Spring Cloud中的使用","slug":"sc/sc-w-jwt","date":"2017-06-28T04:23:31.000Z","updated":"2017-06-29T01:06:05.000Z","comments":true,"path":"sc/sc-w-jwt/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-w-jwt/","excerpt":"","text":"1.JWT的概念jwt是通过HMAC(Hash-based Message Authentication Code)计算信息摘要,也可以用RSA公私钥中的私钥进行签名,这个可以根据业务场景选择,对应jwt最终展示实际上就是一个字符串,它一般右三部分组成,头部(header), 载荷(payload), 签名(signature)。 头部(header)头部一般是关于jwt的描述信息,如其类型以及签名所用的算法等等信息,可以自行定义。如:{“alg”: “HS256”,“typ”:”JWT”}对它进行base64之后,生成的字符串就成了JWT的Header base64后字符串:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 载荷(PayLoad)载荷一般来讲就是实际业务中需要在token中传递的数据信息,一般来说是一些非敏感数据,比如用户ID,过期时间等等其他一些与业务相关的数据,如:{ “accountId”: “admin”, “expTime”: “1498795200”}base64之后字符串:eyJhY2NvdW50SWQiOiJhZG1pbiIsImV4cFRpbWUiOiIxNDk4Nzk1MjAwIn0 签名(Signature)将上面的两个编码后的字符串都用句号 . 连接在一起(头部在前),就形成了eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOiJhZG1pbiIsImV4cFRpbWUiOiIxNDk4Nzk1MjAwIn0最后将上面连接结果进行HS256算法进行加密(也可以是其他加密算法),某些算法需要提供加密密钥,如密钥为:secret,则加密完后得到的字符串就是签名。HS256算法加密后字符串:GsGmzc6su0imWlnoyaCexgq1jNurdD8ObEMuPD_q2a8 最终整个JWT字符串就是:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOiJhZG1pbiIsImV4cFRpbWUiOiIxNDk4Nzk1MjAwIn0.GsGmzc6su0imWlnoyaCexgq1jNurdD8ObEMuPD_q2a8 2.JWT生成及使用流程 用户导航到登录页,输入用户名、密码,进行登录 服务器验证登录鉴权,如果用户合法,根据用户的信息和服务器的规则生成JWT Token 服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法) 用户得到token,存在localStorage、cookie或其它数据存储形式中。 以后用户请求系统中的API时,在请求的header或者Cookie 中加入 X-Token 。 服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。 用户取得结果 JWT工作流程图 3.JWT的JAVA API库jwt的java开源库很多,如:java-jwt, jose4j,nimbus-jose-jwt, jjwt。它们之间对加密方法支持会有些差别,具体可详见以下链接:https://jwt.io/#libraries-io。笔者本文demo采用jjwt进行演示。 4.JWT在Spring Cloud中应用接下来我们直接上代码,来了解JWT在spring cloud中应用,以下只截取与JWT相关代码,关于spring cloud代码可参考其他资料。另一般在真实系统中,会把整个JWT的生成和校验会单独抽取成一个独立的鉴权中心服务,本文只是为演示使用,并未进行抽取深入。 核心代码1.增加maven依赖(此处指列出JWT依赖jjwt,spring cloud相关依赖可查看其它资料) 12345<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.6.0</version></dependency> 2.在eurekaclient项目的服务中增加生成token代码,此处是在登录时。 EurekaclientApplication.java 123456789101112131415@RequestMapping(\"/login\")public String login(@RequestParam String name, @RequestParam String password) { //判断用户名密码是否合法,合法否在进行token生成 // 令牌有效期30天 DateTime dt = new DateTime(); Date expiration = dt.plusDays(30).toDate(); // 生成令牌,参数可以自行组织 String accessToken = Jwts.builder().setHeaderParam(\"alg\", \"HS256\") .setHeaderParam(\"typ\", \"JWT\") .claim(\"accountId\", name) .claim(\"expTime\", expiration) .signWith(SignatureAlgorithm.HS256, CoreConstants.SECRET).compact(); return accessToken;} 3.在service-ribbon中增加服务调用代码 HelloController.java 1234@RequestMapping(value = \"/login\")public String hi(@RequestParam String name, @RequestParam String password) { return jwtService.loginService(name, password);} JWTService.java 1234567891011121314@Servicepublic class JWTService {@Autowiredprivate RestTemplate restTemplate;@HystrixCommand(fallbackMethod = \"hiError\")public String loginService(String name, String password) { return restTemplate.getForObject(\"http://service-hi/login?name=\" + name + \"&password=\" + password, String.class);}public String hiError(String name, String password) { return \"hi,\" + name + \",sorry, error!\";}} 4.在service-zuul项目中的过滤器MyFilter中添加如下代码 MyFilter.java 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273/** * 具体业务逻辑 * @return */ @Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); logger.info(String.format(\"%s >>> %s\", request.getMethod(), request.getRequestURL().toString())); //此处可对请求方法进行刷选 if (!request.getRequestURI().contains(\"login\")) { // 先取Header中X-Token String accessToken = request.getHeader(CoreConstants.X_TOKEN); // 如果令牌为空, 再取Cookie中X-Token if (accessToken == null || accessToken.isEmpty()) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (CoreConstants.X_TOKEN.equals(cookie.getName())) { accessToken = cookie.getValue(); break; } } } } // 如果令牌为空, 再取QueryString中X-Token if (accessToken == null || accessToken.isEmpty()) { accessToken = request.getParameter(CoreConstants.X_TOKEN); } if (accessToken == null || accessToken.isEmpty()) { logger.error(\"token is empty\"); context.setSendZuulResponse(false); context.setResponseStatusCode(401); try { context.getResponse().getWriter().write(\"token is empty\"); } catch (Exception e) { } return null; } else { try { Jws<Claims> claims = Jwts.parser().setSigningKey(CoreConstants.SECRET).parseClaimsJws(accessToken); // 获取用户Id String accountId = claims.getBody().get(\"accountId\").toString(); logger.debug(\"accountId = {}\", accountId); //根据accountId去获取相关信息, 本demo省略 // 过期时间 String expTime = claims.getBody().get(\"expTime\").toString(); logger.debug(\"expTime = {}\", expTime); //对过期时间进行相关判断, 本demo省略 return null; } catch (Exception ex) { if (logger.isErrorEnabled()) { logger.error(ExceptionUtils.getStackTrace(ex)); } // header中令牌不对, 可能被篡改 logger.error(\"token is error\"); context.setSendZuulResponse(false); context.setResponseStatusCode(401); try { context.getResponse().getWriter().write(\"token is error\"); } catch (Exception e) { } return null; } } } logger.debug(\"token is ok\"); return null; } 【注意】需要在service-zuul项目中的yml配置文件中添加一下配置,否则可能会出现com.netflix.zuul.exception.ZuulException:Forwarding error异常,导致服务无法正常调用 zuul: ribbon-isolation-strategy: thread host: connect-timeout-millis: 10000 socket-timeout-millis: 60000 核心代码至此结束。 运行展示依次启动eurekaserver, eurekaclient, service-ribbon, service-feign,service-zuul等项目。此时如直接调用后端服务,将会提示token is empty 如果随意带上X-Token, 会提示token is error,也无法正常调用服务 因此必须在正常登陆后,得到真正的token后,然后在调用服务时传入,才可以正常调用。 得到token 调用服务是传入token后,正常得到服务结果 代码下载:https://github.com/yzun/spring-cloud-study 参考链接:https://jwt.io/ http://blog.csdn.net/forezp/article/details/70148833 转载请标明出处:版权归余正作者和Spring Cloud中国社区所有本文出自Spring Cloud中国社区会员-余正","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"JWT","slug":"JWT","permalink":"http://blog.springcloud.cn/tags/JWT/"}]},{"title":"Spring Cloud Zuul异常处理","slug":"sc/sc-zuul-excpetion","date":"2017-06-24T04:23:31.000Z","updated":"2017-06-24T09:49:59.000Z","comments":true,"path":"sc/sc-zuul-excpetion/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-zuul-excpetion/","excerpt":"最近看到了一个GitHub issue在讨论如何在post类型的zuul filter中设置response body,其中涉及到异常情况下,如何返回一个自定义的response body。正好我在升级spring-cloud,也想弄清楚,spring-cloud-zuul是如何处理异常情况的,所以就仔细看了看这部分的实现细节,现在做个笔记记录下来。","text":"最近看到了一个GitHub issue在讨论如何在post类型的zuul filter中设置response body,其中涉及到异常情况下,如何返回一个自定义的response body。正好我在升级spring-cloud,也想弄清楚,spring-cloud-zuul是如何处理异常情况的,所以就仔细看了看这部分的实现细节,现在做个笔记记录下来。 1.zull请求的生命周期图关于zuul是如何工作的,这里不再介绍,具体可以参看这里。官方给了一个zull请求的生命周期图: 上图中,实线表示请求必然经过的路径,而虚线表示可能经过的路径;从这张图中可以看出: 所有请求都必然按照pre-> route -> post的顺序执行。 post返回response。 如果pre中有自定义filter,则执行自定义filter。 如果pre,route,post发生错误则执行error,然后再执行post。 这张图忽略了很多细节;最明显的就是,自定义的filter可以是pre,route,post,error中的任何一种;其次假如post中发生了异常,执行流程交给error处理完之后,又重新回到post中,会不会又有问题? 所以还是看看代码比较靠谱。以下基于spring-cloud Dalston.RELEASE做代码分析。 调试一下,就可以看到请求进入zuul之后的整个调用链,简单来说如下:ZuulServlet#service -> FilterProcessor#processZuulFilter -> ZuulFilter#runFilter -> [Concret]ZuulFilter#run。 2.源码分析ZuulServlet#service首先找到请求进入zuul filters的入口:ZuulServlet#service(ServletRequest, ServletResponse)。 下面抽出这个函数的主干: 12345678910111213141516171819202122232425try { try { preRoute(); } catch (ZuulException e) { error(e); postRoute(); return; } try { route(); } catch (ZuulException e) { error(e); postRoute(); return; } try { postRoute(); } catch (ZuulException e) { error(e); return; } } catch (Throwable e) { error(new ZuulException(e, 500, \"UNHANDLED_EXCEPTION_\" + e.getClass().getName())); } 这个函数基本遵从但不完全符合官网给出的生命周期图: 正常情况下,请求只经过pre -> route -> post。 两层try...catch,内层只捕获ZuulException,而其他异常由外层捕获。 内层3个try...catch语句,只有pre,route抛出ZuulException时,才会执行errror,再执行post。而当post(88行)抛出ZuulException后,只会执行error。 外层捕获其他异常(内层try语句块中抛出的非ZuulException异常以及内层catch语句中抛出的所有异常)后,将HTTP状态码设置为500,同时交给error处理。 整个流程的终点有两个:post及error;而非只有post一个。 另外看一下error(ZuulException)这个函数到底做了什么: 1234void error(ZuulException e) { RequestContext.getCurrentContext().setThrowable(e); zuulRunner.error();} 异常信息是在这里被加入到RequestContext中的,以供后续的filter使用,然后调用error filters。 至此我们可以得到一个流程图(感觉还不如代码看得清晰-_-!!): FilterProcessor#processZuulFilterFilterPreocessor#processZuulFilter,这个函数调用ZuulFilter,并且会将异常重新抛出,如果是非ZuulException的异常,则转为状态码为500的ZuulException。 123456789101112131415161718192021222324try { Throwable t = null; ZuulFilterResult result = filter.runFilter(); ExecutionStatus s = result.getStatus(); switch (s) { case FAILED: t = result.getException(); break; } if (t != null) throw t; } catch (Throwable e) { if (e instanceof ZuulException) { throw (ZuulException) e; } else { ZuulException ex = new ZuulException(e, \"Filter threw Exception\", 500, filter.filterType() + \":\" + filterName); throw ex; } } 如果ZuulFilter执行失败,即结果状态为FAILED,则从ZuulFilter的执行结果ZuulFilterResult中提取出异常信息(199行),然后抛出(214);在catch语句块中,捕获刚才抛出的异常,判断是否为ZuulException,如果是则直接抛出,否则转化为状态为500的ZuulException再抛出。 看到这里,基本确认的一点是,ZuulFilter中抛出的任何形式的异常,最终都会转化为ZuulException抛给上层调用者,即ZuulServlet#service。但是这里并不是通过try...catch来捕获ZuulFilter执行中抛出的异常,而是从返回结果ZuulFilterResult中直接获取的,这是怎么一回事,需要再看下ZuulFilter#runFilter的实现逻辑。 ZuulFilter#runFilter下面是从ZuulFilter#runFilter()抽取出来的核心代码: 1234567891011ZuulFilterResult zr = new ZuulFilterResult(); try { Object res = run(); zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS); } catch (Throwable e) { zr = new ZuulFilterResult(ExecutionStatus.FAILED); zr.setException(e); } return zr; 这段代码会调用某个具体的ZuulFilter实现的run方法,如果不抛出异常,则返回状态为ExecutionStatus.SUCCESS的ZuulFilterResult(117行);若有任何异常,则将返回结果的状态设置为ExecutionStatus.FAILED(120),同时将异常信息设置到返回结果中(121)。即我们实现一个ZuulFilter,如果不抛出异常,则会被认为是成功的,否则就会被当作失败的。 结合上面两节的代码分析,ZuulFilter中一旦有异常抛出,必然是(或被转化为)ZuulException,然后必然进入到error filters中处理。由此,我们简化一下上面的流程图: 3.SpringCloud中的SendErrorFilter在Dalston.RELEASE之前,spring-cloud-netflix中并不包含error类型的Filter;而处理错误情况的filter为SendErrorFilter,其类型为post,order为0,比SendResponseFilter优先级高(1000),即更早调用。先来分析一下Dalston.RELEASE之前版本的SendErrorFilter,下面的代码片段摘自spring-cloud-netflix 1.2.7.RELEASE: 1234567891011121314151617181920212223242526272829@Value(\"${error.path:/error}\") private String errorPath; @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); // only forward to errorPath if it hasn't been forwarded to already return ctx.containsKey(\"error.status_code\") && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false); } @Override public Object run() { int statusCode = (Integer) ctx.get(\"error.status_code\"); request.setAttribute(\"javax.servlet.error.status_code\", statusCode); Object e = ctx.get(\"error.exception\"); request.setAttribute(\"javax.servlet.error.exception\", e); String message = (String) ctx.get(\"error.message\"); request.setAttribute(\"javax.servlet.error.message\", message); RequestDispatcher dispatcher = request.getRequestDispatcher( this.errorPath); dispatcher.forward(request, ctx.getResponse()); } 从上面的代码中可以得出以下几点: SendErrorFilter的进入条件是:RequestContext中包含error.status_code,且之前从未执行过该filter。(55, 56) 会将错误信息转发给errorPath执行;errorPath可由配置项error.paht指定,默认为/error。(38, 79, 84) 转发的错误信息是从RequestContext中的三个key得到:error.status_code, error.exception, error.message。(65~76) 如果要使用SendErrorFilter,则我们在自己实现自定义ZuulFilter做异常处理的时候,需要注意: 如果是pre, route类型的filter,则捕获所有内部异常,将异常信息设置到error.message中,设置所需返回的HTTP状态码到error.status_code中;然后抛出一个异常。抛出异常是为了将执行流程交给error->post这个执行分支;否则,当前filter会被认为执行成功,继续执行后续的filter。run()方法抛出的异常需是(或继承)RuntimeException,因为IZuulFilter#run()接口没有显示抛出异常。 如果是 post类型: 设置该filter的order,小于0(这是SendErrorFilter)。 仔细考虑shouldFilter()的实现细节,因为异常流也会进入post filters,确定是否需要处理。 run()方法中捕获所有异常,然后设置error.status_code, error.message, error.exception,并且不再抛出异常。否则会进入error filters,但是现在没有,由SendErrorFilter替代;除非自己实现一个error filter,然后禁掉SendErrorFilter。 这个版本中,spring-cloud-netflix提供的这个SendErrorFilter有明显的缺陷,无法处理由post filters抛出的异常,也不符合zuul请求的生命周期图。所以在Dalston.RELEASE之后,即spring-cloud-netflix 1.3.0.RELEASE,将SendErrorFilter的类型改为了error。 下面的代码片段摘自spring-cloud-netflix 1.3.0.RELEASE的SendErrorFilter类: 12345678910111213141516171819202122232425262728293031@Override public String filterType() { return ERROR_TYPE; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); // only forward to errorPath if it hasn't been forwarded to already return ctx.getThrowable() != null && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false); } @Override public Object run() { ZuulException exception = findZuulException(ctx.getThrowable()); request.setAttribute(\"javax.servlet.error.status_code\", exception.nStatusCode); request.setAttribute(\"javax.servlet.error.exception\", exception); request.setAttribute(\"javax.servlet.error.message\", exception.errorCause); } ZuulException findZuulException(Throwable throwable) { if (throwable.getCause() instanceof ZuulRuntimeException) { // this was a failure initiated by one of the local filters return (ZuulException) throwable.getCause().getCause(); } } 需要注意几点: 类型为error(53行)。 进入条件为:RequestContext中有异常,并且该filter从未执行过(65, 66)。异常对象是在ZuulServlet#error(ZuulException)方法中设置的。 run()方法中提取错误信息不再是从RequestContext的三个key(error.status_code, error.message, error.exception)中获取;而是直接从ZuulException对象中获取(73~82)。 如何取得ZuulException对象(100118),最重要的一点是从ZuulRuntimeException中提取ZuulException对象(101103),而ZuulRuntimeException继承RuntimeException。 注意101行代码,是判断throwable.getCause()是否为ZuulRuntimeException,这是因为所有非ZuulException的异常在FilterProcessor#processZuulFilter()(227行)中会被转化为ZuulException。 findZuulException没有贴全,其会优先从自定义filter中抛出的ZuulRuntimeException中提取ZuulException对象。这样就允许我们返回我们想要的错误信息和HTTP状态码。 那基于1.3.0.RELEASE,我们在写自定义filter时,如何做异常处理呢: 将filter内部异常转化为ZuulException,设置自己需要返回的HTTP状态码,然后包装为ZuulRuntimeException抛出。 如若不封装为ZuulRuntimeException,则返回的HTTP状态码为500。 举个例子: 12345678public Object run() { try { // do something } catch (Throwable t) { throw new ZuulRuntimeException(new ZuulException(t, HttpStatus.BAD_REQUEST.value(), t.getMessage())); } return null;} 如果想自定义返回的异常信息的response body的格式,最简单的方法是仿照BasicErrorController重写一下/error接口。","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Zuul","slug":"Spring-Cloud-Zuul","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Zuul/"}]},{"title":"hystrix在spring mvc的使用","slug":"sc/sc-wy-hystrix","date":"2017-06-21T16:00:00.000Z","updated":"2017-06-24T03:00:38.000Z","comments":true,"path":"sc/sc-wy-hystrix/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-wy-hystrix/","excerpt":"","text":"一个大型服务不可避免的需要依赖其他服务,并且有可能需要通过网络请求依赖第三方客户端。这样就有可能因为单个依赖服务延迟而导致整个服务器上的资源被阻塞。更糟糕的是,倘若两个服务相互依赖,有一个服务对另一个服务响应延时就有可能造成雪崩效应,导致两个服务一起崩溃。 现如今微服务架构十分流行,其解决依赖隔离方案hystrix也被大家所认知。但目前还有很多服务还是停留在Spring mvc框架,无法直接使用Spring Cloud集成的hystrix方案。本文先简单介绍hystrix的基本知识,然后介绍hystrix在Spring mvc的使用,最后简单介绍下如何实现项目的hystrix信息监控。 一、简介1、为什么要使用Hystrix在复杂的分布式结构中,每个应用都可能会依赖很多其他的服务,并且这些服务都不可避免地有失效的可能。倘若没有对依赖失败进行隔离,那整个服务可能就会有被拖垮的风险。 例如,一个应用依赖了 30 个服务,并且每个服务能保证 99.99% 的可用率,下面是一些计算结果: 可用率:99.99%^30=99.7% 1亿次请求*0.3%=300,000次失效 换算成时间大约每个月2个小时服务不稳定。 然而,现实更加残酷,如果你没有针对整个系统做快速恢复,即使所有依赖只有 0.01% 的不可用率,累积起来每个月给系统带来的不可用时间也有数小时之多。 当所有依赖都正常,一个请求的拓扑结构如下所示: 当一个依赖服务有延迟,它将会阻塞整个用户请求: 在高QPS的环境下,一个依赖服务的延迟会导致整个服务器上资源都被阻塞。 应用中每一个网络请求或者间接通过客户端库发出的网络请求都是潜在的导致应用失效的原因。更严重的是,这些应用可能被其他服务依赖,由于每个服务都有诸如请求队列,线程池,或者其他系统资源等,一旦某个服务失效或者延迟增高,会导致更严重的级联失效。 hystrix被设计用来: 在通过第三方客户端访问(通常是通过网络)依赖服务出现高延迟或者失败时,为系统提供保护和控制 在分布式系统中防止级联失败 可以进行快速失败(不需要等待)和快速恢复(当依赖服务失效后又恢复正常,其对应的线程池会被清理干净,即剩下的都是未使用的线程,相对于整个 Tomcat 容器的线程池被占满需要耗费更长时间以恢复可用来说,此时系统可以快速恢复) 提供失败回退(Fallback)和优雅的服务降级机制 提供近实时的监控、报警和运维控制手段 2、Hystrix如何解决依赖隔离 将所有请求外部系统(或者叫依赖服务)的逻辑封装到 HystrixCommand 或者 HystrixObservableCommand 对象中,这些逻辑将会在独立的线程中被执行(利用了设计模式中的 Command模式) 对那些耗时超过设置的阈值的请求,Hystrix 采取自动超时的策略。该策略默认对所有 Command 都有效,当然,你也可以通过设置 Command 的配置以自定义超时时间,以使你的依赖服务在引入 Hystrix 之后能达到 99.5% 的性能 为每一个依赖服务维护一个线程池(或者信号量),当线程池占满,该依赖服务将会立即拒绝服务而不是排队等待 划分出成功、失败(抛出异常)、超时或者线程池占满四种请求依赖服务时可能出现的状态 引入『熔断器』机制,在依赖服务失效比例超过阈值时,手动或者自动地切断服务一段时间 当请求依赖服务时出现拒绝服务、超时或者短路(多个依赖服务顺序请求,前面的依赖服务请求失败,则后面的请求不会发出)时,执行该依赖服务的失败回退逻辑 近实时地提供监控和配置变更 当使用 Hystrix 包装了你的所有依赖服务的请求后,拓扑图如下 3、hystrix如何执行hystrix执行分为三种模式,分别为同步执行、异步执行、Reactive模式执行。 同步执行:若原方法返回参数非Future对象且非Observable对象则会构建该模式。使用command.execute(),阻塞,当依赖服务响应(或者抛出异常/超时)时,返回结果; 异步执行:若原方法返回参数为Future对象时构建该模式。使用command.queue(),返回Future对象,通过该对象异步得到返回结果; Reactive模式执行:若原方法返回参数为Observable对象时构建该模式。该模式又分observe()命令和toObservable()命令。observe()命令会立即发出请求,在依赖服务响应(或者抛出异常/超时)时,通过注册的 Subscriber得到返回结果。toObservable()命令只有在订阅该对象时,才会发出请求,然后在依赖服务响应(或者抛出异常/超时)时,通过注册的Subscriber得到返回结果。 在内部实现中,execute()是同步调用,内部会调用queue().get()方法。queue()内部会调用toObservable().toBlocking().toFuture()。也就是说,HystrixCommand 内部均通过一个Observable的实现来执行请求,即使这些命令本来是用来执行同步返回回应这样的简单逻辑。 构建HystrixCommand或者HystrixObservableCommand对象; 执行命令(execute()、queue()、observe()、toObservable()); 如果请求结果缓存这个特性被启用,并且缓存命中,则缓存的回应会立即通过一个Observable对象的形式返回; 检查熔断器状态,确定请求线路是否是开路,如果请求线路是开路,Hystrix将不会执行这个命令,而是直接使用『失败回退逻辑』(即不会执行run(),直接执行getFallback()); 如果和当前需要执行的命令相关联的线程池和请求队列(或者信号量,如果不使用线程池)满了,Hystrix 将不会执行这个命令,而是直接使用『失败回退逻辑』(即不会执行run(),直接执行getFallback()); 执行HystrixCommand.run()或HystrixObservableCommand.construct(),如果这两个方法执行超时或者执行失败,则执行getFallback();如果正常结束,Hystrix 在添加一些日志和监控数据采集之后,直接返回回应; Hystrix 会将请求成功,失败,被拒绝或超时信息报告给熔断器,熔断器维护一些用于统计数据用的计数器。 这些计数器产生的统计数据使得熔断器在特定的时刻,能短路某个依赖服务的后续请求,直到恢复期结束,若恢复期结束根据统计数据熔断器判定线路仍然未恢复健康,熔断器会再次关闭线路。 4、hystrix基本配置hystrix基本配置可以通过四种方式进行设置。 hystrix本身代码默认。这种是在以下三种都没有自定义的情况下使用,默认设置在hystrix-core下的HystrixCommandProperties和HystrixThreadPoolProperties 自定义默认配置。可以使用配置文件进行全局默认配置。例如:hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 通过代码构造实例设置。 动态实例配置。根据实例的key值(commandKey或者threadPollKey)通过配置文件给特定实例进行配置。例如,一个实例的commandKey为commandTest,则为hystrix.command.commandTest.execution.isolation.thread.timeoutInMilliseconds 本文只介绍hystrix常用的command和ThreadPool配置,其余配置可以查看官网 command配置 execution.isolation.strategy:执行隔离策略. Thread是默认推荐的选择。THREAD为每次在一个线程中执行,并发请求数限制于线程池的线程数。SEMAPHORE为在调用线程中执行,并发请求数限制于semaphore信号量的值。 execution.isolation.thread.timeoutInMilliseconds:超时时间,默认1000ms。 execution.timeout.enabled:是否开启超时,默认true。 execution.isolation.thread.interruptOnTimeout:当超时的时候是否中断(interrupt) HystrixCommand.run()执行,默认:true。 fallback.enabled:是否开启fallback,默认:true。 circuitBreaker.enabled:是否开启熔断,默认true。 circuitBreaker.requestVolumeThreshold:设置一个滑动窗口内触发熔断的最少请求量,默认20。例如,如果这个值是20,一个滑动窗口内只有19个请求时,即使19个请求都失败了也不会触发熔断。 circuitBreaker.sleepWindowInMilliseconds:设置触发熔断后,拒绝请求后多长时间开始尝试再次执行。默认5000ms。 circuitBreaker.errorThresholdPercentage:设置触发熔断的错误比例。默认50,即50%。 metrics.rollingStats.timeInMilliseconds:设置滑动窗口的统计时间。熔断器使用这个时间。默认10s metrics.rollingStats.numBuckets:设置滑动统计的桶数量。默认10。metrics.rollingStats.timeInMilliseconds必须能被这个值整除。 threadPool配置 coreSize:设置线程池的core size,这是最大的并发执行数量。默认10。 maximumSize:设置线程池数量极大值,这是可以支持的最大并发量,一般情况下和coreSize是相等的。默认10。该值只有在allowMaximumSizeToDivergeFromCoreSize被设置时才能有效。 maxQueueSize:最大队列长度。设置BlockingQueue的最大长度。默认-1。 如果设置成-1,就会使用SynchronizeQueue。 如果其他正整数就会使用LinkedBlockingQueue。 queueSizeRejectionThreshold:设置拒绝请求的临界值。只有maxQueueSize为-1时才有效。设置设个值的原因是maxQueueSize值运行时不能改变,我们可以通过修改这个变量动态修改允许排队的长度。默认5。(注意:hystrix为每一个依赖服务维护一个线程池或者信号量,当线程池占满+queueSizeRejectionThreshold占满,该依赖服务将会立即拒绝服务而不是排队等待) keepAliveTimeMinutes:设置keep-live时间。默认1分钟。当coreSize==maximumSize时线程池是固定的。只有allowMaximumSizeToDivergeFromCoreSize值设置为true,coreSize和maximumSize才能分成两个部分。当coreSize < maximumSize,该值控制一个线程多久没使用才被释放。 allowMaximumSizeToDivergeFromCoreSize:该值确认maximumSize是否起作用。默认false。 metrics.rollingStats.timeInMilliseconds:和command配置含义一样。 metrics.rollingStats.numBuckets:和command配置含义一样。 倘若使用配置文件进行配置,两种配置可以根据key的字符串进行区分。command都是hystrix.command.commandKey(or default).属性名,threadpool都是hystrix.threadpool.threadpoolKey(groupKey or default).属性名。 二、hystrix在spring mvc的使用hystrix在Spring cloud的使用非常简单,网上也有很多文档,在此就不多讲了。 为使熔断控制和现有代码解耦,hystrix官方采用了Aspect方式。现在介绍hystrix在spring mvc的使用。 1、添加依赖使用maven引入hystrix依赖: <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-javanica</artifactId> <version>1.5.12</version> </dependency> 2、添加配置新建hystrix.properties文件(名字随意定,里面将定义项目所有hystrix配置信息) 新建一个类HystrixConfig public class HystrixConfig { public void init() { Properties prop = new Properties(); InputStream in = null; try { in = HystrixConfig.class.getClassLoader().getResourceAsStream("hystrix.properties"); prop.load(in); in.close(); System.setProperties(prop); } catch (Exception e) { e.printStackTrace(); } } } 在spring的配置文件添加内容: <!-- 添加了就不用加了 --> <aop:aspectj-autoproxy proxy-target-class="true" /> <bean name="hystrixCommandAspect" class="com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect"/> <bean id="hystrixConfig" class="包名.HystrixConfig" init-method="init"/> 新建hystrixConfig bean主要是因为使用spring自带的context:property-placeholder配置加载器,hystrix无法读取。目前我只想到了通过System.setProperties的方式,若有其他方式欢迎指导。 3、hystrixCommand使用举个简单的例子(写成接口方式是方便测试,普通的方法效果是一样的): @ResponseBody @RequestMapping("/test.html") @HystrixCommand public String test(int s) { logger.info("test.html start,s:{}", s); try { Thread.sleep(s * 1000); } catch (Exception e) { logger.error("test.html error.", e); } return "OK"; } 根据例子,我们可以看到和其他方法相比就添加了个@HystrixCommand注解,方法执行后会被HystrixCommandAspect拦截,拦截后会根据方法的基本属性(所在类、方法名、返回类型等)和HystrixCommand属性生成HystrixInvokable,最后执行。例子中,因为HystrixCommand属性为空,所以其groupKey默认为类名,commandKey为方法名。 通过HystrixCommand源码来看下可以设置的属性: @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface HystrixCommand { String groupKey() default ""; String commandKey() default ""; String threadPoolKey() default ""; String fallbackMethod() default ""; HystrixProperty[] commandProperties() default {}; HystrixProperty[] threadPoolProperties() default {}; Class<? extends Throwable>[] ignoreExceptions() default {}; ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER; HystrixException[] raiseHystrixExceptions() default {}; String defaultFallback() default ""; } 其中比较重要的是groupKey、commandKey、fallbackMethod(Fallback时调用的方法,一定要在同一个类中,且传参和返参要一致)。threadPoolKey一般可以不定义,线程池名会默认定义为groupKey。 再来看下HystrixCommandAspect是如何实现拦截的: @Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand)") public void hystrixCommandAnnotationPointcut() { } @Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser)") public void hystrixCollapserAnnotationPointcut() { } @Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()") public Object methodsAnnotatedWithHystrixCommand(final ProceedingJoinPoint joinPoint) throws Throwable { Method method = getMethodFromTarget(joinPoint);//见步骤1 Validate.notNull(method, "failed to get method from joinPoint: %s", joinPoint); if (method.isAnnotationPresent(HystrixCommand.class) && method.isAnnotationPresent(HystrixCollapser.class)) { throw new IllegalStateException("method cannot be annotated with HystrixCommand and HystrixCollapser " + "annotations at the same time"); } MetaHolderFactory metaHolderFactory = META_HOLDER_FACTORY_MAP.get(HystrixPointcutType.of(method));//见步骤2 MetaHolder metaHolder = metaHolderFactory.create(joinPoint);//见步骤3 HystrixInvokable invokable = HystrixCommandFactory.getInstance().create(metaHolder);//见步骤4 ExecutionType executionType = metaHolder.isCollapserAnnotationPresent() ? metaHolder.getCollapserExecutionType() : metaHolder.getExecutionType(); Object result; try { if (!metaHolder.isObservable()) { result = CommandExecutor.execute(invokable, executionType, metaHolder); } else { result = executeObservable(invokable, executionType, metaHolder);//见步骤5 } } catch (HystrixBadRequestException e) { throw e.getCause() != null ? e.getCause() : e; } catch (HystrixRuntimeException e) { throw hystrixRuntimeExceptionToThrowable(metaHolder, e); } return result; } 步骤1:获取切入点方法; 步骤2:根据方法的注解HystrixCommand或者HystrixCollapser生成相应的CommandMetaHolderFactory或者CollapserMetaHolderFactory类。 步骤3:将原方法的属性set进metaHolder中; 步骤4:根据metaHolder生成相应的HystrixCommand,包含加载hystrix配置信息。commandProperties加载的优先级为前缀hystrix.command.commandKey > hystrix.command.default > defaultValue(原代码默认);threadPool配置加载的优先级为 前缀hystrix.threadpool.groupKey.> hystrix.threadpool.default.> defaultValue(原代码默认). 步骤5:执行命令。 倘若需要给该方法指定groupKey和commandKey定义其fallback方法,则可通过添加注解属性来实现。如: @ResponseBody @RequestMapping("/test.html") @HystrixCommand(groupKey = "groupTest", commandKey = "commandTest", fallbackMethod = "back") public String test(int s) { try { Thread.sleep(s * 1000); } catch (Exception e) { } logger.info("test.html start"); return "OK"; } private String back(int s) { return "back"; } groupKey=”groupTest”是将该hystrix操作的组名定义为groupTest,该属性在读取threadPoolProperties时需要用到。读取的策略是先读取已groupTest为键值的配置缓存;若没有则读取已hystrix.threadpool.groupTest.为前缀的配置;若没有则读取hystrix.threadpool.为前缀的配置,最后才读取代码默认的值。 commandKey=”commandTest”是将hystrix操作的命令名定义为commandTest,该属性在读取commandProperties时需要用到。读取的策略与上面的一致,只是前缀由hystrix.threadpool变为hystrix.command。 fallbackMethod=”back”是给该hystrix操作定义一个回退方法,值为回退方法的方法名,并且要与回退方法在同一个类下、相同的参入参数和返回参数。fallbackMethod可级联。 如果要给该方法指定一些hystrix属性,可通过在hystrix.properties中添加一些配置来实现。如给上述方法添加一些hystrix属性,示例如下: #定义commandKey为commandTest的过期时间为3s hystrix.command.commandTest.execution.isolation.thread.timeoutInMilliseconds=3000 #定义所有的默认过期时间为5s,不再是默认是1s。优先级小于上面配置 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000 #定义threadPoolKey为groupTest的线程池大小为15 hystrix.threadpool.groupTest.coreSize=15 #定义所有的线程池大小为为5,不再是默认是10。优先级小于上面配置 hystrix.threadpool.default.coreSize=5 其余的配置方式与例子中的相似,就不一一列举了。 至此,spring mvc就可以为每一个依赖随心添加依赖隔离了。 三、监控hystrix除了隔离依赖服务的调用外,Hystrix还提供了近乎实时的监控,Hystrix会实时的,累加的记录所有关于HystrixCommand的执行信息,包括执行了每秒执行了多少请求,多少成功,多少失败等等。更多指标可以查看官网。 1、添加监控添加依赖使用maven引入hystrix-metrics-event-stream依赖: <dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-metrics-event-stream</artifactId> <version>1.1.2</version> </dependency> 修改web.xml在web.xml中添加代码: <servlet> <description></description> <display-name>HystrixMetricsStreamServlet</display-name> <servlet-name>HystrixMetricsStreamServlet</servlet-name> <servlet-class>com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HystrixMetricsStreamServlet</servlet-name> <url-pattern>/hystrix.stream</url-pattern> </servlet-mapping> 查看效果配置好后,重新启动应用,访问http://ip:port/appname/hystrix.stream,系统会不断刷新以获取实时的数据。 2、Dashboard可以看出,单纯使用字符输出的方式可读性太差,运维人员很难从中就看出系统的当前状态,于是Netflix又开发了一个开源项目dashboard来可视化这些数据,帮助运维人员更直观的了解系统的当前状态,Dashboard使用起来非常方便,其就是一个Web项目,你只需要把hystrix-dashboard.war包下载下来,放到一个Web容器(Tomcat,Jetty等)中即可。 启动容器,访问http://ip:port/hystrix-dashboard/#,就可以看到如下的界面: 按照上述操作点击monitor Streams,就可以查看该服务的hystrix监控了。监控界面如下: 可以看出,Dashboard主要展示了两类信息,一是HystrixCommand的执行情况,即circuit部分;二是线程池的状态,包括线程池名,大小,当前活跃线程说,最大活跃线程数,排队队列大小等,即Thread Pools部分。 然而,在复杂的分布式环境中,需要监控的不是单一一个ip的服务,可能需要监控一个集群甚至几个集群,而每个集群又可能有多个服务器,并且要可以扩展。倘若使用这种方案,运维人员需要添加N多监控路径。为解决该问题,Netflix又提供了一个开源项目Turbine来提供把多个hystrix.stream的内容聚合为一个数据源供Dashboard展示。 3、Turbine部署turbine操作: 下载turbine-web-1.0.0.war,并将war放入web容器中; 在容器下路径为turbine-web-1.0.0/WEB-INF/classes下新建config.properties文件; 根据实际情况配置相应参数,相应配置可以参考官网): 调用http://ip:port/turbine-web/turbine.stream?cluster=${clusterConfigName},查看是否有数据; 打开http://ip:port/hystrix-dashboard/#添加相应的turbine Stream。 配置详情主要包括三个方面: cluster配置:turbine一般会针对每一个cluster进行commandKeyThreadpoolcommandGroupKey数据聚合,其key名称为turbine.aggregator.clusterConfig,值为服务名称以逗号隔开; instances配置:每个服务对应的ip,其key名称为turbine.ConfigPropertyBasedDiscovery.${clusterConfigName}.instances,${clusterConfigName}为服务名,值为ip以逗号分隔; instanceUrlSuffix:hystrix监控url后缀,,其key名称为turbine.instanceUrlSuffix.${clusterConfigName},值为端口+路径,其路径一般为/hystrix.stream。 三者之间的关系是,先定义clusterConfigName,然后根据instances和instanceUrlSuffix拼接出相应url,多个instances会将其metrics统计在一起,然后在http://turbineIP:turbinePORT/turbine-web/turbine.stream?cluster=${clusterConfigName}下进行展示。 示例如下图: Dashboard操作如图: 展示界面如图: 至此,hystrix在spring mvc的应用及其监控操作全部完成。 四、参考链接https://github.com/Netflix/Hystrix/tree/master/hystrix-contrib/hystrix-javanica https://github.com/Netflix/Hystrix/wiki https://github.com/Netflix/Hystrix/wiki/Dashboard https://github.com/Netflix/Turbine/wiki https://github.com/Netflix/Turbine/wiki/Configuration-(1.x)) http://youdang.github.io/categories/%E7%BF%BB%E8%AF%91/ http://www.cnblogs.com/java-zhao/p/5521233.html 转载请标明出处:http://blog.springcloud.cnhttp://tech.lede.com/2017/06/15/rd/server/hystrix/本文出自网易乐得技术团队-投稿于Spring Cloud中国社区","categories":[{"name":"后端","slug":"后端","permalink":"http://blog.springcloud.cn/categories/后端/"}],"tags":[{"name":"Netflix Hystrix","slug":"Netflix-Hystrix","permalink":"http://blog.springcloud.cn/tags/Netflix-Hystrix/"},{"name":"Spring mvc","slug":"Spring-mvc","permalink":"http://blog.springcloud.cn/tags/Spring-mvc/"},{"name":"dashboard","slug":"dashboard","permalink":"http://blog.springcloud.cn/tags/dashboard/"},{"name":"turbine","slug":"turbine","permalink":"http://blog.springcloud.cn/tags/turbine/"}]},{"title":"Spring Cloud项目中通过Feign进行内部服务调用发生401\\407错误无返回信息的问题","slug":"sc/sc-feign-4xx","date":"2017-06-16T06:00:00.000Z","updated":"2017-06-17T07:38:15.000Z","comments":true,"path":"sc/sc-feign-4xx/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-feign-4xx/","excerpt":"上一篇:在Spring Cloud中实现降级之权重路由和标签路由 前言 最近好几个小伙伴,问Spring Cloud项目中通过Feign进行内部服务调用发生401\\407错误无返回信息的问题。这个问题如果没有自定义异常自定义Code或者系统中没有自定义code为401或407的code,基本很少能碰到。刚好Spring Cloud中国社区的VIP会员任聪博客原文也遇到这个,经过和他交流之后。整理出这篇文章希望能帮助更多的人快速定位问题。","text":"上一篇:在Spring Cloud中实现降级之权重路由和标签路由 前言 最近好几个小伙伴,问Spring Cloud项目中通过Feign进行内部服务调用发生401\\407错误无返回信息的问题。这个问题如果没有自定义异常自定义Code或者系统中没有自定义code为401或407的code,基本很少能碰到。刚好Spring Cloud中国社区的VIP会员任聪博客原文也遇到这个,经过和他交流之后。整理出这篇文章希望能帮助更多的人快速定位问题。 问题描述最近在使用Spring Cloud改造现有服务的工作中,在内部服务的调用方式上选择了Feign组件,由于服务与服务之间有权限控制,发现通过Feign来进行调用时如果发生了401、407错误时,调用方不能够取回被调用方返回的错误信息。 产生原因分析产生原因Feign默认使用java.net.HttpURLConnection进行通信,通过查看其子类sun.net.www.protocol.http.HttpURLConnection源码发现代码中在进行通信时单独对错误码为401\\407的错误请求做了处理,当请求的错误码为401\\407时,会关闭请求流,由于此时还并没有将返回的错误信息写入响应流中,所以接收的返回信息中仅仅能获取到response.status(),而response.body()为null。HttpURLConnection相关信息的源码链接 问题源代码示例1234567if (respCode == HTTP_UNAUTHORIZED) { if (streaming()) { disconnectInternal(); throw new HttpRetryException (RETRY_MSG2, HTTP_UNAUTHORIZED); } //其余代码省略} java.net.HttpURLConnection中的HTTP_UNAUTHORIZED的定义如下: 1public static final int HTTP_UNAUTHORIZED = 401; 解决思路关于此问题产生的原因已经很明显了,就是feign.Client实现通信的方式选用了我们不想使用的HttpURLConnection。想到通常在Spring的代码中OCP都是运用得很好的,所以基本上有解决此问题的信心了,最不济就是自己扩展Feign,实现一个自己想要的feign.Client,当然这种事情Spring Cloud基本都会自己搞定,这也是Spring Cloud强大完善的一个地方。通过这个思路查看源码,果然看到了Spring Cloud在使用Feign提前内置了三种通信方式(feign.Client.Default,feign.httpclient.ApacheHttpClient,feign.okhttp.OkHttpClient),其中缺省的情况使用的就是feign.Client.Default,这个就是使用HttpURLConnection通信的方式。 源码解析在Spring Cloud项目中使用了Ribbon的组件,其会帮助我们管理使用Feign,查看org.springframework.cloud.netflix.feign.ribbon.FeignRibbonClientAutoConfiguration源码 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859@ConditionalOnClass({ ILoadBalancer.class, Feign.class })@Configuration@AutoConfigureBefore(FeignAutoConfiguration.class)public class FeignRibbonClientAutoConfiguration { @Bean @ConditionalOnMissingBean public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) { return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory, clientFactory); } @Configuration @ConditionalOnClass(ApacheHttpClient.class) @ConditionalOnProperty(value = \"feign.httpclient.enabled\", matchIfMissing = true) protected static class HttpClientFeignLoadBalancedConfiguration { @Autowired(required = false) private HttpClient httpClient; @Bean @ConditionalOnMissingBean(Client.class) public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) { ApacheHttpClient delegate; if (this.httpClient != null) { delegate = new ApacheHttpClient(this.httpClient); } else { delegate = new ApacheHttpClient(); } return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory); } } @Configuration @ConditionalOnClass(OkHttpClient.class) @ConditionalOnProperty(value = \"feign.okhttp.enabled\", matchIfMissing = true) protected static class OkHttpFeignLoadBalancedConfiguration { @Autowired(required = false) private okhttp3.OkHttpClient okHttpClient; @Bean @ConditionalOnMissingBean(Client.class) public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) { OkHttpClient delegate; if (this.okHttpClient != null) { delegate = new OkHttpClient(this.okHttpClient); } else { delegate = new OkHttpClient(); } return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory); } }} 从feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) 方法结合其上注解我们可以很清楚的知道,当没有feign.ClientBean的时候会默认生成feign.Client.Default来进行通信,这就是之前说的缺省通信方式 从HttpClientFeignLoadBalancedConfiguration、OkHttpFeignLoadBalancedConfiguration,我们可以看到其生效的条件,当classpath中有feign.httpclient.ApacheHttpClient并且配置feign.httpclient.enabled=true(缺省为true)、feign.okhttp.OkHttpClient并且配置feign.okhttp.enabled=true(缺省为true) 当使用ApacheHttpClient或者OkHttpClient进行通信时就不会导致发生401\\407错误时,取不到返回的错误信息了 解决方法通过其上的分析,解决方法已经显而易见了替换默认的Client pom.xml文件中新增依赖替换为默认为okhttp Client 增加依赖 12345<dependency> <groupId>com.netflix.feign</groupId> <artifactId>feign-okhttp</artifactId> <version>8.18.0</version> </dependency> 2.在application.properties增加配置如下: 1feign.okhttp.enabled=true 如何把默认的Client替换为okhttp在这里不做过多阐述,可以参考: 替换为httpclient 增加依赖 12345<dependency> <groupId>com.netflix.feign</groupId> <artifactId>feign-httpclient</artifactId> <version>8.18.0</version> </dependency> 2.在application.properties增加配置如下: 1feign.httpclient.enabled=true 可以参考更换Feign默认使用的HTTP Client 总结 由于新增的依赖没有被start管理,并且缺省不会导致程序启动异常,并且返回响应为null与此依赖没有直接关系,因此不方便定位到问题,特此记录下来,希望能帮助到遇到同样问题的人,如对文章有不同的看法,望给予指正。 本文建立在已经搭建完成Feign的调用基础之上,没有讲述Feign的使用,因为此类文章很多,在此就不重复了,更多的信息可以参考如下文章。 快速使用Spring Cloud Feign作为客户端调用服务提供者spring cloud feign使用okhttp3","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Feign","slug":"Spring-Cloud-Feign","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Feign/"}]},{"title":"Spring Cloud之深入理解Feign之源码解析","slug":"sc/sc-w-feign","date":"2017-06-16T06:00:00.000Z","updated":"2017-06-20T13:03:49.000Z","comments":true,"path":"sc/sc-w-feign/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-w-feign/","excerpt":"","text":"上一篇:深入理解Eureka 什么是FeignFeign是受到Retrofit,JAXRS-2.0和WebSocket的影响,它是一个jav的到http客户端绑定的开源项目。 Feign的主要目标是将Java Http 客户端变得简单。Feign的源码地址:https://github.com/OpenFeign/feign 写一个Feign在我之前的博文有写到如何用Feign去消费服务,文章地址:http://blog.csdn.net/forezp/article/details/69808079 。 现在来简单的实现一个Feign客户端,首先通过@FeignClient,客户端,其中value为调用其他服务的名称,FeignConfig.class为FeignClient的配置文件,代码如下: 123456@FeignClient(value = "service-hi",configuration = FeignConfig.class)public interface SchedualServiceHi { @GetMapping(value = "/hi") String sayHiFromClientOne(@RequestParam(value = "name") String name);} 其自定义配置文件如下,当然也可以不写配置文件,用默认的即可: 123456789@Configurationpublic class FeignConfig { @Bean public Retryer feignRetryer() { return new Retryer.Default(100, SECONDS.toMillis(1), 5); } } 查看FeignClient注解的源码,其代码如下: 1234567891011121314151617181920212223242526@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface FeignClient {@AliasFor("name")String value() default ""; @AliasFor("value")String name() default ""; @AliasFor("value")String name() default "";String url() default "";boolean decode404() default false;Class<?>[] configuration() default {};Class<?> fallback() default void.class;Class<?> fallbackFactory() default void.class;}String path() default "";boolean primary() default true; FeignClient注解被@Target(ElementType.TYPE)修饰,表示FeignClient注解的作用目标在接口上;@Retention(RetentionPolicy.RUNTIME),注解会在class字节码文件中存在,在运行时可以通过反射获取到;@Documented表示该注解将被包含在javadoc中。 feign 用于声明具有该接口的REST客户端的接口的注释应该是创建(例如用于自动连接到另一个组件。 如果功能区可用,那将是用于负载平衡后端请求,并且可以配置负载平衡器使用与伪装客户端相同名称(即值)@RibbonClient 。 其中value()和name()一样,是被调用的 service的名称。url(),直接填写硬编码的url,decode404()即404是否被解码,还是抛异常;configuration(),标明FeignClient的配置类,默认的配置类为FeignClientsConfiguration类,可以覆盖Decoder、Encoder和Contract等信息,进行自定义配置。fallback(),填写熔断器的信息类。 FeignClient的配置默认的配置类为FeignClientsConfiguration,这个类在spring-cloud-netflix-core的jar包下,打开这个类,可以发现它是一个配置类,注入了很多的相关配置的bean,包括feignRetryer、FeignLoggerFactory、FormattingConversionService等,其中还包括了Decoder、Encoder、Contract,如果这三个bean在没有注入的情况下,会自动注入默认的配置。 Decoder feignDecoder: ResponseEntityDecoder(这是对SpringDecoder的封装) Encoder feignEncoder: SpringEncoder Logger feignLogger: Slf4jLogger Contract feignContract: SpringMvcContract Feign.Builder feignBuilder: HystrixFeign.Builder 代码如下: 12345678910111213141516171819202122232425@Configurationpublic class FeignClientsConfiguration {...//省略代码@Bean @ConditionalOnMissingBean public Decoder feignDecoder() { return new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)); } @Bean @ConditionalOnMissingBean public Encoder feignEncoder() { return new SpringEncoder(this.messageConverters); } @Bean @ConditionalOnMissingBean public Contract feignContract(ConversionService feignConversionService) { return new SpringMvcContract(this.parameterProcessors, feignConversionService); }...//省略代码} 重写配置: 你可以重写FeignClientsConfiguration中的bean,从而达到自定义配置的目的,比如FeignClientsConfiguration的默认重试次数为Retryer.NEVER_RETRY,即不重试,那么希望做到重写,写个配置文件,注入feignRetryer的bean,代码如下: 123456789@Configurationpublic class FeignConfig { @Bean public Retryer feignRetryer() { return new Retryer.Default(100, SECONDS.toMillis(1), 5); }} 在上述代码更改了该FeignClient的重试次数,重试间隔为100ms,最大重试时间为1s,重试次数为5次。 Feign的工作原理feign是一个伪客户端,即它不做任何的请求处理。Feign通过处理注解生成request,从而实现简化HTTP API开发的目的,即开发人员可以使用注解的方式定制request api模板,在发送http request请求之前,feign通过处理注解的方式替换掉request模板中的参数,这种实现方式显得更为直接、可理解。 通过包扫描注入FeignClient的bean,该源码在FeignClientsRegistrar类:首先在启动配置上检查是否有@EnableFeignClients注解,如果有该注解,则开启包扫描,扫描被@FeignClient注解接口。代码如下: 1234567891011121314151617private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { Map<String, Object> defaultAttrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName(), true); if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) { String name; if (metadata.hasEnclosingClass()) { name = "default." + metadata.getEnclosingClassName(); } else { name = "default." + metadata.getClassName(); } registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration")); } } 程序启动后通过包扫描,当类有@FeignClient注解,将注解的信息取出,连同类名一起取出,赋给BeanDefinitionBuilder,然后根据BeanDefinitionBuilder得到beanDefinition,最后beanDefinition式注入到ioc容器中,源码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); Set<String> basePackages; Map<String, Object> attrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName()); AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter( FeignClient.class); final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients"); if (clients == null || clients.length == 0) { scanner.addIncludeFilter(annotationTypeFilter); basePackages = getBasePackages(metadata); } else { final Set<String> clientClasses = new HashSet<>(); basePackages = new HashSet<>(); for (Class<?> clazz : clients) { basePackages.add(ClassUtils.getPackageName(clazz)); clientClasses.add(clazz.getCanonicalName()); } AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() { @Override protected boolean match(ClassMetadata metadata) { String cleaned = metadata.getClassName().replaceAll("\\\\$", "."); return clientClasses.contains(cleaned); } }; scanner.addIncludeFilter( new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter))); } for (String basePackage : basePackages) { Set<BeanDefinition> candidateComponents = scanner .findCandidateComponents(basePackage); for (BeanDefinition candidateComponent : candidateComponents) { if (candidateComponent instanceof AnnotatedBeanDefinition) { // verify annotated class is an interface AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent; AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); Map<String, Object> attributes = annotationMetadata .getAnnotationAttributes( FeignClient.class.getCanonicalName()); String name = getClientName(attributes); registerClientConfiguration(registry, name, attributes.get("configuration")); registerFeignClient(registry, annotationMetadata, attributes); } } } }private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { String className = annotationMetadata.getClassName(); BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); validate(attributes); definition.addPropertyValue("url", getUrl(attributes)); definition.addPropertyValue("path", getPath(attributes)); String name = getName(attributes); definition.addPropertyValue("name", name); definition.addPropertyValue("type", className); definition.addPropertyValue("decode404", attributes.get("decode404")); definition.addPropertyValue("fallback", attributes.get("fallback")); definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); String alias = name + "FeignClient"; AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null beanDefinition.setPrimary(primary); String qualifier = getQualifier(attributes); if (StringUtils.hasText(qualifier)) { alias = qualifier; } BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias }); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); } 注入bean之后,通过jdk的代理,当请求Feign Client的方法时会被拦截,代码在ReflectiveFeign类,代码如下: 123456789101112131415161718192021222324public <T> T newInstance(Target<T> target) { Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); for (Method method : target.type().getMethods()) { if (method.getDeclaringClass() == Object.class) { continue; } else if(Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler); for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; } 在SynchronousMethodHandler类进行拦截处理,当被FeignClient的方法被拦截会根据参数生成RequestTemplate对象,该对象就是http请求的模板,代码如下: 12345678910111213141516@Override public Object invoke(Object[] argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); Retryer retryer = this.retryer.clone(); while (true) { try { return executeAndDecode(template); } catch (RetryableException e) { retryer.continueOrPropagate(e); if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue; } } } 其中有个executeAndDecode()方法,该方法是通RequestTemplate生成Request请求对象,然后根据用client获取response。 1234567 Object executeAndDecode(RequestTemplate template) throws Throwable { Request request = targetRequest(template); ...//省略代码 response = client.execute(request, options); ...//省略代码} Client组件其中Client组件是一个非常重要的组件,Feign最终发送request请求以及接收response响应,都是由Client组件完成的,其中Client的实现类,只要有Client.Default,该类由HttpURLConnnection实现网络请求,另外还支持HttpClient、Okhttp. 首先来看以下在FeignRibbonClient的自动配置类,FeignRibbonClientAutoConfiguration ,主要在工程启动的时候注入一些bean,其代码如下: 1234567891011121314@ConditionalOnClass({ ILoadBalancer.class, Feign.class })@Configuration@AutoConfigureBefore(FeignAutoConfiguration.class)public class FeignRibbonClientAutoConfiguration {@Bean @ConditionalOnMissingBean public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) { return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory, clientFactory); }} 在缺失配置feignClient的情况下,会自动注入new Client.Default(),跟踪Client.Default()源码,它使用的网络请求框架为HttpURLConnection,代码如下: 12345@Override public Response execute(Request request, Options options) throws IOException { HttpURLConnection connection = convertAndSend(request, options); return convertResponse(connection).toBuilder().request(request).build(); } 怎么在feign中使用HttpClient,查看FeignRibbonClientAutoConfiguration的源码 12345678910111213141516171819202122232425262728293031@ConditionalOnClass({ ILoadBalancer.class, Feign.class })@Configuration@AutoConfigureBefore(FeignAutoConfiguration.class)public class FeignRibbonClientAutoConfiguration {...//省略代码@Configuration @ConditionalOnClass(ApacheHttpClient.class) @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true) protected static class HttpClientFeignLoadBalancedConfiguration { @Autowired(required = false) private HttpClient httpClient; @Bean @ConditionalOnMissingBean(Client.class) public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) { ApacheHttpClient delegate; if (this.httpClient != null) { delegate = new ApacheHttpClient(this.httpClient); } else { delegate = new ApacheHttpClient(); } return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory); } }...//省略代码} 从代码@ConditionalOnClass(ApacheHttpClient.class)注解可知道,只需要在pom文件加上HttpClient的classpath就行了,另外需要在配置文件上加上feign.httpclient.enabled为true,从 @ConditionalOnProperty注解可知,这个可以不写,在默认的情况下就为true. 在pom文件加上: 123456<dependency> <groupId>com.netflix.feign</groupId> <artifactId>feign-httpclient</artifactId> <version>RELEASE</version></dependency> 同理,如果想要feign使用Okhttp,则只需要在pom文件上加上feign-okhttp的依赖: 12345<dependency> <groupId>com.netflix.feign</groupId> <artifactId>feign-okhttp</artifactId> <version>RELEASE</version></dependency> feign的负载均衡是怎么样实现的呢?通过上述的FeignRibbonClientAutoConfiguration类配置Client的类型(httpurlconnection,okhttp和httpclient)时候,可知最终向容器注入的是LoadBalancerFeignClient,即负载均衡客户端。现在来看下LoadBalancerFeignClient的代码: 123456789101112131415161718192021@Overridepublic Response execute(Request request, Request.Options options) throws IOException { try { URI asUri = URI.create(request.url()); String clientName = asUri.getHost(); URI uriWithoutHost = cleanUrl(request.url(), clientName); FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest( this.delegate, request, uriWithoutHost); IClientConfig requestConfig = getClientConfig(options, clientName); return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse(); } catch (ClientException e) { IOException io = findIOException(e); if (io != null) { throw io; } throw new RuntimeException(e); }} 其中有个executeWithLoadBalancer()方法,即通过负载均衡的方式请求。 1234567891011121314151617181920212223242526272829303132333435public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException { RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, requestConfig); LoadBalancerCommand<T> command = LoadBalancerCommand.<T>builder() .withLoadBalancerContext(this) .withRetryHandler(handler) .withLoadBalancerURI(request.getUri()) .build(); try { return command.submit( new ServerOperation<T>() { @Override public Observable<T> call(Server server) { URI finalUri = reconstructURIWithServer(server, request.getUri()); S requestForServer = (S) request.replaceUri(finalUri); try { return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig)); } catch (Exception e) { return Observable.error(e); } } }) .toBlocking() .single(); } catch (Exception e) { Throwable t = e.getCause(); if (t instanceof ClientException) { throw (ClientException) t; } else { throw new ClientException(e); } } } 其中服务在submit()方法上,点击submit进入具体的方法,这个方法是LoadBalancerCommand的方法: 123456789Observable<T> o = (server == null ? selectServer() : Observable.just(server)) .concatMap(new Func1<Server, Observable<T>>() { @Override // Called for each server being selected public Observable<T> call(Server server) { context.setServer(server); }} 上述代码中有个selectServe(),该方法是选择服务的进行负载均衡的方法,代码如下: 1234567891011121314private Observable<Server> selectServer() { return Observable.create(new OnSubscribe<Server>() { @Override public void call(Subscriber<? super Server> next) { try { Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey); next.onNext(server); next.onCompleted(); } catch (Exception e) { next.onError(e); } } });} 最终负载均衡交给loadBalancerContext来处理,即之前讲述的Ribbon,在这里不再重复。 总结总到来说,Feign的源码实现的过程如下: 首先通过@EnableFeignCleints注解开启FeignCleint 根据Feign的规则实现接口,并加@FeignCleint注解 程序启动后,会进行包扫描,扫描所有的@ FeignCleint的注解的类,并将这些信息注入到ioc容器中。 当接口的方法被调用,通过jdk的代理,来生成具体的RequesTemplate RequesTemplate在生成Request Request交给Client去处理,其中Client可以是HttpUrlConnection、HttpClient也可以是Okhttp 最后Client被封装到LoadBalanceClient类,这个类结合类Ribbon做到了负载均衡。 转载请标明出处:http://blog.csdn.net/forezp/article/details/73480304本文出自方志朋的博客 参考资料https://github.com/OpenFeign/feign https://blog.de-swaef.eu/the-netflix-stack-using-spring-boot-part-3-feign/","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Feign","slug":"Spring-Cloud-Feign","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Feign/"}]},{"title":"Spring Cloud之深入理解Eureka之源码解析","slug":"sc/sc-w-eureka","date":"2017-06-16T06:00:00.000Z","updated":"2017-06-20T13:03:39.000Z","comments":true,"path":"sc/sc-w-eureka/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-w-eureka/","excerpt":"","text":"上一篇:Spring Cloud项目中通过Feign进行内部服务调用发生401\\407错误无返回信息的问题 Eureka的一些概念 Register:服务注册当Eureka客户端向Eureka Server注册时,它提供自身的元数据,比如IP地址、端口,运行状况指示符URL,主页等。 Renew:服务续约Eureka客户会每隔30秒发送一次心跳来续约。 通过续约来告知Eureka Server该Eureka客户仍然存在,没有出现问题。 正常情况下,如果Eureka Server在90秒没有收到Eureka客户的续约,它会将实例从其注册表中删除。 建议不要更改续约间隔。 Fetch Registries:获取注册列表信息Eureka客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与Eureka客户端的缓存信息不同, Eureka客户端自动处理。如果由于某种原因导致注册列表信息不能及时匹配,Eureka客户端则会重新获取整个注册表信息。 Eureka服务器缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka客户端和Eureka 服务器可以使用JSON / XML格式进行通讯。在默认的情况下Eureka客户端使用压缩JSON格式来获取注册列表的信息。 Cancel:服务下线Eureka客户端在程序关闭时向Eureka服务器发送取消请求。 发送请求后,该客户端实例信息将从服务器的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:DiscoveryManager.getInstance().shutdownComponent(); Eviction 服务剔除在默认的情况下,当Eureka客户端连续90秒没有向Eureka服务器发送服务续约,即心跳,Eureka服务器会将该服务实例从服务注册列表删除,即服务剔除。 Eureka的高可用架构如图为Eureka的高级架构图,该图片来自于Eureka开源代码的文档,地址为https://github.com/Netflix/eureka/wiki/Eureka-at-a-glance 。 从图可以看出在这个体系中,有2个角色,即Eureka Server和Eureka Client。而Eureka Client又分为Applicaton Service和Application Client,即服务提供者何服务消费者。 每个区域有一个Eureka集群,并且每个区域至少有一个eureka服务器可以处理区域故障,以防服务器瘫痪。 Eureka Client向Eureka Serve注册,并将自己的一些客户端信息发送Eureka Serve。然后,Eureka Client通过向Eureka Serve发送心跳(每30秒)来续约服务的。 如果客户端持续不能续约,那么,它将在大约90秒内从服务器注册表中删除。 注册信息和续订被复制到集群中的Eureka Serve所有节点。 来自任何区域的Eureka Client都可以查找注册表信息(每30秒发生一次)。根据这些注册表信息,Application Client可以远程调用Applicaton Service来消费服务。 Register服务注册服务注册,即Eureka Client向Eureka Server提交自己的服务信息,包括IP地址、端口、service ID等信息。如果Eureka Client没有写service ID,则默认为 ${spring.application.name}。 服务注册其实很简单,在Eureka Client启动的时候,将自身的服务的信息发送到Eureka Server。现在来简单的阅读下源码。在Maven的依赖包下,找到eureka-client-1.6.2.jar包。在com.netflix.discovery包下有个DiscoveryClient类,该类包含了Eureka Client向Eureka Server的相关方法。其中DiscoveryClient实现了EurekaClient接口,并且它是一个单例模式,而EurekaClient继承了LookupService接口。它们之间的关系如图所示。 在DiscoveryClient类有一个服务注册的方法register(),该方法是通过Http请求向Eureka Client注册。其代码如下: 1234567891011121314boolean register() throws Throwable { logger.info(PREFIX + appPathIdentifier + ": registering service..."); EurekaHttpResponse<Void> httpResponse; try { httpResponse = eurekaTransport.registrationClient.register(instanceInfo); } catch (Exception e) { logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e); throw e; } if (logger.isInfoEnabled()) { logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode()); } return httpResponse.getStatusCode() == 204; } 在DiscoveryClient类继续追踪register()方法,它被InstanceInfoReplicator 类的run()方法调用,其中InstanceInfoReplicator实现了Runnable接口,run()方法代码如下: 12345678910111213141516public void run() { try { discoveryClient.refreshInstanceInfo(); Long dirtyTimestamp = instanceInfo.isDirtyWithTime(); if (dirtyTimestamp != null) { discoveryClient.register(); instanceInfo.unsetIsDirty(dirtyTimestamp); } } catch (Throwable t) { logger.warn("There was a problem with the instance info replicator", t); } finally { Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); } } 而InstanceInfoReplicator类是在DiscoveryClient初始化过程中使用的,其中有一个initScheduledTasks()方法。该方法主要开启了获取服务注册列表的信息,如果需要向Eureka Server注册,则开启注册,同时开启了定时向Eureka Server服务续约的定时任务,具体代码如下: 1234567891011121314151617181920212223242526272829303132333435363738private void initScheduledTasks() { ...//省略了任务调度获取注册列表的代码 if (clientConfig.shouldRegisterWithEureka()) { ... // Heartbeat timer scheduler.schedule( new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread() ), renewalIntervalInSecs, TimeUnit.SECONDS); // InstanceInfo replicator instanceInfoReplicator = new InstanceInfoReplicator( this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2); // burstSize statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { @Override public String getId() { return "statusChangeListener"; } @Override public void notify(StatusChangeEvent statusChangeEvent) { instanceInfoReplicator.onDemandUpdate(); } }; ... } 然后在来看Eureka server端的代码,在Maven的eureka-core:1.6.2的jar包下。打开com.netflix.eureka包,很轻松的就发现了又一个EurekaBootStrap的类,BootStrapContext具有最先初始化的权限,所以先看这个类。 1234567891011121314151617181920212223protected void initEurekaServerContext() throws Exception { ...//省略代码 PeerAwareInstanceRegistry registry; if (isAws(applicationInfoManager.getInfo())) { ...//省略代码,如果是AWS的代码 } else { registry = new PeerAwareInstanceRegistryImpl( eurekaServerConfig, eurekaClient.getEurekaClientConfig(), serverCodecs, eurekaClient ); } PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes( registry, eurekaServerConfig, eurekaClient.getEurekaClientConfig(), serverCodecs, applicationInfoManager ); } 其中PeerAwareInstanceRegistryImpl和PeerEurekaNodes两个类看其命名,应该和服务注册以及Eureka Server高可用有关。先追踪PeerAwareInstanceRegistryImpl类,在该类有个register()方法,该方法提供了注册,并且将注册后信息同步到其他的Eureka Server服务。代码如下: 12345678public void register(final InstanceInfo info, final boolean isReplication) { int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS; if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) { leaseDuration = info.getLeaseInfo().getDurationInSecs(); } super.register(info, leaseDuration, isReplication); replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication); } 其中 super.register(info, leaseDuration, isReplication)方法,点击进去到子类AbstractInstanceRegistry可以发现更多细节,其中注册列表的信息被保存在一个Map中。replicateToPeers()方法,即同步到其他Eureka Server的其他Peers节点,追踪代码,发现它会遍历循环向所有的Peers节点注册,最终执行类PeerEurekaNodes的register()方法,该方法通过执行一个任务向其他节点同步该注册信息,代码如下: 123456789101112public void register(final InstanceInfo info) throws Exception { long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info); batchingDispatcher.process( taskId("register", info), new InstanceReplicationTask(targetHost, Action.Register, info, null, true) { public EurekaHttpResponse<Void> execute() { return replicationClient.register(info); } }, expiryTime ); } 经过一系列的源码追踪,可以发现PeerAwareInstanceRegistryImpl的register()方法实现了服务的注册,并且向其他Eureka Server的Peer节点同步了该注册信息,那么register()方法被谁调用了呢?之前在Eureka Client的分析可以知道,Eureka Client是通过 http来向Eureka Server注册的,那么Eureka Server肯定会提供一个注册的接口给Eureka Client调用,那么PeerAwareInstanceRegistryImpl的register()方法肯定最终会被暴露的Http接口所调用。在Idea开发工具,按住alt+鼠标左键,可以很快定位到ApplicationResource类的addInstance ()方法,即服务注册的接口,其代码如下: 12345678910@POST @Consumes({"application/json", "application/xml"}) public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) { ...//省略代码 registry.register(info, "true".equals(isReplication)); return Response.status(204).build(); // 204 to be backwards compatible } Renew服务续约服务续约和服务注册非常类似,通过之前的分析可以知道,服务注册在Eureka Client程序启动之后开启,并同时开启服务续约的定时任务。在eureka-client-1.6.2.jar的DiscoveryClient的类下有renew()方法,其代码如下: 12345678910111213141516171819/** * Renew with the eureka service by making the appropriate REST call */ boolean renew() { EurekaHttpResponse<InstanceInfo> httpResponse; try { httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null); logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode()); if (httpResponse.getStatusCode() == 404) { REREGISTER_COUNTER.increment(); logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName()); return register(); } return httpResponse.getStatusCode() == 200; } catch (Throwable e) { logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e); return false; } } 另外服务端的续约接口在eureka-core:1.6.2.jar的 com.netflix.eureka包下的InstanceResource类下,接口方法为renewLease(),它是REST接口。为了减少类篇幅,省略了大部分代码的展示。其中有个registry.renew()方法,即服务续约,代码如下: 123456@PUTpublic Response renewLease(...参数省略){ ... 代码省略 boolean isSuccess=registry.renew(app.getName(),id, isFromReplicaNode); ... 代码省略 } 读者可以跟踪registry.renew的代码一直深入研究。在这里就不再多讲述。另外服务续约有2个参数是可以配置,即Eureka Client发送续约心跳的时间参数和Eureka Server在多长时间内没有收到心跳将实例剔除的时间参数,在默认的情况下这两个参数分别为30秒和90秒,官方给的建议是不要修改,如果有特殊要求还是可以调整的,只需要分别在Eureka Client和Eureka Server修改以下参数: 12eureka.instance.leaseRenewalIntervalInSecondseureka.instance.leaseExpirationDurationInSeconds 最后,服务注册列表的获取、服务下线和服务剔除就不在这里进行源码跟踪解读,因为和服务注册和续约类似,有兴趣的朋友可以自己看下源码,深入理解。总的来说,通过读源码,可以发现,整体架构与前面小节的eureka 的高可用架构图完全一致。 Eureka Client注册一个实例为什么这么慢 Eureka Client一启动(不是启动完成),不是立即向Eureka Server注册,它有一个延迟向服务端注册的时间,通过跟踪源码,可以发现默认的延迟时间为40秒,源码在eureka-client-1.6.2.jar的DefaultEurekaClientConfig类下,代码如下: 1234public int getInitialInstanceInfoReplicationIntervalSeconds() { return configInstance.getIntProperty( namespace + INITIAL_REGISTRATION_REPLICATION_DELAY_KEY, 40).get(); } Eureka Server的响应缓存Eureka Server维护每30秒更新的响应缓存,可通过更改配置eureka.server.responseCacheUpdateIntervalMs来修改。 所以即使实例刚刚注册,它也不会出现在调用/ eureka / apps REST端点的结果中。 Eureka Server刷新缓存Eureka客户端保留注册表信息的缓存。 该缓存每30秒更新一次(如前所述)。 因 此,客户端决定刷新其本地缓存并发现其他新注册的实例可能需要30秒。 LoadBalancer RefreshRibbon的负载平衡器从本地的Eureka Client获取服务注册列表信息。Ribbon本身还维护本地缓存,以避免为每个请求调用本地客户端。 此缓存每30秒刷新一次(可由ribbon.ServerListRefreshInterval配置)。 所以,可能需要30多秒才能使用新注册的实例。 综上几个因素,一个新注册的实例,特别是启动较快的实例(默认延迟40秒注册),不能马上被Eureka Server发现。另外,刚注册的Eureka Client也不能立即被其他服务调用,因为调用方因为各种缓存没有及时的获取到新的注册列表。 Eureka 的自我保护模式当一个新的Eureka Server出现时,它尝试从相邻节点获取所有实例注册表信息。如果从Peer节点获取信息时出现问题,Eureka Serve会尝试其他的Peer节点。如果服务器能够成功获取所有实例,则根据该信息设置应该接收的更新阈值。如果有任何时间,Eureka Serve接收到的续约低于为该值配置的百分比(默认为15分钟内低于85%),则服务器开启自我保护模式,即不再剔除注册列表的信息。 这样做的好处就是,如果是Eureka Server自身的网络问题,导致Eureka Client的续约不上,Eureka Client的注册列表信息不再被删除,也就是Eureka Client还可以被其他服务消费。 转载请标明出处:http://blog.csdn.net/forezp/article/details/73017664本文出自方志朋的博客 参考资料http://cloud.spring.io/spring-cloud-static/Dalston.RELEASE/#netflix-eureka-client-starter https://github.com/Netflix/eureka/wiki https://github.com/Netflix/eureka/wiki/Understanding-Eureka-Peer-to-Peer-Communication http://xujin.org/sc/sc-eureka-register/ http://blog.abhijitsarkar.org/technical/netflix-eureka/ http://nobodyiam.com/2016/06/25/dive-into-eureka/","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"}]},{"title":"在Spring Cloud中实现降级之权重路由和标签路由","slug":"sc/sc-ribbon-demoted","date":"2017-06-03T06:00:00.000Z","updated":"2017-06-17T05:06:55.000Z","comments":true,"path":"sc/sc-ribbon-demoted/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-ribbon-demoted/","excerpt":"前言 限流、降级、灰度是服务治理的一个很重要的功能。本文参考Spring Cloud中国社区的VIP会员-何鹰的博客-整理Dubbo自带服务降级、限流功能,spring cloud并没有提供此功能,只能由我们自行实现。这里的限流、降级、灰度都是针对服务实例级别,并不是整个服务级别,整个服务级别可以通过实例部署数量来实现。 限流降级设计场景服务A,部署了3个实例A1、A2、A3。spring cloud默认客户端负载均衡策略是采用轮询方式,A1、A2、A3三个实例流量均分,各1/3。如果这个时候需要将服务A由1.0版升级至2.0版,我们需要做的步骤是:将A1的流量降为0,柔性下线,关闭A1实例并升级到2.0,将A1流量提升为10%观察2.0线上运行情况,如果情况稳定,则逐步开放流量至不限制及1/3。依次在A2,A3上执行上述操作。在上述步骤中,我们想让特别的人使用2.0,其他人还是使用1.0版,稳定后再全员开放。","text":"前言 限流、降级、灰度是服务治理的一个很重要的功能。本文参考Spring Cloud中国社区的VIP会员-何鹰的博客-整理Dubbo自带服务降级、限流功能,spring cloud并没有提供此功能,只能由我们自行实现。这里的限流、降级、灰度都是针对服务实例级别,并不是整个服务级别,整个服务级别可以通过实例部署数量来实现。 限流降级设计场景服务A,部署了3个实例A1、A2、A3。spring cloud默认客户端负载均衡策略是采用轮询方式,A1、A2、A3三个实例流量均分,各1/3。如果这个时候需要将服务A由1.0版升级至2.0版,我们需要做的步骤是:将A1的流量降为0,柔性下线,关闭A1实例并升级到2.0,将A1流量提升为10%观察2.0线上运行情况,如果情况稳定,则逐步开放流量至不限制及1/3。依次在A2,A3上执行上述操作。在上述步骤中,我们想让特别的人使用2.0,其他人还是使用1.0版,稳定后再全员开放。 思路分析,服务A的流量产生有两个方面,一个是外部流量,外网通过zuul过来的流量,一个是内部流量,服务间调用,服务B调用服务A的这类流量。不管是zuul还是内部服务来的,都是要通过ribbon做客户端负载均衡,我们可以修改ribbon负载均衡策略来实现上述限流、降级、灰度功能。 要实现这些想法,我们需要对spring-cloud的各个组件、数据流非常熟悉,这样才能知道该在哪里做扩展。一个典型的调用:外网-》Zuul网关-》服务A-》服务B。。。 spring-cloud跟dubbo一样都是客户端负载均衡,所有调用均由Ribbon来做负载均衡选择服务器,所有调用前后会套一层hystrix做隔离、熔断。服务间调用均用带LoadBalanced注解的RestTemplate发出。RestTemplate-》Ribbon-》hystrix 通过上述分析我们可以看到,我们的扩展点就在Ribbon,Ribbon根据我们的规则,选择正确的服务器即可。 我们先来一个dubbo自带的功能:基于权重的流量控制。dubbo自带的控制台可以设置服务实例粒度的半权,倍权。其实就是在客户端负载均衡时,选择服务器带上权重即可,spring-cloud默认是ZoneAvoidanceRule,优先选择相同Zone下的实例,实例间采用轮询方式做负载均衡。我们的想把基于轮询改为基于权重即可。接下来的问题是,每个实例的权重信息保存在哪里?从哪里取?dubbo放在zookeeper中,spring-cloud放在eureka中。我们只需从eureka拿每个实例的权重信息,然后根据权重来选择服务器即可。具体代码LabelAndWeightMetadataRule(先忽略里面的优先匹配label相关代码)。 工程案例演示 https://github.com/SoftwareKing/spring-cloud-study/tree/master/sc-ribbon-demoted 项目结构 config 配置中心端口:8888,方便起见直接读取配置文件,生产环境可以读取git。application-dev.properties为全局配置。先启动配置中心,所有服务的配置(包括注册中心的地址)均从配置中心读取。 consumer 服务消费者端口:18090,调用服务提供者,为了演示header传递。 core 框架核心包核心jar包,所有微服务均引用该包,使用AutoConfig实现免配置,模拟生产环境下spring-cloud的使用。 eureka 注册中心端口:8761,/metadata端点实现metadata信息配置。 provider 服务提供者端口:18090,服务提供者,无特殊逻辑。 zuul 网关端口:8080,演示解析token获得label并放入header往后传递 案例具体实现基于权重的实现思路LabelAndWeightMetadataRule写好了,那么我们如何使用它,使之生效呢?有3种方式。 1)写个AutoConfig将LabelAndWeightMetadataRule声明成@Bean,用来替换默认的ZoneAvoidanceRule。这种方式在技术验证、开发测试阶段使用短平快。但是这种方式是强制全局设置,无法个性化。 2)由于spring-cloud的Ribbon并没有实现netflix Ribbon的所有配置项。netflix配置全局rule方式为:ribbon.NFLoadBalancerRuleClassName=package.YourRule,spring-cloud并不支持,spring-cloud直接到服务粒度,只支持SERVICE_ID.ribbon.NFLoadBalancerRuleClassName=package.YourRule。 我们可以扩展org.springframework.cloud.netflix.ribbon.PropertiesFactory修正spring cloud ribbon未能完全支持netflix ribbon配置的问题。这样我们可以将全局配置写到配置中心的application-dev.properties全局配置中,然后各个微服务还可以根据自身情况做个性化定制。但是PropertiesFactory属性均为私有,应该是spring cloud不建议在此扩展。参见https://github.com/spring-cloud/spring-cloud-netflix/issues/1741。 3)使用spring cloud官方建议的@RibbonClient方式。该方式仅存在于spring-cloud单元测试中(在我提问后,现在还存在于spring-cloud issue list)。具体代码参见DefaultRibbonConfiguration.java、CoreAutoConfiguration.java。 目前采用第三种方式处理 基于权重的路由测试依次开启 config eureka provide(开两个实例,通过启动参数server.port指定不同端口区分) consumer zuul访问 http://localhost:8761/metadata.html 这是我手写的一个简单的metadata管理界面,分别设置两个provider实例的weight值(设置完需要一段2分钟才能生效),然后访问 http://localhost:8080/provider/user 多刷几次来测试zuul是否按权重发送请求,也可以访问 http://localhost:8080/consumer/test 多刷几次来测试consumer是否按权重来调用provide服务。 基于标签的路由处理基于权重的搞定之后,接下来才是重头戏:基于标签的路由。入口请求含有各种标签,然后我们可以根据标签幻化出各种各样的路由规则。例如只有标注为粉丝的用户才使用新版本(灰度、AB、金丝雀),例如标注为中国的用户请求必须发送到中国的服务器(全球部署),例如标注为写的请求必须发送到专门的写服务实例(读写分离),等等等等,唯一限制你的就是你的想象力。 基于标签的路由实现思路根据标签的控制,我们当然放到之前写的Ribbon的rule中,每个实例配置的不同规则也是跟之前一样放到注册中心的metadata中。需要解决以下几个问题: Q:关键是标签数据如何传过来? A:权重随机的实现思路里面有答案,请求都通过zuul进来,因此我们可以在zuul里面给请求打标签,基于用户,IP或其他看你的需求,然后将标签信息放入ThreadLocal中,然后在Ribbon Rule中从ThreadLocal拿出来使用就可以了。 然而,按照这个方式去实验时,发现有问题,拿不到ThreadLocal。原因是有hystrix这个东西,回忆下hystrix的原理,为了做到故障隔离,hystrix启用了自己的线程,不在同一个线程ThreadLocal失效。 那么还有什么办法能够将标签信息一传到底呢,想想之前有没有人实现过类似的东西,没错sleuth,它的链路跟踪就能够将span传递下去,翻翻sleuth源码,找找其他资料,发现可以使用HystrixRequestVariableDefault,这里不建议直接使用HystrixConcurrencyStrategy,会和sleuth的strategy冲突。代码参见CoreHeaderInterceptor.java。现在可以测试zuul里面的rule,看能否拿到标签内容了。 标签传到HystrixRequestVariableDefault这里的,如果项目中没有使用Hystrix就用不了了,这个时候需要做一个判断在restTemple里面做个判断,没有hystrix就直接threadlocal取。 Q:这里还不是终点,解决了zuul的路由,服务A调服务B这里的路由怎么处理呢?zuul算出来的标签如何往后面依次传递下去呢? 我们还是抄sleuth:把标签放入header,服务A调服务B时,将服务A header里面的标签放到服务B的header里,依次传递下去。这里的关键点就是:内部的微服务在接收到发来的请求时(zuul->A,A->B)我们将请求放入ThreadLocal,哦,不对,是HystrixRequestVariableDefault,还记得上面说的原因么:)。 这个容易处理,写一个spring mvc拦截器即可,代码参见CoreHeaderInterceptor。然后发送请求时自动带上这个里面保存的标签信息,参见RestTemplate的拦截器CoreHttpRequestInterceptor。到此为止,技术上全部走通实现。 总结一下:zuul依据用户或IP等计算标签,并将标签放入header里向后传递,后续的微服务通过拦截器,将header里的标签放入RestTemplate请求的header里继续向后接力传递。标签的内容通过放入类似于ThreadLocal的全局变量(HystrixRequestVariableDefault),使Ribbon Rule可以使用。 基于标签路由的测试参见PreFilter源码,模拟了几个用户的标签,参见LabelAndWeightMetadataRule源码,模拟了OR AND两种标签处理策略。依次开启 config eureka provide(开两个实例,通过启动参数server.port指定不同端口区分) consumer zuul. 访问 http://localhost:8761/metadata.html 设置第一个provide 实例 orLabel为 CN,Test 发送请求头带入Authorization: emt 访问http://localhost:8080/provider/user 多刷几次,可以看到zuul所有请求均路由给了第一个实例。访问http://localhost:8080/consumer/test 多刷几次,可以看到,consumer调用均路由给了第一个实例。 设置第二个provide 实例 andLabel为 EN,Male 发送请求头带入Authorization: em 访问http://localhost:8080/provider/user 多刷几次,可以看到zuul所有请求均路由给了第二个实例。访问http://localhost:8080/consumer/test 多刷几次,可以看到,consumer调用均路由给了第二个实例。 Authorization头还可以设置为PreFilter里面的模拟token来做测试,至此所有内容讲解完毕,技术路线拉通,剩下的就是根据需求来完善你自己的路由策略啦。 伪代码分析实现流程伪代码示例Ribbon默认采用ZoneAvoidanceRule,优先选择同zone下的实例。我们继承这个rule并扩展我们自己的限流功能,仔细阅读ZoneAvoidanceRule及其父类源码。 1234567891011121314151617181920212223242526272829303132333435363738394041424344public class WeightedMetadataRule extends ZoneAvoidanceRule {public static final String META_DATA_KEY_WEIGHT = \"weight\";@Overridepublic Server choose(Object key) { List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key); if (CollectionUtils.isEmpty(serverList)) { return null; } // 计算总值并剔除0权重节点 int sum = 0; Map<Server, Integer> serverWeightMap = new HashMap<>(); for (Server server : serverList) { String strWeight = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata().get(META_DATA_KEY_WEIGHT); int weight = 100; try { weight = Integer.parseInt(strWeight); } catch (Exception e) { // 无需处理 } if (weight <= 0) { continue; } serverWeightMap.put(server, weight); sum += weight; } // 权重随机 int random = (int) (Math.random() * sum); int current = 0; for (Map.Entry<Server, Integer> entry : serverWeightMap.entrySet()) { current += entry.getValue(); if (random < current) { return entry.getKey(); } } return null;}} 使上述代码生效,在zuul网关中加入 1234@Beanpublic IRule weightedMetadataRule(){ return new WeightedMetadataRule();} 代码示例测试打断点测试是否进入WeightedMetadataRule,开启多个服务A实例,通过zuul访问服务A。成功进入断点,代码生效后,我们再来看如何指定metadata。访问eureka restful API (我的eureka服务器端口为8100,修改为你自己的eureka端口)Get http://localhost:8100/eureka/apps这个api可以看到所有服务Get http://localhost:8100/eureka/apps/YOUR_SERVICE_NAME这个api可以看到你的服务信息,包括部署了哪些实例Get http://localhost:8100/eureka/apps/YOUR_SERVICE_NAME/INSTANCE_ID这个api可以看到服务实例的信息,注意其中的metadata节点,目前为emptyPut http://localhost:8100/eureka/apps/YOUR_SERVICE_NAME/INSTANCE_ID/metadata?weight=10通过put方式可以修改metadata的内容,放入weight,设为10 然后稍等两分钟,让zuul更新注册中心中的信息,接着重新访问,调试就可以看到metadata的内容了,并且也是按照权重随机来进行流量限制的,至此hello world搞定。 生产上使用WeightedMetadataRule接下来,在生产环境中,我们如何应用这个WeightedMetadataRule呢,有如下几种方式: 手动指定服务策略,spring cloud ribbon并没有完整实现netflix ribbon的所有配置功能,负载策略默认只能配置微服务级别,无法配置全局默认值。例如:只能配置 SOME_SERVICE_ID.ribbon.NFLoadBalancerRuleClassName=package.WeightedMetadataRule而不支持配置全局默认值 ribbon.NFLoadBalancerRuleClassName=package.WeightedMetadataRule这种方案明显不符合我们的要求。 通过声明Irule spring bean配置全局负载策略1234@Beanpublic IRule weightedMetadataRule(){ return new WeightedMetadataRule();} 这种方式也就是我们上面用的hello world方式,配置后强制所有微服务使用该策略,没有例外,微服务无法个性化定制策略,符合目前需求,但不适于长期规划。 继承重写PropertiesFactory继承重写org.springframework.cloud.netflix.ribbon.PropertiesFactory类,修正spring cloud ribbon未能完全支持netflix ribbon的问题。但是PropertiesFactory属性均为私有,应该是spring cloud不建议在此扩展。参见https://github.com/spring-cloud/spring-cloud-netflix/issues/1741 使用spring cloud官方建议的@RibbonClient方式1234567891011121314151617181920212223242526272829@Configuration@RibbonClients(defaultConfiguration = DefaultRibbonConfiguration.class)public class DefaultRibbonConfiguration { @Value(\"${ribbon.client.name:#{null}}\") private String name; @Autowired(required = false) private IClientConfig config; @Autowired private PropertiesFactory propertiesFactory; @Bean public IRule ribbonRule() { if (StringUtils.isEmpty(name)) { return null; } if (this.propertiesFactory.isSet(IRule.class, name)) { return this.propertiesFactory.get(IRule.class, config, name); } // 默认配置 WeightedMetadataRule rule = new WeightedMetadataRule(); rule.initWithNiwsConfig(config); return rule; }} 总结关于权重随机的性能,上述代码用的数组分段查找法,还可以采用TreeMap二分查找法。可以将权重数组或权重TreeMap缓存起来。根据测试,在实例数量为50个时 缓存权重数组和权重TreeMap,数组分段查找百万次耗时78-125ms,TreeMap二分耗时50-80ms。 这篇文章只是把技术打通,至于如何根据服务器负载情况,自动降级,限流等需求,只需要监控服务器状况,调用eureka接口设置metadata即可(其实我个人建议这方面需求通过docker的自动扩容缩容完成,只是有朋友问到如何通过spring cloud实现)。 下一篇会写基于标签的流量控制。如何控制部分用户使用服务A2.0,其他用户使用服务A1.0。 参考文章江南白衣-服务化之-路由SpringCloud Ribbon 降级、限流、灰度发布","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Ribbon","slug":"Spring-Cloud-Ribbon","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Ribbon/"}]},{"title":"快速使用Spring Cloud Feign作为客户端调用服务提供者","slug":"sc/sc-fegin01","date":"2017-05-13T06:00:00.000Z","updated":"2017-06-17T03:00:05.000Z","comments":true,"path":"sc/sc-fegin01/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-fegin01/","excerpt":"Feign简介Feign是一种声明式、模板化的HTTP客户端。在Spring Cloud中使用Feign, 可以做到使用HTTP请求远程服务时能就像调用本地方法一样的体验,开发者完全感知不到这是远程方法,更感知不到这是个HTTP请求。Feign的Github网址,比如:Feign具有如下特性: 可插拔的注解支持,包括Feign注解和JAX-RS注解 支持可插拔的HTTP编码器和解码器 支持Hystrix和它的Fallback 支持Ribbon的负载均衡 支持HTTP请求和响应的压缩","text":"Feign简介Feign是一种声明式、模板化的HTTP客户端。在Spring Cloud中使用Feign, 可以做到使用HTTP请求远程服务时能就像调用本地方法一样的体验,开发者完全感知不到这是远程方法,更感知不到这是个HTTP请求。Feign的Github网址,比如:Feign具有如下特性: 可插拔的注解支持,包括Feign注解和JAX-RS注解 支持可插拔的HTTP编码器和解码器 支持Hystrix和它的Fallback 支持Ribbon的负载均衡 支持HTTP请求和响应的压缩 Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。它整合了Ribbon和Hystrix,从而不再需要显式地使用这两个组件。Feign还提供了HTTP请求的模板,通过编写简单的接口和注解,就可以定义好HTTP请求的参数、格式、地址等信息。接下来,Feign会完全代理HTTP的请求,我们只需要像调用方法一样调用它就可以完成服务请求。Feign 示例工程 链接:https://github.com/SoftwareKing/spring-cloud-study/tree/master/sc-feign-first 本文最终修改时间:2017-05-20 18:47:23,为了解决问题1和2最终使用版本:Spring Boot的版本为1.5.3.RELEASE,Spring Cloud版本为Dalston.RELEASE 服务消费者中sc-feign-first-consumer的Feign的定义为了让Feign知道在调用方法时应该向哪个地址发请求以及请求需要带哪些参数,我们需要定义一个接口: 123456789101112131415package org.xujin.sc.feign.user.service;import org.springframework.cloud.netflix.feign.FeignClient;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.xujin.sc.feign.user.model.OrderModel;@FeignClient(name = \"sc-feign-first-provider\")//【A】public interface UserFeignService {@RequestMapping(value = \"/sc/order/{id}\", method = RequestMethod.GET)//【B】public OrderModel findOrderById(@PathVariable(\"id\") Long id); //【C】} A: @FeignClient用于通知Feign组件对该接口进行代理(不需要编写接口实现),使用者可直接通过@Autowired注入,如下代码所示。 123 // 注入服务提供者,远程的Http服务@Autowiredprivate UserFeignService userFeignService; B: @RequestMapping表示在调用该方法时需要向/sc/order/{id}发送GET请求。 C: @PathVariable与SpringMVC中对应注解含义相同 服务消费者中Feign的使用123456789101112131415161718192021222324252627282930package org.xujin.sc.feign.user.controller;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import org.xujin.sc.feign.user.model.OrderModel;import org.xujin.sc.feign.user.service.UserFeignService;/** * UserController * @author xujin */@RestControllerpublic class UserController { private static final Logger logger = LoggerFactory.getLogger(UserController.class); // 注入服务提供者,远程的Http服务 @Autowired private UserFeignService userFeignService; // 服务消费者对位提供的服务 @GetMapping(\"/sc/user/{id}\") public OrderModel findByIdByEurekaServer(@PathVariable Long id) { return userFeignService.findOrderById(id); }} 如上代码所示,通过@Autowired将声明的Feign依赖注入即可,调用userFeignService.findOrderById(id)使用。开发者通过userFeignService.findOrderById()就能完成发送HTTP请求和解码HTTP返回结果并封装成对象的过程。 启动测试依次按顺序启动如下工程注册中心: sc-fegin-first-server服务提供者1:sc-fegin-first-provider01服务提供者2:sc-fegin-first-provider02以上工程能正常启动work,但是当启动服务消费者: sc-fegin-first-consumer报错如下。 使用的示例工程的Spring Boot的版本为1.5.2.RELEASE,Spring Cloud版本为Dalston.RELEASE会出现以下错误。 12345678910111213141516171819<!-- 引入spring boot的依赖 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> </parent> <!-- 引入spring cloud的依赖 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement> 访问http://localhost:8010/sc/user/1 ,出现以下错误即:【问题一】feign/Feign$BuilderCaused by: java.lang.NoClassDefFoundError: feign/Feign$Builder12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273java.lang.IllegalStateException: ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2d140a7: startup date [Sun May 14 22:44:43 CST 2017]; parent: org.springframework.context.annotation.AnnotationConfigApplicationContext@4bf48f6 at org.springframework.context.support.AbstractApplicationContext.getApplicationEventMulticaster(AbstractApplicationContext.java:404) [spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.ApplicationListenerDetector.postProcessBeforeDestruction(ApplicationListenerDetector.java:97) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DisposableBeanAdapter.destroy(DisposableBeanAdapter.java:253) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroyBean(DefaultSingletonBeanRegistry.java:578) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingleton(DefaultSingletonBeanRegistry.java:554) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingleton(DefaultListableBeanFactory.java:961) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingletons(DefaultSingletonBeanRegistry.java:523) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingletons(DefaultListableBeanFactory.java:968) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1033) [spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:555) [spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1162) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1151) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.xujin.sc.feign.user.UserConsumerApplication.main(UserConsumerApplication.java:15) [classes/:na]2017-05-14 22:44:44.079 ERROR 2372 --- [ main] o.s.boot.SpringApplication : Application startup failedorg.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'methodValidationPostProcessor' defined in class path resource [org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.class]: Unsatisfied dependency expressed through method 'methodValidationPostProcessor' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.xujin.sc.feign.user.service.UserFeignService': Failed to introspect bean class [org.springframework.cloud.netflix.feign.FeignClientFactoryBean] for lookup method metadata: could not find class that it depends on; nested exception is java.lang.NoClassDefFoundError: feign/Feign$Builder at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:467) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1173) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1067) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:513) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.PostProcessorRegistrationDelegate.registerBeanPostProcessors(PostProcessorRegistrationDelegate.java:223) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.AbstractApplicationContext.registerBeanPostProcessors(AbstractApplicationContext.java:702) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:527) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) ~[spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1162) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1151) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.xujin.sc.feign.user.UserConsumerApplication.main(UserConsumerApplication.java:15) [classes/:na]Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.xujin.sc.feign.user.service.UserFeignService': Failed to introspect bean class [org.springframework.cloud.netflix.feign.FeignClientFactoryBean] for lookup method metadata: could not find class that it depends on; nested exception is java.lang.NoClassDefFoundError: feign/Feign$Builder at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:269) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors(AbstractAutowireCapableBeanFactory.java:1118) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1091) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getSingletonFactoryBeanForTypeCheck(AbstractAutowireCapableBeanFactory.java:923) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryBean(AbstractAutowireCapableBeanFactory.java:804) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch(AbstractBeanFactory.java:558) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:432) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:395) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.BeanFactoryUtils.beanNamesForTypeIncludingAncestors(BeanFactoryUtils.java:220) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1260) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1101) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] ... 19 common frames omittedCaused by: java.lang.NoClassDefFoundError: feign/Feign$Builder at java.lang.Class.getDeclaredMethods0(Native Method) ~[na:1.8.0_112] at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) ~[na:1.8.0_112] at java.lang.Class.getDeclaredMethods(Class.java:1975) ~[na:1.8.0_112] at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:613) ~[spring-core-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:524) ~[spring-core-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:510) ~[spring-core-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:247) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] ... 32 common frames omittedCaused by: java.lang.ClassNotFoundException: feign.Feign$Builder at java.net.URLClassLoader.findClass(URLClassLoader.java:381) ~[na:1.8.0_112] at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_112] at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) ~[na:1.8.0_112] at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_112] ... 39 common frames omitted 经查找解决问题2天查看无果(捂脸,后面写源码分析定位),因此决定将Spring Boot的版本改变为1.4.3.RELEASE,Spring Cloud版本为Camden.SR5之后,按上面的顺序启动,之后测试http://localhost:8010/sc/user/1 ,可以正常work。 Fegin的work原理Spring Cloud应用在启动时,Feign会扫描标有@FeignClient注解的接口,生成代理,并注册到Spring容器中。生成代理时Feign会为每个接口方法创建一个RequetTemplate对象,该对象封装了HTTP请求需要的全部信息,请求参数名、请求方法等信息都是在这个过程中确定的,Feign的模板化就体现在这里。在本例中,我们将Feign与Eureka和Ribbon组合使用,@FeignClient(name = “sc-feign-first-provider”)意为通知Feign在调用该接口方法时要向Eureka中查询名为ea的服务,从而得到服务URL。 Fegin的常见应用Feign的Encoder、Decoder和ErrorDecoderFeign将方法签名中方法参数对象序列化为请求参数放到HTTP请求中的过程,是由编码器(Encoder)完成的。同理,将HTTP响应数据反序列化为java对象是由解码器(Decoder)完成的。 默认情况下,Feign会将标有@RequestParam注解的参数转换成字符串添加到URL中,将没有注解的参数通过Jackson转换成json放到请求体中。 注意,如果在@RequetMapping中的method将请求方式指定为GET,那么所有未标注解的参数将会被忽略,例如: 12@RequestMapping(value = \"/group/{groupId}\", method = RequestMethod.GET)void update(@PathVariable(\"groupId\") Integer groupId, @RequestParam(\"groupName\") String groupName, DataObject obj); 此时因为声明的是GET请求没有请求体,所以obj参数就会被忽略。 在Spring Cloud环境下,Feign的Encoder只会用来编码没有添加注解的参数。如果你自定义了Encoder, 那么只有在编码obj参数时才会调用你的Encoder。 对于Decoder, 默认会委托给SpringMVC中的MappingJackson2HttpMessageConverter类进行解码。只有当状态码不在200 ~ 300之间时ErrorDecoder才会被调用。 ErrorDecoder的作用是可以根据HTTP响应信息返回一个异常,该异常可以在调用Feign接口的地方被捕获到。我们目前就通过ErrorDecoder来使Feign接口抛出业务异常以供调用者处理。 更换Feign默认使用的HTTP ClientFeign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection 。我们可以用Apache的HTTP Client替换Feign原始的http client, 从而获取连接池、超时时间等与性能息息相关的控制能力。Spring Cloud从Brixtion.SR5版本开始支持这种替换,首先在项目中声明Apache HTTP Client和feign-httpclient依赖: 12345678910<!-- 使用Apache HttpClient替换Feign原生httpclient --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <dependency> <groupId>com.netflix.feign</groupId> <artifactId>feign-httpclient</artifactId> <version>8.17.0</version> </dependency> 然后在application.yml中添加如下:123feign: httpclient: enabled: true spring cloud feign使用okhttp3参考 spring cloud feign常见问题参数不会自动传递服务消费者端调用1234@RequestMapping(value = \"/test\", method = RequestMethod.GET) public String hello(@RequestParam(\"name\") String name, @RequestParam(\"age\") int age) { return userFeignService.hello(name, age); } 服务提供者Controller对外服务1234@RequestMapping(value = \"/hello\", method = RequestMethod.GET) public String hello(@RequestParam(\"name\") String name, @RequestParam(\"age\") int age) { return name + age; } Fegin客户端定义调用12@RequestMapping(value = \"/hello\", method = RequestMethod.GET) public String hello(String name, @RequestParam(\"age\") int age); 启动的时候sc-fegin-first-consumer工程不报错。但是当访问http://localhost:8010/test?name=xujin&age=25 ,报错如下123feign.FeignException: status 405 reading UserFeignService#hello(String,int); content:{"timestamp":1494856464666,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/hello"} at feign.FeignException.errorStatus(FeignException.java:62) ~[feign-core-9.3.1.jar:na] Fegin客户端定义修改如下OK,原因是name被自动放到request body。只要有body,就会被feign认为是post请求,所以整个hello是被当作带有request parameter和body的post请求发送出去了,因此出现上面的错误提示。 12@RequestMapping(value = \"/hello\", method = RequestMethod.GET) public String hello(@RequestParam(\"name\") String name, @RequestParam(\"age\") int age); POST多参数调用 POST多参数 Feign端定义: 12@RequestMapping(value = \"/test/post\", method = RequestMethod.POST)public OrderModel post(OrderModel orderModel); 12@RequestMapping(value = \"/test/post\", method = RequestMethod.POST)public OrderModel post(@RequestBody OrderModel orderModel); 以上两种定义方式等价 服务提供者的定义 12345@PostMapping(\"/test/post\")public OrderModel testPost(@RequestBody OrderModel orderModel) { orderModel.setOrderNo(2222222L); return orderModel;} 修改订单号返回证明,服务提供者接到从Feign POST请求过来的数据。 服务消费者端的使用 1234@PostMapping(\"/test/post\")public OrderModel testPost(@RequestBody OrderModel orderModel) { return userFeignService.post(orderModel);} 测试当修改了Feign默认的http Client之后,出现如下错误,具体出错原因还在排查之中,本文会随时更改。【问题二】更换了Feign默认的Client出现HystrixRuntimeException12345678{ \"timestamp\": 1494947172990, \"status\": 500, \"error\": \"Internal Server Error\", \"exception\": \"com.netflix.hystrix.exception.HystrixRuntimeException\", \"message\": \"UserFeignService#post(OrderModel) failed and no fallback available.\", \"path\": \"/test/post\"} 1234567java.lang.IllegalArgumentException: MIME type may not contain reserved characters at org.apache.http.util.Args.check(Args.java:36) ~[httpcore-4.4.5.jar:4.4.5] at org.apache.http.entity.ContentType.create(ContentType.java:182) ~[httpcore-4.4.5.jar:4.4.5] at feign.httpclient.ApacheHttpClient.getContentType(ApacheHttpClient.java:159) ~[feign-httpclient-8.17.0.jar:8.17.0] at feign.httpclient.ApacheHttpClient.toHttpUriRequest(ApacheHttpClient.java:140) ~[feign-httpclient-8.17.0.jar:8.17.0] at feign.httpclient.ApacheHttpClient.execute(ApacheHttpClient.java:83) ~[feign-httpclient-8.17.0.jar:8.17.0] 当关闭之后,访问正常如下所示,醉了同样的代码(PS:捂脸) 123feign: httpclient: enabled: false 1{\"createTime\":1494944311023,\"orderNo\":33333,\"payTime\":1494944311023} GET多参数调用当服务之间GET调用为多参数时,可以使用Map来构建参数传递Feign接口中的示例定义12@RequestMapping(value = \"/test/get\", method = RequestMethod.GET)public String testGet(@RequestParam Map<String, Object> map); 服务消费者的调用 12345678@GetMapping(\"/test/get\")public String testGet() { HashMap<String, Object> map = Maps.newHashMap(); map.put(\"orderNo\", \"1\"); map.put(\"createTime\", new Date()); map.put(\"payTime\", new Date()); return userFeignService.testGet(map);} 个人看来,如果是GET的多参数通过Map进行传递,当参数比较多时,个人建议使用面向对象的思维,通过POST的方式传递对象相对较好。 服务提供者的使用 1234@RequestMapping(value = \"/test/get\", method = RequestMethod.GET)public String testGet(@RequestParam Map<String, Object> map) { return String.valueOf(map);} 访问URL:http://localhost:8010/test/get ,测试OK. 1{orderNo=1, createTime=Sat May 20 19:47:38 CST 2017, payTime=Sat May 20 19:47:38 CST 2017} 总结 本文主要介绍了Feign的基本的定义,以及Feign的work原理和使用Feign的注意事项和常见问题。最后介绍了一下更换Feign默认使用的HTTP Client。主要是遇到一个奇葩的问题,最终没解决更换版本。在下一篇文章中将介绍Feign的其它的使用,例如Feign的继承,日志级别,以及Feign源码分析等 参考文献希望Feign能够支持参数请求使用POJO的Issue建议使用Feign原生的注解的Issue建议增强Feign的功能建议支持可选的Request Body(目前Feign当POST一个null时,会报异常)","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Fegin","slug":"Spring-Cloud-Fegin","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Fegin/"}]},{"title":"API GateWay(网关)那些儿事","slug":"sc/sc-zuul","date":"2017-05-10T06:00:00.000Z","updated":"2017-06-17T05:07:30.000Z","comments":true,"path":"sc/sc-zuul/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-zuul/","excerpt":"为什么需要API Gateway 简化客户端调用复杂度 在微服务架构模式下后端服务的实例数一般是动态的,对于客户端而言如何发现这些动态改变的服务实例的访问地址信息?因此在基于微服务的项目中为了简化前端的调用逻辑,通常会引入API Gateway作为轻量级网关,同时API Gateway中也会实现相关的认证逻辑从而简化内部服务之间相互调用的复杂度。","text":"为什么需要API Gateway 简化客户端调用复杂度 在微服务架构模式下后端服务的实例数一般是动态的,对于客户端而言如何发现这些动态改变的服务实例的访问地址信息?因此在基于微服务的项目中为了简化前端的调用逻辑,通常会引入API Gateway作为轻量级网关,同时API Gateway中也会实现相关的认证逻辑从而简化内部服务之间相互调用的复杂度。 数据裁剪以及聚合 通常而言多余不同的客户端对于显示时对于数据的需求是不一致的,比如手机端或者Web端又或者在低延迟的网络环境或者高延迟的网络环境。 因此为了优化客户端的使用体验,API Gateway可以对通用性的响应数据进行裁剪以适应不同客户端的使用需求。同时还可以将多个API调用逻辑进行聚合,从而减少客户端的请求数,优化客户端用户体验 多渠道支持 当然我们还可以针对不同的渠道和客户端提供不同的API Gateway,对于该模式的使用由另外一个大家熟知的方式叫Backend for front-end, 在Backend for front-end模式当中,我们可以针对不同的客户端分别创建其BFF 遗留系统的微服务化改造 对于系统系统而言进行微服务改造通常是由于原有的系统存在或多或少的问题,比如技术债务,代码质量,可维护性,可扩展性等等。API Gateway的模式同样适用于这一类遗留系统的改造,通过微服务化的改造逐步实现对原有系统中的问题的修复,从而提升对于原有业务响应力的提升。通过引入抽象层,逐步使用新的实现替换旧的实现。 使用Zuul实现API网关Spring Cloud的Zuul组件提供了轻量级网关的功能支持,通过定义路由规则可以快速实现一个轻量级的API网关 123456789101112131415zuul: ignoredPatterns: /api/auth sensitive-headers: "*" ignoreLocalService: true retryable: false host: max-total-connections: 500 routes: service01: path: /service01/** serviceId: service01 stripPrefix: true thirdpart: pateh: /thirdpart/** url: http://thirdpart.api.com 同时除了通过serviceId关联已经注册到Consul的服务实例以外,我们也可以通过zuul直接定义实现对已有服务的直接集成。 这里我们就不过多介绍Zuul的细节,在实际使用中我们会发现直接使用Zuul会存在诸多问题,包括: 性能问题:当存在大量请求超时后会造成Zuul阻塞,目前只能通过横向扩展Zuul实例实现对高并发的支持; WebSocket的支持问题: Zuul中并不直接提供对WebSocket的支持,需要添加额外的过滤器实现对WebSocket的支持;为了解决以上问题,可以通过在Zuul前端部署Nginx实现对Zuul实例的反向代理,同时适当的通过添加Cache以及请求压缩减少对后端Zuul实例的压力。 实现Nginx的动态代理通过Nginx我们可以实现对多实例Zuul的请求代理,同时通过添加适当的缓存以及请求压缩配置可以提升前端UI的请求响应时间。这里需要解决的问题是Nginx如何动态发现Zuul实例信息并且将请求转发到Zuul当中。 consul-template可以帮助我们解决以上问题,consul-template是一个命令行工具,结合consul实现配置文件的动态生成并且支持在配置文件发生变化后触发用户自定义命令。 我们使用了如下的Dockerfile用于构建我们的Nginx服务 1234567891011121314151617181920FROM nginx:1.11.10ADD consul-template /usr/local/binRUN mkdir /etc/consul-templates# 模板文件ADD nginx.tpl /etc/consul-templates/nginx.tplENV CT_FILE /etc/consul-templates/nginx.tplENV NX_FILE /etc/nginx/conf.d/default.conf # 目标文件ENV SERVICE identity # 注册在Consul的服务名COPY dist /usr/share/nginx/htmlRUN mkdir -p /data/cacheCMD /usr/sbin/nginx -c /etc/nginx/nginx.conf \\ & CONSUL_TEMPLATE_LOG=debug \\ consul-template -consul-addr=$CONSUL -template "$CT_FILE:$NX_FILE:/usr/sbin/nginx -s reload"; Nginx配置模板文件 123456789101112131415161718192021# nginx.tplupstream api_server { least_conn; {{range service "identity"}} server {{.Address}}:{{.Port}}; {{else}}server 127.0.0.1:9191;{{end}}}server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; } location /api { proxy_pass http://api_server; }} 其中 123456upstream api_server { least_conn; {{range service "identity"}} server {{.Address}}:{{.Port}}; {{else}}server 127.0.0.1:9191;{{end}}} 会根据当前consul中注册的所有identity服务实例进行模板渲染,并且当配置文件内容发生变化后调用nginx -s reload重新加载Nginx配置从而实现对于后端服务实例的动态代理。 123CMD /usr/sbin/nginx -c /etc/nginx/nginx.conf \\ & CONSUL_TEMPLATE_LOG=debug \\ consul-template -consul-addr=$CONSUL -template "$CT_FILE:$NX_FILE:/usr/sbin/nginx -s reload"; 其它的一些优化建议启用Nginx的Gzip可以对服务器端响应内容进行压缩从而减少一定的客户端响应时间 12345gzip on;gzip_min_length 1k;gzip_buffers 4 32k;gzip_types text/plain application/x-javascript application/javascript text/xml text/css;gzip_vary on; 缓存图片以及其它静态资源可以减少对Zuul实例的请求量 12345678910111213proxy_buffering on;proxy_cache_valid any 10m;proxy_cache_path /data/cache levels=1:2 keys_zone=my-cache:8m max_size=1000m inactive=600m;proxy_temp_path /data/temp;proxy_buffer_size 4k;proxy_buffers 100 8k;location ~* (images) { proxy_pass http://api_server; # cache setting proxy_cache my-cache; proxy_cache_valid 200;} 如果需要通过Nginx实现对Websocket的代理可以添加一下配置 12345678910111213141516171819location /sockjs { proxy_pass http://api_server; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # WebSocket support (nginx 1.4) proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_connect_timeout 90; proxy_send_timeout 90; proxy_read_timeout 90; # !!!Support Spring Boot proxy_pass_header X-XSRF-TOKEN; proxy_set_header Origin "http://localhost:4000"; }","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Zuul","slug":"Spring-Cloud-Zuul","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Zuul/"}]},{"title":"Spring Cloud Zuul的URL转发和路由规则","slug":"sc/sc-zuul-01","date":"2017-04-30T06:00:00.000Z","updated":"2017-06-17T03:15:15.000Z","comments":true,"path":"sc/sc-zuul-01/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-zuul-01/","excerpt":"摘要:最近开了《跟我学Spring Cloud》系列教程,由于最近比较忙,因此更新较慢。由于自己最近在研究基于Netty名为Janus的网关中间件分为janus-Server端和janus-console管控端,纳管Spring Cloud实现市面上网关85%以上的功能,将在2017年5月6号Spring Cloud中国社区北京技术沙龙分享。顺便抽时间把Spring Cloud Zuul相关的东西整理比较。在本篇文章中Spring Cloud的版本更换为Dalston.RELEASE,Spring Boot的版本为1.5.2.RELEASE。 Spring Cloud Zuul Spring Cloud Zuul 通过与 Spring Cloud Eureka 进行整合,将自身注册到 Eureka Server中,与Eureka,Ribbon,Hystrix等整合,同时从 Eureka 中获得了所有其它微服务的实例信息。这样的设计通过把网关和服务治理整合到一起,Spring Cloud Zuul可以获取到服务注册信息,结合Ribbon,Hystrix等更好的实现路由转发,负载均衡等功能。想了解更多的内容,可以参考下面的中英文对照翻译文档。或者查看官网文档。 Spring Cloud Zuul中英文对照翻译① Spring Cloud Zuul中英文对照翻译② Spring Cloud Zuul中英文对照翻译③","text":"摘要:最近开了《跟我学Spring Cloud》系列教程,由于最近比较忙,因此更新较慢。由于自己最近在研究基于Netty名为Janus的网关中间件分为janus-Server端和janus-console管控端,纳管Spring Cloud实现市面上网关85%以上的功能,将在2017年5月6号Spring Cloud中国社区北京技术沙龙分享。顺便抽时间把Spring Cloud Zuul相关的东西整理比较。在本篇文章中Spring Cloud的版本更换为Dalston.RELEASE,Spring Boot的版本为1.5.2.RELEASE。 Spring Cloud Zuul Spring Cloud Zuul 通过与 Spring Cloud Eureka 进行整合,将自身注册到 Eureka Server中,与Eureka,Ribbon,Hystrix等整合,同时从 Eureka 中获得了所有其它微服务的实例信息。这样的设计通过把网关和服务治理整合到一起,Spring Cloud Zuul可以获取到服务注册信息,结合Ribbon,Hystrix等更好的实现路由转发,负载均衡等功能。想了解更多的内容,可以参考下面的中英文对照翻译文档。或者查看官网文档。 Spring Cloud Zuul中英文对照翻译① Spring Cloud Zuul中英文对照翻译② Spring Cloud Zuul中英文对照翻译③ 快速搭建SC Zuul工程目录如下图所示: Code地址:https://github.com/SoftwareKing/spring-cloud-study/tree/master/sc-zuul-first Spring Cloud Zuul原始的URL转发功能 由于sc-zuul-first-provider1的代码极其简单就是一个简单的服务提供者,因此不做过多介绍。下面主要介绍sc-zuul-first-zuul-no-eureka这个工程, URL路由转发功能 创建名为sc-zuul-first-zuul-no-eureka的maven工程,添加依赖,但注意的是该工程只有Zuul的依赖。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 <?xml version=\"1.0\"?><project xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\" xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> <modelVersion>4.0.0</modelVersion> <groupId>org.xujin.sc</groupId> <artifactId>sc-zuul-first-zuul-no-eureka</artifactId> <version>0.0.1-SNAPSHOT</version> <name>sc-zuul-first-zuul-no-eureka</name> <url>http://maven.apache.org</url> <!-- 引入spring boot的依赖 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency> </dependencies> <!-- 引入spring cloud的依赖 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- 添加spring-boot的maven插件 --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project> 说明: 对于 spring-cloud-starter-zuul 依赖,我们可以通过查看它的依赖内容了解 到:该模块中不仅包含了 Netflix Zuul 的核心依赖 zuul-core,它还包含了下面这 些网关服务需要的重要依赖。 spring-cloud-starter-hystrix:该依赖用来在网关服务中实现对微服务 转发时候的保护机制,通过线程隔离和断路器,防止微服务的故障引发 API 网关 资源无法释放,从而影响其他应用的对外服务。 spring-cloud-starter-ribbon:该依赖用来实现在网关服务进行路由转发 时候的客户端负载均衡以及请求重试。 spring-boot-starter-actuator :该依赖用来提供常规的微服务管理端点。另外,在Spring Cloud Zuul中还特别提供了/routes 端点来返回当前的所有路由规则。 2.主入口程序代码如下,使用@EnableZuulProxy注解1234567891011121314package org.xujin.sc.zuul.first.zuul;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.zuul.EnableZuulServer;@SpringBootApplication@EnableZuulProxypublic class SpringCloudZuulApplication { public static void main(String[] args) { SpringApplication.run(SpringCloudZuulApplication.class, args); }} 3.application.yml配置文件信息如下12345server.port=8041spring.application.name=sc-zuul-first-zuul-no-eurekazuul.routes.api-url.path=/api-url/**zuul.routes.api-url.url=http://localhost:8000/ 该配置定义了发往 API 网关服务的请求中,所有符合/api-url/**规则的访问都 将 被 路 由 转 发 到 http://localhost:8000/ 地 址 上 , 也 就 是 说 当 我 们 访 问 http://localhost:8041/api-url/sc/order/1 可以正常的把请求的url转发到http://localhost:8000/sc/order/2 。其 中 , 配 置 属 性 zuul.routes.api-url.path 中的 api-url 部分为路由的名字,可以任意定义, 但是一组 path 和 url 映射关系的路由名要相同。 zuul.routes.api-url.url=http://localhost:8000/ 这个配置了服务提供者sc-zuul-first-provider1的URL 4.测试依次按如下顺序,把各个服务启动。 注册中心为:sc-zuul-first-eureka-server 服务提供者为:sc-zuul-first-provider1,sc-zuul-first-provider2 启动sc-zuul-first-zuul-no-eureka 上述Server启动之后,测试Case: URL路由转发功能测试1.当注解为@EnableZuulProxy时,测试转发。通过访问网关的URL: http://localhost:8041/api-url/sc/order/1 可以正常的把请求的url转发到http://localhost:8000/sc/order/2 Tips:断点跳过之后,返回结果如下,说明当使用@EnableZuulProxy注解的时候,Zuul具有URL转发调用的功能。 2.关闭sc-zuul-first-zuul-no-eureka对应的服务,把主应用程序中的注解@EnableZuulProxy变为@EnableZuulServer,按第1步启动sc-zuul-first-zuul-no-eureka服务,测试。 Tips: 可以看到上图返回结果为200,但是空白。那为什么会这样呢?后面专门对Zuul的源码分析,请读者忽略或自行查看源码。 Spring Cloud Zuul功能 大家知道Spring Cloud的服务治理的粒度是服务应用名,而如下的配置规则硬编码配置主机名和端口,由于Spring Cloud Zuul整合了Ribbon负载均衡器等因此,下面的配置方式不推荐使用比较low。12345server.port=8041spring.application.name=sc-zuul-first-zuul-no-eurekazuul.routes.api-url.path=/api-url/**zuul.routes.api-url.url=http://localhost:8000/ Spring Cloud Zuul功能案例1.为了演示面向服务名为粒度的路由规则,新建了一个名为sc-zuul-first-zuul的工程,该工程与sc-zuul-first-zuul-no-eureka的最大的区别就是在pom.xml文件中,加入spring-cloud-starter-eureka依赖,如下注释所示。1234567891011<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency> <!-- 多了eureka starter --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> </dependencies> 2.application.yml1234567891011server: port: 8040spring: application: name: sc-zuul-first-zuuleureka: client: service-url: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true 3.主应用程序代码SpringCloudZuulApplication.java12345678910111213141516171819package org.xujin.sc.zuul.first.zuul;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.zuul.EnableZuulProxy;/** * * @author xujin * @EnableZuulProxy 声明一个Zuul 代理,该代理使用Ribbon软负载均衡,还整合Hystrix实现熔断 */@SpringBootApplication@EnableZuulProxypublic class SpringCloudZuulApplication { public static void main(String[] args) { SpringApplication.run(SpringCloudZuulApplication.class, args); }} 4.分别依次把sc-zuul-first-eureka-server,sc-zuul-first-zuul,sc-zuul-first-provider1,sc-zuul-first-provider2,sc-zuul-first-consumer,sc-zuul-first-hystrix-dashboard启动。 Spring Cloud Zuul功能演示1.网关的默认路由规则 说明默认情况下,Zuul会代理所有注册到Eureka Server的微服务,并且Zuul的路由规则如下: http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/** 会被转发到serviceId对应的微服务。 http://localhost:8040/sc-zuul-first-provider/sc/order/2 2.网关的负载均衡 http://localhost:8040/sc-zuul-first-provider/sc/order/2 通过网关访问服务提供者,负载均衡打出对应的日志 123 2017-04-30 18:35:37.502\u001b[0;39m \u001b[32m INFO\u001b[0;39m \u001b[35m3443\u001b[0;39m \u001b[2m---\u001b[0;39m \u001b[2m[nio-8000-exec-3]\u001b[0;39m \u001b[36mo.x.s.e.f.o.controller.OrderController \u001b[0;39m \u001b[2m:\u001b[0;39m Zuul路由到服务提供者① 2017-04-30 18:34:06.764\u001b[0;39m \u001b[32m INFO\u001b[0;39m \u001b[35m3444\u001b[0;39m \u001b[2m---\u001b[0;39m \u001b[2m[nio-8001-exec-4]\u001b[0;39m \u001b[36mo.x.s.e.f.o.controller.OrderController \u001b[0;39m \u001b[2m:\u001b[0;39m Zuul路由到服务提供者②\u001b[2m2017-04-30 18:35:37.251\u001b[0;39m \u001b[32m INFO\u001b[0;39m \u001b[35m3444\u001b[0;39m \u001b[2m---\u001b[0;39m \u001b[2m[trap-executor-0]\u001b[0;39m \u001b[36mc.n.d.s.r.aws.ConfigClusterResolver \u001b[0;39m \u001b[2m:\u001b[0;39m Resolving eureka endpoints via configuration 3.集成Hystrix http://localhost:8040/hystrix.stream Spring Cloud Zuul路由规则指定服务路由对外访问路径 123zuul: routes: sc-zuul-first-provider: /order/** 相当于把sc-zuul-first-provider映射为/order/**,访问http://localhost:8040/sc-zuul-first-provider/sc/order/2 可以等价于:http://localhost:8040/order/sc/order/2,其它路由规则,可以从官网文档中阅读尝试。","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Zuul","slug":"Spring-Cloud-Zuul","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Zuul/"}]},{"title":"SC中Eureka Server的HA和安全身份验证","slug":"sc/sc-eureka-02","date":"2017-03-25T06:00:00.000Z","updated":"2017-06-17T03:15:59.000Z","comments":true,"path":"sc/sc-eureka-02/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-eureka-02/","excerpt":"","text":"什么是高可用高可用 High Availability,即高可用HA。在分布式情况下,我们经常说4个9(99.99%)或者5个9(99.999%)。举个简单例子,如果一个微服务分布式系统依赖于30个微服务,每个微服务可用性是99.99%,那么整个微服务系统的可用性就是99.99%的30次方 ≈ 99.7% ,也就是说有0.3%系统是不可用的,0.3%意味着如果Qps很高,有一亿次请求的话,那么就会有30万次失败。换算成时间大约每月有2个小时服务不稳定。特别是随着服务依赖数量的变多,微服务不稳定的概率会成指数性上升。因此要保证微服务应用的HA需要从各方面入手,下面会介绍一下如何实现Eureka Server的HA。参考工程如下所示。 Tips:代码示例:https://github.com/SoftwareKing/spring-cloud-study/tree/master/sc-eureka-ha Eureka Server的HAEureka Server的HA两个工程演示HA 如示例工程所示,我新建了两个Project分别为sc-eureka-ha-server1,sc-eureka-ha-server2, 我们知道在Eureka Server的Standalone模式下面,由于只有一个Eureka Server,所以我们通过配置如下信息关闭Eureka Server的自我注册和抓取注册信息,但是两个Eureka Server之间需要设置为True,相互注册相互感知对方注册信息的变化,从而实现信息同步。 1.sc-eureka-ha-server1的application.yml配置Info 如下: 123456789spring: application: name: sc-eureka-ha-server1server: port: 8761 # 指定该Eureka实例的端口eureka: client: serviceUrl: defaultZone: http://localhost:8762/eureka/ 2.sc-eureka-ha-server2的application.yml配置Info 如下 12345678910spring: application: name: sc-eureka-ha-server2 server: port: 8762 # 指定该Eureka实例的端口eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ 3.主程序入口代码没什么区别如下: 1234567@EnableEurekaServer@SpringBootApplicationpublic class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); }} 4.分别启动sc-eureka-ha-server1和sc-eureka-ha-server2,访问http://localhost:8761/ ,http://localhost:8762/ ,如下: 5.服务提供者sc-eureka-ha-provider其它代码见工程,application.yml如下所示。 1234567891011 server: port: 8000 spring: application: name: sc-eureka-ha-provider eureka: client: service-url: defaultZone: http://localhost:8761/eureka/ tips: 把服务提供者的服务注册信息,注册到Eureka Server 01上。 启动服务提供者,见如下图所示。 片刻服务提供者的信息也同步到Eureka Server02上面 Jar方式演示HA Eureka Server的HA,其实可以通过jar的方式指定使用不同的profile配置的方式,在本地运行两个Eureka Server。只需将Eureka server的application.yml修改如下:12345678910111213141516171819202122232425spring: application: name: sc-eureka-ha-server --- spring: profiles: peer1 server: port: 8761 eureka: instance: hostname: peer1.xujin.org client: serviceUrl: defaultZone: http://peer2.xujin.org:8762/eureka/ --- spring: profiles: peer2 server: port: 8762 eureka: instance: hostname: peer2.xujin.org client: serviceUrl: defaultZone: http://peer1.xujin.org:8761/eureka/ 通过配置switcHosts或者自行配置HostName对应的IP地址,把工程打成jar之后,运行如下命令123 java -jar sc-eureka-ha-server1-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2java -jar sc-eureka-ha-server1-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1 测试如下: 安全身份验证 如果客户端的eureka.client.serviceUrl.defaultZone参数值(即Eureka Server的地址)中包含HTTP Basic Authentication信息,如http://user:password@localhost:8761/eureka,那么客户端就会自动使用该用户名、密码信息与Eureka服务端进行验证。如果你需要更复杂的验证逻辑,你必须注册一个DiscoveryClientOptionalArgs组件,并将ClientFilter组件注入,在这里定义的逻辑会在每次客户端向服务端发起请求时执行。 Tips:代码示例:https://github.com/SoftwareKing/spring-cloud-study/tree/master/sc-eureka-security 访问Eureka Server安全身份验证 如工程sc-eureka-securit中的sc-eureka-security-server工程所示,在pom.xml中增加依赖如下: 1234 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency> application.yml如下 123456789101112131415161718 server: port: 8761 # 指定该Eureka实例的端口eureka: client: #表示是否将自己注册到Eureka Server上,默认为true,当前应用为Eureka Server所以无需注册 registerWithEureka: false #表示是否从Eureka Server获取注册信息,默认为true。因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,故而设为false。 fetchRegistry: false #Eureka Server的访问地址,服务注册和client获取服务注册信息均通过该URL,多个服务注册地址用,隔开 serviceUrl: defaultZone: http://localhost:8761/eureka/ security: basic: enabled: true user: name: xujin password: 123 3.启动Eureka server测试,如下图所示 服务提供者注册Eureka Server安全身份验证1.服务提供者只需注册时修改application.yml 1234567891011 server: port: 8000 spring: application: name: sc-eureka-security-provider eureka: client: service-url: defaultZone: http://xujin:123@localhost:8761/eureka/ Tips:如上所示:http://用户名:密码@localhost:8761/eureka/","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"}]},{"title":"使用Spring Cloud Eureka实现服务注册与发现","slug":"sc/sc-eureka-01","date":"2017-03-23T06:00:00.000Z","updated":"2017-06-17T03:34:15.000Z","comments":true,"path":"sc/sc-eureka-01/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-eureka-01/","excerpt":"","text":"什么是服务注册与发现服务注册与发现 在服务化的早期,服务不是很多,服务的注册与发现并不是什么新鲜的名词,Nginx+内部域名服务器方式,甚至Nginx+host文件配置方式也能完成服务的注册与发现。服务上下线需要在nginx,服务器做相应的配置,一旦服务的IP端口发生变化,都需要在nginx上做相应的配置,为了解决这个问题引入服务注册中心。 服务注册,即服务在启动的时候就将服务的IP,端口,版本号等EndPoint注册到注册中心(Eueka,Zookeeper,Consul)对服务进行统一管理. 服务发现,简单的就是说,不管服务上下线,当对某个服务发起请求时,能够快速的从本地缓存或者注册中心的注册列表中,快速找到服务提供者。 服务化早期的做法示例工程说明 Tips:代码示例:https://github.com/SoftwareKing/spring-cloud-study/tree/master/sc-eureka-first Spring MVC中基于无状态的REST 工程可以参考sc-rest-demo下面的sc-rest-provider和sc-rest-consumer,具体使用如下代码所示:123456789101112131415161718192021@RestController@RequestMapping(\"/sc\")public class ConsumerController { @Autowired private RestTemplate restTemplate; // 从属性文件中读取服务提供的URL @Value(\"${order.orderServiceUrl}\") private String orderServiceUrl; @GetMapping(\"/consumer/{id}\") public OrderModel getOrderInfo(@PathVariable Long id) { // this.restTemplate.getForObject(\"http://localhost:8000/sc/order/\" + // id,OrderModel.class); return this.restTemplate.getForObject(this.orderServiceUrl + \"/sc/order/\" + id, OrderModel.class); }} 大家注意到没,把http://localhost:8000 ,硬编码到程序中,是不是比较low。可以采用上面代码中的方式:orderServiceUrl解决。但是这样还是比较low,下面介绍一下引入Eureka实现服务注册与发现的处理。 使用Eureka实现服务的注册与发现搭建注册中心-Eureka Server 1.引入依赖123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051<?xml version=\"1.0\"?><project xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\" xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> <modelVersion>4.0.0</modelVersion> <!-- 引入spring boot的依赖 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.3.RELEASE</version> </parent> <artifactId>sc-eureka-first-server-HA01</artifactId> <name>sc-eureka-first-server-HA01</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!-- 引入Spring Cloud Eureka依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> </dependencies> <!-- 引入spring cloud的依赖 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement> <!-- 添加spring-boot的maven插件--> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project> 在Resources目录下创建application.yml123456789101112131415server: port: 8761 # 指定该Eureka实例的端口eureka: client: #表示是否将自己注册到Eureka Server上,默认为true,当前应用为Eureka Server所以无需注册 registerWithEureka: false #表示是否从Eureka Server获取注册信息,默认为true。因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,故而设为false。 fetchRegistry: false #Eureka Server的访问地址,服务注册和client获取服务注册信息均通过该URL,多个服务注册地址用,隔开 serviceUrl: defaultZone: http://localhost:8761/eureka/# 参考文档:http://projects.spring.io/spring-cloud/docs/1.0.3/spring-cloud.html#_standalone_mode# 参考文档:http://my.oschina.net/buwei/blog/618756 3.创建Spring Boot主应用程序启动代码12345678910111213141516171819package org.xujin.sc.eureka.server;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;/** * Eureka Server * @author xujin */@SpringBootApplication@EnableEurekaServerpublic class SpringCloudEurekaServer { public static void main(String[] args) { SpringApplication.run(SpringCloudEurekaServer.class, args); }} 启动Eureka server测试: 启动sc-eureka-first-server-HA01,访问http://localhost:8761/ ,如下图所示: 创建服务提供者 1.服务提供者,为了演示在这里提供一个简单的订单查询服务,如工程sc-eureka-first-provider01和sc-eureka-first-provider02所示。 2.主程序入口代码,如下所示:123456789101112131415161718192021package org.xujin.sc.eureka.first.order;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;/** * 服务提供者端,加上@EnableDiscoveryClient注解,完成服务注册。 * @author xujin * @site http://xujin.org */@SpringBootApplication@EnableDiscoveryClient// @EnableEurekaClientpublic class OrderProviderSpringBootAppliaction { public static void main(String[] args) { SpringApplication.run(OrderProviderSpringBootAppliaction.class, args); }} Tips:如果使用Eureka, 可以使用@EnableEurekaClient注解,但是推荐使用@EnableDiscoveryClient代替@EnableEurekaClient注解,因为@EnableDiscoveryClient是一个高度的抽象, 来自于spring-cloud-commons, 由于Spring Cloud选型是中立的因此抽象出该接口, 当服务注册中心选型改变为Eureka,ZK,Consul时,不需要修改原有代码中的注解。 3.服务提供者暴露的服务-OrderController.java12345678910111213@RestControllerpublic class OrderController { @Autowired private OrderService orderService; @GetMapping(\"/sc/order/{id}\") public OrderModel findOrderById(@PathVariable Long id) { OrderModel orderModel = orderService.findOrderByOrderId(id); return orderModel; }} 启动服务提供者,把服务注册信息,注册到Eureka Server注册中心启动sc-eureka-first-provider01,当启动其中一个服务后刷新Eureka Server会出现安全模式,如下图所示: 启动sc-eureka-first-provider02,刷新Eureka Server如下图所示。 创建服务消费者 服务消费者主要是一个简单的用户服务,用户服务查询订单服务的订单信息。 1.引入相应的依赖 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869 <?xml version=\"1.0\"?><project xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\" xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> <modelVersion>4.0.0</modelVersion> <groupId>org.xujin.sc</groupId> <artifactId>sc-eureka-first-consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>sc-eureka-first-consumer</name> <url>http://maven.apache.org</url> <!-- 引入spring boot的依赖 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.3.RELEASE</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.6</version> <scope>provided</scope> </dependency> </dependencies> <!-- 引入spring cloud的依赖 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR4</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <!-- 添加spring-boot的maven插件 --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project> 2.主程序入口代码12345678910111213141516171819202122package org.xujin.sc.eureka.user;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.context.annotation.Bean;import org.springframework.web.client.RestTemplate;//消费者端加入服务发现注解@EnableDiscoveryClient@SpringBootApplicationpublic class UserConsumerApplication { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(UserConsumerApplication.class, args); }} 消费者调用Controller。 12345678910111213141516171819202122232425262728@RestControllerpublic class UserController { private static final Logger logger = LoggerFactory.getLogger(UserController.class); @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; // discoveryClient获取服务列表中,应用名为sc-eureka-first-provider一个服务注册信息 public String serviceUrl() { List<ServiceInstance> list = discoveryClient .getInstances(\"sc-eureka-first-provider\"); if (list != null && list.size() > 0) { return String.valueOf(list.get(0).getUri()); } return null; } @GetMapping(\"/sc/user/{id}\") public Order findByIdByEurekaServer(@PathVariable Long id) { String providerServiceUrl = serviceUrl(); return this.restTemplate.getForObject(providerServiceUrl + \"sc/order/\" + id, Order.class); }} 如上述代码,所示使用discoveryClient.getInstances("sc-eureka-first-provider")获取服务名为sc-eureka-first-provider的服务注册列表信息。 测试先后启动sc-eureka-first-consumer,如没有异常,打开浏览器访问:http://localhost:8010/sc/user/2 ,debug如下所示可以看到 在刷新一下Eureka Server,如图下所示,此时安全模式关闭。 关于安全模式,在本篇文章中,暂不讨论,后面将会专写一篇文章介绍,请暂时忽略。 获取消费者获取服务端消费列表 使用EurekaClient获取服务注册信息 1234567 @Autowiredprivate EurekaClient discoveryClient;public String serviceUrl() { InstanceInfo instance = discoveryClient.getNextServerFromEureka(\"STORES\", false); return instance.getHomePageUrl();} 使用DiscoveryClient获取服务注册信息 12345678910 @Autowiredprivate DiscoveryClient discoveryClient;public String serviceUrl() { List<ServiceInstance> list = discoveryClient.getInstances(\"STORES\"); if (list != null && list.size() > 0 ) { return list.get(0).getUri(); } return null;} 参考链接:https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/asciidoc/spring-cloud-netflix.adoc 小结 上面这个例子使用Eureka实现了服务的注册与发现,但是有一个问题就是获取服务注册列表的方式比较low并且太方便,还有一个问题就是没有使用负载均衡(Load Balance),这样就没法实现微服务的HA。在后面的文章将会介绍Eureka Server的HA和使用Robbin实现LB。。","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"}]},{"title":"Spring Cloud Eureka中文翻译","slug":"sc/sc-fy-eureka","date":"2017-01-25T06:00:00.000Z","updated":"2017-06-17T03:34:56.000Z","comments":true,"path":"sc/sc-fy-eureka/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-fy-eureka/","excerpt":"Eureka学习文档资料: Netflix Eureka详细文档 Spring Cloud中对Eureka的介绍 Spring Cloud Eureka工程中的文档 说明:本文主要是对http://cloud.spring.io/spring-cloud-static/spring-cloud.html#_spring_cloud_netflix ,Eureka相关的内容进行翻译更新,由于工作原因可能存在时效性敬请谅解。 Spring Cloud Netflix提供了对Netflix\b开源项目的集成,使得我们可以以Spring Boot编程风格使用Netflix旗下相关框架。你只需要在程序中添加注解,就能使用成熟的Netflix组件来快速实现分布式系统的常见架构模式。这些模式包括服务发现(Eureka), 断路器(Hystrix), 智能路由(Zuul)和客户端负载均衡(Ribbon)。","text":"Eureka学习文档资料: Netflix Eureka详细文档 Spring Cloud中对Eureka的介绍 Spring Cloud Eureka工程中的文档 说明:本文主要是对http://cloud.spring.io/spring-cloud-static/spring-cloud.html#_spring_cloud_netflix ,Eureka相关的内容进行翻译更新,由于工作原因可能存在时效性敬请谅解。 Spring Cloud Netflix提供了对Netflix\b开源项目的集成,使得我们可以以Spring Boot编程风格使用Netflix旗下相关框架。你只需要在程序中添加注解,就能使用成熟的Netflix组件来快速实现分布式系统的常见架构模式。这些模式包括服务发现(Eureka), 断路器(Hystrix), 智能路由(Zuul)和客户端负载均衡(Ribbon)。 服务发现:Eureka客户端服务发现是微服务架构中的一项核心服务。如果没有该服务,我们就只能为每一个服务调用者手工配置可用服务的地址,这不仅繁琐而且非常容易出错。Eureka包括了服务端和客户端两部分。服务端可以做到高可用集群部署,每一个节点可以自动同步,有相同的服务注册信息。 向Eureka注册服务当客户端向Eureka注册自己时会提供一些元信息,如主机名、端口号、获取健康信息的url和主页等。Eureka通过心跳连接判断服务是否在线,如果心跳检测失败超过指定时间,对应的服务通常就会被移出可用服务列表。 译者注:向Eureka Server注册过的服务会每30秒向Server发送一次心跳连接, Server会根据心跳数据更新该服务的健康状态并复制到其他Server中。如果超过90秒没有收到该服务的心跳数据,则Server会将该服务移出列表。参考文档:https://github.com/Netflix/eureka/wiki/Eureka-at-a-glance Eureka Client代码示例: 1234567891011121314151617@Configuration@ComponentScan@EnableAutoConfiguration@EnableEurekaClient@RestControllerpublic class Application { @RequestMapping(\"/\") public String home() { return \"Hello world\"; } public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(true).run(args); }} (其实就是个普通的Spring Boot应用)。 在这个例子中我们使用了@EnableEurekaClient注解,但是要在使用Eureka的前提下,你也可以使用@EnableDiscoveryClient注解达到同样的效果。除此之外需要在Eureka server上加上配置信息,如下所示: application.yml 1234eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ 其中,defaultZone的作用是给没有指定Zone的客户端一个默认的Eureka地址。 译者注:客户端可以在配置文件中指定当前服务属于哪一个Zone,如果没有指定,则属于默认Zone。 默认的应用名(Service ID)、主机名和端口号分别对应配置信息中的${spring.application.name}、${spring.application.name}和${server.port}参数。 使用@EnableEurekaClient注解后当前应用会同时变成一个Eureka服务端实例(它会注册自身)和Eureka客户端(可以查询当前服务列表),与此相关的配置都在以eureka.instance.*开头的参数下。只要你指定了spring.application.name参数,那么就可以放心的使用默认参数而不需要修改任何配置。 要查看更详细的参数,请参阅EurekaInstanceConfigBean和EurekaClientConfigBean。 Eureka Server的身份验证如果客户端的eureka.client.serviceUrl.defaultZone参数值(即Eureka Server的地址)中包含HTTP Basic Authentication信息,如[http://user:password@localhost:8761/eureka](http://user:password@localhost:8761/eureka),那么客户端就会自动使用该用户名、密码信息与Eureka服务端进行验证。如果你需要更复杂的验证逻辑,你必须注册一个DiscoveryClientOptionalArgs组件,并将ClientFilter组件注入,在这里定义的逻辑会在每次客户端向服务端发起请求时执行。 由于Eureka的限制,Eureka不支持单节点身份验证。 状态页和健康信息指示器Eureka应用的状态页和健康信息默认的url为/info和/health,这与Spring Boot Actuator中对应的Endpoint是重复的,因此你必须进行修改: 1234eureka: instance: statusPageUrlPath: ${management.context-path}/info healthCheckUrlPath: ${management.context-path}/health 客户端通过这些URL获取数据,并根据这些数据来判断是否可以向某个服务发起请求。 使用HTTPS你可以指定EurekaInstanceConfig类中的eureka.instance.[nonSecurePortEnabled,securePortEnabled]=[false,true]属性来指定是否使用HTTPS。当配置使用HTTPS时,Eureka Server会返回以https开头的服务地址。 即使配置了使用HTTPS,Eureka的主页依然是以普通 HTTP 方式访问的。你需要手动添加一些配置来将这些页面也通过HTTPS保护起来: 12345eureka: instance: statusPageUrl: https://${eureka.hostname}/info healthCheckUrl: https://${eureka.hostname}/health homePageUrl: https://${eureka.hostname}/ 注意,eureka,hostname是Eureka原生属性,只有新版本的Eureka才支持该属性。你也可以\b用Spring EL表达式代替:${eureka.instance.hostName} 如果你的应用前端部署了代理,并且SSL的终点是此代理服务器,那么你就需要在应用中解析forwarded请求头。如果你在配置文件中添加了X-Forwarded-*相关参数,Spring Boot中的嵌入式Tomcat会自动解析该请求头。一种表明你没有处理好forwarded请求头的迹象就是你的应用渲染出的HTML页面中链接显示的是错误的主机名和端口号。 健康检查默认情况下,Eureka通过客户端发来的心跳包来判断客户端是否在线。如果你不显式指定,客户端在心跳包中不会包含当前应用的健康数据(由Spring Boot Actuator提供)。这意味着只要客户端启动时完成了服务注册,那么该客户端在主动注销之前在Eureka中的状态会永远是UP状态。我们可以通过配置修改这一默认行为,即在客户端发送心跳包时会带上自己的健康信息。这样做的后果是只有当该服务的状态是UP时才能被访问,其它的任何状态都会导致该服务不能被调用。 1234eureka: client: healthcheck: enabled: true 如果你想对健康检查有更细粒度的控制,你可以自己实现com.netflix.appinfo.HealthCheckHandler接口。 以下内容翻译自Eureka官方手册: Eureka客户端会每隔30s向服务端发送心跳包以告知服务端当前客户端没有挂掉。对于Client来说,服务Server超过90s没有收到该Client的心跳数据,Server就会把该Client移出服务列表。最好不要修改30s的默认心跳间隔,因为Server会使用这个时间数值来判断是否出现了大面积故障。(译者:意思是比如Eureka默认2分钟收不到心跳就认为网络出了故障,你如果把这个心跳间隔改成了3分钟,那就出问题了。) Eureka元数据说明我们有必要花一些时间来了解一下Eureka的元数据,这样就可以添加一些自定义的数据以适应特定的业务场景。像主机名、IP地址、端口号、状态页url和健康检查url都是Eureka定义的标准元数据。这些元数据会被保存在Eureka Server的注册信息中,客户端会读取这些数据来向需要调用的服务直接发起连接。你可以使用以eureka.instance.metadataMap开头的参数来添加你自定义的元数据,所有客户端都会读取到该信息。通过这种方式你能给客户端自定义一些行为。 使用EurekaClient对象当添加了@EnableDiscoveryClient或@EnableEurekaClient注解后,你就可以在应用中使用EurekaClient对象来获取服务列表: 1234567@Autowiredprivate EurekaClient discoveryClient;public String serviceUrl() { InstanceInfo instance = discoveryClient.getNextServerFromEureka(\"STORES\", false); return instance.getHomePageUrl();} 不要在@PostConstruct或@Scheduled方法中使用EurekaClient。在ApplicationContext还没有完全启动时使用该对象会发生错误。 使用Spring的DiscoveryClient对象你没有必要直接使用Netflix原生的EurekaClient对象,在此基础上做一些封装使用起来会更方便。Spring Cloud支持Feign和Spring RestTmpelate,它们都可以使用服务的逻辑名而不是URL地址来查询服务。如果想给Ribbon手工指定服务列表,你可以将<client>.ribbon.listOfServers属性设为逗号分隔的物理地址或主机名, 参数中的client是服务id,即服务名。 你可以使用Spring提供的DiscoveryClient对象从而代码不会与Eureka紧耦合: 12345678910@Autowiredprivate DiscoveryClient discoveryClient;public String serviceUrl() { List<ServiceInstance> list = discoveryClient.getInstances(\"STORES\"); if (list != null && list.size() > 0 ) { return list.get(0).getUri(); } return null;} 为什么注册一个服务这么慢?服务的注册涉及到心跳连接,默认为每30秒一次。只有当Eureka服务端和客户端本地缓存中的服务元数据相同时这个服务才能被其它客户端发现,这需要3个心跳周期。你可以通过参数eureka.instance.leaseRenewalIntervalInSeconds调整这个时间间隔来加快这个过程。在生产环境中你最好使用默认值,因为Eureka内部的某些计算依赖于该时间间隔。 服务发现:Eureka服务端添加spring-cloud-starter-eureka-server,主类代码示例如下: 123456789@SpringBootApplication@EnableEurekaServerpublic class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(true).run(args); }} 服务启动后,Eureka有一个带UI的主页,注册信息可以通过/eureka/*下的URL获取到。 高可用, Zone 和 RegionEureka把所有注册信息都放在内存中,所有注册过的客户端都会向Eureka发送心跳包来保持连接。客户端会有一份本地注册信息的缓存,这样就不需要每次远程调用时都向Eureka查询注册信息。 默认情况下,Eureka服务端自身也是个客户端,所以需要指定一个Eureka Server的URL作为”伙伴”(peer)。如果你没有提供这个地址,Eureka Server也能正常启动工作,但是在日志中会有大量关于找不到peer的错误信息。 Standalone模式只要Eureka Server进程不会挂掉,这种集Server和Client于一身和心跳包的模式能让Standalone(单台)部署的Eureka Server非常容易进行灾难恢复。在 Standalone 模式中,可以通过下面的配置来关闭查找“伙伴”的行为: 1234567891011server: port: 8761eureka: instance: hostname: localhost client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ 注意,serviceUrl中的地址的主机名要与本地主机名相同。 “伙伴”感知Eureka Server可以通过运行多个实例并相互指定为“伙伴”的方式来达到更高的高可用性。实际上这就是默认设置,你只需要指定“伙伴”的地址就可以了: 12345678910111213141516171819202122232425eureka: client: serviceUrl: defaultZone: http://peer1/eureka/,http://peer2/eureka/,http://peer3/eureka/---spring: profiles: peer1eureka: instance: hostname: peer1---spring: profiles: peer2eureka: instance: hostname: peer2---spring: profiles: peer3eureka: instance: hostname: peer3 在上面这个例子中,我们通过使用不同profile配置的方式可以在本地运行两个Eureka Server。你可以通过修改/etc/host文件,使用上述配置在本地测试伙伴感特性。 你可以同时启动多个Eureka Server, 并通过伙伴配置使之围成一圈(相邻两个Server互为伙伴),这些Server中的注册信息都是同步的。If the peers are physically separated (inside a data centre or between multiple data centres) then the system can in principle survive split-brain type failures. 使用IP地址有些时候你可能更倾向于直接使用IP地址定义服务而不是使用主机名。把eureka.instance.preferIpAddress参数设为true时,客户端在注册时就会使用自己的ip地址而不是主机名。","categories":[{"name":"Spring Cloud翻译","slug":"Spring-Cloud翻译","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud翻译/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"}]},{"title":"爱油科技基于SpringCloud的微服务实践","slug":"sc/sc-fx1","date":"2016-11-22T06:00:00.000Z","updated":"2017-06-17T03:18:26.000Z","comments":true,"path":"sc/sc-fx1/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-fx1/","excerpt":"爱油科技基于SpringCloud的微服务实践个人简介刘思贤(微博@starlight36),爱油科技架构师、PMP。主要负责业务平台架构设计,DevOps实施和研发过程持续改进等,关注领域驱动设计与微服务、建设高效团队和工程师文化培养。 摘要本次分享主要介绍了爱油科技基于Docker和Spring Cloud将整体业务微服务化的一些实践经验,主要包括: 微服务架构的分层和框架选型 服务发现和配置管理 服务集成和服务质量保证 基于领域驱动设计 实施DevOps","text":"爱油科技基于SpringCloud的微服务实践个人简介刘思贤(微博@starlight36),爱油科技架构师、PMP。主要负责业务平台架构设计,DevOps实施和研发过程持续改进等,关注领域驱动设计与微服务、建设高效团队和工程师文化培养。 摘要本次分享主要介绍了爱油科技基于Docker和Spring Cloud将整体业务微服务化的一些实践经验,主要包括: 微服务架构的分层和框架选型 服务发现和配置管理 服务集成和服务质量保证 基于领域驱动设计 实施DevOps 从单体应用到微服务 单体应用优点 小而美,结构简单易于开发实现 部署门槛低,单个Jar包或者网站打包即可部署 可快速实现多实例部署 缺点 随着业务发展更多的需求被塞进系统,体系结构逐渐被侵蚀反应堆林立 被技术绑架,难以为特定业务选择平台或框架,尽管可能有更适宜的技术做这件事 协作困难,不同业务的团队在一个系统上进行开发相互冲突 难以扩展,为了热点业务而不得不同时扩容全部业务,或者难以继续扩容 架构拆分拆分:按行分层,按列分业务 在我们的微服务体系中,所有的服务被划分为了三个层次: 基础设施层:为所有业务提供基础设施,包括服务注册、数据库和NoSQL、对象存储、消息队列等基础设施服务,这一层通常是由成熟组件、第三方服务组成。 业务服务层:业务微服务,根据业务领域每个子域单独一个微服务,分而治之。 接入层:直接对外提供服务,例如网站、API接口等。接入层不包含复杂的业务逻辑,只做呈现和转换。 项目中我们主要关注业务服务层和接入层,对于没有足够运维力量的我们,基础设施使用云服务是省事省力的选择。 业务服务层我们给他起名叫作Epic,接入层我们起名Rune,建立之初便订立了如下原则: 业务逻辑层内所有服务完全对等,可相互调用 业务逻辑层所有服务必须是无状态的 接入层所有服务可调用业务逻辑层所有服务,但接入层内部同层服务之间不可调用 接入层不能包含业务逻辑代码 所有微服务必须运行在Docker容器里 业务逻辑层我们主要使用使用Java,接入层我们主要使用PHP或Node。后来随着团队的成长,逐步将接入层全部迁移至Node。 框架选型爱油科技作为一家成品油行业的初创型公司,需要面对非常复杂的业务场景,而且随着业务的发展,变化的可能性非常高。所以在微服务架构设计之初,我们就期望我们的微服务体系能: 不绑定到特定的框架、语言 服务最好是Restful风格 足够简单,容易落地,将来能扩展 和Docker相容性好 目前常见的微服务相关框架: Dubbo、DubboX Spring Cloud Motan Thrift、gRPC 这些常见的框架中,Dubbo几乎是唯一能被称作全栈微服务框架的“框架”,它包含了微服务所需的几乎所有内容,而DubboX作为它的增强,增加了REST支持。 它优点很多,例如: 全栈,服务治理的所有问题几乎都有现成答案 可靠,经过阿里实践检验的产品 实践多,社区有许多成功应用Dubbo的经验 不过遗憾的是: 已经停止维护 不利于裁剪使用 “过于Java”,与其他语言相容性一般 Motan是微博平台微服务框架,承载了微博平台千亿次调用业务。 优点是: 性能好,源自于微博对高并发和实时性的要求 模块化,结构简单,易于使用 与其他语言相容性好 不过: 为“短平快”业务而生,即业务简单,追求高性能高并发。 Apache Thrift、gRPC等虽然优秀,并不能算作微服务框架,自身并不包括服务发现等必要特性。 如果说微服务少不了Java,那么一定少不了Spring,如果说少不了Spring,那么微服务“官配”Spring Cloud当然是值得斟酌的选择。 优点: “不做生产者,只做搬运工” 简单方便,几乎零配置 模块化,松散耦合,按需取用 社区背靠Spring大树 不足: 轻量并非全栈 没解决RPC的问题 实践案例少 根据我们的目标,我们最终选择了Spring Cloud作为我们的微服务框架,原因有4点: 虽然Dubbo基础设施更加完善,但结构复杂,我们很难吃得下,容易出坑 基于Apache Thrift和gRPC自研,投入产出比很差 不想过早引入RPC以防滥用,Restful风格本身就是一种约束。 做选择时,Motan还没有发布 Spring CloudSpring Cloud是一个集成框架,将开源社区中的框架集成到Spring体系下,几个重要的家族项目: spring-boot,一改Java应用程序运行难、部署难,甚至无需Web容器,只依赖JRE即可 spring-cloud-netflix,集成Netflix优秀的组件Eureka、Hystrix、Ribbon、Zuul,提供服务发现、限流、客户端负载均衡和API网关等特性支持 spring-cloud-config,微服务配置管理 spring-cloud-consul,集成Consul支持 服务发现和配置管理Spring Cloud Netflix提供了Eureka服务注册的集成支持,不过没选它是因为: 更适合纯Java平台的服务注册和发现 仍然需要其他分布式KV服务做后端,没解决我们的核心问题 Docker作为支撑平台的重要技术之一,Consul几乎也是我们的必选服务。因此我们觉得一事不烦二主,理所应当的Consul成为我们的服务注册中心。 Consul的优势: 使用Raft一致性算法,能保证分布式集群内各节点状态一致 提供服务注册、服务发现、服务状态检查 支持HTTP、DNS等协议 提供分布式一致性KV存储 也就是说,Consul可以一次性解决我们对服务注册发现、配置管理的需求,而且长期来看也更适合跟不同平台的系统,包括和Docker调度系统进行整合。 最初打算自己开发一个Consul和Spring Cloud整合的组件,不过幸运的是,我们做出这个决定的时候,spring-cloud-consul刚刚发布了,我们可以拿来即用,这节约了很多的工作量。 因此借助Consul和spring-cloud-consul,我们实现了 服务注册,引用了srping-cloud-consul的项目可以自动注册服务,也可以通过HTTP接口手动注册,Docker容器也可以自动注册 服务健康状态检查,Consul可以自动维护健康的服务列表 异构系统可以直接通过Consul的HTTP接口拉取并监视服务列表,或者直接使用DNS解析服务 通过分布式一致性KV存储进行微服务的配置下发 为一些业务提供选主和分布式锁服务 当然也踩到了一些坑: spring-cloud-consul服务注册时不能正确选判本地ip地址。对于我们的环境来说,无论是在服务器上,还是Docker容器里,都有多个网络接口同时存在,而spring-cloud-consul在注册服务时,需要先选判本地服务的IP地址,判断逻辑是以第一个非本地地址为准,常常错判。因此在容器中我们利用entrypoint脚本获取再通过环境变量强制指定。 12345678910111213141516171819202122#!/usr/bin/env bashset -e# If service runs as Rancher service, auto set advertise ip address# from Rancher metadata service.if [ -n \"$RUN_IN_RANCHER\" ]; then echo \"Waiting for ip address...\" # Waiting for ip address sleep 5 RANCHER_MS_BASE=http://rancher-metadata/2015-12-19 PRIMARY_IP=`curl -sSL $RANCHER_MS_BASE/self/container/primary_ip` SERVICE_INDEX=`curl -sSL $RANCHER_MS_BASE/self/container/service_index` if [ -n \"$PRIMARY_IP\" ]; then export SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME=$PRIMARY_IP fi echo \"Starting service #${SERVICE_INDEX-1} at $PRIMARY_IP.\"fiexec \"$@\" 我们的容器运行在Rancher中,所以可以利用Rancher的metadata服务来获取容器的IP地址,再通过SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME环境变量来设置服务发现的注册地址。基于其他容器调度平台也会很相似。 另外一些服务中内置了定时调度任务等,多实例启动时需要单节点运行调度任务。通过Consul的分布式锁服务,我们可以让获取到锁的节点启用调度任务,没获取到的节点等待获取锁。 服务集成为了方便开发人员使用,微服务框架应当简单容易使用。对于很多微服务框架和RPC框架来说,都提供了很好的机制。在Spring Cloud中通过OpenFeign实现微服务之间的快速集成: 服务方声明一个Restful的服务接口,和普通的Spring MVC控制器几乎别无二致: 1234567891011121314@RestController@RequestMapping(\"/users\")public class UserResource { @RequestMapping(value = \"{id}\", method = RequestMethod.GET, produces = \"application/json\") public UserRepresentation findOne(@PathVariable(\"id\") String id) { User user = this.userRepository.findByUserId(new UserId(id)); if (user == null || user.getDeleted()) { throw new NotFoundException(\"指定ID的用户不存在或者已被删除。\"); } return new UserRepresentation(user); }} 客户方使用一个微服务接口,只需要定义一个接口: 1234567@FeignClient(\"epic-member-microservice\")public interface UserClient { @Override @RequestMapping(value = \"/users/{id}\", method = RequestMethod.GET, produces = \"application/json\") User findOne(@PathVariable(\"id\") String id);} 在需要使用UserClient的Bean中,直接注入UserClient类型即可。事实上,UserClient和相关VO类,可以直接作为公共接口封装在公共项目中,供任意需要使用的微服务引用,服务方Restful Controller直接实现这一接口即可。 OpenFeign提供了这种简单的方式来使用Restful服务,这大大降低了进行接口调用的复杂程度。 对于错误的处理,我们使用HTTP状态码作为错误标识,并做了如下规定: 4xx用来表示由于客户方参数错误、状态不正确、没有权限、操作冲突等种种原因导致的业务错误。 5xx用来表示由于服务方系统异常、无法服务等原因服务不可用的错误。 对于服务器端,只需要在一个异常类上添加注解,即可指定该异常的HTTP响应状态码,例如: 123456789101112131415@ResponseStatus(HttpStatus.NOT_FOUND)public class NotFoundException extends RuntimeException { public NotFoundException() { super(\"查找的资源不存在或者已被删除。\"); } public NotFoundException(String message) { super(message); } public NotFoundException(String message, Throwable cause) { super(message, cause); }} 对于客户端我们实现了自己的FeignClientExceptionErrorDecoder来将请求异常转换为对于的异常类,示例如下: 123456789101112131415161718192021222324252627@Componentpublic class FeignClientExceptionErrorDecoder implements ErrorDecoder { private final ErrorDecoder delegate = new ErrorDecoder.Default(); @Override public Exception decode(String methodKey, Response response) { // Only decode 4xx errors. if (response.status() >= 500) { return delegate.decode(methodKey, response); } // Response content type must be json if (response.headers().getOrDefault(\"Content-Type\", Lists.newArrayList()).stream() .filter(s -> s.toLowerCase().contains(\"json\")).count() > 0) { try { String body = Util.toString(response.body().asReader()); // 转换并返回异常对象 ... } catch (IOException ex) { throw new RuntimeException(\"Failed to process response body.\", ex); } } return delegate.decode(methodKey, response); }} 需要注意的是,decode方法返回的4xx状态码异常应当是HystrixBadRequestException的子类对象,原因在于,我们把4xx异常视作业务异常,而不是由于故障导致的异常,所以不应当被Hystrix计算为失败请求,并引发断路器动作,这一点非常重要。 在UserClient.findOne方法的调用代码中,即可直接捕获相应的异常了: 12345try { User user = this.userClient.findOne(new UserId(id));} catch(NotFoundException ex) { ...} 通过OpenFeign,我们大大降低了Restful接口进行服务集成的难度,几乎做到了无额外工作量的服务集成。 服务质量保证微服务架构下,由于调用需要跨系统进行远程操作,各微服务独立运维,所以在设计架构时还必须考虑伸缩性和容错性,具体地说主要包括以下几点要求: 服务实例可以平滑地加入、移除 流量可以均匀地分布在不同的实例上 接口应当资源隔离,防止因为个别接口调用时间过长导致线程池被占满而导致整个服务不可用 能支持接口降级并隔离故障节点,防止集群雪崩 服务能进行平滑升级 Spring Cloud中内置的spring-cloud-netflix的其他组件为我们提供了很好的解决方案: Hystrix - 实现了断路器模式,帮助控流和降级,防止集群雪崩,就像汽车的避震器 Ribbon - 提供了客户端负载均衡器 Zuul - API网关模式,帮助实现接口的路由、认证等 下面主要介绍一下,各个组件在进行服务质量保证中是如何发挥作用的。 ConsulConsul中注册了一致性的可用的服务列表,并通过健康检查保证这些实例都是存活的,服务注册和检查的过程如下: 服务启动完成,服务端口开始监听时,spring-cloud-consul通过Consul接口发起服务注册,将服务的/health作为健康检查端点; Consul每隔5秒访问/health,检查当前微服务是否为UP状态; /health将会收集微服务内各个仪表收集上来的状态数据,主要包括数据库、消息队列是否连通等; 如果为UP状态,则微服务实例被标记为健康可用,否则被标记成失败; 当服务关闭时,先从Consul中取消服务注册,再优雅停机。 这样能够保证Consul中列出的所有微服务状态都是健康可用的,各个微服务会监视微服务实例列表,自动同步更新他们。 HystrixHystrix提供了断路器模式的实现,主要在三个方面可以说明: 图片来自Hystrix项目文档 首先Hystrix提供了降级方法,断路器开启时,操作请求会快速失败不再向后投递,直接调用fallback方法来返回操作;当操作失败、被拒或者超时后,也会直接调用fallback方法返回操作。这可以保证在系统过载时,能有后备方案来返回一个操作,或者优雅的提示错误信息。断路器的存在能让故障业务被隔离,防止过载的流量涌入打死后端数据库等。 然后是基于请求数据统计的断路开关,在Hystrix中维护一个请求统计了列表(默认最多10条),列表中的每一项是一个桶。每个桶记录了在这个桶的时间范围内(默认是1秒),请求的成功数、失败数、超时数、被拒数。其中当失败请求的比例高于某一值时,将会触发断路器工作。 最后是不同的请求命令(HystrixCommand)可以使用彼此隔离的资源池,不会发生相互的挤占。在Hystrix中提供了两种隔离机制,包括线程池和信号量。线程池模式下,通过线程池的大小来限制同时占用资源的请求命令数目;信号量模式下通过控制进入临界区的操作数目来达到限流的目的。 这里包括了Hystrix的一些重要参数的配置项: 参数 说明 circuitBreaker.requestVolumeThreshold 至少在一个统计窗口内有多少个请求后,才执行断路器的开关,默认20 circuitBreaker.sleepWindowInMilliseconds 断路器触发后多久后才进行下一次判定,默认5000毫秒 circuitBreaker.errorThresholdPercentage 一个统计窗口内百分之多少的请求失败才触发熔断,默认是50% execution.isolation.strategy 运行隔离策略,支持Thread,Semaphore,前者通过线程池来控制同时运行的命令,后者通过信号来控制,默认是Thread execution.isolation.thread.interruptOnTimeout 命令执行的超时时间,默认1000毫秒 coreSize 线程池大小,默认10 keepAliveTimeMinutes 线程存活时间,默认为1分钟 maxQueueSize 最大队列长度,-1使用SynchronousQueue,默认-1。 queueSizeRejectionThreshold 允许队列堆积的最大数量 RibbonRibbon使用Consul提供的服务实例列表,可以通过服务名选取一个后端服务实例连接,并保证后端流量均匀分布。spring-cloud-netflix整合了OpenFeign、Hystrix和Ribbon的负载均衡器,整个调用过程如下(返回值路径已经省略): 在这个过程中,各个组件扮演的角色如下: Feign作为客户端工厂,负责生成客户端对象,请求和应答的编解码 Hystrix提供限流、断路器、降级、数据统计 Ribbon提供负载均衡器 Feign负责提供客户端接口收调用,把发起请求操作(包括编码、解码和请求数据)封装成一个Hystrix命令,这个命令包裹的请求对象,会被Ribbon的负载均衡器处理,按照负载均衡策略选择一个主机,然后交给请求对象绑定的HTTP客户端对象发请求,响应成功或者不成功的结果,返回给Hystrix。 spring-cloud-netflix中默认使用了Ribbon的ZoneAwareLoadBalancer负载均衡器,它的负载均衡策略的核心指标是平均活跃请求数(Average Active Requests)。ZoneAwareLoadBalancer会拉取所有当前可用的服务器列表,然后将目前由于种种原因(比如网络异常)响应过慢的实例暂时从可用服务实例列表中移除,这样的机制可以保证故障实例被隔离,以免继续向其发送流量导致集群状态进一步恶化。不过由于目前spring-cloud-consul还不支持通过consul来指定服务实例的所在区,我们正在努力将这一功能完善。除了选区策略外,Ribbon中还提供了其他的负载均衡器,也可以自定义合适的负载均衡器。 总的来看,spring-cloud-netflix和Ribbon中提供了基本的负载均衡策略,对于我们来说已经足够用了。但实践中,如果需要进行灰度发布或者需要进行流量压测,目前来看还很难直接实现。而这些特性在Dubbo则开箱即用。 ZuulZuul为使用Java语言的接入层服务提供API网关服务,既可以根据配置反向代理指定的接口,也可以根据服务发现自动配置。Zuul提供了类似于iptables的处理机制,来帮助我们实现验证权鉴、日志等,请求工作流如下所示: 图片来自Zuul官方文档。 使用Zuul进行反向代理时,同样会走与OpenFeign类似的请求过程,确保API的调用过程也能通过Hystrix、Ribbon提供的降级、控流机制。 Hystrix DashboardHystrix会统计每个请求操作的情况来帮助控制断路器,这些数据是可以暴露出来供监控系统热点。Hystrix Dashboard可以将当前接口调用的情况以图形形式展示出来: 图片来自Hystrix Dashboard官方示例 Hystrix Dashboard既可以集成在其他项目中,也可以独立运行。我们直接使用Docker启动一个Hystrix Dashboard服务即可: 1docker run --rm -ti -p 7979:7979 kennedyoliveira/hystrix-dashboard 为了实现能对整个微服务集群的接口调用情况汇总,可以使用spring-cloud-netflix-turbine来将整个集群的调用情况汇集起来,供Hystrix Dashboard展示。 日志监控微服务的日志直接输出到标准输出/标准错误中,再由Docker通过syslog日志驱动将日志写入至节点机器机的rsyslog中。rsyslog在本地暂存并转发至日志中心节点的Logstash中,既归档存储,又通过ElasticSearch进行索引,日志可以通过Kibana展示报表。 在rsyslog的日志收集时,需要将容器信息和镜像信息加入到tag中,通过Docker启动参数来进行配置: 1--log-driver syslog --log-opt tag="{{.ImageName}}/{{.Name}}/{{.ID}}" 不过rsyslog默认只允许tag不超过32个字符,这显然是不够用的,所以我们自定义了日志模板: 1template (name="LongTagForwardFormat" type="string" string="<%PRI%>%TIMESTAMP:::date-rfc3339% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg%") 在实际的使用过程中发现,当主机内存负载比较高时,rsyslog会发生日志无法收集的情况,报日志数据文件损坏。后来在Redhat官方找到了相关的问题,确认是rsyslog中的一个Bug导致的,当开启日志压缩时会出现这个问题,我们选择暂时把它禁用掉。 领域驱动设计我们使用领域驱动设计(DDD)的方法来构建微服务,因为微服务架构和DDD有一种天然的契合。把所有业务划分成若干个子领域,有强内在关联关系的领域(界限上下文)应当被放在一起作为一个微服务。最后形成了界限上下文-工作团队-微服务一一对应的关系: 身份与访问 - 团队A - 成员微服务 商品与促销 - 团队B - 商品微服务 订单交易 - 团队C - 交易微服务 … 微服务设计在设计单个微服务(Epic层的微服务)时,我们这样做: 使用OOD方法对业务进行领域建模,领域模型应当是充血模型 领域服务帮助完成多个领域对象协作 事件驱动,提供领域事件,供内部或者其他微服务使用 依赖倒置,在适配器接口中实现和框架、组件、SDK的整合 这给我们带来了显著的好处: 服务开发时关注于业务,边界合理清晰 容易直接对领域模型进行单元测试 不依赖特定组件或者平台 事务问题从单体应用迁移到微服务架构时,不得不面临的问题之一就是事务。在单体应用时代,所有业务共享同一个数据库,一次请求操作可放置在同一个数据库事务中;在微服务架构下,这件事变得非常困难。然而事务问题不可避免,非常关键。 解决事务问题时,最先想到的解决方法通常是分布式事务。分布式事务在传统系统中应用的比较广泛,主要基于两阶段提交的方式实现。然而分布式事务在微服务架构中可行性并不高,主要基于这些考虑: 分布式事务需要事务管理器,对于不同语言平台来说,几乎没有有一致的实现来进行事务管理; 并非所有的持久化基施都提供完整ACID的事务,比如现在广泛使用的NoSQL; 分布式事务存在性能问题。 根据CAP理论,分布式系统不可兼得一致性、可用性、分区容错性(可靠性)三者,对于微服务架构来讲,我们通常会保证可用性、容错性,牺牲一部分一致性,追求最终一致性。所以对于微服务架构来说,使用分布式事务来解决事务问题无论是从成本还是收益上来看,都不划算。 对微服务系统来说解决事务问题,CQRS+Event Sourcing是更好的选择。 CQRS是命令和查询职责分离的缩写。CQRS的核心观点是,把操作分为修改状态的命令(Command),和返回数据的查询(Query),前者对应于“写”的操作,不能返回数据,后者对应于“读”的操作,不造成任何影响,由此领域模型被一分为二,分而治之。 Event Sourcing通常被翻译成事件溯源,简单的来说就是某一对象的当前状态,是由一系列的事件叠加后产生的,存储这些事件即可通过重放获得对象在任一时间节点上的状态。 通过CQRS+Event Sourcing,我们很容易获得最终一致性,例如对于一个跨系统的交易过程而言: 用户在交易微服务提交下单命令,产生领域事件PlaceOrderEvent,订单状态PENDING; 支付微服务收到领域事件进行扣款,扣款成功产生领域事件PaidEvent; 交易微服务收到领域事件PaidEvent,将订单标记为CREATED; 若支付微服务发现额度不足扣款失败,产生领域事件InsufficientEvent,交易微服务消费将订单标记为CANCELED。 我们只要保证领域事件能被持久化,那么即使出现网络延迟或部分系统失效,我们也能保证最终一致性。 实践上,我们利用Spring从4.2版本开始支持的自定义应用事件机制将本地事务和事件投递结合起来进行: 领域内业务过程会产生领域事件,通过Spring的应用事件机制进行应用内投递; 监听相应的领域事件,在事务提交前投递至消息队列; 以上全都没有异常发生,则本地事务提交,如果出现异常,本地事务回滚。 一些小经验 使用Spring Configured实现非Spring Bean的依赖注入(自己new的对象也可以注入了,对充血模型非常有用) 使用Swagger UI实现自文档的微服务,写好接口即有文档,即可调试 DevOps到目前为止我们已经有数十个微服务运行于线上了,微服务数目甚至多过了团队人数。如果没有DevOps支持,运维这些微服务将是一场灾难。我们使用Docker镜像作为微服务交付的标准件: Gitlab管理团队项目代码 Gitlab-CI提供构建打包,大家提交的项目都要构建并跑通测试 使用Rancher作为Docker调度平台,Merge后RC分支自动部署 测试通过后统一上线发布 由于时间所限,这里就不展开赘述了。 永不完美基于spring-cloud-consul的配置管理仍然需要完善,对于大规模应用的环境中,配置的版本控制、灰度、回滚等非常重要。SpringCloud提供了一个核,但是具体的使用还要结合场景、需求和环境等,再做一些工作。 对于非JVM语言的微服务和基于SpringCloud的微服务如何协同治理,这一问题仍然值得探索。包括像与Docker编排平台,特别是与Mesos协同进行伸缩的服务治理,还需要更多的实践来支持。 总结 是否选用微服务架构,应当根据业务实际情况进行判断,切勿跟风为了微服务而微服务; 目前来看还没有微服务全栈框架,Spring Cloud也未必是最优方案,技术选型还是应当务实; 微服务架构下,对于业务的理解拆分、领域建模等提出了更高的要求,相比框架,它们才是微服务架构的基石; DevOps是微服务实践中的重要一环,不容小视。","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"},{"name":"实践分享","slug":"实践分享","permalink":"http://blog.springcloud.cn/tags/实践分享/"}]},{"title":"Spring Cloud Eureka服务下线(Cancel)源码分析","slug":"sc/sc-eureka-cancle","date":"2016-11-19T06:00:00.000Z","updated":"2017-06-17T03:08:12.000Z","comments":true,"path":"sc/sc-eureka-cancle/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-eureka-cancle/","excerpt":"Cancel(服务下线)概述在Service Provider服务shut down的时候,需要及时通知Eureka Server把自己剔除,从而避免客户端调用已经下线的服务。 服务提供者端源码分析 在eureka-client-1.4.1中的com.netflix.discovery.DiscoveryClient中shutdown()的867行。","text":"Cancel(服务下线)概述在Service Provider服务shut down的时候,需要及时通知Eureka Server把自己剔除,从而避免客户端调用已经下线的服务。 服务提供者端源码分析 在eureka-client-1.4.1中的com.netflix.discovery.DiscoveryClient中shutdown()的867行。 123456789101112131415161718192021222324252627282930313233/** * Shuts down Eureka Client. Also sends a deregistration request to the * eureka server. */ @PreDestroy @Override public synchronized void shutdown() { if (isShutdown.compareAndSet(false, true)) { logger.info(\"Shutting down DiscoveryClient ...\"); if (statusChangeListener != null && applicationInfoManager != null) { applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId()); } cancelScheduledTasks(); // If APPINFO was registered if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()) { applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN); //调用下线接口 unregister(); } if (eurekaTransport != null) { eurekaTransport.shutdown(); } heartbeatStalenessMonitor.shutdown(); registryStalenessMonitor.shutdown(); logger.info(\"Completed shut down of DiscoveryClient\"); } } Tips @PreDestroy注解或shutdown()的方法是服务下线的入口 在eureka-client-1.4.1中的com.netflix.discovery.DiscoveryClient中unregister()的897行12345678910111213141516 /** * unregister w/ the eureka service. */ void unregister() { // It can be null if shouldRegisterWithEureka == false if(eurekaTransport != null && eurekaTransport.registrationClient != null) { try { logger.info(\"Unregistering ...\"); //发送服务下线请求 EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId()); logger.info(PREFIX + appPathIdentifier + \" - deregister status: \" + httpResponse.getStatusCode()); } catch (Exception e) { logger.error(PREFIX + appPathIdentifier + \" - de-registration failed\" + e.getMessage(), e); } }} Eureka Server服务下线实现细节 在com.netflix.eureka.resources.InstanceResource中的280行中的cancelLease()方法 123456789101112131415@DELETEpublic Response cancelLease( @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) { //调用cancel boolean isSuccess = registry.cancel(app.getName(), id, \"true\".equals(isReplication)); if (isSuccess) { logger.debug(\"Found (Cancel): \" + app.getName() + \" - \" + id); return Response.ok().build(); } else { logger.info(\"Not Found (Cancel): \" + app.getName() + \" - \" + id); return Response.status(Status.NOT_FOUND).build(); }} 在org.springframework.cloud.netflix.eureka.server.InstanceRegistry中的95行的cancel()方法, 123456@Overridepublic boolean cancel(String appName, String serverId, boolean isReplication) { handleCancelation(appName, serverId, isReplication); //调用父类中的cancel return super.cancel(appName, serverId, isReplication);} 在com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl中的376行 123456789101112131415161718 @Override public boolean cancel(final String appName, final String id, final boolean isReplication) { if (super.cancel(appName, id, isReplication)) { //服务下线成功后,同步更新信息到其它Eureka Server节点 replicateToPeers(Action.Cancel, appName, id, null, null, isReplication); synchronized (lock) { if (this.expectedNumberOfRenewsPerMin > 0) { // Since the client wants to cancel it, reduce the threshold (1 for 30 seconds, 2 for a minute) this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin - 2; this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold()); } } return true; } return false;} 4.在com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl中的618行,主要接口实现方式和register基本一致:首先更新自身Eureka Server中服务的状态,再同步到其它Eureka Server中。12345678910111213141516171819202122232425private void replicateToPeers(Action action, String appName, String id, InstanceInfo info /* optional */, InstanceStatus newStatus /* optional */, boolean isReplication) { Stopwatch tracer = action.getTimer().start(); try { if (isReplication) { numberOfReplicationsLastMin.increment(); } // If it is a replication already, do not replicate again as this will create a poison replication if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) { return; } // 同步把服务信息同步到其它的Eureka Server中 for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) { // If the url represents this host, do not replicate to yourself. if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) { continue; } //根据action做相应操作的同步 replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node); } } finally { tracer.stop(); } } 至此,Eureka服务续约源码分析结束,大家有兴趣可自行阅读。 源码分析链接 其它源码分析链接: Spring Cloud中@EnableEurekaClient源码分析: http://blog.xujin.org/sc/sc-enableEurekaClient-annonation/ Spring Cloud Eureka服务注册源码分析: http://blog.xujin.org/sc/sc-eureka-register/ Spring Cloud Eureka服务续约(Renew)源码分析 http://blog.xujin.org/sc/sc-eureka-renew/","categories":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Eureka/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"},{"name":"Spring Cloud 源码分析","slug":"Spring-Cloud-源码分析","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-源码分析/"}]},{"title":"Spring Cloud Eureka服务续约(Renew)源码分析","slug":"sc/sc-eureka-renew","date":"2016-11-13T06:00:00.000Z","updated":"2017-06-17T03:26:16.000Z","comments":true,"path":"sc/sc-eureka-renew/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-eureka-renew/","excerpt":"Renew(服务续约)概述Renew(服务续约)操作由Service Provider定期调用,类似于heartbeat。目的是隔一段时间Service Provider调用接口,告诉Eureka Server它还活着没挂,不要把它T了。通俗的说就是它们两之间的心跳检测,避免服务提供者被剔除掉。请参考:Spring Cloud Eureka名词解释","text":"Renew(服务续约)概述Renew(服务续约)操作由Service Provider定期调用,类似于heartbeat。目的是隔一段时间Service Provider调用接口,告诉Eureka Server它还活着没挂,不要把它T了。通俗的说就是它们两之间的心跳检测,避免服务提供者被剔除掉。请参考:Spring Cloud Eureka名词解释 服务续约配置 Renew操作会在Service Provider定时发起,用来通知Eureka Server自己还活着。 这里有两个比较重要的配置需要如下,可以在Run之前配置。1eureka.instance.leaseRenewalIntervalInSeconds Renew频率。默认是30秒,也就是每30秒会向Eureka Server发起Renew操作。1eureka.instance.leaseExpirationDurationInSeconds 服务失效时间。默认是90秒,也就是如果Eureka Server在90秒内没有接收到来自Service Provider的Renew操作,就会把Service Provider剔除。 Renew源码分析服务提供者实现细节 服务提供者发发起服务续约的时序图,如下图所示,大家先直观的看一下时序图,等阅读完源码再回顾一下。 在com.netflix.discovery.DiscoveryClient.initScheduledTasks()中的1272行,TimedSupervisorTask会定时发起服务续约,代码如下所示:123456789101112// Heartbeat timer scheduler.schedule( new TimedSupervisorTask( \"heartbeat\", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread() ), renewalIntervalInSecs, TimeUnit.SECONDS); 2.在com.netflix.discovery.DiscoveryClient中的1393行,有一个HeartbeatThread线程发起续约操作123456789 private class HeartbeatThread implements Runnable { public void run() { //调用eureka-client中的renew if (renew()) { lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis(); } }} renew()调用eureka-client-1.4.11.jarcom.netflix.discovery.DiscoveryClient中829行renew()发起PUT Reset请求,调用com.netflix.eureka.resources.InstanceResource中的renewLease()续约。12345678910111213141516171819/** * Renew with the eureka service by making the appropriate REST call */ boolean renew() { EurekaHttpResponse<InstanceInfo> httpResponse; try { httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null); logger.debug(\"{} - Heartbeat status: {}\", PREFIX + appPathIdentifier, httpResponse.getStatusCode()); if (httpResponse.getStatusCode() == 404) { REREGISTER_COUNTER.increment(); logger.info(\"{} - Re-registering apps/{}\", PREFIX + appPathIdentifier, instanceInfo.getAppName()); return register(); } return httpResponse.getStatusCode() == 200; } catch (Throwable e) { logger.error(\"{} - was unable to send heartbeat!\", PREFIX + appPathIdentifier, e); return false; } } Netflix中的Eureka Core实现细节 NetFlix中Eureka Core中的服务续约时序图,如下图所示。 打开com.netflix.eureka.resources.InstanceResource中的106行的renewLease()方法,代码如下: 123456789101112private final PeerAwareInstanceRegistry registry@PUTpublic Response renewLease( @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication, @QueryParam(\"overriddenstatus\") String overriddenStatus, @QueryParam(\"status\") String status, @QueryParam(\"lastDirtyTimestamp\") String lastDirtyTimestamp) { boolean isFromReplicaNode = \"true\".equals(isReplication); //调用 boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode); //其余省略} 点开registry.renew(app.getName(), id, isFromReplicaNode);我们可以看到,调用了org.springframework.cloud.netflix.eureka.server.InstanceRegistry中的renew()方法,代码如下: 1234567891011121314151617181920212223 @Override public boolean renew(final String appName, final String serverId, boolean isReplication) { log(\"renew \" + appName + \" serverId \" + serverId + \", isReplication {}\" + isReplication); List<Application> applications = getSortedApplications(); for (Application input : applications) { if (input.getName().equals(appName)) { InstanceInfo instance = null; for (InstanceInfo info : input.getInstances()) { if (info.getHostName().equals(serverId)) { instance = info; break; } } publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId, instance, isReplication)); break; } } //调用com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl中的renew方法 return super.renew(appName, serverId, isReplication);} 3.从super.renew()看到调用了父类中的com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl中420行的renew()方法,代码如下:123456789 public boolean renew(final String appName, final String id, final boolean isReplication) { //服务续约成功, if (super.renew(appName, id, isReplication)) { //然后replicateToPeers同步其它Eureka Server中的数据 replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication); return true; } return false;} 3.1 从上面代码中super.renew(appName, id, isReplication)可以看出调用的是com.netflix.eureka.registry.AbstractInstanceRegistry中345行的renew()方法,代码如下所示12345678910111213141516171819202122232425262728293031323334353637383940public boolean renew(String appName, String id, boolean isReplication) { RENEW.increment(isReplication); Map<String, Lease<InstanceInfo>> gMap = registry.get(appName); Lease<InstanceInfo> leaseToRenew = null; if (gMap != null) { leaseToRenew = gMap.get(id); } if (leaseToRenew == null) { RENEW_NOT_FOUND.increment(isReplication); logger.warn(\"DS: Registry: lease doesn't exist, registering resource: {} - {}\", appName, id); return false; } else { InstanceInfo instanceInfo = leaseToRenew.getHolder(); if (instanceInfo != null) { // touchASGCache(instanceInfo.getASGName()); InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus( instanceInfo, leaseToRenew, isReplication); if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) { logger.info(\"Instance status UNKNOWN possibly due to deleted override for instance {}\" + \"; re-register required\", instanceInfo.getId()); RENEW_NOT_FOUND.increment(isReplication); return false; } if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) { Object[] args = { instanceInfo.getStatus().name(), instanceInfo.getOverriddenStatus().name(), instanceInfo.getId() }; logger.info( \"The instance status {} is different from overridden instance status {} for instance {}. \" + \"Hence setting the status to overridden status\", args); instanceInfo.setStatus(overriddenInstanceStatus); } } renewsLastMin.increment(); leaseToRenew.renew(); return true; } } 其中 leaseToRenew.renew()是调用com.netflix.eureka.lease.Lease中的62行的renew()方法123456789/** * Renew the lease, use renewal duration if it was specified by the * associated {@link T} during registration, otherwise default duration is * {@link #DEFAULT_DURATION_IN_SECS}. */public void renew() { lastUpdateTimestamp = System.currentTimeMillis() + duration;} 3.2 replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);调用自身的replicateToPeers()方法,在com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl中的618行,主要接口实现方式和register基本一致:首先更新自身Eureka Server中服务的状态,再同步到其它Eureka Server中。12345678910111213141516171819202122232425private void replicateToPeers(Action action, String appName, String id, InstanceInfo info /* optional */, InstanceStatus newStatus /* optional */, boolean isReplication) { Stopwatch tracer = action.getTimer().start(); try { if (isReplication) { numberOfReplicationsLastMin.increment(); } // If it is a replication already, do not replicate again as this will create a poison replication if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) { return; } // 同步把续约信息同步到其它的Eureka Server中 for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) { // If the url represents this host, do not replicate to yourself. if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) { continue; } //根据action做相应操作的同步 replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node); } } finally { tracer.stop(); } } 至此,Eureka服务续约源码分析结束,大家有兴趣可自行阅读。 源码分析链接 其它源码分析链接: Spring Cloud中@EnableEurekaClient源码分析: http://blog.xujin.org/sc/sc-enableEurekaClient-annonation/ Spring Cloud Eureka服务注册源码分析: http://blog.xujin.org/sc/sc-eureka-register/","categories":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Eureka/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"},{"name":"Spring Cloud 源码分析","slug":"Spring-Cloud-源码分析","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-源码分析/"}]},{"title":"Spring Cloud中@EnableEurekaClient源码分析","slug":"sc/sc-enableEurekaClient-annonation","date":"2016-11-06T06:00:00.000Z","updated":"2017-06-17T03:02:45.000Z","comments":true,"path":"sc/sc-enableEurekaClient-annonation/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-enableEurekaClient-annonation/","excerpt":"NetFlix Eureka client简介NetFlix Eureka clientEureka client 负责与Eureka Server 配合向外提供注册与发现服务接口。首先看下eureka client是怎么定义,Netflix的 eureka client的行为在LookupService中定义,Lookup service for finding active instances,定义了,从outline中能看到起“规定”了如下几个最基本的方法。服务发现必须实现的基本类:com.netflix.discovery.shared.LookupService,可以自行查看源码。 Eureka client与Spring Cloud类关系 Eureka client与Spring Cloud Eureka Client类图,如下所示:在上图中,我加了前缀,带有S的是Spring Cloud封装的,带有N是NetFlix原生的。","text":"NetFlix Eureka client简介NetFlix Eureka clientEureka client 负责与Eureka Server 配合向外提供注册与发现服务接口。首先看下eureka client是怎么定义,Netflix的 eureka client的行为在LookupService中定义,Lookup service for finding active instances,定义了,从outline中能看到起“规定”了如下几个最基本的方法。服务发现必须实现的基本类:com.netflix.discovery.shared.LookupService,可以自行查看源码。 Eureka client与Spring Cloud类关系 Eureka client与Spring Cloud Eureka Client类图,如下所示:在上图中,我加了前缀,带有S的是Spring Cloud封装的,带有N是NetFlix原生的。 org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient中49行的eurekaClient就是com.netflix.discovery.EurekaClient,代码如下所示:12345678@RequiredArgsConstructorpublic class EurekaDiscoveryClient implements DiscoveryClient { public static final String DESCRIPTION = \"Spring Cloud Eureka Discovery Client\"; private final EurekaInstanceConfig config; // Netflix中的Eureka Client private final EurekaClient eurekaClient; //其余省略} Tips:org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient实现了DiscoveryClient,并依赖于com.netflix.discovery.EurekaClient 点开com.netflix.discovery.EurekaClient查看代码,可以看出EurekaClient继承了LookupService并实现了EurekaClient接口。 1234@ImplementedBy(DiscoveryClient.class)public interface EurekaClient extends LookupService { //其余省略} com.netflix.discovery.DiscoveryClient是netflix使用的客户端,从其class的注释可以看到他主要做这几件事情:a) Registering the instance with Eureka Serverb) Renewalof the lease with Eureka Serverc) Cancellation of the lease from Eureka Server during shutdown 其中com.netflix.discovery.DiscoveryClient实现了com.netflix.discovery.EurekaClient,而spring Cloud中的org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient,依赖于com.netflix.discovery.EurekaClient,因此Spring Cloud与NetFlix的关系由此联系到一起。12345678@Singletonpublic class DiscoveryClient implements EurekaClient { private static final Logger logger = LoggerFactory.getLogger(DiscoveryClient.class); // Constants public static final String HTTP_X_DISCOVERY_ALLOW_REDIRECT = \"X-Discovery-AllowRedirect\"; //其余省略} @EnableEurekaClient注解入口分析 在上面小节中,理清了NetFlix Eureka与Spring cloud中类的依赖关系,下面将以@EnableEurekaClient为入口,分析主要调用链中的类和方法。 @EnableEurekaClient使用 用过spring cloud的同学都知道,使用@EnableEurekaClient就能简单的开启Eureka Client中的功能,如下代码所示。123456789@EnableEurekaClient@SpringBootApplicationpublic class CloudEurekaClientApplication { public static void main(String[] args) { new SpringApplicationBuilder(CloudEurekaClientApplication.class).web(true).run(args); }} 通过@EnableEurekaClient这个简单的注解,在spring cloud应用启动的时候,就可以把EurekaDiscoveryClient注入,继而使用NetFlix提供的Eureka client。 打开EnableEurekaClient这个类,可以看到这个自定义的annotation @EnableEurekaClient里面没有内容。它的作用就是开启Eureka discovery的配置,正是通过这个标记,autoconfiguration就可以加载相关的Eureka类。那我们看下它是怎么做到的。 12345678@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@EnableDiscoveryClientpublic @interface EnableEurekaClient {} 在上述代码中,我们看到,EnableEurekaClient上面加入了另外一个注解@EnableDiscoveryClient,看看这个注解的代码如下所示: 123456789101112/** * Annotation to enable a DiscoveryClient implementation. * @author Spencer Gibb */@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@Import(EnableDiscoveryClientImportSelector.class)public @interface EnableDiscoveryClient {} 这个注解import了EnableDiscoveryClientImportSelector.class这样一个类,其实就是通过这个类来加载需要用到的bean。点开EnableDiscoveryClientImportSelector类,如下代码: 12345678910111213141516@Order(Ordered.LOWEST_PRECEDENCE - 100)public class EnableDiscoveryClientImportSelector extends SpringFactoryImportSelector<EnableDiscoveryClient> { @Override protected boolean isEnabled() { return new RelaxedPropertyResolver(getEnvironment()).getProperty( \"spring.cloud.discovery.enabled\", Boolean.class, Boolean.TRUE); } @Override protected boolean hasDefaultFactory() { return true; }} 看到这里有覆盖了父类SpringFactoryImportSelector的一个方法isEnabled,注意,默认是TRUE,也就是只要import了这个配置,就会enable。 在其父类org.springframework.cloud.commons.util.SpringFactoryImportSelector的String[] selectImports(AnnotationMetadata metadata)方法中正是根据这个标记类判定是否加载如下定义的类。在源码第59行,局部代码如下所示。1234567891011121314151617181920212223242526272829@Override public String[] selectImports(AnnotationMetadata metadata) { if (!isEnabled()) { return new String[0]; } AnnotationAttributes attributes = AnnotationAttributes.fromMap( metadata.getAnnotationAttributes(this.annotationClass.getName(), true)); Assert.notNull(attributes, \"No \" + getSimpleName() + \" attributes found. Is \" + metadata.getClassName() + \" annotated with @\" + getSimpleName() + \"?\"); // Find all possible auto configuration classes, filtering duplicates List<String> factories = new ArrayList<>(new LinkedHashSet<>(SpringFactoriesLoader .loadFactoryNames(this.annotationClass, this.beanClassLoader))); if (factories.isEmpty() && !hasDefaultFactory()) { throw new IllegalStateException(\"Annotation @\" + getSimpleName() + \" found, but there are no implementations. Did you forget to include a starter?\"); } if (factories.size() > 1) { // there should only ever be one DiscoveryClient, but there might be more than // one factory log.warn(\"More than one implementation \" + \"of @\" + getSimpleName() + \" (now relying on @Conditionals to pick one): \" + factories); } return factories.toArray(new String[factories.size()]); } 在源码中70-71行,即在org.springframework.core.io.support.SpringFactoriesLoader 中的109行的loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader)方法12345678910111213141516171819 public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); try { Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List<String> result = new ArrayList<String>(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); String factoryClassNames = properties.getProperty(factoryClassName); result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); } return result; } catch (IOException ex) { throw new IllegalArgumentException(\"Unable to load [\" + factoryClass.getName() + \"] factories from location [\" + FACTORIES_RESOURCE_LOCATION + \"]\", ex); }} 实际调用loadFactoryNames其实加载META-INF/spring.factories下的class。12345 /*** The location to look for factories.* <p>Can be present in multiple JAR files. */ public static final String FACTORIES_RESOURCE_LOCATION = \"META-INF/spring.factories\"; 而在spring-cloud-netflix-eureka-client\\src\\main\\resources\\META-INF\\spring.factories中配置,用于加载一系列配置信息和Dependences Bean可以看到EnableAutoConfiguration的包含了EurekaClientConfigServerAutoConfiguration。1234567891011org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\\org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\\org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\\org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfigurationorg.springframework.cloud.bootstrap.BootstrapConfiguration=\\org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfigurationorg.springframework.cloud.client.discovery.EnableDiscoveryClient=\\org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration 打开org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration可以看到EurekaClientAutoConfiguration具体的注入信息。 具体@EnableEurekaClien注解开启之后,服务启动后,是服务怎么注册的请参考,下面链接:http://blog.xujin.org/sc/sc-eureka-register/ 其它源码分析链接 Spring Cloud中@EnableEurekaClient源码分析: http://blog.xujin.org/sc/sc-enableEurekaClient-annonation/ Spring Cloud Eureka服务注册源码分析: http://blog.xujin.org/sc/sc-eureka-register/ Spring Cloud Eureka服务续约(Renew)源码分析 http://blog.xujin.org/sc/sc-eureka-renew/ Spring Cloud Eureka服务下线(Cancel)源码分析 http://blog.xujin.org/sc/sc-eureka-cancle/","categories":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Eureka/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"},{"name":"Spring Cloud 源码分析","slug":"Spring-Cloud-源码分析","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-源码分析/"}]},{"title":"Spring Cloud Eureka服务注册源码分析","slug":"sc/sc-eureka-register","date":"2016-11-01T06:00:00.000Z","updated":"2017-06-17T03:19:51.000Z","comments":true,"path":"sc/sc-eureka-register/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-eureka-register/","excerpt":"摘要:在上一篇中,介绍了Eureka的相关的知识,解释了Eureka为什么适合做服务发现和注册。接下来,在本篇文章将通过源码分析的方式,看一下Eureka是怎么work的。本章主要介绍Eureka的服务注册。那eureka client如何将本地服务的注册信息发送到远端的注册服务器eureka server上。通过下面的源码分析,看出Eureka Client的定时任务调用Eureka Server的Reset接口,而Eureka接收到调用请求后会处理服务的注册以及Eureka Server中的数据同步的问题。 服务注册 服务注册,想必大家并不陌生,就是服务提供者启动的时候,把自己提供的服务信息,例如 服务名,IP,端口号,版本号等信息注册到注册中心,比如注册到ZK中。那eureka client如何将本地服务的注册信息发送到远端的注册服务器eureka server上。通过下面的源码分析,看出服务注册可以认为是Eureka client自己完成,不需要服务本身来关心。 Eureka Client的定时任务调用Eureka Server的提供接口实现思路其实也挺简单,在com.netflix.discovery.DiscoveryClient启动的时候,会初始化一个定时任务,定时的把本地的服务配置信息,即需要注册到远端的服务信息自动刷新到注册服务器上。首先看一下Eureka的代码,在spring-cloud-netflix-eureka-server工程中可以找到这个依赖eureka-client-1.4.11.jar查看代码可以看到,com.netflix.discovery.DiscoveryClient.java中的1240行可以看到Initializes all scheduled tasks,在1277行,可以看到InstanceInfoReplicator定时任务。","text":"摘要:在上一篇中,介绍了Eureka的相关的知识,解释了Eureka为什么适合做服务发现和注册。接下来,在本篇文章将通过源码分析的方式,看一下Eureka是怎么work的。本章主要介绍Eureka的服务注册。那eureka client如何将本地服务的注册信息发送到远端的注册服务器eureka server上。通过下面的源码分析,看出Eureka Client的定时任务调用Eureka Server的Reset接口,而Eureka接收到调用请求后会处理服务的注册以及Eureka Server中的数据同步的问题。 服务注册 服务注册,想必大家并不陌生,就是服务提供者启动的时候,把自己提供的服务信息,例如 服务名,IP,端口号,版本号等信息注册到注册中心,比如注册到ZK中。那eureka client如何将本地服务的注册信息发送到远端的注册服务器eureka server上。通过下面的源码分析,看出服务注册可以认为是Eureka client自己完成,不需要服务本身来关心。 Eureka Client的定时任务调用Eureka Server的提供接口实现思路其实也挺简单,在com.netflix.discovery.DiscoveryClient启动的时候,会初始化一个定时任务,定时的把本地的服务配置信息,即需要注册到远端的服务信息自动刷新到注册服务器上。首先看一下Eureka的代码,在spring-cloud-netflix-eureka-server工程中可以找到这个依赖eureka-client-1.4.11.jar查看代码可以看到,com.netflix.discovery.DiscoveryClient.java中的1240行可以看到Initializes all scheduled tasks,在1277行,可以看到InstanceInfoReplicator定时任务。 在DiscoveryClient中初始化一个InstanceInfoReplicator,其实里面封装了以定时任务。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475/** * Initializes all scheduled tasks. */ private void initScheduledTasks() { if (clientConfig.shouldFetchRegistry()) { // registry cache refresh timer int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds(); int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); scheduler.schedule( new TimedSupervisorTask( \"cacheRefresh\", scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread() ), registryFetchIntervalSeconds, TimeUnit.SECONDS); } if (clientConfig.shouldRegisterWithEureka()) { int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info(\"Starting heartbeat executor: \" + \"renew interval is: \" + renewalIntervalInSecs); // Heartbeat timer scheduler.schedule( new TimedSupervisorTask( \"heartbeat\", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread() ), renewalIntervalInSecs, TimeUnit.SECONDS); // InstanceInfo replicator /**************************封装了定时任务**********************************/ instanceInfoReplicator = new InstanceInfoReplicator( this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2); // burstSize statusChangeListener = new ApplicationInfoManager.StatusChangeListener() { @Override public String getId() { return \"statusChangeListener\"; } @Override public void notify(StatusChangeEvent statusChangeEvent) { if (InstanceStatus.DOWN == statusChangeEvent.getStatus() || InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) { // log at warn level if DOWN was involved logger.warn(\"Saw local status change event {}\", statusChangeEvent); } else { logger.info(\"Saw local status change event {}\", statusChangeEvent); } instanceInfoReplicator.onDemandUpdate(); } }; if (clientConfig.shouldOnDemandUpdateStatusChange()) { applicationInfoManager.registerStatusChangeListener(statusChangeListener); } //点击可以查看start方法 instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()); } else { logger.info(\"Not registering with Eureka server per configuration\"); } } 2.以initialDelayMs为间隔调用。1234567public void start(int initialDelayMs) { if (started.compareAndSet(false, true)) { instanceInfo.setIsDirty(); // for initial register Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); }} 3.ScheduledExecutorService的task的具体业务定义在com.netflix.discovery.InstanceInfoReplicator.run()中,也就是InstanceInfoReplicator中的98-113行,可以看到调用了了client的register方法。1234567891011121314151617 public void run() { try { discoveryClient.refreshInstanceInfo(); Long dirtyTimestamp = instanceInfo.isDirtyWithTime(); if (dirtyTimestamp != null) { //客户端发送hhtp注册请求的真正入口 discoveryClient.register(); instanceInfo.unsetIsDirty(dirtyTimestamp); } } catch (Throwable t) { logger.warn(\"There was a problem with the instance info replicator\", t); } finally { Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); }} 4.com.netflix.discovery.DiscoveryClient中的 register()方法,大概在811行。123456789101112131415161718/** * Register with the eureka service by making the appropriate REST call. */boolean register() throws Throwable { logger.info(PREFIX + appPathIdentifier + \": registering service...\"); EurekaHttpResponse<Void> httpResponse; try { //Eureka Client客户端,调用Eureka服务端的入口 httpResponse = eurekaTransport.registrationClient.register(instanceInfo); } catch (Exception e) { logger.warn(\"{} - registration failed {}\", PREFIX + appPathIdentifier, e.getMessage(), e); throw e; } if (logger.isInfoEnabled()) { logger.info(\"{} - registration status: {}\", PREFIX + appPathIdentifier, httpResponse.getStatusCode()); } return httpResponse.getStatusCode() == 204;} Eureka server端接到请求后的处理打开spring-cloud-netflix-eureka-server工程或spring-cloud-netflix-eureka-client过程,找到相应的maven依赖jar,如下图所示 1.Eureka server服务端请求入口ApplicationResource.java文件中第183行,如下所示,可以看出Eureka是通过http post的方式去服务注册1234567891011121314151617181920212223242526272829303132333435363738394041424344@POST @Consumes({\"application/json\", \"application/xml\"}) public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) { logger.debug(\"Registering instance {} (replication={})\", info.getId(), isReplication); // validate that the instanceinfo contains all the necessary required fields if (isBlank(info.getId())) { return Response.status(400).entity(\"Missing instanceId\").build(); } else if (isBlank(info.getHostName())) { return Response.status(400).entity(\"Missing hostname\").build(); } else if (isBlank(info.getAppName())) { return Response.status(400).entity(\"Missing appName\").build(); } else if (!appName.equals(info.getAppName())) { return Response.status(400).entity(\"Mismatched appName, expecting \" + appName + \" but was \" + info.getAppName()).build(); } else if (info.getDataCenterInfo() == null) { return Response.status(400).entity(\"Missing dataCenterInfo\").build(); } else if (info.getDataCenterInfo().getName() == null) { return Response.status(400).entity(\"Missing dataCenterInfo Name\").build(); } // handle cases where clients may be registering with bad DataCenterInfo with missing data DataCenterInfo dataCenterInfo = info.getDataCenterInfo(); if (dataCenterInfo instanceof UniqueIdentifier) { String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId(); if (isBlank(dataCenterInfoId)) { boolean experimental = \"true\".equalsIgnoreCase(serverConfig.getExperimental(\"registration.validation.dataCenterInfoId\")); if (experimental) { String entity = \"DataCenterInfo of type \" + dataCenterInfo.getClass() + \" must contain a valid id\"; return Response.status(400).entity(entity).build(); } else if (dataCenterInfo instanceof AmazonInfo) { AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo; String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId); if (effectiveId == null) { amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId()); } } else { logger.warn(\"Registering DataCenterInfo of type {} without an appropriate id\", dataCenterInfo.getClass()); } } } //InstanceRegistry.java文件中的88行的405行register方法 registry.register(info, \"true\".equals(isReplication)); return Response.status(204).build(); // 204 to be backwards compatible } 2.如下图所示可以看到,从ApplicationResource.java怎么进入到PeerAwareInstanceRegistryImpl中的register方法InstanceRegistry.java文件中的88行,可以看到调用PeerAwareInstanceRegistryImpl中的405行register方法123456@Overridepublic void register(final InstanceInfo info, final boolean isReplication) { handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication); //调用PeerAwareInstanceRegistryImpl中的405行register方法 super.register(info, isReplication); } 3.PeerAwareInstanceRegistryImpl中的405行register方法,代码如下所示。阅读方法上面的注释,就知道该方法是注册服务信息并把Eureka Server中的配置信息同步。执行注册的动作在com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.register(InstanceInfo info, boolean isReplication)中,具体代码如下所示: 12345678910111213141516171819202122/** * Registers the information about the {@link InstanceInfo} and replicates * this information to all peer eureka nodes. If this is replication event * from other replica nodes then it is not replicated. * * @param info * the {@link InstanceInfo} to be registered and replicated. * @param isReplication * true if this is a replication event from other replica nodes, * false otherwise. */ @Override public void register(final InstanceInfo info, final boolean isReplication) { int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS; if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) { leaseDuration = info.getLeaseInfo().getDurationInSecs(); } //调用父类方法注册 super.register(info, leaseDuration, isReplication); // 同步Eureka中的服务信息 replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication); } 4.AbstractInstanceRegistry.java中192行,可以看到Eureka真正的服务注册实现的代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586/** * Registers a new instance with a given duration. * * @see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object, int, boolean) */ public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) { try { read.lock(); Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName()); REGISTER.increment(isReplication); if (gMap == null) { final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>(); gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap); if (gMap == null) { gMap = gNewMap; } } Lease<InstanceInfo> existingLease = gMap.get(registrant.getId()); // Retain the last dirty timestamp without overwriting it, if there is already a lease if (existingLease != null && (existingLease.getHolder() != null)) { Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp(); Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp(); logger.debug(\"Existing lease found (existing={}, provided={}\", existingLastDirtyTimestamp, registrationLastDirtyTimestamp); if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) { logger.warn(\"There is an existing lease and the existing lease's dirty timestamp {} is greater\" + \" than the one that is being registered {}\", existingLastDirtyTimestamp, registrationLastDirtyTimestamp); logger.warn(\"Using the existing instanceInfo instead of the new instanceInfo as the registrant\"); registrant = existingLease.getHolder(); } } else { // The lease does not exist and hence it is a new registration synchronized (lock) { if (this.expectedNumberOfRenewsPerMin > 0) { // Since the client wants to cancel it, reduce the threshold // (1 // for 30 seconds, 2 for a minute) this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2; this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold()); } } logger.debug(\"No previous lease information found; it is new registration\"); } Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration); if (existingLease != null) { lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp()); } gMap.put(registrant.getId(), lease); synchronized (recentRegisteredQueue) { recentRegisteredQueue.add(new Pair<Long, String>( System.currentTimeMillis(), registrant.getAppName() + \"(\" + registrant.getId() + \")\")); } // This is where the initial state transfer of overridden status happens if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) { logger.debug(\"Found overridden status {} for instance {}. Checking to see if needs to be add to the \" + \"overrides\", registrant.getOverriddenStatus(), registrant.getId()); if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) { logger.info(\"Not found overridden id {} and hence adding it\", registrant.getId()); overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus()); } } InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId()); if (overriddenStatusFromMap != null) { logger.info(\"Storing overridden status {} from map\", overriddenStatusFromMap); registrant.setOverriddenStatus(overriddenStatusFromMap); } // Set the status based on the overridden status rules InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication); registrant.setStatusWithoutDirty(overriddenInstanceStatus); // If the lease is registered with UP status, set lease service up timestamp if (InstanceStatus.UP.equals(registrant.getStatus())) { lease.serviceUp(); } registrant.setActionType(ActionType.ADDED); recentlyChangedQueue.add(new RecentlyChangedItem(lease)); registrant.setLastUpdatedTimestamp(); invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress()); logger.info(\"Registered instance {}/{} with status {} (replication={})\", registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication); } finally { read.unlock(); } } 说明:注册信息其实就是存储在一个 ConcurrentHashMap","categories":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Eureka/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"},{"name":"Spring Cloud 源码分析","slug":"Spring-Cloud-源码分析","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-源码分析/"}]},{"title":"Spring Cloud Netflix之Eureka下篇原理","slug":"sc/sc-eureka-mid","date":"2016-10-25T06:00:00.000Z","updated":"2017-06-17T03:26:44.000Z","comments":true,"path":"sc/sc-eureka-mid/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-eureka-mid/","excerpt":"","text":"概述名词解释 Renew:我的理解是续约,为什么叫续约呢?Renew(服务续约)操作由Service Provider定期调用,类似于heartbeat。目的是隔一段时间Service Provider调用接口,告诉Eureka Server它还活着没挂,不要把它踢掉。通俗的说就是它们两之间的心跳检测,避免服务提供者被剔除掉。 Cancel(服务下线)一般在Service Provider挂了或shut down的时候调用,用来把自身的服务从Eureka Server中删除,以防客户端调用到不存在的服务。 Fetch Registries(获取注册信息),Fetch Registries由Service Consumer(服务消费者)调用,用来获取Eureka Server上注册的服务info。 Eviction(剔除)Eviction(失效服务剔除)用来定期在Eureka Server检测失效的服务,检测标准就是超过一定时间没有Renew的服务。回顾Eureka架构图Eureka架构图Eureka架构图如下图所示,github地址:https://github.com/netflix/eurekadocument地址:https://github.com/Netflix/eureka/wiki/Eureka-at-a-glance  从图中我们可以看出,Eureka 组件分为两部分:Eureka server和 Eureka client。而客户端又分为 Application Service 客户端和 Application Client 客户端两种。Eureka 的工作机制每个 region 都有自己的 Eureka 服务器集群,每个 zone 至少要有一个 Eureka 服务器以应对 zone 瘫痪。  Application Service 在启动时注册到 Eureka 服务器,之后每 30 秒钟发送心跳以更新自身状态,即Renew(续约)。如果该客户端没能发送心跳更新,它将在 90 秒之后被其注册的 Eureka 服务器剔除,即Eviction(剔除)。来自任意 zone 的 Application Client 可以获取这些注册信息(每隔 30 秒查看一次)并依此定位到在任何区域可以给自己提供服务的提供者(即Fetch Registries),进而进行远程调用。 服务提供者本身携带的Eureka Client既能服务注册,服务续约,也能通过client定位服务和调用其它的服务。 服务注册服务注册 服务注册源码分析,请参考:http://blog.xujin.org/sc/sc-eureka-register/ Renew(服务续约)服务续约 Renew操作会在Service Provider端定期发起,用来通知Eureka Server自己还活着。 这里有两个比较重要的配置需要注意一下:1eureka.instance.leaseRenewalIntervalInSeconds Renew频率。默认是30秒,也就是每30秒会向Eureka Server发起Renew操作。1eureka.instance.leaseExpirationDurationInSeconds 服务失效时间。默认是90秒,也就是如果Eureka Server在90秒内没有接收到来自Service Provider的Renew操作,就会把Service Provider剔除。","categories":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Eureka/"}],"tags":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Eureka/"}]},{"title":"Spring Cloud Netflix之Eureka上篇","slug":"sc/sc-netflix-eureka","date":"2016-10-23T06:00:00.000Z","updated":"2017-06-17T03:25:26.000Z","comments":true,"path":"sc/sc-netflix-eureka/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-netflix-eureka/","excerpt":"前言:Spring Cloud NetFlix这个项目对NetFlix中一些久经考验靠谱的服务发现,熔断,网关,智能路由,以及负载均衡等做了封装,并通过注解的或简单配置的方式提供给Spring Cloud用户用。本文主要介绍 Spring Cloud中的Eureka组件。由于Spring Cloud做技术选型时中立的,因此Spring Cloud也提供了Spring Cloud Zookeeper,Spring Cloud Consul用于服务治理或服务发现供大家选择使用,另外我还发现Spring Cloud etcd这个项目,也可以用于服务注册和发现","text":"前言:Spring Cloud NetFlix这个项目对NetFlix中一些久经考验靠谱的服务发现,熔断,网关,智能路由,以及负载均衡等做了封装,并通过注解的或简单配置的方式提供给Spring Cloud用户用。本文主要介绍 Spring Cloud中的Eureka组件。由于Spring Cloud做技术选型时中立的,因此Spring Cloud也提供了Spring Cloud Zookeeper,Spring Cloud Consul用于服务治理或服务发现供大家选择使用,另外我还发现Spring Cloud etcd这个项目,也可以用于服务注册和发现 什么是 Spring Cloud Netflix ?其官方文档中对自己的定义是如下,官网连接,Github地址 This project provides Netflix OSS integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment and other Spring programming model idioms. With a few simple annotations you can quickly enable and configure the common patterns inside your application and build large distributed systems with battle-tested Netflix components. The patterns provided include Service Discovery (Eureka), Circuit Breaker (Hystrix), Intelligent Routing (Zuul) and Client Side Load Balancing (Ribbon). Spring Cloud Netflix这个项目对于Spring Boot应用来说,它集成了NetFlix OSS的一些组件,只需通过注解配置和Spring环境的通用简单的使用注解,你可以快速的启用和配置这些久经测试考验的NetFlix的组件于你的应用和用于构建分布式系统中。这些组件包含的功能有服务发现(Eureka),熔断器(Hystrix),智能路由(Zuul)以及客户端的负载均衡器(Ribbon) 简单的来说,Spring Cloud NetFlix这个项目对NetFlix中一些久经考验靠谱的服务发现,熔断,网关,智能路由,以及负载均衡等做了封装,并通过注解的或简单配置的方式提供给Spring Cloud用户用。 什么是 Eureka?官网定义是: Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers. We call this service, the Eureka Server. Eureka also comes with a Java-based client component,the Eureka Client, which makes interactions with the service much easier. The client also has a built-in load balancer that does basic round-robin load balancing. 简单来说Eureka就是Netflix开源的一款提供服务注册和发现的产品,并且提供了相应的Java客户端。 为什么要选择 Eureka?那么为什么我们在项目中使用了Eureka呢?主要原因如下: 它提供了完整的Service Registry和Service Discovery实现 首先是提供了完整的实现,并且也经受住了Netflix的生产环境考验,使用比较方便只需通过注解或简单配置的方式即可。 和Spring Cloud无缝集成 Spring Cloud对Eureka做了无缝集成,提供了一套完善的解决方案,所以使用起来非常方便。 另外,Eureka支持嵌入到应用自身的容器中启动,应用启动完之后,既充当了Eureka的角色,同时也是服务的提供者。这样就极大的提高了服务的可用性。 开源 开源代码,方便学习掌握其源码并驾驭它。 参考阅读:为什么不应该使用ZooKeeper做服务发现英文链接:Eureka! Why You Shouldn’t Use ZooKeeper for Service Discovery:http://www.knewton.com/tech/blog/2014/12/eureka-shouldnt-use-zookeeper-service-discovery/中文链接:http://blog.csdn.net/jenny8080/article/details/52448403Eureka vs. Zookeeper:https://groups.google.com/forum/#%21topic/eureka_netflix/LXKWoD14RFY 进一步了解 EurekaEureka基本架构图 上图简要描述了Eureka的基本架构,由3个角色组成: Eureka Server 提供服务注册和发现 Service Provider 服务提供者,服务启动的时候会将自己的服务信息注册到Eureka Service Consumer 服务消费者,从Eureka中获取已注的服务信息,用于调用服务生产者 需要注意一点是:一个Service Provider既可以是Service Consumer,也可以是Service Provider。 集群模式下的Eureka 上图更进一步的展示了3个角色之间的交互。 Service Provider会向Eureka Server做Register(服务注册)、Renew(服务续约)、Cancel(服务下线)等操作。 Eureka Server之间会做注册服务的同步,从而保证状态一致 Service Consumer会向Eureka Server获取注册服务列表,并消费服务","categories":[{"name":"Spring Cloud Eureka","slug":"Spring-Cloud-Eureka","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Eureka/"}],"tags":[{"name":"Spring Cloud Netflix","slug":"Spring-Cloud-Netflix","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Netflix/"},{"name":"Eureka","slug":"Eureka","permalink":"http://blog.springcloud.cn/tags/Eureka/"}]},{"title":"Spring Cloud Sleuth-全链路监控调研","slug":"sc/sc-sleuth","date":"2016-10-21T06:00:00.000Z","updated":"2017-06-17T03:24:59.000Z","comments":true,"path":"sc/sc-sleuth/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-sleuth/","excerpt":"前言:做过软件开发的都知道,对系统进行全链路的监控是非常有必要的。在单体应用中,传统的方式是软件开发者,通过自定义日志的level,日志文件的方式记录单体应用的运行日志。从而排查线上系统出现运行过慢,出现故障,异常等问题,但是在微服务架构或分布式系统中,一个系统被拆分成了A、B、C、D、E等多个服务,而每个服务可能又有多个实例组成集群,采用上诉定位问题的方式就行不通了,你充其量就知道某个服务是应用的瓶颈,但中间发生了什么你完全不知道。而且问题的查询,因为有海量各种各样的日志等文件,导致追溯定位问题等极其不方便。因此需要全链路监控系统的收集,上报,对海量日志实时计算生成,监控告警,视图报表,帮助开发人员快速定位问题。 服务追踪分析一个由微服务构成的应用系统由N个服务实例组成,通过REST请求或者RPC协议等来通讯完成一个业务流程的调用。对于入口的一个调用可能需要有多个后台服务协同完成,链路上任何一个调用超时或出错都可能造成前端请求的失败。服务的调用链也会越来越长,并形成一个树形的调用链。如下图所示:","text":"前言:做过软件开发的都知道,对系统进行全链路的监控是非常有必要的。在单体应用中,传统的方式是软件开发者,通过自定义日志的level,日志文件的方式记录单体应用的运行日志。从而排查线上系统出现运行过慢,出现故障,异常等问题,但是在微服务架构或分布式系统中,一个系统被拆分成了A、B、C、D、E等多个服务,而每个服务可能又有多个实例组成集群,采用上诉定位问题的方式就行不通了,你充其量就知道某个服务是应用的瓶颈,但中间发生了什么你完全不知道。而且问题的查询,因为有海量各种各样的日志等文件,导致追溯定位问题等极其不方便。因此需要全链路监控系统的收集,上报,对海量日志实时计算生成,监控告警,视图报表,帮助开发人员快速定位问题。 服务追踪分析一个由微服务构成的应用系统由N个服务实例组成,通过REST请求或者RPC协议等来通讯完成一个业务流程的调用。对于入口的一个调用可能需要有多个后台服务协同完成,链路上任何一个调用超时或出错都可能造成前端请求的失败。服务的调用链也会越来越长,并形成一个树形的调用链。如下图所示:但是随着服务的增多,对调用链的分析也会越来越负责。设想你在负责下面这个系统,其中每个小点都是一个微服务,他们之间的调用关系形成了复杂的网络。如下图所示: 通过该图,可以看出错综复杂的调用网路图。针对服务化应用全链路追踪的问题,Google发表了Dapper论文,介绍了他们如何进行服务追踪分析。其基本思路是在服务调用的请求和响应中加入ID,标明上下游请求的关系。利用这些信息,可以可视化地分析服务调用链路和服务间的依赖关系。 什么是 Spring Cloud Sleuth ?Spring Cloud Sleuth为Spring Cloud提供了分布式追踪方案,为了更好的理解这个领域中的一些概念,建议先自行搜索学习一下Google Dapper相关的论文,http://research.google.com/pubs/pub36356.html,github Code连接:Spring Cloud Sleuth Code。官方文档地址:http://cloud.spring.io/spring-cloud-sleuth/spring-cloud-sleuth.html. 其官方文档中对自己的定义是如下: Spring Cloud Sleuth implements a distributed tracing solution for Spring Cloud, borrowing heavily from Dapper, Zipkin and HTrace. For most users Sleuth should be invisible, and all your interactions with external systems should be instrumented automatically. You can capture data simply in logs, or by sending it to a remote collector service. 简单来说,Spring Cloud Sleuth就是APM(Application Performance Monitor),全链路监控的APM的一部分,如果要完整的使用该组件需要自己定制化或者和开源的系统集成,例如:ZipKin。 APM(Application Performance Monitor)这个领域最近异常火热。国外该领域知名公司包括New Relic,Appdynamics,Splunk。其中New Relic已经成功IPO,估值超过20亿美元。国内外的个大互联网公司也都有类似大名鼎鼎的APM产品,例如淘宝鹰眼Eagle Eyes,点评的CAT,微博的Watchman,twitter的Zipkin。他们的产品虽未像专业APM公司的产品这样功能强大,但结合各自公司的业务特点,这些产品在支撑业务系统的高性能和稳定性方面,发挥了显著的作用。 Spring Cloud Sleuth和Zipkin对应Dpper的开源实现是Zipkin,支持多种语言包括JavaScript,Python,Java, Scala, Ruby, C#, Go等。其中Java由多种不同的库来支持。 SpringCloudSleuth 借用了 Dapper 的术语 Span 基本工作单元,例如,在一个新建的span中发送一个RPC等同于发送一个回应请求给RPC,span通过一个64位ID唯一标识,trace以另一个64位ID表示,span还有其他数据信息,比如摘要、时间戳事件、关键值注释(tags)、span的ID、以及进度ID(通常是IP地址) span在不断的启动和停止,同时记录了时间信息,当你创建了一个span,你必须在未来的某个时刻停止它。 Trace 一系列spans组成的一个树状结构,例如,如果你要在分布式中大数据存储中使用,Trace将会由一个请求执行调用链形成。 Annotation 用来及时记录一个事件的存在,一些核心annotations用来定义一个请求的开始和结束。cs:Client Sent - 客户端发起一个请求,这个annotion描述了这个span的开始 sr:Server Received - 服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳便可得到网络延迟ss:Server Sent - 注解表明请求处理的完成(当请求返回客户端),如果ss减去sr时间戳便可得到服务端需要的处理请求时间 cr:Client Received - 表明span的结束,客户端成功接收到服务端的回复,如果cr减去cs时间戳便可得到客户端从服务端获取回复的所有所需时间将Span和Trace在一个系统中使用Zipkin注解的过程图形化,如下图所示:","categories":[{"name":"Spring Cloud Sleuth","slug":"Spring-Cloud-Sleuth","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud-Sleuth/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"},{"name":"Spring Cloud Sleuth","slug":"Spring-Cloud-Sleuth","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Sleuth/"},{"name":"全链路监控","slug":"全链路监控","permalink":"http://blog.springcloud.cn/tags/全链路监控/"},{"name":"微服务","slug":"微服务","permalink":"http://blog.springcloud.cn/tags/微服务/"}]},{"title":"什么是Spring Cloud Config?","slug":"sc/sc-config","date":"2016-10-19T06:00:00.000Z","updated":"2017-06-17T03:22:24.000Z","comments":true,"path":"sc/sc-config/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-config/","excerpt":"什么是 Spring Cloud Config ?其官方文档中对自己的定义是如下,官网连接:Spring Cloud Config。 Spring Cloud Config provides server and client-side support for externalized configuration in a distributed system.With the Config Server you have a central place to manage external properties for applications across all environments. 简单来说,Spring Cloud Config就是我们通常意义上的配置中心 - 把应用原本放在本地文件的配置抽取出来放在中心服务器,从而能够提供更好的管理、发布能力。","text":"什么是 Spring Cloud Config ?其官方文档中对自己的定义是如下,官网连接:Spring Cloud Config。 Spring Cloud Config provides server and client-side support for externalized configuration in a distributed system.With the Config Server you have a central place to manage external properties for applications across all environments. 简单来说,Spring Cloud Config就是我们通常意义上的配置中心 - 把应用原本放在本地文件的配置抽取出来放在中心服务器,从而能够提供更好的管理、发布能力。 另外,Spring Cloud Config提供基于以下3个维度的配置管理: 应用 这个比较好理解,每个配置都是属于某一个应用的 环境 每个配置都是区分环境的,如dev, test, prod等 版本 这个可能是一般的配置中心所缺乏的,就是对同一份配置的不同版本管理,比如:可以通过Git进行版本控制。 Spring Cloud Config提供版本的支持,也就是说对于一个应用的不同部署实例,可以从服务端获取到不同版本的配置,这对于一些特殊场景如:灰度发布,A/B测试等提供了很好的支持。 为什么会诞生Spring Cloud Config? 配置中心目前现状:不管是开源的(百度的disconf),还是一些公司自己闭源投入使用的产品已经不少了,那为什么还会诞生Spring Cloud Config呢? 在我看来,Spring Cloud Config在以下几方面还是有比较独特的优势,如下: 基于应用、环境、版本三个维度管理 这个在前面提过了,主要是有版本的支持 配置存储支持Git 这个就比较有特色了,后端基于Git存储,一方面程序员非常熟悉,另一方面在部署上会非常简单,而且借助于Git,天生就能非常好的支持版本 当然,它还支持其它的存储如本地文件、SVN等 和Spring无缝集成 它无缝支持Spring里面Environment和PropertySource的接口 所以对于已有的Spring应用程序的迁移成本非常低,在配置获取的接口上是完全一致的 Spring Cloud Config 入门例子上述节点主要介绍了Spring cloud的相关理论,大家对Spring Cloud Config有了一个初步的认识,接下来例子让大家感受一下Spring cloud config的魅力。 Overview 上图简要描述了一个普通Spring Cloud Config应用的场景。其中主要有以下几个组件: Config Client Client很好理解,就是使用了Spring Cloud Config的应用 Spring Cloud Config提供了基于Spring的客户端,应用只要在代码中引入Spring Cloud Config Client的jar包即可工作 Config Server Config Server是需要独立部署的一个web应用,它负责把git上的配置返回给客户端 Remote Git Repository 远程Git仓库,一般而言,我们会把配置放在一个远程仓库,通过现成的git客户端来管理配置 Local Git Repostiory Config Server的本地Git仓库 Config Server接到来自客户端的配置获取请求后,会先把远程仓库的配置clone到本地的临时目录,然后从临时目录读取配置并返回","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud Config","slug":"Spring-Cloud-Config","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud-Config/"}]},{"title":"Spring Cloud微服务框架主要子项目和RPC框架的对比","slug":"sc/sc-introduce","date":"2016-10-18T02:00:00.000Z","updated":"2017-06-17T03:23:26.000Z","comments":true,"path":"sc/sc-introduce/","link":"","permalink":"http://blog.springcloud.cn/sc/sc-introduce/","excerpt":"","text":"什么是Spring Cloud? Spring Cloud是一个相对比较新的微服务框架,今年(2016)才推出1.0的release版本. 虽然Spring Cloud时间最短, 但是相比Dubbo等RPC框架, Spring Cloud提供的全套的分布式系统解决方案。spring cloud 为开发者提供了在分布式系统(配置管理,服务发现,熔断,路由,微代理,控制总线,一次性token,全居琐,leader选举,分布式session,集群状态)中快速构建的工具,使用Spring Cloud的开发者可以快速的启动服务或构建应用.它们将在任何分布式环境中工作,包括开发人员自己的笔记本电脑,裸物理机的数据中心,和像Cloud Foundry云管理平台。下面是官方对Spring Cloud定义和解释。 Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, one-time tokens, global locks, leadership election, distributed sessions, cluster state). Coordination of distributed systems leads to boiler plate patterns, and using Spring Cloud developers can quickly stand up services and applications that implement those patterns. They will work well in any distributed environment, including the developer’s own laptop, bare metal data centres, and managed platforms such as Cloud Foundry. Spring Cloud主要项目 Spring Cloud 侧重于提供良好的开箱即用的功能,以便支持典型的开发场景和扩展支持。下面主要Spring Cloud项目在微服务框架中的主要子项目,具体的子项目源码分析,以及实现细节,将会在后面的文章中介绍。 Spring Cloud Config—配置中心 Spring Cloud Config就是我们通常意义上的配置中心 - 把应用原本放在本地文件的配置抽取出来放在中心服务器,从而能够提供更好的管理、发布。 在RPC服务治理框架中,一般都会开发一个配置中心和ZK配合使用,用于管理分布式应用中的配置信息。比如熔断的阀值,负载均衡的策略等。 Spring Cloud Netflix–注册中心,服务发现,LB Spring Cloud Netflix通过Eureka Server实现服务注册中心(包括服务注册,服务发现),通过Ribbon实现软负载均衡(load balance,简称LB) 在RPC框架中,例如:dubboX,HSF,OSP(唯品会的RPC框架)等RPC框架,都会通过ZK等实现服务注册,服务发现。当服务启动时,会将服务的IP地址,端口,服务命名,版本号等信息注册到ZK中,同时ZK Node会监听变化,接收最新的服务注册信息到client端或Proxy端。至于LB,都会有自己的实现算法,熔断等都有自己的实现方式。 Hystrix 熔断,包含在服务治理中。 Spring Cloud Sleuth Spring Cloud Sleuth为Spring Cloud提供了分布式追踪方案。全链路监控系统。 APM(Application Performance Monitor)这个领域最近异常火热。国外该领域知名公司包括New Relic,Appdynamics,Splunk。其中New Relic已经成功IPO,估值超过20亿美元。 1.国内外的个大互联网公司也都有类似大名鼎鼎的APM产品,例如淘宝鹰眼Eagle Eyes,点评的CAT,微博的Watchman,twitter的Zipkin。他们的产品虽未像专业APM公司的产品这样功能强大,但结合各自公司的业务特点,这些产品在支撑业务系统的高性能和稳定性方面,发挥了显著的作用。 2.众所周知,中大型互联网公司的后台业务系统由众多分布式组件构成,这些组件由web类型组件,RPC服务化类型组件,缓存组件,消息组件和数据库组件。一个通过浏览器或移动客户端的前端请求到达后台系统后,会经过很多个业务组件和系统组件,并且留下足迹和相关日志信息。但这些分散在每个业务组件和主机下的日志信息不利于问题排查和定位问题的Root Cause。这种监控场景正是应用性能监控系统的用武之地,应用性能监控系统收集,汇总并分析日志信息达到有效监控系统性能和问题的效果. 3.在唯品会体系中,Mercury提供的主要功能包括: 定位慢调用:包括慢Web服务(包括Restful Web服务),慢OSP服务,慢SQL 定位错误:包括4XX,5XX,OSP Error 定位异常:包括Error Exception,Fatal Exception 展现依赖和拓扑:域拓扑,服务拓扑,trace拓扑 Trace调用链:将端到端的调用,以及附加在这次调用的上下文信息,异常日志信息,每一个调用点的耗时都呈现给用户 应用告警:根据运维设定的告警规则,扫描指标数据,如违反告警规则,则将告警信息上报到唯品会中央告警平台 dubbo与Spring Cloud的比较 1.dubbo出自于阿里,Spring cloud出自于Spring社区,基于Spring boot提供一套完整的微服务解决方案。dubbo或者dubbox是RPC框 架,功能是Spring Cloud功能的一个子集。 2.dubbo是RPC服务治理框架,和Spring Cloud一样具备服务注册、发现、路由、负载均衡等能力。但是没有配置中心,完整的好用全链路监 控,需要采用开源的解决方案定制或者自研。Spring cloud的配置中心,全链路监控等组件。从目前来看,Spring Cloud国内中小型企业用的比较多,大型企业可能需要对其需要的组件进行定制化处理。 3.Spring cloud基于注解的服务发现,服务治理等功能具有代码侵入性,dubbo没有代码侵入性,业务开发人员不需要通过注解的方式去关注框架级别的处理。从中间件或者做基础架构的角度来看,其实服务治理等功能对普通的业务程序员应该是透明的,业务程序员不需要关注服务治理框架的使用,专注于业务代码即可。 因此大型企业可能需要对Spring cloud进行定制化处理。更多比较信息,可以参考下面的连接。 http://blog.didispace.com/microservice-framework/","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]},{"title":"关于SpringCloud中国社区以及国内使用情况","slug":"sc/springcloud","date":"2016-10-03T06:00:00.000Z","updated":"2017-06-17T03:01:01.000Z","comments":true,"path":"sc/springcloud/","link":"","permalink":"http://blog.springcloud.cn/sc/springcloud/","excerpt":"Spring Cloud中国社区起源 其实当Spring Cloud项目刚在github上出现的时候,我就一直在关注其项目发展,到了2015年8月,由于个人兴趣研究Spring Cloud项目,由于国内相关文档较少,当时就想建立一个中国社区,于是就先把域名注册了,选中域名为springcloud.cn。 为什么要发起Spring Cloud中国社区 Spring Cloud发展到2016年,国内关注的人越来越多,但是相应学习交流的平台和材料比较分散,不利于学习交流,因此Spring Cloud中国社区应运而生。 Spring Cloud中国社区是国内首个Spring Cloud构建微服务架构的交流社区。我们致力于为Spring Boot或Spring Cloud技术人员提供分享和交流的平台,推动Spring Cloud在中国的普及和应用。 欢迎CTO、架构师、开发者等,在这里学习与交流使用Spring Cloud的实战经验。 目前QQ群人数:7000+,微信群:2000+. 扫描下面二维码或者微信搜索SpringCloud,关注社区公众号 Spring Cloud中国社区QQ群①:415028731 Spring cloud中国社区QQ群②:530321604 Spring Cloud中国社区官网:http://springcloud.cn Spring Cloud中国社区论坛:http://springcloud.cn Spring Cloud中国社区文档:http://docs.springcloud.cn spring cloud目前国内使用情况 中国联通子公司http://flp.baidu.com/feedland/video/?entry=box_searchbox_feed&id=144115189637730162&from=timeline&isappinstalled=0","text":"Spring Cloud中国社区起源 其实当Spring Cloud项目刚在github上出现的时候,我就一直在关注其项目发展,到了2015年8月,由于个人兴趣研究Spring Cloud项目,由于国内相关文档较少,当时就想建立一个中国社区,于是就先把域名注册了,选中域名为springcloud.cn。 为什么要发起Spring Cloud中国社区 Spring Cloud发展到2016年,国内关注的人越来越多,但是相应学习交流的平台和材料比较分散,不利于学习交流,因此Spring Cloud中国社区应运而生。 Spring Cloud中国社区是国内首个Spring Cloud构建微服务架构的交流社区。我们致力于为Spring Boot或Spring Cloud技术人员提供分享和交流的平台,推动Spring Cloud在中国的普及和应用。 欢迎CTO、架构师、开发者等,在这里学习与交流使用Spring Cloud的实战经验。 目前QQ群人数:7000+,微信群:2000+. 扫描下面二维码或者微信搜索SpringCloud,关注社区公众号 Spring Cloud中国社区QQ群①:415028731 Spring cloud中国社区QQ群②:530321604 Spring Cloud中国社区官网:http://springcloud.cn Spring Cloud中国社区论坛:http://springcloud.cn Spring Cloud中国社区文档:http://docs.springcloud.cn spring cloud目前国内使用情况 中国联通子公司http://flp.baidu.com/feedland/video/?entry=box_searchbox_feed&id=144115189637730162&from=timeline&isappinstalled=0 上海米么金服 指点无限(北京)科技有限公司 易保软件 目前在定制开发中 http://www.ebaotech.com/cn/ 广州简法网络 深圳睿云智合科技有限公司 持续交付产品基于Spring Cloud研发 http://www.wise2c.com 猪八戒网 上海云首科技有限公司 华为 整合netty进来用rpc 包括nerflix那套东西 需要注意的是sleuth traceid的传递需要自己写。tps在物理机上能突破20w 东软 南京云帐房网络科技有限公司 四众互联(北京)网络科技有限公司 深圳摩令技术科技有限公司 广州万表网 视觉中国 上海秦苍信息科技有限公司-买单侠 爱油科技(大连)有限公司爱油科技基于SpringCloud的微服务实践 广发银行 卖货郎(http://www.51mhl.com/) 拍拍贷 甘肃电信 新浪商品部 春秋航空 冰鉴科技 万达网络科技集团-共享商业平台-共享供应链中心 网易乐得技术团队 饿了么某技术团队 高阳捷迅信息科技–话费中心业务平台–凭证查询及收单系统数据在统计之中,会一直持续更新,敬请期待! 捐赠社区发展捐赠社区 如果你觉得,Spring Cloud中国社区还可以,为了更好的发展,你可以捐赠社区,点击下面的打赏捐赠,捐赠的钱将用于社区发展和线下meeting up。","categories":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/categories/Spring-Cloud/"}],"tags":[{"name":"Spring Cloud","slug":"Spring-Cloud","permalink":"http://blog.springcloud.cn/tags/Spring-Cloud/"}]}]}