像规划城市一样设计系统:城市规划给软件架构的启示

像规划城市一样设计系统:城市规划给软件架构的启示

50 万人的城市和 5 万人的城市,不只是规模不同,而是本质上不同的系统。软件也一样——当你从单体走向微服务,从千用户走向百万用户,问题已经不在"写代码"上了。城市规划师花了几百年研究如何让复杂系统有序运转,而这些智慧,恰好是每个架构师需要的。


为什么是城市规划

如果你仔细想想,城市和软件系统惊人地相似:

特征 城市 软件系统
核心目标 让大量人高效协同生活 让大量模块高效协同工作
基础设施 道路、水电、管网 数据库、消息队列、缓存
分区 居住区、商业区、工业区 业务域、核心域、支撑域
交通 人流、车流 数据流、请求流
老化 建筑老旧、管道锈蚀 技术债、依赖过时
扩张 新区开发、旧城改造 新服务拆分、遗留系统重构
瓶颈 早高峰拥堵 流量峰值熔断

一个资深架构师面对的问题——如何让系统在增长中保持稳定、如何在旧架构上做增量改造、如何预判瓶颈——和一个城市规划师面对的问题本质上是一样的。

更关键的是,城市规划犯的错误,软件行业正在重复犯


第一课:功能分区——从混乱生长到有序布局

城市的教训

早期城市没有规划。铁匠铺挨着住宅,屠宰场旁边就是集市,气味、噪音、火灾风险混杂在一起。中世纪伦敦多次大火,根本原因就是功能混杂导致火势无法控制。

现代城市规划的核心发明是功能分区(Zoning):将城市划分为居住区、商业区、工业区,不同功能之间用缓冲带隔离。

软件的映射

这不就是限界上下文(Bounded Context)吗?

一个没有"分区"的软件系统:

// 所有东西搅在一起——就像中世纪的城市
class OrderService {
    void createOrder() {
        // 订单逻辑
        // 库存扣减逻辑
        // 支付逻辑
        // 物流逻辑
        // 通知逻辑
        // 用户积分逻辑
        // 优惠券核销逻辑
    }
}

一个"分区"良好的系统:

// 订单上下文——只管订单
class OrderService {
    void createOrder(OrderRequest req) {
        Order order = orderRepo.save(req.toOrder());
        eventBus.publish(new OrderCreatedEvent(order.getId()));
    }
}

// 库存上下文——只管库存
class InventoryService {
    @EventHandler
    void on(OrderCreatedEvent e) {
        inventoryRepo.deduct(e.getOrderId(), e.getItems());
    }
}

// 支付上下文——只管支付
class PaymentService {
    @EventHandler
    void on(OrderCreatedEvent e) {
        paymentGateway.charge(e.getOrderId(), e.getAmount());
    }
}

分区原则

  1. 每个分区只做一件事——住宅区不建工厂,订单服务不处理支付
  2. 分区之间通过"道路"连接——事件总线、API 调用就是城市道路
  3. 分区之间有"缓冲带"——防腐层(Anti-Corruption Layer)就是绿化隔离带,防止一个上下文的"污染"蔓延到另一个

分区的陷阱

城市规划中有一个重要教训:过度分区和分区不足一样糟糕

巴西利亚是过度分区的典型——居住区、商业区、行政区严格分离,结果居民要开车穿越大半个城市才能买一瓶牛奶。城市失去了"混合活力"。

软件中的"巴西利亚":

过度拆分的微服务:
用户姓名查询 → 调用用户服务
用户地址查询 → 调用地址服务  
用户偏好查询 → 调用偏好服务
用户头像查询 → 调用头像服务

一个简单的用户资料页要调 5 个服务
延迟 = max(5 个服务) + 序列化 × 5 + 网络开销 × 5

好的分区是高内聚低耦合的——相关的东西放在一起,不相关的东西隔离开。 不是拆得越细越好,而是每块内部功能完整,对外依赖最少。


第二课:交通网络——数据流的道路规划

城市的教训

交通是城市的血脉。北京的环路与巴黎的放射状路网,代表了两种截然不同的交通哲学。

北京环路的问题:单中心、层层嵌套。所有人都要进城,所有流量都汇聚到几个枢纽节点。结果就是——堵。从三环堵到五环,越往中心越堵。

巴黎的思路不同:多中心、放射 + 环形混合。拉德芳斯是商务中心,拉丁区是文化中心,各有侧重,不必凡事都去"市中心"。

软件的映射

北京环路 = 单体架构的中心数据库

所有服务 → 一个大数据库

  用户服务 ─┐
  订单服务 ─┤
  支付服务 ─┼──→ [超级大数据库] ← 瓶颈!
  物流服务 ─┤
  报表服务 ─┘

当所有流量都汇聚到一个数据库时,它就是"三环"——一旦堵了,全城瘫痪。

巴黎多中心 = 每个限界上下文有自己的数据存储

用户服务 → [用户库]      订单服务 → [订单库]
支付服务 → [支付库]      物流服务 → [物流库]

服务之间通过事件/API通信,而非共享数据库

道路等级与数据管道

城市规划中,道路有明确等级:

道路等级 特点 软件映射
高速公路 大流量、长距离、出入口少 消息队列(Kafka/RabbitMQ)
主干道 连接主要区域、红绿灯控制 服务间同步 API(gRPC/REST)
支路 到达具体目的地、车速低 服务内部方法调用
人行道 短距离、慢速 内存中数据传递

一个常见的架构错误:用高速公路送外卖

# 两个同进程内的模块,却用 Kafka 通信
Module A  Kafka  Module B

# 等同于:出门倒垃圾,先上高速公路,再下来
# 开销:序列化 + 网络传输 + Broker 中转 + 反序列化
# 正确做法:直接方法调用

反过来,用支路跑长途货运也有问题:

# 两个独立部署的服务,通过共享数据库表"通信"
Service A 写入 shared_table,Service B 轮询读取

# 等同于:用乡村小路跑集装箱卡车
# 问题:耦合、性能差、无法独立部署

交通疏导:限流与熔断

城市交通管理有"限号"和"单行道",软件有限流(Rate Limiting)和熔断(Circuit Breaker)

# 城市限号 → 限流
限号: 每天只允许尾号 1/3/5/9 的车上路
限流: 每秒只允许 1000 个请求通过

# 交通管制 → 熔断
前方事故 → 封路 → 车辆绕行
下游服务故障 → 熔断 → 请求走降级路径
// Resilience4j 熔断器——就像交通管制
CircuitBreaker breaker = CircuitBreaker.ofDefaults("paymentService");
Supplier<String> supplier = CircuitBreaker.decorateSupplier(
    breaker, 
    () -> paymentService.charge(request)  // 正常路线
);
String result = Try.ofSupplier(supplier)
    .recover(throwable -> fallbackCharge(request))  // 绕行路线
    .get();

第三课:基础设施先行——别在沙地上盖楼

城市的教训

深圳从渔村到一线城市,只用了 40 年。秘诀之一:基础设施先行。先修路、通水、通电,再招商引资。很多城市反着来——先盖楼卖房,等入住才发现路不够宽、水压不够、学校不够。补救成本远高于一开始就规划好。

软件的映射

软件的"基础设施"是什么?

  • 可观测性:日志、指标、链路追踪——城市的"监控摄像头 + 传感器"
  • CI/CD:自动化构建和部署——城市的"供水供电系统"
  • 安全基线:认证、授权、加密——城市的"消防 + 安防"
  • 数据备份与恢复——城市的"应急避难所"

没有可观测性的服务,就像没有监控的城市——出了问题你甚至不知道。

反模式:
1. 先写业务代码
2. 上线后出 Bug
3. "加个日志吧"
4. 日志打了,发现信息不够
5. 再加指标
6. 指标加了,发现没有告警
7. 告警配了,发现找不到根因(缺链路追踪)
8. 补链路追踪——但要改所有服务

→ 每一步都是补救,总成本远大于一开始就搭建好
精益做法:
1. 先搭建可观测性基座(日志 + 指标 + 链路追踪)
2. CI/CD 流水线就位
3. 安全基线配置好
4. 再开始写业务代码

