jmxterm: "Unable to create a system terminal" 在 Docker 容器内
jmxterm: "Unable to create a system terminal" inside Docker container
我有一个 Docker 图像,其中包含 JRE、一些 Java 网络应用程序和 jmxterm
。后者用于 运行 一些临时管理任务。该图像在 CentOS 7 服务器上使用 Docker 1.13(相当旧但是是通过发行版存储库提供的最新版本)到 运行 Web 应用程序本身。
一切正常,但在将 jmxterm
从 1.0.0 更新到最新版本 (1.0.2) 后,进入 运行ning 容器并启动 jmxterm
:
WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
在此之后,jmxterm
不会对箭头键做出反应(当尝试浏览命令历史记录时),也不会提供自动完成功能。
一些快速调查显示问题可能会在 CentOS 7 的干净环境中重现。说,这就是我如何 bootstrap 系统和容器以及我需要的所有东西:
$ vagrant init centos/7
$ vagrant up
$ vagrant ssh
[vagrant@localhost ~]$ sudo yum install docker
[vagrant@localhost ~]$ sudo systemctl start docker
[vagrant@localhost ~]$ sudo docker run -it --entrypoint bash openjdk:11
root@0c4c614de0ee:/# wget https://github.com/jiaqi/jmxterm/releases/download/v1.0.2/jmxterm-1.0.2-uber.jar
这就是我进入容器的方式 运行 jmxterm
:
[vagrant@localhost ~]$ sudo docker exec -it 0c4c614de0ee sh
root@0c4c614de0ee:/# java -jar jmxterm-1.0.2-uber.jar
WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
root@0c4c614de0ee:/# bea<TAB>
<Nothing happens, but autocompletion had to appear>
观察很少:
- 无论我使用哪个图像,旧的
jmxterm
都不会出现该问题;
- 新的
jmxterm
无论我用哪个图像都会出现问题;
- 问题在我的笔记本电脑上无法重现(它有更新的内核和 Docker);
- 如果我在 CentOS 7 服务器上使用最新的 Docker(来自外部仓库)而不是 CentOS 7 的本机版本 1.13,则问题无法重现。
发生了什么,为什么错误只能在特定环境中重现?有什么解决方法吗?
TLDR: 运行 new jmxterm
versions as java -jar jmxterm-1.0.2-uber.jar < /dev/tty
是一个快速、肮脏和 hacky 的解决方法,用于自动完成和其他东西在交互式容器会话中工作。
快速检查显示 jmxterm
试图通过 运行 tty
实用程序来确定进程使用的终端设备——可能是为了稍后获得终端功能:
root@0c4c614de0ee:/# strace -f -e 'trace=execve,wait4' java -jar jmxterm-1.0.2-uber.jar
execve("/opt/java/openjdk/bin/java", ["java", "-jar", "jmxterm-1.0.2-uber.jar"], 0x7ffed3a53210 /* 36 vars */) = 0
...
[pid 432] execve("/usr/bin/tty", ["tty"], 0x7fff8ea39608 /* 36 vars */) = 0
[pid 433] wait4(432, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 432
WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
实用程序失败,状态为 1,这可能是错误消息的原因。为什么?
root@0c4c614de0ee:/# strace -y tty
...
readlink("/proc/self/fd/0", "/dev/pts/3", 4095) = 10
stat("/dev/pts/3", 0x7ffe966f2160) = -1 ENOENT (No such file or directory)
...
write(1</dev/pts/3>, "not a tty\n", 10not a tty
) = 10
实用程序说“不是 tty”,而我们肯定有。快速检查显示...确实 容器中没有 PTY 设备 尽管 shell 的标准流连接到一个!
root@0c4c614de0ee:/# ls -l /proc/self/fd
total 0
lrwx------. 1 root root 64 Jun 3 21:26 0 -> /dev/pts/3
lrwx------. 1 root root 64 Jun 3 21:26 1 -> /dev/pts/3
lrwx------. 1 root root 64 Jun 3 21:26 2 -> /dev/pts/3
lr-x------. 1 root root 64 Jun 3 21:26 3 -> /proc/61/fd
root@0c4c614de0ee:/# ls -l /dev/pts
total 0
crw-rw-rw-. 1 root root 5, 2 Jun 3 21:26 ptmx
如果我们检查最新的 Docker 相同怎么办?
root@c0ebd608f79a:/# ls -l /proc/self/fd
total 0
lrwx------ 1 root root 64 Jun 3 21:45 0 -> /dev/pts/1
lrwx------ 1 root root 64 Jun 3 21:45 1 -> /dev/pts/1
lrwx------ 1 root root 64 Jun 3 21:45 2 -> /dev/pts/1
lr-x------ 1 root root 64 Jun 3 21:45 3 -> /proc/16/fd
root@c0ebd608f79a:/# ls -l /dev/pts
total 0
crw--w---- 1 root tty 136, 0 Jun 3 21:44 0
crw--w---- 1 root tty 136, 1 Jun 3 21:45 1
crw-rw-rw- 1 root root 5, 2 Jun 3 21:45 ptmx
宾果!现在我们的 PTY 就在它们应该在的地方,所以 jmxterm
与最新的 Docker.
配合得很好
这似乎很奇怪,旧的 Docker 进程连接到某些 PTY,而 /dev/pts
中没有它们的设备,但跟踪 Docker 进程解释了为什么会这样发生。较旧的 Docker 在 之前为容器分配 PTY,然后 设置其他内容(包括输入新的挂载命名空间并将 devpts
挂载到其中,或者只是在中输入挂载命名空间docker exec -it
的情况):
[vagrant@localhost ~]$ sudo strace -p $(pidof docker-containerd-current) -f -e trace='execve,mount,unshare,openat,ioctl'
...
[pid 3885] openat(AT_FDCWD, "/dev/ptmx", O_RDWR|O_NOCTTY|O_CLOEXEC) = 9
[pid 3885] ioctl(9, TIOCGPTN, [1]) = 0
[pid 3885] ioctl(9, TIOCSPTLCK, [0]) = 0
...
[pid 3898] unshare(CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWNET|CLONE_NEWPID) = 0
...
[pid 3899] mount("devpts", "/var/lib/docker/overlay2/3af250a9f118d637bfba5701f5b0dfc09ed154c6f9d0240ae12523bf252e350c/merged/dev/pts", "devpts", MS_NOSUID|MS_NOEXEC, "newinstance,ptmxmode=0666,mode=0"...) = 0
...
[pid 3899] execve("/bin/bash", ["bash"], 0xc4201626c0 /* 7 vars */ <unfinished ...>
注意 newinstance
挂载选项,它确保 devpts
挂载独占其 PTY,不与其他挂载共享。这导致了一个有趣的效果:容器的 PTY 设备保留在主机上并属于主机的 devpts
挂载,而容器化进程仍然可以访问它,因为它刚刚在它生命的开始!
最新的Docker首先为容器挂载devpts
然后然后分配PTY,所以PTY属于容器的devpts
挂载并且在容器的文件系统中可见:
$ sudo strace -p $(pidof containerd) -f -e trace='execve,mount,unshare,openat,ioctl'
...
[pid 14043] unshare(CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWPID|CLONE_NEWNET) = 0
...
[pid 14044] mount("devpts", "/var/lib/docker/overlay2/b743cf16ab954b9a4b4005bca0aeaa019c4836c7d58d6073044e5b48446c3d62/merged/dev/pts", "devpts",
MS_NOSUID|MS_NOEXEC, "newinstance,ptmxmode=0666,mode=0"...) = 0
...
[pid 14044] openat(AT_FDCWD, "/dev/ptmx", O_RDWR|O_NOCTTY|O_CLOEXEC) = 7
[pid 14044] ioctl(7, TIOCGPTN, [0]) = 0
[pid 14044] ioctl(7, TIOCSPTLCK, [0]) = 0
...
[pid 14044] execve("/bin/bash", ["/bin/bash"], 0xc000203530 /* 4 vars */ <unfinished ...>
好吧,问题是由不当的 Docker 行为引起的,但是为什么那个年长的 jmxterm
在相同的环境中工作得很好呢?让我们检查一下(注意,这里使用 Java 8 图像,因为旧的 jmxterm
不能很好地与 Java 11 一起播放):
root@504a7757e310:/# wget https://github.com/jiaqi/jmxterm/releases/download/v1.0.0/jmxterm-1.0.0-uber.jar
root@504a7757e310:/# strace -f -e 'trace=execve,wait4' java -jar jmxterm-1.0.0-uber.jar
execve("/usr/local/openjdk-8/bin/java", ["java", "-jar", "jmxterm-1.0.0-uber.jar"], 0x7fffdcaebdd0 /* 10 vars */) = 0
...
[pid 310] execve("/bin/sh", ["sh", "-c", "stty -a < /dev/tty"], 0x7fff1f2a1cc8 /* 10 vars */) = 0
因此,较旧的 jmxterm
仅使用 /dev/tty
而不是询问 tty
设备名称,并且此方法有效,因为此设备存在于容器中:
root@504a7757e310:/# ls -l /dev/tty
crw-rw-rw-. 1 root root 5, 0 Jun 3 21:36 /dev/tty
这些版本 jmxterm
之间的巨大差异是较新的工具版本使用 jline
的更高主要版本,这是负责与终端交互的库(类似于 readline
在 C 世界中)。 jline
主要版本之间的差异导致 jmxterm
的行为与当前版本 just rely on tty
.
的差异
这一观察使我们找到了既不需要更新 Docker 也不需要修补 jline
/jmxterm
串联的快速而肮脏的解决方法:我们可以只附加 jmxterm
的标准输入强制设置为 /dev/tty
,从而使 jline
使用此设备(现在由 /proc/self/fd/0
引用)而不是 /dev/pts
条目(正式地说,不是总是正确的,但仍然足够临时使用):
root@0c4c614de0ee:/# java -jar jmxterm-1.0.2-uber.jar < /dev/tty
Welcome to JMX terminal. Type "help" for available commands.
$>bea<TAB>
bean beans
现在我们有了自动补全、历史记录和其他我们需要的很酷的东西。
如果您尝试 运行 在 docker 容器或 kubernetes 中的 pod 中创建一个交互式应用程序(需要 tty),那么下面的方法应该有效。
docker-compose 使用:
image: image-name:2.0
container_name: container-name
restart: always
stdin_open: true
tty: true
对于 kubernetes 使用:
spec:
containers:
- name: web
image: web:latest
tty: true
stdin: true
我有一个 Docker 图像,其中包含 JRE、一些 Java 网络应用程序和 jmxterm
。后者用于 运行 一些临时管理任务。该图像在 CentOS 7 服务器上使用 Docker 1.13(相当旧但是是通过发行版存储库提供的最新版本)到 运行 Web 应用程序本身。
一切正常,但在将 jmxterm
从 1.0.0 更新到最新版本 (1.0.2) 后,进入 运行ning 容器并启动 jmxterm
:
WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
在此之后,jmxterm
不会对箭头键做出反应(当尝试浏览命令历史记录时),也不会提供自动完成功能。
一些快速调查显示问题可能会在 CentOS 7 的干净环境中重现。说,这就是我如何 bootstrap 系统和容器以及我需要的所有东西:
$ vagrant init centos/7
$ vagrant up
$ vagrant ssh
[vagrant@localhost ~]$ sudo yum install docker
[vagrant@localhost ~]$ sudo systemctl start docker
[vagrant@localhost ~]$ sudo docker run -it --entrypoint bash openjdk:11
root@0c4c614de0ee:/# wget https://github.com/jiaqi/jmxterm/releases/download/v1.0.2/jmxterm-1.0.2-uber.jar
这就是我进入容器的方式 运行 jmxterm
:
[vagrant@localhost ~]$ sudo docker exec -it 0c4c614de0ee sh
root@0c4c614de0ee:/# java -jar jmxterm-1.0.2-uber.jar
WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
root@0c4c614de0ee:/# bea<TAB>
<Nothing happens, but autocompletion had to appear>
观察很少:
- 无论我使用哪个图像,旧的
jmxterm
都不会出现该问题; - 新的
jmxterm
无论我用哪个图像都会出现问题; - 问题在我的笔记本电脑上无法重现(它有更新的内核和 Docker);
- 如果我在 CentOS 7 服务器上使用最新的 Docker(来自外部仓库)而不是 CentOS 7 的本机版本 1.13,则问题无法重现。
发生了什么,为什么错误只能在特定环境中重现?有什么解决方法吗?
TLDR: 运行 new jmxterm
versions as java -jar jmxterm-1.0.2-uber.jar < /dev/tty
是一个快速、肮脏和 hacky 的解决方法,用于自动完成和其他东西在交互式容器会话中工作。
快速检查显示 jmxterm
试图通过 运行 tty
实用程序来确定进程使用的终端设备——可能是为了稍后获得终端功能:
root@0c4c614de0ee:/# strace -f -e 'trace=execve,wait4' java -jar jmxterm-1.0.2-uber.jar
execve("/opt/java/openjdk/bin/java", ["java", "-jar", "jmxterm-1.0.2-uber.jar"], 0x7ffed3a53210 /* 36 vars */) = 0
...
[pid 432] execve("/usr/bin/tty", ["tty"], 0x7fff8ea39608 /* 36 vars */) = 0
[pid 433] wait4(432, [{WIFEXITED(s) && WEXITSTATUS(s) == 1}], 0, NULL) = 432
WARNING: Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)
实用程序失败,状态为 1,这可能是错误消息的原因。为什么?
root@0c4c614de0ee:/# strace -y tty
...
readlink("/proc/self/fd/0", "/dev/pts/3", 4095) = 10
stat("/dev/pts/3", 0x7ffe966f2160) = -1 ENOENT (No such file or directory)
...
write(1</dev/pts/3>, "not a tty\n", 10not a tty
) = 10
实用程序说“不是 tty”,而我们肯定有。快速检查显示...确实 容器中没有 PTY 设备 尽管 shell 的标准流连接到一个!
root@0c4c614de0ee:/# ls -l /proc/self/fd
total 0
lrwx------. 1 root root 64 Jun 3 21:26 0 -> /dev/pts/3
lrwx------. 1 root root 64 Jun 3 21:26 1 -> /dev/pts/3
lrwx------. 1 root root 64 Jun 3 21:26 2 -> /dev/pts/3
lr-x------. 1 root root 64 Jun 3 21:26 3 -> /proc/61/fd
root@0c4c614de0ee:/# ls -l /dev/pts
total 0
crw-rw-rw-. 1 root root 5, 2 Jun 3 21:26 ptmx
如果我们检查最新的 Docker 相同怎么办?
root@c0ebd608f79a:/# ls -l /proc/self/fd
total 0
lrwx------ 1 root root 64 Jun 3 21:45 0 -> /dev/pts/1
lrwx------ 1 root root 64 Jun 3 21:45 1 -> /dev/pts/1
lrwx------ 1 root root 64 Jun 3 21:45 2 -> /dev/pts/1
lr-x------ 1 root root 64 Jun 3 21:45 3 -> /proc/16/fd
root@c0ebd608f79a:/# ls -l /dev/pts
total 0
crw--w---- 1 root tty 136, 0 Jun 3 21:44 0
crw--w---- 1 root tty 136, 1 Jun 3 21:45 1
crw-rw-rw- 1 root root 5, 2 Jun 3 21:45 ptmx
宾果!现在我们的 PTY 就在它们应该在的地方,所以 jmxterm
与最新的 Docker.
这似乎很奇怪,旧的 Docker 进程连接到某些 PTY,而 /dev/pts
中没有它们的设备,但跟踪 Docker 进程解释了为什么会这样发生。较旧的 Docker 在 之前为容器分配 PTY,然后 设置其他内容(包括输入新的挂载命名空间并将 devpts
挂载到其中,或者只是在中输入挂载命名空间docker exec -it
的情况):
[vagrant@localhost ~]$ sudo strace -p $(pidof docker-containerd-current) -f -e trace='execve,mount,unshare,openat,ioctl'
...
[pid 3885] openat(AT_FDCWD, "/dev/ptmx", O_RDWR|O_NOCTTY|O_CLOEXEC) = 9
[pid 3885] ioctl(9, TIOCGPTN, [1]) = 0
[pid 3885] ioctl(9, TIOCSPTLCK, [0]) = 0
...
[pid 3898] unshare(CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWNET|CLONE_NEWPID) = 0
...
[pid 3899] mount("devpts", "/var/lib/docker/overlay2/3af250a9f118d637bfba5701f5b0dfc09ed154c6f9d0240ae12523bf252e350c/merged/dev/pts", "devpts", MS_NOSUID|MS_NOEXEC, "newinstance,ptmxmode=0666,mode=0"...) = 0
...
[pid 3899] execve("/bin/bash", ["bash"], 0xc4201626c0 /* 7 vars */ <unfinished ...>
注意 newinstance
挂载选项,它确保 devpts
挂载独占其 PTY,不与其他挂载共享。这导致了一个有趣的效果:容器的 PTY 设备保留在主机上并属于主机的 devpts
挂载,而容器化进程仍然可以访问它,因为它刚刚在它生命的开始!
最新的Docker首先为容器挂载devpts
然后然后分配PTY,所以PTY属于容器的devpts
挂载并且在容器的文件系统中可见:
$ sudo strace -p $(pidof containerd) -f -e trace='execve,mount,unshare,openat,ioctl'
...
[pid 14043] unshare(CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC|CLONE_NEWPID|CLONE_NEWNET) = 0
...
[pid 14044] mount("devpts", "/var/lib/docker/overlay2/b743cf16ab954b9a4b4005bca0aeaa019c4836c7d58d6073044e5b48446c3d62/merged/dev/pts", "devpts",
MS_NOSUID|MS_NOEXEC, "newinstance,ptmxmode=0666,mode=0"...) = 0
...
[pid 14044] openat(AT_FDCWD, "/dev/ptmx", O_RDWR|O_NOCTTY|O_CLOEXEC) = 7
[pid 14044] ioctl(7, TIOCGPTN, [0]) = 0
[pid 14044] ioctl(7, TIOCSPTLCK, [0]) = 0
...
[pid 14044] execve("/bin/bash", ["/bin/bash"], 0xc000203530 /* 4 vars */ <unfinished ...>
好吧,问题是由不当的 Docker 行为引起的,但是为什么那个年长的 jmxterm
在相同的环境中工作得很好呢?让我们检查一下(注意,这里使用 Java 8 图像,因为旧的 jmxterm
不能很好地与 Java 11 一起播放):
root@504a7757e310:/# wget https://github.com/jiaqi/jmxterm/releases/download/v1.0.0/jmxterm-1.0.0-uber.jar
root@504a7757e310:/# strace -f -e 'trace=execve,wait4' java -jar jmxterm-1.0.0-uber.jar
execve("/usr/local/openjdk-8/bin/java", ["java", "-jar", "jmxterm-1.0.0-uber.jar"], 0x7fffdcaebdd0 /* 10 vars */) = 0
...
[pid 310] execve("/bin/sh", ["sh", "-c", "stty -a < /dev/tty"], 0x7fff1f2a1cc8 /* 10 vars */) = 0
因此,较旧的 jmxterm
仅使用 /dev/tty
而不是询问 tty
设备名称,并且此方法有效,因为此设备存在于容器中:
root@504a7757e310:/# ls -l /dev/tty
crw-rw-rw-. 1 root root 5, 0 Jun 3 21:36 /dev/tty
这些版本 jmxterm
之间的巨大差异是较新的工具版本使用 jline
的更高主要版本,这是负责与终端交互的库(类似于 readline
在 C 世界中)。 jline
主要版本之间的差异导致 jmxterm
的行为与当前版本 just rely on tty
.
这一观察使我们找到了既不需要更新 Docker 也不需要修补 jline
/jmxterm
串联的快速而肮脏的解决方法:我们可以只附加 jmxterm
的标准输入强制设置为 /dev/tty
,从而使 jline
使用此设备(现在由 /proc/self/fd/0
引用)而不是 /dev/pts
条目(正式地说,不是总是正确的,但仍然足够临时使用):
root@0c4c614de0ee:/# java -jar jmxterm-1.0.2-uber.jar < /dev/tty
Welcome to JMX terminal. Type "help" for available commands.
$>bea<TAB>
bean beans
现在我们有了自动补全、历史记录和其他我们需要的很酷的东西。
如果您尝试 运行 在 docker 容器或 kubernetes 中的 pod 中创建一个交互式应用程序(需要 tty),那么下面的方法应该有效。
docker-compose 使用:
image: image-name:2.0
container_name: container-name
restart: always
stdin_open: true
tty: true
对于 kubernetes 使用:
spec:
containers:
- name: web
image: web:latest
tty: true
stdin: true