组件

github spring-cloud

组件 项目 分类
eureka spring-cloud-netflix 注册中心
zuul spring-cloud-netflix 第一代网关
sidecar spring-cloud-netflix 多语言
ribbon spring-cloud-netflix 负载均衡
hystrix spring-cloud-netflix 熔断器
turbine spring-cloud-netflix 集群监控器
feign spring-cloud-openfeign 声明式 http 客户端
consul spring-cloud-sonsul 注册中心
gateway spring-cloud-gateway 第二代网关
sleuth spring-cloud-sleuth 链路追踪
config spring-cloud-config 配置中心
bus Spring-cloud-bus 总线
pipeline spring-cloud-pipeline 部署总线
dataflow spring-cloud-dataflow 数据处理

配置中心

Spring Cloud Config

doc

Server 端特点

  • HTTP 协议, 外部配置基于资源 API
  • 加密, 解密属性值(对称或非对称)
  • 开箱即用, 通过 Spring Boot 应用中使用 @EnableConfigServer

Client 端特点

  • 绑定到 Config Server 并通过远程的属性资源来初始化 Spring 的 Environment
  • 加密, 解密属性值(对称或非对称)

bootstrap 文件

它类似 application.properties 文件, 但 bootstrap.properties 属于一个应用的 bootstrap 阶段.

server

默认监听端口为 8888

pom

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

存储策略

默认是用 git 来保存的. 配置位置为 spring.cloud.config.server.git.uri

初始化一个 git server :

git init –bare —shared .

git 的 url 可以使用 placeholder

  • https://github.com/myorg/{application}
    
  • {application}

  • {profile}

  • {label}

注意, 不是 ${}

还可以按模式匹配. 参考 Pattern Matching and Multiple Repositories

资源定位规则

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
  • {application} : 对应于客户端的 spring.application.name
  • {profile} 表示激活的 profile(或逗号分隔的 profile 列表). 对应于客户端的 spring.profiles.active (也可逗号分隔的列表)
  • ${label} 是一个可选的服务器端的 git 标签. 默认为 master, 相当于版本号

client

默认连接的 server 地址为 http://localhost:8888

可以通过在 bootstrap.[yml|properties] 文件中的 spring.cloud.config.uri 属性来指定 server 地址

client 中的属性值可以从本地环境或 config server 中获取. 默认情况下, config server 的优先级更高. 即

config server > local

pom

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

指定 server

bootstrap.properties 文件中

# 可以为多个, 逗号分隔开
spring.cloud.config.uri=http://localhost:8888
spring.application.name=myapp

这样子, 它就会从 server 中的读取以下文件.

/myapp/{profile}[/{label}]
/myapp-{profile}.yml
/{label}/myapp-{profile}.yml
/myapp-{profile}.properties
/{label}/myapp-{profile}.properties

加/解密

注意, 全功能的话需要下载 JVM 的 JCE 文件.

添加 keystore

keytool -genkeypair -alias mytestkey -keyalg RSA \
  -dname "CN=Web Server,OU=Unit,O=Organization,L=City,S=State,C=US" \
  -keypass changeme -keystore server.jks -storepass letmein

然后将 server.jks 文件放到 server 的 classpath 下面.

然后可以通过 curl 测试

加密
curl -i -X POST http://localhost:8888/encrypt -d mysecret
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 388
Date: Thu, 14 Mar 2019 09:09:55 GMT

AQAD1xSE+ETByGVK2UmYTXwtUb0YQSBDHzYmkFvKREdw6sQcOl5EZ/bkmCFggQvBBsqAPbIUjKpL12SuexddKZA4CnFlpCmDvw7ZPQXwKunCrH0GAM3VN21C9Wyv9cqclM6SCWnZ9GtuAZlutYaSyCrnG7WSu5m68QI+0iDufRawDlUVEA5LrraD9t+uyL0fzhDIOAAmCLGpciLEVILT3IMFOc2QZOKBztU5cqnE6/5HlcXEP1L+3juMj0W4PUqkgcHDNZa5OFuftXNTmyx04pRjzPyeLWMv68xdl82Mr+rO9nIdxZejt/tppUCAONhIcm7uJZyOItqWZ1KPoH7x1Uy0/7o5RbHjRra/zhOjkR/pigt0j+5Mis7C0RszK+z5qak=


解密
curl -i -X POST http://localhost:8888/decrypt -d "AQBUcpq/8PnmGpuNDlml7sIFHoCWxax1ZQWwrLKTgKv/a6boWslHN7cqaPm/82M0A3qY5CmXU7V1stZxo0cC0I+uCsFCAUypJu7dexSQ4pMvcFdcvdLfQNzJIptnO4pDuXohTOGJISUbInEAcEwgxFUNSWt1WU8YpayXrCRIsFG8dLrfYuT5HtriTAuOHVhMLLgvkhkDQFpPOMLvU+UNsBBi2AFN7tdAQXvkVL0PgpTc7PkRzDQO4aRebXNR2kr5ZIROtv9rovvcYxwx+tfK1lHabUjKLqr7VxaDusZYV/z6qwXcJDLGIvN2oZt/0lgcc4FvDXK+wpUYJ9a/jvfwL1nz7lb2zl7b8BivWFSJ/3RbyQrCxT4SMXMNXZcBou2rP4c="
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 8
Date: Thu, 14 Mar 2019 09:10:24 GMT

mysecret

在 properties 文件中使用

格式为 key={cipher}加密后的内容

hello.demo={cipher}AQBUcpq/8PnmGpuNDlml7sIFHoCWxax1ZQWwrLKTgKv/a6boWslHN7cqaPm/82M0A3qY5CmXU7V1stZxo0cC0I+uCsFCAUypJu7dexSQ4pMvcFdcvdLfQNzJIptnO4pDuXohTOGJISUbInEAcEwgxFUNSWt1WU8YpayXrCRIsFG8dLrfYuT5HtriTAuOHVhMLLgvkhkDQFpPOMLvU+UNsBBi2AFN7tdAQXvkVL0PgpTc7PkRzDQO4aRebXNR2kr5ZIROtv9rovvcYxwx+tfK1lHabUjKLqr7VxaDusZYV/z6qwXcJDLGIvN2oZt/0lgcc4FvDXK+wpUYJ9a/jvfwL1nz7lb2zl7b8BivWFSJ/3RbyQrCxT4SMXMNXZcBou2rP4c=
@Value("${hello.demo}")
private String helloDemo;

@GetMapping("/values")
public Object getValues() {
    return helloDemo;
}

访问后, 即可看到是解密后的 mysecret 了.

client 端会自动解密后使用.

服务发现

  • 单机 : 直接通过域名调用

  • SOA

    • Client 通过 nginx 配置服务方的 upstream ip
    • 服务方对外统一暴露一个域名, 然后在自己的 nginx 里配置 upstream ip [better]
  • 微服务

    • 更新 nginx 配置文件, 然后 reload 或 nginx_http_dyups_module 来动态修改 upstream 而不需要 reload
    • 通过服务注册中心来处理

eureka

rest api

  • 使用 Peer to Peer 的复制模式

zone 及 region

Region : 代表一个独立的地理区域.

AvailabilityZone : 相当于不同区域下的机房.

默认情况下, 资源不会在 region 之间复制. 主要是在 region 下面的 AvailabilityZone 复制.

自我保护机制

SELF PRESERVATION , 用于防止网络偶尔抖动或暂不可用造成服务实例存活的误判.

通过当前注册实例数, 计算每分钟应该从应用实例接收到的心跳数, 如果最近一分钟接收到的续约的次数 <= 指定的阈值的话, 则关闭租约失效剔除, 禁止定时任务剔除失效的实例, 从而保护注册信息.

client

pom

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

application.properties

eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

配置

spring-cloud-starter-netflix-eureka-client 在 classpath , 就有

  • eureka.instance. 前缀的配置属性(表示注册者自身). 配置了 spring.application.name 之后 , 用默认的值一般就已经足够了.(spring.application.name 表示 eureka 服务的 ID 或 VIP)
  • eureka.client. 前缀的配置属性(可以查询注册器去定位其他服务)

禁用 client

eureka.client.enabled=false
# 当 spring.cloud.discovery.enabled 为 false , 也会禁用 client

认证

eureka.client.service-url.defaultZone=http://user:password@localhost:8761/eureka

使用 EurekaClient

不要在 @PostConstruct 或 @Scheduled 方法中使用它. 它是在 SmartLifecycle 中初始的.

@Autowired
private EurekaClient discoveryClient;

public String serviceUrl() {
    InstanceInfo instance = discoveryClient.getNextServerFromEureka("STORES", false);
    return instance.getHomePageUrl();
}

zone

当 client 部署在多个不同地区时, 你可以想这些 client 优先使用在同一个地区的服务. 可以这样子设置

# service1 , zone1
eureka.instance.metadataMap.zone = zone1
eureka.client.preferSameZoneEureka = true