→ 业务代码天然就在基础设施的"保护"下运行
→ 出问题时,三分钟定位而非三小时猜测
# 基础设施先行的实践:新服务脚手架
service-template/
├── Dockerfile              # 容器化
├── .github/workflows/      # CI/CD
├── health/                 # 健康检查
│   ├── readiness.py
│   └── liveness.py
├── observability/          # 可观测性
│   ├── logging.py          # 结构化日志
│   ├── metrics.py          # Prometheus 指标
│   └── tracing.py          # OpenTelemetry 链路追踪
├── security/               # 安全基线
│   ├── auth.py             # 认证中间件
│   └── rate_limit.py       # 限流
└── src/                    # 业务代码——最后才写
    └── ...

"临时"基础设施的诅咒

城市规划有一条铁律:临时建筑往往比永久建筑活得更久

软件也一样——那个"临时写的数据库连接池"用了三年,那个"先用文件存的配置"至今还在生产环境。

如果你觉得某个东西是临时的,那它一定会变成永久的。所以,从一开始就用生产级方案。


第四课:有机生长 vs 大拆大建——演进式架构

城市的教训

城市改造有两种路径:

大拆大建:推平老城,重新规划。巴西利亚就是在空地上从零设计的"理想城市"——功能完美,但缺乏人气,被称为"没有灵魂的城市"。

有机更新:保留城市肌理,逐步改造。巴黎的奥斯曼改造没有推平全部老城,而是在保留街道骨架的基础上拓宽道路、统一立面,形成了今天的美感。

软件的映射

大拆大建 = 重写

"现有系统太烂了,我们用最新技术栈重写!"

旧系统:能跑,虽然丑
新系统:设计完美,但——
  - 花了 18 个月
  - 遗漏了旧系统的 100 个边界 case
  - 上线后用户投诉:功能还不如旧的
  - 最终两套系统并行运行(更糟了)

这就是经典的第二系统综合症(Second-System Effect)——Fred Brooks 在《人月神话》中警告过的:第二个系统往往过度设计,因为设计者试图把"第一版所有没做的东西"全塞进去。

有机更新 = 演进式架构

策略:绞杀者模式(Strangler Fig Pattern)

旧系统继续运行,新系统逐步替代:
1. 在旧系统前加一层路由(Facade)
2. 新功能用新架构写,路由指向新系统
3. 旧功能逐个迁移到新系统
4. 每迁移一个,旧系统就小一分
5. 最终旧系统被"绞杀"——自然退役
  请求 → [路由层] → 新服务(优先)
                ↘ 旧系统(降级)

迁移进度:
Week 1:  5% 请求走新系统
Week 4:  30% 请求走新系统
Week 8:  70% 请求走新系统
Week 12: 100% 请求走新系统 → 旧系统下线

绞杀者模式的名称来自热带雨林中的绞杀榕——它从宿主树的枝干上开始生长,逐渐向下扎根、向上扩展,最终完全包裹并替代宿主。这个过程是渐进的、安全的,宿主树在每一步都还是活的。

演进式架构的核心原则

  1. 可逆性:每个决策都应该是可逆的,或至少是可降级的
  2. 小步迁移:每次只迁移一个功能,验证通过再迁移下一个
  3. 双跑验证:新旧系统同时处理相同请求,对比结果确保一致
  4. 特性开关:用开关控制新旧路径,出问题秒级回滚
# 演进式迁移:特性开关控制
def process_order(order):
    if feature_flags.is_enabled("new_order_service"):
        try:
            return new_order_service.process(order)
        except Exception as e:
            log.error(f"New service failed: {e}")
            return legacy_order_service.process(order)  # 降级
    else:
        return legacy_order_service.process(order)

第五课:公共空间——API 是城市的广场

城市的教训

简·雅各布斯在《美国大城市的死与生》中指出,城市的活力来自公共空间——街道、广场、公园。这些地方不是为某个单一功能设计的,而是允许各种活动自然发生。好的公共空间让城市有"街道眼"——自然监督,安全感上升。

被她严厉批评的是罗伯特·摩西的大拆大建——宽阔的高速公路切断了社区,巨大的住宅楼群缺乏人与人交互的空间,最终导致犯罪率上升、社区衰败。

软件的映射

API 就是软件系统的公共空间

好的 API 像好的广场:

  • 开放但有序:任何人都能来,但有规则(API 契约)
  • 多功能:不同消费者可以用不同方式使用(REST + GraphQL + Webhook)
  • 稳定性:广场不能今天有明天拆(API 版本管理)
  • 自描述:好广场一眼就知道怎么走(API 文档、自描述端点)

差的 API 像摩西的高速公路:

  • 只管通过:只考虑提供方的方便,不管消费者的体验
  • 切割系统:紧耦合的 API 把消费者锁死在提供方的实现细节上
  • 粗暴变更:今天改字段明天改路径,消费者被迫跟着改
# 好的 API 设计——像好广场
/api/v2/orders:
  get:
    summary: 查询订单
    parameters:
      - status: 按状态筛选
      - page: 分页
    responses:
      200:
        content:
          application/json:  # 标准 MIME
        links:
          payment: /api/v2/payments/{orderId}  # 引导到关联资源
          tracking: /api/v2/logistics/{shipmentId}
    # HATEOAS:像广场上的路标,告诉你还能去哪
// 差的 API——像摩西的高速公路
// 返回内部数据库字段,消费者被锁死在实现上
@GetMapping("/internal/orders")
Map<String, Object> getOrder(@RequestParam String ord_id) {
    // 直接返回数据库行——字段名、类型、结构全暴露
    return jdbcTemplate.queryForMap("SELECT * FROM t_ord WHERE id = ?", ord_id);
}

API 的"街道眼"——可观测性

好的公共空间有自然监督(街道眼),差的公共空间是监控死角。

好的 API 有可观测性

  • 每个请求有 traceId,像广场上的摄像头
  • 慢请求有告警,像交通拥堵提示
  • 异常请求有日志,像社区巡逻记录
  • API 调用有统计,像广场人流量监控

第六课:抗震与韧性——城市不会因为一次地震就灭亡

城市的教训

日本建筑有世界最强的抗震标准。核心思路不是"不会震",而是"震了不倒"——允许建筑在地震中变形、晃动,但结构不崩塌,人能活着出来。

这个思路叫韧性(Resilience)——不是追求绝对不故障,而是故障发生时系统能承受并恢复。

软件的映射

高可用不是"不会挂",而是"挂了不影响"

非韧性架构:
  单点故障 → 雪崩 → 全站挂

韧性架构:
  局部故障 → 降级 → 核心功能仍可用

建筑抗震的三重策略,映射到软件:

1. 基础隔震(Base Isolation)= 故障隔离

建筑在底部安装橡胶支座,让地震波不会传递到上层。软件中的故障隔离:

  • 线程池隔离:一个服务的慢请求不会耗尽全局线程池
  • 舱壁模式(Bulkhead):一个服务挂了不影响其他服务
  • 熔断器:检测到故障,主动断开,防止级联

2. 柔性结构(Ductile Structure)= 优雅降级

建筑允许梁柱在极限状态下弯曲变形但不断裂。软件中的优雅降级:

def get_product_detail(product_id):
    """韧性设计:每个依赖都有降级方案"""

    # 核心数据——必须成功
    product = product_repo.get(product_id)
    if not product:
        raise NotFoundError(f"Product {product_id} not found")

    # 非核心:评论——挂了也能看商品
    reviews = safe_call(
        lambda: review_service.get_reviews(product_id),
        fallback=[]  # 降级:返回空评论
    )

    # 非核心:推荐——挂了也不影响
    recommendations = safe_call(
        lambda: recommend_service.get_similar(product_id),
        fallback=[]  # 降级:返回空推荐
    )

    # 非核心:库存——挂了显示"查询中"
    stock = safe_call(
        lambda: inventory_service.get_stock(product_id),
        fallback={"status": "unknown", "text": "库存查询中"}
    )

    return {
        "product": product,       # 核心
        "reviews": reviews,       # 非核心,降级
        "recommendations": recommendations,  # 非核心,降级
        "stock": stock            # 非核心,降级
    }

3. 冗余系统(Redundancy)= 多副本

关键建筑有备用电力、双水源、双消防通道。软件中的冗余:

  • 多副本部署(至少 2 个实例)
  • 多可用区(AZ)部署
  • 数据库主从复制
  • CDN 多节点缓存

第七课:城市记忆——为什么老城区比新城区更有魅力

