[size=medium]1.问题概述[/size]
最近在项目中遇到一个问题,因为业务要求,需要在服务中获取到客户端IP,但是在项目开发部署过程中发现利用[color=gray]java -jar ***.jar[/color]单独运行服务,或者打成镜像再[color=gray]docker run[/color]启动的服务都可以正确的获取到client IP,但是当采用[color=gray]docker stack deploy[/color]发布到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
[size=medium]2.Docker swarm接收外部请求的处理流程[/size]
Docker swarm利用ingress overlay网络处理外部请求,并利用IPVS做外部负载均衡,具体可参考[url=http://fengyilin.iteye.com/blog/2400949]docker swarm-服务发现与负载均衡[/url]
2.1 IPVS有三种NAT、IP Tunneling和 DR,
[list]
[*]NAT工作模式,简单来说就是传统的NAT,进出流量都需要经过调度器,调度器会选择一个目的服务器,将进入流量的目标IP改写为负载均衡到的目标服务器,同时源IP地址也会改为调度器IP地址。机制简单,但限制大,IPVS需要维护每个映射关系,而且进出入流量都需要经过调度器,实际上这个会成为瓶颈。
[*]TUN工作模式,即IP Tunneling模式。这种模式中,调度器将进入的包重新包成一个IP包,然后发送给选定的目的服务器,目的服务器处理后,直接将应答发送给客户(当然该重新封装的报文的源IP地址还是要填成调度器的)。
[*]DR工作模式,即Direct Routing模式。这种模式中,调度器直接重写进入包的mac地址,将其改为选定的目标服务器的mac地址,这样就可以到达服务器。但这样的话需要要求IPVS服务器需要和真实服务器在同一局域网内,且真实服务器必须有真实网卡(这样重写了mac地址的报文才可以才可以到达该服务器)
[/list]
2.2 docker ingress网络的选择
Docker ingress为了满足所有节点都可以接收请求,即便是没有相应服务的节点也要能提供服务(routing mesh),采用了NAT模式,请求进入ingress网络后,会把源地址修改成收到请求的节点的ingress 网络的IP地址,默认情况下是10.0.255.*,再找到具体服务所在的节点,把请求转发过去,把目标地址改成真正服务对应的IP,返回时也是先返回到接收请求的节点再返回到客户端,所有在docker swarm里面的服务获取不到真实的client ip
[size=medium]3.解决方法[/size]
这个问题在docker的issues中有很多人讨论,具体可以看[url=https://github.com/moby/moby/issues/25526]Unable to retrieve user's IP address in docker swarm mode #25526[/url],幸运的是docker 在docker engine 1.3.0中追加了一个新的特性 --publish可以指定mode=host,用来绕过ingress网络,根据这个特性,我们项目的解决思路是在所有的服务外层利用nginx或者zuul等做一个反向代理,并且这个代理不能用[color=gray]docker stack deploy[/color]的形式启动,要用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
有亮点需要说明:
[list]
[*]因为用了mode=host应用就不会利用ingress网络,所以服务对应的task在哪个节点上那个节点才能接收外部请求,为了还要满足在任意节点上都可以访问到服务,所以把发布模式定义成了global
[*]zuul接到请求后单纯的根据配置把请求转发到具体的服务,为了能在zuul中能发现其他的服务,zuul还必须在自建的mynet网络里面
[/list]
zuul配置片段:
zuul:
sensitive-headers: Cookies
add-host-header: true
forceOriginalQueryStringencoding: true
routes:
portal:
path: /portal/**
serviceId: portal
stripPrefix: false
.....
除了用service create方式外,还可以用docker run的方式绕过ingress网络,方法如下
[list]
[*]创建attachable overlay network:[color=gray]docker network create --attachable --driver overlay --subnet 10.0.0.1/16 mynet[/color]
[*]docker run -d -p 8080:8080 --name zuuServer --net mynet image/zuul:1.0.0
[/list]
具体应用服务获取客户端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"
配置参照 [url=https://github.com/banianhost/remux/blob/master/app/nginx.conf#L57]this working nginx configuration[/url]