MIX OTP——使用 ETS 加速

news2024/12/23 14:01:11

每次我们需要查找存储容器时,我们都需要向注册表发送一条消息。如果我们的注册表被多个进程同时访问,注册表可能会成为瓶颈!

在本章中,我们将了解 ETS(Erlang Term Storage)以及如何将其用作缓存机制。

警告!不要过早地将 ETS 用作缓存!记录并分析应用程序性能并确定哪些部分是瓶颈,这样您就知道是否应该缓存以及应该缓存什么。本章仅仅是一个示例,说明一旦您确定了需求,就可以如何使用 ETS。

ETS 作为缓存

ETS 允许我们将任何 Elixir 术语存储在内存表中。使用 ETS 表是通过 Erlang 的 :ets 模块完成的:

创建 ETS 表时,需要两个参数:表名和一组选项。从可用选项中,我们传递了表类型及其访问规则。我们选择了 :set 类型,这意味着键不能重复。我们还将表的访问权限设置为 :protected,这意味着只有创建表的进程可以写入它,但所有进程都可以从中读取。可能的访问控制:

:public — 所有进程都可以读取/写入。

:protected — 所有进程都可以读取。只有所有者进程可以写入。这是默认值。

:private — 仅限于所有者进程读取/写入。

请注意,如果您的读取/写入调用违反了访问控制,则操作将引发 ArgumentError。最后,由于 :set 和 :protected 是默认值,因此我们将从现在开始跳过它们。

ETS 表也可以命名,这样我们就可以通过给定的名称来访问它们:

让我们将 KV.Registry 更改为使用 ETS 表。第一个更改是修改我们的注册表以要求使用名称参数,我们将使用它来命名 ETS 表和注册表进程本身。ETS 名称和进程名称存储在不同的位置,因此不会发生冲突。

打开 lib/kv/registry.ex,让我们更改其实现。我们在源代码中添加了注释以突出显示我们所做的更改:

defmodule KV.Registry do
  use GenServer

  ## Client API

  @doc """
  Starts the registry with the given options.

  `:name` is always required.
  """
  def start_link(opts) do
    # 1. Pass the name to GenServer's init
    server = Keyword.fetch!(opts, :name)
    GenServer.start_link(__MODULE__, server, opts)
  end

  @doc """
  Looks up the bucket pid for `name` stored in `server`.

  Returns `{:ok, pid}` if the bucket exists, `:error` otherwise.
  """
  def lookup(server, name) do
    # 2. Lookup is now done directly in ETS, without accessing the server
    case :ets.lookup(server, name) do
      [{^name, pid}] -> {:ok, pid}
      [] -> :error
    end
  end

  @doc """
  Ensures there is a bucket associated with the given `name` in `server`.
  """
  def create(server, name) do
    GenServer.cast(server, {:create, name})
  end

  ## Server callbacks

  @impl true
  def init(table) do
    # 3. We have replaced the names map by the ETS table
    names = :ets.new(table, [:named_table, read_concurrency: true])
    refs  = %{}
    {:ok, {names, refs}}
  end

  # 4. The previous handle_call callback for lookup was removed

  @impl true
  def handle_cast({:create, name}, {names, refs}) do
    # 5. Read and write to the ETS table instead of the map
    case lookup(names, name) do
      {:ok, _pid} ->
        {:noreply, {names, refs}}

      :error ->
        {:ok, pid} = DynamicSupervisor.start_child(KV.BucketSupervisor, KV.Bucket)
        ref = Process.monitor(pid)
        refs = Map.put(refs, ref, name)
        :ets.insert(names, {name, pid})
        {:noreply, {names, refs}}
    end
  end

  @impl true
  def handle_info({:DOWN, ref, :process, _pid, _reason}, {names, refs}) do
    # 6. Delete from the ETS table instead of the map
    {name, refs} = Map.pop(refs, ref)
    :ets.delete(names, name)
    {:noreply, {names, refs}}
  end

  @impl true
  def handle_info(_msg, state) do
    {:noreply, state}
  end
end

请注意,在我们进行更改之前,KV.Registry.lookup/2 会向服务器发送请求,但现在它直接从 ETS 表中读取,该表在所有进程之间共享。这就是我们正在实施的缓存机制背后的主要思想。

