举个例子:2018 世界杯比赛数据后台
常规的基于数据库的实现方式
常规的实现方式,就是使用一个 MySQL 或者 MongoDB 作为后台的数据库。我的后台程序在接收到用户请求后,向数据库查询数据,然后返回结果为用户。在这个实现方式下,我需要自行编写 RESTful 的接口以及数据库的存取过程。虽然谈不上复杂,却也是不少的代码工作。
基于 CRD 的实现方式
准备工作:安装 Kubebuilder
Kubebuilder 是用来协助编写 K8s CRD 和 Operator 的代码生成工具和框架。在它的帮助下,我可以只写几行基本的资源定义,由它来完成大部分的代码工作。
安装 Kubebuilder:
-
curl -LO https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.0.0-beta.0/kubebuilder_2.0.0-beta.0_linux_amd64.tar.gztar xvf kubebuilder_2.0.0-beta.0_linux_amd64.tar.gzsudo mv kubebuilder_2.0.0-beta.0_linux_amd64 /usr/local/kubebuildercat <<EOF >> ~/.bashrcexport PATH=$PATH:/usr/local/kubebuilder/binEOF
请根据需要到 Kubebuilder releases 检查是否有新版本。
Kubebuilder 需要用到 kustomize,也把它安装上:
-
curl -LO https://github.com/kubernetes-sigs/kustomize/releases/download/v3.1.0/kustomize_3.1.0_linux_amd64chmod +x kustomize_3.1.0_linux_amd64sudo mv kustomize_3.1.0_linux_amd64 /usr/local/bin/kustomize
请根据需要到 kustomize releases 检查是否有新版本。
初始化项目
因为要用到 go mod,所以到一个 GOPATH 外的目录,创建项目:
-
mkdir fifa2018 && cd fifa2018go mod init github.com/fengye87/fifa2018
请根据需要替换 github.com/fengye87/fifa2018 为合适的项目路径。
用 Kubebuilder 生成初始项目代码:
-
kubebuilder init --domain fengye87.me
请根据需要替换 fengye87.me 为合适的值。
先跑起来看看
-
[root@localhost fifa2018]# make run/root/go/bin/controller-gen object:headerFile=./hack/boilerplate.go.txt paths=./api/...go fmt ./...go vet ./...go run ./main.go2019-08-13T17:48:31.438+0800 INFO controller-runtime.metrics metrics server is starting to listen {"addr": ":8080"}2019-08-13T17:48:31.439+0800 INFO setup starting manager2019-08-13T17:48:31.440+0800 INFO controller-runtime.manager starting metrics server {"path": "/metrics"}
到这里,我们的项目已经连上了 K8s,但目前基本上还是个空壳子,处于无所事事的状态。接下来,我们来给它填充点内容。
创建 Match CRD
通过 Kubebuilder 创建 Match CRD(仅创建 Resource 即可,不用创建 controller):
-
kubebuilder create api --group demo --version v1 --namespaced false --kind Match
请根据需要替换 demo 为合适的值。
在 api/v1/match_types.go 中,我们能找到 Match 资源的具体定义:
-
// api/v1/match_types.gotype Match struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec MatchSpec `json:"spec,omitempty"` Status MatchStatus `json:"status,omitempty"`}
可以看到,它和 K8s 的原生资源像是一个模子刻出来的,所以理解它应该不费什么脑筋。我们要修改的是其中的 MatchSpec 定义:
-
// api/v1/match_types.gotype MatchSpec struct { Datetime string `json:"datetime"` HomeTeam string `json:"homeTeam"` HomeTeamGoals int `json:"homeTeamGoals"` AwayTeam string `json:"awayTeam"` AwayTeamGoals int `json:"awayTeamGoals"`}
这就定义了 Match 资源的一些属性。理论上你也可以把这些属性直接定义在 Match 类型上,不过我个人更倾向把它们放到 MatchSpec 类型中。
定义完 CRD 后,需要把 CRD 安装到 K8s 上。直接 make install,没有报错就表示安装成功了,也可以进一步通过 kubectl api-resources|grep matches 的输出来确认下。
无保护地暴露 K8s API
到这里,我们已经能像 Pod、Service 等原生 K8s 资源一样来对待 Match 资源了。我们不仅能通过 kubectl 从命令行操作 Match 资源,还可以通过 K8s 的 RESTful API 进行访问和操作。
但是,稍等一下。通常来说,K8s 的 API 是受到保护的:在缺乏身份认证信息的情况下,我们几乎无法调用什么 API。不妨试下直接访问你的 K8s 根 API(例如 https://127.0.0.1:6443),通常是一个 403 返回。
K8s API 的认证方式有很多种,这里为了简单起见,我决定使用认证代理的模式来简单粗暴的把 K8s API 无保护的暴露出来。认证代理模式实际上是在 K8s API 的前面立一个反向代理,通过该反向代理而来的请求都会被 K8s 认为是已经经过认证了。而 K8s 为了确保请求的来源确实是这个反向代理,需要反向代理在调用 K8s API 的时候出示一个经过 K8s 根证书签名的客户端证书。如果是通过 kubeadm 安装的 K8s,那么证书和私钥分别在 /etc/kubernetes/pki/front-proxy-client.crt 和 /etc/kubernetes/pki/front-proxy-client.key,把它们拷贝到项目的根目录下。此外,虽然不是必须,但建议把根证书 /etc/kubernetes/pki/ca.crt 也拷贝过来。
利用标准库中的 httputil.ReverseProxy 实现一个 K8s 的认证代理(需要在 main 函数中用 goroutine 运行这个函数):
-
// main.gofunc serveProxy() error { u, err := url.Parse("https://127.0.0.1:6443") if err != nil { return err } rp := httputil.NewSingleHostReverseProxy(u) origDirector := rp.Director rp.Director = func(req *http.Request) { origDirector(req) req.Header.Set("X-Remote-User", "kubernetes-admin") req.Header.Set("X-Remote-Group", "system:masters") } clientCert, err := tls.LoadX509KeyPair("front-proxy-client.crt", "front-proxy-client.key") if err != nil { return err } caCert, err := ioutil.ReadFile("ca.crt") if err != nil { return err } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) rp.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ Certificates: []tls.Certificate{clientCert}, RootCAs: caCertPool, }, } return http.ListenAndServe(":3000", rp)}
到这里,我们再次 make run 运行它,用浏览器打开 http://127.0.0.1:3000/apis/demo.fengye87.me/v1/matches,可以看到正常返回了一个空的 MatchList,这是期望中的,因为我们还没有创建过任何 Match 对象。
用 kubectl 创建 Match 对象
目前,Match 资源的 API 已经可以正常工作了,但是还没有数据。因为 Match 资源是 K8s 的 CRD,所以可以非常方便的通过 kubectl 在后台添加 Match 对象:
-
# config/samples/demo_v1_match.yamlapiVersion: demo.fengye87.me/v1kind: Matchmetadata: name: group-a-1spec: datetime: '2018-06-14T18:00:00Z03:00' homeTeam: Russia homeTeamGoals: 5 awayTeam: Saudi Arabia awayTeamGoals: 0
执行 kubectl apply-f config/samples/demo_v1_match.yaml 就可以把上面的比赛数据保存到 K8s 的 API server 中。此时,再通过浏览器访问 http://127.0.0.1:3000/apis/demo.fengye87.me/v1/matches,将得到包含一个 Match 对象的 MatchList:
-
{ "apiVersion":"demo.fengye87.me/v1", "items":[ { "apiVersion":"demo.fengye87.me/v1", "kind":"Match", "metadata":{ "name":"group-a-1", "namespace":"default", ... }, "spec":{ "awayTeamGoals":0, "awayTeam":"Saudi Arabia", "datetime":"2018-06-14T18:00:00Z03:00", "homeTeamGoals":5, "homeTeam":"Russia", } } ], "kind":"MatchList", "metadata":{ "continue":"", "resourceVersion":"1111955", "selfLink":"/apis/demo.fengye87.me/v1/matches" }}
解决索引问题
OK,到目前为止,我没有写任何具体的 API 接口,也没有写任何具体的数据存取过程,仅仅是定义了一下 MatchSpec 中的各个字段,以及一个 40 行代码左右的 K8s 认证代理。但是,我已经得到了一个 Match 资源的 RESTful API,并且能将 Match 对象持久化保存起来,甚至还有一个后台的命令行工具(kubectl)。听起来很不错不是么?
但是有个问题,如果我想检索出东道主俄罗斯的所有比赛怎么办?在通常的数据库方案中,这个简直是小菜一碟,加一个索引就是了。但是在 K8s 的 CRD 中,需要一些操作。答案就是标签。
在 K8s 的原生资源中,我们通常会在 Pod 上打上一些标签,然后在 Service 上通过 selector 来筛选出服务所对应的 Pod。可见标签是一个比较合适的用来做筛选的方式。为 Match 对象加上合适的标签,同时加入更多的几场比赛数据以做测试:
-
# config/samples/demo_v1_match.yamlapiVersion: demo.fengye87.me/v1kind: Matchmetadata: name: group-a-1 labels: homeTeamCode: RUS awayTeamCode: SAUspec: datetime: '2018-06-14T18:00:00Z03:00' homeTeam: Russia homeTeamGoals: 5 awayTeam: Saudi Arabia awayTeamGoals: 0---apiVersion: demo.fengye87.me/v1kind: Matchmetadata: name: group-a-2 labels: homeTeamCode: EGY awayTeamCode: URYspec: datetime: '2018-06-15T17:00:00Z05:00' homeTeam: Egypt homeTeamGoals: 0 awayTeam: Uruguay awayTeamGoals: 1---apiVersion: demo.fengye87.me/v1kind: Matchmetadata: name: group-a-3 labels: homeTeamCode: RUS awayTeamCode: EGYspec: datetime: '2018-06-19T21:00:00Z03:00' homeTeam: Russia homeTeamGoals: 3 awayTeam: Egypt awayTeamGoals: 1---apiVersion: demo.fengye87.me/v1kind: Matchmetadata: name: group-a-4 labels: homeTeamCode: URY awayTeamCode: SAUspec: datetime: '2018-06-20T18:00:00Z03:00' homeTeam: Uruguay homeTeamGoals: 1 awayTeam: Saudi Arabia awayTeamGoals: 0
现在,在请求中加上 labelSelector 参数: http://127.0.0.1:3000/apis/demo.fengye87.me/v1/matches?labelSelector=homeTeamCode=RUS,返回的就只有俄罗斯参加的两场比赛,而不是全部四场比赛。
不过,使用标签进行筛选有一个限制:无法做到“或”。例如如果这里希望获取乌拉圭参加的所有比赛,那么无法在一个请求中完成。因为乌拉圭的两场比赛中,有一场是主场,另一场是客场。由于标签无法实现“homeTeamCode=URY 或 awayTeamCode=URY”这样的或语句,所以无法一步筛选出来。
结论:Long Live Kubernetes
以上就是把 K8s API server 用作数据库的大体操作了,总的来说,一个字“爽”。想象一下,后台 API 中的一个个资源,在实现的时候竟然只需要定义资源的 spec,剩下的竟全是免费午餐。不过,我们也必须看到这种方式的局限性。当涉及一些超越 CRUD 的复杂逻辑时,我们还是必须去实际编写那些业务逻辑代码。但不管怎么说,我们至少看到了 K8s 平台强大的扩展性,所以,Long Live Kubernetes!
作者:SmartX 微服务平台团队。SmartX 是国内超融合基础架构领域的技术领导者,其微服务平台团队专注于为内部微服务支撑平台的研发,持续不断地为微服务应用提供强大易用的平台功能,加速技术和产品落地。
登录后评论
立即登录 注册