0%

数据开发支持工具

在前面的文章《数据应用开发语言和环境》中我们建议使用SQL来作为主要数据开发语言,并且,通常我们需要对标准的SQL进行增强,以便可以更好的支持复杂的数据开发。一些典型的需要新增的特性可以是变量、控制语句、模板等。

增强SQL固然是可以解决我们的数据开发问题,但是它也会给我们带来一些其他的不便。第一个烦恼可能就是,标准的SQL可以在很多数据工具中运行,比如SupersetSQL查询器、Hive的查询控制台等,而使用增强语法的SQL编写的代码则不行。由于我们将标准的SQL增强了,而SQL周边生态工具却无法感知这样的增强,这时各种不便就随之而来了。

支持数据开发过程

如何解决这个问题呢?想要在周边工具中进行SQL扩展不是一件简单的事情,可能需要花费大量的精力和时间。我们只能另寻他法。

从软件开发的视角来看这个问题,可以发现,我们现在有了编程语言,也有了编程语言的执行环境,基本的开发流程确实是打通了,但是还缺少的是对开发过程的支持。一般而言,开发过程支持完善与否将很大程度上决定团队开发效率的高低。下面我们一起来看看如何完善对于开发过程的支持。

开发过程主要包括代码编辑和调试。下面我们来看看如何支持它们。

支持代码编辑

代码编辑在当前还不会成为一个问题,因为:

  • 我们只是在标准SQL语法的基础上进行了增强,现有的编辑器的大部分现有功能还是可以照常使用的
  • 大部分的语法增强是通过SQL语法的注释功能来实现的,可以兼容标准SQL语法
  • 大部分编辑器其实只是提供SQL语法高亮和格式化的功能,新增的语法不会产生很大的影响

支持代码调试

命令行调试器

现在我们来看调试过程。事实上,使用周边的SQL执行工具来快速验证SQL这个过程本身就是代码调试的过程。

有了增强SQL的语法,我们要如何做呢?回顾增强SQL的语法,我们在其中支持了多个步骤,每个步骤可以是执行SQL,定义变量或者调用外部函数。如果可以一个步骤一个步骤运行,并且可以在每个步骤之后查看当前的变量或SQL执行结果,那将是一件不错的事。这其实也就是一般的程序调试过程。

事实上,有了增强SQL的执行器(即前文提到的驱动器),要实现一个具备基本功能的增强SQL调试器并不困难。按照上面的描述,我们只需要在某一个步骤执行完成之后,先暂停执行,并提供接口查询当前上下文的数据即可。在程序暂停时,一般还可以允许运行一些代码,这也不难,提供接口执行SQL即可。这就是一个命令行的程序调试器雏形。

对应到一般在IDE里面进行调试的交互流程上,打断点的过程,就是指定需要在哪一个步骤暂停,至于查看断点时的状态和在断点时执行代码就跟上面的过程完全一致了。

根据前面的分析,我们可以设计一个命令行调试器类Debugger,它可以具有这些接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Debugger:
# 查询执行状态
def is_started(...):
def is_inprogress(...):
def is_finished(...):

# 查询执行步骤信息
def step(...):
def current_step(...):
def next_step(...):
def last_step(...):
def left_step_count(...):
def print_steps(...):

# 查看或设置执行过程中的变量
def vars(...):
def set_vars(...):
def templates(...):
def tempviews(...):
def showdf(...):

# 执行某一步骤,实现暂停、继续等流程控制功能
def step_on(...):
def step_to(...):
def run(...):
def run_to(...):
def restart(...):

# 在断点过程中执行sql
def sql(...):

上面这些接口借助增强SQL的执行器不难实现。有了Debugger类,一个典型的调试过程就变成:

  • 在任意SQL编辑器中编辑代码
  • 打开IPython命令行
  • 创建Debugger对象: d = create_debugger(sql_file=..., ...)
  • 打印所有步骤:d.print_steps()
  • 执行到某一步:d.run_to(3)
  • SQL编辑器中修改代码
  • 重起调试:d.restart()

