今天栽了老大一个坑,在使用Eureka时,Client注册时既没有填主机名,也没有填IP地址,恰巧,2个微服务项目没有注册到同一个域,一个是localhost,一个是IP地址,导致Session无法共享,结果就是全班的同学使用相同的代码,有的同学可以正常共享Session,有的却不行,而我正巧是无法使用共享的Session的……其实,在实际生产环境中是不会出现这个问题的(不光是各微服务项目不会不在一个域,而且,所有的身份验证都会在网关完成,不会出现先访问网关,发现权限不足,然后去其它微服务项目中去验证身份的问题),但是,做练习的时候,想着练习项目都是本机跑起来的,就没管这个事,结果,被坑了几个小时,偷懒,活该!自罚完整文章一篇,免得以后再犯!
1. Eureka简介
Eureka是一个服务发现框架,在大型应用中,会将整个项目拆分为多个微服务项目,每个微服务项目都是可以独立运行的,例如处理“用户”数据的是一个微服务项目,处理“积分”数据的是另一个微服务项目,且多个项目运行在不同的服务器主机上,使用Eureka就可以便捷的管理微服务清单,使得每个微服务项目都能明确所需要调用的服务提供者项目在哪里并实现调用。
关于Eureka的作用,可以把Eureka作为一个“注册中心”,每个微服务项目都会在这个“注册中心”进行“注册”,则Eureka就能记录下当前共启动了多少个微服务项目,各运行在哪台服务器主机上,形成“注册表”,而各微服务项目将可以通过“抓取”以同步到这个“注册表”,从而实现“消费者”项目可以明确“提供者”项目的位置并进行调用。
2. 创建Eureka注册中心
SpringBoot对于Eureka提供了非常好的支持,通过极简的配置即可使用Eureka!
首先,在项目中通过New Module
创建Eureka注册中心项目,如果是第1次使用Spring Cloud家庭的依赖,推荐通过SpringBoot的创建向导来创建子模块项目,以便于添加依赖:
3. 配置Eureka注册中心
生成的straw-eureka-center
中已经配置好了所需的依赖,包括在中配置版本,在
中添加依赖,在
中管理依赖的版本。
先将pom.xml
里关于Spring Cloud家庭的依赖都放在父级项目中进行管理,并且,改为使用Hoxton.SR3
版本(新版SpringBoot默认使用Hoxton.SR6
版本,目前已知的问题有:当响应正文时,响应头为Content-Type: application/xhtml+xml
,导致原本响应的JSON数据格式有误,由于没有及时关注新版本的改动,解决方案暂时未知)。
然后,在straw-eureka-center
当前子模块项目中的pom.xml
为:
<?xml version="1.0" encoding="UTF-8"?> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 cn.tedu straw 0.0.1-SNAPSHOT cn.tedu straw-eureka-center 0.0.1-SNAPSHOT straw-eureka-center org.springframework.cloud spring-cloud-starter-netflix-eureka-server
简单的说,当前项目依赖于父项目后,只需要添加eureka-server
的依赖即可。
然后,在application.properties
中添加配置:
# 服务端口号,在不使用Eureka Server集群的情况下,强烈推荐使用8761# 如果需要了解集群,请参考本文的《5.4. Eureka Server集群》server.port=8761# 默认情况下,Eureka Server会将自身也进行注册并抓取注册表,在不使用Eureka Server集群的情况下是没有意义的,需要将这2个属性设置为falseeureka.client.fetch-registry=falseeureka.client.register-with-eureka=false
并在启动类中添加@EnableEurekaServer
注解:
@SpringBootApplication@EnableEurekaServerpublic class StrawEurekaCenterApplication { public static void main(String[] args) { SpringApplication.run(StrawEurekaCenterApplication.class, args); }}
最后,启动项目,通过 http://localhost:8761 即可看到Eureka注册中心的状态页面:
至此,Eureka Server项目即可告一段落。
4. 配置其它子模块项目
当Eureka项目已经启动后,除非需要添加更加详细的配置信息,否则,以上straw-eureka-center
项目可以不做任何调整,保持它处理运行状态就可以了,其它的子模块项目就可以在Eureka中注册,从而可以被发现并获取注册表!
先将straw-resource
配置为节点,需要在pom.xml
中添加Eureka客户端的依赖:
org.springframework.cloud spring-cloud-starter-netflix-eureka-client
处理以上依赖时,把straw-eureka-center
中的依赖复制过来,把中的
server
改成client
即可。
然后,在straw-resource
的application.properties
中添加:
spring.application.name=resourceeureka.client.service-url.defaultZone=http://localhost:8761/eureka
其实,以上2个属性都可以不必配置!
如果没有配置spring.application.name,在Eureka Server的实例列表中会显示为UNKNOWN,非常不友好,所以推荐配置;
如果没有配置eureka.client.service-url.defaultZone,会默认的从本机(localhost)查找Eureka Server,且由于Eureka Server的默认端口是8761,也会连接到此端口,至于以上配置值最后的eureka,是固定值,所以,只要是使用本机作为Eureka Server且使用8761端口,以上配置是可以省略的,但是,今天就是栽在这些“可以省略”的坑上了,所以,还是显式的配置出来,以后在实际生产环境中肯定不可能使用本机作为Eureka Server,就是必须要配置的了!
另外,网上有些文章中,Eureka Client的启动类还会添加@EnableEurekaClient注解,这个是真没有必要了,新款本的SpringBoot项目中,只要添加了eureka-client的依赖,就会自动启用,不需要通过注解显式的声明这一点,但是,Eureka Server是必须添加@EnableEurekaServer的,也许在以后的版本中也会省掉。
配置完成后,启动这个项目,且此前已经启动straw-eureka-center
项目,则在 http://localhost:8761 的列表中可以看到straw-resource
项目已经被注册:
需要注意:默认情况下,Eureka Client注册的心跳周期是30秒,所以,启动了straw-eureka-center
再启动straw-resource
后,在 http://localhost:8761 也不一定能马上发现straw-resource
,最多等待30秒后即可发现。
当第1个子模块项目第1次注册时,在短时间内,在 http://localhost:8761 页面可能会出现警告,这是因为Eureka Server的自我保护机制所导致的,过一段时间就会消失。关于自我保护机制,可参考本文的《5.5. Eureka Server自我保护机制》。
然后,就是今天被坑的地方了!应该显式的配置各Eureka Client的主机名或IP地址,使得各Eureka Client是统一的,是在同一个域的,否则,就可能导致无法共享Session的问题!
如果需要指定注册的主机名,需要在各Eureka Client中添加配置:
# 不使用IP地址eureka.instance.prefer-ip-address=false# 指定主机名eureka.instance.hostname=localhost
如果要指定为IP地址,则配置为:
# 使用IP地址eureka.instance.prefer-ip-address=true# 指定IP地址eureka.instance.ip-address=127.0.0.1
还可以指定显示在Eureka Server状态列表的实例名称,例如:
# 指定实例名称:应用名称:主机名:服务端口号eureka.instance.instance-id=${spring.application.name}:${eureka.instance.hostname}:${server.port}
或者:
# 指定实例名称:应用名称:主机IP地址:服务端口号eureka.instance.instance-id=${spring.application.name}:${eureka.instance.ip-address}:${server.port}
以“不使用IP地址”、“指定主机名”为例,在Eureka Server的列表中显示效果例如:
5. 进一步了解Eureka
5.1. 相关术语:
- Eureka Server
Eureka服务端项目,可以称之为“注册中心”;在同一套架构中,可能存在多个Eureka Server;
- Eureka Client
在Eureka Server中注册的微服务项目,也可称之为“实例”或“节点”;
- regitry(注册)
Eureka Client向Eureka Server提交注册信息,主要包括实例名、主机地址、端口号等信息,详细数据请参考本文《5.6. Eureka Client注册时底层提交数据》;
- 注册表
记录各Eureka Client信息的数据,也可称之为“实例列表”;
- fetch(抓取)
Eureka Client从Eureka Server中获取被注册的注册表;
Eureka Client会在本地缓存注册表,以保证当Eureka Server不可用时,依然能明确其它实例的状态。
- 续订租约
Eureka Client每间隔一段时间会向Eureka Server发送消息进行“续约”,通过“续约”表示当前Eureka Client处于正常运行状态;
- 心跳周期
Eureka Client向Eureka Server发送“续约”的间隔时间就是“心跳周期”,默认为30秒。
- 服务剔除
如果Eureka Server在若干个心跳周期之后都没有接收到“续约”,会将其从注册表中移除,即注销该实例。
5.2. 常用配置
# 服务续约任务的调用间隔时间,默认为30秒eureka.instance.lease-renewal-interval-in-seconds=30# 服务失效的时间,默认为90秒。eureka.instance.lease-expiration-duration-in-seconds=90
5.3. Eureka Client缓存注册表
Eureka Client会从Eureka Server获取注册表信息并在本地缓存,然后,客户端使用该信息来查找其他服务。
在缓存注册表时,Eureka Client会通过获取最后一个获取周期和当前获取周期之间的增量更新,增量信息在Eureka Server中保持更长时间(约3分钟),因此增量提取可能会再次返回相同的实例,Eureka Client自动处理重复信息。
5.4. Eureka Server集群
如果Eureka Server宕机后,各节点依然可以通过此前缓存的注册表进行远程调用,并不会因为Eureka Server宕机而导致整个系统变得不可用,但是,如果此时出现某些服务上线或下线,各节点缓存的注册中并没有新的信息,就会导致不可用的问题。
为了提高Eureka Server的可用性,可以架设Eureka Server集群,即创建多个Eureka Server项目且同时运行!
其实,每一个Eureka Server也是一个Eureka Client,可以将自身进行注册,也可以抓取注册表,Eureka Server集群的工作原理就是各Eureka Server之间相互注册,并互相抓取注册表!
假设存在3个Eureka Server,分别运行在192.168.1.101:8701
、192.168.1.102:8702
、192.168.1.103:8703
,在Eureka Server 1中的主要配置如下:
# 指定当前Eureka Server运行的端口
server.port=8701
# 指定实例名称
eureka.instance.hostname=eureka-server-1
# 指定注册中心的地址,是另2个Eureka Server的地址,各地址使用逗号分隔
eureka.client.service-url.defaultZone=http://192.168.1.102:8702/eureka, http://192.168.1.103:8703/eureka
# 自己需要注册
eureka.client.register-with-eureka=true
# 自己会抓取注册表
eureka.client.fetch-registry=true
同理,再配置第2个,在Eureka Server 2中的主要配置如下:
server.port=8702eureka.instance.hostname=eureka-server-2eureka.client.service-url.defaultZone=http://192.168.1.101:8701/eureka, http://192.168.1.103:8703/eurekaeureka.client.register-with-eureka=trueeureka.client.fetch-registry=true
在Eureka Server 3中的主要配置如下:
server.port=8703eureka.instance.hostname=eureka-server-3eureka.client.service-url.defaultZone=http://192.168.1.101:8701/eureka, http://192.168.1.102:8702/eurekaeureka.client.register-with-eureka=trueeureka.client.fetch-registry=true
最后,在Eureka Client中把以上3个Eureka Server都添加到配置:
eureka.client.service-url.defaultZone=http://192.168.1.101:8701/eureka, http://192.168.1.102:8702/eureka, http://192.168.1.103:8703/eureka
5.5. Eureka Server自我保护机制
Eureka Server在运行期间会统计“丢失心跳比例”,任何连续三次心跳续订失败的Eureka Client都被认为有不干净的终止,如果15分钟内超过85%的节点都没有正常的心跳,则会开启“自我保护机制”,在Eureka Server的主页会出现如下提示:
其中警告信息如下:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
大意如下:
注意!Eureka可能错误的将已经下线的实例声明为在线,为了安全起见,由于续约尚且低于阈值,因此实例不会过期。
首先,这个保护机制是可以取消的:
eureka.server.enable-self-preservation=false
以上属性默认值是true
,也就是“自我保护机制”默认即开启,如果关闭了,还可以自定义“清除丢失心跳服务的间隔时间”:
eureka.server.eviction-interval-timer-in-ms: 10000
出现自我保护的原因可能有:
启动了Eureka Server,却没有启动任何Eureka Client,对于Eureka Server来说,丢失心跳的比例是100%;
启动整个架构时,由于网络波动、节点服务器负载等故障,导致大量节点不可用;
异常关闭节点服务器,例如节点服务器断电、死机,或者直接杀进程等。
之所以会存在自我保护机制,是为了确保灾难性网络事件不会消除注册表,并将其传播到下游所有Eureka Client。
关于自我保护,会有如下表现:
Eureka Server不再从注册表中移除因为长时间没收到心跳的Eureka Client;
Eureka Server仍然能够接受新Eureka Client的注册和查询请求,但是不会被同步到其它Eureka Client上;
当网络稳定时,当前Eureka Server新的注册信息会被同步到所有Eureka Client。
前提是大量节点长时间无心跳,而以上特征简单的来说,就是:
即使还有坏的节点,注册表也不更新;
各节点依然可以注册,但新的注册表不会同步到各节点,各节点还是使用此前缓存的注册表;
网络稳定后会自动恢复正常。
所以,进入自我保护机制后,再出现任何故障节点,对于其它节点来说也是未知的,仍可能尝试远程调用,当然,结果大概率会失败,同时,上线了新的节点,其它节点也不会知道!所以,在Eureka Server进入自我保护机制后,应该及时排查各节点是否出现故障,如果需要下线某个节点,Eureka协议要求Client在永久离开时执行显式取消注册操作,需要通过API显式下线:
DiscoveryManager.getInstance().shutdownComponent();
5.6. Eureka Client注册时底层提交数据
<?xml version="1.0" encoding="UTF-8"?><xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" attributeFormDefault="unqualified"> <xsd:element name="instance"> <xsd:complexType> <xsd:all> <xsd:element name="hostName" type="xsd:string" /> <xsd:element name="app" type="xsd:string" /> <xsd:element name="ipAddr" type="xsd:string" /> <xsd:element name="vipAddress" type="xsd:string" /> <xsd:element name="secureVipAddress" type="xsd:string" /> <xsd:element name="status" type="statusType" /> <xsd:element name="port" type="xsd:positiveInteger" minOccurs="0" /> <xsd:element name="securePort" type="xsd:positiveInteger" /> <xsd:element name="homePageUrl" type="xsd:string" /> <xsd:element name="statusPageUrl" type="xsd:string" /> <xsd:element name="healthCheckUrl" type="xsd:string" /> <xsd:element ref="dataCenterInfo" minOccurs="1" maxOccurs="1" /> <xsd:element ref="leaseInfo" minOccurs="0"/> <xsd:element name="metadata" type="appMetadataType" minOccurs="0" /> xsd:all> xsd:complexType> xsd:element> <xsd:element name="dataCenterInfo"> <xsd:complexType> <xsd:all> <xsd:element name="name" type="dcNameType" /> <xsd:element name="metadata" type="amazonMetdataType" minOccurs="0"/> xsd:all> xsd:complexType> xsd:element> <xsd:element name="leaseInfo"> <xsd:complexType> <xsd:all> <xsd:element name="evictionDurationInSecs" minOccurs="0" type="xsd:positiveInteger"/> xsd:all> xsd:complexType> xsd:element> <xsd:simpleType name="dcNameType"> <xsd:restriction base = "xsd:string"> <xsd:enumeration value = "MyOwn"/> <xsd:enumeration value = "Amazon"/> xsd:restriction> xsd:simpleType> <xsd:simpleType name="statusType"> <xsd:restriction base = "xsd:string"> <xsd:enumeration value = "UP"/> <xsd:enumeration value = "DOWN"/> <xsd:enumeration value = "STARTING"/> <xsd:enumeration value = "OUT_OF_SERVICE"/> <xsd:enumeration value = "UNKNOWN"/> xsd:restriction> xsd:simpleType> <xsd:complexType name="amazonMetdataType"> <xsd:all> <xsd:element name="ami-launch-index" type="xsd:string" /> <xsd:element name="local-hostname" type="xsd:string" /> <xsd:element name="availability-zone" type="xsd:string" /> <xsd:element name="instance-id" type="xsd:string" /> <xsd:element name="public-ipv4" type="xsd:string" /> <xsd:element name="public-hostname" type="xsd:string" /> <xsd:element name="ami-manifest-path" type="xsd:string" /> <xsd:element name="local-ipv4" type="xsd:string" /> <xsd:element name="hostname" type="xsd:string"/> <xsd:element name="ami-id" type="xsd:string" /> <xsd:element name="instance-type" type="xsd:string" /> xsd:all> xsd:complexType> <xsd:complexType name="appMetadataType"> <xsd:sequence> <xsd:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/> xsd:sequence> xsd:complexType>xsd:schema>