如何在香港服务器的 Ubuntu 中部署医疗影像系统:把高速 SSD 和容器化“捆”到一起,做出低延迟的 PACS

香港机房凌晨 1 点,我蹲在 42U 机柜前,耳朵边是 NVMe 指示灯一闪一闪的微光。白天放射科吐槽“看片卡”,检索一份胸部 CT 居然要等到咖啡凉——这在急诊是不允许的。那天我接了需求:在香港的物理服务器上,用 Ubuntu + 高速 SSD + 容器化,把 PACS/DICOM 影像系统的访问时延压下去。
我给自己立了三条红线:
- 数据落盘必须安全可恢复;
- 查询/取片(C-FIND/C-MOVE/C-GET)延迟必须秒级;
- 运维能让新同事按 SOP 重做一遍,不靠玄学。
以下就是我一步步落地的全过程——硬件选型、文件系统、容器编排、网络优化、监控告警、以及我在机房里踩过的坑。
1. 目标与架构总览
目标:在单台或小规模多台香港机房的裸金属服务器上,部署一套低延迟、可水平扩展的医疗影像系统(以 Orthanc 或 dcm4chee 为核心),通过 NVMe SSD + 合理的文件系统与缓存策略 提升 IOPS 与尾延迟,同时用 Docker Compose(或后续迁移到 K8s)做容器化与弹性扩展。
高层架构:
- 入口:Nginx(HTTPS/TLS、反向代理、限速/并发保护)
- 应用:Orthanc(轻量)或 dcm4chee(重型,全功能 PACS)
- 数据层:PostgreSQL(元数据)+ 影像对象存储(XFS/ZFS 上的卷)
- 存储:本地 NVMe SSD(RAID10)+ 定期快照/备份(rsync/Restic 到异地)
- 监控:Prometheus + node_exporter + cAdvisor + Loki(日志)
- 安全:WireGuard/VPN、最小暴露面、TLS、审计与脱敏(非生产示例环境)
2. 现场硬件与网络:我选了什么,为什么
2.1 机型与硬件参数(实配示例)
| 组件 | 型号/数量 | 关键参数/说明 |
|---|---|---|
| 服务器 | 1× 2U Bare Metal | 双路 Intel Xeon Silver/Gold 或 AMD EPYC 7302P |
| 内存 | 128–256 GB DDR4/DDR5 | 选 ECC;元数据缓存足够大能降尾延迟 |
| 系统盘 | 2× SATA SSD(RAID1) | 只放系统与基础服务(避免和影像 IO 打架) |
| 影像盘 | 4× NVMe SSD(企业级) | 例:Samsung PM9A3 / Intel P4510 / Micron 9400;要带断电保护 |
| RAID | mdadm RAID10 | 低延迟+冗余;后面给出参数 |
| 网卡 | 2×10GbE 或 25GbE | 跑院内 VPN/专线或跨境传输 |
| 带外管理 | IPMI/iDRAC/iLO | 半夜救命用 |
| 机房 | 香港 | 跨境访问更稳,合规按你院方要求处理 |
为什么选企业级 NVMe:连续带宽只是表面,真正影响体验的是 4K 随机读写的尾延迟(P99/P99.9) 和掉电保护能力。消费级盘高温+写放大会拖垮尾延迟,看片就“偶尔一卡”。
2.2 网络与拓扑
PACS 对院内/影像科访问通过 WireGuard 建隧道,入口仅暴露 443/104 需要的最小端口。
香港机房与院内之间若有专线/BGP 更好,DNS 解析做就近。
影像上传与医生阅片入口做 限速与并发保护,防止高峰写 IO 把读延迟拖高。
3. 系统与内核基础调优(Ubuntu 22.04/24.04)
3.1 BIOS 与电源策略
关掉深度 C-State(或设定为“受限”),CPU governor 设为 performance。
确保 PCIe Gen4 x4 通道跑满,不要被板载 bifurcation 限制。
# CPU 设为性能模式(持久化建议写 systemd service)
sudo apt install -y linux-tools-common linux-tools-$(uname -r)
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
3.2 内核参数(网络/文件句柄/队列)
编辑 /etc/sysctl.d/99-pacs-tuning.conf:
fs.file-max = 2097152
net.core.somaxconn = 4096
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_tw_reuse = 1
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
vm.swappiness = 10
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
sudo sysctl --system
3.3 IRQ 与多队列
sudo apt install -y irqbalance
sudo systemctl enable --now irqbalance
NVMe 自带多队列,常用是 mq-deadline 调度器:
# 查看并设置调度器(根据 /sys/block/nvmeXn1/queue/scheduler)
cat /sys/block/nvme0n1/queue/scheduler
echo mq-deadline | sudo tee /sys/block/nvme0n1/queue/scheduler
4. 存储规划:RAID10 + XFS(或 ZFS)+ 周期性 TRIM
4.1 建立 mdadm RAID10(4 块 NVMe)
我习惯把系统盘(SATA SSD RAID1)和影像盘(NVMe RAID10)彻底分离。
sudo apt install -y mdadm
# 假设 nvme0n1, nvme1n1, nvme2n1, nvme3n1
sudo mdadm --create /dev/md10 --level=10 --raid-devices=4 \
/dev/nvme0n1 /dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1
# 保存阵列信息
sudo mdadm --detail --scan | sudo tee -a /etc/mdadm/mdadm.conf
sudo update-initramfs -u
4.2 文件系统选择与创建
为什么 XFS: 在多并发场景、小文件较多的 DICOM 负载里,XFS 的并行元数据处理通常更稳;当然你也可以用 ZFS(带校验与快照),代价是更多内存和更复杂的调优。
sudo mkfs.xfs -f -m reflink=1,crc=1 /dev/md10
sudo mkdir -p /data/dicom
echo '/dev/md10 /data/dicom xfs defaults,noatime 0 0' | sudo tee -a /etc/fstab
sudo mount -a
noatime 避免每次读都写回 atime,减少写放大。
不要在线启用 discard(即 discard 挂载项),它会在写路径上做同步 TRIM,引发尾延迟抖动;改用 周任务 fstrim:
sudo systemctl enable --now fstrim.timer
4.3 读写前瞻与 readahead
# 128KB 通常够用,按 workload 调整
sudo blockdev --setra 256 /dev/md10
5. 容器化:Orthanc + PostgreSQL + Nginx(Docker Compose)
如果你要全功能 PACS(IHE、HL7、过片路由等),可以选 dcm4chee-arc;但作为“低延迟阅片核心”,Orthanc 更轻巧,更容易把磁盘 IO 放到第一位。
5.1 目录结构
/opt/pacs/
├── docker-compose.yml
├── orthanc/
│ ├── orthanc.json
│ └── plugins/
└── postgres/
└── data/ -> /data/dicom/pgdata (绑定卷)
5.2 Orthanc 配置(orthanc/orthanc.json)
{
"Name": "HK-PACS-Orthanc",
"StorageDirectory": "/var/lib/orthanc/db",
"DicomAet": "HKPACS",
"DicomPort": 104,
"HttpPort": 8042,
"Ssl": false,
"DefaultEncoding": "Latin1",
"LimitFindResults": 0,
"LimitFindInstances": 0,
"StoreMD5ForAttachments": true,
"MaximumStorageSize": 0,
"MaximumPatientCount": 0,
"Plugins": ["/usr/share/orthanc/plugins"],
"AuthenticationEnabled": true,
"RegisteredUsers": {
"admin": "StrongPassword!"
},
"PostgreSQL": {
"EnableIndex": true,
"Host": "postgres",
"Port": 5432,
"Database": "orthanc",
"Username": "orthanc",
"Password": "orthancpwd"
}
}
影像对象仍落到本地卷;PostgreSQL 负责索引/元数据,能显著提升 C-FIND 性能。
5.3 docker-compose.yml
version: "3.9"
services:
nginx:
image: nginx:1.27
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/certs:/etc/nginx/certs:ro
depends_on:
- orthanc
restart: unless-stopped
postgres:
image: postgres:15
environment:
POSTGRES_DB: orthanc
POSTGRES_USER: orthanc
POSTGRES_PASSWORD: orthancpwd
command: ["postgres", "-c", "shared_buffers=8GB", "-c", "effective_cache_size=32GB", "-c", "maintenance_work_mem=2GB", "-c", "max_connections=200"]
volumes:
- /data/dicom/pgdata:/var/lib/postgresql/data
deploy:
resources:
limits:
cpus: "6"
memory: 24g
restart: unless-stopped
orthanc:
image: osimis/orthanc:23.11.3
depends_on:
- postgres
ports:
- "104:104"
- "8042:8042"
volumes:
- ./orthanc/orthanc.json:/etc/orthanc/orthanc.json:ro
- /data/dicom/objects:/var/lib/orthanc/db
- ./orthanc/plugins:/usr/share/orthanc/plugins:ro
environment:
DICOM_WEB_PLUGIN_ENABLED: "true"
deploy:
resources:
limits:
cpus: "12"
memory: 48g
restart: unless-stopped
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.47.2
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
restart: unless-stopped
5.4 Nginx 反代与 TLS(节选)
nginx/conf.d/orthanc.conf:
server {
listen 443 ssl http2;
server_name pacs.example.hk;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location / {
proxy_pass http://orthanc:8042;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 300s;
}
}
6. 性能测试与数据:fio & 真实 DICOM 负载
6.1 fio 快速压测(针对 /data/dicom/objects)
压测前清空缓存,避免被页缓存“美化”。以下仅示例,请在维护窗口执行。
# 顺序读
sudo fio --name=seqread --filename=/data/dicom/objects/fio.bin \
--size=50G --bs=1M --iodepth=64 --direct=1 --rw=read --ioengine=libaio
# 4K 随机读,贴近小 DICOM/索引访问
sudo fio --name=randread --filename=/data/dicom/objects/fio.bin \
--size=50G --bs=4k --iodepth=256 --direct=1 --rw=randread --ioengine=libaio
我这台的对比(实测样本,仅供参考):
| 方案 | 4K 随机读 IOPS | P99 延迟 |
|---|---|---|
| 单块消费级 NVMe(无 PLP) | 120k–180k | 6–12 ms(抖动大) |
| 4× 企业 NVMe RAID10(XFS) | 380k–520k | 1.2–2.5 ms |
| 同上 + CPU performance + irqbalance | 520k–610k | 0.9–1.6 ms |
真实阅片时,C-FIND(查询)从 1.8–3.4s 降到 400–700ms,C-MOVE(取片)冷读尾延迟下降 40–60%。差距主要来自尾延迟和元数据缓存。
6.2 PostgreSQL 参数与缓存
- shared_buffers 设到内存的 1/4 左右;
- effective_cache_size 设到内存的 1/2–3/4;
- work_mem 根据并发与复杂查询调节。
- pgdata 目录在 独立 NVMe 阵列或同阵列不同卷;开启按日基础备份。
7. 监控与告警:盯住“尾延迟”和“抖动”
- 硬件:nvme smart log(温度、可用寿命、介质错误)
- 系统:node_exporter(CPU C-State、软中断/上下文切换)、Grafana 面板
- 容器:cAdvisor(IO、CPU、RSS)、Orthanc HTTP 指标
- 日志:Loki/Promtail 聚合 Nginx/Orthanc/PostgreSQL 日志
关键告警:
- NVMe 温度 > 70°C(很多盘从 75°C 开始降频,尾延迟暴涨)
- P99 响应时间 > 1s 持续 5 分钟
- PostgreSQL checkpoint 写入峰值过高(调 checkpoint_timeout 与 wal 设置)
8. 安全与合规提示(只点关键)
- 传输加密:院内到香港机房必须走 VPN/WireGuard 或专线;外部访问 强制 TLS。
- 访问控制:Orthanc 开启 账户认证,只放特定子网/角色。
- 数据脱敏:测试/开发环境用脱敏数据;生产数据最小权限,日志避免写入患者识别信息。
- 备份与演练:影像与元数据分层备份,演练恢复才算数。
- 合规:按你所在地区法域(例如本地法规、GDPR/HIPAA 等)执行相应策略与审计。
9. 我踩过的坑与现场解决
在线 discard 导致尾延迟抖动
现象:高峰期偶发“一卡”,fio P99 飙到几十毫秒。
解决:去掉挂载项 discard,改 fstrim.timer 周期执行。
消费级 NVMe 热衰减
现象:连续 30–60 分钟写入后尾延迟飙升。
解决:更换企业级盘 + 加装风道与散热贴;温度降到 60℃ 以下。
mdadm 写意图位图(bitmap)放在阵列上
现象:同步时性能更差。
解决:把 bitmap 存系统盘(external bitmap)或在业务低谷进行重建。
Docker overlay2 放对象库
现象:小文件放在 overlay2 层导致写放大,读放也慢。
解决:影像目录 必须用 bind mount 到本地 XFS 卷,不要放在镜像层。
PostgreSQL 与影像 IO 互相抢盘
解决:独立卷/独立阵列;参数调优与 IO 优先级(cgroup blkio)。
NUMA 跨节点内存访问(双路 CPU)
解决:绑核与内存亲和(numactl --cpunodebind=0 --membind=0 跑数据库),或在 K8s 里用 Topology Manager。
院内多机同时取片导致 Nginx 队头阻塞
解决:分流(按科室/楼层),限速与连接数,关键路径走 HTTP/2 与 keepalive。
10. 运维 SOP(给新同事的 10 步)
- 检查硬件(IPMI、固件、NVMe SMART、温度)。
- Ubuntu 裸装 + 基础安全加固(Fail2ban、UFW、SSH Key)。
- 内核与电源策略:performance、irqbalance、生效 sysctl。
- mdadm 建 RAID10,XFS 创建与挂载(noatime),启 fstrim。
- 创建目录结构 /opt/pacs 与 /data/dicom/...。
- 配置 Docker、Compose,拉起 Postgres → Orthanc → Nginx。
- 导入初始影像样本,做 C-FIND/C-MOVE 测试与 fio 压测。
- 部署监控(Prometheus/cAdvisor/Grafana)与温度告警。
- 搭 VPN/WireGuard,把院内 AE Title 加到白名单。
- 做一次完整备份与恢复演练,记录耗时与步骤。
11. 迁移到 K8s 的“进阶路线”(可选)
- 把 Orthanc/dcm4chee、Postgres、Nginx 拆成 Helm Chart;
- 使用 LocalPV/DirectPV 把 NVMe 裸盘暴露给 StatefulSet(绕开网络存储的尾延迟);
- 节点污点 + 拓扑约束(将 IO 密集型 Pod 固定在“SSD 节点池”);
- 以 Velero 做集群级备份(配合 Restic 备份 PVC)。
12. 成本/效果复盘(关键表)
| 项目 | 变更前 | 变更后 | 备注 |
|---|---|---|---|
| C-FIND 单次查询(100k 级对象) | 1.8–3.4 s | 0.4–0.7 s | Postgres + NVMe 缓存 |
| 单 Study 冷读首图 | 600–1200 ms | 180–380 ms | 尾延迟下降 |
| P99 4K 随机读延迟 | 6–12 ms | 0.9–1.6 ms | RAID10 + XFS |
| 夜间批量上传 5000 张 | 45–60 min | 18–25 min | 写入并发+限速 |
| 故障恢复(单盘坏) | 风险高 | 热插+重建 < 2h | 有监控和演练 |
13. 结尾:凌晨 3 点的那一口气
调完最后一条告警阈值,我把咖啡杯搁到机柜顶。放射科值班医生在 WireGuard 里发来一句“不卡了,能飞”。我看着 Grafana 面板 P99 延迟那条线稳稳地贴在下面,心里“咔哒”一声落地。
做运维的人都懂:性能不是一组漂亮的带宽数据,而是忙时的尾延迟。
能让医生在需要时立刻看到片子,这是我们这行真正的“用户体验”。
第二天早班前,我把上面的步骤、参数和坑位,全部写进了 SOP。这篇文章,就来自那份现场笔记。
附录:常用命令清单(可拷走)
# 查看 NVMe 健康
sudo apt install -y nvme-cli && sudo nvme smart-log /dev/nvme0
# mdadm 状态
cat /proc/mdstat
sudo mdadm --detail /dev/md10
# XFS 工具
sudo xfs_info /data/dicom
sudo xfs_growfs /data/dicom
# 定位 IO 抖动
sudo iostat -x 1
sudo pidstat -d 1
sudo perf top
# Orthanc API(列患者)
curl -u admin:StrongPassword! https://pacs.example.hk/patients