Apache Phoenix 性能最佳做法
Apache Phoenix 性能的最重要方面是优化基础 Apache HBase。 Phoenix 在 HBase 的顶层创建一个关系数据模型,用于将 SQL 查询转换为 HBase 操作,例如扫描。 表架构的设计、主键中字段的选择和排序,以及索引的用法都会影响 Phoenix 的性能。
表架构设计
在 Phoenix 中创建一个表时,该表将存储在 HBase 表中。 HBase 表包含可统一访问的列组(列系列)。 Phoenix 表中的行是 HBase 表中的行,其中每个行由版本受控的单元格构成,这些单元格与一个或多个列相关联。 在逻辑上,单个 HBase 行是键值对的集合,这些键值对具有相同的行键值。 这就是说,每个键值对具有一个行键属性,特定行的该行键属性值是相同的。
Phoenix 表的架构设计包括主键设计、列系列设计、单个列的设计,以及数据分区方式。
主键设计
在 Phoenix 中的表上定义的主键确定如何将数据存储在基础 HBase 表的行键中。 在 HBase 中,访问特定行的唯一方法就是使用行键。 此外,存储在 HBase 表中的数据已按行键排序。 Phoenix 通过按主键中定义的顺序将行中每个列的值进行串联来生成行键值。
例如,联系人表包含名字、姓氏、电话号码和地址,所有这些数据都包含在同一个列系列中。 可以基于不断递增的序列号定义主键:
rowkey | address | phone | firstName | lastName |
---|---|---|---|---|
1000 | 1111 San Gabriel Dr. | 1-425-000-0002 | John | Dole |
8396 | 5415 San Gabriel Dr. | 1-230-555-0191 | Calvin | Raji |
但是,如果经常按 lastName 执行查询,则使用此主键可能性能不佳,因为每个查询需要扫描整个表才能读取每个 lastName 的值。 此时,可以基于 lastName、firstName 和社会安全号码列定义主键。 最后一列用于区分位于同一地址、使用相同姓名的两位居民(例如父亲和儿子)。
rowkey | address | phone | firstName | lastName | socialSecurityNum |
---|---|---|---|---|---|
1000 | 1111 San Gabriel Dr. | 1-425-000-0002 | John | Dole | 111 |
8396 | 5415 San Gabriel Dr. | 1-230-555-0191 | Calvin | Raji | 222 |
Phoenix 使用此新主键生成的行键是:
rowkey | address | phone | firstName | lastName | socialSecurityNum |
---|---|---|---|---|---|
Dole-John-111 | 1111 San Gabriel Dr. | 1-425-000-0002 | John | Dole | 111 |
Raji-Calvin-222 | 5415 San Gabriel Dr. | 1-230-555-0191 | Calvin | Raji | 222 |
在给定表的第一行中,行键的数据表示为如下所示:
rowkey | key | 值 |
---|---|---|
Dole-John-111 | address | 1111 San Gabriel Dr. |
Dole-John-111 | phone | 1-425-000-0002 |
Dole-John-111 | firstName | John |
Dole-John-111 | lastName | Dole |
Dole-John-111 | socialSecurityNum | 111 |
现在,此行键存储了数据的重复副本。 请考虑要包含在主键中的列大小和数目,因为此值将与基础 HBase 表中的每个单元格包含在一起。
此外,如果主键包含单调递增的值,则应使用盐桶创建表,以帮助避免产生写入热点 - 请参阅将分区数据。
列系列设计
如果某些列的访问频率比其他列更高,应创建多个列系列,将经常访问的列与极少访问列区分开来。
此外,如果某些列往往是一起访问的,可将这些列放在同一个列系列中。
列设计
- 由于大型列的 I/O 开销较大,请将 VARCHAR 列保持为大约 1 MB 以下。 处理查询时,HBase 会将单元格作为一个整体具体化,然后将其发送到客户端。客户端会作为一个整体接收这些单元格,然后将其转交到应用程序代码。
- 使用 protobuf、Avro、msgpack 或 BSON 等紧凑格式存储列值。 不建议使用 JSON,因为它更大。
- 在存储之前考虑压缩数据,以降低延迟和 I/O 开销。
将数据分区
使用 Phoenix 可以控制数据分发到的区域数目,从而大幅提高读/写性能。 创建 Phoenix 表时,可以将数据加盐或预先拆分。
若要在创建过程中给表加盐,请指定盐桶数目:
CREATE TABLE CONTACTS (...) SALT_BUCKETS = 16
此加盐过程将连同主键值一起拆分表,并自动选择值。
若要控制表的拆分位置,可以通过提供拆分所要遵循的范围值,来预先拆分表。 例如,若要创建一个沿着三个区域拆分的表:
CREATE TABLE CONTACTS (...) SPLIT ON ('CS','EU','NA')
索引设计
Phoenix 索引是一个 HBase 表,存储索引表中的部分或全部数据的副本。 索引可以提高特定类型的查询的性能。
如果定义多个索引后查询表,Phoenix 会自动选择查询的最佳索引。 主索引是根据所选的主键自动创建的。
对于预见性查询,还可以通过指定查询的列来创建辅助索引。
设计索引时:
- 只创建所需的索引。
- 限制频繁更新表的索引数。 对的表更新将解释为同时写入主表和索引表。
创建辅助索引
辅助索引将完整表扫描转化为点查找,因此可以提高读取性能,代价是消耗更多的存储空间和降低写入速度。 在创建表后,可以添加或移除辅助索引,且不需要更改现有查询 - 只是查询会更快地运行。 请考虑根据需要创建涵盖索引和/或功能索引。
使用涵盖索引
涵盖索引是包含行中的数据以及已编制索引的值的索引。 找到所需的索引条目后,不需要访问主表。
例如,在示例联系人表中,可以只是基于 socialSecurityNum 列创建辅助索引。 此辅助索引将加快按 socialSecurityNum 值执行筛选的查询的速度,但检索其他字段值时需要针对主表进行另一次读取。
rowkey | address | phone | firstName | lastName | socialSecurityNum |
---|---|---|---|---|---|
Dole-John-111 | 1111 San Gabriel Dr. | 1-425-000-0002 | John | Dole | 111 |
Raji-Calvin-222 | 5415 San Gabriel Dr. | 1-230-555-0191 | Calvin | Raji | 222 |
但是,在指定 socialSecurityNum 的情况下,如果你往往还要查找 firstName 和 lastName,则可以创建一个涵盖索引,并在其中包含 firstName 和 lastName 作为索引表中的实际数据:
CREATE INDEX ssn_idx ON CONTACTS (socialSecurityNum) INCLUDE(firstName, lastName);
通过此涵盖索引,以下查询只需从包含辅助索引的表中读取数据,即可获取所有数据:
SELECT socialSecurityNum, firstName, lastName FROM CONTACTS WHERE socialSecurityNum > 100;
使用功能索引
使用功能索引可以基于预期要在查询中使用的任意表达式创建索引。 创建功能索引以及使用该表达式的查询后,该索引可用于检索结果而不是数据表。
例如,可以创建一个索引,以便根据某人的名字和姓氏组合来执行不区分大小写的搜索:
CREATE INDEX FULLNAME_UPPER_IDX ON "Contacts" (UPPER("firstName"||' '||"lastName"));
查询设计
查询设计的主要考虑因素是:
- 了解查询计划并验证其预期行为。
- 有效联接。
了解查询计划
在 SQLLine 中,依次使用 EXPLAIN 和 SQL 查询来查看 Phoenix 将要执行的操作计划。 检查该计划:
- 是否在适当的情况下使用主键。
- 是否使用适当的辅助索引而不是数据表。
- 是否尽量 RANGE SCAN 或 SKIP SCAN,而不是 TABLE SCAN。
计划示例
举个例子,假设有一个名为 FLIGHTS 的表,其中存储了航班延迟信息。
若要选择 airlineid
为 19805
的所有航班(其中 airlineid
是主键或任何索引中均不存在的字段),请执行以下操作:
select * from "FLIGHTS" where airlineid = '19805';
按如下所示运行说明的命令:
explain select * from "FLIGHTS" where airlineid = '19805';
查询计划如下所示:
CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN FULL SCAN OVER FLIGHTS
SERVER FILTER BY AIRLINEID = '19805'
在此计划中,请注意短语 FULL SCAN OVER FLIGHTS。 此短语表示针对表中的所有行执行了 TABLE SCAN,而没有使用更高效的 RANGE SCAN 或 SKIP SCAN。
现在,假设你要查询 carrier(航空公司)AA
在 2014 年 1 月 2 日 flightnum(航班号)大于 1 的航班。 假设 year、month、dayofmonth、carrier 和 flightnum 列在示例表中存在,并且都包含在复合主键中。 查询如下所示:
select * from "FLIGHTS" where year = 2014 and month = 1 and dayofmonth = 2 and carrier = 'AA' and flightnum > 1;
使用以下代码检查此查询的计划:
explain select * from "FLIGHTS" where year = 2014 and month = 1 and dayofmonth = 2 and carrier = 'AA' and flightnum > 1;
生成的计划为:
CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER FLIGHTS [2014,1,2,'AA',2] - [2014,1,2,'AA',*]
方括号中的值是主键的值范围。 在本例中,范围值是固定的,即年份 2014、月份 1 和月份日期 2,但允许航班号 2 和更大的值 (*
)。 此查询计划确认已按预期使用主键。
接下来,基于名为 carrier2_idx
的 FLIGHTS 表创建一个索引,该索引只出现在 carrier 字段中。 此索引还包含 flightdate
、tailnum
、origin
和 flightnum
作为涵盖列,这些列的数据也存储在索引中。
CREATE INDEX carrier2_idx ON FLIGHTS (carrier) INCLUDE(FLIGHTDATE,TAILNUM,ORIGIN,FLIGHTNUM);
假设你要获取航空公司以及 flightdate
和 tailnum
,如以下查询所示:
explain select carrier,flightdate,tailnum from "FLIGHTS" where carrier = 'AA';
应会看到使用了此索引:
CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER CARRIER2_IDX ['AA']
有关 explain 计划结果中可能显示的完整项列表,请参阅 Apache Phoenix 优化指南中的“Explain 计划”部分。
有效联接
一般而言,除非一侧较小,否则应避免联接,尤其是针对频繁查询。
如果需要,可以结合 /*+ USE_SORT_MERGE_JOIN */
提示执行大型联接,但是,针对大量的行执行大型联接会产生很高的开销。 如果所有右侧表的总体大小超过了可用内存,请使用 /*+ NO_STAR_JOIN */
提示。
方案
以下指导原则描述了一些常用模式。
读取密集型工作负荷
对于读取密集型的用例,请确保使用索引。 此外,为了节省读取时间开销,请考虑创建涵盖索引。
写入密集型工作负荷
对于包含单调递增主键的写入密集型工作负荷,请创建盐桶来帮助避免产生写入热点,代价是:因需要执行更多扫描而导致总体读取吞吐量降低。 此外,在使用 UPSERT 写入大量记录时,请关闭 autoCommit 并批处理记录。
批量删除
删除大型数据集时,请先打开 autoCommit,然后再发出 DELETE 查询,使客户端不需要记住所有已删除行的行键。 AutoCommit 会阻止客户端缓冲受 DELETE 影响的行,因此,Phoenix 可以直接在区域服务器上删除这些行,且无需将其返回到客户端。
不可变和仅限追加
如果方案更看重写入速度而不是数据完整性,请考虑在创建表时禁用预写日志:
CREATE TABLE CONTACTS (...) DISABLE_WAL=true;
有关此选项和其他选项的详细信息,请参阅 Apache Phoenix 语法。