Thoughtworks 的 Ruby

Thoughtworks 从 2006 年开始使用 Ruby 进行生产项目,从那时到 2008 年底,我们已经完成了 41 个 Ruby 项目。为了准备在 QCon 上的演讲,我调查了这些项目,以研究我们从经验中可以吸取哪些教训。我描述了我们迄今为止对 Ruby 的生产力、速度和可维护性的常见问题的想法。到目前为止,我们的结论是 Ruby 是一个可行的平台,应该认真考虑用于许多形式的应用程序,特别是使用 Ruby on Rails 的 Web 应用程序。我还将介绍一些技术方面的教训,包括一些关于使用 Active Record 进行测试的想法。

2009 年 6 月 11 日



我的雇主 Thoughtworks 主要是软件交付公司。我们为人们构建软件,包括为我们自己构建的产品。我们理念的重要组成部分是对不同开发平台的开放性,这样我们就可以为我们广泛的客户选择合适的平台。当我 2000 年加入 Thoughtworks 时,Java 是我们压倒性的主要平台。不久之后,我们开始使用 .NET,这两个平台在十年中期主导了我们的工作。

然而,一些人开始尝试使用 LAMP 脚本语言,特别是 Ruby。Ruby on Rails Web 框架的出现给了 Ruby 很大推动,足以让我们在 2006 年开始使用 Ruby 平台进行一些严肃的项目工作。当我 2009 年写这篇文章时,Ruby 平台在我们工作中占据了稳固的份额,虽然没有 Java 和 C# 高,但也是相当一部分。

在这三年中,我们在实践中了解了 Ruby 的很多知识。2009 年初,我被要求为 QCon 大会做关于我们使用 Ruby 经验的演讲。为了准备这次演讲,我对我们的 Ruby 项目进行了广泛的调查,并向我们的 Ruby 领导人询问他们的想法和经验。这篇文章的完成比我预期的要晚一些,但现在终于完成了。

我把这篇文章分成了三个部分。首先,我将回顾我们 Ruby 项目经验的概况,让您了解我们这些年来一直在处理哪些类型的项目。接下来,我将讨论一些关于 Ruby 的常见问题,以及我们的经验如何回答这些问题。最后,我将介绍一些我们从使用 Ruby 中吸取的教训。

我们项目的形状

在 2006-8 年期间,Thoughtworks 参与了大约 41 个 Ruby 项目。我将 Ruby 项目定义为以 Ruby 为主要开发语言的项目。Ruby 也出现在其他项目中,最近有很多使用 Ruby 进行 Java 项目的构建自动化或功能测试的开发。几乎所有这些项目都涉及 Rails,其中大多数是 Web 站点项目,Rails 在其中至少与 Ruby 一样重要。

图 1:Thoughtworks Ruby 项目在 2006-8 年的峰值人数与参与时长之间的散点图。

图 1 让我们了解了我们参与的项目的规模。这里的人数是指所有参与者(Thoughtworks、客户和其他人员;开发人员、项目经理、分析师等)的峰值人数。时长是我们参与项目的持续时间。

Ruby 项目通常被认为比其他项目更短、更小。遗憾的是,我没有我们其他平台项目的比较数据,无法更好地了解这是否属实。当然,我们可以看到,大多数项目涉及不到 20 人,持续时间不到一年。

有一些项目很突出。我们迄今为止最大的项目是我将称之为亚特兰大项目的项目,峰值人数超过 40 人。另一个大型且持续时间长的项目是泽西项目。这两个项目之间存在着相当程度的轮换,因此我们许多经验丰富的 Ruby 人员都参与了这两个项目。

我在这里提到的第三个项目是 Mingle,它是一个特别有趣的案例,因为它来自 Thoughtworks Studios 的产品,因此我们可以比对客户完成的项目更公开地谈论它。这是一个持续时间长的项目,也是一个国际项目:从澳大利亚开始,转移到北京,现在在北京和旧金山的多地开展。

