数据分布与分区 #
一、分区概述 #
1.1 什么是分区 #
分区是ScyllaDB数据分布的基本单位,每个分区由分区键唯一标识。
text
数据分布概念:
┌─────────────────────────────────────────────────────────┐
│ │
│ Table │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Partition 1 │ Partition 2 │ Partition 3 │ ...│ │
│ │ (Key: A) │ (Key: B) │ (Key: C) │ │ │
│ │ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │ │ │
│ │ │ Row 1 │ │ │ Row 1 │ │ │ Row 1 │ │ │ │
│ │ │ Row 2 │ │ │ Row 2 │ │ │ Row 2 │ │ │ │
│ │ │ Row 3 │ │ │ │ │ │ │ │ │ │
│ │ └─────────┘ │ └─────────┘ │ └─────────┘ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 每个分区存储在一个节点上(含副本) │
└─────────────────────────────────────────────────────────┘
1.2 分区的作用 #
| 作用 | 说明 |
|---|---|
| 数据分布 | 决定数据存储位置 |
| 负载均衡 | 均匀分布数据到各节点 |
| 查询路由 | 快速定位数据位置 |
| 并行处理 | 不同分区可并行处理 |
二、分区键设计 #
2.1 简单分区键 #
sql
-- 单列分区键
CREATE TABLE users (
user_id UUID PRIMARY KEY, -- 分区键
name TEXT,
email TEXT
);
-- 数据分布
-- user_id -> Token -> Node
-- uuid() -> hash(uuid) -> Node A/B/C
2.2 复合分区键 #
sql
-- 多列组成分区键
CREATE TABLE events_by_category (
category TEXT,
event_date DATE,
event_id UUID,
data TEXT,
PRIMARY KEY ((category, event_date), event_id)
);
-- 分区键: (category, event_date)
-- 聚簇列: event_id
-- 数据分布示例
-- ('sports', '2024-01-15') -> Token 1 -> Node A
-- ('sports', '2024-01-16') -> Token 2 -> Node B
-- ('news', '2024-01-15') -> Token 3 -> Node C
2.3 分区键设计原则 #
text
设计原则:
├── 高基数
│ ├── 分区键值多样性高
│ ├── 避免数据倾斜
│ └── 示例:user_id, device_id
├── 查询模式匹配
│ ├── 按查询需求设计
│ ├── WHERE条件包含分区键
│ └── 避免ALLOW FILTERING
├── 数据均匀分布
│ ├── 避免热点分区
│ ├── 合理选择键类型
│ └── 考虑数据增长
└── 分区大小控制
├── 建议<100MB
├── 避免>1GB
└── 控制行数
2.4 分区键选择示例 #
sql
-- 不好的设计:低基数分区键
CREATE TABLE bad_design (
status TEXT PRIMARY KEY, -- 只有几个值:active, inactive, pending
user_id UUID,
name TEXT
);
-- 问题:数据严重倾斜,只有几个分区
-- 好的设计:高基数分区键
CREATE TABLE good_design (
user_id UUID PRIMARY KEY, -- 每个用户一个分区
status TEXT,
name TEXT
);
-- 优点:数据均匀分布
-- 时间序列数据设计
CREATE TABLE time_series_good (
device_id TEXT,
date DATE, -- 分区键的一部分
timestamp TIMESTAMP,
value DOUBLE,
PRIMARY KEY ((device_id, date), timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC);
-- 按设备和日期分区,避免单个分区过大
三、Token环 #
3.1 Token范围 #
text
Token范围:
┌─────────────────────────────────────────────────────────┐
│ │
│ Murmur3Partitioner Token范围: │
│ 最小值: -9223372036854775808 (-2^63) │
│ 最大值: 9223372036854775807 (2^63 - 1) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ -2^63 2^63-1 │ │
│ │ ────────────────────────────────────────────── │ │
│ │ ↑ ↑ ↑ ↑ │ │
│ │ Node A Node B Node C Node A │ │
│ │ Token 1 Token 2 Token 3 Token 1 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
3.2 Token分配 #
bash
# 查看集群Token分布
nodetool ring
# 输出示例
Datacenter: datacenter1
==========
Address Rack Status State Load Owns Token
192.168.1.1 rack1 Up Normal 100 GB 33.33% -9223372036854775808
192.168.1.2 rack1 Up Normal 100 GB 33.33% -3074457345618258603
192.168.1.3 rack1 Up Normal 100 GB 33.33% 3074457345618258602
3.3 Token计算过程 #
python
# Token计算示例
import mmh3
def calculate_token(partition_key):
"""
计算分区键的Token值
"""
# Murmur3哈希
hash_value = mmh3.hash128(partition_key)
# 转换为Token范围
token = hash_value % (2**64) - 2**63
return token
# 示例
token1 = calculate_token("user_001")
token2 = calculate_token("user_002")
四、数据分片 #
4.1 分片过程 #
text
数据分片流程:
┌─────────────────────────────────────────────────────────┐
│ │
│ 1. 客户端写入数据 │
│ INSERT INTO users (user_id, name) VALUES (...); │
│ │ │
│ ▼ │
│ 2. 计算分区键Token │
│ Token = hash(user_id) │
│ │ │
│ ▼ │
│ 3. 查找Token所属节点 │
│ Node = find_node(Token) │
│ │ │
│ ▼ │
│ 4. 写入副本节点 │
│ Write to Node1, Node2, Node3 (RF=3) │
│ │
└─────────────────────────────────────────────────────────┘
4.2 分片示例 #
sql
-- 创建表
CREATE TABLE orders (
order_id UUID,
customer_id UUID,
order_date TIMESTAMP,
amount DECIMAL,
PRIMARY KEY (order_id)
);
-- 插入数据
INSERT INTO orders (order_id, customer_id, order_date, amount)
VALUES (
123e4567-e89b-12d3-a456-426614174000,
987e6543-e21b-12d3-a456-426614174000,
'2024-01-15 10:30:00',
99.99
);
-- 分片过程
-- 1. 分区键: order_id = 123e4567-e89b-12d3-a456-426614174000
-- 2. Token: hash(order_id) = 8765432109876543210
-- 3. 节点: Token范围包含8765432109876543210的节点
-- 4. 副本: 根据复制策略写入副本节点
五、分区大小管理 #
5.1 分区大小影响 #
| 大小 | 影响 |
|---|---|
| < 100MB | 推荐,性能最佳 |
| 100MB - 1GB | 可接受,注意监控 |
| > 1GB | 需要优化,可能影响性能 |
| > 10GB | 严重问题,必须重新设计 |
5.2 分区大小计算 #
sql
-- 查看分区大小
SELECT
partition_key,
COUNT(*) as row_count,
SUM(column_size) as partition_size
FROM table_name
GROUP BY partition_key;
-- 使用nodetool查看表统计
-- nodetool tablestats keyspace.table
5.3 分区大小优化策略 #
sql
-- 策略1:添加时间维度分区
-- 原设计:单个分区过大
CREATE TABLE events_bad (
device_id TEXT PRIMARY KEY,
events LIST<TEXT>
);
-- 优化设计:按日期分区
CREATE TABLE events_good (
device_id TEXT,
event_date DATE,
event_id TIMEUUID,
event_data TEXT,
PRIMARY KEY ((device_id, event_date), event_id)
) WITH CLUSTERING ORDER BY (event_id DESC);
-- 策略2:使用桶分片
CREATE TABLE user_activity_bucket (
user_id UUID,
bucket INT, -- 0-99,随机分配
activity_id TIMEUUID,
activity_data TEXT,
PRIMARY KEY ((user_id, bucket), activity_id)
);
-- 插入时随机选择桶
INSERT INTO user_activity_bucket (user_id, bucket, activity_id, activity_data)
VALUES (uuid(), floor(random() * 100), now(), 'activity data');
六、热点分区 #
6.1 热点分区原因 #
text
热点分区原因:
├── 低基数分区键
│ ├── 状态字段:active, inactive
│ ├── 地区字段:有限区域数
│ └── 类型字段:有限类型数
├── 时间序列数据
│ ├── 按时间分区
│ ├── 最新数据访问频繁
│ └── 写入集中
├── 不均匀数据分布
│ ├── 某些分区键数据量大
│ ├── 热门用户/商品
│ └── 热点事件
└── 顺序写入
├── 自增ID
├── 时间戳作为分区键
└── 导致写入集中
6.2 热点检测 #
bash
# 使用nodetool检测热点
nodetool cfstats keyspace.table | grep "Partition Size"
# 使用ScyllaDB监控工具
# 查看各节点负载是否均衡
nodetool status
# 查看表统计
nodetool tablestats
6.3 热点避免策略 #
sql
-- 策略1:添加随机前缀
CREATE TABLE hot_data_solved (
prefix INT, -- 0-9 随机前缀
original_key TEXT,
data TEXT,
PRIMARY KEY ((prefix, original_key))
);
-- 插入时添加随机前缀
INSERT INTO hot_data_solved (prefix, original_key, data)
VALUES (floor(random() * 10), 'hot_key_1', 'data');
-- 查询时需要扫描所有前缀
SELECT * FROM hot_data_solved
WHERE prefix IN (0,1,2,3,4,5,6,7,8,9)
AND original_key = 'hot_key_1';
-- 策略2:使用复合分区键
CREATE TABLE time_series_balanced (
device_id TEXT,
bucket INT, -- 时间桶
timestamp TIMESTAMP,
value DOUBLE,
PRIMARY KEY ((device_id, bucket), timestamp)
);
-- 策略3:预分桶
CREATE TABLE pre_bucketed (
bucket_id INT, -- 预定义的桶
item_id UUID,
data TEXT,
PRIMARY KEY (bucket_id, item_id)
);
七、分区修复 #
7.1 分区修复场景 #
text
需要修复的场景:
├── 节点故障恢复
├── 数据不一致
├── 副本同步
└── 手动触发修复
7.2 修复操作 #
bash
# 修复整个键空间
nodetool repair keyspace_name
# 修复特定表
nodetool repair keyspace_name table_name
# 修复特定分区
nodetool repair keyspace_name table_name -pr
# 并行修复
nodetool repair keyspace_name -par
# 增量修复
nodetool repair keyspace_name -inc
八、分区数据迁移 #
8.1 节点添加 #
bash
# 添加新节点时数据自动迁移
# 1. 配置新节点
# 2. 指向种子节点
# 3. 启动新节点
# 4. 数据自动再平衡
# 查看迁移状态
nodetool netstats
nodetool streaminfo
8.2 节点移除 #
bash
# 移除节点
nodetool decommission # 在要移除的节点上执行
# 或从其他节点移除故障节点
nodetool removenode <host_id>
# 查看进度
nodetool removenode status
九、最佳实践 #
9.1 分区键设计检查清单 #
text
检查清单:
├── [ ] 分区键基数是否足够高?
├── [ ] 数据是否均匀分布?
├── [ ] 分区大小是否<100MB?
├── [ ] 查询是否包含分区键?
├── [ ] 是否避免了热点?
├── [ ] 是否考虑了数据增长?
└── [ ] 是否考虑了查询模式?
9.2 分区键选择建议 #
| 场景 | 推荐分区键 | 原因 |
|---|---|---|
| 用户数据 | user_id | 高基数,均匀分布 |
| 时间序列 | (device_id, date) | 按设备和日期分区 |
| 订单数据 | order_id | 高基数,唯一标识 |
| 日志数据 | (log_type, date) | 按类型和日期分区 |
| 社交关系 | user_id | 按用户分区 |
十、总结 #
分区要点:
| 概念 | 说明 |
|---|---|
| 分区键 | 数据分布的依据 |
| Token | 分区键的哈希值 |
| Token环 | 数据分布的逻辑结构 |
| vNode | 简化数据分布管理 |
最佳实践:
- 选择高基数分区键
- 控制分区大小
- 避免热点分区
- 匹配查询模式
- 定期监控分区分布
下一步,让我们学习复制策略!
最后更新:2026-03-27