2024年7月

这两年,随着人工智能(AI)和计算能力的发展,AI应用的落地速度大大加快。以ChatGPT为代表的AI应用迅速火遍全球,成为打工人的常用工具。紧接着,多模态、AI Agent等各种高大尚的名词也逐渐进入大众视野,吸引了大量关注。那么,到底什么是AI Agent?下文半支烟将带你详细了解这个概念。

1. 一句话总结:什么是AI Agent

AI Agent,经常被翻译为:智能体或代理。

一句话总结,AI Agent就是一个有着
聪明大脑
而且能够
感知外部环境

采取行动

智能系统

我们可以把它想象成一个能思考和行动的人,而大型语言模型(LLM)就是这个人的“大脑”。通过这个大脑,再加上一些能够感知外部世界和执行任务的部件,AI Agent就变成了一个有“智慧”的机器人。

要让AI Agent充分利用它的“大脑”和各种组件,需要一种协调机制。ReAct机制就是常用的协调机制。通过ReAct机制,AI Agent能够结合外部环境和行动组件,完成复杂的任务。

为什么我们需要AI Agent呢?其实说到底是因为
单一的模型对我们来说作用不大
,我们需要的是一个具备智能的复杂系统。只有复杂系统才能真正的应到到实际生产工作中。

2. 从单一模型到复合AI系统

要理解AI Agent,我们先看看AI领域的一些变化。

以前的AI系统通常是单一模型,受训练数据的限制,只能解决有限的任务或者固定领域的任务,难以适应新的情况。

而现在,我们有了LLM通用大模型,训练的数据更多,能完成更多领域的任务,比如内容生成、文生图、文生视频等等。同时还可以把大模型和各种外部组件结合起来,构建复合AI系统,这样就能解决更复杂的问题。

举个例子,如果直接让单一模型帮我制定一个去三亚的旅游计划,它无法做到。如果让LLM大模型帮我制定一个去三亚的旅游计划,它可以制定一个鸡肋的计划,几乎不可用,因为它不知道我的个人信息、也不知道航班信息,也不知道天气情况。

但如果我们设计一个复杂AI系统,让系统里的LLM大模型能够通过工具能访问我的个人信息,访问互联网上的天气情况,访问航班信息,再结合航班系统的开放接口,就可以自动帮我预定机票,自动制定行程规划了。

这就是复合AI系统的魅力,它能够结合
工具、记忆、其余各种组件
来解决复杂问题。

3. 复合AI系统的模块化

复合AI系统是模块化的,就像拼积木一样。你可以选择不同的模型和组件,然后把它们组合在一起,解决不同的问题。

比如,你可以用一个模型来生成文本,用另一个模型来处理图像,还可以用一些编写的程序代码,一起构建出复杂AI系统。

4. AI Agent的推理与行动能力

AI Agent的核心是让LLM大模型
掌控
复杂AI系统的逻辑,说白了就是让LLM主导AI Agent的思维过程。我们向LLM输入复杂问题,它可以将复杂问题分解并一步步的制定解决方案。

这与设计一个程序系统不同,在AI系统里,LLM大模型会一步一步的思考、制定一步一步的计划,然后一个一个的去解决。并不是按照某个指定程序去执行的。

AI Agent的组件包括:大模型的推理能力、行动能力 和 记忆能力。

  • 大模型的推理能力是 解决问题的核心。
  • 行动能力通过工具(外部程序)实现,模型可以定义何时调用它们以及如何调用它们。工具可以是搜索引擎、计算器、操作数据库等。
  • 记忆能力使大模型能够存储内部日志和对话历史,从而使体验更加个性化。记忆可以帮助大模型在解决复杂问题时保持上下文连贯。

5. 总结

我们正处于AI Agent发展的早期阶段。未来,我们将看到更多的系统利用AI Agent进行"
AI+
"的转型。

总结来说,AI Agent通过整合LLM的推理能力和外部工具的行动能力,能够在复杂环境中自主解决问题。ReAct机制很好的实现了AI Agent的理念。

希望这篇博客对你有帮助!如果你有问题或想法,欢迎在评论区留言,一起探讨!

如果想了解具体的代码实践,可以翻阅我的这篇文章《
基于ReAct机制的AI Agent
》。

=====>>>>>>
关于我
<<<<<<=====

本篇完结!欢迎点赞 关注 收藏!!!

原文链接:
https://mp.weixin.qq.com/s/E3odBUtAGY9bzGIxOFt1HA

前言

Centos7.x系统已于2024年6月30日起停止维护更新,如此这般随着时间的推移,系统可能会出现越来越多的故障和漏洞问题等,这就会导致操作系统会暴露在网络攻击的威胁之下。

这就不得不迫使我转移学习资料和项目到更新的版本或者其他的操作系统。在当今国产化的浪潮中,我选择了由华为主导的openEuler操作系统。原因如下:

  1. 拥有活跃的开源社区支持

    • 活跃社区
      :openEuler拥有一个活跃的开源社区,众多企业和开发者参与其中,共同促进技术的进步和生态的繁荣。
    • 持续更新
      :得益于社区的贡献,也能够持续获得更新,包括功能增强、性能优化和安全补丁。
  2. 拥有强大的兼容性与生态支持

    • 兼容性强
      :具有良好的兼容性,能够很好的兼容Centos 7迁移过来的项目和支持多种硬件架构和软件应用。
    • 丰富的生态
      :华为与众多软硬件厂商合作,构建了较为完善的生态系统。

基于以上的原因以及还未列出的原因,openEuler操作系统是可以满足我的需求的。

安装环境

操作系统 处理器 硬盘大小 内存大小
openEuler 22.03 LTS-SP4 J4125 120G 8G

image

1. 下载Freeswitch

1.1 git clone 下载freeswitch库

[root@localhost data]# git clone https://github.com/signalwire/freeswitch.git
正克隆到 'freeswitch'...
remote: Enumerating objects: 321005, done.
remote: Counting objects: 100% (553/553), done.
remote: Compressing objects: 100% (372/372), done.
remote: Total 321005 (delta 288), reused 330 (delta 153), pack-reused 320452
接收对象中: 100% (321005/321005), 132.64 MiB | 895.00 KiB/s, done.
处理 delta 中: 100% (250772/250772), done.

1.2 官网下载

curl -o freeswitch-1.10.11.-release.tar.gz https://files.freeswitch.org/releases/freeswitch/freeswitch-1.10.11.-release.tar.gz

2. 开始安装前的工作

2.1 安装编译时需要的环境【先安装这个!】

yum -y install tar gcc-c++ nasm yasm make libtool libtool-devel uuid-devel libtiff-devel libjpeg-devel cmake libuuid-devel libatomic sqlite-devel libcurl libcurl-devel pcre pcre-devel speex speex-devel speexdsp speexdsp-devel ldns-devel libedit-devel libsndfile-devel

2.2 configure前需要安装的库

freeswitch官方把
spandsp

sofia-sip
从FreeSWITCH代码仓库单独弄出来了,所以编译前要单独编译安装。

2.2.1. spandsp

