微服务中网络层抽象与改进
2020-04-06 / modified at 2022-09-26 / 3.6k words / 13 mins
️This article has been over 2 years since the last update.

本文从集中API Gateway到Sidecar介绍了抽象网络的多个方案,供读者参考。

需要考虑的因素

  • 网络维护成本,应用适配SDK成本
  • 负载均衡在哪里做,滚动升级,优雅停机,灰度发布
  • 时延/带宽/容灾

什么是网络层抽象?

指应用间通信时,将不会通过确切的IP地址进行连接,而只用知道服务名称即可,剩下全部交给DNS/代理。内部的实现将通过高层来执行,而应用不用关注。

在当前业界中,主要有如下方法

  • 基于L2/L3转发:

    • 虚拟网络SDN/L3 VRRP/ECMP等价路由/RDMA实现自己的VPC与ELB
    • 基于隧道:比如vRouter/Service构造IP-on-IP网络与(看起来)中心化的Service定义
  • 基于L4/L7转发:

    • 将反向代理Nginx作为总转发中心,而不进行点对点通信
    • 基于ExternalService/Sidecar: 基于前置代理劫持服务,实现服务间犹如对接localhost
  • 基于SDK: 比如Eureka/SpringCloud的命名服务,全程控制自己的方案。

其中Sidecar是2018后开始大范围流行的技术,本文将以Consul+Nomad介绍它的作用。

基于基础设施的L2/L3实现

L2方案

此方案在大部分场景中都属于运营商或者云厂商的基础设施,应用层不会涉及。比如

  • 在通信层硬件设备实现(比如实现城域网内更优的直通)
  • 通过L2 on L3的虚拟交换实现(比如OpenStack的vSwitch/Vxlan/智能网卡offload等)

这里难点在于x86设备性能难以追得上智能网卡,导致机房密度升不起来,涉及到通信/协议栈/加速卡/虚拟机绑核等领域,是个大坑,非云领域的专业人员不推荐碰。

L3方案

这里会涉及到VIP,其实VIP本身只是多申请了一个虚拟网卡并用ARP作为看门狗而已,高层封装可以基于SNAT/VRRP(虚拟路由冗余协议)等实现,需要配置软路由(比如netfilter/ovn)等基础指令。一般来说在云服务中一般集成现成的ELB即可,不推荐用虚拟机搭建“高可用”。假如非要用物理裸机搭建

  • 可以考虑至少两台物理机接入双网卡连接外网,然后部署LVS/Nginx + Keepaliave 作为ELB+安全组,然后负载Kubernetes的裸金属集群
  • 或者搭建私有云,它内部一般用了等价路由ECMP,内部甚至用的也是LVS,但是转发的x86设备性价比不高

RDMA方案

RMDA:直接砸钱上InfiniBand实现全套硬件,甚至用RMDA搭建Redis(比如Gauss DB for Redis),缺点是网卡等硬件的昂贵。

总结

此类方案如果涉及到有状态的应用层(比如Gitlab/Postgresql),需要各种应用级的健康检查,事实上运维与方案还是很繁琐的,一般不推荐。这里涉及到SDN/云计算等领域,也是个大坑,假如你没有ELB的云服务,不推荐做涉及到交换机变更级别的应用层设计。

基于中心化的L4/L7 Gateway实现

这个是最简单的方案,比如将Nginx作为中心控制器,应用间互联全部通过Nginx的IP/域名即可。

1
App A -> Nginx -> App B

这个方案虽然简单,但是环境搭建还是有一定的硬编码,负载均衡压力全部绑定到Nginx上了,而且高级的健康检查与调度转移还是很难做;更新时需要手动/Ansible操作。

应用层基于SDK实现

指应用不借助于任何PaaS工具,直接自己搞一套,比如SpringCloud

  • 控制面与数据面可以用任何协议、如何负载均衡完全自由定制,SpringCloud中基于Netflix OSS/Consul+七层负载均衡既可以完成一个简单的Java项目,也可以用Fabio实现一个中心化的动态网关。
  • 但是应用侧与某个框架/SDK将绑定到一起,招聘与维护很麻烦
  • 功能受限于SDK,比如SpringCloud很多不完善,ClientSide负载均衡是不支持blocking queries的,总会失效一段时间,需要配置wait+路由+命名服务,改Spring源码又很费时间(上述踩坑花了很多时间,但是收益很低,而且API更新很快)。
  • 这类实现中,与Kubernetes集成事实上是维护了两份服务框架,因此部分投入浪费了

我曾经接触过某些自己实现网络的项目,这种项目很容易成为烂摊子,比如

  • 将网络通信细节与业务绑定到一起(比如鉴权,SSO登陆)。
  • 跨语言集成(比如NodeJS/祖传Java项目)成本高

