接続を倱わずにPHPでデヌモンを再起動する

さたざたな䌚議で、CLIスクリプトのクラりドに぀いお繰り返し話しおきたした ビデオレポヌト 、 スラむド 。 クラりドは、スケゞュヌルたたはAPIを介しおさたざたなPHPスクリプトを実行するように蚭蚈されおいたす。 原則ずしお、これらのスクリプトはキュヌを凊理し、負荷は玄100台のサヌバヌに「分散」したす。 前に、制埡ロゞックの実装方法に泚目したした。これは、非垞に倚くのサヌバヌ間で負荷を均䞀に分散し、スケゞュヌルに埓っおタスクを生成する圹割を果たしたす。 しかし、これに加えお、CLIでPHPスクリプトを実行し、実行のステヌタスを監芖できるデヌモンを䜜成する必芁がありたした。



もずもずは、圓瀟の他のすべおの悪魔のように、Cで曞かれおいたした。 しかし、プロセッサ時間のかなりの郚分玄10が無駄になっおいるずいう事実に盎面したした。これは、むンタヌプリタヌの起動ずフレヌムワヌクの「コア」のロヌドです。 したがっお、むンタヌプリタヌずフレヌムワヌクを䞀床だけ初期化できるようにするために、PHPでデヌモンを曞き盎すこずにしたした。 これをPhp rock sydず呌びたしたPhproxyd-PHP Proxy Daemon、以前のCデヌモンに䌌おいたす。 個々のクラスを実行する芁求を受け入れ、各芁求でforkを行い、各起動の実行ステヌタスを報告する方法も知っおいたす。 このアヌキテクチャは、すべおの初期化が「りィザヌド」で1回行われ、「子」がリク゚ストの凊理にすでに関䞎しおいる堎合、Apache Webサヌバヌモデルに倚くの点で䌌おいたす。 远加の「バン」ずしお、CLIでオペコヌドキャッシュを有効にする機䌚がありたす。これは、すべおの子がマスタヌプロセスず同じ共有メモリの領域を継承するため、正しく動䜜したす。 起動芁求の凊理の遅延を枛らすために、事前にforkを行うこずができたすプリフォヌクモデルが、今回の堎合、forkの遅延は玄1ミリ秒であり、これは適切です。



ただし、コヌドを頻繁に曎新するため、このデヌモンも頻繁に再起動する必芁がありたす。再起動しないず、このデヌモンにロヌドされるコヌドが叀くなる可胜性がありたす。 各再起動には、゚ンドナヌザヌに察するサヌビス拒吊デヌモンはクラりドだけでなく、サむトの䞀郚にも圹立぀など、peerによる接続リセットのタむプの゚ラヌが倚数発生するため、既に確立された接続を倱うこずなくデヌモンを再起動する方法を探すこずにしたした。 デヌモンに察しおグレヌスフルリロヌドが行われる䞀般的な手法が1぀ありたす。fork-execが行われ、リッスン゜ケットからの蚘述子が子孫に枡されたす。 したがっお、新しい接続はデヌモンの新しいバヌゞョンによっお受け入れられたすが、叀い接続は叀いバヌゞョンを䜿甚しお「倉曎」されたす。



この蚘事では、 グレヌスフルリロヌドの耇雑なバヌゞョンを怜蚎したす。叀い接続は新しいバヌゞョンのデヌモンによっお匕き続き凊理されたすが、それ以倖の堎合は叀いコヌドを実行するため、これは重芁です。



理論



始めに考えおみたしょう私たちが受け取りたい可胜性はありたすか もしそうなら、これを達成する方法は



デヌモンはPOSIX互換のLinuxで実行されるため、次のオプションを䜿甚できたす。



  1. 開いおいるファむルず゜ケットはすべお、開いおいる蚘述子の番号に察応する番号です。 暙準入力、出力、および゚ラヌストリヌムには、それぞれ蚘述子0、1、および2がありたす。
  2. 開いおいるファむル、゜ケット、パむプの間に倧きな違いはありたせんたずえば、読み取り/曞き蟌みシステムコヌルずsendto / recvfromを䜿甚しお゜ケットを操䜜できたす。
  3. forkシステムコヌルが行われるず、開いおいるすべおの蚘述子は、その番号ずファむル内の読み取り/曞き蟌み䜍眮が保持された状態で継承されたす。
  4. execveシステムコヌルが実行されるず、開いおいるすべおの蚘述子も継承され、さらにプロセスPID、したがっおその子ぞのバむンドが保存されたす。
  5. オヌプンプロセス蚘述子のリストは、/ dev / fdディレクトリから入手できたす。Linuxでは、このディレクトリは/ proc / self / fdにシンボリックリンクされおいたす。


したがっお、私たちの仕事は実珟可胜であり、倚くの努力なしに信じられる理由がありたす。 それでは始めたしょう。



PHPパッチ



残念ながら、䜜業を耇雑にする小さな詳现が1぀ありたす。PHPでは、ストリヌムのファむル蚘述子番号を取埗し、番号でファむル蚘述子を開く方法がありたせん代わりに、ファむル蚘述子のコピヌが開かれたす。再起動時および子プロセスの起動時にリヌクが発生しないように、オヌプン蚘述子を慎重に監芖したす。



たず、いく぀かの小さなパッチをPHPコヌドに远加しお、ストリヌムからfdを取埗する機胜を远加し、fopenphp// fd / <num>が蚘述子のコピヌを開かないようにしたす2番目の倉曎は、珟圚のPHPの動䜜。したがっお、代わりにphp// fdraw / <num>などの新しい「アドレス」を远加できたす。



パッチコヌド
diff --git a/ext/standard/php_fopen_wrapper.cb/ext/standard/php_fopen_wrapper.c index f8d7bda..fee964c 100644 --- a/ext/standard/php_fopen_wrapper.c +++ b/ext/standard/php_fopen_wrapper.c @@ -24,6 +24,7 @@ #if HAVE_UNISTD_H #include <unistd.h> #endif +#include <fcntl.h> #include "php.h" #include "php_globals.h" @@ -296,11 +297,11 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, char *path, ch "The file descriptors must be non-negative numbers smaller than %d", dtablesize); return NULL; } - - fd = dup(fildes_ori); - if (fd == -1) { + + fd = fildes_ori; + if (fcntl(fildes_ori, F_GETFD) == -1) { php_stream_wrapper_log_error(wrapper, options TSRMLS_CC, - "Error duping file descriptor %ld; possibly it doesn't exist: " + "File descriptor %ld invalid: " "[%d]: %s", fildes_ori, errno, strerror(errno)); return NULL; } diff --git a/ext/standard/streamsfuncs.cb/ext/standard/streamsfuncs.c index 0610ecf..14fd3b0 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -24,6 +24,7 @@ #include "ext/standard/flock_compat.h" #include "ext/standard/file.h" #include "ext/standard/php_filestat.h" +#include "ext/standard/php_fopen_wrappers.h" #include "php_open_temporary_file.h" #include "ext/standard/basic_functions.h" #include "php_ini.h" @@ -484,6 +485,7 @@ PHP_FUNCTION(stream_get_meta_data) zval *arg1; php_stream *stream; zval *newval; + int tmp_fd; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &arg1) == FAILURE) { return; @@ -502,6 +504,9 @@ PHP_FUNCTION(stream_get_meta_data) add_assoc_string(return_value, "wrapper_type", (char *)stream->wrapper->wops->label, 1); } add_assoc_string(return_value, "stream_type", (char *)stream->ops->label, 1); + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&tmp_fd, 1) && tmp_fd != -1) { + add_assoc_long(return_value, "fd", tmp_fd); + } add_assoc_string(return_value, "mode", stream->mode, 1);
      
      







理にかなっおいる堎合は、stream_get_meta_dataによっお返される結果にfdフィヌルドを远加したしたたずえば、zlibストリヌムの堎合、fdフィヌルドは存圚したせん。 たた、枡されたファむル蚘述子からのdup呌び出しを単玔なチェックに眮き換えたした。 残念ながら、fcntl呌び出しはPOSIX固有であるため、このコヌドはWindows甚に倉曎しないず機胜したせん。そのため、完党なパッチには他のOS甚の远加のコヌドブランチが含たれる必芁がありたす。



再起動オプションのないデヌモン



たず、JSON圢匏でリク゚ストを受信し、䜕らかの回答を提䟛できる小さなサヌバヌを䜜成したす。 たずえば、リク゚ストに含たれる配列内の芁玠の数を返したす。



デヌモンはポヌト31337でリッスンしたす。結果は次のようになりたす。

 $ telnet localhost 31337 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. {"hash":1} #   "Request had 1 keys" {"hash":1,"cnt":2} #   "Request had 2 keys"
      
      







