
Meu nome é Danil Mukhametzyanov e trabalho como desenvolvedor de back-end no Badoo há sete anos. Durante esse tempo, consegui criar e alterar uma grande quantidade de código. Tão grande que um dia um gerente veio até mim e disse: “A cota acabou. Para adicionar algo, você precisa remover algo. "
Ok, isso é apenas uma piada - ele não disse isso. É uma pena! Ao longo de toda a existência da empresa, o Badoo acumulou mais de 5,5 milhões de linhas de código de negócios lógico, excluindo linhas em branco e colchetes de fechamento.
A quantidade em si não é tão assustadora: ele mente, não pede comida. Mas, dois ou três anos atrás, comecei a perceber que leio cada vez com mais frequência e tento descobrir um código que realmente não funciona em um ambiente de produção. Isso está, de fato, morto.
Essa tendência não foi percebida apenas por mim. O Badoo percebeu que nossos engenheiros bem pagos estão constantemente perdendo tempo com códigos mortos.
Eu dei esta palestra no Badoo PHP Meetup # 4
De onde vem o código morto?
Começamos a procurar as causas dos problemas. Os dividiu em duas categorias:
- processo - aqueles que surgem como resultado do desenvolvimento;
- histórico - código legado.
Em primeiro lugar, decidimos desmontar as fontes de processo para evitar o surgimento de novos problemas.
Teste A / B
O Badoo começou a usar testes A / B ativamente há quatro anos. Agora temos cerca de 200 testes em execução constante e todos os recursos do produto passam por esse procedimento.
Como resultado, cerca de 2.000 testes concluídos foram acumulados em quatro anos, e esse número está em constante crescimento. Ela nos assustou com o fato de que cada teste é um pedaço de código morto que não é mais executado e nem é necessário.
A solução para o problema veio rapidamente: começamos a criar automaticamente um tíquete para cortar o código após a conclusão do teste A / B.

Um exemplo de ticket
Mas o fator humano é acionado periodicamente. Repetidamente, encontramos o código de teste que continuou em execução, mas ninguém pensou nisso e não concluiu o teste.
Depois, havia uma estrutura rígida: cada teste deve ter uma data de término. Se o gerente se esquecesse de ler os resultados do teste, ele parava e desligava automaticamente. E, como já mencionei, um tíquete foi criado automaticamente para cortá-lo, mantendo a versão original da lógica do recurso.
Com a ajuda de um mecanismo tão simples, nos livramos de uma grande camada de trabalho.
Diversidade de clientes
Várias marcas são suportadas em nossa empresa, mas o servidor é um. Cada marca é representada em três plataformas: web, iOS e Android. No iOS e no Android, temos um ciclo de desenvolvimento semanal: uma vez por semana, junto com uma atualização, recebemos uma nova versão do aplicativo em cada plataforma.
É fácil adivinhar que, com essa abordagem, em um mês teremos cerca de uma dúzia de novas versões que precisam ser suportadas. O tráfego de usuários é distribuído de maneira desigual entre eles: os usuários estão mudando gradualmente de uma versão para outra. Algumas versões mais antigas têm tráfego, mas é tão pequeno que é difícil mantê-lo. É difícil e inútil.
Então, começamos a contar o número de versões que queremos oferecer suporte. Existem dois limites para o cliente: o limite flexível e o limite rígido.
Quando o soft limit é atingido (quando três ou quatro novas versões já foram lançadas e o aplicativo ainda não foi atualizado), o usuário vê uma tela com um aviso de que sua versão está desatualizada. Quando o limite rígido é atingido (cerca de 10-20 versões "perdidas", dependendo do aplicativo e da marca), simplesmente removemos a opção de pular esta tela. Torna-se um bloqueio: você não pode usar o aplicativo com ele.

