portaldacalheta.pt
  • Principal
  • Ágil
  • Américas
  • Processos Financeiros
  • Design Ux
Processo Interno

Desempenho de E / S do servidor: Node vs. PHP vs. Java vs. Go



Compreender o modelo de entrada / saída (I / O) de seu aplicativo pode significar a diferença entre um aplicativo que lida com a carga a que está sujeito e um que se desmorona diante dos casos de uso do mundo real. Talvez, embora seu aplicativo seja pequeno e não atenda a grandes cargas, isso tenha muito menos importância. Mas conforme a carga de tráfego do seu aplicativo aumenta, trabalhar com o modelo de E / S errado pode levar você a um mundo de ferimentos.

E como quase toda situação em que várias abordagens são possíveis, não é apenas uma questão de qual é a melhor, é uma questão de compreender as compensações. Vamos dar uma volta pela paisagem de I / O e ver o que podemos espiar.



Neste artigo, estaremos comparando Node, Java, Go e PHP com Apache, discutindo como as diferentes linguagens modelam sua E / S, as vantagens e desvantagens de cada modelo e concluiremos com alguns benchmarks rudimentares. Se você está preocupado com o desempenho de I / O de seu próximo aplicativo da web, este artigo é para você.



Noções básicas de I / O: uma atualização rápida

Para entender os fatores envolvidos com E / S, devemos primeiro revisar os conceitos no nível do sistema operacional. Embora seja improvável que tenha que lidar com muitos desses conceitos diretamente, você lida com eles indiretamente por meio do ambiente de tempo de execução do seu aplicativo o tempo todo. E os detalhes são importantes.



Chamadas de sistema

Em primeiro lugar, temos chamadas de sistema, que podem ser descritas da seguinte forma:

  • Seu programa (na “terra do usuário”, como eles dizem) deve solicitar ao kernel do sistema operacional para realizar uma operação de E / S em seu nome.
  • Um “syscall” é o meio pelo qual seu programa pede ao kernel para fazer algo. As especificações de como isso é implementado variam entre os sistemas operacionais, mas o conceito básico é o mesmo. Haverá alguma instrução específica que transfere o controle do seu programa para o kernel (como uma chamada de função, mas com algum molho especial especificamente para lidar com essa situação). De modo geral, syscalls estão bloqueando, o que significa que seu programa aguarda o kernel retornar ao seu código.
  • O kernel executa a operação de E / S subjacente no dispositivo físico em questão (disco, placa de rede, etc.) e responde ao syscall. No mundo real, o kernel pode ter que fazer uma série de coisas para atender a sua solicitação, incluindo esperar que o dispositivo esteja pronto, atualizar seu estado interno, etc., mas como um desenvolvedor de aplicativos, você não se importa com isso. Esse é o trabalho do kernel.

Diagrama de Syscalls



Bloqueio vs. chamadas sem bloqueio

Agora, eu acabei de dizer que as chamadas do sistema estão bloqueando, e isso é verdade em um sentido geral. No entanto, algumas chamadas são categorizadas como “sem bloqueio”, o que significa que o kernel recebe sua solicitação, coloca-a na fila ou no buffer em algum lugar e, em seguida, retorna imediatamente sem esperar que a E / S real ocorra. Portanto, ele “bloqueia” por um breve período de tempo, apenas o suficiente para enfileirar sua solicitação.

Alguns exemplos (de syscalls do Linux) podem ajudar a esclarecer: - read() é uma chamada de bloqueio - você passa um identificador dizendo qual arquivo e um buffer de onde entregar os dados que ele lê, e a chamada retorna quando os dados estão lá. Observe que isso tem a vantagem de ser simples e agradável. - epoll_create(), epoll_ctl() e epoll_wait() são chamadas que, respectivamente, permitem criar um grupo de identificadores para ouvir, adicionar / remover manipuladores desse grupo e, em seguida, bloquear até que haja qualquer atividade. Isso permite que você controle de forma eficiente um grande número de operações de I / O com um único thread, mas estou me adiantando. Isso é ótimo se você precisar da funcionalidade, mas como você pode ver, é certamente mais complexo de usar.



