美文网首页玩转大数据
记一次Ambari LogSearch乱码问题的排查过程

记一次Ambari LogSearch乱码问题的排查过程

作者: AlienPaul | 来源:发表于2019-10-29 16:02 被阅读0次

本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。

背景

环境为一个中文版的Linux系统,ARM64平台,安装了Ambari大数据平台。一次偶然的发现,在Ambari Logsearch查询ambari-agent的日志中存在中文字符乱码的情况。如下图所示:

发生乱码的地方

问题排查

依据Ambari Logsearch官方文档所述,Logsearch收集到的日志数据存储于Ambari Infra的Solr当中。因此,首先需要查看Solr中存储的日志信息是否存在乱码。
进入到Ambari Infra的Solr管理页面。左侧菜单选择hadoop_logs_shard0_replica1,具体shard和replica视所运行的环境而定。
下面检索出我们需要的日志信息,在上个截图中我们发现出错的信息包含有Errno字样。我们检索条件q使用

log_message:*Errno*

检索结果如图所示:


Solr检索结果

很明显,数据在存入Solr时已经存在乱码了。
这样看来,乱码可能发生于两个地方,日志采集的时候乱码或Solr写入的时候乱码。需要从简单到复杂,逐个排查缩小问题范围。

排查目标日志文件

图中ambari-agent的日志采集过来之后发生了乱码,我们先去看看ambari-agent的log文件是否存在问题。
执行如下命令:

cat /var/log/ambari-agent/ambari-agent.log | grep Errno

结果如图所示


ambari-agent.log

日志中的中文可以正常显示。
使用如下命令查看日志文件的编码格式:

root@manager# file -i /var/log/ambari-agent/ambari-agent.log
/var/log/ambari-agent/ambari-agent.log: text/plain; charset=utf-8

日志文件为utf-8格式,没有什么问题。

系统的默认编码格式也是需要验证的,可以执行env命令,输出如下:

LANG=zh_CN.UTF-8
LANGUAGE=zh_CN:zh

默认编码并不存在问题。

接下来我们尝试向Solr中写入自己造的一段含有中文的数据。

新建一个maven项目,加入如下依赖:

<dependency>
        <groupId>org.apache.solr</groupId>
        <artifactId>solr-solrj</artifactId>
        <version>5.5.2</version>
</dependency>

Ambari Infra的Solr使用的是5.5.2版本,因此我们要引入5.5.2版本的solrj库。
编写如下代码:

HttpSolrClient client = new HttpSolrClient("http://10.180.210.10:8886/solr/hadoop_logs");
SolrInputDocument input = new SolrInputDocument();
input.addField("cluster", "Test Cluster");
input.addField("level", "ERROR");
input.addField("event_count", 1);
input.addField("ip", "10.180.210.10");
input.addField("type", "ambari_agent");
input.addField("seq_no", 0);
input.addField("path", "/var/log/ambari-agent/ambari-agent.log");
input.addField("file", "xxx.py");
input.addField("line_number", 1);
input.addField("host", "manager10.bigdata");
input.addField("message_md5", "中文测试 Chinese test");
input.addField("logtime", "2019-10-23T02:51:42.918Z");
input.addField("event_md5", "15717991029189201692080535023729");
input.addField("logfile_line_number", 1);

input.addField("_router_field_", "shard0");

String uuid = UUID.randomUUID().toString();
input.addField("id", uuid);

client.add(input);
client.commit();
client.close();

我们插入数据的message_md5字段中含有中文。
再次打开Solr的管理页面,将这条数据查询出来,如下图所示:

检索自己加入的测试数据

可以看到中文入库时候并没有乱码问题。

问题深入

日志文件的源头和Solr入库时候数据均不存在乱码问题。可以确定乱码发生在日志采集的过程中。Ambari Logsearch组件的日志采集工作是logfeeder负责的。下面需要打开logfeeder模块的源码进行分析。

LogFeeder.java为LogFeeder启动的入口。从main方法逐个跟随,可以到init方法:

private void init() throws Throwable {
  ...
  loadConfigFiles();
  addSimulatedInputs();
  mergeAllConfigs();
  ...
  outputManager.init();
  inputManager.init();
  ...
}

如代码所示。日志文件的读入和输出分别由OutputManagerInputManager负责。

