Golang笔记–05–网关SDK
文章目录
前言
如果直接通过UDP连接到服务器,可能不太稳定。最初基于这个考虑将一个Go程序内置到了网关上,在本地处理UDP数据包,转换为JSON或ProtocolBuffer数据后,通过MQTT上报服务器。
最初只有自己远程控制的需求,就就做了一个通过MQTT下发shell脚本,网关上执行命令返回响应的功能,权限和自由度都很高,但也有风险,执行错误、阻塞、异常操作都可能让网关SDK挂掉。
前几个月在有需求的情况下重新实现一些远程控制的功能,这里做个总结,涉及到的内容包括
- MQTT协议
- 遗嘱机制
- 同步执行回调
- 权限管理
- Linux系统
- 系统状态
- SYS_SYSINFO
- SYS_UNAME
- SYS_STATFS64
- 网络路由
- 默认网卡
- 网络延迟
- systemd与init.d
- Linux可执行文件格式
- 开机启动、自动重启与错误回滚
- 程序升级与日志切割
- 执行shell命令
- 系统状态
- arm与mipsle架构
MQTT协议
遗嘱机制
遗嘱机制只对异常掉线有效,例如拔掉网线,虽然客户端与服务端在心跳时间内都无法感知到对方,但只要发包或者超时后就会触发,而手动关闭程序会产生握手,无法触发遗嘱机制,导致网关状态与数据库不同步。
解决方式是在程序启动后和关闭前,增加应用层设备上下线推送。
同步执行回调
客户端订阅Topic的回调默认同步执行(paho库),假如在回调中执行耗时很长的shell脚本,会阻塞其他topic回调执行,间接导致ping包超时,网关失联。
解决方式是将耗时操作异步执行,并且对竞争性资源加锁。
权限管理
mosquitto内部有用户认证和Topic权限管理两部分的缓存,起始握手会验证用户账号/密码,发布、订阅消息时会检查用户对Topic的权限。
解决方式是服务端程序和mosquitto之间通过数据库共享用户权限信息。
但是msoquitto认证插件的作者停止维护了......没有解决方式,只能fork一个repo,并冻结一个测试过的mosquitto版本,以防出问题时需要修复。
Linux系统
一般需要监控什么呢?监控系统状态(系统信息、主机名、磁盘空间),网络路由(4g、有线、网络延迟),程序升级与回滚,日志切割,执行shell命令...换句话说,这里需要直接使用系统调用,就像调用内核的API
系统状态
系统调用文件位置:syscall目录下的zsyscall_系统_架构.go的文件
数据结构文件位置:syscall目录下的ztypes_系统_架构.go的文件
这里只讨论linux系统下的arm与mipsle架构,那么涉及到的文件有
- syscall/ztypes_linux_arm.go
- syscall/ztypes_linux_mipsle.go
- syscall/zsyscall_linux_arm.go
- syscall/zsyscall_linux_mipsle.go
SYS_SYSINFO
获取启动时间、内存、系统负载相关信息
字段含义:http://man7.org/linux/man-pages/man2/sysinfo.2.html
数据结构如下,两个架构的数据结构是一致的
syscall/ztypes_linux_arm.go
1type Sysinfo_t struct {
2 Uptime int32
3 Loads [3]uint32
4 Totalram uint32
5 Freeram uint32
6 Sharedram uint32
7 Bufferram uint32
8 Totalswap uint32
9 Freeswap uint32
10 Procs uint16
11 Pad uint16
12 Totalhigh uint32
13 Freehigh uint32
14 Unit uint32
15 X_f [8]uint8
16}
syscall/ztype_linux_mipsle.go
1type Sysinfo_t struct {
2 Uptime int32
3 Loads [3]uint32
4 Totalram uint32
5 Freeram uint32
6 Sharedram uint32
7 Bufferram uint32
8 Totalswap uint32
9 Freeswap uint32
10 Procs uint16
11 Pad uint16
12 Totalhigh uint32
13 Freehigh uint32
14 Unit uint32
15 X_f [8]int8
16}
这里补充一点关于Load的计算,一般执行top、uptime、htop命令时,会显示系统在最近三个时间点内(1分钟、5分钟、15分钟)的负载,但系统调用取回的却是三个整数,如何转换成常见的浮点数?还是看文档
http://www.hungry.com/~jamie/cs421/unixcpu/loadl.html
总的来说,就是需要除以一个基准值,这里是65536,内核会生成计算后的数值,其他工具如uptime等也是直接读取计算后的结果
SYS_UNAME
获取主机名相关信息
字段含义:http://man7.org/linux/man-pages/man2/uname.2.html
数据结构如下,两个架构的数据结构是不一致,需要增加额外转换函数,由于Go的限制,需要存放到按 名称_系统_架构.go 的形式命名文件才能自动调用
syscall/ztypes_linux_arm.go
1type Utsname struct {
2 Sysname [65]uint8
3 Nodename [65]uint8
4 Release [65]uint8
5 Version [65]uint8
6 Machine [65]uint8
7 Domainname [65]uint8
8}
syscall/ztype_linux_mipsle.go
1type Utsname struct {
2 Sysname [65]int8
3 Nodename [65]int8
4 Release [65]int8
5 Version [65]int8
6 Machine [65]int8
7 Domainname [65]int8
8}
SYS_STATFS64
获取文件系统信息
字段含义:http://man7.org/linux/man-pages/man2/statfs.2.html
数据结构如下,两个架构的数据结构是不一致,但计算磁盘空间需要的参数类型一致
syscall/ztypes_linux_arm.go
1type Statfs_t struct {
2 Type int32
3 Bsize int32
4 Blocks uint64
5 Bfree uint64
6 Bavail uint64
7 Files uint64
8 Ffree uint64
9 Fsid Fsid
10 Namelen int32
11 Frsize int32
12 Flags int32
13 Spare [4]int32
14 Pad_cgo_0 [4]byte
15}
syscall/ztype_linux_mipsle.go
1type Statfs_t struct {
2 Type int32
3 Bsize int32
4 Frsize int32
5 Pad_cgo_0 [4]byte
6 Blocks uint64
7 Bfree uint64
8 Files uint64
9 Ffree uint64
10 Bavail uint64
11 Fsid Fsid
12 Namelen int32
13 Flags int32
14 Spare [5]int32
15 Pad_cgo_1 [4]byte
16}
网络路由
默认网卡
当一个数据包发出时,是通过4G网卡还是通过有线网卡发送?
有了系统状态相关的开发经验,在获取网络路由时首先就想到系统调用获取路由表,复习了路由表相关的内容后开始动手,然而还是想当然了。当我抽丝剥茧,下探到底层时,发现Go并没有提供这种封装,而且不同系统的命令支持不一样,数据结构不一样,系统调用不一样...显然工作量有点超出预期。
这里换一个取巧的方式,尝试建立与远程主机的UDP连接,这时系统会分配一个本地IP与本地端口,然后遍历本地网卡获取IP,与系统分配的IP对比。若一致,则该网卡为网络路由使用的网卡,这样可以就可以轻松获取到默认网卡,而且不需要直接使用系统调用。
网络延迟
需要使用以下两个不在标准库的包
1"golang.org/x/net/icmp"
2"golang.org/x/net/ipv4"
实现方式是,发起一个ICMP请求,设置超时时间,然后读取响应,从发送数据到接收响应(收到响应或者超时)的总时长即为网络延迟
systemd与init.d
没有systemd的时候才会想念起systemd的好,当你需要管理程序的自启动、错误恢复、升级失败回滚等,就会发现启动一个程序是多么困难。另外程序的返回码、错误输出、标准错误输出等由systemd管理的部分也需要逐一处理,由于是第一次接触,遇到了很多问题
可执行文件格式
elf是linux平台下的默认可执行程序格式,下面是执行file命令,查看nginx可执行文件的输出
1file /usr/sbin/nginx
2/usr/sbin/nginx: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=74a2d2803389b209573e71f387ac72a32f0cd771, stripped
如果需要更详细的内容,可以使用 readelf 命令
1➜ ~ readelf
2Usage: readelf <option(s)> elf-file(s)
3 Display information about the contents of ELF format files
4 Options are:
5 -a --all Equivalent to: -h -l -S -s -r -d -V -A -I
6 -h --file-header Display the ELF file header
7 -l --program-headers Display the program headers
8 --segments An alias for --program-headers
9 -S --section-headers Display the sections' header
10 --sections An alias for --section-headers
11 -g --section-groups Display the section groups
12 -t --section-details Display the section details
13 -e --headers Equivalent to: -h -l -S
14 -s --syms Display the symbol table
15 --symbols An alias for --syms
16 --dyn-syms Display the dynamic symbol table
17 -n --notes Display the core notes (if present)
18 -r --relocs Display the relocations (if present)
19 -u --unwind Display the unwind info (if present)
20 -d --dynamic Display the dynamic section (if present)
21 -V --version-info Display the version sections (if present)
22 -A --arch-specific Display architecture specific information (if any)
23 -c --archive-index Display the symbol/file index in an archive
24 -D --use-dynamic Use the dynamic section info when displaying symbols
25 -x --hex-dump=<number|name>
26 Dump the contents of section <number|name> as bytes
27 -p --string-dump=<number|name>
28 Dump the contents of section <number|name> as strings
29 -R --relocated-dump=<number|name>
30 Dump the contents of section <number|name> as relocated bytes
31 -z --decompress Decompress section before dumping it
32 -w[lLiaprmfFsoRt] or
33 --debug-dump[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames,
34 =frames-interp,=str,=loc,=Ranges,=pubtypes,
35 =gdb_index,=trace_info,=trace_abbrev,=trace_aranges,
36 =addr,=cu_index]
37 Display the contents of DWARF2 debug sections
38 --dwarf-depth=N Do not display DIEs at depth N or greater
39 --dwarf-start=N Display DIEs starting with N, at the same depth
40 or deeper
41 -I --histogram Display histogram of bucket list lengths
42 -W --wide Allow output width to exceed 80 characters
43 @<file> Read options from <file>
44 -H --help Display this information
45 -v --version Display the version number of readelf
在Go的标准库里,/src/debug/elf 目录下的包可以读取可执行程序的信息,这里提了这么多,只是为了实现一个功能,获取可执行文件的架构信息。
由于Go支持交叉编译,如果将一个arm架构的程序错误地上传到mipsle架构的设备上,是一个很麻烦的问题,在程序自升级时,需要检查下载的文件,可以结合平台信息做一次检验。
开机启动、自动重启与错误回滚
支持systemd的系统
- 编写service文件,放置在 /lib/systemd/system 目录下,然后使用systemctl启用开机启动,自动创建 /etc/systemd/system 目录下的软链接
- 自动重启的条件、重启间隔在service文件中设置,若未设置,默认自动重启、重启间隔为100ms
- 错误回滚使用ExecStopPost配置的脚本,可以检查程序退出码,执行回滚或者忽略
systemd相关文档:https://www.freedesktop.org/software/systemd/man/systemd.service.html
支持init.d的系统
- 编写init.d启动脚本,放置在 /etc/init.d 目录下,脚本里需要包括启动优先级、启动命令、停止命令及重启命令,开机启动使用 /etc/init.d/xxxx enable 命令启动
- 自动重启的条件、重启间隔、错误回滚直接由自己编写的脚本处理并控制
- 需要配置目标程序的标准输出、标准错误输出,Linux环境下子进程默认继承父进程的标准输出与标准错误输出,在开发时遇到的过程序标准错误输出为nil,一打印日志就crash,无法正常启动
程序升级与日志切割
将这两个东西放在一起讲,是因为它们都利用了Linux文件系统和磁盘存储的特性。
程序升级的实现
- 下载新文件
- 重命名老文件
- 重命名新文件
- 退出程序,由外部脚本重启
日志切割的实现
- 程序监听自定义系统信号
- logroate程序执行日志文件重命名后向程序发送信号
- 程序收到信号后关闭文件描述符并打开新文件
我们的脚本、命令中都是以文件名引用一个文件,而进程是以文件描述符引用文件。之前遇到过一个问题,手动删除一个容器的日志文件后,磁盘空间并没有释放,执行 docker logs 命令甚至还可以看到持续的日志输出,而重启容器后,日志丢失、磁盘空间释放。
这里引用一段阮一峰博客中的内容:
由于inode号码与文件名分离,这种机制导致了一些Unix/Linux系统特有的现象。
- 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。
- 移动文件或重命名文件,只是改变文件名,不影响inode号码。
- 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。
更详细的内容可以查看这篇博客:理解inode
执行shell命令
我觉得这篇文章足以涵盖大部分的需求,但是即使读了文章,我还是掉进了坑里
文章地址:Advanced command execution in Go with os/exec
- 运行命令 vs 运行命令并展示输出:前者会得到一个返回码,后者会得到可执行程序/脚本的标准输出或标准错误输出,若子进程会持续打印结果到标准输出/标准错误输出,则后者会一直阻塞到程序退出为止
- 若启动子程序的父进程缺失默认标准输出与标准错误输出,子进程也会同样缺失
- 若需要将日志输出到文件,则需要自定义日志框架的标准输出与标准错误输出,结合文件系统特性实现日志切割
arm与mipsle架构
Go支持了许多不同架构的机器,但并不意味者同一段可以在arm平台上运行的代码能够在mipsle平台上正常执行,必须注意
- 依赖硬件特性的库可能失效或转为软件实现,例如sync/atomic库、加密库等
- 栈长度异常,同一段代码出现在arm上可以完整执行,在mipsle上随机停止在某一个位置,但使用函数调用将长段的代码分割后执行却正常
- 变量捕获异常(间接由栈长度异常导致),同一个变量在arm上可以被 go func(){xxxxx}() 捕获,而在mipsle上却提示为空,将函数封装后,复制变量再执行却正常
还有一个无奈的问题,设计功能时需要考察机器配置,例如磁盘写入速度,在树莓派上能够保持10MB/S,但在一些路由器上,却只有200KB/S,足足相差50倍,同样可以推导其他涉及硬件的性能,例如内存、CPU、网卡等。