标签
| 架构 微服务 Spring Cloud
字数
| 14416字
阅读
| 37分钟
微服务架构模式的核心在于如何识别服务的边界,设计出合理的微服务。但如果要将微服务架构运用到生产项目上,并且能够发挥该架构模式的重要作用,则需要微服务框架的支持。
在Java生态圈,目前使用较多的微服务框架就是集成了包括Netfilix OSS以及Spring的Spring Cloud。它包括:
Spring Cloud Config:配置管理工具,支持使用Git存储配置内容,可以实现应用配置的外部化存储,支持客户端配置信息刷新、加密/解密配置内容等。
Spring Cloud Netflix:对Netflix OSS进行了整合。其中又包括:
Eureka:服务治理组件,包含服务注册中心、服务注册与发现。
Hystrix:容器管理组件,实现断路器模式,倘若依赖的服务出现延迟或故障,则提供强大的容错功能。
Ribbon:客户端负载均衡的服务调用组件。
Feign:基于Ribbon和Hystrix的声明式服务调用组件。
Zuul:网关组件,提供智能路由、访问过滤等功能。
Archaius:外部化配置组件。
Spring Cloud Bus:事件、消息总线。
Spring Cloud Cluster:针对ZooKeeper、Redis、Hazelcast、Consul的选举算法和通用状态模式的实现。
Spring Cloud Cloudfoundry:与Pivotal Cloudfoundry的整合支持。
Spring Cloud Consul:服务发现与配置管理工具。
Spring Cloud Stream:通过Redis、Rabbit或者Kafka实现的消息驱动的微服务。
Spirng Cloud AWS:简化和整合Amazon Web Service。
Spring Cloud Security:安全工具包,提供Zuul代理中对OAuth2客户端请求的中继器。
Spring Cloud Sleuth:Spring Cloud应用的分布式跟踪实现,可以整合Zipkin。
Spring Cloud ZooKeeper:基于ZooKeeper的服务发现与配置管理组件。
Spring Cloud Starters:Spring Cloud的基础组件,是基于Spring Boot风格项目的基础依赖模块。
Spring Cloud CLI:用于在Groovy中快速创建Spring Cloud应用的Spring Boot CLI插件。
服务治理
当一个系统的微服务数量越来越多的时候,我们就需要对服务进行治理,提供统一的服务注册中心,然后在其框架下提供发现服务的功能。这样就避免了对多个微服务的配置,以及微服务之间以及与客户端之间的耦合。
Spring Cloud Eureka是对Netflix Eureka的包装,用以实现服务注册与发现。Eureka服务端即服务注册中心,支持高可用配置。它依托于强一致性提供良好的服务实例可用性,并支持集群模式部署。Eureka客户端则负责处理服务的注册与发现。客户端服务通过annotation与参数配置的方式,嵌入在客户端应用程序代码中。在运行应用程序时,Eureka客户端向注册中心注册自身提供的服务,并周期性地发送心跳更新它的服务租约。
搭建服务注册中心
服务注册中心是一个独立部署的服务(你可以认为它也是一个微服务),所以需要单独为它创建一个项目,并在pom.xml中添加Eureka的依赖:
1 2 3 4
<dependency > <groupId > org.springframework.cloudgroupId > <artifactId > spring-cloud-starter-eureka-serverartifactId > dependency >
创建Spring Boot Application:
1 2 3 4 5 6 7
@EnableEurekaServer @SpringBootApplication public class Application { public static void main (String[] args) { new SpringApplicationBuilder(Application.class).web(true ).run(args); } }
注册服务提供者
要让自己编写的微服务能够注册到Eureka服务器中,需要在服务的Spring Boot Application中添加
@EnableDiscoveryClient
注解,如此才能让Eureka服务器发现该服务。当然,pom.xml文件中也需要添加相关依赖:
1 2 3 4
<dependency > <groupId > org.springframework.cloudgroupId > <artifactId > spring-cloud-starter-eurekaartifactId > dependency >
同时,我们还需要为服务命名,并指定地址。这些信息都可以在application.properties配置文件中配置:
1 2 3
spring.application.name=demo-service eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
说明:
Spring更推荐使用yml文件来维护系统的配置,yml文件可以体现出配置节的层次关系,表现力比单纯的key-value形式更好。如果结合使用后面讲到的Spring Cloud Config,则客户端的配置文件必须命名为bootstrap.properties或者bootstrap.yml。与上述配置相同的yml文件配置为:
1 2 3 4 5 6 7 8
spring: application: name: demo-service eureka: client: serviceUrl: defaultZone: http://localhost:1111/eureka/
服务发现与消费
在微服务架构下,许多微服务可能会扮演双重身份。一方面它是服务的提供者,另一方面它又可能是服务的消费者。注册在Eureka Server中的微服务可能会被别的服务消费。此时,就相当于在服务中创建另一个服务的客户端,并通过RestTemplate发起对服务的调用。为了更好地提高性能,可以在服务的客户端引入Ribbon,作为客户端负载均衡。
现在假定我们要为demo-service创建一个服务消费者demo-consumer。该消费者自身也是一个Spring Boot微服务,同时也能够被Eureka服务器注册。这时,就需要在该服务的pom.xml中添加eureka与ribbon的依赖:
1 2 3 4 5 6 7 8
<dependency > <groupId > org.springframework.cloudgroupId >
<artifactId > spring-cloud-starter-eurekaartifactId > dependency ><dependency > <groupId > org.springframework.cloudgroupId > <artifactId > spring-cloud-starter-ribbonartifactId > dependency >
然后在主应用类
ConosumerApplication
中注入
RestTemplate
,并引入
@LoadBalanced
注解开启客户端负载均衡:
1 2 3 4 5 6 7 8 9 10 11 12
@EnableDiscoveryClient @SpringBootApplication public class ConsumerApplication { @Bean @LoadBalanced RestTemplate restTemplate () { return new RestTemplate(); } public static void main (String[] args) { SpringApplication.run(ConsumerApplication.class, args) } }
假设消费demo-service的客户端代码写在demo-consumer服务的其中一个Controller中:
1 2 3 4 5 6 7 8 9 10
@RestController public class ConsumerController { @Autowired RestTemplate restTemplate; @RequestMapping (value = "/demo-consumer" , method = RequestMethod.Get) public String helloConsumer () { return restTemplate.getForEntity("http://demo-service/demo" , String.class).getBody(); } }
通过
RestTemplate
就可以发起对demo-service的消费调用。
声明式服务调用
通过Ribbon和Hystrix可以实现对微服务的调用以及容错保护,但Spring Cloud还提供了另一种更简单的声明式服务调用方式,即Spring Cloud Feign。Feign实际上就是对Ribbon与Hystrix的进一步封装。通过Feign,我们只需创建一个接口并用annotation的方式配置,就可以完成对服务供应方的接口(REST API)绑定。
假设我们有三个服务:
Notification Service
Account Service
Statistics Service
服务之间的依赖关系如下图所示:
要使用Feign来完成声明式的服务调用,需要在作为调用者的服务中创建Client。Client通过Eureka Server调用注册的对应服务,这样可以解除服务之间的耦合。结构如下图所示:
为了使用Feign,需要对应微服务的pom.xml文件中添加如下依赖:
1 2 3 4
<dependency > <groupId > org.springframework.cloudgroupId > <artifactId > spring-cloud-starter-feignartifactId > dependency >
同时,还需要在被消费的微服务Application中添加
@EnableFeignClients
注解。例如在Statistics服务的应用程序类中:
1 2 3 4 5 6 7 8
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class StatisticsApplication { public static void main (String[] args) { SpringApplication.run(StatisticsApplication.class, args); } }
由于Account服务需要调用Statistics服务,因此需要在Account服务项目中增加对应的client接口:
1 2 3 4 5 6 7
@FeignClient (name = "statistics-service" )public interface StatisticsServiceClient { @RequestMapping (method = RequestMethod.PUT, value = "/statistics/{accountName}" , consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) void updateStatistics (@PathVariable("accountName" ) String accountName, Account account) ; }
StatisticsServiceClient接口的
updateStatistics()
方法会调用URI为
/statistics/{accountName}
的REST服务,且HTTP动词为put。这个服务其实对应就是Statistics Service中StatisticsController类中的
saveStatistics()
方法:
1 2 3 4 5 6 7 8 9 10 11
@RestController public class StatisticsController { @Autowired private StatisticsService statisticsService; @RequestMapping (value = "/{accountName}" , method = RequestMethod.PUT) public void saveStatistics (@PathVariable String accountName, @Valid @RequestBody Account account) { statisticsService.save(accountName, account); } }
在Account服务中,如果要调用Statistics服务,都应该通过StatisticsServiceClient接口进行调用。例如,Account服务中的AccountServiceImpl要调用
updateStatistics()
方法,就可以在该类的实现中通过
@autowired
注入StatisticsServiceClient接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@Service public class AccountServiceImpl implements AccountService { @Autowired private StatisticsServiceClient statisticsClient; @Autowired private AccountRepository repository; @Override public void saveChanges (String name, Account update) { statisticsClient.updateStatistics(name, account); } }
Notification服务对Account服务的调用如法炮制。
服务容错保护
在微服务架构中,微服务之间可能存在依赖关系,例如Notification Service会调用Account Service,Account Service调用Statistics Service。真实产品中,微服务之间的调用会更加寻常。倘若上游服务出现了故障,就可能会因为依赖关系而导致故障的蔓延,最终导致整个系统的瘫痪。
Spring Cloud Hystrix通过实现断路器(Circuit Breaker)模式以及线程隔离等功能,实现服务的容错保护。
仍然参考前面的例子。现在系统的微服务包括:
上游服务:demo-service
下游服务:demo-consumer
Eureka服务器:eureka-server
假设上游服务可能会出现故障,为保证系统的健壮性,需要在下游服务中加入容错包含功能。首先需要在demo-consumer服务中添加对hystrix的依赖:
1 2 3 4
<dependency > <groupId > org.springframework.cloudgroupId > <artifactId > spring-cloud-starter-hystrixartifactId > dependency >
然后在demo-consumer的应用程序类中加入
@EnableCircuitBreaker
开启断路器功能:
1 2 3 4 5 6 7 8 9 10 11 12 13
@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
注解的定义如下所示:
1 2 3 4 5 6 7 8
@Target (ElementType.TYPE)@Retention (RetentionPolicy.RUNTIME)@Documented @Inherited @SpringBootApplication @EnableDiscoveryClient @EnableCircuitBreaker public @interface SpringCloudApplication {}
接下来,需要引入一个新的服务类来封装hystrix提供的断路器保护功能,主要是定义当故障发生时需要执行的回调逻辑,即代码中指定的fallbackMethod:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
@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文件中,添加如下依赖:
1 2 3 4 5 6 7 8 9 10 11 12
<dependency > <groupId > org.springframework.cloudgroupId > <artifactId > spring-cloud-starter-hystrixartifactId > dependency ><dependency > <groupId > org.springframework.cloudgroupId > <artifactId > spring-cloud-starter-hystrix-dashboardartifactId > dependency ><dependency > <groupId > org.springframework.cloudgroupId > <artifactId > spring-cloud-starter-actuatorartifactId > 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类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
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:
1 2 3 4 5 6 7 8 9 10 11 12
@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网关的生命周期与过滤过程: