通过 Varnish 使 WordPress 静态化

WordPress 常常被人诟病说加载速度慢,主要原因是每一次打开页面都会查询数据库,即使每次查询的结果都是一样的。WordPress 原生不支持缓存,但可以通过插件支持,比如 Super Cache。我用过一段时间 Super Cache,它本身是一个很好的插件,但协作性不强。它只能加载 WordPress 原生的内容,而我需要的是缓存经过 PageSpeed 优化过的内容,从而达到完全静态化的效果。

曾经有一段时间 KeyCDN 的表现不理想,而 Google CDN 还没有开放,我就自己搭了几台缓存服务器,来缓解源站的压力。这几台缓存服务器是通过 Varnish 实现的。当然用 Nginx 也可以,但是 Nginx 的缓存失效(Purge)功能是收费的,而且部分平台上还是自己编译。于是我选了 Varnish。

Varnish 可以做到的事情

  • 根据 URL(或其它参数)的不同来选择不同的缓存策略;
  • 修改 HTTP 请求和响应的头部内容;
  • 设定缓存时间,时间到了自动失效;
  • 可以通过一个特殊的 HTTP 请求来让一个(或一些)缓存立即失效;

好,接下来就开始配置服务器了。

安装 Varnish

以 Debian 为例,最新的 Varnish 版本是 4.0:

配置 Varnish

先创建一个新的配置文件,比如 /etc/varnish/leonax.vcl,内容如下。其中比较重要的就是三部分:vcl_recv、vcl_hash、vcl_backend_response,可以根据自己的实际情况来调整。必要的注释已经附在相应的代码旁边了。

然后来检查一下配置文件写得对不对。如果配置正确,这段命令会输出一大段处理过的代码;如果有问题,则会输出具体的错误信息。

接着,在 /etc/varnish/default.vcl 文件中,可以找到如下一段内容:

需要把 -f 的值修改为刚才创建的配置文件地址。

重启 Varnish

配置 WordPress

WordPress 的页面需要刷新的情况,无非有三种:新文章、新评论和配置修改。配置的更改一般频繁,我的做法是重新配置之后,手动重启一遍 Varnish,这样最干净。而新评论的情况,刚才在 Varnish 的配置文件中已经处理了。于是我们现在来处理一下新文章的情况。

以下代码需要写在 WordPress 中,比如 functions.php:

收工

做完以上这些,你的站点就可以达到像本站一些的加载速度啦。

WordPress 4.4

WordPress 4.4 在这个月初发布了。由于 WordPress 的新版本质量有下滑的趋势,本来我想等 4.4.1 发布了再升级。后来去翻阅了一下 4.4 的新功能列表,发现没什么特别多的新功能。于是就升掉了,目前看来没有什么大问题。

WordPress 4.4 的一个主要改动是图片会根据访客的屏幕大小而自动适配,不至于图片过大而引起整体布局混乱。不过本站的图片都是我手动修改过大小的,本来就不会产生这样的问题,升级之后也看不出有什么变化。就当它不存在吧。

新功能没什么用就算了,WordPress 又强推了一些本身就没什么用的功能,而且默认开启。

其一是 JSON REST API,这是一个编程操作的接口,是面向第三方工具的。对于普通用户来说,实际上是一个看不见摸不着的功能。由于它非常得新,现在还没什么工具能用得上这个接口。而且新功能多少会带来一些安全隐患,在它没有被反复论证之前,我暂时没什么兴趣让这个接口一直开着。关闭的方法如下,在主题或 functions.php 中添加:

其二是 WordPress 这段时间在大力完善的功能:oEmbed。这个功能的作用是,比如博文中嵌入了一条 YouTube 的链接,本来它只会显示成一条文本链接,但有了 oEmbed 之后,WordPress 会把这个链接自动变成 YouTube 视频,在博文中播放出来。说这个功能没用,是因为一来我一般不在文章中贴视频或者其它外来的东西,二来 oEmbed 支持的都是国外网站,即使转成了视频,国内的访客多数也看不到。

oEmbed 这个功能一直以来都很老实,只是在后台默默地转换链接而已,不过在 4.4 中,它终于跳到了前台。WordPress 4.4 中默认添加了一个 js 引用:wp-embed.min.js。于是我就不爽了,一定要把这个功能给关了。关闭方法如下:

好了,这下终于舒服舒服地用上 WordPress 4.4 了。

写于博客密码泄露之后

很不幸我的博客密码被盗了,一个不名黑客在半夜入侵了我的主机,植入了菜刀,远程控制了机器。然后他窃取了一些重要资料(比如一些羞羞照),并且把主机变成了一台肉机。我发现的时候,肉机已经偷跑了超过 5000 美元的流量......

在电脑手机前偷笑的各位可以稍微停一下了。以上只是我被害妄想症的一种表现,它实际上并没有发生。

由于对安全的偏执,我假想了各种安全漏洞的情形,然后加以防御。之前讨论过 WordPress 的密码安全性插件安全性,这次来探讨一下密码失窃之后,有哪些亡羊补牢的方法。

假设一名黑客偷取了本博客的管理员密码,(先假设只是博客的密码,而不是主机的管理员权限),那么他能做哪些事情?

一,很明显,他可以操作博客的数据库,新建用户、新建文章、删除文章等。

二,黑客可以在后台直接挂马,然后进行更强大的远程操作。

对于第一条,似乎没有特别好的方法,一旦密码被盗,数据库就只能放弃了。当然,如果你的博客没有任何加密的内容,数据库被盗也没什么好怕的,本来就都是公开的东西。

接下来重点说一下第二条。

PHP 有一个很好用也很危险的特性,就是源代码可以直接执行;而 WordPress 也有一个很好的特性,即可以在后台直接修改插件代码。于是一旦黑客控制了后台,他可以在源代码里嵌入一行如下的语句,然后就可以远程控制了。对,PHP 就是那么强大。

要防止这种情况,最有效的方法是在把所有源代码设置为只读,即 Linux 文件属性 400(或 444),这样可以避免任何人对文件进行修改。但这样做之后,自己真想要修改文件的时候,却是非常麻烦,先要 SSH 登录,把文件属性设为可写(如 755),然后再回到后台改文件。另外,这样做也阻止了 WordPress 的自动更新,一定程度上降低了安全性。

对此, WordPress 提供了一个简单的防御方式,可以 wp-config.php 中添加一行:

这样可以禁用掉 WordPress 中修改源代码的界面,包括插件和主题,同时也禁止了安装新的插件和主题。当然这样同样会有上述的问题,即每次修改文件之前,需要先修改 wp-config.php。为了避免对 wp-config.php 的频繁修改,我们可以加一点小小的逻辑,如下:

它的含义是在不是运行 WP Cron 并且 Session 中没有定义 enable_edit 的时候,才禁用对主题和插件的修改。开启 WP Cron 的原因是它会定期检查并更新 WordPress,而 Session 的定义则可以按需开启修改的界面。

手动启用修改界面,还需要一段代码,写在 functions.php 中:

然后访问 http://你的博客地址/wp-admin/admin-ajax.php?action=temp_enable_edit 即可临时开启对插件和主题的修改,有效期到 Session 过期为止。需要注意的是,并不所有的主机都启用了 Session。如果你的主机把 Session 关了,你可以改成使用 Cookie,效果类似。

这种方法是安全和易用的一个折中,它假设了 WordPress 不会有 Bug,比如 DISALLOW_FILE_MODS 工作正常,并且没有安装可以直接运行 PHP 代码的插件

另外还有一些其它的方法仅供参考:

  • 给 wp-admin/ 文件夹加一个密码,比如 Apache 的 Basic Authentication。我觉得每次登录都需要多输一个密码,实在太麻烦了。
  • 重命名 wp-admin/ 文件夹让别人找不到。根据前几年的讨论贴,这种方法并不能完美工作,也就说是 WordPress 的代码某种程度上依赖于 wp-admin 而不能改名。
  • 创建一个新的域名,只用来访问 wp-admin,而原先的域名供访客浏览。这种方法的前提是你愿意支付双倍的 SSL 证书费用。

这个世界上没有绝对的安全,只不持之以恒的防御。

从无到有实现 WordPress 全站 CDN

WordPress 的设计有很多莫名其妙的地方,其中之一就是它的域名设置。WordPress 的每一个站点要设置一个独立的主页(Home)地址,而且只能设置一个。也就是说,即使有两个域名指向同一个博客(比如 a.com 和 b.com),如果博客把主页设置成了 a.com,用户在访问 b.com 的时候,也会被重定向到 a.com。这样造成了 b.com 实际上完全无法访问到。

对于使用 WordPress 的博客主而言,通常一个域名就够了,可能最初 WordPress 的开发人员也是这么想的。但现在的互联网,单一域名已经不足以支撑一个网站了。简单来说,WordPress 配置 CDN 相当麻烦。

CDN,又名内容分发网络,一个把网站内容快速传递给用户的工具。它的工作原理是,预先把内容加载(缓存)到离用户较近的数据中心(通常称为 Edge Server),当用户访问网站时,直接从数据中心读取内容,而不是从源网站(Orginal Server),这样就减少了数据传输的时间。如下图:

一个 CDN 包含多个 Edge Server,如果每个用户都访问离自己较近的 Edge Server,则会比直接访问源网站要快很多。而现在的技术又可以做到当用户访问某一个域名的时候,被自动解析到较近的 Edge Server,用户则完全感受不到差别。

现在做得比较好的 CDN 服务如 CloudFlare。它家提供了一条龙服务,从域名解析到内容分发,一个帐号全搞定。这样做的好处是“傻瓜式”,不需要太多的专业知识就可以配置 CDN;而坏处则是一损俱损,CloudFlare 的服务器经常被墙,一旦被墙了,想脱离 CloudFlare 就重新设置一堆东西,很麻烦。

如果你像我一样不喜欢一键搞定的服务,那我们就来一步一步自己配置全站 CDN 吧。

初始化 CDN 服务

首先,你得有一个 CDN 服务,我用的是 KeyCDN,介绍看这里

初始化(KeyCDN 的术语叫“Zone”)完成之后,一般会得到一个二级域名,比如我的是 leonax-1800.kxcdn.com。然后在自己的域名解析中创建一个 CNAME,指向之前的域名,比如我用的是 www.leonax.net。解析刷新之后,初始化就完成了。

修改链接和资源地址

如上文所说,WordPress 限定了网站的地址,比如本博客设置为“leonax.net”,则所有的资源(JS、CSS 等)和链接都会以“//leonax.net/”开头。即使用户访问了主页“www.leonax.net”,其中的各种资源也会从“leonax.net”加载,这样就达不到 CDN 的效果。于是我们要把页面中所有的“leonax.net”全部替换成“www.leonax.net”。下面有一些方法,可选择用其中的一种或多种组合来达到效果:

修改资源地址

WordPress 提供了 style_loader_src 和 script_loader_src 分别来过滤 JS 和 CSS 的地址,我们要做的是把其中的域名删掉,从而使用相对路径。代码如下:

替换正文中的链接

使用 WordPress 的 the_content 过滤器,可以修改正文内容,具体要改什么,视具体情况而定。注意 WordPress 的短代码(shortcode)解析的优先级为 11,所以下面的代码使用了优先级“8”来避免短代码带来的改动。

Apache 输出全文替换

Apache 提供了 mod_substitute 来对输出内容进行修改。如果你的站点中提供了这项功能,则可以轻松改动网页内容:

mod_pagespeed 域名替换

mod_pagespeed 也提供了类似的域名替换功能,比 mod_substitute 智能的是,mod_pagespeed 可以识别出链接字段,比如 <img src=""> 中的内容,而不像 mod_substitute 那样全局替换:

CDN Pull