# service2 , zone2
eureka.instance.metadataMap.zone = zone2
eureka.client.preferSameZoneEureka = true

server

pom

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
@SpringBootApplication
@EnableEurekaServer
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(true).run(args);
    }

}

standalone 模式

server.port=8671

eureka.instance.hostname=localhost
# 设置为 false, 因为它是 server, 不用注册
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

eureka.client.service-url.defaultZone = http://${eureka.instance.hostname}:${server.port}/eureka/

eureka.server.wait-time-in-ms-when-sync-empty=0
eureka.server.enable-self-preservation=false

management.endpoints.web.exposure.include=*

peer 模式

server1 的 eureka.client.service-url.defaultZone, 设置为 server2

server2 的 eureka.client.service-url.defaultZone, 设置为 server1

server 1

server.port=8000
spring.profiles=server1
eureka.instance.hostname=localhost
eureka.client.service-url.defaultZone = http://server2/eureka/
management.endpoints.web.exposure.include=*

server2

server.port=8001
spring.profiles=server2
eureka.instance.hostname=localhost
eureka.client.service-url.defaultZone = http://server1/eureka/
management.endpoints.web.exposure.include=*

security 中 为 /eureka 禁止 CSRF

@EnableWebSecurity
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().ignoringAntMatchers("/eureka/**");
        super.configure(http);
    }
}

OpenFeign

声明式 HTTP 客户端.

使用

package com.github.feign.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class DemoApplication implements CommandLineRunner {

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

    @Autowired
    private HelloService helloService;

    @Override
    public void run(String... args) throws Exception {
        String result = helloService.search("dubbo");
        System.out.println(result);
    }
}

声明式接口

package com.github.feign.demo;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "hello-client", url = "https://api.github.com", configuration = HelloServiceConfig.class)
public interface HelloService {
    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    String search(@RequestParam("q") String query);
}

服务配置

package com.github.feign.demo;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HelloServiceConfig {
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

配置

FeignClient 注解

  • name
  • url
  • configuration : 配置类, 可以自定义 Feign 的 Encoder, Decoder, LogLevel 等
  • fallback : 容错处理
  • path : 定义当前 FeignClient 的统一前缀

当然, 配置类也可以用配置文件来配置. 如下

fiegn.client.config.[feignName].xxx

全局默认配置:

@EnableFeignCleints 注解有个属性 defaultConfiguration , 这个可以设置全局默认的 feign 配置.

默认情况下. 属性文件的配置优先级 > Java 代码里的配置. 可以通过 feign.client.default-to-properties=false 来改变.

gzip

application.properties 中加入

feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,applicatin/xml,application/json
feign.compression.request.min-request-size=2048
feign.compression.response.enabled=true

这时, 服务接口类的返回值应该用 ResponseEntity<byte[]>

@FeignClient(name = "hello-client", url = "https://api.github.com", configuration = HelloServiceConfig.class)
public interface HelloService {
    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    ResponseEntity<byte[]> search(@RequestParam("q") String query);
}

日志

第一步: 在配置文件中配置

logging.level.完全包限制名.类名=debug

第二步: 配置日志 bean

@Configuration
public class HelloServiceConfig {
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

第三步: 相应的 FeignClient 上关联这个 Configuration

超时

  • robin
  • hystrix

替换 client 的实现

默认情况下, feign 使用 JDK 原生的 URLConnection 来处理 HTTP 请求

http client

pom

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>

<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>8.17.0</version>
</dependency>

application.proerpies

feign.httpclient.enabled=true

ok http

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

application.properties

feign.httpclient.enabled=false
feign.okhttp.enabled=true

负载均衡

Ribbon

  • 服务端负载均衡
    • nginx
    • F5
  • 客户端负载均衡
    • ribbon

使用

要开启服务发现

pom

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

application.properties

eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
management.endpoints.web.exposure.include=*
server.port=7777
spring.application.name=robbin-demo

application

要开启服务发现. @EnableDiscoveryClient

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
public class RibbonDemoApplication {

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

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

}

调用服务

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class TestController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/add")
    public String add(int a, int b) {
        String result = restTemplate.getForObject("http://eureka-client/query/add?a=" + a + "&b=" + b, String.class);
        System.out.println(result);
        return result;
    }
}

注意, 调用服务的 URL 是写注册服务时的应用名即可. 即

http://服务端的 ${spring.application.name}/path?parameter...

均衡策略

image-20190320115454942

  • RandomRule : 随机策略
  • RoundRobinRule : 轮询策略
  • RetryRule : 重试策略
  • BestAvailableRule : 最低并发策略
  • AvailabilityFilterRule : 可用过滤策略
    • 过滤掉一直连接失败并标记为 circuit tripped 的 server
    • 过虑掉高并发连接的 server (active connections 超过阈值)
  • ResponseTimeWeightedRule : 根据 server 响应时间来分配权重. 响应时间越小, 权重越大. 已废弃. 建议用 WeightedResponseTimeRule.
  • WeightedResponseTimeRule : 新版本

超时与重试

服务名.ribbon.ConnectionTimeout=30000
服务名.ribbon.ReadTimeout=30000
服务名.ribbon.MaxAutoRetries=1
服务名.ribbon.MaxAutoRetriesNextServer=1
服务名.ribbon.OkToRetryOnAllOperations=true

饥饿加载

即启动的时候, 就加载配置项的应用上下文. 默认是第一次调用的时候才加载.

ribbon.eager-load.enabled=true
ribbon.eager-load.clients=服务名1,服务名2

脱离 eureka 使用

默认情况下, ribbon 会从 eureka 注册中心中读取服务注册列表. 但也可以使用指定源服务地址

ribbon.eureka.enabled=false
${client}.ribbon.listOfServers=http://host1:port, http://host2:port

注意, ${client} 表示对应于提供微服务的服务名. 即对应提供服务的项目里的 spring.application.name 的名字

熔断器 Hystrix

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
</dependencies>
@SpringBootApplication
@EnableHystrix
@EnableDiscoveryClient
public class ClientApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }
}
@Component
public class UserService implements IUserService{
    
    @Override
    @HystrixCommand(fallbackMethod="defaultUser")
    public String getUser(String username) throws Exception {
        if(username.equals("spring")) {
            return "This is real user";
        }else {
            throw new Exception();
        }
    }
    
     /**
      * 出错则调用该方法返回友好错误
      * @param username
      * @return
      */
     public String defaultUser(String username) {
        return "The user does not exist in this system";
     }
     
}

feign 中使用熔断器

# 关闭熔断器
feign.hystrix.enabled=false
# 开启熔断器
feign.hystrix.enabled=true

开启后:

@FeignCleint(name="xxx", fallback=xxxFallback.class)

这样子, 当服务不可用时, 会降级为调用 xxxFallback.class 的实现方法了.

Dashboard

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency> 
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrixDashboard
public class HystrixDashboardApplication {

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

然后访问 http://localhost:9000/hystrix

网关 Zuul

使用

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

bootstrap.properties

spring.application.name=zuul-server
server.port=5555
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/,http://localhost:8002/eureka/
zuul.routes.${serviceId}.path=/xxx/**
# 以下可不配置. 默认就是这样子的
zuul.routes.${serviceId}.serviceId=${serviceId}

简化版

# 这样子相当于
#zuul.routes.${serviceId}.path=/${serviceId}/**
#zuul.routes.${serviceId}.serviceId=${serviceId}
zuul.routes.${serviceId}=

指定 url

zuul.routes.${serviceId}.path=/xxx/**
# 以下可不配置. 默认就是这样子的
zuul.routes.${serviceId}.url=http://host:port

${serviceId} 即服务提供者的 ${spring.application.name}

这样子, 表示访问网关 http://localhost:5555/xxx/** 的请求, 全部会转发到 http://${serviceId}/** 里去(注意, 没有 /xxx 了的).

启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class ZuulServerApplication {

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

路径匹配符

  • /** 任意数量的路径与字符
  • /* 任意数量的字符
  • /? 单个字符

路由前缀

zuul.prefix=/pre

这样子的话, 访问 zuul 的 path 时要加上 /pre 前缀.

服务屏蔽

zuul.ignored-services=${serviceId}

重定向

zuul.add-host-header=true

重试

zuul.retryable=true
# 同一个服务重试次数
ribbon.MaxAutoRetries=1
# 切换相同服务数量
ribbon.MaxAutoRetriesNextServer=1

zuul 中 filter 生命周期

  • pre
  • route
  • post
  • error

请求之间是通过 RequestContext 来通信

自定义 filter

继承 ZuulFilter .

  • filterType() : 设定 filter 类型. pre, route, post, error 等. 可用 FilterConstants
  • int filterOrder()
  • boolean shouldFilter()
  • Object run()

然后将它注入到 Spring 容器 :

@Bean
public YourFilterType yourFilterType() {
    return new YourFilterType();
}