标签归档:反向代理

绕过代理服务器获取客户端IP

一般来说,Web应用和客户端之间隔着很多个代理服务器,无论正向代理还是反向代理。这样就给本地资讯类应用(新闻、天气等)或者统计(审计,追踪等)需求带来了麻烦,因为在应用通过request.getRemoteAddr()方法(以Java为例)往往只能拿到与自己直接通信的设备IP。因此,如果用户通过直接或者间接的正向代理(ISP提供的缓存服务器等)上网,Web应用只会取到正向代理服务器地址;如果Web应用前端部署着Nginx或者Apache之类的反向代理,Web应用只会取到反向代理的地址。

面对这样的限制,代理厂商利用HTTP自定义Header规避了问题。比如Nginx,可以添加以下配置:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

它向后端应用传递了两个HTTP自定义Header,X-Real-IP和X-Forwarded-For。这两个Header作用类似,都是向后端透传客户端源IP,但是又有一些不同,主要区别在于:

1. X-Forwarded-For已有标准RFC文档(RFC 7239)定义,而X-Real-IP没有。
2. X-Forwarded-For Header格式为X-Forwarded-For: client1, proxy1, proxy2,每级代理都会将与自己直接通信的对端IP追加在X-Forwarded-For中,因此应用还需要额外解析IP列表以获取所需IP。而X-Real-IP只会记录与自己直接通信的对端IP。

由此,反向代理后面的应用服务器可以通过X-Real-IP或者X-Forwarded-For请求头来获取客户端IP:

String xRealIp = request.getHeader("X-Real-IP");
String xForwardIps = request.getHeader("X-Forwarded-For");

另外需要注意的是,HTTP Header是很容易被伪造的,因此,X-Forwarded-For Header中的首个IP也不意味着就一定是客户端源IP。从安全性方面来说,如果应用前不加反向代理,则request.getRemoteAddr()拿到的IP便是可信的,无法伪造;如果加了反向代理,那么X-Real-IP和X-Forwarded-For IP的最后一个IP是可信的,无法伪造。

--EOF--

『大型网站技术架构:核心原理与案例分析』(四)

『大型网站技术架构:核心原理与案例分析』读书笔记系列:
(一):架构演化、模式、要素
(二):高性能架构
(三):高可用架构
(四):可伸缩架构
(五):可扩展架构
(六):安全性架构


『大型网站技术架构』(四):可伸缩架构

“大型“定义:

  • Facebook: 大量用户及大量访问,10亿用户。
  • 腾讯: 功能复杂,产品众多,1600+种产品。
  • Google:大量服务器,100w台服务器。

一、网站架构的伸缩性设计

  • 不同功能进行物理分离实现伸缩

    单一服务器处理所有服务 -> 数据库从应用服务器分离 -> 缓存从应用服务器分离 -> 静态资源从应用服务器分离

    横向分离(分层后分离)、纵向分离(业务分割后分离)

  • 单一功能通过集群规模实现伸缩

    当一头牛拉不动车的时候,不要去寻找一头更强壮的牛,而是用两头牛来拉车。

    集群伸缩性:应用服务器集群伸缩性、数据服务器集群伸缩性(缓存数据服务器集群和存储数据服务器集群)

二、应用服务器集群的伸缩性设计

负载均衡:实现网站伸缩性,改善网站可用性。

负载均衡类型

1. HTTP重定向负载均衡

通过一台HTTP重定向服务器,返回302实现负载均衡。实践中很少见。

优点:简单
缺点:性能差(2次请求)、伸缩性有限(重定向服务器容易成为瓶颈)、被搜素引擎判为SEO作弊(302请求),

2. DNS域名解析负载均衡

在DNS服务器中配置多个A纪录。大型网站用于进行第一级负载均衡。

优点:支持基于地理位置域名解析,加快用户访问速度。
缺点:DNS多级解析,生效和失效时间久。

3. 反向代理负载均衡

利用反向代理服务器(缓存资源、安全等)进行负载均衡。应用层负载均衡。

优点:反向代理功能和负载均衡功能集成,部署简单。
缺点:反向代理服务器容易成为瓶颈。

4. IP负载均衡

在网络层通过修改请求目标地址进行负载均衡。

  • 负载均衡服务器修改目的IP的同时修改源地址,将数据包源地址设为自身IP。(SNAT)
  • 负载均衡服务器同时作为Real Server的网关服务器。(LVS/NAT模式)

优点:相比应用层负载均衡(反向代理)有更好的性能。
缺点:进出流量走负载均衡服务器,依然存在瓶颈。

5. 数据链路层负载均衡

在通信协议的数据链路层修改mac地址进行负载均衡(LVS/DR)。数据三角传输模式,流量从用户->负载均衡服务器->Real Server->用户。

目前使用最广泛。

负载均衡算法

  • 轮询(Round Robin, RR)
  • 加权轮询(Weighted Round Robin, WRR)
  • 随机(Random)
  • 最少连接(Least Connections)
  • 源地址散列(Source Hashing)