这个我真是踩了大坑,由于spandsp一直在更新,所以要用一个稳定版本才行。

git clone https://github.com/freeswitch/spandsp.git
cd spandsp/
git checkout e1e33ecd2b6325fc4f2542da2184c834fa77c5c8
./bootstrap.sh
./configure
make -j$(nproc) && make install

vi /etc/profile
# 文末添加以下内容:
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH
source /etc/profile

2.2.2. sofia-sip

git clone https://github.com/freeswitch/sofia-sip.git
cd sofia-sip
./bootstrap.sh
./configure
make -j$(nproc) && make install

2.2.3. libks

git clone https://github.com/signalwire/libks.git
cd libks
cmake .
make -j$(nproc) && make install

这里有个坑,要将libks2.pc链接或者复制到前面spandsp设置的PKG_CONFIG_PATH中的目录里面去,要不然下面signalwire-c会报错 Package 'libks2', required by 'virtual:world', not found

find /usr -name 'libks2.pc' # 如果不知道libks2.pc的路径就搜索。
ln -sf /usr/lib/pkgconfig/libks2.pc /usr/local/lib/pkgconfig/libks2.pc

2.2.4. signalwire-c

git clone https://github.com/signalwire/signalwire-c.git
cd signalwire-c/
cmake .
make -j$(nproc) && make install
# find /usr -name 'signalwire_client2.pc' # 如果不知道路径就搜索。

2.2.5 x264

git clone https://git.videolan.org/git/x264.git
cd x264
./configure --enable-shared --enable-static --disable-opencl
make -j$(nproc) && make install
cp /usr/local/lib/pkgconfig/x264.pc /usr/lib64/pkgconfig

2.2.6. libav

git clone https://github.com/libav/libav.git
cd libav
git checkout v12.3
./configure --enable-pic --enable-shared  --enable-libx264 --enable-gpl --extra-libs="-ldl"
make -j$(nproc) && make install
cp /usr/local/lib/pkgconfig/libavcodec.pc    /usr/lib64/pkgconfig/
cp /usr/local/lib/pkgconfig/libavdevice.pc   /usr/lib64/pkgconfig/
cp /usr/local/lib/pkgconfig/libavfilter.pc   /usr/lib64/pkgconfig/
cp /usr/local/lib/pkgconfig/libavformat.pc   /usr/lib64/pkgconfig/
cp /usr/local/lib/pkgconfig/libavresample.pc /usr/lib64/pkgconfig/
cp /usr/local/lib/pkgconfig/libavutil.pc     /usr/lib64/pkgconfig/
cp /usr/local/lib/pkgconfig/libswscale.pc    /usr/lib64/pkgconfig/
ldconfig # 切记,复制完一定要执行刷新
2.2.6.1 可能出现的错误一
libavcodec/libx264.c: 在函数‘X264_frame’中:
libavcodec/libx264.c:142:9: 错误:‘x264_bit_depth’未声明(在此函数内第一次使用)
  142 |     if (x264_bit_depth > 8)
      |         ^~~~~~~~~~~~~~
libavcodec/libx264.c:142:9: 附注:每个未声明的标识符在其出现的函数内只报告一次
libavcodec/libx264.c: 在函数‘X264_init_static’中:
libavcodec/libx264.c:577:9: 错误:‘x264_bit_depth’未声明(在此函数内第一次使用)
  577 |     if (x264_bit_depth == 8)
      |         ^~~~~~~~~~~~~~
make: *** [Makefile:44:libavcodec/libx264.o] 错误 1
make: *** 正在等待未完成的任务....

img

解决方法
sed -i 's/x264_bit_depth/X264_BIT_DEPTH/g' libavcodec/libx264.c
2.2.6.2 可能出现的错误二

这里有个坑...,当我执行
make install
时输出如下。然后我去安装yasm然后
make clean
重新跑一下就可以了

INSTALL doc/avconv.1
INSTALL doc/avprobe.1
INSTALL libavdevice/libavdevice.a
make: *** 没有规则可制作目标“util.asm”,由“libavresample/x86/audio_convert.o” 需求。 停止。
解决方法
yum -y install yasm

2.2.7. opus

git clone https://freeswitch.org/stash/scm/sd/opus.git
# 官方仓库有问题可以下载第三方镜像源http://freeswitch.clx.fun:12130/src-releases/libs/opus-1.1-p2.tar.gz或者去官网https://www.opus-codec.org/downloads/
cd opus
./autogen.sh 
./configure
make -j4 && make install
cp /usr/local/lib/pkgconfig/opus.pc /usr/lib64/pkgconfig

3. 检查Freeswitch编译环境

tar zxvf freeswitch-1.10.11.-release.tar.gz
cd freeswitch-1.10.11.-release/
./configure --prefix=/usr/local/freeswitch

这里我是采用
1.2 官网下载
的方式下载官网的压缩包,出现下图的数据就是检查完成了。

img

3.1 出现问题以及解决方法

3.1.1. checking for spandsp >= 3.0... configure: error: no usable spandsp; please install spandsp3 devel package or equivalent

检查2.2.1. spandsp有无安装失败

3.1.2. configure: error: You need to either install libldns-dev or disable mod_enum in modules.conf

检查2.1 安装编译时需要的环境有无安装全

3.1.2. configure: error: You need to either install libedit-dev (>= 2.11) or configure with --disable-core-libedit-support

同上解决方案

如遇此类问题

按理来说执行完
2.1 安装编译时需要的环境
应该就可以了,但是如果还由错误,可以截图留言讨论讨论。

4. 编译Freeswitch

4.1 暂时屏蔽了mod_av、mod_lua,会导致编译不通过,暂时未找到原因【待排查】。

在freeswitch-1.10.11.-release/modules.conf下可以选择需要编译的mod

4.2 编译安装


./configure
之后才能执行编译

cd freeswitch-1.10.11.-release/
make -j$(nproc) && make install

出现下图这样就是安装成功了,接下来就可以开启Freeswitch之旅了
img

编译过程中如果遇到以下错误可以按照下列方法检查
You must install libopus-dev to build mod_opus
,就执行一遍
2.2.7. opus
然后重新
./configure

You must install libsndfile-dev to build mod_sndfile
,就执行
yum
安装
libsndfile-devel
然后重新
./configure

4.3 验证安装

将freeswitch和fs_cli链接到/usr/local/bin/,这样我们就可以直接在命令行输入命令执行啦。

ln -sf /usr/local/freeswitch/bin/freeswitch /usr/local/bin/
ln -sf /usr/local/freeswitch/bin/fs_cli /usr/local/bin/

输入
freeswitch -version
输出下面版本号就是正常得了

FreeSWITCH version: 1.10.11-release~64bit (-release 64bit)

5. Freeswitch 启动!

freeswitch -nc # 后台无console启动
fs_cli # freeswitch自带命令行

img

原文链接:
https://hi.imzlh.top/2024/07/08.cgi

关于njs

