数据要分级存放吗?香港服务器上的列加密 + 全盘加密的性能与成本对比
技术教程 2025-09-30 10:00 221


在我这台香港机房服务器的实测里:只做全盘加密(LUKS)的吞吐损耗在 3–8%;只做列加密(应用/数据库层)的 QPS/TPS 下降 18–25%;两者叠加通常在 22–30% 区间。成本上,列加密主要消耗 CPU 和研发复杂度;全盘加密主要带来 I/O 端的小开销与运维流程变化。分级存放能把“敏感强加密”和“普通合规加密”拆开,既控风险又控成本。

我第一次在香港 MEGA-i 机房做这套方案,是一个周五的凌晨三点。空调出风口呼呼作响,走道尽头只有 KVM 切换器和我笔记本屏幕的亮光。业务方催着要上线一套“强合规”的客户资料系统:卡号、手机号、地址要看得见但又看不全;审计要在不降 SLA 的情况下拿到每小时报表;同时还得满足“介质遗失不可读”。

一句话要求:数据要分级、强敏感字段列加密、底层磁盘全盘加密,不能拖慢线上。

我当时给自己定了三件事:

  • 把存储分层做清楚(Hot/Warm/Cold);
  • 用LUKS/dm-crypt给卷做全盘加密;
  • 对PII 类字段做列加密(可检索、可审计),并留好回滚与故障旁路。

下面是完整落地过程与实测数据。

  • OS:CentOS 7(稳定、大家环境一致;我会给出 cryptsetup 与 PostgreSQL 的新版本来源与兼容点)。
  • 数据库:PostgreSQL 14(PGDG 源),附带 pgcrypto;也给出基于 Vault Transit 的列加密思路。
  • 全盘加密:dm-crypt/LUKS2(XTS-AES-256),NVMe 支持 discard,优化 workqueue。
  • 网络:10GbE,后端只对内提供服务。
  • 场景:香港单机/小集群(读多写少的 OLTP + 周期报表)。

机房与硬件清单(实配)

类别 型号/参数 备注
机房 香港 MEGA-i(等价:Equinix/HK2) 10GbE 交付,稳定低时延
服务器 Supermicro 1U(单路 AMD EPYC 7543P 32C/64T) AES-NI/VAES 支持,频率 2.8–3.7GHz
内存 256GB DDR4-3200 预留给缓存与并发
系统盘 2× SATA SSD 480GB(RAID1) 系统与 /boot(/boot 不加密)
Hot Tier NVMe PCIe 4.0 3.84TB(独立直通) 数据库主表空间、WAL、热点索引
Warm Tier 4× HDD 12TB(直通 HBA,ZFS RAIDZ2 或 XFS JBOD) 冷数据、审计归档
网口 2×10GbE(bond) LACP

为什么选 NVMe 直通:LUKS 在现代 CPU 下大多是算力富余,性能瓶颈更多在介质/调度上;直通 NVMe + 合理的 dm-crypt 优化,基本能把 LUKS 的开销压到 3–8%。

数据分级策略(我线上用的标准)

级别 典型数据 加密策略 介质/层
S1 极敏感 卡号、身份证、手机号原文 列加密 + 全盘加密(两层) Hot(NVMe)
S2 敏感 订单、地址、脱敏手机号 全盘加密 Hot/Warm
S3 一般 报表、审计归档、脱敏历史 可选全盘加密 Warm(HDD)/ 对象存储
S0 公共 配置、缓存、无关 PII 可不加密(按规范) 任意

关键点:把强敏感字段限定在有限列上,用列加密控制最小暴露面;其他数据交给全盘加密兜底,做到“介质丢失不可读”。

方案大图

底层:/dev/nvme1n1、/dev/nvme2n1 做 LUKS2 -> XFS;HDD 归档卷也做 LUKS2(慢 IO 不敏感)。

数据库:PostgreSQL 14,数据目录与 WAL 都放在 NVMe 加密卷;个别冷表/分区迁到 Warm。

列加密:两种路线任选或并用:

  • 轻量:pgcrypto + 盲索引(HMAC);
  • 生产级:Vault Transit(可做收敛/确定性加密便于等值查询),应用侧调用,数据库存密文与盲索引。

部署实操(CentOS 7)

⚠️ 生产建议开一个维护窗口;提前做 iLO/IPMI 远程控制回滚方案。

1)基础与内核/工具准备

# 基础工具
yum -y install epel-release
yum -y install htop iotop numactl tuned tmux jq curl wget

# cryptsetup 2.x(CentOS 7 默认较旧,建议升级到 EPEL 的新版本)
yum -y install cryptsetup

# 可选:更高内核(ELRepo),对 NVMe/blk-mq 更友好
yum -y install https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel -y install kernel-lt
grub2-set-default 0

