问题描述
我最近项目是基于自研的webserver框架实现的,支持C++、Java、Python等语言混合开发,上线一年多都没有发现重大问题。但就在昨天快下班时,运营同事突然打电话说生产环境无法导入报表,于是我登录生产环境,看了一下日志,发现报表文件已经正常上到服务器,但在解析时无法正常打开。
我们的报表都是xls格式,导入功能是Python开发的,我们选择xlrd
库来解析xls文件。查看日志发现xlrd
在打开文件时报“输入/输出错误”,具体日志如下:
[20190614 17:28:48|ERR] system error[Errno 5 输入/输出错误]
由于是线上问题,我马上转存日志后,就重启了webserver服务,重启后就可以正常导入报表,果然是万能的重启大法。
问题分析
但是问题还是要解决,由于是xlrd
模块报错,一开始一直坚信xlrd
库不会有问题,那么很有可能是xls文件损坏引起了,计算了一下报表文件的MD5值,发现文件是完整的,下载到本地可以正常打开,而且在线下环境导入竟然成功了,多次在线下测试都不能重现线上的问题。
排除了不是xls文件的问题后,开始怀疑是线上环境存在文件使用完后没有关闭的情况,导致长时间运行后webserver进程的文件句柄用完了,因此无法正常打开文件,于是用lsof -c webserver | wc -l
命令跟踪了一个websever服务打开的文件数量,发现打开文件数量是稳定了,这就奇怪了,明明是“输入/输出”错误,难道xlrd
库本身有问题,于是将webserver日志级别调到debug级别,等待线上问题重现后查看异常堆栈。
第二天上班时,线上就重新了这个问题,登录线上环境,查看日志发现异常调用堆栈如下:
[20190615 08:50:08|ERR] system error[Errno 5 输入/输出错误]
[20190615 08:50:08|ERR] system error[Traceback (most recent call last):
File "/home/work/application/webserver/pyc/alibaba/importexlist.py", line 56, in main
vec = GetDataList(app.getPath() + url);
File "/home/work/application/webserver/pyc/alibaba/importexlist.py", line 17, in GetDataList
with xlrd.open_workbook(path) as xls:
File "/usr/local/lib/python3.6/site-packages/xlrd/__init__.py", line 157, in open_workbook
ragged_rows=ragged_rows,
File "/usr/local/lib/python3.6/site-packages/xlrd/book.py", line 88, in open_workbook_xls
ragged_rows=ragged_rows,
File "/usr/local/lib/python3.6/site-packages/xlrd/book.py", line 632, in biff2_8_load
cd = compdoc.CompDoc(self.filestr, logfile=self.logfile)
File "/usr/local/lib/python3.6/site-packages/xlrd/compdoc.py", line 119, in __init__
% (len(mem), sec_size), file=logfile)
OSError: [Errno 5] 输入/输出错误
很明显,程序执行到xlrd/compdoc.py
文件第119行出现异常,执行以下命令查看源码内容如下:
head -120 /usr/local/lib/python3.6/site-packages/xlrd/compdoc.py | cat -n
114 mem_data_secs, left_over = divmod(mem_data_len, sec_size)
115 if left_over:
116
117 mem_data_secs += 1
118 print("WARNING *** file size (%d) not 512 + multiple of sector size (%d)"
119 % (len(mem), sec_size), file=logfile)
120 self.mem_data_secs = mem_data_secs
看了源码后有点不淡定了,心中有一万匹草泥马在奔腾,源码中只有print
函数(第119行)与“输入/输出”有关,难道是print
函数的问题,不可能呀,这个是Python的系统函数怎么可能会有问题?继续分析源码,发现print
函数传入了file
参数,但这个参数的值是sys.stdout
。
sys.stdout
这不就是输出终端吗?分析至此,看来Python的print
函数有猫腻,这时用lsof -c webserver
命令重点观察了一下webserver打开文件情况:
webserver 10237 work 0u CHR 136,0 0t0 3 /dev/pts/0(deleted)
webserver 10237 work 1u CHR 136,0 0t0 3 /dev/pts/0(deleted)
webserver 10237 work 2u CHR 136,0 0t0 3 /dev/pts/0(deleted)
对比线下测试环境的lsof -c webserver
命令执行结果,发现上面打开文件记录后面多了一个deleted
标记,说明对应的文件或设备已经被删除了。
webserver 10632 work 0u CHR 136,0 0t0 3 /dev/pts/4
webserver 10632 work 1u CHR 136,0 0t0 3 /dev/pts/4
webserver 10632 work 2u CHR 136,0 0t0 3 /dev/pts/4
看来真的是线上webserver服务终端输出设备的问题,确认问题后立马重启线上webserver服务,再次执行lsof -c webserver
命令输出如下:
webserver 10413 work 0u CHR 136,0 0t0 3 /dev/pts/1
webserver 10413 work 1u CHR 136,0 0t0 3 /dev/pts/1
webserver 10413 work 2u CHR 136,0 0t0 3 /dev/pts/1
重启结果和预期一致,上面输出内容第4列0u
/1u
/2u
实际上是webserver进程启动时最先打开的三个文件句柄,依次对应以下终端设备:
0
号文件—stdin
标准输入设备
1
号文件—stdout
标准输出设备
2
号文件—stderr
错误输出设备
那么在什么情况下文件会出现deleted
删除标记呢?原来webserver服务通过终端启动,当启动终端退出或关闭时,对应的终端设备也就不存在了,如果这时不关闭对应文件句柄,就会出现上面这种情况,不幸的是Python的print
函数会把这种情况当异常处理,于是就产生了这个问题。
解决方案
问题原因找到了,怎么解决这个呢?既然文件的deleted
标记是终端关闭时引起,那么一定是终端关闭时向内核发送了信号,然后内核根据这个信号将对应的文件句柄标记为deleted
。
实际上终端关闭时会向内核发送SIGHUP
信号,我们可以调用signal
函数给SIGHUP
信号注册一个回调方法,然后在回调方法里面关闭终端相关的设备,就是对stdin
/stdout
/stderr
执行close
方法,相关代码如下:
void sighup(int signum)
{
fclose(stdin);
fclose(stdout);
fclose(stderr);
signal(signum, SIG_IGN);
}
signal(SIGHUP, sighup);
后续我会完善webserver
架构的相关文档,然后将webserver
框架发布到开源社区,到时欢迎广大猿友指正。