eureka实战——高可用服务治理框架

字数 2041阅读 3394
本文地址:https://www.jianshu.com/p/9aa452b1def0
代码地址:https://gitee.com/sunnymore/high_availability_eureka
grpc实战文章:https://www.jianshu.com/p/87a352ff637b

之前Sunny有聊过grpc框架,那是用于远程服务调用的框架。今天我们来搭建一个eureka服务治理框架。本文所做的demo代码,Sunny已经上传,欢迎大家根据上面地址fork实践。另外,之前grpc的文章地址也在上面。
关于eureka,从eureka的角度来看,主要分为两个角色:eureka server和eureka client。而从整个系统的角度来看,可以分为三个角色。eureka server叫做服务注册中心,eureka client中可以分为服务提供者和服务调用者。
今天要做的系统大致架构如下,Sunny画的比较简陋:


图片.png

服务注册中心有三个,集成一个高可用集群,且三个eureka server之间互相注册自己,三者之间会进行互现通信。另外有两个服务提供者,以实现服务的高可用性,他们在某台服务注册中心注册服务,三台服务注册中心相互之间会同步服务信息,因此服务消费者在任何一台服务注册中心都能发现所有已注册的服务——事实上,通常会找较近zone的服务注册中心发现服务。服务消费者通过服务注册中心获得目前已经注册的服务列表,通过客户端负载均衡选择某个服务提供者,然后发起远程调用。事实上,包括注册服务、续约、终止服务或者发现服务都是通过restful的形式。服务具体的存储形式是双层map,外层key为服务名,内层key为实例名。


正式开始

一、建立总项目

本项目还是用多模块来做。为了更简洁明了一些,我们真的来建立多个实际模块,而不采取同一个模块多个配置启动多个实例的方式。感兴趣的小伙伴可以自己尝试一下或者和我交流。总项目名为eureka-demo,其中包括eureka-server、service-provider和service-consumer等多个模块。项目目录如下图所示:


图片.png
二、建立服务注册中心

首先,我们需要设置总的项目的pom.xml文件,各模块的一个基础。总的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.sunny</groupId>
    <artifactId>eureka-demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>eurekaserver1</module>
        <module>eurekaserver2</module>
        <module>eurekaserver3</module>
        <module>serviceprovider2</module>
        <module>serviceprovider1</module>
        <module>serviceconsumer</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-parent</artifactId>
        <version>1.5.9.RELEASE</version>
    </parent>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <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>

    </dependencies>

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

这里我们Spring boot的版本用1.5.9.RELEASE。其实之前Sunny想尝试用Spring boot2,但是和Spring cloud多次版本匹配都出现了问题。因此还是选择用Spring boot的1.5.9.RELEASE版本,Spring cloud版本选择Edgware.SR3版本。
然后我们新建立eureka server1的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">
    <parent>
        <artifactId>eureka-demo</artifactId>
        <groupId>com.sunny</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka-server1</artifactId>

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

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Edgware.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


</project>

这里我们以整个项目的pom.xml为基础,另外导入spring-cloud-starter-eureka-server依赖。当然不要忘了spring cloud依赖。
然后我们写application.yml文件:

server:
  port: 1111
eureka:
  instance:
    hostname: peer1
  client:
    serviceUrl:
      defaultZone: http://peer2:1112/eureka/,http://peer3:1113/eureka/
spring:
  application:
    name: eureka-server

这里,我们设置端口为1111,当前实例主机名为peer1。同时,设置eureka.client.serviceUrl.defaultZone为另外两台eureka server的服务注册地址。如果是单eureka server的环境,就将eureka.client.serviceUrl.defaultZone设置为本机的服务注册地址,同时需要设置eureka.client.registerWithEureka和eureka.client.fetchRegistry为false,以防止自己注册自己。同时设置应用名为eureka-server,另外两台服务注册中心也需要设置相同的名称。值得一提的是,这里peer1、peer2和peer3需要在hosts文件中设置为127.0.0.1,在生产环境中可以写真实的域名。
接下来,我们写一个启动类,加上@EnableEurekaServer注解即可。

@EnableEurekaServer
@SpringBootApplication
public class Server1Application {

    public static void main(String[] args) {

        new SpringApplicationBuilder(Server1Application.class).web(true).run(args);

    }

}

另外两台eureka server的pom.xml文件一样,当然除了端口,通过eureka server1中的application.yml文件中的配置就可以发现eureka server2的域名为peer2,其端口为1112;eureka server3的域名为peer3,端口为1113。application.yml文件中的eureka.client.serviceUrl.defaultZone分别设置为另外两个eureka server的服务地址。具体代码可参见传送门源码链接。

三、建立服务提供者

