多路複用實現單服百萬級別RPS吞吐
多路複用其實並不是什麼新技術,它的作用是在一個通訊連線的基礎上可以同時進行多個請求響應處理。對於網路通訊來其實不存在這一說法,因為網路層面只負責資料傳輸;由於上層應用協議的制訂問題,導致了很多傳統服務並不能支援多路複用;如:http1.1,sqlserver和redis等等,雖然有些服務提供批量處理,但這些處理都基於一個RPS下。下面通過圖解來了解釋單路和多路複用的區別。
單路存在的問題
每個請求響應獨佔一個連線,並獨佔連線網路讀寫;這樣導致連線在有大量時間被閒置無法更好地利用網路資源。由於是獨佔讀寫IO,這樣導致RPS處理量由必須由IO承擔,IO操作起來比較損耗效能,這樣在高RPS處理就出現效能問題。由於不能有效的合併IO也會導致在通訊中的頻寬存在浪費情況,特別對於比較小的請求資料包。通訊上的延時當要持大量的RPS那就必須要有更多連線支撐,連線數增加也對資源的開銷有所增加。
多路複用的優點
多路複用可以在一個連線上同時處理多個請求響應,這樣可以大大的減少連線的數量,並提高了網路的處理能力。由於是共享連線不同請求響應資料包可以合併到一個IO上處理,這樣可以大大降低IO的處理量,讓效能表現得更出色。
通過多路複用實現百萬級RPS
多路複用是不是真的如此出色呢,以下在.net core上使用多路複用實現單服務百萬RPS吞吐,並能達到比較低的延時性。以下是測試流程:
由於基礎通訊不具備訊息包合併功能,所以在BeetleX的基礎上做整合測試,主要BeetleX會自動合併訊息到一個Buffer上,從而降低IO的讀寫。
測試訊息結構
本測試使用了Protobuf作為基礎互動訊息,畢竟Protobuf已經是一個二進位制序列化標準了。
請求訊息
[ProtoMember(1)]
public int ID { get; set; }
[ProtoMember(2)]
public Double RequestTime { get; set; }
響應訊息
[ProtoMember(1)] public int EmployeeID { get; set; } [ProtoMember(2)] public string LastName { get; set; } [ProtoMember(3)] public string FirstName { get; set; } [ProtoMember(4)] public string Address { get; set; } [ProtoMember(5)] public string City { get; set; } [ProtoMember(6)] public string Region { get; set; } [ProtoMember(7)] public string Country { get; set; } [ProtoMember(8)] public Double RequestTime { get; set; }
** 服務端處理程式碼**
public static void Response(Tuple<IServer, ISession, SearchEmployee> value)
{
Employee emp = Employee.GetEmployee();
emp.RequestTime = value.Item3.RequestTime;
value.Item1.Send(emp, value.Item2);
System.Threading.Interlocked.Increment(ref Count);
}
public override void SessionPacketDecodeCompleted(IServer server, PacketDecodeCompletedEventArgs e)
{
SearchEmployee emp = (SearchEmployee)e.Message;
multiThreadDispatcher.Enqueue(new Tuple<IServer, ISession, SearchEmployee>(server, e.Session, emp));
}
服務響應物件內容
Employee result = new Employee();
result.EmployeeID = 1;
result.LastName = "Davolio";
result.FirstName = "Nancy";
result.Address = "ja";
result.City = "Seattle";
result.Region = "WA";
result.Country = "USA";
接收訊息後放入佇列,然後由佇列處理響應,設定請求相應請求時間並記錄總處理訊息計數。
客戶端請求程式碼
private static void Response(Tuple<AsyncTcpClient, Employee> data)
{
System.Threading.Interlocked.Increment(ref mCount);
if (mCount > 100)
{
if (data.Item2.RequestTime > 0)
{
double tick = mWatch.Elapsed.TotalMilliseconds - data.Item2.RequestTime;
AddToLevel(tick);
}
}
var s = new SearchEmployee();
s.RequestTime = mWatch.Elapsed.TotalMilliseconds;
data.Item1.Send(s);
}
客戶端測試發起程式碼
for (int i = 0; i < mConnections; i++)
{
var client = SocketFactory.CreateClient<BeetleX.Clients.AsyncTcpClient, TestMessages.ProtobufClientPacket>(mIPAddress, 9090);
client.ReceivePacket = (o, e) =>
{
Employee emp = (Employee)e;
multiThreadDispatcher.Enqueue(new Tuple<AsyncTcpClient, Employee>((AsyncTcpClient)o, emp));
};
client.ClientError = (o, e) =>
{
Console.WriteLine(e.Message);
};
mClients.Add(client);
}
for (int i = 0; i < 200; i++)
{
foreach (var item in mClients)
{
SearchEmployee search = new SearchEmployee();
Task.Run(() => { item.Send(search); });
}
}
整個測試開啟了10個連線,在這10個連線的基礎上進行請求響應複用。
測試配置
測試環境是兩臺伺服器,配置是阿里雲上的12核伺服器(對應的物理機應該是6核12執行緒)
服務和客戶端的系統都是:Ubuntu 16.04
Dotnet core版本是:2.14
測試結果
客戶端統計結果
服務端統計資訊
頻寬統計
測試使用了10個連線進行多路複用,每秒接收響應量在100W,大部分響應延時在1-3毫秒之間