DEV Community

Guilherme Lira
Guilherme Lira

Posted on

ARM vs x86 em Docker

Intro

Hoje bem sabemos que processadores de arquitetura ARM, vem se tornado cada vez mais presentes em servidores, notebooks e em gabinetes e até como opções de flavors para instâncias computacionais em clouds públicas. Sabendo disso meus caros amigos e amigas, vi que podemos ter alguns pequenos desafíos ao trabalhar com o nosso querido Docker. Então escrevi esse artigo pra entendermos um pouco sobre a diferença dessas arquiteturas e o que elas impactam no nosso Container Engine tão amado que é o Docker.

Diferença entre ARM e x86

A principal diferença entre processadores ARM e x86 é que os precessadores do tipo ARM são RISC e os x86 são CISC. Como precisamos ter uma noção sobre essas arquiteturas para darmos seguimeto com o artigo, irei dar uma pequena expicação, nada muito profundo, sobre essas arquiteturas.

RISC ( Reduced Instruction Set Computer ) o que significa que suas instruções são mais reduzidas e que tendem ser mais simples e executadas em ciclos de relógio mais curtos. Uma instrução no padrão RISC é quebrada em instruções menores para que caibam em um ciclo de relógio. Todas as instruções tem um tamanho específico. Diferente do CISC ( Complex Instruction Set Computer ). No modelo CISC temos instruções mais complexas, sendo elas sem tamanho determinado podendo assumir dimensões variáveis de acordo com a quantidade de operações que deverão ser executadas.

Image description

💡 Um ciclo de relógio é a frequência medida em Hertz que determina quantos impulsos serão realizados por segundo naquele computador.

Como buildar uma imagem para uma arquitetura específica

Tendo em mente que a grande maioria dos notebooks, computadores de mesa e datacenters usam processadores x86, vai ser normal gerar nossas imagens nessas máquinas e isso pode acarretar em alguns problemas.

Também partindo do pressuposto que sabemos que um container nada mais é do que um isolamento dentro do SO ( Sistema Operacional ), e por isso ao fazer a geração de uma imagem, não adequada, para uma máquina com a CPU de arquitetura diferente de onde a imagem será executadada, vai acerretar nos problemas que veremos a seguir.

💡 Geralmente imagens muito usadas como RabbitMQ, Ubuntu ou até NodeJs são multi-plataforma, elas já tem imagens expecificas para cada arquitetura de CPU, então não será necessário expecificar, veremos isso mais ao decorrer do artigo.

Possíveis erros que vão acontecer

Bom como vimos acima para cada tipo de arquitetura as instruções são organizadas de uma maneira específica, então é claro que se executarmos um "build", dentro de uma máquina com processador x86, e tentar executar essa imagem dentro de uma máquina de processador ARM, vamos ter o seguinte erro.

