DELPHI高性能大容量SOCKET并发(九):稳定性问题解决

IOCP接收缓存导致的内存错乱

在用IOCP控件写了一个ERP服务器后,服务器会发生运行3天后,出现莫名的内存错误,用FastMM检测,是本没有内存错误的地方,而且内存错误出现的地方也不固定。这是一个不可重现的Bug,后续通过打日志把错误范围缩小后发现,每次出现内存错误之前都是由于有链接断开释放,因此就加了日志逐步定位到是TSocketHandle释放引起的,具体原因是:在IOCP中,每个Socket连接需要投递一个接收请求,并给出数据存放内存,原来是销毁TSocketHandle的同时,销毁投递接收请求的缓存,这样有可能对象销毁后,IOCP返回一个异步接收消息,会导致写入到已销毁的接收缓存,造成内存被重写,导致内存错误。

解决办法,是用锁和对象分离相同的机制,把接收缓存和对象分离,在释放对象的时候不释放接收缓存,等待超过30分钟后,重新使用这个锁和接受缓存,这样做即可以解决内存错乱问题,也起到了锁和接收缓存的池化处理。

具体代码处理:

投递请求缓存和对象分开,采用是锁和对象分离相同的机制。

  {* 客户端对象和锁 *}
  TClientSocket = record
    Lock: TCriticalSection;
    SocketHandle: TSocketHandle;
    IocpRecv: TIocpRecord; //投递请求结构体
    IdleDT: TDateTime;
  end;
  PClientSocket = ^TClientSocket;
在释放TSocketHandle的时候,只释放对象,投递请求缓存不释放,和锁一起保留,加入到空闲列表中。
procedure TSocketHandles.Delete(const AIndex: Integer);
var
  ClientSocket: PClientSocket;
begin
  ClientSocket := FList[AIndex];
  ClientSocket.Lock.Enter;
  try
    ClientSocket.SocketHandle.Free;
    ClientSocket.SocketHandle := nil;
  finally
    ClientSocket.Lock.Leave;
  end;
  FList.Delete(AIndex);
  ClientSocket.IdleDT := Now;
  FIdleList.Add(ClientSocket);
end;
在加入对象的时候,检测空闲列表是否有超过30分钟没使用的,如果有则重复利用。
function TSocketHandles.Add(ASocketHandle: TSocketHandle): Integer;
var
  ClientSocket, IdleClientSocket: PClientSocket;
  i: Integer;
begin
  ClientSocket := nil;
  for i := FIdleList.Count - 1 downto 0 do
  begin
    IdleClientSocket := FIdleList.Items[i];
    if Abs(MinutesBetween(Now, IdleClientSocket.IdleDT)) > 30 then
    begin
      ClientSocket := IdleClientSocket;
      FIdleList.Delete(i);
      Break;
    end;
  end;
  if not Assigned(ClientSocket) then
  begin
    New(ClientSocket);
    ClientSocket.Lock := TCriticalSection.Create;
    ClientSocket.IocpRecv.WsaBuf.buf := GetMemory(MAX_IOCPBUFSIZE);
    ClientSocket.IocpRecv.WsaBuf.len := MAX_IOCPBUFSIZE;
  end;
  ClientSocket.SocketHandle := ASocketHandle;
  ClientSocket.IdleDT := Now;
  ASocketHandle.FLock := ClientSocket.Lock;
  ASocketHandle.FIocpRecv := @ClientSocket.IocpRecv;
  Result := FList.Add(ClientSocket);
end;

CheckDisconnectedClient方法加锁及判断是否正在执行

原来检测释放断开连接的方法如下:

procedure TIocpServer.CheckDisconnectedClient;
var
  i: Integer;
begin
  FSocketHandles.Lock;
  try
    for i := FSocketHandles.Count - 1 downto 0 do
    begin
      if not FSocketHandles.Items[i].SocketHandle.Connected then
      begin
        FSocketHandles.Delete(i);
      end;
    end;
  finally
    FSocketHandles.UnLock;
  end;
end;
这个方法存在以下问题:

1、对整个FSocketHandles加锁,FSocketHandles.Delete在释放的时候又加了一次锁,如果Delete加锁等待,则导致整个FSocketHandles被锁住,这时再加连接就会等待,造成IOCP无法接收连接,从而存在问题。

2、如果某个TScoketHandle执行很长时间,它的Connected属性为False,则FSocketHandles.Delete会锁住,造成和1相同的问题。

解决办法:

1、不对整个FSocketHandles加锁,我每次查找一个Connected为False的连接,避免一次加两个锁。

2、TSocketHandle增加一个属性标识是否正在执行中,在检测断开连接的时候如果正在执行中则跳过。

具体代码如下:

procedure TIocpServer.CheckDisconnectedClient;
var
  iCount: Integer;
  ClientSocket: PClientSocket;

  function GetDisconnectSocket: PClientSocket;
  var
    i: Integer;
  begin
    Result := nil;
    FSocketHandles.Lock;
    try
      for i := FSocketHandles.Count - 1 downto 0 do
      begin
        if (not FSocketHandles.Items[i].SocketHandle.Connected)
          and (not FSocketHandles.Items[i].SocketHandle.Executing) then
        begin
          Result := FSocketHandles.Items[i];
          Break;
        end;
      end;
    finally
      FSocketHandles.UnLock;
    end;
  end;
begin
  ClientSocket := GetDisconnectSocket;
  iCount := 0;
  while (ClientSocket <> nil) and (iCount < 1024 * 1024) do
  begin
    ClientSocket.Lock.Enter;
    try
      if Assigned(ClientSocket.SocketHandle) then
        FreeSocketHandle(ClientSocket.SocketHandle);
    finally
      ClientSocket.Lock.Leave;
    end;
    ClientSocket := GetDisconnectSocket;
    Inc(iCount);
  end;
end;
主要是使用GetDisconnectSocket来返回一个已经断开的连接。TSocketHandle.Executing的赋值只需要在下面方法中赋值即可,因为他执行的进入口和返回口。

procedure TSocketHandle.ProcessIOComplete(AIocpRecord: PIocpRecord;
  const ACount: Cardinal);
begin
  FExecuting := True;
  try
    case AIocpRecord.IocpOperate of
      ioNone: Exit;
      ioRead: //收到数据
      begin
        FActiveTime := Now;
        ReceiveData(AIocpRecord.WsaBuf.buf, ACount);
        if FConnected then
          PreRecv; //投递请求
      end;
      ioWrite: //发送数据完成,需要释放AIocpRecord的指针
      begin
        FActiveTime := Now;
        FSendOverlapped.Release(AIocpRecord);
      end;
      ioStream:
      begin
        FActiveTime := Now;
        FSendOverlapped.Release(AIocpRecord);
        WriteStream; //继续发送流
      end;
    end;
  finally
    FExecuting := False;
  end;
end;

解决这两个稳定性问题后,IOCP支持的ERP服务器已经能支持7*24小时运行。一般当服务器出现稳定性问题后,日志就开始发挥作用,但是太多无用的日志不利于定位到问题点,太少的日志又无法定位,一般写日志的原则是在调用顺序关键点上加日志、继承关键点上加日志、数据流输入输出关键点上加日志;这样出现问题后,可以快速把问题定位到一段代码上,能帮助缩短解决问题的周期。如果出现一个不可重现BUG,能控制在3天解决,出现一个内存错乱BUG,能控制在半个月解决,哪你的日志辅助调试就是有效的。

V1版下载地址:http://download.csdn.net/detail/sqldebug_fan/4510076,需要资源10分,有稳定性问题,可以作为研究稳定性用;

V2版下载地址:http://download.csdn.net/detail/sqldebug_fan/5560185,不需要资源分,解决了稳定性问题和提高性能;免责声明:此代码只是为了演示IOCP编程,仅用于学习和研究,切勿用于商业用途。水平有限,错误在所难免,欢迎指正和指导。邮箱地址:[email protected]


猜你喜欢

转载自blog.csdn.net/SQLDebug_Fan/article/details/9043699
今日推荐