微服務基礎

ML李嘉圖發表於2022-04-18

image-20220317173734208

微服務基礎

前面我們講解了SpringBoot框架,通過使用SpringBoot框架,我們的專案開發速度可以說是得到了質的提升。同時,我們對於專案的維護和理解,也會更加的輕鬆。

可見,SpringBoot為我們的開發帶來了巨大便捷。而這一部分,我們將基於SpringBoot,繼續深入到企業實際場景,探討微服務架構下的SpringCloud。

這個部分我們會更加註重於架構設計上的講解,弱化實現原理方面的研究。

傳統專案轉型

要說近幾年最火熱的話題,那還得是微服務,那麼什麼是微服務呢?

我們可以先從技術的演變開始看起,在我們學習JavaWeb之後,一般的網站開發模式為Servlet+JSP,

但是實際上我們在學習了SSM之後,會發現這種模式已經遠遠落後了。

第一,一個公司不可能去招那麼多同時會前端+後端的開發人員,就算招到,也並不一定能保證兩個方面都比較擅長,相比前後端分開學習的開發人員,顯然後者的學習成本更低,專注度更高。

因此前後端分離成為了一種新的趨勢。通過使用SpringBoot,我們幾乎可以很快速地開發一個高效能的單體應用,只需要啟動一個服務端,我們整個專案就開始執行了,各項功能融於一體,開發起來也更加輕鬆。

但是隨著我們專案的不斷擴大,單體應用似乎顯得有點乏力了。

隨著越來越多的功能不斷地加入到一個SpringBoot專案中,隨著介面不斷增加,整個系統就要在同一時間內響應更多型別的請求。

顯然,這種擴充套件方式是不可能無限使用下去的,總有一天,這個SpringBoot專案會龐大到執行緩慢。並且所有的功能如果都整合在單端上,那麼所有的請求都會全部彙集到一臺伺服器上,對此伺服器造成巨大壓力。

image-20220320174622739

傳統單體架構應用隨著專案規模的擴大,實際上會暴露越來越多的問題,尤其是一臺伺服器無法承受龐大的單體應用部署,並且單體應用的維護也會越來越困難,我們得尋找一種新的開發架構來解決這些問題了。

In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.

Martin Fowler在2014年提出了“微服務”架構,它是一種全新的架構風格。

  • 微服務把一個龐大的單體應用拆分為一個個的小型服務,比如我們原來的圖書管理專案中,有登入、註冊、新增、刪除、搜尋等功能,那麼我們可以將這些功能單獨做成一個個小型的SpringBoot專案,獨立執行。
  • 每個小型的微服務,都可以獨立部署和升級,這樣,就算整個系統崩潰,那麼也只會影響一個服務的執行。
  • 微服務之間使用HTTP進行資料互動,不再是單體應用內部互動了,雖然這樣會顯得更麻煩,但是帶來的好處也是很直接的,甚至能突破語言限制,使用不同的程式語言進行微服務開發,只需要使用HTTP進行資料互動即可。
  • 我們可以同時購買多臺主機來分別部署這些微服務,這樣,單機的壓力就被分散到多臺機器,並且每臺機器的配置不一定需要太高,這樣就能節省大量的成本,同時安全性也得到很大的保證。
  • 甚至同一個微服務可以同時存在多個,這樣當其中一個伺服器出現問題時,其他伺服器也在執行同樣的微服務,這樣就可以保證一個微服務的高可用。

image-20220322090754438

當然,這裡只是簡單的演示一下微服務架構,實際開發中肯定是比這個複雜得多的。

可見,採用微服務架構,更加能夠應對當今時代下的種種考驗,傳統專案的開發模式,需要進行架構上的升級。

走進SpringCloud

前面我們介紹了微服務架構的優點,那麼同樣的,這些優點的背後也存在著諸多的問題:

  • 要實現微服務並不是說只需要簡單地將專案進行拆分,我們還需要考慮對各個微服務進行管理、監控等,這樣我們才能夠及時地尋找和排查問題。
  • 因此微服務往往需要的是一整套解決方案,包括服務註冊和發現、容災處理、負載均衡、配置管理等。
  • 它不像單體架構那種方便維護,由於部署在多個伺服器,我們不得不去保證各個微服務能夠穩定執行,在管理難度上肯定是高於傳統單體應用的。
  • 在分散式的環境下,單體應用的某些功能可能會變得比較麻煩,比如分散式事務。

所以,為了更好地解決這些問題,SpringCloud正式登場。

SpringCloud是Spring提供的一套分散式解決方案,集合了一些大型網際網路公司的開源產品,包括諸多元件,共同組成SpringCloud框架。

並且,它利用Spring Boot的開發便利性巧妙地簡化了分散式系統基礎設施的開發,如服務發現註冊、配置中心、訊息匯流排、負載均衡、熔斷機制、資料監控等,都可以用Spring Boot的開發風格做到一鍵啟動和部署。

由於中小型公司沒有獨立開發自己的分散式基礎設施的能力,使用SpringCloud解決方案能夠以最低的成本應對當前時代的業務發展。

image-20220322102706256

可以看到,SpringCloud整體架構的亮點是非常明顯的,分散式架構下的各個場景,都有對應的元件來處理,比如基於Netflix(奈飛)的開源分散式解決方案提供的元件:

  • Eureka - 實現服務治理(服務註冊與發現),我們可以對所有的微服務進行集中管理,包括他們的執行狀態、資訊等。
  • Ribbon - 為服務之間相互呼叫提供負載均衡演算法(現在被SpringCloudLoadBalancer取代)
  • Hystrix - 斷路器,保護系統,控制故障範圍。暫時可以跟家裡電閘的保險絲類比,當觸電危險發生時能夠防止進一步的發展。
  • Zuul - api閘道器,路由,負載均衡等多種作用,就像我們的路由器,可能有很多個裝置都連線了路由器,但是資料包要轉發給誰則是由路由器在進行(已經被SpringCloudGateway取代)
  • Config - 配置管理,可以實現配置檔案集中管理

Eureka 註冊中心

官方文件:https://docs.spring.io/spring-cloud-netflix/docs/current/reference/html/

小貼士:各位小夥伴在學習的過程中覺得有什麼疑惑的可以直接查閱官方文件,我們會在每一個技術開始之前貼上官方文件的地址,方便各位進行查閱,同時在我們的課程中並不一定會完完整整地講完整個框架的內容,有關詳細的功能和使用方法文件中也是寫的非常清楚的,感興趣的可以深入學習。


微服務專案結構

既然要將單體應用拆分為多個小型服務,我們就需要重新設計一下整個專案目錄結構,這裡我們就建立多個子專案,每一個子專案都是一個服務,這樣由父專案統一管理依賴,就無需每個子專案都去單獨管理依賴了,也更方便一點。

我們首先建立一個普通的SpringBoot專案:

image-20220323105531867

然後不需要勾選任何依賴,直接建立即可,專案建立完成並初始化後,我們刪除父工程的無用檔案,只保留必要檔案,像下面這樣:

image-20220323105859454

接著我們就可以按照我們劃分的服務,進行子工程建立了,建立一個新的Maven專案,注意父專案要指定為我們一開始建立的的專案,子專案命名隨意:

image-20220323110133466

子專案建立好之後,接著我們在子專案中建立SpringBoot的啟動主類:

image-20220323110756722

接著我們點選執行,即可啟動子專案了,實際上這個子專案就一個最簡單的SpringBoot web專案,注意啟動之後最下方有彈窗,我們點選"使用 服務",這樣我們就可以實時檢視當前整個大專案中有哪些微服務了:

image-20220323110917997

image-20220323111056940

接著我們以同樣的方法,建立其他的子專案,注意我們最好將其他子專案的埠設定得不一樣,不然會導致埠占用,我們分別為它們建立application.yml檔案:

image-20220323111733605

接著我們來嘗試啟動一下這三個服務,正常情況下都是可以直接啟動的:

image-20220323111849846

可以看到它們分別執行在不同的埠上,這樣,就方便不同的程式設計師編寫不同的服務了,提交當前專案程式碼時的衝突率也會降低。

接著我們來建立一下資料庫,這裡還是老樣子,建立三個表即可,當然實際上每個微服務單獨使用一個資料庫伺服器也是可以的,因為按照單一職責服務只會操作自己對應的表,這裡UP主比較窮,就只用一個資料庫演示了:

image-20220323112340995

image-20220323112616538

image-20220323112842758

image-20220323112750936

image-20220323112825430

建立好之後,結果如下,一共三張表,各位可以自行新增一些資料到裡面,這就不貼出來了:

image-20220323112922396

如果各位嫌麻煩的話可以下載.sql檔案自行匯入。

接著我們來稍微寫一點業務,比如使用者資訊查詢業務,我們先把資料庫相關的依賴進行匯入,這裡依然使用Mybatis框架,首先在父專案中新增MySQL驅動和Lombok依賴:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
</dependency>

由於不是所有的子專案都需要用到Mybatis,我們在父專案中只進行版本管理即可:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

接著我們就可以在使用者服務子專案中新增此依賴了:

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
</dependencies>

接著新增資料來源資訊(UP用到是阿里雲的MySQL雲資料庫,各位注意修改一下資料庫地址):

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://cloudstudy.mysql.cn-chengdu.rds.aliyuncs.com:3306/cloudstudy
    username: test
    password: 123456

接著我們來寫使用者查詢相關的業務:

@Data
public class User {
    int uid;
    String name;
    String sex;
}
@Mapper
public interface UserMapper {
    @Select("select * from DB_USER where uid = #{uid}")
    User getUserById(int uid);
}
public interface UserService {
    User getUserById(int uid);
}
@Service
public class UserServiceImpl implements UserService {

    @Resource
    UserMapper mapper;

    @Override
    public User getUserById(int uid) {
        return mapper.getUserById(uid);
    }
}
@RestController
public class UserController {

    @Resource
    UserService service;

    //這裡以RESTFul風格為例
    @RequestMapping("/user/{uid}")
    public User findUserById(@PathVariable("uid") int uid){
        return service.getUserById(uid);
    }
}

現在我們訪問即可拿到資料:

image-20220323133820304

同樣的方式,我們完成一下圖書查詢業務,注意現在是在圖書管理微服務中編寫(別忘了匯入Mybatis依賴以及配置資料來源):

@Data
public class Book {
    int bid;
    String title;
    String desc;
}
@Mapper
public interface BookMapper {

    @Select("select * from DB_BOOK where bid = #{bid}")
    Book getBookById(int bid);
}
public interface BookService {
    Book getBookById(int bid);
}
@Service
public class BookServiceImpl implements BookService {

    @Resource
    BookMapper mapper;

    @Override
    public Book getBookById(int bid) {
        return mapper.getBookById(bid);
    }
}
@RestController
public class BookController {

    @Resource
    BookService service;

    @RequestMapping("/book/{bid}")
    Book findBookById(@PathVariable("bid") int bid){
        return service.getBookById(bid);
    }
}

同樣進行一下測試:

image-20220323134742618

這樣,我們一個完整專案的就拆分成了多個微服務,不同微服務之間是獨立進行開發和部署的。

服務間呼叫

前面我們完成了使用者資訊查詢和圖書資訊查詢,現在我們來接著完成借閱服務。

借閱服務是一個關聯性比較強的服務,它不僅僅需要查詢借閱資訊,同時可能還需要獲取借閱資訊下的詳細資訊,比如具體那個使用者借閱了哪本書,並且使用者和書籍的詳情也需要同時出現,那麼這種情況下,我們就需要去訪問除了借閱表以外的使用者表和圖書表。

image-20220323140053749

但是這顯然是違反我們之前所說的單一職責的,相同的業務功能不應該重複出現,但是現在由需要在此服務中查詢使用者的資訊和圖書資訊,那怎麼辦呢?我們可以讓一個服務去呼叫另一個服務來獲取資訊。

image-20220323140322502

這樣,圖書管理微服務和使用者管理微服務相對於借閱記錄,就形成了一個生產者和消費者的關係,前者是生產者,後者便是消費者。

現在我們先將借閱關聯資訊查詢完善了:

@Data
public class Borrow {
    int id;
    int uid;
    int bid;
}
@Mapper
public interface BorrowMapper {
    @Select("select * from DB_BORROW where uid = #{uid}")
    List<Borrow> getBorrowsByUid(int uid);

    @Select("select * from DB_BORROW where bid = #{bid}")
    List<Borrow> getBorrowsByBid(int bid);

    @Select("select * from DB_BORROW where bid = #{bid} and uid = #{uid}")
    Borrow getBorrow(int uid, int bid);
}

現在有一個需求,需要查詢使用者的借閱詳細資訊,也就是說需要查詢某個使用者具體借了那些書,並且需要此使用者的資訊和所有已借閱的書籍資訊一起返回,那麼我們先來設計一下返回實體:

@Data
@AllArgsConstructor
public class UserBorrowDetail {
    User user;
    List<Book> bookList;
}

但是有一個問題,我們發現User和Book實體實際上是在另外兩個微服務中定義的,相當於當前專案並沒有定義這些實體類,那麼怎麼解決呢?

因此,我們可以將所有服務需要用到的實體類單獨放入另一個一個專案中,然後讓這些專案引用集中存放實體類的那個專案,這樣就可以保證每個微服務的實體類資訊都可以共用了:

image-20220323141919836

然後只需要在對應的類中引用此專案作為依賴即可:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>commons</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

之後新的公共實體類都可以在commons專案中進行定義了,現在我們接著來完成剛剛的需求,先定義介面:

public interface BorrowService {

