分类 编程相关 下的文章

最近做的MicroPython小项目,需要用到ping命令的功能,检查指定IP的电脑是否有开机并联网。

找了一圈,找到这个ping功能的源码。没有深究代码,就是能用。而且跟常用的ping命令差不多。

源码没改,直接搬过来,避免以后找不到:

# µPing (MicroPing) for MicroPython
# copyright (c) 2018 Shawwwn <shawwwn1@gmail.com>
# License: MIT
#
# Internet Checksum Algorithm
# Author: Olav Morken
# https://github.com/olavmrk/python-ping/blob/master/ping.py
# @data: bytes
#
# source code from: https://forum.pycom.io/topic/4930/ping-command/15
#
def checksum(data):
    if len(data) & 0x1: # Odd number of bytes
        data += b'\0'
    cs = 0
    for pos in range(0, len(data), 2):
        b1 = data[pos]
        b2 = data[pos + 1]
        cs += (b1 << 8) + b2
    while cs >= 0x10000:
        cs = (cs & 0xffff) + (cs >> 16)
    cs = ~cs & 0xffff
    return cs

def ping(host, count=4, timeout=5000, interval=10, quiet=False, size=64):
    import utime
    import uselect
    import uctypes
    import usocket
    import ustruct
    import uos

    # prepare packet
    assert size >= 16, "pkt size too small"
    pkt = b'Q'*size
    pkt_desc = {
        "type": uctypes.UINT8 | 0,
        "code": uctypes.UINT8 | 1,
        "checksum": uctypes.UINT16 | 2,
        "id": (uctypes.ARRAY | 4, 2 | uctypes.UINT8),
        "seq": uctypes.INT16 | 6,
        "timestamp": uctypes.UINT64 | 8,
    } # packet header descriptor
    h = uctypes.struct(uctypes.addressof(pkt), pkt_desc, uctypes.BIG_ENDIAN)
    h.type = 8 # ICMP_ECHO_REQUEST
    h.code = 0
    h.checksum = 0
    h.id[0:2] = uos.urandom(2)
    h.seq = 1

    # init socket
    sock = usocket.socket(usocket.AF_INET, usocket.SOCK_RAW, 1)
    sock.setblocking(0)
    sock.settimeout(timeout/1000)
    try:
        addr = usocket.getaddrinfo(host, 1)[0][-1][0] # ip address
    except IndexError:
        not quiet and print("Could not determine the address of", host)
        return None
    sock.connect((addr, 1))
    not quiet and print("PING %s (%s): %u data bytes" % (host, addr, len(pkt)))

    seqs = list(range(1, count+1)) # [1,2,...,count]
    c = 1
    t = 0
    n_trans = 0
    n_recv = 0
    finish = False
    while t < timeout:
        if t==interval and c<=count:
            # send packet
            h.checksum = 0
            h.seq = c
            h.timestamp = utime.ticks_us()
            h.checksum = checksum(pkt)
            if sock.send(pkt) == size:
                n_trans += 1
                t = 0 # reset timeout
            else:
                seqs.remove(c)
            c += 1

        # recv packet
        while 1:
            socks, _, _ = uselect.select([sock], [], [], 0)
            if socks:
                resp = socks[0].recv(4096)
                resp_mv = memoryview(resp)
                h2 = uctypes.struct(uctypes.addressof(resp_mv[20:]), pkt_desc, uctypes.BIG_ENDIAN)
                # TODO: validate checksum (optional)
                seq = h2.seq
                if h2.type==0 and h2.id==h.id and (seq in seqs): # 0: ICMP_ECHO_REPLY
                    t_elasped = (utime.ticks_us()-h2.timestamp) / 1000
                    ttl = ustruct.unpack('!B', resp_mv[8:9])[0] # time-to-live
                    n_recv += 1
                    not quiet and print("%u bytes from %s: icmp_seq=%u, ttl=%u, time=%f ms" % (len(resp), addr, seq, ttl, t_elasped))
                    seqs.remove(seq)
                    if len(seqs) == 0:
                        finish = True
                        break
            else:
                break

        if finish:
            break

        utime.sleep_ms(1)
        t += 1

    # close
    sock.close()
    ret = (n_trans, n_recv)
    not quiet and print("%u packets transmitted, %u packets received" % (n_trans, n_recv))
    return (n_trans, n_recv)

最近利用“合宙ESP32C3-Core”做了个MicroPython的小项目,用到了HTTP请求的功能。

找到了以下文章介绍urllib.urequest。据说代码少、性能好,能满足80%的需求。能支持HTTPS的请求。但使用下来,发现功能不完善,最大问题是不支持超时(timeout)设置。

折腾一番后,改为使用urequests,相关功能都比较完善,可以设置timeout、请求头等。

