博客迁移到 Cloudflare

周末收到邮件通知,说博客的流量马上要用完了。发现博客的访问量变高了许多,另外我将博客首页展示了最近的 5 篇文章,有时候贴的图片比较多,就比较耗费流量一些。

之前这个博客一直是跑在一个共享的“空间”上,这是在 Docker 出来之前比较流行的一种共享资源的方式。本质上就是我的博客会和其他的一些网站共用一个虚拟机,通过反向代理和 php 等的配置来隔离资源。但是用户没有权限控制 Nginx,php 等进程,也不能 ssh 登录到主机上做一些操作。只能使用服务提供商提供的文件管理页面,编辑特定位置的反向代理文件等。现在看来颇有一些 SaaS 的意味,呵呵。

由于只能使用一些有限的配置,不方便用脚本自动化一些操作,以及流量、存储空间也不高,所以一直想迁移到一个 VPS 上去部署,最近流量用的越来越高了,正好趁这个机会迁移一下吧。

之前的部署形式非常简单,就是使用“空间提供商”给的反向代理,上传我自己的 wordpress 程序,使用空间提供商提供的数据库,然后 CDN 之类的,空间提供商会自己处理。现在打算迁移到的目标结构是:

VPS 部署

在 VPS 上重新部署博客这一步是最麻烦的,本以为很快就可以搞定,但实际上却花了我一天的时间。

由于之前只能使用空间里面的东西,所以很多东西都是很旧的,比如 MySQL, PHP 等,PHP 太旧就导致 WordPress 不能使用最新版。然后就直接导致直接使用 PHP 7.4 无法部署起来我的博客。需要升级 WordPress.

这里有一个很坑的地方是,Wordpress 其实可以自己更新自己,打开后台界面,点击升级,就可以自动下载软件,升级到最新的版本。能通过后台来管理软件,对很多小白用户来说很友好,估计这也是 WordPress 如此流行的一个原因吧。但是其实对运维来说非常不友好,因为这样就没有隔离用户自己的代码和 WordPress 官方的代码,都是混在一起的。如果不通过后台来升级,我就需要自己下载代码,然后看要复制哪一些到我的博客里面;而通过后台升级,又可能会覆盖一些我自己的修改……

按照官方最新版的 Changelog 复制好文件之后,又陆续与到一些其他的坑,比如:

  • 最新版的依赖已经改变了,网上安装 WordPress 的教程有很多,但是大多数都跑不起来,最好自己去 debug,看看缺少什么就安装什么(才发现 WordPress 的古登堡编辑器已经上线了,我打开之后直摇头,这是什么魔幻编辑器……果断用 Classic Editor 切换回默认的编辑器!);
  • 最新版有一些新的数据库表,需要去检查有哪些 DDL 需要手动执行下;
  • 我之前安装了一个 SSL 插件来将 HTTP 请求重定向到 HTTPS, 但是在部署的时候会导致本地也打不开 HTTP, 需要手动将这个插件删除……
  • 之前安装的一些类似于 cache, 评论插件,代码高亮插件,在最新版的 WordPress 下已经无法工作了,需要挨个去检查有什么替代品;
  • ……

挨个处理好之后,总算是跑起来了。还好按照错误日志的信息一步一步来,没有什么特别复杂的坑。

评论通知方案

之前的博客,在访客的评论得到回复之后,会自动向访客发送一封邮件。我觉得这样可以增加一些有意义的讨论。但是自己维护评论系统其实是个大坑。之前的邮件回复系统已经挂了很久了,我竟然不知道…… 不过这次迁到 VPS 去部署,也需要切换到一个国外的服务商来帮我发送邮件,所以是不可避免地要重新折腾一下了吧。

发送邮件总体来说有三个方案:

  1. 直接从 VPS 主机发。基本行不通,VPS 厂商一般会禁用主机直接发邮件,一般的邮件服务都会把 VPS 的 IP 段给屏蔽掉,来阻止垃圾邮件。所以就算你能从 VPS 上发出去,用户一般也收不到;
  2. 通过 Gmail 这样的域名来发送:这个方案比较可行,但是非常复杂,这个其实就通过 API 调用 Gamil,用 [email protected] 的发件人发送。我尝试过使用 Gamil, SendGrid, mailgun, mailchimp 等等,有一些尝试到一半发现只能发 newsletter 这种,不能发送通知;有一些发现到一般需要人工审核;有一些发现必须要付费。其中 Gmail 这个最烦人,就发送邮件的接口,我要去 Google Cloud Platform 申请一个账号,然后申请一个 OAuth, 然后创建一个 Application, 然后为这个 Application 创建一个 Scope, 有一堆让人看不懂是什么的概念,最后也没搞成,Gmail 说我的应用发邮件是敏感的权限,需要人工审核应用;
  3. 通过一些 Email 服务来发送,但是发件使用自己的 mx 记录。最后是通过 sendinblue 发送的。配置还不算特别复杂,只要添加 4 个 txt 的 DNS 记录来验证你拥有这个域名,然后通过 WordPress 插件 WP Mail SMTP 来集成一下就好了。

最后我修改了一下评论框,默认勾选邮件通知,但是用户也可以取消。

