什么是SideCar?
Sidecar模式定义:
Sidecar 模式是一种常用于微服务架构中的设计模式,该模式允许将应用程序的核心功能与辅助功能(如日志记录、监控、配置管理、网络通信等)分离开来。在这种设计模式中,每个微服务主容器旁边都有一个“边车”(Sidecar)容器运行辅助功能,就像摩托车旁的边车一样,Sidecar 容器和主容器共享同一个生命周期和网络空间。
你专心开车,其他交给我。
整体Sidecar就是这样:
总结:把非核心业务杂七杂八的东西分离出来,微服务的关注点尽可能只在业务上面,减少业务开发的复杂度。
Sidecar主要干啥?
Sidecar模式通常会处理以下范围的功能:
-
网络通信:
- 代理和管理微服务之间的通信,例如用来处理进出的网络流量、服务发现、负载均衡、断路、重试和超时。
-
安全性:
- 实现安全协议,如TLS加密、认证、授权和密钥管理。
-
监控与日志记录:
- 收集和导出日志、性能度量指标和追踪信息,以供监控系统使用。
-
配置管理:
- 动态加载和管理配置数据,而无需重启应用程序。
-
故障处理和调试:
- 提供调试支持,记录崩溃报告和性能瓶颈。
-
健康检查:
- 周期性地检查主服务的健康,并可实施自我修复措施。
-
API 网关或接口转换:
- 处理服务的REST或gRPC API,提供API网关功能,或者对老旧系统进行接口转换。
-
服务依赖抽象:
- 管理服务间的数据库连接、队列等系统资源的访问。
-
请求/响应转换:
- 对进出的请求或响应数据进行转换或扩展。
Sidecar有哪些实现?
Sidecar在容器化和微服务领域有很多实现:
1. Envoy
Envoy是一个开源的、高性能的边缘和服务代理,专门设计用于云原生应用环境。由Lyft公司首次开发发布,后来成为了Cloud Native Computing Foundation(CNCF)的一部分。它的设计允许它同时在应用程序的边缘和内部使用,作为边缘网关、负载均衡器或服务网格的一部分,以提高微服务之间的通信质量和效率。
2. Istio
Istio是一个开源的服务网格,用于连接、保护、控制和观察在Kubernetes集群中运行的服务。它为微服务提供一种统一的方式来管理网络通信,安全性,策略和观察性,无需改变应用本身的代码。Istio利用了Envoy代理,它作为Sidecar部署在每个服务的Pod中,拦截所有进出Pod的网络通信。
3. Linkerd
Linkerd是一个开源的服务网格,专门为云原生应用设计,用以在微服务架构中提供可靠的服务间通信。它提供关键功能,如服务发现、负载均衡、故障处理、请求路由和安全性,在不改动服务代码的情况下,使得服务更容易监控、管理和控制。Linkerd是由Buoyant公司开源的,并且是Cloud Native Computing Foundation(CNCF)的一部分。
4. …
自己搞一个简单的?
我们个人势单力薄,搞一个像他们这么有高级功能的组件还是挺费时间的,况且我们还在温饱线上挣扎。
但是又想在这块搞点小事情,那么整个简单的。
先来个简单的需求
一个业务系统部署在k8s集群中,每个服务采用一个Pod一个服务的方式部署,每个Pod都有一个Service。
就这么简单来一下就可以了,Service关联Pod提供统一访问入口。
那根据SideCar的拓扑,我们可以在Service和Pod中容器之间加一个Sidecar来管理流量,像这样:
在这个拓扑中,所有流量先进SVC(逻辑上哈),然后转发给车最后转发给Pod。
车的具体需求?
- 管理HTTP流量(TCP的暂时不支持):只开发一个IP白名单规则。如果请求这个服务的IP不在这个白名单中,则返回404,否则转发该请求。
- 白名单规则配置:支持单个IP地址,支持CIDR地址。
需求就是这么简单,我们让SVC把流量先转给车,车处理了一下以后再转发给业务服务。
准备开干
首先开发一个HTTP服务器
使用Go built-in API就可以很简单开发实现一个HTTP服务器,就是使用net包:
http.HandleFunc("/", handler)
err = http.ListenAndServe(conf.Server.Addr, nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
几行代码搞定,额,这还是有点快,让人猝不及防。那这样我们的HTTP服务器就搞定了
其次 读取配置文件
我们用yaml来定义白名单配置,白名单中有IP和CIDR地址。
whitelist:
- 192.168.1.1
- 192.168.1.2
- 10.22.81.0/24
- ::1
然后我们使用yaml解析库来读取它。
先来下载一下依赖库:
go get gopkg.in/yaml.v2
然后使用库提供的api来读取yaml文件:
type Conf struct {
Server Server `yaml:"server"`
Downstream Downstream `yaml:"downstream"`
Whitelist []string `yaml:"whitelist"`
}
err = yaml.Unmarshal(source, &conf)
if err != nil {
log.Panic(err)
}
设置HTTP的请求处理函数
我们请求处理函数从X-FORWARDED-FOR
协议头来获取请求的Ip列表字符串,字符串以ip1,ip2,ip3,...
来组成的。
我们只取第一个,就是上面示例的ip1
,因为只有这个IP才表示请求方的IP,其他都是代理方的。
func handler(w http.ResponseWriter, r *http.Request) {
ipStr := r.Header.Get("X-FORWARDED-FOR")
if ipStr == "" {
ipStr = strings.Split(r.RemoteAddr, ":")[0]
} else {
ipStr = strings.TrimSpace(strings.Split(ipStr, ",")[0])
}
// 如果IP地址在白名单中,那么就转发请求
if ipInWhitelist(ipStr, conf.Whitelist) {
serveReverseProxy(conf.Downstream.URL, w, r)
return
}
// 否则返回404
http.NotFound(w, r)
}
转发请求
Go内建包net实现了ReverseProxy,我们直接使用httputil.NewSingleHostReverseProxy
就可以直接转发HTTP请求,很快~。
func serveReverseProxy(target string, w http.ResponseWriter, r *http.Request) {
url, _ := url.Parse(target)
proxy := httputil.NewSingleHostReverseProxy(url)
r.URL.Host = url.Host
r.URL.Scheme = url.Scheme
r.Header.Set("X-Forwarded-Host", r.Header.Get("Host"))
r.Host = url.Host
proxy.ServeHTTP(w, r)
}
这样我们的核心功能就实现了。
一点小细节
实现了核心流程以后,我们还得关注配置中IP的解析。
Go的net 包也提供了IP地址和CIDR地址的解析,我们只需要调用一下
net.ParseIP
net.ParseCIDR
就可以实现IP解析了。
transformToCIDR
这个函数就给IP地址增加/32转换成CIDR形式,大家可以不用太关注。
func ipInWhitelist(ipStr string, whitelist []string) bool {
ip := net.ParseIP(ipStr)
for _, cidr := range whitelist {
_, ipNet, err := net.ParseCIDR(transformToCIDR(cidr))
if err != nil {
return false
}
if ipNet.Contains(ip) {
return true
}
}
return false
}
部署
开发这个简单的需求一百多行代码就可以搞定了,但是部署怎么办?
我们在k8s中不是有Service嘛,我们Service在配置流量转发规则的时候先转到我们的HTTP Server,然后我们HTTP再转发给业务服务。
Pod改造
原来我们的拓扑是这样的:
加上我们自己的sidecar就变成这样了:
随之而来的改造
k8s Pod支持一个Pod多个Container,所以我们可以直接在Pod中增加一个Container:
比如:原来我们的Pod定义是这样的:
- name: my-order-app
image: my-registry-domain-name/ my-order-app:${my-order-app-version}
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
改造后:
containers:
- name: sidecar
image: my-registry-domain-name/my-custom-sidecar:latest
imagePullPolicy: Always
ports:
- containerPort: 8081
- name: my-order-app
image: my-registry-domain-name/ my-order-app:${my-order-app-version}
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
Pod的改造就完成了,接下来就是Service改造了。
Service改造
我们要把Service的目标端口改成我们自己的Sidecar应用才可以。
比如:原来我们的Service是这样的:
Service端口为8080,转发到app的8080端口中。
apiVersion: v1
kind: Service
metadata:
name: my-order-app-service
namespace: my-app
labels:
app: my-order-app
spec:
ports:
- name: my-order-http
port: 8080
protocol: TCP
targetPort: 8080
selector:
app: my-order-app
type: ClusterIP
现在我们要把Target改成8081
,也就是我们的Sidecar HTTP服务器端口。
apiVersion: v1
kind: Service
metadata:
name: my-order-app-service
namespace: my-app
labels:
app: my-order-app
spec:
ports:
- name: my-order-http
port: 8080
protocol: TCP
targetPort: 8081
selector:
app: my-order-app
type: ClusterIP
这样我们的Sidecar就搞定了。
总结
通过使用Sidecar模式,我们能够实现对微服务架构的强化,也就是关注点分离,让你专注于开车,开好车,开快车。
它提供了一种有效的方式去分摊微服务的辅助功能(如日志记录、监控、配置管理、网络通信等),而无需改动微服务的核心业务逻辑。这带来了显著的优点,包括简化开发复杂度、增强服务的可维护性和可伸缩性,以及提供了横切关注点(cross-cutting concerns)的统一管理。
优点
关注点分离:通过将非核心功能从应用程序逻辑中分离出来,开发者可以更专注于业务逻辑。
可插拔性:Sidecar可以独立于应用程序部署和更新,使得维护工作更加灵活。
一致性:在多个服务中应用相同的配置和策略,不需要在每个服务上单独配置。
扩展性:新增功能时,可以在不影响现有服务的情况下,在Sidecar中添加新的组件。
增强的能力:例如,集中日志处理、安全性控制、以及动态路由等。
缺点(Sidecar的通病):
资源使用:每个Sidecar都会消耗额外的计算和内存资源,这可能导致资源的浪费。
复杂性:虽然单个服务的复杂性降低了,但整体架构可能会因为增加的组件而变得复杂。
调试困难:由于Sidecar模式涉及多个容器协同工作,调试和追踪问题可能比传统单体应用更加困难。
网络延迟:每个请求都要通过Sidecar代理,可能会增加网络延迟。