SpringCloud微服務的架構和簡單入門

 


爲什麼要使用微服務

分佈式架構中,垂直應用太多,將核心業務抽取出來,作爲獨立的服務,但缺點是應用之間的調用關係錯綜複雜,太難維護.

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的狀態.此時不僅異常線程無法訪問,所有的服務都無法訪問,這就是熔斷,一種保護機制.防止整個系統被拖垮掉.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章