当你在阿塞拜疆经营一家汽车经纪公司,并从美国进口报废车辆时,你的软件问题与硅谷初创公司截然不同。你优化的目标不是百万级并发用户。你优化的是清晰度、在线率,以及在半夜与相隔 12 个时区的拍卖行协调时,能够亲自动手修复问题的能力。这正是我构建 AutoMakler 时所处的境地。该平台处理从实时拍卖抓取、Carfax 查询到交付估算和支付处理的一切事务。这是一个服务于真实客户的真实生产系统,它运行在大多数开发者口中“极其乏味”的技术栈之上。

没人想拿来吹嘘的技术栈

没有 React。没有 Vue。没有 Redis,没有 Celery,也没有 WebSocket 服务器。后端是使用纯 Python 的 FastAPI。数据库是 PostgreSQL。前端是使用 Jinja2 模板、Bootstrap 和少量 vanilla JavaScript 的服务端渲染 HTML。抓取方面,我使用 Playwright。所有内容都作为一个直接提供 HTML 的单一 Python 进程运行。

没有构建步骤。没有需要审计的 node_modules 文件夹,没有需要配置的转译器,也不需要紧跟前端框架的更迭。部署时,我只是在移动 Python 文件和模板,而不是在编排一套打包器流水线。这种简单并非妥协,它正是核心所在。

如何在没有消息代理的情况下进行任务队列

抓取实时汽车拍卖无法同步进行。由于 Playwright 需要加载页面、执行 JavaScript 并提取数据,单次抓取可能需要几秒钟。在执行期间阻塞用户是不可取的。标准做法是安装 Redis,配置 Celery,并启动一个工作进程池。我跳过了这一切。

相反,AutoMakler 使用 Postgres 作为自己的任务队列。当用户触发抓取时,应用程序会在 tasks 表中写入一行状态为 pending 的新记录。一个 asyncio 后台任务会获取该行并启动浏览器抓取。与此同时,浏览器每隔三秒轮询一次轻量级端点以检查状态。当该行更新为 completed 时,页面刷新并显示结果。

这种模式之所以有效,是因为轮询间隔足够短,感觉响应迅速;同时又足够长,不会压垮服务器。对于计算机来说,三秒钟是永恒,但对于等待外部拍卖网站的人类来说,几乎察觉不到。数据库原生处理并发,而且由于任务只是 Postgres 中的行,我可以通过简单的 SQL 查询来检查队列,而无需在 Celery 日志或 Redis 键中苦苦搜寻。

在没有工作进程池的情况下保持服务器运行

浏览器自动化非常消耗内存。如果同时启动过多的 Playwright 实例,你的服务器就会崩溃。传统的解决方法是使用带有并发限制的管理工作进程池,通常由上述 Redis 和 Celery 组合提供支持。我只用了一行 Python 代码:asyncio.Semaphore

信号量限制了可以同时运行的浏览器实例数量。当新的抓取请求进入时,它要么立即占用一个名额,要么等待名额释放。这一切都在同一个进程内发生。没有外部编排器会失效,没有工作进程会悄无声息地死亡,也不需要额外的基础设施来监控。我的内存占用保持可预测,且保护服务器的代码就在使用它的代码旁边,而不是隐藏在部署清单中。

通过单个回调 URL 进行资金路由

支付处理引入了一个我无法改变的限制。我的支付网关每个商户账户仅允许一个回调 URL,但我需要通过该单一账户处理两个独立项目的交易。建立第二个商户配置文件意味着额外的费用、额外的合规要求以及额外的文书工作,而一家小型经纪公司没有时间处理这些。

解决方法是在将客户引导至网关之前,直接将项目名称编码到订单 ID 字符串中。当回调到达我的服务器时,AutoMakler 会解码该 ID,识别该款项属于哪个项目,并将通知路由到正确的内部处理器。现有的逻辑保持不变。这是一种增量设计:我没有重写支付流程,只是让标识符携带了更多的上下文信息。这种“黑客”手段在事后看来显而易见,但却节省了数小时的架构调整工作。

无需 WebSockets 即可工作的聊天功能

Customer support chat is usually where engineers cave and add WebSockets. I needed in-app messaging, but I also needed to keep the infrastructure footprint tiny. So I reused the same polling strategy that powers the auction scrapes.

Messages are stored in Postgres. When a user sends a message, it writes to the table. The client polls for updates, and the UI reflects new messages and read receipts in near real-time. To keep this fast even as the conversation table grows, I added a Postgres partial index that only covers unread messages for active conversations. The database does not waste cycles scanning old history, and the query planner can satisfy most chat lookups with a tight index range scan.

For a support chat where a few seconds of latency is acceptable, this is perfectly adequate. The users get the feedback they need, and I never had to debug a stale WebSocket connection or manage a separate socket server.

The Honest Downsides

This architecture makes real trade-offs, and pretending otherwise would be dishonest. Polling is chatty. Every three seconds, every active client hits the server. The bandwidth and query load are higher than a persistent socket connection would demand. If the Python process restarts, any in-flight background task dies immediately because there is no external worker to pick it back up. I accept this because the tasks are small and the cost of a retry is low. A failed browser scrape can simply be re-triggered by the user.

There is also a ceiling to this approach. If AutoMakler ever needs to serve thousands of simultaneous scrapes, the single-process model with polling will strain. But that is not the business I am in. I need reliability for dozens of concurrent users, not