架构艺术之应用分层设计

图片来自pixabay.com的ROverhate会员

1. 为什么需要应用分层架构设计

高内聚、低耦合、职责单一,是一个应用的基本设计要求。但是起初设计很好的应用边界,随着业务的扩张,待开发的业务功能越来越多,不断拆分出新应用
,经常出现的问题是,应用之间的相互依赖关系越来越模糊,相互调用,进而出现循环依赖和长链条依赖问题。

举一个简单的业务功能为例,用户下单支付,其需要调用支付中心进行支付、调用会员中心确认可用积分、调用营销中心发放优惠券、调用消息中心发送用户短信等步骤,这里涉及的应用有4个,

  • A 支付中心
  • B 会员中心
  • C 营销中心
  • D 消息中心

一个常见的技术实现是,在应用A中提供一个下单支付的接口,在A中先后调用执行应用B、C、D的接口,串联实现下单支付的功能。这不是理想的技术方案,最大的问题是,支付中心A承担了整个下单支付的流程,不仅需要负责处理各个接口的返回消息,还要处理在接口调用失败下的重试和异常告警,其功能职责不再单一。随着下单支付的业务功能越来越复杂,需要串联的业务流程步骤越多,今天加个促销积分,明天发不同优惠券,支付中心变得臃肿不堪,负责支付的开发工程师苦不堪言。

在调用关系上,到底是A调用B或C,还是从B调用到C,在没有沟通清楚的情况下,各种调用方法实现都有,很容易导致A->B->C->D的长链条调用,或A->B->A的循环调用,应用之间的调用变得复杂。管控的不好,业务架构的可扩展性无从谈起,开发团队之间经常扯皮,一个功能代码到底如何串联?在哪里实现?

这里其实涉及到的关键问题是,应用的逻辑架构和相互依赖关系,这正是应用分层架构设计所要解决的问题。

2. 一个通用的架构分层设计

下面将介绍一个通用的架构分层方案,

应用架构分层设计

如上图所示,这个架构分层设计的要点如下,

  1. 应用根据分层架构划分为四层,从上到下分别为,
    • API网关层:对外提供HTTP接口服务,实现统一的鉴权、流控和降级。
    • 业务聚合层:依赖业务中心服务,调用中心服务所提供的原子业务接口,串联起业务流程,实现基于业务场景的功能接口。其负责流程的异常处理和重试,跟进流程状态。
    • 业务中心服务层:实现单一、独立的原子业务功能,高内聚、低耦合。原子业务的含义是指业务不可拆分,有确定的输入和输出,在输入正常的情况下,必须确保业务的正常完成。
    • 业务数据服务层:提供业务数据的只读查询,不提供写操作。
  2. 应用调用关系必须从上往下调用,不允许同层调用,以免形成互相依赖和循环依赖。
  3. 应用必须按规范格式xxx-gateway/business/center/data命名,以便快速识别其工作的逻辑层次。
  4. 业务中心服务和数据服务应用将共享同一个DAO类库,其数据服务提供的是只读接口。
  5. 业务中心服务层不能相互调用,若有需要,允许通过消息中心进行异步通信调用。
  6. 业务数据服务层不是一定需要,若没有数据查询的需求,可以省略这一层的数据服务应用部署。

这个架构分层顺利执行的关键点主要在于两方面,

1. 应用调用关系严格按照从上往下调用,禁止同层调用,特别是业务中心服务层,各个中心服务不允许相互调用。
2. 应用命名必须严格遵从格式规范,规范的命名将方便团队之间沟通,特别是在架构评审时,大家可以快速识别其所在的逻辑层次,从而判断其调用关系是否合理。

3. 项目代码结构

根据上面的架构分层设计,一个相应的项目代码结构如下,

pphh-demo
  + demo-async        业务异步调用:包括消息或定时任务
  + demo-gateway      业务网关
  + demo-business     业务聚合层应用
  + demo-business-api 业务聚合层接口定义
  + demo-center       业务中心服务应用
  + demo-center-api   业务中心服务接口定义
  + demo-data         业务数据服务应用
  + demo-data-api     业务数据服务接口定义
  + demo-dao          数据库访问类库

4. 带来的好处

作为一个顶层设计,分层架构定义了整体的逻辑应用架构和上下依赖关系,确认各个层次的应用大边界。以此为基础,进行新应用的设计和拆分、定义功能边界,将会更加容易,整体架构的可扩展性也可以得到保证。

从调用链的角度,应用的调用层次深度保持为一个常量,按照上图的分层架构设计,深度最多为5,并且不会出现循环调用和长链条调用的问题。

在团队规模很小时,分层架构设计带来的好处可能会比较小,但是一旦团队规模成长到50人以上、应用个数上升到30个以上时,分层架构设计将发挥越来越大的作用,一旦整个团队对分层架构都达成统一思想,大家对自己开发的功能边界和应用交互都有清晰的认知,随着应用数增多,团队的沟通成本几乎为零。换句话说,任何一个开发人员通过应用名都可以清晰地知道应用之间合理的调用关系。

以上述的用户下单支付为例,在各个中心服务提供相应的原子业务实现,

  • A 支付中心:完成支付功能
  • B 会员中心:确认可用积分
  • C 营销中心:发放优惠券
  • D 消息中心:发送用户短信

然后整个流程的串联放在聚合层(即business层)实现,不再放在支付中心或其它任何中心服务,在聚合层中确保流程的异常处理和重试。任何流程的串联变化,都只需修改聚合层的代码。流程的状态处理完全从中心服务的业务实现中隔离出来,让中心服务专注于业务的原子粒度实现。

当前分层架构也带来一些开销,在团队规模小的时候,需要拆分的应用个数也是可观的,带来部署的机器成本也是存在的。但总体而言,利大于弊。

5. 结束语

分层架构设计提供了一个自顶向下的设计思路,一旦理顺了架构分层,在团队内掌握好架构分层的要点,则应用的边界定义、架构的可扩展性、团队的沟通协作,都是水到渠成的事情,这也正是架构的艺术魅力所在。

k8s集群flannel部署错误异常排查:pod cidr not assigned

图片来自pixabay.com的DerWeg会员

1. 现象

在k8s集群的部署过程中,在初始化k8s master节点之后,准备通过如下kubeadm join命令添加当前worker节点到k8s集群,

kubeadm join xxx:6443 --token xxx.xxx \
>     --discovery-token-ca-cert-hash sha256:xxxx

[preflight] Running pre-flight checks
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...

This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

根据日志输出,可知当前节点已加入集群,通过kubectl get nodes命令可以正常看到节点的状态为ready,但是通过kubectl get pods -A命令查看pods状态时,看到如下的CrashLoopBackOff的异常状态,

$ kubectl get pods -A
NAMESPACE     NAME                                      READY   STATUS              RESTARTS   AGE
kube-system   kube-flannel-ds-j69g6                     0/1     CrashLoopBackOff    3          18m

通过kubectl logs命令查询pod日志可以看到报pod cidr not assigned的异常信息,

$ kubectl logs kube-flannel-ds-j69g6 -n kube-system
I0218 06:23:21.796296       1 main.go:518] Determining IP address of default interface
I0218 06:23:21.796512       1 main.go:531] Using interface with name eth0 and address 10.250.41.77
I0218 06:23:21.796525       1 main.go:548] Defaulting external address to interface address (10.250.41.77)
W0218 06:23:21.796537       1 client_config.go:517] Neither --kubeconfig nor --master was specified.  Using the inClusterConfig.  This might not work.
I0218 06:23:21.906396       1 kube.go:119] Waiting 10m0s for node controller to sync
I0218 06:23:21.906791       1 kube.go:306] Starting kube subnet manager
I0218 06:23:22.906882       1 kube.go:126] Node controller sync successful
I0218 06:23:22.906912       1 main.go:246] Created subnet manager: Kubernetes Subnet Manager - worker-0001
I0218 06:23:22.906918       1 main.go:249] Installing signal handlers
I0218 06:23:22.906963       1 main.go:390] Found network config - Backend type: vxlan
I0218 06:23:22.907016       1 vxlan.go:121] VXLAN config: VNI=1 Port=0 GBP=false Learning=false DirectRouting=false
E0218 06:23:22.907246       1 main.go:291] Error registering network: failed to acquire lease: node "worker-0001" pod cidr not assigned
I0218 06:23:22.907272       1 main.go:370] Stopping shutdownHandler...

当前发生问题的k8s版本为1.20.0,其中配置了flannel作为网络CNI插件。

2. 快速解决方案:手动分配podCIDR

这个问题主要是由于worker节点的flannel组件无法正常获取podCIDR的定义,一个快速的解决方法:可以通过执行如下命令对相应的worker节点添加podCIDR配置,

# 注意:每个worker节点的SUBNET需要区分开,否则k8s pods之间网络访问会不通。
kubectl patch node <NODE_NAME> -p '{"spec":{"podCIDR":"<SUBNET>"}}'

然后可以再次查看节点信息,

# 如下配置是cluster-cidr=172.18.0.0/16所指定网段范围内的一个子网段
$ kubectl patch node worker-0001 -p '{"spec":{"podCIDR":"172.18.1.0/24"}}'

$ kubectl describe node worker-0001
......
PodCIDR:                      172.18.1.0/24
......

过一段时间,再次查看kube-flannel-ds-j69g6 pod,可以看到已经可以正常启动,状态为RUNNING。

$ kubectl get pods -A
NAMESPACE     NAME                                      READY   STATUS              RESTARTS   AGE
kube-system   kube-flannel-ds-j69g6                     0/1     RUNNING             3          18m

这个解决方案是通过手动指定worker节点的可分配IP地址域,但是这个不是最佳的解决方案,最好找到根本原因,让flannel自动配置各个worker节点的podCIDR。

3. 异常根因和自动分配podCIDR

在k8s master集群部署中,通过kubeadm init初始化master节点时,flannel网络插件需要确保初始化命令配置了podCIDR如下启动参数,

  • --pod-network-cidr=172.18.0.0/16

