跳到主要内容
工程的博客

Scala at Scale at Databricks

通过李Haoyi

2021年12月3日 工程的博客

分享这篇文章

Databricks拥有数百名开发人员和数百万行代码,是最大的Scala商店之一。这篇文章将在Databricks上对Scala进行全面的介绍,从它的开始到使用、风格、工具和挑战。我们将涵盖从云基础设施和定制语言工具到围绕管理大型Scala代码库的人工流程等主题。从这篇文章中,您将了解到使Scala at Databricks工作的所有大大小小的事情,对于任何支持在不断发展的组织中使用Scala的人来说,这都是一个有用的案例研究。

使用

Databricks由Apache Spark™的最初创建者构建,并开始作为分布式Scala集合。之所以选择Scala,是因为它是少数拥有可序列化lambda函数的语言之一,而且它的JVM运行时允许与基于hadoop的大数据生态系统进行轻松的互操作。从那时起,Spark和Databricks的发展远远超出了任何人最初的想象。关于这种增长的细节超出了本文的范围,但是最初的Scala基础仍然存在。

语言分解

今天的Scala有点通用语在砖。看看我们的代码库,最流行的语言是Scala,有数百万行,其次是Jsonnet(用于配置管理),Python(脚本,ML, PySpark)和Typescript (Web)。Scala无处不在:分布式大数据处理、后端服务,甚至一些CLI工具和脚本/粘合代码。Databricks并不反对编写非scala代码;我们也有高性能的c++代码,一些Jenkins Groovy,在Nginx中运行的Lua,一些Go和其他东西。但是大量代码仍然在Scala中。

语言分解

Scala的风格

Scala是一种灵活的语言;它可以被写成类似java的面向对象语言,类似haskell的函数式语言,或者类似python的脚本语言。如果我必须描述Databricks编写的Scala风格,我认为它是50% java风格,30% python风格,20%函数风格:

  • 后端服务往往严重依赖于Java库:Netty、Jetty、Jackson、AWS/Azure/GCP-Java-SDK等。
  • 类脚本代码通常使用com-lihaoyi生态系统os-lib, requests-scala, upickle等
  • 我们始终使用基本的函数式编程特性:如函数字面量、不可变数据、case-class层次结构、模式匹配、集合转换等。
  • 不使用“典型的”Scala框架:Play、Akka、Scalaz、Cats、ZIO等。

虽然Scala风格在代码库中有所不同,但它通常保持在更好的java风格和类型安全的python风格之间,具有一些基本的功能特性。即使没有Scala背景或培训,Databricks的新手在阅读代码时通常也没有任何问题,并且可以立即开始做出贡献。Databricks的复杂系统在理解和贡献方面有自己的障碍(编写大规模高性能多云系统并非易事!),但学习足够多的Scala来提高效率通常不是问题。

Scala熟练

几乎Databricks的每个人都写一些Scala,但很少有人是爱好者。我们没有接受过正式的Scala培训。人们有各种各样的背景,在第一天就开始编写Scala,然后随着时间的推移慢慢掌握更多的函数特性。由此产生的类似java - python的风格就是这样的自然结果。

尽管几乎每个人都写一些Scala,但Databricks的大多数人并没有深入研究这门语言。人们首先是基础设施工程师、数据工程师、ML工程师、产品工程师等等。有时,我们不得不深入地处理一些棘手的问题(例如,阴影,反射,宏等),但这远远超出了大多数Databricks工程师需要处理的标准。

当地的工具

总的来说,大多数Databricks代码都存在于单回购中。Databricks将Bazel构建工具用于mono-repo中的所有内容:Scala、Python、c++、Groovy、Jsonnet配置文件、Docker容器、Protobuf代码生成器等。考虑到我们是从Scala开始的,这曾经都是SBT,但我们基本上迁移到Bazel,因为它更好地支持大型代码库。我们仍然在SBT或Mill上维护一些较小的开源回购,当我们试图完成迁移时,一些代码具有并行的Bazel/SBT构建,但我们的大部分代码和基础设施都是围绕Bazel构建的。

Bazel在Databricks

Bazel非常适合大型团队。默认情况下,它是唯一在单独的LXC容器中运行所有构建步骤和测试的构建工具,这有助于避免构建部分之间的意外交互。默认情况下,它是并行的和增量的,随着代码库规模的增长,这一点越来越重要。一旦设置好并开始工作,它在每个人的笔记本电脑或其他机器上的工作方式都是一样的。虽然不是100%的密封,但在实践中,它在很大程度上足以避免与测试间干扰或意外依赖相关的大量问题,这对于随着代码库的增长保持构建的可靠性是至关重要的。我们在博客文章中讨论了使用Bazel来并行化和加速测试运行与Bazel在Databricks的快速并行测试

Bazel的缺点就是需要一个庞大的团队。Bazel封装了从python生成makefiles到现在20年的演变过程,它显示了:有很多积累的麻烦、尖锐的边缘和复杂性。虽然Bazel一旦设置好就可以很好地工作,但是配置Bazel来执行您想要的操作可能是一个挑战。你基本上需要一个2-4人的团队专门研究Bazel才能让它正常运行。

此外,通过使用Bazel,您放弃了许多现有的开源工具和知识。有些图书馆会让你这么做pip安装什么东西吗?提供了一个SBT/Maven/Gradle/Mill插件的工作?某些可执行文件想要成为apt-get安装爱德华吗?对于Bazel,你不能使用这些,而且需要自己编写大量的集成。虽然设置任何单独的集成都不是太难,但您最终往往需要很多集成,这将成为相当可观的时间投资。

虽然这些缺点对于大型组织来说是可以接受的成本,但对于个人项目和小团队来说,这使得Bazel完全不适合。甚至Databricks也有一些小的开源代码库仍然在SBT或Mill上,在那里Bazel没有意义。然而,对于我们的大部分代码和开发人员来说,它们都在Bazel上。

编译时间

Scala编译速度是一个常见的问题,我们投入了大量的精力来缓解这个问题:

  • 设置Bazel使用一个长期存在的后台编译工作线程来编译Scala,以保持编译器JVM的热性和快速性。
  • 为想要使用它的人设置增量编译(通过Zinc)和并行编译(通过Hydra)。
  • 升级到最新版本的Scala 2.12,比以前的版本快得多。

关于这项工作的更多细节在博客文章中在Databricks使用Bazel快速构建Scala.虽然Scala编译器仍然不是特别快,但我们在这方面的投资意味着Scala编译时间并不是我们的工程师所面临的主要痛点。

交叉建筑物

交叉构建是Scala团队的另一个常见问题:Scala在主要版本之间是二进制不兼容的,这意味着支持多个版本的代码需要分别为两个版本编译。即使忽略Scala,支持多个Spark版本也有类似的要求。Databricks的Bazel-Scala集成内置了交叉构建,其中每个构建目标(相当于“模块”或“子项目”)可以指定它支持的Scala版本列表:

cross_scala_lib (base_name =“my_lib”Cross_scala_versions = [“2.11”“2.12”),Cross_deps = [“other_lib”),SRCS = [“Test.scala”),

有了以上的输入,我们的cross_scala_lib函数生成my_lib_2.11而且my_lib_2.12版本的构建目标,并依赖于相应的版本other_lib_2.11而且other_lib_2.12目标。实际上,每个Scala版本都在更大的Bazel构建图中获得自己的构建目标子图。

实际上,每个Scala版本都在更大的Bazel构建图中获得自己的构建目标子图。

这种用于交叉构建的复制构建图的风格与更传统的交叉构建机制相比有几个优点,后者涉及在构建工具中设置的全局配置标志(例如,+ + 2.12.12在SBT):

  • 同一构建目标的不同版本会自动并行构建和测试,因为它们都是同一个大Bazel构建图的一部分。
  • 开发人员可以清楚地看到哪个构建目标支持哪个Scala版本。
  • 我们可以同时使用多个Scala版本,例如,部署一个多jvm应用程序,其中Scala 2.12上的后端服务与Scala 2.11上的Spark驱动程序交互。
  • 我们可以逐步推出对新的Scala版本的支持,这极大地简化了迁移,因为从旧版本到新版本之间没有“大爆炸”的切换。

虽然这种交叉构建技术起源于Databricks,用于我们自己的内部构建,但它已经传播到其他地方:到build工具的交叉构建支持,甚至是旧的SBT构建工具viaSBT-CrossProject

管理第三方依赖关系

第三方依赖关系是预先解析和镜像的;依赖项解析从“热”编辑-编译-测试路径中移除,只有在更新/添加依赖项时才需要重新运行。这是Databricks代码库中的一种常见模式。

我们使用的每个外部下载位置都不可避免地会宕机;无论是Maven中心不稳定,还是PyPI中断,甚至是www.7-zip.org回到500年代。不知何故,这似乎并不重要我们正在下载什么from:外部下载不可避免地会停止工作,这会导致Databricks开发人员停机和沮丧。

我们镜像依赖关系的方式类似于lockfile,这在某些生态系统中很常见:当您更改第三方依赖关系时,您运行一个脚本将lockfile更新为最新解析的依赖关系集。但我们添加了一些曲折:

  • 我们不只是记录依赖项版本,而是将各自的依赖项镜像到我们的内部包存储库。因此,我们不仅避免了依赖第三方包主机进行版本解析,也避免了依赖它们进行下载。
  • 我们不是记录依赖关系的平面列表,而是记录它们之间的依赖关系图。这允许任何依赖于第三方包的内部构建目标精确地引入可传递依赖项,而无需通过网络进行接触。
  • 通过解析多个锁文件,我们可以在同一个代码库中管理多个不兼容的依赖集。这让我们可以灵活地处理不兼容的生态系统,例如Spark 2.4和Spark 3.0,同时仍然可以保证,只要有人坚持使用来自单个锁文件的依赖关系,他们就不会有任何意外的依赖冲突。

这种管理外部依赖关系的方法为我们提供了两全其美的方法。,

正如您所看到的,虽然修改外部依赖关系的“maven/update”过程(虚线箭头)需要访问第三方包回购,但更常见的“bazel build”过程(实线箭头)完全发生在我们控制的代码和基础设施中。

这种管理外部依赖关系的方法为我们提供了两全其美的方法。我们得到了像Maven或SBT这样的工具所提供的细粒度依赖项解决方案,同时还提供了像Pip或Npm这样基于锁文件的工具所提供的固定依赖项版本,以及运行我们自己的包镜像的密封性。这与大多数开源构建工具管理第三方依赖关系的方式不同,但在许多方面它更好。与直接使用第三方包存储库作为构建的一部分的正常方式相比,以这种方式提供依赖关系更快、更可靠,而且不太可能受到第三方服务中断的影响。

产品毛羽工作流

也许我们本地开发经验中最后一个有趣的部分是linting:那些东西可能这是个好主意,但是有足够多的例外,你不能把它们变成错误。这个类别包括Scalafmt, Scalastyle,编译器警告等等。要处理这些问题,我们:

  • 不要在本地开发期间强制执行lininter,这有助于简化开发循环,保持开发速度快。
  • 合并到master时强制linters;这确保了master中的代码是高质量的。
  • 提供逃生舱口,在场景中的lintter是错误的,需要被否决。

这种策略同样适用于所有linter,只是在语法上有细微差别(例如,/ / scalafmt:vs/ / scalastyle:vs@SuppressWarnings作为逃生舱口)。这将从终端中滚动过去的瞬态事物的警告转换为代码中出现的长期构件:

@SupressWarnings数组(“匹配五月详尽的”)val targetCapacityTypefleetSpec.fleetOption匹配{情况下FleetOption.SpotOption (_)>“现货”情况下FleetOption.OnDemandOption (_)>-需求”

所有这些关于棉绒的仪式的目的是迫使人们注意棉绒错误。从本质上讲,linter总是有假阳性,但大多数时候,它们突出了真正的代码气味和问题。强迫人们用注释来掩盖警告,迫使作者和审稿人都要考虑每个警告,并决定它是真的假阳性还是突出了一个真正的问题。这种方法还避免了常见的故障模式,即警告堆积在控制台输出中而不被注意。最后,我们可以更积极地推出新的linter,因为即使没有100%的准确性,假阳性也可以在适当考虑后被覆盖。

远程基础设施

除了在您的机器上本地运行的构建工具外,Databricks的Scala开发还受到一些关键服务的支持。它们在我们的AWS开发和测试环境中运行,对于Databricks的开发工作取得进展至关重要。

Bazel远程缓存

Bazel远程缓存的思想很简单:永远不要在公司范围内编译相同的东西两次。如果您正在编译同事在他们的笔记本电脑上编译的东西,使用相同的输入,那么您应该能够简单地下载他们之前编译的工件。

Bazel远程缓存的思想很简单:永远不要在公司范围内编译相同的东西两次

远程缓存是Bazel构建工具的一个特性,但需要一个实现Bazel远程缓存协议.当时,还没有好的开源实现,所以我们构建了自己的:一个构建在上面的小型golang服务器GroupCache和S3。这大大加快了工作速度,特别是如果您正在从最近的主版本进行增量更改,并且几乎所有内容都已经由某个同事或CI机器编译完成。

Bazel远程缓存也不是没有问题。这又是一项我们需要照顾的服务。有时会缓存坏的工件,导致构建失败。尽管如此,Bazel远程缓存的速度优势足以让我们的开发过程离不开它。

Devbox

Databricks Devbox的想法很简单:在本地编辑代码,在一个强大的云虚拟机上运行,与所有的云基础设施共存。

典型的工作流程是在Intellij中编辑代码,运行bash命令在devbox上构建/测试/部署。下面你可以看到devbox的运行情况:每当用户在IntelliJ中编辑代码时,菜单栏中的绿色“勾”图标会短暂地闪烁到蓝色的“同步”图标,然后再闪烁回绿色,表示同步已经完成:

Devbox有一个定制的高性能文件同步器,可以将本地笔记本电脑的代码更改带到远程VM。连接到fseventsOS-X和inotify在Linux上,它可以实时响应代码更改。当您从编辑器单击到控制台时,您的代码已经同步并可以使用。

这比在笔记本电脑上本地开发有很多优势:

  • Devbox运行Linux,它与我们的CI环境完全相同,而且比开发人员的Mac-OSX笔记本电脑更接近我们的生产环境。这有助于确保您的代码在dev、CI和prod中表现相同。Devbox运行Linux,它与Databricks CI环境相同,比开发人员的Mac-OSX笔记本电脑更接近我们的生产环境。
  • Devbox与我们的kubernetes集群、远程缓存和docker注册表一起生活在EC2中。这意味着在devbox和您所关心的任何东西之间有出色的网络性能。Databricks Devbox与我们的Kubernetes-clusters/remote-cache/docker- registry一起生活在EC2中。这意味着您所关心的任何事情都可以获得出色的网络性能。
  • Bazel/Docker/Scalac不需要与IntelliJ/Youtube/Hangouts争夺系统资源。你的笔记本电脑不会变得很热,风扇不会旋转,你的操作系统(主要是Mac-OSX,适用于Databricks开发人员)也不会滞后。有了Databricks, Bazel/Docker/Scalac不再需要与IntelliJ/Youtube/Hangouts争夺系统资源。
  • Devbox是可定制的,可以运行任何EC2实例类型。想要RAID0-ed临时磁盘以获得更好的文件系统性能?96核和384gb内存来测试一些计算量大的东西?去吧!我们在不使用实例时关闭实例,因此即使使用更昂贵的实例也不会在短时间内耗尽财力。使用Databricks, Devbox是可定制的,可以运行任何EC2实例类型。
  • Devbox是一次性的。apt-get安装错误的东西?不小心rm一些你不应该使用的系统文件?某些第三方安装程序使您的系统处于糟糕的状态?它只是一个EC2实例,因此请丢弃它并获取一个新的实例。

与在Devbox上做事情的速度差异很大:几分钟的上传或下载缩短到几秒钟。需要部署到Kubernetes?上传容器到docker注册表?从远程缓存下载大二进制文件?在带有10G数据中心网络的Devbox上进行操作,比在家用或办公室的笔记本电脑上进行操作要快几个数量级。即使是本地计算/磁盘绑定的工作流,在Devbox上运行也比在开发人员的笔记本电脑上运行更快。

Runbot

Runbot是一个定制的CI平台,用Scalabob体育客户端下载编写,管理我们的弹性“裸EC2”集群,拥有100个实例和10,000个核心。基本上就是一个手工制作的詹金斯,但有我们想要的东西,也没有我们不想要的东西。它大约是Scala的10K-LOC,用于验证合并到Databricks主存储库中的所有拉请求。

Databricks Runbot是一个定制的CI平台,用Scalabob体育客户端下载编写,管理我们的弹性

Runbot利用Bazel构建图,根据更改的代码有选择地对拉请求运行测试,旨在尽快向开发人员返回有意义的CI结果。Runbot还集成了我们其他的开发基础设施:

  • 我们有意让Runbot CI测试环境和Devbox远程开发环境尽可能地相似——甚至运行相同的ami——以尽量避免代码在其中一个中表现不同的情况。
  • Runbot的工作实例充分利用了Bazel远程缓存,允许它们跳过“样板”构建步骤,只重新编译和重新测试可能受到拉请求影响的内容。

关于Runbot系统的更详细介绍可以在博客文章中找到开发Databricks的Runbot CI解决方案

测试碎片

Test Shards可以让开发人员轻松地启动一个密封的databicks -in-a-box,让您通过浏览器或API运行集成测试或手动测试。由于Databricks是一款支持Amazon/Azure/谷歌云平台的多云产品,因此Databricks的Test Shards可以类似地在任何云上启bob体育客户端下载动,从而为您的代码更改提供集成测试和手动测试的地方。

Test Shards可以让开发人员轻松地启动一个密封的databicks -in-a-box,让您通过浏览器或API运行集成测试或手动测试。

一个测试碎片或多或少地包含整个Databricks平台——我们所有的后端服务——只是减少了资源分配和一些简化的基础设施。bob体育客户端下载其中大多数是Scala服务,尽管我们也混合了一些其他语言。

维护Databricks的测试碎片是一个持续的挑战:

  • 我们的测试碎片旨在以尽可能高的保真度准确地反映当前的生产环境。
  • 由于测试碎片被用作迭代开发循环的一部分,因此创建和更新它们应该尽可能快。
  • 我们有数百名开发人员使用测试碎片,为每个测试碎片启动一个完整的生产部署是不可行的,我们必须在保持保真的同时找到捷径。
  • 我们的生产环境正在迅速发展,有新的服务、新的基础设施组件,有时甚至是新的云平台,我们的测试碎片必须跟上。bob体育客户端下载

测试碎片需要大规模和复杂的基础设施,我们遇到了各种我们从未想象过存在的限制。当Azure帐户的资源组用完时该怎么办?当AWS负载均衡器创建成为瓶颈时?当pod的数量使你的Kubernetes集群开始行为不端?虽然“盒子里的数据”听起来很简单,但为数百名开发人员提供这样一个环境的可行性是一个持续的挑战。使用了许多创造性的技术来处理上面的四个约束,并确保Databricks的开发人员使用测试碎片的体验尽可能地顺畅。

Databricks目前在多个云和地区运行数百个测试碎片。尽管维护这样一个环境很有挑战性,但是测试碎片是没有商量余地的。它们提供了一个关键的集成和手动测试环境,然后将您的代码合并到主代码中并交付到登台和生产。

好的部分

Scala/JVM的性能通常都很棒

Databricks并不缺少性能问题,有些是过去的,有些是正在进行的。然而,实际上这些问题都不是Scala或JVM造成的。

这并不是说Databricks有时没有性能问题。然而,它们往往存在于数据库查询、rpc或整个系统架构中。虽然有时一些低效编写的应用程序级代码会导致运行速度变慢,但这类问题通常可以通过分析器和一些重构直接解决。

Scala允许我们编写一些令人惊讶的高性能代码,例如,我们的Sjsonnet配置编译器比它所取代的c++实现快几个数量级,就像我们之前的博客文章中讨论的那样编写一个更快的Jsonnet编译器

但总的来说,Scala/JVM良好性能的主要好处是我们思考得有多少Scala代码的计算性能。虽然性能在大规模分布式系统中是一个棘手的话题,但运行在JVM上的Scala代码的计算性能并不是问题。

灵活的通用语言使共享工具和专业知识变得容易

能够在整个组织中共享工具是非常棒的。我们可以在后端web服务、高性能大数据运行时、小脚本和可执行文件上使用相同的构建工具集成、IDE集成、分析器、linter、代码风格等。

即使代码风格在整个组织中有所不同,所有相同的工具仍然适用,并且它足够熟悉,以至于语言对跳入其中的人没有任何障碍。

这在人力有限的情况下尤其重要。使用上面描述的丰富工具集来维护单个工具链已经是一项巨大的投资。即使我们拥有的语言数量很少,很明显,“二级”语言工具链并不像我们的Scala工具链那样完善,而且将它们提升到同一水平的难度也很明显。为了支持各种不同的语言,不得不重复Scala工具链投资N次,这将是一项非常昂贵的工作,我们迄今为止已经设法避免了。

Scala非常适合编写脚本/胶水!

人们通常认为Scala是用于编译器或Serious Business™后端服务的语言。然而,我们发现Scala也是一种出色的脚本类粘合代码语言!我指的是代码处理子进程、与HTTP api对话、破坏JSON等。虽然Scala JVM运行时的高性能对于脚本来说并不重要,但许多其他平台的好处仍然适用:bob体育客户端下载

  • Scala很简洁。根据所使用的库的不同,它可以像Python或Ruby这样的“传统”脚本语言一样,甚至更简洁,而且同样可读。
  • 脚本/粘合代码通常是最难进行单元测试的。集成测试虽然是可能的,但通常是缓慢而痛苦的;我们不止一次因为运行了太多的集成测试而被第三方服务扼杀!在这种环境中,拥有基本的编译时检查是天赐良机。
  • 部署很好:例如,汇编jar远比Python的pex好,因为它们更标准、简单、密封、性能等。尝试在不同的环境中部署Python代码一直是一个令人头痛的问题,对于某些人来说总是如此酿造安装apt-get安装这会导致我们部署和测试的Python可执行文件崩溃。这在Scala程序集jar中不会发生。

Scala/JVM并不适合编写脚本:对于任何重要的程序,JVM启动开销为0.5-1秒,内存使用很高,编辑/编译/运行Scala程序的迭代循环相对较慢。尽管如此,我们发现使用Scala比使用传统的脚本语言(如Python)有很多好处,并且我们已经在一些人们自然希望使用脚本语言的场景中引入了Scala。甚至Scala的REPL也已被证明是一种有价值的工具,可以方便灵活地与内部和第三方服务进行交互。

结论

Scala at Databricks已被证明是我们构建的坚实基础

Scala并非没有挑战或问题,但任何其他语言或平台都是如此。bob体育客户端下载运行动态语言的大型组织不可避免地投入大量精力来加速它们或添加编译时检查;开发其他静态语言的大型组织不可避免地会在dsl或其他工具上投入精力,试图加快开发速度。虽然Scala没有这两个问题,但它也有自己的问题,我们必须付出努力来克服。

有趣的一点是,我们的许多工具和技术是多么通用。我们的CI系统、开发盒、远程缓存、测试碎片等都不是特定于scala的。我们的依赖管理或检测策略也是如此。其中大部分都适用于任何语言或平台,对编写Python、Typescript或c++的开发人员bob体育客户端下载和编写Scala的开发人员都有好处。事实证明,Scala并不特别;Scala开发人员面临着使用其他语言的开发人员面临的许多相同的问题,有许多相同的解决方案。

另一个有趣的事情是Databricks与Scala生态系统的其他部分是多么的独立;我们从未真正接受过“响应式”思维模式或“核心函数式编程”思维模式。我们做交叉构建、依赖管理和linting等事情的方式与社区中的大多数人非常不同。尽管如此,或者正因为如此,我们已经能够毫无问题地扩展使用Scala的工程团队,并从使用Scala作为整个组织的通用语言中获益。

Databricks对Scala并不是特别教条。我们首先是大数据工程师、基础设施工程师和产品工程师。我们的工程师想要更快的编译时间,更好的IDE支持,或者更清晰的错误消息,并且通常对挑战Scala语言的极限不感兴趣。我们在有意义的地方使用不同的语言,无论是通过Jsonnet进行配置管理,使用Python进行机器学习,还是使用c++进行高性能数据处理。随着业务和团队的发展,我们不可避免地会看到某种程度的分歧和分裂。尽管如此,我们正在从JVM上的Scala统一平台和工具中获益,并希望尽可能长时间地利用这种好处。bob体育客户端下载

Databricks是目前最大的Scala商店之一,拥有不断壮大的团队和不断壮大的业务。如果您认为我们的Scala和开发方法与您产生了共鸣,那么您绝对应该这样做来和我们一起工作吧

免费试用Databricks

相关的帖子

看到所有工程的博客的帖子