Este artigo foi traduzido por máquina.

Lições aprendidas

Otimizar um software de grande escala + serviços de aplicativo

Udi Dahan

Este artigo discute:
  • O WCF e modelos de serviço de três camadas
  • Lidando com conectividade ocasional
  • Segmentação e facilidade de uso
  • Domínios de sincronização
Este artigo usa as seguintes tecnologias:
O WCF, software + serviços

Conteúdo

Sempre o backup para data
Problemas com a camada 2
WCF e camada 3
Problemas de escalabilidade
Problemas no Paradise
Conectividade ocasional afeta os servidores
Threading e usabilidade
O WCF do lado do cliente
Dependências entre objetos-gráfico
Domínios de sincronização
Interação de controlador de exibição
Ligação de dados do thread-safe
Two-way DataBinding
Repositórios do lado do cliente
Desempenho
Lições aprendidas

Criação de software cliente da área de trabalho sofisticados e interativos nunca foi tão fácil. Com o Visual Studio, Windows Presentation Foundation (WPF) e a orientação de aplicativos compostos para WPF (prisma), os desenvolvedores melhor equipados que nunca.

Infelizmente, não é suficiente.

Ao trabalhar em uma em larga escala, o software de cliente rico + aplicativo de serviços, minha equipe encontrado fora da forma de disco rígida que desenvolver clientes conectados ocasionalmente, com vários segmentos, inteligentes que não deadlock ou gerar lixo pelos dados-corridas está longe de trivial. Não é uma falha no conjunto de ferramentas, no entanto. Descobrimos que essas ferramentas precisam ser empregada em padrões muito específicos para atingir a sistemas que estão respondendo e robustos.

Neste artigo eu destacar alguns problemas que executamos no e explique como nós superou. Esperamos que você pode aproveitar essas informações para evitar nossos erros e fazer o melhor uso dessas ferramentas ao criar seu próprio software + serviços de aplicativos.

Sempre o backup para data

Um dos desafios primeiro que nós enfrentados foi a necessidade para que os usuários sempre ser mantido atualizado com o todos os outros usuários estavam fazendo, assim como eventos que ocorrem em outros sistemas. Esse comportamento em tempo real do sistema permitidas usuários para atuar nos eventos como ele aconteceu em grandes quantidades de dados. A versão anterior do sistema permitido-los para pesquisa, classificação e filtro, mas apenas os usuários não foi possível agir em que informações em tempo.

Durante o processo de design de experiência do usuário, muitas das grades na versão anterior do sistema foram substituídos por Messenger-como pop-up mensagens "brindar" informado sobre o usuário sobre eventos importantes. Outros usuários de alterações ativados para definir limites de várias propriedades de entidades que, quando excedido, pop up brindar mensagens. O brindar geralmente contida links que, quando clicado, abrir um formulário mostrando o usuário a entidade afetada — alguns com um antes e depois do modo de exibição.

Enquanto pensando por meio de maneiras de dimensionar os padrões de solicitação/resposta regulares tinham experiência com, nosso interessado chave fornecida no e lembrado nos: "essa capacidade de eventos de superfície do mundo para o usuário em tempo real é essencial para nossos profissionais especializados oferecer suporte a empresa colaborativa, em tempo real que está criando".

Em resumo, era claro que precisávamos de algum tipo de recurso de publicação/assinatura. Tentando suporta visibilidade de usuário de dois segundos em eventos com centenas de usuários apenas um banco de dados central de pesquisa não foi pretende trabalhar (ele interrompe para baixo em cerca de 60 usuários).

Na parte superior de tudo, nossa população de usuários foi móvel — alguns deles desconectar e reconectar-se cada alguns segundos como elas são movidas e saia do Wi-Fi zonas, outras pessoas trabalhando offline para uma hora ou dois enquanto telecomutação e algum trabalho off-line intensivo executar para até uma semana.

Tivemos que mudar conectividade ocasional, sincronização de dados e publicação/assinatura todos ao mesmo tempo. Aprendemos que nós não foi possível resolver todos os problemas do cliente ou do lado do servidor, mas em vez que uma abordagem integrada era necessária desde que as alterações em um lado precisava de alterações correspondentes no outro lado. Neste artigo, descreverei esse processo a partir do servidor trabalhando de frente para o cliente.

Problemas com a camada 2

Nosso modelo de implantação original era o modelo de 2 camadas tradicional encontrado em muitos aplicativos de cliente sofisticada. A dificuldade que tivemos com esse modelo foi empurrar notificações do banco de dados para os clientes.

Uma opção que tentamos era ter toda a interação com o banco de dados feito por meio de procedimentos armazenados que, após confirmar suas transações, chamaria check-out para um serviço da Web hospedado no servidor de banco de dados. Era a responsabilidade de serviço da Web para notificar os clientes do que aconteceu com passando-los o nome do procedimento armazenado que foi chamado, bem como os argumentos que foram passados para o procedimento armazenado.

Isso não funcionar tão bem check-out. Houve dois problemas críticos com essa abordagem — um lógico, a outra física.

O problema lógico era que um único procedimento armazenado pode ser chamado por todos os tipos de caminhos em que as coisas diferentes do significado de código em momentos diferentes. Quando uma notificação de cliente recebida da ativação de um procedimento armazenado, não era sempre claro o que significava e o que brindar para pop-up para o usuário.

O problema físico era que precisávamos de cada cliente para expor seu próprio serviço da Web, que chamaria o serviço da Web no servidor de banco de dados. Isso rapidamente foi vetado pelos profissionais de segurança como uma brecha de segurança gaping, bem como pelas Auditores de regulamentação que eram (corretamente) preocupados com os usuários a tomar decisões em tempo real em dados cujas origens necessariamente não puderam ser rastreadas.

WCF e camada 3

Para resolver os problemas lógicos com a solução de 2 camadas percebemos que precisávamos de ser mais explícito sobre nosso contrato de serviço, informando o nome do procedimento armazenado que executou e seus parâmetros foi explícita sobre o que aconteceu com o, mas não por que ele aconteceu.

Conforme começamos criando nossos contratos de serviço WCF (Windows Communication Foundation), ficou claro que ele não foi suficiente para o foco apenas no nomeação apropriada e escopo de nossos métodos de serviço. Precisamos ser explícitos sobre o contrato de notificações — a maneira que o servidor chamado de volta para os clientes. Contratos de retorno de chamada WCF habilitado nos tenham a mesma quantidade de cuidado em mensagens de servidor para cliente como o contrato de serviço regular. (Para ler mais sobre contratos de retorno de chamada WCF, consulte o artigo do Juval Lowy de outubro de 2006 " O que É necessário saber sobre chamadas de unidirecionais, retornos de chamada E eventos.")

Cada par de comando/evento foi modelado da seguinte maneira:

[ServiceContract(CallbackContract = 
  typeof(ISomethingContractCallback))] 
interface ISomethingContract {
  [OperationContract(IsOneWay = true)] 
  void DoSomething();
}

interface ISomethingContractCallback {
  [OperationContract(IsOneWay = true)] 
  void SomethingWasDone();
}

Para habilitar o nosso serviço WCF para retorno de chamada para clientes diferente daquele que chamou o método atual, tivemos que hospedam o serviço em um separado do processo daquele que o cliente e implantar esse processo em seu próprio servidor, em essência, mover para uma implantação de três camadas.

Enquanto era recomendável que colocamos o serviço na mesma caixa de como o banco de dados, nossos administradores de banco de dados foram preocupados que o serviço pode tirar imediatamente recursos preciosa, hurting assim o desempenho do banco de dados. O benefício adicional de ter uma camada separada de nosso serviço WCF sem monitoração de estado era que, pode facilmente dimensionado-la para máquinas mais sem exigir qualquer reformular com o servidor de banco de dados.

Infelizmente, não era tão fácil quanto pensamos.

Problemas de escalabilidade

Fizemos em dois tipos de problemas de escalabilidade, lógica e física, como antes.

O problema de escalabilidade lógico declarado próprio como o sistema obtido mais funcionalidade. Quando é iniciado pela primeira vez desenvolvimento, tivemos somente um contrato de único servidor que contém todos os métodos nele e um contrato de retorno de chamada única com todos os métodos correspondentes nele. Executamos rapidamente para a parede de Tijolo de muitos chefs desenvolvedor misturador a mesma panela. Depois de mover cada par de comando/evento ao seu próprio par de contrato de serviço/retorno de chamada, esse problema foi resolvido. Ainda a solução apresentou problema outro, mais traiçoeiro.

Ao executar a decisão de ter vários pares de contrato de serviço/retorno de chamada, achamos que o principal problema poderia estar tendo um número impossível de gerenciar de contratos. Que, na verdade, não vá check-out para ser grande parte um problema — a modularidade entre domínios de negócios do aplicativo reduzido que muito bem. O problema era "par".

Um comando pode resultar em vários tipos de notificações, e um tipo de notificação pode ser causado por vários tipos de comandos. O mapeamento um-para-um limpa que tínhamos antes era excessivamente simples e necessários a evoluir para um mapeamento de muitos-para-muitos. Não só estivéssemos nos dimensionamento o número de comandos e notificações no sistema, precisávamos de aumentar o número de relações entre eles. Ele parecia como a representação de um-para-um suporte contratos de retorno de chamada WCF não será capaz de aproveitar nos frente.

Fizemos um problema de escalabilidade física quando dimensionar a nossa camada de serviço do WCF. Precisávamos de alguma maneira de compartilhar a lista de assinantes de contrato de retorno de chamada do cliente. Como trabalhamos através do guia fornecido em Do Juval Lowey outubro de 2006 MSDN Magazine artigo, criamos as partes de infra-estrutura relevante incluindo o serviço pub/sub-rotina. Descobrimos que um serviço único pub/sub-rotina não foi capaz de manipular a quantidade de notificações necessárias para envio de todos os editores para todos os assinantes, em essência, constitui um afunilamento no publisher para comunicação do assinante. Além disso, notificações diferentes tinham prioridades diferentes — algo que era difícil inclinar a infra-estrutura para dar suporte.

Depois de concluir e para trás no tópico várias vezes, a estrutura de serviços de pub/sub-rotina que são fornecidas com melhor era ter uma parte da infra-estrutura pub/sub-rotina por domínio de lógica comercial. Desde que cada domínio também tivesse conjuntos de comandos e notificações clumped ao redor dele, disponibilizados como um bom limite limpo. Esta partição resolvidos muitos dos problemas prioridade tivemos anteriormente ao criar o primeiro nível de escala de infra-estrutura pub/sub-rotina check-out automaticamente.

Problemas no Paradise

Uma das vantagens do WCF é o suporte para várias ligações em várias tecnologias como HTTP e TCP. Nós originalmente optou por trabalhar com WS HTTP desde que ele foi o recurso da maioria dos sofisticados, porém não considerar todas as implicações dessa decisão.

Como nós distribuiu o sistema para nossos usuários, tudo o que parecia ser indo bem, mas essa sensação foi vivia curto. Após cerca de uma hora, iniciou a obtenção chamadas de nossa ajuda pessoal de suporte técnico que "servidores foram recusando conexões."

Nós rapidamente verificado e viu que todos os nossos servidores estavam up e em execução, mas eles não parecem estar fazendo muito. Uso da CPU foi baixa, mesmo com memória, IO e tudo. É verificado o banco de dados para bloqueios e outros nasties, mas tudo estava desmarque lá, muito.

Portanto, começamos a outro processo de servidor no modo de depuração e começou a observar nele para ver o que fazia. Não demorou muito para mostrar se o problema.

A cada 10 a 20 segundos, um thread que foi publicando um evento em um cliente pode travar por 30 segundos. Você pode imaginar o que isso fez a nossa escalabilidade. Porque nós foram bastante próximo ao tamanho de pool segmento recomendado para o número de clientes que foram suporte, estavam executando fora de segmentos. Nesse ponto, o WCF iniciado recusando aceitar novas conexões de clientes.

Achamos que talvez tivéssemos um bug em nosso aplicativo cliente que causou para bloquear o servidor.

Nós deu hunting para as máquinas clientes específicos que causaram os problemas (usando os endereços IP que são separados fora a partir dos logs do servidor). Falamos para os usuários dos computadores para ver se eles tinham experiente quaisquer comportamentos estranhos no aplicativo na perto da hora do problema de servidor. Eles não informar algo fora do comum. Mas uma delas feita uma observação interessante: "o aplicativo tende a obter paralisado a forma Outlook faz, à direita perto da hora minha conexão de WiFi Recorta check-out."

Conectividade ocasional afeta os servidores

Começamos a coleta de logs em vários computadores cliente e servidor e viu o mesmo comportamento repetidas. Sempre que um cliente deu off-line, chamadas para ele bloqueia por 30 segundos.

Isso ficou contador a nossa compreensão do atributo IsOneWay on the OperationContract. Todos os métodos de notificação nossos evento retornou nulo — ocorreu há motivo para os segmentos de servidor para obter bloqueado.

Como abrir o nós detalhamento mais para o comportamento de WsHttpBinding começamos entender como ele funcionava em contratos de operação unidirecional. Quando o servidor ficar notificar um cliente, invocando um método unidirecional no seu proxy, se esse proxy estava conectado anteriormente e tiver em seus caches todo o canal subjacente objetos, ele faz usá-los e tenta chamar o cliente por HTTP. Mesmo que a chamada seja unidirecional, o canal subjacente aguarda uma conexão HTTP possa ser estabelecida e a solicitação HTTP pode ser enviada. Ele não aguarda a resposta HTTP. Infelizmente, se o cliente estiver off-line por qualquer motivo, a conexão HTTP aguardará o tempo de limite HTTP padrão (30 segundos) antes de desistir.

Percebendo que nós não foi possível corrigir esse comportamento, precisávamos encontrar uma vinculação alternativa que seria robusta de desconexão de clientes. Descobrimos que a resposta nas ligações de Microsoft Message Queuing (MSMQ) — especificamente NetMsmqBinding. Felizmente, com o WCF, troca de uma ligação para outro não um aumento de grandes.

Depois de verificar que nosso segmentos de servidor não foram presos mais quando conectividade do cliente deu check-in e check-out, é ativado nossos sights volta ao cliente.

Threading e usabilidade

Não durante a criação de clientes sofisticados que interagem com servidores de uma maneira de solicitação/resposta, segurança do thread foi nunca quanto de uma preocupação para nós. Como os servidores que desenvolvemos tem mais recursos e tornou-se mais poderosos, a quantidade de tempo levou possam responder cresceu bem. Isso é conhecido por causar problemas de utilização quando o aplicativo pára até que o servidor responde. A solução comum é alterar o cliente para que ele chama o servidor de forma assíncrona. Nesse ponto, os desenvolvedores descubra que, quando retornos de chamada do servidor são tratados em um thread de segundo plano, controles da interface do usuário tendem a lançar exceções-los. Finalmente, o código é alterado para que os retornos de chamada servidor alterne para o segmento de primeiro plano e tudo está funcionando novamente.

A coisa é, quando o cliente recebe um fluxo infinitas de notificações de vários servidores, esses padrões não funcionar nos bem.