还是从pom.xml文件说起,服务提供者作为一个eureka client导入的依赖有所不同,不需要导入spring-cloud-starter-eureka-server依赖,需要导入的是spring-cloud-starter-eureka依赖。

<?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">
    <parent>
        <artifactId>eureka-demo</artifactId>
        <groupId>com.sunny</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-provider1</artifactId>

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

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Edgware.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


</project>

然后写配置文件,也与服务注册中心有所不同。

server:
  port: 8081
eureka:
  client:
    serviceUrl:
      defaultZone: http://peer1:1111/eureka/
spring:
  application:
    name: hello-service

这里,端口设置为8081;在peer1上注册服务,如前面所说,只需向一台eureka server注册即可,其他服务注册中心会进行服务同步——当然,这里想要向多台服务注册中心注册也行,地址之间以逗号隔开即可;并且设置应用名为hello-service。
然后我们写一个启动类,ServiceProvider1Application.class,加上EnableEurekaClient注解。

@SpringBootApplication
@EnableEurekaClient
public class ServiceProvider1Application {

    public static void main(String[] args) {

        SpringApplication.run(ServiceProvider1Application.class,args);

    }

}

接着就是写实际的服务了,我们就写最简单的hello world式的restful接口。

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){

        return "hello world";

    }

}

现在服务提供者service provider1基本完成了,我们用同样的代码再写一个service provider2。在配置上,我们将端口设置为8082,其他都可一样,当然也可以将eureka.client.serviceUrl.defaultZone设置为其他两台eureka server的服务注册地址。具体配置如下:

server:
  port: 8082
eureka:
  client:
    serviceUrl:
      defaultZone: http://peer1:1111/eureka/
spring:
  application:
    name: hello-service
四、建立服务消费者

建立服务消费者,还是先看相关的依赖。关于eureka client的依赖和服务提供者一样,另外还需要添加spring-cloud-starter-ribbon的依赖用于客户端负载均衡,默认是轮询的方式。

<?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">
    <parent>
        <artifactId>eureka-demo</artifactId>
        <groupId>com.sunny</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-consumer</artifactId>


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


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Edgware.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

配置和服务提供者基本一样,但是这里,我们让服务消费者的eureka.client.serviceUrl.defaultZone为eureka server2,这里是特意不设置成eureka server1的,喜欢的童鞋可以按自己的喜好设置,甚至是设置多个,用逗号隔开。

spring:
  application:
    name: hello-consumer
server:
  port: 9090
eureka:
  client:
    serviceUrl:
      defaultZone: http://peer2:1112/eureka/

然后就是启动类,这里一样加上EnableEurekaClient的注解。同时创建RestTemplate的bean,加上LoadBalanced注解用于负载均衡。具体如下:

@SpringBootApplication
@EnableEurekaClient
public class ConsumerApplication {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

    public static void main(String[] args) {

        SpringApplication.run(ConsumerApplication.class,args);

    }

}

然后就是具体的服务消费类了,这里我们注入restTemplate实例,并用它来调用服务。同样,我们写成一个restful接口,可以用于验证结果。具体如下:

@RestController
public class ConsumerController {

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/helloConsumer")
    public String helloConsumer(){
        return restTemplate.getForEntity("http://hello-service/hello",String.class).getBody();
    }

}
五、启动并验证

首先我们启动eureka server1、eureka server2和eureka server3。启动过程中控制台中可能会有warn或者error信息,如下面的信息,但是这时候不要慌,这是因为peer2和peer3还未启动,所以出现连接失败的情况,当三个eureka server全部启动后,系统就是完全正常运行的。

2018-06-05 21:27:39.233 ERROR 10164 --- [-target_peer2-2] c.n.e.cluster.ReplicationTaskProcessor   : Network level connection to peer peer2; retrying after delay

com.sun.jersey.api.client.ClientHandlerException: java.net.SocketTimeoutException: Read timed out
    at com.sun.jersey.client.apache4.ApacheHttpClient4Handler.handle(ApacheHttpClient4Handler.java:187) ~[jersey-apache-client4-1.19.1.jar:1.19.1]
    at com.netflix.eureka.cluster.DynamicGZIPContentEncodingFilter.handle(DynamicGZIPContentEncodingFilter.java:48) ~[eureka-core-1.7.2.jar:1.7.2]
    at com.netflix.discovery.EurekaIdentityHeaderFilter.handle(EurekaIdentityHeaderFilter.java:27) ~[eureka-client-1.7.2.jar:1.7.2]
    at com.sun.jersey.api.client.Client.handle(Client.java:652) ~[jersey-client-1.19.1.jar:1.19.1]
    at com.sun.jersey.api.client.WebResource.handle(WebResource.java:682) ~[jersey-client-1.19.1.jar:1.19.1]
    at com.sun.jersey.api.client.WebResource.access$200(WebResource.java:74) ~[jersey-client-1.19.1.jar:1.19.1]
    at com.sun.jersey.api.client.WebResource$Builder.post(WebResource.java:570) ~[jersey-client-1.19.1.jar:1.19.1]
    at com.netflix.eureka.transport.JerseyReplicationClient.submitBatchUpdates(JerseyReplicationClient.java:116) ~[eureka-core-1.7.2.jar:1.7.2]
    at com.netflix.eureka.cluster.ReplicationTaskProcessor.process(ReplicationTaskProcessor.java:71) ~[eureka-core-1.7.2.jar:1.7.2]
    at com.netflix.eureka.util.batcher.TaskExecutors$BatchWorkerRunnable.run(TaskExecutors.java:187) [eureka-core-1.7.2.jar:1.7.2]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_161]
