标签 Go 下的文章

GopherChina讲师专访

今年有幸收到GopherChina大会的组织者、beego开源项目的owner、《Go Web编程》的作者谢孟军童鞋的邀请,以讲师身份参加今年的GopherChina大会。下面是GopherChina对我这个讲师的专访稿^0^。该专访稿将同时被发布在公众号“Go中国(微信号:golangchina)”上面,可点击这里阅读。

1、首先介绍一下自己。

大家好!我叫白明(Tony Bai),目前是东软云科技的一名架构师,专职于服务端开发,日常工作主要使用Go语言。我算是国内较早接触Go语言的程序员兼Advocater了,平时在我的博客微博和微信公众号“iamtonybai”上经常发表一些关于Go语言的文章和Go生态圈内的信息。

在接触Go之前,我主要使用C语言开发电信领域的一些后端服务系统,拥有多年的电信领域产品研发和技术管理经验。我个人比较喜换钻研和分享技术,是《七周七语言》一书的译者之一,并且坚持写技术博客十余年。同时我也算是一个开源爱好者,也在github上分享过自己开发的几个小工具。

目前的主要研究领域包括:GoKubernetesDocker和儿童编程教育等。

2、回忆一下与Golang的渊源。是什么原因决定尝试Golang?自己用Go语言实现的第一个项目是什么?当时 Golang 有什么令人惊喜的表现,又有什么样的小不足,这个不足在Golang已经更新到1.8版本的时候是否已经得到改善?

众所周知,Go语言最初由Robert Griesemer, Ken ThompsonRob Pike在2007年末共同设计和实现,2009年11月份正式发布并开源,并于2012年3月份发布了1.0版本以及Go1规范。我就是在2012年开始接触Go的,那是缘于看到一份由Rob Pike主讲的3-day Go Course资料。从那份资料里,我了解到了Go的设计理念和Go语法。

由于之前浸淫于C语言多年,深知C语言在系统编程以及服务端编程方面的强大,同时也亲身体会到C的语法“陷阱”和C手工内存管理给开发者带来的苦恼。虽然那些年市面上也有其他主流语言可供选择,但在我看来,它们给我带来的心智负担太过沉重,比如:C++“宇宙无敌”的学习和使用复杂性、Java超大的资源消耗和庞大且纷繁芜杂的框架体系、动态语言(rubypython)无静态类型而导致运行时crash时调试的困难、函数式语言(如Haskellclisp)的过于小众和非主流。显然它们都不是我的菜。直到Go的出现,C程序员出身的我一下子就被这门新语言迷住了。

现在想起这件事来,我当时迷上Go应该主要由于以下几点原因:

* 静态类型语言、接近于C的性能(对于C程序员来说,这算是某种天然继承性)
* 简洁的语法
* 内置的并发支持
* GC
* 贯穿整个语言的正交设计和组合编程思路(兼容对OO的支持)
* 工具和功能全面的标准库

而且这几点也是这几年持续支撑我深入学习和使用Go语言的原因。

不过由于Go1刚出来时也十分小众,并且各方面功能还在完善中,我并没有在真实项目中使用Go,这种状况一直持续到2014年末。直到那时,我才在一个小项目中使用Go实现了一个微信公众号的协议接口。当时发现:使用Go实现一些安全协议真是非常方便,因为标准库里内置了很好的支持,比如:各种aes、sha256、tls算法实现。同时,Go内置的testing framework、gofmt、Go pprof工具的表现也是让我感觉用起来十分舒服。

如果非要说当时有什么不足之处的话,那只能是Go对debugging的支持明显不足。即便是到了目前最新的Go 1.8版本,Go在debugging方面虽然有所改善,但和C这样的传统语言来说依然有很大差距。不过好在我们有“print”这个无敌调试武器,Go的这个不足对我影响微乎甚微^0^。

当然随着Go在更多规模稍大项目的使用,Go的包管理问题逐渐浮出水面,这也是整个Go社区都想改进的事情。好在目前已经有了专门的Commitee来做这件事,最新的roadmap显示dep工具将在Go 1.10 dev cycle并入Go tools中。

3、2009年诞生至今,Go语言基本统治了云计算,作为最专业的Go语言专家,您认为这是由于它的哪些优雅的特性?Golang未来还会有什么样的改进和突破?

“作为最专业的Go语言专家”,这一称号的确不敢当。我觉得我个人只是国内Gopher普通一员,能为Go语言在国内的发展做点事情就很高兴了^0^。

Go自从1.5版本自举后,随着ssa优化、GC延迟优化的深入,Go在国内外的使用趋势确实是一片大好,尤其是Go问鼎2016年TIOBE编程语言排行榜的年度语言,让更多的程序员知道Go语言、了解Go语言和使用Go语言。在云计算成为当今IT行业常态的今天,Go在这方面已然成为一个重量级选手。从个人对Go的情感角度出发,我个人是希望Go语言能成为”21世纪的C语言”和云平台第一语言的。不过这是一个过程,需要时间,还需要依靠全世界Gopher和Go Community的共同努力才能实现的。

时代不同,语言的成长环境也有所不同。和上一代和两代的语言似乎有所不同,新一代编程语言是否能进入程序员们的法眼,是否值得程序员去投资,“背景”很重要,即所谓的编程语言也进入了“拼爹”时代。Go语言背靠Google这棵大树,又有Robert Griesemer, Ken Thompson, Rob Pike三巨头坐镇,是真正的“牛二代”,它自然就会得到不少程序员的青睐。我想这是Go吸引眼球的场外因素。

至于Go本身的语言特性,在上一个问题中,我已经做了初步阐述了,这里再补充几点:

* Go是一门以解决Google内部生产环境中的问题(大规模并发服务)为目标的、兼顾在语言设计层面解决一些软件工程问题的面向大规模并发服务的编程语言;
* 开发效率较高(对比主流的C、C++和Java),且执行效率与C相比,没有数量级级别的差异;
* 编译速度超快(相对其他需编译的主流语言),无需喝咖啡等待;
* Go1兼容性的承诺。

Go语言到目前已经演进到1.8版本,Go 1.9开发周期已经打开。今年夏天,Go 1.9发布后,Go似乎就到了版本演进的关键节点,是继续Go1兼容(Go 1.10、Go 1.11…),还是诞生Go2规范,目前并没有明确信息。不过未来的改进和突破,我觉得还是应该建立在Go语言设计的初衷和设计原则之上,这些初衷和原则包括:

目标:
 * 高效的静态编译语言
 * 动态语言的易用性
 * 类型安全和内存安全
 * 对并发和通信的良好支持
 * 高效、低/趋于零延迟的GC
 * 高速编译

原则:

 * 保持概念正交
 * 保持语法简单
 * 保持类型系统精炼,无type hierarchy

从这些年Go的发展来看,基本都是遵循以上目标和原则的。即便Go2出来,不符合上述原则的feature,也是很难加入到Go2里面的。

4、之前是否有关注到Gopher China大会,对大会的风格和内容有什么样的印象?

对于中国大陆地区规模最大,最具影响力的Go大会,我是从第一届就开始关注了,虽然第一届因故没能参加^0^。在去年举行的第二届大会,我是作为早鸟观众参与的哦。而本届则有幸成为讲师。

GopherChina从诞生至今,规模日益扩大,据说今年的参会人员可能突破1000人。而且GopherChina大会从第一届就汇聚了国内一线IT厂商的精英技术人员作为讲师,并得到了Go core team的大力支持。在每一届大会都会邀请到Go team中的核心开发人员参会布道,甚至在第一届大会时还邀请到了Go三巨头之一的Robert Griesemer,极大满足了国内Gopher的求知欲。

而且就我观察,每一届GopherChina大会的主题都涵盖:语言、工程、新兴领域应用等多个环节,颇具多样性和全面性。

5、作为讲师也是参会者,对于今年的Gopher China大会的哪些议题有所期待?

GopherChina每一届都是高手云集,这届也不例外。今年大会的每个议题都令我很是期待。

6、现在很多企业项目都在准备转Go,对于这些项目的负责人有没有建议和经验分享?

Go语言以极易上手著称,同时Go也是一门十分简单的语言(相对于其他主流语言),C、Cpp、Java、Python等程序员转型到Go的曲线并不陡峭,因此团队整体转型为Go的门槛并不高。但还是要有几点是项目负责人需要认真考虑的:

(1) 确认Go适合项目的应用场景

Go不是万能的,不能为了用Go而去用Go。但Go从最初定位为一门系统语言(Sytem Programming Language)逐渐演化成为一门通用语言(General Purpose Programming Language),说明其适应性和应用范围已经十分广泛,目前在云计算、Web开发、大数据、游戏、数据库、IDE、容器等领域均有大规模应用案例。但即便这样,仍然在有些领域的应用需要谨慎,比如嵌入式领域、比如mobile开发,虽然在这两个方面Go都做出了很大的努力,但似乎并没有较大的突破。

(2) 以终为始,从开始就参考Go的最佳实践

Go经过若干年的演化发展,逐渐形成了一些最佳实践,包括:项目代码组织、命名、惯用法、测试方法、错误处理、接口使用等。建议多看官方的talks、blog和世界范围内Go大会的presentation video。

(3) 单元测试全程保障

Go内置了单元测试框架,而单元测试是检验代码设计好坏的基础,也是代码重构的先决条件。建议项目从始至终都要优先考虑对代码编写测试代码。

(4) 充分利用标准库

在Go的应用实践中,你会发现Go标准库已经为你提供了大部分你要使用的功能。甚至有一些极端的Go纯粹主义者只愿意标准库中的函数和方法。Go标准库凝聚了Go team以及相关Contributor的Go代码精华,其稳定性绝对值得信赖。充分和广泛利用标准库也便于项目代码组织、构建和迁移。

(5) 基于go tool建立代码metric视图

对于那些性能敏感的系统,建议在内部环境基于go tool建立起代码的metric视图,监控代码变化给系统性能等带来的影响,利于问题诊断。

最后,请及时反馈Go语言自身问题,你的反馈是Go语言演化的动力

7、有没有你觉得很酷的Gopher?可以回答自己哟~

在github.com/golang/go上,我经常关注Russ Cox的代码。众所周知,Russ Cox是Go核心代码提交次数最多的member,他也除三巨头之外,对Go演化影响着最大的人之一。从近两年的Go team开发活动来看,Russ Cox开发效率很高,并且提出的proposal思维之缜密和全面令人叹服。

Dave Cheney是另一个我经常关注的Gopher,他也是第二届GopherChina大会的受邀讲师。他不遗余力的“鼓吹”Go,并从Go 1.6版本开始,发起了Go Global release party ,成为Go Community又一个节日。他不仅是Go community中的意见领袖,同时也为Go社区贡献不少有用的工具和思想,包括:gberrors等。

Dmitry Vyukov,前Intel Black Belt级工程师,现Google员工,虽然他不是专职Go team的人,但他却是Go scheduler当前版本的核心实现者。虽然近两年似乎在golang的投入并不是那么多,但依然成果丰硕,Go Execution Tracergo-fuzz(据说要加入go核心)都是他的杰作。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

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

在已经过去的2016年Go语言继在2009年之后再次成为编程语言界的明星- 问鼎TIOBE 2016年度语言。这与Go team、Go community和全世界的Gophers的努力是分不开的。按计划在这个2月份,Go team将正式发布Go 1.8版本(截至目前,Go的最新版本是Go 1.8rc3)。在这里我们一起来看一下在Go 1.8版本中都有哪些值得Gopher们关注的变化。

一、语言(Language)

Go 1.8版本依旧坚守Go Team之前的承诺,即Go1兼容性:使用Go 1.7及以前版本编写的Go代码,理论上都可以通过Go 1.8进行编译并运行。因此在臆想中的Go 2.0变成现实之前,每个Go Release版本在语言这方面的“改变”都会是十分微小的。

1、仅tags不同的两个struct可以相互做显式类型转换

在Go 1.8版本以前,两个struct即便字段个数相同且每个字段类型均一样,但如果某个字段的tag描述不一样,这两个struct相互间也不能做显式类型转换,比如:

//go18-examples/language/structtag.go
package main

import "fmt"

type XmlEventRegRequest struct {
    AppID     string `xml:"appid"`
    NeedReply int    `xml:"Reply,omitempty"`
}

type JsonEventRegRequest struct {
    AppID     string `json:"appid"`
    NeedReply int    `json:"reply,omitempty"`
}

func convert(in *XmlEventRegRequest) *JsonEventRegRequest {
    out := &JsonEventRegRequest{}
    *out = (JsonEventRegRequest)(*in)
    return out
}

func main() {
    in := XmlEventRegRequest{
        AppID:     "wx12345678",
        NeedReply: 1,
    }
    out := convert(&in)
    fmt.Println(out)
}

采用Go 1.7.4版本go compiler进行编译,我们会得到如下错误输出:

$go build structtag.go
# command-line-arguments
./structtag.go:17: cannot convert *in (type XmlEventRegRequest) to type JsonEventRegRequest

但在Go 1.8中,gc将忽略tag值的不同,使得显式类型转换成为可能:

$go run structtag.go
&{wx12345678 1}

改变虽小,但带来的便利却不小,否则针对上面代码中的convert,我们只能做逐一字段赋值了。

2、浮点常量的指数部分至少支持16bits长

在Go 1.8版本之前的The Go Programming Language Specificaton中,关于浮点数常量的指数部分的描述如下:

Represent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed exponent of at least 32 bits.

在Go 1.8版本中,文档中对于浮点数常量指数部分的长度的实现的条件放宽了,由支持最少32bit,放宽到最少支持16bits:

Represent floating-point constants, including the parts of a complex constant, with a mantissa of at least 256 bits and a signed binary exponent of at least 16 bits.

但Go 1.8版本go compiler实际仍然支持至少32bits的指数部分长度,因此这个改变对现存的所有Go源码不会造成影响。

二、标准库(Standard Library)

Go号称是一门”Batteries Included”编程语言。“Batteries Included”指的就是Go语言强大的标准库。使用Go标准库,你可以完成绝大部分你想要的功能,而无需再使用第三方库。Go语言的每次版本更新,都会在标准库环节增加强大的功能、提升性能或是提高使用上的便利性。每次版本更新,标准库也是改动最大的部分。这次也不例外,我们逐一来看。

1、便于slice sort的sort.Slice函数

在Go 1.8之前我们要对一个slice进行sort,需要定义出实现了下面接口的slice type:

//$GOROOT/src/sort.go
... ...
type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less reports whether the element with
    // index i should sort before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

标准库定义了一些应对常见类型slice的sort类型以及对应的函数:

StringSlice -> sort.Strings
IntSlice -> sort.Ints
Float64Slice -> sort.Float64s

但即便如此,对于用户定义的struct或其他自定义类型的slice进行排序仍需定义一个新type,比如下面这个例子中的TiboeIndexByRank:

//go18-examples/stdlib/sort/sortslice-before-go18.go
package main

import (
    "fmt"
    "sort"
)

type Lang struct {
    Name string
    Rank int
}

type TiboeIndexByRank []Lang

func (l TiboeIndexByRank) Len() int           { return len(l) }
func (l TiboeIndexByRank) Less(i, j int) bool { return l[i].Rank < l[j].Rank }
func (l TiboeIndexByRank) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }

func main() {
    langs := []Lang{
        {"rust", 2},
        {"go", 1},
        {"swift", 3},
    }
    sort.Sort(TiboeIndexByRank(langs))
    fmt.Printf("%v\n", langs)
}

$go run sortslice-before-go18.go
[{go 1} {rust 2} {swift 3}]

从上面的例子可以看到,我们要对[]Lang这个slice进行排序,我们就需要为之定义一个专门用于排序的类型:这里是TiboeIndexByRank,并让其实现sort.Interface接口。使用过sort包的gophers们可能都意识到了,我们在为新的slice type实现sort.Interface接口时,那三个方法的Body几乎每次都是一样的。为了使得gopher们在排序slice时编码更为简化和便捷,减少copy&paste,Go 1.8为slice type新增了三个函数:Slice、SliceStable和SliceIsSorted。我们重新用Go 1.8的sort.Slice函数实现上面例子中的排序需求,代码如下:

//go18-examples/stdlib/sort/sortslice-in-go18.go
package main

import (
    "fmt"
    "sort"
)

type Lang struct {
    Name string
    Rank int
}

func main() {
    langs := []Lang{
        {"rust", 2},
        {"go", 1},
        {"swift", 3},
    }
    sort.Slice(langs, func(i, j int) bool { return langs[i].Rank < langs[j].Rank })
    fmt.Printf("%v\n", langs)
}

$go run sortslice-in-go18.go
[{go 1} {rust 2} {swift 3}]

实现sort,需要三要素:Len、Swap和Less。在1.8之前,我们通过实现sort.Interface实现了这三个要素;而在1.8版本里,Slice函数通过reflect获取到swap和length,通过结合闭包实现的less参数让Less要素也具备了。我们从下面sort.Slice的源码可以看出这一点:

// $GOROOT/src/sort/sort.go
... ...
func Slice(slice interface{}, less func(i, j int) bool) {
    rv := reflect.ValueOf(slice)
    swap := reflect.Swapper(slice)
    length := rv.Len()
    quickSort_func(lessSwap{less, swap}, 0, length, maxDepth(length))
}

2、支持HTTP/2 Push

继在Go 1.6版本全面支持HTTP/2之后,Go 1.8又新增了对HTTP/2 Push的支持。HTTP/2是在HTTPS的基础上的下一代HTTP协议,虽然当前HTTPS的应用尚不是十分广泛。而HTTP/2 Push是HTTP/2的一个重要特性,无疑其提出的初衷也仍然是为了改善网络传输性能,提高Web服务的用户侧体验。这里我们可以借用知名网络提供商Cloudflare blog上的一幅示意图来诠释HTTP/2 Push究竟是什么:

img{512x368}

从上图中,我们可以看到:当Browser向Server发起Get page.html请求后,在同一条TCP Connection上,Server主动将style.css和image.png两个资源文件推送(Push)给了Browser。这是由于Server端启用了HTTP/2 Push机制,并预测判断Browser很可能会在接下来发起Get style.css和image.png两个资源的请求。这是一种典型的:“你可能会需要,但即使你不要,我也推给你”的处世哲学^0^。这种机制虽然在一定程度上能改善网络传输性能(减少Client发起Get的次数),但也可能造成带宽的浪费,因为这些主动推送给Browser的资源很可能是Browser所不需要的或是已经在Browser cache中存在的资源。

接下来,我们来看看Go 1.8是如何在net/http包中提供对HTTP/2 Push的支持的。由于HTTP/2是基于HTTPS的,因此我们先使用generate_cert.go生成程序所需的私钥和证书:

// 在go18-examples/stdlib/http2-push目录下,执行:

$go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1
2017/01/27 10:58:01 written cert.pem
2017/01/27 10:58:01 written key.pem

支持HTTP/2 Push的server端代码如下:

// go18-examples/stdlib/http2-push/server.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

const mainJS = `document.write('Hello World!');`

func main() {
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/" {
            http.NotFound(w, r)
            return
        }
        pusher, ok := w.(http.Pusher)
        if ok {
            // If it's a HTTP/2 Server.
            // Push is supported. Try pushing rather than waiting for the browser.
            if err := pusher.Push("/static/img/gopherizeme.png", nil); err != nil {
                log.Printf("Failed to push: %v", err)
            }
        }
        fmt.Fprintf(w, `<html>
<head>
<title>Hello Go 1.8</title>
</head>
<body>
    <img src="/static/img/gopherizeme.png"></img>
</body>
</html>
`)
    })
    log.Fatal(http.ListenAndServeTLS(":8080", "./cert.pem", "./key.pem", nil))
}

运行这段代码,打开Google Chrome浏览器,输入:https://127.0.0.1:8080,忽略浏览器的访问非受信网站的警告,继续浏览你就能看到下面的页面(这里打开了Chrome的“检查”功能):

img{512x368}

从示例图中的“检查”窗口,我们可以看到gopherizeme.png这个image资源就是Server主动推送给客户端的,这样浏览器在Get /后无需再发起一次Get /static/img/gopherizeme.png的请求了。

而这一切的背后,其实是HTTP/2的ResponseWriter实现了Go 1.8新增的http.Pusher interface:

// $GOROOT/src/net/http/http.go

// Pusher is the interface implemented by ResponseWriters that support
// HTTP/2 server push. For more background, see
// https://tools.ietf.org/html/rfc7540#section-8.2.
type Pusher interface {
    ... ...
    Push(target string, opts *PushOptions) error
}

3、支持HTTP Server优雅退出

Go 1.8中增加对HTTP Server优雅退出(gracefullly exit)的支持,对应的新增方法为:

func (srv *Server) Shutdown(ctx context.Context) error

和server.Close在调用时瞬间关闭所有active的Listeners和所有状态为New、Active或idle的connections不同,server.Shutdown首先关闭所有active Listeners和所有处于idle状态的Connections,然后无限等待那些处于active状态的connection变为idle状态后,关闭它们并server退出。如果有一个connection依然处于active状态,那么server将一直block在那里。因此Shutdown接受一个context参数,调用者可以通过context传入一个Shutdown等待的超时时间。一旦超时,Shutdown将直接返回。对于仍然处理active状态的Connection,就任其自生自灭(通常是进程退出后,自动关闭)。通过Shutdown的源码我们也可以看出大致的原理:

// $GOROOT/src/net/http/server.go
... ...
func (srv *Server) Shutdown(ctx context.Context) error {
    atomic.AddInt32(&srv.inShutdown, 1)
    defer atomic.AddInt32(&srv.inShutdown, -1)

    srv.mu.Lock()
    lnerr := srv.closeListenersLocked()
    srv.closeDoneChanLocked()
    srv.mu.Unlock()

    ticker := time.NewTicker(shutdownPollInterval)
    defer ticker.Stop()
    for {
        if srv.closeIdleConns() {
            return lnerr
        }
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
        }
    }
}

我们来编写一个例子:

// go18-examples/stdlib/graceful/server.go

import (
    "context"
    "io"
    "log"
    "net/http"
    "os"
    "os/signal"
    "time"
)

func main() {
    exit := make(chan os.Signal)
    signal.Notify(exit, os.Interrupt)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        log.Println("Handle a new request:", *r)
        time.Sleep(10 * time.Second)
        log.Println("Handle the request ok!")
        io.WriteString(w, "Finished!")
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: http.DefaultServeMux,
    }

    go func() {
        if err := srv.ListenAndServe(); err != nil {
            log.Printf("listen: %s\n", err)
        }
    }()

    <-exit // wait for SIGINT
    log.Println("Shutting down server...")

    // Wait no longer than 30 seconds before halting
    ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
    err := srv.Shutdown(ctx)

    log.Println("Server gracefully stopped:", err)
}

在上述例子中,我们通过设置Linux Signal的处理函数来拦截Linux Interrupt信号并处理。我们通过context给Shutdown传入30s的超时参数,这样Shutdown在退出之前会给各个Active connections 30s的退出时间。下面分为几种情况run一下这个例子:

a) 当前无active connections

在这种情况下,我们run上述demo,ctrl + C后,上述demo直接退出:

$go run server.go
^C2017/02/02 15:13:16 Shutting down server...
2017/02/02 15:13:16 Server gracefully stopped: <nil>

b) 当前有未处理完的active connections,ctx 超时

