基于图数据库构建基础设施网络通信拓扑图

Introduction

笔者最近有个需求,需要在没有文档的前提下梳理清公有云上数百台虚拟机之间的依赖关系,遂花了点时间研究了一下。

The value of the idea ?

由于一些历史遗留问题,我们没有办法知道一些资源 (多台云主机) 之间的关系,它们是否是孤立的,还是会和哪些系统进行交互,是否还有必要存在。

对于微服务架构来说很容易做到这种链路状态监控,而对于传统的基础设施层面,这类信息一般都是以文档或者CMDB 以静态方式呈现,而静态的信息又不一定完全准确,维护这些关系的工作也非常的繁琐,如果能够在基础设施的层面建立起类似微服务的链路依赖关系对于运维人员是很有价值的。

Prototype Design

Gets the network TCP/IP connection

如果两台机器有关联,大部分情况下都会基于 TCP/IP 协议进行网络通信,所以能拿到所有虚拟机的网络链接情况并基于这些数据进行分析应该就能得出所有虚拟机之间的依赖。

大家都知道 Linux 上有 netstat -tunlpa 命令可以列出系统目前的所有 TCP/UDP 连接情况

# netstat -tunlpa
Active Internet connections (servers and established)
tcp 0 232 13.20.0.95:22 13.20.0.28:51292 ESTABLISHED 71505/sshd: ubuntu
tcp 0 0 13.20.0.95:22 13.20.0.28:50920 ESTABLISHED 70902/sshd: ubuntu

基于上面的结果我们可以得出以下拓扑关系

13.20.0.95 -> 13.20.0.28

Store data to the graph database

但是上面这种情况比较少见,只是一对一的关系,更多的情况是像下面这样

# netstat -tunlpa
Active Internet connections (servers and established)
tcp 0 0 13.20.0.78:43152 13.20.0.64:16379 ESTABLISHED 19328/redis-server
tcp 0 0 13.20.0.78:33446 13.20.0.79:6380 ESTABLISHED 20614/redis-server
tcp 0 0 13.20.0.78:16381 13.20.0.78:53426 ESTABLISHED 19518/redis-server
tcp 0 0 13.20.0.78:33454 13.20.0.79:6380 ESTABLISHED 20614/redis-server
tcp 0 0 13.20.0.78:6380 13.20.0.64:46524 ESTABLISHED 19328/redis-server
tcp 0 0 13.20.0.78:6382 13.20.0.64:33484 ESTABLISHED 20614/redis-server
tcp 0 0 13.20.0.78:6379 13.20.0.79:33522 ESTABLISHED 23087/redis-server

这种情况我们就难以维护拓扑关系了,我们需要借助专门设计用于存储拓扑关系的图数据库,我们只需要把每个节点的链接信息在图数据库里关联,这里使用 Neo4j

MERGE (ip1:IP{
addr:'13.20.0.95',
hostname:'nebula',
type:'VirtualMachine'
})

MERGE (ip2:IP{
addr:'13.20.0.28',
hostname:'wireguard',
type:'VirtualMachine'
})

MERGE (ip3:IP{
addr:'13.20.0.76',
hostname:'cachecloud-node1',
type:'VirtualMachine'
})

MERGE (ip4:IP{
addr:'13.20.0.77',
hostname:'cachecloud-node2',
type:'VirtualMachine'
})

MERGE (ip5:IP{
addr:'13.20.0.78',
hostname:'cachecloud-node3',
type:'VirtualMachine'
})

MATCH (a:IP),(b:IP)
WHERE a.addr = '13.20.0.95' AND b.addr = '13.20.0.28'
MERGE (a)-[:PEER]->(b)


MATCH (a:IP),(b:IP),(c:IP)
WHERE a.addr = '13.20.0.76' AND b.addr = '13.20.0.77' AND c.addr = '13.20.0.78'
MERGE (a)-[:PEER]->(b)-[:PEER]->(c)-[:PEER]->(a)

在图数据库中展现如下

image-20191228083252504

System architecture design

现在我们已经把逻辑都理清了,接下来就要进行软件工程上的设计与实现了

  1. 在每台虚拟机上部署 Agent 监控网络连接并通过 HTTP 协议上报给服务端
  2. 服务端对 Agent 传输过来的数据进行 聚合、过滤、打标签
  3. 存储到图数据库用于数据分析和可视化

最后我们的系统架构图如下

image-20191228133742415

Putting this into practice

Writing Agent

这我们用 Go 语言来编写 Agent,由于 Go 语言的特性,天生适合这种场景

我要先要获取所有的网络连接情况,通过 netstat 这个库我们可以像 Linux 命令 netstat 一样拿到网络连接信息

package main

import (
"github.com/cakturk/go-netstat/netstat"
"fmt"
)

func main() {
var peers []interface{}

socks, err := netstat.TCPSocks(netstat.NoopFilter)
if err != nil {
panic(err)
}
for _, e := range socks {
peers = append(peers, e.RemoteAddr)
}

fmt.Println(peers, nil)
}

/* output
[0.0.0.0:0 0.0.0.0:0 0.0.0.0:0 13.20.0.28:64765 13.20.0.28:51292 13.20.0.28:63571 13.20.0.95:7687 13.20.0.28:50920] <nil>
*/

然后我们还要拿到当前的主机名和 IP 地址(不考虑多个 IP 地址的情况)

package main

import (
"net"
"os"
"strings"
"log"
"fmt"
)

func main() {
hostname, _ := os.Hostname()
ifaces, _:= net.Interfaces()
var selfIP string
selfIPPrefix := "13.20."
for _, i := range ifaces {
addrs, _ := i.Addrs()
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip != nil && strings.HasPrefix(ip.String(), selfIPPrefix) {
selfIP = ip.String()
}
}
}
if selfIP == "" {
log.Fatal("can not find self ip")
}
fmt.Println(hostname, selfIP)
}

/* output
nebula 13.20.0.95
*/

完整代码放在 https://github.com/AnyISalIn/infra_graph_collector/blob/master/pkg/agent/agent.go

Writing Server

服务端的功能主要是监听一个 HTTP Endpoint,接受 Agent 传递过来的数据,然后构建出对应的 SQL 语句,首先要构建 Agent 自身,然后构建它和 TCP 连接之间的依赖

func buildSelf(host, addr string, c chan string) {
c <- fmt.Sprintf(`MERGE (host:IP {
addr:'%s',
hostname:'%s',
type: 'VirtualMachine'
})`, addr, host)
}

func buildDep(addr string, peer structs.Peer, c chan string) {
c <- fmt.Sprintf(`MERGE (n {
addr:'%s'
})
`, peer.Addr)

c <- fmt.Sprintf(`MATCH (a:IP),(b:IP)
WHERE a.addr = '%s' AND b.addr = '%s'
MERGE (a)-[r:PEER]->(b)
`, addr, peer.Addr)
}

然后把数据存储到图数据库即可,这里使用 Neo4j

for {
sql := <-c
fmt.Println(sql)
_, err = session.WriteTransaction(func(transaction neo4j.Transaction) (interface{}, error) {
result, err = transaction.Run(sql, map[string]interface{}{})
return nil, result.Err()
})
}

完整代码放在 https://github.com/AnyISalIn/infra_graph_collector/blob/master/pkg/server/server.go

Testing

笔者通过 Ansible 将 agent 下发到 50 台虚拟机中,最后在图数据库中展现效果如下

这里的 nebula 就是架构中的 Server 节点,所有的虚拟机都会和这台机器通信,所以所有虚拟机都会和这台节点有关联

image-20191228162211368

我们隐藏 nebula 节点后可以发现几个集群,左边是 Cachecloud 集群,中间是 Kubernetes 集群,右边是 Hadoop 集群,因为 Kubernetes 集群中所有节点一般只会和 Master 通信,所以会出现这种结构的关系,而 Hadoop 和 Cachecloud 集群每个节点都会相互通信,所以会出现这种互联的结构。

image-20191228162243332

Next Step ?

表面上我们已经完成了节点之间的关系图,其实只算完成了一半,实际上我们只对 IP 之间进行了关联,虚拟机会存在多个 IP 的情况,并且对与 Remote Port 我们也没有进行处理,并且在云上,有很多 PAAS 服务无法运行 Agent,也需要单独做处理,这块还有很多要做的工作。

Server 端目前只存储数据,并没有过滤和打标签,如果根据用户提供的一些数据给每个节点打上业务的标签能够分析出更有价值的关系图。

当然这只是笔者花了几个小时的简单尝试而已,如果要应用到生产还是要做很多改动的。