问题
由于把 Postgres 运行在 Docker 容器里,所以经常使用如下命令登录到数据库 docker exec -it -u postgres mypg psql mydb
。然后在 psql 提示符下执行操作。然后经常会出现输入太长的 sql 语句时,psql 会在还没遇到终端宽度限制之前就开始换行,而且是把把输入回显换行到上面一行。这个行为说明了在容器内的 tty 的配置出现了问题。但是,很奇怪的是,退出 docker exec
之后,再次进入,就不会遇到这个问题了。
现象分析
这个问题看起来应该出在是 Docker 容器对于 tty size 的处理上。当出现这个问题的时候,通过 docker exec
进入容器的 shell,执行 stty size
会得到如下结果:
从上面的命令可以看出,stty
获取当前终端的窗口大小失败了,所以 psql
程序的回显就错乱了。不过,这里要注意的是,stty size
命令没有返回失败(返回值为0)。但是,通过 stty rows 40 cols 100
命令设置 tty 的参数确实可以成功的,说明问题可能是因为 tty 参数没有被正常初始化造成的。另外需要说明的是,这个问题不是必现的。
这个问题在 Docker 项目的 issue 里被提到过,地址是 https://github.com/moby/moby/issues/33794。根据该 issue 的记录,目前该问题还没有找到原因。
关于这个问题,我不确定是 kernel 引起的,还是 shell 程序引起的,因为 termio
结构体会从内核拷贝到用户空间。
stty size
的返回值为什么是0?
我想知道为什么stty
命令没有返回失败,所以去看了一下代码。我使用的是 Postgres Alpine 的镜像,安装的 shell 环境是 BusyBox,通过查看 BusyBox 的代码可以找到这里:https://github.com/mirror/busybox/blob/master/coreutils/stty.c#L899,对应的代码是:
这个函数获取当前窗口的大小,stty size
命令会直接调用这个函数。这个函数继续调用了下面这个函数 https://github.com/mirror/busybox/blob/master/libbb/xfuncs.c#L263:
这里的关键点就在于 get_terminal_width_height
函数的内部注释,即 ioctl
可能返回0,但是却没有获取到窗口大小。这种情况下,该函数返回 win.ws_row == 0
,即 1
。当 get_terminal_width_height
函数返回 1
时,调用者 display_window_size
的处理逻辑如下:因为返回值不等于 EINVAL
,所以直接调用 perror_on_device
,这个函数会调用 libbb/perror_msg.c 中的 bb_perror_msg
函数,输出一个错误信息,然后就返回了(并不会导致程序错误退出)。随后,display_window_size
函数返回,stty size
命令正常返回。
这个看起来是个 BusyBox 的 bug,所以有点怀疑是否是 Docker 和 BusyBox 的配合有问题,导致了bug。
解决办法
虽然没有找到原因,但是解决办法还是有的,就是在进入 shell 前显示的设定终端窗口的大小,通过指定 COLUMNS
和 LINES
两个环境变量的方式,命令如下: