You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何控制命令行重负载进程?批量邮件发送命令管控咨询

批量邮件命令的控制、暂停与优化方案

嘿,针对你用Symfony的ContainerAwareCommand(现在官方更推荐直接继承Command类啦)批量发送大量邮件的需求,我来给你梳理下停止/暂停控制、交互优化以及更优的实现方案——毕竟处理几百上千封邮件,稳定性和可控性太重要了!

一、停止/暂停命令?当然可以!

不管是手动触发还是后台控制,都能实现优雅的停止和暂停,核心思路是在循环迭代中检查控制标志,避免强制中断导致数据不一致。

1. 优雅停止命令

方式一:信号触发(适合服务器直接操作)

Linux系统下,你可以给进程发送SIGINT(Ctrl+C)或SIGTERM信号,Symfony 5.3+支持SignalableCommandInterface,让你捕获这些信号并优雅处理:

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class BulkEmailCommand extends Command implements SignalableCommandInterface
{
    private bool $shouldStop = false;
    private OutputInterface $output;

    protected function configure(): void
    {
        $this->setName('email:bulk-send');
    }

    public function getSubscribedSignals(): array
    {
        return [SIGINT, SIGTERM];
    }

    public function handleSignal(int $signal): void
    {
        $this->shouldStop = true;
        $this->output->writeln('<info>收到停止信号,正在处理完当前邮件后退出...</info>');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->output = $output;
        $emailRepo = $this->getContainer()->get(App\Repository\EmailQueueRepository::class);
        $pendingEmails = $emailRepo->findPending(); // 获取待发送邮件

        foreach ($pendingEmails as $email) {
            if ($this->shouldStop) {
                $output->writeln('<info>已停止,退出前已更新未完成邮件状态</info>');
                return Command::SUCCESS;
            }

            // 发送邮件逻辑
            try {
                $this->sendEmail($email);
                $emailRepo->markAsSent($email);
            } catch (\Exception $e) {
                $emailRepo->markAsFailed($email, $e->getMessage());
                $output->writeln("<error>发送失败: {$email->getRecipient()}</error>");
            }
        }

        return Command::SUCCESS;
    }

    private function sendEmail(App\Entity\EmailQueue $email): void
    {
        // 调用Mailer发送邮件,带附件逻辑这里实现
    }
}

执行命令时,用Ctrl+C或者kill <进程ID>就能触发优雅停止,不会中途中断正在发送的邮件。

方式二:数据库触发(适合后台面板控制)

如果没法直接操作服务器,你可以在数据库里加个command_control表,存is_stopped字段,每次迭代时查询这个字段:

// 在execute循环里
$controlStatus = $this->getContainer()->get(App\Repository\CommandControlRepository::class)->findOneBy(['command' => 'email:bulk-send']);
if ($controlStatus && $controlStatus->isStopped()) {
    $this->output->writeln('<info>后台触发停止,正在退出...</info>');
    return Command::SUCCESS;
}

这样在后台面板加个按钮,修改is_stopped为true就能远程停止命令。

2. 暂停/恢复命令

暂停需要让进程进入休眠状态,直到收到恢复信号或数据库状态变更:

信号触发暂停/恢复

用SIGUSR1(暂停)和SIGUSR2(恢复)信号,扩展上面的信号处理逻辑:

private bool $isPaused = false;

public function getSubscribedSignals(): array
{
    return [SIGINT, SIGTERM, SIGUSR1, SIGUSR2];
}

public function handleSignal(int $signal): void
{
    switch ($signal) {
        case SIGINT:
        case SIGTERM:
            $this->shouldStop = true;
            $this->output->writeln('<info>收到停止信号,处理完当前邮件后退出</info>');
            break;
        case SIGUSR1:
            $this->isPaused = true;
            $this->output->writeln('<info>进程已暂停,发送SIGUSR2信号恢复</info>');
            break;
        case SIGUSR2:
            $this->isPaused = false;
            $this->output->writeln('<info>进程已恢复,继续发送邮件</info>');
            break;
    }
}

// 在execute循环里
foreach ($pendingEmails as $email) {
    // 暂停时循环休眠检查
    while ($this->isPaused) {
        sleep(1);
        if ($this->shouldStop) {
            $this->output->writeln('<info>暂停期间收到停止信号,退出</info>');
            return Command::SUCCESS;
        }
    }

    if ($this->shouldStop) break;

    // 发送邮件逻辑...
}

暂停时用kill -USR1 <进程ID>,恢复用kill -USR2 <进程ID>就行。

数据库触发暂停/恢复

和停止逻辑类似,在数据库里加is_paused字段,循环里检查:

while ($controlStatus->isPaused() && !$controlStatus->isStopped()) {
    $this->output->writeln('<info>进程已暂停,等待恢复...</info>');
    sleep(5);
    // 重新查询状态
    $controlStatus = $this->getContainer()->get(App\Repository\CommandControlRepository::class)->findOneBy(['command' => 'email:bulk-send']);
}

二、更多控制与交互方式

