Spring Cloud 微服务实战

阅读《Spring微服务实战》笔记

项目地址:https://gitee.com/liaozb1996/spring-cloud-in-action

第三章 配置服务器

配置管理原则

配置管理原则:

  • 分离:配置部署和服务部署分离
  • 抽象:将服务配置数据的功能抽象到一个服务接口中
  • 集中:将配置信息集中到尽可能少的存储库中
  • 稳定:高可用和冗余

构建配置服务器

Spring Cloud Config 后端存储:文件系统、Git

标注引导类:

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {

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

}

配置服务器配置:

server.port=8888
spring.profiles.active=native
spring.cloud.config.server.native.searchLocations=E:/javaCode/spring_cloud_in_action/configServer/src/main/resources/config/license

创建配置文件:

src/main/resources/config/license/application.properties
src/main/resources/config/license/application-dev.properties

访问配置:

  • http://localhost:8888/license/default
  • http://localhost:8888/license/dev

客户端配置:

spring-cloud-config-client 依赖

boostrap.properties

# 基于文件系统的存储库
spring.application.name=license
spring.profiles.active=dev
spring.cloud.config.uri=http://localhost:8888
# 基于 Git 的存储库
spring.cloud.config.server.git.uri=https://gitee.com/liaozb1996/spring-cloud-in-action-config-repo.git
spring.cloud.config.server.git.searchPaths=license
# spring.cloud.config.server.git.username=user
# spring.cloud.config.server.git.password=password

刷新属性:

  • 调用服务实例的 /refresh 端点
  • 使用 Spring Cloud Bus 机制
  • 重启服务实例

第四章 服务发现

服务发现至关重要的原因

服务发现至关重要的原因:

  • 可以对服务实例进行水平伸缩 (通过抽象服务地址)
  • 发现并自动移除不健康的服务实例

传统服务位置解析的缺点

传统服务位置解析(DNS+负载均衡器)的缺点:

  • 同一时刻只有一个负载均衡器处理负载,容易成为阻塞点
  • 水平伸缩受单个负载均衡器处理能力和商业许可证数量限制
  • 手动的服务注册和服务注销
  • 远程调用需要通过负载均衡将请求映射到服务实例,而不是直接调用服务实例

服务发现实现组件:

  • Eureka 服务发现模式
  • Ribbon 客户端负载均衡模式

构建单机 Eureka 服务

构建 Eureka 服务:

标注引导类:

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

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

}

单机模式配置:

server:
  port: 8761

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

每次注册服务都需要等待30秒,因为 eureka 需要连续接收 3 个心跳包才能使用该服务。

缓存注册表后,客户端每隔30秒会重新到 eureka 刷新注册表。

服务注册:

spring.application.name=organization
server.port=8000

eureka.instance.preferIpAddress=true
eureka.client.registerWithEureka=true
eureka.client.fetchRegistry=true
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka

解决多网卡问题:

eureka.instance.ip-address=127.0.0.1

通过API获取注册表信息:(设置请求头 Accept:application/json

http://localhost:8761/eureka/apps

http://localhost:8761/eureka/apps/organization

Ribbon 客户端负载均衡

与 Ribbon 交互的客户端:

  • DiscoveryClient
  • RestTemplate
  • Feign

当使用二方包时需要在引导类添加 @EntityScan

@SpringBootApplication
@EntityScan(basePackages = "com.example.model")
public class OrganizationApplication {

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

}

配置 RestTemplate:

@Configuration
public class RestTemplateConfig {
    // 标准的 RestTemplate
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

    // 用于远程调用的 RestTemplate
    @Bean
    @LoadBalanced
    public RestTemplate loadBalancedRestTemplate(){
        return new RestTemplate();
    }
}

DiscoveryClient:

@SpringBootApplication
@EnableDiscoveryClient
public class LicenseApplication {

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

}
@Component
@Slf4j
public class OrgDiscoveryClient implements Client{
    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public Organization getOrganization(int id) {
        // 获取服务实例列表
        List instances = discoveryClient.getInstances("organization");
        if (instances.isEmpty()){
            return null;
        }
        // 拼接URL
        String url = instances.get(0).getUri().toString() + "/" + id;
        log.info("url: " + url);
        // 使用标准的 RestTemplate 调用远程服务
        Organization organization = restTemplate.getForObject(url, Organization.class);
        return organization;
    }
}

支持 Ribbon 的 RestTemplate:

@Component
public class OrgRestTemplateClient implements Client {
    @Autowired
    @Qualifier("loadBalancedRestTemplate")
    private RestTemplate restTemplate;


    @Override
    public Organization getOrganization(int id) {
        String url = "http://organization/" + id;
        return restTemplate.getForObject(url, Organization.class);
    }
}

Feign:

OpenFeign 依赖:


    org.springframework.cloud
    spring-cloud-starter-openfeign

@EnableFeignClients
public class LicenseApplication {

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

}

Feign 会在运行时动态生成代理对象:

@Component
@FeignClient("organization")
public interface OrgFeignClient extends Client{

    @GetMapping("/{id}")
    Organization getOrganization(@PathVariable int id);
}

第五章 Netflix Hystrix的客户端弹性模式

远程调用包括对远程资源和远程服务的调用。

远程调用会遇到两个问题:

  • 远程服务奔溃
  • 远程服务性能不佳

客户端弹性模式

四种客户端弹性模式:

  • 客户端负载均衡模式(Netflix Ribbon -- 从服务发现获取并缓存服务实例的物理地址)
  • 断路器模式(监控对远程调用的时间和失败次数)
  • 后备模式(调用失败后从途径获取结果【其他服务、数据源、直接生成】)
  • 舱壁模式(将不同的远程调用隔离到不同的线程池)

为什么客户端弹性模式很重要:

客户端弹性模式提供了三种构建能力:

  • 快速失败(当调用失败到达阈值后,认为远程服务处于降级状态)
  • 优雅失败(从其他路径获取结果)
  • 无缝恢复(远程服务降级后,使少量请求发送到远程服务检测服务是否恢复)

进入 Hystrix

在引导类启动断路器:

@SpringBootApplication
@EnableCircuitBreaker
public class LicenseApplication {

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

}

配置属性手册:https://github.com/Netflix/Hystrix/wiki/Configuration

使用 Hystrix 默认配置对远程调用进行管理:

// 默认超时是 1000 ms
// 默认所有远程调用都在同一线程池中,该线程池有 10 个线程
@HystrixCommand
public Iterable getAllLicense(){
    Util.randomSleep();
    return licenseRepository.findAll();
}

超时配置:execution.isolation.thread.timeoutInMilliseconds

@HystrixCommand(commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
})

