WordPress 多站点 + GlotPress 翻译平台 TTFB 从 45 秒降到 60 毫秒的优化实战

·1 次浏览
  • 作者
    帖子
  • wenpai@wenpai
    楼主

    问题

    一个运行 67 个子站的 WordPress Multisite 翻译平台 (GlotPress 4.0.3 + WP 6.8.3),首页 TTFB 5.7 秒,繁体中文页面 (/zh-tw/、/zh-hk/)TTFB 高达 45 秒,服务器 load 长期 30+。

    服务器配置不差:80 核 62GB 内存,PHP 8.3,Redis 已有 (Object Cache Pro),但 PHP-FPM 22 个 worker 全部 80%+ CPU 。

    根因分析

    排查发现三个叠加因素:

    1. wp-chinese-converter 的 OpenCC 字典排序

    这是最大的性能杀手。插件使用 OpenCC PHP 库做繁简转换,核心瓶颈在 class-wpcc-opencc-converter.php 的 get_prepared_dictionaries() 方法:

    // 53,650 个词条,每次请求都要 uksort
    uksort($flat, function ($a, $b) {
        return mb_strlen($b) <=> mb_strlen($a);
    });
    

    虽然代码里有 $this->prepared[] 做了单次请求内的缓存,但 PHP-FPM 每个请求是独立进程,字典排序每次都要重新做。 53,650 个词条的 uksort + mb_strlen 比较,单次耗时数秒。

    排序之后的 strtr($output, $dict) 对 5 万多词条的替换本身也很慢。

    2. 无 robots.txt,爬虫疯狂爬翻译页面

    没有 robots.txt,MJ12bot 翻到了 /zh-hk/tag/right-sidebar/page/1105(是的,第 1105 页) 。 GPTBot 、 OAI-SearchBot 、 Bytespider 、 SemrushBot 、 ClaudeBot 也在同时爬。

    每个爬虫请求都是一个完整的 PHP 处理 + OpenCC 字典排序 + 大字典 strtr 替换。最近 10000 个请求中约 10% 是这类爬虫,但它们请求的全是 CPU 密集的翻译页面。

    3. Cavalcade cron 任务失控

    Cavalcade(WordPress cron 替代方案) 有 63 个僵尸 action_scheduler_run_queue 任务从 2025 年 11 月卡到现在,5 个 worker 各吃 95% CPU 。加上 WP-Rocket 激活后 67 个子站同时产生 RUCSS 和预加载任务,雪上加霜。

    4. wp-chinese-converter 的 Set-Cookie 阻止缓存

    插件在每个请求上设置 Set-Cookie: wpcc_variant_xxx,导致 nginx fastcgi_cache 默认不缓存这些响应。即使配了缓存也不生效。

    优化方案

    第一步:拦截爬虫 (立即见效)

    创建 robots.txt 拦截无价值爬虫:

    User-agent: MJ12bot
    Disallow: /
    
    User-agent: Bytespider
    Disallow: /
    
    # GPTBot, OAI-SearchBot, SemrushBot, DotBot, AhrefsBot, PetalBot, ClaudeBot 同上
    
    User-agent: *
    Disallow: /wp-admin/
    Disallow: /glotpress/
    Disallow: /translate/
    Disallow: /zh-tw/tag/
    Disallow: /zh-hk/tag/
    

    同时在 nginx 层直接 403 拦截 (robots.txt 只是君子协定):

    # /www/server/panel/vhost/nginx/extension/wpfanyi.com/bot_block.conf
    if ($http_user_agent ~* "(MJ12bot|Bytespider|GPTBot|OAI-SearchBot|SemrushBot|DotBot|AhrefsBot|PetalBot|ClaudeBot)") {
        return 403;
    }
    

    第二步:nginx fastcgi_cache(根本性解决)

    在 nginx http 块定义 cache zone:

    # 0.wpfanyi_fcgi_cache.conf (http 块)
    fastcgi_cache_path /path/to/nginx-cache levels=1:2
      keys_zone=wpfanyi_cache:100m max_size=2g inactive=60m use_temp_path=off;
    

    在 server 块启用缓存,关键是要忽略 wp-chinese-converter 的 Set-Cookie:

    set $skip_cache 0;
    
    if ($request_method = POST) { set $skip_cache 1; }
    if ($arg_s) { set $skip_cache 1; }
    if ($http_cookie ~* "wordpress_logged_in_") { set $skip_cache 1; }
    if ($request_uri ~* "/wp-admin/|/wp-login.php|/wp-json/") { set $skip_cache 1; }
    
    fastcgi_cache wpfanyi_cache;
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
    fastcgi_cache_valid 200 301 60m;
    fastcgi_cache_valid 404 1m;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;
    fastcgi_cache_use_stale error timeout updating http_500 http_503;
    fastcgi_cache_lock on;
    
    # 关键:忽略 wp-chinese-converter 的 Set-Cookie
    fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
    fastcgi_hide_header Set-Cookie;
    
    add_header X-FastCGI-Cache $upstream_cache_status;
    

    第三步:OpenCC 字典 OPcache 文件缓存

    修改 get_prepared_dictionaries() 方法,排序后写入 PHP 文件,后续请求通过 include + OPcache 加载:

    private function get_prepared_dictionaries(string $strategy): array {
        if (isset($this->prepared[$strategy])) {
            return $this->prepared[$strategy];
        }
    
        // 尝试从 PHP 文件缓存读取 (OPcache 加速)
        $cache_file = WP_CONTENT_DIR . "/cache/wpcc-dictionaries/dict_" . md5($strategy) . ".php";
        if (file_exists($cache_file)) {
            $prepared = include $cache_file;
            if (is_array($prepared) && !empty($prepared)) {
                $this->prepared[$strategy] = $prepared;
                return $prepared;
            }
        }
    
        // 首次:排序并写入文件缓存
        $sets = Dictionary::get($strategy);
        // ... uksort 逻辑不变 ...
    
        // 写入 PHP 文件,OPcache 自动缓存字节码
        $export = "<?php\nreturn " . var_export($prepared, true) . ";\n";
        $tmp = $cache_file . ".tmp." . getmypid();
        if (@file_put_contents($tmp, $export, LOCK_EX) !== false) {
            @rename($tmp, $cache_file);
            if (function_exists("opcache_compile_file")) {
                @opcache_compile_file($cache_file);
            }
        }
        return $prepared;
    }
    

    第四步:限制 Cavalcade CPU

    # /etc/systemd/system/cavalcade.service
    [Service]
    Nice=19
    CPUQuota=200%
    CPUWeight=50
    

    优化结果

    指标 优化前 优化后
    服务器 Load 34.87 3.14
    首页 TTFB 5.7s 60ms
    zh-tw 页面 TTFB 45s 66ms
    zh-hk 页面 TTFB 6s 65ms
    PHP-FPM workers 22 个 80%+ CPU 10 个 19% CPU

    经验总结

    1. wp-chinese-converter / OpenCC 是 CPU 杀手 — 5 万词条的 uksort + mb_strlen 比较 + strtr 大字典替换,每个繁体页面都触发。必须加持久化缓存。

    2. Set-Cookie 会静默破坏 HTTP 缓存 — 插件每次请求都设置 cookie,nginx fastcgi_cache 默认不缓存带 Set-Cookie 的响应。需要显式 fastcgi_ignore_headers Set-Cookie 。

    3. 没有 robots.txt 的翻译站点是爬虫磁铁 — 翻译站点 URL 结构 (/zh-tw/tag/xxx/page/N) 会产生海量可爬页面,爬虫会无限翻页。

    4. Cavalcade + action_scheduler 会失控 — 僵尸任务堆积 + WP-Rocket 67 个子站同时产生后台任务 = CPU 爆炸。必须限制 worker 数和 CPU 配额。

    5. nginx fastcgi_cache 是多站点 WordPress 最有效的前端优化 — 一层缓存解决 99% 访客的性能问题,配置简单,效果立竿见影。

    6. Object Cache Pro 的 metrics snapshots 可能膨胀 — wp_sitemeta 中 objectcache_snapshots 可达数百 KB,单次写入耗时数秒,需要定期清理。

    系统

    核心优化策略分析

    你的优化方案非常全面,特别是对 wp-chinese-converter 插件性能瓶颈的定位很准确。让我补充几个 WordPress 层面的优化建议:

    1. 替代 wp-chinese-converter 的方案

    既然这是翻译平台,可以考虑更轻量的繁简转换方案:

    // 在主题的 functions.php 中添加
    add_filter('locale', function($locale) {
        // 根据 URL 路径判断语言变体
        if (strpos($_SERVER['REQUEST_URI'], '/zh-tw/') !== false) {
            return 'zh_TW';
        }
        if (strpos($_SERVER['REQUEST_URI'], '/zh-hk/') !== false) {
            return 'zh_HK';
        }
        return $locale;
    });
    
    // 使用 WordPress 内置的翻译文件,避免实时转换
    load_textdomain('default', WP_LANG_DIR . '/zh_TW.mo');
    

    2. 优化 Cavalcade 和 Action Scheduler

    对于多站点环境,Action Scheduler 任务需要更精细的控制:

    // 在 wp-config.php 中添加
    define('ACTION_SCHEDULER_LOCK_TIMEOUT', 60); // 减少锁超时时间
    define('ACTION_SCHEDULER_QUEUE_RUNNER_CONCURRENT_BATCHES', 1); // 限制并发批次
    define('ACTION_SCHEDULER_GROUP_CONCURRENCY', 1); // 限制组并发
    
    // 清理僵尸任务
    add_action('init', function() {
        global $wpdb;
        // 清理超过 24 小时的失败任务
        $wpdb->query("
            DELETE FROM {$wpdb->prefix}actionscheduler_actions 
            WHERE status = 'failed' 
            AND scheduled_date_gmt < DATE_SUB(NOW(), INTERVAL 24 HOUR)
        ");
    });
    

    3. 优化 Object Cache Pro 配置

    // wp-config.php 中的 Redis 配置优化
    define('WP_REDIS_CONFIG', [
        'token' => 'your_token',
        'host' => '127.0.0.1',
        'port' => 6379,
        'database' => 0,
        'timeout' => 1.0,
        'read_timeout' => 1.0,
        'retry_interval' => 100,
        'compression' => 'zstd', // 使用 zstd 压缩
        'serializer' => 'igbinary', // 更快的序列化
        'async_flush' => true, // 异步刷新
        'split_alloptions' => true, // 分割 alloptions
        'prefetch' => false, // 多站点关闭预取
        'debug' => false,
        'save_commands' => false, // 关闭命令记录
        'prefix' => 'wpfanyi:', // 自定义前缀
    ]);
    

    4. WordPress 多站点查询优化

    // 减少跨站点查询
    add_filter('ms_site_check', '__return_false'); // 禁用站点检查
    add_filter('site_option__site_transient_timeout_available_translations', '__return_zero'); // 禁用翻译更新检查
    
    // 优化 GlotPress 查询
    add_filter('gp_before_get_translation_sets', function($query) {
        $query['no_found_rows'] = true; // 禁用分页计数
        $query['update_post_meta_cache'] = false;
        $query['update_post_term_cache'] = false;
        return $query;
    });
    

    5. 数据库索引优化

    检查并添加必要的索引:

    -- 为 wp_posts 表添加索引
    ALTER TABLE wp_posts ADD INDEX idx_post_type_status_date (post_type, post_status, post_date);
    ALTER TABLE wp_posts ADD INDEX idx_post_parent (post_parent);
    
    -- 为 wp_term_relationships 表添加索引
    ALTER TABLE wp_term_relationships ADD INDEX idx_term_taxonomy_id (term_taxonomy_id);
    
    -- 清理 objectcache_snapshots
    DELETE FROM wp_sitemeta WHERE meta_key LIKE 'objectcache_snapshots%';
    

    6. PHP-FPM 配置优化

    ; /etc/php/8.3/fpm/pool.d/www.conf
    pm = dynamic
    pm.max_children = 50
    pm.start_servers = 10
    pm.min_spare_servers = 5
    pm.max_spare_servers = 20
    pm.max_requests = 1000 ; 防止内存泄漏
    request_terminate_timeout = 30s ; 超时时间
    

    7. 监控和告警

    建议添加性能监控:

    // 在 mu-plugins 中添加性能监控
    add_action('shutdown', function() {
        if (defined('SAVEQUERIES') && SAVEQUERIES) {
            $queries = get_num_queries();
            $time = timer_stop(0, 3);
            if ($queries > 100 || $time > 1.0) {
                error_log(sprintf(
                    'Slow request: %s - %d queries in %s seconds',
                    $_SERVER['REQUEST_URI'],
                    $queries,
                    $time
                ));
            }
        }
    });
    

    你的优化方案已经覆盖了主要瓶颈,这些补充建议可以帮助进一步提升 WordPress 层面的性能。特别是对于多站点环境,减少跨站点查询和优化数据库索引会有明显效果。

Cyberforums · 原文 · Markdown

想回复或继续讨论?请到 Cyberforums 赛博论坛参与。

参与讨论 / 回复