张逸说

出口成张,逸派胡言

0%

基于Spring Cloud的微服务落地

微服务架构模式的核心在于如何识别服务的边界,设计出合理的微服务。但如果要将微服务架构运用到生产项目上,并且能够发挥该架构模式的重要作用,则需要微服务框架的支持。

在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的依赖:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>

创建Spring Boot Application:

@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文件中也需要添加相关依赖:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

同时,我们还需要为服务命名,并指定地址。这些信息都可以在application.properties配置文件中配置:

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文件配置为:

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的依赖:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>

然后在主应用类ConosumerApplication中注入RestTemplate,并引入@LoadBalanced注解开启客户端负载均衡:

@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中:

@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文件中添加如下依赖:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

同时,还需要在被消费的微服务Application中添加@EnableFeignClients注解。例如在Statistics服务的应用程序类中:

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class StatisticsApplication {
public static void main(String[] args) {
SpringApplication.run(StatisticsApplication.class, args);
}
}

由于Account服务需要调用Statistics服务,因此需要在Account服务项目中增加对应的client接口:

@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()方法:

@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接口:

@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的依赖:

<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方式,取决于业务场景是否需要同步方式,还是异步方式;是高性能高并发,还是普通方式;是要求彻底解耦,还是做到一般的松散耦合。我们需要针对实际情况作出实际的判断,作出正确的选择。没有谁坏谁好之分,而是看谁更加的适合。

备注:本文部分内容来自PiggyMetrics,并参考了微服务与Spring Cloud相关文档。