Я пытаюсь настроить API, который использует систему очередей на другом сервере для обработки запросов. Позвольте мне начать то, что я пытаюсь выполнить без системы очередей (без авторизации для простоты): например, используя Postman, отправив запрос GET на URL-адрес https://example.com/products вернет строку JSON, например
[
{
"id": 1,
"name": "some name",
...
},
{
"id": 2,
"name": "some other name",
...
}.
...
]
Код в route/api.php будет выглядеть примерно так:
<php
Route::get('/products', ProductController@index');
И код в app/Http/Controllers/ProductController.php:
<?php
namespace App\Http\Controllers;
class ProductController extends Controller
{
/**
* Return the products.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
// Logic to get the products.
return $products->toJson();
}
}
Что я хотел бы сделать, так это то, что вся бизнес-логика обрабатывается на другом сервере, на котором работают несколько рабочих процессов. Далее следует мое обоснование этого.
- Безопасность: в случае, если нас взломают, скорее всего, это будет клиентский сервер, а не рабочий сервер. Поскольку у последнего есть вся бизнес-логика, хакер в худшем случае сможет получить только данные входящего и исходящего запроса.
- Несколько работников: Получение продуктов, вероятно, не займет много времени, но могут быть и другие запросы, для обработки которых требуется больше времени. В большинстве ситуаций пользователь, делающий запрос, должен будет дождаться результата. Однако другим пользователям, совершающим вызов, не следует этого ждать. Поэтому другой работник может принять этот запрос и выполнить задание.
Это будет рабочий процесс, как я его вижу:
Все свободные воркеры постоянно запрашивают задание в очереди
- User makes request
- Клиентский сервер принимает данные запроса и ставит их в очередь
- Рабочий берет задание из очереди и обрабатывает его
- Рабочий возвращает результат клиентскому серверу
- Клиент-сервер возвращает результат пользователю
Ниже небольшой рисунок, чтобы прояснить ситуацию.
User 1
_ \
| \
\ \ 1.
\ \ request
\ \ -------------
result \ \ / \
5. \ \ | Worker |
\ _| | Server |
\ | --------- |
------------- | / \ |
/ \ | | Worker1 | |
| Client | / | | | | \
| Server 2. | / | \ / | \
| --------- | / | --------- | \
| / \ | / | --------- | \
| | Queue | | / | / \ | \ ---------
| | | | |_ | | Worker2 | | _| / \
| | Job A | | | | | | | DB |
| | Job B | | 3. <----- | \ / | -----> | Server |
| | | | _ | --------- | _ | |
| \ / | | | ... | | \ /
| --------- | \ | --------- | / ---------
\ / \ | / \ | / ---------
------------- \ | | WorkerN | | / / \
_ 4. ? \ | | | | / | Other |
| | \ / | | Servers |
/ / | --------- | | |
1. / / \ / \ /
request / / ------------- ---------
/ /
/ / result
/ / 5.
/ /
|_
User 2
В документации Laravel я наткнулся на очереди, которые, как я думал, легко помогут. Я начал экспериментировать с Beanstalkd, но полагаю, что подойдет любой драйвер очереди. Проблема, с которой я столкнулся, заключается в том, что система очередей работает асинхронно. Как следствие, клиентский сервер просто продолжает работу, не дожидаясь результата. Если я что-то не упустил, похоже, нет возможности заставить систему очередей работать синхронно.
При дальнейшем изучении документации Laravel я наткнулся на трансляцию. Я не уверен, понимаю ли я концепцию трансляции на 100%, но из того, что я действительно понимаю, это то, что получение, похоже, происходит в Javascript. Я бэкэнд-разработчик и хотел бы держаться подальше от Javascript. По какой-то причине мне кажется неправильным использовать здесь javascript, однако я не уверен, что это чувство оправдано.
Глядя дальше в документацию, я наткнулся на Redis. В основном меня заинтриговала функциональность Pub/Sub. Я думал, что клиентский сервер может сгенерировать уникальное значение, отправить его с данными запроса в очередь и подписаться на него. После завершения рабочего процесса он может опубликовать результат с этим уникальным значением. Я думал, что это может покрыть недостающую часть для шага 4. Я до сих пор не уверен, как это будет работать в коде, если эта логика вообще будет работать. Я в основном застрял на той части, где клиент должен слушать и получать данные от Redis.
Я мог упустить что-то очень простое. Знайте, что я относительно новичок в программировании на PHP и в концепции программирования через всемирную паутину. Поэтому, если вы обнаружите, что логика ошибочна или слишком надумана, пожалуйста, дайте мне несколько указателей на другие/лучшие методы.
К вашему сведению, я слышал о Gearman, которые, кажется, могут работать как синхронно, так и асинхронно. Однако я хотел бы держаться подальше от этого, поскольку моя цель — использовать инструменты, предоставляемые Laravel, в полной мере. Я все еще учусь и недостаточно уверен, чтобы использовать слишком много внешних плагинов.
Правка: вот как далеко я продвинулся. Чего мне еще не хватает? Или то, что я прошу (почти) невозможно?
Пользователь звонит http://my.domain.com/worker?message=whoop
Пользователь должен получить ответ в формате JSON.
{"message":"you said whoop"}
Обратите внимание, что в заголовке ответа тип контента должен быть "application/json"
, а не "text/html; charset=UTF-8"
.
Это то, что у меня есть до сих пор:
Два сервера API-сервер и WORKER-сервер. API-сервер получает запросы и ставит их в очередь (локальный Redis). Рабочие на WORKER-сервере обрабатывают задания на API-сервере. Как только воркер обработал задание, результат этого задания транслируется на API-сервер. API-сервер прослушивает широковещательный ответ и отправляет его пользователю. Это делается с помощью Redis и socket.io. Моя проблема в том, что на данный момент, чтобы отправить результат, я отправляю блейд-файл с некоторым Javascript, который прослушивает ответ. Это приводит к типу содержимого "text/html; charset=UTF-8" с элементом, который обновляется после передачи результата от рабочего процесса. Есть ли способ вместо того, чтобы возвращать представление, заставить возврат «ждать», пока результат не будет передан?
API-сервер: route\web.php:
<?php
Route::get('/worker', 'ApiController@workerHello');
API-сервер: app\Http\Controllers\ApiController.php
<?php
namespace App\Http\Controllers;
use App\Jobs\WorkerHello;
use Illuminate\Http\Request;
class ApiController extends Controller
{
/**
* Dispatch on queue and open the response page
*
* @return string
*/
public function workerHello(Request $request)
{
// Dispatch the request to the queue
$jobId = $this->dispatch(new WorkerHello($request->message));
return view('response', compact('jobId'));
}
}
API-сервер: app\Jobs\WorkerHello.php
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class WorkerHello implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $message;
/**
* Create a new job instance.
*
* @param string $message the message
* @return void
*/
public function __construct($message = null)
{
$this->message = $message;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
}
}
WORKER-сервер: app\Jobs\WorkerHello.php
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use App\Events\WorkerResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class WorkerHello implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $message;
/**
* Create a new job instance.
*
* @param string $message the message
* @return void
*/
public function __construct($message = null)
{
$this->message = $message;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// Do stuff
$message = json_encode(['message' => 'you said ' . $this->message]);
event(new WorkerResponse($this->job->getJobId(), $message));
}
}
WORKER-сервер: app\Events\WorkerResponse.php
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class WorkerResponse implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
protected $jobId;
public $message;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($jobId, $message)
{
$this->jobId = $jobId;
$this->message = $message;
}
/**
* Get the channels the event should broadcast on.
*
* @return Channel|array
*/
public function broadcastOn()
{
return new Channel('worker-response.' . $this->jobId);
}
}
API-сервер: socket.js (работает с узлом)
var server = require('http').Server();
var io = require('socket.io')(server);
var Redis = require('ioredis');
var redis = new Redis();
redis.psubscribe('worker-response.*');
redis.on('pmessage', function(pattern, channel, message) {
message = JSON.parse(message);
io.emit(channel + ':' + message.event, channel, message.data);
});
server.listen(3000);
API-сервер: resources\views\response.blade.php
<!doctype html>
<html lang="{{ config('app.locale') }}">
<head>
</head>
<body>
<div id="app">
<p>@{{ message }}</p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
<script>
var socket = io('http://192.168.10.10:3000');
new Vue({
el: '#app',
data: {
message: '',
},
mounted: function() {
socket.on('worker-response.{{ $jobId }}:App\\Events\\WorkerResponse', function (channel, data) {
this.message = data.message;
}.bind(this));
}
});
</script>
</body>
</html>