最近在项目中遇到一个问题,因为业务要求,需要在服务中获取到客户端IP,但是在项目开发部署过程中发现利用 java -jar ***.jar单独运行服务,或者打成镜像再 docker run启动的服务都可以正确的获取到client IP,但是当采用 docker stack deploy发布到docker swarm集群的时候,服务却获取不到正确的client IP了,得到的都是10.255.0.* 这样的IP,因为业务逻辑必须获取正确的client IP,所以有了下面的这个调查。
系统框架:spring boot、spring cloud、docker、docker swarm
spring boot version:1.5.1.RELEASE
spring cloud version:Dalston.SR4
docker环境:
Containers: 7 Running: 4 Paused: 0 Stopped: 3 Images: 31 Server Version: 1.13.1 Storage Driver: overlay Backing Filesystem: extfs Supports d_type: true Logging Driver: json-file Cgroup Driver: cgroupfs Plugins: Volume: local Network: bridge host macvlan null overlay Swarm: active NodeID: qhsk9yp9h2md9qsopj7u6cjvg Is Manager: false Node Address: 192.168.0.138 Manager Addresses: 192.168.0.139:2377 Runtimes: runc Default Runtime: runc Init Binary: docker-init containerd version: aa8187dbd3b7ad67d8e5e3a15115d3eef43a7ed1 runc version: 9df8b306d01f59d3a8029be411de015b7304dd8f init version: 949e6fa Security Options: seccomp Profile: default Kernel Version: 3.10.0-514.el7.x86_64 Operating System: Red Hat Enterprise Linux Server 7.3 (Maipo) OSType: linux Architecture: x86_64 CPUs: 4 Total Memory: 7.549 GiB Name: bogon ID: SZAV:QNS7:DI5C:RYIP:LM5Y:QCF3:KHS5:6KHD:XHC2:2KGF:Y3KI:CWHT Docker Root Dir: /var/lib/docker Debug Mode (client): false Debug Mode (server): false Registry: https://index.docker.io/v1/ Experimental: false Insecure Registries: 192.168.0.102:5000 registry:5000 127.0.0.0/8 Registry Mirrors: http://192.168.0.102:5000 Live Restore Enabled: false
2.Docker swarm接收外部请求的处理流程
Docker swarm利用ingress overlay网络处理外部请求,并利用IPVS做外部负载均衡,具体可参考 docker swarm-服务发现与负载均衡
2.1 IPVS有三种NAT、IP Tunneling和 DR,
- NAT工作模式,简单来说就是传统的NAT,进出流量都需要经过调度器,调度器会选择一个目的服务器,将进入流量的目标IP改写为负载均衡到的目标服务器,同时源IP地址也会改为调度器IP地址。机制简单,但限制大,IPVS需要维护每个映射关系,而且进出入流量都需要经过调度器,实际上这个会成为瓶颈。
- TUN工作模式,即IP Tunneling模式。这种模式中,调度器将进入的包重新包成一个IP包,然后发送给选定的目的服务器,目的服务器处理后,直接将应答发送给客户(当然该重新封装的报文的源IP地址还是要填成调度器的)。
- DR工作模式,即Direct Routing模式。这种模式中,调度器直接重写进入包的mac地址,将其改为选定的目标服务器的mac地址,这样就可以到达服务器。但这样的话需要要求IPVS服务器需要和真实服务器在同一局域网内,且真实服务器必须有真实网卡(这样重写了mac地址的报文才可以才可以到达该服务器)
2.2 docker ingress网络的选择
Docker ingress为了满足所有节点都可以接收请求,即便是没有相应服务的节点也要能提供服务(routing mesh),采用了NAT模式,请求进入ingress网络后,会把源地址修改成收到请求的节点的ingress 网络的IP地址,默认情况下是10.0.255.*,再找到具体服务所在的节点,把请求转发过去,把目标地址改成真正服务对应的IP,返回时也是先返回到接收请求的节点再返回到客户端,所有在docker swarm里面的服务获取不到真实的client ip
3.解决方法
这个问题在docker的issues中有很多人讨论,具体可以看 Unable to retrieve user's IP address in docker swarm mode #25526,幸运的是docker 在docker engine 1.3.0中追加了一个新的特性 --publish可以指定mode=host,用来绕过ingress网络,根据这个特性,我们项目的解决思路是在所有的服务外层利用nginx或者zuul等做一个反向代理,并且这个代理不能用 docker stack deploy的形式启动,要用server create的方式启动,并且要指定publish的mode=host。compose文件在3.2版本中才加入了ports的新语法来支持这个host模式,需要docker engine在17.04.0及以上版本才能支持。
我们项目里面用的是zuul,具体的启动命令是
docker service create --name zuu-server --publish "mode=host,target=8080,published=8080" --mode global --network mynet image/zuul:1.0.0
有亮点需要说明:
- 因为用了mode=host应用就不会利用ingress网络,所以服务对应的task在哪个节点上那个节点才能接收外部请求,为了还要满足在任意节点上都可以访问到服务,所以把发布模式定义成了global
- zuul接到请求后单纯的根据配置把请求转发到具体的服务,为了能在zuul中能发现其他的服务,zuul还必须在自建的mynet网络里面
zuul配置片段:
zuul: sensitive-headers: Cookies add-host-header: true forceOriginalQueryStringencoding: true routes: portal: path: /portal/** serviceId: portal stripPrefix: false .....
除了用service create方式外,还可以用docker run的方式绕过ingress网络,方法如下
- 创建attachable overlay network:docker network create --attachable --driver overlay --subnet 10.0.0.1/16 mynet
- docker run -d -p 8080:8080 --name zuuServer --net mynet image/zuul:1.0.0
具体应用服务获取客户端IP的代码片段:
String ips = request.getHeader("x-forwarded-for"); if (StringUtils.isEmpty(ips)) { ips = request.getHeader("Proxy-Client-IP"); } if (StringUtils.isEmpty(ips)) { ips = request.getHeader("WL-Proxy-Client-IP"); } if (StringUtils.isEmpty(ips)) { ips = request.getRemoteAddr(); } String ip = Arrays.stream(ips.split(",")).filter(ip-> ! StringUtils. equalsIgnoreCase("unkonwn",ip)).findFirst().get();
如果用nginx,
docker service update nginx_proxy \ --publish-rm 80 \ --publish-add "mode=host,published=80,target=80" \ --publish-rm 443 \ --publish-add "mode=host,published=443,target=443"
配置参照 this working nginx configuration