1. 程式人生 > >《深入分析Java Web技術內幕》讀後感之2- JAVA I/O NIO

《深入分析Java Web技術內幕》讀後感之2- JAVA I/O NIO

一、Java I/O的基本架構

Java的I/O操作類在java.io包下,大概有80多個類,這些類可以分成以下4組:

▶ 基於位元組操作的I/O介面:InputStream和OutputStream

▶ 基於字元操作的I/O介面:Reader和Writer

▶ 基於磁碟操作的I/O介面:File

▶ 基於網路操作的I/O介面:Socket

前兩組主要是傳輸資料的資料格式不同,後兩組主要是傳輸資料的傳輸方式不同,資料格式和傳輸方式都是影響I/O操作傳輸效率的關鍵因素。

二、磁碟I/O的工作機制

        讀取和寫入I/O操作都呼叫作業系統提供的介面,因為磁碟裝置是由作業系統管理的,應用程式要訪問物理磁碟只能通過作業系統呼叫的方式來工作。其中讀和寫分別對應read()和write()兩個系統呼叫,而只要是系統呼叫就可能存在核心空間地址和使用者空間地址切換的問題,這是由於作業系統為了保護系統本身的執行安全,而將核心程式執行使用的記憶體空間和使用者程式執行使用的記憶體空間進行隔離造成的。但是這樣雖然保證了核心程式執行的安全性,但是也必然存在資料可能需要從核心空間向用戶空間複製的問題。如果遇到非常耗時的操作,比如磁碟I/O,資料從磁碟複製到核心空間,然後又從核心空間複製到使用者空間,將會非常緩慢。這時作業系統為了加速I/O訪問,在核心空間使用了快取機制,也就是將從磁碟讀取的檔案按照一定的組織方式進行快取,如果使用者程式訪問的是同一段磁碟地址的空間資料,那麼作業系統將從核心快取中直接取出並返回給使用者程式,這樣可以減少I/O的響應時間。

1、訪問檔案的幾種方式

① 標準方式的訪問

        讀取的時候,當應用程式呼叫read()介面時,作業系統檢查在核心的快取記憶體中有沒有需要的資料,如果已經有快取了,就直接從快取中返回,如果沒有,則從磁碟中讀取,然後快取在作業系統的快取中。

        寫入的時候,使用者的應用程式呼叫write()介面將資料從使用者地址空間複製到核心地址空間的快取中,這時對使用者程式來說寫操作就已經完成,至於什麼時候再寫到磁碟中由作業系統來決定,除非顯式的呼叫了sync同步命令。

② 直接I/O的方式

        所謂直接I/O就是應用程式直接訪問磁碟資料,而不經過作業系統的核心資料快取區,這樣做的目的就是減少一次從核心快取區到應用程式快取的資料複製,但是直接I/O也有負面影響,如果訪問的資料不在應用程式快取中,那麼每次資料都會直接從磁碟進行載入,這種直接載入會非常緩慢。通常直接I/O和非同步I/O結合使用,會有比較好的效能。

③ 同步訪問檔案的方式

        它是指資料的讀取和寫入都是同步操作的,它與標準訪問檔案方式不同的是,只有當資料成功寫入到磁碟時才返回給應用程式成功的標誌,這種訪問方式的效能比較差,只有對資料安全性要求比較高的場景中才會使用。通常這種操作方式的硬體都是定製的。

④ 非同步訪問檔案的方式

        這種方式是當資料的執行緒發出請求之後,執行緒會接著去處理其他的事情,而不是阻塞等待,當請求的資料返回後繼續處理下面的操作,這種方式可以明顯的提高應用程式的效率,但是不會改變訪問檔案的效率。

⑤ 記憶體對映的方式

        它是指作業系統將記憶體中的某一塊區域與磁碟中的檔案關聯起來,當要訪問記憶體中的一段資料時,轉換為訪問檔案的某一段資料,這種方式的目的同樣是減少資料從核心空間快取到使用者空間快取的資料複製操作,因為這兩個空間的資料是共享的。

2、Java訪問磁碟檔案

        檔案是資料在磁碟中的唯一最小描述,檔案也是作業系統和磁碟驅動器互動的最小單元,上層應用程式只能通過檔案來操作磁碟上的資料。在Java中通常的File並不代表一個真實存在的檔案物件,當你指定一個路徑描述符時,它就會返回一個代表這個路徑的虛擬物件,這可能是一個真實存在的檔案或者是一個包含多個檔案的目錄。當真正要讀取這個檔案時,如FileInputStream類就是一個操作檔案的類,當在例項化一個FileInputStream物件時,就會建立一個FileDescriptor物件,這個物件就是代表一個真正存在檔案的描述物件,當我們在操作一個檔案物件時就可以通過getFD()方法獲取真正操作的與底層作業系統相關聯的描述,例如可以呼叫FileDescriptor.sync()方法將作業系統快取中的資料強制重新整理到物理磁碟中。

3、Java的序列化技術

        Java序列化就是將一個物件轉化成一個二進位制表示的位元組陣列,通過儲存或轉移這些位元組陣列來達到資料持久化的目的,需要持久化的類必須繼承java.io.Serializable介面。反序列化則是相反的過程,將這個位元組陣列再重新構造成物件。但是反序列化時,必須有原始類作為模板,才能將這個物件還原。

import java.io.*;
 
public class SerializeTest implements Serializable {
    private static final long serialVersionUID = 1L;
 
    private int num = 1390;
 
    public static void main(String[] args) throws Exception {
        //序列化
        FileOutputStream fos = new FileOutputStream("E:\\Learn\\Test\\深入JavaWeb技術內幕\\JavaIOTest\\SerializeTest.dat");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        SerializeTest st = new SerializeTest();
        oos.writeObject(st);
        oos.flush();
        oos.close();
        
        //反序列化
        FileInputStream fis = new FileInputStream("E:\\Learn\\Test\\深入JavaWeb技術內幕\\JavaIOTest\\SerializeTest.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);
        SerializeTest st1 = (SerializeTest) ois.readObject();
        System.out.println(st1.num);
        ois.close();
    }
}

當Java的序列化遇到一些複雜情況時的說明:

① 當父類繼承Serializable介面時,所有的子類都可以被序列化;

② 子類實現了Serializable介面,父類沒有實現,父類中的屬性不能被序列化(不報錯,但資料會丟失),子類中的屬性仍能正確序列化;

③ 如果序列化的屬性是物件,則這個物件也必須實現Serizliazble介面,否則會報錯;

④ 在反序列化時,如果物件的屬性有修改或刪減,則修改的部分屬性會丟失,但不會報錯;

⑤ 在反序列化時,如果serialVersionUID被修改,則反序列化時會失敗。

三、網路I/O的工作機制

1、TCP狀態轉化圖

2、TCP的三次握手

