本文介紹了利用開源 API 閘道器 APISIX 加速 NebulaGraph 多個場景的落地最佳實踐:負載均衡、暴露介面結構與 TLS Termination。
API 閘道器介紹
什麼是 API 閘道器
API 閘道器是位於客戶端和伺服器之間的“中間人”,用於管理、監控和保護 API。它可以在 API 之前執行一些操作,例如:身份驗證、授權、快取、日誌記錄、審計、流量控制、安全、防火牆、壓縮、解壓縮、加密、解密等。
API 閘道器可以工作在 TCP/IP 4 層和 OSI 7 層。跑在 7 層的 API 閘道器可以使用多種協議,例如:HTTP、HTTPS、WebSocket、gRPC、MQTT 等。在這些應用層協議中做一些操作,比如,請求的重寫、轉發、合併、重試、快取、限流、熔斷、降級、鑑權、監控、日誌、審計等等。
這裡舉例一下藉助 API 閘道器可以做的具體的事:
- 在閘道器層增加認證層,比如:JWT 認證、OAuth2 認證、OpenID 認證等等,這樣不需要在每個服務中都做具體的認證整合工作,進而節省許多開發成本。
- 藉助閘道器給跳板機 SSH 流量增加無需客戶端修改的複雜認證,比如:跳轉任何客戶端的 SSH 登入,給出一個網址或者輸入框,引導登陸者透過網頁的 SSO 認證(包含多因素認證),再透過閘道器轉發到 SSH 服務。
- 甚至在閘道器層做 Serverless 資料庫!TiDB 社群的同學們就在做這個事兒,他們從普通的 MySQL 客戶端的登入請求中解析能推斷出轉到需要的 TiDB 示例的資訊,並且在需要 cold start 喚醒例項的時候把連線保持住,可以參考這篇文章:TiDB Gateway。
- 如果你特別慘在維護屎山專案,不得不針對舊版本的應用程式對新版本的服務端進行相容,這時候 API 閘道器也可以透過一些請求重寫,把舊版本的請求轉換成新版本的請求。
只要腦洞大,理論上 API 閘道器可以做很多事。但顯然不是所有的事情都是適合在這一層去做的,通常那些比較通用的事情才適合在這一層去做,上面我只是給出一些典型和極端的具體例子。
Apache APISIX
API 閘道器是從 LB、Reverse Proxy 專案演進過來的。隨著雲原生的興起,API 閘道器也逐漸成為了雲原生的一部分,流行的開源閘道器有:
而且其中很多都是基於 Nginx/OpenResty 的下游專案。這裡就以 Apache APISIX 為例,介紹一下 NebulaGraph 藉助 API 閘道器的幾個實踐。
NebulaGraph 介紹
NebulaGraph 是一個開源的分散式圖資料庫,它的特點是:
- 高效能:可達到每秒百萬級的讀寫,具有極高的擴充套件性,在千億點、萬億邊的資料規模下支援毫秒級的查詢。
- 易擴充套件:分散式的架構可在多臺機器上擴充套件。每臺機器上可以執行多個服務程式,它的查詢層是無狀態的計算儲存分離架構,可以容易地引入不同配置、不同型別的計算層,實現同一叢集上 TP、AP、圖計算等不同負載的混合查詢。
- 易使用:類 SQL 的原生查詢語言,易於學習和使用,同時支援 openCypher。
- 豐富生態:NebulaGraph 的生態系統正在不斷壯大,目前已經有了多個客戶端,包括 Java、Python、Go、C++、JavaScript、Spark、Flink 等,同時也有了多個視覺化工具,包括 NebulaGraph Studio、NebulaGraph Dashboard、NebulaGraph Explorer 等。
本文討論的問題
本文給出了基於 NebulaGraph 叢集應用中涉及到 API 閘道器的幾個場景。
- 查詢介面的負載均衡
- 底層儲存介面的暴露
- 傳輸層的加密
查詢介面負載均衡
首先是圖資料庫查詢介面 graphd 的負載均衡與高可用的問題。
NebulaGraph 核心由三種服務組成:graphd、metad 和 storaged:
所以,在預設情況下,叢集只會暴露 graphd 的介面,提供給客戶端連線,執行 nGQL 的查詢。其中,graphd 是無狀態的,這意味著可以在多個 graphd 之間做負載均衡。這裡,我們有兩種方法:基於客戶端的(Client-Side LB)與基於代理的。
客戶端的負載均衡
客戶端的負載均衡,就是在客戶端,也就是應用程式中,實現負載均衡的邏輯。NebulaGraph 的各個語言的客戶端裡邊已經內建了輪詢(Round-Robin)負載均衡,我們只需要在客戶端配置多個 graphd 的地址就可以了。比如,我們在建立連線池的時候,指定了兩個不同的 graphd 的地址(對應不同程式例項),下面以 Python 程式碼為例:
from nebula3.gclient.net import ConnectionPool
from nebula3.Config import Config
config = Config()
config.max_connection_pool_size = 10
connection_pool = ConnectionPool()
connection_pool.init([('127.0.0.1', 9669), ('127.0.0.1', 49433)], config)
在取得連線的時候,就會從連線池中隨機取得一個連線:
In [10]: connection0 = connection_pool.get_connection()
In [11]: connection1 = connection_pool.get_connection()
# 這兩個連線的 graphd 地址是不同的
In [12]: connection0._port, connection1._port
Out[12]: (9669, 49433)
這種客戶端負載均衡的問題在於配置、實現細節與應用程式碼耦合在一起,如果需要修改負載均衡的策略,就要修改應用程式碼,這樣就會增加應用的複雜度。
代理的負載均衡
基於代理的負載均衡,就是在應用程式之前,增加一個代理層,來實現負載均衡的邏輯。這樣,應用程式就不需要關心負載均衡的問題了。在 K8s 裡的話,我們可以使用 K8s 的 Service 來實現這個代理層。
這是一個在 Minikube 中為 NebulaGraph 叢集中 graphd 建立的 Service:
cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Service metadata:
labels:
app.kubernetes.io/cluster: nebula
app.kubernetes.io/component: graphd app.kubernetes.io/managed-by: nebula-operator
app.kubernetes.io/name: nebula-graph
name: nebula-graphd-svc-nodeport
namespace: default
spec:
externalTrafficPolicy: Local
ports:
- name: thrift
port: 9669
protocol: TCP
targetPort: 9669
nodePort: 30000
- name: http
port: 19669
protocol: TCP
targetPort: 19669
nodePort: 30001
selector:
app.kubernetes.io/cluster: nebula
app.kubernetes.io/component: graphd app.kubernetes.io/managed-by: nebula-operator
app.kubernetes.io/name: nebula-graph
type: NodePort
EOF
建立後,我們就可以透過它暴露的單獨埠來訪問 NebulaGraph 叢集中的 graphd 了:
In [13]: connection_pool = ConnectionPool()
...: connection_pool.init([('192.168.49.2', 9669)], config)
Out[13]: True
In [14]: connection0 = connection_pool.get_connection()
In [15]: connection1 = connection_pool.get_connection()
In [16]: connection0._ip, connection1._ip
Out[16]: ('192.168.49.2', '192.168.49.2')
可以看到,在連線層面上來看,客戶端只知道代理的地址,而不知道 NebulaGraph 叢集中的 graphd 的地址,這樣就實現了客戶端與 NebulaGraph 叢集中的 graphd 的解耦。
然而,當我們在 Connection 之上建立 Session 的時候,就能看到實際上客戶端的不同請求是落在了不同的 graphd 上的:
In [17]: session = connection_pool.get_session('root', 'nebula')
In [18]: session._session_id
Out[18]: 1668670607568178
In [19]: session1 = connection_pool.get_session('root', 'nebula')
In [20]: session1._session_id
Out[20]: 1668670625563307
# 得到每一個 session 的 ID
In [21]: session.execute("SHOW SESSIONS")
# 它們分別對應了兩個不同的 graphd 例項
Out[21]: ResultSet(keys: ['SessionId', 'UserName', 'SpaceName', 'CreateTime', 'UpdateTime', 'GraphAddr', 'Timezone', 'ClientIp'], values: [1668670607568178, "root", "", utc datetime: 2022-11-17T07:36:47.568178, timezone_offset: 0, utc datetime: 2022-11-17T07:36:47.575303, timezone_offset: 0, "nebula-graphd-0.nebula-graphd-svc.default.svc.cluster.local:9669", 0, "172.17.0.1"],[1668670625563307, "root", "", utc datetime: 2022-11-17T07:37:05.563307, timezone_offset: 0, utc datetime: 2022-11-17T07:37:03.638910, timezone_offset: 0, "nebula-graphd-1.nebula-graphd-svc.default.svc.cluster.local:9669", 0, "172.17.0.1"])
底層儲存介面的暴露
在 NebulaGraph 中,可以透過 StorageClient 來訪問底層的儲存介面,這個介面可以用來做一些分析型、資料全掃描計算的工作。
然而,儲存層的分散式服務例項不像 graphd 那樣,它們是有狀態的。這其實與 K8s 或者 Docker Compose 的部署模型是相違背的。如果訪問的應用 storaged 客戶端在叢集外部,我們需要在 NebulaGraph 叢集中的每一個儲存例項上都部署一個代理 Service。這非常不方便,有時候還是一種浪費。
此外,由於 NebulaGraph 內部服務發現機制和 storaged 客戶端的實現機制決定,每一個 storaged 服務實體都是由其內部的 host:port
唯一確定和定址的,這給我們中間的代理工作也帶來了一些麻煩。
總結來看,我們的需求是:
- 能夠從叢集外部訪問 NebulaGraph 的儲存層每一個例項
- 每一個例項的訪問地址(host:port)和內部的地址是完全一致的
為了實現這個需求,我之前的做法是為每一個例項單獨部署一個 graphd 代理(消耗一個地址,保證埠不變),再在外部手動搭一個 Nginx 作為代理,配合 DNS 把內部的地址解析 Nginx 上,然後透過域名找到上游(每一個單獨的 graphd 代理)。本文的延伸閱讀 1、2 中給出了相關的實驗步驟。
最近,我找到了一個相對優雅的可維護的方式:
- 在 NebulaGraph 叢集同一個名稱空間下引入一個 APISIX 閘道器;
- 利用 APISIX 中的 Nginx TCP 代理的封裝 stream-proxy 來暴露 storaged 的介面;
- 為了最終只利用一個叢集的出口(Service,我們利用其支援的 TLSv1.3 中的 extend host name 欄位:SNI 來路由上游),做到用不同域名的 TCP over TLS 指向後端的不同 storaged;
- 只需要 Storage 客戶端能支援 TLSv1.3(傳送 SNI),並且能解析所有 storaged 的地址到 APISIX 的 Service 上即可;
示例圖:
┌────────────────────────────────────────────────────────────────────────────────────┐
│ K8s Cluster │
│ ┌──────────────────────────┐ │
│ ┌────────────────────────────────────┐ │ NebulaGraph Cluster │ │
│ │ APISIX API-GATEWAY │ │ ┌──────────────┐ │ │
│ │ │ │ │ storaged-0 │ │ │
│ │ │ ┌────┼──────▶│ │ │ │
│ │ │ │ │ │ │ │ │
│ │ ┌────────────────────────────┐ │ │ │ └──────────────┘ │ │
│ │ │ stream-proxy │ │ │ │ │ │
┌─────┐ │ .─────. │ │ ┌────┐ │ │ │ │ ┌──────────────┐ │ │
│ │ │╱ ╲ │ │ - addr: 9559 │ │──────┼───┼─┘ │ │ storaged-1 │ │ │
━━┫ DNS ┣━━( Service )╋━━━╋▶ tls: true │ │ │ │ ┌────┼──────▶│ │ │ │
│ │ │`. ,' │ │ │ │──────┼───┼─┘ │ │ │ │ │
└─────┘ │ `───' │ │ │ │ │ │ │ └──────────────┘ │ │
│ │ │ │SNI │ │ │ │ │ │
│ │ │ │ │──────┼───┼─┐ │ ┌──────────────┐ │ │
│ │ │ │ │ │ │ │ │ │ storaged-2 │ │ │
│ │ │ │ │ │ │ └────┼──────▶│ │ │ │
│ │ │ │ │──────┼───┼─┐ │ │ │ │ │
│ │ │ └────┘ │ │ │ │ └──────────────┘ │ │
│ │ └────────────────────────────┘ │ │ │ │ │
│ │ │ │ │ ┌──────────────┐ │ │
│ │ │ │ │ │ storaged-3 │ │ │
│ │ │ └────┼──────▶│ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ └──────────────┘ │ │
│ └────────────────────────────────────┘ └──────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────────────┘
這樣做的好處是:
- 在 APISIX 中比較優雅地維護代理的配置,並且可以用到 APISIX 現代化的流量管理能力;
- 不需要為每一個 storaged 單獨建立 Service,只需要一個 Service、叢集地址就可以了;
- 為流量增加了 TLSv1.3 的加密,提高了安全性。同時,沒有給 NebulaGraph 叢集內部的南北流量帶來的效能損耗;
在本文的結尾,給出了實驗過程,包含了本文提到的所有要點和細節。
傳輸層的加密
我們在前一個問題中提及到了,在 APISIX 閘道器中 terminate TLSv1.3 的連線,藉助 SNI 資訊路由 storaged 的方法。其實,單獨將 graphd 介面的 TLS 交給閘道器來做,好處也是非常明顯的:
- 證照管理在統一的閘道器控制面做,更加方便;
- 證照運維無 NebulaGraph 叢集配置侵入(NebulaGraph 原生支援 TLS 加密,但是加密之後帶來了叢集內部通訊的開銷,而且配置和叢集其他層面配置在一起,證照更新涉及程式重啟,不夠靈活);
具體的方法在後邊實操中也是有體現的。
實操:利用 APISIX 的 stream-proxy 暴露 storaged 的介面
實驗環境:Minikube
本實驗在本地的 Minikube 上做。首先,啟動一個 Minikube。因為 APISIX 內部的 etcd 需要用到 storageclass,我們帶上窮人版的 storageclass 外掛。同時,為了在 K8s 外部訪問 storaged 的時候用和內部相同的域名和埠,將把 node-port
允許的埠擴充到小於 9779 的範圍。
--addons="default-storageclass" \
--extra-config=apiserver.service-node-port-range=1-65535
實驗環境:NebulaGraph on K8s
這裡,我們使用 Nebula Operator 來部署 NebulaGraph 叢集,具體的部署方法可以參考 Nebula Operator 文件:https://docs.nebula-graph.com.cn/3.3.0/nebula-operator/1.introduction-to-nebula-operator/。
我們們做實驗,就偷個懶,用我寫的 Nebula-Operator-KinD 來一鍵部署:
curl -sL nebula-kind.siwei.io/install-on-K8s.sh | bash
實驗環境:APISIX on K8s
首先,是安裝。在 Helm 引數中指定開啟 stream-proxy 的開關:
helm repo add apisix https://charts.apiseven.com
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install apisix apisix/apisix \
--set gateway.type=NodePort \
--set gateway.stream.enabled=true \
--set ingress-controller.enabled=true
# dashboard 也裝上,方便我們繞過 admin API call 做一些方便的操作。
helm install apisix-dashboard apisix/apisix-dashboard
因為截止到現在,APISIX 的 Helm Chart 之中並沒有提供 stream-proxy TCP 的監聽埠的 TLS 支援的配置格式,見:https://github.com/apache/apisix-helm-chart/issues/348。我們需要手動更改 APISIX 的 ConfigMap,把 stream-proxy 的 TLS 配置加上:
kubectl edit ConfigMap apisix
我們編輯把 stream_proxy.tcp
改寫成這樣:
stream_proxy: # TCP/UDP proxy
only: false
tcp: # TCP proxy port list
- addr: 9779
tls: true
- addr: 9559
tls: true
這裡我們需要重建 APISIX Pod,因為 APISIX 的 stream-proxy 的 TLS 配置是在啟動的時候載入的,所以我們需要重建 APISIX Pod:
kubectl delete $(kubectl get po -l "app.kubernetes.io/name=apisix" -o name)
開始實驗
這個實驗的目標是把 NebulaGraph 的 storaged 的介面暴露出來,讓外部的客戶端可以訪問到,而暴露的方式如圖:
┌────────────────────────────────────────────────────────────────────────────────────┐
│ K8s Cluster │
│ ┌──────────────────────────┐ │
│ ┌────────────────────────────────────┐ │ NebulaGraph Cluster │ │
│ │ APISIX API-GATEWAY │ │ ┌──────────────┐ │ │
│ │ │ │ │ storaged-0 │ │ │
│ │ │ ┌────┼──────▶│ │ │ │
│ │ │ │ │ │ │ │ │
│ │ ┌────────────────────────────┐ │ │ │ └──────────────┘ │ │
│ │ │ stream-proxy │ │ │ │ │ │
┌─────┐ │ .─────. │ │ ┌────┐ │ │ │ │ ┌──────────────┐ │ │
│ │ │╱ ╲ │ │ - addr: 9559 │ │──────┼───┼─┘ │ │ storaged-1 │ │ │
━━┫ DNS ┣━━( Service )╋━━━╋▶ tls: true │ │ │ │ ┌────┼──────▶│ │ │ │
│ │ │`. ,' │ │ │ │──────┼───┼─┘ │ │ │ │ │
└─────┘ │ `───' │ │ │ │ │ │ │ └──────────────┘ │ │
│ │ │ │SNI │ │ │ │ │ │
│ │ │ │ │──────┼───┼─┐ │ ┌──────────────┐ │ │
│ │ │ │ │ │ │ │ │ │ storaged-2 │ │ │
│ │ │ │ │ │ │ └────┼──────▶│ │ │ │
│ │ │ │ │──────┼───┼─┐ │ │ │ │ │
│ │ │ └────┘ │ │ │ │ └──────────────┘ │ │
│ │ └────────────────────────────┘ │ │ │ │ │
│ │ │ │ │ ┌──────────────┐ │ │
│ │ │ │ │ │ storaged-3 │ │ │
│ │ │ └────┼──────▶│ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ └──────────────┘ │ │
│ └────────────────────────────────────┘ └──────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────────────┘
我們已經有了所有的框架,我們要往裡填箭頭和圓圈就行。
$ kubectl get po
NAME READY STATUS RESTARTS AGE
apisix-6d89854bc5-5m788 1/1 Running 1 (31h ago) 2d4h
apisix-dashboard-b544bd766-nh79j 1/1 Running 8 (31h ago) 2d10h
apisix-etcd-0 1/1 Running 2 (31h ago) 2d10h
apisix-etcd-1 1/1 Running 2 (31h ago) 2d10h
apisix-etcd-2 1/1 Running 2 (31h ago) 2d10h
nebula-graphd-0 1/1 Running 2 (31h ago) 3d4h
nebula-metad-0 1/1 Running 2 (31h ago) 3d4h
nebula-storaged-0 1/1 Running 2 (31h ago) 3d4h
nebula-storaged-1 1/1 Running 2 (31h ago) 3d4h
nebula-storaged-2 1/1 Running 2 (31h ago) 3d4h
配置 APISIX 的 stream-proxy
參考 APISIX 文件:https://apisix.apache.org/docs/apisix/stream-proxy/#accept-tls-over-tcp-connection。
我們用 APISIX 的 API 來配置 stream-proxy:
apisix_api_key="edd1c9f034335f136f87ad84b625c8f1"
apisix_pod=$(kubectl get po -l \
"app.kubernetes.io/name=apisix" -o name)
kubectl exec -it $apisix_pod -- \
curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 \
-H "X-API-KEY: $apisix_api_key" -X PUT -d \
'{
"sni": "nebula-storaged-0.nebula-storaged-headless.default.svc.cluster.local",
"upstream": {
"nodes": {
"172.17.0.13:9779": 1
},
"type": "roundrobin"
}
}'
kubectl exec -it $apisix_pod -- \
curl http://127.0.0.1:9180/apisix/admin/stream_routes/2 \
-H "X-API-KEY: $apisix_api_key" -X PUT -d \
'{
"sni": "nebula-storaged-1.nebula-storaged-headless.default.svc.cluster.local",
"upstream": {
"nodes": {
"172.17.0.18:9779": 1
},
"type": "roundrobin"
}
}'
kubectl exec -it $apisix_pod -- \
curl http://127.0.0.1:9180/apisix/admin/stream_routes/3 \
-H "X-API-KEY: $apisix_api_key" -X PUT -d \
'{
"sni": "nebula-storaged-2.nebula-storaged-headless.default.svc.cluster.local",
"upstream": {
"nodes": {
"172.17.0.5:9779": 1
},
"type": "roundrobin"
}
}'
這裡需要注意,目前,APISIX 的 stream-proxy 上游節點不支援域名解析是受限於上游的 lua 庫,詳見 issue:https://github.com/apache/apisix/issues/8334。理想情況下,這裡應該給出每一個 storaged 的 SNI 相同的地址作為 upstream.nodes
。像這樣:
kubectl exec -it $apisix_pod -- \
curl http://127.0.0.1:9180/apisix/admin/stream_routes/1 \
-H "X-API-KEY: $apisix_api_key" -X PUT -d \
'{
"sni": "nebula-storaged-0.nebula-storaged-headless.default.svc.cluster.local",
"upstream": {
"nodes": {
"nebula-storaged-0.nebula-storaged-headless.default.svc.cluster.local": 1
},
"type": "roundrobin"
}
}'
配置 APISIX 中 storaged 地址的 TLS 證照
在生產環境下,我們應該以雲原生的方式去管理自籤或者公共信任的證照。這裡,我們就手動利用 MKCert 工具來做這件事兒。
安裝 MKCert:
# 首次執行,需要安裝 mkcert,並且生成根證照
# macOS 的話
brew install mkcert
# ubuntu 的話
apt-get install wget libnss3-tools
# 然後再去 https://github.com/FiloSottile/mkcert/releases/ 下載 mkcert
簽發證照:
mkcert '*.nebula-storaged-headless.default.svc.cluster.local'
利用 APISIX Dashboard 將證照匯入到 APISIX 之中。單獨開一個終端,執行:
export POD_NAME=$(\
kubectl get pods \
-l "app.kubernetes.io/name=apisix-dashboard,app.kubernetes.io/instance=apisix-dashboard" \
-o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(\
kubectl get pod $POD_NAME \
-o jsonpath="{.spec.containers[0].ports[0].containerPort}")
kubectl \
port-forward $POD_NAME 8080:$CONTAINER_PORT --address='0.0.0.0'
瀏覽器訪問:http://10.1.1.168:8080/ssl/list,賬號密碼都是 admin
。點選 Create
按鈕,將剛剛生成的證照匯入到 APISIX 之中。
增加 APISIX 的 NodePort Service
建立一個 NodePort Service,用於暴露 APISIX 的 9779 埠。這樣,我們就可以透過外部的 IP 地址訪問到 APISIX 了。
cat <<EOF | kubectl apply -f -
spec:
selector:
app.kubernetes.io/instance: apisix
app.kubernetes.io/name: apisix
ports:
- protocol: TCP
port: 9779
targetPort: 9779
name: thrift
nodePort: 9779
type: NodePort
EOF
因為前邊 Minikube 中我們配置了埠的範圍覆蓋到了 9779,所以我們可以看到,這個 NodePort Service 的埠在宿主機上也可以從 Minikube ip 的同一個埠訪問到:
$ minikube service apisix-svc
$ minikube service list
|------------------------|---------------------------------|-------------------|---------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|------------------------|---------------------------------|-------------------|---------------------------|
...
| default | apisix-svc | thrift/9779 | http://192.168.49.2:9779 |<---
...
|------------------------|---------------------------------|-------------------|---------------------------|
當然,Minikube 假設我們的服務都是 HTTP 的,給出的 URL 是 HTTP://
的。不用理會它,我們心裡知道它是 TCP over TLS 就好了。
配置 K8s 外部 DNS
這裡需要配置一個 DNS 服務,讓我們可以透過 nebula-storaged-0.nebula-storaged-headless.default.svc.cluster.local
等三個域名透過 Minikube 的 NodePort Service 訪問到 NebulaGraph 的 storaged 服務。
獲得 Minikube 的 IP 地址:
$ minikube ip
192.168.49.2
配置 /etc/hosts
192.168.49.2 nebula-storaged-0.nebula-storaged-headless.default.svc.cluster.local
192.168.49.2 nebula-storaged-1.nebula-storaged-headless.default.svc.cluster.local
192.168.49.2 nebula-storaged-2.nebula-storaged-headless.default.svc.cluster.local
192.168.49.2 nebula-metad-0.nebula-metad-headless.default.svc.cluster.local
驗證 NebulaGraph Storage Client 可以從所有的節點中獲取到資料
這裡,為了方便,我們用到 Python 的客戶端。
由於在寫本文的時候,NebulaGraph Python 客戶端的 StorageClient 尚未支援 TLS,對它支援的 PR 剛好是我為了本實驗寫的:https://github.com/vesoft-inc/nebula-python/pull/239。
所以,這裡從個人分支安裝這個客戶端:
git clone https://github.com/wey-gu/nebula-python.git
cd nebula-python
python3 -m pip install .
python3 -m pip install ipython
# 進入 ipython
ipython
我們在 iPython 中互動式驗證:
from nebula3.mclient import MetaCache, HostAddr
from nebula3.sclient.GraphStorageClient import GraphStorageClient
from nebula3.Config import SSL_config
import ssl
import os
meta_cache = MetaCache([('nebula-metad-0.nebula-metad-headless.default.svc.cluster.local', 9559)],
50000)
storage_addrs = [HostAddr(host='nebula-storaged-0.nebula-storaged-headless.default.svc.cluster.local', port=9779),
HostAddr(host='nebula-storaged-1.nebula-storaged-headless.default.svc.cluster.local', port=9779),
HostAddr(host='nebula-storaged-2.nebula-storaged-headless.default.svc.cluster.local', port=9779)]
# 自簽證照配置
current_dir = os.path.abspath(".")
ssl_config = SSL_config()
ssl_config.cert_reqs = ssl.CERT_OPTIONAL
ssl_config.cert_reqs = ssl.CERT_OPTIONAL
ssl_config.ca_certs = os.path.join(
os.path.expanduser("~/.local/share/mkcert"), 'rootCA.pem'
)
ssl_config.keyfile = os.path.join(
current_dir, 'nebula-storaged-headless.default.svc.cluster.local+1-key.pem'
)
ssl_config.certfile = os.path.join(
current_dir, 'nebula-storaged-headless.default.svc.cluster.local+1.pem'
)
# 例項化 StorageClient
graph_storage_client = GraphStorageClient(meta_cache, storage_addrs, 5000, ssl_config)
# 驗證可以從所有的節點中獲取到資料
resp = graph_storage_client.scan_vertex(
space_name='basketballplayer',
tag_name='player')
while resp.has_next():
result = resp.next()
for vertex_data in result:
print(vertex_data)
結果✅:
("player112" :player{name: "Jonathon Simmons", age: 29})
("player117" :player{name: "Stephen Curry", age: 31})
("player119" :player{name: "Kevin Durant", age: 30})
("player134" :player{name: "Blake Griffin", age: 30})
("player141" :player{name: "Ray Allen", age: 43})
("player144" :player{name: "Shaquille O'Neal", age: 47})
("player149" :player{name: "Ben Simmons", age: 22})
("player100" :player{name: "Tim Duncan", age: 42})
("player101" :player{name: "Tony Parker", age: 36})
("player110" :player{name: "Cory Joseph", age: 27})
("player126" :player{name: "Kyrie Irving", age: 26})
("player131" :player{name: "Paul George", age: 28})
("player133" :player{name: "Yao Ming", age: 38})
("player140" :player{name: "Grant Hill", age: 46})
("player105" :player{name: "Danny Green", age: 31})
("player109" :player{name: "Tiago Splitter", age: 34})
("player111" :player{name: "David West", age: 38})
...
總結
- NebulaGraph 查詢介面的負載均衡可以藉助 K8s Service來做;
- NebulaGraph 底層儲存介面的暴露在 K8s 中可以利用 APISIX Stream Proxy 和 SNI 來優雅實現;
- 利用 API 閘道器對出口傳輸層的加密是一個很好的選擇,相較於用 NebulaGraph 原生的 TLS 的方式。
一些坑
fbthrift Python 並不支援傳送 extend host name(SNI):https://github.com/vesoft-inc/nebula-python/pull/238,寫了 PR 去做支援。這時候 APISIX 中的報錯是 failed to find SNI
:
2022/11/15 10:18:26 [error] 78#78: *1744270 stream [lua] init.lua:842: stream_ssl_phase(): failed to fetch ssl config: failed to find SNI:
please check if the client requests via IP or uses an outdated protocol. If you need to report an issue, provide a packet capture file of the TLS handshake., context:
ssl_certificate_by_lua*, client: 172.17.0.1, server: 0.0.0.0:9779
參考延伸閱讀的 3-6。
此外,我還發現 APISIX stream 裡邊不解析上游 node 域名,我查了所有 DNS 都沒有問題,去提了 issue 才知道是已知問題:https://github.com/apache/apisix/issues/8334,只好先手配 IP:Port
作罷。
2022/11/15 12:26:59 [error] 44#44: *9538531 stream [lua] resolver.lua:47: parse_domain(): failed to parse domain: nebula-storaged-0.nebula-storaged-headless.default.svc.cluster.local, error: failed to query the DNS server: dns client error: 101 empty record received while prereading client data, client: 172.17.0.1, server: 0.0.0.0:9779
2022/11/15 12:26:59 [error] 44#44: *9538531 stream [lua] upstream.lua:79: parse_domain_for_nodes(): dns resolver domain: nebula-storaged-0.nebula-storaged-headless.default.svc.cluster.local error: failed to query the DNS server: dns client error: 101 empty record received while prereading client data, client: 172.17.0.1, server: 0.0.0.0:9779
2022/11/15 12:26:59 [error] 44#44: *9538531 stream [lua] init.lua:965: stream_preread_phase(): failed to set upstream: no valid upstream node while prereading client data, client: 172.17.0.1, server: 0.0.0.0:9779
延伸閱讀
- https://gist.github.com/wey-gu/950e4f4c673badae375e59007d80d372
- https://gist.github.com/wey-gu/699b9a2ef5dff5f0fb5f288d692ddfd5
- https://docs.python.org/3/library/ssl.html#ssl.SSLContext.sslsocket_class
- https://github.com/apache/thrift/commit/937228e030569bf25ceb379c9491426709792701
- https://github.com/apache/thrift/pull/894
- https://github.com/apache/thrift/blob/e8353cb46e9f5e71f9b76f55d6bf59530b7f98ef/lib/py/src/transport/TSSLSocket.py#L184
謝謝你讀完本文 (///▽///)
要來近距離體驗一把圖資料庫嗎?現在可以用用 NebulaGraph Cloud 來搭建自己的圖資料系統喲,快來節省大量的部署安裝時間來搞定業務吧~ NebulaGraph 阿里雲端計算巢現 30 天免費使用中,點選連結來用用圖資料庫吧~
想看原始碼的小夥伴可以前往 GitHub 閱讀、使用、(^з^)-☆ star 它 -> GitHub;和其他的 NebulaGraph 使用者一起交流圖資料庫技術和應用技能,留下「你的名片」一起玩耍呢~