写点什么

DoorDash 的后端服务如何从 Python 迁移到 Kotlin?

  • 2021-07-16
  • 本文字数:6244 字

    阅读完需:约 20 分钟

DoorDash 的后端服务如何从Python迁移到Kotlin?

美国外卖平台 DoorDash 原先的代码库是基于 Django 的单体应用。之前这个平台对业务的支持能力已逼近天花板。为给送餐服务提供更坚实的基础,DoorDash 需要全新设计的技术栈。新平台应能很好地支撑企业的未来增长,并支持团队在构建中持续推陈出新,用上更好的模式。


原系统的每次发布都需更新大量的节点,这显著增加了所需的发布时间。并且每次部署中都有大量的提交,一旦部署存在问题,难以通过对分定位(Bisecting)发现具体导致问题的某次或某些提交,问题定位耗时也更长。此外,原单体应用是基于Python 2Django构建的,而旧版本 Python 正迅速进入寿命终止期(EOL,End of Life),难以继续获得可靠的支持。


为实现具有更好可扩展性的系统,DoorDash 工程团队需要去分解单体应用,确定新服务的界面和交互行为。接下来的首要问题是如何确定支持团队工作的技术栈。通过对多种语言的调研,团队选定了具有丰富的生态系统、与 Java 良好互操作性和对开发人员友好的Kotlin。针对 Kotlin 逐渐暴露出来的痛点问题,团队做出了一些改进。

确定适用的技术栈


当前存在多种可用的服务器端软件构建方案。但是出于以下方面考虑因素,团队考虑只使用单一语言。


  • 有助于团队聚力,推动最佳开发实践在整个工程组织内的共享。

  • 支持针对企业场景构建优化的通用软件库,很好地适应企业规模和持续增长。

  • 极大降低工程人员在团队间转岗的摩擦,推动相互合作。


综合上述因素,对于团队而言,问题并非是否应该使用单一的开发语言,而是应该选定哪一种语言。

选择适合的编程语言


选择编程语言时,团队要从企业的需求着手,考虑因素包括未来服务的体验以及交互方式等。团队很快取得一致,决定对服务间的相互同步通信机制使用gRPC、消息队列使用Apache Kafka。数据存储将继续使用PostgresApache Cassandra,因为团队成员对此经验丰富、技能熟练,并且二者的技术成熟度也非常高,广泛支持所有的现代编程语言。下面还需要考虑其它一些因素。


无论选定何种技术,都需要满足:


  • 高效利用 CPU,可扩展到多核。

  • 易于监控

  • 具有强大的软件库生态支持,使团队可聚焦于业务问题本身。

  • 确保提供良好的开发人员生产率

  • 可靠的扩展性

  • 面向未来,为企业业务增长提供良好支撑。


团队基于上述需求考虑了各种语言,弃用了C++RubyPHPScala等主流语言。尽管这些语言颇受欢迎,但它们难以支撑每秒查询数(QPS)和用户数的增长,不能满足团队对未来技术栈的一项或数项核心需求。基于上述需求,选择范围锁定在KotlinJavaGoRustPython 3。为比较和对比各语言相互之间的优劣之处,团队形成了如下对比表。

Kotlin

优点:


  • 软件库生态系统强大

  • 对 gRPC、HTTP、Kafka、Cassandr 和 SQL 提供一等支持

  • 继承 Java 的生态系统,速度快,可扩展。

  • 原生支持并发原语,避免了 Java 的繁琐,去除了对复杂的Builder/Factory模式的依赖。

  • Java Agent 提供强大的组件内省(introspection),仅需少量编码,即可自动定义并导出度量和追踪,以实现监控。


不足:


  • 通常很少用于服务器端,开发人员缺少可供参考的示例代码。

  • 相比 Go 而言,并发实现相对繁琐。Go 在语言基础层和标准软件库中集成了 goroutine(译者注:原文是“gothreads”)这一核心理念。

Java

优点:


  • 具有强大的软件库生态系统

  • 对 gRPC、HTTP、Kafka、Cassandr 和 SQL 提供一等支持

  • 速度快、可扩展

  • Java Agent 提供强大的组件内省(introspection),仅需少量编码,即可自动定义并导出度量和追踪,以实现监控。


