基于python3的多线程WordPress缓存预加载代码

WARNING: This article may be obsolete
This post was published in 2021-08-30. Obviously, expired content is less useful to users if it has already pasted its expiration date.

预加载

通过预先生成所有链接对应的缓存文件,可以让Wordpress页面的第一次访问就很快。

或者可以看这篇文章的介绍:🔗 [Preload cache - WP Rocket Knowledge Base] https://docs.wp-rocket.me/article/8-preload-cache

WordPress插件提供的preload功能基本上都是用php实现的,本文的preload代码是用python3写的,所以你需要准备一种你喜欢的缓存方案(不一定要wordpress插件,nginx-FastCGI也可以).

原版代码

是这篇文章的python3实现:

备注:

✪ 这是一个短时间快速生成WordPress全站html缓存的程序,建议开启Mysql或者MariaDB的 query_cache ,能够显著缩短运行时间。

✪ 运行本代码前需要清空缓存文件。

✪ 比较适用于:个人站点(只有你一个人发表文章,所以你知道什么时候可以运行这个程序);不太适用于:有多名文章发表管理员的大型站点。

✪ 不同缓存方案(redis, memcached, mysql query cache)的性能对比:🔗 [Memcached和Redis测试(preload.py) - Truxton's blog] https://truxton2blog.com/preload-py-memcached-redis-benchmark/

✪ 相比Wordpress插件(比如wp-rocket, wp super cache, wp fastest cache...)自带的php preload程序:

优点是:多线程/多进程,可自定义preload内容(防止遗漏),

缺点是:脱离了Wordpress体系,对web hosting平台不友好,暂时无法针对单篇文章进行缓存更新。

✪ 理论上适用于绝大多数基于HTML的缓存插件以及FastCGI,但需要关闭缓存插件自带的preload功能,防止功能重叠。

✪ 需要 $ pip3 install lxml requests  .

✪ 需要替换代码里的example.com .

✪ 不同的sitemap插件生成的xml文件名称可能不同,本站使用的是Yoast SEO .

✪ 默认代码在本地运行,能够访问127.0.0.1:443 .

✪ 第70行 ThreadPoolExecutor(max_workers=2) 需要根据cpu核心数进行调整。我个人一般倾向于 调整到cpu利用率正好超过90%  .

✪ 我的服务器只有性能极弱的1核cpu,所以我使用了2个线程;如果你有多核cpu,也许你可以尝试一下 ProcessPoolExecutor  .

✪ 正则表达式在其他wordpress主题上可能需要做调整,并不一定通用,具体表现如下:

1. 如果你不在widget里添加“monthly archives“,那么你不需要第56~61行判断月份的代码。

2. 如果你发现这个程序无法在你的网站上获取分页,那么你需要修改代码第61行的regex:
 pattern_pagination = re.compile(r'(?<=<a class="next page-numbers" href=")[^"]+(?=">)') 
,以及第22行的substring判断代码:
 if 'link rel="next" href=' in response_body: 
,让它和你的网站的分页链接相匹配。

from concurrent.futures import ThreadPoolExecutor
import time
import requests
from lxml import etree
import re
from functools import partial

from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)


def download(pattern_pagination, pattern_embed, url):
    print(url)
    url = url.replace('example.com', '127.0.0.1:443')
    headers = {'Host': 'example.com'}
    response = requests.get(url, headers=headers, verify=False)
    response_body = response.text

    # pattern_pagination = re.compile(r'(?<=<link rel="next" href=")[^"]+(?=" \/>)')  # 查找分页
    # python in 比 regex 更快
    if 'link rel="next" href=' in response_body:
        results_pagination = pattern_pagination.findall(response_body)
        if len(results_pagination) != 0:
            download(pattern_pagination=pattern_pagination, pattern_embed=pattern_embed, url=results_pagination[0])

    # pattern_embed = re.compile(r'(?<=data-src=")[^"]+example[^"]+\/embed\/')  # 查找嵌入页面
    # python in 比 regex 更快
    if '/embed/' in response_body:
        results_embed = pattern_embed.findall(response_body)
        for val in results_embed:
            download(pattern_pagination=pattern_pagination, pattern_embed=pattern_embed, url=val)

    return 1


def get_sitemap(url):
    xmlList = []
    r = requests.get(url)
    root = etree.fromstring(r.content)
    for sitemap in root:
        children = sitemap.getchildren()
        xmlList.append(children[0].text)
    return xmlList


if __name__ == '__main__':
    start = time.time()

    urlList = []
    urlList.extend(get_sitemap("https://example.com/post-sitemap.xml"))
    urlList.extend(get_sitemap("https://example.com/page-sitemap.xml"))
    urlList.extend(get_sitemap("https://example.com/category-sitemap.xml"))
    urlList.extend(get_sitemap("https://example.com/post_tag-sitemap.xml"))

    # add month archive
    response_body = requests.get("https://127.0.0.1:443", headers={'Host': 'example.com'}, verify=False).text
    pattern_month_archive = re.compile(
        r'(?<=href=[\'\"])https:\/\/example.com\/\d+\/\d+\/(?=[\'\"])')  # 查找月份archives
    results_month_archive = pattern_month_archive.findall(response_body)
    for val in results_month_archive:
        urlList.append(val)

    # 去重
    urlList = list(dict.fromkeys(urlList))

    # 先编译好
    pattern_pagination = re.compile(r'(?<=<a class="next page-numbers" href=")[^"]+(?=">)')  # 查找分页
    pattern_embed = re.compile(r'(?<=data-src=")[^"]+example[^"]+\/embed\/')  # 查找embed页面

    pool = ThreadPoolExecutor(max_workers=2)
    dl_func = partial(download, pattern_pagination, pattern_embed)
    results = list(pool.map(dl_func, urlList))
    end = time.time()
    print(time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(start)))
    print(time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(end)))
    print('time in seconds:{:.3f}'.format(end - start))

进一步优化

上面的代码还有很多优化余地;但是优化的越多,代码就越不具有通用性,所以接下来仅仅叙述一些优化思路:

2023年4月12日紧急更新:我对pycurl和requests的性能对比还是没有定数,目前的结论是“pycurl的总体性能和requests几乎完全一样”,测试结果:🔗 [preload.py使用pycurl和requests的性能对比 - Truxton's blog] https://truxton2blog.com/compare-pycurl-requests-in-preload-py/

可能存在争议的内容:

✪ 用 PycURL 代替 requests 能够大幅度提升本程序的性能。在我的服务器上,运行 requests 的代码,(preload.py)cpu平均占用约15%;换成 PycURL 以后,cpu平均占用只有1.3%左右了. 但是一定要注意PycURL使用的ssl库:如果编译PycURL时链接的是 gnutls 而不是 openssl/nss ,则会大幅度损失性能,导致PycURL的cpu占用率达到15%左右(和requests没什么区别了)。

为了避免反复尝试不同编译参数带来的麻烦,推荐使用conda安装PycURL:

$ conda install -c anaconda pycurl

也可以自行编译libcurl --with-openssl:🔗 [PycURL的性能取决于安装方法和编译依赖 - Truxton's blog] https://truxton2blog.com/pycurl-performance-differs/

✪ 依次删除缓存文件:通常情况下的逻辑是“先删除所有缓存文件,然后再运行preload.py”,但这样可能会带来一个潜在的问题:执行preload.py程序的时候cpu占用非常高,如果恰好访问了一个缓存文件丢失的链接,那么这个网页请求就会由php处理,速度可能会慢好几倍。

如果你使用的是类似FastCGI这样的缓存系统,由于FastCGI的路径是可以计算的(绝大多数Wordpress缓存插件的缓存路径也可以计算,有些基于MD5有些基于SHA256...可以查看源代码),所以你可以修改代码,让程序的执行逻辑变为“删除一个页面的缓存文件,通过preload生成新的缓存文件,然后删除下一个页面的缓存文件...”。这样做的好处是:preload程序执行期间访问网站的速度不会大幅度降低。

FastCGI缓存的计算方式可以参考:【半成品】NGINX FastCGI purge and preload for single URL

✪ 如果你开启了 query_cache ,建议在preload.py程序开头部分执行 RESET QUERY CACHE ,清理query cache缓存碎片,进一步加快preload.py的执行速度(前提是你需要给query cache分配足够的内存空间)。以本站点为例,生成300个html页面需要大约30MB的mariadb query cache空间。(2022-11-31)


 Last Modified in 2023-04-12 


Leave a Comment Anonymous comment is allowed / 允许匿名评论