ݺߣ

ݺߣShare a Scribd company logo
ReactPHP + Symfony = PROFIT
aneb 1000req/s s minimálními nároky na server
1. sraz přátel Symfony v Praze (29.10.2015)
Skrz.cz hlídá každé uživatelovo prohlédnutí nabídky. Jsou to miliony
pidirequestů denně. Použít PHP-FPM by znamenalo zbytečně další
server(y). ReactPHP díky asynchronnímu IO dovoluje s minimálními
nároky zpracovávat tisíce req/s. Nechtěli jsme se vzdát Symfony, a
tak vznikl bridge mezi Symfony a asynchronním světem ReactPHP.
Slovníček
• klik = najedu myší na nabídku a zmáčknu tlačítko
• imprese = podíval jsem se na nabídku

(alespoň polovina nabídky byla ve viewportu
alespoň jednu sekundu)
• CTR (click-through rate) = kliky / imprese
Průměrné CTR display reklamy v ČR je 0.08%
(viz http://www.richmediagallery.com/tools/
benchmarks). Když máte 1 klik za sekundu,
každou sekundu k němu přijde ještě přes 1000
impresí.
Takovýhle banner můžete vidět třeba na
Novinky.cz. Jedná se právě o tu
“display reklamu”. Tady Skrz měří tisíce
impresí za sekundu.
Uvnitř Skrzu se opět měří každé zobrazení. Tam je
analytika o to složitější, že se zobrazení musí správně
napárovat na plochu, kde k němu došlo (boxík “Moje
navštívené”, boxík “Nejprodávanější”, ostatní výpisy, s
každou novou feature plochy vznikají a zanikají).
Impresí už není tolik, zato obsahují více dat. Taky má
Skrz řádově lepší CTR, a tudíž více prokliků.
ReactPHP
(neplést s ReactJS!)
http://reactphp.org/
(https://github.com/jakubkulhan/hit-server-bench)
Impresí je tedy hodně. Ale i ty “velké” na Skrzu jsou pořád malinkaté requesty. Největší
zátěz je na IO (čtení/zápis do databáze, resp. čtení/zápis do RabbitMQ).

Řešil jsem, co použít pro takového jednoduché “hitování” serveru. Performance výsledky
k porovnání jsou v odkazovaném repozitáři “hit-server-bench”.

Jelikož PHP a ReactPHP zvládaly dostatečný počet req/s a datový model byl již udělán v
PHP, vyplatilo se zainvestovat do ReactPHP - mohou se používat stejné objekty jako ve
zbytku aplikace. Nechtělo se mi vzdát Symfony dependency injection containeru a
routingu, a tak vznikl bridge mezi ReactPHP a Symfony.
ReactPHP: req, res → λ → void
Symfony: req → λ → res
❓
Problém se vyskytl hned na začátku. Zatímco
ReactPHP předá fci pro zpracování requestu 2
objekty - request a response a nic neočekává na
výstupu; Symfony proteče request a na výstupu
je očekávána response.
req → λ → promise[res]
❗
Řešení se ukázalo jednoduché. Symfony
na výstupu vydá “promise” - objekt, který
zastupuje výsledek výpočtu, který třeba
ještě ani nemusel proběhnout. V ReactPHP
se počká na výsledek promisu a ten se
poté zapíše do response objektu.
github.com/jakubkulhan/
reactphp-symfony
Pro kompletní příklad se podívejte na
můj GitHub. Bude následovat několik
slajdu s ukázkami kódu právě z tohodle
repozitáře.
Boot
$kernel = new AppKernel(
$environment = $input->getOption("environment"),
$environment !== "prod"
);
$kernel->boot();
$loop = Factory::create();
/** @var Container $container */
$container = $kernel->getContainer();
$container->set("react.loop", $loop);
$socket = new Socket($loop);
$http = new Server($socket);
$http->on("request", function (
Request $request,
Response $response
) use ($kernel, $loop) {
// ...
});
$socket->listen(
$port = $input->getOption("port"),
$host = $input->getOption("host")
);
echo "Listening to {$host}:{$port}n";
$loop->run();
ReactPHP → Symfony
$headers = $request->getHeaders();
$cookies = [];
if (isset($headers["Cookie"])) {
foreach ((array)$headers["Cookie"] as $cookieHeader) {
foreach (explode(";", $cookieHeader) as $cookie) {
list($name, $value) = explode("=", trim($cookie), 2);
$cookies[$name] = urldecode($value);
}
}
}
$symfonyRequest = new SymfonyRequest(
$request->getQuery(),
[], // TODO: handle post data
[],
$cookies,
[],
[
"REQUEST_URI" => $request->getPath(),
"SERVER_NAME" => explode(":", $headers["Host"])[0],
"REMOTE_ADDR" => $request->remoteAddress,
"QUERY_STRING" => http_build_query($request->getQuery()),
],
null // TODO: handle post data
);
$symfonyRequest->headers->replace($headers);
$symfonyResponse = $kernel->handle($symfonyRequest);
if ($kernel instanceof TerminableInterface) {
$kernel->terminate($symfonyRequest, $symfonyResponse);
}
Symfony → ReactPHP
if ($symfonyResponse instanceof PromiseInterface) {
$symfonyResponse->then(function (SymfonyResponse $symfonyResponse) use ($response) {
$this->send($response, $symfonyResponse);
}, function ($error) use ($loop, $response) {
echo "Exception: ", (string) $error, "n";
$response->writeHead(500, ["Content-Type" => "text/plain"]);
$response->end("500 Internal Server Error");
$loop->stop();
});
} elseif ($symfonyResponse instanceof SymfonyResponse) {
$this->send($response, $symfonyResponse);
} else {
echo "Unsupported response type: ", get_class($symfonyResponse), "n";
$response->writeHead(500, ["Content-Type" => "text/plain"]);
$response->end("500 Internal Server Error");
$loop->stop();
}
Symfony → ReactPHP (2)
private function send(Response $res, SymfonyResponse $symfonyResponse)
{
$headers = $symfonyResponse->headers->allPreserveCase();
$headers["X-Powered-By"] = "Love";
$cookies = $symfonyResponse->headers->getCookies();
if (count($cookies)) {
$headers["Set-Cookie"] = [];
foreach ($symfonyResponse->headers->getCookies() as $cookie) {
$headers["Set-Cookie"][] = (string)$cookie;
}
}
$res->writeHead($symfonyResponse->getStatusCode(), $headers);
$res->end($symfonyResponse->getContent());
}
Controller
/**
* @Controller
*/
class IndexController
{
/**
* @var LoopInterface
*
* @Autowired
*/
public $loop;
public function indexAction(Request $request)
{
return Response::create("Hello, world!n");
}
public function promiseAction(Request $request)
{
$secs = intval($request->attributes->get("secs"));
$deferred = new Deferred();
$this->loop->addTimer($secs, function () use ($secs, $deferred) {
$deferred->resolve(Response::create("{$secs} seconds later...n"));
});
return $deferred->promise();
}
}
Knihovny
• ReactPHP (např. HTTP klient, ZeroMQ)

https://github.com/reactphp
• MySQL

https://github.com/kaja47/async-mysql

https://github.com/KhristenkoYura/react-mysql

https://github.com/bixuehujin/reactphp-mysql
• Redis

https://github.com/nrk/predis-async
• RabbitMQ

https://github.com/jakubkulhan/bunny
V ReactPHP je potřeba používat speciální
knihovny, které využijí asynchronicity
(použitím synchronní knihovny byste úplně
znegovaly výhody, které ReactPHP má.)

Tučně jsou zvýrazněny ty, co má Skrz
nasazeny v produkci.
Díky!
Otázky?
Dobrá otázka byla: “Použil bys ReactPHP a
Symfony znovu, kdybys stejnou aplikaci stavěl
teď?”

Je důležité uvědomit si, že v době psaní aplikace
(říjen/listopad 2014), byl stack ve Skrzu PHP-only.
Jelikož ReactPHP splňoval výkonové požadavky,
dávalo smysl neuhýbat od PHP. V situaci, co jsme
byli, bych se opět rozhodl stejně.

Od té doby však ve Skrzu přibyl do stack ještě
Golang. Dnes bych již tuhle aplikaci pro sledování
impresí napsal v Golangu.

More Related Content

ReactPHP + Symfony = profit aneb 1000req/s přes Symfony s minimálními nároky na server (1. sraz přátel Symfony v Praze, 29.10.2015)

  • 1. ReactPHP + Symfony = PROFIT aneb 1000req/s s minimálními nároky na server 1. sraz přátel Symfony v Praze (29.10.2015) Skrz.cz hlídá každé uživatelovo prohlédnutí nabídky. Jsou to miliony pidirequestů denně. Použít PHP-FPM by znamenalo zbytečně další server(y). ReactPHP díky asynchronnímu IO dovoluje s minimálními nároky zpracovávat tisíce req/s. Nechtěli jsme se vzdát Symfony, a tak vznikl bridge mezi Symfony a asynchronním světem ReactPHP.
  • 2. Slovníček • klik = najedu myší na nabídku a zmáčknu tlačítko • imprese = podíval jsem se na nabídku
 (alespoň polovina nabídky byla ve viewportu alespoň jednu sekundu) • CTR (click-through rate) = kliky / imprese Průměrné CTR display reklamy v ČR je 0.08% (viz http://www.richmediagallery.com/tools/ benchmarks). Když máte 1 klik za sekundu, každou sekundu k němu přijde ještě přes 1000 impresí.
  • 3. Takovýhle banner můžete vidět třeba na Novinky.cz. Jedná se právě o tu “display reklamu”. Tady Skrz měří tisíce impresí za sekundu.
  • 4. Uvnitř Skrzu se opět měří každé zobrazení. Tam je analytika o to složitější, že se zobrazení musí správně napárovat na plochu, kde k němu došlo (boxík “Moje navštívené”, boxík “Nejprodávanější”, ostatní výpisy, s každou novou feature plochy vznikají a zanikají). Impresí už není tolik, zato obsahují více dat. Taky má Skrz řádově lepší CTR, a tudíž více prokliků.
  • 5. ReactPHP (neplést s ReactJS!) http://reactphp.org/ (https://github.com/jakubkulhan/hit-server-bench) Impresí je tedy hodně. Ale i ty “velké” na Skrzu jsou pořád malinkaté requesty. Největší zátěz je na IO (čtení/zápis do databáze, resp. čtení/zápis do RabbitMQ). Řešil jsem, co použít pro takového jednoduché “hitování” serveru. Performance výsledky k porovnání jsou v odkazovaném repozitáři “hit-server-bench”. Jelikož PHP a ReactPHP zvládaly dostatečný počet req/s a datový model byl již udělán v PHP, vyplatilo se zainvestovat do ReactPHP - mohou se používat stejné objekty jako ve zbytku aplikace. Nechtělo se mi vzdát Symfony dependency injection containeru a routingu, a tak vznikl bridge mezi ReactPHP a Symfony.
  • 6. ReactPHP: req, res → λ → void Symfony: req → λ → res ❓ Problém se vyskytl hned na začátku. Zatímco ReactPHP předá fci pro zpracování requestu 2 objekty - request a response a nic neočekává na výstupu; Symfony proteče request a na výstupu je očekávána response.
  • 7. req → λ → promise[res] ❗ Řešení se ukázalo jednoduché. Symfony na výstupu vydá “promise” - objekt, který zastupuje výsledek výpočtu, který třeba ještě ani nemusel proběhnout. V ReactPHP se počká na výsledek promisu a ten se poté zapíše do response objektu.
  • 8. github.com/jakubkulhan/ reactphp-symfony Pro kompletní příklad se podívejte na můj GitHub. Bude následovat několik slajdu s ukázkami kódu právě z tohodle repozitáře.
  • 9. Boot $kernel = new AppKernel( $environment = $input->getOption("environment"), $environment !== "prod" ); $kernel->boot(); $loop = Factory::create(); /** @var Container $container */ $container = $kernel->getContainer(); $container->set("react.loop", $loop); $socket = new Socket($loop); $http = new Server($socket); $http->on("request", function ( Request $request, Response $response ) use ($kernel, $loop) { // ... }); $socket->listen( $port = $input->getOption("port"), $host = $input->getOption("host") ); echo "Listening to {$host}:{$port}n"; $loop->run();
  • 10. ReactPHP → Symfony $headers = $request->getHeaders(); $cookies = []; if (isset($headers["Cookie"])) { foreach ((array)$headers["Cookie"] as $cookieHeader) { foreach (explode(";", $cookieHeader) as $cookie) { list($name, $value) = explode("=", trim($cookie), 2); $cookies[$name] = urldecode($value); } } } $symfonyRequest = new SymfonyRequest( $request->getQuery(), [], // TODO: handle post data [], $cookies, [], [ "REQUEST_URI" => $request->getPath(), "SERVER_NAME" => explode(":", $headers["Host"])[0], "REMOTE_ADDR" => $request->remoteAddress, "QUERY_STRING" => http_build_query($request->getQuery()), ], null // TODO: handle post data ); $symfonyRequest->headers->replace($headers); $symfonyResponse = $kernel->handle($symfonyRequest); if ($kernel instanceof TerminableInterface) { $kernel->terminate($symfonyRequest, $symfonyResponse); }
  • 11. Symfony → ReactPHP if ($symfonyResponse instanceof PromiseInterface) { $symfonyResponse->then(function (SymfonyResponse $symfonyResponse) use ($response) { $this->send($response, $symfonyResponse); }, function ($error) use ($loop, $response) { echo "Exception: ", (string) $error, "n"; $response->writeHead(500, ["Content-Type" => "text/plain"]); $response->end("500 Internal Server Error"); $loop->stop(); }); } elseif ($symfonyResponse instanceof SymfonyResponse) { $this->send($response, $symfonyResponse); } else { echo "Unsupported response type: ", get_class($symfonyResponse), "n"; $response->writeHead(500, ["Content-Type" => "text/plain"]); $response->end("500 Internal Server Error"); $loop->stop(); }
  • 12. Symfony → ReactPHP (2) private function send(Response $res, SymfonyResponse $symfonyResponse) { $headers = $symfonyResponse->headers->allPreserveCase(); $headers["X-Powered-By"] = "Love"; $cookies = $symfonyResponse->headers->getCookies(); if (count($cookies)) { $headers["Set-Cookie"] = []; foreach ($symfonyResponse->headers->getCookies() as $cookie) { $headers["Set-Cookie"][] = (string)$cookie; } } $res->writeHead($symfonyResponse->getStatusCode(), $headers); $res->end($symfonyResponse->getContent()); }
  • 13. Controller /** * @Controller */ class IndexController { /** * @var LoopInterface * * @Autowired */ public $loop; public function indexAction(Request $request) { return Response::create("Hello, world!n"); } public function promiseAction(Request $request) { $secs = intval($request->attributes->get("secs")); $deferred = new Deferred(); $this->loop->addTimer($secs, function () use ($secs, $deferred) { $deferred->resolve(Response::create("{$secs} seconds later...n")); }); return $deferred->promise(); } }
  • 14. Knihovny • ReactPHP (např. HTTP klient, ZeroMQ)
 https://github.com/reactphp • MySQL
 https://github.com/kaja47/async-mysql
 https://github.com/KhristenkoYura/react-mysql
 https://github.com/bixuehujin/reactphp-mysql • Redis
 https://github.com/nrk/predis-async • RabbitMQ
 https://github.com/jakubkulhan/bunny V ReactPHP je potřeba používat speciální knihovny, které využijí asynchronicity (použitím synchronní knihovny byste úplně znegovaly výhody, které ReactPHP má.) Tučně jsou zvýrazněny ty, co má Skrz nasazeny v produkci.
  • 15. Díky! Otázky? Dobrá otázka byla: “Použil bys ReactPHP a Symfony znovu, kdybys stejnou aplikaci stavěl teď?” Je důležité uvědomit si, že v době psaní aplikace (říjen/listopad 2014), byl stack ve Skrzu PHP-only. Jelikož ReactPHP splňoval výkonové požadavky, dávalo smysl neuhýbat od PHP. V situaci, co jsme byli, bych se opět rozhodl stejně. Od té doby však ve Skrzu přibyl do stack ještě Golang. Dnes bych již tuhle aplikaci pro sledování impresí napsal v Golangu.