杳杳寒山道

杳杳寒山道

唐代:寒山

杳杳寒山道,落落冷涧滨。
啾啾常有鸟,寂寂更无人。
淅淅风吹面,纷纷雪积身。
朝朝不见日,岁岁不知春。

一条幽暗寂静的寒山道上,旁边流淌着飘飘落落的山水小溪。 驻足而立,可以闻见山谷中鸟儿啾啾地啼鸣,四周远望,山中空无一人。 风淅淅沥沥地吹向脸庞,突然空中飘起了雪花,纷纷扬扬地落在了我身上。在这里,一年四季难得见一束温暖的阳关,终年也不知春天的花香鸟语,却是一片静心之地。

架构艺术之应用分层设计

图片来自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