现代初创公司的架构
策划 | Tina
初创公司的技术方面有时会非常多变,包含很多未知因素。使用什么技术栈?哪些组件对现在来说可能被过度使用,但在未来却值得关注?如何在保持足够高的质量标准以维持可维护的代码库的同时,平衡业务特性开发的速度?
在本文中,我想和大家分享一下我们从头开始构建 https://cleanbee.syzygy-ai.com/ 的经验——我们如何根据需求塑造流程,以及当我们用新组件扩展我们的技术栈时,我们的流程是如何演变的。
企业希望征服市场,工程师们则希望能够尝试酷炫的东西,拓展自己的思维。与此同时,业界涌现的新语言、框架和库的数量如此之多,以至于你无法一目了然。而且,通常情况下,如果你刮开“全新事物”的闪亮表面时,你会发现一个很好的旧概念。这很好,如果你很幸运的话。
最激动人心的争论话题之一是流程——无论你是依赖主干开发(trunk-based development),还是喜欢更怪异的 GitHub 流程,或者是 Mob 编程(暴徒式编程)的忠实拥趸,还是觉得花时间在基于 PR 的代码审查上更有效率。
我有过在这样的环境中工作的经验,在这种环境中,工件在没有任何标准化流程的情况下被丢弃在用户身上。在出现问题的情况下,开发人员获得了很多乐趣(并不是!)试图弄清楚组件的实际部署版本。
另一个方面是永远也排不上队的 CI。在你创建了 PR 之后,你必须在最近的 30 分钟内通过打赌 CI 集群是否能找到资源对你的改动进行测试来娱乐自己。有时,平台团队会引入新的、令人兴奋的、有用的特性,这些特性可能会破坏与现有 CI 模板的兼容性。这些可能导致在等待一个小时之后,在最后一分钟你所有的检查都失败了。
我坚信,像往常一样,这一切都取决于团队的成熟度,你正在构建的软件种类,以及各种业务限制,例如,是否存在错误的预算和上市时间与 SLX 的重要性。
我认为,重要的是制定一套大家都认可和遵守的共识程序。如果有证据表明有更好的选择,那就勇敢地去挑战并改变它,能否做到这一点也很重要。
我们开始时的情况如下:
仅十来名开发人员——内部团队和合同工,他们希望并且能够异步工作。
完全的绿地项目——还没有写过任何一行代码。需求是模糊的,但已经开始形成了某种东西。
技术方面——明确需要一个能与移动客户端对话的后端。
一些简单的 Web 前端——静态页面应该足够了(并非如此)。
我们已经开始使用简单的方法——在 GitHub 编写代码,使用基于 PR 的流程,只需要一个单一的需求——在 1~3 天内交付可分裂的票据。这需要一些故事切片的实践,并且似乎通过将票据移动到“done”的能力,显示了一种可见的快速行动的感觉。这对团队来说是一个很好的激励因素,可以让他们接受这一想法。
Linters 和静态分析器可以跳过令人兴奋的讨论,比如每个方法有多少个参数过多。我们将逐步增加自动化测试。我们也尝试 CodeScene。他们有一种非常有前景的方法,可以突出显示代码的重要部分(那些经常更改的部分,肯定应该有更高的可维护性条!),并通过查看代码中的嵌套程度来识别复杂性。对于初创公司来说,这可能有点昂贵,但是 100% 为工程师提供了不错的提示。
在架构方面,人们渴望深入到微服务的“仙境”中去。但是看看这些大玩家之间可怕的联系图,需要追踪他们之间的请求,这对于那些想要快速行动的早期团队来说,简直就是找死。
通过对需求的分析,我们能够检测出三组作业:
核心 API 与通常 Crud 类的活动;
搜索和推荐;
根据时间表做一些有用的事情的工作负载(几乎在偶尔延迟的时候是可以的)。
技术栈的选择:时间有限,期望值高的情况。使用你所知道和掌握的(是的,也许对某些人来说,这是无聊的技术)。因此,Fastapi、REST、无状态、Python、redis 和 Postgres 是我们最好的朋友(是的,我们喜欢 Go 和 Rust,但我们需要多付出一点代价!)。
对于移动客户端,情况有所不同。我们预见到了许多带有状态和与远程服务交互的屏幕,但没有太多定制的、特定于平台的调整。因此,为 iOS 和 Android 提供单一代码库的想法非常有吸引力。
如今,框架的选择真的很广泛,但同样,由于 Flutter 的一些经验,我们决定尝试一下。在移动开发中,要更好地决定的一个重要方面是状态管理。在这里,你会看到很多缩写,这些缩写来自不同的语言和框架,会让你感到困惑。其中包括 MVC、MVVM、VIPER、TCA、RIBs、BLOC 等。
我们的座右铭从最简单的解决方案开始,这些解决方案足以支持必要的功能。简单。这么说吧,我们觉得我们已经理解了。
然而,我们在构建 MVP 之后肯定犯了一个错误,因为我们决定在顶部构建,而不是把它扔掉。因此,在一个阳光灿烂的日子里,我质疑自己的理智:在我注释了代码,清理了所有可能的缓存后,在新的屏幕上仍然没有看到我的改动。是的,死代码应该被删除!
在解决了这些初始手续之后,下一件必要的事情就是能够检查客户端和服务器之间的交互。
API 契约是一件很棒的事情,但是当真实服务器抛出 “模式验证错误” 或因 HTTP 500 错误代码而惨遭失败时,会更明显地出现问题。
后端服务最初分为两组——API 单体、搜索和推荐。前者包含或多或少与数据库交互的简单逻辑,后者包含 CPU 密集型计算,可能需要特定的硬件配置。每个服务都有自己的可伸缩性组。
当我们还在考虑推广策略(并讨论买哪个域名)时,解决方案很简单:尽量减少移动工程师在处理后端(即外部堆栈)方面的困难。让我们把一切都打包到 docker 中。
当我们为本地部署做好一切准备时——移动工程师可以运行 docker-compose 命令,并做好一切准备(经过几次痛苦的尝试,发现了文档中的缺陷,但这些练习的真正价值在于对每一个“卧槽”做出反应并改进它)。
一切都很好,但是在空数据库上运行 API 有什么意义呢?手动输入必要的数据很快就会导致抑郁症(以及增加开发周期的风险)。因此,我们准备了一个精选数据集,并将其插入到本地数据库中,以便能够使用。我们也开始使用它进行自动测试。双赢!当你有几十个具有相似密码的假用户时,身份验证在定义测试场景时就不那么成问题了!
与新技术打交道总是有点危险。你和你的团队不可能什么都知道(有时你认为你知道的事情可以让你感到充实,但那是另一回事了)。尽管如此,它仍然需要评估和调查没有人接触过的东西。
支付、电子邮件、聊天、短信、通知、分析等。每一个现代应用程序通常都代表着与多个第三方提供商紧密相连的业务逻辑。
我们选择合作对象的方法——有时间限制的、尝试用它构建的活动,尝试通过特性、支持的语言选择最有前途的活动,对于提供者来说,还有定价。
后端是数据库的一部分,也应该有一些对象 / 文件存储。迟早,我们还应该拥有 DNS,这样我们的服务才能准备好与这个残酷的大世界打交道。
对云提供商的选择完全基于团队内部现有的专业知识。我们已经在其他项目中使用亚马逊云科技,所以我们决定坚持使用它。可以肯定的是,在亚马逊云科技控制台中可以做所有的事情,但是随着时间的推移,事情变成了一个典型的大泥球,每个人都害怕去触碰,而且根本没有人记得为什么存在这个东西。
好吧,看来基础设施即代码的范式在这里很方便。
从工具上看,选择并不多——供应商特定的亚马逊云科技 CloudFormation、谷歌云(Deployment Manager、Azure 自动化)、Terraform 以及其竞争对手。
基于对 Terraform 的经验,你已经知道我们如何选择了吗?
是的,初始设置将需要一些时间(如果没有控制,在 Terraform 中也很容易成为同样的大泥球),但至少它将有一些关于基础设施的文阿东和它为什么存在的可见性。另一个主要优势是,无论你通过 Terraform 管理什么,都会自动更新(当你或 CI/CD 运行相应的命令时)。
对于亚马逊云科技本身,鉴于我们在亚马逊云科技内部运行一切,我们可以依赖 IAM,并通过将必要的策略附加到虚拟机来承担角色。但是,我们需要与第三方服务整合,并以某种方式向我们的应用程序传递一些秘密,例如,数据库的密码。我们需要一些秘密管理的解决方案。亚马逊云科技有 KMS,GitHub Actions 有它们自己的秘密,除此之外,还有一堆其他提供商。所以,真正的问题是:你需要从秘密管理中得到什么:
审计
基于路径的访问
与 Kubernetes 的集成
签发凭据的能力
Web 用户界面
免费
秘密版本控制
KMS 非常方便,我们设法将其添加到 GitHub Actions 中,但 vault 的用户界面和免费使用它的能力(如果你自己运行它)在这件事上是一种破坏。
一旦我们将应用 docker 化,我们就开始考虑 Kubernetes,因为它提供了一些开箱即用的好处。最重要的是能够旋转必要数量的 pod 以满足性能需求,并且能够以声明的方式定义你的所有需求。因此,如果有足够的自动化水平,没有人应该运行 kubectl apply。亚马逊云科技从 EKS 开始,可以通过 terraform 管理。
另一方面,陡峭的学习曲线(要理解它是如何准确定义应该启动和运行的)和一些可以使用的特定工具是需要重新考虑的合理理由。
如果我们谈论 Kubernetes,并且在每次合并到 main 时发布了 docker 应用,那么 Helm 图表将成为适应现代基础设施栈的下一步。我们已经插入了亚马逊云科技 ECR 来跟踪每一个新的版本,并在专用的 S3 桶中发布 Helm 图,成为我们内部的 Helm 图注册表。
将其整合在一起并不像预期的那样简单。Kubernetes 节点最初不能连接到 ECR 并提出必要的 docker 镜像,用于处理亚马逊云科技 KMS 中的秘密的 terrform 模块(aws-ssm-operator)已被弃用,并且不支持最近的 Kubernetes API,秘密和配置映射也不适合暴露在 pod 中。
首次推出的服务给移动用户带来了快乐——不需要关心本地设置的说明了!在最初的一周左右,虽然,它并不是真正的稳定,但后来,少了一件需要关心的事情。
你需要所有的吗?不需要。
我必须承认,这种组合——Kubernetes 和 Vault 通过 terraform 和 helem ——可能不适合所有人,而且在初始阶段你很可能也不需要它。在合并到 main 时,简单的 docker 推送到 ECR,然后在 ssh 中执行 ec2 && docker pull && docker-compose 来停止启动—— 在 CICD 的发布期间可以很好地工作(至少对于一个愉快的路径)。乍一看,每个人都会很清楚。这正是我们目前重新部署静态网站的方式。我们可以关注 CI 构建的新版本,并将其复制到相应的 s3 桶中。
亚马逊云科技很好,可以为那些愿意探索创业世界阴暗道路的人提供积分。我们可以用它在 GitHub 上节省几分钟的时间,并向 GitHub VM 公开更少的秘密和基础设施吗?
如果是自托管的 Runner,也就是说,当你打开一个 PR 时,分配了一个 Pod 来运行 CI 检查的不是 GitHub VM,而是你自己的 Kubernetes?当然,为 iOS 发布准备一切是很困难的(下面会有更多介绍),但是 Android 和后端肯定可以在旧的 Linux 上运行?!
我们已经通过专用的 k8s pods 构建了它,但是还有一个选项可以在现场 EC2 实例上运行检查。
围绕监控和警报等术语,有很多营销上的废话。
在一些公司,实现这些东西只是为了炫耀,“我们有 X 的特性!”。然而,当出现真正的问题或警报频道因包含不可操作的噪声而不得不静音时,工程师们仍然对他们的生产情况视而不见。
我必须说,我们还有很长的路要走。
只要你搜索这种解决方案,你会发现第一件事就是 ELK 栈和一堆付费提供商。在权衡了维护我们自己的设置所需的时间和精力后,我开始思考付费解决方案可能是值得的。当且仅当你真的能够将获取关于你的应用和基础设施状态的最重要信息的负担委托给现有的解决方案。这完全取决于他们是否有预设的指标、日志解析器和索引映射,你可以很容易地适应你的项目。
对于日志记录,目前,我们依靠 ELK。是的,设置起来或多或少很简单,而且很可能,有些人发现 Elastic 的查询语言每天使用起来非常方便。
在这里,我们仍然在探索各种选择,因为似乎搭配 grep 的老式 kubectl logs 可以更及时地为“app1 pod 的最后一个错误是什么”这样的问题提供见解,而不会迷失在无穷无尽的用户界面控件中。但最有可能的是,Kibana 的用户界面仍然隐藏着我们应该使用的手段,以添加适当的摄取管道,并为 filebeat 的弹性索引选择相应的映射。
为了发出警报,我们设置了 Prometheus 并将其集成到 Slack 中。同样,主要是因为我们以前经历过这种情况。
正如产品发展时通常发生的那样,新的需求引入了新的事物:
现在,作为公开可见内容的一部分,我们需要一些仅供团队使用的资源
要管理特性标志,请访问 vault UI,或使用 Elastic 来找出最后一个 API 错误
当然,有付费的解决方案,或者你可以把一些 Identity 作为服务提供商(Azure active directory)与任何 VPN 提供商混合使用,以对你的团队成员进行身份验证。我们之所以选择 OpenVPN,是因为它们的免费层,并且只向内部网络公开必要的服务,以便那些应该使用凭证登录的用户能够使用这些服务。与使用亚马逊云科技栈相比,它有一个明显的优势——它是免费的(对于有限的连接数)。
到目前为止,我们主要讨论了事物的后端部分。但还有更多。你首先看到的是——移动应用程序!这是最重要的事情。Flutter 或其他的东西也必须被构建、检查、和测试。并以某种方式发布在某个地方,这样利益相关者就可以立即对新特性感到敬畏(并发现新的 bug)。
如果要推广到生产中,你需要通过一系列的手续(截图、修改日志 = 新增内容、审查),这将耽误你的观众享受这些艺术作品。
我必须说,商店的 API 对频繁发布并不真正友好。生成应用并签名时,发布可能需要 15 分钟以上。与其他 API 一样,应用商店的 API 迟早会失败。是的,签名可能是一场噩梦,因为它在不同的平台之间是不同的。而且,如果工程师不把时间浪费在所有这些事情上,从他们的笔记本电脑上准备发布,那就更好了。
你应该考虑的第一件事(可能也是唯一的一件事)是 fastlane——最初,我确实对所有这些新术语如 gems(虽然喜欢这个名字!)和 bundle 有一些偏见,但它很有效。要从 CI 中运行它们,需要做一些努力来处理秘密,如 Android 的 jks 或 iOS 的 match。
接下来,你会开始考虑应用分发的问题:Testflight 是 iOS 世界的一个方便的工具,但 Android 呢?我们最终使用了 App Distribution——来自 Firebase 的解决方案——主要是因为它在第一次尝试后对我们有用。但也有其他的选择(声称对两个平台都有效)。
重要的是,你可以从 Fastlane 做所有的事情!即使你的应用程序不断发展,你开始添加各种额外的东西——分析、聊天、地图、地理——很多都是直接从 Google 的 Firebase 中获得的。由于 Firebase 提供了很多好东西,所以收集分析事件是很自然的事情,在对他们的 IAM 策略进行了一些调整后,我们设置了将原始事件导出到 gs-buckets 中,以便能够与 BigQuery 一起使用。
对于后端,我们从一开始就有自动测试。诸如测试替身(test double)之类的各种实践证明非常有效地防止了回归,即使在复杂的业务逻辑中,通过从端服务进行集成也是如此。在移动端,由于来自 MVP 的代码共存,我们受到了一些限制,自动测试对复杂的业务场景并没有太大的帮助,比如有人想使用我们的服务,但我们不能从他们的银行卡上收费。
手工测试非常耗时且容易出错,尤其是当业务逻辑动态发展,以及最近更新后数据库中的数据状态从领域规则的角度来看变得不可能。
是的,所以通过点击我们正在维护的数据(并确定它是有效的)的应用程序来运行 E2E 测试会很好。如果这些测试不污染实际的数据库、S3 桶和第三方提供商,那就更好了。
我们从一个主分支和一个环境(rds、redis、k8s 命名空间和 s3)开始,由第一批测试人员和开发人员使用。我们没有在公众面前公开,但随着我们越来越接近发布,很明显,对于我们可以打破事物并拥有稳定环境的地方,某种区别是必要的。
在移动应用中,在构建过程中更改 API 的 URL 是一个问题。在后端,有几个方面必须做,以支持部署特定的配置:基础设施方面,通过创建专门的策略和资源,并在代码中需要特定 URL 的几个位进行参数化。除此之外,还有几个存储库,其中一些是独立的,但有些是依赖的——与共享功能的情况一样。
你知道当你更新共享功能而不立即重新部署和测试所有依赖的应用程序会发生什么吗?几天后,当你完全忘记它的时候,你会在依赖的仓库其他地方做一些无关痛痒的、纯粹是表面上的改变,这将导致重新部署并拉取最新的依赖性。
当然,在一个重要的 DEM 期间或之后,你会看到一些愚蠢的错误,这些错误与单个条件缺乏兼容性有关,但是你忘记再次检查。
因此,拆分环境的第一个重要注意事项是,如果更新了一些基本存储库,则自动执行所有相关应用程序的整体部署。你可以要求团队这样做,每个人都同意,但忘记了拉取。
第二个事项——我们需要部署什么?我们是否需要维护每个环境中的所有应用程序,包括负责发送电子邮件或通知的工作?似乎在部署中包含或排除作业的一些标志可能会有所帮助。
E2E,以及之后的阶段,可能不是必要的,每个人都可以在互联网上访问。
将新版本升级到 E2E 和 Staging,必须自动化。
向 Prod 推广新版本,至少现在,最好有控制和手动。
目前,我们有三个目标,可以实现以上所有目标:
E2E:一种环境,集成测试将在精选的数据上运行,以确保基本功能仍然存在。
Staging:在这里进行核心开发,测试人员可以尝试破坏我们构建的东西。
Prod:乐于迎接新用户的地方。
Kubernetes 集群仍然是一个单一的集群。所有内容都在名称空间级别上进行了分割。类似的事情也发生在 RDS 中,其中几个数据库共存于一个 RDS 实例中。
在移动测试的自动化方面,选择并不是很多。你首先要选择是使用任何云端设备提供商还是自己运行测试。
你当然可以把智能手机插入笔记本电脑并运行测试,但如果用 CI 来代替,那不是很好吗(也很正确!)?当你开始考虑提供模拟器和真实设备的提供商时,你会发现移动测试框架的选择并不广泛,但这是你必须做出的第二个选择(而提供商的选择可能会在这里限制你)。另一个重要的考虑因素——是否有特定的硬件要求,即使用 GPU 或 npu?因此,任何模拟对我们来说都是足够的。
我们确定了移动 E2E 测试框架的两个主要选择——Flutter 集成测试和基于 appium 的 pytests。Firebase Test Lab 支持 Flutter 集成测试,尽管它需要进行一些调整,以允许从他们的 IP 范围(有运行模拟器的虚拟机)的请求到达我们的 E2E API。
Appium,Python API 的一部分,它非常有前途,因为它就像使用 testproject 一样。你可以通过应用程序记录每个场景中的所有单击。因此,它不需要特定的编程知识,但它允许你逐步学习。到目前为止,在我们的设置中,Appium 在场景覆盖方面要全面得多。
E2E 测试有一个微小的问题——在模拟器中冷启动应用程序的速度不是很快。如果我们再加上构建应用程序所需的时间以及将调试构建复制到提供商的时间,就会成为快速行动的一个真正瓶颈。
到目前为止,我们已经试验过在一天内运行两次,但让我们看看情况如何。
许多有趣的任务仍在我们的待办事项清单上:
在基础设施方面——性能测试、安全测试、尝试用于 Web 的 Flutter。
在开发方面——为推荐引擎提供和更新机器学习模型,预测清理持续时间,用特征向量构建推荐缓存,混合优化问题以匹配引擎,以及调度工作和博弈论。
而最重要的是,没有什么可以取代真实世界的使用。
只有当你开始收集关于用户行为的真实数据时,你才会看到许多疯狂的事情,所以我们期待着即将到来的发布会!
Dmitry Kruglov,Syzygy AI 公司的首席技术官和联合创始人。
原文链接:
https://betterprogramming.pub/architecture-of-modern-startup-abaec235c2eb
声明:本文为 InfoQ 翻译,未经许可禁止转载。
选择“网红语言”值不值?使用Go和Rust的数据库公司七年经验总结
OpenAI回应ChatGPT不向所有中国用户开放;字节改节奏,双月OKR改季度;马斯克称今年底卸任推特CEO|Q资讯
背负着整个现代网络,却因“缺钱”放弃开源,core-js 负责人痛诉:“免费开源软件的根基已经崩塌了”
微信扫码关注该文公众号作者