今天看啥  ›  专栏  ›  since1986

Spring Cloud与微服务学习笔记-注册与发现

since1986  · 掘金  · Java  · 2018-03-30 02:04

前言

在上一篇文章「Spring Cloud与微服务学习笔记-基本概念」中,我们简要介绍了微服务和Spring Cloud的概念。在本篇文章中,我们将介绍微服务中不可或缺的组成部分:服务注册与发现,并且使用Spring Cloud Netflix中的Eureka组件来搭建一组服务注册与发现服务器。

什么是服务注册与发现

关于这个问题,咱们来设想一个生活中的场景:小张晚上下班回家,饿了就要饿了么,然后拿起了手机,点开美团外卖软件,从中翻了一翻,在饭店列表中看到了一家新店:“老李肉夹馍”,心想,刚好要吃肉夹馍,就这家吧,下单,等吃;另一边,前不久,老板老李,新开了一家店:老李肉夹馍,老李知道外卖能给自己的店带来生意,于是在外卖平台上注册了自己的店,这几天果然生意兴隆,刚刚老李看到了小明下的单,然后接单,做饭。这个场景中,老张是服务的提供方,而小张是服务的消费方,而老李向外卖平台注册后,小张就能够发现并消费老李提供的服务了,这个外卖平台其实就是提供了“服务注册与发现”。

为什么要有“服务注册与发现”

那么,为什么微服务需要服务注册与发现呢?因为没有服务注册与发现会很麻烦的,还是以一个例子来说明吧:平常我们经常会访问各种各样的网站,试想一下,如果访问每一个网站都需要记住很长的一串IP地址的话,那访问起来会有多不方便,况且,网站的IP也不可能自始至终就那莫一个,一旦变化了,难道作为网站的经营者要通知所有忠实的用户新的IP地址是多少吗,好在,我们有DNS这种注册与发现服务,前边说的那些情况,DNS这种注册与发现服务就默默的帮我们处理了。对于微服务也是如此,假如A会消费B的服务,你可以A中硬编码B服务的调用地址的方式来记住调用的路径,但是一旦B的部署环境发生改变,A岂不是也要跟着改,这样变来变去会烦死的,更何况B有可能会有多个实例,A想要消费B就需要从B的众多实例中选出一个来消费,就更不可能用硬编码记住地址这种方式调用了,所以需要有类似于DNS能注册与发现IP地址这样的注册与发现服务来帮助我们去处理这些繁琐的细节。

Eureka

那么,在Spring Cloud体系中,是如何实现注册与发现的呢?Spring Cloud复用了现有的解决方案,支持接入三种成熟的注册与发现的解决方案:ZookeeperConsulEureka,这三种解决方案可以按照需要自己选择(Spring Cloud推荐Eureka),并且Spring Cloud提供了统一的接口来屏蔽他们的区别。

由于我们选择了使用Spring Cloud Netflix体系来实现微服务,而Eureka是包含在Spring Cloud Netflix体系中的,所以我们选择使用Eureka来做注册与发现。

Eureka的概念

什么是Eureka呢?来看官方的介绍

What is Eureka?

Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers. We call this service, the Eureka Server. Eureka also comes with a Java-based client component,the Eureka Client, which makes interactions with the service much easier. The client also has a built-in load balancer that does basic round-robin load balancing. At Netflix, a much more sophisticated load balancer wraps Eureka to provide weighted load balancing based on several factors like traffic, resource usage, error conditions etc to provide superior resiliency.

大致的意思是Eureka是基于REST的,主要目的用于AWS云服务(Amazon Web Services)上服务注册与发现、负载均衡和故障转移的中间层服务器。Eureka包含了服务器端和客户端两部分,其中客户端内置了负载均衡。

如何使用Eureka

如何使用Eureka呢?如果没有Spring Cloud,我们需要配置好一组Eureka服务器,然后在我们的服务中加入Eureka客户端,然后调用客户端API就行了(可以看官方给的例子)。当然,有了Spring Cloud后,我们不用这么做了,Spring Cloud给Eureka做了一层包装,服务器端可以以Spring Boot程序的形式来跑,而客户端则不需显式调用API了,由Spring Cloud提供了统一的声明式接入的方法接入就可以了。