城市的教训

几乎所有旅游城市最吸引人的地方,都是老城区——罗马的 Trastevere、巴黎的玛黑区、北京的胡同。这些地方经历了数百年自然演化,每一代人都留下了痕迹,形成了丰富的"层次感"。

而新城区往往"完美但无聊"——因为缺乏时间的积累和人的使用痕迹。

软件的映射

老代码不是债,是资产——前提是它能跑。

一个运行了 5 年的系统,里面包含了无数边界 case 的处理、踩过的坑的修复、用户反馈的回应。这些信息不在文档里(文档早就过时了),而在代码本身。

重写时丢失的,正是这些"城市记忆":

旧系统中一个看起来"多余"的判断

if order.amount > 10000 and user.level == "silver" and order.type != "bulk":
    order.add_flag("MANUAL_REVIEW")

新系统重写时有人看了这行觉得"没意义"就删了
结果上线后大额银牌用户的异常订单没人审核造成损失

 那行代码是"城市记忆"它记录了一次事故的教训

尊重老代码的态度

  1. 先理解再改:看不懂的代码,先搞清楚为什么存在,再决定是否删除
  2. 保留有原因的丑:有些"丑"代码是特定约束下的最优解
  3. 注释记录决策:用注释记录"为什么这样做",而不仅仅是"做了什么"
  4. 迁移而非重写:用绞杀者模式逐步迁移,保留"城市记忆"

跨行业映射速查表

城市规划概念 软件架构映射 核心思想
功能分区 限界上下文 相关的放一起,不相关的隔开
交通路网 数据流架构 高速公路走大批量,支路走低延迟
基础设施先行 可观测性 + CI/CD 先行 先修路再盖楼
有机更新 演进式架构 逐步迁移优于大拆大建
公共空间 API 设计 开放、有序、稳定、自描述
抗震韧性 故障隔离 + 降级 + 冗余 挂了不影响,而非不会挂
老城区保护 尊重遗留代码 老代码是资产不是债
绿化隔离带 防腐层 防止"污染"蔓延
城市规划法规 架构决策记录(ADR) 重大决策要记录理由
城市体检 架构评估 定期检查系统健康度

给架构师的城市规划书单

如果你对这种跨行业思维感兴趣,推荐几本"非技术"的书:

  1. 《美国大城市的死与生》 — 简·雅各布斯 - 核心洞察:城市的活力来自多样性,而非整齐划一 - 软件启示:好的系统允许不同风格的模块共存

  2. 《城市的胜利》 — 爱德华·格莱泽 - 核心洞察:城市是人类最伟大的发明,密度创造价值 - 软件启示:合理的耦合不是坏事,过度解耦才是

  3. 《弹性基础设施》 — 相关城市规划文献 - 核心洞察:基础设施要能承受冲击并快速恢复 - 软件启示:Chaos Engineering 的理论基础

  4. 《建筑模式语言》 — 亚历山大 - 核心洞察:好的设计由可复用的"模式"组合而成 - 软件启示:这就是 GoF《设计模式》的灵感来源——亚历山大是模式语言的鼻祖


结语:建筑是凝固的音乐,代码是流动的城市

建筑大师歌德说"建筑是凝固的音乐"。我倒觉得,代码是流动的城市——它有道路、有分区、有基础设施、有公共空间,也在不断生长和演变。

一个城市的规划水平,不取决于最高的那栋楼有多漂亮,而取决于最普通的那条路走起来舒不舒服。一个系统的架构水平,不取决于用了多新的技术栈,而取决于最日常的那个操作稳不稳定。

城市规划用几百年教会我们:不要追求完美的蓝图,要追求持续演化的能力。 因为没有人能在第一天就设计出一个容纳百万人的完美城市——城市的伟大在于它能不断适应变化。

软件也一样。别试图设计"终极架构",设计一个能随时间进化的架构。

毕竟,罗马不是一天建成的。你的系统也不应该是。


参考资源: - 《美国大城市的死与生》— Jane Jacobs - 《建筑模式语言》— Christopher Alexander - 《演进式架构》— Neal Ford 等 - 《凤凰架构》— 周志明 - 《发布!软件的设计与部署》— Michael T. Nygard