爲什麼要使用微服務
分佈式架構中,垂直應用太多,將核心業務抽取出來,作爲獨立的服務,但缺點是應用之間的調用關係錯綜複雜,太難維護.
SpringCloud提供了Eureka,它是一個註冊中心.進行服務的治理,實現服務的自動註冊和發現.每個服務各司其職,對外暴露一個rest風格的接口供別的服務調用,互相不關心內部技術實現,調用就能使用,相互獨立,互不干擾.
團隊獨立:每個服務都是一個獨立的開發團隊,人數不能過多。
技術獨立:因爲是面向服務,提供Rest接口,使用什麼技術沒有別人干涉
前後端分離:採用前後端分離開發,提供統一Rest接口,後端不用再爲PC、移動段開發不同接口
數據庫分離:每個服務都使用自己的數據源
部署獨立,服務間雖然有調用,但要做到服務重啓不影響其它服務。有利於持續集成和持續交付。每個服務都是獨立的組件,可複用,可替換,降低耦合,易維護
服務之間的調用方式
- RPC:Remote Produce Call遠程過程調用,類似的還有RMI。自定義數據格式,基於原生TCP通信,速度快,效率高。早期的webservice,現在熱門的dubbo,都是RPC的典型代表
- Http:http其實是一種網絡傳輸協議,基於TCP,規定了數據傳輸的格式。現在客戶端瀏覽器與服務端通信基本都是採用Http協議,也可以用來進行遠程服務調用。缺點是消息封裝臃腫,優勢是對服務的提供和調用方沒有任何技術限定,自由靈活,更符合微服務理念。現在熱門的Rest風格,就可以通過http協議來實現。
Spring的RestTemplate
Spring提供了一個RestTemplate模板工具類,對基於Http的客戶端進行了封裝,並且實現了對象與json的序列化和反序列化,非常方便。RestTemplate並沒有限定Http的客戶端類型,而是進行了抽象,目前常用的3種都有支持:
- HttpClient
- OkHttp
- JDK原生的URLConnection(默認的)
SpringCloud是Spring旗下的項目之一,官網地址:http://projects.spring.io/spring-cloud/
Spring最擅長的就是集成,把世界上最好的框架拿過來,集成到自己的項目中。
SpringCloud也是一樣,它將現在非常流行的一些技術整合到一起,實現了諸如:配置管理,服務發現,智能路由,負載均衡,熔斷器,控制總線,集羣狀態等等功能。其主要涉及的組件包括:
Netflix:
- Eureka:註冊中心
- Zuul:服務網關
- Ribbon:負載均衡
- Feign:服務調用
- Hystix:熔斷器
Eureka
微服務模擬:
服務提供者:提供對數據的訪問,並向外暴露rest風格的查詢接口.
服務調用者:利用httpClient,通過http協議遠程訪問服務提供者,這裏用的是spring提供的restTemplate.
這就是簡單的微服務模型,只有兩個微服務.一個是調用者,一個是提供者.但也是相互的.其中也暴露出了問題:
1.對於調用者來說,不知道service的狀態,是否down機都不知道,還傻逼一樣的去調用.並且訪問的地址也是提供者固定提供的,一旦提供者更改了地址,又忘記告訴了調用者,調用者就404了.歇逼了.
2.對於提供者來說,服務只有一臺,不具有高可用性,這一臺要是宕機了,那不就涼涼了.就算實現了高可用性.調用者還要自己實現負載均衡,不然很多用戶同時訪問同一臺機器,另一臺機器還不是沒用.壓力要平攤.
這個時候,救世主eureka來了. spring的eureka提供了註冊中心服務,就像一直跑黑車的司機突然發現了滴滴一樣.自己可以合法的註冊在滴滴上了,想打車的也能打到車了,司機也不用再蹲點守着人了.eureka的作用就像滴滴一樣.管理了服務提供者的信息,通過心跳,續約機制來進行服務監控.
Eureka:就是服務註冊中心(可以是一個集羣),對外暴露自己的地址
提供者:啓動後向Eureka註冊自己信息(地址,提供什麼服務)
消費者:向Eureka訂閱服務,Eureka會將對應服務的所有提供者地址列表發送給消費者,並且定期更新
心跳(續約):提供者定期通過http方式向Eureka刷新自己的狀態
Eureka的簡單使用
1.導入eureka的啓動器.
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
2.提供eureka的springboot的入口啓動程序
@SpringBootApplication
@EnableEurekaServer //聲明這個應用是一個Eureka應用
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class,args);
}
}
3.配置eureka服務的配置文件,用的是spring的yml文件來實現配置
server:
port: 10086
spring:
application:
name: eureka-server # 應用名稱,會在Eureka中作爲服務的id標識(serviceId)
eureka:
client:
service-url: # EurekaServer的地址,現在是自己的地址,如果是集羣,需要寫其它Server的地址。
defaultZone: http://127.0.0.1:10086/eureka
# register-with-eureka: false # 不註冊自己
# fetch
服務提供方的配置:
1.提供eureka客戶端的maven依賴.
<!-- Eureka客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.在服務方的程序入口處添加客戶端發現的註解
@SpringBootApplication
@MapperScan("cn.doppelganger.user.mapper")
@EnableDiscoveryClient //開啓Eureka客戶端發現功能
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class,args);
}
}
3.服務提供方的配置
server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/database_springboot
username: root
password: root
application:
name: user-service
mybatis:
type-aliases-package: cn.doppelganger.user.pojo
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
cn.doppelganger: debug
eureka:
client:
service-url:
#實現高可用的eureka集羣.同時向兩個eureka註冊服務
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
instance:
ip-address: 127.0.0.1
prefer-ip-address: true #改變註冊的地址,而不是主機名
服務調用方配置:
在springboot程序入口類上添加@EnableDiscoveryClient註解,或者@SpringCloudApplication註解
@SpringCloudApplication
@EnableFeignClients
public class ConsumerApplication {
@Bean//添加到spring容器中,在controller中調用.
@LoadBalanced//ribbon負載均衡註解
public RestTemplate restTemplate() {//spring封裝的http工具,用來進行http的遠程訪問服務
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
server:
port: 8080
spring:
application:
name: consumer
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
instance:
ip-address: 127.0.0.1
prefer-ip-address: true
調用方的controller,不再是寫死的url.二十通過eureka來獲取實例,而動態的獲取服務商的服務.
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/{id}")
public User queryById(@PathVariable("id")Long id){
// 根據服務id(spring.application.name),獲取服務實例列表
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
ServiceInstance instance = instances.get(0);
String url = String.format("http://%s:%s/user/%s",instance.getHost(),instance.getPort(),id);
User user = restTemplate.getForObject(url, User.class);
return user;
eureka的高可用
通過eureka集羣來實現高可用,在一臺服務宕機之後,另外的幾臺可以正常運行.這和分佈式架構的高可用性是一個概念.我的一個eureka的端口10086,另一個則是10087.兩臺eureka也相互註冊了,達到互相監控的目的.同時在服務商和消費者註冊的時候,同時也在兩臺服務器上同時進行註冊
- 爲什麼服務方和消費方都是引的是客戶端的包呢.因爲相對於eureka來說,他們都是客戶機,註冊在eureka上.現在只有兩個服務,服務方就是服務方,但以後服務多了,這裏的消費方也可能成爲別人的服務方.
Ribbon負載均衡
上述的案例只是在服務商的單點模式下進行的,而實際情況中,服務商可能是個服務集羣,此時獲取的服務列表就會有多個,那麼消費方應該訪問哪一個呢.這個就有springCloud中的Ribbon來幫我們實現負載均衡,默認的負載均衡方式是輪詢,當然也可以改,ribbon內置了多種負載均衡的方式.像還有隨機等.具體怎麼實現呢,其實很簡單.
現在我們啓動了兩個服務分別是8081,8082.
因爲負載均衡是對消費者調用哪一個服務器而言的.所以只要在消費者的程序入口加上一個註解即可以實現負載均衡.
@Bean
@LoadBalanced //ribbon實現負載均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
而且,獲取服務商的地址也不需要再去自己拼接了,ribbon底層會根據名字去查找相應的sever名字,來識別服務的ip.
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
String url = "http://user-service/user/" + id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
此時再去通過消費者去調用服務商的查詢接口.一次會訪問到8081,一次會訪問到8082.這就實現了輪詢的負載均衡.是不是很簡單.
Hystrix
微服務中,服務間調用關係錯綜複雜,一個請求,可能需要調用多個微服務接口才能實現,會形成非常複雜的調用,如果此時,某個服務出現異常.例如其中有一個用戶查詢的微服務發生異常,請求阻塞,用戶得不到響應,則這個線程一直不會釋放,於是越來越多的請求進來,越來越多請求阻塞.這就形成了雪崩效應,也可以說蝴蝶效應.一個小的微服務異常,最終導致所有的服務全部不能運行,直到資源耗盡系統崩潰.
Hystrix解決這個問題的手段是服務降級,主要包括,線程隔離和服務熔斷.
Hystrix爲每個依賴服務調用分配一個小的線程池,如果線程池已滿調用將被立即拒絕,默認不採用排隊.加速失敗判定時間。
用戶的請求將不再直接訪問服務,而是通過線程池中的空閒線程來訪問服務,如果線程池已滿,或者請求超時,則會進行降級處理.用戶的請求故障時,不會被阻塞,更不會無休止的等待或者看到系統崩潰,至少可以看到一個執行結果(例如返回友好的提示信息) 。
服務降級雖然會導致請求失敗,但是不會導致阻塞,而且最多會影響這個依賴服務對應的線程池中的資源,對其它服務沒有響應。
服務降級:優先保證核心服務,而非核心服務不可用或弱可用。
觸發Hystix服務降級的情況:
- 線程池已滿
- 請求超時
1.還是引入依賴,因爲我的項目是基於springBoot和springCloud的,引入hystrix的啓動器即可.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2.加入註解
@SpringCloudApplication
@EnableFeignClients
@EnableCircuitBreaker
@SpringCloudApplication //用這個註解去代替上面三個註解.
public class ConsumerApplication {
@Bean//添加到spring容器中,在controller中調用.
@LoadBalanced//ribbon負載均衡註解
public RestTemplate restTemplate() {//spring封裝的http工具,用來進行http的遠程訪問服務
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
3.編寫降級邏輯,當目標服務的調用出現故障,我們希望快速失敗,給用戶一個友好提示。因此需要提前編寫好失敗時的降級處理邏輯,要使用HystixCommond來完成,這個註解是加在方法上的,其中有個fallbackMethod屬性來指定降級後調用的方法.注意這裏有個坑,降級方法的邏輯必須和正常的方法邏輯一致,也就是說參數和返回值都要一致.原來查詢的返回值是User,現在改成了String,因爲RestController都會將他轉化爲json對象.所以這裏改成String也無可厚非.
@GetMapping("{id}")
@HystrixCommand(fallbackMethod = "queryByIdFallBack")
public String queryById(@PathVariable("id") Long id){
String url = "http://user-service/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallBack(Long id){
log.error("查詢用戶信息失敗,id:{}", id);
return "對不起,網絡太擁擠了!";
}
但問題是,正常的controller裏面不可能只有一個訪問的接口,所以可以把降級的邏輯聲明在類上.
@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallBack")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
@HystrixCommand
public String queryById(@PathVariable("id") Long id){
...
}
public String defaultFallBack(){
return "默認提示:對不起,網絡太擁擠了!";
}
}
通過DefaultProperties來解決降級方法的問題.
當服務器宕機的時候,或出現異常訪問,訪問超時的時候,就會執行這個方法,而不會一直卡死在這裏無線等待,導致阻塞越來越多.
服務熔斷
上面所做的只是線程隔離,和服務的降級.分配單獨的線程池給每個服務,如果出錯也只是影響分配的線程池理,線程池佔滿的情況下,會執行上述的降級操作,而不會影響到別的微服務.
熔斷器,又叫斷路器,circuit breaker
Hystix的熔斷狀態機模型:
- Closed:關閉狀態(斷路器關閉),所有請求都正常訪問。
- Open:打開狀態(斷路器打開),所有請求都會被降級。Hystix會對請求情況計數,當一定時間內失敗請求百分比達到閾值,則觸發熔斷,斷路器會完全關閉。默認失敗比例的閾值是50%,請求次數最少不低於20次。
- Half Open:半開狀態,Closed狀態不是永久的,關閉後會進入休眠時間(默認是5S)。隨後斷路器會自動進入半開狀態。此時會釋放部分請求通過,若這些請求都是健康的,則會完全打開斷路器,否則繼續保持關閉,再次進行休眠計時
簡單來說,就像家裏的保險絲一樣,默認狀態下是連着的,熔斷器默認close的狀態就是連着的,所有訪問正常,但當異常出現的時候狀態變爲open,期間,斷路器會去重試異常請求,進入半開狀態,如果成功響應了就返回success,變爲正常狀態,如果還是線程瘋狂阻塞就繼續保持open的狀態.此時不僅異常線程無法訪問,所有的服務都無法訪問,這就是熔斷,一種保護機制.防止整個系統被拖垮掉.