用Eureka来做服务注册与发现的demo

说归说,练归连,想要用的起来,还得实际动手,我们开始吧。我会做两个demo,一个是最基本的单实例eureka server + 单实例service的例子,另一个是多实例eureka server + 多实例service的例子。

单实例eureka server + 单实例service

我们来新建一个多模块的Gradle工程,包含3个模块:eureka-sever、service-a、service-b,分别对应了Eureka服务器和两个服务。(全部代码在我的Github上

先来写Eureka服务器,Spring Cloud对Eureka进行了包装,可以将eureka server做成一个Spring Boot应用,代码如下:

//eureka-server模块构建配置
dependencies {
    compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-eureka-server', version: '1.4.0.RELEASE'
}
//eureka-server模块主启动
package com.github.since1986.learn.cloud.eureka.server;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        new SpringApplicationBuilder(App.class).web(true).run(args);
    }
}
//eureka-server模块主配置
package com.github.since1986.learn.cloud.eureka.server;

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
import org.springframework.context.annotation.Configuration;

@EnableEurekaServer
@Configuration
public class AppConfig {
}
# Spring Cloud配置
spring:
  application:
    name: com-github-since1986-learn-cloud-eureka-server #注意命名要符合RFC 2396,否则会影响服务发现 详见https://stackoverflow.com/questions/37062828/spring-cloud-brixton-rc2-eureka-feign-or-rest-template-configuration-not-wor

server:
  port: 8001

eureka: #这里配置eureka相关
  instance:
    hostname: localhost #服务器实例主机名
  client: #客户端配置
    registerWithEureka: false #由于只有一个服务器实例,我们没必要自己注册自己了
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

至此,我们已经配置好了一个Eureka服务器的实例,大家注意主配置中的@EnableEurekaServer,在Java中我们只需要加这一个注解就好了,一行代码也不用写,其他的都是starterapplication.yml配置中的事了,是不是很方便。

再来看service(我们以service-a为例,service-b与service-a类似,就不复述了)

//service-a 构建配置
dependencies {
    compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-netflix-eureka-client', version: '1.4.0.RELEASE'
}

//service-a主启动
package com.github.since1986.learn.cloud.service.a;

import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

@EnableScheduling
@SpringBootApplication
public class App {

    private final EurekaClient eurekaClient;

    @Autowired
    public App(EurekaClient eurekaClient) {
        this.eurekaClient = eurekaClient;
    }

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

    @Scheduled(initialDelay = 1000 * 60 * 3, fixedRate = 1000 * 30)
    public void printInstanceInfo() {
        InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka("com-github-since1986-learn-cloud-service-b", false);
        System.out.println(String.format("发现了 服务名为 %s IP为 %s 的服务.", instanceInfo.getAppName(), instanceInfo.getIPAddr()));
    }
}
#Spring Cloud配置
spring:
  application:
    name: com-github-since1986-learn-cloud-service-a

server:
  port: 8002

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8001/eureka/

同样,我们还来看主启动,由于我们想测试一下是否能发现别的服务,所以在住启动文件里,我们写了一些调用EurekaClient来做发现的代码,除去这些代码,实际上作为一个普通service,我们在Java中不用写任何特殊的代码,只要配置好starterapplication.yml就自动会注册与发现了,太省心了😆

写好了Eureka服务器和两个service,我们来启动,看一下效果吧,如下图所示我们把三个模块分别建立启动配置:

然后启动,访问我们在application.yml中设置好的Eureka服务器的地址,然后可以看到Spring Cloud为Eureka服务器做的内置的UI,上边可以看到在服务器上注册了哪些service实例。

然后,回过头来,我们来看service-a中我们写的测试服务发现的代码:

