共计 8120 个字符,预计需要花费 21 分钟才能阅读完成。
我的项目背景
在后端微服务中,常见的通常会通过裸露一个对立的网关入口给外界,从而使得整个零碎服务有一个对立的入口和进口,收敛服务;然而,在前端这种对立提供网关出入口的服务比拟少见,经常是各个利用独立提供进来服务,目前业界也有采纳微前端利用来进行利用的调度和通信,其中 nginx 做转发便是其中的一种计划,这里为了收敛前端利用的出入口,我的项目须要在内网去做相干的部署,公网端口无限,因此为了更好接入更多的利用,这里借鉴了后端的网关的思路,实现了一个前端网关代理转发计划,本文旨在对本次前端网关实际过程中的一些思考和踩坑进行演绎和总结,也心愿能给有相干场景利用的同学提供一些解决方面的思路
架构设计
名称 | 作用 | 备注 |
---|---|---|
网关层 | 用来承载前端流量,作为对立入口 | 能够应用前端路由或后端路由来承载,次要作用是流量切分,也能够将繁多利用安排于此处,作为路由与调度的混合 |
应用层 | 用来部署各个前端利用,不限于框架,各个利用之间通信能够通过 http 或者向网关派发,前提是网关层有接管调度的性能存在 | 不限于前端框架及版本,每个利用曾经独自部署实现,相互之间通信须要通过 http 之间的通信,也能够借助 k8s 等容器化部署之间的通信 |
接口层 | 用来从后端获取数据,因为后端部署的不同模式,可能有不同的微服务网关,也有可能有独自的第三方接口,也可能是 node.js 等 BFF 接口模式 | 对于对立共用的接口模式可将其上承至网关层进行代理转发 |
计划抉择
目前我的项目利用零碎业务逻辑较为简单,不太便于对立落载在以类 SingleSPA 模式的微前端模式,因此抉择了以 nginx 为次要技术状态的微前端网关切分的模式进行构建,另外后续须要接入多个第三方的利用,做成 iframe 模式又会波及网络买通之间的问题。因为业务状态,公网端口无限,须要设计出一套可能 1:n 的虚构端口的状态进去,因此这里最终抉择了以 nginx 作为主网关转发来做流量及利用切分的计划。
层级 | 计划 | 备注 |
---|---|---|
网关层 | 应用一个 nginx 作为公网流量入口,利用门路对不同子利用进行切分 | 父 nginx 利用作为前端利用入口,须要作一个负载平衡解决,这里利用 k8s 的负载平衡来做,配置 3 个正本,如果某一个 pod 挂掉,能够利用 k8 的机制进行拉起 |
应用层 | 多个不同的 nginx 利用,这里因为做了门路的切分,因此须要对资源定向做一个解决,具体详见下一部分踩坑案例 | 这里利用 docker 挂载目录进行解决 |
接口层 | 多个不同的 nginx 利用对接口做了反向代理后,接口因为是浏览器正向发送,因此这里无奈进行转发,这里须要对前端代码做一个解决,具体详见踩坑案例 | 后续会配置 ci、cd 构建脚手架以及一些配置一些常见前端脚手架如:vue-cli、cra、umi 的接入插件包 |
踩坑案例
动态资源 404 谬误
[案例形容] 咱们发现在代理完门路后失常的 html 资源是能够定位到的,然而对于 js、css 资源等会呈现找不到的 404 谬误
[案例剖析] 因为目前利用多为单页利用,而单页利用的次要都是由 js 去操作 dom 的,对于 mv* 框架而言通常又会在前端路由及对一些数据进行拦挡操作,因此在对应模板引擎处理过程中须要对资源查找进行相对路径查找
[解决方案] 咱们我的项目构建次要是通过 docker+k8s 进行部署的,因此这里咱们想到将资源门路对立放在一个门路目录下,而这个目录门路须要和父 nginx 利用转发门路的名称相一致,也就是说子利用须要在父利用中须要注册一个路由信息,后续就能够通过服务注册形式进行定位变更等
父利用 nginx 配置
{
"rj": {
"name": "xxx 利用",
"path:"/rj/"
}
}
server {
location /rj/ {proxy_pass http://ip:port/rj/;}
}
子利用
FROM xxx/nginx:1.20.1
COPY ./dist /usr/share/nginx/html/rj/
接口代理 404 谬误
[案例形容] 在解决完动态资源之后,咱们父利用中申请接口,发现接口竟然也呈现了 404 的查问谬误
[案例剖析] 因为目前都是前后端拆散的我的项目,因此后端接口通常也是通过子利用的 nginx 进行方向代理实现的,这样通过父利用的 nginx 转发过去后因为父利用的 nginx 中没有代理接口地址,因此会呈现没有资源的状况
[解决方案] 有两种解决方案,一种是通过父利用去代理后端的接口地址来进行,这样的话会呈现一个问题就是子利用代理的名称如果雷同,并且接口并不只是来自一个微服务,或者会有不同的动态代理以及 BFF 模式,那样对父利用的构建就会呈现复杂度不可控的情景;另一种则是通过扭转子利用中的前端申请门路为约定好的一种门路,比方加上约定好的服务注册中的门路进行隔离。这里咱们兼而有之,对于咱们自研我的项目的接入,会在复利用中进行对立的网关及动态资源转发代理等配置,与子利用约定好路径名,比方后端网关对立以 /api/ 进行转发,对于非自研我的项目的接入,咱们目前须要接入利用进行接口的魔改,后续咱们会提供一个插件库进行常见脚手架的 api 魔改计划,比方 vue-cli/cra/umi 等,对于第三方团队自研的脚手架构建利用须要自行手动更改,但一般来说自定义脚手架团队通常会有一个对立配置前端申请的门路,对于老利用如以 jq 等构建的我的项目,则须要各自手动更改
这里我以 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 过程提供脚手架,不便利用开发者接入构建部署,从而实现工程化的成果,对于可能成倍数复制的操作,咱们都应该想到利用工程化的伎俩来进行解决,而不是一味的投入人工,毕竟机器更善于解决繁多不变的批量且稳固产出的工作,共勉!!!
参考
- 搞懂 nginx 的 rewrite 模块
- 前端网关的思考
- Nginx 系列之代理之后无奈加载动态资源解决办法
- 前端 重定向和转发