Um artigo sobre como resolver o problema de otimizar o processo de exclusão de arquivos de um sistema fragmentado. É sobre um projeto para compartilhar e trabalhar com arquivos. O sistema era uma startup há cerca de 8 anos, depois foi filmado com sucesso e foi vendido várias vezes. O projeto tem 4 desenvolvedores que estão com o projeto desde o início, o que é muito valioso. A documentação, tradicionalmente, não tinha tempo para escrever ou não é muito relevante.
Por que você leu isso e por que escrevi tudo isso? Eu gostaria de falar sobre um ancinho que repousa cuidadosamente dentro do sistema e bate para que as estrelas saiam dos olhos.
Quero agradecer a
Hanna_Hlushakova por trabalharem juntos, por
encerrar o projeto e ajudar na preparação do artigo. Basicamente, você encontrará descrições do problema e o algoritmo para resolvê-lo, que usamos, não há exemplos de código, estruturas de dados ou outras coisas necessárias. Não sei se minha experiência o ajudará a evitar um rake em casa, mas espero que você consiga algo útil. Talvez este artigo seja uma perda absolutamente irrevogável de tempo valioso.

Sobre o projeto
O projeto é um dos líderes na praça Gartner, possui empresas clientes com mais de 300 mil funcionários nos EUA e na Europa e vários bilhões de arquivos para manutenção.
As seguintes tecnologias são usadas no projeto: Microsoft, servidores C # .net, banco de dados MS SQL, 14 servidores ativos + 14 no modo de espelhamento de dados.
O volume de bancos de dados é de até 4 TB, a carga constante no horário comercial é de cerca de 400 mil solicitações por minuto.
Há muita lógica de negócios no banco de dados:
450 mesas
1000 procedimentos armazenados
80.000 linhas de código SQL
Tradicionalmente, eles não tinham tempo para escrever a documentação ou isso não é relevante.
Sobre a tarefa
A tarefa é refazer a exclusão de arquivos do armazenamento que já foram excluídos pelos clientes e o período de armazenamento dos arquivos excluídos expirou, caso eles desejem ser restaurados. Na versão atual, de acordo com os cálculos da própria empresa, os arquivos que foram excluídos há um ano foram armazenados nos servidores, embora de acordo com os termos de negócios eles devessem ter sido armazenados apenas 1 mês. Como alguns dos arquivos estão armazenados no S3, a empresa pagou pelos dados extras e os clientes que usaram o armazenamento local ficaram imaginando por que os arquivos ocupavam mais espaço do que deveriam.
Bancos de dados shardovany para o cliente da empresa.
Como a remoção funcionou anteriormente?

