大数据分析可视化平台 Zeppelin 架构和原理

1,337 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

了解Zeppelin的架构有助于你分析开发过程中碰到的问题以及如何更好的使用 Zeppelin。

Zeppelin 架构

首先我们来了解下 Zeppelin的架构, Zeppelin 主要分3层。

  • Web前端
  • Zeppelin Server
  • Interpreter

  • Zeppelin前端负责前端页面的交互,通过Rest API 和WebSocket的方式与Zeppelin Server进行交互。
  • Zeppelin Server是一个Web server,负责管理所有的note,interpreter 等等,Zeppelin Server不做具体的代码执行,会交给Interpreter来执行代码
  • Interpreter 是一个独立的进程,负责具体前端用户提交的代码的执行(比如Spark Scala代码或者SQL代码等等)。Zeppelin Server与 Interpreter 之间是通过thrift 来进行通信,而且是双向通信。Zeppelin支持目前大部分流行的大数据引擎,上图只展示了其中3种比较常用的引擎:Flink,Spark,Jdbc

Zeppelin Server是独立的进程,进程日志在logs目录下的 zeppelin-{user}-{host}.log, 每个Interpreter也是一个独立的进程,进程日志是 logs目录下的 zeppelin-interpreter-{interpreter}-*.log, 所以如果碰到任何问题可以先去这两个log文件里去查找线索

Zeppelin源码分析---目录结构与modules分析

Zeppelin,于2016-5-18日从Apache孵化器项目毕业成为Apache顶级项目,采用Java(主要)+Scala+R+PythonR+Bash+JS混合开发,采用maven作为build工具。涉及的主要技术stack如下:

  1. 前台 : AngularJS、Node.JS、WebSocket、Grunt、Bower、Highlight.js、BootStrap3js
  2. 后台 : Jetty(embedding)、Thrift、Shiro(权限)、Apache common-exec、Jersey REST API

zeppelin本质上是一个web应用程序,它以独立的jvm进程的方式来启动Interpreter(解释器),交互式(repl)执行各种语言的代码片段,并将结果以html代码片段的形式返回给前端UI。

distribution结构分析

编译完成之后,会在zeppelin-distribution/target/目录下生成如下结构的分发包:

bin目录存储了zeppelin的启停控制脚本:

各个脚本作用如下:

脚本作用
zeppelin-daemon.sh提供以daemon形式启停org.apache.zeppelin.server.ZeppelinServer服务,并调用common.sh和function.sh设置env和classpath
zeppelin.sh以foreground的形式启动ZeppelinServer
Interpreter.sh采用单独进程启动org.apache.zeppelin.interpreter.remote.RemoteInterpreterServer服务,该脚本不会被其他脚本直接调用,实际是通过apache common-exec来调用的
common.sh设置zeppelin运行时需要的env和classpath,如果${ZEPPELIN_HOME}/conf/目录中存在zeppelin-env.sh,则会调用该脚本
function.sh一些公共基础函数

Conf

配置文件作用
shiro.ini供apache shiro框架使用的权限控制文件
zeppelin-env.sh供${ZEPPELIN_HOME}/bin/common.sh脚本调用,设置诸如:SPARK_HOME、HADOOP_CONF_DIR等zeppelin与外围环境集成的环境变量
zeppelin-site.xml.templatezeppelin的配置模板,被注释掉的property是zeppelin的默认配置,可以rename成zeppelin-site.xml然后根据需要override

Interpreter

每个针对具体的某种语言实现的Interpreter,存放的是编译好的jar包,以及该interpreter的依赖

Notebook

该目录是默认的notebook的持久化存储目录,zeppelin默认的notebook持久化实现类是org.apache.zeppelin.notebook.repo.VFSNotebookRepo,该实现会以zeppelin.notebook.dir配置参数指定的路径来存储notebook(默认是json格式)

由于该value指定的uri不带scheme,默认会在${ZEPPELIN_HOME}目录下创建notebook子目录用于存储各个notebook,以notebook的id为子目录名字。

module组成

zeppelin的maven项目共有几十个mudule 几个框架相关的module如下:

