城的灯

分布式系统研发心得

随着SOA、微服务等诸多概念的炒作,再加上Dubbo、Spring Cloud、Netflix OSS等诸多开源软件的支撑,要构建一套大型的分布式系统的技术门槛已经变得非常之低。但是这并不意味着构建一套大型分布式系统就是很容易的,因为任何系统都是领域知识的体现。所以笔者将这些年系统研发过程中的一点感悟记录下来,仅供参考。


统一内部通用语言

Question:

  1. SPU=SKU

    1
    2
    A:把类目X下的SPU给我。
    B:把类目X下的SKU给了A。
  2. ID=CODE

    1
    2
    A:我给你商场ID,你给我商场信息。
    B:A给我商场CODE,我用商场CODE去查询。

为了解决这个问题,所有的团队成员都应该意识到需要创建一种通用的语言,并且时常提醒大家对基本的内容保持专注,在任何需要的时候都使用这种语言。我们应该尽量少地在这些场合使用我们自己的行话,应该使用通用语言,因为它能帮助我们更清晰、更精确地交流。

通用语言连接起设计中的所有的部分,为设计团队今后顺利开展工作建立了前提。 完成一个大规模项目的设计可能需要花费数周乃至数月的时间。团队成员会发现一些初始的概念是不正确的或者不合时宜,或者发现一些需要考虑并放进总体设计中的新的设计元素。没有一种公共的语言,所有的这一切都是不可能的。

这种语言的形成可不是一日之功,需要开展艰难的工作,重点在于确保发现语言的那些关键元素。我们需要发现定义领域和模型的那些关键概念,发现描述它们的适当用词,并开始使用它们。它们当中的一些概念可能很容易被发现,但另一些则不然。

通用语言的重要性,不言而喻,但是我们很多时候却并没有把它提升到一个非常重要的高度,这样导致了内部的沟通效率非常低下。在各种场合、组织内部强化约束这些通用语言的抽象和使用,它并不会立马提升效率,但是你得坚信这样做是值得的。

模型驱动设计(MDD)

产品经理(软件分析人员)和业务领域专家(老板 or 甲方)在一起工作了一段时间,一起找出来了领域的基础元素,理清了元素之间的关系,创建了一个正确的模型,这个模型的确正确捕获了领域知识。然后模型交给软件工程师。开发人员看模型时可能会发现模型中的有些概念或者关系无法使用代码来正确地表达。所以他们使用模型作为灵感的源泉,创建了自己的设计,虽然其设计借鉴了模型的某些思想,但是他们还增加了一些自己的东西。开发过程继续进行,更多的类被添加到代码中,进一步加大了原始模型和最终实现的差距。在这种情况下,很难保证产生好的结果。优秀的开发人员可能会做出一个能够工作的产品,但它能经得起时间的考验吗?它能被很容易地扩展吗?它能被容易地维护吗?当然还有更差的就是设计反推需求,工程师在设计系统的时候,发现流程走不下去,需要提新的需求,而这些需求软件分析人员根本就没有考虑到。这些问题从软件诞生以来就存在,只是在大型分布式系统被成倍的放大。

如果开发人员能够参加需求讨论会议,并在开始做编码设计之前对领域 知识和模型获得一个清晰完整的视图,他们后面的设计工作将会更有效率。最好的方法是将领域建模和系统设计紧密关联起来,领域模型在构建时就考虑到软件实现和设计,如果有问题及时调整和修正方向。这样就能获得一个有效的领域模型,程序员就能非常方便的将这种模型转化为代码。当然并不是这样做就能够保证沟通是没有任何障碍的,很多软件分析人员(产品经理)总以为程序员在刁难他们,因为他们不能理解建模中的难点和技术问题,这也是我坚持认为一个好的软件分析人员一定要写过几年代码。在我们目前的职业生涯中遇见过好几个工程师出身的产品经理,我经常拿他们跟非工程师出身的比较,不论跟程序员的讨论实现细节还是领域模型的抽象,他们的确具有一定优势。

当我们创建一个软件应用时,这个应用的很大一部分并没有直接与领域关联,但它们是基础设施的一部分或者是为软件本身提供服务的。最好能让应用中的领域部分与其余部分相比保持尽可能小(而不是和其余部分掺杂在一起),因为一个典型的应用包含了大量访问数据库、访问文件或网络、用户界面等相关的代码。