Em um servidor global com informações sobre todos os arquivos no sistema, foram formados intervalos de 15 mil identificadores de arquivos.
Então, de acordo com o cronograma, foi iniciada uma pesquisa de servidor para vários identificadores de arquivo.
Os limites do intervalo foram transmitidos para cada fragmento.
O shard em resposta enviou arquivos encontrados do intervalo.
O servidor principal adicionou os arquivos ausentes à tabela da fila para exclusão em seu banco de dados.
Em seguida, na tabela de filas, o serviço de exclusão de arquivo físico do armazenamento recebeu vários identificadores para exclusão, após o que enviou uma confirmação de que iria excluir os arquivos e uma verificação foi iniciada para todos os shards, se esses arquivos foram usados lá.
Com um aumento no número de arquivos, essa abordagem começou a funcionar muito lentamente, pois havia vários bilhões de arquivos e o número de intervalos aumentou significativamente. Os arquivos excluídos ainda permaneciam menos de 5% em comparação com o número total, respectivamente, é muito ineficiente classificar bilhões de arquivos para encontrar vários milhões de arquivos excluídos.
Por exemplo, geralmente depois que um usuário exclui um arquivo, ele deve ser armazenado por 1 mês, caso precise ser restaurado. Após esse período, o arquivo deve ser excluído pelo programa do repositório. Com o número atual de arquivos, o número de intervalos e a velocidade dos desvios, levaria 1 ano para o servidor ignorar completamente todos os intervalos.
É claro que o local não foi liberado e isso causou insatisfação dos usuários, pois seus servidores armazenavam muitas vezes mais arquivos do que deveriam ter sido relatados. A própria empresa de serviços pagou por um lugar adicional no S3, o que foi uma perda direta para ele.
Somente no S3, no momento em que o trabalho começou, 2 Petabytes de arquivos excluídos foram armazenados, e isso é apenas na nuvem. Além disso, havia clientes cujos arquivos eram armazenados em seus servidores dedicados, com o mesmo problema: o espaço do servidor era ocupado por arquivos excluídos pelos usuários, mas não excluídos do servidor.
O que você decidiu fazer?
Decidimos rastrear os eventos de remoção:
- o cliente excluiu o arquivo e expirou.
- o usuário excluiu o arquivo imediatamente sem a possibilidade de recuperação.
Ao excluir um arquivo de um banco de dados fragmentado, decidimos usar uma abordagem otimista e remover uma das verificações para uso. Sabíamos que 99% dos arquivos são usados apenas em um shard. Decidimos adicionar imediatamente o arquivo à fila de exclusão e não verificamos o restante dos shards para o uso desse arquivo, pois a verificação será feita novamente quando a exclusão da loja confirmar o serviço.
Além disso, eles deixaram o JOB atual que verificou os arquivos excluídos por intervalos para adicionar arquivos que foram excluídos antes do lançamento da nova versão.
Tudo o que foi excluído no shard é coletado em uma tabela e depois transferido para um único servidor, com informações sobre todos os arquivos.
Nesse servidor, ele é enviado para a tabela da fila de exclusão.
Na tabela para exclusão antes da exclusão, é verificado se o arquivo não é usado em todos os shards. Esta parte da verificação estava aqui antes da alteração do código e eles decidiram não tocá-la.
O que você precisou mudar no código?
Em cada um dos shards, foi adicionada uma tabela na qual o identificador do arquivo excluído deve ser gravado.
Encontramos todos os procedimentos para excluir arquivos do banco de dados, havia apenas 2. Depois que o usuário excluiu o arquivo, o arquivo permanece no banco de dados por algum tempo.
No procedimento para excluir um arquivo do banco de dados, adicionamos uma entrada a esta tabela local com arquivos excluídos.
No servidor global com arquivos, eles criaram um JOB que baixa uma lista de arquivos dos bancos de dados shard. Apenas chamando um procedimento a partir de um banco de dados shardable, ele faz um DELETE dentro e exibe uma lista de arquivos em OUTPUT. No MS SQL Server, a extração de um servidor remoto é muito mais rápida do que a inserção em um servidor remoto. Tudo isso é feito em blocos.
Esses arquivos são adicionados à tabela da fila de exclusão no servidor global.
Uma tabela com um identificador de fragmento foi adicionada à tabela da fila para saber de onde veio o evento de exclusão.
Como todos o testaram?
Existem 3 ambientes:
O Dev é um ambiente de desenvolvimento. O código é retirado do ramo de desenvolvimento do gith. É possível implantar uma versão diferente do código no IIS e criar várias versões da orquestração. Ele se conectará ao ambiente de desenvolvimento a partir do cliente dentro da VPN. Até recentemente, o inconveniente era apenas com bancos de dados, pois todas as alterações no banco de dados podem interromper o trabalho de outras partes do sistema. Então as bases foram localizadas. O código em execução já pode ser derramado nos servidores dev com bancos de dados para não arruinar o trabalho de todos. Em um ambiente de desenvolvimento, existem 3 shards, em vez de 12, que estão no prod, mas isso geralmente é suficiente para testar a interação.
Preparo - o ambiente é o mesmo com o prod de acordo com a versão do código (quase o mesmo, pois é raro, mas há alterações diretamente no prod pelos administradores). Uma cópia do código da ramificação principal. Às vezes, algumas diferenças com o código no produto foram detectadas no banco de dados, mas, em geral, são idênticas. Existem também três fragmentos no estadiamento e na donzela. Não há carga no teste, bem como no dev. Aqui você pode executar testes de integração completamente, já que o código corresponde ao prod. Todos os testes devem passar, este é um pré-requisito antes de ir para a Implantação.
Laboratório de Perf, onde os testes são feitos sob carga. A carga é criada usando jmeter, 10 vezes menos do que no prod, e há apenas um fragmento, o que às vezes cria inconvenientes. Os dados de vendas são coletados, anonimizados e usados no laboratório de perf. Todos os servidores da mesma configuração que no prod.
A carga é 10 vezes menor, porque supõe-se que seja uma carga aproximada que chega ao prod para 1 shard. A desvantagem é que a base global é muito subutilizada, diferentemente das vendas. E, se as alterações dizem respeito principalmente ao banco de dados global com arquivos, você pode confiar nos resultados do teste apenas aproximadamente - no produto, isso pode não funcionar dessa maneira. Embora o perf lab não corresponda idealmente à carga com o prod, a capacidade de testar sob a carga já ajuda a detectar muitos erros antes de implantar no prod.
Há também um servidor de backup onde você pode ver os dados da venda para capturar alguns casos. Em geral, a empresa opera sob uma licença que proíbe os desenvolvedores de conceder acesso aos dados de vendas e o acesso à equipe de administração e suporte (Operações) ao desenvolvimento, portanto, é necessário solicitar a ajuda dos administradores de banco de dados. Os dados de vendas tornam o teste muito fácil, porque alguns casos surgem apenas em dados do produto e é muito útil estudar os dados em um sistema real para entender como o sistema funciona para o usuário.
Durante os testes no laboratório de desempenho, verificou-se que a carga de exclusão de arquivos do armazenamento não foi realizada a partir da palavra. Ao implementar o teste de carga, escolhemos solicitações mais populares do software cliente, algumas das quais não foram incluídas na limpeza do armazenamento. Como se trata de um banco de dados, acabou por realizar um teste simplificado para todos os objetos alterados com a chamada dos procedimentos alterados em diferentes dados manualmente. (nas opções que eu conhecia).
Além disso, nos testes de integração e desempenho, a ênfase principal foi colocada no tipo mais popular de armazenamento de arquivos.
Um recurso adicional do laboratório de perf, que não foi descoberto imediatamente, é a incompatibilidade da quantidade de dados em algumas tabelas no prod e no perf. No sentido de que todos os JOBs de vendas funcionam no perf, que geram dados, mas nem sempre há algo que processa os dados gerados na tabela. E, por exemplo, a fila de tabela acima mencionada para exclusão no perf é muito maior do que no prod - 20 milhões de registros no perf e 200 mil registros no profissional.
Processo de implantação
O processo de implantação é bastante padrão. Não houve alterações no código do aplicativo para esta tarefa, todas as alterações estão apenas no banco de dados. Um DBA é sempre rolado no banco de dados; esse processo não é automatizado. 2 versões de scripts são preparadas - para aplicar alterações e reverter alterações, e uma instrução para DBA é escrita. Sempre são feitas 2 versões de scripts e são necessariamente testadas para reversão e reversão de alterações. E esses mesmos scripts são usados para aplicar alterações no laboratório de preparação e desempenho antes de iniciar a integração e o teste de carga.
O que aconteceu após a implantação?
Nas primeiras 5 horas após a implantação, ocorreu um milhão de eventos em que o software cliente recebeu um erro ao tentar fazer o download do arquivo. O evento "arquivo corrompido". Isso significa que o cliente está tentando fazer o download do arquivo, mas o arquivo não foi encontrado no repositório. Geralmente, esses eventos não são de todo ou são medidos em 1 a 2 mil por dia.
Devo dizer imediatamente que demorou pelo menos 1 semana para encontrar a causa do fracasso em uma equipe de 3 e, às vezes, 5 pessoas (incluindo eu).
Coletamos toda a lista de arquivos para os quais o evento "Arquivo corrompido" veio.
Apesar do fato de haver mais de 1 milhão de eventos e todos eles serem de usuários diferentes, empresas diferentes, havia apenas 250 arquivos exclusivos nessa lista.
O DBA no servidor de backup foi gerado para investigar os backups do banco de dados no momento em que os eventos chegaram. Existem algumas tabelas nas bases do projeto com todos os tipos de logs que ajudaram na análise. Mesmo ao excluir informações do banco de dados, um log é adicionado ao que foi excluído e por qual evento. No produto, esses registros são armazenados por 1 semana e depois mesclados no servidor de arquivamento.
E o mesmo acontece com as tabelas com logs que ajudaram muito na análise do que aconteceu:
Um log completo com eventos do cliente é mantido em cada shard
No servidor global:
- Log de solicitações para baixar todos os arquivos pelos usuários
- Log de upload de arquivos para o sistema pelos usuários
- Log de eventos FileCorrupt
- Arquivo de log para cancelar a exclusão do armazenamento
- Log de arquivos excluídos do banco de dados
Além disso, um ELK com logs de aplicativos estava disponível.
Conseguimos repetir o erro no ambiente de desenvolvimento, o que confirmou a exatidão da suposição. No início, ninguém levou a sério essa hipótese, pois era muito difícil acreditar que tantos fatores coincidiam ao mesmo tempo e que muitos usuários chegavam nesse momento específico.
O que deu errado?
Descobriu-se que o sistema tinha cerca de 250 (para comparação, bilhões de arquivos no sistema) super mega arquivos populares. 250 sim!
Esses arquivos ainda eram muito antigos. No momento em que esses arquivos foram carregados no sistema, outro sistema para gerar a chave do arquivo para o armazenamento foi usado.
Descobriu-se que, para esse tipo de chave, o método de remoção física do armazenamento físico se comporta de maneira diferente dos outros arquivos.
Na classe com exclusão, há um bloco de código com uma condição específica para arquivos com a chave antiga. O sistema, no momento da exclusão, antes de verificar se o arquivo não está em fragmentos, move esse arquivo antigo para outro local. Bem, isso não deu certo.
E, no momento em que o arquivo foi movido (e lembrarei que é muito popular), se um dos usuários tentar conceder a ele novos direitos de usuário, o software cliente vai para o armazenamento desse arquivo, mas o arquivo não está no lugar certo. Uma vez que é movido, isso não funciona. E o software cliente envia uma mensagem informando que o arquivo está quebrado. No banco de dados, está marcado como quebrado. E todas as informações são excluídas do banco de dados (bem, quase todas).
Enquanto isso, nossa rotina de verificação de fragmentos descobre que o arquivo está sendo usado. E envia uma resposta que você precisa para devolvê-lo. Mas todas as informações já foram excluídas do banco de dados e é impossível devolvê-las.
Engraçado né?
Ou seja, quando o arquivo foi excluído, o usuário estava naquele período em que o arquivo foi movido, os shards verificados e foi nesse momento que o usuário enviou uma solicitação de download.
Aqui está - alta carga de ação, quando as partidas mais incríveis que você combina.

