Подписка

Шустрые фреймворки-расширения (продолжение)

В предыдущей статье, мы создали базовое приложение-блог на PhalconPHP, способное работать с базой данных и выводить записи в виде списка. Ниже закончим разработку функционала приложения и проведем испытания производительности, для проверки скорости работы блога.

Мы остановились на странице, которая выводила список постов, добавим к этому списку постраничную навигацию. Phalcon имеет встроенный пейджинатор, который отлично подходит для нашей задачи.
$page = (int) $_GET["page"];
$paginator = Phalcon_Paginator::factory("Model", array(
   "data" => Post::find(array("order" => "created DESC")),
   "limit"=> 10,
   "page" => $page == 0 ? 1 : $page
));
Номер страницы будем брать из параметров запроса, поэтому перед инициализацией пейджинатора нужно получить номер текущей. Далее создаем объект, которому указываем откуда брать записи, количество записей на странице и номер текущей страницы.
Так как теперь данные мы получаем не напрямую, а через пейджинатор, нужно изменить строку, которая передавала посты в вид на новую:
$this->view->setVar('posts', $paginator->getPaginate());
А в вид который выводит список постов нужно добавить элементы постраничной навигации:
<div style="text-align:center">
   <?php if ($posts->current > 1):?>
      <?=Phalcon_Tag::linkTo("?page=".$posts->before, "Previous") ?>
   <?php endif;?>
   <?=Phalcon_Tag::linkTo("?page=".$posts->next, "Next") ?>
   <?php echo "You are in page ", $posts->current, " of ", $posts->total_pages ?>
</div>
Постраничная навигация готова.
Теперь сделаем страницу для отображения поста, для этого создадим новый контроллер PostContoller:
app/controllers/PostController.php
class PostController extends Phalcon_Controller {
   public function indexAction() {
      $id = (int) $_GET["id"];
      if ($id > 0) {
         $post = Post::findFirst($id);
         if ($post) {
            $this->view->setVar('post', $post);  
            return;
         }
      }
      throw new Exception('Can\'t find post with id #'.$id);
   }
}
Идентификатор поста передается GET параметром id, который мы получаем и проверяем на корректность, после чего извлекаем пост и базы. Если пост с указанным идентификатором найден - передаем его в вид, в противном случае ругаемся ексепшеном.
И конечно же вид, отображающий пост:
app/views/post/index.phtml
<h1><?php print $post->name;?></h1>
<div><?php print $post->body;?></div>
Минимальный функционал недоблога уже готов. Добавим комментарии.
Комментарий будет содержать только текст, без идентификации автора и заголовка. Модель мы сделали раньше, создадим форму для добавления комментария и контроллер, который будет сохранять комментарий в базе.
Форму вставим под текстом поста:
<form method="POST" action="/post/addcomment">
<textarea name="body" style="width:100%;height:50px;border:1px solid grey;"></textarea>
   <input type="hidden" name="post" value="<?php print $post->id;?>"/>
   <input type="submit" value="Comment"/>
</form>
В форме кроме текста комментария нужно передать идентификатор текущего поста, чтобы знать к чему "прикрепить" комментарий.
В PostController добавим действие addcommentAction, которое будет получать данные формы и создавать комментарий:
public function addcommentAction() {
   $body = strip_tags($_POST['body']);
   $postId = (int) $_POST['post'];
   if (!empty($body) && $postId > 0) {
      $comment = new Comment();
      $comment->post_id = $postId;
      $comment->body = $body;
      $comment->save();
   }
   header('Location: /post/?id='.$postId.'&res=saved');
}
Теперь комментарии добавляются, но не выводятся, чтобы вывести список комментариев поста добавим в конец app/post/index.phtml:
<?php foreach ($post->getComment() as $comment):?>
   <div style="padding:10px;margin:10px;border:1px solid green">
<?php print $comment->body;?></div>
<?php endforeach;?>
И последний штрих, добавим блоки "Последние посты", "Случайные посты", "Последние комментарии", "Случайные комментарии" на главную страницу. Для этого получим в контроллере необходимые данные и передадим их в вид:
app/contollers/IndexController.php
$this->view->setVar('randomPosts', Post::find(array("order" => "RAND()", 'limit' => 10)));
   $this->view->setVar('topPosts', Post::find(array("order" => "created DESC", 'limit' => 10)));
   $this->view->setVar('randomComments', Comment::find(array("order" => "RAND()", 'limit' => 5)));
   $this->view->setVar('lastComments', Comment::find(array("order" => "id DESC", 'limit' => 5)));
Выведем полученные данные в колонку справа:
<div style="float:right; width: 200px">
 <div>
  <h3>Random posts</h3>
  <?php foreach($randomPosts as $item): ?>
   <div>
    <?=Phalcon_Tag::linkTo("post/?id=".$item->id, $item->name) ?> 
    (<?php print count($item->getComment());?>)
   </div>
  <?php endforeach;?>
 </div>
 <div>
  <h3>Last posts</h3>
  <?php foreach($topPosts as $item): ?>
   <div>
    <?=Phalcon_Tag::linkTo("post/?id=".$item->id, $item->name) ?> 
    (<?php print count($item->getComment());?>)
   </div>
  <?php endforeach;?>
 </div>
 <div>
  <h3>Random comments</h3>
  <?php foreach($randomComments as $item): ?>
   <div>
    <?=Phalcon_Tag::linkTo("post/?id=".$item->getPost()->id, $item->getPost()->name) ?> 
    -> <?php print $item->body;?>
   </div>
  <?php endforeach;?>
 </div>
 <div>
  <h3>Last comments</h3>
  <?php foreach($lastComments as $item): ?>
    <div>
    <?=Phalcon_Tag::linkTo("post/?id=".$item->getPost()->id, $item->getPost()->name) ?> 
    -> <?php print $item->body;?>
   </div>
  <?php endforeach;?>
 </div>
</div>
Мини-блог готов!

Теперь самое интересное - производительность.
Перед тем как проверять скорость работы, нужно добавить данных, чтобы симулировать работу в реальных условиях. Конечно же добавлять вручную не вариант, поэтому добавим в один из контроллеров метод, который будет генерировать посты и комментарии к ним:
app/controllers/IndexController.php
public function generateAction() {
 $text = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.';
 for ($i = 0; $i < 500; $i++) {
  $post = new Post();
  $post->name = 'Post #'.mt_rand(100000, 999999);
  $post->body = $text;
  $post->created = date('Y-m-d H:i');
  if ($post->save()) {
   $createdPost = Post::findFirst(array("order" => "id DESC"));
   $postId = $createdPost->id;
   $numberOfComments = mt_rand(50, 100);
   for ($j = 0; $j < $numberOfComments; $j++) {
    $comment = new Comment();
    $comment->post_id = (int) $postId;
    $comment->body = substr($text, mt_rand(10, 200), mt_rand(10,200));
    if (!$comment->save()) {
     print_r($comment->getMessages());exit;
    }
   }
  }
 }
}
При вызове /index/generate будет автоматически сгенерировано 500 постов с 50-100 комментариев к каждому.

Статистика

Для измерения производительности будем использовать утилиту Siege, при помощи которой будем симулировать параллельную работу нескольких пользователей и определять время генерации страницы. Siege хороша тем, что умеет принимать файл со списком ссылок, по которым она будет ходить (sitemap).
Напишем действие, которое будет генерировать все такие ссылки, а именно страницы постов и ссылки постраничной навигации:
app/controllers/IndexController.php
public function sitemapAction() {
 $posts = Post::find(array("order" => "created DESC"));
 $baseUrl = 'http://'.$_SERVER['HTTP_HOST'].'/';
 //List pages
 $pagesNumber = ceil(count($posts)/10);
 for ($i = 1; $i < $pagesNumber; $i++) {
  echo $baseUrl.'?page='.$i."\n";
 }
 //Post pages
 foreach ($posts as $post) {
  echo $baseUrl.'post/?id='.$post->id."\n";
 }
}
Теперь выполним /index/sitemap и сохраним полученный результат в файл, например links.txt.

Все, с подготовками закончили, можно начинать тестирование. Запускать siege желательно на другой машине, потому то утилита использует немало ресурсов и может повлиять на результат отбирая их у сайта. Проведем нагрузочное тестирование 10 одновременных пользователей на протяжении 1 минуты, используя сгенерированые ссылки, выбранные случайным образом:
siege -c 10 -i -t 1m -f links.txt
В результате, siege на протяжении минуты будет открывать указанные в файле ссылки в 10 потоках, симулируя постоянную нагрузку 10 пользователей. По завершению получим отчет:
Transactions:          1030 hits
Availability:        100.00 %
Elapsed time:         59.94 secs
Data transferred:         1.04 MB
Response time:          0.08 secs
Transaction rate:        17.18 trans/sec
Throughput:          0.02 MB/sec
Concurrency:          1.41
Successful transactions:        1030
Failed transactions:            0
Longest transaction:         1.63
Shortest transaction:         0.00
Нас интересует параметр "Response time" - среднее время генерации страницы - измерим его для разного количества параллельных пользователей и посмотрим динамику изменения.

Итак, ниже график зависимости времени потраченного на обработку запроса от количества одновременных пользователей.

Начиная с 60 пользователей, сервер перестал справляться с нагрузкой и часть соединений вылетала таймаутом.
Как видно из графика, время генерации страницы ростет линейно, что конечно же является хорошим показателем.

Очевидно, что слабым местом нашего приложения является база данных, не способная быстро обрабатывать большое количество запросов при высоких нагрузках. Модель не поддерживает кеширование запросов, но согласно документации, фреймворк имеет встроенную систему кеширования, умеющею кешировать как в файлы, так и во внешние системы типа Memcache. К сожалению заставить ее работать у меня не получилось из-за ошибки в фреймворке при инициализации кеша.

Заключение

С одной стороны фреймворк хорош и быстр, поддерживает неплохой набор функций и MVC, но молодость дает о себе знать. Примеры описанные в документации не всегда работают, а найти о нем информацию в интернете практически нереально. Кроме того исправить ошибку в фреймворке не каждому под силу, из-за необходимости знания С и технологий написания расширений.

Резюмируя могу только пожелать авторам PhalconPHP сил и терпения, для придания товарного вида довольно интересной идее.

Если пост понравился, можете нажать на гугловский +1, мне будет приятно.
@kkooler

@kkooler

Занимаюсь разработкой высоконагруженных проектов и распределенных систем на PHP.
В свободное время разрабатываю нано-проекты:

Следить за блогом

RSS канал Twitter