应用层协议设计有感

前些天的时候,一位同事问了一些REDIS的网络协议相关的问题,然后交流中谈了一些我的想法,又参考了一些资料,记录下来。

我们在设计一个应用层网络协议是,我们需要关注哪些方面? 或者说一个好的应用层协议应该有哪些属性?

好的应用层协议是可伸缩的。一些应用层协议比如HTTP,会建立几条并行的链接的到服务端,这样做可以减少延迟,增加吞吐量,但是在传输层和服务端看来,这些链接相互之间是没有关联关系的。而且可能会造成额外的一些问题。

在传输层,比如TCP协议采用自适应算法根据网络的条件进行高效的数据传输。这个自适应的过程是在每个链接上进行的。如果同时有多个链接到服务端,那也就是需要针对每个链接进行自适应的调节过程。这就在网络中引入了额外的不必要的压力,而且TCP如果用了慢启动拥塞算法,那么,应用层序还是可以感受到这种额外的延迟。如果客户端的带宽不足,这种问题反而会有可能变的更明显。

在服务端,每个从客户端建立的链接有可能都要经过认证,因此,服务端的负载会随着远大于实际用户数的链接的增加而增大。更严重的情况是,如果有一个用户发起了非常多的链接,服务端的性能可能会严重下降,甚至会导致无法为其他用户提供服务。

HTTP协议1.0版本,提供了持久链接机制(Keep-alive),允许一个链接在处理完事务后可以不用关闭链接,以便为将来的HTTP请求复用现存的链接。这种机制可以避开缓慢的链接建立阶段,而且还可以避免慢启动的拥塞适应阶段,进行更快速的数据传输。但是管理持久链接时要特别小心,防止积累出大量的空闲链接,耗费本地以及远端的资源。并行链接和持久链接配合使用可能是最高效的方式。

类似HTTP的协议一般都是只允许客户端发送请求到服务端,而不允许服务端直接发送数据到客户端。当想要完成服务端推送一些消息到客户端之类的业务需求时,只能让客户端不断的向服务端发送轮询请求。这样会导致浪费网络资源,也会对服务端的资源造成浪费。所以后来引入了WeSocket,可以让HTTP链接转换为一般的链接,就可以避开HTTP协议的要求了。

另外就是类似HTTP的协议,一般都是一个请求发送过去,然后等待一个回应完成,然后再发下一个请求,这种串行化处理会因为网络往返时延导致延迟较高。所以一般会用到Pipeline技术。也就是一次发送多个请求,要求服务端按照相应的顺序发送回来。 这里面的重中之重就是服务端必须要确保发送的顺序是请求的顺序。还有就是如果客户端发送了10个请求,但是服务端在处理完5个请求后发生了错误就关闭了链接,那么客户端就要确保能够重新发送剩余的5个请求,这带来了一定的实现复杂度。

但是类似HTTP的协议,在我看来还有另外一个好处。因为只允许客户端到服务端发送请求,那么会让C/S双方的业务变得调理清晰。比如说,业务服务由客户端请求触发,专门有另外一个链接提供服务端推送数据到客户端。这种职责单一性会带来更好的可维护性。而且这种职责单一性,可以很容易的用其他语言实现,也可以采用同步阻塞IO或者非阻塞IO甚至异步IO机制实现。假设如果一个链接即处理业务请求,又能够让服务端推送数据,那么更好的实现模型是event-driven,如果用同步阻塞IO实现,就会变得异常复杂。在我最近的接触中,发现REDIS的协议就是此种设计。REDIS的协议里大部分都是客户端发起请求,服务端处理后返回结果; 但是有一个特殊的请求,就是SUBSCRIBE,客户端发起这个命令后,如果再通过这个链接发送数据到服务端,服务端会直接返回错误。处于SUBSCRIBE状态的链接只能等着服务端主动推送来的数据。 

可伸缩性的另外一个方面就是服务端和客户端的部署关系。功能实现在服务端总是比实现在客户端能够带来更多的可伸缩性,毕竟大多数情况下,服务端升级总是要比客户端容易一些。

好的应用层协议是高效的。我们大多数都会非常关心传输的效率,总是认为传输速度越快越好。一般的做法是批量请求,一个请求中包含了很多要一起处理的事情;或者重叠冲球,比如HTTP PIPELINE机制和IMAP中针对请求打标机的方式,让客户端无需等待回应就可以继续向服务端发送请求,然后在接收时可以通过客户端的状态机处理回应数据,这样就能够减少等待时间。有些时候,性能可能会和扩展性产生冲突,这个时候就需要自己根据实际场景和需求做权衡。

好的应用层协议是简单的。经验法则是如果一个协议设计的很差,那么在做一些简单的事情时反而变成了挑战。正确的方式是让简单的事情简单的完成,复杂的事情通过复杂的方式完成。另外一条经验法则是,如果应用层协议有两种方式完成同样的事情,那么可能在基础协议设计架构上存在问题。简单并不意味着想法简单,而是说,简单是一种精心设计的,可以解决包括了问题域中的任何事情,甚至是边界的更复杂的问题。一致性是会让设计变得优雅和简单。

好的应用层协议是可扩展的。我们程序员经常会遇到需求的变更,或者一些策略变动较多的问题。这也就意味着,协议本身是逐步演进的。所以一个可扩展的协议对我们来说非常的重要。可扩展的协议可以方便的向前或者向后兼容,比如protobuff,json,http等协议。

好的应用层协议是人类可读的。可读性要根据自己实际场景和需要进行权衡。但是在我个人看来,可读的协议容易帮助排查问题。想象一下,遇到了一个网络问题,需要抓包分析,满眼的二进制流是什么感受?再想象下,如果是HTTP协议,你还有这种困难么?

好的应用层协议是健壮的。Postel的健壮性法则“be conservative in what you send, be liberal in what you accept.(发送时保守,接手时开放)”讲究的更多的是吞掉对方的错误,但是如果对接收到的数据进行检查并返回了错误,那么对对方可能会有更多的好处。假设跨语言实现的时候或者多版本客户端的时候,如果没有一个一致的错误检查,会让人很蛋疼的吧。

 

参考文献:

  1. 《On the Design of Application Protocols》
  2. 《UNIX编程艺术》
posted @ 2014-08-20 23:20 011 阅读(...) 评论(...) 编辑 收藏