多路複用其實並不是什麼新技術,它的作用是在一個通訊連接的基礎上可以同時進行多個請求響應處理。對於網絡通訊來其實不存在這一說法,因爲網絡層面只負責數據傳輸;由於上層應用協議的制訂問題,導致了很多傳統服務並不能支持多路複用;如: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毫秒之間