三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

AI辅助自建状态页:从SaaS到自主可控的工程实践

AI辅助自建状态页:从SaaS到自主可控的工程实践

大家好,我是专注于技术实战分享的博主。在当今SaaS服务盛行的时代,很多团队都依赖外部服务来构建核心系统,比如状态页(Status Page)。但你是否想过,当成本、定制化需求和数据自主权成为瓶颈时,自己动手打造一个会是怎样的体验?本文将分享一个真实的工程实践:我们如何利用现代AI辅助开发工具,从零构建了一个功能完备、高可用的状态页系统,成功替代了年费高达6.5万美元的SaaS产品。整个过程不仅是一次成本优化,更是一次对AI赋能软件开发的深度探索。无论你是想了解AI在工程中的实际应用,还是希望获得一份可复用的状态页自建指南,这篇文章都将为你提供从架构设计、技术选型到代码实现的完整路径。

1. 状态页(Status Page)的核心价值与自建动因

在深入技术细节之前,我们首先要明确什么是状态页,以及为什么值得投入精力去自建。

1.1 什么是状态页?

状态页是一个面向用户(包括内部员工和外部客户)的公开页面,用于实时展示一个或多个服务、系统、API接口的运行健康状况。它通常包含以下核心信息:

  • 服务状态:用颜色(如绿色-运行正常、黄色-性能下降、红色-服务中断)直观标识每个组件的状态。
  • 历史事件:记录所有计划内维护和意外故障的事件时间线,包括开始时间、结束时间、影响范围和事件描述。
  • 订阅通知:允许用户通过邮件、短信或Webhook订阅状态更新。
  • 全局状态:一个概括整体系统健康度的摘要,例如“所有系统运行正常”或“部分服务降级”。

对于互联网公司而言,状态页是SLA(服务等级协议)透明化的重要体现,能有效建立用户信任,并在出现问题时减少客服压力。

1.2 为什么放弃SaaS选择自建?

我们最初使用的正是一款知名的SaaS状态页服务,年费约6.5万美元。促使我们转向自建的原因主要有以下几点:

  1. 高昂的成本:对于高速发展的创业公司或中型团队,每年数万美元的固定支出是一笔不小的开销,尤其是当核心功能相对固定时。
  2. 有限的定制化:SaaS产品通常提供标准化模板和有限的自定义选项。当我们需要深度集成内部监控系统(如Prometheus、Grafana)、使用特定的认证方式或实现独特的业务逻辑时,往往束手无策。
  3. 数据自主与安全:所有监控数据和事件日志都存储在第三方平台,存在数据隐私和合规性风险。自建可以实现数据完全自主可控。
  4. 技术栈统一:自建系统可以完全采用团队熟悉的技术栈(如Python/Django、Node.js、Go),便于维护和与现有基础设施(CI/CD、内部工具)无缝集成。
  5. AI赋能的可行性:当前AI代码生成工具(如Cursor、GitHub Copilot)和大型语言模型(LLM)的成熟,使得快速原型开发和代码编写效率大幅提升,显著降低了自研的初始门槛。

基于以上考量,我们决定启动这个“AI辅助自建状态页”项目,目标是以极低的成本和时间,打造一个不逊于商业产品、且更贴合自身需求的状态页。

2. 技术选型与架构设计

一个稳定可靠的状态页系统,背后需要清晰的技术架构支撑。我们的设计原则是:简单、可靠、易扩展、低成本

2.1 核心技术栈

  • 后端框架Python + FastAPI。选择FastAPI是因为其异步特性、高性能、自动生成API文档(OpenAPI)以及简洁的语法,能极大提升开发效率。
  • 前端框架React + TypeScript + Vite。React生态成熟,组件化开发体验好。TypeScript能提供更好的类型安全。Vite作为构建工具,开发热更新速度极快。
  • 数据库PostgreSQL。作为功能强大的开源关系型数据库,其JSONB类型非常适合存储灵活的事件数据,并且可靠性高。考虑到成本,也可以从SQLite开始,但PostgreSQL更适合生产环境。
  • 缓存与实时推送Redis。用于缓存频繁访问的状态数据,以及作为WebSocket后端(通过Channels或Socket.IO)实现状态变化的实时推送。
  • AI辅助工具Cursor(或VS Code + Copilot)。这是我们本次开发的“加速器”。主要用于:
    • 根据自然语言描述生成函数、组件甚至模块代码。
    • 解释复杂代码逻辑和第三方库用法。
    • 重构代码、编写单元测试。
    • 生成数据库迁移脚本和API文档注释。
  • 部署与基础设施
    • 容器化:Docker + Docker Compose(用于本地和测试环境)。
    • 编排与部署:Kubernetes(生产环境),或使用更简单的方案如Docker Swarm、甚至单个云服务器。
    • 监控:与现有Prometheus/Grafana栈集成,暴露自身健康指标。
    • 反向代理:Nginx或Caddy,处理SSL、静态文件和负载均衡。

2.2 系统架构图

用户浏览器 | v [ Nginx/Caddy ] (SSL终止、静态文件服务) | v [ 前端应用 (React) ] -- API请求 --> [ 后端API (FastAPI) ] | | |<-- WebSocket (实时状态) --| | | v | [ 业务逻辑层 ] | | | v | [ 数据访问层 ] | | | v | [ PostgreSQL ] [ Redis ] | (主存储) (缓存/消息) | | | v |----------------------------- [ 监控数据采集器 ] | v [ Prometheus / 其他监控源 ]

架构说明

  1. 前端独立部署,通过HTTP API和WebSocket与后端通信。
  2. 后端FastAPI应用处理核心业务:状态计算、事件管理、用户订阅等。
  3. 数据库持久化存储组件定义、事件历史、用户订阅等信息。
  4. Redis用于缓存当前全局状态和组件状态,避免频繁查询数据库,同时作为WebSocket消息的后端。
  5. 一个独立的“数据采集器”服务(可以是后台任务或微服务)定期从Prometheus、健康检查端点、第三方API等拉取数据,更新组件状态并触发事件。

3. 环境准备与项目初始化

在开始编码之前,我们需要搭建好本地开发环境。这里假设你已安装Python(3.9+)、Node.js(16+)、Docker和Git。

3.1 创建项目目录结构

mkdir my-status-page cd my-status-page mkdir -p backend/app/{api,core,models,schemas,services} backend/alembic mkdir -p frontend/src/{components,pages,services,types}

3.2 后端环境设置(FastAPI)

进入后端目录并创建虚拟环境及依赖文件。

cd backend python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate

创建requirements.txt文件:

fastapi==0.104.1 uvicorn[standard]==0.24.0 sqlalchemy==2.0.23 psycopg2-binary==2.9.9 alembic==1.12.1 pydantic==2.5.0 pydantic-settings==2.1.0 redis==5.0.1 httpx==0.25.1 python-multipart==0.0.6

安装依赖:

pip install -r requirements.txt

初始化Alembic(数据库迁移工具):

alembic init alembic

3.3 前端环境设置(React + TypeScript + Vite)

进入前端目录并初始化项目:

cd ../frontend npm create vite@latest . -- --template react-ts npm install

安装额外有用的库:

npm install axios react-query @tanstack/react-query npm install socket.io-client npm install date-fns npm install @mui/material @emotion/react @emotion/styled @mui/icons-material # 可选,用于UI组件

4. 核心数据模型与API设计

状态页的核心是数据。我们首先设计数据库模型和对应的Pydantic模式(Schema)。

4.1 定义数据库模型(SQLAlchemy)

创建backend/app/models.py文件:

from sqlalchemy import Column, Integer, String, DateTime, Boolean, Enum, JSON, Text from sqlalchemy.sql import func from sqlalchemy.ext.declarative import declarative_base import enum Base = declarative_base() class ComponentStatus(str, enum.Enum): OPERATIONAL = "operational" DEGRADED_PERFORMANCE = "degraded_performance" PARTIAL_OUTAGE = "partial_outage" MAJOR_OUTAGE = "major_outage" UNDER_MAINTENANCE = "under_maintenance" class IncidentStatus(str, enum.Enum): INVESTIGATING = "investigating" IDENTIFIED = "identified" MONITORING = "monitoring" RESOLVED = "resolved" class Component(Base): __tablename__ = "components" id = Column(Integer, primary_key=True, index=True) name = Column(String(255), nullable=False, index=True) description = Column(Text, nullable=True) group = Column(String(100), nullable=True) # 如 “API”, “Database”, “Website” status = Column(Enum(ComponentStatus), default=ComponentStatus.OPERATIONAL, nullable=False) order = Column(Integer, default=0) # 用于前端排序 created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) class Incident(Base): __tablename__ = "incidents" id = Column(Integer, primary_key=True, index=True) title = Column(String(500), nullable=False) description = Column(Text, nullable=True) status = Column(Enum(IncidentStatus), default=IncidentStatus.INVESTIGATING, nullable=False) impact = Column(Enum(ComponentStatus), nullable=False) # 事件的影响级别 affected_components = Column(JSON, default=list) # 存储受影响的组件ID列表,如 [1, 3] started_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) resolved_at = Column(DateTime(timezone=True), nullable=True) class Subscriber(Base): __tablename__ = "subscribers" id = Column(Integer, primary_key=True, index=True) email = Column(String(255), unique=True, index=True, nullable=False) is_verified = Column(Boolean, default=False) verification_token = Column(String(100), unique=True, nullable=True) subscribed_at = Column(DateTime(timezone=True), server_default=func.now())

