为什么放弃 clojure
重要要点
- AppsFlyer每天使用微服务架构样式构建,处理近70亿个HTTP请求。 包装所有前端服务的系统的入口点是称为API网关的关键任务(非微型)服务。
- 用AppsFlyer默认语言Clojure编写的原始API网关开始积累技术债务。
- 对于新设计的API网关服务的建议,选择Golang作为与Clojure进行基准比较的语言。
- 基准测试是通过NGINX(由Lua增强)以及Golang和Clojure进行的。 与Clojure相比,Go的吞吐量有所提高,因此被选择为实现的语言。
- API网关现在已经以一种类型化的语言构建,这一事实使Golang的库支持和社区能够插入各种功能并更轻松地引入新技术。
- 与以前相比,新部署的解决方案能够支持的流量成倍增长,而且随着流量和请求以10倍的规模增长,从前瞻性角度来看,这是很重要的。
AppsFlyer是领先的移动归因和营销分析平台,每天处理近70+亿个HTTP请求(每分钟约5000万个请求),并采用微服务架构样式构建。 包装所有前端服务的系统的入口点是称为API网关的关键任务(非微型)服务。 从本质上讲,这是将流量从客户路由到我们的后端服务的单点,从而为我们的客户实现了指数级的身份验证和授权,但同时也存在潜在的单点故障。
本文探讨了工程团队为何以及如何从基于Clojure的API网关实现迁移到基于Go的实现。
在API网关中积累技术债务
就像我们的API网关服务一样,我们之前已经讨论过技术债务是如何产生的,并且发生了很多次。
最初,AppsFlyer的服务是Python整体,它需要一个完整的身份验证和授权解决方案作为整体的一部分。 随着时间的流逝,流量和复杂性都在增加,我们迁移到了微服务架构。 因此,我们需要创建一个统一的API网关解决方案,以用作我们的身份验证和授权提供程序。
我们从卷起袖子并用Clojure编写代码开始,跳过设计阶段,并在很大程度上以概念验证模式构建服务。 我们公司是欧洲,中东和非洲地区生产中最大的Clojure商店之一,因此,默认情况下,Clojure在很多情况下都是选择的语言,而无需考虑更多具体项目。 尽管这对于提高速度和“完成工作”的心态是有益的,但对于项目的长期维护而言,它并不是理想的选择。 随着流量的增长,我们很快意识到-新推出的API网关的代码过于复杂,需要不断进行重构以实现所需的吞吐量。
最终,我们走到了一个十字路口,那里的服务太不稳定了,我们意识到我们需要完全重写项目-使用Clojure(但设计更好),或者也探索其他语言选择。 通过这次迭代,我们决定不接受认知偏见并恢复到Clojure舒适区,而是进行构建所需服务所需的适当设计工作,而不仅仅是重新设计已有的服务。
我们最终选择了Golang作为该API网关服务的Clojure基准测试语言,该语言还带来了语言多样性的额外好处,并通过掌握更多语法提高了我们的代码制作技巧。
我们了解了在堆栈中添加另一种编程语言的另一面。 我们坚信CI / CD的心态,并引入了一种新的语言,这种语言不是基于JVM的(与Clojure相对),但它的运行成本很高,但是我们能够在短时间内解决。
当然,还需要学习掌握新语言的曲线,并且需要确保代码在长期内足够出色和健壮,这在实际用特定语言编写第一个项目之前很难知道并查看其在生产中的表现。
我将简要介绍为什么我们选择Go来进行这项特定服务-仅出于某些情况。 Go对构建网络服务,特别是对带有内置反向代理的类代理服务的支持非常强。 与其他解决方案(例如我们在Clojure中使用的http-kit)相比,它的最大优势是能够通过代理流式传输数据,而不是将其存储在内存中,并仅在收到最后一个字节后才将其返回给客户端。从服务器。 此功能与对有效I / O的支持一起,而无需付出我们必须在JVM等其他平台上编写的过于复杂的异步代码的代价,这使得Go的选择非常引人注目。 当我们开始实现服务时,一个明显的优势就是,静态类型的语言使重构代码及其原因变得容易得多,因为类型是自记录代码的绝佳方法。
评估我们的选择
我们知道,要能够正确评估不同语言的适用性,我们需要检查几个方面-每种语言在执行特定任务时的性能以及特定优势。 为了衡量性能,我们知道我们需要在尽可能接近生产模拟的情况下适当地对Clojure vs. Go进行基准测试。
为此,我们首先进行了压力测试,并选择了NGINX(由Lua增强),Golang和Clojure。 与Clojure相比,Go可以提高吞吐量。
测试的基本统计数据:
- 我们使用WRK作为基准测试工具
- 3分钟连拍
- 64个线程
- 1000个连接池
- 2分钟的请求超时
- 每个请求都返回一个重为500kb的静态文件
- 使用c4 xlarge实例从同一可用区触发所有流量以减轻网络噪声
代理解决方案 | 要求/秒 | 反/秒 | 要求总数 | 总交易规模 | 错误要求 | 平均 潜伏 |
直接 | 190 | 72兆字节 | 34500 | 12.8 GB | 〜400(掉落:200) | 4.41秒 |
NGINX | 185 | 73兆字节 | 33486 | 12.7 GB | 〜300(下降:37) | 7.95秒 |
Clojure(基本Http-Kit实现) | 190 | 72兆字节 | 34412 | 12.8 GB | 〜100(掉落:600) | 8.48秒 |
Golang(本地反向代理和http层) | 185 | 73兆字节 | 33443 | 12.7 GB | 〜200(下降:0) | 5.42秒 |
我们不再使用Clojure重写服务,这不仅是因为Go表现出更好的性能,而且还因为我们想挑战自己,并暴露于不同的语言和不同的思维方式。
设计阶段从概述我们要求服务具有的功能开始,并在指定了基本概念之后,我们在向生产用户群迁移到新服务的过程中研究了向后兼容性考虑因素和潜在的陷阱。 一旦我们确保涵盖了所有基础,便开始为项目分配建筑师和开发人员,开始工作。
从概念到交付
我们对项目的编码部分完成了如此Swift的工作感到惊讶,只需大约两个月的工作。 因为这是我们内部首次引入Go,所以我们在项目的编码部分非常小心。 我们对每个函数进行了两次迭代以确保我们做得正确,并进行了许多代码审查。 这是因为我们知道该代码必须精巧和简洁,因为它将作为其他Go项目的源代码。
尽管这是Go中引入的第一个项目,我们还是有机会真正掌握了该语言及其核心功能,因为我们不得不补偿Clojure中用于与堆栈的其他部分(包括Redis)进行通信的库。用户登录计数器的状态,以防止DDoS和僵尸程序)和Kafka(我们管理域事件的CQRS,其中之一是成功或失败的登录),这需要在Go中创建类似的库。
为了匹配Clojure中的生态系统,我们需要集成各种库,例如指标收集库,日志记录库,JWT库等,我们很高兴能找到所有成熟的库级别,这强烈表明社区对Go语言的采用程度-这是在决定迁移到新语言时的重要考虑因素。 它的社区可持续性和成熟度在这一决定中起着重要作用。
大约两个月后,我们就为基本迁移做好了准备,并涵盖并测试了基本功能。 我们开始以受控方式在父组(我们的域组)内迭代地迁移服务到新的API网关,该API网关基本上是一个Canary版本。
我们决定在最初的几周内对前几项服务进行有控制的部署,以便我们能够发现生产中的错误和缺陷,并有时间在推出我们的所有服务之前进行适当的修复。 我们想从原始API解决方案过快迁移的错误中汲取教训,最终导致交付质量低下。
一旦我们准备好并修复了所有缺陷,便开始了所有服务的迁移计划。 其中包括每个服务的迁移指南PDF,其中包括转移到新服务所需的确切步骤,此举所包含的好处以及根据其特定堆栈和依赖关系执行迁移的最佳方法。
为了逐步推出新的反向代理,我们使用了应用程序负载平衡器(ALB)根据一组预定义的URL路由流量,这些URL指示我们希望通过新的API网关与旧的API网关公开的服务之一。
这为如何以最小的工作量和最小的风险路由流量提供了一种非常可控的方法。 我们花了时间,测试了每个迁移的服务,并与负责其面向用户服务的所有其他团队携手合作。 我们花了六个月的时间,但我们设法迁移了约40个微服务,以使用零停机时间的新API网关。
结果
最终结果使我们能够将运行Clojure代码的25个实例(c4xlarge)(能够处理60个并发请求)减少到运行Go代码的两个实例(c3.2xlarge),每分钟能够支持约5000个并发请求,这是一个巨大的进步。 新的体系结构设计还为我们的下一阶段发展提供了足够强大的解决方案,它为我们提供了既可以承受大规模的强大服务,又可以凭借其过程方法轻松地实现业务复杂性的增长,而且还为我们的工具箱增加了新的语言当处理高规模时。
让我们以Clojure和Go中的反向代理解决方案为例。
Clojure:
;; Creating a connection manager
(let [cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 1 :threads 20 :default-per-route 10})])
;; Creating a proxy server using cm (connection manager)
(client/request {:method :get
:url (service/service-uri service-spec uri-match)
:headers (dissoc (into {} (:headers req)) “content-length”)
:body (when-let [len (get-in req [:headers “content-length”])]
(bs/to-byte-array (:body req)))
:follow-redirects false
:throw-exceptions false
:connection-manager cm
: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如何具有许多旨在更好地管理连接池和归入其核心类的反向代理功能的功能。
综上所述
选择在Go中编写新版本的API网关已被证明是一个非常好的决定。 Go的最小学习曲线使其成为在进行真正的生产服务时“实时”学习的出色语言。 它对诸如反向代理之类的低级网络结构的支持以及对性能的一般观念,使得最终结果既是真正可衡量的改进,也是更可靠的解决方案。 由于之前的代码而导致的所有生产问题现在都已过时,向网关添加新功能要容易得多,而且我们现在可以支持的增加的流量使我们所有人在晚上都能睡得更好。
本文已于2019年2月15日更新,以澄清意见讨论中提出的几点要点。
翻译自: https://www.infoq.com/articles/api-gateway-clojure-golang/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1
为什么放弃 clojure