一個人如何完成一家創業公司的技術架構?

作者 | Anthony N. Simon

譯者 | Sambodhi

策劃 | 田曉旭

這是一篇長篇闊論的文章,是關於我使用 SaaS 來運行設置的詳細介紹,文章會涉及到多方面的內容,包括負載均衡、cron 作業監控、訂閱和支付等等。

雖然文章的標題聽起來有些誇張,但我想澄清的是,我們正在討論的是一家壓力不大的個人公司,這家公司是我在德國經營的。自己花錢買的,我很喜歡慢慢來。如果我說 “科技創業”,那大概和大多數人想象的不一樣。

我不能在沒有大量的開源軟件和管理服務的情況下做到這一點。我覺得自己就是站在巨人的肩膀上,他們在我之前做過那麼多艱苦的工作,我非常感謝他們。

從背景角度來說,我經營的是個人 SaaS,這是我發表的一篇關於我所使用的 技術棧 的文章的詳細介紹。在聽從我建議之前,先考慮一下你的情況。在技術選擇上,你的背景很重要,不需要什麼聖盃。

我在 AWS 上使用了 Kubernetes,但是不要陷入需要它的誤區。經過一直非常耐心的團隊的指導,我花了幾年的時間學習了這些工具。因爲這是我最擅長的,所以我的工作效率非常高,並且我能將集中精力在運輸物品上。你們的目標可能不一樣。

閒話少敘,言歸正題。

1 整體架構

基礎設施可以同時處理多個項目,但是爲了演示,我將使用 Panelbear,我最近的 SaaS,作爲此類設置的一個實例。

Panelbear 中的瀏覽器計時圖表,這是我將在本教程中使用的一個示例項目。

就技術而言,該 SaaS 每秒處理來自世界各地的大量請求,並以高效的格式存儲數據,實現實時查詢。

就業務而言,它仍處於起步階段(我是半年前推出的),但它的發展比我預期的要快,特別是我最初爲自己創建的 Django 應用,它是在一個小的虛擬專用服務器上使用 SQLite。對我當時的目標而言,這是非常有效的,而且我可能已經將這一模式推進了很遠。

但是,我變得越來越沮喪,不得不重新使用許多我已經習慣了的工具:零停機部署、自動縮放、健康檢查、自動 DNS / TLS / ingress 規則等等。Kubernetes 寵壞了我,讓我習慣於在保持控制力和靈活性的情況下處理更高級抽象。

快進六個月,經歷了幾次迭代,雖然我目前的設置仍然是 Django 的單體版本,我現在將 Postgres 用作應用數據庫,ClickHouse 用作分析數據,Redis 用作緩存。同時使用 Celery 來調度任務,使用自定義事件隊列來緩衝寫操作。這其中大部分都在託管的 Kubernetes 集羣上運行。

架構概述

聽起來似乎很複雜,但它實際上是老式的單體架構,運行在 Kubernetes 上。如果把 Django 換成 Rails 或 Laravel,你就知道我在說什麼了。令人感興趣的是,如何將所有的東西粘合在一起並自動執行:自動縮放、入口、TLS 證書、故障轉移、日誌、監控,等等。

值得一提的是,我在多個項目中都使用了這種設置,它幫助我降低了成本,而且很容易進行實驗(編寫 Dockerfile 和 git push)。因爲經常有人問我這樣一個問題:和你想的相反,我實際上花了很少的時間去管理基礎設施,通常每個月要花大概 0~2 小時。大部分時間都用來開發功能、做客戶支持,以及拓展業務。

話又說回來,這些工具我都用了好幾年了,都很熟悉。雖然我的設置對它們的能力來說很簡單,但我的日常工作卻花了很多年才做到這一點。因此,我不會說這就是 “陽光和玫瑰”。

不知道誰先說了這句話,但我這樣對朋友們說:“Kubernetes 讓簡單的東西變得複雜,但也讓複雜的東西變得簡單”。

2 自動 DNS、SSL 和負載均衡

既然你已經瞭解了我在 AWS 上託管的 Kubernetes 集羣,並且在其中運行了各種項目,那麼讓我們進入本文的第一站:如何將流量引入集羣。

我的集羣是在一個私有網絡中,因此你無法從公共互聯網中直接訪問。有幾個部分可以控制集羣的訪問和負載均衡流量。

基本上,我讓 Cloudflare 將所有流量代理到 NLB(AWS L4 Network Load Balancer,網絡負載均衡器)。該負載均衡器是公共互聯網和我的私有網絡之間的橋樑。當收到請求後,它將轉發給其中一個 Kubernetes 集羣節點。這些節點分佈在 AWS 中多個可用性區域的私有子網中。所有這些都是順便處理的,但以後還會有更多。

流量被緩存在邊緣,或者轉發到我運營的 AWS 區域中

ingress-nginx 就是這樣做的:“Kubernetes 如何知道該將請求轉發到哪個服務?” 簡單地說,它是一個 NGINX 集羣,由 Kubernetes 管理,是集羣內所有流量的入口。

在將請求發送到相應的應用程序容器之前,NIGIX 適用速度限制和其他流量形成規則。就 Panelbear 而言,應用容器是由 Uvicorn 服務的 Django。

這種方法與傳統的 nginx/gunicorn/Django 的 VPS 方式沒有什麼不同,它帶來了橫向擴展和自動設置 CDN 的優點。它還可以 “一次設置就忘記”,在 Terraform/Kubernetes 之間主要有一些文件,由所有已部署項目共享。

在部署新項目時,入口配置基本上只有 20 行代碼,就像這樣:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
namespace: example
name: example-api
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/limit-rpm: "5000"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "true"
spec:
tls:
- hosts:
- api.example.com
secretName: example-api-tls
rules:
- host: api.example.com
http:
paths:
- path: /
backend:
serviceName: example-api
servicePort: http

這些註釋描述了我想要一個 DNS 記錄,流量由 Cloudflare 代理,通過 Letsencrypt 獲取 TLS 證書,並且應該根據 IP 限制每分鐘的請求率,然後再將請求轉發給我的應用。

Kubernetes 負責讓 infra 中的這些改變反映出期望的狀態。儘管有點兒囉嗦,但在實踐中效果還不錯。

3 自動部署和回滾

推送新提交時發生的操作鏈

無論何時我想要掌握一個項目,它都會在 GitHub Actions 上啓動一個 CI 管道。此管道運行一些代碼庫檢查和端到端測試(使用 Docker compose 來設置整個環境),這些檢查通過後,將創建一個新的 Docker 鏡像,並將其推送到 ECR(AWS 中的 Docker 註冊表)。

就應用倉庫而言,新版本的應用已經過測試,可以作爲 Docker 鏡像部署。

panelbear/panelbear-webserver:6a54bb3

“那麼接下來呢?Docker 有新的鏡像,但是還沒有部署?” Kubernetes 集羣有一個叫做 Flux 的組件,可以自動保存集羣中當前運行的內容以及我的應用最新的鏡像同步。

在我的基礎設施單體倉庫中,Flux 自動跟蹤新版本

Flux 會在 Docker 的新鏡像可用時自動觸發增量推出,並將這些操作記錄到 “基礎設施單體倉庫” 中。

我希望有一個版本控制的基礎設施,這樣每當我在 Terraform 和 Kubernetes 之間的這個倉庫中有新的提交時,它們就可以對 AWS、Cloudflare 和其他服務進行必要的修改,使我的倉庫狀態和部署的內容保持一致。

其所有版本都受版本控制,並且每次部署都有線性歷史。這就是說,多年來,由於我沒有在一些晦澀難懂的用戶界面上通過點擊配置神奇的設置,所以我需要記住的東西更少。

此單體倉庫可作爲可部署文檔,稍後將詳細討論。

4 任其崩潰

幾年前,我用 Actor 併發模型在公司的多個項目中工作,並愛上了其生態系統中的許多想法。一發不可收拾,我很快開始閱讀關於 Erlang 的書,以及它所闡述的讓事情崩潰的哲學。

也許我已經把這個想法做了很多擴展,但是在 Kubernetes 裏,我喜歡使用存活探針(liveliness probes)和自動重啓來達到同樣的效果。

摘自 Kubernetes 文檔:“kubelet 使用存活探針來知道何時重啓容器。例如,存活探針可以捕獲到一個死鎖,即應用程序正在運行,但無法取得進展。在這樣的狀態下重啓容器有助於使應用更可用,儘管有 bug。”

在實踐中,這對我來說很管用。容器和節點本來就是來來往往的,而 Kubernetes 則在 “治療” 不健康的 pod(更像是殺掉)的同時,優雅地將流量轉移到健康的 pod 上。殘忍,但有效。

5 橫向自動縮放

根據 CPU / 內存使用情況,我的應用容器會自動縮放。Kubernetes 將盡可能多的工作負載打包到每個節點上,以便最大限度地利用它。

如果集羣中每個節點的 pod 過多,它將自動生成更多的服務器,以增加集羣容量並減輕負載。類似地,當事情不多的時候,它就會縮減規模。

以下是 Horizontal Pod Autoscaler 的樣子:

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: panelbear-api
namespace: panelbear
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: panelbear-api
minReplicas: 2
maxReplicas: 8
targetCPUUtilizationPercentage: 50

在本例中,它會根據 CPU 的使用情況自動調整panelbear-api pods的數量,從 2 個副本開始,但上限爲 8 個。

6CDN 緩存的靜態資產

在爲我的應用定義入口規則時,註釋cloudflare-proxied: "true"就是告訴 Kubernetes 我要將 Cloudflare 用作 DNS,並通過其 CDN 和 DDoS 保護來代理所有請求。

從此以後,使用它將變得非常簡單。在應用中,我只需設置標準的 HTTP 緩存頭來指定哪些請求可以被緩存以及緩存多長時間。

# Cache this response for 5 minutes
response["Cache-Control"] = "public, max-age=300"

在邊緣服務器上,Cloudflare 將使用這些響應頭來控制緩存行爲。對這種簡單的設置來說,效果非常好。

通過 Whitenoise,我可以從應用容器直接爲靜態文件提供服務,因此可以避免每次部署時將靜態文件上傳到 Nginx/Cloudfront/S3。迄今爲止,它運行得很好,並且 CDN 在填充大部分請求時將對它們進行緩存。其性能非常出色,並且使事情更加簡單。

在一些靜態網站上,我也使用 NextJS,例如 Panelbear 的登陸頁面。通過 Cloudfront/S3 甚至 Netlify 或 Vercel,我可以爲它提供服務,但是隻需將其作爲集羣中的一個容器運行,當請求靜態資產時,Cloudflare 可以輕鬆地緩存它們。對於我來說,這樣做的額外成本爲零,並且我可以重複地使用所有的工具來部署、日誌記錄和監控。

7 應用數據緩存

除了靜態文件緩存外,還有應用數據緩存(例如重型計算結果、Django 模型、限速計數器等)。

我利用了內存中的緩存文檔置換機制 將頻繁訪問的對象保存在內存中,並且沒有網絡調用(純 Python,不涉及 Redis),這對我有好處。

然而,大多數端點只是在集羣中使用 Redis 來緩存。其速度仍然很快,並且緩存的數據可以被所有的 Django 實例共享,即使在重新部署之後,當內存中的緩存被刪除時,這些數據可以可以被共享。

下面是一個實際例子:

我的定價計劃是基於每月的事件分析。爲實現這一目標,需要進行某種計量,以瞭解在當前的賬單期間消耗了多少事件,並實施限制。但是,我不會在客戶超過限額後立即中斷服務。取而代之的是,將自動發送一封 “耗盡容量” 的電子郵件,並在 API 開始拒絕新數據之前爲客戶提供寬限期。

這樣客戶就有足夠的時間在確保數據不丟失的情況下決定升級對他們是否有意義。舉例來說,在流量高峯期,如果他們的內容被病毒式傳播,或者他們只享受週末,而不去查閱郵件。若客戶決定繼續使用目前的計劃而不進行升級,則不會有任何損失,且在使用量回到計劃限制範圍後,一切將恢復正常。

因此,爲了實現這一功能,我使用了一個函數,應用了上面的規則,它需要多次調用數據庫和 ClickHouse,但是會緩存 15 分鐘,以避免在每次請求時重新計算它們。這樣做很好,也很簡單。值得注意的是:計劃變更時緩存將失效,否則升級將在 15 分鐘後生效。

