这是一个长形的帖子,分解了我用来运行SaaS的设置。从负载平衡到Cron作业监控到订阅和付款。在那里覆盖了很多地面,所以扣了!
正如这篇文章的标题一样,宏伟可能会听起来,我应该澄清我们谈论一个低压力,我在德国的公寓里奔跑。它'完全自资,我真的很想把事情慢。它可能不是大多数人想象的,当我说和#34;科技创业公司"
我不会在没有大量的开源软件和管理服务时执行此操作。我觉得我站在巨人的肩膀上,谁在我面前做过所有艰苦的工作,我非常感激。
对于上下文,我运行一个人的SaaS,这是我使用的技术堆栈上的帖子的更详细版本。在遵循我的建议之前,请考虑自己的情况,在技术选择方面,您自己的上下文很重要,而且没有圣杯。
我在AWS上使用Kubernetes,但不要陷入思维的陷阱,你需要这个。我在一个非常耐心的团队中学到了几年内的这些工具。我' m生产性,因为这是我最擅长的,我可以专注于运输东西。你的旅费可能会改变。
顺便说一下,我画了灵感来自Wenbin Fang的博客文章的这篇文章的格式。我真的很喜欢阅读他的文章,你也想看看它!
我的基础架构立即处理多个项目,但是为了说明我将使用PanelBear,我最近的SaaS的东西,作为此设置的真实界限。
从技术角度来看,此SaaS从世界任何地方处理每秒大量请求,并以有效的格式存储数据以进行实时查询。
仍然在其婴儿期间(我六个月前推出的商业&#39),但它已经增长了很快,尤其是我最初为自己建造它作为一个小型vps的sqlite的Django应用程序。为了我当时的目标,它就好了,我可能已经推动了这一模型。
但是,我越来越沮丧必须重新实现很多工具我如此习惯:零停机部署,自动播放,健康检查,自动DNS / TLS / Ingress规则,等等。 Kubernetes宠坏了我,我被习惯于处理更高的抽象,同时保持控制和灵活性。
快进六个月,几个迭代,即使我现在的设置仍然是一个Django Monolith,I' M现在使用Postgres作为App DB,点击分析数据,以及Redis进行缓存。我还使用Celery进行计划任务,以及用于缓冲写入的自定义事件队列。我在托管的Kubernetes群集(eks)上运行大部分这些东西。
它可能听起来很复杂,但它实际上是在Kubernetes上运行的老学校单片架构。用铁轨或小旅行替换django,你知道我在谈论什么。有趣的部分是一切如何粘在一起和自动化:自动播放,入口,TLS证书,故障转移,记录,监控等。
值得注意的是我在多个项目中使用这个设置,这有助于保持我的成本并可轻松启动实验(写入DockerFile和Git Push)。既然我被问到了这一点:与你可能正在思考的相反,我实际上花了很少的时间管理基础设施,通常每月0-2小时。我的大部分时间都花在开发功能,做客户支持,并越来越多的业务。
也就是说,这些是我已经使用了几年的工具,我很熟悉他们。我认为我的设置很简单,因为它的能力是什么,但在我的日常工作中需要多年的生产火灾。所以我不会说这是阳光和玫瑰。
我不知道谁首先说了谁,但我告诉我的朋友是:" kubernetes制作简单的东西复杂,但它也使得复杂的东西更简单"
既然你知道我在AWS上拍摄了一个托管的kubernetes集群,我在它中运行各种项目,让' s发出第一个停止:如何将流量进入群集。
我的群集在私人网络中,因此您将无法直接从公共互联网到达。控制访问权限和负载均衡流量之间有几个部分。
基本上,我将CloudFlare代理到NLB的所有流量(AWS L4网络负载平衡器)。此负载平衡器是公共互联网和我的私人网络之间的桥梁。一旦收到请求,它就会将其转发到一个Kubernetes群集节点。这些节点位于AWS中的多个可用区域的私有子网中。它逐渐管理,但更稍后更多。
流量在边缘缓存,或转发到我操作的AWS区域。
"但Kubernetes如何知道将请求转发到哪个服务?" - 那就是Ingress-Nginx进入的地方。简而言之:由Kubernetes管理的NginX群集,它''群体中的所有流量的入口点。
nginx应用速率限制和其他流量整形规则我在向相应的应用程序容器发送请求之前定义。在PanelBear的情况下,应用程序容器是uvicorn服务的Django。
它与VPS方法中的传统nginx / gunicorn / django不同,'在VPS方法中,增加了水平缩放优势和自动化CDN设置。它也是一个“Setup一次忘记”的东西,主要是Terraform / Kubernetes之间的一些文件,它是由所有部署的项目共享的。
当我部署一个新项目时,它基本上是20行的入口配置,这是:
apiersion:networking.k8s.io/v1beta1种类:Ingress元数据:命名空间:示例名称:示例 - API注释: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:"真实" SPEM:TLS: - 主机: - API.EXAMPLEM。秘密名称:示例 - API - TLS规则: - 主机:API.EXAMPLECOM HTTP:PATHS: - 路径:/ bathend:serviceName:示例 - API ServicePort:HTTP
这些注释描述了我想要一个DNS记录,通过LetSencrypt通过CloudFlare代理的流量,并且它应该在将请求转发到我的应用之前通过IP限制每分钟的请求。
Kubernetes负责使那些infra改变以反映所需的状态。这有点冗长,但它在实践中很好。
每当我推动掌握我的一个项目之一时,它会在GitHub操作上启动CI管道。该管道运行一些代码库检查,端到端测试(使用Docker撰写撰写以设置完整的环境),一旦这些检查传递,它就会构建一个被推送到ECR的新Docker映像(AWS中的Docker注册表)。
就应用程序repo而言,已测试新版本的应用程序,并准备将部署为Docker Image:
"那么接下来会发生什么?有一个新的Docker图像,但没有部署?" - 我的Kubernetes群集有一个名为Flux的组件。它会自动保持同步当前在群集中运行的内容以及我的应用程序的最新图像。
当有一个新的Docker Image提供了新的Docker Image时,助焊剂会自动触发增量卷展栏,并在AN&#34中保留这些动作的记录;基础设施Monorepo"
我想要版本控制的基础架构,这样只要我在Terraform和Kubernetes之间做出新的提交,他们将对AWS,CloudFlare和其他服务进行必要的更改,以将我的回购状态与部署的内容同步。
它全部版本控制,每个部署的线性历史记录。这意味着多年来我要记住的东西更少,因为我没有通过Clicky Clicky在一些晦涩的ui上配置了魔法设置。
几年前,我使用了各种公司项目的actor并发的演员模型,并与其生态系统周围的许多想法坠入爱河。有一件事导致另一件事,很快我就读了关于erlang的书籍,以及它在让事情崩溃的情况下的哲学。
我可能会伸展太多的想法,但在血腥的人中,我喜欢想到活力探测和自动重启作为实现类似效果的手段。
从Kubernetes文档中:“Kubelet使用Liventive探针知道何时重新启动容器。例如,Live Probes可以捕获僵局,其中应用程序正在运行,但无法进行进度。在这样的状态下重新启动容器可以帮助您更好地提供应用程序。“
在实践中,这对我来说很好。容器和节点旨在来,血管对齐会优雅地将流量转移到健康的豆荚,同时治愈不健康的豆荚(更像杀戮)。野蛮,但有效。
我的应用程序容器基于CPU /内存使用情况自动规模。 Kubernetes将尝试每个节点打包尽可能多的工作负载以充分利用它。
如果群集中的每个节点的POD太多,它将自动产生更多服务器以增加集群容量并缓解负载。同样,当没有太多的情况下,它将缩减。
apiersion:autocaling / v1种类:stallypodautoscaler元数据:名称:panelbear - api命名空间:panelbear spec:scaletargetref:appiersion:appionion:appsion:appsion:appsion:deployment name:panelbear - api minreplicas:2 maxreplicas:8 targetcpuutilizationpercenty:5
在此示例中,它将根据CPU使用情况自动调整PanelBear-API Pods的数量,从2副本开始,但盖住8。
在为我的应用程序定义入口规则时,注释CloudFlare-Proxied:"真实"是告诉Kubernetes我想使用CloudFlare for DNS,并通过它的CDN和DDOS保护来代理所有请求。
从那时起,它很容易利用它。我只是在我的应用程序中设置标准HTTP缓存标头,以指定可以缓存哪个请求,以及多长时间。
CloudFlare将使用那些响应标头来控制边缘服务器的缓存行为。这对于这么简单的设置非常好。
我使用Whitenoise直接从我的应用程序容器提供静态文件。这样,我避免在每个部署上将静态文件上传到nginx / cloudfront / s3。到目前为止,它已经很好地工作了,大多数请求将被CDN填补的CDN缓存。它的表演,并保持简单的事情。
我还使用NextJS了解一些静态网站,例如Panelbear的着陆页。我可以通过CloudFront / S3甚至NetWify或Vercel服务,但很容易将其作为群集中的容器运行,让CloudFlare在所要求的要求时缓存静态资产。我来执行此操作的零增加成本,我可以重新使用所有工具进行部署,记录和监控。
除了静态文件缓存,还有#39;还有应用数据缓存(例如,重计算结果,Django模型,限速计数器等)。
一方面,我利用最近使用的内存最少使用(LRU)缓存以保持频繁访问的对象在内存中,并且我会受益于零网络调用(纯Python,没有涉及的Redis)。
但是,大多数端点只需使用群集redis进行缓存。它&#39仍然快速,缓存的数据可以由所有Django实例共享,即使在重新部署之后也可以共享,而内存中缓存会被擦除。
我的定价计划基于每月的分析事件。对于这种一些计量,必须知道在当前结算周期内消耗了多少事件并强制实施限制。但是,当客户交叉限制时,我不会立即中断服务。而不是a"容量耗尽"电子邮件已自动发送,并在API开始拒绝新数据之前给客户提供宽限期。
这意味着为客户提供足够的时间来决定升级是否对它们有意义,同时确保没有数据丢失。例如,在交通飙升期间,如果他们的内容变得病毒,或者如果它们'重新享受周末并没有检查他们的电子邮件。如果客户决定留在目前的计划并没有升级,那么一旦使用内部的计划限制,就没有罚款,事情将恢复正常。
因此,对于此功能,我有一个应用上述规则的函数,这需要多个调用DB和Clickhouse,但是要缓存15分钟,以避免在每个请求上重新计算此项。它' s的足够好,简单。值得注意的是:缓存在计划变更时无效,否则可能需要15分钟才能生效。
@cache(ttl = 60 * 15)def has_enough_capacity(站点:网站) - > BOOL:"""如果站点有足够的容量接受传入事件,则返回true,或者如果它已经过计划限制,则宽限期结束。 """
虽然我在Kubernetes的Nginx-Ingress执行全球速率限制时,我有时需要对每个端点/方法的更具体限制。
为此,我使用优秀的Django RAIRIMIT库来轻松声明每个Django视图的限制。它配置为使用REDIS作为后端使用REDIS,以便跟踪将请求的客户端追踪每个端点(它基于客户端密钥存储散列,而不是IP)。
class mysensitiveActionView(Ratelimitmixin,Loginrequiredmixin):Ratelimit_Key =" user_or_ip" Ratelimit_rate =" 5 / m" Ratelimit_Method ="邮政" RATELIMIT_BLOCK = TRUE DEF GET():。 。 。 def post():。 。 。
在上面的示例中,如果客户端尝试将此特定的端点发布到每分钟超过5次,则随后的呼叫将被HTTP 429拒绝太多请求状态代码。
Django免费为我的模型提供管理面板。它是内置的,它非常方便检查客户支持工作的数据。
我添加了帮助我从UI管理事物的操作。像阻止访问可疑帐户的东西一样,发送公告电子邮件,并批准完整的帐户删除请求(首先是一个软删除,并且在72小时内完全销毁)。
安全性:只能访问面板(ME)的工作人员,我计划在所有帐户中添加2FA以额外的安全性。
此外,每次用户登录时,我都会向帐户的电子邮件发送有关新会话的详细信息,请发送自动安全电子邮件。现在我将其发送到每个新的登录,但我可能会在将来更改它以跳过已知的设备。这不是一个非常“MVP功能”,但我关心安全性,添加并没有复杂。如果有人登录我的帐户,我至少会警告。
当然,还有很多东西来加强应用程序而不是这个,但是这个帖子的范围'
另一个有趣的用例是我运行了很多不同的预定作业作为我的SaaS的一部分。这些是为客户生成日常报告,每15分钟计算使用统计数据,发送员工电子邮件(我收到每日电子邮件,最重要的指标)和Whatnot。
我的设置实际上很简单,我只有几个芹菜工人和一个在群集中运行的芹菜击败计划程序。它们配置为使用REDIS作为任务队列。我花了一个下午来设置一次,幸运的是到目前为止我没有任何问题。
当计划的任务未按预期运行时,我想通过SMS / Slack /电子邮件通知。例如,当每周报告任务卡住或显着延迟时。为此,我使用healthchecks.io,但是结账结束和cronhub,我也一直听到他们的伟大事物。
要抽象他们的API,我写了一款小型Python代码段来自动化监视器创建和状态ping:
def some_hourly_job():#任务逻辑。 。 。 #ping监视服务一旦完成任务完成任务介绍(名称=" send_quota_depleeted_email",permaned_schedule = timedelta(hours = 1),grace_period = timedelta(小时= 2),)。 ping()
所有我的应用程序都是通过环境变量配置的,旧学校但便携式和支持良好。例如,在我的django settings.py中,我会使用默认值设置变量:
apiersion:v1种类:configmap元数据:命名空间:panelbear名称:panelbear - webserver - 配置数据:invite_only:"真" default_from_email:" panelbear团队< [email protected]& gt;" session_cookie_secure:"真" secure_hsts_preload:"真" secure_sl_redirect:"真"
处理方式的秘密是非常有趣的:我想向我的基础架构回购,以及其他配置文件,但应该加密秘密。
为此,我在kubernetes中使用kubeseal。此组件使用不对称的Crypto加密我的秘密,只有授权访问解密密钥的群集可以解密它们。
群集将自动解密秘密并将其传递给相应的容器作为环境变量:
要保护群集中的秘密,我可以通过KMS使用AWS管理的加密密钥,定期旋转。创建Kubernetes集群时,这是一个设置,它完全管理。
在操作上,这意味着我将秘密写为kubernetes中的环境变量,然后我在提交之前运行一个命令来加密它们,然后推动我的更改。
秘密在几秒钟内部署,并且在运行我的容器之前,群集将自动解密它们。
对于实验,我在群集中运行一个vanilla postgres容器,以及每天备份的Kubernetes Cronjob到S3。这有助于保持我的成本,并且刚刚开始,这很简单。
但是,随着项目的增长,如PanelBear,我将数据库从群集中移出到RDS中,让AWS处理加密的备份,安全更新以及所有其他任何其他东西,这些内容无需搞得搞砸。
为了增加安全性,由AWS管理的数据库仍然部署在我的专用网络中,因此它们通过公共互联网无法访问。
我依靠Clickhouse进行高效存储和(软)实时查询PanelBear中的分析数据。这是一个奇妙的柱状数据库,令人难以置信的快速,当您构建数据时,您可以实现高压缩比(较少的存储成本=更高的边距)。
我目前在我的Kubernetes集群中自我主持一个单击小时实例。我使用由AWS管理的加密卷密钥使用状态。我有一个Kubernetes Cronjob,它定期以高效的柱状格式备份所有数据到S3。在灾难恢复的情况下,我有几个脚本来手动备份和恢复来自S3的数据。
Clickhouse到目前为止已经摇滚固体,这是一款令人印象深刻的软件。这是我唯一熟悉的工具,当我开始我的萨斯时,但感谢他们的文档,我能够快速起床和运行。
我认为我想挤出更多的性能(例如,优化更好压缩的字段类型,预先计算物化表并调整实例类型),但现在已经足够了,但现在足够好。
除了Django,我还运行redis,Clickhouse,NextJ的容器等。这些容器必须以某种方式互相交流,以某种方式通过Kubernetes中内置的服务发现。
它非常简单:我定义了容器的服务资源,Kubernetes会自动管理群集中的DNS记录,以将流量路由到校正
......