配置后备策略:后备方法必须在同一类中并且具有相同的方法签名

@HystrixCommand(fallbackMethod = "builderFallbackLicenseList")
public Iterable getAllLicense(){
    Util.randomSleep();
    return licenseRepository.findAll();
}

private Iterable builderFallbackLicenseList(){
    return Arrays.asList(new License(-1, "fallbackLicense"));
}

配置舱壁:

@HystrixCommand(
        threadPoolKey = "licenseRepository",
        threadPoolProperties = {
                    @HystrixProperty(name = "coreSize", value = "10"),
                    @HystrixProperty(name = "maxQueueSize", value = "-1")
        }
)

微调 Hystrix

Hystrix 断路的策略:

  • 当首次遇到错误时,Hystrix 会开启一个10s的时间窗口
  • 统计10s内的调用次数是否达到最少调用次数(默认是20次)
  • 当调用达到最少调用次数时,Hystrix 开始统计失败的百分比,如果百分百达到阈值(默认是50%)则触发断路
  • 当发生断路时,Hystrix 会开启一个5s的时间窗口,即每隔5秒让一个请求通过,以检测远程服务是否恢复
@HystrixCommand(
        commandProperties = {
                @HystrixProperty(
                    name = "execution.isolation.thread.timeoutInMilliseconds", 
                    value = "1000"),
                // 统计调用失败的时间窗口
                @HystrixProperty(
                    name = "metrics.rollingStats.timeInMilliseconds", 
                    value = "10000"),
                // 统计失败百分比的频率
                @HystrixProperty(
                    name = "metrics.rollingStats.numBuckets", 
                    value = "10"),
                // 最少调用次数
                @HystrixProperty(
                    name = "circuitBreaker.requestVolumeThreshold", 
                    value = "20"),
                // 调用失败阈值
                @HystrixProperty(
                    name = "circuitBreaker.errorThresholdPercentage", 
                    value = "50"),
                // 断路后的时间窗口
                @HystrixProperty(
                    name = "circuitBreaker.sleepWindowInMilliseconds", 
                    value = "5000")
        }
    )

Hystrix 有三个级别的配置:

  • 应用程序级别(默认配置)
  • 类级别
  • 方法级别

类级别配置:

@DefaultProperties()
public class LicenseService {}

线程上下文和 Hystrix

Hystrix 有两个隔离策略:

  • TREAD:远程调用在子线程执行(默认)
  • SEMAPHORE:远程调用直接在当前线程执行
@HystrixCommand(
        commandProperties = {
                @HystrixProperty(
                    name = "execution.isolation.strategy", 
                    value = "SEMAPHORE")
        }
    )

如果使用 TREAD 策略,并且要将父线程的上下文传递到子线程中,需要自定义 HystrixConcurrencyStrategy

第六章 Zuul 服务网关

构建 Zuul 服务器

Zuul 提供的功能:路由映射、构建过滤器

依赖:zuul、eureka-client

标注引导类:

@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {

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

}

@EnableZuulServer 不会加载反向代理过滤器,也不会和 eureka 进行通信

zuul 配置:

eureka.instance.preferIpAddress=true
eureka.instance.ipAddress=127.0.0.1
eureka.client.registerWithEureka=true
eureka.client.fetchRegistry=true
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=env,beans

路由映射

Zuul路由映射机制:

  • 通过服务发现自动映射路由
  • 使用服务发现手动映射路由
  • 手动配置静态路由

