img

最近遇到这样一个案例:客户使用了我们的CDN以后,后端作负载均衡的两台Tomcat服务器不能正常处理用户session会话了(客户自身并没有做会话共享),即使使用了ip_hash方式也会出现会话无法保持的情况。在了解用户的业务架构形态以后,很快定位了问题的原因。主要涉及到的知识就是多层反向代理

架构形态分析

该用户业务的初始架构如下:

mark

由上图可见,用户后端使用了一台nginx服务器做反向代理并转发给两台tomcat服务器,由此做负载均衡。nginx前端使用了阿里云的SLB(不是很理解此处加SLB的意义,SLB四层透传后端只挂一台Nginx似乎并没有起到负载均衡的作用)。在这样的架构下,真正起到负载均衡作用的是nginx服务器,访客的真实IP传送到nginx,并分发给后端的tomcat处理,即使没有做session共享,使用ip_hash来保持会话完全没有大问题。

接入CDN以后,相当于在该架构中SLB前面的部分又加上了一层反向代理。如果不对nginx做特殊配置,nginx作为反代服务器拿到的请求IP肯定是CDN节点,并将节点IP转发给tomcat,而不是对真实访客IP做转发,必然会出现问题。这里涉及到一个CDN智能取源的过程,关于CDN的原理不是本文讨论的问题。简单点说,同一个用户发起的连续两次请求不一定是由同一个节点去完成取源动作的。举例说明,客户端1.1.1.1,源站2.2.2.2,节点3.3.3.3和4.4.4.4。1.1.1.1发起的第一次请求是由节点3.3.3.3去源上取数据,但它发起的第二次就可能是节点4.4.4.4或者其他节点去源上取数据。这样一来,后端不做会话共享的话,nginx使用ip_hash就完全没有效果了,必然会导致后端出现会话无法保持的情况。接入CDN以后的示意图如下:

mark

解决办法

要解决这个问题主要有以下几种办法:

第一是后端真实服务器群做会话共享;第二就是nginx反代服务器负载均衡方式设置为cookies粘住,需要使用扩展模块nginx-sticky-module;第三就是修改nginx的配置,使其可以获取到真实访客IP并转发。

架构模拟

本次就这个问题,模拟测试下在经过CDN以后,后端源服务器如何获取到客户端的真实IP。

首先需要搭建一个模拟环境来还原该用户的架构(SLB忽略)。这里使用docker轻松完成架构部署。使用官方的nginx和tomcat镜像,一共需要run三个容器。

docker pull tomcat
docker run --name tomcat01 -p 49158:8080 -v $PWD/test01:/usr/local/tomcat/webapps/test -d tomcat docker run --name tomcat02 -p 49160:8080 -v $PWD/test02:/usr/local/tomcat/webapps/test -d tomcat

docker pull nginx
docker run -p 80:80 --name nginx -v $PWD/www:/www -v $PWD/conf/nginx.conf:/etc/nginx/nginx.conf -v $PWD/logs:/wwwlogs --link tomcat01:tc01 --link tomcat02:tc02 -d nginx

浏览器访问http://IP:端口,出现nginx和tomcat的默认起始页,即说明服务已经正常起来了。

分别进入两台tomcat服务器,在web服务根目录下建立两个war包。

[root@localhost ~]# docker exec -it tomcat01 /bin/bash
root@e330a8e3db61:/usr/local/tomcat# cd webapps/
ROOT/         docs/         examples/     host-manager/ manager/      test/         
root@e330a8e3db61:/usr/local/tomcat/webapps# cd test/
root@e330a8e3db61:/usr/local/tomcat/webapps# echo "111" >index.html
root@e330a8e3db61:/usr/local/tomcat/webapps# exit

[root@localhost ~]# docker exec -it tomcat02 /bin/bash
root@93b228dc59e6:/usr/local/tomcat# cd webapps/
ROOT/         docs/         examples/     host-manager/ manager/      test/         
root@93b228dc59e6:/usr/local/tomcat/webapps# cd test/
root@93b228dc59e6:/usr/local/tomcat/webapps# echo "222" >index.html
root@93b228dc59e6:/usr/local/tomcat/webapps# exit

通过命令分别查看两台tomcat的IP地址:

docker ps
docker inspect $container_id | grep IPAddress # $container_id为容器实际的id

进入nginx容器,修改nginx配置文件,增加以下配置:

upstream tomcat_balance {
           server 172.17.0.2:8080 weight=1;  #此处为tomcat的真实IP,由上一步得到
           server 172.17.0.3:8080 weight=1;
}

server {
      listen 80;
      location / {
             proxy_pass http://tomcat_balance;
      }
}

重载nginx服务,本地浏览器测试,多次刷新可以得到不同的返回结果,说明nginx反代后端tomcat成功了。

mark

mark

到这里,这个基本架构就搭建完成了。

获取用户真实IP

为了更直观看到报文的IP信息,此次使用nginx的echo模块来进行后面的实验。首先安装下环境,官方的镜像缺少很多基础命令。

  • 进入nginx容器
[root@localhost test01]# docker exec -it nginx /bin/bash
  • 替换镜像源
#替换官方源为网易163源
cat>/etc/apt/sources.list<<'EOF'
deb http://mirrors.163.com/debian/ stretch main non-free contrib
deb http://mirrors.163.com/debian/ stretch-updates main non-free contrib
deb http://mirrors.163.com/debian/ stretch-backports main non-free contrib
deb-src http://mirrors.163.com/debian/ stretch main non-free contrib
deb-src http://mirrors.163.com/debian/ stretch-updates main non-free contrib
deb-src http://mirrors.163.com/debian/ stretch-backports main non-free contrib
deb http://mirrors.163.com/debian-security/ stretch/updates main non-free contrib
deb-src http://mirrors.163.com/debian-security/ stretch/updates main non-free contrib
EOF

安装相关依赖和命令:

apt-get update
apt-get install -y vim wget curl
apt-get install gcc g++ automake make
apt-get install -y libpcre3 libpcre3-dev openssl libssl-dev zlib1g zlib1g-dev

使用echo模块来输出相关信息,编译过程:

cd /usr/local/src
#下载echo模块源码包
wget https://github.com/openresty/echo-nginx-module/archive/v0.61.tar.gz
tar zxvf v0.61.tar.gz

#下载nginx源码包
wget http://nginx.org/download/nginx-1.13.10.tar.gz
tar -xzvf nginx-1.13.10.tar.gz
cd nginx-1.13.10/
./configure --prefix=/usr/local/nginx --add-module=/usr/local/src/echo-nginx-module-0.61/

make -j2
mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old
cp -f objs/nginx /usr/local/nginx/sbin/
make upgrade

接下来模拟还原不接入CDN和接入CDN两种业务场景下,客户端请求报文的传输过程。

Ⅰ. 不接CDN时,访客真实IP传输情况

server{}段增加以下配置:

location /ip {
             default_type text/plain;
             echo "X-Real-IP: $remote_addr";
             echo "X-Forward-For: $http_x_forwarded_for";
}

浏览器访问测试:

mark

这里的180.158.xx.xx是本人电脑所用带宽的本地IP。在未接入CDN且客户端是直接访问的网络模式下,nginx获取到的$remote_addr字段即为客户端访问的真实IP。没有使用代理的情况下,$http_x_forwarded_for字段为空。

Ⅱ. 接入CDN时,访客真实IP传输情况

下面是接入CDN以后的IP信息:

mark

此时,nginx上的$remote_addr获取到的183.131.xx.xx为CDN节点的IP,$http_x_forwarded_for获取到的180.158.xx.xx才是用户的真实IP。

$http_x_forwarded_for 字段实际格式:用户IP,第一层代理的IP,第二层代理的IP.....,即每经过一层代理,就会将其代理服务器的IP加到该字段后面。

所以,这种模式下要想获取用户真实IP,只要取$http_x_forwarded_for字段内处于第一个位置的IP即可。nginx的配置如下:

map $http_x_forwarded_for  $clientRealIp
{
    ""    $remote_addr; 
    ~^(?P<firstAddr>[0-9\.]+),?.*$   $firstAddr;
}

location /ip {
       default_type text/plain;
       echo "X-Real-IP: $clientRealIp";
}

这样一来,就可以对用户真实IP做处理了。

