寫在前面
kubectl雖然查詢單個的kubernetes資源或者列表都已經比較方便,但是進行更為多個資源的聯合查詢(比如pod和node),以及查詢結果的二次處理方面卻是kubectl無法勝任的。所以一直以來,我都有想法將kubernetes作為資料庫進行查詢。在去年,我開發了第二個版本的kubesql。相關資訊在https://xuxinkun.github.io/2019/03/11/kubesql/,程式碼留存在https://github.com/xuxinkun/kubesql/tree/python。這個版本較之我最早的spark離線方式已經有所改觀,但是無法應對中型、甚至較小規模的叢集,效能上存在較大問題。部署上也較為繁雜,且不夠穩定,有一些bug(會異常退出)。而且對於label等欄位都無法處理,可用性較差。我總起來不滿意,但是一直沒時間去重構。直到最近,聽了關於presto的一個分享,我感覺重構的機會來了。
這一次kubesql完全拋棄了原有的架構,基於presto進行開發。這裡摘抄一段presto的簡介:presto是一個開源的分散式SQL查詢引擎,適用於互動式分析查詢,資料量支援GB到PB位元組。Presto的設計和編寫完全是為了解決像Facebook這樣規模的商業資料倉儲的互動式分析和處理速度的問題。presto具有豐富的外掛介面,可以極為便捷的對接外部儲存系統。
考慮使用presto的主要原因是避免了SQL查詢引擎的邏輯耦合到kubesql中,同時其穩定和高效能保證了查詢的效率。這樣kubesql的主要邏輯專注於獲取k8s的resource變化,以及將resource轉化為關係型資料的邏輯上。
kubesql使用
重構後的kubesql開源專案地址在https://github.com/xuxinkun/kubesql。
先介紹下如何部署和使用。部署方式目前主要使用docker部署,很快會支援k8s的部署方式。
部署前需要獲取kubeconfig。假設kubeconfig位於/root/.kube/config路徑下,則只要一條命令即可執行。
docker run -it -d --name kubesql -v /root/.kube/config:/home/presto/config xuxinkun/kubesql:latest
如果橋接網路不能通k8s api,則可以使用物理機網路,加入--net=host引數即可。注意presto埠使用8080,可能會有埠衝突。
而後就可以進行使用了。使用命令為
docker exec -it kubesql presto --server localhost:8080 --catalog kubesql --schema kubesql
這時自動進入互動式查詢模式,即可進行使用了。目前已經支援了pods和nodes兩種資源的查詢,對應為三張表,nodes,pods和containers(container是從pod中拆出來的,具體原因見下文原理一節)。
三張表支援的列參見https://github.com/xuxinkun/kubesql/blob/master/docs/table.md。
presto支援一些內建的函式,可以用這些函式來豐富查詢。https://prestodb.io/docs/current/functions.html。
這裡我舉一些使用kubesql查詢的例子。
比如想要查詢每個pod的cpu資源情況(requests和limits)。
presto:kubesql> select pods.namespace,pods.name,sum("requests.cpu") as "requests.cpu" ,sum("limits.cpu") as "limits.cpu" from pods,containers where pods.uid = containers.uid group by pods.namespace,pods.name
namespace | name | requests.cpu | limits.cpu
-------------------+--------------------------------------+--------------+------------
rongqi-test-01 | rongqi-test-01-202005151652391759 | 0.8 | 8.0
ljq-nopassword-18 | ljq-nopassword-18-202005211645264618 | 0.1 | 1.0
又比如我想要查詢每個node上剩餘可以分配的cpu情況(用node上allocatable.cpu減去node上所有pod的requests.cpu的總和)
presto:kubesql> select nodes.name, nodes."allocatable.cpu" - podnodecpu."requests.cpu" from nodes, (select pods.nodename,sum("requests.cpu") as "requests.cpu" from pods,containers where pods.uid = containers.uid group by pods.nodename) as podnodecpu where nodes.name = podnodecpu.nodename;
name | _col1
-------------+--------------------
10.11.12.29 | 50.918000000000006
10.11.12.30 | 58.788
10.11.12.32 | 57.303000000000004
10.11.12.34 | 33.33799999999999
10.11.12.33 | 43.022999999999996
再比如需要查詢所有所有2020-05-12後建立的pod。
presto:kube> select name, namespace,creationTimestamp from pods where creationTimestamp > date('2020-05-12') order by creationTimestamp desc;
name | namespace | creationTimestamp
------------------------------------------------------+-------------------------+-------------------------
kube-api-webhook-controller-manager-7fd78ddd75-sf5j6 | kube-api-webhook-system | 2020-05-13 07:56:27.000
還可以根據標籤來查詢,查詢所有標籤的appid是springboot,且尚未排程成功的pod。以及計數。
標籤appid在pods表裡則會有一列,列名為"labels.appid",使用該列作為條件來刪選pod。
presto:kubesql> select namespace,name,phase from pods where phase = 'Pending' and "labels.appid" = 'springboot';
namespace | name | phase
--------------------+--------------+---------
springboot-test-rd | v6ynsy3f73jn | Pending
springboot-test-rd | mu4zktenmttp | Pending
springboot-test-rd | n0yvpxxyvk4u | Pending
springboot-test-rd | dd2mh6ovkjll | Pending
springboot-test-rd | hd7b0ffuqrjo | Pending
presto:kubesql> select count(*) from pods where phase = 'Pending' and "labels.appid" = 'springboot';
_col0
-------
5
kubesql原理
kubesql的架構如圖所示:
kubesql裡主要有三個模組部分:
- kubesql-watcher: 監聽k8s api pod和node的變化。並將pod和node的結構化資料轉化為關係型資料(以Map的方式進行儲存)。
- kubecache: 用於快取pod和node的資料。
- kubesql-connector: 作為presto的connector,接受來自presto的呼叫,通過kubecache查詢列資訊和對應資料,並返回給presto關於列和資料的資訊。
其中最主要的部分是kubesql-connector。presto外掛開發指南可以參考https://prestodb.io/docs/current/develop.html。我沒有選擇從零開始,而是基於已有的localfile外掛https://github.com/prestodb/presto/tree/0.234.2/presto-local-file進行的開發。如何進行presto的外掛開發,後面我再寫文章來解讀。
由於所有資料都快取在記憶體中,因此幾乎無磁碟需求。但是也需要根據叢集的規模來提供較大的記憶體。
以pod資料為例,pod中主要資料分成三部分,metadata,spec和status。
metadata中比較難以處理的部分是label和annotation。我將label這個map進行展平,每個key都作為一列。比如
labels:
app: mysql
owner: xxx
我使用labels作為字首,拼合labels裡面的key作為列名。從而得到兩條資料為:
labels.app: mysql
labels.owner: xxx
對於pod A存在app的label但是pod B並沒有該標籤,則對於pod B來說,該列labels.app的值則為null。
類似的annotations也是類似的處理方式。從而讓annotations也就可以成為可以用於篩選pod的條件了。
對於spec來說,最大的困難在於containers的處理。因為一個pod裡面可能有若干個containers,因此我直接將containers作為一張新的表。同時在containers表裡增加一個uid的列,用來表明該行資料來自於哪個pod。containers裡面的欄位也對應都加入到containers表中。containers中比較重要的關於資源的如request和limit,我直接使用requests.作為字首,拼合resource作為列名。比如requests.cpu
,requests.memory
等。這裡cpu的單獨處理為double型別,單位為核,比如100m這裡會轉化為0.1。記憶體等則為bigint,單位為B。
對於status中,比較難於處理的是conditions和containerStatus。conditions是一個列表,但是每個condition的type不相同。因此我將type作為字首,用來生成conditon的列名。比如:
conditions:
- lastProbeTime: null
lastTransitionTime: 2020-04-22T09:03:10Z
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: 2020-04-22T09:03:10Z
status: "True"
type: ContainersReady
那麼在pod表中,我對應可以得到這些列:
Column | Type | Extra | Comment |
---|---|---|---|
containersready.lastprobetime | timestamp | ||
containersready.lasttransitiontime | timestamp | ||
containersready.message | varchar | ||
containersready.reason | varchar | ||
containersready.status | varchar | ||
ready.lastprobetime | timestamp | ||
ready.lasttransitiontime | timestamp | ||
ready.message | varchar | ||
ready.reason | varchar | ||
ready.status | varchar |
這樣我就可以通過"ready.status" = "True" 來篩選condition裡type為ready且status為True的pod了。
containerStatus因為與containers一一對應,因此我將containerStatus合併到containers表裡,並且根據container name一一對應起來。
後記
本次重構後kubesql我直接釋出為1.0.0版本,並且已經在日常使用了。且藉助於記憶體和presto的高效能,我測試過5萬pod的叢集,查詢時間為毫秒級。目前暫未發現明顯的bug。大家有發現bug或者新的feature也可以提issue給我。我後期也會再維護該專案。
因為目前只有pods和nodes資源,相對於k8s龐大的資源來說,還只是冰山一角。但是增加每個資源要加入相當數量的程式碼。我也在考慮如何使用openapi的swagger描述來自動生成程式碼。
部署上現在是用docker來部署,馬上也會增加kubernetes的部署方式,這樣會更加便捷。
同時我在考慮,在未來,讓presto的每個worker負責一個叢集的cache。這樣一個presto叢集可以查詢所有的k8s叢集的資訊。該功能還需要再做設計和考慮。