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/organizationservice
5、代码测试
@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);
}
}