@Scheduled(initialDelay = 1000 * 60 * 3, fixedRate = 1000 * 30)
public void printInstanceInfo() {
    InstanceInfo instanceInfo = eurekaClient.getNextServerFromEurek("com-github-since1986-learn-cloud-service-b", false);
    System.out.println(String.format("发现了 服务名为 %s IP为 %s 的服务.", instanceInfo.getAppName(), instanceInfo.getIPAddr()));
}

在这段代码中,我们每30秒尝试发现名为com-github-since1986-learn-cloud-service-b的服务的名字和IP,其中com-github-since1986-learn-cloud-service-b是服务service-b在Eureka中注册的名字,如果正确的话,会在标准输出中输出发现的service-b的名字与IP

至此,我们就完成了一个初步的注册与发现了,在Spring Boot/Cloud体系中的注册与发现是不是非常简单啊😜

多实例eureka server + 多实例service

完成了单实例的注册与发现,我们再来看一下多实例的(全部代码在我的Github上

多个实例与单实例只在application.yml中有所不同,其他的代码区别不大,就不贴了,看一下application.yml吧。

eureka-server的application.yml

spring:
  profiles: eureka1
  application:
    name: com-github-since1986-learn-cloud-eureka-server #注意命名要符合RFC 2396,否则会影响服务发现 详见https://stackoverflow.com/questions/37062828/spring-cloud-brixton-rc2-eureka-feign-or-rest-template-configuration-not-wor

server:
  port: 8001

eureka:
  instance:
    hostname: eureka1
  client:
    serviceUrl:
      defaultZone: http://eureka2:8002/eureka/,http://eureka3:8003/eureka/
    register-with-eureka: true #需要配置这一项才生效(虽然默认这个配置是true,但是可能默认配置被覆盖掉了所以未生效)
---
spring:
  profiles: eureka2
  application:
    name: com-github-since1986-learn-cloud-eureka-server #注意命名要符合RFC 2396,否则会影响服务发现 详见https://stackoverflow.com/questions/37062828/spring-cloud-brixton-rc2-eureka-feign-or-rest-template-configuration-not-wor

server:
  port: 8002

eureka:
  instance:
    hostname: eureka2
  client:
    serviceUrl:
      defaultZone: http://eureka1:8001/eureka/,http://eureka3:8003/eureka/
    register-with-eureka: true #需要配置这一项才生效(虽然默认这个配置是true,但是可能默认配置被覆盖掉了所以未生效)
---
spring:
  profiles: eureka3
  application:
    name: com-github-since1986-learn-cloud-eureka-server #注意命名要符合RFC 2396,否则会影响服务发现 详见https://stackoverflow.com/questions/37062828/spring-cloud-brixton-rc2-eureka-feign-or-rest-template-configuration-not-wor

server:
  port: 8003

eureka:
  instance:
    hostname: eureka3
  client:
    serviceUrl:
      defaultZone: http://eureka1:8001/eureka/,http://eureka2:8002/eureka/
    register-with-eureka: true #需要配置这一项才生效(虽然默认这个配置是true,但是可能默认配置被覆盖掉了所以未生效)

这段配置比较长,仔细看一下,这其实包含了三段类似重复的配置,中间以---分割,并且每一段中都有profiles: eureka<n>这个配置项,这其实是配置了一个Spring Cloud的profile,这个配置文件里有3个profile,实际上也就是配置了3个实例节点,在idea的运行配置中我们选择激活某一个profile,就会启动对用的节点,这样,我们只要配置多个启动项就可以在一套工程里启动3个实力节点了。

然后再来注意每一个profiledefaultZone这一配置项,在多实例的情境下,我们让每一个Eureka服务器都在集群中的其他服务器上注册,集群中的所有服务器会相互同步状态,这样即便一台服务器掉了,还有其他的服务器顶着,这样就保证了注册与发现服务的可用性,这也是我们要配置多实例的原因。

另外还有一点需要注意的,由于咱们是在本地机器上做测试,所以所有Eureka服务器的机器名是相同的,然而要想让多个实例跑起来主机名是不能相同的,参见hostname配置项,这就很矛盾了,但是我们也有办法解决,修改本机的hosts文件就好了,让本机新加出三个不同的host name出来:

#hosts配置
127.0.0.1	eureka1
127.0.0.1	eureka2
127.0.0.1	eureka3

做好了这些,3个Eureka服务器实例就可以跑起来了,service-a和service-b也是如法炮制就好了。最后,我们会一共跑起来3*3=9个实例(我的机子内存小,一下跑起来9个实例感觉机子会卡 😅)

启动起来我们照样,看一下Eureka的UI:

可以看到,连上服务器自身,所有service都被注册了上去,同时,3个Eureka实例也是相互备份的:(可以停掉某个服务,然后刷新这个UI看看变化)

我们还可以操作Eureka服务器提供的REST API来手工控制注册与发现.(API列表见官方文档

调用/eureka/apps可以看到所有注册的服务的信息

同时,我们再来注意测试服务发现的代码,这里略有不同了:

@Scheduled(initialDelay = 1000 * 10, fixedRate = 1000 * 30)
public void printInstanceInfo() {
    List<InstanceInfo> instanceInfos = eurekaClient.getInstancesByVipAddress("com-github-since1986-learn-cloud-service-b", false);
    instanceInfos.forEach(instanceInfo -> {
        System.out.printf("发现了 %s: %s \n", instanceInfo.getAppName(), instanceInfo.getIPAddr());
    });
    System.out.println();
}

这回由于是多实例了,所以我们由一个服务注册名能发现多个实例,作为实验,你可以停掉其中某个实例,来看看会有什么变化。

深入了解Eureka

Eureka为我们的微服务提供了注册与发现服务,大多数情况下,我们只要用就好了,但是,一旦在使用过程中遇到了问题,如果不知道其运作的机理,我们肯定会两眼一抹黑,不知道解决问题的方向。因此,让我们来通过阅读官方文档大致了解一下Eureka运作的机理吧。

Eureka高层架构

我们来看官方给出的High level architecture,也就是高层架构(所谓高层架构我理解的就是相对于源码级实现的架构,在逻辑层面的架构)的说明:

High level architecture

The architecture above depicts how Eureka is deployed at Netflix and this is how you would typically run it. There is one eureka cluster per region which knows only about instances in its region. There is at the least one eureka server per zone to handle zone failures.

Services register with Eureka and then send heartbeats to renew their leases every 30 seconds. If the client cannot renew the lease for a few times, it is taken out of the server registry in about 90 seconds. The registration information and the renewals are replicated to all the eureka nodes in the cluster. The clients from any zone can look up the registry information (happens every 30 seconds) to locate their services (which could be in any zone) and make remote calls.

先来看第一段,大致意思是:在每一个region(AWS”区域”)中只跑一个Eureka的cluster(集群),每个region中的Eureka cluster只负责自己region中的instance(实例)(?这一点官方没说的太明白是什么实例,我理解的是”服务实例”),在region中下分的每一个zone(AWS”可用区域”)里至少包含一个eureka server,来保证可用性(因为zone是AWS中保证可用性的单元,也就是说在同一个region下各个zone可用性是相互独立的,一个zone掉线了不会影响另一个zone,这样就保证了可用性)。

在这段官方文档里面出现的region和zone的概念都是AWS中的概念。因为Eureka设计之初就是为了跑在AWS上的,所以会有这些AWS的概念,我们在开发自己的服务时,想必不会跑在AWS上的,也不必过于纠结与具体概念,这一点,Spring Cloud通过提供默认值的方式替我们处理好了,在Spring Cloud的Eureka客户端集成处理中,为region默认设置提供了”us-east-1”这个值,为zone默认提供了了”defaultZone”这个值,也就是说除非你十分需要,否则不再用手动设置region和zone了,用默认值就好了,这也体现了”约定先于配置”的理念(参看源码org.springframework.cloud.netflix.eureka.EurekaClientConfigBean)。如果实在需要保证极高的可用性的话,大可以参照AWS的region和zone的概念来设计架构并且自行配置eureka的region和zone。(在官方给出的架构图实例说明中,只用到了一个region:us-east-1,分为了3个zone:us-east-1c、us-east-1d、us-east-1e,每一个zone上都是一个eureka server实例)

再来看第二段,大致的意思是:服务注册到eureka server上以后,每30秒发送一次心跳以刷新租约,如果发现在90内没有心跳发送了,服务器会注销此服务的注册信息。注册信息每30秒会在eureka server集群中的各个节点复制。另外服务的发现可以跨zone进行。这一段里没有什么晦涩的概念,大致告诉我们了服务注册是以怎样的模式运作的,知道就好了。

了解服务消费端与服务提供端的通讯

How does the application client and application server communicate?

The communication technology could be anything you like. Eureka helps you find the information about the services you would want to communicate with but does not impose any restrictions on the protocol or method of communication. For instance, you can use Eureka to obtain the target server address and use protocols such as thrift, http(s) or any other RPC mechanisms.

从上边的官方文档中,我们可以看到:Eureka并不会干预服务之间的通讯协议的选择,随便你用RPC、http还是什么方式,Eureka只管帮一个服务发现想要通讯的另一个目标服务,至于发现之后两者之间怎样建立通讯,Eureka就不管了。

扩展阅读

我们在前面做实例时,会发现,Eureka包含了多实例集群的配置启动方式,实际上,在生产环境中,我们一般也不会只跑单实例的,可是,为什么要这样设计呢?是为了预防“单点故障”,那么,“单点故障”是什么呢。

关于“单点故障”

single point of failure,(单点故障,缩写SPOF)是指系统中某一部件一旦失效,就会让整个系统都无法运作,换句话说,单点故障即会导致整体故障。其实通俗一点,就是“把所有鸡蛋都放在了一个篮子里”。

那么如何解决“单点故障”问题?“不要把鸡蛋放在一个篮子里”就好了。“一个篮子”就是单点,而一个篮子变成多个篮子就是“冗余”(“replicate”),而冗余则是解决单点问题的核心思路。我们可以回想一下在做demo时,多实例Eureka服务器UI界面中available-replicas,这个就是现实的当前这个服务器的冗余。

总结

本篇文章中我们学习了在Spring Cloud中怎样做服务注册与发现,并且以两个demo来实践了一下,在下一篇文章「Spring Cloud与微服务学习笔记-服务调用」中,我们会学习怎样调用已经发现的服务。

参考资料:

Spring Cloud技术分析(2)—— 服务治理实践

消除小型 Web 站点单点故障(Single Point of Failure)

单点故障

Understanding eureka client server communication

Spring Cloud技术分析(1)——服务治理

有没有什么单词让你心里一动?

服务注册与发现

基于 Eureka 的服务注册与发现调研

Spring Cloud Eureka — 服务发现

Spring Cloud Eureka:服务注册与发现

Eureka at a glance

深入理解Eureka之源码解析

彩蛋

有没有想过,为什么网飞给自己的服务发现与注册组件起名为“Eureka”呢?,一个很文艺的名字。其实这里边是有一个梗的。

Eureka(希腊语:εὕρηκα;拉丁化:Eureka;词义:“我发现了”)据传,阿基米德在洗澡时发现浮力原理,高兴得来不及穿上裤子,跑到街上大喊:“Eureka(我发现了)!”。

网飞的开发人员取了这个“发现”的典故来为自己的产品命名,不得不说还挺有才学和浪漫精神的。这里插一句题外话:其实不只是网飞用过“Eureka”这个梗,在前两年的一部影视作品《星际穿越》中也使用了“Eureka”这个桥段,在影片中,墨菲解出了方程式后,挥洒稿纸,激动地喊出了:“Eureka”,感兴趣的同学可以回去重温一下这部经典的片子,并且留意一下这个桥段。




原文地址:访问原文地址
快照地址: 访问文章快照