Typecho高可用(HA)部署终极指南(Gemini总结)

· 记录

一、 方案概述

本手册是为基于Debian 11和Docker的Typecho博客量身定制的、最完整的高可用(HA)部署指南。它将指导您完成从调整现有架构到实现全自动故障切换并进行万无一失的数据恢复的每一个步骤。

核心技术架构:

组件 技术实现 目的
网络层 Tailscale 建立安全的虚拟私有网络,加密数据库同步流量。
应用代理 Caddy 提供安全、自动化的HTTPS反向代理,解决521/525错误。
数据同步 MariaDB主从复制 (ROW格式) 实时、精确地同步数据库(文章、评论等),保证数据绝对一致。
rsync + cron + 包装脚本 定时同步文件(主题、插件、上传文件等),并安全地修复权限。
故障切换 Cloudflare API + 有状态监控脚本 自动检测主服务器故障,切换DNS,自动配置备库,并避免循环告警。
告警通知 Telegram Bot API 在故障切换成功后,立即发送通知到您的Telegram。

二、 占位符说明 (操作前必读)

在开始操作前,请准备好以下信息。在后续文档中,所有需要您填写的地方都会用 [占位符] 的形式清晰标出。

占位符 说明 示例
[服务器A的公网IP] 您的主服务器的公网IP地址。 123.45.67.89
[服务器B的公网IP] 您的备用服务器的公网IP地址。 98.76.54.32
[自定义SSH端口] 您服务器的自定义SSH端口。 22022
[自定义数据库端口] 您希望暴露给Tailscale网络的数据库端口,建议非3306。 33061
[Typecho暴露给主机的端口] 您希望Typecho服务暴露给本机的端口,用于Caddy反代。 8080
[您的登录用户名] 您用SSH登录服务器时使用的用户名。 debian_user
[您的邮箱地址] 用于生成SSH密钥注释,方便识别。 user@example.com
[您的博客域名] 您在Cloudflare上配置的完整域名。 blog.yourdomain.com
[Cloudflare区域ID] 在Cloudflare域名概览页找到的Zone ID。 a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
[Cloudflare API令牌] 您创建的用于编辑DNS的API Token。 gH1iJ2kL3mN4oP5qR6sT7uV8wX9yZ0aB1c
[Cloudflare DNS记录ID] 您要修改的A记录的ID。 f1e2d3c4b5a6f1e2d3c4b5a6f1e2d3c4
[TG机器人令牌] 您从@BotFather获取的Telegram机器人Token。 123456:ABC-DEF1234ghIkl-zyx57W2v1u
[TG频道或用户的ChatID] 您希望接收通知的对话ID。 123456789
[数据库ROOT密码] docker-compose.yml中设置的MYSQL_ROOT_PASSWORD Strong_DB_Root_Password
[Typecho数据库密码] docker-compose.yml中为Typecho设置的MYSQL_PASSWORD Strong_Typecho_DB_Password
[数据库复制用的密码] 用于主从复制的专用数据库用户密码。 Replica_Pa$$w0rd_2025!

三、 步骤一:环境初始化

  1. 在服务器A上准备架构 本方案要求数据库文件和应用文件使用绑定挂载(Bind Mount)以便于同步和管理。请确保您的docker-compose.yml文件结构与下方类似。此示例包含了所有必要服务及其端口映射。

    version: '3.8'
    services:
      typecho:
        image: joyqi/typecho:1.2.1-php8.2-apache
        container_name: typecho_app
        restart: always
        ports:
          - "[Typecho暴露给主机的端口]:80"
        volumes:
          - ./app:/app
        depends_on:
          - mariadb
    
      mariadb:
        image: mariadb:10.11
        container_name: typecho_db
        restart: always
        ports:
          - "[服务器A的Tailscale IP]:[自定义数据库端口]:3306"
        environment:
          MYSQL_ROOT_PASSWORD: '[数据库ROOT密码]'
          MYSQL_DATABASE: 'typecho_blog'
          MYSQL_USER: 'typecho_user'
          MYSQL_PASSWORD: '[Typecho数据库密码]'
        volumes:
          - ./mariadb/data:/var/lib/mysql
          - ./mariadb/conf.d:/etc/mysql/conf.d
    
  2. 初始化服务器B环境 (克隆架构)

    • 在服务器A上打包:
      cd ~/docker/typecho/
      tar -czvf typecho_arch.tar.gz .
      
    • 传输到服务器B:
      scp -P [自定义SSH端口] typecho_arch.tar.gz [您的登录用户名]@[服务器B的公网IP]:~/
      
    • 在服务器B上解压:
      mkdir -p ~/docker/typecho
      mv ~/typecho_arch.tar.gz ~/docker/typecho/
      cd ~/docker/typecho/
      tar -xzvf typecho_arch.tar.gz
      
  3. 配置安全网络 (Tailscale)服务器A和服务器B上都执行:

    curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up
    

    【记录】 分别在两台服务器上用 tailscale ip -4 获取并记下各自的Tailscale IP (TS_IP_ATS_IP_B)。

  4. 环境与防火墙准备 (两台服务器均需执行)

    • 安装必要工具:
      sudo apt update
      sudo apt install -y rsync
      
    • 配置防火墙: 在 服务器A 上执行:
      sudo ufw allow [自定义SSH端口]/tcp
      sudo ufw allow [自定义数据库端口]/tcp
      sudo ufw allow 80/tcp
      sudo ufw allow 443/tcp
      
      服务器B 上执行:
      sudo ufw allow [自定义SSH端口]/tcp
      sudo ufw allow 80/tcp
      sudo ufw allow 443/tcp
      

四、 步骤二:应用层代理配置 (Caddy)

此步骤用于解决Cloudflare的521/525错误,两台服务器均需执行以保持环境一致。

  1. 安装Caddy

    sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
    sudo apt update
    sudo apt install caddy
    
  2. 配置Caddyfile

    • 编辑Caddyfile: sudo nano /etc/caddy/Caddyfile
    • 删除所有默认内容,粘贴以下配置:
      [您的博客域名] {
          reverse_proxy localhost:[Typecho暴露给主机的端口]
      }
      
    • 重载Caddy: sudo systemctl reload caddy

五、 步骤三:数据持续同步

5.1 数据库同步 (MariaDB主从复制)
1. 在服务器A (Master) 上操作
2. 在服务器B (Slave) 上操作
3. 【金标准】初始化从库同步

此流程将保证从库是主库的完美克隆。

  1. 在服务器A(主库)上,创建带坐标的完美快照
    cd ~/docker/typecho/
    docker-compose exec mariadb mysqldump -u root -p'[数据库ROOT密码]' --single-transaction --master-data=2 typecho_blog > ~/master_definitive_dump.sql
    
  2. 将快照传输到服务器B(从库)
    scp -P [自定义SSH端口] ~/master_definitive_dump.sql [您的登录用户名]@[服务器B的公网IP]:~/
    
  3. 在服务器B(从库)上,清空并恢复
    • 进入MariaDB容器: docker-compose exec mariadb mysql -u root -p
    • 执行清空操作:
      DROP DATABASE typecho_blog;
      CREATE DATABASE typecho_blog;
      EXIT;
      
    • 从快照恢复数据 (注意-p后没有空格):
      cat ~/master_definitive_dump.sql | docker-compose exec -T mariadb mysql -u root -p'[数据库ROOT密码]' typecho_blog
      
  4. 在服务器B上,使用精确坐标启动同步
    • 从快照文件中提取坐标:
      grep "CHANGE MASTER TO" ~/master_definitive_dump.sql
      
    • 复制CHANGE MASTER TO...后面的内容,修改连接信息后,在B的MariaDB中执行:
      -- 示例,请根据您自己的信息和上一步grep出的坐标修改
      CHANGE MASTER TO
        MASTER_HOST='[服务器A的Tailscale IP]',
        MASTER_USER='repl',
        MASTER_PASSWORD='[数据库复制用的密码]',
        MASTER_PORT=[自定义数据库端口],
        MASTER_LOG_FILE='[从快照文件里看到的文件名]',
        MASTER_LOG_POS=[从快照文件里看到的Position];
      
      START SLAVE;
      
  5. 验证:在B的MariaDB中执行 SHOW SLAVE STATUS\G;,确保 Slave_IO_Running: Yes, Slave_SQL_Running: YesSeconds_Behind_Master: 0
5.2 文件同步 (rsync)
1. 在服务器B上生成SSH密钥并授权到服务器A
2. 在服务器B上创建权限修复脚本
3. 在服务器B上配置Sudo免密执行权限
4. 在服务器B上创建定时同步任务

六、 步骤四:配置自动故障切换

此部分操作均在服务器B上进行。

1. 获取Telegram与Cloudflare凭证
1.1 获取Telegram凭证

我们需要从Telegram获取两项信息:

  1. 机器人令牌 (Bot Token):用于授权脚本通过您的机器人发送消息。
  2. 对话ID (Chat ID):用于指定机器人将消息发送到哪个聊天窗口。
1.2 获取Cloudflare凭证

我们需要从Cloudflare获取三项信息:

  1. 区域ID (Zone ID):代表您的域名。
  2. API令牌 (API Token):用于授权脚本修改DNS。
  3. DNS记录ID (DNS Record ID):您博客域名A记录的唯一标识。
2. 创建并配置监控切换脚本

在获取完所有凭证后,执行 sudo nano /usr/local/bin/cf_failover.sh,粘贴以下脚本,并仔细替换脚本配置区域的所有占位符

#!/bin/bash
# --- 配置区域 ---
CF_API_TOKEN="[Cloudflare API令牌]"
CF_ZONE_ID="[Cloudflare区域ID]"
CF_RECORD_ID="[Cloudflare DNS记录ID]"
DOMAIN_NAME="[您的博客域名]"
PRIMARY_IP="[服务器A的公网IP]"
SECONDARY_IP="[服务器B的公网IP]"
TG_BOT_TOKEN="[TG机器人令牌]"
TG_CHAT_ID="[TG频道或用户的ChatID]"
DB_ROOT_PASSWORD="[数据库ROOT密码]"
DOCKER_COMPOSE_PATH="/home/[您的登录用户名]/docker/typecho/docker-compose.yml"

# --- 脚本行为配置 ---
FAILURE_THRESHOLD=3
LOG_FILE="/tmp/cf_failover.log"
COUNTER_FILE="/tmp/failover_counter"
STATE_FILE="/tmp/failover_active.lock"

# --- 脚本主体 ---
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"; }

if [ -f "$STATE_FILE" ]; then
    log "Failover is already active (lock file exists). Skipping health check."
    exit 0
fi

if [ ! -f "$COUNTER_FILE" ]; then echo 0 > "$COUNTER_FILE"; fi
HEALTH_CHECK_URL="https://$DOMAIN_NAME/"
HTTP_STATUS=$(curl --connect-timeout 5 --max-time 10 -s -o /dev/null -w "%{http_code}" --resolve "$DOMAIN_NAME:443:$PRIMARY_IP" "$HEALTH_CHECK_URL")
if [ "$HTTP_STATUS" -eq 200 ]; then
    log "Health check PASSED for $PRIMARY_IP (Status: $HTTP_STATUS)."
    echo 0 > "$COUNTER_FILE"
