PHP命令行脚本多线程集成咨询:解决Telnet耗时与DB并发写入问题
Hey Michele, let's break down how to tackle your PHP CLI multi-threading issue for those slow Telnet operations—here's a practical approach tailored to your scenario:
You're stuck integrating multi-threading into your CLI script because:
- Telnet operations are too slow when run sequentially
- You can't safely use
$stmt2to write to the database concurrently - You're currently iterating through devices with
$stmt->fetchand need a better task-based approach
The key fix here is to decouple the slow Telnet tasks (which benefit from parallelism) from database writes (which need to be handled sequentially to avoid race conditions or connection issues). Here are two reliable methods for PHP CLI:
Method 1: Use Symfony Process (Third-Party, Easier)
This library simplifies spawning and managing child processes without low-level PCNTL calls.
Step 1: Install the Library
composer require symfony/process
Step 2: Main Script Logic
First, fetch all your devices upfront, then spawn a child process for each Telnet task. Collect results in the main process, then write everything to the database at once.
<?php require 'vendor/autoload.php'; use Symfony\Component\Process\Process; // 1. Fetch all devices first (avoids holding a DB cursor open during parallel tasks) $devices = []; while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $devices[] = $row; } // 2. Spawn processes for Telnet tasks $processes = []; foreach ($devices as $index => $device) { // Pass device details to a dedicated Telnet handler script $process = new Process([ 'php', 'telnet_handler.php', $device['ip'], $device['port'], $device['id'] // Pass device ID to link results later ]); $process->start(); $processes[$index] = ['process' => $process, 'device' => $device]; } // 3. Wait for all processes to finish and collect results $telnetResults = []; foreach ($processes as $procData) { $procData['process']->wait(); if ($procData['process']->isSuccessful()) { $telnetResults[] = [ 'device_id' => $procData['device']['id'], 'output' => $procData['process']->getOutput() ]; } else { // Log failures instead of crashing error_log("Telnet failed for {$procData['device']['ip']}: " . $procData['process']->getErrorOutput()); } } // 4. Write all results to DB sequentially (safe, no concurrency issues) $stmt2 = $pdo->prepare("INSERT INTO device_telnet_results (device_id, output, created_at) VALUES (:device_id, :output, NOW())"); foreach ($telnetResults as $result) { $stmt2->execute($result); }
Step 3: Telnet Handler Script (telnet_handler.php)
This script runs in isolation per device, handling only the Telnet logic:
<?php // Get device details from CLI arguments $ip = $argv[1]; $port = $argv[2]; $deviceId = $argv[3]; // Execute Telnet logic (adjust this to match your actual commands) $socket = fsockopen($ip, $port, $errno, $errstr, 30); if (!$socket) { echo "Connection failed: $errstr ($errno)"; exit(1); } // Send command and read response fwrite($socket, "show status\r\n"); $output = fread($socket, 2048); fclose($socket); // Output the result (main script will capture this) echo $output; exit(0);
Method 2: Use PCNTL (Native PHP Extension)
If you prefer not to use third-party libraries, you can use PHP's built-in PCNTL extension for forking processes.
Example Script
<?php // Enable PCNTL signals (optional but helpful for cleanup) pcntl_signal(SIGCHLD, SIG_IGN); // 1. Fetch all devices $devices = []; while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $devices[] = $row; } $tmpDir = sys_get_temp_dir(); $childPids = []; // 2. Fork child processes for each device foreach ($devices as $device) { $pid = pcntl_fork(); if ($pid === -1) { die("Failed to fork process for {$device['ip']}"); } elseif ($pid === 0) { // Child process: handle Telnet and save result to temp file $socket = fsockopen($device['ip'], $device['port'], $errno, $errstr, 30); $result = $socket ? fread($socket, 2048) : "Error: $errstr ($errno)"; fclose($socket); $tmpFile = "$tmpDir/device_{$device['id']}_result.tmp"; file_put_contents($tmpFile, $result); exit(0); // Exit child process to avoid duplication } else { // Parent process: track child PID and temp file $childPids[$pid] = [ 'device_id' => $device['id'], 'tmp_file' => "$tmpDir/device_{$device['id']}_result.tmp" ]; } } // 3. Wait for all children to finish and collect results $telnetResults = []; foreach ($childPids as $pid => $data) { pcntl_waitpid($pid, $status); if (file_exists($data['tmp_file'])) { $telnetResults[] = [ 'device_id' => $data['device_id'], 'output' => file_get_contents($data['tmp_file']) ]; unlink($data['tmp_file']); // Clean up temp file } } // 4. Write to database sequentially $stmt2 = $pdo->prepare("INSERT INTO device_telnet_results (device_id, output) VALUES (:device_id, :output)"); foreach ($telnetResults as $result) { $stmt2->execute($result); }
- Never share DB connections across processes: Child processes inherit the parent's connection, but it will break once the parent or child tries to use it. Always handle DB writes in the main process.
- Limit concurrency: If you have hundreds of devices, don't spawn a process for all at once—use a queue system to cap parallel processes (e.g., run 10 at a time) to avoid overwhelming your server.
- Error handling: Always log failures instead of letting the script crash. Telnet connections will fail occasionally, and you need to track which devices had issues.
内容的提问来源于stack exchange,提问作者Michele

