使用 Kubernetes,一個人如何支撐起創業公司運作?

一篇來自創業公司的技術分享,主要介紹了在 AWS 上使用 Kubernetes,從負載平衡到 cron 作業監控,再到支付和訂閱。

Kubernetes 是一款開源軟件,你可以利用它大規模地部署和管理容器化應用程序。Kubernetes 管理 Amazon EC2 計算實例集羣,並在這些實例上運行容器以及執行部署、維護與擴展進程。藉助 Kubernetes,你可以在本地和雲上使用相同的工具集運行任何類型的容器化應用程序。

來自哥斯達黎加的軟件工程師 Anthony Najjar Simon(下文中均以作者代替)向我們分享了一個人如何經營運作一家公司。該公司建立在作者在德國的公寓裏,完全由自己出資。他主要介紹了在 AWS 上使用 Kubernetes,從負載平衡到 cron 作業監控,再到支付和訂閱,實現了一人公司的順利運行。

總體概覽圖

基礎設施可以同時處理多個項目,但爲了說明問題,作者使用 SaaS Panelbear 作爲這種設置的實際示例。

_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 讓簡單的東西變得複雜,但它也讓複雜的東西變得簡單。」

**Automatic DNS,SSL,負載均衡
**

作者在 AWS 上有一個託管 Kubernetes 集羣,並在其中運行了各種項目。接下來開始本教程的第一站:如何將流量引入集羣。該集羣在一個私有網絡中,無法從公共互聯網直接訪問它。

_流量在邊緣緩存或者傳達到操作的 AWS 區域。
_

但是,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

自動 rollout 和回滾

_提交新 commit 時發生的反應鏈。
_

就 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 個。

**CDN 靜態資產緩存
**

在爲 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 可以爲所有模型免費提供了一個管理面板。它是內置的,非常方便用於檢查數據以進行客戶支持工作。

_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(
   ,
   expected_schedule=timedelta(hours=1),
   grace_period=timedelta(hours=2),
 ).ping()

App 配置

所有應用程序都是通過環境變量配置的,雖然老式但很便攜,而且具有良好支持。例如,在 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 中的數據。

基於 DNS 的服務發現

除了 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 resourcesterraform/
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 來管理大多數底層雲資源,這可以幫助記錄和跟蹤組成基礎設施的資源和配置。在錯誤恢復時,作者可以使用單個命令啓動和回滾資源。

例如,如下是作者的 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 app 部署清單

類似地,作者所有的 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 安全要求、甚至客戶賬單門戶的所有工作。這些工作沒有訪問支付信息本身,這是一個巨大的解脫,可以專注於產品,而不是高度敏感的話題,如信用卡處理和欺詐預防。

_在 Panelbear 中的客戶計費門戶示例。
_

現在需要做的就是創建一個新的客戶會話,並將客戶重定向到 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
**

作者不需要 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/

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