hue提供了一个很友好的界面,让非专业人士也能够接触轻易地通过SQL完成自己的需求。

随之而来的问题是,外行的SQL能力实在是参差不齐,常常会有个人任务占用大量集群资源,严重影响其他任务。理想的情况下,需要进行一系列的培训和测试,才能开放hue平台的权限。但人非圣贤,孰能无过。即便是熟练工,也可能写出不合格的sql丢进去执行。那应该怎么办?

一个优秀的系统,不能指望人不犯错,而需要把自身打造到坚不可摧。按照这个思路,在SQL执行层做些更严格的检测,如果不合格就不执行或者降低优先级就可以了。据说阿里的dataworks已经实现了,但似乎需要把所有的数据都迁移上去才能用。我们自己当然也可以实现的。

首先,需要一个SQL检查器。这个值得单独一篇文章来写了,暂且跳过。(目前也还真的不懂)。回到正题,如何给hue注入一个sql检查器?

hue的开发架构

hue后端使用Django开发,前后端通过mako模板(类似jinja2)粘合,同时前端使用knockoutjs(类似Vue.js)来做状态管理。

hack过程

整体思路是,后端接收到请求后,在执行前做检查。如果sql有问题,可以直接返回错误,或者在执行后把更易读的错误信息加到response里去。

后端注入

首先在hue平台执行一条sql,抓包能看到是请求的/notebook/api/execute/hive这个路径。从hue的后台源代码找这个url对应的view即可。熟悉Django的话,大概率能追溯到$HUE_HOME/desktop/libs/notebook/src/notebook/api.pyexecute函数。不过这个函数只是个中转站,它负责调用所有引擎进行计算,包括hive,hbase,spark等等。真正的引擎,都在$HUE_HOME/desktop/libs/notebook/src/notebook/connectors里。其中hiveserver2.py文件的execute函数,是真正执行hivesql的函数。

  @query_error_handler
  def execute(self, notebook, snippet):
    db = self._get_db(snippet, interpreter=self.interpreter)

    statement = self._get_current_statement(notebook, snippet)
    session = self._get_session(notebook, snippet['type'])

    query = self._prepare_hql_query(snippet, statement['statement'], session)
    ###### 开始注入, 伪代码
    # statement['statement'] 是从前端提交的sql
    check_result = check(statement['statement'])
    if check_result['error']:
        raise QueryError('self defined error', handle=statement)
    ###### 结束 ######

    try:
      if statement.get('statement_id') == 0:
        if query.database and not statement['statement'].lower().startswith('set'):
          db.use(query.database)
      handle = db.client.query(query, with_multiple_session=True) # Note: with_multiple_session currently ignored
    except QueryServerException, ex:
      raise QueryError(ex.message, handle=statement)

    # All good
    server_id, server_guid = handle.get()
    response = {
      'secret': server_id,
      'guid': server_guid,
      'operation_type': handle.operation_type,
      'has_result_set': handle.has_result_set,
      'modified_row_count': handle.modified_row_count,
      'log_context': handle.log_context,
      'session_guid': handle.session_guid
    }
    response.update(statement)
    ##### 注入: 把自定义的警告消息也混入response
    response['warnings'] = check_result['warn']
    ##### 注入结束
    return response

以上就是函数的源代码,注入的地方已经添加了注释。如果检查出现严重的错误,就停止执行,返回自定义的错误。会直接在前端展示出来,因为raise QueryError就是执行出错时,把错误抛给前端的方式。

而如果有警告信息,就让它随着接口返回。这部分想要被展示出来,就需要研究下前端是如何展示错误信息的,能否把收到的 warnings 也以error的形式展示。

前端展示

Django是典型的MVC模型,从 url 到 view,比较好追溯。前端相对来说就麻烦很多,主要是不熟悉它的架构。尤其是混用了mako模板和kknockoutjs 这个比较旧的框架。

模板和框架的了解过程就省去不表了,先来看看 knockoutjs 的效果吧。如果有hue平台,打开hive editor,在console里测试下面的代码:

var ace_id = $('.ace_editor').attr('id')
# 获取 knockoutjs 的环境
var vm = ko.dataFor(document.getElementById(ace_id));
# push一个error信息
vm.errors.push({message: 'this is an error', help: null, line: null, col: null})

前端接收到后端的response之后,是如何处理的呢?如何在不影响正常执行的情况下, 把警告(提示)信息也展示出来?

打开hive editor时,查看都加载了哪些javascipt文件,查看哪个文件会请求/notebook/api/execute/hive这个url。同时 观察到response中返回的字段有has_result_set, log_context 等,哪个文件包括有这些关键字。二者结合,可以定位到build/static/notebook/js/notebook.ko.js这个文件,而且该文件明显是使用knockoutjs开发的。

读懂之后,把返回的warnings,插入到self.errors就可以了。这也是前文在console中插入错误消息使用的办法。

  <!-- begin: 以下是需要添加的. -->
  if (data.handle.warnings && data.handle.warnings.length) {
    data.handle.warnings.forEach(err => {
      self.errors.push({message: err, help: null, line: null, col: null});
    });
  }
  <!-- end -->
} else {
  self._ajaxError(data, self.execute);
}

顺便提一下,在最新版的hue中,notebook.ko.js被替代为hue/desktop/core/src/desktop/js/apps/notebook/snippet.js

One More Thing

vm.errors.push 的方式,只能展示纯文本。没办法改变字体的大小,颜色,或者添加超链接等等。

如果想要注入HTML标签,可以修改hue/desktop/libs/notebook/src/notebook/templates/editor_components.mako这个文件:

<!-- 原本的样子: 把text改成html就可以了
        <li data-bind="text: message"></li> 
-->
        <li data-bind="html: message"></li>

然后刷新页面,就可以push一个可点击的链接出来。

vm.errors.push({message: '<a href="#">这是一个超链</a>', help: null, line: null, col: null})`

至于改变字体颜色大小等等,添加style属性去定义就好了。

总结

了解hue的整体开发框架(Django, mako, knockoutjs)是很有必要的。摸清来龙去脉之后,需要添加的代码也就十多行。

有了这个注入,可以通过检查,指出sql存在的问题,给出相应的优化建议,甚至附加一个学习链接。系统更加稳健,对新人也更友好。