Tela do limite rígido
Neste caso, é inútil continuar a processar as solicitações vindas do cliente - ele não verá nada além de uma tela.
Mas aqui, como no caso dos testes A / B, surgiu uma nuance. Os desenvolvedores de clientes também são pessoas. Eles usam novas tecnologias, chips de sistemas operacionais - e depois de um tempo, a versão do aplicativo não é mais compatível com a próxima versão do sistema operacional. No entanto, o servidor continua sofrendo porque deve continuar a processar essas solicitações.
Chegamos a uma solução separada para o caso de término do suporte para Windows Phone. Preparamos uma tela que informava ao usuário: “Amamos muito você! Você é muito legal! Mas você pode começar a usar outra plataforma? Novas funções interessantes estarão disponíveis para você, mas aqui não podemos fazer nada. " Via de regra, oferecemos uma plataforma web como plataforma alternativa, que está sempre disponível.
Com um mecanismo tão simples, limitamos o número de versões de cliente que o servidor suporta: aproximadamente 100 versões diferentes de todas as marcas, de todas as plataformas.
Sinalizadores de recursos
No entanto, ao desabilitar o suporte para plataformas mais antigas, não entendemos totalmente se era possível cortar completamente o código que estavam usando. Ou as plataformas que permanecem para versões mais antigas do sistema operacional continuam a usar a mesma funcionalidade?
O problema é que nossa API não foi construída na parte com versão, mas no uso de sinalizadores de recursos. Como chegamos a isso, você pode descobrir neste relatório .
Tínhamos dois tipos de sinalizadores de recursos. Vou falar sobre eles com exemplos.
Características secundárias
O cliente diz ao servidor: “Olá, sou eu. Eu apóio postagens de fotos. " O servidor olha para ele e responde: “Ótimo, suporte! Agora eu sei sobre isso e vou enviar mensagens com fotos. " O principal recurso aqui é que o servidor não pode influenciar o cliente de nenhuma forma - ele simplesmente aceita mensagens dele e é forçado a ouvi-las.
Chamamos esses sinalizadores de recursos secundários. No momento, temos mais de 600.
Qual é a desvantagem de usar essas bandeiras? Periodicamente, há funcionalidades pesadas que não podem ser cobertas apenas pelo lado do cliente - você deseja controlá-las também pelo lado do servidor. Para isso, introduzimos outros tipos de sinalizadores.
Recursos do aplicativo
O mesmo cliente, o mesmo servidor. O cliente diz: “Servidor, aprendi a suportar streaming de vídeo. Ligar? " O servidor responde: "Obrigado, vou manter isso em mente." E acrescenta: “Ótimo. Vamos mostrar ao nosso querido usuário essa funcionalidade, ele ficará feliz. " Ou: "Ok, mas não vamos incluir ainda."
Chamamos esses recursos de recursos do aplicativo. Eles são mais pesados, então temos menos deles, mas ainda o suficiente: mais de 300.
Assim, os usuários mudam de uma versão do cliente para outra. Algum tipo de sinalizador está começando a ser suportado por todas as versões ativas de aplicativos. Ou, inversamente, sem suporte. Não está totalmente claro como controlar isso: 100 versões de cliente, 900 sinalizadores! Para lidar com isso, construímos um painel.
Um quadrado vermelho significa que todas as versões desta plataforma não oferecem suporte a esse recurso; verde - todas as versões desta plataforma suportam este sinalizador. Se a bandeira puder ser desligada e ligada, ela piscará periodicamente. Podemos ver o que acontece em cada versão.

Tela do painel
Bem nesta interface, começamos a criar tarefas para cortar a funcionalidade. Deve-se observar que nem todas as células vermelhas ou verdes em cada linha precisam ser preenchidas. Existem sinalizadores que funcionam apenas em uma plataforma. Existem sinalizadores preenchidos para apenas uma marca.
Automatizar o processo não é tão conveniente, mas, em princípio, não é necessário - você só precisa definir uma tarefa e olhar periodicamente o painel. Na primeira iteração, conseguimos cortar mais de 200 sinalizadores. Isso é quase um quarto das bandeiras que usamos!
É aqui que as fontes do processo terminaram. Eles surgiram como resultado de nosso fluxo de desenvolvimento e integramos com sucesso o trabalho com eles neste processo.
O que fazer com o código legado
Paramos o surgimento de novos problemas nas fontes de processo. E nos deparamos com uma pergunta difícil: o que fazer com o código legado acumulado ao longo dos anos? Abordamos a solução do ponto de vista da engenharia, ou seja, decidimos automatizar tudo. Mas não estava claro como encontrar o código que não estava sendo usado. Ele se escondeu em seu mundinho aconchegante: ele não liga de jeito nenhum, não deixa ninguém saber de si mesmo.
Tínhamos que ir do outro lado: pegar todo o código que tínhamos, coletar informações sobre quais peças são executadas exatamente e depois fazer a inversão.
Em seguida, reunimos e implementamos no nível mínimo - em arquivos. Dessa forma, poderíamos facilmente obter uma lista de arquivos do repositório executando o comando UNIX apropriado.
Restou coletar uma lista de arquivos que são usados na produção. É muito simples: para cada solicitação de desligamento, chame a função PHP correspondente. A única otimização que fizemos aqui é começar a solicitar OPCache em vez de solicitar cada solicitação. Caso contrário, a quantidade de dados seria muito grande.
Como resultado, descobrimos muitos artefatos interessantes. Mas, com uma análise mais profunda, percebemos que faltavam métodos não utilizados: a diferença em seu número era de três a sete vezes.
Descobriu-se que o arquivo pode ser carregado, executado e compilado por causa de apenas uma constante ou um par de métodos. Todo o resto permanecia inútil para estar neste mar sem fundo.
Reunindo uma lista de métodos
No entanto, resultou rápido o suficiente para coletar uma lista completa de métodos. Nós apenas pegamos o analisador de Nikita Popov , alimentamos ele com nosso repositório e pegamos tudo o que temos no código.
A questão permanece: como montar o que está sendo reproduzido na produção? Estamos interessados na produção, porque os testes podem cobrir o que não precisamos. Sem pensar duas vezes, pegamos XHProf. Já foi executado em produção para parte das requisições, portanto tínhamos amostras de perfis que ficam armazenadas nas bases de dados. Bastava ir a esses bancos de dados, analisar os instantâneos gerados - e obter uma lista de arquivos.
Desvantagens do XHProf
Repetimos esse processo em outro cluster onde o XHProf não foi iniciado, mas foi extremamente necessário. Este é um cluster para a execução de scripts de segundo plano e processamento assíncrono, o que é importante para alta carga, pois executa muita lógica.
E então nos certificamos de que o XHProf é inconveniente para nós.
- Requer a mudança do código PHP. Você precisa inserir o código de início de rastreamento, finalizar o rastreamento, obter os dados coletados e gravá-los em um arquivo. Afinal, este é um profiler, mas nós temos produção, ou seja, são muitas requisições, você precisa pensar também na amostragem. No nosso caso, isso foi agravado por um grande número de clusters com diferentes pontos de entrada.
- . . , OPCache. : XHProf, . , core- .
- . . XHProf . ( XHProf): CPU, , . , , . - XHProf aggregator ( XHProf Live Profiler, open-source) , , , . , : «, , », CPU , , Live Profiler . , , .
- XHProf. , . . , . : , ( , youROCK, isso não é exigido pelo lsd , mas era mais conveniente manter um único wrapper sobre ele). Corrigir XHProf não é o que queríamos fazer, porque é um profiler bastante grande (e se quebrarmos algo inadvertidamente?).
Havia outra ideia - excluir certos espaços de nomes, por exemplo, espaços de nomes de fornecedores do compositor, que são executados na produção, porque são inúteis: não iremos refatorar pacotes de fornecedores e cortar código desnecessário deles.
Requisitos de solução
Mais uma vez nos reunimos e examinamos as soluções existentes. E eles formularam a lista final de requisitos.
Primeiro: sobrecarga mínima. Para nós, o XHProf era a barra: não mais do que o necessário.
Em segundo lugar, não queríamos mudar o código PHP.
Terceiro, queríamos que a solução funcionasse em qualquer lugar - tanto no FPM quanto na CLI.
Em quarto lugar, queríamos lidar com os garfos. Eles são usados ativamente em CLI, em servidores em nuvem. Eu não queria fazer uma lógica específica para eles dentro do PHP.
Quinto: amostragem fora da caixa. Na verdade, isso decorre da exigência de não alterar o código PHP. Abaixo vou explicar porque precisamos de amostragem.
Sexto e último:a capacidade de forçar do código. Adoramos quando tudo funciona automaticamente, mas às vezes é mais conveniente iniciar manualmente, ajustar, olhar. Precisávamos habilitar e desabilitar tudo diretamente do código, e não por decisão aleatória do mecanismo mais geral do módulo PHP, que define a probabilidade de inclusão por meio de configurações.
Como funciona o funcmap
Como resultado, temos uma solução que chamamos de funcmap.
Funcmap é essencialmente uma extensão do PHP. Em termos de PHP, este é um módulo PHP. Para entender como isso funciona, vamos dar uma olhada em como o processo e o módulo PHP funcionam.
Então, você inicia um processo. PHP torna possível assinar ganchos ao construir um módulo. O processo inicia, o gancho GINIT (Global Init) é lançado, onde você pode inicializar os parâmetros globais. Então o módulo é inicializado. Constantes podem ser criadas e alocadas lá, mas apenas para um módulo específico, e não para uma solicitação, caso contrário você vai dar um tiro no próprio pé.
Em seguida, a solicitação do usuário chega, o gancho RINIT (Request Init) é chamado. Quando a solicitação é concluída, seu desligamento ocorre e, no final - o desligamento do módulo: MSHUTDOWN e GSHUTDOWN. Tudo é lógico.
Se estamos falando sobre FPM, cada solicitação do usuário chega a um trabalhador já existente. Basicamente, RINIT e RSHUTDOWN trabalham em círculos até que o FPM decida que o trabalhador está desatualizado, é hora de atirar nele e criar um novo. Se estamos falando sobre CLI, então é apenas um processo linear. Tudo será chamado uma vez.