​​‌‌​​​‌‌​‌​​‌‌‍​‌​‌‌‌​​‌‌‌‌​‌​‍​‌​​‌​​​‌​​​‌‌​‍​‌​‌‌​​​‌‌​​​​​‍​​‌​‌‌‌‌‌‌‌‌​​​‍​‌‌​​‌‌‌​‌‌​​‌‌‌‍​‌‌​​​‌‌‌​​​‌​‌‍​​‌‌‌‌‌‌‌‌​​‌‌‍​​​​​​‌​​‌‌​​​​‍​‌‌‌​​​​​​‌‌‌​​​‍‌​‌‌‌‌​​‍‌​‌‌‌​‌‌‍‌​‌‌​​​‌‍​‌‌​​​‌‌​‌‌​‌​​‍​‌​‌​‌‌‌‌‌‌​​​‌‍‌​‌‌​​​‌‍‌​​‌‌​​​‍‌​​‌​‌‌​‍‌​​‌​​​‌‍‌​​​​‌‌‌‍​​‌‌​​​‌‌‌‌​​‌​‍​‌​‌‌​‌​‌​‌‌‌‌​‍​‌​‌​​‌‌​​‌​‌‌‌‍​‌‌‌‌‌​​​‌​​‌​​​‍​‌​‌‌​​​​‌​‌​​‌‍​​​‌​‌​‌‌​‌​‌‌‌‍​​‌‌‌​‌‌‌​​‌​​​‍​​​‌​​​‌‌‌​​​​​‍​‌​​‌​​​‌‌​​​​‌‍‌​‌‌​‌‌​‍‌​‌​‌‌‌‌‍​​‌‌‌‌‌‌‌‌​​‌​‍​​​​​​​​‌‌‌‌​​‌‌‍​​​‌​‌​‌‌​​‌‌‌​‍‌​​‌‌‌‌​‍‌​​‌‌​‌‌‍‌​​‌​​‌​‍‌​​‌​‌‌​‍‌​​‌​​​‌‍​‌‌​​​‌​‌‌‌​​​‌‍‌‌​​‌‌​‌‍‌‌​​‌‌‌‌‍‌‌​​‌‌‌​‍‌‌​​​‌‌‌‍‌‌​‌​​‌​‍‌‌​​‌‌‌‌‍‌‌​​‌​‌‌‍‌‌​‌​​‌​‍‌‌​​‌‌‌‌‍‌‌​​‌‌‌​‍​‌​‌‌​‌‌‌‌​​‌​​‍​‌‌​​​​‌​‌​​​‌‌‍​​​​​​​​‌‌‌‌​​‌‌‍​‌​‌‌​​​‌‌​​​​​‍​​‌‌​‌​​‌‌‌‌​​​‍​‌​‌​​​‌‌​​‌‌‌‌‍​‌​‌​​​‌​‌‌‌‌‌‌‍​​​​​​​​‌‌‌​​‌​‌‍‌​​‌​‌‌‌‍‌​​​‌​‌‌‍‌​​​‌​‌‌‍‌​​​‌‌‌‌‍‌​​​‌‌​​‍‌‌​​​‌​‌‍‌​‌​​​‌‌‍‌​‌​​​‌‌‍‌​​‌​‌‌​‍‌​​‌​‌​​‍‌​​‌​‌‌​‍‌​​​‌​​​‍‌​​‌​‌‌​‍‌‌​‌​​​‌‍‌​​‌​​‌​‍‌​​‌‌​‌​‍‌​‌​​​‌‌‍‌​​‌‌‌‌​‍‌​​​‌‌​‌‍‌​​‌‌‌​​‍‌​​‌​‌‌‌‍‌​​‌​‌‌​‍‌​​​‌​​‌‍‌​​‌‌​‌​‍‌​​​‌‌​​‍‌​‌​​​‌‌‍‌‌​​‌​‌‌‍‌‌​​‌​​​‍‌‌​‌​​​‌‍‌​​‌​‌‌‌‍‌​​​‌​‌‌‍‌​​‌​​‌​‍‌​​‌​​‌‌

后面的nginx反代到tomcat的过程就暂时先不做了。此次主要完成了经过CDN以后,nginx服务器获取用户真实IP的过程。

The End.