Apresentação: Encante a sua IA e as suas APIs com a magia do eBPF 🪄
Transcrição
Dan Finneran: Vamos falar um pouco sobre IA e APIs. Vamos falar sobre o eBPF, que é uma tecnologia relativamente de baixo nível. Podemos dar uma vista de olhos a algum código eBPF, se quiserem mesmo, ou podemos basicamente limitar-nos a falar sobre como o eBPF funciona. Terei todo o prazer em aprofundar qualquer tema que faça sentido para vocês. Quem sou eu? Faço parte de uma empresa chamada Isovalent, que agora faz parte da Cisco, onde me dedico principalmente a ajudar a comunidade a compreender tecnologias como o eBPF ou o projeto Cilium. Todas estas tecnologias são de código aberto. O Cilium é uma solução de rede para clusters do Kubernetes, e o eBPF é uma tecnologia que faz parte do kernel do Linux. Se tiveres um telemóvel Android contigo neste momento, estarás a executar e a utilizar o eBPF sem sequer te aperceberes disso. Além disso, no meu tempo livre, criei várias soluções baseadas em tecnologia nativa da nuvem que fazem parte da CNCF, como balanceadores de carga baseados em software para clusters do Kubernetes e ferramentas de provisionamento «bare metal» para clusters do Kubernetes, entre outras coisas semelhantes.
Para contextualizar, esta palestra centra-se principalmente em alguns dos aspetos que estamos a observar tanto no setor nativo da nuvem como no Kubernetes, mas também naquilo que começamos a ver nas plataformas de produção das pessoas, nas suas implementações e coisas do género. Como praticamente todos sabemos nesta altura, as pessoas estão a criar código a um ritmo cada vez maior. Basicamente, estão a dar instruções, a pedir à IA para fazer algo. Recebem um monte de código e, assim, o trabalho está feito. Existe uma desconexão entre compreender e ser responsável por este código. Eu mantenho vários repositórios de código aberto e estamos agora a começar a ver muitos pedidos de pull gerados por IA, o que é ótimo. É código novo e de boa qualidade; talvez estejam a ser adicionadas novas funcionalidades. No fim de contas, quando pergunto às pessoas que enviaram o pedido de integração: «Podem explicar-me o código, como funciona e como é que vamos dar-lhe suporte daqui para a frente?», normalmente deixam de responder.
Nunca mais tenho notícias delas. Isto é um grande problema, porque se, de repente, aceitar 1 000 linhas de código que não escrevi e que elas não compreendem, quem é que vai dar suporte a isso no futuro? Isto está a tornar-se um problema cada vez mais grave. Este código está simplesmente a aparecer em produção. Se alguém tem uma ideia rápida, programa-a na hora ou recorre à IA para gerar esse código, e ele vai diretamente para produção. De repente, há uma dependência desse código na empresa, e ninguém sabe quem o escreveu, ninguém sabe como funciona e ninguém vai dar suporte a ele no futuro. Mais uma vez, essa desconexão crescente. Existem também algumas questões adicionais. O comportamento destes programas de IA. Tem havido imensas discussões recentemente sobre problemas em que as pessoas têm vindo a entregar cada vez mais controlo sobre coisas como os seus calendários, os seus sistemas de ficheiros e os seus portáteis a estes agentes de IA.
Em alguns casos, houve situações em que o sistema executou um comando «terraform destroy» e destruiu o ambiente de produção, apagou pastas, eliminou código-fonte e coisas do género, na sua convicção de que estava, na verdade, a ajudar. Estes programas lançados em produção, como é que os monitorizamos? Como é que os compreendemos? Como é que alteramos o seu comportamento? Então, finalmente, sabemos o que estamos a ver. Sabemos o que precisamos de fazer. Qual é a melhor implementação para fazer este tipo de coisas? Uma das coisas que vou mostrar hoje é, na verdade, uma prova de conceito, porque no âmbito do Kubernetes existe um novo grupo de trabalho dedicado à criação de elementos como gateways de IA, com o objetivo de capturar, compreender e alterar agentes de IA e os programas baseados em IA nos clusters do Kubernetes. Trata-se de um grupo de trabalho em andamento, no qual estão a padronizar como é que isto se apresentaria. Hoje, vou mostrar-vos uma implementação do que estamos efetivamente a discutir nesse grupo de trabalho.
Esboço
O que vamos abordar hoje: a arquitetura típica de aplicações que acedem a pontos finais de IA, ou seja, como é esse código. Uma breve visão geral do eBPF. O eBPF é de baixo nível. Permite-me fazer o que quiser com um kernel Linux, e em breve também com um kernel Windows. Vamos aprofundar o assunto. Não iremos entrar em pormenores excessivos, uma vez que estamos a falar de programação do kernel. Depois, falaremos sobre a criação de uma solução para monitorizar e controlar estas aplicações alimentadas pelo eBPF e, por fim, faremos um resumo. Haverá também uma demonstração.
Aplicações modernas de IA – Plataformas em evolução
Uma breve introdução às plataformas em evolução. Estão a usar o Kubernetes atualmente ou compreendem como funciona? Tem sido uma espécie de jornada para mim, agora que faço parte da Cisco. Quando começo a falar sobre o Kubernetes, vejo expressões de total desconhecimento, o que é um pouco estranho, porque, fazendo parte de uma cultura de startups, da minha perspetiva, parece que toda a gente conhece o Kubernetes. Como chegámos a este ponto? Há muitos anos, houve evoluções intermináveis. Vimos as CPUs a ganharem cada vez mais núcleos à medida que atingíamos os limites máximos em gigahertz, e cada vez mais memória. Voltámos a recorrer a tecnologias mais antigas para particionar estes sistemas de maior dimensão. As máquinas virtuais tornaram-se o recurso de facto para executar as aplicações e os sistemas operativos. Depois, surgiram os problemas em torno das máquinas virtuais, com as quais os programadores tinham dificuldades, tornando-as difíceis de gerir. O tempo de compilação e, posteriormente, o tempo de implementação eram lentos e considerados demasiado pesados.
Por volta de 2014, surgiu o conceito de contentor, criado por uma pessoa chamada Solomon Hykes na Docker. A normalização para executar as aplicações de forma muito mais rápida consiste em contentorizar essas aplicações. Obtivemos os benefícios de velocidade decorrentes disso. Conseguimos uma menor utilização de recursos. Isso também significou — e as pessoas estão a sentir isso mesmo neste momento — que pudemos eliminar grande parte dos custos com licenças de virtualização, ao migrarmos para tecnologias de código aberto, como os contentores Docker. De certa forma, porém, já tínhamos feito isto há muito tempo, tanto com as «jails» do FreeBSD como com as «Solaris Zones». Basicamente, voltámos a reinventar a roda. E aqui estamos nós.
Partindo daí, evoluindo para conseguirmos avançar muito mais rapidamente, existiam inicialmente vários projetos concorrentes para o fazer. Havia um projeto chamado Nomad, da HashiCorp. Havia a própria Docker, que desenvolveu o Docker Swarm. Fui engenheiro no Docker Swarm há muitos anos. Depois, surgiu o Kubernetes, que veio da Google. Todas estas tecnologias e projetos tinham como objetivo facilitar a gestão do ciclo de vida dessas aplicações. Isso traz muitas vantagens. Não só se gere o ciclo de vida desses contentores, como se torna muito mais fácil lidar com o mesmo, desde o momento em que se inicia a aplicação, se dimensiona e, posteriormente, se desativa. Tenho-me encarregado da configuração, do arranque e, depois, da remoção e limpeza de tudo isso. Quem já geriu ambientes de virtualização sabe que é muito fácil que máquinas virtuais órfãs fiquem espalhadas por toda a infraestrutura.
Tentar descobrir a quem pertencem essas máquinas é bastante complicado e muito difícil de gerir na prática. O Kubernetes trouxe consigo muitas vantagens. São todas muito nativas da nuvem. Escalabilidade automática da própria infraestrutura. Escalabilidade automática para cima e para baixo da própria aplicação. Existem muito mais ferramentas nativas da nuvem para monitorizar o estado dessas aplicações, o que facilita a gestão. A implementação no Kubernetes, para quem ainda não o fez, é toda baseada em YAML, normalmente com muito texto. Na prática, tem a sua especificação, que diz que a minha aplicação deve ser assim. Baseia-se nestes requisitos específicos. Envia isso para o Kubernetes, e o Kubernetes encarrega-se de o executar por si. Nos bastidores, existem dois componentes. Tem o plano de controlo e tem os seus nós de trabalho. É nos nós de trabalho que as cargas de trabalho serão efetivamente executadas. Aplica o seu YAML ao plano de controlo e, em seguida, o plano de controlo encarregar-se-á de comunicar com os nós de trabalho que gere, para que estes executem essas cargas de trabalho por si. Aqui podemos ver que criámos um pod e, dentro desse pod, temos um ou mais contentores. Estes seriam o seu servidor web e, talvez, um agente de monitorização que funciona em paralelo com ele e coisas do género. Essa é a arquitetura típica tanto do cluster do Kubernetes como de uma aplicação que está efetivamente a ser executada dentro desse cluster.
Código com IA
Obviamente, estamos agora a começar a ver aplicações baseadas em IA dentro destes contentores a executarem-se em clusters do Kubernetes. Estão a ser lançadas sem mais nem menos, e ninguém compreende o que fazem ou como funcionam. Para compreender como são realmente, precisamos de analisar um pouco a própria API e perceber como são, na verdade, as interações com a IA. Em última análise, quando se comunica com um LLM, seja ele o OpenAI, o Ollama ou qualquer outro semelhante, trata-se apenas de uma API para interagir. Basicamente, são apenas blobs JSON a ir e vir. Normalmente, trata-se de um pedido e de uma resposta; assim, o pedido que se envia para o backend de um LLM é o modelo que se pretende utilizar. Modelos diferentes produzem tipos diferentes de resposta. Alguns dedicam-se predominantemente à geração de imagens ou vídeo. Outros são mais treinados em informações de programação, para que produzam melhores respostas quando se lhes pede para gerar código.
Existem vários tipos de resposta que pode obter; por isso, muitas vezes o texto será basicamente blocos de código, ou poderá pedir-lhe para escrever um resumo de um documento ou de uma imagem. Descreve como quer que fique — um pôr-do-sol ou o que for — e receberá uma imagem em troca, e coisas desse género. Por fim, temos o prompt. O prompt é, efetivamente, o que a sua solicitação realmente é. Como mencionei, «Quero uma imagem de um pôr-do-sol encantador.» Esse seria o prompt. O LLM processaria isso, faria a sua magia e receberia a resposta. A solicitação baseia-se em prompts. Existem vários tipos diferentes de prompts. Normalmente, o prompt do utilizador é aquilo que eu vou pedir. No entanto, também posso dizer: «Quero um prompt de assistente», e esse prompt de assistente diria, por exemplo, «ao estilo do DALL·E». O meu prompt de utilizador seria: «pinta-me uma imagem que se pareça com isto».
O prompt do assistente serve para definir regras básicas sobre o que o LLM deve realmente produzir, para além de tudo isso. Para gerar qualquer resposta com o LLM, são normalmente utilizados tokens. Há tokens na solicitação que são processados e, posteriormente, tokens que também constam na resposta. São, efetivamente, a base sobre a qual se é cobrado quando se interage com pontos de extremidade de IA. Além disso, é possível especificar limites máximos para os tokens. Não são muitas as pessoas que o fazem, e é por isso que começamos a ver algumas despesas exorbitantes que as pessoas estão a incorrer sem se aperceberem. É possível definir um limite máximo para isso. No entanto, quando esse limite é atingido, o LLM basicamente fornece o que conseguiu construir até esse momento. Uma vez atingido esse limite, poderá receber uma resposta imprecisa ou apenas parcialmente completa. Por fim, estas APIs oferecem um nível de raciocínio, normalmente pequeno, grande ou médio.
Esse é outro método para equilibrar a quantidade de tokens gastos com a qualidade da resposta que irá efetivamente receber. Por fim, outras opções que pode adicionar ao seu pedido incluem o envio de imagens ou a inclusão de texto adicional. Tudo isso é processado pelo LLM para ajudar a gerar uma resposta que faça sentido. Ele pode armazenar em cache a conversa anterior que teve com ele, para que possa construir uma visão holística do que pretende alcançar. Por fim, a resposta. Podemos recebê-la na íntegra. Efetivamente, a solicitação é enviada e, em seguida, aguarda-se o tempo que for necessário para que tudo seja processado. Ou pode-se receber a resposta em fluxo contínuo. Normalmente, sistemas como o Claude fornecem uma resposta em fluxo contínuo. Parece mais uma conversa, à medida que se começa a ver a resposta a ser transmitida para a interface do utilizador a partir dessa perspetiva.
Este é um exemplo de código em Python. Trata-se de uma interação muito rápida com a OpenAI. Basicamente, a URL de um ponto de extremidade da OpenAI ou de um ponto de extremidade compatível com a especificação da OpenAI. Depois, basicamente, estamos apenas a enviar uma mensagem simples de chat aqui. A mensagem é: «Digamos que isto é um teste.» A função consiste, na prática, em escrever um prompt que vamos utilizar. Este será um prompt do utilizador, em que, normalmente, sou eu que peço ao LLM para fazer isto. Podemos adicionar aqui mensagens adicionais de diferentes funções. Funções como a do assistente, que especificarão mais detalhes, tais como garantir que a saída esteja em JSON, ou garantir que a saída esteja num idioma específico, como francês ou alemão, ou coisas do género. Estas mensagens adicionais que podemos utilizar para garantir que a resposta seja o mais precisa possível. Por fim, o modelo lá em baixo.
É esse o modelo que vamos realmente utilizar para gerar a resposta. Agora, começamos a ver coisas como prompts fracos. Começamos a ver modelos codificados de forma rígida. Há uma série de problemas com isso que se tornarão evidentes à medida que avançarmos. As respostas são um payload JSON que se recebe de volta. Haverá uma série de metadados adicionais, tais como o tempo que demorou a gerar a resposta e quantos tokens foram efetivamente consumidos para o efeito. Se for transmitida em fluxo contínuo, receber-se-ão mensagens de «em curso». Ou, se a resposta estiver completa e finalizada, receberemos uma mensagem final em que o estado é definido como «concluído». Por fim, a informação final diz respeito ao tipo de ficheiro — seja imagem, áudio ou vídeo —; a resposta será basicamente um ficheiro multimédia codificado em Base64 para que o possa descodificar e apresentar conforme necessário.
Vamos ver como fica quando interage com isto. Um exemplo rápido de utilização de uma interface de chat para interagir com um modelo. Basicamente, basta especificar aqui uma pergunta simples. Por exemplo: «Como é que apago todos os ficheiros no diretório atual?» A pergunta foi feita a um backend LLM, e isso gerou basicamente uma resposta para mim. Podemos ver aqui que me deu «rm -r *». Esse exemplo de código é, normalmente, o que os agentes procuram para, em seguida, compreenderem e utilizarem essa informação. Já houve casos em que um agente, ao trabalhar num sistema de ficheiros Linux, fez esta pergunta e, em seguida, executou imediatamente esse comando, pensando que era a coisa certa a fazer, e apagou o diretório atual em que estava a trabalhar. O que não é nada bom, porque muitas pessoas também executam estas tarefas como «root», por qualquer motivo que seja. Já houve pessoas que apagaram as suas máquinas Linux na totalidade.
Problemas com código descartável
Problemas com este código. Se estiveres a montar uma destas coisas à pressa, a programar por intuição ou o que quer que seja, terás um modelo codificado de forma rígida, o que pode servir no primeiro dia. No segundo dia, pode acontecer que esse modelo não seja realmente o mais adequado para o que pretendes utilizar. Pode ser muito ineficiente em termos de utilização de tokens. Pode ser impreciso em termos das respostas que está a gerar. Basicamente, estarás a ter uma utilização ineficiente do teu backend LLM. Sem restrições quanto à utilização de tokens. Se te encontrares numa situação em que o teu prompt possa ser adulterado de qualquer forma ou maneira. De repente, sem qualquer limite, pode efetivamente sofrer um ataque de negação de serviço. Alguém poderia manipular o seu prompt de uma forma ou de outra, fazer com que gerasse quantidades massivas de saída do LLM e, efetivamente, causar um ataque de negação de serviço à sua aplicação, ou acabar por lhe custar grandes quantias de dinheiro.
Por fim, é importante filtrar os vários prompts que utiliza ou garantir que não existam prompts adicionais do assistente, para assegurar que a saída faça sentido para o caso de utilização em que está efetivamente a tentar realizar este tipo de tarefas. Houve um bom exemplo disto recentemente. A Chipotle, um restaurante nos EUA, tem um chatbot onde pode ir e fazer-lhe algumas perguntas. Alguém percebeu que, se lhe pedir um burrito e para criar uma lista ligada inversa em Python, ele dá-lhe uma resposta sobre o burrito, mas também começa a gerar imenso código. Por ironia do destino, também descobri que o nosso site da Isovalent fazia o mesmo. Já corrigimos isso. Já não é possível gerar código com o nosso chatbot de apoio.
O que é o eBPF?
Sabemos que há muita coisa a acontecer. Como lidamos com estas aplicações que não pertencem a ninguém, mas que agora estão em produção? Como as observamos? Como as manipulamos? Como controlamos o seu comportamento? Para isso, vamos usar o eBPF. O eBPF faz parte do kernel do Linux. É de código aberto. Já faz parte do kernel do Linux há bastante tempo, desde cerca de 2014. Se tiver um telemóvel Android, está a utilizar o eBPF. Se tiver clusters do Kubernetes a executar CNI, como o Calico ou o Cilium, está a utilizar o eBPF sem se aperceber. Não é preciso conhecer o eBPF para utilizar este tipo de ferramentas. Deparo-me frequentemente com situações em que as pessoas começam subitamente a pensar que precisam de conhecer o código do kernel apenas para realizar tarefas simples de rede dentro de um cluster do Kubernetes. Não é esse o caso.
Nós escrevemos o eBPF por si. Você colhe os benefícios. O eBPF surgiu efetivamente da necessidade de termos mais observabilidade sobre o que realmente se passa no interior de um sistema em funcionamento. Já existiam tecnologias chamadas BPF. Nós pegámos nisso, expandimo-lo e agora temos o eBPF, que nos permite fazer muito mais do que o BPF fazia inicialmente. eBPF significava, de facto, «Extended Berkeley Packet Filter». É isso que temos vindo a dizer a toda a gente há já bastante tempo. No entanto, divergiu tanto do BPF original que isso já não se aplica realmente. O eBPF pode ser o que quer que se queira que seja, nesta altura, do ponto de vista da nomenclatura. Em última análise, torna o kernel programável. Posso alterar um sistema em execução para fazer praticamente o que quiser através do poder do eBPF. Podemos alterar dinamicamente esse comportamento do kernel. Por que é que isso é bom? Antes de fazermos isto, introduzir qualquer coisa no kernel do Linux era um pesadelo.
Normalmente, tem de passar por um processo muito laborioso, em que várias pessoas o analisam, o que é positivo. Queremos garantir que muitas pessoas analisam e verificam o código e coisas do género. Pode demorar meses, se não anos, até esse código chegar ao kernel do Linux. Por fim, o Linus tem de o analisar, altura em que provavelmente te vai repreender e, mesmo assim, acabar por não o permitir no kernel do Linux. Mesmo que consigas fazê-lo, vários anos depois, já está no upstream. Quero-o na Red Hat. Depois, tens de esperar que a Red Hat o atualize no seu kernel, altura em que já te terás reformado porque demorou tanto tempo. Já nem te apetece mais. Simplesmente não é um processo viável.
Na verdade, recuando um pouco, existe outra alternativa: os módulos do kernel. No entanto, um módulo do kernel está vinculado apenas ao kernel para o qual foi compilado. Infelizmente, não é muito fácil transferi-lo entre sistemas diferentes. É muito difícil criar um módulo do kernel que possa ser utilizado por várias pessoas. É aqui que entra o eBPF. A ideia por trás do eBPF é que podemos escrever código eBPF que se integre no kernel do Linux e no kernel do Windows, adicionando a funcionalidade que pretendemos e sendo flexível para ser utilizado em vários sistemas e coisas do género. Desde que tenha um kernel relativamente recente, 5.12, ou seja, qualquer versão dos últimos 5 ou 6 anos, o eBPF funcionará imediatamente. Pode escrever o seu código e integrá-lo em qualquer parte do kernel Linux em execução. Acabaram-se as dependências de ter de esperar que ele chegue efetivamente ao kernel principal upstream.
O que acontece é que temos o nosso kernel. Este irá manipular ficheiros, redes, memória e processos. Temos o nosso programa eBPF e podemos, efetivamente, integrá-lo em várias áreas dentro do kernel do Linux. Hoje vamos ver como integrar programas eBPF na pilha de rede para podermos manipular estas chamadas de IA. O código eBPF é de baixo nível. É o código que está a escrever que vai ser ligado ao kernel do Linux. Tem de ser seguro e protegido. Para isso, o eBPF inclui ferramentas que garantem que o seu código não entre num loop infinito e não possa, de forma alguma, causar uma falha no kernel do Linux, porque adicionar coisas a kernels em execução é bastante assustador. Queremos garantir que esse receio não exista realmente quando estivermos a fazer este tipo de coisas. Podemos integrar programas eBPF em muitas áreas diferentes. Kprobes, sondas do kernel, sondas do utilizador ou Uprobes permitem-nos integrar os nossos programas eBPF em qualquer elemento do espaço do utilizador.
Os pacotes de rede são atualmente o principal caso de utilização do eBPF. Além disso, há ainda uma série de outras áreas. Por exemplo, dispositivos de hardware, módulos de segurança do Linux, eventos de desempenho e coisas do género. É possível ligá-lo a imensas áreas diferentes. Como mencionei, várias áreas do sistema de ficheiros, desde o dispositivo de bloco até ao sistema de ficheiros virtual que se situa por cima de tudo isso. Várias áreas diferentes na pilha de rede, bem como em diversas áreas dentro do próprio kernel do Linux. Um enorme controlo sobre o que podemos ou não querer fazer com um sistema Linux em execução.
Construir um Gateway de IA/API
Sabemos qual é o problema. Dispomos agora de uma tecnologia que nos permitirá começar a alterar este tipo de comportamentos, ligando-nos a um sistema em execução. Vamos ver passo a passo como isso se vai passar. Esta é a lista de tudo o que vamos precisar para construir um gateway de IA que vá funcionar no Kubernetes. O primeiro passo é interceptar de forma transparente o tráfego proveniente do nosso programa agente de IA — ou o que quer que seja — com destino ao nosso ponto final LLM. Vamos redirecionar esse tráfego de forma transparente para um proxy no espaço do utilizador, onde poderemos modificá-lo e alterá-lo. Queremos poder observar todos esses pedidos e respostas de IA. Podemos querer modificá-los, alterar o modelo, analisar o prompt, limitar os tokens — tudo o que for necessário para nos proteger dos problemas que mencionei anteriormente.
Podemos querer bloquear essas solicitações, mais uma vez, com base nessas regras. Onde é que vamos ligar os nossos programas eBPF? Isto é importante porque há muitas áreas diferentes onde podemos fazer tudo isto. Quanto mais abaixo na pilha, mais controlo refinado teremos. No entanto, se eu ligar o meu programa eBPF à própria placa de rede — o que é possível fazer —, posso ligá-lo diretamente ao controlador da placa de interface de rede. Vou apenas receber quadros em bruto, o que será muito difícil de manipular e compreender, uma vez que se trata basicamente de um conjunto de pacotes JSON desordenados. Algumas coisas ficam corrompidas e terão de ser reenviadas. Não será apenas esse pacote. Vou ver todo o tráfego a sair e a entrar no equipamento e coisas do género. Para facilitar um pouco a vida, vamos subir um pouco mais na pilha e ligar-nos à área dos sockets.
Efetivamente, sempre que o nosso programa estabelecer uma ligação, tentando ligar-se a outro ponto final IP — que será o servidor web para o qual estamos a fazer a nossa chamada à API —, é aí que o nosso programa eBPF entrará em ação. Ele irá ver tudo o que está realmente a acontecer e permitir-nos-á começar a fazer alterações. É assim que se apresenta. O nosso processo à esquerda seria o nosso agente de IA que vai tentar estabelecer uma ligação à OpenAI. Vai criar um socket no espaço do utilizador. Esse socket passará pelo iptables, se necessário. Passará por uma área chamada controlo de tráfego no kernel do Linux e, por fim, chegará à placa de rede, e o tráfego seguirá para onde precisa de ir. Estas são as diferentes áreas, em termos de como se apresentam na realidade. Existem várias áreas às quais podemos ligar o nosso programa eBPF. A que oferece melhor desempenho seria no controlador da placa de rede, mas isso apenas nos fornecerá quadros em bruto.
É muito difícil para nós transformar esses quadros brutos de volta num fluxo de dados que faça sentido para a aplicação. O controlo de tráfego, mais uma vez, tem um desempenho muito elevado, mas está dentro do kernel e, mais uma vez, trata-se apenas de pacotes em bruto. Ligar o nosso programa eBPF ao socket permite que o kernel trate de todos os dados propriamente ditos, mas nós poderemos simplesmente ver esses fluxos.
É assim que ficaria. Temos o nosso processo em execução. Temos o conceito de um proxy que se situa ao lado dele. Ligamos o nosso programa eBPF ao socket quando um novo socket é criado e, sem que o processo se aperceba, o seu tráfego é então redirecionado para o nosso proxy. Tudo de forma transparente. Também podemos fazer isto garantindo que não são necessárias reinicializações da aplicação. Podemos, na verdade, forçar a ocorrência de uma nova ligação, o que significa que o nosso programa eBPF irá capturar novamente esse tráfego de forma transparente para a própria aplicação. Esse proxy pode agora começar a ver estes pedidos. É assim que o pedido normalmente se apresentaria. Aqui podemos ver que estamos a utilizar um modelo antigo, pelo que talvez queiramos alterar esse modelo em linha. A própria aplicação não precisa de ser alterada. Ela nem se apercebe de que algo mudou. Estamos a fazer isso de forma transparente para a própria aplicação.
Podemos querer fazer alterações na solicitação de entrada também, pelo que podemos querer alterar o prompt para que recebamos piadas sobre girafas em vez de outros animais e coisas do género. Tal como mencionado anteriormente, podemos querer impor limites noutras áreas. Existem várias áreas diferentes na API através das quais podemos começar a exercer controlo sobre esses comportamentos. É assim que isto normalmente se apresentaria do ponto de vista da resposta. O tráfego parte do nosso agente de IA. O eBPF redirecionou-o para o proxy. O proxy analisou-o e pode ter feito alterações com base em algumas das regras que aplicámos. O proxy estabelece então uma ligação em nome do processo original para o fornecedor de IA e, em seguida, recebemos a nossa resposta. A pergunta original: qual é a piada sobre as girafas? Os modelos de linguagem de grande escala (LLMs) de IA são péssimos a contar piadas. As piadas costumam ser bastante más.
A resposta à pergunta «como se chama quando uma girafa engole um avião de brincar?» é «um avião no pescoço». Podemos ver que a resposta nos fornece a quantidade de tokens necessários para a gerar. Podemos então filtrar com base em tudo isso também. Temos a capacidade de compreender tudo o que entra e sai a partir daí. Por fim, a encriptação também. Podemos aplicar eficazmente a encriptação entre aplicações. Se estiver a executar o seu processo LLM internamente no cluster do Kubernetes, normalmente irá verificar que não há encriptação entre os dois. Podemos efetivamente fazer o mesmo, adicionando um proxy no backend e, em seguida, implementando mTLS entre os dois. As aplicações nem se apercebem. A encriptação é quebrada na outra extremidade, e o tráfego é recebido tal como era anteriormente.
Demonstração
Vamos fazer uma demonstração de praticamente tudo aquilo de que falei. Vamos ter bastante ação no terminal a partir de agora. O cluster do Kubernetes está em funcionamento. Não há nenhuma carga de trabalho a ser executada aqui neste momento. No entanto, na parte inferior está o Ollama, que me permite executar LLMs localmente dentro de um cluster do Kubernetes. O que vamos fazer agora é implementar a nossa carga de trabalho muito básica. Temos um pod de IA, que é, na prática, um agente de IA. É muito simples: tudo o que faz é ligar ao nosso LLM e pedir-lhe que conte uma piada sobre Go a cada poucos segundos. Podemos dar uma vista de olhos rápida a isso. Aí está. É muito simples. Podemos ver aqui o modelo que foi utilizado, quantos tokens foram necessários no prompt e quantos tokens havia na resposta.
Isto está codificado de forma rígida, como começamos a perceber. O modelo não é grande coisa, não nos dá piadas muito boas nem coisas do género. O que vamos fazer é modificar de forma transparente o comportamento deste agente de IA codificado de forma rígida. O que temos aqui é, creio eu, chamado de «watcher». Existem muitos «watchers» no Kubernetes, e todos eles monitorizam os eventos que estão realmente a ocorrer dentro do cluster. O nosso «watcher», que criámos como parte desta prova de conceito, vai estar atento às anotações que forem adicionadas a uma carga de trabalho. A primeira anotação que vou adicionar à nossa carga de trabalho é uma chamada «netflush». Já mencionei isto anteriormente: essa anotação significa que, quando o agente for ativado, utilizará o eBPF para restabelecer uma nova ligação de socket. Por fim, mais uma anotação: AI="true". Basicamente, o observador terá detetado que essa anotação foi adicionada ao nosso pod.
Aqui está. Mesmo no topo, agora podemos ver as nossas três anotações. Três, mas eu só adicionei duas. O que realmente aconteceu foi que, quando adicionámos a «AI="true"», o observador detetou isso e adicionou um contentor adicional ao nosso pod. Se olharmos para a parte de baixo aqui, veremos, logo abaixo, que adicionámos um contentor adicional ao nosso pod. Normalmente, isto não é possível. O Kubernetes tem regras muito rígidas sobre o que se pode e não se pode alterar depois de as alterações terem sido aplicadas a uma carga de trabalho. Por exemplo, alguém já ouviu falar do conceito de «sidecar»? Um «sidecar» normalmente requer que algo esteja a monitorizar as cargas de trabalho que estão a ser aplicadas, as capte antes do servidor da API, as modifique, adicione o «sidecar» e, só então, as execute. Assim que o servidor API inicia a carga de trabalho, não é possível adicionar sidecars à mesma. O que fizemos aqui foi adicionar-lhe um contentor efémero, que é uma funcionalidade relativamente nova adicionada à API do Kubernetes, o que nos permite adicionar uma carga de trabalho adicional ao lado da já existente.
Já temos o nosso gateway configurado aqui. Se analisarmos os registos, podemos ver que o nosso gateway já foi efetivamente iniciado e está a realizar algumas tarefas. Podemos ver aqui que o `netflush` está definido como `true`, e ele vai procurar todas as sessões de rede que já existiam. Já encontrou o nosso cliente de IA. Encontrou o seu ID de processo, por isso sabe que deve procurar esse processo específico. Descobriu que já tinha uma ligação entre si próprio e o Ollama, pelo que sabe para onde se estava, de facto, a dirigir. Forçou-o a estabelecer uma ligação totalmente nova, pelo que agora está a comunicar com o proxy, e o proxy está, por sua vez, a comunicar com o Ollama. Estamos agora a controlar totalmente toda a comunicação entre o nosso agente de IA e aquilo com que ele está, de facto, a comunicar. Vamos controlá-lo. Adicionei um mapa de configuração. Vou mostrar-vos rapidamente este mapa de configuração para que saibam do que estou, de facto, a falar.
Esta é uma política que vamos aplicar. Esta política vai fazer duas coisas. Primeiro, vai substituir o modelo, ou seja, vai trocar o antigo modelo Llama por este modelo Gemma 2; e, mais abaixo, em vez de contar piadas sobre o Go, vai apresentar-nos factos sobre o Go. Podemos ver isso rapidamente agora. Aqui está. O modelo é agora o Gemma 2, em vez do Llama, e as respostas são factos, em vez de piadas sobre o Go e coisas do género.
Podemos demonstrar isto um pouco melhor. O mapa de configuração foi alterado e vamos agora ver nas respostas que, em vez de nos contar piadas sobre o Go, vai apresentar-nos piadas sobre cavalos. Aqui vai. Os cavalos têm três tipos de dentes. Informação muito útil. Temos agora a capacidade de, de forma transparente para a aplicação, substituir elementos como o modelo que está a ser utilizado, alterar o comportamento do prompt, omitir palavras-chave e coisas do género. Pode haver situações em que queiramos bloquear determinados comportamentos. Vamos ver alguns factos sobre coelhos. Daqui a um segundo, vamos ver que isto será bloqueado, porque vou mostrar-vos que temos um filtro que, na prática, diz que, se a resposta contiver alguma coisa sobre coelhos, não deve ser permitida a sua apresentação ao próprio utilizador. Aí está. O Kube-gateway diz que não. Agora, efetivamente, bloqueámos de forma transparente qualquer tipo de resposta que contenha palavras-chave que não queremos, de facto, permitir.
Depois, finalmente, do ponto de vista da observabilidade, podemos querer compreender como as coisas se apresentam na perspetiva da API. Da próxima vez que houver um pedido, poderemos ver como tudo isto se apresenta na realidade. Mais uma vez, de forma transparente em relação a tudo o que está realmente a ocorrer aqui dentro. Podemos ver aqui que, anteriormente, encontrou a referência aos coelhos. Agora podemos ver efetivamente a carga JSON que está a ser enviada para o LLM, que depois analisamos e determinamos se algo deve mudar, se devemos bloquear certas coisas e coisas do género. Por fim, podemos agora ver tanto a solicitação como a resposta. Podemos ver que a solicitação original me contava uma piada sobre o Go. Na verdade, alterámos grande parte dela. Podemos agora ver que a resposta nos fala de coelhos ou rãs, e coisas do género. Temos factos sobre rãs ou piadas sobre rãs. De forma transparente, alterámos o comportamento de um agente de IA em execução sem que este se apercebesse de que alguma coisa tinha realmente mudado. Esta é a demonstração.
Programas de IA — Observabilidade e Segurança com eBPF
Outras formas como agora podemos utilizar o eBPF para aplicar segurança e observabilidade adicionais. Tudo o que discutimos hoje, do ponto de vista da observabilidade e da compreensão, centrou-se na rede e na API. Com o eBPF, podemos interagir com uma variedade de áreas diferentes no interior do sistema em execução. Houve uma palestra que abordou em profundidade como muitos destes agentes de IA irão recorrer a ferramentas adicionais para, eventualmente, realizarem de forma eficaz as tarefas que pretendem executar. Com o eBPF, podemos interagir diretamente com o próprio kernel e definir que, se alguém tentar executar o comando «rm», simplesmente não se permita que esse programa seja executado. Se alguém tentar apagar o /etc/passwd ou abrir o /etc/passwd ou algo do género, podemos interagir com a área eBPF do sistema de ficheiros e, efetivamente, dizer: «Este processo não está autorizado a fazer isso.» Como se pode ver, podemos controlar o comportamento da rede para redirecionar o tráfego de forma transparente para outro local, de modo a podermos agir de forma transparente para a própria aplicação.
O eBPF também pode determinar quais as chamadas de sistema que são permitidas; assim, se algo estiver a tentar elevar privilégios — como aconteceu recentemente numa demonstração em que um agente de IA foi solicitado a fazer algo, mas não tinha privilégios para o fazer, e começou efetivamente a mexer no sistema de ficheiros proc para obter acesso de root — Basicamente, ele conseguiu escapar da sandbox. Normalmente, podemos simplesmente dizer que esse comportamento não é permitido, que simplesmente não se pode fazer isso. Não se tem os privilégios para realizar este tipo de ações. Neste momento, as possibilidades são praticamente ilimitadas em termos do que podemos permitir e do que podemos bloquear num sistema em execução.
Resumo
Há muitas coisas a acontecer neste momento. Hoje em dia, é extremamente fácil gerar código de IA que resolva um problema específico que se tenha. É muito rápido e fácil esse código acabar em produção, a ponto de outras coisas dependerem efetivamente dele. Não há uma responsabilidade real. Estas coisas são muitas vezes atiradas por cima da cerca porque as pessoas avançam tão rapidamente para a próxima tarefa que ninguém assume realmente a responsabilidade pelo que criou. Quando estas coisas são frequentemente colocadas em produção, aquela pessoa que as montou e já seguiu em frente… tipo, quem vai assumir a responsabilidade por isso? Quem é que vai atualizá-lo, caso seja necessário, caso surjam problemas de segurança nas bibliotecas das quais depende, no modelo que está a utilizar ou nos prompts de que faz uso? Isso pode tornar-se um problema realmente grave. Como se pode ver, precisamos de um método para controlar claramente o que é permitido nestas partes dos programas de IA e dos agentes de IA.
O que vos mostrei hoje é uma prova de conceito baseada no grupo de trabalho «AI Egress», que faz parte do projeto Kubernetes. Atualmente, muitas empresas diferentes, incluindo a nossa, estão a trabalhar em conjunto para criar uma normalização que estabeleça regras e uma abordagem consensual, com o objetivo de observar, controlar e determinar o comportamento das aplicações baseadas em IA dentro de clusters do Kubernetes. No entanto, o que vos mostrei hoje também é possível fora de um cluster do Kubernetes. Estamos, efetivamente, a redirecionar o tráfego e a analisar as chamadas à API e coisas do género.
Recursos
Se quiserem saber mais, o ebpf.io é um recurso fantástico. Se quiserem aprender a programar em eBPF, têm tudo lá. Na secção de laboratórios do Cilium, temos vários laboratórios gratuitos, podem aceder a essa secção. Acho que existem 45 laboratórios diferentes, mas há um sobre o eBPF. Se quiser aprender a gerir programas eBPF, temos ambientes de laboratório gratuitos aos quais pode aceder simplesmente através de um navegador web, experimentar e aprender mais. Se estiver realmente interessado no eBPF, há um documentário que pode ver. Acho que tem cerca de uma hora de duração. Explica em pormenor como o eBPF surgiu. O eBPF está praticamente em todo o lado nesta altura. Está em todos os telemóveis Android. O Facebook usa-o para gerir a rede de toda a sua frota Meta e coisas do género. Está presente na maioria dos clusters do Kubernetes, seja para observabilidade, redes ou imposição de comportamentos e coisas do género. Há também o livro da Liz Rice, «What Is eBPF?», disponível para download no site.
Perguntas e Respostas
Participante 1: Pode também mostrar o código do eBPF?
Dan Finneran: Sim. Este é o código que está por trás de tudo aquilo de que falei hoje. Posso explicar-vos isto passo a passo, se realmente quiserem. Na prática, neste trecho de código aqui, se repararem na área onde diz «SEC» — que é o cabeçalho da secção — e «connect4», trata-se de um evento de ligação TCP IPv4, que é o que acontece quando se estabelece uma ligação com algo. É aqui que o nosso código será efetivamente executado antes de o kernel fazer qualquer coisa. O código eBPF executa-se sempre antes do kernel, ou do que quer que seja que o venha a processar. Isso permite-nos alterar coisas e impor comportamentos antes que estes possam realmente afetar um sistema em execução. Normalmente, o que isto faz é que, se for do nosso interesse, vamos redirecionar o tráfego. Alteraremos o destino para onde o tráfego se dirige realmente, de modo a desviá-lo no eBPF, enviando-o para o nosso proxy. Tudo isto é código aberto. Existem vários exemplos de como fazer este tipo de coisa, mas sim, este é um exemplo rápido de código eBPF a interagir com eventos de ligação TCP IPv4.
Bryant: Há algum conselho sobre como documentar algumas destas coisas? Sou um programador Java da velha guarda e comecei a dedicar-me à programação orientada a aspetos — algo semelhante, mas ao nível da JVM — e tornou-se um pesadelo depurar algumas coisas, porque tinha esses aspetos a interferir e estava praticamente a injetar funcionalidades de forma arbitrária. Como é que faço a depuração? Como é que percebo o que se está a passar? Estou ciente de que é muito poderoso, mas será que também pode ser um pouco perigoso?
Dan Finneran: Fizemos este exemplo na página web. Quando entrei para a Isovalent, o primeiro exemplo que me deram foi: «Olha para isto, liga isto ao teu portátil e vai imprimir todos estes eventos, e vai devolver “ok”. Agora tenta dizer “return not ok”, o que, de repente, significava que todos os pacotes eram simplesmente descartados.» Basicamente, provoquei uma negação de serviço no meu portátil a partir do exemplo. É incrivelmente fácil fazer isso. Não há depurador. Tudo o que se tem são, efetivamente, eventos bpf_printk, ou seja, um mecanismo de registo do kernel. Na prática, pode escrever registos no kernel, e o kernel irá apresentá-los no «sys tracing debug», no «trace pipe» ou em qualquer outro formato. Essa é a única forma de compreender o que realmente está a acontecer lá dentro. É isso mesmo. Nem toda a gente precisa de escrever código eBPF. A ideia é, na verdade, escrever o código uma vez e expor as partes necessárias para o que se pretende fazer. Como digo, para toda a parte relacionada com redes que se vê em produção, como no Cilium, no Calico e coisas do género, não é preciso saber eBPF. Basta saber que é isso que está a fazer funcionar a sua rede. Está a analisar o tráfego, a redirecioná-lo e a fazer coisas desse tipo.
Bryant: O criador e o utilizador são pessoas bastante diferentes em alguns aspetos. Tu crias o código, sabes tudo sobre ele, mas eu sou um utilizador do outro lado.
Dan Finneran: Sim. Se quiseres mesmo aprender eBPF, ótimo. Acho que cumpri a minha missão. É um pouco como conduzir um carro. Não precisas mesmo de saber como funciona o motor, especialmente nos carros elétricos modernos de hoje em dia, porque isso é uma loucura. Aqui passa-se o mesmo. Usa as funcionalidades baseadas no eBPF. Se quiseres aprender sobre o eBPF, ótimo, mas não precisas de conhecer ao pormenor os fundamentos do que ele faz.
Participante 2: Mencionaste a restrição das chamadas de sistema (syscalls) no final. Pergunto-me: em que ponto é que isso se torna simplesmente uma corrida ao armamento, e tens de o colocar numa sandbox, já que consigo imaginar que possa começar a clonar-se a si próprio, a bifurcar-se.
Dan Finneran: Os «hooks» que aplicamos são, efetivamente, eventos. No nosso código eBPF, teríamos, por exemplo, a abertura de um ficheiro. Criaríamos o nosso código eBPF, que se associaria à chamada de sistema «file open» ou «open». O nosso código seria executado primeiro. Se um agente de IA tentar abrir o ficheiro /etc/passwd, o nosso programa eBPF será a primeira coisa a acontecer antes mesmo de a chamada de sistema do kernel ocorrer efetivamente. Podemos estar sempre um passo à frente de tudo o que está a acontecer no próprio sistema. Vemos essa chamada de sistema e dizemos: «Não, não tens permissão para fazer isso»; devolvemos «false»; na prática, isso é uma paragem total, não há nada que ele possa realmente fazer. Pode continuar a tentar isso, ou tentar uma chamada de sistema diferente, mas seja o que for, esse é realmente o único mecanismo para abrir ficheiros através de chamadas de sistema no kernel do Linux. Vai sempre acionar o nosso evento. Existe a chamada de sistema ret, pelo que podemos, de facto, ligar-nos ao retorno da chamada de sistema. Podemos permitir que o kernel execute essa chamada de sistema e, em seguida, capturar os resultados de tudo isso, tendo código eBPF que também execute alguma ação com base no resultado dessa chamada. Podemos ligar-nos ao processo antes e depois das chamadas de sistema.
Veja mais apresentações com transcrições