微服务架构spring cloud - 服务治理 Eureka(二)

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/

猜你喜欢

转载自blog.csdn.net/m0_37834471/article/details/81261226