美文网首页
Spring Cloud 微服务实战

Spring Cloud 微服务实战

作者: liaozb1996 | 来源:发表于2020-10-28 19:35 被阅读0次

    阅读《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
    

    访问配置:

    客户端配置:

    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<ServiceInstance> 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 依赖:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    @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<License> 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<License> getAllLicense(){
        Util.randomSleep();
        return licenseRepository.findAll();
    }
    
    private Iterable<License> 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<UserContext> 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<ClientHttpRequestInterceptor> 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 微服务实战

          本文链接:https://www.haomeiwen.com/subject/ihvcvktx.html