        TCP在傳輸時,連線一方的A由作業系統動態選取一個32位長的序列號(Initial Sequence Number),假設A的初始序列號為1000,以該序列號為原點,對自己將要傳送的每個位元組的資料進行編號,1001,1002,1003......並把自己的初始序列號ISN告訴B,讓B知道什麼樣編號的資料是合法的,什麼編號是非法的,比如900就是非法的,同時B可以對A傳送的每一個位元組的資料進行確認,如果A收到B的確認編號為2001,則意味著編號為1001-2000的位元組,共1000個位元組已經安全到達。同理,B也是類似的操作,假設B的初始序列號ISN為2000,以該序列號為原點,對自己將要傳送的每個位元組資料進行編號,2001,2002,2003......並把自己的初始序列號ISN告訴A,以便A可以確認B傳送的每一個位元組,然後,如果B收到A確認編號為4001,則意味著位元組編號為2001-4000,共2000個位元組已經安全到達。所以,由此可以得知,TCP三次握手,握的是通訊雙方資料原點的序列號。

四次握手的過程如下:

① A傳送同步訊號SYN(Synchronization)+A的ISN給B;

② B確認收到A的同步訊號,並記錄A的ISN到本地,命名為B的ACK(Acknowledgement);

③ B傳送同步訊號SYN+B的ISN給A;

④ A確認到B的同步訊號,並記錄B的ISN到本地,命名為A的ACK。

很顯然2和3的這兩個步驟可以合併,只需要三次握手,可以提高連線的速度與效率。

補充:

① 第一個包,即A發給B的SYN中途丟失,沒有到達B

A為週期性超時重傳,直到收到B的確認;

② 第二個包,即B傳送給A的SYN+ACK中途丟失,沒有到達A

B會週期性超時重傳,直到收到A的確認;

③ 第三個包,即A傳送給B的ACK中途丟失,沒有到達B

A發完ACK單方面認為TCP為Established狀態,而B顯然認為TCP為Active狀態

a.假定此時雙方都沒有資料傳送,B會週期性超時重傳,直到收到A的確認,收到之後B的TCP連線狀態也為Established,此時雙向可以發包;

b.假定此時A有資料傳送,B收到A的資料+ACK,自然會切換到Established狀態,並接受A的資料;

c.假定此時B有資料傳送,但現在傳送不了,B會週期性的超時重傳SYN+ACK,直到收到A的確認才可以傳送資料。

參考於:https://www.zhihu.com/question/24853633

3、影響網路傳輸的因素

將一份資料從一個地方正確的傳輸到另外一個地方所需要的時間就被稱之為響應時間,影響響應時間的因素有如下幾點:

① 網路頻寬:它是指一條物理鏈路在1s內能夠傳輸的最大位元數(注意是位元不是位元組,一個位元組8bit);

② 傳輸距離:也就是資料在光纖中要走的距離,雖然光的轉播速度很快,但由於資料在光纖中的移動並不是走直線的,會有一個折射率,大概是光的2/3,這個消耗的時間也就是通常我們所說的傳輸延時;

③ TCP擁塞控制:TCP傳輸是一個“停-等-停-等”的協議,傳輸方和接受方的步調要一致,要達到步調一致就要通過擁塞控制來調節,TCP在傳輸時會設定一個“視窗”,這個“視窗”的大小是由頻寬和RTT(響應時間)來決定的,計算的公式是頻寬(b/s)*RTT(s),通過這個值可以得出理論上最優的TCP緩衝區的大小。

4、Java Socket的工作機制

        Socket這個概念並沒有對應到一個具體的實體,它描述的是計算機之間完成相互通訊的一種抽象功能。大部分情況下,我們使用的都是基於TCP/IP的流Socket,它是一種穩定的通訊協議。兩個主機之間的應用程式通訊,必須通過Socket建立連線,而建立Socket連線必須由底層TCP/IP來建立TCP連線,建立TCP連線需要底層IP來定址網路中的主機。如果是同一臺主機上的應用程式通訊就需要通過埠號來指定,這樣就可以通過一個Socket例項來唯一代表一個主機上的應用程式的通訊鏈路。

5、建立通訊鏈路

        當客戶端要與服務端通訊時,客戶端首先要建立一個Socket例項,作業系統將為這個Socket例項分配一個沒有被使用的本地埠號,並建立一個包含本地地址、遠端地址和埠號的套接字資料結構,這個資料結構將會一直儲存在系統中,直到這個連線關閉。(在Socket例項被正確返回之前,將會先進行TCP的3次握手協議,握手協議完成後,Socket例項才會建立完成,否則將會丟擲IOException錯誤)

