SpringCloud 微服务简介

一、概述

微服务:一系列微小的服务共同形成,每个服务跑在自己的进程里,每个服务为独立的业务开发。

  • 一系列微小的服务共同组成
  • 单独部署,跑在自己的进程里
  • 每个服务为独立的业务开发
  • 分布式的管理

微服务的基础框架

国内实现微服务的两种主要方法

二、Eureka服务注册与发现

SpringCloud是使用Eureka进行服务注册与发现,Dubbo使用Zookeeper进行相应工作。并且自动实现了心跳检测,负载均衡等功能。

两个主要组件:

  1. Eureka Server 注册中心
  2. Eureka Client 服务注册

2.1 Eureka Server注册中心

Server端的配置yml文件样例如下:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
    register-with-eureka: false

spring:
  application:
    name: eureka
server:
  port: 8761

同时需要在Application类中添加注解@EnableEurekaServer

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

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

2.2 Eureka Client注册中心

Server端的配置yml文件样例如下:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: hello

同时需要在Application类中添加注解@EnableDiscoveryClient

@SpringBootApplication
@EnableDiscoveryClient
public class ClientApplication {

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

2.3 Eureka Server高可用

Eureka需要两两互相注册,相应的Client端写每个Eureka的地址,用逗号分割每个地址。

2.4 服务发现的两种方式

  • 客户端发现:由客户端从众多可用的服务列表中,挑选一个获取它的实际IP,并进行访问。这种实现方式要求客户端实现一套自己的选择机制。
  • 服务端发现:用代理的方式,由服务端帮客户端选择具体的某个服务器,对客户端透明

三、应用间通信

HTTP vs RPC,SpringCloud采用HTTP RESTful,而Dubbo采用RPC。

SpringCloud中服务RESTful两种调用方式

  • RestTemplate
  • Feign

3.1 RestTemplate的三种使用方式

第一种使用方式:

@GetMapping("/getProductMsg")
public String getProductMsg(){
    RestTemplate restTemplate=new RestTemplate();
    String response=restTemplate.getForObject("http://localhost:8080/msg",String.class);
    return response;
}

其中http://localhost:8080/msg为所需要的服务的url,String.class为该服务的返回值类型。

但是该种方式需要写死URL,且面临负载均衡多URL的情况。

第二种使用方式:

@Autowired
LoadBalancerClient loadBalancerClient;

@GetMapping("/getProductMsg")
public String getProductMsg(){
    RestTemplate restTemplate=new RestTemplate();
    ServiceInstance serviceInstance=loadBalancerClient.choose("PRODUCT");
    serviceInstance.getHost();
    String url=String.format("http://%s:%s/msg",serviceInstance.getUri(),serviceInstance.getPort());
    String response=restTemplate.getForObject(url,String.class);
    return response;
}

通过ServiceInstance动态获取指定服务的url。

第三种使用方式:

首先用bean的形式配置一下

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

其次在逻辑中:

@Autowired
RestTemplate restTemplate;

@GetMapping("/getProductMsg")
public String getProductMsg(){
    String response=restTemplate.getForObject("http://PRODUCT/msg",String.class);
    return response;
}

这种使用方式更加简单

3.2 Feign的使用方式

Feign是一个声明式REST客户端(伪RPC),采用接口+注解的方式使用。

首先需要在POM文件当中,添加一个dependency

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-openfeign-core</artifactId>
    <version>2.1.0.M2</version>
</dependency>

其次在主类上添加注解

@EnableFeignClients

第三步,定义好需要使用的接口

@FeignClient(name="product")
public interface ProductClient {
    @GetMapping("/msg")
    String productMSG();
}

其中name为需要调用的服务名称,GetMapping为需要调用的具体url名称

第四步,在逻辑中使用

@Autowired
ProductClient productClient;

@GetMapping("/getProductMsg")
public String getProductMsg(){
    String response=productClient.productMsg();
    return response;
}

3.3 客户端负载均衡器:Ribbon

SpringCloud由于采用客户端负载均衡策略,由客户端从服务器拉取可用列表,并且选取某个服务,固采用Ribbon,以下服务均使用到了Ribbon:

  • RestTemplate
  • Feign
  • Zuul

Ribbon实现软负载均衡核心有三点:

  1. 服务发现
  2. 服务选择规则
  3. 服务监听

Ribbon主要组件:

  • ServerList,首先获取所有可用服务列表
  • IRule 最后在剩下的地址中,通过IRule选择一个实例作为最后的结果
  • ServerListFilter 第二部用ServerListFilter过滤掉部分地址

Ribbon默认负载均衡策略:

轮询,如果要更改策略去官网找相应的配置文件配置即可

四、统一配置中心

统一配置中心可以对项目的配置进行统一管理,很方便的在不同的配置间切换。统一配置中心也有相应的Server端和Client端。

Server端会从远端Git将配置拉取下来,放到本地Git中(如果远端git无法访问,则从本地git读取配置),之后其他服务会从Server端读取相应配置。

4.1 Config Server端

首先在创建项目的时候需要勾选Eureka Discovery以及Config Server,因为其本身也是一个微服务Client。

要在Application类中添加注解@EnableDiscoveryClient,@EnableConfigServer

@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer
public class ClientApplication {

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

在ConfigServer的yml文件中添加git的地址,git当中存放的应该是配置文件(yml文件)。

spring:
  cloud:
    config:
      server:
        git:
          uri: 地址
          username: 
          password:

配置完成之后,项目可以自动根据url仓库地址进行映射,例如访问localhost:8080/product-a.yml。后缀名可以转换,例如转换成.json或者.properties,都可以自动转换格式。

注意:

product-a.yml之中的-a是必须加的,否则访问不到。代表的是不同的环境,不同的环境通过在yml文件当中添加注解例如env:a来解决。

并且如果有一个文件名字为product.yml,那么在访问所有其他分支文件的时候,会自动将该文件中的内容合并到访问的文件中,即根文件应该用来放一些通用配置,避免在分支文件中重复书写。

4.2 Config Client端

同Server端一样,首先引入依赖

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

启动类上不需要加注解,在yml文件中添加相关配置项,并且需要将yml文件改名为bootstrap.yml,该名称可以让SpringBoot优先加载该配置文件。否则会出现数据库配置相关问题。

spring:
  application:
    name: order
  cloud:
    config:
      discovery:
        enabled: true
        service-id: Config//该项为ConfigServer在注册中心中注册的名称
      profile: dev

4.3 Bus自动刷新配置

通过Spring Cloud Bus自动刷新配置可以实现配置的热更新,即不重启服务的情况下更新配置。

 

4.3.1 原理分析

其原理是在添加了Bus组建后,ConfigServer会对外提供一个HTTP api接口,每次Git端更新配置后,访问该接口,ConfigServer便会将更新的配置通过MQ推送到各个Client端中。

4.3.2 Bus配置

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

在ConfigServer以及Client端中添加上述maven配置,然后再ConfigServer中配置yml文件,暴露接口。

management:
  endpoints:
    web:
      expose: "*"

坑点:

在更新配置后,需要在Client端使用配置的地方添加注解@RefreshScope。

每次更新完后,访问ConfigServer的localhost:8080/bus-refresh即可更新配置。

五、服务网关和Zuul

网关服务是为了将所有Request统一到一个入口中,而不是访问所有的服务url

常用的网关方案:

  1. Nginx+Lua
  2. Kong  商业化网关
  3. Tyk  开源轻量级可伸缩轻量级网关
  4. Spring Cloud Zuul

5.1 Zuul特点

路由+过滤器=Zuul,其核心是一系列的过滤器。每一个进入Zuul的请求会经过各种过滤器,经过处理后返回给前置网关

Zuul的四种过滤器API