为了模拟这一情况,我们修改一下参数。让每个request handler的sleep时间为30s,而Shutdown ctx的超时时间改为10s。我们再来运行这个demo,并通过curl命令连接该server(curl -v http://localhost:8080),待连接成功后,再立即ctrl+c停止Server,待约10s后,我们得到如下日志:

$go run server.go
2017/02/02 15:15:57 Handle a new request: {GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.30.0] Accept:[*/*]] {} <nil> 0 [] false localhost:8080 map[] map[] <nil> map[] [::1]:52590 / <nil> <nil> <nil> 0xc420016700}
^C2017/02/02 15:15:59 Shutting down server...
2017/02/02 15:15:59 listen: http: Server closed
2017/02/02 15:16:09 Server gracefully stopped: context deadline exceeded

c) 当前有未处理完的active connections,ctx超时之前,这些connections处理ok了

我们将上述demo的参数还原,即request handler sleep 10s,而Shutdown ctx超时时间为30s,运行这个Demo后,通过curl命令连接该server,待连接成功后,再立即ctrl+c停止Server。等待约10s后,我们得到如下日志:

$go run server.go
2017/02/02 15:19:56 Handle a new request: {GET / HTTP/1.1 1 1 map[User-Agent:[curl/7.30.0] Accept:[*/*]] {} <nil> 0 [] false localhost:8080 map[] map[] <nil> map[] [::1]:52605 / <nil> <nil> <nil> 0xc420078500}
^C2017/02/02 15:19:59 Shutting down server...
2017/02/02 15:19:59 listen: http: Server closed
2017/02/02 15:20:06 Handle the request ok!
2017/02/02 15:20:06 Server gracefully stopped: <nil>

可以看出,当ctx超时之前,request处理ok,connection关闭。这时不再有active connection和idle connection了,Shutdown成功返回,server立即退出。

4、Mutex Contention Profiling

Go 1.8中runtime新增了对Mutex和RWMutex的profiling(剖析)支持。golang team成员,负责从go user角度去看待go team的work是否满足用户需求的Jaana B. Dogan在其个人站点上写了一篇介绍mutex profiling的文章,这里借用一下其中的Demo:

//go18-examples/stdlib/mutexprofile/mutexprofile.go

package main

import (
    "net/http"
    _ "net/http/pprof"
    "runtime"
    "sync"
)

func main() {
    var mu sync.Mutex
    var items = make(map[int]struct{})

    runtime.SetMutexProfileFraction(5)
    for i := 0; i < 1000*1000; i++ {
        go func(i int) {
            mu.Lock()
            defer mu.Unlock()
            items[i] = struct{}{}
        }(i)
    }

    http.ListenAndServe(":8888", nil)
}

运行该程序后,在浏览器中输入:http://localhost:8888/debug/pprof/mutex,你就可以看到有关该程序的mutex profile(耐心等待一小会儿,因为数据的采样需要一点点时间^0^):

--- mutex:
cycles/second=2000012082
sampling period=5
378803564 776 @ 0x106c4d1 0x13112ab 0x1059991

构建该程序,然后通过下面命令:

go build mutexprofile.go
./mutexprofile
go tool pprof mutexprofile http://localhost:8888/debug/pprof/mutex?debug=1

可以进入pprof交互界面,这个是所有用过go pprof工具gophers们所熟知的:

$go tool pprof mutexprofile http://localhost:8888/debug/pprof/mutex?debug=1
Fetching profile from http://localhost:8888/debug/pprof/mutex?debug=1
Saved profile in /Users/tony/pprof/pprof.mutexprofile.localhost:8888.contentions.delay.003.pb.gz
Entering interactive mode (type "help" for commands)
(pprof) list
Total: 12.98s
ROUTINE ======================== main.main.func1 in /Users/tony/Test/GoToolsProjects/src/github.com/bigwhite/experiments/go18-examples/stdlib/mutexprofile/mutexprofile.go
         0     12.98s (flat, cum)   100% of Total
         .          .     17:            mu.Lock()
         .          .     18:            defer mu.Unlock()
         .          .     19:            items[i] = struct{}{}
         .          .     20:        }(i)
         .          .     21:    }
         .     12.98s     22:
         .          .     23:    http.ListenAndServe(":8888", nil)
         .          .     24:}
ROUTINE ======================== runtime.goexit in /Users/tony/.bin/go18rc2/src/runtime/asm_amd64.s
         0     12.98s (flat, cum)   100% of Total
         .          .   2192:    RET
         .          .   2193:
         .          .   2194:// The top-most function running on a goroutine
         .          .   2195:// returns to goexit+PCQuantum.
         .          .   2196:TEXT runtime·goexit(SB),NOSPLIT,$0-0
         .     12.98s   2197:    BYTE    $0x90    // NOP
         .          .   2198:    CALL    runtime·goexit1(SB)    // does not return
         .          .   2199:    // traceback from goexit1 must hit code range of goexit
         .          .   2200:    BYTE    $0x90    // NOP
         .          .   2201:
         .          .   2202:TEXT runtime·prefetcht0(SB),NOSPLIT,$0-8
ROUTINE ======================== sync.(*Mutex).Unlock in /Users/tony/.bin/go18rc2/src/sync/mutex.go
    12.98s     12.98s (flat, cum)   100% of Total
         .          .    121:            return
         .          .    122:        }
         .          .    123:        // Grab the right to wake someone.
         .          .    124:        new = (old - 1<<mutexWaiterShift) | mutexWoken
         .          .    125:        if atomic.CompareAndSwapInt32(&m.state, old, new) {
    12.98s     12.98s    126:            runtime_Semrelease(&m.sema)
         .          .    127:            return
         .          .    128:        }
         .          .    129:        old = m.state
         .          .    130:    }
         .          .    131:}
(pprof) top10
1.29s of 1.29s total (  100%)
      flat  flat%   sum%        cum   cum%
     1.29s   100%   100%      1.29s   100%  sync.(*Mutex).Unlock
         0     0%   100%      1.29s   100%  main.main.func1
         0     0%   100%      1.29s   100%  runtime.goexit

go pprof的另外一个用法就是在go test时,mutexprofile同样支持这一点:

go test -mutexprofile=mutex.out
go tool pprof <test.binary> mutex.out

5、其他重要改动

Go 1.8标准库还有两个值得注意的改动,一个是:crypto/tls,另一个是database/sql。

HTTPS逐渐成为主流的今天,各个编程语言对HTTPS连接的底层加密协议- TLS协议支持的成熟度日益被人们所关注。Go 1.8给广大Gophers们带来了一个更为成熟、性能更好、更为安全的TLS实现,同时也增加了对一些TLS领域最新协议规范的支持。无论你是实现TLS Server端,还是Client端,都将从中获益。

Go 1.8在crypto/tls中提供了基于ChaCha20-Poly1305的cipher suite,其中ChaCha20是一种stream cipher算法;而Poly1305则是一种code authenticator算法。它们共同组成一个TLS suite。使用这个suite,将使得你的web service或站点具有更好的mobile浏览性能,这是因为传统的AES算法实现在没有硬件支持的情况下cost更多。因此,如果你在使用tls时没有指定cipher suite,那么Go 1.8会根据硬件支持情况(是否有AES的硬件支持),来决定是使用ChaCha20还是AES算法。除此之外,crypto/tls还实现了更为安全和高效的X25519密钥交换算法等。

Go 1.4以来,database/sql包的变化很小,但对于该包的feature需求却在与日俱增。终于在Go 1.8这个dev cycle中,govendor的作者Daniel TheophanesBrad Fitzpatrick的“指导”下,开始对database/sql进行“大规模”的改善。在Go 1.8中,借助于context.Context的帮助,database/sql增加了Cancelable Queries、SQL Database Type、Multiple Result Sets、Database ping、Named Parameters和Transaction Isolation等新Features。在GopherAcademy的Advent 2016系列文章中,我们可以看到Daniel Theophanes亲手撰写的文章,文章针对Go 1.8 database/sql包新增的features作了详细解释。

