标签 Golang 下的文章

理解Docker容器网络之Linux Network Namespace

由于2016年年中调换工作的原因,对容器网络的研究中断过一段时间。随着当前项目对Kubernetes应用的深入,我感觉之前对于容器网络的粗浅理解已经不够了,容器网络成了摆在前面的“一道坎”。继续深入理解K8s网络、容器网络已经势在必行。而这篇文章就算是一个重新开始,也是对之前浅表理解的一个补充。

我还是先从Docker容器网络入手,虽然Docker与Kubernetes采用了不同的网络模型:K8s是Container Network Interface, CNI模型,而Docker则采用的是Container Network Model, CNM模型。而要了解Docker容器网络,理解Linux Network Namespace是不可或缺的。在本文中我们将尝试理解Linux Network Namespace及相关Linux内核网络设备的概念,并手工模拟Docker容器网络模型的部分实现,包括单机容器网络中的容器与主机连通、容器间连通以及端口映射等。

一、Docker的CNM网络模型

Docker通过libnetwork实现了CNM网络模型。libnetwork设计doc中对CNM模型的简单诠释如下:

img{512x368}

CNM模型有三个组件:

  • Sandbox(沙盒):每个沙盒包含一个容器网络栈(network stack)的配置,配置包括:容器的网口、路由表和DNS设置等。
  • Endpoint(端点):通过Endpoint,沙盒可以被加入到一个Network里。
  • Network(网络):一组能相互直接通信的Endpoints。

光看这些,我们还很难将之与现实中的Docker容器联系起来,毕竟是抽象的模型不对应到实体,总有种漂浮的赶脚。文档中又给出了CNM模型在Linux上的参考实现技术,比如:沙盒的实现可以是一个Linux Network Namespace;Endpoint可以是一对VETH;Network则可以用Linux BridgeVxlan实现。

这些实现技术反倒是比较接地气。之前我们在使用Docker容器时,了解过Docker是用linux network namespace实现的容器网络隔离的。使用docker时,在物理主机或虚拟机上会有一个docker0的linux bridge,brctl show时能看到 docker0上“插上了”好多veth网络设备:

# ip link show
... ...
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:30:11:98:ef brd ff:ff:ff:ff:ff:ff
19: veth4559467@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
    link/ether a6:14:99:52:78:35 brd ff:ff:ff:ff:ff:ff link-netnsid 3
... ...

$ brctl show
bridge name    bridge id        STP enabled    interfaces
... ...
docker0        8000.0242301198ef    no        veth4559467

模型与现实终于有点接驳了!下面我们将进一步深入对这些术语概念的理解。

二、Linux Bridge、VETH和Network Namespace

Linux Bridge,即Linux网桥设备,是Linux提供的一种虚拟网络设备之一。其工作方式非常类似于物理的网络交换机设备。Linux Bridge可以工作在二层,也可以工作在三层,默认工作在二层。工作在二层时,可以在同一网络的不同主机间转发以太网报文;一旦你给一个Linux Bridge分配了IP地址,也就开启了该Bridge的三层工作模式。在Linux下,你可以用iproute2工具包或brctl命令对Linux bridge进行管理。

VETH(Virtual Ethernet )是Linux提供的另外一种特殊的网络设备,中文称为虚拟网卡接口。它总是成对出现,要创建就创建一个pair。一个Pair中的veth就像一个网络线缆的两个端点,数据从一个端点进入,必然从另外一个端点流出。每个veth都可以被赋予IP地址,并参与三层网络路由过程。

关于Linux Bridge和VETH的具体工作原理,可以参考IBM developerWorks上的这篇文章《Linux 上的基础网络设备详解》。

Network namespace,网络名字空间,允许你在Linux创建相互隔离的网络视图,每个网络名字空间都有独立的网络配置,比如:网络设备、路由表等。新建的网络名字空间与主机默认网络名字空间之间是隔离的。我们平时默认操作的是主机的默认网络名字空间。

概念总是抽象的,接下来我们将在一个模拟Docker容器网络的例子中看到这些Linux网络概念和网络设备到底是起到什么作用的以及是如何操作的。

三、用Network namespace模拟Docker容器网络

为了进一步了解network namespace、bridge和veth在docker容器网络中的角色和作用,我们来做一个demo:用network namespace模拟Docker容器网络,实际上Docker容器网络在linux上也是基于network namespace实现的,我们只是将其“自动化”的创建过程做成了“分解动作”,便于大家理解。

1、环境

我们在一台物理机上进行这个Demo实验。物理机安装了Ubuntu 16.04.1,内核版本:4.4.0-57-generic。Docker容器版本:

Client:
 Version:      1.12.1
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   23cf638
 Built:        Thu Aug 18 05:33:38 2016
 OS/Arch:      linux/amd64

Server:
 Version:      1.12.1
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   23cf638
 Built:        Thu Aug 18 05:33:38 2016
 OS/Arch:      linux/amd64

另外,环境中需安装了iproute2和brctl工具。

2、拓扑

我们来模拟一个拥有两个容器的容器桥接网络:

img{512x368}

对应的用手工搭建的模拟版本拓扑如下(由于在同一台主机,模拟版本采用172.16.0.0/16网段):

img{512x368}

3、创建步骤

a) 创建Container_ns1和Container_ns2 network namespace

默认情况下,我们在Host上看到的都是default network namespace的视图。为了模拟容器网络,我们新建两个network namespace:

sudo ip netns add Container_ns1
sudo ip netns add Container_ns2

$ sudo ip netns list
Container_ns2
Container_ns1

创建的ns也可以在/var/run/netns路径下看到:

$ sudo ls /var/run/netns
Container_ns1  Container_ns2

我们探索一下新创建的ns的网络空间(通过ip netns exec命令可以在特定ns的内部执行相关程序,这个exec命令是至关重要的,后续还会发挥更大作用):

$ sudo ip netns exec Container_ns1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

$ sudo ip netns exec Container_ns2 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

$ sudo ip netns exec Container_ns2 ip route

可以看到,新建的ns的网络设备只有一个loopback口,并且路由表为空。

b) 创建MyDocker0 bridge

我们在default network namespace下创建MyDocker0 linux bridge:

$ sudo brctl addbr MyDocker0

$ brctl show
bridge name    bridge id        STP enabled    interfaces
MyDocker0        8000.000000000000    no

给MyDocker0分配ip地址并生效该设备,开启三层,为后续充当Gateway做准备:

$ sudo ip addr add 172.16.1.254/16 dev MyDocker0
$ sudo ip link set dev MyDocker0 up

启用后,我们发现default network namespace的路由配置中增加了一条路由:

$ route -n
内核 IP 路由表
目标            网关            子网掩码        标志  跃点   引用  使用 接口
0.0.0.0         10.11.36.1      0.0.0.0         UG    100    0        0 eno1
... ...
172.16.0.0      0.0.0.0         255.255.0.0     U     0      0        0 MyDocker0
... ...
c) 创建VETH,连接两对network namespaces

到目前为止,default ns与Container_ns1、Container_ns2之间还没有任何瓜葛。接下来就是见证奇迹的时刻了。我们通过veth pair建立起多个ns之间的联系:

创建连接default ns与Container_ns1之间的veth pair – veth1和veth1p:

$sudo ip link add veth1 type veth peer name veth1p

$sudo ip -d link show
... ...
21: veth1p@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff promiscuity 0
    veth addrgenmode eui64
22: veth1@veth1p: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 56:cd:bb:f2:10:3f brd ff:ff:ff:ff:ff:ff promiscuity 0
    veth addrgenmode eui64
... ...

将veth1“插到”MyDocker0这个bridge上:

$ sudo brctl addif MyDocker0 veth1
$ sudo ip link set veth1 up
$ brctl show
bridge name    bridge id        STP enabled    interfaces
MyDocker0        8000.56cdbbf2103f    no        veth1

将veth1p“放入”Container_ns1中:

$ sudo ip link set veth1p netns Container_ns1

$ sudo ip netns exec Container_ns1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
21: veth1p@if22: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0

这时,你在default ns中将看不到veth1p这个虚拟网络设备了。按照上面拓扑,位于Container_ns1中的veth应该更名为eth0:

$ sudo ip netns exec Container_ns1 ip link set veth1p name eth0
$ sudo ip netns exec Container_ns1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
21: eth0@if22: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0

将Container_ns1中的eth0生效并配置IP地址:

$ sudo ip netns exec Container_ns1 ip link set eth0 up
$ sudo ip netns exec Container_ns1 ip addr add 172.16.1.1/16 dev eth0

赋予IP地址后,自动生成一条直连路由:

sudo ip netns exec Container_ns1 ip route
172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1

现在在Container_ns1下可以ping通MyDocker0了,但由于没有其他路由,包括默认路由,ping其他地址还是不通的(比如:docker0的地址:172.17.0.1):

$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.254
PING 172.16.1.254 (172.16.1.254) 56(84) bytes of data.
64 bytes from 172.16.1.254: icmp_seq=1 ttl=64 time=0.074 ms
64 bytes from 172.16.1.254: icmp_seq=2 ttl=64 time=0.064 ms
64 bytes from 172.16.1.254: icmp_seq=3 ttl=64 time=0.068 ms

--- 172.16.1.254 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.064/0.068/0.074/0.010 ms

$ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1
connect: Network is unreachable

