1. 程式人生 > >Day13.高效能RPC設計 學習筆記1

Day13.高效能RPC設計 學習筆記1

一、引言

  1. 系統架構演變
    隨著網際網路的發展,網站應用的規模不斷擴大,常規的垂直應用(MVC)架構已無法應對,分散式服務架構以及流動計算架構(伸縮性)勢在必行,亟需一個治理系統確保架構有條不紊的演進。
    圖01
  • 單一架構:例如早期servlet/jsp - ORM(物件關係對映) Hibernate|MyBatis
  • 垂直架構:將一個應用分層,實現協同開發,便於後期專案升級維護 - MVC Struts1/2|SpringMVC
  • 分散式服務:提升服務整體效能,將一些模組獨立出去(SOA service orented architecture),獨立執行,隨著業務發展,不可避免需要排程第三方服務(物流資訊、第三支付)。RPC
    - Remote Process Call (即時訊息) webservice、Dubbo、SpringCloud
  • 流動式計算架構:要求服務提供方能夠根據負載情況,動態增加或者減少。同時實現服務視覺化,簡化服務運維,提升系統資源的利用率。 - SOA治理框架 微服務 - 對服務進行集中管理調控。
  1. 高併發網際網路設計原則 —— 高負載高儲存高容錯 也稱 AKF拆分原則
    X軸:服務水平擴充套件
    服務具有可複製性 ,水平分割 ,解決一些併發問題
    (水平拓展,比效能的垂直提升成本低-摩爾定律)
    Y軸:服務間各司其職
    弱耦合,垂直分割,物理節點物盡其用
    (泳道設計)
    Z軸

    :對x,y打包,進行物理隔離。
    X中有Y,Y中有X x和y互相轉換
    y中有x:對y軸做垂直拆分,層與層間獨立,為了保證每一層的可用性,每一層做水平分割

  2. 高併發的場景問題 —— 優化思路
    如何讓服務支援x軸的水平擴充套件,利用分散式的思維解決高併發;如何做y中的垂直拆分,是應用之間儘可能隔離,弱化服務件耦合度;結合兩個維度來解決高併發的場景問題。(Socket + IO)

二、網路基礎(TCP/IP)

  • 網路程式設計:通過編碼的方式,讓不同計算機之間相互通訊(資料的傳遞)。

  • 兩個核心問題:
    定址問題:ip地址+port埠號
    協議問題:資料傳輸格式

  • OSI 七層模型(標準,瞭解就好)
    圖02

  • OSI 五層模型(七層的轉變)

  • 協議
    http協議:應用層協議,超文字傳輸協議,不同計算機間傳輸文字的協議(字串),底層tcp協議
    tcp/ip協議:傳輸層協議

tcp協議:傳輸安全,效率低
udp協議:傳輸不安全,效率高

  • 瞭解
    應用層(Http協議:WebService,tomcat協議)——普通程式設計師
    傳輸層(TCP/IP UDP:基於tcp/ip封裝的ftp、smtp/pop3、websocket…定製協議rpc協議)——架構程式設計師 也是程式碼最終觸及到的地方
    物理層(嵌入式開發)

三、傳統 BIO 網路程式設計模型(TCP/IP)

  1. IO模型分類
  • BIO 同步阻塞IO
    傳統IO 或者 Blocking IO
    特點:面向流Input | Output
    【每個執行緒只能處理一個channel(同步的,該執行緒和該channel繫結)。
    執行緒發起IO請求,不管核心是否準備好IO操作,從發起請求起,執行緒一直阻塞,直到操作完成。】

  • NIO 同步非阻塞IO
    New IO 或者Non Blocking IO
    特點:面向緩衝區Buffer(基於通道)
    【每個執行緒可以處理多個channel(非同步)。
    執行緒發起IO請求,立即返回;核心在做好IO操作的準備之後,通過呼叫註冊的回撥函式通知執行緒做IO操作,執行緒開始阻塞,直到操作完成 。】

  • AIO(Async Non Blocking IO) 非同步非阻塞
    【執行緒發起IO請求,立即返回;記憶體做好IO操作的準備之後,做IO操作,直到操作完成或者失敗,通過呼叫註冊的回撥函式通知執行緒做IO操作完成或者失敗 。】

  1. BIO
    方向—— 輸入、輸出流(InputStream | OutputStream)
    型別—— 位元組、字元流(Reader | Writer)
    功能—— 節點、過濾流(BufferedInputStream | BufferedOutputStream )

  2. BIO網路程式設計

  • 服務端:ServerSocket 【接收和響應,請求轉發 - ServerSocket請求響應- Socket
    ①初始化伺服器ServerSocket,繫結監聽埠
    ②等待客戶端連線serverSocket.accept();
    ③處理請求/響應sockect.getInputStream(); / socket.getOutputStream();
    ④關閉資源

  • 客戶端:Socket 【傳送和接收,傳送請求|接收響應:Socket
    ①初始化客戶端Socket,繫結伺服器IP/埠
    ②發起請求/獲取響應socket.getOutputStream(); / socket.getInputStream();
    ③關閉資源

    socket程式設計圖

  1. BIO程式碼
  • Server
