Go的栈空间管理

栈空间管理的基本逻辑

go语言通过goroutine提供了并发编程支持,goroutine是go运行库的功能,而不是操作系统线程实现的,goroutine可以被理解成一个用户态的线程。

既然goroutine是由go运行库管理的,那么go运行库也需要为每个goroutine创建并管理相应的栈空间,为每个goroutine分配的栈空间不能太大,goroutine开多时会浪费大量空间,也不能太小,会导致栈溢出。go语言选择栈的栈空间管理的方式是,一开始给一个比较小的空间,随着需要自动增长。当goroutine不需要那么大的空间时,栈空间也要自动缩小。

分段栈 Segment Stacks

在go 1.3之前,go使用分段栈。

分段栈实现了一种不连续但是可以持续增长的栈,开始时,栈只有一个段,当需要更多的栈空间时,会分配一个新的段,和上一个栈双向链接。这样,一个栈就是由多个双向链接的段所组成的。当新分配的段使用完毕后,新段会被释放掉。

分段栈实现了栈的按需收缩,在增加新分段时也不需要对原有分段中的数据进行拷贝,使得goroutine的使用代价非常低廉。

分段栈的好处是可以按需增长,空间利用率比较高,然而分段栈在某些情况下也存在一定的瑕疵。当一个段即将用尽,这时使用for循环执行一个比较耗空间的函数,会导致函数执行时goroutine进行段的分配,而执行完成返回时,进行段的销毁,这样就会导致在循环中出现多次栈的扩容和收缩,造成很大的性能损失,这种情况被称作栈分裂(Stack Split)。

连续栈 Contiguous Stacks

go 1.3推出了连续栈,连续栈使用了另外一种策略,不再把栈分成一段一段的,当栈空间不够时,直接new一个2倍大的栈空间,并将原先栈空间中的数据拷贝到新的栈空间中,而后销毁旧栈。这样当出现栈空间触及边界时,不会产生栈分裂的情况。

继续假设当前栈空间即将用尽,并且需要在for循环中执行一个比较消耗空间的函数。当该函数执行时,栈空间发生了扩容,变成原先2倍大小,函数执行完成一次后,栈空间的使用量缩小回执行前的大小,但是栈空间的使用量并没有小于栈大小的1/4,不会触发栈收缩,所以在整个for循环执行过程中,不会反复触发栈空间的收缩扩容。

总结

相比于分段栈,连续栈避免了某些场景下栈空间的的频繁伸缩。有一点需要注意的是,连续栈的收缩也是需要重新申请一段空间(原先的1/2大小),并进行栈拷贝操作的。

点击关注知乎专栏Golang私房菜

翻译 | 更快的Python(一)

更快的Python(Python Faster Way)使用代码示例来说明如何书写Python代码能带来更高的性能。本文对代码进行了讲解,从性能和可读性等角度来选择出最适合的写法。

例子1:字符串格式化

  • 最差/最优时间比:1.95
  • 使用建议:Python 3.7或以上推荐使用f-string,其他版本推荐使用format方法。
  • 说明:字符串格式化是代码中最常遇到的情况,虽然在连接少量字符串的情景中,使用+号的性能最优,但是使用+号的代码可读性最差。如果使用Python 3.7或优以上版本,可以使用f-string来解决这个问题,f-string的性能比format方法和%操作符的性能都要高,可读性也比+号好。

例子2:字典的初始化

  • 最差/最优时间比:1.83
  • 使用建议:使用字面量初始化字典(以及其他集合类型)。
  • 说明:Python中初始化集合类型时使用字面量的方式,解释器会直接调用BUILD_MAP等字节码来创建,如果用构造函数的方式来创建,则需要先查询构造方法,再执行构造方法。使用字面量初始化,Python代码也更简洁。

例子3:内置排序方法

  • 最差/最优时间比:1.26
  • 使用建议:根据是否需要修改原始值来决定使用哪个方法。
  • 说明:sorted和list.sort方法是Python中内置的排序方法,sorted方法不会修改原始值,list.sort方法在原始值上直接排序,会修改原始值。比较这两个方法的性能差异,意义不大。

例子4:初始化多个变量

  • 最差/最优时间比:1.01
  • 使用建议:推荐使用第二种。
  • 说明:从字节码中可以看出两种方式出了执行顺序之外,基本一致,所以性能上也非常接近。

例子5:多个变量的比较

  • 最差/最优时间比:1.11
  • 使用建议:推荐使用第二种。
  • 说明:使用第一种方法能带来一定的性能提升,但是提升有限,在实际情况中也很少出现多个变量连续比较大小的情况,并且第一种方法非常不Pythonic,所以推荐使用第二种。

例子6:if true的条件判断

  • 最差/最优时间比:1.17
  • 使用建议:推荐使用第一种。
  • 说明:从字节码上看,第一种方法的性能最高,并且语法上也更加简洁。

例子7:if false的条件判断

  • 最差/最优时间比:1.10
  • 使用建议:推荐使用第一种。
  • 说明:从字节码上看,第一种方法的性能最高,语法角度上,if not写成第二种和第三种都是不推荐的。

例子8:判断list是否为空

  • 最差/最优时间比:1.55
  • 使用建议:根据具体需求,优先使用前两种。
  • 说明:前两种代码性能更高,代码更简洁。同时,空列表a并不等于None,所以使用if a is None无法实现对空列表的判断。

例子9:判断object是否为空

  • 最差/最优时间比:1.00
  • 使用建议:根据具体需求,优先使用前两种。
  • 说明:理由同上一个例子。

例子10:遍历可迭代对象

  • 最差/最优时间比:1.12
  • 使用建议:根据具体情况选择。
  • 说明:两者性能差别不大,使用enumerate方法,可以不需要取对象的长度,可以直接获取到对象的index。

参考文章

关注公众号【Python私房菜】

Go语言中defer的一些坑

defer语句是Go中一个非常有用的特性,可以将一个方法延迟到包裹该方法的方法返回时执行,在实际应用中,defer语句可以充当其他语言中try…catch…的角色,也可以用来处理关闭文件句柄等收尾操作。

defer触发时机

A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

Go官方文档中对defer的执行时机做了阐述,分别是。

  • 包裹defer的函数返回时
  • 包裹defer的函数执行到末尾时
  • 所在的goroutine发生panic时

defer执行顺序

当一个方法中有多个defer时, defer会将要延迟执行的方法“压栈”,当defer被触发时,将所有“压栈”的方法“出栈”并执行。所以defer的执行顺序是LIFO的。

所以下面这段代码的输出不是1 2 3,而是3 2 1。

1
2
3
4
5
6
7
8
9
10
11
func stackingDefers() {
defer func() {
fmt.Println("1")
}()
defer func() {
fmt.Println("2")
}()
defer func() {
fmt.Println("3")
}()
}

坑1:defer在匿名返回值和命名返回值函数中的不同表现

先看下面两个方法执行的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func returnValues() int {
var result int
defer func() {
result++
fmt.Println("defer")
}()
return result
}

func namedReturnValues() (result int) {
defer func() {
result++
fmt.Println("defer")
}()
return result
}

上面的方法会输出0,下面的方法输出1。上面的方法使用了匿名返回值,下面的使用了命名返回值,除此之外其他的逻辑均相同,为什么输出的结果会有区别呢?

要搞清这个问题首先需要了解defer的执行逻辑,文档中说defer语句在方法返回“时”触发,也就是说return和defer是“同时”执行的。以匿名返回值方法举例,过程如下。

  • 将result赋值给返回值(可以理解成Go自动创建了一个返回值retValue,相当于执行retValue = result)
  • 然后检查是否有defer,如果有则执行
  • 返回刚才创建的返回值(retValue)

在这种情况下,defer中的修改是对result执行的,而不是retValue,所以defer返回的依然是retValue。在命名返回值方法中,由于返回值在方法定义时已经被定义,所以没有创建retValue的过程,result就是retValue,defer对于result的修改也会被直接返回。

坑2:在for循环中使用defer可能导致的性能问题

看下面的代码

1
2
3
4
5
6
func deferInLoops() {
for i := 0; i < 100; i++ {
f, _ := os.Open("/etc/hosts")
defer f.Close()
}
}

defer在紧邻创建资源的语句后生命力,看上去逻辑没有什么问题。但是和直接调用相比,defer的执行存在着额外的开销,例如defer会对其后需要的参数进行内存拷贝,还需要对defer结构进行压栈出栈操作。所以在循环中定义defer可能导致大量的资源开销,在本例中,可以将f.Close()语句前的defer去掉,来减少大量defer导致的额外资源消耗。

坑3:判断执行没有err之后,再defer释放资源

一些获取资源的操作可能会返回err参数,我们可以选择忽略返回的err参数,但是如果要使用defer进行延迟释放的的话,需要在使用defer之前先判断是否存在err,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。

正确写法如下。

1
2
3
4
5
6
7
resp, err := http.Get(url)
// 先判断操作是否成功
if err != nil {
return err
}
// 如果操作成功,再进行Close操作
defer resp.Body.Close()

坑4:调用os.Exit时defer不会被执行

当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer并不会被执行。

1
2
3
4
5
6
func deferExit() {
defer func() {
fmt.Println("defer")
}()
os.Exit(0)
}

上面的defer并不会输出。

点击关注知乎专栏Golang私房菜

用Python写算法 | 蓄水池算法实现随机抽样

现在有一组数,不知道这组数的总量有多少,请描述一种算法能够在这组数据中随机抽取k个数,使得每个数被取出来的概率相等。

如果这组数有n个,那么每个数字取到的概率就是k/n,但是这个问题的难点在于不知道这组数的总数,也就是不知道n,那么该怎么计算每个数取到的概率呢?

蓄水池算法

