End-To-End Arguments in System Design 提出在系统设计中,将一些功能放到底层实现可能是没有价值甚至多余的,相比起实现它们的成本而言。相反,只有终端的应用知道自己的具体需求是什么,无论怎么样都需要在终端的应用中实现相同的功能。在底层实现的功能往往只能起到提高性能的作用。
因为底层的抽象层往往对上层应用细节不了解,如果提供太多功能,那么所有使用到的应用都需要负担额外的开销,哪怕实际上它们并不需要相关的功能。而且,有时候底层实现的功能可能核应用的需求不符合,这时候应用反而需要想办法绕过或者修改底层,来实现自己的功能。哪怕底层的功能确实是应用所需要的,其提供的保证往往不足或者过多,应用仍然需要实现自己的一套功能。
例如,在文件传输中,哪怕底层采用了可靠的传输协议,在发送方读取文件或者接收方写入文件的过程中仍然有可能发生错误,导致文件传输出错。而这时候底层的协议并不知道文件传输的语义,也就不能对文件传输的全过程进行校验。所以,应用还是需要实现文件级别的校验和,从而确保文件传输的正确性。如果接收方发现校验和出错,那么可以简单地要求发送方重传。在这种情况下,底层网络是否可靠也不影响应用的正确性。但如果底层网络太过不可靠,那么可能导致无限的出错和重试,造成应用无法正常运行。此时,一个可靠的底层网络可以提高传输的成功率,减少重试次数,节省网络流量,提高系统的整体性能。
而贴近生活一点的例子,比如微信,即使底层使用了 TCP 的连接,我们仍然不能保证消息在发送到服务器之后服务器处理不出错,或者你的号没有被封,所以微信在应用层还是需要一套单独的确认机制来确认消息有没有发送成功。这时候的终端指的是双方的微信软件。但是即使发送成功了,你也没办法保证对面一定看到了消息,如果你想进一步确认,那么你就需要发一条“收到请回复”的消息,然后等对方回复。这时候的终端就是你和聊天对象。
可以看出,端到端的对象即使在同一个应用中,随着终端定义的不同,也会发生改变。再举一个例子就是语音通话。实时语音通话的时候,底层网络如果提供了可靠性,那么意味着会发生重传等情况,造成延迟越来越大。但是在语音通话的过程中,出现偶尔的破音、静音都是可以接受的,大不了没听清的人让对面再说一次。哪怕断线了,也可以重拨。这时候底层的可靠性就是完全多余的,这也是为什么现在的实时音视频协议喜欢用 UDP 协议而不是 TCP 协议。但是,假如不是实时音频通话,而是远程录音,那么如果发生丢包等情况,是没有办法让说话的人重新说一次的,而且由于是写入到录音文件中,实时性没有要求。在这种情况下,底层网络提供可靠性可以简化应用程序的设计,也提高了录音的质量。随着终端的改变,对于不同层次的功能设计要求可能也会发生巨大的改变。
而例如在操作系统中,以前的设计思想是操作系统内核包办一切,例如网络、存储等。但是随着硬件和应用的发展,也出现了类似 DPDK、SPDK 这种应用程序直接接管网络和存储硬件,自己从头实现网络存储栈的。也有类似 eBPF 等技术,修改内核的行为,来适应应用的需求以及提升性能。从另一方面看,这也是 Linux 实现的一个败笔,大家也喜欢在内核修修补补,最后内核变成了忒修斯之船。