Module作用开发语言
zeppelin-server项目的主入口,通过内嵌Jetty的方式提供Web UI和REST服务,并且提供了基本的权限验证服务java
zeppelin-zengine实现Notebook的持久化、检索实现interpreter的自动加载,以及maven式的依赖自动加载java
zeppelin-interpreter为了支持多语言Notebook,抽象出了每种语言都要事先的Interpreter接口,包括:显示、调度、依赖以及和zeppelin-engine之间的Thirft通信协议java
zeppelin-webAngluarJS开发的web页面Javascript(主要是AngularJS、Node.JS以及使用websocket)
zeppelin-display实现向前台Angular元素绑定后台数据scala
zeppelin-spark-dependencies无具体功能,maven的最佳实践,将多个module都要依赖的公共类单独抽离出来,供其他module依赖。目前zeppelin-zinterpreter和zeppelin-spark这2个module依赖它
zeppelin-distribution为了将整个项目打包成发布版,而设置的module,打包格式见src/assembly/distribution.xml

zeppelin涉及到框架层面的几个module为:zeppelin-server、zeppelin-zengine、zeppelin-interpreter,并且三者之间有如下的依赖关系:

Zeppelin源码分析——主要的class分析

Notebook -》 InterpreterSettingManager -》RemoteInterpreterEventServer.start 启动thrift客户端服务并接受interpreter 返回的结果

Notebook -》 InterpreterSettingManager -》 解析所有模版interpreterSettingTemplates -〉InterpreterSetting

Interperter

Interpreter是一个抽象类,该类是zeppelin核心类,Zeppelin提供的核心价值:解释执行各种语言的代码,都是通过该抽象类的每个具体的实现类完成的。Interpreter主要规定了各语言repl解释器需要遵循的“规范(contract)”,包括:

  1. repl解释器的生命周期管理。如open(), close(), destroy(),规定了产生和销毁repl解释器。
  2. 解释执行代码的接口——interpreter(),这些真正产生价值的地方。
  3. 执行代码过程中交互控制和易用性增强,如cancel(), getProgress(), completion(),分别是终止代码的执行、获取执行进度以及代码自动完成。
  4. 解释器的配置接口,如setProperty()、setClassLoaderURL(URL[])等。
  5. 性能优化接口,如getScheduler(),getIntepreterGroup()等。
  6. 解释器注册接口(已经deprecated了),如一系列重载的register接口。

以上体现了zeppelin的repl解释器进程需要受其主进程ZeppelinServer的控制,也是zeppelin设计决策在代码中的体现。

注:现在的解释器注册通过如下2种方式进行:

  1. 将interpreter-setting.json打包到解释器的jar文件中

  2. 放置到如下位置:interpreter/{interpreter}/interpreter-setting.json

RemoteInterpreterService

Thrift协议分析

Apache Thrift是跨语言RPC通信框架,提供了相应的DSL(Domain Specific Language)和支持多种语言的代码生成工具,使得代码开发人员可以只关注具体的业务,而不用关注底层的通信细节。zeppelin使用Thrift定义了其主进程ZeppelinServer与需要采用独立JVM进程运行的各repl解释器之间的通信协议。

关于为什么要采用单独的JVM进程来启动repl解释器进程,本系列的第3篇也有提及,这里再赘述一下:

  1. zeppelin旨在提供一个开放的框架,支持多种语言和产品,由于每种语言和产品都是各自独立演进的,各自的运行时依赖也各不相同,甚至是相互冲突的,如果放在同一JVM中,仅解决冲突,维护各个产品之间的兼容性都是一项艰巨的任务,某些产品版本甚至是完全不能兼容的。
  2. 大数据分析,是否具有横向扩展能力是production-ready一项重要的衡量指标,如果将repl进程与主进程合在一起,会严重影响系统性能。

因此,在有必要的时候,zeppelin采用独立JVM的方式来启动repl进程,并且采用Thrift协议定义了主进程与RemoteInterpreterService进程之间的通信协议,具体如下:

service RemoteInterpreterService {



  void createInterpreter(1: string intpGroupId, 2: string sessionId, 3: string className, 4: map<string, string> properties, 5: string userName) throws (1: InterpreterRPCException ex);

  void init(1: map<string, string> properties) throws (1: InterpreterRPCException ex);

  void open(1: string sessionId, 2: string className) throws (1: InterpreterRPCException ex);

  void close(1: string sessionId, 2: string className) throws (1: InterpreterRPCException ex);

  void reconnect(1: string host, 2: i32 port) throws (1: InterpreterRPCException ex);

  RemoteInterpreterResult interpret(1: string sessionId, 2: string className, 3: string st, 4: RemoteInterpreterContext interpreterContext) throws (1: InterpreterRPCException ex);

  void cancel(1: string sessionId, 2: string className, 3: RemoteInterpreterContext interpreterContext) throws (1: InterpreterRPCException ex);

  i32 getProgress(1: string sessionId, 2: string className, 3: RemoteInterpreterContext interpreterContext) throws (1: InterpreterRPCException ex);

  string getFormType(1: string sessionId, 2: string className) throws (1: InterpreterRPCException ex);

  list<InterpreterCompletion> completion(1: string sessionId, 2: string className, 3: string buf, 4: i32 cursor, 5: RemoteInterpreterContext interpreterContext) throws (1: InterpreterRPCException ex);

  void shutdown();



  string getStatus(1: string sessionId, 2:string jobId) throws (1: InterpreterRPCException ex);



  list<string> resourcePoolGetAll() throws (1: InterpreterRPCException ex);

  // get value of resource

  binary resourceGet(1: string sessionId, 2: string paragraphId, 3: string resourceName) throws (1: InterpreterRPCException ex);

  // remove resource

  bool resourceRemove(1: string sessionId, 2: string paragraphId, 3:string resourceName) throws (1: InterpreterRPCException ex);

  // invoke method on resource

  binary resourceInvokeMethod(1: string sessionId, 2: string paragraphId, 3:string resourceName, 4:string invokeMessage) throws (1: InterpreterRPCException ex);



  void angularObjectUpdate(1: string name, 2: string sessionId, 3: string paragraphId, 4: string object) throws (1: InterpreterRPCException ex);

  void angularObjectAdd(1: string name, 2: string sessionId, 3: string paragraphId, 4: string object) throws (1: InterpreterRPCException ex);

  void angularObjectRemove(1: string name, 2: string sessionId, 3: string paragraphId) throws (1: InterpreterRPCException ex);

  void angularRegistryPush(1: string registry) throws (1: InterpreterRPCException ex);



  RemoteApplicationResult loadApplication(1: string applicationInstanceId, 2: string packageInfo, 3: string sessionId, 4: string paragraphId) throws (1: InterpreterRPCException ex);

  RemoteApplicationResult unloadApplication(1: string applicationInstanceId) throws (1: InterpreterRPCException ex);

  RemoteApplicationResult runApplication(1: string applicationInstanceId) throws (1: InterpreterRPCException ex);



}

与前面的Interpreter类的定义进行对比不难发现,RemoteInterpreterService Thrift接口与Interpreter抽象类定义的接口大部分相同,不同之处在于:

  1. RemoteInterpreterService接口的实现类由于运行在不同的JVM中,需要在每个接口方法中额外传递环境信息,如noteId和className等,如createInterpreter、open、close、cancel等。
  2. RemoteInterpreterService接口中多出了两种类型的接口,一种是为了完成ZeppelinServer进程和RemoteInterpreter进程之间的resource协商(neigotiation),如resourceXXX接口;另一种是为了完成2者之间angular object的前后台双向绑定,如augularXXX接口。

具体文件位置见:

${ZEPPELIN_HOME}/zeppelin-interpreter/src/main/thrift/RemoteInterpreterService.thrift。在其同级目录下,zeppelin还提供了代码生成脚本genthrift.sh:



rm -rf gen-java

rm -rf ../java/org/apache/zeppelin/interpreter/thrift

thrift --gen java RemoteInterpreterService.thrift

thrift --gen java RemoteInterpreterEventService.thrift

for file in gen-java/org/apache/zeppelin/interpreter/thrift/* ; do

  cat java_license_header.txt ${file} > ${file}.tmp

  mv -f ${file}.tmp ${file}

done

mv gen-java/org/apache/zeppelin/interpreter/thrift ../java/org/apache/zeppelin/interpreter/thrift

rm -rf gen-java

可以看出,

${ZEPPELIN_HOME}/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift目录下所有文件都是Thrift的代码生成器根据该接口文件自动生成的。如果我们修改过该接口文件,则需要重新执行该脚本。

RemoteInterpreterProcess

RemoteInterpreterProcess是采用独立JVM启动repl进程的具体执行类,它采用Apache Commons Exec框架来根据Zeppelin主进程的”指示”启动独立进程,具体逻辑如下:

参考文档

www.yuque.com/jeffzhangji…

Zeppelin 架构和原理 · 语雀

flink-learning.org.cn/article

Zeppelin

zeppelin.apache.org/docs/latest…

mp.weixin.qq.com/s/K9b4bZ8u4…