游泳池(蓄水池)大家都不陌生,有些游泳池中的水是活的,有入水管也有出水管,那么和泳池体积相当的水流过之后,是不是泳池中所有的水都会被替换呢?当然不是,有的水在泳池中可能会存留很久,有的可能刚进去就流走了。仿照这种现象,蓄水池抽样算法诞生了,蓄水池算法的关键在于保证流入蓄水池的水和已经在池中的水以相同的概率留存在蓄水池中。并且蓄水池算法可以在不预先知道总量的情况下,在时间复杂度O(N)的情况下,来解决这类采样问题。

核心原理

这一部分涉及公式,为了保证效果直接贴了图过来。

Python实现

接下来尝试用Python实现一下蓄水池算法,由于蓄水池算法是在事先不知道总量的情况下抽样的,所以定义一个方法来接收单个元素,并且把这个方法放在类中,以持有采样后的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import random


class ReservoirSample(object):

def __init__(self, size):
self._size = size
self._counter = 0
self._sample = []

def feed(self, item):
self._counter += 1
# 第i个元素(i <= k),直接进入池中
if len(self._sample) < self._size:
self._sample.append(item)
return self._sample
# 第i个元素(i > k),以k / i的概率进入池中
rand_int = random.randint(1, self._counter)
if rand_int <= self._size:
self._sample[rand_int - 1] = item
return self._sample

测试代码

接下来实现一个测试用例验证实现的算法是否正确,既然是随机抽样,无法通过单词测试来验证是否正确,所以通过多次执行的方式来验证,比如从1-10里随机取样3个数,然后执行10000次取样,如果算法正确,最后结果中1-10被取样的次数应该是相同的,都是3000上下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import unittest
from collections import Counter

from reservoir_sample import ReservoirSample


class TestMain(unittest.TestCase):

def test_reservoir_sample(self):
samples = []
for i in range(10000):
sample = []
rs = ReservoirSample(3)
for item in range(1, 11):
sample = rs.feed(item)
samples.extend(sample)
r = Counter(samples)
print(r)

if __name__ == '__main__':
unittest.main()

输出的结果如下

1
Counter({7: 3084, 6: 3042, 10: 3033, 3: 3020, 8: 3016, 5: 2997, 4: 2986, 2: 2972, 9: 2932, 1: 2918})

上面输出了每个数字被取样到的次数,通过图表可以清晰的看到分布情况

可以看出蓄水池算法对于随机抽样还是非常适合的,每个元素的抽样概率都相同。

代码

上述的算法和测试代码已经放在Github,可以直接下载使用。

关注公众号【Python私房菜】

使用gofmt格式化代码

对于一门编程语言来说,代码格式化是最容易引起争议的一个问题,不同的开发者可能会有不同的编码风格和习惯,但是如果所有开发者都能使用同一种格式来编写代码,开发者就可以将宝贵的时间专注在语言要解决的问题上。

gofmt介绍

Golang的开发团队制定了统一的官方代码风格,并且推出了gofmt工具(gofmt或go fmt)来帮助开发者格式化他们的代码到统一的风格。gofmt是一个cli程序,会优先读取标准输入,如果传入了文件路径的话,会格式化这个文件,如果传入一个目录,会格式化目录中所有.go文件,如果不传参数,会格式化当前目录下的所有.go文件。

gofmt默认不对代码进行简化,使用-s参数可以开启简化代码功能,具体来说会进行如下的转换:

  • 去除数组、切片、Map初始化时不必要的类型声明:
1
2
3
4
如下形式的切片表达式:
    []T{T{}, T{}}
将被简化为:
    []T{{}, {}}
  • 去除数组切片操作时不必要的索引指定
1
2
3
4
如下形式的切片表达式:
    s[a:len(s)]
将被简化为:
    s[a:]
  • 去除迭代时非必要的变量赋值
1
2
3
4
5
6
7
8
如下形式的迭代:
    for x, _ = range v {...}
将被简化为:
    for x = range v {...}
如下形式的迭代:
    for _ = range v {...}
将被简化为:
    for range v {...}

gofmt命令参数列表如下:

1
2
3
4
5
6
7
8
9
10
usage: gofmt [flags] [path ...]
  -cpuprofile string
        write cpu profile to this file
  -d    display diffs instead of rewriting files
  -e    report all errors (not just the first 10 on different lines)
  -l    list files whose formatting differs from gofmt's
  -r string
        rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')
  -s    simplify code
  -w    write result to (source) file instead of stdout

可以看到,gofmt命令还支持自定义的重写规则,使用-r参数,按照pattern -> replacement的格式传入规则。

有如下内容的Golang程序,存储在main.go文件中。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
   a := 1
   b := 2
   c := a + b
   fmt.Println(c)
}

用以下规则来格式化上面的代码。

1
gofmt -r "a + b -> b + a"

格式化的结果如下。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
   a := 1
   b := 2
   c := b + a
   fmt.Println(c)
}

*注意:Gofmt使用tab来表示缩进,并且对行宽度无限制,如果手动对代码进行了换行,gofmt也不会强制把代码格式化回一行。