  • 前置(Pre)  参数校验等前置工作
  • 后置(Post)  对结果进行处理和加工的部分
  • 路由(Route)  将外部请求转发到具体服务上
  • 错误(Error)  在过滤器发生异常后,统一的异常处理的地方
  • 自定义过滤器(Custom)   还可以写一些自定义的过滤器

Zuul整体架构图

Zuul中一个HTTP请求的生命周期

5.2 Zuul的默认使用方式

1、创建项目时需要勾选Config Client、Eureka Discovery和Zuul组件。

spring:
  application:
    name: api-gateway
  cloud:
    config:
      discovery:
        enabled: true
        service-id: Config//该项为ConfigServer在注册中心中注册的名称
      profile: dev
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

2、在主类中添加@EnableZuulProxy注解

3、如果我们想要映射的url为:localhost:8005/product/list

那么我们需要访问localhost:9000/product/product/list

ip地址 : 网关端口 / 服务注册名称 / 服务具体功能url

5.3 自定义路由

在yml中添加如下配置

zuul:
  routes:
    myProduct:
      path: /myProduct/**  //自定义路径
      serviceId: product //要路由到的服务名称

即可变成:localhost:9000/myProduct/product/list

还有一种简洁写法

zuul:
  routes:
    product: /myProduct/**  //自定义路径

5.4 屏蔽某些路由被外部访问

zuul:
  ignored-patterns:
    - /product/product/listForOrder
    - /myProduct/product/listForOrder
    - /**/product/listForOrder   //通配符写法

即可阻止指定连接被外部访问。

5.5 Cookie

默认在使用Zuul时,Cookie是无法传递到内部服务的。需要添加配置

zuul:
  routes:
    sensitiveHeaders:  //直接空白就可以

 5.6 动态路由

利用之前的第四章动态配置即可。但是在配置自动更新配置客户端的时候写法有些区别

@Component
public class ZuulConfig{
    @ConfigurationProperties("zuul")
    @RefreshScope
    public ZuulProperties zuulProperties(){
        return new ZuulProperties();
    }
}

六、基于Zuul的一些功能

6.1 权限校验(Pre过滤器)以及一个Post过滤器Demo

在网关项目中新建一个类TokenFilter

package com.imooc.apigateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;


@Component
public class TokenFilter extends ZuulFilter {
   @Override
   public String filterType() {
       return PRE_TYPE;
   }

   @Override
   public int filterOrder() {
       return PRE_DECORATION_FILTER_ORDER - 1;
   }

   @Override
   public boolean shouldFilter() {
       return true;
   }
   //主要逻辑在该部分写,设计思路为所有请求必须带一个get请求的token

   @Override
   public Object run() {
       RequestContext requestContext = RequestContext.getCurrentContext();
       HttpServletRequest request = requestContext.getRequest();

       //这里从url参数里获取, 也可以从cookie, header里获取
       String token = request.getParameter("token");
       if (StringUtils.isEmpty(token)) {
           requestContext.setSendZuulResponse(false);
           requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
       }
       return null;
   }
}

对Post过滤器的应用,可以写一个类,添加一些值,本样例只是为了展示post过滤器的功能。

package com.imooc.apigateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;

import java.util.UUID;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_RESPONSE_FILTER_ORDER;

@Component
public class addResponseHeaderFilter extends ZuulFilter{
    @Override
    public String filterType() {
        return POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return SEND_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletResponse response = requestContext.getResponse();
        response.setHeader("X-Foo", UUID.randomUUID().toString());
        return null;
    }
}

6.2 限流

对服务器的限流工作,是在前置过滤器中进行设计,应放在所有过滤器前面进行操作。

本Demo采用的限流算法为令牌桶限流,即定速向桶内放置令牌,请求必须获取令牌才能继续进行。如果没有得到令牌就会被拒绝。

限流类:

package com.imooc.apigateway.filter;

import com.google.common.util.concurrent.RateLimiter;
import com.imooc.apigateway.exception.RateLimitException;
import com.netflix.zuul.ZuulFilter;
import org.springframework.stereotype.Component;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVLET_DETECTION_FILTER_ORDER;

@Component
public class RateLimitFilter extends ZuulFilter{