Como cada notificação que chegam no cliente foi sendo processada no thread do primeiro plano, e toda a interação da interface do usuário também foi feita no segmento de primeiro plano, essas duas tarefas terminou Combatendo continuamente para controle. Você pode realmente ver o mouse incorretamente saltar na tela que você tentou movê-lo. Ao digitar em caixas de texto, os caracteres devem preencher apenas como incorretamente.

Percebi isso quando gastar algum tempo no nosso laboratório de teste de carga. Lembro-me olhando o cliente mostrando todas as notificações várias popping up em tempo real, muito feliz com como nossa arquitetura estava trabalhando fora. Levou o mouse para clicar em um link em uma dos toasts e ele mantidos presos como eu movi a ele. Ele lembrado me da maneira como mouse sem fio se comportam como the sua baterias. Mas o mouse foi um mouse USB com fio comum.

Eu winced. Estamos apenas duas semanas fora de uma versão. Todos os nossos testes funcionais foram passar, e os testes de carga foram mostrando que o sistema pode manipular tudo que emitiu-lo. O único problema foi que o sistema não era exatamente funcional sob carga — usabilidade pesadelo teria sido uma melhor descrição.

O WCF do lado do cliente

Antes de entrar novamente no quadro de desenho em nosso cliente, nós procurou algo — tudo — que pode impedir a reconfiguração iminente. Nós pored sobre orientação no MSDN sobre como usar o WCF em ambientes de cliente inteligente (consulte" Gravar clientes inteligentes com o Windows Communication Foundation") e escreveu mais código de verificação de conceito que nunca antes, mas, de alguma forma, nada abordados todas as bases. Parece que funcionou que foram paralisados entre o rock de bloqueio e o local de disco rígido inutilizável.

Uma estratégia que parecia promissor foi baseada em controles seguros que encapsulam a interação com o contexto de sincronização (referência) do Windows Forms de tal forma que, mesmo se eles são acessados por um thread de segundo plano, eles empacotar a chamada de volta para o segmento de interface do usuário.

Como nem todos notificação que chega no cliente relacionado Atualizando a interface do usuário, pode obter muito na facilidade de uso seguindo a maioria dos notificação do lado do cliente processamento em um thread de segundo plano. Ocasionalmente, quando a notificação envolvidos Atualizando a interface do usuário, os controles de seguros garantiria que o processamento da interface do usuário real foi feito no thread correto. Bem, ele parecia como uma solução sólida.

Depois de no futuro e Implementando os controles seguros necessários e formulários, passamos uma boa quantidade de tempo no laboratório de teste executar testes funcionais, enquanto os testes de carga foram executado em segundo plano. Nós tinha tirados o log no cliente e tornou assíncrona também para que nós pode manter níveis de log altos sem diminuindo a interface do usuário.

Neste aplicativo financeiro, vários comerciantes poderiam colaborar em um portfólio de investimento único para atingir vários objetivos em termos de risco, comissão e retornar — às vezes, colaboração em um único comercial. Quando tivemos vários testadores simular esse comportamento, em um das execuções tinham um retorno muito negativo. Enquanto isso não é necessariamente surpreendente no e de si mesmo (vamos enfrentam-lo, se um testador realmente pode fazer o trabalho de um comerciante, seria um testador), o fato de que era tão diferente dos resultados de outras execuções feitas nos levar o aviso. Nossas experiências anteriores desenvolver esse sistema ensinadas nos de que quando nós observados algo incomum, ele significa que alguns suposição que fizemos em nossa programação ativados fora a ser false.

Examinando às pilhas de logs, estava procurando no momento em que um executados quando o retorno financeiro levou um dip. Surpreendentemente, que foi muito claro, havia um ponto de limpar em que o retorno entrou no vermelho. Como trabalhamos nossa maneira e para trás através das entradas de log em torno desse ponto no tempo, nada significava check-out. Tudo o que parecia quase como fazia quando o retorno era positivo.

Um dos nossos os DBAs, um hacker antigo do UNIX, eu aposto que, com alguns regex expressões regulares, ele pode encontrar a diferença principal em uma hora. Desde foram já 3 horas para ele, rapidamente conceded e, 45 minutos mais tarde, ele resurfaced com um sorridente.

Era uma alternância de contexto e a hora de possível pior e ele terminou causando uma corrida de dados. Um segmento foi definição o comércio como iene (aproximadamente) 90 milhões e o outro foi defini-la a 1 milhão US dólares, uma propriedade por vez. Infelizmente, o comércio terminou no estado de 1 milhão iene ou $ 11,000 R$. Que explica o dip no retorno financeiro.

Impedindo que vários segmentos de trabalho com o mesmo objeto ao mesmo tempo não Foguete surgery. Cada thread apenas precisa bloquear o objeto antes de trabalhar com ele (consulte a Figura 1 ). Isso necessário uma passagem completa por meio de muito o código de cliente para certificar-se que tinha bloqueado tudo o que precisamos. Ele, em seguida, necessário um volume razoável de testes para sacuda out os deadlocks que resultaram de nós bloqueio mais do que precisamos.

A Figura 1 bloqueio objetos

//Thread 1
void UserWantsToDoSomethingToTrade(
    Guid tradeId, double value, Currency c) {
  Trade t = InMemoryStore.Get(tradeId);
  lock(t) {
    t.Value = value;
    t.Currency = c;
  }
}