查询路由:http://localhost:8080/actuator/routes

{
    "/organization/**": "organization",
    "/license/**": "license"
}

调用服务:http://localhost:8080/license/license/1 (第一个 license 是服务ID,/license/1 是请求路径)


使用服务发现手动映射路由:

zuul.ignored-services=organization
zuul.routes.organization=/org/**

添加前缀:

zuul.prefix=/api

手动配置静态路由:前面都是基于 eureka 上的服务id进行路由映射的,而这里是直接配置URL

zuul.routes.license-static.path=/license-static/**
zuul.routes.license-static.url=http://localhost:8001

动态加载路由

Git + http://localhost:8080/actuator/refresh (POST)

Zuul 和服务超时

Zuul 使用 Hystrix 和 Ribbon

# Hystrix 默认是调用1秒超时
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
hystrix.command.license.execution.isolation.thread.timeoutInMilliseconds=3000

# Ribbon 默认是5秒超时
license.ribbon.ReadTimeout=7000

过滤器

Zuul 支持三种过滤器类型:前置过滤器、后置过滤器、路由过滤器

前置过滤器

前置过滤器:向通过网关的请求添加 tracking-id

/**
 * Zuul 前置过滤器
 * 如果请求头部未包含 tracking-id,则设置其 tracking-id
 * */
@Component
@Slf4j
public class TrackingFilter extends ZuulFilter {

    @Autowired
    private FilterUtil filterUtil;

    @Override
    public String filterType() {
        return FilterUtil.FILTER_TYPE_PRE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

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

    @Override
    public Object run() throws ZuulException {
        String trackingId = filterUtil.getTrackingId();
        if (trackingId != null){
            log.info("从请求头部中得到 tracking-id:" + trackingId);
        }else {
            trackingId = UUID.randomUUID().toString();
            filterUtil.setTrackingId(trackingId);
            log.info("设置请求头部的 tracking-id:" + trackingId);
        }
        return null;
    }
}

这里使用了 Zuul 的 RequestContext:

Zuul 不允许直接修改请求头部,这里通过 addZuulRequestHeader 添加头部信息,在调用远程服务会自动合并

@Component
public class FilterUtil {
    public static final String FILTER_TYPE_PRE = "pre";

    public static final String TRACKING_ID = "tracking-id";

    public String getTrackingId(){
        RequestContext context = RequestContext.getCurrentContext();
        String trackingId = context.getRequest().getHeader(TRACKING_ID);
        if (trackingId == null){
            trackingId = context.getZuulRequestHeaders().get(TRACKING_ID);
        }
        return trackingId;
    }

    public void setTrackingId(String trackingId){
        RequestContext.getCurrentContext().addZuulRequestHeader(TRACKING_ID, trackingId);
    }
}

为了方便应用获取 tracking-id,这里使用 Filter 获取请求头信息并映射到 UserContext 中:

@Data
public class UserContext {
    private String trackingId;
}
public class UserContextHolder {
    private static ThreadLocal context = ThreadLocal.withInitial(UserContext::new);

    public static String getTrackingId(){
        return context.get().getTrackingId();
    }

    public static void setTrackingId(String trackingId){
        context.get().setTrackingId(trackingId);
    }
}
/**
 * 将请求头的 tracking-id 映射到 UserContext
 * */
@Component
public class UserContextFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String trackingId = httpServletRequest.getHeader("tracking-id");
        UserContextHolder.setTrackingId(trackingId);

        chain.doFilter(request, response);
    }
}

Filter 位于 Spring-Boot-Web 的 javax.servlet.*;

为了在服务间调用传播 tracking-id 这里需要定义一个 和 RestTemplate:

/**
 * 向 RestTemplate 发起的请求注入 tracking-id
 * */
@Slf4j
public class UserContextInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        String trackingId = UserContextHolder.getTrackingId();
        log.info("tracking-id: " + trackingId);
        HttpHeaders headers = request.getHeaders();
        headers.add("tracking-id", trackingId);
        return execution.execute(request, body);
    }
}
@Configuration
public class RestTemplateConfig {

    // 用于远程调用的 RestTemplate
    @Bean
    @LoadBalanced
    public RestTemplate loadBalancedRestTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        List interceptors = restTemplate.getInterceptors();
        if (interceptors == null){
            interceptors = Collections.singletonList(new UserContextInterceptor());
        }{
            interceptors.add(new UserContextInterceptor());
        }
        restTemplate.setInterceptors(interceptors);
        return restTemplate;
    }
}

项目中 license 会远程调用 orgnization,这里需要在两个微服务配置 Filter

后置过滤器

/**
 * 后置过滤器:向 response 注入 tracking-id
 * */
@Component
public class ResponseFilter extends ZuulFilter {

    @Autowired
    private FilterUtil filterUtil;

    @Override
    public String filterType() {
        return FilterUtil.FILTER_TYPE_POST;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

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

    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        context.getResponse().addHeader("tracking-id", filterUtil.getTrackingId());
        return null;
    }
}

你可能感兴趣的:(Spring Cloud 微服务实战)