Caused by: java.net.SocketTimeoutException: Read timed out
    at java.net.SocketInputStream.socketRead0(Native Method) ~[na:1.8.0_161]
    at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) ~[na:1.8.0_161]
    at java.net.SocketInputStream.read(SocketInputStream.java:171) ~[na:1.8.0_161]
    at java.net.SocketInputStream.read(SocketInputStream.java:141) ~[na:1.8.0_161]
    at org.apache.http.impl.io.AbstractSessionInputBuffer.fillBuffer(AbstractSessionInputBuffer.java:161) ~[httpcore-4.4.8.jar:4.4.8]
    at org.apache.http.impl.io.SocketInputBuffer.fillBuffer(SocketInputBuffer.java:82) ~[httpcore-4.4.8.jar:4.4.8]
    at org.apache.http.impl.io.AbstractSessionInputBuffer.readLine(AbstractSessionInputBuffer.java:278) ~[httpcore-4.4.8.jar:4.4.8]
    at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138) ~[httpclient-4.5.3.jar:4.5.3]
    at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56) ~[httpclient-4.5.3.jar:4.5.3]
    at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259) ~[httpcore-4.4.8.jar:4.4.8]
    at org.apache.http.impl.AbstractHttpClientConnection.receiveResponseHeader(AbstractHttpClientConnection.java:286) ~[httpcore-4.4.8.jar:4.4.8]
    at org.apache.http.impl.conn.DefaultClientConnection.receiveResponseHeader(DefaultClientConnection.java:257) ~[httpclient-4.5.3.jar:4.5.3]
    at org.apache.http.impl.conn.AbstractClientConnAdapter.receiveResponseHeader(AbstractClientConnAdapter.java:230) ~[httpclient-4.5.3.jar:4.5.3]
    at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:273) ~[httpcore-4.4.8.jar:4.4.8]
    at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:125) ~[httpcore-4.4.8.jar:4.4.8]
    at org.apache.http.impl.client.DefaultRequestDirector.tryExecute(DefaultRequestDirector.java:684) ~[httpclient-4.5.3.jar:4.5.3]
    at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:486) ~[httpclient-4.5.3.jar:4.5.3]
    at org.apache.http.impl.client.AbstractHttpClient.doExecute(AbstractHttpClient.java:835) ~[httpclient-4.5.3.jar:4.5.3]
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:118) ~[httpclient-4.5.3.jar:4.5.3]
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56) ~[httpclient-4.5.3.jar:4.5.3]
    at com.sun.jersey.client.apache4.ApacheHttpClient4Handler.handle(ApacheHttpClient4Handler.java:173) ~[jersey-apache-client4-1.19.1.jar:1.19.1]
    ... 10 common frames omitted

我们可以查看服务注册中心的相关信息,访问peer1:1111或者peer2:1112或者peer3:1113可以查看。
可以看到服务已经注册上了:


图片.png

同时可以查看相关的可用节点:


图片.png

然后我们启动两个服务提供者,同样在此查看服务注册中心的信息,可以发现服务已经注册上了:
图片.png

然后在启动服务消费者,再次查看服务注册中心的信息:
图片.png

最后,我们来验证服务是否调用成功,访问localhost:9090/helloConsumer,成功得到如下结果:


图片.png

最后的最后,我们验证负载均衡的效果。为了验证负载均衡,我们在服务提供者的restful接口中加入一个打印信息,service provider1中为:
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){

        System.out.println("1");
        return "hello world";

    }

}

service provider2中为:

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){

        System.out.println("2");

        return "hello world";

    }

}

然后多次调用服务消费者的restful接口,发现两个服务提供者的控制台信息中依次打印1和2,这也说明了确实是轮询的。


至此,本篇文章到此结束,喜欢的童鞋可以点个赞。
欢迎转载,转载时请注明原文地址:https://www.jianshu.com/p/9aa452b1def0
童鞋们如果有疑问或者想和我交流的话有两种方式:

第一种

评论留言

第二种

邮箱联系:zsunny@yeah.net