How Edison is helping us build a faster, more powerful Dropbox on the web

Dropbox如何重写核心Web服务栈,为未来十年做准备:逐步淘汰13年积累的技术债,将高流量功能迁移到一个面向未来的平台,支撑公司的多产品演进。

~ ~ ~

2021年秋,Dropbox悄然发布了核心Web文件浏览器的内部Alpha版本。这标志着Dropbox Web架构的一个重要里程碑:我们的文件浏览器成为了单页应用(SPA)。

单独看,这并不特别。SPA的概念存在近二十年,至少有一半时间已在Web上广泛使用。对于多数网站维护者,它几乎是基础要求。但Dropbox.com不同:这是一个庞大的产品界面,跨越数百个后端服务,拥有由数十个团队负责、层次深且复杂的功能。我们推出的不只是SPA,而是支撑它的一整套新平台。这套平台既要正确工作、性能优于旧栈,还必须让希望迁移的团队无须经历艰难重写。

在这样的大型系统及其技术债中工作过的人,都知道哪怕只重写有限部分也很困难。我们推出SPA架构,意味着完整重建Dropbox的Web基础:一个全新的Web框架、技术栈与平台,称为Edison。

Edison历经三年开发,解锁了大量性能与功能改进。它支持不到一秒的开发迭代周期、同构渲染(JavaScript同时运行于客户端和服务器),并使整个Web界面能够统一为动态SPA。这是核心Web系统的全面重写,逐步淘汰约13年的技术债。我们希望它成为未来十年的Web服务栈:既适合最高流量界面,也适合未来产品与功能的灵活平台。

Web对Dropbox意味着什么

“互联网上的事物可以变化得如此之快。”——Tim Berners-Lee

Dropbox于2008年推出网站,距Web发明已近二十年。当时Dropbox仍只是桌面应用,网站承担分发与营销任务。早期真正的核心,是运行在电脑上、默默同步数字生活的应用。我们的工程人才主要专长在同步、网络、分块和存储等基础设施,而不是Web。

随着时间推移,Dropbox一点点建立Web平台,从宣传网站变为账户管理器、文件查看器,最终发展成今天能够提供PDF编辑与视频协作等高级功能的平台。Web的广泛覆盖与普遍可用,让更多用户能够受益于这些能力。

如今,Web对Dropbox全部产品都至关重要:既支持新的文件工作流程,也组织只存在于云端的数据,而且越来越成为争夺用户价值的领域。我们必须拥有一个尽可能为产品工程师提供覆盖面与开发速度的平台。Edison体现了Dropbox如今对Web的重视,以及公司内部和外部文化的变化。

Dropbox的Web发展历程

与所有历史较长的产品界面一样,我们的Web代码经历过多次重大迭代。2012年,架构基于Pyxl——一种在Python中内联HTML的引擎,写过PHP的人会很熟悉。客户端前端大量采用CoffeeScript,后来它走向死胡同,推动我们迁移到TypeScript。我们使用过jQuery,随后弃用;也采用了React。

过去十五年左右,我们的发展轨迹很大程度上呼应了Web生态本身的演进,对同期构建网站的人并不陌生。

更有意思的是前后端集成方式的变化。总体上,我们随Web生态采用开源技术、服务端渲染与交互式前端,但也逐渐构建自定义基础设施,解决自身业务挑战。其中关键部分是自定义Web服务器,以及其中的优化。

这台旧Web服务器称为DWS。如果你在猜缩写代表什么,尽管猜——你大概猜对了。它在近十年间表现良好,支撑网站持续迭代。但随后不可避免地发生两件事:Web继续演进,我们也开始要求DWS完成设计范围以外的工作。

当Web产品能力扩大,DWS的限制给应用功能和开发效率带来难以解决的问题,主要分为两类。

问题一:Pagelet

DWS旨在让功能团队独立迭代。例如,一个团队开发导航,另一个开发侧边栏;它们应当无须持续跨团队协调,就能迭代与发布。DWS不仅让工程师独立开发,也让各自代码在前后端独立执行,确保一个团队不会阻塞或拖慢另一个团队。

为满足这些要求,DWS采用了pagelet架构。Pagelet是页面的一部分,例如导航栏或页脚。在我们的系统中,一个pagelet同时包括后端的数据获取、控制器,以及前端视图代码。

每个pagelet有一个Python控制器,获取所需后端数据,指定页面元数据,并提供带初始化数据的JavaScript入口。各pagelet并行执行,把获取的数据流式写入浏览器响应,HTTP连接在完成流式传输前保持开启。在DWS中,一组pagelet定义为一个路由。

浏览器把每个pagelet的入口模块作为独立React应用执行。准备就绪后立即初始化,并渲染到appshell中指定的HTML位置。Appshell是初始化页面、提供接收内容的根元素的基础HTML。随着各pagelet完成,页面逐渐拼成完整形态。

