Írjunk alkalmazás szervert PHP-ban!
Azt hiszem, nem vagyok egyedül azzal az érzéssel, hogy módfelett böki a csőrömet a PHP-nak az a tulajdonsága, hogy bármilyen szuper rendszert is írunk, az inicializálás minden egyes lekérdezésre lefut. Az alternatív platformokon (Java, Python stb.) már találtak erre megoldásokat, de a PHP-s világban úgy tűnik, ez egyelőre nem szempont. Akit mégis zavar, tartson velem, nézzünk egy kisérleti megvalósítást.
Mi is az az alkalmazás szerver?
Először is tisztázzuk, hogy a Zend által „marketingbullshitelt” Zend Platform az nem application server, az egy sima webszerver PHP támogatással, csókolom.
Miután ezt leszögeztük, nézzük, hogy működik egy igazi application server.
- A szerver elindul daemonként.
- Betölti az alkalmazásunk kódját a memóriába.
- Példányosítja az alkalmazásunk egy megadott osztályát.
- Amikor jön egy lekérdezés, meghívja az említett osztály egy függvényét.
- Miután véget ért a lekérdezés, nem bontja le az osztályunkat.
Mint látható, ez gyökeres ellentétben van a PHP szokásos futásával, ahol a lekérdezés végével a betöltött osztályok kitakarodnak a memóriából, és hiába az opcode cache, a programnak újból le kell futnia, ami értékes processzoridőt jelent. Meg merném kockáztatni, hogy frameworktől függően a kód ¼ és ¾ közötti része fut le tökéletesen fölöslegesen.
Követelmények a megvalósítással szemben
Nézzük, hogy mire lesz szükségünk, ha PHP-ban szeretnénk valami hasonlót alkotni.
- Vagy írnunk kell egy saját webszervert, vagy valamilyen megoldást kell találnunk arra, hogyan injektáljuk be a webszerverre érkező lekérdezéseket.
- Daemonként kell futnia.
- Valamilyen úton-módon biztosítani kell a multiprocesszálás lehetőségét, ha kisérleti fázison túl akarjuk vinni a dolgot.
Ha a feladatot minél tisztábban szerettem volna megvalósítani, valószínűleg C-ben írtam volna modult a FastCGI handlerhez. Az egyszerűség kedvéért viszont inkább majdnem natív PHP-s megoldásokat kerestem:
- Mivel a HTTP requestek feldolgozása dupla munka lenne, és szinte biztos, hogy hibát vétenénk benne, úgy döntöttem, hogy a daemonunk egy Gearman job szerveren keresztül kapja meg a kéréseket. A webszerverben mindössze egyetlen egy PHP fájl fut, ami a környezeti változókat (
$_SERVER
,$_GET
,$_POST
,$_COOKIE
) szerializálja egy JSON tömbbe, majd beteszi a Gearman queue-ba. Ennek mellékhatásaként az egész rendszer szépen fog skálázódni, feltéve hogy a sessionök megosztását megoldjuk. - A daemonizált futás és a multiprocessing natívan is megoldható PHP-ból, de akár supervisord-t is használhatunk hozzá. Ha a natív megoldást használjuk, érdemes figyelni arra, hogy az alkalmazás inicializálása a forkolás után történjen, hogy minden szálnak meglegyen a saját MySQL kapcsolata. Erről részletesebben az Nagy terhelésű rendszerek fejlesztése c. cikksorozatban olvashattok.
Buktatók
Tekintettel arra, hogy itt az eddig ismert szekvenciális rövid lefutású programból hirtelen hosszú lefutású daemon-szál lesz, nem feltétlenül lehet a meglevő programokat hatékonyan átültetni erre az architektúrára. Nézzünk néhány buktatót.
- Gondoljuk végig, mik azok a szolgáltatások, amik több, mint egy requestig élhetnek (pl. MySQL kapcsolat)! Ezek nélkül szinte semmi értelme az application servernek.
- Felejtsük el az autoloadert! Az autoloader egy workaround a PHP performancia problémájára és itt lassítja a feldolgozást, nem gyorsítja. Az alkalmazásunk összes kódját töltsük be akkor, amikor a daemon meghívja azt!
- A globális változókra, singletonokra nagyon figyeljünk, mert azok a program futása után is ott maradhatnak! A memory leakeket próbáljuk meg felderíteni és kiküszöbölni!
- Ha egy változóra (osztály példányra stb.) nincs szükségünk, lehetőleg töröljük azt!
- Gondoljuk végig, hogy melyek az alkalmazásunk olyan részei, amiket kvázi globálisként deklaráltunk eddig, viszont csak egy lekérdezésre érvényesek! (Valószínűleg a session kezelés az egyik ilyen.)
- MySQL kapcsolatnál figyeljünk arra, hogy egy kapcsolat maximum 8 óráig élhet, ezután MySQL server has gone away hibaüzenetet kapunk. A disconnecteket kezelni kell, különben csúnya, megtalálhatatlan hibák lépnek föl!
Proof of Concept
Mindezen intelmek után nézzünk egy proof-of-concept megvalósítást. Kicsit savanyú, kicsit sárga, de a mienk. Ne tessék production kódban használni, csak az elmélet bizonyítására készült:
<?php
namespace \AppServer\Gearman {
class Worker {
protected $_worker;
protected $_class;
function __construct($servers, $function, $class) {
$this->_worker = new \GearmanWorker();
$this->_worker->addServers($servers);
$this->_worker->addFunction($function, array($this, "work"));
$this->_class = $class;
}
function run() {
$ret= $this->_worker->work();
if ($this->_worker->returnCode() != GEARMAN_SUCCESS) {
return false;
} else {
return true;
}
}
function work(\GearmanJob $job) {
$workload = json_decode(base64_decode($job->workload()), true);
if (array_key_exists('server', $workload)) {
$server = new \AppServer\HTTP\Server($workload['server']);
}
if (array_key_exists('get', $workload)) {
$get = new \AppServer\HTTP\Get($workload['get']);
}
if (array_key_exists('post', $workload)) {
$post = new \AppServer\HTTP\Post($workload['post']);
}
if (array_key_exists('cookie', $workload)) {
$cookie = new \AppServer\HTTP\Cookie($workload['cookie']);
}
$response = new \AppServer\HTTP\Response();
$obj = new $this->_class();
$obj->run($server, $get, $post, $cookie, $response);
$responsedata = array(
"status" => $response->getStatusCode(),
"headers" => $response->getHeaders(),
"body" => $response->getBody()
);
return base64_encode(json_encode($responsedata));
}
}
}
namespace \AppServer\HTTP {
interface Servlet {
function run(\AppServer\HTTP\Server $server,
\AppServer\HTTP\Get $get,
\AppServer\HTTP\Post $post,
\AppServer\HTTP\Cookie $cookie,
\AppServer\HTTP\Response $response
);
}
class Response {
protected $_statusCode = 200;
protected $_headers = array();
protected $_cookies = array();
protected $_body = "";
function getStatusCode() {
return $this->_statusCode;
}
function setStatusCode($code) {
$this->_statusCode = $code;
}
function setHeader($name, $value, $replace = false) {
if (!isset($this->_headers[$name]) || $replace) {
$this->_headers[$name] = $value;
}
}
function getHeader($name) {
if (isset($this->_headers[$name])) {
return $this->_headers[$name];
} else {
return null;
}
}
function getHeaders() {
return $this->_headers;
}
function clearHeaders() {
$this->_headers = array();
}
function setCookie($key, $value = "", $expire = false, $path = "", $domain = "", $secure = false, $httponly = false) {
$this->_cookies[$key] = array(
"value" => $value,
"expire" => $expire,
"path" => $path,
"domain" => $domain,
"secure" => (bool)$secure,
"httponly" => (bool)$httponly);
}
function getCookie($key) {
if (in_array($key, $this->_cookies)) {
return $this->_cookies[$key];
} else {
return null;
}
}
function getCookies() {
return $this->_cookies;
}
function setBody($content) {
$this->_body = $content;
}
function getBody() {
return $this->_body;
}
function clearBody() {
$this->_body = "";
}
}
abstract class Raw {
protected $_data = array();
function __construct($data) {
$this->_data = $data;
}
function __get($key) {
return $this->_data[$key];
}
function __set($key, $value) {
return $this->_data[$key];
}
function __isset($key) {
return isset($this->_data[$key]);
}
function __unset($key) {
unset($this->_data[$key]);
}
}
class Cookie extends Raw { }
class Get extends Raw { }
class Post extends Raw { }
class Server extends Raw { }
}
namespace Application {
class Test implements \AppServer\HTTP\Servlet {
function run(\AppServer\HTTP\Server $server,
\AppServer\HTTP\Get $get,
\AppServer\HTTP\Post $post,
\AppServer\HTTP\Cookie $cookie,
\AppServer\HTTP\Response $response
) {
$response->setBody("Test");
}
}
}
namespace {
$terminate = false;
$worker = new \AppServer\Gearman\Worker(getenv('GEARMAN_SERVERS'), getenv('GEARMAN_FUNCTION'), getenv('CLASS'));
while (!$terminate) {
try {
$worker->run();
} catch (\Exception $e) { }
}
}
Az ehhez tartozó webszerver oldali kód:
<?php
function getGearmanServers() {
$servers=getenv("GEARMAN_SERVERS");
$servers = explode(";", $servers);
if (!count($servers)) {
$servers = array("127.0.0.1:4730");
}
shuffle($servers);
return $servers;
}
function getGearmanFunction() {
$func=getenv("GEARMAN_FUNCTION");
if (!$func) {
$func = "serve";
}
return $func;
}
function hardFail($message) {
header("HTTP/1.1 500 Internal Server Error");
$errFile = dirname(__FILE__) . "/500.html";
if (!file_exists($errFile) || !include($errFile)) {
echo("<h1>Internal Server Error</h1>");
}
exit(500);
}
function processResult($result) {
$statusCodeMap = array(
100 => "Continue", 101 => "Switching Protocols",
200 => "OK", 202 => "Accepted",
203 => "Non-Authoritative Information", 204 => "No Content",
205 => "Reset Content", 206 => "Partial Content",
300 => "Multiple Choices", 301 => "Moved Permanently",
302 => "Found", 303 => "See Other", 304 => "Not Modified",
305 => "Use proxy", 307 => "Temporary Redirect",
400 => "Bad Request", 401 => "Unauthorized",
403 => "Forbidden", 404 => "Not Found",
405 => "Method Not Allowed", 406 => "Not Acceptable",
407 => "Proxy Authentication Required",
408 => "Request Timeout", 409 => "Conflict", 410 => "Gone",
411 => "Length Required", 412 => "Precondition Failed",
413 => "Request Entity Too Large",
414 => "Request-URI Too Long", 415 => "Unsupported Media Type",
416 => "Request Range Not Satisfiable",
417 => "Expectation Failed", 500 => "Internal Server Error",
501 => "Bad Gateway", 503 => "Service Unavailable",
504 => "Gateway Timeout", 505 => "HTTP Version Not Supported"
);
if (array_key_exists($result['status'], $statusCodeMap)) {
header("HTTP/1.1 " . $result['status'] . " " . $statusCodeMap[$result['status']]);
} else {
header("HTTP/1.1 200 OK");
}
foreach ($result['headers'] as $key => $value) {
header($key . ": " . $value);
}
echo($result['body']);
}
$func = getGearmanFunction();
$gc = new GearmanClient();
$gc->addServers(implode(",", getGearmanServers()));
$response = $gc->do($func, base64_encode(json_encode(array("server" => $_SERVER, "get" => $_GET, "post" => $_POST, "cookie" => $_COOKIE))));
$response = json_decode(base64_decode($response), true);
if (is_array($response)) {
processResult($response);
} else {
hardFail("Invalid response");
}
Megjegyzés: azért base64 encode-oltam a kommunikációt, mert a jelek szerint a béta állapotban levő PECL Gearman csomagnak problémái vannak a JSON válaszban a } jellel. Van natív PHP-s Gearman osztály, azt azonban nem teszteltem.
Namost, a kis feldolgozónkat ezek után így kell indítani:
$ GEARMAN_SERVERS=127.0.0.1 GEARMAN_FUNCTION=siteneve CLASS="\Application\Test" php minified.php
Letesztelni az index.php parancssori futtatásával futjuk:
$ GEARMAN_SERVERS=127.0.0.1 GEARMAN_FUNCTION=siteneve php index.php
Ha webszerverből szeretnénk indítani, akkor a következő a GEARMAN_SERVERS
és GEARMAN_FUNCTION
változókat SetEnv
vel be kell állítani.
Konklúzió, linkek
Mindez a gyakorlatban működik. Hogy hoz-e sebességbeli növekedést, mindenkinek magának kell eldöntenie. A Gearman egy stabil jószág, ennél sokkal, sokkal többet tud. Írjátok meg a tapasztalataitokat!
Hasznos linkek:
■
Szép
Ha már ilyen magasságokban járunk érdemes esetleg meglesni ezt:
http://code.google.com/p/kargo-event/
Le a kalappal proclub!
A Gearmant még soha nem
Köszönöm, úgy érzem, okosabb
klassz
Gyere gyakrabban
autoload
Az autoload az sohasem performancia problémákat volt hivatott megoldani, és minden esetben lassabb, mint egy fix include.
valoszinuleg lazy loadingot
Tyrael