图 2:显示每个项目每年工作量的条形图。

图 2 从不同的角度看待项目的形状,查看我们参与的各个项目在每年的工作量。条形图上的每个点代表该年一个项目中的总工作量(所有人员)。这张图很好地展现了我们在过去三年中 Ruby 项目的增长幅度。

图 3:显示每个主机国家的项目工作量的条形图。

图 3 按主机国家查看项目。它有点粗略,因为我没有尝试正确处理少数多地点项目或已迁移的项目(例如,Mingle 我归类为中国项目,尽管它的历史更加多样化)。

国家分布显示,美国对 Ruby 工作的兴趣最大。印度也看到了相当多的兴趣,事实上,我们的第一个 Ruby 项目是在班加罗尔完成的。英国的采用率较低。这可能反映了我们早期 Ruby 支持者大多来自美国,而英国对 Ruby 存在相当大的怀疑。印度的参与程度令人鼓舞,传统上印度被认为是采用新技术落后的国家,但我们似乎在让我们的印度办事处与众不同方面做得相当不错。

我们销售 Ruby 工作的经验是,使用 Ruby 这样的动态语言非常适合我们的整体吸引力。我们的优势在于,我们聘请了高素质的人才,而这些人才很难吸引到典型的 IT 机构。Ruby 的理念是创建一个环境,让有才华的开发人员拥有更大的杠杆作用,而不是试图保护能力较低的开发人员免受错误的影响。因此,像 Ruby 这样的环境让我们的开发人员能够更好地发挥他们的真正价值。

Ruby 也符合我们对使用敏捷软件开发流程的偏好。敏捷理念是通过构建软件并定期与客户进行审查来快速获得反馈。开发环境的生产力越高,您就可以越频繁地审查进度,并且敏捷的“检查和适应”流程的效果越好。

关于 Ruby 的问题

Ruby 是正确的选择吗?

回顾我们 41 个项目,也许最重要的是要问,Ruby 平台是否是正确的选择。解决这个问题的一种方法是询问项目的技术负责人,他们是否认为事后看来,这个选择是正确的。

图 4:Ruby 是这个项目的正确平台选择吗?

图 4 所示,投票结果是 36 比 5,非常支持这个选择。作为一个群体,我们的技术负责人通常不会羞于表明他们对技术选择是否满意。因此,我认为这是一个明确的声明,表明 Ruby 平台是一个合理的选择。

我对这五个感到遗憾的项目进行了更深入的调查。首先,在五个项目中的四个项目中,负责人认为使用 Ruby 并不比其他选择更糟糕。Ruby 相对的非主流性意味着我们认为使用 Ruby 必须比其他选择更具优势,如果 Ruby 与更广泛使用的选项相同,那么引入这种非主流技术就不值得。五个项目中的四个项目还报告了由于与 Ruby 不太适合的其他技术的集成而导致的问题。例如,.NET 工具往往与 .NET 技术更好地集成。另外两个项目报告的另一个主题是社会问题,即客户组织中的人员反对 Ruby 或其他动态语言。唯一一个更糟糕的项目表现出这些社会问题,即一个坚决抵制 Ruby 的 IT 组织(在这种情况下,业务赞助人是 Ruby 的粉丝)。

事实上,当我进一步询问在软件项目中使用 Ruby 的危险信号时,唯一明确的答案是围绕社会问题。Ruby 通常被接受或鼓励用于我们的软件开发工作,但最大的避免使用它的信号是来自客户的社会抵制。

Ruby 更有效率吗?

当人们被问及为什么应该在项目中使用 Ruby 时,最常见的答案是提高生产力。一个早期的指标是对一个项目的评估,该评估表明 Ruby 会带来数量级的生产力提升。

因此,对项目技术负责人进行调查并询问他们关于生产力的问题似乎是显而易见的,即 Ruby 是否提高了生产力,如果是,提高了多少。我让他们将此与他们所知的最有效率的传统(Java 或 .NET)项目进行比较。

