Cassandra 数据库数据建模最佳实践

Best practice modeling data for Cassandra databases

我是 Cassandra 的新手,正在寻找有关如何对具有以下一般结构的数据建模的最佳实践:

数据基于"user"(每个客户),每个都提供一个大约 500K-2M 条目的大数据文件(每天定期更新几次 - 有时完全更新,有时仅更新)

每个数据文件都有一定的必填数据字段(~20 个必填字段),但可以自行决定添加额外的列(最多~100 个)。

附加数据字段对于不同的用户来说不一定相同(字段名称或字段类型)

示例(csv 格式:)

user_id_1.csv

| column1 (unique key per user_id)  |  column2  |  column3 |   ...   |  column10  |  additionalColumn1  |  ...additionalColumn_n |
|-----------------------------------|-----------|----------|---------|------------|---------------------|------------------------|
| user_id_1_key_1                   |  value    |  value   |  value  |  value     |                ...  |  value                 |
| user_id_1_key_2                   |  ....     |  ....    |  ....   |  ....      |                ...  |  ...                   |
| ....                              |  ...      |  ...     |  ...    |  ...       |                ...  |  ...                   |
| user_id_1_key_2Million            |  ....     |  ....    |  ....   |  ....      |                ...  |  ...                   |


user_id_XXX.csv (notice that the first 10 columns are identical to the other users but the additional columns are different - both the names and their types)

|             column1 (unique key per user_id)              |  column2  |  column3 |   ...   |  column10  |  additionalColumn1 (different types than user_id_1 and others)  |  ...additional_column_x |
|-----------------------------------------------------------|-----------|----------|---------|------------|-----------------------------------------------------------------|-------------------------|
| user_id_XXX_key_1                                         |  value    |  value   |  value  |  value     |                                                            ...  |  value                  |
| user_id_XXX_key_2                                         |  ....     |  ....    |  ....   |  ....      |                                                            ...  |  ...                    |
| ....                                                      |  ...      |  ...     |  ...    |  ...       |                                                            ...  |  ...                    |
| user_id_XXX_key_500_thousand (less rows than other user)  |  ....     |  ....    |  ....   |  ....      |                                                            ...  |  ...                    |

我考虑过的几个选项:

选项 1:

  1. 创建一个 "global" 密钥space
  2. 创建一个包含所有内容的大 table "data"
  3. 将 user_id 列与所有其他列连接到大 table(包括非强制列)。主键变为 user_id + "column_1"(column_1 根据 user_id 是唯一的)

                                     Keyspace
    +--------------------------------------------------------------------------+
    |                                                                          |
    |                                                                          |
    |                                      Data_Table                          |
    |                +  +--------+-------+--------------------------+-----+    |
    |                |  |        |       |                          |     |    |
    |                |  +-------------------------------------------------+    |
    |                |  |        |       |                          |     |    |
    |    many rows   |  +-------------------------------------------------+    |
    |                |  |        |       |                          |     |    |
    |                |  |        |       |                          |     |    |
    |                |  |        |       |                          |     |    |
    |                |  |        |       |     Many columns         |     |    |
    |                |  |        |       +------------------------> |     |    |
    |                |  |        |       |                          |     |    |
    |                |  +-------------------------------------------------+    |
    |                v  +-------------------------------------------------+    |
    |                                                                          |
    +--------------------------------------------------------------------------+
    

我立即注意到的几件事:

  1. user_id 重复自身的次数与每个用户的条目一样多
  2. 附加列的行非常稀疏(空 null 值),因为用户不一定共享它们
  3. 用户数量相对较少,因此增加了一些列 不是很大(最多 10K 列)
  4. 我可以将每个用户的附加列数据压缩到一个名为 "meta data" 的列中,并为所有用户共享它

选项 2:

根据 User_id

创建密钥space

每个键创建 table "data"space

+-----------------------------------------------------------------------------------+
| column_1 | column_2 | ... | column_n | additional_column_1 | additional_column_n  |
+-----------------------------------------------------------------------------------+

