服務架構的演化

引言:隨着互聯網的發展,網站應用的規模不斷擴大。需求的激增,帶來的是技術上的壓力。系統架構也因此也不斷的演進、升級、迭代。從單一應用,到垂直拆分,到分佈式服務,到SOA,以及現在火熱的微服務架構,還有在Google帶領下來勢洶涌的Service Mesh。

1.1. 集中式架構

當網站流量很小時,只需一個應用,將所有功能都部署在一起,以減少部署節點和成本。此時,用於簡化增刪改查工作量的數據訪問框架(ORM)是影響項目開發的關鍵。
在這裏插入圖片描述
存在的問題:

  • 代碼耦合,開發維護困難
  • 無法針對不同模塊進行針對性優化
  • 無法水平擴展
  • 單點容錯率低,併發能力差

1.2.垂直拆分

當訪問量逐漸增大,單一應用無法滿足需求,此時爲了應對更高的併發和業務需求,我們根據業務功能對系統進行拆分:

在這裏插入圖片描述

優點:

  • 系統拆分實現了流量分擔,解決了併發問題
  • 可以針對不同模塊進行優化
  • 方便水平擴展,負載均衡,容錯率提高

缺點:

  • 系統間相互獨立,會有很多重複開發工作,影響開發效率

1.3.分佈式服務

當垂直應用越來越多,應用之間交互不可避免,將核心業務抽取出來,作爲獨立的服務,逐漸形成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。此時,用於提高業務複用及整合的分佈式調用是關鍵。

在這裏插入圖片描述

優點:

  • 將基礎服務進行了抽取,系統間相互調用,提高了代碼複用和開發效率

缺點:

  • 系統間耦合度變高,調用關係錯綜複雜,難以維護

1.4.服務治理(SOA)

當服務越來越多,容量的評估,小服務資源的浪費等問題逐漸顯現,此時需增加一個調度中心基於訪問壓力實時管理集羣容量,提高集羣利用率。此時,用於提高機器利用率的資源調度和治理中心(SOA)是關鍵

在這裏插入圖片描述

以前出現了什麼問題?

  • 服務越來越多,需要管理每個服務的地址
  • 調用關係錯綜複雜,難以理清依賴關係
  • 服務過多,服務狀態難以管理,無法根據服務情況動態管理

服務治理要做什麼?

  • 服務註冊中心,實現服務自動註冊和發現,無需人爲記錄服務地址
  • 服務自動訂閱,服務列表自動推送,服務調用透明化,無需關心依賴關係
  • 動態監控服務狀態監控報告,人爲控制服務狀態

缺點:

  • 服務間會有依賴關係,一旦某個環節出錯會影響較大
  • 服務關係複雜,運維、測試部署困難,不符合DevOps思想

1.5.微服務

前面說的SOA,英文翻譯過來是面向服務。微服務,似乎也是服務,都是對系統進行拆分。因此兩者非常容易混淆,但其實缺有一些差別:

在這裏插入圖片描述

微服務的特點:

  • 單一職責:微服務中每一個服務都對應唯一的業務能力,做到單一職責
  • 微:微服務的服務拆分粒度很小,例如一個用戶管理就可以作爲一個服務。每個服務雖小,但“五臟俱全”。
  • 面向服務:面向服務是說每個服務都要對外暴露服務接口API。並不關心服務的技術實現,做到與平臺和語言無關,也不限定用什麼技術實現,只要提供Rest的接口即可。
  • 自治:自治是說服務間互相獨立,互不干擾
    • 團隊獨立:每個服務都是一個獨立的開發團隊,人數不能過多。
    • 技術獨立:因爲是面向服務,提供Rest接口,使用什麼技術沒有別人干涉
    • 前後端分離:採用前後端分離開發,提供統一Rest接口,後端不用再爲PC、移動段開發不同接口
    • 數據庫分離:每個服務都使用自己的數據源
    • 部署獨立,服務間雖然有調用,但要做到服務重啓不影響其它服務。有利於持續集成和持續交付。每個服務都是獨立的組件,可複用,可替換,降低耦合,易維護

微服務結構圖:

在這裏插入圖片描述

2.遠程調用方式

無論是微服務還是SOA,都面臨着服務間的遠程調用。那麼服務間的遠程調用方式有哪些呢?

常見的遠程調用方式有以下幾種:

  • RPC:Remote Produce Call遠程過程調用,類似的還有RMI。自定義數據格式,基於原生TCP通信,速度快,效率高。早期的webservice,現在熱門的dubbo,都是RPC的典型

  • Http:http其實是一種網絡傳輸協議,基於TCP,規定了數據傳輸的格式。現在客戶端瀏覽器與服務端通信基本都是採用Http協議。也可以用來進行遠程服務調用。缺點是消息封裝臃腫。

    現在熱門的Rest風格,就可以通過http協議來實現。

2.1.認識RPC

RPC,即 Remote Procedure Call(遠程過程調用),是一個計算機通信協議。 該協議允許運行於一臺計算機的程序調用另一臺計算機的子程序,而程序員無需額外地爲這個交互作用編程。說得通俗一點就是:A計算機提供一個服務,B計算機可以像調用本地服務那樣調用A計算機的服務。

通過上面的概念,我們可以知道,實現RPC主要是做到兩點:

  • 實現遠程調用其他計算機的服務
    • 要實現遠程調用,肯定是通過網絡傳輸數據。A程序提供服務,B程序通過網絡將請求參數傳遞給A,A本地執行後得到結果,再將結果返回給B程序。這裏需要關注的有兩點:
      • 1)採用何種網絡通訊協議?
        • 現在比較流行的RPC框架,都會採用TCP作爲底層傳輸協議
      • 2)數據傳輸的格式怎樣?
        • 兩個程序進行通訊,必須約定好數據傳輸格式。就好比兩個人聊天,要用同一種語言,否則無法溝通。所以,我們必須定義好請求和響應的格式。另外,數據在網路中傳輸需要進行序列化,所以還需要約定統一的序列化的方式。
  • 像調用本地服務一樣調用遠程服務
    • 如果僅僅是遠程調用,還不算是RPC,因爲RPC強調的是過程調用,調用的過程對用戶而言是應該是透明的,用戶不應該關心調用的細節,可以像調用本地服務一樣調用遠程服務。所以RPC一定要對調用的過程進行封裝

RPC調用流程圖:
在這裏插入圖片描述
想要了解詳細的RPC實現,給大家推薦一篇文章:自己動手實現RPC

2.2.認識Http

Http協議:超文本傳輸協議,是一種應用層協議。規定了網絡傳輸的請求格式、響應格式、資源定位和操作的方式等。但是底層採用什麼網絡傳輸協議,並沒有規定,不過現在都是採用TCP協議作爲底層傳輸協議。說到這裏,大家可能覺得,Http與RPC的遠程調用非常像,都是按照某種規定好的數據格式進行網絡通信,有請求,有響應。沒錯,在這點來看,兩者非常相似,但是還是有一些細微差別。

  • RPC並沒有規定數據傳輸格式,這個格式可以任意指定,不同的RPC協議,數據格式不一定相同。
  • Http中還定義了資源定位的路徑,RPC中並不需要
  • 最重要的一點:RPC需要滿足像調用本地服務一樣調用遠程服務,也就是對調用過程在API層面進行封裝。Http協議沒有這樣的要求,因此請求、響應等細節需要我們自己去實現。
    • 優點:RPC方式更加透明,對用戶更方便。Http方式更靈活,沒有規定API和語言,跨語言、跨平臺
    • 缺點:RPC方式需要在API層面進行封裝,限制了開發的語言環境。

例如我們通過瀏覽器訪問網站,就是通過Http協議。只不過瀏覽器把請求封裝,發起請求以及接收響應,解析響應的事情都幫我們做了。如果是不通過瀏覽器,那麼這些事情都需要自己去完成。

