软件工程的角度看k8s
我们先来看看软件工程的定义,其实总结成一句话就是为了交付某些产品或者功能的方法论。 在"没有银弹"的论文中讨论了本质复杂度和偶然复杂度。 本质复杂度是指实现问题的本身就有的复杂度,偶然复杂度是指实现问题的过程中产生的复杂度。
k8s主要要解决的则是软件层面服务部署很运维的复杂度,能够实现软件的快速部署和回滚,最核心的Pod的抽象也是解决了容器创建、调度和销毁的问题。
知道了它解决的核心问题,那么有些内容其实就可以交由第三方来进行拓展,比如长连接的负载均衡则是在应用层相对比较灵活的,涉及业务本身的流量治理的问题,所以k8s本身并不支持长连接的负载均衡,而是交给第三方去做,比如service mesh的sidecar模式就是这样演化出来的。这样既复用了k8s Pod的抽象,也能够在不改动k8s本身代码的情况下去实现长连接的负载均衡。
Kubernetes 技术的本质是什么?
《技术的本质》中提出了新技术是从老技术中孕育出来,这种机制为组合进化。
Kubernetes
也是老技术的新组合,为了部署运维大量集群服务而产生的工具,它使用操作系统已有的功能组合降低应用运维的难度。
技术是那些被捕捉并加以应用的现在的集合。为什么要制造这个概念?
通过概念的定义能让我们去研究和讨论技术是如何发展的。
因为社会的发展不随随人的意志而发生转移,社会的发展依托于技术的发展,那技术的发展是怎么实现的呢?这就是所要思考的技术的本质。
从促进业务发展的角度去看Kubernetes,它的出现也是为了促进 Devops 的发展,让我们的应用运维部署更加简单,减轻了人工的操作,即软件工程中的accidential complexity。
这个时候我们上线一款新的应用的时候,可以专注于解决业务问题本身。
- 有了操作系统隔离技术cgroup的支持,才有容器化部署的基础。
- 有了操作系统提供的 netlink和netfilter功能, 才有了网络功能的基础。
Kubernetes 将这些模块功能进行重新组合,用来解决部署运维时需要手工创建虚拟环境和网络设置等的问题。
搞清楚技术的本质我们也可以组合我们的技术来进行进化,比如我们知道了网络上Kubernetes iptables由于规则增加会带来性能瓶颈,这个时候我们可以更换 iptables 这个零件,换成Linux内核的新功能比如 ipvs或者是 nftable。
理解了 Kubernetes 如何对技术进行编排后,我们对 Kubernetes 新出的特性其实就很容易理解,都是在主程序不变的情况下更换更优质的零件,让整体的性能表现的更好。
我们解决产品需求的功能也是对既有技术的一个编程,通过数据库技术将数据提供给业务产品进行使用。
以物流为例,我们在配送订单的场景时,会使用关系型数据库来解决业务上的功能,通过将每个订单存储成一行的数据,从而在用户需要进行操作或者其他运营统计的时候,可以快速访问到。
然后随着订单量的增大,我们会根据不同场景的操作需求,引入分布式KV缓存、内存缓存、分库分表等优化技术,一步一步替换我们最初的实现。
可以看到我们去实现业务需求的时候也是对现有技术的一个组合。
所以 Kubernetes技术的本质这个问题也显而易见,就是为了解决于手工运维的困难,通过代码来将可以自动化的功能进行编排,让机器代码人工来实现。
技术的出现本身就是为了解决问题,为了促进效率的提升,从而让软件产品的研发提效,更快的产出价值。
多个角度去看 Kuberntes
同一个信息,拥有越多的视角,则能获得的信息越多。
同样是K8s,作为运维开发能看到的是应用部署操作的简易程度。
作为后端研发则是看到它在并发编程的优雅处理,也可以看到它通过将偶然复杂度的封装,让我们研发过程中能屏蔽掉运维部署的复杂度。
作为产品经理能看到的是云原生带来的持续高可用,不需要再过度担心服务 SLA 的问题。
从技术本质的角度看 Kuberntes 容器编排的核心能力,搭配上CNI、CRI、CSI等插件的接入,更大程度的支持了虚拟化的进程。
Kubernetes 模块设计
K8s 不同模块组成了整个集群管理的工作架构。
apiserver 负责接收外部变更,并且提供了informer通知机制,ETCD提供内部存储实现集群状态的管理。
CNI 插件为集群维护网络设置,当有新的资源上线需要网络的时候先为它添加物理上的网络
kube-proxy 实现了网络的负载均衡,通过订阅 informer
来维护CNI创建的网络信息。
kubelet 接收通知来维护各种资源的状态。
因为通知机制是所有组件联动的核心,所以单独成独立的模块进行复用是有价值的,并且模块化后可以单独提升模块的可用性和性能。
分模块后我们做设计也更能聚焦于功能建设,在需要相似功能的依赖时不需要过多思考直接复用,降低了其他依赖模块实现难度,能更好的思考如何服务整体系统而不会掉入细节中。
为什么 Kubernetes 用Go实现?
当我们学 Kubernetes
源码的过程就会发现,它实际上是对集群的状态进行管理,并且在整体架构的抽象上,有 api-server
处理外部的请求,并且将请求通过推送的方式发给 kubelet
组件进行维护。 kubelet
组件则是通过调用 CRI
来实现 sandbox
的创建,所以在事件的传递上,用 Go
的实现是非常简单并且非常高效的。
后端研发不是一门语言说完的,而是由很多技术栈构成的,每个语言都有自己擅长的领域,用契合的技术实现自己的想法才是关键。
Go被推上神坛也是由于它在云原生扮演了重要的作用,在并发编程这一块有context和 协程作为支撑,让它广受欢迎,也由于微服务的推广,可以用go来实现业务系统的后端。
Java虽然在企业级开发里面统一了位置,但是也由于微服务大部分不需要如此复杂和繁重的架构,让go能够占据一席之地。
python在机器学习和算法实现的简便性上无可替代,但是也由于弱类型的特性,让它在做企业级应用上并不适合多人协作,且协程编程这一块过于复杂,导致难以胜任 Kubernetes 这种需要大量并发编程的场景。
通过软件架构设计解决数据库产品使用的性能短板,比如Kubernetes中就使用了大量的并发编程来充分利用cpu 资源,也提升了整体事件变更的处理性能,这也是K8s选择go语言进行编程的原因,因为它的并发编程模型非常简单且高效。
比如我们可以通过 channel 来进行数据的共享,并且可以用 context.Done
的信号来控制所有进程是否退出。
csharp
func watch(ctx context.Context, event chan string){
for {
select {
case <-ctx.Done():
return
case <-event:
// do something
}
}
}
或者是需要通过定时器来定时维护集群的状态
go
func watch(ctx context.Context){
ticker := time.NewTicker(time.Second * 10)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
// do something
}
}
}
编写起来都是十分简单且易于理解的,这样 Kubernetes 就可以更加专注于容器编排的逻辑实现。