如何配置 NGINX 通过一个端口托管多个服务:nginx.conf
Jupyter + HDFS + YARN + Spark,仅使用 NGINX 一个开放端口
导语
本文涵盖
- 通过一个端口托管多个 Web UI 的技巧
- 将 Web UI 隐藏在子位置的陷阱
- 创建可维护的 NGINX 配置的提示和最佳实践
序言
起初,这个任务出现是因为 BigData Team 迁移到了新发布的 Coursera Labs。
摘自“使用 Coursera Labs 创建实验活动”帮助文章的 FAQ
问:是否有机制可以在学生的实验容器中暴露多个端口?
答:Coursera Labs 支持单端口、单应用程序容器。但是,可以通过运行 NGINX 反向代理来实现。例如:如果有两个应用程序正在运行:app1 监听 9000 端口,app2 监听 9001 端口。你可以运行 NGINX 将 /app1 路由到 9000 端口,将 /app2 路由到 9001 端口。在这种情况下,NGINX 端口将从容器中暴露出来。
这听起来很简单,但如果你使用 Jupyter Notebook 作为其中一项服务,NGINX 配置就会变得具有挑战性。你可以找到一些关于 Jupyter 配置的技巧,但它们都是关于将 Web 服务放置在根目录。首先,将一个应用程序放在根级别,而其他应用程序放在单级位置(“`/app1`”是一个单级位置)或也放在根目录,这不是一个好主意。其次,仅仅在建议的位置添加一个单级位置前缀并不能让整个系统正常运行。原因很简单:大多数 Web 服务期望它们位于根目录,并且将内容作为通过根位置连接的处理。因此,需要一些额外的配置。此配置将在下面介绍。
此外,本文还讨论了如何克服错误的端口重定向问题。例如,你请求的端口是 10080,但 NGINX 重定向到了 80 端口。当你的 Docker 容器将本地端口 X 转发到容器的端口 Y(其中 X 不等于 Y,Y 是 NGINX 监听的端口)时,就会出现这种情况。
第一部分:基本配置
注释
- 包含类型,例如,允许浏览器使用 CSS 渲染页面。
- 四个额外的变量(参见“新变量”部分)使用“`map`”指令进行设置,以便将请求的 URL 重定向到正确的 Web 应用程序。
- 可重复的行只能在命名位置中定义一次。然后使用 HTTP 错误处理机制,不同位置的连接数据可以根据这个单一服务的命名位置进行处理。根据我在不同互联网资源上看到的情况,这是最流行的做法。
- 有时,一些指南建议使用 `$host` 变量来设置“`HOST`”头。如果 `$host` 不是用户在浏览器地址栏中输入的,那么在这种情况下代理会中断。其中一种情况是在 NGINX 之前使用了端口转发(例如,参见 `docker run -p`)。因此,我建议使用 `$http_host` 作为“`HOST`”请求头的值。
- 如果代理的是非 HTTP 请求,请更改 `X_Forwarded_Proto`。
- location 块的顺序至关重要,因为 NGINX 会按照使用 locations 及其模式生成的特定顺序匹配 URI 和 locations。
- 服务可以处于开启/关闭/临时关闭状态(Spark UI 在没有初始化上下文时)。永久重定向允许浏览器将某个状态(或某些子状态)保存在缓存中,即使状态实际发生更改也会显示。因此,在“**未找到 Location**”的 case 处理程序中,`return` 指令中不使用 301(永久移动)和 308(永久重定向)。302(找到,之前是“临时移动”)不适用,因为它会在某些浏览器中强制将 `POST` 请求更改为 `GET`,而 307(临时重定向)则保留 HTTP 方法。
- 还提供了调试头部的示例,因为它在许多 NGINX 配置调试问题中都非常有帮助。
user USER GROUP;
worker_processes 1;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
events {
}
http {
# HTTP handler basic configuration
include /etc/nginx/mime.types;
default_type application/octet-stream;
root /usr/share/nginx/html;
# New variables
map $http_referer $valid_http_referer {
~(/jup|/hdfs|/yarn|/spark) $http_referer;
}
map $valid_http_referer $app_link {
# TODO: set default service location
"" $scheme://$http_host/default_service;
~^([^/]+//[^/]+)(/[^/]+)?/? $1$2;
}
map $uri $no_app_uri {
~^/[^/]*(/.*) $1;
default $uri;
}
map $uri $requested_app {
~^/(([^/]+)/)? $scheme://$http_host/$2;
}
# Upstreams
...
# Servers configuration
server {
# Server basic configuration
listen 80;
# Trailing slash auto-completion
if (-d $request_filename) {
rewrite [^/]$ $scheme://$http_host$uri/ permanent;
} # Ensure that you have directories with all app names
# in the root selected above
# Zero-level location configuration
location = / {
if ($http_referer ~ "^.+$") {
return 307 $app_link?$args;
}
return 301 $scheme://$http_host/jup/;
}
# Common additional request headers for webapps
error_page 599 = @common_proxy_headers;
location @common_proxy_headers {
proxy_set_header HOST $http_host;
proxy_set_header Referer $http_referer;
proxy_set_header X_Forwarded_For $remote_addr;
proxy_set_header X_Forwarded_Proto http;
}
# Services' locations
...
# "Location not found" case handler
location ~ / {
if ($requested_app = $app_link) {
return 404;
}
return 307 $app_link$request_uri;
#add_header X-debug "$app_link $requested_app" always;
}
}
}
第二部分:代理到服务的 Location 配置模板
注释
- 带和不带尾部斜杠 `/` 的路径对于 NGINX 来说(通常非常)不同,因此请注意 `/` 的放置。
- `^~` 可防止将请求地址匹配到 `regexp` 位置,如果该地址与此位置匹配。`regexp` 位置用于处理未匹配的位置。
- `return 599;` 引发 HTTP 错误 599,由我们之前定义的处理程序处理(参见 `@common_proxy_headers`)。
location ^~ /some_service/ {
proxy_pass http://some_service_address/;
return 599;
}
如果服务位于单级位置,则应按以下方式修改该位置
location ^~ /some_service/ {
rewrite ^/some_service(.*)$ $1$2 break;
proxy_pass http://some_service_address/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$ $scheme://$http_host/some_service/$2;
}
`rewrite` 会从 uri 中移除匹配的位置。此处需要 `break`,因为它会阻止新的 uri 重新匹配。`proxy_redirect` 返回移除的部分,以便保留调用该地址上的对象(们)的可能性。
第三部分:Jupyter Notebook 代理配置
注释
- 这里还使用了“不要重复自己”机制(自定义错误处理程序、命名位置…)的另外两种用法。
- “static”子部分是可选的,甚至可能有害,因为 Jupyter Notebook 会发生变化。但是,它可以提高性能,因为静态文件将由 NGINX 提供,而不是由带有此部分的代理服务提供。在最终的配置文件中,此部分被省略了。
- Jupyter 内核和终端使用套接字。因此,到它们的连接必须被“升级”。
首先,将以下块放入“`Upstream`”部分
upstream notebook {
server localhost:8888;
}
然后,将此块放入“`Services`' locations”块
location ^~ /jup/ {
rewrite ^/jup(.*)$ $1$2 break;
proxy_pass http://notebook/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$ $scheme://$http_host/jup/$2;
}
# "Static" subsection start
error_page 598 = @jup_static_like;
location ^~ /jup/static {
return 598;
}
location ^~ /jup/custom {
return 598;
}
location ^~ /jup/nbextensions/widgets/notebook/js/ {
root /opt/conda/share/jupyter/nbextensions/jupyter-jupyter-js-widgets;
return 598;
}
location @jup_static_like {
try_files $no_app_uri $no_app_uri/ =404;
#add_header X-debug "$no_app_uri" always;
}
# "Static" subsection end
error_page 597 = @jup_upgrade_to_websocket;
location @jup_upgrade_to_websocket {
proxy_pass http://notebook;
proxy_set_header HOST $http_host;
# websocket support
proxy_http_version 1.1;
proxy_set_header Upgrade "websocket";
proxy_set_header Connection "Upgrade";
proxy_read_timeout 86400;
}
location ^~ /api/kernels {
return 597;
}
location ^~ /terminals {
return 597;
}
第四部分:HDFS+YARN+Spark 代理配置
注释
- HDFS 和 Spark 的代理很简单,只需要使用模板。对于 Spark UI,添加了到 YARN UI 的额外条件重定向。这是必要的,因为当 Spark Context Master 设置为“yarn”时,Spark UI 会自动重定向到 YARN UI。此外,当没有 Spark Context 运行时,将没有正在运行的 Web 应用程序。此时,NGINX 将返回 502,这将由添加的自定义错误处理程序处理。我将 502 更改为 200 并添加了额外的信息。
- HDFS Datanode 页面和 YARN UI 页面在 JS 脚本计算后形成链接,因此需要将链接替换器设置为在其他页面的内容之后作为另一个 JS 脚本。
hdfs_ui_fixer.js 的内容
setTimeout( function() {
$('#table-datanodes>tbody>tr').each(function(index, element){
element.innerHTML = element.innerHTML.replace(
/((>|href=)[^<]*?)(http:)?(\/\/)?\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:50075/g,
'$1/hdfs_node'
)}
);
console.log('server addresses in hdfs ui are fixed');
}, 2000)
yarn_ui_fixer.js 的内容
setTimeout( function() {
$('tbody>tr>td').each(function(index, element){
element.innerHTML = element.innerHTML.replace(
/((>|href=)[^<]*?)(http:)?(\/\/)?\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:8088/g,
'$1/yarn_ui'
)
element.innerHTML = element.innerHTML.replace(
/((>|href=)[^<]*?)(http:)?(\/\/)?\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:8042/g,
'$1/yarn_node'
)
element.innerHTML = element.innerHTML.replace(
/((>|href=)[^<]*?)(http:)?(\/\/)?\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:19888/g,
'$1/yarn_jobhistory'
)
});
console.log('server addresses in yarn ui are fixed');
}, 2000)
- HDFS 节点页面使用内部主机名链接到其上的另一个系统的 Web 服务。如果 `REMOTE_HOSTNAME` 被替换为实际的内部主机名,那么链接到 `nginx.conf` 中定义的位置将显示在返回的 UI 网页上。
location = /hdfs_ui/hdfs_ui_fixer.js {}
location ^~ /hdfs_ui {
rewrite ^/hdfs_ui(.*)$ $1$2 break;
proxy_pass https://:50070/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/hdfs_ui/$2;
sub_filter '</body>' '<script type="text/javascript"
src="/hdfs_ui/hdfs_ui_fixer.js"></script></body>';
}
location ^~ /hdfs_node {
rewrite ^/hdfs_node(.*)$ $1$2 break;
proxy_pass https://:50075/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/hdfs_node/$2;
sub_filter 'http://REMOTE_HOSTNAME:50075' '/hdfs_node';
}
location = /yarn_ui/yarn_ui_fixer.js {}
location ^~ /yarn_ui {
rewrite ^/yarn_ui(.*)$ $1$2 break;
proxy_pass https://:8088/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/yarn_ui/$2;
sub_filter '</html>' '<script type="text/javascript"
src="/yarn_ui/yarn_ui_fixer.js"></script></html>';
}
location ^~ /yarn_node {
rewrite ^/yarn_node(.*)$ $1$2 break;
proxy_pass https://:8042/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/yarn_node/$2;
sub_filter '</html>' '<script type="text/javascript"
src="/yarn_ui/yarn_ui_fixer.js"></script></html>';
}
location ^~ /yarn_jobhistory {
rewrite ^/yarn_jobhistory(.*)$ $1$2 break;
proxy_pass https://:19888/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/yarn_jobhistory/$2;
sub_filter '</html>' '<script type="text/javascript"
src="/yarn_ui/yarn_ui_fixer.js"></script></html>';
}
location ^~ /spark_ui {
error_page 404 502 = @spark_ui_error_page;
rewrite ^/spark_ui(.*)$ $1$2 break;
proxy_pass https://:4040/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/proxy/(.*)$
$scheme://$http_host/yarn_ui/proxy/$2;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/spark_ui/$2;
}
location @spark_ui_error_page {
default_type text/plain;
if (-d $request_filename) {
return 200 "Please, launch a Spark context firstly";
}
return 200 "Please, launch a Spark context firstly
or check url correctness";
}
location ^~ /spark_jobhistory {
rewrite ^/spark_jobhistory(.*)$ $1$2 break;
proxy_pass https://:18080/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/spark_jobhistory/$2;
}
第五部分:完整的 nginx.conf
user USER GROUP;
worker_processes 1;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
events {
}
http {
# HTTP handler basic configuration
include /etc/nginx/mime.types;
default_type application/octet-stream;
root /usr/share/nginx/html;
# New variables
map $http_referer $valid_http_referer {
~(/jup|/hdfs|/yarn|/spark) $http_referer;
}
map $valid_http_referer $app_link {
"" $scheme://$http_host/jup;
~^([^/]+//[^/]+)(/[^/]+)?/? $1$2;
}
map $uri $no_app_uri {
~^/[^/]*(/.*) $1;
default $uri;
}
map $uri $requested_app {
~^/(([^/]+)/)? $scheme://$http_host/$2;
}
# Upstreams
upstream notebook {
server localhost:8888;
}
# Servers configuration
server {
# Server basic configuration
listen 80;
# Trailing slash auto-completion
if (-d $request_filename) {
rewrite [^/]$ $scheme://$http_host$uri/ permanent;
}
# Zero-level location configuration
location = / {
if ($http_referer ~ "^.+$") {
return 307 $app_link?$args;
}
return 301 $scheme://$http_host/jup/;
}
# Common additional request headers for webapps
error_page 599 = @common_proxy_headers;
location @common_proxy_headers {
proxy_set_header HOST $http_host;
proxy_set_header Referer $http_referer;
proxy_set_header X_Forwarded_For $remote_addr;
proxy_set_header X_Forwarded_Proto http;
}
# Services' locations
location ^~ /jup/ {
rewrite ^/jup(.*)$ $1$2 break;
proxy_pass http://notebook/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$ $scheme://$http_host/jup/$2;
}
error_page 597 = @jup_upgrade_to_websocket;
location @jup_upgrade_to_websocket {
proxy_pass http://notebook;
proxy_set_header HOST $http_host;
# websocket support
proxy_http_version 1.1;
proxy_set_header Upgrade "websocket";
proxy_set_header Connection "Upgrade";
proxy_read_timeout 86400;
}
location ^~ /api/kernels {
return 597;
}
location ^~ /terminals {
return 597;
}
location = /hdfs_ui/hdfs_ui_fixer.js {}
location ^~ /hdfs_ui {
rewrite ^/hdfs_ui(.*)$ $1$2 break;
proxy_pass https://:50070/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/hdfs_ui/$2;
sub_filter '</body>' '<script type="text/javascript"
src="/hdfs_ui/hdfs_ui_fixer.js"></script></body>';
}
location ^~ /hdfs_node {
rewrite ^/hdfs_node(.*)$ $1$2 break;
proxy_pass https://:50075/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/hdfs_node/$2;
sub_filter 'http://REMOTE_HOSTNAME:50075' '/hdfs_node';
}
location = /yarn_ui/yarn_ui_fixer.js {}
location ^~ /yarn_ui {
rewrite ^/yarn_ui(.*)$ $1$2 break;
proxy_pass https://:8088/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/yarn_ui/$2;
sub_filter '</html>' '<script type="text/javascript"
src="/yarn_ui/yarn_ui_fixer.js"></script></html>';
}
location ^~ /yarn_node {
rewrite ^/yarn_node(.*)$ $1$2 break;
proxy_pass https://:8042/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/yarn_node/$2;
sub_filter '</html>' '<script type="text/javascript"
src="/yarn_ui/yarn_ui_fixer.js"></script></html>';
}
location ^~ /yarn_jobhistory {
rewrite ^/yarn_jobhistory(.*)$ $1$2 break;
proxy_pass https://:19888/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/yarn_jobhistory/$2;
sub_filter '</html>' '<script type="text/javascript"
src="/yarn_ui/yarn_ui_fixer.js"></script></html>';
}
location ^~ /spark_ui {
error_page 404 502 = @spark_ui_error_page;
rewrite ^/spark_ui(.*)$ $1$2 break;
proxy_pass https://:4040/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/proxy/(.*)$
$scheme://$http_host/yarn_ui/proxy/$2;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/spark_ui/$2;
}
location @spark_ui_error_page {
default_type text/plain;
if (-d $request_filename) {
return 200 "Please, launch a Spark context firstly";
}
return 200 "Please, launch a Spark context firstly
or check url correctness";
}
location ^~ /spark_jobhistory {
rewrite ^/spark_jobhistory(.*)$ $1$2 break;
proxy_pass https://:18080/;
return 599;
proxy_redirect ~^([^/]*://[^/]*)?/(.*)$
$scheme://$http_host/spark_jobhistory/$2;
}
# "Location not found" case handler
location ~ / {
if ($requested_app = $app_link) {
return 404;
}
return 307 $app_link$request_uri;
#add_header X-debug $app_link $requested_app" always;
}
}
}
结语
在本文中,我们通过 `nginx.conf` 文件,配置 NGINX 来代理 Jupyter 以及 HDFS、YARN、Spark UI。这可能是必要的,因为某些包可能不是默认包含的。例如,考虑 subfilter 包,它仅存在于最广泛的默认构建中。此外,从源代码构建有助于摆脱不必要的包。还提供了一些配置脚本。它们有助于根据 `nginx.conf` 调整环境,反之亦然。最后,作为奖励,我们还考虑了 YARN Web UI 的主机名问题。
此处列出的配置文件已在“`bigdatateam/hysh`”Docker 镜像中使用。
在评论部分,留下您对改进配置文件和如何包含 SSH 代理的建议。
感谢 Alexey Dral 的编辑!
作者:Nikolay Veld,来自 BigDataTeam