分类 Linux 下的文章

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,还能存储其它数据,个人觉得比较好玩。

最近接触了两款开源、跨平台、支持多种SQL数据库的数据库管理工具,值得记录一下。

DBeaver
官网:https://dbeaver.io/
2020年疫情期间,在家办公,想找个数据库管理工具,可以在Linux上访问SQL Server数据库,于是遇到DBeaver。界面像Eclipse,容易上手;基于Java,可以跨平台使用;使用JDBC,几乎支持所有数据库。在Linux上,几乎是万能的数据库管理工具了。

HeidiSQL
官网:https://www.heidisql.com/
在Windows上安装MariaDB 10.4.12时,发现自带了HeidiSQL数据库管理工具。界面及操作都跟MySQL Workbench相似,清晰明了,而且支持各种SQL数据。比较意外的是,其基于Delphi开发,所以Linux上需要利用Wine运行。

昨天发现Debian服务器上的Aria2居然不能下载https的链接,才发现编译安装时,忘了设置开启SSL的参数。还是记录一下,以免后面又犯错了。

关于编译安装的教程,可以直接查看官方说明:
https://github.com/aria2/aria2/blob/master/README.rst
https://aria2.github.io/manual/en/html/README.html#how-to-build

1. 安装相关依赖

详见官方文档。注意的是,Linux上,开启SSL,要安装openssllibssl-dev

2. 编译

官方文档已经很详细了,总结脚本如下:

$ git clone https://github.com/aria2/aria2.git
$ cd aria2
$ ./configure --without-gnutls --with-openssl
$ make
$ sudo cp ./src/aria2c /usr/local/bin/

3. 部署服务

关于Aria2配置文件的说明,参考官方文档:
https://aria2.github.io/manual/en/html/aria2c.html#aria2-conf
示例配置文件如下(参考路径:/etc/aria2/aria2.conf):

#OPTIONS
#下载路径
dir=/opt/aria2_download
#log路径
log=/var/log/aria2/aria2.log
#log-level: debug, info, notice, warn or error
log-level=warn
console-log-level=warn
#session
input-file=/var/cache/aria2/aria2.session
#最大下载数,默认5
max-concurrent-downloads=5
#校验完整性,只在bt下有效果,默认false
check-integrity=true
#断点续传
continue=true
 
#HTTP/FTP/SFTP Options
#同时连接的服务器数量,默认1
max-connection-per-server=5
#最大尝试次数,默认5
max-tries=20
#最小文件分割大小,默认20M
#min-split-size=20M
#单个文件最大线程,默认5
#split=5
#超时时间,默认60
#timeout=60
 
#BitTorrent Specific Options
#启用本地发现
bt-enable-lpd=true
#hash校验种子,默认true
bt-hash-check-seed=true
#最大打开文件数量,默认100
bt-max-open-files=200
#单个种子最大连接数
bt-max-peers=100
#在磁力下载中,保留torrent文件
bt-save-metadata=true
#监听端口,默认6881-6999
#listen-port=6881-6999
#最大上传限制,0是无限制
max-overall-upload-limit=100K
#下载完成后做种的设置
seed-ratio=1.0
seed-time=120
#bt-tracker=需要相关的服务地址
 
#RPC Options
#启用rpc
enable-rpc=true
#允许所有访问
rpc-allow-origin-all=true
#监听所有网络
rpc-listen-all=true
#监听端口
rpc-listen-port=6800
#rcp保存上传的元数据,默认false
rpc-save-upload-metadata=true
 
#Advanced Options
#下载时覆盖已经存在的文件,默认false
allow-overwrite=false
#此选项为true可能会导致下载进度丢失,默认false
allow-piece-length-change=true
#总是尝试恢复下载,默认true
always-resume=true
#指定dns服务器
#async-dns=true
#async-dns-server=8.8.4.4,208.67.222.222
#如果文件存在,自动重命名,仅适用于http,ftp
auto-file-renaming=true
#自动保存间隔,控制文件保存在.aria2中
auto-save-interval=60
#作为守护进程启用
daemon=true
#禁用ipv6
disable-ipv6=true
#磁盘缓存,默认16M
disk-cache=16M
#文件是否启用预先分配,默认prealloc
file-allocation=falloc
#最大下载结果在内存中保留数量,默认1000
max-download-result=500
#最大失败重试次数,默认0
max-resume-failure-tries=0
#下载完成时候执行的脚本
#on-bt-download-complete=/etc/aria2/on-bt-download-complete
#on-download-complete=/etc/aria2/on-download-complete
#on-download-error=/etc/aria2/on-download-error
#总体下载速度限制
max-overall-download-limit=1024K
#单个下载最大速度限制
max-download-limit=1024K
#保存下载进度,很有用的配置
save-session=/var/cache/aria2/aria2.session
#保存间隔,默认0
save-session-interval=60