        與之對應的服務端將建立一個ServerSocket例項,這時作業系統也會為ServerSocket例項建立一個底層資料結構,在這個資料結構中包含指定監聽的埠號和包含監聽地址的萬用字元,通常情況下都是“*”,即監聽所有地址,然後呼叫accept()方法進行阻塞狀態,等待客戶端發起請求。當一個新的請求到來時,將為這個連線建立一個新的套接字資料結構,該套接字包含的地址資訊和埠資訊正在請求的源地址和埠。這個新建立的資料結構將會關聯到ServerSocket例項的一個未完成的連線資料結構列表中,這時服務端與之對應的Socket例項並沒有完成建立,而是要等與客戶端的三次握手完成之後,這個服務端的Socket例項才會返回,並將這個Socket例項對應的資料結構從未完成列表中移到已完成列表中,所以與ServerSocket所關聯的列表中每個資料結構都代表與一個客戶端建立的TCP連線。

客戶端示例:

import java.net.Socket;
 
public class ApplicationATest {
    public static void main(String[] args) throws Exception {
        //客戶端
        System.out.println("開始建立客戶端Socket例項......");
        Socket s1=new Socket("192.168.188.1",9001);//IP地址是你自己主機的地址,dos下輸入ipconfig檢視,埠號與服務端保持一致
        if(s1.isConnected()){
            System.out.println("成功與服務端建立遠端連線");
            System.out.println("服務端的遠端地址是:"+s1.getRemoteSocketAddress());
            s1.close();
            System.out.println("連線關閉");
        }
    }
}

服務端示例:

import java.net.ServerSocket;
import java.net.Socket;
 
public class ApplicationBTest {
    public static void main(String[] args) throws Exception {
        //服務端
        System.out.println("開始建立服務端Socket例項......");
        ServerSocket serverSocket = new ServerSocket(9001);//埠號自定義
        System.out.println("開始進入阻塞狀態,正在等待客戶端發起請求......");
        Socket s2 = serverSocket.accept();
        if (s2.isConnected()) {
            System.out.println("成功與客戶端建立連線");
            System.out.println("客戶端的遠端地址是:" + s2.getRemoteSocketAddress());
            s2.close();
            System.out.println("連線關閉");
        }
    }
}

6、資料傳輸

        資料傳輸是我們建立連線的目的,當連線建立成功時,服務端和客戶端都會擁有一個Socket例項,每個例項都有一個InputStream和OutputStream,我們可以通過這兩個物件來交換資料。網路I/O都是以位元組流傳輸的,當建立Socket物件時,作業系統將會為InputStream和OutputStream分配一定大小的快取區,資料的寫入和讀取都是通過這個快取區來完成的,寫入端將資料寫到OutputStream對應的SendQ佇列中,當資料填滿時,資料將被轉移到另一端InputStream的RecvQ佇列中,如果這時RecvQ已經滿了,那麼OutputStream的write()方法將會阻塞,直到RecvQ佇列有足夠的空間容納SendQ傳送的資料。要注意的是,這個快取區的大小及寫入端的速度和讀取端的速度非常影響這個連線的資料傳輸效率,由於可能發生阻塞,所以網路I/O和磁碟I/O不同的是資料的寫入和讀取還要有一個協調的過程,如果兩邊同時傳送資料可能會產生死鎖。

四、NIO的工作方式

可參考連結: 攻破JAVA NIO技術壁壘

1、NIO的工作機制

        Channel和Selector它們是NIO的兩個核心概念,Channel要比Socket更加具體,它代表每一個通訊通道,Selector它可以輪詢每個Channel的狀態,還有一個Buffer類,我們可以通過Buffer來控制資料的傳輸。

示例程式碼:

public void selector() throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Selector selector = Selector.open();//呼叫Selector的靜態工廠建立一個選擇器
        ServerSocketChannel ssc = ServerSocketChannel.open();//建立一個服務端的Channel
        ssc.configureBlocking(false);//設定為非阻塞方式
        ssc.socket().bind(new InetSocketAddress(8080));//將服務端的Channel繫結到一個Socket物件
        ssc.register(selector, SelectionKey.OP_ACCEPT);//註冊監聽的事件,將Channel註冊到選擇器上
        while (true) {//無限迴圈,保持監聽狀態
            Set<SelectionKey> keys = selector.keys();//取得所有的key集合
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = (SelectionKey) iterator.next();
                if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
                    ServerSocketChannel ssc2 = (ServerSocketChannel) key.channel();//獲取這個key所代表的通訊通道物件
                    SocketChannel sc = ssc2.accept();//服務端接受請求
                    sc.configureBlocking(false);
                    sc.register(selector, SelectionKey.OP_READ);
                    iterator.remove();
                } else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
                    SocketChannel sc = (SocketChannel) key.channel();
                    while (true) {
                        buffer.clear();//將緩衝區的索引狀態重置為初始位置
                        int a = sc.read(buffer);//讀取資料到buffer
                        if (a <= 0) {//資料讀取完畢,跳出迴圈
                            break;
                        }
                        //將快取位元組陣列的指標設定為陣列的開始序列即陣列下標0,這樣就可以從buffer開頭,
                        //對該buffer進行讀取了,最多隻能讀取之前寫入的資料長度,而不是整個緩衝的容量大小,
                        //如果沒有這個方法,就是從buffer最後開始讀取,讀出來的都是byte=0時候的字元。
                        buffer.flip();
                    }
                    iterator.remove();
                }
            }
        }
    }

