Elixir 开发:同步异步、进程监控与分布式系统解析
1. 同步与异步方法选择
在系统设计中,同步与异步方法的选择至关重要。建议先使用同步形式,对系统和进程进行分析。同步方法会在系统中内置反压机制,这有助于防止系统崩溃,因为它能对输入源施加压力。若不采取措施,可能会导致灾难性故障、队列溢出和系统资源争用。
另一种方法是使用异步行为,但当发送的消息过多时,要有将 API 函数切换为同步行为的机制,Elixir 内置的日志模块就采用了这种方法。在系统中构建反压或负载均衡机制,对构建真正可扩展的应用程序非常有益。
2. 进程监控与监督树
进程树在 Elixir/Erlang 中很常见,与 GNU/Linux 和 Unix 系统的进程树类似。不过,OTP 通过监督树将进程树提升到了新的层次。监督树与进程树概念略有不同,进程树仅描述进程间的父子关系,而监督树描述的是运行进程的父子监督类以及重启死亡子进程的策略。
以下是监督树的基础结构示例:
S
S
W
W
W
W
其中,S 表示监督者,W 表示工作者。
3. 重启策略
OTP 定义了四种重启策略:
-
一对一(One for one)
:重启死亡的进程。
Supervisor:
one-for-one:
S
W
W
S
W
W
S
W
W
- 一对多(One for all) :当单个进程失败时,重启所有进程。
Supervisor:
one-for-all:
S
W
W
W
S
W
W
W
S
W
W
W
S
W
W
W
- 剩余对一(Rest for one) :从失败的进程到最后一个进程,重启所有子进程。
Supervisor:
Rest-for-one:
S
W
W
W
S
W
W
W
S
W
W
W
S
W
W
W
- 简单一对一(Simple one for one) :是最复杂的策略,被监督的进程通常相同,不一定在监督者启动时静态启动。它将子进程存储为字典,处理大量子进程时速度更快。
4. 重启选项
子进程或工作进程有三种重启选项:
| 选项 | 说明 |
| ---- | ---- |
| :permanent | 进程总是会被重启,即使正常关闭 |
| :temporary | 进程永远不会被重启,适用于连接池等场景 |
| :transient | 仅当进程异常退出时才会重启 |
5. 创建监督者
在 Elixir 中创建监督者与创建其他 OTP 行为类似。以下是为之前的 KV 进程创建简单监督者的示例:
defmodule KV.Supervisor do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, :ok)
end
def init(:ok) do
children = [worker(KV, [])]
supervise(children, strategy: :one_for_one)
end
end
在 IEx 会话中加载模块并测试:
iex(1)> import_file "kv.exs"
...
iex(2)> {:ok, sup} = KV.Supervisor.start_link
{:ok, #PID<0.71.0>}
iex(3)> Supervisor.which_children(sup)
[{KV, #PID<0.72.0>, :worker, [KV]}]
iex(4)> KV.put(:a, 42)
:ok
iex(5)> KV.get(:a)
42
iex(6)> Process.whereis(KV) |> Process.exit :normal
true
iex(7)> KV.get(:a)
nil
可以看到,即使 KV 进程退出,也会被 KV.Supervisor 重启。
6. 快速失败原则
使用进程监督而非异常处理的好处在于,异常处理试图通过捕获错误并使用 try-catch 块传播来保持进程或线程存活,但这可能无法将错误提升到适当的级别,还可能使进程或上下文处于糟糕状态。而快速失败和使用进程监督可以避免这种问题,若进程因任何原因失败,直接重启它,并在监督者和工作者周围添加良好的日志记录,使错误传播更简单。
7. 监督者设计
设计使用监督者的应用程序时,关键在于考虑进程依赖关系。例如:
- 若一个进程依赖另一个进程,后者应在监督树中处于更高位置。
- 若一个进程依赖整个进程树,应先定义该子树。
- 若两个进程相互依赖,可将它们隔离到自己的监督树中,并使用 :one_for_all 策略。
8. OTP 进程初始化假设
启动 OTP 进程时,明确所做的假设很重要。例如,若 GenServer 进程在初始化时调用数据库,就假设该数据库在进程启动时始终可用。若数据库并非总是可用,定义的 OTP 进程假设将失败,可能导致整个应用程序因重启次数过多而失败。因此,必须事先了解 OTP 进程初始化和启动期间可用的资源。
9. 分布式计算的谬误
在分布式计算中,存在一些常见的错误假设:
-
网络可靠
:网络实际上不可靠,可能因各种原因(如线路切断、停电、硬件故障等)而失败。假设远程资源始终可用会导致不确定的失败。
-
无延迟
:即使在同一交换机上,延迟也可能成为问题。看似良好的分布式系统可能会让人忘记网络调用需要时间。
-
带宽无限
:网络速度有限,忘记数据包大小会导致时间成本增加或应用程序失败。
-
网络安全
:假设网络安全是错误的,OTP 在这方面考虑不足,需要开发者自行在应用程序中加入安全机制。
-
拓扑不变
:硬件和服务器会发生变化,假设拓扑始终相同会导致应用程序出现不可达资源错误。
-
只有一个管理员
:互联网由多个管理员操作,若应用程序依赖第三方远程资源,可能会因未考虑其他应用程序或资源的维护窗口而失败。
-
传输成本为零
:网络传输不仅有时间成本,还有空间成本(即金钱)。
-
网络同质
:网络中的设备和系统不一定相同,假设网络同质会导致问题。OTP 协议是开放的,任何实现该协议的节点都可加入网络,支持 C 互操作性或 C 节点。
10. 应对进程和节点失败
在处理进程和节点失败以及网络不可靠问题时,面临选择:
- 乐观假设节点存活并继续发送消息,希望消息最终能送达,但可能会延迟正常失败并导致灾难性后果。
- 悲观假设节点死亡并继续前进,但当被认为死亡的节点实际上存活时,应用程序需要处理这种情况。
11. CAP 定理
CAP 定理指出,在分布式系统中,一致性(C)、可用性(A)和分区容错性(P)三个特性只能选择两个。
| 系统类型 | 特点 |
|---|---|
| CP 系统 | 能在失败时保持数据一致性,但可能对某些请求不可用 |
| AP 系统 | 能在失败时继续响应请求并提供数据,但数据可能陈旧,一致性无法保证 |
分区容错性对于分布式应用程序至关重要,没有它,构建合理的分布式应用程序将变得极其困难。例如,假设有三个独立节点,分区将前两个节点与第三个节点分开,若系统声称具有一致性和可用性,所有节点都必须保持一致和可用,但这在分区恢复时可能导致一致性无法保证。
12. 放宽定义
CAP 中一致性、可用性和分区容错性的定义非常具体,改变这些术语的定义将使系统脱离 CAP 范畴。放宽定义通常是系统或应用程序开发者的选择,因为 CAP 的严格定义并非总是适用于问题域建模。例如,银行交易在现实世界中常使用 24 小时的不确定一致性模型,最终在一天结束时进行协调。还有一些系统采用最终一致性,允许接受多次写入,最终将这些写入复制到其他节点。
Elixir 开发:同步异步、进程监控与分布式系统解析
13. 分布式系统中的 CAP 选择分析
在分布式系统中,CAP 定理是一个重要的理论基础。它指出在一致性(C)、可用性(A)和分区容错性(P)这三个特性中,只能同时满足两个。下面通过 mermaid 流程图来进一步分析不同选择下的系统行为:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B{选择特性}:::decision
B -->|CP| C(一致性优先系统):::process
B -->|AP| D(可用性优先系统):::process
B -->|CA(理论不可行)| E(无法实现系统):::process
C --> F(数据一致,但可能部分不可用):::process
D --> G(持续响应,但数据可能陈旧):::process
E --> H(面临一致性和可用性冲突):::process
F --> I([结束]):::startend
G --> I
H --> I
从这个流程图可以清晰地看到,选择 CP 系统时,系统会优先保证数据的一致性,可能会在某些情况下牺牲部分可用性;选择 AP 系统时,系统会始终保持可用,但数据的一致性可能无法严格保证;而理论上的 CA 系统,由于网络分区的存在,很难在实际中实现。
14. 分布式系统设计的综合考量
在设计分布式系统时,需要综合考虑多个方面的因素,以下是一个详细的列表:
-
网络因素
:要充分认识到网络的不可靠性、延迟、带宽限制以及安全性问题。在设计时,要采用异步消息传递、设置合理的超时时间、控制消息大小,并自行添加安全机制。
-
进程管理
:利用进程监督机制,根据进程的依赖关系合理设计监督树,选择合适的重启策略和选项,确保进程在出现故障时能够快速恢复。
-
CAP 选择
:由于分区容错性是分布式系统的基础,必须在一致性和可用性之间做出权衡。根据应用的具体需求,选择 CP 或 AP 系统。
-
初始化假设
:明确 OTP 进程初始化时的假设,确保依赖的资源在进程启动时是可用的,避免因资源不可用导致应用程序失败。
15. 分布式系统设计示例
为了更好地理解上述理论,下面给出一个简单的分布式系统设计示例。假设我们要设计一个分布式缓存系统,需要考虑以下几个方面:
进程监督设计 :
defmodule DistributedCache.Supervisor do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, :ok)
end
def init(:ok) do
children = [
worker(DistributedCache.Node, []),
supervisor(DistributedCache.ReplicaSupervisor, [])
]
supervise(children, strategy: :one_for_one)
end
end
在这个示例中,
DistributedCache.Supervisor
监督着
DistributedCache.Node
工作进程和
DistributedCache.ReplicaSupervisor
子监督者。
CAP 选择
:
由于缓存系统通常更注重可用性,我们可以选择 AP 系统。当出现网络分区时,允许缓存节点继续提供服务,即使数据可能是陈旧的。在数据一致性方面,可以采用向量时钟等因果排序方法进行协调。
16. 总结
通过对同步与异步方法、进程监控、监督树、分布式计算谬误以及 CAP 定理等方面的讨论,我们了解到在 Elixir 开发分布式系统时需要考虑的多个关键因素。在实际设计中,要充分认识到网络的复杂性和不确定性,合理运用进程监督机制,根据应用的具体需求在一致性和可用性之间做出权衡。同时,要明确 OTP 进程初始化的假设,避免因资源不可用导致应用程序失败。通过综合考虑这些因素,可以构建出更加健壮、可扩展的分布式系统。
以下是一个总结表格,概括了本文讨论的主要内容:
| 主题 | 要点 |
| ---- | ---- |
| 同步与异步 | 先使用同步形式,必要时切换异步;构建反压或负载均衡机制 |
| 进程监控 | 利用监督树,选择合适的重启策略和选项 |
| 分布式计算谬误 | 避免网络可靠、无延迟等错误假设 |
| CAP 定理 | 必须选择分区容错性,在一致性和可用性之间权衡 |
| 监督者设计 | 考虑进程依赖关系,合理设计监督树 |
| OTP 进程初始化 | 明确启动时的假设,确保资源可用 |
超级会员免费看
1202

被折叠的 条评论
为什么被折叠?



