Kubernetes 是一款开源软件,你可以利用它大规模地安排和治理容器化应用程序。Kubernetes 治理 Amazon EC2 计算实例集群,并在这些实例上运转容器以及执行安排、维护与扩展进程。借助 Kubernetes,你可以在本地和云上运用相同的工具集运转任何类型的容器化应用程序。
AWS 利用可扩展并且高度可用的虚拟机基础设施、具有社区支持的效劳集成,以及经过认证的 Kubernetes 托管效劳 Amazon Elastic Kubernetes Service (EKS),大幅简化了在云上运转 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(
name="send_quota_depleted_email",
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 <[email protected]>"
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 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 来治理大多数底层云资源,这可以帮助记录和跟踪组成基础设施的资源和配置。在错误恢复时,作家可以运用单个命令启动和回滚资源。例如,如下是作家的 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/