三、Go工具链(Go Toolchain)

在目前市面上的主流编程语言中,如果说Go的工具链在成熟度和完善度方面排第二,那没有语言敢称自己是第一吧^_^。Go 1.8在Go Toolchain上继续做着持续地改进,下面我们来逐一看看。

1、Plugins

Go在1.8版本中提供了对Plugin的初步支持,并且这种支持仅限于Linux。plugin这个术语在不同语言、不同情景上下文中有着不同的含义,那么什么是Go Plugin呢?

Go Plugin为Go程序提供了一种在运行时加载代码、执行代码以改变运行行为的能力,它实质上由两个部分组成:

  • go build -buildmode=plugin xx.go 构建xx.so plugin文件
  • 利用plugin包在运行时动态加载xx.so并执行xx.so中的代码

C程序员看到这里肯定会有似曾相识的赶脚,因为这和传统的动态共享库在概念上十分类似:

go build -buildmode=plugin xx.go 类似于 gcc -o xx.so -shared xx.c
go plugin包 类似于 linux上的dlopen/dlsym或windows上的LoadLibrary

我们来看一个例子!我们先来建立一个名为foo.so的go plugin:

//go18-examples/gotoolchain/plugins/foo.go

package main

import "fmt"

var V int
var v int

func init() {
        V = 17
        v = 23
        fmt.Println("init function in plugin foo")
}

func Foo(in string) string {
        return "Hello, " + in
}

func foo(in string) string {
        return "hello, " + in
}

通过go build命令将foo.go编译为foo.so:

# go build -buildmode=plugin foo.go
# ldd foo.so
    linux-vdso.so.1 =>  (0x00007ffe47f67000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9d06f4b000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9d06b82000)
    /lib64/ld-linux-x86-64.so.2 (0x000055c69cfcf000)

# nm foo.so|grep Foo
0000000000150010 t local.plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879.Foo
0000000000150010 T plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879.Foo
000000000036a0dc D type..namedata.Foo.

我们看到go plugin的.so文件就是一个标准的Linux动态共享库文件,我们可以通过nm命令查看.so中定义的各种符号。接下来,我们来load这个.so,并查找并调用相应符号:

//go18-examples/gotoolchain/plugins/main.go

package main

import (
        "fmt"
        "plugin"
        "time"
)

func init() {
        fmt.Println("init in main program")
}

func loadPlugin(i int) {
        fmt.Println("load plugin #", i)
        var err error
        fmt.Println("before opening the foo.so")

        p, err := plugin.Open("foo.so")
        if err != nil {
                fmt.Println("plugin Open error:", err)
                return
        }
        fmt.Println("after opening the foo.so")

        f, err := p.Lookup("Foo")
        if err != nil {
                fmt.Println("plugin Lookup symbol Foo error:", err)
        } else {
                fmt.Println(f.(func(string) string)("gophers"))
        }

        f, err = p.Lookup("foo")
        if err != nil {
                fmt.Println("plugin Lookup symbol foo error:", err)
        } else {
                fmt.Println(f.(func(string) string)("gophers"))
        }

        v, err := p.Lookup("V")
        if err != nil {
                fmt.Println("plugin Lookup symbol V error:", err)
        } else {
                fmt.Println(*v.(*int))
        }

        v, err = p.Lookup("v")
        if err != nil {
                fmt.Println("plugin Lookup symbol v error:", err)
        } else {
                fmt.Println(*v.(*int))
        }
        fmt.Println("load plugin #", i, "done")
}

func main() {
        var counter int = 1
        for {
                loadPlugin(counter)
                counter++
                time.Sleep(time.Second * 30)
        }
}

执行这个程序:

# go run main.go
init in main program
load plugin # 1
before opening the foo.so
init function in plugin foo
after opening the foo.so
Hello, gophers
plugin Lookup symbol foo error: plugin: symbol foo not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
17
plugin Lookup symbol v error: plugin: symbol v not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
load plugin # 1 done

load plugin # 2
before opening the foo.so
after opening the foo.so
Hello, gophers
plugin Lookup symbol foo error: plugin: symbol foo not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
17
plugin Lookup symbol v error: plugin: symbol v not found in plugin plugin/unnamed-69e21ef38d16a3fee5eb7b9e515c27a389067879
load plugin # 2 done
... ...

我们来分析一下这个执行结果!

a) foo.go中的代码也包含在main package下,但只是当foo.so被第一次加载时,foo.go中的init函数才会被执行;
b) foo.go中的exported function和variable才能被Lookup到,如Foo、V;查找unexported的变量和函数符号将得到error信息,如:“symbol foo not found in plugin”;
c) Lookup返回的是plugin.Symbol类型的值,plugin.Symbol是一个指向plugin中变量或函数的指针;
d) foo.go中的init在后续重复加载中并不会被执行。

注意:plugin.Lookup是goroutine-safe的。

在golang-dev group上,有人曾问过:buildmode=c-shared和buildmode=plugin有何差别?Go team member给出的答案如下:

The difference is mainly on the program that loads the shared library.

