Acelerando a compilação em 2,5X com desacoplamento de dependências e conteinerização de testes
O tempo de compilação pode ser agravado por dependências internas e externas complexas que evoluem ao longo do processo de desenvolvimento, bem como por mudanças nos ambientes de compilação, como o sistema operacional ou as arquiteturas de hardware. A seguir estão problemas comuns que podem ser encontrados ao trabalhar em projetos de IA ou MLOps em larga escala:
Compilação proibitivamente longa - A integração de código é feita centenas de vezes por dia. Com centenas de milhares de linhas de código em vigor, até mesmo uma pequena alteração pode resultar em uma compilação completa que normalmente leva uma ou mais horas.
Ambiente de compilação complexo - O código do projeto precisa ser compilado em diferentes ambientes, que envolvem diferentes sistemas operacionais, como CentOS e Ubuntu, dependências subjacentes, como GCC, LLVM e CUDA, e arquiteturas de hardware. E a compilação em um ambiente específico normalmente pode não funcionar em um ambiente diferente.
Dependências complexas - A compilação do projeto envolve mais de 30 dependências entre componentes e de terceiros. O desenvolvimento do projeto frequentemente leva a mudanças nas dependências, causando inevitavelmente conflitos de dependência. O controle de versão entre dependências é tão complexo que atualizar a versão das dependências afetará facilmente outros componentes.
O download de dependências de terceiros é lento ou falha - Atrasos de rede ou bibliotecas de dependências de terceiros instáveis causam downloads de recursos lentos ou falhas de acesso, afetando seriamente a integração de código.
Ao desacoplar dependências e implementar a conteinerização de testes, conseguimos reduzir o tempo médio de compilação em 60% enquanto trabalhávamos no projeto open-source de busca por similaridade de embeddings Milvus.
Desacople as dependências do projeto
A compilação do projeto geralmente envolve um grande número de dependências de componentes internos e externos. Quanto mais dependências um projeto tem, mais complexo se torna gerenciá-las. À medida que o software cresce, torna-se mais difícil e custoso alterar ou remover dependências, bem como identificar os efeitos de fazê-lo. A manutenção regular é necessária ao longo do processo de desenvolvimento para garantir que as dependências funcionem adequadamente. Manutenção deficiente, dependências complexas ou dependências defeituosas podem causar conflitos que desaceleram ou paralisam o desenvolvimento. Na prática, isso pode significar downloads de recursos lentos, falhas de acesso que impactam negativamente a integração de código, entre outros. Desacoplar as dependências do projeto pode mitigar defeitos e reduzir o tempo de compilação, acelerando os testes do sistema e evitando atrasos desnecessários no desenvolvimento de software.
Portanto, recomendamos desacoplar as dependências do seu projeto:
- Separe componentes com dependências complexas
- Use diferentes repositórios para gerenciamento de versão.
- Use arquivos de configuração para gerenciar informações de versão, opções de compilação, dependências etc.
- Adicione os arquivos de configuração às bibliotecas de componentes para que sejam atualizados à medida que o projeto itera.
Otimização de compilação entre componentes — Extraia e compile o componente relevante de acordo com as dependências e as opções de compilação registradas nos arquivos de configuração. Marque e empacote os resultados da compilação binária e os arquivos de manifesto correspondentes e, em seguida, faça o upload deles para seu repositório privado. Se nenhuma alteração for feita em um componente ou nos componentes dos quais ele depende, reproduza seus resultados de compilação de acordo com os arquivos de manifesto. Para problemas como atrasos de rede ou bibliotecas de dependências de terceiros instáveis, tente configurar um repositório interno ou usar repositórios espelhados.
Para otimizar a compilação entre componentes:
1.Crie um gráfico de relacionamento de dependências — Use os arquivos de configuração nas bibliotecas de componentes para criar um gráfico de relacionamento de dependências. Use o relacionamento de dependência para recuperar as informações de versão (Git Branch, Tag e Git commit ID) e opções de compilação e muito mais de componentes dependentes upstream e downstream.
Figura 1.
2.Verificar dependências — Gere alertas para dependências circulares, conflitos de versão e outros problemas que surjam entre componentes.
3.Nivelar dependências — Ordene as dependências por Depth First Search (DFS) e mescle antecipadamente componentes com dependências duplicadas para formar um grafo de dependências.
Figura 2.
4.Use o algoritmo MerkleTree para gerar um hash (Root Hash) contendo as dependências de cada componente com base em informações de versão, opções de compilação e muito mais. Combinado com informações como o nome do componente, o algoritmo forma uma tag única para cada componente.
Figura 3.
5.Com base nas informações da tag única do componente, verifique se existe um arquivo de compilação correspondente no repositório privado. Se um arquivo de compilação for recuperado, descompacte-o para obter o arquivo manifest para reprodução; caso contrário, compile o componente, marque os arquivos de objeto de compilação gerados e o arquivo manifest, e envie-os para o repositório privado.
Implemente otimizações de compilação dentro dos componentes — Escolha uma ferramenta de cache de compilação específica da linguagem para armazenar em cache os arquivos de objeto compilados, e envie-os e armazene-os em seu repositório privado. Para compilação em C/C++, escolha uma ferramenta de cache de compilação como CCache para armazenar em cache os arquivos intermediários de compilação C/C++ e, em seguida, arquive o cache local do CCache após a compilação. Essas ferramentas de cache de compilação simplesmente armazenam em cache os arquivos de código alterados um por um após a compilação e copiam os componentes compilados do arquivo de código inalterado para que possam ser diretamente envolvidos na compilação final. A otimização da compilação dentro dos componentes inclui as seguintes etapas:
- Adicione as dependências de compilação necessárias ao Dockerfile. Use Hadolint para realizar verificações de conformidade no Dockerfile para garantir que a imagem esteja em conformidade com as melhores práticas do Docker.
- Espelhe o ambiente de compilação de acordo com a versão sprint do projeto (versão + build), sistema operacional e outras informações.
- Execute o contêiner do ambiente de compilação espelhado e transfira o ID da imagem para o contêiner como uma variável de ambiente. Aqui está um exemplo de comando para obter o ID da imagem: “docker inspect ‘ — type=image’ — format ‘{{.ID}}’ repository/build-env:v0.1-centos7”.
- Escolha a ferramenta de cache de compilação apropriada: Entre no seu contêiner para integrar e compilar seus códigos e verifique em seu repositório privado se existe um cache de compilação apropriado. Se sim, baixe-o e extraia-o para o diretório especificado. Depois que todos os componentes forem compilados, o cache gerado pela ferramenta de cache de compilação é empacotado e enviado para seu repositório privado com base na versão do projeto e no ID da imagem.
Otimização adicional da compilação
Nossa compilação inicial ocupa muito espaço em disco e largura de banda de rede, e leva muito tempo para implantar, tomamos as seguintes medidas:
- Escolha a imagem base mais enxuta para reduzir o tamanho da imagem, por exemplo, alpine, busybox, etc.
- Reduza o número de camadas da imagem. Reutilize dependências o máximo possível. Mescle vários comandos com “&&”.
- Limpe os produtos intermediários durante a construção da imagem.
- Use o cache de imagem para construir a imagem o máximo possível.
À medida que nosso projeto continua a progredir, o uso de disco e os recursos de rede começaram a disparar com o aumento do cache de compilação, enquanto alguns dos caches de compilação são subutilizados. Então fizemos os seguintes ajustes:
Limpar regularmente arquivos de cache — Verifique regularmente o repositório privado (usando scripts, por exemplo) e limpe arquivos de cache que não mudaram por um tempo ou não foram muito baixados.
Cache de compilação seletivo — Armazene em cache apenas compilações que exigem muitos recursos e ignore o cache de compilações que não exigem muitos recursos.
Aproveitando testes em contêineres para reduzir erros, melhorar a estabilidade e a confiabilidade
Os códigos precisam ser compilados em diferentes ambientes, que envolvem uma variedade de sistemas operacionais (por exemplo, CentOS e Ubuntu), dependências subjacentes (por exemplo, GCC, LLVM e CUDA) e arquiteturas de hardware específicas. Código que compila com sucesso em um ambiente específico pode falhar em um ambiente diferente. Ao executar testes dentro de contêineres, o processo de teste se torna mais rápido e mais preciso.
A conteinerização garante que o ambiente de teste seja consistente e que uma aplicação esteja funcionando conforme esperado. A abordagem de testes conteinerizados empacota testes como contêineres de imagem e cria um ambiente de teste verdadeiramente isolado. Nossos testadores descobriram que essa abordagem é bastante útil, o que acabou reduzindo os tempos de compilação em até 60%.
Garanta um ambiente de compilação consistente — Como os produtos compilados são sensíveis a mudanças no ambiente do sistema, erros desconhecidos podem ocorrer em diferentes sistemas operacionais. Precisamos marcar e arquivar o cache do produto compilado de acordo com as mudanças no ambiente de compilação, mas elas são difíceis de categorizar. Por isso, introduzimos a tecnologia de conteinerização para unificar o ambiente de compilação e resolver esses problemas.
Conclusão
Ao analisar as dependências do projeto, este artigo apresenta diferentes métodos para otimização da compilação entre componentes e dentro deles, fornecendo ideias e melhores práticas para construir uma integração contínua de código estável e eficiente. Esses métodos ajudaram a resolver a integração lenta de código causada por dependências complexas, unificar operações dentro do contêiner para garantir a consistência do ambiente e melhorar a eficiência da compilação por meio da reprodução dos resultados da compilação e do uso de ferramentas de cache de compilação para armazenar em cache os resultados intermediários da compilação.
As práticas acima mencionadas reduziram o tempo de compilação do projeto em 60% em média, melhorando significativamente a eficiência geral da integração de código. Daqui em diante, continuaremos paralelizando a compilação entre componentes e dentro deles para reduzir ainda mais os tempos de compilação.
As seguintes fontes foram usadas para este artigo:
- “Desacoplando árvores de código-fonte em componentes de nível de build”
- “Fatores a considerar ao adicionar dependências de terceiros a um projeto”
- “Sobrevivendo a dependências de software”
- “Entendendo dependências: um estudo dos desafios de coordenação no desenvolvimento de software”
Sobre o autor
Zhifeng Zhang é engenheiro DevOps sênior na Zilliz.com, trabalhando no Milvus, um banco de dados vetorial open-source, e instrutor autorizado da universidade de software open-source da LF na China. Ele recebeu seu bacharelado em Internet das Coisas (IOT) pelo Software Engineering Institute of Guangzhou. Ele dedica sua carreira a participar e liderar projetos nas áreas de CI/CD, DevOps, gerenciamento de infraestrutura de TI, toolkit Cloud-Native, conteinerização e otimização do processo de compilação.
Continue lendo

Zilliz Cloud Enterprise Vector Search Powers High-Performance AI on AWS
Zilliz Cloud on AWS powers secure, scalable, ultra-fast vector search for enterprise AI apps, with BYOC, sub-10ms latency, and zero-DevOps simplicity.

Our Journey to 35K+ GitHub Stars: The Real Story of Building Milvus from Scratch
Join us in celebrating Milvus, the vector database that hit 35.5K stars on GitHub. Discover our story and how we’re making AI solutions easier for developers.

Announcing the General Availability of Zilliz Cloud BYOC on Google Cloud Platform
Zilliz Cloud BYOC on GCP offers enterprise vector search with full data sovereignty and seamless integration.