$ docker-compose up -d
$ ... The requested image`s platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
Enter fullscreen mode Exit fullscreen mode

Um ponto muito importante, mais pra frente do artigo veremos como fazer o build correto para cada arquitetura, mas devemos ter uma atenção maior para as dependências do seu código, pois, mesmo que a imagem seja criada da maneira correta, talvez as suas dependências não foram buildadas para arquitetura ARM. Para resolver esse problema em NodeJs, podemos fazer da seguinte maneira, e com um simples comando:

$ npm install --cpu arm64 --os XPTO 
# Esse é so um exemplo você pode usar o SO que quiser
Enter fullscreen mode Exit fullscreen mode

💡 Quando der um pull na sua imagem será possível ver para qual tipo de arquitetura sua imagem foi gerada, com o seguinte comando:

$ docker image inspect rabbitmq:latest --format='{{.Architecture}}'
arm64 # Retorno do comando

Dessa maneira, antes mesmo de usar a imagem, podemos saber se será compativel com a arquitetura da CPU, evitando erros futuros.

Como saber a arquitetura da sua CPU

Para saber qual a arquitetura da sua CPU é so digitar o camando, se você estiver em uma máquina com linux, o seguinte comando:

# Mac M1
$ uname -m
arm64 

# Maquina com processador Intel
$ uname -m
x86_64
Enter fullscreen mode Exit fullscreen mode

Estratégias de build

Como vimos durante todo o artigo, imagens Docker podem suportar múltiplas plataformas. Isso significa que uma simples imagem do Docker pode conter variantes para diferentes arquiteturas. Mais acima comentei que quando imagens tem suporte a multi-pltaforma, o Docker automaticamente seleciona a imagem que corresponde a arquitetura e SO da máquina onde a imagem será baixada. Sabendo disso, vamos ver duas possibilidades de criar uma imagem multi-plataforma.

Antes de começarmos preciso falar um pouco sobre nosso amigo docker buildx, Buildx é um componente do Docker que tem ótimas funcionalidades para geração de imagem, e uma delas é a multi-plataforma. Todas as imagens geradas por ele são executadas com o Moby Buildkit.

Para fazer a criação de imagens multi-plataforma, antes devemos criar uma builder instance, para criar segue o comando abaixo:

$ docker buildx create --use
Enter fullscreen mode Exit fullscreen mode

Ao criar uma imagem multiplataforma a partir de um Dockerfile, efetivamente seu Dockerfile é construído uma vez para cada plataforma. No final da compilação, todas essas imagens são mescladas em uma única imagem multiplataforma.

FROM alpine
RUN echo "Hello" > /hello
Enter fullscreen mode Exit fullscreen mode

Por exemplo, nesse caso acima onde temos um simples Dockerfile, ao executar o comando docker buildx build --platform=linux/amd64,linux/arm64 ., o BuildKit vai subir duas imagens Alpine de diferentes versões, uma para cada tipo de arquitetura passada no comando, e vai executar todas as camandas para cada tipo de arquitetura. Por isso, é muito importante ter uma imagem base que tenha suporte para várias arquiteturas.

Emulação

Para executar esse tipo de build é bem simples, no exemplo acima já ocorre com a estratégia de Emulação, onde seram literalmente emulados os comandos em containers preparados para a arquitetura desejada. Para esse tipo de estratégia não é preciso alteração no Dockerfile. É maneira mais simples de obter uma imagem com suporte a multi-plataforma, mas com essa facilidade vem alguns problemas. Os binários executados dessa maneira precisam converter constantemente suas instruções entre arquiteturas e, portanto, não são executados com velocidade nativa. Ocasionalmente, você também pode encontrar um caso que desencadeia um bug na camada de emulação.
Mas o problema da estratégia de emulação não vai ocorrer na Cross Compilation, que veremos a seguir.

Corss Compilation

Nessa estratégia vamos ter um pouco mais de trabalho, vamos precisar fazer algumas alterações dentro de um Dockerfile, vou usar um caso bem simples, que será a do Alpine. Para conseguirmos uma imagem do alpine que seja mutliplataforma podemos usar algumas variáveis globais pré definidas como o BUILDPLATAFORM, ela sempre corresponderá à plataforma ou ao seu sistema atual e o construtor vai preencher o valor correto para gente.

FROM --platform=$BUILDPLATFORM alpine
Enter fullscreen mode Exit fullscreen mode

Segue a lista com todas as veriaveis:

  • BUILDPLATFORM — Corresponde a plataforma da máquina atual. (linux/amd64)
  • BUILDOS — Componente de SO da BUILDPLATFORM. (linux)
  • BUILDARCH — Tipo de arquitetura da máquina atual. (amd64, arm64, riscv64)
  • BUILDVARIANT — Usado para definir a variante ARM. (v7)
  • TARGETPLATFORM — O valor definido com o flag --platform na compilação
  • TARGETOS - SO definido na flag --platform. (linux)
  • TARGETARCH - Tipo da arquitetura definida na --platform. (arm64)
  • TARGETVARIANT

Vamos ver um exemplo bem simples de uma imagem com a estratégia de cross compilation.

FROM --platform=$BUILDPLATFORM alpine AS build
RUN apk add mycompiler
COPY src src
ARG TARGETPLATFORM
RUN compile -target=$TARGETPLATFORM -o /out/mybinary /src

FROM alpine
RUN apk add helperapp
COPY --from=build /out/mybinary /bin
Enter fullscreen mode Exit fullscreen mode

No Dockerfile acima conseguimos ver como vai funcionar nossa imagem. É so uma imagem de exemplo, mas é possível entender que a imagem terá que ser desenvolvida para se adaptar e gerar seus binários de acordo com a arquitetura escolhida. Para cada linguagem de programação, por exemplo, existe uma maneira de gerar os pacotes ou binários, para a lingauem Go funciona da seguinte maneira, segue o exemplo:

FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS build
WORKDIR /src
COPY . .
ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app .

FROM alpine
COPY --from=build /out/app /bin
Enter fullscreen mode Exit fullscreen mode

E é isso pessoal, espero que esse artigo tenha ajudado vocês de alguma maneira.

Top comments (0)