首先,njs似乎在国内外都不受关注,资料什么的只有
官网参考手册
,出了个问题只能看到Github Issue
所以,这篇文章将我的探索过程展示给大家,njs对于可用存储空间较小的设备真的很友好,相比较于NodeJS、Deno这种80M起步的运行环境真的很轻量
但是,这里有几点需要提一下,入坑需谨慎:

  • 不完善的语法
    • for...of不可用
    • import和export只能使用默认导出
    • try...catch 不能不定义捕获的内容,比如这个就不合法
      try{
      require('fs').statSync('/')
      }catch{
      ngx.log(ngx.INFO, '找不到模块fs')
      }
    • 没有Event支持,如
      addEventListener
    • ...
  • 没有GC
    这表明NJS VM是一次性的,除非手动垃圾回收
  • 反人类的API设计
    比如,fs.open()后不能
    seek()
    ,返回的是
    UInt8Array
  • 社区不完善
    你可能需要自己摸索,甚至有阅读源码和提Issue的勇气
  • ...

入门第一步:TypeScript

虽然njs不支持TypeScript,但是不影响我们使用TypeScript为代码添加类型检查
NJS官方开发了
TypeScript类型定义
,开箱即用
将定义放在type文件夹中,然后使用三斜杠ref语法引入

配置

入口上,我们不能使用export function语法(前文提到过),需要定义一个入口函数然后使用默认导出

async function main(h:NginxHTTPRequest){
    // ...
}
export default { main }

注意
这个时候不能使用
njs-cli
运行,会显示
SyntaxError: Illegal export statement
解决办法:
njs -c "import M from './main.js'; M.main();"

提示
Nginx的Buffer和NodeJS的Buffer很像,我就不多介绍了

文件系统(fs)

使用NJS的目标就是代替NginxLUA模块,NJS复用Nginx的事件循环,因此支持异步操作
异步操作用的最多的就是文件IO,即
fs
使用fs有两种方式(这一点上和NodeJS很像)

  • ES式
    import FS from 'fs';
  • CommonJS式
    const FS = require('fs');

FS内有两种,一种是同步IO(不建议,但API简单)和异步IO(共享Nginx的EventLoop)
下面我们以异步IO为例:

access(): 尝试获取文件

access最大的作用是确保文件是如你所想的,要知道,
Permission Denied
很烦人
这个是官方的实例:

import fs from 'fs'
fs.promises.access('/file/path', fs.constants.R_OK | fs.constants.W_OK)
.then(() => console.log('has access'))
.catch(() => console.log('no access'))
  • 第一个参数(
    字符串
    )是
    文件名
  • 第二个参数(
    数字
    )是
    文件模式
    ,允许使用位或(
    |
    ),官方提供了
    fs.constants
    fs.constants
    里有一些预设变量,方便使用
    • R_OK
      可读 (0b100)
    • W_OK
      可写 (0b10)
    • X_OK
      可执行 (0b1)
    • F_OK
      好歹是个文件(夹) (0b0)

注意
这个函数最大的坑就是没有返回值,如果
没有权限就抛出错误
,千万别忘记
catch

open(): 打开文件

这个函数很关键,用于打开文件

open(path: Buffer|string, flags?: OpenMode, mode?: number): Promise<NjsFsFileHandle>;
  • 第一个参数是
    文件位置
    (
    string
    ),甚至可以传入
    Buffer
  • 第二个参数是打开模式
    | 文件模式 | 描述 |
    |-----|-----|
    | "a" | 打开文件用于追加。 如果文件不存在,则创建该文件|
    | "ax" | 类似于 'a',但如果路径存在,则失败 |
    | "a+" | 打开文件用于读取和追加。 如果文件不存在,则创建该文件 |
    | "ax+" | 类似于 'a+',但如果路径存在,则失败 |
    | "as" | 打开文件用于追加(在同步模式中)。 如果文件不存在,则创建该文件 |
    | "as+" | 打开文件用于读取和追加(在同步模式中)。 如果文件不存在,则创建该文件 |
    | "r" | 打开文件用于读取。 如果文件不存在,则会发生异常 |
    | "r+" | 打开文件用于读取和写入。 如果文件不存在,则会发生异常 |
    | "rs+" | 类似于 'r+',但如果路径存在,则失败 |
    | "w" | 打开文件用于写入。 如果文件不存在则创建文件,如果文件存在则截断文件 |
    | "wx" | 类似于 'w',但如果路径存在,则失败 |
    | "w+" | 打开文件用于读取和写入。 如果文件不存在则创建文件,如果文件存在则截断文件 |
    | "wx+" | 类似于 'w+',但如果路径存在,则失败 |

这个函数重点是返回的结果。什么?看不起?好,那么我们尝试读取文件的一段
我们先看一下结构

  • close()
    关闭这个文件fd
  • fd
    文件fd(file description)
  • read(buffer, buf_offset, read_len, pos)
    • buffer
      传入一个Buffer用于缓冲。当读取完毕时,这个Buffer里有我们想要的数据
    • buf_offset
      这个Buffer开始填充的位置。可以用这个实现一个Buffer读取指定大小的内容
    • read_len
      读取长度,但是如果超出了Nginx的Buffer大小,这个数值相对于实际读取的大小会偏大
    • pos
      这个是我们今天的重头戏
      想要知道如何seek吗?不行,必须使用
      pos
      如果设定为数字,将seek到那个地方并开始读取
      如果设定为
      null
      ,不改变文件指针位置,从当前位置开始读取
      是不是很反人类?
    • 最后返回
      NjsFsBytesRead
      ,其中有两个元素
      • bytesRead
        ,读取的长度
      • buffer
        ,就是你传入的buffer
  • stat()
    等同于
    fs.promises.stat()
    的结果
  • [A]
    write(buffer, buf_offset, read_len, pos)
    • buffer
      老规矩,写入的数据Buffer
    • buf_offset
      这个Buffer开始读取的位置
    • read_len
      从这个Buffer读取用于写入长度,但是如果超出了Nginx的Buffer大小,这个数值相对于实际读取的大小会偏大
    • pos
      和上面
      read()
      的pos参数一致
    • 最后返回
      NjsFsBytesWritten
      ,其中有两个元素
      • bytesWritten
        ,写入的长度
      • buffer
        ,就是你传入的buffer
  • [b]
    write(string, pos, encoding)
    • write()也可以写入字符串
    • string
      等待写入的字符串
    • pos
      和上面
      read()
      的pos参数一致
    • encoding
      编码格式,可选
      utf8
      hex
      base64
      base64url

这是TypeScript定义

interface NjsFsFileHandle {
    close(): Promise<void>;
    fd: number;
    read(buffer: NjsBuffer, offset: number, length: number, position: number | null): Promise<NjsFsBytesRead>;
    stat(): Promise<NjsStats>;
    write(buffer: NjsBuffer, offset: number, length?: number, position?: number | null): Promise<NjsFsBytesWritten>;
    write(buffer: string, position?: number | null, encoding?: FileEncoding): Promise<NjsFsBytesWritten>;
}

关于使用,可以见
https://github.com/imzlh/vlist-njs/blob/master/main.ts#L130,实现纯粹文件拷贝

 const st = await fs.promises.open(from,'r'),
    en = await fs.promises.open(to,'w');
while(true){
    // 读取64k 空间
    const buf = new Uint8Array(64 * 1024),
        readed = await st.read(buf, 0, 64 * 1024, null);

    // 读取完成
    if(readed.bytesRead == 0) break;

    // 防漏式写入
    let writed = 0;
    do{
        const write = await en.write(buf, writed, readed.bytesRead - writed, null);
        writed += write.bytesWritten;
    }while(writed != readed.bytesRead);
}

readdir():扫描文件夹

虽然我们建议返回填满string的数组,但是返回填充了Buffer的数组也不是不行

readdir(path, option)
  • path
    路径,同样可以是Buffer
  • option
    Object对象
    • encoding
      编码格式,可选
      utf8
      (返回字符串)
      buffer
      (返回
      Buffer
      )
    • withFileTypes
      自带stat文件类型的扫描,指定为true,返回的就是
      NjsDirent[]

      • isBlockDevice()
      • isCharacterDevice()
      • isDirectory()
      • isFIFO()
      • isFile()
      • isSocket()
      • isSymbolicLink()
      • name
        文件(夹)名
  • 返回值由option决定,如果什么都没指定,返回字符串数组

realpath(): 相对路径转绝对路径

realpath(path, option)
  • path
    路径,同样可以是Buffer
  • option
    Object对象
    • encoding
      编码格式,可选
      utf8
      (返回字符串)
      buffer
      (返回
      Buffer
      )
  • 返回值由option决定,如果什么都没指定,返回字符串

rename(): 移动文件

注意
跨文件系统(磁盘)移动不能使用rename(),instead,请
拷贝后再删除
实用技巧
什么?你告诉我你不会判断是否跨文件系统(磁盘)?stat()啊

const from = await fs.promises.stat('...'),
    to = await fs.promises.stat('...');
            
// 相同dev使用rename
if(from.dev == to.dev){
    await fs.promises.rename(...);
}else{
    // copy()
    await fs.promises.unlink('...');
}

实例参考:
https://github.com/imzlh/vlist-njs/blob/master/main.ts#L622

rename(from, to)
  • from
    路径,除了string同样可以是Buffer
  • to
    路径,同理,除了string同样可以是Buffer
  • 没有返回值,注意
    catch
    错误情况
unlink(path: PathLike): Promise<void>;
  • path
    路径,同样可以是Buffer
  • 没有返回值

rmdir() 删除文件夹

rmdir(path: PathLike, options?: { recursive?: boolean; }): Promise<void>;
  • path
    路径,同样可以是Buffer
  • options
    • recursive
      递归删除
      ,相当于大名鼎鼎的
      rm -r
      建议体验这个命令,你就知道什么是递归删除了:
      rm -rf /
  • 没有返回值

stat() 获取文件(夹)状态

stat(path: PathLike, options?: { throwIfNoEntry?: boolean; }): Promise<NjsStats>;
  • path
    路径,同样可以是Buffer
  • options
    Object对象
    • throwIfNoEntry
      如果设置为
      true
      ,文件不存在时直接报错,否侧返回
      undefined
  • 返回NjsStat
    • isBlockDevice()
    • isCharacterDevice()
    • isDirectory()
    • isFIFO()
    • isFile()
    • isSocket()
    • isSymbolicLink()
    • dev
      : number 处于的文件系统ID
    • ino
      : number inode数量
    • mode
      : number 文件模式,8进制
    • nlink
      : number 这个文件实际地址硬链接数量,即引用数
    • uid
      : number 所有者User ID
    • gid
      : number 所有者Group ID
    • rdev
      : number 这个文件代表文件系统时表示此文件代表的文件系统ID
    • size
      : number 文件大小
    • blksize
      : number
    • blocks
      : number
    • atimeMs
      : number 最后访问时间戳
    • mtimeMs
      : number 最后修改文件修饰(模式)时间戳
    • ctimeMs
      : number 最后修改时间戳
    • birthtimeMs
      : number 创建时间
    • atime
      :
      Date
      ;
    • mtime
      :
      Date
      ;
    • ctime
      :
      Date
      ;
    • birthtime
      :
      Date
      ;
symlink(target: PathLike, path: PathLike): Promise<void>;
  • target
    目标(要创建软连接的)文件路径,同样可以是Buffer
  • path
    新建的软连接的路径,同样可以是Buffer
  • 没有返回值

writeFile和readFile 偷懒读/写文件的好方法

readFile(path: Buffer|string): Promise<Buffer>;
readFile(path: Buffer|string, options?: {
    flag?: "a" | "ax" | "a+" | "ax+" | "as" | "as+" | "r" | "r+" | "rs+" | "w" | "wx" | "w+" | "wx+"
}): Promise<Buffer>;
readFile(path: Buffer|string, options: {
    flag?: "a" | "ax" | "a+" | "ax+" | "as" | "as+" | "r" | "r+" | "rs+" | "w" | "wx" | "w+" | "wx+",
    encoding?: "utf8" | "hex" | "base64" | "base64url"
} | "utf8" | "hex" | "base64" | "base64url"): Promise<string>;
writeFile(path: Buffer|string, data: string | Buffer | DataView | TypedArray | ArrayBuffer, options?: {
    mode?: number;
    flag?: "a" | "ax" | "a+" | "ax+" | "as" | "as+" | "r" | "r+" | "rs+" | "w" | "wx" | "w+" | "wx+"
}): Promise<void>;

不多作介绍,看定义就行

请求(request)

请求,就是传入主函数的一个参数,函数由
export
导出和
js_import
导入以供nginx调用
这个是函数定义(main.js)

async main(h:NginxHTTPRequest):any;

这个是导出(main.js)

export { main };

这个是导入(nginx http)

js_import SCRIPT from 'main.js';

这个是使用(nginx location)

location /@api/{
    js_content SCRIPT.main;
}

这样,每当请求
/@api/
时,
main()
就会被调用,所有Promise完成时VM会被回收
这里讲4个很常用的技巧

args GET参数

h.args
是一个数组,官方是这么说的

Since 0.7.6, duplicate keys are returned as an array, keys are
case-sensitive, both keys and values are percent-decoded.
For example, the query string
a=1&b=%32&A=3&b=4&B=two%20words
is converted to r.args as:
{a: "1", b: ["2", "4"], A: "3", B: "two words"}

args会自动解码分割,允许重复且重复的会变成一个
Array

这里就很重要了,每一个请求你都需要检查你需要的arg是不是
Array

string
而不能认为只要不是
undefined
就是
string
,下面的代码就是最好的反例

if(typeof h.args.action != 'string')
    return h.return(400,'invaild request: Action should be defined');

当请求
/@api/?action=a&action=b
时,这个函数会错误报错,事实上
Action
已经定义

headersIO

h.headersIn

h.headersOut
是Nginx分割好的Header,你可以直接使用
但是这两个常量有很大的限制,必须是Nginx内部专门定义的Header才会出现
其中,
headersIn
的定义是这样的

