一、为什么“能跑起来”不等于“生产就绪”?——我们被凌晨三点的告警教做人
还记得上线首周那个周三凌晨2:47吗?我正裹着毯子刷手机,钉钉弹出第7条红色告警:“ad-bidding-service-03 连接池满,Connection refused”。三分钟后,运维老张发来一张图:黑底白字的终端日志叠着半干的咖啡渍,工牌一角被浸得发软——那张图后来成了我们团队的“耻辱墙”头图。
故障链很短,但后果极重:单点MySQL连接池(HikariCP max=20)被广告计划轮询任务打穿 → 连接堆积阻塞线程池 → Spring Boot Actuator健康检查失败 → K8s liveness probe连续失败 → Pod被批量驱逐 → 新Pod启动又抢连接 → 雪崩。37%的广告计划在11分钟内停投,客户侧CTR数据断崖式下跌。
我们当时最大的错觉,是把“Postman能调通”当成了交付终点。直到被现实按在地上摩擦才明白:“能跑起来”只证明代码没语法错误;“生产就绪”是系统在CPU飙到95%、磁盘IO堵死、网络延迟跳变200ms时,依然能呼吸、能喊疼、能自己止血。
这不是上线后补监控、加熔断的“优化项”,而是架构设计第一天就必须画进图里的生存底线。我们后来用故障时间线倒逼出三大支柱的落地节奏:
- T+0部署:服务启动成功(
/actuator/health返回UP) - T+12h首次OOM:JVM堆外内存泄漏,Prometheus发现
process_resident_memory_bytes突增 → 引入-XX:NativeMemoryTracking=detail+jcmd <pid> VM.native_memory summary定期巡检 - T+48h灰度中断:因未配置
@SentinelResource(fallback = "defaultBid"),下游画像服务超时直接抛异常 → 全链路强制 fallback 合规检查纳入CI流水线
💡 行动建议:下次写完第一个接口,别急着提测。打开终端,执行这三行:
# 模拟连接池耗尽(HikariCP) curl -X POST http://localhost:8080/actuator/health # 查看当前活跃连接数 echo "show status like 'Threads_connected';" | mysql -u root -p -h 127.0.0.1 | grep Threads_connected # 触发一次OOM(仅测试环境!) curl -X POST http://localhost:8080/debug/oom-trigger如果这三步里有任何一步让你手心冒汗——恭喜,你刚摸到了生产就绪的门槛。

二、高可用不是堆机器,是“主动防崩”——我们的多活+降级实战手记
曾天真地以为:K8s集群跨3个AZ部署 + etcd集群3节点 + Redis哨兵模式 = 高可用。结果云厂商华南区网络抖动持续47秒,etcd leader频繁切换,/healthz 探针间歇性失败 → K8s调度器误判节点失联 → 所有广告策略服务Pod被驱逐重启 → 本地缓存清空 → 流量直击MySQL → 慢查询堆积 → 连锁崩溃。
最讽刺的是,我们花了2周配好“多活”,却没给它装“刹车片”。
真正的高可用,是提前想好:当某一层塌了,系统如何用更廉价的代价,维持核心功能不瘫痪? 我们用三个月打磨出三道防线:
数据层兜底:Caffeine本地缓存 + 策略降级
// 出价策略主逻辑
public BidResult calculateBid(AdRequest request) {
try {
return redisCache.get(request.getAdId(), () ->
dbQuery.fetchStrategy(request.getAdId()));
} catch (Exception e) {
// 降级:返回预设默认策略(非0值!)
log.warn("Cache/DB fail, fallback to default strategy", e);
return DefaultBidStrategy.INSTANCE.apply(request); // 例如:base_bid * 1.15
}
}
服务层熔断:Resilience4j 配置压到毫秒级
# application.yml
resilience4j.circuitbreaker:
instances:
userProfileApi:
failure-rate-threshold: 50
wait-duration-in-open-state: 60s
permitted-number-of-calls-in-half-open-state: 10
# 关键!超时从5s砍到800ms
resilience4j.timelimiter:
instances:
userProfileApi:
timeout-duration: 800ms
流量层智能调度:Nginx+Lua 实现地域级切流
# nginx.conf
lua_shared_dict region_health 10m;
server {
location /bid {
access_by_lua_block {
local health = ngx.shared.region_health:get("south_china")
if health and tonumber(health) > 200 then -- 延迟>200ms
ngx.exec("@east_china") -- 转发至华东节点
end
}
}
}
✅ 上线前必查清单(贴在团队Wiki首页):
- 5个降级开关是否全部可手动触发?(
/api/v1/feature-toggle/bid-fallback)- etcd健康检查是否包含
curl -L http://etcd:2379/health+etcdctl endpoint status双校验?- 所有熔断器是否配置
onFailure回调记录trace_id?- 本地缓存是否设置
maximumSize(10000)+expireAfterWrite(10m)?- 流量调度规则是否通过
ab -n 1000 -c 100 http://test-region-switch验证过?
三、监控不是看Grafana大屏,是让系统自己会“喊疼”
初期我们建了200+告警规则,其中186条是“CPU>80%”“内存>90%”。结果某次Redis集群因持久化阻塞,redis_latency_ms{job="redis"} > 500 持续12分钟——而真正致命的指标 ad_recall_rate{region="shanghai"} < 0.85 却安静如鸡。等销售总监电话打进来:“用户搜不到我们广告了”,才惊觉已损失2小时黄金流量。
监控的本质,是把业务语言翻译成机器能听懂的求救信号。 我们重构为三层指标体系:
| 层级 | 指标示例 | 告警动作 |
|---|---|---|
| 基础设施 | node_cpu_seconds_total{mode="idle"} | P2,钉钉群通知 |
| 中间件 | redis_commands_total{cmd="get",status="error"} | P1,企微@oncall |
| 业务黄金 | ad_serving_success_rate{advertiser="abc"} < 0.99 | P0,电话+短信双触达 |
最关键的突破,是定义了三个“必埋业务黄金指标”:
ad_serving_success_rate(广告成功曝光率)ctr_prediction_error_rate(CTR预测偏差率:|pred_ctr - actual_ctr| / actual_ctr)bid_strategy_version_mismatch(新旧策略版本不一致次数)
配合结构化日志快速下钻:
{
"level": "INFO",
"trace_id": "a1b2c3d4e5f6",
"ad_id": "AD-7890",
"strategy_version": "v2.3.1",
"bid_amount": 1.25,
"recall_source": "realtime_feature_v3",
"event": "bid_decision_complete"
}
当 ad_serving_success_rate 报警时,ELK中输入:
trace_id: "a1b2c3d4e5f6" AND event: "bid_decision_complete" | sort @timestamp
30秒定位到是特征服务返回了空人群包——而非猜测“是不是Redis挂了”。

四、灰度发布不是“切10%流量”,是“让业务敢改、敢试、敢回滚”
第一次灰度,我们用Nginx按IP哈希分流。结果某电商客户所有员工IP段(202.96.0.0/16)全进了灰度池。新算法对长尾商品过度保守,导致其ROI暴跌40%。销售团队凌晨三点订机票飞往客户现场,用Excel手工调价补救……
灰度的核心矛盾从来不是技术,而是信任。 业务方怕的不是技术故障,是“我的客户被当成小白鼠”。
我们迭代出三板斧:
- 流量标识:放弃IP/UA,改用
advertiser_id % 100哈希,确保同一客户100%走同一条路径; - 双写验证:灰度服务同时调用
old-strategy.jar和new-strategy.jar,对比结果:if (Math.abs(oldBid - newBid) / oldBid > 0.05) { // 偏差>5% alert("BID_DEVIATION_HIGH", traceId, oldBid, newBid); disableNewStrategy(); // 自动熔断 } - 一键回滚:K8s ConfigMap热更新 + 预置旧版Deployment YAML,脚本化:
# rollback.sh kubectl apply -f deploy-v2.2.0.yaml && \ kubectl rollout restart deployment/ad-bidding-service # 实测耗时:23秒
📋 灰度Checklist(每次上线前全员签字):
- 验证3类异常case:① 广告主ID为NULL ② 特征服务超时 ③ 用户画像返回空JSON
- 回滚失败兜底方案:手动执行
kubectl set image deployment/ad-bidding-service *:v2.2.0- 销售侧已同步灰度范围及预期影响(附客户名单Excel)
五、那些没人告诉你的“隐形地雷”——配置中心、数据一致性、权限治理
最痛的不是宕机,是“静默失效”:
- Apollo里误删
bid_floor字段,全量出价归零11分钟——因为监控只看/health,不校验/config/validity; - 离线特征任务因YARN资源争抢延迟22分钟,实时服务取到过期人群包,凌晨批量封禁优质客户;
- 市场部实习生用同事账号登录策略平台,把
female_premium_ratio从1.2改成12,引发27起投诉。
这些地雷不炸则已,一炸就是P0。 解法必须务实:
- 配置中心:所有关键配置强制双人复核 + 沙箱环境回放(用昨日10%真实流量请求验证);
- 数据新鲜度:特征服务增加探针,每5分钟检查HDFS文件:
hdfs dfs -ls /data/features/user_profile/ | tail -1 | awk '{print $6}' | \ xargs -I {} date -d "{}" +%s | xargs -I {} expr $(date +%s) - {} # > 900秒(15分钟)则告警 - 权限治理:RBAC绑定AD组 + 二次确认:
@PreAuthorize("hasRole('STRATEGY_EDITOR')") public void updateBidStrategy(@RequestBody StrategyDto dto) { if (requiresSmsConfirm(dto)) { sendSmsCode(dto.getUserId()); // 发送短信验证码 } }
🔍 上线前夜5件事自查表:
- 所有配置项是否设置
defaultValue(避免null导致空指针)?- 最近3次特征任务延迟水位是否<5min?(查Airflow DAG历史)
- 策略编辑权限是否已从“所有人”收回至
ad-strategy-editorsAD组?- Apollo配置变更审计日志是否开启?
bid_floor等兜底值是否在数据库和配置中心双重校验?
六、给后来者的真心话:别迷信架构图,先搞定这三件事
最后说点掏心窝的话。我们花半年画了17版微服务架构图,但真正救了命的,是这三件小事:
第一件事:把故障复盘会变成每周雷打不动的仪式。
哪怕当周零故障,也模拟一次DB宕机:
- 主角:DBA(扮演MySQL)
- 规则:不准说话,只能用
SELECT 1响应 - 目标:SRE在5分钟内完成读库切换+缓存预热
第二件事:给每个核心接口写“死亡测试”脚本。
# death_test_bid_api.py
def test_network_partition():
# 拦截所有出站请求
with patch('requests.post') as mock_post:
mock_post.side_effect = requests.exceptions.ConnectionError
assert bid_api.calculate() == DEFAULT_BID # 必须降级!
def test_disk_full():
# 注入磁盘满错误
with patch('os.statvfs') as mock_stat:
mock_stat.return_value = Mock(f_bavail=0)
assert bid_api.cache_warmup() == "SKIPPED" # 不该崩溃
第三件事:让业务方定义“可用性”。
我问业务总监:“如果今天灰度出问题,你最怕什么?”
他脱口而出:“怕用户搜不到我们的产品。”
——于是我们把search_page_ad_exposure_success_rate设为P0指标,阈值99.99%,比SLA还严苛。
💬 最后一句真心话:
架构图是静态的,故障是动态的;文档是理想的,线上是混沌的。
别等告警响了才想起加监控,别等客户投诉才想起埋日志。
生产就绪的起点,永远是你第一次认真读完那行报错日志的凌晨。