docker容器技术至今已有五年的发展,作为一个工具,已经像Linux命令一样融入我们开发的生活。现在大多开发都使用Mac作为开发机,大都会装一个Docker for mac这个Mac下的docker工具。本文将从以下几个话题进行展开,说明Mac下docker的使用原理。
docker for mac 是docker在Mac机器上的一个docker工具集。集成了Kitematic, Docker图形用户界面,现在又集成了kubernetes。对于开发人员是很方便使用的,安装也很简单,教程也很多。
docker是基于Linux 容器技术开发出来的,Mac OS是unix系统,按照该理论,Mac下是不能运行容器的。之前自己的Mac下跑一个namespace隔离的demo实验,跑完后,一直无法得到理论中的结果。突然才意识到是操作系统环境的问题,Mac OS下也许不支持这种namespace隔离导致的,随后,就倒腾了一台安装ubuntu18.04的虚拟机,跑出了希望的结果。
通过这个例子说明,Mac OS下运行的docker不是原生的docker。容器的daemon应该运行在一个Linux环境虚拟机中。这里只做一个猜想,因为还没找到资料证明认为的这个结论。我们知道,在单机环境中,docker的客户端和daemon的通信是默认方式是socket文件的方式。在Linux环境中,我们要使用docker server端提供的API,就需要设置DOCKER_OPTS= -H tcp://0.0.0.0:2375 server端监听2375端口,我们才可以通过docker 提供的API进行接口调用。
那么Mac OS环境的文件系统找了好久,也没找到docker server端提供的参数配置文件。然后Google后找到了点思路。
Docker for Mac提供的UI界面可以配置该参数。
尝试添加tcp监听端口,结果如下所示。那么就需探索其它方法。
在stackoverflow中看到一个回答,进入docker daemon所在的虚拟机中
screen ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty
linuxkit-025000000001:~# ps -ef | grep docker
1003 root 0:03 containerd-shim -namespace services.linuxkit -workdir /var/lib/containerd/io.containerd.runtime.v1.linux/services.linuxkit/docker-ce -address /run/containerd/containerd.sock -containerd-binary /usr/bin/containerd
1025 root 0:00 /usr/local/bin/docker-init /usr/bin/entrypoint.sh
1176 root 0:00 {start-docker.sh} /bin/sh /usr/bin/start-docker.sh
1184 root 0:00 /usr/sbin/sntpc -v -i 30 gateway.docker.internal
1226 root 1:12 kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --cluster-dns=10.96.0.10 --cluster-domain=cluster.local --cgroups-per-qos=false --enforce-node-allocatable= --network-plugin=cni --cni-conf-dir=/etc/cni/net.d --cni-bin-dir=/opt/cni/bin --cadvisor-port=0 --kube-reserved-cgroup=podruntime --system-reserved-cgroup=systemreserved --cgroup-root=kubepods --hostname-override=docker-for-desktop --fail-swap-on=false
1275 root 0:24 /usr/local/bin/dockerd -H unix:///var/run/docker.sock --config-file /run/config/docker/daemon.json --swarm-default-advertise-addr=eth0 --userland-proxy-path /usr/bin/V**kit-expose-port
1318 root 0:00 /usr/bin/trim-after-delete -- /sbin/fstrim /var/lib/docker
1422 root 0:00 /vsudd -inport 2376:unix:/var/run/docker.sock
1474 root 0:07 docker-containerd --config /var/run/docker/containerd/containerd.toml
linuxkit-025000000001:~# more /run/config/docker/daemon.json
{
"debug" : true,
"experimental" : true
}
说明UI管理的daemon的配置数据是从这里获得。通过以上的实验,我们可以知道,在验证文章开头的那个猜想是正确的,Mac OS下的docker是通过Linux虚拟机的方式运行的。这里看到了linuxkit,这又是一项什么技术,先收一收吧,担心自己还没法驾驭的了。
Docker for mac 的知识点展开就先到这里了。我们的问题是怎么解决Mac OS下docker API的使用问题,目前还没有解决。还需要探索新的东西来解决该问题。
在Linuxkit下的命令行中,我们看到了dockerd -H unix:///var/run/docker.sock,这是docker daemon的默认通信方式,那么我们是否可以将tcp协议转化为socket协议,访问外国网站软件大都基于这样的一种协议转化。在此,还不是十分的清楚明白这种sock文件通信的机制。
来补一补这一节理论知识。
"unix://"这种通信协议被称为UNIX域协议,它主要用于同一主机下,不同进程间的通信,比tcp协议的通信效率高很多。通socket编程又有不同。
unix域提供两类套接字:字节流套接字(类似TCP)和数据报套接字(类似UDP)。使用unix域协议有如下的优势: (1)unix域套接字往往比通信两端位于同一个主机的TCP套接字快出一倍。 (2)unix域套接字可用于在同一个主机上的不同进程之间传递描述符。 (3)unix域套接字较新的实现把客户的凭证(用户ID和组ID)提供给服务器,从而能够提供额外的安全检查措施。
unix域中用于标识客户和服务器的协议地址是普通文件系统中的路径名
来看一个go实现的demo
服务端:server.go
package main
import (
"encoding/binary"
"bytes"
"io"
"os"
"fmt"
"net"
"time"
)
const (
UNIX_SOCK_PIPE_PATH = "/var/run/unixsock_test.sock" // socket file path
)
func main() {
// Remove socket file
os.Remove(UNIX_SOCK_PIPE_PATH)
// Get unix socket address based on file path
uaddr, err := net.ResolveUnixAddr("unix", UNIX_SOCK_PIPE_PATH)
if err != nil {
fmt.Println(err)
return
}
// Listen on the address
unixListener, err := net.ListenUnix("unix", uaddr)
if err != nil {
fmt.Println(err)
return
}
// Close listener when close this function, you can also emit it because this function will not terminate gracefully
defer unixListener.Close()
fmt.Println("Waiting for asking questions ...")
// Monitor request and process
for {
uconn, err := unixListener.AcceptUnix()
if err != nil {
fmt.Println(err)
continue
}
// Handle request
go handleConnection(uconn)
}
}
/*******************************************************
* Handle connection and request
* conn: conn handler
*******************************************************/
func handleConnection(conn *net.UnixConn) {
// Close connection when finish handling
defer func() {
conn.Close()
}()
// Read data and return response
data, err := parseRequest(conn)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v\tReceived from client: %s\n", time.Now(), string(data))
time.Sleep(time.Duration(1) * time.Second) // sleep to simulate request process
// Send back response
sendResponse(conn, []byte(time.Now().String()))
}
/*******************************************************
* Parse request of unix socket
* conn: conn handler
*******************************************************/
func parseRequest(conn *net.UnixConn) ([]byte, error) {
var reqLen uint32
lenBytes := make([]byte, 4)
if _, err := io.ReadFull(conn, lenBytes); err != nil {
return nil, err
}
lenBuf := bytes.NewBuffer(lenBytes)
if err := binary.Read(lenBuf, binary.BigEndian, &reqLen); err != nil {
return nil, err
}
reqBytes := make([]byte, reqLen)
_, err := io.ReadFull(conn, reqBytes)
if err != nil {
return nil, err
}
return reqBytes, nil
}
/*******************************************************
* Send response to client
* conn: conn handler
*******************************************************/
func sendResponse(conn *net.UnixConn, data []byte) {
buf := new(bytes.Buffer)
msglen := uint32(len(data))
binary.Write(buf, binary.BigEndian, &msglen)
data = append(buf.Bytes(), data...)
conn.Write(data)
}
客户端:client.go
package main
import (
"time"
"io"
"encoding/binary"
"bytes"
"fmt"
"net"
)
const (
UNIX_SOCK_PIPE_PATH = "/var/run/unixsock_test.sock" // socket file path
)
var (
exitSemaphore chan bool
)
func main() {
// Get unix socket address based on file path
uaddr, err := net.ResolveUnixAddr("unix", UNIX_SOCK_PIPE_PATH)
if err != nil {
fmt.Println(err)
return
}
// Connect server with unix socket
uconn, err := net.DialUnix("unix", nil, uaddr)
if err != nil {
fmt.Println(err)
return
}
// Close unix socket when exit this function
defer uconn.Close()
// Wait to receive response
go onMessageReceived(uconn)
// Send a request to server
// you can define your own rules
msg := "tell me current time\n"
sendRequest(uconn, []byte(msg))
// Wait server response
// change this duration bigger than server sleep time to get correct response
exitSemaphore = make(chan bool)
select {
case <- time.After(time.Duration(2) * time.Second):
fmt.Println("Wait response timeout")
case <-exitSemaphore:
fmt.Println("Get response correctly")
}
close(exitSemaphore)
}
/*******************************************************
* Send request to server, you can define your own proxy
* conn: conn handler
*******************************************************/
func sendRequest(conn *net.UnixConn, data []byte) {
buf := new(bytes.Buffer)
msglen := uint32(len(data))
binary.Write(buf, binary.BigEndian, &msglen)
data = append(buf.Bytes(), data...)
conn.Write(data)
}
/*******************************************************
* Handle connection and response
* conn: conn handler
*******************************************************/
func onMessageReceived(conn *net.UnixConn) {
//for { // io Read will wait here, we don't need for loop to check
// Read information from response
data, err := parseResponse(conn)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("%v\tReceived from server: %s\n", time.Now(), string(data))
}
// Exit when receive data from server
exitSemaphore <- true
//}
}
/*******************************************************
* Parse request of unix socket
* conn: conn handler
*******************************************************/
func parseResponse(conn *net.UnixConn) ([]byte, error) {
var reqLen uint32
lenBytes := make([]byte, 4)
if _, err := io.ReadFull(conn, lenBytes); err != nil {
return nil, err
}
lenBuf := bytes.NewBuffer(lenBytes)
if err := binary.Read(lenBuf, binary.BigEndian, &reqLen); err != nil {
return nil, err
}
reqBytes := make([]byte, reqLen)
_, err := io.ReadFull(conn, reqBytes)
if err != nil {
return nil, err
}
return reqBytes, nil
}
备注:代码参考 https://blog.csdn.net/lwc5411117/article/details/83018252
通过这个例子,大概能说明unix域协议了吧。我们要实现的目标就是将tcp转化为unix域协议,这里有一个号称网络界瑞士军刀socat可以实现我们的想法。
现在问题解决的思路很清晰了,闲话不说,直接上成熟的解决方案。
通过容器的方式来解决
```
docker run -d -v /var/run/docker.sock:/var/run/docker.sock -p 2376:2375 \
bobrik/socat TCP4-LISTEN:2375,fork,reuseaddr UNIX-CONNECT:/var/run/docker.sock
```
对于以上命令的意思,在此放一个外链socat入门。
测试
$curl -XGET http://127.0.0.1:2376/version | python -mjson.tool
{
"ApiVersion": "1.38",
"Arch": "amd64",
"BuildTime": "2018-07-18T19:13:46.000000000+00:00",
"Components": [
{
"Details": {
"ApiVersion": "1.38",
"Arch": "amd64",
"BuildTime": "2018-07-18T19:13:46.000000000+00:00",
"Experimental": "true",
"GitCommit": "0ffa825",
"GoVersion": "go1.10.3",
"KernelVersion": "4.9.93-linuxkit-aufs",
"MinAPIVersion": "1.12",
"Os": "linux"
},
"Name": "Engine",
"Version": "18.06.0-ce"
}
],
"Experimental": true,
"GitCommit": "0ffa825",
"GoVersion": "go1.10.3",
"KernelVersion": "4.9.93-linuxkit-aufs",
"MinAPIVersion": "1.12",
"Os": "linux",
"Platform": {
"Name": ""
},
"Version": "18.06.0-ce"
}
完美的实现了docker API的使用。
curl 这个工具也是可以支持unix域协议
$curl -XGET --unix-socket /var/run/docker.sock http://127.0.0.1:2375/version | python -mjson.tool
{
"ApiVersion": "1.38",
"Arch": "amd64",
"BuildTime": "2018-07-18T19:13:46.000000000+00:00",
"Components": [
{
"Details": {
"ApiVersion": "1.38",
"Arch": "amd64",
"BuildTime": "2018-07-18T19:13:46.000000000+00:00",
"Experimental": "true",
"GitCommit": "0ffa825",
"GoVersion": "go1.10.3",
"KernelVersion": "4.9.93-linuxkit-aufs",
"MinAPIVersion": "1.12",
"Os": "linux"
},
"Name": "Engine",
"Version": "18.06.0-ce"
}
],
"Experimental": true,
"GitCommit": "0ffa825",
"GoVersion": "go1.10.3",
"KernelVersion": "4.9.93-linuxkit-aufs",
"MinAPIVersion": "1.12",
"Os": "linux",
"Platform": {
"Name": ""
},
"Version": "18.06.0-ce"
}
到此,我们的问题已经完美的解决。对Mac 的docker原理展开做了一个说明,同时对unix域协议通过go语言实现了一个demo,最后通过socat这个强大的网络工具通过docker安装的方式解决了docker API访问的问题。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。