程序员应该知道的时间概念

很多人对“速度”没有什么概念,同机房内 RTT (Round Trip Time)大约是多少?如果将一个应用内的函数的调用拆成两个应用 RPC 调用,将增加多少延迟?打印日志有多快,打印日志的多少会增加多少延迟?

之前看过 Jeff Dan(忘记在哪里看到的了)写一个叫 Latency Numbers Every Programmer Should Know 的东西,我费了半天劲,终于找到了。决定把它贴在这里,传播一下。原文如下,这是从这个 gist 看到的。

有意思的事实是,CPU 的 Cache 到内存的操作都是纳秒级别的,同机房的 RTT 和 SSD 的读写速度在一个量级,机械硬盘比 SSD 慢20倍,比内存慢80倍。

以上的数据是2012年的,技术在发展,这个网页也可以拖动滚动条对不同年代的时间做可视化。2005年之前,CPU和内存的速度在飞速发展,但是2005年之后基本停滞了,之后是网络带宽、硬盘(SSD)快速发展。

https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html

这个网站可以看各个城市的延迟:https://wondernetwork.com/pings/

哦对了,说到 ping,我清明节的时候写了一个小工具 pingtop,可以同时 ping 多个 server 并在终端展示出来,喜欢的同学请点个赞❤️。

最后贴一张有人画的形象化的版本。

其他相关的阅读:

  • Jeff Dan 的一个分享 http://videolectures.net/wsdm09_dean_cblirs/
  • Teach Yourself Programming in Ten Years (我搜这个时间表的时候搜到的,这篇文章值得一看  ——为什么大家都这么着急呢?)
 

用 ssh 传输文件

今天收到了研发同学的一个工单,内容大体是:需要将文件拷贝到服务器上去,请求帮助安装 scp 程序。

我尝试了对那台服务器执行 scp 命令,结果是 -bash: scp: command not found,但是 ssh 是正常可以用的。这种情况下应该是 openssh-clients 被删掉了。

但是!scp 命令是基于 ssh 协议的,既然可以用 ssh ,还要什么 scp 呢!

我们可以直接用 ssh 就可以传输文件,学会了之后,发现它比 scp 还好用,scp 的 path 写起来比较蛋疼。

首先很多人忽略的一个事实是,ssh 可以直接输入命令对远程主机执行,比如 ssh [email protected] "cat access.log" ,就可以直接 cat 出远程文件的 log 内容。ssh 会将远程命令的 stdout 和本地的 stdout 连接起来。可以用这样的命令来看实时的日志: ssh [email protected] "tail -f access.log"。这样就可以在本地执行一条命令就可以了,方便脚本化或记录到 本地 history。

既然 ssh 可以将 stdout 连接起来,那么自然也可以将 stdin 连接起来!比如用这个命令将 ssh key 拷贝到服务器上去。

将一个文件传输到服务器:

上面这个命令的原理就是,将文件内容输入到 stdout 中,用管道和 ssh 连接起来,然后这个 stdout 就成了远程命令的 stdin。

要拷贝整个文件夹呢?没有问题:

如果是像日志这种压缩性能很高的文件,可以考虑压缩之后再传输,远程那边从 stdin 解压缩。而且直接将输入和输出通过管道连接起来,压缩中间生成的文件丝毫不会占用空间呢!

如果要指定远程的目标文件夹,可以使用 tar 的 C 参数来指定,比如远程解压缩到 /tmp 下面:

要注意像图片、视频这种二进制文件,本身就是经过压缩之后的了,如果再使用 tar z 来压缩一遍的话,不会节省多少传输体积,反而会白白耗费 CPU。

实用技巧:如果每天备份 MySQL 到另一台机器,但是不占用本机空间?Crontab 的脚本这么写:

假如一台机器A在一个网络环境,另一个机器B在另一个网络环境,他们之间不互通。但是你的电脑(或堡垒机)能同时用 ssh 登陆两台机器,那么怎么把 Server A 的文件拷贝到  Server B?

用两个 ssh!

理解 ssh 能连接 stdin 和 stdout 了,就有无限的可能了!而且你可以将脚本都放在本地,不用还得本地放一些,远程的机器放一些通过 ssh 来执行。

哦对了,ssh 是一个加密的协议,所以在传输的过程中会看到 CPU 使用上涨,因为这是在加密和(远程服务器)解密。用的时候需要考虑到这个。scp 命令是基于 ssh 的,所以会有一样的问题。

nc 基于 tcp 明文传输的,如果不需要加密,传输内容比较多,可以考虑用这个。

在 Server 端执行 nc 监听端口,将输入到 nc 的内容输出到一个文件中。

然后在 Client 端将要发送的文件输入到服务器的这个端口中:

 

这个独门绝技是 @mrluanma 教我的。

 

DNS 解析的原理

提起 DNS,大家都知道是将域名映射成 IP 的一种协议。但是我花了很长时间才真正理解这个“映射”的过程。网上有很多介绍 DNS 的资料,但是遗漏了很多细节,比如都跨过了哪一些缓存,谁去真正发出查询的 packet。我们电脑要配置 DNS 服务器,如果你有网站的话,你会发现你的网站也有一个配置项叫 DNS 解析服务器,这两种服务器是一样的吗?还有一些图和资料是错误的,比如有些图画的是根域名服务器指向权威域名服务器,给人造成一种错觉是根服务器去查询了权威服务器。

本文试图用简单并且准确的图片和文字解释 DNS 查询的过程,以及一些我曾经有的困惑。如果有何 RFC 冲突的地方那肯定是我错了,请指正。

首先介绍一个 DNS,说白了它就是一个名字到 IP 的映射,因为像 64.202.189.170 这样的 IP 太难记了,所以就有了 DNS 这样的东西,好比是一个电话本,我们打电话(访问网页)的时候,就查一下这个名字(网络地址)对应什么样的 IP,然后拨出去。

如果你有很多联系人的话,查号码的时候肯定不会一个一个看,一般会在手机上下拉到这个字母姓氏的地方,然后再定位到这个人的名字。DNS 也是一样,全球已经有上千万个域名了,并且还在实时的变动和更新,所以DNS就采取了一种“分层”的查询。要查询 www.kawabangga.com 这个域名对应的 IP,实际上查询的是 www.kawabangga.com. 最后这个 . 就是 root nameserver,平时我们都省略了,其实这个才是一切开始的地方。

Recursive resolver(下面会讲到这是啥)只知道 . 的地址(一共13个IP),这就够了。当它查询一个域名的时候,它就去 root nameserver 查询,大体的步骤如下:

  1. . root nameserver 发出查询请求,root nameserver 说,“我也不知道 www.kawabangga.com. 这个域名的地址,但是我知道 .com nameserver 的地址。Root Name Server 是怎么知道所有顶级域名的地址呢?因为它就是干这个的,Root Name Server 维护了所有顶级域名对应的地址,这个文件可以在 https://www.internic.net/domain/root.zone 这里下载。现在是 2019 年3月,这个文件是 2万多行,一共2.15M 。
  2. .com Name Server 这种域名是最顶层的域名,所以叫做顶级域名,Top Level Domain Name Server,TLD Name Server。因为……它在最顶层。.com 域名服务商也就叫做顶级域名服务商,其他有名的顶级域名还有 .org .gov .cn 等。TLD nameserver 这里其实也不知道 www.kawabangga.com. 的 IP 地址,但是它知道 kawabangga.com. 的 nameserver 的地址。
  3. kawabangga.com. 的 Name Server 也叫做 Authoritative Name Server,通常翻译成“权威域名服务器”,但是我更喜欢叫他“被授权的域名服务器”,为什么呢?因为……它是被 .com 域名服务器授权的。上一步,TLDName Server 怎么知道 kawabnagga.com. 的Name Server 在哪里的呢?因为你注册一个域名的时候,本质上是花钱购买 xxx.com. 这个权威域名服务器的管理权益,你把钱给域名注册商,域名注册商告诉 .com. TLD Name Server 说,hey 以后把 xxx.com. 这个域名交给 xxx 域名服务器来解析就好啦!于是你就获得了这个域名的解析权利。kawabangga.com. 的 Authoritative Name Server 地址是 ns1.myhostadmin.net. 当 Recursive resolver 过来查询 www.kawabangga.com. 的地址的时候,ns1.myhostadmin.net. 会告诉它对应的IP。到这里,一次查询就宣告结束了。