    UserBorrowDetail getUserBorrowDetailByUid(int uid);
}
@Service
public class BorrowServiceImpl implements BorrowService{

    @Resource
    BorrowMapper mapper;

    @Override
    public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
        List<Borrow> borrow = mapper.getBorrowsByUid(uid);
        //那麼問題來了,現在拿到借閱關聯資訊了,怎麼呼叫其他服務獲取資訊呢?
    }
}

需要進行服務遠端呼叫我們需要用到RestTemplate來進行:

@Service
public class BorrowServiceImpl implements BorrowService{

    @Resource
    BorrowMapper mapper;

    @Override
    public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
        List<Borrow> borrow = mapper.getBorrowsByUid(uid);
        //RestTemplate支援多種方式的遠端呼叫
        RestTemplate template = new RestTemplate();
        //這裡通過呼叫getForObject來請求其他服務,並將結果自動進行封裝
        //獲取User資訊
        User user = template.getForObject("http://localhost:8082/user/"+uid, User.class);
        //獲取每一本書的詳細資訊
        List<Book> bookList = borrow
                .stream()
                .map(b -> template.getForObject("http://localhost:8080/book/"+b.getBid(), Book.class))
                .collect(Collectors.toList());
        return new UserBorrowDetail(user, bookList);
    }
}

現在我們再最後完善一下Controller:

@RestController
public class BorrowController {

    @Resource
    BorrowService service;

    @RequestMapping("/borrow/{uid}")
    UserBorrowDetail findUserBorrows(@PathVariable("uid") int uid){
        return service.getUserBorrowDetailByUid(uid);
    }
}

在資料庫中新增一點借閱資訊,測試看看能不能正常獲取(注意一定要保證三個服務都處於開啟狀態,否則遠端呼叫會失敗):

image-20220323143753567

可以看到,結果正常,沒有問題,遠端呼叫成功。

這樣,一個簡易的圖書管理系統的分散式專案就搭建完成了。

服務註冊與發現

前面我們瞭解瞭如何對單體應用進行拆分,並且也學習瞭如何進行服務之間的相互呼叫。

但是存在一個問題,就是雖然服務拆分完成,但是沒有一個比較合理的管理機制,如果單純只是這樣編寫,在部署和維護起來,肯定是很麻煩的。

可以想象一下,如果某一天這些微服務的埠或是地址大規模地發生改變,我們就不得不將服務之間的呼叫路徑大規模的同步進行修改,這是多麼可怕的事情。我們需要削弱這種服務之間的強關聯性,因此我們需要一個集中管理微服務的平臺,這時就要藉助我們這一部分的主角了。

Eureka能夠自動註冊並發現微服務,然後對服務的狀態、資訊進行集中管理,這樣當我們需要獲取其他服務的資訊時,我們只需要向Eureka進行查詢就可以了。

image-20220323145051821

像這樣的話,服務之間的強關聯性就會被進一步削弱。

那麼現在我們就來搭建一個Eureka伺服器,只需要建立一個新的Maven專案即可,然後我們需要在父工程中新增一下SpringCloud的依賴,這裡選用2021.0.1版本(Spring Cloud 最新的版本命名方式變更了,現在是 YEAR.x 這種命名方式,具體可以在官網檢視:https://spring.io/projects/spring-cloud#learn):

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>2021.0.1</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

接著我們為新建立的專案新增依賴:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>

下載內容有點多,首次匯入請耐心等待一下。

接著我們來建立主類,還是一樣的操作:

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

彆著急啟動!!!接著我們需要修改一下配置檔案:

server:
  port: 8888
eureka:
	# 開啟之前需要修改一下客戶端設定(雖然是服務端
  client:
  	# 由於我們是作為服務端角色,所以不需要獲取服務端,改為false,預設為true
		fetch-registry: false
		# 暫時不需要將自己也註冊到Eureka
    register-with-eureka: false
    # 將eureka服務端指向自己
    service-url:
      defaultZone: http://localhost:8888/eureka

好了,現在差不多可以啟動了,啟動完成後,直接輸入地址+埠即可訪問Eureka的管理後臺:

image-20220323152537322

可以看到目前還沒有任何的服務註冊到Eureka,我們接著來配置一下我們的三個微服務,首先還是需要匯入Eureka依賴(注意別導錯了,名稱裡面有個starter的才是):

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

然後修改配置檔案:

eureka:
  client:
  	# 跟上面一樣,需要指向Eureka服務端地址,這樣才能進行註冊
    service-url:
      defaultZone: http://localhost:8888/eureka

OK,無需在啟動類新增註解,直接啟動就可以了,然後開啟Eureka的服務管理頁面,可以看到我們剛剛開啟的服務:

image-20220323154722373

可以看到8082埠上的伺服器,已經成功註冊到Eureka了,但是這個服務名稱怎麼會顯示為UNKNOWN,我們需要修改一下:

spring:
  application:
    name: userservice

image-20220323155305545

當我們的服務啟動之後,會每隔一段時間跟Eureka傳送一次心跳包,這樣Eureka就能夠感知到我們的服務是否處於正常執行狀態。

現在我們用同樣的方法,將另外兩個微服務也註冊進來:

image-20220323155948425

那麼,現在我們怎麼實現服務發現呢?

也就是說,我們之前如果需要對其他微服務進行遠端呼叫,那麼就必須要知道其他服務的地址:

User user = template.getForObject("http://localhost:8082/user/"+uid, User.class);

而現在有了Eureka之後,我們可以直接向其進行查詢,得到對應的微服務地址,這裡直接將服務名稱替換即可:

@Service
public class BorrowServiceImpl implements BorrowService {

    @Resource
    BorrowMapper mapper;

    @Resource
    RestTemplate template;

    @Override
    public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
        List<Borrow> borrow = mapper.getBorrowsByUid(uid);

        //這裡不用再寫IP,直接寫服務名稱userservice
        User user = template.getForObject("http://userservice/user/"+uid, User.class);
        //這裡不用再寫IP,直接寫服務名稱bookservice
        List<Book> bookList = borrow
                .stream()
                .map(b -> template.getForObject("http://bookservice/book/"+b.getBid(), Book.class))
                .collect(Collectors.toList());
        return new UserBorrowDetail(user, bookList);
    }
}

接著我們手動將RestTemplate宣告為一個Bean,然後新增@LoadBalanced註解,這樣Eureka就會對服務的呼叫進行自動發現,並提供負載均衡:

@Configuration
public class BeanConfig {
    @Bean
    @LoadBalanced
    RestTemplate template(){
        return new RestTemplate();
    }
}

現在我們就可以正常呼叫了:

image-20220323161809122

不對啊,不是說有負載均衡的能力嗎,怎麼個負載均衡呢?