readonly 'Accept'?: string;
readonly 'Accept-Charset'?: string;
readonly 'Accept-Encoding'?: string;
readonly 'Accept-Language'?: string;
readonly 'Authorization'?: string;
readonly 'Cache-Control'?: string;
readonly 'Connection'?: string;
readonly 'Content-Length'?: string;
readonly 'Content-Type'?: string;
readonly 'Cookie'?: string;
readonly 'Date'?: string;
readonly 'Expect'?: string;
readonly 'Forwarded'?: string;
readonly 'From'?: string;
readonly 'Host'?: string;
readonly 'If-Match'?: string;
readonly 'If-Modified-Since'?: string;
readonly 'If-None-Match'?: string;
readonly 'If-Range'?: string;
readonly 'If-Unmodified-Since'?: string;
readonly 'Max-Forwards'?: string;
readonly 'Origin'?: string;
readonly 'Pragma'?: string;
readonly 'Proxy-Authorization'?: string;
readonly 'Range'?: string;
readonly 'Referer'?: string;
readonly 'TE'?: string;
readonly 'User-Agent'?: string;
readonly 'Upgrade'?: string;
readonly 'Via'?: string;
readonly 'Warning'?: string;
readonly 'X-Forwarded-For'?: string;

这个是
headersOut

'Age'?: string;
'Allow'?: string;
'Alt-Svc'?: string;
'Cache-Control'?: string;
'Connection'?: string;
'Content-Disposition'?: string;
'Content-Encoding'?: string;
'Content-Language'?: string;
'Content-Length'?: string;
'Content-Location'?: string;
'Content-Range'?: string;
'Content-Type'?: string;
'Date'?: string;
'ETag'?: string;
'Expires'?: string;
'Last-Modified'?: string;
'Link'?: string;
'Location'?: string;
'Pragma'?: string;
'Proxy-Authenticate'?: string;
'Retry-After'?: string;
'Server'?: string;
'Trailer'?: string;
'Transfer-Encoding'?: string;
'Upgrade'?: string;
'Vary'?: string;
'Via'?: string;
'Warning'?: string;
'WWW-Authenticate'?: string;
'Set-Cookie'?: string[];

其中最需要注意的是
h.headersOut['Set-Cookie']
是一个数组
当然,大部分情况下这些Header足够你玩了,但是有的时候还是需要自定义的,这个时候
raw
开头的变量上场了

readonly rawHeadersIn: Array<[string, string|undefined]>;
readonly rawHeadersOut: Array<[string, string|undefined]>;

这些都是按照数组
[key, value]
排的,你可以用下面的代码快速找到你想要的

const headers = {} as Record<string, Array<string>>;
h.rawHeadersIn.forEach(item => item[0] in headers ? headers[item[0]].push(item[1]) : headers[item[0]] = [item[1]])
h['X-user-defined'][0]; // 你想要的

如果是自定义输出的话,第一个想到的是不是应该也是
h.rawHeadersOut
?
然而,我发现
官方的示例
中用的不是
rawHeadersOut
而是
headersOut
的确,我在
rawHeadersOut
这些东西的定义下面都发现了

[prop: string]: string | string[] | undefined;

这个让rawHeaders系列更加意味不明了,我也不清楚官方的做法
总之用
headersOut
准没错

用这些函数响应客户端

这个函数发送的是整个请求,调用后这个请求就结束了

return(status: number, body?: NjsStringOrBuffer): void;

这三个函数是用来搭配响应的,但是我不清楚
官方的用意

嘛,大部分时间还是别这么玩吧

sendHeader(): void;
send(part: NjsStringOrBuffer): void;
finish(): void;

NGINX的特色

internalRedirect(uri: NjsStringOrBuffer): void;
parent?: NginxHTTPRequest;
subrequest(uri: NjsStringOrBuffer, options: NginxSubrequestOptions & { detached: true }): void;
subrequest(uri: NjsStringOrBuffer, options?: NginxSubrequestOptions | string): Promise<NginxHTTPRequest>;
subrequest(uri: NjsStringOrBuffer, options: NginxSubrequestOptions & { detached?: false } | string,
           callback:(reply:NginxHTTPRequest) => void): void;
subrequest(uri: NjsStringOrBuffer, callback:(reply:NginxHTTPRequest) => void): void;

是不是很心动?的确,你可以使用
subrequest
分割任务,
internalRedirect
快速服务文件,
parent
在子请求内直接操纵响应
举个例子,你验证完Token想要发送给客户端一个文件

nginx.conf:

location /@files/{
    internal;
    alias /file/;
}

file.js

// ....
h.internalRedirect('/@files/' + file_path);
// 这个时候客户端就接收到了`/files/{file_path}`这个文件

Buffer系列

请注意这一句话

*** if it has not been written to a temporary file.

详情请参看我的这篇踩坑文章
https://hi.imzlh.top/2024/07/09.cgi
总之,这是Nginx的Buffer,而客户端的上传如果大于
client_body_buffer_size
会被写入文件并暴露在变量中
h.variables.request_body_file

readonly requestBuffer?: Buffer;
readonly requestText?: string;

需要注意的是,下面的两项是
subrequest
返回的内容而不会写入客户端Buffer
想要给客户端则需要这样:
r.return(res.status, res.responseText)
这个是Nginx官方的例子

readonly responseBuffer?: Buffer;
readonly responseText?: string;

输出到日志的函数

error(message: NjsStringOrBuffer): void;
log(message: NjsStringOrBuffer): void;
warn(message: NjsStringOrBuffer): void;

这些很好理解,就是
log
warn
error
三个等级的日志

这些函数不要碰

这些函数是
js_body_filter
才能使用的,对于新手像我一样找不到为什么出错的很致命

sendBuffer(data: NjsStringOrBuffer, options?: NginxHTTPSendBufferOptions): void;
done(): void;

其他你感兴趣的

  • httpVersion: string
    HTTP版本号
  • method: string
    HTTP方法,是大写的
  • remoteAddress: string
    客户端地址
  • uri: string
    请求的URL,在subrequest则是subrequest的URL
  • variables: NginxVariables
    Nginx变量,是UTF8字符串
  • rawVariables: NginxRawVariables
    Nginx变量,不同的是值是
    Buffer

全局命名空间

njs

NJS有一个全局命名空间
njs.*
,这里面的东西全局可用不分场合

  • version: string
    njs版本
  • version_number: number
    njs版本,字符串版本
  • on(event: "exit", callback: () => void): void
    VM退出时的回调
  • dump(value: any, indent?: number): string
    pre打印,输出到日志

ngx