三、分布式缓存集群的伸缩性设计

目标:必须让新上线/下线的缓存服务器对整个分布式缓存集群影响最小,也就是说经过调整使整个缓存服务器集群中已经缓存的数据尽可能还被访问到。

一致性Hash:解决集群扩减容时过多节点缓存失效问题。
使用虚拟节点的一致性Hash环:避免集群扩减容造成的节点负载不均问题,通过增加一层虚拟节点与物理节点的映射来使节点增删带来的影响平均到所有节点。

四、数据存储服务器集群的伸缩性设计

数据存储服务器必须保证数据可靠存储、可用性、正确性,伸缩性设计原则与缓存不同。

1. 关系数据库集群的伸缩性设计

  • 主从读写分离
  • 业务分隔、数据分库。跨库不能Join操作、避免事务
  • 数据分片:通过分布式关系数据库访问代理,比如Amoeba、Cobar(Ali)。伸缩性:一致性Hash + 数据迁移

2. NoSQL数据库的伸缩性设计

HBase伸缩性: 依赖可分裂的HRegion及可伸缩的分布式文件系统HDFS实现。

--EOF--

多级反向代理下的维护页面配置

Nginx的error_page指令可以用来在服务器临时维护时呈现静态维护页面,增强用户体验,但是在稍微复杂的反向代理部署架构中,只有error_page还是不够的。比如:
proxy
Tomcat前端接Apache,Apache前端接Nginx,流量入口在Nginx。这样一个两级的反向代理结构下,如果服务器程序升级,Tomcat重启,Apache检测到Tomcat服务不可用时,向上级返回503,那么用户通过Nginx看到的,也是丑陋的503 Service Temporarily Unavailable页面,试图在Nginx配置中加上error_page 503 /50x.html页面也不会起作用。探其原因,在于error_page指令是用来处理由Nginx产生的错误,在Nginx看来,Apache工作正常,且能返回响应码和正确页面,只不过返回的页面对用户来说是个服务不可用页面。如果是Apache停掉,那么Nginx是能够正确呈现error_page指定的维护页面的。

所以,需要另外在Nginx配置中加入proxy_intercept_errors指令,官方说法是:

Determines whether proxied responses with codes greater than or equal to 300 
should be passed to a client or be redirected to nginx for processing with the 
error_page directive.

设置proxy_intercept_errors为on后,Nginx会解析被代理服务器的响应码,如果出现大于300的响应码,就会调用error_page指令处理,如果error_page指令中没有指定返回的错误码,那么Nginx仍会依样返回被代理服务器的响应。

以下为简化的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
 server_name domain.com;
 location / {
  ...
  proxy_pass ...;
  proxy_intercept_errors on;
 } 
 
 error_page 503 /50x.html;
 location = /50x.html {
  root /etc/nginx/maintein;
 }
 #50x.html中引入了同级目录下的static目录中的图片文件。
 location ~ /static {
  root /etc/nginx/maintein;
  rewrite ^(.*)/(static/.*)$ /$2 break;
 }
}

--EOF--

多Region应用系统设计和部署

有一个Web应用系统,需要跨Region部署,每个Region依赖的底层服务各不相同,但是要求是入口统一、单点登录(SSO)。一些较大的业务平台(商务领航)、监控平台、云计算平台(aws)应该都有类似需求。整体结构如下图所示:
multi-regions

用户通过mydomain.com入口进入系统,登陆后可选择进入不同Region,使用不同的后端服务(Service1,Service2...),并且可以随时通过页面选择进入其他Region。除非用户明确切换Region,否则每次请求总是落到相同Region上。

在设计方面,要实现上述系统,需关心以下几点:
1. 单点登陆。在没有可靠的集中式session服务器存在的情况下,将用户认证信息放在Cookie中是个较好的选择,去掉应用系统的状态,方便扩展,应用可以以集群的方式部署在任一Region里。这里意味着,当用户在任一Region登陆成功后,应用系统要向浏览器写认证Cookie,此后,用户无论在Region内部或者Region间跳转都可以不必重新登陆。

2. 底层服务去耦合。应用系统作为业务聚合的地方,需与底层服务尽量松耦合。一般底层服务以RESTful API的形式对外暴露接口,应用系统对其的依赖可以删减至最少,只需将其服务地址(IP+端口号)加入配置项即可。相同Region中的Tomcat集群使用相同配置文件,不同Region使用不同的配置文件。

3. 反向代理标记。架构图中可以看出前端有个Nginx作为反向代理,将请求代理到某个Region下Tomcat集群中的一个节点。Nginx实际上是个透明代理,应用系统必须通过一种方式将用户所选择的Region信息告知Nginx,才能保证每次用户请求落到相同Region上。最简单的做法当然是由应用系统将当前Region的id写入到REGION Cookie里,Nginx根据此Cookie值进行反向代理。也可以选择不侵入系统,应用系统只需给出第一推动力,比如给个参数,此后Nginx会一直将该参数传递下去,实现上述Cookie实现的功能。