不足:


  • 相对 Kotlin 和 Go 而言,并发实现相对繁琐。

  • 编码存在极端冗长问题,难以编写整洁的代码。

Go

优点:


  • 具有强大的软件库生态系统

  • 对 gRPC、HTTP、Kafka、Cassandr 和 SQL 提供一等支持

  • 速度快、可扩展

  • 原生支持并发原语,简化了并发代码的编写。

  • 具有大量可用的服务器端例子程序和文档。


不足:


Rust

优点:


  • 运行速度非常快

  • 没有垃圾回收机制,依然内存和并发安全。

  • 一些大型企业开始采用该语言,因此具有大量投资及很好的发展。

  • 相比其它语言,其提供强大的类型系统,更易于表达复杂理念和模式。


不足:


  • 语言较新,这意味着例子代码、软件库以及具有模式构建和调试经验的开发人员相对不足。

  • 相比其它语言,生态系统不算强大。

  • 当前 async/await 尚未实现标准化。

  • 要掌握内存模型,需要一定的学习时间。

Python 3

优点:


  • 提供强大的软件库生态系统

  • 易用

  • 团队已经具有丰富的经验

  • 易于招聘开发人员

  • 对 GRPC、HTTP、Cassandra 和 SQL 提供一等支持

  • 提供 REPL,便于 App 运行时的测试和调试。


不足:


  • 相比其它语言,运行速度较慢。

  • 解释器全局锁(GIL)难以完全高效利用团队的多核机器。

  • 缺少强大的类型检查特性。

  • 当前对 Kafka 支持不够,特性发布存在延迟。


根据以上对比,团队决定开发一个经过测试和扩展的 Kotlin 组件的“黄金标准”。kotlin 本质上是一种更适合团队的 Java 版本,但缓解了 Java 存在的痛点问题。由此团队选择了 Kotlin,但必须要去解决进一步发展中经历的一些问题。

相对 Java,Kotlin 的优点


相对 Java,Kotlin 的一个最大优点是“空值安全”(null safety)。Kotlin 中,开发人员必须明确定义可为空值的对象,并强制开发人员采用安全方式处理,避免了必须处理大量潜在的运行时异常的痛点。也可使用空值合并(null-coalescing)操作符“?.”,用单行代码实现对可为空值子域的安全访问。例如,Java 实现为:


int subLength = 0;if (obj != null) {  if (obj.subObj != null) {    subLenth = obj.subObj.length();  } }
复制代码


使用 Kotlin 实现为:


val subLength = obj?.subObj?.length() ?: 0
复制代码


尽管上面给出的例子非常简单,但已经足够体现出空值合并操作符的强大之处,即大大降低了代码中条件语句的数量,提高了代码的可读性。


相比其它语言,在实现服务度量的仪表盘监控中,使用 Kotlin 更易于迁移到Prometheus事件监测系统。团队开发了一个注解处理器(Annotation Processor),自动按度量生成相应的函数,确保正确数量的标注按正确的顺序给出。


下面例子给出了 Prometheus 软件库的标准集成代码:


// to declareval SuccessfulRequests = Counter.build(     "successful_requests",    "successful proxying of requests",).labelNames("handler", "method", "regex", "downstream").register()
// to useSuccessfulRequests.label("handlerName", "GET", ".*", "www.google.com").inc()
复制代码


修改为如下代码,API 更不易出错:


// to declare@PromMetric(  PromMetricType.Counter,   "successful_requests",   "successful proxying of requests",   ["handler", "method", "regex", "downstream"])object SuccessfulRequests
// to useSuccessfulRequests("handlerName", "GET", ".*", "www.google.com").inc()
复制代码


采用如上的集成方式,不必再去记住某个度量所具有标注的数量和顺序,而是由编译器和所使用的 IDE 去确保标注的正确数量和名称。DoorDash 使用了分布式追踪,监控的集成可简化为类似于在运行时添加Java Agent。。对于一个新服务,不需要代码属主团队去修改代码,可观测性和架构团队就能快速地推出对应的分布式追踪。


