EUREKA
-
服务注册中心
eureka服务注册中心,可以搭建集群,对外暴露自己的地址
可以在启动类上加@EnableEurekaClient注解,表示作为服务的注册中心
配置:
eureka: client: service-url: defaultZone: http://127.0.0.1:10081/eureka //eureka服务自己的地址,如果是集群可以配置其他eureka地址 register-with-eureka: false #是否注册自己 fetch-registry: false #是否拉去服务列表
spring: application: name: ly-register //服务的名字(id)
也可以使用:eureka.instance.hostname=xxx配置服务的名字
自我保护:
eureka会统计最近15分钟心跳续约的比例是否低于百分85,因为在实际环境中可能因为网络延迟导致心跳失败,这时直接把服务提出就不妥当,因为服务并未宕机。但是服务调用者要做好失败容错
配置
eureka: server: enable-self-preservation: true #开启自我保护,默认 eviction-interval-timer-in-ms: 90000 #90秒没有就剔除
-
服务提供者
启动后向eureka注册自己的信息,(服务提供者也可以是服务消费者)
相关注解:
启用服务的注册与发现
- 1.@EnableDiscoveryClient
- 2.@EnableEurekaCliect
第一个可以兼容其他的注册中心,第二个只能使用eureka作为注册中心才生效
配置
eureka: client: service-url: defaultZone: http://127.0.0.1:10081/eureka //注册中心地址 instance: lease-renewal-interval-in-seconds: 30 #服务30秒发送一次心跳 lease-expiration-duration-in-seconds: 90 #90秒没有发送心跳,就剔除服务 prefer-ip-address: true //表示使用IP地址注册 ip-address: 127.0.0.1 //指定注册的IP地址
-
服务消费者
向eureka订阅服务,eureka会将对应服务的提供者地址列表发给消费者,并且定期更新
注解同服务的提供方:
eureka: client: service-url: defaultZone: http://127.0.0.1:10081/eureka #注册中心地址 registry-fetch-interval-seconds: 5 #5秒拉去一次服务列表 fetch-registry: true #是否拉取服务 register-with-eureka=ture #是否注册自己,默认ture instace: prefer-ip-address: true ip-address: 127.0.0.1
eureka.instance.instance-id 指定eureka界面status下面的名字,默认:ip+应用名称+端口号
其余:遇到之后再补充
ribbo:
默认采用轮询的负载均衡算法
服务id:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡算法全类名这个配置可以修改负载均衡算法,负载均衡算法对应实现类
欢迎补充
Hystrix豪猪
服务的雪崩:
分布式系统环境下,服务间类似依赖非常常见,一个业务调用通常依赖多个基础服务。如下图,
如果哥哥服务正常允许那自然没有问题,但是如果有一个服务不可用了就会出现如下图的情况,

