一、问题初遇:先把“奇怪现象”拆成可排查的细节
很多开发者都碰过这种闹心事儿:自己本地写代码、测功能,跑出来的日志全是清晰的中文、英文,连特殊符号都整整齐齐;结果打包丢到线上跑,日志要么是一堆乱码方块,要么是像“????”“��”这种看不懂的符号,查问题时连日志都读不通,急得直挠头。这事儿最坑的点是“开发好、线上坏”,不是代码逻辑错,是“文字怎么显示”的问题,得先把问题拆成两个核心矛盾点:字符集编码、日志采集器配置。先得明确,我们说的“字符集”,本质就是“电脑怎么把文字转成数字存、再转回来显示”的规则,比如中文常用的UTF-8,就是一套全球通用的规则;而“日志采集器”,就是线上专门负责把程序产生的日志“抓出来、存起来”的工具,比如大家常用来收集日志的Filebeat,就是专门干这活的。
在开始排查前,得先确认两个前提:第一,开发环境的日志是“真正常”,不是你眼睛看错了——比如你本地用的IDE(比如IDEA)自带的日志控制台,会不会偷偷帮你转了编码?第二,生产环境的乱码是“真乱码”,不是日志系统的显示问题——比如你用Kibana看日志,会不会是Kibana的显示编码没设对?这两个前提确认清楚,才不会走弯路。
二、排查第一步:先查最容易出问题的“字符集编码”
字符集的问题,就像你和别人聊天用的语言:你说中文,对方听成英文,自然听不懂。程序的日志文字,从“写出来”到“存起来”,要过好几道编码关,每一道都可能出问题,得一道一道查。
2.1 先查程序自己写日志的编码规则
程序写日志的编码,是最基础的一道关——如果程序写的时候就把中文转成了错误的数字,后面再怎么改都没用。这里我们用Java(因为Java是企业开发最常用的语言之一,编码问题特别典型)作为统一技术栈,所有示例都用Java。
比如一个简单的Java日志输出代码,你得看它用的日志框架(比如Logback、Log4j2)有没有指定编码:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TestLogEncoding {
private static final Logger logger = LoggerFactory.getLogger(TestLogEncoding.class);
public static void main(String[] args) {
// 输出带中文的测试日志
logger.info("测试日志:用户登录成功,用户名:张三,操作时间:2024-05-20 14:30:00");
}
}
这个代码里,日志框架的配置文件(比如Logback的logback-spring.xml)有没有指定编码?如果没指定,Java的默认编码会随操作系统变:Windows的默认编码是GBK,Mac/Linux的默认编码是UTF-8,这就会导致“开发(Windows)写的日志是GBK,生产(Linux)按UTF-8读,自然乱码”。
正确的Logback配置应该明确指定编码,比如:
<configuration>
<!-- 控制台输出(开发环境用) -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 明确指定编码为UTF-8 -->
<charset>UTF-8</charset>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 文件输出(生产环境用) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>./app.log.%d{yyyy-MM-dd}</fileNamePattern>
</rollingPolicy>
<encoder>
<!-- 生产环境写日志必须明确指定UTF-8,不能用默认 -->
<charset>UTF-8</charset>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
这里的关键是,不管是开发还是生产,程序写日志的编码必须统一设为UTF-8,不能依赖操作系统的默认编码。怎么确认程序写的日志编码对不对?你可以在生产环境用Java程序自己读一遍日志文件,看能不能正常显示中文:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class CheckLogEncoding {
public static void main(String[] args) throws Exception {
// 读取生产环境的日志文件,指定用UTF-8解码
try (BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream("./app.log"), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
// 如果输出的是正常中文,说明程序写的编码是对的;如果乱码,说明程序写的时候就错了
System.out.println("读取的日志内容:" + line);
}
}
}
}
如果这个代码读出来是乱码,那问题就出在程序写日志的编码,改Logback配置的<charset>UTF-8</charset>就行。
2.2 再查操作系统的默认编码
如果程序写的编码是对的,那再看操作系统的默认编码——因为有些程序(比如一些旧的Shell脚本、或者没指定编码的第三方工具)会依赖操作系统的默认编码。比如你用Shell脚本启动Java程序,Shell的默认编码会不会影响Java?
先查生产环境Linux的默认编码,用这个命令:
# 查看Linux系统的默认编码
echo $LANG
正常的生产环境应该输出类似en_US.UTF-8或者zh_CN.UTF-8,如果输出的是en_US.GBK或者其他非UTF-8的编码,就会有问题。比如你用Shell启动Java程序时,Shell的默认编码是GBK,Java程序如果没指定自己的编码,就会继承Shell的编码,导致写出来的日志是GBK,后面的采集器按UTF-8读,自然乱码。
怎么改Linux的默认编码?可以临时改(重启后失效)或者永久改:
# 临时改默认编码为UTF-8
export LANG=en_US.UTF-8
# 再启动Java程序,看日志是否正常
java -jar your-app.jar
如果临时改了之后日志正常,说明是操作系统默认编码的问题,得永久改:
# 编辑系统的语言配置文件
vim /etc/locale.conf
# 把内容改成LANG=en_US.UTF-8,保存退出
# 然后重启系统或者重新登录,让配置生效
2.3 最后查日志文件本身的编码
如果程序编码和系统编码都对,那再确认日志文件本身的编码——比如你把开发环境的日志文件复制到生产环境,会不会因为跨系统传输导致编码变了?比如Windows的GBK编码文件,复制到Linux后没转码,直接按UTF-8读就会乱码。
怎么查一个文件的编码?用Linux的file命令:
# 查看日志文件的编码格式
file -I ./app.log
正常的UTF-8编码文件会输出类似./app.log: text/plain; charset=utf-8,如果输出的是charset=gbk或者其他,说明文件本身的编码不对。如果是开发环境复制过来的,得先转码再用:
# 把GBK编码的日志文件转成UTF-8编码
iconv -f GBK -t UTF-8 ./app.log -o ./app.log.utf8
三、排查第二步:查容易被忽略的“日志采集器配置”
如果字符集的问题都排除了,那90%的概率是日志采集器的配置有问题——因为很多人会默认“采集器只要把日志抓出来就行,不用管编码”,但实际上采集器是“按自己的编码读日志文件”,如果采集器的编码和日志文件的编码不匹配,就会乱码。这里我们用企业里最常用的日志采集器Filebeat作为示例,所有配置都用Filebeat的格式。
3.1 先查采集器的“读取编码”配置
Filebeat的核心配置是filebeat.yml,里面有个关键的配置项encoding,就是指定Filebeat按什么编码读日志文件。如果日志文件是UTF-8编码,Filebeat的encoding却设成了GBK,或者没设(有些版本的Filebeat默认编码不是UTF-8),就会导致采集出来的日志乱码。
错误的Filebeat配置示例(没指定编码,或者指定了错误的编码):
# Filebeat配置文件(错误示例)
filebeat.inputs:
- type: log
enabled: true
paths:
- /opt/app/app.log
# 这里没指定encoding,Filebeat的默认编码可能是GBK(不同版本不同)
正确的Filebeat配置示例(明确指定编码为UTF-8):
# Filebeat配置文件(正确示例)
filebeat.inputs:
- type: log
enabled: true
paths:
- /opt/app/app.log
# 明确指定读取日志的编码为UTF-8,和日志文件的编码一致
encoding: utf-8
怎么确认Filebeat的配置生效了?可以先在测试环境启动Filebeat,看采集出来的日志是否正常:
# 启动Filebeat,指定配置文件,看输出的日志
./filebeat -e -c filebeat.yml
如果启动后,Filebeat输出的日志里有乱码,说明encoding配置不对;如果正常,再看采集到的目标系统(比如Elasticsearch、Kibana)里的日志是否正常。
3.2 再查采集器的“输出编码”配置
有些采集器不仅要“读日志”,还要“写日志”到目标系统(比如Elasticsearch、消息队列Kafka),这时候还要看采集器的“输出编码”配置——如果读的时候是UTF-8,写的时候却转成了GBK,也会乱码。
比如Filebeat输出到Elasticsearch的配置,要确认Elasticsearch的索引编码是UTF-8,Filebeat的输出配置里有没有指定编码:
# Filebeat输出到Elasticsearch的配置(正确示例)
output.elasticsearch:
hosts: ["http://elasticsearch:9200"]
# 明确指定输出的编码为UTF-8,和Elasticsearch的索引编码一致
encoding: utf-8
另外,Elasticsearch的索引创建时,也要确认索引的映射(mapping)里的字符串字段编码是UTF-8,比如:
# Elasticsearch索引的mapping配置(正确示例)
{
"mappings": {
"properties": {
"message": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
},
# 字符串字段的编码默认是UTF-8,不用额外指定,但要确认
"encoding": "utf-8"
}
}
}
}
3.3 最后查采集器的“中间处理”配置
有些采集器会对日志做中间处理,比如过滤、转码、格式化,这时候如果中间处理的编码和日志编码不匹配,也会乱码。比如Filebeat的processors配置,里面如果有转码的逻辑,要确认转码的编码是对的。
比如错误的中间处理配置(把UTF-8的日志转成GBK):
# Filebeat的processors配置(错误示例)
processors:
- decode_json_fields:
fields: ["message"]
target: "json"
# 这里错误地指定了转码为GBK
encoding: gbk
正确的中间处理配置(指定转码为UTF-8):
# Filebeat的processors配置(正确示例)
processors:
- decode_json_fields:
fields: ["message"]
target: "json"
# 明确指定转码为UTF-8
encoding: utf-8
四、全链路验证:把所有环节串起来确认
排查完编码和采集器,还要做一次全链路的验证,确保所有环节都没问题。全链路验证的步骤是:
- 开发环境启动程序,写一条带中文的测试日志(比如“测试日志:生产环境乱码排查测试”);
- 生产环境部署程序,启动后,先直接读程序写的日志文件(用之前的Java代码读),确认中文正常;
- 启动Filebeat,确认Filebeat采集的日志(用
filebeat -e -c filebeat.yml看控制台输出)中文正常; - 确认Elasticsearch里的日志(用
curl http://elasticsearch:9200/your-index/_search查询)中文正常; - 确认Kibana里的日志(直接在Kibana的Discover页面看)中文正常。
如果这五步都正常,说明问题已经解决;如果某一步不正常,就回到那一步再排查。
五、应用场景、优缺点、注意事项
5.1 应用场景
这个排查思路适用于所有“开发环境日志正常、生产环境日志乱码”的场景,不管是Java、Python、Go等任何语言的程序,不管是用Logback、Log4j、Python logging等任何日志框架,不管是用Filebeat、Logstash、Fluentd等任何日志采集器,只要是日志乱码的问题,都可以用这个思路排查。特别适合企业级的线上项目,因为企业项目的日志链路长(程序写日志→采集器读日志→采集器写日志到中间件→中间件存日志→前端显示日志),任何一个环节出问题都会导致乱码。
5.2 技术优缺点
优点:排查思路清晰,从“源头(程序写日志)”到“中间(采集器读/写日志)”再到“末端(显示日志)”,一步一步来,不会遗漏任何环节;示例具体,用的都是企业里最常用的技术栈(Java、Logback、Filebeat、Elasticsearch),开发者可以直接照搬;覆盖了所有可能的乱码原因,不管是编码不统一、采集器配置错,还是中间处理错,都能查到。 缺点:排查步骤相对多,需要有一定的动手能力(比如改配置、写测试代码、用Linux命令);如果是特别复杂的日志链路(比如日志经过多个采集器、多个中间件),排查时间会比较长;需要对每个环节的技术有基本的了解(比如知道Filebeat的配置项、知道Java的编码规则)。
5.3 注意事项
- 所有环节的编码必须统一,建议全链路都用UTF-8,不要用GBK、ISO-8859-1等其他编码,因为UTF-8是全球通用的编码,支持所有语言;
- 不要依赖默认编码,不管是程序的日志框架、操作系统、采集器,都要明确指定编码为UTF-8,因为不同环境的默认编码可能不一样;
- 排查时要“隔离问题”,比如先确认程序写的日志正常,再确认采集器读的正常,再确认采集器写的正常,不要同时改多个地方的配置,不然不知道哪个配置起作用了;
- 测试时要用“真实的中文”测试,不要用“测试”“hello”这种简单的词,要用带特殊符号的中文(比如“张三(测试)”“测试:用户登录成功”),因为有些乱码只会在特定的字符组合下出现。
六、总结
开发环境日志正常、生产环境乱码的问题,本质就是“字符集编码不统一”或者“日志采集器配置和编码不匹配”,排查的核心思路就是“从源头到末端,一步一步确认每个环节的编码是否统一”。只要按照“先查程序写日志的编码→再查操作系统的编码→再查日志文件本身的编码→再查采集器的读取编码→再查采集器的输出编码→再查采集器的中间处理编码”的顺序排查,就能找到问题所在。记住一个核心原则:全链路的编码必须统一,都用UTF-8,并且每个环节都要明确指定编码,不要依赖默认。
评论
围绕“面对开发环境日志正常生产环境却乱码的问题,字符集编码与日志采集器配置矛盾的完整彻底排查思路梳理”参与讨论