SPRING CLOUD微服务DEMO-上篇

1. 微服务架构

系统架构的演变从单应用,到根据每个模块垂直拆分,到分布式服务,SOA,到目前进化成了微服务形态。

微服务的特点

  • 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
  • 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
  • 面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
  • 自治:自治是说服务间互相独立,互不干扰
    • 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
    • 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
    • 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口
    • 数据库分离:每个服务都使用自己的数据源
    • 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护

2. 远程调用方式

2.1 RPC/RMI

RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型

2.2 Http

Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议。也可以用来进行远程服务调用。缺点是消息封装臃肿。

2.3 如何选择

既然两种方式都可以实现远程调用,我们该如何选择呢?

  • 速度来看,RPC要比http更快,虽然底层都是TCP,但是http协议的信息往往比较臃肿,不过可以采用gzip压缩。
  • 难度来看,RPC实现较为复杂,http相对比较简单
  • 灵活性来看,http更胜一筹,因为它不关心实现细节,跨平台、跨语言。RPC方式需要在API层面进行封装,限制了开发的语言环境。

因此,两者都有不同的使用场景:

  • 如果对效率要求更高,并且开发过程使用统一的技术栈,那么用RPC还是不错的。
  • 如果需要更加灵活,跨语言、跨平台,显然http更合适

微服务,更加强调的是独立、自治、灵活。而RPC方式的限制较多,因此微服务框架中,一般都会采用基于Http的Rest风格服务。

3. Http客户端工具

可以使用一些流行的开源Http客户端工具请求Rest接口

  • HttpClient
  • OKHttp
  • URLConnection

3.1 RestTemplate

使用Http客户端工具请求Rest接口,得到数据后反序列化成对象。这样做比较麻烦,Spring提供了一个RestTemplate模版工具类,对Http客户端进行了封装(默认使用URLConnection),实现了对象和Json的序列化和反序列化,方便得很。下面搭建一个Spring Boot工程,同时演示RestTemplate如何使用。

4. Spring Boot 搭建项目

微服务的一个重要特点是每个服务都是一个可以独立运行的项目,要是按照以前的SSM的搭建方式,无法做到服务的快速搭建和部署。于是Spring Boot应运而生,它的理念就是约定大于配置,你选好模块,自动帮你搭建好环境,非常便捷。

接下来用Spring Boot搭建两个简单的用户微服务:user-serviceuser-consume,实现的功能是:user-consume使用RestTemplate调用user-service服务

具体参考这篇笔记(子文章)SPRING BOOT搭建两个微服务模块

5. Spring Cloud简介

Spring Cloud是实现微服务架构的技术。它集成了Netflix公司的一些微服务组件。(如果你不知道这家公司,那你肯定不爱看美剧。)

Netflix微服务架构图

  • Eureka:注册中心
  • Zuul:服务网关
  • Ribbon:负载均衡
  • Feign:服务调用
  • Hystix:熔断器

6. 微服务场景模拟

在第四节我们已经搭建好了user-serviceuser-consume两个微服务。

consume使用RestTemplate调用service提供的rest接口,获得json数据,反序列化成User对象返回到前台。其中存在着一些问题:

  • consume中调用的rest接口的地址是硬编码的,不方便维护。
  • 如果service的rest接口变更或关闭了,consume并不知情,最终什么都得不到。
  • service只有1台,一旦宕机,整个服务就不可用了。如果扩展多台,那consume又要自己考虑负载均衡。

接下来介绍的几个组件就是为解决这些问题而生的。

7. Eureka注册中心

7.1 简介

Eureka负责微服务的管理。

如果一个项目有数十个微服务,调用者想要自己找到一个适合的,可用的微服务是很麻烦的一件事。

就比如你想要坐车出门,自己上街拦出租车就很麻烦,要么司机拒载,要么车里已经有人了,要么干脆就没有车来...后来就出现了滴滴,做为一个网约车的“注册中心”,可以为你分配离你最近的空闲出租车。

Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。

同时,服务提供方与Eureka之间通过“心跳”机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。

这就实现了服务的自动注册、发现、状态监控。

7.2 原理图

  • EurekaServer:注册中心,可以是多个Eureka的集群,对外暴露自己的地址。
  • 服务提供者:启动后在注册中心中注册,成功后定期使用http方法向注册中心发送心跳包,表明自己还活着。
  • 客户端消费者:向注册中心订阅服务。注册中心会向消费者发送合适的服务提供者列表,并且定期更新。需要使用某个服务时,可以从列表中找到并调用。

