Clojure的规模化:为什么Python不足以支持AppsFlyer

亲身体验和大规模介绍Clojure

Clojure仍然被认为是一种深奥的语言,是使我们兴奋的JVM语言之一。 关于为什么公司开始使用Clojure或如何使用它来构建Scale的故事并不多。 我们很幸运在AppsFlyer办公室针对 Takipi的Clojure错误查看器演示中听到了使用Clojure的出色示例,在此我们了解了为其移动应用程序测量和跟踪平台提供支持的体系结构。

在这篇文章中,我们将与您分享来自AppsFlyer,管理研发部门的Adi Shacham-Shavit和资深后端开发人员Ron Klein的新朋友的经验。 首先,非常感谢Ron和Adi在AppsFlyer的帮助下 ,将我们带到了Clojure的幕后 ! 如果您对他们有任何疑问并想了解更多信息,请随时使用下面的评论部分。

这是他们的故事:

让我们开始一些数字

  • 每天20亿个事件
  • 最近3个月的访问量翻了一番
  • 数以百计的实例
  • 在过去的一年中,公司从6人发展到50人
  • 10个Clojure开发人员
  • 技术– Redis,Kafka,Couchbase,CouchDB,Neo4j,ElasticSearch,RabbitMQ,Consul,Docker,Mesos,MongoDB,Riemann,Hadoop,Secor,Cascalog,AWS

扩大痛苦

在AppsFlyer,我们实际上是从Python开始的代码库。 两年后,这还不足以处理不断增长的用户和请求。 我们开始遇到一些问题,例如关键的Python进程之一花费太长时间来消化传入的消息,这主要是由字符串操作和Python自己的内存管理系统引起的。 即使在多个进程和服务器之间划分消息也无法克服这一问题。 这最终导致进程中断,并导致数据丢失–第一个“ Python受害者”是报告服务。

采取功能性方法

随着这些困难的累积,我们不得不在以下两个选项中进行选择:

  1. 用C重写我们的一些服务(性能出色,但是编写代码不太有趣),并用Python互操作代码包装(容易实现)
  2. 用更适合数据处理的编程语言重写我们的一些服务

在这一点上,重要的是要提到的是,我们采用了异步事件驱动的方法来处理传入消息,这使系统可以随着流量的增长轻松扩展。

在恶意报告服务开始失败之前,我们一直在考虑将功能编程引入公司的想法。 这与我们的思维方式和体系结构非常吻合,因此进行更改是合乎逻辑的-尤其是由于报告服务故障促使我们已经进行了致电。 在决定采用它之后,出现了第二个障碍,我们应该选择哪种语言?

Clojure

Scala脱颖而出是因为它是面向对象编程和功能编程的混合体,并且更倾向于OOP。 OCaml被废弃是因为社区相对较小,并且全局解释器锁(GIL)一次只能执行一个线程,即使在多核计算机上也是如此(这在Python中也是一个问题)。 Haskell的Monads使我们感到恐惧,所以我们只剩下Clojure。

但这不是我们选择这条道路的唯一原因,Clojure赢得了2个主要问题。 首先,它运行在JVM上,其次,它是一种功能语言,即使在高度并发的环境中,也可以根据需要轻松访问可变状态。

Clojure是Rich Hickey的Lisp编程语言的方言。 这是一种通用编程语言,重点是函数式编程。 与其他Lisps一样,Clojure将代码视为数据,并且具有宏系统。 其核心是不变的值和明确的时间进度结构,旨在促进更健壮的程序(尤其是多线程程序)的开发。

微服务架构

AppsFlyer系统的服务器端旨在连续接收消息(事件),对其进行处理,存储它们,有时还基于这些消息向外部端点调用其他Web请求。 这些“事件”流使我们做出了一些架构决策,这些决策可以帮助我们根据需要进行扩展。 主要决策之一是将系统视为服务的集合,主要通过消息队列(以前是通过Redis的pub / sub,现在是通过Kafka)进行相互通信。 这使我们的服务独立且松散耦合。

事件流

让我们以一个简化的示例为例:“应用程序已安装”事件通过名为“安装”的Kafka主题(队列)发布到整个系统。 我们的报告服务会侦听该主题,以便它可以为相关报告存储此数据。 另外,我们的Postbacks服务会侦听同一主题,并根据自己的规则决定是否调用Web请求以及将端点调用到哪个端点。

由于整个系统是基于微服务的,这些微服务消耗来自公共管道的消息(并将消息发布到公共管道),因此,假设该系统对公共管道具有不错的客户端库,则很容易用任何编程语言来重写它们。 Kafka被用作主要骨干,RabbitMQ被用作实时频道。
Clojure中的并发

Clojure提供了自己的并发方法,可能需要花费一些时间来适应它。 但是,一旦有了思路,在Clojure中完成任务就比采用“常规”方法容易得多。 在大多数情况下,用Clojure编写处理并发性的代码根本不包含锁语句。 这是一个巨大的优势:编码更专注于逻辑本身,而不是围绕锁的问题。

Clojure还具有防止数据损坏的机制。 当然,这需要权衡:线程A拥有的共享资源不包含线程B之前所做的所有更改的可能性很小。通常,Clojure提供了一种很好的不变数据结构的机制,从而确保了数据完整性和一定程度的一致性。 Clojure可以访问JVM可以提供的几乎所有内容,因此您仍然可以使用传统锁。 但是,如果您构建的系统是基于统计信息的,并且您可以容忍轻微的数据丢失(例如,AppsFlyer拥有的分析系统),那么Clojure远远不够。

一个真实的例子

假设我们有一个将其状态保存在键值数据结构map中的服务 。 该映射最初在模块级别定义为空(为简化起见,简化了此示例,因此未编写可完全重用的代码):

(def my-map {})
;; Don't panic, you'll get used to the braces...

上面的语句创建了一个空地图,可通过名称my-map访问

大括号语法之后,吸引Clojure编程的大多数新手的第一件事是命名变量的自由。 Clojure允许使用一些有趣的字符来表示变量名,例如“-”,“?”,“!” 等。考虑一下名为contains的函数背后的简单性吗? 用于检查集合是否包含项目。

将键“ k”和值“ v”添加到给定映射的基本代码是:

(assoc some-map "k" "v")

此代码不会更新原始地图。 Clojure保持其数据结构尽可能不变。 而是,上面的语句返回具有新键和新值的原始地图的新副本。 在幕后,Clojure没有完全复制整个地图。 取而代之的是,它保留带有指向先前修订版本的指针的修订版本以及差异。 聪明吧?

返回我的地图 。 我们必须修改语句,以便可以进行并发:

(def my-map (atom {}))

这个小原子几乎是我们并发方式所需要的。 因此,现在,当正在运行的线程“更新” 我的地图 (阅读:为其创建新的修订版),使其还包含键“ my-key”,其值为42时,代码如下所示:

(swap! my-map assoc "my-key" 42)

该语句更改了my-map ,以使其现在拥有自身的新版本。

到目前为止,我们有一个线程“更新” my-map 。 在Clojure中读取地图并继续前面的示例,看起来像这样:

(get some-map "k")

上面的语句应返回值“ v”。 当使用Clojure的atom时 ,线程从my-map读取值时可以执行以下代码:

(get @my-map "my-key")

唯一的区别是my-map之前很少有“ @”。 它说的是,“嘿,Clojure,给我提供您对我的地图的最新修订。” 如上所述,最新,最新的修订版可能不会包含到目前为止对地图所做的所有更改,但是就数据完整性而言,返回值始终是安全的(例如,未损坏)。

结论

Clojure有其自己的心态–不可变对象,Lispy语法等。主要优势在于它的并发方法,专注于应用程序的逻辑并减少了锁定机制的开销。 这篇文章仅介绍Clojure的少量并发方式。 当我们将AppsFlyer移至Clojure时,我们获得了显着的性能提升。 另外,使用函数式编程可以使我们的代码库很小,每个服务只有几百行代码。 在Clojure中工作的效果大大加快了开发时间,使我们能够在几天之内创建新服务。

翻译自: https://www.javacodegeeks.com/2014/11/clojure-at-scale-why-python-just-wasnt-enough-for-appsflyer.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值