Como funciona o funcmap
Fora desse conjunto, estávamos interessados em dois ganchos. O primeiro é RINIT . Começamos a definir o flag de coleta de dados: é uma espécie de aleatório que foi chamado para amostrar os dados. Se funcionou, processamos esta solicitação: coletamos estatísticas para chamadas de funções e métodos para isso. Se não funcionou, a solicitação não foi processada.
O próximo passo é criar uma tabela hash, se ela não existir. A tabela hash é fornecida internamente pelo próprio PHP. Não há necessidade de inventar nada aqui - apenas pegue e use.
Em seguida, inicializamos o cronômetro. Falarei sobre ele a seguir, por enquanto, basta lembrar que ele é importante e necessário.
O segundo gancho é MSHUTDOWN... Quero observar que é MSHUTDOWN, não RSHUTDOWN. Não queríamos resolver algo para cada pedido - estávamos interessados em todo o trabalhador. No MSHUTDOWN, pegamos nossa tabela hash, examinamos e escrevemos um arquivo (o que poderia ser mais confiável, mais conveniente e versátil do que o bom e velho arquivo?).
A tabela hash é preenchida simplesmente pelo mesmo gancho PHP zend_execute_ex, que é chamado toda vez que uma função definida pelo usuário é chamada. O registro contém parâmetros adicionais pelos quais você pode entender que tipo de função é, seu nome e classe. Nós o aceitamos, lemos o nome, gravamos na tabela hash e, em seguida, chamamos o gancho padrão.
Este gancho não escreve funções embutidas. Se você deseja substituir as funções integradas, há uma funcionalidade separada para isso chamada zend_execute_internal.
Configuração
Como posso configurar isso sem alterar o código PHP? As configurações são muito simples:
- habilitado: esteja habilitado ou não.
- O arquivo para o qual estamos gravando. Há um espaço reservado pid para excluir uma condição de corrida quando diferentes processos PHP gravam no mesmo arquivo ao mesmo tempo.
- Base de probabilidade: nossa bandeira de probabilidade. Se você definir como 0, nenhuma solicitação será escrita; se 100 - então todas as solicitações serão registradas e incluídas nas estatísticas.
- flush_interval. Esta é a frequência com que despejamos todos os dados em um arquivo. Queremos que a coleta de dados seja executada na CLI, mas existem scripts que podem ser executados por muito tempo, consumindo memória se você usar uma grande quantidade de funcionalidade.
Além disso, se tivermos um cluster que não seja tão carregado, o FPM entende que o trabalhador está pronto para processar mais e não mata o processo - ele vive e consome alguma parte da memória. Depois de um determinado período, descarregamos tudo no disco, redefinimos a tabela hash e começamos a preenchê-la novamente. Se, no entanto, o tempo limite ainda não foi atingido, o gancho MSHUTDOWN é acionado, onde finalmente escrevemos tudo.
A última coisa que queríamos era a capacidade de chamar o funcmap a partir do código PHP. A extensão correspondente fornece o único método que permite ativar ou desativar a coleta de estatísticas, independentemente de como a probabilidade funcionou.
Overheads
Ficamos imaginando como tudo isso afeta nossos servidores. Construímos um gráfico que mostra o número de solicitações que chegam a uma máquina de combate real de um dos clusters PHP mais carregados.
Pode haver muitas dessas máquinas, então o gráfico mostra o número de solicitações, não a CPU. O balanceador percebe que a máquina começou a consumir mais recursos do que o normal e tenta equalizar as demandas para que as máquinas sejam carregadas uniformemente. Isso foi o suficiente para entender como o servidor está degradando.
Ligamos nossa extensão sequencialmente em 25%, 50% e 100% e vimos a seguinte imagem:

A linha pontilhada é o número de solicitações que esperamos. A linha principal é o número de solicitações recebidas. Vimos uma degradação de aproximadamente 6%, 12% e 23%: este servidor começou a processar quase um quarto a menos de solicitações recebidas.
Este gráfico, em primeiro lugar, prova que a amostragem é importante para nós: não podemos gastar 20% dos recursos do servidor na coleta de estatísticas.
Resultado falso
A amostragem tem um efeito colateral: alguns métodos não são incluídos nas estatísticas, mas são usados. Tentamos combater isso de várias maneiras:
- . -, . , , , , .
- . , : , , .
Tentamos duas soluções para tratamento de erros. O primeiro é permitir a coleta de estatísticas forçosamente a partir do momento em que o erro foi gerado: coletamos o log de erros e o analisamos. Mas há uma armadilha aqui: quando um recurso cai, o número de erros aumenta instantaneamente. Você começa a processá-los, há muito mais trabalhadores - e o cluster começa a morrer lentamente. Portanto, fazer isso não é totalmente correto.
Como fazer de forma diferente? Lemos e, usando o analisador de Nikita Popov, analisamos as apostas, observando quais métodos são chamados lá. Assim, eliminamos a carga no servidor e reduzimos o número de falsos positivos.
Mas ainda havia métodos que raramente eram chamados e sobre os quais não estava claro se eram necessários ou não. Adicionamos um auxiliar que ajuda a determinar o fato de usar tais métodos: se a amostragem já mostrou que o método raramente é chamado, então você pode ativar o processamento 100% e não pensar no que está acontecendo. Qualquer execução deste método será registrada. Você saberá sobre isso.
Se você tiver certeza de que o método está sendo usado, pode ser um exagero. Talvez esta seja uma funcionalidade necessária, mas rara. Imagine que você tem a opção "Reclamar", que raramente é usada, mas é importante - você não pode cortá-la. Para esses casos, aprendemos como rotular manualmente esses métodos.
Criamos uma interface que mostra quais métodos estão em uso (eles estão em um fundo branco) e quais são potencialmente não usados (eles estão em um fundo vermelho). Aqui você também pode marcar os métodos necessários.
Tela de interface
A interface é ótima, mas vamos voltar ao início, que é o problema que estávamos resolvendo. Consistia no fato de que nossos engenheiros liam código morto. Onde eles lêem isso? No IDE. Imagine como seria fazer um fã de sua arte deixar o mundo IDE por algum tipo de interface da web e fazer algo lá! Decidimos que precisamos encontrar nossos colegas no meio do caminho.
Fizemos um plugin para PhpStorm que carrega todo o banco de dados de métodos não utilizados e mostra se este método é usado ou não. Além disso, você pode marcar o método como sendo usado na interface. Tudo isso irá para o servidor e ficará disponível para o restante dos contribuidores da base de código.
Isso conclui a parte principal do nosso trabalho com o Legacy. Começamos a perceber mais rápido que não estamos executando, respondemos mais rapidamente e não perdemos tempo procurando manualmente por código não utilizado.
A extensão funcmap está disponível no GitHub . Ficaremos felizes se for útil para alguém.
Alternativas
Do lado de fora, pode parecer que nós do Badoo não sabemos o que fazer de nós mesmos. Por que não dar uma olhada no que há no mercado?
Esta é uma pergunta justa. Olhamos - e não havia nada no mercado naquele momento. Foi só quando começamos a implementar ativamente nossa solução que descobrimos que, ao mesmo tempo, um homem chamado Joe Watkins, que vivia na nebulosa Grã-Bretanha, implementou uma ideia semelhante e criou a extensão Tombs.
Não estudamos com muito cuidado, pois já tínhamos nossa própria solução, mas mesmo assim encontramos vários problemas:
- Falta de amostragem. Acima, expliquei por que precisamos disso.
- . , APCu ( ), .
- CLI. , , CLI-, .
- . Tombs, , , , , , . funcmap («» , ): , . Tombs , , FPM CLI. - , .
Primeiro, pense com antecedência sobre como você removerá a funcionalidade que é implementada por um curto período de tempo, especialmente se o desenvolvimento for muito ativo. Em nosso caso, foram testes A / B. Se você não pensar nisso com antecedência, terá que limpar os escombros.
Segundo: conheça seus clientes de vista. Não importa se eles são internos ou externos - você deve conhecê-los. Em algum momento, você precisa dizer a eles: “Querido, pare! Não".
Terceiro: limpe sua API. Isso leva a uma simplificação de todo o sistema.
E quarto: você pode automatizar tudo, até mesmo a busca por código morto. O que nós fizemos.