因為工作需要,去學習了下Docker的embedded DNS. 這個功能似乎是1.10才加進來的,用來對Docker自帶的overlay網絡提供DNS服務(容器)發現。我學習的是Docker1.10.3版本,對應的libnetwork是release/v0.7。這個版本的Embedded DNS僅支持IPv4,后續的1.11版本還會支持IPv6.
Container Network Model(CNM)
Docker的容器網絡模型如下圖所示:
其中
- 圖中少了一個關鍵組件:
NetworkController
,他是一個Docker Daemon唯一的,用來管理Docker的所有網絡環境,跟Docker Daemon打交到; - 每個Sandbox對應一個Container;
- 每個Network對應了一個網絡環境(logical connectivity zone),他背后對應一個network driver,一些關鍵過程是調用其api;
- 每個Endpoint對應一個Sandbox到Network的邏輯鏈接(logical connection).
How it works
什么情況下會使用embedded DNS
方法Daemon.connectToNetwork()
是被調用來將一個container連接到某個網絡的。
func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName string, endpointConfig *networktypes.EndpointSettings, updateSettings bool) (err error) {
if endpointConfig == nil {
endpointConfig = &networktypes.EndpointSettings{}
}
n, err := daemon.updateNetworkConfig(container, idOrName, endpointConfig, updateSettings)
if err != nil {
return err
}
if n == nil {
return nil
}
controller := daemon.netController
sb := daemon.getNetworkSandbox(container)
createOptions, err := container.BuildCreateEndpointOptions(n, endpointConfig, sb) // <--------
if err != nil {
return err
}
endpointName := strings.TrimPrefix(container.Name, "/")
ep, err := n.CreateEndpoint(endpointName, createOptions...)
if err != nil {
return err
}
......
其中BuildCreateEndpointOptions
方法返回了創建一個endpoint所需要的option。而此方法中的如下代碼直接指定了是否要使用embedded DNS。
if !containertypes.NetworkMode(n.Name()).IsUserDefined() { // using embedded DNS if not user defined network mode
createOptions = append(createOptions, libnetwork.CreateOptionDisableResolution())
}
在package github.com/docker/engine-api/types/container
中定義的方法IsUserDefined()
會判斷網絡名不為default
, bridge
, host
, none
, container
時,則認為是用戶定義網絡,進而enable了embedded DNS。
// IsDefault indicates whether container uses the default network stack.
func (n NetworkMode) IsDefault() bool {
return n == "default"
}
// NetworkName returns the name of the network stack.
func (n NetworkMode) NetworkName() string {
if n.IsBridge() {
return "bridge"
} else if n.IsHost() {
return "host"
} else if n.IsContainer() {
return "container"
} else if n.IsNone() {
return "none"
} else if n.IsDefault() {
return "default"
} else if n.IsUserDefined() {
return n.UserDefined()
}
return ""
}
......
// IsUserDefined indicates user-created network
func (n NetworkMode) IsUserDefined() bool {
return !n.IsDefault() && !n.IsBridge() && !n.IsHost() && !n.IsNone() && !n.IsContainer()
}
而整個embedded DNS的生命周期,其實可以從兩個角度來看
從Container的角度
Sandbox每次通過一個Endpoint去Join一個Network都會去發布一把這個ep,
sb.populateNetworkResources(ep)
. 其中回去判斷一把這個Network是否需要resolver,即Embedded DNS. 如果是用戶定義的Network則會enable embedder DNS功能sb.startResolver()
。anyway,Sandbox都會將這個ep的信息保存到NetworkController的一個Map解構中networkController.svcDB
。他保存了每個Network中的每個ep的name、alias和分配的IP的對應關系。是跨host的name resolution的根據。(libnetwork/sandbox.go
)在
sb.startResolver( )
方法中會做embedded DNS的初始化和啟動工作:
-
sb.rebuildDNS()
首先會記錄初始時容器內部的/etc/resolv.conf文件的內容,然后更新/etc/resolv.conf文件,將servername指向127.0.0.11。(127.0.0.0/8網段都是loopback地址)
resolv.conf in container sb.resolver.SetExtServers(sb.extDNS)
將之前讀取的容器初始/etc/resolv.conf文件的nameserver作為embedded DNS的recursive DNS,當embedded DNS不能resolve name的時候就delegate到extDNS。注意這也是為什么--nameserver參數還能工作的原因
,雖然容器內的/etc/resolv.conf文件顯示的nameserver還是127.0.0.11sb.osSbox.InvokeFunc(sb.resolver.SetupFunc())
在Container環境中申請tcp和udp兩個隨機端口,姑且命其為tcp_port和udp_port。-
sb.resolver.Start()
啟動embedded DNS:-
r.setupIPTable()
添加iptables規則將127.0.0.11:53
的name resolution query通過DNAT轉發到127.0.0.11:tcp_port
或127.0.0.11:udp_port
(DNS query默認是udp,效率高)。將出去的query通過SNAT轉換回127.0.0.11:53
。 - 調用
github.com/miekg/dns
包的api啟動兩個DNS server分別監聽tcp_port和udp_port。
iptables and port info
-
- 當DNS query來的時候,resolver會去handle:
r.ServeDNS( )
。resolver會在方法r.handleIPQuery( )
里去resolve name。他會delegate給Sandboxsb.ResolveName( )
,然后是去NetworkController里的SvcDB
里查找:。沒找到就delegate到ExtDNS。
從Docker Daemon的角度
其中紫色部分為上一節關于embedded DNS的啟動過程。(圖片可通過瀏覽器放大。。。)
這部分是整個環境的搭建流程:
在Daemo被創建的時候
NewDaemon( )
會初始化網絡環境d.initNetworkController( )
,其中會創建NetworkControllerlibnetwork.New( )
。此時,會在NetworkController里開啟一個loopc.startWatch( )
,一直監聽watch
和unwatch
兩個channel。這兩個channel分別接受被創建的Endpoint實例和被刪除的Endpoint實例然后分別調用c.processEndpointCreate( )
和c.processEndpointDelete( )
分別去維護NetworkController里的SvcDB里的ep name/alias和IP的對應關系。當調用
n.CreateEndpoint( )
或ep.Join( )
時,會往NetworkController的watch
channel里放入當前的ep,從而完成一個DNS record的注冊當調用
ep.Delete( )
時,會往NetworkController的unwatch
channel里放入當前ep,以完成這個DNS record的注銷。
整個embedded DNS的環境就是這個樣子。