博客的评论框选项,欢迎留言~

实际自己测试了一下,发现提交一条评论大约要花 1s 左右,我怀疑这个发送邮件是同步的,也就是说通过 POST 留下一条评论之后,后台要在邮件要发出去之后再通知用户评论成功了。不过对于评论系统来说也可以接受吧……

备份方案

最麻烦的还是来了,得想个办法,能在下一次轻松在新机器上部署好博客。这里的难点是 WordPress 的代码会自己更新,比如在后台点一下就可以更新插件,Wordpress 版本等。要保证下次我部署的时候还是一模一样的版本和代码,不会因为某些地方下载不到了这种事情导致部署不起来……

最后决定是直接用 git 来管理博客的代码,将整个博客的代码放到一个 repo 中,这个 repo 放到 Github 上,然后把机器的 ssh key 加入到我的账号中,给这个机器此 repo 的写权限。这样就相当于我的博客是一个 WordPress 的 fork, 每次在后台更新了博客代码,就使用 git diff 检查一下 diff, commit 到 git 中。

其中,wp-content/uploads 这个目录是我上传的文件,这部分不是代码,不需要用 git 来 track. 为了方便部署又最小化修改,我直接将这个目录软连接到 /var/www/uploads,因为软连接并不需要目标位置一定存在,所以可以将这个软连接也追踪到 git 中。

这样代码的问题就解决了,我可以使用 git 的记录追踪在后台做的更新,可以保证运行所需要的代码都是有备份的。

然后剩下了两部分比较简单的备份:静态文件和 SQL 文件。每天备份即可。

Cloudflare CDN 设置

VPS 能访问通了之后,再部署 Cloudflare, 这样用户的请求永远到达 Cloudflare, 不会直接去访问我的主机,速度更快一些,也更安全。

这里按照 Cloudflare 的教程,将域名的 DNS 交给 Cloudflare 解析即可。但是中间遇到一个问题搞了好久:在设置成 Cloudflare 代理之后,访问域名总是出现 Error 521 Web Sever Is Down. 直接访问又是能访问通的。由于看不到日志,只能通过 Cloudflare 的界面来配置,所以没什么好的方法 debug. 我就直接在主机上 tcpdump 80 端口。发现 80 端口根本没有请求,这就意味这 Cloudflare 没有把用户的请求转发到 80 端口。

反复确认 DNS 的设置没问题之后,我有点怀疑人生了。

最后把 Cloudflare 的面板一个一个挨着看了一遍,发现一个这样的配置:

SSL/TLS 加密模式 配置

默认选择的是“完全”,也就是说,Cloudflare 请求我的网站去的是 443 端口,期望我是有证书的。而我用 CDN 有一个原因就是我懒得管理证书,直接让 Cloudflare 帮我把认证做好就可以了。至于 Cloudflare 访问我的源站,我觉得这里被劫持的概率不高吧……毕竟只是个博客,也不需要这么高的安全性。

改成“灵活”,果然就可以访问了。

Cloudflare 的 IP 设置

由于用了反向代理,实际到达我的主机的 IP 全部都是 Cloudflare 的 IP,如果看不到访客的真实 IP,那么对评论反垃圾的质量也有影响。

所以这里我自己写一个插件,来从 Cloudflare 发来的请求中获取访客的真实 IP(不要害怕,这年头谁上网没有个代理呢是吧),而不是代理的 IP.

  1. 首先在 wp-content 里面创建一个 mu-pluginsmu-plugins 里面的 Plugins 是 Must Use Plugins, 会自动启用;
  2. 然后在 mu-plugins 创建 cloudflare-realip.php;
  3. 在这个文件中输入以下代码:

这段代码的作用就是拿 Cloudflare 发来的真实 IP 字段,来放到 WordPress 要读取的字段中。

打开网站后台,可以看到插件已经启用了。留言测试,现在显示的就不是 Cloudflare 代理的 IP 了。

将缓存直接上传 CDN

有一个插件叫做 WP Cloudflare Super Page Cache, 可以将页面的缓存直接上传到 CDN 上面,这样用户在访问的时候都不会回源,相当于直接访问 Cloudflare 完成了请求。

但是实际测试下来发现区别不是很大,本地 Cache + CDN 已经比较快了。所以就没有用,因为毕竟 “WP-Super-Cache” 是官网的插件,质量要好一些。WP Cloudflare Super Page Cache 测试下来发现有评论之后不能立即看到评论的问题。

DNS 迁出

到这里,基本上所有的配置都好了。但是改 DNS 的时候,竟然丧心病狂地让我人脸识别。实在受不了了,多年以前不懂事,申请域名找了个国内的厂商,后来麻烦事一堆一堆的,又是实名认证又是人脸识别的。这次索性直接将域名转移到 namecheap 来管理了。

转移不算麻烦,只要在原来的注册商那里拿到密码然后去 namecheap 办理转入就行了。

在这个空间上跑了5年,终于要说再见了,哈哈。没想到部署一个博客花了一整个周末……

之前打开页面至少需要四五秒,CDN + 缓存一套搞下来,现在打开速度只要 2s 了,我在新加坡打开只需要不到 300ms, 这个速度也太爽了。真不知道我之前用那个“空间”那么慢是怎么忍受的……