É importante entender a ordem de magnitude da diferença de tempo aqui. Se um núcleo da CPU está rodando em 3GHz, sem entrar em otimizações que a CPU pode fazer, ele está executando 3 bilhões de ciclos por segundo (ou 3 ciclos por nanossegundo). Uma chamada de sistema sem bloqueio pode levar cerca de 10 segundos de ciclos para ser concluída - ou “alguns nanossegundos relativamente”. Uma chamada que bloqueia as informações recebidas pela rede pode levar muito mais tempo - digamos, por exemplo, 200 milissegundos (1/5 de segundo). E digamos, por exemplo, que a chamada sem bloqueio levou 20 nanossegundos e a chamada bloqueada levou 200.000.000 nanossegundos. Seu processo apenas esperou 10 milhões de vezes mais pela chamada de bloqueio.

Syscalls bloqueadores vs. não bloqueadores



O kernel fornece os meios para bloquear E / S (“leia desta conexão de rede e me dê os dados”) e não-bloquear E / S (“diga-me quando alguma dessas conexões de rede tiver novos dados”). E qual mecanismo é usado irá bloquear o processo de chamada por períodos de tempo dramaticamente diferentes.

Agendamento

A terceira coisa que é crítica a seguir é o que acontece quando você tem muitos threads ou processos que começam a bloquear.



Para nossos propósitos, não existe uma grande diferença entre um thread e um processo. Na vida real, a diferença relacionada ao desempenho mais perceptível é que, como os threads compartilham a mesma memória e os processos cada um tem seu próprio espaço de memória, fazer com que processos separados tenda a ocupar muito mais memória. Mas quando estamos falando sobre programação, o que realmente se resume a uma lista de coisas (threads e processos semelhantes) que cada uma precisa para obter uma fatia do tempo de execução nos núcleos de CPU disponíveis. Se você tiver 300 threads em execução e 8 núcleos para executá-los, terá que dividir o tempo para que cada um receba sua parte, com cada núcleo em execução por um curto período de tempo e, em seguida, passando para o próximo thread. Isso é feito por meio de uma “troca de contexto”, fazendo com que a CPU passe de um thread / processo a outro.

Essas mudanças de contexto têm um custo associado a elas - elas levam algum tempo. Em alguns casos rápidos, pode demorar menos de 100 nanossegundos, mas não é incomum que demore 1000 nanossegundos ou mais, dependendo dos detalhes de implementação, velocidade / arquitetura do processador, cache da CPU, etc.



E quanto mais threads (ou processos), mais troca de contexto. Quando estamos falando sobre milhares de threads e centenas de nanossegundos para cada um, as coisas podem ficar muito lentas.

No entanto, as chamadas sem bloqueio, em essência, dizem ao kernel 'apenas me chame quando tiver algum novo dado ou evento em uma dessas conexões'. Essas chamadas sem bloqueio são projetadas para lidar com grandes cargas de E / S e reduzir a comutação de contexto.

Comigo até agora? Porque agora vem a parte divertida: vamos ver o que algumas linguagens populares fazem com essas ferramentas e tirar algumas conclusões sobre as compensações entre facilidade de uso e desempenho ... e outros petiscos interessantes.

Como uma observação, embora os exemplos mostrados neste artigo sejam triviais (e parciais, com apenas os bits relevantes mostrados); acesso ao banco de dados, sistemas de cache externos (memcache, et. all) e qualquer coisa que requeira I / O vai acabar realizando algum tipo de chamada I / O que terá o mesmo efeito que os exemplos simples mostrados. Além disso, para os cenários em que o I / O é descrito como 'bloqueio' (PHP, Java), as leituras e gravações de solicitação e resposta HTTP são chamadas de bloqueio: Mais uma vez, mais I / O oculto no sistema com seus problemas de desempenho associados levar em consideração.

Muitos fatores influenciam a escolha de uma linguagem de programação para um projeto. Existem até muitos fatores quando você considera apenas o desempenho. Mas, se você está preocupado com o fato de seu programa ser restringido principalmente por E / S, se o desempenho de E / S for decisivo para seu projeto, essas são coisas que você precisa saber.

A abordagem “Keep It Simple”: PHP

Na década de 90, muitas pessoas usavam Conversar sapatos e escrever scripts CGI em Perl. Então veio o PHP e, por mais que algumas pessoas gostem de falar sobre ele, ele tornou muito mais fácil criar páginas da web dinâmicas.

js não é uma função

O modelo que o PHP usa é bastante simples. Existem algumas variações, mas seu servidor PHP comum se parece com:

Uma solicitação HTTP vem do navegador de um usuário e atinge seu servidor da web Apache. O Apache cria um processo separado para cada solicitação, com algumas otimizações para reutilizá-los a fim de minimizar quantos ele precisa fazer (criar processos é, relativamente falando, lento). O Apache chama o PHP e diz a ele para executar o .php apropriado | arquivo no disco. O código PHP executa e bloqueia chamadas de E / S. Você liga para file_get_contents() em PHP e nos bastidores, faz read() syscalls e espera pelos resultados.

E, claro, o código real é simplesmente incorporado à sua página e as operações estão bloqueando:

query('SELECT id, data FROM examples ORDER BY id DESC limit 100'); ?>

Em termos de como isso se integra ao sistema, é assim:

I / O Model PHP

Muito simples: um processo por solicitação. As chamadas de I / O são bloqueadas. Vantagem? É simples e funciona. Desvantagem? Acerte-o com 20.000 clientes simultaneamente e seu servidor irá explodir em chamas. Essa abordagem não é dimensionada bem porque as ferramentas fornecidas pelo kernel para lidar com E / S de alto volume (epoll, etc.) não estão sendo usadas. E para piorar a situação, a execução de um processo separado para cada solicitação tende a usar muitos recursos do sistema, especialmente memória, que geralmente é a primeira coisa que você fica sem em um cenário como este.

Nota: A abordagem usada para Ruby é muito semelhante à do PHP e, de uma forma ampla, geral e ondulada, eles podem ser considerados iguais para nossos propósitos.

A abordagem multithread: Java

Então o Java apareceu, bem na hora em que você comprou seu primeiro nome de domínio e foi legal dizer “ponto com” aleatoriamente após uma frase. E o Java tem multithreading embutido na linguagem, o que (especialmente para quando foi criado) é bastante impressionante.

A maioria dos servidores da Web Java funciona iniciando um novo encadeamento de execução para cada solicitação que chega e, em seguida, neste encadeamento, eventualmente chamando a função que você, como desenvolvedor do aplicativo, escreveu.

Fazer I / O em um Servlet Java tende a se parecer com:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // blocking file I/O InputStream fileIs = new FileInputStream('/path/to/file'); // blocking network I/O URLConnection urlConnection = (new URL('http://example.com/example-microservice')).openConnection(); InputStream netIs = urlConnection.getInputStream(); // some more blocking network I/O out.println('...'); }

Desde nosso doGet O método acima corresponde a uma solicitação e é executado em seu próprio encadeamento, em vez de um processo separado para cada solicitação que requer sua própria memória, temos um encadeamento separado. Isso tem algumas vantagens, como ser capaz de compartilhar estado, dados em cache, etc. entre os threads porque eles podem acessar a memória um do outro, mas o impacto em como ele interage com o cronograma ainda é quase idêntico ao que está sendo feito no PHP exemplo anteriormente. Cada solicitação obtém um novo encadeamento e as várias operações de E / S são bloqueadas dentro desse encadeamento até que a solicitação seja totalmente tratada. Threads são agrupados para minimizar o custo de criação e destruição, mas ainda assim, milhares de conexões significam milhares de threads, o que é ruim para o planejador.

