或者说,每一个拥有巨无霸的不开心的公司都有自己不开心的方式。
就像 Dodo Pizza 业务一样,Dodo IS 系统的开发于 2011 年立即开始。 它基于业务流程完全数字化的理念,并且
本文是“为什么要重写架构并进行如此大规模和长期的改变?”这个问题的“答案”。 到上一篇文章
系列文章“什么是 Dodo IS?” 讲述:
-
Dodo IS 的早期巨石(2011-2015)。 (你在这里)
-
客户端路径:基础立面(2016-2017)。 (进行中...)
-
真正的微服务的历史。 (2018-2019)。 (进行中...)
-
完成整体结构的锯切和结构的稳定。 (进行中...)
原始建筑
2011 年,Dodo IS 的架构是这样的:
该架构中的第一个模块是订单接受。 业务流程是这样的:
-
顾客给比萨店打电话;
-
经理拿起电话;
-
通过电话接受订单;
-
同时,它会在订单接受界面中填写:有关客户的信息、订单详细信息数据和送货地址。
信息系统界面是这样的……
2011 年 XNUMX 月的第一个版本:
开发第一个接单模块的资源有限。 有必要以小团队的方式快速完成大量工作。 这个小团队由2名开发人员组成,他们为整个未来系统奠定了基础。
他们的第一个决定决定了技术栈未来的命运:
-
后端采用 ASP.NET MVC、C# 语言。 开发人员都是 dotnetters;这个堆栈对他们来说很熟悉并且令人愉快。
-
Bootstrap 和 JQuery 上的前端:基于自定义样式和脚本的用户界面。
-
MySQL数据库:无许可费用,易于使用。
-
Windows Server 上的服务器,因为 .NET 只能在 Windows 上运行(我们不会讨论 Mono)。
从物理上来说,这一切都体现在“主人的办公桌”上。
订单接受应用程序的架构
那时大家已经在谈论微服务了,SOA在大型项目中的应用也已经有5年左右的时间了,比如2006年就发布了WCF。 但随后他们选择了可靠且经过验证的解决方案。
这里是。
Asp.Net MVC 是 Razor,它根据表单或客户端的请求生成一个 HTML 页面并在服务器上呈现。 在客户端,CSS 和 JS 脚本已经显示信息,并在必要时通过 JQuery 执行 AJAX 请求。
服务器上的请求属于 *Controller 类,其中的方法处理并生成最终的 HTML 页面。 控制器向称为*服务的逻辑层发出请求。 每项服务都对应于业务的某些方面:
-
例如,DepartmentStructureService 提供有关比萨饼店和部门的信息。 部门是由一个特许经营商管理的一组比萨饼店。
-
ReceivingOrdersService 接收并计算订单内容。
-
而SmsService则通过调用发送短信的API服务来发送短信。
这些服务处理来自数据库的数据并存储业务逻辑。 每项服务都有一个或多个具有适当名称的*存储库。 它们已经包含对数据库中存储过程的查询和映射器层。 存储库具有业务逻辑,尤其是其中许多生成报告数据的存储库。 没有使用ORM,大家都是靠手写sql。
还有一层领域模型和通用帮助器类,例如存储订单的 Order 类。 在图层中,有一个帮助程序,用于根据所选货币转换显示文本。
所有这些都可以用这个模型来表示:
订购方式
让我们考虑创建此类订单的简化初始方法。
最初该网站是静态的。 上面有价格,顶部有一个电话号码和铭文“如果你想要披萨,请拨打该号码并订购”。 为了订购,我们需要实现一个简单的流程:
-
客户访问带有价格的静态网站,选择产品并拨打网站上列出的号码。
-
客户指定他想要添加到订单中的产品。
-
给出了他的地址和姓名。
-
操作员接受订单。
-
订单显示在已接受订单界面中。
这一切都从菜单显示开始。 登录的操作员用户一次仅接受一个订单。 因此,草稿车可以存储在其会话中(用户的会话存储在内存中)。 有一个包含产品和客户信息的 Cart 对象。
客户给产品命名,操作员点击 +
旁边的产品,并向服务器发送请求。 从数据库中提取有关产品的信息,并将有关产品的信息添加到购物车中。
注意。 是的,这里你不必从数据库中拉出产品,而是从前端传输它。 但为了清楚起见,我准确地展示了从基地出发的路径。
接下来,输入客户的地址和姓名。
当您点击“创建订单”时:
-
我们将请求发送到 OrderController.SaveOrder()。
-
我们从会话中获取购物车,其中有我们需要的数量的产品。
-
我们用有关客户端的信息补充 Cart,并将其传递给 ReceivingOrderService 类的 AddOrder 方法,并在该方法中将其保存到数据库中。
-
数据库中有包含订单、订单内容、客户端的表,它们都是相互连接的。
-
订单显示界面去,拉出最新的订单并显示。
新模块
收到订单是重要且必要的。 如果没有销售订单,您就无法经营披萨业务。 因此,该系统大约从 2012 年到 2015 年开始获得功能。 在此期间,系统出现了许多不同的块,我将其称为 模组,与服务或产品的概念相对。
模块是由一些共同业务目标联合起来的一组功能。 此外,它们在物理上位于同一应用程序中。
模块可以称为系统块。 例如,这是一个报告模块、管理界面、
从技术上讲,这些模块被设计为区域(这个想法甚至保留在
...对此:
有些模块是由单独的站点(可执行项目)实现的,因为功能完全独立,部分原因是有些独立、更集中的开发。 这:
-
Site -
第一版 网站 dodopizza.ru。 -
出口:从 Dodo IS 下载 1C 报告。
-
个人方面 — 员工的个人账户。 它是单独开发的,有自己的切入点和单独的设计。
-
fs — 静态托管项目。 后来我们放弃了它,将所有静态内容移至 Akamai CDN。
其余块位于 BackOffice 应用程序中。
名称解释:
-
收银员 - 餐厅收银台。
-
轮班经理 - “轮班经理”角色的界面:比萨饼店销售的运营统计、将产品列入停止清单的能力、更改订单。
-
OfficeManager - “比萨店经理”和“加盟商”角色的界面。 在这里,您可以找到用于设置比萨饼店、奖金促销、接待员工并与员工合作以及报告的功能。
-
PublicScreens - 挂在比萨店的电视和平板电脑的界面。 电视在交货时显示菜单、广告信息和订单状态。
他们使用公共服务层、Dodo.Core 域类的公共块和公共基础。 有时他们仍然可以通过通道互相引导。 此外,dodopizza.ru 或personal.dodopizza.ru 等个别站点也访问公共服务。
当新模块出现时,我们尝试尽可能多地重用已经为服务、存储过程和数据库中的表创建的代码。
为了更好地了解系统中模块的规模,下面是 2012 年的图表和开发计划:
到 2015 年,一切都步入正轨,更多的作品正在投入生产。
-
订单接受已发展成为联络中心的一个单独部分,由操作员接受订单。
-
比萨店里出现了带有菜单和信息的公共屏幕。
-
厨房有一个模块,当新订单到达时,会自动播放语音消息“新披萨”,并为快递员打印发票。 这大大简化了厨房的流程,让员工不会因为大量简单的操作而分心。
-
送货区域变成了一个单独的送货收银台,订单被发送给之前轮班的快递员。 计算工资时考虑了他的工作时间。
与此同时,从 2012 年到 2015 年,出现了 10 多家开发商,开设了 35 家披萨店,系统部署到罗马尼亚,并准备在美国开设点。 开发人员不再参与所有任务,而是分成团队。 每个人都专注于系统的自己的部分。
问题
包括因为架构(但不仅仅是)。
基地内一片混乱
一个基地就很方便。 实现一致性是可能的,但代价是牺牲关系数据库中内置的工具。 使用它既熟悉又方便,特别是在表和数据很少的情况下。
但经过 4 年的开发,该数据库包含约 600 个表、1500 个存储过程,其中许多还具有逻辑。 不幸的是,存储过程在使用 MySQL 时并没有提供太多好处。 它们不被数据库缓存,并且在其中存储逻辑使开发和调试变得复杂。 重用代码也很困难。
许多表没有合适的索引,某处反而索引很多,导致插入困难。 大约 20 个表必须修改——创建订单的事务可能需要大约 3-5 秒。
表中的数据并不总是最合适的形式。 在某些地方有必要进行非规范化。 一些定期接收的数据以 XML 结构形式存储在列中,这增加了执行时间、延长了查询时间并使开发变得复杂。
相同的表受到非常 异构请求。 热门表,如上面提到的表,受到的影响尤其严重 订单 或桌子 比萨店。 它们用于显示厨房和分析中的操作界面。 该网站还联系了他们(
数据未汇总 许多计算都是使用该基础即时进行的。 这造成了不必要的计算和额外的负载。
通常,代码在无法执行操作时会进入数据库。 在某些地方缺乏批量操作,在某些地方需要通过代码将一个请求拆分为多个请求,以加快速度并提高可靠性。
代码中的凝聚力和混乱
那些本该负责自己那部分业务的模块却没有老老实实的去做。 其中一些角色的职能重复。 例如,负责所在城市网络营销活动的本地营销人员必须使用“管理员”界面(设置促销)和“办公室经理”界面(查看促销对网络的影响)。商业)。 当然,两个模块内部都使用相同的服务,该服务与奖金促销一起使用。
服务(一个整体大型项目中的类)可以相互调用以丰富其数据。
使用存储数据的模型类本身, 代码中的工作以不同方式进行。 某个地方有一些构造函数,您可以通过它们指定必需的字段。 在某个地方,这是通过公共财产完成的。 当然,从数据库获取和转换数据是多种多样的。
逻辑位于控制器或服务类中。
这些看起来都是小问题,但它们极大地减慢了开发速度并降低了质量,导致不稳定和错误。
大开发的复杂性
开发本身就出现了困难。 有必要并行地创建系统的不同块。 将每个组件的需求融入到单个代码中变得越来越困难。 同时同意并取悦所有组成部分并不容易。 除此之外,还存在技术限制,特别是在基础和前端方面。 有必要放弃 JQuery,转而使用高级框架,特别是在客户端服务(网站)方面。
系统的某些部分可以使用更适合此目的的数据库。 例如,后来我们有从Redis切换到CosmosDB来存储订单车的先例。
在他们的领域工作的团队和开发人员显然希望他们的服务在开发和部署方面具有更多的独立性。 合并期间的冲突,发布期间的问题。 如果对于 5 个开发人员来说这个问题无关紧要,那么对于 10 个开发人员,甚至随着计划的增长,一切都会变得更加严重。 接下来是移动应用程序的开发(2017 年开始,2018 年有
系统的不同部分需要不同的稳定性指标,但由于系统的连接性很强,我们无法提供这一点。 在管理面板中开发新功能时出现的错误很可能会导致网站接受订单,因为代码是通用且可重用的,数据库和数据也是相同的。
在这样的整体模块化架构的框架内,可能可以避免这些错误和问题:创建职责分离,重构代码和数据库,明确地将各层彼此分开,并每天监控质量。 但所选择的架构解决方案以及对快速扩展系统功能的关注导致了稳定性方面的问题。
博客 Power of Mind 如何在餐厅安装收银机
如果比萨饼店网络(和负载)继续以相同的速度增长,那么一段时间后,下降将导致系统无法恢复。 下面的故事很好地说明了我们在 2015 年开始面临的问题。
在博客中“
-
网络中比萨饼店的数量;
-
自年初以来的网络总收入;
-
今天的收入。
收入统计请求直接进入数据库,开始请求订单数据,直接即时汇总数据并发出金额。
餐馆的收银机进入同一个订单表,上传今天接受的订单列表,并添加新订单。 收银机每 5 秒或在页面刷新时发出请求。
该图如下所示:
秋天的一天,费奥多尔·奥夫钦尼科夫(Fyodor Ovchinnikov)在他的博客上写了一篇很受欢迎的长文。 很多人来到博客并开始仔细阅读所有内容。 当每个来的人都在阅读这篇文章时,收入小部件正常工作并每 20 秒请求一次 API。
该 API 调用一个存储过程来计算网络中所有比萨店自年初以来的所有订单量。 聚合基于非常流行的订单表。 当时所有营业的餐馆的收银台都去它那里。 收银机停止响应,订单不被接受。 它们也没有被网站接受,没有出现在跟踪器上,轮班经理也无法在他的界面中看到它们。
这不是唯一的故事。 到 2015 年秋天,每个星期五系统的负载都非常严重。 有好几次我们关闭了公共 API,有一次我们甚至不得不关闭该网站,因为没有任何帮助。 甚至还有一份服务列表,其中列出了重负载下的关闭顺序。
从这时起,我们与负载和系统稳定性的斗争就开始了(从2015年秋季到2018年秋季)。 就在那时,事情发生了”
业务快速增长
为什么不能“马上办好”呢? 看看下面的图表就知道了。
2014-2015 年,我们还在罗马尼亚开设了一个空缺职位,并正在准备在美国开设一个空缺职位。
该连锁店发展得非常快,新的国家开业,新的披萨店业态出现,例如,在美食广场开设了一家披萨店。 所有这些都需要特别关注扩展 Dodo IS 的功能。 如果没有所有这些功能,没有在厨房中进行跟踪,没有在系统中计算产品和损失,没有在美食广场大厅中显示订单的交付情况,我们现在不太可能谈论“正确”的架构和“正确的”架构。 ”的发展方针。
及时修改架构和普遍关注技术问题的另一个障碍是 2014 年的危机。 这些事情损害了团队的成长机会,特别是对于像 Dodo Pizza 这样的年轻企业来说。
有帮助的快速解决方案
问题需要解决方案。 按照惯例,解决方案可分为 2 组:
-
快速的方法可以扑灭大火,给我们带来一点安全边际,并为我们赢得改变的时间。
-
系统性的,因此也是长期的。 重新设计了一些模块,将单体架构划分为单独的服务(大部分不是微观的,而是宏观的服务,这个还有更多
安德烈·莫列夫斯基的报告 ).
快速变化的干燥清单是:
扩大基地主
当然,为了应对负载,首先要做的就是增加服务器功率。 这是针对主数据库和 Web 服务器完成的。 唉,这只能在一定限度内实现;超出限度就变得太昂贵了。
从2014年开始,我们已经迁移到Azure;我们当时在文章中也写过这个话题“
用于读取的数据库副本
我们为底座制作了两个复制品:
读副本 用于目录请求。 它用于读取目录,例如城市、街道、比萨店、产品(缓慢更改的域),以及在那些可以接受小延迟的界面中。 这些副本有 2 个,我们以与主副本相同的方式确保它们的可用性。
用于报告请求的读取副本。 该数据库的可用性较低,但所有报告都转到它。 它们可能对大量数据重新计算有大量请求,但不会影响主数据库和操作界面。
代码中的缓存
代码中的任何地方(根本)都没有缓存。 这导致对加载的数据库产生额外的(并不总是必要的)请求。 最初,缓存既位于内存中,也位于外部缓存服务(即 Redis)上。 一切都随着时间的推移而失效,设置是在代码中指定的。
后端多台服务器
应用程序的后端也必须进行扩展以承受增加的负载。 有必要从一台 IIS 服务器创建一个集群。 我们搬家了
结果,架构变得更加复杂......
……不过,紧张的心情也得到了一些缓解。
然后需要重做加载的组件,这就是我们所做的。 我们将在下一部分讨论这个问题。
来源: habr.com