Um guia para Pipy, um proxy de rede programável para nuvem
Pipy é um processador de tráfego de rede de código aberto, extremamente rápido e leve. Ele tem uma variedade de casos de uso, incluindo roteadores de borda, balanceamento de carga e proxy, gateways de API, servidores HTTP estáticos, sidecars de malha de serviço e muitos outros aplicativos.
Pipy é um processador de fluxo de rede de código aberto, nativo da nuvem. É modular por design e pode criar um proxy de rede de alto desempenho. Ele foi escrito em C++ e construído sobre a biblioteca de E/S assíncrona Asio. Pipy é ideal para uma variedade de casos de uso, desde roteadores de borda, balanceadores de carga, soluções de proxy, gateways de API, servidores HTTP estáticos, sidecars de malha de serviço e muito mais.
Pipy também vem com suporte JavaScript integrado por meio de PipyJS. PipyJS é altamente personalizável e previsível em desempenho, sem sobrecarga de coleta de lixo. Atualmente, PipyJS faz parte da base de código Pipy, mas não depende dele e no futuro poderá ser movido para um pacote independente.
Guia de início rápido do Pipy
Você pode executar a versão de produção do Pipy usando Podman ou Docker com um dos scripts do tutorial fornecidos no repositório oficial do Pipy Git. A imagem do contêiner Pipy pode ser configurada com algumas variáveis de ambiente:
PIPY_CONFIG_FILE=
define a localização do arquivo de configuração do Pipy.PIPY_SPAWN=n
define o número de instâncias do Pipy que você deseja iniciar, onden
é o número de instâncias. Este é um índice baseado em zero, portanto 0 representa 1 instância. Por exemplo, usePIPY_SPAWN=3
para 4 instâncias.
Inicie o servidor Pipy com este script de exemplo:
$ docker run --rm -e PIPY_CONFIG_FILE=\
https://raw.githubusercontent.com/flomesh-io/pipy/main/tutorial/01-hello \
-e PIPY_SPAWN=1 -p 8080:8080 flomesh/pipy-pjs:latest
Você pode notar que em vez de um arquivo local, este código fornece um link para um script Pipy remoto através da variável de ambiente PIPY_CONFIG_FILE
. Pipy é inteligente o suficiente para lidar com isso.
Para sua referência, aqui está o conteúdo do arquivo tutorial/01-hello/hello.js
:
pipy()
.listen(8080)
.serveHTTP(
new Message('Hi, there!\n')
)
Este script simples define um pipeline de porta, que escuta na porta 8080 e retorna "Olá!" para cada solicitação HTTP recebida na porta de escuta.
Como você expôs a porta local 8080 com o comando docker run, você pode prosseguir com um teste na mesma porta:
$ curl http://localhost:8080
A execução do comando acima exibe Olá!
no console.
Para fins de aprendizagem, desenvolvimento ou depuração, é recomendado prosseguir com a instalação local (compilar o Pipy a partir de fontes ou baixar uma versão para o seu sistema operacional) do Pipy, pois ele vem com um console web de administração junto com documentação e tutoriais.
Uma vez instalado localmente, executar pipy
sem nenhum argumento inicia o console de administração na porta 6060, mas pode ser configurado para escutar em uma porta diferente com a opção --admin-port
.
(Ali Naqvi, CC BY-SA 40)
Para construir o Pipy a partir do código-fonte ou para instalar um binário pré-compilado para o seu sistema operacional, consulte README.md no repositório Pipy Git.
Executando Pipy em um terminal
Para iniciar um proxy Pipy, execute o Pipy com um arquivo de script PipyJS, por exemplo, o script em tutorial/01-hello/hello.js
se você precisar de um servidor de eco simples que responda com o mesmo corpo da mensagem recebido com cada solicitação recebida:
$ pipy tutorial/01-hello/hello.js
Alternativamente, durante o desenvolvimento e depuração, pode-se iniciar o Pipy com uma UI web integrada:
$ pipy tutorial/01-hello/hello.js --admin-port=6060
Para ver todas as opções de linha de comando, use o sinalizador --help
:
$ pipy --help
Pipy é um processador de stream
Pipy opera em fluxos de rede usando um pipeline orientado a eventos onde consome o fluxo de entrada, executa transformações fornecidas pelo usuário e gera o fluxo. Um fluxo de dados pipy pega dados brutos e os abstrai em um evento. Um evento pode pertencer a uma das quatro categorias:
- Dados: os fluxos de rede são compostos de bytes de dados e vêm em pedaços. Pipy abstrai pedaços em um evento Data.
MessageStart, MessageEnd, StreamEnd: esses três eventos que não são de dados funcionam como marcadores, fornecendo aos fluxos de bytes brutos uma semântica de alto nível na qual a lógica de negócios pode confiar.
Design Pipy
O funcionamento interno do Pipy é semelhante ao Unix Pipelines, mas diferentemente dos pipelines Unix, que lidam com bytes discretos, o Pipy lida com fluxos de eventos.
Pipy processa fluxos de entrada por meio de uma cadeia de filtros, onde cada filtro lida com questões gerais, como registro de solicitações, autenticação, descarregamento de SSL, encaminhamento de solicitações e assim por diante. Cada filtro lê sua entrada e grava em sua saída, com a saída de um filtro conectada à entrada do próximo.
Gasodutos
Uma cadeia de filtros é chamada de pipeline e o Pipy categoriza os pipelines em 3 categorias diferentes de acordo com suas fontes de entrada.
Pipeline de porta: lê eventos de dados de uma porta de rede, processa-os e depois grava o resultado de volta na mesma porta. Este é o modelo de solicitação e resposta mais comumente usado. Por exemplo, quando Pipy funciona como um servidor HTTP, a entrada para um pipeline Port é uma solicitação HTTP dos clientes, e a saída do pipeline seria uma resposta HTTP enviada de volta aos clientes.
-
Pipeline de timer: obtém um par de eventos MessageStart e MessageEnd como entrada periodicamente. Útil quando a funcionalidade semelhante ao Cron job é necessária.
Subpipeline: funciona em conjunto com um filtro de junção, como link, que recebe eventos de seu pipeline predecessor, alimenta-os em um subpipeline para processamento, lê de volta a saída do subpipeline pipeline e, em seguida, bombeia-o para o próximo filtro.
A melhor maneira de observar subpipelines e juntar filtros é pensar neles como receptores e chamadores de uma sub-rotina na programação processual. A entrada para o filtro conjunto são os parâmetros da sub-rotina, a saída do filtro conjunto é o seu valor de retorno.
Um pipeline raiz, como Port ou Timer, não pode ser chamado a partir de filtros de junção.
Para obter uma lista de filtros integrados e seus parâmetros:
$ pipy --list-filters$ pipy --help-filters
Contexto
Outra noção importante no Pipy é a de contextos. Um contexto é um conjunto de variáveis anexadas a um pipeline. Cada pipeline obtém acesso ao mesmo conjunto de variáveis em uma instância Pipy. Em outras palavras, os contextos têm a mesma forma. Ao iniciar uma instância do Pipy, a primeira coisa a fazer é definir a forma do contexto definindo variáveis e seus valores iniciais.
Cada pipeline raiz clona o contexto inicial que você define no início. Quando um subpipeline é iniciado, ele compartilha ou clona o contexto de seu pai, dependendo de qual filtro conjunto você usa. Por exemplo, um filtro de link compartilha o contexto de seu pai enquanto um filtro demux o clona.
Para os scripts incorporados em um pipeline, essas variáveis de contexto são suas variáveis globais, o que significa que essas variáveis estão sempre acessíveis aos scripts de qualquer lugar se residirem no mesmo arquivo de script.
Isto pode parecer estranho para um programador experiente porque variáveis globais geralmente significam que são globalmente únicas. Você tem apenas um conjunto dessas variáveis, enquanto no Pipy podemos ter muitos conjuntos delas (contextos), dependendo de quantos pipelines raiz estão abertos para conexões de rede de entrada e quantos subpipelines clonam os contextos de seus pais.
Escrevendo um proxy de rede
Suponha que você esteja executando instâncias separadas de serviços diferentes e queira adicionar um proxy para encaminhar o tráfego para os serviços relevantes com base no caminho do URL da solicitação. Isso lhe daria o benefício de expor um único URL e dimensionar seus serviços no back-end, sem que os usuários precisassem se lembrar de um URL de serviço distinto. Em situações normais, seus serviços seriam executados em nós diferentes e cada serviço poderia ter várias instâncias em execução. Neste exemplo, suponha que você esteja executando os serviços abaixo e queira distribuir o tráfego para eles com base no URI.
serviço-oi em
/hi/*
(127.0.0.1:8080, 127.0.0.1:8082)eco de serviço em
/echo
(127.0.0.1:8081)service-tell-ip em
/ip_/_*
(127.0.0.1:8082)
Os scripts Pipy são escritos em JavaScript e você pode usar qualquer editor de texto de sua escolha para editá-los. Alternativamente, se você instalou o Pipy localmente, você pode usar o Pipy admin Web UI, que vem com realce de sintaxe, preenchimento automático, dicas, bem como a capacidade de executar scripts, tudo no mesmo console.
Inicie uma instância do Pipy, sem nenhum argumento, para que o console de administração do Pipy seja iniciado na porta 6060. Agora abra seu navegador favorito e navegue até [http://localhost:6060](http://localhost:6060/ para ver o compilado -na UI da Web de administração do Pipy.
(Ali Naqvi, CC BY-SA 40)
Crie um programa Pipy
Uma boa prática de design é que o código e as configurações sejam separados. Pipy oferece suporte a esse design modular por meio de seus plug-ins, que você pode chamar de módulos JavaScript. Dito isto, você armazena seus dados de configuração na pasta config e sua lógica de codificação em arquivos separados na pasta plugins. O script do servidor proxy principal é armazenado na pasta raiz, o script do proxy principal (proxy.js
) incluirá e combinará a funcionalidade definida em módulos separados. No final, sua estrutura de pastas final é:
├── config
│ ├── balancer.json
│ ├── proxy.json
│ └── router.json
├── plugins
│ ├── balancer.js
│ ├── default.js
│ └── router.js
└── proxy.js
1.Clique em Nova base de código, digite /proxy
para o nome da base de código na caixa de diálogo e clique em Criar.
Clique no botão + para adicionar um novo arquivo. Digite
/config/proxy.json
como nome de arquivo e clique em Criar. Este é o arquivo de configuração usado para configurar seu proxy.Agora você vê
proxy.json
listado na pasta de configuração no painel esquerdo. Clique no arquivo para abri-lo e adicione a configuração mostrada abaixo e certifique-se de salvar seu arquivo clicando no ícone do disco no painel superior.
{"listen": 8000,"plugins": ["plugins/router.js","plugins/balancer.js","plugins/default.js" ]}
Repita as etapas 2 e 3 para criar outro arquivo, /config/router.json, para armazenar informações de rota. Insira estes dados de configuração:
{"routes": {"/hi/*": "service-hi","/echo": "service-echo","/ip/*": "service-tell-ip" }}
Repita as etapas 2 e 3 para criar outro arquivo, /config/balancer.json, para armazenar seu mapa de serviço para destino. Insira os seguintes dados:
{"services": {"service-hi" : ["127.0.0.1:8080", "127.0.0.1:8082"],"service-echo" : ["127.0.0.1:8081"],"service-tell-ip" : ["127.0.0.1:8082"] }}
Agora é hora de escrever seu primeiro script Pipy, que será usado como substituto padrão quando seu servidor receber uma solicitação para a qual você não tem nenhum destino (um endpoint) configurado. Crie o arquivo
/plugins/default.js
. O nome aqui é apenas uma convenção e Pipy não depende de nomes, então você pode escolher o nome que quiser. O script conterá o código mostrado abaixo, que retorna o código de status HTTP 404 com uma mensagem Nenhum manipulador encontrado:
pipy().pipeline('request').replaceMessage(new Message({ status: 404 }, 'No handler found'))
7.Crie o arquivo /plugins/router.js
, que armazena sua lógica de roteamento:
(config =>
pipy({
_router: new algo.URLRouter(config.routes), })
.export('router', {
__serviceID: '', })
.pipeline('request')
.handleMessageStart(
msg => (
__serviceID = _router.find(
msg.head.headers.host,
msg.head.path, )
) )
)(JSON.decode(pipy.load('config/router.json')))
Crie o arquivo
/plugins/balancer.js
, que armazena sua lógica de balanceamento de carga como uma observação lateral. Pipy vem com vários algoritmos de balanceamento de carga, mas para simplificar, você está usando o algoritmo Round Robin aqui.(config =>pipy({ _services: ( Object.fromEntries( Object.entries(config.services).map( ([k, v]) => [ k, new algo.RoundRobinLoadBalancer(v) ] ) ) ), _balancer: null, _balancerCache: null, _target: '',}).import({ __turnDown: 'proxy', __serviceID: 'router',}).pipeline('session') .handleStreamStart( () => ( _balancerCache = new algo.Cache( // k is a balancer, v is a target (k ) => k.select(), (k,v) => k.deselect(v), ) ) ) .handleStreamEnd( () => ( _balancerCache.clear() ) ).pipeline('request') .handleMessageStart( () => ( _balancer = _services[__serviceID], _balancer && (_target = _balancerCache.get(_balancer)), _target && (__turnDown = true) ) ) .link( 'forward', () => Boolean(_target), '' ).pipeline('forward') .muxHTTP( 'connection', () => _target ).pipeline('connection') .connect( () => _target ))(JSON.decode(pipy.load('config/balancer.json')))
Agora escreva o ponto de entrada, ou o script do servidor proxy, para usar os plug-ins acima. A criação de uma nova base de código (etapa 1) cria um arquivo
main.js
padrão como ponto de entrada. Você pode usá-lo como ponto de entrada principal ou, se preferir usar um nome diferente, sinta-se à vontade para excluirmain.js
e criar um novo arquivo com o nome de sua escolha. Para este exemplo, exclua-o e crie um novo arquivo chamado/proxy.js
. Certifique-se de clicar no ícone da bandeira superior para torná-lo o ponto de entrada principal, para garantir que a execução do script seja iniciada quando você clicar no botão Executar (o ícone de seta à direita).(config =>pipy().export('proxy', { __turnDown: false,}).listen(config.listen) .use(config.plugins, 'session') .demuxHTTP('request').pipeline('request') .use( config.plugins, 'request', 'response', () => __turnDown ))(JSON.decode(pipy.load('config/proxy.json')))
Até agora, seu espaço de trabalho está assim:
(Ali Naqvi, CC BY-SA 40)
Para executar seu script, clique no botão do ícone de reprodução (quarto da direita). Pipy executa seu script de proxy e você vê uma saída semelhante a esta:
(Ali Naqvi, CC BY-SA 40)
Isso mostra que seu servidor proxy está escutando na porta 8000 (que você configurou em /config/proxy.json
). Use curl para executar um teste:
$ curl -i [http://localhost:8000](http://localhost:8000)
HTTP/1.1 404 Not Found
content-length: 10
connection: keep-alive
No handler found
Essa resposta faz sentido porque você não configurou nenhum destino para root. Experimente uma das rotas configuradas, como /hi
:
$ curl -i [http://localhost:8000/hi](http://localhost:8000/hi)
HTTP/1.1 502 Connection Refused
content-length: 0
connection: keep-alive
Você recebe 502 Connection Refused
porque não tem nenhum serviço em execução na porta de destino configurada.
Você pode atualizar /config/balancer.json
com detalhes como o host e a porta dos seus serviços já em execução para adequá-lo ao seu caso de uso, ou você pode simplesmente escrever um script no Pipy para ouvir no seu portas configuradas e retornam mensagens simples.
Salve este código em um arquivo em seu computador local chamado mock-proxy.js
e lembre-se do local onde você o armazenou:
pipy()
.listen(8080)
.serveHTTP(
new Message('Hi, there!\n')
)
.listen(8081)
.serveHTTP(
msg => new Message(msg.body)
)
.listen(8082)
.serveHTTP(
msg => new Message(
`You are requesting ${msg.head.path} from ${__inbound.remoteAddress}\n`
)
)
Abra uma nova janela de terminal e execute este script com Pipy (altere /path/to
para o local onde você armazenou este arquivo de script):
$ pipy /path/to/mock-proxy.js
2022-01-11 18:56:31 [INF] [config]
2022-01-11 18:56:31 [INF] [config] Module /mock-proxy.js
2022-01-11 18:56:31 [INF] [config] ================
2022-01-11 18:56:31 [INF] [config]
2022-01-11 18:56:31 [INF] [config] [Listen on :::8080]
2022-01-11 18:56:31 [INF] [config] ----->|
2022-01-11 18:56:31 [INF] [config] |
2022-01-11 18:56:31 [INF] [config] serveHTTP
2022-01-11 18:56:31 [INF] [config] |
2022-01-11 18:56:31 [INF] [config] <-----|
2022-01-11 18:56:31 [INF] [config]
2022-01-11 18:56:31 [INF] [config] [Listen on :::8081]
2022-01-11 18:56:31 [INF] [config] ----->|
2022-01-11 18:56:31 [INF] [config] |
2022-01-11 18:56:31 [INF] [config] serveHTTP
2022-01-11 18:56:31 [INF] [config] |
2022-01-11 18:56:31 [INF] [config] <-----|
2022-01-11 18:56:31 [INF] [config]
2022-01-11 18:56:31 [INF] [config] [Listen on :::8082]
2022-01-11 18:56:31 [INF] [config] ----->|
2022-01-11 18:56:31 [INF] [config] |
2022-01-11 18:56:31 [INF] [config] serveHTTP
2022-01-11 18:56:31 [INF] [config] |
2022-01-11 18:56:31 [INF] [config] <-----|
2022-01-11 18:56:31 [INF] [config]
2022-01-11 18:56:31 [INF] [listener] Listening on port 8080 at ::
2022-01-11 18:56:31 [INF] [listener] Listening on port 8081 at ::
2022-01-11 18:56:31 [INF] [listener] Listening on port 8082 at ::
Agora você tem seus serviços simulados escutando nas portas 8080, 8081 e 8082. Faça um teste novamente em seu servidor proxy para ver a resposta correta retornada de seu serviço simulado.
Resumo
Você usou vários recursos do Pipy, incluindo declaração de variáveis, importação e exportação de variáveis, plug-ins, pipelines, subpipelines, encadeamento de filtros, filtros Pipy como handleMessageStart
, handleStreamStart
, e link e classes Pipy como JSON, algo.URLRouter
, algo.RoundRobinLoadBalancer
, algo.Cache
e outros. Para obter mais informações, leia a excelente documentação do Pipy e, por meio da interface de administração da web do Pipy, e siga os tutoriais passo a passo que o acompanham.
Conclusão
Pipy da Flomesh é um processador de tráfego de rede de código aberto, extremamente rápido e leve. Você pode usá-lo em uma variedade de casos de uso, desde roteadores de borda, balanceamento de carga e proxy (direto e reverso), gateways de API, servidores HTTP estáticos, sidecars de malha de serviço e muitos outros aplicativos. Pipy está em desenvolvimento ativo e é mantido por committers e colaboradores em tempo integral.