多个跨云服务器之间满带宽测速的一种实现方案

量变引起质变。

项目需求

由于我们现在开发的云平台项目是一个跨云调度的重型计算平台,所以会用到不同的云服务厂商的计算实例服务器,比如阿里云的ECS、亚马逊的EC2或者谷歌云的compute engine等,同时也会在这些计算实例之间进行数据传输。
这些服务器之间的传输速度通常是不同的,即使是同一个云服务厂商内的不同区域服务器之间传输数据,带宽也会有所不同。
所以需要对这些服务器之间的带宽速度进行测量,以供调度进程分配任务和传输数据。
总结起来这个功能实现起来有3个要求:

  1. 对不同区域内的云服务器的TCP/UDP传输速度进行检测。
  2. 必须是满带宽的测速,也就是说两台服务器在测速的时候不能有其它网络程序运行。
  3. 多台服务器之间需要高效地建立两两连接。

针对这3个问题,我实行了以下解决方案。

基本结构

一种简单的设想就是启动测速所需的客户端和服务端,让这些程序之间互相争抢进行测速。

但是这种方式在进程的管理上很容易出现问题,因为每个进程既要操作自己的状态还需要操作其它进程的状态,同时一下子起4个进程也比较浪费,因为事实上每次测速只会有一个进程工作。
所以更好的方式是用主从结构,由一个主进程来启停负责测速的客户/服务端子进程。

这样不但能有效地避免资源的浪费和争抢,同时主进程中也可以集成很多逻辑功能,比如对外提供 REST API,记录日志、备份测试结果等。

当然为了方便部署和程序控制,所有的进程都是部署在 Docker 容器中。

由于主进程需要访问宿主机的 Docker 服务,所以需要开启 Docker 的 remote API 服务,对容器提供REST API进行操作。

功能实现

基本测速

TCP与UDP

网络协议是一层一层封装起来的,而TCP和UDP属于同一层的两种协议。
其中TCP协议在前后端开发中非常常用,因为REST API请求依赖的HTTP(S)协议就是TCP的上层协议,而UDP协议在视频、游戏、下载等业务中使用也非常多。
它们有一些共同点:请求的发起方称为客户端,请求的接收方称为服务端,服务端和客户端可以双向通信。
而它们的侧重点有所区别。TCP更注重稳定,客户端和服务端之间需要建立连接之后才能互相发送数据。
UDP则更注重速度,客户端不需要和服务端建立连接即可直接发送数据,但是如果发送速度太快或者网络不稳定可能会造成丢包,导致对方接收的数据部分丢失。

测速工具

常用的命令行测速工具有iperf和speedtest,相较之下选择了功能更强大的iperf。
iperf是一个比较理想的测速工具,支持TCP、UDP协议,还可以通过参数来制定传输数据大小、传输次数或者传输时间,以及输出结果的格式。
但是由于前面UDP协议的特性,测速会略微麻烦一些,需要找到合适的带宽。
比如按照1Gbps的速度发送数据,丢包率是70%和按照10Mbps的速度发送数据,丢包率是0,那么对数据完整性有要求的话肯定更偏向于后者。
当然实际情况并不是对于丢包率为0就是最好的,而是在可容忍的范围内采用最大速度传输(数据丢了还可以重传不是~)。
这就意味着需要根据实际网络状况不断调整和尝试。
而iperf并没有这么智能,所以UDP这一块采用团队内部开发的一款UDP传输工具,来找到理想的传输速度。

满带宽

要保证满带宽只需要保证测速时没有其它程序占用带宽即可。
由于我们可以启动一台独立的抢占式服务器来运行测速程序,所以其它非测速程序的进程不太可能占用带宽,而容易争抢带宽的是用来测速的子程序。
所以需要让子程序之间是互斥运行,甚至是互斥存在的。
采用状态管理基本上就可以实现,主程序在每次有进程启动的时候将状态置为”connecting”,测速完成后置为”waiting”,只有在”waiting”状态下才可以启动新的子程序进行测速。
但是这只是从代码逻辑层面控制,对于稳定健壮的程序而言,最好还有其它的硬性控制方式。
这时候使用容器的话就可以轻松办到。
凡是需要进行测速的进程都在容器中启动,同时容器的名称都统一,那么一旦程序出现bug,同时启动多个子程序时,Docke r服务则会报错,告知容器名称冲突,从而创建失败。
当然这种方式也有一定的风险,比如上一个进程测速过程中出现问题没有按时退出,那么则无法进行新的测速,所以需要需要设定一个超时时间,超过一段时间后主动停止当前测速子程序。
同时如果主程序意外退出,导致停止失败的话,也要进行处理:在每次启动主程序的时候进行检查,及时销毁未停止的子程序。

多节点

多节点算是非常棘手的问题。试想如果在一段时间内同时在多个云服务器上启动多个测速程序,如何保证他们有序的进行测速呢?
要解决这个问题,先思考一个简单些的问题:
在一段时间内,如何决定哪些云服务器启动服务端子程序哪些云服务器启动客户端子程序呢?
如果按照“主-从”模式的话需要建立一个中心节点来进行控制,但是这样的缺点很多,最重要的一个缺点是如果某个节点与中心节点无法通信那么就无法获得与其它节点通信的机会,及时它和其它节点之间网络畅通。
同时中心节点和其它节点之间也存在多节点通信的问题。
总而言之这种方式下通信的成本太高,服务端与客户端传输数据需要的中间环节太多,很容易出现问题。
所以简单的方式是让云服务器之间互相发起测速请求并响应。
这样的话,主程序的逻辑要分为两个模块,一个模块用来响应请求、、分配端口、启动服务端容器。
另一个用来轮询带测速队列并发起请求、启动客户端容器建立连接。

工作流程大致如下:

这种处理方式还有一种极端情况,就是两个云服务器之间互相请求进行测试,如果双方请求到达时间一致,那么就会同时给对方分配端口,然后同时受到对方分配的端口之后发现服务端已启动于是放弃连接。
于是出现了类似进程“死锁”的状态。
对于这种情况的处理方式是使用时间戳来记录请求发起的时刻,双方通过时间戳的先后来决定是否启动客户端或服务端。
即使更极端的情况出现——双方时间戳相同。那么通过超时回收或者发消息释放端口来建立下一次连接。

弱网络下的处理

弱网络指的是网络不稳定或者带宽较小的情况。
这种情况的处理方式原则上就是重测,但是关于重测有几个需要注意的地方:

  1. 对于带宽较小的情况需要考虑减小传输数据的体积以保证在制定的超时时间内完成测速。
  2. 对于测速失败的情况进行判断并重测,同时限定重测次数,避免无限重测。
  3. 重测时可以让客户端与服务端进行互换测试,通过限定一方发起重测可实现。

总结

总体结构图如下:

很多时候实现一个功能并不困难,但是要把功能实现好却是一件不简单的事。
虽然理论上实现起来只是简单的调用测速工具就可以得到结果,但在实际场景下可用性会变得很低。
比如没有对弱网络的重测机制,那么偶然的网络抖动就会影响到测速结果。
如果没有考虑到多节点争抢连接的问题,那么实际运行在多个云服务器上可能会造成程序错误或测速结果不准确的问题。
要怎么样把功能实现好呢?
至少有两个考虑方向:

  • 倍数思维。比如当前框架支持10个页面没问题,那么如果100个、1000个会不会有性能问题?
  • 极限思维。就是一些极端情况下的处理机制,比如在本文中对超时的处理,对容器互斥的处理等。

作者信息:朱德龙人和未来高级前端工程师。