javascript
Spring Cloud构建微服务架构(五)服务网关
通過之前幾篇Spring Cloud中幾個核心組件的介紹,我們已經可以構建一個簡略的(不夠完善)微服務架構了。比如下圖所示:
alt
我們使用Spring Cloud Netflix中的Eureka實現(xiàn)了服務注冊中心以及服務注冊與發(fā)現(xiàn);而服務間通過Ribbon或Feign實現(xiàn)服務的消費以及均衡負載;通過Spring Cloud Config實現(xiàn)了應用多環(huán)境的外部化配置以及版本管理。為了使得服務集群更為健壯,使用Hystrix的融斷機制來避免在微服務架構中個別服務出現(xiàn)異常時引起的故障蔓延。
在該架構中,我們的服務集群包含:內部服務Service A和Service B,他們都會注冊與訂閱服務至Eureka Server,而Open Service是一個對外的服務,通過均衡負載公開至服務調用方。本文我們把焦點聚集在對外服務這塊,這樣的實現(xiàn)是否合理,或者是否有更好的實現(xiàn)方式呢?
先來說說這樣架構需要做的一些事兒以及存在的不足:
- 首先,破壞了服務無狀態(tài)特點。為了保證對外服務的安全性,我們需要實現(xiàn)對服務訪問的權限控制,而開放服務的權限控制機制將會貫穿并污染整個開放服務的業(yè)務邏輯,這會帶來的最直接問題是,破壞了服務集群中REST API無狀態(tài)的特點。從具體開發(fā)和測試的角度來說,在工作中除了要考慮實際的業(yè)務邏輯之外,還需要額外可續(xù)對接口訪問的控制處理。
- 其次,無法直接復用既有接口。當我們需要對一個即有的集群內訪問接口,實現(xiàn)外部服務訪問時,我們不得不通過在原有接口上增加校驗邏輯,或增加一個代理調用來實現(xiàn)權限控制,無法直接復用原有的接口。
面對類似上面的問題,我們要如何解決呢?下面進入本文的正題:服務網關!
為了解決上面這些問題,我們需要將權限控制這樣的東西從我們的服務單元中抽離出去,而最適合這些邏輯的地方就是處于對外訪問最前端的地方,我們需要一個更強大一些的均衡負載器,它就是本文將來介紹的:服務網關。
服務網關是微服務架構中一個不可或缺的部分。通過服務網關統(tǒng)一向外系統(tǒng)提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了權限控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時將權限控制這些較重的非業(yè)務邏輯內容遷移到服務路由層面,使得服務集群主體能夠具備更高的可復用性和可測試性。
下面我們通過實例例子來使用一下Zuul來作為服務的路有功能。
準備工作
在使用Zuul之前,我們先構建一個服務注冊中心、以及兩個簡單的服務,比如:我構建了一個service-A,一個service-B。然后啟動eureka-server和這兩個服務。通過訪問eureka-server,我們可以看到service-A和service-B已經注冊到了服務中心。
alt
如果您還不熟悉如何構建服務中心和注冊服務,請先閱讀Spring Cloud構建微服務架構(一)服務注冊與發(fā)現(xiàn)。
如果您不想自己動手準備,可以從這里獲取示例代碼:http://git.oschina.net/didispace/SpringBoot-Learning
開始使用Zuul
- 引入依賴spring-cloud-starter-zuul、spring-cloud-starter-eureka,如果不是通過指定serviceId的方式,eureka依賴不需要,但是為了對服務集群細節(jié)的透明性,還是用serviceId來避免直接引用url的方式吧。
|
? 1 2 3 4 5 6 7 8 9 |
? <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> |
- 應用主類使用@EnableZuulProxy注解開啟Zuul
|
? 1 2 3 4 5 6 7 8 9 |
? @EnableZuulProxy @SpringCloudApplication public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(true).run(args); } } |
這里用了@SpringCloudApplication注解,之前沒有提過,通過源碼我們看到,它整合了@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker,主要目的還是簡化配置。這幾個注解的具體作用這里就不做詳細介紹了,之前的文章已經都介紹過。
- application.properties中配置Zuul應用的基礎信息,如:應用名、服務端口等。
|
? 1 2 |
? spring.application.name=api-gateway server.port=5555 |
Zuul配置
完成上面的工作后,Zuul已經可以運行了,但是如何讓它為我們的微服務集群服務,還需要我們另行配置,下面詳細的介紹一些常用配置內容。
服務路由
通過服務路由的功能,我們在對外提供服務的時候,只需要通過暴露Zuul中配置的調用地址就可以讓調用方統(tǒng)一的來訪問我們的服務,而不需要了解具體提供服務的主機信息了。
在Zuul中提供了兩種映射方式:
- 通過url直接映射,我們可以如下配置:
|
? 1 2 3 |
? # routes to url zuul.routes.api-a-url.path=/api-a-url/** zuul.routes.api-a-url.url=http://localhost:2222/ |
該配置,定義了,所有到Zuul的中規(guī)則為:/api-a-url/**的訪問都映射到http://localhost:2222/上,也就是說當我們訪問http://localhost:5555/api-a-url/add?a=1&b=2的時候,Zuul會將該請求路由到:http://localhost:2222/add?a=1&b=2上。
其中,配置屬性zuul.routes.api-a-url.path中的api-a-url部分為路由的名字,可以任意定義,但是一組映射關系的path和url要相同,下面講serviceId時候也是如此。
- 通過url映射的方式對于Zuul來說,并不是特別友好,Zuul需要知道我們所有為服務的地址,才能完成所有的映射配置。而實際上,我們在實現(xiàn)微服務架構時,服務名與服務實例地址的關系在eureka server中已經存在了,所以只需要將Zuul注冊到eureka server上去發(fā)現(xiàn)其他服務,我們就可以實現(xiàn)對serviceId的映射。例如,我們可以如下配置:
|
? 1 2 3 4 5 6 7 8 |
? zuul.routes.api-a.path=/api-a/** zuul.routes.api-a.serviceId=service-A zuul.routes.api-b.path=/api-b/** zuul.routes.api-b.serviceId=service-B eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/ |
針對我們在準備工作中實現(xiàn)的兩個微服務service-A和service-B,定義了兩個路由api-a和api-b來分別映射。另外為了讓Zuul能發(fā)現(xiàn)service-A和service-B,也加入了eureka的配置。
接下來,我們將eureka-server、service-A、service-B以及這里用Zuul實現(xiàn)的服務網關啟動起來,在eureka-server的控制頁面中,我們可以看到分別注冊了service-A、service-B以及api-gateway
alt
嘗試通過服務網關來訪問service-A和service-B,根據配置的映射關系,分別訪問下面的url
- http://localhost:5555/api-a/add?a=1&b=2:通過serviceId映射訪問service-A中的add服務
- http://localhost:5555/api-b/add?a=1&b=2:通過serviceId映射訪問service-B中的add服務
- http://localhost:5555/api-a-url/add?a=1&b=2:通過url映射訪問service-A中的add服務
推薦使用serviceId的映射方式,除了對Zuul維護上更加友好之外,serviceId映射方式還支持了斷路器,對于服務故障的情況下,可以有效的防止故障蔓延到服務網關上而影響整個系統(tǒng)的對外服務
服務過濾
在完成了服務路由之后,我們對外開放服務還需要一些安全措施來保護客戶端只能訪問它應該訪問到的資源。所以我們需要利用Zuul的過濾器來實現(xiàn)我們對外服務的安全控制。
在服務網關中定義過濾器只需要繼承ZuulFilter抽象類實現(xiàn)其定義的四個抽象函數就可對請求進行攔截與過濾。
比如下面的例子,定義了一個Zuul過濾器,實現(xiàn)了在請求被路由之前檢查請求中是否有accessToken參數,若有就進行路由,若沒有就拒絕訪問,返回401 Unauthorized錯誤。
|
? 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 37 38 39 |
? 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(String.format("%s request to %s", 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; } } |
自定義過濾器的實現(xiàn),需要繼承ZuulFilter,需要重寫實現(xiàn)下面四個方法:
- filterType:返回一個字符串代表過濾器的類型,在zuul中定義了四種不同生命周期的過濾器類型,具體如下:
- pre:可以在請求被路由之前調用
- routing:在路由請求時候被調用
- post:在routing和error過濾器之后被調用
- error:處理請求時發(fā)生錯誤時被調用
- filterOrder:通過int值來定義過濾器的執(zhí)行順序
- shouldFilter:返回一個boolean類型來判斷該過濾器是否要執(zhí)行,所以通過此函數可實現(xiàn)過濾器的開關。在上例中,我們直接返回true,所以該過濾器總是生效。
- run:過濾器的具體邏輯。需要注意,這里我們通過ctx.setSendZuulResponse(false)令zuul過濾該請求,不對其進行路由,然后通過ctx.setResponseStatusCode(401)設置了其返回的錯誤碼,當然我們也可以進一步優(yōu)化我們的返回,比如,通過ctx.setResponseBody(body)對返回body內容進行編輯等。
在實現(xiàn)了自定義過濾器之后,還需要實例化該過濾器才能生效,我們只需要在應用主類中增加如下內容:
|
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
? @EnableZuulProxy @SpringCloudApplication public class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(true).run(args); } @Bean public AccessFilter accessFilter() { return new AccessFilter(); } } |
啟動該服務網關后,訪問:
- http://localhost:5555/api-a/add?a=1&b=2:返回401錯誤
- http://localhost:5555/api-a/add?a=1&b=2&accessToken=token:正確路由到server-A,并返回計算內容
對于其他一些過濾類型,這里就不一一展開了,根據之前對filterType生命周期介紹,可以參考下圖去理解,并根據自己的需要在不同的生命周期中去實現(xiàn)不同類型的過濾器。
alt
最后,總結一下為什么服務網關是微服務架構的重要部分,是我們必須要去做的原因:
- 不僅僅實現(xiàn)了路由功能來屏蔽諸多服務細節(jié),更實現(xiàn)了服務級別、均衡負載的路由。
- 實現(xiàn)了接口權限校驗與微服務業(yè)務邏輯的解耦。通過服務網關中的過濾器,在各生命周期中去校驗請求的內容,將原本在對外服務層做的校驗前移,保證了微服務的無狀態(tài)性,同時降低了微服務的測試難度,讓服務本身更集中關注業(yè)務邏輯的處理。
- 實現(xiàn)了斷路器,不會因為具體微服務的故障而導致服務網關的阻塞,依然可以對外服務。
本文完整示例可參考:Chapter9-1-5
【轉載請注明出處】:http://blog.didispace.com/springcloud5/
總結
以上是生活随笔為你收集整理的Spring Cloud构建微服务架构(五)服务网关的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Cloud构建微服务架构(
- 下一篇: Spring Cloud构建微服务架构(