5.1.xx 에서 5.6.xx 로 업그레이드 할 일이 생겼다.

table partition 을 적용하는데 5.5 이후 버전에서 가능한 상황이었는데,
그래서 일단 사용중이던 VM 을 하나 새로 복사해서 테스트를 해봤다.

yum 으로 설치를 했는데,
일단 MySQL 용 yum repository 파일을 다운로드 받았다.




그리고 yum 의 repository 에 MySQL 5.6 을 추가했다.


For an EL6-based system, the command is in the form of:

shell> sudo yum localinstall mysql-community-release-el6-{version-number}.noarch.rpm  




기존 MySQL 도 삭제해야 하는데,
yum remove 명령어로, mysql 관련 패키지들을 모두 지웠다.

그리고 mysql-community-server 를 yum 으로 설치!

설치가 끝나고 나서 우여곡절이 좀 있긴 했지만, 어찌됐던 설치는 마무리 되었다.

우여곡절은.. yum 으로 설치하려는데 5.1 의 패키지와 충돌이 나는 문제가 있었는데,
yum remove 를 모두 해주지 않아서 생겼던 문제인 것 같기도 한 상태이나 정확한 원인은 모르겠다.

분명히 안됐었는데 갑자기 되기 시작했거든.. -_-;

원인 파악 안된 상태에서 해결됐다고 넘어가는거 안좋아하지만, 암튼 원인을 모르기때문에 어쩔수가 없네 ㅠ_ㅠ

여기까지 해서 mysqld 가 뜨기는 했다.



그리고 기존 데이터베이스를 통으로 덤프 떠서 넣고, 서버를 실행시켜서 클라를 붙였더니 (mysqldump --all-databases 옵션 사용)
이번에는 "Cannot load from mysql.proc." 에러가 발생했다.




mysql_upgrade 를 안했는데 되는게 당연히 안되는게 맞지.

그래서 mysql_upgrade 를 수행했는데, 이게 또 엄청 안되더라.

에러도 그냥 FATAL Error 라고 하면서 안되는 황당한 상황.

여기저기 찾아봤는데 --force 옵션을 주라는 말만 나오고, 답이 쉽게 나오지 않는 상태였는데,
결과적으로 이야기해보면, mysql_upgrade 로 준 옵션으로 mysql 서버에 연결이 안되서 발생한 문제였음

-h 로 서버 주소를 주고나서 괜찮아졌다.

원인을 바로 알려주지 않는 에러메시지는 너무 힘들다. 그래서 이렇게 남겨둔다!

끝!

상황


글로벌 서비스 준비중인데, 모든 시간을 KST 로 작업하고 있었다는..


그래서 모두 UTC 기준으로 변경을 하고 싶어서 프로그래머들끼리 가볍게 이야기를 나눠봤다.


여기서 전제조건!


1. 퍼블리셔는 OS 시간을 변경해줄 수 없음

2. 기존 Code 의 변경은 최소화 (로직을 변경하면서 버그를 만들 수 있는 상황이 아님)



우선, 기준이 되는 UTC 를 사용하려면, DB 에서 쓰고있던 Procedure base logic 들도

CURRENT_TIMESTAMP() -> UTC_TIMESTAMP() 로 변경 (1) 이 필요했고,

Java Code 에서 Calendar, Date, Timestamp 를 쓰는 코드를 모두 점검 (2) 해야 했다.


(1) 은 replace 가 용이한 상황이어서 난이도 하하하

(2) 는 일일이 봐야해서 난이도가 높다고 생각했지만 생각보다 코드가 많지 않아서 중하하


라고 판단함




수정 감행


시간이 그리 많지 않아서 신속하게 결정하고, 아래와 같이 진행했다.



1. TimeZone 기반으로 Calendar 를 하나의 함수를 통해 얻어오도록 수정


기존에는 Calendar.getInstance() 를 여기저기서 호출했는데,

특정 날짜관련 클래스를 하나 만들고 거기서 getNow() 를 통해 Calendar 를 얻어가도록 수정



2. DB 의 모든 CURRENT_TIMESTAMP() 를 UTC_TIMESTAMP() 로 수정


기존에는 CURRENT_TIMESTAMP() 를 사용하고 있어서

MySQL 의 서버 시간 설정에 의한 시간값을 얻어가고 있었는데

UTC_TIMESTAMP() 를 이용함으로써, 서버 시간설정에 영향을 받지 않도록 수정



그 후, 원하는대로 값이 저장되는지 확인해봤다.


그런데, 좀 이상했다..


분명, WAS Code 에서 2015-07-01 00:00:00 을 얻었는데,

DB 에 2015-07-01 09:00:00 이 저장됨


그리고 WAS Code 에서 그 값을 읽어서 클라이언트로 전달하면서

클라이언트는 그 시간이 UTC 라고 믿고 기기가 KST 를 표시해야 하니, +09:00 를 추가로 함 -_-


서버가 클라로 줘야하는 시간이 UTC 로 약속되어있기 때문에,

서버에서 DB 의 값을 읽을 때 UTC 로 변환해야할 필요가 있는것 처럼 느껴졌었는데,


Calendar 를 얻어가는 함수 내부에서 TimeZone 을 UTC 로 설정하고 있기 때문에

코드상의 문제는 아닌것 같았고, 좀더 debugging 해서 결국 문제점을 찾았다.




핵심 문제


핵심 문제는 Timestamp 를 사용해서 DB 쪽으로 인자를 전달하는 과정에서

Timestamp 값이 Calendar의 TimeZone 에 의해 변경되지 않고

JVM의 TimeZone 에 의해 결정된 시간으로 저장되는 것을 몰랐던 것이 문제였다.


깊이 몰랐던 것이 문제여서, 이번 기회에 정리를 해두려고 한다.



Calendar 의 속성


Calendar 는 setTimeZone 에 의해 설정된대로 충실히 동작한다.


예를 들어, 2015-07-01 00:00:00 KST 인 값도 TimeZone 만 변경하면

get(Calendar.HOUR_OF_DAY) 를 했을때 설정된 TimeZone 에 의한 값을 return 한다.


set(Calendar.HOUR_OF_DAY, hour) 도, 내부적으로 설정된 TimeZone 에 의한 시간으로 set 한다.


그래서 현재 Calendar 가 무슨 TimeZone 을 갖는지 확인하고,

비교하는 데이터가 동일한 TimeZone 의 값인지 조심해야 한다.



Date 의 속성


Date 는 TimeZone 과 Independent 하다.


Calendar.getInstance().getTime() 의 Date 는, 내부적으로 fastTime 을 갖고 있고,

이 값이 TimeZone 에 의해 해석되는 형식이다.


여담인데, CallableStatement 의 인자를 setDate 로 넘기면, 날짜만 넘긴다.. -_-



MySQL 의 속성


MySQL 에도 TimeZone 이 엄연히 존재하지만,

JDBC 의 setTimestamp 를 통해 전달된 인자를 변경 없이 충실히 저장해주기 때문에

MySQL 내부에서 UTC_TIMESTAMP() 를 사용하면, 사실상 TimeZone Independent 한 작업을 할 수 있을 것 같다.




결론


여하한 이유로 OS 의 시간을 변경할 수 없다면,

JVM 에서 UTC 를 사용하고, MySQL 에서 UTC_TIMESTAMP() 를 사용하는게 좋은 것 같다.


만약, JVM 에서 UTC 를 설정하기조차 할 수 없었다면,

너무 슬펐을 것 같다. -_-


마지막으로, Tomcat6 에서 TimeZone 설정 변경법




/etc/tomcat6/tomcat6.conf 


... 


# Cannot resolve user database reference - naming-factory-dbcp

# the real problem is a cnfe that is avoided by configuring 

# the -Djavax.sql.DataSource.Factory. This fixes the rpm install. 

JAVA_OPTS="${JAVA_OPTS} -Djavax.sql.DataSource.Factory=org.apache.commons.dbcp.BasicDataSourceFactory -Duser.timezone=GMT” 


... 



특정 DB 에 있는 테이블의 스키마가 필요하다는 형의 연락에,
간단하게 table name, column name, column type 을 보여주는 파이썬 코드를 만들어보았음

__author__ = 'novice'

import pymysql

host_addr = '아이피'
user_acc = '디비 계정'
passwd = '비번'
db_name = '데이터베이스'

conn = pymysql.connect(host=host_addr, user=user_acc, passwd=passwd, db=db_name)

cur = conn.cursor(pymysql.cursors.DictCursor)
cur.execute('select * from information_schema.tables where table_schema = %s', db_name)

for row in cur:
table_name = row['TABLE_NAME']

print('-------------------------------------------------------------------------------------')
print('TABLE NAME : ', table_name)
print('-------------------------------------------------------------------------------------')

t_cur = conn.cursor(pymysql.cursors.DictCursor)
t_cur.execute('select * from information_schema.columns where table_schema = %s and table_name = %s',
(db_name, table_name))

for columns in t_cur:
print(columns['COLUMN_NAME'], '\t', columns['COLUMN_TYPE'], )

print('-------------------------------------------------------------------------------------')
print()

t_cur.close()

cur.close()
conn.close()

끝!


플레이어가 성장하는 도중 어디서 이탈을 많이 하는지 측정하기 위해, Stage 진행에 대한 Funnel 정보 (이하, Stage Funnel) 를 그려봤다.
대부분의 아이디어는 컨설팅을 받을 때 배운 것들이고, 그때 했던 것을 복기하는 차원에서 혼자서 해봤다.
우선 간단하게 데이터 정제를 DB 쿼리로 해봤고, 특정 스테이지 기준으로 더이상 정보가 없는 경우 해당 스테이지에 머물렀다고 간주했음
기간이 영향을 줄 수 있는데, 우선은 Funnel 을 그리는데에 집중했고, 이탈을 정의하는 방법에 따라 쿼리의 Where 절을 변경하면 되겠다.
  • 생각의 전개

    • 플레이어의 성장을 측정하기 위해 성장 기준을 무엇으로 삼을지 고민

    • “레벨", "스테이지 진행" 중 "스테이지 진행”을 선택

      • 레벨은 직관적이지만 후반 구간일수록 대변하는 플레이어 집단의 특성이 분산될 것으로 예상

        예를 들어, 특정 레벨 구간 N 의 플레이어 집단에 대해
        소유한 재화의 평균을 M(N), 표준편차를 D(N) 이라고 하고,
        3Lv과 50Lv 을 서로 비교해보면,
        M(3) 과 M(50) 의 관계는 예측하기 어렵겠지만, 적어도 D(3) < D(50) 일 가능성이 크다고 생각된다

        요약) 플레이를 많이 함 -> 재화 획득 / 소진을 여러번 경험함 -> 보유 재화의 분산이 큼

      • 스테이지 진행도 뒤로 갈수록 레벨과 비슷한 양상을 보이게 되나,
        레벨보다는 훨씬 덜 할 것으로 판단되는데,
        특정 스테이지를 플레이 해도 레벨이 오르지 않는 경우가 많은 것을 생각해보면 그저 당연한 일일 뿐..

    • 특정 스테이지에 머물러 있다는 정보도 중요하지만,
      사실 “왜” 머물러 있는지가 가장 중요할거라고 생각하고,
      “왜” 에 대한 가장 단순한 접근으로, 머물러 있기 직전에 해당 스테이지를 클리어 했는지 여부를 기준으로 그려봤음

    • 실패했는데 나가는것과 성공했는데 나가는 것이 어떤 의미를 갖고 있을지 고민중...

library(RMySQL)

# connect to db
conn <- dbConnect(MySQL(), user='', password='', dbname='', host='')

# select tidy data for stage funnel from stats log db
rs <- dbSendQuery(conn, "select cid, max(cd6) as field, 'lose' as result from _stats_log
     where ts > '2015-01-01 00:00:00'
          and ea like '%lose%'
          and el = 'player'
          and ec = 'combat'
     group by cid
union all
select cid, max(cd6), 'win' as result from _stats_log
     where ts > '2015-01-01 00:00:00'
          and ea like '%win%'
          and el = 'player'
          and ec = 'combat'
     group by cid;
")

# fetch result set
table <- fetch(rs, -1)

# clear db resources
dbClearResult(rs)
dbDisconnect(conn)

# result and field are factor
table$result <- as.factor(table$result)
table$field <- as.factor(table$field)

# show summary information
summary(table)

# draw result
library(ggplot2)

p <- ggplot(table, aes(field, fill=result))
p + geom_bar(position="dodge")



R 에서 의사결정나무 (Decision Tree) 를 간단하게 그려봤다

g1_grade, g1_type 의 값에 따라 succeeded 가 결정된다고 가정하고
이를 위한 예제용 데이터를 만들어봤다

의사결정 나무를 그리는 코드는 여기를 참고해서 만들어봤음 : http://www.statmethods.net/advstats/cart.html

코드 예제에서 rpart method=“anova” 로 되어있는데, 그렇게 그렸더니 원하는 결과가 나오지 않아서
박장시님 도움으로 rpart method=“class” 라고 써야한다는 것과 rattle package 에 대해 배움! - 장시님, 감사 (_ _)



# install prerequisite packages
# 이건 처음에 한번만 하면 됨
install.packages("rpart")
install.packages("rpart.plot")
install.packages("rattle")



# load library of rpart
library(rpart)
library(rpart.plot)
library(rattle)


# make sample data
df <- data.frame(g1_grade=3, g1_type=1, succeeded=1, seq=1:1000)
df <- rbind(df, data.frame(g1_grade=3, g1_type=3, succeeded=0, seq=1:800))
df <- rbind(df, data.frame(g1_grade=3, g1_type=2, succeeded=0, seq=1:900))
df <- rbind(df, data.frame(g1_grade=3, g1_type=1, succeeded=0, seq=1:100))
df <- rbind(df, data.frame(g1_grade=2, g1_type=1, succeeded=1, seq=1:100))
df <- rbind(df, data.frame(g1_grade=2, g1_type=1, succeeded=0, seq=1:800))
df <- rbind(df, data.frame(g1_grade=2, g1_type=2, succeeded=0, seq=1:800))
df <- rbind(df, data.frame(g1_grade=2, g1_type=2, succeeded=1, seq=1:100))
df <- rbind(df, data.frame(g1_grade=2, g1_type=3, succeeded=1, seq=1:200))
df <- rbind(df, data.frame(g1_grade=2, g1_type=3, succeeded=0, seq=1:900))

# show structure of df
str(df)
'data.frame': 5700 obs. of 4 variables:
$ g1_grade : num 3 3 3 3 3 3 3 3 3 3 ...
$ g1_type : num 1 1 1 1 1 1 1 1 1 1 ...
$ succeeded: num 1 1 1 1 1 1 1 1 1 1 ...
$ seq : int 1 2 3 4 5 6 7 8 9 10 ...

# make decision tree
tree <- rpart(succeeded ~ g1_type + g1_grade, data = df,
               method = "class", control = rpart.control(minsplit = 10))


# draw it with just simple plot!
plot(tree)
text(tree, use.n=TRUE, all=TRUE, cex=.8)

# check working directory
getwd()

# create post script file to export tree to pdf
post(tree, file = "tree.ps", title = "How to success")

# draw it with fancy tree!
fancyRpartPlot(tree)




그림들은 모두 같은 그림이고 포장만 좀 다르다.
fancyRpartPlot 이 가장 보기 좋다!

데이터를 해석해보자면,

* 전체 데이터 중 75% 는 succeeded = 0, 25% 는 succeeded = 1
     이게 조금 헛갈릴 수 있는데, 각 노드의 두번째 줄에 표시된 값이
     평가 대상 변수의 분포 비율이라고 보면 됨

* 1번 -> 2번 : 65% 가 넘어갔음. 이 데이터는 g1_type = {2, 3}.
     succeeded 분포는 0 이 92%, 1 이 8% 로,
     이를 통해 g1_type 이 2, 3 인 데이터는 succeeded = 1 이 될 확률이 8% 밖에 안된다고 볼 수 있음

* 1번 -> 3번 : 35% 가 넘어갔음. g1_type = 1
     succeeded 분포는 0 이 45%, 1 이 55% 로,
     g1_grade 기준으로 분기가 또 이루어짐

     * 3번 -> 6번 : 3번 조건인 데이터 중 (전체의 35%),
          전체의 16% 에 해당하는 데이터가 g1_grade < 2.5
          이를 통해 g1_type = 1 && g1_grade < 2.5 인 경우 succeeded = 1 이 될 확률이 11% 

     * 3번 -> 7번 : 3번 조건인 데이터 중 (전체의 35%),
          전체의 19% 에 해당하는 데이터가 g1_grade >= 2.5
          이를 통해 g1_type = 1 && g1_grade >= 2.5 인 경우 succeeded = 1 이 될 확률이 91% 

목적상 succeeded = 1 을 원한다면,
데이터의 g1_type = 1 로 만드는 것이 가장 큰 도움이 되고, 그 다음으로 g1_grade 를 2.5 이상으로 만들어야 함

의사결정 나무에 대해 살짝 알아봤는데,
코드 작성 없이 데이터 만으로 의사결정 나무를 만들어주고 시각화 해주는 R 이 매력적으로 느껴짐!



R 에서 MySQL 에 접속해서 SELECT 한 결과로 보고서를 그려보자

JOIN 이 가능하니, MySQL 에 로그를 넣어놓으면 가공하기 더 좋을 것 같다

참고로 아래 그래프는 우리 데이터에서 grade 별 count 에 대한 graph!



# load libraries for access and draw
library ("DBI")
library ("RMySQL")
library ("ggplot2")


# connect to database server
conn = dbConnect(MySQL(), user=’user', password=‘password', dbname=‘dbname', host=‘host')

# get table list
dbListTables(conn)

# query table content
rs = dbSendQuery(conn, "SELECT * FROM table")

# fetch and make data frame from ResultSet
table = fetch(rs)

# get column names
names(table)

# prepare plot for drawing
d = ggplot(table, aes(factor(column)))

# draw simple bar
d + geom_bar()

# release ResultSet resource
dbClearResult(rs)

# release DB Connection resource
dbDisconnect(conn)



문법 익힐겸, 무의미한 데이터를 만들어서 그림을 그려본다

단순히 랜덤 값으로 data frame 을 구성하고, y축 값을 sort 해서 step 을 그리는 구문이다



# load library
library ("ggplot2")

# set data frame with x and y
# y is random from 10 to 20
d = data.frame(x=1:100, y=runif(100, min=10, max=20))

# sort data of y
d$y = sort(d$y)

# draw step
p = ggplot(d, aes(x, y))
p + geom_step()



- Download & Install R

     R Homepage
          http://www.r-project.org/

     서울대 mirror
          http://healthstat.snu.ac.kr/CRAN/


- R Studio Download
     http://www.rstudio.com/products/rstudio/download/

     R Studio 가 참 훌륭한것 같음!


- 시각화 Plugin (ggplot)
     http://docs.ggplot2.org/current/

- install ggplot
     > install.packages("ggplot2")

     - 처음에 실행해서 ggplot2 를 설치하려고 하면
     > tar: Failed to set default locale

     뭐 이런 메시지 나오는데, Mac Terminal 에
     $ defaults write org.R-project.R force.LANG en_US.UTF-8

     http://davidprakash.blogspot.kr/2011/05/r-error-tar-failed-to-set-default.html
     이렇게 하고 재시작 하면 됨..



- 간단한 그래프 그려보기
     > x = 1:1000
     > y = x^2
     > plot(x, y)

Visual Studio 에서 멀티스레드로 작성된 프로그램을 디버깅 할 때에,

해당 브레이크 포인트 (이하 BP) 에 특정 스레드가 접근할 때에만  활성화 되도록 Filter 를 적용할 수 있습니다.

출처 :  http://msdn.microsoft.com/en-us/library/wyakk529(v=vs.100).aspx


BP를 만든 후 BP 창에서 해당 BP에 마우스 우클릭을 합니다.

우클릭 후 나오는 메뉴에서 Filter 를 선택하면 아래와 같은 창이 나오는데

여기서 Thread ID or Name, Process ID or Name 으로 Filter 를 걸 수 있습니다.

 
이것도 결국엔 bson 이용 방법이 문제인데

정말 bson 이 익숙해지면 mongodb 사용은 거의 끝이 날 것 같네요..

 

void mongo_bson_test_time_t( mongo& m ) 
{
//	> db.foo.find()
//	{ "_id" : ObjectId("4f26266439584a7aa0bfce4e"), 
//	  "time" : ISODate("2012-01-30T05:11:00.594Z") }
//
// time 의 값을 time_t 로 읽어서 출력하기

	mongo_cursor cursor;

	mongo_cursor_init( &cursor, &m, "test.foo" );			// collection 을 test.foo 로 지정

	while( mongo_cursor_next( &cursor ) == MONGO_OK ) 
	{
		bson_iterator iterator;

		// time 필드의 값 읽어오기
		if ( bson_find( &iterator, mongo_cursor_bson( &cursor ), "time" ) )
		{
			time_t t = bson_iterator_time_t( &iterator );

			printf( "time: %s\n", ctime( &t ) );
		}
	}

	mongo_cursor_destroy( &cursor );
}


+ Recent posts