这三种 Name Server 的职责都很明确:Root Name Server 负责维护全球顶级域名的地址;TLD Name Server 负责维护每一个购买了域名的人提交的他们的 Name Server 地址;Authoritative Name Server 由域名的持有人自己想怎么解析就怎么解析。但是一般的机构和个人都不会自己假设域名服务器,因为可能子域名的需求不多,比如本博客,就3个子域名而已,所以会选择使用一个公共的 DNS 解析服务,比如 DNSPod。域名注册商现在一般也都提供域名解析服务,CDN 像 CloudFlare 也会有域名解析的服务。像大型网站自己假设解析服务器,一般会使用 BIND 这个很流行的软件。当然也有自己研发 DNS 解析服务的。

下面我们再解释一下上面提到的 Recursive Resolver,有的地方叫 Resolving Name Server,意思就是解析 DNS 的 Server。以上的查询工作都由它来完成。

为什么呢?首先 DNS 设计的第一目的就是要快,这是非常基础的设施,所以必须要非常快。但是再快,全世界这么大的量也扛不住啊,并发高?加缓存!DNS 是层层缓存的。缓存有一个经典的问题就是,贵的缓存比较快。这里的贵不一定是价格贵。你在浏览器访问 www.kawabangga.com 的时候,至少经过了浏览器对 DNS 的缓存、电脑对 DNS 的缓存,路由器对 DNS 的缓存…… 浏览器的DNS缓存肯定最快,但是无法跟其他应用共享,如果每个应用都缓存的话,占用的空间就太大了。路由器上面的缓存可以给多台电脑共享,一般又是在本地,所以理所应当的做了最多的缓存。

如果在终端使用 dig 命令,会发现DNS的结果都是路由器给出的。

在这里,路由器就是开放了53端口,来提供DNS查询服务。它就是 Resolving Name Server,每次查询请求发过来,它检查自己的内存是否有答案,如果没有,就执行上述的查询过程。所以当我们执行 DNS 查询的时候,抓包会发现发出了一个包到路由器53端口,收到了一个回到,查询就结束了,还奇怪怎么和书上写的查询过程不一样呢?其实完整的查询过程是在 Resolving Name Server 上面做的。

当然,一个  Resolving Name Server 并不复杂。如果我们在执行 dig 命令的时候,带上 +trace 标志,dig 就会作为 Resolving Name Server 的角色从 . 开始查询直到查询到最后的答案。

CloudFlare 提供了一个很有名的 DNS 服务,1.1.1.1,这其实也是 Resolving Name Server,dig 命令可以使用 @1.1.1.1 的方式来指定。系统也可以设置 DNS 解析地址(现在你明白这个设置是什么意思了吧)。基本上需要网络的设备都可以设置这个DNS服务地址,比如 PS4,电视盒子。很多玩家知道设置成韩国的DNS服务地址玩游戏有时候会更快一些,原理就是这样,DNS是一个基础的服务,你离Resolving Name Server更近了,解析的也就更快了。Resolving Name Server 缓存的命中率高,也就更快,否则的话Resolving Name Server执行起来查询的过程也是耗费时间的。

 

 

参考资料

  1. How the Domain Name System (DNS) Works
  2. https://miek.nl/2013/november/10/why-13-dns-root-servers/
  3. DNS explained
 

我的线程池怎么没了?

事情的背景是这样的:我们有一个系统 A,通过 HTTP 请求到系统 B,系统 B 处理这个请求需要很长时间,并且请求频率也不同,有时候请求多,有时候请求少,所以会先返回给 A “HTTP 200 OK”, 然后再在一个线程池中继续处理这个请求。B 是一个 uWSGI app,线程池也是在 uWSGI 中开的线程池。

我们发现有一些请求处理到一般就没了,没有处理完成。但是即没有返回值,也没有 Exception 在日志里面。有问题的任务日志大体是这样子的(日志的格式是时间、module 名称、进程 ID、线程ID):

这个问题蛋疼的地方在于,没有 Exception 日志,线程就这么罢工了,转而去处理下一个任务了。而且出现的频率大约是 5 分钟一次,我们有一个 staging 环境,一天大约四五次。俗话说如果一个 BUG 能重现,那么就约等于解决了。而这个问题,是我想了好几天都不知道该怎么复现的。出现的时候转瞬即逝,没有留下一丝痕迹……

我们先后尝试了加上更多的 log(但是没有什么用,task 停止的地方很随意,几乎没有什么规律),试图找出这些有问题的 task 的规律(也没有什么规律)。之前听一哥们讲过用 settrace 打印出来所有的调用和执行日志,我写好了 settrace 的代码,打算要用这个东西了,但是试了一下打出来这个日志是在是太慢了,3s 处理完成的任务,开了 settrace 要 40s 才能完成。所以就暂时没有尝试这个方法,打算实在没什么办法了再考虑开这个日志。

今天看 log 的时候,发现线程 ID 虽然一样,但是进程 ID 已经变了。这个进程是 uWSGI 的进程,于是我怀疑是 uWSGI 销毁了这个进程,并且一并销毁了进程里面的线程池。我的理由是:因为 uWSGI 是一个 HTTP 服务器,它只关心我的所有的 HTTP 请求是否处理完了。我们的模型是先返回 HTTP Response,然后再在线程池处理。对于 uWSGI 来说,它看到已经返回 Response 了,就认为这个请求已经处理完了,我就可以关闭这个进程了。

那么 uWSGI 什么情况下会销毁并重启进程呢?我去翻了文档,发现有几个让 uWSGI respawn 的参数,其中有一个是:

max-requests
argument: required_argument

shortcut: -R

parser: uwsgi_opt_set_64bit

help: reload workers after the specified amount of managed requests

uWSGI 在一个 process 处理的请求书达到了 max-reqeusts 就会 respawn 这个 process。

我在本地尝试了一个,开一个 uWSGI 设置 –max-requests=10,发现确实请求数在 10 左右(不是精确的10,比如说下面的日志就是 14)的时候,这个进程会被 respawn,日志如下:

这个时候也确实留下了没有处理完的任务。

OK,这个问题找到了,那么之前的现象也都解释通了,线程不是自己退出的,是父进程被杀掉了,所以没有留下 traceback。

解决的办法

虽然文档和 uWSGI 代码中将 max-reqeusts 作为了一个 required arg,但是从代码的实现上看,如果设置为 0 的话,这一行 if 是永远不会执行的,也就是说 worker process 就不会被重启。于是我在 uWSGI 的配置文件中将 max-requests 选项给删掉了。临时解决了这个问题。

其实这并不是最好的方案,uWSGI 作为一个 HTTP 服务器,应该只用来处理 HTTP 请求。文档里这样说:

During the life-cycle of your webapp you will reload it hundreds of times.

You need reloading for code updates, you need reloading for changes in the uWSGI configuration, you need reloading to reset the state of your app.

Basically, reloading is one of the most simple, frequent and dangerous operation you do every time.

禁用 max-requests 虽然能保证正常情况下 process 不会被 respawn ,但是在不正常的情况,还是有可能出现的。

像是这种耗时的任务,应该通过中间件发往一个专门的 worker 来处理的,通过任务执行的框架来保证每一个任务都被执行到了。

即使不用消息,也可以用 uWSGI 的 spooler 来处理耗时的任务。

被浪费掉的时间

其实如果我早一点去看一下 uWSGI 的日志,应该是可以发现规律的,每 5000 个请求进程被杀一次,而这也正好是未处理完的任务出现的时间。但是我觉得 uWSGI 日志没有什么有用的信息,有 Nginx + django 日志就够了,所以不久前将 uWSGI 的日志 disable 了。现在看来这部分日志还是有用的,毕竟运行的 Nginx 和 uWSGI,这两个进程的运行状态很重要。至于 Django 的日志,属于业务的日志。

另外从日志可以看到,任务被中断之后,Process ID 已经不一样了,但是 Thread ID 一样,我误以为是线程一直存在,只不过线程自己出了问题。试图在线下重现的时候,我也一直用的 Django 的 runserver 模式,而忽略了服务器上跑的进程其实是 uWSGI 的事实。两个进程都不一样,根本不可能重现。

但是这个 BUG 的根本原因,我归咎于我没有仔细看 uWSGI 的文档。文档和外面的 example 几乎都带上了 max-requests 这个参数,于是我也在自己的配置文件里面写上了。但是我误以为它的意思是:uWSGI 等待队列的最大长度,而不是处理过这么多 requests 就重启。

mrluanma 说:

抄东西过来不看是很不好的习惯。我是会把每个选项是否需要都想清楚的。

uWSGI 文档中的 The Art of Graceful Reloading 说:

Finally: Do not blindly copy & paste!

Please, turn on your brain and try to adapt shown configs to your needs, or invent new ones.

Each app and system is different from the others.

Experiment before making a choice.

记住了。

 

证书换至 Let’s Encrypt(手动模式)

今天小伙伴告诉我博客的证书过期了(我隐约想起来 Kaybase 发给过我一封邮件告诉我网站所有权验证失败,被我给忽略掉了)。早上开始 renew 证书。这个过程太乌龙了,记一下吧,顺便推广一下 Let’s Encrypt。

之前的证书是腾讯云的免费 1 年的 TrustAsia 证书,用了应该两年了,这次第一反应也是打开腾讯云续命。后来万万没有想到被腾讯云的这个登陆搞了半天。

没想到腾讯云登陆不上了…… 需要我提供手机验证码,但是我现在的手机号试了十几次都收不到腾讯云的验证码。打联通的客服,客服让我发短信到一个号码,那个号码一直说“系统繁忙,请稍后再试”,网上的诸如 “发送 11111 到 106xxx 的号码” 也不好使,还有网友说让朋友尝试登陆一下他的账号,然后你往朋友收到验证码的那个号码发送 11111,我试过了也不好使。

我决定换成 Let’s Encrypt 的证书。

Let’s Encrypt 的证书一般是由 Certbot 自动签发的。但是像我这种用古老的 PHP 空间提供商的,就只好使用手动模式了。

以下教程是使用 Mac 系统手动申请网站证书的教程。

首先在 Mac 上安装 Certbot:

然后用手动模式开始申请证书(默认的 log 位置需要 root 权限,我直接用 sudo 跑了)。

然后根据提示输入域名,这里可以输入多个。

同意记录申请机器的 IP。

接下来会出现 ACME 挑战,这个挑战的意义是让你证明的域名是你的(否则的话别人就可以申请一个你的域名的证书,用来做中间人攻击了)。ACME 挑战有两种: http 和 dns,http ACME 挑战就是在你网站的 .well-known/ 下面放一个文件,显示特定的文本。dns ACME 挑战是添加一条 TXT DNS 记录到你的 DNS 解析中。默认使用的是 http,如果要使用 dns 的话,最开始的命令要添加一个选项,如下:

这里准备好证明(http 的话就放好那个文件,dns 的话就准备好解析记录),然后自己试一下(一定要试一下,不然失败了要重来),再按下回车,Cerbot 会去验证。

验证过后证书就颁发好了,Certbot 会打印出证书和 key 的路径。有效期只有三个月,所以手动模式的话还是挺麻烦的。