Tarantool弹药筒:分三行分割Lua后端


在Mail.ru组中,我们拥有Tarantool,一个基于Lua的应用程序服务器和一个数据库。 它既快速又经典,但是单个服务器的资源总是有限的。 垂直缩放也不是万能的。 这就是为什么Tarantool有一些用于水平缩放的工具或vshard模块[1]的原因 。 它允许您将数据分布在多个服务器上,但是您必须花一些时间来配置它并附加业务逻辑。

好消息:我们遇到了麻烦(例如[2][3] )并创建了另一个框架,该框架大大简化了此问题的解决方案。

Tarantool Cartridge是用于开发复杂的分布式系统的新框架。 它使您可以专注于编写业务逻辑,而不是解决基础结构问题。 首先,我将告诉您该框架的工作原理以及它如何有助于编写分布式服务。

那么到底是什么问题呢?


我们有Tarantool和vshard-我们还想要什么?

首先,这是一个方便的问题。 在Lua表中配置了Vshard。 但是,要使包含多个Tarantool进程的分布式系统正常工作,每个地方的配置都必须相同。 没人愿意手动执行此操作,因此使用了各种脚本,Ansible和部署系统。

Cartridge本身基于其自己的分布式配置来管理vshard 配置 。 实际上,这是一个简单的YAML文件,其副本存储在每个Tarantool实例上。 换句话说,框架将监视其配置,以便在任何地方都相同。

第二,这又是方便的问题。 Vshard配置与业务逻辑开发无关,只会分散开发人员的工作。 当我们讨论项目的体系结构时,最有可能涉及独立的组件及其交互。 甚至考虑为3个数据中心部署集群还为时过早。

我们一遍又一遍地解决了这些问题,在某个时候,我们设法开发出一种方法来简化应用程序的整个生命周期:创建,开发,测试,CI / CD,维护。

弹药筒介绍了每个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/ 

这是一个git存储库,其中包含随时可用的“ Hello,World!”。 应用程序。 让我们尝试在安装依赖项(包括框架本身)之后运行它:

 $ tarantoolctl rocks make $ ./init.lua --http-port 8080 

我们已经启动了未来分片应用程序的节点。 如果您感到好奇,可以立即打开在localhost :8080上运行的Web界面,使用鼠标配置一个单节点群集并享受结果,但是不要太过兴奋。 该应用程序尚不知道如何执行任何有用的操作,因此稍后我将告诉您有关部署的信息,现在该编写一些代码了。

开发应用程序


想象一下,我们正在设计一个系统,该系统每天应该接收,保存和创建一个报告。


因此,我们绘制了一个包含三个组件的图:网关,存储和调度程序。 让我们继续研究架构。 由于我们将vshard用作存储,因此我们将vshard-router和vshard-storage添加到图中。 网关和调度程序都不会直接访问存储-为此任务明确创建了一个路由器。


该图看起来很抽象,因为组件仍然无法反映我们将在项目中创建的内容。 我们必须查看该项目如何与真实的Tarantool相对应,因此我们按流程对组件进行分组。


将vshard-router和网关保留在单独的实例上没有多大意义。 如果这已经由路由器负责,为什么还要再次通过网络? 它们应该在同一进程中运行,也就是说,网关和vshard.router.cfg应该在同一进程中初始化,并在本地交互。

在设计阶段,使用三个组件很方便,但是作为开发人员,我不想在编写代码时考虑启动三个Tarantool实例。 我需要运行测试并验证我是否正确编写了网关代码。 或者,我可能想向我的同事展示一项新功能。 为什么在部署三个实例时会遇到麻烦? 因此,角色的概念诞生了。 角色是常规的Lua模块,墨盒负责管理其生命周期。 在此示例中,有四个:网关,路由器,存储和调度程序。 另一个项目可能具有更多角色。 所有角色都可以在一个过程中启动,这就足够了。


当涉及到部署到生产或生产时,我们将根据基础硬件功能为每个Tarantool流程分配一组单独的角色:


拓扑管理


我们还应该将有关正在运行的角色的信息存储在某个地方。 并且“某处”是指上述分布式配置。 这里最重要的是群集拓扑。 在这里,您可以看到5个Tarantool进程的3个复制组:


我们不想丢失数据,因此我们会谨慎处理有关正在运行的流程的信息。 磁带盒使用两阶段提交来监视配置。 一旦我们要更新配置,它将首先检查实例是否可用并准备接受新配置。 之后,将配置应用于第二阶段。 因此,即使一个实例暂时不可用,也不会出错。 根本不会应用该配置,并且您会提前看到一个错误。

