开源软件源码编译指南

之前在博客介绍了 Linux From Scratch, 最近发现 LFS 已经有新的版本 10.1 了,周末打算重新编译一下。

LFS 在首页推荐了两个教程,可以学习编译软件的一些基础常识,一个是 TLDP 的 Building and Installing Software Packages for Linux另一个是 Beginner’s Guide to Installing from Source,相当于是 TLDP 的简略版。边学习边记了一些笔记和总结。那这篇博客,就相当于是简略版的简略版吧。

软件的开发和发行

一个操作系统除了内核,还需要成千上万的软件才能真正去完成一些工作。专有系统一般会帮你管理这些软件,开源系统的软件则是由世界各地的开发者贡献的。这些软件通常不仅可以在 Linux 上使用,只要版权允许,也可以在 BSD 等系统使用,如果版权允许,有些还可以在专有系统运行。比如 Mac, Solaris 系统等。

开源软件发行中有两个常见的术语:

  • 上游(Upstream): 通常指的是软件的原作者;
  • 下游(Downstream): 通常指的是发行版,和用户;

大部分的开源软件都会开放一个版本控制系统管理的代码,任何人可以匿名下载代码,比如从 Github clone. 使用版本管理软件下载代码包含软件的开发历史(commit),可能会比较慢,软件在发布(release)的时候通常会将代码打包提供下载,这样下载就不包含代码提交历史,所以下载会更快一些,也能给世界节省一些带宽,减缓全球气候变暖。也有一些软件没有开放版本控制,只有定期打包开放的代码,这种就只能下载不带历史的源代码了。

发行版,Binary,和编译

Linux (以及其他的操作系统)有很多的发行版,这些发行版所帮你做的一个重要的事情就是给流行的软件打包。让你可以直接从 apt/dnf 等 package manager 下载安装软件。这些 package manager 有些是直接分发 binary 的,这意味着你在安装的时候直接下载下来放到 $PATH 下面就好了,并不需要自己编译。

Package manager 带来的好处是:

  • 方便:要找什么软件直接从它们提供的 repository 下载就好了;
  • 安全:package manager 在下载的时候会自动校验 md5,发行版的开发者也会时刻关注安全问题,所维护的打包软件会及时地做安全更新;

但是另一方面,发行版要维持一定的稳定性。比如你在特定版本的 Ubuntu 上,从官方的 Repository 就只能安装到特定版本的 Python,而不能安装最新的。(当然也有发行版的策略是永远提供最新版的软件。)

在这种情况下如果你想使用最新版的软件,就需要自己编译了。

另一种可能需要自己编译软件的情况是定制。一般开源软件会提供一些参数,比如删掉不必要的功能。这些定制都是要在编译阶段配置的,发行版要兼顾大部分的用户,所以一般都会尽量打开所有的 Feature,如果你需要特定的配置,就需要自己编译了。

自己编译软件的时候也要注意不要搞乱了你的 package manager, 后面可能出现意想不到的错误。最好的方法是阅读对应的 package specification 然后使用 package manager 来编译和安装软件。

源码下载和安全

一般的软件会提供 http 服务或者 ftp 服务让大家下载。大部分人应该都会从网上下载东西,在浏览器里面使用右键,另存为,就可以了。在终端上可以使用 wget 程序或者 curl 来下载。

除了从官网下载之外,一般还会有很多 mirror,其实就是散步在世界各地的备份。这样一来可以备份软件,而来可以让用户就近下载,节省原服务器的带宽,也节省整个世界的带宽。

tarball

之前介绍过文件系统。可以知道,不同的文件系统对于目录(文件夹)和文件的组织方式并不相同。所以在互联网上传输的一般是打包成一个的文件而不是文件夹。

文件打包说白了就是将所有的文件 append 到一个文件中,然后创建一个 “index”,里面包含了所有的文件信息,比如每个文件的名字,offset,length,index 等等,方便不必解包就可以知道里面都有什么,甚至可以从中提取出来某一个文件而不必完全解包。

tar 程序是最常见的打包程序(参考之前的这篇博客)。名字的意思是 “tape archive”,虽然现在文件已经都不存储在 tape 上,而是 disk 上了…… 用 tar 打包的文件通常叫做 tarball. 用 tar 打包可以保留操作系统的权限信息,以及文件的 owner id 等信息。

因为源代码都是文本,压缩比率非常高,很适合压缩之后传输。tar 仅仅是打包,一般打包之后还会进行压缩。常见的压缩有:

  • gzip: 文件后缀是 .tar.gz 表示是一个使用 gzip 压缩之后的 tarball
  • bzip2: 后缀一般是 .tar.bz2
  • xz: 后缀是 .tar.xz

tar 现在的版本已经非常智能了,可以自动识别文件然后调用对应的解压工具进行解压,比如这么使用:

但是 tar 是一个非常古老的程序了,是否支持自动解压取决于版本,参数的使用也取决于版本。本着 Unix 的哲学:一个工具只做一件事情。我比较喜欢这么用,解压交给解压工具,解包交给解包工具:

tar 在解压的时候,如果在当前目录存在重名文件,会直接覆盖掉。一般在软件打包的时候,会将所有的内容放到一个目录内,这样你解包的时候所有的内容也会都在一个新的目录内。但不排除有些软件在打包的时候没有这么做,所以盲目地解压网上下载回来的软件可能搞乱你的当前目录。有两种方法可以避免:

一个在解压之前先看看里面都有些啥:

另一个方法是创建一个新的目录,在里面解压:

解压的时候最好不要盲目地使用这些参数,这些参数可以改变解压的目标目录,很可能覆盖你的系统上的文件:

Windows 和 DOS 上经常用的一种压缩格式是 zip. 可以理解为 zip 是 tar + gzip. zip 不会保留文件系统的权限信息。对应的解压工具是 unzip.

即使使用 tar 解压,如果要保留文件的 owner 信息的话,需要使用 root 用户解压才行。但是有这种需求的情况非常少。非常不推荐使用 root 解包文件!这样可能覆盖重要的系统文件。

需要注意的是,http 服务和 ftp 服务都可能被劫持(即使使用 https 也有被劫持的可能),mirror 上存放的软件可能被篡改。所以下载回来的软件在安装之前一定要验证!

使用 md5sum 校验文件

大部分的文件下载都会提供一个 md5 校验码,我们将文件下载回来之后,只要将文件计算一个 md5sum,然后和官方提供的 md5 对比就可以了。注意这个 md5sum 必须从官方渠道获得,不能从 mirror 获得,否则就没有意义了。