stream_socket_serverを䜿甚しおポヌトのリッスンを開始し、stream_selectを䜿甚しお読み取り/曞き蟌みの準備ができおいる蚘述子を刀別したす。



シンプルな実装コヌドSimple.php
 <?php class Simple { const PORT = 31337; const SERVER_KEY = 'SERVER'; /** @var resource[] (client_id => stream) */ private $streams = []; /** @var string[] (client_id => read buffer) */ private $read_buf = []; /** @var string[] (client_id => write buffer) */ private $write_buf = []; /** @var resource[] (client_id => stream from which to read) */ private $read = []; /** @var resource[] (client_id => stream where to write) */ private $write = []; /** @var int Total connection count */ private $conn_count = 0; public function run() { $this->listen(); echo "Entering main loop\n"; $this->mainLoop(); } protected function listen() { $port = self::PORT; $ip_port = "0.0.0.0:$port"; $address = "tcp://$ip_port"; $server = stream_socket_server($address, $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN); if (!$server) { fwrite(STDERR, "stream_socket_server failed: $errno $errstr\n"); exit(1); } $this->read[self::SERVER_KEY] = $server; echo "Listening on $address\n"; } public function response($stream_id, $response) { $json_resp = json_encode($response); echo "stream$stream_id " . $json_resp . "\n"; $this->write($stream_id, $json_resp . "\n"); } public function write($stream_id, $buf) { $this->write_buf[$stream_id] .= $buf; if (!isset($this->write[$stream_id])) { $this->write[$stream_id] = $this->streams[$stream_id]; } } public function accept($server) { echo "Accepting new connection\n"; $client = stream_socket_accept($server, 1, $peername); $stream_id = ($this->conn_count++); if (!$client) { fwrite(STDERR, "Accept failed\n"); return; } stream_set_read_buffer($client, 0); stream_set_write_buffer($client, 0); stream_set_blocking($client, 0); stream_set_timeout($client, 1); $this->read_buf[$stream_id] = ''; $this->write_buf[$stream_id] = ''; $this->read[$stream_id] = $this->streams[$stream_id] = $client; echo "Connected stream$stream_id: $peername\n"; } private function disconnect($stream_id) { echo "Disconnect stream$stream_id\n"; unset($this->read_buf[$stream_id], $this->write_buf[$stream_id]); unset($this->streams[$stream_id]); unset($this->write[$stream_id], $this->read[$stream_id]); } private function handleRead($stream_id) { $buf = fread($this->streams[$stream_id], 8192); if ($buf === false || $buf === '') { echo "got EOF from stream$stream_id\n"; if (empty($this->write_buf[$stream_id])) { $this->disconnect($stream_id); } else { unset($this->read[$stream_id]); } return; } $this->read_buf[$stream_id] .= $buf; $this->processJSONRequests($stream_id); } private function processJSONRequests($stream_id) { if (!strpos($this->read_buf[$stream_id], "\n")) return; $requests = explode("\n", $this->read_buf[$stream_id]); $this->read_buf[$stream_id] = array_pop($requests); foreach ($requests as $req) { $res = json_decode(rtrim($req), true); if ($res !== false) { $this->response($stream_id, "Request had " . count($res) . " keys"); } else { $this->response($stream_id, "Invalid JSON"); } } } private function handleWrite($stream_id) { if (!isset($this->write_buf[$stream_id])) { return; } $wrote = fwrite($this->streams[$stream_id], substr($this->write_buf[$stream_id], 0, 65536)); if ($wrote === false) { fwrite(STDERR, "write failed into stream #$stream_id\n"); $this->disconnect($stream_id); return; } if ($wrote === strlen($this->write_buf[$stream_id])) { $this->write_buf[$stream_id] = ''; unset($this->write[$stream_id]); if (empty($this->read[$stream_id])) { $this->disconnect($stream_id); } } else { $this->write_buf[$stream_id] = substr($this->write_buf[$stream_id], $wrote); } } public function mainLoop() { while (true) { $read = $this->read; $write = $this->write; $except = null; echo "Selecting for " . count($read) . " reads, " . count($write) . " writes\n"; $n = stream_select($read, $write, $except, NULL); if (!$n) { fwrite(STDERR, "Could not stream_select()\n"); } if (count($read)) { echo "Can read from " . count($read) . " streams\n"; } if (count($write)) { echo "Can write to " . count($write) . " streams\n"; } if (isset($read[self::SERVER_KEY])) { $this->accept($read[self::SERVER_KEY]); unset($read[self::SERVER_KEY]); } foreach ($read as $stream_id => $_) { $this->handleRead($stream_id); } foreach ($write as $stream_id => $_) { $this->handleWrite($stream_id); } } } } $instance = new Simple(); $instance->run();
      
      







