简介

摘自百度百科:
SpringCloud是一系列框架的有序集合。它利用SpringBoot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用SpringBoot的开发风格做到一键启动和部署。SpringCloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过SpringBoot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

反正SpringCloud是一个用来做微服务的框架,主要是基于SpringBoot来完成的,相关的微服务框架还有比较出名的Dubbo和DubboX。关于微服务是用来做什么的,有什么好处自行百度。

SpringCloud是Spring全家桶中的一员,所以SpringCloud对于Spring的其他组件都有着很好的支持。

SpringCloud主要采用的Eureka,Dubbo主要采用的是zookeeper。

上代码

provider 服务提供者

直接新建一个SpringBoot项目,由于是为了示例,本次示例中不使用数据库,只引入了spring-boot-starter-web

项目及其简单,只暴露了一个接口,用于测试,yml文件只配置了端口号为7900

idea创建springcloud工程 idea搭建springcloud项目_java

consumer 服务消费者

同样新建一个SpringBoot项目,使其调用consumer的接口,同样只引入了spring-boot-starter-web,yml文件只是修改了端口号为7910。

idea创建springcloud工程 idea搭建springcloud项目_eureka_02

这里解释一下RestTemplates这个类,关于这个类,这个老哥的解释我觉得非常完整了,RestTemplates在SpringCloud中,消费者通过这个类访问生产者,@bean注解是为了实例化这个类,实例化之后通过@AutoWired注解引入,将其交给Spring进行管理。

分别启动两个类,分别访问localhost:7900/provider/demohttp://localhost:7910/consumer/demo应该都能得到ProviderDemo这个结果,说明consumer成功调用了provider中的方法。

