istio作为服务网格的代表作,为微服务架构提供了服务发现、流量控制、可观测性等服务治理的能力,让微服务本身能够聚焦在业务上。我们团队从21年业务整体上istio后,已经经过了一年多的实践,形成了一套基于istio的服务devops体系。本文对我们的实践方案进行介绍,希望能吸取经验教训,为后续的探索和实践提供一个案例。
一、背景
框架解耦的流量管理能力作为ServiceMesh的看家本事,一直是大多数团队选择ServiceMesh的主要原因。传统的流量管理大多需要在框架层面集成一种中心化的服务发现中心,通过服务发现中心去做流量控制分发。这对服务发现中心的的稳定性要求很高,同时限制了框架选择和扩展性。而ServiceMesh基于 sidecar 形式通信代理,将服务和流量控制解耦,服务不关心流量从哪里来,去往哪个具体ip,sidecar通过流量劫持的形式,对进出服务的流量进行修改,达到控制流量的目的,类似中间人攻击。
这种框架解耦的流量控制特性也是我们当初决定抛弃传统的服务发现和流量管理方式,毅然采用ServiceMesh的原因。
历史方案
在使用ServiceMesh之前,流量管理一直是困扰我们的一大难题,其中最突出的问题是服务发现和灰度发布。
过去我们采用的是完全基于k8s原生态的一套技术栈:
技术栈
- go kubernetes
- grpc框架。我们在grpc上封装了一些通用能力如鉴权、链路、日志等能力,以及http转grpc的能力,方便业务集成开发。
- prometheus、filebeat logstash kafka elastic、jaeger。前期都是我们自己在k8s部署的,后面都迁移到了云上的云监控、cls、apm
- kong网关
- coding的ci/cd
- helm rancher的应用部署管理
弊端
我们在上述技术栈的基础上经过了两年多的发展,虽然能满足大部分基础需求。但是面对日益新增的技术需求仍然显得捉襟见肘。最主要的的弊端集中在两个方面,归根到底都是流量控制的问题。
- 服务发现
我们采用的是grpc框架,并且在此基础上进行了简单的封装。
由于k8s service对grpc长链接负载均衡的局限性:grpc如果和service ip建立长链接,实际只会和一个pod ip建立链接,之后的请求也只会请求到同一个pod(详细原理见:https://kubernetes.io/blog/2018/11/07/grpc-load-balancing-on-kubernetes-without-tears/) 。为了解决这个问题,我们最早的时候使用过自建的etcd注册中心。后来转而基于k8s api server的服务发现中心,即客户端从api server获取每个pod端建立连接(grpc kuberesolver :https://github.com/sercand/kuberesolver) 。
无论那种方法,对注册中心的要求都很高。使用k8s api server方式,在grpc服务过多时对api server压力过大,甚至可能影响k8s本身稳定性。
- 灰度发布
kubernetes原生的灰度发布都是通过service来实现的:一个service同时绑定正式版和灰度版本的deployment,通过调节两种版本pod的数量比例来决定流量分发的比例。
如图,服务B拥有3个pod,当需要服务B就行灰度发布时,创建一个灰度版本的deployment,保持和服务B一样的service selector,就能通过service将25%的流量灰度过去。
pod数量比例=灰度流量比例
这种方式是最常用的k8s原生蓝绿发布方案,对于较小体量的微服务来说差不多够用了。但是其缺点也显而易见:
- 不能精确控制灰度流量比例
- 无法做到精细的流量控制,如通过用户、路由等信息
- 无法级联灰度:如果服务A和服务B需要同时发布,发布时分别进行灰度时,新老版本的流量会交替访问。万一某些接口不兼容,可能造成灾难性的后果。
随着我们业务的发展,上述问题成为困扰我们研发运维的一个巨大难题。一是开发人员经常修改开发环境,开发环境经常处于不可用状态,影响其他人员开发进度;二是几次发布没能做好服务级联服务两两兼容,造成发布事故;三是无法做到灰度流量控制,即使灰度发布也经常造成大量用户不可用。
二、ServiceMesh
为了解决上述难题,流量管理提上了日程。一般来说,流量管理有两种方式:
一是在微服务框架集成的流量控制组件,通常包括了中心化的注册中心和控制平面。如公司了使用范围较广的北极星,或外界的dubbo、consul、etcd等,配合流量控制的hystrix、sentinel等,达到服务注册发现 流量控制的能力。
二就是今天的主角,serviceMesh了。serviceMesh采用基于 sidecar 通信代理,以一种类似中间人攻击的形式,将基础的治理能力从服务框架中完全解耦出去。服务对流量控制的过程完全是透明的,出入流量在经过服务前后,通过sidecar代理对流量“篡改”,达到改变流量规则的目的。
我们最开始采用的也是etcd hystrix的方案,逐渐过渡到k8s endpoint hystrix再到最后的serviceMesh。可以看出我们一步步将服务治理的能力从框架解耦,从需要自我部署维护的组件到云原生化,尽可能的减少开发运维成本。
功能
服务治理与服务业务解耦是ServiceMesh出现的理念,也是相比其他方式最大的优势。
ServiceMesh 作为微服务架构中负责网络通信的基础设施层,按理说能对流量进行任意处理。下面列举了一些主要的功能:
- 动态路由。 可通过路由规则来动态路由到所请求的服务,便于不同环境、不同版本等的动态路由调整。
- 故障注入。 通过引入故障来模拟网络传输中的问题(如延迟)来验证系统的健壮性,方便完成系统的各类故障测试。
- 熔断。 通过服务降级来终止潜在的关联性错误。
- 安全。 在服务网格上实现安全机制(如 TLS),并且很容易在基础设施层完成安全机制更新。
- 多语言支持。 作为独立运行且对业务透明的 Sideca 代理,很轻松地支持多语言的异构系统。 多协议支持。 同多语言一样,也支持多协议。
- 指标和分布式链路追踪。
概括起来,服务网格的能力主要体现在3个方面:
- 流量控制
- 可观测性
- 安全性
istio简介
Istio是由Google、IBM和Lyft发起的开源的Service Mesh框架。今年9月,istio加入CNCF大家族。
Istio由数据平面和控制平面组成。
数据平面:数据平面就是使用envoy作为sidecar的数据代理服务,数据平面也称为转发平面。
控制平面:控制平面用以配置、管理、下发数据平面的转发规则。
控制平面是在城市道路交叉口工作的交通信号灯。数据平面则更像是在道路上行驶、在路口停下并遵守交通信号灯的汽车。
三、落地方案
决定了istio的升级方案后,接下来就是实际落地。腾讯云的服务网格(Tencent Cloud Mesh, TCM)100%兼容支持 Istio API,当然是上istio的不二之选。
当然,即使使用tcm的控制台,对开发同学来说,服务注册、修改路由规则配置等一些场景还是对istio的功能和配置规范有一定的要求,现网发布时一不留神配错了就可能导致系统异常。为了尽量减少开发人员学习成本,并且将istio的特性开发、特性发布充分利用起来,我们对整体devops进行了针对istio的落地改造。
git工作流
首先是git工作流升级
我们之前采用的是一套类似于git flow的工作流:
因为我们总共搭建了开发、测试、预发、正式四个环境,本地feature开发自测,合并到develop联调,test环境提测,master预发布,并最终打tag后发布到正式环境。
这种方式也是我们在缺乏特性开发环境下不得以采用的模式。由于频繁的dev环境提交,dev环境经常不稳定。多个分支代码很容易不一致,管理复杂,增加CR/MR成本。
使用istio后,我们采用了类似github flow的工作流:
istio的流量控制特性可以让我们方便得为每一个服务版本创建一个独立的调试、提测环境。例如开发人员在feature/sth
分支提交后,通过ci流程我们会在istio创建一个路由规则为http header = feature: v2
的匹配规则,开发人员只需要在http 请求加上对应header即可。即使多个服务也只需要沟通商量好使用同一个feature分支提交,就可以调试同一套环境。
因此在这个基础上,我们将git工作流简化,feature分支用以开发、调试,master提测,tag发布。简化了CR/MR流程,让开发更加规范化。
github flow对于敏捷开发确实有着巨大优势,简洁、清晰,有的文章直接将它吹爆。https://insights.thoughtworks.cn/real-agile-workflow-github-flow/
CI/CD
CI/CD的改造最主要是体现在对feature分支的发布和灰度发布的自动化改造。
istio通过流量控制,给了我们提供了独立开发环境的可能。基本思路就是不同的开发分支发布到开发环境后,生成一个新的deployment版本,并且根据规则生成不同的路由规则。不同的开发版本根据不同的路由规则进行隔离,不影响其他环境。
istio本身支持各种各样的路由规则,如:
为了统一,我们决定默认采用根据header路由的形式。新版本服务发布后,在istio中生成对应的header路由规则并匹配到版本上。要做到这些,我们需要对框架、CI流水线、helm发布包都做相对的改造。
客户端改造
不是说istio是对服务和治理完全解耦的吗,为什么我们需要对框架进行改造呢?别急,这只是我们为了解决级联灰度的一个取巧的办法。
如图,当ServiceA和ServiceB同时开发一个版本时,入口流量通过匹配规则路由到了ServiceA的v2版本,要想同样路由到ServiceB的v2版本,我们就需要将请求的匹配条件带在请求里,否则就做不到级联。
- 路由信息传递
因此,我们在服务间进行rpc请求时,默认带上了路由规则的http的header 条件,或grpc的metadata,如feature:v2
。当然不进行客户端改造也是可行的,毕竟istio可以对流量进行任意修改,比如我们可以通过Headers.HeaderOperations规则
对header进行更改( https://istio.io/latest/zh/docs/reference/config/networking/virtual-service/#Headers-HeaderOperations ),甚至使用EnvoyFilter进行更加复杂的操作。当然因为客户端改造不大,我们采用了更加方便快捷的方式。
- 服务发现
在使用了istio后,客户端再也不需要关心目标服务的实际IP的,只需要访问目标服务的k8s service地址,出口流量在到达sidecar后,自然地就会被拦截,并按照规则达到正确的目的ip。因此,我们只需要使用grpc原生的dns resolver就可以了,去掉了之前集成的基于etcd和kubernetes api service服务发现客户端。
编译流水线
接下来是对CI的改进。我们希望开发人员不需要做过多配置,即可生成新的服务版本,并且在服务启动后配置好路由规则。这样开发人员提交代码后,只需要小酌一口咖啡,就可以开始调试。
因此我们直接采用约定俗成的方式:规定代码分支必须符合命名规范
- Feature分支
假如feature分支命名为:feature/new
我们将发布的版本打上feature:new
的label,并且添加header匹配规则 feature:new
- 灰度发布
如果是正式环境灰度,假设tag为v1.0.4
我们将发布的版本打上version:canary-1.0.4
的label,并且添加header匹配规则 versioin:canary-1.0.4
最终这些规则配置都将在CI流程中作为helm部署参数传递到chart包中,因此最重要的变更在于helm包。
Helm包
istio本身是通过VirtualService资源和DestinationRule资源对流量控制规则进行配置的。VirtualService决定了流量分发规则,DestinationRule决定了服务不同版本pod的匹配规则。
规则配置顺序大致如下
- 在服务标准版本发布时,我们会给服务pod打上
version:stable
的标签,并添加vs和dr资源。
- feature版本发布时,需要注意的是istio规则需要在服务运行成功后才能添加规则。如果是按比例灰度流量,服务未启动前就添加规则,那么灰度的流量直接就失败了。 为了解决这个问题,我们开发了一个服务去负责自动化更新vs和dr规则。
自动更新istio规则
- feature版本发布后,pod会带上
feature: {分支后缀}
的label - 我们使用k8s job来执行规则更新操作,并指定annotations:
"helm.sh/hook": post-install,post-upgrade
让服务pod部署后再执行任务 - 下一步需要保证pod处于running状态再更新规则。因为pod状态也是根据探活指针来更新的,所以我们可以直接请求探活路径,根据结果判断是否继续
- 使用istio sdk更新istio规则,先添加DestinationRule规则,再添加VirtualService规则
发布完成后,当我们请求tenant-manager服务时,流量就会进行stable版本;当我们给请求加上header :feature=boss
时,请求就会到达boss版本。并且下游所有服务只有同样存在boss版本,流量也都会就行boss版本。
现网灰度
上述流程介绍了使用istio规则进行开发环境隔离的实践流程,现网服务的灰度发布流程也是如此。但是现网的灰度规则需要一般更复杂,我们是如何做到的呢?
按比例灰度
按比例灰度是一种常见的需求。同样为了减少开发人员直接更新istio规则,我们在CD流水线中增加了对灰度比例的支持:
发布者可以添加灰度权重(总共100),由配置更新job去更新stable版本的灰度版本的权重配置。达到按比例更新的需求。
自定义条件灰度
版本发布后,要怎么注入灰度header,也是困扰开发人员的一个难题,一般来说,header可以在客户端、网关、服务侧3个地方注入。
- 前端header注入
前端header注入一般需要前端同步发版。例如当我们需要对手机端用户进行灰度时,可以发布手机端前端服务,在手机端对用户进行header注入
- 网关注入
如果是需要对某类用户、某种url、或者某个请求参数值进行灰度,这种复杂场景我们要怎么做呢?
这种我们就可以在网关注入header:我们使用了kong网关作为流量入口,并开发了一个自定义的插件用以header注入:
该插件支持按method、url、query、以及jwt json的匹配方式,注入自定义的header
例如:灰度某个学校的用户(我们的鉴权是在网关完成了,鉴权后网关会生成jwt token用以在后端服务间传递鉴权信息,因此插件可以拿到权限信息,如表示学校id的OrdId:claim.OrgId=123),于是我们变对学校id为123的用户注入version:v2
的header,完成灰度条件的注入
- 微服务侧注入
更加复杂的灰度规则就需要开发人员去修改VirtualService或者在服务中手动注入header了。
但是上述两种方式已经满足我们所有灰度需求,这种方式通常也就是本地开发时,本地服务rpc访问集群服务时用到。
我们使用一个单独的grpc kong网关暴露集群中的grpc接口,方便本地开发时连接远程服务。集群中服务部署时会通过反射扫描所有grpc接口注册到该网关
效果
- 全链路灰度 以下是我们线上一次版本发布的示意图,全链路灰度发布为发布提供了更可靠的保障
- 精确灰度比例控制 精确的灰度比例控制可以减少因服务问题造成的影响范围,如灰度60%时发现服务性能达到顶峰,立即回滚比例并优化,再次发布后修复问题
可观测性
istio本身提供了日志、追踪、指标的可观测能力,虽然无法观测服务内部细节,却也可以作为服务能力的补充。
- 请求指标
- 访问日志
- 链路追踪
成本
istio带来了随心所欲的流量控制,但是代价也是昂贵的。最主要的成本就是资源消耗的增加:
- 资源消耗
我们以一个单pod 800qps左右的服务为例,istio的一个sidecar消耗了大约0.3核的cpu,130MB的内存。istio官方数据:https://istio.io/latest/zh/docs/ops/deployment/performance-and-scalability/#CPU-and-memory
今年我们使用grpc proxyless模式,将资源消耗降低了很多,后面我也会单独写一篇文章介绍proxyless
- 请求时延
对于单服务请求,P90延迟增加了5.7ms,和istio官方结论相差不大:https://istio.io/latest/zh/docs/ops/deployment/performance-and-scalability/#latency
四、展望
eBPF
eBPF是一个能够在内核运行沙箱程序的技术,提供了一种在内核事件和用户程序事件发生时安全注入代码的机制,使得非内核开发人员也可以对内核进行控制。
近年来eBPF的概念随处可见,主要应用在网络诊断、优化、安全等多个方面,像k8s的网络插件cilium就是基于eBPF实现的。
在istio中,我们也可以用 eBPF 代替 iptables 规则可以将应用发出的包直接转发到对端的 socket,从而缩短 sidecar 和服务之间的数据路径。 见:https://istio.io/latest/zh/blog/2022/merbridge/
proxyless
proxyless mesh 实际上就是 sdk ServiceMesh的解决方案,微服务框架本身承担了原来sidecar应该做的流量控制能力,sidecar仅保留了配置转发等逻辑。官网介绍:https://istio.io/latest/blog/2021/proxyless-grpc/
xds是一系列配置同步接口的总称,如LDS、RDS、CDS、EDS分别代表listeners、routes、clusters、endpoints的配置同步。grpc通过实现xds接口,接收从istio控制平面下发的配置信息,更新自己的resolver配置。
grpc一直都在xds特性上发力,从1.30版本开始,几乎每个版本都在完善对xds能力的支持。https://grpc.github.io/grpc/core/md_doc_grpc_xds_features.html
虽说这对istio解耦的思想一定程度上是一个退步,但是其对资源使用的降低是显而易见的,并且时延总体变化不大,后面我会写一篇文章介绍我们在grpc proxyless的实践。
ambient mesh
今年9月的时候,istio提出了ambient mesh的概念,见文章: https://istio.io/latest/blog/2022/ambient-security/
大致意思是Ambient mesh 使用了一个共享代理,该共享代理运行在 Kubernetes 集群的每个节点上,用来处理L4流量;另外使用一个或多个基于 Envoy 的 “waypoint proxy”( 来为工作负载进行 L7 处理。Istio 控制平面会配置集群中的 ztunnel,将所有需要进行 L7 处理的流量发送到 waypoint proxy。
个人总结其主要核心为两点:
- 一是拆分流量,按不需要代理、只需L4处理、需要L7处理3种情况拆分流量,使得流量尽可能地减少代理,应代才代。
- 二是流量代理是以deamonSet或deployment的形式部署的,不再以sidecar的形式,可以伸缩,节省大量资源。
istio预计在23年会发布试用版本,也值得期待
总之,istio作为ServiceMesh的代表作,不但得到了 IBM、Google 和 Lyft 等行业领军者的支持,同事拥有众多大厂的线上的实践,取得的不错的效果。但是其在集群过大时,对资源的消耗和性能问题一直饱受诟病,这也是众多大厂在需要istio基础上进行优化二次开发的原因。但是随着istio在这些问题上的方案优化,例如proxyless或ambien mesh,取得了不错的效果,我们也继续探索istio,和大家一起交流讨论。