这种架构的优点

  • 复杂、多功能页面可以纵向切分为易于管理的部分。
  • 产品代码的边界和归属清晰。
  • 每个团队的产品可以独立于其他pagelet渲染。
  • 可以提前取数据。DWS并行发起后端请求,将结果流式写入HTML;JavaScript开始执行时,所需的数据获取已经完成。[1]

这种架构的缺点

  • 取数据指令在Python控制器中定义,但数据由浏览器JavaScript消费,两层必须人工保持同步,容易产生缺陷。也难以静态确认数据是否仍被使用,因此容易取过多数据,或在前端变化后忘记删除不再需要的请求。
  • 跨pagelet边界编写功能并不容易。

第二个限制尤其严重:网站无法发展成一个整体应用,更谈不上SPA。某个pagelet的JavaScript要与另一个交互,需要与负责团队讨论,共同设计消息API。这阻断了许多功能与可能性。

问题二:层之间混杂

设计过分布式系统的工程师都熟悉建立健壮、可组合、灵活接口的原则:职责分离、松耦合。我们面临的问题可以预料:DWS从Python内嵌HTML的系统演变而来,仍保留很多难以移除的服务器与JavaScript客户端耦合。

这意味着,即使工程师只开发几行JavaScript和CSS,也需要运行完整Web服务器栈。系统没有为模块化而设计,JavaScript应用层对服务端渲染数据有许多直接或间接依赖。脱离服务器执行JavaScript通常不可行;即使部分代码可以,也没有通用方法。

对于只有少量路由、几千行代码的项目,运行服务器很简单,很多项目用webpack-dev-server,十到二十秒即可启动。但Dropbox的规模使这种方式难以接受:需要在称为devbox的虚拟服务器环境编译并启动约100个后端服务。这可能需要20分钟,有时甚至翻倍。新入职的前端工程师,可能从“20秒在笔记本加载开发网站”,变为“需要专用远程服务器,等待二三十分钟”。

我们需要让工程师快速、轻松地运行JavaScript,而不必昂贵地构建后端或不断往返远程服务器。让单个模块脱离服务器运行是一回事;Storybook等系统也能测试部分功能,但它们只是局部补丁。工程师不能每次测试都重新研究如何运行代码。我们需要通用解决方案。

介绍Edison

解决Dropbox的Web客户端开发挑战并不直接。第一次渐进式重写始于2016年,在作者加入Dropbox之前,也早于常被拿来与Edison比较的开源项目Next.js首次发布。我们在渐进改进与基础性重建之间反复尝试,经历过几次失败起步和倒退。

回头看,必须满足的功能要求是:

  • 浏览器运行时是单一、统一的JavaScript应用。
  • JavaScript客户端无须服务器即可执行。

关键非功能要求是:

  • 易于迁移。重写全部代码,或让任何一个团队重写其代码,都不可接受。当时有近200万行TypeScript,以及约10万行处理pagelet的后端代码。这既是技术问题,也是组织问题:昂贵重写可能严重延迟新系统,甚至令其无法落地。
  • 性能相同或更好。必须匹配旧系统明确的优化路径,确保并行产品互不影响,取数据早于JavaScript初始化。这同样涉及技术与组织:某些场景下有限的性能倒退也许技术上合理,但受影响团队会反对。

到2020年,我们明确了必要结果:重新从基础设计无需服务器即可运行的客户端架构,以及能进行基于Node渲染的服务器。换言之,需要与现有服务和代码架构紧密集成的同构JavaScript应用栈。

结构

Edison由三个主要部分组成:

  • Edison Server,使用Go编写:通过Envoy代理层接收直接Web流量;把服务端渲染(SSR,[2])委派给Streaming Reactserver;通过我们的gRPC框架Courier获取数据;处理提前获取,并与浏览器中的Edison Client通信。
  • Streaming Reactserver:由Go包装的Node服务([3]),专门负责渲染React树;在渲染期间可以异步向Edison Server发送消息。
  • Edison Client,浏览器运行时:提供主浏览器入口;与Edison Server协作,满足提前取数据保证;较晚的数据请求直接与Courier服务交换;整个页面实现为一个React应用。

无需服务器即可运行,是Edison的关键设计。原则上,可以构建Edison Client和产品代码,让静态CDN提供它们。生产环境没有理由这样做,但这种能力使后续方案成为可能。

既然可以不依赖服务器运行,为什么还要服务器组件?Edison Server是栈中的关键部分:作为运行时加速器补充客户端,负责达到DWS设定的性能目标。下面看它如何做到。

Edison如何加速应用

前文介绍过,pagelet在请求生命周期早期并行处理数据需求,这是服务器必须保留的关键优化。它怎样带来提速?

