Elixir作为python异步的开发目标

在“ Python。 Luciano Ramallo讲述了一个故事。 2000年,卢西亚诺(Luciano)参加了课程,而吉多·范·罗苏姆(Guido van Rossum)则对观众进行了调查。 一旦发生这样的事件,每个人都开始问他问题。 当被问及Python从其他语言中借来了哪些功能时,Guido回答:“ Python的所有优点都被其他语言窃取了。”

真的是 Python长期生活在其他编程语言的上下文中,并从其环境中吸收概念:由于出现了Lisp lambda表达式,所以借用了asyncio,并从libevent复制了Tornado。 但是,如果有人应该借用想法,那就是Erlang。 它创建于30年前,目前在Python中实现或概述的所有概念早已在Erlang中运行:多核,消息作为通信的基础,实时生产系统中的方法调用和自省。 这些想法以某种形式出现在Seastar.io之类的系统中。


如果您不考虑数据科学,而Python如今已在竞争中脱颖而出,那么其他一切都已经在Erlang中实现:使用网络,处理HTTP和Web套接字,使用数据库。 因此,对于Python开发人员来说,了解该语言的发展方向非常重要:沿着30年前已经走过的道路。

为了了解其他语言的发展历史并了解正在取得的进步,我们邀请Erlyvideo.ru项目的作者Maxim Lapshinerlyvideo )到Moscow Python Conf ++

这份报告的文字部分是削减的,即:系统被迫朝哪个方向发展,该系统将继续从简单的线性代码迁移到libevent以及以后的版本,这是常见的,Elixir和Python之间的区别是什么。 我们将特别注意如何以不同的编程语言和平台管理套接字,线程和数据。


Erlyvideo.ru有一个视频监控系统,其中摄像机的访问控制是用Python编写的。 这是该语言的经典任务。 有用户和摄像机,以及他们可以观看的视频:某人看到了一些摄像机,而其他人看到了一个常规站点。

选择Python是因为在它上面编写这样的服务很方便:毕竟有框架,ORM,程序员。 所开发的软件打包并出售给用户。 Erlyvideo.ru是一家销售软件的公司,不仅提供服务。

我想解决Python的哪些问题。

为什么多核会出现此类问题? 我们甚至在Intel之前就在Stadia计算机上运行Flussonic。 但是Python对此有困难:为什么它仍然不能使用服务器的全部80个内核来工作?

如何不遭受开放式插座的困扰? 监视打开的套接字的数量是一个大问题。 当达到极限时,也要关闭并防止泄漏。

被遗忘的全局变量有解决方案吗? 对于任何垃圾收集语言(如Java或C#)来说,泄漏全局变量都是一件难事。

如何在不浪费资源的情况下使用铁? 如果我们想高效地使用服务器,而不想每月在不必要的硬件上投入数十万美元,该如何在不运行40名Jung工人和64 GB RAM的情况下度过难关?

为什么需要多核


为了充分利用所有核心,需要比核心更多的工人。 例如,对于40个处理器核心,需要100个工作人员:一个工作人员进入数据库,另一个工作人员忙于其他工作。

一个工人可以消耗300-400 MB 。 我们仍在用Python而不是在Ruby on Rails上编写此代码,因为它可能消耗更多的时间,而40 GB的RAM将很容易地浪费掉。 它不是很昂贵,但是为什么要在无法购买的地方购买内存。

多核有助于混淆共享数据并减少内存消耗 ,方便,安全地运行许多独立进程。 编程更容易,但是从内存上来说更昂贵。

套接字管理


在Web套接字上,我们从后端轮询摄像机的运行时数据。 Python软件连接到Flussonic,并轮询摄像头的状态数据:无论摄像头是否工作,是否有任何新事件。

另一方面,客户端进行连接,然后通过Web套接字将这些数据发送到浏览器。 我们要实时传输客户端数据:打开和关闭摄像头,猫吃饭,睡觉,撕毁沙发,按下按钮然后将猫赶走。

但是,例如,发生了某种问题:数据库没有响应请求,所有代码都掉了,有两个打开的套接字。 我们开始重新加载,做了一些事情,再一次出现了这个问题-有两个套接字。 DB错误未正确处理,并且两个打开的连接挂起。 随着时间的流逝,这会导致插座泄漏。

被遗忘的全局变量


对通过Web套接字连接的浏览器列表做出了全局决定。 一个人登录到站点,我们为他打开一个网络套接字。 然后,将带有其标识符的Web套接字放入某种全局指令中,结果发现发生了某种错误。

例如,他们在字典中记录了一个连接链接以发送数据。 发生了异常,忘记删除链接并且数据已挂起 。 因此,一段时间之后,开始会丢失64 GB,我想将服务器上的内存增加一倍。 这不是解决方案,因为数据仍然会泄漏。
我们总是会犯错误-我们是人,无法追踪一切。
问题是会发生一些错误,甚至是我们未曾看到的错误。

历史游览


要进入主要话题,让我们深入研究这个故事。 到目前为止,我们谈论的都是Python,Go和Erlang,其他人大约在30年前就走了。 我们使用Python可以走很长的路,并填补了几十年前已经过去的颠簸。 这条路以惊人的方式重复。

多斯


首先,我们来看看DOS,它是最接近的。 在他之前有完全不同的事情,并不是每个人还活着在DOS之前就记得计算机。

DOS程序(几乎)完全占用了计算机 。 例如,在运行游戏时,不执行任何其他操作。 您将不会访问Internet-它尚不存在,甚至无法到达任何地方。 虽然很伤心,但记忆犹新,因为它与青春有关。

合作多任务


由于使用DOS确实很痛苦,因此出现了新的挑战,计算机变得更加强大。 几十年前,他们甚至在Windows 3.11之前就开发了协作多任务处理的概念

数据由进程分开,并且每个进程分别执行:它们以某种方式彼此保护。 一个进程中的错误代码将无法破坏浏览器中的代码(然后,第一个浏览器已经出现)。

下一个问题是:如何在不同进程之间分配计算时间? 那时不是只有一个内核,而是双处理器系统非常罕见。 方案是这样的:例如,一个进程进入磁盘以获取数据时,第二个进程则从OS接收控制权。 当第二个人本人自愿给予时,第一个人将能够获得控制权。 我大大简化了这种情况,但是该过程以某种方式自愿允许将其从处理器中删除

抢先式多任务


协作式多任务处理导致以下问题:该过程可能由于编写不正确而挂起。 如果处理器要花费很长时间来处理,则会阻塞其余部分 。 在这种情况下,计算机崩溃了,无法执行任何操作,例如,切换窗口。

为了解决这个问题,发明了抢先式多任务处理。 现在,操作系统本身严格驱动:将进程从执行中删除,将它们的数据完全分离,保护进程内存彼此之间,并为每个人提供一定的计算时间。 OS为每个进程分配相同的时间间隔

时间安排的问题仍然悬而未决。 时至今日,OS开发人员仍在提出正确的方法,正确的顺序,给谁以及多少时间进行管理。 今天,我们看到了这些想法的发展。


但这还不够。 流程需要交换数据:通过网络很昂贵,但仍然有些复杂。 因此,发明了流动概念
线程是共享公共内存的轻量级进程。
创建流时希望一切都将变得简单,简单和有趣。 现在, 多线程编程被视为反模式 。 如果业务逻辑是用线程编写的,则此代码很可能会被丢弃,因为其中可能存在错误。 如果您觉得没有错误,那么您根本还没有发现它们。

多线程编程是一件极其复杂的事情。 很少有人真正致力于写线程的能力,并且得到了真正有用的东西。

同时,出现了多核计算机 。 他们带来了可怕的东西。 对数据采取了完全不同的方法,关于数据的局部性提出了疑问,现在您需要了解从哪个内核访问哪个数据。

一个核心需要将数据放在这里,另一个核心则要放在这里,在任何情况下都不要混淆这些事情,因为群集实际上是出现在计算机内部的。 在现代计算机内部,当一部分内存焊接到一个核心而另一核心焊接到另一个核心时,就会形成群集。 这些数据之间的传输时间可能会变化几个数量级。

Python示例


考虑一个简单的示例“服务于客户的服务”。 他在多个平台上为商品选择了最佳价格:我们以商品的名义开车,并寻找最低价格的交易大厅。

这是旧的Django Python 2中的代码。今天它不是很流行,很少有人在上面启动项目。

@api_view(['GET']) def best_price(request): name = request.GET['name'] price1 = http_fetch_price('market.yandex.ru', name) price2 = http_fetch_price('ebay.com', name) price3 = http_fetch_price('taobao.com', name) return Response(min([price1,price2,price3])) 

一个请求到达后,我们转到一个后端,然后到另一个后端。 在http_fetch_price地方,线程被阻塞。 这时,整个员工开始前往Yandex.Market,然后前往eBay,然后直到在淘宝上超时,最后给出答案。 所有的时间里,整个工人站着

同时轮询多个后端非常困难。 这是一种糟糕的情况:需要消耗内存,需要启动大量工作程序并需要监视整个服务。 有必要查看此类请求的频率,您是否仍需要运行工作者或是否还有其他请求。 这些正是我所说的问题。 有必要依次询问几个后端

我们在Python中看到了什么? 每个任务只有一个进程,在Python中仍然没有多核。 情况很明显:用此类语言编写安全的简单多核很困难,因为它会降低性能

如果您从不同的流中访问字典,则可以这样编写对数据的访问:将两个Python实例粘贴到内存中,以便它们翻阅数据-只是破坏它们。 例如,要听写而不破坏任何内容,您需要在其前面放置互斥体。 如果每个命令之前都有一个互斥量,那么系统将减慢大约1000倍的速度-这将非常不便。 很难将其拖到多核中。

我们只有一个执行线程, 只有流程可以扩展 。 实际上,我们在流程中重新发明了DOS-2010的脚本语言。 在进程内部,有一个类似于DOS的东西:当我们在做某事时,所有其他进程都无法正常工作。 没有人喜欢巨大的成本超支和反应缓慢。

套接字反应器早在Python出现之前就已经出现了,尽管这个概念本身很早就诞生了。 现在,您可以一次看到几个套接字的就绪状态。

最初,反应堆在诸如nginx之类的服务器上需求旺盛。 包括由于正确使用此技术而引起的流行。 然后,该概念爬入了脚本语言,例如Python和Ruby。
反应堆的想法是,我们继续进行面向事件的编程。

面向事件的编程


一个执行上下文产生一个请求。 在等待答案时,正在执行其他上下文。 值得注意的是,我们几乎经历了从DOS到Windows 3.11的过渡。 只有20年前有人这样做,而在Python和Ruby中则是10年前出现的。

扭曲的


这是一个事件驱动的网络框架。 它出现在2002年,是用Python编写的。 我以上面的示例为例,将其重写为Twisted。

 def render_GET(self, request): price1 = deferred_fetch_price('market.yandex.ru', name) price2 = deferred_fetch_price('ebay.com', name) price3 = deferred_fetch_price('taobao.com', name) dl = defer.DeferredList([price1,price2,price3]) def reply(prices): request.write('%d'.format(min(prices))) request.finish() dl.addCallback(reply) return server.NOT_DONE_YET 

可能存在错误,不准确,并且臭名昭著的错误处理还不够。 但是大概的方案是这样的:我们不提出请求,而是在有时间的某个时候要求去提出这个请求。 在defer.DeferredList行中,我们希望将几个查询的响应收集在一起。

实际上,代码由两部分组成。 第一部分,在请求之前发生了什么,第二部分,在请求之后发生了什么。
面向事件的编程的整个历史充满了在“请求之前”和“请求之后”破坏线性代码的痛苦。
这很痛苦,因为代码片段混合在一起:最后几行仍在原始请求中执行,并且将在之后调用reply函数。

精确地记住这一点并不容易,因为我们破坏了线性代码,但是必须这样做。 无需赘述,从Django重写为Twisted的代码将产生完全令人难以置信的伪加速

想法扭曲

套接字就绪时可以激活对象。
我们将对象从上下文中收集到必要的数据中,并将其激活绑定到套接字。 套接字可用性现在是整个系统最重要的控件之一。 对象将是我们的上下文。

但是与此同时,该语言仍然将异常所在的执行上下文的概念分开。 执行上下文与对象分开存在,并与它们松散地连接 。 这里出现的问题是我们试图在对象内部收集数据:没有它们是没有办法的,但是语言不支持它。

所有这些都导致了经典的回调地狱。 例如,对于他们喜欢Node.js的东西-直到最近,根本没有其他方法,但是它仍然出现在Python中。 问题在于, 外部IO的代码中断会导致回调。

有很多问题。 是否可以“胶合”代码中间隙的边缘? 是否有可能回到正常的人工密码? 如果逻辑对象使用两个套接字并且其中一个关闭,该怎么办? 如何不忘记关闭第二个? 是否有可能以某种方式使用所有内核?

异步IO


这些问题的一个很好的答案是异步IO。 这是一个艰巨的进步,尽管并非易事。 异步IO是一件复杂的事情,其背后有许多痛苦的细微差别。

 async def best_price(request): name = request.GET['name'] price1 = async_http_fetch_price('market.yandex.ru', name) price2 = async_http_fetch_price('ebay.com', name) price3 = async_http_fetch_price('taobao.com', name) prices = await asyncio.wait([price1,price2,price3]) return min(prices) 

代码间隙隐藏在语法async/await 。 我们使用了之前的所有内容,但未使用此代码访问网络。 我们删除了上一个示例中的Callback(reply) ,并将其隐藏在await -用剪刀剪切代码的地方。 它分为两部分:调用部分和回调部分,它们处理结果。

这是一种很棒的语法糖 。 有多种方法可以将多个期望合并为一个。 这很酷,但有一个细微差别: “经典”套接字可以破坏一切 。 在Python中,仍然有大量的库可以同步进入套接字,创建timer library并为您破坏一切。 我不知道该如何调试。

但是asyncio并不能解决泄漏和多核问题 。 因此,尽管已经变得更好,但是没有根本的变化。

一开始我们仍然遇到所有问题:

  • 插座容易泄漏;
  • 易于在全局变量中保留链接;
  • 非常艰苦的错误处理;
  • 多核仍然很难。

怎么办


我不知道这是否会演变,但是我将以其他语言和平台展示其实现。

孤立的执行上下文。 在执行上下文中,结果被累积,套接字被保存:我们通常在其中存储有关回调和套接字的所有数据的逻辑对象。 一个概念:采用执行上下文,将它们粘在执行线程上,并将它们彼此完全隔离。

对象的范式转移。 让我们将上下文连接到执行线程。 有类似物,这不是新鲜的东西。 如果有人尝试编辑Apache源代码并向其中编写模块,那么他知道存在一个Apache池。 Apache池之间不允许链接 。 来自一个Apache池(与请求关联的池)中的数据位于其中,您无法从中获取任何信息。

从理论上讲,这是可能的,但是如果您这样做,那么要么有人责骂,要么他们不接受补丁,或者他们将在生产中进行长期而痛苦的调试。 在那之后,没有人会这样做并且允许其他人这样做。 根本不可能再在上下文之间引用数据,需要完全隔离。

如何交流活动? 所需要的不是很小的单子,它们本身是封闭的并且彼此之间不通信。 我们需要他们进行沟通。 一种方法是消息传递。 这是Windows在进程之间交换消息时所采用的大致路径。 在普通的OS中,您不能链接到另一个进程的内存,但可以通过网络发出信号(如UNIX)或通过消息发出信号(如Windows)。

流程和上下文中的所有资源都成为执行线程 。 我们粘合在一起:

  • 虚拟机中发生异常的运行时数据;
  • 执行线程,例如在处理器上执行的线程;
  • 一个逻辑上收集所有数据的对象。

恭喜-我们在一种编程语言中发明了UNIX! 这个想法是在1969年左右发明的。 到目前为止,它还没有在Python中使用,但是Python可能会实现这一点。 也许她不会来-我不知道。

它有什么作用


首先, 自动控制资源 。 在Moscow Python Conf ++ 2019上,他们您可以在Go上编写程序并处理所有错误。 该程序像手套一样站立,可以工作几个月。 的确如此,但是我们不会处理所有错误。

我们是有生命的人,我们总是有最后期限,渴望做一些有用的事情,而不是处理今天的第535个错误。 散布着错误处理的代码永远不会给任何人带来热情。

因此,我们都写下“幸福的道路”,然后在生产中弄清楚。 老实说:只有当您需要处理某些东西时,我们才开始处理。 防御性编程略有不同,它不是商业开发。

因此, 当我们可以自动控制错误时-很好 。 但是,操作系统是50年前提出的:如果某些进程死了,那么它打开的所有内容都会自动关闭。 今天,没有人需要编写代码来清理被终止进程后的文件。 在任何操作系统中,这种情况已经存在了50年之久,但是在Python中,您仍然需要用手仔细并仔细地遵循它。 真奇怪

您可以将繁重的计算带入不同的环境 ,但是它已经可以进入另一个核心。 我们共享了数据,我们不再需要互斥锁。 您可以在不同的上下文中发送数据,说:“您将在某个地方进行处理,然后让我知道您已经完成并完成了某些工作。”

一个不带“ async / await”字样的asyncio实现 。 虚拟机还可以从运行时提供一些帮助。 这就是我们用async/await讨论的内容:您还可以转换为消息,删除async/await并在虚拟机级别获取它。

Erlang过程


Erlang是30年前发明的。 那时不是很胡子的大胡子家伙看着UNIX,并将所有概念转移到了编程语言。 他们决定现在自己拥有自己的东西,可以在晚上睡觉,无需计算机即可安静地钓鱼。 那时还没有笔记本电脑,但是大胡子的家伙已经知道应该提前考虑这一点。

我们获得了Erlang(Elixir)-主动执行的上下文 。 进一步讲我关于Erlang的例子。 在Elixir上,它看起来差不多,只是有所不同。

 best_price(Name) -> Price1 = spawn_price_fetcher('market.yandex.ru', Name), Price2 = spawn_price_fetcher('ebay.com', Name), Price3 = spawn_price_fetcher('taobao.com', Name), lists:min(wait4([Price1,Price2,Price3])). 

我们启动了多个提取程序-这些是我们正在等待的几个单独的新上下文。 他们等待,收集数据并以最低价格返回结果。 所有这些都类似于async/await ,但是没有单词“ async / await”。

长生不老药的特点


Elixir位于Erlang的基地,所有语言概念都已悄悄移植到Elixir。 有什么特点?

禁止跨处理器链接。 所谓进程,是指虚拟机内部的轻量级进程-上下文。 简化后,如果移植到Python,则在Erlang中禁止另一个对象内的数据链接。 您可以将整个对象链接为一个封闭的框,但不能引用其中的数据。 您甚至无法从语法上获取指向另一个对象内部数据的指针。 您只能了解对象本身。

进程(对象)内部没有互斥体。 这很重要-就我个人而言,我一生中从未希望与调试多线程生产的历史相交。 我不希望任何人这样做。

流程可以绕核心移动,这是安全的。 当将数据从一个地方移到另一个地方时,我们不再需要像Java中那样绕过一堆其他pointer并重写它们:我们没有公共数据和内部链接。 例如,髋关节稀疏问题从何而来? 由于有人引用此数据。

如果我们将堆中的数据传输到另一个位置进行压缩,则需要遍历整个系统。 它可能占用数十GB的空间并更新所有指针-这太疯狂了。

全线程安全 ,因为所有通信都通过消息进行。 在所有这些投降之后,我们挤出排挤过程 。 他容易又便宜。

消息是沟通的基础。 内部对象,普通函数调用以及消息对象之间。 来自网络的数据到达是一条消息,另一个对象的响应是一条消息,外面的其他东西也是一个传入队列中的一条消息。 这不是在UNIX上,因为它尚未扎根。

方法调用。 我们有称为流程的对象。 通过消息调用进程上的方法。

调用方法也正在发送消息。 很好,现在可以超时了。 如果某件事缓慢地回答了我们,我们将在另一个对象上调用该方法。 但是同时我们说,我们准备等待不超过60秒,因为我的客户超时时间为70秒。 我需要去告诉他“ 503”-明天过来,现在他们不在等你了。

此外, 可以推迟电话应答 。 在对象内部,您可以接受调用该方法的请求,然后说:“是的,是的,我现在将您放下,半小时后再回来,我会答复您。” 您不会说话,但会默默地搁置一旁。 我们有时会使用它。

如何使用网络?


您可以编写线性代码,回调或asyncio.gather样式。 这看起来如何的一个例子。

 wait4([ ]) -> [ ]; wait4(List) -> receive {reply, Pid, Price} -> [Price] ++ wait4(List -- [Pid]) after 60000 -> [] end. 

在上一个示例的wait4函数中,我们wait4那些仍在等待答案的人的列表。 如果使用receive方法,我们将从该过程中获得一条消息,则将其写入列表。 如果列表结束,我们将返回所有内容并累积列表。 我们同时要求三个对象来驱动数据。 如果他们在60秒内没有一起管理,并且其中至少有一个没有回答“确定”,我们将有一个空白列表。 但是重要的是,我们必须立即对一大堆对象发出一般超时请求。

有人可能会说:“想想,libcurl也有同样的事情。” 但是在此重要的是,另一方面,不仅可以有一个HTTP行程,而且可以有一个DB行程,以及一些计算,例如,为客户端计算某种最佳数量。

错误处理


错误已从流传递到对象,它们现在是一个并且是 。 现在,错误本身不再附加到线程上,而是附加到执行它的对象上。

这更加合乎逻辑。 通常,当我们在板上绘制各种小正方形和圆形以希望它们能够变为现实并开始为我们带来成果和金钱时,我们通常绘制对象,而不是执行这些对象的流程。 例如,交货时,我们会收到有关另一个物体死亡的自动消息

生产中的自省或调试


这比向产品和借方付款要好,尤其是如果错误仅在高峰时段在负载下发生时更是如此。 在高峰时间,我们说:

-来吧,我现在重新启动!
-走出门,其他人会重新开始!

在这里,我们可以进入一个正在运行的生命系统,该系统目前尚未专门为此做好准备。 为此,您不需要使用探查器,调试器重新构建即可重新启动它。

在实时生产系统中不会造成任何性能损失,我们可以看到一系列流程:流程中的内容,流程的工作方式,废弃流程,检查流程发生了什么。 所有这些都是开箱即用的。

红利


该代码是超级可靠的。 例如,Python与old vs async具有脆弱性,并且它将保留五年,而且不少。 考虑到Python 3的实现速度,您不希望它会很快。

阅读和跟踪消息比调试回调更容易 。 这很重要。 似乎,如果我们还有处理可见消息的回调,那还有什么更好的呢? 由于消息是内存中的一条数据。 您可以用眼睛看一下,并了解这里发生了什么。 可以将其添加到跟踪器中,获取文本文件中的消息列表。 这比回调更方便。

实时生产系统中的 华丽多核 ,内存管理和自省功能

问题所在


当然,Erlang也有问题。

由于无法再引用另一个进程或对象中的数据,导致最大性能损失 。 我们必须移动它们,但这不是免费的。

在进程之间复制数据的开销。 我们可以用C编写一个程序,该程序将在所有80个内核上运行并处理一个数据数组,并且假定它正确且正确地执行了该程序。 在Erlang中,您无法执行此操作:您需要仔细剪切数据,将其分配到多个流程中,并跟踪所有内容。 这种通信消耗资源-处理器周期。

它有多快或多慢? 我们编写Erlang代码已有10年了。 在这十年中幸存下来的唯一竞争对手是用Java编写的。 与他在一起,我们几乎可以实现完全的绩效平衡:有人说我们更糟,有人说我们更糟。 但是从JIT开始,他们遇到了所有麻烦的Java。

我们正在编写一个程序,该程序为数以万计的套接字提供服务,并通过其自身泵送数万GB的数据。 突然发现,在这种情况下, 算法正确性以及在生产中调试所有这些功能的能力比潜在的Java包更重要 。 它已经投入了数十亿美元,但这并没有给Java JIT带来任何神奇的优势。

但是,如果我们要测量愚蠢和毫无意义的基准,例如“计算斐波那契数”,那么Erlang可能会比Python或更差。

消息分配的开销。 有时会痛。 例如,我们在代码中使用C编写了一些代码,在这些地方,这些代码对Erlang根本不起作用。 , , .

Erlang , , . , , receive send receive . — , . , , .

Python


. . , Python - .

, . - Python, , 20 , 40.

, . - , , Elixir, , .

Moscow Python Conf++ . , 6 4 . , , ) ) . Call for Papers 13 , 27 .

Source: https://habr.com/ru/post/zh-CN476112/


All Articles