//Thread 2
void ReceivedNotificationAboutTrade(
    Guid tradeId, TradeNotification tn) {
  Trade t = InMemoryStore.Get(tradeId);
  lock(t) {
    t.Value = tn.Value;
    t.Currency = tn.Currency;
  }
}

Um das coisas que muito reluctantly demos up em foi vinculação de dados nossos objetos na memória para exibições editáveis de usuário. Não temos o usuário bloquear o objeto para a duração do formulário seja aberto, desde que foi possível bloquear um thread de plano de fundo aguardando esse objeto de processar todas as outras notificações.

Com uma quantidade razoável de trepidation, colocamos o sistema por meio a bateria anterior de testes, aguardando o inevitável "algo" que não sabemos sobre isso seriam bola o projeto de seus pés. Novamente.

Como nosso Testador-comerciantes trabalhou o sistema, parecia que os problemas anteriores tinham foi resolvidos. Como cenários mais e mais envolvidos foram executados por meio de, mais usuários e mais tipos de usuários interagiam com o mesmo Portfólio de investimento — alterando perfis de risco, projeções hipotéticos e comparações de históricas. Os testadores realmente banged no sistema de cada direção e mantida por ele. Meia disbelieving que isso possa realmente sê-lo, nós veio volta para os negócios com boa notícia.

Não que elas acredita-se nos. Não QUERO dizer, nosso registro de rastreamento era particularmente impressionante nesse momento. Entrega atrasada 120 % tende a erode confiança dessa maneira. Mas se encontravam naquele sérias necessidade do novo sistema para que terça-feira é lançado em beta.

E na quinta-feira, é revertida check-out.

Dependências entre objetos-gráfico

Quando a lucratividade de uma execução teste único é grosseiramente diferente de outro teste executa, mesmo financeiros novices como os desenvolvedores e testadores levar aviso. Quando mais delicate investimento e comerciais regras violadas, mas os tiver impacto imediato ou em larga escala, novices não vê-lo. Especialistas em fazer.

Não recebo em nitty detalhes financeiras regras aqui, mas tecnicamente o problema era que nossa estratégia de proteção de objeto único era muito simples para regras que envolvem vários objetos.

Era difícil.

Você ver, quando nosso código de chamada bloqueado e atualizado um objeto, esse objeto pode disparar um evento que seria manipulado por vários outros objetos. Cada um desses objetos pode optar por atualizar propriamente dito e disparar eventos de seu próprio, com essa fora de processo percolating afetar muitos objetos. Obviamente, nosso código de chamada não foi possível saber quanto os ripples deve ir para os outros objetos seriam não foram bloqueados. Uma vez, tivemos vários segmentos atualizar os mesmos objetos sem bloqueios.

Houve falar de desistir em eventos e outros mecanismos de comunicação rígida para que nós pode obter alguns segurança do thread, mas a idéia de precisar re-implementar todos os nossas regras complexas (que levou nos tão tempo para obter direito) sem os níveis do .NET foi muito lembrar.

E não era apenas objetos de domínio ou. Objetos de controlador podem obter chamados volta Além disso, alterando seu estado, muito, desativar e ativar os botões da barra de ferramentas e menus, popping up brindar, você nomeie-o.

É necessário um bloqueio global único, mas memórias de uma interface do usuário inutilizável feitas nos cautela de tal solução. Havia também a preocupação de que mesmo se um bloqueio existia, teríamos que examinar cada linha de código no sistema para garantir que o bloqueio foi executado e liberado apropriadamente — não apenas para esta versão do software, mas, para cada manutenção única lançamento e patch daí em diante.

Era um caso de damned se fizer isso, damned se você não fizer isso.

E isso é quando descobrimos que domínios de sincronização.

Domínios de sincronização

O domínio de sincronização fornece sincronização automática de acesso de thread aos objetos declarativamente. Essa classe foi introduzida como parte da infra-estrutura de suporte a .NET remoting. Os desenvolvedores que indicar que uma classe é para ter acesso a seus objetos sincronizados deve ter a classe herdar de ContextBoundObject e marcá-lo com o SynchronizationAttribute da seguinte forma:

[Synchronization]
public class MyController : ContextBoundObject {
  /// All access to objects of this type will be intercepted
  /// and a check will be performed that no other threads
  /// are currently in this object's synchronization domain.
}

Conforme o esperado, toda essa mágica vem com alguma sobrecarga de desempenho, em criação de objetos e acesso. Outra limitação irritante é que classes envolvidas em um domínio de sincronização não é possível ter métodos genéricos ou propriedades, embora pode chamar e caso contrário, fazer uso de genéricos de outras classes.

Nesse ponto no projeto, quando, seria praticamente todas as outras opções, que demos-uma captura.

É planejado com um domínio de sincronização única no qual toda a lógica seria executado. Isso significava que objetos de controlador, objetos de domínio e objetos WCF do lado do cliente precisariam ser no domínio de sincronização. Na verdade, os únicos objetos que precisam ser fora do domínio de sincronização foram Windows Forms próprios, seus controles e quaisquer outros elementos GUI visual.

A coisa interessante que descobrimos foi que nem todos os objetos no domínio sincronização tinham herdam ContextBoundObject ou ter o SynchronizationAttribute aplicado a eles. Em vez disso, somente precisávamos fazer isso para objetos em dos limites do domínio de sincronização. Isso significa que todos os nossos objetos de domínio podem permanecer como antes — um aumento de desempenho grande.

Classes de controlador necessários apenas um pouco mais cuidado.

Interação de controlador de exibição

Em nosso uso do padrão MVC (Model-View-Controller), controladores de interagiam com modos de exibição somente no segmento de interface do usuário. Essa foi uma abordagem diferente que os controles de seguros descrita anteriormente, onde o controlador poderia chamar exibições em assim o segmento de plano de fundo.

Também sabíamos que nem toda notificação recebida do servidor necessário atualizar a interface do usuário, e foi a responsabilidade dos objetos de controlador para tomar essa decisão. Assim, controladores precisará lidar com notificações em um thread de segundo plano e se atualizar a interface do usuário era necessária, seria responsáveis por alternância de threads.

Uma coisa pequena ter em mente, porém, é sempre alternar segmentos de forma assíncrona — caso contrário, você pode deadlock do sistema ou causar problemas de desempenho graves (Falando por experiência).

Um pouco mais tarde, criamos uma classe de base do controlador que encapsulado a segmentação e chamar para manter o código do aplicativo simples (consulte a A Figura 2 ).

Classe base do controlador a Figura 2

[Synchronization]
public class MyController : ContextBoundObject {
  // this method runs on the background thread
  public void HandleServerNotificationCorrectly() 
   {
  // RIGHT: switching threads asynchronously
  Invoker.BeginInvoke( () => CustomerView.Refresh() );

  // other code can continue to run here in the background

  // when this method completes, and the thread exits the
  // synchronization domain, the UI thread in the Refresh
  // method will be able enter this or any other synchronized object.
   }

   // this method runs on the background thread
   public void HandleServerNotificationIncorrectly()
   {
  // WRONG: switching threads synchronously
  Invoker.Invoke( () => CustomerView.Refresh() );

  // code here will NOT be run until Refresh is complete

  // DANGER! If Refresh tries to call into a controller or any other
  //         synchronized object, we will have a deadlock.
   }

   // have the main form injected into this property
   public ISynchronizeInvoke Invoker { get; set; }

   // the view we want to refresh on server notification
   public ICustomerView CustomerView { get; set; }
}

Uma coisa que realmente queríamos, no entanto, foi a ligação de dados. Você mal poderia expressar que estiver fazendo MVC se seu modelo for uma coleção de seqüências de caracteres, duplicatas e ints.

Ligação de dados do thread-safe

O problema que tinha antes com a vinculação de dados era que nossos objetos de modo de exibição mantido referências a objetos de modelo permitindo que o segmento da interface do usuário atualizar os mesmos objetos conforme foram sendo atualizados pelo segmento de plano de fundo. Conforme começamos colocar no lugar o padrão de ViewModel, muitas coisas se tornou muito mais simples — apenas ter controle independente da estrutura desses objetos feitas a diferença.

Nossa próxima etapa em colocar novamente em seguida, ligação de dados era para que objetos de controlador passar clones para suas exibições assim:

public class CustomerController : BaseController {
  // this method runs on the background thread
  public void CustomerOverdrawn(Customer c) {
    ICustomerOverdrawnView v = this.CreateCustomerOverdrawnView();

    v.Customer = c.Clone(); // always remember to clone

    this.CallOnUiThread( () => v.Show() );
  }
}

Embora isso tecnicamente trabalhou, ocorreram alguns problemas com ele. O primeiro problema foi sustentabilidade — como pôde é garantir que todos os desenvolvedores lembrada clonar seus objetos de domínio antes de passá-los para modos de exibição? O segundo problema foi mais técnico — objetos de domínio referenciado uns aos outros clonagem portanto, basta um não significa os outros foram clonados.

Em resumo, precisávamos objetos de domínio a ser profundidade clonado como eles foram passados para modos de exibição. Também era importante que nós não mova qualquer uma dessa complexidade para as exibições. O que precisávamos era criar um proxy genérico do modo de exibição em CreateCustomerOverdrawnView e em que o proxy inspecionar todas as chamadas de método e setters para parâmetros que foram objetos de domínio, executar um clone profundo e, em seguida, passar esse clone para o modo de exibição.

Há tantas tecnologias que permitem que você executar esse proxy e cada um deles faz coisas diferentes. Alguns usam técnicas de programação orientada a aspecto, outros conecta mais direto, mas as táticas próprios não são importantes. Basta sabe que você precisa criar um proxy. No proxy, inclua um método para clonagem profundidade e o dicionário que acompanha para manter o conjunto de trabalho de clones. a Figura 3 mostra nossa solução.

A Figura 3 clonagem objetos para o modo de exibição

// dictionary of references from source objects to their clones
// so that we always return the same clone for the same source object.
private IDictionary<object, object> sourceToClone = 
  new Dictionary<object, object>();

