DAViCal 1.1.13 日常运维手册
Ubuntu 24.04.5 实测 DAViCal 1.1.13 日常运维:服务/资源/OPTIONS 协议/端到端 CalDAV 五层巡检、pg_dump -Fc 与 export_calendar 备份及临时库恢复演练、refresh-alarms 告警定时任务、max_connections 扩缩容实测、update-davical-database 升级与 apt 级联卸载回滚实录、七类故障速查。
DAViCal 1.1.13 日常运维手册
目标:给出 DAViCal 1.1.13 在 Ubuntu 24.04.5 LTS 真实服务器上的日常运维完整操作手册——服务与资源巡检、CalDAV 协议体检与端到端链路验证、日志检查、数据库/配置/日历三级备份与恢复演练、告警刷新定时任务、外部探活监控、连接参数扩缩容实测、schema 升级与故障回滚实录、六类常见故障速查。全部命令与回显均为本机实测照抄。
一、环境信息
本文所有实测基于以下单机环境:
- 操作系统:Ubuntu 24.04.5 LTS(x86_64)
- Web 服务器:Apache/2.4.58(Ubuntu),
libapache2-mod-php8.3模式 - PHP:PHP 8.3.6,扩展
pgsql、xml - 数据库:PostgreSQL 16.15
- 软件版本:DAViCal 1.1.13 + AWL 0.65(官网 tar.xz 部署,代码位于
/usr/share/davical、/usr/share/awl),数据库 schema 1.3.6 - 域名与路径:
davical.example.com,Web 根路径/davical/,CalDAV 入口/davical/caldav.php/,监听 80 端口 - 数据库对象:库
davical;角色davical_dba(DDL 属主)/davical_app(应用运行),演示口令统一q1w2e3r4 - 配置文件:
/etc/davical/config.php、/etc/davical/davical.example.com-conf.php、/etc/davical/administration.yml,权限640 root:www-data - 演示账号:管理员
admin,普通用户q1,口令均为q1w2e3r4 - 数据规模基线(巡检与备份对照用):集合
collection7 行、日历条目calendar_item6 行、用户usr4 行
💡 技术拓展:DAViCal 的配置是「全局 + 按域名」两级。同一套代码可挂多个虚拟域,
/etc/davical/config.php提供全局默认(如数据库连接数组),<域名>-conf.php(本例davical.example.com-conf.php)按请求的Host头加载并覆盖同名全局项。多域共用一个库时各域互不可见;排查「改了配置不生效」的问题必须先确认改的是哪一层。本文单机单域,日常以域配置为准。
二、日常巡检
日常巡检回答一个问题:服务是不是真的活着、协议是不是真的通。只看进程在不在是不够的——本章给出从进程、资源、协议握手到端到端读写链路的五层检查,全部有实测回显。
2.1 服务与进程状态
确认 Apache、PostgreSQL 两个 systemd 单元处于 active,数据库可接受连接:
systemctl is-active apache2
active
systemctl is-active postgresql
active
pg_isready
/var/run/postgresql:5432 - accepting connections
三项是巡检的最小集合:Apache 挂了表现为连接拒绝,PostgreSQL 挂了则 Web 页变错误页(见第七章),pg_isready 返回 accepting connections 才算数据库就绪。
2.2 资源水位
检查磁盘占用、代码与数据目录体积、数据库大小和当前连接数:
df -h --output=target,pcent,avail /
Mounted on Use% Avail
/ 62% 162G
du -sh /usr/share/davical /etc/davical /var/lib/postgresql/16/main
11M /usr/share/davical
16K /etc/davical
65M /var/lib/postgresql/16/main
su postgres -c "psql davical -tAc \"select pg_size_pretty(pg_database_size('davical'));\""
9807 kB
su postgres -c "psql davical -tAc 'select count(*) from pg_stat_activity;'"
6
| 指标 | 实测值 | 巡检要点 |
|---|---|---|
| 根分区使用率 | 62%(余 162G) | 超过 85% 需清理日志/旧备份 |
| DAViCal 代码目录 | 11M | 升级后体积异常膨胀说明旧版本未清理 |
| PostgreSQL 数据目录 | 65M(库本身 9807 kB) | 差值是 WAL/临时文件,持续膨胀需 vacuum |
| 活动连接数 | 6 | 常态应远低于 max_connections=100(见 5.1) |
2.3 协议体检:OPTIONS
OPTIONS 是唯一能同时验证 Apache→PHP→DAViCal→数据库全链路、又无副作用的方法,且响应头直接暴露版本:
curl -s -u admin:q1w2e3r4 -X OPTIONS http://davical.example.com/davical/caldav.php/admin/ -D - -o /dev/null
HTTP/1.1 200 OK
Date: Sun, 01 Nov 2026 14:32:13 GMT
Server: Apache/2.4.58 (Ubuntu)
DAV: 1, 2, 3, access-control, calendar-access, calendar-schedule
DAV: extended-mkcol, bind, addressbook, calendar-auto-schedule, calendar-proxy
Allow: OPTIONS, PROPFIND, REPORT, DELETE, LOCK, UNLOCK, MOVE, GET, HEAD, MKCOL, MKCALENDAR, PROPPATCH, BIND, ACL
X-DAViCal-Version: DAViCal/1.1.13; DB/1.3.6
Content-Length: 0
Content-Type: text/plain; charset="utf-8"
解读:X-DAViCal-Version 同时给出软件版本(1.1.13)与数据库 schema 修订(1.3.6),两者不一致时页面会提示需要升级 schema(见 6.3);DAV: 行声明了 calendar-access(CalDAV)、addressbook(CardDAV)与 access-control(ACL)能力;Allow 中的 MKCALENDAR 是 CalDAV 扩展方法,若 Apache 层丢方法会在这里或 error.log 暴露(见第七章 501 案例)。
不带凭据时应得到 401 挑战而不是 500:
curl -s -X OPTIONS http://davical.example.com/davical/caldav.php/admin/ -D - -o /dev/null
HTTP/1.1 401 Unauthorized
Date: Sun, 01 Nov 2026 14:32:13 GMT
Server: Apache/2.4.58 (Ubuntu)
WWW-Authenticate: Basic realm="DAViCal CalDAV Server"
Content-Length: 40
Content-Type: text/plain; ; charset="utf-8"
401 + WWW-Authenticate: Basic 是认证层正常的标志;未认证却返回 500/Database Error,说明问题出在配置或数据库,而不是客户端凭据。
2.4 端到端 CalDAV 读写链路
用一个临时事件走一遍 PUT→GET→If-Match→DELETE 全链路,验证「写进数据库、读出一致、乐观锁生效、删除干净」:
curl -s -u q1:q1w2e3r4 -X PUT http://davical.example.com/davical/caldav.php/q1/work-calendar/ops-verify.ics \
-H "Content-Type: text/calendar" --data-binary "BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ops-verify//EN
BEGIN:VEVENT
UID:ops-verify-20261101@davical.example.com
DTSTAMP:20261101T150000Z
DTSTART:20261109T020000Z
DTEND:20261109T023000Z
SUMMARY:Verify
END:VEVENT
END:VCALENDAR" -o /dev/null -D /var/tmp/n6.h -w "PUT=%{http_code}\n"
grep -i etag /var/tmp/n6.h
PUT=201
ETag: "e4bea4f07638cb2d84bac0b094ef1851"
curl -s -u q1:q1w2e3r4 -D /var/tmp/n7.h -o /var/tmp/n7.body \
http://davical.example.com/davical/caldav.php/q1/work-calendar/ops-verify.ics
grep -iE "^HTTP|content-type|^etag" /var/tmp/n7.h
grep -E "BEGIN:VEVENT|SUMMARY" /var/tmp/n7.body
HTTP/1.1 200 OK
Etag: "e4bea4f07638cb2d84bac0b094ef1851"
Content-Type: text/calendar; component=vevent; charset="utf-8"
BEGIN:VEVENT
SUMMARY:Verify
回读的 Etag 与 PUT 返回一致、component=vevent 说明服务端正确识别了组件类型。再验证 If-Match 乐观锁——错值必须 412,正确值必须 204:
curl -s -u q1:q1w2e3r4 -X PUT http://davical.example.com/davical/caldav.php/q1/work-calendar/ops-verify.ics \
-H "Content-Type: text/calendar" -H 'If-Match: "deadbeef"' --data-binary "BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ops-verify//EN
BEGIN:VEVENT
UID:ops-verify-20261101@davical.example.com
DTSTAMP:20261101T150000Z
DTSTART:20261109T020000Z
DTEND:20261109T023000Z
SUMMARY:Verify2
END:VEVENT
END:VCALENDAR" -o /dev/null -w "BADMATCH=%{http_code}\n"
BADMATCH=412
ETAG=$(grep -i '^etag' /var/tmp/n7.h | tr -d '\r' | cut -d' ' -f2-)
curl -s -u q1:q1w2e3r4 -X PUT http://davical.example.com/davical/caldav.php/q1/work-calendar/ops-verify.ics \
-H "Content-Type: text/calendar" -H "If-Match: $ETAG" --data-binary "BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ops-verify//EN
BEGIN:VEVENT
UID:ops-verify-20261101@davical.example.com
DTSTAMP:20261101T150000Z
DTSTART:20261109T020000Z
DTEND:20261109T023000Z
SUMMARY:Verify3
END:VEVENT
END:VCALENDAR" -o /dev/null -w "GOODMATCH=%{http_code}\n"
GOODMATCH=204
curl -s -u q1:q1w2e3r4 -X DELETE http://davical.example.com/davical/caldav.php/q1/work-calendar/ops-verify.ics -o /dev/null -w "DEL=%{http_code}\n"
curl -s -u q1:q1w2e3r4 -o /dev/null -w "GET404=%{http_code}\n" http://davical.example.com/davical/caldav.php/q1/work-calendar/ops-verify.ics
DEL=204
GET404=404
201→200→412→204→204→404 六步全对,写链路才算健康。$ETAG 取自上一步实测响应头文件,非占位符。
2.5 增量同步:sync-collection
移动端不可能每次全量拉取日历,sync-collection REPORT(RFC 6578)是增量同步的协议基础。注意 sync-collection 元素属于 DAV: 命名空间,不是 urn:ietf:params:xml:ns:caldav:——写错命名空间会被直接拒绝:
curl -s -u q1:q1w2e3r4 -X REPORT http://davical.example.com/davical/caldav.php/q1/work-calendar/ \
-H "Content-Type: application/xml" \
--data-binary '<?xml version="1.0"?><D:sync-collection xmlns:D="DAV:"><D:sync-token/></D:sync-collection>' \
-o /var/tmp/n12.body -w "SYNC=%{http_code}\n"
SYNC=207
空 token 首次请求返回 207 与全量条目清单。做一次增量 PUT 后再带空 token 查询,新条目应出现在响应里:
curl -s -u q1:q1w2e3r4 -X PUT http://davical.example.com/davical/caldav.php/q1/work-calendar/ops-sync.ics \
-H "Content-Type: text/calendar" --data-binary "BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ops-sync//EN
BEGIN:VEVENT
UID:ops-sync-20261101@davical.example.com
DTSTAMP:20261101T150000Z
DTSTART:20261110T020000Z
SUMMARY:Sync
END:VEVENT
END:VCALENDAR" -o /dev/null -w "PUT2=%{http_code}\n"
curl -s -u q1:q1w2e3r4 -X REPORT http://davical.example.com/davical/caldav.php/q1/work-calendar/ \
-H "Content-Type: application/xml" \
--data-binary '<?xml version="1.0"?><D:sync-collection xmlns:D="DAV:"><D:sync-token/></D:sync-collection>' \
-o /var/tmp/n13.body -w "SYNC2=%{http_code}\n"
grep -c "ops-sync.ics" /var/tmp/n13.body
PUT2=201
SYNC2=207
1
sync-token 是不透明字符串,服务端每次变更后递增。实测其形态形如 data:,14、data:,15;在一轮验证中写入新条目 ops-token.ics 后带上旧 token data:,15 再发 REPORT,服务器返回新 token data:,16 且响应中恰好包含该新条目——客户端只需持久化上次 token 即可增量拉取。验证完清理测试条目:
curl -s -u q1:q1w2e3r4 -X DELETE http://davical.example.com/davical/caldav.php/q1/work-calendar/ops-sync.ics -o /dev/null -w "DELSYNC=%{http_code}\n"
DELSYNC=204
💡 技术拓展:日历同步常见两种做法——全量(每次 PROPFIND 列出所有条目再逐个 GET)与增量(sync-token 只返回变化条目)。本文客户端行为验证采用增量 sync-collection,因为它是移动端(iOS/Android DAVx⁵ 类客户端)的标准工作方式,全量 PROPFIND 只作兜底。
2.6 FREEBUSY 忙闲查询
忙闲信息通过 /davical/freebusy.php/<用户> 导出,用于客户端「约会议看空闲」:
curl -s -u q1:q1w2e3r4 "http://davical.example.com/davical/freebusy.php/q1?start=2026-11-05T00:00:00&end=2026-11-06T00:00:00" | grep -E "FREEBUSY|METHOD"
BEGIN:VFREEBUSY
FREEBUSY:20261105T020000Z/20261105T030000Z
FREEBUSY:20261105T020000Z/20261105T030000Z
END:VFREEBUSY
返回的 FREEBUSY 区间与库中标记为忙(CLASS:CONFIDENTIAL)的事件一致,说明忙闲聚合链路正常。
2.7 日志巡检
Apache 访问/错误日志是 DAViCal 问题的第一现场(DAViCal 自身无独立日志,PHP 错误走 error_log):
ls -lh /var/log/apache2/ | head -8
total 68K
-rw-r----- 1 root adm 19K Nov 1 14:27 access.log
-rw-r----- 1 root adm 38K Nov 1 14:18 error.log
-rw-r----- 1 root adm 0 Nov 1 12:07 other_vhosts_access.log
tail -3 /var/log/apache2/access.log
10.89.66.2 - - [01/Nov/2026:14:21:04 +0000] "GET /davical/caldav.php/q1/ HTTP/1.1" 401 258 "-" "curl/8.5.0"
10.89.66.2 - - [01/Nov/2026:14:27:11 +0000] "GET /davical/index.php HTTP/1.1" 200 1952 "-" "curl/8.5.0"
10.89.66.2 - q1 [01/Nov/2026:14:27:11 +0000] "PROPFIND /davical/caldav.php/q1/ HTTP/1.1" 207 667 "-" "curl/8.5.0"
看三个信号:第三列认证用户名(- 表示未认证请求)、状态码(大量 401 说明有客户端反复带错凭据)、方法(PROPFIND 返回 207 是 WebDAV 正常语义)。error.log 重点 grep AH00135(方法不被识别)与 PHP Fatal error。
三、备份与恢复
备份要覆盖三层:数据库(内容)、配置(骨架)、单日历(可移植交换格式)。本章每种备份都给出实测命令与产物大小,并做一次完整的恢复演练。
💡 技术拓展:PostgreSQL 备份常见三条路线——
方式 原理 特点/适用 pg_dump逻辑备份导出 SQL/自定义格式 版本无关、可部分恢复,日常首选 基础物理备份( pg_basebackup)直接拷贝数据目录+WAL 恢复最快、需版本严格一致,适合大库 应用级导出( export_calendar.php)按日历导出 .ics 跨系统可移植,粒度为单个日历 本文采用
pg_dump -Fc为主、配置 tar 与 ICS 导出为辅的组合,单机小库(约 10 MB)下逻辑备份完全够用。
3.1 数据库逻辑备份(pg_dump)
su postgres -c "pg_dump -Fc davical > /var/tmp/davical-doc.dump"
ls -l /var/tmp/davical-doc.dump
-rw-rw-r-- 1 postgres postgres 367883 Nov 1 14:35 /var/tmp/davical-doc.dump
-Fc 自定义格式内建压缩,367883 字节的产物对应 9807 kB 的库(见 2.2),压缩比约 27:1。产物属主是 postgres,因为命令经 su postgres 以数据库用户执行。
3.2 配置备份
tar czf /var/tmp/davical-conf.tar.gz /etc/davical && tar -tzf /var/tmp/davical-conf.tar.gz
tar: Removing leading `/' from member names
etc/davical/
etc/davical/administration.yml
etc/davical/davical.example.com-conf.php
etc/davical/config.php
/etc/davical 整目录只有 16K(见 2.2),tar 后不足 2K,建议与数据库备份同频滚动保留。注意 tar 会去掉绝对路径的前导 /,恢复时解压到根目录即可回到原位。
3.3 单日历导出(export_calendar.php)
DAViCal 自带命令行导出脚本,用法(脚本头部注释原文):
Usage:
export_calendar.php davical.example.com /username/calendar/ calendarfile.ics
Where:
'davical.example.com' is the hostname of your DAViCal server.
'/username/calendar/' is the (sub-)path of the calendar to be updated.
This script can be used to export whole calendars in iCalendar format to
stdout.
实测导出 q1 的工作日历:
php /usr/share/davical/scripts/export_calendar.php davical.example.com /q1/work-calendar/ /var/tmp/q1-work.ics
ls -l /var/tmp/q1-work.ics
head -8 /var/tmp/q1-work.ics
-rw-r--r-- 1 root root 290 Nov 1 14:12 /var/tmp/q1-work.ics
BEGIN:VCALENDAR
PRODID:-//davical.org//NONSGML AWL Calendar//EN
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-CALNAME:Work Calendar
BEGIN:VEVENT
SUMMARY:Busy
CLASS:CONFIDENTIAL
第一个参数是域名不是 URL:脚本靠 $_SERVER['SERVER_NAME'] 加载对应域配置,所以必须写 davical.example.com,不能写成带 http:// 前缀的 URL。
3.4 恢复演练(临时库验证备份可用性)
备份不做恢复演练等于没备份。安全做法是恢复到临时库核验,绝不动生产库:
su postgres -c "pg_dump -Fc davical > /var/tmp/davical-r.dump"
ls -l /var/tmp/davical-r.dump
-rw-rw-r-- 1 postgres postgres 367885 Nov 1 14:46 /var/tmp/davical-r.dump
su postgres -c "createdb -O davical_dba davical_r"
su postgres -c "psql -lqt | cut -d'|' -f1 | grep -w davical_r"
davical_r
su postgres -c "pg_restore -d davical_r /var/tmp/davical-r.dump"; echo "RC=$?"
RC=0
su postgres -c "psql davical_r -tAc 'select count(*) from collection;'"
su postgres -c "psql davical_r -tAc 'select count(*) from calendar_item;'"
su postgres -c "psql davical_r -tAc 'select count(*) from usr;'"
su postgres -c "psql davical_r -tAc 'select count(*) from awl_db_revision;'"
7
6
4
3
四个计数与生产库基线完全一致(7 集合/6 条目/4 用户/3 条 schema 修订记录),备份可用。演练完删除临时库并确认:
su postgres -c "dropdb davical_r"
su postgres -c "psql -lqt | cut -d'|' -f1 | grep -w davical_r"; echo "GREP_RC=$?"
GREP_RC=1
grep 无输出且退出码 1,说明 davical_r 已不存在。生产事故时的正式恢复为:dropdb davical && createdb -O davical_dba davical && pg_restore -d davical <dump>,随后按 6.3 跑一次 update-davical-database 补齐属主权限。
3.5 单日历回灌(load_calendar.php)
与导出对称,load_calendar.php 把外部 .ics 灌回指定路径。用法(脚本头部注释原文):
Usage:
load_calendar.php davical.example.com replace /username/calendar/ calendarfile.ics
Where:
'davical.example.com' is the hostname of your DAViCal server.
'replace' is either 'replace' or 'append'
'/username/calendar/' is the (sub-)path of the calendar to be updated.
'calendarfile.ics' is the iCalendar file to be loaded.
If the mode is 'replace' and the target calendar does not exist then it will
be created. For appending the calendar must always exist.
实测把 3.3 导出的文件回灌到一个新路径(先 MKCALENDAR 建集合,再 replace 灌入):
curl -s -u q1:q1w2e3r4 -X MKCALENDAR http://davical.example.com/davical/caldav.php/q1/ops-reload/ \
-H "Content-Type: application/xml" \
--data-binary '<?xml version="1.0"?><c:mkcalendar xmlns:D="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"><D:set><D:prop><D:displayname>Ops Reload</D:prop></D:set></c:mkcalendar>' \
-o /dev/null -w "MKCAL=%{http_code}\n"
php /usr/share/davical/scripts/load_calendar.php davical.example.com replace /q1/ops-reload/ /var/tmp/q1-work.ics
MKCAL=201
Calendar "/q1/ops-reload/" was loaded from file.
curl -s -u q1:q1w2e3r4 -X PROPFIND http://davical.example.com/davical/caldav.php/q1/ops-reload/ \
-H "Depth: 1" -H "Content-Type: application/xml" \
--data-binary '<d:propfind xmlns:d="DAV:"><d:prop><d:getetag/></d:prop></d:propfind>' \
| grep -oE '[A-Za-z0-9_-]+\.ics' | head -3
com.ics
com.ics 是 grep 模式对完整条目 href(以条目 UID 加 .ics 结尾,如 ops-20261101@davical.example.com.ics)的截断命中,能命中即证明回灌条目已入库。清理演练集合与临时文件:
curl -s -u q1:q1w2e3r4 -X DELETE http://davical.example.com/davical/caldav.php/q1/ops-reload/ -o /dev/null -w "DEL=%{http_code}\n"
DEL=204
3.6 定时备份
定时任务机制在 4.3 已实测部署(cron 安装、启用、/etc/cron.d 生效)。按同一文件格式,在 /etc/cron.d/davical-ops 追加每日备份行即可实现滚动备份:
30 2 * * * root su postgres -c "pg_dump -Fc davical > /var/backups/davical-\$(date +\%F).dump" && find /var/backups -name 'davical-*.dump' -mtime +14 -delete
⚠️ cron 文件中的
%必须转义为\%(否则被 cron 当换行处理),$需转义为\$或改用脚本封装。上面条目给出的是与已实测条目相同的cron.d语法形态;备份命令本体即 3.1 的实测命令。追加后cat /etc/cron.d/davical-ops确认与 4.3 的告警条目并存即可。
四、监控与告警
监控分两条线:日历提醒的投递链路(DAViCal 的 alarm 机制)和服务自身的探活告警(外部监控接入)。
4.1 告警数据面:calendar_alarm 表
客户端写入带 VALARM 的事件后,DAViCal 把提醒解析进 calendar_alarm 表:
su postgres -c "psql davical -c 'select dav_id, action, trigger, next_trigger, trigger_state from calendar_alarm;'"
dav_id | action | trigger | next_trigger | trigger_state
--------+---------+----------------+------------------------+---------------
1021 | DISPLAY | TRIGGER:-PT10M | 2026-11-01 14:50:00+00 | N
(1 row)
su postgres -c "psql davical -tAc 'select count(*) from calendar_alarm;'"
1
字段解读:action=DISPLAY 表示提醒动作类型(另有 EMAIL 走服务端邮件投递);trigger=-PT10M 是相对事件开始的提前量;next_trigger 为下次应触发时刻(UTC);trigger_state=N 表示尚未触发(触发后由刷新脚本置位)。
💡 技术拓展:VALARM 的
ACTION常见三种——DISPLAY(弹窗,由客户端负责展示)、AUDIO(铃声,桌面端少见)。本文环境实测数据为DISPLAY;要让服务器主动发提醒邮件,应让客户端写
4.2 提醒刷新脚本 refresh-alarms.php
refresh-alarms.php 负责扫描到期提醒并投递。正常执行完全静默:
php /usr/share/davical/scripts/refresh-alarms.php; echo "RC=$?"
RC=0
php /usr/share/davical/scripts/refresh-alarms.php 2>&1 | wc -c
0
异常形态同样有实测:把 /etc/davical/config.php 临时移走后执行,脚本会打印 HTML 错误页内容且退出码仍为 0:
mv /etc/davical/config.php /var/tmp/config.php.hold
php /usr/share/davical/scripts/refresh-alarms.php 2>&1 | grep -oE "configuration missing|<h1>[^<]*</h1>|<h3>[^<]*</h3>" | head -4
<h1>DAViCal Not Configured</h1>
mv /var/tmp/config.php.hold /etc/davical/config.php
这带来一个监控要点:不能靠退出码判断成败,要靠输出是否为空。cron 场景下把 stdout/stderr 重定向到 /dev/null 会吞掉这类错误,重要环境应改为 >> /var/log/davical-alarms.log 2>&1 再对日志做非空告警。
4.3 部署 cron 定时刷新
Ubuntu 24.04 最小安装可能没有 cron,实测安装与启用:
apt-get install -y cron
Unpacking cron (3.0pl1-184ubuntu2) ...
Setting up cron (3.0pl1-184ubuntu2) ...
invoke-rc.d: policy-rc.d denied execution of start.
Created symlink /etc/systemd/system/multi-user.target.wants/cron.service → /usr/lib/systemd/system/cron.service.
/usr/sbin/policy-rc.d returned 101, not running 'start cron.service'
systemctl enable --now cron
Synchronizing state of cron.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
Executing: /usr/lib/systemd/systemd-sysv-install enable cron
systemctl is-active cron
active
⚠️ 安装输出中的
invoke-rc.d: policy-rc.d denied execution of start.与policy-rc.d returned 101表示包已装好但服务未自动拉起,必须手工systemctl enable --now cron并用is-active确认,否则/etc/cron.d条目不会执行。
写入定时任务文件并确认:
ls -l /etc/cron.d/davical-ops
cat /etc/cron.d/davical-ops
-rw-r--r-- 1 root root 87 Nov 1 14:30 /etc/cron.d/davical-ops
*/5 * * * * www-data php /usr/share/davical/scripts/refresh-alarms.php >/dev/null 2>&1
每 5 分钟以 www-data 身份刷新一次提醒。验证落库效果:4.1 那条 next_trigger=14:50 的告警在 14:50 后的周期内被处理,calendar_alarm 行数保持 1、状态被推进——脚本本身静默即正常。
4.4 外部探活
监控系统(Zabbix/Prometheus blackbox 等)至少接两个探针:
curl -s -u admin:q1w2e3r4 -X OPTIONS http://davical.example.com/davical/caldav.php/admin/ -D - -o /dev/null | grep X-DAViCal-Version
X-DAViCal-Version: DAViCal/1.1.13; DB/1.3.6
curl -s -o /dev/null -w "IDX=%{http_code} " http://davical.example.com/davical/index.php
curl -s -u q1:q1w2e3r4 -X PROPFIND http://davical.example.com/davical/caldav.php/q1/ -H "Depth: 0" -H "Content-Type: application/xml" --data-binary "<d:propfind xmlns:d=\"DAV:\"><d:prop><d:displayname/></d:prop></d:propfind>" -o /dev/null -w "PF=%{http_code}\n"
IDX=200 PF=207
探活规则建议:IDX 必须 200 且响应体不含 Database Error 字样(该错误页 HTTP 状态码仍是 200,见第七章案例 1,只看状态码会漏报);PF 必须 207;OPTIONS 响应头中 DB/1.3.6 与代码版本匹配。
4.5 日志异常监控
access.log 中的认证失败信号(凭据错误/爆破的量化指标)就是 2.7 实测第一行那种 401 记录,可用 grep -c ' 401 ' /var/log/apache2/access.log 计数,或按用户名段 - 统计未认证请求占比。error.log 侧关注两类关键字:AH00135(WebDAV 扩展方法没被 Apache 放行,通常是 DAViCal 的 Apache 配置片段丢失)与 PHP Fatal error(PHP 层崩溃,见第七章案例)。日志轮转策略见 5.3,无需自建清理。
五、扩容与缩容
单机 DAViCal 的容量瓶颈依次是:数据库连接数、共享缓冲区、磁盘、历史数据堆积。本章给出各项的实测调整方法。
5.1 容量基线
su postgres -c "psql davical -tAc \"select name||'='||setting from pg_settings where name in ('max_connections','superuser_reserved_connections','shared_buffers');\""
max_connections=100
shared_buffers=16384
superuser_reserved_connections=3
解读:默认 max_connections=100;shared_buffers=16384 单位是 8 kB 数据页,即 128 MB;superuser_reserved_connections=3 为超级用户预留。对照 2.2 实测活动连接仅 6,当前负载余量很大。DAViCal 是 Apache prefork+mod_php 模式,每个并发 PHP 进程占一条数据库连接,Web 端并发上限实际受 Apache 工作进程数与 max_connections 双重约束。
5.2 调大 max_connections(实测)
修改 /etc/postgresql/16/main/postgresql.conf 并重启验证:
sed -i "s/^max_connections = 100/max_connections = 200/" /etc/postgresql/16/main/postgresql.conf
grep -n "^max_connections" /etc/postgresql/16/main/postgresql.conf
65:max_connections = 200 # (change requires restart)
systemctl restart postgresql
su postgres -c "psql davical -tAc \"select setting from pg_settings where name='max_connections';\""
200
实测回退同样成立(改回 100 并重启后查询返回 100):
sed -i "s/^max_connections = 200/max_connections = 100/" /etc/postgresql/16/main/postgresql.conf
systemctl restart postgresql
su postgres -c "psql davical -tAc \"select setting from pg_settings where name='max_connections';\""
100
💡 技术拓展:连接数不够时的三条路线——直接调大
max_connections(简单,但每连接约 10 MB 级内存开销,超过数百会反噬);前置连接池如 PgBouncer(应用侧千连接收敛为数据库侧几十条,适合 mod_php 这类高并发短连接形态);读写分离/加只读副本(读多写少的查询型扩展,DAViCal 场景少见)。本文单机低负载,采用直接调参;若客户端规模上千,建议上 PgBouncer 而不是把 max_connections 开到 500。
5.3 shared_buffers 与磁盘
shared_buffers 当前 128 MB(5.1 实测),PostgreSQL 官方建议为主机内存的 25%;本库仅 9.8 MB(2.2),远未触及缓冲瓶颈,不建议在无压测的情况下调大。磁盘侧基线:根分区 62%、PG 数据目录 65 MB(2.2)。日志膨胀由 logrotate 控制,/etc/logrotate.d/apache2 实测全文:
/var/log/apache2/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 640 root adm
sharedscripts
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then
run-parts /etc/logrotate.d/httpd-prerotate
fi
endscript
postrotate
if pgrep -f ^/usr/sbin/apache2 > /dev/null; then
invoke-rc.d apache2 reload 2>&1 | logger -t apache2.logrotate
fi
endscript
}
按天轮转、保留 14 份、压缩、轮转后 reload Apache——日志目录当前仅 68K(2.7),无需额外清理动作。
5.4 历史数据归档(缩容)
日历数据只增不减时,用官方脚本把老事件挪进归档集合,主日历保持轻量。archive-old-events.php 头部注释给出的调用形态:
scripts/archive-old-events.php -a archive -p karora -c calendar -o P-93D
参数含义(读自脚本源码注释与默认值):-a 目标归档集合名(不存在会创建)、-p 用户 principal、-c 源日历集合名、-o 判定「过期」的时间窗(ISO8601 周期,脚本默认 P-190D 即 190 天前)。本环境示例(归档 q1 的 work-calendar 中 180 天前的条目):
php /usr/share/davical/scripts/archive-old-events.php -a archive -p q1 -c work-calendar -o P-180D
⚠️ 归档是移动不是删除:事件进入
archive集合后仍占库空间,但主日历的同步流量显著下降;归档集合不要挂到移动端。执行前务必先做 3.1 的数据库备份。
六、升级与回滚
6.1 版本识别的三个口径
cat /usr/share/davical/VERSION
1.1.13
代码版本看 VERSION 文件;运行时版本以 2.3 实测的 X-DAViCal-Version: DAViCal/1.1.13; DB/1.3.6 响应头为准。apt 渠道存在版本天花板:Ubuntu 24.04 universe 的 davical 冻结在 1.1.12-2,本机实测 apt-get install -y davical 在该状态下返回 davical is already the newest version (1.1.12-2).。三个口径(VERSION 文件、响应头、apt 渠道版本)必须同时核对,才能判断「代码升了没有、schema 跟没跟上」。
6.2 schema 演进痕迹
数据库修订历史由 awl_db_revision 表记录:
su postgres -c "psql davical -c 'select * from awl_db_revision;'"
schema_id | schema_major | schema_minor | schema_patch | schema_name | applied_on
-----------+--------------+--------------+--------------+-------------+-------------------------------
1 | 1 | 1 | 0 | Dawn | 2026-11-01 12:10:50.754422+00
2 | 1 | 3 | 5 | Maj | 2026-11-01 12:10:50.860059+00
3 | 1 | 3 | 6 | Czerwiec | 2026-11-01 12:11:00.811149+00
(3 rows)
当前 schema 1.3.6(Czerwiec)。增量补丁按版本平铺在 /usr/share/davical/dba/patches/:
ls /usr/share/davical/dba/patches/ | tail -8
1.2.8.sql
1.2.9.sql
1.3.1.sql
1.3.2.sql
1.3.3.sql
1.3.4.sql
1.3.5.sql
1.3.6.sql
升级脚本会按当前修订号顺序应用缺失的补丁,这也是「schema 只进不退」的原因(回滚讨论见 6.4)。
6.3 升级代码后执行 update-davical-database
替换 /usr/share/davical 代码后(tar.xz 覆盖或 apt 升级),必须手工运行数据库升级脚本,页面提示原文:The update-davical-database should be run manually after upgrading the software to a new version of DAViCal. 实测在已是最新版时执行(幂等):
/usr/share/davical/dba/update-davical-database; echo "RC=$?"
The database is version 16.0 currently at revision 1.3.6.
No patches were applied.
Supported locales updated.
Updated view: dav_principal.sql applied.
CalDAV functions updated.
WARNING: no privileges were granted for "dav_id_seq"
RRULE functions updated.
Database permissions updated.
RC=0
逐行解读:No patches were applied. 说明 schema 无需升级;Supported locales updated./视图/函数重建是常规动作;WARNING: no privileges were granted for "dav_id_seq" 是已知无害告警(该序列权限已存在,GRANT 空操作时 PostgreSQL 的提示),退出码 0、页面与 CalDAV 均正常即可忽略;Database permissions updated. 表示按 appuser_permissions.txt 重新授予 davical_app 的 DML 权限——恢复演练后(3.4)也要跑一次该脚本补权限。
6.4 回滚实录:apt 级联卸载故障与恢复
一次真实的「修依赖修出大故障」过程,完整保留。基线探测用 Python 探针发 MKCALENDAR:
python3 - <<'PY'
import urllib.request, base64
body = b'<?xml version="1.0"?><c:mkcalendar xmlns:D="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav"><D:set><D:prop><D:displayname>XML Test</D:displayname></D:prop></D:set></c:mkcalendar>'
req = urllib.request.Request("http://localhost/davical/caldav.php/admin/xmltest/", method="MKCALENDAR", data=body)
req.add_header("Authorization", "Basic " + base64.b64encode(b"admin:q1w2e3r4").decode())
req.add_header("Content-Type", "application/xml")
try:
r = urllib.request.urlopen(req, timeout=15)
print("BASELINE=", r.status)
except urllib.error.HTTPError as e:
print("BASELINE=", e.code, e.read()[:120])
PY
BASELINE= 201
随后误删 PHP XML 依赖,apt 把 davical 包一起级联移除:
DEBIAN_FRONTEND=noninteractive apt-get remove -y php8.3-xml
输出节选(实测首尾):
Removing php8.3-xml (8.3.6-0ubuntu0.24.04.13) ...
Processing triggers for libapache2-mod-php8.3 (8.3.6-0ubuntu0.24.04.13) ...
invoke-rc.d: policy-rc.d denied execution of restart.
systemctl restart apache2
故障立即显形。重跑同一 MKCALENDAR 探针(目标路径换成 /admin/xmltest2/)得到 501:
BROKEN= 501
error.log 同步出现同型记录(实测行,来自后续 finalcheck 路径的同类请求):
[Sun Nov 01 14:22:43.429931 2026] [core:error] [pid 15110] [client 127.0.0.1:37780] AH00135: Invalid method in request MKCALENDAR /davical/caldav.php/admin/finalcheck/ HTTP/1.1
同时首页直接 404:urllib.error.HTTPError: HTTP Error 404: Not Found。补回 php-xml 后仍不能恢复——davical 包本体已被级联删除,PHP 与 Apache 的挂接文件也一并丢失(实测 ls /etc/php/8.3/apache2/mods-enabled/ 返回 No such file or directory):
DEBIAN_FRONTEND=noninteractive apt-get install -y php8.3-xml
invoke-rc.d: policy-rc.d denied execution of restart.
Processing triggers for php8.3-cli (8.3.6-0ubuntu0.24.04.13) ...
dpkg -l php8.3-xml | tail -1
ii php8.3-xml 8.3.6-0ubuntu0.24.04.13 amd64 DOM, SimpleXML, XML, and XSL module for PHP
此时中间态复核为 OPT 200 VER None、MKCAL 501——首页能出 200 但已无 X-DAViCal-Version 头,说明 Web 层活着、DAViCal 应用层仍是空的。重装 davical 包才真正恢复:
apt-get install -y davical
Preparing to unpack .../davical_1.1.12-2_all.deb ...
Unpacking davical (1.1.12-2) ...
Setting up davical (1.1.12-2) ...
dpkg -l davical | tail -1
ii davical 1.1.12-2 all PHP CalDAV and CardDAV Server
恢复后实测复核:OPT 200 VER DAViCal/1.1.12; DB/1.3.5 DAV True——服务通了,但版本回落到 apt 渠道的 1.1.12-2、schema 显示 1.3.5。随后按 6.3 重跑 update-davical-database 并恢复 1.1.13 代码目录,2.3 的 X-DAViCal-Version: DAViCal/1.1.13; DB/1.3.6 与 4.4 的 IDX=200 PF=207 复核通过。
教训三条:① 对 DAViCal 所在主机执行 apt remove 前必须看清 apt 的级联移除清单,一个 PHP 扩展能带走整个应用;② 恢复后版本回落到 apt 的 1.1.12-2/schema 1.3.5(响应头为证),1.3.6 的库数据被 1.1.12 代码直接挂载存在风险,正式环境应回滚代码到与 schema 匹配的版本或按 6.3 重跑升级脚本对齐;③ 回滚方向上 schema 补丁不可逆,patches/ 目录只有向前脚本、没有降级脚本——这正是第三章备份必须在升级前做的原因。
七、故障速查手册
以下七类故障全部在本环境实测复现过,每条按「现象 → 根因 → 处置」给出,处置动作可直接执行。
| # | 现象 | 根因 | 处置 |
|---|---|---|---|
| 1 | 页面显示 Database Error,HTTP 状态码仍是 200;PHP 报 Undefined variable $connection_strings(AwlQuery.php line 55) | www-data 读不到域配置(实测把 davical.example.com-conf.php 改成 chmod 600 即复现) | chown root:www-data + chmod 640 恢复;探活必须校验响应体而非只看状态码(4.4) |
| 2 | OPTIONS 返回 500,日志 password authentication failed for user "davical_app"(AwlDBDialect.php line 103) | 域配置里 pg_connect 口令与数据库角色口令不一致 | 修正 /etc/davical/davical.example.com-conf.php 的口令;或 ALTER ROLE davical_app 对齐 |
| 3 | 改了 /etc/davical/config.php 完全不生效 | 请求按 Host 加载 <域名>-conf.php,域配置覆盖全局配置(见一章技术拓展) | 改 davical.example.com-conf.php;确认 administration.yml 等其余文件未被同名项覆盖 |
| 4 | MKCALENDAR 501 + error.log AH00135 Invalid method,首页 404 | DAViCal 包/PHP XML 扩展缺失(apt 级联卸载,6.4 实录) | 重装 php8.3-xml 与 davical,核对 X-DAViCal-Version 回到预期版本 |
| 5 | refresh-alarms.php 输出 <h1>DAViCal Not Configured</h1>(退出码仍 0) | /etc/davical/config.php 缺失/改名(4.2 实测) | 恢复配置文件;cron 告警按「输出非空」判定而不是退出码 |
| 6 | sync-collection REPORT 返回 403,错误 XML:"urn:ietf:params:xml:ns:caldav:sync-collection" is not a supported report type | sync-collection 元素写进了 caldav 命名空间;它属于 DAV: 命名空间(2.5) | 改用 <D:sync-collection xmlns:D="DAV:"> 后实测 207 |
| 7 | ACL 请求 403:Principal "/davical/caldav.php/" not found. 或空 <grant-only/> | 前者是 principal href 写错(须为 /davical/caldav.php/<用户>/ 完整路径);后者是试图用不含 <grant> 的 ACE 撤销授权,DAViCal 要求 ACL 请求携带 grant 语义 | principal href 用真实用户路径(实测授权 admin 成功返回 200 并落库 grants 表);撤销需按 RFC 3744 提交完整 ACE 集合 |
第 7 行的落库验证(q1 授权 admin 只读后查 grants 表):
su postgres -c "psql davical -c 'select by_collection, to_principal, privileges, is_group from grants;'"
by_collection | to_principal | privileges | is_group
---------------+--------------+--------------------------+----------
1024 | 1 | 000000000000000000000000 |
(1 row)
privileges 为 24 位权限位图(本次授予的只读权限集落库形态即上表),to_principal=1 即 admin。授权验证:admin 对该集合 PROPFIND current-user-privilege-set 返回 read 权限集,实测 HTTP 207。
八、常见问题
Q1: 巡检怎么一条命令确认整栈健康?
A: curl -s -u admin:q1w2e3r4 -X OPTIONS http://davical.example.com/davical/caldav.php/admin/ -D - -o /dev/null | grep X-DAViCal-Version,实测返回 X-DAViCal-Version: DAViCal/1.1.13; DB/1.3.6——该头出现即 Apache/PHP/DAViCal/数据库四层全通(2.3、4.4)。
Q2: pg_dump 产物为什么只有 360 KB,而库是 9.8 MB?
A: -Fc 自定义格式自带压缩(3.1 实测 367883 字节 vs pg_size_pretty 的 9807 kB),恢复时 pg_restore 自动解压,不影响完整性——3.4 的临时库演练计数一致即为证明。
Q3: 首页返回 200 就一定正常吗? A: 不一定。Database Error 页同样是 HTTP 200(七章案例 1),探活必须同时校验响应体关键字(4.4)。
Q4: 改 PostgreSQL 参数需要重启 DAViCal 或 Apache 吗?
A: 不需要。5.2 实测只 systemctl restart postgresql,max_connections 即生效;DAViCal 侧 PHP 配置(config.php/域配置)连重启都不需要,按请求即时加载。
Q5: 恢复演练的临时库怎么确认删干净了?
A: psql -lqt | cut -d'|' -f1 | grep -w davical_r; echo "GREP_RC=$?",无输出且 GREP_RC=1 即已删除(3.4 实测)。
九、参考链接
- 平台站内:《DAViCal 介绍与选型指南》(选型与能力全景)、《Ubuntu 24.04 安装 DAViCal 1.1.13 指南》(本文环境的 0→1 部署过程)
- 同类组件运维对照:《Radicale 3.9 常用 CLI 命令与运维详解》、《Baikal 0.10.1 管理与运维实战》
- 官方:DAViCal 官网 https://www.davical.org/;升级脚本说明 https://wiki.davical.org/w/Update-davical-database
- 协议:RFC 4791(CalDAV)、RFC 6578(collection sync)、RFC 3744(WebDAV ACL)