一、基础知识
微服务:微服务是系统架构上的一种设计风格,它的主旨是将一个原本独立的系统拆分成多个小型服务,
这些小型服务都在各自独立的进程中运行,服务之间通过基于HTTP的RESTful API进行通信协作。
与单系统区别:
单体系统在初期可以非常方便地进行开发与使用,但是随着系统的发展,维护成本会变得越来越大,且难以控制。
为解决庞大臃肿之后产生的难以维护的问题,微服务架构诞生了并被大家所关注。我们将系统功能模块拆分成多个不同的服务,
这些服务都能够独立部署和扩展。
如何实施微服务:
(1)运维的新挑战
进程多,维护难度增大
(2)接口的一致性
拆分了服务,业务逻辑依赖并不会消除,从单体应用中的代码依赖变为了服务间的通信依赖。
一旦原有接口进行了一些修改,那么交互双方也需要协调这样的改变来进行发布,以保证原有接口的正确调用。
(3)分布式的复杂性:
单体->多个独立微服务,服务之间只能通过通信来进行协作,会带来一系列问题,如:网络延迟、分布式事务、异步消息等。
微服务九大特性:
(1)服务组件化
组件,是一个可以独立更换和升级的单元,独立且可以更换升级而不影响其他单元。
在微服务架构中,需要我们对服务进行组件化分解。
(2)按业务组织团队
当决定如何划分微服务时,通常也意味着我们要开始对团队进行重新规划与组织。按以往的方式,我们往往会从技术的层面将团队划分为多个,
比如DBA、运维、后端、前端、设计师等。
在实施微服务架构时,需要采用不同的团队分割方法。
(3)做“产品”的态度
在实施微服务架构的团队中,每个小团队都应该以做产品的方式,对其产品的整个生命周期负责。而不是以项目的模式,以完成开发与交付
并将成果交接给维护都为最终目标。
(4)智能端点与哑管道
在单体应用中,组件间直接通过函数调用的方式进行交互协作。而在微服务架构中,由于服务还在一个进程中,组件间的通信模式发生了改变,若仅仅将原本
在进程内的方法调用改成RPC方式的调用,会导致微服务之间产生烦琐的通信,使得系统表现更为槽糕,所以我们需要更粗粒度的通信协议。
第一种:RESTful API或轻量级的消息发送协议,第二种:通过在轻量级消息总线上传递消息,类似RabbitMQ等一些提供可靠异步交换的中间件。
(5)去中心化治理
在实施微服务架构时,通过采用轻量级的契约定义接口,使得我们对于服务本身的具体技术平台不再那么敏感,这样整个微服务架构系统中的各个组件就能针对其
不同的业务特点选择不同的平台,终于不会出现杀鸡用牛刀等的尴尬。
(6)去中心化管理数据
我们在实施微服务架构时,都希望让每一个服务来管理其自有的数据库,这就是数据管理的去中心化。
(7)基础设施自动化
自动化测试:每次部署前的强心剂,尽可以地获得对正在运行的软件的信心。
自动化部署:解放烦琐枯燥的重复操作以及对多环境的配置管理。
(8)容错设计
单体服务中,存在一挂全持,而在微服务,服务独立运行,不影响其它服务。
在微服务架构中,快速检测出故障源并尽可能地自动恢复是必须被设计和考虑的。
每个服务中实现监控和日志记录的组件,比如服务状态,断路器状态,吞吐量、网络延迟等关键数据的仪表盘等。
(9)演进式设计
通过上面的几点特征,我们已经能够体会到,要实施一个完善的微服务架构,需要考虑的设计与成本并不小,对于没有足够经验的团队来说,甚至要比单体应用
付出更多的代价。
项目初期,以单体系统的方式来设计和实施,系统量初期并不会很大,构架和维护成本都不高。随着系统的发展或者业务的需要,架构师会将一些经常
变动或是有一定时间效应的内容进行微服务处理,并逐渐将原来在单体系统中多变的模块逐步拆分出来,而稳定不太变化的模块就形成一个核心微服务存在于整个
架构之中。
为什么选择Spring Cloud:
服务治理:
Dubbo、DubboX、Netflix的Eureka、Apache的Consul等.
分布式配置管理:
百度的Disconf、Netflix的Archaius、360的QConf、Spring Cloud 的Config、淘宝的Diamond等
批量任务:
当当网的Elastic-Job、LinkedIn的Azkaban、Spring Cloud的Task等
服务跟踪:
京东的Hydra、Spring Cloud的Sleuth、Twitter的Zipkin等
......
Spring Cloud简介
Spring Cloud是一个基于Spring Boot实现的微服务架构开发工具。它为微服务架构中涉及的配置管理、服务治理、断路器,智能路由、微代理、控制总线、全局锁、
决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。
Spring Cloud包含了多个子项目:
(1)Spring Cloud Config:
配置管理工具,支持使用Git存储配置内容,可以使用它实现应用配置外部化存储,并支持客户端配置信息刷新、加密/解密配置内容等。
(2)Spring Cloud Netflix:
核心组件,对多个Netflix OSS开源套件进行整合。
Eureka:服务治理组件,包含服务注册中心,服务注册与发现机制的实现
Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。
Ribbon:客户端负载均衡的服务调用组件。
Feign:基于Ribbon和Hystrix的声明式服务调用组件。
Zuul:网关组件,提供智能路由、访问过滤等功能。
Archaius:外部化配置组件。
(3)Spring Cloud Bus:
事件、消息总线,用于传播集群中的状态变化或事件,以触发后续的处理,比如用来动态刷新配置等。
(4)Spring Cloud Cluster:
针对ZooKeeper、Redis、Hazelcast、Consul的选举算法和通用状态模式的实现
(5)Spring Cloud Cloudfoundry:
与Privotal Cloudfoundry的整合支持。
(6)Spring Cloud Consul:
服务发现与配置管理工具。
(7)Spring Cloud Stream:
通过Redis、Rabbit或者kafka实现的消费微服务,可以通过简单的声明式模型来发送和接收消息。
(8)Spring Cloud AWS:
用于简化整合Amazon Web Service的组件。
(9)Spring Cloud Security:
安全工具包,提供在Zuul代理中对Oauth2客户端请求的中继器。
(10)Spring Cloud Sleuth:
基于Zookeeper的服务发现与配置管理组件。
(11)Spring Cloud Starters:
Spring Cloud的基础组件,它是基于Spring Boot风格项目的基础依赖模块。
(12)Spring Cloud CLI:
用于在Groovy中快速创建Spring Cloud应用的Spring Boot CLI插件。
......
二、微服务构建:Spring Boot
目标: 如何构建Spring Boot项目 如何实现RESTful API接口 如何实现多环境的Spring Boot应用配置 深入理解Spring Boot配置的启动机制 Spring Boot应用的监控与管理 Spring属性加载顺序: (1)在命令中传入的参数 (2)SPRING_APPLICATION_JSON中的属性。SPRING_APPLICATION_JSON是以JSON格式配置在系统环境变量中的内容。 (3)java:comp/env中的JNDI属性 (4)Java的系统属性,可以通过System.getProperties()获得的内容 (5)操作系统的环境变量 (6)通过random.*配置的随机属性 (7)位于当前应用jar包之外,针对不同${profile}环境的配置文件内容,例如application-{profile}.properties或是YAML定义的配置文件。 (8)位于当前应用jar包之内,针对不同${profile}环境的配置文件内容,例如application-{profile}.properties或是YAML定义的配置文件。 (9)位于当前应用jar包之外的application.properties和YAML配置内容。 (10)位于当前应用jar包之内的application.properties和YAML配置内容。 (11)在@Configurtion注解修改的类中,通过@PropertySource注解定义的属性。 (12)应用默认属性,使用SpringApplication.setDefaultProperties定义的内容。 原生端点: (1)应用配置类 获取应用程序中加载的应用配置、环境配置、自动化配置报告等与Spring Boot应用密切相关的配置类信息。 /autoconfig:// 获取应用自动化配置报告 { "positiveMatchers": { // 条件匹配成功的 "EndpointWebMvcAutoConfiguration": [ { "condition": "OnClassCondition", "message": "@ConditionOnClass classes found: javax.servlet.Servlet,org.springframework.web.servlet.DispatcherServlet" }, { "condition": "OnWebApplicationCondition", "message": "found web application StandardServletEnvironment" } ], ... }, "negativeMatchers": { // 条件匹配不成功的 "HealthIndicatorAutoConfiguration.DataSourcesHealthIndicatorConfiguration":[ { "condition": "OnClassCondition", "message": "required @ConditionalOnClass classes not found: org.springframework.jdbc.core.JdbcTemplate" } ], ... } } /beans: // 该端点用来获取应用上下文创建的所有Bean [ { "context": "hello:dev:8881", "parent": null, "beans": [ { "bean": "org.xx.xx.DispatcherServletAutoConfiguration$DispatcherServletConfiguration", "scope": "singleton", "type": "org.springframework.boot.autoconfig.web.xx$$CGLIB$$xx", "resource": "null", "dependences": [ "serverProperties", "spring.mvc.CONFIGURATION_PROPERTIES", "multipartConfigElement" ] }, { "bean": "DispatcherServlet", "scope": "singleton", "type": "org.springframework.boot.autoconfig.web.DispatcherServlet", "resource": "class path resource [org/springframework/boot/autoconfigure/web/DispatcherServletAutoConfiguration$DispatcherServletConfiguration.class]", "dependences": [] } ] } ] /configprops: // 该端点用来获取应用中的属性信息报告 { "configuractionPropertiesReportEndpoint": { "prefix": "endpoints.configprops", "properties": { "id": "configprops", "sensitive": true, "enabled": true } }, ... } /env: // 该端点与/configprops不同,它用来获取应用所有可用的环境属性报告。 { "profiles": [ "dev" ], "server.ports": { "localhost.server.port": 8881 }, "servletContextInitParams": { }, "systemProperties": { "idea.version": "2016.1.3", ... }, "systemEnviroment": { "configsetroot": "C:\WINDOWS\ConfigSetRoot", ... }, "applicationConfig": [classpath:/application-dev.properties]{ "server.port": "8881" }, "applicationConfig": [classpath:/application.properties]{ "server.port": "8885", "spring.profiles.active": "dev", "info.app.name": "spring-boot-hello", "spring.application.name": "hello" } } /mappings: // SpringMVC映射器关系 { "/webjars/**": { "bean": "resourceHandlerMapping" }, "/**": { "bean": "resourceHandlerMapping" }, "/**/favicon.ico": { "bean": "faviconHandlerMapping" }, "{[hello]}": { "bean": "requestMappingHandlerMapping", "method": "public java.lang.String com.didispace.web.HelloController.index()" }, "{[/mappings || /mappings.json],methods=[GET],produres=[application/json]}": { "bean": "endpointHandlerMapper", "method": "public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.EndpointWebMvcAdapter.invoke()" }, ... } /info: // 返回自定义信息 application.produres |-- info.app.name=spring-boot-hello |-- info.app.version=v1.0.0 { "app": { "name": "spring-boot-hello", "version": "v1.0.0" } } (2)度量指标类 获取应用程序运行过程中用于监控的度量指标,比如内存信息、线程池信息、HTTP请求统计等。 /metrics: // 内存信息、线程信息、垃圾回收信息 { "mem": 541305, "mem.free": 317864, "processors": 8, "instance.uptime": 33376471, "uptime": 333853352, "gauge.*": ... // HTTP性能指标之一,延迟量 "counter.*": ... // HTTP性能指标之一,增加量、减少量 ... } /health: // 各类健康指标信息 // 检查器 DiskSpaceHealthIndicator // 低磁盘空间检测 DataSourceHealthIndicator // 检测DataSource连接是否可用 MongoHealthIndicator // 检测Mongo数据库是否可用 RabbitHealthIndicator // 检测Rabbit服务器是否可用 RedisHealthIndicator // 检测Redis服务器是否可用 SolrHealthIndicator // 检测Solr服务器是否可用 自动实现检测器: |-- @Component public class RocketMQHealthIndicator implements HealthIndicator { @Override public Health health() { int errorCode = check(); if (errorCode != 0) { return Health.down().withDetail("Error Code", errorCode).build(); } return Health.up().build(); } private int check() { // 对监控对象检测操作 } } -> { "rocketMQ": { "status": "UP" } } /dump: // 线程信息 java.lang.management.ThreadMXBean的dumpAllThreads方法返回所含同步的活动线程详情。 /trace: // HTTP跟踪信息 org.springframework.boot.actuate.trace.InMemoryTranceRepository [ { "timestamp": 1482570000000, "info": { "method": "GET", "path": "/metricts/mem/", "headers": { "request": { "host": "localhost:8081", "connection": "keep-alive", "cache-control": "no-cache", "user-agent": "Mozilla/5.0(Windows NT 10.0; WOW64), ... }, "response": { "X-Application-Context": "hello:dev:8881", "Content-Type": "application/json;charset=UTF-8", "Transfer-Encoding": "chunked", "Date": "Sat, 24 Dec 2016 09:00:22 GMT", "status": "200" } } } } ] (3)操作控制类 提供了对应用的关闭等操作类功能。 /shutdown endpoint.shutdown.enabled=true
三、服务治理:Spring Cloud Eurake
Spring Cloud Netflix |-- Spring Cloud Eurake 目标: 构建服务注册中心 服务注册与服务发现 Eurake的基础架构 Eurake的服务治理机制 Eurake的配置 1、构建服务注册中心 (1)服务注册: 在服务治理框架中,通常会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机端口号、版本号、通信协议等信息。 服务器A:192.168.0.100:8000、192.168.0.101:8000 服务器B:192.168.0.100:9000、192.168.0.101:9000、192.168.0.102:9000 (2)服务发现: 由于服务治理框架下运作,服务间的调用不再通过指定具体的实例地址来实现,而是通过向服务名发起请求调用实现。 调用方:向服务注册中心咨询服务,并获取所有服务的实例清单,以实现对具体服务实例的访问。 例: 服务C->服务A, |-- 服务C -> 向服务注册中心咨询服务 |-- 将服务器A的位置清单返回-> 服务C |-- 192.168.0.100:8000、192.168.0.101:8000 |-- 通过某种策略取出一个位置来进行服务调用 -> 调用服务192.168.0.101:8000 Eurake服务端:服务注册中心, Eurake客户端:处理服务注册与发现 (3)搭建服务注册中心 |-- demo: |-- 服务端: |-- @EnableEurekaServer @SpringBootApplication public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(true).run(args); } } |-- application.properties |-- server.port=1111 |-- eureka.instance.hostname=localhost |-- eureka.client.register-with-eureka=false // 由于该应用为注册中心,所以设置为false |-- eureka.client.fetch-registry=false // 由于注册中心的职责是维护服务实例,它并不需要去检索服务,所以也设置为false |-- eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/ |-- 客户端: |-- pom.xml spring-boot-starter-web spring-boot-starter-test spring-cloud-starter-eureka spring-cloud-dependences |-- @RestController { Logger logger; @Autowire private DiscoveryClient client; @RequestMapping(value = "/hello", method = RequestMethod.GET) { instance = client.getLocalServiceInstance(); logger.info("/hello,host: " + instance.getHost() + ", service_id:" + instance.getServiceId()); return "Hello World"; } } |-- @EnableDiscoveryClient @SpringBootApplication public class HelloApplication { public static void main(String[] args) { SpringApplication.run(HelloApplication.class, args); } } |-- application.properties |-- spring.application.name=hello-service |-- eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/ -> http://localhost:8080/hello -> com.didispace.web.HelloController : /hello, host: PC-201602152056 service-id: hello-service (4)高可用注册中心 |-- application-peer1.properties |-- spring.application.name=eureka-server |-- service.port=1111 |-- eureka.instance.hostname=peer1 |-- eureka.client.serviceUrl.defaultZone=http://peer2:1112/eureka/ |-- application-peer1.properties |-- spring.application.name=eureka-server |-- service.port=1112 |-- eureka.instance.hostname=peer2 |-- eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/ |-- /etc/hosts 127.0.0.1 peer1 127.0.0.1 peer2 |-- 启动命令: java -jar eureka-server-1.0.0.jar --spring.profiles.active=peer1 java -jar eureka-server-1.0.0.jar --spring.profiles.active=peer2 |-- 访问地址: http://localhost:1111/ |-- 节点1111 |-- 节点1112 http://localhost:1112/ |-- 节点1111 |-- 节点1112 |-- 集群 |-- application.properties spring.application.name=hello-service eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/,http://peer2:2222/eureka/ |-- 启动服务后 http://localhost:1111/、http://localhost:1112/ -> hello-service被注册到了peer1和peer2 如果想以ip地址的形式访问,需要在配置文件中配置参数 |-- eureka.instance.prefer-ip-address=true,默认为false 2、服务注册与服务发现 (1)目标: 构建消费端,用来发现服务及消费服务。 (2)关键名词解析 服务发现-> Eureka的客户端 服务消费-> Ribbon Ribbon:是一个基于HTTP和TCP的客户端负载均衡器,通过ribbonServerList服务端列表去轮询访问以达到均衡。 (3)构建简单事例 |-- 启动服务: $ java -jar hello-service-1.0.0.jar --server.port=8081 $ java -jar hello-service-1.0.0.jar --server.port=8082 |-- Eureka面板 Application AMIs Availablity Zones Status HELLO-SERVICE n/a(2) (2) UP(2)-PC-201602152056:hello-service:80801,PC-201602152057:8082 |-- 创建服务消费者 |-- pom.xml groupId: spring-boot-starter-web spring-cloud-starter-eureka spring-cloud-starter-ribbon spring-cloud-denpendencies |-- @EnableDiscoveryClient @SpringBootApplication ConsumterApplication { // 主类 @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } main { SpringApplication.run(ConsumterApplication.class, args); } } |-- @RestController ConsumterController { @Autowire RestTemplate restTemplate; @RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET) public String helloConsumer() { return restTemplate.getForEntity("http://HELL-SERVICE/hello", String.class).getBody(); } } |-- application.properties spring.application.name=ribbon-consumer server.port=9000 eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/ |-- 启动ribbon-consumer Eureka面板 Application AMIs Availablity Zones Status HELLO-SERVICE n/a(2) (2) UP(2)-PC-201602152056:hello-service:80801,PC-201602152057:8082 RIBBON-CONSUMER n/a(1) (1) UP(1)-PC-201602152056:ribbon-consumer:9000 3、Eureka详解 (1)基础架构 服务注册中心:eurake-server 服务提供者:提供服务的应用 服务消费者:Ribbon来实现服务消费,或其它方式 (2)服务治理机制 服务提供者: A、服务注册: 服务提供者->注册到Eure Server上(同时带上了自身服务的一些元数据信息) -> Eure Server存储一个双层结构Map中, |-- 第一层key:服务名 |-- 第二层key:具体服务的实例名 在服务注册时,需要确认一下eureka.client.register-with-eureka=true参数是否正确,若设置为false将不会启动注册操作 B、服务同步: 服务注册A<--->服务注册B, 服务提供者->发送注册请求->服务注册中心 服务注册中心-> 转发给集群中相连的其它注册中心,从而实现注册中心的服务同步。 C、服务续约: 通过心跳包来持续告诉Eureka Server还活着。两个重要属性: eureka.instance.lease-renewal-interval-in-seconds=30 // 调用间隔时间 eureka.instance.lease-renewal-expiration-in-seconds=90 // 服务失效时间 服务消费者: A、获取服务: 启动服务消费者—>发送Rest请求到->注册中心->服务清单(只读,每隔30秒更新一次) 缓存属性: eureka.client.registry-fetch-interval-in-seconds=30(默认为30,单位秒) B、服务调用: 通过上一步获取了服务清单->Ribbon轮询方式选取服务调用(实现客户端负载均衡) Region:一个Region包含多个Zone Zone:每个服务客户端被注册到一个Zone中,所以每个客户端对应一个Region和Zone(优先访问同处一个Zone的服务提供方) C、服务下线: 服务实例正常关闭操作-> 触发一个服务下线的REST请求给Eureka Server -> Eureka Server将服务状态设置了DOWN 服务注册中心: A、失效剔除: Eurake Server在启动时会创建一个定时任务,默认每隔一段时间(默认60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除出去。 B、自我保护: 服务注册到Eureka Server之后,会将当前的实例注册信息保存起来,让这些实例不会过期,保护期间若实例出了问题,客户端很容易拿到 实际已经不存在的服务实例。 4、源码分析 DiscoveryClient 作用: 向Eureka Server注册服务实例 向Eureka Server服务租约 当服务关闭期间,向Eureka Server取消租约 查询Eureka Server中的服务实例列表 Eureka Client还需要配置一个Eureka Server的URL列表. @link public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { orderedUrls = new LinkedHashMap<>(); String region = getRegion(clientConfig); String[] availZones = clientConfig.getAvailablityZones(clientCofig.getRegion()); if (availZones == null || availZones.length == 0) { availZones = new String[1]; availZones[0] = DEFAULT_ZONE; } ... myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); String zone = availZones[myZoneOffset]; List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); if (serviceUrls != null) { orderedUrls.put(zone, serviceUrls); } ... return orderedUrls; } |-- public static String getRegion(EurekaClientConfig clientConfig) { String region = clientConfig.getRegion(); if (region == null) { region = DEFAULT_ZONE; } region = region.trim().toLowerCase(); return region; } |-- public String[] getAvailablityZones(String region) { String value = this.availablityZones.get(region); if (value == null) { value = DEFAULT_ZONE; } return value.split(","); } |-- 服务注册 private void initScheduledTasks() { ... if (clientConfig.shouldRegisterWithEureka()) { // 注册 instatnceInfoReplicator = new InstanceInfoReplicator( this, instanceInfo, clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2 ); ... instatnceInfoReplicator.start(clientCofig.getInstanceInfoReplicationIntervalSeconds()); // 开启一个线程 } else { logger.info("Not registering with Eureka server per configuration"); } } |-- run () { try { discoverClient.refreshInstanceInfo(); Long dirtyTimestamp = instanceInfo.isDirtyWithTime(); if (dirtyTimestamp != null) { discoverClient.register(); // 触发调用注册的地方 instanceInfo.unsetIsDirty(dirtyTimestamp); } } catch() { logger.warn(...); } finally { Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); } } |-- register() { EurekaHttpResponse<Void> httpResponse; try { httpResponse = eurekaTransport.registrationClient.register(instanceInfo); } catch() { logger.warn(...); throw e; } if (logger.isInfoEnabled()) { logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode()); } return httpResponse.getStatusCode() == 204; } |-- 服务获取与服务续约 private void initScheduledTasks() { ... if (clientConfig.shouldFetchRegistry()) { // 服务获取 ... } if (clientConfig.shouldRegisterWithEureka()) { // 服务续约(与服务注册共用一个if) ... } } |-- 服务注册中心处理 5、配置详解: (1)服务注册类配置(eureka.client为前缀) 指定注册中心: eureka.client.serviceUrl=http://localhost:8761/eureka/ 默认:key=defaultZone,value=http://localhost:8761/eureka/ 单机修改端口地址: eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/ 高可用的注册服务中心: eureka.client.serviceUrl.defaultZone=http://pee1:1111/eureka/,http://peer2:1111/eureka/ 增加安全验证: http://<username>:<password>@localhost:1111/eureka username:安全检验信息的用户名 password:该用户的密码 其它配置: eureka.client.*=xx (2)服务实例类配置(eureka.instance为前缀) 元数据: 包换服务名称、实例名称、实例IP、实例端口等 eureka.instance.<properties>=<value> eureka.instance.metadataMap.<key>=<value> eureka.instance.metadataMap.zone=shanghai 实例名配置:(主机名作为配置值) 默认命名扩展: ${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}} eureka.instance.instanceId=${spring.application.name}:${random.int} 端点配置: |-- homePageUrl 应用主页的url |-- statusPageUrl 状态页的url /info 端点 |-- healthCheckUrl 健康检查的url /health 端点 // 加前缀 management.context-path=/hello eureka.instance.statusPageUrlPath=${management.context-path}/info eureka.instance.healthCheckUrlPath=${management.context-path}/health // 修改原始路径 endpoints.info.path=/appInfo endpoints.health.path=/checkHealth eureka.instance.statusPageUrlPath=/${endpoints.info.path} eureka.instance.healthCheckUrlPath=/${endpoints.health.path} // 绝对路径 eureka.instance.statusPageUrlPath=https://${eureka.instance.hostname}/info eureka.instance.healthCheckUrlPath=https://${eureka.instance.hostname}/health eureka.instance.homePageUrlPath=https://${eureka.instance.hostname}/ 健康检测:(不是依靠/health来实现的,而是通过客户端心跳的方式来保持服务实例的存活) 心跳包能有效检查客户端进程是否正常运作,却无法保证服务端能够正常提供服务。大多数服务依赖第三方,如:数据库、缓存、消息代理等。 此时委托给/health端点,以实现更加全面的健康状态维护。 步骤: |-- pom.xml 引入spring-boot-starter-actuator模块依赖 |-- application.propertie eureka.client.healthcheck.enabled=true // 如做了特殊处理,需要重新配置 |-- 其它配置 eureka.instance.*=xx 6、跨平台支持: (1)通信协议: 默认使用Jersey和XStream配合JSON作为Server和Client之间的通信协议 Jersey: 是JAX-RS的参考实现,主要包括三部分: 核心服务器: JSR311中标准化和API标准,REST通信 核心客户端: Jersey客户端API与REST通信 集成: 提供spring、Guice、Apache Abdera库 XStream: 是用来将对象序列化成XML(JSON)或反序列化为对象的一个JAVA类库。 JAX-RS: @Path @GET@PUT@POST@DELETE @Produces @Consumes @PathParam@QueryParam@HeaderParam@CookieParam@MatrixParam@FormParam
四、客户端负载均衡
spring cloud ribbon调用: 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。 服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。 (1)RestTemplate详解 GET请求: A、getForEntity函数: RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://USER-SERVICE/user?name={1}", String.class, "didi"); String body = responseEntity.getBody(); getForEntity(String url, Class responseType, Object... urlVarliables) |-- url 请求地址 |-- responseType 响应体body的包装类型 |-- urlVarliables 为url的绑定参数 getForEntity(String url, Class responseType, Map urlVarliables) |-- urlVarliables 为url的绑定参数 Map<String, String> params = new HashMap<>(); params.put("name", "didi"); responseEntity = restTemplate.getForEntity("http://USER-SERVICE/user?name={name}", String.class, params); getForEntity(URI url, Class responseType) |-- URI url 用URI代替了url和urlVarliables uriComponents = UriComponentsBuilder.fromUriString( "http://USER-SERVICE/user?name={name}") .build() .expand("dodo") .encode(); uril = uriComponents.toUri(); responseEntity = restTemplate.getForEntity(uri, String.class); B、getForObject函数:(getForEntity函数的进一步封装) RestTemplate restTemplate = new RestTemplate(); result = restTemplate.getForObject(uri, String.class); result = restTemplate.getForObject(uri, User.class); getForObject(String url, Class responseType, Object... urlVarliables) |-- url 请求地址 |-- responseType 响应体body的包装类型 |-- urlVarliables 为url的绑定参数 getForObject(String url, Class responseType, Map urlVarliables) |-- urlVarliables 为url的绑定参数 getForObject(URI url, Class responseType) |-- URI url 用URI代替了url和urlVarliables POST请求: A、postEntity函数: RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> responseEntity = restTemplate.postEntity("http://USER-SERVICE/user?name={1}", String.class, "didi"); String body = responseEntity.getBody(); |-- postEntity(String url, Object request, Class responseType, Object... urlVarliables) |-- postEntity(String url, Object request, Class responseType, Map urlVarliables) |-- postEntity(URI url, Object request, Class responseType) |-- request 普通对象|HttpEntity对象 B、postForObject(): |-- postForObject(String url, Object request, Class responseType, Object... urlVarliables) |-- postForObject(String url, Object request, Class responseType, Map urlVarliables) |-- postForObject(URI url, Object request, Class responseType) C、postForLocation(): |-- postForLocation(String url, Object request, Object... urlVarliables) |-- postForLocation(String url, Object request, Map urlVarliables) |-- postForLocation(URI url, Object request) User user = new User("didi", 40); URI responseURI = restTemplate.postForLocation("http://USER-SERVICE/user", user); PUT请求: RestTemplate restTemplate = new RestTemplate(); restTemplate.put("http://USER-SERVICE/user/{1}", user, id); |-- put(String url, Class responseType, Object... urlVarliables) |-- put(String url, Class responseType, Map urlVarliables) |-- put(URI url, Class responseType) DELETE请求: RestTemplate restTemplate = new RestTemplate(); Long id = 1000L; restTemplate.delete("http://USER-SERVICE/user/{1}", id); |-- delete(String url, Object... urlVarliables) |-- delete(String url, Map urlVarliables) |-- delete(URI url) 源码分析: LoadBalancerClient |-- ServerInstance choose(String serviceId) // 根据服务名称挑选一个对应服务实例 |-- <T> T execute(String serviceId, LoadBalancerRequest<T> request) throw IOException // 使用从负载均衡器中挑选出来的服务实例来执行请求内容 |-- URI reconstructURI(ServiceInstance instance, URI original) // 为系统构建一个合适的host:port形式的URI LoadBalancerAutoConfiguration 条件: |-- @ConditionalOnClass(RestTemplate.class) |-- @ConditionalOnClass(LoadBalancerClient.class) 做了三件事情: |-- 创建一个LoadBalancerInterceptor的bean,用于实现对客户端发起请求时进行拦截,以实现负载均衡。 |-- 创建一个RestTemplateCustomizer的bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器。 |-- 维护一个被@LoadBalanced注解修饰的列表,并在这里初始化,通过调用RestTemplateCustomizer的实例给需要 客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器 LoadBalancerInterceptor private LoadBalancerClient loadBalancer; |-- // 注入 LoadBalancerInterceptor(LoadBalancerClient loadBalancer) { this.loadBalancer = loadBalancer; } |-- intercept(request, body, execution) { return this.loadBalancer.execute(serviceName, new LoadBalancerRequest<ClientHttpResponse>() { @Override public ClientHttpResponse apply(ServiceInstance instance) throw Exception { ServiceRequestWrapper serviceRequest = new ServiceRequestWrapper(request, instance); return execution.execute(serviceRequest, body); } }); } private class ServiceRequestWrapper extends HttpRequestWrapper { private final ServiceInstance instance; ServiceRequestWrapper(request, instance) { super(request); this.instance = instance; } @Override public URI getURI() { uri = LoadBalancerInterceptor.this.loadBalancer.reconstructURI( this.instance, getRequest().getURI()); return uri; } } RibbonLoadedBalancerClient |-- <T> T execute(String serviceId, LoadBalancerRequest<T> request) throw IOException { loadBalancer = getLoadBancer(serviceId); server = getServer(loadBalancer); if (server == null) throw e; ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server)); context = this.clientFactory.getLoadBancerContext(serviceId); statsRecorder = new RibbonStatsRecorder(context, server); try { T returnVal = request.apply(ribbonServer); statsRecorder.recordStats(returnVal); return returnVal; } catch(e) { throw e; } return null; } |-- getServer(loadBalancer) { if (loadBalancer == null) return null; return loadBalancer.chooseServer("default"); } |-- 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(); } ZoneAwareLoadBalancer |-- public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping) { balancer = LoadBalancerBuilder.newBuilder() .withClientConfig(config).withRule(rule).withPing(ping) .withServerListFilter(serverListFilter).withDynamicServerList(serverList) .buildDynamicServerListLoadBalancer(); return balancer; } ServerInstance |-- String getServiceId() |-- String getHost() |-- int getPort() |-- boolean isSecure() |-- URI getUri() |-- Map<String, String> getMetadata() RibbonServer implements ServerInstance { RibbonServer(String serviceId, Server server) { this(serviceId, server, false, Collection.<String,String> emptryMap()); } RibbonServer(String serviceId, Server server, boolean secure, Map<String, String> metadata) { this.serverId = serviceId; this.server = server; this.secure = secure; this.metadata = metadata; } // setter、getter... } apply(final ServiceInstance instance) -> LoadBalancerClient.reconstructURI() // 重构URI来访问 -> execution.execute(serviceRequest, body) -> InterceptingClient.InterceptingRequestExecution.execute() { ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod()); ... return delegate.execute(); } -> request.getURI() -> 调用ServiceRequestWrapper.getURI() -> RibbonLoadedBalancerClient.reconstructURI(ServiceInstance instance, URI original) { serviceId = instance.getServiceId(); context = this.clientFactory.getLoadBancerContext(serviceId); server = new Server(instance.getHost(), instance.getPort()); secure = isSecure(server, serviceId); URI uri = original; if (secure) { uri = UriComponentsBuilder.fromUri(uri),schema("https").build().toUri(); } return context.reconstructURIWithServer(server, uri); } |-- SpringClientFactory clientFactory // 用来创建客户端负载均衡器的工厂类,该工厂类为每一个不同名的Ribbon客户端生成不同的spring上下文 |-- RibbonLoadedBalancerContext context // LoadedBalancerContext子类,该类用于存储被负载均衡器使用上下文内容和API操作 LoadBalancerContext implements IClientConfigAware { ... public URI reconstructURIWithServer(Server server, URI original) { host = server.getHost(); port = server.getPort(); if (host.equals(original.getHost()) && port = original.getPort()) { return original; } String schema = original.getSchema(); if (schema == null) { schema = deriveSchemaAndPortFromPartialUri(original).first(); } try { sb = new StringBuilder(); sb.append(schema).append("://"); if (!String.isNullOrEmpty(original.getRawUserInfo())) { sb.append(original.getRawUserInfo()).append("@"); } sb.append(host); if (port >= 0) { sb.append(":").append(port); } if (!String.isNullOrEmpty(original.getRawQuery())) { sb.append("?").append(original.getRawQuery()); } if (!String.isNullOrEmpty(original.getRawFragment())) { sb.append("#").append(original.getRawFragment()); } URI newURI = new URI(sb.toString()); return newURI; } catch(e) { throw e; } ... } } 负载均衡器: ILoadBalancer |-- AbstractLoadBalancer |-- ServerGroup // 服务实例 ALL:所有服务实例 STATUS_UP:正常服务实例 STATUS_NOT_UP:停止服务实例 |-- @Override Server chooseServer() { return chooseServer(null); } |-- abstract List<Server> getServerList(ServerGroup serverGroup) |-- abstract LoadBancerStats getLoadBancerStats() |-- BaseLoadBalancer 定义了两个存储服务实例Server对象的列表: |-- @Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL) protected volatile List<Server> allServerList = Collections.synchronizedList(new ArrayList<Server>()); |-- @Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL) protected volatile List<Server> upServerList = Collections.synchronizedList(new ArrayList<Server>()); 定义了之前我们提到的用来存储负载均衡器各服务实例属性和统计信息的LoadBalancerStats对象 |-- LoadBalancerStats 定义了检查服务是否正常服务的IPing对象 |-- IPing 定义了检查服务实例操作的执行策略对象IPingStrategy |-- IPingStrategy |-- SerialPingStrategy implements IPingStrategy { @Override public boolean[] pingServer(IPing ping, Server[] servers) { int numCandidates = servers.length; boolean[] results = new Boolean[numCandidates]; if (logger.isDebugEnabled()) { logger.debug(...); } for (int i=0; i < numCandidates; i++) { results[i] = false; try { results[i] = ping.isAlive(server[i]); } catch (Throwable t) { logger.error(...); } } return results; } } 定义了负载均衡处理规则:(BaseLoadBalancer中的chooseServer(Object key)委托给了RoundRobinRule)-> rule.choose(key) |-- IRule |-- RoundRobinRule implements IRule { ... @Override Server choose(key) { ... } } 启动ping任务: |-- BaseLoadBalancer() { // 启动一个用于定时检查Server是否健康的任务 } 实现了ILoadBalancer接口定义的负载均衡器应具备以下一系列基本操作。 |-- addServers(List newServers) { if (isNotEmpty(newServers)) { try { newList = new ArrayList<Server>(); newList.addAll(allServerList); newList.addAll(newServers); setServersList(newList); } catch (e) { logger.error(e); } } } |-- chooseServer(Object key) // 挑选一个具体的服务实例 |-- markServerDown(Server server) // 标记某个服务实例暂停服务 { if (server == null) return; if (!server.isAlive()) return; server.setAlive(false); notifyServerStatusChangeListener(singleton(server)); } |-- getReachableServers() // 获取可用服务实例列表 { return Collections.unmodifiableList(upServerList); } |-- getAllServers() // 返回所有的服务实例清单 { return Collections.unmodifiableList(allServerList); } |-- DynamicServerListLoadBalancer |-- ServerList interface ServerList<T extends Server> { public List<T> getInitialListOfServers(); public List<T> getUpdatedListOfServers(); } |-- DomainExtractingServerList implements ServerList<DiscoveryEnabledServer> { // 委托给了DiscoveryEnabledNIWServerList private ServerList<DiscoveryEnabledServer> list; private IClientConfig clientConfig; private boolean approximateZoneFromHostname; DomainExtractingServerList(ServerList<DiscoveryEnabledServer> list, IClientConfig clientConfig, boolean approximateZoneFromHostname) { this.list = list; this.clientConfig = clientConfig; this.approximateZoneFromHostname = approximateZoneFromHostname; } @Override public List<DiscoveryEnabledServer> getInitialListOfServers() { servers = setZones(this.list.getInitialListOfServers()); return servers; } @Override public List<DiscoveryEnabledServer> getUpdatedListOfServers() { servers = setZones(this.list.getUpdatedListOfServers()); return servers; } ... } |-- DiscoveryEnabledNIWServerList implements DiscoveryEnabledServer{ @Override public List<DiscoveryEnabledServer> getInitialListOfServers() { return obtainServersViaDiscovery(); } @Override public List<DiscoveryEnabledServer> getUpdatedListOfServers() { return obtainServersViaDiscovery(); } private List<DiscoveryEnabledServer> obtainServersViaDiscovery() { if (eurekaClientProvider == null || eurekaClientProvider.get() == null) { return new ArrayList<DiscoveryEnabledServer>(); } serverList = new ArrayList<DiscoveryEnabledServer>(); eurekaClient = eurekaClientProvider.get(); if (vipAddresses != null) { for (String vipAddress : vipAddresses.split(",")) { listOfInstanceInfo = eurekaClient.getInstanceByVipAddress( vipAddress, isSecure, targetRegion); for (InstanceInfo ii : listOfInstanceInfo) { if (ii.getStatus().equals(InstanceStatus.UP)) { ... DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr); des.setZone(DiscoveryClient.getZone(ii)); serverList.add(des); } } } if (serverList.size() > 0 && prioritizeVipAddressBasedServers) { break; } } return serverList; } } |-- List<DiscoveryEnabledServer> setZones(List<DiscoveryEnabledServer> servers) { // 设置id、zone、isAliveFlag、readyToServer等信息 List<DiscoveryEnabledServer> result = new ArrayList<>(); isSecure = this.clientConfig.getPropertyAsBoolean(CommonClientConfigKey.IsSecure, Boolean.TRUE); shouldUseIpAddr = this.clientConfig.getPropertyAsBoolean(CommonClientConfigKey.UseIPAddrFofServer, Boolean.FALSE); for (DiscoveryEnabledServer server : servers) { result.add(new DomainExtractingServer(server, isSecure, shouldUseIpAddr, this.approximateZoneFromHostname)); } return result; } |-- ServerListUpdater interface ServerListUpdater { public interface UpdateAction { void doUpdate(); } // 启动服务器 void start(UpdateAction updateAction); void stop(); String getLastUpdate(); // 获取上一次更新到现在的时间间隔,单位为毫秒 long getDurationSinceLastUpdateMs(); // 获取错过的周期数 int getNumberMissedCycles(); // 获取核心线程数 int getCoreThreads(); } |-- PollingServerListUpdater implements ServerListUpdater // 动态服务列表更新的默认策略(负载均衡器默认实现就是它) { @Override public synchronized void start(final UpdateAction updateAction) { if (isActive.compareAndSet(false, true)) { final wrapperRunnable = new Runnable() { @Override public void run() { if (!isActive.get()) { if (scheduledFuture != null) { scheduledFuture.cancel(true); } return; } try { updateAction.doUpdate(); lastUpdated = System.currentTimeMillis(); } catch (e) { logger.warn(...); } } } scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay( wrapperRunnable, initialDelayMs, refreshIntervalMs, TimeUnit.MILLISECONDS ); } else { logger.info("Already active, no-op"); } } } |-- EurekaNotificationServerListUpdater // 通过Eureka的事件监听器来驱动服务列表的更新操作 |-- ServerListFilter |-- AbstractServerListFilter<T extends Server> implements ServerListFilter<T> { volatile LoadBalancerStats stats; setLoadBalancerStats(LoadBalancerStats stats) { this.stats = stats; } LoadBalancerStats getLoadBalancerStats() { return stats; } } |-- ZoneAffinityServerListFilter extends AbstractServerListFilter { ... List<T> getFilteredListOfServers(List<T> servers) { if (isNotEmpty(servers)) { filteredServers = Lists.newArrayList(Iterables.filter( servers, this.zonAffinityPredicate.getServerOnlyPredicate())); if (shouldEnableZoneAffinity(filteredServers)) { return filteredServers; } else if (zonAffinity) { overrideCounter.increment(); } } return servers; } } |-- shouldEnableZoneAffinity(filteredServers) // 根据复杂算法筛选同区域实例指标(实例数量、断路器断开数、活动请求数、实例平均负载等) |-- blackOutServerPercentage:故障实例百分比(断路器断开数/实例数量)>=0.8 |-- activeRequestsPerServer:实例平均负载>=0.6 |-- availableServers:可用实例数量(实例数量-断路器断开数量)<2 { ... } |-- DefaultNIWSServerListFilter |-- ServerListSubsetFilter:适用于大规模服务器集群(上百或更多)的系统 |-- ... |-- ZoneAwareLoadBalancer 待续。。。