Linux TCP keepalive 计时器是错误的?

ss(1) 加上 -o 参数可以打印 tcp 连接的 timer 信息,例如:

[root@VM-0-11-tlinux ~]# ss -ntpo 'sport = 80'
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 172.16.0.11:80 183.14.31.93:25144 users:(("socat",pid=10624,fd=6)) timer:(keepalive,1min32sec,0)

这是一个用 socat 设定了 120s 空闲(keepalive 的 idle)时间的连接,可以看到它正确打印了 timer 信息,执行 ss(1) 时候 timer 超时时间只剩下 92s 了。

问题:我们在连接的一端发送数据后,预期是 timer 应该把超时时间重制为 120s 开始倒数计算,然而实际并不是这样。客户端发送数据后,我们看下 timer:

[root@VM-0-11-tlinux ~]# ss -ntpo 'sport = 80'
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 172.16.0.11:80 183.14.31.93:25144 users:(("socat",pid=10624,fd=6)) timer:(keepalive,1min28sec,0)

ss(1) 仍然在继续倒数连接空闲超时时间。但是当我们实际去抓包的时候,可以发现确实是以接收到数据为起点 120s 之后才会发起探测的,所以 keepalive 这个机制并没有错,错的是这个 timer 展示。应该是为了减少计算量而采取这种“惰性”展示方式,具体可以参考:https://github.com/torvalds/linux/blob/master/net/ipv4/tcp_timer.c#L662

ps 打印的调度优先级是错误的?

通常我们在 Linux 平台会用 ps(1) 查看当前运行的所有进程信息(快照),它会输出 UID, PPID, PID, TIME, CMD 等信息,如下:

[root@VM-0-11-tlinux ~]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Aug30 ? 00:03:46 /usr/lib/systemd/systemd --system --deserialize 17
root 2 0 0 Aug30 ? 00:00:00 [kthreadd]
root 4 2 0 Aug30 ? 00:00:00 [kworker/0:0H]
root 6 2 0 Aug30 ? 00:00:00 [mm_percpu_wq]
root 7 2 0 Aug30 ? 00:00:44 [ksoftirqd/0]
root 8 2 0 Aug30 ? 00:01:14 [rcu_sched]
root 9 2 0 Aug30 ? 00:00:00 [rcu_bh]
root 10 2 0 Aug30 ? 00:00:00 [migration/0]
...

以上信息有时候不足以排查问题,比如进程的 nice 值,调度优先级都没有显示。怎么办?根据 ps(1) 所述可以用 -l 参数显示包含了 nice 值,PRI 等信息,如下:

[root@VM-0-11-tlinux ~]# ps -el
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
4 S 0 1 0 0 80 0 - 13547 ep_pol ? 00:03:46 systemd
1 S 0 2 0 0 80 0 - 0 kthrea ? 00:00:00 kthreadd
1 I 0 4 2 0 60 -20 - 0 worker ? 00:00:00 kworker/0:0H
1 I 0 6 2 0 60 -20 - 0 rescue ? 00:00:00 mm_percpu_wq
1 S 0 7 2 0 80 0 - 0 smpboo ? 00:00:44 ksoftirqd/0
1 I 0 8 2 0 80 0 - 0 rcu_gp ? 00:01:14 rcu_sched
1 I 0 9 2 0 80 0 - 0 rcu_gp ? 00:00:00 rcu_bh
1 S 0 10 2 0 -40 - - 0 smpboo ? 00:00:00 migration/0
...

看起来调度优先级(PRI)和 nice 信息都有了,但是有些奇怪,它们的值对应关系为: PRI=NI+80。OK,看了手册页,它说 -c 参数可以打印关于调度器的相关信息,我们看看:

[root@VM-0-11-tlinux ~]# ps -elc
F S UID PID PPID CLS PRI ADDR SZ WCHAN TTY TIME CMD
4 S 0 1 0 TS 19 - 13547 ep_pol ? 00:03:46 systemd
1 S 0 2 0 TS 19 - 0 kthrea ? 00:00:00 kthreadd
1 I 0 4 2 TS 39 - 0 worker ? 00:00:00 kworker/0:0H
1 I 0 6 2 TS 39 - 0 rescue ? 00:00:00 mm_percpu_wq
1 S 0 7 2 TS 19 - 0 smpboo ? 00:00:44 ksoftirqd/0
1 I 0 8 2 TS 19 - 0 rcu_gp ? 00:01:14 rcu_sched
1 I 0 9 2 TS 19 - 0 rcu_gp ? 00:00:00 rcu_bh
1 S 0 10 2 FF 139 - 0 smpboo ? 00:00:00 migration/0

打印出了进程所使用的调度策略(CLS),然而,PRI 居然又变了!这次 pid 1(systemd) 的优先级显示的不是 80,而是变成了 19。这是为何?

如果现在就觉得有点混乱的话,我们再看看 top(1) 输出的:

[root@VM-0-11-tlinux ~]# top -b -o -PID
top - 17:20:13 up 20 days, 3:39, 3 users, load average: 0.00, 0.00, 0.00
Tasks: 87 total, 1 running, 46 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 6.2 sy, 0.0 ni, 93.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 876288 total, 69040 free, 96548 used, 710700 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 606368 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 54188 5212 3700 S 0.0 0.6 3:46.98 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.14 kthreadd
4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H
6 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mm_percpu_wq
7 root 20 0 0 0 0 S 0.0 0.0 0:44.81 ksoftirqd/0
8 root 20 0 0 0 0 I 0.0 0.0 1:14.80 rcu_sched
9 root 20 0 0 0 0 I 0.0 0.0 0:00.00 rcu_bh
10 root rt 0 0 0 0 S 0.0 0.0 0:00.00 migration/0

top(1) 输出同样的进程 systemd,它的优先级又变成了 20. 这又是为什么?

top(1) 的手册页会告诉你,它显示的确实是调度优先级:

   16. PR  --  Priority
       The scheduling priority of the task.  If you see `rt' in this field, it means the task is running under real time scheduling priority.

事实上,大多数 Linux 发行版提供的 ps(1), top(1) 命令都属于 procps-ng(https://gitlab.com/procps-ng/procps) 这个软件包,但是为什么它们显示进程调度优先级的时候如此不同,以至于连 ps(1) 自己不同的参数下显示的也不一样?

我们直接看看 ps(1) 的代码,里面有如下注释:

// "PRI" is created by "opri", or by "pri" when -c is used.
//
// Unix98 only specifies that a high "PRI" is low priority.
// Sun and SCO add the -c behavior. Sun defines "pri" and "opri".
// Linux may use "priority" for historical purposes.
//
// According to the kernel's fs/proc/array.c and kernel/sched.c source,
// the kernel reports it in /proc via this:
//        p->prio - MAX_RT_PRIO
// such that "RT tasks are offset by -200. Normal tasks are centered
// around 0, value goes from -16 to +15" but who knows if that is
// before or after the conversion...
//
// <linux/sched.h> says:
// MAX_RT_PRIO is currently 100.       (so we see 0 in /proc)
// RT tasks have a p->prio of 0 to 99. (so we see -100 to -1)
// non-RT tasks are from 100 to 139.   (so we see 0 to 39)
// Lower values have higher priority, as in the UNIX standard.
//
// In any case, pp->priority+100 should get us back to what the kernel
// has for p->prio.
//
// Test results with the "yes" program on a 2.6.x kernel:
//
// # ps -C19,_20 -o pri,opri,intpri,priority,ni,pcpu,pid,comm
// PRI PRI PRI PRI  NI %CPU  PID COMMAND
//   0  99  99  39  19 10.6 8686 19
//  34  65  65   5 -20 94.7 8687 _20
//
// Grrr. So the UNIX standard "PRI" must NOT be from "pri".
// Either of the others will do. We use "opri" for this.
// (and use "pri" when the "-c" option is used)
// Probably we should have Linux-specific "pri_for_l" and "pri_for_lc"
//
// sched_get_priority_min.2 says the Linux static priority is
// 1..99 for RT and 0 for other... maybe 100 is kernel-only?
//
// A nice range would be -99..0 for RT and 1..40 for normal,
// which is pp->priority+1. (3-digit max, positive is normal,
// negative or 0 is RT, and meets the standard for PRI)
//


https://gitlab.com/procps-ng/procps/-/blob/master/ps/output.c#L590

ps(1) 为了兼容各种 UNIX 标准及其实现,包含了多个调度优先级的取值方法(pri, opri, intpri, priority)。PRI 默认取自 opri 变量,如果加了 -c 参数,则取自 pri 变量。总的来说,在 Linux 平台,其实取 priority 变量,即 /proc/[pid]/stat 中的第 18 列 priority 原始值应该是更好的,符合 Linux 调度优先级本意,也与 top(1) 一致,即:

[root@VM-0-11-tlinux ~]# ps -e -o 'priority,nice,cmd'
PRI NI CMD
20 0 /usr/lib/systemd/systemd --system --deserialize 17
20 0 [kthreadd]
0 -20 [kworker/0:0H]
0 -20 [mm_percpu_wq]
20 0 [ksoftirqd/0]
20 0 [rcu_sched]
20 0 [rcu_bh]
-100 - [migration/0]

重命名本地 git 仓库,master -> main

pull 某个许久未同步的 git 仓库时,fetch 成功,但是最终报了如下错误:

$ git pull
Your configuration specifies to merge with the ref ‘refs/heads/master’
from the remote, but no such ref was fetched.

查了一会儿发现是上游把 master 分支给重命名成 main 了。于是本地仓库也需要更新:

$ git checkout master
$ git branch -m master main
$ git fetch
$ git branch –unset-upstream
$ git branch -u origin/main
$ git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main

Google Cloud Platform 的一点点体验

因为自己想搭个 wireguard 服务器在 HK,去 GCP 启动了一个 VM 实例,结果今天发现似乎不得不把它删掉,总结 GCP 使用感想如下:

  1. 居然连实例重装系统都不支持,只能删掉另起一个
  2. 我使用了一个 CentOS 8 的实例,执行一次 yum update 结果 SSH 都挂了,看了下日志,应该是升级 google 相关软件包导致的
  3. SSH 挂掉后,我想通过串口登陆进系统,串口登陆居然是灰的,默认不让从串口登陆。
  4. 于是我以为是 VM 里面可能没起串口,给 reset 了,结果就起不来
  5. 我只能求助伟大的 Google 搜索,发现,GCP 的串口登陆需要设置 metadata。(这是什么奇葩东西?)
  6. 于是我设置了 metadata,终于可以打开串口了,发现实例没起来是因为 SELinux 不知道为什么被开启了。但我有九成的把握,就是升级 google 相关软件包导致
  7. 我想进 GRUB,改内核启动参数,然而发现,GCP 默认把 GRUB 菜单显示也给关了,美其名曰“快速启动”。就不能给一秒钟时间吗?

想了想,没什么办法可以救得回这台实例了。

虽然上面没有我什么重要的东西,但我总结起来,GCP 的这个使用经验,简直就是在一直打我耳光,也许我属于不配使用它的那一类人吧。可能这是 Google 那群人根据自己在公司内部的使用方式提供出来的最佳实践,估计都已经把 OS 这层给越来越“虚”化,不需要 Google 员工在意 OS 层面的东西。但外界是否也达到了这种地步?恐怕 GCP 是还活在自己的梦里。

PlayStation 4 Pro

最近买了个美版 PS4 pro 。其实本来打算是买回给老家的一台新买的 4K + HDR 电视机配的,要不然按照国内的内容源缺乏程度, 4K + HDR 就是个摆设而已。

当然先是寄到了自己广州这边,然而玩了几天 The Last Of Us 之后,我有点喜欢这个游戏机了(挖鼻孔)。于是这几天不跑步,不运动,晚上和周末花了一点时间把它通关了。然而看着已经买了的好多个其它正在下载的游戏,更加觉得自己可以重新开始宅了。。。

以前不太理解有些人为什么会话这么多钱买游戏玩,(PC 游戏盗版一大堆在那里呢),然而有了个游戏机后才发现,它的优点——省心(不用为系统配置操心)确实是最大的卖点。作为一个玩具,它能让人纯粹的从中找到乐趣,不用再关心其它事情,这就是最好的了。

如何正确的引导用户设置好的密码

  1. 可以强迫用户输入密码的长度最小为 N 位。
  2. 不要强迫用户必须输入数字、字母、特殊符号的组合。因为过于复杂的密码用户自己都记不住几个,只能重复的使用常用的几个密码组合,或者是写下来在某个地方,结果是被别人偷看、盗走。
  3. 支付密码,只用数字就可以了,不要强迫用户必须输入其它字母或者符号。用户也许不会天天都用你的 app 进行支付,同样,再说一遍,过于复杂的密码很容易忘记。因为这个密码本身已经有登陆密码的保护了,可以做的一个限制是输错 N 次之后,暂时冻结支付或者要求进入恢复支付密码的步骤。
  4. 让你的登录表单对密码管理器友好。有些登录表格会蓄意不让浏览器记住密码,然而实际上,让浏览器自动保存和填写密码是更好的办法,这样用户可以随机生成密码填进去就完事了。而当密码管理器无法记住密码的时候,用户同样也只能从自己脑海里面的常用密码选一个出来使用,最终的结果是导致用户在不同网站用了相同的密码。
  5. 如果用户选择了记住登录状态,就不要动不动隔了一两天没登陆就把用户登录状态取消了。这点在很多 app 上面非常常见,然而,在使用了随机密码的情况下,输入密码其实是个痛苦的过程。同时,这也是个高风险的过程,因为容易被偷看和监听,尤其是国内这么多网站都还没有部署 HTTPS 的情况下。

CentOS 的默认最大 pid

最近发现新上架的几个服务器(CentOS 6)上面跑的进程的 pid 有些不一样,它们要比原来一些旧的环境上面的 pid 要大很多。看了下系统最大允许的 pid 值:

[root@whereami ~]# cat /proc/sys/kernel/pid_max
196608

这比旧系统上面的 32768 要大很多。

查了一下原因,是这样的:

The kernel begins with a default pid_max value of 32768, and then will adjust it upward based on the number of possible CPUs the system could have. It will use a value of 1024 * num_possible_cpus if that value is larger than the default. The value the kernel will use for pid_max is capped at approximately 4 million.

https://access.redhat.com/solutions/874023

也就是说,在一个系统上面,pid_max 是根据可能的 CPU 个数来确定的(不是实际存在的个数)。具体规则就是可能 CPU 个数乘以 1024。不过为了防止可能 CPU 个数较小的环境上面 pid 很快被用尽,最小的 pid_max 都是从 32768 开始的。

看了一下新服务器的可能 CPU 个数是 192,乘以 1024,刚好就是 196608 了。

Windows 的退化

Windows 本来是优秀应用软件的集中地,尤其是以微软自己的应用程序为代表,这些软件基本很少崩溃,响应迅速,性能非常好。

然而到了 modern apps/UWP 时代之后,这一切都在退化。

已经说不清是多少次被 Windows 10 上面的 UWP 应用的闪退、崩溃给弄到吐血了(其中很多都是微软自己的应用)。并且, UWP 的问题不仅仅是闪退,它的性能也远远不如传统的 Windows 应用,比如:它的启动实在太慢。

我完全不知道 UWP 是否是个好的应用平台,不知道是否是开发者的低能导致的问题。然而这些都不重要。Windows 需要的成功,靠这么烂的软件(平台)质量是只能走向死路的。

ZSH 也不完美

以前有段时间在命令行上折腾过不少时间,接触过 ZSH, Fish 等不同的 Shell,但是后来不玩了,因为 bash-completion 通常来说已经够用了。但是最近发现已经有人弄了个 Oh My Zsh 这样的项目,一些主题还是很不错的,于是重新设置了默认 shell 为 zsh 了。

但是 zsh 也不完美。

➜  ~ rsync -e ssh -avzP 198.1x.x.x:/home/luo/*.deb .
zsh: no matches found: 198.1x.x.x:/home/luo/*.deb

只能切换回去 bash 执行。

使用 Kapacitor 对 InfluxDB 数据进行统计处理

InfluxDB + Telegraf + Grafana 算是一套不错的数据收集、视觉化工具,相比于更为常见的 ELK 架构,它有着自己的特点。查询功能总得来说虽不如 ES 强大,但是优点是——已经够用了,而且它的性能不错,存储的数据占用空间也相比 ES 小。

简单介绍这三个工具:

  • InfluxDB InfluxData 的一款时序数据库,它是这套工具的核心;
  • Telegraf 则是 InfluxData 自己开发的一个数据收集工具,相比于 Logstash 或者 Mozilla 的 Heka,它有了更多的内置实用 input 插件,例如 sysstat, Mongodb, MySQL, 日志文件 grok 和跟踪 … 使用 Telegraf 可以非常容易的监控起系统和各种常见服务的各种参数,基本无需再安装其它专用工具。
  • Grafana 则是一个数据视觉化工具(“画图”的),支持多种数据后端,包括 InfluxDB,Elasticsearch,Zabbix 等。

实际上,InfluxData 是有一个自己的 TICK 架构的(Telegraf + InfluxDB + Chronograf + Kapacitor),其中 Chronograf  是用来画图的,但是看起来似乎还不成熟,不如使用 Grafana 好。Kapacitor 则是一个专门处理 InfluxDB 里面的数据的工具,用它可以对收集到的数据进行统计、触发报警等。。。

好了,说了这么多,其实是想借一个实例说下怎样用 InfluxData 的 Kapacitor 来对 InfluxDB 的数据进行加工处理。

假设:已经搭建好了 InfluxDB + Telegraf + Grafana 这套工具,web 日志收集存储在了 InfluxDB,我们想查看一些统计数值,比如每日独立访问 IP 这样的有价值的数据,我们现在该怎么做?

一般来说,有以下几个选项:

  1. 在 Grafana 里面,我们新建一个图表,每次查看时实时统计出所有的每日独立 IP。但是这个方法实在不妥,web 日志量很大,如果每次一查看那个图表就把 InfluxDB 里面的数据全部过一遍,还要计算、分组,实在太低效,性能太糟糕。对于每日独立 IP 这种数据来说,我们其实并不需要它是实时的,只要每天统计一次即可。
  2. 我们写个 Shell 或者强大一点,Python 脚本去从 InfluxDB 查询出独立 IP 数,再把结果存回 InfluxDB,再把它放到 crontab 里面每天运行,然后 Grafana 建立的图表可以直接查这个新的 measurement。这种方法远比第一种要好很多,但是仍然是需要写脚本的。
  3. 我们可以利用专门的工具——Kapacitor 来做这件事。下面就详细说下如何取每日独立 IP 数。

按照官方的简介,Kapacitor 是一款“Time-Series Data Processing, Alerting and Anomaly Detection”,也就是说——数据处理、报警、异常探测它都能做。

Kapacitor 的安装、配置都很简单,推荐从官网下载 rpm/deb 包安装,然后修改 /etc/kapacitor/kapacitor.conf 文件,在 InfluxDB 连接配置里面设置好账号、密码,就可以启动 kapacitor 服务了。

Kapacitor 有一个后台服务,用户需要做的事就是编写 TICKscript 脚本,然后把这个脚本加载。

来看这个计算每日独立 IP 的脚本,ip.tick:

batch
    |query('''
        SELECT count(distinct(clientip)) AS uip
        FROM "telegraf"."retentionPolicy"."log_measurement"
    ''')
        .period(1d)
        .cron('0 0 * * *')
    |influxDBOut()
        .database('logstats')
        .measurement('site_uip')
        .tag('kapacitor', 'true')

其中,batch 指令表示这是一个对数据进行一次批量处理的任务; |query() 表示这是数据查询的处理节点,我们在 query() 里面直接写入 InfluxDB 的类SQL 查询语句即可,但并没有加入 WHERE 条件对查询时间进行限定,这是为何?因为后面的 .period() 指定了查询范围为一天;然后再利用 .cron() 这个属性,我们可以告诉 Kapacitor 每天零点都执行一次这个查询。|influxDBOut() 则是将查询出来的数据存入 InfluxDB,它的几个属性也比较好理解,不再详说。

保存文件后,执行以下命令:

$ kapacitor define log_uip -tick ip.tick -type batch \
   -dbrp telegraf.retentionPolicy
$ kapacitor enable log_uip

就定义和启用了这个 Kapacitor 任务。接下来还可以用 kapacitor show log_uip 或者 kapacitor list tasks 等命令查看任务状态。看到了吧,这样比脚本加 crontab 还是要简单不少的。(kapacitor 命令的参数含义可以用 help 子命令或者这个文档链接查看。)

在它运行过后,我们就可以在 Grafana 里面取 logstats 数据库里面 site_uip 的 uip 出来进行画图了。

总结:这篇文章只是用一实例展示了 Kapacitor 的功能之一,实际上 Kapacitor 的功能远不止这些,(报警等功能,因为我的环境早有这种功能,没有使用过。)tick 脚本的语法也比上面展示出来的部分强大很多,其数据处理节点也很丰富。总得来说,InfluxData 的这套 TICK 架构似乎还在快速发展的起步阶段(尤其是除 InfluxDB 外的那三部分),但是其实用性已经有一定吸引力了。