Erlang OTP 中 IRC 聊天服务器的设计与原型实现

Design and Prototypical Implementation of an IRC Chat Server in Erlang OTP

作者: Annette Grueber、Tom Jaschinski 和 Tobias Winkler

引言

在本文撰写之时,数字服务为企业和日常生活提供关键功能。由于数字化的进步,对数字服务的依赖性正在迅速增长。这一过程不仅体现在相互连接的设备数量不断增长,而且还体现在事件期间服务不可用造成的影响:2021 年 10 月 4 日,Meta(前 Facebook)及其所有组织(例如 Instagram、WhatsApp、Facebook Messenger)宕机长达七个小时。此次宕机给该公司和相关企业造成了巨额利润损失。[1]

这次宕机表明,现代解决方案必须设计得具有弹性,以便在事件期间能够提供服务。有多种解决方案可以开发高可用性和可靠的服务,这些解决方案可以应用于系统架构和设计中的各个层面。一些编程语言专门设计来应对这些挑战。函数式编程语言 Erlang 提供了开发这些弹性服务的内在功能。

因此,本文介绍了基于 Erlang 中的互联网中继聊天 (IRC) 协议的通信服务的示例性设计和开发,以研究其可用性特性。

背景

本章介绍编程语言 Erlang 的基础知识。各小节概述了函数式编程和 Erlang 特有的特性。

函数式编程

函数式编程是一种编程范式。各种特性 مشخص了函数式编程的构成

  • 纯函数: 纯函数是确定性函数,对于相同的输入值,始终产生相同的输出值。因此,可以得出结论,函数不会受到外部的影响。因此,不存在副作用。[2]

  • 不可变性: 不可变性指的是数据不能被更改的事实。一旦变量被赋值,该变量就不能再被重新初始化。因此,重新初始化只能通过引入具有调整值的新变量来实现。由于命令式编程语言中的经典循环需要通过例如递增变量来更新变量,因此函数式编程中没有循环语句。迭代数据需要递归函数调用。[2]

  • 引用透明性: 纯函数的属性以及变量的不可变性导致了引用透明性。这意味着一旦函数的结果可用,它就可以用于相同的输入值。[2]

为了说明上述特性,下图依次显示了数字 n 的阶乘的计算。左侧显示了使用循环的经典命令式方法的算法。右侧表示使用递归函数调用的函数式方法中的算法。

 

asdf
图 1:命令式编程与函数式编程

 

Erlang

Erlang 是一种最初由 Ericsson 开发的函数式编程语言。下面将讨论和介绍原生特性和开放电信平台 (OTP) 库。

原生特性

Erlang 提供了原生特性,例如用于实现高可用服务的 高级指令。示例性特性包括进程监督器、热插拔功能、进程间通信原则以及名为开放电信平台 (OTP) 的框架。OTP 是一个库,可以简化使用 Erlang 开发稳定和并行应用程序的过程。OTP 内的主要组件是所谓的行为,这些行为将应用程序的特定业务逻辑与提供通用功能(例如标准化进程间通信)的通用指令分离开来。例如,客户端、服务器或监督器,将在接下来的章节中进行介绍。[3]

进程

Erlang 最重要的特性之一是其基于 Actor 模型的进程。该模型将并行活动描述为 Actor,Actor 通过消息交换相互通信,并且没有共享内存。Actor 的任务是

  • 接收和处理消息

  • 向其他 Actor 发送消息

  • 启动更多 Actor

  • 更改 Actor 的本地状态

在 Erlang 中,Actor 称为进程。[4] 在下面的插图中,展示了 Erlang 中两个进程 A 和 B 的通信。

 

asd
图 2:进程通信示例

 

插图的左侧部分显示了进程 A 向进程 B 发送消息“hello”。A 必须将其进程标识 ID (Pid_A) 发送给 Actor B,以便 B 知道消息来自何处。此外,A 需要来自 B 的进程标识 (Pid_B) 以将消息寻址到正确的进程。为此,必须在发送消息的进程(在本例中为 A)中调用 spawn 函数,并移交特定参数:进程 B 的模块名称、进程 B 的主函数和相应的函数参数。spawn 函数启动进程 B 并返回其进程标识 ID (pid)。这使得可以使用感叹号将左侧的消息 {self (), hello} 发送到右侧的进程。self () 函数返回进程 A 的 pid。

在右侧,该图显示了如何在进程 B 中实现消息接收,在本例中通过使用 receive 块语句。在 receive 块中,箭头语句的左侧表示接收到的消息的预期语法。如果消息与指定的语法对应,则执行箭头右侧的特定操作。

Erlang 进程在 Erlang 自己的运行时环境中实现,并在其中单独处理。创建这样的进程只需要几个字节的内存,因此计算速度很快。[3]

监督器

监督器是一个进程,它启动、停止和监视其他从属进程,例如其他监督器或工作进程。工作进程是一个在执行应用程序的业务逻辑时被监视的进程,但自身不监视进程。因此,监督器可以用于构建分层进程结构,以构建容错应用程序。[5]

热插拔

热插拔是一个术语,描述系统在运行时交换组件的能力。这种机制通过使其能够在维护期间运行来提高系统的可用性,从而实现低甚至零停机时间。热插拔适用于硬件和软件组件。在运行时交换代码在高度可用的服务环境中(例如电信网络和关键任务系统)非常有用。

Erlang 原生支持热插拔源代码模块,方法是利用一个名为 Erlang 代码服务器的集成组件,该组件存储和管理模块执行和版本控制。[6] 热插拔基于两种类型的 Erlang 函数调用

  • 本地函数调用: 本地函数调用描述从一个模块到同一模块内函数的函数调用,例如 foo()。

  • 完全限定函数调用: 完全限定函数调用描述从一个模块到指定模块的外部函数调用,例如 module_a:foo()。[7]

以下步骤逐步介绍了在 Erlang 中热插拔示例模块的简单方法

  1. Erlang 模块 A 的版本 1 已加载并在代码服务器中运行。 

    sdf
    图 3:热插拔 – 模块“A”版本 1 已执行

     

  2. 系统的操作员决定在运行时更新模块 A,因此将版本 2 加载到代码服务器中。[7]  

    sdef
    图 4:热插拔 – 模块“A”版本 1 和 2 已加载

     

  3. 将版本 2 加载到代码服务器后,版本 1 仍将执行,直到运行中的进程对模块 A 中的函数执行完全限定的函数调用。[7]  

    Hot Swap – Module ‘A’ version 1 and 2 executed
    图 5:热插拔 – 模块“A”版本 1 和 2 已执行

     

  4. 如果存在并行进程运行模块 A,则可能会同时执行两个代码版本:一个进程运行模块 A 的旧版本 1,直到所有本地函数调用都已完成。第二个并行进程已经执行新版本,因为它在将新版本 2 加载到代码服务器后已经完成了本地函数调用。[7]  

    Hot Swap –Module ‘A’ Version 2 executed
    图 6:热插拔 – 模块“A”版本 2 已执行

     

  5. 如果版本 1 不再执行,则代码服务器将删除旧模块。[7]  

    Hot Swap – Module ‘A’ Version 1 removed
    图 7:热插拔 – 模块“A”版本 1 已删除

     

此示例仅考虑了一个 Erlang 模块的热插拔。在跨越多个计算节点的真实 Erlang 系统中,由于以下问题,热插拔难度增加

  • 多个 Erlang 代码模块

  • 并行处理

  • 复杂的模块依赖关系

  • 版本更新失败时的降级要求

  • 确定要交换的模块的合适范围

Erlang OTP 通过提供用于热插拔的高级 API 来应对这些挑战,该 API 使用配置文件来描述版本升级和降级。此文件称为“appup”文件。[8] 可以将“appup”文件的集合编译为所谓的“relup”文件,然后在热插拔期间加载这些文件。[9]

Mnesia

Mnesia 是一个数据库系统,专门为分布式和高可扩展的 Erlang 系统设计和开发。它支持存储任何 Erlang 数据结构,例如元组和列表。因此,与许多其他数据库不同,Mnesia 的优势在于不需要转换为其他数据类型。[10]

互联网中继聊天

互联网中继聊天 (IRC) 在 1993 年的 RFC 1459 中指定。该协议描述了一个简单的架构,其中 IRC 客户端可以通过 IRC 服务器相互发送文本消息。客户端始终连接到服务器实例。服务器可以连接到其他 IRC 服务器以形成 IRC 网络,该网络中继传输的消息。客户端可以向其他客户端直接发送消息,也可以向频道发送消息。频道由多个用户组成,这些用户接收发布到频道的所有消息。频道可以由具有适当用户权限的客户端管理。在 IRC 网络中传输的消息不会存储在 IRC 服务器上。因此,消息只能发送给在线用户。该协议通常与基于 TCP/IP 的通信一起使用。[11]