go fmt和gofmt

gofmt是一个独立的cli程序,而go中还有一个go fmt命令,go fmt命令是gofmt的简单封装。

1
2
3
4
5
6
7
8
9
10
11
usage: go fmt [-n] [-x] [packages]

Fmt runs the command 'gofmt -l -w' on the packages named
by the import paths. It prints the names of the files that are modified.
For more about gofmt, see 'go doc cmd/gofmt'.
For more about specifying packages, see 'go help packages'.
The -n flag prints commands that would be executed.
The -x flag prints commands as they are executed.
To run gofmt with specific options, run gofmt itself.

See also: go fix, go vet.

go fmt命令本身只有两个可选参数-n和-x,-n仅打印出内部要执行的go fmt的命令,-x命令既打印出go fmt命令又执行它,如果需要更细化的配置,需要直接执行gofmt命令。

go fmt在调用gofmt时添加了-l -w参数,相当于执行了gofmt -l -w

goland中配置gofmt

Goland是JetBrains公司推出的Go语言IDE,是一款功能强大,使用便捷的产品。

在Goland中,可以通过添加一个File Watcher来在文件发生变化的时候调用gofmt进行代码格式化,具体方法是,点击Preferences -> Tools -> File Watchers,点加号添加一个go fmt模版,Goland中预置的go fmt模版使用的是go fmt命令,将其替换为gofmt,然后在参数中增加-l -w -s参数,启用代码简化功能。添加配置后,保存源码时,goland就会执行代码格式化了。

参考文章

https://golang.org/cmd/gofmt/

https://golang.org/doc/effective_go.html

https://openhome.cc/Gossip/Go/gofmt.html

https://github.com/hyper0x/go_command_tutorial/blob/master/0.9.md

点击关注知乎专栏Golang私房菜

搭建Kubernetes集群时DNS无法解析问题的处理过程

问题描述

在搭建Kubernetes集群过程中,安装了kube-dns插件后,运行一个ubuntu容器,发现容器内无法解析集群外域名,一开始可以解析集群内域名,一段时间后也无法解析集群内域名。

1
2
3
4
5
$ nslookup kubernetes.default
Server: 10.99.0.2
Address 1: 10.99.0.2 kube-dns.kube-system.svc.cluster.local

nslookup: can't resolve 'kubernetes.default'

排查过程

在排查问题前,先思考一下Kubernetes集群中的DNS解析过程,在安装好kube-dns的集群中,普通Pod的dnsPolicy属性是默认值ClusterFirst,也就是会指向集群内部的DNS服务器,kube-dns负责解析集群内部的域名,kube-dns Pod的dnsPolicy值是Default,意思是从所在Node继承DNS服务器,对于无法解析的外部域名,kube-dns会继续向集群外部的dns进行查询,过程如图。

Ubuntu容器是一个普通的Pod,在Linux系统中,/etc/resolv.conf是存储DNS服务器的文件,普通Pod的/etc/resolv.conf文件应该存储的是kube-dns的Service IP。

1
2
3
nameserver 10.99.0.2  # 这里存储的是kube-dns的Service IP
search default.svc.cluster.local. svc.cluster.local. cluster.local.
options ndots:5

查看后发现/etc/resolv.conf文件中存储的是kube-dns的Service IP,证明这一步没有问题,接下来查看一下kube-dns的Pod,先进入kube-dns的Pod中检查一下/etc/resolv.conf文件,这里存储的应该是集群外部的DNS服务器地址,查看后发现,这里存储的地址是127.0.0.53,进一步查看kube-dns Pod的log,发现出现了非常多的i/o timeout错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:38019->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:57567->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:52599->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:42539->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:46885->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:44189->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:56505->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:47320->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:42464->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:49203->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:58103->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:47148->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:36883->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:40968->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:55672->127.0.0.53:53: i/o timeout

现在基本上可以发现问题的原因了,kube-dns只能解析集群内部地址,而集群外部地址应该发给外部DNS服务器进行解析,由于kube-dns Pod中的/etc/resolv.conf文件存储的DNS服务器地址是127.0.0.53,127...*都是回环地址,也就是集群外域名的DNS解析请求会再次发送回kube-dns,导致形成一个循环,这也是一秒钟会出现几十次i/o timeout日志的原因,请求会不断的在kube-dns中循环,kube-dns就像一个黑洞一样,吃掉了所有dns解析请求,不断累积的请求最终会导致整个集群的网络出现卡顿。

为什么

虽然问题的原因找到了,但是为什么kube-dns Pod中/etc/resolv.conf文件存储的DNS服务器是127.0.0.53?

kube-dns Pod的dnsPolicy值是Default,查看一下Kubernetes文档。

Default“: The Pod inherits the name resolution configuration from the node that the pods run on. See related discussion for more details.

所以kube-dns的/etc/resolv.conf文件是从Node中继承来的,查看Node中的/etc/resolv.conf文件,存储的DNS服务器地址确实是127.0.0.53,那么下一个问题出现了,在Node中发送DNS解析请求为什么不会产生回环的问题呢?

Node使用的是Ubuntu 18.04 Server,在这个版本的系统中,DNS解析请求并不是直接发给所在网络的DNS服务器的,Ubuntu 18.04中有一个systemd-resolved服务,为本地应用程序提供了DNS解析服务,例如nslookup localhost,解析程序从/etc/resolv.conf文件中找到DNS服务器127.0.0.53,发送解析请求,systemd-resolved会监听在53端口上,捕获到解析请求后,如果是自己可以解析的,例如localhost,会直接返回127.0.0.1,如果不能解析,才会发送给外部服务器,而外部服务器的地址存储在/run/systemd/resolve/resolv.conf文件中,这个文件是systemd-resolved服务器的配置文件,过程如图。

怎么破

理解了问题的来龙去脉,解决问题的办法也就应运而生。在Kubernetes集群中,kubelet是worker组建,负责管理Pod,根据kubernetes文档,kubelet默认会从Node的/etc/resolv.conf文件读取DNS服务器地址,使得dnsPolicy是Default的Pod得以继承,kubelet中的–resolv-conf参数可以指定这个配置文件的地址。在Ubuntu 18.04中,将这个参数设置为systemd-resolved的DNS服务器配置文件/run/systemd/resolve/resolv.conf,Pod就会继承真正的外部DNS服务器。

总结

通过对问题的探究,也理解了Kubernetes集群中DNS解析的完整过程,如图。

\ 在Ubuntu 16.04中也是类似的逻辑,只不过systemd-resolved换成了dnsmasq,监听地址是127.0.1.1 * 在具体实践过程中,也顺便探究了CoreDNS和KubeDNS架构和解析逻辑上的区别,不过不在此问题的讨论范围,有兴趣的朋友可以自己看一下。 * 如果Kubernetes集群是安装在NAT网络下的虚拟机上,虚拟机(也就是Kubernetes集群中的Node)中/etc/resolv.conf文件可能被修改为NAT的地址,也就不会出现上面这个问题。*

参考内容

https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/
https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
https://kubernetes.io/docs/tasks/administer-cluster/dns-custom-nameservers/
https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html
https://github.com/kubernetes/kubernetes/issues/49411
https://github.com/kubernetes/kubernetes/issues/45828

Golang环境安装和依赖管理

2015年,Go 1.5加入了一个试验性的vendor机制(到2016年的Go 1.6版变为默认开启),vendor机制就是在项目中加入了vendor文件夹,用于存放依赖,这样就可以将不同项目的依赖隔离开。

Golang一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Golang提供了方便的安装包,支持Windows、Linux、Mac系统。

下载安装包

Golang的官网是https://golang.org/,如果官网打不开,可以访问https://golang.google.cn/这个域名。在官网点击Download Go会进入下载页,可以看到这里提供了针对各个系统的安装包,也提供了源码,可以下载源码编译安装。

下载运行安装包后,在terminal中执行go env命令,如果出现下面的输出说明已经安装成功。

GOROOT与GOPATH

仔细看上面的输出,会发现其中有一个GOPATH,又有一个GOROOT,那么到底哪个才是Golang的运行环境呢。

首先访问一下GOROOT这个路径,会发现其中包含bin、lib等文件夹。GOROOT就是Golang的安装路径,其中包含Golang编译、工具、标准库等,在安装后就会存在。

和GOROOT不同,GOPATH是工作空间路径,从go 1.8开始,如果GOPATH没有被设置,会有一个默认值,在Unix上为$HOME/go,在Windows上为%USERPROFILE%/go,当调用go build时,它会在GOPATH中寻找源码。访问一下GOPATH这个路径,会发现其中只有pkg、bin、src三个文件夹,并且里面基本是空的,这是一个约定的目录结构,src文件夹用来存放源码、pkg存放编译后生成的文件,bin存放编译后生成的可执行文件。项目代码需要在GOPATH/src路径下。

GOPATH路径下出了存放项目代码,还存放所有通过go get安装的依赖,项目代码和依赖代码是平级的,当各个项目都有很多依赖的时候,这个GOPATH路径下的代码量会多的吓人,并且难以拆分。

Vendor

当使用go run或者go build命令时,会首先从当前路径下的vendor文件夹中查找依赖,如果vendor不存在,才会从GOPATH中查找依赖。

然而我们安装依赖通常使用go get或者go install命令,这两个命令依旧会把依赖安装到GOPATH路径下。

包管理工具dep

Vendor只是go官方提供的一个机制,但是包管理的问题依然没有解决,并且也没有对依赖进行版本管理。如果要实现上述的功能,还需要借助包管理工具。

Go官方给出了包管理工具的对比:https://github.com/golang/go/wiki/PackageManagementTools

dep是官方的试验性包管理工具,可以通过如下脚本安装

curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh

安装完成后,进入项目路径,执行

1
dep init

项目中会出现两个文件一个目录

1
2
3
Gopkg.toml
Gopkg.lock
vendor

dep包管理的流程如图

  • solving功能,它将当前项目中的导入包和Gopkg.toml中的规则作为输入,不可变的依赖关系图作为传递完成后的输出,形成Gopkg.lock。

  • vendor功能,将Gopkg.lock中的信息作为输入,确保项目编译时能使用在Gopkg.lock文件中锁定的版本。

使用如下命令添加依赖

1
dep ensure -add [github.com/gin-gonic/gin](http://github.com/gin-gonic/gin)

使用如下命令更新Gopkg.lock

1
dep ensure -update

欢迎关注知乎专栏【Golang私房菜】

你所不知道的Python | 函数参数的演进之路

函数参数处理机制是Python中一个非常重要的知识点,随着Python的演进,参数处理机制的灵活性和丰富性也在不断增加,使得我们不仅可以写出简化的代码,也能处理复杂的调用。

关键字参数

调用时指定参数的名称,且与函数声明时的参数名称一致。

关键字参数是Python函数中最基础也最常见的,我们写一个记账的函数,参数是需要记录的时间和金额。

1
2
def add_record(date, amount):
print('date:', date, 'amount:', amount)

这里的amount参数就是一个关键字参数,关键字参数支持两种调用方式:

  • 位置调用
  • 关键字调用

位置调用,就是按参数的位置进行调用,例如传入两个参数,第一个是字符串2018-07-06,第二个是整数10,那么这两个参数会被分别赋予date和amount变量,如果顺序反过来,则这两个参数分别赋予amount和date变量。

1
2
add_record('2018-07-06', 10)  # 输出date: 2018-07-06 amount: 10
add_record(10, '2018-07-06') # 输出date: 10 amount: 2018-07-06

关键字调用,可以忽略参数顺序,直接指定参数。

1
add_record(amount=10, date='2018-07-06')  # 虽然参数顺序反了,但是使用了关键字调用,所以依然输出date: 2018-07-06 amount: 10

仅限关键字参数

我们定义一个Person类,并实现它的__init__方法

1
2
3
4
5
6
7
class Person(object):
def __init__(self, name, age, gender, height, weight):
self._name = name
self._age = age
self._gender = gender
self._height = height
self._weight = weight

当初始化这个类的时候,我们可以使用关键字调用,也可以使用位置调用。

1
2
Person('Wendy', 24, 'female', 160, 48)
Person('John', age=27, gender='male', height=170, weight=52)

对比上面两种方式,我们会发现参数多的时候通过关键字指定参数不仅更加清晰,也更具有可读性。如果我们希望函数只允许关键字调用,该如何做呢?Python 3.0中,引入了一种新的仅限关键字参数,能实现我们的需求。

下面将age以后的参数修改为只允许关键字调用,定义函数时想指定仅限关键字参数,要把它们放到前面有星号的参数后面,在Python中有星号的参数是可变参数的意思,如果不想支持可变参数,可以在参数中放一个星号作为分割。

1
2
3
4
5
6
7
8
9
10
11
class Person(object):
# 参数中的星号作为关键字参数和仅限关键字参数的分割
def __init__(self, name, *, age='22', gender='female', height=160, weight=50):
self._name = name
self._age = age
self._gender = gender
self._height = height
self._weight = weight

Person('Wendy', 24, 'female', 160, 48) # 报错,age以后参数不允许位置调用
Person('John', age=27, gender='male', height=170, weight=52) # 正常执行

普通参数和仅限关键字参数中间由一个星号隔离开,星号以后的都是仅限关键字参数,只可以通过关键字指定,而不能通过位置指定。

参数默认值

在函数声明时,指定参数默认值,调用时不传入参数则使用默认值,相当于可选参数。

1
2
3
4
def add_record(date, amount=0):
print('date:', date, 'amount:', amount)

add_record('2018-07-06') # 输出date: 2018-07-06 amount: 0

上面代码中没有传入amount参数,所以amount直接被置为默认值0。有一点需要注意的是,默认参数需要设置在必选参数后面,并且默认参数既可以通过位置调用,也可以通过关键字调用。

1
2
3
add_record('2018-07-06', 10)  # 通过位置指定参数
add_record('2018-07-06', amount=10) # 通过位置指定参数
add_record(amount=10, '2018-07-06') # 报错,默认参数必须在必选参数后面

参数默认值既支持关键字参数,也支持仅限关键字参数。

可变长参数

“可变长”顾名思义是允许在调用时传入多个参数,可变长参数适用于参数数量不确定的场景,可变参数有两种,一种是关键字可变长参数,另一种是非关键字可变长参数。

非关键字可变长参数的写法是在参数名前加一个星号,Python会将这些多出来的参数的值放入一个元组中,由于元组中只有参数值而没有参数名称,所以是关键字参数。

1
2
3
4
5
6
7
8
def print_args(*args):
print(args)

print_args(1, 2, 3, 4, 5) # 输出元组(1, 2, 3, 4, 5)

a = [1, 2, 3, 4, 5]
print_args(a) # 直接传入时,列表a会被当作一个元素,所以输出([1, 2, 3, 4, 5],)
print_args(*a) # 在传参时加星号可以将可迭代参数解包,所以列表a中每一个元素都被当作一个参数传入,输出(1, 2, 3, 4, 5)

关键字可变长参数的写法是在参数名前加两个星号,Python会将这些多出来的参数的值放入一个字典中,由于字典中只有参数值而没有参数名称,所以是非关键字参数。

1
2
3
4
5
def print_kwargs(**kw_args):
print(kw_args)

a = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
print_kwargs(**a) # 使用关键字可变参数时, {'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4}

函数注解

Python 3中为函数定义增加的另一个新功能是函数注解,所谓函数注解,就是可以在函数参数和返回值上添加任意的元数据。

1
2
def create_person(name: str, age: int, gender: str = 'female', height: int = 160)  -> bool:
return True

用create_person方法举例,可以看到在每个参数后面都跟了一个参数类型,在函数后面则是返回值类型,函数注解可以用在文档编写、类型检查中,在支持函数注解的IDE中,如果传入参数和返回的类型不符合函数注解中的类型,IDE会提示错误。

但是函数注解只是一个元数据,Python解释器执行时候并不会去检查类型,所以下面这种情况也是合法的。

1
Person(name=123, age='John')  # 并不会报错

总结

Python有着非常好入门的特点,但是随着语言本身的演进,很多高级功能也在持续加入,用好这些功能可以使我们的Python代码拥有更高的可读性,适应更加复杂的应用场景。

扫码关注【Python私房菜】,我会在这里持续发布Python相关原创技术文章

你所不知道的Python | 字符串连接的秘密

字符串连接,就是将2个或以上的字符串合并成一个,看上去连接字符串是一个非常基础的小问题,但是在Python中,我们可以用多种方式实现字符串的连接,稍有不慎就有可能因为选择不当而给程序带来性能损失。

方法1:加号连接

很多语言都支持使用加号连接字符串,Python也不例外,只需要简单的将2个或多个字符串相加就可以完成拼接。

1
2
3
a = 'Python'
b = '私房菜'
r = a + b # 输出'Python私房菜'

方法2:使用%操作符

在Python 2.6以前,%操作符是唯一一种格式化字符串的方法,它也可以用于连接字符串。

1
2
3
a = 'Python'
b = '私房菜'
r = '%s%s' % (a, b) # 输出'Python私房菜'

方法3:使用format方法

format方法是Python 2.6中出现的一种代替%操作符的字符串格式化方法,同样可以用来连接字符串。

1
2
3
a = 'Python'
b = '私房菜'
r = '{}{}'.format(a, b)

方法4:使用f-string

Python 3.6中引入了Formatted String Literals(字面量格式化字符串),简称f-string,f-string是%操作符和format方法的进化版,使用f-string连接字符串的方法和使用%操作符、format方法类似。

1
2
3
a = 'Python'
b = '私房菜'
r = f'{a}{b}'

方法5:使用str.join()方法

字符串有一个内置方法join,其参数是一个序列类型,例如数组或者元组等。

1
2
3
a = 'Python'
b = '私房菜'
r = ''.join([a, b])

对比测试

既然连接字符串有这么多方法,那么使用时到底选择哪种呢?我们从代码可读性和性能两个层面来评估一下上面5种方法。

使用timeit模块,分别执行上述5种方法的示例代码100000次,执行时间如图。

可以看到,%操作符、format和f-string都是字符串格式化方法,性能依次递增,加号连接的性能和f-string不相上下。

有一点需要注意的是,字符串类型是不可变的,所以每一次应用加号连接字符串都会生成一个新的字符串,连接多个字符串时,效率低下就是必然的了,我们将一次连接的字符串提升到10个和20个,再来进行2轮测试,下面是连接20个字符串时的耗时情况。

和连接两个字符串时的结果出现了一些不同,首先使用加号连接的方式在字符串数量较多时(大于10个),性能会急剧下降,str.join()方法在连接大量字符串时性能最好。

总结

连接少量字符串时
使用加号连接符在性能和可读性上都是明智的,如果对可读性有更高的要求,并且使用的Python 3.6以上版本,f-string也是一个非常好的选择,例如下面这种情况,f-string的可读性显然比加号连接好得多。

1
2
a = f'姓名:{name} 年龄:{age} 性别:{gender}'
b = '姓名:' + name + '年龄:' + age + '性别:' + gender

连接大量字符串时
joinf-string都是性能最好的选择,选择时依然取决于你使用的Python版本以及对可读性的要求,f-string在连接大量字符串时可读性并不一定好。切记不要使用加号连接,尤其是在for循环中。

欢迎关注我的公众号【Python私房菜】

Ubuntu 18.04 LTS安装KVM虚拟机

前一阵使用在最新的Ubuntu 18.04上安装了KVM来虚拟一个小的VM集群,将主要过程和其中遇到的一些问题记录下来。

准备工作

首先需要检查一下CPU是否支持虚拟化,执行一下命令来检查/proc/cpuinfo文件中是否又虚拟化相关的字眼,如果有的话表明CPU支持虚拟化技术。

1
egrep -c '(svm|vmx)' /proc/cpuinfo

上面命令执行结果如果返回0,表示CPU不支持虚拟化技术。当然主板BIOS中的虚拟化技术也可能不是默认开启的,如果没有开启需要手动开启一下。

安装KVM

执行以下命令安装KVM

1
2
sudo apt update
sudo apt install qemu qemu-kvm libvirt-bin bridge-utils virt-manager

将libvirtd添加自启动

1
2
sudo systemctl start libvirtd.service
sudo systemctl enable libvirtd.service

网络模式

KVM安装完成后,首先需要进行网络设定,KVM支持四种网络模式:

  • 桥接模式
  • NAT模式
  • 用户网络模式
  • 直接分配设备模式

主要讲一下前两种

桥接(Bridge)模式

在桥接模式下,宿主机和虚拟机共享同一个物理网络设备,虚拟机中的网卡和物理机中的网卡是平行关系,所以虚拟机可以直接接入外部网络,虚拟机和宿主机有平级的IP。

桥接模式

原本宿主机是通过网卡eth0连接外部网络的,网桥模式会新创建一个网桥br0,接管eth0来连接外部网络,然后将宿主机和虚拟机的网卡eth0都绑定到网桥上。

使用桥接模式需要进行以下操作:

编辑/etc/network/interfaces,增加如下内容

1
2
3
4
5
auto br0
iface br0 inet dhcp # 网桥使用DHCP模式,从DHCP服务器获取IP
bridge_ports enp3s0 # 网卡名称,网桥创建前连接外部的网卡,可通过ifconfig命令查看,有IP地址的就是
bridge_stp on # 避免数据链路出现死循环
bridge_fd 0 # 将转发延迟设置为0

接下来需要重启networking服务(如果是通过SSH连接到宿主机上的,这一步会导致网络中断,如果出现问题可能导致连不上宿主机,最好在宿主机上直接操作)

1
systemctl restart networking.service

使用ifconfig命令查看IP是否从enp3s0(网桥创建前的网卡)变到了br0上,如果没有变化则需要重启。如果宿主机ip已经成功变到网桥上,并且宿主机能正常上网而虚拟机获取不到ip,可能是ufw没有允许ip转发导致的,编辑/etc/default/ufw允许ip转发。

1
DEFAULT_FORWARD_POLICY="ACCEPT"

重启ufw服务让设置生效

1
systemctl restart ufw.service

NAT(Network Address Translation)模式

NAT模式是KVM默认的网络模式,KVM会创建一个名为virbr0的虚拟网桥,但是宿主机和虚拟机对于网桥来说不是平等的了,网桥会把虚拟机藏在背后,虚拟机会被分配一个内网IP,而从外网访问不到虚拟机。

NAT模式

安装Linux虚拟机

使用如下命令安装安装Linux虚拟机

1
2
3
4
5
6
7
8
sudo virt-install -n ubuntu_3
--description "ubuntu_3"
--os-type=linux --os-variant=ubuntu17.10 --ram=1024 --vcpus=1
--disk path=/var/lib/libvirt/images/ubuntu_3.img,bus=virtio,size=50 # 磁盘位置,大小50G
--network bridge:br0 # 这里网络选择了桥接模式
--accelerate
--graphics vnc,listen=0.0.0.0,keymap=en-us # VNC监听端口,注意要选择en-us作为key-map,否则键位布局可能会乱
--cdrom /home/zzy/Downloads/ubuntu-18.04-live-server-amd64.iso # 安装ISO路径

安装Windows 10虚拟机

安装Windows 10虚拟机会出现没有virtio驱动的问题,导致安装程序找不到硬盘,需要先下载virtio驱动。

https://fedoraproject.org/wiki/Windows_Virtio_Drivers

创建虚拟机时,将其加入到CD-ROM中

1
2
3
4
5
6
7
8
9
10
sudo virt-install -n win10
--description "win10"
--os-type=win --os-variant=win10
--ram=4096 --vcpus=2
--disk path=/var/lib/libvirt/images/win_10.img,bus=virtio,size=100
--network bridge:br0
--accelerate
--graphics vnc,listen=0.0.0.0,keymap=en-us
--cdrom =/home/zzy/Downloads/cn_windows_10_consumer_editions_version_1803_updated_march_2018_x64_dvd_12063766.iso
--cdrom=/home/zzy/Downloads/virtio-win.iso

使用VNC客户端连接虚拟机

执行以下命令查看虚拟机的列表

1
sudo virus list

通过上一步查处的虚拟机列表,查看单台机器的VNC端口

1
sudo virsh vncdisplay ubuntu_3  # ubuntu_3是虚拟机名称

知道了VNC端口号,就可以使用VNC客户端连接到虚拟机完成安装了。