Spring Cloud的系列文章,按理来说应该从微服务的介绍开始,我的确是这样做的。在开始本文之前,我还写了一篇介绍微服务的文章,然而效果并不满意,所以暂且不发,网上关于微服务概念的介绍很是泛滥,其中不乏优质文章,也无需我在其中滥竽充数。我希望在完成整个Spring Cloud系列文章后,再重头写这篇引文,我会在其中针对性的埋下雷点,让读者能够从解决问题的角度,来认识微服务和Spring Cloud提供的构建、部署工具,以求最大之成效。今天聊的是微服务的服务发现工具Eureka。
我认为在技术学习的过程中,一个不小的障碍来自我们对技术名词预设的难度。“面向切面编程”、“服务发现”可能就是这样一类,听上去挺复杂,其实也就那么回事儿。
假设有这样一个场景,我们在豆瓣上浏览电影信息,那么服务端电影基本信息,电影豆瓣评分和豆瓣用户信息由三个服务分别提供。
可以想见,无论这个client是客户端还是其它的service,我们在coding的时候,都需要将这三个服务的ip和port作为配置项记录下来,然后在对应的请求处调用。现在看来不需要发现机制,用配置文件管理就可以!
然而随着项目进行,用户量的增加,为了保证产品的健壮,我们可能对于一些重要服务进行分布式部署,比如movie info是咱们网站的主要提供内容,我们会将同一个movie info application部署到多个实例上,这样一来,即便访问量增加,也可以保证每个实例的负载在可承受的范围内,同时,一旦其中一台服务器宕机,整个系统仍然可以正常运转,此时的架构如下:
这个时候会让程序员为难,之前在配置文件中,对于movie info application服务直接用ip1:port1指代,现在一个服务由3个ip-port指代,代码中如何体现呢?另外我们怎么知道某一时刻应该访问哪一个服务呢?当然你可以用很tricky的方法,比如将新增的服务地址一样写入配置文件,然后针对每一个请求都以轮询的方式调用不同的配置地址。不难看出这种写法扩展性差,而且还有一个隐患,如果我们的服务是在云端,往往服务器的ip地址动态,因为产品扩容,发布失败等原因,所在服务器的ip地址会发生改变,类似以上方式,将ip-port硬编码在配置文件中,可能会导致不停修改配置文件的尴尬局面。
基于此我们希望有这样一个中间件,服务跑起来的时候,会主动去告诉中间件“我是谁,我在哪”,中间件记录该服务,客户端在请求的时候,只要告诉中间件对应服务的名字,就会获得该服务的真实路径。这就是服务注册和服务发现的过程:
下面认识一下本文的主角——Eureka /juˈriːkə/
Eureka起初是Netflix(制作过纸牌屋、绝命毒师)因为自身微服务项目孵化出来的开源产品,Spring将其融合进了Spring Cloud全家桶,因此你在Spring官网找相关资源时,实际上它是在Spring Cloud Netflix项目当中。
在Eureka的官方描述当中对其定义和架构有所描述:Eureka是CS架构,其服务端是一个基于REST(具象状态传输)的服务,主要用于AWS云中定位服务,以实现中间层服务器的负载平衡和故障转移;客户端设有内置的基础轮询式的负载均衡器。所以我们回复一下之前的实际问题,服务消费者发起对movie-info-application的请求,该请求会到达Eureka服务发现,查询注册表,将所有名叫movie-info-application的服务地址返回,再由Eureka Client中的负载均衡模块经过计算,确定最终访问哪一个地址并进行访问。
我们看一下Netflix自建的Eureka高级架构:
图中的三个Eureka Server可以看作是一个Eureka集群(cluster),一个region(图中的region为us-east-1,这是aws里面的概念)会部署一个集群,每个zone(图中zone为c、d、e)中最少部署一个Eureka Server。服务提供方(Application Service)和服务消费方(Application Client)都会集成Eureka Client。就服务提供方(图中最左侧的Application Service)而言,每次项目启动之后,都会向us-east-1c中的Eureka Server进行注册,注册成功后,不同的Eureka Sever之间会进行注册表的拷贝,保证注册表同步。服务提供方在默认情况下会每个30s向Eureka发一次Renew请求,告诉它自己还活着,避免自己从注册表中被踢掉,也就是心跳检测。如果Eureka在90s内没有收到某个服务的renew请求,则将其从注册表中除名。如果服务挂了,或者正常关闭,则服务提供方会发送cancel请求给Eureka告诉它删除注册表记录。另一方面,当服务消费方需要调用服务时,它会向与同一zone的Eureka发出get registry请求,获取注册表,而后由Eureka Client自带的负载均衡模块决定具体的服务器地址,从而进行真正的服务请求(make remote call)。
我们可以快速的构建三个Eureka项目,其中一个作为Eureka Server,剩余为Eureka Client。
实践步骤:
- 创建Eureka Server
- 通过Eureka Client在Server中注册各个微服务
- 通过Eureka Client调用其它微服务
- 创建Eureka Server
-> 进入https://start.spring.io/选择add dependency:Eureka Server
-> Download
-> 配置Eureka Server application.properties
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defalt-zone=http://${eureka.instance.hostname}:${server.port}/eureka/
-> 在项目入口添加@EnableEurekaServer注解
@SpringBootApplication
@EnableEurekaServer
public class SpringcloudEurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudEurekaServerApplication.class, args);
}
}
此时我们直接运行项目,然后访问默认的8080端口,可以看到Eureka控制台。可见当前没有任何注册到Server的服务(No instances availavle)
如果你留意一下此时的项目日志,你会发现系统一直在报错:
为什么会这样!明明我们build的应用就是eureka server,它怎么还在获取all instance registry!其实每个eureka server在默认情况下也是一个eureka client,还记得之前提到过,Netflix推荐使用eureka server集群么,它们相互之间互为eureka client,它们相互注册,这样一来即便一个server挂掉,其它的Server也有它的注册信息。在我们的demo中,只有一个eureka server,它不需要跟任何其它的server进行replicate,配置application.properties
//通常情况,eureka server端口为8761
server.port=8761
eureka.instance.hostname=localhost
//作为client,是否将自己注册到Eureka Server,默认为true
eureka.client.register-with-eureka=false
//作为client,是否从Eureka Server获取注册表信息,默认为true
eureka.client.fetch-registry=false
通过配置告诉eureka server关闭client身份,仅作为独立的server。
2.通过Eureka Client在Server中注册各个微服务
在此之前,我们创建简单的3个application,它们对应的端口分别是本地的8082、8083和8084。
创建三个service:movie-catalog-service(8082),movie-info-service(8083),ratings-data-service(8084)。其中catalog会去调用info和data取一些数据,业务逻辑不重要,关键在于以服务发现的形式调用。
- movie-info-service
application.properties
//设置该应用的名称,该名称会展示在Eureka控制台中,也是作为该服务在注册表中的key值(value为ip:port)
spring.application.name=movie-info-service
server.port=8083
MovieResource.java
@RestController
@RequestMapping("/movies")
public class MovieResource {
@RequestMapping("/{movieId}")
public Movie getMovieInfo(@PathVariable("movieId") String movieId) {
//定义一个Movie.java这里不赘述
return new Movie("1", "东邪西毒", "很好的一步电影");
}
}
- ratings-data-service
application.properties
spring.application.name=ratings-data-service
server.port=8083
RatingsResource.java
@RestController
@RequestMapping("/ratingsdata")
public class RatingsResource {
@RequestMapping("/user/{userId}")
public UserRating getUserRatings(@PathVariable("userId") String userId) {
UserRating userRating = new UserRating();
userRating.setUserId(userId);
//为了方便hard code。UserRating.java不赘述
userRating.setRating = 80;
return userRating;
}
}
- movie-catalog-service
application.properties
spring.application.name=movie-catalog-service
server.port=8082
MovieCatalogServiceApplication.java
@SpringBootApplication
@EnableDiscoveryClient
public class MovieCatalogServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MovieCatalogServiceApplication.class, args);
}
@LoadBalanced //加上该注解后,restTemplate才会将application-name作为key,去注册中心查找,否则application-name会被当作域名而无法访问。
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
CatalogResource.java
@RestController
@RequestMapping("/catalog")
public class CatalogResource {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/{userId}")
public List getCatalog(@PathVariable("userId") String userId) {
//直接指定application name
UserRating userRating = restTemplate.getForObject("http://RATINGS-DATA-SERVICE/ratingsdata/user/" + userId, UserRating.class);
return userRating.getRatings().stream()
.map(rating -> {
Movie movie = restTemplate.getForObject("http://MOVIE-INFO-SERVICE/movies/" + rating.getMovieId(), Movie.class);
return new CatalogItem(movie.getName(), movie.getDescription(), rating.getRating());
})
.collect(Collectors.toList());
}
}
几个model的为代码:CatalogItem Movie Rating UserRating
public class CatalogItem {
private String name;
private String desc;
private int rating;
}
public class Movie {
private String movieId;
private String name;
private String description;
}
public class Rating {
private String movieId;
private int rating;
}
public class UserRating {
private String userId;
private int rating;
}
整个过程如下图所示:
这里需要说明的是如果同一个服务被分布式的存储在多个服务器上,举个例子movie-info-application挤在192.168.0.113:8083上存在,也在192.168.0.124:8083存在,那么根据key在服务注册服务器返回得到的是包含113和114的地址列表,这个列表返回给movie-catalog-service,由ribbon进行负载均衡,从中选则一个ip作为请求发送的ip。
以上是Eureka的基本介绍,下一节继续Spring Cloud——断路器!