@cache(ttl=60 * 15)
def has_enough_capacity(site: Site) -> bool:
"""
Returns True if a Site has enough capacity to accept incoming events,
or False if it already went over the plan limits, and the grace period is over.
"""

8 單個端點速率限制

儘管我在 Kubernetes 的 nginx-ingress 上執行全局速率限制,但是有時候我想要更具體地限制每個端點 / 方法。

爲了實現這一點,我使用了優秀的 Django Ratelimit 庫爲每個 Django 視圖輕鬆聲明限制。其配置爲使用 Redis 作爲後端,以跟蹤向每個端點發出請求的客戶端(它存儲的是基於客戶端密鑰的哈希值,而不是基於 IP)。

例如:

class MySensitiveActionView(RatelimitMixin, LoginRequiredMixin):
ratelimit_key = "user_or_ip"
ratelimit_rate = "5/m"
ratelimit_method = "POST"
ratelimit_block = True
def get():
...
def post():
...

在上面的例子中,如果客戶端每分鐘試圖向此特定端點發送 5 次以上 POST,則會以HTTP 429 Too Many Requests狀態碼拒絕後續調用。

當速率受限時,會收到友好的錯誤消息

9 應用管理

Django 免費爲我所有的模型提供了一個管理面板。它是內置的,而且對於隨時檢查客戶支持工作的數據非常方便。

Django 的內置管理面板對於隨時提供客戶支持非常有用

在用戶界面上,我添加了動作來幫助我管理事情。例如,阻止對可疑賬戶的訪問,發送公告郵件,以及請求批准完全刪除賬戶(首先是軟刪除,72 小時後徹底銷燬)。

安全性方面:只有員工用戶能夠訪問面板(我),爲提高安全性,我打算在所有賬戶上添加 2FA。

另外,每一次用戶登錄,我都會自動將包含新會話詳情的安全郵件發送到該賬戶郵箱。我將在每次新登陸時發送,但將來我可能會更改此操作,以跳過已知設備。它並非很 “MVP 的功能”,但是我關注安全性,並且添加它也不復雜。最起碼如果有人登錄了我的賬戶,我會收到警告。

當然,對應用的強化內容遠不止這些,但這不在本文的討論範疇。

登陸時可能收到的安全活動電子郵件示例

10 運行計劃作業

另外一個有趣的用例是,我在 SaaS 中運行了許多不同的計劃工作。例如,爲我的客戶生成每日報告,每 15 分鐘計算一次使用情況統計,給員工發郵件(我每天都會收到包含最重要指標的郵件)等等。

這個設置實際上很簡單,我在集羣中只運行了幾個 Celery worker 和一個 Celery beat 調度器。它們被配置爲將 Redis 用作任務隊列。我花了一個下午的時間設置了一次,幸運的是,到目前爲止,我還沒有遇到任何問題。

當計劃任務未按預期運行時,我希望通過 SMS/Slack/Email 獲得通知。例如,當每週報告任務被卡住或明顯延遲時。爲此,我使用了 Healthchecks.io,但是你也可以看看 Cronitor 和 CronHub,因爲我也聽到了一些關於它們的好消息。

來自 Healthchecks.io 的 cron 作業監視指示板

我編寫了一小段 Python 代碼,對其 API 進行抽象,以自動創建監視程序和狀態 ping:

def some_hourly_job():
# Task logic
...
# Ping monitoring service once task completes
TaskMonitor(
,
expected_schedule=timedelta(hours=1),
grace_period=timedelta(hours=2),
).ping()

11 應用配置

全部應用都是通過環境變量來配置的,雖然已經過時,但是具有良好的可移植性和支持性。舉例來說,在 Djangosettings.py文件中,我會爲默認值設置變量。

INVITE_ONLY = env.str("INVITE_ONLY", default=False)

像下面這樣在代碼中的任意位置使用:

from django.conf import settings
# If invite-only, then disable account creation endpoints
if settings.INVITE_ONLY:
...

在 Kubernetesconfigmap中,我可以覆蓋以下環境變量:

apiVersion: v1
kind: ConfigMap
metadata:
namespace: panelbear
name: panelbear-webserver-config
data:
INVITE_ONLY: "True"
DEFAULT_FROM_EMAIL: "The Panelbear Team <support@panelbear.com>"
SESSION_COOKIE_SECURE: "True"
SECURE_HSTS_PRELOAD: "True"
SECURE_SSL_REDIRECT: "True"

12 保守祕密

處理祕密的方式非常有趣,我想把它們和其他配置文件一起提交到我的基礎設施倉庫,但祕密應該被加密。

爲了實現這個目標,我在 Kubernetes 中使用了 kubeseal。這個組件使用非對稱加密技術對我的祕密進行加密,並且只有獲得授權可以訪問解密密鑰的集羣才能對其進行解密。

舉例來說,你可能會在我的基礎設施倉庫中發現以下內容:

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: panelbear-secrets
namespace: panelbear
spec:
encryptedData:
DATABASE_CONN_URL: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq...
SESSION_COOKIE_SECRET: oi7ySY1ZA9rO43cGDEq+ygByri4OJBlK...

集羣將自動解密祕密,並將其作爲環境變量傳遞給相應的容器:

DATABASE_CONN_URL='postgres://user:pass@my-rds-db:5432/db'
SESSION_COOKIE_SECRET='this-is-supposed-to-be-very-secret'

爲了在集羣中保護祕密,我使用 AWS 通過 KMS 管理的加密密鑰定期輪換。當創建 Kubernetes 集羣時,這是一個單獨的設置,並且可以完全管理。

就操作而言,這意味着我將祕密作爲環境變量寫入 Kubernetes manifests,然後運行一個命令對 Kubernetes manifests 進行加密,然後在提交前推送更改。

只需幾秒鐘就可以部署祕密,在運行我的容器前,集羣會負責自動解密。

13 關係數據:Postgres

爲了進行實驗,我在集羣內運行一個普通的 Postgres 容器,以及一個每天備份到 S3 的 Kubernetes cronjob。這樣可以幫助我節省開支,而且對於新手來說,也很簡單。

不過,隨着 Panelbear 等項目的發展,我會把數據庫從集羣中轉移到 RDS,讓 AWS 負責加密備份、安全更新以及所有其他無聊的事情。

爲提高安全性,AWS 管理的數據庫仍在私有網絡中部署,因此不能通過公共互聯網訪問。

14 列式數據:ClickHouse

在 Panelbear 中,我依靠 ClickHouse 有效地存儲分析數據和(軟)實時查詢。它是一種神奇的列式數據庫,其速度驚人,並且當你的數據結構化得很好時,你可以獲得高壓縮比(更小的存儲成本 = 更高的利潤)。

當前,我在 Kubernetes 集羣中自行託管一個 ClickHouse 實例。我用一個由 AWS 管理的帶有加密卷密鑰的 StatefulSet。我有一個 Kubernetes CronJob,它定期以高效列式格式將所有數據備份到 S3. 對於災難恢復,我有幾個腳本用於在 S3 中手動備份和恢復數據。

直到現在,ClickHouse 仍然堅不可摧,它是一款令人印象深刻的軟件。當我開始使用 SaaS 時,這是我唯一不熟悉的工具,但是由於他們的文檔,我能夠很快上手運行。

如果想要擠出更多的性能(例如,優化字段類型以獲得更好的壓縮、預計算物化表以及優化實例類型), 我認爲這是一個容易實現的方法,但是現在它已經足夠好了。

15 基於 DNS 的服務發現

除了 Django 之外,我還爲 Redis、ClickHouse、NextJS 等運行容器。這些容器必須以某種方式相互通信,而這是通過 Kubernetes 中內置的服務發現實現的。

這很簡單。我爲容器定義了一個服務資源,Kubernetes 會自動管理集羣內的 DNS 記錄,並將流量路由到相應的服務。

舉例來說,給定集羣中公開的 Redis 服務:

apiVersion: v1
kind: Service
metadata:
name: redis
namespace: weekend-project
labels:
app: redis
spec:
type: ClusterIP
ports:
- port: 6379
selector:
app: redis

通過以下 URL,我可以從集羣的任何地方訪問這個 Redis 實例:

redis://redis.weekend-project.svc.cluster:6379

注意,URL 包含服務名稱和項目名稱空間。這樣,你的所有集羣服務就可以輕鬆地彼此通信,無論它們運行在集羣的哪個位置。

例如,下面是如何通過環境變量配置 Django 在集羣內使用 Redis:

apiVersion: v1
kind: ConfigMap
metadata:
name: panelbear-config
namespace: panelbear
data:
CACHE_URL: "redis://redis.panelbear.svc.cluster:6379/0"
ENV: "production"
...

即使容器在自動縮放期間跨節點移動, Kubernetes 也會自動使 DNS 記錄與正常 pod 保持同步。這個背後的工作方式很有趣,但是超出了本文的討論範圍。

16 版本控制的基礎設施

我想要的是版本控制的、可複製的基礎設施,可以使用一些簡單的命令來創建和銷燬它。

爲了實現這一點,我在一個單體倉庫中使用 Docker、Terraform 和 Kubernetes manifests,包含了所有的基礎設施,甚至跨多個項目。我還將爲每個應用 / 項目使用一個單獨的 git repo,但是這段代碼並不知道它將在什麼環境中運行。

如果你熟悉 12-Factor,這種分離可能會讓你想起一兩件事情。從本質上講,我的應用並不知道它將要運行的具體基礎設施,而是通過環境變量來配置。

通過在 git repo 中描述我的基礎設施,我就不需要在一些晦澀的用戶界面中跟蹤每一個小資源和配置設置。這樣,當災難恢復時,我可以使用一條命令來恢復我的整個棧。

下面是一個文件夾結構的例子,你可以在下文的單體倉庫上找到:

# Cloud resources
terraform/
aws/
rds.tf
ecr.tf
eks.tf
lambda.tf
s3.tf
roles.tf
vpc.tf
cloudflare/
projects.tf
# Kubernetes manifests
manifests/
cluster/
ingress-nginx/
external-dns/
certmanager/
monitoring/
apps/
panelbear/
webserver.yaml
celery-scheduler.yaml
celery-workers.yaml
secrets.encrypted.yaml
ingress.yaml
redis.yaml
clickhouse.yaml
another-saas/
my-weekend-project/
some-ghost-blog/
# Python scripts for disaster recovery, and CI
tasks/
...
# In case of a fire, some help for future me
README.md
DISASTER.md
TROUBLESHOOTING.md

這種設置的另一個好處是,所有的移動件都在同一位置描述。我可以配置和管理可重用的組件,比如集中式日誌、應用監控和加密祕密等等。

17 用於雲資源的 Terraform

我使用 Terraform 來管理大部分的基礎雲資源。它有助於我記錄和跟蹤構成基礎設施的資源和配置。如果發生災難恢復,我可以使用一個命令來啓動和回滾資源。

舉例來說,這裏有一個 Terraform 文件,用於爲 30 天后過期的加密備份創建私有 S3 存儲桶:

resource "aws_s3_bucket" "panelbear_app" {
bucket = "panelbear-app"
acl    = "private"
tags = {
Name        = "panelbear-app"
Environment = "production"
}
lifecycle_rule {
id      = "backups"
enabled = true
prefix  = "backups/"
expiration {
days = 30
}
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm     = "AES256"
}
}
}
}

18 用於 App 部署的 Kubernetes manifest

我所有的 Kubernetes manifests 也被描述在基礎設施單體倉庫的 YAML 文件中。我把它們分成了兩個目錄:clusterapps

我在cluster羣目錄中描述了所有集羣範圍內的服務和配置,比如 nginx-ingress,加密祕密,prometheus scrapers 等等。基本上是可重用位。

而在apps目錄中,每個項目都包含一個命名空間,描述了部署它所需要的內容(入口規則、部署、祕密、卷等)。

Kubernetes 最酷的功能之一就是,你可以自定義棧中的任何東西。例如,如果我希望使用可調整大小的加密 SSD 卷,那麼可以在集羣中定義新的 “StorageClass”。Kubernetes 以及在這種情況下,AWS 會協調和實現神奇的東西。例如:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: encrypted-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
encrypted: "true"
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

既然我可以繼續將這種持久性存儲附加到任何部署中,Kubernetes 就會爲我管理請求的資源:

# Somewhere in the ClickHouse StatefulSet configuration
...
storageClassName: encrypted-ssd
resources:
requests:
storage: 250Gi
...

19 訂閱和支付

我用 Stripe Checkout 來保存所有工作,包括處理付款、創建結賬屏幕、處理信用卡 3D 安全要求,甚至客戶賬單門戶。

我無法接觸到支付信息本身,這極大地減輕了我的負擔,使我能夠集中精力放在我的產品上,而非信用卡處理和反欺詐等高度敏感的問題上。

Panelbear 中的客戶計費門戶示例

我們只需要創建一個新的客戶會話,然後將客戶重定向到 Stripe 的託管頁面。接着我監聽 webhook,瞭解客戶是否升級 / 降級 / 取消,並相應地更新數據庫。

這其中肯定有一些重要的內容,比如確認 webhook 確實來自 Stripe(你必須用祕密驗證請求籤名),但是 Stripe 的文檔非常好地涵蓋了所有的內容。

只有很少的計劃,所以很容易在代碼庫中管理它們。基本上我是這麼想的:

# Plan constants
FREE = Plan(
code='free',
display_name='Free Plan',
features={'abc', 'xyz'},
monthly_usage_limit=5e3,
max_alerts=1,
stripe_price_id='...',
)
BASIC = Plan(
code='basic',
display_name='Basic Plan',
features={'abc', 'xyz'},
monthly_usage_limit=50e3,
max_alerts=5,
stripe_price_id='...',
)
PREMIUM = Plan(
code='premium',
display_name='Premium Plan',
features={'abc', 'xyz', 'special-feature'},
monthly_usage_limit=250e3,
max_alerts=25,
stripe_price_id='...',
)
# Helpers for easy access
ALL_PLANS = [FREE, BASIC, PREMIUM]
PLANS_BY_CODE = {p.code: p for p in ALL_PLANS}

之後,我可以將它用於任何 API 端點、cron 作業和管理任務,以確定那些限制 / 功能適合特定的客戶。在BillingProfile模型中,給定客戶的當前計劃是稱爲plan_code的列。由於我計劃在某些時候添加組織 / 團隊,因此我將把BillingProfile遷移到賬戶所有者 / 管理用戶,所以我將用戶和賬單信息分離。

如果你在一家電商裏提供成千上萬的單品,那麼這個模式是不能被擴展的,但是對於我來說,這個模式非常有效,因爲 SaaS 通常只有幾個計劃。

20 日誌

無需使用任何日誌代理或類似工具來檢測代碼。只要將日誌記錄到 stdout,Kubernetes 就會自動收集,然後對日誌進行循環更新。還可以使用 FluentBit 自動地將這些日誌發送到像 Elasticsearch/Kibana 這樣的地方,但是爲了簡單起見,我還沒有這樣做。

我用 Stern 檢查日誌,這是用於 Kubernetes 的 一個小 CLI 工具,它可以非常輕鬆地跟蹤多個 pod 應用日誌。舉例來說,stern-ningress-nginx可以跟蹤我的 nginxpods 訪問日誌,甚至在多個節點之間進行跟蹤。

21 監控和警報

起初,我使用自託管的 Prometheus/Grafana 來自動監控集羣和應用指標。但是,我覺得自託管我的監控棧並不舒服,因爲如果集羣中發生了什麼問題,我的警報系統也會隨之癱瘓(不太好)。

如果說有什麼東西是絕對不能壞的,那就是你的監控系統,否則你基本上就是在沒有儀器的情況下飛行。這就是爲什麼我把監控 / 警報系統改爲託管服務(New Relic)。

所有我的服務都有一個 Prometheus 集成,能夠自動記錄和轉發指標到兼容的後端,如 Datadog、New Relic、Grafana Cloud 或自託管的 Prometheus 實例(我曾經做過)。爲了遷移到 New Relic,我需要做的就是使用他們的 Prometheus Docker 鏡像,然後關閉自託管的監控棧。