当Service A的流量波动很大,流量经常会突然性增加!那么在这种情况下,就算Service A能扛得住请求,Service B和Service C未必能扛得住这突发的请求。
此时,如果Service C因为抗不住请求,变得不可用。那么Service B的请求也会阻塞,慢慢耗尽Service B的线程资源(tomcat 默认可以支持200并发),Service B就会变得不可用。紧接着,Service A也会不可用。这种一个服务失败,导致整条链路的服务都失败的情形,我们称之为服务雪崩。
2、引起雪崩的原因和服务雪崩的三个阶段
原因大致有四:
1、硬件故障;
2、程序Bug;
3、缓存击穿(用户大量访问缓存中没有的键值,导致大量请求查询数据库,使数据库压力过大);
4、大量请求;服务雪崩的第一阶段: 服务不可用;
第二阶段:调用端重试加大流量(用户重试/代码逻辑重试);
第三阶段:服务调用者不可用(同步等待造成的资源耗尽);3、解决方案
- 应用扩容(扩大服务器承受力)加机器
- 请求缓存:支持将一个请求与返回结果做缓存处理(结果变化少,读请求并发高,可以放进redis缓存)
- 请求合并:将相同的请求合并然后进行批处理
- 线程隔离:同一个服务中不同请求用不同线程池,满了返回失败。这样一个请求出现问题也不会影响其他线程(hystrix内部已经为我们实现了)
- 服务降级:用户请求故障时,不会阻塞不会无休止等待看到系统奔溃,至少可以返回一个执行结果,如友好提示。(请求超时或者线程池满或者服务熔断会触发降级)
- 服务熔断:牺牲局部服务保护整体系统稳定的措施,(就如同电路中的断路器)
spring cloud使用:
在启动类上加注解激活hystrix功能:
@EnableHystrix或者@EnableCircuitBreaker
俩者区别:@EnableHystrix和@EnableCircuitBreaker区别,,其实没区别,推荐用@EnableHystrix
1.在调用其他服务的方法上加@HystrixCommand注解并指定降级的方法,指定fallbackMethod=”方法名指定“,注意降级的方法返回值和参数要和原方法返回值要一致
2.还可以使用注解@defaultProperties注解在类上为当前类的所有方法指定默认fallbackmethod,指定fallbackMethod=”方法名指定“ 注意这种写法不能给降级方法加参数,要使用空参的方法。然后在方法上只需要加@HystrixCommand开启降级就好了,这样可以避免重复写第一个的fallbackMethod。
注意:超时,或者抛出异常,熔断,都会触发降级。 (异常时候可以通过对应办法获得异常进行处理,这里还未运用到暂时不讨论)
超时时长配置:
1.为每个方法配置超时时长
- @HystrixCommand的commandProperties属性,属性值是一个数组可以定义多个@HystrixProperty指定属性。通过@HystrixProperty(name="xxx",value="xxx")配置,超时时长对应的name为”exeecution.isolation.thread.timeoutInMillisecinds “。value单位毫秒。
配置如下
@HystrixCommand(commandProperties={ @HystrixProperty(name="exeecution.isolation.thread.timeoutInMillisecinds",value="3000") })
2.为一个类下的所有方法配置超时时长
- @defaultProperties注解里好像可以为整个类配置超时时长
3.整个微服务全局配置超时时长:可以使用配置文件配置全局超时时长(没提示):
- hystrix.command.default.exeecution.isolation.thread.timeoutInMillisecinds == 3000 单位为毫秒
- 也可以使用配置文件给一个服务或者方法配置超时时长。就把default改为对应服务名,或者方法。
熔断器(就如同学校宿舍的保险丝)
1.断路器机制
断路器很好理解, 当Hystrix Command最近20次请求后端服务的失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务(快速失败). 断路器保持在开路状态一段时间后(休眠时间窗)(默认5秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN),一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力.
@HystrixCommand(commandProperties={ @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="10") #10次统计 @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value="10") #休眠时间窗 @HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="60") #错误百分比 })
feign
没引入feign时的服务见调用相对比较麻烦,引入feign后:feign是声明式的web service客户端,它让微服务之间的调用变得更简单了,不用写让人恶心的url路径,类似controller调用service。
引入feign依赖,
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
在启动类上使用注解@EnableFeignClients
然后写接口:
@FeignClient("服务名称") public interface UserClient{ @GetMapping("请求路径") 返回值 方法名(参数列表); }
然后动态代理生成之歌实现类,spring自动注入了这个接口的实现类,我们在要使用的地方用@Autowried注入就可以了
但是这并不是最佳的方法,最佳的方法应该时服务提供方提供对应接口和方法,我们使用时直接集成他提供的接口,然后在接口上使用@FeignClient("服务名称")让它生效
最佳实践如下:商品微服务提供接口
public interface GoodsApi { /** * 根据spuid查询spu详情 * * @param spuId * @return */ @GetMapping("/spu/detail/{id}") SpuDetail querySpuDetailById(@PathVariable("id") Long spuId); }
然后再要调用这个服务的地方,继承它,并且加上@FeignClient("服务名称")注解让它生效
@FeignClient("item-service") public interface GoodsClient extends GoodsApi { }
- 超时时长配置
引入feign后超时时长配置:(超时后抛出异常)
ribbon.connectiontimeout:连接超时时长
ribbon.readtimeout:读取返回的超时时长
- feign熔断配置
feign的熔断和hystrix不一样
熔断:配置feign.hystrix.enable:true 开启熔断机制(feign是默认关闭的)
然后在远程服务接口上使用@FeignClient注解的fallback属性指定(注意指定的是一个类,这个类需要实现@FeignClient注解的接口,并且需要注入spring容器)
@FeignClient(value="item-service",fallback=GoodsClientImpl.class ) public interface GoodsClient extends GoodsApi { }
@Component public class GoodsClientImpl implements GoodsClient { 实现的方法,就是对应的降级的方法 }
- feign请求压缩
feign.compression.request.enable:true #开启请求压缩
feign.compression.request.mime-type:text/html,.... #设置压缩类型
feign.compression.request。min-request-size:2048 #设置触发压缩的大小下限,大于这个值才压缩
feign.compression.response.enable:true #开启响应压缩
- feign日志控制(了解)
zuul网关
Zuul是NetFlix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用。Zuul的核心是一些列的过滤器,这些过滤器可以完成以下功能。
- 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符合的请求。
- 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
- 动态路由:动态的将请求路由到不同的后端集群。
- 压力测试:逐渐增加指向集群的流量,以了解性能。
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求。
- 静态响应处理:在边缘位置直接建立部分响应,从而避免其转发到内部集群。
- 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB(Elastic Load Balancing)使用的多样化,以及让系统的便越更贴近系统的使用者。
启动类加@EnzbleZuulServer或者@EnableZuulProxy,俩者区别:EnableZuulProxy是高配版,多了几个过滤器,具体后面完善
zuul路由配置,如果不配置路由规则zuul会自动生成zuul.routes.服务id:服务id/**的路由规则,也可以自己配置zuul.routes.服务id:路由规则
zuul: prefix: /api #全局路由前缀。有这个前缀才会进来进行路由匹配,转发是忽略 routes: item-service: /item/** search-service: /search/** upload-service: path: /upload/** serviceid: upload-service strip-prefix: false #不忽略路由前缀,就是转发时候带着upload转发请求 user-service: /user/** auth-service: /auth/** cart-service: /cart/** add-host-header: true #添加地址头信息, sensitive-headers: #添加地址头信息后一些敏感头信息会被过滤掉这个配置就是配置忽略的头生么都不写就是都不过滤,如果不配置内部会默认过滤掉cookie一些敏感头信息,这样配置让cookie可以正常写入,做jwt鉴权时候需要
如果一些服务不想对外暴露可以使用配置:zuul.ignored-service=服务id
网关允许跨域请求
//跨域请求设置 @Configuration public class GlobalCorsConfig { @Bean public CorsFilter corsFilter(){ // 1.添加CORS配置信息 CorsConfiguration corsConfiguration = new CorsConfiguration(); // 允许的域,不要写*(代表匹配所有域),否则cookie无法使用 corsConfiguration.addAllowedOrigin("http://manage.leyou.com"); corsConfiguration.addAllowedOrigin("http://www.leyou.com"); // 是否发送cookie信息 corsConfiguration.setAllowCredentials(true); // 允许的请求方式 corsConfiguration.addAllowedMethod("OPTIONS"); corsConfiguration.addAllowedMethod("HEAD"); corsConfiguration.addAllowedMethod("GET"); corsConfiguration.addAllowedMethod("PUT"); corsConfiguration.addAllowedMethod("POST"); corsConfiguration.addAllowedMethod("DELETE"); corsConfiguration.addAllowedMethod("PATCH"); // 允许的头信息 corsConfiguration.addAllowedHeader("*"); // 预检请求有效期 corsConfiguration.setMaxAge(3600L); // 添加映射路径,我们拦截一切请求 UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",corsConfiguration); // 返回新的CorsFilter return new CorsFilter(urlBasedCorsConfigurationSource); } }
zuul过滤器和登陆拦截
ZuulFilter implement IZullFilter
我们可以实现ZuulFilter类完成一次额过滤逻辑
方法:
- public String filterType() { return "pre"; }
方法返回值表示过滤器类型,有一次啊类型:
- pre
可以在请求被路由之前调用。适用于身份认证的场景,认证通过后再继续执行下面的流程。
- route
在路由请求时被调用。适用于灰度发布场景,在将要路由的时候可以做一些自定义的逻辑。
- post
在 route 和 error 过滤器之后被调用。这种过滤器将请求路由到达具体的服务之后执行。适用于需要添加响应头,记录响应日志等应用场景。
- error
处理请求时发生错误时被调用。在执行过程中发送错误时会进入 error 过滤器,可以用来统一记录错误信息。
2.public int filterOrder() { return 10; }
表示过滤器的执行顺序,数字越小,优先度越高
3.public boolean shouldFilter()
是否执行该过滤器,true 为执行,false 为不执行,这个也可以利用配置中心来实现,达到动态的开启和关闭过滤器。
4.public Object run() throws ZuulException
过滤器的执行逻辑,通过获取zuul上下文对象。然后再逻辑判断之后通过context.setSendZuulResponse(false);设置不转发给微服务context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());返回对应信息给前端
//过滤逻辑, @Override public Object run() throws ZuulException { //初始化zuul上下文对象 RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); //获取cookie中的token信息 String cookie = CookieUtils.getCookieValue(request, jwtProperties.getCookieName()); try { JwtUtils.getInfoFromToken(cookie,jwtProperties.getPublicKey()); } catch (Exception e) { e.printStackTrace(); context.setSendZuulResponse(false); //设置不转发请求 //设置响应状态码,401 context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); } return null; }
//未登录用户过滤 @Component @EnableConfigurationProperties({JwtProperties.class, FilterProperties.class}) public class LoginFilter extends ZuulFilter { @Autowired private JwtProperties jwtProperties; @Autowired private FilterProperties filterProperties; //过滤器类型,pre前置过滤器,请求进入zuul执行 @Override public String filterType() { return "pre"; } //过滤器执行顺序 @Override public int filterOrder() { return 10; } //表示是否执行run过滤 @Override public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); String requestURL = request.getRequestURL().toString();//http://www.****.com/***全路径 List<String> allowPaths = filterProperties.getAllowPaths();//白名单,不进行登陆判断 for (String allowPath : allowPaths){ if (allowPaths.contains(allowPath)){//判断完整的url是否包含白名单片段 //白名单直接不执行run过滤 return false; } } return true; } //过滤逻辑, @Override public Object run() throws ZuulException { //初始化zuul上下文对象 RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); //获取cookie中的token信息 String cookie = CookieUtils.getCookieValue(request, jwtProperties.getCookieName()); try { JwtUtils.getInfoFromToken(cookie,jwtProperties.getPublicKey()); } catch (Exception e) { e.printStackTrace(); context.setSendZuulResponse(false); //设置不转发请求 //设置响应状态码,401 context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); } return null; } }
@ConfigurationProperties(prefix = "leyou.jwt") public class JwtProperties { private String pubKeyPath; //公钥 private String cookieName; private PublicKey publicKey;//公钥对象 private final Logger log = LoggerFactory.getLogger(JwtProperties.class); @PostConstruct public void init() { try{ File pubKey = new File(pubKeyPath); if (pubKey.exists()){ this.publicKey=RsaUtils.getPublicKey(pubKeyPath); } }catch (Exception e){ log.error("获取公钥失败",e); throw new RuntimeException(); } } public String getPubKeyPath() { return pubKeyPath; } public void setPubKeyPath(String pubKeyPath) { this.pubKeyPath = pubKeyPath; } public PublicKey getPublicKey() { return publicKey; } public String getCookieName() { return cookieName; } public void setCookieName(String cookieName) { this.cookieName = cookieName; } }
@ConfigurationProperties(prefix = "leyou.filter") public class FilterProperties { private List<String> allowPaths; public List<String> getAllowPaths() { return allowPaths; } public void setAllowPaths(List<String> allowPaths) { this.allowPaths = allowPaths; } }