图 5:Ruby 为这个项目提高了多少生产力?(与您所知的最佳传统工具相比。)

您应该对这些结果持保留态度。毕竟,我们无法客观地衡量软件生产力。这些只是每个项目技术负责人做出的主观、定性的评估。(我没有从所有项目中获得回复。)但是,它们仍然表明确实存在生产力提升。

人员配置方面的考虑进一步强化了这种说法。管理我们亚特兰大办事处的 Scott Conley 报告说,一旦 Ruby 项目开始运行,他预计他们需要比其他技术多 50% 的人员专注于需求准备工作。

我们看到的一件事是,你不应该期望这些生产力提升会立即出现。我多次听到人们对新的 Ruby 团队在最初几周的缓慢进度感到震惊,这是我所说的 改进峡谷 的结果。Ruby 团队确实需要时间才能掌握平台的工作原理,在此期间,他们的速度会比你预期的慢。

改进峡谷是一种常见的现象,通常的缓解措施是确保团队中有一些经验丰富的人员。然而,我们的历史表明,这里最重要的经验是支持 Ruby 所具有的元编程功能的动态语言的经验,而不是专门的 Ruby 经验。正如 Scott Conley 所说:区别在于效率风险和交付风险。一个拥有动态语言经验但几乎没有 Ruby 经验的团队最初会比较慢(效率风险),但一个没有任何动态语言经验的团队可能会产生一个复杂的代码库,这可能会危及整体交付。

Ruby 速度慢吗?

简而言之,“是的”。在网上搜索基准测试,你会发现许多调查表明,即使按照脚本语言的标准,Ruby 也是一只乌龟。

然而,总的来说,这对我们来说无关紧要。我们对 Ruby 的大多数使用都是用于构建数据库支持的网站。几十年来,我访问过许多使用 Ruby 和其他技术的类似项目,几乎每个项目都花费时间处理性能问题,并且在几乎所有情况下,这些性能问题都是数据库访问。人们花费时间调整 SQL,而不是调整他们的处理代码。因此,由于大多数应用程序都是 I/O 绑定的,因此使用缓慢的语言进行处理不会对系统的整体性能产生任何明显的影响。

你会注意到我在上面段落中使用了常见的专家式含糊其辞的用语。虽然几乎每个项目都是 I/O 绑定的,但你确实会遇到偶尔的例外——而 Mingle 就是一个有趣的例子。Mingle 在很多方面都很独特。它非常动态的显示方式意味着它无法使用任何页面缓存来提高性能,这立即使其与大多数 Web 应用程序不同。因此,它不是 I/O 绑定的,为了获得良好的性能,它需要比许多人预期的更多硬件(一个四核盒子,带有 2GB 内存,以支持 20-40 人的团队)。

但 Mingle 团队仍然认为他们在使用 Ruby 上做出了正确的选择。Mingle 团队已经非常快地构建了许多功能,他们认为从 Ruby 获得的生产力提升值得最终产品更高的硬件需求。就像很多事情一样,这是一个硬件与生产力的权衡——这是计算中最古老的权衡之一。每个团队都需要考虑哪个更重要。这里的好消息是 Mingle 具有良好的水平可扩展性(向其投入更多处理器,你将获得成比例的良好性能)。在这些情况下,硬件可扩展性通常是你能拥有的最有价值的东西,因为硬件成本一直在下降。

我应该再次强调。对于大多数项目来说,Ruby 的速度无关紧要,因为几乎所有项目都是 I/O 绑定的。Mingle 是一个例外,而不是常见情况。

Ruby 代码库难理解吗?

我们经常听到的一个关于 Ruby 的担忧是,它的动态类型、对元编程的支持以及缺乏工具使其容易留下难以理解的代码库。总的来说,这在实践中并没有成为我们的问题。我听到的故事是,你可以用更少的代码来实现相同的功能,这意味着与主流语言相比,更容易保持代码整洁。

也就是说,重要的是要记住我们的背景。Thoughtworks 的开发人员在能力方面往往远高于平均水平,并且也非常热衷于高度纪律化的方法,例如极限编程。我们高度重视测试(这在 Ruby 社区中普遍存在),这些测试在很大程度上保持了代码库的清晰。因此,我无法说我们的经验是否会延续到能力和纪律性较差的开发人员身上。(即使其他语言的工具和相对控制也无法阻止我们看到一些非常糟糕的代码,因此,一个糟糕的 Ruby 代码库是否会更糟糕,这是一个值得怀疑的问题。)

我们已经看到了一种关于元编程的态度的常见序列。

图 6:关于元编程的感受的进展

  • 可怕且糟糕:人们对元编程持谨慎态度,并且很少使用它。
  • 可怕且良好:人们开始看到元编程的价值,但仍然对使用它感到不舒服。
  • 简单且良好:随着人们变得舒适,他们开始过度使用它,这会使代码库变得复杂。
  • 简单且糟糕:人们对元编程持谨慎态度,并意识到它在小剂量时非常有用。

最终,我最喜欢的关于这些技术的类比是,它们就像处方药。它们在少量时非常有价值,但你需要确保不要过量服用。

就像很多事情一样,经验是这里最大的帮助者,因为它可以让你更快地度过这条曲线。特别是,重要的是要预料到这种采用曲线,特别是过度使用。在学习新事物时,人们通常会在某个阶段过度使用它,因为如果没有越过界限,就很难知道界限在哪里。建立一个沙箱也很有用——一个相对封闭的代码库区域,供人们在其中过度使用元编程。有了合适的沙箱,以后更容易撤销过度使用。

Ruby 是一个可行的平台吗?

所有这些问题都归结为我们面临的关键问题:Ruby(和 Rails)对我们和我们的客户来说是一个可行的平台吗?到目前为止,答案是响亮的“是”。它提供了明显的生产力提升,使我们能够对客户更具响应能力,并更快地为客户生产更好的软件。这并不是说它适合所有情况。选择开发平台从来都不是一个简单的选择,尤其是在它通常更多地是一个社会选择而不是技术选择的情况下。但标题结论是,Ruby 是一个值得考虑的选择,值得我们将其保留在我们的工具箱中。

这里一个有趣的问题是其他不太常见的语言的作用。我们是否应该使用 Groovy、F#、Python、Smalltalk 等?我不会感到惊讶,因为我们对 Ruby 看到的许多权衡对于这些其他语言来说也是正确的。我希望将来我们能看到其中一些被添加到我们的工具箱中。

我还应该强调,在使用这些语言和主流 Java/C# 选项时,这不是一个非此即彼的问题。我一直主张,使用像 Java/C# 这样的语言的开发团队也应该使用脚本语言来执行各种支持任务。Ruby 是一个很好的选择,我们看到这种组合在我们的项目中越来越多。随着 JVM 和 CLR 上对这些语言的支持不断增加,我们看到了更多将不同语言与不同优势混合的机会——Neal Ford 将这种方法称为多语言编程

一些开发技巧

在本节的最后,我将回顾一下我们从使用 Ruby 中吸取的一些经验教训。

使用 Active Record 进行测试

在我们开始使用 Ruby 的时候,就如何最好地在 Rails 中的 Active Record 数据库层存在的情况下组织测试存在争议。基本问题是,大多数情况下,企业应用程序的性能主要受数据库访问的影响。我们发现,通过使用测试替身,我们可以大大加快测试速度。拥有快速的测试对于我们以测试为中心的开发过程至关重要。Kent Beck 建议基本提交构建时间不超过十分钟。如今,我们的大多数项目都实现了这一点,而使用数据库替身是实现这一目标的重要组成部分。

Active Record 的问题在于,它将数据库访问代码与业务逻辑结合在一起,因此创建数据库替身相当困难。Mingle 团队对此的反应是接受 Rails 将数据库紧密绑定,因此在真实数据库上运行所有提交测试。

相反的观点是由亚特兰大和泽西团队最坚定地提倡的。Ruby 具有一个强大的功能,允许你在运行时重新定义方法。你可以使用它来获取一个 Active Record 类,并将该类中的数据库访问方法重新定义为存根。该团队开始使用 gem unitrecord 来帮助实现这一点。

在三年中,我们还没有看到这场辩论中出现普遍接受的胜利者。Mingle 团队在约 8 分钟内对真实 postgres 数据库运行了数千次测试。(他们并行执行测试以利用多个核心。)亚特兰大和泽西团队认为,他们的提交测试在使用存根的情况下运行 2 分钟,而没有存根的情况下运行 8 分钟,这很有价值。权衡是直接数据库测试的简单性与存根测试的更快提交构建。

虽然这两个团队对他们在这场辩论中的立场都感到满意,但使用存根给亚特兰大/泽西团队带来了另一个问题。随着团队越来越熟悉使用方法存根,他们越来越多地使用它——不可避免地过度使用,单元测试会将除被测试方法之外的所有方法都存根。这里的问题,就像使用替身一样,是测试脆弱。当你改变应用程序的行为时,你还要改变许多模仿旧行为的替身。这种过度使用导致这两个团队都从存根单元测试转向使用更多带有直接数据库访问的 Rails 风格的功能测试。

Active Record 泄漏

人们报告的常见情况是花费时间在 SQL 上折腾。Active Record 在很大程度上隐藏了程序员的数据库访问,但它无法完全隐藏它——本质上是抽象泄漏。因此,人们必须花费相当多的时间直接使用 SQL。

这种泄漏是对象/关系映射框架的常见特征。几乎每次我和项目中的人交谈时,他们都会说 O/R 映射框架大约 80-90% 的时间有效地隐藏了 SQL,但你确实需要花费一些时间在 SQL 上工作才能获得不错的性能。因此,在这方面,Active Record 与任何其他 O/R 映射器并没有什么不同。

事实上,我听到的一个评论是,使用 Active Record,抽象可以干净地打破。在与 DHH 聊天时,他总是强调他认为使用关系数据库的开发人员应该知道如何使用 SQL。Active Record 简化了常见情况,但一旦你开始处理更复杂的情况,它就期望你直接使用 SQL。

我不认为 O/R 抽象的泄漏是对这些框架的谴责。这些框架的目的是通过使常见操作更容易来提高生产力。它允许团队将精力集中在真正重要的少数情况下。问题只出现在团队认为抽象是密不透风的,并且没有努力使用 SQL 的时候。这是一个常见的错误,但不是在正确使用时放弃 O/R 框架的真正优势的理由。

长时间运行的请求

我们遇到的一个常见问题是,应用程序在承担需要一些时间才能完成的任务时会陷入混乱。如果这样做很天真,会导致 Web 请求处理程序在处理请求时长时间处于黑暗状态,令人担忧。

这是任何人机界面中非常常见的问题,并且有一个常见的解决方案——将任务交给后台进程或线程。任何使用过富客户端 GUI 应用程序的人都会认识到这样做。但是,如果这种移交和通信做得不好,人们会陷入困境。

我更喜欢的方式,幸运的是,大多数 ThoughtWorkers 也同意,是使用一个 actor。在这个模型中,Web 请求处理程序会获取任何长时间运行的任务,将其包装在一个命令中,并将其放入队列中。然后,后台 actor 会监控队列,从队列中获取命令并执行它们,并在完成每个命令时向人机交互 actor 发出信号。队列通常从数据库中的一个表开始,然后根据需要迁移到一个真正的消息队列系统。