还有一个命名空间叫做
ngx.*
,这里面的东西与nginx相关
东西太多,我就介绍最重要的

  • fetch(init: NjsStringOrBuffer | Request, options?: NgxFetchOptions): Promise<Response>
    和Web很像的
    fetch
    API,只是第二个参数大缩水了
    • body?: string
    • headers?: NgxHeaders
    • method?: string
    • verify?: boolean
      是否验证SSL证书,默认验证,不符合会报错
  • log(level: number, message: NjsStringOrBuffer): void
    写入到Nginx日志,level可以是这些
    • ngx.INFO
    • ngx.WARN
    • ngx.ERR
  • readonly shared: NgxGlobalShared
    共享池,这个很有用,重点介绍下
    当多个VM需要共享一个数据时,我们第一个想到的解决方法时数据库(DataBase)
    但是njs现在不支持数据库,作为过渡,这个
    shared
    就是解决方法
    通过共享池,共享同样的数据,再使用共享锁就可以实现了
    其中共享池名称 大小 类型由
    js_shared_dict_zone
    定义
    这些是可利用的所有函数
    • ngx.shared.[共享池名称].add()
    • ngx.shared.[共享池名称].capacity 共享池大小
    • ngx.shared.[共享池名称].clear()
    • ngx.shared.[共享池名称].delete()
    • ngx.shared.[共享池名称].freeSpace()
    • ngx.shared.[共享池名称].get()
    • ngx.shared.[共享池名称].has()
    • ngx.shared.[共享池名称].incr() 增大一个键对应的值的大小
    • ngx.shared.[共享池名称].items()
    • ngx.shared.[共享池名称].keys()
    • ngx.shared.[共享池名称].name
    • ngx.shared.[共享池名称].pop()
    • ngx.shared.[共享池名称].replace()
    • ngx.shared.[共享池名称].set()
    • ngx.shared.[共享池名称].size() 这个共享池元素的数量
    • ngx.shared.[共享池名称].type 类型
      string

      number
      ,由
      js_shared_dict_zone
      定义
  • worker_id
    工作进程的ID,对于定时任务指定很有效

大家好,我是码农先森。

PHP 唯一的爽点就是开发起来「哇真快」这刚好和外包公司的需求相契合,在 Web 领域的芒荒年代 PHP 以王者姿态傲视群雄。如果 PHP 敢说第二,就没有哪门子语言敢称第一,连 Java 都要礼让三分。我刚开始出来工作时,就误入了长沙一家常年在猪八戒网上接单的外包公司,公司所有的项目都用 PHP 来开发,前后端也不分离,团队成员都是全干工程师。

相信大多数的 PHPer 或多或少都有前后端通吃的经历,拥有这样类似伪全栈技术的 PHP 程序员,正是外包公司所热衷的,招聘的HR小姐姐都对你面面相觑。一上来就能干活,有时候还是一个人直接干一个项目,都省去了沟通的环节,那效率相当杠杠的。用武侠剧中的情节来形容就是「快刀斩乱马」,项目干好了就奖金SPA伺候,干不好就卷铺盖走人,多你一个不多,少你一个也不少。对于我们 PHPer 来说,反正人和项目只要有一个能跑就行,人员流动性极大,有可能还没认清同事的脸,隔天不是你消失就是他消失了。

常年游荡于外包公司的 PHPer 都是这个地方打一枪,那个地方放一炮。很多人说外包公司这不好那不好,确实我也认同外包公司对技术人员并不友好。但是不可否认外包公司也给很多人提供了饭碗,特别是那些从培训班速成的 PHPer,绝大多数都被外包公司所吸收了。外包公司既有功也就过,功是解决就业,过是造就了一批低水平一年经验重复十年的 PHPer,还写了一堆难以维护的代码,牵一发而动全身,还动不动在关键的地方写上 Sleep 美名其曰这是以后性能优化的点,正印证了 PHPer 时常自我调侃的一句话「开发一时爽,维护火葬场」。

回到本文的主题「PHP 程序员为什么依然是外包公司的香饽饽?」我想除了 PHP 开发快之外,还有就是 PHPer 的用人成本低。对于外包公司来说「效率为王,成本至上」的企业文化一直挂在墙头,尤其显眼。众所周知,外包公司接的项目都是被中间商层层扒皮,最后落到手的收益微薄的可怜。这种现象就断定了外包公司不可能会高薪招聘其他语言的程序员,因此 PHPer 正对了外包公司的胃口。

PHP 开发之所以快不仅是缘于语言本身简单灵活,还因于及其完善的 Web 开源生态环境。拥有大量流行著名的开源系统,比如博客系统 WordPress、织梦系统 DedeCms、论坛系统discuz,后台管理系统 FastAdmin、CatchAdmin 等等。其次还有众多的开源框架,比如 Laravel、YII、CI、ThinkPHP、Webman、Hyperf 等。网上现成的解决方案也比比皆是,满足了外包公司的拿来即用主义,简直爽的不行,甚至可能还是零开发成本,直接一键部署,三下五除二,完事收工!

PHPer 和外包公司的关系,就类似网上的一句广告语「你刚好需要,我正好专业」。外包公司需要考虑效率、成本,而 PHPer 正好高效率、低成本,直接撞到了外包公司的心坎里去了,那在他们眼中 PHPer 能不香吗?

时过境迁,外包公司只能是 PHPer 的一个过客,不要把外包公司当成家。茫茫人生路,道阻且长,对于 PHPer 来说要想在技术这条路获取职业上的长足发展,就需要去往更高的平台,比如产品研发、技术研发导向型的公司。

希望对大家能有所启发,也欢迎在评论区发表不同观点,感谢阅读。


欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。

Zabbix

【1】、简介

  • zabbix是一个高度集成的监控解决方案
  • 可以实现企业级的开源分布式监控
  • Zabbix通过C/S模式采集监控数据
  • Zabbix通过B/S模式实现Web管理
  • 实施监控的几个方面
    • 数据采集:使用agent(可安装在软件的系统上)、SNMP(简单网络管理协议,用于网络设备的数据采集)
    • 数据存储:使用MySQL数据库
    • 数据展示:通过web页面
  • Zabbix通过在远程主机上安装agent进行数据采集。存储到MySQL数据库,通过web页面进行展示

【2】、安装Zabbix6.0

https://www.zabbix.com/cn/download?zabbix=6.0&os_distribution=rocky_linux&os_version=8&components=server_frontend_agent&db=mysql&ws=nginx

image-20240702225016408

环境准备

主机名 IP
ZabbixServer 192.168.121.5/24
web1 192.168.121.100/24
web2 192.168.121.200/24
# 1、更新软件仓库,清除缓存
rpm -Uvh https://repo.zabbix.com/zabbix/6.0/rhel/8/x86_64/zabbix-release-6.0-4.el8.noarch.rpm
dnf clean all

# 2、装Zabbix server,Web前端,agent、数据库MySQL
dnf install -y zabbix-server-mysql zabbix-web-mysql zabbix-nginx-conf zabbix-sql-scripts zabbix-selinux-policy zabbix-agent mysql-server

# 3、创建初始数据库
mysql
mysql> create database zabbix character set utf8mb4 collate utf8mb4_bin;
mysql> create user zabbix@localhost identified by 'zabbix';
mysql> grant all privileges on zabbix.* to zabbix@localhost;
mysql> set global log_bin_trust_function_creators = 1;
mysql> quit;

# 4、导入初始架构和数据
cp /usr/share/zabbix-sql-scripts/mysql/server.sql.gz .
gzip -d server.sql.gz 
mysql -uzabbix -pzabbix < server.sql