我們先來看看,同一個伺服器實際上是可以註冊很多個的,但是它們的埠不同,比如我們這裡建立多個使用者查詢服務,我們現在將原有的埠配置修改一下,由IDEA中設定啟動引數來決定,這樣就可以多建立幾個不同埠的啟動項了:

image-20220323162858616

image-20220323162926482

可以看到,在Eureka中,同一個服務出現了兩個例項:

image-20220323163010052

現在我們稍微修改一下使用者查詢,然後進行遠端呼叫,看看請求是不是均勻地分配到這兩個服務端:

@RestController
public class UserController {

    @Resource
    UserService service;
    
    @RequestMapping("/user/{uid}")
    public User findUserById(@PathVariable("uid") int uid){
        System.out.println("我被呼叫拉!");
        return service.getUserById(uid);
    }
}

image-20220323163335257

可以看到,兩個例項都能夠均勻地被分配請求:

image-20220323163448765

image-20220323163457877

這樣,服務自動發現以及簡單的負載均衡就實現完成了,並且,如果某個微服務掛掉了,只要存在其他同樣的微服務例項在執行,那麼就不會導致整個微服務不可用,極大地保證了安全性。

註冊中心高可用

雖然Eureka能夠實現服務註冊和發現,但是如果Eureka伺服器崩潰了,豈不是所有需要用到服務發現的微服務就GG了?

為了避免,這種問題,我們也可以像上面那樣,搭建Eureka叢集,存在多個Eureka伺服器,這樣就算掛掉其中一個,其他的也還在正常執行,就不會使得服務註冊與發現不可用。當然,要是物理黑客直接炸了整個機房,那還是算了吧。

image-20220323205531185

我們來看看如何搭建Eureka叢集,這裡由於機器配置不高,就搭建兩個Eureka伺服器組成叢集。

首先我們需要修改一下Eureka服務端的配置檔案,這裡我們建立兩個配置檔案,:

server:
  port: 8801
spring:
  application:
    name: eurekaserver
eureka:
  instance:
  	# 由於不支援多個localhost的Eureka伺服器,但是又只有本地測試環境,所以就只能自定義主機名稱了
  	# 主機名稱改為eureka01
    hostname: eureka01
  client:
    fetch-registry: false
    # 去掉register-with-eureka選項,讓Eureka伺服器自己註冊到其他Eureka伺服器,這樣才能相互啟用
    service-url:
    	# 注意這裡填寫其他Eureka伺服器的地址,不用寫自己的
      defaultZone: http://eureka01:8801/eureka
server:
  port: 8802
spring:
  application:
    name: eurekaserver
eureka:
  instance:
    hostname: eureka02
  client:
    fetch-registry: false
    service-url:
      defaultZone: http://eureka01:8801/eureka

這裡由於我們修改成自定義的地址,需要在hosts檔案中將其解析到172.0.0.1才能回到localhost。

image-20220323210218653

對建立的兩個配置檔案分別新增啟動配置,直接使用spring.profiles.active指定啟用的配置檔案即可:

image-20220323212308857

接著啟動這兩個註冊中心,這兩個Eureka管理頁面都可以被訪問,我們訪問其中一個:

image-20220323210937341

image-20220323210619533

可以看到下方replicas中已經包含了另一個Eureka伺服器的地址,並且是可用狀態。

接著我們需要將我們的微服務配置也進行修改:

eureka:
  client:
    service-url:
    	# 將兩個Eureka的地址都加入,這樣就算有一個Eureka掛掉,也能完成註冊
      defaultZone: http://localhost:8801/eureka, http://localhost:8802/eureka

可以看到,服務全部成功註冊,並且兩個Eureka服務端都顯示為已註冊:

image-20220323211032311

接著我們模擬一下,將其中一個Eureka伺服器關閉掉,可以看到它會直接變成不可用狀態:

image-20220323211354516

當然,如果這個時候我們重啟剛剛關閉的Eureka伺服器,會自動同步其他Eureka伺服器的資料。


LoadBalancer 負載均衡

前面我們講解了如何對服務進行拆分、如何通過Eureka伺服器進行服務註冊與發現,那麼現在我們來看看,它的負載均衡到底是如何實現的,實際上之前演示的負載均衡是依靠LoadBalancer實現的。

在2020年前的SpringCloud版本是採用Ribbon作為負載均衡實現,但是2020年的版本之後SpringCloud把Ribbon移除了,進而用自己編寫的LoadBalancer替代。

那麼,負載均衡是如何進行的呢?

負載均衡

實際上,在新增@LoadBalanced註解之後,會啟用攔截器對我們發起的服務呼叫請求進行攔截(注意這裡是針對我們發起的請求進行攔截),叫做LoadBalancerInterceptor,它實現ClientHttpRequestInterceptor介面:

@FunctionalInterface
public interface ClientHttpRequestInterceptor {
    ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException;
}

主要是對intercept方法的實現:

public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
    URI originalUri = request.getURI();
    String serviceName = originalUri.getHost();
    Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
    return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}

我們可以打個斷點看看實際是怎麼在執行的,可以看到:

image-20220323220519463

image-20220323220548051

服務端會在發起請求時執行這些攔截器。

那麼這個攔截器做了什麼事情呢,首先我們要明確,我們給過來的請求地址,並不是一個有效的主機名稱,而是服務名稱,那麼怎麼才能得到真正需要訪問的主機名稱呢,肯定是得找Eureka獲取的。

我們來看看loadBalancer.execute()做了什麼,它的具體實現為BlockingLoadBalancerClient

//從上面給進來了服務的名稱和具體的請求實體
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    String hint = this.getHint(serviceId);
    LoadBalancerRequestAdapter<T, DefaultRequestContext> lbRequest = new LoadBalancerRequestAdapter(request, new DefaultRequestContext(request, hint));
    Set<LoadBalancerLifecycle> supportedLifecycleProcessors = this.getSupportedLifecycleProcessors(serviceId);
    supportedLifecycleProcessors.forEach((lifecycle) -> {
        lifecycle.onStart(lbRequest);
    });
  	//可以看到在這裡會呼叫choose方法自動獲取對應的服務例項資訊
    ServiceInstance serviceInstance = this.choose(serviceId, lbRequest);
    if (serviceInstance == null) {
        supportedLifecycleProcessors.forEach((lifecycle) -> {
            lifecycle.onComplete(new CompletionContext(Status.DISCARD, lbRequest, new EmptyResponse()));
        });
      	//沒有發現任何此服務的例項就拋異常(之前的測試中可能已經遇到了)
        throw new IllegalStateException("No instances available for " + serviceId);
    } else {
      	//成功獲取到對應服務的例項,這時就可以發起HTTP請求獲取資訊了
        return this.execute(serviceId, serviceInstance, lbRequest);
    }
}

所以,實際上在進行負載均衡的時候,會向Eureka發起請求,選擇一個可用的對應服務,然後會返回此服務的主機地址等資訊:

image-20220324120741736

自定義負載均衡策略

LoadBalancer預設提供了兩種負載均衡策略:

  • RandomLoadBalancer - 隨機分配策略
  • (預設) RoundRobinLoadBalancer - 輪詢分配策略

現在我們希望修改預設的負載均衡策略,可以進行指定,比如我們現在希望使用者服務採用隨機分配策略,我們需要先建立隨機分配策略的配置類(不用加@Configuration):

public class LoadBalancerConfig {
  	//將官方提供的 RandomLoadBalancer 註冊為Bean
    @Bean
    public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory){
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

接著我們需要為對應的服務指定負載均衡策略,直接使用註解即可:

@Configuration
@LoadBalancerClient(value = "userservice",      //指定為 userservice 服務,只要是呼叫此服務都會使用我們指定的策略
                    configuration = LoadBalancerConfig.class)   //指定我們剛剛定義好的配置類
public class BeanConfig {
    @Bean
    @LoadBalanced
    RestTemplate template(){
        return new RestTemplate();
    }
}

接著我們在BlockingLoadBalancerClient中新增斷點,觀察是否採用我們指定的策略進行請求:

image-20220324221750289

image-20220324221713964

發現訪問userservice服務的策略已經更改為我們指定的策略了。

OpenFeign實現負載均衡

官方文件:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/

Feign和RestTemplate一樣,也是HTTP客戶端請求工具,但是它的使用方式更加便捷。首先是依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

接著在啟動類新增@EnableFeignClients註解:

@SpringBootApplication
@EnableFeignClients
public class BorrowApplication {
    public static void main(String[] args) {
        SpringApplication.run(BorrowApplication.class, args);
    }
}

那麼現在我們需要呼叫其他微服務提供的介面,該怎麼做呢?我們直接建立一個對應服務的介面類即可:

@FeignClient("userservice")   //宣告為userservice服務的HTTP請求客戶端
public interface UserClient {
}

接著我們直接建立所需型別的方法,比如我們之前的:

RestTemplate template = new RestTemplate();
User user = template.getForObject("http://userservice/user/"+uid, User.class);

現在可以直接寫成這樣:

@FeignClient("userservice")
public interface UserClient {

  	//路徑保證和其他微服務提供的一致即可
    @RequestMapping("/user/{uid}")
    User getUserById(@PathVariable("uid") int uid);  //引數和返回值也保持一致
}

接著我們直接注入使用(有Mybatis那味了):

@Resource
UserClient userClient;

@Override
public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
    List<Borrow> borrow = mapper.getBorrowsByUid(uid);
    
    User user = userClient.getUserById(uid);
    //這裡不用再寫IP,直接寫服務名稱bookservice
    List<Book> bookList = borrow
            .stream()
            .map(b -> template.getForObject("http://bookservice/book/"+b.getBid(), Book.class))
            .collect(Collectors.toList());
    return new UserBorrowDetail(user, bookList);
}

訪問,可以看到結果依然是正確的:

image-20220324181614387

並且我們可以觀察一下兩個使用者微服務的呼叫情況,也是以負載均衡的形式進行的。

按照同樣的方法,我們接著將圖書管理服務的呼叫也改成介面形式:

image-20220324181740566

最後我們的Service程式碼就變成了:

@Service
public class BorrowServiceImpl implements BorrowService {

    @Resource
    BorrowMapper mapper;

    @Resource
    UserClient userClient;
    
    @Resource
    BookClient bookClient;

    @Override
    public UserBorrowDetail getUserBorrowDetailByUid(int uid) {
        List<Borrow> borrow = mapper.getBorrowsByUid(uid);

        User user = userClient.getUserById(uid);
        List<Book> bookList = borrow
                .stream()
                .map(b -> bookClient.getBookById(b.getBid()))
                .collect(Collectors.toList());
        return new UserBorrowDetail(user, bookList);
    }
}

繼續訪問進行測試:

image-20220324181910173

OK,正常。

當然,Feign也有很多的其他配置選項,這裡就不多做介紹了,詳細請查閱官方文件。


Hystrix 服務熔斷

官方文件:https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/1.3.5.RELEASE/single/spring-cloud-netflix.html#_circuit_breaker_hystrix_clients

我們知道,微服務之間是可以進行相互呼叫的,那麼如果出現了下面的情況會導致什麼問題?

image-20220324141230070

由於位於最底端的服務提供者E發生故障,那麼此時會直接導致服務ABCD全線崩潰,就像雪崩了一樣。

image-20220324141706946

這種問題實際上是不可避免的,由於多種因素,比如網路卡頓、系統故障、硬體問題等,都存在一定可能,會導致這種極端的情況發生。因此,我們需要尋找一個應對這種極端情況的解決方案。

為了解決分散式系統的雪崩問題,SpringCloud提供了Hystrix熔斷器元件,他就像我們家中的保險絲一樣,當電流過載就會直接熔斷,防止危險進一步發生,從而保證家庭用電安全。可以想象一下,如果整條鏈路上的服務已經全線崩潰,這時還在不斷地有大量的請求到達,需要各個服務進行處理,肯定是會使得情況越來越糟糕的。

我們來詳細看看它的工作機制。

服務降級

首先我們來看看服務降級,注意一定要區分開服務降級和服務熔斷的區別,服務降級並不會直接返回錯誤,而是可以提供一個補救措施,正常響應給請求者。這樣相當於服務依然可用,但是服務能力肯定是下降了的。

我們就基於借閱管理服務來進行講解,我們不開啟使用者服務和圖書服務,表示使用者服務和圖書服務已經掛掉了。

這裡我們匯入Hystrix的依賴(此專案已經停止維護,SpringCloud依賴中已經不自帶了,所以說需要自己單獨匯入):

   <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
     		<version>2.2.10.RELEASE</version>
    </dependency>

接著我們需要在啟動類新增註解開啟:

@SpringBootApplication
@EnableHystrix   //啟用Hystrix
public class BorrowApplication {
    public static void main(String[] args) {
        SpringApplication.run(BorrowApplication.class, args);
    }
}

那麼現在,由於使用者服務和圖書服務不可用,所以查詢借閱資訊的請求肯定是沒辦法正常響應的,這時我們可以提供一個備選方案,也就是說當服務出現異常時,返回我們的備選方案:

@RestController
public class BorrowController {

    @Resource
    BorrowService service;

    @HystrixCommand(fallbackMethod = "onError")    //使用@HystrixCommand來指定備選方案
    @RequestMapping("/borrow/{uid}")
    UserBorrowDetail findUserBorrows(@PathVariable("uid") int uid){
        return service.getUserBorrowDetailByUid(uid);
    }
		
  	//備選方案,這裡直接返回空列表了
  	//注意引數和返回值要和上面的一致
    UserBorrowDetail onError(int uid){
        return new UserBorrowDetail(null, Collections.emptyList());
    }
}

可以看到,雖然我們的服務無法正常執行了,但是依然可以給瀏覽器正常返回響應資料:

image-20220324150253610

image-20220324150310955

服務降級是一種比較溫柔的解決方案,雖然服務本身的不可用,但是能夠保證正常響應資料。

服務熔斷

熔斷機制是應對雪崩效應的一種微服務鏈路保護機制,當檢測出鏈路的某個微服務不可用或者響應時間太長時,會進行服務的降級,進而熔斷該節點微服務的呼叫,快速返回”錯誤”的響應資訊。當檢測到該節點微服務響應正常後恢復呼叫鏈路。

實際上,熔斷就是在降級的基礎上進一步升級形成的,也就是說,在一段時間內多次呼叫失敗,那麼就直接升級為熔斷。

我們可以新增兩條輸出語句:

@RestController
public class BorrowController {

    @Resource
    BorrowService service;

    @HystrixCommand(fallbackMethod = "onError")
    @RequestMapping("/borrow/{uid}")
    UserBorrowDetail findUserBorrows(@PathVariable("uid") int uid){
        System.out.println("開始向其他服務獲取資訊");
        return service.getUserBorrowDetailByUid(uid);
    }

    UserBorrowDetail onError(int uid){
        System.out.println("服務錯誤,進入備選方法!");
        return new UserBorrowDetail(null, Collections.emptyList());
    }
}

接著,我們在瀏覽器中瘋狂點選重新整理按鈕,對此服務瘋狂發起請求,可以看到後臺:

image-20220324152044551

一開始的時候,會正常地去呼叫Controller對應的方法findUserBorrows,發現失敗然後進入備選方法,但是我們發現在持續請求一段時間之後,沒有再呼叫這個方法,而是直接呼叫備選方案,這便是升級到了熔斷狀態。

我們可以繼續不斷點選,繼續不斷地發起請求:

image-20220324152750797

可以看到,過了一段時間之後,會嘗試正常執行一次findUserBorrows,但是依然是失敗狀態,所以繼續保持熔斷狀態。

所以得到結論,它能夠對一段時間內出現的錯誤進行偵測,當偵測到出錯次數過多時,熔斷器會開啟,所有的請求會直接響應失敗,一段時間後,只執行一定數量的請求,如果還是出現錯誤,那麼則繼續保持開啟狀態,否則說明服務恢復正常執行,關閉熔斷器。

我們可以測試一下,開啟另外兩個服務之後,繼續點選:

image-20220324153044583

可以看到,當另外兩個服務正常執行之後,當再次嘗試呼叫findUserBorrows之後會成功,於是熔斷機制就關閉了,服務恢復執行。

總結一下:

image-20220324153935858

OpenFeign實現降級

Hystrix也可以配合Feign進行降級,我們可以對應介面中定義的遠端呼叫單獨進行降級操作。

比如我們還是以使用者服務掛掉為例,那麼這個時候肯定是會遠端呼叫失敗的,也就是說我們的Controller中的方法在執行過程中會直接丟擲異常,進而被Hystrix監控到並進行服務降級。

而實際上導致方法執行異常的根源就是遠端呼叫失敗,所以我們換個思路,既然使用者服務呼叫失敗,那麼我就給這個遠端呼叫新增一個替代方案,如果此遠端呼叫失敗,那麼就直接上替代方案。那麼怎麼實現替代方案呢?我們知道Feign都是以介面的形式來宣告遠端呼叫,那麼既然遠端呼叫已經失效,我們就自行對其進行實現,建立一個實現類,對原有的介面方法進行替代方案實現:

@Component   //注意,需要將其註冊為Bean,Feign才能自動注入
public class UserFallbackClient implements UserClient{
    @Override
    public User getUserById(int uid) {   //這裡我們自行對其進行實現,並返回我們的替代方案
        User user = new User();
        user.setName("我是替代方案");
        return user;
    }
}

實現完成後,我們只需要在原有的介面中指定失敗替代實現即可:

//fallback引數指定為我們剛剛編寫的實現類
@FeignClient(value = "userservice", fallback = UserFallbackClient.class)
public interface UserClient {

    @RequestMapping("/user/{uid}")
    User getUserById(@PathVariable("uid") int uid);
}

現在去掉BorrowController@HystrixCommand註解和備選方法:

@RestController
public class BorrowController {

    @Resource
    BorrowService service;

    @RequestMapping("/borrow/{uid}")
    UserBorrowDetail findUserBorrows(@PathVariable("uid") int uid){
        return service.getUserBorrowDetailByUid(uid);
    }
}

最後我們在配置檔案中開啟熔斷支援:

feign:
  circuitbreaker:
    enabled: true

啟動服務,呼叫介面試試看:

image-20220325122629016

image-20220325122301779

可以看到,現在已經採用我們的替代方案作為結果。

監控頁面部署

除了對服務的降級和熔斷處理,我們也可以對其進行實時監控,只需要安裝監控頁面即可,這裡我們建立一個新的專案,匯入依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    <version>2.2.10.RELEASE</version>
</dependency>

接著新增配置檔案:

server:
  port: 8900
hystrix:
  dashboard:
    # 將localhost新增到白名單,預設是不允許的
    proxy-stream-allow-list: "localhost"

接著建立主類,注意需要新增@EnableHystrixDashboard註解開啟管理頁面:

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashBoardApplication {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashBoardApplication.class, args);
    }
}

啟動Hystrix管理頁面服務,然後我們需要在要進行監控的服務中新增Actuator依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Actuator是SpringBoot程式的監控系統,可以實現健康檢查,記錄資訊等。在使用之前需要引入spring-boot-starter-actuator,並做簡單的配置即可。

新增此依賴後,我們可以在IDEA中檢視執行情況:

image-20220324225633805

然後在配置檔案中配置Actuator新增暴露:

management:
  endpoints:
    web:
      exposure:
        include: '*'

接著我們開啟剛剛啟動的管理頁面,地址為:http://localhost:8900/hystrix/

image-20220324225733550

在中間填寫要監控的服務:比如借閱服務:http://localhost:8301/actuator/hystrix.stream,注意後面要新增/actuator/hystrix.stream,然後點選Monitor Stream即可進入監控頁面:

image-20220324230515009

可以看到現在都是Loading狀態,這是因為還沒有開始統計,我們現在嘗試呼叫幾次我們的服務:

image-20220324230559068

可以看到,在呼叫之後,監控頁面出現了資訊:

image-20220324230703600

可以看到5次訪問都是正常的,所以顯示為綠色,接著我們來嘗試將圖書服務關閉,這樣就會導致服務降級甚至熔斷,然後再多次訪問此服務看看監控會如何變化:

image-20220324230923472

可以看到,錯誤率直接飆升到100%,並且一段時間內持續出現錯誤,中心的圓圈也變成了紅色,我們繼續進行訪問:

image-20220324231022133