为了使缓存机制发挥作用,创建的 ETS 表需要具有访问权限 :protected(默认值),因此所有客户端都可以从中读取,而只有 KV.Registry 进程可以写入它。我们还在启动表时设置了 read_concurrency: true,针对并发读取操作的常见场景优化了表。

我们上面执行的更改破坏了我们的测试,因为注册表在启动时需要 :name 选项。此外,某些注册表操作(例如 lookup/2)需要将名称作为参数而不是 PID 给出,因此我们可以进行 ETS 表查找。让我们更改 test/kv/registry_test.exs 中的设置函数以修复这两个问题:

由于每个测试都有一个唯一的名称,我们使用测试名称来命名我们的注册表。这样,我们不再需要传递注册表 PID,而是通过测试名称来识别它。还请注意,我们将 start_supervised! 的结果分配给下划线 (_)。这个习语通常用于表示我们对 start_supervised! 的结果不感兴趣。

一旦我们更改设置,某些测试将继续失败。您甚至可能会注意到测试在运行之间不一致地通过和失败。例如,“生成存储容器”测试:

可能在这一行失败:

如果我们刚刚在上一行创建了 bucket,这一行怎么会失败呢?

失败的原因在于,出于教学目的,我们犯了两个错误:

我们过早地进行了优化(通过添加这个缓存层)

我们使用 cast/2(而我们应该使用 call/2)

竞争条件?

使用 Elixir 进行开发并不能让您的代码摆脱竞争条件。但是,Elixir 的抽象(默认情况下不共享任何内容)使发现竞争条件的根本原因变得更加容易。

在我们的测试中,操作和我们可以在 ETS 表中观察到此更改的时间之间存在延迟。以下是我们预期会发生的情况:

1.我们调用 KV.Registry.create(registry, "shopping")
2.注册表创建存储桶并更新缓存表
3.我们使用 KV.Registry.lookup(registry, "shopping") 从表中访问信息
4.上面的命令返回 {:ok, bucket}

但是,由于 KV.Registry.create/2 是一个强制转换操作,因此该命令将在我们实际写入表之前返回!换句话说,发生了以下情况:

1.我们调用 KV.Registry.create(registry, "shopping")
2.我们使用 KV.Registry.lookup(registry, "shopping") 从表中访问信息
3.上面的命令返回 :error
4.注册表创建存储容器并更新缓存表

要修复故障,我们需要使用 call/2 而不是 cast/2 使 KV.Registry.create/2 同步。这将保证客户端仅在对表进行更改后才能继续。让我们回到 lib/kv/registry.ex 并更改函数及其回调,如下所示:

我们将回调从 handle_cast/2 更改为 handle_call/3,并将其更改为使用所创建存储容器的 PID 进行回复。一般来说,Elixir 开发人员更喜欢使用 call/2 而不是 cast/2,因为它也提供背压 — 您会阻塞直到收到回复。在不必要时使用 cast/2 也可以被视为过早优化。

让我们再次运行测试。不过这次,我们将传递 --trace 选项:

当您的测试出现死锁或存在竞争条件时,--trace 选项非常有用,因为它会同步运行所有测试(async: true 无效)并显示有关每个测试的详细信息。如果您多次运行测试,您可能会看到此间歇性故障:

根据失败消息,我们预期存储容器不再存在于表中,但它仍然存在!这个问题与我们刚刚解决的问题相反:虽然以前创建存储容器的命令和更新表之间存在延迟,但现在存储容器进程终止和其条目从表中删除之间存在延迟。由于这是一种竞争条件,您可能无法在您的机器上重现它,但它确实存在。

上次我们通过将异步操作(强制转换)替换为同步调用来修复竞争条件。不幸的是,我们用于接收 :DOWN 消息并从 ETS 表中删除条目的 handle_info/2 回调没有同步等效项。这次,我们需要找到一种方法来保证注册表已处理存储容器进程终止时发送的 :DOWN 通知。

一个简单的方法是在执行存储容器查找之前向注册表发送同步请求。 Agent.stop/2 操作是同步的,仅在存储容器进程终止后返回。因此,一旦 Agent.stop/2 返回,注册表就收到了 :DOWN 消息,但可能尚未处理该消息。为了保证 :DOWN 消息的处理,我们可以执行同步请求。由于消息是按顺序处理的,因此一旦注册表回复同步请求,则 :DOWN 消息肯定已经处理完毕。