2)创建 LUKS2 加密卷(NVMe 热数据)

# 以 /dev/nvme1n1 为例
cryptsetup luksFormat /dev/nvme1n1 \
  --type luks2 \
  --cipher aes-xts-plain64 --key-size 512 \
  --hash sha256 \
  --pbkdf argon2id --iter-time 2000 \
  --pbkdf-memory 1048576 --pbkdf-parallel 4

# 打开并优化 workqueue(降低额外调度开销)
cryptsetup open /dev/nvme1n1 luks_hot \
  --perf-no_read_workqueue --perf-no_write_workqueue

# XFS 建议 4K 扇区对齐
mkfs.xfs -f -s size=4096 /dev/mapper/luks_hot
mkdir -p /data/hot
mount -o noatime,nodiratime /dev/mapper/luks_hot /data/hot

持久化:

# 获取 LUKS UUID
cryptsetup luksUUID /dev/nvme1n1

# /etc/crypttab
# name    <luks-uuid>          none  luks,discard,no-read-workqueue,no-write-workqueue
luks_hot UUID=<YOUR-LUKS-UUID> none  luks,discard,no-read-workqueue,no-write-workqueue

# /etc/fstab
/dev/mapper/luks_hot  /data/hot  xfs  defaults,noatime,nodiratime  0 0

TRIM 建议:在线 discard 会带来一点抖动。我线上改成周任务:

systemctl enable fstrim.timer && systemctl start fstrim.timer

3)PostgreSQL 14 安装与放置在加密卷

# PGDG 源
yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
yum -y install postgresql14-server postgresql14-contrib

# 数据目录迁移到 /data/hot/pg14
mkdir -p /data/hot/pg14
chown -R postgres:postgres /data/hot/pg14
sudo -u postgres /usr/pgsql-14/bin/initdb -D /data/hot/pg14

# 服务配置
sed -i 's#^Environment=PGDATA=.*#Environment=PGDATA=/data/hot/pg14#' /usr/lib/systemd/system/postgresql-14.service
systemctl daemon-reload
systemctl enable --now postgresql-14

核心调优(节选) /data/hot/pg14/postgresql.conf:

shared_buffers = 64GB
effective_cache_size = 160GB
wal_level = replica
wal_compression = on
max_wal_size = '16GB'
random_page_cost = 1.1
effective_io_concurrency = 256
maintenance_work_mem = 4GB
checkpoint_timeout = '15min'
synchronous_commit = on   # 金融/强一致需求保留

WAL 放 NVMe:pg_wal 在 NVMe 上效果最明显;Warm 层只放历史分区/归档。

4)列加密路线 A:pgcrypto + 盲索引(不依赖外部 KMS)

CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- 存储密文、盲索引;明文只在应用层
CREATE TABLE customers (
  id BIGSERIAL PRIMARY KEY,
  name TEXT,
  phone_enc BYTEA,          -- 加密后
  phone_idx BYTEA,          -- 盲索引(HMAC)
  card_enc  BYTEA,
  card_idx  BYTEA,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- KMS 外部管理:这里演示变量,生产请由应用注入,不要硬编码
-- hmac(key, data, 'sha256') 返回 bytea,可对等值查询建立索引
-- 盲索引:不泄露明文但支持等值匹配
CREATE INDEX ON customers (phone_idx);
CREATE INDEX ON customers (card_idx);

-- 插入/查询示例(演示函数,上线改在应用层做)
-- 假设 key1 为对称密钥(来自 Vault/KMS/硬件密钥),base64 传入
-- 加密:pgp_sym_encrypt(bytea/text, keytext, 'compress-algo=0, cipher-algo=aes256')

应用侧(伪代码,Python 为例):

from base64 import b64encode
import hashlib
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes

K_ENC = b"...32bytes-symmetric-key..."   # 从 Vault/KMS 拉取,按租户或按列滚动
K_HMAC = b"...32bytes-hmac-key..."

def blind_index(value: str) -> bytes:
    # 归一化再 HMAC,支持等值检索
    v = value.strip().lower().encode()
    return hashlib.pbkdf2_hmac('sha256', v, K_HMAC, 1, dklen=32)  # 或直接 HMAC-SHA256

def enc_gcm(value: str) -> bytes:
    v = value.encode()
    nonce = get_random_bytes(12)
    cipher = AES.new(K_ENC, AES.MODE_GCM, nonce=nonce)
    ct, tag = cipher.encrypt_and_digest(v)
    return nonce + ct + tag  # 存入 BYTEA(长度 = 12+len(v)+16)

# 查询时:where phone_idx = blind_index(input_phone)

优点:轻量、可快速落地。

限制:密钥轮转要自建;GCM 是随机 IV,不适合等值查询,因此要靠盲索引字段实现检索(注意防撞与盐策略)。

5)列加密路线 B:HashiCorp Vault Transit(支持“确定性/收敛加密”)

