Лучшая практика для жизни прокси-сервера WCF - или как часто закрывать прокси-сервер WCF?
Я работал над WPF-приложением, которое использует WCF для доступа к логике и базе данных на стороне сервера.
Я начал с одного прокси-объекта клиента WCF, который я неоднократно использовал для вызова методов на сервере. После некоторого использования прокси-сервера сервер в конечном итоге исключит исключение:
System.ServiceModel.EndpointNotFoundException: There was no endpoint listening at http://.../Service/BillingService.svc that could accept the message. This is often caused by an incorrect address or SOAP action. See InnerException, if present, for more details. ---> System.Net.WebException: Unable to connect to the remote server ---> System.Net.Sockets.SocketException: An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full
.
Я думаю, это связано с тем, что каждый служебный вызов открывает новый сокет от прокси-сервера до сервера и никогда не закрывает его. В конце концов сервер был затоплен и начал отказывать в запросах.
После краткого поиска, я решил, что мне нужно периодически закрывать() прокси. Образцы, которые я нашел, вырождены. Этот дал некоторые полезные подсказки, но на самом деле не отвечает на вопрос. Я также видел рекомендации, чтобы избежать шаблона use() (и вместо этого применить try/catch/finally), потому что метод dispose Dispose может вызывать исключение (yuck).
Кажется, что рекомендуемый шаблон формируется следующим образом:
[TestClass]
public class WCFClientUnitTest
{
BillingServiceClient _service;
[TestMethod]
public void TestGetAddressModel()
{
List<CustomerModel> customers = null;
try
{
_service = new BillingServiceClient();
customers = _service.GetCustomers().ToList();
}
catch
{
_service.Abort();
_service = null;
throw;
}
finally
{
if ((_service != null) &&
(_service.State == System.ServiceModel.CommunicationState.Opened))
_service.Close();
_service = null;
}
if (customers != null)
foreach (CustomerModel customer in customers)
{
try
{
_service = new BillingServiceClient();
AddressModel address = (AddressModel)_service.GetAddressModel(customer.CustomerID);
Assert.IsNotNull(address, "GetAddressModel returned null");
}
catch
{
_service.Abort();
_service = null;
throw;
}
finally
{
if ((_service != null) &&
(_service.State == System.ServiceModel.CommunicationState.Opened))
_service.Close();
_service = null;
}
}
}
Итак, мой вопрос по-прежнему вращается вокруг того, как долго я должен поддерживать клиентский прокси? Должен ли я открывать/закрывать его для каждого запроса на обслуживание? Это кажется чрезмерным для меня. Не понесу ли я значительный успех?
То, что я действительно хочу сделать, это создать и открыть канал и сделать краткий всплеск повторяющихся коротких последовательных вызовов службы по каналу. Затем хорошо закройте канал.
В качестве побочного примечания, пока я его еще не реализовал, я скоро добавлю к этой службе модель безопасности (как SSL, так и ACL), чтобы ограничить, кто может вызвать методы службы. В одном из ответов на этот пост упоминается, что пересмотр контекста аутентификации и безопасности делает повторный запуск канала для каждого вызова службы расточительным, но просто рекомендует избегать построения контекста безопасности.
EDIT 11/3/2010: Это кажется важным, поэтому я добавляю его к вопросу...
В ответ на комментарий/предложение Andrew Shepherd я перезапустил мой unit test с отключением TrendMicro AntiVirus, контролируя вывод netstat -b. Netstat смог зафиксировать значительный рост открытых портов, принадлежащих WebDev.WebServer40.exe. Подавляющее большинство портов находилось в состоянии TIME_WAIT. Microsoft говорит, что порты могут задерживаться в NET_WAIT после того, как клиент закроет соединение...
ПРИМЕЧАНИЕ. Нормально иметь гнездо в состояние TIME_WAIT в течение длительного периода времени времени. Время указано в RFC793 в два раза больше максимального сегмента Время жизни (MSL). MSL определяется как 2 минуты. Таким образом, сокет может находиться в TIME_WAIT до тех пор, пока 4 минут. Некоторые системы реализуют разные значения (менее 2 минут) для MSL.
Это заставляет меня думать, что если каждый вызов службы открывает новый сокет на сервере, и потому что я вызываю службу в узком цикле, я мог бы легко затопить сервер, в результате чего у него закончились доступные сокеты и в turn генерирует исключение, указанное выше.
Поэтому мне нужно продолжить один из двух путей:
1) попытка пакетных вызовов службы, чтобы они повторно использовали сокет на стороне сервера
2) измените мой контракт на обслуживание, чтобы я мог вернуть более крупные куски данных с меньшим количеством вызовов.
Первый выбор кажется мне лучше, и я продолжу его преследовать. Я отправлю назад то, что обнаруживаю, и приветствую дальнейшие комментарии, вопросы и ответы.
Ответы
Ответ 1
Кажется, здесь есть несколько вещей.
Сначала unit test я запускал и отслеживал с помощью netstat -b, указывал, что Cassini (WebDev.WebServer40.exe) был владельцем портов, которые накапливались в состоянии TIME_WAIT. Как отмечается в статье MSFT kb, нормальным является то, что порты будут задерживаться после установления связи FIN, пока приложение ожидает доставки медленных пакетов в сети и очереди сообщений. Конфигурация по умолчанию, равная 2 минутам, объясняет, почему я увидела симуляцию портов после завершения моего unit test. Хотя можно изменить MSL через параметр реестра, он не рекомендуется.
Но, важный момент, который я почти забыл, заключается в том, что служба работает под Cassini. Когда я переключаю конечную точку сервера на работу под IIS7, я не испытываю никакого роста порта вообще! Я не могу объяснить, означает ли это, что клиент повторно использует порты или IIS7 лучше, чем Cassini, при очистке портов после их завершения.
Теперь это не полностью отвечает на мой вопрос о том, как часто я должен закрывать прокси-сервер WCF. Это просто означает, что я не имел, чтобы закрыть прокси-сервер часто.
Я вижу, что все еще есть компромисс ресурсов, чтобы поддерживать прокси-сервер открытым в течение длительного периода времени.
Если у вас много (то есть тысяч) клиентов, обращающихся к вашей службе WCF, может иметь смысл освободить ресурсы сервера между вызовами или небольшие партии вызовов. В этом случае обязательно используйте механизм try/catch/finally и не используйте(), потому что хотя прокси-сервер службы реализует IDisposable, метод close() может генерировать исключение, если служба находится в состоянии сбоя.
С другой стороны (как и в моем конкретном случае), если вы только ожидаете, что несколько клиентов получат доступ к вашей службе WCF, вам не потребуется дополнительная сложность часто и явно открывать и закрывать прокси-сервер службы. Поэтому я намерен открыть прокси-сервер один раз, когда мое приложение запустится, и оставьте его открытым до завершения приложения. Я намереваюсь внедрить метод вспомогательного вызова службы (похоже на: Обновление клиента WCF при истечении срока действия SCT?), который будет перерабатывать соединение, на всякий случай, когда он когда- неисправное состояние. Тогда мне не нужно беспокоиться об управлении временем прокси-сервера.
Пожалуйста, дайте мне знать, если я думаю, что неправильно читаю результаты своих тестов, или если у вас есть лучшее решение.
Ответ 2
(Проводка совершенно другого второго ответа)
Если мы говорим о "лучшей практике", то наилучшей практикой с WCF является "крупнозернистые методы". Если клиент вызывает метод несколько раз в цикле, тогда вся бизнес-логика должна быть перенесена в саму службу.
Например.
[DataContract]
class CustomerAndAddress
{
[DataMember]
CustomerModel Customer;
[DataMember]
AddressModel Address;
}
[ServiceContract]
class BillingService
{
[OperationContract]
CustomerAndAddress[] GetAllCustomersAndAddresses();
}
или, более вероятно, в реальном мире:
[ServiceContract]
class BillingService
{
[OperationContract]
CustomerReportData FetchCustomerReportInfo(CustomerReportParameters parameterSet);
}
Сказав это, мне все еще интересно узнать, можете ли вы снять то, что вы пытаетесь.
Ответ 3
Я написал (ну, нашел и изменил) обертку, чтобы помочь правильно утилизировать службу. Извините за VB. В нем есть некоторые дополнительные вещи, такие как MessageViewerInspector
, поэтому я могу перехватить XML, который летает и выходит из службы. Пока просто игнорируйте его.
РЕДАКТИРОВАТЬ. Я знаю, что на самом деле это не отвечает на ваш вопрос о том, как поддерживать всплески запросов, но это, безусловно, сделает использование сервиса немного более чистым, с кодовым названием.
Imports System.ServiceModel
Namespace WCF
''' <summary>
''' This helper fixes an issue with WCF services where an exception gets thrown
''' during the Dispose() call of the client, so it doesn't actually get disposed.
''' </summary>
''' <typeparam name="TProxy"></typeparam>
''' <typeparam name="TChannel"></typeparam>
''' <remarks></remarks>
Public Class ServiceProxyHelper(Of TProxy As {ClientBase(Of TChannel), New}, TChannel As Class)
Implements IDisposable
Private _proxy As TProxy
Private _inspector As MessageViewerInspector
Public ReadOnly Property ServiceProxy() As TProxy
Get
If Not _proxy Is Nothing Then
Return _proxy
Else
Throw New ObjectDisposedException("ServiceProxyHelper")
End If
End Get
End Property
Public ReadOnly Property Inspector() As MessageViewerInspector
Get
If _inspector Is Nothing Then
_inspector = New MessageViewerInspector()
End If
Return _inspector
End Get
End Property
Public Sub New()
_proxy = New TProxy()
_proxy.Endpoint.Behaviors.Add(Me.Inspector)
End Sub
Public Sub New(ByVal endpointAddress As String)
Me.New()
If Not Me._proxy Is Nothing AndAlso Not String.IsNullOrEmpty(endpointAddress) Then
Me._proxy.Endpoint.Address = New EndpointAddress(endpointAddress)
End If
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Try
If Not _proxy Is Nothing Then
If _proxy.State <> CommunicationState.Faulted Then
_proxy.Close()
Else
_proxy.Abort()
End If
End If
Catch ex As CommunicationException
_proxy.Abort()
Catch ex As TimeoutException
_proxy.Abort()
Catch ex As Exception
_proxy.Abort()
Throw
Finally
_proxy = Nothing
End Try
End Sub
End Class
End Namespace
Затем вы можете использовать его следующим образом:
Using myService As New ServiceProxyHelper(Of MyService.MyServiceClient, MyService.IMyService)
Try
' Do work
myService.ServiceProxy.DoWork()
Catch ex As FaultException(Of MyService.MyServiceException)
' Log exception
End Try
End Using
Ответ 4
Вы поставили свой собственный ответ в вопросе.
"То, что я действительно хочу сделать, это создать и открыть канал и сделать краткий всплеск повторяющихся коротких последовательных вызовов службы по каналу. Затем красиво закройте канал"
Это правильно.
Применив это к приведенному вами примеру, вы можете упростить его как:
try
{
_service = new BillingServiceClient();
customers = _service.GetCustomers().ToList();
if (customers != null)
foreach (CustomerModel customer in customers)
{
AddressModel address = (AddressModel)_service.GetAddressModel(customer.CustomerID);
Assert.IsNotNull(address, "GetAddressModel returned null");
}
}
catch
{
_service.Abort();
_service = null;
throw;
}
finally
{
if ((_service != null) &&
(_service.State == System.ServiceModel.CommunicationState.Opened))
_service.Close();
_service = null;
}
Позже Изменить: О, держись, я просто перечитываю свой вопрос. Вы сказали, что это произойдет после нескольких повторных вызовов. Я предполагаю, что приведенный выше код действительно вызывает сбой?