Framadate 1.1.19 运维与升级手册
Framadate 1.1.19 生产运维实录:Apache+MariaDB 形态下的日常巡检、mysqldump 备份恢复(实测 7310 字节/5 表全量校验)、日志监控、过期清理机制、迁移幂等升级与回滚、数据库停机降级表现(302 maintenance.php)等全链路操作均附实测回显。
Framadate 1.1.19 运维与升级手册
目标:给出一台已按标准方式部署的 Framadate 1.1.19 服务器(Ubuntu 22.04.5 + Apache 2.4.52 + PHP 8.1.2 + MariaDB 10.6.23,监听 8145 端口)的日常运维完整操作手册——巡检、备份恢复、监控告警、清理机制、升级回滚与故障处置,全部命令与回显取自真实运行环境。安装过程见 《Ubuntu 22.04 安装 Framadate 1.1.19 指南》。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- Web 服务器:Apache 2.4.52,
Listen 8145,vhost/etc/apache2/sites-available/framadate.conf - PHP:8.1.2-1ubuntu2.28(mod_php)
- 数据库:MariaDB 10.6.23,库名
framadate,账号framadate(密码q1w2e3r4),表前缀fd_ - 应用目录:
/var/www/framadate(属主www-data:www-data) - 应用版本:
const VERSION = '1.1.19';(app/inc/constants.php第 21 行实测) - 数据现状:1 张日期投票(公开 ID
dvdEakcXnbWf7eXf,截止 2026-11-14)、2 条投票记录、5 张业务表
二、日常巡检
2.1 服务与端口状态
巡检第一步确认两个 systemd 服务与监听端口都活着:
systemctl is-active apache2
systemctl is-active mariadb
ss -ltn | grep 8145
curl -s -o /dev/null -w "首页 HTTP %{http_code}\n" http://localhost:8145/
active
active
LISTEN 0 511 0.0.0.0:8145 0.0.0.0:* users:(("apache2",pid=15383,fd=4),("apache2",pid=15388,fd=4))
首页 HTTP 200
(ss 回显节选:apache2 主进程 + 5 个工作进程共用监听 fd,此处展示前两个。)任何一环非 active/非 200,直接进入第七章故障处置。
2.2 应用体检页 admin/check.php
Framadate 自带安装体检页,运行期同样可用:它逐项校验 PHP 版本(对照 PHP_NEEDED_VERSION = '7.3')、intl/mbstring/openssl 扩展与数据库连通性。实测:
curl -s -o /dev/null -w "check.php %{http_code}\n" http://localhost:8145/admin/check.php
curl -s http://localhost:8145/admin/check.php | grep -oE "<title>[^<]*" | head -1
check.php 200
<title>Installation checking
页面为全绿即环境健康;若换 PHP 大版本或裁剪扩展后出现红色条目,按提示补装对应 php-* 包。
2.3 数据面巡检:管理工具页与库表
admin/ 目录是内置运维工具箱(实测全部 200):
curl -s -o /dev/null -w "admin/polls.php %{http_code}\n" http://localhost:8145/admin/polls.php
curl -s -o /dev/null -w "admin/logs.php %{http_code}\n" http://localhost:8145/admin/logs.php
curl -s -o /dev/null -w "admin/purge.php %{http_code}\n" http://localhost:8145/admin/purge.php
admin/polls.php 200
admin/logs.php 200
admin/purge.php 200
admin/polls.php:列出站内全部投票(标题、创建人、邮箱、截止日、公开 ID),实测回显:
<td>Reunion du projet</td>
<td>Admin</td>
<td>admin@framadate.local</td>
<td>14/11/26</td>
<td>dvdEakcXnbWf7eXf</td>
admin/logs.php:在线查看应用日志(即 2.4 节stdout.log);admin/purge.php:过期投票清理页(机制见第三章 3.4 节);- 根目录
find_polls.php:面向用户的「找回我的投票」页,凭管理员邮箱收链接,实测 200,标题Où sont mes sondages ?。
库表级巡检(行数与迁移记录):
mysql -uframadate -pq1w2e3r4 framadate -e \
"SELECT (SELECT COUNT(*) FROM fd_poll) polls,
(SELECT COUNT(*) FROM fd_slot) slots,
(SELECT COUNT(*) FROM fd_vote) votes,
(SELECT COUNT(*) FROM fd_framadate_migration) migrations;"
polls slots votes migrations
1 3 2 13
migrations 恒为 13(1.1.19 全部迁移数),若某日变少说明迁移表被误动,需立即排查。
2.4 应用日志 stdout.log
配置项 LOG_FILE = 'admin/stdout.log' 决定应用事件日志位置(相对应用根目录),实测全文:
20261101 152043 [CREATE_POLL] id:tXeTu4G6InJoAHGd, title: Reunion du projet, format:D, admin:Admin, mail:admin@framadate.local
20261101 152114 [CREATE_POLL] id:dvdEakcXnbWf7eXf, title: Reunion du projet, format:D, admin:Admin, mail:admin@framadate.local
格式为「时间戳 [事件] 键值对」。事件类型实测两类:CREATE_POLL(建票)与 EXPIRATION(清理动作,见 3.4 节)。注意第一条 tXeTu4G6InJoAHGd 即安装期 SMTP 500 事故留下的孤儿票日志——票已从 fd_poll 删除,日志仍留痕,属正常现象。该文件随事件增长,纳入日志轮转(见 4.1 节)。
三、备份与恢复
3.1 数据库备份(mysqldump)
Framadate 的全部状态都在 MariaDB 的 5 张表里,mysqldump 即完整业务备份。实测:
mysqldump -uframadate -pq1w2e3r4 --single-transaction framadate > /root/framadate-backup-2026-11-01.sql
ls -l /root/framadate-backup-2026-11-01.sql
grep -c "INSERT INTO" /root/framadate-backup-2026-11-01.sql
grep -E "Table structure" /root/framadate-backup-2026-11-01.sql
-rw-r--r-- 1 root root 7310 Nov 1 14:38 /root/framadate-backup-2026-11-01.sql
4
-- Table structure for table `fd_comment`
-- Table structure for table `fd_framadate_migration`
-- Table structure for table `fd_poll`
-- Table structure for table `fd_slot`
-- Table structure for table `fd_vote`
要点:--single-transaction 对 InnoDB 表做一致性快照、不锁表;备份含 5 表结构与 4 组 INSERT(当前数据量)。生产建议每日备份并保留 7 天,实测写入 crontab:
(crontab -l 2>/dev/null; echo "17 3 * * * mysqldump -uframadate -pq1w2e3r4 --single-transaction framadate | gzip > /var/backups/framadate-\$(date +\%F).sql.gz") | crontab -
crontab -l | tail -1
systemctl is-active cron
17 3 * * * mysqldump -uframadate -pq1w2e3r4 --single-transaction framadate | gzip > /var/backups/framadate-$(date +%F).sql.gz
active
⚠️ 注意:cron 行里的口令是演示值;生产环境应改用
mysql_config_editor登录路径或/etc/mysql/debian.cnf委托 root 身份,避免口令进 crontab。
3.2 恢复演练
恢复到独立库验证备份可用性(不碰生产库),实测:
mysql -uroot -e "CREATE DATABASE IF NOT EXISTS framadate_restore;"
mysql -uroot framadate_restore < /root/framadate-backup-2026-11-01.sql
mysql -uroot framadate_restore -e "SHOW TABLES; SELECT id,title FROM fd_poll;"
Tables_in_framadate_restore
fd_comment
fd_framadate_migration
fd_poll
fd_slot
fd_vote
id title
dvdEakcXnbWf7eXf Reunion du projet
5 表齐全、投票行完整即备份合格。生产整体回滚时把导入目标换成 framadate 库即可;迁移记录表随备份一起恢复,恢复后 admin/migration.php 重跑仍显示全部 Passé,不会二次迁移。
3.3 代码与配置备份
代码目录(含 vendor/ 依赖)整体打包即应用侧备份,实测打包产物 3.6M:
cd /var/www
tar czf /root/framadate-code-$(date +%F).tar.gz --exclude="framadate/app/inc/config.php" framadate
ls -lh /root/framadate-code-*.tar.gz
-rw-r--r-- 1 root root 3.6M Nov 1 14:38 /root/framadate-code-2026-11-01.tar.gz
app/inc/config.php 含数据库口令,单独走加密渠道保存、不进公共备份包;tpl_c/ 编译缓存可再生,无需备份。
3.4 过期清理机制(purge)
Framadate 不自动删票,需运维触发:admin/purge.php 页面提交后调用 PurgeService::purgeOldPolls(),删除「已过截止日且超出宽限期」投票及其 slot/vote/comment 级联数据。源码实测:
// admin/purge.php 触发逻辑
if ($action === 'purge' && $securityService->checkCsrf('admin', $_POST['csrf'])) {
$count = $purgeService->purgeOldPolls();
$message = __('Admin', 'Purged:') . ' ' . $count;
}
- 宽限期由配置
PURGE_DELAY = 60控制(截止日之后第 61 天起可被清除); - 清理动作带 CSRF 校验,浏览器打开
admin/purge.php点按钮即可;命令行等价做法实测(同会话取令牌再 POST):
cd /tmp
curl -s -c jar http://localhost:8145/admin/purge.php -o p.html
CSRF=$(grep -oE 'name="csrf" value="[^"]+"' p.html | head -1 | sed -E 's/.*value="([^"]+)".*/\1/')
curl -s -b jar http://localhost:8145/admin/purge.php \
--data-urlencode "action=purge" --data-urlencode "csrf=$CSRF" | grep -oE 'Purg[^<]*'
Purgés : 0
返回 Purgés : 0(法语界面下消息按 Purgés : 0 渲染):当前唯一活跃票截止 2026-11-14、未到「截止日+60 天」窗口,清理安全空跑。每次执行向 stdout.log 写 [EXPIRATION] Going to purge N poll(s)... 事件行(仅当 N>0)。
💡 说明:对外服务的实例建议把清理做成每日 cron(用上面的取令牌-POST 两步法即可脚本化),避免过期票无限堆积;宽限天数想收紧就改
PURGE_DELAY。
四、监控与告警
4.1 日志源与轮转
三个日志源各司其职:
| 文件 | 内容 | 来源配置 |
|---|---|---|
/var/log/apache2/framadate_access.log | 访问流水(combined) | vhost CustomLog |
/var/log/apache2/framadate_error.log | PHP Fatal/Warning、Apache 错误 | vhost ErrorLog |
/var/www/framadate/admin/stdout.log | 应用事件(建票/清理) | LOG_FILE 常量 |
实测错误日志样例(一次 strftime 兼容故障的完整记录):
[Sun Nov 01 14:23:56 2026] [php:error] [pid 14735] [client 127.0.0.1:58184]
PHP Fatal error: Uncaught TypeError: strftime(): Argument #2 ($timestamp) must be
of type ?int, string given in /var/www/framadate/exportcsv.php:80
stdout.log 不在 Apache 日志目录下,给 logrotate 加一条即可:
cat > /etc/logrotate.d/framadate <<'EOF'
/var/www/framadate/admin/stdout.log {
weekly
rotate 8
compress
missingok
notifempty
create 0644 www-data www-data
}
EOF
logrotate -d /etc/logrotate.d/framadate 2>&1 | head -5
WARNING: logrotate in debug mode does nothing except printing debug messages! Consider using verbose mode (-v) instead if this is not what you want.
reading config file /etc/logrotate.d/framadate
Reading state from file: /var/lib/logrotate/status
state file /var/lib/logrotate/status does not exist
reading config file 无报错即配置语法合格(-d 为干跑,不真轮转)。
4.2 关键告警规则
最小可用三条(cron 每小时 grep 一次即可起步,实测计数命令):
grep -c "PHP Fatal" /var/log/apache2/framadate_error.log
grep -c "SQLSTATE" /var/log/apache2/framadate_error.log
tail -100 /var/log/apache2/framadate_access.log | grep -cE ' (5[0-9]{2}) '
2
1
0
PHP Fatal增量 > 0:应用级故障(本项目已知两处,见第七章);SQLSTATE出现:数据库断连,服务已自动降级到维护页(见 6.4 节);- 访问日志 5xx 占比突增:优先看 error log 对应时间点。
⚠️ 注意:
stdout.log与admin/目录位于 DocumentRoot 内,默认即可被公网访问(实测curl http://localhost:8145/admin/stdout.log返回 200 并吐出日志内容)——日志里的投票人邮箱属敏感信息。上线前务必在 vhost 内封禁:
<FilesMatch "stdout\.log">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
</FilesMatch>
或直接把 LOG_FILE 指到 DocumentRoot 之外(如 /var/log/framadate/stdout.log,目录需 chown www-data)。
五、扩容缩容
Framadate 是「无状态 PHP 多页应用 + 单库」结构,扩缩容只在两处做文章:
- 应用层可横向复制:任意台数服务器共享同一 MariaDB 与同一份代码即可分流;PHP 会话仅用于 CSRF 令牌与语言偏好(响应头实测
Set-Cookie: PHPSESSID=...),文件会话默认落各机/tmp,多机场景要么前置会话粘滞,要么把session.save_handler指到 Redis(改/etc/php/8.1/apache2/php.ini后systemctl reload apache2即生效)。 - 数据库是唯一有状态组件:容量规划即 MariaDB 规划;投票数据行小(当前 1 票 3 槽 2 票记录库仅 7.3KB dump),单实例 MariaDB 足以支撑万级活跃投票,瓶颈出现时优先加索引内存(
innodb_buffer_pool_size),再考虑读写分离——应用无只读模式,读写分离需中间件,一般到不了这一步。
💡 说明:官方未提供集群/多主形态,应用层也没有节点注册概念,「扩容」= 加 Web 机 + LB 反代(反代场景记得设
const APP_URL,否则邮件/链接里的域名取自动探测值出错)。
六、升级与回滚
6.1 版本与迁移机制
升级安全性建立在两点:
- 版本号常量:
app/inc/constants.php实测const VERSION = '1.1.19';(第 21 行)、const PHP_NEEDED_VERSION = '7.3';(第 24 行); - 迁移判重表
fd_framadate_migration:admin/migration.php内置 14 个迁移类(AddColumn_ValueMax_In_poll_For_1_1重复登记一次,实际 13 条),凡记录表已有的条目直接跳过。实测重跑:
curl -s -o /dev/null -w "migration重跑 %{http_code}\n" http://localhost:8145/admin/migration.php
mysql -uframadate -pq1w2e3r4 framadate -e "SELECT COUNT(*) FROM fd_framadate_migration;"
migration重跑 200
COUNT(*)
13
页面逐条显示 Passé(已执行跳过),重跑无副作用——这是升级/回滚操作可反复演练的基础。
6.2 升级到新版本步骤
1.1.x 系列(1.1.17/1.1.18/1.1.19)数据库结构完全一致,CHANGELOG.md 实测 1.1.19 仅一行修复(移除邮件 X-Mailer 头),此类版本升级只换代码、不触发新迁移。以「从 1.1.18 升级到 1.1.19」为例的标准五步:
# 1. 备份(第三章命令,缺一不可)
mysqldump -uframadate -pq1w2e3r4 --single-transaction framadate | gzip > /var/backups/pre-upgrade.sql.gz
# 2. 下载新版本源码包并解包
wget https://github.com/framasoft/Framadate/archive/refs/tags/1.1.19.tar.gz -O /tmp/fd.tar.gz
tar xzf /tmp/fd.tar.gz -C /tmp
# 3. 替换代码,保留配置与权限
mv /var/www/framadate /var/www/framadate.old
mv /tmp/framadate-1.1.19 /var/www/framadate
cp /var/www/framadate.old/app/inc/config.php /var/www/framadate/app/inc/config.php
chown -R www-data:www-data /var/www/framadate
# 4. 拉取新版依赖(外网不通时按安装篇走代理 192.168.1.72:7897)
cd /var/www/framadate && composer install --no-dev
# 5. 触发迁移并验证
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8145/admin/migration.php
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8145/
第 5 步两条命令实测均应 200(本机对 1.1.19 执行迁移与首页验证的实测回显:migration重跑 200、首页 HTTP 200);admin/check.php 全绿即升级完成,观察正常后删除 framadate.old。
⚠️ 注意(本地补丁必须重打):项目 2021-12 后停更,PHP 8.1 的两处兼容修复不会随升级带过来——
exportcsv.php第 80 行的(int)强转补丁(CSV 导出 500)与config.php的use_smtp => false(建票 500)。升级后先跑一遍curl "http://localhost:8145/exportcsv.php?poll=dvdEakcXnbWf7eXf",若 500 立即重打补丁:sed -i 's/strftime(\$format, \$slot->title)/strftime($format, (int) $slot->title)/' \ /var/www/framadate/exportcsv.php
6.3 回滚
回滚 = 反向执行 6.2:代码目录换回 framadate.old(或解包 3.3 节代码备份),数据库用 pre-upgrade.sql.gz 恢复:
gunzip -c /var/backups/pre-upgrade.sql.gz | mysql -uroot framadate
迁移记录表随库一起回滚,无需单独处理;回滚后验证同 6.2 第 5 步两条 curl。
6.4 维护窗口与自动降级
Framadate 没有主动维护开关,但具备数据库断连自动降级能力,实测停库表现:
systemctl stop mariadb
curl -s -o /dev/null -w "停库后首页: %{http_code} -> %{redirect_url}\n" http://localhost:8145/
curl -sL http://localhost:8145/ | grep -oE "SQLSTATE[^<]*" | head -1
停库后首页: 302 -> http://localhost:8145/maintenance.php
SQLSTATE[HY000] [2002] No such file or directory
停掉 MariaDB 后所有请求被 302 到 maintenance.php(提示稍后再试),不产生 500 雪崩——这正是做数据库维护(如在线 DDL、升级 MariaDB)的推荐姿势:停库即全站优雅降级,维护完 systemctl start mariadb,实测恢复:
systemctl start mariadb
curl -s -o /dev/null -w "恢复后首页: %{http_code}\n" http://localhost:8145/
恢复后首页: 200
七、故障排查手册
按「现象 → 定位 → 处置」组织运维侧高频故障;功能级 bug(投票提交、ICS 导出等)见第九章 FAQ。
7.1 首页 500 白屏
定位顺序:tail -20 /var/log/apache2/framadate_error.log 看 PHP Fatal 具体文件行号。三类常见根因实测:
SQLSTATE[HY000] [2002]:MariaDB 未运行或 socket 路径不符 →systemctl start mariadb;Permission denied写tpl_c/:Smarty 编译缓存不可写 →chown -R www-data:www-data /var/www/framadate/tpl_c;Failed to open stream: config.php:配置文件丢失/路径错 → 从备份恢复app/inc/config.php。
7.2 建票最后一步 500(SMTP connect() failed)
错误日志实测原文:
PHP Fatal error: Uncaught PHPMailer\PHPMailer\Exception: SMTP connect() failed.
in /var/www/framadate/vendor/phpmailer/phpmailer/src/PHPMailer.php:1982
#3 /var/www/framadate/create_date_poll.php(213): Framadate\Services\MailService->send()
处置:sed -i "s/'use_smtp' => true/'use_smtp' => false/" /var/www/framadate/app/inc/config.php;随后清理已写库的孤儿票:
mysql -uframadate -pq1w2e3r4 framadate -e \
"DELETE FROM fd_slot WHERE poll_id='tXeTu4G6InJoAHGd';
DELETE FROM fd_poll WHERE id='tXeTu4G6InJoAHGd';"
(孤儿票 ID 从 stdout.log 的 [CREATE_POLL] 行反查。)
7.3 CSV 导出 500(strftime TypeError)
见 6.2 节 ⚠️ 的补丁命令,打完 exportcsv.php?poll=dvdEakcXnbWf7eXf 实测 200、text/csv; charset=utf-8。
7.4 投票短链 404 但查询串正常
.htaccess、AllowOverride All、mod_rewrite 三件套缺一即此症状,逐项核对命令见 《Ubuntu 22.04 安装 Framadate 1.1.19 指南》 9.3 节;改完 systemctl reload apache2 用 curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8145/dvdEakcXnbWf7eXf 复测(实测 200)。
7.5 管理员链接丢失
管理权 = 24 位 admin_id,用户侧丢了可走 find_polls.php 凭邮箱重发(需 use_smtp=true 且 SMTP 可用)。运维侧直接查库兜底(实测):
mysql -uframadate -pq1w2e3r4 framadate -e "SELECT id,admin_id FROM fd_poll WHERE id='dvdEakcXnbWf7eXf';"
id admin_id
dvdEakcXnbWf7eXf dvdEakcXnbWf7eXfgPd6E5R4
拼上 /<admin_id>/admin 即管理页地址(干净链接规则 4.7 节已重写路由)。
八、常见问题
8.1 purge 会不会误删活跃投票
不会。purgeOldPolls() 只选「截止日 + PURGE_DELAY(60 天) 已过」的票;实测当前唯一活跃票(截止 2026-11-14)执行清理返回 Purgés : 0(见 3.4 节命令)。宽限期想缩短改 PURGE_DELAY 即可,但建议保持默认——投票人可能迟来管理结果。
8.2 界面默认法语,怎么切英文
DEFAULT_LANGUAGE = 'fr' 是站点主语言;访客侧带 ?lang=en 即切英文,实测首页标题从 Organiser des rendez-vous simplement, librement. - Framadate 变为 Make your polls - Framadate。支持的 9 种语言见配置 $ALLOWED_LANGUAGES(fr/en/oc/es/de/nl/it/br/ca)。
8.3 升级后要不要重新 composer
要。vendor/ 不随 tag 源码包分发(安装篇 4.5 节),换代码目录后必须 composer install --no-dev;framagit.org 直连超时的机器记得先 export 代理。
8.4 数据库账号权限能再收紧吗
可以。应用只需 framadate 库的 DML/DDL(迁移建表需要 DDL),无法只给 DML;但可禁止该账号远程登录(保持 localhost)、并用 REVOKE ALL ON *.* 确认无全局权限——实测 SHOW GRANTS 仅 USAGE ON *.* + framadate.* 库级授权,已是最小形态。
九、参考链接
- 《Framadate 1.1.19 介绍与选型指南》(架构与数据模型背景)
- 《Ubuntu 22.04 安装 Framadate 1.1.19 指南》(部署三方案与 .htaccess 细节)
- 源码内官方文档:
doc/TECHNICAL.md、doc/TREEVIEW.md(页面职责树) - mysqldump 手册:https://mariadb.com/kb/en/mysqldump/
- logrotate 手册:https://manpages.ubuntu.com/manpages/jammy/man8/logrotate.8.html