我们再给Container_ns1添加一条默认路由,让其能ping通物理主机上的其他网络设备或其他ns空间中的网络设备地址:

$ sudo ip netns exec Container_ns1 ip route add default via 172.16.1.254
$ sudo ip netns exec Container_ns1 ip route
default via 172.16.1.254 dev eth0
172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1

$ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1
PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data.
64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.068 ms
64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.076 ms
64 bytes from 172.17.0.1: icmp_seq=3 ttl=64 time=0.069 ms

--- 172.17.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.068/0.071/0.076/0.003 ms

不过这时候,如果想在Container_ns1中ping通物理主机之外的地址,比如:google.com,那还是不通的。为什么呢?因为ping的icmp的包的源地址没有做snat(docker是通过设置iptables规则实现的),导致出去的以172.16.1.1为源地址的包“有去无回”了^0^。

接下来,我们按照上述步骤,再创建连接default ns与Container_ns2之间的veth pair – veth2和veth2p,由于步骤相同,这里就不列出那么多信息了,只列出关键操作:

$ sudo ip link add veth2 type veth peer name veth2p
$ sudo brctl addif MyDocker0 veth2
$ sudo ip link set veth2 up
$ sudo ip link set veth2p netns Container_ns2
$ sudo ip netns exec Container_ns2 ip link set veth2p name eth0
$ sudo ip netns exec Container_ns2 ip link set eth0 up
$ sudo ip netns exec Container_ns2 ip addr add 172.16.1.2/16 dev eth0
$ sudo ip netns exec Container_ns2 ip route add default via 172.16.1.254

至此,模拟创建告一段落!两个ns之间以及它们与default ns之间连通了!

$ sudo ip netns exec Container_ns2 ping -c 3 172.16.1.1
PING 172.16.1.1 (172.16.1.1) 56(84) bytes of data.
64 bytes from 172.16.1.1: icmp_seq=1 ttl=64 time=0.101 ms
64 bytes from 172.16.1.1: icmp_seq=2 ttl=64 time=0.083 ms
64 bytes from 172.16.1.1: icmp_seq=3 ttl=64 time=0.087 ms

--- 172.16.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.083/0.090/0.101/0.010 ms

$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.2
PING 172.16.1.2 (172.16.1.2) 56(84) bytes of data.
64 bytes from 172.16.1.2: icmp_seq=1 ttl=64 time=0.053 ms
64 bytes from 172.16.1.2: icmp_seq=2 ttl=64 time=0.092 ms
64 bytes from 172.16.1.2: icmp_seq=3 ttl=64 time=0.089 ms

--- 172.16.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.053/0.078/0.092/0.017 ms

当然此时两个ns之间连通,主要还是通过直连网络,实质上是MyDocker0在二层起到的作用。以在Container_ns1中ping Container_ns2的eth0地址为例:

Container_ns1此时的路由表:

$ sudo ip netns exec Container_ns1 ip route
default via 172.16.1.254 dev eth0
172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1

ping 172.16.1.2执行后,根据路由表,将首先匹配到直连网络(第二条),即无需gateway转发便可以直接将数据包送达。arp查询后(要么从arp cache中找到,要么在MyDocker0这个二层交换机中泛洪查询)获得172.16.1.2的mac地址。ip包的目的ip填写172.16.1.2,二层数据帧封包将目的mac填写为刚刚查到的mac地址,通过eth0(172.16.1.1)发送出去。eth0实际上是一个veth pair,另外一端“插”在MyDocker0这个交换机上,因此这一过程就是一个标准的二层交换机的数据报文交换过程, MyDocker0相当于从交换机上的一个端口收到以太帧数据,并将数据从另外一个端口发出去。ping应答包亦如此。

而如果是在Container_ns1中ping某个docker container的地址,比如172.17.0.2。当ping执行后,根据Container_ns1下的路由表,没有匹配到直连网络,只能通过default路由将数据包发给Gateway: 172.16.1.254。虽然都是MyDocker0接收数据,但这次更类似于“数据被直接发到 Bridge 上,而不是Bridge从一个端口接收(这块儿与我之前的文章中的理解稍有差异)”。二层的目的mac地址填写的是gateway 172.16.1.254自己的mac地址(Bridge的mac地址),此时的MyDocker0更像是一块普通网卡的角色,工作在三层。MyDocker0收到数据包后,发现并非是发给自己的ip包,通过主机路由表找到直连链路路由,MyDocker0将数据包Forward到docker0上(封装的二层数据包的目的MAC地址为docker0的mac地址)。此时的docker0也是一种“网卡”的角色,由于目的ip依然不是docker0自身,因此docker0也会继续这一转发流程。通过traceroute可以印证这一过程:

$ sudo ip netns exec Container_ns1  traceroute 172.17.0.2
traceroute to 172.17.0.2 (172.17.0.2), 30 hops max, 60 byte packets
 1  172.16.1.254 (172.16.1.254)  0.082 ms  0.023 ms  0.019 ms
 2  172.17.0.2 (172.17.0.2)  0.054 ms  0.034 ms  0.029 ms

$ sudo ip netns exec Container_ns1  ping -c 3 172.17.0.2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=63 time=0.084 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=63 time=0.101 ms
64 bytes from 172.17.0.2: icmp_seq=3 ttl=63 time=0.098 ms

--- 172.17.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.084/0.094/0.101/0.010 ms

现在,你应该大致了解docker engine在创建单机容器网络时都在背后做了哪些手脚了吧(当然,这里只是简单模拟,docker实际做的要比这复杂许多)。

四、基于userland proxy的容器端口映射的模拟

端口映射让位于容器中的service可以将服务范围扩展到主机之外,比如:一个运行于container中的nginx可以通过宿主机的9091端口对外提供http server服务:

$ sudo docker run -d -p 9091:80 nginx:latest
8eef60e3d7b48140c20b11424ee8931be25bc47b5233aa42550efabd5730ac2f

$ curl 10.11.36.15:9091
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

容器的端口映射实际是通过docker engine的docker proxy功能实现的。默认情况下,docker engine(截至docker 1.12.1版本)采用userland proxy(–userland-proxy=true)为每个expose端口的容器启动一个proxy实例来做端口流量转发:

$ ps -ef|grep docker-proxy
root     26246  6228  0 16:18 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9091 -container-ip 172.17.0.2 -container-port 80

docker-proxy实际上就是在default ns和container ns之间转发流量而已。我们完全可以模拟这一过程。

我们创建一个fileserver demo:

//testfileserver.go
package main

import "net/http"

func main() {
    http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))
}

我们在Container_ns1下启动这个Fileserver service:

$ sudo ip netns exec Container_ns1 ./testfileserver

$ sudo ip netns exec Container_ns1 lsof -i tcp:8080
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
testfiles 3605 root    3u  IPv4 297022      0t0  TCP *:http-alt (LISTEN)

可以看到在Container_ns1下面,8080已经被testfileserver监听,不过在default ns下,8080端口依旧是avaiable的。

接下来,我们在default ns下创建一个简易的proxy:

//proxy.go
... ...

var (
    host          string
    port          string
    container     string
    containerport string
)

func main() {
    flag.StringVar(&host, "host", "0.0.0.0", "host addr")
    flag.StringVar(&port, "port", "", "host port")
    flag.StringVar(&container, "container", "", "container addr")
    flag.StringVar(&containerport, "containerport", "8080", "container port")

    flag.Parse()

    fmt.Printf("%s\n%s\n%s\n%s", host, port, container, containerport)

    ln, err := net.Listen("tcp", host+":"+port)
    if err != nil {
        // handle error
        log.Println("listen error:", err)
        return
    }
    log.Println("listen ok")

    for {
        conn, err := ln.Accept()
        if err != nil {
            // handle error
            log.Println("accept error:", err)
            continue
        }
        log.Println("accept conn", conn)
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    cli, err := net.Dial("tcp", container+":"+containerport)
    if err != nil {
        log.Println("dial error:", err)
        return
    }
    log.Println("dial ", container+":"+containerport, " ok")

    go io.Copy(conn, cli)
    _, err = io.Copy(cli, conn)
    fmt.Println("communication over: error:", err)
}

在default ns下执行:

./proxy -host 0.0.0.0 -port 9090 -container 172.16.1.1 -containerport 8080
0.0.0.0
9090
172.16.1.1
80802017/01/11 17:26:10 listen ok

我们http get一下宿主机的9090端口:

$curl 10.11.36.15:9090
<pre>
<a href="proxy">proxy</a>
<a href="proxy.go">proxy.go</a>
<a href="testfileserver">testfileserver</a>
<a href="testfileserver.go">testfileserver.go</a>
</pre>

成功获得file list!

proxy的输出日志:

2017/01/11 17:26:16 accept conn &{{0xc4200560e0}}
2017/01/11 17:26:16 dial  172.16.1.1:8080  ok
communication over: error:<nil>

由于每个做端口映射的Container都要启动至少一个docker proxy与之配合,一旦运行的container增多,那么docker proxy对资源的消耗将是大大的。因此docker engine在docker 1.6之后(好像是这个版本)提供了基于iptables的端口映射机制,无需再启动docker proxy process了。我们只需修改一下docker engine的启动配置即可:

在使用systemd init system的系统中如果为docker engine配置–userland-proxy=false,可以参考《当Docker遇到systemd》这篇文章。

由于这个与network namespace关系不大,后续单独理解^0^。

六、参考资料

1、《Docker networking cookbook
2、《Docker cookbook

2016小结

每到年终岁尾,历史上受到过中国文化影响的国家和地区都有评选当年年度汉字的传统,比如:2016年马来西亚年度汉字为“贪”,鬼子国日本年度汉字为“金”,中国台湾地区年度汉字为“苦”,而大陆地区的年度汉字据说是“规”。其实每个人心中都有一个自己的年度汉字,2016年,我个人的年度汉字为“变”。

一、离职

其实,这两年我求变的步伐一直没有停歇,只是今年迈出了实质性的一步。2016年4月末,就是在参加完GopherChina大会后,我就义无反顾的离开了工作10年多的老东家(也许很多人对于我的忠诚程度感觉很惊讶^0^),加盟了本地另外一家以IDC为基础、追求成为东北地区一流数据和基础设施服务商的初创企业。

我的新的直属领导是公司的技术VP,很牛逼的一个人,也是一名互联网老兵。据说他几乎以一人之力将公司IDC从无到有的建立起来(从商务采购谈判到IDC技术),并组建团队,打造公司云基础设施平台。当时我怀揣的极大的热情希望能在这样的一个新环境下,在公司的重点技术领域:云计算(基于OpenStack的公有云平台)、大数据技术、容器平台(与Rancher公司合作开发容器管理平台)等方向深入下去。但事情的发展往往是这样的套路:你越是期待的,结果却事与愿违。

当时正值公司刚刚确定了新的一年的几个重点战略方向,其中一个就是面向Goverment的智慧城市建设方向,我们戏称:”To G业务”。公司大老板希望我能以一个技术架构师的角色,对公司整个面向智慧城市的技术架构、产品和服务进行梳理,形成公司对应To G方向的核心产品套件和方法论。当时的我对于什么是智慧城市基本上是小白一个,无奈老板发话,只能硬着头皮上。

在后续若干个月的梳理过程中,我渐渐发现这个工作中技术绝对不是主要的因素,重要的是对智慧城市的深入理解。而智慧城市建设的纷繁芜杂,加上没有实战经验,驾驭起来又岂能是短短几个月的事情?输出的成果物我自己感觉都很苍白无力。那个阶段,我在各方面是备受煎熬:工作量是庞大的,老板要求也高,关键是还没有什么成就感。并且渐渐地我发现大老板似乎希望我能继续在smart city这个领域继续钻研下去,甚至成为专家型选手。这显然与我对自己的定位和规划不符,我没有成为智慧城市专家的愿望和热情,自觉也没有这方面的能力。于是在工作了大致五个月的时候,在输出了近六本成果物之后(没错,我这几个月的成果物就是一本本薄薄的书,如果你在市面上能有幸看到署有我的名字的关于智慧城市的著作,也不要惊讶哦^0^,不过看不到的可能性更大),我选择了离开。

这次跳槽从一般意义来说,也许是失败的。但个人觉得这几个月我还是有很多收获的。Hard模式让我个人也有了更快的成长,尤其是在内心抗压上。同时,在其他方面也有不少收获,这些收获不是在技术层面,而是在格局、眼界以及接触的人的圈子方面:由于角色的原因,接触到很多外部公司的相对级别较高的人,和他们一起交流,增长了许多见识。

二、蛰伏

离开的时候其实有几个机会,但是考虑到东北当前经济环境下的创业企业的情况,于是决定先回到老东家,不过这次换到了另外一个部门(以前的老领导负责的一个部门,这里感谢老领导收留^0^),我也从新回归技术兼部分技术管理,我把这个阶段称为蛰伏。一方面,将当前团队的产品打磨好,一方面等待下一次“变”的机会。

顺便简要说一下当前所做的事情。当前团队规模不大,5 dev + 1美工美女,致力于制作一个相对通用的互联网产品运营平台,一个类APaaS平台,与国内主流运营渠道能力对接(比如:微信等),简化商家在产品营销和运营时应用开发、部署和运维的门槛,为应用提供支持负载均衡和快速弹性伸缩的环境,以保障应用在业务波峰也可以正常运作。平台的底层采用的是Kubernetes,这也是10月份以来我为何发表大量有关容器Kubernetes博文的原因。团队目前也在摸着石头过河,无论是对方向的把握还是对技术的探索。

团队采用了一些较新的小众流行的“技术栈“,包括:golangvue.js 2.0等。目前团队还在招前后端开发,沈阳的朋友有意者可以留言联系。

三、小目标

优秀是一种习惯。反过来,不是所有习惯都能让你优秀,比如那些众所周知的“坏毛病”。

2017,从现在开始,我要改掉如下的一些“坏毛病”:

  • 不吃垃圾食品,比如方便面、KFC等;
  • 不躺着床上看书,除非是为了入眠^_^;
  • 拖延症,或多或少还是有一点的。

总是告诉女儿:活到老学到老!作为爸爸,必须带头身体力行,2017自然不能忘记学习。除了当前工作涉及到的golang、docker、k8s的应用和深入之外,目前考虑到的可能学习和实践的方向还包括:

  • AI:近两年大热的方向,特别是机器学习这一支。如果不跟上,就要落伍了。不过进入AI领地不是那么容易。要学的太多,而且很有难度。
  • Blockly:在国外,尤其是主流欧美国家,“编程一小时”活动开展的如火如荼,无论成人还是未成年的儿童少年,对于编程的兴趣与日俱增。我相信这一趋势将来也必将在国内“蔓延”开来。而Google开源的Blockly作为很多编程网站开发编程activity的基础是值得学习、研究和实践的。

img{368x512}
图:女儿在接受编程思维训练

四、自我寄语

新一年,风险与机遇并存。
但我心中那团火,永不熄!

使用Visual Studio Code辅助Go源码编写

作为VIMer,日常编码中,Vim编辑器依然是我的首选。以前以C语言为主要语言的时候是这样,现在以Go为主要语言时亦是这样。不过近期发现Mac上使用Vim在编写Go代码时,Vim时不时的“抽风”:出现一些“屏幕字符被篡改”的问题,比如下面这幅图中”func”变成了”fknc”:

虽然一段时间后,显示会自动更正过来,但这种“篡改”是会让你产生“幻觉”的。你会想:是不是我真的将”func”写成”fknc”了呢?久而久之,这个瑕疵将会影响你的编码效率。至于为何会出现这个问题,初步怀疑可能是因为vim加载较多插件导致的一些性能问题,我在安装了Ubuntu 16.04的台式机上至今还没发现这个问题(相同的.vimrc配置)。

于是,我打算找一款辅助编辑器,用于在被上面这个问题折磨得开始“厌恶”Vim的某些时候,切换一下,平复一下心情^0^。我看中了Microsoft开源的Visual Studio Code,简称:VSCode。

一、与Microsoft的Visual Studio的渊源

Microsoft做IDE还是很专业的,也是很认真的。大学那时候学C,嫌弃Turbo C太简陋,基本上都是在D版Visual Studio 6.0上完成各种作业和小程序的制作的。后来在2001年微软发布了.net战略,发布了C#语言,同时也发布了Visual Studio .NET IDE。估计我也算是国内第一批使用到Visual Studio.NET IDE的人吧,那时候微软俱乐部在校园里免费发送Vs.net beta版光盘,我拿到了一份,并第一时间体验了vs.net。Visual Studio .NET与之前的VS 6.0有着天壤之别,功能强大,界面也做了重新设计,支持微软的各种语言,包括C#、C/C++(包括managed c++)、VB、ASP.net等,并在一年后的正式版发布后,逐渐在桌面应用程序开发中成为霸主,把那个时候在IDE领域的竞争对手Borland公司彻底打垮。但Visual Studio从此也变得更加庞大和臃肿,安装一个VS,没有几个G空间是不行的。想想那个时候机器的配置,跑个VS.net还真是心有而力不足。

工作之后,进入服务端编程领域,结识了Unix、Linux以及VimGCC,就再也没怎么碰过Visual Studio。随着工作OS也从Windows切换到Ubuntu,基本就和VS绝缘了。之后随着Java语言成为企业级应用的主角、Web时代的到来以及开源IDE(比如:Eclipse)的兴起,微软的Visual Studio不再那么耀眼,或者说是人们对于IDE的关注并不像开发GUI程序那个年代那么强烈了。但鉴于微软自身产品体系的庞大,VS始终在市场中占有一席之地。

而近些年,一些跨平台、轻量级、插件结构、支持智能感知、可随意定制的文本编辑器的出现,比如:Sublime TextAtom等让开发人员喜不自禁。这些编辑器并非定位于IDE,但功能又不输给IDE很多,尤其在支持编码、调试这些环节,它们完全可以与专业IDE媲美,但资源消耗却是像Visual Studio、Eclipse这样大而全的IDE所无法匹敌的。而Visual Studio Code恰是微软在这方面的一个尝试,也是微软最新公司战略的体现之一:拥抱所有开发者(不仅仅是Windows上的哦)。

二、VSCode安装

VSCode发布于2015年4月的Build大会上。发布后,迅速得到开发者响应,大家普遍反映:VSCode性能不错、关注细节、体验良好,虽然当时VSCode的插件还不算丰富。一年多过去后,VSCode已经演化到了1.8.1版本(截至2016年12月末),支持所有主流编程语言的开发,配套的插件也十分丰富了。VSCode的安装简单的很,这一向都是微软的强项,你可以在其官方站上下载到各个平台的安装包(Linux平台也有.deb/.rpm两种包格式供选择,并提供32bit和64bit两种版本)。下载后安装即可。

1、VSCode配置和数据存储路径

VSCode安装后,一般不必关心其配置和数据存储路径的位置。但作为有一些Geek精神的developer来说,弄清楚其安装和配置的来龙去脉还是很有意义的。

在Mac上:

VSCode存储运行数据和配置文件的目录在:~/Library/Application Support/Code下:

 ~/Library/Application Support/Code]$ls
Backups/        CachedData/        Cookies-journal        Local Storage/        User/
Cache/            Cookies            GPUCache/        Preferences        storage.json

$ls User
keybindings.json    locale.json        settings.json        snippets/        workspaceStorage/

在Ubuntu中:

VSCode存储运行数据和配置文件的目录在~/.config/Code下面:

~/.config/Code$ ls
Backups  Cache  CachedData  Cookies  Cookies-journal  GPUCache  Local Storage  storage.json  User

至于Windows平台,请自行探索^_^。

2、启动方式

VSCode有两种启动方式:桌面启动和命令行启动。桌面启动自不必说了。命令行启动的示例如下:

$ code main.go

code命令会打开一个VSCode窗口并加载命令参数中的文件内容,这里是main.go。

三、VSCode的配置

一般来说,VSCode启动即可用了。但要想发挥出VSCode的能量,我们必须对其进行一番配置。VSCode的配置有几十上百项,这里无法全覆盖,仅说明一下我个人比较关注的。

1、安装插件

像VSCode这种小清新文本编辑器要想对编程语言有很好的支持,必须安装相应语言的插件。以Go为例,我们至少要安装vscode-go插件。vscode-go之于VSCode,就好比vim-go之于VIM。并且和vim-go类似,vscode-go实现的各种Features也是依赖诸多已存在的Go周边工具,包括:

gocode: go get -u -v github.com/nsf/gocode
godef: go get -u -v github.com/rogpeppe/godef
gogetdoc: go get -u -v github.com/zmb3/gogetdoc
golint: go get -u -v github.com/golang/lint/golint
go-outline: go get -u -v github.com/lukehoban/go-outline
goreturns: go get -u -v sourcegraph.com/sqs/goreturns
gorename: go get -u -v golang.org/x/tools/cmd/gorename
gopkgs: go get -u -v github.com/tpng/gopkgs
go-symbols: go get -u -v github.com/newhook/go-symbols
guru: go get -u -v golang.org/x/tools/cmd/guru
gotests: go get -u -v github.com/cweill/gotests/...

因此,要想实现vscode-go官网页面中demo中哪些神奇的Feature,你必须将上面的这些依赖工具逐一安装成功。如果缺少一个依赖工具,VSCode会在窗口右下角的状态栏里显示:“Analysis Tools Missing”字样,以提示你安装这些工具。

VSCode当然也支持Vim-mode的编辑模式,如果你也和我一样,喜欢用vim-mode在VSCode中进行编辑,可以安装VSCodeVim插件

VSCode的插件安装方式分为两种:在线安装和VSIX方式安装。

在线安装,顾名思义,即在VSCode的窗口左侧边栏中点击“Extensions”按钮,在打开的Extensions搜索框中搜索你想要的插件名称,或者选择预制的条件获得插件信息。选中你要安装的插件,点击“Install”按钮即可完成安装。

VSIX安装:即到插件官网将插件文件下载到本地(插件安装文件一般以.vsix或.zip结尾),在窗口中选择:”Install from VSIX…”,选择你下载的插件文件即可。

安装后的插件都被放在~/.vscode/extensions目录下(mac和linux)。

2、更改语言设置

VSCode在初次启动时会判断当前系统语言,并以相应的语言作为默认窗口显示语言。比如:我的是中文OS X系统,那么默认VSCode的窗口文字都是中文。如果我要将其改为英文,应该如何操作呢?

F1登场!这里的F1可不是赛车比赛,而是快捷键F1,估计也是整个VSCode最常用的快捷键之一了。敲击F1后,VSCode会显示其“Command Palette”输入框,这里面包含了当前VSCode可以执行的所有操作命令,支持Search。我们输入”language”,在搜索结果中选择“Configure Language”,VSCode打开一个新的编辑窗口,加载~/Library/Application Support/Code/User/locale.json文件:

{
    // 定义 VSCode 的显示语言。
    // 请参阅 https://go.microsoft.com/fwlink/?LinkId=761051,了解支持的语言列表。
    // 要更改值需要重启 VSCode。
    "locale": "zh-cn"
}

当前语言为中文,如果我们要将其改为英文,则修改该文件中的”locale”项:

{
    // 定义 VSCode 的显示语言。
    // 请参阅 https://go.microsoft.com/fwlink/?LinkId=761051,了解支持的语言列表。
    // 要更改值需要重启 VSCode。
    "locale": "en-US"
}

保存,重启VSCode。再次启动的VSCode将会以英文界面示人了。

3、User Settings和Workspace Settings

UserSettings是一种“全局”设置,而Workspace Settings则顾名思义,是一种针对一个特定目录或project的设置。

UserSettings设置后的数据保存在~/Library/Application Support/Code下(以mac为例),而Workspace Setting设置后的数据则保存在某个项目特定目录下的.vscode目录下。

在菜单栏,选择【Preferences -> User Settings】可以打开~/Library/Application Support/Code/User/settings.json文件。默认情况下,该文件为空。VSCode采用默认设置。如果你要个性化设置,那么可将对应的配置项copy一份到settings.json中,并赋予其新值,保存即可。新值将覆盖默认值。以字体大小为例,我们将默认的editor.fontSize 12改为10:

// Place your settings in this file to overwrite the default settings
{
    "editor.fontSize": 10,
}

保存后,可以看到窗口中所有文字的Size都变小了。

在菜单栏,选择【Preferences -> Workspace Settings】可打开当前工作目录下的.vscode的settings.json文件,其工作原理和配置方法与User Settings一样,只是生效范围仅限于该工作区范畴。

4、Color Theme

VSCode内置了主流的配色方案,比如:monokai、solarized dark/light等。F1,输入”color”搜索,选择:“Perefences: Color Theme”(在MAC上也可以用cmd+k, cmd+t打开),在下拉列表中选择你喜欢的配色Theme即可,即可生效。

四、vscode-go的使用

前面说过,和vim-go一样,vscode-go插件实现了Go编码中需要的各种功能:自动format、自动增删import、build on save、lint on save、定义跳转、原型信息快速提示、自动补全、code snippets等。另外它通过带颜色的波浪线提示代码问题(虽然有时候反应有点慢),包括语法问题、不符合idiomatic go规则的问题(比如appId这个命名,它会建议你改为appID)等。

code snippets非常好用,内置的code snippets在~/.vscode/extensions/lukehoban.Go-0.6.51/snippets/go.json中可以找到,类似这样的定义:

//~/.vscode/extensions/lukehoban.Go-0.6.51/snippets/go.json
{
        ".source.go": {
                "single import": {
                        "prefix": "im",
                        "body": "import \"${1:package}\""
                },
                "multiple imports": {
                        "prefix": "ims",
                        "body": "import (\n\t\"${1:package}\"\n)"
                },
                "single constant": {
                        "prefix": "co",
                        "body": "const ${1:name} = ${2:value}"
                },
                "multiple constants": {
                        "prefix": "cos",
                        "body": "const (\n\t${1:name} = ${2:value}\n)"
                },
                "type interface declaration": {
                        "prefix": "tyi",
                        "body": "type ${1:name} interface {\n\t$0\n}"
                },
                "type struct declaration": {
                        "prefix": "tys",
                        "body": "type ${1:name} struct {\n\t$0\n}"
                },
                "package main and main function": {
                        "prefix": "pkgm",
                        "body": "package main\n\nfunc main() {\n\t$0\n}"
                },
... ...

敲入”prefix”的值,比如”ims”,输入tab,vscode-go将为你展开为:

import (
    "package"
)

在使用vscode时遇到过一次代码自动补全“失灵”的问题。vscode-go只会提示:”PANIC,PANIC,PANIC”。经查,这个是gocode daemon的问题,我的解决方法是:

gocode close //关闭gocode daemon
gocode -s &  //重启之。

五、小结

在诸多轻量级编辑器中,我还是比较看好vscode的,毕竟其背后有着Microsoft积淀多年的IDE产品开发经验。并且和Microsoft以往产品最大的不同就是其是开源项目。

关于Vscode的使用和奇技淫巧可以参见其官方的这篇文档“VS Code Tips and Tricks”。

关于Vscode的各种周边工具和资料列表,请参考Awesome-vscode项目

快捷键往往是开发人员的最爱,VSCode官方制作了三个平台的VSCode的快捷键worksheet:

https://code.visualstudio.com/shortcuts/keyboard-shortcuts-windows.pdf

https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf

https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf

VSCode还在快速发展,离完善还有不小提升空间。比如:在使用过程中也发现了VSCode 窗口无响应或代码编辑错乱之情况。不过作为Go编码的一个辅助编辑器,VSCode还是完全胜任和超出预期的。




这里是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


文章

评论

  • 正在加载...

分类

标签

归档











更多