  private static final RateLimiter RATE_LIMITER = RateLimiter.create(100);

  /**
   * to classify a filter by type. Standard types in Zuul are "pre" for pre-routing filtering,
   * "route" for routing to an origin, "post" for post-routing filters, "error" for error handling.
   * We also support a "static" type for static responses see  StaticResponseFilter.
   * Any filterType made be created or added and run by calling FilterProcessor.runFilters(type)
   *
   * @return A String representing that type
   */
  @Override
  public String filterType() {
    return PRE_TYPE;
  }

  /**
   * filterOrder() must also be defined for a filter. Filters may have the same  filterOrder if precedence is not
   * important for a filter. filterOrders do not need to be sequential.
   *
   * @return the int order of a filter
   */
  @Override
  public int filterOrder() {
    return SERVLET_DETECTION_FILTER_ORDER - 1;
  }

  /**
   * a "true" return from this method means that the run() method should be invoked
   *
   * @return true if the run() method should be invoked. false will not invoke the run() method
   */
  @Override
  public boolean shouldFilter() {
    return true;
  }

  /**
   * if shouldFilter() is true, this method will be invoked. this method is the core method of a ZuulFilter
   *
   * @return Some arbitrary artifact may be returned. Current implementation ignores it.
   */
  @Override
  public Object run() {
    if (!RATE_LIMITER.tryAcquire()) {
      throw new RateLimitException();
    }

    return null;
  }
}

令牌不足抛出异常:

package com.imooc.apigateway.exception;


public class RateLimitException extends RuntimeException {
}

6.3 跨域

在前后端分离的设计框架中,如果前端用ajax请求的话,会涉及到跨域访问的问题。

SpringBoot可以通过添加@CrossOrigin的注解,但是缺点是要在每个类中添加注解。本文介绍在Zuul里增加CorsFilter过滤器来实现相关功能。

  • @CrossOrigin有一个参数(allowCredentials=”true”)为允许cookie跨域

添加一个配置类CorsConfig:

package com.imooc.apigateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Arrays;

@Configuration
public class CorsConfig {

  @Bean
  public CorsFilter corsFilter() {
    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    final CorsConfiguration config = new CorsConfiguration();
    //是否支持Cookie跨域
    config.setAllowCredentials(true);
    //原始域名
    config.setAllowedOrigins(Arrays.asList("*")); //http:www.a.com
    //允许的头
    config.setAllowedHeaders(Arrays.asList("*"));
    //允许的方法,get post等
    config.setAllowedMethods(Arrays.asList("*"));
    //缓存时间,在时间内对相同请求不进行检查
    config.setMaxAge(300l);

    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
  }
}

其中以下代码段表示要对哪些域名进行配置,/**为正则形式表示对所有都进行配置。

source.registerCorsConfiguration("/**", config);

七、 服务容错和Hystrix

在项目当中,可能出现缓存穿透,缓存击穿,缓存雪崩等情况,具体介绍可见https://blog.csdn.net/zeb_perfect/article/details/54135506该博客

7.1 Spring Cloud Hystrix

Spring Cloud Hystrix是Spring Cloud自带的一个组件,是基于Netflix开发的一款组件

其具有服务器常见所需要的功能:

  • 服务降级
  • 服务熔断
  • 依赖隔离
  • 监控
  • ……

7.2 服务降级

7.2.1 服务降级的配置

在流量过大时,需要进行服务降级,以保证核心业务的可用性,届时非核心服务会不可用或弱可用。

1、首先在SpringCloud中引入依赖。

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

2、在启动类上添加注解@EnableCircuitBreaker,当然如果你的项目会用到@EnableCircuitBreaker以及@EnableDiscoveryClient这两个注解,可以将SpringBootApplication这个注解替换为SpringCloudApplication注解。

3、书写Controller

package com.imooc.order.controller;

import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;

@RestController
@DefaultProperties(defaultFallback = "defaultFallback")
public class HystrixController {
  @HystrixCommand
  @GetMapping("/getProductInfoList")
  public String getProductInfoList(@RequestParam("number") Integer number) {
    if (number % 2 == 0) {
      return "success";
    }

    RestTemplate restTemplate = new RestTemplate();
    return restTemplate.postForObject("http://127.0.0.1:8005/product/listForOrder",
        Arrays.asList("157875196366160022"),
        String.class);

//		throw new RuntimeException("发送异常了");
  }

  private String fallback() {
    return "太拥挤了, 请稍后再试~~";
  }

  private String defaultFallback() {
    return "默认提示:太拥挤了, 请稍后再试~~";
  }
}

其中

@DefaultProperties(defaultFallback = "defaultFallback")

表示的是出现异常时,默认的执行方法是defaultFallback这个方法

7.2.2 服务超时

在网站访问压力过大时,会出现某些服务访问时间过长的情况。在配置了服务降级的情况下,可能会出现直接服务降级的情况。如果不想要这种情况发生,可以配置一个超时时间

  @HystrixCommand(commandProperties={
//默认为1秒,在这里改成了3秒
      @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
})

yml配置法:

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000

配置完后,在想要使用降级的类上添加@HystrixCommand注解

7.3 服务熔断及依赖隔离

Hystrix的依赖隔离类似于Docker的舱壁模式,Docker实现的是进程隔离,使得容器之间互不影响。而Hystrix实现的是线程隔离,会为每个HystrixCommand进行线程隔离,使得即使某个服务出现问题也不会对其他线程产生影响。他会自动为每个函数实现依赖隔离。

断路器,顾名思义,类似于电路中的保险丝,当服务产生异常情况并达到一定阈值之后,会进行服务的熔断,使得服务无法访问。

上图表示了熔断器的三种状态:

  • 关闭状态:此时熔断器关闭,服务正常使用
  • 打开状态:此时熔断器开启,服务降级无法使用
  • 半打开状态:此时熔断器半开启,只允许部分请求访问服务,如果请求都成功,则将断路器关闭。反之打开断路器
  @HystrixCommand(commandProperties={
      //设置熔断
      @HystrixProperty(name="circuitBreaker.enabled",value="true"),
      //请求失败数达到一定次数,启动断路器。在滚动时间窗口即下面的配置,断路器最小的请求数
      @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="20"),
      //断路器各个逻辑的持续时间,如进入断路状态后到半打开状态之间的时间
      @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value="10000"),
      //设置断路器打开,设置的最小成功百分比,如果发生十次调用有七次异常,则打开断路器
      @HystrixProperty(name="circuitBreaker.errorThresholdPrecentage",value="60"),
})

7.4 Zuul超时配置

有时第一次启动Zuul的时候会出现超时的问题,由于Zuul启动的时候会采用懒加载的形式,即有的类会延迟加载。就需要单独配置一下Zuul工程的超时时间。

八、服务url追踪

由于不是SpringCloud自有业务,在这里就不多说了,推荐一篇美团的技术博客来了解服务追踪

https://tech.meituan.com/mt_mtrace.html

九、Rancher服务部署(k8s、Kubernetes)

9.1 Kubernetes简介

Kubernetes是一个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes的目标是让部署容器化的应用简单并且高效(powerful),Kubernetes提供了应用部署、规划、更新、维护的一种机制。

简单来说我们可以采用容器管理软件k8s来统一的管理我们的容器。

了解即可,建议自己去百度上搜搜看看