本文已参与「新人创作礼」活动,一起开启掘金创作之路。
了解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如下:
- 前台 : AngularJS、Node.JS、WebSocket、Grunt、Bower、Highlight.js、BootStrap3js
- 后台 : 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.template | zeppelin的配置模板,被注释掉的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-web | AngluarJS开发的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)”,包括:
- repl解释器的生命周期管理。如open(), close(), destroy(),规定了产生和销毁repl解释器。
- 解释执行代码的接口——interpreter(),这些真正产生价值的地方。
- 执行代码过程中交互控制和易用性增强,如cancel(), getProgress(), completion(),分别是终止代码的执行、获取执行进度以及代码自动完成。
- 解释器的配置接口,如setProperty()、setClassLoaderURL(URL[])等。
- 性能优化接口,如getScheduler(),getIntepreterGroup()等。
- 解释器注册接口(已经deprecated了),如一系列重载的register接口。
以上体现了zeppelin的repl解释器进程需要受其主进程ZeppelinServer的控制,也是zeppelin设计决策在代码中的体现。
注:现在的解释器注册通过如下2种方式进行:
-
将interpreter-setting.json打包到解释器的jar文件中
-
放置到如下位置:interpreter/{interpreter}/interpreter-setting.json
RemoteInterpreterService
Thrift协议分析
Apache Thrift是跨语言RPC通信框架,提供了相应的DSL(Domain Specific Language)和支持多种语言的代码生成工具,使得代码开发人员可以只关注具体的业务,而不用关注底层的通信细节。zeppelin使用Thrift定义了其主进程ZeppelinServer与需要采用独立JVM进程运行的各repl解释器之间的通信协议。
关于为什么要采用单独的JVM进程来启动repl解释器进程,本系列的第3篇也有提及,这里再赘述一下:
- zeppelin旨在提供一个开放的框架,支持多种语言和产品,由于每种语言和产品都是各自独立演进的,各自的运行时依赖也各不相同,甚至是相互冲突的,如果放在同一JVM中,仅解决冲突,维护各个产品之间的兼容性都是一项艰巨的任务,某些产品版本甚至是完全不能兼容的。
- 大数据分析,是否具有横向扩展能力是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抽象类定义的接口大部分相同,不同之处在于:
- RemoteInterpreterService接口的实现类由于运行在不同的JVM中,需要在每个接口方法中额外传递环境信息,如noteId和className等,如createInterpreter、open、close、cancel等。
- 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主进程的”指示”启动独立进程,具体逻辑如下: