请求接口会发现接口超时的现象,结果是数据库出现读超时(处理步骤,解决方案)

shiroi

环境:

  • 1.webman项目是搭建在内网的
  • 2.mysql数据库使用的是阿里云数据库

发现的痕迹:

  • 1.一开始搭建服务运行是没问题(本地服务 + 阿里云数据库),只是有时候请求接口会很久,那时候也没去详细探究
  • 2.后面使用的人多起来,经常有人反馈请求接口超时,这时候使用官方的教程php start.php status去排查busy的进程,发现结果都是在查询数据库这一块阻塞住了
  • 3.因为这个问题,所以把数据库切换为本地,发现这个问题不出现了,没有接口超时了,所以问题就集中在网络这一块,把数据库切换回阿里云数据库,然后在这个基础加了个数据库超时重连的机制卸载了基类模型上

    • config/database.php
      return [
      ...,
      'options' => [
        \PDO::ATTR_TIMEOUT => 3, //设置与数据库通信的超时值(以秒为单位)
      ]
      ];
    • app/model/BaseModel.php

      namespace app\model;
      
      use Exception;
      use Illuminate\Database\Eloquent\Builder;
      use Illuminate\Database\QueryException;
      use support\Db;
      use support\Model;
      
      class BaseModel extends Model
      {
          /** @var int 最大重试次数 */
          protected int $retryAttempts = 3;
      
          /** @var int 每次重试延迟(毫秒) */
          protected int $retryDelay = 100;
      
          /** @var array|string[] $defaultOrder 默认排序 */
          protected array $defaultOrder = ['id' => 'desc'];
      
          /** @var int DEFAULT_SOFT_DELETE 默认软删除值 */
          const DEFAULT_SOFT_DELETE = 0;
      
          /** @var string 创建时间 */
          public const CREATED_AT = 'create_time';
      
          /** @var string 更新时间 */
          public const UPDATED_AT = 'update_time';
      
          /**
           * Indicates if the model should be timestamped.
           * @var bool
           */
          public $timestamps = false;
      
          protected static function boot()
          {
              parent::boot();
      
              //设置默认排序
              static::addGlobalScope('order', function (Builder $builder) {
                  if(property_exists(static::class, 'defaultOrder')) foreach ((new static())->defaultOrder as $field => $order)
                      $builder->orderBy($field, $order);
              });
      
              static::creating(function (Model $model) {
                  if (self::hasColumn($model, static::CREATED_AT)) {
                      $model->{static::CREATED_AT} = time();
                  }
                  if (self::hasColumn($model, static::UPDATED_AT)) {
                      $model->{static::UPDATED_AT} = time();
                  }
              });
      
              static::updating(function ($model) {
                  if (self::hasColumn($model, static::UPDATED_AT)) {
                      $model->{static::UPDATED_AT} = time();
                  }
              });
          }
      
          protected static function hasColumn($model, $column): bool
          {
              $schemaBuilder = $model->getConnection()->getSchemaBuilder();
              $columns = $schemaBuilder->getColumnListing($model->getTable());
      
              return in_array($column, $columns);
          }
      
          /**
           * 封装数据库操作,捕获丢包并重连
           * @param callable $callback
           * @return mixed
           * @throws Exception
           */
          public function runQueryWithReconnect(callable $callback)
          {
              $attempts = 0;
              while ($attempts < $this->retryAttempts) {
                  try {
                      return $callback();
                  } catch (QueryException $e) {
                      $attempts++;
                      if ($this->isLostConnectionError($e) && $attempts < $this->retryAttempts) {
                          echo "\033[31m[重试] 数据库连接丢失,尝试重新连接... 第 {$attempts} 次\033[0m\n";
                          Db::reconnect();
                          usleep($this->retryDelay * 1000); // 等待指定时间后重试
                      } else {
                          throw $e;
                      }
                  }
              }
              throw new Exception("\033[31m尝试 {$this->retryAttempts} 次后仍无法连接到数据库。\033[0m");
          }
      
          /**
           * 判断是否为连接丢失错误
           */
          protected function isLostConnectionError($exception): bool
          {
              $errorMessage = $exception->getMessage();
              $lostConnectionMessages = [
                  'server has gone away',
                  'no connection to the server',
                  'Lost connection to MySQL server',
                  'is dead or not enabled',
                  'Error while sending',
                  'decryption failed or bad record mac',
                  'server closed the connection unexpectedly',
              ];
      
              foreach ($lostConnectionMessages as $message) {
                  if (stripos($errorMessage, $message) !== false) {
                      return true;
                  }
              }
              return false;
          }
      
          /**
           * 全局封装动态方法调用
           * @param $method
           * @param $parameters
           * @return mixed
           * @throws Exception
           */
          public function __call($method, $parameters)
          {
              return $this->runQueryWithReconnect(function () use ($method, $parameters) {
                  return parent::__call($method, $parameters);
              });
          }
      
          /**
           * 全局封装静态方法调用
           * @param $method
           * @param $parameters
           * @return mixed
           * @throws Exception
           */
          public static function __callStatic($method, $parameters)
          {
              $instance = new static();
              return $instance->runQueryWithReconnect(function () use ($instance, $method, $parameters) {
                  return $instance->$method(...$parameters);
              });
          }
      }

      但是发现接口还是会超时,还是在请求数据库出现阻塞

  • 4.发送接口不行,那我就把接口都迁移到socket去,但是结果还是一样,一开始都有收发消息,下图
    截图
    后面只有发送消息,下图
    截图
    在查了一下进程,还是数据库阻塞了
  • 5.后面在论坛查看有没有类似相关的问题,还真给我找到了一模一样的问题,https://www.workerman.net/q/6040 ,其实问题就是mysql驱动没有断线重连机制
  • 6.问题知道了,那就寻找解决

    • 方案一:在socket上每个一段时间就把socket连接踢出,客户端那边是已经做了重连机制的
      app\socket\BaseSocket.php

      namespace app\socket;
      
      use Workerman\Timer;
      use Workerman\Worker;
      
      class BaseSocket
      {
          /** @var int 设置心跳间隔 */
          const HEARTBEAT_TIME = 50;
      
          /**
           * 启动服务执行
           * @param Worker $worker
           * @return void
           */
          public function onWorkerStart(Worker $worker)
          {
              Timer::add(10, function () use ($worker) {
                  $time_now = time();
                  foreach ($worker->connections as $connection) {
                      // 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间
                      if (empty($connection->lastMessageTime)) {
                          $connection->lastMessageTime = $time_now;
                          continue;
                      }
                      // 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接
                      if ($time_now - $connection->lastMessageTime > self::HEARTBEAT_TIME) {
                          $connection->close();
                      }
                  }
              });
          }
      }

      这个方案确实减少了很多阻塞的问题,但是还是存在阻塞,治标不治本

    • 方案二:设置pdo读写超时时间,查了很多文档也没查到,最后问了一下chatgpt还真找到了个写法,
      config/database.php

    return [
       ...,
       'options' => [
          \PDO::ATTR_TIMEOUT => 3, //设置与数据库通信的超时值(以秒为单位)
          \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
          \PDO::ATTR_TIMEOUT => 3, //设置与数据库通信的超时值(以秒为单位)
          \PDO::MYSQL_ATTR_INIT_COMMAND => '
              SET SESSION wait_timeout = 30;
              SET SESSION net_read_timeout = 5;
              SET SESSION net_write_timeout = 5;
          ', //设置等待超时 读超时 写超时
          'modes' => [
                'STRICT_TRANS_TABLES',
                'NO_ZERO_IN_DATE',
                'ERROR_FOR_DIVISION_BY_ZERO',
                'NO_ENGINE_SUBSTITUTION',
            ],
       ]
    ];

    实测了几天再也没发现数据库阻塞的问题,这个问题得已解决

如有出现相关的问题的小伙伴,可以参考一下

211 1 1
1个评论

SillyDog

最后得方法1 与方法 2可以再解释的通俗一点嘛

  • 暂无评论

shiroi

560
积分
0
获赞数
0
粉丝数
2024-02-22 加入
×
🔝