ggaaooppeenngg

为什么计算机科学是无限的但生命是有限的

最新的 kubeadm 的设计文档在这里,如果把里面设计大概看一遍就能够理解里面的流程了,可以说设计的还是很缜密的,并且大大简化了 k8s 的运维工作。

kubeadm 安全设施

主要解释 kubeadm init 和 kubeadm join 的过程和实现

kubeadm init

  1. 首先进行 preflight-checks ,检查系统是否满足初始化的状态。
  2. 创建自签名的 CA,并且生成和签发各个 component 的私钥和证书 (/etc/kubernetes/pki),如果文件已存在就不会再生成了,比如要给 apiserver 添加域名可以重新签发一个证书,然后重启就好了。
  3. 写入各个服务的配置文件,以及一个 admin.conf (/etc/kubernetes/)
  4. 配置 kubelet 的动态配置加载 (disable by default)
  5. 配置静态 pod (/etc/kubernetes/manifests)
  6. 给 master 添加 taint 和 label,让其他 pod 默认不会运行在 master 上
  7. 生成用于让其他 kubelet 加入的 token
  8. 配置用 token 加入的可以自动确认 CSR(也就是用 CA 自动签 kubelet 的证书)
  9. 设置 kube-dns
  10. 检查 self-hosting,如果设置了,就把 static pod 转成 daemonset

是否使用外部 CA 的条件是,目录下有 CA 证书,但是没有 CA 私钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if res, _ := certsphase.UsingExternalCA(i.cfg); !res {

// PHASE 1: Generate certificates
if err := certsphase.CreatePKIAssets(i.cfg); err != nil {
return err
}

// PHASE 2: Generate kubeconfig files for the admin and the kubelet
if err := kubeconfigphase.CreateInitKubeConfigFiles(kubeConfigDir, i.cfg); err != nil {
return err
}

} else {
fmt.Println("[externalca] The file 'ca.key' was not found, yet all other certificates are present. Using external CA mode - certificates or kubeconfig will not be generated.")
}

具体的生成 PKI 相关的配置的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// CreatePKIAssets will create and write to disk all PKI assets necessary to establish the control plane.
// If the PKI assets already exists in the target folder, they are used only if evaluated equal; otherwise an error is returned.
func CreatePKIAssets(cfg *kubeadmapi.MasterConfiguration) error {

certActions := []func(cfg *kubeadmapi.MasterConfiguration) error{
CreateCACertAndKeyFiles,
CreateAPIServerCertAndKeyFiles,
CreateAPIServerKubeletClientCertAndKeyFiles,
CreateEtcdCACertAndKeyFiles,
CreateEtcdServerCertAndKeyFiles,
CreateEtcdPeerCertAndKeyFiles,
CreateEtcdHealthcheckClientCertAndKeyFiles,
CreateAPIServerEtcdClientCertAndKeyFiles,
CreateServiceAccountKeyAndPublicKeyFiles,
CreateFrontProxyCACertAndKeyFiles,
CreateFrontProxyClientCertAndKeyFiles,
}

for _, action := range certActions {
err := action(cfg)
if err != nil {
return err
}
}

fmt.Printf("[certificates] Valid certificates and keys now exist in %q\n", cfg.CertificatesDir)

return nil
}

列表中的函数都是用来生成所有证书和私钥的,主要依靠 k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil 来完成。私钥很容易生成,没什么需要特别配置的,主要看 CA 的证书里面配了啥,因为这个跟 k8s 的鉴权有关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// NewSelfSignedCACert creates a CA certificate
func NewSelfSignedCACert(cfg Config, key *rsa.PrivateKey) (*x509.Certificate, error) {
now := time.Now()
tmpl := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(0),
Subject: pkix.Name{
CommonName: cfg.CommonName,
Organization: cfg.Organization,
},
NotBefore: now.UTC(),
NotAfter: now.Add(duration365d * 10).UTC(),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
}

certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key)
if err != nil {
return nil, err
}
return x509.ParseCertificate(certDERBytes)
}

openssl x509 -in /etc/kubernetes/pki/ca.crt -text -noout 可以查看 ca 证书里面的内容,和我们看到的配置是一致的。

然后我们再看一下 apiserver 的私钥和证书是怎么签的,首先把生成的 CA 证书和私钥加载进来,然后生成私钥并且签出自己的证书。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func CreateAPIServerCertAndKeyFiles(cfg *kubeadmapi.MasterConfiguration) error {

caCert, caKey, err := loadCertificateAuthority(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName)
if err != nil {
return err
}

apiCert, apiKey, err := NewAPIServerCertAndKey(cfg, caCert, caKey)
if err != nil {
return err
}

return writeCertificateFilesIfNotExist(
cfg.CertificatesDir,
kubeadmconstants.APIServerCertAndKeyBaseName,
caCert,
apiCert,
apiKey,
)
}

这里比较重要,因为 SAN 这个是用来匹配域名的,如果这里没写好,HTTPS 是拒绝访问的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// NewAPIServerCertAndKey generate certificate for apiserver, signed by the given CA.
func NewAPIServerCertAndKey(cfg *kubeadmapi.MasterConfiguration, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) {

altNames, err := pkiutil.GetAPIServerAltNames(cfg)
if err != nil {
return nil, nil, fmt.Errorf("failure while composing altnames for API server: %v", err)
}

config := certutil.Config{
CommonName: kubeadmconstants.APIServerCertCommonName,
AltNames: *altNames,
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
apiCert, apiKey, err := pkiutil.NewCertAndKey(caCert, caKey, config)
if err != nil {
return nil, nil, fmt.Errorf("failure while creating API server key and certificate: %v", err)
}

return apiCert, apiKey, nil
}

apiserver 的证书也是对的,马赛克的部分是我自己配的一些 IP 和 域名,和 kubeadm 配的一些 kube-dns 用的 overlay 网络上的 master 域名,其他 master 上的 components 也是类似的,在配高可用的时候要把每个 master 节点上的域名和 IP 都配上。kubelet 的证书也要配对,就在 join 里面介绍了,这个会用来鉴权 node 的身份。

其他部分的代码和设计文档的描述是一致的,所以没什么好看的,感觉一个好的设计文档是非常重要的,代码只是设计的实现,如果设计本身就可读性很强,阅读代码只是辅助理解一些细节和找 BUG 用的而已。

kubeadm join

  1. 首先用 token 鉴权的方式获取 apiserver 的 CA 证书,并且通过 SHA256 验证。
  2. 加载动态配置,如果 master 上有的话。
  3. TLS 初始化,首先用 token 鉴权的方式把自己的 CSR 发给 apiserver,然后签发自己的证书,这个证书要用来检查 node 的身份的。
  4. 配置 kubelet 和 server 开始建立连接。

新版本的 kubelet 有些变动,支持把一些 featuregate 配置写到文件里面,比如这个阻止 fork bomb 的配置,新版本只能用配置文件写了,不能通过参数配置。

kubeadm 的详细过程如下:

首先会把 flags 传入 NodeConfiguration 中,开始 AddJoinConfigFlags(cmd.PersistentFlags(),如果没有 nodeNamecfg 默认用 host 的 name 并且小写化通过 GetHostname获得,和在宿主机上执行 hostname 是一致的。在初始化之前先 尝试启动 TryStartKubelet 过程,然后把 token 和 server 写入(证书 data 是怎么生成的?),先配置kubelet-bootstrap-config

整体流程如下,token 验证是基于 JWT 的,所以 token 的格式是 "^([a-z0-9]{6})\\.([a-z0-9]{16})$",分两部分,tokenID.tokenSecret

1
2
3
4
5
6
7
8
discovery.For
->GetValidatedClusterInfoObject 这个是获取 CA 证书的过程
->token.RetrieveValidatedClusterInfo 这一步是 JWT 验证
->tokenutil.ParseToken 得到 tokenID 和 tokenSecret
->pubKeyPins.Allow 加载用于检验 CA 证书的 HASH 值
->buildInsecureBootstrapKubeConfig (用 token-bootstrap-client 身份)获取信息
-> 从kube-public 获取 configmap (和 kubectl describe configmap cluster-info -n kube-public 中的信息是一致的)这个configmap 里面包含 JWS 签名和 master 的 CA 证书,获取对应 tokenID 的 jws token,验证 token 成功,并且验证 CA 的证书 hash,如果通过说明这个 master 是可信的,然后拿到 CA 证书以后开始构建自己的证书,建立 secure config。
->

kubeconfigutil.WriteToDisk 会把 bootstrap-kubelet.conf 写入到配置目录中,kubeadm 的任务就完成了,之前的 kubeadm 会代替 kubelet 生成 kubelet.conf(其中用的是公钥鉴权),现在移走了,kubelet 启动的时候会尝试使用这个配置文件建立 HTTPS 的鉴权配置文件。

可以看一下 kubelet 用这个配置给 master 发 CSR 以后得到了什么,可以看到是生成了自己的证书的。

并且这个证书里面的 SAN 也是用来进行 Node 鉴权的身份确认的信息。用 openssl x509 -in /var/lib/kubelet/pki/kubelet-client.crt -text -noout查看信息。

红线部分就是 Node 鉴权的信息,基于这个确认 node 的身份。另外一个是 Role Based Access Control,那个主要是配给 pod,限制 pod 行为用的,类似于 linux 的 chmod

kubeadm 高可用配置

如果没有安全配置,其实高可用挺好配置的,现在加入了安全配置,虽然麻烦,但是还是能理解 k8s 的良苦用心的,理解了整个鉴权的过程以后就可以做,现在支持配置文件初始化其实更好配置了。可以基于这个文档配置,算了我还是不总结了,看懂上面的,照着配置就好了。主要是自己生成 CA,他们用的 cfsslcfssljson,这个工具比 openssl 好用一点,比较容易配置。生成 ca 证书和私钥以后,就可以构建 HTTPS 的 etcd。

先创建 ca 的配置文件 ca-config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"signing": {
"default": {
"expiry": "43800h"
},
"profiles": {
"server": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
]
},
"client": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"client auth"
]
},
"peer": {
"expiry": "43800h",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
]
}
}
}
}

然后生成用于自签名的 csr 的配置文件 ca-csr.json,用于签发自签名的 CA 证书。

1
2
3
4
5
6
7
{
"CN": "etcd",
"key": {
"algo": "rsa",
"size": 2048
}
}

结果就是目录下面生成了,ca-key.pemca.pem,这个命令不太按规则,对应的叫 ca.keyca.crtpem 是密钥保存的格式。

接下来用 client.json 获得自己的私钥和通过 ca 签的证书。

1
2
3
4
5
6
7
{
"CN": "client",
"key": {
"algo": "ecdsa",
"size": 256
}
}

执行 cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client client.json | cfssljson -bare client,生成了 client-key.pem,client.pem,分别是私钥和证书,把文件 ca.pem,ca-key.pem, client.pem,client-key.pem,ca-config.json, 拷贝到每台 master 机器上面,一般放到 /etc/kubernetes/pki/ 下面。

然后

1
2
3
4
5
6
7
cfssl print-defaults csr > config.json
sed -i '0,/CN/{s/example\.net/'"$PEER_NAME"'/}' config.json
sed -i 's/www\.example\.net/'"$PRIVATE_IP"'/' config.json
sed -i 's/example\.net/'"$PEER_NAME"'/' config.json

cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server config.json | cfssljson -bare server
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer config.json | cfssljson -bare peer

签出出两对密钥和证书,把每台的机器的 域名 和 IP 替换掉示例的 example 配置,就是这些地方可以改成自己的域名和地址,这个会被用来 check。

可以放到 kubelet 的 static pod 里面,启动 etcd,两个证书分别是用作 server 验证和 client 验证的,server 让别人访问的时候相信你,peer 是你访问别人的时候让别人相信你。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
cat >/etc/kubernetes/manifests/etcd.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
labels:
component: etcd
tier: control-plane
name: <podname>
namespace: kube-system
spec:
containers:
- command:
- etcd --name ${PEER_NAME} \
- --data-dir /var/lib/etcd \
- --listen-client-urls https://${PRIVATE_IP}:2379 \
- --advertise-client-urls https://${PRIVATE_IP}:2379 \
- --listen-peer-urls https://${PRIVATE_IP}:2380 \
- --initial-advertise-peer-urls https://${PRIVATE_IP}:2380 \
- --cert-file=/certs/server.pem \
- --key-file=/certs/server-key.pem \
- --client-cert-auth \
- --trusted-ca-file=/certs/ca.pem \
- --peer-cert-file=/certs/peer.pem \
- --peer-key-file=/certs/peer-key.pem \
- --peer-client-cert-auth \
- --peer-trusted-ca-file=/certs/ca.pem \
- --initial-cluster etcd0=https://<etcd0-ip-address>:2380,etcd1=https://<etcd1-ip-address>:2380,etcd2=https://<etcd2-ip-address>:2380 \
- --initial-cluster-token my-etcd-token \
- --initial-cluster-state new
image: k8s.gcr.io/etcd-amd64:3.1.10
livenessProbe:
httpGet:
path: /health
port: 2379
scheme: HTTP
initialDelaySeconds: 15
timeoutSeconds: 15
name: etcd
env:
- name: PUBLIC_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: PRIVATE_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: PEER_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- mountPath: /var/lib/etcd
name: etcd
- mountPath: /certs
name: certs
hostNetwork: true
volumes:
- hostPath:
path: /var/lib/etcd
type: DirectoryOrCreate
name: etcd
- hostPath:
path: /etc/kubernetes/pki/etcd
name: certs
EOF

然后每个节点的 master init 的配置文件按照下面这个配置,client.pem 是用来让 etcd 相信 apiserver 的。private-ipload-balancer-ip 都是要写到证书的 SAN 里面的,不然用这些 ip 是访问不了 apiserver 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: kubeadm.k8s.io/v1alpha1
kind: MasterConfiguration
api:
advertiseAddress: <private-ip>
etcd:
endpoints:
- https://<etcd0-ip-address>:2379
- https://<etcd1-ip-address>:2379
- https://<etcd2-ip-address>:2379
caFile: /etc/kubernetes/pki/etcd/ca.pem
certFile: /etc/kubernetes/pki/etcd/client.pem
keyFile: /etc/kubernetes/pki/etcd/client-key.pem
networking:
podSubnet: <podCIDR>
apiServerCertSANs:
- <load-balancer-ip>
apiServerExtraArgs:
apiserver-count: "3"
EOF

至于怎么做 loadbalance 可以用七层的也可以用四层的,七层把证书配到负载均衡服务上,四层的就不用,自己在裸机器上做可以用 vip + nginx 做一个四层的,也可以把证书放到 nginx 上做七层的,但是在云环境下,都不怎么支持自己配置 vip,需要用云厂商的 lb 服务,这个就看具体提供商的服务怎么配了。

首先公钥念“gongyue”,而不是公钥,拼音打字打多了就发现得这么读。

HTTPS 是基于 SSL/TLS 的安全 HTTP 协议,其实 HTTPS 主要还是看安全套接字层,在这之上还是一个 HTTP 协议,这里主要总结一下我们现在主流用到的加密体系,比如我们经常看到的 .pem.crt.key.csr 还有 CA 啊之类的是啥东西,并且这些东西都如何工作和应用的。

在密码学里面,有几个角色,类似于中国的甲乙丙丁,一个是 Alice 和 Bob,这是正常通信的两个人,还有一个是 Eve,是信道上具备窃听能力的人,另外一个是 Mallory,这个人可以妨碍网络流量,主动攻击。

对称加密

其实最简单的加密算法就是对称加密

Alice 和 Bob 有一个共有的密码,也就是只有两个人拥有,相互传输的密文只能通过这个密钥解开。

首先加密需要基于密钥,凯撒密码可不可以,可以是可以,但是被人知道算法以后,就可以被所有人破解,并且优秀的加密算法应该被人验证,如果不是公开的算法就没有办法验证,当然一些保守的加密方法会不公开,这样其实也很难解密,但是互联网的加密协议很显然是用在千家万户的,所以一定要是经得起推敲的加密算法。

分组密码

分组密码的作用是,一般会用 128 位一个分组,这样加密的好处是即使一个小的变化也会导致输出大量的变化,这样攻击者很难通过出现频率分析加密方式(比如 HTTP 开头都是一样的,所以使用顺序加密,很容易用 HTTP 的通用开头做输入得出加密方法)。但是分组密码的小影响会导致大改变的特性导致攻击者没办法这么做。

(如果攻击者把有的流量都记录下来,等有一天通过方法获得密钥就能解开这些数据了,可能是未来算力提高,或者通过法律手段,斯诺登的加密信箱就是 FBI 让加密邮箱公司强制提供的)

哈希函数

哈希函数其实很熟悉了,解释一下 MAC。

MAC

MAC 是 message authetication code,是密钥的哈希函数,因为普通哈希函数,如果 Mallory 可以直接用假的数据用哈希算出结果发给 Bob,缺少身份验证,MAC 就是带密钥的哈希函数,HMAC 其实就是把密钥和消息组合在一起的协议。

非对称加密

非对称加密又叫做公钥加密,对称加密固然好,但是对称密钥在团体中使用的话,大家都要共享,密钥给来给去的很容易出问题。对称加密就没有这个问题,可以方便传播,对称密钥分私钥和公钥,一个用于加密一个用于解密,私钥加密的数据只能用公钥解密。公钥的出现使得密钥可以大范围传播。

数字签名

数字签名主要验证消息的真实性,对消息进行验证。主要是对消息进行哈希,然后用私钥加密,追加到文档中做身份验证,这样用公钥解的开的话就能证明消息的发送者。

TLS 的具体协议

PKI 公钥基础设施

PKI 主要是用来保证公钥的可信,通过中间的权威机构(CA)签发的公钥也就是证书才能被认可是合法的证书。

证书

证书包含了版本、序列号、签名算法、颁发者、有效期、使用者、公钥。证书也有证书链,比如 root CA 可以签发 中间 CA 的证书。根证书一般是跟着操作系统一起造就装好了的,大公司的操作系统和浏览器都有自己的根证书库,自带就在电脑上。

应用

RSA 是目前最广泛应用的密钥算法,破解 1024 位的 RSA 密钥的成本大约是 1000 万人民币,现在一般用 2048 位的 RSA,基本无解。

其实公钥体系已经很完全了,但是大部分出问题都情况是私钥泄漏,管理自己的私钥非常重要。

1. 不要用 CA 生成的私钥,尽量自己生成。
2. 不要用刚开机的机器生成随机数,这台机器获得的外界熵不够多。
3. 定期更换私钥
4. 不要随意传播私钥
5. 安全存储私钥,这个主要是中间 CA 可能要用,普通服务直接换私钥就可以。

下面就用 OpenSSL 进行一些实验。

首先我们生成一个 RSA 密钥,前面提到了最好用 2048 位的,用 AES-128 算法来加密保存,会让你输入密码,这个密钥基于这个密码保存,这个文件的格式叫 PEM 所以如果看到 PEM 格式的文件就知道是私钥了。

1
2
3
4
5
6
7
$openssl genrsa -aes128 -out test.key 2048
Generating RSA private key, 2048 bit long modulus
...+++
.............................+++
e is 65537 (0x10001)
Enter pass phrase for test.key:
Verifying - Enter pass phrase for test.key:

可以看这个文件,看起来一通乱七八糟的东西,但是根据开头的信息,我们有办法用密码把私钥解析出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$cat test.key
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,52551A2438582E22358335433B7BAEE0

GiVITGQMbkGxzBYestzFNX4KMUgP84A2p49mOBozQWkAol+zU5llumFXrj/bzATq
OiH01+UN7sqKXD+lNwXsvL2bhhWFGe4h80CbCaVhPYnAtNtxU4HNTbAnCFZPmTHg
wC5JCHlJvt6TGIOeOeyTj9qnCeIKd1XJGYypG8syzzaKuNbSwlKN8DrUKGJP/w/8
gCPKT2AA2l53ysxBI6i2jHAxQSs1Y+K1jFrZjgObT3QDN4eqT1Io/waSDAiB8tkl
rW35ZYO0Toe3iJgTOp325v5dvC6mCOvL0QAQWDz7y239l8fAdyDJUO2tSzjXfIii
FM12NaHfw5m+GcT6brqlwbAOL6BSMX8Q+Dj/fdeDoPOF+pKnG4AGW3LPEY6LJgq0
pkExHJ5cl/nEL2Q2H3yekvPjW40XDZfjyQSivwEJGhsmAq3+tfis/7h5f5Rg/2yh
Vb8kkPQTu0SofS1VFkSQN6iJe5A3imjMkacKDoKtafORR1lWu0noaOqr757orcgI
hK847ijdFdwjstycrGBdN1INr0Yx/FnyaPYNR2XZhWD4uQwZ1tMVge5duw8jgHrH
ZwCeBAgfpSynbMy/1GTru1sO2T6qpZj9pfao59jVcN1fka+YKI5oEdxFFc97wcgk
XVX9lVrcPrI0bMyuhje5Vi04HbOxpd4GmtnNPKTwtmMbDsSVVIX+hlFltSBl+0in
PsKlUeBGjAmUapT3x1v7OP5K7K8YvszHMehbctDS2E9bZstCwhnsMog3b+Jxhw40
gXsOc6Vb3kJljkPXu6k7qGGkqzVqUuUMSaWlE87s5Cm4ZyS8c5IPQvmQk4S/1AJP
v5sD3TObRlJAIw0MEItPY4daQBMyIXPr+UXUAKfMoFK8bXG9aNpACKm/pQ1pVMj1
eE0+lzQ1UZUVM0GotBZGce+TtbrU/I/dbhGLA2KKyoohsCSH2yV+wGIMrmmbUiqH
a46FYwJLtFdDZ9m3ZVj1KCMza9/B2ylIetCX98C3/fVd81L3rxpSmbpWvzWYhw+Y
205t8p26WUloAQrkP+kqw0HDsiDeEU2QilvNwtA5qlfd8666rJEJpBg50jM8Sb0R
JYsG8Mc2a9CNpXt5pVi2kHdoRRkiSeGDh9xTnOlvnCc77p4MMlvcC3bqHwGe85ON
0q5xVJMyLwzAXR82X0FunRiiDNBO3Pg6k+UBALmLNrgieet57Jdva90OnkEA0Ihg
sYHHnfsMMJ6EnW4mNov8tdVi7mExK8K+RRe1vu+zAqzx/UuKkjekW0NdzQjHv4Hs
mX/9XzVE2cLG9GLWVojyTS5XUg16NBn7qM3HRQmQoAgXHlfCJUx33xMjTL3glbNH
65jpvG1S7l6U45BGMI3d2eQ0XfiSUHOHh/zHOIGRl7JcHyoK082IzivPbHkrlolv
o1E+4sgpFc09rQYgcW2cb6Nr3H3aYOlC86iPn4Ecxj8M/wznH/JNjjfoKKhmFWcU
EtmrreXjR8QlUCXCHZCA3DOi/MujcPU0qGZkaz+Ttii4NACZoHpedL+XQ9EcXci7
xkFO4NxsezDunpvXNHgkzES+as5lNxSa4wC4tUbC9h4zhgbeiOGBSTL8Aq0MxwX3
-----END RSA PRIVATE KEY-----

解析私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
$openssl rsa -text -in test.key
Enter pass phrase for test.key:
Private-Key: (2048 bit)
modulus:
00:a0:18:a6:70:ce:65:64:ea:f1:43:30:35:ff:cf:
b4:5a:8e:20:52:04:84:bf:bf:3a:b4:a7:65:94:a5:
df:b5:14:09:61:ba:79:8a:43:d1:fe:cc:d9:96:d3:
81:5b:82:d7:63:e1:e9:6f:30:5e:b0:3f:fe:65:c4:
e1:d5:b2:e3:ce:ba:fc:7b:5e:b2:83:f0:9f:3f:c2:
15:39:b4:fa:e7:3c:ff:42:96:e7:a6:7a:29:e1:ba:
0b:c7:99:aa:ac:07:2a:2b:74:b3:f8:10:8d:0f:91:
44:a4:fa:48:c1:aa:88:0e:86:ff:c1:da:59:c8:dd:
32:d5:5b:15:f4:80:3e:2f:d7:d3:92:09:63:54:d0:
01:46:78:cd:5c:5d:f1:1c:ad:a7:ab:84:5c:86:e1:
25:69:6d:6c:6c:df:90:5f:af:ca:6f:43:17:50:05:
b0:77:3d:92:e9:e7:4c:66:c3:58:08:96:60:7a:16:
02:d3:6f:56:cb:df:41:69:eb:83:f3:28:b7:82:0a:
c2:c6:b4:a3:6e:f1:2d:7f:ec:ea:87:7b:94:4b:8b:
b8:e1:72:0d:00:c1:8d:9f:cc:03:32:de:74:6e:26:
29:0b:4f:f4:41:93:1c:9c:ae:22:41:81:71:b6:9c:
8c:17:15:63:d5:86:ce:74:b2:99:fb:7f:ff:37:c8:
03:7b
publicExponent: 65537 (0x10001)
privateExponent:
1a:ae:40:fe:c7:c6:ea:1c:a5:7c:97:0a:48:c9:aa:
ba:f4:b8:ba:32:7a:95:22:1f:7c:7f:f1:53:e6:98:
f3:aa:95:2d:ae:50:17:14:da:68:66:67:54:d5:86:
d7:63:64:d6:06:8e:4a:b3:7a:f4:50:95:eb:0b:f6:
bf:10:83:1a:ae:da:e9:0c:8d:1f:a3:f8:46:3d:e8:
1f:a7:e3:b0:a9:df:b8:8f:41:a7:e2:f0:1b:e8:4f:
92:42:2f:c9:5f:a0:4d:81:b3:84:81:ed:a0:4c:8b:
6e:1b:30:08:e6:8c:aa:2f:21:6c:83:21:37:72:75:
c8:4c:d7:c9:d9:9d:83:87:67:05:4d:6d:28:72:71:
63:3b:b6:82:f8:42:0f:94:af:f2:b1:d8:c5:d3:3f:
50:bf:13:61:b2:0b:de:6b:34:42:cd:29:27:04:c9:
ff:49:14:75:d0:d5:e5:5c:4b:29:a1:95:c3:c5:e5:
34:46:9e:81:d4:9d:c3:c4:06:c9:96:90:39:90:fb:
db:06:77:fa:46:73:38:60:0e:e3:40:7b:d0:5d:a0:
97:0d:6e:0b:39:d6:99:63:a6:ee:67:b7:94:35:e2:
63:cf:02:a1:eb:0a:f0:50:99:6f:30:ae:6b:ef:1e:
14:a0:1a:f4:8e:ed:cd:81:bf:3d:2b:9d:b5:9e:b8:
21
prime1:
00:cc:ff:e2:f4:39:5d:33:de:96:15:e6:7c:d2:e6:
a3:56:a9:6a:09:0c:e9:26:94:36:41:92:b9:db:c9:
09:20:28:9d:bc:c6:76:60:88:93:97:81:16:86:da:
4d:65:0e:87:ec:ef:15:6d:c9:06:f7:99:12:eb:4a:
a6:7e:49:9d:1a:68:ca:35:57:5c:4b:2f:32:2e:e4:
76:87:a5:02:94:27:1a:1f:38:28:58:77:68:2d:5d:
fa:c2:fd:c4:09:80:e4:eb:14:84:cc:73:06:96:4b:
08:8e:da:1c:55:38:8d:8c:7f:19:01:fa:54:b3:62:
d4:cb:4c:df:01:e0:02:d8:c3
prime2:
00:c7:ec:f1:2b:83:26:d1:35:e3:55:00:3e:a9:7d:
2e:f0:68:4d:27:77:3f:d5:1c:99:ef:a0:98:3c:fd:
fd:d7:8f:51:f5:82:e7:8f:37:34:a7:1a:1f:c4:83:
44:ab:11:62:54:7a:5e:5c:a4:7f:d6:dd:f8:45:3c:
b6:bc:1e:b5:56:df:60:65:66:aa:43:82:f5:7a:7c:
72:3b:3d:fe:33:d4:27:b2:c5:9a:07:36:b4:ca:bc:
1d:a7:7a:5f:9c:1a:75:2b:2c:57:97:5a:b8:a9:de:
0e:8a:8c:84:ff:51:e9:12:e9:d4:8b:bf:de:5f:98:
52:9c:08:55:42:e1:70:be:e9
exponent1:
15:76:dd:7e:90:db:0f:69:48:f1:b6:16:6f:c6:b2:
67:8a:89:8d:b5:0a:5c:7d:bc:48:95:62:5c:7e:ea:
33:b1:cd:02:4d:0d:6c:02:20:e2:06:24:23:ae:8b:
d7:fe:f3:80:7d:70:12:f4:af:84:11:45:07:d9:e3:
20:e9:f8:47:21:9d:ba:84:11:27:d6:23:3d:01:b2:
df:75:09:96:15:9a:08:96:ca:b2:a8:9e:01:d2:0b:
45:8b:68:91:4e:2b:a9:e9:96:16:0a:1d:30:73:5e:
cc:06:4e:5d:25:f4:bc:37:3a:99:18:6a:f1:f5:71:
2e:70:38:11:6c:31:20:1d
exponent2:
00:a6:f0:8f:21:4a:4e:6b:7b:97:ec:2e:5c:24:a2:
c7:43:2f:94:dd:53:92:15:9d:e0:5c:5b:b9:43:94:
c3:15:f0:32:fb:d2:e7:10:8b:84:87:d4:24:9a:af:
11:f3:d6:7c:49:16:35:1d:1e:af:30:f8:00:8b:af:
fa:d6:72:bd:f1:60:6c:d9:bf:34:85:53:21:2f:ba:
22:98:9d:57:5a:67:d9:0e:4a:3a:27:b3:e2:9b:37:
21:7b:eb:8f:52:86:35:38:6b:ba:68:43:f4:d6:c2:
f9:59:6f:a4:ce:9d:d3:05:5c:03:82:fe:1f:ed:aa:
ff:b0:12:b5:3f:37:88:31:a1
coefficient:
09:97:9e:dc:20:fe:c5:e2:34:47:d8:64:de:bb:ad:
70:65:4d:08:49:c8:cf:28:40:f6:87:43:09:c9:63:
bc:d8:cd:11:53:78:ba:ad:1a:f0:8b:e7:fa:1c:5f:
c9:9d:5f:ae:e1:2a:7f:87:7a:7f:1a:e3:c8:b5:8d:
eb:b2:af:18:c6:1e:07:43:f0:e7:be:4e:bc:c6:1b:
77:b8:43:36:58:3a:b5:8a:2c:f7:76:37:c7:97:4c:
8c:fd:47:71:09:f8:76:fe:8d:0f:e1:3a:30:56:5c:
2b:70:60:9d:fa:53:74:8a:db:b9:04:78:ce:1c:1d:
28:ca:78:81:53:07:de:5e
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAoBimcM5lZOrxQzA1/8+0Wo4gUgSEv786tKdllKXftRQJYbp5
ikPR/szZltOBW4LXY+HpbzBesD/+ZcTh1bLjzrr8e16yg/CfP8IVObT65zz/Qpbn
pnop4boLx5mqrAcqK3Sz+BCND5FEpPpIwaqIDob/wdpZyN0y1VsV9IA+L9fTkglj
VNABRnjNXF3xHK2nq4RchuElaW1sbN+QX6/Kb0MXUAWwdz2S6edMZsNYCJZgehYC
029Wy99BaeuD8yi3ggrCxrSjbvEtf+zqh3uUS4u44XINAMGNn8wDMt50biYpC0/0
QZMcnK4iQYFxtpyMFxVj1YbOdLKZ+3//N8gDewIDAQABAoIBABquQP7HxuocpXyX
CkjJqrr0uLoyepUiH3x/8VPmmPOqlS2uUBcU2mhmZ1TVhtdjZNYGjkqzevRQlesL
9r8Qgxqu2ukMjR+j+EY96B+n47Cp37iPQafi8BvoT5JCL8lfoE2Bs4SB7aBMi24b
MAjmjKovIWyDITdydchM18nZnYOHZwVNbShycWM7toL4Qg+Ur/Kx2MXTP1C/E2Gy
C95rNELNKScEyf9JFHXQ1eVcSymhlcPF5TRGnoHUncPEBsmWkDmQ+9sGd/pGczhg
DuNAe9BdoJcNbgs51pljpu5nt5Q14mPPAqHrCvBQmW8wrmvvHhSgGvSO7c2Bvz0r
nbWeuCECgYEAzP/i9DldM96WFeZ80uajVqlqCQzpJpQ2QZK528kJICidvMZ2YIiT
l4EWhtpNZQ6H7O8VbckG95kS60qmfkmdGmjKNVdcSy8yLuR2h6UClCcaHzgoWHdo
LV36wv3ECYDk6xSEzHMGlksIjtocVTiNjH8ZAfpUs2LUy0zfAeAC2MMCgYEAx+zx
K4Mm0TXjVQA+qX0u8GhNJ3c/1RyZ76CYPP39149R9YLnjzc0pxofxINEqxFiVHpe
XKR/1t34RTy2vB61Vt9gZWaqQ4L1enxyOz3+M9QnssWaBza0yrwdp3pfnBp1KyxX
l1q4qd4OioyE/1HpEunUi7/eX5hSnAhVQuFwvukCgYAVdt1+kNsPaUjxthZvxrJn
iomNtQpcfbxIlWJcfuozsc0CTQ1sAiDiBiQjrovX/vOAfXAS9K+EEUUH2eMg6fhH
IZ26hBEn1iM9AbLfdQmWFZoIlsqyqJ4B0gtFi2iRTiup6ZYWCh0wc17MBk5dJfS8
NzqZGGrx9XEucDgRbDEgHQKBgQCm8I8hSk5re5fsLlwkosdDL5TdU5IVneBcW7lD
lMMV8DL70ucQi4SH1CSarxHz1nxJFjUdHq8w+ACLr/rWcr3xYGzZvzSFUyEvuiKY
nVdaZ9kOSjons+KbNyF7649ShjU4a7poQ/TWwvlZb6TOndMFXAOC/h/tqv+wErU/
N4gxoQKBgAmXntwg/sXiNEfYZN67rXBlTQhJyM8oQPaHQwnJY7zYzRFTeLqtGvCL
5/ocX8mdX67hKn+Hen8a48i1jeuyrxjGHgdD8Oe+TrzGG3e4QzZYOrWKLPd2N8eX
TIz9R3EJ+Hb+jQ/hOjBWXCtwYJ36U3SK27kEeM4cHSjKeIFTB95e
-----END RSA PRIVATE KEY-----

这个私钥是我自己生成的,没用来干什么,所以直接展示了,但是生产环境的私钥要妥善保管,现在我们生成共钥,输入密码,读取密钥,然后 -pubout 表示生成公钥。

1
2
3
4
5
6
7
8
9
10
11
12
13
$openssl rsa -in test.key -pubout -out test-public.key
Enter pass phrase for test.key:
writing RSA key
$cat test-public.key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoBimcM5lZOrxQzA1/8+0
Wo4gUgSEv786tKdllKXftRQJYbp5ikPR/szZltOBW4LXY+HpbzBesD/+ZcTh1bLj
zrr8e16yg/CfP8IVObT65zz/Qpbnpnop4boLx5mqrAcqK3Sz+BCND5FEpPpIwaqI
Dob/wdpZyN0y1VsV9IA+L9fTkgljVNABRnjNXF3xHK2nq4RchuElaW1sbN+QX6/K
b0MXUAWwdz2S6edMZsNYCJZgehYC029Wy99BaeuD8yi3ggrCxrSjbvEtf+zqh3uU
S4u44XINAMGNn8wDMt50biYpC0/0QZMcnK4iQYFxtpyMFxVj1YbOdLKZ+3//N8gD
ewIDAQAB
-----END PUBLIC KEY-----

创建证书需要发起 CSR(certificate signing request),到 CA 那里,这个 csr 包含了申请者的信息和申请者的公钥。下面就是创建 csr 的命令,其中比较重要的是要配置好 Common Name,这个会拿来和访问的 host 进行匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$openssl req -new -key test.key -out test.csr
Enter pass phrase for test.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:CN
State or Province Name (full name) []:SH
Locality Name (eg, city) []:SH
Organization Name (eg, company) []:.
Organizational Unit Name (eg, section) []:.
Common Name (eg, fully qualified host name) []:www.example.com
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:1234

可以查看里面的信息是否正确,req 表示处理 csr 文件,-text 一般是用于展示文件内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
$openssl req -text -in test.csr -noout
Certificate Request:
Data:
Version: 0 (0x0)
Subject: C=CN, ST=SH, L=SH, CN=www.example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:a0:18:a6:70:ce:65:64:ea:f1:43:30:35:ff:cf:
b4:5a:8e:20:52:04:84:bf:bf:3a:b4:a7:65:94:a5:
df:b5:14:09:61:ba:79:8a:43:d1:fe:cc:d9:96:d3:
81:5b:82:d7:63:e1:e9:6f:30:5e:b0:3f:fe:65:c4:
e1:d5:b2:e3:ce:ba:fc:7b:5e:b2:83:f0:9f:3f:c2:
15:39:b4:fa:e7:3c:ff:42:96:e7:a6:7a:29:e1:ba:
0b:c7:99:aa:ac:07:2a:2b:74:b3:f8:10:8d:0f:91:
44:a4:fa:48:c1:aa:88:0e:86:ff:c1:da:59:c8:dd:
32:d5:5b:15:f4:80:3e:2f:d7:d3:92:09:63:54:d0:
01:46:78:cd:5c:5d:f1:1c:ad:a7:ab:84:5c:86:e1:
25:69:6d:6c:6c:df:90:5f:af:ca:6f:43:17:50:05:
b0:77:3d:92:e9:e7:4c:66:c3:58:08:96:60:7a:16:
02:d3:6f:56:cb:df:41:69:eb:83:f3:28:b7:82:0a:
c2:c6:b4:a3:6e:f1:2d:7f:ec:ea:87:7b:94:4b:8b:
b8:e1:72:0d:00:c1:8d:9f:cc:03:32:de:74:6e:26:
29:0b:4f:f4:41:93:1c:9c:ae:22:41:81:71:b6:9c:
8c:17:15:63:d5:86:ce:74:b2:99:fb:7f:ff:37:c8:
03:7b
Exponent: 65537 (0x10001)
Attributes:
challengePassword :unable to print attribute
Signature Algorithm: sha256WithRSAEncryption
33:83:fa:d3:a1:7d:1b:5c:cc:cb:b1:19:99:79:e4:b8:29:fc:
0e:ac:e6:40:f5:13:f0:d7:f7:2b:67:d4:32:39:78:3f:0b:f0:
5e:2c:f4:5c:c1:14:f0:f7:82:5d:1e:c5:bf:00:3e:87:d2:b5:
ed:a7:46:75:70:da:db:53:f1:19:37:15:63:09:63:a8:4d:74:
19:ed:c5:3a:50:7b:db:5a:68:f0:88:37:54:23:0d:bb:4d:c3:
b6:1a:3f:1d:93:24:17:f3:c5:66:c8:9c:43:67:e8:3b:cc:48:
20:8e:9e:da:a6:a0:48:90:6d:b1:bc:ff:0d:39:62:7b:8c:5c:
cb:ec:ce:e1:de:0c:f3:5b:51:3e:5c:ab:ad:6f:f5:96:9c:e5:
12:9e:1b:a7:27:90:fe:d3:9f:f9:c2:9d:7e:b5:62:ac:f9:45:
33:6a:a7:b5:c2:ab:b7:18:a8:a6:91:15:26:27:a4:c9:84:26:
88:85:3e:68:99:8c:f4:c6:32:8d:61:71:83:cb:86:96:92:2e:
c7:bc:76:e0:59:82:e8:fe:47:39:da:f0:57:72:f7:59:c4:ba:
7a:51:23:13:bc:8c:75:07:d7:2d:cf:2b:69:07:20:80:27:6d:
6d:ae:cb:27:5d:ef:0c:92:99:a4:02:45:5b:58:ac:e9:71:1e:
ee:5f:54:78

可以看到里面的信息,还有签名算法,以及公钥等等。

我们可以用自己的私钥给自己签名,比如 x509 是证书的格式。

1
2
3
4
5
$openssl x509 -req -days 365 -in test.csr -signkey test.key -out test.crt
Signature ok
subject=/C=CN/ST=SH/L=SH/CN=www.example.com
Getting Private key
Enter pass phrase for test.key:

也可以把两步结合起来,直接创建自签名的证书,openssl req -new -x509 -days 365 -key test.key -out test.crt,如果不想要交互式的可以直接

1
2
openssl req -new -x509 -days 365 -key test.key -out test.crt \
-subj "/C=CN/L=BJ/O=HaiDian/CN=www.example.com"

CN 只能写一个,虽然可以写泛域名,但是要支持多个域名可以通过扩展字段 SAN(Subject Alternative Name)来解决。

我的一点观点

我个人觉得 nvidia 的 CUDA 太封闭了,不是很能明白这样封闭的产品怎么能够长久生存,仅仅是因为家大业大么,如果有家公司推出了八成性能的 GPU,但是整套开发的生态非常友好,是不是会像 Android 取代诺基亚一样,还是 nvidia 就是苹果,就算封闭环境也能保证强劲体验,这我也不好说了。

安装

CUDA+cudnn 装起来挺麻烦的,反正如果有错误的话,可以检查 CUDA samples 里面的 deviceQuery 是否成功,如果不成功可以用 strace 看一下少了什么东西,再想办法安装上去。检查 cudnn samples 是否成功也是一样的,里面有一个 mnistDNN 的例子。

Hello World!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

__global__ void helloFromGPU (void)
{
printf("Hello world!\n");
}

int main(void)
{
printf("Hello World! from CPU\n");

helloFromGPU <<<1, 10>>>();
cudaDeviceReset();
return 0;
}

以上就是一段 GPU 的 Hello world,执行下面的代码可以看到我们在 GPU 上并行执行了十个 “Hello world!” 的打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ nvcc -arch sm_20 hello.cu -o hello
$ ./hello
Hello World! from CPU
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!
Hello world!

CUDA

CUDA 全称 Compute Unified Device Architecure,用于定义 GPU 的架构标准。GPU 的工作方式主要依赖于多核心的并行计算,CUDA 提供了方便的模型进行这种模式的编程,下面就会简单介绍一下 CUDA 的架构以及基于 GPU 的编程。

CUDA 一次启动的线程称为网格(grid),网格中包含块,每个块包含新程,是一个二维的模式,这张图片就说明得很清晰,首先是由 Grid 组成,然后每个 Block 有 shared memory,同 block 的线程可以访问 shared memory,不同的 Block 的线程只能访问全局的内存,这种结构也方便设计和实现并行算法。

cuda-arch

CUDA 的编译器 nvcc 是 gcc 的一个扩展,支持编写运行在 GPU 上的函数,其中的,<<x, y>> 扩展就是用来指定 block 和 thread 的数量的。

比如典型的俩个向量相加的例子:

1
2
3
4
void sumArrayOnHost(float *A, float *B, float *C, const int N) {
for (int i = 0; i < N; i++)
C[i] = A[i] + B[i]
}

但是 GPU 的核函数怎么写呢,是这样的。

1
2
3
4
__global__ void sumArrayOnGPU(float *A, float *N, float C) {
int i = threadIdx.x
C[i] = A[i] + B[i]
}

然后,如果 N 是 32 的话,可以如下调用,__global__ 表示这个函数可以在 host 调用也可以在 GPU 上调用,用个 32 个线程的 block 算这个向量和,N 隐性包含在了定义中。

1
sumArrayOnGPU<<1, 32>>(float *A, float *B, float *C)

线程组织

基于 CPU 和 GPU 的异构计算平台可以优势互补,CPU 负责处理逻辑复杂的串行程序,而 GPU 重点处理数据密集型的并行计算程序,从而发挥最大功效。

线程组织主要依据网格(现在网络这个词其实比较容易混淆,可以是神经网络可以是计算机网络,这里指的是线程的组织形式)模型。主要分 grid, block, thread,组织的方式就靠索引来决定,可以通过 block 索引和 thread 索引进行线程的定位。CUDA 提供了块内线程同步的方法,但是没有提供块间同步的原语。

线程束分化

线程束 = warp

CPU 有很强的分支预测的能力,会预加载指令,如果预测正确,执行代价就很小,但是 GPU 在这方面是很弱的,因为线程束当中如果执行分支出现不同,那出现分支的线程就会被禁止执行,这也是用 GPU 做并行编程的时候需要参考的一个很重要的因素,保证并行的线程出现线程束分化的情况尽量少。

下面就是线程束分化的例子。

并行归约问题

并行归约是指如何对并行问题进行归约,比如说相邻配对和交错配对。

设备管理

管理 GPU 主要是通过 CUDA API 或者 nvidia-smi 命令来获取。

SM(流式多处理器)

上面讲的是抽象上的分层,但是实际的物理层面承载 GPU 的是 SM。最早的 GPU 架构叫 Fermi,然后是 Kepler,然后才是 Tesla。一般说的 cm_20, sm_20 就指这种计算能力和架构,新款的 GPU 计算能力要更强一点。

内存模型

CUDA 的内存模型和 CPU 是类似的也是多级的结构。线程有局部内存,块之间有全局内存。GPU 和 CPU 都是主要采用 DRAM 来做主存,CPU 的一级缓存会用 SRAM 做。CPU 的多级缓存对用户来说不是很需要考虑,尽量屏蔽了其中的细节,但是 GPU 相对来说会把这种分级结构暴露给用户,这对编程来说也是一种新的挑战。

后面

用 GPU 计算一些计算密集型的程序,速度是真快,比 CPU 块很多,所以这也是为什么大量深度学习的应用都要通过 GPU 加速的原因。

本文通过裸写神经网络的方法,帮助理解神经网络的工作方式,直接在 klab 上查看就可以。

kube-dns 是 kubernetes 基于 DNS 的服务发现模块,主要由三个容器组成,分别是 dnsmasq, kube-dns, sidecar,整体的结构如图。

sidecar

sidecar 是一个监控健康模块,同时向外暴露metrics 记录,但是为啥叫三蹦子不知道。

接受的探测参数是

--probe=<label>,<server>,<dnsname>[,<interval_seconds>][,<type>]

例子如下

--probe=dnsmasq,127.0.0.1:53,kubernetes.default.svc.cluster.local,5,A

等于是每隔 5s 向127.0.0.1:53 进行 DNS 查询 kubernetes.default.svc.cluster.local 的 A 记录

对应的结构体是

dnsmasq

dnsmasq-nanny 是 dnsmasq 的保姆进程,dnsmasq 是一个简易的 DNS server。

dnsmasq-nanny “–” 后面是 dnsmasq 的参数,比如下面这个参数表示的是把 server=/cluster.local/127.0.0.1#10053 当作 dnsmasq 的配置,10053 是 kube-dns 的地址,也就是把 cluster.local 的域名拦截转到 kube-dns 进行解析,剩下的通过正常的域名解析流程。

--server=/cluster.local/127.0.0.1#10053

dnsmasq 简单来说扮演的是集群当中的一个传统 dns server 并且把集群内部的 dns 查询拦截到 kube-dns 当中通过中心化的方法进行 dns 查询,集群的 dns 查询主要依靠 kube-dns。

kube-dns

kube-dns 主要基于 skydns 来实现。

k8s.io/dns/pkg/dns/dns.goKubeDNS.Start 下面有 endpoints 和 services 的 controllers,会把 service 注册到 kube-dns 的 cache 当中 (k8s.io/dns/pkg/dns/treecache),这里有 k8s 域名命名规范

主要的实现方式是 skydns 接受一个后端实现。

KubeDNS.Records KubeDNS.ReverseRecord 基于 TreeCache 实现 DNS 记录存储的后端,从而使得 skydns 提供 DNS 服务。

总结

整体来说 kube-dns 还是一个比较简单的模块,基于 kube-apiserver 的一个控制器,提供中心化的 DNS 查询。

kube-controller-manager 可以认为是一个守护进程用于监视 apiserver 暴露的集群状态,并且不断地尝试把当前状态向集群的目标状态迁移。为了避免频繁查询 apiserver,apiserver 提供了 watch 接口用于监视资源的增加删除和更新,client-go 对此作了抽象,封装一层 informer 来表示本地 apiserver 状态的 cache。这个视频 有一个 Google 工程师讲解的 client-go 的详细内容,这篇 七牛前同事的文章介绍了 informer 的整体结构,写得也很好。

client 当中的 controller

处理事件的 controller 由几部分构成,首先是 Config 当中的可配置部分,下图是 controller 的关系,controller 实现了 Controller 接口。

controller 从 Queue 当中通过 Pop 获取对象交给 Process 回调处理,DeltaFIFO 和 FIFO 是类似的,只是 DeltaFIFO 可以处理删除事件,一般都用 DeltaFIFO。ListerWatcher 就是用客户端构造出来的,针对对应资源的 List Watch 方法的集合,List 用于获取最开始的对象获取,Watch 用于监控之后的变化,所有最开始的时候现有的对象会通过 List 传给 Add 回调,同步了当前状态以后再不断接受新的变化,但是 Watch 本身是有超时机制的,不能永久监听,所以再超时之后还会通过 List 方法,先同步一次再进行删除操作。Resync Period 表示把 cache 中的对象重新入队给回调函数处理,这种情况一般是可能你可能漏掉了更细操作,或者是之前的一些失败了。大部分情况用不到这个选项,可以非常相信 etcd 的功能。

调用路径

1
2
3
4
5
cache.NewListWatchFromClient -> listWatcher
cache.NewIndexer -> store
cache.NewLister(store) -> lister
cache.NewReflector
go refector.Run()

还有一个关键结构是 reflector,reflector 会把对象转化成对应的需要的对象并且 Add 到 Queue 当中去。

Informer 本身的框架是异步的,所以为了做并发控制就引入了 workqueue 的组件,workqueue 有 rate limit 的功能,并且能够合并更新操作。

注意不要修改传入的对象,因为他们要和 cache 一致,如果要写对象的话,需要使用 api.Scheme.Copy 这个函数,进行深度拷贝,所有的 k8s Object 都要支持深拷贝的方法。

kube-controller 当中的插件式 controller

k8s.io/kubernetes/cmd/kube-controller-manager/app/controllermanager.go 中的NewControllerInitializers 函数有大部分 controller 的列表,bootstrapsignertokencleaner 是默认关闭的。

1
2
3
4
var ControllersDisabledByDefault = sets.NewString(
"bootstrapsigner",
"tokencleaner",
)

需要特殊初始化的是 serviceaccount-tok,另外的是 NewControllerInitializers 当中的 controller 了。所有的 controller 初始化函数都要满足如下接口。

1
2
3
4
// InitFunc is used to launch a particular controller.  It may run additional "should I activate checks".
// Any error returned will cause the controller process to `Fatal`
// The bool indicates whether the controller was enabled.
type InitFunc func(ctx ControllerContext) (bool, error)

node controller

接下来看一个具体的 controller,startNodeController

startNodeController 首先解析 ClusterCIDRServiceCIDR 两个子网范围,下面是 NodeController 初始化需要的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
nodeController, err := nodecontroller.NewNodeController(
ctx.InformerFactory.Core().V1().Pods(),
ctx.InformerFactory.Core().V1().Nodes(),
ctx.InformerFactory.Extensions().V1beta1().DaemonSets(),
ctx.Cloud,
ctx.ClientBuilder.ClientOrDie("node-controller"),
ctx.Options.PodEvictionTimeout.Duration,
ctx.Options.NodeEvictionRate,
ctx.Options.SecondaryNodeEvictionRate,
ctx.Options.LargeClusterSizeThreshold,
ctx.Options.UnhealthyZoneThreshold,
ctx.Options.NodeMonitorGracePeriod.Duration,
ctx.Options.NodeStartupGracePeriod.Duration,
ctx.Options.NodeMonitorPeriod.Duration,
clusterCIDR,
serviceCIDR,
int(ctx.Options.NodeCIDRMaskSize),
ctx.Options.AllocateNodeCIDRs,
ipam.CIDRAllocatorType(ctx.Options.CIDRAllocatorType),
ctx.Options.EnableTaintManager,
utilfeature.DefaultFeatureGate.Enabled(features.TaintBasedEvictions),
utilfeature.DefaultFeatureGate.Enabled(features.TaintNodesByCondition),
)

InformerFactory 是用来构造具体 resource 的 informer 的工厂类型,构造了 pods, nodes, daemonsets 的 informer, 说明 node controller 需要 watch 这几种 resource 的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Run starts an asynchronous loop that monitors the status of cluster nodes.
func (nc *Controller) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()

glog.Infof("Starting node controller")
defer glog.Infof("Shutting down node controller")

if !controller.WaitForCacheSync("node", stopCh, nc.nodeInformerSynced, nc.podInformerSynced, nc.daemonSetInformerSynced) {
return
}

// Incorporate the results of node status pushed from kubelet to master.
go wait.Until(func() {
if err := nc.monitorNodeStatus(); err != nil {
glog.Errorf("Error monitoring node status: %v", err)
}
}, nc.nodeMonitorPeriod, wait.NeverStop)

if nc.runTaintManager {
go nc.taintManager.Run(wait.NeverStop)
}

if nc.useTaintBasedEvictions {
// Handling taint based evictions. Because we don't want a dedicated logic in TaintManager for NC-originated
// taints and we normally don't rate limit evictions caused by taints, we need to rate limit adding taints.
go wait.Until(nc.doNoExecuteTaintingPass, scheduler.NodeEvictionPeriod, wait.NeverStop)
} else {
// Managing eviction of nodes:
// When we delete pods off a node, if the node was not empty at the time we then
// queue an eviction watcher. If we hit an error, retry deletion.
go wait.Until(nc.doEvictionPass, scheduler.NodeEvictionPeriod, wait.NeverStop)
}

<-stopCh
}

现在看一下运行的时候是如何做的,首先要调用 controller.WaitForCacheSync 等待 node,pod,daemonSet 的 inofrmer 同步,这是因为在 kube-controller-manager 当中使用的子 controller 使用的 informer 都是共享型的,也就是多个 controller 之间共享一个 informer 的 cache,所以在开始的时候需要保证所有的 sharedInformerFactory 创建的 informers 之间的 cache 先等待一次一致。controller 的读基本上是从 cache 读的,只要写才会打到 etcd 里面,然后等待 cache 的更新回调。

nodeController 主要分成两部分,一部分是 monitorNodeStatus , 它首先从 informer 的 cache 当中 list 新添加的节点和删除的节点,和 newZoneRepresentations。node 是分 zone 的,这在单可用区的 cluster 当中是个空字符串,但是如果 labels 中有 failure-domain.beta.kubernetes.io/zonefailure-domain.beta.kubernetes.io/region 就会构成不同的可用区的划分, 这个适用于仅仅在一家云厂商分不同可用区的时候可以用到。这篇文档 描述了多 zone 的 cluster 的内容,没有 zone 有相应的可用状态,如果某个 zone 变成不可用需要把 pod 从这个 zone 当中剔除,所以 pod 的 failover 是以 zone 为单位的。

处理 node 比较啰嗦,tryUpdateNodeStatus 尝试获取当前的 conditions 更新并且获取 conditions。处理 node 过程主要是标记 node 为不可用的 node,或者把不可用的状态恢复过来。

接下来的就是处理 pod eviction 的部分,另外,这篇文档解释了一些 kubelet 支持的资源耗尽的情况下 kubelet 的剔除策略。

taintManager.Run 处理 taint 的逻辑,这篇文档 解释了 taint 和 toleration 的关系,以及基于 taint 的 eviction 策略。首先看 node 的更新,然后把上面不能 tolerate 的 pod 传给 handlePodUpdate,然后 pod 有更新也会 handlePodUpdate,在 pod 更新的时间中会让 node 抢占一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for {
select {
case <-stopCh:
break
case nodeUpdate := <-tc.nodeUpdateChannel:
tc.handleNodeUpdate(nodeUpdate)
case podUpdate := <-tc.podUpdateChannel:
// If we found a Pod update we need to empty Node queue first.
priority:
for {
select {
case nodeUpdate := <-tc.nodeUpdateChannel:
tc.handleNodeUpdate(nodeUpdate)
default:
break priority
}
}
// After Node queue is emptied we process podUpdate.
tc.handlePodUpdate(podUpdate)
}
}

最后是 eviction 的部分,基于 eviction 的会把 pod 直接删除,基于 taint 只是打上标记,然后通过上面的 tiantManager 剔除。和 pod eviction 不同,taint eviction 是通过限制加 taint 的速率控制 raltelimit 的。

podGCController

pod GC Controller 只 watch pod 一种资源,比较简单。

k8s.io/kubernetes/pkg/controller/podgc/gc_controller.go 下。

gcc.gcOrphaned 删除 node 不存在的 pod,gcc.gcUnscheduledTerminating 删除正在终止,但是没有调度的 pod,gcc.gcTerminated 删除已经被终止的 pod。

其他 controller

其他 controller 也是类似的,从 apiserver 获取状态,并且向对应的状态迁移,这也是为什么 kubernetes 的命令和资源都是宣告式的原因。

kubernetes 概览

以下是 k8s 的整体架构,在 master 节点上主要是 kube-apiserver(整合了 kube-aggregator),还有 kube-scheduler,以及 kube-controller-manager,包括后端存储 etcd。

其中 kube-apiserver 是一个比较关键的部分,而且前期写得坑很多,导致这一部分虽然看起来是一个 API server 其实代码很复杂,特别冗余,而且目前对 kube-apiserver 还要做拆分,能够支持插入第三方的 apiserver,也就是又一个 aggregated apiserver 的 feature,也是和 kube-apiserver 和里面包的一层 genericserver 揉合在一起了,感觉一个大的系统 API server 越写越挫是一个通病,还好现在 k8s 迷途知返正在调整。

kube-apiserver

Kube-apiserver 可以是认为在 generic server 上封装的一层官方默认的 apiserver,有第三方需要的情况下,自己也可以在 generic server 上封装一层加入到集成模式中,这里主要介绍 kube-apiserver 的结构。

restful API

kube-apiserver 是一个 restful 服务,请求直接通过 HTTP 请求发送,例如创建一个 ubuntu 的 pod,用以下的 pod.yaml 文件。

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: ubuntu1
labels:
name: ubuntu1
spec:
containers:
- name: ubuntu1
image: ubuntu
command: ["sleep", "1d"]

执行命令 kubectl create -f ./pod.yaml -v=8,可以看到对应的 POST 请求如下。

1
2
3
4
5
6
7
8
Request Body: {"apiVersion":"v1","kind":"Pod","metadata":{"labels":{"name":"ubuntu1"},"name":"ubuntu1","namespace":"default"},"spec":{"containers":[{"command":["sleep","1d"],"image":"ubuntu","name":"ubuntu1"}],"schedulerName":"default-scheduler"}}
curl -k -v -XPOST -H "Content-Type: application/json" -H "Accept: application/json" -H "User-Agent: kubectl/v1.7.5 (linux/amd64) kubernetes/17d7182" https://localhost:6443/api/v1/namespaces/default/pods
POST https://localhost:6443/api/v1/namespaces/default/pods 201 Created in 6 milliseconds
Response Headers:
Content-Type: application/json
Content-Length: 1208
Date: Wed, 18 Oct 2017 15:04:17 GMT
Response Body: {"kind":"Pod","apiVersion":"v1","metadata":{"name":"ubuntu1","namespace":"default","selfLink":"/api/v1/namespaces/default/pods/ubuntu1","uid":"9c9af581-b415-11e7-8033-024d1ba659e8","resourceVersion":"486154","creationTimestamp":"2017-10-18T15:04:17Z","labels":{"name":"ubuntu1"}},"spec":{"volumes":[{"name":"default-token-p0980","secret":{"secretName":"default-token-p0980","defaultMode":420}}],"containers":[{"name":"ubuntu1","image":"ubuntu","command":["sleep","1d"],"resources":{},"volumeMounts":[{"name":"default-token-p0980","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"Always"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","securityContext":{},"schedulerName":"default-scheduler","tolerations":[{"key":"node.kubernetes.io/not-ready","operator":"Exists","effect":"NoExecute","tolerationSeconds":300},{"key":"node.alpha.kubernetes.io/unreachable","operator":"Exists","effect":"NoExecute","tolerationSeconds":300}]},"status":{"phase":"Pending","qosClass":"BestEffort"}}

从 url path 里面可以看到几个划分,path 的分类大概有下面这几种。

路径上整体分成 group, version, resource, 作为核心 API group 的 core(包括 pod, node 之类的 resource),不带 group,直接接在 /api/ 后面,其他的 api group 则接在 /apis 后面。以 pod 为例,pod 对应的数据类型如下,这个数据结构和 POST 请求中的结构的参数是一致的。

如果是 job 的话则是在,pkg/apis/batch/v2alpha1/types.go,和 API 路径是对应的。例子当中 kubectl 加上 level 大于 8 的 log 就会打印请求和相应的 body,可以看到 request body 和上面的数据结构是一致的。这个请求会发送到 apiserver 进行处理并且返回存储之后的 pod。

重要结构体

Config

父结构,主要的配置内容,其中有一个结构 RESTOptionsGetter genericregistry.RESTOptionsGetter 是和 API 初始化相关的,这个接口的实现是在 k8s.io/apiserver/pkg/server/options/etcd.go 中的 storageFactoryRestOptionsFactory 实现的,对应的实现函数是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func (f *storageFactoryRestOptionsFactory) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
storageConfig, err := f.StorageFactory.NewConfig(resource)
if err != nil {
return generic.RESTOptions{}, fmt.Errorf("unable to find storage destination for %v, due to %v", resource, err.Error())
}

ret := generic.RESTOptions{
StorageConfig: storageConfig,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: f.Options.DeleteCollectionWorkers,
EnableGarbageCollection: f.Options.EnableGarbageCollection,
ResourcePrefix: f.StorageFactory.ResourcePrefix(resource),
}
if f.Options.EnableWatchCache {
sizes, err := ParseWatchCacheSizes(f.Options.WatchCacheSizes)
if err != nil {
return generic.RESTOptions{}, err
}
cacheSize, ok := sizes[resource]
if !ok {
cacheSize = f.Options.DefaultWatchCacheSize
}
ret.Decorator = genericregistry.StorageWithCacher(cacheSize)
}

return ret, nil
}

APIGroupInfo

APIGroupInfo 主要定义了一个 API 组的相关信息,观察一下 APIGroupInfo 是如何初始化的。

k8s.io/pkg/master/master.go 当中,每个 Resource 都要提供自己的 Provider,比如说 storagerest 就在 k8s.io/kubernetes/pkg/registry/storage/rest/storage_storage.go 定义了 NewRESTStorage 方法。而默认的 resource 的 legacy provider 单独处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
if c.ExtraConfig.APIResourceConfigSource.AnyResourcesForVersionEnabled(apiv1.SchemeGroupVersion) {
legacyRESTStorageProvider := corerest.LegacyRESTStorageProvider{
StorageFactory: c.ExtraConfig.StorageFactory,
ProxyTransport: c.ExtraConfig.ProxyTransport,
KubeletClientConfig: c.ExtraConfig.KubeletClientConfig,
EventTTL: c.ExtraConfig.EventTTL,
ServiceIPRange: c.ExtraConfig.ServiceIPRange,
ServiceNodePortRange: c.ExtraConfig.ServiceNodePortRange,
LoopbackClientConfig: c.GenericConfig.LoopbackClientConfig,
}
m.InstallLegacyAPI(&c, c.GenericConfig.RESTOptionsGetter, legacyRESTStorageProvider)
}

然后通过调用 k8s.io/kubernetes/pkg/registry/core/rest.LegacyRESTStorageProviderNewLegacyRESTStorage 来初始化基础对象的 apigroup info,比如初始化 podStorage,serviceStorage 和 nodeStorage 等等。legacy ApiGrouInfo 的 Scheme, ParamaterCodec, NegotiatedSerializer 都是用 "k8s.io/kubernetes/pkg/api" 包下的全局变量初始化的。

1
2
3
Scheme:                      api.Scheme,
ParameterCodec: api.ParameterCodec,
NegotiatedSerializer: api.Codecs,

然后合并成一个 restStorage 存入 apiGroupInfo 中。

1
2
3
4
5
6
7
8
9
10
11
restStorageMap := map[string]rest.Storage{
"pods": podStorage.Pod,
"pods/attach": podStorage.Attach,
"pods/status": podStorage.Status,
"pods/log": podStorage.Log,
"pods/exec": podStorage.Exec,
"pods/portforward": podStorage.PortForward,
"pods/proxy": podStorage.Proxy,
"pods/binding": podStorage.Binding,
"bindings": podStorage.Binding,
...

举个例子 podStorage 就是用的 genericregistry.Store,这是一个通用的 etc 辅助结构,把 etcd 抽象成存储结构。

1
2
3
4
5
// REST implements a RESTStorage for pods
type REST struct {
*genericregistry.Store
proxyTransport http.RoundTripper
}

serialization

pkg/api.Codecs 是全局默认的 codec 来自下面这段代码。

1
2
3
4
func NewCodecFactory(scheme *runtime.Scheme) CodecFactory {
serializers := newSerializersForScheme(scheme, json.DefaultMetaFactory)
return newCodecFactory(scheme, serializers)
}

默认具体定义了这几种 serilizer。

1
2
3
4
5
func newSerializersForScheme(scheme *runtime.Scheme, mf json.MetaFactory) []serializerType {
jsonSerializer := json.NewSerializer(mf, scheme, scheme, false)
jsonPrettySerializer := json.NewSerializer(mf, scheme, scheme, true)
yamlSerializer := json.NewYAMLSerializer(mf, scheme, scheme)
...

而且标准库的 json 有很严重的性能问题,换用了 json-iter 但是有很多标准库不兼容的问题,性能提升了大概 20% 但是没办法和进主线,我尝试在上面工作的了一段时间,改了两个问题还是有错,由于时间关系,暂时放弃了这个工作,相关的 issue 在这里

filters

首先通过 ./staging/src/k8s.io/apiserver/pkg/server/config.go 下的 DefaultBuildHandlerChain 构建 filters。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
handler := genericapifilters.WithAuthorization(apiHandler, c.RequestContextMapper, c.Authorizer, c.Serializer)
handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.MaxMutatingRequestsInFlight, c.RequestContextMapper, c.LongRunningFunc)
handler = genericapifilters.WithImpersonation(handler, c.RequestContextMapper, c.Authorizer, c.Serializer)
if utilfeature.DefaultFeatureGate.Enabled(features.AdvancedAuditing) {
handler = genericapifilters.WithAudit(handler, c.RequestContextMapper, c.AuditBackend, c.AuditPolicyChecker, c.LongRunningFunc)
} else {
handler = genericapifilters.WithLegacyAudit(handler, c.RequestContextMapper, c.LegacyAuditWriter)
}
failedHandler := genericapifilters.Unauthorized(c.RequestContextMapper, c.Serializer, c.SupportsBasicAuth)
if utilfeature.DefaultFeatureGate.Enabled(features.AdvancedAuditing) {
failedHandler = genericapifilters.WithFailedAuthenticationAudit(failedHandler, c.RequestContextMapper, c.AuditBackend, c.AuditPolicyChecker)
}
handler = genericapifilters.WithAuthentication(handler, c.RequestContextMapper, c.Authenticator, failedHandler)
handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true")
handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.RequestContextMapper, c.LongRunningFunc, c.RequestTimeout)
handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver, c.RequestContextMapper)
handler = apirequest.WithRequestContext(handler, c.RequestContextMapper)
handler = genericfilters.WithPanicRecovery(handler)
return handler
}
panic recover

genericfilters.WithPanicRecovery 在 handler 的最外层对出现的 panic 恢复,并且打印每次请求的 log,所以你想观察 API 请求的情况可以 grep wrap.go 就能看到。

request context

apirequest.WithRequestContext 给 request 绑定一个 Context

RequestInfo

跟路 url 提取后续请求需要的 group, version, namespace, verb, resource 等信息。

WithTimeoutForNonLongRunningRequests

限制 API 调用时间,超时处理提前终止 write。

WithCORS

允许跨域访问。

authentication

k8s.io/apiserver/pkg/endpoints/filters/authentication.go 下。WithAuthentication 插入鉴权信息,例如证书鉴权,token 鉴权等,并且从鉴权信息当中获取 user 信息(可能是 service account 也可能是外部用户)user 身份是由 里面的几种方式确认的

authorization

检查是否有权限进行对应资源的操作。一种是 RBAC 一种是 Node。具体这两种方式可以看这个介绍,RBAC 主要是针对服务的,而 Node 模式主要是针对 kubelet 的。

impersonation

让用户伪装成其他用户,比如 admin 可以用普通用户的身份创建资源。

路由

通过 genericapiserver 的 InstallLegacyAPIGroup 就注册到路由当中。具体的做法就是根据 version, resource, sub resource, verb 等信息构造路由,然后用 go-restful 注册处理函数。比如说 GET

1
2
3
4
5
6
7
route := ws.GET(action.Path).To(handler).
Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Operation("read"+namespaced+kind+strings.Title(subresource)+operationSuffix).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
Returns(http.StatusOK, "OK", producedObject).
Writes(producedObject)

handler 里面做的内容就是序列化,然后根据具体的要求(GET DELETE 等)到 etcd 中操作,当然本身还有一层缓存,这取决于 API 的 options 是希望更新还是直接读缓存(缓存会比 etcd 旧一些),比如对于 kubelet 会不断查询 node 信息,但是 kubelet 本身并不需要最新的信息,这个时候就会从缓存中读取。

性能调优

开启代理 kubectl proxy,就可以通过 localhost 直接访问 kube-apiserver HTTP 服务。然后执行 go tool pprof http://localhost:8001/debug/pprof/profile 可以获得 profile 结果,下图红色的部分就是调用耗时最多的部分。

除此之外,kube-apiserver 本身也暴露了很多 prometheus 的 metrics 但是往上现在没有现成的模板,只能根据自己的需求来在 prometheus 当作做 query。可以在 k8s.io/apiserver/pkg/endpoints/metrics/metrics.go 里面看到。

之前也说过,超时间调用时会打 log 的,在代码中保存了一些 trace 日志,可以通过 grep Trace来过滤。Trace[%d] 这样开头, %d 是一个 id 可以看到具体的 trace 信息。

最近因为 k8s 的默认调度器功能太丰富,太“高级”了,一些屌丝特性没有满足,所以前段时间自己魔改了一下满足了一些屌丝特性,暂时叫做乞丐调度器,顺便把默认的调度器代码翻了一下,这里对默认的代码做一下总结。

CreateScheduler

CreateScheduler 会从 policy file 当中获取算法的配置信息。接口k8s.io/kubernetes/plugin/pkg/scheduler.Configurator 定义了构造一个 scheduler 的配置信息。k8s.io/kubernetes/plugin/pkg/scheduler/factor.ConfigFactory 是这个接口的一个实现。c.Create 会把 AlgorithmProvider 配置的 PredicatePriority 的 key 当作参数传给,f.CreateFromKeys 这样主要对应的 key 注册了的话就会有对应的算法绑定到 scheduler 上面。目前有两种 Provider,一种是用默认 predicate 和 默认 priority 的 AlgorithmProvider,另一种是把 LeastRequestedPriority 换成 MostRequestedPriority 的自动伸缩友好的 AlgorithmProvider。

1
2
3
4
5
6
// Registers algorithm providers. By default we use 'DefaultProvider', but user can specify one to be used
// by specifying flag.
factory.RegisterAlgorithmProvider(factory.DefaultProvider, defaultPredicates(), defaultPriorities())
// Cluster autoscaler friendly scheduling algorithm.
factory.RegisterAlgorithmProvider(ClusterAutoscalerProvider, defaultPredicates(),
copyAndReplace(defaultPriorities(), "LeastRequestedPriority", "MostRequestedPriority"))

Scheduler Server

options.ScheduleServer 是服务端对应的配置结构,其中有几个成员。

  1. KubeSchedulerConfiguration 调度器的配置
  2. Master 表示 API server 的地址
  3. Kubeconfig k8s 配置文件的路径

func Run(s *options.SchedulerServer) error 会根据 Sechduler Server 来运行。EventBroadcaster 接受事件,并且把事件发送给事件处理者( EventSink watcher, log),startHTTP 主要是是 profiling 接口,心跳检测接口和 prometheus 的 instrumenting 接口。informerFactory,看起来是一个异步同步信息的 cache,平时调度是直接走 cache,更新的时候才会走 API。最后配置了选主的话会从 Etcd 拿到锁,并且拿到 Master 的锁。

k8s.io/kubernetes/plugin/pkg/scheduler.Scheduler.Run

初始化以后,Run 对应的是一个 0 秒循环的大 loop(相当于每次 loop 等于主动调用一次 Go runtime.Sched()),在每次循环当中都会调用 sched.scheduleOne,首先 NextPod 会同步等待一个 pod 变成 available 的状态,并且跳过正在被删除的 pod,然后调用 sched.schedule 走到具体的调度算法当中,整个过程是串行,没有批量调度 pod 的操作。在进行具体的调度算法之后,会得到一个可行的 node,如果调度失败的话会,并且调度失败的原因是找不到合适的 node 的话,就尝试 sched.preempt,这个的作用就是尝试在替换现有 pod 的情况下能够获得调度机会的策略,那么就抢占已经被调度的 pod,标记目标 pod 的 Annotation 然后踢出权重最低的那个 pod。如果成果获得一个可调度的节点,就通过把本地 cache 先更新到已经调度之后的状态,标记 pod 已经在要调度的 node 上,也就是调用 sched.assume 假设 pod 已经调度到了节点上,再异步的通过 ApiServer 的接口,sched.bind 让 pod 正在运行到 node 上。

sched.schedule

schedule 调用 algorithm/scheduler_interface.go下面定义的调度器的接口 Schedule。对应的实现在 core/generic_scheduler.go 下面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ScheduleAlgorithm is an interface implemented by things that know how to schedule pods
// onto machines.
type ScheduleAlgorithm interface {
Schedule(*v1.Pod, NodeLister) (selectedMachine string, err error)
// Preempt receives scheduling errors for a pod and tries to create room for
// the pod by preempting lower priority pods if possible.
// It returns the node where preemption happened, a list of preempted pods, and error if any.
Preempt(*v1.Pod, NodeLister, error) (selectedNode *v1.Node, preemptedPods []*v1.Pod, err error)
// Predicates() returns a pointer to a map of predicate functions. This is
// exposed for testing.
Predicates() map[string]FitPredicate
// Prioritizers returns a slice of priority config. This is exposed for
// testing.
Prioritizers() []PriorityConfig
}

Schedule 会根据调度算法得到一个合适的节点,而 Preempt 则是尝试抢占一个 pod 以获得调度到节点上的机会。PredicatesPriorities 则是两个重要的部分,Predicates 类似一个过滤器,对节点进行筛选,而 Priorities 则是对筛选出来的节点进行权重的排序,最后得到一个合适的调度节点。

算法工厂

算法工厂就是注册 PreciatePriority 的地方,之前已经说了可以通过 AlgorithmProvider 获得一组 PredicatePriority,比如 DefaultProvider 提供了默认的一套,如果不用 Provider,需要在 policy file 当中另外指定要使用的 PredicatePriority,不过目前好像没有用这种方式,还是通过 Provider 指定了一套要使用的算法 。注册 AlgorithmProvider 是通过factory.RegisterAlgorithmProvider,然后调用 NewGenericScheduler (在 k8s.io/kubernetes/plugin/pkg/scheduler/core/generic_scheduler.go 当中),初始化要用到的 predicates 和 priorities。

调度过程

Schedule 其实很简单,就是通过 findNodesThatFit,先根据 Predicate 过滤出合适的 Node,然后调用 PrioritizeNodes,用 Priorities 对 Node 根据算法的权重进行排序,因为每个 node 要走的流程是一样的并且最终结果相互没有影响,所以这个过程是并发的,这篇文章的图画的很好,示意很明显。

最后会得到一个最理想的节点,再通过 bing 告诉 API server 这个节点被选中了。

sched.preempt

抢占过程是在 pod 没有找到合适的节点情况下,如果能在踢出一个 pod 获得调度机会的情况下进行抢占。抢占算是一个比较新的特性,在 1.8 里面都是默认关掉的,要打开的话需要指定kube-scheduler--feature-gates=PodPriority=true 还有 apiserver 的 --runtime-config=scheduling.k8s.io/v1alpha1=true。可以通过添加 PriorityClass 把 pod 分权重,现在这个特性算是给 pod 也加上的权重。

1
2
3
4
5
6
7
8
apiVersion: v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for XYZ service pods only."

然后可以在spec当中指定 priorityClassName: high-priority,这样这么大的权重,这个 pod 就很难被抢占了,具体流程如下图。

podEligibleToPreemptOthers 主要判断如果 目标 pod 被标记为(通过 pod 的 annotation 标记)已经要抢占其他 pod,并且有一个优先级小于 目标 pod 的 pod 即将被删除 (p.DeletionTimestamp != nil ),就直接退出,因为这个时候这个被删除的 pod 其实在为目标 pod 腾出空间了,在下次调度的时候就会获得调度机会。nodesWherePreemptionMightHelp,类似于 schedule 的时候的 predicate 阶段,只不过多了一步是通过尝试移除 pod 跑一遍 predicates 看看这个节点能不能被通过。 selectNodesForPreemption 则和 priority 的阶段类似,把删除 pod 之后的可以通过的节点进行排序选出一个排名最高的节点。再通过 selectVictimsOnNode 把节点上的 pod 按照节点的 priority 排序选出“受害者”,越高越难被抢占。可以在 spec 里面设置这个值,选出了节点上的受害者以后,通过pickOneNodeForPreemption,主要的依据是拥有最低的最高 pod 权重的节点先被选出来,比如 node1 上 pod 的最高权重是 10,node2 上 pod 的最高权重是 8,那么 node2 被选中,如果有平局的话,最少的“受害者”先选,如果还平局,随机选一个。最后得到一个要被抢占的节点。

自定义调度器的方式

自定义调度器有三种方法。

第一种是通过添加 PredicatePriority 的方式,做微调,这种方式比较简单,只要定义好对应的函数并且通过函数工厂注册就可以。

第二种是使用自定义的调度器,具体的方法可以看官方文档,通过把 pod 的 spec.schedulerName 指向自定义的调度器就可以把调度任务转到自己实现的服务。

第三种是使用 extender,extender 本身和调度器的过程类似,接口是如下定义的,主要是针对一些不算受集群本身控制的资源,需要通过外部调用来进行调度的情况,相关文档在这里

性能测试

目前单机简单的测试条件下,1s 钟可以调度成功 450 左右的 pod,具体的性能参数还要慢慢挖掘。

flannel 是一个中心化的 overlay 容器网络,设计简单,容易理解,对于 k8s 来说,有一个假设:所有容器都可以和集群里任意其他容器或者节点通信,并且通信双方看到的对方的 IP 地址就是实际的地址,主要的好处就是不需要任何的端口映射和地址转换,拥有一张扁平的网络更容易管理,而且由于是基于 Etcd 的中心化的管理,所以对于一些 IP 变化异常频繁的场景来说,比一些去中心化的方案能够较及时同步网络拓扑关系。

IP 地址的管理

flannel 的 IP 地址是通过 Etcd 管理的,在 k8s 初始化的时候指定 pod 大网的网段 --pod-network-cidr=10.244.0.0/16,flanneld 可以直接通过 Etcd 管理,如果启动的时候指定了 --kube-subnet-mgr,可以直接通过 k8s 的 apiserver 来获得一个小网段的租期,通过 kubectl get <NodeName> -o jsonpath='{.spec.podCIDR}' 可以获取对应节点的 CIDR 表示的网段,flannel 是以节点为单元划分小网段的,每个节点上的 pod 在这个例子当中是划分一个 10.244.x.0/24 的网段,所以总共能分配 255 个节点,每个节点上可以分配 253 个 pod。结构如下图所示,每个节点上都会有一个 flanneld 用于管理自己网段的租期。

可以通过在 host 上 cat /run/flannel/subnet.env 查看同步下来的信息,例如:

1
2
3
4
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.0.1/24
FLANNEL_MTU=8951
FLANNEL_IPMASQ=true

说明当前节点分配的网段是 10.244.0.1/24。在每个节点上因为已经确定了网段,用 ipam 就可以管理这一范围 ip 地址的分配,所以本身 pod 的 IP 分配和中心 Etcd 没有太多联系。

基本工作原理

简单来说就是通过建立 VXLAN 隧道,通过 UDP 把 IP 封装一层直接送到对应的节点,实现了一个大的 VLAN。没有使用 IPoIP 或者 GRE 主要是因为一些云厂商比如 AWS 的安全策略只能支持 TCP/UDP/ICMP。

flannel 本身会创建一个类似下面这样配置的 CNI bridge 设备。

1
2
3
4
5
6
7
8
9
10
11
12
{
"name" : "cni0",
"type" : "bridge",
"mtu" : 8973,
"ipMasq" : true,
"isGateway" : true,
"ipam" : {
"type" : "host-local",
"subnet" : "10.244.0.1/24",
"routes" : [ { "dst" : "10.244.0.0/16" } ]
}
}

具体的网络拓扑图如下,所用的网段开头是 10.1,但是划分是一致的,图里面的 docker0 应该是 cni0,flannel0 应该是 flannel.1,这个命名的区别主要是不带点的是 UDP 封装,带点的是 vxlan 封装,图片比较早。

第一步,首先是从容器中(10.1.20.3)出来,走桥接去到 cni0 (10.1.20.1),通过brctl show 可以看到 cni0 接了两个容器的 veth。

第二步,然后根据路由规则,在宿主机上能够用ip route,找到一条走到大网段的路由。10.15.0.0/16 dev flannel.1,到达 flannel.1

第三步,flannel.1 会走 vxlan,这个是在内核实现的,如果用 UDP 封装就是在用户态实现的,用户态实现的等于把包从内核过了两遍,没有直接用 vxlan 封装的直接走内核效率高,所以基本上不会使用 UDP 封装。对应的 vxlan 配置可以通过 bridge fdb flannel.1 看到,没有一条隧道就会有一条这样的转发表。因为到达每个对应网段的信息是在 Etcd 上分配的 flannel.1 只要 watch 然后发现有更改的时候对应配置隧道指向对应容器网段的宿主机 IP 就可以。

1
8a:55:a7:e2:e9:18 dev flannel.1 dst 192.168.0.100 self permanent

第四步,走宿主机的网络到达对端,对端的 vxlan 收到以后会拆开封装,丢到协议栈里面。

第五步,根据路由 ip route,中的一条10.1.15.0/24 dev cni0 proto kernel scope link src 10.1.15.1,送到 cni0 然后再转发给容器 10.1.15.2, 最后就能完成 pod 跨界点的互通了。

优缺点

因为整个的网段分配是存在 Etcd 里面的,节点只要 watch 然后根据网段建隧道就可以,相对来说中心化的系统设计比较简单,而且对于 IP 地址变动能够及时反应,特别是节点和容器都有剧烈变化的时候(别问我为啥物理节点会有剧烈变化,创业公司玩法怎么省钱怎么来……),相比于去中心化的一些设计能够更快同步一些。当然建隧道是一个点对点的规模,也就是如果有 n 个节点建隧道的话,每个节点上都要建 n-1 条隧道。

一种改进方式是使用 host-gw 的后端方式,以及 ipvlan,不过目前 ipvlan 还没有支持,这里有一个各种后端实现的比较,vxlan 表现很最差,host-gw 的做法是不用隧道的方式,而是把路由信息直接写到节点上,直接打平到节点上,等于是节点之间是一个大网,每个节点上的小网段通过路由的方式和大网互通,将到达各个节点的网段的路由刷到节点上,而不是建 vxlan 隧道的方式,比如文中的例子,会有这样的路由。

1
2
10.1.15.0/24 via 192.168.0.100 dev eth0 
10.1.15.0/24 dev cni0 proto kernel scope link src 10.1.20.1

然而,由于 flannel 只能够修改各个主机的路由表,一旦主机直接隔了个其他路由设备,比如三层路由器,这个包就会在路由设备上被丢掉。这样一来,host-gw 的模式就只能用于二层直接可达的网络。

一般来说我们把字典树称作 trie,这是一个用于查找字符串的数据结构,举个简单的例子。下面就是一个把 b,abc,abd,bcd,abcd,efg,hii 这 6 个单词构造成字典树的例子。

在路由表中我们可以把子网看作一个二进制字符串,对于 IPv4 来说,字符串的长度就有 32 位。

以上面这个为例子,有编号 0 到 14 这几个二进制的字符串(字符集和只有 0 和 1),右边是他们的具体字符串,这样的字典树有一点像二叉树。如果把它画出来是这样的。

对这个树做压缩有一个办法就是把单节点作压缩,形成下面这个树,这个就是路径压缩,path compressed trie。

我们需要在压缩的地方进行标记,比如 skip 4 那里把一段 1011 压缩掉了。而 LC-trie 指的是 Level Compressed trie,这个树会把层级进行压缩,因为我们得到的这个字典树实际上是一个稀疏树,高度并不是平衡的,所以为了达到平衡,需要做的一件事情是把高度进行压缩,压缩之后变成下面这个形式,这样整棵树就会更加扁平。

这个树是有两种类型的节点,一种是 leaf,保存了路由具体的信息的叶子结点,一种是 trie node(tnode)保存了中间节点,子节点可能是 tnode 或者 leaf。trie 上有几个数据要存储,一个是 bits,这个表示的是子节点的选择度(这个怎么理解呢,就是我接下来的子节点是八叉了,因为我把原来的树压缩了,所以现在不需要二选一,现在直接来个八选一就可以),对于一个八叉的压缩来说,就要有一个 3 位的数字来存储,也就是 log(8),当然也可以能是 7 叉,但是必须是 2 的指数。而 pos 表示的是从那个 bit 开始,它的作用和 skip 类似只不过不是一个相对值,而是一个累加值,表示我们要从哪开始(从字符串的起始位置开始数)。

我们先看一下搜索的代码,其实很简单,就是不断匹配公共前缀直到直到找到叶子节点( bits == 0)。匹配前缀的方式比较 tricky,用异或进行确认。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* rcu_read_lock needs to be hold by caller from readside */
static struct key_vector *fib_find_node(struct trie *t,
struct key_vector **tp, u32 key)
{
struct key_vector *pn, *n = t->kv;
unsigned long index = 0;

do {
pn = n;
n = get_child_rcu(n, index);

if (!n)
break;

index = get_cindex(key, n);

/* This bit of code is a bit tricky but it combines multiple
* checks into a single check. The prefix consists of the
* prefix plus zeros for the bits in the cindex. The index
* is the difference between the key and this value. From
* this we can actually derive several pieces of data.
* if (index >= (1ul << bits))
* we have a mismatch in skip bits and failed
* else
* we know the value is cindex
*
* This check is safe even if bits == KEYLENGTH due to the
* fact that we can only allocate a node with 32 bits if a
* long is greater than 32 bits.
*/
if (index >= (1ul << n->bits)) {
n = NULL;
break;
}

/* keep searching until we find a perfect match leaf or NULL */
} while (IS_TNODE(n));

*tp = pn;

return n;
}

具体看一下 get_cindex 匹配的方式,#define get_cindex(key, kv) (((key) ^ (kv)->key) >> (kv)->pos),对 tnode 和 被比较的 key 做异或,这个怎么理解呢,看下面的图的例子这是正确匹配以后的结果,灰色代表 0,蓝色代表 1,两个值进行异或的话,首先 pos 会被右移掉,然后 bits 的部分会原样保留,因为 tnode 的这部分都是 0。然后亲址的部分如果完全匹配的话结果就都是 0 ,但是如果不完全匹配的话,结果就会比 index 还要大,因为高位还有 1,所以这就是为什么 index >= (1ul << n->bits 能判断是否匹配的前缀的原因。

然后我们再看一下插入的流程。首先算出匹配到当前节点的子节点(有可能有,有可能没有)。

1
2
3
4
5
6
7
8
9
10
11
12

static int fib_insert_node(struct trie *t, struct key_vector *tp,
struct fib_alias *new, t_key key)
{
struct key_vector *n, *l;

l = leaf_new(key, new);
if (!l)
goto noleaf;

/* retrieve child from parent node */
n = get_child(tp, get_index(key, tp));

如果有子节点,就要创建一个新的 tnode,再把这个 key 给插入。

1
2
3
4
5
6
7
8
/* Case 2: n is a LEAF or a TNODE and the key doesn't match.
*
* Add a new tnode here
* first tnode need some special handling
* leaves us in position for handling as case 3
*/
if (n) {
struct key_vector *tn;

__fls find last set bit,就是找到 pos,然后扩展出有两个选择(2 的 1 次方)的 tnode。

1
2
3
tn = tnode_new(key, __fls(key ^ n->key), 1);
if (!tn)
goto notnode;

设置 tn 的 父节点为 tp,然后把 key 插入到 tn 当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
	/* initialize routes out of node */
NODE_INIT_PARENT(tn, tp);
put_child(tn, get_index(key, tn) ^ 1, n);

/* start adding routes into the node */
put_child_root(tp, key, tn);
node_set_parent(n, tn);

/* parent now has a NULL spot where the leaf can go */
tp = tn;
}

/* Case 3: n is NULL, and will just insert a new leaf */
node_push_suffix(tp, new->fa_slen);
NODE_INIT_PARENT(l, tp);
put_child_root(tp, key, l);

开始进行平衡调整树形。

1
2
3
4
5
6
7
8
	trie_rebalance(t, tp);

return 0;
notnode:
node_free(l);
noleaf:
return -ENOMEM;
}

再看一下树的高度是如何进行调整的,从当前节点一直向上压缩。

1
2
3
4
5
static void trie_rebalance(struct trie *t, struct key_vector *tn)
{
while (!IS_TRIE(tn))
tn = resize(t, tn);
}
1
2
3
4
5
6
#define MAX_WORK 10
static struct key_vector *resize(struct trie *t, struct key_vector *tn)
{
#ifdef CONFIG_IP_FIB_TRIE_STATS
struct trie_use_stats __percpu *stats = t->stats;
#endif

利用 container_of 获取父节点。

1
struct key_vector *tp = node_parent(tn);

获取子节点,初始化 max_work 为 10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned long cindex = get_index(tn->key, tp);
int max_work = MAX_WORK;

pr_debug("In tnode_resize %p inflate_threshold=%d threshold=%d\n",
tn, inflate_threshold, halve_threshold);

/* track the tnode via the pointer from the parent instead of
* doing it ourselves. This way we can let RCU fully do its
* thing without us interfering
*/
BUG_ON(tn != get_child(tp, cindex));

/* Double as long as the resulting node has a number of
* nonempty nodes that are above the threshold.
*/

should_inflate 决定要不要压缩的依据是根据动态压缩算法来的(引用4),直观的来说就是高度超过了一个动态计算的阈值,并且还没压缩超过十次就会继续压缩。这个动态阈值的算法是用非空子节点的数目如果超过压缩之后子节点数目的一半就值得压缩。而 inflate 做的事情就把层级压缩一层,也就是把 children 的 children 按照 bits 的匹配放到 parent 的 new_children 当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	while (should_inflate(tp, tn) && max_work) {
tp = inflate(t, tn);
if (!tp) {
#ifdef CONFIG_IP_FIB_TRIE_STATS
this_cpu_inc(stats->resize_node_skipped);
#endif
break;
}

max_work--;
tn = get_child(tp, cindex);
}

/* update parent in case inflate failed */
tp = node_parent(tn);

/* Return if at least one inflate is run */
if (max_work != MAX_WORK)
return tp;

到这里说明一次调整都没有发生,说明节点很稀疏,也就是把节点分开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	/* Halve as long as the number of empty children in this
* node is above threshold.
*/
while (should_halve(tp, tn) && max_work) {
tp = halve(t, tn);
if (!tp) {
#ifdef CONFIG_IP_FIB_TRIE_STATS
this_cpu_inc(stats->resize_node_skipped);
#endif
break;
}

max_work--;
tn = get_child(tp, cindex);
}

只有一个孩子,可以进行 path compress,没必要再多一个中间节点。

1
2
3
4
5
6
7
	/* Only one child remains */
if (should_collapse(tn))
return collapse(t, tn);

/* update parent in case halve failed */
return node_parent(tn);
}

整个的 LC-trie 的结构大致如此,主要用于路由表中路由规则的快速匹配,是在 3.6 之后引进的,摒弃了之前用哈希来查找路由表的算法。

引用

  1. The-Art-Of-Programming-By-July
  2. Fast address lookup for Internet routers
  3. LC-trie implementation notes
  4. Implementing a dynamic compressed trie