在部署方面,主要涉及应用系统部署和Nginx部署:
1. 应用系统。应用系统只要做到了无状态,就可以很简单的以Tomcat集群方式对外提供服务。注意每个集群下的节点配置文件不同,主要是RegionId, 底层服务地址等有所区别。

2. Nginx。各个Tomcat集群中的节点负载均衡交给Nginx upstream模块实现,一个集群对应一段upstream配置,均衡策略根据需要选择。例如:

1
2
3
4
5
6
7
8
9
10
upstream region1 {
  server 10.10.1.1;
  server 10.10.1.2;
  server 10.10.1.3;
}
 
upstream region2 {
  server 10.10.2.1;
  server 10.10.2.2;
}

多Region的配置方面,要实现Region间切换,Nginx需支持根据regionid参数反向代理,要实现Region内停留,需支持根据Cookie值反向代理。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#省略部分配置
location / {
  if ($args ~ regionid=1){
    proxy_pass http://region1;
    break;
  }
  if ($args ~ regionid=2){
    proxy_pass http://region2;
    break;
  }
  if ($http_cookie ~* "REGION=1" ){
    proxy_pass http://region1;
    break;
  }
  if ($http_cookie ~* "REGION=2" ){
    proxy_pass http://region2;
    break;
  }
  proxy_pass http://region1;
 
  #省略部分配置
}

上述配置的含义是,当url中携带显式的regionid参数时,根据参数值代理到相应Region,该参数只有用户选择切换Region时才会携带。当url中不带regionid参数,则根据REGION Cookie的值进行反向代理。否则,Nginx默认将请求代理到默认Region上。

--EOF--

WebSocket via nginx

WebSocket的握手流程[1]如下:
浏览器请求:

1
2
3
4
5
6
7
8
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

服务器响应:

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

细节不展开了,只需关注浏览器和服务器之间如何通过Upgrade和Connection header进行协商,将一个HTTP连接升级为WebSocket。

nginx 1.3版本以后开始支持WebSocket的反向代理。因为WebSocket的握手主要是通过标准HTTP请求和响应来实现的,因此对于反向代理服务器,nginx在7层只要正确处理好WebSocket协议的握手流程,使得本来在tcp连接上跑HTTP协议的,现在用来跑WebSocket,只要握手完成剩下的交互就是tcp层面的事,跟HTTP协议没有关系了。注意,如果nginx版本小于1.3,对WebSocket进行反向代理时会出现“WebSocket is closed before the connection is established.”的警告。

nginx支持WebSocket需进行以下配置(官方文档):

1
2
3
4
5
6
location /chat/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

HTTP 1.1的长连接是WebSocket协商的基础,另外因为Upgrade和Connection header在HTTP协议中都是hop-by-hop header,因此在反向代理的时候需要把这两个header回填,发送到服务器端。从全局来看,就是客户端(浏览器)同nginx之间建立一条tcp长连接,nginx和后端服务器之间建立一条(或多条,视upstream的server节点个数)tcp长连接,这些长连接都是用来传递WebSocket数据帧。

以下是一个真实例子,使用场景来为:
Openstack组件中有个nova-novncproxy服务,支持用户通过浏览器以VNC的方式登陆虚拟机。现在有一个类似域名为https://console.ustack.com的Web管理平台,在VNC登陆虚拟机页面嵌入noVNC地址。这里的问题是,Web管理平台是https的,如果noVNC地址是http,那么高版本浏览器为了安全考虑都会屏蔽noVNC页面,理由是https页面内嵌了http页面;如果给noVNC页面也加个https支持,那就有点浪费了,给IP地址买张证书不划算,给域名买张证书不划算的同时还浪费一个域名。所以最好的方式就是在Web管理平台的前端nginx机器上为noVNC做个反向代理,用户看到的都是Web管理平台的域名,这样在交互上也显得统一。结合上文描述的nginx支持WebSocket的配置,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#web管理平台
upstream web {
  server 127.0.0.1:8080;
  keepalive 80;
}
 
#noVNC
upstream vnc {
  server 111.26.23.10:6080;
  keepalive 80;
}
 
 
server {
  listen 443;
  server_name console.ustack.com;
  ssl on;
  ssl_certificate /etc/nginx/ssl/server.crt;
  ssl_certificate_key /etc/nginx/ssl/server.key;
 
  location / {
    proxy_pass http://web;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_http_version 1.1;
  }
 
  location /vnc {
    rewrite ^/vnc/(.*) /$1 break;
    proxy_pass http://vnc;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_http_version 1.1;
  }
 
  location /websockify {
    proxy_pass http://vnc;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_read_timeout 86400;
  }
}

其中,/websockify location就是用来反向代理WebSocket请求,proxy_read_timeout参数是为了避免长时间没有数据收发时连接被nginx关闭。

[1] WebSocket. http://en.wikipedia.org/wiki/WebSocket.

--EOF--