除了停止暂停,你还能让命令更“聪明”:

  • 实时进度监控:用Symfony的ProgressBar组件,在控制台显示发送进度:
    use Symfony\Component\Console\Helper\ProgressBar;
    
    // 在execute里
    $progressBar = new ProgressBar($output, count($pendingEmails));
    $progressBar->start();
    
    foreach ($pendingEmails as $email) {
        // ...发送邮件
        $progressBar->advance();
    }
    
    $progressBar->finish();
    $output->writeln('<info>所有邮件发送完成!</info>');
    
  • 详细日志记录:用Monolog记录每封邮件的发送状态(成功/失败)、错误信息,方便后续排查:
    $logger = $this->getContainer()->get('monolog.logger.email');
    // 发送成功时
    $logger->info("邮件发送成功: {$email->getRecipient()}");
    // 发送失败时
    $logger->error("邮件发送失败: {$email->getRecipient()}, 错误: {$e->getMessage()}");
    
  • 断点续传:不要一次性加载所有邮件,用分页查询每次处理N封,同时在数据库记录已处理的最后ID,下次启动从该ID继续:
    $lastProcessedId = $controlStatus->getLastProcessedId() ?? 0;
    $pendingEmails = $emailRepo->findPendingAfterId($lastProcessedId, 100); // 每次处理100封
    
    foreach ($pendingEmails as $email) {
        // ...发送邮件
        $controlStatus->setLastProcessedId($email->getId());
        $this->getContainer()->get('doctrine.orm.default_entity_manager')->flush();
    }
    
  • 后台面板控制:做一个简单的后台页面,显示当前发送进度、已发送数量,提供停止/暂停/恢复按钮,直接修改数据库状态或者调用服务器API发送信号。

三、更优的实现与监控方案

如果是发送数千封邮件,直接用Command循环其实不是最优解,推荐用消息队列来重构:

1. 消息队列方案(推荐)

用Symfony Messenger或者RabbitMQ/Redis作为队列,优势在于:

  • 异步处理:把邮件发送任务拆成一个个消息,放到队列里,消费者进程异步处理,不会阻塞Command;
  • 自动重试:失败的邮件可以自动重试(配置重试次数和间隔),不用手动处理;
  • 横向扩展:可以启动多个消费者进程,并行处理,提高发送速度;
  • 可靠性:即使消费者进程意外崩溃,重启后会继续处理未完成的任务,不需要手动记录断点。

示例实现:

第一步:定义邮件消息

// src/Message/EmailMessage.php
namespace App\Message;

class EmailMessage
{
    public function __construct(
        private string $recipient,
        private string $subject,
        private string $content,
        private ?string $attachmentPath = null
    ) {}

    // Getters...
    public function getRecipient(): string { return $this->recipient; }
    public function getSubject(): string { return $this->subject; }
    public function getContent(): string { return $this->content; }
    public function getAttachmentPath(): ?string { return $this->attachmentPath; }
}

第二步:定义消息处理器

// src/MessageHandler/EmailMessageHandler.php
namespace App\MessageHandler;

use App\Message\EmailMessage;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Psr\Log\LoggerInterface;

class EmailMessageHandler implements MessageHandlerInterface
{
    public function __construct(
        private MailerInterface $mailer,
        private LoggerInterface $logger
    ) {}

    public function __invoke(EmailMessage $message)
    {
        $email = (new \Symfony\Component\Mime\Email())
            ->to($message->getRecipient())
            ->subject($message->getSubject())
            ->html($message->getContent());

        if ($message->getAttachmentPath()) {
            $email->attachFromPath($message->getAttachmentPath());
        }

        try {
            $this->mailer->send($email);
            $this->logger->info("邮件发送成功: {$message->getRecipient()}");
        } catch (TransportExceptionInterface $e) {
            $this->logger->error("邮件发送失败: {$message->getRecipient()}, 错误: {$e->getMessage()}");
            throw $e; // 触发重试
        }
    }
}

第三步:批量发送消息到队列

// 原BulkEmailCommand的execute方法
protected function execute(InputInterface $input, OutputInterface $output): int
{
    $emailRepo = $this->getContainer()->get(App\Repository\EmailQueueRepository::class);
    $pendingEmails = $emailRepo->findPending();
    $bus = $this->getContainer()->get('messenger.bus.default');

    foreach ($pendingEmails as $email) {
        $message = new EmailMessage(
            $email->getRecipient(),
            $email->getSubject(),
            $email->getContent(),
            $email->getAttachmentPath()
        );
        $bus->dispatch($message);
        $emailRepo->markAsQueued($email);
    }

    $output->writeln("<info>共 ".count($pendingEmails)." 封邮件已加入队列,等待处理</info>");
    return Command::SUCCESS;
}

然后启动消费者进程:

php bin/console messenger:consume async -vv

可以启动多个消费者进程来提高处理速度。

2. 监控方案

  • 命令监控:如果坚持用Command,用Monolog记录详细日志,然后用Grafana或者ELK栈可视化日志,监控发送成功率、耗时等;
  • 队列监控:RabbitMQ有自带的管理界面,可以查看队列长度、消费者数量、处理速度;Redis可以用redis-cli info stats查看;Symfony Messenger可以用php bin/console messenger:stats查看队列状态;
  • 进程监控:用systemd或者supervisord管理Command或消费者进程,设置自动重启,确保进程意外退出后能自动恢复。比如用supervisord配置多个消费者进程,自动重启崩溃的进程。

总结

  • 停止/暂停Command完全可行,通过信号或数据库触发,核心是在循环中检查控制标志;
  • 可以通过进度条、日志、后台面板实现更多交互和控制;
  • 对于大量邮件,消息队列是更优的方案,能提升可靠性、扩展性和可维护性;
  • 结合日志、队列监控工具和进程管理工具,能确保任务稳定运行。

内容的提问来源于stack exchange,提问作者Apyc

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:19:03