源码没改,保存为文件名urequests.py即可:

"""
code from: https://github.com/micropython/micropython-lib/blob/master/python-ecosys/urequests/urequests.py
version: a3d6d29b1b9de2bb147e0751c08a39608ebe06c8
"""
import usocket


class Response:
    def __init__(self, f):
        self.raw = f
        self.encoding = "utf-8"
        self._cached = None

    def close(self):
        if self.raw:
            self.raw.close()
            self.raw = None
        self._cached = None

    @property
    def content(self):
        if self._cached is None:
            try:
                self._cached = self.raw.read()
            finally:
                self.raw.close()
                self.raw = None
        return self._cached

    @property
    def text(self):
        return str(self.content, self.encoding)

    def json(self):
        import ujson

        return ujson.loads(self.content)


def request(
    method,
    url,
    data=None,
    json=None,
    headers={},
    stream=None,
    auth=None,
    timeout=None,
    parse_headers=True,
):
    redirect = None  # redirection url, None means no redirection
    chunked_data = data and getattr(data, "__iter__", None) and not getattr(data, "__len__", None)

    if auth is not None:
        import ubinascii

        username, password = auth
        formated = b"{}:{}".format(username, password)
        formated = str(ubinascii.b2a_base64(formated)[:-1], "ascii")
        headers["Authorization"] = "Basic {}".format(formated)

    try:
        proto, dummy, host, path = url.split("/", 3)
    except ValueError:
        proto, dummy, host = url.split("/", 2)
        path = ""
    if proto == "http:":
        port = 80
    elif proto == "https:":
        import ussl

        port = 443
    else:
        raise ValueError("Unsupported protocol: " + proto)

    if ":" in host:
        host, port = host.split(":", 1)
        port = int(port)

    ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM)
    ai = ai[0]

    resp_d = None
    if parse_headers is not False:
        resp_d = {}

    s = usocket.socket(ai[0], usocket.SOCK_STREAM, ai[2])

    if timeout is not None:
        # Note: settimeout is not supported on all platforms, will raise
        # an AttributeError if not available.
        s.settimeout(timeout)

    try:
        s.connect(ai[-1])
        if proto == "https:":
            s = ussl.wrap_socket(s, server_hostname=host)
        s.write(b"%s /%s HTTP/1.0\r\n" % (method, path))
        if not "Host" in headers:
            s.write(b"Host: %s\r\n" % host)
        # Iterate over keys to avoid tuple alloc
        for k in headers:
            s.write(k)
            s.write(b": ")
            s.write(headers[k])
            s.write(b"\r\n")
        if json is not None:
            assert data is None
            import ujson

            data = ujson.dumps(json)
            s.write(b"Content-Type: application/json\r\n")
        if data:
            if chunked_data:
                s.write(b"Transfer-Encoding: chunked\r\n")
            else:
                s.write(b"Content-Length: %d\r\n" % len(data))
        s.write(b"Connection: close\r\n\r\n")
        if data:
            if chunked_data:
                for chunk in data:
                    s.write(b"%x\r\n" % len(chunk))
                    s.write(chunk)
                    s.write(b"\r\n")
                s.write("0\r\n\r\n")
            else:
                s.write(data)

        l = s.readline()
        # print(l)
        l = l.split(None, 2)
        if len(l) < 2:
            # Invalid response
            raise ValueError("HTTP error: BadStatusLine:\n%s" % l)
        status = int(l[1])
        reason = ""
        if len(l) > 2:
            reason = l[2].rstrip()
        while True:
            l = s.readline()
            if not l or l == b"\r\n":
                break
            # print(l)
            if l.startswith(b"Transfer-Encoding:"):
                if b"chunked" in l:
                    raise ValueError("Unsupported " + str(l, "utf-8"))
            elif l.startswith(b"Location:") and not 200 <= status <= 299:
                if status in [301, 302, 303, 307, 308]:
                    redirect = str(l[10:-2], "utf-8")
                else:
                    raise NotImplementedError("Redirect %d not yet supported" % status)
            if parse_headers is False:
                pass
            elif parse_headers is True:
                l = str(l, "utf-8")
                k, v = l.split(":", 1)
                resp_d[k] = v.strip()
            else:
                parse_headers(l, resp_d)
    except OSError:
        s.close()
        raise

    if redirect:
        s.close()
        if status in [301, 302, 303]:
            return request("GET", redirect, None, None, headers, stream)
        else:
            return request(method, redirect, data, json, headers, stream)
    else:
        resp = Response(s)
        resp.status_code = status
        resp.reason = reason
        if resp_d is not None:
            resp.headers = resp_d
        return resp


def head(url, **kw):
    return request("HEAD", url, **kw)


