5.1 RowKey 设计:决定落点的第一因 本节摘要:RowKey 决定一行数据落到哪个 Region、哪台 RegionServer,因此设计 RowKey 等于设计整个集群的负载分布。本节给出"查询模式 → 行键算术 → 散列模式"的方法论,对比顺序、加盐、哈希、反转四种模式的落点行为与代价,并演示热点从发生到诊断到修复的完整闭环。 方法论:从查询清单出发 RowKey 设计的第一步不是想"键长什么样",而是列出这张表的全部查询模式,再逐条翻译成行键区间。
本节摘要:RowKey 决定一行数据落到哪个 Region、哪台 RegionServer,因此设计 RowKey 等于设计整个集群的负载分布。本节给出"查询模式 → 行键算术 → 散列模式"的方法论,对比顺序、加盐、哈希、反转四种模式的落点行为与代价,并演示热点从发生到诊断到修复的完整闭环。
RowKey 设计的第一步不是想"键长什么样",而是列出这张表的全部查询模式,再逐条翻译成行键区间。一张订单表的典型清单:
| 查询 | 频率 | 行键算术表达 |
|---|---|---|
| 按订单号点查 | 高频 | [订单号, +∞) 长度 1 的区间 |
| 查某用户全部订单 | 高频 | 前缀 用户ID 的区间 |
| 查某用户近期订单 | 高频 | 用户ID + [时间窗] 的复合区间 |
| 按时间段全量对账 | 低频 | 全表扫描或按时间分表 |
翻译之后矛盾立刻显形:查询 2、3 要求用户 ID 在前;查询 4 若靠单表扫描则希望时间在结构里;而写入端若用数据库自增或时间戳直接当前缀,全部写入会顺序挤进最后一个 Region(3.1 节的落点计算:单调递增键永远落在"最大区间")。Schema 设计就是拿着这些互相冲突的要求做仲裁。
模式一:顺序键(反例)。 ts=1724055123 直接做前缀。落点行为:新数据永远打在表尾 Region,一台机器吃满写流量,前面的 Region 闲置——教科书级热点。唯一的好处是时间范围扫描是天然连续区间。
模式二:加盐(salting)。 键前拼一个随机或轮转前缀(0–9 或 00–0f):
原始键: u1001-1724055123 u1001-1724055700 u2002-1724055901 加盐后: 3-u1001-1724055123 7-u1001-1724055700 1-u2002-1724055901
写入被均匀摊到所有 Region,但同一用户的订单散落在多个前缀下,查询 2、3 必须并发发 N 个前缀扫描再拼结果。随机盐还让"读己所写"变难(写入方得记住自己用的盐)。
模式三:哈希前缀。 md5(用户ID) 取前 4 位做前缀,后接原键。与加盐的差别:同一主体永远得到同一前缀,该主体的数据连续存放,点查与前缀扫描一次定位、无需拼接。代价是哈希前缀的数量分布必须与预分区分界对齐(3.1 节),且完全放弃跨主体的范围有序。
模式四:反转固定字段。 把尾部本身分布均匀的字段反转到前面。用户号 10001、10002、... 尾号均匀,反转成 10001→10001(此处反转二进制后按字节倒序)后首字节均匀散开;后接时间戳保留"同主体内按时间有序",查询 2、3 完美支持。中文场景的常见变体:手机号反转(尾号随机性最好)、md5 用户名等。
// 组合键生成:反转用户号 + 倒排时间戳 + 订单号 byte[] rowkey = Bytes.concat( reverse(userId), // 散列位 均匀摊开 Bytes.toBytes(Long.MAX_VALUE - timestamp), // 倒排时间 新单在前 orderId.getBytes()); // 兜底唯一性
倒排时间戳是常见搭配:同主体扫描时最新数据排在最前,LIMIT N 只扫头部即返回, scans 宽度最省。
复现热点(用 4.2 节的 Shell 循环):预分区 10 个 Region 的表,灌 10 万行时间戳前缀键:
hbase:080:0> (1..100000).each { |i| hbase:081:1* put 'hot_demo', "1724055123-#{format('%08d', i)}", hbase:082:1* 'cf:v', "data-#{i}" }
灌完看 Web UI 的 Region Servers 页:只有一个 Region 的请求量与 Store 大小暴涨,其余纹丝不动。用 locate_region 也能确认所有键都落在 [9000, 空) 那个区间。修复三步:
md5(uid) 前 2 字符十六进制(256 个桶)+ uid + 倒排 ts;10, 20, ... f0,或直接 SPLITS_FILE 提供文件);改键后同样灌数,Web UI 的请求量曲线变成一条水平线——落点分布从"独木桥"变成"十车道"。
⚠️ 常见坑:为了散列把行键设计得很长(如整个 md5 32 位前缀)。行键字节在每条 KeyValue 里重复出现(2.3 节),键越长存储与索引膨胀越狠。工程折中:散列前缀 2–4 字节足够,整键控制在 50–100 字节内。
| 场景 | 推荐模式 | 牺牲的能力 |
|---|---|---|
| 只按主体点查(画像、设备状态) | 哈希前缀 | 跨主体范围扫 |
| 按主体 + 时间区间查(订单、消息) | 反转主体字段 + 倒排时间 | 无明显牺牲 |
| 顺序到达需要快速摊开(埋点流水) | 加盐(轮转) | 单主体连续性、读己所写 |
| 低频写入、范围扫为主(维表、配置) | 顺序键即可 | 无(量小无所谓热点) |
最后一条常被忽略:热点是流量概念。一天几万条的表用顺序键毫无问题,别为不存在的问题引入拼接复杂度。
💡 关键直觉:RowKey = 分区函数 + 索引。你每加一个字节,都在同时回答两个问题:"这行落在哪"和"这行怎么被找到"。两个答案冲突时,去查频率更高的那一边。
行键定了横向分布,纵向的物理参数(列族、版本、压缩、块大小)是 5.2 节的话题。