前言
目前docker资源监控方案很多,各种高大上:Prometheus(配合xxx-exporter...)+Grafana或cAdvisor+InfluxDB+Grafana等等
但都只适合大型生产环境或使用了K8s情况下,对于我只希望在测试环境获取docker宿主机及容器的资源使用情况的需求来看,确实小题大做。有没有轻便的方案呢?自己动手,利用glances api加一些自定义代码构建自己需要的图形化界面也是可以的,这种虽然土,但更实际。
为什么说这种是土但更实际
因为只支持单个宿主机及其容器、不支持分布式及集群、没有告警(当然经过二次开发支持也不是不可能,但已经有高大上系列了,没必要再造轮子)
但在测试环境,主要的资源监控需求来自于我需要在性能测试时获取应用所在容器及宿主机资源”随着时间变动“情况,glances已经满足资源获取需求,但是缺少grafana那种“时间×资源”视图展示方式。
因为只需要测试时查看资源曲线,因此不需要做持久化。这里采用使用GlancesApi、配合websocket推送及前端画图(vue、echarts)画出需要的折线图。
Glances安装
- glances是python编写的一个用于资源监控的可运行模块,最近的版本已经开始支持docker及其容器。
它使用了python的psutil、docker等工具进行资源情况获取,使用了bottle作为简单的web页面展示。 - 安装要求
宿主机是CentOS系统,在宿主机上安装python及pip - 安装方式
yum -y install gcc gcc-c++ autoconf pcre pcre-devel make automake
yum install python-devel.x86_64
pip install glances
pip install docker
pip install bottle
运行 glances,可以直接在控制台展示宿主机、容器的各个资源利用情况(cpu、内存、swap、网络):
image2019-7-15 10_2_22.png
运行glances -w 可以在浏览器端展示详情的信息:
image2019-7-15 10_6_28.png
也提供api接口,比如:
image2019-7-15 10_7_56.png
自定义WEB UI编写
采用Go做Api调用者,进行资源信息收集,前端用Vue框架的echarts组件进行直线图的绘制。
为什么不直接调用glances的api?
因为这种方式下如果多个浏览器访问,将会并发产生很多个api调用,增加此Host机器的负载。
此处使用Go运行一个背景进程,每5s调用glances API获取资源信息,利用websocket进行广播:不管多少个浏览器(websocket 连接)访问该页面,只会5s产生1次API调用。
大致流程图如下(退出逻辑没有画):
效果如下(第1版:Host的cpu、mem、swap使用率、主要进程收集,容器的mem使用量、cpu使用率收集)
1.gif
代码参考
后端代码(go-iris)
// -------------这个在main文件中声明并在app定义后进行调用---------------------
func setupWebsocket(app *iris.Application) {
// create our echo websocket server
ws := websocket.New(websocket.Config{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
})
ws.OnConnection(dockerMonitor.HandleGlances)
app.Get("/echo", ws.Handler())
}
// --------下面是另一个文件-----------
package dockerMonitor
import (
"github.com/kataras/iris/websocket"
"github.com/levigross/grequests"
"fmt"
"time"
"sync"
"strings"
)
type AllMetrics struct {
Now string `json:"now"`
Docker DockerMetrics `json:"docker"`
Network []interface{} `network`
QuickLook HostQuickLook `json:"quicklook"`
ProcessList []ProcessInfo `json:"processlist"`
}
type DockerMetrics struct {
Version interface{} `json:"version"`
Containers []ContainerMetrics `json:"containers"`
}
type ContainerMetrics struct {
Id string `json:"Id"`
Name string `json:"name"`
Key string `json:"key"`
Status string `json:"Status"`
Io map[string]interface{} `json:"io"`
Image []string `json:"Image"`
CpuPercent float64 `json:"cpu_percent"`
MemoryUsage float64 `json:"memory_usage"`
Network map[string]interface{} `json:"network"`
}
type HostQuickLook struct {
MemoryPercent float64 `json:"mem"`
SwapPercent float64 `json:"swap"`
CpuPercent float64 `json:"cpu"`
Percpu []PerCpuMetrics `json:"percpu"`
}
type PerCpuMetrics struct {
CpuNumber int `json:"cpu_number"`
SoftIrq float64 `json:"softirq"`
Iowait float64 `json:"iowait"`
GuestNice float64 `json:"guest_nice"`
System float64 `json:"system"`
Guest float64 `json:"guest"`
Idle float64 `json:"idle"`
User float64 `json:"user"`
Irq float64 `json:"irq"`
Total float64 `json:"total"`
Steal float64 `json:"steal"`
Nice float64 `json:"nice"`
}
type ProcessInfo struct {
Name string `json:"name"`
Username string `json:"username"`
Status string `json:"status"`
Ppid int `json:"ppid"`
Pid int `json:"pid"`
NumThreads int `json:"num_threads"`
CpuPercent float64 `json:"cpu_percent"`
MemoryPercent float64 `json:"memory_percent"`
Cmdline []string `json:"cmdline"`
}
// 全局变量
var wsMtx sync.Mutex
var wsStatus bool
var stopFlag chan int=make(chan int)
func glancesDocker(c websocket.Connection, hostport string) {
rOpt:=&grequests.RequestOptions{
RequestTimeout:30*time.Second,
}
// fmt.Println(wsStatus)
wsMtx.Lock()
if !wsStatus {
wsStatus=true
wsMtx.Unlock()
dockerloop:
for {
select {
case <-stopFlag:
fmt.Println("stop Emit msg...")
break dockerloop
default:
apiurl:=fmt.Sprintf("http://%s/api/3/all", hostport)
rsp, err := grequests.Get(apiurl, rOpt)
if err != nil {
fmt.Printf("%v\n", err)
break dockerloop
}
defer rsp.Close()
allMetrics:= AllMetrics{}
rsp.JSON(&allMetrics)
c.To(websocket.All).Emit("glances", allMetrics)
time.Sleep(5 * time.Second)
}
}
wsMtx.Lock()
wsStatus=false
wsMtx.Unlock()
}else{
wsMtx.Unlock()
}
}
func HandleGlances(c websocket.Connection){
// fmt.Println(stopFlag)
c.On("glances",func(msg string) { // "192.168.60.91:61208"
hostport:=""
msgSlc:=strings.Split(msg,";")
if len(msgSlc)==2 && strings.TrimSpace(msgSlc[1])!=""{
hostport=msgSlc[1]
}
go glancesDocker(c,hostport)
if msg=="STOP"{
stopFlag<-1
}
})
}
前端绘制相关折线图(Vue,elementUI,iview,echarts)
<template>
<div>
<el-row>
<Drawer :closable="false" width="1000" v-model="showDrawer">
<h2 style="text-align:center">宿主机进程列表(Top100)</h2>
<Table :columns="processCols" :data="processList" height="900"></Table>
</Drawer>
<el-col :span="12">
<b>选择宿主机:</b>
<el-radio-group v-model="HostAddr" @change="changeHostAddr()" >
<el-radio :label="`192.168.60.91:61208`" border>192.168.60.91:61208</el-radio>
<el-radio :label="`192.168.60.110:61208`" border>192.168.60.110:61208</el-radio>
</el-radio-group>
<ButtonGroup shape="circle" style="margin-left:100px">
<Button type="primary" @click="openWebsocket()">
<Icon type="ios-analytics"/>请求资源
</Button>
<Button type="primary" @click="closeWebsocket()">
<Icon type="ios-square"/> 停止获取
</Button>
</ButtonGroup>
</el-col>
<el-col :span="12" >
<div style="float:right;margin: 9px">
<Button type="info" @click="showDrawer=true" ghost>查看宿主机进程列表</Button>
</div>
</el-col>
</el-row>
<el-divider>HOST资源监控</el-divider>
<div id="host_metrics" style="width:100%;height:500px;border:1px solid #ccc;padding:10px;"></div>
<el-divider>各个容器资源监控</el-divider>
<el-row>
<el-col :span="6">
<h2>选择需要检查的容器(不超过6个)</h2>
<el-checkbox-group v-model="curContainers" @change="changeContainer()">
<el-checkbox v-for="(contr,i) in containers" :key="i" :label="contr" ></el-checkbox>
</el-checkbox-group>
</el-col>
<el-col :span="18">
<div id="docker_cpu" style="width:100%;height:500px;border:1px solid #ccc;padding:10px;"></div>
<div id="docker_mem" style="width:100%;height:500px;border:1px solid #ccc;padding:10px;"></div>
</el-col>
</el-row>
</div>
</template>
<script>
import echarts from "echarts";
import { constants } from "fs";
import { setInterval, setTimeout } from "timers";
import expandRow from "./扩展Glances.vue";
export default {
name: "basecharts",
components: {},
data() {
return {
showDrawer: false,
transFlag: false,
HostAddr: null,
hostMetrics: null,
dockerCpuMetrics: null,
dockerMemMetrics: null,
containers: [],
curContainers: [],
dockersTime: [],
dockersCpu: [],
dockersMem: [],
host_time: [],
host_cpu_pct: [],
host_mem_pct: [],
host_swap_pct: [],
processCols: [
{
type: "expand",
width: 30,
render: (h, params) => {
return h(expandRow, {
props: {
row: params.row
}
});
}
},
{ title: "Name", key: "name", sortable: true },
{ title: "Username", key: "username", sortable: true },
{ title: "Status", key: "status", sortable: true },
{ title: "进程id", key: "pid" },
{ title: "父进程id", key: "ppid", sortable: true },
{ title: "NumThreads", key: "num_threads" },
{ title: "Cpu(%)", key: "cpu_percent", sortable: true },
{
title: "Memory(%)",
key: "memory_percent",
sortable: true,
width: 130
}
],
processList: []
};
},
mounted() {
this.initWebSocketHost();
this.hostMetrics = echarts.init(document.getElementById("host_metrics"));
this.hostMetrics.setOption({
//初始化
title: { text: "Host的资源使用率(%)" },
tooltip: { trigger: "axis" },
legend: {
data: ["CPU使用占比\x25", "内存使用占比\x25", "SWAP使用占比\x25"]
},
xAxis: { type: "category", data: [] },
yAxis: {},
series: [
{
name: "CPU使用占比\x25",
type: "line",
data: [],
lineStyle: { width: 3 },
label: { show: false, formatter: "{c}%" }
},
{
name: "内存使用占比\x25",
type: "line",
data: [],
lineStyle: { width: 3 },
label: { show: false, formatter: "{c}%" }
},
{
name: "SWAP使用占比\x25",
type: "line",
data: [],
lineStyle: {
type: "solid", //solid,dashed
width: 2,
color: "rgb(17, 93, 232)"
},
label: {
show: false,
formatter: "{c}%",
color: "rgb(17, 93, 232)",
position: "bottom"
}
}
]
});
this.dockerCpuMetrics = echarts.init(document.getElementById("docker_cpu"));
this.dockerCpuMetrics.setOption({
//初始化
title: { text: "docker容器的CPU使用率(%)" },
tooltip: { trigger: "axis" },
legend: {
right: "right",
orient: "vertical",
data: []
},
xAxis: {
type: "category",
data: []
},
yAxis: {},
series: []
});
this.dockerMemMetrics = echarts.init(document.getElementById("docker_mem"));
this.dockerMemMetrics.setOption({
//初始化
title: { text: "docker容器的Mem量(MiB)" },
tooltip: { trigger: "axis" },
legend: {
right: "right",
orient: "vertical",
data: []
},
xAxis: {
type: "category",
data: []
},
yAxis: {},
series: []
});
let othis = this;
window.addEventListener("resize", function() {
othis.hostMetrics.resize();
othis.dockerCpuMetrics.resize();
othis.dockerMemMetrics.resize();
});
},
methods: {
openWebsocket() {
if (!this.HostAddr) {
this.$alert("请选择目标主机");
return;
}
this.WebSocketHostSend(
"iris-websocket-message:glances;0;START;" + this.HostAddr
);
this.transFlag = true;
},
closeWebsocket() {
this.WebSocketHostSend("iris-websocket-message:glances;0;STOP");
this.transFlag = false;
},
initWebSocketHost() {
//初始化weosocket
var serverhost = document.location.host;
const wsuri = `ws://${serverhost}/echo`;
this.websockHost = new WebSocket(wsuri);
this.websockHost.onopen = this.onopenWebSocketHost;
this.websockHost.onerror = this.onerrorWebSocketHost;
this.websockHost.onclose = this.oncloseWebSocketHost;
this.websockHost.onmessage = this.onmessageWebSocketHost;
},
onopenWebSocketHost() {
console.log("open iris-websocket...");
},
oncloseWebSocketHost() {
console.log("close iris-websocket...");
},
onerrorWebSocketHost() {
this.initWebSocketHost(); //连接建立失败重连
},
onmessageWebSocketHost(e) {
var jsonD = e.data.replace(/iris-websocket-message:glances;\d+;/, "");
var jsonData = JSON.parse(jsonD);
var t = new Date(jsonData.now);
var cpuPercent = jsonData.quicklook.cpu;
var memPercent = jsonData.quicklook.mem;
var swapPercent = jsonData.quicklook.swap;
this.host_time.push(
t.getHours() + ":" + t.getMinutes() + ":" + t.getSeconds()
);
this.host_cpu_pct.push(cpuPercent);
this.host_mem_pct.push(memPercent);
this.host_swap_pct.push(swapPercent);
this.hostMetrics.setOption({
series: [
{ name: "CPU使用占比\x25", data: this.host_cpu_pct },
{ name: "内存使用占比\x25", data: this.host_mem_pct },
{ name: "SWAP使用占比\x25", data: this.host_swap_pct }
],
xAxis: { data: this.host_time }
});
var containerList = jsonData.docker.containers;
this.containers = [];
for (var ct of containerList) {
this.containers.push(ct.name);
}
var dockersCpuData = [];
var dockersMemData = [];
for (var i in this.curContainers) {
for (var ct of containerList) {
if (ct.name == this.curContainers[i]) {
if (!this.dockersCpu[i]) {
this.dockersCpu[i] = [];
}
this.dockersCpu[i].push(ct.cpu_percent);
if (!this.dockersMem[i]) {
this.dockersMem[i] = [];
}
this.dockersMem[i].push(ct.memory_usage / 1024.0 / 1024.0);
}
}
dockersCpuData.push({
name: this.curContainers[i],
data: this.dockersCpu[i],
type: "line"
});
dockersMemData.push({
name: this.curContainers[i],
data: this.dockersMem[i],
type: "line"
});
}
if (this.curContainers.length > 0) {
this.dockersTime.push(
t.getHours() + ":" + t.getMinutes() + ":" + t.getSeconds()
);
}
this.dockerCpuMetrics.setOption({
legend: { data: this.curContainers },
series: dockersCpuData,
xAxis: { data: this.dockersTime }
});
this.dockerMemMetrics.setOption({
legend: { data: this.curContainers },
series: dockersMemData,
xAxis: { data: this.dockersTime }
});
if (!this.showDrawer) {
// 面板打开时,不更新数据
var oProcesses = jsonData.processlist.slice(0, 100);
var tmpP = {};
this.processList = [];
for (var op of oProcesses) {
if (!op.cmdline || op.cmdline.length === 0) {
continue;
}
tmpP = {};
tmpP.name = op.name;
tmpP.username = op.username;
tmpP.status = op.status;
tmpP.ppid = op.ppid;
tmpP.pid = op.pid;
tmpP.num_threads = op.num_threads;
tmpP.cpu_percent = op.cpu_percent;
tmpP.memory_percent = parseInt(op.memory_percent * 100) / 100.0;
tmpP.cmd = op.cmdline[0];
tmpP.cmdargs = op.cmdline.slice(1).join(" ");
this.processList.push(tmpP);
}
}
},
WebSocketHostSend(Data) {
this.websockHost.send(Data); //数据发送
},
changeContainer() {
if (this.curContainers.length > 6) {
this.$alert("为了数据清晰,不要选择超过6个容器");
this.curContainers = this.curContainers.slice(0, -1);
}
this.dockersTime = [];
this.dockersCpu = [];
this.dockersMem = [];
},
cleanDockerMetrics() {
this.dockerCpuMetrics.setOption({
legend: { data: [] },
series: [],
xAxis: { data: [] }
});
this.dockerMemMetrics.setOption({
legend: { data: [] },
series: [],
xAxis: { data: [] }
});
},
changeHostAddr() {
this.closeWebsocket();
this.cleanDockerMetrics();
this.containers = [];
this.curContainers = [];
this.dockersTime = [];
this.dockersCpu = [];
this.dockersMem = [];
this.host_time = [];
this.host_cpu_pct = [];
this.host_mem_pct = [];
this.host_swap_pct = [];
this.processList = [];
}
}
};
</script>
<style scoped>
.content-title {
clear: both;
font-weight: 400;
line-height: 50px;
margin: 10px 0;
font-size: 22px;
color: #1f2f3d;
}
</style>
子模块文件-扩展Glances.vue
<style scoped>
.expand-row{
margin-bottom: 2px;
}
</style>
<template>
<div>
<el-row class="expand-row">
{{ row.cmd }}
</el-row>
<el-row>
{{ row.cmdargs }}
</el-row>
</div>
</template>
<script>
export default {
name: "expandRow",
props: {
row: Object
}
};
</script>
网友评论