Um marco importante é que na versão 1.4 Java (e uma atualização significativa novamente na 1.7) ganhou a capacidade de fazer chamadas de E / S sem bloqueio. A maioria dos aplicativos, da web e outros, não o usa, mas pelo menos está disponível. Alguns servidores web Java tentam tirar proveito disso de várias maneiras; entretanto, a grande maioria dos aplicativos Java implantados ainda funcionam conforme descrito acima.

Java de modelo de E / S

Java nos aproxima e certamente tem algumas boas funcionalidades prontas para uso para I / O, mas ainda não resolve realmente o problema do que acontece quando você tem um aplicativo fortemente vinculado a I / O que está sendo pressionado o solo com muitos milhares de threads de bloqueio.

E / S sem bloqueio como um cidadão de primeira classe: Nó

O garoto popular no bloco quando se trata de melhor I / O é o Node.js. Qualquer pessoa que tenha tido uma breve introdução ao Node foi informada de que ele é 'não bloqueador' e que lida com I / O de forma eficiente. E isso é verdade em um sentido geral. Mas o diabo está nos detalhes e os meios pelos quais essa feitiçaria foi realizada importam quando se trata de performance.

Essencialmente, a mudança de paradigma que o Node implementa é que, em vez de dizer essencialmente 'escreva seu código aqui para lidar com a solicitação', eles dizem 'escreva o código aqui para começar a lidar com a solicitação'. Cada vez que você precisa fazer algo que envolve E / S, você faz a solicitação e fornece uma função de retorno de chamada que o Node chamará quando terminar.

O código de nó típico para fazer uma operação de E / S em uma solicitação é assim:

qual empresa deve obter a maior parte de seus retornos sobre os investimentos existentes?
http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); });

Como você pode ver, existem duas funções de retorno de chamada aqui. O primeiro é chamado quando uma solicitação é iniciada e o segundo é chamado quando os dados do arquivo estão disponíveis.

O que isso faz é basicamente dar ao Node uma oportunidade de lidar de forma eficiente com a E / S entre esses callbacks. Um cenário onde seria ainda mais relevante é onde você está fazendo uma chamada de banco de dados no Node, mas não vou me preocupar com o exemplo porque é exatamente o mesmo princípio: você inicia a chamada de banco de dados e dá ao Node uma função de retorno de chamada, executa as operações de E / S separadamente usando chamadas sem bloqueio e, em seguida, invoca sua função de retorno de chamada quando os dados solicitados estão disponíveis. Este mecanismo de enfileirar as chamadas de I / O e deixar o Node lidar com isso e, em seguida, obter um retorno de chamada é chamado de 'Loop de evento'. E funciona muito bem.

I / O Model Node.js

No entanto, há um problema com esse modelo. Sob o capô, o motivo para isso tem muito mais a ver com a forma como o mecanismo V8 JavaScript (mecanismo JS do Chrome que é usado pelo Node) é implementado 1 então alguma coisa. O código JS que você escreve é ​​executado em um único thread. Pense nisso por um momento. Isso significa que, embora a E / S seja realizada usando técnicas eficientes de não bloqueio, seu JS pode, que está fazendo operações vinculadas à CPU, ser executado em um único thread, cada pedaço de código bloqueando o próximo. Um exemplo comum de onde isso pode ocorrer é o loop nos registros do banco de dados para processá-los de alguma forma antes de enviá-los ao cliente. Aqui está um exemplo que mostra como isso funciona:

var handler = function(request, response) { connection.query('SELECT ...', function (err, rows) { if (err) { throw err }; for (var i = 0; i

Embora o Node controle a E / S de maneira eficiente, for loop no exemplo acima está usando ciclos de CPU dentro de seu único thread principal. Isso significa que, se você tiver 10.000 conexões, esse loop pode fazer com que todo o seu aplicativo seja rastreado, dependendo de quanto tempo leva. Cada solicitação deve compartilhar uma fatia de tempo, uma de cada vez, em seu thread principal.

A premissa em que todo este conceito se baseia é que as operações de I / O são a parte mais lenta, portanto, é mais importante tratá-las de forma eficiente, mesmo que isso signifique fazer outro processamento em série. Isso é verdade em alguns casos, mas não em todos.

O outro ponto é que, embora seja apenas uma opinião, pode ser muito cansativo escrever um monte de callbacks aninhados e alguns argumentam que isso torna o código significativamente mais difícil de seguir. Não é incomum ver callbacks aninhados em quatro, cinco ou até mais níveis dentro do código do Node.

Estamos de volta às trocas. O modelo Node funciona bem se o seu principal problema de desempenho for E / S. No entanto, o seu calcanhar de Aquiles é que você pode ir para uma função que está lidando com uma solicitação HTTP e colocar um código de uso intensivo da CPU e fazer com que cada conexão seja rastreada, se não for cuidadoso.

Naturalmente sem bloqueio: vá

Antes de entrar na seção Go, é apropriado que eu divulgue que sou um fanboy de Go. Eu o usei para muitos projetos e sou um defensor abertamente de suas vantagens de produtividade, e as vejo em meu trabalho quando o uso.

Dito isso, vamos ver como ele lida com I / O. Um recurso importante da linguagem Go é que ela contém seu próprio agendador. Em vez de cada thread de execução correspondente a um único thread do sistema operacional, ele funciona com o conceito de “goroutines”. E o tempo de execução Go pode atribuir uma goroutine a uma thread do SO e executá-la ou suspendê-la e não ser associada a uma thread do SO, com base no que a goroutine está fazendo. Cada solicitação que vem do servidor HTTP de Go é tratada em um Goroutine separado.

O diagrama de como o agendador funciona é assim:

I / O Model Go

Sob o capô, isso é implementado por vários pontos no tempo de execução Go que implementam a chamada de I / O, fazendo a solicitação para escrever / ler / conectar / etc., Colocar a goroutine atual para dormir, com as informações para acordar a goroutine de volta quando outras ações podem ser tomadas.

Na verdade, o tempo de execução Go está fazendo algo não muito diferente do que o Node está fazendo, exceto que o mecanismo de retorno de chamada está embutido na implementação da chamada de E / S e interage com o planejador automaticamente. Ele também não sofre a restrição de ter que ter todo o código do manipulador executado no mesmo encadeamento. Go mapeará automaticamente seus Goroutines para quantos encadeamentos do SO considerar apropriados com base na lógica em seu agendador. O resultado é um código como este:

func ServeHTTP(w http.ResponseWriter, r *http.Request) { // the underlying network call here is non-blocking rows, err := db.Query('SELECT ...') for _, row := range rows { // do something with the rows, // each request in its own goroutine } w.Write(...) // write the response, also non-blocking }

Como você pode ver acima, a estrutura básica do código do que estamos fazendo se assemelha àquela das abordagens mais simplistas e, ainda assim, consegue E / S sem bloqueio nos bastidores.

Na maioria dos casos, isso acaba sendo “o melhor dos dois mundos”. E / S sem bloqueio é usado para todas as coisas importantes, mas seu código parece estar bloqueando e, portanto, tende a ser mais simples de entender e manter. A interação entre o agendador Go e o agendador do sistema operacional cuida do resto. Não é mágica completa e, se você construir um sistema grande, vale a pena investir tempo para entender mais detalhes sobre como ele funciona; mas, ao mesmo tempo, o ambiente que você obtém funciona e dimensiona muito bem.

Go pode ter seus defeitos, mas de modo geral, a maneira como lida com I / O não está entre eles.

Mentiras, Mentiras Amaldiçoadas e Pontos de Referência

É difícil fornecer tempos exatos na troca de contexto envolvida com esses vários modelos. Eu também poderia argumentar que é menos útil para você. Então, em vez disso, darei alguns benchmarks básicos que comparam o desempenho geral do servidor HTTP desses ambientes de servidor. Tenha em mente que muitos fatores estão envolvidos no desempenho de todo o caminho de solicitação / resposta HTTP de ponta a ponta, e os números apresentados aqui são apenas alguns exemplos que reuni para fornecer uma comparação básica.

Para cada um desses ambientes, escrevi o código apropriado para ler em um arquivo de 64k com bytes aleatórios, executei um hash SHA-256 nele N número de vezes (N sendo especificado na string de consulta da URL, por exemplo, .../test.php?n=100 ) e imprima o hash resultante em hexadecimal. Eu escolhi isso porque é uma maneira muito simples de executar os mesmos benchmarks com alguma E / S consistente e uma maneira controlada de aumentar o uso da CPU.

Vejo essas notas de benchmark para um pouco mais de detalhes sobre os ambientes usados.

Primeiro, vamos dar uma olhada em alguns exemplos de baixa concorrência. Executar 2.000 iterações com 300 solicitações simultâneas e apenas um hash por solicitação (N = 1) nos dá o seguinte:

Número médio de milissegundos para concluir uma solicitação em todas as solicitações simultâneas, N = 1

práticas recomendadas de manipulação de exceção de primavera
Os tempos são o número médio de milissegundos para concluir uma solicitação em todas as solicitações simultâneas. Menor é melhor.

É difícil tirar uma conclusão apenas desse gráfico, mas isso me parece que, neste volume de conexão e computação, estamos vendo tempos que mais têm a ver com a execução geral das próprias linguagens, muito mais que o I / O. Observe que as linguagens que são consideradas “linguagens de script” (digitação livre, interpretação dinâmica) têm desempenho mais lento.

Mas o que acontecerá se aumentarmos N para 1000, ainda com 300 solicitações simultâneas - a mesma carga, mas 100x mais iterações de hash (significativamente mais carga de CPU):

Número médio de milissegundos para concluir uma solicitação em todas as solicitações simultâneas, N = 1000

Os tempos são o número médio de milissegundos para concluir uma solicitação em todas as solicitações simultâneas. Menor é melhor.

De repente, o desempenho do Node cai significativamente, porque as operações com uso intenso de CPU em cada solicitação estão bloqueando umas às outras. E, curiosamente, o desempenho do PHP fica muito melhor (em relação aos outros) e bate o Java neste teste. (É importante notar que no PHP a implementação SHA-256 é escrita em C e o caminho de execução está gastando muito mais tempo nesse loop, já que estamos fazendo 1000 iterações hash agora).

Agora vamos tentar 5000 conexões simultâneas (com N = 1) - ou o mais próximo que eu poderia chegar. Infelizmente, para a maioria desses ambientes, a taxa de falha não foi insignificante. Para este gráfico, veremos o número total de solicitações por segundo. Quanto mais alto melhor :

Número total de solicitações por segundo, N = 1, 5.000 req / s

Número total de solicitações por segundo. Mais alto é melhor.

E a imagem parece bem diferente. É uma suposição, mas parece que em alto volume de conexão, a sobrecarga por conexão envolvida com a geração de novos processos e a memória adicional associada a ele no PHP + Apache parece se tornar um fator dominante e prejudicar o desempenho do PHP. Claramente, Go é o vencedor aqui, seguido por Java, Node e finalmente PHP.

Embora os fatores envolvidos com o seu rendimento geral sejam muitos e também variem amplamente de aplicativo para aplicativo, quanto mais você entender sobre o que está acontecendo nos bastidores e as compensações envolvidas, melhor para você.

Em suma

Com tudo o que foi dito acima, está bastante claro que, conforme as linguagens evoluíram, as soluções para lidar com aplicativos de grande escala que fazem muita E / S evoluíram com isso.

Para ser justo, PHP e Java, apesar das descrições neste artigo, têm implementações do E / S sem bloqueio disponível para uso dentro Aplicativos da web . Mas eles não são tão comuns quanto as abordagens descritas acima, e a sobrecarga operacional associada de manutenção de servidores usando essas abordagens precisa ser levada em consideração. Sem mencionar que seu código deve ser estruturado de uma forma que funcione com tais ambientes; seu PHP “normal” ou aplicativo da web Java geralmente não será executado sem modificações significativas em tal ambiente.

Como comparação, se considerarmos alguns fatores significativos que afetam o desempenho, bem como a facilidade de uso, obtemos o seguinte:

Língua Threads vs. Processos E / S sem bloqueio Fácil de usar
PHP Processos Não
Java Tópicos acessível Requer Callbacks
Node.js Tópicos sim Requer Callbacks
Ir Threads (Goroutines) sim Nenhum retorno de chamada necessário


Threads geralmente são muito mais eficientes em termos de memória do que processos, uma vez que compartilham o mesmo espaço de memória, enquanto os processos não. Combinando isso com os fatores relacionados a E / S sem bloqueio, podemos ver que pelo menos com os fatores considerados acima, conforme descemos na lista, a configuração geral relacionada a E / S melhora. Portanto, se eu tivesse que escolher um vencedor no concurso acima, certamente seria Go.

Mesmo assim, na prática, a escolha de um ambiente no qual construir seu aplicativo está intimamente ligado à familiaridade que sua equipe tem com esse ambiente e à produtividade geral que você pode obter com ele. Portanto, pode não fazer sentido para todas as equipes apenas mergulhar e começar a desenvolver aplicativos e serviços da web em Node ou Go. Na verdade, encontrar desenvolvedores ou a familiaridade de sua equipe interna é frequentemente citado como o principal motivo para não usar uma linguagem e / ou ambiente diferente. Dito isso, os tempos mudaram muito nos últimos quinze anos.

Esperamos que o texto acima ajude a pintar um quadro mais claro do que está acontecendo nos bastidores e lhe dê algumas idéias de como lidar com a escalabilidade do mundo real para seu aplicativo. Boas entradas e saídas!

A atriz Eleanor Parker de ‘Música no Coração’ dá seu último suspiro aos 91

Outros Do Mundo

A atriz Eleanor Parker de ‘Música no Coração’ dá seu último suspiro aos 91
Gênio da matemática pioneiro morre de câncer de mama aos 40

Gênio da matemática pioneiro morre de câncer de mama aos 40

Mundo

Publicações Populares
As crianças estão passando mais 'tempo sozinhos' com os pais. O que isso significa?
As crianças estão passando mais 'tempo sozinhos' com os pais. O que isso significa?
Um tutorial de fluxo de trabalho de design para desenvolvedores: entregue melhor UI / UX no prazo
Um tutorial de fluxo de trabalho de design para desenvolvedores: entregue melhor UI / UX no prazo
Aprimore o fluxo do usuário - um guia para análise de UX
Aprimore o fluxo do usuário - um guia para análise de UX
Trabalhos a serem realizados: Transforme as necessidades do cliente em soluções de produtos
Trabalhos a serem realizados: Transforme as necessidades do cliente em soluções de produtos
O presidente dos EUA, Donald Trump, critica a ex-NSA Susan Rice por desmascarar funcionários
O presidente dos EUA, Donald Trump, critica a ex-NSA Susan Rice por desmascarar funcionários
 
Todos Juntos Agora - Uma Visão Geral do Design Inclusivo
Todos Juntos Agora - Uma Visão Geral do Design Inclusivo
Métodos de pesquisa UX e o caminho para a empatia do usuário
Métodos de pesquisa UX e o caminho para a empatia do usuário
Como o GWT desbloqueia a realidade aumentada em seu navegador
Como o GWT desbloqueia a realidade aumentada em seu navegador
Um guia para codificação UTF-8 em PHP e MySQL
Um guia para codificação UTF-8 em PHP e MySQL
O que Force Touch significa para IU e UX?
O que Force Touch significa para IU e UX?
Publicações Populares
  • cfo significa o que na empresa
  • como criar fontes no photoshop
  • o que é um empréstimo conversível
  • tabela power pivot excel 2013
  • a repentina compreensão da solução de um problema é chamada de _____.
  • como mudar para python 3
Categorias
  • Ágil
  • Américas
  • Processos Financeiros
  • Design Ux
  • © 2022 | Todos Os Direitos Reservados

    portaldacalheta.pt