else
    log "Health check FAILED for $PRIMARY_IP (Status: $HTTP_STATUS)."
    FAIL_COUNT=$(<"$COUNTER_FILE")
    FAIL_COUNT=$((FAIL_COUNT + 1))
    echo $FAIL_COUNT > "$COUNTER_FILE"
    log "Failure count is now $FAIL_COUNT."
    if [ "$FAIL_COUNT" -ge "$FAILURE_THRESHOLD" ]; then
        log "Failure threshold reached. Triggering failover to $SECONDARY_IP."
        UPDATE_RESPONSE=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$CF_RECORD_ID" \
             -H "Authorization: Bearer $CF_API_TOKEN" \
             -H "Content-Type:application/json" \
             --data '{"type":"A","name":"'$DOMAIN_NAME'","content":"'$SECONDARY_IP'","ttl":60,"proxied":true}')
        log "Cloudflare API response: $UPDATE_RESPONSE"
        
        if echo "$UPDATE_RESPONSE" | grep -q '"success":true'; then
            log "Cloudflare DNS updated successfully."
            
            log "Attempting to disable read-only mode on Server B."
            docker-compose -f "$DOCKER_COMPOSE_PATH" exec -T mariadb mysql -u root -p"$DB_ROOT_PASSWORD" -e "SET GLOBAL read_only = OFF;"
            log "Read-only mode disabled successfully. Server B is now fully operational."

            touch "$STATE_FILE"
            log "Lock file created at "$STATE_FILE". Monitoring is now paused."

            if [[ -n "$TG_BOT_TOKEN" && -n "$TG_CHAT_ID" ]]; then
                NOTIFICATION_MESSAGE="🚨 **Typecho 故障切换成功** 🚨%0A%0A**状态**: 主服务器A无响应,流量已自动切换至备用服务器B。**数据库已设为可写,服务完全接管。**%0A%0A**域名**: \`$DOMAIN_NAME\`%0A%0A请尽快检查主服务器A的状态!"
                curl -s -o /dev/null -X POST "https://api.telegram.org/bot$TG_BOT_TOKEN/sendMessage" -d chat_id="$TG_CHAT_ID" -d text="$NOTIFICATION_MESSAGE" -d parse_mode="Markdown"
                log "Telegram notification sent."
            fi
            echo 0 > "$COUNTER_FILE"
        else
            log "Cloudflare API call FAILED. Please check API response above. Failover aborted."
        fi
    fi
fi
3. 启动监控

执行 sudo chmod +x /usr/local/bin/cf_failover.shsudo crontab -e,添加: * * * * * /usr/local/bin/cf_failover.sh


七、 步骤五:测试与验证

  1. 正常状态检查: 访问博客。检查B的数据库同步状态和文件同步情况。
  2. 模拟故障: 在服务器A上执行 cd ~/docker/typecho/ && docker-compose stop
  3. 观察切换: 在服务器B上执行 tail -f /tmp/cf_failover.log
  4. 验证结果: 几分钟后,刷新博客应仍可访问且功能正常。Cloudflare后台DNS记录应已更新,同时您的Telegram应收到告警。

八、 步骤六:故障恢复与回切 (终极可靠流程)

当服务器A修复后,严格按以下步骤手动切回,确保数据万无一失。

1. 阶段一:准备
  1. 服务器B上,执行 sudo crontab -e注释掉监控脚本行。
  2. 服务器B上,删除状态锁文件sudo rm /tmp/failover_active.lock
2. 阶段二:数据反向同步 (B -> A)

