服务热重启

由于 swoole 常驻内存的特性,修改文件后需要重启worker进程才能将被修改的文件重新载入内存中,我们可以自定义Process的方式实现文件变动自动进行服务重载

热重载进程

新建文件 App/Process/HotReload.php 并添加如下内容,也可以放在其他位置,请对应命名空间

  1. <?php
  2. /**
  3. * Created by PhpStorm.
  4. * User: evalor
  5. * Date: 2018-11-26
  6. * Time: 23:18
  7. */
  8. namespace App\Process;
  9. use EasySwoole\Component\Process\AbstractProcess;
  10. use EasySwoole\EasySwoole\ServerManager;
  11. use EasySwoole\Utility\File;
  12. use Swoole\Process;
  13. use Swoole\Table;
  14. use Swoole\Timer;
  15. /**
  16. * 暴力热重载
  17. * Class HotReload
  18. * @package App\Process
  19. */
  20. class HotReload extends AbstractProcess
  21. {
  22. /** @var \swoole_table $table */
  23. protected $table;
  24. protected $isReady = false;
  25. protected $monitorDir; // 需要监控的目录
  26. protected $monitorExt; // 需要监控的后缀
  27. /**
  28. * 启动定时器进行循环扫描
  29. */
  30. public function run($arg)
  31. {
  32. // 此处指定需要监视的目录 建议只监视App目录下的文件变更
  33. $this->monitorDir = !empty($arg['monitorDir']) ? $arg['monitorDir'] : EASYSWOOLE_ROOT . '/App';
  34. // 指定需要监控的扩展名 不属于指定类型的的文件 无视变更 不重启
  35. $this->monitorExt = !empty($arg['monitorExt']) && is_array($arg['monitorExt']) ? $arg['monitorExt'] : ['php'];
  36. if (extension_loaded('inotify') && empty($arg['disableInotify'])) {
  37. // 扩展可用 优先使用扩展进行处理
  38. $this->registerInotifyEvent();
  39. echo "server hot reload start : use inotify\n";
  40. } else {
  41. // 扩展不可用时 进行暴力扫描
  42. $this->table = new Table(512);
  43. $this->table->column('mtime', Table::TYPE_INT, 4);
  44. $this->table->create();
  45. $this->runComparison();
  46. Timer::tick(1000, function () {
  47. $this->runComparison();
  48. });
  49. echo "server hot reload start : use timer tick comparison\n";
  50. }
  51. }
  52. /**
  53. * 扫描文件变更
  54. */
  55. private function runComparison()
  56. {
  57. $startTime = microtime(true);
  58. $doReload = false;
  59. $dirIterator = new \RecursiveDirectoryIterator($this->monitorDir);
  60. $iterator = new \RecursiveIteratorIterator($dirIterator);
  61. $inodeList = array();
  62. // 迭代目录全部文件进行检查
  63. foreach ($iterator as $file) {
  64. /** @var \SplFileInfo $file */
  65. $ext = $file->getExtension();
  66. if (!in_array($ext, $this->monitorExt)) {
  67. continue; // 只检查指定类型
  68. } else {
  69. // 由于修改文件名称 并不需要重新载入 可以基于inode进行监控
  70. $inode = $file->getInode();
  71. $mtime = $file->getMTime();
  72. array_push($inodeList, $inode);
  73. if (!$this->table->exist($inode)) {
  74. // 新建文件或修改文件 变更了inode
  75. $this->table->set($inode, ['mtime' => $mtime]);
  76. $doReload = true;
  77. } else {
  78. // 修改文件 但未发生inode变更
  79. $oldTime = $this->table->get($inode)['mtime'];
  80. if ($oldTime != $mtime) {
  81. $this->table->set($inode, ['mtime' => $mtime]);
  82. $doReload = true;
  83. }
  84. }
  85. }
  86. }
  87. foreach ($this->table as $inode => $value) {
  88. // 迭代table寻找需要删除的inode
  89. if (!in_array(intval($inode), $inodeList)) {
  90. $this->table->del($inode);
  91. $doReload = true;
  92. }
  93. }
  94. if ($doReload) {
  95. $count = $this->table->count();
  96. $time = date('Y-m-d H:i:s');
  97. $usage = round(microtime(true) - $startTime, 3);
  98. if (!$this->isReady == false) {
  99. // 监测到需要进行热重启
  100. echo "severReload at {$time} use : {$usage} s total: {$count} files\n";
  101. ServerManager::getInstance()->getSwooleServer()->reload();
  102. } else {
  103. // 首次扫描不需要进行重启操作
  104. echo "hot reload ready at {$time} use : {$usage} s total: {$count} files\n";
  105. $this->isReady = true;
  106. }
  107. }
  108. }
  109. /**
  110. * 注册Inotify监听事件
  111. */
  112. private function registerInotifyEvent()
  113. {
  114. // 因为进程独立 且当前是自定义进程 全局变量只有该进程使用
  115. // 在确定不会造成污染的情况下 也可以合理使用全局变量
  116. global $lastReloadTime;
  117. global $inotifyResource;
  118. $lastReloadTime = 0;
  119. $files = File::scanDirectory(EASYSWOOLE_ROOT . '/App');
  120. $files = array_merge($files['files'], $files['dirs']);
  121. $inotifyResource = inotify_init();
  122. // 为当前所有的目录和文件添加事件监听
  123. foreach ($files as $item) {
  124. inotify_add_watch($inotifyResource, $item, IN_CREATE | IN_DELETE | IN_MODIFY);
  125. }
  126. // 加入事件循环
  127. swoole_event_add($inotifyResource, function () {
  128. global $lastReloadTime;
  129. global $inotifyResource;
  130. $events = inotify_read($inotifyResource);
  131. if ($lastReloadTime < time() && !empty($events)) { // 限制1s内不能进行重复reload
  132. $lastReloadTime = time();
  133. ServerManager::getInstance()->getSwooleServer()->reload();
  134. }
  135. });
  136. }
  137. public function onShutDown()
  138. {
  139. // TODO: Implement onShutDown() method.
  140. }
  141. public function onReceive(string $str)
  142. {
  143. // TODO: Implement onReceive() method.
  144. }
  145. }

添加好后在全局的 EasySwooleEvent.php 中,注册该自定义进程

  1. public static function mainServerCreate(EventRegister $register)
  2. {
  3. $swooleServer = ServerManager::getInstance()->getSwooleServer();
  4. $swooleServer->addProcess((new HotReload('HotReload', ['disableInotify' => false]))->getProcess());
  5. }

因为虚拟机中inotify无法监听到FTP/SFTP等文件上传的事件,将 disableInotify 设置为 true ,可以关闭inotify方式的热重启,使得虚拟机环境下,强制使用文件循环扫描来触发重载操作,同理 OSX 开发环境下,没有Inotify扩展,将自动使用扫描式重载