为什么 Cloudflare 提供免费的服务?

在我的博客切换到 Cloudflare 的 CDN 上之后,我发现速度快的惊人,原来 18s 才能加载完首页的全部内容,现在只需要 0.5s 左右就全部加载完了。在 Cloudlflare 的后台看到,我这个小博客每天消耗的 Cloudflare 的流量在 3G 左右。高的时候可能有 10G 了。

CDN 的流量费用一般是 $0.10/GB, 那么 Cloudflare 为什么能够给用户提供免费服务呢?在网上找到了一个 2016 年的回答(有趣的是,这个回答里面说 CDN 的流量是 $0.10/GB, 5 年之后的今天这个价格似乎没怎么变化),列出了 Cloudflare 能这样做的 5 个原因:

  1. 数据。有了广泛的免费用户,Cloudflare 就能获得更多的数据,从而为付费用户提供更好的保护服务;
  2. 客户推荐。很多 Cloudflare 的免费用户后来将 Cloudflare 带到了工作中。很多头部客户都来自于客户雇员使用了 Cloudflare 的免费服务,而向公司推荐;
  3. 人才推荐。Cloudflare 希望雇佣世界上最聪明的人。很多企业 SaaS 服务提供者都在招人方面花费了很多钱。Cloudflare 没有这么多的投入,但是也有源源不断的人才推荐。在 2015 年,Cloudflare 的面试雇佣率是 1.6%, 与一些最大的 ToC 互联网公司相当;
  4. QA. Cloudflare 面临的一个问题是,如何在产品级别的规模下测试软件的质量。现在,当有新的功能的时候,他们一般先推向免费用户(实际在我使用的过程中,发现 Cloudflare 有一些 Feature, 作为免费用户,是无法关闭的)。很多志愿者会测试这些 Feature 并提交 bug. 这让 Cloudflare 产品的迭代速度比一般的 SaaS 公司都要快;
  5. 带宽鸡与蛋的问题。通常,为了得到更便宜的带宽,需要购买更大的规模,但是要从付费用户那里获得大规模使用,你首先要给他们极具竞争力的价格。免费用户解决了这个先有鸡还是先有蛋的问题。Cloudflare 用户的多样性让他们有了很大的筹码,可以让他们去和各地的电信服务商谈判,做流量对等交换协议。极大降低了带宽成本。

今天(2016年),Cloudflare 的毛利率超过了 70%, 即使大部分客户没有付钱,他们依然可以实现收支平衡。

Cloudflare 创办于 2009 年,已经在去年(2020年)上市,现在市值 327 亿美金。上面这个回答,来自于5年前,Cloudflare 的 CEO Matthew Prince.

 

博客迁移到 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.

 

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