https://github.com/carnellj/spmia-chapter4
服务发现对于微服务和基于云的应用程序至关重要,主要原因有两个:
通过服务发现,服务消费者能够将服务的物理位置抽象出来
由于服务消费者不知道实际服务实例的物理位置,因此可以从可用服务池中添加或移除服务实例
在非云的世界中,应用程序定位资源物理位置服务的解析通常由DNS和网络负载均衡器的组合来解决。
这种模型适用于在企业数据中心内部运行的应用程序,以及在一组静态服务器上运行少量服务的情况,但对基于云的微服务应用程序来说,这种模式并不适用。原因有以下几个:
单点故障:负载均衡器出现故障,依赖它的应用程序也会出现故障,是集中式阻塞点
有限的水平可伸缩性:在服务集中到单个负载均衡器集群的情况下,跨多个服务器水平伸缩负载均衡基础设施的能力有限
静态管理:大多数传统的负载均衡器不是为快速注册和注销服务设计的。它们使用集中式数据库来存储规则的路由,添加新路由的唯一方法通常是通过供应商的专有API来进行添加
复杂:由于负载均衡器充当服务的代理,它必须将服务消费者的请求映射到物理服务
在云中的服务发现
在这个模型中,当服务消费者需要调用一个服务时:
它将联系服务发现服务,获取它请求的所有服务实例,然后在服务消费者的机器上本地缓存数据
每当客户端需要调用该服务时,服务消费者将从缓存中查找该服务的位置信息。通常客户端缓存将使用简单的负载均衡算法,如“轮询”负载均衡算法,以确保服务调用分布在多个服务实例之间
然后,客户端将定期与服务发现进行联系,并刷新服务实例的缓存。客户端缓存最终是一致的,但是始终存在这样的风险:在客户端联系服务发现实例以进行刷新和调用时,调用可能会被定向到不健康的服务实例上
如果在调用服务的过程中,服务调用失败,那么本地的服务发现缓存失效,服务发现客户端将尝试从服务发现代理刷新数据
Spring Eureka服务
一、Eureka Server
1、pom.xml添加Eureka server依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>2、application.yml配置Eureka server
eureka: client: fetch-registry: false register-with-eureka: false server: wait-time-in-ms-when-sync-empty: server: port: 8761参数说明:
server.port:设置Eureka服务默认端口,默认端口就是8761
eureka.client.register-with-eureka:告知服务,在Spring Boot Eureka应用程序启动时不要通过Eureka服务注册,因为它本身就是Eureak服务(因为这里是Eureka Server,不需要将自己注册进去,只提供Eureka Client被发现)
euraka.client.fetch-registry:设置为false以便Eureka服务启动时,它不糊尝试在本地缓存注册表信息(提供给Euraka Client服务消费者就需要本地缓存,方便服务消费者下次访问同一个服务时,从本地缓存直接定位,而不需要再走服务发现)
eureka.client.server.wait-time-in-ms-when-sync-empty:Eureka在启动时不会马上通告任何通过它注册的服务,默认情况下它会等待5min,让所有的服务都有机会在通告之前通过它来注册。进行本地测试时添加该属性,将有助于加快Eureka服务启动和显示通过它来注册服务所需的时间
每次服务注册需要30s的时间才能显示在Eureka服务中,因为Eureka需要从服务接收3此连续心跳包ping,每次心跳包ping间隔10s,然后才能使用这个服务
3、Application添加注解启动Eureka Server
@SpringBootApplication @EnableEurekaServer // 添加注解启动Eureka Server public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }4、测试访问
http://localhost:8761二、Eureka Client
1、pom.xml添加Eureka client依赖
<dependency> <groudId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency>2、application.yml配置Eureka client
spring: application: name: organizationservice profile: active: default coud: config: enabled: true eureka: client: fetch-registry: true register-with-eureka: true service-url: defaultZone: http://localhost:8761/eureka instance: prefer-ip-address: true参数说明:
eureka.instance.prefer-ip-address:在默认情况下,Eureka在尝试注册服务时,将会使用主机名让外界与它进行联系。这种方式在基于服务器的环境中运行良好,在这样的环境中,服务会被分配一个DNS支持的主机名。但是,在基于容器的部署(如Docker)中,容器将以随机生成的主机名启动,并且该容器没有DNS记录。
如果没有将 eureka.instance.prefer-ip-address 设置为true,那么客户端应用程序将无法正确地解析主机名的位置,因为该容器不存在DNS记录
spring.application.name:使用Eureka注册的服务名称,当该服务注册到Eureka server时,显示的就是该名称;通常该属性会放在bootstrap.yml中,这里为了测试直接放在这里
eureka.client.register-with-eureka:设置Eureka启动的时候注册到Eureka server
eureka.client.fetch-registry:注册到Eureka server后,服务消费者访问服务时缓存该服务位置,下次访问时直接使用缓存
eureka.server-url.defaultZone:Eureka client注册到Eureka server的ip地址
每个通过Eureka注册的服务都会两个与之相关的组件:应用程序ID和实例ID。应用程序ID始终是由spring.application.name属性设置;实例ID是一个随机数,用于代表单个服务实例。
3、激活Eureka client
@SpringBootApplication @EnableDiscoveryClient // 激活Eureka client,使应用程序能够使用DiscoveryClient和Ribbon库 public class EurekaClientApplication { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }4、测试访问Eureka client
http:localhost:8761/eureka/organizationservice5、代码测试
@GetMapping("/{licenseId}/{clientType}") public License getLicensesWithClient( @PathVariable("organizationId") String organizationId, @PathVariable("licenseId) String licenseId, @PathVariable("clientType") String clientType) { return licenseService.getLicense(organizationId, licenseId, clientType); } public License getLicense(String organizationId, String licenseId, String clientType) { License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId); Organization org = retrieveOrgInfo(organizationId, clientType); return license .withOrganizationName(org.getName()) .withContactName(org.getContactName()) .withContactEmail(org.getContactEmail()) .withContactPhone(org.getContactPhone()) .witchComment(config.getExampleProperty()); } 使用DiscoveryClient查找信息(该方式存在问题:没有利用Ribbon负载均衡;开发人员做了太多工作,要硬编码调用的URL地址访问) @Component public class OrganizationDiscoveryClient { // DiscoveryClient是用于与Ribbon交互的类 // DiscoveryClient在实际运用中,只有在服务需要查询Ribbon以了解哪些服务和服务实例已经通过它注册时,才应该直接使用 @Autowired private DiscoveryClient discoveryClient; public Organization getOrganization(String organizationId) { // 这里实例化了RestTemplate而不是通过注解的原因: // 一旦应用程序通过@EnableDiscoveryClient注解启用了Spring DiscoveryClient // 有Spring管理的RestTemplate都将注入一个启动了Ribbon的拦截器,拦截器将改变使用RestTemplate类创建URL的行为 // 直接实例化能避免这种行为 RestTemplate restTemplate = new RestTemplate(); // ServiceInstance类用于保存关于服务的特定实例(比如主机名、端口和URL) List<ServiceInstance> instances = discoveryClient.getInstances("organizationservice")); if (instances.size == 0) return null; // 构建访问服务的URL String serviceUri = String.format("%s/v1/organizations/%s", instances.get(0).getUri().toString(), organizationId); // 路由到要访问的服务 ResponseEntity<Organization> restExchange = restTemplate.exchange( serviceUri, HttpMethod.GET, null, Organization.class, organizationId); return restExchange.getBody(); } } 带有Ribbon功能的RestTemplate(因为有Ribbon,所以具备负载均衡,访问该服务后会有缓存) @SpringBootApplication public class EurekaClientApplication { // @LoadBalanced注解告诉Spring Cloud创建一个支持Ribbon的RestTemplate @LoadBalanced @Bean public RestTemplate getRestTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(EurekaClientApplication.class, args); } } @Component public class OrganizationRestTemplateClient { @Autowired RestTemplate restTemplate; pulbic Organization getOrganization(String organizationId) { // http://organizationservice/v1/organizations/{organizationId} // 格式:http://{applicationId}/v1/organizations/{organizationId} // 启用Ribbon的RestTemplate将解析传递给它的URL,并使用传递的内容作为服务器名称,该服务器名称作为从Ribbon查询服务实例的键 ResponseEntity<Organization> restExchange = restTemplate.exhange( "http://organizationservcie/v1/organizations/{organizationId}", HttpMethod.GET, null, Organization.class, organizationId); return restExchange.getBody(); } } Feign调用服务 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> @SpringBootApplication @EnableFeignClients // 添加Feign public class EurekaClientApplication { public static void main(String[] args) { SpringApplication.run(EurekaClientApplication.class, args); } } @FeignClient("organizationservice") // 要访问服务的application name public interface OrganizationFeignClient { // 定义要访问的服务接口,被访问的接口也应该有一个该方法 @RequestMapping(method=RequestMethod.GET, value="/v1/organizations/{organizationId}", consumes="application/json") Organization getOrganization(@PathVariable("organizationId") String organizationId); } @Component public class OrganizationClient { @Autowired private OrganizationFeignClient client; public Organization getOrganization(String organizationId) { return client.getOrganization(organizationId); } }