前言
目前docker資源監(jiān)控方案很多骇笔,各種高大上:Prometheus(配合xxx-exporter...)+Grafana或cAdvisor+InfluxDB+Grafana等等
但都只適合大型生產(chǎn)環(huán)境或使用了K8s情況下渠牲,對于我只希望在測試環(huán)境獲取docker宿主機(jī)及容器的資源使用情況的需求來看讳苦,確實(shí)小題大做纽甘。有沒有輕便的方案呢桥状?自己動(dòng)手,利用glances api加一些自定義代碼構(gòu)建自己需要的圖形化界面也是可以的绞幌,這種雖然土,但更實(shí)際则披。
為什么說這種是土但更實(shí)際
因?yàn)橹恢С謫蝹€(gè)宿主機(jī)及其容器共缕、不支持分布式及集群、沒有告警(當(dāng)然經(jīng)過二次開發(fā)支持也不是不可能收叶,但已經(jīng)有高大上系列了骄呼,沒必要再造輪子)
但在測試環(huán)境,主要的資源監(jiān)控需求來自于我需要在性能測試時(shí)獲取應(yīng)用所在容器及宿主機(jī)資源”隨著時(shí)間變動(dòng)“情況判没,glances已經(jīng)滿足資源獲取需求蜓萄,但是缺少grafana那種“時(shí)間×資源”視圖展示方式。
因?yàn)橹恍枰獪y試時(shí)查看資源曲線澄峰,因此不需要做持久化嫉沽。這里采用使用GlancesApi、配合websocket推送及前端畫圖(vue俏竞、echarts)畫出需要的折線圖绸硕。
Glances安裝
- glances是python編寫的一個(gè)用于資源監(jiān)控的可運(yùn)行模塊,最近的版本已經(jīng)開始支持docker及其容器魂毁。
它使用了python的psutil玻佩、docker等工具進(jìn)行資源情況獲取,使用了bottle作為簡單的web頁面展示席楚。 - 安裝要求
宿主機(jī)是CentOS系統(tǒng)咬崔,在宿主機(jī)上安裝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
運(yùn)行 glances,可以直接在控制臺展示宿主機(jī)烦秩、容器的各個(gè)資源利用情況(cpu垮斯、內(nèi)存、swap只祠、網(wǎng)絡(luò)):
image2019-7-15 10_2_22.png
運(yùn)行g(shù)lances -w 可以在瀏覽器端展示詳情的信息:
image2019-7-15 10_6_28.png
也提供api接口兜蠕,比如:
image2019-7-15 10_7_56.png
自定義WEB UI編寫
采用Go做Api調(diào)用者,進(jìn)行資源信息收集抛寝,前端用Vue框架的echarts組件進(jìn)行直線圖的繪制熊杨。
為什么不直接調(diào)用glances的api?
因?yàn)檫@種方式下如果多個(gè)瀏覽器訪問盗舰,將會并發(fā)產(chǎn)生很多個(gè)api調(diào)用猴凹,增加此Host機(jī)器的負(fù)載。
此處使用Go運(yùn)行一個(gè)背景進(jìn)程岭皂,每5s調(diào)用glances API獲取資源信息,利用websocket進(jìn)行廣播:不管多少個(gè)瀏覽器(websocket 連接)訪問該頁面沼头,只會5s產(chǎn)生1次API調(diào)用爷绘。
大致流程圖如下(退出邏輯沒有畫):
效果如下(第1版:Host的cpu书劝、mem、swap使用率土至、主要進(jìn)程收集购对,容器的mem使用量、cpu使用率收集)
代碼參考
后端代碼(go-iris)
// -------------這個(gè)在main文件中聲明并在app定義后進(jìn)行調(diào)用---------------------
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())
}
// --------下面是另一個(gè)文件-----------
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
}
})
}
前端繪制相關(guān)折線圖(Vue陶因,elementUI骡苞,iview,echarts)
<template>
<div>
<el-row>
<Drawer :closable="false" width="1000" v-model="showDrawer">
<h2 style="text-align:center">宿主機(jī)進(jìn)程列表(Top100)</h2>
<Table :columns="processCols" :data="processList" height="900"></Table>
</Drawer>
<el-col :span="12">
<b>選擇宿主機(jī):</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>查看宿主機(jī)進(jìn)程列表</Button>
</div>
</el-col>
</el-row>
<el-divider>HOST資源監(jiān)控</el-divider>
<div id="host_metrics" style="width:100%;height:500px;border:1px solid #ccc;padding:10px;"></div>
<el-divider>各個(gè)容器資源監(jiān)控</el-divider>
<el-row>
<el-col :span="6">
<h2>選擇需要檢查的容器(不超過6個(gè))</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 "./擴(kuò)展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: "進(jìn)程id", key: "pid" },
{ title: "父進(jìn)程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", "內(nèi)存使用占比\x25", "SWAP使用占比\x25"]
},
xAxis: { type: "category", data: [] },
yAxis: {},
series: [
{
name: "CPU使用占比\x25",
type: "line",
data: [],
lineStyle: { width: 3 },
label: { show: false, formatter: "{c}%" }
},
{
name: "內(nèi)存使用占比\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("請選擇目標(biāo)主機(jī)");
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: "內(nèi)存使用占比\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) {
// 面板打開時(shí)楷扬,不更新數(shù)據(jù)
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); //數(shù)據(jù)發(fā)送
},
changeContainer() {
if (this.curContainers.length > 6) {
this.$alert("為了數(shù)據(jù)清晰解幽,不要選擇超過6個(gè)容器");
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>
子模塊文件-擴(kuò)展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>