以 Python 为例,在这个下载地址( https://www.python.org/downloads/release/python-395/ ) 有提供 md5 码:

下载这个 XZ compressed source tarball 然后校验:

md5 一样,就可以认为文件是和官方完全一致的。

有些软件要下载多个文件,会提供一个 checksum-file, 可以使用这个命令,会自动根据 checksum-file 里面的文件逐个检查 md5:

使用 GPG key 校验文件

有些软件是使用 GPG key 签名来证明软件来源。作者用自己的 key 对文件签名,然后将文件与签名文件一同发布,用户需要下载四个东西:

  1. 作者的 public key
  2. 作者的 gpg key 指纹,证明 public 的来源真实
  3. 软件包
  4. 软件包的签名

校验的步骤是:

  1. 导入作者的 public key
  2. 检查 public key,确认指纹,然后信任此 key
  3. 通过软件包的签名来验证软件包

这里依然以上面的 Python 源码包为例,来通过 gpg key 进行校验。

第一步,找到作者的 public key。在 https://www.python.org/downloads/ 可以看到,Python 3.8 和 3.9 的 Release Manager 是 Łukasz,我们根据这个 key 的地址将这个 key 下载下来。

Python 官方开发者的 gpg key



然后确认这个 key 的指纹,与作者公布的指纹做对比。发现一致,即可信任。

Łukasz 公布在 keybase 上的 GPG public key



或者可以直接通过指纹 sign 这个 key,比上面的步骤要简单一些,是等效的。

最后,就可以下载代码以及 .asc 后缀的签名文件了。

阅读文档!

源码文件在解压之后,一般在最上层的目录下会有 README 或者 INSTALL 类似的文件,这是安装的文档说明。解压之后需要做的第一件事就是阅读这些文件。

文档中会有两个地方比较重要:

  1. 编译和安装这个软件需要的依赖,如果没有依赖就安装这个软件,要么编译有问题,要么会运行有问题;
  2. 软件编译的时候接收的一些参数;

Patching

某些软件可能在一些环境下无法正常工作,如果软件流行的话,在网上搜一下解决办法,一般可以找到一些人发布的解决办法。通过修改一些参数或者代码就可以了。叫做 Patch。

Patch 有两种:

  1. 修改脚本:比如 sed/awk 脚本,运行这些脚本可以修改源代码。例子
  2. Patch 文件:diff 的形式,人类可读。需要使用 Patch 程序去“应用”这个 diff,即“让这个 diff 生效” 例子

常见的编译系统

简单点说,编译就是将下载回来的代码变成可以在操作系统执行的 binary 文件。所以分成两步:

  1. 编译(make)
  2. 将编译好的文件放到系统的 $PATH 下面,这样可以在任意路径执行软件 (make install)

作者会将编译所用到的一些文件都放在 tarball 里面。同时,作者会希望有更多的人使用他的软件(谁不想呢?),所以会尽量让编译过程变简单一些。大部分的软件会使用 make 软件作为编译的工具。

这里不展开讲 make 了,简单点说,你提供给 make 一个 makefile,里面按照 make 的格式告诉 make:“如果目标文件不是基于最新的源码,就执行这个动作”,其中,目标文件可以是最终的软件,也可以是中间的产物。make 会自动帮你解决好先后顺序问题。

然而,make 的描述能力有限。但是想要处理起来所有的配置项,也捉襟见肘。所以这里又出现了另外一个工具:configure. 通常,软件包不会直接包含一个 makefile,而是会包含一个 configure 脚本以及一个 makefile.in 模板文件。执行 ./configure 就可以生成一个 makefile 文件,然后再执行 make.

所以,一般的编译流程如下:

另外,configure 也不是手写的,是由另一叫做 autotools 的 GNU 软件生成的。但是这个不需要用户使用,而是由软件作者来生成好。

最后一步 make install 的作用是将编译好的 binary 复制到系统的 $PATH 下。除了这一步以外,其他的步骤最好都使用普通用户来执行。

总体的流程如下:

有些软件没有使用 configure 脚本。因为它们足够简单,将 configure 的逻辑直接写到 makefile 里面了。比如 Redis,就没有。安装的时候只要 make 就可以了。

configuremake 生成的产物可能会和源代码文件混合在一起,弄得比较混乱,可以使用 make clean 清理编译产物。

但是对于每次编译,都在一个临时文件夹里面进行,或许是一个更好的办法。

其他的一些编译方式

cmake 现在也比较流行了,cmakeconfigure 类似,会基于传入的参数生成一个 makefile。除了使用下面这个命令替换 ./configure 之外,其他的步骤基本一样。

再次提醒一下,编译软件之前先看文档是一个好习惯。

有一些软件使用基于 Python/Perl 的编译工具,这些具体就参考文档好了。

还有一些工具直接是脚本语言编写的,这样的软件不需要编译,只要将软件拷贝到解释器读取的目录即可。不过一般这些语言都会自带一些包管理工具。

环境变量

configure 接受的配置项可以通过两种方式指定。

一种是环境变量。

或者直接 export 到当前的 shell 中然后执行。

另一种是通过执行 configure 时候的命令行参数。具体支持的参数,可以通过 ./configure --help 来查看。

编译安装文档

软件附带的文档没有统一的形式,常见的一些有:

  • 提供在线的 HTML 形式的文档;
  • 提供标准的 archive file, 可以直接下载;
  • 单独提供文档下载,可以通过 man/info 等查看;
  • make installdoc 安装
  • ……

总之还是参考文档来安装文档吧!

错误排查

编译是会很容易出错的一个过程。发生错误的时候一般根据编译错误一步一步排查就好了。要点是不要乱猜,要根据事实去推测。

举个最近遇到的问题。在一台机器上编译 Python3 失败了。./configure 的错误提示如下:

根据提示去查看 config.log 日志文件,在文件中发现这么一段:

可以知道是 gcc 尝试测试编译一段简单的程序失败了。失败原因是 ld 不支持 -p 参数。

然后查看机器上的 ld 为什么不支持。

通过这几个命令可以看到这个机器上有很多个 ld ,而使用的 ldelfutils 里面的 ld,并不是 GNU 的 ld。编译的时候将 /usr/bin 这个路径提前就好了。

大部分错误的情况是缺失了一些依赖,喜欢走弯路的话可以去网上搜索缺失的文件的名字,运气好找到缺失的依赖安装了就好了,不喜欢走弯路的话可以参考安装文档。也有时候缺失的依赖是可选的,可以通过 ./configure 的参数忽略掉。

编译器一般有一些高级的参数可以优化性能,但是如果不是很了解自己在做啥的话,最好还是不要碰。(既然你还在读这篇文章,还是不要碰了吧。)

编译出来的文件一般会带有一些 debug 参考的信息,一般用户用不着,可以通过 strip 命令来剪掉。

这是在 Mac 系统上编译出来的 Python3.9,strip 之后少了0.8M.

不过今天的硬盘已经这么不值钱了,也没什么必要其实。

最后有些软件可能在安装之后需要一些特定的配置才能运行。还是那句话,读文档。

练习

看到这里,编译下来 LFS 基本就没什么问题了,可以按照 LFS 从头开始编译一些 Linux 的工具试试,这里列出了一个能工作的 Linux 需要的基本软件。

 

为什么 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.


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.