微服務架構模式的核心在於如何識別服務的邊界,設計出合理的微服務。但如果要將微服務架構運用到生產專案上,並且能夠發揮該架構模式的重要作用,則需要微服務框架的支援。
在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的組合對路由進行配置。
微服務技術是程式設計師繞不開的話題,想要了解更多微服務架構知識點的,可以關注我一下,我後續也會整理更多關於微服務架構這一塊的知識點分享出來,另外順便給大家推薦一個交流學習群:650385180,裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化這些成為架構師必備的知識體系。
分散式配置中心
為什麼要引入一個分散式配置中心?一個微服務就需要至少一個配置檔案,怎麼管理分散在各個微服務中的配置檔案呢?如果微服務採用的是不同的技術棧,如何來統一微服務的配置呢?微服務是部署在不同的節點中,顯然我們無法在單機中實現對分散式節點的配置管理。這就是引入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方式,取決於業務場景是否需要同步方式,還是非同步方式;是高效能高併發,還是普通方式;是要求徹底解耦,還是做到一般的鬆散耦合。我們需要針對實際情況作出實際的判斷,作出正確的選擇。沒有誰壞誰好之分,而是看誰更加的適合。