创建可扩展的架构






4.94/5 (7投票s)
评估了我希望在新架构中看到的内容的文章。
引言
最近,在准备与同事就新项目的架构方法进行会议时,我开始写下我希望在新架构中看到的东西。我决定将其格式化,以便对处于相同境地的人有所帮助。
通用架构设计
在大多数应用程序中,都有某种用户界面(UI),应用程序通过该界面与应用程序用户进行交互。根据我的经验,注重用户界面的开发人员在处理架构的更深层时通常表现不佳。考虑到这一点,我尽量使 UI 开发人员将使用的数据和方法尽可能地精炼和简单。这意味着业务对象名称为“房屋”,属性为颜色,函数为保存。
鉴于业务对象将在 UI 中使用,在其中放入需要扩展的计算或任何业务流程是不可行的。因此,需要某种服务器层,该服务器层可以独立运行在任何机器上,用于操作业务对象。在我过去的项目中,这些服务器处理了与数据库的所有通信以及所有主要的数据处理。
随着系统需求的增长,这些服务器可以移动到它们自己的机器上,或者在多台机器之间进行复制。例如,可以有一个用于 UI 的房屋服务器,以及一个仅用于报告的第二个房屋服务器。它们都可以与同一个数据库服务器通信,或者报告服务器可以与数据库服务器的只读镜像副本通信。一旦走上这条路,可能性是无限的。
你的代码生成器
为了实现此处提到的所有内容,需要编写大量的代码,而代码生成器将是成功的关键。我通常会编写自己的生成器,但市面上也有一些非常好的商业化产品。需要记住的一点是,要将生成的代码与手写代码分开。这样,如果需要为所有对象添加新功能,您就可以确保不会覆盖您手写到某个对象中的内容。Microsoft 在其部分类中提供了很好的选择。
通用对象设计
总的来说,对象应该看起来像或具有与其代表的名词相似的属性。例如,客户将具有姓名、地址、电话号码、身高、体重、眼睛颜色等。这些属性通常会键入数据库表,然后使用代码生成器来构建具有表属性的对象,以及一些附加属性或函数,例如“保存”和“删除”。此外,代码生成器通常会生成一个包含单个对象倍数的对象。回到我们的客户示例,如果为客户表运行代码生成器,它可能会创建一个名为“Customer”的类,以及一个名为“Customers”的类,该类可能具有“Sort”、“Search”或“Bulk Insert”等属性。
对象通常反映它们所代表的名词以及它们所使用的屏幕或报告。也就是说,不要把你的对象强行塞进它们使用的屏幕或报告。而是开发一个适合屏幕的新对象。使用一个不太合适的对象是可以接受的,但如果搜索结果需要从多个表中提取几列,则创建一个新对象来处理搜索结果。这样,搜索结果将准确地反映您所需的内容,并可以减少序列化时间、网络带宽使用和数据库调用。
序列化
序列化是将存储在内存中的对象的值转换为一长串零和一的数组,然后可以通过 TCP/IP 传输或写入磁盘的过程。在 Microsoft 的 C# 中,这些零和一的数组称为二进制流。
序列化通常被认为是您当前使用的黑盒架构为您处理的魔术。对于小型到中型应用程序,序列化也不值得关注;只有当可扩展性成为问题时,才应按表、屏幕、作业或报告的基准来分析序列化过程。
多线程
多线程可以以多种方式定义,但最容易解释的方式是创建多个独立的代码块或工作,这些代码块或工作可以传递给机器的各个处理器或核心。
很多年前,除非需要主程序继续运行,否则多线程甚至都不是一个考虑因素。多核处理器的普及帮助多线程成为软件开发的前沿。通过使用多线程,对代码进行一些更改,可以使其运行速度比以前快 2 倍、4 倍,最终快 6 倍。就个人而言,我发现随着我承担的项目越来越大,性能问题越来越突出,多线程成为了成功与失败的区别。
另一方面,如果不正确地进行多线程,可能会弊大于利。创建的线程数应直接与可用的处理器数相关。通常,如果有一个处理器或核心在使用,并且为该处理器启动了四个工作线程,那么线程切换的开销将使代码运行得比将所有线程的工作合并成一个要慢。但是,有一个例外可以考虑:如果一个线程将有空闲时间,而处理器可以在第一个线程空闲时处理第二个线程的工作,那么保持线程分离可能会有益。此外,还必须考虑线程可能依赖的任何其他机器的处理能力。例如,如果创建 12 个线程在六个处理器上运行,但这 12 个线程依赖于对四处理器机器的调用,那么该系统可能无法针对理想性能进行优化。
此外,重要的是要考虑当前使用的机器将来可能被具有更多处理器的新机器取代。为了应对处理器变化的问题,代码应该能够为机器中的处理器数量进行自我优化;或者,至少应该有一个简单且集中的方法来告诉代码创建多少个线程。性能测试和调优将有助于确保该领域的成功。
通信方法
根据定义,如果创建一个可扩展的架构,那么在扩展时必须在机器之间建立某种通信方式。在我过去的项目中,我个人使用 Microsoft 的 .NET Remoting 技术在机器之间进行通信。鉴于我相信我能做得比微软做得更好(事实证明对于序列化来说是如此),我将在我的下一个项目中使用套接字通信。使用套接字通信需要自己编写请求/响应协议,但它是通用的,并且(理论上)任何使用任何开发平台的都可以发送请求对象,并返回一个包含该请求结果的响应对象。
将多线程与序列化结合使用
有多种方法可以使序列化处理大量数据。这些方法都涉及多线程。在所有情况下,序列化几乎都是相同的,但其他考虑因素将决定合适的方法。
在大多数情况下,将一个包含多个对象的数组分成更小的数组,这取决于最佳线程数。一旦对象被分成多个数组,就可以提交多个线程来执行序列化工作。现在数据已经被序列化,并且有多个(二进制流)的零和一数组,这时“其他考虑因素”就发挥作用了。根据目标,这些多个数组可以合并成一个,或者分成更小的数组,以优化下一个处理步骤。在编写数据库驱动的应用程序时,这是将序列化数据传递给另一个服务器以准备数据库的插入或更新语句的理想场所。此时,可能会遇到前面提到的“依赖项”之一,即需要考虑数据库服务器处理的工作量和类型所需的最大数据库连接数,并相应地调整序列化数组的数量。
数据规范化
数据或数据库的规范化是将数据分离成字段和表的过程,从而最大限度地减少数据重复。数据规范化的方式可以决定应用程序的成败。大多数人倾向于将数据规范化到极致,这会像规范化不足一样损害性能。
考虑到数据库表不一定需要反映业务对象。例如,程序员正在创建一个应用程序,该应用程序全天以 15 分钟的间隔跟踪多个设备的温度。常识表明,会创建一个包含键、设备键、日期/时间、温度的表。如果一天分为 15 分钟的间隔,程序员最终会为每个设备、每天写入 96 行数据,其中 96 行是设备键和日期的重复。这是大量必须过滤的无用数据,更不用说数据库服务器在索引所有冗余数据行时所承受的工作量了。为了解决这个问题,应该考虑每台设备、每天写入一行;包含键、设备键、基本温度,以及 95 个字节字段,这些字段反映了温度相对于基本值的变化,或者一个 95 字节的数组。如果温度在 15 分钟内不太可能变化超过 8 度,则字节字段或数组可以减半,字节的前半部分用于第一个 15 分钟,后半部分用于第二个 15 分钟。这样,数据库服务器的负载将大大减轻,但数据看起来与呈现给用户所需的数据完全不同。以正确的格式获取这些数据将给表示层带来不必要的压力。这就是可扩展架构将发挥作用的地方。创建温度对象,就像它们将在报告或屏幕上显示的那样;例如,如果您想要每 15 分钟一行。在这种情况下,表示层应该通过首选的通信方法调用温度对象序列化服务器。从数据库请求数据行,并使用数据库中的数据创建多个温度对象。如果请求多天或几周的数据,架构应该自动创建多个线程并将这些天的数据组传递给这些线程,这样,数百个对象可以同时快速创建并序列化,然后返回给表示层。
最小化数据库使用
开发人员倾向于过度利用他们可用的任何数据库。数据库只是存储数据的地方。因此,它不应该用于任何其他目的,除了尽可能快速有效地存储数据。应用程序不应该用数据库过程编写,这是与可扩展性背道而驰的。可扩展性意味着数据处理代码保留在可以从服务器(或多个服务器)运行的框架中。除非数据库可以将其扩展到多个服务器,否则不应将其放入数据库的存储过程中。
保存整个记录
开发人员常犯的一个错误是保存整个对象,即使只有一个小属性发生了变化。更好的选择是序列化一个包含数据的位数组,该数组将告诉应用程序序列化对象中包含哪些数据,不包含哪些数据。如果只序列化已更改的数据,并且只更新数据库中已更改的字段,那么硬件负载将大大减轻。
摘要
尽管所有这些信息可能都不是新的,但本文档应该能够帮助不同经验水平的开发人员。特定于平台的的信息被最小化,但我还借鉴了我个人的经验,其中大部分是在 Microsoft 的 C# 中,以帮助说明要点。