Vault 可用 aes256-gcm96,配合 derived=true + convergent_encryption=true 与 context,实现同值同密文,方便等值索引(风险:泄露频次信息;按需启用)。

安装与启用 Transit(简化示例):

# 安装略,单机可开发模式或 Raft 存储;生产至少 3 节点
vault server -config=/etc/vault.d/config.hcl

export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='<root-or-app-token>'

# 创建密钥
curl -s --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data '{"type":"aes256-gcm96","derived":true,"convergent_encryption":true}' \
  $VAULT_ADDR/v1/transit/keys/pii

加密/解密(应用侧):

# 加密(确定性:提供 context,相同明文+context => 相同密文)
PLAINTEXT=$(echo -n "15500001234" | base64)
CTX=$(echo -n "phone-v1" | base64)
curl -s --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data "{\"plaintext\":\"$PLAINTEXT\",\"context\":\"$CTX\"}" \
  $VAULT_ADDR/v1/transit/encrypt/pii

# 解密
CIPHERTEXT="vault:v1:...."
curl -s --header "X-Vault-Token: $VAULT_TOKEN" \
  --request POST \
  --data "{\"ciphertext\":\"$CIPHERTEXT\",\"context\":\"$CTX\"}" \
  $VAULT_ADDR/v1/transit/decrypt/pii

数据库里存 ciphertext 与用于检索的派生索引(可以是同样的 context 下对明文做 HMAC/BLAKE2)。

优点:密钥生命周期、审计、轮转能力强;应用无密钥明文。

注意:Transit 故障时要有降级/队列;确定性加密会暴露出现频次(映射关系),严控字段与访问。

基准测试与结果

测试方法

fio(块层):4K 随机读写、QD 32,直接对 XFS 文件;分别在未加密、LUKS场景。

pgbench(业务层):scale=100,clients=64,持续 300s;分别在四种工况:

  • A) 无加密;B) 仅 LUKS;C) 仅列加密(对 2 个字段做加解密 + 盲索引);D) LUKS + 列加密。

CPU:mpstat 观察加密核占用;perf 采样热点。

fio 结果(NVMe 热层)

场景 4K 随机读 (IOPS) 4K 随机写 (IOPS) p99 延迟 (读/写, μs) CPU us%
未加密 520,000 320,000 980 / 1,450 31
LUKS(XTS-AES-256) 492,000 (-5.4%) 303,000 (-5.3%) 1,060 / 1,560 36

解读:现代 EPYC 的 AES 指令很能打;LUKS 开销控制在 3–8% 合理区间。

pgbench 结果(数据库 + 应用)

场景 TPS 相对变化 CPU us% 备注
A 无加密 18,200 基线 52 仅业务逻辑
B 仅 LUKS 17,300 -5% 55 I/O 层轻微开销
C 仅列加密 14,100 -22% 68 应用/函数加解密 + 盲索引
D LUKS + 列加密 13,400 -26% 70 两者叠加非线性,但接近相加

解读:列加密的主要成本在 CPU 与函数路径(以及失去原生索引后靠盲索引的额外存储/计算)。全盘加密影响不大,更多是 I/O 栈上微小抖动。

成本评估(以香港托管/租用常见价格区间为参考)

仅 LUKS 仅列加密 叠加
硬件增量 几乎无(CPU 带 AES 即可) 可能需要更高主频/更多核 同列加密
性能损耗 3–8% 18–25% 22–30%
开发复杂度 低(运维侧) 高(应用 + KMS)
维护成本 低(密钥托管+开机解锁流程) 中-高(密钥滚动、KMS 可用性)
直接费用 几乎 0 可能多 1 个配置档(CPU/实例/月) 同列加密
合规性 兜底 可满足字段级最小暴露 最严

经验数:为了把 C→D 的性能损耗打回去,加 4–8 个 vCPU 或换高主频 SKU通常就够,月成本在香港多几十到一百多美元不等(因供应商而异)。

线上坑与解决

cryptsetup 版本过旧

  • 现象:--perf-no_read_workqueue 参数不识别。
  • 处理:升级 cryptsetup 到 2.x;必要时上 ELRepo 的长期支持内核。

NVMe + online discard 抖动

现象:峰值延迟抬头。

处理:取消挂载 discard,改用 fstrim.timer 周期 TRIM。

pgcrypto 盲索引碰撞/不可复现

现象:查询不到或误命中。

处理:统一归一化规则(去空格、大小写、全角半角),盲索引加入租户盐(tenant-salt);写入前后做一致性校验。

Vault Transit 不可用

策略:应用侧熔断 + 队列,写入走暂存 Kafka;读路径支持旧密文解密与Key version 迁移;运维准备“只读降级”预案。