在团队看来,Kotlin 的另一个非常强大之处是协程(Coroutines)。协程模式让开发人员无需纠结于回调这个天坑,能使用近乎命令式编程的方式去编写代码,这正是大部分开发人员更为驾轻就熟的方式。协程也非常易于组合,必要时可并行运行。下面例子给出了团队使用的某个 Kafka 消费者:


val awaiting = msgs.partitions().map { topicPartition ->   async {       val records = msgs.records(topicPartition)       val processor = processors[topicPartition.topic()]       if (processor == null) {           logger.withValues(               Pair("topic", topicPartition.topic()),           ).error("No processor configured for topic for which we have received messages")       } else {           try {               processRecords(records, processor)           } catch (e: Exception) {               logger.withValues(                   Pair("topic", topicPartition.topic()),                   Pair("partition", topicPartition.partition()),               ).error("Failed to process and commit a batch of records")           }       }   }}awaiting.awaitAll()
复制代码


Kotlin 协程支持在编码中按分区快速地切分消息,并对每个分区启动一个处理消息的协程,不破坏消息插入队列时的顺序。此后,在检查偏移并返回 Broker 前,连接所有的 Future。


Kotlin 支持团队以更可靠和可扩展的方式快速推进。从上面的例子中可见一斑。

解决推广 Kotlin 中遇到的问题


为更好地利用 Kotlin 的全部特性,团队必须要解决以下问题:


  • 如何培训团队更高效地使用 Kotlin

  • 建立使用协程的最佳实践

  • 解决与 Java 互操作上的痛点

  • 进一步简化依赖管理


下面展开介绍团队时如何解决上述问题的

培训团队使用 Kotlin


采用 Kotlin 的一个最大问题,就是如何确保提升团队的开发速度。团队中大多数人具备优秀的 Python 开发背景,后端团队具有一些 Java 和 Ruby 经验。考虑到在后端开发中很少使用 Kotlin,因此团队必须要建立指导后端开发人员使用 Kotlin 的良好指南。


尽管在线上可以找到大量的学习教程,但是大多数Kotlin线上社区主要专注于安卓开发。团队的高级开发人员编写了“如何使用 Kotlin 编程”,其中给出了编程建议和代码片段。我们团队发布了“碎片化学习教程”(Lunch and Learns session),告诉开发人员如何避免一些常见的坑,如何有效地使用 IntelliJ IDE 开展工作。


团队更多地传授开发人员 Kotlin 的函数式编程方面内容,包括如何使用模式匹配、不可变性默认优先等理念。团队建立了人人可加入、提问并获得建议的线上交流小组(Slack Channel),形成了一个 Kotlin 工程互助指导社区。通过以上工作,团队构建一个强大的、熟练掌握 Kotlin 的工程人员团队,并在团队进一步扩展时能传承知识,形成可持续发展和改进的内循环。

避免掉进协程中的坑


团队在选择 Kotlin 时,尚缺少对协程的支持(译者注:2018年10月,Kotlin 1.3推出了coroutines稳定特性)。因此团队选定 gRPC 作为服务间通信方法,为充分利用 Kotlin 需做一定改进。当时 gRPC-Java 是 Kotlin gRPC 服务的唯一选择,因为 Java 中并不存在协程,因此 gRPC-Java 也缺少对协程的支持。


两个开源项目Kroto-plusProtokruft可以解决这个问题,团队最终分别使用了这两个解决方案的各一部分去设计服务,创建更具原生感的解决方案。最近gRPC-Kotlin发布了一般可用(GA)版。为提供更好的 Kotlin 构建系统体验,团队我们正在迁移服务以使用官方绑定版本。


对于已转向 Kotlin 的安卓开发人员,对协程中存在的其它坑应该并不陌生。例如,不要在请求中重用CoroutineContexts,因为一旦取消或出现异常,CoroutineContext 就会转入“cancelled”状态,这意味着任何进一步尝试在此 Context 中加载协程将会产生失败。


正因为此,需对服务器处理的每个请求新建一个 CoroutineContext,不能再依赖于ThreadLocal变量,因为协程可在 Context 中换入换出,导致数据不正确或被覆盖。另一个需要警惕的坑是避免使用未绑定的GlobalScope加载协程,会导致资源上的问题。

解决虚引用 Java NIO 问题


