Como nós da Sportmaster escolhemos um sistema de cache. Parte 1

Olá! Meu nome é Alexey Pyankov, sou desenvolvedor da Sportmaster. Neste post , falei sobre como o trabalho no site Sportmaster começou em 2012, quais iniciativas conseguimos avançar e vice-versa, que rake coletamos.

Hoje, quero compartilhar os pensamentos que seguem outra história - escolher um sistema de cache para o back-end java no painel de administração do site. Esse enredo é de particular importância para mim - embora a história tenha ocorrido apenas 2 meses, mas nesses 60 dias trabalhamos por 12 a 16 horas e sem um dia de folga. Eu nunca pensei e imaginei que você pudesse trabalhar tanto.

Portanto, divido o texto em duas partes, para não fazer o download completo. Pelo contrário, a primeira parte será muito fácil - preparação, introdução, algumas considerações sobre o que é cache. Se você já é um desenvolvedor experiente ou já trabalhou com caches - do lado técnico, provavelmente não há nada de novo neste artigo. Mas, para um júnior, uma pequena revisão pode dizer para onde olhar, se ele se encontrar em uma encruzilhada.



Quando a nova versão do site Sportmaster foi lançada em produção, os dados vieram de certa forma, para dizer o mínimo, não muito conveniente. A base foram as tabelas preparadas para a versão anterior do site (Bitrix), que precisou ser reforçada no ETL, trazida para um novo visual e enriquecida com pequenas coisas diferentes de uma dúzia de sistemas. Para que uma nova foto ou descrição do produto apareça no site, você precisa esperar até o dia seguinte - atualizar apenas à noite, 1 vez por dia.

A princípio, havia tantas preocupações desde as primeiras semanas de produção que tais inconvenientes para os gerenciadores de conteúdo eram um pouco. Mas, assim que tudo se acalmou, o desenvolvimento do projeto continuou - alguns meses depois, no início de 2015, começamos a desenvolver ativamente o painel de administração. Em 2015 e 2016, tudo está indo bem, vamos divulgá-lo regularmente, a área de administração cobre a maior parte da preparação dos dados e estamos nos preparando para o fato de que em breve a coisa mais importante e difícil será confiada à nossa equipe - a linha de produtos (completa preparação e manutenção de dados para todos os produtos). Mas no verão de 2017, pouco antes do lançamento da linha de produtos, o projeto estará em uma situação muito difícil - justamente por causa de problemas de cache. Eu quero falar sobre esse episódio na segunda parte desta publicação em duas partes.

Mas, neste post, começarei de longe, como alguns pensamentos - idéias sobre cache, que rolar antes de um grande projeto com antecedência seriam um bom passo.

Quando uma tarefa de cache surge


A tarefa de armazenamento em cache não aparece apenas. Somos desenvolvedores, escrevemos um produto de software e queremos que ele seja procurado. Se o produto estiver em demanda e for bem-sucedido, os usuários chegarão. E mais vem e mais. E, portanto, existem muitos usuários e, em seguida, o produto se torna altamente carregado.

Nos primeiros estágios, não pensamos em otimização e desempenho do código. O principal é a funcionalidade, instale rapidamente o piloto e teste as hipóteses. E se a carga aumentar, bombearemos ferro. Nós aumentamos em um fator de dois, três, cinco, que seja 10 vezes. Em algum lugar aqui - as finanças não permitirão mais. E quantas vezes o número de usuários aumentará? Não será apenas 2-5-10, mas se for bem-sucedido, será de 100-1000 e até 100 mil vezes. Ou seja, mais cedo ou mais tarde, mas terá que fazer a otimização.

Digamos que parte do código (vamos chamar essa parte de função) funcione indecentemente por um longo tempo, e queremos reduzir o tempo de execução. Função - pode ser o acesso ao banco de dados, pode ser a execução de uma lógica complexa - o principal é que demora muito tempo. Quanto você pode reduzir o lead time? No limite - pode ser reduzido a zero, não mais. E como você pode reduzir o tempo de execução para zero? Resposta: geralmente exclua a execução. Em vez disso, retorne imediatamente o resultado. E como você sabe o resultado? Resposta: calcule ou procure em algum lugar. Calcular é muito tempo. E espiar é, por exemplo, lembrar o resultado que a função produziu da última vez quando chamada com os mesmos parâmetros.

Ou seja, a implementação da função não importa para nós. Basta saber de quais parâmetros o resultado depende. Então, se os valores dos parâmetros forem apresentados na forma de um objeto que pode ser usado como chave em algum armazenamento, podemos salvar o resultado do cálculo e lê-lo na próxima vez. Se esses resultados de leitura e gravação forem mais rápidos do que executar a função, obtemos lucro em velocidade. O valor do lucro pode chegar a 100, 1000 e 100 mil vezes (10 ^ 5 é mais provável uma exceção, mas no caso de uma base bastante atrasada, é bem possível).

Principais requisitos de armazenamento em cache


A primeira coisa que pode se tornar um requisito para um sistema de armazenamento em cache é uma velocidade de leitura rápida e, em uma extensão um pouco menor, uma velocidade de gravação. É assim, mas apenas até lançarmos o sistema em produção.

Vamos jogar esse caso.

Suponha que forneçamos a carga atual com ferro e agora estamos gradualmente introduzindo o cache. Os usuários estão crescendo um pouco, a carga está aumentando - adicionamos caches um pouco, os prendemos aqui e ali. Isso já ocorre há algum tempo, e agora as funções pesadas praticamente não são chamadas - toda a carga principal recai sobre o cache. O número de usuários durante esse período aumentou N vezes.

E se o suprimento inicial de ferro pudesse ser 2 a 5 vezes, com a ajuda do cache poderíamos aumentar a produtividade a cada 10 ou, em um bom caso, 100 vezes, em alguns lugares, possivelmente 1000. Ou seja, processamos o mesmo ferro 100 vezes mais solicitações. Ótimo, merece um pão de gengibre!

Mas agora, a certa altura, por acidente, o sistema travou e o cache travou. Nada de especial - afinal, o cache foi selecionado sob demanda "leitura e gravação em alta velocidade, o resto não importa".

Em relação à carga inicial, a reserva de ferro foi de 2 a 5 vezes, e a carga durante esse período aumentou de 10 a 100 vezes. Com a ajuda do cache, eliminamos as chamadas para funções pesadas e, portanto, tudo voou. E agora, sem cache - quantas vezes nosso sistema cede? o que vai acontecer conosco? O sistema irá cair.

Mesmo que nosso cache não tenha travado, mas apenas tenha sido limpo por um tempo, ele precisará ser aquecido, e isso levará algum tempo. E neste momento - o fardo principal recairá sobre o funcional.

Conclusão: os projetos de alta carga no prod exigem do sistema de armazenamento em cache não apenas alta velocidade de leitura e gravação, mas também segurança dos dados e resistência a falhas.

Farinha de escolha


No projeto com o painel de administração, a escolha foi a seguinte: primeiro eles colocaram o Hazelcast, porque já estavam familiarizados com este produto a partir da experiência do site principal. Mas, aqui esta escolha não teve êxito - para o nosso perfil de carga, a Hazelcast está trabalhando não apenas devagar, mas muito devagar. E naquele momento, já nos inscrevemos nos termos da retirada do produto.

Spoiler: como exatamente as circunstâncias surgiram, que perdemos um golpe e tivemos uma situação aguda e tensa - vou contar na segunda parte - e como saímos e como saímos. Mas agora - apenas digo que foi muito estresse e "pense - de alguma forma, não acho, agite a garrafa". "Agitar a garrafa" também é um spoiler, sobre isso um pouco mais.

O que nos fizemos:

  1. Fazemos uma lista de todos os sistemas solicitados pelo google e StackOverflow. Pouco mais de 30
  2. , . , - — , .
  3. , , , . , – , .
  4. 17- , . « », .

Mas essa é uma opção quando você precisa escolher um sistema que "rastreie em velocidade" em testes pré-preparados. E se ainda não existem testes e gostaria de escolher mais rapidamente?

Simularemos essa opção (é difícil imaginar que o desenvolvedor do middle + mora no vácuo e, no momento da seleção, ele ainda não havia decidido qual produto experimentar em primeiro lugar - portanto, uma discussão mais aprofundada provavelmente é um teórico / filosofia / sobre um júnior).

Depois de determinar os requisitos, começaremos a escolher uma solução da caixa. Por que reinventar a roda: vamos usar um sistema de cache pronto.

