Kubernetes 是一款開源軟體,你可以利用它大規模地部署和管理容器化應用程式。Kubernetes 管理 Amazon EC2 計算例項叢集,並在這些例項上執行容器以及執行部署、維護與擴充套件程序。藉助 Kubernetes,你可以在本地和雲上使用相同的工具集執行任何型別的容器化應用程式。AWS 利用可擴充套件並且高度可用的虛擬機器基礎設施、具有社群支援的服務整合,以及經過認證的 Kubernetes 託管服務 Amazon Elastic Kubernetes Service (EKS),大幅簡化了在雲上執行 Kubernetes 的過程。來自哥斯大黎加的軟體工程師 Anthony Najjar Simon(下文中均以作者代替)向我們分享了一個人如何經營運作一家公司。該公司建立在作者在德國的公寓裡,完全由自己出資。他主要介紹了在 AWS 上使用 Kubernetes,從負載平衡到 cron 作業監控,再到支付和訂閱,實現了一人公司的順利執行。基礎設施可以同時處理多個專案,但為了說明問題,作者使用 SaaS Panelbear 作為這種設定的實際示例。作者表示,從技術角度來看,這種 SaaS 每秒需要處理來自世界各地的大量請求,以高效的格式儲存資料,以便進行實時查詢;從業務角度來看,它還處於初級階段(六個月前才開始推進),但發展速度很快,在預期範圍內。然而,令人沮喪的是,作者不得不重新實現以前非常熟悉的工具:零停機部署、彈性伸縮、安全檢查、自動 DNS / TLS / ingress 規則等。作者以前使用 Kubernetes 來處理更高層級的抽象概念,同時進行監控和保持靈活性。六個月過去了,歷經幾次迭代,目前的設定仍然是 Django monolith。作者現在使用 Postgres 作為應用程式資料庫,ClickHouse 用於分析資料,Redis 用於快取。作者還將 Celery 用於預期任務,並使用自定義事件佇列緩衝資料寫入,並在一個託管 Kubernetes 叢集(EKS)上執行這些東西。上述內容聽起來可能很複雜,但實際上是一個在 Kubernetes 上執行的老式整體架構,並且用 Rails 或 Laravel 替換 Django。有趣的部分是如何將所有內容複合在一起並進行自動化,包括彈性伸縮、ingress、TLS 協議、失效轉移、日誌記錄、監視等。值得注意的是,作者在多個專案中使用了這個設定,這有助於降低成本並非常輕鬆地啟動實驗(編寫 Dockerfile 和 git push)。可能有人會問,這需要花費很多的時間,但實際上作者花很少的時間管理基礎設施,通常每月只需花費 2 個小時以內的時間。其餘大部分時間都花在開發特性、做客戶支援和發展業務上。作者經常告訴朋友的一句話是:「Kubernetes 讓簡單的東西變得複雜,但它也讓複雜的東西變得簡單。」作者在 AWS 上有一個託管 Kubernetes 叢集,並在其中執行了各種專案。接下來開始本教程的第一站:如何將流量引入叢集。該叢集在一個私有網路中,無法從公共網際網路直接訪問它。但是,Kubernetes 如何知道將請求轉發到哪個服務呢?這就是 ingress-nginx 的作用所在。簡而言之:它是一個由 Kubernetes 管理的 NGINX 叢集,是叢集內所有流量的入口點。NGINX 在將請求傳送到相應的 app 容器之前,會應用速率限制和其他流量整形規則。在 Panelbear 的例子中,app 容器是由 Uvicorn 提供服務的 Django。它與 VPS 方法中的 nginx/gunicorn/Django 沒有太大的不同,具有額外的橫向縮放優勢和自動 CDN 設定。大多數是 Terraform/Kubernetes 之間的一些檔案,所有部署的專案都共享它。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
就 application repo 而言,該 app 新版本已測試過,並準備作為 Docker 映象部署:panelbear/panelbear-webserver:6a54bb3
接下來怎麼做?有了新的 Docker 映象,但沒有部署?Kubernetes 叢集有一個叫做 flux 的元件,它會自動同步叢集中當前執行的內容和 app 的最新影像。Flux 自動跟蹤基礎架構 monorepo 中的新版本。
當有了新的 Docker 映象可用時,Flux 會自動觸發增量卷展欄(incremental rollout),並在 Infrastructure Monrepo 中記錄這些操作。可以將此 monorepo 視為可部署的文件,但稍後將詳細介紹。該 app 容器基於 CPU / 記憶體使用進行自動擴充套件。Kubernetes 嘗試在每個節點上打包儘可能多的工作負載,以充分利用它。如果叢集中每個節點有太多的 pod,則將自動生成更多的伺服器以增加叢集容量並減輕負載。類似地,當沒有太多事情發生時,它也會縮小。在本例中,它將根據 CPU 使用情況自動調整 panelbear api pod 的數量,從 2 個副本開始,但上限為 8 個。在為 app 定義 ingress 規則時,標註「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 快取請求的靜態資產是很容易的。這樣做沒有額外的成本,而且可以重用所有工具進行部署、日誌記錄和監視。除靜態檔案快取之外,作者還需要應用程式資料快取(如繁重計算的結果、Django 模型、速率限制計數器等)。作者的定價計劃基於每月的分析事件。為此,有必要進行某種計量,以瞭解在當前計費週期內消耗了多少事件,並強制執行限制。不過,作者不會在顧客超限時立即中斷服務。相反,系統會自動傳送一封容量耗盡的電子郵件,並在 API 開始拒絕新資料之前給客戶一個寬限期。因此,對於這個特性,有一個應用上述規則的函式,它需要對 DB 和 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.
"""
雖然作者在 Kubernetes 上的 nginx-ingress 強制執行全域性速率限制,但同時希望在每個端點 / 方法的基礎上實施更具體的限制。為此,作者使用 Django Ratelimit 庫來輕鬆地宣告每個 Django 檢視的限制,使用 Redis 作為後端來跟蹤向每個端點發出請求的客戶端(其儲存基於客戶端金鑰的雜湊,而不是基於 IP)。例如:在上面的示例中,如果客戶端試圖每分鐘向這個特定的端點 POST 超過 5 次,那麼後續的呼叫將使用 HTTP 429 Too Many Requests 狀態碼拒絕。Django 可以為所有模型免費提供了一個管理皮膚。它是內建的,非常方便用於檢查資料以進行客戶支援工作。作者新增了一些操作來管理來自 UI 的東西,比如阻止訪問可疑賬戶、傳送公告郵件等。安全方面:只有員工使用者可以訪問皮膚,併為所有賬戶計劃新增 2FA 作為額外安全保障。此外,每次使用者登入時,作者都會自動向帳戶的電子郵件傳送一封安全電子郵件,其中包含新會話的詳細資訊。現在作者在每次新登入時都會傳送它,但將來可能會更改它以跳過已知裝置。另一個有趣的用例是,作為 SaaS 的一部分,作者執行了許多不同的排程作業。這些工作包括為客戶生成每日報告、每 15 分鐘計算一次使用情況、傳送員工電子郵件等。這個設定實際上很簡單,只需要幾個 Celery workers 和一個 Celery beat scheduler 在叢集中執行。它們被配置為使用 Redis 作為任務佇列。當計劃任務未按預期執行時,作者希望透過 SMS/Slack/Email 獲得通知。例如,當每週報告任務被卡住或嚴重延遲時,可以使用 Healthchecks.io,但同時也檢查 Cronitor 和 CronHub。來自 Healthchecks.io 的 cron 作業監控儀表板。
為了抽象 API,作者寫了一個 Python 程式碼片段來自動建立監控器和狀態提示:def some_hourly_job():
# Task logic
...
# Ping monitoring service once task completes
TaskMonitor(
name="send_quota_depleted_email",
expected_schedule=timedelta(hours=1),
grace_period=timedelta(hours=2),
).ping()
所有應用程式都是透過環境變數配置的,雖然老式但很便攜,而且具有良好支援。例如,在 Django settings.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:
...
可以重寫 Kubernetes configmap 中的環境變數: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"
作者使用 Kubernetes 中的 kubeseal 元件,它使用非對稱加密來加密,只有授權訪問解密金鑰的叢集才能解密。如下程式碼所示:叢集將自動解密,並將其作為環境變數傳遞給相應的容器:DATABASE_CONN_URL='postgres://user:pass@my-rds-db:5432/db'
SESSION_COOKIE_SECRET='this-is-supposed-to-be-very-secret'
為了保護叢集中的隱私,作者透過 KMS 使用 AWS 管理的加密金鑰。在建立 Kubernetes 叢集時,這是一個單獨的設定,並且它是完全受管理的。對於實驗,作者在叢集中執行原版 Postgres 容器,並執行每日備份到 S3 的 Kubernetes cronjob。隨著專案進展,對於 Panelbear 等,作者將資料庫從叢集轉移到 RDS 中,讓 AWS 負責加密備份、安全更新等操作。為了增加安全性,AWS 管理的資料庫仍然部署在作者的專用網路中,因此它們無法透過公共網際網路訪問。作者依靠 ClickHouse 對 Panelbear 中的分析資料進行高效儲存和實時查詢。這是一個非常棒的列式資料庫,速度非常快,當將資料組織得很好時,你可以獲得高壓縮比(儲存成本越低 = 利潤率越高)。目前,作者在 Kubernetes 叢集中自託管了一個 ClickHouse 例項。作者有一個 Kubernetes CronJob,它定期地將所有資料以高效的列格式備份到 S3。在災難恢復(disaster recovery)的情況下,作者使用幾個指令碼來手動備份和恢復 S3 中的資料。除了 Django,作者還執行 Redis、ClickHouse、NextJS 等容器。這些容器必須以某種方式相互通訊,並透過 Kubernetes 中的內建服務發現(service discovery)來實現。很簡單:作者為容器定義了一個服務資源,Kubernetes 自動管理叢集中的 DNS 記錄,將流量路由到相應的服務。例如,給定叢集中公開的 Redis 服務:可以透過以下 URL 從叢集的任何位置訪問此 Redis 例項:redis://redis.weekend-project.svc.cluster:6379
注意:服務名稱和專案名稱空間是 URL 的一部分。這使得所有叢集服務都可以很容易地實現互通訊。下圖展示了作者如何透過環境變數配置 Django,用來使用叢集中的 Redis:Kubernetes 將自動保持 DNS 記錄與 pod 同步,即使容器在自動伸縮期間跨節點移動。作者希望透過一些簡單的命令來建立和銷燬版本控制、可複製的基礎架構。為了實現這一點,作者在 monorepo(包含 all-things 架構) 中使用 Docker、Terraform 和 Kubernetes manifests,甚至在跨專案中也如此。對於每個應用程式 / 專案,作者都使用一個單獨的 git repo。作者透過在 git repo 中描述基礎架構,不需要跟蹤某些 obscure UI 中的每個小資源和配置設定。這樣能夠在災難恢復時使用一個命令還原整個堆疊。下面是一個示例資料夾結構,在 infra monorepo 上可能找到的內容:# 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
這種設定的另一種優勢是,所有的移動部件都在同一個地方描述。作者可以配置和管理可重用的元件,如集中式日誌記錄、應用程式監控和加密機密等。作者採用 Terraform 來管理大多數底層雲資源,這可以幫助記錄和跟蹤組成基礎設施的資源和配置。在錯誤恢復時,作者可以使用單個命令啟動和回滾資源。例如,如下是作者的 Terraform 檔案之一,用於為加密備份建立一個私有 S3 bucket,該 bucket 在 30 天后過期: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"
}
}
}
}
類似地,作者所有的 Kubernetes 清單都在基礎設施 monorepo 中的 YAML 檔案中描述,並將它們分為兩個目錄 cluster 和 apps。在 cluster 目錄中,作者描述了所有叢集範圍的服務和配置,如 nginx-ingress、encrypted secrets、prometheus scrapers 等。這些基本上是可重用的位元。apps 目錄在每個專案中包含一個名稱空間,描述部署所需的內容,如 ingress rules、deployments、secrets、volumes 等。Kubernetes 的一個很酷的地方是:你可以定製幾乎所有關於堆疊的東西。因此,如果你想使用可調整大小的加密 SSD volumes,則可以在叢集中定義一個新的 StorageClass。Kubernetes 和 AWS 將協調產生作用,如下所示:現在,作者可以為任何部署附加這種型別的持久儲存,Kubernetes 管理請求的資源:作者採用 Stripe Checkout 來儲存付款、建立結賬螢幕、處理信用卡 3D 安全要求、甚至客戶賬單門戶的所有工作。這些工作沒有訪問支付資訊本身,這是一個巨大的解脫,可以專注於產品,而不是高度敏感的話題,如信用卡處理和欺詐預防。現在需要做的就是建立一個新的客戶會話,並將客戶重定向到 Stripe 託管頁面之一。然後,監聽客戶是否升級 / 降級 / 取消的網路鉤子(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}
作者將 Stripe 應用在 API 端點、cron job 和管理任務中,以確定哪些限制 / 特性適用於特定的客戶,當前計劃用的是 BillingProfile 模型上的 plan_code。作者還將使用者與帳單資訊分開,因為計劃在某個時間新增組織 / 團隊,這樣就可以輕鬆地將帳單配置檔案遷移到帳戶所有者 / 管理員使用者。當然,如果你在電子商務商店中提供數千種單獨的產品,這種模式是無法擴充套件的,但它對作者來說非常有效,因為 SaaS 通常只有幾個計劃。作者不需要 logging agen 之類的東西測試程式碼,只需登入 stdout、Kubernetes,即可自動收集 log。你也可以使用 FluentBit 自動將這些 log 傳送到 Elasticsearch/Kibana 之類的應用上,但為了保持簡單,作者還沒有這麼做。為了檢查 log,作者使用了 stern,這是一個用於 Kubernetes 的小型 CLI 工具,可以非常容易地跨多個 pod 跟蹤應用程式 log。例如,stern -n ingress-nginx 會跟蹤 nginx pod 的訪問 log,甚至跨越多個節點。最開始,作者採用一個自託管 Prometheus/Grafana 來自動監控叢集和應用指標。然而,作者不喜歡自託管監控堆疊,因為在叢集中一旦出現錯誤,那麼告警系統也會隨之崩潰。作者所有的服務都有 Prometheus 整合,該整合可自動記錄指標並將指標轉發到相容的後端,例如 Datadog、New Relic、Grafana Cloud 或自託管的 Prometheus 例項。如果你想遷移到 New Relic,需要使用 Prometheus Docker 映像,並關閉自託管監控堆疊。New Relic 儀表盤示例彙總了最重要的統計資料。
使用 New Relic 探測器監測世界各地執行時間。
從自託管的 Grafana/Loki/Prometheus 堆疊遷移到 New Relic 簡化了操作介面。更重要的是,即使 AWS 區域關閉,使用者仍然會收到警報。至於如何從 Django app 中公開指標,作者利用 django prometheus 庫,只需在應用程式中註冊一個新的計數器 / 儀表:這一指標和其他指標將在伺服器的 / metrics 端點中公開。Prometheus 每分鐘都會自動抓取這個端點,將指標傳送至 New Relic。由於 Prometheus 整合,這個指標會自動出現在 New Relic 中。
每個人都認為自己的應用程式沒有錯誤,直到進行錯誤跟蹤時才發現錯誤。異常很容易在日誌中丟失,更糟的是,你知道異常的存在,但由於缺少上下文而無法復現問題。作者採用 Sentry 來聚合應用程式中的錯誤。檢測 Django app 非常簡單,如下所示Sentry 非常有幫助,因為它自動收集了一堆關於異常發生時出現何種異常的上下文資訊:異常發生時,Sentry 會聚集異常並通知使用者。
作者使用 Slack #alerts 通道來集中所有的警告,包括停機、cron job 失敗、安全警告、效能退化、應用程式異常等。這樣做的好處是當多個服務同時進行 ping 操作時,可以將問題關聯起來,並處理看似不相關的問題。澳大利亞悉尼 CDN 端點下降導致的 Slack 警告。
在進行深入研究時,作者還使用 cProfile 和 snakeviz 之類的工具來更好地瞭解分配、呼叫次數以及有關 app 效能的其他統計資訊。 cProfile 和 snakeviz 是可用於分析本地 Python 程式碼的工具。
作者還使用本地計算機上的 Django debug toolbar 來方便地檢查檢視觸發的查詢,預覽開發期間傳送的電子郵件。Django 的 Debug 工具欄非常適合在本地開發中檢查內容、以及預覽事務性郵件。
原文連結:https://anthonynsimon.com/blog/one-man-saas-architecture/