1. /usr/ports/mail/qmail 설치 2. /usr/ports/sysutils/daemontools 설치 (setuidgid,multilog 사용하기위함) 3. /usr/ports/sysutils/ucspi-tcp 설치 (svc 사용하기 위함인데, 난 적용안했음-_-) 4. /usr/ports/mail/queue-fix 설치 (queue폴더 변경을위해 설치했음,기본폴더 쓸꺼면 설치안해도됨)
# cd tmp
# fetch http://inter7.com/qmailmrtg7/qmailmrtg7-3.1.tar.gz
# gunzip qmailmrtg7-3.1.tar.gz
# tar -xvf qmailmrtg7-3.1.tar
# cd qmailmrtg7-3.1
# make all
# make install
# rm /etc/qmail.mrtg.cfg
위의 설치 과정을 보면 마지막에 /etc/qmail.mrtg.cfg 파일을 삭제하는 것을 알 수 있습니다. qmailmrtg7는 설치 과정에서 설정 파일을 /etc 디렉토리에 복사하는데, 설정 파일은 /usr/local/etc/mrtg 디렉토리에 위치하거나, 기본 MRTG 설정 파일 /usr/local/etc/mrtg/mrtg.cfg에 통합되는 것이 이상적이기 때문에 삭제합니다. 다음 섹션에서 보다 자세히 다룹니다.
/var/qmail/rc에서 구동되는 각각의 데몬 구동용 명령어들이 multilog의 장점을 취하기 위해서는 아래의 스크립트에서 HOST.DOMAIN을 메일서버의 FQDN(Fully Qualified Domain Name, 역주: 인터넷 도메인 주소라고 보시면 됩니다)으로 수정하여야 합니다.
#!/bin/sh
#
# This script starts and stops the qmail mail functions.
#
# Suck in the configuration variables.
if [ -r /etc/defaults/rc.conf ]; then
. /etc/defaults/rc.conf
source_rc_confs
elif [ -r /etc/rc.conf ]; then
. /etc/rc.conf
fi
case "$1" in
start)
case ${qmail_smtp_enable} in
[Yy][Ee][Ss])
# Start the qmail smtp daemon
env - PATH="/var/qmail/bin:/usr/local/bin" \
tcpserver -v -H -R -x /etc/tcp.smtp.cdb \
-c200 -u82 -g81 0 25 fixcrio /var/qmail/bin/qmail-smtpd \
2>&1 | /usr/local/bin/setuidgid qmaill \
/usr/local/bin/multilog t n100 s1000000 /var/log/smtp &
echo -n " qmail-smtp"
;;
esac
case ${qmail_pop_enable} in
[Yy][Ee][Ss])
# Start the qmail pop daemon
env - PATH="/var/qmail/bin:/usr/local/bin" \
tcpserver -v -H -R -c200 0 110 \
/var/qmail/bin/qmail-popup HOST.DOMAIN \
/usr/local/bin/checkpassword /var/qmail/bin/qmail-pop3d \
Maildir 2>&1 | /usr/local/bin/setuidgid qmaill \
/usr/local/bin/multilog t n100 s1000000 /var/log/pop3 &
echo -n " qmail-pop"
;;
esac
case ${qmail_enable} in
[Yy][Ee][Ss])
# Start qmail
exec env - PATH="/var/qmail/bin:$PATH" \
qmail-start ./Maildir | /usr/local/bin/setuidgid qmaill \
/usr/local/bin/multilog t n100 s1000000 /var/log/qmail &
echo -n " qmail"
;;
esac
;;
stop)
# Stop the smtp daemon
smtppid=`ps -axw | grep tcpserver | grep smtp | grep -v grep | awk '{ print $1 }'`
if [ "$smtppid" != "" ]; then
kill $smtppid
echo -n " qmail-smtp"
fi
# Stop the pop daemon
poppid=`ps -axw | grep tcpserver | grep popup | grep -v grep | awk '{ print $1 }'`
if [ "$poppid" != "" ]; then
kill $poppid
echo -n " qmail-pop"
fi
# Stop qmail
qmailpid=`ps -axw | grep qmail-send | grep -v grep | awk '{ print $1 }'`
if [ "$qmailpid" != "" ]; then
kill $qmailpid
echo -n " qmail"
fi
;;
*)
echo "Usage: `basename $0` {start|stop}" >&2
;;
esac
exit 0
\ 문자로 종결되는 행은 원래 다음 행과의 하나의 긴 라인 입니다만, 여기에서는 \ 문자를 사용해 두개의 행으로 나누어 표현하고 있습니다.
이제 병목 구간의 원인에 따라 어떠한 방법으로 스크립트의 실행 시간을 최적화할 수 있는지 알아보자.
코드 최적화 : 이 방법은 잘못된 for() 또는 while() 루프와 같이 많은 연산이 이루어지는 부분을 최적화하는 것이다.
출력 버퍼링 및 압축 : 만일 브라우저로 출력해야 하는 내용이 많아서 속도 저하가 생긴다면 이 방법을 사용한다.
데이터베이스 최적화 : 속도 저하의 원인이 데이터베이스 쿼리 또는 테이터베이스 연결 및 함수 사용에 있다면 이 방법을 사용한다.
캐싱 : 이 방법은 문제의 원인이 페이지를 생성하는 시간에 있으며 데이터베이스 쿼리를 최적화할 수 없을 때 사용된다. 캐시를 사용하는 또 다른 이유는 정적인 데이터의 빈번한 요청 때문이다.
코드 최적화
보통 코드 자체가 성능 문제의 원인이 되지는 않는다. 대부분의 최적화 문제는 데이터베이스 또는 데이터 입출력에서 발생한다.
문제의 원인이 함수나 특정 코드 때문이라는 것이 확실해 지기 전에는 PHP 코드를 최적화하면 안된다. 주어진 PHP 스크립트에서 실행 시간의 10%는 코드 실행에 사용되면 90%는 입출력과 데이터베이스 작업에 사용된다.
그러나 어떤 사이트나 애플리케이션을 느리게 만드는 대규모 연산을 하는 특별한 경우가 있다. 이럴 경우 코드를 최적화하는 방법은 다음과 같다.
루프 점검
가능하면 빠른 함수 사용
데이터를 출력하는 최선의 방법 선택
데이터를 입력하는 최선의 방법 선택
echo() 문을 적게 사용
젠드 옵티마이저 사용
루프 검사
코드 최적화를 시작하는 좋은 방법은 반복적으로 실행하는 루프의 내부를 검사하는 것이다. 어떠한 연산이나 문자열 처리도 루프 외부에서 일어나지 않는지 확인한다.
만일 파일 입출력이 루프 내부에서 처리된다면 루프 외부에서는 입출력이 일어나지 않도록 한다. 파일 전체를 메모리에서 읽으면 루프 내부에서 블럭 열기, 읽기, 닫기 작업을 하는 것보다 빠르다. 파일의 일부분을 읽기 위해 루프를 사용한다면 전체 파일을 순차적으로 읽고 필요 없는 부분을 버리는 것이 효과적이다.
Import!
file() 함수는 텍스트 파일을 처리할 경우에 적합하다.
보다 빠른 함수의 사용
PHP에서 어떤 함수들은 보다 많은 연산을 필요로 한다. 이러한 함수들은 보다 간단한 함수로 대체될 수 있다.
ereg() 대신 strstr() 사용 : 만일 정규표현식을 사용하지 않는다면 ereg() 대신 더 빠른 strstr()를 사용한다.
ereg_replace() 대신 str_replace() 사용 : 위와 마찬가지로 문자열 치환에 정규표현식을 사용하지 않는다면 ereg_replace() 보다 속도가 바른 str_replace()를 사용한다.
만일 정규표현식을 사용해야 한다면 PHP의 ereg_*() 함수보다 속도가 빠른 PCRE(Perl compatible regular expression!s) 함수를 사용한다. 자세한 것은 7장을 참고하자.
데이터를 출력하는 최상의 방법을 선택
데이터를 브라우저로 출력하는 방법은 세 가지가 있다.
직접 출력
echo()
print()
직접 출력
<?php
echo("Echoing output\n");
print("Printing output\n");
?>
만일 PHP 변수나 동적인 내용이 필요 없다면 직접 출력을 사용하는 것이 바람직하다. 이 방법은 PHP가 코드를 해석하지 않기 때문에 가장 빠르다.
데이터를 입력하는 최상의 방법 선택
데이터를 입력하는 방법은 세 가지가 있다.
readfile()
include()
require()
만일 파일에 PHP 코드가 포함되어 있지 않다면 readfile()을 사용한다. 이 방법은 파일을 인클루드하기 전에 데이터를 파싱하지 않으므로 좀더 빠르다. include()와 require()은 PHP 코드가 발견되면 실행한다. PHP 스크립트에서 HTML 헤더는 readfile()을 사용하는 것이 바람직하다.
echo 문 적게 사용하기
echo() 문은 실행에 시간이 걸리므로 적게 사용하고 긴 echo() 문이 가독성이 좋다. 예를 들면:
echo("hello\n
this is a test\n
of the echo statement");
위의 코드가 아래 코드보다 빠르다:
echo("hello\n");
echo("this is a test\n");
echo("of the echo statement");
이것은 PHP를 위한 플러그인으로 여러 가지 기술을 통해 다양한 방법으로 코드를 최적화한다.
일반적으로 입출력을 많이 하는 스크립트는 젠드 옵티마이저를 사용해도 별 효과가 없고 대규모 연산을 하는 스크립트는 성능이 크게 향상된다.
출력 버퍼링과 압축
대부분의 페이지 생성 엔진은 브라우저에 동적 내용을 보내기 위해 출력 함수를 많이 사용한다. 이러한 스크립트는 변수를 가진 print() 문을 많이 사용하며 출력을 많이 하므로 성능에 문제가 된다. 따라서 동적으로 생성되는 내용이 많을수록 스크립트의 실행은 느려진다.
출력 버퍼링(Output buffering)은 스크립트의 입출력 시간을 감소시킨다. 이 기능은 PHP4.0.3 이상의 버전에서 제공되는 출력 버퍼링 함수를 이용해 입출력이 많은 프로그램에 적용할 수 있는 일반적인 기술이다.
출력 버퍼의 개념은 버퍼의 내용을 출력하기 전에 출력할 내용을 모두 메모리 버퍼에 저장하는 것이다. 이 방법은 다음과 같은 장점을 가지고 있다.
입출력 작업이 줄어 성능이 크게 향상된다.
브라우저로 보내기 전에 출력할 내용을 조작할 수 있다.
입출력 작업이 순차적으로 빠르게 이루어진다.
이 방법은 단점은 스크립트가 종료될 때까지 클라이언트가 기다려야 한다는 것이다. 따라서 애플리케이션 설계방식이나 스크립트 실행 시간에 따라 사용자는 시스템에 문제가 있다고 판단하고 브라우저를 닫거나 무엇인가 잘못되었다는 결론을 내릴 수 있다.
출력 버퍼링 예제
다음은 PHP에서 출력 버퍼링 사용 방법을 보여주는 간단한 예제이다:
<?php
ob_start();
echo("This is a test\n");
echo("More content\n");
ob_end_flush();
?>
ob_start() 함수는 출력 버퍼링을 시작한다. ob_start() 뒤에는 브라우저로 출력하는 모든 함수가 데이터를 데이터 버퍼로 보낸다.
버퍼의 내용은 ob_end_flush() 함수를 이용해 브라우저로 출력되는데, 다음과 같은 두가지 작업을 한다. 우선 사용 중인 출력 버퍼를 받고, 그 다음 버퍼의 내용을 브라우저로 출력한다.
Import!ant
위의 예제에서는 ob_end_flush() 함수가 필요하지 않는데, 그 이유는 PHP 인터프리터가 스크립트의 실행이 종료될 때 버퍼의 내용을 출력하기 때문이다. 그러나 코드의 신뢰성을 높이기 위해서는 이 함수를 사용하는 것이 좋다.
출력 버퍼링 함수
void ob_start([string output_callback])
이 함수는 출력을 만들어 내는 모든 함수들이 사용할 새로운 출력 버퍼를 시작한다. 선택사항으로 콜백 함수를 전달할 수도 있다. 콜백 함수가 전달되면 스크립트가 종료되거나 ob_end_flush()가 실행될 때 이 함수가 호출된다. 이 함수는 버퍼의 내용을 받고 이것을 바탕으로 한 작업 결과를 리턴한다.
예를 들어, 스크립트의 출력 결과에서 foo를 bar로 변경하는 검열(censorship) 콜백 함수를 사용하려면 다음과 같이 한다:
<?php
function censorship($buffer)
{
return str_replace('foo', 'bar', $buffer);
}
ob_start('censorship');
echo("This is a foo test of our program\n");
echo("I can't write foo!\n");
ob_end_flush();
?>
위 스크립트의 실행 결과는 다음과 같다.
[image]
이것은 스크립트의 최종 출력을 처리해야 할 경우에 유용하다.
string ob_get_contents(void)
위의 함수는 사용 중인 출력 버퍼의 내용을 리턴한다. 만일 출력 버퍼가 시작되지 않았다면 false를 리턴한다.
string ob_get_length(void)
위의 함수는 사용 중인 출력 버퍼의 길이를 리턴한다. 만일 사용 중인 출력 버퍼가 없다면 false를 리턴한다.
void ob_end_flush(void)
위의 함수는 사용 중인 버퍼를 종료하고 그 내용을 브라우저로 출력하거나 사용 중인 버퍼에 우위를 갖는다(버프 스택 참조). 이 함수가 실행되면 버퍼는 버려지므로 이를 처리하려면 ob_end_flush()를 호출하기 전에 ob_get_contents()를 사용해야 한다.
버퍼 스택
PHP에서 사용하는 버퍼링 방식은 스택(stack)이다. 이것은 이미 ob_start()를 호출한 블럭 내부에서 ob_start()를 호출할 수 있다는 것을 의미한다. 두 번째 ob_start() 호출은 새로운 버퍼를 생성하고 ob_end_flush()가 실행되면 이 버퍼의 내용은 부모 버퍼로 전단된다. 두 번째 ob_end_flush()를 호출하면 버퍼의 내용은 브라우저로 출력된다.
이것은 페이지 생성 엔진에서 출력 버퍼링을 사용하고 함수나 모듈의 출력을 처리해야 할 경우에 유용하다.
PHP 출력 압축하기
PHP가 데이터를 브라우저로 출력할 때는 HTML 헤더(content type header)를 이용해 가공되지 않은 데이터(raw data)를 보낸다. 최근의 브라우저는 압축된 데이터도 지원하므로 서버에서 gzip으로 압축된 데이터를 브라우저로 보내면 브라우저는 이 데이터의 압축을 풀어 출력하게 된다.
이 방법을 이용하면 스크립트의 성능과 많은 내용을 만들어 내는 페이지 생성 엔진의 성능을 증가시킬 수 있다. 이 방식으로 테스트한 결과 실행 시간이 약 60% 정도 감소했다.
PHP에서는 ob_gzhandler() 콜백 함수를 사용할 수 있다. 만일 우리가 ob_gzhandler() 함수를 ob_start()로 보내면 ob_end_flush() 함수가 실행되었을 때 PHP는 다음 작업을 한다.
버퍼의 내용을 ob_gzhandler() 함수로 전달한다.
ob_gzhandler() 함수는 브라우저가 보낸 헤더를 이용해 브라우저가 gzip 인코딩을 지원하는지 판단한다.
만일 gzip 데이터를 사용할 수 있다면 버퍼의 내용을 압축하고 브라우저로 보낼 헤더가 만들어진다.
압축된 데이터와 헤더를 브라우저로 전송한다.
출력할 내용을 압축하면 성능이 크게 개선되고 대역폭도 감소한다. 출력 버퍼를 압축하려면 다음과 같이 한다:
ob_start('ob_gzhandler');
브라우저는 gzip으로 압축된 내용을 받는다. 또한 php.ini 파일에서 아래와 같이 설정하면 ob_gzhandler() 함수를 ob_start()의 디폴트 콜백 함수로 사용할 수 있다:
output_handler = 'ob_gzhandler'
Import!ant
ob_gzhandler() 콜백 함수는 PHP4.0.5 이상에서만 사용할 수 있다. 이전 버전의 PHP는 ob_gzhandler() 함수에 매우 큰 메모리 누수 문제가 있다.
만일 압축된 내용을 전송하는 것에 문제가 있다면 vary HTTP 헤더를 사용한다. 또한 일부 브라우저는 POST 요청의 결과로 압축된 내용이 올 때 문제가 있는 것으로 알려져 있다.
데이터베이스 최적화
이 방법은 최적화 문제에서 가장 중요한 부분이다. 스크립트는 실행 시간의 대부분을 데이터베이스 작업에 사용하므로 몇 가지 방법을 통해 데이터베이스 작업의 성능을 개선하는 방법을 살펴보자.
여기서는 MySQL을 이용해 설명하지만 이 개념은 PostgreSQL, 오라클 등 다른 데이터베이스로도 확대될 수 있다.
쿼리 분석
일단 성능 저하의 문제가 쿼리에 있다는 것이 밝혀졌다면 각 쿼리에서 무엇이 문제인지를 알아내야 한다. 우선 불필요한 조인을 사용하지 않는지 확인하고 쿼리를 빠르게 만들 다른 방법이 있는지 확인해야 한다. 두 개의 큰 테이블을 조인하는 것보다 쿼리를 두 번하는 것이 빠르다.
쿼리 실행 방법
만일 제대로 만들어진 쿼리의 실행 시간이 매우 길다면 다음의 MySQL EXPLAIN 문을 이용해 MySQL이 쿼리를 어떻게 처리하는지 확인한다:
EXPLAIN SELECT ... FROM ... WHERE ...
간단히 SELECT 문 앞에 EXPLAIN만을 추가하면 된다. MySQL은 쿼리를 실행하고 다음과 같은 처리 정보를 가진 테이블을 리턴한다.
table : 출력된 행에서 참조하는 테이블
type : 사용되는 조인 형식. 조인 형식에 대한 자세한 정보는 뒤에 나오는 조인 형식을 참고한다.
possible_keys : 쿼리를 수행하기 위해 MySQL이 사용할 수 있는 컬럼. 만일 비어있다면 관련된 색인이 없다는 것이다. 이 경우 WHERE 절을 분석해 적절한 색인을 추가하면 성능이 개선된다.
key : 쿼리를 수행하기 위해 MySQL이 선택한 색인. 만일 NULL이면 색인이 사용되지 않은 것이다. 만일 MySQL이 잘못된 색인을 선택한다면 USE INDEX/IGNORE INDEX를 이용해 강제로 색인을 지정할 수 있다.
key_len : MySQL이 사용하기로 결정한 키의 길이
ref : 테이블에서 행을 선택하기 위해 사용된 컬럼 또는 상수
rows : 테이블에서 행을 선택하기 위해 MySQL이 반드시 검사해야 하는 행의 수
Extra : MySQL이 쿼리를 실행하는 방식에 대한 추가 정보. 다음과 같은 정보가 사용된다.
Distinct : 조건에 맞는 첫 번째 행을 발견하면 더 이상 검색을 진행하지 않는다.
Not exits : MySQL은 쿼리에 LEFT JOIN 최적화를 할 수 있는데, LEFT JOIN 조건에 맞는 행의 조합을 찾아내면 더 이상 테이블의 행을 검사하지 않는다.
Using filesort : MySQL은 검색된 행을 정렬해서 가져오기 위해 추가적인 작업이 필요하다. 이 작업은 조인 형식에 따라 모든 행을 검사하고 WHERE 절에 해당하는 모든 행이 "정렬 키 + 포인터"를 저장함으로써 가능하다. 각 행을 순서대로 가져오기 전에 이 키가 먼저 정렬된다.
Using index : 실제 행을 검사하지 않고 테이블의 색인 트리에 있는 정보만을 사용하는 경우이다. 테이블에서 가져오는 모든 컬럼이 같은 색인의 일부분인 경우를 의미한다.
Using temporary : MySQL은 결과를 저장하기 위해 임시 테이블을 만들 필요가 있다. 이것은 하나의 테이블에서 서로 다른 컬럼에 사용된 GROUP BY의 결과 행을 ORDER BY로 정렬할 때 사용된다.
쿼리를 가능한 빠르게 만들려면 Using filesort와 Using temporary를 살펴본다. 만일 이러한 부분이 발견된다면 쿼리를 최적화할 필요가 있다.
조인 형식
아래는 조인(JOIN) 형식을 좋은 것부터 차례대로 나열한 것이다.
system : 테이블에 오직 하나의 행만을 가지는 경우이다. 시스템 조인은 일반적이 아니며(보통 테이블은 하나 이상의 행을 갖는다) 검사해야 할 행이 하나이므로 가장 빠르다.
eq_ref : 한 테이블의 행이 다른 테이블의 각 행과 조합을 이루는 경우. 이 형식은 시스템 조인을 제외하고 가장 좋은 조인 형식이다. 이것은 색인의 모든 부분이 조인에 참여하고 색인이 UNIQUE 또는 프라이머리 키인 경우이다.
ref : 다른 테이블의 행과 조합하기 위해 색인 값에 매치되는 모든 행이 읽혀진다. ref는 조인이 키의 가장 좌측 부분(leftmost prefix)만을 사용하거나 키가 UNIQUE 또는 PRIMARY KEY가 아닐 경우이다. 만일 비교에 사용되는 키의 행이 적다면 훌륭한 조인 형식이다.
range : 색인에서 행을 선택하기 위해 지정된 범우의 행만 검색한다. key 컬럼은 사용되는 색인을 나타낸다.
ALL : 다른 테이블의 행과 조합을 위해 테이블 전체를 스캔하는 경우이다. 이 방식은 다른 경우에 비해 좋지 않다. 적절한 색인을 추가해 ALL 형식을 피하고 컬럼이 상수 값을 통해 검색되도록 하는 것이 좋다.
index : 색인 트리만 스캔된다는 점을 빼면 ALL과 동일하다. 보통 색인 파일이 데이터 파일보다 작기 때문에 ALL 보다 빠르다.
EXPLAIN의 출력 테이블에 있는 rows 컬럼 값을 이용해 훌륭한 조인 형식인지를 알 수 있다. 이 컬럼은 쿼리를 수행하기 위해 얼마나 많은 행을 검색해야 하는지를 나타낸다.
MySQL 문서에 있는 다음 예는 EXPLAIN 명령의 결과를 이용해 어떻게 조인을 최적화하는지 보여준다. 우리는 여기서 몇 개의 필드를 가진 가상의 데이터베이스를 이용하므로 데이터베이스, 테이블, 필드의 이름은 무시해도 된다. 이 예제의 목적은 EXPLAIN의 결과를 어떻게 분석하는지를 아는 것이다.
SELECT 구문을 조사하기 위해 EXPLAIN을 사용한다:
EXPLAIN SELECT tt.TicketNumber, tt.TimeIn,
tt.ProjectReference, tt.ExtimatedShipDate,
tt.ActualShipDate, tt.ClientID,
tt.ServiceCodes, tt.RepetitiveID,
tt.CurrentPRocess, tt.currentDBPerson,
tt.RecodeVolume, tt.DPPprinted, et.COUNTRY,
et_1.COUNTRY, do.CUSTNAME,
FROM tt, et,et AS et_1
WHERE
tt.SubmitTime IS NULL
AND tt.ACtualIPC = et.EMPLOYID
AND tt.AsignedPC = et_1.EMPLOYID
AND tt.ClientID = do.CUSTOMER;
비교하려는 컬럼이 다음과 같이 정의되었다고 가정해 보자.
테이블
컬럼
컬럼 타입
tt
ActualPC
CHAR(10)
tt
AssignedPC
CHAR(10)
tt
ClientID
CHAR(10)
tt
EMPLOYID
CHAR(10)
tt
CUSTNMBR
CHAR(10)
그리고 각 테이블은 다음과 같은 색인을 가지고 있다.
테이블
색인
tt
ActualPC
tt
AssignedPC
tt
ClientID
tt
EMPLOYID (primary key)
tt
CUSTNMBR (primary key)
tt.ActualPC 값은 골고루 분포되어 있지 않다.
아무런 최적화도 수행하지 않았을 경우 EXPLAIN 문의 실행 결과는 다음과 같다.
Table
Type
Possible Keys
Key
Key Length
Ref
Rows
et
ALL
PRIMARY
NULL
NULL
NULL
74
do
ALL
PRIMARY
NULL
NULL
NULL
2135
et_1
ALL
PRIMARY
NULL
NULL
NULL
74
tt
ALL
AssignedPC, ClientID, ActualPC
NULL
NULL
NULL
3872
보는 바와 같이 모든 테이블에 ALL이 사용된다. 이것은 MySQL이 조인을 위해 모든 테이블에 대해 전체를 스캔하는 것을 의미하며 개선이 필요한 쿼리이다. 이 예에서는 45,268,558,720 행(74*2135*74*3872)을 검사해야 하는데, 이것은 상당한 시간이 걸리게 된다.
한가지 문제점은 컬럼 형식이 다르게 정의되어 있으면 MySQL이 컬럼의 색인을 효과적으로 이용할 수 없다는 것이다. 이 경우, 길이가 다르게 선언되지만 않는다면 VARCHAR와 CHAR는 같다. 이 예제에서 tt.ActualPC는 CHAR(10)이고 et.EMPLOYID는 CHAR(15)이므로 길이에서 차이가 난다. 따라서 ActualPC의 길이를 10에서 15로 변경할 필요가 있다:
mysql> ALTER TABLE tt MODIFY ActualPC VARCHAR(15);
이제 tt.ActualPC와 et.EMPLOYID는 둘 다 VARCHAR(15)가 되었다. EXPLAIN을 다시 한 번 실행해 보자.
Table
Type
Possible Keys
Key
Key Length
Ref
Rows
tt
ALL
AssignedPC, ClientID, ActualPC
NULL
NULL
NULL
3872
do
ALL
PRIMARY
NULL
NULL
NULL
2135
et_1
ALL
PRIMARY
NULL
NULL
NULL
74
et
eq_ref
PRIMARY
PRIMARY
15
tt.ActualPC
1
검사해야 하는 행의 수가 74만큼 줄어들었다. 두 번째는 tt.AssignedPC = et_1.EPLOYID와 tt.ClientID = do.CUSTNBR의 비교 부분에서 컬럼 길이가 맞지 않는 문제를 해결해야 한다:
mysql> ALTER TABLE tt MODIFY AssignedPC VARHCAR(15), MODIFY ClientID VARCHAR(15);
다시 EXPLAIN을 실행한 결과는 다음과 같다.
Table
Type
Possible Keys
Key
Key Length
Ref
Rows
et
ALL
PRIMARY
NULL
NULL
NULL
74
tt
ref
AssignedPC, ClientID, ActualPC
ActualPC
15
et.EMPLOYID
52
et_1
eq_ref
PRIMARY
PRIMARY
15
tt.AssignedPC
1
do
eq_ref
PRIMARY
PRIMARY
15
tt.ClientID
1
이 쿼리는 매우 바람직하게 최적화되었다. 대부분의 쿼리에 있어 이러한 방법이 적용되므로 EXPLAIN을 이용해 색인을 생성하고 길이가 다른 문제를 해결할 수 있다. 데이터 모델을 설계할 때 길이가 다른 키에 대해 MySQL이 어떻게 동작하는지를 염두에 둔다면 쿼리의 실행 속도가 느려졌을 때 테이블을 변경해야 하는 일은 발생하지 않을 것이다.
테이블 최적화
우리가 테이블에서 레코드를 삭제하면 MySQL은 나중에 INSERT 작업에서 데이터를 재사용하도록 링크 목록으로 유지한다. 만일 DELETE 작업을 많이 한다거나 가변 길이를 가진 행에 큰 변화가 생겼다면 우리는 이 테이블의 분산된 데이터를 모으는 작업(defragment)을 해야 한다. 이 작업은 OPTIMIZE TABLE 문을 이용한다:
OPTIMIZE TABLE tbl_name [, tbl_name]
OPTIMIZE TABLE은 다음과 같은 작업을 한다.
삭제되거나 분산된 행을 수정한다.
정령되지 않은 모든 색인 페이지를 정렬한다.
테이블 통계 정보를 업데이트한다.
Import!ant
OPTIMIZE 명령을 실행하는 동안에는 테이블이 잠기므로 이용자가 많은 시간에는 실행하지 않도록 한다.
데이터 모델 최적화
데이터베이스 쿼리를 최적화하는 가장 좋은 단계는 데이터베이스 모델을 설계하는 단계이다. 다음 팁을 염두해 두고 데이터베이스를 설계하면 성능이 향상될 거이다.
가장 효율적인 데이터 형식을 사용한다. MySQL은 다양한 데이터 타입을 지원한다. 작은 데이터형을 사용하면 쿼리 속도가 빨라진다. 가능한 작은 정수형을 사용한다. 예를 들어 MEDIUMINT는 INT보다 좋다. 사용하기에 적당한 숫자형은 다음과 같다.
컬럼 형식
필요한 공간
TINYINT
1 바이트
SMALLINT
2 바이트
MEDIUMINT
3 바이트
INT
4 바이트
INTEGER
4 바이트
BIGINT
8 바이트
FLOAT(X)
X <= 24이면 4 바이트, 25 <= X <= 53이면 8 바이트
FLOAT
4 바이트
DOUBLE
8 바이트
DOUBLE PRECISION
8 바이트
REAL
8 바이트
DECIMAL(M,D)
D>0이면 M+2 바이트, D=0 이면 M+1 바이트 (M
NUMERIC(M,D)
D>0이면 M+2 바이트, D=0 이면 M+1 바이트 (M
가능하면 컬럼을 NOT NULL로 선언한다. 속도를 빠르게 하고 컬럼 당 1비트를 아낄 수 있다.
가능하면 TEXT, BLOB, VARCHAR 컬럼을 피한다. 만일 불가피하게 가변폭 컬럼을 사용해야 한다면 가능하면 많이 사용한다. 한 테이블에 가변폭 컬럼이 존재한다면 고정폭 컬럼이 이미 쓸모없게 된다. 또한 고정폭 컬럼 사이에 블럽(blob) 컬럼이 끼어 있다면 이 블럽 컬럼을 별도의 테이블로 분리하고 고정폭 컬럼을 가진 테이블을 참조하도록 한다.
테이블의 프라이머리 키는 가능하면 작아야 한다. 이 방법은 각 해의 식별자로 효과적으로 처리할 수 있게 만든다.
색인의 효율을 증가시킨다. 만일 우리가 어떤 컬럼의 앞자리 X번째 문자가 고유하다는 것을 안다면 이 부분만을 이용하는 색인을 만들도록 한다. 이것이 좀더 효율적이다.
테이블의 모든 컬럼을 이용해 색인을 만들지 않는다. 색인은 SELECT 문을 빠르게 해주지만 UPDATE, INSERT, DELETE 작업의 속도는 느려진다. 필요한 색인만을 생성한다.
색인 사용
색인은 데이터베이스 엔진이 쿼리를 실행할 때 사용하는 기본적인 해결 방법이다. 테이블 전체를 검색하는 것보다 색인을 검색하는 것이 몇 배 이상 빠르다.
Import!ant
테이블에 하나 이상의 색인을 추가하면 쿼리의 90%를 최적화할 수 있다.
많은 데이터베이스 설계자들이 색인에 신경을 쓰지 않고 속도 문제가 생겼을 경우에 색인을 만든다. 물론 이 방법도 문제를 해결할 수 있지만 좋은 방법은 아니다. 제대로 설계된 데이터베이스는 반드시 색인을 염두에 두고 만들어져야 한다. 가장 좋은 색인은 CREATE TABLE 문에서 만들어 져서 많은 쿼리에 사용된 것이다. 그러나 불필요하게 만든 쿼리는 업데이트 속도를 저하시킨다.
MySQL은 자동적으로 색인을 사용한다. 주어진 쿼리에서 어떤 색인이 사용되었는지 확인하려면 앞서 설명된 EXPLAIN 명령을 사용하면 된다.
SELECT 쿼리 최적화
SELECT 쿼리를 최적화하려면 색인을 추가할 수 있는지 확인한다. EXPLAIN을 사용해 색인이 사용되고 있는지를 확인한다.
INSERT 쿼리 최적화
테이블에 레코드를 삽입하는 작업은 다음과 같은 단계를 거친다.
연결
서버로 쿼리 전송
쿼리 분석
레코드 삽입
색인 삽입(각 색인마다 한 번씩)
연결 종료
INSERT 대신 INSERT DELAYED를 사용하면 보다 빠른 속도를 얻을 수 있다. 서바상의 작업 종료와는 무관하게 클라이언트는 insert 명령이 성공했다는 확인 메시지를 받는다. MySQL 서버는 클라이언트에게 실행이 완료되기를 기다리라는 메시지를 보내지 않고 insert 작업을 나중에 처리한다.
파일에서 데이터를 로딩할 때 LOAD DATA INFILE을 사용하면 INSERT 문을 여러 번 사용하는 것 보다 20배 정도 빠르다(ORACLE/MSSQL에서는 BCP라고 부른다).
쉼표로 구분된 파일을 데이터베이스로 로드하려면 다음 명령을 사용한다:
LOAD DATA INFILE data.txt INTO TABLE foo FIELDS TERMINATED BY ',';
LOAD DATA INFILE은 다음과 같은 형식이다:
LOAD DATA [LOW_PRIORTY | CONCURRENT] [LOCAL] INFILE 'file_name.txt'
[REPLACE | IGNORE]
INTO TABLE tbl_name
[FIELDS
[TERMINATED BY '\t']
[[OPTIONALLY] ENCLOSED BY '']
[ESCAPED BY '\\']
]
[LINES TERMINATED BY '\n']
[IGNORE number LINES]
[(col_name,...)]
LOAD DATA INFILE을 이용해 데이터를 MySQL 테이블에 넣은 자세한 방법은 MySQL 메뉴얼을 참고하면 된다.
insert 작업에 앞서 테이블을 잠그면 작업속도가 빨라진다:
LOCK TABLES a WRITE;
INSERT ....
INSERT ....
UNLOCK TABLES;
UPDATE 쿼리 최적화
UPDATE 문은 SELECT 문을 실행한 뒤에 쓰기 작업을 하는 것으로 생각할 수 있다. 따라서 UPDATE xx FROM yy WHERE zz 쿼리를 최적화하는 것은 쓰기 작업은 항상 존재하므로 SELECT xx FROM yy WHERE zz 쿼리를 최적화하는 것과 같다. 따라서 UPDATE 쿼리의 최적화는 SELECT 쿼리의 최적화와 동일하다.
DELETE 쿼리 최적화
레코드를 삭제하는 시간은 색인의 수에 정확히 비례한다. 이것는 MySQL이 레코드를 테이블과 각각의 색인에서 지워야 하기 때문이다. 테이블의 전체 레코드를 삭제하려면 TRUNCATE TABLE tb_name을 사용하는 것이 DELETE from tb_name을 사용하는 것보다 빠르다. TRUNCATE TABLE은 각 행의 색인과 데이터를 지울 필요 없이 전체 테이블과 색인을 지운다.
연결 최적화
테이터베이스 쿼리를 실행할 때 시간을 소요하는 마지막 원인은 바로 데이터베이스 연결 과정이다. 몇몇 애플리케이션에서 우리는 매번 쿼리를 할 때마다 데이터베이스 연결을 하고, 사용하고, 종료하는 것을 볼 수 있다. 그러나 데이터베이스에 일단 연결이 되면 여러 개의 쿼리를 실행할 수 있으므로 이것은 비효율적인다. PHP를 이용하면 쿼리를 실행할 때마다 데이터베이스에 연결하지 않고 지속적인 연결을 할 수 있다.
접속을 할 때 이 함수는 우선 동일한 호스트 명, 사용자 아이디, 암호를 사용한 지속적 연결이 있는지 찾는다. 만일 발견이 된다면 새로 연결하지 않고 기존의 연결 식별자를 리턴한다. 또한 이 연결은 스크립트가 종료되어도 끊어지지 않고 나중에 다시 사용할 수 있도록 연결 상태를 유지한다.
그 밖의 최적화 팁
최적화에 사용할 수 있는 그 밖의 팁은 다음과 같다.
업데이트가 많이 발생하는 테이블에 복잡한 SELECT 쿼리를 사용하지 않는다.
변경이 빈번하게 발생하는 데이블에는 VARCHAR와 BLOB 컬럼을 사용하지 않는다.
단지 크다는 이유만으로 하나의 큰 테이블을 서로 다른 테이블로 분리하는 것은 옳지 않다.
해시 컬럼 소개 만일 어떤 컬럼이 짧고 고유하다면 여러 컬럼을 사용하는 색인보다 빠르다. MySQL에서는 이 특별한 컬럼을 쉽게 사용할 수 있다. SELECT * FROM tb_name WHERE hash=MD5(concat(col1,col2)) AND col1='x' AND col2='y';
카운터는 실시간으로 업데이트한다. 만일 카운트와 같이 많은 행을 기반으로 하는 정보를 처리해야 한다면 별도의 테이블을 만들고 이 카운트를 실시간으로 업데이트하는 것이 바람직하다. 다음과 같은 업데이트는 매우 빠르다. UPDATE tb_name SET count=count+1 WHERE col='x';
거대한 테이블을 검색하는 대신 요약 테이블을 이용한다. 실시간으로 통계를 내는 것보다 요약 정보를 유지하는 것이 훨씬 빠르다.
디폴트 값을 가지는 컬럼을 이용한다. 입력되는 값이 디폴트가 아닐 경우에만 명시하면 된다.
고유한 값이나 키를 만들려면 AUTO_INCREMENT 컬럼을 사용한다.
캐싱
스크립트 실행 시간의 대부분은 데이터베이스 작업과 입출력 작업에서 소모된다. 입출력과 데이터베이스 쿼리를 최적화한 뒤에는 스크립트의 실행 시간을 개선해야 한다. 만일 느려지는 작업이 있다면 가장 좋은 최적화 방법은 해당 작업을 피하는 것이다. 이것은 캐싱을 사용하면 가능하다.
캐싱의 정의
캐싱(Caching) 처리 작업이나 생성 작업 없이 데이터를 다시 사용할 수 있도록 저장하는 것을 말한다. PHP 프로그래밍에서는 캐싱은 두 번씩 생성할 필요가 없도록 동적으로 만들어진 데이터를 저장하는 것을 의미한다. 데이터를 만드는 과정이 복잡할수록 캐싱의 효과가 크다.
캐싱의 중요성
캐싱은 매우 중요한 기술이다. 무엇보다 데이터를 동적으로 생성하는 작업이 단순한 파일 읽기 작업으로 대체되어 실행 시간이 줄어든다. 또한 캐싱은 웹 서버와 데이터베이스 서버의 부하를 줄이는 좋은 방법이다. 수많은 연결과 트랜잭션이 발생하는 대형 사이트에서 캐싱은 필요한 데이터베이스 쿼리의 수를 줄여준다.
캐싱의 또 다른 중요한 장점은 외부 데이터에 대한 사이트의 의존성을 줄여준다는 것이다. 만일 데이터의 일부가 데이터베이스 혹은 다른 사이트에서 제공된다면 해당 사이트가 다운되어도 캐시에 저장된 데이터를 이용해 작업을 계속할 수 있다.
캐싱의 장점
캐싱의 다음과 같은 장점을 가지고 있다.
일반적으로 파일에서 데이터를 읽는 작업은 데이터베이스나 다른 소스에서 데이터를 생성하는 것보다 빠르므로 성능이 향상된다.
서버와 데이터베이스의 부하를 감소시킨다.
독립성이 좋아진다. 만일 데이터베이스가 다운되어도 사이트는 영향을 받지 않는다.
캐싱의 단점
물론 캐싱도 몇 가지 단점을 가지고 있다. 그 중 가장 나쁜 것은 사이트가 복잡해진다는 것이다. 캐시 데이터를 이용하기 위해 데이터를 캐시에 저장하고, 데이터가 유효한지 확인하고, 필요한 경우 캐시를 업데이트하는 작업이 추가해야 한다. 캐시에 저장되는 데이터의 형식에 따라 캐싱 로직은 그리 간단하지 않고 캐시를 위한 특별한 함수를 만들어야 하는 경우도 있다.
일반적인 캐싱 정책
우선 캐싱 일반적인 캐싱 메커니즘의 로직을 살펴보자. 이것은 대부분의 캐싱 구조에 적합하고 캐싱 시스템을 설정하기에 앞서 결정을 내리는 데 도움이 된다.
[이미지]
접근 데이터 확인 캐싱의 원리는 간단한다. 다시 요청되었을 때 다시 반복할 필요 없도록 어떤 작업 결과를 저장하는 것이다. 데이터를 저장할 때 어디에서 생성되었는지는 상관하지 않으므로, 데이터가 이미 캐시에 있는지 검사하기 위해 데이터의 이름을 부여해야 한다. 예를 들어, 함수의 실행 결과는 cache_funcName.dat(명명 규칙 부분 참조)와 같은 이름을 부여할 수 있다. 만일 printTable() 함수를 사용한다면 이 함수의 실행 결과는 cache_printTable.dat라는 이름의 파일로 저장할 수 있다.
캐시 데이터의 유효성 확인 만일 캐시에 이미 주어진 이름의 데이터가 존재한다면 이 데이터가 유효한 지를 검사해야 한다. 예를 들어, 캐시의 데이터가 저장된 지 5분 미만이라면 유효하다고 판단할 수 있는데 따라서 5분마다 새로운 데이터를 만들어야 한다. 다른 방법은 변화가 있기 전까지는 캐시 데이터가 유효하다고 판단하는 것이다. PHP에서 파일의 최종 수정 시간 filemtime() 함수를 이용해 알 수 있다.
캐시에서 데이터 가져오기 여러분은 캐시에 접근하고 주어진 이름의 캐시 데이터를 가져온다. 데이터에 접근하는 방법은 저장 방식에 따라 다르다. 파일 기반의 캐싱 시스템의 경우 캐시에서 데이터를 가져오는 것은 파일을 열고 그 내용을 읽는 것을 의미한다. 만일 캐시가 다른 저장 방법을 사용한다면 접근 방법은 바뀔수 있다.
데이터 생성 이것은 캐싱 시스템의 외부에서 동적으로 데이터를 만드는 과정이다. 이것은 실제로 캐시에 저장되는 코드 부분이면 동적인 데이터를 만들어 내는 PHP 스크립트에 해당된다.
캐시에 데이터 저장 또는 업데이트 만일 캐시에 데이터가 없거나 유효하지 않으면 생성된 데이터를 캐시에 저장한다. 그 다음 불필요한 작업을 하지 않고 저장된 데이터를 이용할 수 있다.
데이터 출력 print(), echo() 등의 함수를 이용해서 클라이언트에게 데이터를 전송하는 일반적인 작업을 의미한다.
따라서 우리는 다음과 같은 것들을 정의해야 한다.
데이터를 저장하고, 업데이트하고, 가져오는 함수로 이루어진 캐시 저장 메소드
캐시 데이터를 구별할 수 있는 명명 규칙
캐시 데이터의 유효성을 판단할 수 있는 기준
주기적으로 캐시의 내용을 갱신하는 정책
캐시를 위한 저장 방식
캐싱 시스템에서 정의해야 하는 첫 번째 요소는 캐시 데이터를 저장하는 방식이다. 몇 가지 방식이 있지만 가장 흔한 것은 각각의 캐시 데이터를 하나의 파일로 저장하는 것이다. 일반적으로 다음과 같은 방식이 사용된다.
데이터베이스 사용
각각의 캐시 자료를 하나의 파일로 나타냄
모든 캐시 자료를 하나의 DBM 파일로 나타냄
공유 메모리(shared memory) 사용
데이터베이스 사용
우리는 주로 데이터베이스에 만들어낸 데이터를 저장하기 때문에 이 방법은 그다지 바람직하지 않다. 그러나 데이터를 다른 사이트에서 가져오거나 매우 느린 방법을 통해 만들어 내는 시스템의 경우 데이터베이스를 이용하는 방법은 신뢰성이 있다.
데이터베이스의 명명규칙은 각각의 데이터에 접근할 수 있는 프라이머리 키가 만들어지는 것이어야 한다. 데이터 저장, 검색, 업데이트, 삭제 작업은 SQL문을 통해 이루어진다. 캐시 데이터의 검색 작업을 빠르게 하기 위해 테이블은 프라이머리 키로 색인되어 있어야 한다. 또한 테이블에는 캐시 데이터의 유효성을 확인할 수 있도록 타임스탬프(timestamp)가 필요하며, 캐시 데이터의 유효성 나타내는 플래그 컬럼을 둘 수도 있다.
파일 사용
파일을 사용할 경우에는 각각의 캐시 데이터를 하나의 파일로 생성한다. 명명 규칙은 각각의 데이터에 고유한 이름을 부여할 수 있어야 한다. 파일을 업데이트하는 작업은 파일을 삭제하고 새로 생성하는 것을 의미한다. 유효성은 파일의 최종 수정일 등의 시스템 정보를 이용해 검사할 수 있다.
캐시 데이터가 몇만개씩 있다면 이 방식은 그다지 효율적이지 못하다. 파일의 수가 많으면 아이노드(inode)가 부족할 수도 있고 파일 검색이나 저장 성능에 문제가 있을 수 있기 때문에 파일 시스템에 수천 개의 파일을 두는 것은 바람직하지 못하다. 캐시 데이터의 수가 적다면 이 방법이 유용하다.
DBM 파일 사용
데이터베이스를 사용하기를 원하지 않거나 캐시 데이터를 각각의 파일로 만들기를 원하지 않는다면 DBM 파일은 좋은 방법이다. DBM 파일은 데이터베이스에서 사용하는 함수를 제공하며 속도가 빠르다. PHP에서는 다양한 DBM 방식을 사용할 수 있다. 예를 들어, SleepyCat의 DB2(http://www.sleepcat.com/)는 캐싱 시스템에 성공적으로 사용되어 왔다.
비록 DBM 파일이 일반적인 파일보다 약간 느리지만 캐시 데이터의 수가 증가함에 따라 발생하는 문제는 없다.
공유 메모리 사용
우리는 또한 PHP의 공유 메모리 함수를 이용해 캐시 데이터를 공유 메모링 저장할 수 있다. 공유 메모리를 이용함으로써 우리는 스크립트에서 접근할 수 있는 공유 메모리 세그먼트를 정의할 수 있다. PHP 스크립트는 이 세그먼트를 이용해 캐시 데이터를 저장한다. 이 방법은 다소 복잡하지만 상당히 빠르다. 그러나 메모리는 상당히 비싼 자원이기 때문에 방대한 메모리를 캐시 데이터로 낭비할 수는 없고 다른 저장 방법을 찾아보아야 한다.
메모리 캐시
만일 정말 빠른 캐싱 시스템을 원하고 서버가 리부팅되었을 경우의 데이터 손실이 중요하지 않다면 캐시 데이터를 메모리에 저장할 수 있다. 이것을 위한 가장 좋은 방법은 메모리로 맵핑되어 있는 파일 시스템에 캐시 데이터를 만드는 것이다. 리눅스 시스템에서는 이 작업이 간단하게 이루어진다.
그 다음 우리는 디렉토리에 있는 파일이나 DBM 파일을 메모리에 직접 저장하면 된다. 디스크 입출력 작업이 발생하지 않기 때문에 이 방식의 작업은 상당히 빠르다.
Import!ant
웹 서버가 많은 캐시 파일보다는 메모리를 사용하도록 하는 것이 바람직하다. 그러나 많은 메모리를 가지고 있고 성능이 매우 중요하다면 이 방법을 선택할 수 있다.
명명 규칙
명명 규칙(naming convention)은 데이터를 캐시로 저장하기 위해 사용되는 단계이며 어떤 종류의 데이터를 캐싱하느냐에 따라 다르다.
데이터 종류
처리 방법
함수 결과
캐시 이름을 cache_funcName.dat와 같은 방식으로 사용한다.
인크루드 파일 결과
캐시 이름을 lcache_fileName.dat와 같은 방식으로 사용한다.
스크립트 결과
캐시 이름을 cache_fileName.dat와 같은 방식으로 사용한다.
생성된 파일
캐시 이름을 $REQUEST_URI의 md5() 해시를 이용한다. 만일 사이트에 사용자 등록 시스템이 있다면 md5() 해시 작업을 하기 전에 사용자의 아이디를 URI에 추가해야 한다. 그렇지 않으면 모든 사용자가 같은 정보를 보게 된다.
md5()는 메시지를 암호화하는 함수이다. 이 함수는 문자열을 몇 번의 작업을 통해 128비트 암호문으로 만들어 주며 다음과 같은 특징을 가지고 있다.
만일 문자열의 일부만을 변경해도 md5 값은 변경된다.
동일한 md5 값을 갖는 두 문자열을 찾아내는 것이 어렵다.
md5 값을 이용해 원래의 문자열을 알아내는 것은 불가능하다.
따라서 md5()는 고유한 값을 가지는 문자열을 만들어내는 훌륭한 함수이며 동일한 md5 값을 가지는 두 개의 문자열을 만들어 낼 확률은 상당히 낮다. 23장에는 보안을 위해 md5()와 비슷한 기능을 하는 다른 함수가 설명된다.
유효성 검사
유효성 검사는 캐시 데이터가 유효인지 아닌지를 검사하는 가장 좋은 방법이다. 가장 일반적인 방법은 캐시 파일이나 데이터의 최종 수정일을 검사해 지정된 시간보다 나중에 만들어졌으면 유효한 것이다. 이것은 지정된 주기마다 캐시 데이터를 갱신할 수 있는 좋은 방법이다. 예를 들어, 어떤 홈페이지를 캐시에 저장해 놓고 10분마다 캐시의 내용을 갱신할 수 있다.
다른 유효성 검사는 캐시에 저장되는 데이터와 저장 방식에 따라 다르다. 예를 들어, XML 파일을 처리하고 결과를 출력하는 과정을 캐시에 저장했다면 XML 파일이 변경되었을 경우에는 쓸모가 없어진다. 이 경우에는 XML 파일의 최종 수정일을 검사하고 지정된 기간을 넘겼는지 확인해서 캐시를 갱신해야 한다.
데이터 갱신 정책
캐시 데이터가 무한정 커지기를 바라지 않는다면 캐시를 갱신하는 정책을 만들어야 한다. 오래된 캐시 데이터를 검사하는 과정은 주기적으로 실행되어야 한다. 만일 오래된 데이터가 발견되면 삭제해야 한다. 이러한 작업을 위한 알고리즘은 여러 가지가 있다.
예를 들면, "x"분 이상된 데이터, 최근에 "n"번 이하로 사용된 데이터 등을 삭제할 수 있다. 이러한 방법을 사용하려면 접근 횟수, 최근 사용 시간 등의 추가적인 정보를 캐시에 저장해야 한다. 어떤 경우에 시스템에서 이러한 정보를 제공하기도 하며 어떤 경우에는 직접 이 정보를 저장해야 한다.
일반적으로 캐시를 갱신하는 것에는 LRU(Least Recently Used) 알고리즘이 가장 적합하다. 갱신 작업은 보통 cron을 이용해 주기적으로 실시한다. 이 과정은 20장에서 설명된 crontab(UNIX 시스템)을 이용해 설정할 수 있다.
LRU 알고리즘은 캐시에서 n개의 데이터를 삭제해야 한다면 가장 오랜 시간동안 사용되지 않은 데이터를 삭제한다. 이 정책은 최근에 사용된 파일은 데이터가 사용될 경우 다시 접근하게 된다는 것을 가정하고 있다. 오랫동안 사용되지 않은 데이터는 다시 사용될 가능성이 낮기 때문에 삭제될 것이다.
캐시되어야 하는 데이터
일반적으로 컨텐츠와 데이터베이스 쿼리 두 종류의 데이터가 캐시에 저장된다.
컨텐츠 캐싱하기
컨텐츠를 캐싱한다는 것은 동적으로 생성된 컨텐츠를 하나의 파일이나 파일 그룹에 저장하고 나중에 데이터를 만들어 내지 않고 이 데이터를 가져오는 것을 의미한다. 보통 데이터의 생성 과정이 복잡하거나 느리고 다른 외부 요인에 영향을 받는 경우 캐싱이 필요하다. 컨텐츠를 캐싱하려면 두 가지 방법을 사용할 수 있는데, 생성되는 모든 컨텐츠를 캐싱하는 것과 모듈이나 코드의 일부분을 캐싱하는 것이다.
일반적인 캐싱 구조
다음은 모든 동적 페이지를 만들어 내는 페이지 엔진을 가지고 있다고 가정한 경우의 일반적인 캐싱 구조이다. 우리는 명명 규칙의 URI를 만들어 내기 위해 md5() 해시를 이용한다. 또한 PHP 출력 버퍼링을 이용해 캐시에 유효한 데이터가 없을 경우 모든 출력을 버퍼에 저장하고, 이 데이터를 파일로 저장한다. 스크립트가 다시 실행되면 이 파일을 검사한다. 만일 지정된 URI의 파일이 존재하고 10분 이상 경과하지 않았다면 데이터를 생성하지 않고 이 파일을 읽어 그 내용을 브라우저로 보낸다:
<?php
// 첫 번째로 캐시 데이터의 이름을 만든다.
$cache_name = md5($REQUEST_URI);
$time = date('U');
// 캐시에 유효한 데이터가 있는지 검사한다.
// 캐시 데이터는 10분(600초) 동안 유효하다.
if (file_exists($cache_name) && ($time - filemtime($cache_name)) < 600) {
$data = readfile($cache_name);
echo($data);
} else {
ob_start();
// 컨첸츠를 생성하는 일반적인 코드
echo("Hello world\n");
$data = ob_get_contents();
$fh = fopen($cache_name, 'w+');
fwrite($fh, $data);
fclose($fh);
ob_end_flush();
}
?>
데이터베이스 쿼리 캐싱
만일 모든 컨텐츠를 캐시로 저장할 필요가 없다면 복잡한 SELECT 쿼리의 결과만을 저장할 수도 있다. 이 경우에는 데이터베이스 쿼리를 캐싱하는 일반적인 캐시 구조를 만들기가 어려우므로 적절한 다른 방법을 이용한다. 또한 캐시 데이터의 유효성을 나타내는 플래그가 필요하고 캐시 데이터를 업데이트하는 스크립트가 필요하다.
일반적인 데이터베이스 캐싱 구조
데이터베이스 추상화 계층을 이용하면 한 곳에서 모든 데이터베이스 작업을 제어할 수 있다. 모든 쿼리를 위한 일반적인 캐시 시스템을 만드는 것도 가능하다. 어려운 점은 쿼리를 위한 명명 규칙을 위한 일반적인 캐시 시스템을 만드는 것도 가능하다. 어려운 점은 쿼리를 위한 명명 규칙을 정의하는 일과 쿼리가 데이터베이스를 업데이트했을 때 캐시 데이터가 유효하다는 표시를 하는 것이다. 이를 위해 아래와 같은 방법을 생각해볼 수 있다.
캐시 데이터 이름을 만들기 위해 SELECT 문의 md5() 해시를 이용하고 "유효한 데이터"를 나타내는 플래그를 각각의 캐시 데이터에 저장한다. 그리고 쿼리가 어떤 테이블에서 데이터를 가져오는지를 저장한다. 만일 UPDATE, INSERT, DELETE 쿼리일 경우 유효하지 않다는 표시를 하거나 변경된 테이블을 사용하는 모든 캐시 데이터를 삭제한다.
데이터베이스 쿼리 캐시를 위한 명명 규칙은 다음과 같다:
md5hash_table1_table2_table3_table4.dat
md5hash 부분은 SELECT 문의 md5() 값이며, 그 뒤에 이 쿼리에서 사용되는 모든 테이블이 나온다(사용되는 테이블을 알기 위해 쿼리를 파싱해야 한다).
SELECT 문은 md5()로 변환하기 쉽고, 리턴하면서 value_*로 시작하는 파일이 있는지 확인할 때 사용된다. 만일 없다면 쿼리를 실행하고 결과 값을 적당한 파일에 저장한다. 만일 이 파일이 존재한다면 그냥 파일에서 데이터를 가져온다.
UPDATE, DELETE, INSERT 문의 경우, 각 SQL 문에 포함된 테이블을 이용하는 모든 캐시 데이터를 검사해야 한다. 사용되는 테이블을 알기 위해 다시 쿼리를 파싱해야 하며, 이 데이블을 사용하는 캐시 데이터를 지워야 한다(이 작업은 파일 시스템 명령어를 이용하면 간단하다).
데이터베이스를 위한 일반적인 캐싱 시스템은 컨텐츠 캐싱 시스템보다 적용과 유지가 어려운 경향이 있다. 만일 컨텐츠를 캐싱하는 것이 불가능하다면 데이터베이스 쿼리의 특별한 캐싱은 비교적 간단하지만 유지하기가 힘들다.
이럴 경우 하드웨어를 업그레이드하거나 라운드 로빈(round robin), 생산자-소비자 모델(producer-consumer model)을 이용해야 한다.
PHP 엔진 최적화
마지막 최적화 방법은 PHP 엔진 자체에 관한 것이다. PHP는 실행을 위해 스크립트를 파싱해서 중간 단계(intermediate)의 코드로 변형한다. 그 다음 젠드 엔진은 이 코드를 토큰(tokens)으로 파싱하고, 내부 구조를 처리하고, 나머지 부분은 PHP로 전달한다. 최적화가 가능한 부분은 중간 단계의 코드를 캐시로 만드는 것이다. 만일 중간 단계의 코드를 캐싱해주는 적절한 제품을 사용한다면 소스코드가 변경되지 않는 이상 PHP 엔진은 소스코드를 파싱할 필요가 없다.
위의 세 가지 제품은 PHP 엔진이 동일한 소스 파일을 반복적으로 파상하지 않도록 중간 단계의 코드를 파일로 저장한다. 테스트 결과 이 세가지 제품은 모두 실행시간이 10에서 20% 정도 빨라졌다. Zend Accelarator는 차세대 젠드 캐시로, 성능 개선을 위한 고급 기능을 제공하고 스트리밍을 관리할 수 있는 기능이 추가되었다. 또한 젠드 옵티마이저도 통합되어 있다.
리눅스 시스템 관리자가 되기 위해서는 많은 것을 알아두어야 한다. 시스템 관리자의 관리 여하에 따라 많은 사람들의 시스템 장애를 초래할 수 있기 때문이다. 물론 시스템 관리자가 모든 것을 미리 예방할 수는 없다. 하지만 불가피한 상황을 제외하고는 시스템이 정상적으로 작동되도록 해야한다. 이번 호에서는 시스템, 네트워크, APM, 메일, 보안, 장애 발생시 복구 등에서 일어날 수 있는 시스템 관리자의 행동요령에 대해 알아볼 것이다. 시스템 관리자는 항상 모니터와 키보드와 함께 한다는 사실을 기억해야 한다.
막강한 시스템 길들이기
시스템이 네트워크에 연결되어 있다면, 다음과 같이 한국 표준시간 서버에서 표준시간을 받아서 설정할 수 있다.
# rdate -s time.kriss.re.kr
시스템이 온라인 상태가 아니라면 아래와 같이 수동으로 설정할 수도 있다.
# date -s ?1999-12-30 22:22:40?
위와 같이 실행하면 실행할 때만 적용되므로 이후 시간이 늦어지는 것을 막기 위해서는 주기적으로 변경 가능하게 크론(/etc/crontab)에 설정하는 것이 좋다. .profile은 로그인시 적용되는 내용들이고, rc.local은 시스템 부팅시 실행해야 할 것들을 적어 놓은 것이다. 사용자 홈디렉토리의 .profile 이 /etc에 있는 설정 파일보다 우선하기 때문에 홈 디렉토리에 .profile 에 패스를 설정해주거나 쉘 환경 파일 등을 설정해 주면 계정 내에서 적용이 된다. rc.local에는 부팅시 가장 마지막에 실행되므로 일반적으로 부팅시 실행되어야 할 데몬 등을 적어준다.
리눅스 시스템의 자원 정보는 proc 파일시스템 구조를 통해서 알 수 있다. 이는 실제로 디스크 용량을 차지하는 파일들이 아닌 가상의 디렉토리 구조이며 리눅스 커널에 의해 사용되는 시스템의 정보를 담는 곳으로 사용된다. 다음의 위치에서 하드웨어에 대한 정보 및 시스템 관련 정보들을 확인할 수 있다. 위와 같이 관련된 정보에 해당하는 파일 이름이 존재한다. 이 파일들은 텍스트 포맷이므로 cat 명령을 통해서 확인할 수 있다.
Tcp Syn Flooding은 웹으로의 공격이 대부분이므로 syn_recv 프로세스가 일정 개수가 넘게 되면 아파치를 재시작한다. 지속적인 공격일 경우 대처 방안으로 두 가지 방법이 있다. 첫째, sysctl -a |grep syn_backlog으로 확인 후 backlog를 늘려주거나 둘째, sysctl -a |grep syncookies로 확인 후 syncookies의 값을 1로 바꾸어준다. syn_backlog의 값을 조정해주는 방법은 다음과 같다.
Umount시 위와 같은 메시지가 나는 것은 unmount하려는 디렉토리에서 실행되고 있는 프로세스가 있기 때문이다. 예로 /tmp 디렉토리를 umount시키려 할 때 위의 메시지가 뜨는 경우 mysql. socket파일이 /tmp에 있는 경우를 들 수 있다. 이 경우에는 해당 파일시스템에서 실행중인 프로세스를 제거해야 하나 일일이 제거가 번거로우므로 Fuser에서 -k 옵션을 사용하면 간단히 해결할 수 있다.
Fuser -km 장치명
디렉토리나 파일 퍼미션 중 setuid는 소유자의 권한을 잠시 빌려 실행 후 권한을 돌려주고 실행을 마치게 되는데 실행도중 인터럽트가 발생한다면 정상적으로 권한을 반환하지 못하게 되어 소유자의 권한을 그대로 가지고 있게 된다. 이때 파일의 소유자가 루트였다면 이것은 보안에 문제가 될 수 있으며 이런 점을 이용해 해킹에 많이 사용된다. Setuid가 걸려 있는 파일 중에 실행권한이 있으며 루트권한일 경우에는 위험하다. 특정 디렉토 리에서 setuid가 걸려있는 파일을 찾으려면 find /usr -perm 4755와 같이 perm 옵션으로 찾을 수 있다.
다음과 같이 ~/.bash_profile를 실행해서 변경이 적용되도록 한다.
# source ~/.bash_profile
리눅스 시스템을 재부팅하고 lilo가 뜨면 ‘linux single’로 부팅한다. Tab 키를 누르면 등록되어 있는 라벨이 모두 보이므로, 여기에서 선택하도록 한다. 부팅 후 쉘 명령어 화면에서 /etc/passwd 파일에서 암호 부분을 삭제하거나 passwd를 실행하여 루트의 패스워드를 새로 설정해 준다.
# passwd root
위의 명령을 입력한 후 변경할 패스워드를 입력하면 된다.
보통 파티션을 나누는 것에 대해서 별다른 고려 없이 /로 모든 것을 잡아서 설치하는 경우가 종종 있다. 이럴 경우 설치시 편리하지만, 나중에 파일시스템에 문제가 생기거나 효율적으로 파티션을 관리하기에는 많은 어려움이 있다. 파티션을 나눌때는 어떤 용도로 쓸 것인지에 대해서 충분히 생각한 후 파티션을 해야 한다. 다음은 9.1GB 스카시 하드디스크를 기준으로 웹 서버에 이용될 서버에 대해 파티션한 경우의 예다. /var 디렉토리와 같이 항상 새로운 자료가 쌓이는 곳은 안전성이 우선시 되므로, ext3 파일시스템이 유리하며, /usr와 같이 내용 변화 없이 빠르게 액세스하여 쓸 수 있어야 하는 부분은 ext2 시스템을 이용하여 성능에 초점을 두면 좋을 것이다.
1024KB인 경우에는 블럭이 작은 만큼 4096KB보다 하드의 낭비가 적다. 1023KB의 데이터를 저장하는 경우, 기본 블럭사이즈가 1024KB일 때는 1K 공간이 사용되지만, 4096KB가 기본 블럭이라면 4K를 차지하게 된다. 하지만 아주 작은 파일들이 많은 경우 해당 데이터를 액세스하는 데는 1024KB가 4096KB보다 더 걸리게 되므로 퍼포먼스가 급격히 떨어지게 된다. 따라서 자신이 이용하는 시스템의 특성과 용도에 맞게 블럭 사이즈를 지정해서 사용하면 된다.
RAID는 ‘Redundant Array of Inexpensive (or Independant) Disks’의 약어다. RAID 시스템은 여러 드라이브의 집합을 하나의 저장장치처럼 다룰 수 있게 하고, 장애가 발생했을 때 데이터를 잃어버리지 않게 하며 각각에 대해 독립적으로 동작할 수 있도록 한다.
시스템의 다운, 데이터 손실에 대비하여 보통 여러 가지 RAID 레벨 중에서 1과 5번 방법을 많이 사용한다. RAID 1(mirroring)의 특징은 빠른 기록 속도와 함께 장애 복구 능력이 있다는 것이다. 2대의 드라이브만으로 구성할 수 있기 때문에 작은 시스템에 적합하다. 읽을 똑같은 하드가 복제되고 있으므로, 시스템에 문제 발생시 서비스 지연 시간이 매우 짧아서 웹 서비스를 하는 곳에서 유용하게 쓸 수 있다. 하지만 한 하드의 내용이 또 다른 하드에 똑같이 복사되므로 하드용량의 낭비가 심하다. RAID 5(distributed parity)는 작고 랜덤한 입출력이 많은 경우 더 나은 성능을 제공한다. 빠른 기록 속도가 필수적이지 않다면, 일반적인 다중사용자 환경을 위해 가장 좋은 선택이다. 그러나 최소한 3대, 일반적으로는 5대 이상의 드라이브가 필요하다. 변경된 내용이 있을 경우 그것만 기록한다. 일반적으로 RAID 1은 ECC 계산을 하지 않으므로 RAID 5보다 빠르고, raid5는 하드 공간을 좀 더 여유있게 쓸 수 있다는 장점을 지닌다.
먼저 시스템의 전체 용량이 어떻게 되고, 그 중에서 백업할 가치가 있는 것은 어떤 부분인지를 결정한다. 사용할 백업 장비와 종류를 알아보고, 총 백업 시간과 어느 정도 부하가 걸리는지 예상해보고 테스트 해 본 후 마지막으로 백업 스케줄을 정한다. Full 백업은 백업할 자료를 처음부터 끝까지 다 기록하는 것이고, Incremental 백업은 이전의 데이터와 비교해서 새로 추가된 내용만 백업하는 방법이다. 따라서 Full 백업시 완전히 데이터를 백업할 수 있지만 시간이 많이 걸리고, 시스템에 부하를 초래할 수 있는 반면에 Incremental 백업은 빠른 시간내에 백업을 할 수 있지만, 백업하는 시간에 따라 데이터가 완전히 백업되지 못할 경우도 있을 수 있다.
SNMP는 ‘Simple Network Management Protocol’의 약자다. 네트워크에 연결되어 있는 장치에서 네트워크에 관련된 정보를 모으고 문제점 등을 보고할 수 있는 기능을 제공하는 프로토콜이다. 구성 요소는 에이전트와 매니저가 있다. 이것은 서버/클라이언트 구조로서 에이전트가 서버에 해당되고, 매니저가 클라이언트에 해당한다. 에러가 발생하는 경우는 선택한 장비에 SNMP가 Enable이 안 되었거나, 네트워크에 문제가 있어서 모니터링 하려는 장비까지 프로토콜이 전송되지 않는 경우, community 값이 잘못 사용된 경우 등이 있다.
/etc/rc.d/init.d이 디렉토리에 있는 서비스를 ‘서비스명’ stop 또는 start 시키거나 재시작시킨다.
quota를 이용하면 된다. df 명령으로 사용자의 홈디렉토리가 있는 디바이스를 확인한다.
ilesystem 1k-blocks Used Available Use% Mounted on /dev/sda5 3028080 878480 1995780 31% / /dev/sda1 62217 7713 51291 13% /boot /dev/sda6 2759260 2088820 530276 80% /home2 /dev/sdb1 8744304 6496724 1803388 78% /home3 /dev/sdc1 35296928 25597968 7905940 76% /home4 /dev/sda10 202220 6 191774 0% /tmp /dev/sda7 1517920 1280648 160164 89% /usr /dev/sda8 608724 426992 150812 74% /var # edquota username Quotas for user jhk1: /dev/sda6: blocks in use: 47584, limits (soft = 0, hard = 0) /* 이 부분에 설정 */ inodes in use: 4590, limits (soft = 0, hard = 0) /dev/sda8: blocks in use: 4, limits (soft = 0, hard = 0) inodes in use: 1, limits (soft = 0, hard = 0) Soft는 용량에 설정되어 있는 용량은 넘어도 어느 정도 여유가 있지만, hard 용량에 설정된 크기는 절대적이다. 따라서 hard 용량을 사용자는 넘을 수 없다. 일반적으로 soft 용량을 hard 용량보다 조금 더 적게 설정해 놓는다. 쿼터 조정후 quotacheck /dev/sda6를 해줘서 체크를 해 주도록 한다.
파일명이 하이픈(-)으로 시작하는 파일
rm ./-filename 상대경로를 이용하여 파일명을 지정해줌 rm -- -filename --를 이용 그 이후에 오는 '-filename'이라는 파일이 옵 션이 아닌 파일이라는 것을 밝힘
/etc/inittab에서 사용하지 않은 가상콘솔 레벨을 주석처리 해주면 된다.
# Run gettys in standard runlevels 1:2345:respawn:/sbin/mingetty tty1 2:2345:respawn:/sbin/mingetty tty2 3:2345:respawn:/sbin/mingetty tty3 #4:2345:respawn:/sbin/mingetty tty4 #5:2345:respawn:/sbin/mingetty tty5 #6:2345:respawn:/sbin/mingetty tty6
먼저 psacct라는 패키지가 필요하다. 설치되지 않은 경우 rpm이나 소스 등을 직접 설치 한다(대부분 배포본에 기본적으로 포함되어 있으므로 그대로 사용하면 된다). 다음과 같이 명령하면 사용한 명령어를 확인할 수 있다.
더미 로그 파일 생성(데이타를 기록할 파일 생성) # touch /var/log/pacct # /sbin/accton /var/log/pacct 체크를 시작하게 하는 명령어 실행 # lastcomm 사용자계정 사용자가 수행한 명령어 체크 */ tar xvfpz 압축파일 또는 .tgz -C 특정경로 특정 파일의 절대경로(또는 파 일명)로 입력하면 된다. test.tgz 파일에서 /home/test /test.txt파일 을 /tmp 디렉토리에 압축해제를 한다면, tar xvfpz test.tgz - C /tmp /home/test/test.txt와 같이 하면 된다.
TTL이란 Time To Live의 약자다. 이것은 라우팅 에러로 인하여 데이터그램이 네트워크를 영원히 떠돌아다니는 것을 방지한다. 라우터는 네트워크 간을 이동하는 데이터그램의 TTL 필드를 감소시키며 TTL 필드가 0이 되는 데이터그램은 버린다(drop). IPv4 멀티캐스트에서 TTL은 문턱값(threshold)의 의미를 지닌다. 다음 예를 보면 그 용도가 분명해진다. 회사에서 모든 호스트가 속하는 아주 길고 대역폭에 한 부서가 대역폭을 많이 차지하는 인터넷 방송을 한다면, 랜에는 엄청난 용량의 트래픽이 발생할 것이다. 인터넷 방송도 하길 원하지만, 멀티캐스트 트래픽 때문에 인터넷 전체가 마비되어서는 안된다. 멀티캐스트 트래픽이 라우터간을 얼마나 멀리까지 이동할 수 있도록 할 것인지 제한할 필요가 있다. 이것이 TTL의 용도다.
TCP Wrapper를 사용하는 방법과 ipchains를 사용할 수 있는데 커널 2.4 버전부터는iptables을 사용한다. hosts.allow와 hosts. deny를 사용한다면, hosts.deny 파일에서 다음과 같이 모두 제한을 한다.
all : ALL
hosts.allow 파일에서 허용할 IP를 여러 개 설정할 경우 다음과 같이 스페 이스로 구분하여 준다.
all : xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx ....
시스템에 기본적으로 설치된 아래의 명령들을 사용하여 네트워크가 정상적으로 작동하지 않는 경우 여러 가지 테스트를 해볼 수 있다. /etc/sysconfig 디렉토리 밑에 하드웨어에 대한 정보가 나오는데 이더넷 카드가 여러 개 꽂혀 있다면 ifcfg-eth1, ifcfg-eth2 식으로 확인할 수 있다.
사용하는 IP를 변경하거나, 새로운 네트워크 카드 추가시에는 ifcfg-eth0 파일을 수정한 후에 반드시 ifdown ifcfg-eth0, ifup ifcfg-eth0 명령을 실행해 주어야 변경된 IP가 적용된다. 또는 /etc/rc.d /init.d/network restart를 실행해 주어도 된다.
Apache PHP MySQL
아파치에만 적용되는 내용은 아니지만 standalone으로 설정할 경우에는 /etc/rc.d/rc.local나 /etc/rc.d/rc3.d/밑에 설정되어 데몬으로 실행되며, inetd로 설정할 경우 /etc/inetd.conf에 추가되어 실행되어 텔넷이나 FTP와 같이 시스템 프로세스로 실행되므로 접속이 많은 httpd 인 경우 standalone으로 설정하여야 한다. 그리고 inetd로 설정시에는 한정된 프로세스만 수용 가능하며 반응속도가 standalone 방식에 비해 느리다.
httpd -t 옵션으로 우선 syntax error부터 확인한 후 syntax error가 있으면 먼저 수정을 해주고 Logs 디렉토리에서 에러 로그 파일을 확인하여 수정 후 재실행한다.
php3 버전의 경우 index.php3을 php4의 경우 index.php라는 파일을 다음과 같은 내용으로 작성하여 웹에서 열어보면 버전 및 연동 현황을 확인할 수 있다.
phpinfo(); ?>
먼저 php설치 후 apache 컴파일시 php 모듈 넣어서 재컴파일 해준다. ./configure --prefix=/usr/local/apache --activate- module=src/modules/php4/libphp4.a
아파치에서 bandwidth 모듈이 삽입되어 있는 상태라면 모든 호스트에 대해 1024byte로 속도를 제한하기 위해 아파치에서 설정해 주는 부분은 다음과 같다. Httpd.conf에서 BandWidthModule On라고 설정 후 BandWidth all 1024라고 설정한다.
아파치에서 index.html 파일이 없을 때 디렉토리 목록 출력을 원하지 않을 경우에는 DocumentRoot 디렉토리쪽에 설정되어져 있는 옵션에서 Indexes를 삭제한다. 또한 특정 디렉토리에서만 인덱스를 허용치 않을 경우에는 특정 디렉토리의 .htaccess 파일안에 ‘Options -Indexes’ 이 부분을 삽입하면 된다.
안전한 메일 관리법
센드메일에서 한번에 보낼 수 있는 메일 용량은 /etc/mail /send mail .cf 파일에서 MaxMessageSize 부분에서 다음과 같이 주석을 제거하고 바이트 단위로 설정을 해줄 수 있다. 받는 메일 계정의 용량은 Mlocal 부분에서 M=1000000 부분에서 바이트 단위로 제한량을 적는다.
MaxMessageSize=1000000
relay를 막는 방법도 있지만 그건 외부에서 로컬 서버를 SMTP로 사용하지 못하도록만 할 수 있으며 iptables를 이용하면 로컬 서버에서 보내는 메일에 대해 제한이 가능하다.
# iptables -A OUTPUT -p tcp --syn --dport 25 -j DROP
-A 기존의 iptable에 추가 -p 프로토콜 -dport 포트 넘버 로컬에서 외부로 보내는 메일이라면 remote의 25번 포트로 접속이 되므로 OUTPUT 패킷 중 목적지 포트가 25번인 패킷만 drop 한다. 메일 송수신은 tcp이므로 --syn을 추가하지 않을 경우에는 3 way-handshaking에 의해 메일을 받을 수도 없게 되므로 반드시 --syn을 추가해야 한다. 보내는 메일은 일단 메일큐 디렉토리에 저장된 후 발송되므로 메일큐 디렉토리를 삭제하거나 다른 이름으로 변경하면 메일을 발송할 수 없게 된다.
/etc/mail/access 파일에서 Relay 여부를 설정한다.
localhost RELAY
변경한 후 적용하려면 다음과 같이 실행해 준다. 또는 인증 기능(SMTP AUTH)이 지원되는 최신 버전의 센드메일을 사용한다. # makemap hash /etc/mail/access < /etc/mail/access
간단한 방법으로 다음과 같이 텔넷으로 센드메일 포트인 25번으로 접속해 보면 알 수 있다
# telnet jimmy.tt.co.kr 25
가상 계정을 이용해서 해결할 수 있다. 아웃룩에서 jhk라는 계정을 설정하면 jhk at jungheekim.co.kr, webmaster@jungheekim.co.kr로 오는 메일을 모 두 받아 볼 수 있다.
# vi /etc/mail/virtusertable webmaster at jungheekim.co.kr jhk(jhk계정에 webmaster라는 계정이 가상계정으로 설정)
해외에 출장이 잦은 사용자가 메일을 자신이 사용하는 웹메일로 포워딩해 달라고 하고, 회사에 돌아와서도 포워딩된 메일을 아웃룩에서 다시 받아보길 원한다면 다음과 같이 한다. 해당 사용자의 홈디렉토리 밑에 .forward 파일을 만들어서 이메일 주소를 입력하고 자신의 계정에는 \를 추가해 주어야 루프를 막을 수 있다.
vi ~junghee/.forward sitsme75 at hanmail.net, junghee.kim at tt.co.kr
메일을 확인할 수 없는 상황일 때, 메일 수신 후 자동으로 미리 작성되어 있는 메시지를 보낼 수 있는 방법(즉 자동응답 메일 작성 방법)은 자신의 홈디렉토리에 “.procmailrc” 파일을 만들고 다음의 내용을 입력한다. ------------------------------------------- :0 h c * !^FROM_DAEMON * !^X-Loop: YOUR@EMAIL | (formail -r -A"Precedence: junk" -I"From: YOUR_NAME " -A"X-Loop: YOUR@EMAIL cat $HOME/autoreply.txt) | $SENDMAIL -t -------------------------------------------- 그리고 ‘autoreply.txt’ 파일에 답변 글을 작성하면 그 내용이 자동 답변된다.
아웃룩에서 메일을 받아보려고 하는데, POP3가 다운되어 반응하지 않을 때 다음과 같이 조정한다. inetd는 기본적으로 1분에 fork 할 수 있는 인스턴스가 40으로 제한되어 있으므로 이 값을 늘려줘야 한다. POP3 부분에서 nowait.200이나 적절한 수만큼 늘려주면 된다. nowait 뒤에 반드시 .(점)을 찍고 허용할 만큼의 POP 데몬의 수를 입력한다. 이후 iinetd를 재시작하면 적용된다.
A # vi /etc/inetd.conf # Pop and imap mail services et al #pop-2 stream tcp nowait root /usr/sbin/tcpd ipop2d pop-3 stream tcp nowait root /usr/sbin/tcpd ipop3d #imap stream tcp nowait root /usr/sbin/tcpd imapd
철통 보안 관리
① 현재 서버에서 사용하지 않고, 보안상 취약점이 있는 데몬에 대해 서비스를 중지한다. ② TCP Wrapper와 ipchains를 이용한다. 커널 2.4에서는 iptables를 이용해 각 서비스에 대해서 접속을 허락하거나, 제한한다. ③ 섀도우 패스워드를 반드시 이용한다. ④ su 권한의 사용을 특정 사용자만 가능하도록 정의한다. ⑤ 원격에서 루트 권한으로 접속할 수 없도록 한다. ⑥ 지속적으로 패치한다.
echo 1> /proc/sys/net/ipv4/icmp_echo_ignore_all
다시 응답하게 하려면 다음과 같이 실행하면 된다.
echo 0> /proc/sys/net/ipv4/icmp_echo_ignore_all
보통 백도어 파일은 rm 명령으로도 삭제되지 않는다. 속성이 있을 경우 다음과 같이 삭제 한다.
# lsattr /usr/sbin/in.fingerd lsattr 1.12, 9-Jul-98 for EXT2 FS 0.5b, 95/08/09 -----a-- /usr/sbin/in.fingerd ==> a 속성이 있음을 확인
chattr -a /usr/sbin/in.fingerd chattr 1.12, 9-Jul-98 for EXT2 FS 0.5b, 95/08/09 ==> -a로 속성을 해제
lpd는 내부와 원격 프린트 작업을 수행하는 BSD 라인 프린터 데몬이다. lpd 데몬의 접근 권한을 가지고 있는 내부 시스템이나 원격 시스템의 사용자가 특별히 변형된 불완전한 프린트작업을 요청하고 이어서 프린터 큐의 디스플레이를 요청하게 되면 해당 시스템에 버퍼 오버플로우를 일으킬 수 있다. 결국 관리자 권한으로 내부 시스템에 공격 코드를 실행시킬 수 있게 된다. 따라서 패치를 해주거나 서비스를 하지 않는다면 데몬을 중지하는 것이 좋다.
BIND 4.x, 8.x에서 문제가 검출되었다. BIND 8 버전에서는 트랜잭션 시그너쳐(TSIG) 핸들링 코드에 버퍼오버플로우 취약점을 포함하고 있다. 유효한 키를 포함하지 않는 TSIG를 발견하는 경우 BIND 8 버전에서는 에러응답을 보내기 위한 코드를 실행하게 되며, 이때 발생하는 변수 초기화 방식의 차이에 의해 해당 취약점이 발생하게 된다. DNS 시스템에 대한 요청 접근만으로 해당 취약점을 발생시킬 수 있으므로 이로 인한 위험성은 크게 된다. BIND 4 버전에서는 nslookupComplain( ) 내부에 있는 문자 배열(syslog 를 위한 에러 메시지 작성 버퍼)에 대해 버퍼 오버플로우 취약점을 포함하고 있다. 특수한 포맷 형태를 가진 쿼리를 전송함으로써 해당 취약점을 발생시킨다. 또한 nslookupComplain( ) 내부에 있는 문자 배열(syslog를 위한 에러 메시지 작성 버퍼)에 대해 입력 검증(input validation) 취약점을 포함하고 있다. 이것은 특수한 포맷 형태를 가진 쿼리를 전송함으로써 입력 검증 취약점을 발생시킨다. BIND 4,8 버전에서는 해당 서버가 쿼리를 처리하는 동안 정보가 누출(information leak)될 수 있는 취약점을 포함하고 있다. 특수한 포맷 형태를 가진 쿼리 전송을 통해 공격자가 프로그램 스택에 접근할 수 있게 함으로써 해당 취약점을 발생시킨다. 해결책은 BIND 버전은 8.2.3 이상이나 9.1버전으로 업그레이드하는 것이다. 이것은 해결책이 아니라 시스템 관리자가 반드시 해야 할 일이다.
장애 발생시 복구
대부분 정전이 발생한 후에도 시스템은 정상적으로 부팅되며 파일시스템도 자동으로 check하지만 간혹 관리자가 수동으로 해주어야 하는 경우가 발생한다. 리눅스가 다운 되었을때 보통 Power OFF를 하는데, 이때 문제가 발생할 수 있으므로 Magic SysRq라는 것을 이용하여 안전하게 재부팅하는 방법을 이용한다. Magic SysRq key란 시스템의 제어가 불가능한 상태(일반적으로 ‘다운’되었다고 한다)에서도 제어를 가능하게 해주므로 커널 컴파일시 Kernel hacking ---> [*] Magic SysRq key를 체크해야 한다. Magic SysRq key를 사용하려면 다음과 같이 /proc/sys/kernel /sysrq 값을 1로 만들어야 한다.
# echo 1 > /proc/sys/kernel/sysrq lilo: linux init=/bin/sh
그러면 커널이 뜨고 나서 바로 shell prompt ‘#’가 나타난다. 이때에는 filesystem도 read only로 마운트 되고, 동작하는 deamon process도 전혀 없는 상태가 된다. 그 상태에서 수동으로 모든 파일 시스템을 체크한다.
# fsck [-t ext2] 장치명 # e2fsck 장치명
위의 명령 사용시 문제가 생긴 블록의 수정 여부를 묻게 되는데 ‘y’를 선택하고 만약 수정여부를 묻는 질문이 많다면 -y 옵션을 사용하여 자동으로 ‘y’를 선택하게 할 수 있다.
# e2fsck -y 장치명
Ctrl-Alt-Del로 리부팅하면 아주 심하게 깨지거나, 디스크에 이상이 있지 않는 한 복구가 된다.
Here is a 15 minute procedure to setup an SSL-aware Apache test-only webserver under /usr/local/apache/ (for the complete detailed installation step-by-step list please read the INSTALL file):
Fetch and extract the distributions of Apache, mod_ssl and OpenSSL
$ cd mod_ssl-2.8.28-1.3.37
$ ./configure \
--with-apache=../apache_1.3.37 \
--with-ssl=../openssl-0.9.8b \
--prefix=/usr/local/apache
$ cd ..
$ cd apache_1.3.37
$ make
$ make certificate
$ make install
Fire up your SSL-aware Apache and try it out (please replace "local-host-name" with the fully qualified domain name (FQDN) of your website which you entered at the "make certificate" step above)
1 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 2 " 인클루드의 Vim 설정 파일 3 " 마지막 수정: 2004-12-01 16:34:14 4 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 5 6 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 7 " 프로그램 기본 설정 8 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 9 10 " 메뉴를 사용하지 않는다. 대부분의 명령보다 선행되어야 한다. 11 let did_install_default_menus = 1 12 let did_install_syntax_menu = 1 13 let skip_syntax_sel_menu = 1 14 15 " 오리지널 Vi 와의 호환성을 없애고, Vim 만의 기능들을 쓸 수 있게 함. 16 set nocp 17
18 " 모든 옵션을 원래대로 복원 19 set all& 20 21 " 명령어 기록을 남길 갯수 지정 22 set hi=100 23 24 " 백스페이스 사용 25 set bs=indent,eol,start 26 27 " 인코딩 설정 28 "let &tenc=&enc 29 "set enc=utf-8 30 set fenc=utf-8 31 set fencs=utf-8,uhc,unicode 32 33 " 홈 디렉토리가 존재할 때에만 사용할 수 있는 기능들 34 if exists("$HOME") 35 36 " 홈 디렉토리를 구한다. 37 " 특정 시스템에서는 홈 디렉토리 경로 끝에 / 또는 \ 문자가 38 " 붙어 있기 때문에, 그것들을 제거한다. 39 let s:home_dir = $HOME 40 let s:temp = strpart(s:home_dir,strlen(s:home_dir)-1,1) 41 if s:temp == "/" || s:temp == "\\" 42 let s:home_dir = strpart(s:home_dir,0,strlen(s:home_dir)-1) 43 endif 44 45 " 임시 디렉토리 설정 46 if isdirectory(s:home_dir."/.vim/tmp") 47 set swf 48 set dir=~/.vim/tmp 49 else 50 set noswf 51 set dir=. 52 endif 53 54 " 백업 디렉토리 설정 55 if isdirectory(s:home_dir."/.vim/backup") 56 set bk 57 set bdir=~/.vim/backup 58 set bex=.bak 59 else 60 set nobk 61 endif 62 63 endif 64 65 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 66 " 파일 형식 설정 67 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 68 69 " 파일의 종류를 자동으로 인식 70 filetype on 71 72 " 몇몇 커스텀 확장자들에게 파일 형식 설정 73 "au BufRead,BufNewFile *.dic setl ft=php 74 75 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 76 " 편집 기능 설정 77 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 78 79 " 커서의 위치를 항상 보이게 함. 80 set ru 81 82 " 완성중인 명령을 표시 83 set sc 84 85 " 줄번호 표시 86 set nu 87 88 " 탭 크기 설정 89 set ts=4 90 set sw=4 91 92 " 탭 -> 공백 변환 기능 (사용 안함) 93 set noet 94 set sts=0 95 96 " 자동 줄바꿈 안함 97 set nowrap 98 99 " 마지막 편집 위치 복원 기능 100 au BufReadPost * 101 \ if line("'\"") > 0 && line("'\"") <= line("$") | 102 \ exe "norm g`\"" | 103 \ endif 104 105 " 클립보드를 unnamed 레지스터로 매핑 106 if has("gui_running") 107 set cb=unnamed 108 endif 109 110 " magic 기능 사용 111 set magic 112 113 " 여러 가지 이동 동작시 줄의 시작으로 자동 이동 114 set sol 115 116 " 비주얼 모드에서의 동작 설정 117 set sel=exclusive 118 119 " SHIFT 키로 선택 영역을 만드는 것을 허용 120 set km=startsel,stopsel 121 122 " 가운데 마우스 버튼으로 붙여넣기 하는 것을 무효화한다. 123 map <MiddleMouse> <Nop> 124 map! <MiddleMouse> <Nop> 125 126 " 사용자 괄호 찾기를 사용한다. 127 set mps+=<:> 128 129 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 130 " 검색 기능 설정 131 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 132 133 " 검색어 강조 기능 134 set hls 135 136 " 검색시 파일 끝에서 처음으로 되돌리기 안함 137 set nows 138 139 " 검색시 대소문자를 구별하지 않음 140 set ic 141 142 " 똑똑한 대소문자 구별 기능 사용 143 set scs 144 145 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 146 " 모양 설정 147 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 148 149 " GUI 이면, 시작시 크기 설정 150 if has("gui_running") 151 set lines=50 152 set co=125 153 endif 154 155 " 시작시 전체화면으로 설정 156 if has("win32") 157 au GUIEnter * simalt ~x 158 endif 159 160 " 추적 수준을 최대로 161 set report=0 162 163 " 항상 status 라인을 표시하도록 함. 164 set ls=2 165 166 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 167 " GUI 설정 168 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 169 170 " 폰트 설정 171 if has("gui_running") 172 set gfn=GulimChe\ 9 173 " set gfn=Jung9\ 9 174 " set gfn=Fixedsys:h12:cHANGEUL 175 " set gfn=굴림체:h9:cHANGEUL 176 endif 177 178 " GUI 여서 마우스가 사용 가능하면... 179 if has("gui_running") 180 181 " 마우스를 사용하지 않는다. 182 set mouse= 183 184 " 마우스 모델을 popup 으로 함. 185 set mousem=popup 186 187 endif 188 189 " 스크롤바를 표시하지 않는다 190 if has("gui_running") 191 set go-=l 192 set go-=L 193 set go-=r 194 set go-=R 195 set go-=b 196 endif 197 198 " 툴바를 보이지 않게 한다. 199 if has("gui_running") 200 set go-=T 201 endif 202 203 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 204 " Syntax Highlighting 기능 설정 205 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 206 207 " 파일 형식에 따른 Syntax Highlighting 기능을 켠다 208 syntax on 209 210 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 211 " indent 설정 212 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 213 214 " 자동 들여쓰기 사용 안함 215 set noai 216 217 " 똑똑한 들여쓰기 사용 안함 218 set nosi 219 220 " C indent 를 사용하는 경우 (Vim 6.2 기준으로 indent 파일이 없거나, 버그가 있는 경우) 221 au FileType php,javascript,jsp,css setl cin 222 223 " 각 언어의 표준 indent 를 사용하는 경우 224 au FileType c,cpp,html,vim,java,sh,python 225 \ if expand("<amatch>") != "" | 226 \ if exists("b:did_indent") | 227 \ unlet b:did_indent | 228 \ endif | 229 \ runtime! indent/<amatch>.vim | 230 \ endif 231 232 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 233 " 컬러 스킴 (desert) 234 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 235 236 if has("gui_running") 237 " Vim color file 238 " Maintainer: Hans Fugal <hans@fugal.net> 239 " Last Change: $Date: 2003/05/06 16:37:49 $ 240 " URL: http://hans.fugal.net/vim/colors/desert.vim 241 242 " cool help screens 243 " :he group-name 244 " :he highlight-groups 245 " :he cterm-colors 246 247 set background=dark 248 if version > 580 249 " no guarantees for version 5.8 and below, but this makes it stop 250 " complaining 251 hi clear 252 if exists("syntax_on") 253 syntax reset 254 endif 255 endif 256 let g:colors_name="desert" 257 258 hi Normal guifg=White guibg=grey20 259 260 " highlight groups 261 hi Cursor guibg=khaki guifg=slategrey 262 "hi CursorIM 263 "hi Directory 264 "hi DiffAdd 265 "hi DiffChange 266 "hi DiffDelete 267 "hi DiffText 268 "hi ErrorMsg 269 hi VertSplit guibg=#c2bfa5 guifg=grey50 gui=none 270 hi Folded guibg=grey30 guifg=gold 271 hi FoldColumn guibg=grey30 guifg=tan 272 hi IncSearch guifg=slategrey guibg=khaki 273 "hi LineNr 274 hi ModeMsg guifg=goldenrod 275 hi MoreMsg guifg=SeaGreen 276 hi NonText guifg=LightBlue guibg=grey30 277 hi Question guifg=springgreen 278 hi Search guibg=peru guifg=wheat 279 hi SpecialKey guifg=yellowgreen 280 hi StatusLine guibg=#c2bfa5 guifg=black gui=none 281 hi StatusLineNC guibg=#c2bfa5 guifg=grey50 gui=none 282 hi Title guifg=indianred 283 hi Visual gui=none guifg=khaki guibg=olivedrab 284 "hi VisualNOS 285 hi WarningMsg guifg=salmon 286 "hi WildMenu 287 "hi Menu 288 "hi Scrollbar 289 "hi Tooltip 290 291 " syntax highlighting groups 292 hi Comment guifg=SkyBlue 293 hi Constant guifg=#ffa0a0 294 hi Identifier guifg=palegreen 295 hi Statement guifg=khaki 296 hi PreProc guifg=indianred 297 hi Type guifg=darkkhaki 298 hi Special guifg=navajowhite 299 "hi Underlined 300 hi Ignore guifg=grey40 301 "hi Error 302 hi Todo guifg=orangered guibg=yellow2 303 304 " color terminal definitions 305 hi SpecialKey ctermfg=darkgreen 306 hi NonText cterm=bold ctermfg=darkblue 307 hi Directory ctermfg=darkcyan 308 hi ErrorMsg cterm=bold ctermfg=7 ctermbg=1 309 hi IncSearch cterm=NONE ctermfg=yellow ctermbg=green 310 hi Search cterm=NONE ctermfg=grey ctermbg=blue 311 hi MoreMsg ctermfg=darkgreen 312 hi ModeMsg cterm=NONE ctermfg=brown 313 hi LineNr ctermfg=3 314 hi Question ctermfg=green 315 hi StatusLine cterm=bold,reverse 316 hi StatusLineNC cterm=reverse 317 hi VertSplit cterm=reverse 318 hi Title ctermfg=5 319 hi Visual cterm=reverse 320 hi VisualNOS cterm=bold,underline 321 hi WarningMsg ctermfg=1 322 hi WildMenu ctermfg=0 ctermbg=3 323 hi Folded ctermfg=darkgrey ctermbg=NONE 324 hi FoldColumn ctermfg=darkgrey ctermbg=NONE 325 hi DiffAdd ctermbg=4 326 hi DiffChange ctermbg=5 327 hi DiffDelete cterm=bold ctermfg=4 ctermbg=6 328 hi DiffText cterm=bold ctermbg=1 329 hi Comment ctermfg=darkcyan 330 hi Constant ctermfg=brown 331 hi Special ctermfg=5 332 hi Identifier ctermfg=6 333 hi Statement ctermfg=3 334 hi PreProc ctermfg=5 335 hi Type ctermfg=2 336 hi Underlined cterm=underline ctermfg=5 337 hi Ignore cterm=bold ctermfg=7 338 hi Error cterm=bold ctermfg=7 ctermbg=1 339 else 340 " 사용하는 터미널 종류에 따라 밝음, 어두움을 설정 341 set bg=light 342 endif 343 344 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 345 " 단축키 설정 346 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 347 348 " 상용구 설정 349 iab xdate <C-R>=strftime("%Y-%m-%d %H:%M:%S")<CR> 350 iab xtime <C-R>=strftime("%H:%M:%S")<CR> 351 iab xname 인클루드 352 353 " BufExplorer 플러그인 (42) 354 nnoremap <silent> <F5> :BufExplorer<CR> 355 356 " Vim 자체 Explore 기능 357 nnoremap <silent> <F6> :Explore<CR> 358 359 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 360 " 기타 설정 361 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 362 363 " 매크로 실행중에 화면을 다시 그리지 않음 364 set lz 365 366 " 프로그램 시작시 플러그인 로드 367 set lpl 368 369 "noeol 설정 370 "au BufNew * set bin | set noeol 371 "set bin | set noeol 372 373 """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" 374 " End Of File 375 """"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
를 누르면 Cache를 지우고 모든 파일을 새로고친다는 사실.. 헉.. 웹개발에 관련되신 분들이라면 왜 이기능이 편한지 아실 것으로 사료된다. 어제까지만 해도 작업중인 내용을 검수할때 '자동으로'를 꺼놓고 '페이지열때마다'로 설정해서 시스템에 엄청난 리소스 압박이었는데. 헉.. 이렇게 간단하게 해결하다니.. 으으..