このデヌモンのコヌドは暙準を超えおいたすが、実装の詳现に泚意したいず思いたす特定の接続を参照しおすべおの読み取りおよび曞き蟌みバッファヌを保存し、芁求を読み取った同じ堎所で盎接芁求を凊理したす。 これらの芁求の1぀が再起動される可胜性があるため、これは重芁です。この堎合、次の芁求の凊理には至りたせん。 それでも、リク゚ストをただ読み取っおいないため、次回同じ蚘述子からのstream_selectは同じ結果を返したす。 したがっお、コマンドハンドラヌから盎接再起動しおも、1぀の芁求が倱われるこずはありたせん耇数のコマンドが同じ接続で同時に送信され、これらのコマンドの1぀が再起動する堎合を陀く。



それでは、デヌモンの再起動をどのようにしお可胜にしたすか



再起動および確立された接続を保存するデヌモン



最も単玔な䟋では、有甚なこずを行う方法がわからなかったので、最初に説明した悪魔をただ曞いおみたしょう。 次のようなものを取埗したすコマンドは「command_name [JSON-data]」の圢匏でデヌモンに送信されたす。答えはJSONの圢匏です。

 $ telnet localhost 31337 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. #      restart #      "Restarted successfully" #    run {"hash":1,"params":[1,2,3],"class":"TestClass1"} #   {"error_text":"OK"} #     ( child TestClass1   ) restart "Restarted successfully" #   :    check {"hash":1} {"error_text":"Still running"} #  5     :  TestClass1   check {"hash":1} {"retcode":0} #     ,    free check {"hash":1} {"retcode":0} free {"hash":1} {"error_text":"OK"} restart "Restarted successfully" #   ,          restart restart {"error_text":"Restarted successfully"} bye Connection closed by foreign host.
      
      







再起動のアむデアは簡単です。必芁なすべおの情報を含むファむルを䜜成し、起動時にそれを読み取っお、開いおいるファむル蚘述子を埩元しようずしたす。



最初に、再起動ファむルに曞き蟌むコヌドを蚘述したす。



 echo "Creating restart file...\n"; if (!$res = $this->getFdRestartData()) { fwrite(STDERR, "Could not get restart FD data, exiting, graceful restart is not supported\n"); exit(0); } /* Close all extra file descriptors that we do not know of, including opendir() descriptor :) */ $dh = opendir("/proc/self/fd"); $fds = []; while (false !== ($file = readdir($dh))) { if ($file[0] === '.') continue; $fds[] = $file; } foreach ($fds as $fd) { if (!isset($this->known_fds[$fd])) { fclose(fopen("php://fd/" . $fd, 'r+')); } } $contents = serialize($res); if (file_put_contents(self::RESTART_DIR . self::RESTART_FILENAME, $contents) !== strlen($contents)) { fwrite(STDERR, "Could not fully write restart file\n"); unlink(self::RESTART_DIR . self::RESTART_FILENAME); }
      
      







デヌタ配列を取埗するコヌドgetFdRestartData関数を以䞋に瀺したす。



 $res = []; foreach (self::$restart_fd_resources as $prop) { $res[$prop] = []; foreach ($this->$prop as $k => $v) { $meta = stream_get_meta_data($v); if (!isset($meta['fd'])) { fwrite(STDERR, "No fd in stream metadata for resource $v (key $k in $prop), got " . var_export($meta, true) . "\n"); return false; } $res[$prop][$k] = $meta['fd']; $this->known_fds[$meta['fd']] = true; } } foreach (self::$restart_fd_props as $prop) { $res[$prop] = $this->$prop; } return $res;
      
      





コヌドでは、2皮類のプロパティがあるこずを考慮しおいたす。

  1. 接続されたリ゜ヌスを含むプロパティ$ restart_fd_resources = ['read'、 'write'、 'streams']。
  2. 生でシリアル化できる接続に関するバッファおよびその他の情報を含むプロパティ$ restart_fd_props = ['read_buf'、 'write_buf'、 'conn_count']。


たた、再起動ファむルに保存されおいるすべおのfdを蚘憶し、他のすべお存圚する堎合を閉じたす。そうしないず、ファむル蚘述子がリヌクする可胜性があるためです。



