C10k破局(一)——線程池和消息隊列實現高併發服務器

一、C10k的由來

互聯網的基礎就是網絡通信,早期的互聯網可以說是一個小羣體的集合。互聯網還不夠普及,用戶也不多,一臺服務器同時在線100個用戶估計在當時已經算是大型應用了,所以並不存在什麼 C10K 的難題。互聯網的爆發期應該是在www網站,瀏覽器,雅虎出現後。最早的互聯網稱之爲Web1.0,互聯網大部分的使用場景是下載一個HTML頁面,用戶在瀏覽器中查看網頁上的信息,這個時期也不存在C10K問題。

Web2.0時代到來後就不同了,一方面是普及率大大提高了,用戶羣體幾何倍增長。另一方面是互聯網不再是單純的瀏覽萬維網網頁,逐漸開始進行交互,而且應用程序的邏輯也變的更復雜,從簡單的表單提交,到即時通信和在線實時互動,C10K的問題才體現出來了。因爲每一個用戶都必須與服務器保持TCP連接才能進行實時的數據交互。

然而最初的服務器都是基於進程/線程模型的,對於每一個TCP連接都分配1個進程(或者線程)去處理。而進程又是操作系統最昂貴的資源,一臺機器無法創建很多進程。如果是C10K就要創建1萬個進程,那麼單機而言操作系統是無法承受的(往往出現效率低下甚至完全癱瘓)。如果是採用分佈式系統,維持1億用戶在線需要10萬臺服務器,成本巨大。

基於上述考慮,如何突破單機性能侷限,是高性能網絡編程所必須要直面的問題。這些侷限和問題最早被Dan Kegel 進行了歸納和總結,並首次成系統地分析和提出解決方案,後來這種普遍的網絡現象和技術侷限都被大家稱爲 C10K 問題。

二、傳統的BIO

BIO模型圖

採用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後爲每個客戶端創建一個新的線程進行處理,處理完成之後,通過輸出流返回應答給客戶端,最後再銷燬線程。這就是典型的一請求一應答通信模型。正如我們前面所說的,這種模型侷限性非常大,我們需要尋求更好的模型來解決高併發的問題。

三、僞異步IO的實現

(一)、定義

1、同步與異步

同步和異步通常用來形容一次方法調用

同步方法調用一旦開始,調用者必須等到方法調用返回後,才能繼續後續的行爲。

異步方法調用更想一個消息傳遞,一旦開始,方法調用就會立即返回,調用者就可以繼續後續的操作。而,異步方法通常在另外一個線程中執行着,整個過程不會妨礙調用者的工作。

2、僞異步IO

Java中提供了線程池和任務隊列的機制來幫助我們實現僞異步IO模型。大致的處理過程爲:每當服務器接收到一個客戶端的連接請求,我們就把socket包裝成一個task,並把這個task丟進線程池中,由線程池來處理該任務。線程池中的最大線程數是一定的,JVM會調度其中的空閒線程來處理我們的任務,當沒有可用線程時,任務將會被加入任務列隊中進行等待。這樣子我們就不用爲每個客戶端請求都分配一個線程了。

而爲何我們說這種處理方法是僞異步IO。主要是因爲雖然它已經可以實現一個或多個線程處理N個客戶端請求,但是它的底層仍然用的是同步阻塞IO機制,所以我們說這種處理方式是僞異步IO。

(二)、線程池簡單說明

1、參數說明

2、處理機制

如果想更仔細地瞭解線程池的知識,可以讀下這篇博客:https://www.cnblogs.com/dolphin0520/p/3932921.html

(三)、代碼實現

之前寫過BIO模型的聊天室(《JAVA簡單聊天室的實現》),所能處理的最大併發數大概在2000到2300左右。今天我們就利用線程池來對這個聊天室進行簡單的改造,並測試下它的性能。

1、線程池類

package server;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ChatServerHandlerExcutePool {
	private ExecutorService executor;
	
	public ChatServerHandlerExcutePool(int maxPoolSize, int queueSize) {
		/*
		 * Runtime.getRuntime().availableProcessors():JVM運行所能創建的最大線程數
		 * 空閒線程存活時間爲120s
		 */
		executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),maxPoolSize,120L,TimeUnit.SECONDS,new ArrayBlockingQueue<java.lang.Runnable>(queueSize));
	}

	public void execute(java.lang.Runnable task) {
		executor.execute(task);
	}

}

2、ChatServer類的改造

package server;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;


 
public class ChatServer {
	
	
	//主函數入口
	public static void main(String[] args) throws IOException {
		//實例化一個服務器類的對象
		ChatServer cs=new ChatServer();
		//調用方法,爲指定端口創建服務器
		cs.setUpServer(9003);
	}
 
	private void setUpServer(int port) throws IOException {
		// TODO Auto-generated method stub
		ServerSocket server=new ServerSocket(port);
		//打印出當期創建的服務器端口號
		System.out.println("服務器創建成功!端口號:"+port);
		ChatServerHandlerExcutePool singleExecutor = new ChatServerHandlerExcutePool(50,10000);
		Socket socket=null;
		while(true) {
			//等待連接進入
			socket=server.accept();
			System.out.println("進入了一個客戶機連接:"+socket.getRemoteSocketAddress().toString());
			
			//啓動一個線程去處理這個對象
//			ServerThread st=new ServerThread(socket);
//			st.start();
			
			//創建一個線程,並加入線程池
			singleExecutor.execute(new ServerThread(socket));
		}
	}
}

(四)、性能測試

接着我們運行服務器,並且利用Jmeter來測試一下服務器的性能

設定啓動1w個線程

查看結果

沒有異常,說明1w個線程都已成功發送消息並且接受到返回的信息了。

(五)、侷限性

雖然相較於傳統的BIO,僞異步IO的性能有了一定的提升,但是仍然存在不少的問題

(六)、故障

 

 

後續我們可以嘗試用NIO編程來進一步提升服務器的性能。

說明:本文主體內容來自《Netty權威指南》一書

本文所用的聊天室代碼地址:https://github.com/Alexlingl/Chatroom

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章