Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 27, 2021 06:33 pm GMT

Webservers, Proxies, Load Balancers e Api Gateways.

Se voc esta lendo esse post tem 99.90% de chance de voc estar lendo ele atravs da internet, o outro 0.10% de chance voc provavelmente assinante do Internet Impressa, um servio aonde voc semanalmente recebe todas novidades da internet impressa por correio. Voc tambm pode assinar a internet em video que te manda fitas VHS dos melhores videos da internet, ou se voc quiser uma fita com um AMV do Rock Lee contra o Gaara ao som de Linkin Park. Acho que eles tambm tem o Internet em video XXX que voc j sabe o que .

Mas no estamos aqui para falar sobre esse grandioso servio, na verdade esse post o contrario, aqui ns vamos falar sobre como podemos servir nossas aplicaes atravs da internet.

Voc pode achar o repositrio com o exemplo em: https://github.com/andre2w/webserver-post-examples

Webservers

Comeando pelo mais bsico ns temos que servir arquivos estticos. Voc vai l escreve todo o HTML, CSS e JavaScript para um site, igual se fazia antigamente, quando a era tudo mato. Agora voc precisa um jeito de servir esses arquivos.

Assim como a Internet Impressa uma grande parte da internet costumava ser arquivos estticos que no mudavam programaticamente. Algum tinha que ir l e escrever o .html, o .css e o .js e esses arquivos eram enviados para o servidor e assim que voc entrasse no site esses arquivos eram enviados para o seu browser do jeito que eles foram escritos.
Sem transpilador, sem polyfill, sem nenhuma alterao.

Mas agora precisamos servir esses arquivos para quem acessa o seu site, e no s isso preciso diferenciar qual pagina voc est acessando atravs do caminho na URL. Ns podemos fazer isso utilizando um Webserver.

Geralmente o Webservers serve os arquivos convertendo o caminho da url em uma estrutura de pastas. Ento se voc tentar acessar:

<http://localhost:3030/blog/posts/webserver.html>

Um Webserver configurado para ter a pasta pages como a raiz vai procurar no seguinte caminho:

pages+- blog+-- posts+--- webserver.html

Web servers tambm serve para servir imagens e qualquer outro arquivo esttico atravs de maneira eficiente. Ento vamos criar um webserver bsico em TypeScript s por motivos que podemos.

Comeamos os arquivos que vamos servir, vamos comear com um index.html e um 404.html dentro de uma pasta chamada pages.

<!-- pages/index.html --><!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head><body>    <h1>Hello, World!</h1></body></html>
<!-- pages/404.html --><!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head><body>    Pagina no encontrada!</body></html>

Agora vamos criar o nosso Webserver para servir essas duas paginas. Em src/index.ts ns criamos.

import http from "http";import path from "path";import fs from "fs";const requestListener = (req: http.IncomingMessage, res: http.ServerResponse) => {    if (req.url === "/") {        const index = fs.readFileSync(path.join(__dirname, "..", "pages", "index.html"));        res.writeHead(200); // Retorna o status code 200 - OK        res.end(index);     // Envia o conteudo do arquivo index.html como resposta     } else {        const notFound = fs.readFileSync(path.join(__dirname, "..", "pages", "404.html"));        res.writeHead(404); // Retorna o status code 404 - Not Found        res.end(notFound);    }};const server = http.createServer(requestListener);server.listen(3030);

Agora se rodarmos essa aplicao e acessarmos [localhost:3030](http://localhost:3030) voc vai ver o contedo de index.html e se for qualquer outra pagina voc vera o contedo de 404.html. Agora precisamos mudar isso para servirmos outras paginas baseadas em nosso diretrio pages.

import http from "http";import path from "path";import fs from "fs";/** * Esse metodo tenta ler um arquivo baseado na url passada * se a url for /blog/posts/webserver.html ele vai tentar ler em um arquivo * em ./pages/blog/posts/webserver.html caso no encontre ele vai retornar * o conteudo de 404.html   * @param filePath  * @returns { status: number; content: Buffer } */const serveFile = (filePath?: string) => {    if (filePath === undefined || filePath === "/") {        const index = fs.readFileSync(path.join(__dirname, "..", "pages", "index.html"));        return { status: 200, content: index };    }    try {        const parsedPath = filePath.split("/");        const content = fs.readFileSync(path.join(__dirname, "..", "pages", ...parsedPath));        return { status: 200, content };    } catch {        const notFound = fs.readFileSync(path.join(__dirname, "..", "pages", "404.html"));        return { status: 404, content: notFound };    }}const requestListener = (req: http.IncomingMessage, res: http.ServerResponse) => {    const result = serveFile(req.url);    res.writeHead(result.status);    res.end(result.content);};const server = http.createServer(requestListener);server.listen(3030);

Agora qualquer estrutura de pastas que for criada sob o diretrio pages pode ser servido como uma url.

cgi-bin e PHP

Provavelmente vai ter uma pessoa que comeou a ler a parte sobre arquivos estticos e comeou a pensar:

E PHP? Ele usa o Apache que um web server mas ele no tem paginas estticas.

No caso do PHP ele modifica o webserver (no sei se j vem incluso hoje em dia) para chamar uma aplicao que vai gerar o html que ele vai servir. O webserver ainda est servido paginas estticas mas a cada request uma pagina nova gerada.

Com cgi-bin a mesma coisa, voc chama um script que gera a pagina baseada nos parmetros que voc passou para o script.

Se fossemos adicionar algo que emulasse um cgi-bin em nosso webserver precisaramos fazer o seguinte:

  • Criar uma pasta para os scripts. Vamos chamar a pasta de scripts.
  • Depois adicionar o primeiro script em JavaScript que imprima um html.
// scripts/cgi-bin-test.jsconsole.log(`<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head><body>    <h1>cgi-bin example</h1></body></html>`);
  • Agora adicionamos o cdigo para chamar o script no nosso webserver
import http from "http";import path from "path";import fs from "fs";import { execSync } from "child_process";interface Response {    status: number;    content: Buffer;}const notFound = () => {    const notFound = fs.readFileSync(path.join(__dirname, "..", "pages", "404.html"));    return { status: 404, content: notFound }; }/** * Executa um arquivo .js dentro do folder scripts e retorna o stdout * como o conteudo da pagina. *  * Executa usando node.exe porque estou usando o windows e o execFileSync funciona  * atravs do cmd e no powershell  * @param scriptPath string * @returns Response */const cgiBin = (scriptPath: string) => {    const result = execSync(`node.exe ${path.join(__dirname, "..", "scripts", scriptPath)}`);    return { status: 200, content: result }}/** * Mapeia uma url para a execuo de um script com o metodo cgiBin */const routeMap: Record<string, () => Response> = {    "/cgi-bin": () => { return cgiBin("cgi-bin-test.js") }}/** * Tenta ler um arquivo dentro do diretorio pages  * @param filePath string * @returns Response */const loadPage = (filePath: string) => {    const parsedPath = filePath.split("/");    const content = fs.readFileSync(path.join(__dirname, "..", "pages", ...parsedPath));    return { status: 200, content };}/** * Esse metodo tenta ler um arquivo baseado na url passada * se a url for /blog/posts/webserver.html ele vai tentar ler em um arquivo * em ./pages/blog/posts/webserver.html caso no encontre ele vai retornar * o conteudo de 404.html   * @param filePath  * @returns { status: number; content: Buffer } */const serveFile: (filepath?: string) => Response = (filePath) => {    if (filePath === undefined || filePath === "/") {                return loadPage("index.html");    }    try {        if (routeMap[filePath] !== undefined) {            return routeMap[filePath]();        } else {            return loadPage(filePath);        }    } catch {        return notFound();    }}const requestListener = (req: http.IncomingMessage, res: http.ServerResponse) => {    const result = serveFile(req.url);    res.writeHead(result.status);    res.end(result.content);};const server = http.createServer(requestListener);server.listen(3030);

Reverse Proxy

Agora como que conseguimos usar um webserver com outas linguagens que no se integram com o webserver, tipo Java ou C#? Voc no quer ficar chamando um .jar ou .dll toda hora, e essas linguagens possuem

Nesse caso podemos utilizar um Reverse Proxy. O trabalho do proxy receber os requests e repassar eles para a aplicao correta. Esse tipo de proxy muito utilizado quando voc tem varias aplicaes no mesmo servidor. Voc pode redirecionar baseado em informaes como:

  • headers
  • url
  • domnio
  • cookies

Outra vantagem do proxy reverso que voc reduz o numero de portas expostas em seu servidor j que nesse caso voc pode ter mltiplas aplicaes acessveis atravs da porta 80 e 443

+------------------------------------------+| Server A                 +-:3031-+       ||                    +-----| APP 1 |       ||     +-:80---+      |     +-------+       ||     | Proxy |------+                     ||     +-:443--+      |     +-:3032-+       ||                    +-----| APP 2 |       ||                          +-------+       |+------------------------------------------+

Como pode se ver no "diagrama" acima, temos um servidor rodando duas aplicaes expondo as portas 80 e 443 que so para HTTP e HTTPS e conectando em duas aplicaes. Nesse caso as portas 3031 e 3032 no precisam ser exposta.

E se quisermos fazer um reverse proxy para ir com o nosso webserver?

  • A primeira coisa que temos que fazer requests para o nosso webserver, ento vamos implementar um cliente http
// src/http-client.tsimport http from "http";interface GetResultProps {    status: number;    body: string;}interface GetProps {    path: string;    port: number;    callback: (result: GetResultProps) => void;}export function get({ path, port, callback }: GetProps): void {    let body = "";    http.get({ path, port }, (res) => {        res.on('data', chunk => {            body += chunk;        });        res.on('close', () => callback({ status: res.statusCode!, body }) );    });}
  • Agora implementamos o proxy como um webserver, mas ao invs de ler arquivos ele vai fazer um request para o webserver.
import http from "http";import { get } from "./http-client";/** * Cria um mapeamento de uma rota para uma porta. * Nesse caso esperamos que todas as rotas sero mapeadas localmente */const portMap: Record<string, number> = {    "/blog": 3030}const requestListener = (req: http.IncomingMessage, res: http.ServerResponse) => {    /**     * Busca no mapeamento se a rota atual esta mapeada para alguma porta.      * Ele verifica o inicio da rota, ento uma rota como:     *                    /blog/posts/webserver.ts      * ser mapeada para a porta 3030 como foi configurado em portMap.      */    const destination = Object.entries(portMap).find(entry => req.url?.startsWith(entry[0]));    if (destination) {        get({            path: req.url!,            port: destination[1],            callback: result => {                res.writeHead(result.status);                res.end(result.body);            }        })    } else {        res.writeHead(404);        res.end("Rota no configurada");    }};const server = http.createServer(requestListener);server.listen(3031);

Refactor Time

Eu fiz algumas alteraes no cdigo para ficar mais fcil de usa-lo. Algumas das coisas que mudei:

  • Adicionei classes para o Webserver e LoadBalancer para ficar mais simples de configurar outras instancias.
  • Adicionei a opo de passar a porta do webserver como argumento quando rodando pelo yarn webserver <porta>.

Load Balancer

Talvez voc no tenha varias aplicaes rodando em uma maquina, muito pelo contrario, um misero servidor no o suficiente para a sua aplicao, voc criou a prxima rede social de fake news e o sucesso est cobrando o seu preo.

Como ter varias maquinas rodando uma aplicao tudo atravs do mesmo endpoint? Voc j sabe que um proxy reverso poderia fazer isso, mas tem um problema. O proxy reverso direciona baseado em algum valor do request logo ele no funcionaria corretamente.
ai que entra os Load Balancers, eles funcionam como um proxy reverso mas ao invs de utilizar as informaes do request para definir para qual servidor ele vai redirecionar ele usa outro tipo de informao para redirecionar para um servidor contendo a aplicao.

Ento vamos comear escrevendo o Load Balancer sem nenhuma estratgia de balanceamento.

import http, { IncomingMessage } from "http";import { get, GetResultProps } from "../http-client";interface Backend {    host?: string;    port: number;}interface Strategy {    getServer: () => Backend;    onConnectionClose(backend: Backend): void;}class LoadBalancer {    constructor(private readonly port: number, private readonly strategy: Strategy) {}    start() {        const requestListener = (req: http.IncomingMessage, res: http.ServerResponse) => {            const backend = this.strategy.getServer();            const callback = (result: GetResultProps) => {                res.writeHead(result.status);                res.end(result.body);                  this.strategy.onConnectionClose(backend);            };            console.log(`Enviando request para ${JSON.stringify(backend)}`);            this.makeRequest(backend, req, callback);        }        console.log(`Iniciando Load Balancer na porta ${this.port}`);        const server = http.createServer(requestListener);        server.listen(this.port);    }    private makeRequest(backend: Backend, originalRequest: IncomingMessage, callback: (result: GetResultProps) => void) {        get({            headers: originalRequest.headers,            path: originalRequest.url!,            port: backend.port,            host: backend.host,            callback         });    }}

A partir desse cdigo podemos comear a implementar nossas estratgias.

Estratgias de Balanceamento

Para balancear a carga entre os servidores, os Load Balancers vem com varias estratgias de balanceamento, Aqui temos algumas das mais usadas.

Round Robin

Qual seria a coisa mais fcil a se fazer para distribuir os requests entre as maquinas? Que tal voc mandar um request para cada maquina sequencialmente? Round Robin basicamente isso, o Load Balancer manda um request para cada servidor listado no Load Balancer em forma sequencial.

Para implementar o balanceamento com Round Robin bem simples:

class RoundRobin implements Strategy {    private index = -1;     constructor(private readonly backends: Backend[]) {}    getServer(): Backend {        this.index++;        if (this.index >= this.backends.length) {            this.index = 0;        }        return this.backends[this.index];    }    }// Agora iniciamos o LoadBalancer com ele// Na porta 3031 e 3032 est rodando o webserver.const roundRobin = new RoundRobin([    { port: 3031 }, { port: 3032 }]);new LoadBalancer(3030, roundRobin).start();

Least Connections

Voc deve estar pensando que Round Robin um algoritmo bem simplista. E sim, se as conexes em um servidor ficarem ativas por muito tempo em um servidor e o Load Balancer continuar enviando conexes o seu servidor vai acabar sobrecarregado e vai cair. Ento uma maneira de resolver esse problema configurar o Load Balancer para enviar os requests para o servidor que tem menos conexes ativas. O Load Balancer sabe qual o servidor porque ele mantem a conexo aberta at receber uma resposta.

A implementao da estratgia do Least Connections seria assim:

class LeastConnections implements Strategy {    /**     * Cria um mapa com o numero de conexoes em cada backend     */    private readonly connections: Map<Backend, number>;    constructor(backends: Backend[]) {        if (backends.length === 0) {            throw new Error("No  possivel criar uma estratgia sem backends")        }       this.connections = new Map();       backends.forEach(backend => this.connections.set(backend, 0));    }    getServer(): Backend {        console.log(`Escolhendo uma conexao`);        const backend = this.pickLeastConnectedBackend();        console.log(`Backend utilizado ${JSON.stringify(backend)}`);        this.connections.set(backend, this.connections.get(backend)! + 1);        return backend;    }    /**     * Seleciona o Backend com o menor numero de conexes comparando todos os backends     * No  demorado porque o numero de Backends  pequeno      * @returns Backend     */    private pickLeastConnectedBackend(): Backend{        let selectedBackend: [Backend, number] | undefined;        for (const entry of this.connections.entries()) {            if (selectedBackend === undefined) {                selectedBackend = entry;            }            if (entry[1] < selectedBackend[1]) {                selectedBackend = entry;            }        }        const backend = selectedBackend![0];        return backend;    }    /**     * Diminui o numero de conexes no backend       * @param backend Backend     */    onConnectionClose(backend: Backend) {        this.connections.set(backend, this.connections.get(backend)! - 1);    }}const leastConnections = new LeastConnections([    { port: 3031 }, { port: 3032 }]);new LoadBalancer(3030, leastConnections).start();

Agora para testar isso precisamos de uma maneira em que um dos webservers fique esperando, podemos fazer isso criando um novo script pro cgi-bin que vai esperar 10 segundos antes de responder.

// scripts/sleep.jsfunction sleep(milliseconds) { return new Promise(resolve => setTimeout(resolve, milliseconds));}const template = `<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head><body>    <h1>Sleep Example</h1></body></html>`;sleep(10000).then(() => console.log(template));

E adicionamos o script no nosso webserver

const webserver = new Webserver({    port,    rootFolder: path.join(__dirname, "..", "..", "pages"),    cgiBinRootFolder: path.join(__dirname, "..", "..", "scripts"),    cgiBinMapping: {        "/cgi-bin": "cgi-bin-test.js",        "/sleep": "sleep.js"    }});webserver.start();

Agora se voc for em seu navegador e em uma aba acessar [localhost:3030/sleep](http://localhost:3030/sleep) e em outra aba acessar [localhost:3030/blog/webserver.html](http://localhost:3030/blog/webserver.html) e ficar atualizando a pagina voc ver que todos os requests para /blog/webserver.html esto indo para o servidor na porta 3032 pois a porta 3031 j tem um request.

Weighted Round Robin

Nem sempre os servidores que voc vai distribuir as aplicaes so iguais, voc pode ter um servidor bom top da NASA e um Athlon 64 com 2 Gb de ram. Voc sabe qual dos servidores deve receber mais requests. Nesse caso voc pode usar uma estratgia chamada Weighted, que voc pode atribuir um peso para o servidor e o Load Balancer vai dar preferencia ao servidores mais pesados.

Cada Load Balancer pode ter um algoritmo ou maneira diferente de atribuir o peso, mas para fins de entendermos como esse algoritmo funcionaria vamos pensar que temos 3 servidores. Um servidor maior e outros dois menores, os dois menores possuem a mesma configurao e voc quer que o servidor maior receba metade dos requests.
Metade dos requests seria 50% (espero no estar errado), os outros 50% queremos dividir entre as duas maquinas, o que seria 25%.

Logo configuraramos nosso Load Balancer assim:

import { Strategy, Backend } from "./strategy"export type WeightedBackend = Backend & {    weight: number}new WeightedRoundRobin([  { port: 3031, weight: 20 },  { port: 3032, weight: 80 }]);

Agora como o nosso Load Balancer pode enviar os requests considerando o peso de cada maquina? Bem nossa configurao soma um total de 100%. Ento vamos remover a porcentagem para facilitar e s utilizar o 100. Voc pode criar um array com 100 valores e distribuir os servidores nesse array baseado no peso. O resultado seria algo assim:

    0         1      2..48     49       50       51...73    74     75      76...98    99[server1, server1, ..., ..., server1, server2, ..., ..., server2, server3, ..., ..., server3 ]

Agora cada request recebido pelo Load Balancer um servidor selecionado sequencialmente no array e quando chega no fim s voltar para o inicio do array e continuar assim enquanto receber algum request. claro que esse algoritmo bem ingnuo e tem muito espao para otimizao, mas no isso que queremos aqui. O objetivo ver como a porta 3032 toma a maior parte do array e ira receber metade dos requests como esperado.

Para nossa implementao ns vamos fazer algo que precisa de menos espao na memria e que seja um pouco mais tranquilo de configurar. Essa implementao permite voc passar qualquer valor acima de 0 para o weight:

import { Strategy, Backend } from "./strategy"export type WeightedBackend = Backend & {    weight: number}export class WeightedRoundRobin implements Strategy {    private index = -1;     private readonly backends: WeightedBackend[];    private readonly totalWeight: number;    /**     * Transforma a propriedade weight do backend em um valor sequencial somando com valor anterior      * E calcula o peso total para utilizarmos como valor maximo do indice     * @param backends WeightedBackend[]     */     constructor(backends: WeightedBackend[]) {       this.backends = backends.map((backend, index) => {           if (backend.weight <= 0) {               throw new Error(`Weight tem que ser maior que 0, valor atual: ${backend.weight}`)           }           if (index === 0) {               return backend           } else {               return {                   ...backend,                   weight: backend.weight + backends[index-1].weight               };           }       });       this.totalWeight = backends.reduce((acc, backend) => acc + backend.weight, 0)    }    getServer(): Backend {        this.index++;        if (this.index > this.totalWeight) {            this.index = 0;        }        /**         * Seleciona o backend em que o indice esteja dentro do weight          */        return this.backends.reduce((acc, backend) => {            if (this.index > acc.weight && this.index <= backend.weight) {                return backend;            } else {                return acc;            }        }, { port: 0, weight: -1 });    }        onConnectionClose(backend: Backend) {}}

Agora basta voc fazer 30 requests para o Load Balancer e ver para qual servidor ele vai.

Resource Based:

Para encerrarmos temos o Resource Based, que vai checar a utilizao de recursos de cada servidor, isso pode ser feito atravs de uma aplicao instalada no servidor aonde o Load Balancer fica observando os valores ou at mesmo por endpoints em http que so disponibilizados por quem opera o servidor.

Eu no vou implementar esse porque seria muito trabalho. Mas acredito que agora voc deve ter uma ideia de como deve funcionar.

No s isso

claro, h muitos outros tipos de algoritmos s que esses so os principais. Se voc quiser saber mais sobre isso a Wikipedia em ingls tem uma lista de algoritmos. https://en.wikipedia.org/wiki/Load_balancing_(computing)

Norvana

A verdade que quando estamos trabalhando com uma aplicao web ns precisamos combinar todas as funcionalidades que ns vimos para termos sistemas que so escalveis e seguros. A maior parte dos softwares que fazem isso eles todas as funes juntas. Ento ns temos que fazer o mesmo com o nosso cdigo, precisamos unir todas as tribos assim como o Norvana fez.

import http, { IncomingMessage } from "http";import path from "path";import fs from "fs";import { exec } from "child_process";import { WebserverProps, Response } from "./webserver/webserver";import { MappedRoute, RouteMapper } from "./reverse-proxy/reverse-proxy";import { Backend, Strategy } from "./load-balancer/strategy";import { get, GetResultProps } from "./http-client";import { RoundRobin } from "./load-balancer/round-robin";import { argv, exit } from "process";import { LeastConnections } from "./load-balancer/least-connections";import { WeightedRoundRobin } from "./load-balancer/weighted-round-robin";type NorvanaProps = WebserverProps & {    reverseProxy: Array<MappedRoute & {        strategy: Strategy    }>};class Norvana {    private readonly routeMapper: RouteMapper<NorvanaProps['reverseProxy'][0]>;    private readonly webserverProps: WebserverProps;    private readonly port: number;    constructor(norvanaProps: NorvanaProps) {        this.routeMapper = new RouteMapper<NorvanaProps['reverseProxy'][0]>(norvanaProps.reverseProxy);        this.webserverProps = norvanaProps;        this.port = norvanaProps.port;    }    start() {        const requestListener = (req: http.IncomingMessage, res: http.ServerResponse) => {            /**             * Faz o trabalho do proxy reverso e pega a rota necessaria             */            const destination = this.routeMapper.routeFor(req.url!);            if (destination === undefined) {                /**                 * Serve arquivos estaticos assim como um webserver                 */                this.serveFile(response => {                    res.writeHead(response.status, { 'Content-Type': 'text/html' });                    res.end(response.content);                }, req.url!);                return;            }            /**             * Faz o trabalho do load balancer e define o servidor             */            const backend = destination.strategy.getServer();            const callback = (result: GetResultProps) => {                res.writeHead(result.status, { 'Content-Type': 'text/html' });                res.end(result.body);                destination.strategy.onConnectionClose(backend);            };            /**             * Faz o request para o servidor na rota correta e balanceado             */            this.makeRequest(backend, req, callback);        }        const server = http.createServer(requestListener);        server.listen(this.port);    }    private makeRequest(backend: Backend, originalRequest: IncomingMessage, callback: (result: GetResultProps) => void) {        get({            headers: originalRequest.headers,            path: originalRequest.url!,            port: backend.port,            host: backend.host,            callback         });    }    /**     * Esse metodo tenta ler um arquivo baseado na url passada     * se a url for /blog/posts/webserver.html ele vai tentar ler em um arquivo     * em ./pages/blog/posts/webserver.html caso no encontre ele vai retornar     * o conteudo de 404.html       * @param filePath      * @returns { status: number; content: Buffer }     */    private serveFile(callback: (response: Response) => void, filePath: string): void {        if (filePath === undefined || filePath === "/") {            this.loadPage("index.html", callback);        } else if (this.webserverProps.cgiBinMapping[filePath!] !== undefined) {            this.cgiBin(this.webserverProps.cgiBinMapping[filePath], callback);        } else {            this.loadPage(filePath, callback);        }    }    /**     * Tenta ler um arquivo dentro do diretorio pages      * @param filePath string     * @returns Response     */    private loadPage(filePath: string, callback: (response: Response) => void) {        const parsedPath = filePath.split("/");        fs.readFile(path.join(this.webserverProps.rootFolder, ...parsedPath), (err, data) => {            if (err) {                callback({ status: 500, content: Buffer.from("Alguma coisa errada no est correta") });            } else {                callback({ status: 200, content: data});            }        });    }    private cgiBin(scriptPath: string, callback: (response: Response) => void) {        exec(`node.exe ${path.join(this.webserverProps.cgiBinRootFolder, scriptPath)}`, (err, stdout) => {            if (err) {                callback({ status: 500, content: Buffer.from(err.message) });                return;            }            callback({ status: 200, content: Buffer.from(stdout) })        });    }}const port = Number.isInteger(parseInt(argv[2])) ? parseInt(argv[2]) : 3030;const strategies: Record<string, Strategy> = {    "round-robin": new RoundRobin([{ port: 3031 }, { port: 3032 }]),    "least-connections": new LeastConnections([{ port: 3031 }, { port: 3032 }]),    "weighted-round-robin": new WeightedRoundRobin([{ port: 3031, weight: 3 }, { port: 3032, weight: 22 }])}const strategyName = argv[3];const strategy = strategies[strategyName];if (strategy === undefined) {    console.log(`Estrategia ${strategyName}  invalida, Por favor escolha uma das estrategias: ${Object.keys(strategies).join(", ")}`);    exit(1);}new Norvana({    port,    rootFolder: path.join(__dirname, "..", "pages"),    cgiBinRootFolder: path.join(__dirname, "..", "scripts"),    cgiBinMapping: {        "/cgi-bin": "cgi-bin-test.js",        "/sleep": "sleep.js"    },    reverseProxy: [        { matcher: /^\/blog/, port: 3031, strategy }    ]}).start();

Voc tambm pode ver a verso refatorada no repositrio. Agora para testar essa monstruosidade voc pode seguir os passos:

  • Abra trs terminais
  • Inicie um webserver na porta 3031 em um dos terminais
  • Inicia um webserver na porta 3032 em outro terminal
  • E no terminal livre voc pode iniciar o Norvana

Agora todos os requests para /blog/webserver.html ira pelo reverse proxy e load balancer, voc pode notar isso atravs dos logs.

Agora os requests para /cgi-bin e /sleep sero servidos como um webserver pelo o norvana mesmo.

API Gateway

Por fim ns temos API Gateways, que pode ser considerado o pice de entre (eu juro que isso um termo real, tipo pice de rsistance, mas nesse caso a pea de entrada) no mundo da plataformizao e microservios. Quando voc comea a criar muitos servios coisas que antes eram mais triviais de se lidar comeam a ficar mais complexo. Ento algo que era implementado uma vez em um monlito tem que ser implementado para cada servio, coisas como autenticao, autorizao e limites de uso tem que ser implementado mltiplas vezes.

Uma ideia que pode vir a cabea implementar uma biblioteca que faa isso, s que bibliotcas no so to simples assim, voc ainda tem que escrever o cdigo de integrao e tambm voc tem que manter a biblioteca em todas as aplicaes, imagine se voc tem mais de 1500 servios assim como o Monzo na Inglaterra. Uma mudana na API da biblioteca pode causar grandes problemas de migrao.

Ento para resolver isso esse tipo de cdigo movido para outro lugar, nos exemplos que dei todos so coisas que queremos fazer antes de processar qualquer request, logo podemos mover isso para a entrada da plataforma. E o que temos na entrada da plataforma nesse caso? Bem provavelmente um proxy reverso, um web server ou balanceador de carga. Mas no caso deles ns nos preocupamos apenas em servir os requests, a unica lgica involvida para saber qual servidor deve receber o request.

ai que entra o API Gateway, ele serve para dar uma camada extra de lgica aonde podemos agrupar operaes comuns que no esto relacionadas ao negcio em um nico ponto. Agora todos os requests que chegam aos servios j esto devidamente validados.

Sndrome de No Inventado Aqui

A internet um lugar perigoso, todas as ruas da internet tem dois caras numa moto esperando para voc dar um vacilo. Ns implementamos algumas coisas aqui, voc pode olhar e pensar "Isso no to difcil" esse tipo de pensamento chamado de Sndrome de No Inventado Aqui aonde as pessoas acham que elas tem que desenvolver todo software que eles vo rodar.

Isso um problema srio, qualquer software de infra-estrutura pode parecer simple mas na hora de implementar os detalhes e fazer o software seguro as coisas ficam muito difceis. Ento se um dia voc se ver implementando qualquer uma das coisas que vemos aqui para colocar em produo e voc no trabalhar para uma empresa de infra-estrutura, pare e procure ajuda.


Original Link: https://dev.to/andre2w/webservers-proxies-load-balancers-e-api-gateways-1374

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To