让我们通过在 test/kv/registry_test.exs 的两个“删除”测试中的 Agent.stop/2 之后创建一个“虚假”存储容器(即同步请求)来实现此目的:

我们的测试现在应该(总是)通过!

这就是我们的优化章节。我们使用 ETS 作为缓存机制,其中读取可以从任何进程发生,但写入仍然通过单个进程序列化。更重要的是,我们还了解到,一旦可以异步读取数据,我们就需要注意它可能引入的竞争条件。

在实践中,如果您发现自己需要动态进程的注册表,则应使用 Elixir 提供的 Registry 模块。它提供的功能类似于我们使用 GenServer + :ets 构建的功能,同时还能够同时执行写入和读取。它已经过基准测试,即使在具有 40 个核心的机器上也可以扩展到所有核心。

接下来,让我们讨论外部和内部依赖关系以及 Mix 如何帮助我们管理大型代码库。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1887778.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【信息系统项目管理师】常见图表

作文里面的画图题用语言描述画图过程 合同 采购综合评分标准 责任分配矩阵 成本预算表 成本估算 成本管理计划 活动清单 活动属性 变更日志 问题日志 项目章程 自己再添加更多内容 甘特图 甘特图包含以下三个含义: 1、以图形或表格的形式显示活动; 2、…

JavaScript中window对象 , location对象以及history对象使用方法详细介绍

2.BOM(Browser Object Model) 操作浏览器的。常用的浏览器对象: 1.window对象:Window 对象表示浏览器中打开的窗口。 2.location对象:Location 对象包含有关当前 URL 的信息。Location 对象是 window 对象的一部分&…

[PyTorch]:加速Pytorch 模型训练的几种方法(几行代码),最快提升八倍(附实验记录)

本篇文章转自:Some Techniques To Make Your PyTorch Models Train (Much) Faster 本篇博文概述了在不影响 PyTorch 模型准确性的情况下提高其训练性能的技术。为此,将 PyTorch 模型包装在 LightningModule 中,并使用 Trainer 类来实现各种训…

使用 Python 五年后,我发现学 python 必看这三本书!少走一半弯路

第一本 《Python编程-从入门到实践》 适合零基础的读者 豆瓣评分:9.1 推荐指数:5颗星 推荐理由: 本书是针对所有层次的 Python 读者而作的 Python 入门书。全书分为两部分: 第一部分介绍使用Python 编程所必须了解的…

将excel表格转换为element table(上)

最近有个功能需要将excel展示到html 界面里面,看是简单的一个需求也是需要费尽心思才完得成 原始数据 想要把excel 读取出来,于是使用xlsl的插件 npm i xlsx通过插件可以获取到已经分析好的数据 然后使用sheet_to_html将数据转换为html 再使用v-htm…

ROS2 RQT

1. RQT是什么 RQT是一个GUI框架,通过插件的方式实现了各种各样的界面工具。 强行解读下:RQT就像插座,任何电器只要符合插座的型号就可以插上去工作。 2.选择插件 这里我们可以选择现有的几个RQT插件来试一试,可以看到和话题、参…

视频太大怎么压缩变小?6款视频压缩软件免费版分享

视频太大怎么压缩得又小又清晰呢?无论是视频文件传输、视频文件存储,还是进行自媒体视频上传,都对视频文件的大小有一定的限制。高质量的视频文件往往伴随着文件占据大量存储空间,导致文件传输速度变慢。今天教大家6种视频压缩软件…

配置WLAN 示例

规格 仅AR129CVW、AR129CGVW-L、AR109W、AR109GW-L、AR161W、AR161EW、AR161FGW-L、AR161FW、AR169FVW、AR169JFVW-4B4S、AR169JFVW-2S、AR169EGW-L、AR169EW、AR169FGW-L、AR169W-P-M9、AR1220EVW和AR301W支持WLAN-FAT AP功能。 组网需求 如图1所示,企业使用WLAN…

搜维尔科技:数据手套为什么要选择SenseGlove

了解 SenseGlove SenseGlove 是一支由电子工程师、触觉研究人员和计算机视觉专家、XR 开发人员、UX 设计师和产品创新者组成的科幻爱好者团队,他们拥有丰富人类能力和赋予 Metaverse 意义的技能和热情。 推进触觉技术是我们实现这一目标的方式。 公司及产品背景 S…