(具体实现代码见这里

打印代码执行报告

为了辅助数据开发人员更清楚的理解增强SQL的执行过程,我们最好能打印每一步骤的执行情况,比如实际执行的SQL、执行开始时间、结束时间、当前步骤在整个执行过程中耗时百分比等信息。

一个简单的报告可以设计如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
===================== REPORT FOR step-1 ==================
config: StepConfig(target=..., condition=None, line_no=1)
sql: select 1 as a
status: SUCCEEDED
start time: 2021-04-10 10:05:30, end time: 2021-04-10 10:05:33, execution time: 2.251653s - 8.14%
messages:


===================== REPORT FOR step-2 ==================
config: StepConfig(target=log.a, condition=None, line_no=4)
sql: select '1' as a
status: SUCCEEDED
start time: 2021-04-10 10:05:33, end time: 2021-04-10 10:05:33, execution time: 0.069165s - 0.25%
messages:
a='1'

...

为此,我们可以定义一个执行报告搜集器(ReportCollector),每当一个步骤开始或结束执行时,SQL执行器应当通知报告搜集器搜集该步骤的执行信息。在整个流程执行完成之后,SQL执行器可以调用报告搜集器打印整个过程中搜集到的执行报告。

有了报告搜集器,我们就可以更清楚的了解增强SQL执行过程中的细节了。由于我们的SQL执行基于Spark实现,有了这个报告搜集器,一些简单的Spark程序优化还可以直接通过查看报告来完成。

报告搜集器是一个十分好用的功能,当然需要集成到调试器中了。通过在Debugger类中加入report()方法,我们在调试过程中可以随时打印程序执行报告。

打印日志与执行检查

打印日志也是我们调试程序的常用手段,如何在增强SQL中支持日志打印呢?可以考虑定义一个任务类型为log,按照如下方式来使用:

1
2
-- target=log.some_info_about_this_log
select 1 as var_1, 2 as var_2

日志打印结果可以在上述任务报告中出现,一个比较直观的设计可以是:

1
2
3
4
5
6
7
===================== REPORT FOR step-1 ==================
config: StepConfig(target=log.some_info_about_this_log, condition=None, line_no=1)
sql: select 1 as var_1, 2 as var_2
status: SUCCEEDED
start time: 2021-04-10 10:05:33, end time: 2021-04-10 10:05:33, execution time: 0.069165s - 0.25%
messages:
var_1=1, var_2=2

很多编程语言都提供了assert语法,用以在开发过程中进行及时的假设验证,我们也可以在增强SQL增加这样的支持。可以考虑定义一个任务类型为check,按照如下方式来使用:

1
2
-- target=check.actual_should_equal_expected
select 1 as actual, 2 as expected

如果从结果集中的获取的actual值与expected值不相等,则此任务会失败,并打印错误消息。同时,这样的错误可以在上述任务报告中体现,一个比较直观的设计可以是:

1
2
3
4
5
6
7
===================== REPORT FOR step-1 ==================
config: StepConfig(target=check.actual_should_equal_expected, condition=None, line_no=10)
sql: select 1 as actual, 2 as expected
status: FAILED
start time: 2021-04-10 10:25:32, end time: 2021-04-10 10:25:32, execution time: 0.071442s - 0.52%
messages:
check [actual_should_equal_expected] failed! actual=1, expected=2, check_data=[...]

通过调试模式屏蔽调试过程中的副作用

有了调试器,现在可以愉快的写代码了。但很快我们就会发现另一个需要解决的问题,那就是调试过程可能导致写入某些外部数据库表。这将带来一些风险,因为我们有可能在调试的时候把一些不应该被覆盖的数据库表给覆盖了。

要解决这个问题也很简单,我们可以在SQL执行器中引入一个debug标记来实现。有了debug标记,在执行某一步骤的时候,可以判断是否是向外部数据库表做写操作,如果是且debugtrue,则跳过写操作,只是将该数据创建一个TempView而已。

上述写表操作只是一个场景而已,有了debug标记,我们还可以做很多事情,比如打印更多的调试信息等。

Debugger类在调用SQL执行器时,应当将debug标记设置为true,这样我们就不用担心调试的时候产生任何不想发生的副作用了。

Web数据开发环境

JupyterLab中调试代码

有了上面这些功能,调试器看起来是不错了,但是要与IDE的交互体验比起来,命令行版本的还是过于简单了。能不能想办法增强一下呢?

数据分析师常用的用于运行代码的工具要算JupyterLab了。作为一个打开网页就能用的开发环境,JupyterLab有非常多十分好用的功能,比如,可以一段一段的定义和执行代码,可以支持嵌入Markdown文档,可以支持可视化结果展示,可以编辑多种语言代码等等。

JupyterLab能不能作为我们的代码编辑器使用呢?

查看JupyterLab最新版本,我们会发现JupyterLab提供了Code Console的功能,且可以支持多个编辑器分屏。其操作界面如下:

jupyter lab console

此时,大家可能已经想到了,可以借助这样的交互来实现我们的代码调试功能。JupyterLab不仅给我们提供了一个不错的编辑代码的界面,利用Code Console还可以实现一边写代码一边调试。

JupyterLab中配置好调试器后,一个典型的使用过程如下:

debugger

使用JupyterLab还有一系列的其他好处,比如:

  • 开发人员无需安装配置本地环境(这常常非常耗时),只需要一个浏览器即可开始编写代码。
  • 可以直接配置JupyterLab连接到数据平台集群环境,这样我们就可以直接在集群环境中调试,执行与生产时同样的代码,于是上线代码就更有信心了。

在容器中启动JupyterLab

在基于Hadoop的大数据集群中进行数据开发时,常常还有一个不够方便的地方,那就是客户端环境的构建。

我们常常需要集成了多种集群组件的客户端,比如Spark, HDFS, Hive, HBase等,这些客户端的配置需要保持和集群同步。如果自己去构建这样的客户端,不仅耗时,而且很容易出错。

Ambari可以帮助我们自动配置集群节点,如软件安装,配置同步等繁琐的工作Ambari都可以帮我们搞定。当需要使用集群的客户端环境时,常常也是通过Ambari配置的集群节点来实现。

使用Ambari配置的集群节点作为客户端却有另一个缺点,那就是这样的节点常常由于数量较少而在团队中间共享(由于资源占用问题,我们一般不会配置过多的客户端节点)。

既然是共享的节点,大家都在节点上面操作,就容易发生冲突。比如,小A用自己的帐号登录了(通过kinit),此时小B想要访问集群,如果不使用其他的操作系统帐号,小B就会直接用到小A的帐号权限来访问系统,这不是期望的行为。还比如,小A需要在客户端中安装某一个版本1依赖库,而小B需要在客户端中安装同一个依赖库的版本2,这就产生了冲突,需要小A和小B相互协调才行。

容器技术是解决此问题的一个很好的方式。容器可以提供必要的环境隔离,使得团队成员可以自由的在自己的环境中进行操作,无需担心对他人造成影响。

如何实现呢?其实我们只需要一个运行了sshd的容器即可。通过暴露特定的端口,我们可以把运行着sshd的容器作为一个节点,注册到Ambari中,然后利用Ambari帮我们安装好相关的依赖软件。

软件安装完成之后,我们可以通过docker save命令将这样容器保存为一个基础容器镜像。然后通过运行多个此容器,我们就拥有了多个此类客户端了。由于容器运行成本非常低,可以为每个需要编写代码的团队成员运行一个容器作为他自己的客户端使用。这样一来,开发人员环境隔离问题就迎刃而解了。

在容器环境中运行一个JupyterLab来支持开发是一个不错的主意。这样一来,每个人都拥有了自己的一套独立的用JupyterLab打造的开发环境了。

总结

前面的文章中我们提到使用增强SQL来进行数据开发,但是这带来了一些额外的使用成本。本文讨论了如何支持增强SQL的代码编辑和调试功能。

通过实现一个增强SQL调试器,并在JupyterLab中运行此调试器,我们可以打造了一个基于Web的轻量级数据开发环境,这能很大程度上提高数据开发的效率。为了更好的支持数据开发,我们还可以考虑在SQL执行器中增加执行报告搜集的功能,在调试器中随时打印执行报告对于数据开发是一件好事。除此之外,还可以在SQL执行器中引入调试标记,这可以用来避免调试过程的可能的副作用。

欢迎关注我的其它发布渠道