本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。
背景
环境为一个中文版的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写入的时候乱码。需要从简单到复杂,逐个排查缩小问题范围。
排查目标日志文件
图中ambari-agent的日志采集过来之后发生了乱码,我们先去看看ambari-agent的log文件是否存在问题。
执行如下命令:
cat /var/log/ambari-agent/ambari-agent.log | grep Errno
结果如图所示

日志中的中文可以正常显示。
使用如下命令查看日志文件的编码格式:
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();
...
}
如代码所示。日志文件的读入和输出分别由OutputManager
和InputManager
负责。
接着观察InputManager
的init
方法:
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
会循环调用每个input
的monitor
方法。
boolean monitor() {
if (isReady()) {
LOG.info("Starting thread. " + getShortDescription());
thread = new Thread(this, getNameForThread());
thread.start();
return true;
} else {
return false;
}
}
monitor启动了一个thread。
继续查看Input
的run
方法。发现在run
方法中调用了start
方法。
我们这里重点关注Input
的子类InputFile
。InputFile
的start
方法如下所示:
@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();
...
}
openLogFile
在InputFile
中,如下所示:
@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这个变量:

发现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
。如果var2
是null
系统通过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。

发现乱码问题已经解决。再次evaluate
br
变量
确定编码格式已经修改为UTF-8格式。
后记
通过上述较为繁琐的debug过程。我们了解了Ambari LogSearch组件的logfeeder的工作方式。同时通过阅读JDK的源码了解到Java处理输入的默认编码格式是通过file.encoding
这个系统变量获取的。
本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。
网友评论