在不确定性中前行:一次技术探索与实践的思考
开篇:为什么我们需要持续的技术探索?

2022年初,我所在的团队面临一个看起来“简单”的重构项目——将原有的用户行为埋点系统进行性能优化和架构升级,以支撑业务快速增长下的日均数十亿条数据采集需求。这个任务听起来并不复杂,但真正做起来才发现,它像一块石头投入了湖面,激起了层层涟漪。
我们原系统的实现逻辑很简单粗暴:前端触发事件后通过 HTTP 接口上传到后端,后端接收到后直接写入 Kafka,然后下游处理。这套机制在初期完全够用,但在用户量快速上涨后问题逐渐暴露出来:接口频繁超时、Kafka 分区积压严重、数据丢失偶有发生。
更让人头疼的是,产品方提出了新的诉求:“我们需要更高的采集精度和更低的延迟”。这意味着不仅要解决现有系统的问题,还要提升整个链路的数据质量。我们意识到,这不是简单的性能调优就能搞定的事情,而是一次从底架构到上层逻辑的全面审视和重构。
于是,在一次小组会议上,我说出了那句大家耳熟能详的话:“这次我们得好好地做一些技术探索。”接下来的内容,就是那段经历中真实的挣扎、尝试与成长过程。
问题描述:一场“不起眼”的性能瓶颈带来的连锁反应

原始系统的瓶颈
我们的原始系统结构如下:
前端SDK -> Nginx + Java服务(SpringBoot) -> Kafka Producer -> Kafka集群
这套架构看似清晰,但在数据吞吐量增长到每天超过15亿条的时候,出现了几个关键问题:
- HTTP接口不稳定:Java服务负责接收HTTP请求,由于每次写Kafka是同步操作(为了控制错误码返回),导致请求线程被阻塞,QPS下降明显。
- Kafka分区热点严重:我们一开始使用了 user_id 作为 key 来做消息分发,这导致某些用户量大的业务产生分区倾斜,进而引起消费滞后。
- 高可用性差:Java服务一旦重启或扩容,都会出现短时间内的服务不可用,影响客户端埋点上报。
- 数据可靠性低:部分请求因为网络波动或服务异常而导致丢弃,缺乏补偿机制。
这些问题叠加在一起,使得我们在高峰期间经常看到监控告警满屏红,运维同学半夜还在查日志、调参数。
解决方案:从异步化到架构分层的设计思路

架构设计目标
面对这些痛点,我们设定了以下改造目标:
- 性能保障:支持更高并发访问,降低接口延迟。
- 稳定性提升:避免单点故障,提高容错能力。
- 数据准确性增强:引入补偿机制,减少数据丢失。
- 扩展性友好:未来新增功能能灵活接入,不破坏原有结构。
新架构选型与权衡
基于这些目标,我们决定采用如下的新架构:
前端SDK
↓
Nginx(负载均衡)
↓
API网关层(Golang编写)
↓
高性能缓存队列(Redis Stream / 异步Buffer)
↓
Worker层(批量写 Kafka)
↓
Kafka集群 -> 实时/离线处理管道
为什么选择 Golang 编写 API 层?
Java 虽然功能强大,但在高并发 IO 场景下存在明显的性能瓶颈。我们做了对比测试,发现相同硬件条件下,Golang 的 HTTP 服务在处理每秒万级并发时表现优异,而且内存占用小得多。对于只是做数据转发的任务,Golang 成为理想选择。
为什么加入 Redis Stream 作为 Buffer?
最初我们考虑直接异步化写 Kafka,但发现这样虽然提升了吞吐量,却牺牲了对失败的感知能力,且一旦 Kafka 出现短暂异常,会导致大量请求直接失败。因此我们决定加一层缓冲层,既保证写入效率,又能在后端服务暂时不可用时暂存数据,提升整体链路稳定性。
最终我们采用了 Redis Stream 这种结构,因为它天然支持持久化、多消费者组、自动重试等特性,非常适合做这种中间缓存队列。
代码实践:几个关键组件的实现片段

API 网关层(Golang)
下面是 Golang 接收请求并写入 Redis Stream 的核心代码片段:
package main
import (
"github.com/go-redis/redis/v8"
"net/http"
)
func handleTrack(w http.ResponseWriter, r *http.Request) {
// 获取请求体数据
body := parseRequestBody(r)
// 写入 Redis Stream
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
ctx := context.Background()
_, err := client.XAdd(ctx, &redis.XAddArgs{
Stream: "event_stream",
MaxLen: 1000000,
Values: map[string]interface{}{
"raw": string(body),
},
}).Result()
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
Worker 层(Python 消费 Redis Stream 并写 Kafka)
from kafka import KafkaProducer
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
producer = KafkaProducer(bootstrap_servers='kafka-host:9092')
while True:
stream_data = r.xread({'event_stream': '0-0'}, count=1000, block=1000)
for stream in stream_data:
for msg_id, data in stream[1]:
producer.send('tracking_topic', value=data['raw'])
r.xdel('event_stream', msg_id) # 删除已发送的消息
当然在实际工程中,我们会增加更多细节处理:比如失败重试、幂等校验、监控埋点等。上面只是一个简化的示意版本。
踩坑经验:那些我们以为不会出事的地方都出了事
1. Redis Stream 自增ID导致的重复消费
一开始我们没有设置 XADD 的 MAXLEN,结果在高并发下 Redis 内存暴涨,差点炸掉。后来设置了一个上限值,并采用懒删除的方式清理旧数据,缓解了这一问题。
另一个问题是重复消费问题。原本我们想依赖 Redis Stream 的消费者组机制来实现“仅消费一次”,但由于程序退出未确认消费进度,导致同一批数据多次投递。
解决方案是在消费完数据后显式调用 XACK,并在本地维护偏移量索引,同时结合数据库记录已消费的 message ID,防止重复处理。
2. 批量写 Kafka 效率低下的问题
Worker 使用 Python 的 KafkaProducer 默认配置进行写入,但我们发现吞吐量始终上不去。排查后发现是默认配置中的 linger.ms=0 导致每条消息立即发送,无法合并成 batch。我们将该参数设置为 20ms,并适当调整 batch.size 后,吞吐量提升了近三倍。
效果总结:性能、稳定性和可观测性的全面提升
经过两个月的打磨上线,新系统带来了显著变化:
- QPS 提升超过3倍:从原来平均 5k QPS 上升到 15k+,P99 延迟下降了 40%;
- 数据完整性提升:配合重试机制和补偿流程,数据丢失率接近为零;
- Kafka 分区负载更均衡:通过重新设计 Key 分配规则(使用 event_type + hash(user_id) 两级划分),热点问题得到缓解;
- 运维成本大幅下降:系统具备更强的自愈能力,告警频率减少 80%,日常值班压力减轻了不少。
更重要的是,我们构建了一套可插拔的埋点处理框架,未来无论是对接 Flink 进行实时分析,还是做 AI 预测模型,都可以在当前基础上快速搭建。
经验分享:给同行朋友的几点建议
1. 技术选型要“因地制宜”,而不是“跟风换库”
在做架构决策时,不要盲目追求新技术,而应该回归本质问题。举个例子:如果我们坚持保留 Java 栈,也可以通过 Netty 替代 Spring Boot 来实现高性能 HTTP 接入层,或者采用 Vert.x 等响应式框架。关键是理解每个技术栈的优势和适用场景。
2. 异步不是万能药,需要配套的容错机制
很多人一提到性能问题就想到“异步化”,但真正的挑战在于如何设计好完整的“回退”路径。例如,当 Redis 不可用时,是否要考虑本地磁盘 buffer?当 Kafka 写入失败时,是否有重试机制?有没有兜底的人工补偿流程?
3. 系统边界要清晰,模块之间职责分明
在我们的案例中,我们将功能划分为四层:接入、缓存、处理、输出。每层之间的接口明确,便于扩展和替换。这种设计让我们在后续引入 Flink 实时聚合引擎时,几乎不影响其他层级。
4. 多维度监控比单一指标更有价值
在改造过程中,我们不仅监控了请求成功率、延迟、写 Kafka 耗时等基础指标,还针对 Redis Stream 的堆积情况、消费偏移的变化趋势做了详细大盘展示。正是这些细粒度的观测,帮我们更快定位了问题根源。
结语:技术探索的本质,是一种不断迭代的成长
写到这里,我突然想起了那个深夜加班改 Redis 配置的夜晚。当时我们几个人围着电脑屏幕看着监控图表一点点上升的吞吐量,那种成就感至今难忘。
技术探索从来都不是为了炫技,也不是为了追风口,而是在面对真实业务挑战时的一种务实回应。你可能永远不知道哪天会出现下一个“小改进带来大收益”的转折点,但只要你保持对问题的敏感和技术的好奇心,总能在一次次实践中找到最优解。
我也常常在想,我们所积累的经验,其实就像是一个个“坑”的集合。但正是这些坑,教会了我们什么才是可行的,什么是值得坚守的。
希望这篇分享能给正在技术路上摸爬滚打的朋友们一些启发。愿我们在不确定的时代,依旧保持技术信仰,持续探索前行。

评论 0