拓扑部分还具有每个复制组的领导者这样的重要参数。 通常,这是接受写入的实例。 其余大多数通常是只读的,尽管可能会有例外。 有时,勇敢的开发人员并不担心冲突,可以同时将数据写入多个副本。 但是,某些操作不应执行两次。 这就是为什么我们有一个领导者。


角色生命周期


为了使项目体系结构包含抽象角色,框架必须以某种方式能够管理它们。 自然地,无需重新启动Tarantool流程即可管理角色。 有四个用于角色管理的回调。 墨盒本身根据来自分布式配置的信息来调用它们,从而将配置应用于特定角色。

 function init() function validate_config() function apply_config() function stop() 

每个角色都有一个init函数。 它被调用一次:启用角色或重新启动Tarantool时。 例如,在这里可以方便地初始化box.space.create,或者调度程序可以运行一些后台光纤,这些光纤可以定期完成任务。

init功能可能还不够。 墨盒允许角色访问用于存储拓扑的分布式配置。 在相同的配置中,我们可以声明一个新部分,并在其中存储部分业务配置。 在我的示例中,这可能是调度程序角色的数据方案或调度设置。

每次分布式配置更改时,集群都会调用validate_configapply_config 。 在两阶段提交中应用配置时,群集将验证每台服务器上的每个角色已准备好接受此新配置,并在必要时向用户报告错误。 当每个人都同意该配置时,将调用apply_config

角色还支持用于清除垃圾的stop方法。 如果我们说此服务器上不需要调度程序,则它可以停止使用init开始的光纤。

角色可以彼此交互。 我们习惯于编写Lua函数调用,但是该过程可能没有必要的作用。 为了方便网络访问,我们使用了一个称为rpc(远程过程调用)的辅助模块,该模块是基于标准Tarantool net.box模块构建的。 例如,如果您的网关希望直接要求调度程序立即而不是一天之内执行任务,这将很有用。

另一个要点是确保容错能力。 墨盒使用SWIM协议[4]监视运行状况。 简而言之,进程通过UDP相互交换“谣言”,也就是说,每个进程都将其最新消息告知邻居,然后它们做出响应。 如果突然没有答案,Tarantool会怀疑出问题了,过一会儿,它会宣布死亡,并将此消息发送给所有人。


根据此协议,墨盒会组织自动故障转移。 每个进程都监视其环境,如果领导者突然停止响应,则副本可以声明其角色,而盒式磁带将相应地配置正在运行的角色。


您在这里必须小心,因为频繁来回切换可能会导致复制期间的数据冲突。 当然,自动故障转移绝对不应该随机打开。 您应该对正在发生的事情有一个清晰的认识,并确保当领导者恢复并重新获得王冠时复制不会崩溃。

综上所述,这些角色似乎类似于微服务。 从某种意义上说,它们只是Tarantool流程中的模块,并且存在几个基本差异。 首先,所有项目角色都必须使用相同的代码库。 而且所有Tarantool进程都应在相同的代码库中运行,这样就不会让人感到意外,就像我们尝试初始化调度程序时一样,但是根本就没有调度程序。 同样,我们不应该允许代码版本有所不同,因为在这种情况下系统行为很难预测和调试。

与Docker不同,我们不能仅拍摄角色的“映像”,将其转移到另一台机器上并在其中运行。 我们的角色不像Docker容器那样孤立。 而且,我们不能在同一实例上运行两个相同的角色。 这个角色是存在还是不存在; 从某种意义上讲,这是一个单例。 第三,在整个复制组中角色应该相同,因为否则,这看起来很荒谬:数据相同,但是行为不同。

部署工具


我答应向您展示墨盒如何帮助您部署应用程序。 为了使生活更轻松,该框架创建了RPM软件包:

 $ cartridge pack rpm myapp # will create ./myapp-0.1.0-1.rpm $ sudo yum install ./myapp-0.1.0-1.rpm 

安装的软件包几乎包含您需要的所有内容:应用程序和已安装的Lua依赖项。 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] Tarantool»2.2»参考»岩石参考»模块vshard
[2] 如何基于Tarantool实现Alfa-Bank投资业务的核心
[3] 下一代计费架构:过渡到Tarantool
[4] SWIM-集群构建协议
[5] GitHub-tarantool /墨盒-cli
[6] GitHub-tarantool /墨盒

Source: https://habr.com/ru/post/zh-CN470812/


All Articles