在 Mail.ru Group,我们有 Tarantool - 这是 Lua 中的应用程序服务器,它也可以兼作数据库(反之亦然?)。 它既快又酷,但一台服务器的功能仍然不是无限的。 垂直缩放也不是万能的,所以 Tarantool 有水平缩放的工具 - vshard 模块
好消息:我们收集了一些大人物(例如
问题是什么?
我们有狼蛛,我们有 vshard - 您还想要什么?
首先,这是一个方便的问题。 vshard 配置是通过 Lua 表进行配置的。 为了使多个 Tarantool 进程的分布式系统正常工作,各处的配置必须相同。 没有人愿意手动执行此操作。 因此,使用了各种脚本、Ansible 和部署系统。
Cartridge 本身管理 vshard 配置,它基于其 自己的分布式配置。 它本质上是一个简单的 YAML 文件,其副本存储在每个 Tarantool 实例中。 简化之处在于框架本身监视其配置并确保它在各处都相同。
其次,这又是一个方便的问题。 vshard配置与业务逻辑的开发无关,只会分散程序员的注意力。 当我们讨论项目的架构时,我们最常讨论的是各个组件及其交互。 现在考虑将集群部署到 3 个数据中心还为时过早。
我们一遍又一遍地解决这些问题,在某个时候,我们成功地开发了一种方法,可以简化应用程序在整个生命周期中的使用:创建、开发、测试、CI/CD、维护。
Cartridge 为每个 Tarantool 进程引入了角色的概念。 角色是一个允许开发人员专注于编写代码的概念。 项目中可用的所有角色都可以在一个 Tarantool 实例上运行,这足以进行测试。
Tarantool 墨盒的主要特点:
- 自动化集群编排;
- 使用新角色扩展应用程序的功能;
- 用于开发和部署的应用程序模板;
- 内置自动分片;
- 与 Luatest 测试框架集成;
- 使用WebUI和API进行集群管理;
- 打包和部署工具。
你好,世界!
我迫不及待地想展示框架本身,所以我们将把有关架构的故事留到以后,先从简单的事情开始。 如果我们假设 Tarantool 本身已经安装,那么剩下的就是
$ tarantoolctl rocks install cartridge-cli
$ export PATH=$PWD/.rocks/bin/:$PATH
这两个命令将安装命令行实用程序并允许您从模板创建第一个应用程序:
$ cartridge create --name myapp
这就是我们得到的:
myapp/
├── .git/
├── .gitignore
├── app/roles/custom.lua
├── deps.sh
├── init.lua
├── myapp-scm-1.rockspec
├── test
│ ├── helper
│ │ ├── integration.lua
│ │ └── unit.lua
│ ├── helper.lua
│ ├── integration/api_test.lua
│ └── unit/sample_test.lua
└── tmp/
这是一个带有现成的“Hello, World!”的 git 存储库。 应用。 让我们尝试立即运行它,之前已经安装了依赖项(包括框架本身):
$ tarantoolctl rocks make
$ ./init.lua --http-port 8080
因此,我们为未来的分片应用程序运行一个节点。 一个好奇的外行人可以立即打开 Web 界面,用鼠标配置一个节点的集群并享受结果,但现在高兴还为时过早。 到目前为止,应用程序还不能做任何有用的事情,所以我稍后会告诉你有关部署的信息,但现在是时候编写代码了。
应用开发
试想一下,我们正在设计一个必须每天接收数据、保存数据并构建报告的项目。
我们开始绘制图表并在其上放置三个组件:网关、存储和调度程序。 我们正在进一步研究架构。 由于我们使用 vshard 作为存储,因此我们将 vshard-router 和 vshard-storage 添加到该方案中。 网关和调度程序都不会直接访问存储;这就是路由器的用途,这就是它创建的目的。
该图仍然不能准确地代表我们将在项目中构建的内容,因为组件看起来很抽象。 我们仍然需要看看如何将其投影到真正的 Tarantool 上 - 让我们按进程对组件进行分组。
将 vshard-router 和 gateway 保留在单独的实例上没有什么意义。 如果这已经是路由器的责任,为什么我们还需要再次上网呢? 它们必须在同一进程中运行。 即gateway和vshard.router.cfg都在一个进程中初始化,并让它们在本地交互。
在设计阶段,使用三个组件很方便,但作为开发人员,我在编写代码时不想考虑启动 Tarnatool 的三个实例。 我需要运行测试并检查我是否正确编写了网关。 或者也许我想向我的同事演示一项功能。 为什么我要经历部署三个副本的麻烦? 角色的概念就这样诞生了。 角色是一个常规的luash模块,其生命周期由Cartridge管理。 在这个例子中,有四个——网关、路由器、存储、调度器。 另一个项目中可能还有更多。 所有角色都可以在一个进程中运行,这就足够了。
当部署到登台或生产时,我们将根据硬件功能为每个 Tarantool 进程分配自己的一组角色:
拓扑管理
有关哪些角色正在运行的位置的信息必须存储在某个地方。 这个“某处”就是分布式配置,我上面已经提到过。 最重要的是集群拓扑。 以下是 3 个 Tarantool 进程的 5 个复制组:
我们不想丢失数据,因此我们谨慎对待有关正在运行的进程的信息。 Cartridge 使用两阶段提交来跟踪配置。 一旦我们想要更新配置,它首先检查所有实例是否可用并准备好接受新配置。 之后,第二阶段应用配置。 因此,即使一份副本暂时不可用,也不会发生什么坏事。 该配置将不会被应用,并且您会提前看到错误。
同样在拓扑部分中,指示了诸如每个复制组的领导者这样的重要参数。 通常这是正在录制的副本。 其余的通常是只读的,但也可能有例外。 有时勇敢的开发人员不怕冲突,可以并行地将数据写入多个副本,但有些操作无论如何都不应该执行两次。 为此,有一个领导者的标志。
角色的一生
为了使抽象角色存在于这样的架构中,框架必须以某种方式管理它们。 当然,无需重新启动 Tarantool 进程即可进行控制。 有 4 个回调来管理角色。 Cartridge 本身将根据其分布式配置中写入的内容来调用它们,从而将配置应用于特定角色。
function init()
function validate_config()
function apply_config()
function stop()
每个角色都有一个功能 init
。 当启用角色或重新启动 Tarantool 时,它会被调用一次。 例如,初始化 box.space.create 很方便,或者调度程序可以启动一些后台纤程,该纤程将在特定时间间隔执行工作。
一种功能 init
可能还不够。 Cartridge 允许角色利用它用来存储拓扑的分布式配置。 我们可以在同一配置中声明一个新部分,并在其中存储业务配置的片段。 在我的示例中,这可能是调度程序角色的数据模式或计划设置。
集群调用 validate_config
и apply_config
每次分布式配置发生变化时。 当通过两阶段提交应用配置时,集群会检查每个角色是否已准备好接受此新配置,并在必要时向用户报告错误。 当大家都认为配置正常后,那么 apply_config
.
角色也有方法 stop
,这是清理角色输出所必需的。 如果我们说该服务器上不再需要调度程序,它可以停止它启动的那些光纤 init
.
角色可以相互交互。 我们习惯在Lua中编写函数调用,但可能会发生给定进程没有我们需要的角色的情况。 为了方便通过网络进行调用,我们使用了rpc(远程过程调用)辅助模块,该模块是在Tarantool内置的标准netbox的基础上构建的。 例如,如果您的网关想要直接要求调度程序立即完成工作,而不是等待一天,这可能会很有用。
另一个重要的一点是确保容错能力。 Cartridge使用SWIM协议来监控健康状况
Cartridge基于该协议组织自动故障处理。 每个进程都会监视其环境,如果领导者突然停止响应,副本可以接管其角色,并且 Cartridge 相应地配置运行角色。
这里需要小心,因为频繁的来回切换可能会导致复制过程中的数据冲突。 当然,您不应该随意启用自动故障转移。 我们必须清楚地了解正在发生的事情,并确保在领导者恢复并将王冠归还给他后,复制不会中断。
从这一切,你可能会觉得角色和微服务很相似。 从某种意义上说,它们只是作为 Tarantool 进程内的模块。 但也存在一些根本性的差异。 首先,所有项目角色必须位于同一代码库中。 所有 Tarantool 进程都应该从相同的代码库启动,这样就不会出现像我们尝试初始化调度程序时那样的意外,但它根本不存在。 另外,您不应允许代码版本存在差异,因为在这种情况下系统的行为很难预测和调试。
与 Docker 不同,我们不能只获取角色“镜像”,将其带到另一台机器并在那里运行。 我们的角色并不像 Docker 容器那样孤立。 此外,我们不能在一个实例上运行两个相同的角色。 角色要么存在,要么不存在;从某种意义上说,它是一个单例。 第三,整个复制组内的角色必须相同,否则就会很荒谬——数据相同,但配置不同。
部署工具
我承诺展示 Cartridge 如何帮助部署应用程序。 为了让其他人的生活更轻松,该框架打包了 RPM 包:
$ cartridge pack rpm myapp -- упакует для нас ./myapp-0.1.0-1.rpm
$ sudo yum install ./myapp-0.1.0-1.rpm
安装的软件包几乎包含您需要的所有内容:应用程序和安装的依赖项。 Tarantool 也将作为 RPM 包的依赖项到达服务器,我们的服务已准备好启动。 这是通过 systemd 完成的,但首先您需要编写一些配置。 至少指定每个进程的 URI。 例如,三个就足够了。
$ sudo tee /etc/tarantool/conf.d/demo.yml <<CONFIG
myapp.router: {"advertise_uri": "localhost:3301", "http_port": 8080}
myapp.storage_A: {"advertise_uri": "localhost:3302", "http_enabled": False}
myapp.storage_B: {"advertise_uri": "localhost:3303", "http_enabled": False}
CONFIG
这里有一个有趣的细微差别。 我们不只指定二进制协议端口,而是指定进程的整个公共地址,包括主机名。 这是必要的,以便集群节点知道如何相互连接。 使用 0.0.0.0 作为advertise_uri 地址不是一个好主意;它应该是外部IP 地址,而不是套接字绑定。 没有它,什么都不会工作,所以 Cartridge 根本不会让你启动带有错误的advertise_uri的节点。
现在配置已准备就绪,您可以启动进程。 由于常规的 systemd 单元不允许启动多个进程,因此 Cartridge 上的应用程序是通过所谓的方式安装的。 实例化单元的工作方式如下:
$ sudo systemctl start myapp@router
$ sudo systemctl start myapp@storage_A
$ sudo systemctl start myapp@storage_B
在配置中,我们指定了 Cartridge 为 Web 界面提供服务的 HTTP 端口 - 8080。让我们看看它:
我们看到虽然进程正在运行,但它们尚未配置。 墨盒还不知道谁应该与谁复制,也无法自行做出决定,所以它正在等待我们的行动。 但我们没有太多选择:新集群的生命周期从第一个节点的配置开始。 然后我们将其他人添加到集群中,并为他们分配角色,此时就可以认为部署成功完成。
经过一周漫长的工作后,让我们倒一杯您最喜欢的饮料,放松一下。 该应用程序可以使用。
结果
结果如何? 尝试、使用、留下反馈、在 Github 上创建票证。
引用
[1]
来源: habr.com