Spring Cloud(一)—— Eureka让你的服务被发现

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高级架构

图中的三个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调用其它微服务
  1. 创建Eureka Server
    -> 进入https://start.spring.io/选择add dependency:Eureka Server
    -> Download
    Spring Initialzr

    -> 配置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)
Eureka Server Console

如果你留意一下此时的项目日志,你会发现系统一直在报错:
错误提示

为什么会这样!明明我们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<CatalogItem> 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;
}

整个过程如下图所示:


服务发现流程.png

这里需要说明的是如果同一个服务被分布式的存储在多个服务器上,举个例子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——断路器!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容