将多个SQL查询合并的两种方式

说明:单个简单查询是非常容易的,但是为了避免多次访问访问数据库,我们会尽可能通过表关联将业务所需要的字段值一次性查出来。而有时候不太清楚表之间的关联关系(这取决于对业务的熟悉程度),或者实际情况就…

ubuntu 安装并启用 samba

环境:ubuntu server 24.04 步骤如下: sudo apt update sudo apt install samba修改配置文件: sudo vi /etc/samba/smb.conf新增内容: [username]path /home/[username]available yesvalid users [username]read only nobrow…

2021强网杯

一、环境 网上自己找 二、步骤 2.1抛出引题 在这个代码中我们反序列&#xff0c;再序列化 <?php$raw O:1:"A":1:{s:1:"a";s:1:"b";};echo serialize(unserialize($raw));//O:1:"A":1:{s:1:"a";s:1:"b";…

RFID技术在粉末涂料配料生产线的精准应用

RFID技术在粉末涂料配料生产线的精准应用 应用背景 随着科技的快速发展&#xff0c;智能化、自动化已经成为现代工业生产的重要趋势。RFID&#xff08;无线射频识别&#xff09;技术以其独特的优势&#xff0c;如非接触式识别、高速读取、大容量数据存储等&#xff0c;在多个…

206.贪心算法:摆动序列(力扣)

代码展示 class Solution { public:int wiggleMaxLength(vector<int>& nums) {if (nums.size() < 1) return nums.size(); // 如果数组长度小于等于1&#xff0c;返回数组长度int curdiff 0; // 当前元素和前一个元素的差值int prediff 0; // 前一个差值int…

品牌推广怎么样?掌握正确做法,让品牌大放异彩!

品牌推广对于初创公司来说是一项至关重要的任务。在市场众多品牌中&#xff0c;如何脱颖而出&#xff0c;是每个品牌方都要考虑的问题。 作为一名手工酸奶品牌的创始人&#xff0c;目前全国复制了100多家门店&#xff0c;我来分享下&#xff0c;如何推广&#xff0c;可以让品牌…

Git 基础-创建版本库 git init、添加到暂存区git add、查看状态git status、查看改动git diff

1.创建版本库 git init 在目录中创建新的 Git 仓库。 你可以在任何时候、任何目录中这么做&#xff0c;完全是本地化的。 在目录中执行 git init&#xff0c;就可以创建一个 Git 仓库了。 注意: 没事不要手动修改 .git 目录里面的文件&#xff0c;不然改乱了&#xff0c;可能就…

zabbix“专家坐诊”第244期问答

问题一 Q&#xff1a;请教一下&#xff0c;我的zabbix6.0配置的基于snmptrap上报的日志提取关键字推送告警&#xff0c;正则表达式能否帮忙看看怎么弄&#xff1f;我这配置的提示一直不正确&#xff1f; A&#xff1a;具体看一下这里的信息。 Q&#xff1a;这个我是直接复制的…

亚马逊卖家专属ERP系统,无任何隐形FY。

三千多终身使用的ERP&#xff0c;图片翻译、文本图片翻译&#xff0c;无任何隐藏fy&#xff0c;不限制授权店铺&#xff0c;不限制开通子账号。 现在来讲下我们店飞飞ERP的铺货功能。 点击数据采集&#xff0c;选择需要的产品&#xff0c;可以批量编辑、批量编辑价格、一键翻…

权限控制信息

查看权限控制信息 修改权限控制信息 chmod命令 作用&#xff1a;修改文件、文件夹的权限细节&#xff0c;即第一图中序号1的部分。只能是文件、文件夹所属用户或root用户有权修改。 语法&#xff1a;chmod [-R] 权限 文件或文件夹 选项&#xff1a; -R&#xff0c; 对文件夹内…

如何申请小牛翻译API,超简单哦~~~

1.【注册】或【登录】小牛翻译官网账号。 账号注册成功后&#xff0c;小牛翻译每天提供免费的100积分&#xff0c;积分是全平台通用的&#xff0c;100积分相当于20万字符/34个文档/34张图片/25次语音翻译。&#xff08;在线翻译和API服务均可用哦~&#xff09; 2.登录账号后&a…