def get(url, **kw):
    return request("GET", url, **kw)


def post(url, **kw):
    return request("POST", url, **kw)


def put(url, **kw):
    return request("PUT", url, **kw)


def patch(url, **kw):
    return request("PATCH", url, **kw)


def delete(url, **kw):
    return request("DELETE", url, **kw)

最后,用的时候要注意:

  1. 响应结果需要手工关闭,避免再发起请求时会报错。
  2. POST的数据,需要进行URL编码。
  3. 除非POST的数据是JSON,否则最好加上请求头“Content-type”。

示例代码如下:

import urequests

r = urequests.post("https://abc.com/path", data='id=123&name=apple', timeout=10, headers={'User-Agent': 'Micropython(ESP32C3)', 'Content-type': 'application/x-www-form-urlencoded'})
print(r.status_code)  # 打印相应状态,整数,正常为200
print(r.content)  # 打印响应数据
r.close() #  关闭连接

使用Termux多年了。主要是可以安装N多Linux命令,一个应用即可实现N多功能。目前最新版是0.118.0,推荐从F-Droid安装。

这里记录一下相关的初始化动作。

1 相关资料

2 设置国内源

跟大多数Linux发行版类似,软件源设置为国内镜像,有效提高安装、更新软件的效率。官方收录了软件源的中国镜像网站:

Termux Packages - Mirrors Hosted in China

我选择了清华大学的镜像:

Termux | 镜像使用帮助 | 清华大学开源软件镜像站

跟Debian类似,直接修改apt的source.list文件即可。即修改$PREFIX/etc/apt/sources.list文件的内容如下:

# The termux repository mirror from TUNA:
deb https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-main stable main
deb https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-root root stable
deb https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-x11 x11 main

保存后,执行apt update,更新一下本地的缓存。

3 安装Termux工具

执行以下命令,安装Termux的相关工具。如果默认安装了,则不用再装。

pkg install termux-tools

4 开启储存访问

执行termux-setup-storage即可。对应Android的sdcard目录为~/storage/shared

5 关闭震动

本人不喜欢以震动方式提示错误,习惯关闭它。编辑文件~/.termux/termux.properties,末尾添加:

bell-character = ignore

6 设置辅助键盘(the extra keys)

官方详细说明: Termux Wiki - Extra Keys Row

值得一提,可以设置按键上滑,让辅助键盘拥有更多的功能。参考配置,修改文件~/.termux/termux.properties,末尾添加:

extra-keys = [ \
   [{key: ESC, popup: {macro: "CTRL d", display: "exit"}}, \
   {key: '/', popup: '\'}, \
   {key: '-', popup: '_'}, \
   {key: '(', popup: '{'}, \
   {key: UP, popup: PGUP}, \
   {key: ')', popup: '}'}, \
   {key: QUOTE, popup: '*'}], \
   [{key: TAB, popup: ':'}, \
   {key: CTRL}, \
   {key: ALT}, \
   {key: LEFT, popup: HOME}, \
   {key: DOWN, popup: PGDN}, \
   {key: RIGHT, popup: END}, \
   {key: APOSTROPHE, popup: '`'}] \
]

7 开启root

  • 如果系统已root,安装tsu,可以切换root用户,或者使用sudo以root用户执行命令。
pkg install tsu
sudo 命令
  • 如果Android系统没有root,安装proot,即可执行需要root权限的命令。
pkg install proot
proot 命令

8 安装vim及配置

本人习惯vim,其它编辑程序,可以选择nanoemacs等。安装命令如下:

pkg install vim

重点配置,编辑文件~/.vimrc,末尾添加:

" 解决中文显示
set fileencodings=utf-8,gb2312,gb18030,gbk,ucs-bom,cp936,latin1
set enc=utf8
set fencs=utf8,gbk,gb2312,gb18030

" 显示行号
set nu

" 颜色主题
colorscheme desert

" 语法高亮
syntax on

最后使配置生效:

source .vimrc

9 Termux相关应用

  • Termux:API

    从Termux访问Android功能。可以实现脚本控制Android。
  • Termux:Boot

    允许程序在启动时运行的Termux扩展应用。
  • Termux:Float

    在浮动终端窗口中使用Termux。
  • Termux:Styling

    自定义Termux终端的样式。
  • Termux:Tasker

    从Tasker运行Termux脚本的Tasker插件。需要配合Tasker使用。
  • Termux:Widget

    从主屏幕启动Termux命令。实现通过点击桌面图标执行相关的sh脚本。

10 其它常用命令

  • OpenSSH

    • 说明:完整的SSH客户端。
    • 安装命令:pkg install openssh
  • ADB

    • 说明:可以使用ADB连接本机,无需root权限就能执行input命令等。
    • 安装命令:pkg install android-tools

终于用上了Windows 11,版本是22h2。记录一下针对开发人员的相关优化。

1 安装时跳过TPM限制

安装过程,在提示“这台电脑无法安装Windows11”的界面,按Shift + F10,弹出CMD窗口输入“regedit”,打开注册表编辑器。

在注册表编辑器进入[HKEY_LOCAL_MACHINE\SYSTEM\Setup],新建“项”,名为“LabConfig”(注意大小写一致)。

在“LabConfig”下,新建两个“DWORD (32位)值”,如下(记得注意大小写一致):

  • 数值名称:BypassTPMCheck,数值数据:00000001,基数:十六进制(H)
  • 数值名称:BypassSecureBootCheck,数值数据:00000001,基数:十六进制(H)

完成后关闭“注册表编辑器”、“CMD窗口”,按返回上一步的按钮,再按下一页,就通过系统限制检测。

2 安装时避免强制登录账户

按Shift + F10,弹出CMD窗口输入“regedit”,打开注册表编辑器。

在注册表编辑器进入[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE],新建一个“DWORD (32位)值”,如下(记得注意大小写一致),如下。

  • 数值名称:BypassNRO,数值数据:00000001,基数:十六进制(H)

退出注册表编辑器后,输入命令“logoff”,即可即可跳过强制联网登录账号。

如果不想编辑注册表,在CMD窗口输入命令“oobe\BypassNRO.cmd”,系统重启后即可。

3 创建本地用户

安装成功后,需要创建本地用户。最好不要登录微软账户。

最简单的是创建和使用管理员账号(Administrator)登录,避免访问其它硬盘的已存在文件时,提示需要管理员账号。

如果使用自定义账号(即拥有管理员权限的非Administrator账号),需要修改电脑上已存在文件的权限,改为新增账号拥有“完全控制”的权限。如果存在大量零碎文件,这个过程超级漫长。

4 解锁任务栏

在Windows 11中,用户无法通过“任务栏”使用“快速启动工具栏”,通过安装“ExplorerPatcher”解锁相关功能。

ExplorerPatcher - GitHub
https://github.com/valinet/ExplorerPatcher

我整理了一堆快捷方式,大多是常用的,利用“快速启动工具栏”,实现快速点击,类似开始菜单的效果。

5 优化内存占用

1)关闭不必要的自启动

依次进入:设置 -> 应用 -> 启动。把不需要自启软件关闭。

2)关闭动画效果

依次进入:设置 -> 辅助功能 -> 视觉效果。“动画效果”设置关闭。这个优化,大概省了1GB内存。

6 恢复旧的右键菜单

新的右键菜单过于简单,隐藏了太多的功能。使用管理员运行CMD,输入以下命令。执行成功后重启,即可看到效果。

reg.exe add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve

7 安装 WinMerge

开源的文件对比和合并工具,用于替代Beyond Compare。

启用“高级菜单”,选择文件对比时更方便。运行WinMerge,依次进入“编辑”->“选项”->左侧菜单点“系统集成”,在“资源管理器”下,勾选“添加到上下文菜单”、“启用高级菜单”。

8 显示多个时区的时间

依次进入:设置 -> 时间和语言 -> 日期和时间 -> 相关链接 -> 附加时钟。设置完毕后按“确定”。建议设置以下时间:

  • (UTC) 协调世界时。

设置完成后,点击任务栏的时间,即可看到新增的时区时间。

9 Office软件

目前使用开源的LibreOffice,代替微软Office,用着还行。

10 安装WSL2

Window上运行Linux软件,包括GUI软件。这样连SSH客户端都不用安装了。

主要过程是,开启Windows功能、安装内核、安装Linux发行版。相关文档如下:

11 解决不能自动关机

关机时,会提示有进程正在运行,需要点按钮才能关机。这问题导致使用shutdown命令和远程桌面都不能关机。

解决方案:打开注册表,进入“\HKEY_USERS.DEFAULT\Control Panel\Desktop”,新建“字符串值(S)”,名称为“AutoEndTasks”,值为“1”。

Nmon (Nigel's Monitor) 是AIX系统与Linux 系统上,开源免费的监控资源的工具。Njmon则是其下一代的形态。

Njmon的主要特点是:

  • 采用JSON保存数据。
  • 原生支持发送数据到InfluxDB。
  • 可配置不监控的数据。
  • 不支持终端显示数据。
  • 添加了相关工具,包括njmond、nmeasure、njmonchart等。

由于想玩玩InfluxDB,于是按照官方建议,部署了Njmon + InfluxDB + Grafana。然后总结以下缺点:

  • Njmon对Linux支持不足。比如Debian系统,需要自己编译。但是编译过程没什么困难。
  • Njmon虽然支持InfluxDB v2+,但Grafana上没找到能直接使用的模板。Grafana找到的模板是针对InfluxDB v1。
  • 相对Nmon,Njmon参考资料比较少。但是官方有提供Youtube视频教程。

Njmon + InfluxDB + Grafana的部署要点:

  1. njmon命令运行于“nimon”模式,直接连接到InfluxDB时,需要加参数-I
  2. njmon命令连接InfluxDB v2+,需要使用-O 组织名称 -T token这两个参数进行验证。

    # 示例,njmon连接到InfluxDB v2
    sudo ./njmon_Debian11_unknown_v80 -I -s 1 -i 127.0.0.1 -p 8086 -x bucket -O 组织名 -T xxxxxxxxxxx
  3. InfluxDB v2+同时提供v1接口时,需要创建对应数据库(Database)和保存策略(Retention Policies)。

    # 示例,InfluxDB v2创建对应的v1数据库和保存策略
    influx v1 dbrp create --db bucket-db --rp bucket-rp --bucket-id xxxxxx --default --org '组织名' --token 'xxxxxxxxxxx'
  4. InfluxDB可以使用Docker部署。

    # 示例,使用Docker部署InfluxDB v2.4.0
    docker run --name influxdb -p 8086:8086 -v /opt/influxdb/config.yml:/etc/influxdb2/config.yml --volume /opt/influxdb/data:/var/lib/influxdb2 influxdb:2.4.0
  5. Grafana如果没找到相关模板,只能找接近的,再自己修改一下。

总结。体验过InfluxDB + Grafana,挺爽的。只是,如果是用作监控,那么采集数据端不一定使用Njmon。

dstat是一个Linux监控工具。可定制采集数据,可设置采集频率,可输出字符界面和导出CSV。默认一秒一条监测数据。其中以top开头的参数,可以记录检测类型最大值的进程。比如--top-cpu记录CPU占用最大的命令,--top-cpu-adv还会记录CPU占用最大的进程ID等。参数--time的时间格式,需要通过环境变量DSTAT_TIMEFMT进行定义。

示例操作命令:

# 设置时间格式
export DSTAT_TIMEFMT='%Y-%m-%d %H:%M:%S'

# 执行监测,并导出CSV文件
dstat --time --cpu --mem --disk --io --net --sys --top-cpu-adv --top-mem --top-bio-adv --top-io-adv --output /opt/dstat_log/dstat_$(date +%Y%m%d).csv

使用时,可结合tmux,随时查看其采集数据,即时输出在终端。导出的CSV文件,需要下载到本地,并使用第三方工具生成图表。

在众多监控方案中,dstat不算优秀的解决方案,而且只有采集数据的功能。其记录数据,采用CSV格式。如果终端不够宽时,不能完整显示每行的采集数据。而且CSV格式不好扩展,比如--top-cpu-adv记录的数据,不适合机器理解。这里记录一下相关经验。

1 正式版的bug

安装过0.7.3和0.7.4两个版本,并使用Python3运行,都存在以下两个Bug。幸好是使用Python开发,可以直接修复。其安装路径为/usr/bin/dstat

a)在Debian 10以上使用Python3运行时,出现以下Bug:

/usr/bin/dstat:2619: DeprecationWarning: the imp module is deprecated in favour of importlib and slated for removal in Python 3.12; see the module's documentation for alternative uses
  import imp
Terminal width too small, trimming output.
Traceback (most recent call last):
  File "/usr/bin/dstat", line 2847, in <module>
    main()
  File "/usr/bin/dstat", line 2687, in main
    scheduler.run()
  File "/usr/lib/python3.10/sched.py", line 151, in run
    action(*argument, **kwargs)
  File "/usr/bin/dstat", line 2806, in perform
    oline = oline + o.showcsv() + o.showcsvend(totlist, vislist)
  File "/usr/bin/dstat", line 547, in showcsv
    if isinstance(self.val[name], types.ListType) or isinstance(self.val[name], types.TupleType):
NameError: name 'types' is not defined. Did you mean: 'type'?

解决办法,参考以下文档:

简单来说,改两行代码。如下:

# 第547行,改为:
if isinstance(self.val[name], (tuple, list)):

# 第552行,改为:
elif isinstance(self.val[name], str):

b)--top-mem参数统计错误的bug

参考文章:

修改方法def proc_splitline(filename, sep=None),改为:

if filename.startswith("/proc/") and filename.endswith("/stat") and filename != "/proc/stat":
    tmp = linecache.getline(filename, 1).split(sep)
    it = [i for i,c in enumerate(tmp) if c.endswith(')')]
    it = 2 if not it else it[-1]+1
    return tmp[0:1] + [' '.join(tmp[1:it])] + tmp[it:]
else:
    return linecache.getline(filename, 1).split(sep)

2 应用场景

感觉比较适合单机版,或者指定采集一些系统数据。不适合生产机大规模部署。如果非要用dstat不可,可以考虑 dstat + Fluentd + Influxdb + Grafana 这种组合方案。

最近因工作需要,研究了一下Linux服务器的监控方案,收获颇丰。

1 监控需求

服务器需要监控什么?可以分为硬件和软件,或者系统数据和业务数据。一般的监控解决方案,都是针对硬件、操作系统和常用软件(比如数据库、Docker之类)。涉及业务数据,需要二次开发。

2 监控方案设计

一般的监控方案,分为采集、存储、展示、告警,这四大模块或者功能。针对被监控服务器的数量,可灵活实施四个模块的部署方式。比较完善的整体解决方案,还包括“控制”模块,实现服务器集群的统一管理。

2.1 采集

数据采集程序,或者叫“探针”,一般是部署在被监控服务器上的程序。用于采集相关数据,要求占用系统资源小(主要是CPU、内存、磁盘、网络等),对系统影响小。

数据采集的方式,可以采用“推”(push)和“拉”(pull)模式。

“推”是数据采集程序主动把数据从采集端发到存储端。数据具有良好的实时性,方便内网部署并推送到外网服务器。但采集端太多,或者采集的数据比较大,需要考虑存储端的承受能力。遇到存储端没有收到数据时,不能确定是网络问题还是采集端问题。

“拉”是数据采集程序暴露出来,例如开放HTTP服务的端口,存储端去访问并获取数据。存储端也有更多的主动权,决定拉取频率,甚至决定采集样本,能降低带宽、减轻存储端的压力。采集不到数据时,能区分网络问题和采集端问题。如果对数据有实时需求,采集端需要保存未拉走的数据。

针对采集端有时只能部署在内网的情况,有的解决方案会提供“代理”或者“跳板”功能,实现采集端与存储断之间的数据连接。

2.2 存储

数据存储,就是把监控数据持久化,可以是文件(比如CSV文件),也可以是数据库(比如MySQL、InfluxDB)。目前主流的方案,基本采用时序数据库,例如InfluxDB。专门针对这种大量连续时间的数据,提供存储、查询、统计等功能。

2.3 展示

数据展示,一般是把监控数据生成图表,以便更直观地查看和分析。简单的方案是用Microsoft Excel之类的软件,根据导出CSV文件的数据,生成各种图表。主流和灵活的方案是使用可视化软件,例如Grafana,连接时序数据库并生成各种图表。Grafana能够实现实时展示和历史分析。

2.4 告警

通过检测采集的数据,发现超过指定危险指标时,向相关人员发送消息,就是告警。由于相关人员一般不会24小时盯着服务器,所以需要机器进行告警。InfluxDB、Prometheus、Grafana等都有告警功能。一般开源系统只提供邮件或Web Hook(调用钉钉接口)通知,商业系统(例如:阿里云的云监控)会有短信或电话通知。

3 解决方案

服务器监控的解决方案,像编程语言一样,没有一个万能方案,需要根据情况进行选择。这里列举一些相关软件或方案。

3.1 dstat

基于Python。默认一秒采集一条数据,数据定制性高。数据可显示在终端,也可导出CSV文件。需要使用第三方软件,例如Microsoft Excel之类,生成图表和分析数据。高级玩法是,搭配Fluentd,保存数据到InfluxDB,再用Grafana展示、分析。

优点是占用资源小,数据简单。缺点是由于使用CSV格式,复杂数据记录得不够好。而且当前版本(0.7.4)有bug,部署时需要自己修正。

3.2 njmon

C语言开发。nmon的升级版,可设置不收集的数据,数据格式采用JSON。不支持终端显示,原生支持导出文件和发送到InfluxDB。有官方工具处理保存的JSON文件。推荐的玩法是njmon + InfluxDB + Grafana。

优点是占用资源小,作者对整个监控方案考虑比较全面。缺点是对AIX较好但对Linux支持不足。比如Debian 11需要自己编译。Grafana的njmon模板大多针对AIX,而且仅有的Linux模板是针对InfluxDB v1,即使用InfluxQL而不是Flux。

3.3 glances

基于Python,开源跨平台,界面优秀。支持三种模式:单独运行、C/S、Web。提供XML-RPC服务、RESTful JSON接口,也可把数据保存到其它系统,包括InfluxDB。本身支持配置“Actions”,根据事件触发相应脚本,实现告警。

占用资源较大(包括CPU和磁盘空间),界面优秀,玩法多。适合桌面系统的监控。

3.4 InfluxDB

InfluxDB只是个时序数据库,但是该公司开发了Telegraf作为数据收集工具(采用“推”模式),并且InfluxDB新增了告警和图形化展示,形成一个完整的数据收集方案。

3.5 Prometheus

基于Go,监控、告警工具,使用“拉”模式采集数据。

3.6 Zabbix

企业级的开源的服务器监控管理系统,是完整的解决方案,基本可以替代阿里云的云监控之类的系统。Web控制台基于PHP,支持中文显示;采集端基于C,升级版改为Go;数据存储使用MySQL,未支持时序数据库是最突出的缺点。另外,告警功能不支持电话和短信通知。

非常适合企业内部管理服务器集群,便于运维人员使用。

3.7 阿里云的云监控

一般云主机的服务商都提供云监控功能,且基本监控免费,高级功能收费。阿里云的云监控,还能监控非阿里云的主机。使用这些云监控前,要确定是否可以把服务器监控数据发给云厂商,甚至安装云厂商的采集软件。

4 方案选型

根据不同的情况,总结一下各个方案的选型。

4.1 云主机

如果是购买云主机,可以考虑云服务提供商的云监控,一般免费提供基础监控功能,例如阿里云的云监控。但前提是,云主机可以安装云监控的采集端软件,并且接受相关数据上传到云服务提供商那边。另外,高级监控功能,需要付额使用。

4.2 运维管理的服务器集群

企业内部,有专门的运维人员管理服务器,针对硬件或操作系统相关数据的监控,则可以考虑Zabbix。

4.3 业务数据

node_exporter + Prometheus + Grafana,或者 Telegraf + InfluxDB + Grafana,这种方案适合收集业务日志。部署了InfluxDB,还能存储其它数据,个人觉得比较好玩。

近来项目遇到要显示SVG图像文件。本来SVG已不是新鲜事物,应该很好使用,现实并非如此。

设计说用AI(Adobe Illustrator)做出来的图,用Chrome打开会出现错误,比如图形位置不对、该显示的图形没显示等。用文本文件打开那图,会显示xml标签开头,而不是svg标签开头。

图片显示有误的问题,只能让设计去修正了。至于文件格式的问题,找到svgcleaner这个工具,转换一下就好了,体积还有所减少。虽然svgcleaner貌似不更新了,但工作正常。

以前一直用Inkscape代替曾经的Flash,设计矢量图,比如图标之类,还不错。不知道为什么大厂出品的AI会是这么多问题。

近来遇到关于库存与并发的问题。由于一直接触的系统都是没有考虑过商品库存的并发,加上解决过的并发问题,也只是简单直接地采用锁表的方式。所以导致踩坑。

1 问题1,商品基础数据与库存数量,设计在同一个表。

商品基础数据,包括库存数量,主要用于查询。但库存数量,还要解决经常变化,且可能出现并发的情况。如果简单使用锁,即使只锁一行数据,也会导致正在进行下单(涉及扣减库存)的商品不能被浏览(因为锁住,不能查询)。

为了减轻这个情况,下单时,检查库存数量是否足够购买时,不锁数据,等到保存订单数据,真正扣减库存时才加锁。本想着通过减少锁数据的时间,减少商品数据不能查询的情况。但是系统采用Java开发,使用了Spring + Hibernate框架。而Hibernate在事务内使用了一级缓存,即事务内未提交时,查询到的业务数据都放到一级缓存。事务内查询时,会先查询一级缓存,若命中,则不再查询数据库。就导致了检查库存时已获取了商品数据,扣减库存时(从一级缓存获取)不能获取到最新库存(特别是两个客户同时下单同一个商品的情况),最后在并发情况下扣减库存,就出现库存扣少1了的问题。

解决方案很简单,把商品基础数据与库存数据分开两个表存放。库存数据在扣减时,不影响商品浏览。

2 问题2,库存数量,需减少锁定时间。

由于客户浏览商品,或者添加商品到购物车,都需要查询库存数据。如果使用悲观锁,即锁表或锁数据后不能查询,会导致客户不能浏览。参考了以下文章,决定使用乐观锁,即不使用数据库锁。

目前系统规模比较小,且没有涉及分布式,于是决定在扣减库存时直接更新数据的方式。即使用update语句扣减库存时,用where条件判断是否足够扣减,并返回是否扣减成功。

由于使用MySQL,update语句不能返回指定数据(但是,sql server可以使用update...output,PostgreSQL可用update...returning)。加上Hibernate不能同时执行update和select两个语句,最后采用存储过程。参考以下网址:

3 解决方案

总的来说,使用乐观锁(即没有使用数据库的锁),并利用MySQL存储过程实现扣减库存后返回结果。

1)库存表

create table `product_stock` (
    `productId` bigint not null comment '商品ID',
    `instock` int not null default '0' comment '库存数量',
    `createTime` datetime(3) default null comment '创建时间',
    `updateTime` datetime(3) default null comment '更新时间',
    primary key ( productId )
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_0900_ai_ci comment='商品库存';

2)扣减库存的存储过程

利用存储过程的out参数,返回扣减结果。当outUpdateQty返回的值大于零,扣减成功,否则失败。扣减成功,outStockAfter的值才是正确。

delimiter //
create procedure `product_reduce_instock`(
    in inProductId bigint, /*传入参数:商品ID*/
    in inReduceQty int, /*传入参数:扣减数量*/
    out outUpdateQty int, /*传出参数:实际扣减数量*/
    out outStockAfter int /*传出参数:更新后库存数量*/
)
begin
    -- 初始化返回的值
    set @updateQty=0;
    set @stockAfter=0;
    
    -- 执行扣减库存
    update product_stock 
    set instock = (@stockAfter := instock - (@updateQty := inReduceQty)), updateTime = now() 
    where productId = inProductId and instock >= inReduceQty;
    
    -- 传出参数赋值,即返回扣减结果
    set outUpdateStock=@updateQty;
    set outStockAfter=@updateQty;
end //

本文记录了从jQurey转到原生JavaScript开发的相关处理。

一 历史

二十一世纪初,IE 6还在统治浏览器的时代,出现了一批JavaScript框架。除了提高前端开发效率,还屏蔽了各个浏览器的JavaScript接口差异。那时有3个产品印象比较深刻:

  1. prototype,http://prototypejs.org/
    其特点是在原生JavaScript基础上做扩展,定义通用的方法或接口,屏蔽各个浏览器的差异。很轻量,个人比较喜欢。
  2. Ext JS,https://www.sencha.com/products/extjs/
    数据与界面分离,提供丰富的UI组建,便于页面开发。当时浏览器JavaScript性能不高,用起来不够流畅,不适合简单排版布局的页面。但是对于开发一些管理系统,确实很方便。
  3. jQuery,https://jquery.com/
    最大特别是查找HTML元素很方便(前提是熟悉其搜索语法),有点函数式编程的味道。在那个需要手工修改HTML界面的年代,确实很方便。

二 当前

看看当前的浏览器,已经是Webkit内核的天下,加上IE已亡,ECMAScript 6普及……各个浏览器的JavaScript兼容性大大提高。所以,我们可以直接采用浏览器原生JavaScript,替代jQuery这类用于遍历或搜索DOM的框架。当然,复杂的界面,主要是响应式前端框架(AngularJS、React、VUE)的世界。

三 实现方法

主要参考这个文章,从jQuery转到原生JavaScript。

另外,对于页面上的异步请求(ajax),该文章没有提出timeout的处理。以下整理一个示例:

// 请求错误的类,用于传递错误信息
let RespError = class {
  constructor(code, msg, respJson) {
    this.code = code;
    this.msg = msg;
    this.respJson = respJson;
  }
};

// POST提交Json数据。调用ajaxJson方法前加上async就是同步调用,直接调用就是异步调用
// 默认超时10秒
let ajaxJson = async (url, formParam={}, onSuccess=(respJson)=>{}, 
    onFailed=(respError)=>{}, timeoutSec=10) => {
  let controller = new AbortController();
  let timeoutId = setTimeout(() => {
    // 超时后停止请求
    controller.abort();
    // 抛出超时的错误
    onFailed(new RespError(-1, 'TIMEOUT', null));
  }, timeoutSec * 1000);
  try {
    // 发起请求
    let response = await fetch(url, {
        signal: controller.signal, // 用于接收中断请求信号
        method: 'POST',
        cache: 'no-cache',
        headers: {
          // 声明请求的参数是JSON
          'Content-Type': 'application/json; charset=UTF-8'
        },
        body: JSON.stringify(formParam)
    });
    // 注意,响应的数据只能获取一次,包括response.json()和response.text()
    let respJson = await response.json();
    if(!response.ok) {
      // 请求失败,抛出自定义的错误对象
      throw new RespError(response.status, response.statusText, respJson);
    }
    onSuccess(respJson);
  } catch(err) {
    onFailed(err instanceof RespError ? err : new RespError(-1, err.message, null));
  } finally {
    // 请求结束,停止执行定时函数。避免相应成功后,抛出超时的错误。
    clearTimeout(timeoutId);
  }
};

关于Fetch的使用,参考:

  1. Fetch API 教程
    https://www.ruanyifeng.com/blog/2020/12/fetch-tutorial.html
  2. mdn web docs - 使用 Fetch
    https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch