#9 转载:康威定律

2024-03-04

几乎所有我喜欢的软件架构师,都认同康威定律(Conway Law),认为这个定律非常重要,足以影响到所有系统。而且,你没法与之抗争,想要抗拒康威定律注定要失败。

康威定律的最好表述是:“任何系统的构成,都反映了设计这个系统的组织结构。”

它的出处是 Melvin Conway 在 1968 年写的一篇文章。后来,弗雷德·布鲁克斯(Fred Brooks)在著名的《人月神话》(The Mythical Man-Month)引用了这条定律。

Melvin Conway 观察到,软件系统的架构看起来与构建它的开发团队的组织结构非常相似。

最初的描述是,如果一个团队编写一个编译器,那么它将是一个单通道编译器;但是,如果两个团队共同开发,那么它将是一个双通道编译器。这个描述后来被发现,广泛适用于大量系统。

正如我的同事 Chris Ford 对我说的:“软件耦合是由人类交流促成的。” 如果我可以轻松与代码作者交谈,那么我就更容易对代码有更深入的了解,因此我的代码更容易耦合到该代码。

应对康威定律的第一步是不要与之抗争。我仍然记得一位技术主管,他刚刚被任命为 ​​ 一个大型新项目的架构师,该项目由分布在世界各地不同城市的六个团队组成。“我做出了第一个架构决定”,他告诉我:“就是这个系统将有六个主要的子系统。我不知道它们会是什么子系统,但肯定会有六个。”

为了适应康威定律,现在有一种策略,就是一旦定下软件架构,就相应改变组织结构,让紧密耦合模块的开发者更容易沟通。

#8 异常流量监控

2023-12-09

突然来了大量客户请求,我们无法判断这些请求是正常的,还是客户被攻击所致。为了避免产生严重客损,我们需要识别到这些突然出现的流量高峰,并进行通知以及执行一些处理措施。

客户配置

比如:

request_limit = {
  rate: {
    // 客户配置
    3600: 1000, // 指定 API 1 小时之内的请求量上限
    86400: 8000, // 指定 API 1 天之内的请求量上限
  },
  rate_computed: {
    // 根据之前的请求情况,结合客户配置计算出来的值(不小于客户配置)
    3600: 1000,
    86400: 8000,
  },
  strategy: {
    times_to_notify: 1, // 连续触发几次之后通知
    times_to_suspend: 3, // 连续触发几次之后暂停
    times_to_intercept: 12, // 连续触发几次之后拦截请求
  },
};
  1. 默认给所有客户开启最近 1 小时和最近 24 小时的限制。
  2. 速率控制以小时为单位([1, 24]

定时检查

  1. 定时执行
 5 10 15 20 25 30
35 40 45 50 55  0
  1. 查询过去 1 天有请求的客户清单,根据系统参数(可能还有业务上其他条件)跳过忽略检查客户

  2. 逐个客户查询最近 5 分钟内的请求量,并将 5 分钟数据统计到小时内
    然后逐个时间区间配置,判断总请求量是否超出预设值

end_min = (minute // 5) * 5
start_min = end_minute - 5
  1. 如果流量异常,按照指定策略触发相关事件

客户处理

PS:这个机制的目的是处理异常流量,如果正常的请求量上来了应该调整客户配置。

  1. 警告页面给出客户过去的请求情况,标出告警时间段的数据
  2. 让客户选择处理方案:
  3. 忽略并继续
  4. 清空暂停队列并继续
    可以查看下载受影响的请求信息
  5. 暂不处理
  6. 往后 24 小时之内,下一次触发告警的数量,提供一个默认值(当前请求量的 1.5 倍)
    另一种思路:如果客户选择继续请求,不用填下一次告警的数量,后面计算的时候直接跳过过去一段时间(比如 6 小时)的请求量。

#7 DDD: 领域驱动设计

2022-04-09

我们的业务逻辑实际上与程序架构、数据库、缓存等严重耦合在一起,我一直觉得这是一种糟糕的设计。在我的想象中,最完美的情况是程序的核心应该用一种接近自然语言的 DSL(领域特定语言)来完整描述业务逻辑。

开发业务的人,谁关心我们的对象是在堆上还是在栈上,谁关心物理服务器是什么型号,甚至我们连数据库提供的功能都不愿多用,想尽可能减少对指定数据库的依赖。现在的服务网格也在剥离程序自身的一些架构方面的控制逻辑。

随着程序设计不断地剥离非业务逻辑的趋势,在不远的未来,早晚会实现我所想象的这种开发模式。

可能说的有点远,有点大。至少,就程序员熟悉的分层这种拆分复杂逻辑的思想来说,我们应该尽可能只采用程序设计语言的基础语法,没有任何外部依赖地,描述业务逻辑。这样的代码不是更容易理解,更方便审计,更便于维护吗?同时还更健壮。

我觉得我的这些想法和 DDD 不谋而合。

注意:DDD 适用于领域复杂度高、需要长期维护和扩展的业务系统。但对于简单的 CRUD 应用,引入了更多抽象,带来不必要的复杂性,还是应该短平快一些。
注意:重点是灵活应用设计思想,让架构为业务服务,而不是为了架构而架构。

核心思想

  1. 程序的核心的是业务逻辑的描述,贴近业务,而不是整个系统围绕着数据库,然后业务逻辑和技术实现混杂在一起,分散在程序的各处,可能叫做 XXXController、XXXService、XXXUtil。
  2. 划分子模块(),不同子模块之间通过 API、消息队列、事件驱动等方式进行通信,而不是共享数据。
  3. 强调统一描述,对问题的描述中不要增加业务实现的部分,增加团队沟通成本。
    比如:订单状态包括用户未支付、用户已支付、商家确认、商家备货、商家发货、物流发货、物流配送、用户签收、订单完成、订单取消、订单退款等等状态,存储在数据库里面可能是一个 int 类型字段,
    如果在代码中订单状态变更写的是 setStatus(1)、setStatus(2)、setStatus(3),这样做是非常糟糕的。
    如果这样的表述出现在内部沟通中,除了做这个开发的人,其他人看到也是一脸懵。
    所以状态的状态

领域驱动设计

DDD(Domain-Driven Design,领域驱动设计)是一种软件设计方法,强调软件设计应该以业务领域为中心,而不是以技术为中心。DDD 由 Eric Evans 在 2003 年提出。

领域驱动设计(Domain-Driven Design,简称 DDD)是一种以 业务需求 为核心的软件设计方法,强调 构建符合业务逻辑的领域模型,并通过 明确的边界良好的架构设计 来提升软件的可维护性和可扩展性。DDD 主要包括两个核心部分:战略设计战术设计

战略设计(Strategic Design)

战略设计关注如何合理划分业务领域,确定子系统的边界,并定义它们之间的关系。核心概念包括:

界限上下文(Bounded Context)

界限上下文是业务系统中的一个独立单元,包含特定的业务逻辑、数据模型和规则。不同上下文之间通过清晰的接口 进行交互,以避免模型混乱。

领域(Domain)与子域(Subdomain)

  • 领域(Domain):指整个业务范畴,例如“电子商务”“订单管理”等。
  • 子域(Subdomain):将复杂业务拆分为多个独立子域,例如在电子商务系统中,可能包含“商品管理”“订单管理”“支付处理”等子域。
    • 核心子域(Core Subdomain):业务的核心竞争力,最需要投入设计和优化的部分。
    • 支撑子域(Supporting Subdomain):为核心业务提供支持的部分,例如 CRM、客服管理等。
    • 通用子域(Generic Subdomain):可以复用的领域,如用户认证、日志管理等。

战术设计(Tactical Design)

战术设计关注如何在代码层面 实现领域模型,确保业务逻辑的清晰表达和长期可维护性。核心概念包括:

领域模型(Domain Model)

领域模型是对业务逻辑的抽象,主要由以下组成部分:

  • 实体(Entity):具有唯一标识(ID)的对象,状态可能随时间变化,例如“订单”“用户”等。
  • 值对象(Value Object):无唯一标识,通常用于描述属性,如“地址”“货币金额”。
  • 聚合(Aggregate):由多个实体和值对象组成的业务单元,具有一致性约束。
  • 聚合根(Aggregate Root):聚合的核心实体,负责维护聚合内部的一致性,对外提供访问接口。

仓储(Repository)

仓储模式用于 管理实体的持久化,屏蔽底层数据库操作,提供对象级的访问方式。例如 OrderRepository 负责管理订单的存取。

领域服务(Domain Service)

当某些业务逻辑无法归属于单个实体时,使用 领域服务(无状态)。例如“计算订单折扣”可能涉及多个对象,可放在 OrderDiscountService 中。

领域事件(Domain Event)

领域事件用于 表示业务中的重要事件,支持异步处理和系统解耦。例如“订单已支付”可以触发多个后续操作(发货、通知用户等)。

其他类似名词

  • BDD(Business-Driven Development,业务驱动开发):强调通过业务需求来驱动开发过程,通常使用自然语言编写测试用例。
  • BDD(Behavior-Driven Development,行为驱动开发):强调通过行为来定义软件功能,通常使用 Gherkin 语言编写测试用例。
  • FDD(Feature-Driven Development,特性驱动开发):强调通过特性来驱动开发过程,每个特性都是一个可交付的增量。
  • TDD(Test-Driven Development,测试驱动开发):强调在编写代码之前先编写测试用例,通过测试驱动代码的实现。

参考资料与拓展阅读

#6 周数问题:今天是今年的第几周

2021-11-16

2021-01-01

TLDR:

  1. ISO 8601 标准:国际标准规定周一为一周的开始,包含 1 月 4 日的那一周为第一周
  2. Java: 使用 WeekFields.ISO 获取 ISO 标准周数,或使用本地化设置
  3. Python: 使用 strftime%W (本地化) 或 %V (ISO) 参数获取周数
  4. Go: 使用 ISOWeek() 方法获取 ISO 标准周数
  5. MySQL: 使用 WEEK() 函数的 mode 3 或 7 获取 ISO 标准周数

Java

import java.time.LocalDate;
import java.time.temporal.WeekFields;
import java.util.Locale;

public class WeekNumberExample {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2021, 1, 1);
        // 使用 ISO 标准计算周数(周一为一周开始)
        int weekNumber = date.get(WeekFields.ISO.weekOfYear());
        System.out.println("ISO week number: " + weekNumber);

        // 使用本地化周数计算
        WeekFields weekFields = WeekFields.of(Locale.getDefault());
        int localizedWeek = date.get(weekFields.weekOfYear());
        System.out.println("Localized week number: " + localizedWeek);
    }
}

Python

import datetime

# 说明不同格式化参数的含义:
# %Y - 年份 (基于日历年度)
# %W - 周数 (周一作为一周的开始,第一周是包含第一个周一的周)
# %G - 年份 (基于ISO周数)
# %V - 周数 (ISO周数,周一作为一周开始,第一周是包含1月4日的那一周)

for i in range(10):
    d = datetime.date(2021 - i, 1, 1)
    print((str(d), d.weekday(), d.strftime('%Y %W / %G %V')))
('2021-01-01', 4, '2021 00 / 2020 53')
('2020-01-01', 2, '2020 00 / 2020 01')
('2019-01-01', 1, '2019 00 / 2019 01')
('2018-01-01', 0, '2018 01 / 2018 01')
('2017-01-01', 6, '2017 00 / 2016 52')
('2016-01-01', 4, '2016 00 / 2015 53')
('2015-01-01', 3, '2015 00 / 2015 01')
('2014-01-01', 2, '2014 00 / 2014 01')
('2013-01-01', 1, '2013 00 / 2013 01')
('2012-01-01', 6, '2012 00 / 2011 52')

Go

package main

import (
    "fmt"
    "time"
)

func main() {
    // Go 中计算周数
    t := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)

    // 使用 ISO 标准计算周数
    _, week := t.ISOWeek()
    fmt.Printf("ISO week number: %d\n", week)

    // 使用 YearDay 计算基于年的周数
    yearDay := t.YearDay()
    weekFromYearDay := (yearDay + 6) / 7
    fmt.Printf("Week from year day: %d\n", weekFromYearDay)
}

MySQL 中的实现

WEEK(date[, mode]) 函数:

Mode First day of week Range Week 1 is the first week …
0 Sunday 0-53 with a Sunday in this year
1 Monday 0-53 with 4 or more days this year
2 Sunday 1-53 with a Sunday in this year
3 Monday 1-53 with 4 or more days this year
4 Sunday 0-53 with 4 or more days this year
5 Monday 0-53 with a Monday in this year
6 Sunday 1-53 with 4 or more days this year
7 Monday 1-53 with a Monday in this year

总结一下,就是三个维度:

  1. 周数从 0 开始还是从 1 开始
  2. 以周一算每周的第一天还是周日
  3. 按每周第一天开始算,还是按每周四天开始算
0 周日 每周第一天 0
2 周日 每周第一天 1
4 周日 每周四天 0
6 周日 每周四天 1

1 周一 每周四天 0
3 周一 每周四天 1
5 周一 每周第一天 0
7 周一 每周第一天 1
SELECT NULL AS Mode, WEEK('2021-01-01'), WEEK('2021-01-04')
UNION SELECT 0 AS Mode, WEEK('2021-01-01', 0), WEEK('2021-01-04', 0)
UNION SELECT 1 AS Mode, WEEK('2021-01-01', 1), WEEK('2021-01-04', 1)
UNION SELECT 2 AS Mode, WEEK('2021-01-01', 2), WEEK('2021-01-04', 2)
UNION SELECT 3 AS Mode, WEEK('2021-01-01', 3), WEEK('2021-01-04', 3)
UNION SELECT 4 AS Mode, WEEK('2021-01-01', 4), WEEK('2021-01-04', 4)
UNION SELECT 5 AS Mode, WEEK('2021-01-01', 5), WEEK('2021-01-04', 5)
UNION SELECT 6 AS Mode, WEEK('2021-01-01', 6), WEEK('2021-01-04', 6)
UNION SELECT 7 AS Mode, WEEK('2021-01-01', 7), WEEK('2021-01-04', 7);
Mode WEEK('2021-01-01') WEEK('2021-01-04')
NULL 0 1
0 0 1
1 0 1
2 52 1
3 53 1
4 0 1
5 0 1
6 53 1
7 52 1

我定义的每周迭代名称(例如:2020-01A)

  1. 每周第一天为周一。
  2. 按照周一属于哪个月计算。
import datetime
from datetime import timedelta