有些时候,我们可能需要在 CDN 模式下输出 www.leonax.net,而在访问源站的时候则依然得到 leonax.net。这时就需要对 CDN 发来的请求进行定向输出。一般 CDN 方面会给出一些特殊的信息,比如 KeyCDN 会设置一个特殊的 X-Pull 头,标记这个请求是从 KeyCDN 发来的。于是我们可以对这一类请求做特殊处理(Apache):

CORS

Cross-origin resource sharing,跨来源资源共享,是一个浏览器的特性。简单来说,leonax.net 和 www.leonax.net 是两个域,如果 leonax.net 引用了来自 www.leonax.net 的资源,而 www.leonax.net 没有允许这么做的话,浏览器会主动拒绝加载相应资源的。所以,如果两个域都用了 CDN 的资源,则在 www.leonax.net 中,需要允许 CORS。这一项在 KeyCDN 中默认是开启的,我也就没有多关心。

CDN Purge

CDN 的主要原理是缓存,也就是把网页内容缓存在 Edge Server 中,每次响应用户请求的时候,不必再向源站请求数据。这样就产生了缓存过期的问题。比如博客中有人留言,博客页面更新了之后,CDN 却不知道这件事,依然输出不包含留言的页面,这样就不对了。为了解决这个问题,CDN 通常都会提供一种清除缓存的方式,称为 CDN Purge。 KeyCDN 的做法是发出一个如下的 HTTPS 请求,用于清除一个特定页面的缓存:

CDN 方面搞定了,WordPress 依然成问题,因为我们也不知道页面什么时候更新。研究了一下发现,我可以利用 WP Super Cache,在 Super Cache 的缓存页面被删除的同时,也发送一个请求给 KeyCDN,同时删除那边的缓存,一举两得:

上述的代码会在博文更新、或是有新留言的时候触发,会稍微减慢响应速度,更好的办法应该利用 WordPress 的 wp_schedule_single_event 方法,不过暂时还没有研究。

总结

把上述所有内容串起来,就有了现在的全站 CDN

把 WP Super Cache 的静态文件移到内存中

WP Super Cache 是一个很好的插件,它可以把 WordPress 静态化。静态化是指,博客变成了一堆 HTML 文件,只要访客不留言,在整个浏览过程中,服务器中是不会有 PHP 程序在运行的,只有 Apache(或者其它 Web 服务器)在响应网页请求。这样就极大地减少了服务器的负载。

但是 WP Super Cache 有一个小小的问题,就是它的缓存(静态文件)是和 WordPress 的程序文件放在一起的,路径是 wp-content/cache/supercache/。这样有两个问题,一是这些临时文件污染了 WordPress 的目录,比如你想备份 WordPress 的程序文件的话,一不小心就把这些没什么用的缓存文件也一起备份了;二是这个缓存目录是放在硬盘上,Apache 依然需要从硬盘上读取它们,对于硬盘速度较慢的 VPS 来说(比如传说中的阿里云),这种静态化机制可能提升不了太多的性能。

于是,我们就来把这些缓存扔进内存中吧。

Linux 提供了一种磁盘类型叫作 tmpfs,它的作用是把一个文件夹伪装成普通的文件夹,但实际内容只放在内存中,以提高文件的访问速度。比较著名 tmpfs 文件夹有 /dev、/run 等。tmpfs 的优点自然是速度快,当然缺点也显而易见,就是机器重启了,tmpfs 就被清空了。不过呢,用它来存放一些临时文件就非常合适了。

我们选择 /run/blog/cache 来替代 wp-content/cache/。以 Ubuntu 为例,下面的命令可以用来创建文件夹,并设置相关权限:

把上述内容存放在 /var/www/create_tmp.sh,因为这段命令不是运行一次就好了,它要在每次重启之后,都运行一遍。要做到这一点,我们需要 Cron Job,做法是运行:

然后在最后添加新的一行:

OK,目录创建完毕,接下去要告诉 WP Super Cache 使用那个目录,做法是在 wp-content/wp-cache-config.php 文件中找到这一行:

把它改成:

然后 WP Super Cache 就会在新的目录下生成静态文件了。

然后还需要改一个地方,如果你使用了“mod_rewrite 缓存模式”,你还需要改动一下 WP Super Cache 的 RewriteRule,大致的改动是这样的,可能需要根据实际情况做不同的修改:

之后,重启一下 Apache,就全部搞定了。

试用 WordPress 4.2 中的“绘文字”功能 ?

Emoji,又称“绘文字”,是起源于日本的一种聊天时用的表情符号。由于其广泛流行,一些字符已经进入了 Unicode 编码,成为了一种跨平台的语言。

举例说明,在 iPhone 的默认输入法中,就可以找到表情符号,如下面这一大堆,如果使用这些表情符号在本站留言,这些表情可以正常地显示出来,而不是乱码。

这是一项在 WordPress 4.2 中新增的功能,它统一了不同操作系统和浏览器中的显示效果。折衷的方法是把 Emoji 字符全部替换成了图片。由于 Emoji 字符在不断发展中,不停地有新字符被创造出来,于是 WordPress 并没有把这些图片都放进发行版中,而是使用了一个 CDN 做实时分发,CDN 前缀是 s.w.org。这个域名在国内似乎不能正常访问,所以想要使用 Emoji 的话,需要做了一些调整:

当然,你需要一个可用的 Emoji 文件的镜像,可以自己架设一个反向代理,或者使用第三方的代理,比如这篇文章中就提供了一个。

顺便提一下,在 Mac OS X 中,可以使用 Command + Ctrl + 空格,来打开 Emoji 的输入面板。㊙️

WordPress 中实现回复可见的一种方法

“回复可见”是论坛中的一种常见的样式,它要求用户回复了这个贴子,才可以看到其内容。这样有助增加互助,提高论坛的人气。

在有了博客之后,不少博主也都想着在博客中使用同样的方式,增加博客的回复量。不过现有的方式都不是那么理想。

WordPress 和传统的论坛有一些不同:

  1. WordPress 一般不需要注册即可回复,也就是任何人,只需填写昵称和邮箱,就相当于已经注册了,想看到隐藏的内容并不难;
  2. WordPress 所使用的主机较为低端,多数博客需要添加缓存插件才可以正常工作,而回复可见的模式会破坏缓存。

目前现存的一些回复可见的实现,都是通过后台查询当前用户的邮箱,如果数据库中有回复记录,则向其显示隐藏的内容。这是一种标准的论坛方式的实现,好是好,但对主机要求比较高,不能用缓存,每次用户访问的时候,都要检查一下数据库,多数博客用不起这样的插件。

于是我就另辟蹊径,用了另一种方式。效果可以看之前的一篇文章,用户体验和论坛中的基本一样,并且页面可以被缓存。

大致的实现方式是这样的:

  • 被隐藏的内容和页面的其它部分一起,同时输出到客户端上;
  • 被隐藏的内容事先被做了处理,用户默认是无法看到的;
  • 用户回复了之后,浏览器的 Cookie 中会被写入自己的邮箱地址;
  • 页面中有一段客户端脚本(Javascript)来检测 Cookie 中的邮箱地址,是不是已在回复列表中存在;
  • 如果已存在,则自动解码那段被隐藏的内容并显示给用户。

这样一来,除了记录回复的过程和往常一样需要经过数据库之外,隐藏的内容不需要多次经过后台处理,效率非常高。当然坏处也是有的,即用户不须回复,通过其它手段也可以获取被隐藏的内容。但由于 WordPress 本身的匿名回复特性,实际上回复一下,比破解原始内容要容易得多,我想不会有人会选择后者吧。

这个功能已经集成在我的插件 Enigma 中,有兴趣的朋友可以下载下来试试看。用法是这样的:

[enigma text="以下内容回复可见"]
用户回复之后看到的内容
[/enigma]

用户没有回复之前,看到的是“以下内容回复可见”,只有回复了,才可以看到真实的内容。

目前这个功能还处于试验期,有一些已知的问题:

  • 和 Ajax 回复插件不兼容,用户需要刷新页面才可以看到隐藏的内容;
  • 和回复分页功能不兼容,不在当前分页的回复,将被认为是没有回复过。