假设查看文件预览:页面需要一个文件列表,以及覆盖其上的PDF预览。一个简化实现可能依次执行:

  1. 服务器对最初的 /browse 请求返回页面的JavaScript入口包。
  2. 入口加载后,浏览应用请求用户数据。
  3. 得到数据后,浏览应用判断还需要预览JavaScript应用。
  4. 相关数据加载后,页面完成渲染,用户看到内容。

时间线如下:

纯客户端JavaScript应用的顺序加载瀑布
纯客户端JavaScript应用的顺序加载瀑布

当依赖链这样线性执行,延迟很快累积到不可接受的程度。这里已大幅简化,生产应用可能有数百个以不同方式交互的模块。

服务器加速在这里发挥作用。Edison Server能够预先分析整个应用树,预判许多请求:

预加载模块和数据后的同一执行流程
预加载模块和数据后的同一执行流程

这种应用分析加速,依赖在React树服务端渲染期间,Edison Server与Streaming Reactserver之间的跨服务通信。收到请求后:

  1. Edison将路由解析到JavaScript入口模块,打包渲染入口所需的数据,发送给Streaming Reactserver。
  2. Streaming Reactserver开始服务端渲染。[4]
  3. 渲染时遇到取数据调用,具体为 Edison.prefetch,便异步通知Edison Server;后者立即发起数据获取,React渲染继续进行。
  4. 取数据完成后,无论React是否渲染结束,Edison Server都可把结果流式写入HTML响应,使数据在应用实际需要之前就已准备好。

这样我们获得了与DWS相同的性能优势。更好的是,取数据请求与需要它们的代码位于一起,全部应用逻辑也集中在系统同一层。

迁移到Edison

在Edison之前,我们分阶段将Web页面API从Python与Pyxl逐渐迁移到JavaScript。这些可管理的步骤,让团队能够渐进升级,并在途中获得收益。

Edison旨在无缝延续这一趋势。发布时,已跟上旧栈演进的团队拥有:负责加载数据并提供初始化数据的Python pagelet执行器,以及接收数据并渲染产品的JavaScript应用代码。

迁移只需相对可控的调整:一个数据servicer(可用Python、Go或Dropbox支持的其他语言)加载数据并通过gRPC返回初始化数据;一组调用 Edison.prefetch 请求servicer的JavaScript应用代码。

多数情况下,工作仅是重构一个Python文件,用Servicer API替代Pagelet API重新封装,再增加JavaScript入口模块,包装原有逻辑并执行预取。关键是,团队可以:

  • 渐进进行,仅向内部流量开放Edison版本。
  • 不复制代码,因为前后端核心代码都能共享。
  • 有充分预期在迁移一开始就获得功能对等。

要求团队做多次渐进重构,需要时间与组织投入,但非常重要。Edison准备发布时,主要Web服务的迁移成本已经大幅降低。即使大型页面也能使用相同JavaScript同时运行于DWS与Edison,使渐进发布易于实现。

已解决的问题

  • 将全部产品代码移到TypeScript层,拆解了混杂的应用层。
  • 依赖服务端React渲染过程中启动的异步gRPC调用,保留提前获取数据的加速能力。
  • 把取数据的两个事实来源合并为一个,使维护更容易、代码更清晰。
  • 保留独立产品模块(原来的pagelet)对后端数据提供者的控制,并允许在所需初始化数据返回后立即执行JavaScript。
  • 整个应用成为连贯的React树,工程师可以轻松编写跨原pagelet边界的功能。
  • 重构为纯React的路由,可以同时运行在DWS和Edison上,并为安全而渐进迁移。

还剩下什么?最后一块是开发效率。在DWS中,执行任何JavaScript都要运行完整栈、超过100个服务。在Edison推出的这个阶段仍然如此。下面介绍它让我们能够进一步做什么。

在Edison之上继续构建

把工程师迁移到Edison界面,带来两件事。首先,可以立即围绕单一、连贯的React树开发,也就是开头所述的SPA迁移。其次,正确拆分客户端与服务器层后,客户端不再必须由Web服务器提供,这使大量开发流程能够简化。

SPA迅速发展

SPA并非即时万能药,也不一定是架构终点。对于许多以静态内容为主的网站,更轻量的客户端就能解决同样问题。但Dropbox的核心功能是文件上传、移动与操作,Web产品本质上需要像完整的图形文件管理器一样工作。把用户操作与页面状态从界面导航中解耦非常重要。

具体收益包括:

  • 文件树导航更加直观,例如可以把文件拖入侧边栏;此前这两者是完全独立的React应用。
  • 可以在原本独立的界面间提供视觉过渡,例如文件夹导航,帮助用户理解变化,降低认知负担。
  • 用一次API调用完成页面导航,而不是承担完整页面加载的全部成本,改善性能。