Se você está apenas começando e pesquisará no Google, mais ou menos o pedido, mas, em geral, as diretrizes serão assim. Primeiro de tudo, você tropeça em Redis, é ouvido em toda parte. Então você descobrirá que existe o EhCache como o sistema mais antigo e comprovado. Em seguida, será escrito sobre Tarantool - um desenvolvimento doméstico no qual existe um aspecto único da solução. E também o Ignite, porque agora está em alta popularidade e conta com o apoio da SberTech. No final, há Hazelcast, porque no mundo corporativo, muitas vezes brilha no meio de grandes empresas.

Esta lista não termina aí: existem dezenas de sistemas. E nós estragamos apenas um. Pegue os 5 sistemas selecionados para o "concurso de beleza" e faça uma seleção. Quem será o vencedor?

Redis


Lemos o que eles escrevem no site oficial.
Redis é um projeto de código aberto. Oferece armazenamento de dados na memória, capacidade de salvar no disco, particionar automaticamente em partições, alta disponibilidade e recuperação de quebras de rede.

Parece que está tudo bem, você pode pegar e estragar tudo - tudo o que ele precisa é o que faz. Mas vamos apenas procurar o interesse dos outros candidatos.

Ehcache


EhCache - “o cache mais usado para Java” (tradução do slogan do site oficial). Também de código aberto. E aqui entendemos que o Redis não está sob java, mas geral, e para interagir com ele, você precisa de um wrapper. E o EhCache será mais conveniente. O que mais o sistema promete? Confiabilidade, comprovação, funcionalidade total. Bem, e ela é a mais comum. E armazena em cache terabytes de dados.

Redis é esquecido, estou pronto para escolher o EhCache.

Mas uma sensação de patriotismo me leva a ver o que faz o Tarantool ser bom.

Tarantool


Tarantool - atende à designação "Plataforma de integração de dados em tempo real". Parece muito difícil, por isso lemos a página em detalhes e encontramos uma declaração alta: "Caches 100% dos dados na RAM". Isso deve levantar questões - afinal, pode haver muito mais dados do que memória. A descriptografia é que aqui está implícito que o Tarantool não executa serialização para gravar dados em um disco da memória. Em vez disso, ele usa os recursos de baixo nível do sistema quando a memória simplesmente mapeia para um sistema de arquivos com desempenho de E / S muito bom. Em geral, eles fizeram algo maravilhoso e legal.

Vejamos a implementação: estrada corporativa Mail.ru, Avito, Beeline, Megafon, Alfa-Bank, Gazprom ...

Se ainda houvesse alguma dúvida sobre o Tarantool, a introdução da implementação do Mastercard me mataria. Eu tomo Tarantool.

Mas mesmo assim…

Acender


... há o Ignite , declarado como "uma plataforma de computação na memória ... velocidades na memória em petabytes de dados". Há também muitas vantagens: cache distribuído na memória, armazenamento e cache de valor-chave mais rápidos, escala horizontal, alta disponibilidade, integridade rigorosa. Em geral, verifica-se que o mais rápido é o Ignite.

Implementações: Sberbank, American Airlines, Yahoo! Japão E ainda descubro que o Ignite não é apenas implementado no Sberbank, mas a equipe da SberTech envia seu pessoal para a equipe do Ignite, para finalizar o produto. Isso é completamente cativante e estou pronto para tomar o Ignite.

É completamente incompreensível porque, olho para o quinto ponto.

Hazelcast


Vou ao site da Hazelcast , leio. E acontece que a solução mais rápida para o cache distribuído é o Hazelcast. Ele tem ordens de magnitude mais rápidas do que todas as outras soluções e, em geral, é um líder no campo da grade de dados na memória. Neste contexto, tome outra coisa - não se respeite. Ele também usa armazenamento de dados redundantes para operação contínua do cluster sem perda de dados.

Tudo, estou pronto para tomar Hazelcast.

Comparação


Mas se você olhar, todos os cinco candidatos são tão pintados que cada um deles é o melhor. Como escolher? Podemos ver qual é o mais popular, procurar comparações e a dor de cabeça passará.

Encontramos essa análise , escolha nossos 5 sistemas.



Aqui estão classificadas: no topo da Redis, em segundo lugar está Hazelcast, Tarantool e Ignite estão ganhando popularidade, o EhCache foi e permanece.

