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