将GitHub升级到Ruby 2.7

2020-08-26 14:06:10

经过几个月的工作,我们在7月份将GitHub部署到使用Ruby2.7的产品中。对于那些不熟悉GitHub堆栈的人,我们从一开始就在Ruby上运行。许多年前,我们在Ruby(和Rails!)的分支上运行GitHub。虽然这种情况已经有一段时间了,但这段经历告诉我们,跟上新发布的版本是多么重要。

Ruby 2.7是一个独特的升级,因为Ruby Core团队已经否决了关键字参数的行为方式。在此版本中,当方法需要关键字参数时,Ruby的未来版本将不再接受传递选项散列。在GitHub,我们致力于在Ruby和Rails上运行免弃版,以防止在未来的升级中落后。尽早识别重大更改非常重要,这样我们才能在必要时发展应用程序。

为了运行不推荐使用的Ruby2.7,我们必须修复超过11k的警告。修复这么多警告(其中一些警告来自外部库)需要大量的协调和团队合作。为了取得成功,我们需要一个坚实的战略来分享工作。

就像我们在升级Rurails时所做的那样,我们通过使用环境变量将我们的应用程序设置为在Ruby2.6和Ruby2.7中都是双引导的。这使得我们很容易进行向后兼容的更改,将这些更改合并到主分支,并避免为升级维护长时间运行的分支。这也使得其他需要进行更改的工程团队可以更轻松地使用新的Ruby版本运行他们的系统。由于我们的应用程序非常大(超过400k行!)。每天有多少变化(100个PR!),这极大地简化了我们的升级过程。

一旦我们运行了构建,我们还没有完全准备好请求其他团队帮助修复警告。因为Ruby警告只是测试输出中的字符串,所以我们需要捕获弃用,并将它们转换为每个团队需要修复的列表。

为了实现这一点,我们用猴子修补了Ruby中的安全警告模块。以下是我们猴子贴片的简化版本:

模块警告def self.warn(警告)root=ENV[";rails_root";].to_s+";/";warning=warning.gsub(root,";)line=caller_locations.find do|location|location.path.end_with?(";_test.rb";)end Origin=line&;.path&;.gsub(root,&#。[warning.chomp,Origin]STDERR.print(消息)结束。

该修补程序将弃用警告和导致警告的测试路径存储在将警告写入文件然后进行处理的EWarningCollector对象中:

Class WarningsCollector<;ParallelCollector def process filename=";warnings.txt";path=File.join(dir,filename)File.open(path,";a";)do|f|@data.each do|message,Origin|f.put[message,Origin].join(";*^.^*";)#ascii art,以便我们以后可以拆分它。结束结束脚本=File.absolute_path(";../script/process-ruby-warnings";,__FILE__)系统(脚本,目录)结束。

*WarningCollector#process方法将所有警告存储在名为wwarnings.txt的文件中。然后,我们使用CODEOWNERS解析警告,并将其转换为对应于每个所属团队的文件。

一旦我们处理了所有警告,我们就为那些团队打开了问题,这些问题提供了在新的Ruby版本中引导应用程序的易于遵循的指导。我们的警告报告包括发出警告的文件、警告本身和触发警告的测试套件。它们看起来是这样的:

-[x]`app/Jobs/delete_job.rb`-**警告**-第16行:警告:不推荐使用最后一个参数作为关键字参数;可能应该将**添加到调用中-**触发这些警告的测试套件**-test/job/delete_job.rb。

此流程帮助我们避免了跨团队的重复工作,并使确定每个警告的所有权和状态变得简单。

我们跟踪了Ruby2.7CI版本中的警告计数,以确保新代码不会引入新警告。几个月后,我们与40个团队、30多个GEM升级和11k个警告进行了协调,我们的CI构建是100%无警告的。未维护的宝石被维护的宝石取代。一旦我们修复了警告,我们就修改了猴子补丁,在Ruby2.7中引发了错误,这确保了所有进入GitHub代码库的新代码都是无警告的。

您可能正在阅读这篇文章,并想知道为什么值得做所有这些工作,并在Ruby升级上投入工程资源和时间。如果您编写Ruby已经有一段时间了,您可能已经意识到这个特殊升级的困难。在12月份发布之前,这就是Ruby社区讨论的话题。不管这次升级有多难,我们都看到了令人印象深刻的性能提升。Ruby Core团队正在很好地实现Ruby 3.0速度提高3倍的承诺。

首先,我们看到应用程序在生产模式下启动所需的时间减少了。在生产环境中(这是当整个应用程序被紧急加载时),我们看到引导时间从平均约90秒下降到约70秒。那是20秒的下降。这是一张图表:

更快的引导时间意味着更快的部署,这意味着您可以更快地获得我们的功能、错误修复和性能改进!

除了引导时间的改善外,我们还看到对象分配减少了,从约780k分配减少到约668k分配。对象分配会影响可用内存,因此尽可能降低这些数字很重要。

除了升级的性能优势外,确保您使用最新版本的语言和框架有助于保持应用程序的健康。通过这个过程,我们发现了许多不再在应用程序中使用的无主代码,并将其删除。我们还借此机会删除或替换了应用程序中未维护的gem。

对于维护的gem,我们通过为我们的应用程序中发出警告的任何gem发送补丁来回馈社区,这些gems包括rails、rails控制器测试、水豚、工厂机器人、视图组件、POSIX-SPOWN、GitHub-DS、Rruby-Kafka等等。GitHub坚信支持开源社区,升级是我们直接做到这一点的众多方式之一。

部署任何重大升级都有风险,但在GitHub,我们设计的流程可以大幅降低这种风险。

对于Ruby和Rails升级,我们运行双构建,直到我们确定所有测试都通过并且代码稳定。此外,我们让所有从事核心产品点击工作的团队在试运行环境中测试他们的代码库区域,以确保新版本没有明显的问题。

推出升级是一件大事,因此我们谨慎地增加在新版本上运行的流量百分比,并验证每个部署在Sentry中没有错误,在Datadog中没有回归。对于此部署,我们推出了2%的流量,并很快发现了一个新的冻结字符串异常。由于我们的流程,我们能够快速回滚,在一个端点中看到错误的用户不到10个。

一旦我们修复了冻结的字符串异常,我们就重新启动了推出过程,并再次部署到2%的流量。在进入下一个百分比之前,我们等待15分钟:30%的Kubernetes分区。我们再次等待了大约15分钟,在确认没有倒退之后,我们又部署了30%的Kubernetes分区,总共部署了60%的Kubernetes分区。

最后,我们部署了30%的非Kubernetes部署分区。这些部署需要更长的时间,因为它们需要编译Ruby。等待Ruby编译15分钟有点让人伤脑筋,但一切都很顺利。从那里开始,我们进行了全面生产部署,并在30分钟后合并了升级。总体而言,整个部署花费了大约2个小时。

在GitHub,我们已经投资于构建部署Ruby和Rails升级的流程,这样我们就可以确信它们是尽可能低的风险。我们在部署Ruby升级时没有宕机,我们对客户的影响几乎为零。

对于任何想知道这次升级是否值得的公司来说,答案是:100%。即使没有性能改进,落后于Ruby升级也会对代码库的稳定性造成严重的负面影响。升级Ruby支持您的应用程序健康,提高性能,修复语言和框架错误,并引导语言的未来!

在GitHub,我们不仅相信开源社区,我们相信坚实的基础是迈向稳定、弹性和正常运行的应用程序的第一步。在最新版本的Ruby上运行可以帮助我们做到这一点。我们期待着Ruby2.8及以后的版本。升级愉快!