基于lua-resty-websocket库实现系统监控

一、实现原理

通过lua脚本执行linux的系统监控命令(shell命令),并将执行结果得到的数据通过websocket协议传递给客户端,供客户端展示

二、实现技术

  • 前端展示:html5的websocket技术、highcharts图表展示库
  • 后端实现:lua-resty-websocket库、io.popen函数

三、知识准备与拓展

(1)html5中的websocket协议
属性:
属性名 含义
readyState websocket的状态
0:表示未连接
1:表示准备连接
2:连接成功(连接已打开)
3:连接关闭或连接未打开
bufferedAmount 表示使用send方法发送的而且还在队列中的数据的字节数
事件:
事件名 事件含义
open 打开连接事件
message 收到消息事件
close 关闭连接事件
error 连接发生错误事件
方法:
方法名 作用
send 发送数据
close 关闭连接
使用:
  1. var ws = new WebSocket("ws://localhost:8000/vmstats");
  2. ws.onopen = function(){
  3. //在打开连接时发送数据
  4. ws.send("hello");
  5. console.log('open');
  6. };
  7. ws.onmessage = function (e) {
  8. //接收到消息数据
  9. console.log(e);
  10. };
  11. ws.onclose = function(){
  12. //连接关闭
  13. console.log('close');
  14. };

更多内容可以查看:http://www.tutorialspoint.com/html5/html5_websocket.htm

(2)vmstat
  • 功能:Linux系统监控命令,获取当前系统的内存、进程、CPU、IO等状态

  • 用法:vmstat [interval] [times]

  • 参数:第一个参数interval表示间隔时间,第二个参数times表示采集次数
  • 返回数据:

    • r: 运行进程数,

    • b: 阻塞进程数

    • swpd: 已使用虚拟内存,

    • free: 空闲物理内存,

    • buff: 已使用缓存,

    • cache: 缓存大小

    • si: 从磁盘中读入的虚拟内存,

    • so: 从虚拟内存中写入磁盘大小

    • bi: 从块设备接收的块大小,

    • bo: 从块设备改善的块大小

    • ir: 每秒中断次数,

    • cs: 上下文切换次数

    • us: 用户CPU时间,

    • sy: 系统CPU时间,

    • id: 空闲CPU时间,

    • wa: 等待IO CPU时间

(3)io.popen
  • 作用:使用一个单独的进程执行某个程序,并返回这个程序执行的文件句柄
  • 用法:io.popen(program[, mode])
  • 参数:第一个参数是程序名(可以是shell脚本命令),第二个参数是模式(读写)
  • 返回数据:执行程序的返回结果
  • 说明:用它可以执行shell命令,类似于文件读写(因为os.execute无法获取执行命令的结果),它返回一个句柄,所以还得通过读文件来获取内容

四、实现细节

(1)项目目录

(2)配置部分

设置lua库文件搜索目录

  1. lua_package_path '/data/web/vmstats/lib/?.lua;;';
  2. server {
  3. listen 8000;
  4. #根据自身情况做调整
  5. set $web_root /data/web/vmstats;
  6. location / {
  7. root $web_root/public;
  8. }
  9. location =/vmstats {
  10. default_type text/html;
  11. content_by_lua_file $web_root/server.lua;
  12. }
  13. }
(3)后端部分
  1. local server = require "resty.websocket.server"
  2. local shell = require 'resty.shell'
  3. local json = require 'cjson'
  4. local wb, err = server:new{
  5. timeout = 5000, -- 单位是毫秒(连接有效时间)
  6. max_payload_len = 65535,
  7. }
  8. if not wb then
  9. ngx.log(ngx.ERR, "failed to new websocket: ", err)
  10. return ngx.exit(444)
  11. end
  12. local data, typ, err = wb:recv_frame()
  13. if not data then
  14. ngx.log(ngx.ERR, "failed to receive a frame: ", err)
  15. return ngx.exit(444)
  16. end
  17. if typ == "close" then
  18. -- send a close frame back:
  19. local bytes, err = wb:send_close(1000, "enough, enough!")
  20. if not bytes then
  21. ngx.log(ngx.ERR, "failed to send the close frame: ", err)
  22. return
  23. end
  24. local code = err
  25. ngx.log(ngx.INFO, "closing with status code ", code, " and message ", data)
  26. return
  27. end
  28. if typ == "ping" then
  29. -- send a pong frame back:
  30. local bytes, err = wb:send_pong(data)
  31. if not bytes then
  32. ngx.log(ngx.ERR, "failed to send frame: ", err)
  33. return
  34. end
  35. elseif typ == "pong" then
  36. -- just discard the incoming pong frame
  37. else
  38. ngx.log(ngx.INFO, "received a frame of type ", typ, " and payload ", data)
  39. end
  40. local res = shell.vmstat() --执行shell脚本中的vmstat(具体见lib/resty/shell.lua文件,其实就是读取io.popen("vmstat"))的执行结果
  41. if res then
  42. res.time = ngx.time()
  43. local data = json.encode(res)
  44. bytes, err = wb:send_text(data)
  45. if not bytes then
  46. ngx.log(ngx.ERR, "failed to send a text frame: ", err)
  47. return ngx.exit(444)
  48. end
  49. end
  50. local bytes, err = wb:send_close(1000, "enough, enough!")
  51. if not bytes then
  52. ngx.log(ngx.ERR, "failed to send the close frame: ", err)
  53. return
  54. end
(4)前端部分
html部分:
  1. <!doctype html>
  2. <html lang="">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="description" content="">
  6. <meta name="viewport" content="width=device-width, initial-scale=1">
  7. <meta http-equiv="x-ua-compatible" content="ie=edge">
  8. <title>系统负载统计</title>
  9. <link rel="stylesheet" href="/static/js/bootstrap/css/bootstrap.min.css">
  10. </head>
  11. <body>
  12. <div class="col-xs-11">
  13. <div id="Processes-canvas">
  14. </div>
  15. <div id="Memory-canvas">
  16. </div>
  17. <div id="Swap-canvas">
  18. </div>
  19. <div id="IO-canvas">
  20. </div>
  21. <div id="System-canvas">
  22. </div>
  23. <div id="CPU-canvas">
  24. </div>
  25. </div>
  26. <script type="text/javascript" src="/static/js/jquery.js"></script>
  27. <script type="text/javascript" src="/static/js/bootstrap/js/bootstrap.min.js"></script>
  28. <script type="text/javascript" src="/static/js/highcharts/highcharts.js"></script>
  29. <script type="text/javascript" src="/static/js/reconnect.js"></script>
  30. <script type="text/javascript" src="/static/js/stats.js"></script>
  31. </body>
  32. </html>
js部分:stats.js
  1. var descriptions = {
  2. 'Processes': {
  3. 'title' : '进程',
  4. 'items' : {
  5. 'r': '运行进程数',
  6. 'b': '阻塞进程数'
  7. },
  8. 'unit':'个'
  9. },
  10. 'Memory': {
  11. 'title' : '内存',
  12. 'items' : {
  13. 'swpd': '已使用虚拟内存',
  14. 'free': '空闲物理内存',
  15. 'buff': '已使用缓存',
  16. 'cache': '缓存大小'
  17. },
  18. 'unit':'B'
  19. },
  20. 'Swap': {
  21. 'title':'交换内存',
  22. 'items':{
  23. 'si': '从磁盘中读入的虚拟内存',
  24. 'so': '从虚拟内存中写入磁盘大小'
  25. },
  26. 'unit':'B'
  27. },
  28. 'IO': {
  29. 'title':'IO开销',
  30. 'items':{
  31. 'bi': '从块设备接收的块大小',
  32. 'bo': '从块设备改善的块大小'
  33. },
  34. 'unit':'个'
  35. },
  36. 'System': {
  37. 'title':'系统',
  38. 'items':{
  39. 'ir': '每秒中断次数',
  40. 'cs': '上下文切换次数'
  41. },
  42. 'unit':'次'
  43. },
  44. 'CPU': {
  45. 'title':'CPU',
  46. 'items':{
  47. 'us': '用户CPU时间',
  48. 'sy': '系统CPU时间',
  49. 'id': '空闲CPU时间',
  50. 'wa': '等待IO CPU时间'
  51. },
  52. 'unit':'秒'
  53. }
  54. }
  55. var num = 0;
  56. //websocket连接
  57. function streamStats() {
  58. var ws = new ReconnectingWebSocket('ws://' + location.host + '/vmstats');
  59. ws.onopen = function() {
  60. console.log('connect');
  61. ws.send('hello');
  62. };
  63. ws.onclose = function() {
  64. console.log('disconnect');
  65. };
  66. ws.onmessage = function(e) {
  67. receiveStats(JSON.parse(e.data));
  68. };
  69. }
  70. //实始化图表
  71. function initCharts() {
  72. for(var opt in descriptions) {
  73. var dataList = [];
  74. for (var v in descriptions[opt].items) {
  75. dataList.push({
  76. name: descriptions[opt]['items'][v],
  77. data: [
  78. [(new Date()).getTime(),0]
  79. ]
  80. });
  81. }
  82. Highcharts.setOptions({
  83. global: {
  84. useUTC: false
  85. }
  86. });
  87. $('#'+opt+'-canvas').highcharts({
  88. chart: {
  89. type: 'spline',
  90. animation: Highcharts.svg,
  91. marginRight: 10
  92. },
  93. title: {
  94. text: descriptions[opt].title
  95. },
  96. credits : {
  97. enabled:false
  98. },
  99. xAxis: {
  100. maxPadding : 0.05,
  101. minPadding : 0.05,
  102. type: 'datetime',
  103. tickWidth:5
  104. },
  105. yAxis: {
  106. title: {
  107. text: descriptions[opt].unit
  108. },
  109. plotLines: [{
  110. value: 0,
  111. width: 1,
  112. color: '#808080'
  113. }]
  114. },
  115. tooltip: {
  116. formatter: function() {
  117. return '<b>'+ this.series.name +'</b>('+num+')<br/>'+
  118. Highcharts.dateFormat('%H:%M:%S', this.x) +'<br/>'+
  119. Highcharts.numberFormat(this.y, 2);
  120. }
  121. },
  122. legend: {
  123. enabled: true
  124. },
  125. exporting: {
  126. enabled: false
  127. },
  128. series: dataList
  129. });
  130. }
  131. }
  132. //接收数据
  133. function receiveStats(stats) {
  134. console.log(stats);
  135. var time = stats.time*1000;
  136. for(var opt in descriptions) {
  137. var chart = $('#'+opt+'-canvas').highcharts();
  138. var i = 0;
  139. for (var v in descriptions[opt].items) {
  140. chart.series[i].addPoint([time, parseInt(stats[v])], true, (num>120?true:false));
  141. i++;
  142. }
  143. }
  144. num++;
  145. }
  146. $(function() {
  147. initCharts();
  148. streamStats();
  149. });

项目源码:https://github.com/shixinke/openresty-practices/tree/master/websocket

项目预览图: