前言这大抵是春节前的最后一篇随笔了吧，预计是在除夕前完成。同时，这也是我今年思考过的问题之一，谨以此文作为今年工作的收尾之作。不过说实话，这个选题拿来单独开一篇随笔，是否有点太过敷衍了呢？毕竟其中原理十分简单易懂。但我还是决定开这个题，主要还是因为，我认为这篇随笔可以是后续微服务治理有关的文章的前言，就权当一点开胃小菜吧！ 本篇随笔会从传统HTTP服务开始介绍，分析gRPC和HTTP的相似和不同之处，再来介绍如何在Kubernetes中实现gRPC的负载均衡。正文负载均衡在‘哪一层’实现？在开始进入正题之前，我们需要先聊一个概念性问题——负载均衡在哪里实现？这点我就放上OSI七层模型图来讲吧。 我们都知道，Kubernetes中最经典的负载均衡方案便是其核心组件之一——KubeProxy。KubeProxy在K8s中通过IPVS或iptables，转发来自外部或内部的流量请求和数据包。它事实上并没有解析HTTP或者其它七层协议的数据，只是简单地转发三层的TCP/UDP数据。这个时候我们说这个负载均衡是在三层（转发TCP/UDP数据包）实现的，因为HTTP报文(七层协议报文)的内容到HTTP服务器时仍然与原始报文一致。 此外，对于传统的HTTP业务，还有说法是通过Nginx等HTTP服务器进行反向代理。对于Nginx来讲，它不仅会解析三层TCP协议，还会解析七层HTTP协议的数据包，再发送给上游服务器。显然，这个时候的负载均衡是在七层完成的。因为Nginx事实上解析并读取了到达的请求流量，并且对请求进行了再封包才发送至上游服务。HTTP服务如何实现负载均衡？在过去，针对HTTP协议的负载均衡方案，就如上文所述。HTTP/1在协议上不存在多路复用等特性，无论负载均衡在三层还是在七层实现，实际的效果都不会差的太大。所以我们在Kubernetes中常用KubeProxy的TCP负载均衡(Service IP)模式来暴露HTTP服务。此外，常见方案有Nginx(L7)、HAProxy(L3/L7)、Apache2(L7)、Traefik(L3/L7)等。 笔者注: 我这里讨论的HTTP协议是HTTP/1(含1.x)，HTTP/2、gRPC协议的方案需要纯L7层代理，在下一段讨论。gRPC服务为何需要特殊方案实现负载均衡？首先来聊聊gRPC协议，gRPC协议事实上是基于HTTP/2调整得来的，本身兼具HTTP/2的诸多特性。HTTP/2相较HTTP/1做出了许多改进，这些改进也同时导致了传统L3负载均衡方案(TCP连接级负载均衡)在HTTP/2上并不起作用。为什么L3负载均衡方案对HTTP/2(gRPC)无效？HTTP/2在数据传输效率上有了质的提升，其主要原因就是它的多路复用特性上。多路复用，即同一客户端对同一服务端的多次请求，可以复用同一个TCP连接完成。换句话来讲，HTTP/2在设计上就倾向在一条TCP连接上把所有事情做完。 我们目前的业务场景，其实可以简单用这样一张图表示: 如图，gRPC在星河内部主要应用于对性能要求高的业务层和计算层服务的连接中，这个连接在业务层服务启动时就会建立。这也意味着，基于TCP连接进行的L3负载均衡(连接级负载均衡)，对HTTP/2并不起作用——因为在KubeProxy、HAProxy(L3)、Traefik(L3)等负载均衡器眼里，不考虑服务重启、更新的情况下，无论L7的HTTP/2协议进行了多少次请求，在L3层都仅仅是一条TCP连接。对计算层服务来讲，这种粒度的负载均衡会直接导致单个服务副本负载过高，在高峰时期出现“一核有难，多核围观”的情况。当然这里提到的问题跟CPU核心调度没有关系，只是拿来做一个类比。这种模式下，L3负载均衡完全无法满足业务场景的需要。gRPC负载均衡的方案目前主流的对gRPC服务实现负载均衡的方案，主要分客户端负载均衡和服务端负载均衡。当然这里的客户端意思不是用户的客户端，而是业务层服务和计算层服务之间的关系——业务层主动访问计算层服务的gRPC Endpoints，此时称业务层服务为客户端，计算层服务为服务端。 服务端负载均衡的方案较多，在K8s环境下的实现主要是Envoy、Istio等服务网格(Service Mesh)方案的边车(Sidecar)服务。关于这方面的介绍应该是后面的文章的重头戏，本篇文章主要介绍客户端负载均衡的方案，所以此处不过多赘述。 关于客户端负载均衡，目前我们采用的方案是，通过Headless Service + Cluster DNS进行服务发现，客户端同时维系多个与gRPC服务的连接，并定期查询和刷新连接池。这块go-grpc的开发者写的很好，通过简单的代码就可以实现这些逻辑:// Connect 初始化客户端连接 // useTLS: 本次连接是否使用TLS加密 // endpoints: 连接使用的端点，格式应为 passthrough:///<addr>:<port>, // 如果要结合K8s使用Headless Service，格式应为dns:///<service-domain>:<port> func Connect(endpoints string, useTLS bool) (*grpc.ClientConn, error) { options := []grpc.DialOption{} if !useTLS { options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials())) } options = append(options, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(512*1024*1024))) options = append(options, grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(512*1024*1024))) options = append(options, grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`)) options = append(options, grpc.WithTimeout(5*time.Minute)) channel, err := grpc.Dial(endpoints, options...) if err != nil { return err } return channel, nil }客户端负载均衡方案的缺点那么古尔丹，代价是什么呢？go-grpc实现的的dns-resolver每次进行刷新，都会断开所有gRPC连接并重新建立。在高并发高流量场景下，就算是很短很短的闪断也会造成部分请求异常。并且，通过DNS来做服务发现也存在诸多问题。例如业务层服务端无法主动获悉计算层服务端的端点变动，只能每隔几分钟被动地查询刷新一次，这确实给服务热备、动态扩容更新等操作带来了困难。 客户端负载均衡并不是完美的的方案，针对这些问题，目前没有较好的解决办法。这也许也是服务端负载均衡存在的意义，这个就留到后面再介绍和研究了。后记以上就是本文的正文部分了，感谢你耐心读到这里。这篇文章被设计为后续service-mesh方面的开胃菜，所以也请关注我后续的文章，还会分享一些有意思的东西。 Q.E.D.0xC4A12024-02-07 21:25 提笔2024-02-08 13:00 完稿希望本篇文章的观点能获得您的认同。本文编写时间较短，某些内容缺乏仔细考证和审稿，如您有别的看法，欢迎在下方评论区讨论，或通过“关于”页面联系我。 祝各位新年快乐，我们来年(?)再见！
从零开始的K8s生产实践 #2 环境初始化和基础集群部署
前言在前面的两篇我们介绍了本次实践的方案和Ceph的部署。在部署完Ceph之后，我们即将进入本系列的正题——Kubernetes部署。实践环境声明在开始之前，再放一下机器配置: 节点名(Host) 内网IP 公网访问 硬件配置 ap-zj-storage-0 192.168.100.12 NAT 6C/12G ap-zj-storage-1 192.168.100.11 NAT 6C/12G ap-zj-worker-0 192.168.100.7 NAT 20C/72G ap-zj-worker-1 192.168.100.6 NAT 20C/72G ap-zj-master-0 192.168.100.3 直连/NAT(默认) 4C/8G ap-zj-master-1 192.168.100.4 NAT 4C/8G ap-zj-master-2 192.168.100.5 NAT 4C/8G实践软件环境初始化Kubernetes需要socat和conntrack，在集群的所有机器上都安装:sudo apt install socat conntrackDocker安装考虑到国内复杂的网络环境，我们需要提前安装并配置Docker:sudo apt update sudo apt install docker.io安装完后，使用docker version确定docker相关版本无误，笔者的输出是:Client: Version: 20.10.12 API version: 1.41 Go version: go1.17.3 Git commit: 20.10.12-0ubuntu4 Built: Mon Mar 7 17:10:06 2022 OS/Arch: linux/amd64 Context: default Experimental: true Server: Engine: Version: 20.10.12 API version: 1.41 (minimum version 1.12) Go version: go1.17.3 Git commit: 20.10.12-0ubuntu4 Built: Mon Mar 7 15:57:50 2022 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.5.9-0ubuntu3.1 GitCommit: runc: Version: 1.1.0-0ubuntu1.1 GitCommit: docker-init: Version: 0.19.0 GitCommit: 然后，编辑/etc/docker/daemon.json并输入如下配置:{ "registry-mirrors": ["https://<DOCKER-REGISTRY-MIRROR>"] }笔者此处不对registry mirror进行公开，星河成员请联系我获取内部mirror地址。 配置好这些之后，我们就能进入集群部署了。 [tip type="yellow"]具体情况可能随着时间推移产生变化，本段内容您可结合Kubesphere文档进行理解，本文所写的内容存在时效性。[/tip]KubeKey安装您可以从GitHub Release下载安装:sudo mkdir /opt/kube-data sudo cd /opt/kube-data sudo wget https://ghproxy.com/github.com/kubesphere/kubekey/releases/download/v3.0.6/kubekey-v3.0.6-linux-amd64.tar.gz sudo export KKZONE=cn sudo tar -xzvf kubekey-v3.0.6-linux-amd64.tar.gz chmod +x kk也可以通过官方脚本安装:sudo mkdir /opt/kube-data sudo cd /opt/kube-data sudo export KKZONE=cn sudo curl -sfL https://get-kk.kubesphere.io | VERSION=v3.0.6 sh - sudo chmod +x kkKubeKey配置文件创建先让KubeKey导出一个样板配置文件:./kk create config --with-kubesphere v3.3.0 --with-kubernetes v1.21.5考虑到生产环境需要，此处使用比较老的稳定版本，您可根据需求自行调整。然后按照实际更改相关内容，贴一下笔者修改后的config:apiVersion: kubekey.kubesphere.io/v1alpha2 kind: Cluster metadata: name: sample spec: hosts: - {name: zj-cluster-master-001, address: 192.168.100.3, internalAddress: 192.168.100.3, user: root} - {name: zj-cluster-master-002, address: 192.168.100.4, internalAddress: 192.168.100.4, user: root} - {name: zj-cluster-master-003, address: 192.168.100.5, internalAddress: 192.168.100.5, user: root} - {name: zj-cluster-worker-0, address: 192.168.100.7, internalAddress: 192.168.100.7, user: root} - {name: zj-cluster-worker-1, address: 192.168.100.6, internalAddress: 192.168.100.6, user: root} roleGroups: etcd: - zj-cluster-master-001 - zj-cluster-master-002 - zj-cluster-master-003 master: - zj-cluster-master-001 - zj-cluster-master-002 - zj-cluster-master-003 worker: - zj-cluster-worker-0 - zj-cluster-worker-1 controlPlaneEndpoint: ##Internal loadbalancer for apiservers ## 注意，如果master节点小于三个，可以不用部署lb internalLoadbalancer: haproxy ##If the external loadbalancer was used, 'address' should be set to loadbalancer's ip. domain: <按照实际需求设置，最好为非公网解析的域名，此处就不贴出来了> address: "" port: 6443 kubernetes: version: v1.21.5 clusterName: <按喜好设置> proxyMode: ipvs masqueradeAll: false maxPods: 150 nodeCidrMaskSize: 24 network: plugin: calico kubePodsCIDR: 10.99.64.0/18 kubeServiceCIDR: 10.99.0.0/18 registry: privateRegistry: "" --- apiVersion: installer.kubesphere.io/v1alpha1 kind: ClusterConfiguration metadata: name: ks-installer namespace: kubesphere-system labels: version: v3.3.0 spec: persistence: storageClass: "" authentication: jwtSecret: "" zone: "" local_registry: "" namespace_override: "" # dev_tag: "" etcd: monitoring: true endpointIps: localhost port: 2379 tlsEnable: true common: core: console: enableMultiLogin: true port: 30880 type: NodePort # apiserver: # resources: {} # controllerManager: # resources: {} redis: enabled: false volumeSize: 2Gi openldap: enabled: false volumeSize: 2Gi minio: volumeSize: 20Gi monitoring: # type: external endpoint: http://prometheus-operated.kubesphere-monitoring-system.svc:9090 GPUMonitoring: enabled: false gpu: kinds: - resourceName: "nvidia.com/gpu" resourceType: "GPU" default: true es: # master: # volumeSize: 4Gi # replicas: 1 # resources: {} # data: # volumeSize: 20Gi # replicas: 1 # resources: {} logMaxAge: 7 elkPrefix: logstash basicAuth: enabled: false username: "" password: "" externalElasticsearchHost: "" externalElasticsearchPort: "" alerting: enabled: false # thanosruler: # replicas: 1 # resources: {} auditing: enabled: false # operator: # resources: {} # webhook: # resources: {} devops: enabled: false # resources: {} jenkinsMemoryLim: 8Gi jenkinsMemoryReq: 5000Mi jenkinsVolumeSize: 40Gi jenkinsJavaOpts_Xms: 1200m jenkinsJavaOpts_Xmx: 1600m jenkinsJavaOpts_MaxRAM: 2g events: enabled: false # operator: # resources: {} # exporter: # resources: {} # ruler: # enabled: true # replicas: 2 # resources: {} logging: enabled: false logsidecar: enabled: true replicas: 2 # resources: {} metrics_server: enabled: false monitoring: storageClass: "" node_exporter: port: 9100 # resources: {} # kube_rbac_proxy: # resources: {} # kube_state_metrics: # resources: {} # prometheus: # replicas: 1 # volumeSize: 20Gi # resources: {} # operator: # resources: {} # alertmanager: # replicas: 1 # resources: {} # notification_manager: # resources: {} # operator: # resources: {} # proxy: # resources: {} gpu: nvidia_dcgm_exporter: enabled: false # resources: {} multicluster: clusterRole: none network: networkpolicy: enabled: false ippool: type: none topology: type: none openpitrix: store: enabled: true servicemesh: enabled: false istio: components: ingressGateways: - name: istio-ingressgateway enabled: false cni: enabled: false edgeruntime: enabled: false kubeedge: enabled: false cloudCore: cloudHub: advertiseAddress: - "" service: cloudhubNodePort: "30000" cloudhubQuicNodePort: "30001" cloudhubHttpsNodePort: "30002" cloudstreamNodePort: "30003" tunnelNodePort: "30004" # resources: {} # hostNetWork: false iptables-manager: enabled: true mode: "external" # resources: {} # edgeService: # resources: {} terminal: timeout: 600 telemetry_enabled: false然后在你启动KubeKey的机器上创建SSH公钥:ssh-keygen -o创建之后将它同步到集群的所有机器的authorized_keys文件内，方便KubeKey连接操作。部署Kubernetes和Kubesphere然后就可以启动部署流程了:./kk create cluster -f config.yaml如果前面的操作没问题的话，此处KubeKey将连接上集群内的所有机器并进行安装操作，确认安装后将会自动进行部署。
前言各位读者早上好！在前面的文章里，我们介绍了Ceph作为K8s的一种可靠的持久化存储系统的优势和部署: 《用 Ceph 实现 K8s 集群数据持久化 #1》 《用 Ceph 实现 K8s 集群数据持久化 #2》 在我着手编写Ceph这个系列的第三章的时候我就萌生了一个想法——如果要介绍到Ceph在K8s里的具体应用，可能在Ceph这个系列不是这么好接，所以当时就决定将Ceph的第三章——K8s部署Ceph-CSI组件放到K8s这边来介绍。不过因为学业原因这个系列也是咕了大半年了，最近刚好有一个新的集群要部署，所以就来填一下之前没填上的坑ww。 [tip type="red"]在开始本系列内容前，请读者注意: 本博客记载的所有内容均为笔者个人实践的总结，存在时效性和一定谬误。仅供您进行参考，请客观看待内容，切忌照搬照抄。[/tip]与之前的文章的风格相同，本系列仍将致力于将实验室和纸面上的理论搬进服务器机房和实际生产，所以我们不会堆砌太多纸面数据证明方案，如果您对方案有自己的看法也可以尝试付诸实践。正文实操环境声明由于不同操作系统和机器间存在配置差异，本文默认系统环境为Ubuntu 22.04 Server，机器详情如下: 节点名(Host) 内网IP 公网访问 硬件配置 ap-zj-storage-0 192.168.100.12 NAT 6C/12G ap-zj-storage-1 192.168.100.11 NAT 6C/12G ap-zj-worker-0 192.168.100.7 NAT 20C/72G ap-zj-worker-1 192.168.100.6 NAT 20C/72G ap-zj-master-0 192.168.100.3 直连/NAT(默认) 4C/8G ap-zj-master-1 192.168.100.4 NAT 4C/8G ap-zj-master-2 192.168.100.5 NAT 4C/8G方案策略设计网络策略(集群内)Kubernetes集群内的IP一共有两类: Pod IP与Service IP，所以在开始部署前，我们需要考虑这两种IP的选址，笔者使用了下列网段，各位可以根据实际情况调整：Pod IP CIDR: 10.98.64.0/18 Service IP CIDR: 10.98.0.0/18存储策略Ceph是一种高性能的集群化存储方案，其在K8s持久化存储方面的应用的主要形式有CephRBD和CephFS。笔者所采用的存储方案为CephFS(CSI接入)，此处选择CephFS主要是基于其兼容的挂载方式多样，能满足星河对集群多节点同时读写一个PVC的需求。节点策略由于笔者此处部署的是生产集群，所以采用了 主节点:计算节点:存储节点=3:2:2 的部署方案，其中存储节点上不部署Kubernetes，仅部署Ceph。
前言对于K8s来说，数据持久化一直是一大难题，因为伴随着pods的迁移和重建，数据不可能一直在一个节点上呆着，必须保证在重建的时候数据不丢失(数据持久化)。伴随着这个需求，各种存储卷插件也在社区的努力下慢慢迭代，最终成长为我们今日所见的，发达的CSI存储标准。正文公主连结——早期K8s的卷插件不同于今日高度标准化的CSI(Container Storage Interface, 容器存储接口)，在早期K8s版本(v1.0.0左右)的时候，社区就开发出了各种适用于K8s的卷插件，这些插件可以将外部存储连接到K8s，因为它们本身与K8s高度绑定，所以这些早期的卷插件被称为“in-tree plugins”。公主焊接——将插件编译进K8s核心然而，许多人认为这些插件存在局限性(例如大规模部署不便)，因此他们另辟蹊径，将这些插件以补丁的形式焊死在K8s核心中。借此，他们编译了自己的K8s版本，并将其安装在自己的服务器中。FlexVolume——焊接了但没有完全焊死随着时间的流逝，K8s社区逐渐意识到，直接编译存在严重的问题————“焊死”插件会导致人们都需要独立编译维护一个K8s核心以适应需求。换句话说，人们需要一个“钓鱼竿”来坐在钓箱上舒舒服服地钓鱼，而不是在水中拿着树枝，用千奇百怪的方法叉鱼。基于此，K8s社区开发了FlexVolume插件和其配套的驱动程序，它是一个用于调用第三方FlexVolume驱动程序的方法与变量的逻辑封装。同编译的区别从本质上来说，FlexVolume和前文提到直接将插件编译进K8s核心的方式有相似性——它们都在内核中包含了存储卷逻辑。但FlexVolume的通用性更强，它并没有“焊死”一种存储插件，而是将一些与某一外部存储强关联的逻辑(例如说私有协议、特定API的调用和实现)摘除，做成了FlexVolume插件和其配套的驱动程序。FlexVolume驱动程序那么，什么是FlexVolume驱动程序呢? FlexVolume驱动程序可以是一个二进制文件、Python脚本甚至Bash脚本，总而言之它是一个可执行文件。FlexVolume插件可以通过命令行参数调用它，同时它能以JSON格式返回包含预定义字段的报文。命令行参数可以同时提供多个，但根据约定，第一个命令行参数将作为方法，其余参数则作为调用这个方法的参数。部署难题倘若要使用FlexVolume插件，必须先部署FlexVolume驱动程序到集群，但部署它就意味着这个集群中的所有节点的某个预定义路径中，都得含有这个FlexVolume驱动程序。预定义路径一般是固定的，但在不同的K8s发行版中，预定义路径可能有所不同。正如上面所说的，将FlexVolume驱动程序部署到集群中的所有节点，是一项十分艰巨的任务——某些大企业的集群可能有上百个需要部署的节点。但俗话说只要思想不滑坡，办法总比困难多，只要愿意做其实这也是能部署的。但不幸的是，即使你有坚定的意志或者充足的人手来完成这些事，当添加新节点、自动水平缩放亦或是节点故障需要替换维护的时候，集群中大概率会出现新节点。而这些新节点往往不带有FlexVolume驱动程序，在这种情况下除非你手动复制它，否则你压根不可能在这些节点上使用PVC，数据持久化更无从谈起。及时雨——DaemonSet你是个好人上述问题其实可以通过K8s的DaemonSet解决，当集群创建新节点的时候，它将自动获取DaemonSet中的新容器，这个容器可以挂载节点的目录作为本地卷，然后将FlexVolume驱动程序所需的文件拷贝到本地卷，也就是节点的目录内。但我们不合适尽管DaemonSet解决了新节点需要手动添加FlexVolume驱动程序的问题，但是很多驱动实际上依赖于宿主机(节点)自身的一些软件包，例如CephFS/RBD的驱动本身需要宿主机带有ceph-common软件包才能进行调用。而事实上无论用什么样的方法，K8s都无法通过现有资源在新节点上自动执行shell(以安全的方法)，这也就意味着这些软件包还是得手动配置。in-tree插件的致命缺点上述的几种插件，经常被统称为“in-tree plugin”，即in-tree插件。前文提到过它们与k8s本身保持高度绑定关系，这意味着倘若要升级这些插件，就必须直接连着k8s集群版本一起升级。倘若有一天，我们发现现有的插件需要进行必要的升级，而这个需要升级的插件刚好是in-tree插件，则这个升级必须重启整个K8s集群。倘若这是在生产环境，就只能苦哈哈地找老板和客户说明情况，然后顶着因为造成生产环境事故而被炒鱿鱼的风险升级集群了！CSI——救世主在目睹上述问题后，K8s社区急需一种能够优雅地解决数据持久化问题的方法，于是CSI应运而生，K8s社区在K8s 1.9中发布了它的Alpha版本。CSI是什么？不同于FlexVolume，CSI不是一种插件，它是一种用于创建自定义组件以与数据存储一同使用的成熟标准。CSI如何工作？CSI使用由第三方开发的CSI驱动程序(例如Ceph的ceph-csi)。这个驱动程序通常由下列两部分组成:Controller——控制器Controller是管理PV(持久卷)的控制器，通常(绝大多数情况下)通过StatefulSet形式部署。Node——节点Node能够将PV(持久卷)安装到集群其他节点，与FlexVolume驱动程序的部署有异曲同工之妙——它也是通过DaemonSet在其他节点上部署的。同时它通过gRPC协议与Controller通信。CSI的优点是什么？不再需要手动编写返回由于开发人员实现了驱动程序容器化，不像FlexVolume，它已经不再需要人们手动创建JSON格式返回。通过集群管理的驱动程序CSI驱动程序统一部署在K8s集群而非单个节点上，这意味着一切都发生在由K8s集群部署和控制的容器内部，这将更易于管理。套接字请求CSI插件避免了FlexVolume插件与驱动程序间荒谬至极的命令行参数+JSON返回通信，而是采用更加正规的IPC套接字通信。总结正如前文所言，CSI的优势完美地弥补了此前几种方式的不足，作为一种用于创建自定义组件以与数据存储一同使用的成熟标准，它一经推出就受到广大开发者的热烈欢迎。由于它本身的优势，即使诸如Ceph、AWS EBS、S3这种已经有了自己的存储插件的外部存储，也已经实现了CSI驱动程序并投入生产环境使用。在2019年初的时候，in-tree插件已经被弃用(移除)了，虽然社区仍然会维护这些插件，但更多的新功能只会被添加到CSI而不是这些老旧的插件，我们后续也会继续探索CSI在K8s容器集群中的应用。
错误platform.linux_distribution问题表现RuntimeError: AttributeError: module 'platform' has no attribute 'linux_distribution'原因在Python 3.8及更高版本中，platform.linux_distribution已经被删除了，所以ceph会报错。解决解决方法简单粗暴，直接修改程序逻辑就好。编辑 /usr/lib/python3/dist-packages/ceph_deploy/hosts/remotes.py然后找到platform_information函数，将下列两行用try语句包裹:linux_distribution = _linux_distribution or platform.linux_distribution distro, release, codename = linux_distribution()然后设置三个变量的默认值，修改后是这样的:def platform_information(_linux_distribution=None): """ detect platform information from remote host """ """ linux_distribution = _linux_distribution or platform.linux_distribution distro, release, codename = linux_distribution() """ distro = release = codename = None try: linux_distribution = _linux_distribution or platform.linux_distribution distro, release, codename = linux_distribution() except AttributeError: pass保存退出，然后再次尝试运行。ceph-deploy文本显示问题表现[a1][INFO ] Running command: sudo fdisk -l [ceph_deploy][ERROR ] Traceback (most recent call last): [ceph_deploy][ERROR ] File "/usr/lib/python3/dist-packages/ceph_deploy/util/decorators.py", line 69, in newfunc [ceph_deploy][ERROR ] return f(*a, **kw) [ceph_deploy][ERROR ] File "/usr/lib/python3/dist-packages/ceph_deploy/cli.py", line 166, in _main [ceph_deploy][ERROR ] return args.func(args) [ceph_deploy][ERROR ] File "/usr/lib/python3/dist-packages/ceph_deploy/osd.py", line 434, in disk [ceph_deploy][ERROR ] disk_list(args, cfg) [ceph_deploy][ERROR ] File "/usr/lib/python3/dist-packages/ceph_deploy/osd.py", line 375, in disk_list [ceph_deploy][ERROR ] if line.startswith('Disk /'): [ceph_deploy][ERROR ] TypeError: startswith first arg must be bytes or a tuple of bytes, not str [ceph_deploy][ERROR ] 原因类型不通问题，需要以bytes形式匹配。解决打开 /usr/lib/python3/dist-packages/ceph_deploy/osd.py查找下列代码:if line.startswith('Disk /'):将其替换为:if line.startswith(b'Disk /'):保存退出，重新运行即可。残留Block问题如果用ceph-deploy强制删除了原来的集群，那原来的OSD可能会保留在磁盘内。所以当再次部署的时候，使用ceph-deploy擦除磁盘的时候可能导致异常情况:[x1][INFO ] Running command: /usr/sbin/ceph-volume lvm zap /dev/sdb [x1][WARNIN] --> Zapping: /dev/sdb [x1][WARNIN] --> --destroy was not specified, but zapping a whole device will remove the partition table [x1][WARNIN] stderr: wipefs: error: /dev/sdb: probing initialization failed: Device or resource busy [x1][WARNIN] --> failed to wipefs device, will try again to workaround probable race condition [x1][WARNIN] stderr: wipefs: error: /dev/sdb: probing initialization failed: Device or resource busy [x1][WARNIN] --> failed to wipefs device, will try again to workaround probable race condition [x1][WARNIN] stderr: wipefs: error: /dev/sdb: probing initialization failed: Device or resource busy [x1][WARNIN] --> failed to wipefs device, will try again to workaround probable race condition [x1][WARNIN] stderr: wipefs: error: /dev/sdb: probing initialization failed: Device or resource busy [x1][WARNIN] --> failed to wipefs device, will try again to workaround probable race condition [x1][WARNIN] stderr: wipefs: error: /dev/sdb: probing initialization failed: Device or resource busy [x1][WARNIN] --> failed to wipefs device, will try again to workaround probable race condition [x1][WARNIN] stderr: wipefs: error: /dev/sdb: probing initialization failed: Device or resource busy [x1][WARNIN] --> failed to wipefs device, will try again to workaround probable race condition [x1][WARNIN] stderr: wipefs: error: /dev/sdb: probing initialization failed: Device or resource busy [x1][WARNIN] --> failed to wipefs device, will try again to workaround probable race condition [x1][WARNIN] stderr: wipefs: error: /dev/sdb: probing initialization failed: Device or resource busy [x1][WARNIN] --> failed to wipefs device, will try again to workaround probable race condition [x1][WARNIN] --> RuntimeError: could not complete wipefs on device: /dev/sdb [x1][ERROR ] RuntimeError: command returned non-zero exit status: 1 [ceph_deploy][ERROR ] RuntimeError: Failed to execute command: /usr/sbin/ceph-volume lvm zap /dev/sdb要想解决这个问题，就得手动删除磁盘中残余的Block，先用lsblk查看磁盘情况lsblk我这边显示的情况是:NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT loop1 7:1 0 55.5M 1 loop /snap/core18/2409 loop2 7:2 0 71.3M 1 loop /snap/lxd/16099 loop3 7:3 0 61.9M 1 loop /snap/core20/1518 loop4 7:4 0 67.8M 1 loop /snap/lxd/22753 loop5 7:5 0 47M 1 loop /snap/snapd/16292 loop6 7:6 0 55.6M 1 loop /snap/core18/2538 sda 8:0 0 50G 0 disk ├─sda1 8:1 0 1M 0 part └─sda2 8:2 0 50G 0 part / sdb 8:16 0 1T 0 disk └─ceph--66fb0189--7b8a--423e--a26c--f4a85545f396-osd--block--df953059--5020--4c8c--8b82--4dd8a22a0b1c 253:0 0 1024G 0 lvm rbd0 252:0 0 20G 0 disk rbd1 252:16 0 20G 0 disk rbd2 252:32 0 2G 0 disk rbd3 252:48 0 20G 0 disk rbd4 252:64 0 4G 0 disk rbd5 252:80 0 8G 0 disk rbd6 252:96 0 8G 0 disk rbd7 252:112 0 8G 0 disk 可以看到在要抹除的磁盘/dev/sdb下，存在一个前ceph集群残留的存储块: ceph--66fb0189--7b8a--423e--a26c--f4a85545f396-osd--block--df953059--5020--4c8c--8b82--4dd8a22a0b1c把它擦除掉就好了sudo dmsetup remove --force ceph--66fb0189--7b8a--423e--a26c--f4a85545f396-osd--block--df953059--5020--4c8c--8b82--4dd8a22a0b1c删除这个block之后，磁盘里可能还存在一些LVM或者分区，用wipefs强行写入删除:wipefs -af /dev/sdb
本篇接上回用Ceph实现K8s集群数据持久化 #1。Ceph集群部署环境信息本次部署以星河武汉金银潭集群为例，环境信息如下:内网网段: 172.23.1.0/24 网关: 172.23.1.1 gateway 节点A1: 172.23.1.2 a1 Ubuntu-20.04-LTS SSD*1(/dev/nvme0n1) 节点A2: 172.23.1.3 a2 Ubuntu-20.04-LTS SSD*1(/dev/nvme0n1) 节点A3: 172.23.1.4 a3 Ubuntu-20.04-LTS SSD*1(/dev/nvme0n1) Master节点: A1注意: 本教程仅供参考，在进行任何操作前请结合实际情况思考，切莫照搬操作，否则可能造成生产环境灾难!配置集群节点免密登录在A1(Master节点)生成ssh key并copy到各个机器上:ssh-keygen -o ssh-copy-id -i ~/.ssh/id_rsa.pub root@a2 ssh-copy-id -i ~/.ssh/id_rsa.pub root@a3 注意: 下述操作能且只能在Master节点进行 初始化Master节点安装ceph-deployceph-deploy是一个ceph的简易部署工具，能简化很多操作流程，您可以通过该命令安装它:sudo apt install ceph-deploy -y创建集群在安装完ceph-deploy后，您需要创建一个文件夹用于存放ceph-deploy的临时文件。我这里选的是/opt/ceph-deploy，对应操作如下:mkdir /opt/ceph-deploy chmod +775 /opt/ceph-deploy然后运行ceph-deploy初始化集群ceph-deploy new a1 a2 a3此处部署的时候您可能会遇到一个Python错误，详见platform.linux_distribution问题。警告此处输入的“a1 a2 a3”均为节点的hostname，在执行相关操作前，请确认您能通过集群内部DNS解析出这些hostname或者确认集群里每个节点的/etc/hosts设置中均有相关hostname解析配置，否则可能会导致严重问题。启动集群安装Ceph软件包(可选)如果您此前已经通过apt安装ceph或者您希望采用自己的安装方式，则在已经顺利完成所有模块的安装的前提下，该步骤是可选的。如果您希望使用ceph-deploy安装它，您可以在Master节点使用如下命令:ceph-deploy install a1 a2 a3 // 请将该hostname替换成您自己的集群节点的hostname启动Ceph Monitor完成Ceph安装后，您需要先启动Ceph Monitor，它负责管理集群的状态和配置信息，因此需要先启动，您可以通过在Master节点执行以下命令启动它。ceph-deploy mon create-initial // 初始化Monitor ceph-deploy admin a1 // 此处因为A1是Master节点，所以让A1来当Monitor，选其他的也可以启动Ceph ManagerCeph Manager也是一个必须部署的组件，以下命令同样在Master节点执行:ceph-deploy mgr create a1 // 同Monitor进行完上述操作后，集群的核心服务就大部分启动完成了，但还需要构建OSD才能对外提供存储服务。配置OSD卷初始化磁盘首先查看各个节点的磁盘:ceph-deploy disk list a1 a2 a3 此处可能会出现一个错误，解决方法详见ceph-deploy文本显示问题。在列表里找到您希望让Ceph独占的磁盘，记下它的路径。例如此处星河部署的是/dev/nvme0n1。然后用ceph-deploy将它擦除:ceph-deploy disk zap a1 /dev/nvme0n1 ceph-deploy disk zap a2 /dev/nvme0n1 ceph-deploy disk zap a3 /dev/nvme0n1如果擦除磁盘受阻，且之前有部署过一个Ceph集群，解决方法详见残留Block问题。警告在Ceph Luminous版本及后续推出的版本中，设置目录作为Ceph OSD已经被放弃，请确认您使用的是一整块磁盘，而非一个小目录。创建OSD卷在擦除磁盘后，方可创建OSD卷:ceph-deploy osd create a1 --data /dev/nvme0n1 ceph-deploy osd create a2 --data /dev/nvme0n1 ceph-deploy osd create a3 --data /dev/nvme0n1至此，Ceph的核心部件全部部署完成，关于K8s方面的应用详见后文。 停！你不该再继续深入阅读了！接下来的内容非常危险！请不要随意执行！重开如果你发觉你搞砸了一切，除了重装系统，你可以通过ceph-deploy来reset环境。警告这个reset是真正意义上的reset，会将你的系统里所有有关于ceph的内容全部抹掉，包括OSD、Monitor、Manager、Keys。删除OSDceph osd tree ceph osd out osd.x ceph osd crush remove osd.x ceph osd rm osd.x抹除其他内容ceph-deploy purge <NodeName>2022.11.21补充: 请一并删除data:ceph-deploy purgedata <NodeName>
前言对于K8s来说，高可用是其一大亮点，对于生产环境来说，无论是服务高可用还是数据高可用都是不可忽视的部分。在参考NFS、LocalEBS、GlusterFS、Ceph等存储方案后，我最终选择了使用Ceph作为集群数据持久化方案。其他实现方式的缺陷NFS在K8s最近(1.20.0+)的StackoverFlow、Github issues里能经常看到关于NFS的报错，根据K8s社区测试的结果，普遍得出NFS在K8s 1.20.0+版本会出现CrashLoopBackOff的错误，其原因是NFS可能会引起 failed to obtain lock 和 input/output error 等问题，从而导致容器崩溃。这对于生产环境服务是不可接受的。GlusterFSGlusterFS框架本身并不支持对外提供RESTful API，所以其RESTful API是由另一个社区项目——Heketi进行适配的。K8s经由这个项目才能以CSI的方式调用GlusterFS。然而Heketi的项目近况并不好，在其2021年的一次更新中，README.md里提到了:The Heketi project is now in deep maintenance status. This means that only critical bugs or security defects are being considered for inclusion by the project maintenance team. Expect the project to be archived soon. Expect slow replies to issues.意思就是项目组决定，现在不会给heketi开发新功能了，只会在现有基础上补缺补漏，并将在未来不久彻底停止维护。并且其还提到了:It has been well over a year since we first entered maintenance mode. To anyone looking at creating a new install using Heketi: we highly encourage you to look at other appraoches to dynamic storage provisioning, especially if you're not already very familiar with Heketi/Gluster.这意味着使用GlusterFS+Heketi作为CSI接口提供数据持久化已不再被建议，将来如果遇到问题，社区将无法获得官方支持，故弃用GlusterFS。LocalEBSLocalEBS是很多K8s初学者上路用的第一个的数据持久化方案，它可能适合测试或者跑跑自己的服务，但由于其功能有限，性能瓶颈较大，无法保证数据高可用且对存储卷声明支持较弱(仅支持ReadWriteOnce这一种方式)，所以弃用。为什么使用Ceph?高性能高性能ですから！我可是高性能的嘛Ceph采用CRUSH算法，并行度和数据分布均衡度降维打击了一众传统的集中式存储元数据寻址方案。而且其理论上能支持上千个存储节点同时存在于一个集群之内，TB到PB级的数据存储。高扩展性Ceph是去中心化的，其实际性能随着节点增加而线性增长，并且扩展灵活，不会对现有集群造成过多破坏。高可用Ceph没有单点故障导致的整体服务崩溃，在一些常见的故障场景下，它能实现自愈和故障域隔离，同时数据保持强一致性。高兼容性Ceph支持三种存储接口: 块存储、文件存储、对象存储，并且其支持自定义接口，支持多种语言实现的驱动。综上所述，我认为Ceph是目前最能满足星河存储需求的集群方案，但Ceph也有两种方式实现K8s集群的数据持久化，详见《用 Ceph 实现 K8s 集群数据持久化 #2》
如果你变更IP后使用K8S重建Cluster，那么遇到一切问题请先尝试删除“kk”可执行文件根目录下的kubekey文件夹！