您好,登錄后才能下訂單哦!
微服務架構模式的核心在于如何識別服務的邊界,設計出合理的微服務。
但如果要將微服務架構運用到生產項目上,并且能夠發揮該架構模式的重要作用,則需要微服務框架的支持。
在 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 的依賴:
<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: 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。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。