Elixir实战:12 构建分布式系统 (1) 分发原语
zhezhongyun 2025-01-05 21:29 44 浏览
本章涵盖
- 使用分发原语进行工作
- 构建容错集群
- 网络考虑事项
现在您已经建立了一个待办事项 HTTP 服务器,是时候让它更可靠了。要拥有一个真正可靠的系统,您需要在多台机器上运行它。单台机器代表了单点故障,因为机器崩溃会导致系统崩溃。相比之下,使用多台机器的集群,即使个别机器出现故障,系统仍然可以继续提供服务。
此外,通过将多台机器进行集群,您有机会实现横向扩展。当系统的需求增加时,您可以向集群中添加更多机器以适应额外的负载。这个想法在图 12.1 中进行了说明。
在这里,您有多个节点共享负载。如果一个节点崩溃,剩余的负载将分散到存活的节点上,您可以继续提供服务。如果负载增加,您可以向集群添加更多节点以承载额外的负载。客户端访问一个明确定义的端点,并且对内部集群的细节一无所知。
分布式系统显然提供了显著的好处,而 Elixir 和 Erlang 为您提供了一些简单而强大的分布式原语。基于 Erlang 的分布式系统的核心工具是进程和消息。您可以向另一个进程发送消息,无论它是在同一个 BEAM 实例中运行还是在远程机器上的另一个实例中运行。
不要将此与传统的 RPC 方法混淆,在这种方法中,远程调用被包装成看起来像本地调用。Erlang 及其扩展 Elixir 采取了相反的路线,它们的分布式特性在一开始就显现出来。如果你仔细想想,一个运行大量进程的典型并发系统已经可以被视为分布式的。
与远程服务类似,进程各自独立运行,完全隔离。向另一个本地进程发出请求可以视为远程调用,而消息传递与远程网络通信有很多相似之处。在基本版本中,您发送一条消息,但不知道其结果。您甚至无法确定消息是否会到达目标。如果您想要更强的保证,可以设计协议使目标向您发送响应(例如,通过使用同步调用)。此外,您还必须考虑传递消息的成本(内容会被复制),而这一特性有时会影响多个进程之间通信协议的设计。
所有这些属性在 Erlang 并发模型和分布式系统中是共同的,您需要考虑它们。好消息是,设计良好的并发系统在许多方面已经准备好在多台机器上分布。
这种转变绝非免费的。分布式系统引入了一系列需要解决的非平凡挑战。但多亏了可用的简单分布式构建块——其中许多您已经熟悉——您可以专注于分布式系统的核心挑战。
在本章中你将看到,将你的待办事项系统转变为一个基本的容错集群并不需要太多。为此,你需要熟悉基本的分布式原语。
12.1 分发原语
分布式 BEAM 系统是通过连接集群中的多个节点构建的。节点是一个与之关联名称的 BEAM 实例。
您可以在同一主机上或不同机器上启动多个节点,并且可以连接这些节点。一旦节点连接,您可以通过依赖熟悉的消息传递机制在不同节点上的不同进程之间进行通信。
12.1.1 启动集群
要设置集群,您需要启动几个节点。启动节点可以像在启动 shell 时使用 --sname 参数一样简单:
$ iex --sname node1@localhost ?
iex(node1@localhost)1> ?
提供节点名称
? 外壳报告节点名称。
使用 --sname 将您的 BEAM 实例转换为名称为 node1@localhost 的节点。 @ 字符之前的部分是一个前缀,唯一标识单台机器上的节点。第二部分 ( localhost ) 标识主机机器。如果您省略主机部分,将自动使用主机机器的名称。
--sname 参数设置了一个短名称,在该名称中,主机仅通过其名称进行识别。也可以提供一个长名称,在该名称中,主机通过完全合格的符号名称或 IP 地址进行识别。我们将在本章的最后部分对此进行更详细的讨论。
一旦您启动了一个节点,您可以通过调用 Kernel.node/0 函数来获取它的名称:
iex(node1@localhost)1> node()
:node1@localhost ?
该节点的名称
如您从输出中所见,节点名称在内部表示为一个原子。
使用节点通常在您想将其连接到另一个节点时是有意义的。让我们试试这个。保持 node1 运行,并启动另一个操作系统 shell 会话。现在,启动 node2 并将其连接到 node1 :
$ iex --sname node2@localhost
iex(node2@localhost)1> Node.connect(:node1@localhost) ?
true
? 连接到另一个节点
对 Node.connect/1 的参数是一个表示目标节点名称的原子。当调用 Node.connect/1 时,BEAM 尝试与目标 BEAM 实例建立 TCP 连接。一旦连接建立,节点被视为已连接,所有它们之间的通信都通过此连接进行。
您可以通过调用 Node.list/0 来证明节点是连接的,该调用返回与当前节点连接的所有节点的列表(当前节点不在列表中)。在 node1 和 node2 上尝试此操作会得到预期的结果:
iex(node1@localhost)2> Node.list()
[:node2@localhost] ?
iex(node2@localhost)2> Node.list()
[:node1@localhost] ?
? 连接到节点 1 的节点
? 连接到 node2 的节点
可以连接多个节点。实际上,默认情况下,BEAM 尝试建立一个完全连接的集群。如果您启动第三个节点 node3 并将其连接到 node2 ,则与 node2 连接的所有其他节点都建立了连接:
$ iex --sname node3@localhost
iex(node3@localhost)1> Node.connect(:node2@localhost)
iex(node3@localhost)2> Node.list()
[:node2@localhost, :node1@localhost] ?
? node3 连接到所有节点。
这在您想要建立一个完全连接的多个节点集群的场景中非常有用。向这样的集群添加新节点相当于与集群中的单个节点建立连接。新节点将自动连接到集群中的所有节点。
要获取集群中所有节点的列表,包括当前节点,可以使用 Node.list/1 :
iex(node1@localhost)3> Node.list([:this, :visible])
[:node1@localhost, :node2@localhost, :node3@localhost]
:this 选项表示您希望当前节点出现在列表中。 :visible 选项表示您还希望获取所有可见节点的列表。可以将节点设置为隐藏,正如我将在本章最后一节中解释的那样。
检测断开节点
节点断开连接值得特别提及。在连接建立后,每个节点定期向所有连接的对等节点发送滴答消息,以检查它们是否仍然存活。所有未能对四个连续的滴答消息作出响应的节点将被视为断开连接,并从连接节点列表中移除。
没有自动尝试重新连接这些节点,但可以使用 Node.monitor/1 函数(https://hexdocs.pm/elixir/Node.xhtml#monitor/2)注册并接收节点断开连接的通知。此外,您可以借助 :net_kernel.monitor_nodes (https://www.erlang.org/doc/man/net_kernel.xhtml#monitor_nodes-1)监控所有节点的连接和断开。我稍后会在讨论网络分区时演示这如何工作。
12.1.2 节点之间的通信
一旦您启动并连接了一些节点,就可以使它们协作。尝试此操作的一个简单方法是使用 Node.spawn/2 ,它接收一个节点名称(一个原子)和一个 lambda。然后,该函数在目标节点上生成一个新进程,并在该进程中运行 lambda。
例如,从 node1 您可以在 node2 上生成一个进程。
清单 12.1 在另一个节点上生成进程
iex(node1@localhost)4> Node.spawn(
:node2@localhost, ?
fn -> IO.puts("Hello from #{node()}") end ?
)
Hello from node2@localhost
? 目标节点
在目标节点上运行
输出证明该 lambda 已在另一个节点上执行。
组长流程
在列表 12.1 中发生了一些意想不到的事情。尽管 lambda 已在 node2 上执行,但输出却在 node1 的 shell 中打印。怎么会这样呢?原因在于 Erlang 如何进行标准 I/O 操作。
所有标准 I/O 调用(例如 IO.puts/1 )都被转发到组领导者——一个负责执行实际输入或输出的进程。一个派生进程从生成它的进程继承组领导者,即使您在另一个节点上生成进程。因此,您的进程可能在 node2 上运行,但其组领导者仍在 node1 上。因此,要打印的字符串是在 node2 上创建的(如字符串内容所证明),但输出是在 node1 上打印的。
另一个重要的原语是向进程发送消息的能力,无论它们的位置如何。这个特性也被称为位置透明性。发送操作始终以相同的方式工作,无论目标进程运行在哪个节点上。
让我们看一个简单的例子。从 node1 开始,您将启动一个在 node2 上运行的计算,然后将结果发送回 node1 :
iex(node1@localhost)5> caller = self()
iex(node1@localhost)6> Node.spawn(
:node2@localhost,
fn -> send(caller, {:response, 1+2}) end ?
)
iex(node1@localhost)7> flush()
{:response, 3} ?
? 将响应发送回呼叫者
接收方收到响应。
这个例子清楚地类似于标准的进程使用。您在远程节点上生成一个进程,然后从生成的进程中将消息发送回 caller 。注意 caller 变量是如何使用的。尽管 lambda 在另一个节点上运行,闭包机制仍然有效。
最后,您使用 iex shell 的 flush 助手,该助手从当前进程邮箱中获取所有消息并将其打印到控制台。这证明消息已在调用节点上接收。
消息发送没有限制。任何在一个 BEAM 实例中有效的内容都可以在不同的实例中使用(有一个小的注意事项,在侧边栏中描述了 lambda)。当目标进程在另一个节点上时,消息使用 :erlang.term_to_binary/1 编码,并在目标节点上使用 :erlang.binary_to_term/1 解码。
避免生成 lambda 或将其发送到不同的节点
您可以从您的 shell 中生成 lambda,这是一种特殊情况,因为 shell 定义的 lambda 嵌入了它们自己的代码,并在每次调用时动态解释。相比之下,只有当两个节点都由完全相同的编译代码提供支持时,才能远程生成(或通过消息发送到远程节点)在模块函数中定义的 lambda。如果您开始运行一个多节点集群,然后需要更新代码,这些要求很难满足。您无法同时升级集群中的所有节点,因此在某些时候,节点上的代码将会不同。
因此,通常最好避免将 lambda 传递给远程节点。相反,您应该使用 Node.spawn/4 函数,该函数接受一个模块、函数和参数(MFA)列表,以识别要在目标节点上调用的函数。只要模块在目标节点上存在并导出相应的函数,这样使用是安全的。
在多节点环境中,“本地注册”这个术语终于开始有意义。当您在本地注册一个进程时,注册的范围仅限于当前节点。这意味着您可以在不同的节点上使用相同的注册名称(但每个节点上只能使用一次)。例如,为 node1 和 node2 注册 shell 进程:
iex(node1@localhost)8> Process.register(self(), :shell)
true
iex(node2@localhost)3> Process.register(self(), :shell)
true
拨打 send(:shell, some_message) 将根据您调用 send 的节点,将消息发送到 node1 或 node2 。
可以通过使用 {some_alias, some_node} 来引用另一个节点上本地注册的进程。例如,要从 node1 发送消息到 node2 shell,可以这样做:
iex(node1@localhost)9> send(
{:shell, :node2@localhost}, ?
"Hello from node1!"
)
? 识别在另一个节点上注册的过程
然后,在 node2 ,您可以验证消息是否已接收:
iex(node2@localhost)4> flush()
"Hello from node1!"
您还可以在发出 GenServer 请求(转换和调用)时使用 {some_alias, some_node} 形式。最后,有两个特殊函数 GenServer.abcast/3 和 GenServer.multi_call/4 ,允许您向给定节点上所有本地注册的进程发出请求。
12.1.3 过程发现
过程发现是集群中非常重要的操作,但在无集群模式下也使用相同的操作。事实上,无论是否为分布式系统,进程通信的典型模式始终是相同的:
- 客户端进程必须获取服务器的 PID。
- 客户端向服务器发送消息。
在第一步中,您发现了一个过程。您在第 9 章中使用了 Registry 模块的一种发现形式。
即使在单节点系统中,您仍然必须以某种方式找到目标进程的 PID。这在分布式环境中并没有改变,但您必须使用其他发现方式,因为 Registry 不支持集群,只在本地节点的范围内工作。
全球注册
进行集群范围发现的最简单方法是使用 :global 模块 (https://www.erlang.org/doc/man/global.xhtml),该模块提供了全局名称注册功能。例如,如果您将待办事项系统作为多节点集群运行,您可能希望每个待办事项列表运行一个进程(当然,除非您追求冗余)。全局名称注册使您能够实现这一点。
作为一个例子,您可以注册 node1 shell 进程作为处理 Bob 待办事项列表的进程:
iex(node1@localhost)10> :global.register_name({:todo_list, "bob"}, self())
:yes
结果 ( :yes ) 表示全局注册成功。当前进程的全局(集群范围)别名现在是 {:todo_list, "bob"} 。
此时,集群中所有节点上的所有进程都可以找到注册在此别名下的进程。尝试在同一别名下全局注册 node2 shell 进程将失败:
iex(node2@localhost)5> :global.register_name({:todo_list, "bob"}, self())
:no
全球注册的工作原理
全球注册没有特别的魔法。它是用纯 Erlang 实现的,你可以在 Elixir 中自己重新实现。它只是一个复杂的、多节点感知的进程注册表版本。
当您尝试注册一个全局别名时,会设置一个集群范围的锁,防止其他节点进行竞争注册。然后,进行检查以查看该别名是否已经注册。如果没有,则所有节点都会被告知新的注册。最后,锁被释放。显然,这涉及大量的通信,并且在节点之间传递了几条小消息。
您可以使用 :global.whereis_name/1 来查找该过程:
iex(node2@localhost)6> :global.whereis_name({:todo_list, "bob"})
#PID<7954.90.0>
请注意,查找是本地的。当进行注册时,所有节点都会被联系,并将注册信息缓存到它们的本地 ETS 表中。之后在任何节点上的每次查找都是在该节点上进行的,无需额外的通信。这意味着查找可以快速执行,而注册则需要节点之间的通信。
看看这个 PID 的形状: #PID<7954.90.0> 。PID 字符串表示中的第一个数字不是 0,这表明您正在处理来自其他节点的进程。
识别远程进程
现在应该很明显,PID 同时标识本地和远程进程。在几乎所有情况下,您不需要担心进程的物理位置。但您应该了解一些与网络相关的 PID 细节。
您到目前为止看到的所有 PID 都有类似的形式: <0.X.0> ,其中 X 是一个正整数。在内部,每个进程都有一个节点范围内唯一的标识符。这个标识符可以在字符串表示的最后两个数字中看到。如果您在单个节点上创建足够的进程,第三个数字也将大于零。
第一个数字表示节点编号——一个内部标识符,指示进程运行的节点。当这个数字为零时,进程来自本地节点。相反,当输出包含形式为 <X.Y.Z> 的 PID 且 X 不为零时,可以确定这是一个远程进程。要以编程方式确定进程运行的节点,可以使用 Kernel.node/1 (https://hexdocs.pm/elixir/Kernel.xhtml#node/1)。
全球注册允许您将所有需要操作相同资源(在本例中为待办事项列表)的请求转发到集群中的单个同步点(一个进程)。这与您在单节点设置中使用的模式完全相同,现在应用于节点集群。稍后当您开始使待办系统分布式时,您将看到这一点的实际应用。
全球注册也可以与 GenServer 一起使用,如以下代码片段所示:
GenServer.start_link(
__MODULE__,
arg,
name: {:global, some_global_alias} ?
)
GenServer.call({:global, some_global_alias}, ...) ?
? 在全局别名下注册进程
? 可以使用全局别名来发起请求。
最后,如果注册的进程崩溃或拥有者节点断开连接,别名将在所有其他机器上自动注销。
进程组
另一个常见的发现模式发生在您想要将多个进程注册在同一个别名下时。这听起来可能很奇怪,但在您想要对集群中的进程进行分类并向某个类别中的所有进程广播消息的情况下,这是非常有用的。
例如,在冗余集群中,您希望保留相同数据的多个副本。拥有多个副本可以让您在节点崩溃时生存。如果一个节点终止,集群中应该有其他地方存在一个副本。
对于这个问题,您可以使用 :pg (进程组)模块(https://www.erlang.org/doc/man/pg.xhtml)。该模块允许您创建任意命名的集群范围组并将多个进程添加到这些组中。此添加会在所有节点之间传播,稍后,您可以查询该组并获取属于它的所有进程的列表。
让我们试试这个。您将设置 node1 和 node2 的两个 shell 进程来处理 Bob 的待办事项列表。为此,您需要将这两个进程添加到该组中。
启动第一个节点,并通过调用 :pg.start_link/0 确保 :pg 正在运行:
iex(node1@localhost)1> :pg.start_link()
启动第二个节点,将其连接到第一个节点,并启动 :pg :
iex(node2@localhost)1> Node.connect(:node1@localhost)
iex(node2@localhost)2> :pg.start_link()
您可以使用函数 :pg.join/2 将两个 shell 进程添加到一个组中:
iex(node1@localhost)2> :pg.join({:todo_list, "bob"}, self())
:ok
iex(node2@localhost)3> :pg.join({:todo_list, "bob"}, self())
:ok
第一个参数是一个任意的术语,用于唯一标识该组。第二个参数是要添加到该组中的进程的 PID。
在这一点上,这两个进程都在进程组中,并且两个节点都可以看到这一点:
iex(node1@localhost)3> :pg.get_members({:todo_list, "bob"})
[#PID<8531.90.0>, #PID<0.90.0>]
iex(node2@localhost)4> :pg.get_members({:todo_list, "bob"})
[#PID<0.90.0>, #PID<7954.90.0>]
您如何使用此技术?当您想要更新 Bob 的待办事项列表时,您可以查询相应的进程组并获取负责 Bob 列表的所有进程的列表。然后,您可以向所有进程发出请求(例如,通过使用 GenServer.multi_call/4 )。这确保集群中的所有副本都得到更新。
但是当您需要发出查询(例如,检索待办事项列表条目)时,您可以在组中的单个进程上执行此操作(无需对所有副本执行多个查询,除非您想要更好的信心)。因此,您可以从进程组中选择一个单一的 PID。
就像 :global 模块一样, :pg 是用纯 Erlang 实现的,也是一个复杂的进程注册表版本。组的创建和加入在集群中传播,但查找是在本地缓存的 ETS 表上进行的。进程崩溃和节点断开连接会被自动检测,且不存在的进程会从组中移除。
12.1.4 链接和监视器
链接和监视器即使在不同节点上运行的进程也能正常工作。如果发生以下任何事件,进程将收到退出信号或 :DOWN 通知消息(在监视器的情况下):
- 链接或监控进程崩溃
- BEAM 实例或运行链接或监控进程的整个机器崩溃
- 网络连接丢失
让我们快速证明这一点。你将启动两个节点,连接它们,并从 node1 的 shell 设置一个监视器到 node2 的 shell:
$ iex --sname node1@localhost
$ iex --sname node2@localhost
iex(node2@localhost)1> Node.connect(:node1@localhost)
iex(node2@localhost)2> :global.register_name({:todo_list, "bob"}, self())
iex(node1@localhost)1> Process.monitor( ?
:global.whereis_name({:todo_list, "bob"})
)
? 监控另一个节点上的过程
现在,您可以终止 node2 并刷新 node1 中的消息:
iex(node1@localhost)2> flush()
{:DOWN, #Reference<0.0.0.99>, :process, #PID<7954.90.0>, :noconnection}
如您所见,您收到了一条通知,监控的进程不再运行。这使您能够检测分布式系统中的错误并从中恢复。实际上,错误检测机制的工作方式与并发系统相同,这并不令人惊讶,因为并发也是一种分布原语。
12.1.5 其他分发服务
其他有趣的服务作为 Erlang 标准库的一部分提供。我将在这里简要提及它们,但一旦你开始编写分布式系统,绝对应该花时间研究它们。
我已经提到过许多基本原语可以在 Node 模块中找到 ( https://hexdocs.pm/elixir/Node.xhtml)。此外,您还可以在 :net_kernel ( https://www.erlang.org/doc/man/net_kernel.xhtml) 和 :net_adm ( https://www.erlang.org/doc/man/net_adm.xhtml) 模块中找到一些有用的服务。
偶尔,您需要在其他节点上发出函数调用。正如您所看到的,这可以通过 Node.spawn 来完成,但这是一种低级方法,通常不适合。 Node.spawn 的问题在于它是一种“发出后不再关注”的操作,因此您对其结果一无所知。
更常见的是,您可能希望获取远程函数调用的结果或在多个节点上调用一个函数并收集所有结果。在这种情况下,您可以参考 :rpc Erlang 模块 (https://erlang.org/doc/man/rpc.xhtml),该模块提供了各种有用的辅助工具。
例如,要在另一个节点上调用一个函数并获取其结果,可以使用 :rpc.call/4 ,它接受一个节点和一个 MFA 来标识要远程调用的函数。以下是一个在 node2 上远程调用 Kernel.abs(-1) 的示例:
iex(node1@localhost)1> :rpc.call(:node2@localhost, Kernel, :abs, [-1])
1
在 :rpc 模块中包含的其他有用助手允许您在集群中的多个节点上发出远程函数调用。稍后当您向数据库添加复制功能时,您将看到这一点的实际应用。
消息传递是核心分布式原语
许多服务,例如 :rpc ,都是用纯 Erlang 实现的。就像 :global 和 :pg 一样, :rpc 依赖于透明的消息传递和向远程节点上本地注册的进程发送消息的能力。例如, :rpc 依赖于本地注册的 :rex 进程的存在(该进程在 Erlang 的 :kernel 应用启动时启动)。在其他节点上进行 RPC 调用相当于向目标节点上的 :rex 进程发送包含 MFA 的消息,从这些服务器调用 apply/3 并发送回响应。
如果您想深入了解 Erlang 系统上的分布式编程,我建议花一些时间研究 rpc.erl 、 pg.erl 和 global.erl 的代码,以了解各种分布式习惯用法和模式。
我还想提到集群范围的锁。这些锁在 :global 模块中实现,它们允许您获取一个任意命名的锁。一旦您获得了特定的锁,集群中的其他进程在您释放之前无法获取它。
让我们看看这个如何运作。启动 node1 和 node2 并将它们连接起来。然后,在 node1 上,尝试使用 :global.set_lock/1 获取锁:
iex(node1@localhost)1> :global.set_lock({:some_resource, self()})
true
您提供的元组由资源 ID 和请求者 ID 组成。资源 ID 是一个任意术语,而请求者 ID 标识一个唯一的请求者。两个不同的请求者不能在集群中获取相同的锁。通常,您会希望使用进程 ID 作为请求者 ID,这意味着在任何时候,最多只有一个进程可以获取该锁。
获取锁涉及与集群中的其他节点进行通信。一旦 :set_lock 返回,您就知道您已经获得了锁,集群中的其他人无法获取它。尝试在 node2 上获取锁:
iex(node2@localhost)1> :global.set_lock({:some_resource, self()}) ?
? 在锁释放之前阻塞
在 node2 上的 shell 进程将无限期等待(这可以通过额外参数进行配置),直到锁变得可用。只要您在 node1 上释放锁,它将在 node2 上被获取:
iex(node1@localhost)2> :global.del_lock({:some_resource, self()})
iex(node2@localhost)2> ?
? 锁现在由节点 2 上的外壳进程持有。
还有一个简单的助手可用于获取-释放模式,形式为 :global.trans/2 ( https://erlang.org/doc/man/global.xhtml#trans-2),它获取锁;运行提供的 lambda;最后,释放锁。
锁定通常是您应该避免的,因为它会导致与经典同步方法相同类型的问题。过度依赖锁会增加死锁、活锁或饥饿的可能性。一般来说,您应该通过进程进行同步,因为这样更容易推理系统。
但如果谨慎使用,锁有时可以提高性能。请记住,消息传递是有相关成本的;在分布式系统中尤其如此,因为消息必须被序列化并通过网络传输。如果消息非常大,这可能会引入显著的延迟并影响系统性能。
锁在这里可以提供帮助,因为它们允许您在不同节点上同步多个进程,而无需向另一个进程发送大量消息。以下是这个想法的一个草图。假设您需要确保在整个集群中对大量数据的处理是串行的(在任何时刻,整个集群中最多只能有一个进程在运行)。通常,这通过将数据传递给一个作为同步点的进程来完成。但是,传递大量数据可能会引入性能损失,因为数据必须被复制并通过网络传输。为了避免这种情况,您可以使用锁来同步不同的进程,然后在调用者上下文中处理数据:
def process(large_data) do
:global.trans( ?
{:some_resource, self},
fn ->
do_something_with(large_data) ?
end
)
end
获取集群范围的锁
在调用者进程中运行
调用 :global.trans/2 确保集群范围内的隔离。在任何时刻,集群中最多只能有一个进程在 :some_resource 上运行 do_something_with/1 。由于 do_something_with/1 在调用进程中运行,您避免了向另一个同步进程发送大量消息。调用 :global.trans/2 会在节点之间引入额外的通信,但用于获取锁的消息要比将 large_data 的内容传递给另一个节点上的另一个进程小得多,因此您节省了带宽。
这结束了我们对分发基础知识的讨论。我没有提到一些重要的方面,这些方面在你开始使用网络作为消息传递的通信渠道时会出现。我们将在本章的最后一节重新讨论这个主题。现在,让我们专注于使待办事项系统更加分布式。
相关推荐
- 前端面试:聊聊 meta 标签?(meta标签用法)
-
提供给页面的一些元信息(名称/值对),有助于SEO。Meta标签是HTML中用于定义文档类型声明的标签。它们通常被用在head标签中,与文档的body标签相关联。在一个有head...
- web网页性能分析系列(网页性能指标有哪些)
-
在前端开发中,App或者WebPage性能的好坏和响应速度,尤其是App端显得格外重要,一直都是前端很头疼的问题。专业的测试工具可以知道自己的网页还有哪些需要优化的地方,总体的评分是多少,是否合乎用...
- [抓狂瞬间] 5 大差异 + 布局绝招!前端元素面试通关秘籍
-
刚入行的搬砖工程师,面对面试必问题“行内元素和块级元素有啥区别”,是不是心跳加速,大脑却一片空白?别担心!这看似基础的问题,实则是面试逆袭的突破口。今天就带你深入剖析,用5个关键差异,解锁前端布...
- python中Django视图(view)的详解(附示例)
-
本篇文章给大家带来的内容是关于python中Django视图(view)的详解(附示例),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。一个视图函数(类),简称视图,是一个简单的Pyt...
- MySQL进行整库数据备份「表(结构+数据)、视图、函数、事件」
-
前言 通常情况下,我们需要改什么地方就备份什么地方就可以了,但也免不了需要整库备份的时候,本文记录实现MySQL使用脚本进行整库数据备份【表(结构+数据)、视图、函数、事件】 主要是使用mys...
- python入门-day14-周末小项目(python周末培训班哪个好)
-
周末小项目-简易记事本的内容。这是一个综合练习,结合之前学过的函数、文件操作和异常处理,设计一个简单的命令行记事本程序。我会用清晰的步骤和代码带你实现添加、查看、删除笔记并保存到文件的功能,确保...
- Python必会的50个代码操作(python代码介绍)
-
学习Python时,掌握一些常用的程序操作非常重要。以下是50个Python必会的程序操作,主要包括基础语法、数据结构、函数和文件操作等。1.HelloWorldprint("Hello,...
- ScalersTalk成长会Python小组第7周学习笔记
-
Scalers点评:在2015年,ScalersTalk成长会完成Python小组完成了《Python核心编程》第1轮的学习。到2016年,我们开始第二轮的学习,并且将重点放在章节的习题上。Pytho...
- 电脑CMD命令与电脑工作效率提升(cmd使用提升命令)
-
在日常使用电脑同时按“windows+R”,可以弹出来一个框然后输入CMD弹出以下框:在这个框中输入一些内容可以简化一些我们的电脑操作好的,以下是一些最常用的CMD命令,按功能分类整理,适合日常使用和...
- 多线程实现消息推送并可重试3次以及1小时后重试
-
#-*-coding:utf-8-*-"""CreatedonTueApr2209:05:462025@author:1""...
- RBAC权限模型(rbac权限模型的优点)
-
RBAC权限模型RBAC权限模型(Role-BasedAccessControl)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。准备工作CREATETABLE`s...
- 如何使用PIL生成验证码?(pixivic验证码)
-
web项目中遇到使用验证码的情况有很多,进行介绍下使用PIL生成验证码的方法。安装开始安装PIL的过程确实麻烦各种问题层出不绝,不过不断深入后就没有这方面的困扰了:windows安装:直接安装Pil...
- 技术是这样应用的(一)(技术的运用)
-
WindowsServer2003路由与远程访问在计算机网络教室使用过程中网络访问控制的实现目前很多计算机网络教室采用双网卡服务器的路由和远程访问功能,通过NAT地址转换实现教室内学生机的互联网访...
- Flask-RESTful 用法指南(flask写restful接口)
-
Flask-RESTful是一个Flask扩展,用于快速构建RESTfulAPI。它提供了简单的语法来创建资源路由,并内置了请求解析和响应格式化功能。##安装首先安装Flask-REST...
- Python办公自动化系列篇之三:PowerPoint演示文稿(.pptx)
-
作为高效办公自动化领域的主流编程语言,Python凭借其优雅的语法结构、完善的技术生态及成熟的第三方工具库集合,已成为企业数字化转型过程中提升运营效率的理想选择。该语言在结构化数据处理、自动化文档生成...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- HTML 参考手册 (28)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML常用标签 (29)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)