接着观察InputManagerinit方法:

public void init() {
  ...
  // InputManager维护了一个inputList
  for (Input input : inputList) {
    try {
      // 逐个初始化所有的input
      input.init()
      ...
    } cache (Exception e) {
      ...
    }
  }
  ...
}

如注释所示InputManager维护了一个inputList。该变量在之前读取配置文件的时候初始化。
观察源码列表,Input有如下3个子类:

  • InputFile
  • InputS3File
  • InputSimulate

接下来我们在Ambari管理页面中打开logfeeder的配置文件,发现如下片段:

{
  "input":[
    {
      "type":"ambari_agent",
      "rowtype":"service",
      "path":"{{ambari_agent_log_dir}}/ambari-agent.log"
    },
    {
      ...
    }
    ]
}

ambari-agent.log文件全是对应了一个Input,因为ambari-agent.log是一个普通的file,它是由InputFile负责读取的。

然后LogFeeder会调用monitor方法:

public void monitor() {
  for (Input input : inputList) {
    if (input.isReady()) {
      input.monitor();
    }
    ...
  }
  ...
}

monitor会循环调用每个inputmonitor方法。

  boolean monitor() {
    if (isReady()) {
      LOG.info("Starting thread. " + getShortDescription());
      thread = new Thread(this, getNameForThread());
      thread.start();
      return true;
    } else {
      return false;
    }
  }

monitor启动了一个thread。
继续查看Inputrun方法。发现在run方法中调用了start方法。
我们这里重点关注Input的子类InputFileInputFilestart方法如下所示:

  @Override
  void start() throws Exception {
    boolean isProcessFile = getBooleanValue("process_file", true);
    if (isProcessFile) {
      if (tail) {
        processFile(logFiles[0]);
      } else {
        for (File file : logFiles) {
          try {
            processFile(file);
            if (isClosed() || isDrain()) {
              LOG.info("isClosed or isDrain. Now breaking loop.");
              break;
            }
          } catch (Throwable t) {
            LOG.error("Error processing file=" + file.getAbsolutePath(), t);
          }
        }
      }
      close();
    } else {
      copyFiles(logFiles);
    }
  }

该方法针对所有的logFiles,逐个调用processFile来处理他们。

processFile的代码在AbstractInputFile中,如下所示,不关注的代码已省略:

protected void processFile(File logPathFile) throws FileNotFoundException, IOException {
  ...
  BufferedReader br = null;
  ...
  br = openLogFile(logPathFile);
  ...
  String line = br.readLine();
  ...
}

openLogFileInputFile中,如下所示:

  @Override
  protected BufferedReader openLogFile(File logFile) throws FileNotFoundException {
    BufferedReader br = new BufferedReader(LogsearchReaderFactory.INSTANCE.getReader(logFile));
    fileKey = getFileKey(logFile);
    base64FileKey = Base64.byteArrayToBase64(fileKey.toString().getBytes());
    LOG.info("fileKey=" + fileKey + ", base64=" + base64FileKey + ". " + getShortDescription());
    return br;
  }

紧接着查看LogsearchReaderFactory.INSTANCE.getReader方法

  public Reader getReader(File file) throws FileNotFoundException {
    LOG.debug("Inside reader factory for file:" + file);
    if (GZIPReader.isValidFile(file.getAbsolutePath())) {
      LOG.info("Reading file " + file + " as gzip file");
      return new GZIPReader(file.getAbsolutePath());
    } else {
      return new FileReader(file);
    }
  }

返回的是new FileReader(file)
分析到这里,可以尝试在processFile

 String line = br.readLine();

这一行加入断点进行远程调试。

在这之前,需要先找到logsearch的启动入口,执行如下命令

vim /var/lib/ambari-server/resources/common-services/LOGSEARCH/0.5.0/package/scripts/logfeeder.py

发现如下方法

  def start(self, env, upgrade_type=None):
    import params
    env.set_params(params)
    self.configure(env)

    Execute((format('{logfeeder_dir}/run.sh'),),
            environment={'LOGFEEDER_INCLUDE': format('{logsearch_logfeeder_conf}/logfeeder-env.sh')},
            sudo=True)

这里调用的是logfeeder安装目录中的run.sh。查看logfeeder的安装包或者用搜索可知logfeeder的安装目录位于/usr/lib/ambari-logsearch-logfeeder
先配置好idea的远程调试,编译/usr/lib/ambari-logsearch-logfeeder/run.sh文件。修改logfeeder的启动命令行,加入idea提示修改的启动参数,最终如下所示:

nohup $JAVA -cp "$LOGFEEDER_CLI_CLASSPATH:$LOGFEEDER_CONF_DIR:$script_dir/libs/*:$script_dir/classes" $LOGFEEDER_GC_OPTS $LOGFEEDER_JAVA_MEM $LOGFEEDER_JAVA_OPTS $JMX -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 org.apache.ambari.logfeeder.LogFeeder $* > $LOGFILE 2>&1 &

重启logfeeder,运行到断点处,如图所示:

断点调试
果然,读取进来的日志就是乱码的。
我们再evaluate一下br这个变量:
br变量
发现cs果然有问题,编码格式竟然是US-ASCII编码的,乱码的原因在此。
那么问题来了,代码中没有明确给定采用的编码格式,配置文件中也没有,Java默认是怎样获取默认的编码格式的呢?

查看JDK中FileReader的构造函数源码,发现FileReader集成自InputStreamReader

public class FileReader extends InputStreamReader {
    public FileReader(String fileName) throws FileNotFoundException {
        super(new FileInputStream(fileName));
    }
    ...
}

查看父类InputStreamReader对应的构造函数

public InputStreamReader(InputStream in) {
    super(in);
    try {
        sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
    } catch (UnsupportedEncodingException e) {
        // The default encoding should always be available
        throw new Error(e);
    }
}

重点是

sd = StreamDecoder.forInputStreamReader(in, this, (String)null);

这一行。

进入forInputStreamReader方法:

public static StreamDecoder forInputStreamReader(InputStream var0, Object var1, String var2) throws UnsupportedEncodingException {
    String var3 = var2;
    if (var2 == null) {
        var3 = Charset.defaultCharset().name();
    }

    try {
        if (Charset.isSupported(var3)) {
            return new StreamDecoder(var0, var1, Charset.forName(var3));
        }
    } catch (IllegalCharsetNameException var5) {
    }

    throw new UnsupportedEncodingException(var3);
}

我们调用这个方法的时候var2传入的是null。如果var2null系统通过Charset.defaultCharset().name()来获取默认的编码。
接着进入Charset.defaultCharset()方法:

public static Charset defaultCharset() {
    if (defaultCharset == null) {
        synchronized (Charset.class) {
            String csn = AccessController.doPrivileged(
                new GetPropertyAction("file.encoding"));
            Charset cs = lookup(csn);
            if (cs != null)
                defaultCharset = cs;
            else
                defaultCharset = forName("UTF-8");
        }
    }
    return defaultCharset;
}

最终我们发现Java读取的是file.encoding这个系统变量。
按道理来说,系统的LANG环境变量配置了默认的UTF-8编码,这里获取的应该是正确的,但是在这台arm服务器上并非如此。

我们使用

ps -ef | grep logfeeder

命令查看logfeeder的运行参数,发现Ambari也没有为它指定file.encoding这个系统变量。
我们修改logfeeder的启动脚本run.sh文件。加入-Dfile.encoding=UTF-8,如下所示:

nohup $JAVA -cp "$LOGFEEDER_CLI_CLASSPATH:$LOGFEEDER_CONF_DIR:$script_dir/libs/*:$script_dir/classes" $LOGFEEDER_GC_OPTS $LOGFEEDER_JAVA_MEM $LOGFEEDER_JAVA_OPTS $JMX -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Dfile.encoding=UTF-8 org.apache.ambari.logfeeder.LogFeeder $* > $LOGFILE 2>&1 &

重启服务后再次debug。

修改启动脚本后debug
发现乱码问题已经解决。再次evaluate br变量
再次evaluate br变量
确定编码格式已经修改为UTF-8格式。

后记

通过上述较为繁琐的debug过程。我们了解了Ambari LogSearch组件的logfeeder的工作方式。同时通过阅读JDK的源码了解到Java处理输入的默认编码格式是通过file.encoding这个系统变量获取的。

本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。

相关文章

网友评论

    本文标题:记一次Ambari LogSearch乱码问题的排查过程

    本文链接:https://www.haomeiwen.com/subject/tfowvctx.html