Brex – 使用 Kotlin 构建后端服务

2021-08-09 17:21:40

自 Brex 成立以来,我们的大部分后端代码都是用 Elixir 编写的。正如我们的联合创始人 Pedro 在这篇文章中所解释的那样,作为一家处于早期阶段的公司,我们在很大程度上取得了成功。 Elixir 是一种富有表现力的函数式语言,具有很强的可读性,并提供强大的并发控制。随着我们的一体化财务平台发展到包含更多的产品和服务,我们的工作负载和工作负载编排用例的复杂性和规模也不断扩大。构建和维护内部库以支持 Elixir 作为我们的主要后端语言变得非常耗时,而且生态系统中的其他用户似乎很少共享足够的用例来使 OSS 协作互惠互利。随着我们的代码库规模不断扩大,大规模重构成为一个真正的问题。虽然动态打字在我们年轻时提高了速度,但随着我们的成长,它减慢了我们的速度。由于所有这些原因,我们开始寻找一种语言,它可以提供我们对 Elixir 的大部分喜爱,并在我们的用例中解决它的缺点。编程语言的选择可能是主观的,并且经常引起争议。为了做出最适合我们业务的决定,我们根据我们喜欢 Elixir 的属性和我们遇到的痛点,创建了一个全面的评估标准来评估不同的语言。发展速度:未来会很美好,我们想快点实现。我们的选择需要帮助我们紧迫而专注地行动。在这里,类型系统是一个不可协商的选择。对不变性和函数式编程具有一流支持的语言很重要。我们想要快速的编译器和出色的开发人员工具。学习曲线:它应该使代码易于编写、易于阅读且不太难学。新工程师应该能够轻松上手。可维护性:构建金融系统意味着长期优化,我们希望我们的代码易于维护和更改。编写线程安全、可维护和可读的更简单的代码应该是惯用的和容易的。

社区、生态系统和工具:如果我们将更多精力集中在构建我们的一体化金融平台上,我们将更好地为客户服务。有了一个充满活力、包容性和不断发展的社区和生态系统,这会容易得多。可扩展性:语言需要随着我们的客户以及我们工程组织的规模而扩展。 Kotlin 在我们的准则上做得非常好,虽然它有很多出色的地方,但我们希望这篇文章更多地关注推动我们做出决定的因素。类型系统:我们知道具有现代类型系统的语言对于扩展我们的工程代码库和组织是必要的。类型推断、空安全、密封类、枚举类、数据类等语言特性使其成为真正引人注目的类型系统选择。尽管具有强大的功能集,但 Kotlin 编译器的运行速度非常快。 IntelliJ 集成使得在大型代码库中导航、读取和编写代码非常高效。最重要的是,虽然该语言包括一个现代的、强大的类型系统,但它相当容易学习和使用。生态系统:感谢 Kotlin 和更广泛的 JVM 周围令人难以置信的生态系统,我们能够站在巨人的肩膀上。我们发现社区非常友好、包容和快速发展。 Kotlin 与 Java 的互操作使得利用大量为 Java 编写的代码变得非常简单。重用久经考验的解决方案让我们有更多时间为企业构建 Brex 的一体化财务工具。现代语言特点:Kotlin 作为一种语言简洁、富有表现力且易于学习。 Kotlin 中的函数式编程提供了高级选项,但没有陡峭的学习曲线。它为不变性提供了一流的支持,包括一个用于不可变集合的标准库 性能:有大量证据表明构建在 JVM 之上的大规模高性能系统。虽然 Kotlin 的后端开发相对较新,但我们使用的大多数底层框架都经过了实战测试(gRPC、netty、JDBC 访问层)。在大多数情况下,生成的字节码与 Java 生成的字节码非常相似,因此这增加了我们的信心。此外,Kotlin 协程提供了出色的并发控制,并且可以轻松编写没有回调地狱的异步代码。

由于上述所有原因以及更多原因,它显然是一个强有力的候选人。也就是说,我们仍然想自己验证声明,更重要的是,说服工程师采用 Kotlin。任何足够复杂的技术挑战都有大量的技术和人员问题需要解决。 Brex 的大部分工程师都喜欢在 Elixir 工作。虽然我们都同意它不是最适合我们未来的,但 Kotlin 必须满足一个很高的标准。我们最初通过尝试在 Kotlin 中重写现有服务来构建原型。这项服务相当小,是 Brex 典型服务的理想选择。它涵盖了足够的基础,原型可以为我们提供早期验证。我们挑选框架和库选择的技术策略是由长寿驱动的。我们想押注行业同行使用的东西,而且不太可能过时。在正确的选择上花几天的时间可以为我们在未来节省数月或数年的努力。不用说,我们定义的好是基于我们的评估标准。对我们有用的可能对其他人不起作用,将来也可能对我们不起作用。知道我们使用了当时已知的所有信息,我们会睡得更好,并对我们的选择产生信心。我们对优化内部一致性和在 Kotlin 服务之间保持高度的工程移动性有着强烈的感受。内部一致性培养跨团队共享和分散的专业知识。我们花了大部分时间将我们的理想选择粘合在一起并评估不同的技术选择。我们每个决定的主要标准是查看采用率和受欢迎程度,但最重要的是,我们想看看是否有大型工程组织投资的证据。一些选择看起来不错,适用于小型项目,但缺乏证据表明它们可以扩展到大型代码库和系统。其他一些会使编写惯用的 Kotlin 代码变得困难。我们不希望我们的代码看起来笨拙并且需要相互冲突的心理模型。我们将尝试呈现我们今天所处位置的一英里高的视图,我们希望在以后的帖子中更深入地研究它们。我们的代码存在于 monorepo 中,虽然 gradle 是一个更容易选择的开始,但我们不相信它会为具有复杂依赖图的 monorepo 扩展。幸运的是 bazel 已经在 Brex 使用,我们使用 rules_kotlin 来构建和测试 Kotlin 目标。我们使用 rules_jvm_external 进行外部依赖管理,以防止 JVM 上可怕的菱形依赖问题。在我们所有的选择中,bazel 是学习曲线最陡峭的一个,一些工程师最挣扎。但我们喜欢快速、密封和可扩展的构建系统,并认为这是最好的长期解决方案。

我们的后端由通过 gRPC 进行通信的微服务组成,protobuf 作为我们的交换格式。对于异步通信,我们使用构建在 Kafka 之上的内部发布-订阅系统。我们使用 Micronaut 来构建这些微服务、事件消费者、HTTP 服务器和 cron 工作负载。我们喜欢将 Micronaut 及其依赖注入框架视为构成我们服务的粘合剂。 Micronaut 的面向方面编程功能强大,但有些人发现它具有更陡峭的学习曲线。由于这不是一个常见的设置,我们必须添加支持以添加 micronaut 插件以与 bazel 一起使用。使用 micronaut 构建的服务具有低启动时间和低内存开销,我们发现这确实令人印象深刻。我们使用 grpc-kotlin 来编写 gRPC 服务和客户端。对协程的支持很好,但是,生成的 protobuf 绑定是基于 Java 的,我们无法获得 Kotlin 语言的全部好处。我们认为这是一个可以接受的折衷方案,直到有正式的 Kotlin protobuf API。大多数服务使用 PostgreSQL 作为持久层。我们发现 Exposed 框架是一个令人愉快的选择。公开的 DSL 展示了 Kotlin 在编写类型安全 SQL 操作方面的强大功能。该框架是新的并且正在迅速获得采用,但文档可发现性可能会更好。我们使用 flyway 运行我们的数据库迁移,并使用 testcontainers 来测试数据库交互。我们发现 JVM 可观察性生态系统是最成熟的。使用 Micronaut 添加这些集成是无缝的。我们使用 opentracing 进行分布式跟踪,使用 micrometer 收集指标。我们使用以 logback 为后端的 KotlinLogging。我们发现与使用 Datadog 和 Sentry 的现有可观察性堆栈的集成非常简单。我们选择了 kotest 和 mockk 来编写测试。与公开的框架非常相似,kotest DSL 真正展现了 Kotlin 语言的最佳特性,它提供了一种强大而简单的方法来编写富有表现力和美观的测试。我们正在使用许多通用库,但特别是我们对 Arrow 和 Guava 的使用感到非常满意。在我们的原型之后,我们对 Kotlin 的未来感到非常乐观,我们想继续推进它。我们希望在 Brex 上采用 Kotlin 的方式错开,我们征集了多个有兴趣成为早期采用者的团队。此外,在我们的内部 Hackathon 中,我们通过设立特别奖项来鼓励团队使用 Kotlin 进行构建。

最初的反馈是非常积极的,但有些部分需要改进并且会让人们感到沮丧。我们很快意识到任何疣都会在多个服务中放大和复制。我们领先于他们很重要,因为如果不加以解决,他们会产生巨大的影响。我们采取了以下多管齐下的方法,通过补充说:我们是书面文化的忠实信徒,这尤其重要,因为我们是一家远程优先的公司。书面文档是一种非常可扩展且有效的大型团体交流方式。我们编写了关于从设置本地环境、教育资源、设置服务器、客户端、数据库等等的所有内容的内部指南。这是作为一个 wiki 编写的,鼓励人们阅读它并保持更新。很多时候,我们会在 Brex 遇到以前从未见过的案例,我们利用这些机会为后代改进它。最后,非常重要的是要注意不正确和过时的技术文档是文档债务,文档债务是技术债务。很难正确设置新服务,尤其是对于新工程师而言。我们在后台添加了一个脚手架工作流——一个 Spotify 的开源开发者平台,我们用于开发者工作流。这使得新服务的加入变得更加顺畅,特别是因为大多数构建服务的人没有使用我们的新技术堆栈。我们仍然发现需要编写一些作为粘合剂的核心库,并在某些情况下复制缺失的功能。我们还有一类可以简化构建服务的库。例如,Micronaut 通常使用 YAML 文件进行配置,但由于 Brex 中的许多服务共享非常相似的配置,因此存在大量重复和关键配置。此外,YAML 不是类型安全的,而且很容易出错。我们构建了一个小层,以使用更小的配置集以编程方式生成属性源。最重要的是,对我们来说,对早期采用者的反馈非常敏感和接受是至关重要的。自从解决了早期采用者的主要问题后,每一个新的后端服务都是用 Kotlin 编写的。今天,刚接触 Kotlin 的工程师可以很快上手,我们能够以更高的信心更快地采取行动。我们的 Kotlin 代码库正在增长,我们还有很多工作要做。我们很高兴成为全球不断增长的使用 Kotlin 构建软件的工程师的一员。快来加入我们,为企业打造 Brex 的多合一金融服务。