我个人不太推荐这种集成SDK的方法,团队投入不增值。

但是有一个例外,就是Erlang,它在应用层实现了全部上述功能,但是这个门槛较高,需要强团队才可以。

基于Kubernetes的L3的Service(VIP)实现私有网络

此方案是在现有网络中实现一个可信的私有内网(IP networking fabric ),服务间对外需要NAT,对内可以直连,此方案需要另外加负载均衡(比如Kubernetes需要定义一个Service的yaml)。

1
2
3
4
// 外部请求
网络流量 -> Service(NAT与LB) -> 多个pod管理(preHook, gracefullyShutdown)
// 服务间请求(请求VIP)
PodA -> ServiceB's VIP -> 多个PodB

以Kubernetes的tigera/calico为例,它在容器内通过虚拟网卡拦截请求,并将请求转发给网络虚拟层,内部通过iptable(IP-in-IP)/IPVS(Internet Protocol Virtual Server)进行iptable-NAT(而不需要封包)实现了一套内网

PlaneimplementationComments
Data Planeiptable/IPVS+virtual network interfaceCPU based
Control PlaneetcdSimple LB

应用间通信可以通过kubernetes自带的DNS服务直接解析到Service的VIP,而不是直连的场景

参考:

缺点

  • 安装与选型比较复杂,达到生产级有很多运维成本(比如etcd的安全),除非购买现成云服务
  • YAML配置文件很粗放,定制全靠修改label/annotaion/CRD,就像Java把业务逻辑扔到XML一样。由于嵌套逻辑被flatten,导致初学者很难掌握各个YAML间的层次关系
  • 环境依存非常明显,需要一整套生态,有Vendor lock-in的风险
  • 它会在机器上配置非常多的路由表,本地进行remoteDebug需要层层跳转与NAT转发,配置复杂

基于前置Sidecar/Mesh代理

传统的DNS解析

自上而下:在传统服务通信中,通过ns记录让上游DNS递归转发给你管理的DNS,可以实现私有的域名管理,比如就近CDN。

自下而上:客户端通过编辑host或者内网DNS,实现私有DNS的管理。

什么是Sidecar?

Sidecar在英文上的翻译是摩托车一侧的小车,苹果也把Mac旁边的iPad显示器也叫做Sidecar。从纸面含义上,sidecar表示核心业务组件周边的支撑组件。在Azure等云厂商中把这种主业务与支撑组件共同部署的模式叫做Sidecar。通过引入主容器应用的辅助容器,可在不修改原生应用源码的基础上快速实现网络代理、健康检测、收集日志等通用功能。简单的说,它就是容器级别的AOP。

下面是在部署中三种AOP的实现形式

LevelimplementationComments
ByteCodeJava/SDK/AOPstrong bind with logic
ProcessLinux LD_PRELOADWorks only on single machine
ContainerSidecar(Pod/DaemonSet)rely on cloud storage/container network interface

举个例子,假如我有个祖传CURD的Java项目,想集成命名服务,那么有如下多个方法进行改造

  • Java层引入SDK包进行集成,在DNS请求等位置进行AOP编码。但是Jar包冲突,硬编码配置肯定是要走一趟的,而且改造祖传代码还要做各种兼容与测试验证,很麻烦
  • 容器层在构建Dockerfile时,将另一个命名服务的代理也打进去,让它们内部在一个Docker镜像中互相通信。这是一种折衷的方法,但是后续升级命名服务/优雅停机比较难
  • 基于PasS平台实现,比如在Kubernetes/Nomad中的Pod定义两个Container,一个为业务逻辑,一个为命名服务。第二个服务可以把DNS/负载均衡全部给搞定。

Sidecar与网络虚拟化

虽然Sidecar的术语很高端,但是本质就是流量劫持,让调用方只用知道某个服务的名称与端口既可使用,但是在底层,仍然需要实现中心化的网络控制面与数据面。

PlaneimplementationComments
Data PlaneCNI/TLS proxy
Control Planeconsul/etcdService rules

它的优点

  • 可以不依赖vRouter虚拟网络,支持在公网上点对点加密连接
  • 在TCP层实现了控制面与数据面,也是软交换,性能肯定没有网卡强,但是配置灵活简单
  • 支持localhost代理模式(类似Fake IP),现有应用不用修改代码,如同本地开发,调试成本比网关层层配置更加简单
  • 实现有ExternalService/Envoy/Istio/Consul Connect等实现了L4/L7透明穿透,有成熟的LB方法

缺点

  • 学习成本高,需要掌握很多概念,目前尚没有类似K8s这种垄断级的项目
  • 引入了更多的控制面组件,攻击面与排错成本会更高

这种方案对应用变更最低,应该是后续主流。

Nomad+Consul例子解析

Nomad Connect官网的例子实验,它的HCL默认部署了如下服务

  • count-api: 业务API(一个计数器),端口为9001,但是没有显式地配置,它没有集成任何SDK
  • count-dashboard: 消费者,它想消费count-api:8080的数据来显示到前端网页(9002)

注意这里网络里的配置mode = "bridge"指cni插件,而不是广义的桥接

当启动完成后,可以发现

  • count-api/count-dashboard: 业务组件没有集成任何Consul SDK就实现服务注册了,调用服务犹如请求localhost一样
  • 两个sidecar服务: 是envoy代理,它们是业务组件的前置Proxy

在Nomad下,自动开启了如下网络端口

NameDynamic?Host AddressMapped Port
connect-proxy-count-dashboardYes10.211.55.40:2096820968
count-dashboardNo10.211.55.40:90029002
connect-proxy-count-apiYes10.211.55.40:3170631706

在应用层,将完全不用关心任何IP信息,全部走localhost请求即可。

在底层,通过envoy/pause代理屏蔽网络细节,调用链大致如下

1
count-dashboard -> sidecar proxy -(加密流量)-> sidecar proxy -> count-api

参考

注意做好localhost的流量加密配置,这里可能被nc转发出去。

总结

对比

多种对比如下

Name基于Nginx转发基于L2/L3基于Kubernetes基于SideCar
命名服务中心域名ARP/VIPService/etcdConsul
负载均衡中心代理固定算法ServiceVIPEnvoy
滚动发布自己实现ReplicatedSetConsul/Nomad
优点搭建简单自己实现ELB和安全组应用层不用关注二进制部署开发方便
成本&&缺点运维后期复杂网络能力要求高边际成本高学习与网络定位成本
推荐场景快餐应用无状态负载覆盖边际成本覆盖边际成本或涉及公网流量

我个人推荐如下

  • 假如当前项目是SpringCloud(Eureka/Consul),要么基于Java继续完善填充;要么接入PaaS,抛弃业务层的微服务;
  • 假如你是从头开始做项目,建议直接基于Kubernetes/Sidecar开发,直接抛弃SpringCloud,不要去折腾各种starter了。

当前技术选型方案一览

以下是当前(2022年)我比较推荐的方案,以下两个方案均很少有硬编码,而且都是declarative语言,而非过程时语言。

  • HCL: 基于Consul/Nomad搭建项目,对外暴露使用Fabio/Traefik作为LB,服务间使用Consul Connect作为SideCar,访问外部服务同样使用SideCar。
  • YAML: 基于Kubernetes,对外暴露使用Traefik+CRD实现LB,服务间使用ServiceVIP中转,访问外部服务全部使用ExternalService进行port-forwading。

优点如下

  • 这两套PaaS间相互转换(以及后续升级,甚至退化为Nginx)时,不会有Vendor lock-in问题
  • 应用层无任何SDK需要引入,只借助了DNS/FS/SIGNAL即可交互
  • 所有的外部IP/域名均被代理转发简化,所有的配置文件/密钥均可挂载,意味着代码中没有绑死的IP
  • 常见的滚动升级/限流/灰度均基于代理(的filter/middleware/splitter)实现,业务层不用加一堆starter

缺点如下

  • 规模低时都有边际成本,规模上来后社区版PaaS运维负担也很重,最终可能还是要买企业版/招人维护PaaS
  • 由于流量基于软交换代理,网络定位更复杂,时延可能有抖动(需要绑核)

如果你当前没有现成的PaaS环境,我推荐试下基于HCL的方案,单机搭建也就一下午不到,它的边际成本相比更低,攻击面更小,而且HCL的表达能力的确比YAML,在POC阶段到正式上线均没太大的负担。

我在项目中,CI构建采用了nomad集群并进行了高度定制,而Web业务使用了k8s的传统方案。

未来

  • 在不远的将来,SpringCloud这种基于SDK的方案由于经济效益差,难以分工,后期可能越来越少;但是SpringBoot作为单体应用还将继续发展。
  • IP-on-IP/Service-on-IP等网络虚拟化将越来越普遍
  • Sidecar方案上手成本越来越低,后续可能基于Sidecar出现更多的高可用方案。

最后又回到知识的宽度与广度的问题,我曾经投入大量时间去分析研究Eureka与SpringCloud等某个技术细节,但是目前来看基本上全部过时了。虽然分析能力(基于实证的演绎)有提升,但是这个是舍本逐末,产出是很低效的,后续要基于形而上的思维去分析。