public class BIOBootstrapServer {
    public static void main(String[] args) throws IOException {
        server();
    }
    public static void server() throws IOException {
        //建立服務轉發ServerSocket
        ServerSocket ss = new ServerSocket();
        ss.bind(new InetSocketAddress(9999)); //server可以省略好hostname

        ExecutorService threadPool= Executors.newFixedThreadPool(10);
        while(true) {
            //等待請求到來,轉發產生Socket(系統內部資源)
            final Socket socket = ss.accept(); //沒有請求就等待,阻塞
            //final 匿名內部類使用區域性變數要final修飾
            threadPool.submit(new Runnable() {
                public void run() {
                    try {
                        //利用IO,讀取使用者請求
                        InputStream req = socket.getInputStream();
                        InputStreamReader isr = new InputStreamReader(req);
                        BufferedReader br = new BufferedReader(isr);

                        String line = null;
                        StringBuilder sb = new StringBuilder();
                        while ((line = br.readLine()) != null) {
                            sb.append(line);
                        }
                        System.out.println("伺服器收到:" + sb.toString());
                        //給出使用者響應
                        OutputStream res = socket.getOutputStream();
                        PrintWriter pw = new PrintWriter(res);
                        pw.println(new Date().toLocaleString());
                        pw.flush();
                        socket.shutdownOutput(); //告知客戶端寫結束
                        //關閉系統資源
                        socket.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
  • Client
public class BIOBootstrapClient {
    public static void main(String[] args) throws IOException {
        send("這是一條來自client 的 message!");
    }
    public static void send(String msg) throws IOException {
        //建立Socket物件
        Socket s = new Socket();
        s.connect(new InetSocketAddress("127.0.0.1",9999)); //所以幾乎給ip和埠的,底層走的就是socket

        //傳送請求
        OutputStream req = s.getOutputStream();
        PrintWriter pw=new PrintWriter(req);
        pw.println(msg);
        pw.flush();
        s.shutdownOutput();//告知服務端寫結束

        //接收響應
        InputStream res = s.getInputStream();
        InputStreamReader isr = new InputStreamReader(res);
        BufferedReader br = new BufferedReader(isr);
        String line=null;
        StringBuilder sb=new StringBuilder();
        while((line=br.readLine())!=null){
            sb.append(line);
        }
        System.out.println("客戶端收到:"+sb.toString());
        //關閉資源
        s.close();
    }
}

思考下,BIO模型的缺點?
accept轉發產生socket資源,一次請求轉發等待請求響應的過程有阻塞。main執行緒作accept請求轉發、子執行緒作socket讀寫IO處理。

  • 機器間通訊是靠傳輸 套接字 完成的。

套接字,是支援TCP/IP的網路通訊的基本操作單元,可以看做是不同主機之間的程序進行雙向通訊的端點,簡單的說就是通訊的兩方的一種約定,用套接字中的相關函式來完成通訊過程。
簡單的舉例說明下:Socket = Ip address + TCP/UDP + port。

每個socket代表一個系統的隨機埠,用來在服務和客戶端之間建立連線。
在這裡插入圖片描述

  1. BIO網路程式設計問題
    通過將相應處理和轉發進行分離,但是在轉發的時候,伺服器是通過先開啟執行緒資源(計算),然後在計算中動用IO操作,IO存在匯流排佔用問題,導致當前執行緒操作的IO資源沒有就緒,導致系統計算資源的浪費。如果要解決該問題,必須先考慮讓IO就緒,然後在開啟執行緒處理或者理解為讓執行緒去處理一些非阻塞的IO操作。

BIO程式設計詬病 :多執行緒模型解決服務端併發,但是無法判斷當前處理的IO狀態是否就緒,此時就會導致執行緒在做無畏等待,導致系統資源利用率不高。 先開執行緒 -> 線上程等待IO就緒->處理IO->執行緒資源釋放

四、NIO程式設計模型

  • Channel(FileChannel-讀寫檔案、ServerSocketChannel-請求轉發、SocketChannel-響應處理)
  • ByteBuffer (位元組緩衝區)
  1. 建立FileChannel
//可讀的FileChannel
FileChannel fr=new FileInputStream("xxx路徑").getChannel();
//可寫的FileChannel
FileChannel fw=new FileOutputStream("xxx路徑").getChannel();

檔案操作

//讀檔案件
fr.read(緩衝區)
//寫檔案
fw.write(緩衝區)    
  1. 位元組緩衝區
    圖03

flip:pos賦值給limit,post歸0
clear:恢復變數為初始狀態

  1. 一個基於NIO的檔案拷貝【面試&筆試】
    【NIO沒有流的概念,用通道來處理。】
public class FileCopyDemo {
    public static void main(String[] args) throws IOException {
        //建立讀通道 磁碟讀入資料 到 ByteBuffer
        FileInputStream in = new FileInputStream("C:\\Users\\Administrator\\Desktop\\123.txt");
        FileChannel fcr = in.getChannel(); //fileChannelRead

        //建立寫通道 將 ByteBuffer 資料寫入磁碟
        FileOutputStream out = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\321.txt");
        FileChannel fcw= out.getChannel(); //fileChannelWrite

        //建立ByteBuffer
        ByteBuffer buffer = ByteBuffer.wrap(new byte[1024]);//.allocate()也可以

        while (true){
            buffer.clear();
            int n = fcr.read(buffer);
            if(n==-1) break;
            buffer.flip();
            fcw.write(buffer);
        }
        //關閉資源
        fcr.close();
        fcw.close();
    }
}
  1. “掛羊頭,賣狗肉” —— NIO的API,BIO的程式設計模型
public class NIOBootstrapServer_Fake {
    public static void main(String[] args) throws IOException {
        server();
    }
    public static void server() throws IOException {
        //建立服務轉發ServerSocket
        ServerSocketChannel ss = ServerSocketChannel.open();
        ss.bind(new InetSocketAddress(9999));
        ExecutorService threadPool= Executors.newFixedThreadPool(10);
        while(true) {
            //等待請求到來,轉發產生Socket(系統內部資源)
            final SocketChannel socket = ss.accept();// 阻塞
            threadPool.submit(new Runnable() {
                public void run() {
                    try{
                        //讀取客戶端響應
                        ByteBuffer buffer=ByteBuffer.allocate(1024);

                        ByteArrayOutputStream baos=new ByteArrayOutputStream();//類似stringbuilder,用來暫存用

                        while (true){
                            buffer.clear();
                            int n = socket.read(buffer);
                            if(n==-1) break;
                            buffer.flip();
                            baos.write(buffer.array(),0,n);
                        }
                        System.out.println("伺服器收到:"+new String(baos.toByteArray()));

                        //給出客戶端響應

                        ByteBuffer respBuffer = ByteBuffer.wrap((new Date().toLocaleString()).getBytes());
                        socket.write(respBuffer);

                        socket.shutdownOutput();//告知客戶端寫結束
                        //關閉系統資源
                        socket.close();
                    }catch (Exception e){

                    }
                }
            });
        }
    }
}
  1. 通道選擇器
    圖
    在這裡插入圖片描述

NIO核心思想,將網路程式設計中所有操作(轉發,讀請求,寫響應)封裝成事件Event,之後用事件形式通知程式進行計算,事件發生則通知程式,這樣處理的IO都是就緒的IO,保證計算資源的充分利用。這時候,引入通道選擇器的概念。
我們需要把通道註冊到通道選擇器的註冊列表中,由通道選擇器維護註冊列表;一旦我們所關注的事件發生了,他會把關注的事件封裝到事件處理列表當中;為了防止處理空或無效IO,每個事件處理結束後,要從列表中移除;移除不代表取消註冊。只有兩種可能才取消註冊,一種是通道關閉,一種是主動登出。