# token验证
rpc-secret=123456

Systemd的服务配置文件(参考路径:/etc/systemd/system/aria2.service):

[Unit]
Description=Aria2 Service
After=network.target

[Service]
Type=forking
User=www-data
Group=www-data
WorkingDirectory=/var/cache/aria2
ExecStart=/usr/local/bin/aria2c --conf-path=/etc/aria2/aria2.conf -D
ExecReload=/usr/bin/kill -HUP $MAINPID
RestartSec=1min
Restart=on-failure

[Install]
WantedBy=multi-user.target

4. 客户端

我用yaaw,纯静态页面,服务器上部署个Nginx即可:
https://github.com/binux/yaaw

某天接触到某个用Golang实现的程序,不仅体积小,还支持几乎所有种类的CPU,于是下决心学习一下Go这门语言。虽然刚出来的时候就想学,但那时据说有很多坑(比如语法可能会变),就放弃了。现在连被成为垃圾的包管理也有了升级,觉得是时候去学。恰好要把一个二级域名绑定IP的小服务迁移到VPS上,于是干脆用Go重新实现(原来是用Python3)这个服务。

首先要阅读相关教程。初学者教程当然是官方入门教程:
https://tour.go-zh.org/

官方入门教程太简单(毕竟Go本身语法就是简单),还需要阅读其它相关知识:
1)Go搭建一个Web服务器
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/03.2.md
2)golang读取json配置文件
[https://blog.csdn.net/benben_2015/article/details/79134734]
3)文件读写
https://wiki.jikexueyuan.com/project/the-way-to-go/12.2.html
4)go-extend,获取请求的IP的代码
https://github.com/thinkeridea/go-extend/blob/master/exnet/ip.go
5)golang 发送GET和POST示例
https://segmentfault.com/a/1190000013262746
6)GoDNS中dnspod客户端的代码
https://github.com/TimothyYe/godns/blob/master/handler/dnspod/dnspod_handler.go
7)Go语言(golang)的错误(error)处理的推荐方案
https://www.flysnow.org/2019/01/01/golang-error-handle-suggestion.html

这个小服务,就是个web服务。客户端发起带有key和token的请求,此服务会验证有效的授权,然后把对应的二级域名与客户端IP绑定。配置信息以json格式保存在文本文件。客户端IP会记录在对应log文件,以方便每次比较客户端IP是否变化了。每次更新二级域名与IP的绑定,则会记录log。相关文件及代码如下:

配置文件,config.json:

{
"ServIpPort": ":12345"
,"DnsKey": "dnspod的key"
,"DnsToken": "dnspod的token"
,"DomainId": "dnspod的域名id"
,"SubDomainId": {"abc":"dnspod的二级域名id", "efg":"dnspod的二级域名id"}
,"Users": [
        {
                "Key":"client1"
                ,"Token":"aaa123456"
                ,"SubDomains": ["abc"]
        }
        ,{
                "Key":"client2"
                ,"Token":"xxx789012"
                ,"SubDomains": ["efg"]
        }
]
}

小服务的代码,ddnsServ.go:

package main

import (
    "fmt"
    "log"
    "net"
    "net/http"
    "net/url"
    "strings"
    "os"
    "path/filepath"
    "io/ioutil"
    "time"
    "encoding/json"
)

type User struct {
    Key string
    Token string
    SubDomains []string
}

type Config struct {
    ServIpPort string
    DnsKey string
    DnsToken string
    DomainId string
    SubDomainId map[string]string /*key:sub domain name, value:sub domain id*/
    Users []User
}

var (
    curPath string
    ipLogPath string
    historyPath string
    config Config
)

func init() {
    curPath, _ = filepath.Abs(filepath.Dir(os.Args[0])) 
    ipLogPath = curPath + "/ip"
    historyPath = curPath + "/log"
    for _, path := range []string{ipLogPath, historyPath} {
        if err := initPath(path); err != nil {
            fmt.Printf("Init failed. Error info:%s\n", err)
            os.Exit(-1)
            return
        }
    }

    if err := initConfig(curPath + "/config.json"); err != nil {
        fmt.Printf("Init failed. Error info:%s\n", err)
        os.Exit(-1)
        return
    }
}

