前言
该文章是阿里云云原生课堂第四章学习笔记,链接如下:
阿里云:https://edu.aliyun.com/lesson_1651_16895?spm=5176.10731542.0.0.5eae7abdPMypiS#_16895(非常好的课程)
Pod
在说 Pod 之前,先简单介绍一下容器,对容器有过了解的应该知道,容器本身就是一个进程
只不过 容器是一个视图被隔离、资源所限制的进程(通过 Namespace、Cgroup、chroot 命令来实现进程、资源、文件系统的隔离,所以容器的隔离性是远不如 VM 的)
在 Docker 中 pid 为 1 的进程其实就是应用本身,所以我们可以理解为:容器就是应用本身
所以我们管理容器其实就是相当于在管理容器本身
那么 K8s (全名:Kubernetes )是什么?
试想在云环境下,都是容器如果没有统一编排管理的话,比如我开了容器,那么放到哪个集群的哪台服务器上呢?如果当前容器收到请求过大 CPU 占用过高我如何扩容呢?我总不可能人工去做吧...
K8s 是一个容器编排平台,能够对容器进行编排管理、扩充等。K8s 能将容器放到一个集群上的某个机器上去、能帮助自动发布、自动回滚、水平伸缩等等...
那么 K8s 中的 Pod 又是什么?
Pod 简单的来说就是单个或多个容器的集合
Ps:其实这样介绍还是很抽象这里先有一个概念,还是无法说清楚 Pod 的由来,所以会在下文会进行介绍
为什么会有 Pod
在前面说了 K8s 能对容器自动进行编排,那么 K8s 又要引入 Pod 这个概念呢?
接下来我们结合例子来看:
在真实业务环境中,往往都不是单个容器存在的,很多时候容器之间都是紧密合作的
例如:
第一个容器叫做 App,就是业务容器,它会写日志文件;
第二个容器叫做 LogCollector,它会把刚刚 App 容器写的日志文件转发到后端的 ElasticSearch 中
那么这两个容器之间就是紧密结合的
两个容器的资源需求是这样的:
App 容器需要 1G 内存,LogCollector 需要 0.5G 内存,而当前集群环境的可用内存是这样一个情况:Node_A:1.25G 内存,Node_B:2G 内存
那么在容器调度上,如果先将 App 调度到 Node_A 上,那么 LogConllector 就没有办法调度到 Node_A 上了,因为资源不够
所以 K8s 的解决方案就是将这里的 App 和 LogConllector 放到一个 Pod 里面,并将 Pod 作为 K8s 的最小资源调度单位,这样就不会出现上面那样的问题了,这样由于 Pod 需要 1.5 G 的内存资源所以 Pod 就会被放到 Node_B 中
ps:Pod 在 K8s 中只是一个逻辑单位,在实际使用中还是2个容器,只不过 K8s 把这两个容器看做一个单位
那么有可能这时候又会有人有疑惑,为什么不把这两个应用都部署到一个容器中?
在前文提到过,在 Docker 中 pid 为 1 的进程其实就是应用本身,所以我们可以理解为:容器就是应用本身
所以如果我们将多个应用打包进一个容器中,那么我们也只能对 pid 为 1 的应用进行管理,对于其他的应用我们就无法有效管理了,所以最好的解决办法就是将应用进行分开,即一个容器就是一个应用
超亲密关系
引用课程中的截图
像正常的亲密关系我们可以通过调度来进行解决
但是超亲密关系由于两个容器之间的交互会非常频繁,所以对性能需求会比较高
所以针对这些情况我们需要在 pod 内进行解决处理
Pod 要解决的就是:如何让一个 Pod 里的多个容器之间最高效的共享某些资源和数据
具体的解法分为两个部分:共享网络和共享存储。
共享网络
直接引用原文了,说的已经非常清楚了hh
Kubernetes 里的解法是这样的:它会在每个 Pod 里,额外起一个 Infra container 小容器来共享整个 Pod 的 Network Namespace。
Infra container 是一个非常小的镜像,大概 100~200KB 左右,是一个汇编语言写的、永远处于“暂停”状态的容器。由于有了这样一个 Infra container 之后,其他所有容器都会通过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中。
所以说一个 Pod 里面的所有容器,它们看到的网络视图是完全一样的。即:它们看到的网络设备、IP地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于 Pod 第一次创建的这个 Infra container。这就是 Pod 解决网络共享的一个解法。
在 Pod 里面,一定有一个 IP 地址,是这个 Pod 的 Network Namespace 对应的地址,也是这个 Infra container 的 IP 地址。所以大家看到的都是一份,而其他所有网络资源,都是一个 Pod 一份,并且被 Pod 中的所有容器共享。这就是 Pod 的网络实现方式。
由于需要有一个相当于说中间的容器存在,所以整个 Pod 里面,必然是 Infra container 第一个启动。并且整个 Pod 的生命周期是等同于 Infra container 的生命周期的,与容器 A 和 B 是无关的。这也是为什么在 Kubernetes 里面,它是允许去单独更新 Pod 里的某一个镜像的,即:做这个操作,整个 Pod 不会重建,也不会重启,这是非常重要的一个设计。
共享存储
共享存储也比较简单,我们通过让 Volume 共享即可,即 Pod 中的容器都共享一个 Volume 这样的话就实现了共享存储了
容器设计模式:SideCar
还是以例子来引出需求(即为什么会有sidecar的产生)
我现在要发布一个应用,这个应用是 JAVA 写的,有一个 WAR 包需要把它放到 Tomcat 的 web APP 目录下面,这样就可以把它启动起来了。
可是像这样一个 WAR 包或 Tomcat 这样一个容器的话,怎么去做,怎么去发布?这里面有几种做法。
第一种我们就不考虑了,每次版本迭代都需要重新制作镜像太过于麻烦了
那么接下来我们来看第二种,通过 volume 的方式将war包挂载到容器中
这种方法看着比第一种要好多了但是还是会有一些问题,因为容器是具有迁移性的,有可能今天在 A 明天就在 B 了,那么我们就需要利用分布式存储系统来从而确保无论位置怎么变化都能找得到
注意:即使有了分布式存储系统做 Volume,你还需要负责维护 Volume 里的 WAR 包。比如:你需要单独写一套 Kubernetes Volume 插件,用来在每次 Pod 启动之前,把应用启动所需的 WAR 包下载到这个 Volume 里,然后才能被应用挂载使用到。
这样操作带来的复杂程度还是比较高的,且这个容器本身必须依赖于一套持久化的存储插件(用来管理 Volume 里的 WAR 包内容)
InitContainer
所以像这样的组合方式,有没有更加通用的方法?哪怕在本地 Kubernetes 上,没有分布式存储的情况下也能用、能玩、能发布。
实际上方法是有的,在 Kubernetes 里面,像这样的组合方式,叫做 Init Container。
InitContainer 是最先运行的,所以可以通过定义 InitContainer 事先将镜像中的内容存储到 Volumne 中,这时 Volume 中已存在我们需要的 war 包了 所以后面启动 Tomcat 容器的时候只要挂载 Volumn 就可以了
这样就可以构成一个自包含的Pod ,可以把这一个 Pod 在全世界任何一个 Kubernetes 上面都顺利启用起来。不用担心没有分布式存储、Volume 不是持久化的,它一定是可以公布的。
来简单弄个例子:
创建一个含有 sample.war 的镜像,编写一个 dockerfile
FROM alpine:latest
COPY sample.war /
CMD [ "/bin/sh" ]
至于 tomcat 的话直接用官方的就行了
yaml 如下
apiVersion: v1
kind: Pod
metadata:
name: javaweb
labels:
app: javaweb
spec:
initContainers:
- name: war
image: kpli0rn/sample:lastest
command: ["cp","/sample.war","/app"]
volumeMounts:
- mountPath: /app
name: app-volume
containers:
- name: tomcat
image: kpli0rn/tomcat:8.5.73-jdk8-openjdk-slim
command: ["/usr/local/tomcat/bin/catalina.sh","run"]
volumeMounts:
- mountPath: /usr/local/tomcat/webapps
name: app-volume
ports:
- containerPort: 8080
hostPort: 8001
volumes:
- name: app-volume
emptyDir: {}
部署
状态正常
暴露端口 kubectl expose pod javaweb --type=LoadBalancer --port=8080
SideCar
像这样的一个概念,在 Kubernetes 里面就是一个非常经典的容器设计模式,叫做:“Sidecar”
SideCar 即在 Pod 中定义一个辅助容器来进行辅助工作就像上面的 InitContainer ,只负责将war装载到 Volume 中
SideCar 能做很多事情,接下来列举几个课堂上提到的案例
日志收集
代理容器
这个最下面会有简单的例子
适配容器
这个实现原理也不复杂,我们只需将收到收到特定接口来做一个转发
/healthz => /metrics
接下来举一个 SideCar 充当代理的例子
代理容器案例
我们利用 go 来编写一个代理容器来做个小案例,这个代理容器主要是将请求转发到业务容器,然后利用 httpbin 来充当业务容器,利用 minikube 来充当 K8s 的本地节点
K8s 入门教程:https://kubernetes.io/zh/docs/tutorials/hello-minikube/
代理
config.go
Port 是 Proxy 的端口,ServicePort是我们业务的端口 80
const (
Port = 8000
ServicePort = 80
)
首先创建一个 Proxy 结构体
type Proxy struct {}
既然是代理的话我们主要有以下几个功能:
- 请求转发
- 响应转发
- 日志记录
请求转发如下:
Proxy 将收到的请求转发到 80 端口,并返回 response ,时间,error
func (proxy *Proxy) ForwardRequest(req *http.Request) (*http.Response, time.Duration, error){
proxyUrl := fmt.Sprintf("http://127.0.0.1:%d%s", config.ServicePort, req.RequestURI)
fmt.Printf("Original URL: http://%s%s\n",req.Host, req.RequestURI)
fmt.Printf("Proxy URL: http://%s\n",proxyUrl)
// send http request to inner
httpClient := &http.Client{}
// create Request, But not send
proxyReq, err := http.NewRequest(req.Method, proxyUrl, req.Body)
start := time.Now()
res, err := httpClient.Do(proxyReq)
duration := time.Since(start)
return res, duration, err
}
响应转发:
将从 80 收到的请求转法给外部
func (proxy *Proxy) WriteResponse(w http.ResponseWriter, resp *http.Response){
for name, value := range resp.Header{
w.Header()[name] = value
}
w.Header().Set("Server","Tiny-Proxy") // 增加自定义header头
w.WriteHeader(resp.StatusCode)
io.Copy(w,resp.Body)
resp.Body.Close()
}
弄一下日志记录:
func (proxy *Proxy) PrintMsg(req *http.Request, resp *http.Response, duration time.Duration){
fmt.Printf("Request Duration: %v\n",duration)
fmt.Printf("Request Length: %d\n",req.ContentLength)
fmt.Printf("Response Length: %d\n",resp.ContentLength)
fmt.Printf("Response StatusCode: %d\n",resp.StatusCode)
}
最后都添加到 ServeHTTP 方法中,由于最后是通过 http.ListenAndServe 来进行的开启,所以我们的结构体必须要实现 ServeHTTP 方法
func (proxy *Proxy) ServeHTTP(w http.ResponseWriter,req *http.Request){
res, duration, err := proxy.ForwardRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway) //return the error
return
}
// forward request
proxy.ForwardRequest(req)
// sendback response
proxy.WriteResponse(w,res)
proxy.PrintMsg(req, res, duration)
}
func main(){
// Proxy 必须要实现 ServeHTTP 方法
http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d",config.Port),&core.Proxy{})
}
然后go编译一下,简单的写个 dockerfile,docker build -t xxxx .
并编译成镜像传到 dockerhub 中
FROM alpine:latest
COPY TinyProxy .
CMD [ "./TinyProxy" ]
EXPOSE 8000
部署
准备好了之后就可以开始编写 yaml ,kubectl 可以通过 kubectl apply -f xxxx.yaml
来进行快速创建 Pod
yaml编写说明:https://kubernetes.io/zh/docs/concepts/overview/working-with-objects/kubernetes-objects/
apiVersion: v1
kind: Pod
metadata:
name: httpbin-pod
labels:
app: httpbin
spec:
containers:
- name: service01
image: kennethreitz/httpbin # 业务镜像
ports:
- containerPort: 80
hostPort: 80
- name: proxy
image: kpli0rn/tinyproxy:lastest # 自己编写的代理镜像
ports:
- containerPort: 8000
利用 kubectl get pods
来查看 pod 情况
默认情况下,Pod 只能通过 Kubernetes 集群中的内部 IP 地址访问。 要使得 httpbin-pod
容器可以从 Kubernetes 虚拟网络的外部访问,你必须将 Pod 暴露为 Kubernetes Service
kubectl expose pod httpbin-pod --type=LoadBalancer --port=8000
通过 kubectl 查看创建的 Service
最后运行 minikube service httpbin-pod 就创建好了
访问网址,response 中发现存在了我们的 header 头说明一切正常
同时也可以通过 kubectl logs 来查看指定容器的输出日志
参考链接
sidecar实验 :https://www.servicemesher.com/blog/hand-crafting-a-sidecar-proxy-like-istio/
阿里云:https://edu.aliyun.com/lesson_1651_16895?spm=5176.10731542.0.0.5eae7abdPMypiS#_16895