设计

本节介绍用 Erlang 实现的 IRC 服务器的设计。

软件架构

开发了以下软件架构以满足 RFC 1459 中规定的要求。为了设计 IRC 服务器,软件组件被分类为以下组件类

  • 监督器组件: 监督器为运行业务逻辑的正在运行的业务流程提供管理功能。

  • Erlang 业务流程: Erlang 业务流程正在运行 IRC 服务器的业务逻辑。

  • Mnesia 数据库表: Mnesia 表是启用系统中数据持久性的组件。

  • 实用程序组件: 实用程序组件为业务流程中运行的模块提供可重用的功能。

  • 外部组件: 外部组件是通过定义的 API 与设计的系统交互的系统。

生成的软件组件架构如图 8 所示。

IRC Server Component Diagram
图 8:IRC 服务器组件图

 

如组件图所示,IRC 服务器由分层结构中的三个监督器组成。“MainSupervisor”组件监视两个子监督器:一个用于管理“IrcApi”进程的“ApiSupervisor”和一个监视运行主业务逻辑(“消息处理程序”和“CommandExecutor”组件)的进程的“MessageHandler Supervisor”。这种监督器结构类似于集中式控制方法,其中“MainSupervisor”可以启动和终止整个 IRC 服务器实例,同时将业务流程监视委托给专用监督器。这种结构使子监督器“ApiSupervisor”和“MessageHandlerSupervisor”能够具有不同的进程重启策略,从而提高了受监视进程的独立性,从而在发生错误时提高了服务可靠性和鲁棒性。

此外,为了加强进程的解耦,使用了消息驱动的进程间通信。“IrcApi”、“MessageHandler”和“CommandExecutor”组件通过异步消息相互传递信息。通过这种方式,所有进程都可以独立存在和运行,同时避免了进程死锁的可能性。

用于访问数据持久层(Mnesia 表)的实用程序组件“DataAccessHandler”为管理 IRC 数据提供了内部的、面向用例的高级 API。该组件在其函数中聚合了 Mnesia 表提供的 CRUD 操作,例如向 IRC 服务器添加新用户。

外部组件可以通过 TCP 连接访问服务器。由于 IRC 客户端和其他 IRC 服务器使用类似的消息,因此对两种外部组件类型都使用了统一的“IrcApi”。接收到的 TCP/IP 通信的 IP 地址和端口号标识消息发送者(例如用户客户端或外部 IRC 服务器实例),这导致不同的命令执行过程。

数据模型

图 9 显示了设计的 IRC 服务器的数据模型。所有数据都存储在前面介绍的 Mnesia 数据库中。根据 RFC1459 标准实现 IRC 服务器需要以下五个 Mnesia 表。

  • users: 连接到 IRC 网络的每个客户端都存储在此表中。根据 IRC 标准的定义,每个用户的昵称都是唯一的,并用作标识客户端的主键。服务器实例使用用户表中的“socket_id”属性通过 TCP 发送 IRC 消息,并将连接的客户端彼此区分开来。
  • user_modes: 客户端可以具有某些模式,这些模式可以在“user_modes”表中指定。
  • channels: 频道具有唯一的名称用于标识,并且可以包含指示其用途的频道描述。参与频道的所有用户都链接到此表中的频道信息。
  • channel_modes: 此表包含频道的所有设置。
  • servers: 此表用于存储有关 IRC 网络的其他服务器的信息。
IRC Data Model
图例:● 属性 ◼ 表格 ◆ 关系
图 9:IRC 数据模型

评估

执行负载测试以分析设计的系统在不断增加的负载下的行为方式。

Tsung

该测试基于分布式负载测试工具 Tsung,该工具与协议无关,目前支持常见的网络协议,例如 HTTP、SOAP、TCP。该工具是用 Erlang 开发的,可能是由于性能、可扩展性和容错等内在优势。这种弹性确保了 Tsung 最重要的功能:从一台计算机模拟许多并发用户。[12]

负载测试资源

负载测试在 VirtualBox 中执行,资源如下

Load Test Resources
图 10:负载测试资源

负载测试配置

