服务器监控应用(1):服务端开发

最近发现家里的 mini 主机上的虚拟机频繁重启,有时候宿主机也会重启,但是由于虚拟机都是设置的开机自动启动,所以重启后自己不去查看 Uptime 的话还不一定知道虚拟机重启过。基于这个事情,我打算开发一套简单的服务器监控应用到自己的网站,目前已经上线一段时间了,现在说一下这个服务的开发过程。

Demo 演示页面:tendcode.com/monitor/dem...

架构介绍

先看一下架构图:

  • 客户端:使用 HTTP 请求定期向服务端上报服务器的信息
  • 服务端:提供接口供客户端调用,将客户端上报的信息进行校验和处理,存入数据库;并且提供接口展示服务器信息
  • 前端UI:定期自动请求服务端数据,刷新页面,展示数据

关于这种服务器监听,有的也叫服务器探针,很多探针为了数据的实时性,采用了 WebSocket 的方式,让客户端和服务端进行连接,实时上报数据。但是我感觉这种监控不需要太高频率的上报,3秒左右的频率已经跟实时没差了。

参考项目(主要参考了该项目的 UI 界面和数据更新方式):ServerStatus

前端页面开发

UI 主题设计

说实话,很多时候开发一个新功能,后端的实现其实很快就写完了,大部分的时候都是花在了前端的 UI 设计上面。我又是对 UI 有偏执的人,经常对一些边边角角都要调试的舒服才行,但是奈何自己也不是前端工程师,所以很多时候就要花费大量精力去调整。

但是还好,在我做这个项目的时候,我已经找到了参考的 UI 页面,而且非常好的信息是,这个页面是基于 bootstrap3 的组件设计的,而我的博客是 bootstrap4 所以基本是可以直接拿了稍加修改就可以了。

先看原版页面

再来看看我自己改后的页面效果,主打一个简洁扁平:

展开显示服务器详情:

暗色模式:

移动端适配

::: primary

🎉 移动端适配

我在移动端适配问题上也考虑了很多东西,比如如何平衡显示的数据量和页面展示效果,最后的方案是移动端把非重要的信息隐藏起来,然后放入详情中显示,这样既可以保证数据量跟 PC 端一致,也可以保证页面简洁美观。 :::

数据刷新机制

前端的数据刷新机制很简单,就是使用 js 设置了一个定时器,每隔N秒(我设置的3秒)请求一次服务端的数据,然后渲染成 html 内容更新到页面。

具体代码见 html 中:

html 复制代码
<script>
	// 页面打开执行一次
	get_servers("{{ csrf_token }}", "{% url 'monitor:get_server_list' %}");
	// 每隔3秒执行一次
	setInterval(() => get_servers("{{ csrf_token }}", "{% url 'monitor:get_server_list' %}"), 3000);
</script>

这里有几个小细节需要注意:

细节1:将服务端状态转换成 css 样式,以便显示不同的状态

比如服务器传递的状态的字段是 online 或者 offline,这个时候就可以利用 css 样式,把 online 设置成绿色, offline 设置成红色。

在 js 代码里面类似这样:

js 复制代码
let status_bg = 'bg-success';
// 根据状态使用不同的颜色的点
if (status !== "online") {
	status_bg = 'bg-danger'
}

进度条的颜色也是同样的原理,根据服务端返回的不同的数值,给进度条设置不同的颜色即可。

细节2:保持详情页展开状态

由于每条数据的详情的展开还是关闭状态是由 html 的属性决定的,所以每次刷新数据的时候,必须保留原本的状态才行,不然会出现用户明明点开了一条数据的详情,结果数据自动刷新给关闭了。

具体实现 js 如下:

js 复制代码
let is_show = '';
// 保留原来的展开状态
if ($('#more-info-' + i).hasClass('show')) {
	is_show = 'show'
}

先拿到每条数据的展开状态,然后在刷新的时候回写回去,这样就可以保证页面只是数据在变动,状态不变。

后端服务开发

后端需要创建一个模型用来管理和存放主机的信息。

模型设计

直接看源码:

python 复制代码
class MonitorServer(models.Model):
    name = models.CharField('名称', max_length=30, unique=True, help_text='用于看板中显示')
    interval = models.IntegerField('上报间隔', default=5,
                                   help_text='上报间隔时间,超过这个时间的两倍还没有更新数据就标记为离线状态,单位:秒')
    sort_order = models.IntegerField('排序', default=99, help_text='自定义排序依据')
    push_url = models.CharField('推送地址', max_length=60,
                                help_text='客户端推送的地址,为了支持代理推送或者本地推送')
    username = models.CharField('用户名', max_length=10, unique=True,
                                help_text='推送用户名,唯一')
    password = models.CharField('密码', max_length=10, unique=True,
                                help_text='第一次添加后自动生成密钥,更改后会重新生成密钥')
    secret_key = models.CharField("加密Key", max_length=64, blank=True, null=True,
                                  help_text='保存后自动生成')
    secret_value = models.CharField("密钥", max_length=256, blank=True, null=True,
                                    help_text='保存后自动生成')
    data = models.TextField('上报数据', blank=True, null=True, help_text='json格式')
    active = models.BooleanField('是否有效', help_text='用来过滤,无效的不显示', default=True)

    create_date = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
    update_date = models.DateTimeField(verbose_name='更新时间', auto_now=True)

    class Meta:
        verbose_name = '监控服务'
        verbose_name_plural = verbose_name
        ordering = ['sort_order']

    def __str__(self):
        return self.name

一些关键字段的解释:

  • name :用来显示主机的名称,唯一
  • interval :上报频率,这个数据是客户端上报上来的数据,就是让客户端告知服务端上报频率是多久,然后服务端根据这个频率去判断客户端是否掉线,规则就很简单,超过这个时间的两倍还没有更新数据就标记为离线状态
  • sort_order :用来调整显示的顺序
  • push_url :推送地址,给客户端定义固定的推送地址,这个主要是用来生成密钥,让客户端自己解析出地址,可以让其他人看不到推送地址
  • username :用户名,用来生成密钥的
  • password :密码,用来生成密钥的
  • secret_key :密钥Key,保存的时候自动随机生成一个,不需要输入
  • secret_value :保存的时候自动生成的
  • data :上报数据,json格式
  • active :激活状态,用来管理哪些主机需要显示

自动生成密钥

虽然上面填写了一堆字段,但是有的字段是可以随意填写的,比如用户名和密码,随便填写不用重复就行。密钥数据是会自动生成的。

密钥的生产逻辑:创建数据保存后,自动随机生成一个 secret_key,然后把 push_urlusernamepasswordsecret_key 四个信息加密得到 secret_value,并且,每次只要前面三个数据变动了,secret_keysecret_value 都会重新生成。

具体看这个自动生成密钥的代码:

python 复制代码
def save(self, *args, **kwargs):
    if not self.pk or self._fields_have_changed(['username', 'password', 'push_url']):
        # 如果是首次添加数据或者密码字段发生变化,则生成随机32位密码
        secret_key = str(uuid.uuid4()).replace('-', '')[:32]
        plain_text = f'{self.username}::{self.password}::{self.push_url}'
        cipher = AESCipher(secret_key)
        secret_value = cipher.encrypt(plain_text)
        self.secret_value = secret_value
        self.secret_key = secret_key
    super().save(*args, **kwargs)

def _fields_have_changed(self, fields):
    if self.pk:
        # 如果是更新数据,则检查指定字段是否发生变化
        original_instance = MonitorServer.objects.get(pk=self.pk)
        for field in fields:
            if getattr(self, field) != getattr(original_instance, field):
                return True
        return False
    return True  # 如果是首次添加数据,返回 True,表示字段已更改

加解密类的实现:

python 复制代码
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

class AESCipher:
    def __init__(self, _key):
        self.key = _key

    def encrypt(self, _plaintext):
        _cipher = AES.new(self.key.encode(), AES.MODE_CBC)
        _plaintext = pad(_plaintext.encode(), AES.block_size)
        _encrypted_text = _cipher.iv + _cipher.encrypt(_plaintext)
        return base64.b64encode(_encrypted_text).decode()[:128]

    def decrypt(self, ciphertext):
        ciphertext = base64.b64decode(ciphertext)
        iv = ciphertext[:AES.block_size]
        _cipher = AES.new(self.key.encode(), AES.MODE_CBC, iv)
        _decrypted_text = unpad(_cipher.decrypt(ciphertext[AES.block_size:]), AES.block_size)
        return _decrypted_text.decode()

注意:为了保证密钥的长度不会太长,因此对用户名和密码长度做了严格限制。

只需要给客户端提供 secret_keysecret_value 信息即可,客户端可以实现跟服务端一样的加解密方法,把其他信息解析出来。

上报数据接口设计

  • 请求地址:/monitor/server/push,解析密钥后得到
  • 请求方法:POST
  • 请求头:
    • Content-Type:application/json
    • Push-Username:用户名,解析密钥后得到
    • Push-Password:密码,解析密钥后得到
    • Push-Key:密钥Key
    • Push-Value:密钥值
  • 请求体:JSON格式,需要提供数据采集的所有字段,并且格式满足规范,否则上报失败

请求体参考:

json 复制代码
{
    "interval": 6,
    "uptime": 699196,
    "system": "darwin-23.1.0-arm64-darwin-14.1.2",
    "cpu_cores": 8,
    "cpu_model": "",
    "cpu": 25.59598494338249,
    "load_1": "3.52",
    "load_5": "3.78",
    "load_15": "4.04",
    "memory_total": 17179869184,
    "memory_used": 14047379456,
    "swap_total": 11811160064,
    "swap_used": 10225909760,
    "hdd_total": 494384795648,
    "hdd_used": 261241573376,
    "network_in": "7.70K",
    "network_out": "7.90K",
    "process": 557,
    "thread": 4038,
    "tcp": 0,
    "udp": 0,
    "version": "",
    "client_version": "0.1.3"
}

::: info 注意

上报数据的接口最重要的一个环节是进行数据校验和数据转换,需要防止客户端上报不合规的数据进而导致数据显示异常。 :::

展示数据接口设计

  • 请求地址:/monitor/server 需要认证,管理员才能访问
  • 请求方法:GET
  • 返回体:json

返回格式:

json 复制代码
{
    "code": 0,
    "error": "",
    "message": "",
    "data": {
        "list": [
            {
                "interval": 6,
                "uptime": "2 \u5929",
                "system": "darwin-23.1.0-arm64-darwin-14.1.2",
                "cpu_cores": 8,
                "cpu_model": "",
                "cpu": 21.9,
                "load_1": "3.22",
                "load_5": "3.14",
                "load_15": "3.37",
                "memory_total": "16.0G",
                "memory_used": "12.92G",
                "swap_total": "8.0G",
                "swap_used": "6.98G",
                "hdd_total": "460.43G",
                "hdd_used": "198.66G",
                "network_in": "81.20K",
                "network_out": "51.97K",
                "process": 570,
                "thread": 3959,
                "tcp": 0,
                "udp": 0,
                "version": "",
                "client_version": "0.1.1",
                "memory": 80.8,
                "hdd": 43.1,
                "status": "offline",
                "name": "Macbook",
                "date": "2024-04-02T22:33:40.884"
            }
        ]
    }
}

通过获取 code 是否为0判断接口是否成功。

客户端开发

客户端可以使用 Python 开发,也可以使用其他语言,比如 Golang 开发。客户端的本质就是采集主机数据,然后进行上报。只要按照服务端给定的接口上报主机信息就可以。

我自己期初用 Python 开发了一个版本,但是考虑到 Python 的运行需要运行环境,这就给客户端的部署增加了很大的负担。因此最后使用 Golang 开发了客户端,具体开发查看 服务器监控应用(2):使用 Golang 开发客户端

下一步

后续打算做一下通知服务,就是当服务器掉线后通知给指定的管理员,很多探针都会有这个功能,事件通知对于监控的确是一个必须的功能。

目前想到的几个需求:

  1. 可以选择要通知的服务器
  2. 使用定时任务定时检查
  3. 掉线的机器一天只会通知几次,可以设置一下通知规则,避免无脑发通知
  4. 可以记录当天发送过的通知次数,超过次数就停止通知
  5. 通知方式可以多样,当然邮件通知是最基本的,直接使用Django配置的邮箱服务器就行,其他通知方式暂时没想好有没有必要,可以先预留一下第三方通知

文章首发:tendcode.com/subject/art...

相关推荐
倔强青铜三8 小时前
Django 6.0来袭!这些新特性,真的令人振奋!
人工智能·python·django
Java水解1 天前
Django实现接口token检测的实现方案
后端·django
飞Link1 天前
【Django】Django 调用外部 Python 程序的完整指南
后端·python·django·sqlite
码界奇点1 天前
基于Django与Vue.js的RBAC权限管理系统设计与实现
vue.js·python·车载系统·django·毕业设计·源代码管理
计算机学姐1 天前
基于Python的智能点餐系统【2026最新】
开发语言·vue.js·后端·python·mysql·django·flask
计算机学姐2 天前
基于Python的在线考试系统【2026最新】
开发语言·vue.js·后端·python·mysql·django·flask
码界奇点2 天前
基于Django REST framework与Vue的前后端分离后台管理系统设计与实现
vue.js·后端·python·django·毕业设计·源代码管理
Q_Q5110082852 天前
python+springboot+django/flask基于深度学习的音乐推荐系统
spring boot·python·django·flask·node.js·php
Q_Q5110082852 天前
python+springboot+django/flask基于深度学习的淘宝用户购物可视化与行为预测系统
spring boot·python·django·flask·node.js·php
Q_Q5110082852 天前
python+django/flask+vue基于spark的西南天气数据的分析与应用系统
spring boot·python·spark·django·flask·node.js