对于服务与服务之间往往需要高效的吞吐的信息交互,但在绝大部分服务应用中为了实现高吞吐交互都是基于连接池模式,即通过多个TCP连接来提高吞吐量,这种设计完全是通过增加IO的读写量来实现高效吞吐。
如果能减少连数和降低IO量(合并)来提高吞吐量那在同样吞吐交互的情况即可以节省更多的资源。但这种设计需要从上层应用调度和协议设计相结合才能实现,同样这个应用调度设计会变得比较复杂。接下来介绍一下如何实现这样一个应用模型。
协议制定
如果想在一个TCP连接上达到最高的吞吐能力,那必须制定一个连接可复用的协议,即协议支持一个TCP连接给多个会话并发接收和发送消息。接下来通过一些协议的工作情况来做对比
HTTP1.1
这协议是当前使用最广泛的通讯应用协议,它的通讯模式如下:
协议约束了当前连接必须等待响应后才能继续下一个请求,如果需要高吞吐交互那只能通过更多的连接来支持;上层应用大量的连接和IO读写会带来更多资源的开销。每个请求最少使用一次socket write和read.
HTTP1.1 Pipelining
这是一种特性则是针对场景的应用方式,其处理方式如下
这样的好处理就即多个请求响应可以合并到单个socket write和read里,节省IO读写的次数大大提高性能。但这种模式下一个连接也只能针对一个会话使用,很难应用多到个会话并发中。它的请求响应顺序还需要保持一致。
HTTP2.0
HTTP2.0协议提供了连接复用能力,这样多个请求就无须排列等上一个完成即可以处理下一下。
它们的请求响应并没有固定的顺序,不过有一个ID标签来维护它们的响应关系。只要不同会话的通讯交互ID不同,那即使多会话在并发上用同一个连接处理也不会产生影响。对于多个Request和Response是不是会合并到一个socket write和read里那就要看应用的模型设计了。
总体上来说HTTP2.0协议支持单TCP连接的高吞吐应用需求,在设计过程中可能参考相关特性。只有协议并不能满足应用的需要,更重要是多个会话使用同一个连接进行交互的同时相互不存在影响。
调度模型设计
如果想一个连接满足多个会话并发需要,那必须设计一个针对消息的发送和接收调度模型;通过这个调度模型把会话并发之前的影响切割出来。同样这个模型对消息处理进行分离切割外还需要加入IO合并处理,把多请求响应尽可能用最少的socket write和read来处理从而达到更低的资源使用。
整个逻辑设计起来比较复杂,通过Stream把Socker操作的buffer进行隔离,这样在读和写的过程都可以做一个批量的Socket read和write.消息解码和编码通过队列做一个分隔,在消息处理通过线程池把不同会话的请求进行一个隔离处理。
应用效果
由于BeetleX已经完全基于这种模型来设计,但上层扩展还是需要做消息处理调度。接下来看一下BeetleX.XRPC基于这种模型进行远程接口调用达到怎样的效果。以下是一个简单用户信息获取的服务和Client接口调用测试代码
//server
public Task<List<User>> List(int count)
{
List<User> result = new List<User>();
for (int i = 0; i < count; i++)
{
User user = new User();
user.ID = Guid.NewGuid().ToString("N");
user.City = "GuangZhou";
user.EMail = "Henryfan@msn.com";
user.Name = "henryfan";
user.Remark = "http://ikende.com";
result.Add(user);
}
return Task.FromResult(result);
}
//client
static async Task MutilTest()
{
while (true)
{
await UserService.List(1);
System.Threading.Interlocked.Increment(ref mCount);
}
}
开启了500个用户用同一个tcp client进行服务调用的测试结果,
RPS达到30万,针对E3-1230V2这CPU资源来说,使用连接池的方式都难以达到20万RPS的接口调用,但基于一个连接反而提升到30万;这么高的吞吐归功于连接复用的过程中对读写网络IO合并了。
由于这个模型设计每个连接只能由于一个线程来处理协议分析,当协议比较复杂的情况那一定程度限制了吞吐能力。当这种模型碰到CPU资源充足的情况下可以适当地加多一到两个连接即可满足需求。
接下来针对一个连接多会话并发能跑到多大的带宽?把测试代码修改一下
//client
static async Task MutilTest()
{
while (true)
{
await UserService.List(100);
System.Threading.Interlocked.Increment(ref mCount);
}
}
改成每次返回100个对象。
E3-1230V2的CPU在测试过程占用50%的资源 ,单个TCP连接跑到接近2G的带宽,RPS大概在2.5万,每个请求返回100个对象列表;大量消息序列化占用了比较多的协议处理资源。
注意
在这种模型不要刻意去通过加连接数来提高吞吐,加大连接数意味着socket的write和read合并数量降低,同样数量的吞吐可能引起更高的write和read这样反倒影响性能。具体分配多少个连接是最优则看硬件资源而定,就拿E3-1230V2这个测试情况来说,如果多加一个连接吞吐量并没有太多提高但CPU使用率则提升了好多,主要是socket的write和read的数量增加太多了。
这种模型还有一个缺点是低吞吐的情况延时有所提高,主要是跨越几个线程调度,每个环节不饱和的情况下会引线程在线池间的切换引起一些调度上的延时。
示例代码:
https://github.com/beetlex-io/BeetleX-Samples/tree/master/XRPC.Performance
BeetleX
开源跨平台通讯框架(支持TLS)
提供高性能服务和大数据处理解决方案
https://beetlex.io