// performs a deep clone of the given entity
public object Clone(object entity) {
  if (entity.GetType().IsValueType)
  return entity;

  if (entity is string)
  return (entity as string).Clone();

  if (entity is IEnumerable) {
    object list = 
      Activator.CreateInstance(entity.GetType()) as IEnumerable;
    MethodInfo addMethod = entity.GetType().GetMethod("Add");

    foreach (object o in (entity as IEnumerable))
      addMethod.Invoke(list, new object[] {Clone(o)});

    return list;
  }

  if (sourceToClone.ContainsKey(entity))
    return sourceToClone[entity];

  object result = Activator.CreateInstance(entity.GetType());
  sourceToClone[entity] = result;

  foreach(FieldInfo field in 
    entity.GetType().GetFields(BindingFlags.Instance | 
    BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | 
    BindingFlags.Public)) field.SetValue(result, 
    Clone(field.GetValue(entity)));

  return result;
}

Esse método passa por todas as propriedades e campos do objeto, clonagem-los conforme necessário. Se ele encontrar uma referência a um objeto que anteriormente foi clonada no dicionário, ele não clonar novamente, mas retorna o clone do primeiro. Dessa forma, o método cria uma imagem espelhada de um determinado gráfico de objetos, impedindo que o modo de exibição obtenham acesso a um objeto que pode ser usado em um thread de segundo plano.

Após tudo isso foi feito, nossos modos de exibição agora podem livremente ligar aos objetos determinado domínio. O que nós pode não contar com foi vinculação de dados bidirecional de objetos de domínio atualiza automaticamente os modos de exibição ligados.

Two-way DataBinding

Em cenários de ligação de dados regular, quando o objeto vinculado for alterado, ele gera eventos permitindo que o modo de saber que ele deve ser atualizado. Isso realmente ajuda dissociar um sistema, como as partes que manipulam a comunicação com o servidor não precisam atualizar os modos de exibição quando eles atualizar um objeto de domínio. Na nossa nova arquitetura de thread-safe, como os objetos WCF do lado do cliente não atualizar as mesmas instâncias de objetos de domínio como aquelas que são vinculados aos modos de exibição, nós perder algumas das vantagens de vinculação de dados bidirecional.

É importante compreender que, em um multithread ambiente é ter desistir na ligação de dados bidirecional. Se um thread de segundo plano fosse atualizar um objeto de domínio vinculado a interface do usuário, o comportamento de INotifyPropertyChanged (referência) causaria o thread de segundo plano para atualizar a interface do usuário diretamente e o aplicativo falhar.

Controladores agora precisam assumir a responsabilidade de saber qual modo de exibição precisa ser atualizado quando. Por exemplo, quando um usuário possui um formulário abre mostrando os detalhes de um comerciais pendente e o cliente recebe notificação sobre alterações para o comércio, o controlador relevante deve atualizar o formulário. Eis como fizemos ele originalmente:

public class TradeController : BaseController {
  public void Init() {
    Commands.Open<Trade>.Activated += (args => {
      TradeForm f = OpenTradeForm(args.Trade);

      args.Trade.Updated += this.CallOnUiThread(
        () =>  f.TradeUpdated(args.Trade)
      );
    });
  }
}

Esse código manipula o comando open genérico para uma feira de negócios, abra um formulário e mostrando o comércio solicitado. Ela também especifica que quando o comércio é atualizado, o formulário é passado o comércio atualizado.

Estamos well-pleased com esse código limpo, simples e direto.

Isto é, até que percebemos tinha um bug nele.

Quando o usuário aberto seus comerciais segundo (ou qualquer comerciais depois disso), quando qualquer um dos trades anteriores deve ser atualizado, o formulário mostra o comércio atualizado — mesmo que o usuário não preocupam-lo mais. É necessário para ter mais cuidado no descarte do nossos retornos de chamada:

public class TradeController : BaseController {
  public void Init() {
    Commands.Open<Trade>.Activated += (args => {
      TradeForm f = OpenTradeForm(args.Trade);

      Delegate tradeUpdated = this.CallOnUiThread(
        () =>  f.TradeUpdated(args.Trade)
      );

      args.Trade.Updated += tradeUpdated;

      f.Closed += () => args.Trade -= tradeUpdated;
    });
  }
}

A diferença aqui é que nós está mantendo à referência do representante para que nós pode cancelar inscrição da atualização comerciais quando o formulário é fechado. Bug corrigido.

Exceto por uma coisa pouco.

Esse código seria correto em um sistema de single-threaded, mas não no ambiente de clone severo-guerra do nosso cliente multithreaded, referências são sempre o que você pensa.

O objeto comercial eventargs veio em algum lugar — a ativação de um comando, para ser específico. Comandos são ativados por usuários, no thread da interface do usuário, como resultado do usuário fazer algo — nesse caso, duas vezes em uma feira de negócios em uma grade. Mas, se o comércio é mostrado em uma grade, que significa um controlador de tinha dar a esse modo de exibição e no processo do comércio seria ter sido clonado.

Em resumo, desde a notificações do servidor relativos ao que comercial não funcionaria diretamente com o objeto clonado na grade, o evento atualizadas o controlador se inscreve para acima seriam nunca geradas. Assim, o formulário exibindo os detalhes do comércio não poderia obter atualizado.

Algo está faltando da solução.

Repositórios do lado do cliente

Nossos controladores precisa de alguns forma de localizar check-out quando uma instância específica de uma entidade é atualizada, mas sem depender de referências de objeto.

Se cada entidade tinha um identificador e havia um repositório de memória dessas entidades no cliente que pode ser consultado, o controladores podem usá-lo para obter a referência com autoridade para uma entidade com base em identificadores provenientes de interface do usuário.

Aqui está nosso controlador comerciais aparência quando usando o repositório.

public class TradeController : BaseController {
  public void Init() {
    Commands.Open<Trade>.Activated += (args => {
      TradeForm f = OpenTradeForm(args.Trade);
      Delegate tradeUpdated = this.CallOnUiThread(
        (trade) =>  f.TradeUpdated(trade) );
      this.Repository<Trade>.When((t => t.Id == args.Trade.Id))
       .Subscribe(tradeUpdated);
      f.Closed += 
       () => this.Repository<Trade>.Unsubscribe(tradeUpdated);
    });
  }
}

Nosso controlador agora está usando o repositório para se inscrever alterações na instância comercial específico com o identificador fornecido pela interface do usuário. A última parte do quebra-cabeça é simplesmente ter nossos objetos WCF do lado do cliente usar esse mesmo repositório para atualizar os objetos de domínio do lado do cliente.

Desempenho

Depois de colocar todas as partes no lugar, reescrever partes significativas do nosso aplicativo de cliente, apresentando as estruturas de suporte e passar por nosso teste de desempenho, descobrimos que ainda houve problemas.

Sob carga pesada, quando muitos objetos tinham sido atualizados ao redor do mesmo tempo, as notificações grandes sendo processadas no lado do cliente causado a interface do usuário para se tornar lento. Nossa instrumentação mostrou que quanto maior a notificação, quanto maior o domínio de sincronização mantido pelo segmento de plano de fundo — durante o qual o usuário não pôde executar qualquer ação exigir a lógica de controlador.

Tentamos otimizar o código de cliente, refatoração de objetos de domínio para que os gráficos devem ser menor levando menos tempo para clonar e tudo que poderia pensar. Nada que fizemos no cliente ajudou.

E isso é quando um do júnior servidor desenvolvedores diziam até um pouco hesitantly, sugerindo que pode alterar o código servidor para publicar mais de mensagem de uma notificação. Em vez de colocar todas as entidades alteradas em uma mensagem, poderíamos fazer uma entidade por mensagem ou coisa, possivelmente ainda torna configurável.

E ele feito todas as a diferença no mundo.

Desde entre processamento uma notificação e a próxima que retreated o segmento de plano de fundo no cliente do domínio de sincronização, isso permitido o segmento da interface do usuário obter em e realizar algum trabalho em nome do usuário. O fato de que ela demorou um pouco mais para as notificações grandes ser processado uma mensagem por vez no cliente foi perfeitamente aceitável.

Lições aprendidas

Quando é iniciado check-out sobre o desenvolvimento do projeto, é considerado seria assim como qualquer outro sistema de cliente rico/banco de dados. Os desafios que seria enfrentados ambos arquitetura e tecnicamente foram maiores que a coisa que seria visto. Lidar com nível baixo de threading emite os dois servidor e cliente, Noções básicas sobre conectividade ocasional como afeta o desempenho e manutenção de um nível aceitável de interatividade do usuário ao evitar deadlocks e as condições de corrida — havia tantas peças que tinham clicar juntos apenas à direita para a coisa toda a trabalhar. Todas as tecnologias suporte existiam, ele era em como nós colocá-los juntos.

No final, no entanto, como é visto que nossos usuários colaborar em tempo real entre o mundo, nós entendido que não pôde ser qualquer outra forma. Empresas cujos sistemas dependiam usuários recebendo, classificação e agrupamento informações apenas não conseguiria concorrer. Software + serviços, clientes conectados ocasionalmente e a revolução multicore significa nunca maior produtividade e a lucratividade de uma organização capaz de fazer a fração.

Nosso interessado estivesse certo. A capacidade a superfície de eventos do mundo para o usuário em tempo real, na verdade, é essencial para profissionais especializados oferecer suporte a empresas de colaboração, em tempo real.

Após a conclusão do projeto, eu estava um pouco preocupado que todos os padrões e técnicas que seria retirados não servem me em todos os que há uma transição para desenvolvimento de aplicativos mais estilo de linha de negócios. Tem informar que eu estava agradavelmente surpreso.

Em uma nota mais pessoal, pode informar que nada melhor para a carreira de um desenvolvedor que os gerentes de linha de negócios raving sobre seu aplicativo e o desenvolvimento propriamente dito é muito mais interessante. Experimente.

udi Dahan é O software Simplist, um MVP e conectado o Supervisor de tecnologias trabalhando no WCF, WindowsWF e Oslo. Ele fornece treinamento, mentoring e high-end arquitetura consultando serviços, especializado no design de arquitetura .NET orientada por serviços, escalonável e seguro. Udi pode ser contatado por meio de seu blog: www.UdiDahan.com.