加拿大节点的速度测试

我在新加坡访问速度

还不太清楚 Cloudflare 在国内访问的质量怎么样,如果你发现现在打开这个博客速度有些问题,可以通过评论告诉我一下,谢谢。

 

最近的工作感悟

我在 Shopee 维护一个 Service Mesh 系统,大部分的 RPC 调用要经过这个系统,这个系统每分钟要处理上千万的请求。我们在本文中就把它叫做 Oitsi 系统吧,方便描述一些。干的事情其实和 Istio 是差不多的。

Oitsi 将对 RPC 调用设置了很多错误码,类似于 HTTP 协议的 404, 502 等等。Application 报出来的错误码在一个区间,Oitsi 内部产生的错误在另一个区间,比如 0-1000,类似于 System Internal Error. 监控这些错误码可以让我们知道这个系统的运行情况。

这个系统自从接手之后就有一个问题,就是它每时每刻都在报出来很多内部错误,比如发生内部超时,路由信息找不到,等等,每分钟有上万个错误。然而,系统的运行是完全正常的。

Oitsi 系统在正常情况下的错误

从这个脱敏之后的监控可以看到,经常有一些错误一下子动辄上万,除了图中几 K 的那些错误,在 1K 以下有更多密集的错误,只不过它们都被其他巨量的错误给拉平了,在这张图不明显。

这就给我们造成了很多问题:到底是 Oitsi 真出了问题,还是属于“正常的错误”?很难判断,每次发生这种情况都费时费力。大部分情况都是排查一番,然后发现是用户“滥用”造成的问题,不需要关心。而它又掩盖了很多真实的问题,比如一个新的版本发布之后偶尔会有一些内部的错误,是不应该发生的,却被真实的问题掩盖住了。基于这样的监控数据我们也无法设置告警,因为这些噪音太多了,即使有告警,也和没有一样。

这让我想起之前在蚂蚁的工作,我们有类似的问题。我有一年多的时间都在一个叫做“故障定位”的项目上。在蚂蚁我们也有很多告警(99%的)都是无效的,给 On Call 的同事带来很多噪音和打扰。在蚂蚁的思路是:开发一个“智能系统”(AI Ops),当告警发生的时候,自动地判断这个告警是不是噪音,是不是真正的问题,问题出在了哪里。拿到 Oitsi 的例子上说,当现在一个错误的数量突增,那么这个智能故障定位系统就去检查 Oitsi 的一些指标是否正常,导致告警的服务具体是什么,它之前是不是一直有类似的监控曲线模式,如果有,说明它一直在发生,是正常的,我们可以不管。

这样做了一年,效果还是不怎么样。我倒是发现,很多告警的规则本身就有问题,比如一个请求量每分钟只有两位数的服务,领导的要求是 “1分钟发现故障,5分钟定位故障”,不要说自动定位,就算是人去判断都不靠谱。为了达成这个目标,监控团队设置了很多非常敏锐的告警,交给定位团队说:“我们负责发现问题,你们负责定位问题。如果出问题了,1分钟之内有告警触发,那么我们的工作就达标了。但是至于没有问题我们也触发了很多噪音告警,就是你们的工作了。”  它们的 KPI 确实是完成了,只要有故障必定有告警。但事实是,在很多情况下,告警发出来,大家打开监控,盯着监控:“再等等看,看下一分钟,有请求进来了,服务没问题!”

所以这一年工作里,我有一个想法,就是在源头解决问题比使用高级的魔法系统去解决问题要简单、彻底很多。我们真的需要这么多人来开发一个“魔法系统”来帮我们诊断这种问题吗?

比如监控配置的不对,那就优化监控。监控为什么配置的不对?监控系统太难用,UI 让人捉摸不透,配置了告警无法调试,监控只能保存7天的数据,不能基于历史的监控数据配置告警。很多人为了“规则”,对服务配上了告警然后就走了,至于后面告警触发了,也不去响应。

回到 Oitsi 的问题上,我找了几个服务,发现这些 Oitsi 内部错误上并不能完全说是“正常的错误”,毕竟它是错误,没有错误会是正常的。只能说它没有导致线上问题而已。它们是可以被修复的。于是一个月前,我决定从源头去解决这些问题。把所有不应该报告出来的错误都消灭掉。

乍一看这么多错误数,用那么多团队在用,看起来是难以管理的,性价比非常低的工作。但是毕竟也没有人催我要快点完成,我可以一点一点去做。做一点错误就少一些(只要我解决问题的速度比新的问题出现的速度快)。

于是我按照下面的流程开始处理:

  1. 在 Jira(我们内部的工单系统)建立一个专题 tag,叫做 oitsi-abuse, 后面的工单可以关联这个 tag, 这样,可以在处理的时候方便参考之前的 Case;
  2. 创建一个监控,专门针对错误做一个面板,点击面板右侧的 Legend 可以直接跳到服务的监控面板,在服务的监控面板上显示下游,并且关联 CMDB 的 PIC(Person in charge).
  3. 这样,我从错误数最高的服务开始,查看监控,看下游服务,以及机器上的日志,看相关的错误码是什么时候开始的,到底是什么引起的,确定了是服务的问题就创建工单给这个服务的负责人,然后跟他联系,说明这个有什么问题,会对我们的监控、告警造成什么影响,需要修复。
  4. 等他确认问题,然后要求提供一个 ETA (预计修复的时间),把 ETA 写到工单中,到了时间去检查确认。
  5. 如果是 Oitsi 本身的问题,去找 Oitsi 开发同事排查问题。
  6. 等所有的问题都解决了的话,对错误设置告警,一有错误就去联系开发。一般情况下,都是他们做的配置变更或者发布引起了问题。这样对于业务其实是更加健康的,我们发现问题的能力更强了。

就这样,其实这样坐下来就发现只有那么几类问题,排查的速度越来越快。中间还发现一个库,它会去对 Oitsi 服务做心跳检查,这个检查设置不当会有一些错误。很多引用了这个库的应用都有一直在报错误的问题。但是我们系统本身其实已经做了探活可以保证心跳之类的问题了,沟通之后这个库的心跳检查行为可以下线。于是库发布了新的版本,我找所有的引用者去升级版本,很多错误一下子就消失了,非常有成就感。

这项工作的进度比我想象中的要快,一个多月,联系了 20 多个团队。虽然说也遇到了一些很扯的事情,明明是服务 A 的问题,就直接让我去找下游,让我们排查半天,最后又说回来找服务 A 负责人,拉了个群,摆出来日志,才承认是自己的问题,开始排查。但是大部分团队都非常配合,说明问题之后马上去排查,发现问题下一个版本就修复了。如此默契的合作让我感到惊讶又幸福!现在,系统错误维持在 200 以下了,并且现有的错误都已经找到了根因,还有3个服务待修复。最晚的会在 2 个周之后发布修复。可以预见到在不远的未来,这个系统将会成为一个 0 错误的系统!

今天系统报出的错误,还是有一些服务在一直报错,不过已经大大减少了。

这项工作虽然不涉及任何的 KPI 之类的,也没有什么技术含量,还都是一些“沟通”的工作,但是却带给我很大的成就感。我相信它也会在未来节省我很多时间。比如说我们评估系统的 SLI 和 SLO,由于 false alarm 太多,导致要花很多工作确定 down time 有多少,现在直接通过监控就可以确定了。

这项工作带给我的一些感想:

  1. 从源头解决问题最彻底;
  2. 不要害怕沟通;
  3. 错误的发生都有原因,排查下去,零就是零,一就是一(从这个 Case 看,也确实所有的错误都可以被解决的);
  4. 每个公司都有脏活,累活(毕业去的第一家公司维护爬虫,也有很多脏活、累活),这些都需要有人去做;

需要补充一下,我并不是完全否定做故障定位的思路。毕竟之前在蚂蚁,有四五个组在做相同的东西,我们(和其他做一样东西的组)尝试过非常多的思路,也有很多人因为这些晋升了(你说去联系了无数个团队,排查了很多问题,这有什么 impact 呢?你说自己做了一个“智能定位”系统,晋升就稳了吧。)。印象比较深刻的是有个项目制定了上千个(他们称为)决策树,简单来说就是:如果发生这个,就去检查这个。颇有成效,很多配置不当的告警就被这种规则给过滤掉了(虽然我觉得直接改报警要好一些)。我非常佩服他们的毅力。

说了这么多湿货,再说点干货。我们其实还有一个问题没有解决。如果读者有思路,欢迎评论:

在 Service Mesh 中,所有的服务都是通过 Agent 来调用的。比如 App1 要调用 App2,它会把请求发到本地的 Agent 中,由 Agent 去调用 App2 所在机器的 Agent.

这里,超时的问题就难处理。比如我们设置了 1s 超时。假如说 server 端的 Application 超时了,那么 Server 段的 Agent 可以报告一个应用超时错误,不算做我们 Oitsi 系统错误。但是对于客户端的 Agent 呢?它无法知道到底是 Server 的应用超时了,还是 Server 的 Agent 超时了。所以对于 Server 超时的情况下,客户端的 Agent 总会报出一个内部超时错误。

这种错误,我们当前还是无法区分是否是由应用引起的。

有关这个超时问题,可以看下我们在 Twitter 上的讨论:https://twitter.com/laixintao/status/1407885941541203973


2021年07月09日更新:

有关错误的告警和噪音问题,Cloudflare 提出了一种方法,通过 SLO 来监控错误:Smart(er) Origin Service Level Monitoring。为了避免噪音,这里监控的目标是 burn rate, 即 SLO 被消耗的速度。为了减少这个速度带来的噪音,需要达到两个条件才能 fire 告警:

  1. short indicator
  2. long indicator

短时间内 burn rate 上升很快,会触发 short indicator, 而又需要持续一些时间,才能触发 long indicator.


2021年10月1日更新:

看了一篇类似的文章:Embrace the Grind

piglei 的翻译:《拥抱磨砺

 

P99 是如何计算的

Latency (延迟)是我们在监控线上的组件运行情况的一个非常重要的指标,它可以告诉我们请求在多少时间内完成。监控 Latency 是一个很微妙的事情,比如,假如一分钟有 1亿次请求,你就有了 1亿个数字。如何从这些数字中反映出用户的真实体验呢?

之前的公司用平均值来反应所有有关延迟的数据,这样的好处是计算量小,实施简单。只需要记录所有请求的一个时间总和,以及请求次数,两个数字,就可以计算出平均耗时。但问题是,平均耗时非常容易掩盖真实的问题。比如现在有 1% 的请求非常慢,但是其余的请求很快,那么这 1% 的请求耗时会被其他的 99% 给拉平,将真正的问题掩盖。

所以更加科学的一种监控方式是观察 P99/P95/P90 等,叫做 Quantile。简单的理解,P99 就是第 99% 个请求所用的耗时。假如 P99 现在是 10ms, 那么我们可以说 “99% 的请求都在 10ms 内完成”。虽然在一些请求量较小的情况下,P99 可能受长尾请求的影响。但是由于 SRE 一般不会给在量小的业务上花费太多精力,所以这个问题并不是很大。

但是计算就成了一个问题。P99 是计算时间的分布,所以我们是否要保存下来 1亿个请求的时间,才能知道第 99% 的请求所用的时间呢?

这样耗费的资源太大了。考虑到监控所需要的数据对准确性的要求并不高。比如说 P99 实际上是 15.7ms 但是计算得到数据是 15.5ms,甚至是 14ms,我认为都是可以接受的。我们关注更多的是它的变化。“P99 耗时从 10.7ms 上涨到了 14ms” 和 “P99耗时从 11ms 上涨到了 15.5ms” 这个信息对于我们来说区别并不是很大。(当然了,如果是用于衡量服务是否达到了服务等级协议 SLO 的话,还是很大的。这样需要合理地规划 Bucket 来提高准确性)。

所以基于这个,Prometheus 采用了一种非常巧妙的数据结构来计算 Quantile: Histogram.

Histogram 本质上是一些桶。举例子说,我们为了计算 P99,可以将所有的请求分成 10 个桶,第一个存放 0-1ms 完成的请求的数量,后面 9 个桶存放的请求耗时上区间分别是 5ms 10ms 50ms 100ms 200ms 300ms 500ms 1s 2s. 这样只要保存 10 个数字就可以了。要计算 P99 的话,只需要知道第 99% 个数字落在了哪一个桶,比如说落在了 300ms-500ms 的桶,那我们就可以说现在的 99% 的请求都在 500ms 之内完成(这样说不太准确,如果准确的说,应该是第 99% 个请求在 300ms – 500ms 之间完成)。这些数据也可以用来计算 P90, P95 等等。

由于我们的监控一般是绘制一条曲线,而不是一个区间。所以 P99 在 300-500 之间是不行的,需要计算出一个数字来。

Prometheus 是假设每一个桶内的数据都是线性分布的,比如说现在 300-500 的桶里面一共有 100 个请求,小于300个桶里面一共有 9850 个请求。所有的桶一共有 1万个请求。那么我们要找的 P99 其实是第 10000 * 0.99 = 9900 个请求。第 9900 个请求在 300-500 的桶里面是第 9900 – 9850 = 50 个请求。根据桶里面都是线性分布的假设,第50个请求在这个桶里面的耗时是 (500 – 300) * (50/100) = 400ms, 即 P99 就是 400ms.

可以注意到因为是基于线性分布的假设,不是准确的数据。比如假设 300-500 的桶中耗时最高的请求也只有 310ms, 得到的计算结果也会是 400ms. 桶的区间越大,越不准确,桶的区间越小,越准确。


写这篇文章,是因为昨天同事跑来问我,“为啥我的日志显示最慢的请求也才 1s 多,但是这个 P999 latency 显示是 3s?”

我查了一下确实如他所说,但是这个结果确实预期的。因为我们设置的桶的分布是:10ms, 50ms, 100ms, 500ms, 1s, 5s, 10s, 60s.

如上所说,Promtheus 只能保证 P999 latency 落在了 1s – 5s 之间,但不能保证误差。

如果要计算准确的 Quantile, 可以使用 Summary 计算。简单来说,这个算法没有分桶,是直接在机器上计算准确的 P99 的值,然后保存 P99 这个数字。但问题一个是在机器本地计算,而不是在 Prometheus 机器上计算,会占用业务机器的资源;另一个是无法聚合,如果我们有很多实例,知道每一个实例的 P99 是没有什么意义的,我们更想知道所有请求的 P99. 显然,原始的信息已经丢失,这个 P99 per instance 是无法支持继续计算的。

另外一个设计巧妙的地方是,300-500 这个桶保存的并不是 300-500 耗时的请求数,而是 <500ms 的请求数。也就是说,后面的桶的请求数总是包含了它前面的所有的桶。这样的好处是,虽然我们保存的数据没有增加(还是10个数字),但是保存的信息增加了。假如说中间丢弃一个桶,依然能够计算出来 P99. 在某些情况下非常有用,比如监控资源不够了,我们可以临时不收集前5个桶,依然可以计算 P99.

 

使用 mtr 检查网络问题,以及注意事项

在检查两个 IP 之间的网络情况的时候,常用的工具有两个:ping 可以检查两个 IP 之间通不通,以及延迟有多少;traceroute 可以检查从一个 IP 到另一个 IP 需要经过哪些 hop。

mtr 将这两者结合了起来:使用 traceroute 将两个 IP 之间需要经过的 hop 找出来,然后依次去 ping 这些 hop,就可以看到当前的 IP 到所有的这些 hop 的延迟和丢包率,这样在某些情况下就可以诊断出来丢包和延迟发生在哪一个节点上。

mtr 的安装和使用非常简单,和 ping 类似,只要执行 mtr <ip> 命令,就可以得到如下的界面:

这里展示的是从一台 DigitalOcean 的机器上到 1.1.1.1 这个 IP 的每一个 hop 信息。这个界面非常简单易懂,列出了从当前 IP 到目标 IP 之间要经过的 hop,中间的丢包率和 ping 出来的延迟。这个界面和 htop 一样,可以调整 Display Mode, fields 的显示顺序等,按照界面提示操作即可。

mtr 在使用的时候有一些需要注意的地方。

中间节点探测结果不一致问题

经常会看到中间某些节点丢包率比后面的节点还要高,这可能是中间节点对 ICMP 协议限速,导致中间节点可能看到 packet loss, 但是后面的节点没有,或者后面节点 loss 的数量比前面少。这种情况下,永远相信后面的节点。原理很简单,mtr 和 traceroute 的原理类似,都是发送 TTL=1,2,3,4,5… 的包探测出 IP 包的路由节点,然后去 ping 这些节点。所以这里的丢包率是从本地依次 ping 这些节点的丢包率。假如中间某个节点发生了丢包,那么它后面的节点一定会丢包,因为后面节点要可达必须经过中间的节点。像上面图中那种情况,第 2 个节点有 10% 的丢包率,后面的反而没有,说明节点 2 并不是真正地丢包,只是对你的 ping 丢了包,实际的包没有丢。

如果发生真正的丢包,会是这样子:

从某一个节点开始,后面的节点都会发生丢包。

但是其实也可以看到,节点 3 和 4 5 比后面的节点的丢包率要高,说明真实的丢包率只是 40%. 即丢包率以后面的节点为准

Latency 也同理,可能看到中间节点的 latency 比后面的要高。很显然,如果它 latency 真的高,那么后面节点的 latency 不能比前面节点的 latency 还小。所以 latency 不一致的情况下,以后面节点的为准。

来回路径不一致的问题

发送过去的包的路由,和返回的包的路由,并不总是一致的。所以如果有条件的话,最好从两端都使用 mtr 进行诊断,或许会发现不一样的线索。

但是对于 Latency 来说,如果两边的路由一致,但实际只有在一边去向另一边的时候有延迟,那 mtr 是无法检测出来的。因为 mtr 本质上是用 ping 来检测延迟,ping 只能得到来回总共的时间,不能得到单边的时间。

中间出现 ??? 的情况

如上图,第一个节点。有时候 mtr 的报告中会出现 ??? 的标志,这是因为 traceroute 拿不到中间节点的信息,一般是因为这个节点被设置成不回应 ICMP 包,但是能够正常转发包。所以这种情况下即不能拿到 IP,也无法测试丢包率,延迟等。

使用 tcp

现在的 mtr 也支持通过 tcp ping 了,发送 SYN 包进行探测,但是很多设备不会回应 TCP 包,所以会看到大量的 packet loss, 带来很多误导。

不要去排查每一个网络问题!

不要去试图明白每一次丢包背后的原因。网络协议本身就是设计成有很多容错和降级的。任何时候都有可能发生路有错误,网络拥塞,设备维护等问题。如果 mtr 显示丢包率有 10%,一般不会有什么大问题,因为一般的上层应用协议都会处理好这部分丢包。如果去排查每一次网络丢包问题,只会徒增人力成本。

(真希望我亲爱的开发同学们能理解这一点)

参考资料:

  1. Diagnosing Network Issues with MTR
  2. Traceroute: Finding meaning among the stars
  3. Understanding the Ping and Traceroute Commands
 

在终端快速选中上一个命令的输出内容

在终端里面一个非常日常的操作,就是选中上一个命令的输出内容。比如使用文本处理程序处理一些服务器的 IP,最后得到一个结果,将这个结果复制给同事,或者粘贴到工单系统中;在终端上使用 date 程序转换日期的格式,最后要将这个日期复制到别的地方去使用;比如最常见的一个操作,使用 date +%s 命令得到当前的时间戳,然后复制这个时间戳。如果你使用终端的话,基本上每天都要重复这个操作几十次。

本文就来讨论这个最简单的操作:复制上一个命令的输出结果。

虽然是一个看似很简单的操作,但是我却为了如何能在这个操作上节省几秒钟苦苦思索了多年。也发现了很多人同样在寻找一个方法来高效地执行这个操作。这篇文章将会讨论几种方法来实现这个动作,虽然最后我使用的方法并不是我发明的。发明它的人也同样花了很长的时间(按作者原话说 “Look, it’s still quarantine, okay?”),所以背后的奇技淫巧和神奇的思路也同样精彩!希望这篇文章能给读者每天节省几秒钟,也能在阅读的过程中带来一些乐趣。

笨蛋的方法

这是最显而易见的一种方法。为了复制上一个命令得到的内容,我们要将右手拿开,放到鼠标上,选中文本,然后按下 Command + C (在其他的系统上是别的按键),将内容复制到剪切板里面。

也同样显而易见,这么做太浪费时间了。首先,所有需要将手从键盘上拿开的操作都是浪费时间的;其次,选中操作也不是那么简单,开头和结尾需要定位两次。使用键盘是一个 0 或 1 的操作。按下了就是按下了,没有按就是没有按,闭上眼睛也能操作。而鼠标需要精确地定位,闭上眼睛是绝对无法完成的。如果遇到要复制的命令有很长的输入(比如要复制一段 cat info.log | grep code=404 | tail 的日志输出),那么要同时使用鼠标进行定位和翻页,变成了一个超高难度的动作。

这样增加心智的东西,太反人类了。

朴素的方法

Unix 系统里面的管道真是一个伟大的发明!因为在终端里面程序的输出是一个 stdout, 所以理论上,我们就可以使用一个程序,将它的 stdout 导入到系统的剪切板里面去。比如在 Mac OS X 上面可以使用 pbcopy 将程序的输出内容导入到剪切板中,然后使用 pbpaste 粘贴出来,或者直接使用 Command + V 粘贴。

在其它的 Linux 系统中也可以做到类似的事情,比如 xselxclip 。其实原理非常简单,只需要调用系统提供的剪切板相关的 API,将 stdin 的内容写入到进去就好了。

类似这样使用管道的工具还有很多,比如 fpp 工具。可以自动地识别出来 stdout 中的 file path,然后提供一个 GUI 让你选择文件,按下 Enter 打开。比如 git stat | fpp 这样用。

这种方法的优点是可靠,不涉及鼠标操作。虽然也并不是特别高效,因为要敲很多字母(不过可以使用 alias)。

这类使用管道的最大的缺点就是不是所见即所得的。很多时候需要敲下命令,看到 stdout 确认没有问题,然后再敲一遍命令后面加上 | pbcopy 加到剪切板中,在遇到运行时间很长,或者需要消耗很大资源的时候,就有点不合适了。虽然可以一次性使用 tee 程序既输出到 stdout 又输出到 pipe 中,但是这样一来运行命令的心智负担又太大了。这么长的命令难以形成肌肉记忆,所以本质上来说,效率也算是特别高。

优雅的方法

另一个既简单又傻瓜的方法是使用 iTerm2 自带的功能,在 iTerm2 中选择 “Edit -> Select Output of Last Command” 即可选中上一条命令的输出,使用快捷键的话是 Command + Shift + A .

如果你看到这个选项是灰色的,说明你没有安装 shell 集成。在菜单栏选择 “Install Shell Integration” 即可,iTerm2 会帮你执行一个 Curl xxx | bash 来安装相关的依赖。

这种方法的优点是使用足够简单,一个快捷键就够了,而且这是选中+复制,并不需要再按下 Command + C。如果大部分时间使用的终端模拟器都是 iTerm2 的话,这个方法也足够了。

缺点也显而易见,这是 iTerm2 提供的功能,如果你要使用 Ubuntu,就不行了。另外,它的工作原理是,它知道你在 iTerm2 中运行的命令,所以可以捕获命令的输出信息。这样就带来一些很严重的问题,比如,如果你使用 Tmux 的,那么在 iTerm2 看来,无论你在 Tmux 里面开多少个 session 和 window, 对它来说都是一个程序,也就无法在 Tmux 里面成功捕获 stdout 了。

Tmux 可能有人不用,那还有一个场景应该无法避免,就是 ssh. 同理,你 ssh 到一台机器上去执行命令,对于 iTerm2 来说它都只看到一个 ssh 命令,所以如果这样复制的话,它会把你在 ssh 命令下看到的所有内容都复制下来。(其实上面提到的 pbcopy 同理,也无法在 ssh 远程机器上工作的。)

而要想在 ssh 下也工作,就必须不区别是在远程机器上执行的命令,还是本地执行的命令,从整个终端模拟器的 buffer 入手。使用正则匹配或许是个好的方法。

黑客的方法

由于没有一个方法能够省心省力地完成这个工作,我这几年来每天都过得郁郁寡欢。

某天在 hackernews 上看有人分享 Tmux 复制文本的操作方法,就点进去读了一下,稍微有些失望,因为这些东西我已经知道了。但是这时候网页突然载入完成了播客中的 gif,在 gif 中发现有一段竟然是在命令的 output 之间跳来挑去!这就是我苦苦寻找多年的东西!

在确认这并不是 Tmux 本身的功能之后,我发邮件问了作者是如何做到能在 Tmux 里面快速选择上一命令输出的。

没想到作者很快回复了我的邮件。

整个 idea 非常简单,使用一个脚本即可实现,只用到了 Tmux 自身的命令。核心思想是去复制当前 cursor 所在的 Shell Prompt 和上一个 Shell Prompt 之间的内容,使用 Tmux 的命令控制光标移动,选择文字。

脚本如下(现在作者有一篇博客,Quickly copy the output of the last shell command you ran ,很详细地介绍了这个脚本每一步都在干什么)。

bind -n 的意思是将这个操作绑定到 root key table,默认是绑定到 Prefix table,改成绑定到 root 的话,这个操作就不涉及按下 Tmux 的 Prefix key 了。S-M-Up 是 Shift + Option + Up 这三个键一起按下的意思,即将这三个键一起按下绑定成下面这个脚本。

然后这个脚本进入 copy-mode,先控制光标到行头。之后分成两个 block,首先看 if 不满足的下面的那个 block,基本上就是向前寻找之前的一个 Shell Prompt,如果找到了,就从这里开始复制,这样,两个 Shell Prompt 之间的内容就被选中了。再来看 if 里面的内容,意思是当前行如果有 Shell Prompt 的话,就直接复制整行。这样就可以做到,依次往上选中上一个 output,上一个命令, 再上一个 output,再上一个命令,…… 缺点就是只能支持向上选择,不支持向下选择。不过其实也够用了。if 里面的那个嵌套的 if 是处理 Tmux 在 vi 的 copy-mode 下的一个 Corner case, 详细的解释可以去看原文。

这里有一个很 triky 的地方,就是如果你的 Shell Prompt 的格式里面有空格的话,比如以 $␣ 来结尾,在 Tmux 的复制模式下,对于没有执行过命令的行,比如多按了几次回车,Tmux 会直接将这些行中 Shell Prompt 的空格删除,这样就造成我们的脚本无法匹配到空格。比如下面这个 Shell,复制模式下在 date 和 echo 命令中间的三行就没有空格了。

这里解决的方法是,将 Shell Prompt 最后的空格,改成 Non-breaking space, Unicode 码是 \u00A0 。(可以看到,上面的脚本匹配的其实就是这个 Unicode)。如果使用 Vim 的话,可以在输入模式下按下 Ctrl + V ,进入ins-special-keys mode, 然后依次输入 u 0 0 a 0,就可以输入这个 Unicode。

这样,对我来说几乎就是一个完美的方案了。如果去读作者的博客,就会发现这里面的坑实在太多了,Tmux 在 vi 的 copy mode 下的行为,去掉空格的行为,跳转行为(在行被 Wrap 的情况下必须执行两次 start-of-line 才能真正跳转到行头,等等。估计作者也是花了很多时间才写好这个脚本。

在 ssh 的情况下,理论上也可以做到,因为这个方法是针对 Tmux 显示的 buffer 进行操作。但是要改下这个脚本的匹配,因为远程的主机的 Prompt 可能和你本地电脑不一样。上面的脚本使用的 search-forward-text ,如果改成 search-forward 就可以按照正则搜索。

没有实现的方法

这个方法是我很久之前做的一个尝试,只不过到现在都还没有完成。

之前看到过这么一个项目:tmux-url-select。它能帮助你快速选择当前 Tmux 窗口中的 URL,复制或者打开。

我去看了一下代码,发现思路非常神奇。它是这么做的:

  1. 先 capture 下来当前窗口的全部内容;
  2. 打开一个新的窗口,覆盖掉了原来的窗口;
  3. 新的窗口其实是一个新的 GUI 程序,然后将老窗口的内容放上来,在这个程序内实现了选择、跳转、定义按键等工作;

由于用户实际上是进入了一个新的程序,但是这个新的程序通过修改 Tmux 窗口的名字,让用户还感觉自己在 Tmux 中一样。由于是一个新的程序,那它就可以不受限制的做到任何是事情了!

所以我看到这个东西,第一个想法就是将它 fork 过来,将选择 URL 改成选择上一个命令的 output. 实际上也应该是可行的。到现在没有完成的原因是…… 这个项目是 Perl 写的,Perl 看起来不像是人写的东西。

Ian (上文提到的作者)也说,思路很有趣,但是长期来看,不如花时间提升 Tmux 自身的 copy mode 收益更多。他准备提交 patch 给 Tmux.

其他的方法

在和作者 Ian 的交流中,他还告诉我其他一些他使用的工具,也非常实用。

tmux-thumbs 这个很有意思。这个就像是 vimium/vimperator 的操作模式一样,可以让你快速通过一些按键去选择当前 buffer 的文本块。

extrakto 和上面的工具类似,但是这个工具是用的 fzf 模式。可以通过模糊搜索,查找当前 buffer 出现过的文本,进行快速选中。但是好像不能复制出到剪切板。


2021年07月06日更新:

我发现在服务器上复制命令的时候,连同命令本身以及 prompt 一起复制更加实用,因为我们的 prompt 带有机器的 hostname,包括机器标签,ip 等。复制命令可以让同事知道这个 output 是怎么来的,打开的文件路径?awk?grep?都可以一眼就能看出来。所以我将这个脚本改了一下,← 是只复制 output,↑ 是向上复制 prompt、命令以及 output,↓ 也一样,只不过是向下选择。

详情见:https://twitter.com/laixintao/status/1412081667498332161

实现代码如下( commit ,可以关注下 myrc 这个仓库,我将我的所有配置文件都放在这里,从这里可以看到最新的版本):