docker swarm获取客户端IP

1.问题概述
最近在项目中遇到一个问题,因为业务要求,需要在服务中获取到客户端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

猜你喜欢

转载自fengyilin.iteye.com/blog/2401156