1. 程式人生 > >分散式服務框架學習筆記1 應用架構演進

分散式服務框架學習筆記1 應用架構演進

傳統垂直應用架構

業界曾比較流行的有:
LAMP架構:Linux+Apache+PHP(前後端分離)+MySQL(讀寫分離)
MVC架構:Spring+Struts+iBatis/Hibernate+Tomcat

在高併發、大流量的應用場景中,需要做叢集,通常的組網方案是前端通過F5等負載均衡器做七層負載均衡(或者使用SLB等軟負載),後端做對等叢集部署。

隨著業務的不斷髮展,應用規模日趨龐大,傳統垂直架構開發模式的弊端變得越來越突出。這就需要將核心業務抽取出來,作為獨立的服務,逐漸形成穩定的服務中心,使用前端應用能更快速地響應多變的市場市場需求。
同時將公用能力API抽取出來,作為獨立的公共服務供其他呼叫者消費,以實現服務的共享和重用,降低開發和運維成本。應用拆分之後會按照模組獨立部署,介面呼叫由本地API演進成跨程序的遠端方法呼叫,此時RPC框架應運而生。

RPC架構

RPC(Remote Procedure Call),它是一種程序間通訊方式。允許像呼叫本地服務一樣呼叫遠端服務,它的具體實現方式可以不同,如Sprign的HTTP Invoker,Facebook的Thrift二進位制私有協議通訊。
RPC由20世紀80年代Bruce Jay Nelson提出。

RPC框架原理

RPC框架的目標就是讓遠端過程(服務)呼叫更加簡單、透明,RPC框架負責遮蔽底層的傳輸方式(TCP或者UDP)、序列化方式(XML/JSON/二進位制)和通訊細節。框架使和者只需要瞭解誰在什麼位置提供了什麼樣的遠端服務介面即可,開發者不需要關心底層通訊細節和呼叫過程。
這裡寫圖片描述

RPC框架實現的幾個核心技術點總結如下:
1. 遠端服務提供者需要以某種形式提供服務呼叫相關的資訊,包括但不限於服務介面定義、資料結構,或者中間太空的服務定義檔案,如:Thrift的IDL檔案、WS-RPC的WSDL檔案定義,甚至也可以是服務端的介面說明文件;服務呼叫者需要通過一定的途徑獲取遠端服務呼叫相關資訊,例如服務端介面定義Jar包匯入,獲取服務端的IDL檔案等
2. 遠端代理物件:服務呼叫者呼叫的服務實際是遠端服務的本地代理,對於Java語言,它的實現就是JDK的動態代理,通過動態代理的攔截機制,將本地呼叫封裝成遠端服務呼叫
3. 通訊:RPC框架與具體的協議無關,如Spring的遠端呼叫支援HTTP Invoke、RMI Invoke,MessagePack使用的是私有的二進位制壓縮協議
4. 序列化:遠端通訊,需要將物件轉換成二進位制碼流進行傳輸,不同的序列化框架,支援的資料型別、資料包大小、異常型別及效能等都不同。不同的RPC框架應用場景不同,因此技術選擇也會存在很大差異。一些做得比較好的RPC框架,可以支援多種序列化方式,有的甚至支援使用者自定義序列化框架(hadoop avro)

最簡單 RPC 框架實現

下面通過Java原生的序列化、Socket通訊、動態代理和反射機制,實現最簡單 RPC 框架。它由三部分組成:
1. 服務提供者:執行在服務端,負責提供服務介面定義和服務實現類
2. 服務釋出者:執行在RPC服務端,負責將本地服務釋出成遠端服務,供其他消費者呼叫
3. 本地服務代理:執行在RPC客戶央,通過代理呼叫遠端服務提供者,然後將結果進行封裝返回給本地消費者

程式碼架構:
這裡寫圖片描述

原始碼:

服務端介面定義和實現

EchoService

public interface EchoService {
    String echo(String ping);
}

EchoServiceImpl

public class EchoServiceImpl implements EchoService{

    @Override
    public String echo(String ping) {

        return ping != null ? ping + " --> I am ok.":"I am ok.";
    }

}

RPC服務端 服務釋出者

RpcExporter

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * RPC 服務端服務釋出者
 * @author hs26661
 *
 */
public class RpcExporter {
    static Executor executor = Executors.newFixedThreadPool(Runtime
            .getRuntime().availableProcessors());

    public static void exporter(String hostName, int port) throws Exception {
        ServerSocket server = new ServerSocket();
        server.bind(new InetSocketAddress(hostName, port));  //監聽 客戶端的TCP連線
        try {
            while (true) {
                executor.execute(new ExporterTask(server.accept()));
            }
        } finally {
            server.close();
        }
    }

    private static class ExporterTask implements Runnable {
        Socket client = null;

        public ExporterTask(Socket client) {
            this.client = client;
        }

        @Override
        public void run() {
            ObjectInputStream input = null;
            ObjectOutputStream output = null;

            try {
                input = new ObjectInputStream(client.getInputStream());
                String interfaceName = input.readUTF();  //客戶端傳送的碼流
                Class<?> service = Class.forName(interfaceName); //反序列化為物件
                String methodName = input.readUTF();
                Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
                Object[] arguments = (Object[]) input.readObject();
                Method method = service.getMethod(methodName, parameterTypes);
                Object result = method.invoke(service.newInstance(), arguments);   //呼叫服務端方法
                output = new ObjectOutputStream(client.getOutputStream());
                output.writeObject(result);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (output != null)

                    try {
                        output.close(); //呼叫完成,釋放物件
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                if (input != null)
                    try {
                        input.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
            }
            if (client != null)
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    }
}

RPC客戶端本地服務代理

RPCImporter

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.Socket;

public class RpcImporter<S> {
    @SuppressWarnings("unchecked")
    public S importer(final Class<?> serviceClass,final InetSocketAddress addr){
        return (S) Proxy.newProxyInstance(serviceClass.getClassLoader(), new Class<?>[]{serviceClass.getInterfaces()[0]},
        new InvocationHandler(){
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {

                Socket socket=null;   //建立socket 客戶端,根據指定地址連線遠端服務提供者
                ObjectOutputStream output = null;
                ObjectInputStream input=null;
                try
                {
                    socket = new Socket();
                    socket.connect(addr);
                    output = new ObjectOutputStream(socket.getOutputStream());
                    output.writeUTF(serviceClass.getName());
                    output.writeUTF(method.getName());
                    output.writeObject(method.getParameterTypes());
                    output.writeObject(args);   //把遠端服務呼叫者需要的介面類、方法名、引數列表等編碼後傳送給服務提供者
                    input=new ObjectInputStream(socket.getInputStream());    //同步阻塞等待服務端返回應答,獲取應答之後返回
                    return input.readObject();    
                }
                finally
                {
                    if(socket!=null)socket.close();
                    if(output!=null)output.close();
                    if(input!=null)input.close();
                }
            }
        }); 
    }
}

測試程式碼

import java.net.InetSocketAddress;

public class RpcClient {

    public static void main(String[] args) {
        new Thread(new Runnable(){

            @Override
            public void run() {
                try{
                    RpcExporter.exporter("localhost",8088);
                }
                catch(Exception e){
                    e.printStackTrace();
                }

            }

        }).start();
        RpcImporter<EchoService> importer=new RpcImporter<EchoService>();
        EchoService echo = importer.importer(EchoServiceImpl.class, new InetSocketAddress("localhost",8088));
        System.out.println(echo.echo("Are you ok?"));
    }

}

執行過程

Created with Raphaël 2.1.0建立非同步釋出服務端的執行緒並啟動,接收RPC客戶端的請求根據請求引數呼叫服務實現類,返回結果給客戶端建立客戶端服務代理類,構造RPC請求引數,發起RPC呼叫將呼叫結果輸出到控制檯

執行結果:
這裡寫圖片描述

業界主流的 RPC 框架

  • Facebook開發的遠端服務呼叫框架 Apache Thrift
  • Haoop的子專案 Avro-RPC
  • caucho 提供的基於biary-RPC實現的遠端通訊框架Hession

RPC 框架面臨的挑戰

當服務越來越多時,服務URL配置管理變得非常困難,F5等硬體負載均衡器的單點壓力也越來越大。此時需要一個服務註冊中心,動態地註冊和發現服務,使服務的位置透明。消費者在本地快取服務提供者列表,實現軟負載均衡,這可以降低對F5等硬體負載均衡器的依賴,也能降低硬體成本。

隨著業務的發展,服務間依賴關係變得錯綜複雜,甚至分不清哪個應用要在哪個應用之前啟動,架構師不能完整地描述應用之間的呼叫關係。需要一個分散式訊息跟蹤系統視覺化展示服務呼叫鏈,用於依賴分析、業務呼叫路徑梳理等,幫助架構師清理不合理的服務依賴,防止業務服務架構腐化。
服務的呼叫量越來越大,服務的容量問題就暴露出來,某個服務需要多少機器支撐、什麼時候該加機器?為了解決容量規劃問題,需要採集服務呼叫KPI資料,進行彙總和分析,通過計算得出服務部署例項數和伺服器的配置規格。
服務上線前的審批、下線通知,需要統一的服務生命週期管理流程進行管控。不同的服務安全許可權不同,需要保證敏感服務不被誤呼叫,制定服務的安全策略。
服務化之後,隨之而來的就是服務治理問題,單憑RPC框架無法解決服務治理問題。

SOA 服務化架構

SOA是一種粗粒度、鬆耦合的以服務為中心的架構,介面之間通過定義明確的協議和介面進行通訊。SOA幫助工程師站在一個新的高度理解企業級架構中各種元件的開發和部署形式,可以幫助企業系統架構以更迅速、可靠和可重用的形式規劃整個業務系統。相比傳統的非服務化架構,SOA能夠更加從容地應對複雜企業系統整合和需要的快速變化。

SOA 面向服務的一般原則總結如下

  • 服務可複用
  • 服務共享一個標準契約
  • 服務是鬆耦合的
  • 服務是底層邏輯的抽象
  • 服務是可組合、可編排的
  • 服務是可自治的
  • 服務是無狀態的
  • 服務是可被自動發現的

服務治理

  • 服務定義
  • 服務生命週期管理
  • 服務版本治理
  • 服務註冊中心
  • 服務監控
  • 執行期服務質量保障
  • 快速的故障定界定位手段
  • 服務安全

微服務架構(MSA)

主要特徵如下:
- 原子服務
- 高密度部署
- 敏捷交付
- 微自治

本系統文章來自《分散式服務框架 原理與實踐》中國工信出版集團 電子工業出版社 李林鋒/著