现代网络在为您带来实时实时Web应用程序方面做得很好。还是呢?这篇文章探讨了当前最先进的Web体系结构中缺少的内容,应该改进的地方以及我们拥有的哪些工具。
传统的Web体系结构需要将DB,服务器和浏览器与RPC和REST调用结合在一起:
但是,如今的JS擅长完成我们传统上希望服务器提供的几乎所有功能。 JS可以处理视图逻辑(比服务器更好),JS与数据库对话,执行查询和解析结果集都没有问题。
当您仅编写服务器代码来满足客户端对数据库的要求并将结果传回时,您可能已经有这种感觉。感觉很蠢。感觉很多余。所有这些趋势-同构代码,js编译语言,node.js-源自在两个地方运行相同代码的愿望。这个目标是错误的。您不想在两个地方运行相同的代码。您可能需要,但仅是要处理不良(旧)体系结构的后果。两次运行完全相同的验证不会使数据更有效。
用于直接与数据库对话的UI应用程序。然后介绍了瘦客户端;为了支持这一点,已将中间智能代理或服务器添加到了Web堆栈中。如今,客户又变多了,但是我们一直将服务器放在中间。只是出于习惯。
不,最终DB将直接与浏览器对话。软件可能还不存在,但这只是时间问题。在此之前,请记住:现在,服务器是通用组件,而不是浏览器。客户端逻辑处于控制之中。 JS发出命令,服务器/数据库跟随它们。
即使我们可以摆脱服务器的限制并在浏览器中处理所有内容,我们仍然处在一个临时的,不一致的,总是迟来的Web应用程序的时代。
如果您将此页面保持打开状态几分钟,将会发现页面的不同部分具有不同的年龄。有些是静态的,除非您刷新页面,否则它们永远不会更新。有些几乎立即更新。有些会定期刷新。即使您在页面上查看相同的数据段(例如用户名),也可能在不同的地方看起来有所不同,具体取决于上一次更新包含它的组件有多久。 Facebook是一种现代的,先进的Web应用程序,但它还不够实时。除了页面加载后的几秒钟,几乎所有时间,每个Facebook页面都是陈旧且不一致的。当然,这与Facebook无关,每个其他Web应用程序的每个其他页面也都是陈旧的。尽管有嗡嗡声,但实时网络尚未登陆。
有些人可能会争论是否需要完全解决。我可以不定期刷新页面。我不会经常更改用户名。我们热爱人们的Facebook,并且可以承受一些技术怪癖。
我同意。对于绝大多数人来说,这可能不是当前最紧迫的问题。但是人们习惯的方法总是不好的。如果人们学会了适应某些事物并不意味着我们应该停止寻求改进。调查疯狂和似乎无法到达的地方可能仍会为我们提供有用的见解。
Web技术堆栈的创建是基于人们正在查看很少更改的,主要是静态数据的假设。通常,它在随手加载丢失的数据片段方面做得不错。投入AJAX / WebSockets和定期更新,最好的是将网页从不同时期的数据片段中拼接出来。可以预料的通过使用HTTP,JS,SQL,REST的最自然的方式自然可以得到此结果。为了超越此范围,我们需要忘记对它们的了解。我们需要避免短而众所周知的路径。我们不应该害怕做不自然的事情。
我们真正想要的甚至不是请求-响应协议。在较高的层次上,我们希望尽可能紧密地连接数据源和客户端,而库则负责所有协商细节。这些是我们感兴趣的东西:
数据的一致视图。我们正在查看的内容应在某个时间点保持一致。我们不希望在某个地方拼凑静态数据,而在另一个地方拼凑一下陈旧的数据,而又不想在整个地方都有新鲜的稀有数据。人们一次就能看到整个页面。一致性消除了人们看到的矛盾的任何可能性,一致的应用看起来很理智,并建立了信任。
总是新鲜的数据。我们在客户端上看到的所有数据现在都应该是相关的。一切尽在最细微的细节。理想情况下,包括所有资源,甚至是运行该应用程序的代码。如果我上传了一个新的用户图片,则希望将其重新加载到当前人们可能看到的所有屏幕上。即使它显示在一秒钟长的自定义通知弹出窗口中。
即时响应。用户界面不应等到服务器确认用户的操作后再进行。动作效果应立即显示。
处理网络故障。网络不是可靠的通信设备,但是可以在它们之上构建可靠的协议。网络故障,数据包丢失,连接丢失,重复项均不应破坏我们的一致性保证。
离线。显然,数据不会是最新的,但是至少我应该能够进行本地修改,然后在重新联机时合并更改。
无底层连接管理,重试,重复数据删除。这些都是乏味的,容易出错的细节,带有细微的差别。应用程序开发人员不应手动处理这些问题:他们将始终选择易于实施或快速实施的方式,从而牺牲用户体验。底层库应注意细节。
无论其技术可行性如何,这些都是有价值的目标。在未来的几十年中,我们可能会,甚至可能还没有看到其中的一些研究,实施或什至没有成为行业标准。我们可能会看到许多失败的方法,并且(希望)有一些成功的尝试来解决这些问题。然而,目标将保持不变。
这篇文章的其余部分是关于从何处开始寻求网络圣杯的推测。我没有所有的答案。而且(剧透警报)我不知道如何使Facebook变得实时。但是我们必须从某个地方开始。
顶部的大数据源是我们在项目中拥有的所有数据。在到达客户之前,它必须通过两个过滤器。第一个是安全过滤器。它过滤掉所有用户无权查看的数据,只留下个人,共享和公共行。第二个过滤器仅保留用户感兴趣的部件。对于UI,它表示呈现当前页面所需的部件。
通过这两个过滤器的所有内容均应立即实时发送给客户端。根据定义,这是客户端呈现页面所需的一切。
这里有两个技巧。第一个是效率。网页通常非常复杂,它们可能会跟踪数百个不同的对象,它们可能会跟踪复杂查询的结果,它们可能会跟踪聚合。而且我们可能同时拥有数千个实时客户端。
这是Future Web领域中最未开发的部分。您可能习惯于用查询来描述数据需求。查询是一种从存储中获取数据的方法。为了获得实时,我们需要这些查询以其他方式进行工作。客户仍然通过查询定义其需求。像往常一样,此查询可用于初始数据获取。然后,将使用相同的查询来过滤整个数据库的变更日志,并确定服务器应将哪个部分推送到哪个客户端。提取是关于尝试获取给定查询的数据。推送是关于根据更改的数据查找受影响的订阅。
我们这里需要的可能是一种新的查询语言,例如可逆SQL。我们需要我们假设的ReversibleQL在两个方向上高效运行。为了获得这些属性,ReversibleQL可能必须比SQL更简单,更严格。或者可能是两种不同的语言。我不知道。 Meteor.js通过仅限制对文档和集合的订阅来解决此问题。 RethinkDB的人们正在尝试建立透明可逆的查询,但是他们对内部细节不是很热衷。我曾经在一家初创公司工作,该公司提供实时查询结果来为ESPN,WWE,CNN,Scripps和Washington Post提供评论。我不会说这是在公园里散步,我们必须为此建立定制的基础架构,但是它肯定可以在50K RPS的规模以及各种用户指定的简单查询中实现。我们只需要像这样的开源。
第二个陷阱是过滤器和订阅会随着时间而变化。 (通常)这不是性能问题,而更多是组织问题。您如何跟踪订阅?如何检测死者以及如何收集垃圾?在客户上,我们可以受益于一个尚未得到充分称赞的React功能:组件生命周期。通过监听didMount和willUnmount,我们可以可靠地跟踪组件(及其订阅)何时进入和离开。这样一来,我们就能始终确切地知道访客在看什么。但是,在服务器上,这只是超时,重新计数,定期清理和仔细的编码。这里没有火箭科学。
虽然我们仍处于“从数据库到客户端”的数据流中,但我们还是来谈谈可靠性。实时可以,但可靠的实时则更好。如果您不能充分交付其中的一些数据,那么推动数据更改毫无意义:没有一致性,结果将不会那么好。在重新连接过程中(未进行完整性验证)从changefeed中丢失的单个DELETE可能导致灾难性的UI故障。出于相同的原因,重新排序也是毫无疑问的。这是分布式数据库的一个区域,浏览器在其中充当同级对象之一。例如,DB可以提供高度一致的事件日志,而DB客户端同步协议可以利用这一点。或者,我们可能选择最终的一致性,CRDT和反熵措施。无论如何,如今分布式计算非常拥挤,假设我们知道如何做得很好。传统上,浏览器不被视为对等体(尽管CouchDB正朝着这个方向发展),但实际上它并没有改变。仅这次,网络拆分不仅仅是真实的,而是日常工作。
现在,我们谈论的是监听更改,而不是启动更改。当然,应该捕获每个本地操作,将其转换为更改/增量,放入某种队列中,然后在后台发送给服务器。即使使用当前的Web堆栈也很容易。
这里缺少的地方是滞后补偿和离线工作。没有什么比没有响应的输入引起更多的烦恼和沮丧。网络太慢了,即使人在线并且连接良好,我们也应该补偿往返费用。这就是为什么我们在将所有用户操作发送到服务器之前立即显示所有用户操作的效果的原因。
离线是滞后补偿的一种特殊情况,其中滞后是无限的。离线模式应看起来像没有实时更新的普通应用。无需区别对待:我们只是认为增量队列可能会无限期增长,并使本地变更的过程可持续。
集中的,应用程序范围内的状态管理在这里至关重要。我无法想象如何在每个变量的基础上临时实现这样的事情。但是,如果您将所有应用程序状态都视为某种类型的存储(例如中继存储,嵌套不可变字典或DataScript DB),则可以在系统级实现同步。您还需要明确的变更管理,以通过网络发送增量,跟踪增量,将其保存在本地存储中并将其应用于本地状态。
这更像是一篇“使您进入正确的思维模式”的文章。我不知道任何基于这种架构的应用,甚至没有概念证明的例子。没有现成的框架可以立即使用。两年前,您甚至很难独自组装这样的东西,因为没有具有所需属性的软件。
RethinkDB的目标是相同的,但是缺少客户端存储和可靠的推送。
我们有具有本地存储和滞后补偿功能的Relay,但是没有来自服务器的实时推送(他们正在考虑在将来添加它们)。
Meteor.js更加紧密地结合在一起:它解决了一个简单的小型无序JSON文档集合的简单讨论的问题。它具有延迟补偿,本地存储,服务器推送,订阅,服务器数据过滤。我找不到有关其DDP协议的一致性和可靠性保证的信息,也找不到有关服务器推送随着订阅数量的扩展程度的信息。
我个人看到使用Clojure,Datomic和DataScript构建这样的系统的绝佳机会:
Datomic是通用数据库,它还维护所有事务的日志。单调的事务ID使可靠的同步成为一项轻松的任务(日志复制)。
Datomic具有反应式事务队列,它是公共API的一部分,这意味着有效的反应式推送无需轮询或复制日志解析。
Datomic具有类似于RDF的数据模型(datom =实体,属性,值),对于安全性和订阅过滤器而言,其粒度级别很高。 Datom是可以同步的最小信息,而这正是Datomic数据库的组成。
在客户端上,我们具有模仿Datomic API的DataScript,因此从本质上讲,它是相同的数据库,具有相同的数据模型,并且两者可以很好地协同工作。
Datomic和DataScript具有可序列化的事务格式,无需专门发明任何代表增量的内容。
DataScript是不可变的,这意味着我们可以保留本地更改,应用/撤消它们,重新排序,丢弃和临时构建临时本地数据库。此属性对于DataScript来说是非常基本的,因为它具有不变性,这意味着没有隐藏的限制或对某些更改的部分支持。
响应式UI通过具有自上而下的React渲染的DataScript而言非常简单。如果您的用户界面既大又复杂,并且需要根据数据库中的更改进行详尽的更新,那么它也非常简单,尽管目前没有现成的库。
订阅语言是此堆栈中最大的缺失部分。 Datomic和DataScript所说的Datalog是一种非常强大的语言,但是很难有效地反向。如果您要通过数据库进行的交易量非常大,则必须确定每个交易应向哪些客户进行更新。每个客户端都不能运行数据日志查询。
网络仅与当前构建它的工具一样好。正确的工具来自正确的目标。如我所见,最终网络将按照完全被动的原则工作。这篇文章列出了未解决问题的地图,并讨论了解决这些问题的可能方法。
我还创建了开源的东西:Fira Code,AnyBar,DataScript和Rum。如果您喜欢我的工作,并希望及早获得我的文章(以及其他好处),则应该在Patreon上为我提供支持。