在 express 项目中接入 OpenTelemetry
技术探索,不止是写代码

记得刚入行那会儿,我满脑子都是“我要成为一个技术大牛”。当时觉得,只要能把代码写得又快又好,能看懂各种算法和设计模式,就能在技术这条路上走得远。但工作几年之后我发现,真正的技术成长,远远不只是敲代码这么简单。
这篇文章我想从一个真实项目经历讲起,谈谈我在技术探索与实践中的一些感受和思考。希望它能给正在这条路上前行的你一些启发,或者至少让你觉得:原来大家都一样,也曾踩过坑、也曾在深夜里怀疑自己。
项目背景:一场线上突发故障
去年年底,我在一家做在线教育的公司负责后端架构优化。我们的主平台是一个基于 Node.js 的服务集群,承载了数万并发用户的访问流量。那天晚上十点左右,突然收到报警:用户登录接口响应时间暴增到几秒甚至超时,前端报错一片。
我们第一时间查看日志,发现数据库连接池被打满了。进一步分析后发现问题出在一个新上线的功能模块——为了提升用户体验,我们在用户首次登录时新增了一个数据预加载机制,提前把后续要用的数据拉取好缓存起来。
按说这个逻辑是没问题的,但问题在于,这个预加载操作是串行执行的。也就是说,用户登录请求来了以后,必须等所有预加载任务完成才能返回响应。这导致每个登录请求处理时间变长,进而拖慢整个系统响应速度,最终形成连锁反应。
挑战来了:如何在不影响业务的前提下快速修复?
这个问题看似简单,实则牵一发而动全身:
- 如果直接回滚版本,虽然能解决当前问题,但会影响用户体验;
- 如果只是去掉预加载部分,那相当于牺牲了原本的设计初衷;
- 更重要的是,我们需要搞清楚:为什么这个改动会引发严重性能问题?是系统承受不了并发压力,还是代码结构存在隐患?
我花了一晚上的时间分析整个调用链路,发现有几个关键问题:
- 没有异步处理机制:预加载全部是同步请求,严重影响主线程响应速度;
- 缺乏熔断与限流策略:当某个外部服务响应慢或不可用时,没有自动降级机制;
- 监控体系不完善:虽然有监控大盘,但对具体接口调用耗时和内部链路追踪覆盖不足。
解决方案:技术不是孤立的工具,而是协作的艺术
第二天早上,我和团队召开了紧急会议,决定采取以下几个措施:
1. 引入异步任务队列
我们将原本在登录过程中触发的预加载逻辑,改为通过消息队列(MQ)异步处理。这样用户的登录流程可以在主线程中快速完成,具体的预加载任务由独立的工作进程来执行。
// 原始同步预加载(伪代码)
async function login(req) {
const user = await db.getUser(req.body);
const data1 = await loadResourceA(); // 资源 A
const data2 = await loadResourceB(); // 资源 B
await cacheDataToRedis(data1, data2); // 缓存资源
return res.send({ user });
}

// 改进后的异步方式(使用 RabbitMQ 示例)
const amqp = require('amqplib');
async function login(req) {
const user = await db.getUser(req.body);
const conn = await amqp.connect('amqp://localhost');
const ch = await conn.createChannel();
await ch.assertQueue('preload_queue');
ch.sendToQueue('preload_queue', Buffer.from(JSON.stringify({
userId: user.id,
timestamp: Date.now()
})));
return res.send({ user });
}
这里省略了错误处理和持久化细节,实际中需要加入重试、死信队列等机制。
2. 加入熔断与限流机制
我们引入了 Resilience4JS 和 Express 中间件组合来为关键接口添加容错能力。比如设置接口每秒最多 5000 个请求,并在失败率达到一定阈值时自动进入熔断状态。
const RateLimiter = require('express-rate-limit');
const apiLimiter = RateLimiter({
windowMs: 60 * 1000, // 1 minute
max: 5000,
message: 'Too many requests from this IP.'
});
app.use('/api', apiLimiter);
同时在异步任务处理层加入熔断器:
const { CircuitBreaker } = require('@agile-ts/circuit-breaker');
const cb = new CircuitBreaker(() => fetchExternalResource(), {
timeout: 3000,
errorThresholdPercentage: 50
});
3. 完善监控体系
我们在原有的 Prometheus + Grafana 监控基础上,增加了分布式追踪组件 Zipkin,用于记录完整的请求链路。
service:
name: user-service
exporters:
zipkin:
endpoint: http://zipkin:9411/api/v2/spans
这样我们就能直观地看到每个接口的调用时间、异常情况、以及链路依赖关系。
开发过程中的几个坑
说实话,改完这些看起来挺顺的,但在落地过程中我们也踩了不少坑:
坑一:本地调试 MQ 不通
我们刚开始在本地测试消息队列的时候,发现 RabbitMQ 总是连不上。一开始以为是代码写错了,结果折腾了半天才发现是 Docker 容器的网络配置出了问题。后来统一用 Docker Compose 启动整套环境才解决。
教训: 环境一致性很重要,尽量用容器化工具保持本地和线上一致。
坑二:异步任务堆积
上线初期有一段时间,预加载任务积压了很多未被消费的消息,导致 Redis 缓存迟迟没生成。最后查出来是因为工作进程太少了,而且其中一个节点 CPU 被占满后无法继续处理。
解决方案:
- 扩容消费节点;
- 使用 Kubernetes 做弹性扩缩容;
- 给工作进程加健康检查,有问题自动重启。
坑三:监控埋点漏掉关键字段
有一段时间我们看不到完整调用链,后来发现是服务之间传递 Trace ID 的时候格式有问题。虽然用了标准库,但因为中间夹杂了部分非 HTTP 请求(比如 Kafka),导致上下文没正确透传。
经验总结: 分布式系统里,跨协议追踪是个难点,要尽早规划好全链路打通的方式。
效果与收益:稳了!
这一轮整改上线之后,系统稳定性明显上升:
| 指标 | 上线前 | 上线后 |
|---|---|---|
| 登录接口平均响应时间 | 1800ms | 220ms |
| 登录超时率 | 12% | <0.5% |
| 数据预加载完成率 | - | >98% |
| 报警频率 | 高频 | 几乎归零 |
更难得的是,这次改进也为后续的技术演进打下了基础:
- 我们开始推动微服务拆分;
- 异步任务中心成为公共能力模块;
- 全链路追踪成为每个新服务的标准配置项。
我的经验分享:技术成长离不开三点
经历过这一次项目之后,我对“技术探索与实践”有了更深的认识,这里想跟大家分享几点自己的体会:
1. 不要只盯着代码本身
很多时候,技术瓶颈并不是写不好某段逻辑,而是整体系统设计不合理。代码只是工具,系统的可维护性、可扩展性、可观测性才是真正的挑战。
2. 多看日志,少拍脑袋
遇到问题先别急着改代码,先把日志、监控、调用链理清楚。有时候你以为的问题根本不是问题根源所在。
3. 学会权衡与妥协
理想主义谁都想过,但现实中往往需要做出让步。比如为了尽快止损选择临时降级策略,而不是马上重构整个模块。这是技术人的成熟表现。
4. 持续学习比一次性掌握更重要
现在技术更新太快,很多知识学完两年就过时了。真正有用的,是那种面对新技术能快速上手、理解其核心原理的能力。
5. 技术之外也要懂业务
那次事故之所以会发生,是因为我们没有充分评估预加载功能对系统整体的影响。技术是服务于业务的,脱离业务谈技术就是耍流氓。
最后的小感悟:写代码的人要有敬畏之心
有时候我觉得,写代码就像种树。前期你可以随便挖个坑就埋下去,但如果根基不牢,风一大就倒了。技术探索也是如此——我们要做的不只是堆叠炫酷的新技术,更要关注它们是否适合当前的土壤和气候。
每次写完一段代码,我都习惯性地问自己:“这段代码会不会让人半夜爬起来排查?”如果答案是肯定的,那就要重新考虑设计思路了。
技术探索从来都不是一个人的战场,也不是一蹴而就的事情。希望我们在不断探索的路上,都能保持一份热爱、一份耐心,也保持一点谦卑。
如果你也正走在技术的成长之路上,愿我们共勉!

评论 0