从v0到上线:一个双非学生的云原生爬虫优化实战
说实话,写这篇文章的时候我还有点心虚。毕竟我才大二,靠着自学混进了这家创业公司做后端开发——没错,就是那种简历上写着“熟悉K8s”、实际连etcd都没碰过几次的野生选手。两个月前入职时,mentor看我用Vim写代码还调侃说:“现在年轻人还玩复古?”但谁让我在学校里穷得装不起JetBrains全家桶呢?不过现在回头看,Vim真香,特别是当你要在容器里临时改配置的时候。
事情的起因是上周产品经理甩过来的需求:“我们要做一个竞品价格监控系统,每天抓取几万个商品页面,数据要准、延迟要低、成本还得控制住。”我一听就头大——这不就是个爬虫嘛,但加了“云原生”和“高可用”两个buff之后,难度直接拉满。更绝的是,deadline定在下周三,理由是“老板要在投资人会议上展示”。
初版方案(v0):天真得可爱
第一天晚上,我吭哧吭哧撸了个Python脚本,用requests+BeautifulSoup,本地跑起来挺顺。我还得意地跟同事说:“这不就完了?”结果第二天就被运维小哥按在地上摩擦——“你这玩意儿怎么部署?单机跑?崩了咋办?日志在哪?”
于是v0诞生了:一个简单的Flask应用,挂了个Celery队列,用Redis做任务分发。代码结构大概是这样:
# v0_crawler.py
import requests
from bs4 import BeautifulSoup
from celery import Celery
app = Celery('crawler', broker='redis://redis:6379/0')
@app.task
def fetch_product(url):
resp = requests.get(url, timeout=10)
soup = BeautifulSoup(resp.text, 'html.parser')
price = soup.select_one('.price').text
return {'url': url, 'price': price}
本地测试没问题,一上K8s就翻车。Pod频繁OOMKilled,日志里全是ConnectionResetError,而且因为没做去重,同一个URL被反复抓取。最惨的是,某天半夜,目标网站突然加了Cloudflare,我的爬虫直接被ban,整个集群CPU飙到90%——就因为我没处理异常,任务无限重试。
当时真的想砸电脑。但转念一想,这不就是成长的机会吗?
ChatGPT不是万能药,但能救命
崩溃之际,我打开了ChatGPT(别笑,我们这种小公司可没预算买Copilot)。我问它:“K8s里运行高并发爬虫的最佳实践是什么?”它给了一堆建议:用Headless Chrome?不行,太重;用Scrapy?学习成本高;最后它提到一个关键点:分布式爬虫的核心不是抓得多快,而是如何优雅降级和自愈。
这句话点醒了我。我开始重新设计架构:
- 任务调度层:用Kafka替代Redis,天然支持分区和回溯
- 执行层:每个爬虫Pod只负责单一站点,避免互相影响
- 代理池:集成免费代理IP轮换,绕过反爬
- 指标监控:暴露Prometheus metrics,比如成功率、响应时间
最关键的是,我学会了不要追求一次完美。先让系统跑起来,再迭代优化。这大概就是所谓“工程思维”吧——学校里没人教这个,全靠踩坑悟出来。
架构升级:拥抱云原生
第二周,我推翻了v0,搞了个叫crawler-v1的新版本。核心思想是:把爬虫当成微服务来治理。
1. 任务分片与状态管理
我用Kafka的topic分区来实现任务分片。比如jd-products topic有8个分区,每个分区对应一个商品类目。这样即使某个类目被封,也不影响其他类目。
# crawler-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: crawler-jd
spec:
replicas: 4
template:
spec:
containers:
- name: crawler
image: my-registry/crawler:v1
env:
- name: KAFKA_TOPIC
value: "jd-products"
- name: SITE_NAME
value: "jd"
resources:
limits:
memory: "512Mi"
cpu: "500m"
2. 动态代理与User-Agent轮换
反爬是最大痛点。我写了个简单的代理中间件:
# proxy_manager.py
import random
PROXIES = [
"http://1.2.3.4:8080",
"http://5.6.7.8:3128",
# ... 从免费代理网站爬来的IP
]
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
]
def get_random_proxy():
return random.choice(PROXIES)
def get_random_ua():
return random.choice(USER_AGENTS)
虽然简陋,但配合随机延时(time.sleep(random.uniform(1, 3))),成功率从40%提升到了75%。
3. 指标监控不能少
我在代码里加了Prometheus client:
from prometheus_client import Counter, Histogram
REQUEST_COUNT = Counter('crawler_requests_total', 'Total requests', ['site', 'status'])
REQUEST_DURATION = Histogram('crawler_request_duration_seconds', 'Request duration', ['site'])
def fetch_with_metrics(url, site):
start = time.time()
try:
# ... 发起请求
REQUEST_COUNT.labels(site=site, status='success').inc()
except Exception as e:
REQUEST_COUNT.labels(site=site, status='error').inc()
finally:
REQUEST_DURATION.labels(site=site).observe(time.time() - start)
然后在Grafana上建了个面板,实时看各站点的成功率。运维大哥看到后居然夸我:“小伙子,有点东西!”
性能对比:数字不会骗人
折腾一周后,我做了个简单对比:
| 指标 | v0 (单机) | v1 (K8s集群) |
|---|---|---|
| 日抓取量 | ~5k | ~50k |
| 平均成功率 | 42% | 78% |
| 内存占用/Pod | N/A | 380Mi |
| 故障恢复时间 | 手动重启 | < 30秒(K8s自愈) |
| 运维复杂度 | 高(需盯日志) | 低(看Grafana就行) |
最爽的是,现在加新站点只需要:
- 创建新的Kafka topic
- 部署一个新Deployment(改个env变量)
- 在Grafana加个filter
完全不用动核心代码。这就是云原生的魅力啊!
开发心得:别怕从v0开始
回顾这段经历,我最大的感悟是:所有伟大的系统,都是从一个烂得不能再烂的v0开始的。
在学校时,我总想写出“完美代码”,结果经常卡在设计阶段。工作后才发现,业务不等人,先跑起来再说。K8s给了我们快速试错的能力——Pod挂了?自动重建。配置错了?滚动更新回滚。这种安全感,是本地开发无法比拟的。
另外,别迷信工具。ChatGPT帮我打开了思路,但具体实现还是得自己动手。比如它建议用Headless Browser,但我评估后发现内存开销太大,果断放弃。技术选型没有银弹,只有权衡。
最后,感谢我的mentor没在我v0翻车时把我开了。也感谢那个“不讲武德”的产品经理,逼我两周内从爬虫小白进化成(伪)云原生工程师。
后记:昨天老板在会上演示了我们的监控系统,投资人问:“这数据准吗?”老板拍胸脯:“绝对准,我们有个大二实习生写的!” 全场哄笑,但我知道,这笑声里有认可。
(完)
注:本文所有代码均为简化示例,实际项目中还需处理Cookie、验证码、动态渲染等更复杂场景。另外,爬虫请遵守robots.txt,别干坏事!

评论 0