灰度发布可以在服务正式上线前,提前用小流量对新功能进行验证,提前发现问题,避免故障影响所有用户,对业务稳定性非常有价值。
得物社区后端技术栈以 golang 为主,本文记录了社区后端在灰度环境建设过程中遇到的挑战,以及对应的探索和实践。
2.1 小得物 V1
要实现小得物环境只部署部分应用,正确路由流量而不报错,需要网关层、RPC 等调用层感知集群内后端服务有没有部署。
Ingress 这层,其实相当于接了 k8s 的注册中心,它是可以感知到集群是否有可用 upstream。但开源配置无法支持这个需求,二开比较复杂,这个也不在社区的控制范围内。
这个时期社区应用正在进行容器新老集群迁移,在容器 Ingress 之前加了一层 DLB(可以简单理解为 Nginx),通过 location 来区分应用是否部署新集群,以及新老集群流量灰度。
年中时社区第一批约 15 个 C 端应用上线小得物,同时对小得物环境的监控告警等基础设施进行了完善。
2.2 小得物 V2
V1 的核心问题在于引流机制是 DNS。DNS 的优势在于它是在客户端生效,是去中心化的。但也有很多缺点,比如控制维度单一,只有客户端 IP、地域。只依靠这个,灰度流量大小难以精准控制,想要的基于 UID、header 的灰度规则也没法实现。
想要做 UID 灰度引流,一般都是在入口网关上做。灰度配置可能经常需要开关、调整流量大小,如果配置错误或出现 bug,则影响所有流量。
因此想到一个折中的方案,从生产 DLB 根据 UID 引入 5% 灰度流量至小得物 DLB,小得物上再通过二次灰度规则控制流量大小在 0-5%。最大流量限定为 5%,生产只配置一次,后续开关、规则调整均在小得物 DLB 上进行。虽然多用了一个 DLB,但减少生产 DLB 配置变更频率,缩小了爆炸半径。
之前做新老集群迁移的生产 DLB,本来准备下掉,现在正好可以利用起来。对 DLB 进行了版本升级,配置好灰度规则后,就有了现在 V2 的架构。
架构升级后:
# uid 路由规则
uidRoute:
start: 2000
end: 2500
# header 头路由规则
headerRoute:
xdw :
3.1 依赖队列自动生成
同时与发布平台打通,发布时触发静态分析,自动生成发布依赖状态图。以前都是版本 owner 手动画这个图,在办公沟通群众同步。通过自动化手段,大幅提高了效率和用户体验。
在前文提到的小得物 V2 架构中,灰度流量在社区小得物 DLB 中控制。因此在小得物发布过程中,可以直接通过 openAPI 将小得物流量摘除。没有了流量,就可以无视应用间依赖,直接批量将所有应用并发部署,大幅提高小得物环境部署效率。
同时摘流后,再通过 API 将流量梯度拉升,从 0% 缓慢提升至 5%,每次引流都会触发稳定生产 SOS 事件中心的自动巡检,根据配置的巡检规则,计算出得分,展示与七天平均值偏差较大的异常点,帮助版本 owner 提前发现灰度问题。
效果图:
RPC 路由这个功能,大多数据 RPC 调用系统都有。社区目前的 RPC 是基于 grpc-go 扩展实现的,很多人都说 grpc 没有服务治理功能,但实际上 grpc 有着良好的扩展性和丰富的生态。得物 go 框架基于 grpc-go 只用了千余行代码即可实现拥有服务发现、多注册中心、多服务名、地址路由、自定义 interceptor 等完备功能的 RPC 调用系统。
env == "xdw"
,优先路由至小得物节点,在下游服务未部署小得物时兜底至生产节点,保证可用性。metadata:
env: xdw
registries:
# grpc 协议
nacos-grpc:
type: nacos-grpc
url: http://xdw.xxx.com:80
priority: 11
# http 协议
nacos-http:
type: nacos-http
url: http://xdw.xxx.com:80
priority: 11
# 备用注册中心
nacos-bak:
type: nacos-grpc
url: http://bak.xxx.com:80
priority: 11
# java 服务
java-nacos:
type: java-dubbo
url: http://java.xxx.com:80
priority: 11
multi-nacos:
type: ref
refs: # 多注册中心引用
- nacos-grpc
- nacos-bak
direct:
type: direct
priority: 15
client:
requestTimeout: 700
pickers:
- target: "*"
desc: "优先使用小得物地址"
match:
tag: env == "xdw"
- target: "*"
desc: "兜底,无小得物地址时使用所有地址"
match:
tag: "*"
targetMap:
sns-aaa:
services: # 多服务名
- registryName: muilti-nacos
serviceName: sns-aaa
- registryName: nacos-http
serviceName: sns-aaa-http
sns-bbb:
services:
- registryName: muilti-nacos
serviceName: sns-bbb
# java dubbo 服务
java-ccc:
services:
- registryName: java-nacos
serviceName: "com.xxx.DubboTestGrpcServiceGrpc$ITestGrpcService:1.0:"
# 直连地址
direct-ddd:
services:
- registryName: direct
serviceName: ddd.xxx.com:8080
drpc:
remoteConfig:
type: ark
url: https://ark.xxx.com?ns=XDW&cf=drpc.yaml
client:
targetMap:
sns-aaa:
# 超时配置
methodTimeout:
AaaService/FooMethod: 100
sns-bbb:
methodTimeout:
BbbService/BarMethod: 50
社区小得物与生产环境公用一套 DB、 MQ 中间件。应用代码中 MQ producer、comsuer,HTTP、GRPC API 是在一个进程中。如果消息没有隔离逻辑,小得物打开消费,则会与生产节点成为同级消费者,消费生产消息。而小得物环境机器配置较低,消费速度慢会影响业务。
在没有 MQ 消息隔离前,采取一个笨办法,直接关闭小得物 MQ 消费。但这样小得物的消息是靠生产处理,在小得物有 MQ 相关新版本变更时,需要考虑新老兼容的问题。
随着社区阿里云 MQ 迁移 DMQ 进入收尾阶段,DMQ Go SDK 也趋于稳定,开始尝试使用程序化方案解决 MQ 灰度消费的问题。
最开始跟小得物团队了解了一下最初的方案,小得物和生产使用不同的 MQ 实例,这样就要求 producer、consumer 在小得物全量部署。对于跨业务域的 topic 需要消息同步机制。感觉复杂度过高,资源成本和维护成本都很高。
后面看到一篇 阿里云分享的 RocketMQ 灰度方案,其采用消息打标、group 隔离、SQL 属性过滤实现消息灰度,感觉这才是理想的方案。
这里说一下 tag 过滤和 SQL 过滤,tag 过滤大家比较常用,但一条消息只能有一个 tag,常被业务占用,且不能支持 != 这样的条件。而 SQL 过滤就灵活得多,可以使用消息 properties 自定义 kv 键值对,SQL 的 NOT、BETWEEN、IN 等关键词都可以使用。
找中间件团队沟通,他们表示 SQL 过滤性能较差,暂不支持。建议使用 Java 染色环境类似的方案,在客户端过滤。虽然客户端过滤,有很多无效的网络传输,但成本较低,只需要改造一下业务框架中 MQ SDK 即可,也能解决 MQ 灰度的问题。经过压测,小得物环境过滤生产环境高 QPS 生产的消息或是 group 积压的大量消息, 对应用不会造成较大的性能影响,于是采用了此方案。
consumer 消费的隔离比较简单,MQ 的机制是不同 group 消息消费都是独立的,每个 group 都能收到topic 全量消息。
在业务框架中根据染色环境配置,增加不同的处理逻辑。
如果是染色环境(小得物):
producer 发送消息时,在消息 properties 中添加流量标 X-Flow-Flag=[prefix]。
consumer 启动时自动给配置的 group 添加 [prefix]。消费时过滤掉 properties 不包含流量标 X-Flow-Flag=[prefix] 的消息,直接 ack。
trafficRoute:
colorEnv: xdw
trafficRoute:
excludeEnvList: [xdw]
查看了一下 DMQ 的 Java 源码,发现 Boroker 回查时是通过消息 properties 中的 group 来查找在线 producer。那么跟 consumer 类似,给 trans producer 配上 group ,给小得物 group 加上环境前缀即可实现事务回查隔离。用于 trans producer 的 group 只是一个标识,甚至不需要在 DMQ 后台申请。
*文/ 无风
关注得物技术,每周一三五晚18:30更新技术干货