文章出處

在此之前

在此之前,你需要知道中間件的概念,可能在過往的從業生涯這個名詞無數次的從你的眼前、耳畔都留下了足記,但是它的樣子依然很模糊。
今天要說的服務化框架其實就是中間件的范疇,我們來看下,什么是中間件:

中間件是為軟件應用提供了操作系統所提供的服務之外的服務,可以把中間件描述為“軟件膠水”。中間件不是操作系統的一部分,不是數據庫管理系統,也不是軟件應用的一部分,而是能夠讓軟件開發者方便的處理通訊、輸入和輸出,能夠專注在他們自己應用的部分。

從這段定義來看,我們要通俗易懂的描述中間件這個概念實在有些困難。所以我們首先借助我們了解的操作系統、數據庫管理系統的概念,將中間件與他們撇清關系,而后通過一段較為抽象的說明對中間件下了個定義。

舉個例子,如果你知道消息隊列,比如我們常用的RabbitMQ,它就是中間件,它可以為我們削峰,為我們提供異步處理業務的可能,它就是名副其實的軟件膠水。下面我們從另外一個側面——服務化框架來體會下中間件是一種什么樣的存在。

服務化框架是怎么來的

《大型網站的自強之路》中我們看到了大型網站的一步步演化,從單應用到多應用,從單庫到分庫分表,所有這些演化都是源于業務、訪問量、并發量的增加。

這樣一個網站結構簡單又清晰,一般來說可以滿足常規需求。但是隨著業務越來越復雜,網站規模也日益擴大,原本清晰劃分的應用模塊A、B和C上加上了很多不屬于他們的代碼。這樣的狀況持續的時間越長,網站的結構就變的越來越沒有邊界,也就在高內聚低耦合的路上漸行漸遠。

舉例來說,某天我們在應用A上加的無關代碼太多以至于我們不能再冠之以“應用A”的名號。假設應用A表示的是商品系統,我們慢慢的在其基礎上加入了商品的展示功能,商品的添加、商品的更新等等功能,代碼不斷的增加,維護的人也越來越多,讓這個應用模塊變的愈加臃腫復雜。

這時候我們需要做應用級別的拆分,對于用戶管理系統我們可以單獨提出一塊,訂單系統提出一塊,商品系統作為一塊……慢慢的,我們的網站結構圖就成了這樣

現在從功能劃分這個角度來看,我們的系統相較于之前著實清晰了不少,再也不用很多不相干的代碼擠在一起了。但是在實際使用過程中,我們發現有些東西我們可以作進一步的抽象,比如,在一個電商網站中,在訂單系統和交易系統中,我們都有依賴用戶系統。雖然各個應用模塊看似分工明確,但實際上他們之間還是有些藕斷絲連。

我們進一步抽象,將這些被多個應用模塊的應用抽象為一個服務,添加一個服務層,使得各大應用之間的交集演變為服務層的一個服務,這樣對于公共的服務抽象出來,應用層只需要調用相應的服務即可,再也不用自己重復造輪子了。

這樣我們就得到了服務化的框架,這個框架有它自身的好處:

  • 結構清晰 應用層和服務層以及底層基礎層結構清晰明了
  • 穩定性 通過服務層的隔離,使得應用層不在直接操作接觸底層服務如DB緩存等,提供了系統的穩定性
  • 解耦 使得原來還交錯依賴的應用模塊耦合度降低
  • 高擴展性 如果需要接入新的服務或者應用,直接水平擴展即可,前面的解耦為高可擴展提供了支持

現如今,我們經常提到的微服務,也就是這種思想。一個模塊就是一個完整的服務,內部通過調用底層的基礎服務,然后對外提供服務,這樣耦合性低且易于維護。

服務化框架是怎么用的

我想應該沒有哪個有位青年,在當時學到Socket編程的時候能夠克制自己的好奇心,對于聊天室這個小東西無動于衷,甚至都不愿意多看它一眼。
總之對于聊天之略知一二的應該就了解他實際上使用的Socket編程。通過啟動一個Server端,然后監聽一個端口,再起一個客戶端,客戶端向服務端發送請求,服務端接受到請求后,做出相應的回復并將內容通過網絡傳輸到客戶端,這時候客戶端就可以看到服務端回復的內容。

一個Server端和Client端的通訊Java版本的實現大致是這樣的
Server服務端:

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static final int PORT = 12345;//監聽的端口號   

    public static void main(String[] args) {  
        System.out.println("服務器啟動...\n");  
        Server server = new Server();  
        server.init();  
    }  

    public void init() {  
        try {  
            ServerSocket serverSocket = new ServerSocket(PORT);  
            while (true) {  
                // 一旦有堵塞, 則表示服務器與客戶端獲得了連接  
                Socket client = serverSocket.accept();  
                // 處理這次連接  
                new HandlerThread(client);  
            }  
        } catch (Exception e) {  
            System.out.println("服務器異常: " + e.getMessage());  
        }  
    }  

    private class HandlerThread implements Runnable {  
        private Socket socket;  
        public HandlerThread(Socket client) {  
            socket = client;  
            new Thread(this).start();  
        }  

        public void run() {  
            try {  
                // 讀取客戶端數據  
                DataInputStream input = new DataInputStream(socket.getInputStream());
                String clientInputStr = input.readUTF();//這里要注意和客戶端輸出流的寫方法對應,否則會拋 EOFException
                // 處理客戶端數據  
                System.out.println("客戶端發過來的內容:" + clientInputStr);  

                // 向客戶端回復信息  
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());  
                System.out.print("請輸入:\t");  
                // 發送鍵盤輸入的一行  
                String s = new BufferedReader(new InputStreamReader(System.in)).readLine();  
                out.writeUTF(s);  

                out.close();  
                input.close();  
            } catch (Exception e) {  
                System.out.println("服務器 run 異常: " + e.getMessage());  
            } finally {  
                if (socket != null) {  
                    try {  
                        socket.close();  
                    } catch (Exception e) {  
                        socket = null;  
                        System.out.println("服務端 finally 異常:" + e.getMessage());  
                    }  
                }  
            } 
        }  
    }  
}

Client端

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

public class Client {
    public static final String IP_ADDR = "localhost";//服務器地址 
    public static final int PORT = 12345;//服務器端口號  

    public static void main(String[] args) {  
        System.out.println("客戶端啟動...");  
        System.out.println("當接收到服務器端字符為 \"OK\" 的時候, 客戶端將終止\n"); 
        while (true) {  
            Socket socket = null;
            try {
                //創建一個流套接字并將其連接到指定主機上的指定端口號
                socket = new Socket(IP_ADDR, PORT);  

                //讀取服務器端數據  
                DataInputStream input = new DataInputStream(socket.getInputStream());  
                //向服務器端發送數據  
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());  
                System.out.print("請輸入: \t");  
                String str = new BufferedReader(new InputStreamReader(System.in)).readLine();  
                out.writeUTF(str);  

                String ret = input.readUTF();   
                System.out.println("服務器端返回過來的是: " + ret);  
                // 如接收到 "OK" 則斷開連接  
                if ("OK".equals(ret)) {  
                    System.out.println("客戶端將關閉連接");  
                    Thread.sleep(500);  
                    break;  
                }  

                out.close();
                input.close();
            } catch (Exception e) {
                System.out.println("客戶端異常:" + e.getMessage()); 
            } finally {
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        socket = null; 
                        System.out.println("客戶端 finally 異常:" + e.getMessage()); 
                    }
                }
            }
        }  
    }  
}

從代碼中,我們可以發現,這個Socket通訊的例子中無論是服務端還是客戶端都是放在localhost上的,這個就是典型的單機版本的通訊。
單集版的聊天室實在太簡陋,以至于服務端和客戶端都是你的電腦localhost,很多中型、大型的網站和應用需要有自己的服務端,好比你安裝的QQ或是微信是無法把服務端安裝到你手機上的,你的手機其實只是充當了一個客戶端的角色。下面我們來看看服務化框架是如何從集中式走向分布式的。

拋開單機版的電腦,邁向分布式的服務化

現在一個財務系統中有一塊需要計算每個月的工資,這時候有一個SalaryCalculator類,其中有一個很多方法,我們關心的是其中的一個計算本月實得工資的方法,類如下所示

public class SalaryCalculator{
    public BigDecimal getTotalSalary(BigDecimal baseSalary, BigDecimal performanceSalary) {
        return baseSalary + performanceSalary;
    }
}

現在我們來算一下小王這個月的工資

public static void getSalary() {
    SalaryCalculator salaryCalculato = new SalaryCalculator();
    System.out.println("Mr Wang earn totalSalary: " + salaryCalculator.getTotalSalary(10000 + 199.9))
}

或者我們通過依賴注入的方式直接注入SalaryCalculator,然后調用他的getTotalSalary()方法。

我們太熟悉這樣調用一個類的方法了,但是引入服務化的思想,我們該想想如果現在結算工資的模塊已經抽象成一個服務,單獨打包并部署在一臺tomcat的容器上,這時候我們該如何調用,還是直接new或者注入?
跳出了你的服務端和客戶端二合一的電腦,在分布式的服務化框架下我們壓根就不知道這個結算服務在哪臺機子上,甚至不知道要調用的是哪個方法。

分布式大環境下,我們需要換一種思維

遠程調用區別于本地調用主要多了尋址(找到服務所在地址列表)和Socket通訊(好比上面提到的聊天室)。

客戶端
這時候如果我們還想要調用到這個結算工資的方法,我們需要分為如下幾步:

  • 獲取可用服務地址列表

    List<String> l = getAvailableServiceAddresses("SalaryCalculator.getTotalSalary")
  • 確定要調用服務的目標機器

    String address = chhoseTarget(l)
  • 建立連接

    Socket s = new Socket(address)
  • 請求的序列化

    byte[] request = getRequest(baseSalary, performanceSalary)
  • 接受結果

    byte[] response = new byte[10240];
    s.getInputStream().read(response)
  • 解析結果

    int result = getResult(response);
    return result;

    從以上各個步驟,我們可以看到首先我們需要根據調用的服務名稱來獲取提供服務的機器列表,并進一步確定提供服務的目標機器的信息,如地址端口號等。這個過程就可以簡單的理解為一個路由尋址,找到提供服務的機器的信息。后面就是客戶端建立連接以及通訊的過程了。

服務端
以上是客戶端需要做的操作,那么作為響應和接收并處理請求的服務端需要做些什么,大概分為以下幾步:

  • 接收請求,從請求中拿到一些標識信息,比如服務的名稱,要調用的方法和入參等
  • 定位需要提供的服務,根據客戶端傳來的標識信息,服務端決定在本地如果提供服務
  • 具體響應處理,這時候調用響應的方法并返回結果
  • 傳輸結果,方法調用完成,通過網絡傳回結果數據

細說服務提供方和服務調用方

服務調用方
在分布式的服務框架中,我們不能像集中式的環境中那么隨意,通過new一個對象,從而調用類中的方法屬性等。在分布式環境下,我們要考慮到網絡傳輸,當然就需要序列化和反序列化。除此以外,我們還需要根據一些規則找到我們需要調用的服務,最終完成調用,然后通過網絡傳輸返回結果。

調用服務既然需要規則,那么我們就需要配置規則,大家最為屬性的可能就是類似于Spring中的xml格式的配置了。好比這樣

<bean id = "salaryCalculator" class="com.jackie.ServiceFramework.ConsumerBean">
    <property name = "interfaceName">
        <value>com.jackie.SalaryCalculator</value>
    </propperty>
    <property name = "version">
        <value>1.0.0</value>
    </propperty>
    <property name = "group">
        <value>Salary</value>
    </propperty>
</bean>

這里的ConsumerBean可以認為是一個通用對象,是完成本地和遠程服務的橋梁,具體的配置主要包含了以下幾個屬性:

  • interfaceName 接口名稱,通過該屬性,我們就知道要訪問調用的接口是哪個。上面的ConsumerBean在明確要調用哪個接口后就會生成這個接口的代理,從而再調用具體的方法供本地使用

  • version 版本號,如果你有稍稍接觸這種服務框架,就應該知道我們的服務框架是在不斷完善的,修改了某個模塊后需要對外發布上線,這時候就需要修改版本。對于需要使用新功能的,我們則需要引用新版本的服務,也就是通過這里的version指定

  • group 分組,我們在請求某個接口時,可能該接口部署在不同機器上,我們通過group這個屬性將這些機器分組,那么調用者就可以根據分組名來調用服務了,這么做的好處就是通過分組名將調用者隔離了。其實上面的version屬性也是一種隔離,是將不同版本的服務隔離。

  • 調用方通過這樣的配置企圖找到需要的接口或者方法以供本地調用。那么我們又是如何真正的從調用端走向服務端,中間是如何找到我們在配置文件中指定配置信息對應的服務呢,這里介紹一種方式,見圖

圖中有調用方也有服務方,當然,實際場景中的調用方和服務方的數量并不僅僅是圖中的兩個。那調用方怎么知道提供服務的有幾個,都是誰,這時候我們需要有一個目錄查詢的角色,通過查找服務注冊中心,調用方就知道當前有誰,并如何找到它。當然了,切實到具體選擇那臺機器,那又是負載均衡的事兒了,可以采用的策略如隨機(random)、輪詢(round-robin)或者權重等方式。

服務提供方

上面提到了調用方是如何完成自身配置并通過一些規則和策略找到自己想要的服務。這里我們看看服務提供方通過怎樣的方式對外提供服務。
調用方有自己的配置來表明要調用的服務是長什么樣,對應的,服務提供方自然已有自己的配置來供調用方識別。好比這樣

<bean id = "salaryCalculator" class="com.jackie.ServiceFramework.ProviderBean">
    <property name = "interfaceName">
        <value>com.jackie.SalaryCalculator</value>
    </propperty>
    <property name = "target">
        <ref>salaryCalculatorImpl</ref>
    </propperty>
    <property name = "version">
        <value>1.0.0</value>
    </propperty>
    <property name = "group">
        <value>Salary</value>
    </propperty>
</bean>

這個配置我們已經很熟悉了,相較于調用方的配置,我們一眼就發現這里多了個target屬性。這個屬性主要是告訴調用方具體要調用的實現類是哪個。另外,還有一個不同就是這里的ConsumerBean變成了ProviderBean,這個主要職責是將自己的服務注冊到上圖中的服務注冊查找中心,這樣就是告訴別人我能提供什么服務,可以通過什么方式找到我。

十分鐘到了,你了解了么

switch(your status) {
    case: get it
        give like;
        break;
    case: ambiguous
        share your question under comment area;
        break;
    case: still no idea about it
        repeat read from head and refer to other guides
        break;
    default:
        you win!!!
        break;
}

參考文獻:《大型網站系統與Java中間件實現》


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()