随着这部分产品成熟,SPA值得单独写一篇文章。这里先继续讨论客户端与服务器拆分,以及称为Edison Localmode的开发效率改进。

Edison Localmode

客户端无须Web服务器即可运行后,我们可以:打包源代码并部署到CDN,消除相应基础设施;将应用部署到Electron,形成统一跨平台代码流水线;从工程师笔记本直接提供完整Web客户端,实现快速迭代。本文最关注第三项,因为它解决开发效率问题。

Localmode的目标很简单:Web客户端开发者只需运行Web客户端。开发JavaScript和CSS时,不应不断触碰Web服务器或后端服务,而应在纯客户端代码库中工作,通过API与Dropbox其他系统交互。

Localmode是一个可选择开启的系统,直接从工程师开发笔记本提供全部静态资源(JavaScript和CSS)。工程师照常从Dropbox Web服务器加载页面,该服务器可为自己的开发服务器或预发布环境;然后启动一个轻量的本地Node资源服务器,并开启Localmode。直到关闭之前,全部静态资源都由本机提供。

工程师随后不必再操作服务器。本地轻量服务器监视文件系统,实时重新转译代码([5]),直接补丁更新正在运行的页面,让改动立即出现在工作界面中。

这件事容易描述,却极具改变意义。对比如下:

以前

  1. 工程师修改并保存源文件。
  2. 手动运行命令重新加载devbox,同步文件、重建并重启服务。
  3. 等待15至30秒,切回浏览器刷新。若代码位于交互流程深处,例如表单第三阶段,每次刷新都要手动重新建立状态。
  4. 页面加载后检查代码是否正常。
  5. 重复上述过程,直到完成功能。

现在

  1. 工程师修改并保存源文件。
  2. 还没来得及切回浏览器,代码就已经转译并运行在页面中,状态完整保留,不需人工干预。

对于此前每隔几分钟就重新加载devbox的工程师,直接节省的时间十分可观。如果每15分钟重载一次,按保守的重载速度估计,每小时可能节省一分钟;每天工作六小时,全年大约少浪费四个工作日。

还有难以量化的收益:等待30秒时,工程师往往查看Slack消息,被打断心流,浪费更多时间。即时反馈让他们更连续地保持专注,加快修改速度。

这对开发效率与满意度都有很大帮助。过去devbox前端开发令人沮丧;Localmode让原本繁琐的工作重新变得迅速、令人满足。

未来

Edison是Dropbox多方面积累的结果。随着Web越来越重要,人才结构也从主要是后端工程师,扩展到一支强大的全栈Web工程师队伍。没有这种文化变化,就不可能有Edison。

开发Edison也让我们更理解自身系统的约束。它揭示了DWS很多隐含假设,使它们能够被理解和隔离。为此,需要渐进调整API及使用它们的代码。这让我们更清楚,未来与开源生态对齐应该是什么样,以及需要做什么。与开源对齐能降低对专用工具的依赖,进一步提升效率,我们仍在继续努力。

同时,Edison已经成为产品开发的重要加速器。在DWS中难以实现或需要漫长项目的任务和功能,如今变得容易;此前充满打断与延迟的开发,也变得顺畅、连续。它让我们更自由地思考Web产品应该是什么,构想更接近前沿的Dropbox.com。

如果创新产品、体验和基础设施令你兴奋,欢迎与我们一起构建未来。访问Dropbox招聘页面,了解职位;也可在Instagram和Facebook关注@LifeInsideDropbox,了解在这里创造更清晰工作方式的体验。

~ ~ ~

注释

  1. DWS早期并行获取数据,是Dropbox的重要性能优化,且早于我们能够使用JavaScript服务端渲染。当时,朴素架构可能要求前端自行取数据,让可交互时间增加数百毫秒。Pagelet有问题,但提前并行加载数据,以及能够管理这些数据的客户端运行时,并不是问题所在。
  2. 服务端渲染指服务器在初始响应中提供页面内容的完整HTML,让浏览器在JavaScript应用加载执行前就能渲染,改善性能;也让机器人和搜索引擎无须执行大型应用即可读取文字。我们的文件操作应用不这样做,因为页面仅对认证用户开放,而且没有JavaScript交互时HTML作用有限,收益不大。基于CMS的公开页面与Dropbox Paper等其他部分使用SSR。
  3. Edison Server及Node外围包装层都用Go编写。Go很好地支持并发,使高度并行过程容易理解;性能高,也与Dropbox服务生态紧密集成。
  4. Streaming Reactserver的后端渲染可用于加速,也可提供服务端HTML。我们并非总使用服务器HTML,但对某些页面很重要,尤其是依赖搜索引擎可见性的页面。
  5. 转译是把TypeScript和CSS Modules源代码转换为发送给浏览器的形式。过去这系列转换必须在devbox上执行,现在也能在本地完成。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容