Mas vejamos o método de cálculo : links para sites, interesse geral no sistema, ofertas de emprego - ótimo! Ou seja, quando meu sistema cair, direi: “Não, é confiável! Aqui estão muitas ofertas de emprego ... ". Uma comparação tão simples não funcionará.

Todos esses sistemas não são apenas sistemas de cache. Eles ainda têm muita funcionalidade, inclusive quando os dados não são transferidos para o cliente para processamento, mas sim: o código que precisa ser executado nos dados é transferido para o servidor, é executado lá e o resultado é retornado. E como um sistema separado para armazenamento em cache, eles não são considerados com tanta frequência.

Bem, não desista, encontramos uma comparação direta de sistemas. Pegue as duas principais opções - Redis e Hazelcast. Estamos interessados ​​em velocidade, podemos compará-los por esse parâmetro.

Hz vs Redis


Encontramos essa comparação :


azul é Redis, vermelho é Hazelcast. O Hazelcast vence em todos os lugares, e a lógica é dada: é multiencadeado, altamente otimizado, cada thread trabalha com sua própria partição, para que não haja bloqueios. E o Redis é de thread único, não obtém ganho com as modernas CPUs multi-core. O Hazelcast possui E / S assíncrona, o Redis-Jedis possui soquetes de bloqueio. No final, o Hazelcast usa um protocolo binário, enquanto o Redis é orientado a texto, o que significa que é ineficiente.

Apenas no caso, nos voltamos para outra fonte de comparação. O que ele vai nos mostrar?

Redis vs Hz


Outra comparação :


aqui, pelo contrário, vermelho é Redis. Ou seja, Redis supera Hazelcast no desempenho. Na primeira comparação, Hazelcast venceu, na segunda - Redis. Aqui, eles explicaram com muita precisão por que Hazelcast venceu na comparação anterior.

Acontece que o resultado do primeiro foi realmente fraudado: Redis foi levado na caixa de base e Hazelcast foi preso por um caso de teste. Acontece que: em primeiro lugar, ninguém pode ser confiável e, em segundo lugar, quando escolhemos um sistema, ainda precisamos configurá-lo corretamente. Essas configurações incluem dezenas, quase centenas de parâmetros.

Agitando uma garrafa


E todo o processo que acabamos de fazer, posso explicar com essa metáfora: "Agite a garrafa". Ou seja, agora você não pode programar, agora o principal é poder ler o fluxo de pilha. E na minha equipe há uma pessoa, um profissional, que trabalha assim em momentos críticos.

O que ele está fazendo? Ele vê uma coisa quebrada, vê um rastreio de pilha, pega algumas de suas palavras (quais são sua experiência no programa), pesquisa no Google, encontra o fluxo de pilha entre as respostas. Sem ler, sem pensar, entre as respostas da pergunta - ele escolhe algo mais parecido com a frase "faça isso e aquilo" (escolher uma resposta como essa é o talento dele, pois nem sempre é a resposta que coletou mais curtidas), ele usa , parece: se algo mudou, tudo bem. Se não mudou, estamos revertendo. E repetimos a pesquisa de verificação inicial. E de maneira tão intuitiva, ele consegue que, após algum tempo, o código funcione. Ele não sabe o porquê, ele não sabe o que fez, ele não pode explicar. Mas! Esta infecção funciona. E o "fogo extinto". Agora entendemos o que fizemos. Quando o programa funciona, é muito mais fácil.E economiza significativamente tempo.

Este método é muito bem explicado por esse exemplo.

Era muito popular coletar um veleiro em uma garrafa. Ao mesmo tempo, o veleiro é grande e frágil, e o gargalo da garrafa é muito estreito; você não pode empurrá-lo para dentro. Como montá-lo?



Existe um método assim, muito rápido e muito eficaz.

O navio consiste em um monte de pequenas coisas: paus, cordas, velas, cola. Colocamos tudo isso em uma garrafa.
Pegamos a garrafa com as duas mãos e começamos a tremer. Estamos tremendo, tremendo. E geralmente - você obtém lixo completo, é claro. Mas às vezes. Às vezes você pega um navio! Mais precisamente, algo semelhante a um navio.

Estamos mostrando isso a alguém: "Serge, veja!?". E de fato, de longe - como se um navio. Mas então você não pode deixar isso passar.