keyspace_user1         keyspace_user2                     keyspace_user_n
+----------------+    +---------------+                  +---------------+
|                |    |               |                  |               |
|                |    |               |                  |               |
|   +-+-+--+-+   |    |    +-+--+--+  |                  |   +--+--+---+ |
|   | | |  | |   |    |    | |  |  |  |   many keyspaces |   |  |  |   | |
|   | | |  | |   |    |    | |  |  |  | +------------->  |   |  |  |   | |
|   | | |  | |   |    |    | |  |  |  |                  |   |  |  |   | |
|   | | |  | |   |    |    | |  |  |  |                  |   |  |  |   | |
|   +--------+   |    |    +-------+  |                  |   +---------+ |
+----------------+    +---------------+                  +---------------+

备注:

  1. 许多键space(每个用户键space)
  2. 避免每行添加 "user_id" 值(我可以使用键 space 名称作为用户 ID)
  3. 每个键很少 tablesspace(在这个例子中每个键只有 1 tablespace)

选项 3:

1) 创建全局密钥space 2) 根据 user_id 创建一个 table(必填列及其 table 的附加列)

+---------------------------------------------------------------+
|                            Keyspace                           |
|                                                               |
|       user_1        user_2                         user_n     |
|    +--+---+--+   +--+--+--+                      +--+--+--+   |
|    |  |   |  |   |  |  |  |                      |  |  |  |   |
|    |  |   |  |   |  |  |  |                      |  |  |  |   |
|    |  |   |  |   |  |  |  |                      |  |  |  |   |
|    |  |   |  |   |  |  |  |                      |  |  |  |   |
|    |  |   |  |   |  |  |  |                      |  |  |  |   |
|    +--+---+--+   +--+--+--+                      +--+--+--+   |
|                                                               |
|                                                               |
+---------------------------------------------------------------+

备注

  1. 全局键space
  2. A table 每 user_id ("many" tables)
  3. 避免每行重复用户 ID

选项 4:(这有意义吗?)

创建一个多键spaces(例如"x"个键spaces)每个持有tables的范围(table每个用户)

                      keyspace_1                                                                                keyspace_x
+---------------------------------------------------------------+                         +---------------------------------------------------------------+
|                                                               |                         |                                                               |
|                                                               |                         |                                                               |
|       user_1        user_2                        user_n/x    |                         |     user_n-x      user_n-x+1                       user_n     |
|    +--+---+--+   +--+--+--+                      +--+--+--+   |                         |    +--+------+   +--+--+--+                      +--+--+--+   |
|    |  |   |  |   |  |  |  |                      |  |  |  |   |        "X" keyspaces    |    |  |   |  |   |  |  |  |                      |  |  |  |   |
|    |  |   |  |   |  |  |  |                      |  |  |  |   | +---------------------> |    |  |   |  |   |  |  |  |                      |  |  |  |   |
|    |  |   |  |   |  |  |  |                      |  |  |  |   |                         |    |  |   |  |   |  |  |  |                      |  |  |  |   |
|    |  |   |  |   |  |  |  |                      |  |  |  |   |                         |    |  |   |  |   |  |  |  |                      |  |  |  |   |
|    |  |   |  |   |  |  |  |                      |  |  |  |   |                         |    |  |   |  |   |  |  |  |                      |  |  |  |   |
|    +--+---+--+   +--+--+--+                      +--+--+--+   |                         |    +--+---+--+   +--+--+--+                      +--+--+--+   |
|                                                               |                         |                                                               |
|                                                               |                         |                                                               |
+---------------------------------------------------------------+                         +---------------------------------------------------------------+

备注:

  1. 多键spaces
  2. 每个用户多个 tables
  3. 需要 "lookup" 来确定哪个键 space 包含所需的 table

选项 5:

将数据拆分为多个 tables 和多个 keyspaces

备注: 1. 在某些情况下需要来自多个 table 的 "joining" 信息 2.好像比较复杂


所有场景的一般注意事项:

  1. 写入次数比读取次数少
  2. 每天数百万次阅读
  3. 流量根据 user_id 波动 - 一些 user_id 的流量很大,而一些 user_id 的流量要少得多。需要根据此指标进行调整
  4. 有些 user_id 的更新(写入)频率高于其他
  5. 我们有多个跨地域的数据中心,应该同步
  6. 每个主键都有一个长尾(一些键被多次访问而另一些键很少被访问)

尝试以下架构:

CREATE TABLE data (
    userid bigint,
    key text,
    column text,
    value text,
    PRIMARY KEY (userid, key)
);

这里