在出現大量錯誤的情況下保持持續訪問,可以看到此時已經將服務熔斷,Circuit更改為Open狀態,並且圖中的圓圈也變得更大,表示壓力在持續上升。


Gateway 路由閘道器

官網地址:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/

說到路由,想必各位一定最先想到的就是家裡的路由器了,那麼我們家裡的路由器充當的是一個什麼角色呢?

我們知道,如果我們需要連線網際網路,那麼就需要將手機或是電腦連線到家裡的路由器才可以,而路由器則連線光貓,光貓再通過光纖連線到網際網路。

也就是說,網際網路方向傳送過來的資料,需要經過路由器才能到達我們的裝置。而路由器充當的就是資料包中轉站,所有的區域網裝置都無法直接與網際網路連線,而是需要經過路由器進行中轉,我們一般說路由器下的網路是內網,而網際網路那一端是外網。

image-20220324164439809

我們的區域網裝置,無法被網際網路上的其他裝置直接訪問,肯定是能夠保證到安全性的。並網際網路傳送過來的資料,需要經過路由器進行解析,識別到底是哪一個裝置的資料包,然後再傳送給對應的裝置。

而我們的微服務也是這樣,一般情況下,可能並不是所有的微服務都需要直接暴露給外部呼叫,這時我們就可以使用路由機制,新增一層防護,讓所有的請求全部通過路由來轉發到各個微服務,並且轉發給多個相同微服務例項也可以實現負載均衡。

image-20220325130147758

在之前,路由的實現一般使用Zuul,但是已經停更,而現在新出現了由SpringCloud官方開發的Gateway路由,它相比Zuul不僅效能上得到了一定的提升,並且是官方推出,契合性也會更好,所以我們這裡就主要講Gateway。

部署閘道器

現在我們來建立一個新的專案,作為我們的閘道器,這裡需要新增兩個依賴:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

第一個依賴就是閘道器的依賴,而第二個則跟其他微服務一樣,需要註冊到Eureka才能生效,注意別新增Web依賴,使用的是WebFlux框架。

然後我們來完善一下配置檔案:

server:
  port: 8500
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8801/eureka, http://localhost:8802/eureka
spring:
  application:
    name: gateway

現在就可以啟動了:

image-20220324170951878

但是現在還沒有配置任何的路由功能,我們接著將路由功能進行配置:

spring:
  cloud:
    gateway:
    	# 配置路由,注意這裡是個列表,每一項都包含了很多資訊
      routes:
        - id: borrow-service   # 路由名稱
          uri: lb://borrowservice  # 路由的地址,lb表示使用負載均衡到微服務,也可以使用http正常轉發
          predicates: # 路由規則,斷言什麼請求會被路由
            - Path=/borrow/**  # 只要是訪問的這個路徑,一律都被路由到上面指定的服務

路由規則的詳細列表(斷言工廠列表)在這裡:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories,可以指定多種型別,包括指定時間段、Cookie攜帶情況、Header攜帶情況、訪問的域名地址、訪問的方法、路徑、引數、訪問者IP等。也可以使用配置類進行配置,但是還是推薦直接配置檔案,省事。

接著啟動閘道器,搭載Arm架構晶片的Mac電腦可能會遇到這個問題:

image-20220325150924472

這是因為沒有找到適用於此架構的動態連結庫,不影響使用,無視即可,希望以後的版本能修復吧。

可以看到,我們現在可以直接通過路由來訪問我們的服務了:

image-20220324171724493

注意此時依然可以通過原有的服務地址進行訪問:

image-20220324171909828

這樣我們就可以將不需要外網直接訪問的微服務全部放到內網環境下,而只依靠閘道器來對外進行交涉。

路由過濾器

路由過濾器支援以某種方式修改傳入的 HTTP 請求或傳出的 HTTP 響應,路由過濾器的範圍是某一個路由,跟之前的斷言一樣,Spring Cloud Gateway 也包含許多內建的路由過濾器工廠,詳細列表:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories

比如我們現在希望在請求到達時,在請求頭中新增一些資訊再轉發給我們的服務,那麼這個時候就可以使用路由過濾器來完成,我們只需要對配置檔案進行修改:

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
      - id: borrow-service
        uri: lb://borrowservice
        predicates:
        - Path=/borrow/**
      # 繼續新增新的路由配置,這裡就以書籍管理服務為例
      # 注意-要對齊routes:
      - id: book-service
        uri: lb://bookservice
        predicates:
        - Path=/book/**
        filters:   # 新增過濾器
        - AddRequestHeader=Test, HelloWorld!
        # AddRequestHeader 就是新增請求頭資訊,其他工廠請查閱官網

接著我們在BookController中獲取並輸出一下,看看是不是成功新增了:

@RestController
public class BookController {

    @Resource
    BookService service;

    @RequestMapping("/book/{bid}")
    Book findBookById(@PathVariable("bid") int bid,
                      HttpServletRequest request){
        System.out.println(request.getHeader("Test"));
        return service.getBookById(bid);
    }
}

現在我們通過Gateway訪問我們的圖書管理服務:

image-20220325150730814

image-20220325151220776

可以看到這裡成功獲取到由閘道器新增的請求頭資訊了。

除了針對於某一個路由配置過濾器之外,我們也可以自定義全域性過濾器,它能夠作用於全域性。但是我們需要通過程式碼的方式進行編寫,比如我們要實現攔截沒有攜帶指定請求引數的請求:

@Component   //需要註冊為Bean
public class TestFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {   //只需要實現此方法
        return null;
    }
}

接著我們編寫判斷:

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    //先獲取ServerHttpRequest物件,注意不是HttpServletRequest
    ServerHttpRequest request = exchange.getRequest();
    //列印一下所有的請求引數
    System.out.println(request.getQueryParams());
    //判斷是否包含test引數,且引數值為1
    List<String> value = request.getQueryParams().get("test");
    if(value != null && value.contains("1")) {
        //將ServerWebExchange向過濾鏈的下一級傳遞(跟JavaWeb中介紹的過濾器其實是差不多的)
        return chain.filter(exchange);
    }else {
        //直接在這裡不再向下傳遞,然後返回響應
        return exchange.getResponse().setComplete();
    }
}

可以看到結果:

image-20220325154443063

image-20220325154508853

成功實現規則判斷和攔截操作。

當然,過濾器肯定是可以存在很多個的,所以我們可以手動指定過濾器之間的順序:

@Component
public class TestFilter implements GlobalFilter, Ordered {   //實現Ordered介面
  
    @Override
    public int getOrder() {
        return 0;
    }

注意Order的值越小優先順序越高,並且無論是在配置檔案中編寫的單個路由過濾器還是全域性路由過濾器,都會受到Order值影響(單個路由的過濾器Order值按從上往下的順序從1開始遞增),最終是按照Order值決定哪個過濾器優先執行,當Order值一樣時 全域性路由過濾器執行 優於 單獨的路由過濾器執行。


Config 配置中心

官方文件:https://docs.spring.io/spring-cloud-config/docs/current/reference/html/

經過前面的學習,我們對於一個分散式應用的技術選型和搭建已經瞭解得比較多了,但是各位有沒有發現一個問題,如果我們的微服務專案需要部署很多個例項,那麼配置檔案我們豈不是得一個一個去改,可能十幾個例項還好,要是有幾十個上百個呢?那我們一個一個去配置,豈不直接猝死在工位上。

所以,我們需要一種更加高階的集中化地配置檔案管理工具,集中地對配置檔案進行配置。

Spring Cloud Config 為分散式系統中的外部配置提供伺服器端和客戶端支援。使用 Config Server,您可以集中管理所有環境中應用程式的外部配置。

image-20220325171754862

實際上Spring Cloud Config就是一個配置中心,所有的服務都可以從配置中心取出配置,而配置中心又可以從GitHub遠端倉庫中獲取雲端的配置檔案,這樣我們只需要修改GitHub中的配置即可對所有的服務進行配置管理了。

部署配置中心

這裡我們接著建立一個新的專案,並匯入依賴:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
  	<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

老規矩,啟動類:

@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigApplication.class, args);
    }
}

接著就是配置檔案:

server:
  port: 8700
spring:
  application:
    name: configserver
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8801/eureka, http://localhost:8802/eureka

先啟動一次看看,能不能成功:

image-20220325173932623

這裡我們以本地倉庫為例(就不用GitHub了,卡到懷疑人生了),首先在專案目錄下建立一個本地Git倉庫,開啟終端,在桌面上建立一個新的本地倉庫:

image-20220325220843990

然後我們在資料夾中隨便建立一些配置檔案,注意名稱最好是{服務名稱}-{環境}.yml:

image-20220325221411834

然後我們在配置檔案中,新增本地倉庫的一些資訊(遠端倉庫同理),詳細使用教程:https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_git_backend

spring:
  cloud:
    config:
      server:
        git:
        	# 這裡填寫的是本地倉庫地址,遠端倉庫直接填寫遠端倉庫地址 http://git...
          uri: file://${user.home}/Desktop/config-repo
          # 預設分支設定為你自己本地或是遠端分支的名稱
          default-label: main

然後啟動我們的配置伺服器,通過以下格式進行訪問:

比如我們要訪問圖書服務的生產環境程式碼,可以使用 http://localhost:8700/bookservice/prod/main 連結,它會顯示詳細資訊:

image-20220325221946363

也可以使用 http://localhost:8700/main/bookservice-prod.yml 連結,它僅顯示配置檔案原文:

image-20220325222309095

當然,除了使用Git來儲存之外,還支援一些其他的方式,詳細情況請查閱官網。

客戶端配置

服務端配置完成之後,我們接著來配置一下客戶端,那麼現在我們的服務既然需要從伺服器讀取配置檔案,那麼就需要進行一些配置,我們刪除原來的application.yml檔案(也可以保留,最後無論是遠端配置還是本地配置都會被載入),改用bootstrap.yml(在application.yml之前載入,可以實現配置檔案遠端獲取):

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
spring:
  cloud:
    config:
    	# 名稱,其實就是檔名稱
      name: bookservice
      # 配置伺服器的地址
      uri: http://localhost:8700
      # 環境
      profile: prod
      # 分支
      label: main

配置完成之後,啟動圖書服務:

image-20220325224708591

可以看到已經從遠端獲取到了配置,並進行啟動。


微服務CAP原則

經過前面的學習,我們對SpringCloud Netflix以及SpringCloud官方整個生態下的元件認識也差不多了。

image-20220325230915356

CAP原則又稱CAP定理,指的是在一個分散式系統中,存在Consistency(一致性)、Availability(可用性)、Partition tolerance(分割槽容錯性),三者不可同時保證,最多隻能保證其中的兩者。

一致性(C):在分散式系統中的所有資料備份,在同一時刻都是同樣的值(所有的節點無論何時訪問都能拿到最新的值)

可用性(A):系統中非故障節點收到的每個請求都必須得到響應(比如我們之前使用的服務降級和熔斷,其實就是一種維持可用性的措施,雖然服務返回的是沒有什麼意義的資料,但是不至於使用者的請求會被伺服器忽略)

分割槽容錯性(P):一個分散式系統裡面,節點之間組成的網路本來應該是連通的,然而可能因為一些故障(比如網路丟包等,這是很難避免的),使得有些節點之間不連通了,整個網路就分成了幾塊區域,資料就散佈在了這些不連通的區域中(這樣就可能出現某些被分割槽節點存放的資料訪問失敗,我們需要來容忍這些不可靠的情況)

總的來說,資料存放的節點數越多,分割槽容忍性就越高,但是要複製更新的次數就越多,一致性就越難保證。同時為了保證一致性,更新所有節點資料所需要的時間就越長,那麼可用性就會降低。

所以說,只能存在以下三種方案:

AC 可用性+一致性

要同時保證可用性和一致性,代表著某個節點資料更新之後,需要立即將結果通知給其他節點,並且要儘可能的快,這樣才能及時響應保證可用性,這就對網路的穩定性要求非常高,但是實際情況下,網路很容易出現丟包等情況,並不是一個可靠的傳輸,如果需要避免這種問題,就只能將節點全部放在一起,但是這顯然違背了分散式系統的概念,所以對於我們的分散式系統來說,很難接受。

CP 一致性+分割槽容錯性

為了保證一致性,那麼就得將某個節點的最新資料傳送給其他節點,並且需要等到所有節點都得到資料才能進行響應,同時有了分割槽容錯性,那麼代表我們可以容忍網路的不可靠問題,所以就算網路出現卡頓,那麼也必須等待所有節點完成資料同步,才能進行響應,因此就會導致服務在一段時間內完全失效,所以可用性是無法得到保證的。

AP 可用性+分割槽容錯性

既然CP可能會導致一段時間內服務得不到任何響應,那麼要保證可用性,就只能放棄節點之間資料的高度統一,也就是說可以在資料不統一的情況下,進行響應,因此就無法保證一致性了。雖然這樣會導致拿不到最新的資料,但是隻要資料同步操作在後臺繼續執行,一定能夠在某一時刻完成所有節點資料的同步,那麼就能實現最終一致性,所以AP實際上是最能接受的一種方案。


比如我們實現的Eureka叢集,它使用的就是AP方案,Eureka各個節點都是平等的,少數節點掛掉不會影響正常節點的工作,剩餘的節點依然可以提供註冊和查詢服務。而Eureka客戶端在向某個Eureka服務端註冊時如果發現連線失敗,則會自動切換至其他節點。只要有一臺Eureka伺服器正常執行,那麼就能保證服務可用(A),只不過查詢到的資訊可能不是最新的(C)

相關文章