For c-shared, we can't assume anything about the host, so the c-shared dynamic library must be self-contained, but for plugin, we know the host program will be a Go program built with the same runtime version, so the toolchain can omit at least the runtime package from the dynamic library, and possibly more if it's certain that some packages are linked into the host program. (This optimization hasn't be implemented yet, but we need the distinction to enable this kind of optimization in the future.)

2、默认的GOPATH

Go team在Go 1.8以及后续版本会更加注重”Go语言的亲民性”,即进一步降低Go的入门使用门槛,让大家更加Happy的使用Go。对于一个Go初学者来说,一上来就进行GOPATH的设置很可能让其感到有些迷惑,甚至有挫折感,就像建立Java开发环境需要设置JAVA_HOME和CLASSPATH一样。Gophers们期望能做到Go的安装即可用。因此Go 1.8就在这方面做出了改进:支持默认的GOPATH。

在Linux/Mac系下,默认的GOPATH为$HOME/go,在Windows下,GOPATH默认路径为:%USERPROFILE%/go。你可以通过下面命令查看到这一结果:

$ go env
GOARCH="amd64"
GOBIN="/home/tonybai/.bin/go18rc3/bin"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/tonybai/go"
GORACE=""
GOROOT="/home/tonybai/.bin/go18rc3"
GOTOOLDIR="/home/tonybai/.bin/go18rc3/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build313929093=/tmp/go-build -gno-record-gcc-switches"
CXX="g++"
CGO_ENABLED="1"
PKG_CONFIG="pkg-config"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"

BTW,在Linux/Mac下,默认的GOROOT为/usr/local/go,如果你的Go环境没有安装到这个路径下,在没有设置$GOROOT环境变量的情况下,当你执行go subcommand相关命令时,你会看到如下错误:

$go env
go: cannot find GOROOT directory: /usr/local/go

3、其他变化

Go 1.8删除了Go 1.7中增加的用于关闭ssa新后端的”-ssa=0” compiler flag,并且将ssa backend扩展到所有architecture中,对ssa后端也进一步做了优化。与此同时,为了将来进一步的性能优化打基础,Go 1.8还引入了一个新编译器前端,当然这对于普通Gopher的Go使用并没有什么影响。

Go 1.8还新增go bug子命令,该命令会自动使用默认浏览器打开new issue页面,并将采集到的issue提交者的系统信息填入issue模板,以帮助gopher提交符合要求的go issue,下面是go bug打开的issue page的图示:

img{512x368}

四、性能变化(Performance Improvement)

无论是Gotoolchain、还是runtime(包括GC)的性能,一直都是Go team重点关注的领域。本次Go 1.8依旧给广大Gophers们带来了性能提升方面的惊喜。

首先,Go SSA后端扩展到所有architecture和新编译器前端的引入,将会给除X86-64之外架构上运行的Go代码带来约20-30%的运行性能提升。对于x86-64,虽然Go 1.7就已经开启了SSA,但Go 1.8对SSA做了进一步优化,x86-64上的Go代码依旧可能会得到10%以内的性能提升。

其次,Go 1.8持续对Go compiler和linker做性能优化,和1.7相比,平均编译链接的性能提升幅度在15%左右。虽然依旧没有达到Go 1.4的性能水准。不过,优化依旧在持续进行中,目标的达成是可期的。

再次,GC在低延迟方面的优化给了我们最大的惊喜。在Go 1.8中,由于消除了GC的“stop-the-world stack re-scanning”,使得GC STW(stop-the-world)的时间通常低于100微秒,甚至经常低于10微秒。当然这或多或少是以牺牲“吞吐”作为代价的。因此在Go 1.9中,GC的改进将持续进行,会在吞吐和低延迟上做一个很好的平衡。

最后,defer的性能消耗在Go 1.8中下降了一半,与此下降幅度相同的还有通过cgo在go中调用C代码的性能消耗。

五、小结兼参考资料

Go 1.8的变化不仅仅是以上这些,更多变化以及详细的描述请参考下面参考资料中的“Go 1.8 Release Notes”:

以上demo中的代码在这里可以找到。

理解Kubernetes网络之Flannel网络

第一次采用kube-up.sh脚本方式安装Kubernetes cluster目前运行良好,master node上的组件状态也始终是“没毛病”:

# kubectl get cs
NAME                 STATUS    MESSAGE              ERROR
controller-manager   Healthy   ok
scheduler            Healthy   ok
etcd-0               Healthy   {"health": "true"}

不过在第二次尝试用kubeadm安装和初始化Kubernetes cluster时遇到的各种网络问题还是让我“心有余悸”。于是趁上个周末,对Kubernetes的网络原理进行了一些针对性的学习。这里把对Kubernetes网络的理解记录一下和大家一起分享。

Kubernetes支持FlannelCalicoWeave network等多种cni网络Drivers,但由于学习过程使用的是第一个cluster的Flannel网络,这里的网络原理只针对k8s+Flannel网络。

一、环境+提示

凡涉及到Docker、Kubernetes这类正在active dev的开源项目的文章,我都不得不提一嘴,那就是随着K8s以及flannel的演化,本文中的一些说法可能不再正确。提醒大家:阅读此类技术文章务必结合“环境”。

这里我们使用的环境就是我第一次建立k8s cluster的环境:

# kube-apiserver --version
Kubernetes v1.3.7

# /opt/bin/flanneld -version
0.5.5

# /opt/bin/etcd -version
etcd Version: 3.0.12
Git SHA: 2d1e2e8
Go Version: go1.6.3
Go OS/Arch: linux/amd64

另外整个集群搭建在阿里云上,每个ECS上的OS及kernel版本:Ubuntu 14.04.4 LTS,3.19.0-70-generic。

在我的测试环境,有两个node:master node和一个minion node。master node参与workload的调度。所以你基本可以认为有两个minion node即可。

二、Kubernetes Cluster中的几个“网络”

之前的k8s cluster采用的是默认安装,即直接使用了配置脚本中(kubernetes/cluster/ubuntu/config-default.sh)自带的一些参数,比如:

//摘自kubernetes/cluster/ubuntu/config-default.sh

export nodes=${nodes:-"root@master_node_ip root@minion_node_ip"}
export SERVICE_CLUSTER_IP_RANGE=${SERVICE_CLUSTER_IP_RANGE:-192.168.3.0/24}
export FLANNEL_NET=${FLANNEL_NET:-172.16.0.0/16}

从这里我们能够识别出三个“网络”:

  • node network:承载kubernetes集群中各个“物理”Node(master和minion)通信的网络;
  • service network:由kubernetes集群中的Services所组成的“网络”;
  • flannel network: 即Pod网络,集群中承载各个Pod相互通信的网络。

node network自不必多说,node间通过你的本地局域网(无论是物理的还是虚拟的)通信。

service network比较特殊,每个新创建的service会被分配一个service IP,在当前集群中,这个IP的分配范围是192.168.3.0/24。不过这个IP并不“真实”,更像一个“占位符”并且只有入口流量,所谓的“network”也是“名不符实”的,后续我们会详尽说明。

flannel network是我们要理解的重点,cluster中各个Pod要实现相互通信,必须走这个网络,无论是在同一node上的Pod还是跨node的Pod。我们的cluster中,flannel net的分配范围是:172.16.0.0/16。

在进一步挖掘“原理”之前,我们先来直观认知一下service network和flannel network:

Service network(看cluster-ip一列):

# kubectl get services
NAME           CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
index-api      192.168.3.168   <none>        30080/TCP   18d
kubernetes     192.168.3.1     <none>        443/TCP     94d
my-nginx       192.168.3.179   <nodes>       80/TCP      90d
nginx-kit      192.168.3.196   <nodes>       80/TCP      12d
rbd-rest-api   192.168.3.22    <none>        8080/TCP    60d

Flannel network(看IP那列):

# kubectl get pod -o wide
NAME                           READY     STATUS    RESTARTS   AGE       IP            NODE
my-nginx-2395715568-gpljv      1/1       Running   6          91d       172.16.99.3   {master node ip}
nginx-kit-3872865736-rc8hr     2/2       Running   0          12d       172.16.57.7   {minion node ip}
... ...

三、平坦的Flannel网络

1、Kubenetes安装后的网络状态

首先让我们来看看:kube-up.sh在安装k8s集群时对各个K8s Node都动了什么手脚!

a) 修改docker default配置

在ubuntu 14.04下,docker的配置都在/etc/default/docker文件中。如果你曾经修改过该文件,那么kube-up.sh脚本方式安装完kubernetes后,你会发现/etc/default/docker已经变样了,只剩下了一行:

master node:
DOCKER_OPTS=" -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.99.1/24 --mtu=1450"

minion node:
DOCKER_OPTS=" -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.57.1/24 --mtu=1450"

可以看出kube-up.sh修改了Docker daemon的–bip选项,使得该node上docker daemon在该node的fannel subnet范围以内为启动的Docker container分配IP地址。

b) 在etcd中初始化flannel网络数据

多个node上的Flanneld依赖一个etcd cluster来做集中配置服务,etcd保证了所有node上flanned所看到的配置是一致的。同时每个node上的flanned监听etcd上的数据变化,实时感知集群中node的变化。

我们可以通过etcdctl查询到这些配置数据:

master node:

//flannel network配置
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get  /coreos.com/network/config
{"Network":"172.16.0.0/16", "Backend": {"Type": "vxlan"}}

# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls  /coreos.com/network/subnets
/coreos.com/network/subnets/172.16.99.0-24
/coreos.com/network/subnets/172.16.57.0-24

//某一node上的flanne subnet和vtep配置
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get  /coreos.com/network/subnets/172.16.99.0-24
{"PublicIP":"{master node ip}","BackendType":"vxlan","BackendData":{"VtepMAC":"b6:bf:4c:81:cf:3b"}}

minion node:
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get  /coreos.com/network/subnets/172.16.57.0-24
{"PublicIP":"{minion node ip}","BackendType":"vxlan","BackendData":{"VtepMAC":"d6:51:2e:80:5c:69"}}

或用etcd 提供的rest api:

# curl -L http://127.0.0.1:{etcd listen port}/v2/keys/coreos.com/network/config
{"action":"get","node":{"key":"/coreos.com/network/config","value":"{\"Network\":\"172.16.0.0/16\", \"Backend\": {\"Type\": \"vxlan\"}}","modifiedIndex":5,"createdIndex":5}}
c) 启动flanneld

kube-up.sh在每个Kubernetes node上启动了一个flanneld的程序:

# ps -ef|grep flanneld

master node:
root      1151     1  0  2016 ?        00:02:34 /opt/bin/flanneld --etcd-endpoints=http://127.0.0.1:{etcd listen port} --ip-masq --iface={master node ip}