【3】、配置zabbix

1、数据收集

配置zabbix-server

# 修改数据库的密码
[root@ZabbixServer:192.168.121.5 ~]$vim  +129 /etc/zabbix/zabbix_server.conf
DBPassword=zabbix

配置zabbix-agent

# 修改主机名
[root@ZabbixServer:192.168.121.5 ~]$vim /etc/zabbix/zabbix_agentd.conf
Hostname=ZabbixServer

2、数据呈现

主配置文件:/etc/nginx/nginx.conf

php配置文件:/etc/nginx/default.d/php.conf

php和nginx连接配置文件:/etc/nginx/conf.d/php-fpm.conf

nginx提供的zabbix的页面配置文件:/etc/nginx/conf.d/zabbix.conf

将 /etc/nginx/conf.d/zabbix.conf的第二行和第三行的注释取消

3、启动服务

---
- name: start server
  hosts: ZabbixServer
  tasks:
    - name: start server
      service:
        name: "{{item}}"
        state: started
        enabled: yes
      loop:
        - zabbix-server
        - zabbix-agent
        - nginx
        - php-fpm

4、访问web页面

# /etc/nginx/conf.d/zabbix.conf,写着nginx监听的端口
server {
        listen          8080;
        server_name     example.com;

访问
http://192.168.121.5:8080

在第一次访问时,需要先进行设置、配置、安装

image-20240703093331071

【4】、在客户端安装zabbi-agent

# web1、web2
[root@web2:192.168.121.200 ~]$rpm -Uvh https://repo.zabbix.com/zabbix/6.0/rhel/8/x86_64/zabbix-release-6.0-4.el8.noarch.rpm
[root@web2:192.168.121.200 ~]$yum install -y zabbix-agent

# 修改zabbix-agent的配置文件
vim /etc/zabbix/zabbix-agentd.conf
117 Server=192.168.121.5,127.0.0.1 # 允许192.168.121.5和本机监控自己
# 启动zabbix-agent
systemctl restart zabbix-agent

【5】、web页面端操作

zabbix 服务端 zabbix_server 默认使用 10051 端口,客户端 zabbix_agent2 默认使用 10050 端口。

1、添加监控主机

image-20240703140958851

image-20240703141111958

常见报错情况1

这通常是由于web1端的zabbix-agent没有启动,导致10050端口没有开启,所以会出现连接被拒绝的情况,当前也有可能因为防火墙的问题

image-20240703141334977

常见报错情况2

这是由于权限被拒绝导致的报错。大概是因为zabbix-server不能连接到zabbix-agent,通常是由于zabbix-agent.conf编写错误导致的,需检查配置文件

image-20240703141536989

2、系统自带监控项

  • Space utilization:以百分比的形式显示磁盘利用率
  • Used space:已用磁盘空间
  • Available memory:可用内存
  • CPU idle time:CPU空闲时间不易过低
  • Load average(1m avg)、Load average(5m avg)、Load average(15m avg):CPU1分钟、5分钟、15分钟的平均负载。这个值不应长期大于核心数
  • Interface eth0:Bits received:网卡接收到的数据流量
  • Interface eth0:Bits sent:网卡发送的数据量
  • Number of processes:系统运行的进程数
  • Number of logged in users:已登录的用户数

image-20240703142935282

image-20240703143123673

image-20240703143347952

【6】、自定义监控项

1、在被控端创建key,被控端被监控的内容叫做key,可以理解为他就是一个变量名,具体的名字自己决定

2、在web页面中创建监控项。监控项对于key值

graph LR;
A[模板]--->|包含|B[监控项]--->|对应|C[key]

1、实现监控web1用户数量的监控项

# 创建key的语法,命令的执行结果就是value
UserParameter=自定义key值,命令


# 1、创建自定义配置文件,文件名自定义
[root@web1:192.168.121.100 /etc/zabbix/zabbix_agentd.d]$vim usercnt.conf
UserParameter=usercnt,sed -n '$=' /etc/passwd
[root@web1:192.168.121.100 /etc/zabbix/zabbix_agentd.d]$systemctl restart zabbix-agent.service

# 2、验证添加的配置项
# 在web1和ZabbixServer上安装zabbix-get软件包,安装好后使用zabbix_get命令检查能否看到新增的key
[root@web1:192.168.121.100 ~]$yum install -y zabbix-get
[root@web1:192.168.121.100 ~]$zabbix_get -s 127.0.0.1 -k usercnt
31
# 在ZabbixServer上测试
[root@ZabbixServer:192.168.121.5 /]$zabbix_get -s 192.168.121.100 -k usercnt
31
# 让ZabbixServer也可以监控用户数量
[root@web1:192.168.121.100 ~]$scp /etc/zabbix/zabbix_agentd.d/usercnt.conf root@192.168.121.5:/etc/zabbix/zabbix_agentd.d/usercnt.conf
[root@ZabbixServer:192.168.121.5 /]$systemctl restart zabbix-server.service 
[root@ZabbixServer:192.168.121.5 /]$systemctl restart zabbix-agent.service 
[root@ZabbixServer:192.168.121.5 /]$zabbix_get -s 127.0.0.1 -k usercnt
34

【7】、配置告警

  • 默认情况下,监控项不会自动发送告警消息
  • 需要配置触发器与告警,并且通过通知方式发送信息给联系人
  • 触发器:设置条件,当条件达到时,将会执行某个动作
  • 动作:触发器条件达到后要采取的行为,比如发邮件或执行命令

1、用户数超过40,发送警告邮件

  • 当web1的用户数量超过30时,会认为这是一个问题(problem)
  • 当出现问题时,将会执行动作
  • 执行的动作时给管理员发邮件
  • 给管理员发邮件,还要配置邮件服务器地址,以及管理员Email地址

(1)、创建触发器规则

在一个模板中添加触发器

image-20240703225322567

image-20240703225858190

image-20240703225918368

点击添加,进行配置

image-20240703230007615

配置完成后添加

image-20240703230234571

(2)、创建邮件类型的报警媒介

选择email电子媒介

image-20240703230340864

image-20240703230420346

(3)、为用户关联邮箱

image-20240703230549735

image-20240703230619291

image-20240703230643477

image-20240703230656505

(4)、创建动作

当出现Problem状态时,给admin发邮件

image-20240703230805737

image-20240703230824554

image-20240703230838166

image-20240703230845288

image-20240703230853660

回到“创建动作”页面后,点击“操作”:

image-20240703230914524

image-20240703230924847

image-20240703230934983

(5)、在zabbixserver上配置邮件服务

yum install -y postfix mailx
systemctl enable postfix --now

# 在web1创建用户,使总用户数超过50
[root@web1 ~]# for user in user{1..5}
> do
> useradd $user
> done

# 查看Linux中的日志
[root@zabbixserver ~]# mail   # 查看邮件 
Heirloom Mail version 12.5 7/5/10.  Type ? for help.
"/var/spool/mail/root": 2 messages 2 new
>N  1 zzg@tedu.cn           Sat Dec 31 16:47  21/932   "Problem: usercnt_gt_50"
 N  2 zzg@tedu.cn           Sat Dec 31 16:48  21/932   "Problem: usercnt_gt_50"

image-20240703231138893

2、添加钉钉机器人

(1)、添加钉钉机器人

image-20240704092026358

image-20240704092035789

image-20240704092050302

image-20240704092129142

image-20240704092228134

编写脚本并测试

# 安装钉钉机器人脚本需要用到的模块 
[root@zabbixserver ~]# yum install -y python3-requests
  
# 编写钉钉机器人脚本
[root@zabbixserver ~]# vim /usr/lib/zabbix/alertscripts/dingalert.py 
#!/usr/bin/env python3
import json
import requests 
import sys
def send_msg(url, remiders, msg): 
    headers = {'Content-Type': 'application/json; charset=utf-8'} 
    data = { 
        "msgtype": "text", 
        "at": { 
            "atMobiles": remiders, 
            "isAtAll": False, 
        }, 
        "text": { 
            "content": msg, 
        } 
    } 
    r = requests.post(url, data=json.dumps(data), headers=headers)
    return r.text 
if __name__ == '__main__': 
    msg = sys.argv[1] 
    remiders = [] 
    url = '钉钉机器人Webhook地址'  # 注意此处需输入机器人的webhook地址 
    print(send_msg(url, remiders, msg))
[root@zabbixserver ~]# chmod +x /usr/lib/zabbix/alertscripts/dingalert.py 

# 注意消息中要包含关键字warn
[root@zabbixserver ~]# /usr/lib/zabbix/alertscripts/dingalert.py 'warn: 晚上吃什么' 
{"errcode":0,"errmsg":"ok"}

image-20240704092502742

(2)、添加报警媒介类型

image-20240704093211457

image-20240704092919100

添加消息模板

image-20240704093235432

image-20240704093246095

(3)、添加到用户

image-20240704093431720

image-20240704093454228

image-20240704093522553

(4)、添加触发器

image-20240704093848215

image-20240704093910570

(5)、添加动作

image-20240704094042291

image-20240704094119707

每间隔一分钟发一次消息

image-20240704094217205

(6)、测试

[root@ZabbixServer:192.168.121.5 ~]$for i in {1..8}
> do
> useradd user${i}
> done
[root@ZabbixServer:192.168.121.5 ~]$zabbix_get -s 127.0.0.1 -k usercnt
43

image-20240704094402067

image-20240704094527068

【8】、自动发现

  • 当被监控的设备非常多的时候,手工添加见会变得非常不方便
  • 可以使用自动发现功能,实现添加主机、添加到主机组、链接模板
  • 自动发现流程
    • 创建自动发现规则
    • 创建动作,当主机被发现后,执行什么操作
    • 通过动作,添加主机,将模板应用到发现的主机

1、创建自动发现规则

image-20240704103414515

image-20240704103905110

2、创建发现动作

image-20240704104115687

image-20240704104159883

image-20240704104312211

【9】、主动监控

  • 默认zabbix使用的是被动监控,住被动监控都是针对被监控主机而言的
  • 被动监控:Server向Agent发起请求,索取监控数据,此种模式常用
  • 主动监控:Agent向Server发起请求,向Server汇报

1、修改配置文件

[root@web2 ~]# vim /etc/zabbix/zabbix_agentd.conf  
117 # Server=127.0.0.1,192.168.88.5 
142 StartAgents=0
171 ServerActive=192.168.88.5
242 RefreshActiveChecks=120
 
# 重启服务 
[root@web2 ~]# systemctl restart zabbix-agent.service  
	# 端口号消失
[root@web2 ~]# ss -tlnp | grep :10050 

image-20240704112251407

image-20240704112257719

image-20240704112309271

【10】、拓扑图

  • 在zabbix中可以搭建拓扑图,反映拓扑结构

image-20240704114857985

image-20240704114910980

  • 添加元素:
    • 地图元素:可以是一个装饰图形,也可以对应具体的设备
    • 形状:长方形、圆形
    • 链接:连线,多个设备之间才能使用链接

1、完善拓扑图

  • 添加交换机图标

image-20240704151755477

image-20240704151805705

  • 添加链接:按住ctrl键,点选两台设备,点链接边上的添加

image-20240704151829685

  • 添加web服务器

image-20240704151846590

【11】、监控nginx

1、stub_status模块

  • 用于实时监控ngnix的网络链接,这个模块是nginx官方提供的一个模块
# 配置nginx,在nginx配置文件中加入
location /status {
    stub_status on;
}

# 访问测试
[root@web1:192.168.121.100 ~]$curl http://192.168.121.100/status
Active connections: 1 
server accepts handled requests
 1 1 1 
Reading: 0 Writing: 1 Waiting: 0 
# Active connections:当前客户端与nginx之间的连接数。它等于下面Reading / Writing / Waiting之和
# accepts:自nginx启动之后,客户端访问的总量
# handled:自nginx启动之后,处理过的客户端连接总数,通常等于accepts的值。
# requests:自nginx启动之后,处理过的客户端请求总数。
# Reading:正在读取客户端的连接总数。
# Writing:正在向客户端发送响应的连接总数。
# Waiting:空闲连接

2、编写脚本,获取各项数据

#!/bin/bash


case $1  in
Active)
		curl -s  http://192.168.121.100/status | awk '/Active/{print $NF}'
		;;