在這裏插入圖片描述

2.3.如何選擇?

既然兩種方式都可以實現遠程調用,我們該如何選擇呢?

  • 速度來看,RPC要比http更快,雖然底層都是TCP,但是http協議的信息往往比較臃腫,不過可以採用gzip壓縮。
  • 難度來看,RPC實現較爲複雜,http相對比較簡單
  • 靈活性來看,http更勝一籌,因爲它不關心實現細節,跨平臺、跨語言。

因此,兩者都有不同的使用場景:

  • 如果對效率要求更高,並且開發過程使用統一的技術棧,那麼用RPC還是不錯的。
  • 如果需要更加靈活,跨語言、跨平臺,顯然http更合適

那麼我們該怎麼選擇呢?

微服務,更加強調的是獨立、自治、靈活。而RPC方式的限制較多,因此微服務框架中,一般都會採用基於Http的Rest風格服務。

3.Http客戶端工具

既然微服務選擇了Http,那麼我們就需要考慮自己來實現對請求和響應的處理。不過開源世界已經有很多的http客戶端工具,能夠幫助我們做這些事情,例如:

  • HttpClient
  • OKHttp
  • URLConnection

接下來,我們就一起了解一款比較流行的客戶端工具:HttpClient

3.1.HttpClient

3.1.1.介紹

HttpClient是Apache公司的產品,是Http Components下的一個組件。

官網地址:http://hc.apache.org/index.html

在這裏插入圖片描述

特點:

  • 基於標準、純淨的Java語言。實現了Http1.0和Http1.1
  • 以可擴展的面向對象的結構實現了Http全部的方法(GET, POST, PUT, DELETE, HEAD, OPTIONS, and TRACE)
  • 支持HTTPS協議。
  • 通過Http代理建立透明的連接。
  • 自動處理Set-Cookie中的Cookie。

3.1.2.使用

我們導入課前資料提供的demo工程:《http-demo》

發起get請求:

    @Test
    public void testGet() throws IOException {
        HttpGet request = new HttpGet("http://www.baidu.com");
        String response = this.httpClient.execute(request, new BasicResponseHandler());
        System.out.println(response);
    }

發起Post請求:

@Test
public void testPost() throws IOException {
    HttpPost request = new HttpPost("http://www.oschina.net/");
    request.setHeader("User-Agent",
                      "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36");
    String response = this.httpClient.execute(request, new BasicResponseHandler());
    System.out.println(response);
}

嘗試訪問昨天編寫的接口:http://localhost/hello

這個接口返回一個User對象

@Test
public void testGetPojo() throws IOException {
    HttpGet request = new HttpGet("http://localhost/hello");
    String response = this.httpClient.execute(request, new BasicResponseHandler());
    System.out.println(response);
}

我們實際得到的是一個json字符串:

{
    "id": 8,
    "userName": "liuyan",
    "password": "123456",
    "name": "柳巖",
    "age": 21,
    "sex": 2,
    "birthday": "1995-08-07T16:00:00.000+0000",
    "created": "2014-09-20T03:41:15.000+0000",
    "updated": "2014-09-20T03:41:15.000+0000",
    "note": "柳巖同學在傳智播客學表演"
}

如果想要得到對象,我們還需要手動進行Json反序列化,這一點比較麻煩。

3.1.3.Json轉換工具

HttpClient請求數據後是json字符串,需要我們自己把Json字符串反序列化爲對象,我們會使用JacksonJson工具來實現。

JacksonJson是SpringMVC內置的json處理工具,其中有一個ObjectMapper類,可以方便的實現對json的處理:

對象轉json

// json處理工具
    private ObjectMapper mapper = new ObjectMapper();
    @Test
    public void testJson() throws JsonProcessingException {
        User user = new User();
        user.setId(8L);
        user.setAge(21);
        user.setName("柳巖");
        user.setUserName("liuyan");
        // 序列化
        String json = mapper.writeValueAsString(user);
        System.out.println("json = " + json);
    }

