AutoCheckNodeStatus.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use App\Components\ServerChan;
  5. use App\Http\Models\Config;
  6. use App\Http\Models\EmailLog;
  7. use App\Http\Models\SsNode;
  8. use App\Http\Models\SsNodeInfo;
  9. use App\Mail\nodeCrashWarning;
  10. use Cache;
  11. use Mail;
  12. use Log;
  13. class AutoCheckNodeStatus extends Command
  14. {
  15. protected $signature = 'autoCheckNodeStatus';
  16. protected $description = '自动检测节点状态';
  17. protected static $config;
  18. public function __construct()
  19. {
  20. parent::__construct();
  21. self::$config = $this->systemConfig();
  22. }
  23. public function handle()
  24. {
  25. $jobStartTime = microtime(true);
  26. // 监测节点状态
  27. $this->checkNodeStatus();
  28. $jobEndTime = microtime(true);
  29. $jobUsedTime = round(($jobEndTime - $jobStartTime), 4);
  30. Log::info('执行定时任务【' . $this->description . '】,耗时' . $jobUsedTime . '秒');
  31. }
  32. // 监测节点状态
  33. private function checkNodeStatus()
  34. {
  35. $title = "节点异常警告";
  36. $nodeList = SsNode::query()->where('status', 1)->get();
  37. foreach ($nodeList as $node) {
  38. // TCP检测
  39. $tcpCheck = $this->tcpCheck($node->ip, $node->ssh_port);
  40. if (false !== $tcpCheck && $tcpCheck) {
  41. $content = '节点无异常';
  42. if ($tcpCheck === 1) {
  43. $content = "节点**{$node->name}【{$node->ip}】**异常:**服务器宕机**";
  44. } else if ($tcpCheck === 2) {
  45. $content = "节点**{$node->name}【{$node->ip}】**异常:**海外不通**";
  46. } else if ($tcpCheck === 3) {
  47. $content = "节点**{$node->name}【{$node->ip}】**异常:**TCP阻断**";
  48. }
  49. // 通知管理员
  50. $this->notifyMaster($title, $content, $node->name, $node->server);
  51. }
  52. // 10分钟内无节点负载信息且TCP检测认为不是宕机则认为是SSR(R)后端炸了
  53. $node_info = SsNodeInfo::query()->where('node_id', $node->id)->where('log_time', '>=', strtotime("-10 minutes"))->orderBy('id', 'desc')->first();
  54. if ($tcpCheck !== 1 && (empty($node_info) || empty($node_info->load))) {
  55. // 通知管理员
  56. $this->notifyMaster($title, "节点**{$node->name}【{$node->ip}】**异常:**心跳异常**", $node->name, $node->server);
  57. }
  58. }
  59. }
  60. // 获取check-host的节点列表
  61. private function getCheckHostServers()
  62. {
  63. $cacheKey = 'check_host_servers';
  64. if (Cache::has($cacheKey)) {
  65. return Cache::get($cacheKey);
  66. }
  67. $servers = $this->curlRequest("https://check-host.net/servers");
  68. $servers = json_decode($servers, JSON_OBJECT_AS_ARRAY);
  69. if (!$servers) {
  70. // 删除这个缓存,防止异常
  71. Cache::forget($cacheKey);
  72. return [];
  73. }
  74. // 每7天更新一次check-host的节点列表
  75. Cache::put($cacheKey, $servers['servers'], 10080);
  76. return $servers;
  77. }
  78. // 随机获取一个check-host的海外检测节点
  79. private function getRandomServer()
  80. {
  81. $servers = $this->getCheckHostServers();
  82. if (!$servers) {
  83. return 'us1.node.check-host.net'; // 没有数据时返回美国节点1,防止异常
  84. }
  85. if ($servers) {
  86. //$servers = array_except($servers, 'cn1.node.check-host.net');
  87. //$randServer = array_rand($servers);
  88. $offset = array_search('cn1.node.check-host.net', $servers);
  89. array_slice($servers, $offset, 1); // 剔除值
  90. return array_rand($servers); // 取出一个随机值
  91. }
  92. }
  93. // TCP检测
  94. private function tcpCheck($ip, $sshPort)
  95. {
  96. try {
  97. $overseasNode = $this->getRandomServer();
  98. $result = $this->curlRequest("https://check-host.net/check-tcp?host={$ip}:{$sshPort}&node=cn1.node.check-host.net&node=" . $overseasNode);
  99. $result = json_decode($result, JSON_OBJECT_AS_ARRAY);
  100. if ($result['ok'] != 1) {
  101. throw new \Exception("节点探测失败");
  102. }
  103. // 天若有情天亦老,我为长者续一秒
  104. sleep(1);
  105. // 拿到结果
  106. $result = $this->curlRequest("https://check-host.net/check-result/" . $result['request_id']);
  107. $result = json_decode($result, JSON_OBJECT_AS_ARRAY);
  108. if (!$result['cn1.node.check-host.net'] && !$result[$overseasNode]) {
  109. return 1; // 中美都不通,服务器宕机
  110. } else if ($result['cn1.node.check-host.net'] && !$result[$overseasNode]) {
  111. return 2; // 中通美不通,无法出国,可能是安全组策略限制(例如:阿里云、腾讯云)
  112. } else if (!$result['cn1.node.check-host.net'] && $result[$overseasNode]) {
  113. return 3; // 美通中不通,说明被墙进行TCP阻断
  114. } else {
  115. return 0; // 正常
  116. }
  117. } catch (\Exception $e) {
  118. Log::error('节点监测请求失败:' . $e);
  119. return false;
  120. }
  121. }
  122. /**
  123. * 通知管理员
  124. *
  125. * @param string $title 消息标题
  126. * @param string $content 消息内容
  127. * @param string $nodeName 节点名称
  128. * @param string $nodeServer 节点域名
  129. */
  130. private function notifyMaster($title, $content, $nodeName, $nodeServer)
  131. {
  132. $this->notifyMasterByEmail($title, $content, $nodeName, $nodeServer);
  133. $this->notifyMasterByServerchan($title, $content);
  134. }
  135. /**
  136. * 发邮件通知管理员
  137. *
  138. * @param string $title 消息标题
  139. * @param string $content 消息内容
  140. * @param string $nodeName 节点名称
  141. * @param string $nodeServer 节点域名
  142. */
  143. private function notifyMasterByEmail($title, $content, $nodeName, $nodeServer)
  144. {
  145. if (self::$config['is_node_crash_warning'] && self::$config['crash_warning_email']) {
  146. try {
  147. Mail::to(self::$config['crash_warning_email'])->send(new nodeCrashWarning(self::$config['website_name'], $nodeName, $nodeServer));
  148. $this->addEmailLog(1, $title, $content);
  149. } catch (\Exception $e) {
  150. $this->addEmailLog(1, $title, $content, 0, $e->getMessage());
  151. }
  152. }
  153. }
  154. /**
  155. * 通过ServerChan发微信消息提醒管理员
  156. *
  157. * @param string $title 消息标题
  158. * @param string $content 消息内容
  159. */
  160. private function notifyMasterByServerchan($title, $content)
  161. {
  162. if (self::$config['is_server_chan'] && self::$config['server_chan_key']) {
  163. $serverChan = new ServerChan();
  164. $serverChan->send($title, $content);
  165. }
  166. }
  167. /**
  168. * 添加邮件发送日志
  169. *
  170. * @param int $userId 接收者用户ID
  171. * @param string $title 标题
  172. * @param string $content 内容
  173. * @param int $status 投递状态
  174. * @param string $error 投递失败时记录的异常信息
  175. */
  176. private function addEmailLog($userId, $title, $content, $status = 1, $error = '')
  177. {
  178. $emailLogObj = new EmailLog();
  179. $emailLogObj->user_id = $userId;
  180. $emailLogObj->title = $title;
  181. $emailLogObj->content = $content;
  182. $emailLogObj->status = $status;
  183. $emailLogObj->error = $error;
  184. $emailLogObj->created_at = date('Y-m-d H:i:s');
  185. $emailLogObj->save();
  186. }
  187. // 系统配置
  188. private function systemConfig()
  189. {
  190. $config = Config::query()->get();
  191. $data = [];
  192. foreach ($config as $vo) {
  193. $data[$vo->name] = $vo->value;
  194. }
  195. return $data;
  196. }
  197. /**
  198. * 发起一个CURL请求
  199. *
  200. * @param string $url 请求地址
  201. * @param array $data POST数据,留空则为GET
  202. *
  203. * @return mixed
  204. */
  205. private function curlRequest($url, $data = [])
  206. {
  207. $data = json_encode($data, JSON_UNESCAPED_UNICODE);
  208. $ch = curl_init();
  209. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  210. curl_setopt($ch, CURLOPT_TIMEOUT, 500);
  211. // 为保证第三方服务器与微信服务器之间数据传输的安全性,所有微信接口采用https方式调用,必须使用下面2行代码打开ssl安全校验。
  212. // 如果在部署过程中代码在此处验证失败,请到 http://curl.haxx.se/ca/cacert.pem 下载新的证书判别文件。
  213. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  214. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  215. curl_setopt($ch, CURLOPT_URL, $url);
  216. curl_setopt($ch, CURLOPT_HTTPHEADER, [
  217. 'Content-Type: application/json',
  218. 'Content-Length: ' . strlen($data)
  219. ]);
  220. // 如果data有数据,则用POST请求
  221. if ($data) {
  222. curl_setopt($ch, CURLOPT_POST, 1);
  223. curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
  224. }
  225. $result = curl_exec($ch);
  226. curl_close($ch);
  227. return $result;
  228. }
  229. }