func initPath(path string) error {
    s, err := os.Stat(path)
    if err == nil && !s.IsDir() {
        return fmt.Errorf("The path is existed, but it is not a directory! Path is:%s", path)
    }
    if err != nil && os.IsNotExist(err) {
        e := os.Mkdir(path, os.ModePerm)
        return e 
    }
    return nil
}

func initConfig(configPath string) error {
    configData, err := ioutil.ReadFile(configPath)
    if err != nil {
        return fmt.Errorf("Failed to read config file: %s! Error info: \n%s", configPath, err)
    }

    err = json.Unmarshal(configData, &config)
    if err != nil {
        return fmt.Errorf("Failed to load config data! Error info: \n%s", err)
    }
    return nil
}

// get client IP address 
func GetClientIP(r *http.Request) string {
    xForwardedFor := r.Header.Get("X-Forwarded-For")
    ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
    if ip != "" {
        return ip
    }

    ip = strings.TrimSpace(r.Header.Get("X-Real-Ip"))
    if ip != "" {
        return ip
    }

    if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
        return ip
    }

    return ""
}

func GetLogFilePath(logPath string, subDomain string) string {
    return fmt.Sprintf("%s/%s.log", logPath, subDomain)
}

// get the file path which saved ip address of subDomain
func GetIpLog(subDomain string) string {
    path := GetLogFilePath(ipLogPath, subDomain)
    buf, err := ioutil.ReadFile(path)
    if err != nil {
        return ""
    }
    return string(buf)
}

func SaveIpLog(subDomain string, ip string) {
    path := GetLogFilePath(ipLogPath, subDomain)
    ioutil.WriteFile(path, []byte(ip), 0644)
    /*
    err := ioutil.WriteFile(path, []byte(ip), 0644)
    if err != nil {
        panic(err.Error())
    }
    */
}

func SaveHistoryLog(subDomain string, ip string) error {
    path := GetLogFilePath(historyPath, subDomain)
    logFile, err := os.OpenFile(path, os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0644)
    if err != nil {
        return fmt.Errorf("Failed to open history log file: %s! Error info: \n%s", path, err)
    }
    defer logFile.Close()

    nowStr := time.Now().Format("2006-01-02 15:04:05")
    log := fmt.Sprintf("%s, ip:%s\n", nowStr, ip)
    logByte := []byte(log)
    n, err := logFile.Write(logByte)
    if err == nil && n < len(logByte) {
        return fmt.Errorf("Failed to save history log file: %s! Error info: \nwrite file failed", path)
    }
    return nil
}

func UpdateDns(subDomain string, ip string) error {
    values := url.Values{}
    values.Add("login_token", config.DnsKey + "," + config.DnsToken)
    values.Add("format", "json")
    values.Add("lang", "en")
    values.Add("error_on_empty", "no")

    values.Add("domain_id", config.DomainId)
    values.Add("record_id", config.SubDomainId[subDomain])
    values.Add("sub_domain", subDomain)
    values.Add("record_type", "A")
    values.Add("record_line", "默认")
    values.Add("value", ip)

    client := &http.Client{}
    req, err := http.NewRequest("POST", "https://dnsapi.cn/Record.Modify", strings.NewReader(values.Encode()))
    if err != nil {
        // handle error
    }

    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Set("Accept", "text/json")
    resp, err := client.Do(req)

    defer resp.Body.Close()

    _, err2 := ioutil.ReadAll(resp.Body)
//    fmt.Println(string(s))
    if err2 != nil {
        return err2
    }
    return nil
}

func handler(w http.ResponseWriter, r *http.Request) {
    // Verify authorization
    var subDomains []string
    key := r.PostFormValue("key") // get key of POST
    token := r.PostFormValue("token") // get token of POST
    for _, user := range config.Users {
        if user.Key == key && user.Token == token {
            subDomains = user.SubDomains
            break
        }
    }
    if subDomains == nil || len(subDomains) <= 0 {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    // get IP
    ip := GetClientIP(r)
    fmt.Fprintf(w, "%s", ip)

    for _, subDomain := range subDomains {
        // get last IP of subDomain
        ipLog := GetIpLog(subDomain) 
        if ip == ipLog {
            // IP is not changed
            continue
        }
    
        // update DNS, bind IP to subDomain
        err := UpdateDns(subDomain, ip)
        if err == nil {
            // update success, save new IP
            SaveIpLog(subDomain, ip)
            SaveHistoryLog(subDomain, ip)
        }
    }
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(config.ServIpPort, nil))
}

关于部署,就用nginx弄个反向代理,指向这个小服务的端口。由于VPS上装了Debian9,可以配置systemd来设置系统服务。

客户端只需发起post请求,把key和token发过来就可以了。以下用curl实现:

#!/bin/sh

#curl命令的参数解析:
# -X 请求的方法。这里用了POST,在HTTPS传输中,数据被加密
# --connect-timeout 连接超时时间,单位:秒
# -m/--max-time 数据传输的最大允许时间,单位:秒
# https://rpi.f...... 请求的URL
# -H/--header 请求头。要设置多个请求头,则设置多个-H参数
# -d/--data 请求数据。
curl -k -X POST --connect-timeout 5 -m 10 https://youdomain.xxx:12345/api/update_dns -H 'cache-control: no-cache' -H 'content-type: application/x-www-form-urlencoded' -d 'key=client1&token=aaa123456'

目前这个小服务工作良好。除了体积有点大(约7MB),其它都挺满意的。

近来迁移了自家的服务器,顺便记录一下Flask项目的部署。

这里采用Nginx + Supervisor + Python3 + uWSGI + Flask的方案。其中建议把uWSGI替换成Gunicorn,据说采用纯Python实现的Gunicorn,更方便打包为Dockor镜像,并且性能几乎跟Supervisor一样。这个留待以后再研(折)究(腾)。

新服务器采用Debian 9,部署过程参考以下文章:
Flask+uwsgi+Nginx部署应用
https://www.jianshu.com/p/84978157c785


1. 安装项目虚拟环境

假设项目文件夹为`/opt/flask_proj`,安装命令如下:

sudo apt update
sudo apt install python3 python3-pip
sudo pip3 install virtualenv
cd /opt/flask_proj
virtualenv venv
pip3 install -r requirements.txt

其中:
1)安装virtualenv需要用root用户,否则不会安装到/usr/local/bin/virtualenv,并且需要自行添加到path。
2)requirements.txt为flask项目所需的库。这个要看项目是否需要安装。
3)如果是迁移项目,可以在迁移前生成requirements.txt文件:pip3 freeze > requirements.txt


2. 安装uWSGI

如果requirements.txt里已包含uWSGI,则不用重复安装。安装命令如下:

pip3 install uwsgi

在项目文件夹下,新建uWSGI配置文件config.ini,参考内容如下:

[uwsgi]
master = true
home = venv
wsgi-file = manage.py
callable = app
socket = :5001
processes = 4
threads = 2
buffer-size = 32768


3. 安装supervisor

安装命令如下:

sudo apt-get install supervisor

在文件夹/etc/supervisor/conf.d下新建项目对应的配置文件,例如flask_proj.conf,参考内容如下:

[program:flask_proj]
# 启动命令入口
command=/opt/flask_proj/venv/bin/uwsgi /opt/flask_proj/config.ini
# 命令程序所在目录
directory=/opt/flask_proj
# 运行命令的用户名
user=user
autostart=true
autorestart=true
# 日志地址
stdout_logfile=/opt/flask_proj/logs/uwsgi_supervisor.log

启动supervisor服务:

sudo service supervisor start

4. 安装Nginx

一般安装系统自带的版本就够用了:

sudo apt install nginx

在文件夹/etc/nginx/sites-available下新建配置文件flask_proj,参考内容如下:

server {
    listen 443 ssl http2 default_server;
    #listen [::]:443 ssl;
    server_name www.abc.xyz;

    ssl_certificate /etc/letsencrypt/live/www.abc.xyz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.abc.xyz/privkey.pem;

    index index.html index.htm;

    gzip            on;
    gzip_min_length 1k;
    gzip_buffers    4 16k;
    #gzip_proxied    expired no-cache no-store private auth;
    gzip_comp_level 5;
    gzip_types      text/html text/css text/javascript text/json text/plain text/xml application/javascript application/json application/soap+xml application/x-javascript application/x-www-form-urlencoded application/xhtml+xml application/xml;

    location / {
        include uwsgi_params;
        uwsgi_pass 127.0.0.1:5001;
        uwsgi_param UWSGI_PYHOME /opt/flask_proj/venv;
        uwsgi_param UWSGI_CHDIR /opt/flask_proj;
        uwsgi_param UWSGI_SCRIPT manage:app;
        uwsgi_read_timeout 100;
    }  
}

注意:
1)这里只配置了https,相关的SSL证书,可以到https://letsencrypt.org/免费申请。
2)开启了gzip压缩。

最后添加链接文件,并重启Nginx:

sudo ln -s /etc/nginx/sites-available/flask_proj /etc/nginx/sites-enable/flask_proj
sudo service nginx restart



从G1时代开始,就了解到因为Android使用Linux内核,可以利用chroot运行大量Linux发行版。但是由于当时ARM CPU性能低下及内存不足,一般只能使用Terminal字符界面,或者ssh过去。然后升级过设备,并装上了“XServer XSDL”(Android上的Xserver)用来体验图形界面,但是手机屏幕太小,一弹出虚拟键盘就基本把桌面挡住了。后来想起X Window是基于客户端/服务器模式的,应该可以用PC电脑之类提供Xserver,显示手机上的Linux图形界面。最后终于弄明白了配置,出来的效果还是不错,至少可以用浏览器流畅播放视频了。

所需设备
S机,用于提供X Server的设备,最好是屏幕比较大的PC电脑(台式机或笔记本)。系统最好是Linux,装上X11。后来发现Windows也可以,因为有Xming。当然,Android也是可以,因为有XServer XSDL。这里只记录Linux的。

A机,Android设备(手机或平板),运行Linux发行版的X Client。Android上有很多装Linux的应用了,这里推荐Linux Deploy,因为这是github.com上的开源项目。装Linux的步骤不详述,这个应用已经做得很好了。

网络设置
只要两个设备在同一局域网内就可以了。下面列出几种方式:
1)使用有线/无线路由器组建局域网。路由器是性能瓶颈,特别是A机无线连接到路由。
2)A机分享无线网络,S机连过去。缺点是只能利用A机的移动网络上网,费钱……
3)S机分享无线网络,A机连过去。S机需要装个无线网卡,并成为性能瓶颈。
4)A机开启开发者模式,通过USB线连接S机。S机利用ADB命令的forward功能映射端口。ADB命令成为性能瓶颈。
5)A机分享有线网络,通过USB线连接S机。这个方案性能最佳,且使用设备最小,又不影响A机的网络。但A机可能会出现发热的情况。

X Server设置
这一步花了很多时间。理解后就是S机上的一个命令和一个配置文件,即如何启动X Server和设置验证。相关原理可查Google,这里设置X Server为:1.0(从0开始算,第2个服务),默认使用6001端口。
1)利用xhost设置可访问X Server的客户端IP。在S机上修改配置文件/etc/X1.hosts,若不存在则新建。把A机的IP地址填进去,并保存。
2)运行以下命令启动X Server:

sudo Xorg :1.0 -listen tcp

X Client设置
A机上需要装好桌面环境。推荐LXDE吧,轻量。启动前,连好网络,在Linux Deploy上设置:

图形界面 -> 勾选启用
图形子系统 -> 选X11
图形界面设置 -> 显示编号 -> 1.0
           -> X服务器地址 -> 填写S机的IP地址
           -> XServer XSDL -> 不要勾选
桌面环境 -> 选LXDE

然后启动Linux即可(实在太方便了)。对应的命令就是:

export DISPLAY=S机_IP:1.0
startlxde

总结
A机的性能越好,并且S机的性能越差,这个方案的实用性就越高。比起微软的“Continue on PC”,三星的DeX,Superbook等方案,便宜很多,并把旧电脑利用起来。使用过程中,不会影响A机的来电、通知等日常用途。但是目前来看,实用性确实不高。后面看看能不能用Raspberry Pi Zero + 显示屏 + 大容量移动电源来作为S机,以提高便携性。

CFO离职,捡了个烂电脑回来(CPU是i3-4000M,第4代酷睿,内存4G)。比10年前的酷睿T2450好太多了,而且是64位系统。前段时间有新闻报道说,Google的内部系统Goobuntu,准备抛弃Ubuntu,直接基于Debian。于是也想试试直接装Debian来用。其实用习惯了Lubuntu,只要桌面也装LXDE,应该用起来没什么区别。
第一次装Debian,发现居然连下载哪个ISO都不知道。一开始下个live CD的iso文件,发 现虽然启动界面有选项可以安装系统,但是按抓给你过程要检查CD-ROM。没用光盘的话,不能继续进行安装。后来才明白要用netinst的iso。

