Stream从Python切换到Go的原因
作者Thierry Schellenbach 译者 安翔
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配置创建部分功能。例如,我们希望“simplegauss”以五天的时间窗、一天的偏移量以及0.3的衰减因子来调用“decaygauss”。
· 解析“默认”配置,以便在活动数据中发现未定义的字段时进行回退。
· 使用步骤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来编写。