
在我这台香港机房服务器的实测里:只做全盘加密(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 | 2× 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 + 流水线,避开单次大扫描。
密钥策略:
- 按租户/按列/按周期分级密钥;
- 轮转留足重加密窗口与双读双写期;
- 审计链路写满(谁、何时、在哪台机、请求了哪类密钥)。
我如何落地“数据要不要分级存放?”
- 要分级,而且先分级再选技术。
- S1 列加密 + LUKS:卡号、身份证、手机号等“失窃即高损害”的数据;
- S2 仅 LUKS:订单、地址(已脱敏);
- S3 可只存 LUKS/对象存储侧加密:报表/归档。
- 把“贵”的列加密用在刀刃上,把“廉价稳”的全盘加密铺底。
运维清单(上线前自检)
- 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);
你可以直接复用我的决策模板
- 先分级:S1/S2/S3;
- S1(极敏感)= 列加密 + LUKS;
- S2(敏感)= LUKS;
- S3(一般)= 归档 +(可选)介质侧加密;
- 做一组 A/B/C/D 四工况基准,量化你自己的损耗与余量;
- 把损耗换回去:加核/调优,把成本算清楚;
- 备份、密钥、告警和演练,缺一不可