眾所周知, RSS3 自己的 L2 主網已經上線穩定運營了一段時間了,我想從 DevOps 的角度和大家分享一下如何運行自己的 L2 網絡的故事,以及如何和雲原生 Kubernetes 集成。
文中提到的各種 chart 已經在 rss3-network org 上開源
https://github.com/RSS3-Network/helm-charts
Optimism Node Architecture#
用一張圖說明如下
Optimism (以下簡稱 OP) 的 L2 Network,主要是由 op-geth
和 op-node
組成。和 L1 節點相同的是, geth 仍然用於存儲數據和提供 RPC 響應;不同的是,由 op-node 來構建整個去中心化的網絡,和決定數據的同步。
Cloudnative upgrade#
可能是我孤陋寡聞,也不清楚其他家的 L2 RPC 的雲原生部署方式,並且 github 上發現其他的基於 op 的 helm chart 都不是很好用,所以就開始自己編寫了一套 VSL 的 chart 圖表 (理論上可以用於其他 L2 鏈的部署,但是未經任何測試)
Q1 security auth with JWT#
op-node 和 op-geth 必須是 1 <-> 1 對應的模式,所以採用了 Kubernetes Sidecar 模式,它們作為一個 RPC Pod 的兩個 container,共用一個 network namespace,可以通過 127.0.0.1 進行互相訪問。
在 op-geth 的環境變量裡,有一個 GETH_AUTHRPC_JWTSECRET
,相應的在 op-node 的環境變量裡有一個 OP_NODE_L2_ENGINE_AUTH
需要配置,這兩個容器通過一個相同的 jwt secret 進行通信。
上面已經講到,使得這個 auth endpoint 永遠在 127.0.0.1,端口也只開放 localhost 的來解決這個問題。不僅如此, jwt token 的值的管理也是一個問題,因此我們採用了 initContainer 的命令 openssl rand -hex 32 > {{ .Values.jwt.path }}
來在每一次 Pod 創建時隨機生成這個問題,這樣不僅做到了 network namespace 上的安全,也做到了 secret 上的隨機安全。
Q2 idempotent op-node peer id#
在 Q1 中講到了,我們可以用 openssl rand 命令,在每個 pod 裡都隨機生成一串隨機的字符串作為 jwt secret,這使得 op-geth 和 op-node 的通信更加安全。
然而,op-node 有一個 peer id,其他的節點可以通過 固定的 OP_NODE_P2P_STATIC
來設置初始同步節點的地址。作為節點的運營方,自然是需要給每個節點一個固定的 peer id 的。
做到完全隨機很容易,但是做到幂等,又不增加複雜度也是一個問題。經過研究發現,只要 op-node 的環境變量 OP_NODE_P2P_PRIV_PATH
不變,那麼 peer id 就不會變。問題從固定一個 peer id,變成固定一個文件的內容了。
自然的我們可以通過寫一大串環境變量,例如 POD_PRIV_0 到 POD_PRIV_N 來解決問題。但是這不優雅,沒有辦法快速的創建一個新的 pod,每次都需要先新建一個環境變量,並且會被注入到每一個容器,因為 statefulset replicas 沒有辦法對單獨的 pod 進行獨立的設置。
幸運的是, openssl 支持傳入參數作為隨機數種子,那麼我們只要保證隨機數種子相同,生成出來的文件內容也會相同,自然就能做到幂等了。那麼在一個 statefulset 裡,有什麼東西是每一個 pod 不同的呢,沒錯就是 pod 本身的信息, pod 的 name 和 namespace 等。於是我們就可以通過 pod 的信息,配合 kubernetes downward api,生成一個幂等的 seed
部分配置如下
- name: generate-key
image: openquantumsafe/openssl3
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: P2P_ROOT_KEY
valueFrom:
secretKeyRef:
key: {{ .Values.node.p2p.generateSecretKey }}
name: {{ .Values.node.p2p.generateSecretName }}
- name: P2P_GENERATE_KEY
value: $(P2P_ROOT_KEY)-$(POD_NAMESPACE)-$(POD_NAME)
command: ["/bin/sh", "-c"]
args:
- |
echo ${P2P_GENERATE_KEY} | openssl sha256 -binary | xxd -p -c 32 > {{ .Values.node.p2p.privateKeyPath }};
openssl rand -hex 32 > {{ .Values.jwt.path }};
這裡引入了一個新的環境變量,P2P_ROOT_KEY
,是為了保證如果有多個集群,多個相同的環境下, peer id 不會衝突
好了,經過如此操作,我們就能保證,我們每一個 L2 RPC 的 Pod, Peer id 都是唯一,並且調度 Pod 不會導致 id 變化。
Q3 HA sequencer#
第三個問題,由於 L2 網絡裡只能有一個活躍的 sequencer 負責出塊,所以整個 sequencer 的高可用和出塊設置也是一個比較頭疼的問題。
這便是 VSL-Reconcile的由來。
首先,我們會部署多個開啟 sequencer 和 admin-api 的 sequencer 節點,但是默認都是處於停止出塊的狀態。然後將由 reconcile 決定哪一個節點作為當前的出塊節點,並且把 sequencer 域名的流量切到出塊節點。
這一部分,我參考了 Vault HA 的模式
Vault 是一個 secret manager,高可用模式下,只有一個實例是 active 的,其他的都是 standby。因此我們的 sequencer 節點也是如此。
參考 Vault 的 Kubernetes 服務發現模式,我們也給 sequencer pod 打 label 來控制 pod 的調度和流量切換。
首先,我們會給所有的 sequencer pod 打上 vsl.rss3.io/synced=false
的標籤,這意味著區塊同步沒有跟上進度。
然後會檢查目前區塊的同步進度,把標籤更改成 vsl.rss3.io/synced=true
並且,service 只會選擇 vsl.rss3.io/synced
為 true
的 Pod 作為負載,這樣使得由於 RPC 請求流量過高,需要增加 Pod 副本數量時,新創建的 Pod 不會被放入請求中,因為它們還沒有同步完畢。
然後我們會挑選所有 vsl.rss3.io/synced=true
的 sequencer Pod,打上 vsl.rss3.io/active=false
的 label,這個標記意味著,這個 Pod 可以成為出塊節點。
當 reconcile 開啟某一個節點的出塊功能後, vsl.rss3.io/active
會變成 true
, sequencer 域名的 loadbalancer 也帶有 label selector vsl.rss3.io/active=true
,這樣無論哪一個 Pod 作為當前的出塊節點,都能夠正確、自動地把流量路由到 Pod。