就像 Active Record 的泄漏一样,我指出这一点不是因为它对 Rails 应用程序来说是一个特殊的问题,我们在各种应用程序中都看到了这一点。值得指出的是,对于许多使用 Rails 的人来说,似乎很容易忘记这种事情会发生,因此他们需要使用这种模式。我们发现 Rails 使 Web 应用程序的大部分重复部分更容易、更快地完成——但更复杂的部分仍然存在。

部署

Rails 应用程序很容易构建,但遗憾的是部署起来非常麻烦。使用多个 mongrel Web 服务器的包的常见场景在最好的情况下也相当难以设置。由于与 Ruby 经验中大部分其他部分的流畅性形成对比,这一点非常突出。

目前的一致意见是,Phusion Passenger 使整个过程变得非常简单,现在是使用 MRI 的推荐部署方法。

我们也是使用 JRuby 进行部署的忠实粉丝。JRuby 允许人们使用标准的 Java Web-App 堆栈,这可以在许多企业环境中使其更容易处理。Mingle 也使用这种方法来简化客户的安装。事实上,Mingle 团队使用 MRI 进行所有开发,但部署到 JRuby。他们这样做是因为 MRI 更快的启动时间使其开发速度更快。(JRuby 需要 JVM 启动,这明显很犹豫。)

控制 Gems

Ruby 包含一个包管理系统 Ruby Gems,它可以轻松地安装和升级第三方库。Rails 也有插件可以为 Rails 执行类似的任务。这些都是好工具,但如果不同的机器设置了不同版本的不同库,团队很容易陷入混乱。

处理这个问题有两种方法。第一种方法是获取所有第三方库的源代码副本,并将其检入源代码管理。这样,简单的检出操作就可以获取所有库的正确版本。第二种方法是使用一个脚本下载并激活所有库的正确版本。这个脚本需要保存在源代码管理中。

类似地,大多数团队也会获取 Rails 源代码的副本。这使他们能够直接对 Rails 应用补丁,以修复任何错误或其他重要问题。然后,这些补丁可以发送给核心团队。使用分布式版本控制系统(如 git)使管理变得容易得多。这当然比我们过去不得不反编译和修补 Java 应用程序服务器要容易得多。

安排更新时间

Ruby,特别是 Rails,发展很快。Rails 系统经常更新,并包含我们想要使用的功能。我们发现,我们需要确保安排时间处理 Rails 更新,并将这些更新纳入计划过程。它们比其他平台更重要,但好消息是,有一系列新的功能不断涌现。

在 Windows 上开发

Ruby 出生于 Unix 世界,大多数加入该平台的人使用斜杠作为目录路径。在 Windows 平台上运行、部署和开发 Ruby 是可能的,但它也更加棘手。我们的一般建议是使用 Unix 平台进行所有开发。Mac 通常是首选,但许多人也会使用其他 FOSS Unix。

我们希望随着 Iron Ruby 的发展,这种情况会改变。能够选择在基本 Unix、JVM 或 CLR 上部署 Ruby 应用程序将是一件好事。实际上,这将使 Ruby 成为跨多个平台运行时支持的特别灵活的选择。它还有助于我们的 .NET 项目将 Ruby 作为与主线 .NET 语言结合使用的脚本语言。


致谢

除了往常一样,如果没有许多同事的合作,我就不可能把这一切拼凑起来。虽然我多年来一直在使用 Ruby 进行许多个人工作,但一个人拼凑自己的个人网站和我们与客户一起做的应用程序之间存在很大差异。我感谢我的许多同事抽出时间为我提供我需要的信息,以便真正评估 Ruby 的价值。

与任何 Ruby 用户一样,我们也要感谢更广泛的 Ruby 和 Rails 社区。对于任何开源工作,社区的作用都是至关重要的,因此,Thoughtworks 向所有 Ruby 黑客和 Ruby 开发者说:ありがとうございました。

重大修订

2009 年 6 月 11 日:首次发布在 martinfowler.com 上

2009 年 6 月 3 日:TW 内部审查草案