标签 Go 下的文章

Go 1.6中值得关注的几个变化

北京时间2016年2月18日凌晨,在Go 1.5发布 半年后,Go 1.6正式Release 了。与Go 1.5的“惊天巨变”(主要指Go自举)相比,Go 1.6的Change 算是很小的了,当然这也与Go 1.6的dev cycle过于短暂有关。但Go社区对此次发布却甚是重视,其热烈程度甚至超出了Go 1.5。在Dave Cheney的倡导 下,Gophers们在全球各地举行了Go 1.6 Release Party。 Go Core Team也在Reddit上开了一个AMA – Ask Me Anything,RobPike、Russ Cox(Rsc)、Bradfitz等Go大神齐上阵,对广大Gophers们在24hour内的问题有问必答。

言归正传,我们来看看Go 1.6中哪些变化值得我们关注。不过在说变化之前,我们先提一嘴Go 1.6没变的,那就是Go语言的language specification,依旧保持Go 1兼容不变。预计在未来的几个stable release版本中,我们也不会看到Go language specification有任何改动。

一、cgo

cgo的变化在于:
1、定义了在Go code和C code间传递Pointer,即C code与Go garbage collector共存的rules和restriction;
2、在runtime加入了对违规传递的检查,检查的开关和力度由GODEBUG=cgocheck=1[0,2]控制。1是默认;0是关闭检 查;2是更全面彻底但代价更高的检查。

这个Proposal是由Ian Lance Taylor提出的。大致分为两种情况:

(一) Go调用C Code时

Go调用C Code时,Go传递给C Code的Go Pointer所指的Go Memory中不能包含任何指向Go Memory的Pointer。我们分为几种情况来探讨一下:

1、传递一个指向Struct的指针

//cgo1_struct.go
package main

/*
#include <stdio.h>
struct Foo{
    int a;
    int *p;
};

void plusOne(struct Foo *f) {
    (f->a)++;
    *(f->p)++;
}
*/
import "C"
import "unsafe"
import "fmt"

func main() {
    f := &C.struct_Foo{}
    f.a = 5
    f.p = (*C.int)((unsafe.Pointer)(new(int)))
    //f.p = &f.a

    C.plusOne(f)
    fmt.Println(int(f.a))
}

从cgo1_struct.go代码中可以看到,Go code向C code传递了一个指向Go Memory(Go分配的)指针f,但f指向的Go Memory中有一个指针p指向了另外一处Go Memory: new(int),我们来run一下这段代码:

$go run cgo1_struct.go
# command-line-arguments
./cgo1_struct.go:12:2: warning: expression result unused [-Wunused-value]
panic: runtime error: cgo argument has Go pointer to Go pointer

goroutine 1 [running]:
panic(0x4068400, 0xc82000a110)
    /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
main.main()
    /Users/tony/test/go/go16/cgo/cgo1_struct.go:24 +0xb9
exit status 2

代码出现了Panic,并提示:“cgo argument has Go pointer to Go pointer”。我们的代码违背了Cgo Pointer传递规则,即便让f.p指向struct自身内存也是不行的,比如f.p = &f.a。

2、传递一个指向struct field的指针

按照rules中的说明,如果传递的是一个指向struct field的指针,那么”Go Memory”专指这个field所占用的内存,即便struct中有其他field指向其他Go memory也不打紧:

//cgo1_structfield.go
package main

/*
#include <stdio.h>
struct Foo{
    int a;
    int *p;
};

void plusOne(int *i) {
    (*i)++;
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    f := &C.struct_Foo{}
    f.a = 5
    f.p = (*C.int)((unsafe.Pointer)(new(int)))

    C.plusOne(&f.a)
    fmt.Println(int(f.a))
}

上述程序的运行结果:

$go run cgo1_structfield.go
6

3、传递一个指向slice or array中的element的指针

和传递struct field不同,传递一个指向slice or array中的element的指针时,需要考虑的Go Memory的范围不仅仅是这个element,而是整个Array或整个slice背后的underlying array所占用的内存区域,要保证这个区域内不包含指向任意Go Memory的指针。我们来看代码示例:

//cgo1_sliceelem.go
package main

/*
#include <stdio.h>
void plusOne(int **i) {
    (**i)++;
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    sl := make([]*int, 5)
    var a int = 5
    sl[1] = &a
    C.plusOne((**C.int)((unsafe.Pointer)(&sl[0])))
    fmt.Println(sl[0])
}

从这个代码中,我们看到我们传递的是slice的第一个element的地址,即&sl[0]。我们并未给sl[0]赋值,但sl[1] 被赋值为另外一块go memory的address(&a),当我们将&sl[0]传递给plusOne时,执行结果如下:

$go run cgo1_sliceelem.go
panic: runtime error: cgo argument has Go pointer to Go pointer

goroutine 1 [running]:
panic(0x40dbac0, 0xc8200621d0)
    /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
main.main()
    /Users/tony/test/go/go16/cgo/cgo1_sliceelem.go:19 +0xe4
exit status 2

由于违背规则,因此runtime panic了。

(二) C调用Go Code时

1、C调用的Go函数不能返回指向Go分配的内存的指针

我们看下面例子:

//cgo2_1.go

package main

// extern int* goAdd(int, int);
//
// static int cAdd(int a, int b) {
//     int *i = goAdd(a, b);
//     return *i;
// }
import "C"
import "fmt"

//export goAdd
func goAdd(a, b C.int) *C.int {
    c := a + b
    return &c
}

func main() {
    var a, b int = 5, 6
    i := C.cAdd(C.int(a), C.int(b))
    fmt.Println(int(i))
}

可以看到:goAdd这个Go函数返回了一个指向Go分配的内存(&c)的指针。运行上述代码,结果如下:

$go run cgo2_1.go
panic: runtime error: cgo result has Go pointer

goroutine 1 [running]:
panic(0x40dba40, 0xc82006e1c0)
    /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
main._cgoexpwrap_872b2f2e7532_goAdd.func1(0xc820049d98)
    command-line-arguments/_obj/_cgo_gotypes.go:64 +0x3a
main._cgoexpwrap_872b2f2e7532_goAdd(0x600000005, 0xc82006e19c)
    command-line-arguments/_obj/_cgo_gotypes.go:66 +0x89
main._Cfunc_cAdd(0x600000005, 0x0)
    command-line-arguments/_obj/_cgo_gotypes.go:45 +0x41
main.main()
    /Users/tony/test/go/go16/cgo/cgo2_1.go:20 +0x35
exit status 2

2、Go code不能在C分配的内存中存储指向Go分配的内存的指针

下面的例子模拟了这一情况:

//cgo2_2.go
package main

// #include <stdlib.h>
// extern void goFoo(int**);
//
// static void cFoo() {
//     int **p = malloc(sizeof(int*));
//     goFoo(p);
// }
import "C"

//export goFoo
func goFoo(p **C.int) {
    *p = new(C.int)
}

func main() {
    C.cFoo()
}

不过针对此例,默认的GODEBUG=cgocheck=1偏是无法check出问题。我们将GODEBUG=cgocheck改为=2试试:

$GODEBUG=cgocheck=2 go run cgo2_2.go
write of Go pointer 0xc82000a0f8 to non-Go memory 0x4300000
fatal error: Go pointer stored into non-Go memory

runtime stack:
runtime.throw(0x4089800, 0x24)
    /Users/tony/.bin/go16/src/runtime/panic.go:530 +0x90
runtime.cgoCheckWriteBarrier.func1()
    /Users/tony/.bin/go16/src/runtime/cgocheck.go:44 +0xae
runtime.systemstack(0x7fff5fbff8c0)
    /Users/tony/.bin/go16/src/runtime/asm_amd64.s:291 +0x79
runtime.mstart()
    /Users/tony/.bin/go16/src/runtime/proc.go:1048
... ...
goroutine 17 [syscall, locked to thread]:
runtime.goexit()
    /Users/tony/.bin/go16/src/runtime/asm_amd64.s:1998 +0x1
exit status 2

果真runtime panic: write of Go pointer 0xc82000a0f8 to non-Go memory 0×4300000

二、HTTP/2

HTTP/2原本是bradfitz维护的x项目,之前位于golang.org/x/net/http2包中,Go 1.6无缝合入Go标准库net/http包中。并且当你你使用https时,client和server端将自动默认使用HTTP/2协议。

HTTP/2与HTTP1.x协议不同在于其为二进制协议,而非文本协议,性能自是大幅提升。HTTP/2标准已经发布,想必未来若干年将大行其道。

HTTP/2较为复杂,这里不赘述,后续maybe会单独写一篇GO和http2的文章说明。

三、Templates

由于不开发web,templates我日常用的很少。这里粗浅说说templates增加的两个Feature

trim空白字符

Go templates的空白字符包括:空格、水平tab、回车和换行符。在日常编辑模板时,这些空白尤其难于处理,由于是对beatiful format和code readabliity有“强迫症”的同学,更是在这方面话费了不少时间。

Go 1.6提供了{{-和-}}来帮助大家去除action前后的空白字符。下面的例子很好的说明了这一点:

//trimwhitespace.go
package main

import (
    "log"
    "os"
    "text/template"
)

var items = []string{"one", "two", "three"}

func tmplbefore15() {
    var t = template.Must(template.New("tmpl").Parse(`
    <ul>
    {{range . }}
        <li>{{.}}</li>
    {{end }}
    </ul>
    `))

    err := t.Execute(os.Stdout, items)
    if err != nil {
        log.Println("executing template:", err)
    }
}

func tmplaftergo16() {
    var t = template.Must(template.New("tmpl").Parse(`
    <ul>
    {{range . -}}
        <li>{{.}}</li>
    {{end -}}
    </ul>
    `))

    err := t.Execute(os.Stdout, items)
    if err != nil {
        log.Println("executing template:", err)
    }
}

func main() {
    tmplbefore15()
    tmplaftergo16()
}

这个例子的运行结果:

$go run trimwhitespace.go

    <ul>

        <li>one</li>

        <li>two</li>

        <li>three</li>

    </ul>

    <ul>
    <li>one</li>
    <li>two</li>
    <li>three</li>
    </ul>

block action

block action提供了一种在运行时override已有模板形式的能力。

package main

import (
    "log"
    "os"
    "text/template"
)

var items = []string{"one", "two", "three"}

var overrideItemList = `
{{define "list" -}}
    <ul>
    {{range . -}}
        <li>{{.}}</li>
    {{end -}}
    </ul>
{{end}}
`

var tmpl = `
    Items:
    {{block "list" . -}}
    <ul>
    {{range . }}
        <li>{{.}}</li>
    {{end }}
    </ul>
    {{end}}
`

var t *template.Template

func init() {
    t = template.Must(template.New("tmpl").Parse(tmpl))
}

func tmplBeforeOverride() {
    err := t.Execute(os.Stdout, items)
    if err != nil {
        log.Println("executing template:", err)
    }
}

func tmplafterOverride() {
    t = template.Must(t.Parse(overrideItemList))
    err := t.Execute(os.Stdout, items)
    if err != nil {
        log.Println("executing template:", err)
    }
}

func main() {
    fmt.Println("before override:")
    tmplBeforeOverride()
    fmt.Println("after override:")
    tmplafterOverride()
}

原模板tmpl中通过block action定义了一处名为list的内嵌模板锚点以及初始定义。后期运行时通过re-parse overrideItemList达到修改模板展示形式的目的。

上述代码输出结果:

$go run blockaction.go
before override:

    Items:
    <ul>

        <li>one</li>

        <li>two</li>

        <li>three</li>

    </ul>

after override:

    Items:
    <ul>
    <li>one</li>
    <li>two</li>
    <li>three</li>
    </ul>

四、Runtime

降低大内存使用时的GC latency

Go 1.5.x用降低一些吞吐量的代价换取了10ms以下的GC latency。不过针对Go 1.5,官方给出的benchmark图中,内存heap size最多20G左右。一旦超过20G,latency将超过10ms,也许会线性增长。
在Go 1.6中,官方给出的benchmark图中当内存heap size在200G时,GC latency依旧可以稳定在10ms;在heap size在20G以下时,latency降到了6ms甚至更小。

panic info

Go 1.6之前版本,一旦程序以panic方式退出,runtime便会将所有goroutine的stack信息打印出来:

$go version
go version go1.5.2 darwin/amd64
[ ~/test/go/go16/runtime]$go run panic.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x0 pc=0x20d5]

goroutine 1 [running]:
main.main()
    /Users/tony/test/go/go16/runtime/panic.go:19 +0x95

goroutine 4 [select (no cases)]:
main.main.func1(0x8200f40f0)
    /Users/tony/test/go/go16/runtime/panic.go:13 +0x26
created by main.main
    /Users/tony/test/go/go16/runtime/panic.go:14 +0x72
... ...

而Go 1.6后,Go只会打印正在running的goroutine的stack信息,因此它才是最有可能造成panic的真正元凶:

go 1.6:
$go run panic.go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x0 pc=0x20d5]

goroutine 1 [running]:
panic(0x61e80, 0x8200ee0c0)
    /Users/tony/.bin/go16/src/runtime/panic.go:464 +0x3e6
main.main()
    /Users/tony/test/go/go16/runtime/panic.go:19 +0x95
exit status 2

map race detect

Go原生的map类型是goroutine-unsafe的,长久以来,这给很多Gophers带来了烦恼。这次Go 1.6中Runtime增加了对并发访问map的检测以降低gopher们使用map时的心智负担。

这里借用了Francesc Campoy在最近一期”The State of Go”中的示例程序:

package main

import "sync"

func main() {
    const workers = 100

    var wg sync.WaitGroup
    wg.Add(workers)
    m := map[int]int{}
    for i := 1; i <= workers; i++ {
        go func(i int) {
            for j := 0; j < i; j++ {
                m[i]++
            }
            wg.Done()
        }(i)
    }
    wg.Wait()
}

执行结果:

$ go run map.go
fatal error: concurrent map writes
fatal error: concurrent map writes
... ...

这里在双核i5 mac air下亲测时,发现当workers=2,3,4均不能检测出race。当workers >= 5时可以检测到。

五、其他

手写parser替代yacc生成的parser

这个变化对Gopher们是透明的,但对于Go compiler本身却是十分重要的。

Robert Riesemer在Go 1.6代码Freezing前commit了手写Parser,以替代yacc生成的parser。在AMA上RobPike给出了更换Parser的些许理由:
1、Go compiler可以少维护一个yacc工具,这样更加cleaner;
2、手写Parser在性能上可以快那么一点点。

Go 1.6中GO15VENDOREXPERIMENT将默认开启

根据当初在Go 1.5中引入vendor时的计划,Go 1.6中GO15VENDOREXPERIMENT将默认开启。这显然会导致一些不兼容的情况出现:即如果你的代码在之前并未使用vendor机制,但目录组织中有vendor目录。Go Core team给出的解决方法就是删除vendor目录或改名。

遗留问题是否解决

Go 1.5发布后,曾经发现两个问题,直到Go 1.5.3版本发布也未曾解决,那么Go 1.6是否解决了呢?我们来验证一下。

internal问题

该问题的具体细节可参看我在go github上提交的issue 12217,我在自己的experiments中提交了问题的验证环境代码,这次我们使用Go 1.6看看internal问题是否还存在:

$cd $GOPATH/src/github.com/bigwhite/experiments/go15-internal-issue-12217
$cd otherpkg/
$go build main.go
package main
    imports github.com/bigwhite/experiments/go15-internal-issue-12217/mypkg/internal/foo: use of internal package not allowed

这回go compiler给出了error,而不是像之前版本那样顺利编译通过。看来这个问题是fix掉了。

GOPATH之外vendor机制是否起作用的问题

我们先建立实验环境:

$tree
.
└── testvendor
    └── src
        └── proj1
            ├── main.go
            └── vendor
                └── github.com
                    └── bigwhite
                        └── foo
                            └── foolib.go

进入proj1,build main.go

go build main.go
main.go:3:8: cannot find package "github.com/bigwhite/foo" in any of:
    /Users/tony/.bin/go16/src/github.com/bigwhite/foo (from $GOROOT)
    /Users/tony/Test/GoToolsProjects/src/github.com/bigwhite/foo (from $GOPATH)

go 1.6编译器没有关注同路径下的vendor目录,build失败。

我们设置GOPATH=xxx/testvendor后,再来build:

$export GOPATH=~/Test/go/go16/others/testvendor
$go run main.go
Hello from temp vendor

这回编译运行ok。

由此看来,Go 1.6 vendor在GOPATH外依旧不生效。

六、小结

Go 1.6标准库细微变化还是有很多的,在Go 1.6 Release Notes中可细细品味。

Go 1.6的编译速度、编译出的程序的运行性能与Go 1.5.x也大致无二异。

另外本文实现环境如下:

go version go1.6 darwin/amd64
Darwin tonydeair-2.lan 13.1.0 Darwin Kernel Version 13.1.0: Thu Jan 16 19:40:37 PST 2014; root:xnu-2422.90.20~2/RELEASE_X86_64 x86_64

实验代码可在这里下载。

理解Docker跨多主机容器网络

Docker 1.9 出世前,跨多主机的容器通信方案大致有如下三种:

1、端口映射

将宿主机A的端口P映射到容器C的网络空间监听的端口P’上,仅提供四层及以上应用和服务使用。这样其他主机上的容器通过访问宿主机A的端口P实 现与容器C的通信。显然这个方案的应用场景很有局限。

2、将物理网卡桥接到虚拟网桥,使得容器与宿主机配置在同一网段下

在各个宿主机上都建立一个新虚拟网桥设备br0,将各自物理网卡eth0桥接br0上,eth0的IP地址赋给br0;同时修改Docker daemon的DOCKER_OPTS,设置-b=br0(替代docker0),并限制Container IP地址的分配范围为同物理段地址(–fixed-cidr)。重启各个主机的Docker Daemon后,处于与宿主机在同一网段的Docker容器就可以实现跨主机访问了。这个方案同样存在局限和扩展性差的问题:比如需将物理网段的地址划分 成小块,分布到各个主机上,防止IP冲突;子网划分依赖物理交换机设置;Docker容器的主机地址空间大小依赖物理网络划分等。

3、使用第三方的基于SDN的方案:比如 使用Open vSwitch – OVSCoreOSFlannel 等。

关于这些第三方方案的细节大家可以参考O’Reilly的《Docker Cookbook》 一书。

Docker在1.9版本中给大家带来了一种原生的跨多主机容器网络的解决方案,该方案的实质是采用了基于VXLAN 的覆盖网技术。方案的使用有一些前提条件:

1、Linux Kernel版本 >= 3.16;
2、需要一个外部Key-value Store(官方例子中使用的是consul);
3、各物理主机上的Docker Daemon需要一些特定的启动参数;
4、物理主机允许某些特定TCP/UDP端口可用。

本文将带着大家一起利用Docker 1.9.1创建一个跨多主机容器网络,并分析基于该网络的容器间通信原理。

一、实验环境建立

1、升级Linux Kernel

由于实验环境采用的是Ubuntu 14.04 server amd64,其kernel版本不能满足建立跨多主机容器网络要求,因此需要对内核版本进行升级。在Ubuntu的内核站点 下载3.16.7 utopic内核 的三个文件:

linux-headers-3.16.7-031607_3.16.7-031607.201410301735_all.deb
linux-image-3.16.7-031607-generic_3.16.7-031607.201410301735_amd64.deb
linux-headers-3.16.7-031607-generic_3.16.7-031607.201410301735_amd64.deb

在本地执行下面命令安装:

sudo dpkg -i linux-headers-3.16.7-*.deb linux-image-3.16.7-*.deb

需要注意的是:kernel mainline上的3.16.7内核没有带linux-image-extra,也就没有了aufs 的驱动,因此Docker Daemon将不支持默认的存储驱动:–storage-driver=aufs,我们需要将storage driver更换为devicemapper

内核升级是一个有风险的操作,并且是否能升级成功还要看点“运气”:我的两台刀片服务器,就是一台升级成功一台升级失败(一直报网卡问题)。

2、升级Docker到1.9.1版本

从国内下载Docker官方的安装包比较慢,这里利用daocloud.io提供的方法 快速安装Docker最新版本:

$ curl -sSL https://get.daocloud.io/docker | sh

3、拓扑

本次的跨多主机容器网络基于两台在不同子网网段内的物理机承载,基于物理机搭建,目的是简化后续网络通信原理分析。

拓扑图如下:

img{512x368}

二、跨多主机容器网络搭建

1、创建consul 服务

考虑到kv store在本文并非关键,仅作跨多主机容器网络创建启动的前提条件之用,因此仅用包含一个server节点的”cluster”。

参照拓扑图,我们在10.10.126.101上启动一个consul,关于consul集群以及服务注册、服务发现等细节可以参考我之前的一 篇文章

$./consul -d agent -server -bootstrap-expect 1 -data-dir ./data -node=master -bind=10.10.126.101 -client=0.0.0.0 &

2、修改Docker Daemon DOCKER_OPTS参数

前面提到过,通过Docker 1.9创建跨多主机容器网络需要重新配置每个主机节点上的Docker Daemon的启动参数:

ubuntu系统这个配置在/etc/default/docker下:

DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4  -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-advertise eth0:2375 --cluster-store consul://10.10.126.101:8500/network --storage-driver=devicemapper"

这里多说几句:

-H(或–host)配置的是Docker client(包括本地和远程的client)与Docker Daemon的通信媒介,也是Docker REST api的服务端口。默认是/var/run/docker.sock(仅用于本地),当然也可以通过tcp协议通信以方便远程Client访问,就像上面 配置的那样。非加密网通信采用2375端口,而TLS加密连接则用2376端口。这两个端口已经申请在IANA注册并获批,变成了知名端口。-H可以配置多个,就像上面配置的那样。 unix socket便于本地docker client访问本地docker daemon;tcp端口则用于远程client访问。这样一来:docker pull ubuntu,走docker.sock;而docker -H 10.10.126.101:2375 pull ubuntu则走tcp socket。

–cluster-advertise 配置的是本Docker Daemon实例在cluster中的地址;
–cluster-store配置的是Cluster的分布式KV store的访问地址;

如果你之前手工修改过iptables的规则,建议重启Docker Daemon之前清理一下iptables规则:sudo iptables -t nat -F, sudo iptables -t filter -F等。

3、启动各节点上的Docker Daemon

以10.10.126.101为例:

$ sudo service docker start

$ ps -ef|grep docker
root      2069     1  0 Feb02 ?        00:01:41 /usr/bin/docker -d --dns 8.8.8.8 --dns 8.8.4.4 --storage-driver=devicemapper -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-advertise eth0:2375 --cluster-store consul://10.10.126.101:8500/network

启动后iptables的nat, filter规则与单机Docker网络初始情况并无二致。

101节点上初始网络driver类型:
$docker network ls
NETWORK ID          NAME                DRIVER
47e57d6fdfe8        bridge              bridge
7c5715710e34        none                null
19cc2d0d76f7        host                host

4、创建overlay网络net1和net2

在101节点上,创建net1:

$ sudo docker network create -d overlay net1

在71节点上,创建net2:

$ sudo docker network create -d overlay net2

之后无论在71节点还是101节点,我们查看当前网络以及驱动类型都是如下结果:

$ docker network ls
NETWORK ID          NAME                DRIVER
283b96845cbe        net2                overlay
da3d1b5fcb8e        net1                overlay
00733ecf5065        bridge              bridge
71f3634bf562        none                null
7ff8b1007c09        host                host

此时,iptables规则也并无变化。

5、启动两个overlay net下的containers

我们分别在net1和net2下面启动两个container,每个节点上各种net1和net2的container各一个:

101:
sudo docker run -itd --name net1c1 --net net1 ubuntu:14.04
sudo docker run -itd --name net2c1 --net net2 ubuntu:14.04

71:
sudo docker run -itd --name net1c2 --net net1 ubuntu:14.04
sudo docker run -itd --name net2c2 --net net2 ubuntu:14.04

启动后,我们就得到如下网络信息(容器的ip地址可能与前面拓扑图中的不一致,每次容器启动ip地址都可能变化):

net1:
    net1c1 - 10.0.0.7
    net1c2 - 10.0.0.5

net2:
    net2c1 - 10.0.0.4
    net2c2 -  10.0.0.6

6、容器连通性

在net1c1中,我们来看看其到net1和net2的连通性:

root@021f14bf3924:/# ping net1c2
PING 10.0.0.5 (10.0.0.5) 56(84) bytes of data.
64 bytes from 10.0.0.5: icmp_seq=1 ttl=64 time=0.670 ms
64 bytes from 10.0.0.5: icmp_seq=2 ttl=64 time=0.387 ms
^C
--- 10.0.0.5 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.387/0.528/0.670/0.143 ms

root@021f14bf3924:/# ping 10.0.0.4
PING 10.0.0.4 (10.0.0.4) 56(84) bytes of data.
^C
--- 10.0.0.4 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1008ms

可见,net1中的容器是互通的,但net1和net2这两个overlay net之间是隔离的。

三、跨多主机容器网络通信原理

在“单机容器网络”一文中,我们说过容器间的通信以及容器到外部网络的通信是通过docker0网桥并结合iptables实现的。那么在上面已经建立的跨多主机容器网络里,容器的通信又是如何实现的呢?下面我们一起来理解一下。注意:有了单机容器网络基础后,这里很多网络细节就不再赘述了。

我们先来看看,在net1下的容器的网络配置,以101上的net1c1容器为例:

$ sudo docker attach net1c1

root@021f14bf3924:/# ip route
default via 172.19.0.1 dev eth1
10.0.0.0/24 dev eth0  proto kernel  scope link  src 10.0.0.4
172.19.0.0/16 dev eth1  proto kernel  scope link  src 172.19.0.2

root@021f14bf3924:/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
8: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether 02:42:0a:00:00:04 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.4/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aff:fe00:4/64 scope link
       valid_lft forever preferred_lft forever
10: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.19.0.2/16 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe13:2/64 scope link
       valid_lft forever preferred_lft forever

可以看出net1c1有两个网口:eth0(10.0.0.4)和eth1(172.19.0.2);从路由表来看,目的地址在172.19.0.0/16范围内的,走eth1;目的地址在10.0.0.0/8范围内的,走eth0。

我们跳出容器,回到主机网络范畴:

在101上:
$ ip a
... ...
5: docker_gwbridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
    link/ether 02:42:52:35:c9:fc brd ff:ff:ff:ff:ff:ff
    inet 172.19.0.1/16 scope global docker_gwbridge
       valid_lft forever preferred_lft forever
    inet6 fe80::42:52ff:fe35:c9fc/64 scope link
       valid_lft forever preferred_lft forever
6: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
    link/ether 02:42:4b:70:68:9a brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 scope global docker0
       valid_lft forever preferred_lft forever
11: veth26f6db4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP
    link/ether b2:32:d7:65:dc:b2 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::b032:d7ff:fe65:dcb2/64 scope link
       valid_lft forever preferred_lft forever
16: veth54881a0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP
    link/ether 9e:45:fa:5f:a0:15 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::9c45:faff:fe5f:a015/64 scope link
       valid_lft forever preferred_lft forever

我们看到除了我们熟悉的docker0网桥外,还多出了一个docker_gwbridge网桥:

$ brctl show
bridge name    bridge id        STP enabled    interfaces
docker0        8000.02424b70689a    no
docker_gwbridge        8000.02425235c9fc    no        veth26f6db4
                            veth54881a0

并且从brctl的输出结果来看,两个veth都桥接在docker_gwbridge上,而不是docker0上;docker0在跨多主机容器网络中并没有被用到。docker_gwbridge替代了docker0,用来实现101上隶属于net1网络或net2网络中容器间的通信以及容器到外部的通信,其职能就和单机容器网络中docker0一样。

但位于不同host且隶属于net1的两个容器net1c1和net1c2间的通信显然并没有通过docker_gwbridge完成,从net1c1路由表来看,当net1c1 ping net1c2时,消息是通过eth0,即10.0.0.4这个ip出去的。从host的视角,net1c1的eth0似乎没有网络设备与之连接,那网络通信是如何完成的呢?

这一切是从创建network开始的。前面我们执行docker network create -d overlay net1来创建net1 overlay network,这个命令会创建一个新的network namespace。

我们知道每个容器都有自己的网络namespace,从容器的视角看其网络名字空间,我们能看到网络设备诸如:lo、eth0。这个eth0与主机网络名字空间中的vethx是一个虚拟网卡pair。overlay network也有自己的net ns,而overlay network的net ns与容器的net ns之间也有着一些网络设备对应关系。

我们先来查看一下network namespace的id。为了能利用iproute2工具对network ns进行管理,我们需要做如下操作:

$cd /var/run
$sudo ln -s /var/run/docker/netns netns

这是因为iproute2只能操作/var/run/netns下的net ns,而docker默认的net ns却放在/var/run/docker/netns下。上面的操作成功执行后,我们就可以通过ip命令查看和管理net ns了:

$ sudo ip netns
29170076ddf6
1-283b96845c
5ae976d9dc6a
1-da3d1b5fcb

我们看到在101主机上,有4个已经建立的net ns。我们大胆猜测一下,这四个net ns分别是两个container的net ns和两个overlay network的net ns。从netns的ID格式以及结合下面命令输出结果中的network id来看:

$ docker network ls
NETWORK ID          NAME                DRIVER
283b96845cbe        net2                overlay
da3d1b5fcb8e        net1                overlay
dd84da8e80bf        host                host
3295c22b22b8        docker_gwbridge     bridge
b96e2d8d4068        bridge              bridge
23749ee4292f        none                null

我们大致可以猜测出来:

1-da3d1b5fcb 是 net1的net ns;
1-283b96845c是 net2的net ns;
29170076ddf6和5ae976d9dc6a则分属于两个container的net ns。

由于我们以net1为例,因此下面我们就来分析net1的net ns – 1-da3d1b5fcb。通过ip命令我们可以得到如下结果:

$ sudo ip netns exec 1-da3d1b5fcb ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
    link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.1/24 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::b80a:bfff:fecc:a1e0/64 scope link
       valid_lft forever preferred_lft forever
7: vxlan1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UNKNOWN
    link/ether ea:0c:e0:bc:19:c5 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::e80c:e0ff:febc:19c5/64 scope link
       valid_lft forever preferred_lft forever
9: veth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP
    link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::4b0:c6ff:fe93:25f3/64 scope link
       valid_lft forever preferred_lft forever

$ sudo ip netns exec 1-da3d1b5fcb ip route
10.0.0.0/24 dev br0  proto kernel  scope link  src 10.0.0.1

$ sudo ip netns exec 1-da3d1b5fcb brctl show
bridge name    bridge id        STP enabled    interfaces
br0        8000.06b0c69325f3    no        veth2
                            vxlan1

看到br0、veth2,我们心里终于有了底儿了。我们猜测net1c1容器中的eth0与veth2是一个veth pair,并桥接在br0上,通过ethtool查找veth序号的对应关系可以证实这点:

$ sudo docker attach net1c1
root@021f14bf3924:/# ethtool -S eth0
NIC statistics:
     peer_ifindex: 9

101主机:
$ sudo ip netns exec 1-da3d1b5fcb ip -d link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP
    link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
    bridge
7: vxlan1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br0 state UNKNOWN
    link/ether ea:0c:e0:bc:19:c5 brd ff:ff:ff:ff:ff:ff
    vxlan
9: veth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master br0 state UP
    link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
    veth

可以看到net1c1的eth0的pair peer index为9,正好与net ns 1-da3d1b5fcb中的veth2的序号一致。

那么vxlan1呢?注意这个vxlan1并非是veth设备,在ip -d link输出的信息中,它的设备类型为vxlan。前面说过Docker的跨多主机容器网络是基于vxlan的,这里的vxlan1就是net1这个overlay network的一个 VTEP,即VXLAN Tunnel End Point – VXLAN隧道端点。它是VXLAN网络的边缘设备。VXLAN的相关处理都在VTEP上进行,例如识别以太网数据帧所属的VXLAN、基于 VXLAN对数据帧进行二层转发、封装/解封装报文等。

至此,我们可以大致画出一幅跨多主机网络的原理图:

img{512x368}

如果在net1c1中ping net1c2,数据包的行走路径是怎样的呢?

1、net1c1(10.0.0.4)中ping net1c2(10.0.0.5),根据net1c1的路由表,数据包可通过直连网络到达net1c2。于是arp请求获取net1c2的MAC地址(在vxlan上的arp这里不详述了),得到mac地址后,封包,从eth0发出;
2、eth0桥接在net ns 1-da3d1b5fcb中的br0上,这个br0是个网桥(交换机)虚拟设备,需要将来自eth0的包转发出去,于是将包转给了vxlan设备;这个可以通过arp -a看到一些端倪:

$ sudo ip netns exec 1-da3d1b5fcb arp -a
? (10.0.0.5) at 02:42:0a:00:00:05 [ether] PERM on vxlan1

3、vxlan是个特殊设备,收到包后,由vxlan设备创建时注册的设备处理程序对包进行处理,即进行VXLAN封包(这期间会查询consul中存储的net1信息),将ICMP包整体作为UDP包的payload封装起来,并将UDP包通过宿主机的eth0发送出去。

4、71宿主机收到UDP包后,发现是VXLAN包,根据VXLAN包中的相关信息(比如Vxlan Network Identifier,VNI=256)找到vxlan设备,并转给该vxlan设备处理。vxlan设备的处理程序进行解包,并将UDP中的payload取出,整体通过br0转给veth口,net1c2从eth0收到ICMP数据包,回复icmp reply。

我们可以通过wireshark抓取相关vxlan包,高版本wireshark内置VXLAN协议分析器,可以直接识别和展示VXLAN包,这里安装的是2.0.1版本(注意:一些低版本wireshark不支持VXLAN分析器,比如1.6.7版本):

img{512x368}

关于VXLAN协议的细节,过于复杂,在后续的文章中maybe会有进一步理解。

理解Docker容器端口映射

在”理解Docker单机容器网络“一文中,还有一个Docker容器网络的功能尚未提及,那就是Docker容器的端口映射。即将容器的服务端口P’ 绑定到宿主机的端口P上,最终达到一种效果:外部程序通过宿主机的P端口访问,就像直接访问Docker容器网络内部容器提供的服务一样。

Docker针对端口映射前后有两种方案,一种是1.7版本之前docker-proxy+iptables DNAT的方式;另一种则是1.7版本(及之后)提供的完全由iptables DNAT实现的端口映射。不过在目前docker 1.9.1中,前一种方式依旧是默认方式。但是从Docker 1.7版本起,Docker提供了一个配置项:–userland-proxy,以让Docker用户决定是否启用docker-proxy,默认为true,即启用docker-proxy。本文续前文,继续探讨使用端口映射时Docker容器网络的通信流程。

本文中的实验环境依旧保持与上文相同:docker 1.9.1,ubuntu 12.04宿主机,docker image基于官方ubuntu 14.04 image做的一些软件安装。

一、–userland-proxy=true(defaut)的情况下端口映射

我们首先在实验环境下采用默认的方式进行端口映射,即–userland-proxy=true。

我们来建立一个 新container – container3(172.17.0.4),实现了0.0.0.0:12580 -> container3:12580。

$docker run -it --name container3 -p 12580:12580 dockernetworking/ubuntu:14.04 /bin/bash

这个命令执行后,iptables增加了三条rules:

filter forward链:
Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     tcp  --  !docker0 docker0  0.0.0.0/0            172.17.0.4           tcp dpt:12580

nat output链:
Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DNAT       tcp  --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:12580 to:172.17.0.4:12580

nat postrouting链:

Chain POSTROUTING (policy ACCEPT 24 packets, 1472 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  tcp  --  *      *       172.17.0.4           172.17.0.4           tcp dpt:12580

我们可以看到了一个DNAT target,是在nat output链中,这个是一个关键点。同样是考虑到调试的方便,在这新增的rules前面,增加LOG target,新的iptables导出内容为:

iptables.portmap.stage1.rules

# Generated by iptables-save v1.4.12 on Fri Jan 15 15:31:06 2016
*raw
: PREROUTING ACCEPT [5737658:60554342802]
:OUTPUT ACCEPT [4294004:56674784720]
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
COMMIT
# Completed on Fri Jan 15 15:31:06 2016
# Generated by iptables-save v1.4.12 on Fri Jan 15 15:31:06 2016
*filter
:INPUT ACCEPT [4444190:53498587744]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [4292173:56674165678]
: DOCKER - [0:0]
:FwdId0Od0 - [0:0]
:FwdId0Ond0 - [0:0]
:FwdOd0 - [0:0]
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j FwdOd0
-A FORWARD -i docker0 ! -o docker0 -j FwdId0Ond0
-A FORWARD -i docker0 -o docker0 -j FwdId0Od0
-A OUTPUT ! -s 127.0.0.1/32 -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-PortmapFowardDocker:" --log-level 7
-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j ACCEPT
-A FwdId0Od0 -i docker0 -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Od0:" --log-level 7
-A FwdId0Od0 -i docker0 -o docker0 -j ACCEPT
-A FwdId0Ond0 -i docker0 ! -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Ond0:" --log-level 7
-A FwdId0Ond0 -i docker0 ! -o docker0 -j ACCEPT
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j LOG --log-prefix "[TonyBai]-FwdOd0:" --log-level 7
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
# Completed on Fri Jan 15 15:31:06 2016
# Generated by iptables-save v1.4.12 on Fri Jan 15 15:31:06 2016
*nat
: PREROUTING ACCEPT [24690:5091417]
:INPUT ACCEPT [10942:2271167]
:OUTPUT ACCEPT [7756:523318]
: POSTROUTING ACCEPT [7759:523498]
: DOCKER - [0:0]
:LogNatPostRouting - [0:0]
-A PREROUTING -p icmp -j LOG --log-prefix "[TonyBai]-Enter iptables:" --log-level 7
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterNatInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LogNatPostRouting
-A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-PortmapNatPostRouting:" --log-level 7
-A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j MASQUERADE
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-PortmapNatOutputDocker:" --log-level 7
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 12580 -j DNAT --to-destination 172.17.0.4:12580
-A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix "[TonyBai]-NatPostRouting:" --log-level 7
-A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
COMMIT
# Completed on Fri Jan 15 15:31:06 2016

另外我们可以查看到宿主机中多了一个进程,这就是前面所说的docker-proxy,每增加一个端口映射,宿主机就会多出一个docker-proxy进程:

root      5742  2113  0 08:48 ?        00:00:00 docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 12580 -container-ip 172.17.0.4 -container-port 12580

1、从10.10.126.187访问宿主机(10.10.126.101)的12580端口

10.10.126.187是与101在同一直连网路的主机,我们在其上执行telnet 10.10.126.101 12580。如果container3中有server在监听12580,则建立连接和数据通信(发送一个hello)的过程如下。

【187到101的tcp握手sync包】

101从eth0网卡收到目的地址是自己的sync数据包:

Jan 15 16:04:54 pc-baim kernel: [28410.162828] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.162862] [TonyBai]-NatPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0

由于目的地址就是自己,因此在iptables中走input chain将数据包发给user层:

Jan 15 16:04:54 pc-baim kernel: [28410.162885] [TonyBai]-FilterInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.162900] [TonyBai]-NatInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0

【101回复ack sync包】

101上的用户层是docker-proxy在监听12580端口,当收到sync后,会回复ack sync。由于是user空间自产包,路由后走output链。

Jan 15 16:04:54 pc-baim kernel: [28410.162933] [TonyBai]-RawOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.162948] [TonyBai]-FilterOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=28960 RES=0x00 ACK SYN URGP=0

【187回复ack,101与187握手完成】

187回复握手过程最后的一个ack。这个过程与sync类似:

Jan 15 16:04:54 pc-baim kernel: [28410.163397] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=32618 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.163437] [TonyBai]-FilterInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=32618 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0

重点是接下来发生的事情:101上的docker-proxy向container3上的server程序建立tcp连接!

【host向container3发送sync】

Jan 15 16:04:54 pc-baim kernel: [28410.163863] [TonyBai]-RawOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=5768 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.163901] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=5768 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

我们看到SYN数据包源地址用的是172.17.0.1,不知是否是docker-proxy内部有意选择了网桥的ip。由于是user层发出的包,于是走iptables output链。

【container3回复ack sync】

container3回复ack sync,目的地址是172.17.0.1,host从docker0网卡收到ack sync数据,路由后发现是发给自己的包,于是走input chain.

Jan 15 16:04:54 pc-baim kernel: [28410.164000] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.164026] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=28960 RES=0x00 ACK SYN URGP=0

【host回复ack,host与container3握手完成】

host回复握手过程最后的一个ack。user空间自产数据包,于是走output chain:

Jan 15 16:04:54 pc-baim kernel: [28410.164049] [TonyBai]-RawOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=5769 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.164058] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=5769 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0

【187 在已经建立的连接上发送”hello”】

187发送hello to host,docker-proxy收到hello数据:

Jan 15 16:04:58 pc-baim kernel: [28413.840854] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=32619 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK PSH URGP=0
Jan 15 16:04:58 pc-baim kernel: [28413.840874] [TonyBai]-FilterInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=32619 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK PSH URGP=0

【host返回 ack push】

Jan 15 16:04:58 pc-baim kernel: [28413.840893] [TonyBai]-RawOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=22415 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=227 RES=0x00 ACK URGP=0
Jan 15 16:04:58 pc-baim kernel: [28413.840902] [TonyBai]-FilterOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=22415 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=227 RES=0x00 ACK URGP=0

接下来,docker-proxy将hello从已有连接上转发给container3。

【host转发hello到container3】

Jan 15 16:04:58 pc-baim kernel: [28413.841000] [TonyBai]-RawOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=59 TOS=0x00 PREC=0x00 TTL=64 ID=5770 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK PSH URGP=0
Jan 15 16:04:58 pc-baim kernel: [28413.841026] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=59 TOS=0x00 PREC=0x00 TTL=64 ID=5770 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK PSH URGP=0

【container3回复ack 】

Jan 15 16:04:58 pc-baim kernel: [28413.841101] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=61139 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=227 RES=0x00 ACK URGP=0
Jan 15 16:04:58 pc-baim kernel: [28413.841119] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=61139 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=227 RES=0x00 ACK URGP=0

通信过程到此结束。通过这个过程,我们至少了解到两点:

1、docker-proxy将外部建立在host:12580上的连接上的数据转发到container中,反之亦然,如果container 通过与host已经建立的连接向外发送数据,docker-proxy也会将数据转发给187。
2、通过iptables log输出我们可以看到:为了port map而添加的DNAT和MASQUERADE 并没有被匹配到,也就是说在这个过程中并没有用到DNAT,而是完全依靠docker-proxy做的4层代理。

2、从宿主机上访问10.10.126.101:12580

我们在宿主机本机上访问10.10.126.101:12580,看看这个通信过程与上面的是否有差异。

【与本机12580端口建立连接,发送sync包】

由于是user层发送数据包,因此走iptables output链。

Jan 15 16:40:15 pc-baim kernel: [30532.594545] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=53747 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0

在output链上,匹配到nat output上的规则:

Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    1    60 LOG        tcp  --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:12580 LOG flags 0 level 7 prefix "[TonyBai]-PortmapNatOutputDoc"
    1    60 DNAT       tcp  --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:12580 to:172.17.0.4:12580

于是这里将做一个DNAT,数据包的目的地址10.10.126.101被替换为172.17.0.4。

Jan 15 16:40:15 pc-baim kernel: [30532.594561] [TonyBai]-PortmapNatOutputDoc IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=53747 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0

Jan 15 16:40:15 pc-baim kernel: [30532.594572] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=53747 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0

DNAT后,将按照目的地址做一个重新路由:叫实际路由。消息实际重定向到docker0进行封包发送,sync包直接进入到container3 中。

【container3发送ack sync包】

docker0出来的ack sync 通过input chain送到user空间。这块应该由一个自动un-DNAT,将172.17.0.4自动转回10.10.126.101,但通过iptables日志无法确认这点。

Jan 15 16:40:15 pc-baim kernel: [30532.594615] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 15 16:40:15 pc-baim kernel: [30532.594624] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=28960 RES=0x00 ACK SYN URGP=0

【host发送ack,完成握手】

host回复ack。user层自产包,走output链,看rawoutput,dst依旧是126.101(telnet自然不应该知道 172.17.0.4的存在),但是filter output 前,iptables对该地址自动做了dnat,无需重新进入到nat output链,因为之前已经进过了。在filter output中,我们看到dst ip已经变成了container3的ip地址:

Jan 15 16:40:15 pc-baim kernel: [30532.594637] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=53748 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0
Jan 15 16:40:15 pc-baim kernel: [30532.594643] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=53748 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0

【host发送hello】

这个过程同上,不赘述。

Jan 15 16:40:18 pc-baim kernel: [30535.344921] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=53749 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK PSH URGP=0
Jan 15 16:40:18 pc-baim kernel: [30535.344956] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=53749 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK PSH URGP=0

【container回复ack】

不赘述。

Jan 15 16:40:18 pc-baim kernel: [30535.345027] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=43021 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=227 RES=0x00 ACK URGP=0
Jan 15 16:40:18 pc-baim kernel: [30535.345056] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=43021 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=227 RES=0x00 ACK URGP=0

从这个过程可以看到,在宿主机上访问container的映射端口,通信流程不走docker-proxy,而是直接通过output 的dnat将数据包被直接转给container中的server程序。

3、container to container

在container1中telnet 10.10.126.101 12580会发生什么呢?这里就不长篇大论的列log了,直接给出结论:通过docker-proxy转发,因为不满足nat output中DNAT的匹配条件。

二、在–userland-proxy=false的情况下

我们修改了一下/etc/default/docker配置,为DOCKER_OPTS增加一个option: –userland-proxy=false。

DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4 --userland-proxy=false"

重启docker daemon并清理iptables规则(-F),并启动做端口映射的container3。启动后,你会发现之前的docker-proxy并没有出现在启动进程列表中,iptables的规则与–userland-proxy=true时也有所不同:

$ sudo iptables -nL -v
Chain INPUT (policy ACCEPT 1645 packets, 368K bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DOCKER     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0
    0     0 ACCEPT     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
    0     0 ACCEPT     all  --  docker0 !docker0  0.0.0.0/0            0.0.0.0/0
    0     0 ACCEPT     all  --  docker0 docker0  0.0.0.0/0            0.0.0.0/0

Chain OUTPUT (policy ACCEPT 263 packets, 134K bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     tcp  --  !docker0 docker0  0.0.0.0/0            172.17.0.4           tcp dpt:12580

$ sudo iptables -t nat -nL -v
Chain PREROUTING (policy ACCEPT 209 packets, 65375 bytes)
 pkts bytes target     prot opt in     out     source               destination
   71 49357 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 98 packets, 39060 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 34 packets, 2096 bytes)
 pkts bytes target     prot opt in     out     source               destination
   21  1302 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 34 packets, 2096 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  all  --  *      docker0  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match src-type LOCAL
    0     0 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0
    0     0 MASQUERADE  tcp  --  *      *       172.17.0.4           172.17.0.4           tcp dpt:12580

Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:12580 to:172.17.0.4:12580

可以看到nat表中prerouting链增加了target为DOCKER链的规则,并且Docker链中对dnat的匹配条件也放开了,只要是dst-type是LOCAL的,dport=12580的,都将ip映射为172.17.0.4。

由于iptables的规则有所变化,因此因此我的log target的匹配条件也该调整一下了,调整后的iptables为:

iptables.portmap.stage1.tmp.rules

# Generated by iptables-save v1.4.12 on Mon Jan 18 09:06:06 2016
*mangle
: POSTROUTING ACCEPT [0:0]
-A POSTROUTING -o docker0 -m addrtype --src-type LOCAL -j LOG --log-prefix "[TonyBai]-manglepost1" --log-level 7
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix "[TonyBai]-manglepost2" --log-level 7
-A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-manglepost3" --log-level 7
COMMIT

*raw
: PREROUTING ACCEPT [1008742:377375989]
:OUTPUT ACCEPT [426678:274235692]
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
COMMIT
# Completed on Mon Jan 18 09:06:06 2016
# Generated by iptables-save v1.4.12 on Mon Jan 18 09:06:06 2016
*filter
:INPUT ACCEPT [187016:64478647]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [81342:51955911]
: DOCKER - [0:0]
:FwdId0Od0 - [0:0]
:FwdId0Ond0 - [0:0]
:FwdOd0 - [0:0]
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j FwdOd0
-A FORWARD -i docker0 ! -o docker0 -j FwdId0Ond0
-A FORWARD -i docker0 -o docker0 -j FwdId0Od0
-A OUTPUT ! -s 127.0.0.1/32 -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-PortmapFowardDocker" --log-level 7
-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j ACCEPT
-A FwdId0Od0 -i docker0 -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Od0:" --log-level 7
-A FwdId0Od0 -i docker0 -o docker0 -j ACCEPT
-A FwdId0Ond0 -i docker0 ! -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Ond0:" --log-level 7
-A FwdId0Ond0 -i docker0 ! -o docker0 -j ACCEPT
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j LOG --log-prefix "[TonyBai]-FwdOd0:" --log-level 7
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
# Completed on Mon Jan 18 09:06:06 2016
# Generated by iptables-save v1.4.12 on Mon Jan 18 09:06:06 2016
*nat
: PREROUTING ACCEPT [34423:7014094]
:INPUT ACCEPT [9475:1880078]
:OUTPUT ACCEPT [3524:218202]
: POSTROUTING ACCEPT [3508:217098]
: DOCKER - [0:0]
:LogNatPostRouting1 - [0:0]
:LogNatPostRouting2 - [0:0]
:LogNatPostRouting3 - [0:0]
-A PREROUTING -p icmp -j LOG --log-prefix "[TonyBai]-Enter iptables:" --log-level 7
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterNatInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A OUTPUT -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -p tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatPostrouteEnter" --log-level 7
-A POSTROUTING -p tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatPostrouteEnter" --log-level 7
-A POSTROUTING -o docker0 -m addrtype --src-type LOCAL -j LogNatPostRouting1
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LogNatPostRouting2
-A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LogNatPostRouting3
-A DOCKER -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-PortmapNatPrerouting" --log-level 7
-A DOCKER -p tcp -m tcp --dport 12580 -j DNAT --to-destination 172.17.0.4:12580
-A LogNatPostRouting1 -o docker0 -m addrtype --src-type LOCAL -j LOG --log-prefix "[TonyBai]-NatPost1" --log-level 7
-A LogNatPostRouting1 -o docker0 -m addrtype --src-type LOCAL -j MASQUERADE
-A LogNatPostRouting2 -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix "[TonyBai]-NatPost2" --log-level 7
-A LogNatPostRouting2 -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A LogNatPostRouting3 -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatPost3" --log-level 7
-A LogNatPostRouting3 -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j MASQUERADE
COMMIT
# Completed on Mon Jan 18 09:06:06 2016

接下来,我们按照上面的方法再做一遍实验例子,看看通信流程有何不同。这次我们将187主机换为10.10.105.71,其他无差别。

1、 在71上telnet 10.10.126.101 12580

宿主机从eth0接口收到syn,nat prerouting中做DNAT。路由后,通过forward链转发到docker0:

Jan 18 13:35:55 pc-baim kernel: [278835.389225] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389275] [TonyBai]-NatPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389290] [TonyBai]-PortmapNatPreroutinIN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389326] [TonyBai]-PortmapFowardDockerIN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=62 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389339] [TonyBai]-NatPostrouteEnterIN= OUT=docker0 SRC=10.10.105.71 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=62 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

接下来从docker0网卡收到container3的ack syn应答,在从eth0转发出去前自动un-DNAT, src ip从172.17.0.4变为101.0126.101,但这个在日志中看不出来。

Jan 18 13:35:55 pc-baim kernel: [278835.389496] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.105.71 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=41502 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389519] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.105.71 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=41502 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389528] [TonyBai]-manglepost2IN= OUT=eth0 PHYSIN=veth0d66af2 SRC=172.17.0.4 DST=10.10.105.71 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=41502 WINDOW=28960 RES=0x00 ACK SYN URGP=0

回送ack,这回无需再匹配natprerouting链,前面进过链一次,后续自动进行DNAT:

Jan 18 13:35:55 pc-baim kernel: [278835.390079] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=61481 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.390149] [TonyBai]-PortmapFowardDockerIN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=62 ID=61481 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0

这次我们看到,在这种方式下,外部流量也是通过DNAT方式导入到container中的。

2、在宿主机上 telnet 10.10.126.101 12580

telnet发起tcp握手,syn包进入output链,匹配到nat output规则,做DNAT。目的ip转换为172.17.0.4。注意继续向下,我们看iptables匹配到了NatPost1,也就是规则:

-A LogNatPostRouting1 -o docker0 -m addrtype --src-type LOCAL -j MASQUERADE

即将源地址伪装为出口网卡docker0的当前地址:172.0.0.1。于是实际上进入到container3的syn数据包的源地址为172.0.0.1,目的地址:172.0.0.4。

Jan 18 13:49:43 pc-baim kernel: [279663.426497] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426526] [TonyBai]-PortmapNatPreroutinIN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426545] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426553] [TonyBai]-manglepost1IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426561] [TonyBai]-NatPostrouteEnterIN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426567] [TonyBai]-NatPost1IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0

container3返回ack,从宿主机角度来看,相当于从docker0网卡收到ack。我们看到进来的原始数据:dst = 172.17.0.1,这是上面MASQUERADE的作用。在进入input链前,做自动un-SNAT,目的地址由172.17.0.1转换为10.10.126.101。在真正送到user层之前(output链等同的左边同纬度位置),做自动un-DNAT(但在下面日志中看不出来),src由172.17.0.4变为10.10.126.101。数据包的变换总体次序依次为:即DNAT -> SNAT -> (应答包)un-SNAT -> un-DNAT。

Jan 18 13:49:43 pc-baim kernel: [279663.426646] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=52736 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426665] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=52736 WINDOW=28960 RES=0x00 ACK SYN URGP=0

宿主机回复ack,握手完成。由于之前走过nat output和post链,因此这里不会再匹配,而是自动DNAT和SNAT:

Jan 18 13:49:43 pc-baim kernel: [279663.426690] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=40855 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426707] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=40855 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426719] [TonyBai]-manglepost1IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=40855 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0

3、从container1 telnet 10.10.126.101 12580

container1向服务发起tcp连接,宿主机从docker0网卡收到sync包。

Jan 18 13:51:10 pc-baim kernel: [279750.806496] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:51:10 pc-baim kernel: [279750.806519] [TonyBai]-NatPrerouting:IN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:51:10 pc-baim kernel: [279750.806531] [TonyBai]-PortmapNatPreroutinIN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

做DNAT后,再次路由到docker0,于是走forward链,但是没有匹配上nat postrouting,也就没有做SNAT:

Jan 18 13:51:10 pc-baim kernel: [279750.806581] [TonyBai]-FwdId0Od0:IN=docker0 OUT=docker0 PHYSIN=veth44a97d7 PHYSOUT=veth0d66af2 MAC=02:42:ac:11:00:04:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:51:10 pc-baim kernel: [279750.806608] [TonyBai]-NatPostrouteEnterIN= OUT=docker0 PHYSIN=veth44a97d7 PHYSOUT=veth0d66af2 SRC=172.17.0.2 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0

container3回复ack sync。宿主机从docker0收到ack sync包,目的地址172.17.0.2,再次路由到docker0。

Jan 18 13:51:10 pc-baim kernel: [279750.806719] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:ac:11:00:02:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=54408 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 18 13:51:10 pc-baim kernel: [279750.806746] [TonyBai]-FwdOd0:IN=docker0 OUT=docker0 PHYSIN=veth0d66af2 PHYSOUT=veth44a97d7 MAC=02:42:ac:11:00:02:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=54408 WINDOW=28960 RES=0x00 ACK SYN URGP=0

由于之前docker0上做过DNAT,因此从docker0回到172.17.0.2时,src地址会自动un-DNAT,从172.17.0.4改为10.10.126.101,不过在上面日志中看不出这一点。

172.17.0.2回复ack,握手完成,DNAT自动进行:

Jan 18 13:51:10 pc-baim kernel: [279750.806823] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=31889 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
Jan 18 13:51:10 pc-baim kernel: [279750.806852] [TonyBai]-FwdOd0:IN=docker0 OUT=docker0 PHYSIN=veth44a97d7 PHYSOUT=veth0d66af2 MAC=02:42:ac:11:00:04:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=31889 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0

三、网络性能考量

docker-proxy常被docker使用者诟病,一是因为每个映射端口都要启动一个docker-proxy进程,映射端口多了,大量进程被创建、被调度势必消耗大量系统资源;二来,在高负载场合,docker-proxy的转发性能也力不从心。理论上,docker-proxy代理转发流量的方式在性能方面要比单纯iptables DNAT要弱上一些。不过我在单机上通过sparkyfish测试的结果倒是二者相差不大,估计是因为我仅仅启动了一个docker-proxy,系统负荷并不大的缘故。




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多