Cointime

扫码下载App
iOS & Android

如何为初创公司设计软件架构

一个好的初创公司的软件架构看起来会与你为企业系统建立的架构非常不同。以下就是对我们有用的一些方法。

到处都有很多关于如何为企业系统构建软件的信息。如果你在为一个初创公司设计系统,这些外部消息的模式和技术中的很多都不能发挥什么作用。高度的不确定性,对最大的灵活性和快速的节奏的需求,以及严重的人力限制,这些都是对于初创公司来说独有的挑战。但另一方面来说,初创企业可以做出企业无法做到的折中方案。

为我们的应用程序häpps决定出一个好的后台架构,是我们必须做出的最具挑战性的决定之一。即使对有经验的软件架构师来说,这个决定其实也很复杂,因为它为未来的发展设定了局限。我们是一家初创公司,因此只有两个程序员,没有多余的资金,这件事就使一切变得更加复杂。另外,做出架构决定是一个持续的过程,因为新兴的应用程序往往会往不可预见的方向发展。拥有一个能够快速发展并适应新要求的后端架构是非常重要的。灵活性是一个重要的要求--或者说它甚至可能是最重要的要求。但是,为初创企业建立一个灵活的架构意味着什么?

在我的职业生涯中,我为初创企业和企业系统设计(或参与设计)了许多后端架构。所有这些经验都用于设计和重新设计我们自己的应用后台。在这篇博文中,我想分享我们的讨论、争论和不安全性,以及由此产生的架构,还有我们面临的挑战和妥协。我将介绍对我们有效的架构,以及如果让我重新做一遍架构,我会在其中改变些什么。然而,请注意,这篇文章并不是适合所有应用程序的权威手册。它也是相当有个人想法,其中所介绍的方法可能会产生争议。

我们的应用程序是做什么的?

简单来说,该应用程序允许用户创建所谓的“事件”。这些是临时组成的团体,不是围绕着一组固定的人,而是围绕着一个场合,更像是一个聚会或一个派对。用户可以邀请人们参加活动,被邀请的用户可以接受或拒绝邀请。一旦用户接受了邀请,他们就成为了事件的一部分,可以在聊天中阅读和写作,并参与进一步的计划。每个事件都有一个GIF、视频或图片作为其背景媒体。除此以外,用户还有个人资料,并可以互相交朋友。后台系统的一个出奇复杂的部分只与推送通知有关。这些通知在大量的场合被发送: 从朋友请求到新的聊天信息、反应、接受的邀请、时间或地点的变化,或者作为公开邀请和即将发生的事件的提醒。

该应用程序现在具有相当复杂的后端架构,涵盖至少十几个服务以及在它们之间共享数据和事件的多种方式。所有这些都是由一个人建立的,并且所有这些都能顺利运行。在写这篇文章的时候,整体的复杂程度要比一开始大得多。但是因为一些小的功能和改进,需要在服务之间进行大量的信息来回传递。上面提到的应用程序的主要功能几乎都是我们从一开始就建立的。

微型服务通常不适用于初创公司

当我们开始建立häpps的时候,微型服务的风头很大并且很深地影响了我们的讨论。我们很快就同意,微型服务这个词有一点误导性,因为它似乎暗示着将服务切得越小越好。将你的代码库分割成多个独立的服务有很多缺点: 每个服务都必须被维护和部署,并且增加了共享数据结构、复杂的通信协议和基础设施所需的模板。当我们发布我们的应用程序时,大多数服务所使用的通用库已经被更新了一百多次。而每个服务最终都要效仿这一行为。

而且: 对于一些企业规模的系统来说,多服务架构的大部分优势对初创企业来说根本没有意义。

最重要的是: 独立部署服务的能力对早期创业公司来说并不重要。如果你只有一个或两个团队,你就相当于没有自主团队。在这种情况下,多个服务的单独部署就是一种障碍,而并非一种优势。只要每个人都能同时在所有终端工作,而且你不会因为数百名用户同时使用你的平台而面临收入损失的风险,那么就不用担心整个系统崩溃。

如果你足够幸运,单体应用的横向扩展不足以维持后端运行,那么你也将不得不重写大部分业务逻辑,并在数据结构、数据库查询、缓存和序列化方面进行性能改进。如果您从相对较小的用户群开始,使用最新的技术和框架,您应该能够轻松地将您的系统扩展两到三个数量级,然后再更改您的宏观架构。

因此,所有这些听起来都像是反对多服务架构的有力案例——事实确实如此。但正如你所看到的,我们最终还是想通了。你仍然有理由拆分你的后端。而且还有更好和更坏的方法来做。如果使服务在其不同的领域内尽可能小不是提出解决方案,那么我们要如何确定合理的边界?

行动起来,把事情外包出去

在你的应用程序后端,有很多组件--虽然很关键--但与你的应用程序的核心领域无关。这些也是你不应该自己建立的服务:

  • 很显然,认证服务
  • 推送通知或云信息分发器
  • 媒体服务,存储上传的图片并负责缩小、裁剪、缩放和缓存等工作
  • 一个聊天服务器(?)

在这个项目中,我最喜欢做的决定之一是就使用谷歌Firebase。我们都知道我们并不想把它用于数据存储或计算(而且我们很高兴我们没有这样做!)。但我们实施了Firebase Auth,所以我们可以使用他们的简单工具包来处理所有与授权相关的事情,包括社交登录和密码重设。这些都是你作为一个必须提供功能的应用开发者,为了确保安全而不能花费更多时间去做的事情。

我们还使用了谷歌Firebase云信息服务,以拥有一个单一的界面,向安卓和IOS设备同时发送推送通知。这个决定的确在一段时间内为我们提供了很好的服务,但谷歌缺乏灵活性,无法满足我们不断增长的推送通知的复杂性,使我们开始怀疑这个决定是否正确。也许我们使用OneSignal这样的系统会更好。

我们最终使用了一个自行开发的服务来处理我们上传的媒体,原因是我们在一个有类似要求的旧项目中使用过它。如果你还没有的话,我建议使用CDN或某种可随时部署的开源容器来完成这项工作。当你试图把你的用户的数据使用量保持在合理的范围内时,媒体管理就会变得相当复杂。

聊天服务器在某种程度上来说是一个指向不明的案例。虽然建立一个聊天是一个相当复杂的问题,但它也是一个之前已经被解决过数百次的问题。我们就是否应该实施一个现成的聊天服务器解决方案或在一个更低级的WebSocket框架上建立自己的解决方案进行了长时间的讨论。我们最终决定采用后者,我也很高兴我们这样做了。总而言之,事实证明,聊天服务器的实际聊天部分比我们想象的要容易实现(但这只适用于后端!)。另一方面,聊天原来与其他应用程序的许多核心功能紧密相连,所以在一个通用产品上构建会变得越来越困难。

既然如此,我们就来谈谈后端系统的可重用组件。

考虑建立可反复使用的东西

在一个应用程序之外的层面上考虑可重用性,可能看起来有点夸张--但我向你保证,这并不是。你可能相信你现在构建的东西会一炮而红,但更有可能的情况是,你失败了,从中吸取教训,最终再次构建东西。但即使不是这样--如果你正在构建的产品会是一个成功的产品,构建一些可重复使用的东西也将提高你的能力,以适应你的MVP得到的任何用户反馈。

当我们开始建立我们的第二个应用程序时,很明显,我们希望其能尽可能快地产生进度,以便能够验证我们的想法。我们决定使用熟悉的技术和一个更简单的架构。我们还研究了旧的项目以寻找可重复使用的代码。我们的媒体服务--一个处理图片上传和提供静态媒体的简单的Python应用程序--是一个可以在稍作修改后重复使用的组件。

我们知道,这一次可能不是我们最后一次决定建立一个产品--无论它是否会成功。所以我们决定尝试建立可重复使用的服务和组件。

并非每个服务都可以或应该在构建时考虑到可重用性。 我见过最多的错误之一是倾向于为非常具体的东西构建通用的东西:你的领域逻辑。这样做的结果是会一个过于复杂的、模板式的代码库,会使你的产品开发速度减慢。而思考这个问题也会推动你走向正确的方向。系统的哪一部分确实代表了您产品的核心业务逻辑?哪一部分不是?你的聊天服务器的核心部分是什么?它不应该对你的应用程序有任何特殊的了解。用户配置文件服务?不用关心它管理什么样的配置文件。推送通知网关?它只管理设备代币和转发通知到Firebase。异步消息中心?它的工作是注册客户端并传递任意的JSON消息。所有这些系统都可以以一种可重用的方式建立。它们应该是这样的! 这并不一定意味着它们都必须是由HTTP APIs连接的独立服务。如果你承诺使用一种编程语言和一个网络框架,你不妨将它们作为模块来构建,并将它们很好地隔离开来,以便在未来的任何时候都能将它们从你的独立体中分离出来。

你不用有压力地让所有的东西都尽可能的可重复使用。毕竟,你的目标仍然是快速交付,而且最终也不确定你是否会在其他地方使用到这些系统。但是,考虑可重用性也会迫使你从领域逻辑和隔离的角度来考虑你的整个后端系统,这一点在以后会给你回报。

要务实

因为并不是所有在纸上讨论的东西在实践中都是有趣的。基于妥协和纯粹的实用主义,我们最终拆分了一些系统并加入了些其他系统。

我们的交友服务就是其中的一个例子。该应用程序使用户能够添加他人为朋友。所以在本质上,相应的服务只用于管理用户ID之间的关系。理论上,它可以用于所有种类的应用程序,所以一开始,它就是一个独立的服务。但我们很快就意识到,它与用户资料服务是紧密耦合的: 通常情况下,如果一个用户资料的列表被返回给客户端,那列表中的每个资料都应该包括一个标志,表明该用户是否是请求列表的用户的朋友。另一方面,当一个好友列表从交友服务中返回时,它通常应该包括诸如用户姓名之类的资料信息。我们最终将这些服务合二为一,但将它们的逻辑放在不同的模块中。

人很容易让自己迷失在孤立系统的思维循环中。可以考虑一个应该返回朋友建议列表的 API。这些建议可能包括朋友的朋友--存储在友谊服务中的信息。但在第二次迭代中,你可能还想推荐那些与用户参加过同一事件的人。这些信息来自一个完全不同的系统,因此很容易产生争论,友谊建议实际上应该是一个基于自己的微型服务,因为它结合了来自不同领域的数据。话又说回来,将用户配置文件列表连接到单个建议列表所需的最少量业务逻辑并不能证明维护和部署它自己的服务是合理的。在这样的情况下,也许是时候换个角度,在其他独立的系统之间实现一些领域意识。这看起来不美观,但实际很有效: 让您现有的一项服务返回用户的友谊建议,即使它不是最合适的。在我们的案例中,我们决定让happenings服务来做这项工作,以保持友谊服务中属于另一个领域的代码的干净。你也可以选择反过来做,或者建立一种多项服务,作为小型服务的集合来工作。

同步/异步通信模式的边界

将你的系统分成服务的另一个原因某种程度上来说与可扩展性有关。考虑到一个复杂的动作会触发更多复杂的、异步的副作用。

以一个决定加入一个事件的用户为例。以决定加入事件的用户为例。 在事件服务层面,在后端向所有注册的客户端发送异步更新提示,要求他们从服务器重新加载更新的事件之前,将在用户和事件之间创建一个新的关系。但是这个动作会引发很多其他的副作用: 一个系统消息将被发送到聊天后台,以便在聊天中显示一个信息消息--但只有在该功能被激活的情况下。个性化的推送通知将被发送至未知数量的朋友和其他相关发生的成员。这又需要查询数据库,了解发生和用户之间的其他关系、现有的友谊、用户档案和通知设置。该系统还必须更新每个能看到该事件的人的IOS设备上的通知徽章数量。然而,确定正确的徽章数量,需要重新计算或收集未读聊天信息的总数量和其他一些信息。

同步执行多个任务会导致请求时间过长,最终导致请求失败。此外,这些任务产生的副作用的数量往往比核心功能的数量增加得更快。即使你自己写了项目中的每一行代码,要确定有多少其他行为是由复杂的行为引发的也将会是一个挑战。

从大局出发,将这些东西解耦通常是一个明智的决定。沿着这些边界拆分系统,例如通过使用像 rabbitMQ 或 Kafka 这样的消息代理可以在以后的可扩展性方面具有优势。然而,在产品开发的早期阶段,通常只使用一个应用程序并使用内部事件发射器就足够了。

我们是如何做到的,以及我们下次会如何做

基于以上讨论的所有考虑,我们最终将我们的后端系统分为多个服务。用于认证、推送通知、媒体管理和监控的二级服务都是没有问题的。

我们还决定按照通信模式和可重用性考虑将系统分开。然而,我们很快就学到了实用主义的一课: 那些相互之间有很多交流的系统--而且只是同步的--一开始就不应该被分开,即使它们的领域建议这么做。相反,严格的模块化会让你走得更远。

我们仍然对我们选择的架构感到非常的满意。它运行平稳,扩展毫不费力,特别是当我们增加新功能时。即使它涉及到多个领域服务的相互通信,我们几乎没有遇到过与服务间通信相关的问题。这主要归功于我们全面的端到端测试覆盖,以及我们在服务间直接而稳健的通信模式。

但这也需要大量的工作: 我们花了相当多的时间来开发一个通用库,它里面不包含任何领域逻辑,但包含服务间通信所需的所有接口和数据结构。这也是为了保持工作步骤简介而做出的妥协。

开发适合创业公司的强大的服务间通信协议也需要时间。所有这些都是我们迟早要做的好工作,如果我们的应用程序会取得巨大的成功--但它并没有。这也是我们的收获。

这就是为什么,如果我们不得不重新做一遍,这次我会选择一个稍微不同的方法。本着加快实现最小可行产品的精神,我们仍有余地削减花哨的架构模式,建立一个更简单的后端架构--你猜对了--我们的老朋友,单片机。

在建立和部署一个多服务景观的过程中,我们学到了很多关于软件架构的知识。我们在如何以最小的努力削减、开发和维护一个多服务景观方面获得了珍贵的经验。我们还从我们设计的服务间通信模式中学到了很多。然而,如果你的目标不是学习分布式系统的架构,而是有效地运送产品,那么选择更加单一的方法能学到更多。如果我们重新开始,我会选择建立一个单一的系统,该系统由隔离良好的模块组成,它们之间的API不显眼。为了取代像rabbitMQ这样独立的消息代理,至少一开始我会选择一个本地的事件发射器。这种方法可以为我们节省大量的时间,我们可以把这些时间花在新的功能上--或者花在我们被严重忽视的地方--市场营销上。

灵活性

多年以来,我们学到的最宝贵的经验是,你必须快速行动。这尤其适用于产品开发的早期阶段。你几乎肯定会对你的产品和它的功能究竟会怎么被用户接受而犯一些错。很少有想法会真正最终被实验成功,至少在第一次的时候。在这个意义上,拥有高度的灵活性也意味着: 无论你建造的是什么,都应该足够便宜,以便在用户不适应的情况下被抛弃而损失很小,但又足够坚实,以便在它成功的情况下可以很容易地改进和建立。目标是尽可能少花时间在未经验证的功能上,这样你就有尽可能多的时间来跟进用户喜欢的功能。

评论

所有评论

推荐阅读