結果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4zUQV0Ql-1576475727537)(assets/1526877496885.png)]

json轉普通對象

// json處理工具
private ObjectMapper mapper = new ObjectMapper();
@Test
public void testJson() throws IOException {
    User user = new User();
    user.setId(8L);
    user.setAge(21);
    user.setName("柳巖");
    user.setUserName("liuyan");
    // 序列化
    String json = mapper.writeValueAsString(user);

    // 反序列化,接收兩個參數:json數據,反序列化的目標類字節碼
    User result = mapper.readValue(json, User.class);
    System.out.println("result = " + result);
}

結果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UkrvQufO-1576475727537)(assets/1526877647406.png)]

json轉集合

json轉集合比較麻煩,因爲你無法同時把集合的class和元素的class同時傳遞到一個參數。

因此Jackson做了一個類型工廠,用來解決這個問題:

// json處理工具
private ObjectMapper mapper = new ObjectMapper();
@Test
public void testJson() throws IOException {
    User user = new User();
    user.setId(8L);
    user.setAge(21);
    user.setName("柳巖");
    user.setUserName("liuyan");

    // 序列化,得到對象集合的json字符串
    String json = mapper.writeValueAsString(Arrays.asList(user, user));

    // 反序列化,接收兩個參數:json數據,反序列化的目標類字節碼
    List<User> users = mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, User.class));
    for (User u : users) {
        System.out.println("u = " + u);
    }
}

結果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Gfkn4M8r-1576475727538)(assets/1526877995530.png)]

json轉任意複雜類型

當對象泛型關係複雜時,類型工廠也不好使了。這個時候Jackson提供了TypeReference來接收類型泛型,然後底層通過反射來獲取泛型上的具體類型。實現數據轉換。

// json處理工具
private ObjectMapper mapper = new ObjectMapper();
@Test
public void testJson() throws IOException {
    User user = new User();
    user.setId(8L);
    user.setAge(21);
    user.setName("柳巖");
    user.setUserName("liuyan");

    // 序列化,得到對象集合的json字符串
    String json = mapper.writeValueAsString(Arrays.asList(user, user));

    // 反序列化,接收兩個參數:json數據,反序列化的目標類字節碼
    List<User> users = mapper.readValue(json, new TypeReference<List<User>>(){});
    for (User u : users) {
        System.out.println("u = " + u);
    }
}

結果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8zwyfEgN-1576475727539)(assets/1526877988488.png)]

3.3.Spring的RestTemplate

Spring提供了一個RestTemplate模板工具類,對基於Http的客戶端進行了封裝,並且實現了對象與json的序列化和反序列化,非常方便。RestTemplate並沒有限定Http的客戶端類型,而是進行了抽象,目前常用的3種都有支持:

  • HttpClient
  • OkHttp
  • JDK原生的URLConnection(默認的)

首先在項目中註冊一個RestTemplate對象,可以在啓動類位置註冊:

@SpringBootApplication
public class HttpDemoApplication {

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

	@Bean
	public RestTemplate restTemplate() {
        // 默認的RestTemplate,底層是走JDK的URLConnection方式。
		return new RestTemplate();
	}
}

在測試類中直接@Autowired注入:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = HttpDemoApplication.class)
public class HttpDemoApplicationTests {

	@Autowired
	private RestTemplate restTemplate;

	@Test
	public void httpGet() {
		User user = this.restTemplate.getForObject("http://localhost/hello", User.class);
		System.out.println(user);
	}
}
  • 通過RestTemplate的getForObject()方法,傳遞url地址及實體類的字節碼,RestTemplate會自動發起請求,接收響應,並且幫我們對響應結果進行反序列化。
    在這裏插入圖片描述
發佈了48 篇原創文章 · 獲贊 11 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章