TCP Socket Высокая загрузка ЦП / памяти через 5 минут - PullRequest
0 голосов
/ 15 января 2019

Я создаю серверное приложение, которое принимает входящие TCP-соединения. (примерно 300 уникальных клиентов). Важно отметить, что я не контролирую клиентов.

Я обнаружил, что некоторые из подключающихся клиентов остаются бездействующими в течение некоторого времени после установления первоначального подключения и отправки первого обновления статуса. Когда они простаивают более 5 минут, загрузка ЦП приложения увеличивается до 90% и остается там.

Для решения этой проблемы я встроил токен отмены, который срабатывает через 4 минуты. Это позволяет мне разорвать соединение. Затем клиент обнаруживает это и повторно подключается примерно через минуту. Это решает проблему высокой загрузки ЦП, но побочным эффектом является высокая загрузка памяти, поскольку, похоже, произошла утечка памяти. Я подозреваю, что ресурсы удерживаются предыдущим объектом сокета.

У меня есть объект client, который содержит соединение с сокетом и информацию о подключенном клиенте. Он также управляет входящими сообщениями. Существует также класс менеджера, который принимает входящие соединения. Затем он создает объект client, назначает ему сокет и добавляет объект client в параллельный словарь. Каждые 10 секунд он проверяет словарь на наличие клиентов, для которых установлено значение _closeConnection = true, и вызывает их метод dispose.

Вот код объекта клиента:

public void StartCommunication()
    {
        Task.Run(async () =>
        {
            ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[75]);
            while (IsConnected)
            {
                try
                {
                    // This is where I suspect the memory leak is originating - this call I suspect is not properly cleaned up when the object is diposed
                    var result = await SocketTaskExtensions.ReceiveAsync(ClientConnection.Client, buffer, SocketFlags.None).WithCancellation(cts.Token);

                    if (result > 0)
                    {
                        var message = new ClientMessage(buffer.Array, true);
                        if(message.IsValid)
                            HandleClientMessage(message);
                    }
                }
                catch (OperationCanceledException)
                {
                    _closeConnection = true;
                    DisconnectReason = "Client has not reported in 4 mins";
                }
                catch (Exception e)
                {
                    _closeConnection = true;
                    DisconnectReason = "Error during receive opperation";
                }
            }
        });
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _closeConnection = true;
            cts.Cancel();
            // Explicitly kill the underlying socket
            if (UnitConnection.Client != null)
            {
                UnitConnection.Client.Close();
            }

            UnitConnection.Close();
            cts.Dispose();
        }
    }

Метод расширения задачи:

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
        {
            if (task != await Task.WhenAny(task, tcs.Task))
            {
                throw new OperationCanceledException(cancellationToken);
            }
        }

        return task.Result;
    }

Код Mananger:

    public bool StartListener()
    {
        _listener = new TcpListenerEx(IPAddress.Any, Convert.ToInt32(_serverPort));
        _listener.Start();
        Task.Run(async () =>
        {
            while (_maintainConnection) // <--- boolean flag to exit loop
            {
                try
                {
                    HandleClientConnection(await _listener.AcceptTcpClientAsync());
                }
                catch (Exception e)
                {
                    //<snip>
                }
            }
        });
        return true;
    }

    private void HandleClientConnection(TcpClient client)
    {
        Task.Run(async () =>
        {
            try
            {
                // Create new Coms object
                var client = new ClientComsAsync();
                client.ClientConnection = client;
                // Start client communication
                client.StartCommunication();

                //_clients is the ConcurrentDictionary

                ClientComsAsync existingClient;
                if (_clients.TryGetValue(client.ClientName, out existingClient) && existingClient != null)
                {
                    if (existingClient.IsConnected)
                        existingClient.SendHeatbeat();
                    if (!existingClient.IsConnected)
                    {
                        // Call Dispose on existing client
                        CleanUpClient(existingClient, "Reconnected with new connection");
                    }
                }
            }
            catch (Exception e)
            {
                //<snip>
            }
            finally
            {
                //<snip>
            }
        });
    }

    private void CleanUpClient(ClientComsAsync client, string reason)
    {
        ClientComsAsync _client;
        _units.TryRemove(client.ClientName, out _client);
        if (_client != null)
        {
            _client.Dispose();
        }
    }

1 Ответ

0 голосов
/ 31 января 2019

Когда они простаивают более 5 минут, загрузка ЦП приложения увеличивается до 90% и остается там.

Для решения этой проблемы я встроил токен отмены, который срабатывает через 4 минуты.

Правильный ответ - решить проблему высокой загрузки ЦП.

Похоже, это здесь:

while (IsConnected)
{
  try
  {
    var result = await SocketTaskExtensions.ReceiveAsync(ClientConnection.Client, buffer, SocketFlags.None);

    if (result > 0)
    {
      ...
    }
  }
  catch ...
  {
    ...
  }
}

Сокеты странные, и правильно работать с необработанными сокетами TCP / IP. Кроме того, я всегда рекомендую разработчикам использовать что-то более стандартное, например HTTP или WebSockets, но в этом случае вы не контролируете клиентов, так что это не вариант.

В частности, ваш код не обрабатывает случай, когда result == 0. Если клиентские устройства изящно закрыли свой сокет, вы увидите result из 0, сразу же вернетесь назад и продолжите получать result из 0 - плотный цикл, который использует CPU.

Это, конечно, предполагается, что IsConnected остается true. И это может быть возможно ...

Вы не показываете, где IsConnected установлено в вашем коде, но я подозреваю, что оно находится в обработке ошибок после отправки сообщения пульса. Вот почему это может работать не так, как ожидалось ... Я подозреваю, что клиентские устройства закрывают свой поток отправки (ваш поток приема), сохраняя при этом свой поток получения (ваш поток отправки) открытым. Это один из способов закрыть сокет, который иногда считается «более вежливым», поскольку он позволяет другой стороне продолжать отправку данных, даже если эта сторона закончила отправку. (Это с точки зрения клиентского устройства, поэтому «другая сторона» - это ваш код, а «эта сторона» - это клиентское устройство).

И это совершенно законно для сокетов, потому что каждый подключенный сокет - это два потока, а не один, каждый из которых может быть независимо закрыт. Если это произойдет, ваше сердцебиение все равно будет отправлено и получено без ошибок (и, вероятно, просто молча отброшено клиентским устройством), IsConnected останется true, и цикл чтения станет синхронным и поглотит ваш ЦП.

Чтобы решить эту проблему, добавьте проверку для result == 0 в цикле чтения и очистите клиент так же, как если бы сердцебиение не удалось отправить.

...