Flink on Zeppelin问题四则(并没有优雅的解决方案

前言

最近我们正式调研Zeppelin作为Flink SQL开发套件的可能性,于是clone了最新的Zeppelin v0.10-SNAPSHOT源码,自行编译并部署到了预发布环境的新Flink集群中。Flink版本为1.13.0,Hadoop版本为CDH 6.3.2自带的3.0.0。经过两天的探索,发现了一些问题,在百忙之中抽出点时间简要记录一下并不成功的troubleshooting过程。

Flink Interpreter不加载

安装好Zeppelin并配置好Flink Interpreter的各项参数之后(采用生产环境推荐的Flink on YARN + Interpreter on YARN + Isolated Per Note模式),编写Note无法执行,提示找不到FlinkInterpreter类,如下图所示。

排查:

  • 去${ZEPPELIN_HOME}/interpreter/flink目录下观察,可以发现名为zeppelin-flink-0.10.0-SNAPSHOT-2.11/2.12.jar的两个JAR包,并且FlinkInterpreter已经被正确地打进了JAR包里。
  • 去zeppelin-env.sh中修改ZEPPELIN_JAVA_OPTS环境变量,添加-verbose:class参数打印类加载日志,从中未发现任何以org.apache.zeppelin.flink为前缀的类被加载。
  • 登录Interpreter进程所在的那台NodeManager,查看Interpreter的临时目录,结构如下图。

但是,Interpreter进程的classpath中并没有zeppelin/interpreter/flink/*,自然无法加载Interpreter了。为什么会这样?来到负责启动Interpreter的bin/interpreter.sh文件,第125行:

INTERPRETER_ID=$(basename "${INTERPRETER_DIR}")
if [[ "${INTERPRETER_ID}" != "flink" ]]; then
  # don't add interpreter jar for flink, FlinkInterpreterLauncher will choose the right interpreter jar based
  # on scala version of current FLINK_HOME.
  addJarInDirForIntp "${INTERPRETER_DIR}"
fi

可见这里对Flink做了一个特殊的处理。根据注释的描述,FlinkInterpreterLauncher会根据用户的Flink版本选择对应Scala版本的JAR包。查看该类的源码,确实如此(有一个chooseFlinkAppJar()方法,略去)。然而继续向上追踪FlinkInterpreterLauncher的调用链,发现它并没有在任何与YARN有关的方法中被使用,也就是说上面选择JAR包的动作根本没发生。

由于我们仍然仅使用基于Scala 2.11的Flink,故可以将目录中的2.12包删掉,并修改interpreter.sh注释掉if语句,问题临时解决。更好的解决方法是将上述的选择JAR包逻辑写入YARN Launcher内,但侵入性较大,留待今后操作。

YARN Application模式无效

根据文档描述,YARN Application模式与普通的YARN模式相比会更节省资源,因为JobManager和Interpreter跑在一个Container内,如下图所示。

我们确认与Hadoop相关的各项参数、环境变量都设置好之后,将Note的flink.execution.mode参数改为yarn-application,运行之,报出如下异常。

对比一下上节贴出的Interpreter临时目录结构,容易发现这里的路径是错的。来到FlinkScalaInterpreter类,将flinkHomeflinkConfDirhiveConfDir做如下的修改。

mode = ExecutionMode.withName(
  properties.getProperty("flink.execution.mode", "LOCAL")
    .replace("-", "_")
    .toUpperCase)
if (mode == ExecutionMode.YARN_APPLICATION) {
  if (flinkVersion.isFlink110) {
    throw new Exception("yarn-application mode is only supported after Flink 1.11")
  }
  // use current yarn container working directory as FLINK_HOME, FLINK_CONF_DIR and HIVE_CONF_DIR
  val workingDirectory = new File(".").getAbsolutePath
  flinkHome = workingDirectory + "/flink"
  flinkConfDir = workingDirectory + "/flink/conf"
  hiveConfDir = workingDirectory + "/hive_conf"
}

重新编译打包并替换掉原来的Interpreter包,再次执行,又报出如下异常,提示Application ID为空。

话休絮烦,直接贴出对应的源码:

val (effectiveConfig, cluster) = fetchConnectionInfo(config, configuration, flinkShims)
this.configuration = effectiveConfig
cluster match {
  case Some(clusterClient) =>
    // local mode or yarn
    if (mode == ExecutionMode.LOCAL) {
      LOGGER.info("Starting FlinkCluster in local mode")
      this.jmWebUrl = clusterClient.getWebInterfaceURL
      this.displayedJMWebUrl = this.jmWebUrl
    } else if (mode == ExecutionMode.YARN) {
      LOGGER.info("Starting FlinkCluster in yarn mode")
      this.jmWebUrl = clusterClient.getWebInterfaceURL
      val yarnAppId = HadoopUtils.getYarnAppId(clusterClient)
      this.displayedJMWebUrl = getDisplayedJMWebUrl(yarnAppId)
    } else {
      throw new Exception("Starting FlinkCluster in invalid mode: " + mode)
    }
  case None =>
    // yarn-application mode
    if (mode == ExecutionMode.YARN_APPLICATION) {
      // get yarnAppId from env `_APP_ID`
      val yarnAppId = System.getenv("_APP_ID")
      LOGGER.info("Use FlinkCluster in yarn application mode, appId: {}", yarnAppId)
      this.jmWebUrl = "http://localhost:" + HadoopUtils.getFlinkRestPort(yarnAppId)
      this.displayedJMWebUrl = getDisplayedJMWebUrl(yarnAppId)
    } else {
      LOGGER.info("Use FlinkCluster in remote mode")
      this.jmWebUrl = "http://" + config.host.get + ":" + config.port.get
      this.displayedJMWebUrl = getDisplayedJMWebUrl("")
    }
}

@Internal
def fetchConnectionInfo(
    config: Config,
    flinkConfig: Configuration,
    flinkShims: FlinkShims): (Configuration, Option[ClusterClient[_]]) = {
  config.executionMode match {
    case ExecutionMode.LOCAL => createLocalClusterAndConfig(flinkConfig)
    case ExecutionMode.REMOTE => createRemoteConfig(config, flinkConfig)
    case ExecutionMode.YARN => createYarnClusterIfNeededAndGetConfig(config, flinkConfig, flinkShims)
    case ExecutionMode.YARN_APPLICATION => (flinkConfig, None)
    case ExecutionMode.UNDEFINED => // Wrong input
      throw new IllegalArgumentException("please specify execution mode:\n" +
        "[local | remote <host> <port> | yarn | yarn-application ]")
  }
}

上面的代码有些令人迷惑:为什么YARN Application模式下没有做任何操作,只是返回了一个空的ClusterClient?另外,_APP_ID是Flink ApplicationMaster启动时设置的环境变量,这样操作一定可以拿得到么?

当然这个问题比较复杂,笔者也尚未认真研究过YARN Application模式相关的源码,需要时间来处理。但可以肯定至少在我们的环境下,需要做较大的改动才能让它正常使用。在完全解决之前,仍然采用传统YARN模式也无伤大雅。

配置Note只读权限后无法切换视图

为了保证安全,我们强制新建的Note都为私有(即Reader、Writer、Runner、Owner初始值都是用户自己),然后按需对相关同学开放权限。

一般情况下,所有人都可以读Note。但是只将Reader权限放开后,除Owner之外的人看到的都是白板。这是因为Note对只读权限者变成了report视图,只能看到结果,不展示SQL源码,如下图所示。

但是,如果尝试切换成default视图,就会提示需要Writer权限才可以:

这就有些匪夷所思了。将Zeppelin日志等级设为DEBUG,重复切换视图操作,可以发现在NotebookServer的事件循环里产生了NOTE_UPDATE事件。

DEBUG [2021-07-28 19:03:52,097] ({qtp306612792-317} NotebookServer.java[onMessage]:255) - RECEIVE: NOTE_UPDATE, RECEIVE PRINCIPAL: bigdata_dev, RECEIVE TICKET: f9118802-14cd-40fc-8e60-caeb0267aac2, RECEIVE ROLES: ["role1"], RECEIVE DATA: {id=2GE65N3RS, name=WorkflowAliBinlog, config={isZeppelinNotebookCronEnable=false, looknfeel=default, personalizedMode=false}}
 WARN [2021-07-28 19:03:52,098] ({qtp306612792-317} SimpleServiceCallback.java[onFailure]:50) - HTTP 403 Forbidden

这是因为Note的视图风格直接存储在.zpln文件内(叫做looknfeel),所以修改它就相当于修改Note了 = =

将NOTE_UPDATE的权限赋给Reader显然不现实,考虑到我们几乎不会用到simple和report视图,将simple视图作为只读的情况比较合适。

但是,来到zeppelin-web项目下之后,发现代码只读性、代码编辑器的可见性和视图之间的耦合过紧,改了数十处HTML和JS代码之后仍然未能达到想要的效果。经过试验,只读用户还是可以看到非HEAD commit的代码的,切换版本凑合也能用,此事暂时搁置。

Zeppelin日志被"Saving note"信息淹没

我们采用的Notebook Repo是GitNotebookRepo(本地)+FileSystemNotebookRepo(远程HDFS)的组合。启动了几个Flink SQL任务之后,在Zeppelin日志中看到如下格式的信息刷屏。

INFO [2021-07-30 19:32:10,160] ({pool-11-thread-16} VFSNotebookRepo.java[save]:144) - Saving note 2GDVAVC4W to etl-mq/analytics_access_log_app_2GDVAVC4W.zpln

这是因为在每个Note对应作业的JobManager中,都会启动一个名为FlinkJobProgressPoller的线程,以zeppelin.flink.job.check_interval的间隔(默认1秒,我们改成了5秒)检查并更新任务的状态。如上一节所述,这些信息也都保存在.zpln文件内,所以会导致频繁写文件。并且这个线程做的事情非常多,代码如下所示。

@Override
public void run() {
  while (!Thread.currentThread().isInterrupted() && running.get()) {
    JsonNode rootNode = null;
    try {
      synchronized (running) {
        running.wait(checkInterval);
      }
      rootNode = Unirest.get(flinkWebUrl + "/jobs/" + jobId.toString())
              .asJson().getBody();
      JSONArray vertices = rootNode.getObject().getJSONArray("vertices");
      int totalTasks = 0;
      int finishedTasks = 0;
      for (int i = 0; i < vertices.length(); ++i) {
        JSONObject vertex = vertices.getJSONObject(i);
        totalTasks += vertex.getInt("parallelism");
        finishedTasks += vertex.getJSONObject("tasks").getInt("FINISHED");
      }
      LOGGER.debug("Total tasks:" + totalTasks);
      LOGGER.debug("Finished tasks:" + finishedTasks);
      if (finishedTasks != 0) {
        this.progress = finishedTasks * 100 / totalTasks;
        LOGGER.debug("Progress: " + this.progress);
      }
      String jobState = rootNode.getObject().getString("state");
      if (jobState.equalsIgnoreCase("finished")) {
        break;
      }

      long duration = rootNode.getObject().getLong("duration") / 1000;
      if (isStreamingInsertInto) {
        if (isFirstPoll) {
          StringBuilder builder = new StringBuilder("%angular ");
          builder.append("<h1>Duration: {{duration}} </h1>");
          builder.append("\n%text ");
          context.out.clear(false);
          context.out.write(builder.toString());
          context.out.flush();
          isFirstPoll = false;
        }
        context.getAngularObjectRegistry().add("duration",
                toRichTimeDuration(duration),
                context.getNoteId(),
                context.getParagraphId());
      }

      // fetch checkpoints info and save the latest checkpoint into paragraph's config.
      rootNode = Unirest.get(flinkWebUrl + "/jobs/" + jobId.toString() + "/checkpoints")
              .asJson().getBody();
      if (rootNode.getObject().has("latest")) {
        JSONObject latestObject = rootNode.getObject().getJSONObject("latest");
        if (latestObject.has("completed") && latestObject.get("completed") instanceof JSONObject) {
          JSONObject completedObject = latestObject.getJSONObject("completed");
          if (completedObject.has("external_path")) {
            String checkpointPath = completedObject.getString("external_path");
            LOGGER.debug("Latest checkpoint path: {}", checkpointPath);
            if (!StringUtils.isBlank(checkpointPath) && !checkpointPath.equals(latestCheckpointPath)
              Map<String, String> config = new HashMap<>();
              config.put(LATEST_CHECKPOINT_PATH, checkpointPath);
              context.getIntpEventClient().updateParagraphConfig(
                      context.getNoteId(), context.getParagraphId(), config);
              latestCheckpointPath = checkpointPath;
            }
          }
        }
      }
    } catch (Exception e) {
      LOGGER.error("Fail to poll flink job progress via rest api", e);
    }
  }
}

考虑到直接对它下手的复杂度,目前只能暂时在log4j配置中屏蔽掉VFSNotebookRepo的INFO日志输出。随着今后任务增多,会继续评估Zeppelin Server和磁盘的压力,并尽可能寻找优化的方法。

The End

其实还有个Maven Repo解析与添加Nexus私服认证方面的问题,但这个更复杂,并且与Flink无关,就不废话了。

民那周末快乐~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容