Python非常适合快速编写更高层次的应用,但并不总能提供企业级所需的高性能。C创建高性能的可执行文件,但添加功能需要更多时间。这篇文章分享了将EinsteinAnalytics企业软件从C-Python混合迁移到完全Go应用程序的经验。我们很少有机会针对同一任务直接比较两种技术。但有时星星会如此巧合,以至于你从当前技术堆栈中获得的东西是负面的,而一项新技术恰好满足了你的确切需求,或者项目的规模和功能集超过了超出现有技术的能力。在Salesforce,过去几年我们遇到过这种情况。我们将大部分EinsteinAnalytics后端从Python-C混合平台移植到Go。Go是谷歌为大规模现代软件工程设计的语言。传说谷歌工程师想要创建一种专为大型应用程序设计的语言,并在等待大型C++项目编译时开始设计Go。这篇文章分享了我们将企业级软件从C-Python混合迁移到(几乎)完全Go应用程序的经验。EinsteinAnalytics将商业智能处理添加到Salesforce实例。通过基于云的AI处理,它直接从SalesforceCRM数据和尽可能多的外部客户数据(无论结构和格式如何)生成可操作的见解或预测、管道报告、绩效指标。在幕后,给定的Salesforce实例将EinsteinAnalytics功能公开为常规SalesforceRESTAPI的一部分。它们链接到一个查询服务器集群,每个查询服务器都针对缓存在内存中的链接数据集提供查询服务,但它们可以填充来自集群中任何节点的缓存数据。为了管理所有这些请求,我们在每个服务器上都有一个优化的流程,将请求路由到适当的节点并将响应转发给API请求的发起者。这些调用对于读取数据集的任何查询服务器都是本地的。本地意味着快速。较大的数据集被分区,无状态查询协调器聚合来自远程分区子查询的数据。数据集使用ETL(提取、转换、加载)批量创建,然后以专有的列式数据库格式存储。最初成为EinsteinAnalytics产品的查询引擎和数据集创建工具是用C语言编写的,使用Python包装器提供高级功能分析查询、RESTAPI服务器、表达式引擎等。从本质上讲,该产品提供了两全其美的优势。Python非常适合快速编写更高级别的应用程序,但并不总能提供企业级所需的高性能。C创建高性能的可执行文件,但添加功能需要更多时间。最初,这种组合奏效了。然而,经过多年的软件开发,EinsteinAnalytics开始遇到性能下降问题。这是因为许多不属于核心查询引擎的功能已添加到Python包装器中。这样可以快速开发和部署功能,但随着时间的推移,它们会拖累整个系统。Python的多线程性能不是很好,所以要求包装器执行的次数越多,它的性能就越差。之前的团队已经在考虑将包装器移植到Go,所以我们也做了一些研究。我们很快意识到,在企业级系统上,我们将面临另外两个问题。首先,Python使用松散类型,这对于小型团队快速开发新想法并将其投入生产非常有用,但对于一些客户为此支付数百万美元的企业级应用程序就不太好了。其次,我们预见到巨大的依赖噩梦即将来临,因为部署正确的Python库、版本和文件是一件苦差事。所以在2014年,我们决定将Python包装器移植到Go。我们最初对年轻的Go生态系统持谨慎态度,但当我们查看该语言的设计目标时(转到Google:软件工程服务中的语言设计),我们印象深刻。它专为软件工程而设计,而不仅仅是语言的复杂性,因此它的优势包括可靠的内置工具、快速的编译和部署以及轻松的故障排除。企业软件的真正问题是阅读代码的时间多于编写代码的时间。我们感谢Go使代码易于理解。在Python中,您可以编写超级优雅的列表理解和几乎具有数学美感的代码。但是,如果您没有参与编写代码,那么这种优雅可能是以牺牲可读性为代价的。第一个项目进展顺利。我们对新项目的性能和可维护性非常满意。我们遇到的少数抱怨之一是,在选择可伸缩性而不是原始性能以帮助他们进行垃圾收集时存在语言权衡:他们决定开始将原始类型存储为指针而不是接口中的值,这给我们带来了性能开销和额外分配。完全迁移到Go的体验非常好,以至于在2016年编写具有更好优化器的新查询引擎内核并改进我们的数据集创建工具时,我们决定在Go中运行。我们获得专业知识的速度与Go生态系统的成熟速度一样快,因此减少开销并使我们的代码可在单一语言中重用是有意义的。此外,我们希望消除CGO接口的开销。最大的外卡是性能。Go在其Goroutines中使用异步IO的轻量级“绿色线程”模型,这为我们提供了优于Python的多线程优势,但C代码的运行速度与它需要的一样快——它使用内置的安全性来换取速度,加上C编译器更成熟并且有更好的优化。我们的团队创建了一个概念验证(POC),其性能几乎与C引擎一样好,但前提是我们使用正确的编程模式:缓冲所有IO以减少Go系统调用的开销。在系统调用中,当前的Goroutines屈服于调用。如果可能出现紧密循环,请使用结构而不是接口来最小化接口方法的间接开销。在紧密循环中使用预分配的缓冲区(类似于io.Reader的工作方式)以最小化垃圾收集压力。批处理数据行是解决糟糕的编译器内联的一种解决方法,它可以使实际计算更接近数据并最大限度地减少每个函数调用的开销。我们在2017年完成了重写,新的Go版本EinsteinAnalytics于2018年上线。通过将所有内容保持在同一种语言中,我们可以重用代码并提高生产力。跨平台和可移植性的潜力使移植代码变得容易。如果我们需要在移动应用程序中使用任何此代码,我们可以将其交叉编译到iOS或Android,它就可以工作。早些时候,我说过这个版本(几乎)完全是用Go编写的。一个例外是我们的集群管理器,这可能看起来很奇怪,因为Kubernetes和其他类型的集群协调应用程序是Go的最常见用法,但负责该服务的团队使用Java感觉更舒服。让团队拥有他们的组件很重要;你不能强迫人们做他们不想做的事情。尽管Go有一些必须解决的局限性,但我们对结果非常满意。Go将继续改进。他们通过将编译器移至静态单赋值形式来解决编译器中的一些缺陷,这使得进行花哨的优化变得更加容易。垃圾收集变得越来越高效,编译器通常足够聪明,可以执行逃逸分析来检测何时可以在堆栈而不是堆上廉价地分配变量值。作为一名开发人员,如果你想用任何语言编写高性能代码,你都需要熟悉编译器的工作原理。这还不是语言的全部。Go有一个非常简单的参考文档——只有两页!但是了解编译器需要收集所有这些点点滴滴,其中详细说明了您正在使用的特定版本的Go中可用的所有优化。在这些端口之后,我们的团队在Go及其编译器技术方面积累了一些专业知识。但仍然存在一些问题。例如,您可以轻松地将数据写入成本较低的堆栈,而不是成本较高的堆。您甚至不会仅通过阅读代码就知道发生了这种情况。因此,与任何需要高性能的新语言一样,您需要密切监视进程并创建有关CPU和内存使用情况的基线。然后与社区分享你学到的东西,让它变得不那么本地化。结论选择一种较新的语言并将其引入企业公司可能是一场赌博。幸运的是,Go生态系统与我们一起成长。Google继续支持该语言的开发,并已被许多其他大公司采用。我们现在有一个工程师团队全职致力于Go,并且我们继续取得一些积极的成果。我们期待与Go社区一起成长,并分享更多我们从经验中学到的东西。Salesforce认为,支持像Go这样的开源技术可以推动我们的行业向前发展,开启新的职业生涯,并建立对我们创造的产品的信任。
