可以得到结论,就是文件在某些位置被插入了Hex
93
(对应ASCII
147
)字节,导致文件渲染异常。
分析乱码位置是否有规律
因为这个文件实际上是通过环境变量 +
envsubst
渲染模板文件形成的,而这段文本就是来源于一个环境变量。
把
agreement.js
中的这段文本截取出来,然后用16进制编辑器看看出现
93
的位置上是否存在共同点。
offset
delta
delta - 1
512 x
echo $FOO > foo-1.txt
export FOO=`echo ${FOO} | gojq tostring`
echo $FOO > foo-2.txt
环境变量 FOO
是容器运行时外部给的,脚本先把值写到 foo-1.txt
,然后执行一些脚本,更新了这个环境变量,然后把结果写到 foo-2.txt
,而问题就出在 foo-2.txt
上,说明问题出在第二行。
插入字节的位置
回顾本问题,本问题是有一定概率会出现,而不是每次都会出现,因此使用Hex Friend(Mac上的软件)打开“好的文件”和几个“坏的文件”,对比插入的字节的位置有何不同。
和第一个坏的文件对比,可以看到字节插入位置,以及插入的值是0x82
:
和第二个坏的文件对比,可以看到字节插入位置,以及插入的值是0xB9
:
可以发现:
只要出现问题,那么插入的字节位置是固定的
每次出现问题所插入的字节是随机的,但是对同一个文件来说这个值是相同的。
先总结一下:
环境变量值存在汉字,而且长度超出了 512 字节,
有一定概率会出现脚本执行结果文件里,插入了随机字节
随机字节插入的位置是固定的
出现问题的文件里,随机字节的值是相同的
不同的问题文件,随机字节的值不相同
那么推理:
汉字 UTF-8 编码占用 3 个字节,在脚本执行过程是 512 字节 一块一块读取的,那么就会有概率正好截断在汉字 UTF-8 3 字节的中间,导致了这个问题。
到这里可以先排除gojq
的问题,理由有二:
和开发人员沟通,脚本之前使用的是 jq
,而现在使用的是 gojq
,不同工具,遇到相同问题,两个工具都有BUG的概率不大。
另外,经测试nginx:1.21.5-alpine
会发生问题,而nginx:1.21.5
不会发生问题,而且gojq
这个工具都是同一个二进制,因此gojq
有BUG可能性排除。
测试脚本在这里。
因为这个问题出现存在一定概率,因此写一个测试脚本循环跑以下几个测试:
1)测试变量 output 到文件,是否会损坏(md5sum检查):
echo $FOO > /tmp/foo.txt
if ! md5sum -c /foo.txt.md5 > /dev/null; then
echo "$msg_prefix failed: case 1"
fail=true
2)测试 |
管道符读取变量时,是否会损坏(md5sum 字符模式):
got_md5=$(echo $FOO | md5sum -b | cut -d ' ' -f 1)
if [[ "$got_md5" != "$want_md5" ]]; then
echo "$msg_prefix failed: case 2"
fail=true
3)测试 |
管道符读取变量时,是否会损坏(md5sum 二进制模式):
got_md5=$(echo $FOO | md5sum | cut -d ' ' -f 1)
if [[ "$got_md5" != "$want_md5" ]]; then
echo "$msg_prefix failed: case 3"
fail=true
4)测试 gojq
结果赋予新变量,新变量输出到文件,是否损坏(md5检查):
FOO_MOD=$(echo $FOO | gojq tostring)
echo $FOO_MOD > /tmp/foo-mod.txt
if ! md5sum -c /foo-mod.txt.md5 > /dev/null ; then
echo "$msg_prefix failed: case 4"
fail=true
5)测试 gojq
结果赋予新变量,直接检查新变量值,是否损坏(md5检查):
FOO_MOD=$(echo $FOO | gojq tostring)
got_md5=$(echo $FOO_MOD | md5sum -b | cut -d ' ' -f 1)
if [[ "$got_md5" != "$want_mod_md5" ]]; then
echo "$msg_prefix failed: case 5"
fail=true
6)测试 gojq
结果赋予新变量,新变量输出到文件,是否损坏(md5检查),使用double quote 变量的方式:
FOO_MOD=$(echo "$FOO" | gojq tostring)
echo "$FOO_MOD" > /tmp/foo-mod-q.txt
if ! md5sum -c /foo-mod-q.txt.md5 > /dev/null ; then
echo "$msg_prefix failed: case 6"
fail=true
7)测试 gojq
结果赋予新变量,直接检查新变量值,是否损坏(md5检查),使用double quote 变量的方式:
FOO_MOD=$(echo "$FOO" | gojq tostring)
got_md5=$(echo "$FOO_MOD" | md5sum -b | cut -d ' ' -f 1)
if [[ "$got_md5" != "$want_mod_q_md5" ]]; then
echo "$msg_prefix failed: case 7"
fail=true
8)测试 gojq
,跳过变量赋值,直接检查结果是否损坏(md5检查):
got_md5=$(echo "$FOO" | gojq tostring | md5sum | cut -d ' ' -f 1)
if [[ "$got_md5" != "$want_mod_q_md5" ]]; then
echo "$msg_prefix failed: case 8"
fail=true
脚本执行结果(例子):
Docker run round xxx
md5sum: WARNING: 1 of 1 computed checksums did NOT match
failed: case 4
failed: case 5
md5sum: WARNING: 1 of 1 computed checksums did NOT match
failed: case 6
failed: case 7
运行一段时间后,发现只要出错,case 4, 5, 6, 7 是一起出错的。
4、5 和 6、7 的差别在于是否使用了 double quote 变量的方式 ,都有错说明 double quote 变量不能解决这个问题
4、6 和 5、7 的差别在于是输出到文件检查,还是直接检查变量,都有错说明 输出文件不是关键,而是变量的值本身就损坏了
同时注意到,case 8 虽然在逻辑上和 case 4、5、6、7 一样,但是却不出错。两者的区别在于是否使用新变量来接收gojq
的返回值。
这就说明,问题出在新变量赋值上。
所以问题出在,在bash脚本中给一个变量赋值一个很长的包含中文的值,就会有概率出错。
而且测试脚本的执行方式有两种:
循环创建容器执行测试脚本
创建单个容器,脚本内循环执行
发现,前一种方式,概率出现问题。而后一种方式如果一开始没有问题,就一直没有问题,如果一开始有问题,就一直有问题。
测试脚本2
现在有了一个强有力的推测:在bash脚本中给一个变量赋值一个很长的包含中文的值,就会有概率出错。
那么就新写一个测试脚本,这里。
也不需要gojq
了,直接把一个文件的内容赋值给一个变量:
FOO=$(cat /tmp/foo.txt)
want_q_md5=$(cat /tmp/foo.txt.md5 | cut -d ' ' -f 1)
got_md5=$(echo "$FOO" | md5sum -b | cut -d ' ' -f 1)
if [[ "$got_md5" != "$want_q_md5" ]]; then
echo "failed"
echo "$FOO" > /tmp/foo-corrupt.txt
fail=true
echo "succeed"
测试结果:
Docker run round 1
succeed
Docker run round 2
failed
Docker run round 3
succeed
Docker run round 4
succeed
Docker run round 5
failed
到这里就可以证实这个猜测了。
到GNU Bash的邮件列表中搜索到这个BUG:
Corrupted multibyte characters in command substitutions,符合我们遇到的问题,提到在 5.1.16 版本里修复了这个问题
Long variable value get corrupted sometimes,我这里也提了一个issue(提之前没有好好搜索)
关于这个 BUG 的 patch 见 这里、这里 和 这里
我在 Alpine Linux 提议更新 Bash 版本,见 issue
我在 Debian 邮件列表 提议更新 Bash 版本,见这里,Debian Bullseye 关于这个问题的 Bug Report
容器中Java进程产生大量僵尸进程的问题
容器中Java进程产生大量僵尸进程的问题(续)