在上面這段程式中,是將Server端的監聽連線請求的事件和處理請求的事件放在一個執行緒中,但是在應用中,我們通常會把它們放在兩個執行緒中,一個執行緒專門負責監聽客戶端的連線請求,而且是以阻塞方式執行的;另外一個執行緒專門負責處理請求,這個專門負責處理請求的執行緒才會真正採用NIO的方式,比如Web伺服器Tomcat和Jetty都是採用這種方式。

下面是基於NIO工作方式的Socket請求處理方式的處理過程:

        Selector可以監聽一組Channel上的I/O狀態,前提是這些Channel已經註冊到Selector中,Selector可以呼叫select()檢查已經註冊的通訊通道上I/O是否已經準備好,如果沒有通訊通道狀態發生變化,那麼select方法會阻塞等待或在超時後返回0,如果多個通道有資料,那麼它將會把這些資料分配到對應的Buffer中。所以NIO的關鍵是有一個執行緒來處理所有連線的資料互動,而每個連線的資料互動都不是阻塞方式,因此可以同時處理大量的連線請求。

2、Buffer的工作方式

可以把Buffer簡單地理解為一組基本資料型別的元素列表,它通過幾個變數來儲存這個資料的當前位置狀態:capacity, position, limit, mark:

索引 說明
capacity 緩衝區陣列的總長度
position 下一個要操作的資料元素的位置
limit 緩衝區陣列中不可操作的下一個元素的位置:limit<=capacity
mark 用於記錄當前position的前一個位置或者預設是-1

這裡寫圖片描述

舉例:我們通過ByteBuffer.allocate(11)方法建立了一個11個byte的陣列的緩衝區,初始狀態如上圖,position的位置為0,capacity和limit預設都是陣列長度。當我們寫入5個位元組時,變化如下圖: 
這裡寫圖片描述

這時我們需要將緩衝區中的5個位元組資料寫入Channel的通訊通道,所以我們呼叫ByteBuffer.flip()方法,變化如下圖所示(position設回0,並將limit設成之前的position的值): 
這裡寫圖片描述

這時底層作業系統就可以從緩衝區中正確讀取這個5個位元組資料併發送出去了。在下一次寫資料之前我們再呼叫clear()方法,緩衝區的索引位置又回到了初始位置。

 

呼叫clear()方法:position將被設回0,limit設定成capacity,換句話說,Buffer被清空了,其實Buffer中的資料並未被清空,只是這些標記告訴我們可以從哪裡開始往Buffer裡寫資料。如果Buffer中有一些未讀的資料,呼叫clear()方法,資料將“被遺忘”,意味著不再有任何標記會告訴你哪些資料被讀過,哪些還沒有。如果Buffer中仍有未讀的資料,且後續還需要這些資料,但是此時想要先先寫些資料,那麼使用compact()方法。compact()方法將所有未讀的資料拷貝到Buffer起始處。然後將position設到最後一個未讀元素正後面。limit屬性依然像clear()方法一樣,設定成capacity。現在Buffer準備好寫資料了,但是不會覆蓋未讀的資料。

呼叫mark()方法:它將記錄當前position的上一次位置,之後可以通過呼叫reset()方法恢復到這個position。

