Kylin, Mondrian, Saiku 系统的整合

本文主要介绍有赞数据团队为了满足在不同维度查看、分析重点指标的需求而搭建的 OLAP 分析工具。这个工具对 Kylin、Mondrian 以及 Saiku 做了一个整合,主要工作包括一些定制化的修改以及环境的配置。
目前这个系统还处于一个需要优化、完善的过程,这篇博文也会相应地更新。

背景

有赞发展的初期,数据团队主要的工作之一就是根据运营人员的报表需求,编写 sql,从 hive 中获得数据并写入 mysql 中存储。最后,前端人员写相应的代码展现 mysql 中存储的报表数据。
随着公司业务的快速发展,如此长周期的报表开发流程已经很难跟上运营人员的分析需求了。为了避免深陷报表开发、维护的泥潭,数据组决定调研大数据场景下的 OLAP 分析工具。参考了明略数据的解决方案之后,我们选择整合KylinMondrianSaiku来实现这样一个 OLAP 系统。

三巨头

Kylin

kylin 是 apache 软件基金会的顶级项目,一个开源的分布式多维分析工具。下面是摘自 Kylin 官网的介绍:

Apache Kylin™ is an open source Distributed Analytics Engine designed to provide SQL interface and multi-dimensional analysis (OLAP) on Hadoop supporting extremely large datasets, original contributed from eBay Inc.

个人的理解是:Kylin 通过预计算所有合理的维度组合下各个指标的值并把计算结果存储到 HBASE 中的方式,大大提高分布式多维分析的查询效率。Kylin 接收 sql 查询语句作为输入,以查询结果作为输出。通过预计算的方式,将在 hive 中可能需要几分钟的查询响应时间下降到毫秒级。更细致的关于 Kylin 的介绍,可以参考我的另一片博客Kylin 初体验

Mondrian

Mondrian is an Open Source Business Analytics engine that enables organizations of any size to give business users access to their data for interactive analysis. You can build powerful Business Intelligence solutions with Mondrian as your Online Analytical Processing (OLAP) engine, enabling multidimensional queries against your business data, using the powerful MDX query language.

Mondrian 是一个 OLAP 分析的引擎,主要工作是根据事先配置好的 schema,将输入的多维分析语句MDX(Multidimensional Expressions) 翻译成目标数据库/数据引擎的执行语言(比如 SQL)。

Saiku

Saiku allows business users to explore complex data sources, using a familiar drag and drop interface and easy to understand business terminology, all within a browser. Select the data you are interested in, look at it from different perspectives, drill into the detail. Once you have your answer, save your results, share them, export them to Excel or PDF, all straight from the browser.

Saiku 提供了一个多维分析的用户操作界面,可以通过简单拖拉拽的方式迅速生成报表。Saiku 的主要工作是根据事先配置好的 schema,将用户的操作转化成 MDX 语句提供给 Mondrian 引擎执行。

技术架构

架构图

Kylin + Mondrian + Saiku 是一个简单的三层架构。git 上开源的 Saiku 的项目已经整合了 mondrian 的 jar 包。所以构建这样一个三层架构主要的工作是将 Mondrian 的 schema 和 Kylin 的 schema 对应起来,同时需要针对 Kylin 的语法对 Mondrian 做一些 Kylin dialect 的定制开发。
Git 上已经有一个整合 Kylin,Mondrian 以及 Saiku 的项目。照着这个项目的指引,可以很轻松的搭建这么一个三层的系统。在此,致谢开源项目作者mustangore

一些细节

介绍完整体的结构,下面讲一些构建过程中遇到的坑。有些可能是我们的理解还不够深入,有些可能随着开源软件版本的升级已经不再是一个坑了。希望能给大家带来一些帮助,如果是由于我们理解的偏差导致踩到的坑,也希望大家留言给出指正:)
本套系统构建基于 kylin1.5, Mondrian4.4 以及 Saiku3.7.4。底层是 Hive0.14 以及 Hbase0.98。

关于 schema

前面提到,要让系统运转,Kylin 的 schema 必须和 mondrian 的 schema 能够对接上。Kylin 是根据自身 cube 配置的 schema 来进行预计算的,schema 决定 Kylin 能够接收的 sql 查询的范围。Mondrian 又根据自身的 shema 翻译 MDX 到 sql, Mondrian 的 schema 决定它生成的 sql 的范围。如果两者有不一致的情况,就可能导致 Mondrian 生成的 sql 无法被 Kylin 执行。
kylin 的 schema 配置比较简单,管理页面上有一套图形界面指引你一步步地构建一个星型模型,配置 di mension、measure。不过要把 cube 设计得高效,Kylin 还是有不少高级地设置的,比如选择 attribute group, derived dimension 等。官网上有详细的介绍
Mondrian 的 schema 没有比较好的图形配置工具,需要手写 Mondrian schema 的 XML 文档,文档格式参考官方文档,通过 Saiku 上传。
需要注意的坑:

  • 不要用 view 作为 lookup table 在设计 Kylin cube 时,用 hive view 作为 fact table 是一个比较好的实践方式,可以屏蔽一些底层数据结构变化对 Kylin cube 的影响。但是不要用 view 作 look up table,在 build cube 计算维度表容量时会出问题。
  • Kylin 无法在预计算指标时制定条件 比如有两个字段:order_pay, is_payed。我们可以配置 sum(order_pay) 作为订单金额, sum(is_payed) 作为付款订单数。但是没法配置 sum(order_pay) where is_payed = 1 来表示付款订单金额。我们需要在 fact view 中添加字段 payed_order_pay 表示付款的订单金额。
  • 尽量在 Kylin 中用 int 类型 比如 is_payed 字段,就 0/1 两个值,通常我们在 hive 里可以设置为 tiny int 类型的字段。但是在 Kylin 中,针对 tiny int 和 int 类型的字段配置出来的 measure 类型是不一样的,tiny int 类型的字段得到的 measure 在和 Saiku 结合时可能会出现问题。
  • 把 hive 表放在 default 库中 Kylin 添加 hive table 的时候是可以指定 hive table 所在库的,但是建议将 fact table、lookup table 都放在 default 库中。因为在 Mondrian 的 schema 中,physical table 是默认去 default 库查找的,目前还没有发现很好的在 Mondrian schema 中指定数据库的方式。

关于 count distinct

Kylin 配置 cube 的时候可以指定某个 measure 的聚合方式为 count distinct,有精准计算的方式也有基于 hyperloglog 算法的近似计算方式。同样,在 Mondrian 的 schema 里也可以配置 count distinct 的指标聚合方式。
看上去一切都 OK,然而问题来了:
Kylin 的 count distinct 语法只针用 count distinct 聚合的指标字段,在计算维度表大小的时候,kylin 无法计算类似 select count(distinct date) from lu_date这样的 sql 语句。在mustangore 的项目中,对 Mondrian 打了 Kylin-dialect 的补丁。其中添加了一个 JdbcDialect 的实现:


public class KylinDialect extends JdbcDialectImpl {

public static final JdbcDialectFactory FACTORY =
        new JdbcDialectFactory(KylinDialect.class, DatabaseProduct.KYLIN) {
            protected boolean acceptsConnection(Connection connection) {
                return super.acceptsConnection(connection);
            }
        };

/**
 * Creates a KylinDialect.
 *
 * @param connection Connection
 * @throws SQLException on error
 */
public KylinDialect(Connection connection) throws SQLException {
    super(connection);
}

@Override
public boolean allowsCountDistinct() {
    return false;
}

@Override
public boolean allowsJoinOn() {
    return true;
}

}

注意到:allowsCountDistinct() 函数被设置成了 return false;
mustangore 通过这种方式避免了 Mondrian 计算维度大小的时候 count disctinct,然而这种一杆子打死的方式也使得 Mondrian 计算 count distinct 的指标的时候出现问题:select count(distinct XXX) from tableA这样的语句会被翻译成select count YYY from (select distinct XXX as YYY from tableA),而 Kylin 又不能很好的执行后者。为了解决这个两难的问题,我们深入到 Mondrian 的源码中去,找到了计算维度表大小的代码:


private static String generateColumnCardinalitySql(
Dialect dialect,
String schema,
String table,
String column)
{
final StringBuilder buf = new StringBuilder();
String exprString = dialect.quoteIdentifier(column);
if (dialect.allowsCountDistinct()) {
// e.g. “select count(distinct product_id) from product”
buf.append("select count(distinct “)
.append(exprString)
.append(”) from ");
dialect.quoteIdentifier(buf, schema, table);
return buf.toString();
}
else if (dialect.allowsFromQuery()) {
// Some databases (e.g. Access) don’t like ‘count(distinct)’,
// so use, e.g., “select count(*) from (select distinct
// product_id from product)”
buf.append(“select count(*) from (select distinct “)
.append(exprString)
.append(” from “);
dialect.quoteIdentifier(buf, schema, table);
buf.append(”)”);

注意到只有 dialect.allowsCountDistinct()为 true 时才会用 count distinct 来计算维度表大小。 我们只要将 Kylin dialect 的 allowsCountDistinct() 设置为 true,同时在 generateColumnCardinalitySql 添加一个判断条件:

  if (dialect.allowsCountDistinct()
            && !dialect.getDatabaseProduct().name().equalsIgnoreCase("KYLIN")) {
            ...
就可以实现和 kylin 的 count distcint measure 的正常对接了。

关于 Kylin sql

有了处理 count distinct 的问题的经验,我们发现,只要了解 Kylin sql 的特点,针对 Kylin sql 定制 Mondrian 的 Kylin—diect 就能将 Mondrian 和 kylin 较好的对接。经过在 Kylin1.5 的交互界面中的测试,我们列出如下的区别:

  • 不能 limit beg, end 只能 limit length
  • 不支持 union, union all
  • 不支持 where exists 子句

结束语

以上是有赞数据团队实现多维分析工具的探索过程。总的来说,Kylin + Saiku + Mondrian 的一套流程是能走通的,中途遇到一些零碎的问题没有完全列出来。通常是因为 Kylin 只支持 cube 范围内的查询,如果 Mondrian 翻译出的 sql 超出这个范围就会引起系统的错误。通常有三种解决方案:
- 重新构建 Kylin cube,让它能覆盖更广范围的查询
- 修改 Mondrian schema,让它的 cube 描述和 Kylin cube 吻合
- 定制化开发 Mondrian 的 Kylin dialect,让 Mondrian 生成符合 Kylin 特点的 sql

目前我们还在对这套三层框架做一些定制化的功能开发以及性能的调优工作。希望这篇文章能给大家带来些帮助,也希望有独特见解或者发现我们的理解不对的朋友们留言交流。