PmaControl logo PmaControl
  • 首页
  • PmaControl
    • AI智能代理 13个本地代理
    • 定价方案 Community、Cloud、On-Premise、Premium
    • 文档 指南、API、架构
    • 客户 28+企业
    • 常见问题 25个问题 / 7个类别
    数据库
    • MariaDB 30 篇文章
    • MySQL 10 篇文章
    • Galera Cluster 6 篇文章
    • MaxScale 3 篇文章
    • ProxySQL 2 篇文章
    • Amazon Aurora MySQL 0 篇文章
    • Azure Database 0 篇文章
    • ClickHouse 0 篇文章
    • GCP CloudSQL 0 篇文章
    • Percona Server 0 篇文章
    • SingleStore 0 篇文章
    • TiDB 0 篇文章
    • Vitess 0 篇文章
    解决方案
    • 全天候支持 MariaDB & MySQL紧急支持
    • Observabilité SQL 监控、告警、拓扑
    • Haute disponibilité 复制、故障转移、Galera
    • Disaster Recovery 备份、恢复、RPO/RTO
    • Sécurité & conformité 审计、GDPR、SOC2
    • Migration & upgrade 零停机、pt-osc、gh-ost
  • 定价方案
  • 资源
    • 文档 技术指南与API
    • 常见问题 25个常见问题
    • 客户评价 客户反馈与案例
    • 博客 文章与洞察
    • 路线图 即将推出的功能
    专业领域
    • Observabilité SQL 监控、告警、Dot3拓扑
    • Haute disponibilité 复制、故障转移、Galera
    • Sécurité & conformité 审计、GDPR、SOC2、ISO 27001
    • Disaster Recovery 备份、恢复、RPO/RTO
    • Performance & optimisation Digests、EXPLAIN、调优
    • Migration & upgrade 零停机、pt-osc
    快速链接
    • GitHub Wiki 26页 — 安装、引擎、插件
    • 源代码 GitHub官方仓库
    • 全天候支持 MariaDB & MySQL紧急支持
    • 预约演示 30分钟 — 真实架构
  • 全天候支持
  • 预约演示
预约演示
🇫🇷 FR Français 🇬🇧 EN English 🇵🇱 PL Polski 🇷🇺 RU Русский 🇨🇳 ZH 中文
← 返回博客

共享锁与中枢文件:PmaControl 中的锁竞争问题

发布于 2026年3月19日 作者 Aurélien LEQUOY
pmacontrol php concurrency performance architecture
分享 X LinkedIn Facebook Email PDF
共享锁与中枢文件:PmaControl 中的锁竞争问题

PHP 中的共享内存问题

PmaControl 是一款用 PHP 编写的监控工具。它的守护进程从数十甚至数百个 MariaDB / MySQL 实例中采集指标,然后将结果存储供 Web 仪表盘使用。

PHP 在进程之间没有原生的共享内存机制(不同于 Go 的 goroutine 或 Java 的线程)。每个 PHP worker 都是一个独立的进程。为了在采集守护进程和 Web 服务器之间共享数据,PmaControl 使用了中枢文件(pivot files)——一种通过文件系统自行实现的共享内存方案。

StorageFile 类(灵感来自 SharedMemory 模式)将数据序列化为 JSON 并写入磁盘文件。并发访问通过 flock() 配合 LOCK_EX(排他锁)来管理。

LOCK_EX 工作正常

我们首先验证的问题是:flock() 配合 LOCK_EX 是否真正保证了互斥? 是否存在静默覆写数据的风险?

答案很明确:是的,LOCK_EX 工作正常。Linux 内核保证在任意时刻只有一个进程能持有某个文件的 LOCK_EX。其他进程会等待(阻塞),直到锁被释放。

我们通过压力测试进行了验证:

// Test de concurrence sur flock()
// Lancé avec 50 processus simultanés
$fp = fopen('/tmp/pivot_test.json', 'c+');
if (flock($fp, LOCK_EX)) {
    $data = json_decode(fread($fp, filesize('/tmp/pivot_test.json')), true);
    $data['counter'] = ($data['counter'] ?? 0) + 1;
    ftruncate($fp, 0);
    rewind($fp);
    fwrite($fp, json_encode($data));
    flock($fp, LOCK_UN);
}
fclose($fp);

在 50 个并发进程下运行 10,000 次迭代后,计数器的值恰好为 500,000。没有丢失任何写入,没有静默覆写。

问题不在于正确性,而在于锁竞争。

瓶颈所在

PmaControl 使用中枢文件来存储被监控服务器的实时状态。目录结构如下:

/var/lib/pmacontrol/pivot/
  server_status.json      ← 所有服务器的状态
  server_42_metrics.json  ← 服务器 42 的详细指标
  server_43_metrics.json
  ...

问题出在 server_status.json 文件上。每个守护进程 worker 在采集完某台服务器的指标后,都会用新状态更新这个中心文件。操作流程:

  1. 获取 server_status.json 的 LOCK_EX
  2. 读取完整内容(包含所有服务器的 JSON)
  3. 修改对应服务器的条目
  4. 重写整个文件
  5. 释放锁

在 10 台服务器、采集间隔 10 秒的情况下,这没有问题。但到了 100 台服务器时,worker 们开始互相阻塞,等待获取锁。

锁竞争测量

我们对 StorageFile 进行了埋点,以测量 flock() 的等待时间:

$start = microtime(true);
flock($fp, LOCK_EX);
$wait = microtime(true) - $start;

不同服务器数量下的测量结果:

服务器数量 flock() 平均等待时间 P99
10 0.2 ms 1.1 ms
50 4.8 ms 28 ms
100 18 ms 142 ms
200 67 ms 480 ms
500 312 ms 1.8 s

超过 100 台服务器后,P99 超过 100ms。在 500 台服务器时,某些 worker 需要等待近 2 秒才能写入状态——而采集间隔只有 10 秒。这意味着 20% 的时间预算花在了等待锁上。

文件为何不断增大

server_status.json 文件包含所有服务器的状态。100 台服务器时约 200 KB,500 台服务器时约 1 MB。

每次更新:

  1. 读取 1 MB 的 JSON
  2. 解析 1 MB 为 PHP 数据结构
  3. 修改 2 KB(仅一台服务器)
  4. 序列化 1 MB 为 JSON
  5. 写入 1 MB 到磁盘

这个比率荒谬至极:2 KB 的有效数据却产生 4 MB 的 I/O。

解决方案:按 server_id 分片

建议是按 server_id 对中枢文件进行分片:

/var/lib/pmacontrol/pivot/
  status/
    server_42.json    ← 2 KB,仅一台服务器
    server_43.json
    server_44.json
    ...

每个 worker 只需要锁定自己负责的服务器文件,全局锁竞争不复存在。

测量结果

分片后:

服务器数量 flock() 平均等待时间 P99
100 0.1 ms 0.5 ms
200 0.1 ms 0.6 ms
500 0.2 ms 0.8 ms

锁竞争几乎完全消失。等待时间不再取决于服务器数量,而是取决于采集同一台服务器的 worker 数量(通常为 1)。

代价

仪表盘现在需要读取 N 个文件而不是一个文件来显示全局视图。读取代码从:

// Avant : un seul fichier
$allStatus = json_decode(file_get_contents('pivot/server_status.json'), true);

变为:

// Après : N fichiers
$allStatus = [];
foreach (glob('pivot/status/server_*.json') as $file) {
    $serverId = extractServerId($file);
    $allStatus[$serverId] = json_decode(file_get_contents($file), true);
}

代码量更多了,但读取天然是非阻塞的(得益于通过 rename() 实现的原子写入,读取时不需要 LOCK_EX)。

超越文件系统:Redis 和 memcached

对于大规模部署(超过 500 台服务器),即使使用了分片,文件系统方案也会达到极限:

  • I/O 延迟:每次写入都触及磁盘(除非有 Linux 页面缓存)
  • Inode 压力:500 个中枢文件 = 500 个 inode
  • 没有 TTL:已删除服务器的中枢文件会一直保留,直到手动清理

下一步自然的演进是将 StorageFile 替换为 Redis 或 memcached 后端:

// Interface abstraite
interface StorageBackend {
    public function get(string $key): ?array;
    public function set(string $key, array $data, int $ttl = 0): void;
}

// Implémentation fichier (actuelle)
class StorageFile implements StorageBackend { ... }

// Implémentation Redis (future)
class StorageRedis implements StorageBackend { ... }

Redis 消除了锁竞争问题(服务端原子操作)、TTL 问题(原生过期机制)和性能问题(全内存操作)。

为什么不直接切换到 Redis

PmaControl 的设计理念是安装简单:无外部依赖,只需一台 PHP 服务器,不需要 Redis 或 RabbitMQ。中枢文件方案使得在最小化安装的 Debian 上无需任何前置条件即可部署。

将 Redis 作为强制依赖会破坏这一设计哲学。最终采用的方案是保留 StorageFile 作为默认后端(带分片),同时为大规模部署提供 StorageRedis 作为可选方案。

建议总结

规模 建议 后端
1-50 台服务器 单一中枢文件 StorageFile
50-200 台服务器 按 server_id 分片 StorageFile(分片)
200-500 台服务器 分片 + 高速 SSD StorageFile(分片)
500+ 台服务器 Redis / memcached StorageRedis

结论

flock() 配合 LOCK_EX 工作正常——不会发生静默覆写。但当所有 worker 共享同一个中枢文件时,锁竞争在超过 100 台服务器后就成为真实的问题。

解决方案是按 server_id 分片:每个 worker 锁定自己的文件,消除全局锁竞争。对于超大规模部署,Redis 接替文件系统。

文件系统并非 PHP 共享内存的糟糕选择,只需要清楚它在何时会达到极限。

分享 X LinkedIn Facebook Email PDF
← 返回博客

评论 (0)

暂无评论。

发表评论

PmaControl
+33 6 63 28 27 47 contact@pmacontrol.com
法律声明 GitHub 联系我们
不要等到故障发生才了解您的架构。 © 2014-2026 PmaControl — 68Koncept