问题:
这样的编码方式是将接口(http://localhost:7900/provider/demo)硬编码在代码中,但是项目发布之后,ip地址必然是变动的。而且,硬编码的方式肯定是无法实现负载均衡的,就是说如果同时启动多个provider服务,这种硬编码方式是无法根据负载均衡策略去调用服务的。

解决方法:
在Dubbo中使用的ZooKeeper作为服务注册与发现的容器,在Springcloud中使用的是Eureka作为服务注册与发现的容器。

Eureka

同样创建一个SPringBoot项目,下面是pom.xml文件,我是在idea中直接添加spring-cloud-starter-netflix-eureka-server这个依赖的。

<?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>cn.edu.nuaa</groupId>
    <artifactId>eureka-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>eureka-demo</name>
    <description>eureka-demo</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>
    </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>
        </dependencies>
    </dependencyManagement>

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

</project>

然后在Springboot的启动类上添加@EnableEurekaServer或者@EnableDiscoveryClient,这两个注解的注解的区别主要是:

@EnableEurekaServer是基于 spring-cloud-netflix依赖,只能为eureka作用,是专门给Eureka用的
@EnableDiscoveryClient是基于 spring-cloud-commons依赖,并且在classpath中实现,是给比如zookeeper、consul使用的,
旧版本的@EnableEurekaServer的源码上面也是也是有@EnableDiscoveryClient注解的。
本示例是使用Eureka的,故推荐使用@EnableEurekaServer

@SpringBootApplication
@EnableEurekaServer
public class EurekaDemoApplication {

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

然后在yml文件中配置如下信息:

server:
  port: 8761
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka/

这个端口号官网设置成8761,这里也设置成8761;

`register-with-eureka: false这个的默认值为true,设置为true不会对使用不会有很大的影响,但是在启动的时候会保下面的错误:
was unable to refresh its cache! status = Cannot execute request on any known server
是因为启动的时候自己注册了自己而引起的冲突

defaultZone配置eureka的地址,这个如果有多个注册中心,则用逗号隔开。
为了服务注册中心的安全考虑,很多时候我们都会为服务注册中心加入安全认证。通常与SpringSecurity整合,主要是SpringSecurity的内容,这里不做讨论(主要是我自己尝试的时候失败了,啊哈哈哈)。

配置信息完成之后,启动eureka项目,访问localhost:8761便可以得到下面的界面,目前上面两个写的provider和consumer还没有注册到该注册中心:

idea创建springcloud工程 idea搭建springcloud项目_Springcloud_03

关于Eureka配置的含义及其默认值主要参考下表:

服务注册类配置

省略了eureka.client前缀

参数名

说明

默认值

enabled

启用Eureka客户端

true

registryFetchIntervalSeconds

从Eureka服务端获取注册信息的间隔时间,单位为秒

30

instanceInfoReplicationIntervalSeconds

更新实例信息的变化到Eureka服务端的间隔时间,单位为秒

30

initialInstanceInfoReplicationIntervalSeconds

初始化实例信息到Eureka服务端的间隔时间,单位为秒

40

eurekaServiceUrlPollIntervalSeconds

轮询Eureka服务端地址更改的间隔时间,单位为秒。当我们与Spring CLoud Config整合,动态刷新Eureka的serviceURL地址时需要关注该参数

300

eurekaServerReadTimeoutSeconds

读取Eureka Server信息的超时时间,单位为秒

8

eurekaServerConnectTimeoutSeconds

链接Eureka Server的超时时间,单位为秒

5

eurekaServerTotalConnections

从Eureka客户端到所有Eureka服务端的连接总数

200

eurekaServerTotalConnectionsPerHost

从Eureka客户端到每个Eureka服务端主机的连接总数

50

eurekaConnectionIdleTimeoutSeconds

Eureka服务端连接的空闲关闭时间,单位为秒

30

heartbeatExecutorThreadPoolSize

心跳连接池的初始化线程数

2

heartbeatExecutorExponentialBackOffBound

心跳超时重试延迟时间的最大乘数值

10

cacheRefreshExecutorThreadPoolSize

缓存刷新线程池的初始化线程数

2

cacheRefreshExecutorExponentialBackOffBound

缓存刷新重试延迟时间的最大乘数值

10

useDnsForFetchingServiceUrls

使用DNS来获取Eureka服务端的serviceUrl

false

registerWithEureka

是否要将自身的实例信息注册到Eureka服务端

true

preferSameZoneEureka

是否偏好使用处于相同Zone的Eureka服务端

true

filterOnlyUpInstances

获取实例时是否过滤,仅保留UP状态的实例

true

fetchRegistry

是否从Eureka服务端获取注册信息

true

服务实例类配置

eureka.instance.instanceId为实例的id,可以自定义,默认值为

${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}

省略了eureka.instance为前缀

参数名

说明

默认值

preferIpAddress

是否优先使用IP地址作为主机名的标识

false

leaseRenewalIntervalInSeconds

Eureka客户端向服务端发送心跳的时间间隔,单位为秒

30

leaseExpirationDurationInSeconds

Eureka服务端在收到最后一次心跳之后等待的时间上限,单位为秒。超过该时间之后服务端会将该服务实例从服务清单中剔除,从而禁止服务调用请求被发送到该实例上

90

nonSecurePort

非安全的通信端口号

80

securePort

安全的通信端口号

443

nonSecurePortEnabled

是否启用非安全的通信端口号

true

securePortEnabled

是否启用安全的通信端口号

appname

服务名,默认取spring.application.name的配置值,如果没有则为unknown

hostname

主机名,不配置的时候讲根据操作系统的主机名来获取

注册服务到Eureka

回到之前写的provider-demo项目中,在项目中添加下面这个依赖,注意version:

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

在之前的SpringCloud版本中,还有出现过下面的依赖,但是目前最新的版本是上面这个,亲测,在本示例中两个都是可以的。

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

然后在项目的启动类上添加@EnableEurekaClient注解

@SpringBootApplication
@EnableEurekaClient
public class ProviderDemoApplication {

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

从依赖的名字spring-cloud-starter-netflix-eureka-client的名字就知道这个依赖是用来做eureka的客户端,然后注册服务到eureka上的。然后在项目的启动类上的添加的注解为@EnableEurekaClient

回到上文中会发现搭建eureka注册中心的时候会发现引入的包是spring-cloud-starter-netflix-eureka-server,在项目的启动类上的添加的注解为@EnableEurekaServer还是很好记忆和理解的。

最后在yml文件中添加上eureka注册中心的地址并可以了:

server:
  port: 7900
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

分别启动一下eureka和provider项目,然后访问一下localhost:8761便会得到下面的页面:

idea创建springcloud工程 idea搭建springcloud项目_idea创建springcloud工程_04

可以看到provider的服务已经注册上eureka上了。
发现application的名称为UNKNOWN,可以在yml文件中添加下面内容修改名字:

spring:
  application:
    name: provider-demo

重启一下服务便可以看到名字,这里就不贴图片了。

有可能会遇到下面的红色字体:

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的自我保护的心跳机制导致的,即如果真实的服务已经Down掉,但在注册中心界面服务却一直存在,且显示为UP状态。解决方法可以添加下面内容,上面有eureka的配置信息中解释含义:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
  instance:
    lease-renewal-interval-in-seconds: 1
    lease-expiration-duration-in-seconds: 2
服务监控

对于服务的众多信息可能都需要监控和监听,SpringCloud主要采用的是下面这个依赖对其实现监听的:

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

添加这个依赖之后重启服务会发现console中多了下图这几个端点:

idea创建springcloud工程 idea搭建springcloud项目_ribbon_05

/actuator/health和/actuator/info以及/actuator这三个就是actuator提供的端点,注意以前的版本是没有/actuator前缀的,2.0以后的版本都加了/actuator前缀。可以访问localhost:7900/actuator尝试看监听的都是一些什么内容

如果要开启全部的监听端点,可以在yml文件中加入下面的配置信息:

management:
  endpoints:
    web:
      exposure:
        include: "*"

重启服务便可以看到所有的端点都开启用了。

主要的端点的含义可以参考下面的表格:

端点

描述

actuator

为其他端点提供“发现页面”。要求Spring HATEOAS在classpath路径上。

auditevents

陈列当前应用程序的审计事件信息。

autoconfig

展示自动配置信息并且显示所有自动配置候选人以及他们“被不被”应用的原因。

beans

显示应用程序中所有Spring bean的完整列表。

configprops

显示所有配置信息。

dump

dump所有线程。

env

陈列所有的环境变量。

flyway

Shows any Flyway database migrations that have been applied.

health

显示应用程序运行状况信息

info

显示应用信息。

loggers

显示和修改应用程序中的loggers配置。

liquibase

显示已经应用的任何Liquibase数据库迁移。

metrics

显示当前应用程序的“指标”信息。

mappings

显示所有@RequestMapping的url整理列表。

shutdown

关闭应用(默认情况下不启用)。

trace

显示跟踪信息(默认最后100个HTTP请求)。

使用同样的方法把consumer-demo一起注册到Eureka上。

ribbon

上文只是将服务注册到eureka上,但是consumer还是硬编码调用,前文也有提到这种硬编码方式肯定是不合理的,一来服务上线之后,IP地址肯定是变动的, 再则,采用硬编码的方式是无法实现负载均衡的。

ribbon便是一个用来做负载均衡的组件。

点进spring-cloud-starter-netflix-eureka-client依赖,会发现,这个依赖中已经添加了spring-cloud-starter-netflix-ribbon,故在项目中可以直接使用ribbon,不用重新添加依赖。

在加载restTemplates的方法上添加@LoadBalanced注解,使其具有负载均衡的能力,然后将硬编码的ip地址换成服务提供者的应用名字(application.name属性的值),修改之后controller便变成了下面的样子,其他地方不做修改。

@RestController
public class ConsumerController {

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

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/demo")
    public String ConsumerDemo(){
        return this.restTemplate.getForObject("http://provider-demo:7900/provider/demo", String.class);
    }

}

重启一下Eureka,provider-demo,consumer-demo,分别访问localhost:7900/provider/demohttp://localhost:7910/consumer/demo应该都能得到ProviderDemo 这个结果,说明consumer成功调用了provider中的方法。便解决了不采用硬编码的方式,使得consumer-demo调用provider-demo接口。

ribbon默认的负载均衡策略是轮询的。测试一下:

首先启动多个provider-demo项目,我这里分别用的接口是7900和7901两个接口,可以在Eureka中看一下provider的信息:

idea创建springcloud工程 idea搭建springcloud项目_java_06

然后在consumer-demo项目的controller中添加下面的内容,代码应该很容易看懂就是模拟loadBalancerClient客户端来访问名字为provider-demo的服务接口,然后输出访问的接口端口号。

@Autowired
 private LoadBalancerClient loadBalancerClient;

 @RequestMapping("/consumer/getInterfaceInfo")
 public void getInterfaceInfo(){
     ServiceInstance choose = loadBalancerClient.choose("provider-demo");
     System.out.println(choose.getPort());
 }

然后分别访问一下多次访问localhost:7910//consumer/getInterfaceInfo这个URL,可以看到控制台输出的信息在下面,很容易看出是轮询。

7901
7900
7901
7900
7901
7900
7901
7900
7901
7900
7901

ribbon有多种修改负载均衡的策略,可以通过代码,也可以通过配置文件,个人觉得配置文件的方法比较方便,其他的方法可以自行百度:

在consumer-demo的application.yml文件中添加下面的内容,这里使用的是随机的策略,该测试就不贴图了,反正最后结果是随机的:

#服务提供者的名字
provider-demo:  
  ribbon:
    # 所要采用的策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

ribbon所有的策略可以参照下表:

策略名

策略描述

实现说明

BestAvailableRule

选择一个最小的并发请求的server

逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最小的server

AvailabilityFilteringRule

过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值)

使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个server的运行状态

WeightedResponseTimeRule

根据响应时间分配一个weight,响应时间越长,weight越小,被选中的可能性越低。

一个后台线程定期的从status里面读取评价响应时间,为每个server计算一个weight。Weight的计算也比较简单responsetime 减去每个server自己平均的responsetime是server的权重。当刚开始运行,没有形成status时,使用roubine策略选择server。

RetryRule

对选定的负载均衡策略机上重试机制。

在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server

RoundRobinRule

roundRobin方式轮询选择server

轮询index,选择index对应位置的server

RandomRule

随机选择一个server

在index上随机,选择index对应位置的server

ZoneAvoidanceRule

复合判断server所在区域的性能和server的可用性选择server

使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。