添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

可以得到结论,就是文件在某些位置被插入了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进程产生大量僵尸进程的问题(续)