Transcription
好的,我们开始直播了。大家好。我是内特·伯克佩克,今天我的嘉宾是戴维·希尼米尔·汉森,我认为对于这个观众来说,他不需要任何介绍。谢谢你来参加并同意做这件事,戴维。哦,当然。我的荣幸。那么,对于刚收听的任何人,只是为了提供一些背景信息,这次采访是我为“Rails性能完全指南”进行的六次采访之一。虽然这次是公开的,出于显而易见的原因,其他五次采访仅供“Rails性能完全指南”的购买者观看,这是我写的一门关于Rails应用程序性能的课程。所以,你可以通过访问railspeed.com来查看其他采访。采访对象包括Sidekick的Mike Pam,Active Record的维护者Sean Griffin,以及许多其他很棒的人。所以,网址是railspeed.com。好了。那么,你从像框架的作者这样的人开始,从哪里开始呢?我想我会从,嗯,让我们谈谈Basecamp。当人们问我关于Rails性能或者他们的应用程序性能时,我经常会讲的一个故事,或者说我猜想我经常会讲的一个故事,是我从你们那里听到的,那就是对杰夫·贝索斯对你说过的话的转述,他说,十年后没有人会醒来希望他们的应用程序运行得更慢。你以前说过,速度是一项功能,所以我想知道你在Basecamp是如何执行这一点的?比如,速度是一项功能,它在你的组织中是如何实现的?比如,人们是否会提交像bug报告一样的东西,当他们认为某个东西太慢时?有没有一个标准,比如,好的,生产环境中任何比100毫秒慢的东西,我们都会将其归入bug堆?在Basecamp是如何实现的?我认为这实际上分为两部分。第一部分是[音乐]在开始开发时就将其放在首位,你如何设置一切,对我们来说,这已经有很多事情了。就服务器端性能而言,俄罗斯套娃缓存策略可能是最重要的。我们自2012年Basecamp的最后一个新版本以来就一直使用这种策略,当时我确实开始更加“军事化”地追求快速的页面响应,这在很大程度上是为了确保每一次页面更改都感觉良好、快速且现代。当我们2012年推出新版本的Basecamp时,肯定有一个转变,对吧?早期版本是2003年的,我们对其进行了修补和扩展等等,但它诞生于一个不同的时代,当时只是说,嘿,你可以有600毫秒的服务器渲染时间,这也不是什么大问题。然后到了2012年,你就不行了。如果仍然这样做,它就不会感觉像一个好的现代应用程序。所以那时我们想出了很多缓存策略,对于不知道俄罗斯套娃缓存的人来说,它基本上是缓存中的缓存,并且尽可能少地过期。所以,如果你有一个完整的页面,它可能包含50个不同的缓存。你改变了其中一个元素,你可能会过期其中50个缓存,从而仍然重用其中的55个。然后,这种策略使我们达到了Rails的那个神奇数字,大约100毫秒,因为毫无疑问,如果没有缓存,Ruby和Rails无法让你达到10毫秒、20毫秒,甚至100毫秒,如果你做足够复杂的事情。如果你想到Basecamp页面这样的东西,我们的一些页面确实有数千个数据点,比如数千个小信息。也就是说,如果你在没有任何缓存的情况下运行整个页面,我很久以前在一个页面上做过,我看到了850个SQL查询,这听起来很疯狂,对吧?但这本身也是一种深思熟虑的策略。其中一项深思熟虑的策略是,像N+1这样的东西是一种功能,而通常被视为bug,对吧?如果你有一个N+1查询,它意味着你每个元素执行一个SQL查询。所以,如果你有五个块或者50个收件箱中的电子邮件之类的,那就是50个SQL调用,对吧?这听起来像一个bug。嗯,在俄罗斯套娃缓存设置中,它不是一个bug。它是一项功能。因为这些单独调用的好处是它们在自己的时间线上被单独缓存,并且它们非常简单。因为绕过N+1的整个方法是进行连接,进行更复杂的查询,这些查询需要更长的时间来计算并给数据库带来更大的压力。如果你能简化这些查询,使它们非常简单,但数量更多,那么你就赢了。前提是你有一个能够支持它的缓存策略,对吧?所以,例如,如果你说你有一个收件箱,里面有50个项目。第一次,嗯,甚至不是第一次。你可以想象,哦,第一次渲染它,它会触发50个调用。不,不会。第一次渲染它,它会触发一个调用,因为里面只有一个元素。比如,这是拥有关于缓存如何工作以及何时过期的长期视图的一部分。因为如果你正在缓慢地构建这个缓存,最终你会得到50封电子邮件在那个收件箱里,是的,如果你清除了所有的缓存,你就会受到50次的打击,但你是逐步构建的。所以每次有人看到那个页面时,你只执行了一个额外的查询或两个额外的查询,因为你一次只做一个。我用收件箱举例,但一个更好的例子是Basecamp中的聊天。我们有N+1的聊天。比如,如果你冷启动Basecamp 3的聊天页面,它会触发我不知道的数百个SQL查询,对吧?因为它从不做,因为这些缓存是逐步构建的,所以你永远不会支付全部费用。至少用户永远不会看到全部的费用。如果他们看到了,如果你不得不清除它们,好吧,那一次,好吧,页面花了1.5秒渲染。因为如果你使用俄罗斯套娃缓存策略,并且采用“我每个控制器只做一个SQL查询”的方法,这将是一个很大的事情,它将连接三个或四个表,并包含五个或六个模型,对吧?你每次都要承担这个成本,无论有多少缓存是热的。确实如此。这真的很令人兴奋。我老实说一直这样想。这可以说是我们正在使用的这种方法的一个重大启示。而且,当我看到人们说,“哦,Rails不积极执行SQL查询是一个真正的问题,它们在控制器中被积极执行,这样我就可以看到它们在哪里。”你在说什么?我不想执行任何会被俄罗斯套娃缓存捕获的SQL查询,对吧?而且,你设置的许多查询有时根本不会被执行。我一直在Basecamp的控制器中设置查询,我期望它们不会运行。它们被设置为延迟执行,只有在某些东西过期时才会运行。所以,例如,如果你看标准的Basecamp,然后你说你去了消息索引之类的。这些消息都会将它们的过期信息冒泡到项目。所以它们都会,如果一条新消息进来,它会触及项目。所以项目是关键的根过期点。如果项目过期了,比如我们重新加载页面。这是一个简单的例子,还有一些其他的考虑因素。但我想说的是,如果那个项目没有改变,我不想执行“选择所有来自消息,其中项目ID等于5”。我想尽可能延迟它,这样就有很大的机会它会被包含在俄罗斯套娃缓存中,从而永远不会被执行。所以,这就是Rails从许多不同的角度支持许多东西的地方,也是为什么如果你孤立地看待这些东西,只看“哦,为什么Rails不惰性执行查询”,这样我就看不到控制器中的性能时间。你有点错过了重点,你错过了我们为什么这样做的重点,对吧?你也可以,对吧?这是我发现的另一件有趣的事情,一些批评是,你只需要添加.load。如果你想要积极执行,那就添加到所有东西中,它将在你声明或运行它的地方执行。总之,回到主题,我深入研究了Basecamp的性能以及我们如何执行它的一些非常细微之处。这就是我们执行它的方式。我们试图从各个角度考虑它。然后我们试图将其融入到由多个支柱支持的架构中,这样我们就可以依靠使用Rails和Ruby来处理拥有数千个数据点并以这种方式设计页面的事情,并且仍然获得低于100毫秒的响应时间,因为我后来意识到性能的另一件事是,有性能,然后有“足够好”。我看到很多“你好,世界”的基准测试,它们是“哦,我在纳秒内生成我的‘你好,世界’”。谁在乎呢?你遇到过客户说:“哦,是的,你的页面在100毫秒内渲染,这太不够好了。我需要纳秒。”从来没有人要求过这个,对吧?所以,这就是性能策略非常受我们业务目标的影响。我们希望达到一个用户无法区分100毫秒和一纳秒的程度,通常他们无法区分。网络和客户端以及所有其他因素太多了,这些东西都会被吞没,对吧?这就引出了第二点。所以Basecamp 2,也就是2012年的模型,非常注重服务器端优化。这就是我们从可能需要400毫秒的页面,然后通过这些缓存策略将其降低到100或80毫秒或其他什么。我们认为太棒了。我们为什么认为太棒了?因为那是我们测量的。所以我们测量的是,哦,天哪,我们将这个页面的性能提高了4倍。嗯,再次,用户根本不在乎你在服务器端生成东西需要多长时间。他们在乎的是,从他们点击按钮到他们获得下一个页面以及他们需要的所有信息需要多长时间。嗯,这又回到了服务器端是其中的一个组成部分,而且很重要,但也许不是最重要的。最重要的是你的其余HTTP管道。你的其余设置是你的JavaScript。是你的CSS。是你的CDN设置。是所有那些其他东西,对吧?我们在2012年没有像我们最近发布的Basecamp,BCam 3那样努力地关注这些,因为我们没有测量它。很多人也没有测量它,因为它有点难。如果你只是设置了Skylight或其他性能测量工具,它们通常专注于服务器端,这很好。这很容易做到,而且你应该这样做,因为你应该知道,如果你遇到大的峰值,你应该做些什么。但它只提供了一个非常不完整的画面。当我们开始测量客户端的完整请求时,我们发现有时我们的页面在服务器端需要100毫秒来生成,但仍然需要一秒半才能完成整个更改。为什么会这样?哦,因为我们注入了一个非常低效的jQuery插件来做一个[ __ ]下拉菜单,初始化需要200毫秒,对吧?然后你说,“等等,什么?我花了这么长时间来建立这个俄罗斯套娃缓存的宏伟架构,以达到100毫秒,然后我花了双倍的预算在一个该死的jQuery插件上做一个下拉菜单。”你是在开玩笑吧。直到我们开始测量这个,我们才发现这一点。所以,今天的Basecamp 3,在员工使用时,角落里有一个小跟踪器,它测量这个东西从头到尾需要多长时间。不仅仅是服务器端,而是整个请求,所有的初始化,所有需要运行的JavaScript,一切,对吧?这也是我们真正拥抱Turbolinks的部分原因,因为一旦我们开始看到这些变化,对吧?比如,我认为这是早期Turbolinks的反对和可能的误解的一部分。人们很难衡量它。所以他们认为,嗯,可能没关系,因为我的服务器端时间仍然看起来一样。为什么我要费力去用Turbolinks,当它确实有一些,有趣的是,与jQuery插件的不兼容性,这在很多方面实际上被证明是一项功能,而不是一个bug,因为它要求你实际检查这些jQuery插件并找出它们到底在做什么。在很多情况下,你会发现你可以用100行JavaScript写出你刚刚拉进来的4000行jQuery插件。所以,这有点跑题了。核心概念是,我认为一个粗略的经验法则,我应该在引用它之前检查一下,但我还是要引用它,那就是,假设我们服务器端在100毫秒内完成,那么使用Turbolinks,整个请求大约需要300毫秒,这是我在特定线路上的特定延迟等等等等,免责声明,免责声明,免责声明,然后如果我们没有Turbolinks,同样的服务器端设置将需要900毫秒,对吧?显然,900毫秒已经超出了可接受的范围,感觉快速、现代等等。300毫秒从头到尾,在真实的西班牙连接上,我认为我就是这样做的,这非常好,感觉很棒。所以,这是我们对Turbolinks的热情的一部分,以及多平台开发和原生等等。总之,我可能应该让你提问了,因为我从一个无辜的挑衅中咆哮了大约15分钟,关于你如何处理性能?嗯,这是一个广泛的问题,旨在引发这样的回应。所以,我想人们已经注意到,你可以在右边提问。我正在看那些,其中一个问题是我在听你说话时也想到的,那就是,你显然,当你描述这种故意编写N+1时,当你有俄罗斯套娃缓存策略时,你显然是假设了,你知道在你的后端,你能让这些缓存保持多久的温暖,对吧?比如,我们经常被程序员告知,缓存是个好东西,但我们不应该假设每次去访问缓存时,它总是在那里,对吧?所以,是的,你显然在后端做了一些事情,比如,你知道,你比大多数人做得更多,以长期保持缓存的温暖。背后并没有太多意图。我记得2012年的时候,我发了一张照片,成为当年被转发最多的博客文章之一,那就是“1万美元能买到多少内存”,上面排着模块之类的。所以,电脑太便宜了。内存便宜得离谱。如果你经营一家真正的企业,我的定义是产生金钱的企业,并且你以市场价格雇佣程序员,对于我构建的那种应用程序来说,大多数计算支出都是微不足道的。我在这里设置了所有免责声明,以便做出一个不可辩驳的声明。当然,如果你想做WhatsApp,或者服务十亿人,并且基本上向他们收取很少的费用,每年一美元。是的。好吧,你有不同的担忧,而我没有。这难道不很棒吗?我们可以为不同的目的使用不同的工具。我争辩并认为,Basecamp作为一个应用程序原型,代表了世界上大多数应用程序将要看到的绝大多数的规模、性能吞吐量等等。有一些应用程序远远超过了我们在Basecamp所做的。我们没有做互联网规模或网络规模,我猜那是这个词。我们没有做Twitter、Facebook、Instagram、WhatsApp等需要处理的网络规模。你知道还有什么吗?几乎没有人这样做。比如,需要处理十亿用户,甚至一亿用户,甚至一千万用户的用户群,这是一个幂律。大多数人根本不需要处理这些问题。当他们试图从服务十亿用户、每年一美元、以高性能和盈利的方式所需的工具、技术和框架中获得灵感时,他们就会误入歧途,因为这些工具与他们正在做的工作无关,也不适合他们。这就是为什么我如此热衷于推广Basecamp作为一个原型,因为我认为它覆盖了更广泛的范围,并涵盖了更多的用例。因此,我相信我正在构建的工具,使我能够构建Basecamp,适合更多的人。是的。而且我们在这里使用的性能技术尤其适合更多的人。那么,你如何处理冷缓存呢?比如,你有一个备用的内存缓存实例放在某个地方吗?比如,如果一个坏了,你如何避免完全冷缓存的情况?是的,我们有多个服务器,所以没有单一的缓存故障点,这没有意义。我的意思是,内存缓存,而且我认为我们现在实际上正在使用Redis进行缓存,你可以将它们设置在分布式集群中,这样任何一台机器的故障都不会是灾难性的。但我也发现有趣的是,事情也在以其他方式发展。比如,如果你确实需要加载冷缓存,事情会有点慢。比如,我们已经超额配置了,但并不是离谱的程度。我们仍然可以处理偶尔缓存元素会丢失的情况,这根本不是问题。而且,也许在2005年或2008年左右,这些事情就不再是这样了。我真的不在乎2008年发生了什么。我关心的是2016年对我的业务和我的范围来说是什么。而且,我认为事情只会朝着这个方向发展,这就是为什么我总是对性能讨论感到困惑,尤其是当它们对比和比较框架时。就像,伙计,2003年,你知道内存和CPU花了多少钱,有多复杂,当时的Ruby有多慢,当时的Rails有多慢?就像我们又向前推进了13年,成本只是暴跌,而Ruby和Rails的性能实际上在很多方面都飙升了,而上涨的是程序员的成本,这是唯一重要的变量。比如,如果我看看我们在Basecamp上在程序员、工资、福利和医疗保健上的支出,你认为我会在乎一个额外的1万美元的内存缓存服务器的成本吗?我根本不在乎,因为它只是微不足道的。我们还有另一个问题,或者说一个陈述,在聊天中,关于我只是要读一下。纳秒响应时间的目的不是单个请求的最终用户体验。它是为了机器密度和每秒处理数千个请求。但我认为你会说,嗯,我的意思是,我们的服务器成本,如果我们所有的Basecamp服务器都能在纳秒内运行,那对生意来说是不可行的。说真的。是的。我的意思是,字面意思,让我们做一个思想实验。假设从100毫秒到一纳秒,我必须多雇佣50%的员工来处理一个生产力降低50%的环境。我会做这样的交易吗?绝对不会。我会以20%的价格这样做吗?绝对不会。我会以2%的价格这样做吗?也许吧。我不是反对免费的性能。我喜欢免费的性能。我愿意为此付出的代价,当然不是从我冰冷的双手上夺走Ruby。当然不是Rails,当然不是我们通过这种方式构建系统所获得的生产力。而且,这又回到了Basecamp 3目前是25000行代码的事实。比如,如果我正在构建一个只有五个屏幕的应用程序,比如WhatsApp之类的,其中应用程序本身的复杂性非常低,也就是说,它有很少的功能点,这是一个不同的故事,这是一个不同的场景。如果你写了大约500行代码,好吧,你真的不在乎,比如,哦,我可以从100毫秒到一纳秒,如果我写了大约一千行代码,好吧,算我一个,对吧?如果我必须从25000行代码写到75000行代码,你不会让我痛苦地接受这个论点,这又回到了机器密度到底意味着什么。嗯,它可能意味着更低的运营成本,因为你需要更少的机器。它可能意味着你出于环保原因这样做,比如,嘿,节省一些电费更好。嗯,除非你已经在[ __ ]远程工作,住在森林里,并且从不使用汽车,那么也许这不会在全球层面产生影响,对吧?所以,把这些愿望放在背景下,它们重要吗?而这正是我经常看到人们陷入“乐乐之地”的地方。你只是说,是的,孤立地看,如果你只看那件事,那件事可能很重要。当你把它放到整体的背景下,它就无关紧要了。而且,我认为这是我从商业和软件开发中吸取的最重要的教训,那就是将事物置于背景中,并将单个元素与整体进行权衡。比如,我把时间花在哪里?好吧,我可以把时间花在这件事上。这件事在宏观上重要吗?好吧,不。那么我不应该在那里花时间。这适用于资源,适用于人,适用于所有其他事情,对吧?它也适用于快乐和幸福等等。比如,我会花多少钱,或者换个说法,我需要赚多少钱才能,我在这里做一个夸张的说法,但只是为了论证,从Ruby切换到Java,我必须每天写Java。我实际上在想,真的有这样的数字吗?你能给我一亿美元,然后我必须写十年Java吗?我认为你不能。说真的,我不是在开玩笑。比如,字面意思,如果你出现说,“嘿,伙计,你必须每天写六个小时的Java,而且不能使用任何转译器。它必须是纯粹的Java,甚至是Java 8。”然后我会在十年内得到一亿美元。我可能会说,“嗯,去吧,去做别的事情。去做别的事情。”嗯,这很有趣,因为这似乎触及了我想要问的一个问题,那就是,我认为,如果你只看Rails核心团队,看看他们随着时间的推移如何变化,我认为很多程序员最终会在他们的职业生涯中转向其他语言,对吧?这不,我猜没有那么极端,但我认为很多人,也许是刚开始接触Rails的人,这也是我写这整个课程的原因,他们有很多焦虑,比如,Rails是否足够快,与Rust、Ecto、JavaScript的流行语言或任何其他语言相比。是什么阻止了你产生这种感觉,因为听起来你会在坟墓里写Ruby。嗯,不一定,首先,但我会写Ruby,直到出现一个更漂亮、更友好、感觉更好的语言。我的意思是,直到我坟墓,这是一个很长的时间跨度。我真诚地希望我们在未来80年里能想出比Ruby更好的东西,否则,我认为这说明了马茨的才华,或者我们无法取得进步。但我认为你的观点,焦虑。是什么阻止了我的焦虑?现实世界。我只是部署代码,我看到,哦,它完全服务于它的目的,在一个非常合理的预算内,而且我对此没有任何问题。太棒了。让我们继续吧。这就是为什么我如此热衷于传播它,因为Basecamp已经比世界上绝大多数应用程序大得多。我们需要处理的用户和客户数量,比如,我们绝对是低于1%的。而且,当这是如此可行的时候,99%的人就不应该惊慌失措。比如,我在这里的1%说,伙计们,没事的,再次,在这个我设定的“没问题”的定义内,有我的免责声明。比如,99%的人不必担心。其次,我听了15年的这些担忧。15年前,当一切都贵10倍,慢10倍的时候,它们就不真实。现在它们对绝大多数人来说仍然不真实,在绝大多数情况下。所以,这只是其中一件事,你只是觉得,那可能只是人类心理。比如,这就是我们所做的。我们对那些最终无关紧要的事情感到惊慌失措。看看这个世界上其他各种事情,对吧?人们说,“天哪,恐怖分子。”然后他们吃一个1200卡路里的汉堡,加培根和薯条等等。比如,对吧?哦,那些恐怖分子,他们总有一天会抓住我的,对吧?比如,与每天死于心脏病的10万多人相比。或者他们敢在城市里过马路。你知道在芝加哥一地,有多少人死于过马路吗?我认为大约有1000人在交通事故中死亡,对吧?在美国,每年有35000人或30,是的。每年有35000人死于交通事故,对吧?所以,从死亡的角度来看,我会被一个挥舞AK-47的ISIS战士杀死吗?不,我不会。在编程和性能的背景下,我的初创公司会因为我用Ruby和Rails编写它而倒闭,因为它太慢了吗?不,它不会。所以,再次,我不是说,是的,有0.001%的可能性是真的。如果发生在你身上,我很抱歉,但很可能不会。所以,过你的生活,经营你的生意,无论是害怕你将如何死去,还是你的应用程序是否足够快,都要基于概率和几率以及可能发生的事情。然后停止担心炸弹。所以,回到我们之前谈论的一个话题,我认为这是一个好问题。它来自聊天,迪伦·韦尔奇。你谈到了设置你的俄罗斯套娃缓存,让你故意进行N+1,但实际上从未发生过。你如何避免在需要获取三个、四个、五个、一打查询的情况下,避免在你自己和缓存服务器之间进行相同的N次往返?我不完全确定我理解。比如,嗯,你知道,比如,如果你回到你的电子邮件和收件箱的例子,对吧?那么,我如何避免50次回到我的缓存来获取每个片段,来回传递?是的,这是一个很棒的问题,也是我们必须解决并最近解决的问题。缓存现在有这些很棒的东西叫做multi。所以你可以对集合进行multi-get,然后在一个缓存查询中获取你需要的东西,而不是,因为,即使你正在从缓存中获取东西,即使缓存是本地的,你说的对,你可能会在缓存上进行N+1,而不是在集合上。我们做了很多工作,我不确定这何时实现,我认为Rails 5肯定拥有所有这些,当你执行render collection cache true时,它有一个完整的multi-get自动集合获取。当你这样做时,它会遍历集合,查找缓存并以这种方式获取它们。但是,在某些情况下,这根本不可能。比如,有些页面会触发我不知道的15个缓存调用,对吧?某些东西过期了等等,对吧?通常发生的情况是,一旦整个俄罗斯套娃设置完成,并且没有任何变化,下一个请求者就可以在一个请求中获得整个娃娃。但是,如果在那之前和之后有什么变化,你仍然会过期一些东西,所以你仍然需要重新计算一些东西。只是它很快,对吧?你可以通过Redis或任何其他方式完成大部分调用,在一毫秒或更少的时间内,但仍然是一毫秒或左右。所以,如果你做了150个,你仍然会达到150毫秒。所以你必须注意这一点。但是有所有这些策略来运行multi-get等等,你可以用来避免绝大多数情况。只是在那之后,它就不是问题了。只是为了暂时留在俄罗斯套娃缓存上。我认为很多人在开始使用基于键的过期和俄罗斯套娃缓存时会遇到的一个问题是,他们会遇到这些巨大的缓存键,比如连续五个模型。那么你有这些问题吗?你如何解决它们?我如何避免拥有这些超复杂的缓存键?是的,我,我非常坚持拥有一个缓存,一个。所以,就像俄罗斯套娃一样,缓存也随着过期而上下流动,这通常是围绕触发层级过期。你触及,所以如果你一直向下,你有一个评论属于一个消息,它触及,所以当你添加一个评论时,它会触及消息,当消息被触及时,它会触及项目,比如说,这样你就有了三层。所以,如果你添加一个评论,它会使项目过期。所以你可以让很多东西依赖于项目。例如,如果项目页面可以仅仅依赖于项目,而不是项目中的东西,因为你已经设置了项目在其中任何东西被创建或渲染时都会被触及。消息也是如此,对吧?添加了一个新评论,消息被触及。因此,整个消息页面可以仅仅依赖于评论。但即便如此,仍然有很多情况,例如,在一个页面上,你可能有一些个性化信息,或者一些有时间过期限制的东西。而这正是JavaScript喷洒的奇妙之处。所以,你采用这种方法,缓存中的是理想化的形式,适合所有人,然后你放入替换键,比如当前用户,然后你通过JavaScript替换它们。有时我们甚至会做得更复杂,不仅仅是当前用户,我们还可以做一些设置,比如这个页面被缓存了,然后我们有时会进行多次HTTP调用来获取东西,因为我们知道这些调用很便宜。一个例子是,在Basecamp的消息页面上,我们有一个小按钮,显示你是否关注这个页面,这个布尔值取决于用户,但我们不想让整个缓存依赖于用户。这会破坏目的,对吧?我们当然不想让缓存同时依赖于消息和那个布尔值。这是不成比例的。所以,我们所做的是,我们渲染一个虚拟按钮,但这个虚拟关注按钮上有一个JavaScript喷洒,它会进行一次额外的HTTP调用来检查该状态。这听起来效率极低,但实际上并非如此,因为你将页面分解成非常便宜和简单的渲染和获取。所以,你可以通过预加载来做到这一点,你也可以通过将东西塞进头部来做到这一点。是的,我注意到你的身体和头部都有东西,那是每个用户都有的,对吧?你们在Basecamp上似乎做的一件事是,有一个我认为是元标签,上面有用户的名字和首字母缩写,我认为这可能只是被JavaScript到别处了。是的,正是如此。这就是它的作用。你试图制作这些广泛的模板,它们适用于尽可能多的人,在尽可能多的情况下,并且依赖于尽可能少的键,然后你的出口是JavaScript替换其余部分。所以,我们一直在谈论平均时间和中位数时间,但我认为,你曾经公开说过,显然,只关注平均值会让你对应用程序的性能有一个不完整的了解。为什么看95%甚至99.9%的百分位数时间很重要?我想知道一些例子,比如你一直在寻找95%或99%的龙,比如Basecamp有什么问题,你挖出来,比如,1%的人有这样的经历。是的。所以,这正是你需要去那里寻找的原因,因为,哦,看99%的人并不多。但是,如果你每天有100万人使用你的应用程序,1%仍然是很多人,对吧?比如,那是1000人左右?这是我通常会出错的脑算。那是很多人,对吧?他们得到了那种体验,如果对他们来说感觉不对,对他们来说仍然感觉不对。所以我们去那里追逐龙,而且我们经常出于自私的原因这样做。例如,最近我们追逐的一条龙是,在Campfire中发布消息需要一点时间。当你发布一条消息时,嗯,不是Campfire,Campfire是Basecamp 3中聊天的名称。当你发布一条Basecamp 3聊天消息时,它会通过一个作业系统,对吧?比如,你把它放进一个作业队列,因为它需要在你发布新消息时做很多工作。它需要更新消息房间,但它也需要将消息中继给其他当前不在房间里的人。它需要更新未读计数。它需要更新整个项目页面的通用缓存。它实际上需要做相对大量的工作,对吧?这项工作可能需要几百毫秒。我认为我们大约是200或250毫秒左右。而这是一种情况,你输入一些东西,你按回车键,你不想感觉到延迟。在这种情况下,300毫秒实际上可能是一个问题,对吧?一旦你回到100毫秒左右,它就不再是一个问题了。总之,为了做到这一点,我们把工作放进了作业队列,我们发现有时这些队列的深度会飙升,我们想,“为什么它们会飙升?”我们发现其中一些作业需要40秒。我们想,“什么?为什么有人说句话然后听到回应需要这么长时间?”哦,嗯,那个房间里有大约2500人,我们想,“哦,[ __ ],我根本没有为那个设计它。我设计它是为了最多100人,在大多数情况下,更像是50人,对吧?所以,我们所做的工作量,我们是串行进行的,如果你一次做一个用户,比如,让我更新他的未读计数,如果他不在网上,让我通过电子邮件发送给他,如果他需要推送通知,所有这些工作,我们都是一次一个地做的。如果你为2500人这样做,那么,不出所料,至少在我们的设置中需要40秒。所以,为了支持2500人,我们要么应该串行化,要么不串行化,并行化,这样我们就批量进行作业工作,比如,每100人一个作业,这样每个单独的作业只需要400毫秒或左右。然后它就会没问题。但总之,这是一个例子,其中一条龙出现了,实际上给其他人造成了问题。而且,有时这些龙会发生的情况是,不仅仅是那个人自己能看到问题。当你的系统设计成依赖于快速响应时,问题可能会溢出。另一个例子是,如果你有一个不成熟的应用程序服务器设置,其中在单个应用程序工作进程中发生排队,而不是一个集中的分发点。你可能会有一个应用程序工作进程,它的队列是5D。所以,如果第五个必须等待第一个、第二个、第三个完成非常慢的请求,那么他们的体验就会很糟糕。我认为这是一种反模式。但我的意思是,你应该做的是,在工作进程空闲之前,不要将人们发送到工作进程。但有些设置,我们过去有过这样的设置,情况并非如此。我们在每个单独的工作进程中排队了少量任务,对吧?我们说,“嘿,这个工作进程有三个队列。”那不是问题。嗯,如果这个请求需要两秒钟,那就有问题了,因为现在排队等待的其他两个人将获得他们的请求时间加上完成的这两秒钟。所以,而且,它也只是一个好方法,通常情况下,这甚至不是性能问题,而是你的UI没有为这种情况设计。所以,有时当我们看到页面需要很长时间来渲染时,是因为我们没有,例如,设计页面让2500人同时使用它,它没有无限滚动,或者它没有部分加载,或者其他什么。它只是试图在一个小对话框中显示2500个头像之类的。嗯,没用,对吧?它会崩溃。即使我们能快速做到,它仍然是一个糟糕的用户体验,因为系统不是这样设计的。所以,在很多情况下,这些龙只是你没有为之设计的边缘情况的指示器。你应该去调查一下。无论你是否能让它们足够快,通常都无关紧要,因为在大多数情况下,真正的答案是你必须重新设计它们。所以,我们快到了,现在正朝着Rails 5发布迈进。截至本次录音,我认为我们处于beta 4,beta 5。所以,让我们谈谈,我想我们得谈谈Action Cable,对吧?这是Rails 5发布时每个人都会谈论的事情,当人们写他们的Rails 5博客文章时。所以,也许简单地解释一下这个功能是什么,但我真的很想谈谈Action Cable背后的性能故事,因为我知道Basecamp的Campfire在过去十年里一直是“长轮询”的重度用户。比如,你曾经做过长轮询,现在是WebSockets。这是一个非常有趣的转变,让我们谈谈那里的性能故事。好的。Action Cable是一种让编写WebSockets功能感觉像编写Rails应用程序的其余部分一样有趣和富有成效的方式。它基本上使添加WebSockets到你的应用程序的复杂性不比添加另一个控制器更复杂,并允许你重用你的整个领域模型,你所有的Active Record和其他Ruby模型在WebSockets工作,并且仍然以高性能的方式完成。所以,那里的性能故事是双重的。一是Action Cable现在需要持久连接,对吧?这就是WebSockets的全部内容。它创建了一个持久连接,这也是它快速的原因之一,因为它每次都不进行握手。它不交换密码等等。没有SSL,也没有头部等等。它是一种相当高效的格式,对吧?所以你可以快速地来回发送东西,但你付出的代价是性能,二是维护这些长期运行的开放连接来回的复杂性。嗯,有一个性能方面是,你能保持多少这样的连接,需要多少硬件来维持它们。结果是,首先,这是我们担心的事情之一,哦,[ __ ],我们将有多少连接,然后当我们开始测试时,我们发现,不相关,不是瓶颈。我们可以保持大量大量的开放连接。瓶颈是工作。也就是说,当你实际尝试做某事时,只是打开一个连接,让某人在该连接上什么都不说。嗯,惊喜,这并不需要太多。现在有很多其他框架,比如,在这方面做得很好,对吧?比如,在一个盒子上打开两百万个连接,我说,嗯,他会做什么?你能让那两百万人在这同一个盒子上做任何有趣的事情吗?如果不能呢?
这有多大帮助?可能帮助不大,对吧?
就像如果这两人中的每一个人都必须执行一项任务,假设需要 100 毫秒,对吧?两百万人乘以 100 毫秒,那就是很多秒、分钟或小时,对吧?就像它不会那样运作一样。
现在你可以做其他事情。你可以有一个单一的盒子,然后把东西塞进某种队列里,然后是队列在工作,等等。但工作仍然需要完成,我认为这是 WebSocket 性能讨论中我感觉被忽略的部分,就像“Hello World”被吹捧为有意义的东西一样。
哦,我的框架的开销是谁在乎呢?如果你要做的工作是标准的 100 毫秒,而 Rails 在单个“Hello World”上的开销是 2 毫秒,那并不重要。你正在做价值 100 毫秒的工作,你还在乎开销是 2 毫秒还是 2 纳秒吗?这根本不相关。
我认为对于 WebSocket 工作来说,情况也是如此。如果你正在做实际的、真实的工作,那么保持连接的开销与完成实际工作相比微不足道。
如果你说,那么工作是什么?让我们再次以聊天为例,对吧?有人聊天,他们要么发送消息,要么接收消息。在这两种情况下,都涉及大量工作。至少在我们的应用程序上是这样,因为我们做了很多事情,我们不会直接在原始套接字上回显它,对吧?我们需要在数据库中记录它。我们需要更新缓存。我们需要 ping 移动应用程序。我们需要发送电子邮件。我们需要做大量的工作。
结果发现,瓶颈在于完成所有工作,并以一种我们有足够的工作服务器来完成部分工作,而我们自己会在应用程序服务器上完成一些工作的方式来设置它。但至少这个故事是相当熟悉的,因为它与扩展普通 Web 应用程序的故事相同。
因为在普通 Web 应用程序中,如果你可以缓存一切,就像如果没有实际工作,你只是从 Redis 中提取一些东西并直接通过 HTTP 管道发送。好吧,你可以做很多这样的事情,对吧?这有多相关?你的应用程序将有多少时间花在这上面?不多。
更相关的是当有人发表评论时。好吧,你必须更新数据库,你必须发送,你必须做所有工作,而且在许多情况下,开销根本不重要,如果工作量甚至很小,开销就不相关。
开销通常只有在你基本上不做任何工作时才不相关。你什么时候不做任何工作?好吧,当你只是想演示有多少人可以连接到某个东西,或者你可以说多少次“Hello World”时,你就是在不做任何工作。
这些实验中的任何一个与我为 Basecamp 所做的工作类型相关吗?绝对不。它们什么都没告诉我。它们没有告诉我这会如何帮助我。
所以,我认为大多数人会发现有这两个因素。一个是如果你真的只是有一个某种公共应用程序,在那里你有两百万个连接,而且两百万是一个真实数字,你需要支持它,而且他们不进行单独的工作,但他们只需要获得某种更新,比如计数器发生了变化,你可以非常有效地发送出去,或者其他什么。也许这是一个有用的用例,但对我所做的任何工作都没有用。
我所做的所有工作都是人们在实际工作并获得真实更新,对吧?就像……而且它发生在足够小的群体中,以至于 Basecamp 的一个人在某个项目中的更新不会广播给两百万人。它会广播给大约 15 个人,然后有数百甚至数千人正在做完全相同的事情,做一项工作,然后需要广播给大约 2 到 25 个人左右,然后再次,重点又回到了做工作上,工作是瓶颈,而不是连接开销。
所以,这再次取决于你的应用程序。这取决于公共的,等等,等等,等等。
我再次认为 Basecamp 是许多人的典型应用程序,它比连接两百万人到某个公共服务器并发送某种更新更相似,更像大多数人的应用程序。
那么,让我们来谈谈另一个大型标志性功能,我特别兴奋的那个,那就是 Turbolinks 5。
是的。
所以,我想你能否简要评论一下,对于像我这样关注 Turbolinks 开发的人来说,发生了什么?我想我们都感到困惑,当时我说,嗯,Turbolinks 3 要来了,它不再来了。所以,你能评论一下发生了什么,以及为什么 Turbolinks 被完全重写吗?
是的。Turbolinks 3 basically 是我们用 Basecamp 2 发布的 Turolinks 的主分支。所以,对于 Basecamp 2,我们只专注于将 Basecamp 的桌面 Web 版本通过 Turbolinks 实现。我们没有在移动设备或其他任何地方使用 Turbolinks。我们当时正在与 Shopify 合作,因为他们以类似的方式使用它。我们开始探索很多东西,比如部分更新,对我来说更重要的是“永久性”的概念,即页面的某些 DOM 元素可以在页面更改之间保持不变。所以你可以有一个持久的菜单栏在顶部,这令人惊讶地正是 BCAP 3 所使用的。它有一个 Turbolinks 的永久菜单标题,这样它就不会改变,并且可以在页面请求之间保持其 DOM 计数器等。
总之,我们开始进行所有这些工作,然后在这个开发过程中,这个过程被拉长了很长时间,并且由于各种原因,由于部分更新的各种问题和复杂性,从未最终完成。我们开始开发新版本的 Basecamp,对于新版本的 Basecamp,我们在使用 Turbolinks 的方式上变得更加雄心勃勃。基本上,我们想要一个宏伟的整体,它不仅为桌面 Web 提供支持,而且在更大程度上为我们的原生应用程序提供支持,比 BCX 或 BC2 中的情况更甚。
在 Basecamp 2 中,我们有原生优化的视图。所以我们有一个完整的,他们在使用,我们使用相同的控制器,但是原生或移动设备获得了自己从头开始编写的视图,以及自己的 JavaScript 和 CSS,这样我们就可以对其进行精细调整,使其非常快速。
好吧,2012 年到 2016 年之间发生了一些变化,移动设备的速度大大加快了,它们变得如此之快,以至于在移动设备和桌面设备之间重用相同的 JavaScript 和 CSS 代码库要可行得多。一旦发生这种情况,我们就想,哈利路亚,现在我不用为视图重写我的功能两次了,对吧?所以我们可以只使用相同的完整视图,并对不同平台进行一些 CSS 的微调。这太棒了。
但如果你不能在你的移动平台上使用 Turbolinks,那就不那么棒了。你可以使用 Turbolinks,如果你只是做移动 Web。但我们也想做原生应用程序。我们发现尝试使用 Turbolinks 3 和原生包装器,其质量不符合我们的要求。
有很多复杂的原因。一个简单的解释是,例如,在 iOS 中,你可以滑动返回你的历史记录。如果你做到一半,你会看到你当前所在的屏幕,然后你会看到你后面的历史屏幕。要做到这一点,在原生应用程序中实际上非常复杂。我们尝试了各种各样的东西,用两个缓冲区等等。我们想把所有这些都包装起来,这样我们就有一个框架可以处理它。所以 Turbolinks 可以与原生应用程序和原生包装器进行交互,这样你就无法分辨什么是原生的,什么是 Web 的。这需要对 iOS 和 Android 之间更改页面的生命周期模式等进行很多让步。
所以,这就是我们最终重写 Turbolinks 的原因。我们专门重写了 Turbolinks,使其适合嵌入到 iOS 和 Android 的原生应用程序中。
而且,让我们也公平地说,因为 Turbolinks 有点混乱。当我最初编写第一版 Turbolinks 时,我认为它大约有 90 行,可以放在一页 JavaScript 中。到 3 版本完成时,它已经有 600 行混乱的代码了。而且只有两个人完全理解它是如何工作的。所以这是另一个积极的副作用,我们基本上可以回顾到目前为止 Turbolinks 的全部演变,然后说,“好吧,我们学到了一些东西。现在,那是演示,然后让我们重写完整的版本。”
但是,我认为更重要的驱动因素是将其嵌入到原生应用程序中,然后这导致我们制作了完全的原生框架,这是我们以前从未做过的事情。我们发布了适用于 iOS 的 Turbolinks 5 框架,适用于 Android 的 Turbolinks 5 框架,完全为那些想要遵循这个宏伟整体模型的用户设置,该模型位于中心,为原生应用程序提供支持。
是的。所以,现在你有适用于 Windows、Mac、Android、iOS 的 Basecamp 原生应用程序,然后我想你也可以算上 Web,对吧?所以是五个平台,有多少程序员?
确切地。
对。就像我们有……人们在新功能上工作,比如……六个人在 Web 上,然后四个人在所有移动应用程序上,对吧?所以有 10 个人在做这样的功能。如果你不走宏伟路线,这是不可能的。而我所说的宏伟是指整体,对吧?如果你要制作完全的原生应用程序,你需要更大的团队。至少如果你的应用程序的规模接近 Basecamp 的规模,对吧?
嗯,让我们也公平地说,Basecamp 是一个非常大的应用程序。它有超过 200 个单独的屏幕。你能想象制作 200 个原生屏幕吗?我无法想象。而且我甚至无法想象团队会有多大,需要多长时间,对吧?如果 Basecamp 中的所有东西都通过 JSON 发送,然后必须将其实现为原生,我的意思是,这将花费永恒,而且我们将不得不拥有一个公司,其规模将是现在的五倍。我们不希望这些。
我认为这就是真正的承诺,也是我如此热衷于 Turbolinks 的真正原因,尽管面临很多批评。它不能与这个插件一起使用,或者它使事情变得复杂,或者它不是客户端的,或者它不是等等等等。
好吧,不管是什么 [哔]。听着,这就是它允许我们做的事情。我的意思是,这是神奇的技术。我说,在我们放入 Rails 和我使用的其他所有东西中,Turbolinks 可能是我们拥有的最重要的生产力工具,它使 Basecamp 3 能够跨越这么多平台推出。因为如果替代方案真的是在它和重写整个应用程序至少两次,以获得某种手工优化的 HTML、CSS 和 JavaScript,或者用完全的客户端原生客户端实现来做所有事情之间进行选择。我们只是……我们……这是不可能的。
是的。我的意思是,它只是实现了以前不可能实现的事情,我不能对很多其他事情说这句话。
是的。我……我认为,尤其是在 Turbolinks 最初出现的时候,中小型公司或自由职业开发者有点忽视了它,因为我不知道是不是因为它不够有趣?我不知道 Turbolinks 5 现在有多少行 CoffeeScript,但它是一个极其简单易懂的库。如果你理解 `document.body.body = xhr.body` 在 `xhr` 对象中。那就是 Turbolinks。其余的只是,你知道,花哨的东西,对吧?就是这样。它不是,哦,我们有不同的组件,组件有状态和 props,我们要……重渲染世界,伙计。它不是……它只是不有趣,我认为这是问题的一部分,也是可能不切实际的期望,因为它太简单了。人们认为,我也会承担一些责任。我们可能过度宣传它,说它是一个无需任何修改就可以直接替换的插件,而这永远不会是真的,对吧?
世界上所有的 Java jQuery 插件通常都是这样编写的:让我们在每次页面更改时丢弃整个过程,我们在所有生命周期和事物中进行钩子,以使其成为现实。而使用 Turbolinks,你正在从 CGI 模型切换到持久进程模型。
我实际上记得当我们为 Rails 这样做的时候。Rails 最初是 CGI。所以对于那些在 2004 年初使用过 Rails 的人来说,没有快速 CGI。没有持久进程,因为整个东西太小了。我们实际上在每次页面请求时都在重新计算整个东西。事实证明,这很快就不可行了,对吧?出于我们拥有持久进程的所有原因。只是我们不必在每次请求时重新编译相同的东西,对吧?这在某些方面是该死的 [哔],在其他方面则不是 [哔],对吧?那仍然是 PHP。所以 PHP 仍然是这样工作的,而且它也可以运行持久进程,但它的许多易于理解性来自于它不运行持久进程的事实。每次丢弃一个进程要容易得多。
我见过你过去提到你多么尊重 PHP 最初是如何……让我们把它放在标记中,以及它对人们来说有多么简单易懂。而这种简单性的很大一部分也来自于 CGI 模型,来自于非持久的执行模型,对吧?你可以做各种疯狂的事情,这些事情会在 200 个请求中泄漏一千吉字节,但它不会,因为它每次都被丢弃了。而这正是 Turbolinks 会发生的改变。人们习惯了 CGI 模型,被迫与持久进程模型进行协调。
是的。这很有趣,因为这里还有另一个平行之处,那就是我们用于 Rails 的加速器 Spring,它正在经历同样的反应过程:哦,[哔],现在它是一个持久进程,然后它有一些缺点,有一些额外的复杂性,但这是一个非常值得付出的代价,因为它使你能够做各种其他美妙的事情。
是的。对于那些可能不在 Twitter 上关注我的人来说,如果你看看世界上排名前五的 Rails 应用程序,按规模计算,你知道,Basecamp、Shopify、Cookpad、GitHub,它们都在使用某种形式的 Turbolinks 解决方案。其中一些直接使用 Turbolinks,比如 Cookpad。据我所知,每个规模化的 Rails 网站都使用类似的解决方案。除非……我的意思是,如果你使用完全的客户端 MVC,并且你只是用 Rails 来发送 JSON,我的意思是,这是另一回事,你也可以这样做,而且那也很棒。那不是我喜欢做的,对于我所做的很多工作,但它也是一种完全有效的操作模式。
好的,我们时间到了,但我最后想问一个来自聊天的问题。因为我也想问同样的事情,只要你能分享。Basecamp 3 的 Gemfile 今天看起来是什么样的?这里具体的问题是你们在使用什么后台处理?所以,Sneakers、Sidekick,随便什么。我想知道你们使用的是什么应用程序服务器。Puma、Passenger,或者随便什么。你们的 Gemfile 看起来是什么样的?
是的。我们使用 Unicorn 来处理常规的 Web 视图。我们使用 Puma 来处理 Action Cable 服务器。我们将这两件事分开了。Rails 5 中的新默认是 Puma,所以你可以在一个进程中运行所有东西。你也可以使用 Passenger。但无论如何,我们只在网站上使用 Unicorn,在 Cable 端使用 Puma。我们使用 Resque 进行作业处理。
让我想想。还有什么有趣的吗?我们的文件不长。也许这也是为什么我们不使用很多插件,比如 Devise 或其他很多人使用的东西。我们过去使用了很多测试插件。里面没有隐藏的 RSpec 或其他类似的东西。所以我们有一些内部工具供我们自己使用,但是……让我现在就进去看看我是否能找到其他看起来很诱人的东西。
让我想想。是的,一些秘密的 RSpec 插件,你不会告诉任何人,你……我试图想,我的意思是,我们使用了很多,所以围绕 Resque,我们有很多东西,比如动态队列、多作业 fork 池、调度程序……还有什么?我们使用自己的存储解决方案,然后我们也使用 S3。让我想想,我们有很多关于一些仪器化的东西,回到我们自己的数据仓库设置。
现在我们刚刚尝试了 Sentry。哦,我是 Ruby 客户端的维护者。所以如果你有任何问题,请告诉我。太棒了。我们最近才切换到它。我们有自己的家庭解决方案,然后我们想,嘿,为什么我们要这样做?不再需要这样做了。我们还开始使用 Skylight 做一些事情。
让我想想还有什么。我认为差不多了。就像,它并不那么有趣。整个文件有 168 行,而且还有很多注释和拆分的东西。所以,我的意思是,当然,这也是真的,对吧?因为 Rails 的默认堆栈在很大程度上就是 Basecamp。所以我们可以依赖所有默认设置,因为我们已经选择了它们,这就是我们想要它们的原因。所以没有替代品,对吧?如果 Rails 堆栈中的某些东西对 Basecamp 不起作用,那么它可能不会在 Rails 堆栈中存在太久,因为我可以决定它里面有什么,如果我不喜欢它,为什么它会在里面呢?如果我喜欢它,为什么它不在里面呢?
所以,是的,这可能有点作弊,对每个人来说都不是那么容易获得,也不是每个人都有自己的 10 年 Web 框架。
是的,这有帮助。
所以,时间到了。非常感谢你这样做。我玩得很开心。而且,对于所有观看的人来说,这将在 YouTube 上播出。它将在我的 YouTube 频道上播出。这就是我使用 Hangouts on Air 的原因,因为这会自动录制并上传。所以,如果你错过了部分内容,你可以再次观看,或者……你知道,给你的同事看,或者随便什么。
所以,再次感谢你,David。
我的荣幸。
好的,伙计。保重。