Zuul
代碼地址:https://github.com/jedyang/springCloud
先來看一個(gè)簡單的微服務(wù)架構(gòu)圖。
用戶請求通過負(fù)載均衡nginx,網(wǎng)關(guān)路由zuul,到達(dá)服務(wù)層。服務(wù)統(tǒng)一注冊在注冊中心Eureka,配置項(xiàng)同意在配置中心管理,配置托管在git倉庫。
Zuul的主要功能是路由轉(zhuǎn)發(fā)和過濾。路由轉(zhuǎn)發(fā)可以類比spring的servlet-mapping功能。zuul默認(rèn)已集成ribbon負(fù)載均衡。
新建一個(gè)工程zuul
創(chuàng)建時(shí)勾選web、eureka discover、zuul-
代碼開啟zuul功能
@EnableZuulProxy @EnableEurekaClient @SpringBootApplication public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ZuulApplication.class, args); } }
@EnableZuulProxy注解開啟zuul功能
-
改造之前的服務(wù)
在學(xué)習(xí)ribbon和feign時(shí),我在一個(gè)工程里做了演示。現(xiàn)在想要演示zuul路由分發(fā)功能,最好將之前的工程拆分一下。我這里就不拆了,通過修改端口號提供兩個(gè)服務(wù)出來。一個(gè)service-ribbon在端口8764,另一個(gè)service-feign在8765。
這是之前的配置文件:eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ server: port: 8764 # port: 8765 # 切換端口,啟動不同服務(wù) spring: application: name: service-ribbon # name: service-feign
以此啟動之前的服務(wù)
注冊中心8761。
服務(wù)provider在8762。(只啟動一個(gè)好了)
服務(wù)consumer service-ribbon在8764
服務(wù)consumer service-feign在8765
-
配置下zuul工程
eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ server: port: 8766 spring: application: name: service-zuul zuul: routes: api-a: path: /api-ribbon/** serviceId: service-ribbon api-b: path: /api-feign/** serviceId: service-feign
以/api-ribbon/ 開頭的請求都轉(zhuǎn)發(fā)給service-ribbon服務(wù);以/api-feign/開頭的請求都轉(zhuǎn)發(fā)給service-feign服務(wù);
測試
瀏覽器請求http://localhost:8766/api-feign/feignHi?name=yunsheng
返回hi yunsheng,i am from port:8762
證明zuul將請求轉(zhuǎn)發(fā)給了8765。
改一下請求http://localhost:8766/api-ribbon/feignHi?name=yunsheng
返回錯(cuò)誤。因?yàn)閍pi-ribbon會轉(zhuǎn)發(fā)給8764,沒有對應(yīng)的服務(wù)。
過濾器功能
除了請求路由以外,zuul另一個(gè)重要功能就是filter了。
代碼
@Component
public class MyFilter extends ZuulFilter {
@Override
public String filterType() {
/* 過濾時(shí)機(jī)
pre:路由之前
routing:路由之時(shí)
post: 路由之后
error:發(fā)送錯(cuò)誤調(diào)用
*/
return "pre";
}
/**
* 過濾器的優(yōu)先級
* 數(shù)字越大優(yōu)先級越低
*
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 該過濾器開關(guān)
*
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 過濾器方法
*
* @return
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String username = request.getParameter("name");// 獲取請求的參數(shù)
if (null != username && username.equals("yunsheng")) {// 如果請求的參數(shù)不為空,且值為chhliu時(shí),則通過
ctx.setSendZuulResponse(true);// 對該請求進(jìn)行路由
ctx.setResponseStatusCode(200);
ctx.set("isSuccess", true);// 設(shè)值,讓下一個(gè)Filter看到上一個(gè)Filter的狀態(tài)
return null;
} else {
ctx.setSendZuulResponse(false);// 過濾該請求,不對其進(jìn)行路由
ctx.setResponseStatusCode(401);// 返回錯(cuò)誤碼
ctx.setResponseBody("{\"result\":\"name is not correct!\"}");// 返回錯(cuò)誤內(nèi)容
ctx.set("isSuccess", false);
return null;
}
}
}
測試一下,http://localhost:8766/api-ribbon/ribbonHi?name=yunshen
返回{"result":"name is not correct!"}
配置中心config
spring cloud的配置項(xiàng)管理是以托管在git倉庫中的文件為存儲介質(zhì)。
配置中心分為兩部分:server端負(fù)責(zé)連接git倉庫,得到配置文件。client端負(fù)責(zé)對外提供api,從server端查詢具體配置信息。
server端
新建工程
勾選eureka discovert、config server-
代碼
@SpringBootApplication @EnableConfigServer public class ConfigApplication { public static void main(String[] args) { SpringApplication.run(ConfigApplication.class, args); } }
還是注解開啟對應(yīng)的功能
-
配置
配置application.properties#spring.application.name=config-server server.port=8767 # 配置git倉庫地址 spring.cloud.config.server.git.uri=https://github.com/jedyang/springCloud/ # 倉庫路徑 spring.cloud.config.server.git.searchPaths=configRepo # 倉庫的分支 spring.cloud.config.label=master # 用戶名和密碼,例子的是公開倉庫,不需要 #spring.cloud.config.server.git.username=your username #spring.cloud.config.server.git.password=your password
創(chuàng)建配置文件
這是我創(chuàng)建的配置文件
- 測試
http://localhost:8767/thekey/dev/master
看到響應(yīng):
{"name":"thekey","profiles":["dev"],"label":"master","version":"d6a477aa1da5862e9e42553d644e136efbea9296","state":null,"propertySources":[]}
但是。。其實(shí)這只代表能訪問到倉庫的某個(gè)分之下,并不是真的配置內(nèi)容。
比如訪問http://localhost:8767/a/a/
依然能得到響應(yīng):
{"name":"a","profiles":["a"],"label":null,"version":"d6a477aa1da5862e9e42553d644e136efbea9296","state":null,"propertySources":[]}
從響應(yīng)中我們也可以看出,比如訪問http://localhost:8767/a/b/c時(shí),a代表應(yīng)用名(在client中配置,現(xiàn)在是在server工程,不要急),b是環(huán)境如dev、test,c代表git上的分支,現(xiàn)在只有master(所以要么不寫,寫錯(cuò)會報(bào)錯(cuò))。
還有其他訪問方式:
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties、
client端
新建工程
勾選web、config client-
配置
這里是配置bootstrap.properties。一定要注意啊。坑。spring.application.name=config-client spring.cloud.config.label=master spring.cloud.config.profile=dev spring.cloud.config.uri= http://localhost:8767/ server.port=8768
最終對應(yīng)的文件是spring.application.name-spring.cloud.config.profile.properties
-
代碼
@SpringBootApplication @RestController public class ConfigClientApplication { public static void main(String[] args) { SpringApplication.run(ConfigClientApplication.class, args); } @Value("${thekey}") String theValue; @RequestMapping(value = "/getValue") public String getValue() { return theValue; } }
做一個(gè)rest服務(wù)查詢下配置的值。
原理就是通過@Value注解取值 測試
訪問http://localhost:8768/getValue
得到配置的值
集群化
到現(xiàn)在為止,我們的配置服務(wù)是單點(diǎn)的,在生產(chǎn)環(huán)境要求高可用的情況下,是存在風(fēng)險(xiǎn)的。現(xiàn)在我們將其集群化。
集群化的方法也很簡單,將服務(wù)注冊到eureka,通過eureka服務(wù)調(diào)用。
這里我們還是復(fù)用之前的注冊中心8761。
-
改造server端
在配置文件中加上
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
指定服務(wù)注冊地址啟動類加上
@EnableEurekaClient
注解 -
改造client端
添加依賴<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency>
修改配置
spring.application.name=config-client spring.cloud.config.label=master spring.cloud.config.profile=dev #spring.cloud.config.uri= http://localhost:8767/ server.port=8768 eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/ spring.cloud.config.discovery.enabled=true spring.cloud.config.discovery.serviceId=config-server
可以看到,現(xiàn)在修改為通過serviceId來獲取服務(wù),如果有多個(gè)服務(wù),可以實(shí)現(xiàn)集群化和負(fù)載均衡。
3.測試
tips:client端一定要在server啟動成功后再起,因?yàn)樾枰獜膕erver獲取配置,如果解析不到,會報(bào)錯(cuò)。
服務(wù)鏈路追蹤
在復(fù)雜業(yè)務(wù)系統(tǒng)中,排查問題是一件痛苦的事情,尤其是超時(shí)問題。你必須知道全鏈路的服務(wù)調(diào)用關(guān)系,以及服務(wù)花費(fèi)的時(shí)間。
針對服務(wù)化應(yīng)用全鏈路追蹤的問題,Google發(fā)表了Dapper論文,介紹了他們?nèi)绾芜M(jìn)行服務(wù)追蹤分析。其基本思路是在服務(wù)調(diào)用的請求和響應(yīng)中加入ID,標(biāo)明上下游請求的關(guān)系。利用這些信息,可以可視化地分析服務(wù)調(diào)用鏈路和服務(wù)間的依賴關(guān)系。
Zipkin是對Dapper論文的開源實(shí)現(xiàn),Spring Cloud Sleuth對Zipkin進(jìn)行了封裝,以便加入spring cloud全家桶。
Spring Cloud Sleuth(以下簡稱sleuth)借用了Dapper的術(shù)語。
span。簡單的理解就是一個(gè)最小的服務(wù)單元。例如發(fā)送一個(gè)RPC請求是一個(gè)span,發(fā)送一個(gè)響應(yīng)給RPC請求也是一個(gè)span。每個(gè)span用64bit的唯一id標(biāo)示。span上會包含其他信息,如描述、注解(理解成標(biāo)簽),觸發(fā)這個(gè)span的上一個(gè)span的id,最重要的時(shí)間信息。
trace。就是有一個(gè)個(gè)span組成的調(diào)用鏈路。
-
annotion。我們javaer習(xí)慣稱為注解,但是這里理解成標(biāo)簽比較合適。常用的核心標(biāo)簽:
- cs,client sent
- sr,server received
- ss,server sent
- cr,client received
可以理解,sr-cs=網(wǎng)絡(luò)傳輸時(shí)間。ss-sr=服務(wù)處理時(shí)間。cr-cs得到整個(gè)服務(wù)需要的時(shí)間。
開始代碼
zipkin server
-
新建一個(gè)工程zipkin-server
我的依賴如下:<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-server</artifactId> </dependency> <dependency> <groupId>io.zipkin.java</groupId> <artifactId>zipkin-autoconfigure-ui</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
里面的zipkin-server這個(gè)依賴,在idea創(chuàng)建的時(shí)候選不到,但是又是必須的。有知道的麻煩告訴我一下。
代碼
啟動器加注解@EnableZipkinServer
配置
加一下端口server.port=9411
最好使用這個(gè)端口,應(yīng)該是有依賴關(guān)系。我換成其他的會報(bào)錯(cuò)。還不清楚具體怎么依賴的。
創(chuàng)建相互調(diào)用的服務(wù)
我創(chuàng)建了兩個(gè)工程。app1和app2。
-
依賴
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
代碼
App1:
@SpringBootApplication @RestController public class App1Application { public static void main(String[] args) { SpringApplication.run(App1Application.class, args); } @Autowired private RestTemplate restTemplate; @Bean public RestTemplate getRestTemplate() { return new RestTemplate(); } @RequestMapping("/hi1") public String callHi1() { return restTemplate.getForObject("http://localhost:9002/hi2", String.class); } @RequestMapping("/hi3") public String callHi3() { return "i'm hi 33333"; } }
App2:
@SpringBootApplication @RestController public class ZipkinApp2Application { public static void main(String[] args) { SpringApplication.run(ZipkinApp2Application.class, args); } @Autowired private RestTemplate restTemplate; @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } @RequestMapping("/hi2") public String callHi2(){ return restTemplate.getForObject("http://localhost:9001/hi3", String.class); } }
-
配置
APP1:
server.port=9001 spring.zipkin.base-url=http://localhost:9411 spring.application.name=service-app1
APP2:
server.port=9002 spring.zipkin.base-url=http://localhost:9411 spring.application.name=service-app2
就是hi1-->hi2-->hi3
依次啟動服務(wù)。
查看http://localhost:9411/zipkin/
看一條鏈路
可以看到時(shí)間和標(biāo)簽