备份加密与密钥分离

工具:restic/borg + 存储端 KMS(对象存储在港区);密钥与数据异地分存。

性能优化清单(我线上生效的)

CPU:cpupower frequency-set -g performance;核/中断绑核(NVMe/10GbE)。

I/O 调度:NVMe 设置 none:

echo none > /sys/block/nvme0n1/queue/scheduler

dm-crypt:no-read-workqueue,no-write-workqueue;sector-size=4096;NVMe 用 allow-discards 但 TRIM 定时。

XFS:noatime,nodiratime;日志合适大小,避免频繁 sync 抖动。

PostgreSQL:

  • WAL 独立 NVMe;wal_compression=on;
  • 对列加密字段只做盲索引而非普通索引;
  • 业务查询尽量等值匹配,减少需要解密做过滤的场景;
  • 批量解密用 LIMIT/OFFSET + 流水线,避开单次大扫描。

密钥策略:

  • 按租户/按列/按周期分级密钥;
  • 轮转留足重加密窗口与双读双写期;
  • 审计链路写满(谁、何时、在哪台机、请求了哪类密钥)。

我如何落地“数据要不要分级存放?”

  1. 要分级,而且先分级再选技术。
  2. S1 列加密 + LUKS:卡号、身份证、手机号等“失窃即高损害”的数据;
  3. S2 仅 LUKS:订单、地址(已脱敏);
  4. S3 可只存 LUKS/对象存储侧加密:报表/归档。
  5. 把“贵”的列加密用在刀刃上,把“廉价稳”的全盘加密铺底。

运维清单(上线前自检)

  •  grep aes /proc/cpuinfo 存在(AES-NI)
  •  cryptsetup 2.x,能识别 --perf-no_*_workqueue
  •  crypttab/fstab 均已配置,dracut -f 后重启验证能解锁
  •  fstrim.timer 已启用
  •  PostgreSQL 数据目录与 WAL 在 NVMe 加密卷
  •  列加密字段只暴露密文与盲索引,无明文落库
  •  Vault(如使用)高可用 + 令牌续期 + 审计开启
  •  备份/恢复演练(含密钥丢失/轮转场景)
  •  基准测试与阈值纳入监控(p99、CPU us%、队列深度)

我为什么安心按下“上线”按钮

那天清晨五点多,报表作业跑完,pgbench 的监控曲线像一条被熨平的带子。我在 KVM 前又复盘了一遍:
强敏感数据被列加密“紧紧抱在怀里”,其余数据有全盘加密“盖着被子”;NVMe 的 IOPS 只掉了一个边角,TP99 没被拉爆。Vault 的告警板绿着,fstrim 的定时器亮着。
我合上笔记本,走出机房,天已经亮了。
上线这件事,不只是跑通功能,而是在风险、性能、成本之间找到那个让人睡得着的平衡点。

附:关键命令/配置片段汇总

# LUKS2 初始化(NVMe)
cryptsetup luksFormat /dev/nvme1n1 --type luks2 \
  --cipher aes-xts-plain64 --key-size 512 \
  --pbkdf argon2id --iter-time 2000 --pbkdf-memory 1048576 --pbkdf-parallel 4

cryptsetup open /dev/nvme1n1 luks_hot \
  --perf-no_read_workqueue --perf-no_write_workqueue
mkfs.xfs -f -s size=4096 /dev/mapper/luks_hot

# fstab/crypttab
# /etc/crypttab
luks_hot UUID=<LUKS-UUID> none luks,discard,no-read-workqueue,no-write-workqueue
# /etc/fstab
/dev/mapper/luks_hot  /data/hot  xfs  defaults,noatime,nodiratime  0 0

# PostgreSQL 14
yum -y install postgresql14-server postgresql14-contrib
sudo -u postgres /usr/pgsql-14/bin/initdb -D /data/hot/pg14
systemctl enable --now postgresql-14

SQL(pgcrypto + 盲索引骨架)

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE customers (
  id BIGSERIAL PRIMARY KEY,
  name TEXT,
  phone_enc BYTEA,
  phone_idx BYTEA,
  card_enc  BYTEA,
  card_idx  BYTEA,
  created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX ON customers (phone_idx);
CREATE INDEX ON customers (card_idx);

你可以直接复用我的决策模板

  1. 先分级:S1/S2/S3;
  2. S1(极敏感)= 列加密 + LUKS;
  3. S2(敏感)= LUKS;
  4. S3(一般)= 归档 +(可选)介质侧加密;
  5. 做一组 A/B/C/D 四工况基准,量化你自己的损耗与余量;
  6. 把损耗换回去:加核/调优,把成本算清楚;
  7. 备份、密钥、告警和演练,缺一不可