New Relic 儀表盤示例,包含最重要的統計數據摘要

使用 New Relic 的探針監測世界各地的正常運行時間

從自託管的 Grafana/Loki/Prometheus 棧遷移到 New Relic,減少了我的操作面。更重要的是,即使我的 AWS 區域宕機了,我仍然會收到警報。

你也許想知道我是如何從 Django 應用中公開指標的。在我的應用中,我利用了優秀的 django-prometheus 庫來簡單地註冊一個新的計數器 / 儀表。

from prometheus_client import Counter
EVENTS_WRITTEN = Counter(
"events_total",
"Total number of events written to the eventstore"
)
# We can increment the counter to record the number of events
# being written to the eventstore (ClickHouse)
EVENTS_WRITTEN.incr(count)

這會在服務器的/metrics端點中公開該指標和其他指標(僅在集羣內可訪問)。Prometheus 每分鐘都會自動抓取該端點,並將指標轉發給 New Relic。

由於 Prometheus 的集成,該指標會自動顯示在 New Relic 中

22 錯誤跟蹤

人人都認爲在他們的應用中沒有錯誤,除非開始錯誤跟蹤。異常太容易在日誌中丟失,或者更糟糕的是,你意識到了它,但由於缺乏上下文,無法重現問題。

用 Sentry 來彙總整個應用中的錯誤並通知我。檢測 Django 應用非常簡單:

SENTRY_DSN = env.str("SENTRY_DSN", default=None)
# Init Sentry if configured
if SENTRY_DSN:
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[DjangoIntegration(), RedisIntegration(), CeleryIntegration()],
# Do not send user PII data to Sentry
# See also inbound rules for special patterns
send_default_pii=False,
# Only sample a small amount of performance traces
traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.008),
)

這很有用,因爲它在異常發生時自動收集一堆上下文信息:

當發生異常情況時,Sentry 彙總並通知我

通過 Slack 的 #alerts 頻道,我可以集中所有的警報:宕機時間、cron 作業失敗、安全警報、性能下降、應用異常等等。這樣做非常好,因爲當多個服務在同一時間向我發出看似不相關的問題的警告時,我就能把問題關聯起來。

Slack 警報示例,圖爲 CDN 端點在澳大利亞悉尼宕機

23 資料收集與其他好處

在需要深入研究時,我還將使用諸如 cProfile 和 snakeviz 這樣的工具,以更好地瞭解有關應用性能的分配、調用次數和其他統計數據。這聽起來很花哨,但是它們是非常容易使用的工具,而且在過去幫助我找出各種問題,這些問題使我的儀表盤因看似不相關的代碼而變慢。

cProfile 和 snakeviz 是很好的工具,可以在本地對 Python 代碼進行配置文件。

在本地機器上,我還使用 Django Debug Toolbar 輕鬆地檢查視圖觸發的查詢,在開發期間預覽發送的電子郵件,以及其他一些好處。

Django 的 Debug 工具欄對於檢查本地開發和預覽事務性電子郵件非常有用

24 結語

如果你看到這裏,我希望你喜歡這篇文章。它最終比我最初計劃的要長很多,因爲有很多地方要涉及。

如果你還不熟悉這些工具,可以考慮先使用一個託管平臺,比如 Render 或 DigitalOcean 的 App Platform(無利益相關,只是聽說這兩個平臺很不錯)。它們可以幫助你把精力集中在產品上,而且還能得到我在本文提到的好處。

“你是不是什麼都用 Kubernetes?” 不是的,不同的項目有不同的需求。比如這個博客就是託管在 Vercel 上的。

有趣的是,我花在寫這篇文章上的時間比實際設置我所描述的一切還要多。9000 多字,幾周的工作斷斷續續,很顯然,我是個寫得慢的人。

話雖如此,我確實打算寫更多的後續文章,介紹一些具體的技巧和訣竅,並分享更多一路走來的經驗教訓。特別是作爲一名工程師,我有很多需要學習的地方。

作者介紹

Anthony N. Simon,Stylight 工程師,Panelbear 創始人。

原文鏈接

https://anthonynsimon.com/blog/one-man-saas-architecture/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/odjAsFc5kWEbU_LdrI-J2A