├── .gitignore ├── .gitmodules ├── .travis.yml ├── CNAME ├── LICENSE ├── Makefile ├── config.yaml ├── content ├── en │ ├── _index.md │ └── troubleshooting │ │ └── _index.md └── zh │ ├── _index.md │ ├── avoid │ ├── _index.md │ ├── cases │ │ ├── _index.md │ │ ├── arp-cache-overflow-causes-healthcheck-failed.md │ │ ├── cross-vpc-connect-nodeport-timeout.md │ │ ├── dns-lookup-5s-delay.md │ │ ├── dns-resolution-abnormal.md │ │ ├── eviction-leads-to-service-disruption.md │ │ ├── high-legacy-from-pod-to-another-apiserver.md │ │ ├── kubernetes-overflow-and-drop.md │ │ ├── lb-with-local-externaltrafficpolicy-timeout-occasionally.md │ │ ├── livenesprobe-failed-occasionally.md │ │ ├── low-cps-from-lb-to-nodeport.md │ │ ├── no-route-to-host.md │ │ └── schemaerror-when-using-kubectl-apply-or-edit.md │ ├── cgroup-leaking.md │ ├── conntrack-conflict.md │ ├── dotnet-configuration-auto-reload.md │ ├── handle-cgroup-oom-in-userspace-with-oom-guard.md │ ├── nodelocal-dns.md │ ├── scale-keepalive-service.md │ └── tcp_tw_recycle-causes-packet-loss.md │ ├── best-practice │ ├── _index.md │ ├── deploy │ │ ├── _index.md │ │ ├── highly-available.md │ │ └── resource-utilization.md │ ├── etcd.md │ ├── kernel.md │ └── master.md │ ├── bigdata-ai │ ├── _index.md │ └── flink-on-kubernetes │ │ ├── _index.md │ │ ├── flink-on-kubernetes.md │ │ ├── intro.md │ │ ├── job-cluster.md │ │ ├── native-kubernetes.md │ │ └── session-cluster.md │ ├── cluster │ ├── _index.md │ ├── ingress │ │ ├── _index.md │ │ ├── nginx │ │ │ ├── _index.md │ │ │ └── install-nginx-ingress.md │ │ └── traefik │ │ │ ├── _index.md │ │ │ └── install-traefik-ingress.md │ ├── metrics │ │ ├── _index.md │ │ └── install-metrics-server.md │ ├── network │ │ ├── _index.md │ │ ├── flannel │ │ │ ├── _index.md │ │ │ └── deploy.md │ │ └── understanding.md │ └── runtime │ │ ├── _index.md │ │ └── containerd │ │ ├── _index.md │ │ └── install-containerd.md │ ├── configuration │ ├── _index.md │ └── helm │ │ ├── _index.md │ │ ├── helm-faq.md │ │ ├── install-helm.md │ │ └── upgrade-helm-v2-to-v3.md │ ├── deploy │ ├── _index.md │ ├── addons │ │ ├── _index.md │ │ ├── coredns.md │ │ └── kube-proxy.md │ ├── appendix │ │ ├── _index.md │ │ └── install-kubectl.md │ ├── kubespray.md │ ├── manual │ │ ├── _index.md │ │ ├── bootstrapping-etcd.md │ │ ├── bootstrapping-master.md │ │ ├── bootstrapping-worker-nodes.md │ │ ├── deploy-critical-addons.md │ │ └── prepare.md │ └── selection.md │ ├── dev │ ├── _index.md │ ├── client-go.md │ └── golang-build.md │ ├── ingress │ ├── _index.md │ ├── choose.md │ ├── nginx-ingress │ │ ├── _index.md │ │ └── install-nginx-ingress.md │ ├── traefik-ingress │ │ ├── _index.md │ │ ├── install-traefik-ingress.md │ │ └── use-traefik-to-support-login.md │ └── understand.md │ ├── log │ ├── _index.md │ └── loki-stack.md │ ├── metrics │ └── _index.md │ ├── monitoring │ ├── _index.md │ ├── alerting.md │ ├── build-cloud-native-large-scale-distributed-monitoring-system │ │ ├── _index.md │ │ ├── optimize-prometheus-in-large-scale.md │ │ ├── thanos-arch.md │ │ └── thanos-deploy.md │ ├── dingtalk-alert.md │ ├── kube-prometheus-quickstart.md │ ├── promql.md │ └── wechat-work-alert.md │ ├── reference │ └── net-shell.md │ ├── security │ ├── _index.md │ ├── cert │ │ ├── _index.md │ │ ├── autogenerate-certificate-with-cert-manager.md │ │ └── install-cert-manger.md │ ├── permission │ │ ├── _index.md │ │ ├── app.md │ │ └── user.md │ └── user │ │ ├── _index.md │ │ └── create-user-using-csr-api.md │ ├── trick │ ├── _index.md │ ├── efficient-kubectl.md │ ├── perf-in-container.md │ ├── shell │ │ ├── _index.md │ │ ├── network.md │ │ ├── node.md │ │ └── pod.md │ ├── wildcard-domain-forward.md │ └── yaml │ │ ├── _index.md │ │ ├── affnity.md │ │ └── deployment.md │ └── troubleshooting │ ├── _index.md │ ├── handle │ ├── _index.md │ ├── arp_cache-overflow.md │ ├── disk-full.md │ ├── high-load.md │ ├── memory-fragmentation.md │ ├── pid-full.md │ └── runnig-out-of-inotify-watches.md │ ├── network │ ├── _index.md │ ├── dns.md │ ├── lb-healthcheck-failed.md │ ├── low-throughput.md │ ├── manual.md │ ├── service-cannot-resolve.md │ └── service-unrecheable.md │ ├── node │ ├── _index.md │ ├── arp_cache-neighbor-table-overflow.md │ ├── cannot-allocate-memory.md │ ├── kernel-solft-lockup.md │ ├── no-space-left-on-device.md │ └── not-ready.md │ ├── others │ ├── _index.md │ ├── daemonset-not-scheduled.md │ ├── job-cannot-delete.md │ ├── kubectl-exec-or-logs-failed.md │ ├── namespace-stuck-on-terminating.md │ ├── node-all-gone.md │ └── slow-apiserver.md │ ├── pod │ ├── _index.md │ ├── container-proccess-exit-by-itself.md │ ├── healthcheck-failed.md │ ├── keep-containercreating-or-waiting.md │ ├── keep-crashloopbackoff.md │ ├── keep-error.md │ ├── keep-imageinspecterror.md │ ├── keep-imagepullbackoff.md │ ├── keep-pending.md │ ├── keep-terminating.md │ ├── keep-unkown.md │ └── slow-terminating.md │ └── trick │ ├── _index.md │ ├── analysis-exitcode.md │ ├── capture-packets-in-container.md │ └── use-systemtap-to-locate-problems.md ├── images └── logo.png ├── script ├── deploy.sh └── update-index.sh └── static ├── images ├── flink-on-k8s.jpg ├── flink-stream.png ├── grafana-dashboard-pod.png ├── grafana-select-dashboard.png ├── loki-grafana-data-source.png ├── loki-log.png ├── spark-on-k8s.png └── tf-operator.png └── img ├── alipay-1.png ├── alipay-10.png ├── alipay-100.png ├── alipay-2.png ├── alipay-5.png ├── alipay-50.png ├── alipay-btn.png ├── avatar.jpg ├── geek_daily_qrcode.jpg ├── wechat-1.png ├── wechat-10.png ├── wechat-100.png ├── wechat-2.png ├── wechat-5.png ├── wechat-50.png └── wechat-btn.png /.gitignore: -------------------------------------------------------------------------------- 1 | algolia-*.yaml 2 | public/ 3 | resources/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "themes/hugo-theme-learn"] 2 | path = themes/hugo-theme-learn 3 | url = https://github.com/imroc/hugo-theme-learn 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: stable 3 | 4 | git: 5 | submodules: false 6 | 7 | before_install: 8 | - export TZ='Asia/Shanghai' 9 | - git checkout master 10 | - git clone --depth 1 -b gh-pages https://${GH_REF} _book 11 | 12 | install: 13 | - npm install gitbook-cli -g 14 | 15 | script: 16 | - make build 17 | 18 | after_script: 19 | - cd _book 20 | - git init 21 | - git config user.name "roc" 22 | - git config user.email "roc@imroc.io" 23 | - git add . 24 | - git commit -m "Auto push from travis ci." 25 | - git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages 26 | 27 | branches: 28 | only: 29 | - master 30 | env: 31 | global: 32 | - GH_REF: github.com/imroc/kubernetes-practice-guide.git -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | k8s.imroc.io -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # It's necessary to set this because some environments don't link sh -> bash. 2 | SHELL := /bin/bash 3 | 4 | 5 | # .PHONY: all 6 | # linux: build_linux 7 | 8 | .PHONY: index 9 | index: 10 | script/update-index.sh 11 | 12 | .PHONY: index_zh 13 | index_zh: 14 | script/update-index.sh zh 15 | 16 | .PHONY: index_en 17 | index_en: 18 | script/update-index.sh en 19 | 20 | .PHONY: update 21 | update: 22 | script/deploy.sh -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | baseURL: "https://k8s.imroc.io/" 2 | languageCode: zh 3 | defaultContentLanguage: zh 4 | title: "Kubernetes 实践指南" 5 | theme: "hugo-theme-learn" 6 | metaDataFormat: "yaml" 7 | defaultContentLanguageInSubdir: false 8 | 9 | markup.goldmark.renderer.unsafe: true 10 | 11 | params: 12 | description: "Kubernetes 实践指南" 13 | author: "roc" 14 | showVisitedLinks: true 15 | disableBreadcrumb: false 16 | disableNextPrev: false 17 | ordersectionsby: weight 18 | defaultModifierDisplayName: roc 19 | defaultModifierURL: "https://imroc.io" 20 | cdn: 21 | enable: true 22 | css: "https://kubetencent-1251707795.cos.accelerate.myqcloud.com/css/bundle.min.b0e6163350b1169d986890f4bd317db6286105863b7a53274abdb5c8d935eaa1.css" 23 | js: "https://kubetencent-1251707795.cos.accelerate.myqcloud.com/js/bundle.min.8d534562810f42a5942ec72a0b7ae599c8bf0c96a4c8d6fafffa9bf21fce9d77.js" 24 | logo: 25 | text: "Kubernetes 实践指南" 26 | image: "https://res.cloudinary.com/imroc/image/upload/c_scale,w_64/v1583293443/kubernetes-practice-guide/img/kubernetes.png" 27 | github: 28 | repo: "imroc/kubernetes-practice-guide" 29 | gitcomment: 30 | owner: "imroc" 31 | repo: "kubernetes-practice-guide" 32 | clientId: "6e0166abfd4b13fb3f6e" 33 | admin: ["imroc"] 34 | clientSecret: "9960a67408e0cc826566869e0e3247017004d007" 35 | 36 | outputs: 37 | home: [ "HTML", "RSS", "JSON"] 38 | 39 | languages: 40 | zh: 41 | editURL: "https://github.com/imroc/kubernetes-practice-guide/edit/master/content/zh/" 42 | weight: 1 43 | contentDir: content/zh 44 | languageName: 简体中文 45 | dateFormat: "2006-01-02" 46 | reward: true 47 | algolia: 48 | index: "kubernetes-practice-guide-zh" 49 | key: "5b4678e9995fe4211c4fa43ad2ffdab5" 50 | appID: "B6Q6PSGUV5" 51 | menu: 52 | - shortcuts: 53 | name: " GitHub 仓库" 54 | identifier: "ds" 55 | url: "https://github.com/imroc/kubernetes-practice-guide" 56 | weight: 10 57 | en: 58 | weight: 2 59 | algolia: 60 | index: "kubernetes-practice-guide-en" 61 | key: "5b4678e9995fe4211c4fa43ad2ffdab5" 62 | appID: "B6Q6PSGUV5" 63 | contentDir: content/en 64 | languageName: English 65 | menu: 66 | - shortcuts: 67 | name: " GitHub Repo" 68 | identifier: "ds" 69 | url: "https://github.com/imroc/kubernetes-practice-guide" 70 | weight: 10 -------------------------------------------------------------------------------- /content/en/troubleshooting/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Troubleshooting Guide" 3 | chapter: true 4 | --- 5 | 6 | TODO 7 | {{% children depth=3 %}} -------------------------------------------------------------------------------- /content/zh/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Kubernetes 实践指南" 3 | --- 4 | 5 | {{% notice info %}} 6 | 本书还正在起草,部分内容还不太成熟,请关注文章头部的成熟度提示,无提示的表示已成熟 7 | {{% /notice %}} 8 | 9 | ## 目录 10 | 11 | > 注:不能点击的标题是还正在路上的内容 12 | 13 | {{% children depth=10 %}} 14 | 15 | ## 在线阅读 16 | 17 | 本书将支持中英文两个语言版本,通常文章会先用中文起草并更新,等待其内容较为成熟完善,更新不再频繁的时候才会翻译成英文,点击左上角切换语言。 18 | 19 | * 中文: https://k8s.imroc.io 20 | * English: https://k8s.imroc.io/en 21 | 22 | ## 项目源码 23 | 24 | 项目源码存放于 Github 上: [https://github.com/imroc/kubernetes-practice-guide](https://github.com/imroc/kubernetes-practice-guide) 25 | 26 | ## 贡献 27 | 28 | 欢迎参与贡献和完善内容,贡献方法参考 [CONTRIBUTING](https://github.com/imroc/kubernetes-practice-guide/blob/master/CONTRIBUTING/) 29 | 30 | ## License 31 | 32 | ![](https://res.cloudinary.com/imroc/image/upload/v1583293970/kubernetes-practice-guide/img/licensebutton.png?classes=no-margin) 33 | 34 | [署名-非商业性使用-相同方式共享 4.0 \(CC BY-NC-SA 4.0\)](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh) 35 | -------------------------------------------------------------------------------- /content/zh/avoid/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "避坑指南" 3 | weight: 30 4 | --- 5 | 6 | ## 目录 7 | 8 | {{% children depth=2 %}} 9 | -------------------------------------------------------------------------------- /content/zh/avoid/cases/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "踩坑分享" 3 | --- 4 | 5 | {{% children %}} -------------------------------------------------------------------------------- /content/zh/avoid/cases/arp-cache-overflow-causes-healthcheck-failed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ARP 缓存爆满导致健康检查失败" 3 | --- 4 | 5 | ## 案例 6 | 7 | TKE 一用户某集群节点数 1200+,用户监控方案是 daemonset 部署 node-exporter 暴露节点监控指标,使用 hostNework 方式,statefulset 部署 promethues 且仅有一个实例,落在了一个节点上,promethues 请求所有节点 node-exporter 获取节点监控指标,也就是或扫描所有节点,导致 arp cache 需要存所有 node 的记录,而节点数 1200+,大于了 `net.ipv4.neigh.default.gc_thresh3` 的默认值 1024,这个值是个硬限制,arp cache记录数大于这个就会强制触发 gc,所以会造成频繁gc,当有数据包发送会查本地 arp,如果本地没找到 arp 记录就会判断当前 arp cache 记录数+1是否大于 gc_thresh3,如果没有就会广播 arp 查询 mac 地址,如果大于了就直接报 `arp_cache: neighbor table overflow!`,并且放弃 arp 请求,无法获取 mac 地址也就无法知道探测报文该往哪儿发(即便就在本机某个 veth pair),kubelet 对本机 pod 做存活检查发 arp 查 mac 地址,在 arp cahce 找不到,由于这时 arp cache已经满了,刚要 gc 但还没做所以就只有报错丢包,导致存活检查失败重启 pod 8 | 9 | ## 解决方案 10 | 11 | 调整部分节点内核参数,将 arp cache 的 gc 阀值调高 (`/etc/sysctl.conf`): 12 | 13 | ``` bash 14 | net.ipv4.neigh.default.gc_thresh1 = 80000 15 | net.ipv4.neigh.default.gc_thresh2 = 90000 16 | net.ipv4.neigh.default.gc_thresh3 = 100000 17 | ``` 18 | 19 | 并给 node 打下label,修改 pod spec,加下 nodeSelector 或者 nodeAffnity,让 pod 只调度到这部分改过内核参数的节点,更多请参考本书 [处理实践: arp_cache 溢出](../../handle/arp_cache-overflow/) 20 | -------------------------------------------------------------------------------- /content/zh/avoid/cases/cross-vpc-connect-nodeport-timeout.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "跨 VPC 访问 NodePort 经常超时" 3 | --- 4 | 5 | 现象: 从 VPC a 访问 VPC b 的 TKE 集群的某个节点的 NodePort,有时候正常,有时候会卡住直到超时。 6 | 7 | 原因怎么查? 8 | 9 | 当然是先抓包看看啦,抓 server 端 NodePort 的包,发现异常时 server 能收到 SYN,但没响应 ACK: 10 | 11 | ![](https://imroc.io/assets/blog/troubleshooting-k8s-network/no_ack.png) 12 | 13 | 反复执行 `netstat -s | grep LISTEN` 发现 SYN 被丢弃数量不断增加: 14 | 15 | ![](https://imroc.io/assets/blog/troubleshooting-k8s-network/drop_syn.png) 16 | 17 | 分析: 18 | 19 | - 两个VPC之间使用对等连接打通的,CVM 之间通信应该就跟在一个内网一样可以互通。 20 | - 为什么同一 VPC 下访问没问题,跨 VPC 有问题? 两者访问的区别是什么? 21 | 22 | 再仔细看下 client 所在环境,发现 client 是 VPC a 的 TKE 集群节点,捋一下: 23 | 24 | - client 在 VPC a 的 TKE 集群的节点 25 | - server 在 VPC b 的 TKE 集群的节点 26 | 27 | 因为 TKE 集群中有个叫 `ip-masq-agent` 的 daemonset,它会给 node 写 iptables 规则,默认 SNAT 目的 IP 是 VPC 之外的报文,所以 client 访问 server 会做 SNAT,也就是这里跨 VPC 相比同 VPC 访问 NodePort 多了一次 SNAT,如果是因为多了一次 SNAT 导致的这个问题,直觉告诉我这个应该跟内核参数有关,因为是 server 收到包没回包,所以应该是 server 所在 node 的内核参数问题,对比这个 node 和 普通 TKE node 的默认内核参数,发现这个 node `net.ipv4.tcp_tw_recycle = 1`,这个参数默认是关闭的,跟用户沟通后发现这个内核参数确实在做压测的时候调整过。 28 | 29 | 解释一下,TCP 主动关闭连接的一方在发送最后一个 ACK 会进入 `TIME_AWAIT` 状态,再等待 2 个 MSL 时间后才会关闭(因为如果 server 没收到 client 第四次挥手确认报文,server 会重发第三次挥手 FIN 报文,所以 client 需要停留 2 MSL的时长来处理可能会重复收到的报文段;同时等待 2 MSL 也可以让由于网络不通畅产生的滞留报文失效,避免新建立的连接收到之前旧连接的报文),了解更详细的过程请参考 TCP 四次挥手。 30 | 31 | 参数 `tcp_tw_recycle` 用于快速回收 `TIME_AWAIT` 连接,通常在增加连接并发能力的场景会开启,比如发起大量短连接,快速回收可避免 `tw_buckets` 资源耗尽导致无法建立新连接 (`time wait bucket table overflow`) 32 | 33 | 查得 `tcp_tw_recycle` 有个坑,在 RFC1323 有段描述: 34 | 35 | ` 36 | An additional mechanism could be added to the TCP, a per-host cache of the last timestamp received from any connection. This value could then be used in the PAWS mechanism to reject old duplicate segments from earlier incarnations of the connection, if the timestamp clock can be guaranteed to have ticked at least once since the old connection was open. This would require that the TIME-WAIT delay plus the RTT together must be at least one tick of the sender’s timestamp clock. Such an extension is not part of the proposal of this RFC. 37 | ` 38 | 39 | 大概意思是说 TCP 有一种行为,可以缓存每个连接最新的时间戳,后续请求中如果时间戳小于缓存的时间戳,即视为无效,相应的数据包会被丢弃。 40 | 41 | Linux 是否启用这种行为取决于 `tcp_timestamps` 和 `tcp_tw_recycle`,因为 `tcp_timestamps` 缺省开启,所以当 `tcp_tw_recycle` 被开启后,实际上这种行为就被激活了,当客户端或服务端以 `NAT` 方式构建的时候就可能出现问题。 42 | 43 | 当多个客户端通过 NAT 方式联网并与服务端交互时,服务端看到的是同一个 IP,也就是说对服务端而言这些客户端实际上等同于一个,可惜由于这些客户端的时间戳可能存在差异,于是乎从服务端的视角看,便可能出现时间戳错乱的现象,进而直接导致时间戳小的数据包被丢弃。如果发生了此类问题,具体的表现通常是是客户端明明发送的 SYN,但服务端就是不响应 ACK。 44 | 45 | 回到我们的问题上,client 所在节点上可能也会有其它 pod 访问到 server 所在节点,而它们都被 SNAT 成了 client 所在节点的 NODE IP,但时间戳存在差异,server 就会看到时间戳错乱,因为开启了 `tcp_tw_recycle` 和 `tcp_timestamps` 激活了上述行为,就丢掉了比缓存时间戳小的报文,导致部分 SYN 被丢弃,这也解释了为什么之前我们抓包发现异常时 server 收到了 SYN,但没有响应 ACK,进而说明为什么 client 的请求部分会卡住直到超时。 46 | 47 | 由于 `tcp_tw_recycle` 坑太多,在内核 4.12 之后已移除: [remove tcp_tw_recycle](https://github.com/torvalds/linux/commit/4396e46187ca5070219b81773c4e65088dac50cc) 48 | -------------------------------------------------------------------------------- /content/zh/avoid/cases/dns-resolution-abnormal.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "DNS 解析异常" 3 | --- 4 | 5 | 现象: 有个用户反馈域名解析有时有问题,看报错是解析超时。 6 | 7 | 第一反应当然是看 coredns 的 log: 8 | 9 | ``` bash 10 | [ERROR] 2 loginspub.xxxxmobile-inc.net. 11 | A: unreachable backend: read udp 172.16.0.230:43742->10.225.30.181:53: i/o timeout 12 | ``` 13 | 14 | 这是上游 DNS 解析异常了,因为解析外部域名 coredns 默认会请求上游 DNS 来查询,这里的上游 DNS 默认是 coredns pod 所在宿主机的 `resolv.conf` 里面的 nameserver (coredns pod 的 dnsPolicy 为 "Default",也就是会将宿主机里的 `resolv.conf` 里的 nameserver 加到容器里的 `resolv.conf`, coredns 默认配置 `proxy . /etc/resolv.conf`, 意思是非 service 域名会使用 coredns 容器中 `resolv.conf` 文件里的 nameserver 来解析) 15 | 16 | 确认了下,超时的上游 DNS 10.225.30.181 并不是期望的 nameserver,VPC 默认 DNS 应该是 180 开头的。看了 coredns 所在节点的 `resolv.conf`,发现确实多出了这个非期望的 nameserver,跟用户确认了下,这个 DNS 不是用户自己加上去的,添加节点时这个 nameserver 本身就在 `resolv.conf` 中。 17 | 18 | 根据内部同学反馈, 10.225.30.181 是广州一台年久失修将被撤裁的 DNS,物理网络,没有 VIP,撤掉就没有了,所以如果 coredns 用到了这台 DNS 解析时就可能 timeout。后面我们自己测试,某些 VPC 的集群确实会有这个 nameserver,奇了怪了,哪里冒出来的? 19 | 20 | 又试了下直接创建 CVM,不加进 TKE 节点发现没有这个 nameserver,只要一加进 TKE 节点就有了 !!! 21 | 22 | 看起来是 TKE 的问题,将 CVM 添加到 TKE 集群会自动重装系统,初始化并加进集群成为 K8S 的 node,确认了初始化过程并不会写 `resolv.conf`,会不会是 TKE 的 OS 镜像问题?尝试搜一下除了 `/etc/resolv.conf` 之外哪里还有这个 nameserver 的 IP,最后发现 `/etc/resolvconf/resolv.conf.d/base` 这里面有。 23 | 24 | 看下 `/etc/resolvconf/resolv.conf.d/base` 的作用:Ubuntu 的 `/etc/resolv.conf` 是动态生成的,每次重启都会将 `/etc/resolvconf/resolv.conf.d/base` 里面的内容加到 `/etc/resolv.conf` 里。 25 | 26 | 经确认: 这个文件确实是 TKE 的 Ubuntu OS 镜像里自带的,可能发布 OS 镜像时不小心加进去的。 27 | 28 | 那为什么有些 VPC 的集群的节点 `/etc/resolv.conf` 里面没那个 IP 呢?它们的 OS 镜像里也都有那个文件那个 IP 呀。 29 | 30 | 请教其它部门同学发现: 31 | 32 | - 非 dhcp 子机,cvm 的 cloud-init 会覆盖 `/etc/resolv.conf` 来设置 dns 33 | - dhcp 子机,cloud-init 不会设置,而是通过 dhcp 动态下发 34 | - 2018 年 4 月 之后创建的 VPC 就都是 dhcp 类型了的,比较新的 VPC 都是 dhcp 类型的 35 | 36 | 真相大白:`/etc/resolv.conf` 一开始内容都包含 `/etc/resolvconf/resolv.conf.d/base` 的内容,也就是都有那个不期望的 nameserver,但老的 VPC 由于不是 dhcp 类型,所以 cloud-init 会覆盖 `/etc/resolv.conf`,抹掉了不被期望的 nameserver,而新创建的 VPC 都是 dhcp 类型,cloud-init 不会覆盖 `/etc/resolv.conf`,导致不被期望的 nameserver 残留在了 `/etc/resolv.conf`,而 coredns pod 的 dnsPolicy 为 “Default”,也就是会将宿主机的 `/etc/resolv.conf` 中的 nameserver 加到容器里,coredns 解析集群外的域名默认使用这些 nameserver 来解析,当用到那个将被撤裁的 nameserver 就可能 timeout。 37 | 38 | 临时解决: 删掉 `/etc/resolvconf/resolv.conf.d/base` 重启 39 | 40 | 长期解决: 我们重新制作 TKE Ubuntu OS 镜像然后发布更新 41 | 42 | 这下应该没问题了吧,But, 用户反馈还是会偶尔解析有问题,但现象不一样了,这次并不是 dns timeout。 43 | 44 | 用脚本跑测试仔细分析现象: 45 | 46 | - 请求 `loginspub.xxxxmobile-inc.net` 时,偶尔提示域名无法解析 47 | - 请求 `accounts.google.com` 时,偶尔提示连接失败 48 | 49 | 进入 dns 解析偶尔异常的容器的 netns 抓包: 50 | 51 | - dns 请求会并发请求 A 和 AAAA 记录 52 | - 测试脚本发请求打印序号,抓包然后 wireshark 分析对比异常时请求序号偏移量,找到异常时的 dns 请求报文,发现异常时 A 和 AAAA 记录的请求 id 冲突,并且 AAAA 响应先返回 53 | 54 | ![](https://imroc.io/assets/blog/troubleshooting-k8s-network/dns-id-conflict.png) 55 | 56 | 正常情况下id不会冲突,这里冲突了也就能解释这个 dns 解析异常的现象了: 57 | 58 | - `loginspub.xxxxmobile-inc.net` 没有 AAAA (ipv6) 记录,它的响应先返回告知 client 不存在此记录,由于请求 id 跟 A 记录请求冲突,后面 A 记录响应返回了 client 发现 id 重复就忽略了,然后认为这个域名无法解析 59 | - `accounts.google.com` 有 AAAA 记录,响应先返回了,client 就拿这个记录去尝试请求,但当前容器环境不支持 ipv6,所以会连接失败 60 | 61 | 那为什么 dns 请求 id 会冲突? 62 | 63 | 继续观察发现: 其它节点上的 pod 不会复现这个问题,有问题这个节点上也不是所有 pod 都有这个问题,只有基于 alpine 镜像的容器才有这个问题,在此节点新起一个测试的 `alpine:latest` 的容器也一样有这个问题。 64 | 65 | 为什么 alpine 镜像的容器在这个节点上有问题在其它节点上没问题? 为什么其他镜像的容器都没问题?它们跟 alpine 的区别是什么? 66 | 67 | 发现一点区别: alpine 使用的底层 c 库是 musl libc,其它镜像基本都是 glibc 68 | 69 | 翻 musl libc 源码, 构造 dns 请求时,请求 id 的生成没加锁,而且跟当前时间戳有关 (`network/res_mkquery.c`): 70 | 71 | ``` c 72 | /* Make a reasonably unpredictable id */ 73 | clock_gettime(CLOCK_REALTIME, &ts); 74 | id = ts.tv_nsec + ts.tv_nsec/65536UL & 0xffff; 75 | ``` 76 | 77 | 看注释,作者应该认为这样id基本不会冲突,事实证明,绝大多数情况确实不会冲突,我在网上搜了很久没有搜到任何关于 musl libc 的 dns 请求 id 冲突的情况。这个看起来取决于硬件,可能在某种类型硬件的机器上运行,短时间内生成的 id 就可能冲突。我尝试跟用户在相同地域的集群,添加相同配置相同机型的节点,也复现了这个问题,但后来删除再添加时又不能复现了,看起来后面新建的 cvm 又跑在了另一种硬件的母机上了。 78 | 79 | OK,能解释通了,再底层的细节就不清楚了,我们来看下解决方案: 80 | 81 | - 换基础镜像 (不用alpine) 82 | - 完全静态编译业务程序(不依赖底层c库),比如go语言程序编译时可以关闭 cgo (CGO_ENABLED=0),并告诉链接器要静态链接 (`go build` 后面加 `-ldflags '-d'`),但这需要语言和编译工具支持才可以 83 | 84 | 最终建议用户基础镜像换成另一个比较小的镜像: `debian:stretch-slim`。 85 | -------------------------------------------------------------------------------- /content/zh/avoid/cases/eviction-leads-to-service-disruption.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "驱逐导致服务中断" 3 | --- 4 | 5 | TODO 优化 6 | 7 | ## 案例 8 | 9 | TKE 一客户的某个节点有问题,无法挂载nfs,通过新加节点,驱逐故障节点的 pod 来规避,但导致了业务 10min 服务不可用,排查发现其它节点 pod 很多集体出现了重启,主要是连不上 kube-dns 无法解析 service,业务调用不成功,从而对外表现为服务不可用。 10 | 11 | 为什么会中断?驱逐的原理是先封锁节点,然后将旧的 node 上的 pod 删除,replicaset 控制器检测到 pod 减少,会重新创建一个 pod,调度到新的 node上,这个过程是先删除,再创建,并非是滚动更新,因此更新过程中,如果一个deployment的所有 pod 都在被驱逐的节点上,则可能导致该服务不可用。 12 | 13 | 那为什么会影响其它 pod?分析kubelet日志,kube-dns 有两个副本,都在这个被驱逐的节点上,所以驱逐的时候 kube-dns 不通,影响了其它 pod 解析 service,导致服务集体不可用。 14 | 15 | 那为什么会中断这么久?通常在新的节点应该很会快才是,通过进一步分析新节点的 kubelet 日志,发现 kube-dns 从拉镜像到容器启动之间花了很长时间,检查节点上的镜像发现有很多大镜像\(1~2GB\),猜测是拉取镜像有并发限制,kube-dns 的镜像虽小,但在排队等大镜像下载完,检查 kubelet 启动参数,确实有 `--registry-burst` 这个参数控制镜像下载并发数限制。但最终发现其实应该是 `--serialize-image-pulls` 这个参数导致的,kubelet 启动参数没有指定该参数,而该参数默认值为 true,即默认串行下载镜像,不并发下载,所以导致镜像下载排队,是的 kube-dns 延迟了很长时间才启动。 16 | 17 | ## 解决方案 18 | 19 | * 避免服务单点故障,多副本,并加反亲和性 20 | * 设置 preStop hook 与 readinessProbe,更新路由规则 21 | 22 | -------------------------------------------------------------------------------- /content/zh/avoid/cases/high-legacy-from-pod-to-another-apiserver.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 访问另一个集群的 apiserver 有延时" 3 | --- 4 | 5 | 现象:集群 a 的 Pod 内通过 kubectl 访问集群 b 的内网地址,偶尔出现延时的情况,但直接在宿主机上用同样的方法却没有这个问题。 6 | 7 | 提炼环境和现象精髓: 8 | 9 | 1. 在 pod 内将另一个集群 apiserver 的 ip 写到了 hosts,因为 TKE apiserver 开启内网集群外内网访问创建的内网 LB 暂时没有支持自动绑内网 DNS 域名解析,所以集群外的内网访问 apiserver 需要加 hosts 10 | 2. pod 内执行 kubectl 访问另一个集群偶尔延迟 5s,有时甚至10s 11 | 12 | 观察到 5s 延时,感觉跟之前 conntrack 的丢包导致 [DNS 解析 5S 延时](dns-lookup-5s-delay/) 有关,但是加了 hosts 呀,怎么还去解析域名? 13 | 14 | 进入 pod netns 抓包: 执行 kubectl 时确实有 dns 解析,并且发生延时的时候 dns 请求没有响应然后做了重试。 15 | 16 | 看起来延时应该就是之前已知 conntrack 丢包导致 dns 5s 超时重试导致的。但是为什么会去解析域名? 明明配了 hosts 啊,正常情况应该是优先查找 hosts,没找到才去请求 dns 呀,有什么配置可以控制查找顺序? 17 | 18 | 搜了一下发现: `/etc/nsswitch.conf` 可以控制,但看有问题的 pod 里没有这个文件。然后观察到有问题的 pod 用的 alpine 镜像,试试其它镜像后发现只有基于 alpine 的镜像才会有这个问题。 19 | 20 | 再一搜发现: musl libc 并不会使用 `/etc/nsswitch.conf` ,也就是说 alpine 镜像并没有实现用这个文件控制域名查找优先顺序,瞥了一眼 musl libc 的 `gethostbyname` 和 `getaddrinfo` 的实现,看起来也没有读这个文件来控制查找顺序,写死了先查 hosts,没找到再查 dns。 21 | 22 | 这么说,那还是该先查 hosts 再查 dns 呀,为什么这里抓包看到是先查的 dns? (如果是先查 hosts 就能命中查询,不会再发起dns请求) 23 | 24 | 访问 apiserver 的 client 是 kubectl,用 go 写的,会不会是 go 程序解析域名时压根没调底层 c 库的 `gethostbyname` 或 `getaddrinfo`? 25 | 26 | 搜一下发现果然是这样: go runtime 用 go 实现了 glibc 的 `getaddrinfo` 的行为来解析域名,减少了 c 库调用 (应该是考虑到减少 cgo 调用带来的的性能损耗) 27 | 28 | issue: [net: replicate DNS resolution behaviour of getaddrinfo(glibc) in the go dns resolver](https://github.com/golang/go/issues/18518) 29 | 30 | 翻源码验证下: 31 | 32 | Unix 系的 OS 下,除了 openbsd, go runtime 会读取 `/etc/nsswitch.conf` (`net/conf.go`): 33 | 34 | ``` go 35 | if runtime.GOOS != "openbsd" { 36 | confVal.nss = parseNSSConfFile("/etc/nsswitch.conf") 37 | } 38 | ``` 39 | 40 | `hostLookupOrder` 函数决定域名解析顺序的策略,Linux 下,如果没有 `nsswitch.conf` 文件就 dns 比 hosts 文件优先 (`net/conf.go`): 41 | 42 | ``` go 43 | // hostLookupOrder determines which strategy to use to resolve hostname. 44 | // The provided Resolver is optional. nil means to not consider its options. 45 | func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder) { 46 | ...... 47 | // If /etc/nsswitch.conf doesn't exist or doesn't specify any 48 | // sources for "hosts", assume Go's DNS will work fine. 49 | if os.IsNotExist(nss.err) || (nss.err == nil && len(srcs) == 0) { 50 | ...... 51 | if c.goos == "linux" { 52 | // glibc says the default is "dns [!UNAVAIL=return] files" 53 | // https://www.gnu.org/software/libc/manual/html_node/Notes-on-NSS-Configuration-File.html. 54 | return hostLookupDNSFiles 55 | } 56 | return hostLookupFilesDNS 57 | } 58 | ``` 59 | 60 | 可以看到 `hostLookupDNSFiles` 的意思是 dns first (`net/dnsclient_unix.go`): 61 | 62 | ``` go 63 | // hostLookupOrder specifies the order of LookupHost lookup strategies. 64 | // It is basically a simplified representation of nsswitch.conf. 65 | // "files" means /etc/hosts. 66 | type hostLookupOrder int 67 | 68 | const ( 69 | // hostLookupCgo means defer to cgo. 70 | hostLookupCgo hostLookupOrder = iota 71 | hostLookupFilesDNS // files first 72 | hostLookupDNSFiles // dns first 73 | hostLookupFiles // only files 74 | hostLookupDNS // only DNS 75 | ) 76 | 77 | var lookupOrderName = map[hostLookupOrder]string{ 78 | hostLookupCgo: "cgo", 79 | hostLookupFilesDNS: "files,dns", 80 | hostLookupDNSFiles: "dns,files", 81 | hostLookupFiles: "files", 82 | hostLookupDNS: "dns", 83 | } 84 | ``` 85 | 86 | 所以虽然 alpine 用的 musl libc 不是 glibc,但 go 程序解析域名还是一样走的 glibc 的逻辑,而 alpine 没有 `/etc/nsswitch.conf` 文件,也就解释了为什么 kubectl 访问 apiserver 先做 dns 解析,没解析到再查的 hosts,导致每次访问都去请求 dns,恰好又碰到 conntrack 那个丢包问题导致 dns 5s 延时,在用户这里表现就是 pod 内用 kubectl 访问 apiserver 偶尔出现 5s 延时,有时出现 10s 是因为重试的那次 dns 请求刚好也遇到 conntrack 丢包导致延时又叠加了 5s 。 87 | 88 | 解决方案: 89 | 90 | 1. 换基础镜像,不用 alpine 91 | 2. 挂载 `nsswitch.conf` 文件 (可以用 hostPath) 92 | -------------------------------------------------------------------------------- /content/zh/avoid/cases/livenesprobe-failed-occasionally.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 偶尔存活检查失败" 3 | --- 4 | 5 | 现象: Pod 偶尔会存活检查失败,导致 Pod 重启,业务偶尔连接异常。 6 | 7 | 之前从未遇到这种情况,在自己测试环境尝试复现也没有成功,只有在用户这个环境才可以复现。这个用户环境流量较大,感觉跟连接数或并发量有关。 8 | 9 | 用户反馈说在友商的环境里没这个问题。 10 | 11 | 对比友商的内核参数发现有些区别,尝试将节点内核参数改成跟友商的一样,发现问题没有复现了。 12 | 13 | 再对比分析下内核参数差异,最后发现是 backlog 太小导致的,节点的 `net.ipv4.tcp_max_syn_backlog` 默认是 1024,如果短时间内并发新建 TCP 连接太多,SYN 队列就可能溢出,导致部分新连接无法建立。 14 | 15 | 解释一下: 16 | 17 | ![](https://imroc.io/assets/blog/troubleshooting-k8s-network/backlog.png) 18 | 19 | TCP 连接建立会经过三次握手,server 收到 SYN 后会将连接加入 SYN 队列,当收到最后一个 ACK 后连接建立,这时会将连接从 SYN 队列中移动到 ACCEPT 队列。在 SYN 队列中的连接都是没有建立完全的连接,处于半连接状态。如果 SYN 队列比较小,而短时间内并发新建的连接比较多,同时处于半连接状态的连接就多,SYN 队列就可能溢出,`tcp_max_syn_backlog` 可以控制 SYN 队列大小,用户节点的 backlog 大小默认是 1024,改成 8096 后就可以解决问题。 20 | -------------------------------------------------------------------------------- /content/zh/avoid/cases/low-cps-from-lb-to-nodeport.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "LB 压测 NodePort CPS 低" 3 | --- 4 | 5 | 现象: LoadBalancer 类型的 Service,直接压测 NodePort CPS 比较高,但如果压测 LB CPS 就很低。 6 | 7 | 环境说明: 用户使用的黑石TKE,不是公有云TKE,黑石的机器是物理机,LB的实现也跟公有云不一样,但 LoadBalancer 类型的 Service 的实现同样也是 LB 绑定各节点的 NodePort,报文发到 LB 后转到节点的 NodePort, 然后再路由到对应 pod,而测试在公有云 TKE 环境下没有这个问题。 8 | 9 | * client 抓包: 大量SYN重传。 10 | * server 抓包: 抓 NodePort 的包,发现当 client SYN 重传时 server 能收到 SYN 包但没有响应。 11 | 12 | 又是 SYN 收到但没响应,难道又是开启 `tcp_tw_recycle` 导致的?检查节点的内核参数发现并没有开启,除了这个原因,还会有什么情况能导致被丢弃? 13 | 14 | `conntrack -S` 看到 `insert_failed` 数量在不断增加,也就是 conntrack 在插入很多新连接的时候失败了,为什么会插入失败?什么情况下会插入失败? 15 | 16 | 挖内核源码: netfilter conntrack 模块为每个连接创建 conntrack 表项时,表项的创建和最终插入之间还有一段逻辑,没有加锁,是一种乐观锁的过程。conntrack 表项并发刚创建时五元组不冲突的话可以创建成功,但中间经过 NAT 转换之后五元组就可能变成相同,第一个可以插入成功,后面的就会插入失败,因为已经有相同的表项存在。比如一个 SYN 已经做了 NAT 但是还没到最终插入的时候,另一个 SYN 也在做 NAT,因为之前那个 SYN 还没插入,这个 SYN 做 NAT 的时候就认为这个五元组没有被占用,那么它 NAT 之后的五元组就可能跟那个还没插入的包相同。 17 | 18 | 在我们这个问题里实际就是 netfilter 做 SNAT 时源端口选举冲突了,黑石 LB 会做 SNAT,SNAT 时使用了 16 个不同 IP 做源,但是短时间内源 Port 却是集中一致的,并发两个 SYN a 和SYN b,被 LB SNAT 后源 IP 不同但源 Port 很可能相同,这里就假设两个报文被 LB SNAT 之后它们源 IP 不同源 Port 相同,报文同时到了节点的 NodePort 会再次做 SNAT 再转发到对应的 Pod,当报文到了 NodePort 时,这时它们五元组不冲突,netfilter 为它们分别创建了 conntrack 表项,SYN a 被节点 SNAT 时默认行为是 从 port_range 范围的当前源 Port 作为起始位置开始循环遍历,选举出没有被占用的作为源 Port,因为这两个 SYN 源 Port 相同,所以它们源 Port 选举的起始位置相同,当 SYN a 选出源 Port 但还没将 conntrack 表项插入时,netfilter 认为这个 Port 没被占用就很可能给 SYN b 也选了相同的源 Port,这时他们五元组就相同了,当 SYN a 的 conntrack 表项插入后再插入 SYN b 的 conntrack 表项时,发现已经有相同的记录就将 SYN b 的 conntrack 表项丢弃了。 19 | 20 | 解决方法探索: 不使用源端口选举,在 iptables 的 MASQUERADE 规则如果加 `--random-fully` 这个 flag 可以让端口选举完全随机,基本上能避免绝大多数的冲突,但也无法完全杜绝。最终决定开发 LB 直接绑 Pod IP,不基于 NodePort,从而避免 netfilter 的 SNAT 源端口冲突问题。 21 | -------------------------------------------------------------------------------- /content/zh/avoid/cases/schemaerror-when-using-kubectl-apply-or-edit.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "kubectl edit 或者 apply 报 SchemaError" 3 | --- 4 | 5 | ## 问题现象 6 | 7 | kubectl edit 或 apply 资源报如下错误: 8 | 9 | ``` 10 | error: SchemaError(io.k8s.apimachinery.pkg.apis.meta.v1.APIGroup): invalid object doesn't have additional properties 11 | ``` 12 | 13 | 集群版本:v1.10 14 | 15 | ## 排查过程 16 | 17 | 1. 使用 `kubectl apply -f tmp.yaml --dry-run -v8` 发现请求 `/openapi/v2` 这个 api 之后,kubectl在 validate 过程报错。 18 | 2. 换成 kubectl 1.12 之后没有再报错。 19 | 3. `kubectl get --raw '/openapi/v2'` 发现返回的 json 内容与正常集群有差异,刚开始返回的 json title 为 `Kubernetes metrics-server`,正常的是 Kubernetes。 20 | 4. 怀疑是 `metrics-server` 的问题,发现集群内确实安装了 k8s 官方的 `metrics-server`,询问得知之前是 0.3.1,后面升级为了 0.3.5。 21 | 5. 将 metrics-server 回滚之后恢复正常。 22 | 23 | ## 原因分析 24 | 25 | 初步怀疑,新版本的 metrics-server 使用了新的 openapi-generator,生成的 openapi 格式和之前 k8s 版本生成的有差异。导致旧版本的 kubectl 在解析 openapi 的 schema 时发生异常,查看代码发现1.10 和 1.12 版本在解析 openapi 的 schema 时,实现确实有差异。 26 | -------------------------------------------------------------------------------- /content/zh/avoid/cgroup-leaking.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "cgroup 泄露" 3 | --- 4 | 5 | ## 内核 Bug 6 | 7 | `memcg` 是 Linux 内核中用于管理 cgroup 内存的模块,整个生命周期应该是跟随 cgroup 的,但是在低版本内核中\(已知3.10\),一旦给某个 memory cgroup 开启 kmem accounting 中的 `memory.kmem.limit_in_bytes` 就可能会导致不能彻底删除 memcg 和对应的 cssid,也就是说应用即使已经删除了 cgroup \(`/sys/fs/cgroup/memory` 下对应的 cgroup 目录已经删除\), 但在内核中没有释放 cssid,导致内核认为的 cgroup 的数量实际数量不一致,我们也无法得知内核认为的 cgroup 数量是多少。 8 | 9 | 关于 cgroup kernel memory,在 [kernel.org](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v1/memory.html#kernel-memory-extension-config-memcg-kmem) 中有如下描述: 10 | 11 | ``` 12 | 2.7 Kernel Memory Extension (CONFIG_MEMCG_KMEM) 13 | ----------------------------------------------- 14 | 15 | With the Kernel memory extension, the Memory Controller is able to limit 16 | the amount of kernel memory used by the system. Kernel memory is fundamentally 17 | different than user memory, since it can't be swapped out, which makes it 18 | possible to DoS the system by consuming too much of this precious resource. 19 | 20 | Kernel memory accounting is enabled for all memory cgroups by default. But 21 | it can be disabled system-wide by passing cgroup.memory=nokmem to the kernel 22 | at boot time. In this case, kernel memory will not be accounted at all. 23 | 24 | Kernel memory limits are not imposed for the root cgroup. Usage for the root 25 | cgroup may or may not be accounted. The memory used is accumulated into 26 | memory.kmem.usage_in_bytes, or in a separate counter when it makes sense. 27 | (currently only for tcp). 28 | 29 | The main "kmem" counter is fed into the main counter, so kmem charges will 30 | also be visible from the user counter. 31 | 32 | Currently no soft limit is implemented for kernel memory. It is future work 33 | to trigger slab reclaim when those limits are reached. 34 | ``` 35 | 36 | 这是一个 cgroup memory 的扩展,用于限制对 kernel memory 的使用,但该特性在老于 4.0 版本中是个实验特性,存在泄露问题,在 4.x 较低的版本也还有泄露问题,应该是造成泄露的代码路径没有完全修复,推荐 4.3 以上的内核。 37 | 38 | ## 造成容器创建失败 39 | 40 | 这个问题可能会导致创建容器失败,因为创建容器为其需要创建 cgroup 来做隔离,而低版本内核有个限制:允许创建的 cgroup 最大数量写死为 65535 \([点我跳转到 commit](https://github.com/torvalds/linux/commit/38460b48d06440de46b34cb778bd6c4855030754#diff-c04090c51d3c6700c7128e84c58b1291R3384)\),如果节点上经常创建和销毁大量容器导致创建很多 cgroup,删除容器但没有彻底删除 cgroup 造成泄露\(真实数量我们无法得知\),到达 65535 后再创建容器就会报创建 cgroup 失败并报错 `no space left on device`,使用 kubernetes 最直观的感受就是 pod 创建之后无法启动成功。 41 | 42 | pod 启动失败,报 event 示例: 43 | 44 | ``` bash 45 | Events: 46 | Type Reason Age From Message 47 | ---- ------ ---- ---- ------- 48 | Normal Scheduled 15m default-scheduler Successfully assigned jenkins/jenkins-7845b9b665-nrvks to 10.10.252.4 49 | Warning FailedCreatePodContainer 25s (x70 over 15m) kubelet, 10.10.252.4 unable to ensure pod container exists: failed to create container for [kubepods besteffort podc6eeec88-8664-11e9-9524-5254007057ba] : mkdir /sys/fs/cgroup/memory/kubepods/besteffort/podc6eeec88-8664-11e9-9524-5254007057ba: no space left on device 50 | ``` 51 | 52 | dockerd 日志报错示例: 53 | 54 | ``` bash 55 | Dec 24 11:54:31 VM_16_11_centos dockerd[11419]: time="2018-12-24T11:54:31.195900301+08:00" level=error msg="Handler for POST /v1.31/containers/b98d4aea818bf9d1d1aa84079e1688cd9b4218e008c58a8ef6d6c3c106403e7b/start returned error: OCI runtime create failed: container_linux.go:348: starting container process caused \"process_linux.go:279: applying cgroup configuration for process caused \\\"mkdir /sys/fs/cgroup/memory/kubepods/burstable/pod79fe803c-072f-11e9-90ca-525400090c71/b98d4aea818bf9d1d1aa84079e1688cd9b4218e008c58a8ef6d6c3c106403e7b: no space left on device\\\"\": unknown" 56 | ``` 57 | 58 | kubelet 日志报错示例: 59 | 60 | ``` bash 61 | Sep 09 18:09:09 VM-0-39-ubuntu kubelet[18902]: I0909 18:09:09.449722 18902 remote_runtime.go:92] RunPodSandbox from runtime service failed: rpc error: code = Unknown desc = failed to start sandbox container for pod "osp-xxx-com-ljqm19-54bf7678b8-bvz9s": Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "process_linux.go:258: applying cgroup configuration for process caused \"mkdir /sys/fs/cgroup/memory/kubepods/burstable/podf1bd9e87-1ef2-11e8-afd3-fa163ecf2dce/8710c146b3c8b52f5da62e222273703b1e3d54a6a6270a0ea7ce1b194f1b5053: no space left on device\"" 62 | ``` 63 | 64 | 新版的内核限制为 `2^31` \(可以看成几乎不限制,[点我跳转到代码](https://github.com/torvalds/linux/blob/3120b9a6a3f7487f96af7bd634ec49c87ef712ab/kernel/cgroup/cgroup.c#L5233)\): `cgroup_idr_alloc()` 传入 end 为 0 到 `idr_alloc()`, 再传给 `idr_alloc_u32()`, end 的值最终被三元运算符 `end>0 ? end-1 : INT_MAX` 转成了 `INT_MAX` 常量,即 `2^31`。所以如果新版内核有泄露问题会更难定位,表现形式会是内存消耗严重,幸运的是新版内核已经修复,推荐 4.3 以上。 65 | 66 | ### 规避方案 67 | 68 | 如果你用的低版本内核\(比如 CentOS 7 v3.10 的内核\)并且不方便升级内核,可以通过不开启 kmem accounting 来实现规避,但会比较麻烦。 69 | 70 | kubelet 和 runc 都会给 memory cgroup 开启 kmem accounting,所以要规避这个问题,就要保证kubelet 和 runc 都别开启 kmem accounting,下面分别进行说明: 71 | 72 | #### runc 73 | 74 | runc 在合并 [这个PR](https://github.com/opencontainers/runc/pull/1350/files) \(2017-02-27\) 之后创建的容器都默认开启了 kmem accounting,后来社区也注意到这个问题,并做了比较灵活的修复, [PR 1921](https://github.com/opencontainers/runc/pull/1921) 给 runc 增加了 "nokmem" 编译选项,缺省的 release 版本没有使用这个选项, 自己使用 nokmem 选项编译 runc 的方法: 75 | 76 | ``` bash 77 | cd $GO_PATH/src/github.com/opencontainers/runc/ 78 | make BUILDTAGS="seccomp nokmem" 79 | ``` 80 | 81 | docker-ce v18.09.1 之后的 runc 默认关闭了 kmem accounting,所以也可以直接升级 docker 到这个版本之后。 82 | 83 | #### kubelet 84 | 85 | 如果是 1.14 版本及其以上,可以在编译的时候通过 build tag 来关闭 kmem accounting: 86 | 87 | ``` bash 88 | KUBE_GIT_VERSION=v1.14.1 ./build/run.sh make kubelet GOFLAGS="-tags=nokmem" 89 | ``` 90 | 91 | 如果是低版本需要修改代码重新编译。kubelet 在创建 pod 对应的 cgroup 目录时,也会调用 libcontianer 中的代码对 cgroup 做设置,在 `pkg/kubelet/cm/cgroup_manager_linux.go` 的 `Create` 方法中,会调用 `Manager.Apply` 方法,最终调用 `vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/memory.go` 中的 `MemoryGroup.Apply` 方法,开启 kmem accounting。这里也需要进行处理,可以将这部分代码注释掉然后重新编译 kubelet。 92 | 93 | ## 参考资料 94 | 95 | * 一行 kubernetes 1.9 代码引发的血案(与 CentOS 7.x 内核兼容性问题): [http://dockone.io/article/4797](http://dockone.io/article/4797) 96 | * Cgroup泄漏--潜藏在你的集群中: [https://tencentcloudcontainerteam.github.io/2018/12/29/cgroup-leaking/](https://tencentcloudcontainerteam.github.io/2018/12/29/cgroup-leaking/) 97 | -------------------------------------------------------------------------------- /content/zh/avoid/conntrack-conflict.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "conntrack 冲突导致丢包" 3 | state: "TODO" 4 | --- 5 | 6 | TODO -------------------------------------------------------------------------------- /content/zh/avoid/dotnet-configuration-auto-reload.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ".Net Core 配置文件在Kubernetes中无法热加载" 3 | LastModifierDisplayName: "yulibaozi" 4 | LastModifierURL: "https://yulibaozi.com" 5 | date: 2020-05-09 6 | --- 7 | 8 | 9 | ## 问题描述 10 | 11 | 在使用 kubernetes 部署应用时, 我使用 `kubernetes` 的 `configmap` 来管理配置文件: `appsettings.json` 12 | , 修改configmap 的配置文件后, 我来到了容器里, 通过 `cat /app/config/appsetting.json` 命令查看容器是否已经加载了最新的配置文件, 很幸运的是, 通过命令行查看容器配置发现已经处于最新状态(修改configmap后10-15s 生效), 我尝试请求应用的API, 发现API 在执行过程中使用的配置是老旧的内容, 而不是最新的内容。在本地执行应用时并未出现配置无法热更新的问题。 13 | 14 | ``` bash 15 | # 相关版本 16 | kubernetes 版本: 1.14.2 17 | # 要求版本大于等于 3.1 18 | .Net core: 3.1 19 | 20 | # 容器 os-release (并非 windows) 21 | 22 | NAME="Debian GNU/Linux" 23 | VERSION_ID="10" 24 | VERSION="10 (buster)" 25 | VERSION_CODENAME=buster 26 | ID=debian 27 | HOME_URL="https://www.debian.org/" 28 | SUPPORT_URL="https://www.debian.org/support" 29 | BUG_REPORT_URL="https://bugs.debian.org/" 30 | 31 | # 基础镜像: 32 | mcr.microsoft.com/dotnet/core/sdk:3.1-buster 33 | mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim 34 | 35 | ``` 36 | 37 | ## 问题猜想 38 | 39 | 通过命令行排查发现最新的 `configmap` 配置内容已经在容器的指定目录上更新到最新,但是应用仍然使用老旧的配置内容, 这意味着问题发生在: configmap->**容器->应用**, 容器和应用之间, 容器指定目录下的配置更新并没有触发 `.Net` 热加载机制, 那究竟是为什么没有触发配置热加载,需要深挖根本原因, 直觉猜想是: 查看 `.Net Core` 标准库的配置热加载的实现检查触发条件, 很有可能是触发的条件不满足导致应用配置无法重新加载。 40 | 41 | ## 问题排查 42 | 43 | 猜想方向是热更新的触发条件不满足, 我们熟知使用 `configmap` 挂载文件是使用[symlink](https://en.wikipedia.org/wiki/Symbolic_link)来挂载, 而非常用的物理文件系统, 在修改完 `configmap` , 容器重新加载配置后,这一过程并不会改变文件的修改时间等信息(从容器的角度看)。对此,我们做了一个实验,通过对比configmap修改前和修改后来观察配置( `appsettings.json` )在容器的属性变化(注: 均在容器加载最新配置后对比), 使用 `stat` 命令来佐证了这个细节点。 44 | 45 | **Before:** 46 | 47 | ``` bash 48 | root@app-785bc59df6-gdmnf:/app/Config# stat appsettings.json 49 | File: Config/appsettings.json -> ..data/appsettings.json 50 | Size: 35 Blocks: 0 IO Block: 4096 symbolic link 51 | Device: ca01h/51713d Inode: 27263079 Links: 1 52 | Access: (0777/lrwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root) 53 | Access: 2020-04-25 08:21:18.490453316 +0000 54 | Modify: 2020-04-25 08:21:18.490453316 +0000 55 | Change: 2020-04-25 08:21:18.490453316 +0000 56 | Birth: - 57 | ``` 58 | 59 | **After:** 60 | 61 | ``` bash 62 | root@app-785bc59df6-gdmnf:/app/Config# stat appsettings.json 63 | File: appsettings.json -> ..data/appsettings.json 64 | Size: 35 Blocks: 0 IO Block: 4096 symbolic link 65 | Device: ca01h/51713d Inode: 27263079 Links: 1 66 | Access: (0777/lrwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root) 67 | Access: 2020-04-25 08:21:18.490453316 +0000 68 | Modify: 2020-04-25 08:21:18.490453316 +0000 69 | Change: 2020-04-25 08:21:18.490453316 +0000 70 | Birth: - 71 | ``` 72 | 73 | 通过标准库源码发现, `.Net core` 配置热更新机制似乎是基于文件的最后修改日期来触发的, 根据上面的前后对比显而易见, `configmap` 的修改并没有让容器里的指定的文件的最后修改日期改变,也就未触发 `.Net` 应用配置的热加载。 74 | 75 | ## 解决办法 76 | 77 | 既然猜想基本得到证实, 由于不太熟悉这门语言, 我们尝试在网络上寻找解决办法,很幸运的是我们找到了找到了相关的内容, [fbeltrao](https://github.com/fbeltrao) 开源了一个第三方库([ConfigMapFileProvider](https://github.com/fbeltrao/ConfigMapFileProvider)) 来专门解决这个问题,**通过监听文件内容hash值的变化实现配置热加载**。 78 | 于是, 我们在修改了项目的代码: 79 | 80 | 81 | **Before:** 82 | ``` csharp 83 | // 配置被放在了/app/Config/ 目录下 84 | var configPath = Path.Combine(env.ContentRootPath, "Config"); 85 | config.AddJsonFile(Path.Combine(configPath, "appsettings.json"), 86 | optional: false, 87 | reloadOnChange: true); 88 | ``` 89 | 90 | **After:** 91 | 92 | ``` csharp 93 | // 配置被放在了/app/Config/ 目录下 94 | config.AddJsonFile(ConfigMapFileProvider.FromRelativePath("Config"), 95 | "appsettings.json", 96 | optional: false, 97 | reloadOnChange: true); 98 | ``` 99 | 100 | 修改完项目的代码后, 重新构建镜像, 更新部署在 `kubernetes` 上的应用, 然后再次测试, 到此为止, 会出现两种状态: 101 | 102 | 1. 一种是你热加载配置完全可用, 非常值得祝贺, 你已经成功修复了这个bug; 103 | 2. 一种是你的热加载配置功能还存在 bug, 比如: 上一次请求, 配置仍然使用的老旧配置内容, 下一次请求却使用了最新的配置内容,这个时候, 我们需要继续向下排查: `.NET Core` 引入了`Options`模式,使用类来表示相关的设置组,用强类型的类来表达配置项(白话大概表述为: 代码里面有个对象对应配置里的某个字段, 配置里对应的字段更改会触发代码里对象的属性变化), 示例如下: 104 | 105 | **配置示例:** 106 | ``` bash 107 | cat appsettings.json 108 | "JwtIssuerOptions": { 109 | "Issuer": "test", 110 | "Audience": "test", 111 | "SecretKey": "test" 112 | ... 113 | } 114 | ``` 115 | 116 | **代码示例:** 117 | 118 | ``` csharp 119 | services.Configure(Configuration.GetSection("JwtIssuerOptions")); 120 | ``` 121 | 122 | 而 Options 模式分为三种: 123 | 124 | 1. `IOptions`: Singleton(单例),值一旦生成, 除非通过代码的方式更改,否则它的值不会更新 125 | 2. `IOptionsMonitor`: Singleton(单例), 通过IOptionsChangeTokenSource<> 能够和配置文件一起更新,也能通过代码的方式更改值 126 | 3. `IOptionsSnapshot`: Scoped,配置文件更新的下一次访问,它的值会更新,但是它不能跨范围通过代码的方式更改值,只能在当前范围(请求)内有效。 127 | 128 | 在知道这三种模式的意义后,我们已经完全找到了问题的根因, 把 `Options` 模式设置为:`IOptionsMonitor`就能解决完全解决配置热加载的问题。 129 | 130 | ## 相关链接 131 | 132 | 1. [配置监听ConfigMapFileProvider](https://github.com/fbeltrao/ConfigMapFileProvider) 133 | 2. [相似的Issue: 1175](https://github.com/dotnet/extensions/issues/1175) 134 | 3. [官方Options 描述](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.1) 135 | 4. [IOptions、IOptionsMonitor以及IOptionsSnapshot 测试](https://www.cnblogs.com/wenhx/p/ioptions-ioptionsmonitor-and-ioptionssnapshot.html) -------------------------------------------------------------------------------- /content/zh/avoid/scale-keepalive-service.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "解决长连接服务扩容失效" 3 | --- 4 | 5 | 在现网运营中,有很多场景为了提高效率,一般都采用建立长连接的方式来请求。我们发现在客户端以长连接请求服务端的场景下,K8S的自动扩容会失效。原因是客户端长连接一直保留在老的Pod容器中,新扩容的Pod没有新的连接过来,导致K8S按照步长扩容第一批Pod之后就停止了扩容操作,而且新扩容的Pod没能承载请求,进而出现服务过载的情况,自动扩容失去了意义。 6 | 7 | 对长连接扩容失效的问题,我们的解决方法是将长连接转换为短连接。我们参考了 nginx keepalive 的设计,nginx 中 keepalive_requests 这个配置项设定了一个TCP连接能处理的最大请求数,达到设定值(比如1000)之后服务端会在 http 的 Header 头标记 “`Connection:close`”,通知客户端处理完当前的请求后关闭连接,新的请求需要重新建立TCP连接,所以这个过程中不会出现请求失败,同时又达到了将长连接按需转换为短连接的目的。通过这个办法客户端和云K8S服务端处理完一批请求后不断的更新TCP连接,自动扩容的新Pod能接收到新的连接请求,从而解决了自动扩容失效的问题。 8 | 9 | 由于Golang并没有提供方法可以获取到每个连接处理过的请求数,我们重写了 `net.Listener` 和 `net.Conn`,注入请求计数器,对每个连接处理的请求做计数,并通过 `net.Conn.LocalAddr()` 获得计数值,判断达到阈值 1000 后在返回的 Header 中插入 “`Connection:close`” 通知客户端关闭连接,重新建立连接来发起请求。以上处理逻辑用 Golang 实现示例代码如下: 10 | 11 | ``` go 12 | package main 13 | 14 | import ( 15 | "net" 16 | "github.com/gin-gonic/gin" 17 | "net/http" 18 | ) 19 | 20 | // 重新定义net.Listener 21 | type counterListener struct { 22 | net.Listener 23 | } 24 | 25 | // 重写net.Listener.Accept(),对接收到的连接注入请求计数器 26 | func (c *counterListener) Accept() (net.Conn, error) { 27 | conn, err := c.Listener.Accept() 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &counterConn{Conn: conn}, nil 32 | } 33 | 34 | // 定义计数器counter和计数方法Increment() 35 | type counter int 36 | 37 | func (c *counter) Increment() int { 38 | *c++ 39 | return int(*c) 40 | } 41 | 42 | // 重新定义net.Conn,注入计数器ct 43 | type counterConn struct { 44 | net.Conn 45 | ct counter 46 | } 47 | 48 | // 重写net.Conn.LocalAddr(),返回本地网络地址的同时返回该连接累计处理过的请求数 49 | func (c *counterConn) LocalAddr() net.Addr { 50 | return &counterAddr{c.Conn.LocalAddr(), &c.ct} 51 | } 52 | 53 | // 定义TCP连接计数器,指向连接累计请求的计数器 54 | type counterAddr struct { 55 | net.Addr 56 | *counter 57 | } 58 | 59 | func main() { 60 | r := gin.New() 61 | r.Use(func(c *gin.Context) { 62 | localAddr := c.Request.Context().Value(http.LocalAddrContextKey) 63 | if ct, ok := localAddr.(interface{ Increment() int }); ok { 64 | if ct.Increment() >= 1000 { 65 | c.Header("Connection", "close") 66 | } 67 | } 68 | c.Next() 69 | }) 70 | r.GET("/", func(c *gin.Context) { 71 | c.String(200, "plain/text", "hello") 72 | }) 73 | l, err := net.Listen("tcp", ":8080") 74 | if err != nil { 75 | panic(err) 76 | } 77 | err = http.Serve(&counterListener{l}, r) 78 | if err != nil { 79 | panic(err) 80 | } 81 | } 82 | 83 | ``` -------------------------------------------------------------------------------- /content/zh/avoid/tcp_tw_recycle-causes-packet-loss.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "tcp\_tw\_recycle 引发丢包" 3 | --- 4 | 5 | `tcp_tw_recycle` 这个内核参数用来快速回收 `TIME_WAIT` 连接,不过如果在 NAT 环境下会引发问题。 6 | 7 | RFC1323 中有如下一段描述: 8 | 9 | `An additional mechanism could be added to the TCP, a per-host cache of the last timestamp received from any connection. This value could then be used in the PAWS mechanism to reject old duplicate segments from earlier incarnations of the connection, if the timestamp clock can be guaranteed to have ticked at least once since the old connection was open. This would require that the TIME-WAIT delay plus the RTT together must be at least one tick of the sender’s timestamp clock. Such an extension is not part of the proposal of this RFC.` 10 | 11 | * 大概意思是说TCP有一种行为,可以缓存每个连接最新的时间戳,后续请求中如果时间戳小于缓存的时间戳,即视为无效,相应的数据包会被丢弃。 12 | * Linux是否启用这种行为取决于tcp\_timestamps和tcp\_tw\_recycle,因为tcp\_timestamps缺省就是开启的,所以当tcp\_tw\_recycle被开启后,实际上这种行为就被激活了,当客户端或服务端以NAT方式构建的时候就可能出现问题,下面以客户端NAT为例来说明: 13 | * 当多个客户端通过NAT方式联网并与服务端交互时,服务端看到的是同一个IP,也就是说对服务端而言这些客户端实际上等同于一个,可惜由于这些客户端的时间戳可能存在差异,于是乎从服务端的视角看,便可能出现时间戳错乱的现象,进而直接导致时间戳小的数据包被丢弃。如果发生了此类问题,具体的表现通常是是客户端明明发送的SYN,但服务端就是不响应ACK。 14 | * 在4.12之后的内核已移除tcp\_tw\_recycle内核参数: [https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4396e46187ca5070219b81773c4e65088dac50cc](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4396e46187ca5070219b81773c4e65088dac50cc) [https://github.com/torvalds/linux/commit/4396e46187ca5070219b81773c4e65088dac50cc](https://github.com/torvalds/linux/commit/4396e46187ca5070219b81773c4e65088dac50cc) 15 | 16 | -------------------------------------------------------------------------------- /content/zh/best-practice/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "最佳实践" 3 | weight: 40 4 | 5 | --- 6 | 7 | ## 目录 8 | 9 | {{% children depth=3 %}} -------------------------------------------------------------------------------- /content/zh/best-practice/deploy/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "服务部署最佳实践" 3 | weight: 10 4 | --- 5 | 6 | ## 目录 7 | 8 | {{% children depth=3 %}} -------------------------------------------------------------------------------- /content/zh/best-practice/etcd.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "ETCD 优化" 3 | --- 4 | 5 | ## 高可用部署 6 | 7 | 部署一个高可用ETCD集群可以参考官方文档: https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/clustering.md 8 | 9 | > 如果是 self-host 方式部署的集群,可以用 etcd-operator 部署 etcd 集群;也可以使用另一个小集群专门部署 etcd (使用 etcd-operator) 10 | 11 | ## 提高磁盘 IO 性能 12 | 13 | ETCD 对磁盘写入延迟非常敏感,对于负载较重的集群建议磁盘使用 SSD 固态硬盘。可以使用 diskbench 或 fio 测量磁盘实际顺序 IOPS。 14 | 15 | ## 提高 ETCD 的磁盘 IO 优先级 16 | 17 | 由于 ETCD 必须将数据持久保存到磁盘日志文件中,因此来自其他进程的磁盘活动可能会导致增加写入时间,结果导致 ETCD 请求超时和临时 leader 丢失。当给定高磁盘优先级时,ETCD 服务可以稳定地与这些进程一起运行: 18 | 19 | ``` bash 20 | sudo ionice -c2 -n0 -p $(pgrep etcd) 21 | ``` 22 | 23 | ## 提高存储配额 24 | 25 | 默认 ETCD 空间配额大小为 2G,超过 2G 将不再写入数据。通过给 ETCD 配置 `--quota-backend-bytes` 参数增大空间配额,最大支持 8G。 26 | 27 | ## 分离 events 存储 28 | 29 | 集群规模大的情况下,集群中包含大量节点和服务,会产生大量的 event,这些 event 将会对 etcd 造成巨大压力并占用大量 etcd 存储空间,为了在大规模集群下提高性能,可以将 events 存储在单独的 ETCD 集群中。 30 | 31 | 配置 kube-apiserver: 32 | 33 | ``` bash 34 | --etcd-servers="http://etcd1:2379,http://etcd2:2379,http://etcd3:2379" --etcd-servers-overrides="/events#http://etcd4:2379,http://etcd5:2379,http://etcd6:2379" 35 | ``` 36 | 37 | ## 减小网络延迟 38 | 39 | 如果有大量并发客户端请求 ETCD leader 服务,则可能由于网络拥塞而延迟处理 follower 对等请求。在 follower 节点上的发送缓冲区错误消息: 40 | 41 | ``` bash 42 | dropped MsgProp to 247ae21ff9436b2d since streamMsg's sending buffer is full 43 | dropped MsgAppResp to 247ae21ff9436b2d since streamMsg's sending buffer is full 44 | ``` 45 | 46 | 可以通过在客户端提高 ETCD 对等网络流量优先级来解决这些错误。在 Linux 上,可以使用 tc 对对等流量进行优先级排序: 47 | 48 | ``` bash 49 | $ tc qdisc add dev eth0 root handle 1: prio bands 3 50 | $ tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip sport 2380 0xffff flowid 1:1 51 | $ tc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip dport 2380 0xffff flowid 1:1 52 | $ tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip sport 2379 0xffff flowid 1:1 53 | $ tc filter add dev eth0 parent 1: protocol ip prio 2 u32 match ip dport 2379 0xffff flowid 1:1 54 | ``` 55 | -------------------------------------------------------------------------------- /content/zh/best-practice/kernel.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "内核参数优化" 3 | --- 4 | 5 | ``` ini 6 | # 允许的最大跟踪连接条目,是在内核内存中 netfilter 可以同时处理的“任务”(连接跟踪条目) 7 | net.netfilter.nf_conntrack_max=10485760 8 | net.netfilter.nf_conntrack_tcp_timeout_established=300 9 | # 哈希表大小(只读)(64位系统、8G内存默认 65536,16G翻倍,如此类推) 10 | net.netfilter.nf_conntrack_buckets=655360 11 | 12 | # 每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目 13 | net.core.netdev_max_backlog=10000 14 | 15 | # 表示socket监听(listen)的backlog上限,也就是就是socket的监听队列(accept queue),当一个tcp连接尚未被处理或建立时(半连接状态),会保存在这个监听队列,默认为 128,在高并发场景下偏小,优化到 32768。参考 https://imroc.io/posts/kubernetes-overflow-and-drop/ 16 | net.core.somaxconn=32768 17 | 18 | # 没有启用syncookies的情况下,syn queue(半连接队列)大小除了受somaxconn限制外,也受这个参数的限制,默认1024,优化到8096,避免在高并发场景下丢包 19 | net.ipv4.tcp_max_syn_backlog=8096 20 | 21 | # 表示同一用户同时最大可以创建的 inotify 实例 (每个实例可以有很多 watch) 22 | fs.inotify.max_user_instances=8192 23 | 24 | # max-file 表示系统级别的能够打开的文件句柄的数量, 一般如果遇到文件句柄达到上限时,会碰到 25 | # Too many open files 或者 Socket/File: Can’t open so many files 等错误 26 | fs.file-max=2097152 27 | 28 | # 表示同一用户同时可以添加的watch数目(watch一般是针对目录,决定了同时同一用户可以监控的目录数量) 默认值 8192 在容器场景下偏小,在某些情况下可能会导致 inotify watch 数量耗尽,使得创建 Pod 不成功或者 kubelet 无法启动成功,将其优化到 524288 29 | fs.inotify.max_user_watches=524288 30 | 31 | net.core.bpf_jit_enable=1 32 | net.core.bpf_jit_harden=1 33 | net.core.bpf_jit_kallsyms=1 34 | net.core.dev_weight_tx_bias=1 35 | 36 | net.core.rmem_max=16777216 37 | net.core.wmem_max=16777216 38 | net.ipv4.tcp_rmem=4096 12582912 16777216 39 | net.ipv4.tcp_wmem=4096 12582912 16777216 40 | 41 | net.core.rps_sock_flow_entries=8192 42 | 43 | # 以下三个参数是 arp 缓存的 gc 阀值,相比默认值提高了,当内核维护的 arp 表过于庞大时候,可以考虑优化下,避免在某些场景下arp缓存溢出导致网络超时,参考:https://k8s.imroc.io/avoid/cases/arp-cache-overflow-causes-healthcheck-failed 44 | 45 | # 存在于 ARP 高速缓存中的最少层数,如果少于这个数,垃圾收集器将不会运行。缺省值是 128 46 | net.ipv4.neigh.default.gc_thresh1=2048 47 | # 保存在 ARP 高速缓存中的最多的记录软限制。垃圾收集器在开始收集前,允许记录数超过这个数字 5 秒。缺省值是 512 48 | net.ipv4.neigh.default.gc_thresh2=4096 49 | # 保存在 ARP 高速缓存中的最多记录的硬限制,一旦高速缓存中的数目高于此,垃圾收集器将马上运行。缺省值是 1024 50 | net.ipv4.neigh.default.gc_thresh3=8192 51 | 52 | net.ipv4.tcp_max_orphans=32768 53 | net.ipv4.tcp_max_tw_buckets=32768 54 | 55 | vm.max_map_count=262144 56 | 57 | kernel.threads-max=30058 58 | 59 | net.ipv4.ip_forward=1 60 | 61 | # 避免发生故障时没有 coredump 62 | kernel.core_pattern=core 63 | ``` 64 | -------------------------------------------------------------------------------- /content/zh/best-practice/master.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Master 优化" 3 | --- 4 | 5 | Kubernetes 自 v1.6 以来,官方就宣称单集群最大支持 5000 个节点。不过这只是理论上,在具体实践中从 0 到 5000,还是有很长的路要走,需要见招拆招。 6 | 7 | 官方标准如下: 8 | 9 | * 不超过 5000 个节点 10 | * 不超过 150000 个 pod 11 | * 不超过 300000 个容器 12 | * 每个节点不超过 100 个 pod 13 | 14 | ## Master 节点配置优化 15 | 16 | GCE 推荐配置: 17 | 18 | * 1-5 节点: n1-standard-1 19 | * 6-10 节点: n1-standard-2 20 | * 11-100 节点: n1-standard-4 21 | * 101-250 节点: n1-standard-8 22 | * 251-500 节点: n1-standard-16 23 | * 超过 500 节点: n1-standard-32 24 | 25 | AWS 推荐配置: 26 | 27 | * 1-5 节点: m3.medium 28 | * 6-10 节点: m3.large 29 | * 11-100 节点: m3.xlarge 30 | * 101-250 节点: m3.2xlarge 31 | * 251-500 节点: c4.4xlarge 32 | * 超过 500 节点: c4.8xlarge 33 | 34 | 对应 CPU 和内存为: 35 | 36 | * 1-5 节点: 1vCPU 3.75G内存 37 | * 6-10 节点: 2vCPU 7.5G内存 38 | * 11-100 节点: 4vCPU 15G内存 39 | * 101-250 节点: 8vCPU 30G内存 40 | * 251-500 节点: 16vCPU 60G内存 41 | * 超过 500 节点: 32vCPU 120G内存 42 | 43 | ## kube-apiserver 优化 44 | 45 | ### 高可用 46 | 47 | * 方式一: 启动多个 kube-apiserver 实例通过外部 LB 做负载均衡。 48 | * 方式二: 设置 `--apiserver-count` 和 `--endpoint-reconciler-type`,可使得多个 kube-apiserver 实例加入到 Kubernetes Service 的 endpoints 中,从而实现高可用。 49 | 50 | 不过由于 TLS 会复用连接,所以上述两种方式都无法做到真正的负载均衡。为了解决这个问题,可以在服务端实现限流器,在请求达到阀值时告知客户端退避或拒绝连接,客户端则配合实现相应负载切换机制。 51 | 52 | ### 控制连接数 53 | 54 | kube-apiserver 以下两个参数可以控制连接数: 55 | 56 | ``` bash 57 | --max-mutating-requests-inflight int The maximum number of mutating requests in flight at a given time. When the server exceeds this, it rejects requests. Zero for no limit. (default 200) 58 | --max-requests-inflight int The maximum number of non-mutating requests in flight at a given time. When the server exceeds this, it rejects requests. Zero for no limit. (default 400) 59 | ``` 60 | 61 | 节点数量在 1000 - 3000 之间时,推荐: 62 | 63 | ``` bash 64 | --max-requests-inflight=1500 65 | --max-mutating-requests-inflight=500 66 | ``` 67 | 68 | 节点数量大于 3000 时,推荐: 69 | 70 | ``` bash 71 | --max-requests-inflight=3000 72 | --max-mutating-requests-inflight=1000 73 | ``` 74 | 75 | ## kube-scheduler 与 kube-controller-manager 优化 76 | 77 | ### 高可用 78 | 79 | kube-controller-manager 和 kube-scheduler 是通过 leader election 实现高可用,启用时需要添加以下参数: 80 | 81 | ``` bash 82 | --leader-elect=true 83 | --leader-elect-lease-duration=15s 84 | --leader-elect-renew-deadline=10s 85 | --leader-elect-resource-lock=endpoints 86 | --leader-elect-retry-period=2s 87 | ``` 88 | 89 | ### 控制 QPS 90 | 91 | 与 kube-apiserver 通信的 qps 限制,推荐为: 92 | 93 | ``` bash 94 | --kube-api-qps=100 95 | ``` 96 | 97 | ## 集群 DNS 高可用 98 | 99 | 设置反亲和,让集群 DNS (kube-dns 或 coredns) 分散在不同节点,避免单点故障: 100 | 101 | ``` yaml 102 | affinity: 103 | podAntiAffinity: 104 | requiredDuringSchedulingIgnoredDuringExecution: 105 | - weight: 100 106 | labelSelector: 107 | matchExpressions: 108 | - key: k8s-app 109 | operator: In 110 | values: 111 | - kube-dns 112 | topologyKey: kubernetes.io/hostname 113 | ``` 114 | -------------------------------------------------------------------------------- /content/zh/bigdata-ai/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "大数据与 AI" 3 | weight: 60 4 | --- 5 | 6 | ## 目录 7 | 8 | {{% children %}} 9 | -------------------------------------------------------------------------------- /content/zh/bigdata-ai/flink-on-kubernetes/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Flink on Kubernetes" 3 | weight: 10 4 | chapter: true 5 | --- 6 | 7 | 介绍 Flink 在 Kubernetes 上的实践 8 | -------------------------------------------------------------------------------- /content/zh/bigdata-ai/flink-on-kubernetes/flink-on-kubernetes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Flink on Kubernetes 方案" 3 | weight: 20 4 | state: Alpha 5 | --- 6 | 7 | 8 | 将 Flink 部署到 Kubernetes 有 Session Cluster、Job Cluster 和 Native Kubernetes 三种集群部署方案。 9 | 10 | ## Session Cluster 11 | 12 | 相当于将静态部署的 [Standalone Cluster](https://ci.apache.org/projects/flink/flink-docs-release-1.10/ops/deployment/cluster_setup.html) 容器化,TaskManager 与 JobManager 都以 Deployment 方式部署,可动态提交 Job,Job 处理能力主要取决于 TaskManager 的配置 (slot/cpu/memory) 与副本数 (replicas),调整副本数可以动态扩容。这种方式也是比较常见和成熟的方式。 13 | 14 | ## Job Cluster 15 | 16 | 相当于给每一个独立的 Job 部署一整套 Flink 集群,这套集群就只能运行一个 Job,配备专门制作的 Job 镜像,不能动态提交其它 Job。这种模式可以让每种 Job 拥有专用的资源,独立扩容。 17 | 18 | ## Native Kubernetes 19 | 20 | 这种方式是与 Kubernetes 原生集成,相比前面两种,这种模式能做到动态向 Kubernetes 申请资源,不需要提前指定 TaskManager 数量,就像 flink 与 yarn 和 mesos 集成一样。此模式能够提高资源利用率,但还处于试验阶段,不够成熟,不建议部署到生产环境。 21 | 22 | ## 总结 23 | 24 | TODO 25 | -------------------------------------------------------------------------------- /content/zh/bigdata-ai/flink-on-kubernetes/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Flink 介绍" 3 | weight: 10 4 | --- 5 | 6 | ## Flink 概述 7 | 8 | Apache Flink 是一个面向数据流处理和批量数据处理的可分布式的开源计算框架,它基于同一个Flink流式执行模型(streaming execution model),能够同时支持流处理和批处理两种应用类型。 9 | 10 | 由于流处理和批处理所提供的SLA(服务等级协议)是完全不相同,流处理一般需要支持低延迟、Exactly-once保证,而批处理需要支持高吞吐、高效处理。所以在实现的时候通常是分别给出两套实现方法,或者通过一个独立的开源框架来实现其中每一种处理方案; 比如:实现批处理的开源方案有MapReduce、Spark,实现流处理的开源方案有Storm,Spark的Streaming 其实本质上也是微批处理。 11 | 12 | Flink在实现流处理和批处理时,与传统的一些方案完全不同,它从另一个视角看待流处理和批处理,将二者统一起来:Flink是完全支持流处理,也就是说作为流处理看待时输入数据流是无界的;批处理被作为一种特殊的流处理,只是它的输入数据流被定义为有界的。 13 | 14 | ## 流式框架的演进 15 | 16 | Storm 是流式处理框架的先锋,实时处理能做到低延迟,但很难实现高吞吐,也不能保证精确一致性(exactly-once),即保证执行一次并且只能执行一次。 17 | 18 | 后基于批处理框架 Spark 推出 Spark Streaming,将批处理数据分割的足够小,也实现了流失处理,并且可以做到高吞吐,能实现 exactly-once,但难以做到低时延,因为分割的任务之间需要有间隔时间,无法做到真实时。 19 | 20 | 最后 Flink 诞生了,同时做到了低延迟、高吞吐、exactly-once,并且还支持丰富的时间类型和窗口计算。 21 | 22 | ## Flink 基本架构 23 | 24 | ### JobManager 与 TaskManager 25 | 26 | Flink 主要由两个部分组件构成:JobManager 和 TaskManager。如何理解这两个组件的作用?JobManager 负责资源申请和任务分发,TaskManager 负责任务的执行。跟 k8s 本身类比,JobManager 相当于 Master,TaskManager 相当于 Worker;跟 Spark 类比,JobManager 相当于 Driver,TaskManager 相当于 Executor。 27 | 28 | JobManager 负责整个 Flink 集群任务的调度以及资源的管理,从客户端获取提交的任务,然后根据集群中 TaskManager 上 TaskSlot 的使用情况,为提交的应用分配相应的 TaskSlot 资源并命令 TaskManager 启动从客户端中获取的应用。JobManager 是集群中的Master节点,整个集群有且仅有一个active的JobManager,负责整个集群的任务管理和资源管理。JobManager和TaskManager之间通过Actor System 进行通信,获取任务的执行情况并通过Actor System 将应用的任务的执行情况发送到客户端。同时在任务的执行过程中,Flink JobManager 会触发Checkpoints 操作,每个TaskManager 节点接受的到checkpoints触发命令后,完成checkpoints操作,所有的checkpoint协调过程都是在Flink JobManager中完成。当任务完成后,JobManager会将任务执行信息返回到客户端,并释放掉TaskManager中的资源以供下一次任务使用。 29 | 30 | TaskManager 相当于整个集群的slave 节点,负责具体的任务执行和对应任务在每个节点上的资源申请与管理。客户端通过将编写好的flink应用编译打包,提交到JobManager,然后JobManager会根据已经注册在jobmanger中TaskManager的资源情况,将任务分配到有资源的TaskManager节点,然后启动并运行任务。TaskManager从JobManager那接受需要部署的任务,然后使用slot资源启动task,建立数据接入网络连接,接受数据并处理。同时TaskManager之间的数据交互都是通过数据流的方式进行的。 31 | 32 | ![](/images/flink-on-k8s.jpg) 33 | 34 | ### 有界数据流和无界数据流 35 | 36 | Flink用于处理有界和无界数据: 37 | 38 | * 无界数据流:无界数据流有一个开始但是没有结束,它们不会在生成时终止并提供数据,必须连续处理无界流,也就是说必须在获取后立即处理event。对于无界数据流我们无法等待所有数据都到达,因为输入是无界的,并且在任何时间点都不会完成。处理无界数据通常要求以特定顺序(例如事件发生的顺序)获取event,以便能够推断结果完整性。 39 | * 有界数据流:有界数据流有明确定义的开始和结束,可以在执行任何计算之前通过获取所有数据来处理有界流,处理有界流不需要有序获取,因为可以始终对有界数据集进行排序,有界流的处理也称为批处理。 40 | 41 | ![](/images/flink-stream.png) 42 | 43 | ### 编程模型 44 | 45 | https://ci.apache.org/projects/flink/flink-docs-release-1.10/concepts/programming-model.html -------------------------------------------------------------------------------- /content/zh/bigdata-ai/flink-on-kubernetes/job-cluster.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Job Cluster 模式部署" 3 | weight: 30 4 | state: TODO 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /content/zh/bigdata-ai/flink-on-kubernetes/native-kubernetes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Native Kubernetes 模式部署" 3 | weight: 40 4 | state: Alpha 5 | --- 6 | 7 | ## 与 Kubernetes 集成 8 | 9 | 在 flink 1.10 之前,在 k8s 上运行 flink 任务都是需要事先指定 TaskManager 的个数以及CPU和内存的,存在一个问题:大多数情况下,你在任务启动前根本无法精确的预估这个任务需要多少个TaskManager,如果指定多了,会导致资源浪费,指定少了,会导致任务调度不起来。本质原因是在 Kubernetes 上运行的 Flink 任务并没有直接向 Kubernetes 集群去申请资源。 10 | 11 | 在 2020-02-11 发布了 flink 1.10,该版本完成了与 k8s 集成的第一阶段,实现了向 k8s 动态申请资源,就像跟 yarn 或 mesos 集成那样。 12 | 13 | ## 部署步骤 14 | 15 | 确定 flink 部署的 namespace,这里我选 "flink",确保 namespace 已创建: 16 | 17 | ``` bash 18 | kubectl create ns flink 19 | ``` 20 | 21 | 创建 RBAC (创建 ServiceAccount 绑定 flink 需要的对 k8s 集群操作的权限): 22 | 23 | ``` yaml 24 | apiVersion: v1 25 | kind: ServiceAccount 26 | metadata: 27 | name: flink 28 | namespace: flink 29 | 30 | --- 31 | 32 | apiVersion: rbac.authorization.k8s.io/v1 33 | kind: ClusterRoleBinding 34 | metadata: 35 | name: flink-role-binding 36 | roleRef: 37 | apiGroup: rbac.authorization.k8s.io 38 | kind: ClusterRole 39 | name: edit 40 | subjects: 41 | - kind: ServiceAccount 42 | name: flink 43 | namespace: flink 44 | ``` 45 | 46 | 利用 job 运行启动 flink 的引导程序 (请求 k8s 创建 jobmanager 相关的资源: service, deployment, configmap): 47 | 48 | ``` yaml 49 | apiVersion: batch/v1 50 | kind: Job 51 | metadata: 52 | name: boot-flink 53 | namespace: flink 54 | spec: 55 | template: 56 | spec: 57 | serviceAccount: flink 58 | restartPolicy: OnFailure 59 | containers: 60 | - name: start 61 | image: flink:1.10 62 | workingDir: /opt/flink 63 | command: ["bash", "-c", "$FLINK_HOME/bin/kubernetes-session.sh \ 64 | -Dkubernetes.cluster-id=roc \ 65 | -Dkubernetes.jobmanager.service-account=flink \ 66 | -Dtaskmanager.memory.process.size=1024m \ 67 | -Dkubernetes.taskmanager.cpu=1 \ 68 | -Dtaskmanager.numberOfTaskSlots=1 \ 69 | -Dkubernetes.container.image=flink:1.10 \ 70 | -Dkubernetes.namespace=flink"] 71 | ``` 72 | 73 | * `kubernetes.cluster-id`: 指定 flink 集群的名称,后续自动创建的 k8s 资源会带上这个作为前缀或后缀 74 | * `kubernetes.namespace`: 指定 flink 相关的资源创建在哪个命名空间,这里我们用 `flink` 命名空间 75 | * `kubernetes.jobmanager.service-account`: 指定我们刚刚为 flink 创建的 ServiceAccount 76 | * `kubernetes.container.image`: 指定 flink 需要用的镜像,这里我们部署的 1.10 版本,所以镜像用 `flink:1.10` 77 | 78 | 部署完成后,我们可以看到有刚刚运行完成的 job 的 pod 和被这个 job 拉起的 flink jobmanager 的 pod,前缀与配置 `kubernetes.cluster-id` 相同: 79 | 80 | ``` bash 81 | $ kubectl -n flink get pod 82 | NAME READY STATUS RESTARTS AGE 83 | roc-cf9f6b5df-csk9z 1/1 Running 0 84m 84 | boot-flink-nc2qx 0/1 Completed 0 84m 85 | ``` 86 | 87 | 还有 jobmanager 的 service: 88 | 89 | ``` bash 90 | $ kubectl -n flink get svc 91 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 92 | roc ClusterIP 172.16.255.152 8081/TCP,6123/TCP,6124/TCP 88m 93 | roc-rest LoadBalancer 172.16.255.11 150.109.27.251 8081:31240/TCP 88m 94 | ``` 95 | 96 | 访问 http://150.109.27.251:8081 即可进入此 flink 集群的 ui 界面。 97 | 98 | ## 参考资料 99 | 100 | * Active Kubernetes integration phase 2 - Advanced Features: https://issues.apache.org/jira/browse/FLINK-14460 101 | * Apache Flink 1.10.0 Release Announcement: https://flink.apache.org/news/2020/02/11/release-1.10.0.html 102 | * Native Kubernetes Setup Beta (flink与kubernetes集成的官方教程): https://ci.apache.org/projects/flink/flink-docs-release-1.10/ops/deployment/native_kubernetes.html 103 | -------------------------------------------------------------------------------- /content/zh/bigdata-ai/flink-on-kubernetes/session-cluster.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Session Cluster 模式部署" 3 | weight: 22 4 | state: Alpha 5 | --- 6 | 7 | 8 | 参考官方文档: https://ci.apache.org/projects/flink/flink-docs-stable/ops/deployment/kubernetes.html#flink-session-cluster-on-kubernetes 9 | 10 | 准备资源文件(`flink.yaml`): 11 | 12 | ``` yaml 13 | apiVersion: v1 14 | kind: ConfigMap 15 | metadata: 16 | name: flink-config 17 | labels: 18 | app: flink 19 | data: 20 | flink-conf.yaml: |+ 21 | jobmanager.rpc.address: flink-jobmanager 22 | taskmanager.numberOfTaskSlots: 1 23 | blob.server.port: 6124 24 | jobmanager.rpc.port: 6123 25 | taskmanager.rpc.port: 6122 26 | jobmanager.heap.size: 1024m 27 | taskmanager.memory.process.size: 1024m 28 | log4j.properties: |+ 29 | log4j.rootLogger=INFO, file 30 | log4j.logger.akka=INFO 31 | log4j.logger.org.apache.kafka=INFO 32 | log4j.logger.org.apache.hadoop=INFO 33 | log4j.logger.org.apache.zookeeper=INFO 34 | log4j.appender.file=org.apache.log4j.FileAppender 35 | log4j.appender.file.file=${log.file} 36 | log4j.appender.file.layout=org.apache.log4j.PatternLayout 37 | log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %-60c %x - %m%n 38 | log4j.logger.org.apache.flink.shaded.akka.org.jboss.netty.channel.DefaultChannelPipeline=ERROR, file 39 | --- 40 | 41 | apiVersion: apps/v1 42 | kind: Deployment 43 | metadata: 44 | name: flink-jobmanager 45 | spec: 46 | replicas: 1 47 | selector: 48 | matchLabels: 49 | app: flink 50 | component: jobmanager 51 | template: 52 | metadata: 53 | labels: 54 | app: flink 55 | component: jobmanager 56 | spec: 57 | containers: 58 | - name: jobmanager 59 | image: flink:latest 60 | workingDir: /opt/flink 61 | command: ["/bin/bash", "-c", "$FLINK_HOME/bin/jobmanager.sh start;\ 62 | while :; 63 | do 64 | if [[ -f $(find log -name '*jobmanager*.log' -print -quit) ]]; 65 | then tail -f -n +1 log/*jobmanager*.log; 66 | fi; 67 | done"] 68 | ports: 69 | - containerPort: 6123 70 | name: rpc 71 | - containerPort: 6124 72 | name: blob 73 | - containerPort: 8081 74 | name: ui 75 | livenessProbe: 76 | tcpSocket: 77 | port: 6123 78 | initialDelaySeconds: 30 79 | periodSeconds: 60 80 | volumeMounts: 81 | - name: flink-config-volume 82 | mountPath: /opt/flink/conf 83 | securityContext: 84 | runAsUser: 9999 # refers to user _flink_ from official flink image, change if necessary 85 | volumes: 86 | - name: flink-config-volume 87 | configMap: 88 | name: flink-config 89 | items: 90 | - key: flink-conf.yaml 91 | path: flink-conf.yaml 92 | - key: log4j.properties 93 | path: log4j.properties 94 | --- 95 | 96 | apiVersion: apps/v1 97 | kind: Deployment 98 | metadata: 99 | name: flink-taskmanager 100 | spec: 101 | replicas: 2 102 | selector: 103 | matchLabels: 104 | app: flink 105 | component: taskmanager 106 | template: 107 | metadata: 108 | labels: 109 | app: flink 110 | component: taskmanager 111 | spec: 112 | containers: 113 | - name: taskmanager 114 | image: flink:latest 115 | workingDir: /opt/flink 116 | command: ["/bin/bash", "-c", "$FLINK_HOME/bin/taskmanager.sh start; \ 117 | while :; 118 | do 119 | if [[ -f $(find log -name '*taskmanager*.log' -print -quit) ]]; 120 | then tail -f -n +1 log/*taskmanager*.log; 121 | fi; 122 | done"] 123 | ports: 124 | - containerPort: 6122 125 | name: rpc 126 | livenessProbe: 127 | tcpSocket: 128 | port: 6122 129 | initialDelaySeconds: 30 130 | periodSeconds: 60 131 | volumeMounts: 132 | - name: flink-config-volume 133 | mountPath: /opt/flink/conf/ 134 | securityContext: 135 | runAsUser: 9999 # refers to user _flink_ from official flink image, change if necessary 136 | volumes: 137 | - name: flink-config-volume 138 | configMap: 139 | name: flink-config 140 | items: 141 | - key: flink-conf.yaml 142 | path: flink-conf.yaml 143 | - key: log4j.properties 144 | path: log4j.properties 145 | --- 146 | 147 | apiVersion: v1 148 | kind: Service 149 | metadata: 150 | name: flink-jobmanager 151 | spec: 152 | type: ClusterIP 153 | ports: 154 | - name: rpc 155 | port: 6123 156 | - name: blob 157 | port: 6124 158 | - name: ui 159 | port: 8081 160 | selector: 161 | app: flink 162 | component: jobmanager 163 | --- 164 | 165 | apiVersion: v1 166 | kind: Service 167 | metadata: 168 | name: flink-jobmanager-rest 169 | spec: 170 | type: NodePort 171 | ports: 172 | - name: rest 173 | port: 8081 174 | targetPort: 8081 175 | selector: 176 | app: flink 177 | component: jobmanager 178 | ``` 179 | 180 | 安装: 181 | 182 | ``` bash 183 | kubectl apply -f flink.yaml 184 | ``` 185 | 186 | 如何访问 JobManager 的 UI ?在 TKE 或者 EKS 上,支持 LoadBalancer 类型的 Service,可以将 JobManager 的 UI 用 LB 暴露: 187 | 188 | ``` bash 189 | kubectl patch service flink-jobmanager -p '{"spec":{"type":"LoadBalancer"}}' 190 | ``` 191 | 192 | 卸载: 193 | 194 | ``` bash 195 | kubectl delete -f flink.yaml 196 | ``` 197 | 198 | > 若要部署到不同命名空间,请提前创建好命名空间并在所有 kubectl 命令后加 -n 199 | -------------------------------------------------------------------------------- /content/zh/cluster/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "集群方案" 3 | weight: 3 4 | chapter: true 5 | hidden: true 6 | --- 7 | {{% children depth=3 %}} -------------------------------------------------------------------------------- /content/zh/cluster/ingress/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Ingress 方案" 3 | --- 4 | -------------------------------------------------------------------------------- /content/zh/cluster/ingress/nginx/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Nginx Ingress" 3 | --- -------------------------------------------------------------------------------- /content/zh/cluster/ingress/nginx/install-nginx-ingress.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "安装 nginx ingress controller" 3 | --- 4 | 5 | ## 最佳安装方案 6 | 7 | 如何暴露 ingress 访问入口? 最佳方案是使用 LoadBalancer 类型的 Service 来暴露,即创建外部负载均衡器来暴露流量,后续访问 ingress 的流量都走这个负载均衡器的地址,ingress 规则里面配的域名也要配置解析到这个负载均衡器的 IP 地址。 8 | 9 | 这种方式需要集群支持 LoadBalancer 类型的 Service,如果是云厂商提供的 k8s 服务,或者在云上自建集群并使用了云厂商提供的 cloud provider,也都是支持的,创建 LoadBalancer 类型的 Service 的时候会自动调云厂商的接口创建云厂商提供的负载均衡器产品(通常公网类型的负载均衡器是付费的);如果你的集群不是前面说的情况,是自建集群并且有自己的负载均衡器方案,并部署了相关插件来适配,比如 MetalLB 和 Porter,这样也是可以支持 LoadBalancer 类型的 Service 的。 10 | 11 | ## 使用 helm 安装 12 | 13 | ``` bash 14 | helm install stable/nginx-ingress \ 15 | --name nginx \ 16 | --namespace kube-system \ 17 | --set controller.ingressClass=nginx \ 18 | --set controller.publishService.enabled=true \ 19 | ``` 20 | 21 | * `controller.ingressClass`: 创建的 ingress 中包含 `kubernetes.io/ingress.class` 这个 annotation 并且值与这里配置的一致,这个 nginx ingress controller 才会处理 (生成转发规则) 22 | * `controller.publishService.enabled`: 这个置为 true 主要是为了让 ingress 的外部地址正确显示 (显示为负载均衡器的地址),因为如果不配置这个,默认情况下会将 ingress controller 所有实例的节点 ip 写到 ingress 的 address 里 23 | 24 | 安装完成后如何获取负载均衡器的 IP 地址?查看 nginx ingress controller 的 service 的 `EXTERNAL-IP` 就可以: 25 | 26 | ``` bash 27 | $ kubectl -n kube-system get service nginx-nginx-ingress-controller 28 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 29 | nginx-nginx-ingress-controller LoadBalancer 172.16.255.194 119.28.123.174 80:32348/TCP,443:32704/TCP 10m 30 | ``` 31 | 32 | 如果需要新的流量入口,可以按照同样的方法用 helm 安装新的 release,注意要设置不同的 `controller.ingressClass`,将希望用新流量入口暴露的 ingress 的 `kubernetes.io/ingress.class` annotation 设置成这里的值就可以。 33 | 34 | 如果转发性能跟不上,可以增加 controller 的副本,设置 `controller.replicaCount` 的值,或者启用 HPA 自动伸缩,将 `controller.autoscaling.enabled` 置为 true,更多细节控制请参考官方文档。 35 | 36 | ## 配置优化 37 | 38 | 配置更改如果比较多推荐使用覆盖 `values.yaml` 的方式来安装 nginx ingress: 39 | 40 | 1. 导出默认的 `values.yaml`: 41 | ``` bash 42 | helm inspect values stable/nginx-ingress > values.yaml 43 | ``` 44 | 2. 修改 `values.yaml` 中的配置 45 | 3. 执行 helm install 的时候去掉 `--set` 的方式设置的变量,替换为使用 `-f values.yaml` 46 | 47 | 有时可能更新 nginx ingress 的部署,滚动更新时可能造成部分连接异常,可以参考服务平滑更新最佳实践 [使用 preStopHook 和 readinessProbe 保证服务平滑更新不中断](/best-practice/high-availability-deployment-of-applications.md#smooth-update-using-prestophook-and-readinessprobe),nginx ingress 默认加了 readinessProbe,但 preStop 没有加,我们可以修改 `values.yaml` 中 `controller.lifecycle`,加上 preStop,示例: 48 | 49 | ``` yaml 50 | lifecycle: 51 | preStop: 52 | exec: 53 | command: ["/bin/bash", "-c", "sleep 30"] 54 | ``` 55 | 56 | 还可以 [使用反亲和性避免单点故障](/best-practice/high-availability-deployment-of-applications.md#use-antiaffinity-to-avoid-single-points-of-failure),修改 `controller.affinity` 字段示例: 57 | 58 | ``` yaml 59 | affinity: 60 | podAntiAffinity: 61 | requiredDuringSchedulingIgnoredDuringExecution: 62 | - weight: 100 63 | labelSelector: 64 | matchExpressions: 65 | - key: app 66 | operator: In 67 | values: 68 | - nginx-ingress 69 | - key: component 70 | operator: In 71 | values: 72 | - controller 73 | - key: release 74 | operator: In 75 | values: 76 | - nginx 77 | topologyKey: kubernetes.io/hostname 78 | ``` 79 | 80 | ## 参考资料 81 | 82 | * Github 主页: https://github.com/kubernetes/ingress-nginx 83 | * helm hub 主页: https://hub.helm.sh/charts/nginx/nginx-ingress 84 | * 官方文档: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/ 85 | -------------------------------------------------------------------------------- /content/zh/cluster/ingress/traefik/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Traefik Ingress" 3 | --- 4 | -------------------------------------------------------------------------------- /content/zh/cluster/ingress/traefik/install-traefik-ingress.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "安装 traefik ingress controller" 3 | --- 4 | 5 | ## 最佳安装方案 6 | 7 | 如何暴露 ingress 访问入口? 最佳方案是使用 LoadBalancer 类型的 Service 来暴露,即创建外部负载均衡器来暴露流量,后续访问 ingress 的流量都走这个负载均衡器的地址,ingress 规则里面配的域名也要配置解析到这个负载均衡器的 IP 地址。 8 | 9 | 这种方式需要集群支持 LoadBalancer 类型的 Service,如果是云厂商提供的 k8s 服务,或者在云上自建集群并使用了云厂商提供的 cloud provider,也都是支持的,创建 LoadBalancer 类型的 Service 的时候会自动调云厂商的接口创建云厂商提供的负载均衡器产品(通常公网类型的负载均衡器是付费的);如果你的集群不是前面说的情况,是自建集群并且有自己的负载均衡器方案,并部署了相关插件来适配,比如 MetalLB 和 Porter,这样也是可以支持 LoadBalancer 类型的 Service 的。 10 | 11 | ## 使用 helm 安装 12 | 13 | ``` bash 14 | helm install stable/traefik \ 15 | --name traefik \ 16 | --namespace kube-system \ 17 | --set kubernetes.ingressClass=traefik \ 18 | --set kubernetes.ingressEndpoint.useDefaultPublishedService=true \ 19 | --set rbac.enabled=true 20 | ``` 21 | 22 | * `kubernetes.ingressClass=traefik`: 创建的 ingress 中包含 `kubernetes.io/ingress.class` 这个 annotation 并且值与这里配置的一致,这个 traefik ingress controller 才会处理 (生成转发规则) 23 | * `kubernetes.ingressEndpoint.useDefaultPublishedService=true`: 这个置为 true 主要是为了让 ingress 的外部地址正确显示 (显示为负载均衡器的地址),因为如果不配置这个,默认情况下会将 ingress controller 所有实例的节点 ip 写到 ingress 的 address 里 24 | * `rbac.enabled` 默认为 false,如果没有事先给 default 的 service account 绑足够权限就会报错,通常置为 true,自动创建 rbac 规则 25 | 26 | ## 参考资料 27 | 28 | * Github 主页: https://github.com/containous/traefik 29 | * helm hub 主页: https://hub.helm.sh/charts/stable/traefik 30 | * 官方文档: https://docs.traefik.io 31 | -------------------------------------------------------------------------------- /content/zh/cluster/metrics/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Metrics 方案" 3 | --- 4 | -------------------------------------------------------------------------------- /content/zh/cluster/metrics/install-metrics-server.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "安装 metrics server" 3 | --- 4 | 5 | ## 官方 yaml 安装 6 | 7 | 下载: 8 | 9 | ``` bash 10 | git clone --depth 1 https://github.com/kubernetes-sigs/metrics-server.git 11 | cd metrics-server 12 | ``` 13 | 14 | 修改 `deploy/1.8+/metrics-server-deployment.yaml`,在 `args` 里增加 `--kubelet-insecure-tls` (防止 metrics server 访问 kubelet 采集指标时报证书问题 `x509: certificate signed by unknown authority`): 15 | 16 | ``` yaml 17 | containers: 18 | - name: metrics-server 19 | image: k8s.gcr.io/metrics-server-amd64:v0.3.6 20 | args: 21 | - --cert-dir=/tmp 22 | - --secure-port=4443 23 | - --kubelet-insecure-tls # 这里是新增的一行 24 | ``` 25 | 26 | 安装: 27 | 28 | ``` bash 29 | kubectl apply -f deploy/1.8+/ 30 | ``` 31 | 32 | ## 参考资料 33 | 34 | * Github 主页: https://github.com/kubernetes-sigs/metrics-server 35 | -------------------------------------------------------------------------------- /content/zh/cluster/network/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "网络方案" 3 | --- -------------------------------------------------------------------------------- /content/zh/cluster/network/flannel/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Flannel" 3 | --- 4 | -------------------------------------------------------------------------------- /content/zh/cluster/network/understanding.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "彻底理解集群网络" 3 | --- 4 | 5 | ## 什么是集群网络 6 | 7 | TODO 8 | 9 | ## K8S 网络模型 10 | 11 | TODO 12 | 13 | ## 如何实现 K8S 集群网络 14 | 15 | TODO 16 | 17 | ## 公有云 K8S 服务是如何实现集群网络的 18 | 19 | TODO 20 | 21 | ## CNI 插件 22 | 23 | TODO 24 | 25 | ## 开源网络方案 26 | 27 | TODO 28 | 29 | ## 参考资料 30 | 31 | * Cluster Networking: https://kubernetes.io/docs/concepts/cluster-administration/networking/ 32 | -------------------------------------------------------------------------------- /content/zh/cluster/runtime/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "运行时方案" 3 | --- 4 | -------------------------------------------------------------------------------- /content/zh/cluster/runtime/containerd/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Containerd" 3 | --- 4 | -------------------------------------------------------------------------------- /content/zh/cluster/runtime/containerd/install-containerd.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "安装 containerd" 3 | --- 4 | 5 | ## 二进制部署 6 | 7 | 下载二进制: 8 | 9 | ``` bash 10 | wget -q --show-progress --https-only --timestamping \ 11 | https://github.com/opencontainers/runc/releases/download/v1.0.0-rc8/runc.amd64 \ 12 | https://github.com/containerd/containerd/releases/download/v1.3.0/containerd-1.3.0.linux-amd64.tar.gz \ 13 | https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.16.1/crictl-v1.16.1-linux-amd64.tar.gz 14 | 15 | sudo mv runc.amd64 runc 16 | ``` 17 | 18 | 安装二进制: 19 | 20 | ``` bash 21 | tar -xvf crictl-v1.16.1-linux-amd64.tar.gz 22 | chmod +x crictl runc 23 | sudo cp crictl runc /usr/local/bin/ 24 | 25 | mkdir containerd 26 | tar -xvf containerd-1.3.0.linux-amd64.tar.gz -C containerd 27 | sudo cp containerd/bin/* /bin/ 28 | ``` 29 | 30 | 创建 containerd 启动配置 `config.toml`: 31 | 32 | ``` bash 33 | sudo mkdir -p /etc/containerd/ 34 | cat << EOF | sudo tee /etc/containerd/config.toml 35 | [plugins] 36 | [plugins.cri.containerd] 37 | snapshotter = "overlayfs" 38 | [plugins.cri.containerd.default_runtime] 39 | runtime_type = "io.containerd.runtime.v1.linux" 40 | runtime_engine = "/usr/local/bin/runc" 41 | runtime_root = "" 42 | EOF 43 | ``` 44 | 45 | 创建 systemd 配置 `containerd.service`: 46 | 47 | ``` bash 48 | cat < coredns.yaml 46 | kubectl apply -f coredns.yaml 47 | ``` 48 | -------------------------------------------------------------------------------- /content/zh/deploy/addons/kube-proxy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "以 Daemonset 方式部署 kube-proxy" 3 | --- 4 | 5 | kube-proxy 可以用二进制部署,也可以用 kubelet 的静态 Pod 部署,但最简单使用 DaemonSet 部署。直接使用 ServiceAccount 的 token 认证,不需要签发证书,也就不用担心证书过期问题。 6 | 7 | 先在终端设置下面的变量: 8 | 9 | ``` bash 10 | APISERVER="https://10.200.16.79:6443" 11 | CLUSTER_CIDR="10.10.0.0/16" 12 | ``` 13 | 14 | * `APISERVER` 替换为 apiserver 对外暴露的访问地址。有同学想问为什么不直接用集群内的访问地址(`kubernetes.default` 或对应的 CLUSTER IP),这是一个鸡生蛋还是蛋生鸡的问题,CLSUTER IP 本身就是由 kube-proxy 来生成 iptables 或 ipvs 规则转发 Service 对应 Endpoint 的 Pod IP,kube-proxy 刚启动还没有生成这些转发规则,生成规则的前提是 kube-proxy 需要访问 apiserver 获取 Service 与 Endpoint,而由于还没有转发规则,kube-proxy 访问 apiserver 的 CLUSTER IP 的请求无法被转发到 apiserver。 15 | * `CLUSTER_CIDR` 替换为集群 Pod IP 的 CIDR 范围,这个在部署 kube-controller-manager 时也设置过 16 | 17 | 为 kube-proxy 创建 RBAC 权限和配置文件: 18 | 19 | ``` bash 20 | cat < etcd-csr.json < hosts 需要包含 etcd 每个实例所在节点的内网 IP 45 | 46 | 会生成下面两个重要的文件: 47 | 48 | * `etcd-key.pem`: kube-apiserver 证书密钥 49 | * `etcd.pem`: kube-apiserver 证书 50 | 51 | ## 下载安装 ETCD 52 | 53 | 下载 release 包: 54 | 55 | ``` bash 56 | wget -q --show-progress --https-only --timestamping \ 57 | "https://github.com/etcd-io/etcd/releases/download/v3.4.1/etcd-v3.4.1-linux-amd64.tar.gz" 58 | ``` 59 | 60 | 解压安装 `etcd` 和 `etcdctl` 到 PATH: 61 | 62 | ``` bash 63 | tar -xvf etcd-v3.4.1-linux-amd64.tar.gz 64 | sudo mv etcd-v3.4.1-linux-amd64/etcd* /usr/local/bin/ 65 | ``` 66 | 67 | ## 配置 68 | 69 | 创建配置相关目录,放入证书文件: 70 | 71 | ``` bash 72 | sudo mkdir -p /etc/etcd /var/lib/etcd 73 | sudo cp ca.pem etcd-key.pem etcd.pem /etc/etcd/ 74 | ``` 75 | 76 | etcd 集群每个成员都需要一个名字,这里第一个成员名字用 infra0,第二个可以用 infra1,以此类推,你也可以直接用节点的 hostname: 77 | 78 | ``` bash 79 | NAME=infra0 80 | ``` 81 | 82 | 记当前部署 ETCD 的节点的内网 IP 为 INTERNAL_IP: 83 | 84 | ``` bash 85 | INTERNAL_IP=10.200.16.79 86 | ``` 87 | 88 | 记所有 ETCD 成员的名称和成员间通信的 https 监听地址为 ETCD_SERVERS (注意是 2380 端口,不是 2379): 89 | 90 | ``` bash 91 | ETCD_SERVERS="infra0=https://10.200.16.79:2380,infra1=https://10.200.17.6:2380,infra2=https://10.200.16.70:2380" 92 | ``` 93 | 94 | 创建 systemd 配置: 95 | 96 | ``` bash 97 | cat < 31 | 32 | ``` bash 33 | curl -L https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -o cfssl 34 | curl -L https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -o cfssljson 35 | curl -L https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 -o cfssl-certinfo 36 | 37 | chmod +x cfssl cfssljson cfssl-certinfo 38 | sudo mv cfssl cfssljson cfssl-certinfo /usr/local/bin/ 39 | ``` 40 | 41 | ### 安装 kubectl 42 | 43 | ``` bash 44 | wget -q --show-progress --https-only --timestamping \ 45 | https://storage.googleapis.com/kubernetes-release/release/v1.16.1/bin/linux/amd64/kubectl 46 | 47 | chmod +x kubectl 48 | mv kubectl /usr/local/bin/ 49 | ``` 50 | 51 | ## 生成 CA 证书 52 | 53 | 由于各个组件都需要配置证书,并且依赖 CA 证书来签发证书,所以我们首先要生成好 CA 证书以及后续的签发配置文件: 54 | 55 | ``` bash 56 | cat > ca-csr.json < ca-config.json < 由于这里是 CA 证书,是签发其它证书的根证书,这个证书密钥不会分发出去作为 client 证书,所有组件使用的 client 证书都是由 CA 证书签发而来,所以 CA 证书的 CN 和 O 的名称并不重要,后续其它签发出来的证书的 CN 和 O 的名称才是有用的 106 | -------------------------------------------------------------------------------- /content/zh/deploy/selection.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "方案选型" 3 | weight: 10 4 | state: TODO 5 | --- -------------------------------------------------------------------------------- /content/zh/dev/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "开发指南" 3 | weight: 100 4 | state: Alpha 5 | --- 6 | -------------------------------------------------------------------------------- /content/zh/dev/client-go.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "使用 client-go 开发 k8s 应用" 3 | state: TODO 4 | --- -------------------------------------------------------------------------------- /content/zh/dev/golang-build.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Go 语言编译原理与优化" 3 | state: Alpha 4 | --- 5 | 6 | ## 编译阶段 (Compilation) 7 | 8 | ### debug 参数 9 | 10 | `-m` 打印编译器更多想法的细节 11 | 12 | ``` bash 13 | -gcflags '-m' 14 | ``` 15 | 16 | `-S` 打印汇编 17 | 18 | ``` bash 19 | -gcflags '-S' 20 | ``` 21 | 22 | ### 优化和内联 23 | 24 | 默认开启了优化和内联,但是debug的时候开启可能会出现一些奇怪的问题,通过下面的参数可以禁止任何优化 25 | 26 | ``` bash 27 | -gcflags '-N -l' 28 | ``` 29 | 30 | 内联级别: 31 | 32 | - `-gcflags='-l -l'` 内联级别2,更积极,可能更快,可能会制作更大的二进制文件。 33 | - `-gcflags='-l -l -l'` 内联级别3,再次更加激进,二进制文件肯定更大,也许更快,但也许会有 bug。 34 | - `-gcflags=-l=4` (4个-l)在 Go 1.11 中将支持实验性的[中间栈内联优化](https://github.com/golang/go/issues/19348)。 35 | 36 | ### 逃逸分析 37 | 38 | - 如果一个局部变量值超越了函数调用的生命周期,编译器自动将它逃逸到堆 39 | - 如果一个通过new或make来分配的对象,在函数内即使将指针传递给了其它函数,其它函数会被内联到当前函数,相当于指针不会逃逸出本函数,最终不返回指针的话,该指针对应的值也都会分配在栈上,而不是在堆 40 | 41 | ## 链接阶段 (Linking) 42 | 43 | - Go 支持 internal 和 external 两种链接方式: internal 使用 go 自身实现的 linker,external 需要启动外部的 linker 44 | - linker 的主要工作是将 `.o` (object file) 链接成最终可执行的二进制 45 | - 对应命令: `go tool link`,对应源码: `$GOROOT/src/cmd/link` 46 | - 通过 `-ldflags` 给链接器传参,参数详见: `go tool link --help` 47 | 48 | ### 关于 CGO 49 | 50 | - 启用cgo可以调用外部依赖的c库 51 | - go的编译器会判断环境变量 `CGO_ENABLED` 来决定是否启用cgo,默认 `CGO_ENABLED=1` 即启用cgo 52 | - 源码文件头部的 `build tag` 可以根据cgo是否启用决定源码是否被编译(`// +build cgo` 表示希望cgo启用时被编译,相反的是 `// +build !cgo`) 53 | - 标准库中有部分实现有两份源码,比如: `$GOROOT/src/os/user/lookup_unix.go` 和 `$GOROOT/src/os/user/cgo_lookup_unix.go` ,它们有相同的函数,但实现不一样,前者是纯go实现,后者是使用cgo调用外部依赖来实现,标准库中使用cgo比较常见的是 `net` 包。 54 | 55 | ### internal linking 56 | 57 | - link 默认使用 internal 方式 58 | - 直接使用 go 本身的实现的 linker 来链接代码, 59 | - 功能比较简单,仅仅是将 `.o` 和预编译的 `.a` 写到最终二进制文件中(`.a`文件在 `$GOROOT/pkg` 和 `$GOPATH/pkg` 中,其实就是`.o`文件打包的压缩包,通过 `tar -zxvf` 可以解压出来查看) 60 | 61 | ### external linking 62 | 63 | - 会启动外部 linker (gcc/clang),通过 `-ldflags '-linkmode "external"'` 启用 external linking 64 | - 通过 `-extldflags` 给外部 linker 传参,比如: `-ldflags '-linkmode "external" -extldflags "-static"'` 65 | 66 | ### static link 67 | 68 | go编译出来就是一个二进制,自带runtime,不需要解释器,但并不意味着就不需要任何依赖,但也可以通过静态链接来做到完全不用任何依赖,全部”揉“到一个二进制文件中。实现静态链接的方法: 69 | 70 | - 如果是 `external linking`,可以这样: `-ldflags '-linkmode external -extldflags -static'` 71 | - 如果用默认的 `internal linking`,可以这样: `-ldflags '-d'` 72 | 73 | ### ldflags 其它常用参数 74 | 75 | - `-s -w` 是去除符号表和DWARF调试信息(可以减小二进制体积,但不利于调试,可在用于生产环境),示例: `-ldflags '-s -w'` 76 | - `-X` 可以给变量注入值,比如编译时用脚本动态注入当前版本和 `commit id` 到代码的变量中,通常程序的 `version` 子命令或参数输出当前版本信息时就用这种方式实现,示例:`-ldflags '-X myapp/pkg/version/version=v1.0.0'` 77 | 78 | ## 使用 Docker 编译 79 | 80 | 使用 Docker 编译可以不用依赖本机 go 环境,将编译环境标准化,特别在有外部动态链接库依赖的情况下很有用,可以直接 run 一个容器来编译,给它挂载源码目录和二进制输出目录,这样我们就可以拿到编译出来的二进制了,这里以编译cfssl为例: 81 | 82 | ``` bash 83 | ROOT_PKG=github.com/cloudflare/cfssl 84 | CMD_PKG=$ROOT_PKG/cmd 85 | LOCAL_SOURCE_PATH=/Users/roc/go/src/$ROOT_PKG 86 | LOCAL_OUTPUT_PATH=$PWD 87 | GOPATH=/go/src 88 | ROOT_PATH=$GOPATH/$ROOT_PKG 89 | CMD_PATH=$GOPATH/$CMD_PKG 90 | docker run --rm \ 91 | -v $LOCAL_SOURCE_PATH:$ROOT_PATH \ 92 | -v $LOCAL_OUTPUT_PATH:/output \ 93 | -w $ROOT_PATH \ 94 | golang:1.13 \ 95 | go build -v \ 96 | -ldflags '-d' \ 97 | -o /output/ \ 98 | $CMD_PATH/... 99 | ``` 100 | 101 | 编译镜像可以参考下面示例(使用docker多阶段构建,完全静态编译,没有外部依赖): 102 | 103 | ``` dockerfile 104 | FROM golang:1.12-stretch as builder 105 | MAINTAINER rockerchen@tencent.com 106 | ENV BUILD_DIR /go/src/cloud.tencent.com/qc_container_cluster/hpa-metrics-server 107 | WORKDIR $BUILD_DIR 108 | 109 | COPY ./ $BUILD_DIR 110 | RUN CGO_ENABLED=0 go build -v -o /hpa-metrics-server \ 111 | -ldflags '-d' \ 112 | ./ 113 | 114 | FROM ubuntu:16.04 115 | MAINTAINER rockerchen@tencent.com 116 | RUN apt-get update -y 117 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y curl iproute2 inetutils-tools telnet inetutils-ping 118 | RUN apt-get install --no-install-recommends --no-install-suggests ca-certificates -y 119 | COPY --from=builder /hpa-metrics-server /hpa-metrics-server 120 | RUN chmod a+x /hpa-metrics-server 121 | ``` 122 | 123 | ## 参考资料 124 | 125 | * Go 性能调优之 —— 编译优化: https://segmentfault.com/a/1190000016354799 126 | -------------------------------------------------------------------------------- /content/zh/ingress/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Ingress 指南" 3 | weight: 12 4 | state: TODO 5 | --- 6 | -------------------------------------------------------------------------------- /content/zh/ingress/choose.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Ingress 方案选型" 3 | weight: 20 4 | state: TODO 5 | --- 6 | -------------------------------------------------------------------------------- /content/zh/ingress/nginx-ingress/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Nginx Ingress" 3 | weight: 30 4 | --- -------------------------------------------------------------------------------- /content/zh/ingress/nginx-ingress/install-nginx-ingress.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "安装 nginx ingress controller" 3 | --- 4 | 5 | ## 最佳安装方案 6 | 7 | 如何暴露 ingress 访问入口? 最佳方案是使用 LoadBalancer 类型的 Service 来暴露,即创建外部负载均衡器来暴露流量,后续访问 ingress 的流量都走这个负载均衡器的地址,ingress 规则里面配的域名也要配置解析到这个负载均衡器的 IP 地址。 8 | 9 | 这种方式需要集群支持 LoadBalancer 类型的 Service,如果是云厂商提供的 k8s 服务,或者在云上自建集群并使用了云厂商提供的 cloud provider,也都是支持的,创建 LoadBalancer 类型的 Service 的时候会自动调云厂商的接口创建云厂商提供的负载均衡器产品(通常公网类型的负载均衡器是付费的);如果你的集群不是前面说的情况,是自建集群并且有自己的负载均衡器方案,并部署了相关插件来适配,比如 MetalLB 和 Porter,这样也是可以支持 LoadBalancer 类型的 Service 的。 10 | 11 | ## 使用 helm 安装 12 | 13 | 14 | ``` bash 15 | helm install stable/nginx-ingress \ 16 | --name nginx \ 17 | --namespace kube-system \ 18 | --set controller.ingressClass=nginx \ 19 | --set controller.publishService.enabled=true \ 20 | ``` 21 | 22 | * `controller.ingressClass`: 创建的 ingress 中包含 `kubernetes.io/ingress.class` 这个 annotation 并且值与这里配置的一致,这个 nginx ingress controller 才会处理 (生成转发规则) 23 | * `controller.publishService.enabled`: 这个置为 true 主要是为了让 ingress 的外部地址正确显示 (显示为负载均衡器的地址),因为如果不配置这个,默认情况下会将 ingress controller 所有实例的节点 ip 写到 ingress 的 address 里 24 | 25 | 安装完成后如何获取负载均衡器的 IP 地址?查看 nginx ingress controller 的 service 的 `EXTERNAL-IP` 就可以: 26 | 27 | ``` bash 28 | $ kubectl -n kube-system get service nginx-nginx-ingress-controller 29 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 30 | nginx-nginx-ingress-controller LoadBalancer 172.16.255.194 119.28.123.174 80:32348/TCP,443:32704/TCP 10m 31 | ``` 32 | 33 | 如果需要新的流量入口,可以按照同样的方法用 helm 安装新的 release,注意要设置不同的 `controller.ingressClass`,将希望用新流量入口暴露的 ingress 的 `kubernetes.io/ingress.class` annotation 设置成这里的值就可以。 34 | 35 | 如果转发性能跟不上,可以增加 controller 的副本,设置 `controller.replicaCount` 的值,或者启用 HPA 自动伸缩,将 `controller.autoscaling.enabled` 置为 true,更多细节控制请参考官方文档。 36 | 37 | ## 配置优化 38 | 39 | 配置更改如果比较多推荐使用覆盖 `values.yaml` 的方式来安装 nginx ingress: 40 | 41 | 1. 导出默认的 `values.yaml`: 42 | ``` bash 43 | helm inspect values stable/nginx-ingress > values.yaml 44 | ``` 45 | 2. 修改 `values.yaml` 中的配置 46 | 3. 执行 helm install 的时候去掉 `--set` 的方式设置的变量,替换为使用 `-f values.yaml` 47 | 48 | 有时可能更新 nginx ingress 的部署,滚动更新时可能造成部分连接异常,可以参考服务平滑更新最佳实践 [使用 preStopHook 和 readinessProbe 保证服务平滑更新不中断](/best-practice/high-availability-deployment-of-applications.md#smooth-update-using-prestophook-and-readinessprobe),nginx ingress 默认加了 readinessProbe,但 preStop 没有加,我们可以修改 `values.yaml` 中 `controller.lifecycle`,加上 preStop,示例: 49 | 50 | ``` yaml 51 | lifecycle: 52 | preStop: 53 | exec: 54 | command: ["/bin/bash", "-c", "sleep 30"] 55 | ``` 56 | 57 | 还可以 [使用反亲和性避免单点故障](/best-practice/high-availability-deployment-of-applications.md#use-antiaffinity-to-avoid-single-points-of-failure),修改 `controller.affinity` 字段示例: 58 | 59 | ``` yaml 60 | affinity: 61 | podAntiAffinity: 62 | requiredDuringSchedulingIgnoredDuringExecution: 63 | - weight: 100 64 | labelSelector: 65 | matchExpressions: 66 | - key: app 67 | operator: In 68 | values: 69 | - nginx-ingress 70 | - key: component 71 | operator: In 72 | values: 73 | - controller 74 | - key: release 75 | operator: In 76 | values: 77 | - nginx 78 | topologyKey: kubernetes.io/hostname 79 | ``` 80 | 81 | ## 参考资料 82 | 83 | * Github 主页: https://github.com/kubernetes/ingress-nginx 84 | * helm hub 主页: https://hub.helm.sh/charts/nginx/nginx-ingress 85 | * 官方文档: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/ 86 | -------------------------------------------------------------------------------- /content/zh/ingress/traefik-ingress/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Traefik Ingress" 3 | weight: 40 4 | --- 5 | -------------------------------------------------------------------------------- /content/zh/ingress/traefik-ingress/install-traefik-ingress.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "安装 traefik ingress controller" 3 | weight: 20 4 | --- 5 | 6 | ## 最佳安装方案 7 | 8 | 如何暴露 ingress 访问入口? 最佳方案是使用 LoadBalancer 类型的 Service 来暴露,即创建外部负载均衡器来暴露流量,后续访问 ingress 的流量都走这个负载均衡器的地址,ingress 规则里面配的域名也要配置解析到这个负载均衡器的 IP 地址。 9 | 10 | 这种方式需要集群支持 LoadBalancer 类型的 Service,如果是云厂商提供的 k8s 服务,或者在云上自建集群并使用了云厂商提供的 cloud provider,也都是支持的,创建 LoadBalancer 类型的 Service 的时候会自动调云厂商的接口创建云厂商提供的负载均衡器产品(通常公网类型的负载均衡器是付费的);如果你的集群不是前面说的情况,是自建集群并且有自己的负载均衡器方案,并部署了相关插件来适配,比如 MetalLB 和 Porter,这样也是可以支持 LoadBalancer 类型的 Service 的。 11 | 12 | ## 使用 helm 安装 13 | 14 | > 参考官方文档: https://docs.traefik.io/getting-started/install-traefik/ 15 | 16 | {{% notice info %}} 17 | traefik 官方默认的 Helm Chart 是 v1.x 版本 (stable/traefik),v2.x 的 chart 还在试验阶段 18 | {{% /notice %}} 19 | 20 | 下面是 traefik 官方稳定版的 chart 安装方法(traefik v1.x): 21 | 22 | {{< tabs name="traefik-v1" >}} 23 | {{{< tab name="Helm 2" codelang="bash" >}} 24 | helm install stable/traefik \ 25 | --name traefik \ 26 | --namespace kube-system \ 27 | --set kubernetes.ingressClass=traefik \ 28 | --set kubernetes.ingressEndpoint.useDefaultPublishedService=true \ 29 | --set rbac.enabled=true 30 | {{< /tab >}} 31 | {{< tab name="Helm 3" codelang="bash" >}} 32 | helm install traefik stable/traefik \ 33 | --namespace kube-system \ 34 | --set kubernetes.ingressClass=traefik \ 35 | --set kubernetes.ingressEndpoint.useDefaultPublishedService=true \ 36 | --set rbac.enabled=true 37 | {{< /tab >}}} 38 | {{< /tabs >}} 39 | 40 | * `kubernetes.ingressClass=traefik`: 创建的 ingress 中包含 `kubernetes.io/ingress.class` 这个 annotation 并且值与这里配置的一致,这个 traefik ingress controller 才会处理 (生成转发规则) 41 | * `kubernetes.ingressEndpoint.useDefaultPublishedService=true`: 这个置为 true 主要是为了让 ingress 的外部地址正确显示 (显示为负载均衡器的地址),因为如果不配置这个,默认情况下会将 ingress controller 所有实例的节点 ip 写到 ingress 的 address 列表里,使用 `kubectl get ingress` 查看时会很丑陋并且容易误解。 42 | * `rbac.enabled` 默认为 false,如果没有事先给 default 的 service account 绑足够权限就会报错,通常置为 true,自动创建 rbac 规则 43 | 44 | 如果喜欢尝鲜,可以装 traefik v2.x 版本的 chart: 45 | 46 | {{< tabs name="traefik-v2" >}} 47 | {{{< tab name="Helm 2" codelang="bash" >}} 48 | git clone --depth 1 https://github.com/containous/traefik-helm-chart.git 49 | kubectl apply -f traefik-helm-chart/traefik/crds 50 | helm repo add traefik https://containous.github.io/traefik-helm-chart 51 | helm repo update 52 | helm install traefik/traefik \ 53 | --name traefik \ 54 | --namespace kube-system 55 | {{< /tab >}} 56 | {{< tab name="Helm 3" codelang="bash" >}} 57 | helm repo add traefik https://containous.github.io/traefik-helm-chart 58 | helm repo update 59 | helm install traefik traefik/traefik \ 60 | --namespace kube-system \ 61 | --set ports.web.port=80 \ 62 | --set ports.websecure.port=443 \ 63 | --set="additionalArguments={--providers.kubernetesingress}" 64 | {{< /tab >}}} 65 | {{< /tabs >}} 66 | 67 | ## 参考资料 68 | 69 | * Github 主页: https://github.com/containous/traefik 70 | * helm hub 主页: https://hub.helm.sh/charts/stable/traefik 71 | * 官方文档: https://docs.traefik.io 72 | -------------------------------------------------------------------------------- /content/zh/ingress/traefik-ingress/use-traefik-to-support-login.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "使用 Traefik V2 让你的后台管理页面支持登录" 3 | weight: 30 4 | --- 5 | 6 | 我们安装的应用,它们的后台管理页面很多不支持用户登录(认证),所以没有将其暴露出来对外提供访问,我们经常用 kubectl proxy 的方式来访问,这样比较不方便。 7 | 8 | 这里以 Traefik 本身的后台管理页面为例,将其加上 basic auth 登录才允许访问。 9 | 10 | 创建用户名密码: 11 | 12 | ``` bash 13 | USERNAME=roc 14 | PASSWORD=mypassword 15 | ``` 16 | 17 | 创建 Secret: 18 | 19 | ``` bash 20 | USERS=$(htpasswd -nb USERNAME PASSWORD) 21 | cat < 参考官方文档: https://github.com/grafana/loki/blob/master/docs/installation/helm.md 21 | 22 | `loki/loki-stack` 这个 chart 包含 loki stack 涉及的各个组件: 23 | 24 | * loki: 以 Statefulset 方式部署,可横向扩容 25 | * promtail: 以 Daemonset 方式部署,采集每个节点上容器日志并发送给 loki 26 | * grafana: 默认不开启,如果集群中已经有 grafana 就可以不用在部署 grafana,如果没有,部署时可以选择也同时部署 grafana 27 | 28 | 首先添加 repo: 29 | 30 | ``` bash 31 | helm repo add loki https://grafana.github.io/loki/charts 32 | helm repo update 33 | ``` 34 | 35 | 执行安装: 36 | 37 | {{< tabs name="tab_with_code" >}} 38 | {{{< tab name="Helm 2" codelang="bash" >}} 39 | helm upgrade --install loki loki/loki-stack 40 | # 安装到指定命名空间 41 | # helm upgrade --install loki loki/loki-stack -n monitoring 42 | # 持久化 loki 的数据,避免 loki 重启后数据丢失 43 | # helm upgrade --install loki loki/loki-stack --set="loki.persistence.enabled=ture,loki.persistence.size=100G" 44 | # 部署 grafana 45 | # helm upgrade --install loki loki/loki-stack --set="grafana=true" 46 | {{< /tab >}} 47 | {{< tab name="Helm 3" codelang="bash" >}} 48 | helm install loki loki/loki-stack 49 | # 安装到指定命名空间 50 | # helm install loki loki/loki-stack -n monitoring 51 | # 持久化 loki 的数据,避免 loki 重启后数据丢失 52 | # helm install loki loki/loki-stack --set="loki.persistence.enabled=ture,loki.persistence.size=100G" 53 | # 部署 grafana 54 | # helm install loki loki/loki-stack --set="grafana.enabled=true" 55 | {{< /tab >}}} 56 | {{< /tabs >}} 57 | 58 | 进入 grafana 界面,添加 loki 作为数据源:Configuration-Data Sources-Add data source-Loki,然后填入 loki 在集群中的地址,比如: http://loki.monitoring.svc.cluster.local:3100 59 | 60 | ![](/images/loki-grafana-data-source.png) 61 | 62 | 数据源添加好了,我们就可以开始查询分析日志了,点击 `Explore`,下拉选择 loki 作为数据源,切到 `Logs` 模式(不用 `Metrics` 模式),在 `Log labels` 按钮那里就能通过 label 筛选日志了。 63 | 64 | ![](/images/loki-log.png) 65 | 66 | 更多用法请参考 [官方文档](https://github.com/grafana/loki/tree/master/docs) 67 | -------------------------------------------------------------------------------- /content/zh/metrics/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Metrics 指南" 3 | weight: 13 4 | state: TODO 5 | --- 6 | -------------------------------------------------------------------------------- /content/zh/monitoring/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "监控指南" 3 | weight: 43 4 | state: Alpha 5 | chapter: true 6 | --- 7 | 8 | 由浅入深,平滑学习曲线让你快速掌握云原生监控系统的搭建与使用。 9 | 10 | {{% children depth=2 %}} 11 | -------------------------------------------------------------------------------- /content/zh/monitoring/alerting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "告警" 3 | hidden: true 4 | --- 5 | 6 | 先将 alertmanager 配置文件 dump 下来: 7 | 8 | ``` bash 9 | kubectl -n monitoring get secret alertmanager-main -o jsonpath='{.data.alertmanager\.yaml}' | base64 -d > alertmanager-main.yaml 10 | ``` 11 | 12 | -------------------------------------------------------------------------------- /content/zh/monitoring/build-cloud-native-large-scale-distributed-monitoring-system/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "打造云原生大型分布式监控系统" 3 | weight: 1000 4 | state: Alpha 5 | chapter: false 6 | --- 7 | 8 | {{% children depth=2 %}} 9 | -------------------------------------------------------------------------------- /content/zh/monitoring/build-cloud-native-large-scale-distributed-monitoring-system/thanos-arch.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Thanos 架构详解" 3 | weight: 20 4 | date: 2020-03-30 5 | --- 6 | 7 | ## 概述 8 | 9 | 之前在 [大规模场景下 Prometheus 的优化手段](../optimize-prometheus-in-large-scale.md) 中,我们想尽 "千方百计" 才好不容易把 Prometheus 优化到适配大规模场景,部署和后期维护麻烦且复杂不说,还有很多不完美的地方,并且还无法满足一些更高级的诉求,比如查看时间久远的监控数据,对于一些时间久远不常用的 "冷数据",最理想的方式就是存到廉价的对象存储中,等需要查询的时候能够自动加载出来。 10 | 11 | Thanos (没错,就是灭霸) 可以帮我们简化分布式 Prometheus 的部署与管理,并提供了一些的高级特性:**全局视图**,**长期存储**,**高可用**。下面我们来详细讲解一下。 12 | 13 | ## Thanos 架构 14 | 15 | 这是官方给出的架构图: 16 | 17 | ![](https://imroc.io/assets/blog/thanos-arch.jpg) 18 | 19 | 这张图中包含了 Thanos 的几个核心组件,但并不包括所有组件,为了便于理解,我们先不细讲,简单介绍下图中这几个组件的作用: 20 | 21 | * Thanos Query: 实现了 Prometheus API,将来自下游组件提供的数据进行聚合最终返回给查询数据的 client (如 grafana),类似数据库中间件。 22 | * Thanos Sidecar: 连接 Prometheus,将其数据提供给 Thanos Query 查询,并且/或者将其上传到对象存储,以供长期存储。 23 | * Thanos Store Gateway: 将对象存储的数据暴露给 Thanos Query 去查询。 24 | * Thanos Ruler: 对监控数据进行评估和告警,还可以计算出新的监控数据,将这些新数据提供给 Thanos Query 查询并且/或者上传到对象存储,以供长期存储。 25 | * Thanos Compact: 将对象存储中的数据进行压缩和降低采样率,加速大时间区间监控数据查询的速度。 26 | 27 | ## 架构设计剖析 28 | 29 | 如何理解 Thanos 的架构设计的?我们可以自己先 YY 一下,要是自己来设计一个分布式 Prometheus 管理应用,会怎么做? 30 | 31 | ### Query 与 Sidecar 32 | 33 | 首先,监控数据的查询肯定不能直接查 Prometheus 了,因为会存在许多个 Prometheus 实例,每个 Prometheus 实例只能感知它自己所采集的数据。我们可以比较容易联想到数据库中间件,每个数据库都只存了一部分数据,中间件能感知到所有数据库,数据查询都经过数据库中间件来查,这个中间件收到查询请求再去查下游各个数据库中的数据,最后将这些数据聚合汇总返回给查询的客户端,这样就实现了将分布式存储的数据集中查询。 34 | 35 | 实际上,Thanos 也是使用了类似的设计思想,Thanos Query 就是这个 "中间件" 的关键入口。它实现了 Prometheus 的 HTTP API,能够 "看懂" PromQL。这样,查询 Prometheus 监控数据的 client 就不直接查询 Prometheus 本身了,而是去查询 Thanos Query,Thanos Query 再去下游多个存储了数据的地方查数据,最后将这些数据聚合去重后返回给 client,也就实现了分布式 Prometheus 的数据查询。 36 | 37 | 那么 Thanos Query 又如何去查下游分散的数据呢?Thanos 为此抽象了一套叫 Store API 的内部 gRPC 接口,其它一些组件通过这个接口来暴露数据给 Thanos Query,它自身也就可以做到完全无状态部署,实现高可用与动态扩展。 38 | 39 | ![](https://imroc.io/assets/blog/thanos-querier.svg) 40 | 41 | 这些分散的数据可能来自哪些地方呢?首先,Prometheus 会将采集的数据存到本机磁盘上,如果我们直接用这些分散在各个磁盘上的数据,可以给每个 Prometheus 附带部署一个 Sidecar,这个 Sidecar 实现 Thanos Store API,当 Thanos Query 对其发起查询时,Sidecar 就读取跟它绑定部署的 Prometheus 实例上的监控数据返回给 Thanos Query。 42 | 43 | ![](https://imroc.io/assets/blog/thanos-sidecar.png) 44 | 45 | 由于 Thanos Query 可以对数据进行聚合与去重,所以可以很轻松实现高可用:相同的 Prometheus 部署多个副本(都附带 Sidecar),然后 Thanos Query 去所有 Sidecar 查数据,即便有一个 Prometheus 实例挂掉过一段时间,数据聚合与去重后仍然能得到完整数据。 46 | 47 | 这种高可用做法还弥补了我们上篇文章中用负载均衡去实现 Prometheus 高可用方法的缺陷:如果其中一个 Prometheus 实例挂了一段时间然后又恢复了,它的数据就不完整,当负载均衡转发到它上面去查数据时,返回的结果就可能会有部分缺失。 48 | 49 | 不过因为磁盘空间有限,所以 Prometheus 存储监控数据的能力也是有限的,通常会给 Prometheus 设置一个数据过期时间 (默认15天) 或者最大数据量大小,不断清理旧数据以保证磁盘不被撑爆。因此,我们无法看到时间比较久远的监控数据,有时候这也给我们的问题排查和数据统计造成一些困难。 50 | 51 | 对于需要长期存储的数据,并且使用频率不那么高,最理想的方式是存进对象存储,各大云厂商都有对象存储服务,特点是不限制容量,价格非常便宜。 52 | 53 | Thanos 有几个组件都支持将数据上传到各种对象存储以供长期保存 (Prometheus TSDB 数据格式),比如我们刚刚说的 Sidecar: 54 | 55 | ![](https://imroc.io/assets/blog/thanos-sidecar-with-objectstore.png) 56 | 57 | ### Store Gateway 58 | 59 | 那么这些被上传到了对象存储里的监控数据该如何查询呢?理论上 Thanos Query 也可以直接去对象存储查,但会让 Thanos Query 的逻辑变的很重。我们刚才也看到了,Thanos 抽象出了 Store API,只要实现了该接口的组件都可以作为 Thanos Query 查询的数据源,Thanos Store Gateway 这个组件也实现了 Store API,向 Thanos Query 暴露对象存储的数据。Thanos Store Gateway 内部还做了一些加速数据获取的优化逻辑,一是缓存了 TSDB 索引,二是优化了对象存储的请求 (用尽可能少的请求量拿到所有需要的数据)。 60 | 61 | ![](https://imroc.io/assets/blog/thanos-store-gateway.png) 62 | 63 | 这样就实现了监控数据的长期储存,由于对象存储容量无限,所以理论上我们可以存任意时长的数据,监控历史数据也就变得可追溯查询,便于问题排查与统计分析。 64 | 65 | ### Ruler 66 | 67 | 有一个问题,Prometheus 不仅仅只支持将采集的数据进行存储和查询的功能,还可以配置一些 rules: 68 | 69 | 1. 根据配置不断计算出新指标数据并存储,后续查询时直接使用计算好的新指标,这样可以减轻查询时的计算压力,加快查询速度。 70 | 2. 不断计算和评估是否达到告警阀值,当达到阀值时就通知 AlertManager 来触发告警。 71 | 72 | 由于我们将 Prometheus 进行分布式部署,每个 Prometheus 实例本地并没有完整数据,有些有关联的数据可能存在多个 Prometheus 实例中,单机 Prometheus 看不到数据的全局视图,这种情况我们就不能依赖 Prometheus 来做这些工作,Thanos Ruler 应运而生,它通过查询 Thanos Query 获取全局数据,然后根据 rules 配置计算新指标并存储,同时也通过 Store API 将数据暴露给 Thanos Query,同样还可以将数据上传到对象存储以供长期保存 (这里上传到对象存储中的数据一样也是通过 Thanos Store Gateway 暴露给 Thanos Query)。 73 | 74 | ![](https://imroc.io/assets/blog/thanos-ruler.png) 75 | 76 | 看起来 Thanos Query 跟 Thanos Ruler 之间会相互查询,不过这个不冲突,Thanos Ruler 为 Thanos Query 提供计算出的新指标数据,而 Thanos Query 为 Thanos Ruler 提供计算新指标所需要的全局原始指标数据。 77 | 78 | 至此,Thanos 的核心能力基本实现了,完全兼容 Prometheus 的情况下提供数据查询的全局视图,高可用以及数据的长期保存。 79 | 80 | 看下还可以怎么进一步做下优化呢? 81 | 82 | ### Compact 83 | 84 | 由于我们有数据长期存储的能力,也就可以实现查询较大时间范围的监控数据,当时间范围很大时,查询的数据量也会很大,这会导致查询速度非常慢。通常在查看较大时间范围的监控数据时,我们并不需要那么详细的数据,只需要看到大致就行。Thanos Compact 这个组件应运而生,它读取对象存储的数据,对其进行压缩以及降采样再上传到对象存储,这样在查询大时间范围数据时就可以只读取压缩和降采样后的数据,极大地减少了查询的数据量,从而加速查询。 85 | 86 | ![](https://imroc.io/assets/blog/thanos-compact.png) 87 | 88 | ### 再看架构图 89 | 90 | 上面我们剖析了官方架构图中各个组件的设计,现在再来回味一下这张图: 91 | 92 | ![](https://imroc.io/assets/blog/thanos-arch.jpg) 93 | 94 | 理解是否更加深刻了? 95 | 96 | 另外还有 Thanos Bucket 和 Thanos Checker 两个辅助性的工具组件没画出来,它们不是核心组件,这里也就不再赘述。 97 | 98 | ## Sidecar 模式与 Receiver 模式 99 | 100 | 前面我们理解了官方的架构图,但其中还缺失一个核心组件 Thanos Receiver,因为它是一个还未完全发布的组件。这是它的设计文档: https://thanos.io/proposals/201812_thanos-remote-receive.md/ 101 | 102 | 这个组件可以完全消除 Sidecar,所以 Thanos 实际有两种架构图,只是因为没有完全发布,官方的架构图只给的 Sidecar 模式。 103 | 104 | Receiver 是做什么的呢?为什么需要 Receiver?它跟 Sidecar 有什么区别? 105 | 106 | 它们都可以将数据上传到对象存储以供长期保存,区别在于最新数据的存储。 107 | 108 | 由于数据上传不可能实时,Sidecar 模式将最新的监控数据存到 Prometheus 本机,Query 通过调所有 Sidecar 的 Store API 来获取最新数据,这就成一个问题:如果 Sidecar 数量非常多或者 Sidecar 跟 Query 离的比较远,每次查询 Query 都调所有 Sidecar 会消耗很多资源,并且速度很慢,而我们查看监控大多数情况都是看的最新数据。 109 | 110 | 为了解决这个问题,Thanos Receiver 组件被提出,它适配了 Prometheus 的 remote write API,也就是所有 Prometheus 实例可以实时将数据 push 到 Thanos Receiver,最新数据也得以集中起来,然后 Thanos Query 也不用去所有 Sidecar 查最新数据了,直接查 Thanos Receiver 即可。另外,Thanos Receiver 也将数据上传到对象存储以供长期保存,当然,对象存储中的数据同样由 Thanos Store Gateway 暴露给 Thanos Query。 111 | 112 | ![](https://imroc.io/assets/blog/thanos-receiver.png) 113 | 114 | 有同学可能会问:如果规模很大,Receiver 压力会不会很大,成为性能瓶颈?当然设计这个组件时肯定会考虑这个问题,Receiver 实现了一致性哈希,支持集群部署,所以即使规模很大也不会成为性能瓶颈。 115 | 116 | ## 总结 117 | 118 | 本文详细讲解了 Thanos 的架构设计,各个组件的作用以及为什么要这么设计。如果仔细看完,我相信你已经 get 到了 Thanos 的精髓,不过我们还没开始讲如何部署与实践,实际上在腾讯云容器服务的多个产品的内部监控已经在使用 Thanos 了,比如 [TKE](https://cloud.tencent.com/product/tke) (公有云 k8s)、[TKEStack](https://github.com/tkestack/tke) (私有云 k8s)、[EKS](https://console.cloud.tencent.com/tke2/ecluster) (Serverless k8s)。 下一篇我们将介绍 Thanos 的部署与最佳实践,敬请期待。 119 | -------------------------------------------------------------------------------- /content/zh/monitoring/dingtalk-alert.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "钉钉告警" 3 | weight: 40 4 | state: TODO 5 | draft: true 6 | --- 7 | 8 | ## 通过 webhook 告警 9 | 10 | 参考: https://theo.im/blog/2017/10/16/release-prometheus-alertmanager-webhook-for-dingtalk/ 11 | 12 | github: https://github.com/timonwong/prometheus-webhook-dingtalk 13 | -------------------------------------------------------------------------------- /content/zh/monitoring/kube-prometheus-quickstart.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "使用 kube-promethues 快速上手集群监控" 3 | weight: 10 4 | state: Alpha 5 | --- 6 | 7 | ## kube-prometheus 介绍 8 | 9 | kube-prometheus 包含了在 k8s 环境下各种主流的监控组件,将其安装到集群可以快速搭建我们自己的监控系统: 10 | 11 | * prometheus-operator: 让 prometheus 更好的适配 k8s,可直接通过创建 k8s CRD 资源来创建 prometheus 与 alertmanager 实例及其监控告警规则 (默认安装时也会创建这些 CRD 资源,也就是会自动部署 prometheus 和 alertmanager,以及它们的配置) 12 | * prometheus-adapter: 让 prometheus 采集的监控数据来适配 k8s 的 [resource metrics API](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/instrumentation/resource-metrics-api.md) 和 [custom metrics API](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/instrumentation/custom-metrics-api.md),`kubectl top` 和 HPA 功能都依赖它们。 13 | * node-exporter: 以 DaemonSet 方式部署在每个节点,将节点的各项系统指标暴露成 prometheus 能识别的格式,以便让 prometheus 采集。 14 | * kube-state-metrics: 将 k8s 的资源对象转换成 prometheus 的 metrics 格式以便让 prometheus 采集,比如 Node/Pod 的各种状态。 15 | * grafana: 可视化展示监控数据的界面。 16 | 17 | 项目地址: https://github.com/coreos/kube-prometheus 18 | 19 | ## 快速安装 20 | 21 | 如果只是想学习如何使用,可以参考 [官方文档](https://github.com/coreos/kube-prometheus#quickstart) 一键部署到集群: 22 | 23 | ``` bash 24 | git clone https://github.com/coreos/kube-prometheus.git 25 | cd kube-prometheus 26 | # Create the namespace and CRDs, and then wait for them to be availble before creating the remaining resources 27 | kubectl create -f manifests/setup 28 | until kubectl get servicemonitors --all-namespaces ; do date; sleep 1; echo ""; done 29 | kubectl create -f manifests/ 30 | ``` 31 | 32 | ## 进入 grafana 界面 33 | 34 | 你可以通过将 grafana 的 service 类型改为 NodePort 或 LoadBalancer,也可以用 Ingress 来暴露 grafana 的界面, 如果你本机能通过 kubectl 访问集群,那可以直接通过 `kubectl port-forward` 端口转发来暴露访问: 35 | 36 | ``` bash 37 | kubectl port-forward service/grafana 3000:3000 -n monitoring 38 | ``` 39 | 40 | 然后打开 http://localhost:3000 即可进入 grafana 的界面,初始用户名密码都是 admin,输入后会强制让改一下初始密码才允许进入。 41 | 42 | 因为 kube-prometheus 为我们预配置了指标采集规则和 grafana 的 dashboard 展示配置,所以进入 grafana 界面后,点击左上角即可选择预配置好的监控面板: 43 | 44 | ![](/images/grafana-select-dashboard.png?classes=no-margin) 45 | 46 | 选择一些看下效果探索下吧: 47 | 48 | ![](/images/grafana-dashboard-pod.png?classes=no-margin) 49 | -------------------------------------------------------------------------------- /content/zh/monitoring/promql.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "PromQL 技巧" 3 | weight: 2000 4 | --- 5 | 6 | ## 复制标签名 7 | 8 | pod_name --> pod: 9 | 10 | ``` promql 11 | label_replace( 12 | container_cpu_system_seconds_total, 13 | "pod", "$1", "pod_name", "(.*)" 14 | ) 15 | ``` 16 | 17 | 新标签名跟其它指标: 18 | 19 | ``` promql 20 | sum by (pod)( 21 | irate( 22 | ( 23 | label_replace( 24 | container_cpu_system_seconds_total{container_name!=""}, 25 | "pod", "$1", "pod_name", "(.*)" 26 | ) * on (namespace,pod) group_left(workload,workload_type) mixin_pod_workload{namespace="$namespace", workload=~"$workload", workload_type=~"$workload_type"} 27 | )[1m:15s] 28 | ) 29 | ) 30 | ``` 31 | -------------------------------------------------------------------------------- /content/zh/monitoring/wechat-work-alert.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "企业微信告警" 3 | weight: 30 4 | state: TODO 5 | draft: true 6 | --- 7 | 8 | ## 原生支持的企业微信告警 9 | 10 | 配置参考:https://prometheus.io/docs/alerting/configuration/#wechat_config 11 | 操作参考: https://songjiayang.gitbooks.io/prometheus/content/alertmanager/wechat.html 12 | 13 | ## 通过企业微信群机器人告警 14 | 15 | 计划开源一款适配 alertmanager 的 webhook 程序,调企业微信群机器人的 webhook 发送群消息告警,告警内容根据 alertmanager 传来的内容进行封装 16 | -------------------------------------------------------------------------------- /content/zh/reference/net-shell.md: -------------------------------------------------------------------------------- 1 | # 网络排障脚本 2 | 3 | ## 观察是否有 conntrack 冲突 4 | watch -n1 'conntrack -S | awk -F = "{print \$7}" | awk "{sum += \$1} END {print sum}"' -------------------------------------------------------------------------------- /content/zh/security/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "安全指南" 3 | weight: 40 4 | --- 5 | 6 | ## 目录 7 | 8 | {{% children depth=3 %}} -------------------------------------------------------------------------------- /content/zh/security/cert/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "集群证书管理" 3 | --- 4 | -------------------------------------------------------------------------------- /content/zh/security/cert/install-cert-manger.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "安装 cert-manager" 3 | weight: 10 4 | --- 5 | 6 | 参考官方文档: [https://docs.cert-manager.io/en/latest/getting-started/install/kubernetes.html](https://docs.cert-manager.io/en/latest/getting-started/install/kubernetes.html) 7 | 8 | 介绍几种安装方式,不管是用哪种我们都先规划一下使用哪个命名空间,推荐使用 `cert-manger` 命名空间,如果使用其它的命名空间需要做些更改,会稍微有点麻烦,先创建好命名空间: 9 | 10 | ```bash 11 | kubectl create namespace cert-manager 12 | ``` 13 | 14 | ## 使用原生 yaml 资源安装 15 | 16 | 直接执行 `kubectl apply` 来安装: 17 | 18 | ```bash 19 | kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.13.1/cert-manager.yaml 20 | ``` 21 | 22 | > 使用 `kubectl v1.15.4` 及其以下的版本需要加上 `--validate=false`,否则会报错。 23 | 24 | ## 校验是否安装成功 25 | 26 | 检查 cert-manager 相关的 pod 是否启动成功: 27 | 28 | ``` bash 29 | $ kubectl get pods --namespace cert-manager 30 | 31 | NAME READY STATUS RESTARTS AGE 32 | cert-manager-5c6866597-zw7kh 1/1 Running 0 2m 33 | cert-manager-cainjector-577f6d9fd7-tr77l 1/1 Running 0 2m 34 | cert-manager-webhook-787858fcdb-nlzsq 1/1 Running 0 2m 35 | ``` 36 | -------------------------------------------------------------------------------- /content/zh/security/permission/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "集群权限控制" 3 | --- 4 | 5 | ## 账户类型 6 | 7 | K8S 主要有以下两种账户类型概念: 8 | 9 | * 用户账户 \(`User`\): 控制人的权限。 10 | * 服务账户 \(`ServiceAccount`\): 控制应用程序的权限 11 | 12 | 如果开启集群审计,就可以区分某个操作是哪个用户或哪个应程序执行的。 13 | -------------------------------------------------------------------------------- /content/zh/security/permission/app.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "控制应用权限" 3 | --- 4 | 5 | 不仅用户 (人) 可以操作集群,应用 (程序) 也可以操作集群,通过给 Pod 设置 Serivce Account 来对应用进行授权,如果不设置会默认配置一个 "default" 的 Service Account,几乎没有权限。 6 | 7 | ## 原理 8 | 9 | 创建 Pod 时,在 apiserver 中的 service account admission controller 检测 Pod 是否指定了 ServiceAccount,如果没有就自动设置一个 "default",如果指定了会检测指定的 ServiceAccount 是否存在,不存在的话会拒绝该 Pod,存在话就将此 ServiceAccount 对应的 Secret 挂载到 Pod 中每个容器的 `/var/run/secrets/kubernetes.io/serviceaccount` 这个路径,这个 Secret 是 controller manager 中 token controller 去 watch ServiceAccount,为每个 ServiceAccount 生成对应的 token 类型的 Secret 得来的。 10 | 11 | Pod 内的程序如果要调用 apiserver 接口操作集群,会使用 SDK,通常是 [client-go](https://github.com/kubernetes/client-go) , SDK 使用 in-cluster 的方式调用 apiserver,从固定路径 `/var/run/secrets/kubernetes.io/serviceaccount` 读取认证配置信息去连 apiserver,从而实现认证,再结合 RBAC 配置可以实现权限控制。 12 | 13 | ## 使用 RBAC 细化应用权限 14 | 15 | ServiceAccount 仅针对某个命名空间,所以 Pod 指定的 ServiceAccount 只能引用当前命名空间的 ServiceAccount 的,即便是 "default" 每个命名空间也都是相互独立的,下面给出几个 RBAC 定义示例。 16 | 17 | `build-robot` 这个 ServiceAccount 可以读取 build 命名空间中 Pod 的信息和 log: 18 | 19 | ``` yaml 20 | apiVersion: v1 21 | kind: ServiceAccount 22 | metadata: 23 | name: build-robot 24 | namespace: build 25 | 26 | --- 27 | 28 | apiVersion: rbac.authorization.k8s.io/v1 29 | kind: Role 30 | metadata: 31 | namespace: build 32 | name: pod-reader 33 | rules: 34 | - apiGroups: [""] 35 | resources: ["pods", "pods/log"] 36 | verbs: ["get", "list"] 37 | 38 | --- 39 | 40 | apiVersion: rbac.authorization.k8s.io/v1 41 | kind: RoleBinding 42 | metadata: 43 | name: read-pods 44 | namespace: build 45 | subjects: 46 | - kind: ServiceAccount 47 | name: build-robot 48 | apiGroup: rbac.authorization.k8s.io 49 | roleRef: 50 | kind: Role 51 | name: pod-reader 52 | apiGroup: rbac.authorization.k8s.io 53 | ``` 54 | 55 | ## 为 Pod 指定 ServiceAccount 56 | 57 | 示例: 58 | 59 | ``` yaml 60 | apiVersion: v1 61 | kind: Pod 62 | metadata: 63 | name: build 64 | namespace: build 65 | spec: 66 | containers: 67 | - image: imroc/build-robot:v1 68 | name: builder 69 | serviceAccountName: build-robot 70 | ``` 71 | 72 | ## 为应用默认指定 imagePullSecrets 73 | 74 | ServiceAccount 中也可以指定 imagePullSecrets,也就是只要给 Pod 指定了这个 ServiceAccount,就有对应的 imagePullSecrets,而如果不指定 ServiceAccount 会默认指定 "default",我们可以给 "default" 这个 ServiceAccount 指定 imagePullSecrets 来实现给某个命名空间指定默认的 imagePullSecrets 75 | 76 | 创建 imagePullSecrets: 77 | 78 | ``` bash 79 | kubectl create secret docker-registry --docker-server= --docker-username= --docker-password= --docker-email= -n 80 | ``` 81 | 82 | * ``: 是要创建的 imagePullSecrets 的名称 83 | * ``: 是要创建的 imagePullSecrets 所在命名空间 84 | * ``: 是你的私有仓库的地址 85 | * ``: 是你的 Docker 用户名 86 | * `` 是你的 Docker 密码 87 | * `` 是你的 Docker 邮箱 88 | 89 | 指定默认 imagePullSecrets: 90 | 91 | ``` bash 92 | kubectl patch serviceaccount default -p '{"imagePullSecrets": [{"name": ""}]}' -n 93 | ``` 94 | 95 | * ``: 是 ServiceAccount 要关联的 imagePullSecrets 的名称 96 | * ``: 是 ServiceAccount 所在的命名空间,跟 imagePullSecrets 在同一个命名空间 97 | 98 | ## 参考资料 99 | 100 | * Configure Service Accounts for Pods: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ 101 | -------------------------------------------------------------------------------- /content/zh/security/permission/user.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "控制用户权限" 3 | --- 4 | 5 | 为了简单方便,小集群或测试环境集群我们通常使用最高权限的 admin 账号,可以做任何操作,但是如果是重要的生产环境集群,可以操作集群的人比较多,如果这时还用这个账号可能就会比较危险,一旦有人误操作或故意搞事就可能酿成大错,即使 apiserver 开启审计也无法知道是谁做的操作,所以最好控制下权限,根据人的级别或角色创建拥有对应权限的账号,这个可以通过 RBAC 来实现\(确保 `kube-apiserver` 启动参数 `--authorization-mode=RBAC`\),基本思想是创建 User 或 ServiceAccount 绑定 Role 或 ClusterRole 来控制权限。 6 | 7 | ## User 来源 8 | 9 | User 的来源有多种: 10 | 11 | * token 文件: 给 `kube-apiserver` 启动参数 `--token-auth-file` 传一个 token 认证文件,比如: `--token-auth-file=/etc/kubernetes/known_tokens.csv` 12 | * token 文件每一行表示一个用户,示例: `wJmq****PPWj,admin,admin,system:masters` 13 | * 第一个字段是 token 的值,最后一个字段是用户组,token 认证用户名不重要,不会识别 14 | * 证书: 通过使用 CA 证书给用户签发证书,签发的证书中 `CN` 字段是用户名,`O` 是用户组 15 | 16 | ## 使用 RBAC 控制用户权限 17 | 18 | 下面给出几个 RBAC 定义示例。 19 | 20 | 给 roc 授权 test 命名空间所有权限,istio-system 命名空间的只读权限: 21 | 22 | ```yaml 23 | kind: Role 24 | apiVersion: rbac.authorization.k8s.io/v1 25 | metadata: 26 | name: admin 27 | namespace: test 28 | rules: 29 | - apiGroups: ["*"] 30 | resources: ["*"] 31 | verbs: ["*"] 32 | 33 | --- 34 | 35 | kind: RoleBinding 36 | apiVersion: rbac.authorization.k8s.io/v1 37 | metadata: 38 | name: admin-to-roc 39 | namespace: test 40 | subjects: 41 | - kind: User 42 | name: roc 43 | apiGroup: rbac.authorization.k8s.io 44 | roleRef: 45 | kind: Role 46 | name: admin 47 | apiGroup: rbac.authorization.k8s.io 48 | 49 | --- 50 | 51 | kind: Role 52 | apiVersion: rbac.authorization.k8s.io/v1 53 | metadata: 54 | name: readonly 55 | namespace: istio-system 56 | rules: 57 | - apiGroups: ["*"] 58 | resources: ["*"] 59 | verbs: ["get", "watch", "list"] 60 | 61 | --- 62 | kind: RoleBinding 63 | apiVersion: rbac.authorization.k8s.io/v1 64 | metadata: 65 | name: readonly-to-roc 66 | namespace: istio-system 67 | subjects: 68 | - kind: User 69 | name: roc 70 | apiGroup: rbac.authorization.k8s.io 71 | roleRef: 72 | kind: Role 73 | name: readonly 74 | apiGroup: rbac.authorization.k8s.io 75 | ``` 76 | 77 | 给 roc 授权整个集群的只读权限: 78 | 79 | ```yaml 80 | kind: ClusterRole 81 | apiVersion: rbac.authorization.k8s.io/v1 82 | metadata: 83 | name: readonly 84 | rules: 85 | - apiGroups: ["*"] 86 | resources: ["*"] 87 | verbs: ["get", "watch", "list"] 88 | 89 | --- 90 | kind: ClusterRoleBinding 91 | apiVersion: rbac.authorization.k8s.io/v1 92 | metadata: 93 | name: readonly-to-roc 94 | subjects: 95 | - kind: User 96 | name: roc 97 | apiGroup: rbac.authorization.k8s.io 98 | roleRef: 99 | kind: ClusterRole 100 | name: readonly 101 | apiGroup: rbac.authorization.k8s.io 102 | ``` 103 | 104 | 给 manager 用户组里所有用户授权 secret 读权限: 105 | 106 | ``` yaml 107 | apiVersion: rbac.authorization.k8s.io/v1 108 | kind: ClusterRole 109 | metadata: 110 | name: secret-reader 111 | rules: 112 | - apiGroups: [""] 113 | resources: ["secrets"] 114 | verbs: ["get", "watch", "list"] 115 | 116 | --- 117 | 118 | apiVersion: rbac.authorization.k8s.io/v1 119 | kind: ClusterRoleBinding 120 | metadata: 121 | name: read-secrets-global 122 | subjects: 123 | - kind: Group 124 | name: manager 125 | apiGroup: rbac.authorization.k8s.io 126 | roleRef: 127 | kind: ClusterRole 128 | name: secret-reader 129 | apiGroup: rbac.authorization.k8s.io 130 | ``` 131 | 132 | ## 配置 kubeconfig 133 | 134 | ```bash 135 | # 如果使用证书认证,使用下面命令配置用户认证信息 136 | kubectl config set-credentials --embed-certs=true --client-certificate= --client-key= 137 | 138 | # 如果使用 token 认证,使用下面命令配置用户认证信息 139 | # kubectl config set-credentials --token='' 140 | 141 | # 配置cluster entry 142 | kubectl config set-cluster --server= --certificate-authority= 143 | # 配置context entry 144 | kubectl config set-context --cluster= --user= 145 | # 配置当前使用的context 146 | kubectl config use-context 147 | # 查看 148 | kubectl config view 149 | ``` 150 | 151 | ## 参考资料 152 | 153 | * [https://kubernetes.io/zh/docs/reference/access-authn-authz/service-accounts-admin/](https://kubernetes.io/zh/docs/reference/access-authn-authz/service-accounts-admin/) 154 | * [https://kubernetes.io/docs/reference/access-authn-authz/rbac/](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) 155 | -------------------------------------------------------------------------------- /content/zh/security/user/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "用户管理" 3 | --- 4 | 5 | ## 目录 6 | 7 | {{% children depth=3 %}} -------------------------------------------------------------------------------- /content/zh/security/user/create-user-using-csr-api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "利用 CSR API 创建用户" 3 | --- 4 | 5 | k8s 支持 CSR API,通过创建 `CertificateSigningRequest` 资源就可以发起 CSR 请求,管理员审批通过之后 `kube-controller-manager` 就会为我们签发证书,确保 `kube-controller-manager` 配了根证书密钥对: 6 | 7 | ``` bash 8 | --cluster-signing-cert-file=/var/lib/kubernetes/ca.pem 9 | --cluster-signing-key-file=/var/lib/kubernetes/ca-key.pem 10 | ``` 11 | 12 | ## 创建步骤 13 | 14 | 我们用 cfssl 来创建 key 和 csr 文件,所以需要先安装 cfssl: 15 | 16 | ``` bash 17 | curl -L https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -o cfssl 18 | curl -L https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -o cfssljson 19 | curl -L https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 -o cfssl-certinfo 20 | 21 | chmod +x cfssl cfssljson cfssl-certinfo 22 | sudo mv cfssl cfssljson cfssl-certinfo /usr/local/bin/ 23 | ``` 24 | 25 | 指定要创建的用户名: 26 | 27 | ``` bash 28 | USERNAME="roc" 29 | ``` 30 | 31 | 再创建 key 和 csr 文件: 32 | 33 | ``` bash 34 | cat < ${USERNAME}.pem 83 | ``` 84 | 85 | 得到证书文件: 86 | 87 | ``` 88 | roc.pem 89 | ``` 90 | 91 | 至此,我们已经创建好了用户,用户的证书密钥对文件: 92 | 93 | ``` 94 | roc.pem 95 | roc-key.pem 96 | ``` 97 | 98 | ## 配置 kubeconfig 99 | 100 | ``` bash 101 | # 增加 user 102 | kubectl config set-credentials ${USERNAME} --embed-certs=true --client-certificate=${USERNAME}.pem --client-key=${USERNAME}-key.pem 103 | 104 | # 如果还没配 cluster,可以通过下面命令配一下 105 | kubectl config set-cluster --server= --certificate-authority= 106 | 107 | # 增加 context,绑定 cluster 和 user 108 | kubectl config set-context --cluster= --user=${USERNAME} 109 | 110 | # 使用刚增加的 context 111 | kubectl config use-context 112 | ``` 113 | 114 | ## 配置用户权限 115 | 116 | 我们可以用 RBAC 控制用户权限,参考 [使用 RBAC 控制用户权限](user.md#rbac) 117 | 118 | ## 参考资料 119 | 120 | * Manage TLS Certificates in a Cluster: https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster/ 121 | -------------------------------------------------------------------------------- /content/zh/trick/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "奇技淫巧" 3 | weight: 41 4 | --- 5 | 6 | {{% children %}} 7 | -------------------------------------------------------------------------------- /content/zh/trick/efficient-kubectl.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "kubectl 高效技巧" 3 | --- 4 | 5 | ## k 命令 6 | 7 | 是否有过因为使用 kubectl 经常需要重复输入命名空间而苦恼?是否觉得应该要有个记住命名空间的功能,自动记住上次使用的命名空间,不需要每次都输入?可惜没有这种功能,但是,本文会教你一个非常巧妙的方法完美帮你解决这个痛点。 8 | 9 | 将如下脚本粘贴到当前shell\(注册k命令到当前终端session\): 10 | 11 | ```bash 12 | function k() { 13 | cmdline=`HISTTIMEFORMAT="" history | awk '$2 == "kubectl" && (/-n/ || /--namespace/) {for(i=2;i<=NF;i++)printf("%s ",$i);print ""}' | tail -n 1` 14 | regs=('\-n [\w\-\d]+' '\-n=[\w\-\d]+' '\-\-namespace [\w\-\d]+' '\-\-namespace=[\w\-\d]+') 15 | for i in "${!regs[@]}"; do 16 | reg=${regs[i]} 17 | nsarg=`echo $cmdline | grep -o -P "$reg"` 18 | if [[ "$nsarg" == "" ]]; then 19 | continue 20 | fi 21 | cmd="kubectl $nsarg $@" 22 | echo "$cmd" 23 | $cmd 24 | return 25 | done 26 | cmd="kubectl $@" 27 | echo "$cmd" 28 | $cmd 29 | } 30 | ``` 31 | 32 | mac 用户可以使用 dash 的 snippets 功能快速将上面的函数粘贴,使用 `kk.` 作为触发键 \(dash snippets可以全局监听键盘输入,使用指定的输入作为触发而展开配置的内容,相当于是全局代码片段\),以后在某个终端想使用 `k` 的时候按下 `kk.` 就可以将 `k` 命令注册到当前终端,dash snippets 配置如图所示: 33 | 34 | ![kk](https://imroc.io/assets/blog/dash_kk.png) 35 | 36 | 将 `k` 当作 `kubectl` 来用,只是不需要输入命名空间,它会调用 kubectl 并自动加上上次使用的非默认的命名空间,如果想切换命名空间,再常规的使用一次 kubectl 就行,下面是示范: 37 | 38 | ![demo](https://imroc.io/assets/blog/k.gif) 39 | 40 | 哈哈,是否感觉可以少输入很多字符,提高 kubectl 使用效率了?这是目前我探索解决 kubectl 重复输入命名空间的最好方案,一开始是受 [fuck命令](https://github.com/nvbn/thefuck) 的启发,想用 go 语言开发个 k 命令,但是发现两个缺点: 41 | 42 | * 需要安装二进制才可以使用(对于需要在多个地方用kubectl管理多个集群的人来说实在太麻烦) 43 | * 如果当前 shell 默认没有将历史输入记录到 history 文件\( bash 的 history 文件默认是 `~/.bash_history`\),那么将无法准确知道上一次 kubectl 使用的哪个命名空间 44 | 45 | 这里解释下第二个缺点的原因:ssh 连上服务器会启动一个 shell 进程,通常是 bash,大多 bash 默认配置会实时将历史输入追加到 `~/.bash_history`里,所以开多个ssh使用history命令看到的历史输入是一样的,但有些默认不会实时记录历史到`~/.bash_history`,而是记在当前 shell 进程的内存中,在 shell 退出时才会写入到文件。这种情况新起的进程是无法知道当前 shell 的最近历史输入的,[fuck命令](https://github.com/nvbn/thefuck) 也不例外。 46 | 47 | 所以最完美的解决方案就是注册函数到当前shell来调用,配合 dash 的 snippets 功能可以实现快速注册,解决复制粘贴的麻烦 48 | 49 | -------------------------------------------------------------------------------- /content/zh/trick/perf-in-container.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "在容器内使用 perf" 3 | state: TODO 4 | --- 5 | 6 | ## 条件 7 | 8 | 运行 perf 主要会用到 `perf_event_open` 系统调用,要在容器内使用,需要满足以下条件: 9 | 1. 设置内核参数:`kernel.perf_event_paranoid = -1` 10 | 2. 把系统调用perf_event_open放到白名单中(获取dockerd 默认的seccomp profile,然后把系统调用perf_event_open从CAP_SYS_ADMIN移出放到白名单)或者设置seccomp=unconfined 11 | 而在k8s场景下,由于kubelet默认会给pod配置seccomp=unconfined的SecurityOpt选项,所以只需要满足第一个条件即可 12 | -------------------------------------------------------------------------------- /content/zh/trick/shell/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "实用命令与脚本" 3 | --- 4 | 5 | 目录: 6 | {{% children %}} -------------------------------------------------------------------------------- /content/zh/trick/shell/network.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "网络调试相关脚本" 3 | --- 4 | 5 | ## 进入容器 netns 6 | 7 | 粘贴脚本到命令行: 8 | 9 | ``` bash 10 | function e() { 11 | set -eu 12 | ns=${2-"default"} 13 | pod=`kubectl -n $ns describe pod $1 | grep -A10 "^Containers:" | grep -Eo 'docker://.*$' | head -n 1 | sed 's/docker:\/\/\(.*\)$/\1/'` 14 | pid=`docker inspect -f {{.State.Pid}} $pod` 15 | echo "entering pod netns for $ns/$1" 16 | cmd="nsenter -n --target $pid" 17 | echo $cmd 18 | $cmd 19 | } 20 | ``` 21 | 22 | 进入在当前节点上运行的某个 pod 的 netns: 23 | 24 | ``` bash 25 | # 进入 kube-system 命名空间下名为 metrics-server-6cf9685556-rclw5 的 pod 所在的 netns 26 | e metrics-server-6cf9685556-rclw5 kube-system 27 | ``` 28 | 29 | 进入 pod 的 netns 后就使用节点上的工具在该 netns 中做操作,比如用 `ip a` 查询网卡和ip、用 `ip route` 查询路由、用 tcpdump 抓容器内的包等。 30 | 31 | ## 不断尝试建立TCP连接测试网络连通性 32 | 33 | ``` bash 34 | while true; do echo "" | telnet 10.0.0.3 443; sleep 0.1; done 35 | ``` 36 | 37 | * `ctrl+c` 终止测试 38 | * 替换 `10.0.0.3` 与 `443` 为需要测试的 IP/域名 和端口 39 | -------------------------------------------------------------------------------- /content/zh/trick/shell/pod.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 相关脚本" 3 | --- 4 | 5 | 6 | ## 清理 Evicted 的 pod 7 | 8 | ``` bash 9 | kubectl get pod -o wide --all-namespaces | awk '{if($4=="Evicted"){cmd="kubectl -n "$1" delete pod "$2; system(cmd)}}' 10 | ``` 11 | 12 | ## 清理非 Running 的 pod 13 | 14 | ``` bash 15 | kubectl get pod -o wide --all-namespaces | awk '{if($4!="Running"){cmd="kubectl -n "$1" delete pod "$2; system(cmd)}}' 16 | ``` 17 | 18 | ## 升级镜像 19 | 20 | ``` bash 21 | NAMESPACE="kube-system" 22 | WORKLOAD_TYPE="daemonset" 23 | WORKLOAD_NAME="ip-masq-agent" 24 | CONTAINER_NAME="ip-masq-agent" 25 | IMAGE="ccr.ccs.tencentyun.com/library/ip-masq-agent:v2.5.0" 26 | ``` 27 | 28 | ``` bash 29 | kubectl -n $NAMESPACE patch $WORKLOAD_TYPE $WORKLOAD_NAME --patch '{"spec": {"template": {"spec": {"containers": [{"name": "$CONTAINER_NAME","image": "$IMAGE" }]}}}}' 30 | ``` -------------------------------------------------------------------------------- /content/zh/trick/yaml/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "yaml 片段" 3 | --- -------------------------------------------------------------------------------- /content/zh/trick/yaml/affnity.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "亲和与反亲和" 3 | --- 4 | 5 | 6 | ## 尽量调度到不同节点 7 | 8 | ``` yaml 9 | affinity: 10 | podAntiAffinity: 11 | preferredDuringSchedulingIgnoredDuringExecution: 12 | - weight: 100 13 | podAffinityTerm: 14 | labelSelector: 15 | matchExpressions: 16 | - key: k8s-app 17 | operator: In 18 | values: 19 | - kube-dns 20 | topologyKey: kubernetes.io/hostname 21 | ``` 22 | 23 | ## 必须调度到不同节点 24 | 25 | ``` yaml 26 | affinity: 27 | podAntiAffinity: 28 | requiredDuringSchedulingIgnoredDuringExecution: 29 | - weight: 100 30 | labelSelector: 31 | matchExpressions: 32 | - key: k8s-app 33 | operator: In 34 | values: 35 | - kube-dns 36 | topologyKey: kubernetes.io/hostname 37 | ``` 38 | 39 | ## 只调度到有指定 label 的节点 40 | 41 | ``` yaml 42 | affinity: 43 | nodeAffinity: 44 | requiredDuringSchedulingIgnoredDuringExecution: 45 | nodeSelectorTerms: 46 | matchExpressions: 47 | - key: ingress 48 | operator: In 49 | values: 50 | - true 51 | ``` -------------------------------------------------------------------------------- /content/zh/trick/yaml/deployment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Deployment" 3 | --- 4 | 5 | ## 最简单的 nginx 测试服务 6 | 7 | ``` yaml 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: nginx 12 | spec: 13 | replicas: 1 14 | selector: 15 | matchLabels: 16 | app: nginx 17 | template: 18 | metadata: 19 | labels: 20 | app: nginx 21 | spec: 22 | containers: 23 | - name: nginx 24 | image: nginx 25 | 26 | --- 27 | 28 | apiVersion: v1 29 | kind: Service 30 | metadata: 31 | name: nginx 32 | labels: 33 | app: nginx 34 | spec: 35 | type: ClusterIP 36 | ports: 37 | - port: 80 38 | protocol: TCP 39 | name: http 40 | selector: 41 | app: nginx 42 | ``` 43 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "排错指南" 3 | weight: 20 4 | 5 | --- 6 | 7 | kuernetes 问题定位手册(脑图):[https://www.processon.com/view/link/5e4662ade4b0d86ec4018e50](https://www.processon.com/view/link/5e4662ade4b0d86ec4018e50) 8 | 9 | ## 目录 10 | 11 | {{% children depth=3 %}} -------------------------------------------------------------------------------- /content/zh/troubleshooting/handle/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "处理实践" 3 | weight: 20 4 | --- 5 | 6 | {{% children %}} 7 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/handle/arp_cache-overflow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "arp_cache 溢出" 3 | --- 4 | 5 | ## 如何判断 arp_cache 溢出? 6 | 7 | 内核日志会有有下面的报错: 8 | 9 | ``` txt 10 | arp_cache: neighbor table overflow! 11 | ``` 12 | 13 | 查看当前 arp 记录数: 14 | 15 | ``` bash 16 | $ arp -an | wc -l 17 | 1335 18 | ``` 19 | 20 | 查看 arp gc 阀值: 21 | 22 | ``` bash 23 | $ sysctl -a | grep gc_thresh 24 | net.ipv4.neigh.default.gc_thresh1 = 128 25 | net.ipv4.neigh.default.gc_thresh2 = 512 26 | net.ipv4.neigh.default.gc_thresh3 = 1024 27 | net.ipv6.neigh.default.gc_thresh1 = 128 28 | net.ipv6.neigh.default.gc_thresh2 = 512 29 | net.ipv6.neigh.default.gc_thresh3 = 1024 30 | ``` 31 | 32 | 当前 arp 记录数接近 `gc_thresh3` 比较容易 overflow,因为当 arp 记录达到 `gc_thresh3` 时会强制触发 gc 清理,当这时又有数据包要发送,并且根据目的 IP 在 arp cache 中没找到 mac 地址,这时会判断当前 arp cache 记录数加 1 是否大于 `gc_thresh3`,如果没有大于就会 时就会报错: `arp_cache: neighbor table overflow!` 33 | 34 | ## 解决方案 35 | 36 | 调整节点内核参数,将 arp cache 的 gc 阀值调高 (`/etc/sysctl.conf`): 37 | 38 | ``` bash 39 | net.ipv4.neigh.default.gc_thresh1 = 80000 40 | net.ipv4.neigh.default.gc_thresh2 = 90000 41 | net.ipv4.neigh.default.gc_thresh3 = 100000 42 | ``` 43 | 44 | 分析是否只是部分业务的 Pod 的使用场景需要节点有比较大的 arp 缓存空间。 45 | 46 | 如果不是,就需要调整所有节点内核参数。 47 | 48 | 如果是,可以将部分 Node 打上标签,比如: 49 | 50 | ``` bash 51 | kubectl label node host1 arp_cache=large 52 | ``` 53 | 54 | 然后用 nodeSelector 或 nodeAffnity 让这部分需要内核有大 arp_cache 容量的 Pod 只调度到这部分节点,推荐使用 nodeAffnity,yaml 示例: 55 | 56 | ``` yaml 57 | template: 58 | spec: 59 | affinity: 60 | nodeAffinity: 61 | requiredDuringSchedulingIgnoredDuringExecution: 62 | nodeSelectorTerms: 63 | - matchExpressions: 64 | - key: arp_cache 65 | operator: In 66 | values: 67 | - large 68 | ``` 69 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/handle/disk-full.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "磁盘爆满" 3 | --- 4 | 5 | ## 什么情况下磁盘可能会爆满 ? 6 | 7 | kubelet 有 gc 和驱逐机制,通过 `--image-gc-high-threshold`, `--image-gc-low-threshold`, `--eviction-hard`, `--eviction-soft`, `--eviction-minimum-reclaim` 等参数控制 kubelet 的 gc 和驱逐策略来释放磁盘空间,如果配置正确的情况下,磁盘一般不会爆满。 8 | 9 | 通常导致爆满的原因可能是配置不正确或者节点上有其它非 K8S 管理的进程在不断写数据到磁盘占用大量空间导致磁盘爆满。 10 | 11 | ## 磁盘爆满会有什么影响 ? 12 | 13 | 影响 K8S 运行我们主要关注 kubelet 和容器运行时这两个最关键的组件,它们所使用的目录通常不一样,kubelet 一般不会单独挂盘,直接使用系统磁盘,因为通常占用空间不会很大,容器运行时单独挂盘的场景比较多,当磁盘爆满的时候我们也要看 kubelet 和 容器运行时使用的目录是否在这个磁盘,通过 `df` 命令可以查看磁盘挂载点。 14 | 15 | ### 容器运行时使用的目录所在磁盘爆满 16 | 17 | 如果容器运行时使用的目录所在磁盘空间爆满,可能会造成容器运行时无响应,比如 docker,执行 docker 相关的命令一直 hang 住, kubelet 日志也可以看到 PLEG unhealthy,因为 CRI 调用 timeout,当然也就无法创建或销毁容器,通常表现是 Pod 一直 ContainerCreating 或 一直 Terminating。 18 | 19 | docker 默认使用的目录主要有: 20 | 21 | * `/var/run/docker`: 用于存储容器运行状态,通过 dockerd 的 `--exec-root` 参数指定。 22 | * `/var/lib/docker`: 用于持久化容器相关的数据,比如容器镜像、容器可写层数据、容器标准日志输出、通过 docker 创建的 volume 等 23 | 24 | Pod 启动可能报类似下面的事件: 25 | 26 | ``` txt 27 | Warning FailedCreatePodSandBox 53m kubelet, 172.22.0.44 Failed create pod sandbox: rpc error: code = DeadlineExceeded desc = context deadline exceeded 28 | ``` 29 | 30 | ``` txt 31 | Warning FailedCreatePodSandBox 2m (x4307 over 16h) kubelet, 10.179.80.31 (combined from similar events): Failed create pod sandbox: rpc error: code = Unknown desc = failed to create a sandbox for pod "apigateway-6dc48bf8b6-l8xrw": Error response from daemon: mkdir /var/lib/docker/aufs/mnt/1f09d6c1c9f24e8daaea5bf33a4230de7dbc758e3b22785e8ee21e3e3d921214-init: no space left on device 32 | ``` 33 | 34 | ``` txt 35 | Warning Failed 5m1s (x3397 over 17h) kubelet, ip-10-0-151-35.us-west-2.compute.internal (combined from similar events): Error: container create failed: container_linux.go:336: starting container process caused "process_linux.go:399: container init caused \"rootfs_linux.go:58: mounting \\\"/sys\\\" to rootfs \\\"/var/lib/dockerd/storage/overlay/051e985771cc69f3f699895a1dada9ef6483e912b46a99e004af7bb4852183eb/merged\\\" at \\\"/var/lib/dockerd/storage/overlay/051e985771cc69f3f699895a1dada9ef6483e912b46a99e004af7bb4852183eb/merged/sys\\\" caused \\\"no space left on device\\\"\"" 36 | ``` 37 | 38 | Pod 删除可能报类似下面的事件: 39 | 40 | ``` txt 41 | Normal Killing 39s (x735 over 15h) kubelet, 10.179.80.31 Killing container with id docker://apigateway:Need to kill Pod 42 | ``` 43 | 44 | ### kubelet 使用的目录所在磁盘爆满 45 | 46 | 如果 kubelet 使用的目录所在磁盘空间爆满(通常是系统盘),新建 Pod 时连 Sandbox 都无法创建成功,因为 mkdir 将会失败,通常会有类似这样的 Pod 事件: 47 | 48 | ``` txt 49 | Warning UnexpectedAdmissionError 44m kubelet, 172.22.0.44 Update plugin resources failed due to failed to write checkpoint file "kubelet_internal_checkpoint": write /var/lib/kubelet/device-plugins/.728425055: no space left on device, which is unexpected. 50 | ``` 51 | 52 | kubelet 默认使用的目录是 `/var/lib/kubelet`, 用于存储插件信息、Pod 相关的状态以及挂载的 volume (比如 `emptyDir`, `ConfigMap`, `Secret`),通过 kubelet 的 `--root-dir` 参数指定。 53 | 54 | ## 如何分析磁盘占用 ? 55 | 56 | * 如果运行时使用的是 Docker,请参考本书 排错技巧: 分析 Docker 磁盘占用 (TODO) 57 | 58 | ## 如何恢复 ? 59 | 60 | 如果容器运行时使用的 Docker,我们无法直接重启 dockerd 来释放一些空间,因为磁盘爆满后 dockerd 无法正常响应,停止的时候也会卡住。我们需要先手动清理一点文件腾出空间好让 dockerd 能够停止并重启。 61 | 62 | 可以手动删除一些 docker 的 log 文件或可写层文件,通常删除 log: 63 | 64 | ``` bash 65 | $ cd /var/lib/docker/containers 66 | $ du -sh * # 找到比较大的目录 67 | $ cd dda02c9a7491fa797ab730c1568ba06cba74cecd4e4a82e9d90d00fa11de743c 68 | $ cat /dev/null > dda02c9a7491fa797ab730c1568ba06cba74cecd4e4a82e9d90d00fa11de743c-json.log.9 # 删除log文件 69 | ``` 70 | 71 | * **注意:** 使用 `cat /dev/null >` 方式删除而不用 `rm`,因为用 rm 删除的文件,docker 进程可能不会释放文件,空间也就不会释放;log 的后缀数字越大表示越久远,先删除旧日志。 72 | 73 | 然后将该 node 标记不可调度,并将其已有的 pod 驱逐到其它节点,这样重启 dockerd 就会让该节点的 pod 对应的容器删掉,容器相关的日志(标准输出)与容器内产生的数据文件(没有挂载 volume, 可写层)也会被清理: 74 | 75 | ``` bash 76 | kubectl drain 77 | ``` 78 | 79 | 重启 dockerd: 80 | 81 | ``` bash 82 | systemctl restart dockerd 83 | # or systemctl restart docker 84 | ``` 85 | 86 | 等重启恢复,pod 调度到其它节点,排查磁盘爆满原因并清理和规避,然后取消节点不可调度标记: 87 | 88 | ``` bash 89 | kubectl uncordon 90 | ``` 91 | 92 | ## 如何规避 ? 93 | 94 | 正确配置 kubelet gc 和 驱逐相关的参数,即便到达爆满地步,此时节点上的 pod 也都早就自动驱逐到其它节点了,不会存在 Pod 一直 ContainerCreating 或 Terminating 的问题。 95 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/handle/memory-fragmentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "内存碎片化" 3 | --- 4 | 5 | ## 判断是否内存碎片化严重 6 | 7 | 内存页分配失败,内核日志报类似下面的错: 8 | 9 | ```bash 10 | mysqld: page allocation failure. order:4, mode:0x10c0d0 11 | ``` 12 | 13 | * `mysqld` 是被分配的内存的程序 14 | * `order` 表示需要分配连续页的数量\(2^order\),这里 4 表示 2^4=16 个连续的页 15 | * `mode` 是内存分配模式的标识,定义在内核源码文件 `include/linux/gfp.h` 中,通常是多个标识相与运算的结果,不同版本内核可能不一样,比如在新版内核中 `GFP_KERNEL` 是 `__GFP_RECLAIM | __GFP_IO | __GFP_FS` 的运算结果,而 `__GFP_RECLAIM` 又是 `___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM` 的运算结果 16 | 17 | 当 order 为 0 时,说明系统以及完全没有可用内存了,order 值比较大时,才说明内存碎片化了,无法分配连续的大页内存。 18 | 19 | ## 内存碎片化造成的问题 20 | 21 | ### 容器启动失败 22 | 23 | K8S 会为每个 pod 创建 netns 来隔离 network namespace,内核初始化 netns 时会为其创建 nf\_conntrack 表的 cache,需要申请大页内存,如果此时系统内存已经碎片化,无法分配到足够的大页内存内核就会报错\(`v2.6.33 - v4.6`\): 24 | 25 | ```bash 26 | runc:[1:CHILD]: page allocation failure: order:6, mode:0x10c0d0 27 | ``` 28 | 29 | Pod 状态将会一直在 ContainerCreating,dockerd 启动容器失败,日志报错: 30 | 31 | ```text 32 | Jan 23 14:15:31 dc05 dockerd: time="2019-01-23T14:15:31.288446233+08:00" level=error msg="containerd: start container" error="oci runtime error: container_linux.go:247: starting container process caused \"process_linux.go:245: running exec setns process for init caused \\\"exit status 6\\\"\"\n" id=5b9be8c5bb121264899fac8d9d36b02150269d41ce96ba6ad36d70b8640cb01c 33 | Jan 23 14:15:31 dc05 dockerd: time="2019-01-23T14:15:31.317965799+08:00" level=error msg="Create container failed with error: invalid header field value \"oci runtime error: container_linux.go:247: starting container process caused \\\"process_linux.go:245: running exec setns process for init caused \\\\\\\"exit status 6\\\\\\\"\\\"\\n\"" 34 | ``` 35 | 36 | kubelet 日志报错: 37 | 38 | ```text 39 | Jan 23 14:15:31 dc05 kubelet: E0123 14:15:31.352386 26037 remote_runtime.go:91] RunPodSandbox from runtime service failed: rpc error: code = 2 desc = failed to start sandbox container for pod "matchdataserver-1255064836-t4b2w": Error response from daemon: {"message":"invalid header field value \"oci runtime error: container_linux.go:247: starting container process caused \\\"process_linux.go:245: running exec setns process for init caused \\\\\\\"exit status 6\\\\\\\"\\\"\\n\""} 40 | Jan 23 14:15:31 dc05 kubelet: E0123 14:15:31.352496 26037 kuberuntime_sandbox.go:54] CreatePodSandbox for pod "matchdataserver-1255064836-t4b2w_basic(485fd485-1ed6-11e9-8661-0a587f8021ea)" failed: rpc error: code = 2 desc = failed to start sandbox container for pod "matchdataserver-1255064836-t4b2w": Error response from daemon: {"message":"invalid header field value \"oci runtime error: container_linux.go:247: starting container process caused \\\"process_linux.go:245: running exec setns process for init caused \\\\\\\"exit status 6\\\\\\\"\\\"\\n\""} 41 | Jan 23 14:15:31 dc05 kubelet: E0123 14:15:31.352518 26037 kuberuntime_manager.go:618] createPodSandbox for pod "matchdataserver-1255064836-t4b2w_basic(485fd485-1ed6-11e9-8661-0a587f8021ea)" failed: rpc error: code = 2 desc = failed to start sandbox container for pod "matchdataserver-1255064836-t4b2w": Error response from daemon: {"message":"invalid header field value \"oci runtime error: container_linux.go:247: starting container process caused \\\"process_linux.go:245: running exec setns process for init caused \\\\\\\"exit status 6\\\\\\\"\\\"\\n\""} 42 | Jan 23 14:15:31 dc05 kubelet: E0123 14:15:31.352580 26037 pod_workers.go:182] Error syncing pod 485fd485-1ed6-11e9-8661-0a587f8021ea ("matchdataserver-1255064836-t4b2w_basic(485fd485-1ed6-11e9-8661-0a587f8021ea)"), skipping: failed to "CreatePodSandbox" for "matchdataserver-1255064836-t4b2w_basic(485fd485-1ed6-11e9-8661-0a587f8021ea)" with CreatePodSandboxError: "CreatePodSandbox for pod \"matchdataserver-1255064836-t4b2w_basic(485fd485-1ed6-11e9-8661-0a587f8021ea)\" failed: rpc error: code = 2 desc = failed to start sandbox container for pod \"matchdataserver-1255064836-t4b2w\": Error response from daemon: {\"message\":\"invalid header field value \\\"oci runtime error: container_linux.go:247: starting container process caused \\\\\\\"process_linux.go:245: running exec setns process for init caused \\\\\\\\\\\\\\\"exit status 6\\\\\\\\\\\\\\\"\\\\\\\"\\\\n\\\"\"}" 43 | Jan 23 14:15:31 dc05 kubelet: I0123 14:15:31.372181 26037 kubelet.go:1916] SyncLoop (PLEG): "matchdataserver-1255064836-t4b2w_basic(485fd485-1ed6-11e9-8661-0a587f8021ea)", event: &pleg.PodLifecycleEvent{ID:"485fd485-1ed6-11e9-8661-0a587f8021ea", Type:"ContainerDied", Data:"5b9be8c5bb121264899fac8d9d36b02150269d41ce96ba6ad36d70b8640cb01c"} 44 | Jan 23 14:15:31 dc05 kubelet: W0123 14:15:31.372225 26037 pod_container_deletor.go:77] Container "5b9be8c5bb121264899fac8d9d36b02150269d41ce96ba6ad36d70b8640cb01c" not found in pod's containers 45 | Jan 23 14:15:31 dc05 kubelet: I0123 14:15:31.678211 26037 kuberuntime_manager.go:383] No ready sandbox for pod "matchdataserver-1255064836-t4b2w_basic(485fd485-1ed6-11e9-8661-0a587f8021ea)" can be found. Need to start a new one 46 | ``` 47 | 48 | 查看slab \(后面的0多表示伙伴系统没有大块内存了\): 49 | 50 | ```bash 51 | $ cat /proc/buddyinfo 52 | Node 0, zone DMA 1 0 1 0 2 1 1 0 1 1 3 53 | Node 0, zone DMA32 2725 624 489 178 0 0 0 0 0 0 0 54 | Node 0, zone Normal 1163 1101 932 222 0 0 0 0 0 0 0 55 | ``` 56 | 57 | ### 系统 OOM 58 | 59 | 内存碎片化会导致即使当前系统总内存比较多,但由于无法分配足够的大页内存导致给进程分配内存失败,就认为系统内存不够用,需要杀掉一些进程来释放内存,从而导致系统 OOM 60 | 61 | ## 解决方法 62 | 63 | * 周期性地或者在发现大块内存不足时,先进行drop\_cache操作: 64 | 65 | ```bash 66 | echo 3 > /proc/sys/vm/drop_caches 67 | ``` 68 | 69 | * 必要时候进行内存整理,开销会比较大,会造成业务卡住一段时间\(慎用\): 70 | 71 | ```bash 72 | echo 1 > /proc/sys/vm/compact_memory 73 | ``` 74 | 75 | ## 如何防止内存碎片化 76 | 77 | TODO 78 | 79 | ## 附录 80 | 81 | 相关链接: 82 | 83 | * [https://huataihuang.gitbooks.io/cloud-atlas/content/os/linux/kernel/memory/drop\_caches\_and\_compact\_memory.html](https://huataihuang.gitbooks.io/cloud-atlas/content/os/linux/kernel/memory/drop_caches_and_compact_memory.html) 84 | 85 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/handle/pid-full.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "PID 耗尽" 3 | --- 4 | 5 | ## 如何判断 PID 耗尽 6 | 7 | 首先要确认当前的 PID 限制,检查全局 PID 最大限制: 8 | 9 | ``` bash 10 | cat /proc/sys/kernel/pid_max 11 | ``` 12 | 13 | 也检查下线程数限制: 14 | 15 | ``` bash 16 | cat /proc/sys/kernel/threads-max 17 | ``` 18 | 19 | 再检查下当前用户是否还有 `ulimit` 限制最大进程数。 20 | 21 | 确认当前实际 PID 数量,检查当前用户的 PID 数量: 22 | 23 | ``` bash 24 | ps -eLf | wc -l 25 | ``` 26 | 27 | 如果发现实际 PID 数量接近最大限制说明 PID 就可能会爆满导致经常有进程无法启动,低版本内核可能报错: `Cannot allocate memory`,这个报错信息不准确,在内核 4.1 以后改进了: https://github.com/torvalds/linux/commit/35f71bc0a09a45924bed268d8ccd0d3407bc476f 28 | 29 | ## 如何解决 30 | 31 | 临时调大 PID 和线程数限制: 32 | 33 | ``` bash 34 | echo 65535 > /proc/sys/kernel/pid_max 35 | echo 65535 > /proc/sys/kernel/threads-max 36 | ``` 37 | 38 | 永久调大 PID 和线程数限制: 39 | 40 | ``` bash 41 | echo "kernel.pid_max=65535 " >> /etc/sysctl.conf && sysctl -p 42 | echo "kernel.threads-max=65535 " >> /etc/sysctl.conf && sysctl -p 43 | ``` 44 | 45 | k8s 1.14 支持了限制 Pod 的进程数量: https://kubernetes.io/blog/2019/04/15/process-id-limiting-for-stability-improvements-in-kubernetes-1.14/ 46 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/handle/runnig-out-of-inotify-watches.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "inotify watch 耗尽" 3 | --- 4 | 5 | 每个 linux 进程可以持有多个 fd,每个 inotify 类型的 fd 可以 watch 多个目录,每个用户下所有进程 inotify 类型的 fd 可以 watch 的总目录数有个最大限制,这个限制可以通过内核参数配置: `fs.inotify.max_user_watches` 6 | 7 | 查看最大 inotify watch 数: 8 | 9 | ```bash 10 | $ cat /proc/sys/fs/inotify/max_user_watches 11 | 8192 12 | ``` 13 | 14 | 使用下面的脚本查看当前有 inotify watch 类型 fd 的进程以及每个 fd watch 的目录数量,降序输出,带总数统计: 15 | 16 | ```bash 17 | #!/usr/bin/env bash 18 | # 19 | # Copyright 2019 (c) roc 20 | # 21 | # This script shows processes holding the inotify fd, alone with HOW MANY directories each inotify fd watches(0 will be ignored). 22 | total=0 23 | result="EXE PID FD-INFO INOTIFY-WATCHES\n" 24 | while read pid fd; do \ 25 | exe="$(readlink -f /proc/$pid/exe || echo n/a)"; \ 26 | fdinfo="/proc/$pid/fdinfo/$fd" ; \ 27 | count="$(grep -c inotify "$fdinfo" || true)"; \ 28 | if [ $((count)) != 0 ]; then 29 | total=$((total+count)); \ 30 | result+="$exe $pid $fdinfo $count\n"; \ 31 | fi 32 | done <<< "$(lsof +c 0 -n -P -u root|awk '/inotify$/ { gsub(/[urw]$/,"",$4); print $2" "$4 }')" && echo "total $total inotify watches" && result="$(echo -e $result|column -t)\n" && echo -e "$result" | head -1 && echo -e "$result" | sed "1d" | sort -k 4rn; 33 | ``` 34 | 35 | 示例输出: 36 | 37 | ```bash 38 | total 7882 inotify watches 39 | EXE PID FD-INFO INOTIFY-WATCHES 40 | /usr/local/qcloud/YunJing/YDEyes/YDService 25813 /proc/25813/fdinfo/8 7077 41 | /usr/bin/kubelet 1173 /proc/1173/fdinfo/22 665 42 | /usr/bin/ruby2.3 13381 /proc/13381/fdinfo/14 54 43 | /usr/lib/policykit-1/polkitd 1458 /proc/1458/fdinfo/9 14 44 | /lib/systemd/systemd-udevd 450 /proc/450/fdinfo/9 13 45 | /usr/sbin/nscd 7935 /proc/7935/fdinfo/3 6 46 | /usr/bin/kubelet 1173 /proc/1173/fdinfo/28 5 47 | /lib/systemd/systemd 1 /proc/1/fdinfo/17 4 48 | /lib/systemd/systemd 1 /proc/1/fdinfo/18 4 49 | /lib/systemd/systemd 1 /proc/1/fdinfo/26 4 50 | /lib/systemd/systemd 1 /proc/1/fdinfo/28 4 51 | /usr/lib/policykit-1/polkitd 1458 /proc/1458/fdinfo/8 4 52 | /usr/local/bin/sidecar-injector 4751 /proc/4751/fdinfo/3 3 53 | /usr/lib/accountsservice/accounts-daemon 1178 /proc/1178/fdinfo/7 2 54 | /usr/local/bin/galley 8228 /proc/8228/fdinfo/10 2 55 | /usr/local/bin/galley 8228 /proc/8228/fdinfo/9 2 56 | /lib/systemd/systemd 1 /proc/1/fdinfo/11 1 57 | /sbin/agetty 1437 /proc/1437/fdinfo/4 1 58 | /sbin/agetty 1440 /proc/1440/fdinfo/4 1 59 | /usr/bin/kubelet 1173 /proc/1173/fdinfo/10 1 60 | /usr/local/bin/envoy 4859 /proc/4859/fdinfo/5 1 61 | /usr/local/bin/envoy 5427 /proc/5427/fdinfo/5 1 62 | /usr/local/bin/envoy 6058 /proc/6058/fdinfo/3 1 63 | /usr/local/bin/envoy 6893 /proc/6893/fdinfo/3 1 64 | /usr/local/bin/envoy 6950 /proc/6950/fdinfo/3 1 65 | /usr/local/bin/galley 8228 /proc/8228/fdinfo/3 1 66 | /usr/local/bin/pilot-agent 3819 /proc/3819/fdinfo/5 1 67 | /usr/local/bin/pilot-agent 4244 /proc/4244/fdinfo/5 1 68 | /usr/local/bin/pilot-agent 5901 /proc/5901/fdinfo/3 1 69 | /usr/local/bin/pilot-agent 6789 /proc/6789/fdinfo/3 1 70 | /usr/local/bin/pilot-agent 6808 /proc/6808/fdinfo/3 1 71 | /usr/local/bin/pilot-discovery 6231 /proc/6231/fdinfo/3 1 72 | /usr/local/bin/sidecar-injector 4751 /proc/4751/fdinfo/5 1 73 | /usr/sbin/acpid 1166 /proc/1166/fdinfo/6 1 74 | /usr/sbin/dnsmasq 7572 /proc/7572/fdinfo/8 1 75 | ``` 76 | 77 | 如果看到总 watch 数比较大,接近最大限制,可以修改内核参数调高下这个限制。 78 | 79 | 临时调整: 80 | 81 | ```bash 82 | sudo sysctl fs.inotify.max_user_watches=524288 83 | ``` 84 | 85 | 永久生效: 86 | 87 | ```bash 88 | echo "fs.inotify.max_user_watches=524288" >> /etc/sysctl.conf && sysctl -p 89 | ``` 90 | 91 | 打开 inotify\_add\_watch 跟踪,进一步 debug inotify watch 耗尽的原因: 92 | 93 | ```bash 94 | echo 1 >> /sys/kernel/debug/tracing/events/syscalls/sys_exit_inotify_add_watch/enable 95 | ``` 96 | 97 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/network/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "网络排错" 3 | weight: 50 4 | --- 5 | 6 | {{% children %}} -------------------------------------------------------------------------------- /content/zh/troubleshooting/network/dns.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "DNS 解析异常" 3 | --- 4 | 5 | ## 5 秒延时 6 | 7 | 如果DNS查询经常延时5秒才返回,通常是遇到内核 conntrack 冲突导致的丢包,详见 [案例分享: DNS 5秒延时](/troubleshooting/cases/dns-lookup-5s-delay.md) 8 | 9 | ## 解析超时 10 | 11 | 如果容器内报 DNS 解析超时,先检查下集群 DNS 服务 \(`kube-dns`/`coredns`\) 的 Pod 是否 Ready,如果不是,请参考本章其它小节定位原因。如果运行正常,再具体看下超时现象。 12 | 13 | ### 解析外部域名超时 14 | 15 | 可能原因: 16 | 17 | * 上游 DNS 故障 18 | * 上游 DNS 的 ACL 或防火墙拦截了报文 19 | 20 | ### 所有解析都超时 21 | 22 | 如果集群内某个 Pod 不管解析 Service 还是外部域名都失败,通常是 Pod 与集群 DNS 之间通信有问题。 23 | 24 | 可能原因: 25 | 26 | * 节点防火墙没放开集群网段,导致如果 Pod 跟集群 DNS 的 Pod 不在同一个节点就无法通信,DNS 请求也就无法被收到 27 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/network/lb-healthcheck-failed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "LB 健康检查失败" 3 | --- 4 | 5 | 可能原因: 6 | 7 | * 节点防火墙规则没放开 nodeport 区间端口 \(默认 30000-32768\) 检查iptables和云主机安全组 8 | * LB IP 绑到 `kube-ipvs0` 导致丢源 IP为 LB IP 的包: [https://github.com/kubernetes/kubernetes/issues/79783](https://github.com/kubernetes/kubernetes/issues/79783) 9 | 10 | TODO: 完善 11 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/network/low-throughput.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "网络性能差" 3 | --- 4 | 5 | ## IPVS 模式吞吐性能低 6 | 7 | 内核参数关闭 `conn_reuse_mode`: 8 | 9 | ``` bash 10 | sysctl net.ipv4.vs.conn_reuse_mode=0 11 | ``` 12 | 13 | 参考 issue: https://github.com/kubernetes/kubernetes/issues/70747 14 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/network/manual.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "网络排错手册" 3 | weight: 10 4 | --- 5 | 6 | ## 连接队列溢出 7 | 8 | 查看是否全连接或半连接队列溢出导致丢包,造成部分连接异常 (timeout): 9 | 10 | ``` bash 11 | $ netstat -s | grep -E 'overflow|drop' 12 | 3327 times the listen queue of a socket overflowed 13 | 32631 SYNs to LISTEN sockets dropped 14 | ``` 15 | 16 | 进入容器 netns 后,查看各状态的连接数统计: 17 | 18 | ``` bash 19 | netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' 20 | ``` 21 | 22 | 故障案例: 23 | * 健康检查失败 24 | * 网络时同时不通 25 | 26 | 解决方案: 27 | * 调大 sommaxconn 28 | * 调大 backlog 29 | * 若是 nginx,还受 nginx 本身的 backlog 配置,也调大下 30 | 31 | ## conntrack 表爆满 32 | 33 | 看内核日志: 34 | ``` bash 35 | # demsg 36 | $ journalctl -k | grep "nf_conntrack: table full" 37 | nf_conntrack: nf_conntrack: table full, dropping packet 38 | ``` 39 | 40 | 若有以上报错,证明 conntrack 表满了,需要调大 conntrack 表: 41 | 42 | ``` bash 43 | sysctl -w net.netfilter.nf_conntrack_max=1000000 44 | ``` 45 | 46 | ## arp 表爆满 47 | 48 | 看内核日志: 49 | 50 | ``` bash 51 | # demsg 52 | $ journalctl -k | grep "neighbor table overflow" 53 | arp_cache: neighbor table overflow! 54 | ``` 55 | 56 | 若有以上报错,证明 arp 表满了,查看当前 arp 记录数: 57 | 58 | ``` bash 59 | $ arp -an | wc -l 60 | 1335 61 | ``` 62 | 63 | 查看 arp gc 阀值: 64 | 65 | ``` bash 66 | $ sysctl -a | grep gc_thresh 67 | net.ipv4.neigh.default.gc_thresh1 = 128 68 | net.ipv4.neigh.default.gc_thresh2 = 512 69 | net.ipv4.neigh.default.gc_thresh3 = 1024 70 | ``` 71 | 72 | 调大 arp 表: 73 | ``` bash 74 | sysctl -w net.ipv4.neigh.default.gc_thresh1=80000 75 | sysctl -w net.ipv4.neigh.default.gc_thresh2=90000 76 | sysctl -w net.ipv4.neigh.default.gc_thresh3=100000 77 | ``` 78 | 79 | ## 端口监听挂掉 80 | 81 | 如果容器内的端口已经没有进程监听了,内核就会返回 Reset 包,客户端就会报错连接被拒绝,可以进容器 netns 检查下端口是否存活: 82 | 83 | ``` bash 84 | netstat -tunlp 85 | ``` 86 | 87 | ## tcp_tw_recycle 导致丢包 88 | 89 | 在低版本内核中(比如 3.10),支持使用 tcp_tw_recycle 内核参数来开启 TIME_WAIT 的快速回收,但如果 client 也开启了 timestamp (一般默认开启),同时也就会导致在 NAT 环境丢包,甚至没有 NAT 时,稍微高并发一点,也会导致 PAWS 校验失败,导致丢包: 90 | ``` bash 91 | # 看 SYN 丢包是否全都是 PAWS 校验失败 92 | $ cat /proc/net/netstat | grep TcpE| awk '{print $15, $22}' 93 | PAWSPassive ListenDrops 94 | 96305 96305 95 | ``` 96 | 97 | 参考资料: 98 | * https://github.com/torvalds/linux/blob/v3.10/net/ipv4/tcp_ipv4.c#L1465 99 | * https://www.freesoft.org/CIE/RFC/1323/13.htm 100 | * https://zhuanlan.zhihu.com/p/35684094 101 | * https://my.oschina.net/u/4270811/blog/3473655/print 102 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/network/service-cannot-resolve.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Service 无法解析" 3 | --- 4 | 5 | ## 集群 DNS 没有正常运行\(kube-dns或CoreDNS\) 6 | 7 | 检查集群 DNS 是否运行正常: 8 | 9 | * kubelet 启动参数 `--cluster-dns` 可以看到 dns 服务的 cluster ip: 10 | 11 | ```bash 12 | $ ps -ef | grep kubelet 13 | ... /usr/bin/kubelet --cluster-dns=172.16.14.217 ... 14 | ``` 15 | 16 | * 找到 dns 的 service: 17 | 18 | ```bash 19 | $ kubectl get svc -n kube-system | grep 172.16.14.217 20 | kube-dns ClusterIP 172.16.14.217 53/TCP,53/UDP 47d 21 | ``` 22 | 23 | * 看是否存在 endpoint: 24 | 25 | ```bash 26 | $ kubectl -n kube-system describe svc kube-dns | grep -i endpoints 27 | Endpoints: 172.16.0.156:53,172.16.0.167:53 28 | Endpoints: 172.16.0.156:53,172.16.0.167:53 29 | ``` 30 | 31 | * 检查 endpoint 的 对应 pod 是否正常: 32 | 33 | ```bash 34 | $ kubectl -n kube-system get pod -o wide | grep 172.16.0.156 35 | kube-dns-898dbbfc6-hvwlr 3/3 Running 0 8d 172.16.0.156 10.0.0.3 36 | ``` 37 | 38 | ## Pod 与 DNS 服务之间网络不通 39 | 40 | 检查下 pod 是否连不上 dns 服务,可以在 pod 里 telnet 一下 dns 的 53 端口: 41 | 42 | ```bash 43 | # 连 dns service 的 cluster ip 44 | $ telnet 172.16.14.217 53 45 | ``` 46 | 47 | 如果检查到是网络不通,就需要排查下网络设置: 48 | 49 | * 检查节点的安全组设置,需要放开集群的容器网段 50 | * 检查是否还有防火墙规则,检查 iptables 51 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/network/service-unrecheable.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Service 不通" 3 | state: "Alpha" 4 | --- 5 | 6 | ## 集群 dns 故障 7 | 8 | TODO 9 | 10 | ## 节点防火墙没放开集群容器网络 \(iptables/安全组\) 11 | 12 | TODO 13 | 14 | ## kube-proxy 没有工作,命中 netlink deadlock 的 bug 15 | 16 | * issue: https://github.com/kubernetes/kubernetes/issues/71071 17 | * 1.14 版本已修复,修复的 PR: https://github.com/kubernetes/kubernetes/pull/72361 18 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/node/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "节点排错" 3 | weight: 30 4 | --- 5 | 6 | 本章包含各种经典报错,分析可能原因并给出相应的解决方案 7 | 8 | {{% children %}} -------------------------------------------------------------------------------- /content/zh/troubleshooting/node/arp_cache-neighbor-table-overflow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "arp_cache: neighbor table overflow! (arp缓存溢出)" 3 | --- 4 | 5 | 节点内核报这个错说明当前节点 arp 缓存满了。 6 | 7 | 查看当前 arp 记录数: 8 | 9 | ``` bash 10 | $ arp -an | wc -l 11 | 1335 12 | ``` 13 | 14 | 查看 gc 阀值: 15 | 16 | ``` bash 17 | $ sysctl -a | grep net.ipv4.neigh.default.gc_thresh 18 | net.ipv4.neigh.default.gc_thresh1 = 128 19 | net.ipv4.neigh.default.gc_thresh2 = 512 20 | net.ipv4.neigh.default.gc_thresh3 = 1024 21 | ``` 22 | 23 | 当前 arp 记录数接近 gc_thresh3 比较容易 overflow,因为当 arp 记录达到 gc_thresh3 时会强制触发 gc 清理,当这时又有数据包要发送,并且根据目的 IP 在 arp cache 中没找到 mac 地址,这时会判断当前 arp cache 记录数加 1 是否大于 gc_thresh3,如果没有大于就会 时就会报错: `neighbor table overflow!` 24 | 25 | ## 什么场景下会发生 26 | 27 | 集群规模大,node 和 pod 数量超多,参考本书避坑宝典的 [案例分享: ARP 缓存爆满导致健康检查失败](../../../cases/arp-cache-overflow-causes-healthcheck-failed/) 28 | 29 | ## 解决方案 30 | 31 | 调整部分节点内核参数,将 arp cache 的 gc 阀值调高 (`/etc/sysctl.conf`): 32 | 33 | ``` bash 34 | net.ipv4.neigh.default.gc_thresh1 = 80000 35 | net.ipv4.neigh.default.gc_thresh2 = 90000 36 | net.ipv4.neigh.default.gc_thresh3 = 100000 37 | ``` 38 | 39 | 并给 node 打下label,修改 pod spec,加下 nodeSelector 或者 nodeAffnity,让 pod 只调度到这部分改过内核参数的节点 40 | 41 | ## 参考资料 42 | 43 | - Scaling Kubernetes to 2,500 Nodes: https://openai.com/blog/scaling-kubernetes-to-2500-nodes/ 44 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/node/cannot-allocate-memory.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Cannot allocate memory" 3 | --- 4 | 5 | 容器启动失败,报错 `Cannot allocate memory`。 6 | 7 | ## PID 耗尽 8 | 9 | 如果登录 ssh 困难,并且登录成功后执行任意命名经常报 `Cannot allocate memory`,多半是 PID 耗尽了。 10 | 11 | 处理方法参考本书 [处理实践: PID 耗尽](../../../handle/pid-full/) 12 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/node/kernel-solft-lockup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "soft lockup (内核软死锁)" 3 | --- 4 | 5 | ## 内核报错 6 | 7 | ``` log 8 | Oct 14 15:13:05 VM_1_6_centos kernel: NMI watchdog: BUG: soft lockup - CPU#5 stuck for 22s! [runc:[1:CHILD]:2274] 9 | ``` 10 | 11 | ## 原因 12 | 13 | 发生这个报错通常是内核繁忙 (扫描、释放或分配大量对象),分不出时间片给用户态进程导致的,也伴随着高负载,如果负载降低报错则会消失。 14 | 15 | ## 什么情况下会导致内核繁忙 16 | 17 | * 短时间内创建大量进程 (可能是业务需要,也可能是业务bug或用法不正确导致创建大量进程) 18 | 19 | ## 参考资料 20 | 21 | * What are all these "Bug: soft lockup" messages about : https://www.suse.com/support/kb/doc/?id=7017652 22 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/node/no-space-left-on-device.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "no space left on device" 3 | --- 4 | 5 | - 有时候节点 NotReady, kubelet 日志报 `no space left on device` 6 | - 有时候创建 Pod 失败,`describe pod` 看 event 报 `no space left on device` 7 | 8 | 出现这种错误有很多中可能原因,下面我们来根据现象找对应原因。 9 | 10 | ## inotify watch 耗尽 11 | 12 | 节点 NotReady,kubelet 启动失败,看 kubelet 日志: 13 | 14 | ``` bash 15 | Jul 18 15:20:58 VM_16_16_centos kubelet[11519]: E0718 15:20:58.280275 11519 raw.go:140] Failed to watch directory "/sys/fs/cgroup/memory/kubepods": inotify_add_watch /sys/fs/cgroup/memory/kubepods/burstable/pod926b7ff4-7bff-11e8-945b-52540048533c/6e85761a30707b43ed874e0140f58839618285fc90717153b3cbe7f91629ef5a: no space left on device 16 | ``` 17 | 18 | 系统调用 `inotify_add_watch` 失败,提示 `no space left on device`, 这是因为系统上进程 watch 文件目录的总数超出了最大限制,可以修改内核参数调高限制,详细请参考本书 [处理实践: inotify watch 耗尽](../../../handle/runnig-out-of-inotify-watches/) 19 | 20 | ## cgroup 泄露 21 | 22 | 查看当前 cgroup 数量: 23 | 24 | ``` bash 25 | $ cat /proc/cgroups | column -t 26 | #subsys_name hierarchy num_cgroups enabled 27 | cpuset 5 29 1 28 | cpu 7 126 1 29 | cpuacct 7 126 1 30 | memory 9 127 1 31 | devices 4 126 1 32 | freezer 2 29 1 33 | net_cls 6 29 1 34 | blkio 10 126 1 35 | perf_event 3 29 1 36 | hugetlb 11 29 1 37 | pids 8 126 1 38 | net_prio 6 29 1 39 | ``` 40 | 41 | cgroup 子系统目录下面所有每个目录及其子目录都认为是一个独立的 cgroup,所以也可以在文件系统中统计目录数来获取实际 cgroup 数量,通常跟 `/proc/cgroups` 里面看到的应该一致: 42 | 43 | ``` bash 44 | $ find -L /sys/fs/cgroup/memory -type d | wc -l 45 | 127 46 | ``` 47 | 48 | 当 cgroup 泄露发生时,这里的数量就不是真实的了,低版本内核限制最大 65535 个 cgroup,并且开启 kmem 删除 cgroup 时会泄露,大量创建删除容器后泄露了许多 cgroup,最终总数达到 65535,新建容器创建 cgroup 将会失败,报 `no space left on device` 49 | 50 | 详细请参考本书 [案例分享: cgroup 泄露](../../../summary/cgroup-leaking/) 51 | 52 | ## 磁盘被写满(TODO) 53 | 54 | Pod 启动失败,状态 `CreateContainerError`: 55 | 56 | ``` bash 57 | csi-cephfsplugin-27znb 0/2 CreateContainerError 167 17h 58 | ``` 59 | 60 | Pod 事件报错: 61 | 62 | ``` bash 63 | Warning Failed 5m1s (x3397 over 17h) kubelet, ip-10-0-151-35.us-west-2.compute.internal (combined from similar events): Error: container create failed: container_linux.go:336: starting container process caused "process_linux.go:399: container init caused \"rootfs_linux.go:58: mounting \\\"/sys\\\" to rootfs \\\"/var/lib/containers/storage/overlay/051e985771cc69f3f699895a1dada9ef6483e912b46a99e004af7bb4852183eb/merged\\\" at \\\"/var/lib/containers/storage/overlay/051e985771cc69f3f699895a1dada9ef6483e912b46a99e004af7bb4852183eb/merged/sys\\\" caused \\\"no space left on device\\\"\"" 64 | ``` 65 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/node/not-ready.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Node NotReady" 3 | state: TODO 4 | --- -------------------------------------------------------------------------------- /content/zh/troubleshooting/others/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "其它排错" 3 | weight: 70 4 | --- 5 | 6 | {{% children %}} -------------------------------------------------------------------------------- /content/zh/troubleshooting/others/daemonset-not-scheduled.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Daemonset 没有被调度" 3 | state: Alpha 4 | --- 5 | 6 | Daemonset 的期望实例为 0,可能原因: 7 | 8 | * controller-manager 的 bug,重启 controller-manager 可以恢复 9 | * controller-manager 挂了 10 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/others/job-cannot-delete.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Job 无法被删除" 3 | --- 4 | 5 | ## 原因 6 | 7 | * 可能是 k8s 的一个bug: [https://github.com/kubernetes/kubernetes/issues/43168](https://github.com/kubernetes/kubernetes/issues/43168) 8 | * 本质上是脏数据问题,Running+Succeed != 期望Completions 数量,低版本 kubectl 不容忍,delete job 的时候打开debug\(加-v=8\),会看到kubectl不断在重试,直到达到timeout时间。新版kubectl会容忍这些,删除job时会删除关联的pod 9 | 10 | ## 解决方法 11 | 12 | 1. 升级 kubectl 版本,1.12 以上 13 | 2. 低版本 kubectl 删除 job 时带 `--cascade=false` 参数\(如果job关联的pod没删完,加这个参数不会删除关联的pod\) 14 | 15 | ```bash 16 | kubectl delete job --cascade=false 17 | ``` 18 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/others/kubectl-exec-or-logs-failed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "kubectl 执行 exec 或 logs 失败" 3 | state: Alpha 4 | --- 5 | 6 | 通常是 apiserver --> kubelet:10250 之间的网络不通,10250 是 kubelet 提供接口的端口,`kubectl exec` 和 `kubectl logs` 的原理就是 apiserver 调 kubelet,kubelet 再调运行时 (比如 dockerd) 来实现的,所以要保证 kubelet 10250 端口对 apiserver 放通。检查防火墙、iptables 规则是否对 10250 端口或某些 IP 进行了拦截。 7 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/others/namespace-stuck-on-terminating.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Namespace 卡在 Terminating" 3 | state: Alpha 4 | --- 5 | 6 | ## Namespace 上存在 Finalizers 7 | 8 | 删除 ns 后,一直卡在 Terminating 状态。通常是存在 finalizers,通过 `kubectl get ns xxx -o yaml` 可以看到是否有 finalizers: 9 | 10 | ``` bash 11 | $ kubectl get ns -o yaml kube-node-lease 12 | apiVersion: v1 13 | kind: Namespace 14 | metadata: 15 | ... 16 | finalizers: 17 | - finalizers.kubesphere.io/namespaces 18 | labels: 19 | kubesphere.io/workspace: system-workspace 20 | name: kube-node-lease 21 | ownerReferences: 22 | - apiVersion: tenant.kubesphere.io/v1alpha1 23 | blockOwnerDeletion: true 24 | controller: true 25 | kind: Workspace 26 | name: system-workspace 27 | uid: d4310acd-1fdc-11ea-a370-a2c490b9ae47 28 | spec: {} 29 | ``` 30 | 31 | 此例是因为之前装过 kubesphere,然后卸载了,但没有清理 finalizers,将其删除就可以了。 32 | 33 | k8s 资源的 metadata 里如果存在 finalizers,那么该资源一般是由某应用创建的,或者是这个资源是此应用关心的。应用会在资源的 metadata 里的 finalizers 加了一个它自己可以识别的标识,这意味着这个资源被删除时需要由此应用来做删除前的清理,清理完了它需要将标识从该资源的 finalizers 中移除,然后才会最终彻底删除资源。比如 Rancher 创建的一些资源就会写入 finalizers 标识。 34 | 35 | 如果应用被删除,而finalizer没清理,删除资源时就会一直卡在terminating,可以手动删除finalizer来解决。 36 | 37 | 手动删除方法: 38 | 1. `kubectl edit ns xx` 删除 `spec.finalizers`。 39 | 2. 如果k8s版本较高会发现方法1行不通,因为高版本更改 namespace finalizers 被移到了 namespace 的 finalize 这个 subresource (参考[官方文档API文档](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/namespaces.md#rest-api)),并且需要使用 `PUT` 请求,可以先执行 `kubectl proxy` 然后再起一个终端用 curl 模拟请求去删 `finalizers`: 40 | ``` bash 41 | curl -H "Content-Type: application/json" -XPUT -d '{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"delete-me"},"spec":{"finalizers":[]}}' http://localhost:8001/api/v1/namespaces/delete-me/finalize 42 | ``` 43 | > 替换 `delete-me` 为你的 namespace 名称 44 | 45 | 参考资料: 46 | 47 | * Node Lease 的 Proposal: https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/0009-node-heartbeat.md 48 | 49 | ## metrics server 被删除 50 | 51 | 删除 ns 时,apiserver 会调注册上去的扩展 api 去清理资源,如果扩展 api 对应的服务也被删了,那么就无法清理完成,也就一直卡在 Terminating。 52 | 53 | 下面的例子就是使用 prometheus-adapter 注册的 resource metrics api,但 prometheus-adapter 已经被删除了: 54 | 55 | ``` bash 56 | $ kubectl get apiservice 57 | ... 58 | v1beta1.metrics.k8s.io monitoring/prometheus-adapter False (ServiceNotFound) 75d 59 | ... 60 | ``` 61 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/others/node-all-gone.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Node 全部消失" 3 | state: Alpha 4 | --- 5 | 6 | ## Rancher 清除 Node 导致集群异常 7 | 8 | ### 现象 9 | 10 | 安装了 rancher 的用户,在卸载 rancher 的时候,可能会手动执行 `kubectl delete ns local` 来删除这个 rancher 创建的 namespace,但直接这样做会导致所有 node 被清除,通过 `kubectl get node` 获取不到 node。 11 | 12 | ### 原因 13 | 14 | 看了下 rancher 源码,rancher 通过 `nodes.management.cattle.io` 这个 CRD 存储和管理 node,会给所有 node 创建对应的这个 CRD 资源,metadata 中加入了两个 finalizer,其中 `user-node-remove_local` 对应的 finalizer 处理逻辑就是删除对应的 k8s node 资源,也就是 `delete ns local` 时,会尝试删除 `nodes.management.cattle.io` 这些 CRD 资源,进而触发 rancher 的 finalizer 逻辑去删除对应的 k8s node 资源,从而清空了 node,所以 `kubectl get node` 就看不到 node 了,集群里的服务就无法被调度。 15 | 16 | ### 规避方案 17 | 18 | 不要在 rancher 组件卸载完之前手动 `delete ns local`。 19 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/others/slow-apiserver.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "APIServer 响应慢" 3 | state: TODO 4 | --- -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 排错" 3 | weight: 40 4 | --- 5 | 6 | 本文是本书排查指南板块下问题排查章节的 Pod排错 一节,介绍 Pod 各种异常现象,可能的原因以及解决方法。 7 | 8 | ## 常用命令 9 | 10 | 排查过程常用的命名如下: 11 | 12 | * 查看 Pod 状态: `kubectl get pod -o wide` 13 | * 查看 Pod 的 yaml 配置: `kubectl get pod -o yaml` 14 | * 查看 Pod 事件: `kubectl describe pod ` 15 | * 查看容器日志: `kubectl logs [-c ]` 16 | 17 | ## Pod 状态 18 | 19 | Pod 有多种状态,这里罗列一下: 20 | 21 | * `Error`: Pod 启动过程中发生错误 22 | * `NodeLost`: Pod 所在节点失联 23 | * `Unkown`: Pod 所在节点失联或其它未知异常 24 | * `Waiting`: Pod 等待启动 25 | * `Pending`: Pod 等待被调度 26 | * `ContainerCreating`: Pod 容器正在被创建 27 | * `Terminating`: Pod 正在被销毁 28 | * `CrashLoopBackOff`: 容器退出,kubelet 正在将它重启 29 | * `InvalidImageName`: 无法解析镜像名称 30 | * `ImageInspectError`: 无法校验镜像 31 | * `ErrImageNeverPull`: 策略禁止拉取镜像 32 | * `ImagePullBackOff`: 正在重试拉取 33 | * `RegistryUnavailable`: 连接不到镜像中心 34 | * `ErrImagePull`: 通用的拉取镜像出错 35 | * `CreateContainerConfigError`: 不能创建 kubelet 使用的容器配置 36 | * `CreateContainerError`: 创建容器失败 37 | * `RunContainerError`: 启动容器失败 38 | * `PreStartHookError`: 执行 preStart hook 报错 39 | * `PostStartHookError`: 执行 postStart hook 报错 40 | * `ContainersNotInitialized`: 容器没有初始化完毕 41 | * `ContainersNotReady`: 容器没有准备完毕 42 | * `ContainerCreating`:容器创建中 43 | * `PodInitializing`:pod 初始化中 44 | * `DockerDaemonNotReady`:docker还没有完全启动 45 | * `NetworkPluginNotReady`: 网络插件还没有完全启动 46 | 47 | ## 问题导航 48 | 49 | 有时候我们无法直接通过异常状态找到异常原因,这里我们罗列一下各种现象,点击即可进入相应的文章,帮助你分析问题,罗列各种可能的原因,进一步定位根因: 50 | 51 | {{% children %}} -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/container-proccess-exit-by-itself.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "容器进程主动退出" 3 | --- 4 | 5 | 容器进程如果是自己主动退出(不是被外界中断杀死),退出状态码一般在 0-128 之间,根据约定,正常退出时状态码为 0,1-127 说明是程序发生异常,主动退出了,比如检测到启动的参数和条件不满足要求,或者运行过程中发生 panic 但没有捕获处理导致程序退出。除了可能是业务程序 BUG,还有其它许多可能原因,这里我们一一列举下。 6 | 7 | ## DNS 无法解析 8 | 9 | 可能程序依赖 集群 DNS 服务,比如启动时连接数据库,数据库使用 service 名称或外部域名都需要 DNS 解析,如果解析失败程序将报错并主动退出。解析失败的可能原因: 10 | 11 | * 集群网络有问题,Pod 连不上集群 DNS 服务 12 | * 集群 DNS 服务挂了,无法响应解析请求 13 | * Service 或域名地址配置有误,本身是无法解析的地址 14 | 15 | ## 程序配置有误 16 | 17 | * 配置文件格式错误,程序启动解析配置失败报错退出 18 | * 配置内容不符合规范,比如配置中某个字段是必选但没有填写,配置校验不通过,程序报错主动退出 19 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/healthcheck-failed.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 健康检查失败" 3 | --- 4 | 5 | * Kubernetes 健康检查包含就绪检查\(readinessProbe\)和存活检查\(livenessProbe\) 6 | * pod 如果就绪检查失败会将此 pod ip 从 service 中摘除,通过 service 访问,流量将不会被转发给就绪检查失败的 pod 7 | * pod 如果存活检查失败,kubelet 将会杀死容器并尝试重启 8 | 9 | 健康检查失败的可能原因有多种,除了业务程序BUG导致不能响应健康检查导致 unhealthy,还能有有其它原因,下面我们来逐个排查。 10 | 11 | ## 健康检查配置不合理 12 | 13 | `initialDelaySeconds` 太短,容器启动慢,导致容器还没完全启动就开始探测,如果 successThreshold 是默认值 1,检查失败一次就会被 kill,然后 pod 一直这样被 kill 重启。 14 | 15 | ## 节点负载过高 16 | 17 | cpu 占用高(比如跑满)会导致进程无法正常发包收包,通常会 timeout,导致 kubelet 认为 pod 不健康。参考本书 [处理实践: 高负载](../../handle/high-load/) 一节。 18 | 19 | ## 容器进程被木马进程杀死 20 | 21 | 参考本书 [处理实践: 使用 systemtap 定位疑难杂症](../../trick/use-systemtap-to-locate-problems/) 进一步定位。 22 | 23 | ## 容器内进程端口监听挂掉 24 | 25 | 使用 `netstat -tunlp` 检查端口监听是否还在,如果不在了,抓包可以看到会直接 reset 掉健康检查探测的连接: 26 | 27 | ```bash 28 | 20:15:17.890996 IP 172.16.2.1.38074 > 172.16.2.23.8888: Flags [S], seq 96880261, win 14600, options [mss 1424,nop,nop,sackOK,nop,wscale 7], length 0 29 | 20:15:17.891021 IP 172.16.2.23.8888 > 172.16.2.1.38074: Flags [R.], seq 0, ack 96880262, win 0, length 0 30 | 20:15:17.906744 IP 10.0.0.16.54132 > 172.16.2.23.8888: Flags [S], seq 1207014342, win 14600, options [mss 1424,nop,nop,sackOK,nop,wscale 7], length 0 31 | 20:15:17.906766 IP 172.16.2.23.8888 > 10.0.0.16.54132: Flags [R.], seq 0, ack 1207014343, win 0, length 0 32 | ``` 33 | 34 | 连接异常,从而健康检查失败。发生这种情况的原因可能在一个节点上启动了多个使用 `hostNetwork` 监听相同宿主机端口的 Pod,只会有一个 Pod 监听成功,但监听失败的 Pod 的业务逻辑允许了监听失败,并没有退出,Pod 又配了健康检查,kubelet 就会给 Pod 发送健康检查探测报文,但 Pod 由于没有监听所以就会健康检查失败。 35 | 36 | ## SYN backlog 设置过小 37 | 38 | SYN backlog 大小即 SYN 队列大小,如果短时间内新建连接比较多,而 SYN backlog 设置太小,就会导致新建连接失败,通过 `netstat -s | grep TCPBacklogDrop` 可以看到有多少是因为 backlog 满了导致丢弃的新连接。 39 | 40 | 如果确认是 backlog 满了导致的丢包,建议调高 backlog 的值,内核参数为 `net.ipv4.tcp_max_syn_backlog`。 41 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/keep-containercreating-or-waiting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 一直处于 ContainerCreating 或 Waiting 状态" 3 | --- 4 | 5 | ## Pod 配置错误 6 | 7 | * 检查是否打包了正确的镜像 8 | * 检查配置了正确的容器参数 9 | 10 | ## 挂载 Volume 失败 11 | 12 | Volume 挂载失败也分许多种情况,先列下我这里目前已知的。 13 | 14 | ### Pod 漂移没有正常解挂之前的磁盘 15 | 16 | 在云尝试托管的 K8S 服务环境下,默认挂载的 Volume 一般是块存储类型的云硬盘,如果某个节点挂了,kubelet 无法正常运行或与 apiserver 通信,到达时间阀值后会触发驱逐,自动在其它节点上启动相同的副本 (Pod 漂移),但是由于被驱逐的 Node 无法正常运行并不知道自己被驱逐了,也就没有正常执行解挂,cloud-controller-manager 也在等解挂成功后再调用云厂商的接口将磁盘真正从节点上解挂,通常会等到一个时间阀值后 cloud-controller-manager 会强制解挂云盘,然后再将其挂载到 Pod 最新所在节点上,这种情况下 ContainerCreating 的时间相对长一点,但一般最终是可以启动成功的,除非云厂商的 cloud-controller-manager 逻辑有 bug。 17 | 18 | ### 命中 K8S 挂载 configmap/secret 的 subpath 的 bug 19 | 20 | 最近发现如果 Pod 挂载了 configmap 或 secret, 如果后面修改了 configmap 或 secret 的内容,Pod 里的容器又原地重启了(比如存活检查失败被 kill 然后重启拉起),就会触发 K8S 的这个 bug,团队的小伙伴已提 PR: https://github.com/kubernetes/kubernetes/pull/82784 21 | 22 | 如果是这种情况,容器会一直启动不成功,可以看到类似以下的报错: 23 | 24 | ``` bash 25 | $ kubectl -n prod get pod -o yaml manage-5bd487cf9d-bqmvm 26 | ... 27 | lastState: terminated 28 | containerID: containerd://e6746201faa1dfe7f3251b8c30d59ebf613d99715f3b800740e587e681d2a903 29 | exitCode: 128 30 | finishedAt: 2019-09-15T00:47:22Z 31 | message: 'failed to create containerd task: OCI runtime create failed: container_linux.go:345: 32 | starting container process caused "process_linux.go:424: container init 33 | caused \"rootfs_linux.go:58: mounting \\\"/var/lib/kubelet/pods/211d53f4-d08c-11e9-b0a7-b6655eaf02a6/volume-subpaths/manage-config-volume/manage/0\\\" 34 | to rootfs \\\"/run/containerd/io.containerd.runtime.v1.linux/k8s.io/e6746201faa1dfe7f3251b8c30d59ebf613d99715f3b800740e587e681d2a903/rootfs\\\" 35 | at \\\"/run/containerd/io.containerd.runtime.v1.linux/k8s.io/e6746201faa1dfe7f3251b8c30d59ebf613d99715f3b800740e587e681d2a903/rootfs/app/resources/application.properties\\\" 36 | caused \\\"no such file or directory\\\"\"": unknown' 37 | ``` 38 | 39 | ## 磁盘爆满 40 | 41 | 启动 Pod 会调 CRI 接口创建容器,容器运行时创建容器时通常会在数据目录下为新建的容器创建一些目录和文件,如果数据目录所在的磁盘空间满了就会创建失败并报错: 42 | 43 | ```bash 44 | Events: 45 | Type Reason Age From Message 46 | ---- ------ ---- ---- ------- 47 | Warning FailedCreatePodSandBox 2m (x4307 over 16h) kubelet, 10.179.80.31 (combined from similar events): Failed create pod sandbox: rpc error: code = Unknown desc = failed to create a sandbox for pod "apigateway-6dc48bf8b6-l8xrw": Error response from daemon: mkdir /var/lib/docker/aufs/mnt/1f09d6c1c9f24e8daaea5bf33a4230de7dbc758e3b22785e8ee21e3e3d921214-init: no space left on device 48 | ``` 49 | 50 | 解决方法参考本书 [处理实践:磁盘爆满](/troubleshooting/handle/disk-full.md) 51 | 52 | ## 节点内存碎片化 53 | 54 | 如果节点上内存碎片化严重,缺少大页内存,会导致即使总的剩余内存较多,但还是会申请内存失败,参考 [处理实践: 内存碎片化](/troubleshooting/handle/memory-fragmentation.md) 55 | 56 | ## limit 设置太小或者单位不对 57 | 58 | 如果 limit 设置过小以至于不足以成功运行 Sandbox 也会造成这种状态,常见的是因为 memory limit 单位设置不对造成的 limit 过小,比如误将 memory 的 limit 单位像 request 一样设置为小 `m`,这个单位在 memory 不适用,会被 k8s 识别成 byte, 应该用 `Mi` 或 `M`。, 59 | 60 | 举个例子: 如果 memory limit 设为 1024m 表示限制 1.024 Byte,这么小的内存, pause 容器一起来就会被 cgroup-oom kill 掉,导致 pod 状态一直处于 ContainerCreating。 61 | 62 | 这种情况通常会报下面的 event: 63 | 64 | ``` txt 65 | Pod sandbox changed, it will be killed and re-created。 66 | ``` 67 | 68 | kubelet 报错: 69 | 70 | ``` txt 71 | to start sandbox container for pod ... Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "process_linux.go:301: running exec setns process for init caused \"signal: killed\"": unknown 72 | ``` 73 | 74 | ## 拉取镜像失败 75 | 76 | 镜像拉取失败也分很多情况,这里列举下: 77 | 78 | * 配置了错误的镜像 79 | * Kubelet 无法访问镜像仓库(比如默认 pause 镜像在 gcr.io 上,国内环境访问需要特殊处理) 80 | * 拉取私有镜像的 imagePullSecret 没有配置或配置有误 81 | * 镜像太大,拉取超时(可以适当调整 kubelet 的 --image-pull-progress-deadline 和 --runtime-request-timeout 选项) 82 | 83 | ## CNI 网络错误 84 | 85 | 如果发生 CNI 网络错误通常需要检查下网络插件的配置和运行状态,如果没有正确配置或正常运行通常表现为: 86 | 87 | * 无法配置 Pod 网络 88 | * 无法分配 Pod IP 89 | 90 | ## controller-manager 异常 91 | 92 | 查看 master 上 kube-controller-manager 状态,异常的话尝试重启。 93 | 94 | ## 安装 docker 没删干净旧版本 95 | 96 | 如果节点上本身有 docker 或者没删干净,然后又安装 docker,比如在 centos 上用 yum 安装: 97 | 98 | ``` bash 99 | yum install -y docker 100 | ``` 101 | 102 | 这样可能会导致 dockerd 创建容器一直不成功,从而 Pod 状态一直 ContainerCreating,查看 event 报错: 103 | 104 | ``` 105 | Type Reason Age From Message 106 | ---- ------ ---- ---- ------- 107 | Warning FailedCreatePodSandBox 18m (x3583 over 83m) kubelet, 192.168.4.5 (combined from similar events): Failed create pod sandbox: rpc error: code = Unknown desc = failed to start sandbox container for pod "nginx-7db9fccd9b-2j6dh": Error response from daemon: ttrpc: client shutting down: read unix @->@/containerd-shim/moby/de2bfeefc999af42783115acca62745e6798981dff75f4148fae8c086668f667/shim.sock: read: connection reset by peer: unknown 108 | Normal SandboxChanged 3m12s (x4420 over 83m) kubelet, 192.168.4.5 Pod sandbox changed, it will be killed and re-created. 109 | ``` 110 | 111 | 可能是因为重复安装 docker 版本不一致导致一些组件之间不兼容,从而导致 dockerd 无法正常创建容器。 112 | 113 | ## 存在同名容器 114 | 115 | 如果节点上已有同名容器,创建 sandbox 就会失败,event: 116 | 117 | ``` 118 | Warning FailedCreatePodSandBox 2m kubelet, 10.205.8.91 Failed create pod sandbox: rpc error: code = Unknown desc = failed to create a sandbox for pod "lomp-ext-d8c8b8c46-4v8tl": operation timeout: context deadline exceeded 119 | Warning FailedCreatePodSandBox 3s (x12 over 2m) kubelet, 10.205.8.91 Failed create pod sandbox: rpc error: code = Unknown desc = failed to create a sandbox for pod "lomp-ext-d8c8b8c46-4v8tl": Error response from daemon: Conflict. The container name "/k8s_POD_lomp-ext-d8c8b8c46-4v8tl_default_65046a06-f795-11e9-9bb6-b67fb7a70bad_0" is already in use by container "30aa3f5847e0ce89e9d411e76783ba14accba7eb7743e605a10a9a862a72c1e2". You have to remove (or rename) that container to be able to reuse that name. 120 | ``` 121 | 122 | 关于什么情况下会产生同名容器,这个有待研究。 123 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/keep-crashloopbackoff.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 处于 CrashLoopBackOff 状态" 3 | --- 4 | 5 | Pod 如果处于 `CrashLoopBackOff` 状态说明之前是启动了,只是又异常退出了,只要 Pod 的 [restartPolicy](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy) 不是 Never 就可能被重启拉起,此时 Pod 的 `RestartCounts` 通常是大于 0 的,可以先看下容器进程的退出状态码来缩小问题范围,参考本书 [排错技巧: 分析 ExitCode 定位 Pod 异常退出原因](../../../trick/analysis-exitcode/) 6 | 7 | ## 容器进程主动退出 8 | 9 | 如果是容器进程主动退出,退出状态码一般在 0-128 之间,除了可能是业务程序 BUG,还有其它许多可能原因,参考: [容器进程主动退出](../container-proccess-exit-by-itself/) 10 | 11 | ## 系统 OOM 12 | 13 | 如果发生系统 OOM,可以看到 Pod 中容器退出状态码是 137,表示被 `SIGKILL` 信号杀死,同时内核会报错: `Out of memory: Kill process ...`。大概率是节点上部署了其它非 K8S 管理的进程消耗了比较多的内存,或者 kubelet 的 `--kube-reserved` 和 `--system-reserved` 配的比较小,没有预留足够的空间给其它非容器进程,节点上所有 Pod 的实际内存占用总量不会超过 `/sys/fs/cgroup/memory/kubepods` 这里 cgroup 的限制,这个限制等于 `capacity - "kube-reserved" - "system-reserved"`,如果预留空间设置合理,节点上其它非容器进程(kubelet, dockerd, kube-proxy, sshd 等) 内存占用没有超过 kubelet 配置的预留空间是不会发生系统 OOM 的,可以根据实际需求做合理的调整。 14 | 15 | ## cgroup OOM 16 | 17 | 如果是 cgrou OOM 杀掉的进程,从 Pod 事件的下 `Reason` 可以看到是 `OOMKilled`,说明容器实际占用的内存超过 limit 了,同时内核日志会报: `Memory cgroup out of memory`。 可以根据需求调整下 limit。 18 | 19 | ## 节点内存碎片化 20 | 21 | 如果节点上内存碎片化严重,缺少大页内存,会导致即使总的剩余内存较多,但还是会申请内存失败,参考 [处理实践: 内存碎片化](../../../handle/memory-fragmentation/) 22 | 23 | ## 健康检查失败 24 | 25 | 参考 [Pod 健康检查失败](../healthcheck-failed/) 进一步定位。 26 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/keep-error.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 一直处于 Error 状态" 3 | --- 4 | 5 | TODO: 展开优化 6 | 7 | 通常处于 Error 状态说明 Pod 启动过程中发生了错误。常见的原因包括: 8 | 9 | * 依赖的 ConfigMap、Secret 或者 PV 等不存在 10 | * 请求的资源超过了管理员设置的限制,比如超过了 LimitRange 等 11 | * 违反集群的安全策略,比如违反了 PodSecurityPolicy 等 12 | * 容器无权操作集群内的资源,比如开启 RBAC 后,需要为 ServiceAccount 配置角色绑定 13 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/keep-imageinspecterror.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 一直处于 ImageInspectError 状态" 3 | --- 4 | 5 | 通常是镜像文件损坏了,可以尝试删除损坏的镜像重新拉取 6 | 7 | TODO: 完善 -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/keep-imagepullbackoff.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 一直处于 ImagePullBackOff 状态" 3 | --- 4 | 5 | ## http 类型 registry,地址未加入到 insecure-registry 6 | 7 | dockerd 默认从 https 类型的 registry 拉取镜像,如果使用 https 类型的 registry,则必须将它添加到 insecure-registry 参数中,然后重启或 reload dockerd 生效。 8 | 9 | ## https 自签发类型 resitry,没有给节点添加 ca 证书 10 | 11 | 如果 registry 是 https 类型,但证书是自签发的,dockerd 会校验 registry 的证书,校验成功才能正常使用镜像仓库,要想校验成功就需要将 registry 的 ca 证书放置到 `/etc/docker/certs.d//ca.crt` 位置。 12 | 13 | ## 私有镜像仓库认证失败 14 | 15 | 如果 registry 需要认证,但是 Pod 没有配置 imagePullSecret,配置的 Secret 不存在或者有误都会认证失败。 16 | 17 | ## 镜像文件损坏 18 | 19 | 如果 push 的镜像文件损坏了,下载下来也用不了,需要重新 push 镜像文件。 20 | 21 | ## 镜像拉取超时 22 | 23 | 如果节点上新起的 Pod 太多就会有许多可能会造成容器镜像下载排队,如果前面有许多大镜像需要下载很长时间,后面排队的 Pod 就会报拉取超时。 24 | 25 | kubelet 默认串行下载镜像: 26 | 27 | ``` txt 28 | --serialize-image-pulls Pull images one at a time. We recommend *not* changing the default value on nodes that run docker daemon with version < 1.9 or an Aufs storage backend. Issue #10959 has more details. (default true) 29 | ``` 30 | 31 | 也可以开启并行下载并控制并发: 32 | 33 | ``` txt 34 | --registry-qps int32 If > 0, limit registry pull QPS to this value. If 0, unlimited. (default 5) 35 | --registry-burst int32 Maximum size of a bursty pulls, temporarily allows pulls to burst to this number, while still not exceeding registry-qps. Only used if --registry-qps > 0 (default 10) 36 | ``` 37 | 38 | ## 镜像不不存在 39 | 40 | kubelet 日志: 41 | 42 | ``` bash 43 | PullImage "imroc/test:v0.2" from image service failed: rpc error: code = Unknown desc = Error response from daemon: manifest for imroc/test:v0.2 not found 44 | ``` -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/keep-pending.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 一直处于 Pending 状态" 3 | --- 4 | 5 | Pending 状态说明 Pod 还没有被调度到某个节点上,需要看下 Pod 事件进一步判断原因,比如: 6 | 7 | ``` bash 8 | $ kubectl describe pod tikv-0 9 | ... 10 | Events: 11 | Type Reason Age From Message 12 | ---- ------ ---- ---- ------- 13 | Warning FailedScheduling 3m (x106 over 33m) default-scheduler 0/4 nodes are available: 1 node(s) had no available volume zone, 2 Insufficient cpu, 3 Insufficient memory. 14 | ``` 15 | 16 | 下面列举下可能原因和解决方法。 17 | 18 | ## 节点资源不够 19 | 20 | 节点资源不够有以下几种情况: 21 | 22 | * CPU 负载过高 23 | * 剩余可以被分配的内存不够 24 | * 剩余可用 GPU 数量不够 (通常在机器学习场景,GPU 集群环境) 25 | 26 | 如果判断某个 Node 资源是否足够? 通过 `kubectl describe node ` 查看 node 资源情况,关注以下信息: 27 | 28 | * `Allocatable`: 表示此节点能够申请的资源总和 29 | * `Allocated resources`: 表示此节点已分配的资源 (Allocatable 减去节点上所有 Pod 总的 Request) 30 | 31 | 前者与后者相减,可得出剩余可申请的资源。如果这个值小于 Pod 的 request,就不满足 Pod 的资源要求,Scheduler 在 Predicates (预选) 阶段就会剔除掉这个 Node,也就不会调度上去。 32 | 33 | 34 | ## 不满足 nodeSelector 与 affinity 35 | 36 | 如果 Pod 包含 nodeSelector 指定了节点需要包含的 label,调度器将只会考虑将 Pod 调度到包含这些 label 的 Node 上,如果没有 Node 有这些 label 或者有这些 label 的 Node 其它条件不满足也将会无法调度。参考官方文档:https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector 37 | 38 | 如果 Pod 包含 affinity(亲和性)的配置,调度器根据调度算法也可能算出没有满足条件的 Node,从而无法调度。affinity 有以下几类: 39 | 40 | * nodeAffinity: 节点亲和性,可以看成是增强版的 nodeSelector,用于限制 Pod 只允许被调度到某一部分 Node。 41 | * podAffinity: Pod 亲和性,用于将一些有关联的 Pod 调度到同一个地方,同一个地方可以是指同一个节点或同一个可用区的节点等。 42 | * podAntiAffinity: Pod 反亲和性,用于避免将某一类 Pod 调度到同一个地方避免单点故障,比如将集群 DNS 服务的 Pod 副本都调度到不同节点,避免一个节点挂了造成整个集群 DNS 解析失败,使得业务中断。 43 | 44 | ## Node 存在 Pod 没有容忍的污点 45 | 46 | 如果节点上存在污点 (Taints),而 Pod 没有响应的容忍 (Tolerations),Pod 也将不会调度上去。通过 describe node 可以看下 Node 有哪些 Taints: 47 | 48 | ``` bash 49 | $ kubectl describe nodes host1 50 | ... 51 | Taints: special=true:NoSchedule 52 | ... 53 | ``` 54 | 55 | 通常解决方法有两个: 56 | 57 | 1. 删除污点: 58 | 59 | ``` bash 60 | kubectl taint nodes host1 special- 61 | ``` 62 | 63 | 2. 给 Pod 加上这个污点的容忍: 64 | 65 | ``` yaml 66 | tolerations: 67 | - key: "special" 68 | operator: "Equal" 69 | value: "true" 70 | effect: "NoSchedule" 71 | ``` 72 | 73 | 我们通常使用后者的方法来解决。污点既可以是手动添加也可以是被自动添加,下面来深入分析一下。 74 | 75 | ### 手动添加的污点 76 | 77 | 通过类似以下方式可以给节点添加污点: 78 | 79 | ``` bash 80 | $ kubectl taint node host1 special=true:NoSchedule 81 | node "host1" tainted 82 | ``` 83 | 84 | 另外,有些场景下希望新加的节点默认不调度 Pod,直到调整完节点上某些配置才允许调度,就给新加的节点都加上 `node.kubernetes.io/unschedulable` 这个污点。 85 | 86 | ### 自动添加的污点 87 | 88 | 如果节点运行状态不正常,污点也可以被自动添加,从 v1.12 开始,`TaintNodesByCondition` 特性进入 Beta 默认开启,controller manager 会检查 Node 的 Condition,如果命中条件就自动为 Node 加上相应的污点,这些 Condition 与 Taints 的对应关系如下: 89 | 90 | ``` txt 91 | Conditon Value Taints 92 | -------- ----- ------ 93 | OutOfDisk True node.kubernetes.io/out-of-disk 94 | Ready False node.kubernetes.io/not-ready 95 | Ready Unknown node.kubernetes.io/unreachable 96 | MemoryPressure True node.kubernetes.io/memory-pressure 97 | PIDPressure True node.kubernetes.io/pid-pressure 98 | DiskPressure True node.kubernetes.io/disk-pressure 99 | NetworkUnavailable True node.kubernetes.io/network-unavailable 100 | ``` 101 | 102 | 解释下上面各种条件的意思: 103 | 104 | * OutOfDisk 为 True 表示节点磁盘空间不够了 105 | * Ready 为 False 表示节点不健康 106 | * Ready 为 Unknown 表示节点失联,在 `node-monitor-grace-period` 这么长的时间内没有上报状态 controller-manager 就会将 Node 状态置为 Unknown (默认 40s) 107 | * MemoryPressure 为 True 表示节点内存压力大,实际可用内存很少 108 | * PIDPressure 为 True 表示节点上运行了太多进程,PID 数量不够用了 109 | * DiskPressure 为 True 表示节点上的磁盘可用空间太少了 110 | * NetworkUnavailable 为 True 表示节点上的网络没有正确配置,无法跟其它 Pod 正常通信 111 | 112 | 另外,在云环境下,比如腾讯云 TKE,添加新节点会先给这个 Node 加上 `node.cloudprovider.kubernetes.io/uninitialized` 的污点,等 Node 初始化成功后才自动移除这个污点,避免 Pod 被调度到没初始化好的 Node 上。 113 | 114 | ## 低版本 kube-scheduler 的 bug 115 | 116 | 可能是低版本 `kube-scheduler` 的 bug, 可以升级下调度器版本。 117 | 118 | ## kube-scheduler 没有正常运行 119 | 120 | 检查 maser 上的 `kube-scheduler` 是否运行正常,异常的话可以尝试重启临时恢复。 121 | 122 | ## 驱逐后其它可用节点与当前节点有状态应用不在同一个可用区 123 | 124 | 有时候服务部署成功运行过,但在某个时候节点突然挂了,此时就会触发驱逐,创建新的副本调度到其它节点上,对于已经挂载了磁盘的 Pod,它通常需要被调度到跟当前节点和磁盘在同一个可用区,如果集群中同一个可用区的节点不满足调度条件,即使其它可用区节点各种条件都满足,但不跟当前节点在同一个可用区,也是不会调度的。为什么需要限制挂载了磁盘的 Pod 不能漂移到其它可用区的节点?试想一下,云上的磁盘虽然可以被动态挂载到不同机器,但也只是相对同一个数据中心,通常不允许跨数据中心挂载磁盘设备,因为网络时延会极大的降低 IO 速率。 125 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/keep-unkown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod 一直处于 Unknown 状态" 3 | --- 4 | 5 | TODO: 完善 6 | 7 | 通常是节点失联,没有上报状态给 apiserver,到达阀值后 controller-manager 认为节点失联并将其状态置为 `Unknown`。 8 | 9 | 可能原因: 10 | 11 | * 节点高负载导致无法上报 12 | * 节点宕机 13 | * 节点被关机 14 | * 网络不通 15 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/pod/slow-terminating.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pod Terminating 慢" 3 | state: Alpha 4 | --- 5 | 6 | ## 可能原因 7 | 8 | * 进程通过 bash -c 启动导致 kill 信号无法透传给业务进程 9 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/trick/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "排错技巧" 3 | weight: 10 4 | --- 5 | 6 | {{% children %}} 7 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/trick/analysis-exitcode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "分析 ExitCode 定位 Pod 异常退出原因" 3 | LastModifierDisplayName: "roc" 4 | LastModifierURL: "https://imroc.io" 5 | date: 2020-03-12 6 | --- 7 | 8 | 使用 `kubectl describe pod ` 查看异常 pod 的状态: 9 | 10 | ```bash 11 | Containers: 12 | kubedns: 13 | Container ID: docker://5fb8adf9ee62afc6d3f6f3d9590041818750b392dff015d7091eaaf99cf1c945 14 | Image: ccr.ccs.tencentyun.com/library/kubedns-amd64:1.14.4 15 | Image ID: docker-pullable://ccr.ccs.tencentyun.com/library/kubedns-amd64@sha256:40790881bbe9ef4ae4ff7fe8b892498eecb7fe6dcc22661402f271e03f7de344 16 | Ports: 10053/UDP, 10053/TCP, 10055/TCP 17 | Host Ports: 0/UDP, 0/TCP, 0/TCP 18 | Args: 19 | --domain=cluster.local. 20 | --dns-port=10053 21 | --config-dir=/kube-dns-config 22 | --v=2 23 | State: Running 24 | Started: Tue, 27 Aug 2019 10:58:49 +0800 25 | Last State: Terminated 26 | Reason: Error 27 | Exit Code: 255 28 | Started: Tue, 27 Aug 2019 10:40:42 +0800 29 | Finished: Tue, 27 Aug 2019 10:58:27 +0800 30 | Ready: True 31 | Restart Count: 1 32 | ``` 33 | 34 | 在容器列表里看 `Last State` 字段,其中 `ExitCode` 即程序上次退出时的状态码,如果不为 0,表示异常退出,我们可以分析下原因。 35 | 36 | ## 退出状态码的区间 37 | 38 | * 必须在 0-255 之间 39 | * 0 表示正常退出 40 | * 外界中断将程序退出的时候状态码区间在 129-255,\(操作系统给程序发送中断信号,比如 `kill -9` 是 `SIGKILL`,`ctrl+c` 是 `SIGINT`\) 41 | * 一般程序自身原因导致的异常退出状态区间在 1-128 \(这只是一般约定,程序如果一定要用129-255的状态码也是可以的\) 42 | 43 | 假如写代码指定的退出状态码时不在 0-255 之间,例如: `exit(-1)`,这时会自动做一个转换,最终呈现的状态码还是会在 0-255 之间。我们把状态码记为 `code` 44 | 45 | * 当指定的退出时状态码为负数,那么转换公式如下: 46 | 47 | ```text 48 | 256 - (|code| % 256) 49 | ``` 50 | 51 | * 当指定的退出时状态码为正数,那么转换公式如下: 52 | 53 | ```text 54 | code % 256 55 | ``` 56 | 57 | ## 常见异常状态码 58 | 59 | * 137 \(被 `SIGKILL` 中断信号杀死\) 60 | * 此状态码一般是因为 pod 中容器内存达到了它的资源限制\(`resources.limits`\),一般是内存溢出\(OOM\),CPU达到限制只需要不分时间片给程序就可以。因为限制资源是通过 linux 的 cgroup 实现的,所以 cgroup 会将此容器强制杀掉,类似于 `kill -9`,此时在 `describe pod` 中可以看到 Reason 是 `OOMKilled` 61 | * 还可能是宿主机本身资源不够用了\(OOM\),内核会选取一些进程杀掉来释放内存 62 | * 不管是 cgroup 限制杀掉进程还是因为节点机器本身资源不够导致进程死掉,都可以从系统日志中找到记录: 63 | 64 | > ubuntu 的系统日志在 `/var/log/syslog`,centos 的系统日志在 `/var/log/messages`,都可以用 `journalctl -k` 来查看系统日志 65 | 66 | * 也可能是 livenessProbe \(存活检查\) 失败,kubelet 杀死的 pod 67 | * 还可能是被恶意木马进程杀死 68 | * 1 和 255 69 | * 这种可能是一般错误,具体错误原因只能看容器日志,因为很多程序员写异常退出时习惯用 `exit(1)` 或 `exit(-1)`,-1 会根据转换规则转成 255 70 | 71 | ## 状态码参考 72 | 73 | 这里罗列了一些状态码的含义:[Appendix E. Exit Codes With Special Meanings](http://tldp.org/LDP/abs/html/exitcodes.html) 74 | 75 | ## Linux 标准中断信号 76 | 77 | Linux 程序被外界中断时会发送中断信号,程序退出时的状态码就是中断信号值加上 128 得到的,比如 `SIGKILL` 的中断信号值为 9,那么程序退出状态码就为 9+128=137。以下是标准信号值参考: 78 | 79 | ```text 80 | Signal Value Action Comment 81 | ────────────────────────────────────────────────────────────────────── 82 | SIGHUP 1 Term Hangup detected on controlling terminal 83 | or death of controlling process 84 | SIGINT 2 Term Interrupt from keyboard 85 | SIGQUIT 3 Core Quit from keyboard 86 | SIGILL 4 Core Illegal Instruction 87 | SIGABRT 6 Core Abort signal from abort(3) 88 | SIGFPE 8 Core Floating-point exception 89 | SIGKILL 9 Term Kill signal 90 | SIGSEGV 11 Core Invalid memory reference 91 | SIGPIPE 13 Term Broken pipe: write to pipe with no 92 | readers; see pipe(7) 93 | SIGALRM 14 Term Timer signal from alarm(2) 94 | SIGTERM 15 Term Termination signal 95 | SIGUSR1 30,10,16 Term User-defined signal 1 96 | SIGUSR2 31,12,17 Term User-defined signal 2 97 | SIGCHLD 20,17,18 Ign Child stopped or terminated 98 | SIGCONT 19,18,25 Cont Continue if stopped 99 | SIGSTOP 17,19,23 Stop Stop process 100 | SIGTSTP 18,20,24 Stop Stop typed at terminal 101 | SIGTTIN 21,21,26 Stop Terminal input for background process 102 | SIGTTOU 22,22,27 Stop Terminal output for background process 103 | ``` 104 | 105 | ## C/C++ 退出状态码 106 | 107 | `/usr/include/sysexits.h` 试图将退出状态码标准化\(仅限 C/C++\): 108 | 109 | ```text 110 | #define EX_OK 0 /* successful termination */ 111 | 112 | #define EX__BASE 64 /* base value for error messages */ 113 | 114 | #define EX_USAGE 64 /* command line usage error */ 115 | #define EX_DATAERR 65 /* data format error */ 116 | #define EX_NOINPUT 66 /* cannot open input */ 117 | #define EX_NOUSER 67 /* addressee unknown */ 118 | #define EX_NOHOST 68 /* host name unknown */ 119 | #define EX_UNAVAILABLE 69 /* service unavailable */ 120 | #define EX_SOFTWARE 70 /* internal software error */ 121 | #define EX_OSERR 71 /* system error (e.g., can't fork) */ 122 | #define EX_OSFILE 72 /* critical OS file missing */ 123 | #define EX_CANTCREAT 73 /* can't create (user) output file */ 124 | #define EX_IOERR 74 /* input/output error */ 125 | #define EX_TEMPFAIL 75 /* temp failure; user is invited to retry */ 126 | #define EX_PROTOCOL 76 /* remote error in protocol */ 127 | #define EX_NOPERM 77 /* permission denied */ 128 | #define EX_CONFIG 78 /* configuration error */ 129 | 130 | #define EX__MAX 78 /* maximum listed value */ 131 | ``` 132 | 133 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/trick/capture-packets-in-container.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "容器内抓包定位网络问题" 3 | --- 4 | 5 | 在使用 kubernetes 跑应用的时候,可能会遇到一些网络问题,比较常见的是服务端无响应\(超时\)或回包内容不正常,如果没找出各种配置上有问题,这时我们需要确认数据包到底有没有最终被路由到容器里,或者报文到达容器的内容和出容器的内容符不符合预期,通过分析报文可以进一步缩小问题范围。那么如何在容器内抓包呢?本文提供实用的脚本一键进入容器网络命名空间\(netns\),使用宿主机上的tcpdump进行抓包。 6 | 7 | ## 使用脚本一键进入 pod netns 抓包 8 | 9 | * 发现某个服务不通,最好将其副本数调为1,并找到这个副本 pod 所在节点和 pod 名称 10 | 11 | ```bash 12 | kubectl get pod -o wide 13 | ``` 14 | 15 | * 登录 pod 所在节点,将如下脚本粘贴到 shell \(注册函数到当前登录的 shell,我们后面用\) 16 | 17 | ```bash 18 | function e() { 19 | set -eu 20 | ns=${2-"default"} 21 | pod=`kubectl -n $ns describe pod $1 | grep -A10 "^Containers:" | grep -Eo 'docker://.*$' | head -n 1 | sed 's/docker:\/\/\(.*\)$/\1/'` 22 | pid=`docker inspect -f {{.State.Pid}} $pod` 23 | echo "entering pod netns for $ns/$1" 24 | cmd="nsenter -n --target $pid" 25 | echo $cmd 26 | $cmd 27 | } 28 | ``` 29 | 30 | * 一键进入 pod 所在的 netns,格式:`e POD_NAME NAMESPACE`,示例: 31 | 32 | ```bash 33 | e istio-galley-58c7c7c646-m6568 istio-system 34 | e proxy-5546768954-9rxg6 # 省略 NAMESPACE 默认为 default 35 | ``` 36 | 37 | * 这时已经进入 pod 的 netns,可以执行宿主机上的 `ip a` 或 `ifconfig` 来查看容器的网卡,执行 `netstat -tunlp` 查看当前容器监听了哪些端口,再通过 `tcpdump` 抓包: 38 | 39 | ```bash 40 | tcpdump -i eth0 -w test.pcap port 80 41 | ``` 42 | 43 | * `ctrl-c` 停止抓包,再用 `scp` 或 `sz` 将抓下来的包下载到本地使用 `wireshark` 分析,提供一些常用的 `wireshark` 过滤语法: 44 | 45 | ```bash 46 | # 使用 telnet 连上并发送一些测试文本,比如 "lbtest", 47 | # 用下面语句可以看发送的测试报文有没有到容器 48 | tcp contains "lbtest" 49 | # 如果容器提供的是http服务,可以使用 curl 发送一些测试路径的请求, 50 | # 通过下面语句过滤 uri 看报文有没有都容器 51 | http.request.uri=="/mytest" 52 | ``` 53 | 54 | ## 脚本原理 55 | 56 | 我们解释下步骤二中用到的脚本的原理 57 | 58 | * 查看指定 pod 运行的容器 ID 59 | 60 | ```bash 61 | kubectl describe pod -n mservice 62 | ``` 63 | 64 | * 获得容器进程的 pid 65 | 66 | ```bash 67 | docker inspect -f {{.State.Pid}} 68 | ``` 69 | 70 | * 进入该容器的 network namespace 71 | 72 | ```bash 73 | nsenter -n --target 74 | ``` 75 | 76 | 依赖宿主机的命名:`kubectl`, `docker`, `nsenter`, `grep`, `head`, `sed` 77 | 78 | -------------------------------------------------------------------------------- /content/zh/troubleshooting/trick/use-systemtap-to-locate-problems.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "使用 Systemtap 定位疑难杂症" 3 | --- 4 | 5 | ## 安装 6 | 7 | ### Ubuntu 8 | 9 | 安装 systemtap: 10 | 11 | ```bash 12 | apt install -y systemtap 13 | ``` 14 | 15 | 运行 `stap-prep` 检查还有什么需要安装: 16 | 17 | ```bash 18 | $ stap-prep 19 | Please install linux-headers-4.4.0-104-generic 20 | You need package linux-image-4.4.0-104-generic-dbgsym but it does not seem to be available 21 | Ubuntu -dbgsym packages are typically in a separate repository 22 | Follow https://wiki.ubuntu.com/DebuggingProgramCrash to add this repository 23 | 24 | apt install -y linux-headers-4.4.0-104-generic 25 | ``` 26 | 27 | 提示需要 dbgsym 包但当前已有软件源中并不包含,需要使用第三方软件源安装,下面是 dbgsym 安装方法\(参考官方wiki: [https://wiki.ubuntu.com/Kernel/Systemtap](https://wiki.ubuntu.com/Kernel/Systemtap)\): 28 | 29 | ```bash 30 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C8CAB6595FDFF622 31 | 32 | codename=$(lsb_release -c | awk '{print $2}') 33 | sudo tee /etc/apt/sources.list.d/ddebs.list << EOF 34 | deb http://ddebs.ubuntu.com/ ${codename} main restricted universe multiverse 35 | deb http://ddebs.ubuntu.com/ ${codename}-security main restricted universe multiverse 36 | deb http://ddebs.ubuntu.com/ ${codename}-updates main restricted universe multiverse 37 | deb http://ddebs.ubuntu.com/ ${codename}-proposed main restricted universe multiverse 38 | EOF 39 | 40 | sudo apt-get update 41 | ``` 42 | 43 | 配置好源后再运行下 `stap-prep`: 44 | 45 | ```bash 46 | $ stap-prep 47 | Please install linux-headers-4.4.0-104-generic 48 | Please install linux-image-4.4.0-104-generic-dbgsym 49 | ``` 50 | 51 | 提示需要装这两个包,我们安装一下: 52 | 53 | ```bash 54 | apt install -y linux-image-4.4.0-104-generic-dbgsym 55 | apt install -y linux-headers-4.4.0-104-generic 56 | ``` 57 | 58 | ### CentOS 59 | 60 | 安装 systemtap: 61 | 62 | ```bash 63 | yum install -y systemtap 64 | ``` 65 | 66 | 默认没装 `debuginfo`,我们需要装一下,添加软件源 `/etc/yum.repos.d/CentOS-Debug.repo`: 67 | 68 | ```bash 69 | [debuginfo] 70 | name=CentOS-$releasever - DebugInfo 71 | baseurl=http://debuginfo.centos.org/$releasever/$basearch/ 72 | gpgcheck=0 73 | enabled=1 74 | protect=1 75 | priority=1 76 | ``` 77 | 78 | 执行 `stap-prep` \(会安装 `kernel-debuginfo`\) 79 | 80 | 最后检查确保 `kernel-debuginfo` 和 `kernel-devel` 均已安装并且版本跟当前内核版本相同,如果有多个版本,就删除跟当前内核版本不同的包\(通过`uname -r`查看当前内核版本\)。 81 | 82 | 重点检查是否有多个版本的 `kernel-devel`: 83 | 84 | ```bash 85 | $ rpm -qa | grep kernel-devel 86 | kernel-devel-3.10.0-327.el7.x86_64 87 | kernel-devel-3.10.0-514.26.2.el7.x86_64 88 | kernel-devel-3.10.0-862.9.1.el7.x86_64 89 | ``` 90 | 91 | 如果存在多个,保证只留跟当前内核版本相同的那个,假设当前内核版本是 `3.10.0-862.9.1.el7.x86_64`,那么使用 rpm 删除多余的版本: 92 | 93 | ```bash 94 | rpm -e kernel-devel-3.10.0-327.el7.x86_64 kernel-devel-3.10.0-514.26.2.el7.x86_64 95 | ``` 96 | 97 | ## 使用 systemtap 揪出杀死容器的真凶 98 | 99 | Pod 莫名其妙被杀死? 可以使用 systemtap 来监视进程的信号发送,原理是 systemtap 将脚本翻译成 C 代码然后调用 gcc 编译成 linux 内核模块,再通过 `modprobe` 加载到内核,根据脚本内容在内核做各种 hook,在这里我们就 hook 一下信号的发送,找出是谁 kill 掉了容器进程。 100 | 101 | 首先,找到被杀死的 pod 又自动重启的容器的当前 pid,describe 一下 pod: 102 | 103 | ```bash 104 | ...... 105 | Container ID: docker://5fb8adf9ee62afc6d3f6f3d9590041818750b392dff015d7091eaaf99cf1c945 106 | ...... 107 | Last State: Terminated 108 | Reason: Error 109 | Exit Code: 137 110 | Started: Thu, 05 Sep 2019 19:22:30 +0800 111 | Finished: Thu, 05 Sep 2019 19:33:44 +0800 112 | ``` 113 | 114 | 拿到容器 id 反查容器的主进程 pid: 115 | 116 | ```bash 117 | $ docker inspect -f "{{.State.Pid}}" 5fb8adf9ee62afc6d3f6f3d9590041818750b392dff015d7091eaaf99cf1c945 118 | 7942 119 | ``` 120 | 121 | 通过 `Exit Code` 可以看出容器上次退出的状态码,如果进程是被外界中断信号杀死的,退出状态码将在 129-255 之间,137 表示进程是被 SIGKILL 信号杀死的,但我们从这里并不能看出是被谁杀死的。 122 | 123 | 如果问题可以复现,我们可以使用下面的 systemtap 脚本来监视容器是被谁杀死的\(保存为`sg.stp`\): 124 | 125 | ```bash 126 | global target_pid = 7942 127 | probe signal.send{ 128 | if (sig_pid == target_pid) { 129 | printf("%s(%d) send %s to %s(%d)\n", execname(), pid(), sig_name, pid_name, sig_pid); 130 | printf("parent of sender: %s(%d)\n", pexecname(), ppid()) 131 | printf("task_ancestry:%s\n", task_ancestry(pid2task(pid()), 1)); 132 | } 133 | } 134 | ``` 135 | 136 | * 变量 `pid` 的值替换为查到的容器主进程 pid 137 | 138 | 运行脚本: 139 | 140 | ```bash 141 | stap sg.stp 142 | ``` 143 | 144 | 当容器进程被杀死时,脚本捕捉到事件,执行输出: 145 | 146 | ```text 147 | pkill(23549) send SIGKILL to server(7942) 148 | parent of sender: bash(23495) 149 | task_ancestry:swapper/0(0m0.000000000s)=>systemd(0m0.080000000s)=>vGhyM0(19491m2.579563677s)=>sh(33473m38.074571885s)=>bash(33473m38.077072025s)=>bash(33473m38.081028267s)=>bash(33475m4.817798337s)=>pkill(33475m5.202486630s) 150 | ``` 151 | 152 | 通过观察 `task_ancestry` 可以看到杀死进程的所有父进程,在这里可以看到有个叫 `vGhyM0` 的奇怪进程名,通常是中了木马,需要安全专家介入继续排查。 153 | 154 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/images/logo.png -------------------------------------------------------------------------------- /script/deploy.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -eux 4 | 5 | OUTPUT="public" 6 | 7 | echo -e "\033[0;32mDeploying updates to GitHub...\033[0m" 8 | 9 | msg="rebuilding site `date`" 10 | if [ $# -eq 1 ] 11 | then msg="$1" 12 | fi 13 | 14 | # Build the project. 15 | hugo -d $OUTPUT # if using a theme, replace by `hugo -t ` 16 | 17 | # Go To Public folder 18 | cd $OUTPUT 19 | 20 | # Add changes to git. 21 | git add . 22 | 23 | # Commit changes. 24 | 25 | git commit -m "$msg" 26 | 27 | # Push source and build repos. 28 | git push origin gh-pages 29 | 30 | # Come Back 31 | cd .. 32 | 33 | git add . 34 | git commit -m "$msg" 35 | git push origin master 36 | 37 | -------------------------------------------------------------------------------- /script/update-index.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function update_zh() { 4 | cd content 5 | hugo-algolia -s -i "zh/**" --config ../algolia-zh.yaml --output ../public/algolia.json 6 | cd ../ 7 | } 8 | 9 | function update_en() { 10 | hugo-algolia -s --config algolia-en.yaml -i "content/en/**" 11 | } 12 | 13 | function update_all() { 14 | update_zh 15 | update_en 16 | } 17 | 18 | 19 | if [ "$1" == "zh" ]; then 20 | update_zh 21 | elif [ "$1" == "en" ]; then 22 | update_en 23 | else 24 | update_all 25 | fi -------------------------------------------------------------------------------- /static/images/flink-on-k8s.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/images/flink-on-k8s.jpg -------------------------------------------------------------------------------- /static/images/flink-stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/images/flink-stream.png -------------------------------------------------------------------------------- /static/images/grafana-dashboard-pod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/images/grafana-dashboard-pod.png -------------------------------------------------------------------------------- /static/images/grafana-select-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/images/grafana-select-dashboard.png -------------------------------------------------------------------------------- /static/images/loki-grafana-data-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/images/loki-grafana-data-source.png -------------------------------------------------------------------------------- /static/images/loki-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/images/loki-log.png -------------------------------------------------------------------------------- /static/images/spark-on-k8s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/images/spark-on-k8s.png -------------------------------------------------------------------------------- /static/images/tf-operator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/images/tf-operator.png -------------------------------------------------------------------------------- /static/img/alipay-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/alipay-1.png -------------------------------------------------------------------------------- /static/img/alipay-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/alipay-10.png -------------------------------------------------------------------------------- /static/img/alipay-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/alipay-100.png -------------------------------------------------------------------------------- /static/img/alipay-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/alipay-2.png -------------------------------------------------------------------------------- /static/img/alipay-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/alipay-5.png -------------------------------------------------------------------------------- /static/img/alipay-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/alipay-50.png -------------------------------------------------------------------------------- /static/img/alipay-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/alipay-btn.png -------------------------------------------------------------------------------- /static/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/avatar.jpg -------------------------------------------------------------------------------- /static/img/geek_daily_qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/geek_daily_qrcode.jpg -------------------------------------------------------------------------------- /static/img/wechat-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/wechat-1.png -------------------------------------------------------------------------------- /static/img/wechat-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/wechat-10.png -------------------------------------------------------------------------------- /static/img/wechat-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/wechat-100.png -------------------------------------------------------------------------------- /static/img/wechat-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/wechat-2.png -------------------------------------------------------------------------------- /static/img/wechat-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/wechat-5.png -------------------------------------------------------------------------------- /static/img/wechat-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/wechat-50.png -------------------------------------------------------------------------------- /static/img/wechat-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imroc/kubernetes-practice-guide/4d8d68eb78bc101f1b1314d85561fa57165c257e/static/img/wechat-btn.png --------------------------------------------------------------------------------