Nginx map module 语法和 websocket 反代的行为细节

https://nginx.org/en/docs/http/ngx_http_map_module.html

基本用法

语法: map $var1 $var2 { map list }
默认值:
配置段: http(可以存在多个 map 区域)
map 指定了两个变量,其顺序和映射表中的列对应,这里的变量 var1 是 Nginx 自带变量,var2 则是自定义变量名,Nginx 自带变量表可以在 https://nginx.org/en/docs/varindex.html 查到。
map 的大括号内则是映射表,映射表由两列组成,匹配模式(string)和需要设定的值(variable)。
在 map 块里的参数指定了源变量值和结果值的对应关系。

  • 匹配模式可以是一个简单的字符串或者正则表达式,使用正则表达式要用(‘~’)。
  • 一个正则表达式如果以 “~” 开头,表示这个正则表达式对大小写敏感。以 “~*”开头,表示这个正则表达式对大小写不敏感。
  • 不使用正则的时候,默认使用大小写不敏感匹配。
  • 如果源变量值包含特殊字符如 ~,则要以 \来转义。

一个简单的例子:

效果为:这个区块指定了 $http_user_agent$is_curl两个变量进行关联, "~curl"即是匹配模式,如果 User-Agent 变量包含 curl(正则大小写敏感),则将 $is_curl变量设为1,默认为0,这个变量是全局有效的,比如,可以在 log_format 中直接使用变量 $is_curl, 也可以用于其他 location 的 if 判断。

复杂匹配逻辑

在复杂情景下,大括号内可以使用以下几个参数:

  • default (value): 默认值,当不指定时,默认值为空字符串
  • hostnames: 允许使用通配进行前缀或后缀匹配,如 example.* 1;,这个参数必须写在映射列表的最前面
  • include (file): nginx 的标准 include 语法,类似 include conf.d/*的效果,等同于直接将文件内容完整替换到此处
  • volatile: 指定不允许被缓存的值

当满足多个匹配条件时,匹配的顺序是:

  1. 不带通配的字符串完全匹配
  2. 最长前缀匹配的字符串,如 *.example.com
  3. 最长后缀匹配的字符串,辱 mail.*
  4. 第一个匹配的正则
  5. default 指定的值

不能在 map 块的映射表里使用 Nginx 的引用命名捕获或位置捕获变量(即类似 $1 的变量,这个用法常用于 rewrite)。如 ~^/someURI/(.*)  /locationxxxx/$1; 这样会报错 nginx: [emerg] unknown variable。

实践:反代 Websocket

情景:为模板渲染方便,使用类似以下格式的 proxy.conf 配置文件,upstrem 在其他位置另行指定,满足在 proxy.conf 完全不进行修改时,满足 upstream 为 websocket 和 http 的情景。主要困难在于 Nginx 反代 websocket 时需要主动添加 Upgrade 头(HAProxy 不需要作出任何特殊修改,直接使用 http 反代的配置文件可以正常工作),而向一般服务器发送多余的 header 可能引起 400 bad request 等问题,因此需要做出兼容处理。两年前在 https://www.starduster.me/2016/08/23/brief-talk-of-websocket/ 曾经讨论过 websocket 的协议设计、特点和反代的配置,将这个问题重新拿出来讨论一下。

已知 websocket 连接发起时完整请求头形如:

Websocket 的实现方式是通过发送 Upgrade 头,但是 Upgrade 和 Connection 都是逐跳(hop-by-hop)的 header,即只在一跳内有效而不会被传递到源站,这个 header 在正常情况下经过代理的时候会被去掉,使用 map 指令可以根据客户端是否有 Upgrade:websocket 来判断是否需要向后发送 Upgrade 和 Connection。

查 Nginx 变量表,知所有 http header 都是 Nginx 内置变量,如 Upgrade 头的数据可以直接使用 $http_upgrade 变量取得,

验证的时候根据抓包分析发现一个问题,Nginx 会对所有没有 Connection 头的请求向后加上 Connection:close 头,我尝试显示的指定 Connection: keep-alive 时,Nginx 依旧向 upstream 发送了 Connection: close (默认情况不指定 proxy_http_version 1.1; 则 nginx 发往 upstream 的请求是 HTTP/1.0),这一行为似乎是 nginx 的默认行为,作为反代时 nginx 不会向 upstream 复用已建立的连接。

尝试改为

此时判断的逻辑就变为:判断客户端是否传递了 Upgrade: websocket,如果有,则传递 Connection 头并原样传递 Upgrade 头,反之默认为空,不发送 Connection 头,做出这个一修改后 websocket 和 http 的行为依旧正常,但是抓包显示 Nginx 已经不会显示的向 upstream 发送 Connection: close ,这个配置的兼容性有待讨论。

(目前确实遇到过使用官方示例的配置方式会导致 websocket 断连的情况,暂时没有研究具体这个行为为何会导致断连,暂时使用上述配置解决)

PS, Nginx 作为反代时, ngx_http_upstream_module 在 upstream 区块中的 keepalive参数表示的是维持的空闲连接数,注意不要望文生义, https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据