Monthly Archives: December 2008

검색엔진 Solr로 교체

여태까지 회사에서 운영중인 서비스에서 CLucene 기반으로 제작한 자체 검색서버를 사용했었는데, 이번에 Solr로 교체합니다. 교체 이유는 가끔씩 서버가 죽는 경우가 생기는데 원인파악이 안되더군요. 주로 인덱싱을 열심히 시키면 죽고, 인덱싱하는 동안 검색서비스가 많이 느려지더군요. 그래서 실시간 인덱싱이 필요한 경우를 제외하고는 하루에 한번씩 한가한 시간에 전체 인덱싱을 오프라인으로 해서 인덱스 디렉토리를 교체하는 형태로 서비스를 하고 있었습니다. 이렇게 운영하면서 서비스에 크게 문제가 있지는 않았지만, C++로 작성하다보니 새로운 요구사항이 들어올때 제가 직접 일을 처리하게 되더군요.

예전엔 Solr라는 거 자체가 있었는지 몰랐었고, 자바로 되어 있는 루씬은 좀 거부감이 있었는데.. Solr의 기능들을 보니 자체구현한 검색엔진을 유지보수/확장하는 거보다 여러가지 장점들이 눈에 보이더군요. 먼저… 유지보수 측면에서 XML만 편집하면, 스키마를 업데이트할수 있는 장점이 있고, 리플리케이션 또한 꼭 필요한 기능이라는 생각이 들더군요. 그리고 HTTP 기반으로 돌기 때문에 여러 검색서버를 띄우고 로드밸랜싱도 가능하겠더군요… 그리고 성능최적화를 위한 기능들과 문서들… 아직 제대로 보지는 못했지만 다른 이들의 경험에서 배울수 있는것이 많을거라 생각했습니다.

디비와 Solr의 동기화는 Python을 이용해서 직접 작성했습니다. Python으로 디비에서 필드들을 읽어들여 XML화하여 Solr에 저장했습니다. 예전엔 디비의 업데이트가 일어나는 부분에서 인덱싱을 다시 하도록 로직을 짰었는데… 이게 관리가 잘 안되더군요. phpmyadmin에서 디비 편집하는 경우도 있고… 예전엔 하루에 한번씩 전체 인덱싱을 다시하여 이런 문제를 해결했었는데, 좀더 근본적인 대책을 찾아야겠더군요. MySQL 5.0의 트리거를 사용하여서 변화가 있는 데이타를 추적하여 자동으로 디비와 Solr와 동기화 되게 했습니다. 현재 사용중인 MySQL의 버전이 대부분 4.X라 검색서버에 MySQL 5를 설치하고, 필요한 테이블만 리플리케이션 되게 세팅했습니다. (참조하는 마스터 디비가 두개라 하나의 머신에 여러개의 MySQL 인스턴스를 돌리는 방법을 이용했습니다.) 그리고 트리거를 설치하여 변화가 있는 부분을 _changed_에 쓰도록 했습니다.

create table _changed_ (no int NOT NULL auto_increment,
id varchar(100) character set utf8,
type varchar(100) character set utf8,
action varchar(100) character set utf8,
date datetime NOT NULL,
KEY no (no));

create trigger tr_news_insert after insert on news
for each row INSERT INTO _changed_ (id, type, action, date) values (NEW.no, “news”, “index”, NOW());

create trigger tr_news_update before UPDATE on news
for each row INSERT INTO _changed_ (id, type, action, date) values (NEW.no, “news”, “index”, NOW());

create trigger tr_news_delete before DELETE on news
for each row INSERT INTO _changed_ (id, type, action, date) values (OLD.no, “news”, “delete”, NOW());

crontab에서 1분에 한번씩 _changed_ 테이블을 보고 Solr에 변경된 사항을 적용하도록 했습니다.

리플리케이션은 rsyncd를 이용하도록 되어있어서 루씬처럼 업데이트 항목만 다른 파일로 관리되는 경우 효율적으로 동기화가 가능합니다. 다만 optimize할때는 파일들 모아서 전체를 다시 쓰기 때문에 마스터에서 자주 optimize하는거는 추천하지 않습니다.

마스터에서 commit할때 snapshooter를 호출하도록 하고, slave에서는 5분에 한번씩 snappuller와 snapinstaller를 호출하도록 했습니다. slave는 따로 데이타를 복사할 필요가 없어서 slave 폴더만 압축해두면 아주 빠르고 쉽게, 동기화와 서비스를 할수 있더군요. 디비 리플리케이션이 이렇게 될수 있다면.. 이라는 생각이 드네요.

php에서는 phps 포맷으로 검색결과를 받아가도록 했습니다.