iso文件制作U盘启动盘很简单,只要两行命令。其中sdX是U盘的设备文件名。如果U盘已挂载,要先卸载,再执行

$ sudo cp debian-9.4.0-amd64-netinst.iso /dev/sdX
$ sudo sync

后面的安装步骤基本按着提示一步一步进行。有个文章写得挺好的,可以参考:
如何安装 Debian 9?
http://scottming.com/2017/08/06/how_to_install_debian9/

有两个步骤需要说明一下:
第一,需要找无线网卡的firmware,放在另一个U盘,再插到电脑上。安装程序会自动设别并安装。这是为了后面安装软件时连上网络。或者可以使用有线连接吧。

第二,到了分区那一步,不能按默认的整个硬盘分区。因为要保留原来的Windows系统,所以只能选择手动操作。而且第一次搞LVM分区,完全不懂操作。后来参考了这个文章才能顺利搞掂:
ubuntu 12.10 安装 LVM分区(图文)
http://blog.sina.com.cn/s/blog_56a70c0401018dki.html

简单来说,要注意几点:
1)由于GRUB Legacy不支持LVM,所以无法在LVM上创建/boot分区。也就是除了/boot分区,其它分区都可以在LVM上创建。
2)先把分区设置为LVM分区,再点LVM分区管理的选项进行详细设置。
3)LVM分区管理,先创建逻辑卷组,再创建逻辑卷,多个逻辑卷就等于是多个分区。
4)最后把逻辑卷设置对应的挂载点。

最后总算是顺利安装完毕。

后记
用了一段时间后,终于明白Ubuntu的存在价值了。Debian 9即使装上LXDE,还是有一些不顺手,例如屏幕亮度调节。后来还是换上Lubuntu。

手上有个远古时代的上网本,华硕(ASUS) EeePC 900。删掉原来的Windows XP,装上Lubuntu 16.10。原电池废了,淘宝换了个新的,续航能达到5小时(大概吧)。但是一般用来远程ssh到raspberry pi,没必要开启图形界面(特别是发现用screen可以同一界面打开多个shell)。需要解决的问题是中文的显示和输入。Google过后,最终的解决方案是fbterm(完美显示中文) + fcitx-fbterm(实现中文输入)。

fbterm的安装参考这个(英文):
https://fcitx-im.org/wiki/Fbterm

中文版本的完整安装教程,参考这个:
http://ikuduku.com/blog/fbterm-display-and-input-Chinese-in-tty

关于中文输入法,我选择google拼音(fcitx-googlepinyin)。记录一下相关操作:

1)安装

#安装相关软件
sudo apt-get install fbterm fcitx fcitx-fbterm fcitx-googlepinyin fcitx-configtool
#让fbterm可以设置快捷键,主要是切换输入法
sudo setcap 'cap_sys_tty_config+ep' /usr/bin/fbterm
#把当前用户加入到video组
sudo gpasswd -a <your username> video

2)编辑当前用户的配置

vi ~/.fbtermrc

修改如下:

#设置字体
font-name=Mono
#设置字体大小
font-size=14
#设置输入法
input-method=fcitx-fbterm

3)设置开机启动

#设置开机进入shell界面,这是Ubuntu 16.04及以后版本的设置方法
sudo systemctl set-default multi-user.target
#设置进入图形界面的命令,然后用`sudo startx`命令即可进入图形界面
sudo sh -c "echo \#\!/bin/sh > /usr/local/bin/startx"
sudo sh -c "echo systemctl start lightdm >> /usr/local/bin/startx"
sudo chmod +x /usr/local/bin/startx
#设置进入shell后自动启动fbterm,而图形界面的shell则不会启动fbterm
printf '\nif [ $TERM = 'linux' ]; then\n  fcitx-fbterm-helper -l\nfi' >> ~/.bashrc

大概就这样了。使用过程中,还是有一点不习惯。例如快捷键的变换,Ctrl + Alt + F1 ~ F6 不能切换tty,而是切换字符编码,使用 Ctrl + Alt + 1 ~ 0 可以切换10个窗口。