새벽 4시, 이유없이 디스크 유틸이 튄다면? 디스크 성능에 영향을 주는 크론잡

Overview

새벽에 디스크 성능에 영향을 주는 요소로는 대표적으로 백업과 같은 디비 운영적인 업무가 있습니다. 각 운영 정책에 따라 다르겠지만, 순간적인 시스템 부하에도 굉장히 민감한 서비스 경우에는 별도의 스탠바이 용도의 슬레이브 서버를 두고 그곳에서 백업을 하기 마련입니다.

이런 상황 – 마스터에서는 백업과 같은 무거운 디스크 작업이 일어나지 않는 상황에서 알 수 없는 이유로 새벽 4시 혹은 4시 22분에 디스크가 유틸이 튀는 경우가 있습니다. 그리고 가벼운 쿼리일지라도 대거 슬로우 쿼리로 잡히기도 합니다.

범인은 의외로 리눅스 설치 시 기본적으로 등록되는 두 가지 크론잡에 있는데요, 얼마 전 이와 비슷한 사례를 경험하게 되어 공유 드립니다. (단, 고수님들은 출입금지!)

Default Cron Jobs

OS 설정에 따라 다르겠지만, CentOS를 설치하게 되면 다음과 같이 배치 작업이 등록이 되어 있습니다.

$ cat /etc/crontab
01 * * * * root run-parts /etc/cron.hourly >/dev/null 2>&1
02 4 * * * root run-parts /etc/cron.daily >/dev/null 2>&1
22 4 * * 0 root run-parts /etc/cron.weekly >/dev/null 2>&1
42 4 1 * * root run-parts /etc/cron.monthly >/dev/null 2>&1

이 상태에서 별다른 변경을 하지 않았다면 다음 두 개의 크론잡이 기본적으로 동작하게 되는데, 경우에 따라 두 개의 잡이 데이터베이스의 트랜잭션 로그 혹은 데이터 파일 쪽 디스크에 영향을 주어 순간 퍼포먼스가 크게 저하될 수 있습니다.

/etc/cron.daily/mlocate.cron
/etc/cron.daily/makewhatis.cron
/etc/cron.weekly/makewhatis.cron

간단하게 위 크론잡에 대해서 알아보도록 하겠습니다.

1) mlocate

파일 검색을 빠르게 검색하기 위해, 파일에 대한 색인 정보를 모아 데이터베이스를 만드는 역할을 하며, 매일 4시에 동작합니다.
mloate.cron에 포함된 내용은 아래와 같습니다.

$ cat /ec/cron.daily/mlocate.cron
 
#!/bin/sh
nodevs=$(< /proc/filesystems awk '$1 == "nodev" { print $2 }') 
renice +19 -p $$ >/dev/null 2>&1
/usr/bin/updatedb -f "$nodevs"

또한 이와 관련된 설정은 /etc/updatedb.conf 에 위치합니다.

$ cat /etc/updatedb.conf
PRUNEFS = "auto afs gfs gfs2 iso9660 sfs udf"
PRUNEPATHS = "/afs /media /net /sfs /tmp /udev /var/spool/cups /var/spool/squid /var/tmp"
  • PRUNEFS
    updatedb 가 스캔하지 않을 파일 시스템 리스트
  • PRUNEPATHS
    updatedb 가 스캔하지 않을 디렉토리 패스 리스트

매일 새벽 4시에 도는 작업으로 renice로 우선순위를 낮춰놓아도 디스크 자원을 아래와 같이 크게 잡아먹기도 합니다.

CPU     %user     %nice   %system   %iowait    %steal     %idle
all      0.00      0.51      3.03     46.46      0.00     50.00
all      0.00      1.01      4.55     44.44      0.00     50.00
all      1.00      0.50      2.00     47.50      0.00     49.00
all      0.51      0.00      2.03     47.72      0.00     49.75
all      0.50      0.50      3.50     46.50      0.00     49.00

만약 새벽 4시정도에 디스크 유틸이 이유없이 튀고 있다면, 이와 관련하여 살펴보시기 바랍니다.

2) makewhatis

처음 크론잡을 살펴보았을 때 별 것 아니라고 생각했었지만, 매주 일요일 새벽 4시 22분에 알 수 없는 디스크 유틸을 유발하던 장본인이었습니다.

간단하게 설명하자면, man에 관련된 내용을 신규 생성 또는 업데이트하며, 월~토요일은 증분으로 일요일은 전체를 풀로 새로 작성을 합니다.

## 일단위 크론잡
$ cat /etc/cron.daily/makewhatis.cron
.. 중략 ..
makewhatis -u -w
 
## 주단위 크론잡
$ cat /etc/cron.weekly/makewhatis.cron
.. 중략 ..
makewhatis -w

일 단위에서는 -u 옵션을 주어서, 기존 데이터베이스에 단순히 업데이트를 하지만, 주 단위 작업에서는 완전히 새로 작성합니다. 평일에는 크게 문제가 없다가 매주 일요일 새벽 4시 22분 정도에 시스템이 알 수 없이 튀는 현상을 보인다면, makewhatis 작업을 의심해보시기 바랍니다. ^^
다음은 간단하게 테스트 장비에서 돌렸을 때의 시스템 상황입니다.(다를 수 있으니, 참고만 하세요. ^^)

CPU     %user     %nice   %system   %iowait    %steal     %idle
all     31.00      0.00     17.50      4.50      0.00     47.00
all     30.46      0.00     14.72      7.61      0.00     47.21
all     31.31      0.00     13.64      9.09      0.00     45.96
all     31.31      0.00     15.15      6.06      0.00     47.47
all     35.18      0.00     16.08      1.51      0.00     47.24

Conclusion

운영 정책 및 트래픽에 따라 다르겠지만, 적어도 실DB 장비에서 굳이 디스크 자원을 소비하면서까지 이러한 인덱싱 혹은 매뉴얼을 작성할 필요는 없을 것이라 생각합니다. (물론 이것은 지극히 개인적인 소견이기는 하지만..^^)

만약 새벽 4시 이후로 알 수 없는 이유로 디스크 유틸이 튀고, 이로 인하여 슬로우 로그가 대거 발생하고 있다면 리눅스 새벽 크론작업을 의심해보시기 바랍니다.

(무언가 거창하게 시작했는데.. 성급하게 끝내버린 듯한 이 기분은 무엇일까요? ^^;;)

CentOS 6.x에서 MySQL 운영 시 반드시 확인해봐야 할 파라메터!

Overview

MySQL 내부에서는 최대 허용 가능한 Connection을 설정할 수 있습니다. 하지만 OS 파라메터의 제약으로 때로는 임계치만큼 Connection을 늘릴 수 없는 경우가 발생하기도 합니다. 게다가, 만약 OS가 업그레이드되면서 관련 Default Value 가 변경되었다면? 이유없는 장애가 발생할 수도 있는 것이죠.

오늘은 OS 파라메터 중 CentOS 버전 별 nproc 값에 의한 Max Connection 제한에 대해 포스팅하겠습니다.

Environment

1) CentOS 5.8

CentOS 5.x버전의 nproc(Max User Processes) 기본 값은 다음과 같습니다.

$ ulimit -a | grep processes
max user processes              (-u) 4095

2) CentOS 6.3

이에 반해 CentOS 6.x버전부터는 /etc/security/limit.conf에 nproc에 특별한 설정을 하지 않는 한 1,024를 기본값으로 가집니다.

$ ulimit -a | grep processes
max user processes              (-u) 1024

이 설정은 CentOS 6.x버전부터 사용자 로그인 시 하단 파일에서 1,024 값을 기본값으로 세팅하며, 시스템 리소스를 제한하고자 새로 추가된 설정입니다.

$ cat /etc/security/limits.d/90-nproc.conf
# Default limit for number of user's processes to prevent
# accidental fork bombs.
# See rhbz #432903 for reasoning.
*          soft    nproc     1024

Connection Test

MySQL DB 재시작 직후 pstree  명령으로 확인을 하면 16개의 데몬이 떠있는 것으로 확인됩니다. 물론 top 혹은 ps 명령어로 확인 시에는 단일 프로세스로 확인됩니다.

$ pstree | grep mysql
     |-mysqld_safe---mysqld---16*[{mysqld}]

이 상태에서 단순 Connection만 늘리는 프로그램을 돌려봅니다. 임계치(2000개)만큼만 Connection을 생성하는 간단한 JAVA 프로그램을 로직이며, Connection Open 이후 별다른 Close 작업을 하지는 않습니다.

아래 로직을 CentOS5.8, CentOS6.3에서 nproc 기본 값과 일부 변경 이후 DB를 재시작하여 테스트를 진행합니다.

import java.sql.*;
import java.util.Random;
public class Test {
        public static void main(String[] argv) throws ClassNotFoundException, SQLException{
            // Set Connection Limit
            int connLimit = 2000;

            Connection[] conn = new Connection[connLimit];
            Class.forName("com.mysql.jdbc.Driver");

            // Get Connection
            for(int i = 0; i < connLimit; i++){
                System.out.println(i);
                conn[i] = DriverManager.getConnection("jdbc:mysql://10.0.0.101:3306/dbatest","dbatest","");
            }

            // To Keep Java Program - no exit
            Statement stmt = conn[0].createStatement();
            for(int i = 0;;i++){
                stmt.execute("select sleep(5)");
                System.out.println(i+"th!!");
            }
        }
}

Test Result – CentOS 5.8

1) Default (nproc = 4095)

2,000개 신규 Connection생성에는 문제가 없었습니다.

mysql> select count(*)
    -> from information_schema.processlist;
+----------+
| count(*) |
+----------+
|     2001 |
+----------+
1 row in set (0.01 sec)

2) nproc 변경 (nproc = 200)

그러나 다음과 같이 nproc 값을 200으로 설정 후 동일한 테스트를 진행합니다.

## change max user processes limit
$ ulimit -u 200

## DB restart
$ /etc/init.d/mysqld restart

신규 Connection을 183개 생성 후 Java 콘솔에서 다음 에러와 함께 프로그램이 종료됩니다.

Exception in thread "main" java.sql.SQLException: null,  message from server: "Can't create a new thread (errno 11); if you are not out of available memory, you can consult the manual for a possible OS-dependent bug"

위 상태에서 Linux 콘솔에서 신규 Connection 생성 시에도 다음과 같은 에러가 발생합니다. 하단의 경우 TCP/IP가 아닌 Socket을 통한 접근입니다.

$ /usr/local/mysql/bin/mysql -uroot
ERROR 1135 (HY000): Can't create a new thread (errno 11); if you are not out of available memory, you can consult the manual for a possible OS-dependent bug

pstree로 확인한 mysqld 프로세스 개수입니다.

pstree | grep mysql
     |-mysqld_safe---mysqld---199*[{mysqld}]

Test Result – CentOS 6.3

1) Default (nproc = 1024)

앞선 테스트와 동일하게 Java 프로그램을 실행하였을 때 2,000개의 신규 세션을 맺을것으로 기대되나, 1007개 세션 생성 이후 다음과 같은 에러가 발생합니다.

Exception in thread "main" java.sql.SQLException: null,  message from server: "Can't create a new thread (errno 11); if you are not out of available memory, you can consult the manual for a possible OS-dependent bug"

2) nproc 변경 (nproc = 4095)

nproc를 4095개로 설정 및 DB재시작 후 앞선 테스트를 동일하게 수행합니다.

## change max user processes limit
$ ulimit -u 4095

## DB restart
$ /etc/init.d/mysqld restart

결과적으로 2000개의 세션을 생성하는데 문제가 전혀 없습니다.

mysql> select count(*) 
    -> from information_schema.processlist;
+----------+
| count(*) |
+----------+
|     2001 |
+----------+
1 row in set (0.01 sec)

Solution

가장 간단한 해결 방안은 limit.conf 파일에 nproc 값을 넣는 방법입니다.

$ vi /etc/security/limits.conf
## 하단 라인 추가
*          -    nproc     4095

MySQL 재시작 후 위 파라메터가 제대로 적용이 됐는 지 확인합니다. Max Processes값이 여전히 1024라면 콘솔에 다시 접속하여 MySQL을 재시작합니다.

$ cat /proc/<mysql_pid>/limits
Limit                     Soft Limit           Hard Limit
Max cpu time              unlimited            unlimited 
Max file size             unlimited            unlimited 
Max data size             unlimited            unlimited 
Max stack size            10485760             unlimited 
Max core file size        0                    unlimited 
Max resident set          unlimited            unlimited 
Max processes             4095                 4095      
Max open files            50000                50000     
Max locked memory         32768                32768     
Max address space         unlimited            unlimited 
Max file locks            unlimited            unlimited 
Max pending signals       4095                 4095      
Max msgqueue size         819200               819200    
Max nice priority         0                    0         
Max realtime priority     0                    0

Max processes 값이 4095로 상향 조정된 것을 확인할 수 있습니다.

어디에 관련 값을 명시하는 것이 좋을 지는 시스템에 맞게 설정하시면 되겠어요. ^^ 특히나 여러 서버가 동시에 올라오는 공유 서버라면, nproc 파라메터 변경 하나로 전체 프로세스에 영향을 미치며 서버 리소스를 크게 잡을 수도 있기 때문이죠. ㅎ

Conclusion

Active Session이 1,000개인 상태라면 분명 DB에 상당한 부하가 발생합니다. 예전 벤치마킹에서 Active Session이 50개 이상부터는 QPS가 더이상 증가하지도 않는 결과도 나왔습니다.

하지만 실 사용환경에서는 Active Session이 50개 미만이나, Connection 수는 1,000개 이상 존재하는 경우는 다분합니다. 단순히 Connection만 맺고 특별한 SQL을 실행하지 않으므로 Sleep 상태로 머물러 있는 상태이죠.

이 경우 DB내부 Connection 제한이 아닌 OS Process Limit 개수 영향으로 예기치 않는 문제가 발생할 수 있습니다. 분명 MySQL의 세션은 쓰레드임에도 불구하고, OS에서는 Process로 인식하는 현상은 참으로 놀라운 일이네요. ^^;; 왜그럴지..ㅎㅎ

특히, CentOS 5.x버전에서 CentOS 6.x버전으로 OS를 업그레이드하였다면, 관련 서버 설정을 점검하여 잠재적인 장애 이슈를 사전에 제거할 필요가 있습니다.

리눅스에 MySQL 설치하기(CentOS 5.6)

MySQL DBMS 를 설치할 때 제가 적용하는 내용을 공유합니다.
root 계정으로 설치 준비를 하고, mysql 계정으로 DB를 구동합니다.
일단 하단 내용들은 root계정으로 수행을 합니다. 

OS 계정 추가

다음과 같이 dba 그룹을 추가하고 그 밑에 mysql 계정을 추가합니다.

groupadd -g 600 dba
useradd -g 600 -u 605 mysql
passwd mysql

Linux 설정 변경

세션 Limit 를 설정합니다.

vi /etc/security/limits.conf
##하단 내용 추가
mysql            soft    nproc  8192
mysql            hard    nproc  16384
mysql            soft    nofile 8192
mysql            hard    nofile 65536

OS에서 limits.conf 파일을 읽어들이도록 설정합니다. 없으면 생성합니다.

vi /etc/pam.d/login
## 하단 내용 추가
session    required     pam_limits.so

/etc/profile 에 다음 내용을 추가하여 login 시 적용되도록 합니다.

vi /etc/profile
##
if [ $USER = "mysql" ]; then
  if [ $SHELL = "/bin/ksh" ]; then
    ulimit -p 16384
    ulimit -n 65536
  else
    ulimit -u 16384 -n 65536
  fi
fi

MySQL 데이터 저장 디렉토리를 생성합니다.

mkdir -p /data/mysql/mysql-data
mkdir -p /data/mysql/mysql-tmp
mkdir -p /data/mysql/mysql-iblog
mkdir -p /data/mysql/mysql-binlog

MySQL 설치 파일 다운로드

하단 실행 시 x86_64 가 있으면 64비트이고, i686 이 있으면 32비트입니다.

## OS 버전 확인 ##
uname -a
Linux ..중략.. EDT 2010 x86_64 x86_64 x86_64 GNU/Linux

이제 MySQL Download 의 “Linux – Generic” 탭에서 자신의 OS에 맞는 MySQL Server 받으세요. 현재 Release되는 주 버전은 MySQL 5.5.x이나, 여기서는 MySQL 5.1.57 64비트 버전으로 설명드리겠습니다.

굴욕적이지만, 한국보다는 일본 mirror서버에서 받는 것이 빠르다는..-_-;;

cd /usr/local/
## 설치 파일 다운로드
wget http://dev.mysql.com/get/Downloads/MySQL-5.5/mysql-5.5.19-linux2.6-x86_64.tar.gz/from/http://ftp.iij.ad.jp/pub/db/mysql/

MySQL 기본 설정

시스템에 따라 데이터 파일과 같은 일부 변수 값이 달라질 수 있으니, 자신의 시스템에 맞게 수정해서 사용하세요.

vi /etc/my.cnf
[client]
port = 3306
socket = /tmp/mysql.sock

[mysqld]
# generic configuration options
port = 3306
socket = /tmp/mysql.sock

back_log = 100
max_connections = 500
max_connect_errors = 10
table_open_cache = 2048
max_allowed_packet = 16M
join_buffer_size = 8M
read_buffer_size = 2M
read_rnd_buffer_size = 16M
sort_buffer_size = 8M
bulk_insert_buffer_size = 16M
thread_cache_size = 128
thread_concurrency = 16
query_cache_type = 0
default_storage_engine = innodb
thread_stack = 192K
lower_case_table_names = 1
max_heap_table_size = 128M
tmp_table_size = 128M
local_infile = 0
max_prepared_stmt_count = 256K
event_scheduler = ON
log_bin_trust_function_creators = 1
secure_auth = 1
skip_external_locking
skip_symbolic_links
#skip_name_resolve

## config server and data path
basedir = /usr/local/mysql
datadir = /data/mysql/mysql-data
tmpdir = /data/mysql/mysql-tmp
log_bin = /data/mysql/mysql-binlog/mysql-bin
relay_log = /data/mysql/mysql-binlog/mysql-relay
innodb_data_home_dir = /data/mysql/mysql-data
innodb_log_group_home_dir = /data/mysql/mysql-iblog

## config character set
##utf8
character_set_client_handshake = FALSE
character_set_server = utf8
collation_server = utf8_general_ci
init_connect = "SET collation_connection = utf8_general_ci"
init_connect = "SET NAMES utf8"

## bin log
binlog_format = row
binlog_cache_size = 4M

## Replication related settings
server_id = 1
expire_logs_days = 7
slave_net_timeout = 60
log_slave_updates
#read_only

## MyISAM Specific options
key_buffer_size = 32M
myisam_sort_buffer_size = 8M
myisam_max_sort_file_size = 16M
myisam_repair_threads = 1
myisam_recover = FORCE,BACKUP

# *** INNODB Specific options ***
innodb_additional_mem_pool_size = 16M
innodb_buffer_pool_size = 2G

innodb_data_file_path = ibdata1:1G:autoextend
innodb_file_per_table = 1
innodb_thread_concurrency = 16
innodb_flush_log_at_trx_commit = 2
innodb_log_buffer_size = 8M
innodb_log_file_size = 128M
innodb_log_files_in_group = 3
innodb_max_dirty_pages_pct = 90
innodb_flush_method = O_DIRECT
innodb_lock_wait_timeout = 120
innodb_support_xa = 0
innodb_file_io_threads = 8

[mysqldump]
quick
max_allowed_packet = 16M

[mysql]
no_auto_rehash

[myisamchk]
key_buffer_size = 512M
sort_buffer_size = 512M
read_buffer = 8M
write_buffer = 8M

[mysqlhotcopy]
interactive_timeout

[mysqld_safe]
open_files_limit = 8192

MySQL Server 설치

## 압축 해제
cd /usr/local
tar xzvf mysql-5.5.19-linux2.6-x86_64.tar.gz
## 관리를 위한 심볼릭 링크 생성
ln -s mysql-5.5.19-linux2.6-x86_64 mysql
## 설치 파일 권한 변경
chown -R mysql.dba /usr/local/mysql*
## 시작 스크립트 복사
cp mysql/support-files/mysql.server /etc/init.d/mysqld
## 관련 파일 권한 설정
chown mysql.dba /data/mysql/*
chown mysql.dba /etc/my.cnf
chown mysql.dba /usr/local/mysql*

여기서부터는 이제 mysql 계정으로 실행을 합니다.
관리를 위해서 몇몇 alias를 설정하는 부분입니다.^^

su - mysql
cat >> ~/.bash_profile
## 하단 내용 입력
export MYSQL_HOME=/usr/local/mysql
export PATH=$PATH:$MYSQL_HOME/bin:.
export ADMIN_PWD="ROOT 패스워드"

alias ll="ls -al --color=auto"
alias mydba="mysql -uroot -p$ADMIN_PWD"
alias mymaster="mysql -uroot -p$ADMIN_PWD -e'show master status;'"
alias myslave="mysql -uroot -p$ADMIN_PWD -e'show slave status\G'"
alias mh="cd $MYSQL_HOME"
alias md="cd /data/mysql/mysql-data"
alias mt="cd /data/mysql/mysql-tmp"
alias mb="cd /data/mysql/mysql-binlog"
alias mi="cd /data/mysql/mysql-data"
alias dp="cd /data/mysql/mysql-data"

## 환경 변수 적용
. ~/.bash_profile

MySQL Server 구동

cd /usr/local/mysql
## 기본 데이터베이스 설치
./scripts/mysql_install_db
## MySQL 데몬 Startup
/etc/init.d/mysqld start

MySQL 보안 설정

처음 DB를 올리면 보안 면에서 취약한 부분이 있습니다.
기본적인 보안 정책을 적용하도록 합니다.
mysql root  계정 패스워드만 설정하고 나머지는 Enter만 쭉 치면 됩니다.

cd /usr/local/mysql
./bin/mysql_secure_installation
NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MySQL
SERVERS IN PRODUCTION USE! PLEASE READ EACH STEP CAREFULLY!

In order to log into MySQL to secure it, we'll need the current
password for the root user. If you've just installed MySQL, and
you haven't set the root password yet, the password will be blank,
so you should just press enter here.

Enter current password for root (enter for none):
OK, successfully used password, moving on...

Setting the root password ensures that nobody can log into the MySQL
root user without the proper authorisation.

Change the root password? [Y/n]
New password:
Re-enter new password:
Password updated successfully!
Reloading privilege tables..
... Success!

By default, a MySQL installation has an anonymous user, allowing anyone
to log into MySQL without having to have a user account created for
them. This is intended only for testing, and to make the installation
go a bit smoother. You should remove them before moving into a
production environment.

Remove anonymous users? [Y/n]
... Success!

Normally, root should only be allowed to connect from 'localhost'. This
ensures that someone cannot guess at the root password from the network.

Disallow root login remotely? [Y/n]
... Success!

By default, MySQL comes with a database named 'test' that anyone can
access. This is also intended only for testing, and should be removed
before moving into a production environment.

Remove test database and access to it? [Y/n]
- Dropping test database...
ERROR 1008 (HY000) at line 1: Can't drop database 'test'; database doesn't exist
... Failed! Not critical, keep moving...
- Removing privileges on test database...
... Success!

Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.

Reload privilege tables now? [Y/n]
... Success!

Cleaning up...

All done! If you've completed all of the above steps, your MySQL
installation should now be secure.

이제 MySQL DB 설치가 다 끝났습니다. 참 쉽죠잉~!^^