一个 Pod 被创建后, 一直卡在ContainerCreating的状态, 执行describe命令查看该 Pod 详细信息后发现如下 Event

1
2
3
4
5
6
7
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m default-scheduler Successfully assigned 61f983b5-19ca-4b33-8647-6b279ae93812 to k8node3
Normal SuccessfulMountVolume 2m kubelet, k8node3 MountVolume.SetUp succeeded for volume "default-token-7r9jt"
Warning FailedCreatePodSandBox 2m (x12 over 2m) kubelet, k8node3 Failed create pod sandbox: rpc error: code = Unknown desc = failed to start sandbox container for pod "61f983b5-19ca-4b33-8647-6b279ae93812": Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "process_linux.go:286: decoding sync type from init pipe caused \"read parent: connection reset by peer\""
Normal SandboxChanged 2m (x12 over 2m) kubelet, k8node3 Pod sandbox changed, it will be killed and re-created.

以上 Event 信息中, 能解读到的信息极其有限

  • Failed create pod sandbox: Google 提供的 pause 容器启动失败
  • oci runtime error: 运行时接口出的问题, 我的环境中运行时环境为 docker
  • connection reset by peer: 连接被重置
  • Pod sandbox changed, it will be killed and re-created: pause 容器引导的 Pod 环境被改变, 重新创建 Pod 中的 pause 引导

看完上面的报错信息并不能准确定位到问题的根源, 只能大致了解到是因为创建SandBox失败导致的, 接下来查看 kubelet 的日志

1
2
3
4
5
6
7
8
9
10
Oct 31 16:33:57 k8node3 kubelet[1865]: E1031 16:33:57.551282    1865 remote_runtime.go:92] RunPodSandbox from runtime service failed: rpc error: code = Unknown desc = failed to start sandbox container for pod "61f983b5-19ca-4b33-8647-6b279ae93812": Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "process_linux.go:286: decoding sync type from init pipe caused \"read parent: connection reset by peer\""
Oct 31 16:33:57 k8node3 kubelet[1865]: E1031 16:33:57.551415 1865 kuberuntime_sandbox.go:54] CreatePodSandbox for pod "61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)" failed: rpc error: code = Unknown desc = failed to start sandbox container for pod "61f983b5-19ca-4b33-8647-6b279ae93812": Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "process_linux.go:286: decoding sync type from init pipe caused \"read parent: connection reset by peer\""
Oct 31 16:33:57 k8node3 kubelet[1865]: E1031 16:33:57.551459 1865 kuberuntime_manager.go:646] createPodSandbox for pod "61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)" failed: rpc error: code = Unknown desc = failed to start sandbox container for pod "61f983b5-19ca-4b33-8647-6b279ae93812": Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "process_linux.go:286: decoding sync type from init pipe caused \"read parent: connection reset by peer\""
Oct 31 16:33:57 k8node3 kubelet[1865]: E1031 16:33:57.551581 1865 pod_workers.go:186] Error syncing pod 77b2b948-dce4-11e8-afec-b82a72cf3061 ("61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)"), skipping: failed to "CreatePodSandbox" for "61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)" with CreatePodSandboxError: "CreatePodSandbox for pod \"61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)\" failed: rpc error: code = Unknown desc = failed to start sandbox container for pod \"61f983b5-19ca-4b33-8647-6b279ae93812\": Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused \"process_linux.go:286: decoding sync type from init pipe caused \\\"read parent: connection reset by peer\\\"\""
Oct 31 16:33:58 k8node3 kubelet[1865]: E1031 16:33:58.718255 1865 remote_runtime.go:92] RunPodSandbox from runtime service failed: rpc error: code = Unknown desc = failed to start sandbox container for pod "61f983b5-19ca-4b33-8647-6b279ae93812": Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "process_linux.go:286: decoding sync type from init pipe caused \"read parent: connection reset by peer\""
Oct 31 16:33:58 k8node3 kubelet[1865]: E1031 16:33:58.718406 1865 kuberuntime_sandbox.go:54] CreatePodSandbox for pod "61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)" failed: rpc error: code = Unknown desc = failed to start sandbox container for pod "61f983b5-19ca-4b33-8647-6b279ae93812": Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "process_linux.go:286: decoding sync type from init pipe caused \"read parent: connection reset by peer\""
Oct 31 16:33:58 k8node3 kubelet[1865]: E1031 16:33:58.718443 1865 kuberuntime_manager.go:646] createPodSandbox for pod "61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)" failed: rpc error: code = Unknown desc = failed to start sandbox container for pod "61f983b5-19ca-4b33-8647-6b279ae93812": Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "process_linux.go:286: decoding sync type from init pipe caused \"read parent: connection reset by peer\""
Oct 31 16:33:58 k8node3 kubelet[1865]: E1031 16:33:58.718597 1865 pod_workers.go:186] Error syncing pod 77b2b948-dce4-11e8-afec-b82a72cf3061 ("61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)"), skipping: failed to "CreatePodSandbox" for "61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)" with CreatePodSandboxError: "CreatePodSandbox for pod \"61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)\" failed: rpc error: code = Unknown desc = failed to start sandbox container for pod \"61f983b5-19ca-4b33-8647-6b279ae93812\": Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused \"process_linux.go:286: decoding sync type from init pipe caused \\\"read parent: connection reset by peer\\\"\""
Oct 31 16:36:02 k8node3 kubelet[1865]: E1031 16:36:02.114171 1865 kubelet.go:1644] Unable to mount volumes for pod "61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)": timeout expired waiting for volumes to attach or mount for pod "default"/"61f983b5-19ca-4b33-8647-6b279ae93812". list of unmounted volumes=[default-token-7r9jt]. list of unattached volumes=[default-token-7r9jt]; skipping pod
Oct 31 16:36:02 k8node3 kubelet[1865]: E1031 16:36:02.114262 1865 pod_workers.go:186] Error syncing pod 77b2b948-dce4-11e8-afec-b82a72cf3061 ("61f983b5-19ca-4b33-8647-6b279ae93812_default(77b2b948-dce4-11e8-afec-b82a72cf3061)"), skipping: timeout expired waiting for volumes to attach or mount for pod "default"/"61f983b5-19ca-4b33-8647-6b279ae93812". list of unmounted volumes=[default-token-7r9jt]. list of unattached volumes=[default-token-7r9jt]

kubelet 的日志中, 与 describe 出来的信息差不多, tail 的时候更直观的感觉到频繁的Sandbox创建的过程, 既然是 OCI 运行时报错, 只能去 docker 的日志中找找看了

1
2
3
4
5
6
7
8
9
10
Oct 31 16:33:58 k8node3 dockerd[1715]: time="2018-10-31T16:33:58.671146675+08:00" level=error msg="containerd: start container" error="oci runtime error: container_linux.go:247: starting container process caused \"process_linux.go:286: decoding sync type from init pipe caused \\\"read parent: connection reset by peer\\\"\"\n" id=029d9e843eedb822370c285b5abf1f37556461083d3bda2c7af38b3b00695b0f
Oct 31 16:33:58 k8node3 dockerd[1715]: time="2018-10-31T16:33:58.671871096+08:00" level=error msg="Create container failed with error: oci runtime error: container_linux.go:247: starting container process caused \"process_linux.go:286: decoding sync type from init pipe caused \\\"read parent: connection reset by peer\\\"\"\n"
Oct 31 16:33:58 k8node3 dockerd[1715]: time="2018-10-31T16:33:58.717553371+08:00" level=error msg="Handler for POST /v1.27/containers/029d9e843eedb822370c285b5abf1f37556461083d3bda2c7af38b3b00695b0f/start returned error: oci runtime error: container_linux.go:247: starting container process caused \"process_linux.go:286: decoding sync type from init pipe caused \\\"read parent: connection reset by peer\\\"\"\n"
Oct 31 16:34:22 k8node3 dockerd[1715]: time="2018-10-31T16:34:22.759631102+08:00" level=error msg="Handler for POST /v1.27/containers/207f0ffb4b5ecc5f8261af40cd7a2c4c2800a2c30b027c4fb95648f8c1b00274/stop returned error: Container 207f0ffb4b5ecc5f8261af40cd7a2c4c2800a2c30b027c4fb95648f8c1b00274 is already stopped"
Oct 31 16:34:22 k8node3 dockerd[1715]: time="2018-10-31T16:34:22.768603351+08:00" level=error msg="Handler for POST /v1.27/containers/03bf9bfcf4e3f66655b0124d6779ff649b2b00219b83645ca18b4bb08d1cc573/stop returned error: Container 03bf9bfcf4e3f66655b0124d6779ff649b2b00219b83645ca18b4bb08d1cc573 is already stopped"
Oct 31 16:34:22 k8node3 dockerd[1715]: time="2018-10-31T16:34:22.777073508+08:00" level=error msg="Handler for POST /v1.27/containers/7b37f5aee7afe01f209bcdc6b3568b522fb0bbda5cb4b322e10b05ec603f5728/stop returned error: Container 7b37f5aee7afe01f209bcdc6b3568b522fb0bbda5cb4b322e10b05ec603f5728 is already stopped"
Oct 31 16:34:22 k8node3 dockerd[1715]: time="2018-10-31T16:34:22.785774443+08:00" level=error msg="Handler for POST /v1.27/containers/1a01419973e4701b231556d74c619c30e0966889948e810b46567f08475ec431/stop returned error: Container 1a01419973e4701b231556d74c619c30e0966889948e810b46567f08475ec431 is already stopped"
Oct 31 16:34:22 k8node3 dockerd[1715]: time="2018-10-31T16:34:22.794198279+08:00" level=error msg="Handler for POST /v1.27/containers/c3c4049e7b1942395b3cc3a45cf0cc69b34bab6271cb940a70c7d9aed3ba6176/stop returned error: Container c3c4049e7b1942395b3cc3a45cf0cc69b34bab6271cb940a70c7d9aed3ba6176 is already stopped"
Oct 31 16:34:22 k8node3 dockerd[1715]: time="2018-10-31T16:34:22.802698120+08:00" level=error msg="Handler for POST /v1.27/containers/8d2c8a4cd5b43b071a9976251932955937d5b1f0f34dca1482cde4195df4747d/stop returned error: Container 8d2c8a4cd5b43b071a9976251932955937d5b1f0f34dca1482cde4195df4747d is already stopped"
Oct 31 16:34:22 k8node3 dockerd[1715]: time="2018-10-31T16:34:22.811103238+08:00" level=error msg="Handler for POST /v1.27/containers/7fdb697e251cec249c0a17f1fdcc6d76fbec13a60929eb0217c744c181702c1f/stop returned error: Container 7fdb697e251cec249c0a17f1fdcc6d76fbec13a60929eb0217c744c181702c1f is already stopped"

Docker 的日志中, 除了已经看了很多遍的connection reset by peer之外, 还有一些新的发现

  • xxx is already stopped: 看日志, 感觉是向容器接口发送了 POST 请求以 stop 容器, 但是该容器已经被 stop 掉了

Docker 的日志和 kubelet 的日志的共同点就是, kubelet 频繁 recreate Sandbox

执行 docker container ls -a 命令发现存在大量 create 状态的 pause 容器

查看 demesg -T 信息, 发现了大量 oom-killer 的字眼的日志, 初步判断是由于内存溢出, 导致系统主动 kill 进程.

发生这样的情况的概率并不高, 一般情况下有两种类型的 oom kill

  • 由于 pod 内进程超出了 pod 指定 Limit 限制的值, 将导致 oom kill, 此时 pod 退出的 Reason 会显示 OOMKilled
  • 另一种情况是 pod 内的进程给自己设置了可用内存, 比如 jvm 内存限制设置为2G, pod Limit 设置为6G, 此时由于程序的原因导致内存使用超过2G 时, 也会引发 oom kill

这两种内存溢出的 kill 区别是第一种原因直接显示在 pod 的 Event 里; 第二种你在 Event 里找不到, 在宿主机的 dmesg 里面可以找到 invoked oom-killer 的日志

这次的情况看起来像属于第二种情况, 于是赶紧再次 describe pod, 查看 Limit 限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Containers:
61f983b5-19ca-4b33-8647-6b279ae93812:
Container ID:
Image: reg.lvrui.io/public/testpublish:latest
Image ID:
Port: <none>
Host Port: <none>
State: Waiting
Reason: ContainerCreating
Ready: False
Restart Count: 0
Limits:
cpu: 1
memory: 2k
Requests:
cpu: 1
memory: 2k
Environment:
key: value
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-7r9jt (ro)

终于找到了原因, 就是因为对内存的 Limit 导致的. 这里显示内存 Limit 为2k, 实际上是因为在创建资源时, 写的是 2000(不加单位时, 默认单位为 bytes), k8s 自动转成了以小写 k 的单位表示方式, 所以就变成了2k

理论上来说, 按照之前的经验, 此种情况(实际使用内存超过 Limit 内存的情况)应该属于第一种, 会在 Event 里显示的由于 OOMkilled 原因导致 Terminated 状态. 但实际情况却是在 Event 里找不到 oom kill 的日志, 且 pod 状态处于 ContainerCreating 状态.

  • 由于 OOMkilled 处于 Terminated 的状态是因为 pod 已经正常被引导创建后, 导致的内存溢出
  • 由于系统 invoked oom-killer 导致的处于 ContainerCreating 状态的 pod 是因为 pod 还没正常被创建, pod 中的 pause 容器都没有被正常引导就已经被 cgroup 的内存限制而招来杀身之祸

在调试的场景下, 容器在使用中通常需要进入容器并执行一些工具命令, 比如 wget, htop, lsof, rz, sz 等. 本篇文章就讨论如何为镜像定制安装这些工具

传统的工具安装方式

传统的运维中, 安装 Linux 工具直接使用各自发行版的包管理器就可以了, 方便, 快捷, 省心

  • RedHat 系列: yum install lsof
  • Debian 系列: apt update && apt install lsof

当使用物理机或虚拟机时, 使用上面的安装方法没有任何问题, 但是到了容器镜像领域, 按照上面的安装方式就不行了. 试过的童鞋会发现, 一个 centos 的基础镜像大约 200MB 左右, 用以上yum的方式安装完lsof工具之后, 镜像的体积增大到了 300MB 仅仅一个小工具的安装, 就会让容器镜像的体积快速膨胀, 虽然成功的完成了安装, 单这显然不是我们希望看到的结果

1
2
3
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 75835a67d134 9 days ago 200MB

镜像中的工具安装方式

在镜像中安装基础工具, 就不能像在传统虚拟机里那样粗犷操作了, 镜像的体积是个非常敏感的数字, 牵动着打包者的神经, 以下乃安装工具之正道

  • RedHat 系列: yum install lsof && yum clean all
  • Debian 系列: apt update && apt install lsof && apt clean && apt autoclean && rm -fr /var/lib/apt/lists/*

经过善后处理的安装方式, 仍以 centos 安装 lsof 工具为例, 安装完成后, 体积为 223MB 相比之前 300MB 的体积, 可以说已经得到了很大的优化

1
2
3
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
mycentos 0.1 80bb7d931f74 1 minutes ago 223MB

Debian 系列比较特殊, apt clean 操作是为了删除掉 /var/cache/apt/archives/ 下的软件包, 后面还指定删除了 /var/lib/apt/lists/* 此路径下的所有文件, 是为了删除 apt update 操作后更新到此目录的远程服务器提供的软件列表信息

为容器定制安装工具包

经过上面的实践, 在 centos 镜像中安装 lsof 工具的体积成本, 已经成功从 300MB 降到了 223 MB, 是否还有更极致的安装方法呢? 当然有的, 就是直接拷贝二进制文件到镜像中

找到命令文件位置及依赖

找命令位置

启动临时容器, 传统安装 lsof

1
docker container run -it centos:latest /bin/bash

进入到容器后, 正常安装工具包

1
yum -y install lsof

查看该工具包安装的所有文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@7b532cd952dc /]# rpm -qa | grep lsof
lsof-4.87-5.el7.x86_64
[root@7b532cd952dc /]# rpm -ql lsof-4.87-5.el7.x86_64
/usr/sbin/lsof
/usr/share/doc/lsof-4.87
/usr/share/doc/lsof-4.87/00.README.FIRST
/usr/share/doc/lsof-4.87/00.README.FIRST_4.87
/usr/share/doc/lsof-4.87/00CREDITS
/usr/share/doc/lsof-4.87/00DCACHE
/usr/share/doc/lsof-4.87/00DIALECTS
/usr/share/doc/lsof-4.87/00DIST
/usr/share/doc/lsof-4.87/00FAQ
/usr/share/doc/lsof-4.87/00LSOF-L
/usr/share/doc/lsof-4.87/00MANIFEST
/usr/share/doc/lsof-4.87/00PORTING
/usr/share/doc/lsof-4.87/00QUICKSTART
/usr/share/doc/lsof-4.87/00README
/usr/share/doc/lsof-4.87/00TEST
/usr/share/doc/lsof-4.87/00XCONFIG
/usr/share/doc/lsof-4.87/README.lsof_4.87
/usr/share/man/man8/lsof.8.gz

以上文件中, 除了第一个主程序外, 其他的都是文档文件, 由于 centos 基础镜像中并不包含 man 命令, 所以, 默认这些文件都没有实际存在在磁盘中, 我们要拷贝的目标只有第一个主程序

安装完成后, 找到软件包的安装位置(由于没有 which 命令, 我们可以使用 find 命令)

1
2
[root@7b532cd952dc /]# find / -name "lsof"
/usr/sbin/lsof

找依赖

使用 ldd 命令, 找到 lsof 命令依赖的库文件

1
2
3
4
5
6
7
8
[root@7b532cd952dc /]# ldd /usr/sbin/lsof
linux-vdso.so.1 => (0x00007ffca8dd6000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f09b6b7e000)
libc.so.6 => /lib64/libc.so.6 (0x00007f09b67b1000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f09b654f000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f09b634b000)
/lib64/ld-linux-x86-64.so.2 (0x00007f09b6da5000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f09b612f000)
  • 第一列:程序需要依赖什么库
  • 第二列: 系统提供的与程序需要的库所对应的库
  • 第三列:库加载的开始地址

通过上面的信息,我们可以得到以下几个信息:

  • 通过对比第一列和第二列,我们可以分析程序需要依赖的库和系统实际提供的,是否相匹配
  • 通过观察第三列,我们可以知道在当前的库中的符号在对应的进程的地址空间中的开始位置

在本文的场景中, 我们需要关心的就是第一列和第二列, 我们需要记下第一列的所有库文件, 然后到初始的 centos 镜像中去挨个儿查找

哪些库文件需要查找呢, 着重找那些 第一列 => 第二列 的库文件在新镜像中是否存在, 因为出现了=>标志, 就意味着当前容器提供了这个文件给程序使用

拿上面的例子来说, 着重查找以下文件:

  • libselinux.so.1 => /lib64/libselinux.so.1
  • libc.so.6 => /lib64/libc.so.6
  • libpcre.so.1 => /lib64/libpcre.so.1
  • libdl.so.2 => /lib64/libdl.so.2
  • /lib64/ld-linux-x86-64.so.2
  • libpthread.so.0 => /lib64/libpthread.so.0

如果你嫌多的话, 还可以执行以下命令, 查找该程序没有使用到的直接依赖文件

1
2
3
[root@7b532cd952dc /]# ldd -u /usr/sbin/lsof
Unused direct dependencies:
/lib64/libselinux.so.1

也就是说, 在上面的列表中, 可以去掉检查 /lib64/libselinux.so.1 该库文件

检查依赖

启动一个新的原始镜像进行上面检查列表的查找

1
2
3
4
5
6
docker container run -it centos:latest /bin/bash                                                                          
[root@099f82deeb3d /]#
[root@099f82deeb3d /]# find / -name "libc.so.6"
/usr/lib64/libc.so.6

# ...... 按照上面的列表文件, 挨个儿查找

确保原始镜像中的对应列表文件都已经存在, 如果有部分库文件不存在, 需要在后面的操作中, 和 lsof 主程序文件一同拷贝新的镜像中

拷贝程序及依赖库

我们将第一启动的容器, 使用 yum 安装了 lsof 命令的容器称之为 A 容器; 将第二次启动的原始容器, 检查了依赖文件的容器称之为 B 容器

将 A 容器的主程序文件以及在 B 容器中缺失的库文件一同拷贝出来(操作对象: A 容器)

1
2
3
4
5
6
# 找到 A 容器的 containerID: 7b532cd952dc
# 我们假设在 B 容器中检查文件列表时, 同时缺失了 `libc.so.6` 库文件 (注意: 是假设!!!)

# 将主文件和缺失的库文件从容器中拷贝到本地目录
docker container cp 7b532cd952dc:/usr/sbin/lsof ./
docker container cp 7b532cd952dc:/usr/lib64/libc.so.6 ./

然后制作新的镜像, 以支持使用 lsof 命令

新的 dockerfile 内容如下:

1
2
3
4
FROM centos:latest

COPY lsof /usr/sbin/
COPY libc.so.6 /usr/lib64/

注意: 由于上面👆我是假设的缺失了libc.so.6文件, 所以在 dockerfile 里加入了这个文件的拷贝, 实际上这个文件在 centos 基础镜像是存在的, 而且是以软连接的形式存在的, 当我们真的COPY libc.so.6 /usr/lib64/这么操作时, 反而会报错. 一定注意: 此处只是演示缺失库文件的处理情况, 库文件已经存在的话, 最好不要动原配!!!

然后我们执行编译新镜像的命令

1
docker build -t mycentos:0.2 ./

进入到新的容器验证 lsof 是否可以正常使用

1
2
3
4
5
6
7
8
9
10
docker container run -it mycentos:0.2 /bin/bash                                                                           
[root@032d75e40f56 /]# lsof
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 1 root cwd DIR 0,133 4096 47026 /
bash 1 root rtd DIR 0,133 4096 47026 /
bash 1 root txt REG 8,1 964544 917638 /usr/bin/bash
bash 1 root mem REG 8,1 62184 919647 /usr/lib64/libnss_files-2.17.so
bash 1 root mem REG 8,1 2173512 919510 /usr/lib64/libc-2.17.so
bash 1 root mem REG 8,1 19776 919537 /usr/lib64/libdl-2.17.so
......

支持, 关于为容器镜像定制的Linux工具命令的工作就已经全部完成了, 我们再看下定制版的镜像体积大小

1
2
3
docker image ls                                                                                                           
REPOSITORY TAG IMAGE ID CREATED SIZE
mycentos 0.2 805d98b565c8 7 minutes ago 201MB

精简到了只有 201MB 与原始的镜像相比, 只增加了 1MB


参考文档:

查看开机启动项

1
systemctl list-unit-files

查看某一服务是否开启启动

1
systemctl is-enabled kubelet.service

删除 deployment 资源后, 某些 pod 资源的状态一直显示 Terminating, 进入到该 pod 所在的宿主机, 查看 kubelet 日志:

1
journalctl -u kubelet -f

会发现有删除某些目录/文件 device or resource busy 的错误出现, 说明有其他的进程在挂载容器内使用的目录

使用如下脚本, 检查目录挂载情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@k8s-node ~]# cat leak.sh 
#!/bin/bash
declare -A map
for i in `find /proc/*/mounts -exec grep $1 {} + 2>/dev/null | awk '{print $1"#"$2}'`
do
pid=`echo $i | awk -F "[/]" '{print $3}'`
point=`echo $i | awk -F "[#]" '{print $2}'`
mnt=`ls -l /proc/$pid/ns/mnt |awk '{print $11}'`
map["$mnt"]="exist"
cmd=`cat /proc/$pid/cmdline`
echo -e "$pid\t$mnt\t$cmd\t$point"
done

for i in `ps aux|grep docker-containerd-shim |grep -v "grep" |awk '{print $2}'`
do
mnt=`ls -l /proc/$i/ns/mnt 2>/dev/null | awk '{print $11}'`
if [[ "${map[$mnt]}" == "exist" ]];then
echo $mnt
fi
done

执行脚本, 后跟device or resource busy的目录绝对路径

1
sh leak.sh /var/lib/kubelet/pods/81791176-a505-11e7-accf-5254fe5a9007/volumes/kubernetes.io~secret/default-token-pzyxh

执行后, 可以看到该目录被哪些进程所挂载

1
8392    mnt:[4026532536]    /bin/bash/start.sh--logtostderr -v=2    /var/lib/kubelet/pods/81791176-a505-11e7-accf-5254fe5a9007/volumes/kubernetes.io~secret/default-token-pzyxh

一级一级往上追踪父进程

1
2
3
4
5
[root@k8s-node ~]# ps -ef | grep 8392
root 8392 8345 0 9月30 ? 00:00:00 /bin/bash /start.sh --logtostderr -v=2
root 8420 8392 0 9月30 ? 00:17:19 /usr/bin/python /usr/bin/supervisord -c supervisord.conf
root 13757 7126 0 13:35 pts/2 00:00:00 grep --color=auto 8392
[root@k8s-node ~]#

我遇到的情况是另一个专门用于收集日志的容器, 挂载了这个容器中的数据目录, 导致删除该容器时, 报device or resource busy的错误

这种情况下, 你可以是用kubectl execdocker exec进入到容器中, 将该目录 umount 掉, 过一会儿, 该 pod 即可被 Kubernetes 正确删除


参考文档:

验证证书是否是某 ca 机构颁发

1
2
root@k8s-master:/var/lib/kubelet/pki# openssl verify -CAfile /etc/kubernetes/pki/ca.crt kubelet-client-current.pem
kubelet-client-current.pem: OK

kubelet-client-current.pem 证书是 /etc/kubernetes/pki/ca.crt 该 ca 机构颁发

查看证书详情

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
root@k8s-master:~/ssl/admin# openssl x509 -in /etc/kubernetes/pki/front-proxy-client.crt -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 8005382448281716991 (0x6f18d4ec20fa88ff)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=kubernetes
Validity
Not Before: Sep 11 15:15:40 2018 GMT
Not After : Sep 11 15:15:40 2019 GMT
Subject: CN=front-proxy-client
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:c5:b4:21:b9:ad:4e:19:25:e2:12:78:5b:e3:75:
3f:41:70:8d:05:f6:30:7f:72:8d:e7:cb:8b:82:de:
be:be:fb:5b:57:fd:4f:7d:12:b7:e3:a8:d3:05:02:
59:83:95:dd:14:19:c9:13:cf:96:80:1e:f0:70:39:
30:4c:b6:a4:5b:47:83:14:04:fd:b7:b9:6d:4b:3d:
a0:f1:e3:39:68:5d:b4:7d:af:3e:99:aa:43:30:d9:
e9:45:87:47:c2:4f:61:81:0b:d3:f7:83:bb:98:5c:
8e:e5:97:16:8e:23:a3:03:28:19:58:ee:3a:6b:de:
ad:bf:54:42:90:80:4f:8f:28:76:bc:49:8d:35:d1:
2e:e5:37:8a:aa:d2:be:ec:be:12:d6:b8:88:0b:85:
88:1b:7d:1e:23:37:25:2d:c0:ea:d6:4e:6b:5f:81:
93:26:6e:be:69:a1:67:e5:75:9a:85:db:a4:56:30:
54:13:21:c5:41:46:9a:fd:c9:41:11:13:b9:b8:77:
ba:74:26:85:5c:e2:78:b6:61:3b:5f:3c:bc:1a:00:
3a:e7:e7:2d:a7:3a:04:17:46:86:66:c3:6c:25:3a:
f5:b5:fa:d6:28:cc:7c:6e:08:47:2f:0e:08:f2:8a:
46:33:e1:00:d7:9a:ce:e8:fe:80:e9:80:c4:58:53:
c4:f1
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Client Authentication
Signature Algorithm: sha256WithRSAEncryption
41:0b:22:92:b5:33:4a:57:76:2f:69:51:12:d1:64:46:4d:f9:
72:1d:bb:32:2a:f4:c1:e4:58:ae:a3:53:be:8b:b3:da:6b:10:
ee:4a:15:4d:ad:71:27:6b:d5:dc:ff:12:56:a8:b7:8e:fd:b3:
ae:90:07:26:6b:e3:16:11:c7:56:79:db:04:f8:09:ca:c5:19:
c9:0c:54:8e:84:d0:09:e2:34:ed:9b:9c:b4:e8:6a:ae:6a:d1:
b2:b6:51:22:1c:c0:67:da:9d:1b:06:da:b7:cf:42:2a:98:38:
d0:48:93:4d:ef:71:73:60:6c:09:19:1d:d9:4e:f1:2d:d2:d4:
a5:e9:2c:a9:5a:b1:39:54:27:07:09:81:29:54:f8:37:20:db:
70:8e:7d:16:23:4f:5b:21:a3:02:df:a6:a9:87:f6:a0:10:4a:
43:73:97:4d:14:b7:4b:06:00:1c:c6:a8:45:d8:97:df:0e:1e:
70:87:98:ee:08:1f:31:e7:cf:6d:9d:06:05:47:8f:6a:e6:10:
c8:d7:0a:d3:7a:36:42:a5:05:ef:ba:bb:26:58:aa:ac:fc:8d:
e4:11:45:12:ef:49:b9:fb:7a:f1:74:c7:41:d5:2f:1b:c2:15:
7d:87:e1:28:7d:05:e5:59:dc:1d:16:0f:e5:ac:a0:07:e4:f0:
cf:21:42:a4

kubelet 与 kube-apiserver 之间的通信是双向的, kubelet 既需要访问 kube-apiserver 获取分配到自己节点上的 pod 信息, kube-apiserver 也需要主动访问 kubelet 拉取日志, 状态, 监控数据等信息, 所以对两个组件来说, 认证是双向的, kube-apiserver 需要持有 kubelet 的客户端证书, 以完成 kubelet 对自己身份的校验; kubelet 也需要持有 kube-apiserver 的客户端证书, 完成 kube-apiserver 对自己身份的认证.

Kubelet 的认证

默认情况下, 对 kubelet 的 https 请求, 如果没有被配置的其他身份验证拒绝的话, 则被视为匿名请求, 并为这个请求赋予system:anonymous用户名和system:unauthenticated用户组

禁用匿名访问

如需要禁用匿名访问, 可以在启动 kubelet Daemon 时加入--anonymous-auth=false配置, 当有匿名访问时, 将回复401 Unauthorized响应未认证的请求

开启 X509 认证

  • kubelet 启动时添加--client-ca-file参数, 并指定签发客户端证书的 ca 根证书所在路径
  • kube-apiserver 启动时添加--kubelet-client-certificate--kubelet-client-key参数, 并分别为其指定由 kubelet ca 根证书签发的客户端证书和秘钥

Kubelet 的授权

任何被成功认证的请求(包括匿名请求)都将被授权. 默认的授权模式为AlwaysAllow, 即允许所有类型的请求

细分对 kubelet API 访问权限的原因

  • 匿名访问启用时, 应限制其调用 kubelet API 的能力
  • 客户端证书身份认证启用时, 只允许配置 CA 签名的客户端证书使用 kubelet API

细分权限

  • 确保authorization.k8s.io/v1beta1该 API Group 在 kube-apiserver 中是被启动的状态
  • 在 kubelet Daemon 启动参数中, 确保配置了--authorization-mode=Webhook--kubeconfig两个参数

kubelet 在接收到每个请求后, 会向指定的 kube-apiserver 发起 SubjectAccessReview API 的请求, 来确定该请求是否被允许


参考文档

如果你使用过kubeadm部署过Kubernetes的环境, master主机节点上就一定会在相应的目录创建了一大批证书文件, 本篇文章就来说说kubeadm到底为我们生成了哪些证书

在Kubernetes的部署中, 创建证书, 配置证书是一道绕不过去坎儿, 好在有kubeadm这样的自动化工具, 帮我们去生成, 配置这些证书. 对于只是想体验Kubernetes或只是想测试的亲来说, 这已经够了, 但是作为Kubernetes的集群维护者来说, kubeadm更像是一个黑盒, 本篇文章就来说说黑盒中关于证书的事儿~

使用kubeadm创建完Kubernetes集群后, 默认会在/etc/kubernetes/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
root@k8s-master:/etc/kubernetes/pki# tree
.
|-- apiserver.crt
|-- apiserver-etcd-client.crt
|-- apiserver-etcd-client.key
|-- apiserver.key
|-- apiserver-kubelet-client.crt
|-- apiserver-kubelet-client.key
|-- ca.crt
|-- ca.key
|-- etcd
| |-- ca.crt
| |-- ca.key
| |-- healthcheck-client.crt
| |-- healthcheck-client.key
| |-- peer.crt
| |-- peer.key
| |-- server.crt
| `-- server.key
|-- front-proxy-ca.crt
|-- front-proxy-ca.key
|-- front-proxy-client.crt
|-- front-proxy-client.key
|-- sa.key
`-- sa.pub

1 directory, 22 files

以上22个文件就是kubeadm为我们创建的所有证书相关的文件, 下面我们来一一解析

证书分组

Kubernetes把证书放在了两个文件夹中

  • /etc/kubernetes/pki
  • /etc/kubernetes/pki/etcd

我们再将这22个文件按照更细的粒度去分组

Kubernetes 集群根证书

Kubernetes 集群根证书CA(Kubernetes集群组件的证书签发机构)

  • /etc/kubernetes/pki/ca.crt
  • /etc/kubernetes/pki/ca.key

以上这组证书为签发其他Kubernetes组件证书使用的根证书, 可以认为是Kubernetes集群中证书签发机构之一

由此根证书签发的证书有:

  1. kube-apiserver 组件持有的服务端证书

    • /etc/kubernetes/pki/apiserver.crt
    • /etc/kubernetes/pki/apiserver.key
  2. kubelet 组件持有的客户端证书, 用作 kube-apiserver 主动向 kubelet 发起请求时的客户端认证

    • /etc/kubernetes/pki/apiserver-kubelet-client.crt
    • /etc/kubernetes/pki/apiserver-kubelet-client.key

注意: Kubernetes集群组件之间的交互是双向的, kubelet 既需要主动访问 kube-apiserver, kube-apiserver 也需要主动向 kubelet 发起请求, 所以双方都需要有自己的根证书以及使用该根证书签发的服务端证书和客户端证书. 在 kube-apiserver 中, 一般明确指定用于 https 访问的服务端证书和带有CN 用户名信息的客户端证书. 而在 kubelet 的启动配置中, 一般只指定了 ca 根证书, 而没有明确指定用于 https 访问的服务端证书, 这是因为, 在生成服务端证书时, 一般会指定服务端地址或主机名, kube-apiserver 相对变化不是很频繁, 所以在创建集群之初就可以预先分配好用作 kube-apiserver 的 IP 或主机名/域名, 但是由于部署在 node 节点上的 kubelet 会因为集群规模的变化而频繁变化, 而无法预知 node 的所有 IP 信息, 所以 kubelet 上一般不会明确指定服务端证书, 而是只指定 ca 根证书, 让 kubelet 根据本地主机信息自动生成服务端证书并保存到配置的cert-dir文件夹中.

好了, 至此, Kubernetes集群根证书所签发的证书都在上面了, 算上根证书一共涉及到6个文件, 22-6=16, 我们还剩下16个文件

汇聚层证书

kube-apiserver 的另一种访问方式就是使用 kubectl proxy 来代理访问, 而该证书就是用来支持SSL代理访问的. 在该种访问模式下, 我们是以http的方式发起请求到代理服务的, 此时, 代理服务会将该请求发送给 kube-apiserver, 在此之前, 代理会将发送给 kube-apiserver 的请求头里加入证书信息, 以下两个配置

API Aggregation允许在不修改Kubernetes核心代码的同时扩展Kubernetes API. 开启 API Aggregation 需要在 kube-apiserver 中添加如下配置:

1
2
3
4
5
6
7
--requestheader-client-ca-file=<path to aggregator CA cert>
--requestheader-allowed-names=front-proxy-client
--requestheader-extra-headers-prefix=X-Remote-Extra-
--requestheader-group-headers=X-Remote-Group
--requestheader-username-headers=X-Remote-User
--proxy-client-cert-file=<path to aggregator proxy cert>
--proxy-client-key-file=<path to aggregator proxy key>

官方警告: 除非你了解保护 CA 使用的风险和机制, 否则不要在不通上下文中重用已经使用过的 CA

如果 kube-proxy 没有和 API server 运行在同一台主机上,那么需要确保启用了如下 apiserver 标记:

--enable-aggregator-routing=true

1
2
客户端 ---发起请求---> 代理 ---Add Header:发起请求---> kube-apiserver
(客户端证书) (服务端证书)

kube-apiserver 代理根证书(客户端证书)

用在requestheader-client-ca-file配置选项中, kube-apiserver 使用该证书来验证客户端证书是否为自己所签发

  • /etc/kubernetes/pki/front-proxy-ca.crt
  • /etc/kubernetes/pki/front-proxy-ca.key

由此根证书签发的证书只有一组:

代理层(如汇聚层aggregator)使用此套代理证书来向 kube-apiserver 请求认证

  1. 代理端使用的客户端证书, 用作代用户与 kube-apiserver 认证
    • /etc/kubernetes/pki/front-proxy-client.crt
    • /etc/kubernetes/pki/front-proxy-client.key

参考文档:

至此, 刨除代理专用的证书外, 还剩下 16-4=12 个文件

etcd 集群根证书

etcd集群所用到的证书都保存在/etc/kubernetes/pki/etcd这路径下, 很明显, 这一套证书是用来专门给etcd集群服务使用的, 设计以下证书文件

etcd 集群根证书CA(etcd 所用到的所有证书的签发机构)

  • /etc/kubernetes/pki/etcd/ca.crt
  • /etc/kubernetes/pki/etcd/ca.key

由此根证书签发机构签发的证书有:

  1. etcd server 持有的服务端证书

    • /etc/kubernetes/pki/etcd/server.crt
    • /etc/kubernetes/pki/etcd/server.key
  2. peer 集群中节点互相通信使用的客户端证书

    • /etc/kubernetes/pki/etcd/peer.crt

    • /etc/kubernetes/pki/etcd/peer.key

      注: Peer:对同一个etcd集群中另外一个Member的称呼

  3. pod 中定义 Liveness 探针使用的客户端证书

    kubeadm 部署的 Kubernetes 集群是以 pod 的方式运行 etcd 服务的, 在该 pod 的定义中, 配置了 Liveness 探活探针

    • /etc/kubernetes/pki/etcd/healthcheck-client.crt

    • /etc/kubernetes/pki/etcd/healthcheck-client.key

      当你 describe etcd 的 pod 时, 会看到如下一行配置:

      1
      Liveness:       exec [/bin/sh -ec ETCDCTL_API=3 etcdctl --endpoints=https://[127.0.0.1]:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt --key=/etc/kubernetes/pki/etcd/healthcheck-client.key get foo] delay=15s timeout=15s period=10s #success=1 #failure=8
  4. 配置在 kube-apiserver 中用来与 etcd server 做双向认证的客户端证书

    • /etc/kubernetes/pki/apiserver-etcd-client.crt
    • /etc/kubernetes/pki/apiserver-etcd-client.key

至此, 介绍了涉及到 etcd 服务的10个证书文件, 12-10=2, 仅剩两个没有介绍到的文件啦, 胜利✌️在望, 坚持一下~

Serveice Account秘钥

最后介绍的这组”证书”其实不是证书, 而是一组秘钥. 看着后缀名是不是有点眼熟呢, 没错, 这组秘钥对儿其实跟我们在Linux上创建, 用于免密登录的密钥对儿原理是一样的~

这组的密钥对儿仅提供给 kube-controller-manager 使用. kube-controller-manager 通过 sa.key 对 token 进行签名, master 节点通过公钥 sa.pub 进行签名的验证

  • /etc/kubernetes/pki/sa.key
  • /etc/kubernetes/pki/sa.pub

至此, kubeadm 工具帮我们创建的所有证书文件都已经介绍完了, 整个 Kubernetes&etcd 集群中所涉及到的绝大部分证书都差不多在这里了. 有的行家可能会看出来, 至少还少了一组证书呀, 就是 kube-proxy 持有的证书怎么没有自动生成呀. 因为 kubeadm 创建的集群, kube-proxy 是以 pod 形式运行的, 在 pod 中, 直接使用 service account 与 kube-apiserver 进行认证, 此时就不需要再单独为 kube-proxy 创建证书了. 如果你的 kube-proxy 是以守护进程的方式直接运行在宿主机的, 那么你就需要为它创建一套证书了. 创建的方式也很简单, 直接使用上面第一条提到的 Kubernetes 集群根证书 进行签发就可以了(注意CN和O的设置)

下一篇文章中, 将实际操作创建所有证书的流程(含kube-proxy证书), 并逐一解释参数的使用


参考文档:

#HTTP发展历史

HTTP 0.9

  • 只有一个GET方法
  • 没有header等描述数据信息
  • 服务器发送完毕, 就关闭TCP连接

HTTP 1.0

  • 增加了很多方法, 例如POST, PUT, DELETE等
  • 增加了status code和header
  • 增加了多字符集的支持, 多部分发送, 权限, 缓存等

HTTP 1.1

  • 持久连接/长连接
  • Pipeline 客户端可以在同一个连接中发送多个请求, 服务端按顺序返回(串行)
  • 增加host头, 有了host之后可以实现一台web服务可以接收/处理多个域名的请求
  • 增加了其他更多的方法

HTTP 2.0

  • 2.0所有的数据以二进制进行传输(帧), 在1.1大部分是以字符串的形式传输
  • 得益于基于帧的传输, 可以实现在同一个连接里面发送多个请求不再需要按顺序来返回(并行)
  • 头信息压缩, 1.1的header头以字符串的形式进行传输, 占用了大量带宽
  • 推送, 1.1之前只支持客户端主动向服务端发起请求, 服务端被动向客户端响应请求; 2.0之后服务可以主动向客户端发送数据(例子🌰: 客户端在发起请求访问服务端页面时, 先请求html页面, 当客户端收到服务端发来的html数据之后, 客户端浏览器进行解析并渲染, 在其中, 发现了引用到了其他文件, 如css, js文件, 均以url链接形式引用, 此时浏览器会按顺序再次对新的资源发起请求; 2.0中, 此场景下, 客户端在请求了html页面后, 服务端可以实现主动将css, js数据推送到客户端, 从而解决了1.1串行请求资源的性能低下问题. 并且服务端主动推送资源到客户端, 也可以解决客户端因各种原因, 请求不到指定资源的情况)

2.0 以提高效率为主要目标对1.1进行了大量改进

经典五层模型

1
2
3
4
5
6
7
8
9
应用层  —> HTTP/FTP
🔽
传输层 —> TCP/UDP
🔽
网络层: 数据在节点之间传输创建逻辑链路
🔽
数据链路层: 在通信的实体间建立数据链路连接(0101)
🔽
物理层: 定义物理设备如何传输数据

传输层

向用户提供可靠的端到端的服务, 传输层向高层屏蔽了下层数据通信的细节

应用层

为应用软件提供服务, 构建在传输层协议之上, 屏蔽网络通信细节

HTTP(TCP)的三次握手

三次握手是TCP连接中的概念, HTTP不存在连接的概念, HTTP只有请求和响应的概念

每个HTTP的请求与响应都是需要基于一个TCP的连接的

User http requests ———— TCP connection ———— Server http response

在HTTP 1.0 版本中, 客户端发起一个HTTP请求, 此时, 会创建一条TCP连接通道, 在服务端处理完毕, 响应给客户端之后, 这条TCP连接就会被断掉

在HTTP 1.1版本中, 通过声明特殊的属性, 可以实现一个TCP连接上, 可以发送多个HTTP请求(长连接/Keepalive)

在HTTP 2.0 版本中, 可以实现, 同一个客户端请求同一个服务端, 只需要一个TCP连接即可, 因为2.0版本支持并行发送请求, 和并行接收请求, 不需要再额外创建更多的TCP连接

三次握手

1
2
3
4
client                                                 server
———————SYN=1, Seq=x—————— >
<——SYN=1, ACK=x+1, Seq=Y——
———————ACK=Y+1, Seq=Z————>

go语言只支持封装, 不支持继承和多态. Java是典型的支持面向对象的语言之一, 他本身支持面向对象的三大特征, 封装继承和多态; 到了Python中, Python显式的支持封装和继承, Python多态的特性大家争论不休, 虽然Python没有显式的多态语法, 但是由于Python这种动态语言的设计, 他本身就是多态的; 到了Go这里, 却只支持封装了, 与其说Go语言是一种面向对象的语言, 倒不如说他是一种面向接口的语言

go语言没有class, 只有struct, Go语言中也没有构造函数的说法

定义结构体

在结构体中, 可以为不同的key定义不同的数据类型, 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合

结构体更类似于在传统的分层开发领域中的module层(实体层), 一般情况下, 实体层中的每个成员属性都对应了数据库中数据表的一个字段, 实力层存在的意义在于以对象的形式进行传值. 拿三层开发来说, 一般分为表示层/接口层, 业务逻辑层, 数据访问层. 每层与每层之间的数据流转正是通过引入的实体层, 实例化实体层的对象来实现的. 结构体之于Go, 类似于实体层中的类之于Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
ee := employee{id:234, name:"sam", age:45, department:"daas", city:"BeiJing"}
fmt.Println(e)
fmt.Println(ee)
}

执行结果:

1
2
{123 larry 28 paas TangShan}
{234 sam 45 daas BeiJing}

访问结构体成员

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
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

func main() {
var e1 employee // 声明e1变量为employee类型
var e2 employee // 声明e2变量为employee类型

// 为e1对象赋值
e1.id = 123
e1.name = "larry"
e1.age = 28
e1.department = "paas"
e1.city = "TangShan"

// 为e2对象赋值
e2.id = 234
e2.name = "sam"
e2.age = 45
e2.department = "daas"
e2.city = "BeiJing"

// 访问结构体对象的成员
fmt.Println(e1.name, e1.age)
fmt.Println(e2.name, e2.age)
}

执行结果:

1
2
larry 28
sam 45

结构体作为函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

func printEmpInfo(emp employee) {
fmt.Println(emp.name, emp.city)
}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
ee := employee{id:234, name:"sam", age:45, department:"daas", city:"BeiJing"}

printEmpInfo(e)
printEmpInfo(ee)
}

结构体用作函数参数的时候, 最像三层开发中的实体层用法

结构体指针

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
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}

var e1 *employee

e1 = &e

fmt.Println(e1.age, e.age)

e1.age = 29

fmt.Println(e1.age, e.age) // 由于e1对象拿到的是e对象的指针, 所以对任意一个变量赋值都会被更改
}

执行结果:

1
2
28 28
29 29

方法

在Python中, 方法和函数的区别可以简单理解为在类中的函数就叫方法. 在Go语言中, 什么是方法呢, 可以简单认为, 为结构体定义的函数就叫方法

与其他语言不同的是, 为结构体定义的方法, 从形式上看不是写在结构体里面的, 而是写在结构体外面的

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
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

// 定义结构体方法
// (emp employee) 在函数名的前面定义了接收者, 类似于其他语言的this, 或是Python的self
func (emp employee) printEmpInfo(arg string) {
fmt.Printf("我是形式参数 %s\n", arg)
fmt.Println(emp.name)
fmt.Println(emp.age)
}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
e.printEmpInfo("PolarSnow")
}

执行结果:

1
2
3
我是形式参数 PolarSnow
larry
28

上面的demo中, e.printEmpInfo("PolarSnow") e对象调用了printEmpInfo方法, 并显式传递了PolarSnow参数, 其实在背后, 在存在一个隐式传参, 就是e把自己传递给了方法定义中的(emp employee), 类似于Java中的this, 和Python中的self

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

// 定义普通函数
func printEmpInfo(emp employee, arg string) {
fmt.Printf("我是形式参数 %s\n", arg)
fmt.Println(emp.name)
fmt.Println(emp.age)
}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
printEmpInfo(e, "PolarSnow")
}

这么写效果是一样的, 只是调用的时候是直接调用函数, 把结构体对象当做形式参数传递, 而不是调用结构体自己的方法, 将自己隐式传递到方法中

结构体方法中隐式传参的问题

注意: 默认情况下, Go语言所有参数都是传值, func (emp employee) printEmpInfo(arg string) 也不例外, 同样是值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

// 定义结构体方法
func (emp employee) setEmpAge(age int) {
emp.age = age
fmt.Println("我是结构体方法里的值", emp.age)
}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
e.setEmpAge(29)
fmt.Println("我是结构体方法外面的值", e.age)
}

执行结果:

1
2
我是结构体方法里的值 29
我是结构体方法外面的值 28
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
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

// 定义结构体方法
// 只要将接收结构体对象的数据类型前加*(取指针) 则方法内对结构体对象成员的修改就会同步到方法外部
func (emp *employee) setEmpAge(age int) {
emp.age = age
fmt.Println("我是结构体方法里的值", emp.age)
}

func main() {
e := employee{id:123, name:"larry", age:28, department:"paas", city:"TangShan"}
// 调用方不需要任何更改, Go会自动去识别是应该传值还是传址
e.setEmpAge(29)
fmt.Println("我是结构体方法外面的值", e.age)
}

执行结果:

1
2
我是结构体方法里的值 29
我是结构体方法外面的值 29

注意: 只有指针才可以改变结构体中的内容

nil指针也是可以调用方法的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type employee struct {
id int
name string
age int
department string
city string

}

func (emp *employee) setEmpAge(age int) {
if emp == nil {
fmt.Println("employee 对象为 nil 指针")
return
}
}

func main() {
var e *employee
e.setEmpAge(29)
}

执行结果:

1
employee 对象为 nil 指针

值接收者 vs 指针接收者

  • 要改变内容必须使用指针接收者
  • 结构过大也需要考虑使用指针接受者
  • 一致性: 如果有指针接收者, 最好都是指针接收者

在Go语言中, 当我们尝试遍历一个字符串的时候

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

import "fmt"

func main() {
s := "我是带😈emoji的字符串!"
fmt.Println(s)

for i, ch := range s {
fmt.Printf("%d %c\n", i, ch)
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我是带😈emoji的字符串!
0 我
3 是
6 带
9 😈
13 e
14 m
15 o
16 j
17 i
18 的
21 字
24 符
27 串
30 !

每个i对应的都是一个字节, 结果中出现的0 3 6 9是对应了获取底层字节的索引码, 第二个字符的字节码之所以会从第3位开始取, 是因为第一个中文字符其实占用了三个字节, 所以下一个字需要从索引为3的字节开始读取

Go语言中, 有一种数据类型叫rune, 他是由一个int32类型构成的新的数据类型, 专门用来处理Go语言中的字符问题

下面的demo中, 我们使用强制类型转换来将字符串, 强制转换为rune类型

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

import "fmt"

func main() {
s := "我是带😈emoji的字符串!"
fmt.Println(s)

for i, ch := range []rune(s) {
fmt.Printf("%d %c\n", i, ch)
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我是带😈emoji的字符串!
0 我
1 是
2 带
3 😈
4 e
5 m
6 o
7 j
8 i
9 的
10 字
11 符
12 串
13 !

小结:

  • rune相当于Go语言中的char
  • 使用range遍历position, rune
  • 使用utf8.RuneCountInString来获得字符的长度
  • 使用len(s)来获得字节的长度, len([]rune(s))来获得字符的长度
  • 使用[]byte获得字节

使用strings包操作字符串

一下列出常用的函数, 更多使用方法和demo请参考官方文档: https://golang.org/pkg/strings/

Compare

func Compare(a, b string) int

Compare函数用来比较两个字符串, 返回值为int类型, 如果两个字符串相等, 返回0; 如果a>b返回1; a<b返回-1

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

import (
"fmt"
"strings"
)

func main() {
fmt.Println(strings.Compare("a", "b"))
fmt.Println(strings.Compare("a", "a"))
fmt.Println(strings.Compare("b", "a"))
}

执行结果:

1
2
3
-1
0
1

Contains

func Contains(s, substr string) bool

Contains函数用来判断第二个参数的字符串是否是第一个参数字符串的子串, 返回值为bool类型

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

import (
"fmt"
"strings"
)

func main() {
fmt.Println(strings.Contains("seafood", "foo"))
fmt.Println(strings.Contains("seafood", "bar"))
fmt.Println(strings.Contains("seafood", ""))
fmt.Println(strings.Contains("", ""))
}

执行结果:

1
2
3
4
true
false
true
true

Count

func Count(s, substr string) int

Count函数用来计算子串在s字符串中出现的次数

Fields

func Fields(s string) []string

Fields函数以空格为分隔符, 将字符串分割成slice

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

import (
"fmt"
"strings"
)

func main() {
fmt.Printf("Fields are: %q", strings.Fields(" foo bar baz "))
}

执行结果:

1
Fields are: ["foo" "bar" "baz"]

Index

func Index(s, substr string) int

Index函数返回第一个substr在s中出现的位置, 如果没有找到, 将返回-1

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

import (
"fmt"
"strings"
)

func main() {
fmt.Println(strings.Index("chicken", "ken"))
fmt.Println(strings.Index("chicken", "dmr"))
}

执行结果:

1
2
4
-1

Join

func Join(a []string, sep string) string

Join函数用来将slice以指定的字符拼接成字符串

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

import (
"fmt"
"strings"
)

func main() {
s := []string{"foo", "bar", "baz"}
fmt.Println(strings.Join(s, ", "))
}

执行结果:

1
foo, bar, baz

LastIndex

func LastIndex(s, substr string) int

LastIndex函数用来查找在s中最后一次出现子串的位置

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

import (
"fmt"
"strings"
)

func main() {
fmt.Println(strings.Index("go gopher", "go"))
fmt.Println(strings.LastIndex("go gopher", "go"))
fmt.Println(strings.LastIndex("go gopher", "rodent"))
}

执行结果:

1
2
3
0
3
-1

Repeat

func Repeat(s string, count int) string

Repeat函数返回以指定次数重复的字符(串)

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

import (
"fmt"
"strings"
)

func main() {
fmt.Println("ba" + strings.Repeat("na", 2))
}

执行结果:

1
banana

Replace

func Replace(s, old, new string, n int) string

替换

Split

func Split(s, sep string) []string

将字符串以指定字符切割成slice

Title

func Title(s string) string

每个单词的首字符大写

ToLower

func ToLower(s string) string

转小写

ToUpper

func ToUpper(s string) string

转大写

Trim/TrimLeft/TrimRight

func Trim(s string, cutset string) string

去掉两边空格/去掉左边空格/去掉右边空格