探讨迪纳哥纸的审查
erlang(20), distributed-systems (3)最近我有幸读了这本书Amazon Dynamo paper,并试图在一些关于它的结构化笔记下拍打,但是只因为纸张掠过一个以相当快其轻快的速度掠过一个主题。
我认为记录这些想法的一个好方法(以及在我的脑海中巩固它们)是经历编写分布式键值存储的过程,然后逐步添加Dynamo论文中讨论的增强功能。在本系列的最后,我们将重新实现来自Dynamo的大多数有趣的想法一distributed Erlang system.
Each post will present an idea and then apply it to a sample distributed key-value store that evolves over the course of the series. (Some posts may contain several ideas, with the aim to keep each post long enough to deserve its own URI but short enough to be consumed in one reading.)
为什么迪纳摩纸?
因为涵盖了大量主题。覆盖的一些主题是虚拟节点,一致散列,merkle trees,增量缩放,矢量时钟,gossip protocols,seeds andsloppy quorums. 这些主题中的任何一个都可能是一篇独立的论文,结果27页的Dynamo论文不得不略过许多细节,
However, a series of articles gives more leeway to explore these ideas; building a project adds a bit of a hands on feel, which I find to be the most instructive method of presenting material.
为什么是二郎?
Erlang是设计分布式系统的优异拟合。这不是颂歌消息传递,但更多的是对pg2模块和注册和交互的能力与在其他主机上运行的进程何时在本地运行。
我一直在享受用Clojure做实验,已经很久了Python Fan.,但既不是 - 很少有少数编程语言 - 可以随意横跨机器运营,随着休闲的易尔朗。
这是一个重要的消息,所以请原谅我一ting it once more: for distributed systems the key advantages of Erlang are the OTP application platform and the Erlang virtual machine. Pattern matching, message passing and the functional paradigm are all powerful language features, but they are replaceable; the OTP and the Erlang VM are not1.
先决条件?
本系列的目标受众是狂热的程序员,而不是狂热的Erlang程序员。要通读这个系列,你只需要一点坚持。要完成本系列,您需要熟悉Erlang语法,并拥有erl
已安装,但对pg2
等等,不是必需的。
相反,除了讨论Dynamo实现之外,本系列的第二个目标是介绍尽可能多的实现和部署Erlang应用程序的重要概念,因此本系列的目的是为Erlang新手提供启发性的阅读。
我们的原型
在第一个条目中,我们将把分布式键值存储的初始版本放在一起。
我们定义的初始接口是:
start(不)- >起动.停止()- >停止.get(钥匙)- >{好的,价值}。get(钥匙,超时)- >{好的,价值}。设置(钥匙,价值)- >{好的,updated}。设置(钥匙,价值,超时)- >{好的,updated}。
Such that usage looks like:
2>千伏:start(5)。起动3>千伏:get(一)。{好的,undefined}4>千伏:设置(一,10)。{好的,updated}5>千伏:get(一)。{好的,10}6>千伏:get(一)。{好的,10}7>千伏:停止().停止
For the most part these function definitions won't change, butstart
需要一些改进,因为我们添加了更多的配置选项。现在让我们研究一下最初的实现(在本系列中会有很大的变化)。
start
和停止
我们将非常依赖pg2throughout this series.pg2
是用于创建全局进程组的模块,其中进程自身寄存器,因此可以从其他进程中容易地发现。
这个true value is that registered processes are not just discoverable to other processes on the same node, but to processes running on other nodes as well. Since nodes can be run on different physical machines,pg2
让我们能够在最少的麻烦下跨越机器的能力。
When usingpg2
,第一步是通过创建过程组通过pg2:create/1
(一个幂等操作,使得在现有组上调用它不会伤害它),并通过以下方式添加后进程PG1:加入/ 2
. 稍后,可以通过从组中删除进程第2页:离开/2
.(过程组本身可能通过第2页:删除/1
。)
Thus, we can implementstart/1
如下所示(其中一个参数是表示要创建的节点数的整数):
%% @doc create N processes in distributed key-value store%% @spec start(integer()) -> startedstart(不)- >pg2:创造(千伏),清单:Foreach.(乐趣(_)- >pg2:join(千伏,产卵(千伏,商店,[[]]))end,清单:序号(0,不)),起动.
几件事要注意:
- 我们还没有描述
kvs:store / 1
功能尚不,因此上面的代码尚未成功编译。 繁殖/3
用于使用指定的函数和参数创建进程,并返回进程标识符。- 的第二个参数
pg2:join/2
返回的进程标识符繁殖/3
.2 - 你可以跑了
start/1
多次,在多个节点上,所有创建的流程都将加入同一流程组。因此,这个start函数已经支持跨多个物理主机使用进程。
甚至比那么简单start/1
,这是我们的实施停止/ 0.
:
%% @doc stop all pids in KVS process group%% stop() - >停止。停止()- >清单:Foreach.(乐趣(PID)- >pg2:离开(千伏,PID),PID!停止end,pg2:获取\u成员(千伏)),停止.
不otice that停止/ 0.
将删除所有进程from the千伏
过程组。这意味着它将删除在其他物理主机上创建的进程。暂时是合理的,但我们可能希望在系列进步时创建一个更细微的方法来停止。三
另一个有趣的方面停止/ 0.
这是本系列的第一个消息传递示例。4在erlang的消息传递的语法是
接收者PID!TermToSend.
在哪里?接收者PID
是接收器的进程标识符,以及TermToSend
is an arbitrary Erlang term. These are all legal examples of message passing:
PID!好的.PID!{好的,[{一,10},{b,20}]}.PID![一,b,c,d]。PID!d;商店(name,<<“遗嘱”>>,d:新的())。
不ow that we've written thestart/1
和停止/ 0.
函数,是时候写商店/1
包含真实键值存储逻辑的函数。
键值商店/1
这个商店/1
功能是实现的核心。它维护键值存储,处理更新和传播值,并处理检索客户端的值。
In this first implementation we are making a couple of strategic decisions:
- We are storing values in a property list, because it is very simple. It goes without saying that a property list is not a suitable datastructure for large numbers of values (average complexity of
o(n / 2)
对于读取和更新,最坏的情况复杂性O(N)
)。我们稍后会解决这个问题。 - We are storing all data in every processes. That is, we're doing replication instead of sharding. This is a temporary condition, which we'll improve on throughout the series.
- Reading always reads from a single process, which would be appropriate if we verified all other processes had successfully updated before reporting that a write succeeded. This'll improve over time as well.
With those caveats in mind, here is our initial implementation of商店/1
:
%% @doc implementation of distributed key-value store%% @spec store(proplist()) - >术语()%% proplist = [{term(), term()}]商店(数据)- >接收{Sender,get,钥匙}- >%用于检索值的客户端接口Sender!{自己(),得到了,proplists:get_value(钥匙,数据)},商店(数据);{Sender,设置,钥匙,价值}- >% client interface for updating values清单:Foreach.(乐趣(PID)- >PID!{自己(),update,钥匙,价值}end,pg2:获取\u成员(千伏)),Sender!{自己(),收到,{设置,钥匙,价值}},商店(数据);{Sender,update,钥匙,价值}- >通过第一接收节点发送给所有节点Sender!{自己(),updated,钥匙,价值},商店([{钥匙,价值}|proplists:删除(钥匙,数据)]);{_Sender,updated,_钥匙,_价值}- >商店(数据);停止- >好的end.
它有许多方面商店/1
简要介绍一下:
这是第一次出现
接收
关键字,这是在erlang中使用消息的下半部分(上半部是!
操作员)。接收
和!
合作如下:发件人进程中的%接收者!{设置,一,“b”}。%接收过程中接收{设置,钥匙,Val}- >io:格式(“收到〜P.=>〜P.~n",[钥匙,Val]),end.
还要注意的是
自我/ 0.
函数返回所谓的过程的PID,因此当发送者希望接收器响应时经常使用它。发件人进程中的%接收者!{自己(),get,name}。
%接收过程中接收{Sender,get,钥匙}- >价值=从某处得到(钥匙),Sender!{得到了,钥匙,价值)end.
pg2:get_members/1
takes a process group name and returns a list of all pids which are currently members. It, along withpg2:get_closest_pid / 1
是从进程组获取pid的两种机制。不ote the distinction we're making between
设置
和update
:设置
是更新值的外部接口,以及update
是传播值的内部接口。(也请注意,我们忽略了
updated
message at this point.)
不ow that the商店/1
implementation is done, it's time to write some convenient wrappers to export from the千伏
模块,简化了与之交互商店/1
乐趣ction.
get
和设置
界面
虽然可以直接与流程进行通信,但这样做需要大量的知识。你需要知道他们是在千伏
进程组。你需要知道他们想要的信息的格式。您还需要确保交互是同步的(默认情况下Erlang消息传递是异步的,但是我们将看到下面的同步消息传递的标准模式)。
我们可以通过编写界面功能来缓解这些问题,以便在键值存储中执行两个重要函数:设置和检索。
首先让我们来看看实施获取/1
和获取/2
.
%% @doc retrieve value for key%% @spec get(term()) -> value() | undefined%% value = term()get(钥匙)- >get(钥匙,?TIMEOUT)。%% @doc retrieve value for key, with timeout%% @spec get(term(),integer()) - > val()|超时()%%val={ok,term()}{ok,未定义}%%超时={错误,超时}get(钥匙,超时)- >PID=pg2:get_closest_pid.(千伏),PID!{自己(),get,钥匙},接收{PID,得到了,价值}- >{好的,价值}之后超时- >{错误,timeout}end.
在这个例子中出现了一些新的东西:首先,pg2:get_closest_pid / 1
,which randomly selects a process in the specified process group, with preference for pids running on the same node. This is useful for evenly distributing work across the pids in a process group5.
第二,注意发送,使用接收阻止响应模式,通常用于两个进程之间的同步通信。
这个设置/2
和设置/三
乐趣ctions are very similar to获取/2
;按照相同的模式,它们仅在发送的消息和预期响应中不同。
%% @doc update value for key%% @spec set(term(), term()) -> {ok, updated} | {error, timeout}设置(钥匙,Val)- >设置(钥匙,Val,?TIMEOUT)。%% @doc更新键的值,超时%% @spec set(term(), term()) -> {ok, updated} | {error, timeout}设置(钥匙,Val,超时)- >PID=pg2:get_closest_pid.(千伏),PID!{自己(),设置,钥匙,Val},接收{PID,收到,{设置,钥匙,Val}}- >{好的,updated}之后超时- >{错误,timeout}end.
不应该有任何东西设置/三
我们之前没有遇到过哪些,因此我们可以继续执行最简单的分布式键值存储形式的最后任务。
千伏
Module
最后,我们需要编写标题千伏
模块,只需要几行。
-模块(千伏)。-出口([get/1,get/2,设置/2,设置/三,start/1,停止/0,商店/1]).-定义(TIMEOUT,500)。
注意我们正在定义TIMEOUT
,which is the number of milliseconds to wait for a response. You might also want to change500
到无穷
,which means no timeout is used. That said, for now 500 milliseconds is functionally equivalent to infinity, so 500 seems like a reasonable default.6
在结束时
这个full code written in this entry is available on GitHub. (请注意,我链接到的是千伏.erl
文件,不是最新版本的代码。)
不ow that we've written the infrastructure for this series, soon we'll be able to start examining the ideas in the Dynamo paper and incrementally building something that looks ever so slightly similar.
下一篇文章有空并讨论添加持久写入和一致的读取。
Okay, okay. Anything is replaceable. I guess we could write a distributed key-value store on top of Hadoop if we really wanted to. I'm sure that would be great fun.↩
In more serious implementations,
pg2:join/2
is usually an OTPGen_Server.将自己添加到创建过程中,并在终止时移除自己。对于此示例,处理生成的PID将导致比处理完整的Gen_Servers堆栈更简洁的代码,这些代码不是没有其特质的。↩如果您想知道如何轻松地将其扩展为仅停止本地节点上启动的进程,请查看
Erlang:节点/ 1
函数,它返回运行特定进程的节点。↩由于前面提到的gen_服务器,很多Erlang系统(如果不是大多数的话)都是在没有使用显式消息传递操作符(即。
!
). 取而代之的是发电机_服务器:呼叫/2
和Gen_Server.:c一st/2
用于分别用于同步和异步通信。在某些时候,该系列结束时,将代码转换为Gen_Server是值得的,但我很早就认为它将简单地模糊样板背后的有趣想法。↩
注意,不保证随机性,而且它总是将工作分配给局部节点,因此在某些病理情况下,可能会使用
pg2:get_closest_pid / 1
.If you need guaranteed balancing, you'll need to write a wrapper on top of
pg2:get_members/1
并且要么检查↩Generally all
-define
statements for a module are kept in a.hrl.
file. Since we only have one definition, for the time being I felt it was simpler to include it in the same file, but in general this isn't a best practice.↩