7.3 搭建注册中心

搭建过程和第4节说的一样,注意选择Eureka模块即可。

7.3.1 代码和配置文件

  • 启动类

@EnableEurekaServer注解表示这是一个注册中心

@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}
  • 配置文件

现在我们只启动一个eureka作为注册中心。

这是Spring官方文档中的Standalone Eureka Server的建议配置。

注意eureka下的client配置,这个配置意思是eureka应用作为一个客户端,对注册中心的动作。

其中service-url的配置必须要填写,内容是注册中心的地址,如果有多个,逗号隔开。

defaultZone路径后面必须加上/eureka后缀,别问我为啥。

server:
  port: 10086 # 端口
spring:
  application:
    name: eureka-server # 应用名称,会在Eureka中显示
eureka:
  client:
    register-with-eureka: false # 是否注册自己的信息到EurekaServer,默认是true
    fetch-registry: false # 是否拉取服务列表,默认是true
    service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
      defaultZone: http://127.0.0.1:${server.port}/eureka

访问http://localhost:10086即可看到注册中心的内容,eureka只有一个,服务列表为空:

7.4 将user-service注册到eureka

7.4.1 添加依赖

eureka客户端依赖

<!-- Eureka客户端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Spring Cloud的依赖,注意我这里的版本是Greenwich.SR1

    <!-- SpringCloud的依赖 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <!-- Spring的仓库地址 -->
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

7.4.2 开启EurekaClient功能

添加@EnableDiscoveryClient注解

@SpringBootApplication
@EnableDiscoveryClient // 开启EurekaClient功能
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

7.4.3 配置eureka客户端属性

server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/XXXXX?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: XXXXX
    password: XXXXX
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
  application:
    name: user-service # 应用名称
mybatis:
  type-aliases-package: com.vplus.demo.userservice.pojo
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true # 当调用getHostname获取实例的hostname时,返回ip而不是host名称
    ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找

7.4.4 效果

7.5 user-consume从eureka中获取服务

7.5.1 添加依赖

同上7.4.1

7.5.2 开启EurekaClient功能

同上7.4.2

7.5.3 配置eureka客户端属性

server:
  port: 8082
spring:
  application:
    name: user-consume # 应用名称
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
    ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找

7.5.4 修改UserService

  • 之前是调用UserDao,UserDao使用RestTemplate请求远程接口得到数据。
  • 现在我们在service层,从eureka中拉取服务列表,得到接口地址后请求数据。
@Service
public class UserService {
    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;// Eureka客户端,可以获取到服务实例信息

    public List<User> queryUserByIds(List<Long> ids) {
        List<User> users = new ArrayList<>();
        // String baseUrl = "http://localhost:8081/user/";
        // 根据服务名称,获取服务实例
        List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
        // 因为只有一个UserService,因此我们直接get(0)获取
        ServiceInstance instance = instances.get(0);
        // 获取ip和端口信息
        String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/";
        ids.forEach(id -> {
            // 我们测试多次查询,
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次间隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }

}

7.6 eureka集群

7.6.1 搭建集群

Eureka可以集群搭建形成高可用的注册中心。多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。

我们要两个Eureka,端口号分别是10086、10087

可以使用Idea的启动器复制功能复制eureka的启动器。

将应用A的启动器复制出一个A2,A启动后,修改配置再启动A2,这样我们就可以得到两个配置不同的应用了。

先把原来的eureka配置信息改为

server:
  port: 10086 # 端口
spring:
  application:
    name: eureka-server # 应用名称,会在Eureka中显示
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10087/eureka

启动EurekaApplication,启动起来后再将配置改为

server:
  port: 10087 # 端口
spring:
  application:
    name: eureka-server # 应用名称,会在Eureka中显示
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

再启动EurekaApplication2,此时两个注册中心就形成了集群。

两个eureka相互注册,10086的defaultZone的url地址的端口号为10087,注意这一点。

再将两个客户端的Eureka相关配置改为

defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka

访问http://localhost:10086http://localhost:10087都可以看到效果:

7.7 Eureka相关配置

7.7.1 服务提供者

服务提供者主要做两个动作:服务注册服务续约

  • 服务注册的对应配置,true表示服务启动后会向注册中心发送注册请求,默认就是开着的。
eureka
	client
		register-with-erueka: true
  • 服务续约的对应配置,这些都是默认值
eureka:
  instance:
    #服务失效时间,默认值90秒
    lease-expiration-duration-in-seconds: 90
    #服务续约(renew)的间隔,默认为30秒
    lease-renewal-interval-in-seconds: 30

默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,注意,这是服务提供者告诉注册中心我每30秒续约一次,90秒没有续约,就表示我失效了,是配置在服务提供者上的,不是配置在注册中心上的。

我实验的时候,关闭一个服务,eureka几乎是瞬间就知道服务down了

试了好几次都是这样,可能我的关闭动作出发了什么东西吧,时间有限,先不管这个问题,留个坑将来看。

后来我注意到服务关闭后会输出一句:Unregistering ...,推测服务正常关闭时会自己通知注册中心。

  • 实例ID名的修改

在页面上,服务提供者实例ID显示为:localhost:user-service:8081,

格式为:${hostname} + ${spring.application.name} + ${server.port},对应配置为

eureka:
  instance:
    instance-id: ${spring.application.name}:${server.port}

修改后启动,变成了

7.7.2 服务消费者

消费者需要拉取服务列表,拉取时间间隔默认为30秒1次,对应配置为,如果是开发环境可适当缩小方便开发

eureka:
  client:
    registry-fetch-interval-seconds: 30

7.7.3 失效剔除和自我保护

  • 失效剔除:每个一段时间剔除掉失效的服务,默认为60s,为了方便开发设置为1s。

    服务器怎么知道失效了?看看上面服务提供者的配置,注意这几个配置的联系。

  • 自我保护:生产环境中,由于网络延迟等原因,失效服务并不一定是真正失效了。如果被标记为失效的服务太多,超过了85%,此时eureka会把这些服务保护起来,先不剔除,保证大多数服务还可用。在开发中将自我保护模式关掉,方便开发。

eureka:
  server:
    enable-self-preservation: false # 关闭自我保护模式(默认为打开)
    eviction-interval-timer-in-ms: 1000 # 	扫描失效服务的间隔时间为1s(默认为60s)

8. Robbin负载均衡

8.1 开启两个user-service

具体操作参照7.6.1,这里设置两个service的端口为8080和8081

8.2 开启负载均衡

注意,是在调用端即consume上开启负载均衡,Eureka中已经集成了Ribbon,无需引入新的依赖,只需要在调用端的RestTemplate的注册Bean方法上添加注解:@LoadBalanced即可。这个方法位于UserConsumerApplication里。

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

具体的调用方式也需要修改,将UserService中的queryUserByIds方法修改成这样。

public List<User> queryUserByIds(List<Long> ids) {
       List<User> users = new ArrayList<>();
        // 地址直接写服务名称即可
        String baseUrl = "http://user-service/user/";
        ids.forEach(id -> {
            // 我们测试多次查询,
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次间隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }
}

8.3 默认负载均衡策略分析

org.springframework.cloud.client.loadbalancer包里面有一个LoadBalancerInterceptor,它就是实现负载均衡的拦截器。

跟踪源码,找到了RibbonLoadBalancerClient,用consume多次请求接口,断点调试

execute(String serviceId, LoadBalancerRequest<T> request, Object hint)方法:

就是轮询

8.4 修改负载均衡策略

一个配置即可修改,有多种配置规则,这里使用随机规则。

注意格式,是以服务名称开头的。

server:
  port: 8082
spring:
  application:
    name: user-consume # 应用名称
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
    registry-fetch-interval-seconds: 5
  instance:
    prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
    ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找
###################################负载均衡配置###############################################
user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

8.5 重试机制

如果有一台服务突然挂掉了,而eureka还来不及将其清除出服务列表,或者消费者拉取的服务列表还有缓存,一旦请求到这台挂掉的服务就会报错。虽然多次请求后结果也能出来,但体验非常不好。

Ribbon的重试机制就是解决这个问题的。

引入依赖

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

开启Spring Cloud的重试机制并配置

server:
  port: 8082
spring:
  application:
    name: user-consume # 应用名称
  cloud:
    loadbalancer:
      retry:
        enabled: true # 开启Spring Cloud的重试功能
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
    registry-fetch-interval-seconds: 5
  instance:
    prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
    ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找

user-service:
  ribbon:
    ConnectTimeout: 250 # Ribbon的连接超时时间
    ReadTimeout: 1000 # Ribbon的数据读取超时时间
    OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
    MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
    MaxAutoRetries: 1 # 对当前实例的重试次数

就好了。

posted on 2019-05-28 12:17  vplus  阅读(5253)  评论(1编辑  收藏  举报