使用pexpect连接K8s Pod时匹配shell提示符触发TIMEOUT超时排查
问题背景
手动执行kubectl exec进入PostgreSQL所在Pod的bash进程可以正常运行,操作流程如下:
➜ kubectl exec -it pg-postgresql-0 -n ns-sql -c postgresql -- bash I have no name!@pg-postgresql-0:/$ I have no name!@pg-postgresql-0:/$ I have no name!@pg-postgresql-0:/$ exit
使用pexpect编写Python自动化脚本实现相同操作,初始代码如下:
➜ cat sql.py #!/usr/bin/env python3 import pexpect cmd = "kubectl exec -it pg-postgresql-0 -n ns-sql -c postgresql -- bash" child = pexpect.spawn(cmd) child.expect('I have no name!@pg-postgresql-0:/$ ') child.sendline('psql -U postgres') child.expect('user postgres: ') child.sendline('passW0rd') child.expect('postgres=#') child.interact()
执行脚本后触发30秒超时报错,从错误日志的缓冲区字段可以看到,目标shell提示符已经被读取到缓冲区中,但依然匹配失败:
➜ ./sql.py Traceback (most recent call last): File "./sql.py", line 6, in <module> child.expect('I have no name!@pg-postgresql-0:/$ ') File "/home/bhakta/dev/python/venv/lib/python3.7/site-packages/pexpect/spawnbase.py", line 344, in expect timeout, searchwindowsize, async_) File "/home/bhakta/dev/python/venv/lib/python3.7/site-packages/pexpect/spawnbase.py", line 372, in expect_list return exp.expect_loop(timeout) File "/home/bhakta/dev/python/venv/lib/python3.7/site-packages/pexpect/expect.py", line 181, in expect_loop return self.timeout(e) File "/home/bhakta/dev/python/venv/lib/python3.7/site-packages/pexpect/expect.py", line 144, in timeout raise exc pexpect.exceptions.TIMEOUT: Timeout exceeded. <pexpect.pty_spawn.spawn object at 0x7f086ebc8e90> command: /usr/local/bin/kubectl args: ['/usr/local/bin/kubectl', 'exec', '-it', 'pg-postgresql-0', '-n', 'ns-sql', '-c', 'postgresql', '--', 'bash'] buffer (last 100 chars): b'I have no name!@pg-postgresql-0:/$ \r\x1b[KI have no name!@pg-postgresql-0:/$ ' before (last 100 chars): b'I have no name!@pg-postgresql-0:/$ \r\x1b[KI have no name!@pg-postgresql-0:/$ ' after: <class 'pexpect.exceptions.TIMEOUT'> match: None match_index: None exitstatus: None flag_eof: False pid: 178145 child_fd: 5 closed: False timeout: 30 delimiter: <class 'pexpect.exceptions.EOF'> logfile: None logfile_read: None logfile_send: None maxread: 2000 ignorecase: False searchwindowsize: None delaybeforesend: 0.05 delayafterclose: 0.1 delayafterterminate: 0.1 searcher: searcher_re: 0: re.compile(b'I have no name!@pg-postgresql-0:/$ ')
根本原因
问题出在匹配规则的写法上:
pexpect.expect()方法默认将传入的字符串作为正则表达式编译匹配,你写的匹配串里的$位置在斜杠/之后、空格之前,会被正则解析为行尾锚点元字符,不会被当做提示符末尾的普通美元符号匹配。- 该匹配规则翻译为正则逻辑就是:匹配
I have no name!@pg-postgresql-0:/字符串,紧接着必须是行尾位置,行尾后还必须跟一个空格——这是永远不可能成立的匹配条件:一方面行尾是一行内容的结束位置,后面不可能存在任何字符;另一方面实际缓冲区里的内容是斜杠后先跟美元符号$,再跟空格,和正则要求的结构完全不符。 - 缓冲区里出现两次重复的提示符,是因为tty初始化时终端会重绘一次提示符,中间插入的
\r\x1b[K是终端控制字符(回车+清除光标到行尾内容),属于正常现象,不是匹配失败的核心原因。
调试方法
遇到这类pexpect匹配问题,按以下步骤排查即可:
- 开启读取日志:在spawn子进程后添加
child.logfile_read = sys.stdout.buffer,执行脚本时会把所有从子进程读到的原始字节(包括不可见的控制字符、转义序列)完整打印出来,不用靠报错的截断缓冲区猜内容。 - 区分匹配模式:如果不需要正则匹配,优先用
expect_exact()方法做精确字符串匹配,避免正则元字符导致的意外匹配失败。 - 简化匹配规则:不要硬编码完整的长提示符,优先匹配提示符末尾的固定特征片段,减少终端控制字符、动态内容(比如当前路径、用户名)对匹配的干扰。
修复方案
两种改法任选其一即可:
- 用精确字符串匹配,绕过正则解析:
# 把原来的expect行替换成下面这行 child.expect_exact('I have no name!@pg-postgresql-0:/$ ')
- 转义正则里的元字符,让$作为普通字符匹配:
# 注意前面加r表示原生字符串,\$转义后匹配字面量$ child.expect(r'I have no name!@pg-postgresql-0:/\$ ')
如果后续匹配psql密码提示、postgres提示符遇到同类问题,按相同逻辑处理即可。
内容的提问来源于stack exchange,提问作者Bhakta Raghavan
相关产品推荐
相关产品推荐

