[疑难杂症2024-002]一个“显而易见“的问题,是如何进入生产环境的?

本文由Markdown语法编辑器编辑完成。

1. 前言

最近在处理一个在医院上线的系统的问题。这个问题,由于关联的模块比较多,至少涉及到3个模块之间的功能调用。因此,协调大家都有时间来排查问题不是很方便。这个问题就拖了有一周左右。医院那边一直在催促公司派人解决,也没有多大进展。

后来实在不行了,必须要解决了。只好硬着头皮上了。

问题其实也比较直观。就是回传给医院的图像,出现了巨大的灰色的框。这样导致有效的图像,都被遮挡住了。而这些图像是要交给患者打印胶片的,那肯定就不能正常的交付了,也就影响了医生的工作流,因此他们也比较着急,希望能尽快解决。

在这里插入图片描述

2. 问题定位

程序员的工作,有点类似医生。也是需要先观察现象,根据现象,推测可能的原因。由于在最终推送到医院的PACS前,会经过3个服务的处理。那只能是从前往后依次排查了。

在这里插入图片描述

由于三个模块,各自负责的人不同。这个时候,很容易相互推诿,都会认为自己的模块肯定是没问题的,一定是别的模块出了差错,引起了问题。那这个时候怎么办呢?只能采用联想-排除的方法,就是先根据从现场反馈的,一切线索,尽可能地排除掉一些可能性。将产生问题的点,缩小范围。

现场给到的线索有哪些呢?
1> 系统上线后,偶然会有这样的灰色方框的问题。有的医生反馈,有的不反馈;
2> 最近出现灰色方框的比较多了,影响了医生的正常工作;
3> 自动重建会出现问题;但是手动重建,再推送到PACS, 图像是正常的;
4> 自动重建和手动重建的操作,中间隔了多久?这中间服务器到底发生了什么情况?
5> 自动重建和手动重建,用的算法代码是否一致? 是否有可能是因为代码不一致导致的?
6> 拉取图像,确认只拉取了一次。且拉取下来的图像,和PACS上显示的序列内数量也是一致的.

然后我又查了chatgpt, 询问关于MPR, MIP如果出现灰色方框时,有什么可能的原因。chatgpt给出的回复大致分为三点:
在这里插入图片描述
gpt给出的三个可能性中。最大的可能性是第一种,也是我们认为概率最大的。

但是,这个怎么解释,手动重建就是正常的呢?

[2024.03.29]
书接上回。

其实最令我们费解的是,为什么手动重建就可以重建出完整的图像,但是自动重建就会有灰色的方框呢?
它们明明用的都是同一个路径下面的nii.gz的体数据哦。

由于chatgpt提到了数据不完整。

因此为了验证这种猜测。我首先拿一个完整的序列,用生成nii.gz的算法,手动生成一个体数据。
生成体数据,可以依赖simpleITK提供的python代码,很方便。

import SimpleITK as sitk

series_path = "/path/to/dicom"
volume_file_path = "/path/to/xxx.nii.gz"
reader = sitk.ImageSeriesReader()
dicom_series = reader.GetGDCMSeriesFileNames(series_path)
reader.SetFileNames(dicom_series)
dicom_volume = reader.Execute()

# 修改生成体数据的像素类型,由默认的int32修改为int16,减小生成体数据的大小.
dicom_volume = sitk.Cast(dicom_volume, sitk.sitkInt16)
sitk.WriteImage(dicom_volume, volume_file_path)

1> 完整的一个序列,生成体数据, 1.nii.gz;
2> 去掉前面50张,生成体数据, 2.nii.gz;
3> 从中间,随机的删除掉50张,生成体数据, 3.nii.gz;

总之,就是随机的从序列内,删除掉不确定的张数,让这个序列内的影像不再连续。
然后将生成的nii.gz文件,依次导入slicer中进行观察。
在这里插入图片描述

根据观察,如果人为地将序列内的部分影像删除,再将生成体数据,放到slicer里面渲染MPR.
可以看到冠状位和矢状位,会出现明显的阶梯。就是由于缺失图像,导致的不连续。但是并没有出现医院反馈的,灰色方框的问题。

那接下来该如何定位呢?

[2024.04.01]
书接上回。

说到定位问题,有时候可能真的是得相信灵感。
当时早上我到单位后,手动删除序列内的部分影像后,在slicer里面渲染出来的MPR仍然是正常不带灰色方框的。已经不知道下一步该如何定位问题了。
这时负责C模块的同事来单位了,我赶紧叫住他,给他说明我目前排查问题的进展。他说了一句,有没有可能自动重建的时候,体数据还没有生成呢?

如果是我查问题前,他这么说,我是一定不认可的。
因为日志写得很清楚,
1> A模块日志显示:图像已经从PACS拉下来了,而且显示拉取下来的张数,和最终生成体数据的张数,是相等的。而且之后也没有发生过二次拉取,就是一次就拉取完整了;
2> B模块日志显示,在A模块发出指令后,B模块也生成体数据完成。也是一次就生成了,没有第二次生成。

