Stream 最近将其后端核心服务从 Python 改成了 Go。虽然他们的某些模块仍然在使用 Python,但是公司已决定从现在开始使用 Go 来编写对性能要求较高的代码。文中,Stream 的 CEO 兼创始人 Thierry Schellenbach 将解释他们决定转向 Go 的原因。
影响项目或者产品编程语言选型的因素有很多。与任何技术决策一样,选择编程语言时同样需要多方面权衡,即使这样,最终的选择结果都很难是完美的。我们最近将后端的核心服务从 Python 改成了 Go,原因有很多,好处也很多。
为了理解这一变化的重要性,需要先了解我们的产品。Stream 是一套用于构建、伸缩、定制化新闻源和活动流的 API。每个月为 3 亿多用户提供约 10 亿次 API 请求。我们尤其关注性能和可靠性,这两点因素决定了我们制定的每项技术决策。
性能更优
Go 最大的卖点在于它的性能,无论在运行还是编译时它都有突出的性能优势。它与 Java 或者 C++ 的运算速度几乎相当。在实际使用中,我们发现它比 Python 大约快 30 倍。
选择快速工具对提升系统性能非常重要,因此我们对 Cassandra、PostgreSQL、Redis 以及其他一些技术进行了优化。然而,很多时候我们发现系统仍然存在瓶颈,而瓶颈正好在于我们的编程语言 Python。Python 在执行序列化、排序和聚合等计算密集型任务时需要花费很长的时间,有时比从网络上存取和检索数据花费的时间更长。我们知道这个时间是可以优化的。从 Python 切换到 Go 就可以缩短时间,这样一来,应用程序代码就更像是服务之间的粘合剂,而不再是优化中的主要瓶颈。
用 Go 编写的 Go 编译器也非常快。Stream 中最复杂的微服务就采用 Go 编写,它的编译时间仅仅需要 6 秒,Java 和 C++ 等工具链则慢得多,快则一分钟,慢则数小时。
名副其实的简单
简单是 Go 的重要特征!我敢向你保证,阅读 Go 语言的代码明显感觉更加简单。我们已经从多个 Python 代码库中迁移出来,我们发现这些 Python 代码的风格和框架会因为作者的不同而风格各异,往往带有很多作者个性化的东西。而 Go 恰恰相反,它推崇干净的代码风格,同时要求作者编写代码时严格遵守规范,禁止作者“自作聪明”。虽然这样有时候会使用更加冗长的代码,牺牲了代码的简洁性,但是却让代码更容易阅读和理解了。这样一来,Go 才得以加快开发人员阅读他人代码的速度,同时,阅读自己曾经编写的代码也更容易。
原生并发性
Go 在语言层面通过 goroutine 和 channel 支持了并发。此概念源自 Tony Hoare 的 CSP 模式,它让程序员处理并发变得不再困难。
goroutine 类似于操作系统的线程,但其运行消耗的系统资源更小,每个 goroutine 仅需几 KB 的堆栈空间。Go 运行时可以在操作系统线程之上处理多路 goroutine。虽然在后台执行,但它对于程序员来说是可见的。单个程序拥有数千个 goroutine 也并不罕见。比如,net/http 软件包中的服务器程序针对每个 HTTP 请求都会创建一个 goroutine。
在 Go 中启动 goroutine 非常简单,只需通过 go 关键字添加一个函数调用,即可启动一个 goroutine,并让该函数运行在自己的 goroutine 中。
Go 有一句重要的格言,即:不要通过共享内存来通信,相反,通过通信来共享内存 。Goroutine 之间通过 channel 进行通信,channel 的使用方法与 goroutine 一样简单。Channel 拥有类型,可以通过直观的箭头语法轻松实现 goroutine 之间的数据传递。尽管 channel 使用简单,但是其功能非常强大。在设计时只要预先稍作考虑,与传统的系统相比,使用 Go 便能够轻而易举地开发大规模并发系统。
使用简单的并发工具可以解决那些经常导致错误的问题。Go 内置了竞态条件检测器,可以更轻松地检测异步代码中的竞争状态。
语言生态
跟 C++ 和 Java 这样已经高度普及的传统语言相比,Go 仍然是编译语言领域的新手。虽然目前大约只有 5%的程序员知道 Go,但是得益于它的易用性,这个数字在不断增长。虽然 Go 语言速度快且功能强,但它只有 25 个保留字。相比于 C++ 的 92 个保留字,以及 Java 的 53 个保留字,Go 显得非常简洁。过多的保留字会增加程序员的学习成本。
由于 Go 上手非常容易,因此组建 Go 开发团队相比其他语言来说更容易。Go 初学者可以很快入门并精通该语言。这使得雇主甚至可以招聘其他背景的开发人员,然后加以短期培训即可使其成为合格的 Go 工程师。
Go 提供的内置库开箱即用且功能强大。使用“net/http”仅需几行代码即可实现 HTTP 服务器,并且还支持 http/2、TLS 和 websocket。Go 社区软件包的生态系统也很出色,已经出现了很多与 Redis、RabbitMQ、PostgreSQL、模板以及 RocksDB 相关的库,它们运行稳定且更新频繁。
其他优势
在前文中我提到了 Go 并不鼓励程序员“自作聪明”,它并没有提供可能会节省时间的功能,比如可嵌套的三元运算符。
Go 采用另一种方式来节省时间,它既没有选择制表符也没有选择空格,而是转而使用了 gofmt。它是一种命令行工具,可与大多数编辑器集成并自动将代码格式化为特定的格式。即使格式不正确代码仍会编译,但是拉取请求会被忽略,除非代码通过 gofmt 并且能够保持整个代码库格式一致。这使得代码评审人员能够专注在代码上,而不必在格式上浪费时间。
Go 有助于开发微服务。谷歌的 protobuf 和 gRPC 是微服务间通信的基础,Go 对它们提供了很好的支持。作为开发人员,我们只需在清单文件中定义一项服务,工具便会自动生成客户端和服务器端代码,并且保证代码的高性能以及很低的网络负载。此外,清单文件还可以被其他语言用来生成他们自己的客户端和服务器端代码。所以,如果我们决定用其他技术来替代部分架构,之后的任务会更加简单。
Python vs. Go
Stream 服务强大功能之一是 feed 排名。feed 排名允许我们的用户为 feed 指定一个评分函数,以便控制排序方式。评分算法可以提供很多变量来确定排名,其中基于流行度的一个例子可能是这样的:
{ "functions":{ "simple_gauss":{ "base":"decay_gauss", "scale":"5d", "offset":"1d", "decay":"0.3" }, "popularity_gauss":{ "base":"decay_gauss", "scale":"100", "offset":"5", "decay":"0.5" } }, "defaults": { "popularity": 1 }, "score":"simple_gauss(time)*popularity" }
- 为了支持这种排名方法,Python 和 Go 代码都需要解析表达式计算得分。在这种情况下,我们需要将字符串 “ simple_gauss(time)* popular ” 变成一个函数,它将活动数据作为输入,并输出分数。
- 基于 JSON 配置创建部分功能。例如,我们希望“simple_gauss”以五天的时间窗、一天的偏移量以及 0.3 的衰减因子来调用“decay_gauss”。
- 解析“默认”配置,以便在活动数据中发现未定义的字段时进行回退。
- 使用步骤 1 中的功能对 feed 中的所有活动数据进行评分。
开发 Python 版本的排名代码需要花费大约三天时间,包括编写代码、单元测试和编写文档。接下来,团队需要大约两周的时间来优化代码。其中一项优化是将分数表达式 (simple_gauss(time)* popular )转换为抽象语法树。该团队还实施了高速缓存逻辑,预先计算了将来某些时间的分数。
相比之下,开发这些代码的 Go 版本大约花费了四天时间,并且不需要再对其性能实施进一步的优化。虽然 Python 用来开发初期版本更快,但是整体来说使用 Go 开发的工作量要小得多。
Go 的语言特性使得在优化代码时能够节省大量的时间。使用 Python 时,我们不得不将表达式解析为抽象语法树,并优化和剖析每一个函数。由于 Go 比 Python 快得多,因此我们不需要花太多精力优化代码。最终的结果是,Go 代码的执行速度比精心优化的 Python 代码大约快 40 倍。
用 Go 来构建 Stream 系统中的某些组件相比用 Python 花费了更多的时间。总体来说,开发 Go 代码要花费更多的精力,但团队用来优化代码性能的时间则更少。
结论
Go 非常适用于开发微服务。它的速度非常快,具有原生并发原语,完美支持多种现有工具,并且开发起来乐趣无穷。与 Ruby 或 Python 等脚本语言相比,编写 Go 代码可能需要更长的时间,但其维护成本要低得多,加之其代码无需太多优化,因此你可以节省大量的时间。
需要注意的是,对于某些适合使用 Python 开发的模块,Stream 仍然使用 Python。例如,我们的仪表板、网站以及用于个性化订阅的机器学习都使用 Python 实现,因为 Python 提供的这些工具更好用。我们不会马上完全弃用 Python,但是对于性能要求较高的代码,我们今后会使用 Go 来编写。
原文链接: https://jaxenter.com/stream-switch-go-142279.html
感谢无明对本文的审校。
评论