Tendo nos recuperado de surpresa e revertendo tudo, garantimos que os arquivos dos usuários estejam vivos, pois foram restaurados a partir dos discos de outros clientes.
Naturalmente, tudo correu bem nos testes, porque, durante o teste, os arquivos mais recentes foram excluídos com um novo tipo de chave usado nos últimos 5 anos e não são transferidos para outro local de armazenamento durante o tempo da exclusão.
Nosso otimismo diminuiu e decidimos não seguir o caminho mais otimista.
Retrospectiva
Decidimos que precisamos adicionar testes para diferentes tipos de armazenamento
Adicione uma carga ao perf lab que use chamadas quando removidas do armazenamento
Fechar condições de corrida famosas
Adicionar monitoramento (apesar de estar nos planos, mas não se encaixar no escopo original)
Sobre o monitoramento
Eles decidiram fazer o monitoramento imediatamente, mas ele desapareceu em segundo plano, pois era necessário implantar mais rapidamente.
Para o monitoramento, o projeto usou o Zabbix, ELK, Grafana, NewRelic, SQL Sentry e uma versão de teste do AppDynamics.
Sobre isso, no pef lab estavam o NewRelic e o SQL Sentry.
Já medimos todas as métricas do sistema e, por isso, queríamos medir as métricas de negócios. Tive experiência em organizar esse monitoramento através do Zabbix - decidimos fazer o mesmo.
O esquema é muito simples no banco de dados para criar uma tabela na qual coletar as métricas necessárias pelo JOB e um procedimento que fará o upload das métricas coletadas no Zabbix.
Métricas:
- O número de arquivos na fila para exclusão em uma base global
- Número de arquivos na fila pelo servidor
- Número de arquivos enviados ao desinstalador
- Número de arquivos excluídos
- Número de eventos FileCorrupt
- O número de arquivos a serem excluídos em cada shard
O monitoramento foi implementado e implantado no produto separadamente, antes de começarem a implantar uma nova implementação de exclusão.
Nova solução
Em geral, decidimos que era melhor comer demais do que não dormir e fizemos um novo plano.
- verifique no mesmo fragmento que ninguém mais usa o arquivo e transfira apenas arquivos não utilizados para o servidor;
- ao transferir para o servidor, colete todos os arquivos na tabela e verifique se os arquivos não são usados nos shards antes de serem colocados na tabela da fila de desenfileiramento;
- ao usar um arquivo e procurá-lo no sistema, marque a fila de desenfileiramento na tabela como um arquivo que requer verificação;
- retornar apenas arquivos para os quais não houve pesquisas;
- arquivos que foram pesquisados, verifique novamente se há fragmentos;
- em geral, remova a verificação do procedimento que exclui o arquivo, pois ele deve funcionar rapidamente - e o arquivo usado não deve alcançá-lo em princípio;
- leve em consideração no procedimento que tudo exclui pelo arquivo batido que está em processo de exclusão e não exclui as informações nele.
O ponto 6 durante a implantação incluiu a remoção da verificação em várias etapas. Primeiro, eles deixaram o cheque e, uma semana depois, o cheque dos arquivos dos funcionários da empresa foi desativado; depois de duas semanas, o cheque foi desativado.
O que você precisou mudar no código?
Mais uma vez, todas as alterações se aplicam apenas ao banco de dados.
A escala das alterações foi a maior no servidor global:
Adicione uma tabela intermediária na qual adicionar todos os arquivos baixados dos shards.
Faça um JOB que verifique os arquivos na tabela intermediária que eles não são usados nos shards.
Na fila de arquivos excluídos, adicione um campo com a data em que o arquivo foi acessado pela última vez e adicione um índice.
Encontre todos os procedimentos com acesso ao arquivo - resultou em 5 procedimentos. Adicione a eles um bloco alterando a data do último uso na tabela de filas. A data muda sempre, independentemente de ter sido preenchida ou não.
Adicione o programa de exclusão aos procedimentos de emissão de arquivos para que ele exiba apenas arquivos com uma data de uso vazia.
Adicione um JOB que colete todos os arquivos com a data de uso e as verificações (com um atraso de 10 minutos, o que é necessário para que o software cliente possa adicionar o arquivo ao shard, geralmente leva até 2 minutos, mas decidiu jogar com segurança), use o arquivo em todos os shards. Após a conclusão da verificação, a data de uso é redefinida para zero se o arquivo não for encontrado, caso contrário, o arquivo será excluído da fila. Se a data de uso foi alterada durante o processo de verificação, os dados não são alterados, pois supõe-se que, enquanto a verificação estava em andamento, o arquivo pudesse ser carregado no fragmento, no qual a verificação já havia sido concluída e era necessário um novo ciclo de verificação para o arquivo.Em fragmentos:No procedimento que exclui arquivos da tabela com arquivos excluídos, foi necessário adicionar uma verificação para verificar se o arquivo não foi usado. O procedimento perdeu sua simplicidade e beleza não muito - em DELETE com saída, eles simplesmente adicionaram NOT EXISTS.Adicionamos um JOB que, em segundo plano, era proveniente dos arquivos de tabela usados no servidor.Testes
Cenários sobre o uso de todas as opções de armazenamento foram adicionados aos testes de integração.Eles também escreveram novos casos para testar a nova funcionalidade de remoção de arquivos.O Perf Lab adicionou carga ao servidor global. Além disso, adicionamos uma carga correspondente à exclusão de arquivos do armazenamento.Implantar
Os scripts foram preparados para a aplicação e reversão de alterações no banco de dados. O DBA rolou scripts e, durante o teste de carga, eles não prestaram atenção aos bloqueios nas tabelas de filas para excluir arquivos. Como resultado, não corrigimos o índice, que não era o mais ideal.Por esse motivo, foi necessário desativar as verificações de JOB por intervalos e analisar e adicionar identificadores de arquivos excluídos manualmente, por arquivos que foram excluídos no sistema antes da implantação do novo código.Resultados
Como resultado, como resultado da implantação, novos arquivos excluídos são excluídos do armazenamento dentro de 24 horas.Os arquivos excluídos antes do lançamento do novo sistema foram criados no backup e adicionados à fila para prod.Como resultado, dados em excesso no S3, no valor de 2 petabytes, foram excluídos. O mesmo aconteceu com os arquivos em servidores clientes dedicados e agora o local deles no servidor coincide com o local exibido nos clientes.O índice curvo na tabela da fila ainda vive em prod, a tarefa de alterar o índice na lista de pendências, mas ligeiramente adiado devido a tarefas de prioridade mais alta.