SQL Server使用VDI与命名管道还原数据库报38(EOF)错误问题
我使用Go与C开发了Windows平台下的Microsoft SQL Server数据库备份/还原应用,通过VDI(虚拟设备接口)和命名管道实现数据传输。目前备份功能运行完全正常,但执行还原操作时流程始终无法完成,SQL Server会中止操作并抛出38错误(EOF,文件已结束)。
当前实现架构:C侧实现命名管道服务端,同时通过VDI虚拟设备读取管道数据传输至SQL Server;Go侧实现命名管道客户端,测试阶段逻辑为读取磁盘上的有效备份文件写入管道,后续将对接其他写入数据源。经测试,直接读取本地备份文件可正常完成数据库还原,仅当链路中引入命名管道时会触发崩溃。
- 备份文件有效性确认:应用生成的备份文件可通过SQL Server Management Studio正常完成数据库还原;
- 命名管道模式确认:命名管道服务端与客户端均采用duplex读写模式,与已公开的同类问题场景不一致。
C++ 命名管道服务端核心代码(仅展示VDC_Read处理逻辑)
hPipe = CreateNamedPipe(fname, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, 1024 * 16, 1024 * 16, NMPWAIT_USE_DEFAULT_WAIT, NULL); if (hPipe != INVALID_HANDLE_VALUE) { printf("[ExecuteTransfer] Named pipe %s created successfully\n", fname); } if (ConnectNamedPipe(hPipe, NULL) != FALSE) // 等待客户端连接管道 { printf("[ExecuteTransfer] Starting transfer....\n"); while (SUCCEEDED (hr)) { dwTransferredBytes = 0; printf("[ExecuteTransfer] Executing GetCommand...\n"); hr = vd->GetCommand(INFINITE, &cmd); if (!SUCCEEDED (hr)) { printf("[ExecuteTransfer] Error executing GetCommand. x%X\n", hr); break; } else { printf("[ExecuteTransfer] GetCommand executed successfully\n"); } switch (cmd->commandCode) { case VDC_Read: printf("[ExecuteTransfer] VDC_Read\n"); printf("[ExecuteTransfer] %d bytes to read....\n", cmd->size); fSuccess = ReadFile(hPipe, cmd->buffer, cmd->size, &dwTransferredBytes, NULL); gle = GetLastError(); if (fSuccess) { completionCode = ERROR_SUCCESS; printf("[ExecuteTransfer] %d bytes read....\n", dwTransferredBytes); } else { if (gle == ERROR_BROKEN_PIPE) printf("[ExecuteTransfer] Read Error: client disconnected.\n"); else printf("[ExecuteTransfer] Read Error, GLE=%d.\n", gle); } if (dwTransferredBytes == 0) { printf("[ExecuteTransfer] 0 bytes transferred. Handling EOF...\n"); completionCode = ERROR_HANDLE_EOF; } break;
Go 命名管道客户端测试代码
测试逻辑为读取本地有效备份文件写入C++创建的命名管道,其中20220531081651.CSYN4HMG0F3.emorydev.FULL为应用生成的可正常还原的备份文件。
vReadFromFileFinished := make(chan bool) go func() { var vFileToRead *os.File var err error time.Sleep(time.Second * 3) // 等待C++侧创建命名管道 vFilenameToRead := "c:\\tmp\\20220531081651.CSYN4HMG0F3.emorydev.FULL" if vFileToRead, err = os.OpenFile(vFilenameToRead, os.O_RDONLY, 0666); err != nil { fmt.Println("[restore] Error opening file", vFilenameToRead) os.Exit(-1) } defer vFileToRead.Close() vReader := bufio.NewReader(vFileToRead) fmt.Println("[restore] Opening Named Pipe", gvDataSource.MSSQL.GetNamedPipeString()) vFileToWrite, _ := os.OpenFile(gvDataSource.MSSQL.GetNamedPipeString(), os.O_RDWR, 0666) // defer vFileToWrite.Close() vWriter := bufio.NewWriter(vFileToWrite) var vBytesRead int var vBytesWritten int var vTotalBytesRead int var vTotalBytesWritten int vBuffer := make([]byte, 4096) for { vBytesRead, _ = vReader.Read(vBuffer) if vBytesRead == 0 { break } vBytesWritten, _ = vWriter.Write(vBuffer) vTotalBytesRead = vTotalBytesRead + vBytesRead vTotalBytesWritten = vTotalBytesWritten + vBytesWritten } fmt.Printf("[restore] Read from %s and write to % s finished\n", vFilenameToRead, gvDataSource.MSSQL.GetNamedPipeString()) fmt.Printf("[restore] Total bytes read = %d\n", vTotalBytesRead) fmt.Printf("[restore] Total bytes written = %d\n", vTotalBytesWritten) vReadFromFileFinished <- true }() <-vReadFromFileFinished
初始场景运行现象
SQL Server VDI标准执行流程为GetCommand-命令处理-CompleteCommand,前期运行正常,直到单次从管道读取的字节数小于GetCommand要求的字节数后,会触发VDC_ClearError调用,最终GetCommand返回x80770004错误中止。日志显示Go侧读取的备份文件总大小为3105280字节,但实际写入管道的仅为73728字节。Windows事件查看器报错如下:
BackupIoRequest::ReportIoError: 备份设备读取错误,操作系统返回错误38(已到达文件末尾)
调整缓冲区逻辑后的运行现象
修改C++侧逻辑,在GetCommand后、调用CompleteCommand前循环读取直到填满GetCommand要求的缓冲区大小,此时不再触发EOF错误,但执行若干次VDC_Read后GetCommand会异常中止,返回x-7F88FFFC错误,日志显示总传输字节数仅为205312字节,Go侧写入管道字节数为208896字节。此时Windows事件查看器记录SQLVDI触发主动中止,同时SQL Server生成EXCEPTION_ACCESS_VIOLATION(访问违例)的堆栈转储,异常信息显示执行RESTORE DATABASE语句时触发内存访问违规,当前无法定位该访问违例的产生原因,寻求同类问题的解决方案。
内容的提问来源于stack exchange,提问作者Pere