呼叫rewind()方法:它可以將position設回0,所以你可以重讀Buffer中的所有資料,limit保持不變,仍然表示能從Buffer中讀取多少個元素。

3、NIO的資料訪問方式

        NIO提供了比傳統的檔案訪問方式更好的方法,NIO有兩個優化方法:一個是FileChannel.transferTo、FileChannel.transferFrom;另一個是FileChannel.map。

① FileChannel.transferXXX與傳統的訪問檔案方式相比可以減少資料從核心到使用者空間的複製,資料直接在核心空間中移動,在Linux中使用sendfile系統呼叫。

② FileChannel.map將檔案按照一定大小塊對映為記憶體區域,當程式訪問這個記憶體區域時將直接操作這個檔案資料,這種方式省去了資料從核心空間向用戶空間複製的損耗。這種方式適合對大檔案的只讀性操作,如大檔案的MD5校驗。但是這個種方式是和作業系統底層I/O實現相關的。

五、I/O調優

1、磁碟I/O優化

① 增加快取,減少磁碟訪問次數;

② 優化磁碟的管理系統,設計最優的磁碟方式策略、磁碟定址策略;

③ 設計合理的磁碟儲存資料塊,以及訪問這些資料塊的策略,比如我們可以給存放的資料設計索引,通過定址索引來加快和減少磁碟的訪問量,還可以採用非同步和非阻塞的方式加快磁碟的訪問速度;

④ 應用合理的RAID策略提升磁碟I/O

2、TCP網路引數調優

        要能夠建立一個TCP連線,必須知道對方的IP和一個未被使用的埠號,由於32位的作業系統的埠號通常由兩個位元組來表示,也就是隻有2^16=65535個,所以一臺主機能夠同時建立的連線數是有限的。在Linux中可以通過檢視/proc/sys/net/ipv4/ip_local_port_range檔案來知道當前這個主機可以使用的埠範圍。

 
  1. [[email protected] ~]# cat /proc/sys/net/ipv4/ip_local_port_range

  2. 32768 60999

        如果可以分配的埠號偏少,遇到大量併發請求時就會成為瓶頸,由於埠有限導致大量請求等待建立連結,這樣效能就壓不上去。如果發現有大量的TIME_WAIT時,可以設定/proc/sys/net/ipv4/tcp_fin_timeout為更小的值來快速釋放請求。可以使用netstat -n | awk '/^tcp/{++state[$NF]} END {for(key in state) print key,"\t",state[key]}'來檢視網路連線情況。

TCP引數調優表如下:

    echo "1024 65535" > /proc/sys/net/ipv4/ip_local_port_range設定向外連線可用埠範圍 表示可以使用的埠為65535-1024個(0~1024為受保護的)
 
  echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse 設定time_wait連線重用 預設0
 
  echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle 設定快速回收time_wait連線 預設0
 
  echo 180000 > /proc/sys/net/ipv4/tcp_max_tw_buckets 設定最大time_wait連線長度 預設262144
 
  echo 1 > /proc/sys/net/ipv4/tcp_timestamps  設定是否啟用比超時重發更精確的方法來啟用對RTT的計算 預設0
 
  echo 1 > /proc/sys/net/ipv4/tcp_window_scaling 設定TCP/IP會話的滑動視窗大小是否可變 預設1
 
  echo 20000 > /proc/sys/net/ipv4/tcp_max_syn_backlog 設定最大處於等待客戶端沒有應答的連線數 預設2048
 
  echo 15 > /proc/sys/net/ipv4/tcp_fin_timeout  設定FIN-WAIT狀態等待回收時間 預設60
 
  echo "4096 87380 16777216" > /proc/sys/net/ipv4/tcp_rmem  設定最大TCP資料傳送緩衝大小,分別為最小、預設和最大值  預設4096    87380   4194304
 
  echo "4096 65536 16777216" > /proc/sys/net/ipv4/tcp_wmem 設定最大TCP資料 接受緩衝大小,分別為最小、預設和最大值  預設4096    87380   4194304
 
  echo 10000 > /proc/sys/net/core/somaxconn  設定每一個處於監聽狀態的埠的監聽佇列的長度 預設128
 
  echo 10000 > /proc/sys/net/core/netdev_max_backlog 設定最大等待cpu處理的包的數目 預設1000
 
  echo 16777216 > /proc/sys/net/core/rmem_max 設定最大的系統套接字資料接受緩衝大小 預設124928
 
  echo 262144 > /proc/sys/net/core/rmem_default  設定預設的系統套接字資料接受緩衝大小 預設124928
 
  echo 16777216 > /proc/sys/net/core/wmem_max  設定最大的系統套接字資料傳送緩衝大小 預設124928
 
  echo 262144 > /proc/sys/net/core/wmem_default  設定預設的系統套接字資料傳送緩衝大小 預設124928
 
  echo 2000000 > /proc/sys/fs/file-max 設定最大開啟檔案數 預設385583

