前端网关踩坑实践

2021年11月22日 阅读数:9
这篇文章主要向大家介绍前端网关踩坑实践,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

项目背景

在后端微服务中,常见的一般会经过暴露一个统一的网关入口给外界,从而使得整个系统服务有一个统一的入口和出口,收敛服务;然而,在前端这种统一提供网关出入口的服务比较少见,经常是各个应用独立提供出去服务,目前业界也有采用微前端应用来进行应用的调度和通讯,其中nginx作转发即是其中的一种方案,这里为了收敛前端应用的出入口,项目须要在内网去作相关的部署,公网端口有限,于是为了更好接入更多的应用,这里借鉴了后端的网关的思路,实现了一个前端网关代理转发方案,本文旨在对本次前端网关实践过程当中的一些思考和踩坑进行概括和总结,也但愿能给有相关场景应用的同窗提供一些解决方面的思路javascript

架构设计

名称 做用 备注
网关层 用来承载前端流量,做为统一入口 可使用前端路由或后端路由来承载,主要做用是流量切分,也能够将单一应用布置于此处,做为路由与调度的混合
应用层 用来部署各个前端应用,不限于框架,各个应用之间通讯能够经过http或者向网关派发,前提是网关层有接收调度的功能存在 不限于前端框架及版本,每一个应用已经单独部署完成,相互之间通讯须要经过http之间的通讯,也能够借助k8s等容器化部署之间的通讯
接口层 用来从后端获取数据,因为后端部署的不一样形式,可能有不一样的微服务网关,也有可能有单独的第三方接口,也多是node.js等BFF接口形式 对于统一共用的接口形式可将其上承至网关层进行代理转发

方案选择

目前项目应用系统业务逻辑较为复杂,不太便于统一落载在以类SingleSPA形式的微前端形式,于是选择了以nginx为主要技术形态的微前端网关切分的形式进行构建,另外后续须要接入多个第三方的应用,作成iframe形式又会涉及网络打通之间的问题。因为业务形态,公网端口有限,须要设计出一套可以1:n的虚拟端口的形态出来,于是这里最终选择了以nginx做为主网关转发来作流量及应用切分的方案。css

层级 方案 备注
网关层 使用一个nginx做为公网流量入口,利用路径对不一样子应用进行切分 父nginx应用做为前端应用入口,须要做一个负载均衡处理,这里利用k8s的负载均衡来作,配置3个副本,若是某一个pod挂掉,能够利用k8的机制进行拉起
应用层 多个不一样的nginx应用,这里因为作了路径的切分,于是须要对资源定向作一个处理,具体详见下一部分踩坑案例 这里利用docker挂载目录进行处理
接口层 多个不一样的nginx应用对接口作了反向代理后,接口因为是浏览器正向发送,于是这里没法进行转发,这里须要对前端代码作一个处理,具体详见踩坑案例 后续会配置ci、cd构建脚手架以及一些配置一些常见前端脚手架如:vue-cli、cra、umi的接入插件包

踩坑案例

静态资源404错误

[案例描述] 咱们发如今代理完路径后正常的html资源是能够定位到的,可是对于js、css资源等会出现找不到的404错误html

[案例分析] 因为目前应用多为单页应用,而单页应用的主要都是由js去操做dom的,对于mv*框架而言一般又会在前端路由及对一些数据进行拦截操做,于是在对应模板引擎处理过程当中须要对资源查找进行相对路径查找前端

[解决方案] 咱们项目构建主要是经过docker+k8s进行部署的,于是这里咱们想到将资源路径统一放在一个路径目录下,而这个目录路径须要和父nginx应用转发路径的名称相一致,也就是说子应用须要在父应用中须要注册一个路由信息,后续就能够经过服务注册方式进行定位变动等vue

父应用nginx配置java

{
    "rj": {
        "name": "xxx应用",
        "path: "/rj/"
    }
}
server {
    location /rj/ {
        proxy_pass http://ip:port/rj/;
    }
}

子应用node

FROM xxx/nginx:1.20.1
COPY ./dist /usr/share/nginx/html/rj/

接口代理404错误

[案例描述] 在处理完静态资源以后,咱们父应用中请求接口,发现接口竟然也出现了404的查询错误ios

[案例分析] 因为目前都是先后端分离的项目,于是后端接口一般也是经过子应用的nginx进行方向代理实现的,这样经过父应用的nginx转发过来后因为父应用的nginx中没有代理接口地址,于是会出现没有资源的状况nginx

[解决方案] 有两种解决方案,一种是经过父应用去代理后端的接口地址来进行,这样的话会出现一个问题就是子应用代理的名称若是相同,而且接口并不仅是来自一个微服务,或者会有不一样的静态代理以及BFF形式,那样对父应用的构建就会出现复杂度不可控的情形;另外一种则是经过改变子应用中的前端请求路径为约定好的一种路径,好比加上约定好的服务注册中的路径进行隔离。这里咱们兼而有之,对于咱们自研项目的接入,会在复应用中进行统一的网关及静态资源转发代理等配置,与子应用约定好路径名,好比后端网关统一以/api/进行转发,对于非自研项目的接入,咱们目前须要接入应用进行接口的魔改,后续咱们会提供一个插件库进行常见脚手架的api魔改方案,好比vue-cli/cra/umi等,对于第三方团队自研的脚手架构建应用须要自行手动更改,但通常来讲自定义脚手架团队一般会有一个统一配置前端请求的路径,对于老应用如以jq等构建的项目,则须要各自手动更改web

这里我以vue-cli3构建的方案进行一个示范:

// config
export const config = {
    data_url: '/rj/api'
};
// 具体接口
// 一般这里会作一些axios的路由拦截处理等
import request from '@/xxx';
// 这里对baseUrl作了统一入口,只需更改这里的baseurl入口便可
import { config } from '@/config';

// 具体接口
export const xxx = (params) => 
    request({
        url: config.data_url + '/xxx'
    })

源码浅析

nginx做为一个轻量的高性能web服务器,其架构及设计是极具借鉴意义的,对node.js或其余web框架的设计具备必定的指导思路

nginx是用C语言书写的,于是其将整个架构经过模块进行组合,其中包含了常见的诸如:HTTP模块、事件模块、配置模块以及核心模块等,经过核心模块来调度和加载其它模块,从而实现了模块之间的相互做用

这里咱们主要是须要经过location中的proxy_pass对应用进行转发,于是,咱们来看一下proxy模块中对proxy_pass的处理

ngx_http_proxy_module

static char *ngx_http_proxy_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

static ngx_command_t ngx_http_proxy_commands[] = {
    {
        ngx_string("proxy_pass"),
        NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_TAKE1,
        ngx_http_proxy_pass,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    }
};


static char *
ngx_http_proxy_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_proxy_loc_conf_t *plcf = conf;

    size_t                      add;
    u_short                     port;
    ngx_str_t                  *value, *url;
    ngx_url_t                   u;
    ngx_uint_t                  n;
    ngx_http_core_loc_conf_t   *clcf;
    ngx_http_script_compile_t   sc;

    if (plcf->upstream.upstream || plcf->proxy_lengths) {
        return "is duplicate";
    }

    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);

    clcf->handler = ngx_http_proxy_handler;

    if (clcf->name.len && clcf->name.data[clcf->name.len - 1] == '/') {
        clcf->auto_redirect = 1;
    }

    value = cf->args->elts;

    url = &value[1];

    n = ngx_http_script_variables_count(url);

    if (n) {

        ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

        sc.cf = cf;
        sc.source = url;
        sc.lengths = &plcf->proxy_lengths;
        sc.values = &plcf->proxy_values;
        sc.variables = n;
        sc.complete_lengths = 1;
        sc.complete_values = 1;

        if (ngx_http_script_compile(&sc) != NGX_OK) {
            return NGX_CONF_ERROR;
        }

#if (NGX_HTTP_SSL)
        plcf->ssl = 1;
#endif

        return NGX_CONF_OK;
    }

    if (ngx_strncasecmp(url->data, (u_char *) "http://", 7) == 0) {
        add = 7;
        port = 80;

    } else if (ngx_strncasecmp(url->data, (u_char *) "https://", 8) == 0) {

#if (NGX_HTTP_SSL)
        plcf->ssl = 1;

        add = 8;
        port = 443;
#else
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "https protocol requires SSL support");
        return NGX_CONF_ERROR;
#endif

    } else {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid URL prefix");
        return NGX_CONF_ERROR;
    }

    ngx_memzero(&u, sizeof(ngx_url_t));

    u.url.len = url->len - add;
    u.url.data = url->data + add;
    u.default_port = port;
    u.uri_part = 1;
    u.no_resolve = 1;

    plcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0);
    if (plcf->upstream.upstream == NULL) {
        return NGX_CONF_ERROR;
    }

    plcf->vars.schema.len = add;
    plcf->vars.schema.data = url->data;
    plcf->vars.key_start = plcf->vars.schema;

    ngx_http_proxy_set_vars(&u, &plcf->vars);

    plcf->location = clcf->name;

    if (clcf->named
#if (NGX_PCRE)
        || clcf->regex
#endif
        || clcf->noname)
    {
        if (plcf->vars.uri.len) {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                               "\"proxy_pass\" cannot have URI part in "
                               "location given by regular expression, "
                               "or inside named location, "
                               "or inside \"if\" statement, "
                               "or inside \"limit_except\" block");
            return NGX_CONF_ERROR;
        }

        plcf->location.len = 0;
    }

    plcf->url = *url;

    return NGX_CONF_OK;
}

ngx_http

static ngx_int_t
ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
    ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)
{
    ngx_uint_t             i, default_server, proxy_protocol;
    ngx_http_conf_addr_t  *addr;
#if (NGX_HTTP_SSL)
    ngx_uint_t             ssl;
#endif
#if (NGX_HTTP_V2)
    ngx_uint_t             http2;
#endif

    /*
     * we cannot compare whole sockaddr struct's as kernel
     * may fill some fields in inherited sockaddr struct's
     */

    addr = port->addrs.elts;

    for (i = 0; i < port->addrs.nelts; i++) {

        if (ngx_cmp_sockaddr(lsopt->sockaddr, lsopt->socklen,
                             addr[i].opt.sockaddr,
                             addr[i].opt.socklen, 0)
            != NGX_OK)
        {
            continue;
        }

        /* the address is already in the address list */

        if (ngx_http_add_server(cf, cscf, &addr[i]) != NGX_OK) {
            return NGX_ERROR;
        }

        /* preserve default_server bit during listen options overwriting */
        default_server = addr[i].opt.default_server;

        proxy_protocol = lsopt->proxy_protocol || addr[i].opt.proxy_protocol;

#if (NGX_HTTP_SSL)
        ssl = lsopt->ssl || addr[i].opt.ssl;
#endif
#if (NGX_HTTP_V2)
        http2 = lsopt->http2 || addr[i].opt.http2;
#endif

        if (lsopt->set) {

            if (addr[i].opt.set) {
                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                   "duplicate listen options for %V",
                                   &addr[i].opt.addr_text);
                return NGX_ERROR;
            }

            addr[i].opt = *lsopt;
        }

        /* check the duplicate "default" server for this address:port */

        if (lsopt->default_server) {

            if (default_server) {
                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                   "a duplicate default server for %V",
                                   &addr[i].opt.addr_text);
                return NGX_ERROR;
            }

            default_server = 1;
            addr[i].default_server = cscf;
        }

        addr[i].opt.default_server = default_server;
        addr[i].opt.proxy_protocol = proxy_protocol;
#if (NGX_HTTP_SSL)
        addr[i].opt.ssl = ssl;
#endif
#if (NGX_HTTP_V2)
        addr[i].opt.http2 = http2;
#endif

        return NGX_OK;
    }

    /* add the address to the addresses list that bound to this port */

    return ngx_http_add_address(cf, cscf, port, lsopt);
}

static ngx_int_t
ngx_http_add_addrs(ngx_conf_t *cf, ngx_http_port_t *hport,
    ngx_http_conf_addr_t *addr)
{
    ngx_uint_t                 i;
    ngx_http_in_addr_t        *addrs;
    struct sockaddr_in        *sin;
    ngx_http_virtual_names_t  *vn;

    hport->addrs = ngx_pcalloc(cf->pool,
                               hport->naddrs * sizeof(ngx_http_in_addr_t));
    if (hport->addrs == NULL) {
        return NGX_ERROR;
    }

    addrs = hport->addrs;

    for (i = 0; i < hport->naddrs; i++) {

        sin = (struct sockaddr_in *) addr[i].opt.sockaddr;
        addrs[i].addr = sin->sin_addr.s_addr;
        addrs[i].conf.default_server = addr[i].default_server;
#if (NGX_HTTP_SSL)
        addrs[i].conf.ssl = addr[i].opt.ssl;
#endif
#if (NGX_HTTP_V2)
        addrs[i].conf.http2 = addr[i].opt.http2;
#endif
        addrs[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;

        if (addr[i].hash.buckets == NULL
            && (addr[i].wc_head == NULL
                || addr[i].wc_head->hash.buckets == NULL)
            && (addr[i].wc_tail == NULL
                || addr[i].wc_tail->hash.buckets == NULL)
#if (NGX_PCRE)
            && addr[i].nregex == 0
#endif
            )
        {
            continue;
        }

        vn = ngx_palloc(cf->pool, sizeof(ngx_http_virtual_names_t));
        if (vn == NULL) {
            return NGX_ERROR;
        }

        addrs[i].conf.virtual_names = vn;

        vn->names.hash = addr[i].hash;
        vn->names.wc_head = addr[i].wc_head;
        vn->names.wc_tail = addr[i].wc_tail;
#if (NGX_PCRE)
        vn->nregex = addr[i].nregex;
        vn->regex = addr[i].regex;
#endif
    }

    return NGX_OK;
}

总结

对于前端网关而言,不仅能够将网关单独独立出来进行分层,也能够采用类SingleSPA的方案利用前端路由进行网关的处理和应用调起,从而实现实现仍是单页应用的控制,只是单独拆分出了各个子应用,这样作的好处是各个子应用之间能够经过父应用或者总线进行相互间的通讯,以及公共资源的共享和各自私有资源的隔离,对于本项目而言,目前业态更适合使用单独网关层的方式来实现,而使用nginx则能够实现更小的配置来接入各个应用,实现前端入口的收敛,这里后续会为构建ci、cd过程提供脚手架,方便应用开发者接入构建部署,从而实现工程化的效果,对于可以成倍数复制的操做,咱们都应该想到利用工程化的手段来进行解决,而不是一味的投入人工,毕竟机器更擅长处理单一不变的批量且稳定产出的工做,共勉!!!

参考