第三章:数据库架构设计 —— 三层架构、高可用与灾备

好,我们直接进入正题。数据库怎么搭,其实是个老生常谈的问题。但追溯系统有个特殊之处:数据一旦写入,几乎不允许修改,而且必须随时能查。这就逼着我们在架构上多下功夫。

我个人习惯把整个系统拆成三层:采集层、存储层、应用层。每一层各司其职,哪一层出了问题,都不至于拖垮全局。

3.1 三层架构:各扫门前雪

你想想看,如果让一个数据库既负责接收生产线上每秒上千条的记录,又负责给审计人员跑复杂的追溯查询,那它迟早会崩溃。分开,才是正道。

核心原则:采集层只收数据,存储层只管存管,应用层只提供服务。解耦越彻底,系统越稳定。

3.1.1 采集层(Ingestion Layer)

采集层的任务就一个:把设备日志、DCS(分散控制系统)信号、人工录入等各类数据,可靠地收进来,然后扔给存储层。我在做生物制药项目时就发现,很多老设备还在用串口或OPC DA,采样频率又不固定。采集层必须消化这些异构协议。

  • 协议适配器: OPC UA、Modbus、MQTT、HTTP API,甚至手动CSV导入,都得支持。
  • 数据校验: 时间戳是否合理?数值是否在量程内?重复数据是否要去重?这些事不能在入库后才处理。
  • 缓冲与重试: 一旦存储层暂时不可用,采集层得能本地缓冲,等恢复了再批量推送。我曾经见过一次因为网络抖动,丢失了半个小时的工艺参数,那回真是被GMP审计追着问。
# 采集层日志片段示例
2025-02-18 10:23:45 [INFO] OPC-UA订阅: 节点 ns=2;s=TEMP_REACTOR_01 值=98.7
2025-02-18 10:23:45 [INFO] 写入Kafka topic: raw_traceability_data 成功
2025-02-18 10:23:46 [WARN] 存储层响应超时,启动本地缓冲(队列大小=1024)

3.1.2 存储层(Storage Layer)

存储层是整个架构的心脏。GMP要求数据完整性,说白了就是不能丢、不能改、随时能查到原始状态。我个人偏爱用时序数据库+对象存储的组合。

数据类别存储引擎说明
工艺参数(高频)时序数据库(TimescaleDB / InfluxDB)高写入吞吐,保留所有原始数据
审计日志(结构化)关系型数据库(PostgreSQL)强事务支持,关联查询灵活
原始文件(批次报告、图谱)对象存储(MinIO / S3兼容)成本低,支持版本控制与不可变存储

避坑提醒: 千万别把原始数据和计算后的数据混在一个表里。我做过一个项目,当初图省事,用同一张表存原始值和修正值,结果审计时根本说不清楚哪个是原始记录。后来只能硬着头皮做数据迁移,差点加班到崩溃。

3.1.3 应用层(Application Layer)

应用层是给审计员、工程师和QA(质量保证)用的。它不碰原始存储,只通过API(应用程序编程接口)读取。这样即使应用层被攻击或误操作,物理数据也不会受损。

应用层的核心是查询加速。原始数据可能分散在时序库、对象存储和关系数据库里,应用层负责把它们拼起来,返回一个完整的追溯视图。

// 应用层API示例(简化)
GET /api/v1/traceability/?product_id=BATCH2024-001
Response:
{
  "batch_id": "BATCH2024-001",
  "steps": [
    {"step": "混合", "time": "2024-12-01 08:00", "temp": 98.7, "operator": "张三"},
    {"step": "灌装", "time": "2024-12-01 09:30", "speed": 120, "operator": "李四"}
  ],
  "files": ["minio://batch_reports/BATCH2024-001.pdf"]
}

3.2 高可用设计:别让单点害了你

为什么高可用?因为GMP生产不能停。设备一直在跑,数据一直在产生。如果数据库挂了,采集层缓存总有满的时候,一旦溢出,数据就真的丢了。我经历过一次存储节点硬盘故障,因为是单节点,恢复花了6小时,那6小时的生产数据全部变成“可疑”,后续只能重新评估放行,成本极高。

高可用设计的几个关键点:

  • 采集层高可用: 至少2个采集节点做负载均衡,一个挂了,另一个自动接管。用Keepalived或Nginx都能实现。
  • 存储层高可用: 时序库和关系库都采用主从复制(Master-Slave)或集群模式。我倾向用同步复制,虽然会牺牲一点性能,但能保证主库挂了,从库数据完全一致,不丢任何一条记录。

我的个人经验: 配置PostgreSQL流复制时,记得设置 synchronous_commit = on。虽然写性能会下降10%-15%,但在GMP追溯场景里,数据不丢比速度快更重要。你可以用采集层缓冲来对冲这个延迟。

# PostgreSQL同步复制配置示例(主库)
synchronous_commit = on
synchronous_standby_names = '*'
# 从库配置
primary_conninfo = 'host=10.0.1.10 port=5432 user=replicator password=xxx application_name=slave1'
  • 应用层高可用: API服务做水平扩展,无状态设计。前面挂个负载均衡器(如HAProxy),后面挂多个应用实例。实例挂了,负载均衡自动剔除。

3.3 冗余与灾备策略:别等火起了再挖井

高可用应对的是小故障——节点宕机、网络中断。灾备应对的是大灾难——机房火灾、勒索病毒、整个集群不可用。

冗余是手段,灾备是策略。说白了,你得问自己一个问题:如果整个主数据中心现在就消失了,我能不能在4小时内恢复生产记录?

3.3.1 数据冗余策略

在存储层内部,我用多副本+纠删码组合。时序数据用3副本,保证写入不丢;历史归档用纠删码(例如 4+2),节省存储成本。

推荐做法: 生产环境至少保持本地3副本,外加一个异地副本(可配置为异步复制)。这样即使主库被勒索,异地副本也能在几小时内恢复。

3.3.2 灾备策略:两地三中心

虽然听起来有点重,但GMP大厂基本都在用。核心思路:

  • 生产中心: 主库集群+本地备份
  • 同城灾备: 与主库延迟<10ms的机房间做同步复制,应对机房级别故障
  • 异地灾备: 跨城市异步复制,应对区域性灾难

我曾经帮一家疫苗企业搭建过这套架构。当时异地灾备走专线,带宽只有200Mbps,为了不阻塞生产写入,我采用了异步批处理复制:每5分钟压缩一次WAL(预写日志)日志,然后传到异地。丢数据的窗口控制在5分钟以内,GMP审计完全接受。

# 异地灾备的pg_dump定时任务(crontab示例)
# 每半小时做一次增量备份到异地NFS
*/30 * * * * pg_basebackup -D /backup/remote --wal-method=stream -h localhost -U backup_user
# 同步完成后,rsync到灾备中心
*/31 * * * * rsync -avz /backup/remote/ backup@dr-site:/data/pg_archive/

3.3.3 灾难恢复演练(DR Drill)

画个架构图很容易,真到演习的时候才发现问题。我建议每半年做一次真实的灾备切换,不能只是纸面演练。我们之前一次演习,发现异地灾备库的序列(Sequence)没有同步,导致恢复后的批次号跟之前重复,差点酿成质量事故。

重点提醒: 灾备环境应该跟生产环境版本完全一致——包括数据库版本、操作系统补丁、插件版本。我见过有人只同步数据不同步插件,结果灾备恢复后,某个自定义聚合函数跑不了,修复花了一整天。

3.4 架构总览:一张图说清楚

下面这张SVG图展示了我们这一章讨论的三层架构,以及高可用、冗余、灾备如何贯穿其中。你可以把它贴在项目白板上,方便跟团队对齐。

PDB溯源数据库:三层架构与高可用/灾备 采集层(Ingestion Layer) OPC UA 采集器 MQTT 桥接器 HTTP API 网关 CSV/手动录入 本地缓冲队列 + 故障重试 存储层(Storage Layer)— 高可用集群 时序库(主) 同步复制 关系库(主) 同步复制 对象存储 3副本 + 纠删码 本地3副本 ⬅ 同城灾备(同步) ⬅ 异地灾备(异步) 应用层(Application Layer) 追溯查询API 审计报告服务 可视化仪表盘 无状态水平扩展 高可用:主从同步 + 负载均衡 灾备:同城同步+异地异步

从这张图能看清数据流动的路径:采集层接收各类源数据 → 存储层负责安全保管并保证高可用 → 应用层提供查询和审计服务。同时,横向的冗余与灾备策略贯穿整个存储层,确保数据在任何灾难下都能恢复。

其实搭建三层架构最难的,不是技术选型,而是坚持分层。项目忙起来的时候,团队很容易想着“先入库再说,后面再分”。一旦这么做了,后续的查询优化、灾备恢复都会变得一团糟。嗯,这是我踩过的最深的坑之一。

这一章的内容到这里。记住,架构是上层设计,真正考验人的是落地时的每一个决策——用同步还是异步?副本设几份?灾备多久演练一次?答案都在你的业务场景里。