【问题标题】:Why does calling the Tesseract process cause this service to crash randomly?为什么调用 Tesseract 进程会导致该服务随机崩溃?
【发布时间】:2020-02-28 17:41:14
【问题描述】:

我有一个 .NET Core 2.1 服务,它在 Ubuntu 18.04 VM 上运行并通过 Process 实例调用 Tesseract OCR 4.00。我想使用一个 API 包装器,但我只能找到一个可用的,而且它只是最新版本的 Tesseract 的测试版——稳定的包装器使用版本 3 而不是 4。过去,这项服务运行良好,但我一直在更改它,以便减少从磁盘写入和读取文档/图像数据的频率,以提高速度。该服务用于调用更多的外部进程(例如 ImageMagick),由于 API 的存在,这些进程是不必要的,所以我一直在用 API 调用替换它们。

最近我一直在使用从真实数据中提取的示例文件对此进行测试。这是一份传真文档 PDF,有 133 页,但由于灰度和分辨率的原因,它只有 5.8 MB。该服务获取一个文档,将其拆分为单独的页面,然后分配多个线程(每页一个线程)来调用 Tesseract 并使用Parallel.For 处理它们。线程限制是可配置的。我知道 Tesseract 有自己的多线程环境变量 (OMP_THREAD_LIMIT)。我在之前的测试中发现,将其设置为“1”对于我们目前的设置来说是理想的,但在我最近针对此问题的测试中,我尝试将其设置为未设置(动态值)而没有任何改进。

问题出乎意料的是,当调用 Tesseract 时,服务会挂起大约一分钟然后崩溃,journalctl 中显示的唯一错误是:

dotnet[32328]: Error while reaping child. errno = 10
dotnet[32328]:    at System.Environment.FailFast(System.String, System.Exception)
dotnet[32328]:    at System.Environment.FailFast(System.String)
dotnet[32328]:    at System.Diagnostics.ProcessWaitState.TryReapChild()
dotnet[32328]:    at System.Diagnostics.ProcessWaitState.CheckChildren(Boolean)
dotnet[32328]:    at System.Diagnostics.Process.OnSigChild(Boolean)

对于这个特定的错误,我在网上根本找不到任何东西。在我看来,根据我对 Process 类所做的相关研究,当进程退出并且 dotnet 试图清理它正在使用的资源时,就会发生这种情况。尽管我尝试了许多“猜测”,例如更改线程限制值,但我什至不知道如何解决这个问题。线程之间没有交叉。每个线程都有自己的页面分区(基于Parallel.For 对集合的分区方式),并且它设置为在这些页面上工作,一次一个。

这是进程调用,从多个线程中调用(我们通常设置的限制为 8):

private bool ProcessOcrPage(IMagickImage page, int pageNumber, object instanceId)
        {
            var inputPageImagePath = Path.Combine(_fileOps.GetThreadWorkingDirectory(instanceId), $"ocrIn_{pageNumber}.{page.Format.ToString().ToLower()}");
            string outputPageFilePathWithoutExt = Path.Combine(_fileOps.GetThreadOutputDirectory(instanceId),
                    $"pg_{pageNumber.ToString().PadLeft(3, '0')}");
            page.Write(inputPageImagePath);

            var cmdArgs = $"-l eng \"{inputPageImagePath}\" \"{outputPageFilePathWithoutExt}\" pdf";
            bool success;

            _logger.LogStatement($"[Thread {instanceId}] Executing the following command:{Environment.NewLine}tesseract {cmdArgs}", LogLevel.Debug);

            var psi = new ProcessStartInfo("tesseract", cmdArgs)
            {
                RedirectStandardError = true,
                RedirectStandardOutput = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };

            // 0 is not the default value for this environment variable. It should remain unset if there 
            // is no config value, as it is determined dynamically by default within OpenMP.
            if (_processorConfig.TesseractThreadLimit > 0) 
                psi.EnvironmentVariables.Add("OMP_THREAD_LIMIT", _processorConfig.TesseractThreadLimit.ToString());

            using (var p = new Process() { StartInfo = psi })
            {
                string standardErr, standardOut;
                int exitCode;
                p.Start();
                standardOut = p.StandardOutput.ReadToEnd();
                standardErr = p.StandardError.ReadToEnd();        
                p.WaitForExit();
                exitCode = p.ExitCode;

                if (!string.IsNullOrEmpty(standardOut))
                    _logger.LogStatement($"Tesseract stdOut:\n{standardOut}", LogLevel.Debug, nameof(ProcessOcrPage));
                if (!string.IsNullOrEmpty(standardErr))
                    _logger.LogStatement($"Tesseract stdErr:\n{standardErr}", LogLevel.Debug, nameof(ProcessOcrPage));
                success = p.ExitCode == 0;
            }

            return success;
        }

编辑 4:在聊天中与 Clint 进行了多次测试和讨论后,这是我们学到的。该错误是从 Process 事件“OnSigChild”引发的,从堆栈跟踪中可以明显看出这一点,但无法挂钩引发此错误的同一事件。给定 10 秒的超时时间,该过程永远不会超时(Tesseract 通常只需要几秒钟来处理给定的页面)。奇怪的是,如果进程超时被删除并且我等待标准输出和错误流关闭,它将挂起 20-30 秒,但在此挂起时间内进程不会出现在ps auxf 中。据我所知,Linux 能够确定进程已完成执行,但 .NET 不是。否则,错误似乎是在进程执行完成的那一刻引发的。

对我来说最莫名其妙的仍然是,与我们在生产中使用的该代码的工作版本相比,代码的流程处理部分确实没有太大变化。这表明这是我在某处犯的错误,但我根本无法找到它。我想我必须在 dotnet GitHub 跟踪器上打开一个问题。

【问题讨论】:

  • 你确定这段代码可以编译吗,你的流程实例是p,我也看到你这样调用ExitCode process.ExitCode;
  • 复制粘贴代码时出错,我现在改正

标签: c# linux multithreading .net-core tesseract


【解决方案1】:

“收割孩子时出错”

进程占用内核中的一些资源,在 Unix 上,当父进程死亡时,init 进程负责清理 Zombine 和孤儿进程(也就是收割子进程)的内核资源。 .NET Core 在子进程终止后立即获取它们。

"我发现删除 stdout 和 stderr 流 ReadToEnd 调用导致进程立即结束而不是挂起, 同样的错误”

错误是由于您在进程完成之前就过早地调用p.ExitCode 而使用ReadToEnd 您只是在延迟此活动

更新代码总结

  • StartInfo.FileName 应该指向你要启动的文件名
  • UseShellExecute 如果进程应该直接从可执行文件创建,则为 false;如果您打算在启动进程时使用 shell,则为 true;
  • 向标准输出和错误流添加了异步读取操作
  • AutoResetEvents 在输出时发出信号,在操作完成时发出错误提示
  • Process.Close()释放资源
  • 在 Arguments 属性上设置和使用ArgumentList 更容易

Redhat Blog on NetProcess on Linux

修订模块

private bool ProcessOcrPage(IMagickImage page, int pageNumber, object instanceId)
{
    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();
    int exitCode;
    var inputPageImagePath = Path.Combine(_fileOps.GetThreadWorkingDirectory(instanceId), $"ocrIn_{pageNumber}.{page.Format.ToString().ToLower()}");
        string outputPageFilePathWithoutExt = Path.Combine(_fileOps.GetThreadOutputDirectory(instanceId),
                $"pg_{pageNumber.ToString().PadLeft(3, '0')}");
    page.Write(inputPageImagePath);

    var cmdArgs = $"-l eng \"{inputPageImagePath}\" \"{outputPageFilePathWithoutExt}\" pdf";
    bool success;

    _logger.LogStatement($"[Thread {instanceId}] Executing the following command:{Environment.NewLine}tesseract {cmdArgs}", LogLevel.Debug);


    using (var outputWaitHandle = new AutoResetEvent(false))
    using (var errorWaitHandle = new AutoResetEvent(false))
    {
        try
        {
            using (var process = new Process())
            {
                process.StartInfo = new ProcessStartInfo
                { 
                    WindowStyle = ProcessWindowStyle.Hidden,
                    FileName = "tesseract.exe", // Verify if this is indeed the process that you want to start ?
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    Arguments = cmdArgs,
                    WorkingDirectory = Path.GetDirectoryName(path)
                };



                if (_processorConfig.TesseractThreadLimit > 0) 
                    process.StartInfo.EnvironmentVariables.Add("OMP_THREAD_LIMIT", _processorConfig.TesseractThreadLimit.ToString());


                process.OutputDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        outputWaitHandle.Set();
                    }
                    else
                    {
                        output.AppendLine(e.Data);
                    }
                };
                process.ErrorDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        errorWaitHandle.Set();
                    }
                    else
                    {
                        error.AppendLine(e.Data);
                    }
                };

                process.Start();

                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                  if (!outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds) && !errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds) && !process.WaitForExit(ProcessTimeOutMiliseconds))
                  {
                    //To cancel the read operation if the process is stil reading after the timeout this will prevent ObjectDisposeException
                    process.CancelOutputRead();
                    process.CancelErrorRead();

                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine("Timed Out");

                    //To release allocated resource for the Process
                    process.Close();
                    //Timed out
                    return  false;
                  }

                  Console.ForegroundColor = ConsoleColor.Green;
                  Console.WriteLine("Completed On Time");

                 exitCode = process.ExitCode;

                  if (!string.IsNullOrEmpty(standardOut))
                    _logger.LogStatement($"Tesseract stdOut:\n{standardOut}", LogLevel.Debug, nameof(ProcessOcrPage));
                  if (!string.IsNullOrEmpty(standardErr))
                    _logger.LogStatement($"Tesseract stdErr:\n{standardErr}", LogLevel.Debug, nameof(ProcessOcrPage));

                 process.Close();

                 return exitCode == 0 ? true : false;
            }
        }
        Catch
        {
           //Handle Exception
        }
    }
}

【讨论】:

  • 感谢您的回答。您的帖子让我对 Process API 有了很多深入的了解,但不幸的是,在使用此代码时,我仍然收到相同的错误,而且不一致。看起来即使使用超时,在进程超时之前我仍然会收到“收割孩子时出错”,这会导致 dotnet 崩溃并重新启动服务。我将不得不尝试几次,看看它是否与导致 Tesseract 崩溃的单个页面数据有某种关系,但它不应该相关,因为该文件在该代码的旧版本中运行良好。
  • @LucasLeblanc,嗯,我希望你已经尝试了 sn-p 并且没有错过任何一行,你能告诉我它在哪一行抛出“收获孩子时出错”。我们可以发起群聊,我很好奇,想深入了解一下
  • 没有追踪到一行。该异常与其余代码“异步”发生。在我的原始帖子中检查它的堆栈跟踪,即完整的堆栈跟踪。我确实使用了你的代码,我唯一添加的是日志语句
  • 不幸的是,我无法创建聊天室,因为我在这个赏金上使用了太多的代表。你能做到吗?
  • @LucasLeblanc,我已经创建并邀请你加入聊天室,如果你知道了,请告诉我
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-28
  • 1970-01-01
  • 2019-09-08
  • 1970-01-01
  • 1970-01-01
  • 2015-10-15
相关资源
最近更新 更多