资讯动态

caveman项目复盘:用Python标准库打造零依赖内部服务

发布时间:2026/10/27 9:28:42 来源:尧图企业网站定制
caveman这个词最近成了我手头一个项目的代号。不是跟风什么原始人生活方式实际情况是我把一个正在维护的内部服务从全家桶技术栈里捞了出来一路砍到只剩 Python 标准库然后把项目命名为 caveman取穴居人的意思——没有花哨工具没有庞大框架靠最简单的工具把事办成。这个服务叫 linkbox是一个内部链接收藏与跳转工具。最终的运行形态是一个 Python 文件、一个 SQLite 数据库文件、一个 systemd 服务外部依赖为零。这篇文章是我对 caveman 项目的完整复盘为什么这么干、怎么一步步落地、中间踩了哪些坑以及什么情况下千万不要学我。如果你是中小团队的技术负责人或者正被依赖地狱和构建速度折磨这个思路应该能给你一些新启发。本文会分成五块来聊首先是项目思路然后是关键技术选型再给一套完整的实操落地记录接着是踩坑实录最后把方案的适用边界划清楚。1. 项目思路caveman 是什么它解决什么问题1.1 工具通胀复杂度不会白来但也不一定有用先说我观察了很久的一个现象叫做工具通胀。一个内部工具的真实需求往往很小比如链接收藏就是存一条 URL、打几个标签、提供一个跳转接口。放在十年前一个 PHP 文件或者一个 Python 脚本就搞定了。但你要是今天把它落成一个标准团队项目会经历什么前端选型要 React要路由、状态管理、构建工具链后端要框架、ORM、分层架构、接口规范文档部署要容器镜像、编排平台、CI/CD 流水线数据要数据库实例、连接池、迁移脚本。每一步单看都有道理合在一起却把一个 200 行的工具膨胀成 2000 个文件的仓库。我见过太多仓库package.json 里躺着几百个依赖其中一半从项目创建到现在都没人用过。构建环境一升级锁定的版本崩一片新人入职光装环境要花一个礼拜。这不是技术债这是技术通胀——技术在膨胀业务价值一分没涨。caveman 就是我对抗技术通胀的一次实验。核心思路只有一句话能用标准库解决的绝不引入第三方库能用单文件承载的绝不拆服务能抓包看明白的绝不先上链路追踪。1.2 三条核心军规我在项目里给自己立了几条军规后续所有技术决策都围绕它们展开。军规一依赖数封顶。核心服务对第三方依赖的额度是 0只用标准库。边缘脚本最多放宽到 1 到 2 个但必须在注释里写明引入理由。这一条最狠也最有效因为它逼着你在引入任何包之前重新评估需求是否真的存在。军规二优先标准库。Python 标准库已经覆盖了绝大多数基础需求http.server 做 HTTP 服务json 做序列化sqlite3 做持久化hashlib 做摘要concurrent.futures 做并发。这些模块看着朴素但稳定性极高、升级风险为零而且你完全掌控它。站在标准库的肩膀上就不需要额外加一层脚手架。军规三最简可运行的架构才是好架构。业务规模没有到某个量级之前单体脚本要优先于模块化单体模块化单体要优先于微服务。我始终认为架构不是一步到位设计出来的而是被业务规模逼出来的。提前上复杂架构等于让团队给不存在的流量付租金。这三条军规不是拍脑袋定的而是我把过往项目里踩过的坑一条条反推出来的。后面会详细讲它们怎么落地到具体代码。1.3 极简不等于裸写肯定会有人质疑这不是退化回菜鸟写法吗裸写 HTTP 处理、用字符串拼 HTML那不是十年前的老套路我需要澄清一点caveman 不是反对框架而是反对无意识引入框架。框架是工具不是信仰。一个框架能稳定解决问题、团队熟悉、升级成本可控那当然可以用。caveman 反对的是需求还没说清楚就先上架构别人都在用所以我也要用。换句话说caveman 是一种用最少可维护工具达成目标的工程态度跟水平高低没有关系。把复杂系统做简单永远比把简单系统做复杂难得多。前者需要你看清需求本质后者只需要往项目里堆依赖。2. 关键技术选型一个石器时代服务怎么搭这一章把 caveman 技术方案的关键决策拆开看网络层用什么、数据层怎么存、前端怎么处理、依赖怎么控制。2.1 网络层用 http.server 撑起 API 服务linkbox 要提供三个能力网页入口、API 接口、短链接跳转。在 Python 标准库里视野里最合适的是 http.server 模块。这里有个关键点用 BaseHTTPRequestHandler 和 ThreadingHTTPServer 的组合而不是更基础的 HTTPServer。原因在于线程模型。HTTPServer 是单线程模型一个请求没处理完后面的请求全部排队只要遇到一个慢请求整个服务就像卡死。ThreadingHTTPServer 则是一个请求一个线程慢请求不会阻塞其他请求。对于 linkbox 这样的小流量内部工具线程开销完全可以接受。核心代码大概是这样的from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import json class LinkHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path.startswith(/go/): self._handle_redirect() elif self.path.startswith(/api/links): self._handle_list_links() else: self.send_error(404) def do_POST(self): if self.path.startswith(/api/links): self._handle_create_link() else: self.send_error(404) def _send_json(self, data, status200): body json.dumps(data).encode(utf-8) self.send_response(status) self.send_header(Content-Type, application/json; charsetutf-8) self.send_header(Content-Length, str(len(body))) self.end_headers() self.wfile.write(body) if __name__ __main__: server ThreadingHTTPServer((0.0.0.0, 8000), LinkHandler) server.serve_forever()这段代码里没有第三方框架没有装饰器路由没有配置文件但一个能用的 HTTP 服务已经成型。路由逻辑就在 do_GET 和 do_POST 里总共不到二十行谁来看都能立刻看懂。框架路由确实优雅但为此引入几百个依赖文件对一个只有几个页面的内部工具来说性价比太低。从实际经验看http.server 有两点提醒需要留意。第一默认线程数不受控遇到极端情况可能线程膨胀建议用 concurrent.futures 包一层线程池做限制后面实操章有具体做法。第二生产环境前面最好挂一层 nginx 做 TLS 和访问日志让标准库服务专注业务逻辑。2.2 数据层SQLite 当数据库JSON 做缓存数据存储是这次选型里争议最大的点。我一开始考虑直接存 JSON 文件后来发现两个痛点一是搜索和筛选要在内存里全部扫一遍数据到上千条就开始力不从心二是多线程写入同一个 JSON 文件时会出现覆盖丢失的问题。最后选了 Python 标准库自带的 sqlite3。SQLite 在小型内部工具里完全够用它不需要独立部署数据库服务备份就是复制一个文件迁移也不需要独立的迁移工具。对 linkbox 每天几百条写入的场景SQLite 的性能绰绰有余而且 SQL 的筛选能力比在内存里遍历不知道强到哪里去了。建表语句极为简单CREATE TABLE IF NOT EXISTS links ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, title TEXT NOT NULL, tags TEXT NOT NULL DEFAULT , created_at TEXT NOT NULL DEFAULT (datetime(now)) ); CREATE INDEX IF NOT EXISTS idx_links_tags ON links(tags);写入和查询用参数化 SQL不拼字符串防止注入。举个例子import sqlite3 def create_link(url, title, tags): conn sqlite3.connect(linkbox.db) try: conn.execute( INSERT INTO links (url, title, tags) VALUES (?, ?, ?), (url, title, tags), ) conn.commit() finally: conn.close() def search_links(keyword): conn sqlite3.connect(linkbox.db) try: rows conn.execute( SELECT * FROM links WHERE title LIKE ? OR tags LIKE ?, (f%{keyword}%, f%{keyword}%), ).fetchall() return rows finally: conn.close()这段代码没有任何 ORM但逻辑清楚、性能可预期。说到 SQLite 的实战经验强烈建议开启 WAL 模式可以显著改善并发读写时的体验。一行 PRAGMA 语句PRAGMA journal_modeWAL;执行后写事务不再阻塞读请求。注意SQLite 适合低并发写场景开启 WAL 后并发读体验大幅改善但它仍然不是为高并发写入设计的数据库。如果业务写入 QPS 上百就该考虑迁移到 PostgreSQL 了但那一天意味着你的业务已经长起来了。这一层设计背后有一个常见实践补充如果数据量真的超过 SQLite 承受范围比如单表几百万行、QPS 上千那就说明业务已经长大了到时候再迁移也不迟。caveman 的原则是先让数据量来证明它需要重型方案而不是一开始就给它安排上。2.3 前端与模板放弃构建工具前端是砍得最狠的部分。linkbox 只有三个页面首页、收藏表单、列表页交互程度极低。如果按常规思路引入一套前端框架加构建工具光 node_modules 就有几十万个文件构建时还要处理依赖图、转译、打包。对一个内部工具来说这笔账怎么算都不划算。我的做法是原生 HTML 加少量 JavaScript页面用 Python 的 f-string 直接渲染。这里有一个安全细节必须提渲染用户输入时一定要做 HTML 转义否则会有 XSS 风险。我写了一个十行的小函数解决这件事。import html def render_page(title, body): return f!DOCTYPE html html headmeta charsetutf-8title{html.escape(title)}/title/head body{body}/body /html这个版本没有前端框架没有打包器没有热更新页面加载却快得惊人。后来我在项目里加了一点原生 JavaScript用 fetch 调 API代码量控制在 200 行以内依然没有引入任何 npm 包。我要说明适用范围这种方案适合页面少、交互简单、使用者是内部员工的服务。如果要做一个面向外部用户、状态复杂、需要大量交互的 Web 应用请老老实实用成熟框架caveman 不背这个锅。2.4 依赖封顶的检查方法砍掉依赖只是开始难的是后续持续不再引入依赖。我在 CI 里加了一个简单的检查脚本扫描 import 语句统计来自 site-packages 的第三方模块数量超过阈值就构建失败。除了脚本检查日常 review 也是防线之一。最核心的动作是这几个requirements.txt 里要么为空要么只有数字个依赖每次团队讨论要不要加个 XX 库时先写清楚要解决的具体问题任何依赖升级都必须触发一次完整回归测试因为在一个零依赖的系统里根本不存在升级依赖这件事。这一套检查逻辑没有用任何额外工具就是 shell 脚本加 grep充分体现了 caveman 的精神用最原始的手段解决最实际的问题。3. 实操记录linkbox 从零到部署这一章记录我实际搭建整个过程包含需求定义、核心代码、并发参数和部署资源。你可以直接照着复现。3.1 需求收敛先做减法实操开始前先列需求更重要的是列不做清单。linkbox 的需求收敛到三个功能收藏提交 URL、标题、标签浏览列表展示、按标签筛选、关键词搜索跳转通过短链接跳转到收藏的 URL。明确不做的有用户系统和权限控制、统计分析大盘、浏览器插件、移动端适配。这些功能全部砍掉之后整个项目的复杂度和一个普通博客脚本差不多。我认为这是 caveman 能成立的最重要条件需求不收敛再极简的技术栈也没有意义。3.2 核心代码实现完整代码在项目仓库里这里拆解最关键的三块。第一块是路由与请求分发。前面已经给了 BaseHTTPRequestHandler 的骨架实际运行中我把请求路径解析成一个简单的路由表按路径前缀分发def _route(self): path self.path.split(?)[0] if path.startswith(/go/): return self._redirect_handler(path) if path /api/links: if self.command GET: return self._list_handler() if self.command POST: return self._create_handler() return self._not_found()第二块是跳转逻辑。/go/ 后面的 ID 从数据库查到对应 URL返回 302 重定向。这里有个细节状态码用 302 而不是 301因为 301 会被浏览器永久缓存之后改链接会很麻烦。这是踩过坑才知道的。提示跳转链接如果被浏览器缓存成永久重定向等你后来想修正 URL用户那边还是一直访问旧地址。内部工具尤其要用 302除非你确定这个链接永远不会变。第三块是静态资源。页面里的 CSS 和 JavaScript 文件很小我用 /static/ 前缀直接读文件返回顺手做个 Content-Type 判断总共二十行代码。3.3 并发与线程池参数估算小服务也要讲并发参数不能裸上 ThreadingHTTPServer。我基于 Littles Law 来做估算并发连接数等于 QPS 乘以平均响应时间。linkbox 的实际场景大概是峰值 50 个请求每秒平均响应 100 毫秒乘起来并发数是 5。考虑到线程创建的系统开销线程池开 10 到 20 个就非常宽裕了。我把 ThreadingHTTPServer 替换成了自定义的线程池版本核心做法是继承 socketserver.ThreadingMixIn重写 process_request 方法将请求丢进一个固定大小的线程池。from concurrent.futures import ThreadPoolExecutor class PooledHTTPServer(ThreadingHTTPServer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._pool ThreadPoolExecutor(max_workers16) def process_request(self, request, client_address): self._pool.submit(self.process_request_thread, request, client_address)这样即使接口被打满线程数也固定在 16不会拖垮操作系统。这是从真实运维事故里学到的无上限线程池在流量突增时会瞬间创建上千个线程直接把内存吃光。3.4 系统服务部署与资源对比部署方式我用 systemd避免 nohup 方式容易丢日志和崩溃后不自愈的问题。单元文件大致如下[Unit] Descriptionlinkbox caveman service Afternetwork.target [Service] WorkingDirectory/opt/linkbox ExecStart/usr/bin/python3 /opt/linkbox/linkbox.py Restartalways RestartSec3 Userlinkbox [Install] WantedBymulti-user.target服务起来之后实测资源占用让我自己都愣了一下常驻内存 38MBCPU 平时几乎为零进程数一个第三方依赖零个。对比之前的 Spring Boot 版本那个版本启动就要 700MB。差距来自两个地方一是没有几十万字节的框架类文件二是没有中间层运行时的开销。4. 踩坑实录没有框架之后遇到的那些事没有框架带来自由也带走了一些惯性。这一章我真实复盘了几个让人头疼的问题以及最终的排查与解法。4.1 没有 ORM查询逻辑怎么组织才不乱第一个大问题是查询逻辑。以前有 ORM定义 Model、调用 filter 方法一切都很顺手。在纯 sqlite3 环境下我一开始把所有查询都塞在路由处理函数里半个月后代码就乱成一团。解决方法很朴素把数据访问收敛到独立的仓储函数模块。所有 SQL 语句只出现在那个模块里路由层只调用函数。这就够了不需要引入任何抽象框架。同时我在建表语句里维护一个 schema_version 表记录结构变更用最原始的迁移脚本解决升级问题。别人看到可能会觉得简陋但它的稳定性和可理解性极高。4.2 日志和请求追踪怎么做第二个问题是没有现成的请求日志中间件。之前用框架日志和请求 ID 是自动的。回到裸代码这些都要自己做。我用 logging 模块打日志然后在路由分发处生成一个 request_id放进每个日志条目。排查问题时直接 grep 这个 ID就能把一个请求从进入到返回的完整链路看明白。import uuid, logging logger logging.getLogger(linkbox) def _dispatch(handler): request_id uuid.uuid4().hex[:8] logger.info(request %s %s %s, request_id, handler.command, handler.path) try: handler._route() except Exception: logger.exception(request %s failed, request_id) handler.send_error(500)这二三十行代码把之前框架里最常用的功能找补了回来。不需要上什么全面追踪平台内部工具做到这个程度已经够用。4.3 团队里被框架惯坏的同事怎么处理这一点是纯管理问题但也实实在在影响项目推进。团队里部分同事习惯了框架自带的模式对没有 ORM、没有路由装饰器的代码无法适从第一反应是这代码好原始。我的处理方式有三个要点。第一先立文档一页纸的维护指南写清楚访问入口、测试方式、生产启动命令。第二留好护栏CI 里加上依赖数量检查和基本冒烟测试保证任何人改动后系统不会坏。第三做示范每次修复 bug 时在群里同步一段为什么可以这么简单的解释。几个月下来团队对新代码的态度从抗拒变成了接受甚至有人开始主动在别的小工具里复制这套模式。4.4 常见问题速查表收集了几个最容易踩的坑做成表格方便查阅。问题原因解法多线程写入数据丢失JSON 文件并发写互相覆盖改用 SQLite开启 WAL 模式页面出现乱码或注入渲染时未做 HTML 转义使用 html.escape 转义用户输入跳转链接被缓存使用了 301 状态码改用 302 临时重定向流量突增线程爆炸线程池无上限固定线程池max_workers 设 16排查问题无从下手没有请求链路日志每个请求生成 request_id 并打日志团队抗拒裸代码缺乏文档和约定补维护文档CI 加护栏逐步示范表格里没有一条需要引入新框架才能解决这本身就是 caveman 想证明的事。5. 边界什么时候该用 caveman什么时候千万别用任何方案都有边界caveman 绝非万能。这一章把适用和不适用两种情况都说透免得有人拿着这篇文章去为难自己的项目。5.1 值得用的场景内部业务工具、运营后台、管理面板使用者是内部员工对体验要求不极端需求变化快简单技术栈改起来更快。原型验证与概念验证POC用最少成本验证想法跑通了再换成正式架构省下的时间都是实打实的。生命周期短的小系统比如某次活动页、一次性数据迁移工具不需要为三个月后的系统提前准备十年后的架构。个人自动化任务定时脚本、文件整理、API 对接一个文件跑完即走。被依赖地狱折磨的存量小项目可以选一个小模块先做降级改造验证效果。这些场景的共同点是业务规模小、变化快、生命周期短、维护者少。技术栈的选择应该服务这个现实而不是反过来。5.2 坚决不用的场景面向外部用户的高并发核心业务QPS 上千、要求 99.99% 可用性的系统裸代码加单机部署无法满足可用性要求这不是人力能补回来的。复杂状态前端应用交互复杂、状态多、需要实时协作的 Web 应用原生 JS 会写到你崩溃成熟前端框架是必要的。严格权限审计系统需要细粒度权限、操作审计、合规要求的系统必须依赖成熟的框架和中间件自己手写风险太大。没有愿意维护裸代码的团队如果团队只有你会写这种风格且你离职后没人能接手那么极简方案会变成一场灾难。业务已明确增长且要长期演进的项目数据量、团队规模都在指数增长此时投资一些复杂度是合理的预期。5.3 小范围试验的三个建议如果你认可这套思路但不想立即把整个项目推翻我建议先小范围试验。第一选一个边缘模块做降级比如内部报表接口、数据同步脚本改造风险低效果却直观。第二明确试验指标包依赖数量变化、启动时间、内存占用、部署耗时用数字说话比争论架构更有效。第三设定回归条件如果连续三个月业务增长超过预期再评估是否需要迁移到重型方案。caveman 不是一个终点方案它是一种审视习惯。用它做参照能帮你发现系统里那些不知道为什么存在的复杂度。这些复杂度本身往往是历史遗留不是业务必需。caveman 项目实际跑了半年多最让我意外的不是省下的那点资源而是我重新捡回了一种把工具完全掌握在手里的感觉。框架解决的问题是真实的但框架带来的错觉也是真实的一个系统里有多少复杂度属于业务本身又有多少只是被工具链传染的惯性很多人从没仔细算过这笔账。我给自己的下一步计划是把这种极简审视变成常态动作。每次大到选型、小到加依赖都先问一句这周写下的代码里有多少是在解决业务问题有多少是在给工具链填坑这个问题不起眼但它能保证复杂度永远是你主动选择的而不是悄无声息堆上来的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