上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-06 10:06 阅读量:1209


香港机房凌晨 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 步)

  1. 检查硬件(IPMI、固件、NVMe SMART、温度)。
  2. Ubuntu 裸装 + 基础安全加固(Fail2ban、UFW、SSH Key)。
  3. 内核与电源策略:performance、irqbalance、生效 sysctl。
  4. mdadm 建 RAID10,XFS 创建与挂载(noatime),启 fstrim。
  5. 创建目录结构 /opt/pacs 与 /data/dicom/...。
  6. 配置 Docker、Compose,拉起 Postgres → Orthanc → Nginx。
  7. 导入初始影像样本,做 C-FIND/C-MOVE 测试与 fio 压测。
  8. 部署监控(Prometheus/cAdvisor/Grafana)与温度告警。
  9. 搭 VPN/WireGuard,把院内 AE Title 加到白名单。
  10. 做一次完整备份与恢复演练,记录耗时与步骤。

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
目录结构
全文