userid  -> userid
key     -> column1
column  -> column name from column2
value   -> column value

以下数据的插入示例:

| column1 (unique key per user_id)  |  column2      |  column3        |
|-----------------------------------|---------------|-----------------|
| key_1                             |  value12      |  value13        | 
| key_2                             |  value22      |  value23        |

插入语句:

INSERT INTO data (userid , key , column , value ) VALUES ( 1, 'key_1', 'column2', 'value12');
INSERT INTO data (userid , key , column , value ) VALUES ( 1, 'key_1', 'column3', 'value13');
INSERT INTO data (userid , key , column , value ) VALUES ( 1, 'key_2', 'column2', 'value22');
INSERT INTO data (userid , key , column , value ) VALUES ( 1, 'key_2', 'column3', 'value23');

这种类型的集成挑战通常由关系系统中的 EAV(实体属性值)数据模型解决(如 Ashrafaul 演示的那样)。考虑 EAV 模型时的关键考虑因素是无限数量的列。当然,可以在像 Cassandra 或 ScyllaDB 这样的 CQL 系统中模仿 EAV 数据模型。 EAV 模型非常适合写作,但在阅读时会带来挑战。您还没有真正详细说明您的阅读注意事项。您需要返回所有列还是需要返回每个用户的特定列?

文件

话虽如此,Cassandra 和 ScyllaDB 固有的一些进一步考虑可能会指向您在问题中描述的某些设计上采用统一的 EAV 模型。 Cassandra 和 ScyllaDB 都将键空间和数据库布局为磁盘上的文件。文件数量基本上是键空间数量乘以 tables 数量的乘积。因此,您拥有的键空间、tables 或两者的组合越多,磁盘上的文件就越多。这可能是文件描述符和其他 os 文件处理问题。由于您提到的长尾访问,可能每个文件都一直处于打开状态。这不是很理想,尤其是从冷启动开始时。

[为清楚起见编辑] 在所有条件相同的情况下,一个 keyspace/table 生成的文件总是比许多 keyspace/table 生成的文件少。这与存储的数据量或压缩策略无关。

宽行

但是回到数据模型。 Ashraful 的模型有一个主键 (userid) 和另一个集群键 (key->column1)。由于每个用户文件 (500K-2M) 中 "entries" 的数量,并假设每个条目都是由平均 60 列组成的行,您基本上要做的是为每个分区创建 500k-2m * 60 平均列行键从而创建非常大的分区。 Cassandra 和 Scylla 通常不喜欢非常大的分区。他们可以处理大分区吗?实际上,大分区会影响性能吗,是的。

更新或版本控制

你提到了更新。基础 EAV 模型将仅代表 most 最近的更新。没有版本控制。您可以做的是将时间添加为聚类键,以确保随着时间的推移维护列的历史值。

阅读

如果您想要返回所有列,您可以将所有内容序列化为一个 json 对象并将其放在一个列中。但我想这不是你想要的。在基于 key/value 的系统(如 Cassandra 和 Scylla)的主键(分区键)模型中,您需要知道键的所有组件才能取回数据。如果将唯一行标识符 column1 放入主键中,您将需要提前知道它,如果 ose 也放入主键中,其他列名也同样如此。

分区和 Composite 分区键

分区数决定了集群的并行度。总分区数或总语料库中分区的基数会影响集群硬件的利用率。更多分区 = 更好的并行性和更高的资源利用率。

我在这里可能做的是修改 PRIMARY KEY 以包含 column1。然后我会使用 column 作为聚类键(它不仅规定了分区内的唯一性,而且还规定了排序顺序 - 所以在你的列命名约定中考虑这一点)。

在下面的 table 定义中,您需要在 WHERE 子句中提供 useridcolumn1 作为等式。

CREATE TABLE data (
    userid bigint,
    column1 text,
    column text,
    value text,
    PRIMARY KEY ( (userid, column1), column )
);

我还有一个单独的 table,也许 columns_per_user,它记录每个 userid 的所有列。像

CREATE TABLE columns_per_user (
    userid bigint,
    max_columns int,
    column_names text
    PRIMARY KEY ( userid )
);

其中 max_columns 是该用户的总列数,column_names 是实际的列名。您可能还有一列显示每个用户的条目总数,例如 user_entries int 基本上是每个用户 csv 文件中的行数。