End to End Argument(可能是最重要的系统设计论文)

End to End Argument(可能是最重要的系统设计论文)

如果在初学分布式系统环境下的系统设计的时候,可以让未来的自己给过去的自己说一句话,那么我会对自己说:去看End to End Argument.... —— Alexis

今天分享我觉得是最重要的一篇论文,没有遇到它之前,我学习分布式环境下的系统设计的时候, (比如怎么用微服务,消息队列,使用数据库,云服务交互。。),走了不少弯路,遇到很多很疑惑的问题无法解答。希望通过这篇让大家少走我走过的那些弯路。

End To End Argument可能是你最应该了解的分布式系统设计的一个事实:“端到端的可靠通信,只能通过通信两端的application层来保证,而中间件(比如SQS, Kinesis, ActiveMQ, 到更低层Netty乃至TCP)只能提高效率,而无法保证通信的可靠性。

对于了解网络编程和TCP通信的同学来说,这可能很反直觉,因为TCP已经是保证可靠通信了,而SQS,Kinesis, ActiveMQ,等中间件也无一不宣传自己的“可靠通信”,甚至exact once msg processing“。

怎么回事儿?其实即使你不去看这篇论文,也能很容易通过例子理解。

举两个例子:

非计算机世界的例子

比如A要告诉B一件事情,A要保证B“知道”了这件事情是“高层意图”,声波的传递是“低层”,面对面说话,物理世界“保证”声波传递到了B,但是由于B可能在想别的事情走神儿了,或者由于噪音没听清,所以B未必最终“知道”了这件事。


计算机世界的例子

再比如A通过QQ, 微信,短信,chime等手段告诉B,“外星人要来了”这件事,在TCP层,一般来说当OS kernel收到TCP信息之后,把msg放在socket的buffer里就可以直接ack当前的TCP信息了,而当A端的TCP收到这个ack,那么A端程序的socket blocking call则可以返回。然而这并不代表着B的app一定已经从socket的buff里拿到并“处理”了这条消息(比如B的app正在忙处理别的信息,或者由于机器CPU忙,B的app没有拿到CPU分片)

可以看到,TCP的可靠,只是保证TCP层,两端的OS的socket实现把数据从一端“确定,可靠的”放在了另外一端的socket buffer里。如果在TCP ack之后,app从socket buffer拿数据之前,我们把B的app杀掉重启,那么即使TCP可靠的发送了信息,B也没有得知“外星人要来了”这件事。没有可靠的完成系统的高层语意。而由于A不确定B有没有得知这个消息,那么A就需要在app的层面重试。

现在很多通信软件可以在发送端显示“delivered”, 这一般是App层实现的ack语意,即B端的app真正从B的机器的socket buffer里把信息拿出来之后,处理完毕(比如记录在文件系统,在ui显示新消息,发出滴滴滴的声音,什么叫做处理完毕,取决于app的实现决定),然后B端的app给A端发送一条特殊的“我的程序已经收到这条信息”的application层的ack(这个app层的ack,则需要另外一次完整的TCP通信 B->A:tcp msg-> A->B:tcp ack),这时A端才能推理得知B的app一定已经处理了socket buffer,那么就可以在A端的app显示“delivered”。

可以看到,想要在app层面可靠的delivery信息,只能依靠app自己来实现ack,这是因为是app定义了什么叫“处理完毕”,所以也只有app自己知道什么时机,我们可以告诉A端“我已经收到你的信息,不需要重试了”。注意,在A的socket blocking write返回,到A真的收到B的app层的ack之前这段时间,对A来说,B处于“收到或者没收到信息”的叠加状态。A可以持续等,也可以重发信息。如果A选择等待,而如果B被kill了重启,取决于app的实现,A可能需要一直等待永远也不会发回来的app层的ack(一般来说都有app层的timeout)。而如果A选择重发,但是其实B已经收到了A的信息,只是由于B忙于处理别的程序,或者B的app层的ack在网络上延迟了。那么A的重发信息就是B的重复信息了。(可以看到,即使我们使用了已经会dedup的TCP协议,在高层还是会出现重复信息),这是由于对于TCP来说,它根本不知道先后两次信息代表的是一个意思。在app层,什么叫做“重复”,“相等”的信息,是由app定义的,TCP对此一无所知。

下边是最重要的。

即使A端的App显示“delivered”,“消息”也还没有可靠传递

系统设计应根据设计目的来认清,到底什么是端到端里的“端”(end 2 end里的end)。在我们的例子里,我们的目的是让B知道“外星人要来了”这件事。那么考虑以下没达到目的的情况:

  1. B的app收到信息了,ack A delivered了 => 但是B没看这条信息。
  2. app设计为当B的客户端ui显示在屏幕,向A发送第二次ack,告诉A“消息已被阅读”,A的app显示“read” => 看的人不是B,或者B误点了一下没看。
  3. A和B达成协议,当B收到A的消息之后,一定要回复一个只有A和B知道的密码并复制A发送的消息来确认消息接收。那么只有B看到这条信息之后才能进行以上操作,那么A才可以推理得出消息可靠传递了,通信目的达到了。

...

...

...么?

答案是没有,因为什么是“外星人要来了”这条消息是有歧义的,比如A是真的想跟B说“来自外星世界的人要来地球了”,而如果B曾经跟A买过一台“外星人电脑”,那么B可能理解为“他的外星人电脑”要送货到达了。如果AB想要更可靠的通信,则需要B用自己的话解释一遍A的话,做到端到端的理解,直到A在意识上确定,B“懂了”(在意识上接受完毕了这个消息)。这也是一般老师保证学生理解了教学内容的方法,或者沟通无比重要的事情的时候,发送端保证接收端在意识上“成功接受”的方法。

这说明:系统设计中,消息的“可靠”传递是需要上下文(context)的,人与人的交流的信息,只是传递信息的delta,而不是全部完整的信息(比如先递归的严格定义每一个词的意思,把定义和描述一起发送过去),因为这是最有效的通信。


然而,即使B可以用自己的话诠释A的话,消息也没有“完美”传递,因为A在理解B的话的时候也可能由于歧义理解错误而“误以为B已经懂了”; 所以,End 2 End Argument也可以一定程度解释人类世界的各种误解和纠纷。只有发出信息的人才“完美”理解消息的意思,而接收方只能根据“双方可能的共有context”来理解这个信息。但是由于人与人微妙的差别,所以只有发送方才能“完美”的给“自己”诠释消息的意思。甚至发送方的context也在根据世界的变化而变化。所以那个能“完美”诠释消息的自己,是上一秒的自己。(而时间越久,你的context和原来的自己就偏差的越大,所以你越来越不能“完美”诠释你之前发送的信息)


总而言之,简单的抽象来说,从系统分层的角度讲,高层的“可靠”,“顺序”,“重复”的概念和低层比如TCP的“可靠”,“”顺序“,“去重”不是一回事儿。系统设计应考虑高层系统语意,而以低层语意为工具,而系统的真实想要达成的意图比工具要重要的多。


本文夹杂了作者的很多私货和个人多年系统设计经验的理解,不过直到app层的ack那边,意思还是和End2End Argument的原作者一致的。想要原汁原味的论文可以戳下边链接。

http://web.mit.edu/Saltzer/www/publications/endtoend/endtoend.pdfweb.mit.edu


PS: 一定程度克服End 2 End Argument的方法也不是没有,我们可以反转中间件和app的角色,让中间件变成end,而用户的app变成inject进“中间件framework”的logic,Google的MillWheel和DataFlow就是很好的例子。见:阿莱克西斯:简单解释MillWheel: Google的internal Stream System

这,也即是Data Center As Database & Application as Database trigger的未来的系统构架方式,见:阿莱克西斯:评:Streaming System(简直炸裂,强势安利)(数据存在云上,来自于云,计算于云,结果储存于云,这样,云才是end,任何用户逻辑就变成了“中间件”)

编辑于 2019-02-02

文章被以下专栏收录