Existe outro caminho. Os caras estão usando os mais avançados, como hackers.

Deu uma tarefa a um sujeito, ele fez tudo e foi embora. E você olha - parece ter sido feito. E depois de um tempo, quando é necessário refinar o código - aí começa por causa dele ... É bom que ele já tenha conseguido fugir para longe. Estes são os caras que, usando o exemplo de uma garrafa, farão isso: veja, onde está o fundo - o vidro se dobra. E não está totalmente claro se é transparente ou não. Em seguida, os "hackers" cortam esse fundo, inserem o navio lá, depois colam o fundo novamente, e como deveria.

Do ponto de vista da definição do problema, tudo parece estar correto. Mas aqui está um exemplo de navios: por que esse navio em geral, quem precisa? Não possui nenhuma funcionalidade. Geralmente esses navios são presentes para pessoas de alto escalão que o colocam em uma prateleira acima de si mesmas, como uma espécie de símbolo, como um sinal. E agora, se essa pessoa, a chefe de uma grande empresa ou um funcionário de alto escalão, como está a bandeira com esse lixo, no qual o pescoço é cortado? Seria melhor se ele nunca soubesse disso. Então, como são feitos esses navios que podem ser apresentados a uma pessoa importante?

O único lugar, chave, com o qual realmente não há nada a ser feito, é o edifício. E o casco do navio passa pelo pescoço. Enquanto o navio está saindo da garrafa. Mas não é só montar um navio, é uma verdadeira joalheria. Alavancas especiais são adicionadas aos componentes, o que permite que sejam levantadas mais tarde. Por exemplo, as velas são dobradas, levemente afundadas e, com a ajuda de uma pinça, são muito joias, com certeza, são puxadas e levantadas. O resultado é uma obra de arte que pode ser apresentada com uma consciência limpa e orgulho.

E se queremos que o projeto seja bem-sucedido, deve haver pelo menos um joalheiro na equipe. Quem se preocupa com a qualidade do produto e leva em consideração todos os aspectos, sem sacrificar um único, mesmo em tempos de estresse, quando as circunstâncias exigem urgência em detrimento do importante. Todos os projetos bem-sucedidos que são sustentáveis, que resistiram ao teste do tempo, são construídos sobre esse princípio. Eles têm algo muito preciso e único, algo que usa todos os recursos disponíveis. No exemplo de um navio em uma garrafa, é demonstrado que o casco do navio passa pelo pescoço.

Voltando à tarefa de escolher nosso servidor de armazenamento em cache, como esse método poderia ser aplicado? Eu proponho essa opção a partir de todos os sistemas que existem - não sacuda a garrafa, não escolha, mas veja o que, em princípio, eles têm algo a procurar ao escolher um sistema.

Onde procurar gargalo


Vamos tentar não agitar a garrafa, não para resolver tudo o que está acontecendo, mas vamos ver quais tarefas surgem, se de repente, para a sua tarefa - projetar você mesmo esse sistema. Obviamente, não montaremos a bicicleta, mas usaremos esse esquema para nos orientar em quais pontos prestar atenção nas descrições dos produtos. Delineamos esse esquema.



Se o sistema for distribuído, teremos vários servidores (6). Digamos quatro (é conveniente colocar na foto, mas, é claro, pode haver qualquer número deles). Se os servidores estiverem em nós diferentes, significa que algum código está girando em todos eles, o que é responsável por garantir que esses nós formem um cluster e, em caso de interrupção, se conectem, se reconheçam.

Ainda precisa de uma lógica de código (2), que é realmente sobre cache. Os clientes interagem com esse código por meio de alguma API. O código do cliente (1) pode estar na mesma JVM e acessá-lo pela rede. A lógica implementada no interior é a decisão de quais objetos deixar no cache e qual lançar. Usamos a memória (3) para armazenar o cache, mas, se necessário, também podemos salvar parte dos dados no disco (4).

Vamos ver em quais partes a carga ocorrerá. Na verdade, cada seta e cada nó serão carregados. Em primeiro lugar, entre o código do cliente e a API, se for uma interação de rede, o subsidence pode ser bastante perceptível. Em segundo lugar, dentro da estrutura da própria API - tendo substituído a lógica complexa, podemos executar a CPU. E seria bom se a lógica não dirigisse memória mais uma vez. E permanece a interação com o sistema de arquivos - na versão usual, ele é serializado / restaurado e gravado / lido.