Tsung 中的负载测试通过 XML 文件配置。测试运行的总时长来自构成负载进度的七个到达阶段:[12]

 <load>

 <arrivalphase phase="1" duration="60" unit="second">

 <users arrivalrate="10" unit="second"/>

 </arrivalphase>

 <arrivalphase phase="2" duration="60" unit="second">

 <users arrivalrate="5" unit="second"/>

 </arrivalphase>

 <arrivalphase phase="3" duration="60" unit="second">

 <users arrivalrate="4" unit="second"/>

 </arrivalphase>

 <arrivalphase phase="4" duration="60" unit="second">

 <users arrivalrate="3" unit="second"/>

 </arrivalphase>

 <arrivalphase phase="5" duration="60" unit="second">

 <users arrivalrate="5" unit="second"/>

 </arrivalphase>

 <arrivalphase phase="6" duration="60" unit="second">

 <users arrivalrate="1.2" unit="second"/>

 </arrivalphase>

 <arrivalphase phase="7" duration="60" unit="second">

 <users arrivalrate="1" unit="second"/>

 </arrivalphase>

 </load>

图 11:Tsung 配置文件 - 负载部分

 

测试运行的第一阶段持续 60 秒,每秒创建 10 个新用户。因此,在此阶段总共创建了 600 个用户。在测试过程中,每秒创建的用户越来越少,直到测试达到第 4 阶段。在第 5 阶段,用户连接略有增加,然后新用户的数量下降。这样就模拟了用户量的上升和下降。

配置的负载测试的总时长(420 秒)来自累加的阶段持续时间。但是,如下一小节所示,结果超出了此持续时间。在新用户不断添加,直到测试完成。这就是 Tsung 在 420 秒的持续时间后收集结果的原因。此后,服务器仍需时间来处理这些客户端的请求并关闭连接。

每个用户客户端发送到服务器的请求在会话中定义,如下面的 XML 代码片段所示

<session type="ts_raw">

<!—Declaration and initialization of nickname, username and channelname-->

<!—Defined Requests-->

<!—Nick and User Request-->

<!—Join Request-->

<!—Send message Requests-->

<request subst="true">

    <raw data="PRIVMSG #%%_channelname%% :nachricht" ack="local"/>

</request>

<request subst="true">

    <raw data="PRIVMSG #%%_nickname%% :nachricht" ack="local"/>

</request>

<!—Quit Request-->

</session>

图 12:Tsung 配置文件 - 会话部分

 

Tsung 中的类型“ts_raw”允许将流量发送到 TCP/UDP 服务器。因此,也可以使用 Tsung 测试通过 TCP/UDP 传输的专有或非常用的网络协议。在“session”标记内,变量 nickname、username 和 channelname 用随机字符串初始化,以创建具有不同昵称和用户名的用户,以及不同的频道,因为昵称和频道名称在 IRC 中是唯一的。之后,列出了每个创建的用户要执行的请求。这些将是

  • User Request: 指定新用户的属性,例如用户名、主机名等

  • Nick Request: 给用户一个昵称或更改它

  • Join Request: 用户加入频道

  • Privmsg Request: 向频道发送消息

  • Privmsg Request: 向另一个用户发送消息

  • Quit Request: 关闭用户和服务器之间的连接

下一章介绍本章定义的负载测试配置的结果。

负载测试结果

如图 13 所示,在整个测试过程中,许多用户同时连接到服务器。

IRC Load Test – Simultaneous IRC Users Over Time
图 13:IRC 负载测试 – 随时间变化的并发 IRC 用户数

 

如折线图所示,测试结束时并发用户的最大数量为 1683。

IRC Load Test – Test Result
图 14:IRC 负载测试 – 测试结果

 

在图 14 右侧的图中,显示了每秒的请求数(红色)和连接数(绿色)。“连接”是指客户端和服务器之间建立的链接,“请求”是指通过此链接从客户端发送到服务器的消息。每秒 44.6 个请求的最大速率在测试运行开始时已经很高。在此增加之后,该数字在测试期间会根据测试配置而减少。如连接速率所示,最初连接的测试用户更多,而随着时间的推移,连接的测试用户更少。在测试运行期间,总共发送了 6732 个请求。在添加所有用户后,请求计数和连接计数不会突然减少,而是逐渐减少。这是因为服务器在定义的 420 秒测试运行时结束后需要处理时间。