因此,将一个复杂的程序划分成多个层。为每一个层开发一个内聚的设计,让每个层仅依赖于它底下的那些层。遵照标准的架构模式实现与其上面的那些层的低耦合。将领域模型相关的代码集中到一个层中,把它从用户界面、应用和基础设施代码中隔离开来。领域对象不必再承担显示自己、保存自己、管理应用任务的职责,而是专注于表达领域模型。这会让一个模型逐渐进化得足够丰满、足够清晰,以便捕获基本的业务知识, 并且能够正常工作。

常用的分层架构方案如下:

说明
展示层 向用户展示软件的功能和信息
应用层 应用的逻辑的协调,不包含业务逻辑和业务对象的状态,包含会话的上下文
领域层 核心业务逻辑,业务对象状态保持,包含完整的领域信息
基础层 实体对象的持久化,公用库,基础服务

分层就必然面临着边界的划分问题,因为对于一个大型的分布式应用,每一层都是极其庞杂的,所以是先按领域划分模块然后再分层。各个模块清晰的被划分出来,各层被清晰的定义出来,这样就可以有效的避免代码混乱和降低管理成本。因此,将一个复杂的程序划分成多个层。为每一个层开发一个内聚的设计,让每个层仅依赖于它底下的那些层。遵照标准的架构模式实现与其上面的那些层的低耦合。将领域模型相关的代码集中到一个层中,把它从用户界面、应用和基础代码中隔离开来。领域对象不必再承担显示自己、保存自己、管理应用任务的职责,而是专注于表达领域模型。这会让一个模型逐渐进化得足够丰满、足够清晰,以便捕获基本的业务知识,并且能够正常工作。因此,将一个复杂的程序划分成多个层。为每一个层开发一个内聚的设计,让每个层仅依赖于它底下的那些层。遵照标准的架构模式实现与其上面的那些层的低耦合。将领域模型相关的代码集中到一个层中,把它从用户界面、应用和基础设施代码中隔离开来。领域对象不必再承担显示自己、保存自己、管理应用任务的职责,而是专注于表达领域模型。这会让一个模型逐渐进化得足够丰满、足够清晰,以便捕获基本的业务知识,并且能够正常工作。当实体、值对象、服务被清晰无误的定义出来,领域知识的落地就基本完成。

虽然内聚始于类和方法,它也可以用在模块级别。模块内聚一般分为两种,通信性内聚和功能性内聚,把操作相同数据的服务放在一个模块叫做通信性内聚,把具有强关联性的业务逻辑放在一个模块叫功能性内聚,功能性内聚被认为是最佳实践。不论如何聚合,我们还是会看到很多对象会跟其他的对象发生关联,形成了一个复杂的关系网,不论是一对一、一对多还是多对多。来自模型的挑战常常不是让它们尽量完整,而是让它们尽量地简单和容易理解。这意味着,除非直到模型中嵌入了对领域的深层理解,否则大多数时候需要对模型中的关系进行消减和简化。 首先,要删除模型中非基本的关联关系。它们可能在领域中是存在的,但它们在我们的模型中不是必要的,所以我们要删除它们。其次,可以通过添加约束的方式来减少多重性。如果很多对象满足一种关系,那么在这个关系上加入正确的约束之后,很有可能只有一个对象会继续满足这种关系。第三,很多时候双向关联可以被转换成非双向的关联。

接口设计原则

  1. 尽可能保证接口幂等性,不论是读还是写。读不需要多解释,写的并发问题,所以乐观锁等不错的思路。
  2. 接口要尽可能可以降级
  3. 需求确定之后,接口和数据模型先行,协议和字段需要在Java Doc和Swagger API Docs中详细描述
  4. 领域层的接口先从一个比较大的服务边界开始,然后随着时间推移基于业务需求来重构成更小的。我们应该关注微服务的范围,而不是一味的把服务做小。一个服务的(正确的)大小应该等于满足某个特定业务能力所需要的大小。他们应该是内聚而完整的。
  5. 接口需要向下兼容,所以接口都需要带上版本号,如果底层的数据结构发生了颠覆性变化,要充分考虑老版本接口问题,特别是C端产品。
  6. 接口要尽量保证无状态,如果的确有状态,可以按状态划分为多个接口。
  7. 不能过度抽象接口,例如public Map query(Map map)这类接口,维护和使用都非常困难。但是也不能不抽象,否者接口数会暴增,随着业务的稳定,有必要抽象合并。