初始化完毕之后,可以在配置文件/etc/kubernetes/manifest/kube-controller-manager.yaml中看到如下信息,

  • --allocate-node-cidrs=true
  • --cluster-cidr=172.18.0.0/16

同时在安装flannel cni网络插件时,通过kubectl apply kube-flannel.yml命令,kube-flannel配置文件中的Network参数需要和pod-network-cid保持一致,

  net-conf.json: |
    {
      "Network": "172.18.0.0/16",
      "Backend": {
        "Type": "vxlan"
      }
    }

在k8s集群上述初始化的过程中,若不小心出现如下情况,

  1. 对master主机初始化时,有1台或多台未正常配置podCIDR参数,即在kubeadm init命令中未添加pod-network-cid参数。
  2. 通过kubectl apply kube-flannel.yml命令添加flannel网络插件时,其指定的Network和pod-network-cid不一致。

都会导致flannel插件无法自动识别和分配pod cidr,从而出现pod cidr not assigned异常问题。

若出现此类问题,可以详细检查如上k8s集群配置。在配置都正确的情况下,各个k8s worker节点上的flannel将能够正常自动配置各个worker节点的podCIDR,如下为三个worker节点上查询到自动分配的网段,

# master-0001
# 172.18.0.1/24
$ cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.18.0.0/16
FLANNEL_SUBNET=172.18.0.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

# worker-0002
# 172.18.1.1/24
$ cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.18.0.0/16
FLANNEL_SUBNET=172.18.1.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

# worker-0003
# 172.18.2.1/24
$ cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.18.0.0/16
FLANNEL_SUBNET=172.18.2.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

4. 参考资料

  1. flannel touble shooting
  2. stack overflow: Kubernetes worker nodes not automatically being assigned podCidr on kubeadm join

新手指南:k8s集群单机部署

图片来自pixabay.com的designerpoin会员

k8s已经成为业界容器编排技术的平台标准,本文介绍了在单机上如何部署一个k8s集群,同时承担master和worker节点角色,采用flannel网络插件搭建其底层网络模型,部署完毕之后运行一个简单nginx服务。通过部署单机k8s集群,我们可以快速进行相关k8s集群的测试、调试和学习。

本文所安装k8s版本为1.20.0。

k8s集群的单机部署主要有如下步骤,

  1. 检查和配置环境,使之符合k8s所要求。
  2. 安装k8s所需的容器运行时,本文选用了Docker。
  3. 安装k8s集群管理工具kubeadm/kubelet/kubectl。
  4. 通过kubeadm初始化k8s集群。
  5. 通过kubectl部署网络插件flannel。

k8s集群的安装过程不是特别复杂,但是由于国内墙的原因,在获取一些相关容器镜像时,会遇到困难,这主要影响第4步和第5步的k8s集群初始化。本文安装过程中所使用到的镜像列表将在文末给出,供读者参考。博主本人一般先尝试通过阿里云等国内镜像源服务拉取相关镜像,若还是无法拉取,则借助代理服务访问外网镜像源。

在无镜像拉取问题的情况下,整个部署过程预计在1个小时内可以完成。安装过程中可能遇到的一些常见问题和解决方案,在文末将一一列出。

1. 检查当前环境,按要求初始化环境

1.1 安装要求

安装k8s集群的基本要求如下,

  • 至少2核CPU + 2G内存
  • 操作系统版本必须符合如下要求
    • Ubuntu 16.04+
    • Debian 9+
    • CentOS 7+
    • Red Hat Enterprise Linux (RHEL) 7+
    • Fedora 25+
    • HypriotOS v1.0.1+
    • Flatcar Container Linux (使用 2512.3.0 版本测试通过)
  • 集群中的所有机器的网络彼此均能相互连接
  • 节点之中不可以有重复的主机名、MAC 地址或 product_uuid。
  • 查看k8s所需端口,确保这些端口未被防火墙拦截,并检查所需端口在主机上没有被占用。
  • 禁用交换分区。

详细的安装要求见这里

本文安装所使用到的主机配置为,
* 2核CPU + 4G内存
* 操作系统为Ubuntu 20.10,Linux 5.8.0-41-generic
* 单机版网络,单以太网卡接入局域网,未开启防火墙

1.2 查看系统CPU和内存情况

# 查看系统CPU
$ cat /proc/cpuinfo
# 查看系统memory
$ cat /proc/meminfo

1.3 查看系统版本

$ uname -a
Linux k8s-master01 5.8.0-41-generic #46-Ubuntu SMP Mon Jan 18 16:48:44 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.10
DISTRIB_CODENAME=groovy
DISTRIB_DESCRIPTION="Ubuntu 20.10"

1.4 查看系统Mac和product_uuid

查看系统Mac和product_uuid,在正式环境中,必须保证每个主机节点唯一。

# 查看当前主机的网络适配器和Mac地址
$ ifconfig -a
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.0.2.15  netmask 255.255.255.0  broadcast 10.0.2.255
        inet6 fe80::7f65:6807:9e8e:8ac3  prefixlen 64  scopeid 0x20<link>
        ether 08:00:27:52:8d:82  txqueuelen 1000  (Ethernet)
        RX packets 84  bytes 35282 (35.2 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 117  bytes 17453 (17.4 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 150  bytes 12646 (12.6 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 150  bytes 12646 (12.6 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# 查看enp0s3的mac地址
$ cat /sys/class/net/enp0s3/address 
08:00:27:52:8d:82

# 查看当前主机product_uuid
$ sudo cat /sys/class/dmi/id/product_uuid
975ca0e1-d319-3745-9915-a61ef4297ddf

1.5 查看网卡适配器和路由

查看路由命令,当前主机只有一个网卡适配器,缺省路由0.0.0.0正常指向网关地址。

$ route -v
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    100    0        0 enp0s3
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
link-local      0.0.0.0         255.255.0.0     U     1000   0        0 enp0s3

若主机上有多个网卡适配器,则需要确认当前k8s组件能够通过缺省路由访问到正确的目标网卡适配器。

1.6 查看netfilter组件

netfilter是一个运行在Linux内核空间、实现网络流量包过滤/地址转换的框架。iptables是一个工作在应用空间、管理netfilter流量规则的应用程序,其通过netfilter回调hook,将网络流量包分类映射到相应的流量规则集合,实现流量管控。

由于k8s集群中容器的IP地址是动态分配的,k8s通过iptables/netfilter控制应用服务流量数据包,实现集群内应用服务流量的动态负载均衡。

如下命令可以加载netfilter组件并允许iptables过滤网桥流量。

# 加载netfilter
$ modprobe br_netfilter

# 确认netfilter的加载情况,若能看到如下的命令输出,则说明netfilter已被加载
$ lsmod | grep br_netfilter
br_netfilter           28672  0
bridge                200704  1 br_netfilter

# 允许iptables过滤网桥流量
$ cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
br_netfilter
EOF

$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF

$ sudo sysctl --system

1.7 关闭swap

k8s不支持Linux swap功能,这是由于出于集群性能和稳定性考虑,但未来这个问题会被得到解决,可能在1.22这个版本开启swap支持,更多关于这个问题的详细讨论请见:Kubelet/Kubernetes should work with Swap Enabled

关闭swap命令,

# 查看内存中的swap分配情况
$ free -m
              total        used        free      shared  buff/cache   available
Mem:           3932         854         457          15        2620        2783
Swap:          2047           0        2047


# 临时关闭swap
$ sudo swapoff -a 

# 查看内存中的swap分配为0
$ free -m 
              total        used        free      shared  buff/cache   available
Mem:           3932        1265        1074          12        1592        2499
Swap:             0           0           0

# 永久关闭swap(请使用root用户运行如下命令)
$ echo "vm.swappiness=0" >> /etc/sysctl.conf                                                                    
$ sysctl -p /etc/sysctl.conf 

1.8 检查所需端口和主机配置

k8s在master节点和worker节点有不同的端口需求

master节点(控制平面),

  • TCP 6443,Kubernetes API 服务器
  • TCP 2379-2380,etcd 服务器客户端 API
  • TCP 10250,Kubelet API
  • TCP 10251,kube-scheduler
  • TCP 10252,kube-controller-manager

worker节点(工作平面),

  • TCP 10250,Kubelet API
  • TCP 30000-32767,NodePort 服务

在搭建过程中需要确认上述端口没有被占用,并且没有被防火墙拦截。

# 为了方便演示快速部署,建议直接关闭防火墙(若有的话)。
$ systemctl stop firewalld && systemctl disable firewalld

1.9 其它

为了方便安装和演示,建议打开如下三个shell窗口,

  • 第一个shell窗口:切换到root账号下,方便运行需要root权限的系统命令,包括工具安装命令、执行kubeadm命令实现k8s集群的启动和管理等。
  • 第二个shell窗口:查看后台系统日志,执行命令tail -f /var/log/syslog,可以在这里查看到k8s集群启动和运行相关信息。
  • 第三个shell窗口:通过kubectl命令,和k8s控制平面沟通,在k8s集群中运行容器服务。

2. 安装docker作为k8s的容器运行时

k8s可以支持不同的容器运行时,常见的有,

  • containerd
  • CRI-O
  • Docker

本文采用docker作为k8s的容器运行时,如下安装步骤参考了官网资料,但有略微调整,

# 安装curl工具,若已安装可以跳过
$ sudo apt install curl

# Install packages to allow apt to use a repository over HTTPS
$ sudo apt-get update && sudo apt-get install -y \
  apt-transport-https ca-certificates curl software-properties-common gnupg2

# 添加docker apt repository
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key --keyring /etc/apt/trusted.gpg.d/docker.gpg add -
$ sudo add-apt-repository \
  "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) \
  stable"

# 安装docker
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli

# 安装完毕之后可以在如下路径查看到docker container runtime,这是k8s的缺省搜索路径
# https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/
# kubeadm automatically tries to detect an installed container runtime by scanning through a list of well known Unix domain sockets.
$ ll /var/run/docker.sock

# 配置 Docker daemon,设置cgroupdriver为systemd
# 
sudo mkdir /etc/docker
$ cat <<EOF | sudo tee /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}
EOF

# 重启 docker 后台服务
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

3. 安装k8s启动管理工具kubelet/kubeadm/kubectl

k8s提供了三个管理工具,用于k8s集群的启动和管理,

  • kubeadm:一个k8s管理工具,通过kubeadm init命令初始化master节点,通过kubeadm join命令部署worker节点。
  • kubelet:一个k8s集群组件,接受k8s控制平面指令,启动和管理当前节点上的k8s pods和containers。
  • kubectl:一个k8s命令行工具,方便和k8s控制平面进行交互,下发指令和查看集群状态。

这三个工具是搭建k8s集群的必要软件,如下为这三个工具软件的安装步骤

# 添加k8s apt国内源(请使用root用户运行如下命令)
$ curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add - 
$ cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
EOF
$ apt-get update

# 安装kubelet/kubeadm/kubectl
$ apt-get install -y kubelet kubeadm kubectl

# 启动kubelet
$ sudo systemctl daemon-reload
$ sudo systemctl restart kubelet

# 查看kubelet日志
$ tail -f /var/log/syslog

注:若在/var/log/syslog的日志中看到如下kubelet服务启动失败信息,可以先不用担心,在k8s master节点初始化之前,出现如下信息是正常的,kubelet会定时尝试连接k8s api server,直到成功。

kubelet[18544]: #011/workspace/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/go.opencensus.io/stats/view/worker.go:32 +0x57
systemd[1]: kubelet.service: Main process exited, code=exited, status=255/EXCEPTION
systemd[1]: kubelet.service: Failed with result 'exit-code'.

4. 启动k8s集群

下面将使用kubeadm工具启动k8s集群,启动命令如下,

kubeadm init <args>

在运行如上命令之前,建议做两个事情,

  • 运行kubeadm config images pull查看所需的镜像列表,由于gcr.io在国内被墙的原因,建议把这些镜像通过国内阿里云镜像源先拉取下来,再通过docker tag命令标记到gcr.io本地仓库下。详细步骤见4.1。
  • 考虑k8s service和pod的网段划分,避免和主机节点的网段冲突,本文在安装过程中设计的三个网段划分如下,
    • 主机节点网段:10.0.2.0/8
    • k8s service网段:10.1.0.0/16
    • k8s pod网段:10.244.0.0/16

4.1 获取k8s集群所需镜像

如下脚本可以方便获取启动k8s集群所需的镜像,

# kube-adm-images.sh
# 如下镜像列表和版本,请运行kubeadm config images list命令获取
# 
images=(
    kube-apiserver:v1.20.2
    kube-controller-manager:v1.20.2
    kube-scheduler:v1.20.2
    kube-proxy:v1.20.2
    pause:3.2
    etcd:3.4.13-0
    coredns:1.7.0
)

for imageName in ${images[@]} ; do
    docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/$imageName
    docker tag registry.cn-hangzhou.aliyuncs.com/google_containers/$imageName k8s.gcr.io/$imageName
done

如下为本地拉取的镜像列表,

# 获取k8s集群控制平面所需的镜像
$ kubeadm config images list
19385 version.go:101] could not fetch a Kubernetes version from the internet: unable to get URL "https://dl.k8s.io/release/stable-1.txt": Get "https://storage.googleapis.com/kubernetes-release/release/stable-1.txt": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
19385 version.go:102] falling back to the local client version: v1.20.0
k8s.gcr.io/kube-apiserver:v1.20.0
k8s.gcr.io/kube-controller-manager:v1.20.0
k8s.gcr.io/kube-scheduler:v1.20.0
k8s.gcr.io/kube-proxy:v1.20.0
k8s.gcr.io/pause:3.2
k8s.gcr.io/etcd:3.4.13-0
k8s.gcr.io/coredns:1.7.0

# 查看本地镜像
$ docker images list
docker images
REPOSITORY                                                                    TAG        IMAGE ID       CREATED         SIZE
k8s.gcr.io/kube-proxy                                                         v1.20.0    10cc881966cf   8 weeks ago     118MB
k8s.gcr.io/kube-controller-manager                                            v1.20.0    b9fa1895dcaa   8 weeks ago     116MB
k8s.gcr.io/kube-scheduler                                                     v1.20.0    3138b6e3d471   8 weeks ago     46.4MB
k8s.gcr.io/kube-apiserver                                                     v1.20.0    ca9843d3b545   8 weeks ago     122MB
k8s.gcr.io/etcd                                                               3.4.13-0   0369cf4303ff   5 months ago    253MB
k8s.gcr.io/coredns                                                            1.7.0      bfe3a36ebd25   7 months ago    45.2MB
k8s.gcr.io/pause                                                              3.2        80d28bedfe5d   11 months ago   683kB

4.2 运行kubeadm init命令

接下来就是运行kubeadm init命令来启动k8s集群,注意启动命令中的service-cidr和pod-network-cidr参数设置。

# 主机节点网段:10.0.2.0/8
# k8s service网段:10.1.0.0/16
# k8s pod网段:10.244.0.0/16
# 启动k8s集群
$ sudo kubeadm init \
   --image-repository registry.aliyuncs.com/google_containers \
   --kubernetes-version v1.20.2 \
   --service-cidr=10.1.0.0/16 \
   --pod-network-cidr=10.244.0.0/16

# 如下为完整运行日志
$ sudo kubeadm init \
>    --image-repository registry.aliyuncs.com/google_containers \
>    --kubernetes-version v1.20.2 \
>    --service-cidr=10.1.0.0/16 \
>    --pod-network-cidr=10.244.0.0/16
[init] Using Kubernetes version: v1.20.2
[preflight] Running pre-flight checks
    [WARNING SystemVerification]: this Docker version is not on the list of validated versions: 20.10.3. Latest validated version: 19.03
[preflight] Pulling images required for setting up a Kubernetes cluster
[preflight] This might take a minute or two, depending on the speed of your internet connection
[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'
[certs] Using certificateDir folder "/etc/kubernetes/pki"
[certs] Generating "ca" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local michaelk8s-virtualbox] and IPs [10.1.0.1 10.0.2.15]
[certs] Generating "apiserver-kubelet-client" certificate and key
[certs] Generating "front-proxy-ca" certificate and key
[certs] Generating "front-proxy-client" certificate and key
[certs] Generating "etcd/ca" certificate and key
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [localhost michaelk8s-virtualbox] and IPs [10.0.2.15 127.0.0.1 ::1]
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [localhost michaelk8s-virtualbox] and IPs [10.0.2.15 127.0.0.1 ::1]
[certs] Generating "etcd/healthcheck-client" certificate and key
[certs] Generating "apiserver-etcd-client" certificate and key
[certs] Generating "sa" key and public key
[kubeconfig] Using kubeconfig folder "/etc/kubernetes"
[kubeconfig] Writing "admin.conf" kubeconfig file
[kubeconfig] Writing "kubelet.conf" kubeconfig file
[kubeconfig] Writing "controller-manager.conf" kubeconfig file
[kubeconfig] Writing "scheduler.conf" kubeconfig file
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Starting the kubelet
[control-plane] Using manifest folder "/etc/kubernetes/manifests"
[control-plane] Creating static Pod manifest for "kube-apiserver"
[control-plane] Creating static Pod manifest for "kube-controller-manager"
[control-plane] Creating static Pod manifest for "kube-scheduler"
[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests". This can take up to 4m0s
[apiclient] All control plane components are healthy after 15.002558 seconds
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config-1.20" in namespace kube-system with the configuration for the kubelets in the cluster
[upload-certs] Skipping phase. Please see --upload-certs
[mark-control-plane] Marking the node michaelk8s-virtualbox as control-plane by adding the labels "node-role.kubernetes.io/master=''" and "node-role.kubernetes.io/control-plane='' (deprecated)"
[mark-control-plane] Marking the node michaelk8s-virtualbox as control-plane by adding the taints [node-role.kubernetes.io/master:NoSchedule]
[bootstrap-token] Using token: xxx.xxxx
[bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
[bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to get nodes
[bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
[bootstrap-token] configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
[bootstrap-token] configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
[bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
[kubelet-finalize] Updating "/etc/kubernetes/kubelet.conf" to point to a rotatable kubelet client certificate and key
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 10.0.2.15:6443 --token xxx.xxx --discovery-token-ca-cert-hash sha256:xxxx 

在k8s集群启动之后,可以通过kubectl get pods -A命令查看集群服务列表,

# 配置环境变量KUBECONFIG
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

# 查看k8s集群状态
$ kubectl cluster-info
Kubernetes control plane is running at https://10.0.2.15:6443
KubeDNS is running at https://10.0.2.15:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

# k8s集群控制平面服务列表,
# 1. k8s api server
# 2. k8s controller manager
# 3. k8s scheduler
# 4. k8s proxy
# 5. etcd
# 6. coredns

# 查看控制平面的服务列表
$ kubectl get pods -n kube-system
NAMESPACE     NAME                                            READY   STATUS    RESTARTS   AGE
kube-system   coredns-7f89b7bc75-685w4                        0/1     Pending   0          3m47s
kube-system   coredns-7f89b7bc75-tcp2b                        0/1     Pending   0          3m47s
kube-system   etcd-michaelk8s-virtualbox                      1/1     Running   0          3m53s
kube-system   kube-apiserver-michaelk8s-virtualbox            1/1     Running   0          3m53s
kube-system   kube-controller-manager-michaelk8s-virtualbox   1/1     Running   0          3m53s
kube-system   kube-proxy-6pvcb                                1/1     Running   0          3m47s
kube-system   kube-scheduler-michaelk8s-virtualbox            1/1     Running   0          3m53s

# 查看集群初始化配置,在正式环境的搭建过程中,可以通过kubeadm config的方式来初始化master/worker节点
$ sudo kubeadm config print init-defaults
$ sudo kubeadm config print join-defaults

这个时候查看kubelet的状态,可以看到已经正常启动。

# 查看kubelet的状态
$ systemctl status kubelet
● kubelet.service - kubelet: The Kubernetes Node Agent
     Loaded: loaded (/lib/systemd/system/kubelet.service; enabled; vendor preset: enabled)
    Drop-In: /etc/systemd/system/kubelet.service.d
             └─10-kubeadm.conf
     Active: active (running)
       Docs: https://kubernetes.io/docs/home/
   Main PID: 97961 (kubelet)
      Tasks: 17 (limit: 4650)
     Memory: 68.7M
     CGroup: /system.slice/kubelet.service
             └─97961 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubel>

但是若查看/var/log/syslog日志,仍然可以看到如下cni config uninitialized的异常信息,

$ tail -f /var/log/syslog
kubelet[22127]: 22127 cni.go:239] Unable to update cni config: no networks found in /etc/cni/net.d
kubelet[22127]: 22127 kubelet.go:2163] Container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized

其告知cni网络插件未配置,这是下一步安装工作需要做的事情。

5. 安装k8s网络插件Flannel

Flannel是一个k8s cni插件,在L3层可以通过VXLAN+UDP技术建立了一个覆盖网络。其安装步骤非常简单,直接运行kubectl apply命令即可,但是由于墙的原因,建议先把如下的镜像下载到本地。

docker images 
REPOSITORY               TAG        IMAGE ID       CREATED         SIZE
quay.io/coreos/flannel   v0.13.0    e708f4bb69e3   3 months ago    57.2MB

然后执行如下安装命令,

# 下载kube-flannel.yml文件,部署flannel网络插件
# 文件地址:https://gitee.com/pphh/blog/blob/master/210215_k8s_deployment/kube-flannel.yml
$ kubectl apply -f kube-flannel.yml

# 安装完毕之后,检查如下cni配置文件,若能找到则说明安装完成
$ cat /etc/cni/net.d/10-flannel.conflist 
{
  "name": "cbr0",
  "cniVersion": "0.3.1",
  "plugins": [
    {
      "type": "flannel",
      "delegate": {
        "hairpinMode": true,
        "isDefaultGateway": true
      }
    },
    {
      "type": "portmap",
      "capabilities": {
        "portMappings": true
      }
    }
  ]
}

# 查看flannel服务
$ kubectl get pods -A | grep flannel
NAMESPACE     NAME                                            READY   STATUS    RESTARTS   AGE
kube-system   kube-flannel-ds-2ntmg                           1/1     Running   0          54m

# 查看日志,
$ tail -f /var/log/syslog
Joining mDNS multicast group on interface flannel.1.IPv6 with address fe80::801e:edff:fece:975c.
New relevant interface flannel.1.IPv6 for mDNS.
Registering new address record for fe80::801e:edff:fece:975c on flannel.1.*.

6. 启动一个nginx服务

6.1 设置master节点可以运行pod

在默认情况下,出于安全原因,在master节点上不允许运行调度pod。在本例子中,为了方便部署可运行的单节点k8s集群,将关闭这个限制。

$ kubectl taint nodes --all node-role.kubernetes.io/master-
node/michaelk8s-virtualbox untainted

6.2 启动一个nginx演示程序

# 下载k8s-deployment-nginx.yml文件,启动nginx
# 文件地址:https://gitee.com/pphh/blog/blob/master/210215_k8s_deployment/k8s-deployment-nginx.yml
$ kubectl apply -f k8s-deployment-nginx.yml

# 下载k8s-deployment-nginx-svc.yml文件,启动nginx服务
# 文件地址:https://gitee.com/pphh/blog/blob/master/210215_k8s_deployment/k8s-deployment-nginx-svc.yml
$ kubectl apply -f k8s-deployment-nginx-svc.yml

# 查看nginx状态
$ kubectl get pods -a

$ 查看服务运行情况
$ kubectl get svc
NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE
kubernetes      ClusterIP   10.1.0.1     <none>        443/TCP          70m
nginx-service   NodePort    10.1.8.186   <none>        8000:32000/TCP   8s

打开浏览器访问如下地址,

  • http://10.1.8.186:8000/

可以看到welcome to nginx的欢迎信息,

8. 安装过程中遇到的一些问题

8.1 无法查看docker镜像列表

执行docker images命令时,告知permission denied的异常,

$ docker images
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get http://%2Fvar%2Frun%2Fdocker.sock/v1.24/images/json: dial unix /var/run/docker.sock: connect: permission denied

解决方案:切换到root账号下执行docker images命令即可。

8.2 拉取docker镜像问题

本文安装的k8s集群所用到的镜像如下,

$ sudo su -
root@michaelk8s-VirtualBox:~# docker images
REPOSITORY                                                                    TAG        IMAGE ID       CREATED         SIZE
nginx                                                                         latest     f6d0b4767a6c   3 weeks ago     133MB
k8s.gcr.io/kube-proxy                                                         v1.20.0    10cc881966cf   8 weeks ago     118MB
k8s.gcr.io/kube-scheduler                                                     v1.20.0    3138b6e3d471   8 weeks ago     46.4MB
k8s.gcr.io/kube-apiserver                                                     v1.20.0    ca9843d3b545   8 weeks ago     122MB
k8s.gcr.io/kube-controller-manager                                            v1.20.0    b9fa1895dcaa   8 weeks ago     116MB
quay.io/coreos/flannel                                                        v0.13.0    e708f4bb69e3   3 months ago    57.2MB
k8s.gcr.io/etcd                                                               3.4.13-0   0369cf4303ff   5 months ago    253MB
k8s.gcr.io/coredns                                                            1.7.0      bfe3a36ebd25   7 months ago    45.2MB
k8s.gcr.io/pause                                                              3.2        80d28bedfe5d   11 months ago   683kB
nginx                                                                         latest     f6d0b4767a6c   3 weeks ago     133MB

建议先尝试通过阿里云等国内镜像源服务拉取相关镜像,源地址有,

  • registry.aliyuncs.com/google_containers/kube-proxy
  • registry.cn-hangzhou.aliyuncs.com/google_containers/kube-proxy

若还是无法拉取,则考虑借助代理服务,访问外网镜像源。

8.3 k8s服务状态Init:ErrImagePull

查看k8s服务,有时候可以看到pod的状态一直为Init:ErrImagePull,这也是由于镜像无法拉取到本地导致,尝试切换镜像源。

# kubectl get pods -A
NAMESPACE     NAME                                            READY   STATUS    RESTARTS   AGE
kube-system   kube-flannel-ds-2ntmg                           0/1     Init:ErrImagePull   0          4m51s

8.4 k8s集群初始化时报yaml配置文件已经存在

在主机上第二次运行kubeadm init命令时,会告知yaml配置文件已经存在的错误信息。

$ kubeadm init
[init] Using Kubernetes version: v1.20.2
[preflight] Running pre-flight checks
    [WARNING SystemVerification]: this Docker version is not on the list of validated versions: 20.10.3. Latest validated version: 19.03
error execution phase preflight: [preflight] Some fatal errors occurred:
    [ERROR FileAvailable--etc-kubernetes-manifests-kube-apiserver.yaml]: /etc/kubernetes/manifests/kube-apiserver.yaml already exists
    [ERROR FileAvailable--etc-kubernetes-manifests-kube-controller-manager.yaml]: /etc/kubernetes/manifests/kube-controller-manager.yaml already exists
    [ERROR FileAvailable--etc-kubernetes-manifests-kube-scheduler.yaml]: /etc/kubernetes/manifests/kube-scheduler.yaml already exists
    [ERROR FileAvailable--etc-kubernetes-manifests-etcd.yaml]: /etc/kubernetes/manifests/etcd.yaml already exists
    [ERROR Swap]: running with swap on is not supported. Please disable swap
    [ERROR DirAvailable--var-lib-etcd]: /var/lib/etcd is not empty
[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`
To see the stack trace of this error execute with --v=5 or higher

解决方案:运行kubeadm reset重置本地配置,然后再次执行kubeadm init命令。

kubeadm启动k8s集群还有其它的报错信息,比如,

[ERROR Swap]: running with swap on is not supported. Please disable swap.

这个根据日志提示,关闭swap即可。

或者希望跳过该错误提示,可以对kubeadm/kubelet分别做如下配置,

  • kubeadm的启动命令中指定--ignore-preflight-errors=Swap
  • kubelet的配置文件中添加配置failSwapOn: False

8.5 系统日志报无网络插件Unable to update cni config

在安装了kubelet之后,通过sudo systemctl restart kubelet启动,但是发现服务没有正常启动,查看系统日志,可以看到如下信息,

5644 cni.go:239] Unable to update cni config: no networks found in /etc/cni/net.d
5644 kubelet.go:2163] Container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized

在安装Flannel插件之前,这个是正常报错信息,安装之后该报错信息会消失。

8.6 运行kubectl命令报连接server localhost:8080被拒绝

异常信息如下,

$ kubectl describe pod
The connection to the server localhost:8080 was refused - did you specify the right host or port?

解决方案:这是大多数原因是由于未进行kubectl的环境配置,请按照如下方法二选一进行配置,然后执行kubectl即可解决。

# 方法一:使用配置文件
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

# 方法二:使用环境变量
$ export KUBECONFIG=/etc/kubernetes/admin.conf

8.7 其它

更多k8s安装问题请见这里

9. k8s集群组件部署概图

10. 参考资料

  1. k8s工具安装指南
  2. 通过kubeadm初始化k8s集群
  3. Kubelet/Kubernetes should work with Swap Enabled
  4. k8s容器运行时
  5. firewalling, NAT, and packet mangling for linux
  6. k8s高可用拓扑信息
  7. CNI技术规格文档
  8. flannel介绍