支持现代Java非阻塞IO(NIO)标准的软件库,可以很好地与 Kotlin 协程互操作。但在选定 Kotlin 后,我们发现很多宣称支持 Java NIO 的软件库的实现方式并非可扩展的。它们在底层协议和标准实现中并非基于 NIO 原语,而是使用线程池包裹阻塞 IO。我们称这种 NIO 实现策略为“虚引用(Phantom)Java NIO”。


虚引用 NIO 策略实现的副作用是线程池在协程环境中很容易耗尽,由于其本质上是阻塞 IO,会导致高峰值延迟。为确保线程池的规模能满足团队的需求,虚引用 NIO 软件库都需要对线程池做调优,恰当调优和资源维护的需求增加了开发人员工作量。因此,使用真正的 NIO 或 Kotlin 原生库,通常会提供更好的性能、更易于扩展,实现更优的开发人员工作流。

依赖项管理:使用Gradle颇具挑战


相比 Rust Cargo 和 Go module 等最新解决方案,构建系统和依赖管理无论对新手还是熟悉 Java/JVM 生态者都相当不够直观。尤其是对于团队而言,一些依赖直接或间接地对版本升级非常敏感。Kafka 和 Scala 等项目并不遵循语义化版本管理(semantic versioning),这会导致编译成功而应用却由于一些看上去毫不相干的奇怪回溯(backtrace)而启动失败的问题。


寸积铢累,团队逐渐掌握了哪些项目通常会导致此类问题,积累了一些如何捕获并过滤问题的例子。特别是,Gradle 针对如何查看依赖树提供了一些有参考的页面,非常适用于此类问题。掌握多项目代码库的进入导出情况,需假以时日,期间非常容易导致冲突需求和环形依赖。


预先规划多项目代码库的布局,对项目的长期发展是大有裨益的。尽量确保依赖树简单,避免基础代码库对任一子项目的依赖(并且永不依赖),进而在此基础上做迭代构建,防止出现难以调试或厘清的依赖链。DoorDash 主要使用了JFrog Artifactory,简化了软件库在代码库间的共享。

Kotlin 在 DoorDash 的未来发展


DoorDash 服务标准将继续完全采用 Kotlin,平台团队正基于GuiceArmeria努力构建下一代服务标准,并通过预先部署监控、分布式追踪、异常追踪、运行时配置管理工具和安全集成等工具和功能,简化团队的开发工作流。


这些工作有助于 DooDash 开发共享性更好的代码,减轻开发人员查找依赖项、协同工作和维持最新依赖的负担。构建此类系统的投资,已体现在团队具备了针对涌现的需求而快速启动新服务的能力。Kotlin 支持开发人员聚焦于业务用例,减少了编写 Java 生态中不可避免的模板代码所用的时间。总而言之,Kotlin 是很好的选择,我们期待这一语言和生态的持续改进。


基于 DoorDash 的经验,强烈推荐后端工程人员首选 Kotlin。Kotlin 是更好的 Java 语言,该理念在 DoorDash 得到了验证,带来了更大的开发人员生产率,降低了运行时发现的错误。这些优点支持团队聚焦于解决业务需求,增加敏捷性和速度。未来 DoorDash 将继续投资于 Kotlin,希望继续与更广泛的生态合作,开发以 Kotlin 为主的更强大服务器端用例。

答疑


问题:为什么没有选定 Python 3?


答:尽管 Python 3 具有更强大的生态,但整个生态系统依然不够健壮。Pip 在依赖管理上存在很多问题,而 conda、poetry、pipend、pip-tools 等工具也并未解决问题。对于构建和软件包工具存在同样问题。


使用协程时遇到的最大坑:取消或异常会导致 CoroutineContext 进入“cancelled”状态,这意味着进一步尝试在此上下文中加载协程将会失败,对于服务器处理的每个请求,需要创建一个新的 CoroutineContext。更坏情况时,新的上下文每次创建的代价很大。需要建立一类发生异常后无需取消的特殊任务类型,以及建立很好的协程异常处理。


团队使用 Kotlin 在 Apache Flink 中实现流处理。为解决虚引用 NIO 问题,团队拟出了一个符合“黄金准则”的软件库列表。其中软件库或是很好地实现了协程,或是提供预优化版本的库。


问题:非阻塞 IO 是如何实现的?DoorDash 最终使用了第三方软件库,还是推出了自己的?DoorDash 的主要 IO 是网络调用、文件系统还是消息代理?


答:DoorDash 构建了自己的软件库,针对特定服务使用 gRPC。


问题:DoorDash 在从 Python 迁移到 Kotlin 中,是如何解决 CI/CD 问题的?


答:团队采用 CI/CD 的层级和版本一直在演进,至少在今年就发生了不少变化,每次迭代都会向前推进一步。


原文链接:


https://doordash.engineering/2021/05/04/migrating-from-python-to-kotlin-for-our-backend-services/

2021-07-16 15:215304
用户头像

发布了 391 篇内容, 共 134.3 次阅读, 收获喜欢 256 次。

关注

评论

发布
暂无评论
发现更多内容

6 大原则!助你构建高绩效的研发强军 | Liga译文

LigaAI

经验分享 研发团队 研发效能管理 高绩效团队 企业号 2024年5月 PK 榜

通过考证深入了解TiDB

TiDB 社区干货传送门

社区活动 数据库架构选型 学习&认证&课程

Kubernetes 审计日志采集与分析最佳实践

观测云

#k8s

“7”势如虹|突破集成边界,谷云科技成为iPaaS领头羊!

RestCloud

周年

mysql经典 1 异步复制after sync和after commit

趁早

AI日报|谷歌发布Astra反击GPT-4o,字节发布9个自研大模型,腾讯混元开源文生图大模型...

可信AI进展

冲刺PCTA

TiDB 社区干货传送门

学习&认证&课程

记TiDB学习之路

TiDB 社区干货传送门

社区活动

TiDB x KubeBlocks 集成案例

TiDB 社区干货传送门

管理与运维

多点数千套集群实践:从“MySQL 又不是不能用,为什么选择 TiDB?”到“能用 TiDB 就不用 MySQL”

TiDB 社区干货传送门

实践案例 社区活动 管理与运维 TUG 话题探讨 数据库前沿趋势

入围全球机器人顶会ICRA 2024!毫末在RoboDrive2024挑战赛崭露头角

Geek_2d6073

GPT搜索引擎原型曝光!新模型GPT4-Lite驱动,虽然鸽了发布会但代码已上传

Openlab_cosmoplat

交友网站系统平台搭建 语音社交app源码,语音聊天交友 社交app 婚恋交友专业开发

DUOKE七七

小程序源码 交友小程序

TiDB Cloud x Datadog 集成案例

TiDB 社区干货传送门

应用适配

使用 TiDB Vector 搭建 RAG 应用 - TiDB 文档问答小助手

TiDB 社区干货传送门

版本测评 新版本/特性解读 数据库前沿趋势

analyze 采样率是怎么算出来的(v6.5.3)

TiDB 社区干货传送门

TiDB 源码解读 6.x 实践

AI Agent深度解析:潜力与挑战并存的智能新世界

可信AI进展

人工智能「

如何跨越 LangChain 应用研发的最后一公里

Jade@pluto-lang

IaC AWS CDK Cloud Native LLM langchain

瞬间优化 ChatGPT 指令的 2 种方法

蓉蓉

GPT #人工智能 ChatGPT4

跨端自渲染绘制的实践与探索

京东零售技术

前端 React 企业号 5 月 PK 榜 跨端渲染 多端兼容

【TiDB 社区升级互助材料】TiDB 版本升级最全材料包

TiDB 社区干货传送门

版本升级

多点 x TiDB:在出海多云多活架构中,多点运维 TiDB 的实战分享

TiDB 社区干货传送门

实践案例 社区活动

北京测试开发高薪线下周末班,手把手带你提升职业技能

测试人

软件测试

AI晚报|GPT-4o Omni金牌团队首揭秘!苹果将推出全新辅助功能,小米大模型MiLM正式通过备案...

可信AI进展

#人工智能

DBA福利来了!《YashanDB数据库概念手册》正式发布

Geek_2d6073

DoorDash 的后端服务如何从Python迁移到Kotlin?_架构_Matt Anger_InfoQ精选文章