Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过“心跳”机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。
这就实现了服务的自动注册、发现、状态监控。
原理图
Eureka就是服务注册中心(可以是一个集群),对外暴露自己的地址
- 提供者:启动后向Eureka注册自己信息(地址,提供什么服务)
- 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新
- 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态
server:
port: 8007 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://127.0.0.1:${server.port}/eureka
register-with-eureka: false
fetch-registry: false
@SpringBootApplication
@EnableEurekaServer
public class SpringclouddemoeurekaApplication {
public static void main(String[] args) {
SpringApplication.run(SpringclouddemoeurekaApplication.class, args);
}
}
注册到eureka服务
server:
port: 8081
spring:
application:
name: service-provider # 应用名称,注册到eureka后的服务名称
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:8007/eureka
@SpringBootApplication
//注册中心 客户端 其他服务可以
@EnableDiscoveryClient
//或 只适用于Eureka服务
@EnableEurekaClient
public class SpringclouddemozuulApplication {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(SpringclouddemozuulApplication.class, args);
}
}
@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient; // eureka客户端,可以获取到eureka中服务的信息
@GetMapping
@ResponseBody
public User queryUserById(@RequestParam("id") Long id){
// 根据服务名称,获取服务实例。有可能是集群,所以是service实例集合
List instances = discoveryClient.getInstances("service-provider");
// 因为只有一个Service-provider。所以获取第一个实例
ServiceInstance instance = instances.get(0);
// 获取ip和端口信息,拼接成服务地址
String baseUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/user/" + id;
User user = this.restTemplate.getForObject(baseUrl, User.class);
return user;
}
}
负载均衡Ribbon
Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
开启负载均衡@Bean
@LoadBalanced // 开启负载均衡的注解
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
controller类 url的ip与端口变成服务名称
@Controller
@RequestMapping("/consumer")
public class StuController {
@Autowired
RestTemplate restTemplate;
@RequestMapping("/findStu")
@ResponseBody
public List findStu(){
String baseUrl = "http://service-provider/findStu";
List list = restTemplate.getForObject(baseUrl, List.class);
return list;
}
}
负载均衡默认设置是轮询
改为随机的要在服务的调用者上加上这个配置,服务名称.ribbon.NFLoadBalancerRuleClassName
service-provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
熔断机制Hystrix
Hystrix,英文意思是豪猪,全身是刺,看起来就不好惹,是一种保护机制。
Hystrix也是Netflix公司的一款组件。
主页:https://github.com/Netflix/Hystrix/
那么Hystix的作用是什么呢?具体要保护什么呢?
Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路:
如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。
如果此时,某个服务出现异常:
例如微服务I发生异常,请求阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。
Hystix解决雪崩问题的手段有两个:
- 线程隔离
- 服务熔断
线程隔离示意图:
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队(防止雪崩).加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?
服务降级:优先保证核心服务,而非核心服务不可用或弱可用。
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。
触发Hystix服务降级的情况:
- 线程池已满
- 请求超时
开启熔断 注解org.springframework.cloud spring-cloud-starter-netflix-hystrix
//熔断的注解
@EnableCircuitBreaker
//注册中心的客户端
@EnableEurekaServer
@SpringBootApplication
public class SpringclouddemocustomerApplication {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(SpringclouddemocustomerApplication.class, args);
}
}
可以看到,我们类上的注解越来越多,在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解:@SpringCloudApplication
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
@SpringCloudApplication
public class GecServiceConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(GecServiceConsumerApplication.class, args);
}
}
编写降级逻辑
我们改造gec-service-consumer,当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用HystixCommond来完成:
修改com.gec.service.controller.StuController 添加降级代码
@Controller
@RequestMapping("/consumer")
public class StuController {
@Autowired
RestTemplate restTemplate;
@RequestMapping("/findStu")
@ResponseBody
@HystrixCommand(fallbackMethod = "findStuFallBack" )
public String findStu(){
String baseUrl = "http://service-provider/findStu";
List list = restTemplate.getForObject(baseUrl, List.class);
return list.toString();
}
// 服务降级逻辑方法
public String findStuFallBack(){
return "请求超时,稍候再试";
}
}
@HystrixCommand(fallbackMethod = “queryByIdFallBack”):用来声明一个降级逻辑的方法
当gec-service-provder正常提供服务时,访问与以前一致。但是当我们将gec-service-provider停机时,会发现页面返回了降级处理信息:
设置超时
**在之前的案例中,请求在超过1秒后都会返回错误信息,这是因为Hystix的默认超时时长为1,我们可以通过配置修改这个值:
**
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds来设置Hystrix超时时间。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms
Feign——整合Ribbon,hystrix
Feign 可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
Feign 只需定义接口并且使用注解就可以使用
Feign 还集成了RIbbon,默认支持负载均衡功能
开启Feign功能org.springframework.cloud spring-cloud-starter-openfeign
@SpringBootApplication
feign
@EnableFeignClients
熔断的注解
@EnableCircuitBreaker
//@SpringCloudApplication
//注册中心
@EnableEurekaServer
public class CloudNewsWebApplication {
public static void main(String[] args) {
SpringApplication.run(CloudNewsWebApplication.class, args);
}
}
删除RestTemplate:feign已经自动集成了Ribbon负载均衡的RestTemplate。所以,此处不需要再注册RestTemplate。
在gec-service-consumer工程中,添加UserClient远程调用接口
@FeignClient(value = "cloud-news-query", qualifier = "cloud-news-query", fallback = NewsClientFallback.class)
public interface NewsFeignClient {
@LoadBalanced
@GetMapping("/queryNews")
ResultDTO> queryNews();
@LoadBalanced
@GetMapping("/queryCategory")
ResultDTO> queryCategory();
@LoadBalanced
@GetMapping("/queryNewById")
ResultDTO queryNewById(@RequestParam(name = "id") Integer id);
@LoadBalanced
@PostMapping("/findByCategoryIdTitle")
ResultDTO> findByCategoryIdTitle(@RequestBody News news);
}
首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像
@FeignClient,声明这是一个Feign客户端,类似@Mapper注解。同时通过value属性指定服务名称
接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果
feign:
hystrix:
enabled: true # 开启feign 的熔断功能
实现刚才编写的StuClient接口,作为fallback的处理类
@Component
public class NewsClientFallback implements NewsFeignClient{
@Override
public ResultDTO> queryNews() {
return ResultUtil.error(CodeEnum.FAil.code,"请求超时");
}
@Override
public ResultDTO> queryCategory() {
return ResultUtil.error(CodeEnum.FAil.code,"请求超时");
}
@Override
public ResultDTO queryNewById(Integer id) {
return ResultUtil.error(CodeEnum.FAil.code,"请求超时");
}
@Override
public ResultDTO> findByCategoryIdTitle(News news) {
return ResultUtil.error(CodeEnum.FAil.code,"请求超时");
}
}
controller类
@Controller
public class NewsController {
@Qualifier("cloud-news-query")
@Autowired
NewsFeignClient newsFeignClient;
@Qualifier("cloud-news-edit")
@Autowired
NewsEditFeignClient newsEditFeignClient;
@GetMapping("/queryNews")
public ModelAndView queryNews(@ModelAttribute("newsCondition") News news) {
ModelAndView modelAndView = new ModelAndView();
ResultDTO> newsResultDTO = newsFeignClient.queryNews();
ResultDTO> categoryResultDTO = newsFeignClient.queryCategory();
modelAndView.addObject("newsList", newsResultDTO.getData());
modelAndView.addObject("cateList", categoryResultDTO.getData());
modelAndView.setViewName("index");
return modelAndView;
}
@GetMapping("/toUpdate/{id}")
public ModelAndView toupdate(@PathVariable int id) {
ModelAndView modelAndView = new ModelAndView();
ResultDTO newsResultDTO = newsFeignClient.queryNewById(id);
ResultDTO> categoryResultDTO = newsFeignClient.queryCategory();
modelAndView.addObject("newsDetail", newsResultDTO.getData());
modelAndView.addObject("categoryList", categoryResultDTO.getData());
modelAndView.setViewName("updateNews");
return modelAndView;
}
@PostMapping("/doUpdateSubmit")
public String doUpdateSubmit(News news) {
ResultDTO integerResultDTO = newsEditFeignClient.updateNewsById(news);
return "redirect:/queryNews";
}
@PostMapping("/queryByCondition")
public ModelAndView queryByCondition(@ModelAttribute("newsCondition") News news) {
ModelAndView modelAndView = new ModelAndView();
ResultDTO> newsResultDTO = newsFeignClient.findByCategoryIdTitle(news);
ResultDTO> categoryResultDTO = newsFeignClient.queryCategory();
modelAndView.addObject("newsList", newsResultDTO.getData());
modelAndView.addObject("cateList", categoryResultDTO.getData());
modelAndView.setViewName("index");
return modelAndView;
}
}
zuul-网关
使用Spring Cloud实现微服务的架构基本成型,大致是这样的:
我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。
在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。我们把焦点聚集在对外服务这块,直接暴露我们的服务地址,是不合理的
先来说说这样架构需要做的一些事儿以及存在的不足:
-
破坏了服务无状态特点。
为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。(在无状态的Web服务中,每一个Web请求都必须是独立的,请求之间是完全分离的。Server没有保存Client的状态信息,所以Client发送的请求必须包含有能够让服务器理解请求的全部信息,包括自己的状态信息。使得一个Client的Web请求能够被任何可用的Server应答,从而将Web系统扩展到大量的Client中。)
从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外考虑对接口访问的控制处理。
-
无法直接复用既有接口。
当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。
为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器的 服务网关。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
Zuul加入后的架构
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
org.springframework.cloud spring-cloud-starter-netflix-zuul
@EnableZuulProxy// 开启网关的注解
@SpringBootApplication
public class GecZuulApplication {
public static void main(String[] args) {
SpringApplication.run(GecZuulApplication.class, args);
}
}
spring:
application:
name: gec-zuul
server:
port: 8899
zuul:
routes:
service-provider: # 路由名称 通常都是微服务的名称
path: /service-provider # 映射路径
url: http://localhost:8080 # 映射路径对应的url地址
整合eureka注册服务,自动路由
org.springframework.cloud spring-cloud-starter-netflix-eureka-client
zuul:
routes:
service-provider: # 路由名称 通常都是微服务的名称
path: /service-provider # 映射路径
serviceId: service-provider # 服务名称
@EnableDiscoveryClient // 开启eureka客户端功能
@EnableZuulProxy // 开启网关的注解
@SpringBootApplication
public class GecZuulApplication {
public static void main(String[] args) {
SpringApplication.run(GecZuulApplication.class, args);
}
}
zuul:
routes:
service-provider: /service-provider
eureka:
client:
registry-fatch-interval-seconds: 5 # 获取服务列表的周期
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:8007/eureka
spring:
application:
name: gec-zuul
server:
port: 8899