def get_week_iteration_name(date):
    """
    根据周一所属月份计算周迭代名称
    格式:YYYY-MM{A-Z}
    返回:str, date, date (迭代名称,周一日期,周日日期)
    """
    weekday = date.weekday()
    monday = date - timedelta(days=weekday)
    sunday = monday + timedelta(days=6)

    year = monday.year
    month = monday.month

    # 计算该月第几个周一
    # 找到该月第一个周一
    first_day = datetime.date(year, month, 1)
    first_monday_offset = (7 - first_day.weekday()) % 7
    first_monday = first_day + timedelta(days=first_monday_offset)

    # 如果第一个周一不在本月,说明该月第一天就是周一
    if first_monday.month != month:
        first_monday = first_day

    # 计算是第几个周一
    week_index = ((monday - first_monday).days // 7) + 1
    # 转换为字母标识 (A=1, B=2, ...)
    week_letter = chr(ord('A') + week_index - 1)

    return f"{year}-{month:02d}{week_letter}", monday, sunday

test_dates = [
    datetime.date(2021, 1, 1),   # Friday
    datetime.date(2021, 1, 4),   # Monday
    datetime.date(2021, 1, 8),   # Friday
    datetime.date(2020, 12, 28), # Monday
]
for d in test_dates:
    weekday = d.weekday()
    name, monday, sunday = get_week_iteration_name(d)
    print(f"{d} -> {name} {weekday + 1} (Mon: {monday}, Sun: {sunday})")

#5 什么是一个设计良好的系统?

2021-08-24

在满足需求、有良好的使用体验(基于真实的用户看法,不是开发者自己觉得)基础上,进一步提更高的要求:

  • 接口响应超时
  • CPU load 升高
  • 内存
  • GC 频繁
  • 死锁
  • 大数据量存储

一、三高(高可用 + 高性能 + 高并发)

1.1 高可用(可用性) Availability

可用性强调的是服务的总体持续时间,比如可用性级别为三个九(99.9%)意味着一年 365 天里总共停止服务 8.76 个小时。

可靠性 Reliability

  • 功能如预期运行(宁可抛出异常,不要默默地给错误运行)
  • 出现故障的频率
  • 出现故障也应该不对系统造成太大的影响
  • 可维修性:故障定位容易、修复步骤简单、修复成本低廉

和高可用的区别:

  1. 大部分时候,这两个指标都是密切相关的
  2. 如果我有一个电视机,每天都准时看儿童频道的《动漫世界》一个小时,然后过去三年也都用得挺好,没有送去修过(导致我看不了),那可以说高可用了(正常运行时间 / 预期运行时间),但是如果这个电视机每次看的时候可能隔一会儿需要拍一巴掌(中间出雪花的时间可以忽略),那这就不能说这个电视机是可靠了。
  3. 又比如说,我又买了另一个电视机,不需要拍巴掌了,我看的挺开心,不过才高兴两年,坏了,看不了了,楼下电器维修店的小哥搞不定,联系厂家售后维修,一下就是两周看不了电视。对于一个家用电器,两年坏一次可以说高可靠,但是可用性就有问题了((365 - 7) / 365 = 98%)。

  4. MTBF Mean Time Between Failure 无故障时间,每隔多久遇到一次故障

  5. MTTR Mean Time To Repair 修复时间,每次遇到故障需要维修多久
  6. MTTF Mean Time To Failure

  7. 根据当前的可用性级别,然后指定合理的可行性目标

  8. 操作规范/最佳实践
  9. 预防性维护 Preventive Maintenance
  10. 合理的 PM 计划:可实施,正确的时机
  11. 重点是影响最大的故障
  12. 不断的改进和优化

稳定性 Stability

  • 系统运行平稳(不一定如预期执行)

连续性 Continuity

故障出现之后的处理能力:故障能否快速修复,能否不影响主要业务的运行。

比如我有两台电视机,坏了一台,我还有另一台可以看,虽然另一台是之前淘汰的一台黑白电视,效果差点,好歹能用啊。
这台电视就是我们常说的灾难备份,或者说容灾备份。

1.2 高性能 HP Performance

  • 处理速度快
  • 资源占用低

1.3 高并发 HC Concurrency

  • 响应时间
  • 吞吐量
  • 并发用户数

二、安全性

  • 基础设施安全:操作系统、网络、数据库
  • 数据安全
  • 脱敏:降低数据泄漏所产生的消极影响

三、易用性

四、其他

3.1 可测试性

3.2 可维护性

运维方便,运营方便,二次开发方便

  • 代码注释与文档
  • 合理的日志
  • 监控与数据统计
  • 误操作发生率低
  • 可理解性:代码可读性,合理的技术框架/系统架构
  • 可拓展性:新增功能不需要修改现有结构的能力。分层,合理的耦合度(尽可能的高内聚、低耦合),合理的功能抽象,数据结构可拓展

3.3 可伸缩性 Scalable/Scalability

在系统不需要修改(顶多改改配置)的前提下,通过增加或减少硬件资源来动态提升或降低系统各项能力。

  1. 通常采用服务器集群的方式。
  2. 集群一上来,就和高可用有很大关联。

3.4 可扩展性 Extensibility

通过合理的设计,使得新增功能时,不需要改动现有功能,或者只需要对现有系统进行少量调整就行。
总之,将新功能对现有业务逻辑的影响降到最低。

3.5 可观测性

能够方便快捷的了解到程序运行情况。
合理的日志,指标数据的暴露(Pull/Push)。

#3 23 种设计模式

2019-05-26

什么是设计模式

https://en.wikipedia.org/wiki/Design_Patterns
https://en.wikipedia.org/wiki/Software_design_pattern

我们说的 23 中设计模式,是来自一本 1994 年出版的书:《Design Patterns: Elements of Reusable Object-Oriented Software》。
这本书的中文名字叫:《设计模式:可复用面向对象软件的基础》。
写这本书的四位作者,被人成为 GoF(Gang of Four),也就是四人帮的意思(因为我们这边的关系,这个词在西方也比较著名,所以...)。

其中定义了设计模式,就是类似的问题的一种解决方案,或者说优秀的设计风格、编码风格,提升代码重用,同时实现高内聚、低耦合,让程序会更加可理解,更加可拓展,更加可维护。

  1. 一般谈到设计模式,都是在面向对象编程。但是我理解这是一种编程思想,和面向对象并不是强绑定关系。这点需要更多的思考和总结。
  2. 这里只研究 GoF 的 23 种设计模式,不是说这就是设计模式的全部,这个可能每个人有不同的看法。
  3. 根据维基百科信息,GoF 后来想重新整理这本书,重新分类,增删一些模式,但是最终没有达成一致。
  4. 根据维基百科信息,有人认为本书提到的设计模式只是语言特性缺失,在其他语言中,部分模式是不需要的。
    1. 就好像我们教材上基于 C 语言的数据结构,有部分在现代语言中是不需要开发者去编码实现一样,编译器做的很好了。

六大原则 SOLID

  1. 单一职责原则(Single Responsibility Principle)
  2. 开闭原则(Open Closed Principle)
  3. 里氏替换原则(Liskov Substitution Principle)
  4. 迪米特法则(Law of Demeter),又叫:最少知识法则(Least Knowledge Principle)
  5. 接口隔离原则(Interface Segregation Principle)
  6. 依赖倒置原则(Dependence Inversion Principle)

有些资料中还加入了一个:合成复用原则 Composite/Aggregate Reuse Principle,减少继承,多用聚合。

  • https://zhuanlan.zhihu.com/p/110130347
  • https://cloud.tencent.com/developer/article/1650116

模式清单

创建型模式 Creational

  1. 工厂模式 Factory Pattern
  2. 问题:
  3. 方案:
  4. 效果:
  5. 抽象工厂模式 Abstract Factory Pattern
  6. 问题:
  7. 方案:
  8. 效果:
  9. 单例模式 Singleton Pattern
  10. 问题:
  11. 方案:
  12. 效果:
  13. 建造者模式 Builder Pattern
  14. 问题:
  15. 方案:
  16. 效果:
  17. 原型模式 Prototype Pattern
  18. 问题:
  19. 方案:
  20. 效果:

结构型模式 Structural

  1. 代理模式 Proxy Pattern
  2. 问题:
  3. 方案:
  4. 效果:
  5. 适配器模式 Adapter Pattern
  6. 问题:
  7. 方案:
  8. 效果:
  9. 桥接模式 Bridge Pattern
  10. 问题:
  11. 方案:
  12. 效果:
  13. 组合模式 Composite Pattern
  14. 问题:
  15. 方案:
  16. 效果:
  17. 装饰器模式 Decorator Pattern
  18. 问题:
  19. 方案:
  20. 效果:
  21. 外观模式 Facade Pattern
  22. 问题:
  23. 方案:
  24. 效果:
  25. 享元模式 Flyweight Pattern
  26. 问题:
  27. 方案:
  28. 效果:

行为型模式 Behavioral

  1. 责任链模式 Chain of Responsibility Pattern
  2. 问题:
  3. 方案:
  4. 效果:
  5. 命令模式 Command Pattern
  6. 问题:
  7. 方案:
  8. 效果:
  9. 解释器模式 Interpreter Pattern
  10. 问题:
  11. 方案:
  12. 效果:
  13. 迭代器模式 Iterator Pattern
  14. 问题:
  15. 方案:
  16. 效果:
  17. 中介者模式 Mediator Pattern
  18. 问题:
  19. 方案:
  20. 效果:
  21. 备忘录模式 Memento Pattern
  22. 问题:
  23. 方案:
  24. 效果:
  25. 观察者模式 Observer Pattern
  26. 问题:
  27. 方案:
  28. 效果:
  29. 状态模式 State Pattern
  30. 问题:
  31. 方案:
  32. 效果:
  33. 策略模式 Strategy Pattern
  34. 问题:
  35. 方案:
  36. 效果:
  37. 模板模式 Template Pattern
    • 问题:
    • 方案:
    • 效果:
  38. 访问者模式 Visitor Pattern
    • 问题:
    • 方案:
    • 效果:

参考资料与拓展阅读