左侧的图也适用。它显示了连接时间(绿线)的平均值和请求持续时间(红线)的平均值,单位为毫秒 (ms)。“连接时间”指定客户端和服务器之间的连接持续时间,“请求时间”提供有关服务器响应客户端请求所需时间的信息。在 420 秒的测试运行中,在多个十秒的时间间隔内测量的平均请求时间范围为 2.05 毫秒到 3.02 毫秒。对于连接时间,范围为 0.488 毫秒到 1.86 毫秒。

概要

与命令式编程风格相比,将函数式编程范式应用于特定问题是独特的。函数式编程语言允许开发人员使用不同的方法来实现算法。此外,数据处理也保持简单。没有类,因此无法使用面向对象的方法。Erlang 作为一种函数式编程语言,提供了额外的功能来简化函数式编程方法的使用。Erlang 中的监督器是一个高级组件,它使受监督的进程能够抵抗意外错误。热插拔能够在运行时更改代码,但需要耗时的准备工作以确保服务可用性。Erlang 中的标准 IO 已被证明是有问题的,因为整个进程在终端输入时被阻止,并且无法被任何其他进程正确终止。由于 Erlang 通常处理多个进程,因此 IO 可能会导致意外问题,例如临时死锁和崩溃。

IRC 服务器目前实现了基本功能。可以进行连接、加入和管理频道以及聊天。借助监督器和并行进程的简单使用,IRC 服务器在负载和可靠性方面非常稳定,尤其是在滥用的情况下,几乎不可能杀死整个 IRC 服务器。

总之,Erlang 主要用于分布式系统,在这些系统中,可靠性和可扩展性非常重要。这意味着它非常适合通信系统,例如开发的 IRC 服务器。就有关 Erlang 的文档而言,与更流行的编程语言(例如 Python、Java、JavaScript)相比,关于 Erlang 工具的资源较少。

参考文献

[1] Wikipedia,2021 年 Facebook 宕机。[在线]。可用:https://en.wikipedia.org/ wiki/2021_Facebook_outage(访问日期:2021 年 11 月 29 日)。

[2] TK,“函数式编程基本原理简介”,freeCodeCamp.org,2018 年 11 月 15 日。https://www.freecodecamp.org/news/an-introduc tion-to-the-basic-principles-of-functional-programming-a2c2a15c84/(访问日期:2021 年 11 月 10 日)。

[3] Ericsson,《系统架构用户指南:简介》。[在线]。可用:https://erlang.org.cn/doc/system_architecture_intro/sys_arch_intro.html(访问日期:2021 年 11 月 9 日)。

[4] M. Grotz,《Elixir 和 Erlang:轻松实现并发》。[在线]。可用:https://www.informatik-aktuell.de/entwicklung/programmiersprachen/elixir-und erlang-nebenlaeufigkeit-ganz-einfach.html(访问日期:2021 年 11 月 9 日)。

[5] Ericsson,《Erlang 参考手册:STDLIB》。[在线]。可用:https://www.er lang.org/doc/man/supervisor.html(访问日期:2021 年 11 月 9 日)。

[6] Ericsson,《Erlang 内核参考手册:Code》。[在线]。可用:https:// www.erlang.org/doc/man/code.html(访问日期:2021 年 11 月 6 日)。

[7] Ericsson,《Erlang 参考手册:代码替换》。[在线]。可用:https://erlang.org.cn/doc/reference_manual/code_loading.html#code-replacement(访问日期:2021 年 11 月 6 日)。

[8] Ericsson,《系统架构支持库:Appup》。[在线]。可用:https://erlang.org.cn/doc/man/appup.html(访问日期:2021 年 11 月 6 日)。

[9] Ericsson,《Erlang 参考手册:系统架构支持库 (SASL)》。[在线]。可用:https://erlang.org.cn/doc/man/relup.html(访问日期:2021 年 11 月 6 日)。

[10] Ericsson,《Erlang 参考手册:Mnesia》。[在线]。可用:https://www.er lang.org/doc/man/mnesia.html(访问日期:2021 年 11 月 9 日)。

[11] rfc1459。[在线]。可用:https://datatracker.ietf.org/doc/html/rfc1459(访问日期:2021 年 12 月 16 日)。

[12] 1. 简介 — Tsung 1.7.0 文档。[在线]。可用:http://tsung.er lang-projects.org/user_manual/introduction.html(访问日期:2021 年 11 月 9 日)。

加载 Disqus 评论