数据分布与分区 #

一、分区概述 #

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 简化数据分布管理

最佳实践:

  1. 选择高基数分区键
  2. 控制分区大小
  3. 避免热点分区
  4. 匹配查询模式
  5. 定期监控分区分布

下一步,让我们学习复制策略!

最后更新:2026-03-27