minion node:
root     11940     1  0  2016 ?        00:07:05 /opt/bin/flanneld --etcd-endpoints=http://{master node ip}:{etcd listen port} --ip-masq --iface={minion node ip}

一旦flanneld启动,它将从etcd中读取配置,并请求获取一个subnet lease(租约),有效期目前是24hrs,并且监视etcd的数据更新。flanneld一旦获取subnet租约、配置完backend,它会将一些信息写入/run/flannel/subnet.env文件。

master node:
# cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.16.0.0/16
FLANNEL_SUBNET=172.16.99.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

minion node:
# cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.16.0.0/16
FLANNEL_SUBNET=172.16.57.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

当然flanneld的最大意义在于根据etcd中存储的全cluster的subnet信息,跨node传输flannel network中的数据包,这个后面会详细说明。

d) 创建flannel.1 网络设备、更新路由信息

各个node上的网络设备列表新增一个名为flannel.1的类型为vxlan的网络设备:

master node:

# ip -d link show
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether b6:bf:4c:81:cf:3b brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local {master node local ip} dev eth0 port 0 0 nolearning ageing 300

minion node:

349: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether d6:51:2e:80:5c:69 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local  {minion node local ip} dev eth0 port 0 0 nolearning ageing 300

从flannel.1的设备信息来看,它似乎与eth0存在着某种bind关系。这是在其他bridge、veth设备描述信息中所没有的。

flannel.1设备的ip:

master node:

flannel.1 Link encap:Ethernet  HWaddr b6:bf:4c:81:cf:3b
          inet addr:172.16.99.0  Bcast:0.0.0.0  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1450  Metric:1
          RX packets:5993274 errors:0 dropped:0 overruns:0 frame:0
          TX packets:5829044 errors:0 dropped:292 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1689890445 (1.6 GB)  TX bytes:1144725704 (1.1 GB)

minion node:

flannel.1 Link encap:Ethernet  HWaddr d6:51:2e:80:5c:69
          inet addr:172.16.57.0  Bcast:0.0.0.0  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1450  Metric:1
          RX packets:6294640 errors:0 dropped:0 overruns:0 frame:0
          TX packets:5755599 errors:0 dropped:25 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:989362527 (989.3 MB)  TX bytes:1861492847 (1.8 GB)

可以看到两个node上的flannel.1的ip与k8s cluster为两个node上分配subnet的ip范围是对应的。

下面是两个node上的当前路由表:

master node:

# ip route
... ...
172.16.0.0/16 dev flannel.1  proto kernel  scope link  src 172.16.99.0
172.16.99.0/24 dev docker0  proto kernel  scope link  src 172.16.99.1
... ...

minion node:

# ip route
... ...
172.16.0.0/16 dev flannel.1
172.16.57.0/24 dev docker0  proto kernel  scope link  src 172.16.57.1
... ...

以上信息将为后续数据包传输分析打下基础。

e) 平坦的flannel network

从以上kubernetes和flannel network安装之后获得的网络信息,我们能看出flannel network是一个flat network。在flannel:172.16.0.0/16这个大网下,每个kubernetes node从中分配一个子网片段(/24):

master node:
  --bip=172.16.99.1/24

minion node:
  --bip=172.16.57.1/24

root@node1:~# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls  /coreos.com/network/subnets
/coreos.com/network/subnets/172.16.99.0-24
/coreos.com/network/subnets/172.16.57.0-24

用一张图来诠释可能更为直观:

img{512x368}

这个是不是有些像x86-64的虚拟内存寻址空间啊(同样是平坦内存地址访问模型)!

在平坦的flannel network中,每个pod都会被分配唯一的ip地址,且每个k8s node的subnet各不重叠,没有交集。不过这样的subnet分配模型也有一定弊端,那就是可能存在ip浪费:一个node上有200多个flannel ip地址(xxx.xxx.xxx.xxx/24),如果仅仅启动了几个Pod,那么其余ip就处于闲置状态。

2、Flannel网络通信原理

这里我们模仿flannel官方的那幅原理图,画了一幅与我们的实验环境匹配的图,作为后续讨论flannel网络通信流程的基础:

img{512x368}

如上图所示,我们来看看从pod1:172.16.99.8发出的数据包是如何到达pod3:172.16.57.15的(比如:在pod1的某个container中ping -c 3 172.16.57.15)。

a) 从Pod出发

由于k8s更改了docker的DOCKER_OPTS,显式指定了–bip,这个值与分配给该node上的subnet的范围是一致的。这样一来,docker引擎每次创建一个Docker container,该container被分配到的ip都在flannel subnet范围内。

当我们在Pod1下的某个容器内执行ping -c 3 172.16.57.15,数据包便开始了它在flannel network中的旅程。

Pod是Kubernetes调度的基本unit。Pod内的多个container共享一个network namespace。kubernetes在创建Pod时,首先先创建pause容器,然后再以pause的network namespace为基础,创建pod内的其他容器(–net=container:xxx),这样Pod内的所有容器便共享一个network namespace,这些容器间的访问直接通过localhost即可。比如Pod下A容器启动了一个服务,监听8080端口,那么同一个Pod下面的另外一个B容器通过访问localhost:8080即可访问到A容器下面的那个服务。

在之前的《理解Docker容器网络之Linux Network Namespace》一文中,我相信我已经讲清楚了单机下Docker容器数据传输的路径。在这个环节中,数据包的传输路径也并无不同。

我们看一下Pod1中某Container内的路由信息:

# docker exec ba75f81455c7 ip route
default via 172.16.99.1 dev eth0
172.16.99.0/24 dev eth0  proto kernel  scope link  src 172.16.99.8

目的地址172.16.57.15并不在直连网络中,因此数据包通过default路由出去。default路由的路由器地址是172.16.99.1,也就是上面的docker0 bridge的IP地址。相当于docker0 bridge以“三层的工作模式”直接接收到来自容器的数据包(而并非从bridge的二层端口接收)。

b) docker0与flannel.1之间的包转发

数据包到达docker0后,docker0的内核栈处理程序发现这个数据包的目的地址是172.16.57.15,并不是真的要送给自己,于是开始为该数据包找下一hop。根据master node上的路由表:

master node:

# ip route
... ...
172.16.0.0/16 dev flannel.1  proto kernel  scope link  src 172.16.99.0
172.16.99.0/24 dev docker0  proto kernel  scope link  src 172.16.99.1
... ...

我们匹配到“172.16.0.0/16”这条路由!这是一条直连路由,数据包被直接送到flannel.1设备上。

c) flannel.1设备以及flanneld的功用

flannel.1是否会重复docker0的套路呢:包不是发给自己,转发数据包?会,也不会。

“会”是指flannel.1肯定要将包转发出去,因为毕竟包不是给自己的(包目的ip是172.16.57.15, vxlan设备ip是172.16.99.0)。
“不会”是指flannel.1不会走寻常套路去转发包,因为它是一个vxlan类型的设备,也称为vtep,virtual tunnel end point。

那么它到底是怎么处理数据包的呢?这里涉及一些Linux内核对vxlan处理的内容,详细内容可参见本文末尾的参考资料。

flannel.1收到数据包后,由于自己不是目的地,也要尝试将数据包重新发送出去。数据包沿着网络协议栈向下流动,在二层时需要封二层以太包,填写目的mac地址,这时一般应该发出arp:”who is 172.16.57.15″。但vxlan设备的特殊性就在于它并没有真正在二层发出这个arp包,因为下面的这个内核参数设置:

master node:

# cat /proc/sys/net/ipv4/neigh/flannel.1/app_solicit
3

而是由linux kernel引发一个”L3 MISS”事件并将arp请求发到用户空间的flanned程序。

flanned程序收到”L3 MISS”内核事件以及arp请求(who is 172.16.57.15)后,并不会向外网发送arp request,而是尝试从etcd查找该地址匹配的子网的vtep信息。在前面章节我们曾经展示过etcd中Flannel network的配置信息:

master node:

# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls  /coreos.com/network/subnets
/coreos.com/network/subnets/172.16.99.0-24
/coreos.com/network/subnets/172.16.57.0-24

# curl -L http://127.0.0.1:{etcd listen port}/v2/keys/coreos.com/network/subnets/172.16.57.0-24
{"action":"get","node":{"key":"/coreos.com/network/subnets/172.16.57.0-24","value":"{\"PublicIP\":\"{minion node local ip}\",\"BackendType\":\"vxlan\",\"BackendData\":{\"VtepMAC\":\"d6:51:2e:80:5c:69\"}}","expiration":"2017-01-17T09:46:20.607339725Z","ttl":21496,"modifiedIndex":2275460,"createdIndex":2275460}}

flanneld从etcd中找到了答案:

subnet: 172.16.57.0/24
public ip: {minion node local ip}
VtepMAC: d6:51:2e:80:5c:69

我们查看minion node上的信息,发现minion node上的flannel.1 设备mac就是d6:51:2e:80:5c:69:

minion node:

#ip -d link show

349: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether d6:51:2e:80:5c:69 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local 10.46.181.146 dev eth0 port 0 0 nolearning ageing 300

接下来,flanned将查询到的信息放入master node host的arp cache表中:

master node:

#ip n |grep 172.16.57.15
172.16.57.15 dev flannel.1 lladdr d6:51:2e:80:5c:69 REACHABLE

flanneld完成这项工作后,linux kernel就可以在arp table中找到 172.16.57.15对应的mac地址并封装二层以太包了。

到目前为止,已经呈现在大家眼前的封包如下图:

img{512x368}

不过这个封包还不能在物理网络上传输,因为它实际上只是vxlan tunnel上的packet。

d) kernel的vxlan封包

我们需要将上述的packet从master node传输到minion node,需要将上述packet再次封包。这个任务在backend为vxlan的flannel network中由linux kernel来完成。

flannel.1为vxlan设备,linux kernel可以自动识别,并将上面的packet进行vxlan封包处理。在这个封包过程中,kernel需要知道该数据包究竟发到哪个node上去。kernel需要查看node上的fdb(forwarding database)以获得上面对端vtep设备(已经从arp table中查到其mac地址:d6:51:2e:80:5c:69)所在的node地址。如果fdb中没有这个信息,那么kernel会向用户空间的flanned程序发起”L2 MISS”事件。flanneld收到该事件后,会查询etcd,获取该vtep设备对应的node的”Public IP“,并将信息注册到fdb中。

这样Kernel就可以顺利查询到该信息并封包了:

master node:

# bridge fdb show dev flannel.1|grep d6:51:2e:80:5c:69
d6:51:2e:80:5c:69 dst {minion node local ip} self permanent

由于目标ip是minion node,查找路由表,包应该从master node的eth0发出,这样src ip和src mac地址也就确定了。封好的包示意图如下:

img{512x368}

e) kernel的vxlan拆包

minion node上的eth0接收到上述vxlan包,kernel将识别出这是一个vxlan包,于是拆包后将flannel.1 packet转给minion node上的vtep(flannel.1)。minion node上的flannel.1再将这个数据包转到minion node上的docker0,继而由docker0传输到Pod3的某个容器里。

3、Pod内到外部网络

我们在Pod中除了可以与pod network中的其他pod通信外,还可以访问外部网络,比如:

master node:
# docker exec ba75f81455c7 ping -c 3 baidu.com
PING baidu.com (180.149.132.47): 56 data bytes
64 bytes from 180.149.132.47: icmp_seq=0 ttl=54 time=3.586 ms
64 bytes from 180.149.132.47: icmp_seq=1 ttl=54 time=3.752 ms
64 bytes from 180.149.132.47: icmp_seq=2 ttl=54 time=3.722 ms
--- baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 3.586/3.687/3.752/0.072 ms

这个通信与vxlan就没有什么关系了,主要是通过docker引擎在iptables的POSTROUTING chain中设置的MASQUERADE规则:

mastre node:

#iptables -t nat -nL
... ...
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.16.99.0/24       0.0.0.0/0
... ...

docker将容器的pod network地址伪装为node ip出去,包回来时再snat回容器的pod network地址,这样网络就通了。

四、”不真实”的Service网络

每当我们在k8s cluster中创建一个service,k8s cluster就会在–service-cluster-ip-range的范围内为service分配一个cluster-ip,比如本文开始时提到的:

# kubectl get services
NAME           CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
index-api      192.168.3.168   <none>        30080/TCP   18d
kubernetes     192.168.3.1     <none>        443/TCP     94d
my-nginx       192.168.3.179   <nodes>       80/TCP      90d
nginx-kit      192.168.3.196   <nodes>       80/TCP      12d
rbd-rest-api   192.168.3.22    <none>        8080/TCP    60d

这个cluster-ip只是一个虚拟的ip,并不真实绑定某个物理网络设备或虚拟网络设备,仅仅存在于iptables的规则中:

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */

# iptables -t nat -nL|grep 192.168.3
Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
KUBE-SVC-XGLOHA7QRQ3V22RZ  tcp  --  0.0.0.0/0            192.168.3.182        /* kube-system/kubernetes-dashboard: cluster IP */ tcp dpt:80
KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  0.0.0.0/0            192.168.3.1          /* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-SVC-AU252PRZZQGOERSG  tcp  --  0.0.0.0/0            192.168.3.22         /* default/rbd-rest-api: cluster IP */ tcp dpt:8080
KUBE-SVC-TCOU7JCQXEZGVUNU  udp  --  0.0.0.0/0            192.168.3.10         /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-SVC-BEPXDJBUHFCSYIC3  tcp  --  0.0.0.0/0            192.168.3.179        /* default/my-nginx: cluster IP */ tcp dpt:80
KUBE-SVC-UQG6736T32JE3S7H  tcp  --  0.0.0.0/0            192.168.3.196        /* default/nginx-kit: cluster IP */ tcp dpt:80
KUBE-SVC-ERIFXISQEP7F7OF4  tcp  --  0.0.0.0/0            192.168.3.10         /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
... ...

可以看到在PREROUTING环节,k8s设置了一个target: KUBE-SERVICES。而KUBE-SERVICES下面又设置了许多target,一旦destination和dstport匹配,就会沿着chain进行处理。

比如:当我们在pod网络curl 192.168.3.22 8080时,匹配到下面的KUBE-SVC-AU252PRZZQGOERSG target:

KUBE-SVC-AU252PRZZQGOERSG  tcp  --  0.0.0.0/0            192.168.3.22         /* default/rbd-rest-api: cluster IP */ tcp dpt:8080

沿着target,我们看到”KUBE-SVC-AU252PRZZQGOERSG”对应的内容如下:

Chain KUBE-SVC-AU252PRZZQGOERSG (1 references)
target     prot opt source               destination
KUBE-SEP-I6L4LR53UYF7FORX  all  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */ statistic mode random probability 0.50000000000
KUBE-SEP-LBWOKUH4CUTN7XKH  all  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */

Chain KUBE-SEP-I6L4LR53UYF7FORX (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all  --  172.16.99.6          0.0.0.0/0            /* default/rbd-rest-api: */
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */ tcp to:172.16.99.6:8080

Chain KUBE-SEP-LBWOKUH4CUTN7XKH (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all  --  172.16.99.7          0.0.0.0/0            /* default/rbd-rest-api: */
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */ tcp to:172.16.99.7:8080

Chain KUBE-MARK-MASQ (17 references)
target     prot opt source               destination
MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK or 0x4000

请求被按5:5开的比例分发(起到负载均衡的作用)到KUBE-SEP-I6L4LR53UYF7FORX 和KUBE-SEP-LBWOKUH4CUTN7XKH,而这两个chain的处理方式都是一样的,那就是先做mark,然后做dnat,将service ip改为pod network中的Pod IP,进而请求被实际传输到某个service下面的pod中处理了。

五、参考资料




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


文章

评论

  • 正在加载...

分类

标签

归档











更多