背景
最近業務需要使用Flink, 於是把之前Flink的相關技術拿出來重新回顧一下, 於是想起這個之前一直沒有去解決的問題. 本文主要講解如何解決這一問題以及發生這個問題的根本原因.
運行Flink 官方docker image
此處不多說,訪問docker hub flink官方的Image. 選擇自己需要版本的flink官方鏡像(此處我選的是flink:scala_2.11 因爲要使用到scala shell所以選的scala版本不是最新的) 然後按照官方給的docker-compose 文件簡單改動一下啓動即可
version: "3"
services:
jobmanager:
image: flink:scala_2.11
expose:
- "6123"
ports:
- "8081:8081"
command: jobmanager
environment:
- "JOB_MANAGER_RPC_ADDRESS=jobmanager"
taskmanager:
image: flink:scala_2.11
expose:
- "6121"
- "6122"
depends_on:
- jobmanager
command: taskmanager
links:
- "jobmanager:jobmanager"
environment:
- "JOB_MANAGER_RPC_ADDRESS=jobmanager"
此時訪問web ui localhost:8081 查看 jobmanager 的 logs 和 stdout
file unavailable 無法查看logs 和 stdout. 提交一個wordcount程序
object StreamingJob {
def main(args: Array[String]) {
val env = StreamExecutionEnvironment.getExecutionEnvironment
val textStream = env.fromElements(
"hello word",
"this is flink demo"
)
val counts = textStream.flatMap { _.toLowerCase().split("\\W+") } .map { (_, 1) }.keyBy(0).sum(1)
counts.print()
env.execute("word count")
}
}
查看結果, 運行結束且正常.
查看taskmanager的logs 和 stdout 依然是什麼都沒有, 此程序有print運行結果的,但是實際上並未查看到.
針對taskmanger容器 使用docker logs 查看日誌
docker logs 18a4a82ad9fe(taskmanager container id)
看到日誌裏面有print出來結果
Source: Collection Source -> Flat Map -> Map (1/1) 13fcdc91c88d72a6785c2b3d6f6e23c1.
(hello,1)
(word,1)
(this,1)
(is,1)
(flink,1)
(demo,1)
只是這個結果目前不能在web ui 的 taskmanager stdout上面查看.
解決方案
遇到此類配置相關的問題, 想必大家第一時間就是去stackoverflow上面去找答案. 上面確實有這個問題的答案,我把連接貼到這裏:Apache Flink: The file STDOUT is not available on the TaskExecutor
解決方案裏面更改了docker-entrypoint.sh 文件將jobmanager和taskmanager 的啓動相關參數, 下面是解決方案裏面的改動
下面是改動之前的腳本:
if [ "$1" = "help" ]; then
echo "Usage: $(basename "$0") (jobmanager|${COMMAND_STANDALONE}|taskmanager|help)"
exit 0
elif [ "$1" = "jobmanager" ]; then
shift 1
prepare_job_manager_start
exec $(drop_privs_cmd) "$FLINK_HOME/bin/jobmanager.sh" start-foreground "$@"
elif [ "$1" = ${COMMAND_STANDALONE} ]; then
shift 1
prepare_job_manager_start
exec $(drop_privs_cmd) "$FLINK_HOME/bin/standalone-job.sh" start-foreground "$@"
elif [ "$1" = "taskmanager" ]; then
shift 1
echo "Starting Task Manager"
copy_plugins_if_required
TASK_MANAGER_NUMBER_OF_TASK_SLOTS=${TASK_MANAGER_NUMBER_OF_TASK_SLOTS:-$(grep -c ^processor /proc/cpuinfo)}
set_common_options
set_config_option taskmanager.numberOfTaskSlots ${TASK_MANAGER_NUMBER_OF_TASK_SLOTS}
if [ -n "${FLINK_PROPERTIES}" ]; then
echo "${FLINK_PROPERTIES}" >> "${CONF_FILE}"
fi
envsubst < "${CONF_FILE}" > "${CONF_FILE}.tmp" && mv "${CONF_FILE}.tmp" "${CONF_FILE}"
exec $(drop_privs_cmd) "$FLINK_HOME/bin/taskmanager.sh" start-foreground "$@"
fi
exec "$@"
對比一下兩者的區別, 改動前爲start-foreground
應該是前臺啓動的意思 改動之後是直接start
看樣子像是變成了後臺啓動. 然後在腳本最後執行了一個tail -f XXXX
的命令 表示持續輸出某個文件的內容到console.
懂docker 容器技術的人看到這個tail -f XXX
應該瞬間明白了爲什麼作者要這樣處理. 容器中運行的程序至少一個前臺進程, 如果全是後臺進程,那麼容器一啓動就會立即停止. 所以當我們容器中的程序一定要以後臺進程方式運行的時候,一般容器的啓動腳本中都會加一個tail -f XXXXX
保證有一個前臺程序一直在run, 這樣容器不會被關閉.
所以將docker-entrypoint.sh 執行jobmanager.sh 以及 執行taskmanager.sh的地方更改並在最後加上
exec /bin/bash -c "tail -f $FLINK_HOME/log/*.log"
保證容器中一直有前臺進程運行,改動後的docker-entrypoint.sh 部分如下:
if [ "$1" = "help" ]; then
echo "Usage: $(basename "$0") (jobmanager|${COMMAND_STANDALONE}|taskmanager|help)"
exit 0
elif [ "$1" = "jobmanager" ]; then
shift 1
prepare_job_manager_start
$FLINK_HOME/bin/jobmanager.sh start "$@"
elif [ "$1" = ${COMMAND_STANDALONE} ]; then
shift 1
prepare_job_manager_start
$FLINK_HOME/bin/standalone-job.sh start "$@"
elif [ "$1" = "taskmanager" ]; then
shift 1
echo "Starting Task Manager"
copy_plugins_if_required
TASK_MANAGER_NUMBER_OF_TASK_SLOTS=${TASK_MANAGER_NUMBER_OF_TASK_SLOTS:-$(grep -c ^processor /proc/cpuinfo)}
set_common_options
set_config_option taskmanager.numberOfTaskSlots ${TASK_MANAGER_NUMBER_OF_TASK_SLOTS}
if [ -n "${FLINK_PROPERTIES}" ]; then
echo "${FLINK_PROPERTIES}" >> "${CONF_FILE}"
fi
envsubst < "${CONF_FILE}" > "${CONF_FILE}.tmp" && mv "${CONF_FILE}.tmp" "${CONF_FILE}"
$FLINK_HOME/bin/taskmanager.sh start "$@"
fi
sleep 1
exec /bin/bash -c "tail -f $FLINK_HOME/log/*.log"
用這個文件替換掉官方的docker-entrypoint.sh 構建自己的flink image 替換掉官方的image
運行docker-compose.yml 訪問localhost:8081查看jobmanager的log
發現通過web ui可以查看到日誌了.
同樣提交word count的程序查看stdout運行結果
至此,這個問題已經解決了.
問題根本原因
按照上面的步驟可以解決遇到的問題,但是需要找到問題的根本,是什麼原因導致官方的鏡像不能夠通過web ui查看log和 stdout, 只能夠通過docker logs {container_id}
的方式去查看日誌和運行結果. 追本溯源還是 jobmanager.sh 和 taskmanager.sh 兩個腳本. 改前與改後的區別就是傳參不同, 一個是start-foreground
一個是start
查看jobmanager.sh源碼發現傳入這兩個參數之後真正執行的腳本是不同的
if [[ $STARTSTOP == "start-foreground" ]]; then
exec "${FLINK_BIN_DIR}"/flink-console.sh $ENTRYPOINT "${args[@]}"
else
"${FLINK_BIN_DIR}"/flink-daemon.sh $STARTSTOP $ENTRYPOINT "${args[@]}"
fi
官方鏡像傳入的start-foreground
後執行的腳本是flink-console.sh
而改動之後執行的是flink-daemon.sh. 其實看到這裏,大概就明白七八成了. 官方的方式運行的是flink-console.sh 即是命令行模式, 憑經驗命令行模式內容都打印在命令行裏, 屬於前臺執行進程. 而改動之後執行的是flink-daemnon.sh 表示flink將以後臺進程執行. 這也能解釋在改動的docker-entrypoint.sh 爲什麼最後會加一個tail -f XXXX的命令了. 因爲此處讓flink後臺執行了, 如果不加tail命令, 那麼容器中只有flink這個後臺進程,一啓動會立刻關閉.
在繼續深入下去看flink-console.sh 和 flink-daemon.sh 發現有一段日誌設置相關的代碼比較有意思:
flink-console.sh
log_setting=("-Dlog4j.configuration=file:${FLINK_CONF_DIR}/log4j-console.properties" "-Dlogback.configurationFile=file:${FLINK_CONF_DIR}/logback-console.xml")
以及 flink-daemon.sh
FLINK_LOG_PREFIX="${FLINK_LOG_DIR}/flink-${FLINK_IDENT_STRING}-${DAEMON}-${id}-${HOSTNAME}"
log="${FLINK_LOG_PREFIX}.log"
out="${FLINK_LOG_PREFIX}.out"
log_setting=("-Dlog.file=${log}" "-Dlog4j.configuration=file:${FLINK_CONF_DIR}/log4j.properties" "-Dlogback.configurationFile=file:${FLINK_CONF_DIR}/logback.xml")
這裏在flink-daemon.sh 中有一個 -Dlog.file的配置 log.file 非常關鍵的系統變量.而在flink-console.sh裏面沒有設置這個系統變量.
此時可以查看一下flink下conf裏面關於日誌的配置文件:
比如看log4j.properties
################################################################################
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
################################################################################
# This affects logging for both user code and Flink
log4j.rootLogger=INFO, file
# Uncomment this if you want to _only_ change Flink's logging
#log4j.logger.org.apache.flink=INFO
# The following lines keep the log level of common libraries/connectors on
# log level INFO. The root logger does not override this. You have to manually
# change the log levels here.
log4j.logger.akka=INFO
log4j.logger.org.apache.kafka=INFO
log4j.logger.org.apache.hadoop=INFO
log4j.logger.org.apache.zookeeper=INFO
# Log all infos in the given file
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.file=${log.file}
log4j.appender.file.append=false
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %-60c %x - %m%n
# Suppress the irrelevant (wrong) warnings from the Netty channel handler
log4j.logger.org.apache.flink.shaded.akka.org.jboss.netty.channel.DefaultChannelPipeline=ERROR, file
發現裏面log4j.appender.file.file
正好使用的是 log.file
這個系統變量.
所以這個問題的根源就是這個log.file
系統變量配置問題. 在官方鏡像執行過程中, 由於運行的是flink-console.sh 腳本, 沒有設置這個log.file
因此不會再容器中{FLINK_HOME}/log
下面生成log以及stdout(可以自己啓動官方鏡像,然後進入容器去check,確實沒有log和out文件生成). 而web ui 正好是從這個目錄下獲取log和stdout. 改動之後使用flink-daemon.sh 啓動時, 會去設置這個log.file
系統變量, flink啓動後, log以及out都會存放在{FLINK_HOME}/log
, 使用web ui 就能看到這些結果了. 其實當flink不以容器啓動的時候,都能夠看到log文件夾下面有log和out的文件, 官方鏡像啓動使用flink-console.sh 讓flink前臺啓動,保證容器啓動時有前臺進程,所以導致log和out沒有寫到log文件夾中, 進而也無法通過web ui 去查看log和stdout了.這就是這個問題的根本原因.