模型说明

  • Component: 代表一个被监控的服务或组件。
  • Incident: 代表一个事件(故障或维护)。
  • Subscriber: 订阅状态更新的用户。
  • 使用Enum确保状态值的一致性。
  • JSON字段用于存储灵活的数据结构,如受影响的组件列表。

4.2 定义Pydantic模式(Schema)

创建backend/app/schemas.py文件。这些模式用于API请求/响应的数据验证和序列化。

from pydantic import BaseModel, EmailStr from datetime import datetime from typing import Optional, List from .models import ComponentStatus, IncidentStatus # Component 相关 class ComponentBase(BaseModel): name: str description: Optional[str] = None group: Optional[str] = None order: int = 0 class ComponentCreate(ComponentBase): pass class ComponentUpdate(BaseModel): status: Optional[ComponentStatus] = None description: Optional[str] = None class Component(ComponentBase): id: int status: ComponentStatus created_at: datetime updated_at: Optional[datetime] = None class Config: from_attributes = True # 替代旧的 orm_mode # Incident 相关 class IncidentBase(BaseModel): title: str description: Optional[str] = None impact: ComponentStatus affected_components: List[int] = [] class IncidentCreate(IncidentBase): pass class IncidentUpdate(BaseModel): status: Optional[IncidentStatus] = None description: Optional[str] = None resolved_at: Optional[datetime] = None class Incident(IncidentBase): id: int status: IncidentStatus started_at: datetime updated_at: Optional[datetime] = None resolved_at: Optional[datetime] = None class Config: from_attributes = True # 系统状态摘要 class SystemStatusSummary(BaseModel): overall_status: ComponentStatus components: List[Component] ongoing_incidents: List[Incident]

4.3 实现核心API端点

创建backend/app/api/endpoints目录,并创建components.py,incidents.py,status.py等路由文件。

status.py为例,实现获取系统状态摘要的API:

from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from typing import List from ...core.database import get_db from ...models import Component, Incident, ComponentStatus from ...schemas import SystemStatusSummary, Component as ComponentSchema, Incident as IncidentSchema from ...services.status_calculator import calculate_overall_status router = APIRouter() @router.get("/summary", response_model=SystemStatusSummary) async def get_system_status_summary(db: Session = Depends(get_db)): """ 获取系统整体状态摘要,包括全局状态、所有组件状态和进行中的事件。 此端点会被前端频繁调用,应考虑加入缓存(如Redis)。 """ # 获取所有组件 db_components = db.query(Component).order_by(Component.order).all() components = [ComponentSchema.from_orm(c) for c in db_components] # 获取所有未解决的事件 db_incidents = db.query(Incident).filter(Incident.resolved_at.is_(None)).order_by(Incident.started_at.desc()).all() incidents = [IncidentSchema.from_orm(i) for i in db_incidents] # 计算整体状态(取所有组件状态中最严重的那个) overall_status = calculate_overall_status([c.status for c in db_components]) return SystemStatusSummary( overall_status=overall_status, components=components, ongoing_incidents=incidents )

这里引用了calculate_overall_status服务函数和get_db依赖。我们需要实现它们。

创建backend/app/services/status_calculator.py:

from ..models import ComponentStatus def calculate_overall_status(component_statuses: List[ComponentStatus]) -> ComponentStatus: """ 根据所有组件的状态,计算系统整体状态。 优先级:MAJOR_OUTAGE > PARTIAL_OUTAGE > DEGRADED_PERFORMANCE > UNDER_MAINTENANCE > OPERATIONAL """ status_priority = { ComponentStatus.MAJOR_OUTAGE: 5, ComponentStatus.PARTIAL_OUTAGE: 4, ComponentStatus.DEGRADED_PERFORMANCE: 3, ComponentStatus.UNDER_MAINTENANCE: 2, ComponentStatus.OPERATIONAL: 1, } if not component_statuses: return ComponentStatus.OPERATIONAL # 返回优先级最高的状态 return max(component_statuses, key=lambda s: status_priority[s])

创建backend/app/core/database.py:

from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from ..core.config import settings engine = create_engine(settings.DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def get_db(): db = SessionLocal() try: yield db finally: db.close()

配置文件backend/app/core/config.py可以从环境变量读取设置。

5. AI辅助开发实战:加速编码与逻辑实现

这是本项目最具特色的部分。我们将展示如何利用AI工具(以Cursor为例)快速完成繁琐或复杂的编码任务。

5.1 使用AI生成数据库迁移脚本

在修改了模型(models.py)后,我们需要创建数据库迁移。传统方式需要手动编写Alembic迁移文件,但我们可以用AI辅助。

操作步骤

  1. 在Cursor中打开终端,激活虚拟环境。
  2. 输入命令:alembic revision --autogenerate -m "Add subscribers table"
  3. Alembic会自动对比模型与当前数据库,生成一个迁移文件(如a1b2c3d4e5f6_add_subscribers_table.py),但这个文件可能不完美。
  4. 打开生成的迁移文件,你可以直接向Cursor提问:“请检查这个Alembic自动生成的迁移脚本,是否有语法错误或逻辑问题?并为我添加必要的import语句注释。”
  5. AI会分析脚本,指出潜在问题(如缺失的import),并给出修正建议。你只需确认并应用。

5.2 使用AI编写复杂业务逻辑

假设我们需要实现一个“数据采集器”服务,它需要从多个源头(健康检查端点、Prometheus、第三方API)获取数据,并更新组件状态。

我们可以向AI描述需求:

“请用Python编写一个类StatusAggregator,它有以下方法:

  1. check_http_endpoint(url: str) -> ComponentStatus: 发送HTTP GET请求,根据状态码和响应时间判断状态。
  2. query_prometheus(promql: str, threshold: float) -> ComponentStatus: 查询Prometheus,根据结果值与阈值比较返回状态。
  3. aggregate_and_update(db_session): 循环所有配置的组件,根据其配置的检查方式调用对应方法,更新数据库中的组件状态,如果状态发生变化,则创建一个新的事件(Incident)或更新已有事件。”

AI(如Cursor的Chat模式)会根据描述,生成结构清晰、包含错误处理和日志记录的代码框架。开发者只需要填充具体的Prometheus查询客户端(如prometheus_api_client)和数据库操作细节即可。这大大减少了查阅文档和编写样板代码的时间。

5.3 使用AI生成前端React组件

对于前端,AI同样强大。例如,我们需要一个显示组件状态卡的React组件。

向AI提供提示

“请创建一个React函数组件,名为ComponentStatusCard,使用TypeScript。它接收一个component对象作为prop,对象包含id,name,description,status,group字段。根据status的值(operational, degraded_performance, partial_outage, major_outage, under_maintenance)显示不同的背景色和图标。使用Tailwind CSS类来设置样式。同时,显示一个最后更新时间。”

AI生成的代码可能如下:

// frontend/src/components/ComponentStatusCard.tsx import React from 'react'; import { CheckCircle, Warning, Error, Build, Info } from '@mui/icons-material'; // 假设使用MUI图标 import { Component } from '../types'; import { formatDistanceToNow } from 'date-fns'; interface ComponentStatusCardProps { component: Component; } const ComponentStatusCard: React.FC<ComponentStatusCardProps> = ({ component }) => { const statusConfig = { operational: { color: 'bg-green-100 text-green-800', icon: <CheckCircle />, label: '运行正常' }, degraded_performance: { color: 'bg-yellow-100 text-yellow-800', icon: <Warning />, label: '性能下降' }, partial_outage: { color: 'bg-orange-100 text-orange-800', icon: <Error />, label: '部分中断' }, major_outage: { color: 'bg-red-100 text-red-800', icon: <Error />, label: '严重中断' }, under_maintenance: { color: 'bg-blue-100 text-blue-800', icon: <Build />, label: '维护中' }, }; const config = statusConfig[component.status]; return ( <div className={`p-4 rounded-lg shadow-md ${config.color} border-l-4 ${config.color.replace('bg-', 'border-')}`}> <div className="flex items-center justify-between"> <div className="flex items-center space-x-3"> <div className="text-2xl">{config.icon}</div> <div> <h3 className="font-semibold text-lg">{component.name}</h3> <p className="text-sm opacity-75">{component.group || '未分组'}</p> {component.description && <p className="text-sm mt-1">{component.description}</p>} </div> </div> <div className="text-right"> <span className="font-bold">{config.label}</span> <p className="text-xs mt-1"> 更新于 {component.updated_at ? formatDistanceToNow(new Date(component.updated_at), { addSuffix: true }) : '未知'} </p> </div> </div> </div> ); }; export default ComponentStatusCard;

开发者可以在此基础上进一步调整样式和逻辑。AI快速生成了完整的组件结构、样式映射和逻辑,节省了大量手动编码时间。

6. 实现实时状态更新(WebSocket)

状态页的“实时性”至关重要。我们使用WebSocket在状态变化时主动推送给所有连接的客户端。

6.1 后端WebSocket端点(FastAPI)

FastAPI内置了对WebSocket的良好支持。创建backend/app/api/ws.py

from fastapi import APIRouter, WebSocket, WebSocketDisconnect from typing import List import json from ...services.status_calculator import calculate_overall_status from ...core.redis_client import redis_client import asyncio router = APIRouter() class ConnectionManager: def __init__(self): self.active_connections: List[WebSocket] = [] async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) def disconnect(self, websocket: WebSocket): self.active_connections.remove(websocket) async def broadcast(self, message: dict): for connection in self.active_connections: try: await connection.send_json(message) except: # 处理断开连接的异常 pass manager = ConnectionManager() @router.websocket("/ws/status") async def websocket_endpoint(websocket: WebSocket): await manager.connect(websocket) try: # 连接建立时,立即发送一次当前状态 # 状态可以从Redis缓存获取,避免每次查库 cached_status = await redis_client.get("system_status_summary") if cached_status: await websocket.send_json(json.loads(cached_status)) else: # 如果缓存为空,发送一个默认状态或从数据库计算 await websocket.send_json({"overall_status": "operational", "components": [], "incidents": []}) # 保持连接,等待客户端断开 while True: # 可以在这里接收客户端指令,例如订阅特定组件 data = await websocket.receive_text() # 处理指令(可选) # await handle_client_message(data) await asyncio.sleep(1) # 防止空循环占用过高CPU except WebSocketDisconnect: manager.disconnect(websocket)

