Declaração do problema
Periodicamente, tenho a tarefa de compartilhar arquivos em uma rede local, por exemplo, com um colega do projeto.
Pode haver muitas soluções para isso - Samba / FTP / scp. Você pode simplesmente enviar o arquivo para um local público como o Google Drive, anexá-lo a uma tarefa no Jira ou até enviá-lo por e-mail.
Mas tudo isso, em um grau ou outro, é inflexível, em algum lugar requer ajuste preliminar e tem suas próprias limitações (por exemplo, o tamanho máximo do investimento).
E você quer algo mais leve e flexível.
Sempre fiquei agradavelmente surpreso com a oportunidade no Linux, usando os meios disponíveis, de criar rapidamente uma solução prática.
Digamos, eu frequentemente resolvia a tarefa mencionada usando o python do sistema com a seguinte linha única
$ python3 -mhttp.server Serving HTTP on 0.0.0.0 port 8000 ...
Este comando inicia o servidor da web na pasta atual e permite obter uma lista de arquivos e baixá-los através da interface da web. Mais dessas coisas podem ser despejadas aqui .
Existem vários inconvenientes. Agora, para transferir o link de download para um colega, você precisa saber o seu endereço IP na rede.
É conveniente usar o comando
$ ifconfig -a
E, a partir da lista resultante de interfaces de rede, selecione a apropriada e componha manualmente um link no formato http: // IP: 8000 , que você pode enviar.
Segunda inconveniência: este servidor é de thread único. Isso significa que, embora um de seus colegas faça o download do arquivo, o segundo nem será capaz de baixar a lista de arquivos.
Terceiro, é inflexível. Se você precisar transferir apenas um arquivo, ele abrirá a pasta inteira, ou seja, você precisará executar esses gestos (e depois limpar o lixo):
$ mkdir tmp1 $ cp file.zip tmp1 $ cd tmp1 $ python3 -mhttp.server
O quarto inconveniente - não há maneira fácil de baixar todo o conteúdo de uma pasta.
Para transferir o conteúdo de uma pasta, eles geralmente usam uma técnica chamada tar pipe .
Eles fazem algo assim:
$ ssh user@host 'cd /path/to/source && tar cf - .' | cd /path/to/destination && tar xvf -
Se de repente não estiver claro, explicarei como funciona. A primeira parte do comando tar cf - .
cria um arquivo morto do conteúdo da pasta atual e grava na saída padrão. Além disso, essa saída é transmitida por canal através de um canal ssh seguro para a entrada de um tar xvf -
semelhante tar xvf -
que executa o procedimento oposto, ou seja, lê a entrada padrão e descompacta na pasta atual. De fato, o arquivo morto é transferido, mas sem a criação de um arquivo intermediário!
A inconveniência dessa abordagem também é óbvia. Precisamos de acesso ssh de uma máquina para outra, e isso quase nunca é feito no caso geral.
É possível alcançar todas as opções acima, mas sem os problemas descritos?
Então, é hora de formalizar o que construiremos:
- Programa fácil de instalar (binário estático)
- O que permitirá que você transfira um arquivo e uma pasta com todo o conteúdo
- Com compressão opcional
- O que permite que o host baixe o (s) arquivo (s) usando apenas ferramentas * nix padrão (wget / curl / tar)
- O programa imediatamente após o lançamento emitirá os comandos exatos para fazer o download
Solução
Na conferência do JEEConf , da qual participei há pouco tempo, o tópico Graal foi levantado repetidamente. O assunto está longe de ser novo, mas para mim foi um gatilho finalmente sentir essa fera com minha própria mão.
Para aqueles que ainda não estão no tópico (existe realmente algo assim ainda? OO), deixe-me lembrá-lo de que o GraalVM é uma JVM tão bombeada da Oracle com recursos adicionais, sendo os mais visíveis:
- JVM Polyglot - a capacidade de executar perfeitamente Java, Javascript, Python, Ruby, R, etc. código
- Suporte para compilação AOT - compilando Java diretamente no binário nativo
- Um recurso menos notável, mas muito interessante - o compilador C2 foi reescrito do C ++ para o Java, a fim de facilitar o seu desenvolvimento. Isso já produziu resultados visíveis. Esse compilador faz muito mais otimizações no estágio de conversão do bytecode Java em código nativo. Por exemplo, ele é capaz de eliminar com mais eficiência as alocações. O Twitter conseguiu reduzir o consumo de CPU em 11% apenas ativando essa configuração, que em sua escala proporcionou uma economia notável de recursos (e dinheiro).
Você pode atualizar a idéia de Graal, por exemplo, neste artigo .
Como escreveremos em Java, o recurso mais relevante para nós será a compilação AOT.
Na verdade, o resultado do desenvolvimento é apresentado neste repositório do Github .
Exemplo de uso para transferir um único arquivo:
$ serv '/path/to/report.pdf' To download the file please use one of the commands below: curl http://192.168.0.179:17777/dl > 'report.pdf' wget -O- http://192.168.0.179:17777/dl > 'report.pdf' curl http://192.168.0.179:17777/dl?z --compressed > 'report.pdf' wget -O- http://192.168.0.179:17777/dl?z | gunzip > 'report.pdf'
Um exemplo de uso ao transferir o conteúdo de uma pasta (todos os arquivos, incluindo os anexados!):
$ serv '/path/to/folder' To download the files please use one of the commands below. NB! All files will be placed into current folder! curl http://192.168.0.179:17777/dl | tar -xvf - wget -O- http://192.168.0.179:17777/dl | tar -xvf - curl http://192.168.0.179:17777/dl?z | tar -xzvf - wget -O- http://192.168.0.179:17777/dl?z | tar -xzvf -
Sim, tão simples!
Observe - o próprio programa determina o endereço IP correto no qual os arquivos estarão disponíveis para download.
Observações / Pensamentos
É claro que um dos objetivos na criação do programa era a sua compacidade. E aqui está o resultado alcançado:
$ du -hs `which serv` 2.4M /usr/local/bin/serv
Incrivelmente, toda a JVM, junto com o código do aplicativo, se encaixa em alguns miseráveis megabytes! Claro, tudo está um pouco errado, mas mais sobre isso mais tarde.
De fato, o compilador Graal produz um binário ligeiramente maior que 7 megabytes de tamanho. Eu decidi comprimir ainda mais com o UPX .
Isso acabou sendo uma boa ideia, pois o tempo de lançamento aumentou enquanto era muito insignificante:
Opção descompactada:
$ time ./build/com.cmlteam.serv.serv -v 0.1 real 0m0.001s user 0m0.001s sys 0m0.000s
Comprimido:
$ time ./build/serv -v 0.1 real 0m0.021s user 0m0.021s sys 0m0.000s
Para comparação, o tempo de lançamento da "maneira tradicional":
$ time java -cp "/home/xonix/proj/serv/target/classes:/home/xonix/.m2/repository/commons-cli/commons-cli/1.4/commons-cli-1.4.jar:/home/xonix/.m2/repository/org/apache/commons/commons-compress/1.18/commons-compress-1.18.jar" com.cmlteam.serv.Serv -v 0.1 real 0m0.040s user 0m0.030s sys 0m0.019s
Como você pode ver, duas vezes mais lento que a versão UPX.
Em geral, um tempo de início curto é um dos pontos fortes do GraalVM. Isso, além do baixo consumo de memória, leva a um entusiasmo significativo com o uso dessa tecnologia para microsserviços e sem servidor.
Tentei tornar o programa lógico o mínimo possível e usar o mínimo de bibliotecas. Em princípio, essa abordagem geralmente é justificada e, nesse caso, eu tinha preocupações de que a adição de dependências de terceiros de terceiros aumentasse significativamente o arquivo de programa resultante.
Por exemplo, é por isso que não usei uma dependência de terceiros para um servidor da Web Java (e há muitas para todos os gostos e cores), mas usei a implementação JDK de um servidor da web com.sun.net.httpserver.*
Pacote. Na verdade, o uso do pacote com.sun.*
É considerado uma com.sun.*
, mas considero isso permitido neste caso, pois estou compilando o código nativo e, portanto, não há dúvida de compatibilidade entre a JVM.
No entanto, meus medos foram completamente em vão. No programa, usei duas dependências por conveniência
commons-cli
- para analisar argumentos de linha de comandocommons-compress
- para gerar um tarball de pasta e uma compressão gzip opcional
Ao mesmo tempo, o tamanho do arquivo aumentou um pouco. Atrevo-me a sugerir que o compilador Graal é muito inteligente para não colocar todos os jar-nicks de plug-in no arquivo executável, mas apenas o código deles que é realmente usado pelo código do aplicativo.
A compilação no código Graal nativo é realizada pelo utilitário de imagem nativa . Vale ressaltar que esse processo consome muitos recursos. Digamos, na minha configuração não tão lenta com uma CPU Intel 7700K integrada, esse processo leva 19 segundos. Portanto, recomendo que, ao desenvolver, execute o programa como de costume (via java) e colete o binário na fase final.
Conclusões
Parece-me que o experimento foi muito bem-sucedido. Ao desenvolver o uso do kit de ferramentas Graal, não encontrei nenhum problema intransponível ou mesmo significativo. Tudo funcionou de forma previsível e estável. Embora, quase certamente, tudo não seja tão tranquilo se você tentar criar algo mais complexo dessa maneira, por exemplo, um aplicativo no Spring Boot . No entanto, várias plataformas já foram apresentadas nas quais o suporte nativo ao Graal é declarado. Entre eles estão Micronaut , Microprofile , Quarkus .
Quanto ao desenvolvimento do projeto, uma lista de melhorias planejadas para a versão 0.2 já está pronta. Além disso, no momento, a montagem do binário final é implementada apenas para Linux x64. Espero que essa omissão seja corrigida no futuro, principalmente porque o compilador de native-image
da Graal suporta MacOS e Windows. Infelizmente, ainda não suporta compilação cruzada, o que poderia facilitar muito as coisas.
Espero que o utilitário apresentado seja útil para pelo menos alguém da respeitável comunidade habr. Ficarei duplamente feliz se houver quem queira contribuir com o projeto .