次に、起動時にこのファむルをロヌドし、䜕も起こらなかったかのように、開いおいる蚘述子を匕き続き䜿甚する必芁がありたす:)。 2぀の関数再起動ファむルの読み蟌みずファむル蚘述子に関する情報のダりンロヌドのコヌドを以䞋に瀺したす。



ファむルのダりンロヌド



 if (!file_exists(self::RESTART_DIR . self::RESTART_FILENAME)) { return; } echo "Restart file found, trying to adopt it\n"; $contents = file_get_contents(self::RESTART_DIR . self::RESTART_FILENAME); unlink(self::RESTART_DIR . self::RESTART_FILENAME); if ($contents === false) { fwrite(STDERR, "Could not read restart file\n"); return; } $res = unserialize($contents); if (!$res) { fwrite(STDERR, "Could not unserialize restart file contents"); return; } foreach (self::$restart_props as $prop) { if (!array_key_exists($prop, $res)) { fwrite(STDERR, "No property $prop in restart file\n"); continue; } $this->$prop = $res[$prop]; } $this->loadFdRestartData($res);
      
      







ファむル蚘述子の配列を展開しお戻すloadFdRestartData関数



 $fd_resources = []; foreach (self::$restart_fd_resources as $prop) { if (!isset($res[$prop])) { fwrite(STDERR, "Property '$prop' is not present in restart fd resources\n"); continue; } $pp = []; foreach ($res[$prop] as $k => $v) { if (isset($fd_resources[$v])) { $pp[$k] = $fd_resources[$v]; } else { $fp = fopen("php://fd/" . $v, 'r+'); if (!$fp) { fwrite(STDERR, "Failed to open fd = $v, exiting\n"); exit(1); } stream_set_read_buffer($fp, 0); stream_set_write_buffer($fp, 0); stream_set_blocking($fp, 0); stream_set_timeout($fp, self::CONN_TIMEOUT); $fd_resources[$v] = $fp; $pp[$k] = $fp; } } $this->$prop = $pp; } foreach (self::$restart_fd_props as $prop) { if (!isset($res[$prop])) { fwrite(STDERR, "Property '$prop' is not present in restart fd properties\n"); continue; } $this->$prop = $res[$prop]; }
      
      





オヌプンファむル蚘述子のread_bufferずwrite_bufferを再蚭定し、タむムアりトを構成したす。 奇劙なこずに、これらの操䜜の埌、PHPはこれらのファむル蚘述子に察しおを静かに受け入れ、これらが゜ケットであるこずを知らなくおも、通垞は読み取り/曞き蟌みを続けたす。



最埌に、ワヌカヌの実行ステヌタスを起動および远跡するためのロゞックを蚘述する必芁がありたす。 これは蚘事のトピックずは関係がないため、デヌモンの完党な実装はgithubリポゞトリに配眮されたす。リンクは以䞋にありたす。



おわりに



そのため、この蚘事では、JSONプロトコルを介しお通信し、実行プロセスを監芖しながら別のプロセスで任意のクラスを実行できるデヌモンの実装に぀いお説明したした。 個々のクラスを実行するには、 forkモデルを䜿甚しおク゚リを実行するため、リク゚ストを凊理するために、むンタヌプリタヌを再起動しおフレヌムワヌクをロヌドする必芁はなく、CLIでオペコヌドキャッシュを䜿甚するこずが可胜になりたす。 コヌドを曎新するたびにデヌモンを再起動する必芁があるため、このデヌモンをスムヌズに再起動する必芁がありたす圓瀟では、コヌドは「ホットフィックス」の圢匏で数分ごずに曎新されるこずがありたす。



再起動はexecveシステムコヌルを実行するこずにより行われ、その結果、すべおの子孫は芪にバむンドされたたたになりたすプロセスのPIDはexecveで倉曎されないため。 開いおいるファむル蚘述子もすべお保存されるため、すでに開いおいる接続のナヌザヌからの芁求を匕き続き凊理できたす。 すべおのネットワヌクバッファ、実行䞭の子に関する情報、および開いおいる蚘述子は、個別の再起動ファむルに保存され、デヌモンの新しいむンスタンスによっお読み取られ、その埌、暙準むベントルヌプで䜜業が続行されたす。



完党な実装コヌドは、GitHubの次のアドレスで芋るこずができたす github.com/badoo/habr/tree/master/phprocksyd



質問、提案、説明を歓迎したす。



よろしく

ナヌリナヌロックナスレッディノフ

リヌドPHP開発者

バドゥヌ



All Articles