1.什么是服务治理
实现各个微服务实例的自动化注册与发现,如果没有服务治理那么只能静态配置服务地址,麻烦且容易出错,不易维护。
2.服务治理的两大核心
服务注册
例子:用户注册会员,会员注册中心就有了这个用户的详细信息
也就是我们通常所说的注册中心,每个微服务会将自己的信息(主机与端口号、版本号、通信协议等等)登记到注册中心例如192.168.88.8:8000和192.168.88.8:8001。当服务把自身信息向注册中心注册后,那么注册中心就会维护已经注册的注册清单。
服务发现
例子:管理员要找这个用户的家庭地址,通过这个用户名字在会员注册中心就可以查询到用户地址
服务发现就是说当我们要调用某一个服务的时候,由于有了注册中心的缘故。我们不再需要记住真正的服务地址(ip+端口),只需要向注册中心询问被调用的服务地址信息就可以了。也就是说我们往往只需要一个服务名字,然后通过这个名字去注册中心询问拿到地址。
3.spring cloud Eureka与Netfix Eurek的认识与联系
由于spring cloud是个大项目,它是由多个子项目组成的,每个子项目由子项目自己负责更新迭代。所以有了Netflix这个组件,里面就包括了Eureka。spring cloud Eureka 则是由Netfix Eureka来实现的。 Netflix oss是开源组件,解决了分布式的多种功能的解决方案。由于spring 系列的一贯原则是提供接口不提供实现,用来达到最高的系统兼容度,奉行不重复造车轮子的理解,所以对于服务治理的实现可以由多种选择,这里选择的是Eureka,而这个Eureka就是Netfix公司所实现的开源项目,成为Netfix oss开源项目。
4.Eureka的基础认识和客户端与服务端
Eureka组件包括了客户端和服务端,主要是用java实现的组件,所以适用于java实现的分布式。不过Eureka的服务端是可以共用的,由于使用了Restful api 所以可以共用一个服务端。所以我们只需要实现不同的客户端,例如node.js的客户端组件。
Eureka 服务端
也就是服务注册中心,所有服务注册中心都支持高可用的配置(高可用的意思就是常年不停机不死机咯),通过集群部署的方式当有分片故障的时候,其他分片继续工作,当故障解决后,开始通过异步方式复制各自的状态
Eurekak 客户端
作用肯定就是将自身的信息,也就是当前客户端的信息注册到Eureka服务端也就是注册中心那里。通过心跳方式告知服务端来更新它的服务契约,方式服务崩溃而不知道。允许从服务端查询自己的服务信息,缓存到本地并且刷新服务状态,例如配置信息的更新, 客户端包括了服务注册和服务发现的功能。
5.服务治理的专有名词
服务提供者
服务注册:通过rest的方法将自己的一些相关信息(服务信息),注册中心也就是eureka server 接受到这样的信息后,会将这些数据存储在一个双层map结构,第一层key是服务名字,第二层key是服务的具体实例名字
服务同步:比如有多个服务提供者注册了多个服务注册中心,由于服务注册中心之间相互注册,所以当有一个服务提供者注册后,注册中心会将这个请求转发到其他已经注册的服务注册中心也就是Eureka server
服务续约:利用心跳来告诉注册中心自己还活着,防止注册中心将自己踢出服务列表
服务消费者
获取服务::启动当前服务后,会发送一个REST请求给服务注册中心,获取到一份由注册中心维护的服务清单,该清单在注册中心以每隔30秒更新一次
eureka.client.fetch-registry=true 是否获取服务清单
eureka.client.registry-fetch-interval-seconds=30 缓存清单间隔更新时间(秒)
服务调用:获取到清单后,服务消费者就可以通过服务提供者的服务名字去调用服务接口,例如通过ribbion的轮询方式去实现去掉用,并且实现了负载均衡。而这个访问实例的选择,又有zone何region的区别,一个region包括多个zone,每一个服务都注册到一个zone里面。在调用服务提供者的时候,优先访问同一个Zone的服务提供者。
服务下线:当服务正常关闭的时候,会通过发送一个REST请求告诉Eureka server 要下线了,Eureka Server接受到信息后,会将该服务设置为down状态,并发出下线时间传播出去。
服务注册中心
失效剔除:由于大多数情况下,多数服务都会由于不正常的原因而关闭了服务,所以服务注册中心为了保证当前服务清单的正确性,会每隔60秒将当前超时的没有续约的服务剔除。
自我保护:运行期间,当某个服务的心跳失败比例15分钟内占85%,eureka server会将当前实例保存下来,如果在保护期间有客户端调用那么就很容易出错,所以需要一些机制去预防。
eureka.server.enable-self-preservation=false 是否关闭自我保护机制
6.源码分析
Eureka 客户端 DiscoveryClient
配置注册中心Eureka server 的URL列表
从源码大致可以看出,首先获取region,接着获取zone数组。之后才加载Eureka server的具体地址
eureka.client.region 定义region的名字,默认为default
eureka.client.availability-zones= , , , 默认为defaultZone
具体细节不再阐述
eureka.client.region=region2
#配置region2内的可用zone
eureka.client.availability-zones.region2=zone2-1,zone2-2
#配置每个zone的注册中心的地址
eureka.client.service-url.zone2-1=http://server2-1:1112/eureka/
eureka.client.service-url.zone2-2=http://server2-2:1113/eureka/
eureka.client.region=region2
eureka.client.availability-zones.region2=zone2-2,zone2-1
eureka.client.service-url.zone2-1=http://server2-1:1112/eureka/
eureka.client.service-url.zone2-2=http://server2-2:1113/eureka/
public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
List<String> orderedUrls = new ArrayList();
String region = getRegion(clientConfig);
String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
if (availZones == null || availZones.length == 0) {
availZones = new String[]{"default"};
}
logger.debug("The availability zone for the given region {} are {}", region, availZones);
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
int currentOffset = myZoneOffset == availZones.length - 1 ? 0 : myZoneOffset + 1;
while(currentOffset != myZoneOffset) {
serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
if (currentOffset == availZones.length - 1) {
currentOffset = 0;
} else {
++currentOffset;
}
}
if (orderedUrls.size() < 1) {
throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
} else {
return orderedUrls;
}
}
服务注册
发起了一个http的请求,传入一个InstanceInfo对象也就是服务信息了。
boolean register() throws Throwable {
logger.info("DiscoveryClient_{}: registering service...", this.appPathIdentifier);
EurekaHttpResponse httpResponse;
try {
httpResponse = this.eurekaTransport.registrationClient.register(this.instanceInfo);
} catch (Exception var3) {
logger.warn("DiscoveryClient_{} - registration failed {}", new Object[]{this.appPathIdentifier, var3.getMessage(), var3});
throw var3;
}
if (logger.isInfoEnabled()) {
logger.info("DiscoveryClient_{} - registration status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
服务获取与服务续约
第一个服务获取判断的条件就是我们在配置文件那里设置的
eureka.client.fetch-registry=true 是否获取注册中心额服务清单
eureka,client.registry-fetch-interval-seconds=30 获取服务清单的刷新间隔时间,确保能访问健康的服务实例
第二个条件就是是否注册了,不难理解注册了服务肯定是需要心跳来维持服务的健康,所以服务获取和服务续约在同一个if条件下
eureka.instance.lease-renewal-interval-in-seconds 服务心跳时间,默认30秒刷新一次
eureka.instance.lease-expiration-duration-in-seconds 注册中心设置的剔除任务时间,保证服务健康有效
private void initScheduledTasks() {
int renewalIntervalInSecs;
int expBackOffBound;
if (this.clientConfig.shouldFetchRegistry()) {
renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();
expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
this.scheduler.schedule(new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
}
if (this.clientConfig.shouldRegisterWithEureka()) {
renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: renew interval is: {}", renewalIntervalInSecs);
this.scheduler.schedule(new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
this.instanceInfoReplicator = new InstanceInfoReplicator(this, this.instanceInfo, this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
this.statusChangeListener = new StatusChangeListener() {
public String getId() {
return "statusChangeListener";
}
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN != statusChangeEvent.getStatus() && InstanceStatus.DOWN != statusChangeEvent.getPreviousStatus()) {
DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
} else {
DiscoveryClient.logger.warn("Saw local status change event {}", statusChangeEvent);
}
DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate();
}
};
if (this.clientConfig.shouldOnDemandUpdateStatusChange()) {
this.applicationInfoManager.registerStatusChangeListener(this.statusChangeListener);
}
this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
服务注册中心
首先是对传过来分InstanceInfo 也就是服务信息进行一系列校验
然后调用register方法进行注册,不过首先是发布了一个包含服务信息的事件,之后将InstanceInfo的数据存储到ConcurrentHashMap数据结构里面,注册完毕。
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info, @HeaderParam("x-netflix-discovery-replication") String isReplication) {
logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
if (this.isBlank(info.getId())) {
return Response.status(400).entity("Missing instanceId").build();
} else if (this.isBlank(info.getHostName())) {
return Response.status(400).entity("Missing hostname").build();
} else if (this.isBlank(info.getIPAddr())) {
return Response.status(400).entity("Missing ip address").build();
} else if (this.isBlank(info.getAppName())) {
return Response.status(400).entity("Missing appName").build();
} else if (!this.appName.equals(info.getAppName())) {
return Response.status(400).entity("Mismatched appName, expecting " + this.appName + " but was " + info.getAppName()).build();
} else if (info.getDataCenterInfo() == null) {
return Response.status(400).entity("Missing dataCenterInfo").build();
} else if (info.getDataCenterInfo().getName() == null) {
return Response.status(400).entity("Missing dataCenterInfo Name").build();
} else {
DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
if (dataCenterInfo instanceof UniqueIdentifier) {
String dataCenterInfoId = ((UniqueIdentifier)dataCenterInfo).getId();
if (this.isBlank(dataCenterInfoId)) {
boolean experimental = "true".equalsIgnoreCase(this.serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
if (experimental) {
String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
return Response.status(400).entity(entity).build();
}
if (dataCenterInfo instanceof AmazonInfo) {
AmazonInfo amazonInfo = (AmazonInfo)dataCenterInfo;
String effectiveId = amazonInfo.get(MetaDataKey.instanceId);
if (effectiveId == null) {
amazonInfo.getMetadata().put(MetaDataKey.instanceId.getName(), info.getId());
}
} else {
logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
}
}
}
this.registry.register(info, "true".equals(isReplication));
return Response.status(204).build();
}
}
public void register(InstanceInfo info, int leaseDuration, boolean isReplication) {
this.handleRegistration(info, leaseDuration, isReplication);
super.register(info, leaseDuration, isReplication);
}
private void handleRegistration(InstanceInfo info, int leaseDuration, boolean isReplication) {
this.log("register " + info.getAppName() + ", vip " + info.getVIPAddress() + ", leaseDuration " + leaseDuration + ", isReplication " + isReplication);
this.publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration, isReplication));
}
7.配置详解
首先要清楚其实所有的微服务都是一个客户端包括注册中心,因为注册中心也存在相互注册的情况。
所以配置方面就从客户端开始下手
服务注册相关的配置信息:注册中心地址、服务获取的间隔时间、可用区域
服务实例相关的配置信息:包括服务实例的名称、ip地址、端口号、健康检查路径等
eureka服务端大部分情况无需配置
服务注册类配置(全部以eureka.client开头)
指定注册中心的地址
默认为http://localhost:8761/eureka/
单个注册中心:eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
多个注册中心:eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/,http://localhost:2222/eureka/
安全的注册中心: eureka.client.serviceUrl.defaultZone=http://<username>@<password>localhost:1111/eureka/
username 为安全配置的用户名,password为安全配置的密码
其他注册类配置
eureka.client.enable=true 开启eureka客户端
eureka.client.registryFetchIntervalSeconds=30 从Eureka服务端获取注册信息的间隔时间,单位秒
eureka.client.instanceInfoReplicationIntervalSeconds=30 更新实例信息的变化到Eureka服务端的间隔时间,单位秒
eureka.client.initialInstanceInfoReplicationIntervalSeconds=40 初始化实例信息到Eureka服务端的间隔时间,单位为秒
eureka.client.eurekaServiceUrlPollIntervalSeconds=300 轮询Eureka服务端地址更改的间隔时间,单位为秒。当我们与Spring Cloud Config配合,动态刷新Eureka的serviceURL地址时需要关注该参数
eureka.client.eurekaServerReadTimeOutSeconds=8 读取Eureka server信息的超时时间
eureka.client.eurekaServerConnectTimeOutSeconds=5 连接Eureka server的超时时间
eureka.client.eurekaServerTotalConnections=200 从eureka客户端到所有eureka服务端的所有连接总数
eureka.client.eurekaServerTotalConnectionsPreHost=50 从eureka客户端到每个eureka服务端的连接总数
eureka.client.eurekaConnectionIdleTimeOutSeconds=30 eureka服务端连接的空闲关闭时间,
eureka.client.heartbeatExecutorThreadPoolSize=2 心跳连接池的初始化线程数
eureka.client.heartbeatExecutorExponentialBackOffBound =10 心跳超时重试延迟时间的最大乘数值=
eureka.client.cacheRefreshExecutorThreadPoolSize=2 缓存刷新线程池的初始化线程数
eureka.client.cacheRefreshExecutorExponentialBackOffBound=10 缓存刷新重试延迟时间的最大乘数值
eureka.client.useDnsForFetchingServiceUrls=false 使用DNS来获取Eureka服务端的serviceUrl
eureka.client.registerWithEureka=true 是否要将自身的实例信息注册到Eureka的服务端
eureka.client.preferSameZoneEureka=true 是否偏好使用处于相同Zone的Eureka的服务端
eureka.client.filterOnlyUpInstance=true 获取实例时是否过滤,仅保留UP状态的实例
eureka.client.fetchRegistry=true 是否从Eureka服务端获取注册信息
服务实例类配置
eureka.instance.metadataMap.zone=shanghai 自定义key,value格式的元数据
eureka.instance.instanceId=${spring.cloud.client.hostname}:${spring.applicaiton.name}:${spring.applicaiton.instant_id}:${server.port} 为实例名的默认命名方式,是区别同一服务的多个实例的一个命名规则
eureka.instance.instanceId=${spring.cloud.client.hostname}:${random.int} 开启随机端口的实例名
端点配置,一般用于需要加路径前缀的时候,或者直接修改路径,或者使用https
managment.context-path=/hello
eureka.isntance.statusPageUrlPath=${managment.context-path}/info
eureka.isntance.healthCheckUrlPath=${managment.context-path}/health
eureka.isntance.healthCheckUrlPath=/myhealth
eureka.isntance.healthCheckUrlPath=https://${eureka.instacne.hostname}/health
其他配置
eureka.instance.preferIpAddress=false 是否优先使用IP地址作为主机名的标致
eureka.instance.leaseRenewallntervalInSeconds=30 Eureka客户端向服务端发送心跳的时间间隔,单位为秒
eureka.instance.leaseExpirationInSeconds=90 服务端在等待心跳的最大等待时间,超过则提出该服务
eureka.instance.nonSecurePort=80 非安全的通信端口
eureka.instance.securePort=443 安全的通信端口
eureka,instance.appname 服务名,默认取spring.application.name的配置信息,如果没有则为unknow
eureka.instance.hostname 主机名,不配置的时候根据操作系统的主机名获取
8.项目实战
创建一个eureka server,分为一个注册中心和多个注册中心的情况,一个注册中心的时候要禁用服务发现和服务注册的功能,注意到开启Eureka server的注解为@EnableEurekaServer
@EnableEurekaServer
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
server.port=1111
#当前服务主机名
eureka.instance.hostname=llg-1
##是否注册服务,由于本身为注册中心,所以禁用
#eureka.client.register-with-eureka=false
##检索服务,应该是检索服务信息,由于本身为注册中心,所以禁用
#eureka.client.fetch-registry=false
#服务注册中心的配置内容,指定服务注册中心的位置
eureka.client.service-url.defaultZone=http://llg-2:1112/eureka/
spring.application.name=eureka-server
server.port=1112
#当前服务主机名
eureka.instance.hostname=llg-2
##是否注册服务,由于本身为注册中心,所以禁用
#eureka.client.register-with-eureka=false
##检索服务,应该是检索服务信息,由于本身为注册中心,所以禁用
#eureka.client.fetch-registry=false
#服务注册中心的配置内容,指定服务注册中心的位置
eureka.client.service-url.defaultZone=http://llg-1:1111/eureka/
spring.application.name=eureka-server
eureka.instance.prefer-ip-address=false
创建eureka client客户端,注意到开启客户端注解为@EnableDiscoveryClient
@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
server.port=2223
spring.application.name=hello-service
eureka.client.service-url.defaultZone=http://llg-1:1111/eureka/,http://llg-2:1112/eureka/