Interação adicional com o cluster. Provavelmente, ele estará no mesmo sistema, mas pode ser separadamente. Aqui, você também precisa considerar a transferência de dados, a velocidade da serialização de dados e a interação entre o cluster.

Agora, por um lado, podemos imaginar “que engrenagens girarão” no sistema de cache ao processar solicitações de nosso código e, por outro lado, podemos estimar quais e quantas solicitações nosso código gerará para esse sistema. Isso é suficiente para fazer uma escolha mais ou menos sóbria - para escolher um sistema para o nosso caso de uso.

Hazelcast

Vamos ver como essa decomposição se aplica à nossa lista. Por exemplo, Hazelcast.

Para colocar / obter dados do Hazelcast, o código do cliente acessa (1) a API. Hz permite que você inicie o servidor como incorporado e, nesse caso, acessar a API é uma chamada de método dentro da JVM, você pode lê-lo gratuitamente.

Para calcular a lógica em (2), Hz depende de um hash da matriz de bytes da chave serializada - ou seja, a serialização da chave ocorrerá de qualquer maneira. Essa é uma sobrecarga inevitável para Hz.
As estratégias de remoção estão bem implementadas, mas em casos especiais - você pode conectar o seu próprio. Você não precisa se preocupar com esta parte.

O armazenamento (4) pode ser conectado. Bem. A interação (5) para incorporado pode ser considerada instantânea. A troca de dados entre nós no cluster (6) - sim, é. Isso contribui para a resiliência ao custo da velocidade. O recurso Hz do cache próximo permite reduzir o preço - os dados recebidos de outros nós do cluster serão armazenados em cache.

O que pode ser feito nessas condições para aumentar a velocidade?

Por exemplo, para evitar serializar a chave em (2), em cima do Hazelcast, aperte outro cache para obter os dados mais quentes. Na Sportmaster, a cafeína foi escolhida para esse fim.

Para torcer no nível (6), o Hz oferece dois tipos de armazenamento: IMap e ReplicatedMap.


Vale a pena dizer como a Hazelcast entrou na pilha de tecnologias Sportmaster.

Em 2012, quando trabalhamos no primeiro piloto do futuro site, foi o Hazelcast que acabou sendo o primeiro link que o mecanismo de pesquisa emitiu. O conhecido começou "pela primeira vez" - ficamos impressionados com o fato de que apenas duas horas depois, quando inserimos o Hz no sistema, ele funcionou. E funcionou bem. Até o final do dia em que adicionamos alguns testes, ficamos felizes. E esse suprimento de vigor foi suficiente para superar as surpresas que Hz lançou ao longo do tempo. Agora, a equipe Sportmaster não tem motivos para recusar o Hazelcast.

Mas argumentos como “o primeiro link no mecanismo de pesquisa” e “HelloWorld rapidamente montado” - isso, é claro, são uma exceção e uma característica do momento em que a escolha ocorreu. Esses testes para o sistema selecionado começam com o lançamento no produto e é nesse estágio que você deve prestar atenção ao escolher qualquer sistema, incluindo cache. Na verdade, no nosso caso, podemos dizer que escolhemos o Hazelcast por acaso, mas depois descobrimos que escolhemos o caminho certo.

Para a produção, é muito mais importante: monitoramento, falhas de processamento em nós individuais, replicação de dados, custo de dimensionamento. Ou seja, vale a pena prestar atenção nas tarefas que surgirão exatamente quando o sistema for suportado - quando a carga for dezenas de vezes maior do que o planejado, quando acidentalmente preenchermos algo na direção errada, quando você precisar lançar uma nova versão do código, substituir os dados e fazê-lo despercebido para clientes.

Para todos esses requisitos, o Hazelcast é definitivamente adequado.

Continua


Mas Hazelcast não é uma panacéia. Em 2017, escolhemos o Hazelcast para o cache no painel do administrador, simplesmente contando com a boa impressão da experiência passada. Isso desempenhou um papel fundamental em uma piada muito má, e é por isso que estávamos em uma situação difícil e “heroicamente” saímos dela por 60 dias. Mas mais sobre isso na próxima parte.

Enquanto isso ... Feliz Novo Código!

All Articles