注意,以上設定都是臨時性的,系統重啟後就會丟失。

 

▶ cat /proc/net/netstat:檢視TCP的統計資訊;

▶ cat /proc/net/snmp:檢視當前系統的連線情況;

▶ netstat -s:檢視網路的統計資訊。

3、網路I/O優化

① 減少網路互動的次數。

        要減少網路互動的次數通常需要在網路互動的兩端設定快取,如Oracle的JDBC就提供了對查詢結果的快取,在客戶端和伺服器端都有,可以有效減少對資料庫的訪問。除了設定快取還可以合併訪問請求,比如在查詢資料庫時,我們要查詢10個ID,可以每次查一個ID,也可以一次查10個ID。再比如,在訪問一個頁面進通常會有多個JS和CSS檔案,我們可以將多個JS檔案合併在一個HTTP連結中,每個檔案用逗號隔開,然後傳送到後端的Web伺服器。

② 減少網路傳輸資料量的大小。

        通常的辦法是將資料壓縮後再傳輸,比如在HTTP請求中,通常Web伺服器將請求的Web頁面gzip壓縮後再傳輸給瀏覽器。還有就是通過設計簡單的協議,儘量通過讀取有用的協議頭來獲取有價值的資訊,比如在設計代理程式時,4層代理和7層代理都是在儘量避免讀取整個通訊資料來獲取所需要的資訊。

③ 儘量減少編碼。

        在網路傳輸中資料都是以位元組形式進行傳輸的,但是我們要傳送的資料都是字元形式的,從字元到位元組必須編碼,但是這個編碼過程是比較費時的,所以在經過網路I/O傳輸時,儘量直接以位元組形式傳送。

④ 根據應用場景設計合適的互動方式。

a. 同步與非同步

        同步就是一個任務的完成需要依賴另一個任務時,只有等待被依賴的任務完成後,依賴的任務才能完成,這是一種可靠的任務序列,要成功都成功,要失敗都失敗。而非同步不需要等待被依賴的任務完成,只是通知被依賴的任務要完成什麼工作,只要自己完成了整個任務就算完成了,所以它是不可靠的任務序列,比如打電話和發信息。同步能夠保證程式的可靠性,而非同步可以提升程式的效能。

b. 阻塞與非阻塞

        阻塞就是CPU停下來等待一個慢的操作完成後,CPU才接著完成其它的工作,非阻塞就是在這個慢的操作執行時,CPU去做其它工作,等這個慢的操作完成時,CPU在完成後續的操作。雖然非阻塞的方式可以明顯提高CPU的利用率,但是也可能有不好的效果,就是系統的執行緒切換會比較頻繁。

c. 兩種方式的組合

        組合的方式有四種,分別是:同步阻塞、非同步阻塞、同步非阻塞、非同步非阻塞,這四種方式對I/O效能都有影響,如下所示:

組合方式  效能分析
同步阻塞 這種方式I/O效能一般很差,CPU大部分時間處於空閒狀態
同步非阻塞 這種方式通常能提升I/O效能,但是會增加CPU消耗
非同步阻塞 這種方式經常用於分散式資料庫,比如在一個分散式資料庫中,通常有一份是同步阻塞的記錄,
還有2~3份會備份一起寫到其他機器上,這些備記錄通常都採用非同步阻塞的方式來寫I/O
非同步非阻塞 這種組合方式用起來比較複雜,只有在一些非常複雜的分散式情況下使用,叢集之間的訊息同步
機制一般使用這種I/O組合方式,它適合同時要傳多份相同的資料到叢集中的不同機器,同時資料
的傳輸量雖然不大卻非常頻繁的情況


注意:雖然非同步和非阻塞能夠提升I/O的效能,但是也會帶來一些額外的效能成本,比如會增加執行緒數量從而增加CPU的消耗,同時也會導致程式設計複雜度的上升,如果設計的不合理,反而會導致效能下降。

六、設計模式解析之介面卡模式

        介面卡就是把一個類的介面變換成客戶端所能接受的另一種介面,從而使兩個介面不匹配而無法在一起工作的兩個類能夠在一起工作。通常被用於在一個專案需要引用一些開源框架在一起工作的情況下,這些框架的內部都有一些關於環境資訊的介面,需要從外部傳入,但是外部的介面不一定能夠匹配,在這種情況下,就需要介面卡模式來轉換介面。

1、介面卡模式的類結構

Target(目標介面):客戶端期待的介面;

Adaptee(源介面):需要被適配的介面;

Adapter(介面卡):將源介面適配成目標介面,繼承源介面,實現目標介面。

2、Java I/O中的介面卡模式

        InputStreamReader和OutputStreamWriter類分別繼承了Reader和Writer介面,但是要建立它們的物件必須在建構函式中傳入一個InputStream和OutputStream的例項。InputStreamReader實現了Reader介面,並且持有了InputStream的引用,這裡是通過StreamDecoder類間接持有的,因為從byte到char需要經過編碼。很顯然,介面卡就是InputStreamReader類,源介面就是InputStream,目標介面就是Reader類。OutputStreamWriter也是類似的方式。

七、設計模式解析之裝飾器模式

        裝飾器模式,顧名思義,就是將某個類重新裝扮一下,讓它的功能變得更加強大,但是作為原來這個類的使用者,還不應該感受到裝飾前和裝飾後有什麼不同,否則就破壞了原有類的結構,所以裝飾器模式要做到對被裝飾類的使用者透明,這是對裝飾器模式的一個要求。

1、裝飾器模式的類結構

Component:抽象元件角色,定義一組抽象的介面,規定這個被裝飾元件都有哪些功能;

ConcreteComponent:實現這個抽象元件的所有功能;

Decorator:裝飾器角色,它持有一個Component物件例項的引用,定義一個與抽象元件一致的介面;

ConcreteDecorator:具體的裝飾器實現者,負責實現裝飾器角色定義的功能。

2、Java I/O中的裝飾器模式

        以FileInputStream為例,InputStream類就是以抽象元件存在的,而FileInputStream就是具體元件,它實現了抽象元件的所有功能。FilterInputStream類無疑就是裝飾角色,它實現了InputStream類的所有功能,並且持有InputStream物件例項的引用。BufferedInputStream是具體的裝飾器實現者,它給InputStream類附加了功能。這個裝飾器類的作用就是使得InputStream讀取的資料儲存在記憶體中,從而提高讀取的效能。與這個裝飾器類有類似功能的是LineNumberInputStream類,它的作用就是提高按行讀取資料的功能。

八、介面卡模式與裝飾器模式的區別

        介面卡模式和裝飾器模式都有一個別名,就是包裝模式,它們看似都是起到包裝一個類或物件的作用,但是使用它們的目的很不一樣。介面卡模式是要將一個介面轉變成另一個介面,它的目的是通過改變介面來達到重複使用的目的;而裝飾器模式不是要改變被裝飾物件的介面,而是恰恰要保持原有的介面,但是增強原有物件的功能,或者改變原有物件的處理方法從而提升效能。

--------------------- 本文來自 依韻_ 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/Alexshi5/article/details/79481259?utm_source=copy