文章目录
前言
目前kubeedge 1.4 的版本中,edgecore 使用service 的方式在edge node上进行管理。EdgeMesh并没有使用传统 ServiceMesh 中走 sidecar 的模式,ServiceMesh要求底层必须安装CNI插件实现容器的互通。有趣的是EdgeMesh同时也支持CNI这部分的配置,如果用户想使用传统的 k8s 网络,也可在边缘节点安装 CNI 网络插件,这种模式下,Edge Node就和云上Node处于同一个网络平面上了。
一、EdgeMesh 的作用
首先谈谈EdgeMesh 的作用,在edge node 不使用与云端一致的CNI插件的时候,部署在edge node 上的pod像是一个个孤立的小岛,它的网络就是使用的默认Docker 的网桥模式,和你单独启动一个docker 容器,在网络上看没有分别。EdgeMesh 的存在可以使得边缘与边缘之间互联互通和边缘向云端(单向)互通。
二、EdgeMesh 的入口
Beehive 是一种协程间通讯的消息总线,具体介绍可以参这里, 它的使用很简单,这个小工具可以让我们把自己的服务,做一个module 层级的拆分,使得小模块之间的通讯非常简便。EdgeMesh就是作为edgecore 中的一个小module启动的。
启动部分在Edgecore 的registerModules()
中,edgemesh.Register(c.Modules.EdgeMesh)
,在注册的同时,EdgeMesh 会创建一个TCP连接的server端,默认监听的是Docker0网桥的40001 端口。这个TCP的server 将会监听所有的TCP请求,并处理这些请求,处理的过程就是对微服务进行服务发现和服务响应的过程。其代码在:func InitConfigure(e *v1alpha1.EdgeMesh)
。
三、EdgeMesh 的启动
1. plugin.Install()
这里,Edgemesh 使用了一个不是很主流的,在微服务开发框架中还略有版权争议的微服务框架go-chasis去实现,这个框架使用起来比较简单,它会把一些微服务的策略开放出来,由用户去指定。这里EdgeMesh指定了两个部分:
- 设置服务发现的实现,因为这里不是使用service center 的方式,而是通过查询service名称去查询pod 的endpoint,并且通过Host Network 或者Container Network去访问POD,这里说,目前edgemesh 只支持Host Network 和Container Network,我们看下它是怎么实现的:
这里设置了服务发现的配置,它只需要真正去实现这个接口registry.DefaultServiceDiscoveryService = meshRegistry.NewEdgeServiceDiscovery(opt)
它首先根据服务名称,microServiceName,也就是k8s 格式的service名称,根据字符串解析出来service 的name 和 namespace以及端口。然后通过metaclient 发起service 和 pod 的查询请求,拿到真正的backend,拼接成一个个go-chasis 所需要microServiceInstances。func (esd *EdgeServiceDiscovery) FindMicroServiceInstances
- 设置go-chasis的负载均衡的策略,从代码看,都是一些常见的负载均衡策略,不再赘述了。剩下就是一些不太重要的设置。接着启动这部分组件的初始化工作。
control.Init(opts) // init archaius archaius.Init()
2. listener.Init()
listener 是一个Service信息的存储层。
-
unused 保存未使用的service fake ip。
-
svcDesc保存service ip 和 服务名称、服务端口。
创建到匹配到edge pod的service, k8s 是不会被k8s分配 clusterIP的,那么你如果想要发起TCP连接的话,还是必须要有一个IP的,所以EdgeMesh在启动时,会给所有当前node 所能匹配到的service初始化一个Fake IP。大家自然也会认识到,后续一定会有service 的增删改,是的,在EdgeMesh的启动的最后一步,EdgeMesh启动了beehive 的Receive,它接收的是关于信息是service和pod的增删改。部分代码:
listener.MsgProcess(msg) // MsgProcess processes messages from metaManager func MsgProcess(msg model.Message) { // process services if svcs := filterResourceTypeService(msg); len(svcs) != 0 { klog.Infof("[EdgeMesh] %s services: %d resource: %s", msg.GetOperation(), len(svcs), msg.Router.Resource) for i := range svcs { svcName := svcs[i].Namespace + "." + svcs[i].Name svcPorts := getSvcPorts(svcs[i], svcName) switch msg.GetOperation() { case "insert": cache.GetMeshCache().Add("service"+"."+svcName, &svcs[i]) klog.Infof("[EdgeMesh] insert svc %s.%s into cache", svcs[i].Namespace, svcs[i].Name) addServer(svcName, svcPorts)
如果是addServer,那addServer会用来从unused 里选一个Fake IP保存在svcDesc里。
svcDesc里保存的内容会在每次TCP请求来临时使用到。
3. proxy.Init()
这部分代码相对简单,主要做的是对当前edge node 设置iptable 规则和路由规则。
这里看到,我们设置了三条规则,所有走到目标地址为Fake IP的网段的TCP package在流经Docker0 网桥的时候,都会被做一个DNAT转移到上边TCP listener 所监听的TCP server 上去。
proxier = &Proxier{
iptables: iptInterface,
inboundRule: "-p tcp -d " + config.Config.SubNet + " -i " + config.Config.ListenInterface + " -j " + meshChain,
outboundRule: "-p tcp -d " + config.Config.SubNet + " -o " + config.Config.ListenInterface + " -j " + meshChain,
dNatRule: "-p tcp -j DNAT --to-destination " + config.Config.Listener.Addr().String(),
}
与此同时,edge node 上也会设置相关的route rule
gw := config.Config.ListenIP
route = netlink.Route{
Dst: dst,
Gw: gw,
}
设置后,会启动一个协程,同步iptables 的变化
// sync
go proxier.sync()
4.listener.Start()
这一步可以说是最关键的一步了,在listener.Init()的时候,TCPserver 已经Listen,准备接收TCP请求了,在当前步骤,Start() 会 处于阻塞接受Accept 请求的状态,根据tcp connection,封装一个proto,这个概念是go-chasis 的概念,proto 必须实现process 接口。这里实现的是http proto。
一个go chasis 典型的处理过程是为每一次的请求,创建handler chain,并将该次请求,封成invocation, 一般情况下,它处理的是http的请求,这里我们是L4 的TCP请求,所以这里有一步很必要:
req, err := http.ReadRequest(bufio.NewReader(p.Conn))
这里会把tcp请求升级到L7, 这样我们就可以从Header里拿到真实的k8s 格式的service name,service name 如此重要,我们可以基于此获取service 资源,并且基于此获取到pod 的列表。这部分我们待会儿深入分析,现在我们拿到了L7 的request,这时候根据go-chasis的使用实践,需要封装invocation了,这里最关键的就是inv.MicroServiceName = req.Host,在第一步中,服务发现的接口:FindMicroServiceInstances,最重要的参数就是MicroServiceName,服务发现根据这个名称去解析service 所在的namespace 和它的name。
// set invocation
inv.MicroServiceName = req.Host
inv.SourceServiceID = ""
inv.Protocol = "rest"
inv.Strategy = config.Config.LBStrategy
inv.Args = req
inv.Reply = &http.Response{}
接下来是三个操作:
- 创建handler chain
go-chasis 默认提供了很多种handler,由于loadbalance 是我们自己实现的,所以是必要要加上的,handler.Transport是必须要加的,所以http 这个chain就集成2个handler。
c, err := handler.CreateChain(common.Consumer, "http", handler.Loadbalance, handler.Transport)
- Next 调用
p.req = req
c.Next(inv, p.responseCallback)
Next代码点进去也很简单,由于CreateChain,就是给往chain里插入多个handler,所以Next 函数,就是遍历这么多handler,一个个处理,当处理到最后一个handler 的时候,就调用下一步的responseCallback返回。
- 实现返回的回调入口
// responseCallback implements http handlerchain callback
func (p *HTTP) responseCallback(data *invocation.Response) error {
...
}
返回的实现,就是把这个微服务的result 写会到connection里。
p.Conn.Write(respBytes)
5. dns.Start()
在edgecore 启动后,它会把当前宿主机的/etc/resolve.conf 里的第一个DNS server 设置成docker0 网桥的IP,也就是说,所有的edge node 和 该node 上的pod 发起的DNS 请求会发给docker0 53 端口。
这个DNSserver 的实现,没有过多需要关注的,只需要看下核心code 部分:
func recordHandle(que *dnsQuestion, req []byte) (rsp []byte, err error) {
继续看到这里:
lookupFromMetaManager
// lookupFromMetaManager confirms if the service exists
func lookupFromMetaManager(serviceURL string) (exist bool, ip string) {
name, namespace := common.SplitServiceKey(serviceURL)
s, _ := metaClient.Services(namespace).Get(name)
if s != nil {
svcName := namespace + "." + name
ip := listener.GetServiceServer(svcName)
klog.Infof("[EdgeMesh] dns server parse %s ip %s", serviceURL, ip)
return true, ip
}
klog.Errorf("[EdgeMesh] service %s is not found in this cluster", serviceURL)
return false, ""
}
这一步我们就可以拿到,在listener那一步分配给该service 的Fake IP。有了这个IP,我们就可以根据这个fake ip(一定是落在了proxy 创建的3条链里)把L4的package转发到docker0 的40001端口。
6. 接收beehive message
msg, err := beehiveContext.Receive(constant.ModuleNameEdgeMesh)
if err != nil {
klog.Warningf("[EdgeMesh] receive msg error %v", err)
continue
}
klog.V(4).Infof("[EdgeMesh] get message: %v", msg)
listener.MsgProcess(msg)
这一步里,listener 会处理接收到的关于service 或者pod 的message,如果是删除了service 那么就回收Fake IP,删除缓存里的service 和pod 信息,更新svcDesc。
总结
我们分析了edgemesh 的全部实现逻辑,看到了它和service mesh 需要sidecar完全不一样,它更像是整合了k8s里kube-proxy 和 core-dns 功能的一个组件。看得到实现很简单,也意味着稳定性高。当然它也存在一些弊端:
-
我们可以从FindMicroServiceInstances它的实现看到:
// all pods share the same hostport, get from pods[0] if pods[0].Spec.HostNetwork { // host network hostPort = int32(targetPort) } else { // container network for _, container := range pods[0].Spec.Containers { for _, port := range container.Ports { if port.ContainerPort == int32(targetPort) { hostPort = port.HostPort } } } }
它只支持host network 和 container network,这和官方文档
的描述是一致的。这就对网络安全提出了挑战,你的防火墙策略根本没法开,因为首先你不知道pod 事先会落在哪个Node ,其次还会可能面临Node 上Port 的冲突,这简直是运维小哥哥的噩梦。 -
功能本身的限制
它只能实现edge node 节点间的数据流通信,和边缘访问云端的单向通信,无法实现云端对边缘的数据通信要求。目前看,单纯考虑edge mesh 的实现过程,我们从原理上就无法实现,我们看下一个典型的http请求的过程:
云端到边缘的数据流通信,假设我们在云端发起一个:curl http://nginx-svc.default.svc.cluster.local:12345
,那么pod 会首先查看core dns,而我们知道这个svc 根本没有cluster ip,所以请求在dns 这一步就停止了。不会走到后面的步骤。集群外就更无法访问了。 -
pod 无法kubectl log 或者 kubectl exec 原因还是云与边不是同一个网络平面。