在現代軟體開發中,持續最佳化服務以提升使用者經驗至關重要。實驗設計提供了一種有效的方法,可以在最小化風險的前提下,驗證新功能和改進對使用者行為的影響。透過 A/B 測試等實驗方法,可以比較不同版本服務的表現,並根據資料驅動的洞察做出決策。服務網格技術的應用,使得流量切分和版本控制更加靈活,方便進行更精細的實驗控制。此外,Kubernetes Operator 模式為自定義資源管理和實驗佈署提供了更便捷的途徑,透過自定義資源定義(CRD)和控制器,可以簡化實驗流程,並提高實驗的可觀察性和可控性。
實驗設計在服務最佳化中的關鍵作用
在開發和最佳化服務的過程中,實驗設計扮演著至關重要的角色。與混沌測試和負載測試不同,實驗的目的不是發現服務架構和運作中的問題,而是找出改善使用者使用體驗的方法。實驗是一種長期對服務進行的變更,通常涉及使用者經驗的調整,其中一小部分使用者(例如1%的流量)會獲得稍微不同的體驗。透過比較對照組(未進行變更的組別)和實驗組(獲得不同體驗的組別)之間的差異,可以瞭解變更的影響,並決定是否繼續實驗或更廣泛地推廣這些變更。
實驗目標
在構建服務時,我們通常會設定一個目標。這個目標往往是提供有用的、易於使用且令客戶或使用者滿意的服務。但是,我們如何知道是否達到了這個目標?相較於觀察服務在混沌環境下的當機或在負載測試中表現出來的極限,瞭解使用者如何體驗我們的服務更為複雜。
傳統的方法包括調查問卷,使用者被詢問對當前服務的感受。雖然這有助於瞭解當前服務的表現,但很難用來預測未來變更的影響。就像效能迴歸一樣,最好能在變更全面推出之前瞭解其影響。這是任何實驗的主要目標:以對使用者經驗的最小影響來學習。
實驗的前提條件
就像小時候參加科學展覽一樣,每一個好的實驗都始於一個好的假設,這也是我們服務實驗的自然前提。我們需要對即將進行的變更有所猜測,並預測它將如何影響使用者經驗。
當然,為了了解對使用者經驗的影響,我們還需要能夠衡量使用者經驗。這些資料可以來自調查問卷,透過詢問使用者滿意度或淨推薦值等指標來收集。也可以來自與使用者行為相關的被動指標,例如使用者在網站上停留的時間或點選的頁面數量等。
一旦有了假設和衡量使用者經驗的方法,就可以開始實驗了。
設定實驗
設定實驗有兩種不同的方法,具體採用哪種方法取決於被測試的內容。可以將多種可能的體驗包含在單一服務中,也可以佈署兩個服務副本,並使用服務網格來引導流量。
第一種方法是將兩個版本的程式碼都納入釋出二進位制檔案中,並根據服務接收到的請求屬性在實驗和對照之間切換。可以使用HTTP頭、Cookie或查詢引數讓使用者明確選擇加入實驗,也可以根據請求的特徵(如源IP地址)隨機選擇使用者進行實驗。例如,可以選擇IP地址末位為1的使用者參與實驗。
import random
def select_user_for_experiment(user_ip):
# 簡單示例:根據使用者IP末位判斷是否參與實驗
if int(user_ip.split('.')[-1]) % 10 == 1:
return True
return False
# 示例用法
user_ip = "192.168.1.1"
if select_user_for_experiment(user_ip):
print("使用者被選入實驗組")
else:
print("使用者在對照組")
內容解密:
這段程式碼展示瞭如何根據使用者的IP地址末位數字來決定是否將其納入實驗組。函式select_user_for_experiment接收一個IP地址作為輸入,將其按.分割後取出最後一段,轉換為整數後檢查是否能被10整除餘1。如果是,則傳回True,表示該使用者被選入實驗組;否則傳回False,表示使用者留在對照組。
常見的實作實驗的方法是使用顯式的功能標記(Feature Flagging),使用者透過提供查詢引數或Cookie來選擇加入實驗。這是一種允許特定客戶試用新功能或展示新功能而不廣泛釋出的好方法。功能標記也可以用於在不穩定的情況下快速開啟或關閉功能。有許多開源專案,如Flagger,可以用來實作功能標記。
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title 服務最佳化中實驗設計的關鍵作用
package "Kubernetes Cluster" {
package "Control Plane" {
component [API Server] as api
component [Controller Manager] as cm
component [Scheduler] as sched
database [etcd] as etcd
}
package "Worker Nodes" {
component [Kubelet] as kubelet
component [Kube-proxy] as proxy
package "Pods" {
component [Container 1] as c1
component [Container 2] as c2
}
}
}
api --> etcd : 儲存狀態
api --> cm : 控制迴圈
api --> sched : 調度決策
api --> kubelet : 指令下達
kubelet --> c1
kubelet --> c2
proxy --> c1 : 網路代理
proxy --> c2
note right of api
核心 API 入口
所有操作經由此處
end note
@enduml
圖表翻譯: 此圖示展示了設定實驗的兩種主要方法之間的流程。首先判斷是否使用功能標記,如果使用,則根據請求引數決定是否開啟實驗功能;如果不使用,則根據使用者IP來選擇是否將其納入實驗組。最終根據不同的條件傳回對應的內容給使用者。
實驗設計與實施的最佳實踐
在軟體開發與服務營運的過程中,實驗設計扮演著至關重要的角色。適當的實驗設計能夠幫助開發團隊理解變更對使用者經驗的影響,並在廣泛推出前進行必要的調整與最佳化。
實驗實施的兩種主要方法
實驗的實施主要有兩種方法:將實驗程式碼與生產程式碼放在同一個二進位制檔案中,或是佈署多個不同版本的服務。
方法一:在同一個二進位制檔案中進行實驗
將實驗程式碼與控制程式碼放在同一個二進位制檔案中的優點是簡單易行,便於直接佈署到生產環境。然而,這種方法有兩個主要的缺點:
- 穩定性風險:如果實驗程式碼不穩定並導致當機,可能會影響生產環境中的流量。
- 更新困難:由於任何變更都與服務的完整版本釋出相關聯,因此更新實驗或推出新實驗的速度較慢。
方法二:佈署多個版本的服務
另一種方法是佈署兩個或多個不同版本的服務。其中,控制版本的生產服務接收大部分流量,而實驗版本的服務則接收一小部分流量。透過服務網格(Service Mesh)技術,可以將一小部分流量路由到實驗版本的服務。
這種方法的優點包括:
- 更高的靈活性:由於實驗版本與生產版本是獨立的,因此可以隨時佈署新版本的實驗或多個實驗版本,而不會影響大部分流量。
- 更好的穩定性:如果實驗程式碼開始失敗,服務網格可以快速將其從使用中移除,將對使用者的影響降至最低。
然而,這種方法需要更多的基礎設施建設,包括獨立的監控系統,以確保實驗版本的失敗不會被生產環境中的成功請求所掩蓋。
監控與反饋的重要性
在使用多個服務版本進行實驗時,獨立的監控系統至關重要。這不僅能幫助檢測實驗版本的失敗,還能提供足夠的上下文資訊,以區分生產環境與實驗環境的監控訊號。
Operator 模式的介紹與應用
Kubernetes 的一大優勢在於其可擴充套件性,允許操作人員透過 Operator 模式擴充套件核心 API。Operator 模式允許開發者封裝、佈署和維護與 Kubernetes API 緊密整合的應用程式,使得應用程式能夠原生地執行在 Kubernetes 環境中,並具備平滑升級、調解跨服務、自定義擴充套件和內嵌可觀察性等功能。
為什麼需要 Operator?
Operator 的出現主要是為瞭解決 Kubernetes 應用程式的管理和維護問題。透過 Operator,開發者可以將應用程式的知識和操作經驗嵌入到應用程式中,從而簡化應用程式的管理和維護工作。
Operator 的應用場景
Operator 可以用於各種場景,包括但不限於:
- 應用程式佈署和管理:Operator 可以自動化應用程式的佈署和管理工作,簡化操作流程。
- 平滑升級:Operator 可以幫助實作應用程式的平滑升級,減少升級過程中的中斷和風險。
- 自定義擴充套件:Operator 可以根據實際需求實作自定義的擴充套件策略,提高應用程式的彈性和可擴充套件性。
- 內嵌可觀察性:Operator 可以將可觀察性嵌入到應用程式中,提供更深入的洞察和分析能力。
Operator 關鍵元件
Operator Framework 本身是一個具有明確軟體開發工具包(SDK)、生命週期管理和發布工具的開源工具包。目前已經有許多專案圍繞 Operator 模式的概念進行開發,以使社群更容易開發 Operator。Kubernetes 社群中的 API Machinery SIG 成員贊助了 kubebuilder 的開發,為 Operator 的兩個主要元件:自定義資源定義(CRD)和控制器提供了一個基本的 SDK。在社群的支援下,kubebuilder 正被定位為所有 Operator 的基礎 SDK,其他專案如 KUDO、KubeOps 和 Kopf 也正在使用它。本章的範例將根據 kubebuilder 語法進行討論,但許多 Operator SDK 的概念都非常相似。
自定義資源定義(CRD)
在實際應用中,僅使用原生 Kubernetes 資源來定義複雜的應用程式依賴和資源往往會變得非常具有挑戰性。平台工程師通常需要建立複雜的 YAML 範本,並使用渲染管道和其他資源(如作業和初始化容器)來管理大型應用程式所需的自定義。但是,自定義資源定義(CRD)允許開發人員擴充套件 Kubernetes API,以提供新的資源型別,這些資源型別可以更好地代表應用程式的資源需求。
Kubernetes 允許使用 CustomResourceDefinition 介面動態註冊新資源,並自動為您指定的版本註冊新的 RESTful 資源路徑。與許多原生 Kubernetes 資源不同,CRD 可以獨立維護並在需要時更新。CRD 將在 spec 欄位下定義資源的規範,並具有 spec.scope 定義由 CRD 建立的自定義資源是否為名稱空間或叢集範圍的資源。
Kubernetes API 物件、資源、版本、群組和種類別
Kubernetes 中的物件是實際儲存在系統中的實體,用於表示叢集的狀態。物件本身是典型的 CRUD 操作在叢集中作用的目標。基本上,物件將是整個資源定義的狀態,例如 Pod 或 PersistentVolume。
Kubernetes 資源是 API 中的一個端點,代表特定種類別的物件集合。因此,Pod 資源將包含 Pod 物件的集合。可以在叢集中輕鬆檢視:
kubectl api-versions
NAME SHORTNAMES APIVERSION NAMESPACED KIND
bindings v1 true Binding
componentstatu... cs v1 false ComponentS...
configmaps cm v1 true ConfigMap
edited for space
mutatingwebhoo... admissionregistration... false MutatingWe...
validatingwebh... admissionregistration... false Validating...
customresource... crd,crds apiextensions.k8s.io/... false CustomReso...
apiservices apiregistration.k8s.i... false APIService
controllerrevi... apps/v1 true Controller...
daemonsets ds apps/v1 true DaemonSet
deployments deploy apps/v1 true Deployment
replicasets rs apps/v1 true ReplicaSet
statefulsets sts apps/v1 true StatefulSet
群組將相似關注點的物件聚集在一起。這種分組與版本控制結合,允許同一群組中的物件單獨管理和更新。群組在物件的 apiVersion 欄位中的 RESTful 路徑中表示。在 Kubernetes 中,核心群組(也稱為舊版)將位於 /api/ REST 路徑下。通常在 Pod 規範或 Deployment YAML 中看到 apiVersion 欄位中省略了基本路徑,如下所示:
kind: Deployment
apiVersion: apps/v1
metadata:
name: sample
spec:
selector:
matchLabels:
如同任何好的 API 一樣,Kubernetes API 是版本化的,並使用不同的 API 路徑支援多個版本。Kubernetes 版本控制有相關的指導方針,在版本控制自定義資源時也應遵循這些指導方針。API 也可能根據 API 的支援或穩定性分為不同的級別,因此經常會看到 Alpha、Beta 或 Stable API。在叢集中,例如,可能有 v1 和 v1beta1 用於相同的群組:
kubectl api-versions
---
- excerpt
autoscaling/v1
autoscaling/v2
autoscaling/v2beta1
autoscaling/v2beta2
通常,Kind 和 Resource 被用作相同的上下文;然而,資源是 Kind 的具體實作。通常存在直接的 Kind 到 Resource 的關係,例如定義 kind: Pod 規範時,將在叢集中建立 Pod 資源。有時存在一對多的關係,例如 Scale Kind,可以由不同的資源(如 Deployment 或 ReplicaSet)傳回。這被稱為子資源。
建立我們的 API
自定義資源定義(CRD)可以手動以 YAML 建立;然而,kubebuilder 和其他 Operator SDK 將根據提供的程式碼自動生成 API 定義。在 kubebuilder 中,可以在專案初始化後建立 API 的框架和所需的 Go 程式碼。要初始化專案,一旦滿足 kubebuilder 及其先決條件,就可以從包含專案檔案的新目錄中執行 init 命令:
$ kubebuilder init --domain platform.evillgenius.com --repo platform.evillgenius.com/platformapp --project-name=pe-app
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/[email protected]
go: downloading sigs.k8s.io/controller-runtime v0.14.1
go: downloading k8s.io/apimachinery v0.26.0
....................................................... removed for brevity ...
Update dependencies:
$ go mod tidy
go: downloading github.com/go-logr/zapr v1.2.3
go: downloading go.uber.org/zap v1.24.0
go: downloading github.com/onsi/ginkgo/v2 v2.6.0
go: downloading github.com/onsi/gomega v1.24.1
go: downloading gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f
go: downloading github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e
Next: define a resource with:
$ kubebuilder create api
這將建立一些基本檔案和預留的樣板程式碼:
$ tree
.
├── config
│ ├── default
│ │ ├── kustomization.yaml
程式碼解析
上述指令和程式碼建立了一個基本的 Operator 專案結構。下面是對這些步驟的詳細解析:
kubebuilder init:此命令初始化一個新的 Operator 專案。它會建立必要的目錄結構和檔案,包括config目錄、main.go檔案等。--domain、--repo和--project-name:這些引數用於指定專案的網域、Go 模組路徑和專案名稱。go get sigs.k8s.io/[email protected]:此命令下載controller-runtime套件,這是建立 Operator 所需的。go mod tidy:此命令整理 Go 模組的相依性。