CodeIgniter

Queue & Cron

수업소개

이번 수업에서는 큐(Queue)와 크론(Cron) 그리고 배치(Batch)라는 것에 대해서 알아본다. 이것을 통해서 사용자 입장에서는 더 빠르게, 시스템의 입장에서는 더 효율적으로 무거운 작업을 처리하는 방법을 알아보자.

하드코어한 상황

서비스에는 복잡하고, 많은 시간을 요구하는 작업들이 있다. 이를테면 동영상을 서비스한다고 해보자. 동영상은 인코딩이라는 과정을 거쳐서 인터넷을 통해서 서비스되게 된다. 그런데 인코딩이라는 과정이 상당히 많은 시간이 소비되는 작업이다. 그렇다고 사용자를 인코딩이 끝날 때까지 기다리게 할 수는 없는 노릇이다. 이런 경우 업로드가 끝나면 사용자와의 연결을 일단 끊고 백그라운드로 인코딩을 진행한 후에 작업이 끝나면 이메일을 보내는 것과 같은 피드백을 통해서 작업이 종료 되었음을 사용자가 인지할 수 있게 하면 된다. 이렇게 작업을 처리하는 것을 배치(Batch)라고 한다. 

우리의 상황

우리가 지금까지 만은 웹에플리케이션의 사용자가 1만명이 됐다. 그럼 글을 하나 작성할 때마다 1만명에게 이메일을 발송해야 하는데, 인당 1초의 시간이 걸린다고해도 1만명에게 이메일을 보내려면 1만 초 시간으로 환산하면 2시간이 걸린다. 그럼 글 하나를 작성 할 때마다 사용자가 2시간을 기다리게 하면 어떻게 될까? 사용자에게 버림 받을 것이다. 그리고 이렇게 과중한 작업을 만약 1만명의 사용자가 동시에 요청한다면 어떻게 될까? 장비에게서 배신을 당할 것이다. 지금부터 배치작업을 우리 상황에 적용해보자. 우선 알아야 할 몇가지 개념이 있다. 

동기적 VS 비동기적

동기적(synchronization)이란 어떤 일을 처리함에 있어서 한번에 하나의 일만을 처리하는 것을 의미한다. 말이 어렵지만 사실 별거는 아니다. 이를테면 1만명에게 이메일 전송을 끝낼 때까지 사용자를 붙잡고 있다면 이것은 동기적인 것이다. 하지만 일단 그런 일을 해야 한다는 것을 파일이나 데이터베이스에 기록했다가 백그라운드로 그 일을 처리한다면 이것은 비동기적(asynchronization)인 것이 된다. 그럼 사용자는 이메일이 전송되고 있는 동안에도 계속해서 서비스를 사용할 수 있게 되는 것이다. 나중에 해야 할 일을 어디에 어떻게 기록할 것인지, 그것을 누가 처리할 것인지를 뒤에서 살펴본다.

동기적인 처리 (현행)
 
비동기적 처리 (개선후)

Queue

그럼 처리해야 할 일을 기록했다고 차자. 이것을 어떻게 처리해야 할까? 먼저 기록한 일부터 하나씩 차례 차례 처리하면 된다. 이런 것을 큐(Queue)라고 한다. 아래 그림은 큐가 왜 필요한지를 설명하는 그림이다. 

우리는 해야 할 일을 데이터베이스에 기록할 것이다. 데이터베이스는 데이터를 입력하고 출력하는 것이 쉽고, 또 큐를 처리할 머신을 여러대로 확장했을 때도 쉽게 접근 할 수 있다는 장점이 있다. 데이터베이스 이외에도 파일에 큐를 기록하는 것도 가능하고, 또 큐를 전문적으로 관리해주는 소프트웨어들도 많이 있다. 아래 URL을 방문해보면 이런 서비스로는 어떤 것이 있고, 각각은 어떤 특징이 있는지를 살펴볼 수 있다. stackoverflow.com

Batch

배치(batch)란 일괄적으로 작업을 처리한다는 의미다. 예제에서 토픽 저장을 할 때마다 해당 작업을 처리하는 것이 아니라 이메일을 발송해야 한다는 사실만 일단 기록해두고, 실제 이메일 전송 작업은 백그라운드로 일괄적 처리할 것이다. 

Cron

크론은 유닉스 계열 (리눅스 포함)에 기본적으로 포함된 스케줄러다. 스케줄러라는 것은 정해진 시간에 어떤 작업을 수행하도록 하는 소프트웨어를 말한다. 특정 시간에 어떤 스크립트가 실행되도록 크론에 정의해두면 그 시간에 그 스크립트를 실행해준다. 이에 대한 자세한 설명은 리눅스 수업 크론편을 참고하자.

CLI 수업에서 만든 예제는 바로 크론을 이용하기 위한 것이다. 큐에 쌓여있는 작업을 하나씩 수행하도록 할 때 크론을 이용할 것이다. 이렇게 해야 할일을 순차적으로 쌓아놨다가 일괄적으로 처리하는 것을 배치(batch) 작업이라고 부른다. 

예제

데이터베이스

우선 작업(TODO)을 기록할 데이터베이스 테이블을 만들어보자. 

CREATE TABLE `batch` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `job_name` varchar(50) NOT NULL,
  `context` text NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • jobname : 처리할 작업의 이름이 들어간다. 예 : email
  • context : 해야 할 작업에 대한 구체적인 정보가 들어간다. json 데이터를 사용할 것이다. 

코드

/application/models/batch_model.php

우선 batch 테이블에 데이터를 추가하고 가져오기 위해서 모델을 만들자. /application/models/batch_model.php 파일을 만들고 배치 작업을 추가하기 위한 add 메소드와 배치 작업의 내역을 가져올 gets 메소드를 생성한다.

<?php
class Batch_model extends CI_Model {

    function __construct()
    {       
        parent::__construct();
    }


    function gets(){
        return $this->db->query("SELECT * FROM batch")->result();
    }

    function add($option)
    {
        $this->db->set('job_name', $option['job_name']);
        $this->db->set('context', $option['context']);
        $this->db->insert('batch');
        $result = $this->db->insert_id();
        return $result;
    }

    function delete($option){
        return $this->db->delete('batch', array('id'=>$option['id']));   
    }
}

/application/controllers/topic.php

이제 토픽을 추가 했을 때 이메일을 발송하는 대신 batch 작업의 큐에 등록하는 방법을 알아보자. 아래 예제는 topic.php 파일 중 add 함수의 내용이다. 주석 중 'Batch Queue에 notify_email_add_topic 추가'라고 되어 있는 부분이 기존의 로직을 대체한 부분이다.

    function add(){

        // 로그인 필요

        // 로그인이 되어 있지 않다면 로그인 페이지로 리다이렉션
        if(!$this->session->userdata('is_login')){
            $this->load->helper('url');
            redirect('/auth/login?returnURL='.rawurlencode(site_url('/topic/add')));
        }

        $this->_head();
        $this->_sidebar();
        
        $this->load->library('form_validation');

        $this->form_validation->set_rules('title', '제목', 'required');
        $this->form_validation->set_rules('description', '본문', 'required');
        
        if ($this->form_validation->run() == FALSE)
        {
             $this->load->view('add');
        }
        else
        {
            $topic_id = $this->topic_model->add($this->input->post('title'), $this->input->post('description'));
            
            // Batch Queue에 notify_email_add_topic 추가
            $this->load->model('batch_model');
            $this->batch_model->add(array('job_name'=>'notify_email_add_topic', 'context'=>json_encode(array('topic_id'=>$topic_id))));

            $this->load->helper('url');
            redirect('/topic/get/'.$topic_id);
        }
        
        $this->_footer();
    }

/application/controllers/cli/batch.php

batch.php 파일을 cli에서 동작할 에플리케이션이다. 이것은 최종적으로 cron을 통해서 정기적으로 동작하도록 할 것이다. 

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
class Batch extends MY_Controller {
    function __construct(){
		parent::__construct();
	}
	function process(){
        $this->load->model('batch_model');
        $queue = $this->batch_model->gets();
        foreach($queue as $job){
            switch($job->job_name){
                case 'notify_email_add_topic':
                    $context = json_decode($job->context);
                    $this->load->model('topic_model');
                    $topic = $this->topic_model->get($context->topic_id);
                    $this->load->model('user_model');
                    $users = $this->user_model->gets();     
                    $this->load->library('email');
                    $this->email->initialize(array('mailtype'=>'html'));
                    foreach($users as $user){
                        $this->email->from('master@ooo2.org', 'master');
                        $this->email->to($user->email);
                        $this->email->subject($topic->title);
                        $this->email->message($topic->description);
                        $this->email->send();
                        echo "{$user->email}로 메일 전송을 성공 했습니다.\n";
                    }
                    $this->batch_model->delete(array('id'=>$job->id));
                    break;
            }
        }

	}
}

테스트

지금까지 작업한 내용이 잘 작동하는지 확인해보자. 우선 batch 테이블에 작업을 예약하기 위해서 새로운 글을 적성한다. 그리고 쉘이나 커멘드(cmd)를 이용해서 batch.php 파일을 실행해보자. 아래와 같이 한다. 그 결과 이메일을 발송했다는 메시지가 출력된다면 성공한 것이다. 

php index.php /cli/batch process;

Cron

이제 크론을 이용해서 백그라운드 작업을 자동화해보자. 이 기능은 윈도우나 웹호스팅과 같은 환경에서는 사용할 수 없을 것이다. 하지만 각각의 운영체제마다 스케줄링을 실행해주는 방법이 있기 때문에 그 방법을 사용하면 된다. 아래 명령은 크론의 스케줄링 정책을 변경하는 파일을 편집한다. 