[CODE type=php]
<?php
   function solr_search_get($args) {
     $sr = file_get_contents(“http://solrserver:solrport/solr/select?”.$args.”&wt=phps”);
     return unserialize($sr);
   }
   print_r(solr_search_get(“q=hello”));
?>
[/HTML][/CODE]

자체 제작 CLucene 검색서버와 Solr의 퍼포먼스는 … 차이가 좀 납니다. 다른 서버에서 돌려서 정확한 비교는 어렵지만, 검색속도의 차이는 크지 않습니다. Solr 서버의 경우 최근에 들어온 서버인데 성능은 비슷합니다. 인덱싱은 큰 차이가 납니다. 자체 검색 서버의 경우 20분이면 전체 인덱싱을 하는데, Solr에서 50분 정도 걸립니다. 아마도 XML로 변환되고, HTTP로 데이타가 들어가야해서 속도차이가 많이 나는듯합니다. XML 변환을 python으로 하는 것도 성능에 영향을 주었을듯 하네요.

2008년 회사 SVN 로그 분석

작년에 TortoiseSVN의 통계 그래프 기능을 이용해서 분석을 했었는데, 올해는 약간의 프로그래밍으로 좀더 자세히 분석을 해보았습니다. 작년에는 단순히 commit 회수만 통계에 반영이 되었는데, 이번에는 commit 회수와 commit할때 라인수 (정확히는 svn diff 의 라인수)로 분석을 했습니다. 분석을 위해서 svn log, svn diff의 결과값을 python으로 취합하여 분석했습니다. svn log, svn diff에서 결과를 가져오는게 생각보다 시간이 오래걸려서 commit별로 결과를 파일에 저장하는 script와 분석하는 script를 따로 작성했습니다.

먼저 svn에 로그인할때 암호를 물어보지 않도록 .ssh/id_rsa 키를 생성, 패스워드를 제거하고, svn의 상위 디렉토리만 checkout 했습니다.

$ svn checkout -N svn+ssh://mix1009@www.company.com/SVNROOT/Develop

여기서 아래 python 스크립트(svnlog_dump.py)를 이용해서 svn_stat.txt 파일을 생생했습니다.

[CODE type=”python”]
import os

def main():
   log_s = os.popen(“svn log”)
   while 1:
       line = log_s.readline()
       if not line: break
       line = line.strip(“\n”)
       x = line.split(“|”)

       if len(x) != 4: continue

       rev = x[0].strip()[1:]
       user = x[1].strip()
       date = x[2].strip().split(” “)[0]
       comment = x[3].strip().split(” “)[0]

       #print(line)
       wc = os.popen(“svn diff -c %s | wc” % rev).read()
       wc = wc.strip(“\n”)
       wc = “,”.join(wc.split())
       wc = wc.strip(” “)
       open(“svn_stat.txt”, “a”).write(“%s, %s, %s, %s, %s\n” % (rev, user, date, comment, wc))
       print(“%s, %s, %s, %s, %s” % (rev, user, date, comment, wc))

if __name__ == ‘__main__':
   main()
[/HTML][/CODE]

각 라인별로 하나의 commit의 내용을 저장합니다. revision, 커밋한사용자, 날짜, 커밋설명라인수, diff라인수 등이 저장됩니다.

아래 python 스크립트(svnlog_analyze.py)를  이용하여 svn_stat.txt 내용을 분석합니다.

[CODE type=”python”]
# diff length > 5000 -> source import or copy
validcommitthreshold = 5000

m = {} # year -> {user -> [comment, commitcount, linecount, validlinecount]}
f = open(“svn_stat.txt”)
while 1:
   line = f.readline()
   if not line: break
   line = line.strip(“\n”)
   x = line.split(“,”)
   #[‘9674′, ‘ mix1009′, ‘ 2007-03-30′, ‘ 1′, ‘ 15′, ’36’, ‘522’]

   if len(x) != 7: continue

   #print line
   rev = x[0].strip()
   user = x[1].strip()
   date = x[2].strip()
   comment = int(x[3].strip())
   linecount = int(x[4].strip())

   year = date[:4]
   #print year, user, comment, linecount
   if not m.has_key(year):
       m[year] = {}

   ym = m[year]
   if not ym.has_key(user):
       # [comment, commitcount, linecount, validlinecount]
       ym[user] = [0, 0, 0, 0]

   validlinecount = 0
   if linecount <= validcommitthreshold:
       validlinecount = linecount

   ym[user] = [ym[user][0]+comment, ym[user][1]+1, ym[user][2]+linecount, ym[user][3]+validlinecount]

k = m.keys()
k.sort()
for year in k:
  print(“[%s]”%year)
  for user in m[year].keys():
   print(”    %s : %s”%(user, m[year][user]))
[/HTML][/CODE]

연도별로 나눠서 각 사용자의 commit설명라인수, commit회수, commit diff 라인수, 유효 commit diff 라인수를 출력합니다. 유효 commit이라는걸 만든 이유는… 소스 import나 copy 같은걸 할 경우 상당히 많은 양의 commit 라인수가 발생해서 정확한 분석이 안되서.. commit 라인수의 분포를 액셀에서 그려보고, 어느 정도의 라인수까지가 소스를 손으로 작성했는지 대충 분석해서 diff가 5000라인 정도 이상이면 소스 import나 copy로 취급했습니다.

2007년과 2008년 결과값입니다.

[2007]
   mix1009 : [929, 687, 1183649, 117023]
   user1 : [152, 142, 432097, 46889]
   user2 : [199, 197, 475667, 70801]
   user3 : [197, 187, 24676, 24676]
[2008]
   mix1009 : [776, 574, 1135963, 119820]
   user1 : [316, 281, 1634243, 101233]
   user2 : [203, 163, 120608, 46729]
   user3 : [428, 292, 44474, 44474]
   user4 : [141, 132, 46616, 30751]

올해는 제가 회의도 많고 해서 개발에서 좀 빠져있을려고 했는데… 아직은 개발에서 손뗄수 있는 시기는 아닌거 같네요. 그리고 모든 팀원들의 퍼포먼스를 끌어올리기 위해서 좀더 노력해야겠다는 생각이 드네요.