accepts)
		curl -s  http://192.168.121.100/status | awk 'NR==3{print $1}'
		;;
handled)
		curl -s  http://192.168.121.100/status | awk 'NR==3{print $2}'
		;;
requests)
		curl -s  http://192.168.121.100/status | awk 'NR==3{print $3}'
		;;
Reading)
		curl -s  http://192.168.121.100/status | awk '/Reading/{print $2}'
		;;
Writing)
		curl -s  http://192.168.121.100/status | awk '/Writing/{print $4}'
		;;
Waiting)
		curl -s  http://192.168.121.100/status | awk '/Waiting/{print $NF}'
		;;
esac

设置key值

# key的语法格式
 
UserParameter=key[*],<command> $1
 
# key[*]中的*是参数,将会传给后面的位置变量$
UserParameter=nginx_status[*],/usr/local/bin/nginx_status.sh $1
[root@web1:192.168.121.100 ~]$zabbix_get -s 127.0.0.1 -k nginx_status[Active]
1

3、在zabbix上创建监控项

image-20240704161904052

image-20240704161912471

image-20240704161920159

image-20240704161929838

三、Prometheus

  • Prometheus是一个开源的监控和报警工具包,最初由SoundCloud构建
  • 也是一款监控软件,也是一个时序数据库(tsdb)。Prometheus将其指标收集并存储为时间序列数据,即指标信息与记录时的时间戳以及成为标签的可选键值对一起存储
  • 主要使用在容器监控方面,也可以用于常规的主机监控
  • 主要使用Google公司开发的go语言编写
  • Prometheus是一个框架,可以和其他组件完美结合

image-20240705082520827