我们需要一个机制,在组件状态或事件更新时,调用manager.broadcast()。这可以在更新数据库的Service层中实现,例如在update_component_status函数中,更新完数据库和Redis缓存后,广播新状态。

6.2 前端WebSocket连接(React)

在前端,我们使用socket.io-client来连接WebSocket。创建一个自定义Hook来管理连接和状态。

// frontend/src/hooks/useWebSocket.ts import { useEffect, useRef, useState } from 'react'; import io, { Socket } from 'socket.io-client'; import { SystemStatusSummary } from '../types'; const useWebSocket = (url: string) => { const socketRef = useRef<Socket | null>(null); const [status, setStatus] = useState<SystemStatusSummary | null>(null); const [isConnected, setIsConnected] = useState(false); useEffect(() => { // 初始化Socket.io连接 socketRef.current = io(url, { transports: ['websocket'], // 优先使用WebSocket reconnection: true, reconnectionAttempts: 5, }); socketRef.current.on('connect', () => { console.log('WebSocket connected'); setIsConnected(true); }); socketRef.current.on('system_status_update', (data: SystemStatusSummary) => { console.log('Received status update:', data); setStatus(data); }); socketRef.current.on('disconnect', () => { console.log('WebSocket disconnected'); setIsConnected(false); }); // 清理函数 return () => { if (socketRef.current) { socketRef.current.disconnect(); } }; }, [url]); return { status, isConnected }; }; export default useWebSocket;

然后在主页面组件中使用这个Hook:

// frontend/src/pages/StatusPage.tsx import React from 'react'; import useWebSocket from '../hooks/useWebSocket'; import ComponentStatusCard from '../components/ComponentStatusCard'; import { SystemStatusSummary } from '../types'; const StatusPage: React.FC = () => { const wsUrl = import.meta.env.VITE_WS_URL || `ws://${window.location.host}/ws/status`; const { status, isConnected } = useWebSocket(wsUrl); // 你也可以同时使用HTTP API作为WebSocket的降级或初始数据加载 // const { data: initialStatus } = useQuery('status', fetchStatusSummary); const displayStatus = status; // 优先使用WebSocket数据 if (!displayStatus) { return <div>加载中...</div>; } return ( <div className="container mx-auto px-4 py-8"> <div className="mb-8"> <div className={`p-4 rounded-lg text-center font-bold text-xl ${ displayStatus.overall_status === 'operational' ? 'bg-green-500 text-white' : displayStatus.overall_status === 'major_outage' ? 'bg-red-500 text-white' : 'bg-yellow-500 text-black' }`}> 系统状态: {displayStatus.overall_status === 'operational' ? '一切正常' : '存在问题'} <div className="text-sm mt-2"> WebSocket连接: {isConnected ? '✅ 已连接' : '❌ 断开'} </div> </div> </div> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {displayStatus.components.map(component => ( <ComponentStatusCard key={component.id} component={component} /> ))} </div> {/* 事件时间线部分 */} <div className="mt-12"> <h2 className="text-2xl font-bold mb-4">近期事件</h2> {/* 渲染 incidents... */} </div> </div> ); }; export default StatusPage;

7. 部署与生产环境考量

开发完成后,我们需要将应用部署到生产环境。这里给出一个基于Docker Compose的简单部署方案。

7.1 Docker化应用

后端 Dockerfile (backend/Dockerfile):

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # 运行数据库迁移和启动命令的脚本 COPY entrypoint.sh . RUN chmod +x entrypoint.sh CMD ["./entrypoint.sh"]

后端入口脚本 (backend/entrypoint.sh):

#!/bin/bash set -e # 等待数据库就绪(可选,但推荐) # wait-for-it.sh db:5432 --timeout=30 # 运行数据库迁移 alembic upgrade head # 启动FastAPI应用 exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

前端 Dockerfile (frontend/Dockerfile):

FROM node:18-alpine as build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

Nginx配置 (frontend/nginx.conf)用于处理前端路由和代理API请求。

7.2 Docker Compose 编排

创建docker-compose.yml在项目根目录:

version: '3.8' services: db: image: postgres:15-alpine environment: POSTGRES_USER: statuspage POSTGRES_PASSWORD: your_secure_password POSTGRES_DB: statuspage volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U statuspage"] interval: 10s timeout: 5s 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: 5s retries: 5 backend: build: ./backend depends_on: db: condition: service_healthy redis: condition: service_healthy environment: DATABASE_URL: postgresql://statuspage:your_secure_password@db:5432/statuspage REDIS_URL: redis://redis:6379/0 ports: - "8000:8000" volumes: - ./backend:/app # 开发时挂载代码,生产环境应移除 frontend: build: ./frontend depends_on: - backend ports: - "3000:80" # 可选:数据采集器服务 collector: build: ./backend # 可以共享后端镜像 command: python -m app.worker.collector depends_on: - backend - redis environment: DATABASE_URL: postgresql://statuspage:your_secure_password@db:5432/statuspage REDIS_URL: redis://redis:6379/0 restart: unless-stopped volumes: postgres_data: redis_data:

运行docker-compose up -d即可启动所有服务。

7.3 生产环境最佳实践

  1. 配置管理:使用环境变量或专门的配置管理工具(如HashiCorp Vault),切勿将密码硬编码在代码或Compose文件中。
  2. 安全性
    • 为数据库和Redis设置强密码。
    • 后端API应配置CORS,仅允许前端域名访问。
    • 考虑为管理API端点添加认证(如JWT)。
    • 使用HTTPS(可通过Nginx配置或云服务商负载均衡器提供)。
  3. 监控与告警:状态页本身也需要被监控。可以将其健康检查端点(如/health)纳入你的全局监控体系(如Prometheus Blackbox Exporter)。
  4. 高可用:对于生产环境,应考虑数据库的主从复制、Redis哨兵或集群,以及后端服务的多实例部署(通过Kubernetes或Docker Swarm)。
  5. 备份:定期备份PostgreSQL数据库。
  6. 日志:将所有服务的日志集中收集(如使用ELK栈或Loki),便于排查问题。