但是当我们查C模块的日志时,发生了一些异常。
正常情况下,线上环境,模块不应该出现很多的报错。但是,
C模块日志,有很多的报错。甚至有一个关键的报错,就是它在进行自动重建时,体数据的文件竟然没有找到。也可以理解为,还没有开始生成。

我的大脑飞快地联想,突然意识到一个问题。
那就是,当时为了更快地处理任务,我们把一些耗时的操作,都做成了异步。就比如生成体数据的逻辑,是异步逻辑。
这意味着,当B模块通知C模块,开始自动重建时,它其实并不能确认体数据已经生成完毕。因为它是发起了一个异步的任务。

那么这样再看,问题就很明显了。C模块在自动重建时,会遇到三种情况:
1> 系统I/O很快,C模块自动重建时,体数据已经生成完整;
2> 系统I/O压力大,体数据还没来得及生成,或正在生成中。
后面两种情况,都可能会导致C模块渲染时,出现灰色方框的问题。

其实事后再看,那个灰色的方框,它下面1/3是图像清晰的,而上面2/3是灰色的。也说明文件指针正在从下往下一层一层地写体数据。但是在写文件的过程中,另一个进程来读图,导致读取到的上面,未写完的2/3是灰色的。

问题定位后,再修复的话,就很简单了。
找到了B模块中,生成体数据的逻辑。把之前的异步生成,直接修改为同步生成,即可修复。

# 问题代码
from multiprocessing import Process

def _generate_volume_file(series_path, volume_path):
	# 生成体数据的逻辑.
    reader = sitk.ImageSeriesReader()
    dicom_series = reader.GetGDCMSeriesFileNames(series_path)
    reader.SetFileNames(dicom_series)
    dicom_volume = reader.Execute()
    # 修改生成体数据的像素类型,由默认的int32修改为int16,减小生成体数据的大小.
    dicom_volume = sitk.Cast(dicom_volume, sitk.sitkInt16)
    sitk.WriteImage(dicom_volume, volume_path)

# 修改前
Process(target=_generate_volume_file, args=(series_path, volume_path)).start()

# 修改后:
# Process(target=_generate_volume_file, args=(series_path, volume_path)).start()
_generate_volume_file(series_path, volume_path)

将上面代码中的: Process().start()去掉,直接运行里面的生成体数据的函数即可。

修改完成后,再看回传给PACS后的MPR和MIP的影像,就是正常的了。困然了一周多的问题,终于解决了。
在这里插入图片描述

3. 回顾与总结:

困扰了一周的问题,终于解决了,内心当然是非常欢喜的。但是,当我仔细想,为什么这个看起来,显而易见的问题,在公司的几轮测试中,都没有发现呢。就这样就交付到了现场。

后来仔细复盘,中间是有几次,可以在交付前,就解决这个问题的。

1> 这个医院其实去年10月份就上线了这个产品。但是中间12月份应该是进行过一次大的基础服务的升级。而基础服务升级,也势必会影响产品线的功能。最主要的影响是,其实生成体数据的机制发生了改变。
之前是通过定时任务,就是B模块通知C模块,是通过定时任务,每隔1分钟,开始将这1分钟内所有处理完的序列,通知C模块进行重建。那么这1分钟的定时轮询,是可以让B模块将体数据生成完整的。而且最重要的是,之前就是同步生成体数据;
但是,本次系统大升级,显然是忽略了这个细节。忽略了这里当时为什么要用同步,而是统一修改为了异步。
2> 测试组在进行第二轮测试时,也是主要观察推送的任务,是否已经完成了。没有太去实际地关注推送到PACS上面的图片,是否能够正常的展示。
由于第一轮测试时,已经基本都测试过了。所以第二轮测试时,这块可能也忽略了。
3> 研发在遇到医院反馈的问题时,也是凭借过去的经验,总觉得刚上线时,都是正常的;中间也没有做过太大的修改,怎么突然就不正常工作了呢?一定是医院这边,或者第三方出现了一些故障。

局部最优不是全局最优

在软件设计和开发时,每个模块往往希望自己的性能做得非常好,每分钟能处理多少任务。但是,如果作为一个系统要考虑的话,每个模块都做到最快,就会面临着竞争,资源争抢等现实问题。
有时候如果内存占用过高,还会触发linux系统的oom, 将占用内存高的进程,或随机kill掉某个进程,导致整个服务无法正常工作,得不偿失。

本案例中,B模块,为了能够不影响处理任务的速度,而将生成体数据作为异步处理,但是忽略了实际场景中,C模块读取体数据的时机。从而埋下了隐患。在以后的开发中,争取能够引以为戒。

程序员的工作,真得像医生一样。找准症结,一针下去,药到病除。

你想不想体验一下这样的工作呢?

(完)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

inter_peng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值