Spring-Cloud之服务的注册与发现(Eureka)及其源码分析

关于Spring cloud和微服务的概念:

https://www.cnblogs.com/xiaojunbo/p/7090742.html

什么是Eureka?

Eureka是Netflix开源的一个RESTful服务,主要用于服务的注册发现。

有两个组件组成:Eureka服务器和Eureka客户端。

Eureka服务用以提供服务注册、发现,以一个war的形式提供或者编译源码,将war拷贝进tomcat即可提供服务

Eureka服务器用作服务注册服务器。相对于client端的服务端,为客户端提供服务,通常情况下为一个集群

Eureka客户端是一个java客户端,通过向Eureka服务发现注册的可用的eureka-server,向后端发送请求。用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。Netflix在其生产环境中使用的是另外的客户端,它提供基于流量、资源利用率以及出错状态的加权负载均衡。(引用:http://blog.csdn.net/jek123456/article/details/74171055

 Spring Cloud Eureka

需要的是组件上Spring cloud netflix的Eureka,这是一个服务注册和发现模块

分为两个部分:

@EnableEurekaClient:该注解表明应用既作为Eureka实例又作为Eureka client可以发现注册的服务

@EnableEurekaServer:该注解表明应用为eureka服务,有可以联合多个服务作为集群,对外提供服务注册和发现功能

 服务注册与发现对于微服务系统来说非常重要。有了服务发现与注册,你就不需要整天改服务调用的配置文件了,你只需要使用服务的标识符,就可以访问到服务。他的功能类似于dubbo的注册中心(register)。

   服务发现:服务发现是微服务基础架构的关键原则之一。试图着手配置每个客户端或某种格式的约定可以说是非常困难的和非常脆弱的。Eureka是Netflix服务发现的一种服务和客户端。这种服务是可以被高可用性配置的和部署,并且在注册的服务当中,每个服务的状态可以互相复制给彼此。  

   服务注册:当一个客户端注册到Eureka,它提供关于自己的元数据(诸如主机和端口,健康指标URL,首页等)Eureka通过一个服务从各个实例接收心跳信息。如果心跳接收失败超过配置的时间,实例将会正常从注册里面移除

基本环境:

IDE: Intellij IDEA 2018.5

jdk:1.8

maven:4.0.0

 

一、创建eureka server

file -> new -> project -> 选择spring initialzr

一直next直到这里:

创建完成的pom.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-config-server</artifactId>
            </dependency>


        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

启动一个服务注册中心,只需要一个注解@EnableEurekaServer,在这里加:

(每个人的类名可能都不一样)

效果如下:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;


@EnableEurekaServer
@SpringBootApplication
public class DemoApplication {

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

eureka是一个高可用的组件,它没有后端缓存,每一个实例注册之后需要向注册中心发送心跳(因此可以在内存中完成),在默认情况下erureka server也是一个eureka client ,必须要指定一个 server。eureka server的配置文件application.properties(或者application.yml):

server.port = 8761

eureka.instance.hostname = localhost
eureka.client.registerWithEureka = false
eureka.client.fetchRegistry = false
eureka.client.serviceUrl.defaultZone = http://${eureka.instance.hostname}:${server.port}/eureka/

如果是application.yml文件可以把它.properties文件改成.yml文件,变成树的结构就行了:

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

通过eureka.client.registerWithEureka:false和fetchRegistry:false来表明自己是一个eureka server.

run这个工程,打开http://localhost:8761

可以看到如下界面:

No application available:因为还没有注册服务当然不可能有服务被发现了。

二、创建eureka client

前面都是一样的,但是pom.xml配置要改,改后如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>service-hi</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-eureka</artifactId>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

通过注解@EnableEurekaClient 表明自己是一个eurekaclient.

package com.example.demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@EnableEurekaClient
@RestController
@EnableAutoConfiguration
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
    @Value("${server.port}")
    String port;
    @RequestMapping("/hi")
    public String home(@RequestParam String name) {
        return "hi "+ name+",i am from port:" +port;
    }
}

还需要在配置文件中注明自己的服务注册中心的地址,application.properties配置文件如下:

eureka.client.serviceUrl.defaultZone = http://localhost:8761/eureka/
server.port = 8762
spring.application.name = service-hi

同理,如果是application.yml文件,是这样的:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8762
spring:
  application:
    name: service-hi

需要指明spring.application.name,这个很重要,这在以后的服务与服务之间相互调用一般都是根据这个name 。 
启动工程,打开http://localhost:8761 ,即eureka server 的网址:

这个服务已经注册在服务中了,服务名为SERVICE-HI ,端口为8762

打开: http://localhost:8762/hi?name=mercy

可以看到:

Eureka client 到Server的调用过程源码分析

首次从DiscoveryClient 构造函数中调用的initScheduledTasks方法说起。初始化了多个定时任务,其中一个定时任务就是间隔的获取远端的注册信息。

private void initScheduledTasks() {
  scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new CacheRefreshThread()
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

CashRefreshThread:线程定时refresh:

class CacheRefreshThread implements Runnable {
        public void run() {
            refreshRegistry();
        }
    }
void refreshRegistry() {       
 boolean success = fetchRegistry(remoteRegionsModified);
            if (success) {
                registrySize = localRegionApps.get().size();
                lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
            }        
    }

获取远端注册信息,根据参数会决定是调用getAndStoreFullRegistry()进去全部刷新,还是调用 getAndUpdateDelta(applications);只是更新。

不管哪种,维护的都是本地一个private final AtomicReference<Applications> localRegionApps = new AtomicReference<Applications>();对象。

前面说到,DiscoveryClient是netflix客户端提供的服务注册发现的句柄,Applications里包括了所有的注册服务(wraps all the registry information returned by eureka server.),

其他接口,不管是查询服务的,还是查询服务实例的,也就是从在从这个wrapper里可以找到。

private boolean fetchRegistry(boolean forceFullRegistryFetch) {      
            Applications applications = getApplications();
            if (clientConfig.shouldDisableDelta()       
                getAndStoreFullRegistry();
            } else {
                getAndUpdateDelta(applications);
            }
            applications.setAppsHashCode(applications.getReconcileHashCode());
            logTotalInstances();       

        // Notify about cache refresh before updating the instance remote status
        onCacheRefreshed();
        // Update remote status based on refreshed data held in the cache
        updateInstanceRemoteStatus();
        // registry was fetched successfully, so return true
        return true;
    }

如何调用远端eureka的注册信息,是通过一个httpclient来完成的。eureka client访问server的接口定义在EurekaHttpClient中。这也是eureka server向外提供的全部数据接口。

com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient.getApplications其实就是发起一个Rest请求

@Override
public EurekaHttpResponse<Applications> getApplications(String… regions) {
return getApplicationsInternal(“apps/”, regions);
}

 private EurekaHttpResponse<Applications> getApplicationsInternal(String urlPath, String[] regions) {
            WebResource webResource = jerseyClient.resource(serviceUrl).path(urlPath);
            if (regions != null && regions.length > 0) {
                regionsParamValue = StringUtil.join(regions);
                webResource = webResource.queryParam("regions", regionsParamValue);
            }
            Builder requestBuilder = webResource.getRequestBuilder();
            addExtraHeaders(requestBuilder);
            response = requestBuilder.accept(MediaType.APPLICATION_JSON_TYPE).get(ClientResponse.class);
}

服务端的一个rest 的resource来提供服务。提供的服务在这里可以找到:

https://github.com/Netflix/eureka/wiki/Eureka-REST-operations

resource通过ResponseCacheImpl来访问到 AbstractInstanceRegistry中提供的接口来获取数据。AbstractInstanceRegistry 是真正存储和处理注册信息的地方。

com . netflix . eureka . registry . AbstractInstanceRegistry .getApplication

public Application getApplication(String appName) {
        boolean disableTransparentFallback = serverConfig.disableTransparentFallbackToOtherRegion();
        return this.getApplication(appName, !disableTransparentFallback);
    }

public Application getApplication(String appName, boolean includeRemoteRegion) {
        Application app = null;
        Map<String, Lease<InstanceInfo>> leaseMap = registry.get(appName);
        if (leaseMap != null && leaseMap.size() > 0) {
            for (Entry<String, Lease<InstanceInfo>> entry : leaseMap.entrySet()) {
                if (app == null) {
                    app = new Application(appName);
                }
                app.addInstance(decorateInstanceInfo(entry.getValue()));
            }
        } else if (includeRemoteRegion) {
            for (RemoteRegionRegistry remoteRegistry : this.regionNameVSRemoteRegistry.values()) {
                Application application = remoteRegistry.getApplication(appName);
                if (application != null) {
                    return application;
                }
            }
        }
        return app;
    }

 

posted @ 2018-07-07 09:04  机智的小八  阅读(1968)  评论(0编辑  收藏  举报