我如何构建正常运行时间监控服务架构
目录
目录
简介
要求
VPS 提供商和服务器
使用的技术
节点如何工作
核心服务器如何工作
总结
什么不起作用
为什么花了这么长时间?
简介
你好呀!
这是我的第一篇文章,所以可能不完美😇
大约一年前,我决定构建一个正常运行时间监控服务,于是就有了https://pingr.io。
它是一个网络应用程序,它会持续检查您的 URL 是否以 200 OK 或您喜欢的任何其他响应代码进行响应。
这个想法其实很简单。你可能会觉得发起一个 HTTP 请求其实并不难。
但我花了一年时间。😱
在本文中我想描述它是如何工作的。
我并不是说这个架构很好。
架构在某种程度上是在尽快推出产品和拥有优秀架构之间的妥协。
我不想再花一年的时间来使其完美,因为在开发过程中保持动力相当困难。
我很想听听哪些方面可以改进以及哪些方面我做错了。
让我们开始吧。
要求
乍一看,发起 HTTP 请求似乎很简单。然而,服务端需要满足很多要求。以下是其中一些:
- 应该从分布在世界各地的多个节点检查 URL。
- 最低检查频率为 1 分钟
- 您应该能够添加新节点
- 每个节点每分钟都应该发出一定数量的 HTTP 请求。起初,500-1000 个请求我还可以接受。但随着用户数量的增长,这个数字可能会更高。
- 除了 HTTP 检查之外,该服务还应检查 SSL 证书(是否有效/是否即将过期等)
- 显示 URL 状态的 UI 应该实时更新
- 应根据用户的选择,通过不同的方式通知用户正常运行/停机事件
VPS提供商和服务器
我认为,如果您希望尽快启动并运行您的产品,那么为您满意的服务支付更多费用是可以的。
因此我尝试了 ScaleWay 和 Digital Ocean,但最终我将我的所有服务器都转移到了 Digital Ocean,因为我更喜欢它。
我所拥有的:
- Ubuntu VPS 带 MySQL。3 GB / 1 vCPU
- 5 个 Ubuntu VPS,位于世界各地。3 GB / 1 vCPU
- Ubuntu VPS 作为核心服务器。4 GB / 2 vCPU
- Redis 数据库:1 GB RAM / 1vCPU
关于 MySQL。一开始我用的是他们的 RDS,因为它本来就是用来做数据库的,应该没什么问题。但当我想修改 my.cnf 文件时,我意识到我没法这么做。我喜欢掌控一切,至少在学习初期是这样。
因此,目前,我决定只安装带有 MySQL 的 VPS,因为它可以给我更多的控制权。
为什么是 MySQL?不知道。我一直只用这个数据库。
为什么是 Ubuntu?不知道。我一直只用这个操作系统。😝
我使用的技术
- Laravel 用于后端
- 前端的 VueJS
- MySQL 数据库
- Redis 用于队列
- 用于发送请求的 cURL(Guzzle 库)
- Supervisord 帮助我的员工保持正常运行
- ...
- 利润!
artisan 命令列表
当我写这篇文章时,我意识到如果我在这里列出我使用的所有 Laravel 命令可能会有很大帮助:
php artisan monitor:run-uptime-checks {--frequency=1}
frequency
- 调度正常运行时间检查任务。每分钟由 cron 运行一次php artisan checks:push
- 从本地 Redis 数据库获取检查结果到临时 MySQL 表。由 Supervisord 持续运行php artisan checks: pull
- 从临时 MySQL 表中获取检查结果,并计算监控状态/正常运行时间/其他指标。由 Supervisord 持续运行。
节点如何工作
这是文章最重要的部分之一,描述了单个节点的工作原理。
我应该注意我有一个Monitor
实体,代表应该检查的 URL。
让显示器做好正常运行时间检查的准备
该php artisan monitor:run-uptime-checks
命令根据某些条件从数据库中获取监视器。
其中一个要求是正常运行时间检查频率,也就是我们应该多久检查一次监控器。并非每个用户都希望每分钟都检查一次他们的网站。
然后,使用Laravel 调度机制,可以轻松设置以不同的频率运行此命令。
将频率作为参数传递可以帮助我仅获取需要检查的监视器,具体取决于用户设置的频率。
// In the RunUptimeChecks command, we fetch monitors by frequency specified by the user
$schedule->command(RunUptimeChecks::class, ['--frequency=1'])
->everyMinute();
$schedule->command(RunUptimeChecks::class, ['--frequency=5'])
->everyFiveMinutes();
$schedule->command(RunUptimeChecks::class, ['--frequency=10'])
->everyTenMinutes();
$schedule->command(RunUptimeChecks::class, ['--frequency=15'])
->everyFifteenMinutes();
$schedule->command(RunUptimeChecks::class, ['--frequency=30'])
->everyThirtyMinutes();
$schedule->command(RunUptimeChecks::class, ['--frequency=60'])
->hourly();
排队人数
然后,我将正常运行时间检查任务放入每个监视器的 Redis 队列中。命令如下RunUptimeChecks
:
foreach ($monitors as $monitor) {
RunUptimeCheck::dispatch(
(object) $monitor->toArray(),
$node->id
);
}
👉 您可能会在这里注意到一些奇怪的事情:(对象)$monitor->toArray()。
起初,我将 Monitor 模型传递给了作业。然而,两者之间有一个显著的区别:当你将模型传递给作业时,它只会在队列中存储一个模型 ID。然后,当作业执行时,Laravel 会连接到数据库来获取该模型,这导致了数百个不必要的连接。
这就是为什么我传递一个对象而不是实际模型,其序列化得相当好。
另一种可能的方法是将这种SerializesModels
特质从工作中移除,这也可能有效,但我还没有尝试过。
因此,完成此操作后,队列中就会有一定数量的作业准备执行。
运行正常运行时间检查
要执行作业,我们需要运行php artisan queue:work
命令。
我们还需要的是:
- 运行许多命令实例,因为我们需要每分钟进行尽可能多的检查
- 如果命令失败,我们需要撤销它并再次运行。
为了这个目的,我使用了Supervisord。
它的作用是生成 N 个 PHP 进程,每个进程都是queue:work
一个命令。如果失败,Supervisord 会重新运行该命令。
根据 VPS 内存和 CPU 核心数,我们可能会调整进程数量。这样,我们就可以增加每分钟执行的正常运行时间检查次数。
存储临时结果
现在,我们进行检查后,需要将其存储在某个地方。
首先,我将检查结果存储在本地 Redis 数据库中。
由于可能有很多进程会不断地将数据推送到队列,因此 Redis 非常适合此用途,因为它是一个内存数据库,速度非常快。
然后我有另一个命令php artisan checks:push
从 Redis 数据库获取支票并将其批量插入到raw_checks
MySQL 表中。
所以我得到了两个表:monitor_checks
和raw_checks
。第一个表包含监视器最近一次成功的检查及其所有失败的检查。我不会存储每分钟每个节点的每次检查,因为这会导致数十亿条记录,而且对最终用户来说价值不大。
该raw_checks
表充当核心服务器和所有节点服务器之间的桥梁。
每次检查后我们需要做很多事情:
- 重新计算每个节点的正常运行时间
- 重新计算每个节点的平均响应时间
- 根据节点信息重新计算监视器正常运行时间/响应时间
- 如果需要,发送通知
一次获取多张支票并在靠近 MySQL 服务器的服务器上进行所有计算会更加合理。
例如,印度节点与位于德国的MySQL服务器之间的连接相对较慢。
因此目标是使节点尽可能独立。
它们针对 MySQL 连接所做的一切就是:获取需要检查的监视器并存储检查结果。就是这样。
核心服务器的工作原理
在核心服务器上,我运行php artisan:checks-pull
命令,它的行为类似于守护进程:它有一个无限循环,从raw_checks
表中获取检查并计算平均正常运行时间、响应时间等。
除此之外,它还负责对停机通知作业进行排队。
事实上就是这样:我们有更新状态、正常运行时间和响应时间属性的监视器。
更新实时数据
为了更新 Web 应用程序上的监控状态,我使用了Pusher和Laravel 的广播功能。
因此设置很简单。
-
php artisan checks:pull
从原始表中获取检查,查看监视器是否在线,如果不在线,则触发MonitorOffline
事件,并使用 Pusher 进行广播。 -
Web 应用程序看到 Pushed 的新事件并将监视器标记为离线
总结
总结一下:
- 每个节点都有一个 cron 作业,用于获取监控列表并将正常运行时间检查作业放入本地 Redis 队列
- Supevrisord 运行的许多线程检查队列并发出 HTTP 请求
- 结果存储在Redis中
- 然后将存储在本地 Redis 中的一堆支票移至 MySQL 服务器中的原始支票表
- 核心服务器获取支票并进行计算。
每个节点都具有相同的机制。现在,有了来自原始表的持续检查流,核心服务器可以执行许多操作,例如计算节点的平均响应时间等等。
如果我想扩展节点数量,我只需克隆节点服务器,做一些小配置,就这样。
哪些地方没有发挥作用
-
起初,我把所有检查都存进了数据库,包括失败的和成功的。结果产生了数十亿条记录。但用户大多不需要它。现在,我有一个聚合表,用来存储每小时的正常运行时间和响应时间。
-
起初,正常运行时间检查作业自行完成所有逻辑:我没有任何临时/桥接数据库。因此,它连接到 MySQL 服务器,计算新的正常运行时间等等。当监视器数量增加到 100 个左右时,这种方法就立即失效了。因为每个检查作业可能执行 10-20 个查询。10-20 个查询 * 5 个节点 * 100 个监视器。所以,它完全不可扩展。
-
使用专用的 Redis 服务器代替
raw_checks
数据表。由于原始支票表的行为类似于缓存,因此考虑使用 Redis 来实现此目的可能是合理的。但不知何故,我一直丢失支票数据。
我尝试了 Redis 的订阅/发布功能,以及单纯的数据存储功能。最终我放弃了,转而使用了一种我熟悉的机制:MySQL。
另外,我想我在某处读到过,如果我们需要 100% 的信心来存储数据,那么 Redis 并不是最好的解决方案。
为什么花了这么长时间?
由于多种因素。
- 我有一份全职工作,所以我只能在晚上工作。
- 我以前没有过这样的经历。每个项目都是独一无二的。
- 我有一些心理问题,我在这篇文章中描述了这些问题
- 我尝试过很多不同的方法,让它在多台显示器的情况下也能正常工作。所以我几乎重复了好几次同样的操作。
- 考虑到我也构建了一个 UI 并独自完成了所有工作。