服务容错保护
在微服务架构中,微服务之间可能存在依赖关系,例如Notification Service会调用Account Service,Account Service调用Statistics Service。真实产品中,微服务之间的调用会更加寻常。倘若上游服务出现了故障,就可能会因为依赖关系而导致故障的蔓延,最终导致整个系统的瘫痪。
Spring Cloud Hystrix通过实现断路器(Circuit Breaker)模式以及线程隔离等功能,实现服务的容错保护。
仍然参考前面的例子。现在系统的微服务包括:
- 上游服务:demo-service
- 下游服务:demo-consumer
- Eureka服务器:eureka-server
假设上游服务可能会出现故障,为保证系统的健壮性,需要在下游服务中加入容错包含功能。首先需要在demo-consumer服务中添加对hystrix的依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency>
然后在demo-consumer的应用程序类中加入@EnableCircuitBreaker
开启断路器功能:
@EnableCircuitBreaker @EnableDiscoveryClient @SpringBootApplication public class ConsumerApplication { @Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args) } }
注意:Spring Cloud提供了@SpringCloudApplication
注解简化如上代码。该注解事实上已经包含了前面所述的三个注解。@SpringCloudApplication
注解的定义如下所示:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootApplication @EnableDiscoveryClient @EnableCircuitBreaker public @interface SpringCloudApplication {}
接下来,需要引入一个新的服务类来封装hystrix提供的断路器保护功能,主要是定义当故障发生时需要执行的回调逻辑,即代码中指定的fallbackMethod:
@Service public class ConsumerService { @Autowired RestTemplate restTemplate; @HystrixCommand(fallbackMethod = "consumerFallback") public String consume() { return restTemplate.getForEntity("http://demo-service/demo", String.class).getBody(); } public String consumerFallback() { return "error"; } } @RestController public class ConsumerController { @Autowired ConsumerService consumerService; @RequestMapping(value = "/demo-consumer", method = RequestMethod.Get) public String helloConsumer() { return consumerService.consume(); } }
服务监控
微服务架构将服务的粒度分解的足够细,这使得它在保证服务足够灵活、足够独立的优势下,也带来了管理和监控上的挑战,服务与服务之间的依赖也变得越来越复杂。因此,对服务健康度和运行指标的监控就变得非常重要。
Hystrix提供了Dashboard用以监控Hystrix的各项指标信息。为了监控整个系统的微服务,我们需要为Hystrix Dashboard建立一个Spring Boot微服务。在该服务项目的pom文件中,添加如下依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-actuator</artifactId> </dependency>
服务的Application类需要添加@EnableHystrixDashboard
,以启用Hystrix Dashboard功能。同时,可能需要根据实际情况修改application.properties配置文件,例如选择可用的端口号等。
如果要实现对集群的监控,则需要加入Turbine。
API网关
理论上,客户端可以直接向每个微服务直接发送请求。但是这种方式是存在挑战和限制的,调用者需要知道所有端点的地址,分别对每一段信息执行http请求,然后将结果合并到客户端。
一般而言,针对微服务架构模式的系统,采用的都是前后端分离的架构。为了明显地隔离开前端与后端的边界,我们通常可以专门为前端的消费者定义更加粗粒度的Open Service。这些Open Service是对外的RESTful API服务,可以通过F5、Nginx等网络设备或工具软件实现对各个微服务的路由与负载均衡,并公开给外部的客户端调用(注意,内部微服务之间的调用并不需要通过Open Service)。这种对外公开的Open Service通常又被称为边缘服务(edge service)。
如果这些Open Service需要我们自己去开发实现并进行服务的运维,在系统规模不断增大的情况下,会变得越来越困难。例如,当增加了新的微服务又或者IP地址发生变动时,都需要运维人员手工维护这些路由规则与服务实例列表。又例如针对所有垂直分隔的微服务,不可避免存在重用的横切关注点,例如用户身份认证、授权或签名校验等机制。我们不能在所有微服务中都去添加这些相同的功能,因为这会造成横切关注点的冗余。
解决的办法是引入API网关(API Gateway)。它是系统的单个入口点,用于通过将请求路由到适当的后端服务或者通过调用多个后端服务并聚合结果来处理请求。此外,它还可以用于认证、insights、压力测试、金丝雀测试(canary testing)、服务迁移、静态响应处理和主动变换管理。Spring Cloud为API网关提供的解决方案就是Spring Cloud Zuul,它是对Netflix Zuul的包装。
路由规则与服务实例维护
Zuul解决路由规则与服务实例维护的方法是通过Spring Cloud Eureka。API Gateway自身就是一个Spring Boot服务,该服务自身被注册为Eureka服务治理下的应用,同时它会从Eureka中获得所有其他微服务的实例信息。这样的设计符合DRY原则,因为Eureka已经维护了一套服务实例信息,Zuul直接重用了这些信息,无需人工介入。
对于路由规则,Zuul默认会将服务名作为ContextPath创建路由映射,基本上这种路由映射机制就可以满足微服务架构的路由需求。倘若需要一些特殊的配置,Zuul也允许我们自定义路由规则,可以通过在API网关的Application类中创建PatternServiceRouteMapper来定义自己的规则。
横切关注点
诸如授权认证、签名校验等业务逻辑本身与微服务应用所要处理的业务逻辑没有直接关系,我们将这些可能横跨多个微服务的功能称为“横切关注点”。这些横切关注点往往会作为“装饰”功能在服务方法的前后被调用。Spring Cloud Zuul提供了一套过滤器机制,允许开发者创建各种过滤器,并指定哪些规则的请求需要执行哪个过滤器。
自定义的过滤器继承自ZuulFilter类。例如我们要求客户端发过来的请求在路由之前需要先验证请求中是否包含accessToken参数,如果有就进行路由,否则就拒绝,并返回401 Unauthorized错误,则可以定义AccessFilter类:
public class AccessFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(AccessFilter.class); @Override public String filterType() { return "pre" } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString()); Object accessToken = request.getParameter("accessToken"); if (accessToken == null) { log.warn("access token is empty"); ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); return null; } log.info("access token ok"); return null; } }
要让该自定义过滤器生效,还需要在Zuul服务的Application中创建具体的Bean:
@EnableZuulProxy @SpringCloudApplication public class ZuulApplication { public static void main(String[] args) { new SpringApplicatonBuilder(ZuulApplication.class).web(true).run(args); } @Bean public AccessFilter accessFilter() { return new AccessFilter(); } }
Zuul一共提供了四种过滤器:
- pre filter
- routing filter
- post filter
- error filter
下图来自官网,它展现了客户端请求到达Zuul API网关的生命周期与过滤过程:
通过starter添加Zuul的依赖时,自身包含了spring-cloud-starter-hystrix与spring-cloud-starter-ribbon模块的依赖,因此Zuul自身就拥有线程隔离与断路器的服务容错功能,以及客户端负载均衡。但是,倘若我们使用path与url的映射关系来配置路由规则,则路由转发的请求并不会采用HystrixCommand来包装,因而这类路由是没有服务容错与客户端负载均衡作用的。所以在使用Zuul时,应尽量使用path和serviceId的组合对路由进行配置。
分布式配置中心
为什么要引入一个分布式配置中心?一个微服务就需要至少一个配置文件,怎么管理分散在各个微服务中的配置文件呢?如果微服务采用的是不同的技术栈,如何来统一微服务的配置呢?微服务是部署在不同的节点中,显然我们无法在单机中实现对分布式节点的配置管理。这就是引入Spring Cloud Config的目的。
Spring Cloud Config提供了服务端和客户端支持。服务端是一个独立的微服务,同样可以注册到Eureka服务器中。每个需要使用分布式配置中心的微服务都是Spring Cloud Config的客户端。Spring Cloud Config默认实现基于Git仓库,既可以进行版本管理,还可以通过本地Git库起到缓存作用。Spring Cloud Config不限于基于Spring Cloud开发的系统,而是可以用于任何语言开发的程序,并支持自定义实现。
配置中心服务端
Spring Cloud Config Server作为配置中心服务端,提供如下功能:
- 拉取配置时更新git仓库副本,保证是最新结果
- 支持数据结构丰富,yml, json, properties等
- 配合Eureke可实现服务发现,配合cloud bus可实现配置推送更新
- 配置存储基于git仓库,可进行版本管理
- 简单可靠,有丰富的配套方案
建立一个Config服务,需要添加如下依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency>
服务的Application类需要添加@EnableConfigServer
注解:
@SpringBootApplication @EnableConfigServer public class ConfigApplication { public static void main(String[] args) { SpringApplication.run(ConfigApplication.class, args); } }
配置服务的基本信息和Git仓库的信息放在application.yml文件中:
spring: cloud: config: server: git: uri: http://localhost/workspace/springcloud-demo username: user password: password server: port: 8888 security: user: password: ${CONFIG_SERVICE_PASSWORD}
Git库与配置服务
在Config服务中配置了Git服务器以及Git库的信息后,我们就可以在git库中提交配置文件。存储在git库中配置文件的名字以及分支名(默认为master分支)会组成访问Config服务的URI。假设有一个服务为Notification服务,则它在配置中心服务端的配置文件为notification-dev.yml,内容如下:
devMode: true spring: application: name: notification jdbc: host: localhost port: 3306 user: root password: 123456 logging: file: demo
配置中心客户端
需要读取配置中心服务端信息的微服务都是配置中心的客户端,为了能够读取配置服务端的信息,这些微服务需要:
- 在pom中添加对spring-cloud-starter-config的依赖
- 在bootstrap.properties或者bootstrap.yml中配置获取配置的config-server位置
例如,Account服务的配置是由Spring Cloud Config进行管理的。在它的资源目录下,提供了bootstrap.yml配置文件,内容如下所示:
spring: application: name: account-service cloud: config: uri: http://config:8888 fail-fast: true password: ${CONFIG_SERVICE_PASSWORD} username: user
注意,该配置文件除了配置了该Account服务应用的name之外,主要是支持该应用获得配置服务端的信息。微服务自身的配置信息则统一放到配置中心服务端的文件中,并由Git库进行管理。例如,Account服务的详细配置在配置中心服务端的account-dev.yml文件中:
security: oauth2: client: clientId: account-service clientSecret: ${ACCOUNT_SERVICE_PASSWORD} accessTokenUri: http://auth-service:5000/uaa/oauth/token grant-type: client_credentials scope: server spring: data: mongodb: host: account-mongodb username: user password: ${MONGODB_PASSWORD} database: piggymetrics port: 27017 server: context-path: /accounts port: 6000
Spring Cloud Config通过Git实现分布式的配置管理。当配置中心服务端的配置信息发生变更时,各个作为配置客户端的微服务会向Git库提交pull更新,获得最新的配置信息。
当然,Spring Cloud Config还可以使用SVN库进行配置管理,也支持简单的本地文件系统的存储方式。此时需要将spring.profiles.active
设置为native,并设置搜索配置文件的路径。如果不配置路径,默认在src/main/resources
目录下搜索。如下配置文件:
spring: cloud: config: server: native: search-locations: classpath:/shared profiles: active: native
搜索路径放在classpath下的shared目录下,那么在代码中,目录就是resources/shared
。如果使用本地文件系统管理配置文件,则无法支持分布式配置管理以及版本管理,因此在生产系统下,还是推荐使用Git库的方式。
总结
在实施微服务时,我们可以将微服务视为两个不同的边界。一个是与前端UI的通信,称为Open Service(Edge Service),通过引入API Gateway来实现与前端UI的通信。另一个是在边界内业务微服务之间的通信,通过Feign实现微服务之间的协作。所有的微服务都会通过Eureka来完成微服务的注册与发现。一个典型的基于Spring Cloud的微服务架构如下所示:
微服务的集成可以通过Feign+Ribbon以RESTful方式实现通信,也可以基于RPC方式(可以结合Protocol Buffer)完成服务之间的通信,甚至可以通过发布事件与订阅事件的机制。事件机制可以使微服务之间更加松散耦合。这时,我们可以引入RabbitMQ或Kafka来做到服务与服务之间的解耦。事件机制是异步和非阻塞的,在某些业务场景下,它的性能会更加的好。Spring Cloud也提供了相关的组件Spring Cloud Stream来支持这种事件机制。
对于微服务之间的协作,到底选择Feign这种REST方式、事件机制或者RPC方式,取决于业务场景是否需要同步方式,还是异步方式;是高性能高并发,还是普通方式;是要求彻底解耦,还是做到一般的松散耦合。我们需要针对实际情况作出实际的判断,作出正确的选择。没有谁坏谁好之分,而是看谁更加的适合
本文链接: http://zhangyi.xyz/micro-service-based-on-spring-cloud/