sudo crontab -e

편집화면에서 아래의 내용을 입력한다. 

*/1 * * * * php /var/www/lecture/index.php cli/batch process > /var/www/lecture/application/logs/batch.access.log 2> /var/www/lecture/application/logs/batch.error.log

*/1 * * * * 는 1분에 한번씩 실행하도록 한 것이다. 

위의 내용에서 /var/www/lecture/index.php 파일은 현재 필자가 사용하고 있는 예제의 경로다. 이 경로는 자신의 환경에 맞게 변경해야 한다. 

> /var/www/lecture/application/logs/batch.access.logs 는 batch.access.logs 파일에 스크립트의 실행결과를 저장하는 명령이다. 

2> /var/www/lecture/application/logs/batch.error.logs 는 batch.error.logs 파일에 실행도중 발생한 에러를 저장하는 명령이다. 

우리가 만든 스크립트는 중복실행되면 문제가 생긴다. 이를테면 이메일 1만건을 전송하는 과정에서 10분이 걸리는데 1분에 한번씩 본 스크립트가 실행되면 동일한 작업이 중복 실행된다. 그럼 이메일이 중복해서 발송될 것이다. 이것을 해결하는 방법은 크론의 실행시간을 충분히 늘리거나, 중복실행을 방지하기 위한 메커니즘을 구현해야 한다. 

태그

참고

댓글

댓글 본문
  1. jeisyoon
    2021.058.Queue & Cron - OK
  2. root> php index.php cli/Batch process; 실행시

    Parse error: syntax error, unexpected T_CONST in /home/hosting_users/skatls4840/www/system/core/CodeIgniter.php on line 58

    이건 무슨 이유 때문인지 설명부탁드립니다.
  3. JustStudy
    고맙습니다
  4. 구현
    출처 : http://www.cikorea.net......e/1

    // system/core/Input.php 라인 351
    //변경 전
    $this->ip_address = $_SERVER['REMOTE_ADDR'];
    //변경 후
    $this->ip_address = $this->server('remote_addr');
    대화보기
    • 구현
      이렇게 배치 controller를 만들게 되면 웹에서 호출할 수 있습니다.
      public function __construct(){
      parent::__construct();
      if (!$this->input->is_cli_request()) show_error('Direct access is not allowed');
      }
      이런식으로 생성자부분에서 체크해주시면 좋을 것 같네요^^
    • 스미
      cli 에서 세션 쓰면 안되요
      대화보기
      • 샤핀
        오타제보:
        우리의 상황

        우리가 지금까지 만은 -> 만든
      • 코코딩
        이게...
        리눅스 CLI 에서

        php index.php /cli/batch process; 라고 명령어를 치면




        <div style="border:1px solid #990000;padding-left:20px;margin:0 0 10px 0;">

        <h4>A PHP Error was encountered</h4>

        <p>Severity: Notice</p>
        <p>Message: Undefined index: REMOTE_ADDR</p>
        <p>Filename: core/Input.php</p>
        <p>Line Number: 351</p>

        </div><div style="border:1px solid #990000;padding-left:20px;margin:0 0 10px 0;">

        <h4>A PHP Error was encountered</h4>

        <p>Severity: Warning</p>
        <p>Message: Cannot modify header information - headers already sent by (output started at /var/www/html/system/core/Exceptions.php:185)</p>
        <p>Filename: libraries/Session.php</p>
        <p>Line Number: 688</p>

        </div><!DOCTYPE html>
        <html>
        <head>
        <meta charset="utf-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <!-- Bootstrap -->
        <link href="/static/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">
        <style>
        body{
        padding-top:60px;
        }
        .form_control{
        padding-top:20px;
        }
        </style>
        <link href="/static/lib/bootstrap/css/bootstrap-responsive.css" rel="stylesheet">
        </head>
        <body>
        <div class="navbar navbar-fixed-top">
        <div class="navbar-inner">
        <div class="container">

        <!-- .btn-navbar is used as the toggle for collapsed navbar content -->
        <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        </a>

        <!-- Be sure to leave the brand out there if you want it shown -->
        <a class="brand" href="#">JavaScript</a>

        <!-- Everything you want hidden at 940px or less, place within here -->
        <div class="nav-collapse collapse">
        <ul class="nav pull-right">
        <li><a href="/index.php/auth/login">濡?洹몄??/a></li>
        <li><a href="/index.php/auth/register">????媛€??</a></li>

        </ul>
        </div>
        </div>
        </div>
        </div>
        <div class="container">
        <div class="row-fluid">議댁?ы??吏€ ???? ???댁? ?????? </div>
        </div>
        <script src="http://code.jquery.com/jquery.js"></script>
        <script src="/static/lib/bootstrap/js/bootstrap.min.js"></script>
        </body>
        </html>

        이렇게 나오는 이유가 뭘까요....?