构建 Web 服务器与应用配置
1. 构建 Web 服务器
1.1 代码实现
以下是处理
entries
请求的代码:
entries
|> Enum.map(&"#{&1.date} #{&1.title}")
|> Enum.join("\n")
conn
|> Plug.Conn.put_resp_content_type("text/plain")
|> Plug.Conn.send_resp(200, formatted_entries)
end
...
end
此代码与
add_entry
的处理方式类似。由于
entries
是
GET
请求,所以使用
get
宏而非
post
。在这个实现中,会先解码参数,然后获取所需的条目,接着生成要发送给客户端的文本表示并返回响应。
1.2 代码组织优势
代码组织将 HTTP 服务器视为系统核心的接口。在两个请求中,都会将输入转换为特定领域的类型,然后调用与 HTTP 无关的代码(如
Todo.Server.add_entry
)来执行操作。系统核心并不知道也不关心它是在 HTTP 请求的上下文中运行。这种关注点分离使得代码更易于使用、测试和扩展。
1.3 测试系统
可以使用
iex -S mix
启动系统并发出请求。例如,使用命令行
curl
工具:
$ curl -d "" \
"http://localhost:5454/add_entry?list=bob&date=2023-12-19&title=Dentist"
OK
$ curl "http://localhost:5454/entries?list=bob&date=2023-12-19"
2023-12-19 Dentist
1.4 系统工作原理
1.4.1 HTTP 服务器工作方式
每个连接由不同的进程管理,不同的请求在不同的进程中处理。底层的 Cowboy Web 服务器使用一个进程监听端口,为每个传入请求生成一个单独的进程。
graph LR
A[TCP/HTTP 监听器] -->|监听端口| B[请求处理器]
B -->|生成进程| C[处理请求 1]
B -->|生成进程| D[处理请求 2]
1.4.2 进程优势
- 并发:CPU 资源得到最大利用,系统可扩展。
- 轻量级:可以轻松管理多个并发连接。
- 抢占式调度:偶尔的长时间运行、CPU 密集型请求不会使整个系统瘫痪。
- 进程隔离:单个请求的崩溃不会影响系统的其余部分。
1.4.3 请求处理同步性
独立的请求不会相互阻塞,而对同一待办事项列表的多个请求会同步处理。这是因为待办事项缓存的工作方式,对某个列表的所有请求都通过负责该列表的同一进程处理。
graph LR
A[请求 Alice 的列表] --> B[Todo.Server for Alice]
C[请求 Bob 的列表] --> D[Todo.Server for Bob]
E[请求 Bob 的列表] --> D
D -->|同步点| D
1.5 系统性能
使用
wrk
工具进行 30 秒的负载测试,系统每秒吞吐量约为 40,000 个请求,平均延迟为 1 毫秒,99% 分位数延迟为 7 毫秒。测试期间所有 CPU 核心都处于忙碌状态,证明系统没有瓶颈,具有高度并发性,能够充分利用所有核心。不过,实现中存在一些可能的问题:
- 每次修改列表时都存储整个列表。
- 待办事项列表中的基于日期的查找会遍历整个列表。
- 每个待办事项服务器查找都要经过待办事项缓存动态监管器。
1.6 调用与发送消息
1.6.1 区别
- Casts :即发即忘的请求,调用者发送消息后立即继续执行其他操作,无法得知请求的结果。
- Calls :阻塞请求,调用者等待服务器响应。
1.6.2 影响
在系统中,除了需要返回响应的情况,都选择使用
casts
。但
casts
有缺点,可能会给最终用户提供错误的响应。使用
calls
更一致,但系统的响应能力会降低,因为整个系统依赖于数据库工作进程的吞吐量。可以引入中间进程来解决这个问题,先提供请求已排队的即时响应,然后尽力处理请求,并发送请求状态的后续通知。
2. 应用配置
2.1 应用环境
OTP 应用程序可以使用应用环境进行配置,这是一个键值对的内存存储,键是原子,值是 Elixir 术语。可以通过配置脚本文件提供应用环境值,
mix
工具会在应用程序启动前将配置加载到应用环境中,然后可以使用
Application
模块的函数检索环境值。
2.1.1 配置 HTTP 端口示例
以下是在
config/runtime.exs
中配置 HTTP 端口的代码:
import Config
http_port = System.get_env("TODO_HTTP_PORT", "5454")
config :todo, http_port: String.to_integer(http_port)
可以使用
Application.fetch_env!/2
函数检索配置的值:
$ iex -S mix
iex(1)> Application.fetch_env!(:todo, :http_port)
5454
$ TODO_HTTP_PORT=1337 iex -S mix
iex(1)> Application.fetch_env!(:todo, :http_port)
1337
然后可以修改
Todo.Web
的代码以从应用环境中读取端口:
defmodule Todo.Web do
...
def child_spec(_arg) do
Plug.Cowboy.child_spec(
scheme: :http,
options: [port: Application.fetch_env!(:todo, :http_port)],
plug: __MODULE__
)
end
...
end
2.2 不同环境配置
在不同的 Mix 环境中可能需要使用不同的设置。例如,开发和测试环境可以使用不同的 HTTP 端口。可以修改
config/runtime.exs
来实现:
import Config
http_port =
if config_env() != :test,
do: System.get_env("TODO_HTTP_PORT", "5454"),
else: System.get_env("TODO_TEST_HTTP_PORT", "5455")
config :todo, http_port: String.to_integer(http_port)
验证配置是否生效:
$ iex -S mix
iex(1)> Application.fetch_env!(:todo, :http_port)
5454
$ MIX_ENV=test iex -S mix
iex(1)> Application.fetch_env!(:todo, :http_port)
5455
同样,可以使数据库文件夹的配置在不同环境中不同,以避免测试数据污染开发数据。
2.3 配置脚本注意事项
-
build-time 配置脚本
:
config/config.exs在项目编译前执行,如果在不同机器上构建和运行系统,可能会导致配置不当,应尽量少用。 - 库开发 :开发库时应避免通过应用环境获取参数,应设计 API 接受所有参数作为函数参数,这样库用户可以更灵活地提供参数。
3. 构建分布式系统
3.1 分布式系统优势
单个机器存在单点故障,而使用多台机器组成的集群可以提高系统的可靠性。当需求增加时,可以通过添加更多机器来实现水平扩展。
3.2 分布式原语
Elixir 和 Erlang 提供了简单而强大的分布式原语,核心工具是进程和消息。可以向另一个进程发送消息,无论该进程是在同一 BEAM 实例中运行还是在远程机器上的另一个实例中运行。
3.3 与传统 RPC 区别
与传统的 RPC 方法不同,Erlang 和 Elixir 的分布式特性在早期就体现出来。一个典型的并发系统运行多个进程时就可以被视为分布式系统。进程相互独立运行,向另一个本地进程发出请求可以被视为远程调用,消息传递与远程网络通信有很多共同之处。在基本版本中,发送消息后无法得知其结果,也不能确定消息是否能到达目标。如果需要更强的保证,可以设计协议让目标发送响应。同时,需要考虑消息传递的成本,这有时会影响多个进程之间通信协议的设计。
3.4 分布式系统的网络考虑
3.4.1 网络延迟
在分布式系统中,网络延迟是一个不可忽视的问题。由于消息需要在不同的节点之间传输,网络延迟会导致消息的传递时间增加,从而影响系统的响应速度。例如,当一个客户端向远程节点发送请求时,如果网络延迟较高,客户端可能需要等待较长时间才能收到响应。
| 延迟类型 | 描述 | 影响 |
| ---- | ---- | ---- |
| 固定延迟 | 由网络拓扑和传输介质决定的基本延迟 | 增加请求响应时间 |
| 可变延迟 | 受网络拥塞、带宽限制等因素影响的延迟 | 导致响应时间不稳定 |
3.4.2 网络分区
网络分区是指由于网络故障或其他原因,导致部分节点之间无法通信的情况。在分布式系统中,网络分区可能会导致数据不一致和服务不可用。例如,当一个集群中的部分节点与其他节点失去连接时,这些节点可能会继续独立运行,从而导致数据的不一致。
graph LR
A[节点 1] -->|网络正常| B[节点 2]
C[节点 3] -->|网络正常| D[节点 4]
A -.->|网络分区| C
3.4.3 解决方案
为了应对网络延迟和网络分区问题,可以采取以下措施:
-
缓存机制
:在本地节点缓存经常访问的数据,减少对远程节点的请求,从而降低网络延迟的影响。
-
数据复制
:将数据复制到多个节点,当发生网络分区时,每个分区的节点仍然可以访问部分数据,保证服务的可用性。
-
一致性协议
:使用一致性协议(如 Paxos、Raft 等)来保证数据在不同节点之间的一致性,即使发生网络分区也能尽量减少数据不一致的情况。
3.5 构建容错集群
3.5.1 容错的重要性
在分布式系统中,节点故障是不可避免的。构建容错集群可以确保系统在部分节点故障的情况下仍然能够继续提供服务。例如,当一个节点崩溃时,其他节点可以接管其工作,保证系统的正常运行。
3.5.2 实现方法
- 节点冗余 :在集群中部署多个节点,当某个节点出现故障时,其他节点可以继续提供服务。
- 健康检查 :定期检查节点的健康状态,及时发现并处理故障节点。
- 自动恢复 :当节点出现故障时,系统能够自动进行恢复操作,如重启节点、重新分配任务等。
graph LR
A[节点 1] -->|健康检查| B{节点是否健康?}
B -->|是| C[继续服务]
B -->|否| D[自动恢复]
D -->|恢复成功| C
D -->|恢复失败| E[标记为故障节点]
3.6 分布式系统的监控与管理
3.6.1 监控指标
为了确保分布式系统的正常运行,需要监控一些关键指标,如节点的 CPU 使用率、内存使用率、网络带宽、请求响应时间等。通过监控这些指标,可以及时发现系统的性能问题和故障隐患。
| 监控指标 | 描述 | 作用 |
| ---- | ---- | ---- |
| CPU 使用率 | 节点 CPU 的使用情况 | 评估节点的计算能力 |
| 内存使用率 | 节点内存的使用情况 | 检测内存泄漏和资源耗尽问题 |
| 网络带宽 | 节点的网络传输速率 | 评估网络性能 |
| 请求响应时间 | 客户端请求的响应时间 | 评估系统的响应速度 |
3.6.2 管理工具
可以使用一些管理工具来监控和管理分布式系统,如 Prometheus、Grafana 等。Prometheus 是一个开源的监控系统,用于收集和存储时间序列数据;Grafana 是一个可视化工具,用于展示监控数据。通过这些工具,可以直观地了解系统的运行状态,及时发现并解决问题。
总结
- 构建 Web 服务器时,采用进程处理请求可以提高系统的并发性能和可靠性,同时要注意实现中的性能问题和调用方式的选择。
- 应用配置可以通过应用环境实现,不同的 Mix 环境可以使用不同的配置,同时要注意配置脚本的使用和库开发的参数传递方式。
- 构建分布式系统可以提高系统的可靠性和可扩展性,但需要考虑网络延迟、网络分区等问题,通过节点冗余、健康检查等方法构建容错集群,并使用监控工具确保系统的正常运行。
超级会员免费看

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



