
《What the Bubble Got Right (2004)》是一篇写在互联网泡沫破裂之后的技术评论文章标题直译过来是“泡沫做对了什么”。很多开发者看到这个标题时第一反应可能是困惑泡沫不是已经被证明是一场资本游戏了吗但在工程层面这个标题谈的并不是股价和估值而是互联网泡沫期间被大规模建设、后来却支撑起整个现代 Web 的基础设施和技术方向。真正经历过那段时间的项目最后活下来的往往不是最会讲故事的创业公司而是那些把网络、机房、协议和工具链真正搭起来的人。这类复盘后来成为技术圈讨论“长期主义”的常见起点。程序员读它并不是为了看历史故事而是为了回答一个很现实的问题当市场热潮退去以后哪些技术决策依然有价值本文会顺着这个思路先拆解 2004 年这类复盘沉淀下来的工程判断再给出一套最小可运行的项目骨架用来说明“先搭基础设施、再叠加功能”的选型方式。读完以后你可以把这套思路直接带进技术选型、架构评审和线上故障排查。1. 先理解这篇 2004 年复盘在技术层面肯定了哪些东西1.1 泡沫破灭留下的不只是亏损还有被真实建成的生产能力20 世纪 90 年代末到 2000 年初互联网公司大规模融资随后又把钱大量投入到光纤网络、数据中心、服务器集群、电商系统和 Web 应用中。2001 年到 2002 年泡沫破裂资本市场看到的是估值归零和公司倒闭但从工程角度看有一批物理资产和技术设施已经建成并且没有因为股价波动而消失。这就是“Bubble Got Right”的核心含义金融泡沫的估值可能是错的但同一时期铺下去的网络带宽、机房空间、服务器硬件和 Web 技术栈却被后续十多年的互联网产品持续使用。今天点开一个页面背后至少经历了 DNS 解析、CDN 回源、负载均衡、应用处理、缓存读取、数据库查询等多层链路而这些链路能成为默认能力恰恰是因为早期已经有人投入了巨大的工程成本去铺设和验证。对普通开发者来说这个判断的启发是不要只盯着一个新概念表面的热度要看这个概念对应的工程资产是否能在热潮退去后继续复用。热点会变光纤和协议不会随股价一起消失。1.2 为什么 2004 年这个时间点很关键2000 年到 2002 年是大量项目被砍掉的高峰期2003 年市场开始慢慢恢复到 2004 年技术圈已经有了一批“回过头来看”的复盘文章。2004 年讨论“泡沫做对了什么”距离泡沫破裂已经有一段时间足够区分哪些是短期投机哪些是长期资产。这个时间点对现在做技术判断同样有意义。一个新技术刚出现时市面上全是赞美和 Demo不适合立刻给出结论。等第一轮热度消退、真实问题暴露、维护成本变清晰之后再回头看哪些功能、哪些协议、哪些架构取舍被保留下来才更接近答案。2004 年的文章之所以具备参考价值就是因为它已经完成了这一步“冷却”。1.3 从“能用就行”到“以后还能改”的立场转变泡沫时期很多项目的开发逻辑是“尽快上线、尽快融资”系统能跑就行模块之间没有边界数据格式各写各的。泡沫之后幸存者发现真正值得投入的是那些能让系统持续演进的东西清晰的协议、松耦合的模块、可替换的依赖、可观测的运行状态。这个概念放到今天依然是选择标准。一个接口如果只考虑当前页面能用不考虑版本兼容不做限流和降级短期内确实上线快但下一轮需求来临时改造成本会成倍增长。2004 年复盘里最值得工程师吸收的信息就是把“验收标准”从“能不能跑”提升到“以后能不能改、能不能查、能不能换”。2. 今天仍然每天在用那场泡沫留下的几类工程资产2.1 带宽和边缘节点解决了“远距离访问”的基本矛盾泡沫前互联网体验差的核心原因是带宽不足页面加载慢视频和图片基本不可用。大批互联网公司投入宽带网络和骨干网建设虽然当时产能过剩但后续的流媒体、移动互联网、在线协作工具都受益于这套基础网络。现代工程中的 CDN 和边缘节点可以看作是这套思路的延续。把静态资源放到离用户更近的节点减少跨地域传输比无限增加源站带宽更划算。今天搭建服务时即使只是一个小型 Web 应用也值得在部署之前想清楚哪些请求需要走源站哪些资源可以边缘缓存缓存失效规则是什么。2.2 通用计算集群解决了“按需扩容”的成本模型泡沫时期建设了大量机房但早期机房的利用率并不高。后来虚拟化、云计算把“买机器”变成了“租资源”按需扩容才变成一种标准能力。现在的容器编排、弹性伸缩、Serverless本质上都延续了同一个目标不让计算资源成为业务波动的瓶颈。从工程实践看这意味着设计系统时要尽量让应用无状态让会话和临时数据落到外部组件比如 Redis、数据库或对象存储。这样当流量上涨时可以直接增加应用实例而不是在一台机器的内存里找数据。2.3 HTTP、开放标准和开源软件解决了“互操作”问题泡沫之前很多系统使用私有协议和自定义格式服务之间很难互通。HTTP、HTML、XML、TCP/IP 这些开放标准后来成为所有系统的共同语言。2004 年的复盘里技术人员已经意识到基于开放标准构建的系统最大的优势是不同团队、不同公司之间可以低成本协作。今天的 REST、GraphQL、gRPC虽然在传输效率上各有取舍但都建立在开放协议之上。这种选择让中间件、网关、监控系统能够统一接入而不是每个服务都维护一套专有通信方式。开源软件也遵循同样的逻辑代码公开、社区公开、依赖路径清晰比黑盒组件更容易做审计和替换。2.4 监控与容量规划解决了“无数据决策”问题泡沫项目最容易犯的错误是广告打得很大但没有人能回答系统每天处理多少请求、错误率是多少、哪个环节最慢。泡沫之后技术团队开始认真建设监控体系用数据决定扩容和优化方向。这个习惯后来发展成 SRE 和可观测性工程。现代链路里指标、日志、链路追踪相互配合。没有监控时线上问题只能靠用户投诉发现有监控后可以在故障扩大前看到错误率曲线、延迟分位数和资源水位。下面的表格把泡沫期投入方向、后续价值、现代工程载体对应起来泡沫期投入方向泡沫后沉淀出的价值今天的工程载体骨干网和带宽建设跨地域访问成为可能CDN、边缘节点、多区域部署机房和通用服务器计算资源按需分配云主机、容器、Kubernetes、ServerlessHTTP 和开放协议系统之间低成本互操作REST API、API 网关、gRPC开源操作系统和框架降低重复开发成本、便于审计Linux、Python、Java、Node.js监控和容量规划用数据而不是感觉做扩容决策Prometheus、Grafana、告警规则数据仓库和日志体系从业务数据中持续提取价值数据湖、OLAP、日志平台有了这张对应关系再去看今天的技术生态就会发现那些反复被讨论的组件几乎都能在历史上找到原型。它们不是凭空冒出来的而是为了解决某个长期存在的工程约束。3. 用一套最小可复现系统模拟“长期主义”骨架3.1 骨架设计无状态应用、缓存、数据库、反向代理、监控理论讲得再多不如用一个最小系统把它落下来。下面的示例不会模拟 2004 年的技术环境而是把“泡沫做对了什么”抽象成现代架构里的五个决策应用服务保持无状态可以通过反向代理水平扩展。缓存只承担临时数据不承担持久化责任。数据库只保存核心数据不直接暴露给客户端。所有服务都带健康检查编排系统能感知真实可用状态。从服务上线第一天就接入指标采集而不是等出问题再补。这套骨架很简单但已经包含了一条完整请求链路。你可以在本地 Docker 环境中把它跑起来也可以在此基础上继续添加消息队列、对象存储和告警规则。示例只用于演示设计思路实际项目需要根据自己的包名、路径、版本和部署环境调整。3.2 项目结构先规划目录结构避免文件散落bubble-demo/ ├── app/ │ ├── Dockerfile │ ├── main.py │ └── requirements.txt ├── nginx/ │ └── nginx.conf ├── prometheus/ │ └── prometheus.yml └── docker-compose.yml这里要解释目录职责app目录存放业务服务代码和构建镜像所需的文件。nginx目录提供入口反向代理配置。prometheus目录保存指标采集配置。docker-compose.yml负责把服务编排起来并定义网络、数据卷、健康检查。3.3 编写应用服务应用服务使用 Python Flask提供三个接口健康检查、读取带缓存的数据、暴露指标。代码刻意保持简单重点在于表现“无状态应用 外部缓存”的套路。先写依赖文件app/requirements.txtflask3.0.3 gunicorn22.0.0 redis5.0.7 psycopg2-binary2.9.9 prometheus-client0.20.0再写应用代码app/main.pyimport os import time import psycopg2 import redis from flask import Flask, jsonify from prometheus_client import CONTENT_TYPE_LATEST, Counter, generate_latest app Flask(__name__) REQUESTS Counter(app_requests_total, Total requests handled by app) redis_client redis.Redis( hostos.getenv(REDIS_HOST, redis), portint(os.getenv(REDIS_PORT, 6379)), decode_responsesTrue, ) def get_db_connection(): return psycopg2.connect( hostos.getenv(POSTGRES_HOST, postgres), portint(os.getenv(POSTGRES_PORT, 5432)), databaseos.getenv(POSTGRES_DB, appdb), useros.getenv(POSTGRES_USER, appuser), passwordos.getenv(POSTGRES_PASSWORD, apppass), ) app.before_request def increment_request_counter(): REQUESTS.inc() app.get(/health) def health(): redis_ok True db_ok True try: redis_client.ping() except Exception: redis_ok False try: conn get_db_connection() cur conn.cursor() cur.execute(select 1) cur.fetchone() cur.close() conn.close() except Exception: db_ok False if redis_ok and db_ok: return jsonify(statusok, redisTrue, dbTrue) return jsonify(statusdegraded, redisredis_ok, dbdb_ok), 503 app.get(/data/key) def read_data(key): cached redis_client.get(key) if cached is not None: return jsonify(sourcecache, keykey, valuecached) time.sleep(0.05) value fvalue-of-{key} redis_client.setex(key, 60, value) return jsonify(sourcestore, keykey, valuevalue) app.get(/metrics) def metrics(): return generate_latest(), 200, {Content-Type: CONTENT_TYPE_LATEST}这段代码有三个关键点。第一应用启动后不保存任何本地业务状态所有需要共享的数据都放在 Redis 或数据库里因此 Gunicorn 可以启动多个 worker后续也可以横向扩容。第二/health不只是返回一个固定字符串而是真实检查 Redis 和 PostgreSQL 的连通性这样编排系统才能知道服务是否真的可用。第三/metrics暴露 Prometheus 文本格式指标不需要额外引入复杂框架就能把请求量采集出来。接下来写应用镜像的app/DockerfileFROM python:3.12-slim WORKDIR /app RUN apt-get update apt-get install -y --no-install-recommends curl \ rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py . EXPOSE 8000 CMD [gunicorn, --workers, 2, --bind, 0.0.0.0:8000, main:app]容器里安装 curl 是为了让/health健康检查可以本地访问应用端口。这里有一点实际工程经验很多轻量镜像没有 curl 或 wget健康检查命令会因为找不到命令而一直失败所以构建镜像时要显式安装健康检查工具。3.4 编写编排文件在项目根目录创建docker-compose.ymlservices: app: build: ./app environment: REDIS_HOST: redis POSTGRES_HOST: postgres POSTGRES_DB: appdb POSTGRES_USER: appuser POSTGRES_PASSWORD: apppass ports: - 8000:8000 depends_on: redis: condition: service_healthy postgres: condition: service_healthy healthcheck: test: [CMD, curl, -f, http://localhost:8000/health] interval: 10s timeout: 3s retries: 5 nginx: image: nginx:1.26-alpine ports: - 8080:80 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro depends_on: app: condition: service_healthy healthcheck: test: [CMD, wget, -q, -O, -, http://localhost/health] interval: 10s timeout: 3s retries: 5 redis: image: redis:7-alpine command: [redis-server, --appendonly, yes] volumes: - redis-data:/data healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 3s retries: 5 postgres: image: postgres:16-alpine environment: POSTGRES_DB: appdb POSTGRES_USER: appuser POSTGRES_PASSWORD: apppass volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U appuser -d appdb] interval: 10s timeout: 3s retries: 5 prometheus: image: prom/prometheus:v2.53.0 volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus-data:/prometheus ports: - 9090:9090 depends_on: - app grafana: image: grafana/grafana:11.1.0 ports: - 3000:3000 environment: GF_SECURITY_ADMIN_PASSWORD: admin123 volumes: - grafana-data:/var/lib/grafana depends_on: - prometheus volumes: redis-data: postgres-data: prometheus-data: grafana-data:这里值得重点解释的是depends_on里的condition: service_healthy。普通depends_on只能保证服务启动顺序不能保证被依赖的服务已经可连接。如果 Redis 和 PostgreSQL 还没有启动完成应用进程连接数据库失败后可能直接退出或者进入反复重启的状态。使用健康检查条件可以等到依赖服务好了再启动应用这是编排环境里最基本的可靠性设计。3.5 编写代理和监控配置Nginx 配置放在nginx/nginx.confevents {} http { upstream app_backend { server app:8000; } server { listen 80; location / { proxy_pass http://app_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } }这个配置把外部请求统一从 80 端口转发到应用服务的 8000 端口。proxy_set_header是为了把客户端真实 IP 和原始 Host 传给后端否则后端只能看到 Nginx 容器地址无法做访问日志分析和权限控制。Prometheus 配置放在prometheus/prometheus.ymlglobal: scrape_interval: 10s evaluation_interval: 10s scrape_configs: - job_name: bubble-app metrics_path: /metrics static_configs: - targets: [app:8000]这套配置让 Prometheus 每 10 秒从应用服务的/metrics拉取一次指标。拉取目标写在容器网络内部所以使用服务名app而不是localhost。实际生产环境里这个地址可能是实例 IP、服务发现名称或 DNS 记录但抓取思路一致。4. 核心设计为什么能扛周期关键参数逐步拆解4.1 无状态应用加外部缓存为什么能应对流量波动示例里应用服务只负责执行逻辑所有跨请求数据都放在 Redis。这样做的好处是当流量增长时可以直接增加应用实例而不需要担心某个请求落在哪个实例上。对应到泡沫时期的复盘这就是“通用计算集群”的价值资源可以调度应用可以复制。如果应用把用户会话保存在本地内存那么反向代理必须做会话粘滞扩容时还要处理数据不一致。使用外部缓存后会话和临时数据集中管理应用实例变成可丢弃的资源。在生产环境Redis 也要考虑持久化和高可用不能只靠appendonly yes开启一个持久化配置了事。生产环境应该规划主从、哨兵或集群模式并设置合理的过期策略防止内存被占满。4.2 反向代理不是“转发”那么简单示例里的 Nginx 承担了几个职责作为统一入口屏蔽内部容器网络细节。通过upstream定义后端服务组为未来扩展多个应用实例留好位置。透传客户端真实 IP保证日志和审计信息可用。提供健康检查端点让外部负载均衡器或监控系统确认入口服务可用。这个设计也呼应了“开放协议”的价值。客户端访问的是 HTTP 标准端口不需要知道后端具体是什么语言、什么框架、跑在哪个容器里。只要协议稳定后端技术栈可以替换客户端不需要改动。在真实项目里反向代理还需要加上超时、重试、限流、TLS 终止、访问日志格式定义等参数。下面表格整理了示例里几个主要服务的端口和检查方式服务对外端口容器内部端口健康检查数据持久化app80008000本地/health无nginx808080本地/health无redis无对外端口6379redis-cli pingredis-data卷postgres无对外端口5432pg_isreadypostgres-data卷prometheus90909090容器默认prometheus-data卷grafana30003000容器默认grafana-data卷注意Redis 和 PostgreSQL 没有对外映射端口。这样做的意义是减少暴露面只有应用服务需要通过内部网络访问它们外部用户不应该直接操作数据库和缓存。生产环境还应该通过防火墙规则、安全组或 Kubernetes NetworkPolicy 再做一层限制。4.3 健康检查参数间隔、超时、重试如何决定故障恢复速度示例里健康检查配置为interval: 10s、timeout: 3s、retries: 5。这几个参数直接影响故障发现和恢复速度interval太短会频繁发起探测浪费资源太长服务挂了不能及时被发现。timeout是单次探测等待时间超过这个时间就认为失败。如果服务响应超过 3 秒会被判定不健康。retries是连续失败几次才标记为不可用避免网络抖动导致误判。调优时要结合业务启动时间和依赖情况。如果应用启动需要 30 秒健康检查的失败重试阈值就不能太严格否则容器会被反复杀掉。如果业务接口本身就要 5 秒才能响应健康检查路径应该设计成“快速确认进程存活和依赖连通”不要在健康检查里跑很重的查询。4.4 可观测性为什么要从第一天接入示例里用prometheus_client暴露了一个计数器指标看起来很简单但它回答了三个问题请求有没有进来、量有多大、趋势如何。没有指标时遇到线上卡顿只能排查实例状态和日志有指标后可以先看请求量、错误率、延迟分布再进入具体日志。生产环境的可观测性至少包括三层指标Prometheus、Grafana、告警规则。日志统一采集到 Elasticsearch、Loki 或云日志服务。链路追踪通过 OpenTelemetry 记录请求经过的每个服务。从 2004 年的复盘视角看可观测性是最典型的“泡沫做对了”的工程资产。它不会直接产生业务功能但能让系统在复杂环境下被理解、被控制。越是早期接入后期排查问题时的成本越低。5. 运行、压测和验证看数据而不是看感觉5.1