24

AppsFlyer 将 API 网关服务从 Clojure 迁移到 Golang

 5 years ago
source link: https://www.infoq.cn/article/8OVxwDLKasaSb4_kKjCh?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文要点

  • AppsFlyer 每天处理超过 700 亿个 HTTP 请求,并且是使用微服务架构风格构建。系统的入口点是一个被称为 API 网关的关键任务(非微型)服务,它封装了所有前端服务。

  • 原先的 API 网关使用了 AppsFlyer 的默认语言 Clojure,其技术债务开始增加。

  • 为获得有关新设计的 API 网关服务的建议,他们选择使用 Golang 作为对 Clojure 进行基准测试的语言。

  • 作为一个选项,基准测试是在 NGINX (经过 Lua 增强)上进行的,同时使用了 Golang 和 Clojure。Go 比 Clojure 提供了更好的吞吐量,因此被选为首选的实现语言。

  • API 网关现在是用类型化语言构建的,借助 Golang 的库支持和社区的力量,我们可以更轻松地添加各种功能及引入新技术。

  • 新部署的解决方案能够支持的流量比现在多得多——随着流量和请求以 10 倍的规模增长,从前瞻性的角度来看,这是非常重要的。

AppsFlyer 是一个领先的移动归因和市场分析平台,每天处理近 700 亿次 HTTP 请求(大约每分钟处理 5000 万次请求),使用微服务架构风格构建。系统的入口点是一个被称为 API 网关的关键任务(非微型)服务,它封装了所有前端服务。本质上,就是将客户流量通过单点路由到我们的后端服务,这极大地简化了客户端的身份验证和授权,但同时也可能导致单点故障。

a6ZVjem.jpg!web

本文探讨了工程团队为什么以及如何从基于 Clojure 的 API 网关实现迁移到基于 Go 的实现。

API 网关的技术债务日益增加

我们之前已经讨论过 技术债务 是如何产生的,并且这种情况经常发生,就像我们的 API 网关服务那样。

最初,AppsFlyer 的服务是一个 Python 单体,它需要一个身份验证和授权解决方案作为这个单体的一部分。随着时间的推移,流量和复杂性不断增加,我们迁移到了微服务架构。因此,我们需要创建一个统一的 API 网关解决方案,它将作为我们的身份验证和授权提供者。

我们稍微做了下准备,就开始用 Clojure 编写,跳过了设计阶段,以概念验证模式构建服务。我们公司是 EMEA 最大的 Clojure 工场之一,因此,在很多情况下,Clojure 都是默认选择的语言,无需考虑手头的具体项目。虽然这有利于提高速度,符合“把事情做完”的心态,但对于项目的长期维护来说就不那么理想了。随着流量的增长,我们很快意识到——新推出的 API 网关的代码太复杂了,需要不断地重构以支持所需的吞吐量。

我们最终到了需要做出抉择的时候,服务太不稳定了,我们意识到,我们需要完全重写这个项目——要么用 Clojure(但是要有更好的设计),要么探索其他语言选项。通过这次迭代,我们决定抛弃我们的认知偏见,不再回到我们的 Clojure 舒适区,而是做恰当的设计工作来构建我们需要的服务,而不仅仅是重新构建我们已经拥有的服务。

我们最终选择了 Golang 作为这个 Clojure API 网关服务的比较基准,这也额外带来了语言多样性的好处,而且,掌握额外的语法有益于我们的代码技艺心态。

我们理解了向堆栈中添加另一种编程语言的另一面。我们是 CI/CD 思想的忠实信徒,我们引入了一种新的语言,它不是基于 JVM(与 Clojure 完全不同)的,这有其运维成本,但是我们能够在短时间内解决这个问题。

当然,掌握一门新语言也有学习曲线,而且需要确保代码长期来看足够优秀健壮,在使用特定的语言实际编写第一个项目,并看一下它在生产中的表现之前,这很难知道。

我将简要说明下为什么我们选择 Go 来提供这个特定的服务(仅供参考)。作为一种过程式异步语言,Go 为我们提供了函数式编程(我们已经在内部使用)、面向对象功能和更好的扩展。它是一种类型化语言,使得维护变得更加容易,而且不需要重新发明轮子。尤其是对于我们打算重写的服务,它有一个得天独厚的优势,就是内置了一个经过实践证明的 反向代理 。作为同步语言,Clojure 非常适合多线程和并发,而事实证明,这个特定的服务有很大的 I/O 开销。

评估我们的选项

我们知道,为了能够恰当地评估不同语言的适应性,我们需要进行几个方面的检查——每种语言的性能以及对于手头的具体任务所带来的特定好处。要测量性能,我们知道,我们需要在尽可能接近生产的环境中对 Clojure 和 Go 进行适当的基准测试。

为此,我们开始做压力测试,我们把 NGINX(经过 Lua 增强)作为一个选项,同时使用了 Golang 和 Clojure。Go 比 Clojure 提供了更高的吞吐量。

以下是有关测试的基本统计数据:

  • 我们使用 WRK 作为我们的基准测试工具

  • 3 分钟的突发流量

  • 64 线程

  • 1000 个连接池

  • 2 分钟请求超时

  • 每个请求返回一个 500kb 的静态文件

  • 为了降低使用 c4 xlarge 实例的网络噪声,所有的流量都从同一个 AZ 发起

代理方案 每秒请求数 每秒事务量 总请求数 总事务量 错误请求 平均延迟 直连 190 72 MB 34500 12.8 GB ~ 400 (drop:200) 4.41 Sec NGINX 185 73 MB 33486 12.7 GB ~ 300 (drop:37) 7.95 Sec Clojure(基本 Http-Kit 实现) 190 72 MB 34412 12.8 GB ~ 100 (drop:600) 8.48 Sec Golang(原生反向代理 &http 层) 185 73 MB 33443 12.7 GB ~ 200 (drop: 0) 5.42 Sec

除了上述这些结果之外,从语法的角度来看,Go 也提供了额外的好处,作为一种类型化语言,它容易更新和迭代,而且更容易通过现有的包和库进行扩展(许多东西不用从头开始写),从功能的角度来看,其内核内置的反向代理组件是一个重要的好处。

我们放弃使用 Clojure 重写服务,避免走捷径去复制过时的代码,践行类型化思维方式。

在设计阶段,首先列出服务需要提供的功能,在确定好基本概念之后,我们研究了在将我们的生产用户迁移到新服务时的向后兼容性问题和潜在陷阱。一旦我们能够确定我们已经覆盖了所有的主要部分,我们就开始为项目分配架构师和开发人员,并开始相关工作。

从概念到交付

我们很惊讶,这么快就完成了项目的一部分编码,大约只需要两个月的工作。因为这是我们第一次将 Go 引入内部,我们在进行项目这部分的编码时很小心。对于每个功能,我们做了两次迭代,以确保我们做对了,而且,我们还做了大量的代码评审。这是因为我们知道这段代码必须精雕细琢,干净利落,因为这将作为后续其他 Go 项目的一个基础。

尽管这是我们引入的第一个 Go 项目,但我们有机会好好掌握这门语言及其核心功能,因为我们必须弥补 Clojure 在与栈中其他部分如 Redis(用户登录计数器的持久状态,预防 DDoS 和 bot 病毒)和 Kafka(我们管理着一个域事件 CQRS,其中一个是成功或不成功的登录)进行通信时所使用的库的不足,这需要我们在 Go 中创建类似的库。Clojure 这种语言需要我们的应用程序编译为 Java 字节码,并且,作为一个先决条件,需要在我们作为部署目标的机器上运行 JVM,而 Go 是一种静态编译语言,不需要在一台机器上运行一个 VM。

为了实现开箱即用的功能,如 CPU 和内存使用情况指标、业务逻辑计数器等等——我们基本上需要从头开始编写整个栈,这使我们可以更快地深入到 Golang 的错综复杂之处。

在大约两个月之后,我们做好了基本的迁移准备,覆盖了基本的功能和测试。我们开始在父节点组(域组)中以可控的方式将服务迭代地迁移到新的 API 网关,基本上是一次金丝雀发布。

在这个过程的最初几个周里,我们决定以可控的方式推出第一批的几个服务,这样,我们就可以发现生产中的错误和缺陷,并且有时间在推出我们所有的服务之前修复它们。在从最初的 API 解决方案迁移时,由于速度过快而导致了错误,并最终导致了低质量交付,我们希望吸取教训。

一旦我们觉得自己已经准备好并修复了所有的缺陷,我们就开始了所有服务的迁移计划。这包括每个服务的迁移指南 PDF,其中包括迁移到新服务所需的具体步骤,这一举措的好处,以及根据具体的栈和依赖的不同确定执行迁移的最优方法。

为了以循序渐进的方式推出一个新的反向代理,我们使用了一个应用程序负载均衡器(ALB)基于一组预定义的 URL 来进行流量路由,这些 URL 是我们想要通过新旧 API 网关暴露的服务。

FFZnqey.jpg!web

这为我们如何以最小的努力和风险路由流量提供了一种非常可控的方法。我们把我们的时间用在了测试每个要迁移的服务以及与其他负责用户服务的团队的协作上。我们花了六个月的时间,但我们设法使大约 40 个微服务在零停机的情况下迁移到了新的 API 网关。

结果

最终的结果是,我们从 25 个运行 Clojure 代码的实例(c4 xlarge)——能够处理 60 个并发请求——减少到两个运行 Go 代码的实例(c3.2xlarge),大约能够支持一分钟 5000 个并发请求,这是一个巨大的改进。对于我们下一阶段的增长,新的架构设计也是一个足够健壮的解决方案,它给我们提供了一个功能强大的服务,得益于其过程式方法,可以承受业务复杂性的大规模增长,同时,它也为我们处理大规模业务的工具箱添加了一门新语言。

让我们通过例子看一下 Clojure 和 Go 中的反向代理解决方案。

Clojure 中:

复制代码

;; Creating a connection manager


(let [cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout1:threads20:default-per-route10})])


;; Creating a proxy server using cm (connection manager)
(client/request {:method:get
:url(service/service-uri service-spec uri-match)
:headers(dissoc (into {} (:headersreq)) “content-length”)
:body(when-let [len (get-inreq [:headers“content-length”])]
(bs/to-byte-array (:bodyreq)))
:follow-redirectsfalse
:throw-exceptionsfalse
:connection-managercm
:as:stream}))

Golang 中:

复制代码

func NewProxy(spec *serviceSpec.ServiceSpec, director func(*http.Request), respDirector func(*http.Response) error, dialTimeout, dialKAlive, transTLSHTimeout, transRHTimeout time.Duration) *MultiReverseProxy{
return &MultiReverseProxy{
proxy:&httputil.ReverseProxy{
Director:director,//Request director function
ModifyResponse:respDirector,
Transport:&http.Transport{
Dial:(&net.Dialer{
Timeout:dialTimeout,//limits the time spent establishing a TCP connection (if a new one is needed).
KeepAlive:dialKAlive,//limits idle keep a live connection.
}).Dial,
TLSHandshakeTimeout:transTLSHTimeout,//limits the time spent performing the TLS handshake.
ResponseHeaderTimeout:transRHTimeout,//limits the time spent reading the headers of the response.
},
},

请注意,Golang 的许多特性都是为了更好地管理连接池以及将反向代理功能集成到其核心类中。

总结

事实是,使用类型化语言构建使我们能够借助 Golang 的库支持和社区的力量更轻松地插入各种功能并引入新技术。对我们来说,从性能和吞吐量的角度来看,最重要的部分是改善我们的客户体验。新部署的解决方案能够支持比现在多得多的流量——随着我们的流量和请求以 10 倍的规模增长,从前瞻性的角度来看,这是非常重要的。

关于作者

Asaf Yonay是 AppsFlyer 研发组经理,非常乐于接受管理和技术挑战,在其中加入人的元素,把它们变成成功的故事。Asaf 坚信,可以通过定义流程帮助研发团队成长和发展,而又没有速度损失,他始终在迎接全栈的挑战——相信这可以让经理进化为领导者。他一直在初创企业中承担不同的角色,包括技术支持、QA 和各种研发角色,使用 Clojure、Golang、Node.js 和 Python 构建可伸缩的、可靠的系统来支撑 React 和 Angular 服务,并使用 Kafka、Aerospike 和 Neo4J 处理大规模或复杂的业务逻辑状态。

查看英文原文: Rewriting an API Gateway Service from Clojure to Golang: AppsFlyer Experience Report


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK