新网创想网站建设,新征程启航
为企业提供网站建设、域名注册、服务器等服务
微服务架构模式的核心在于如何识别服务的边界,设计出合理的微服务。
目前创新互联已为成百上千家的企业提供了网站建设、域名、雅安服务器托管、网站托管运营、企业网站设计、八步网站维护等服务,公司将坚持客户导向、应用为本的策略,正道将秉承"和谐、参与、激情"的文化,与客户和合作伙伴齐心协力一起成长,共同发展。
但如果要将微服务架构运用到生产项目上,并且能够发挥该架构模式的重要作用,则需要微服务框架的支持。
在 Java 生态圈,目前使用较多的微服务框架就是集成了包括 Netflix OSS 以及 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 实现的消息驱动的微服务。
Spring Cloud AWS:简化和整合 Amazon Web Service。
Spring Cloud Security:安全工具包,提供 Zuul 代理中对 OAuth3 客户端请求的中继器。
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 的依赖:
org.springframework.cloud spring-cloud-starter-eureka-server
创建 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 文件中也需要添加相关依赖:
org.springframework.cloud spring-cloud-starter-eureka
同时,我们还需要为服务命名,并指定地址。这些信息都可以在 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 的依赖:
org.springframework.cloud spring-cloud-starter-eureka org.springframework.cloud spring-cloud-starter-ribbon
然后在主应用类 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 文件中添加如下依赖:
org.springframework.cloud spring-cloud-starter-feign
同时,还需要在被消费的微服务 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 的依赖:
org.springframework.cloud spring-cloud-starter-hystrix
然后在 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 文件中,添加如下依赖:
org.springframework.cloud spring-cloud-starter-hystrix org.springframework.cloud spring-cloud-starter-hystrix-dashboard org.springframework.cloud spring-cloud-starter-actuator
服务的 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 服务,需要添加如下依赖:
org.springframework.cloud spring-cloud-config-server
服务的 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: oauth3: 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 方式,取决于业务场景是否需要同步方式,还是异步方式;是高性能高并发,还是普通方式;是要求彻底解耦,还是做到一般的松散耦合。
我们需要针对实际情况作出实际的判断,作出正确的选择。没有谁坏谁好之分,而是看谁更加的适合。
作者:张逸
简介:架构编码实践者,IT 文艺工作者,大数据平台架构师,兼爱 OO 与 FP,热衷于编程语言学习与技艺提升,致力于将主流领域驱动设计与函数式编程、响应式编程以及微服务架构完美结合。他的个人微信公众号为「逸言」,个人博客:http://zhangyi.xyz。