8. 常见问题与排查思路

在自建和运维状态页的过程中,你可能会遇到以下典型问题。

问题现象可能原因排查步骤与解决方案
前端无法连接到后端API1. CORS未配置。
2. 后端服务未启动或端口错误。
3. 网络策略/防火墙阻止。
1. 检查后端FastAPI的CORS中间件配置,确保允许前端源。
2. 检查后端服务日志,确认是否在指定端口监听。
3. 使用curl或浏览器开发者工具的网络面板测试API端点。
WebSocket连接失败或频繁断开1. 反向代理(如Nginx)未正确配置WebSocket代理。
2. 后端WebSocket路径错误。
3. 防火墙或负载均衡器超时设置过短。
1. 在Nginx配置中添加proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "upgrade";
2. 检查前端连接的WebSocket URL是否正确。
3. 调整代理的超时时间(如proxy_read_timeout)。
组件状态不更新1. 数据采集器(Collector)服务未运行或出错。
2. 采集器配置的目标地址不可达或返回异常。
3. 状态更新逻辑有Bug,未成功写入数据库或Redis。
1. 检查Collector服务的日志和进程状态。
2. 手动测试Collector配置的健康检查端点。
3. 在数据库和Redis中直接查询组件状态,确认数据是否已更新。检查状态计算和广播逻辑。
数据库迁移失败1. 数据库连接字符串错误。
2. 模型定义与现有数据库表结构冲突。
3. 迁移脚本存在语法错误。
1. 确认DATABASE_URL环境变量正确。
2. 检查Alembic版本历史,尝试回滚 (alembic downgrade -1) 后再升级。
3. 手动检查生成的迁移脚本,使用AI辅助分析错误。
页面加载缓慢1. 数据库查询未优化,缺少索引。
2. 未使用缓存,频繁查询数据库。
3. 前端资源过大。
1. 为频繁查询的字段(如Component.status,Incident.resolved_at)添加数据库索引。
2. 确保系统状态摘要等高频数据被缓存在Redis中。
3. 使用前端代码分割、压缩和CDN加速静态资源。
用户订阅邮件发送失败1. SMTP服务器配置错误。
2. 邮件被标记为垃圾邮件。
3. 发送邮件任务队列阻塞。
1. 检查邮件服务(如SendGrid, SMTP)的API密钥或配置。
2. 检查邮件域名SPF/DKIM记录。
3. 考虑使用异步任务队列(如Celery)处理邮件发送,避免阻塞主请求。

9. 总结与项目收益

通过这个项目,我们成功地将一个年成本6.5万美元的SaaS状态页替换为自建系统。回顾整个过程,核心收益远不止成本节约:

  1. 完全的控制权与定制能力:我们可以深度集成内部监控告警系统,定义独特的业务状态逻辑,UI/UX完全自主设计。
  2. 数据自主与安全:所有监控数据、事件历史和用户信息都存储在自己的基础设施内,满足了更高的安全和合规要求。
  3. 技术栈统一与团队成长:项目使用了团队熟悉的技术,降低了维护成本。同时,利用AI工具进行开发,让团队成员更高效地学习和实践全栈开发与DevOps技能。
  4. AI辅助开发的真实体验:本项目是AI在软件工程中落地的绝佳案例。从生成样板代码、编写业务逻辑到调试和文档编写,AI工具(如Cursor)显著提升了开发效率,尤其适用于快速原型开发和填补知识盲区。
  5. 可扩展的坚实基础:我们构建的系统架构清晰,易于扩展。未来可以轻松添加新的监控源、通知渠道(如Slack, Discord)、多租户支持或更复杂的分析功能。

给读者的建议: 如果你也面临类似的SaaS成本或定制化问题,自建或许是一个值得考虑的选项。启动前,请务必评估团队的技术能力和运维成本。从一个小而精的核心功能开始,利用现代开发工具和AI辅助,完全可以在短时间内构建出满足生产要求的系统。本项目的完整代码已在GitHub开源,你可以基于此进行二次开发,快速搭建属于你自己的状态页。

← 返回列表