此流程将保证服务器A是服务器B的完美克隆。

  1. 在服务器B(临时主库)上,创建带坐标的完美快照
    • 暴露数据库端口: 编辑 nano ~/docker/typecho/docker-compose.yml,在mariadb服务下添加ports部分,然后 docker-compose up -d
    • 放行防火墙: sudo ufw allow [自定义数据库端口]/tcp
    • 创建快照:
      cd ~/docker/typecho/
      docker-compose exec mariadb mysqldump -u root -p'[数据库ROOT密码]' --single-transaction --master-data=2 typecho_blog > ~/reverse_definitive_dump.sql
      
  2. 在服务器B上,执行一次性的文件同步,将故障期间上传的文件推送到服务器A:
    rsync -avz -e 'ssh -p [自定义SSH端口]' --delete --exclude='config.inc.php' ~/docker/typecho/app/ [您的登录用户名]@[服务器A的公网IP]:~/docker/typecho/app/
    
  3. 将数据库快照传输到服务器A(临时从库)
    scp -P [自定义SSH端口] ~/reverse_definitive_dump.sql [您的登录用户名]@[服务器A的公网IP]:~/
    
  4. 在服务器A上,清空并恢复
    • 进入MariaDB容器: docker-compose exec mariadb mysql -u root -p
    • 执行清空操作:
      STOP SLAVE;
      DROP DATABASE typecho_blog;
      CREATE DATABASE typecho_blog;
      EXIT;
      
    • 从快照恢复数据:
      cat ~/reverse_definitive_dump.sql | docker-compose exec -T mariadb mysql -u root -p'[数据库ROOT密码]' typecho_blog
      
  5. 在服务器A上,使用精确坐标启动同步
    • 从快照文件中提取坐标: grep "CHANGE MASTER TO" ~/reverse_definitive_dump.sql
    • 复制CHANGE MASTER TO...后面的内容,修改连接信息后,在A的MariaDB中执行:
      -- 示例,请根据您自己的信息和上一步grep出的坐标修改
      CHANGE MASTER TO
        MASTER_HOST='[服务器B的Tailscale IP]',
        MASTER_USER='repl', -- 复用'repl'用户
        MASTER_PASSWORD='[数据库复制用的密码]', -- 复用之前的密码
        MASTER_PORT=[自定义数据库端口],
        MASTER_LOG_FILE='[从快照文件里看到的文件名]',
        MASTER_LOG_POS=[从快照文件里看到的Position];
      
      START SLAVE;
      
  6. 【保险锁】验证同步完成:在A的MariaDB中反复执行 SHOW SLAVE STATUS\G;,直到 Seconds_Behind_Master 稳定为 0
3. 阶段三:服务切回
  1. 当且仅当完成以上数据库和文件两个反向同步步骤后,登录Cloudflare网站,手动将DNS记录改回**[服务器A的公网IP]**。
4. 阶段四:恢复原始架构

在流量已经成功切回服务器A后,我们需要将主从关系恢复到最初的 A(Master) -> B(Slave) 状态。

  1. 在服务器A上 (恢复Master角色):

    • 进入服务器A的MariaDB容器:docker-compose exec mariadb mysql -u root -p
    • 执行以下命令,停止其作为临时从库的角色,并重置为主库:
      STOP SLAVE;
      RESET MASTER;
      
    • 现在,获取A作为主库的新的、干净的日志坐标,以供B来跟随:
      SHOW MASTER STATUS;
      
    • 【记录】记下A服务器上显示的新的 FilePosition 值。
  2. 在服务器B上 (恢复Slave角色):

    • 进入服务器B的MariaDB容器:docker-compose exec mariadb mysql -u root -p
    • 执行以下命令,彻底重置其状态,并重新指向服务器A:
      STOP SLAVE;
      RESET SLAVE ALL; -- 清理所有旧的、临时的复制状态
      SET GLOBAL read_only = ON; -- 【重要】重新将数据库设为只读模式
      
      -- 使用您刚刚从服务器A记录下的新坐标
      CHANGE MASTER TO
        MASTER_HOST='[服务器A的Tailscale IP]',
        MASTER_USER='repl',
        MASTER_PASSWORD='[数据库复制用的密码]',
        MASTER_PORT=[自定义数据库端口],
        MASTER_LOG_FILE='[A服务器新的File值]',
        MASTER_LOG_POS=[A服务器新的Position值];
      
      START SLAVE;
      
5. 阶段五:收尾与最终验证
  1. 在服务器B上,清理临时配置

    • 关闭数据库端口暴露: 编辑 nano ~/docker/typecho/docker-compose.yml删除或注释掉为反向同步临时添加的ports部分。
    • 应用配置: cd ~/docker/typecho/ && docker-compose up -d
    • 关闭防火墙端口: sudo ufw deny [自定义数据库端口]/tcp
  2. 在服务器B上,恢复自动监控

    • 执行 sudo crontab -e取消cf_failover.sh 脚本的注释,恢复自动故障切换功能。
  3. 最终验证

    • 服务器B的MariaDB中,最后一次检查状态:
      SHOW SLAVE STATUS\G;
      
    • 您应该能看到一个完全健康的、指向服务器A的主从关系,并且 Seconds_Behind_Master0

本文作者: 𝓬𝓸𝓵𝓪 🚀
本文链接: http://142.171.55.106:8086/archives/55/
最后修改:
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!