Software Development

Java Pitfalls: Excuting an external program using Runtime.exec() method or ProcessBuilder class

Java 프로그램에서 외부 프로그램을 실행하고 싶을 때, Runtime.exec() 또는 Java 1.5에서 추가된 ProcessBuilder를 사용해 Process 객체를 얻을 수 있다. 처음으로 이런 프로그램을 짤 때, 실수한 것이 없어보이는데도 실행한 프로그램이 종료되지 않는 경우가 있다. 더군다가 어떤 경우엔 정상적으로 종료되고 어떤 경우엔 종료가 되지않는 경우도 보인다.

원인은 바로 Process 클래스의 API Reference에 있는 다음과 같은 설명에서 찾을 수 있다.

The created subprocess does not have its own terminal or console. All its standard io (i.e. stdin, stdout, stderr) operations will be redirected to the parent process through three streams (getOutputStream(), getInputStream(), getErrorStream()). The parent process uses these streams to feed input to and get output from the subprocess. Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, and even deadlock.

즉, Java에서는 Child 프로세스로의 표준 입출력을 Parent 프로세스가 다루어주어야 한다. 당연하게도, Child의 출력을 언제까지고 버퍼링할 수 없기 때문에, 다른 프로그램을 실행할 때는, 특히 표준 입출력이 존재하는 프로그램을 실행할 경우에는, 항상 이 스트림들의 입력 또는 출력을 제대로 다루어주어야 한다.

이러한 동작은 일반적으로 프로그래머들이 익숙한 시스템콜들, 이를테면 UNIX 계열의 fork()/exec()나 Windows의 CreateProcess()처럼, parent 프로세스의 터미널/콘솔을 공유하는 동작과는 다르기 때문에, 프로그래머들이 쉽게 간과하고 실수하기 쉽다. 더군다나 API Reference가 이러한 점을 명확히 설명하고 있지도 않다.

해결책은 당연하게도 표준 입력이 필요할 때는 Process.getOutputStream()을 이용하여 필요한 입력을 해주고 스트림을 닫아주어야하고, 표준 출력이 필요할 때는 Process.getInputStream() 그리고 Process.getErrorStream()을 이용하여 스트림을 비워주어야한다. 드물겠지만 만약 실행할 프로그램이 interactive한 프로그램일 경우에는 입출력에 좀 더 신경을 써야한다.

다음은 표준 출력을 비워주기 위한 코드가 들어간 간단한 예다.

Runtime rt = Runtime.getRuntime();
try {
Process proc = rt.exec("cmd /c dir");
// Process proc = new ProcessBuilder().command("cmd", "/c", "dir").start();
InputStream is = proc.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
int exitVal = proc.waitFor();
System.out.println("Process exited with " + exitVal);
} catch (Exception e) {
System.err.println("Failed to execute: " + e.getMessage());
}

표준 출력 뿐만 아니라 표준 에러까지도 처리를 할 수 있어야 좀 더 일반적인 코드일 것이다. 표준 에러의 버퍼가 차버리면 표준 출력을 비우려고 해도 블럭될 수 있다. 이러한 문제는 Java World의 When Runtime.exec() won’t 라는 글에서 StreamGobbler라는 클래스를 도입해서 약간 더 우아하게 해결하고 있다. 하지만, 간단한 작업을 하기 위해 쓰레드를 사용하고 있기 때문에 좀 오버킬이라는 생각이 든다.

표준 출력과 표준 에러를 간단하게 무시하고 다른 프로그램을 실행할 수 있도록 하는 옵션이 있다면 자주 사용할 수 있으며 편리할 듯 하다. 또는, 디폴트 동작을 일반적인 시스템콜의 동작과 비슷하게 만들어주는 것도 괜찮을 듯하다. 시간이 나면 이런 방식의 Wrapper를 찾아보거나 만들어보아야겠다.

Java Pitfalls: Excuting an external program using Runtime.exec() method or ProcessBuilder class 더 읽기"

Silverlight 1.0: Getting Started

갑자기 자다 깨는 바람에, DDJ의 지난 10월 기사인 Silverlight 1.0: Getting Started을 읽었다.

이 기사는 Silverlight의 소위 Hello, World를 수행하기 위한 과정을 설명하고 있는데, 쓸데없이 장황한 면이 있다. Microsoft가 제공하는 Silverlight 1.0 Quickstarts를 읽는 것이 나아보인다. 요점은 Silverlight 플러그인을 HTML 내에 삽입하기 위해서는 OBJECT나 EMBED/NOEMBED element를 사용하지말고, Silverlight SDK에서 제공되는 javascript(Silverlight.js)를 사용해야한다는 것이다.

한편, 요샌 RIA 기술에 별로 신경을 쓰지못해서 깨닫지 못하고 있었는데, Microsoft의 SilverlightXAML + Javascript(+.NET languages) 기술, Adobe의 FlexMXML + Actionscript 기술이 SDK와 다른 SDK들로부터의 지원, 개발 도구, 미디어 기술 등과 함께 포장되어나온 것이다. 오래 전에 강문식 군이 XAML을 가지고 놀던 기억이 난다.

Silverlight든 Flex든 XAML/MXML+Javascript/Actionscript의 장점을 고스란히 지니게 된다. 별다른 개발 도구 없이도 서버사이드에서 텍스트 파일을 살짝 수정하는 것만으로도 바로 브라우저에 UI 변경사항이 반영된다는 것이다. 기존의 RIA 기술인 ActiveX나 Flash와 비교해보면 RIA 개발 효율이 상당히 높아질 수 있을 것같다. C로 CGI를 만들던 시절과 Perl/PHP로 Web App을 개발하는 현재를 비교해보면 말이다. Web App을 만들 일이 생긴다면 한번 시도해보고 싶다.

섣불리 얘기하기는 꺼려지지만, XAML/MXML+Javascript/Actionscript들을 서버사이드에 두고 HTTP 프로토콜로 접근한다면, 이들의 용도는 한정될 수 밖에 없는 것 같다. 이를테면 중요한 비즈니스 제약들을 여기에(만) 넣을 수는 없다는 것이다. – 예전에 Flex Architecture를 흘끗 본 기억으로는 서버사이드에서 실행되는 컴포넌트 기술도 포함하고 있었던 것 같고, Silverlight를 아우르는 ASP.NET도 당연히 그러한 기술을 포함하고 있겠지만, 여기서는 Silverlight와 Flex의 핵심 기술들만 얘기하자. – 하지만, 궁극적으로는 클라이언트 상에서 동작하는 RIA 기술 자체가 그런 문제를 가지고 있는 것이지 이 기술들이 가지고 있다고 볼 수는 없는 것 같다. 실질적으로도 (적어도 현재는) RIA 기술들은 주로 Presentation 위주로 사용되며, 중요한 트랜잭션 (이를테면 신용카드 결제) 들은 다른 방법으로 (예를 들어, 서버사이드에서 실행되는 로직) 해결하는 것이 일반적인 상황인 것 같다. 다시 말하면, RIA 기술은 Presentation 위주로 사용하고, 중요한 비즈니스 로직은 서버사이드에서 실행하는 식으로 이러한 문제는 해결되고 있는 것 같다. 다른 해결책도 물론 있다. 정말 필요하다면 암호화와 신뢰를 보장하기 위한 보안 모델을 사용할 수도 있다. 이 방법 역시 ActiveX에서 사용하고 있는 것이다 (ActiveX의 인증서!). 하지만, 사람들은(개발자든 사용자든) 보안으로 인한 비용들(귀찮음이든 개발 비용이든)을 별로 좋아하지 않는다는 것이 이 방법의 문제점이다.

잡설이 길었다. RIA 기술들의 실제적인 필요성은 점점 부각되고 있다. Microsoft와 Adobe가 열정적으로 RIA 기술을 지원하고 있으며, 특정 부문에서 이들을 대체할만한 기술 (e.g. HTML 5)이 단기간 내 – 3년 내에 – 나오기는 하더라도 – 성숙하기는 힘들지 않을까 예상된다. 이런 환경에서 RIA와 관련이 없는 개발자라고 하더라도 Silverlight 또는 Flex는 한번쯤 주목해볼만한 기술일 것이다. 과연, 어느 쪽이 주도권을 잡을 것인가? 어쩌면 HTML 4일지도. 후훗.

부록 1: Silverlight는 Microsoft의 구현 만 있는 것이 아니라 Mono 팀의 구현인 Moonlight도 있다.

부록 2: XML의 응용인 XAML보다 IronRuby를 이용한 Silverlight DSL이 훨씬 읽기편하고 예뻐보인다. 인간이 읽고 써야하는 선언적 코드(e.g. Configuration, Markup, Schema, …)에서 XML보다는 Agile 계열의 언어를 활용한 DSL을 사용하는 프랙티스들이 요즘 많이 나오고 있다. 아직은 실험적인 단계라고 해야겠지만, 내겐 긍정적으로 보인다.

Silverlight 1.0: Getting Started 더 읽기"

Database Access Layer

일반적으로 여러 DBMS 벤더 별로 플랫폼 또는 언어에 따라 접근할 수 있는 서로 다른 API들을 제공한다. 이들을 한꺼풀 덮어씌워 공통적인 방법으로 접근하도록 해주는 API를 Database Access Layer 또는 Database Abstraction Layer라고 부른다.

JDBC, ODBC, ADO.NET, Perl의 DBI, PHP의 PDO, Pear::MDB2 (Pear::DB, Pear::MDB) 등은 모두 Database Access Layer에 해당하는 것들이다. 각자 특정 플랫폼 또는 프로그래밍 언어에서 표준적인 위치를 가지고 있다. 물론 이외에도 이들과 경쟁하는 API들이 있으며, 이 외의 플랫폼 또는 언어들도 이러한 API를 가지고 있다.

Jeremy Zawodny는 LtU에서도 이슈가 된 모양Database Abstraction Layers Must Die!라는 글에서,

  • 성능을 떨어뜨리고 복잡도를 증가시킨다.
  • 데이터베이스를 쉽게 바꿀 수 있다는 이점은 별로 중요하지 않으며, 실제로는 쉽게 바꿀 수 없다.
  • 데이터베이스 기능을 완전하게 활용할 수 없고, 튜닝도 제대로 할 수 없다.

는 점에서 Database Abstraction Layer를 사용하기 보다는, 데이터베이스에 접근하는 부분을 ‘라이브러리’를 사용해서 모듈화하고, 만일에 하나라도 데이터베이스를 변경할 일이 생긴다면, 이를 수정하면 된다고 설명하고 있다. 그의 주장은 일견 옳다.

이 외에도

하지만, 여러 사람이 지적한대로, 그가 얘기하는 ‘라이브러리’가 바로 Database Abstraction Layer다. 그리고,

  • 성능은 떨어질 수 있지만, scalability도 떨어지는 것은 아니다.
  • 여러 데이터베이스를 지원해야하는 소프트웨어도 존재한다.
  • 하나의 소프트웨어에서 데이터베이스를 바꾸지 않더라도, 프로그래머는 여러 소프트웨어에서 서로 다른 데이터베이스를 접근해야한다.
  • Database Abstraction Layer 라기보다는 Database Access Layer다. 즉, 데이터베이스의 공통적인 기능에는 공통적인 방법으로 접근할 수 있도록 하지만, 특정한 데이터베이스가 지원하는 기능을 접근하도록 만들 수도 있다.

는 면에서 Database Access Layer는 유용하다.

JDBC를 예로 들어보면,

  • 많은 Java 프로그래머들은 특정 데이터베이스 API를 익힐 필요없이 JDBC만을 알아도 데이터베이스에 접근하는 기본적인 프로그램을 짤 수 있다.
  • 특정한 데이터베이스가 지원하는 대부분의 기능은 SQL이라는 불투명한 데이터를 전달하는 API를 통하거나, 설정을 통해 동작방식을 제어하는 방식으로 접근할 수 있다.

‘왜 PHP 개발자들은 Pear::MDB2 또는 PDO를 아직 사용하지 않는가’라고 물어보면 분명 위와 같은 이유로 반대하는 사람들이 많을 것이라고 생각한다. 이 글이 그에 대한 대답이다. 그리고 한가지 더, JDBC, ODBC, ADO.NET, Perl DBI 등의 성공은 결정적으로 Database Access Layer의 유용함을 반영하고 있다. JDBC를 한번이라도 사용해본 사람들이 PHP의 mysql이나 mysqli 모듈로 돌아갈 것이라고 절대 상상할 수 없다.

Database Access Layer 더 읽기"

Dryad

Microsoft의 MapReduce라고 부를 수 있는 DryadGoogle Tech Talks 비디오를 보았다.

MapReduce와의 가장 큰 차이점은 acyclic graph 모델을 사용한다는 것이다. 강연에서는 계속 optimization에 관해 얘기하는데, 내가 가장 궁금한 점은 이러한 프로그래밍 모델이 사용자에게 어떻게 보일 것인가 하는 것이다. 분명 acyclic graph 모델이 유용하게 쓰일 수 있는 경우는 있을테지만, 너무 복잡한 프로그래밍 모델은 현실적으로 많은 프로그래머가 접근하지 못하게 만든다. 게다가 acyclic graph 모델이라는 아이디어 자체는 그리 새로운 것은 아니다. MapReduce 페이퍼에서도 인용하고 있는 Condor가 훨씬 generic한 분산 환경이다.

요즘 팀에서 주로 하는 일이 대용량 데이터 처리다보니 이런 문제들에 대해 많이 고민하게 되는데, 현재는 단순한 파이프라인 (정확히는 Pipeline & Filter) 모델을 분산된 멀티프로세서 환경에 최적화하면서, 동시에 사용자가 쉽게 프로그래밍할 수 있는가를 고민하고 있다. 물론, 후자가 더욱 어려운 일이다.

Dryad 더 읽기"

Exception Handling

Rob Walling의 Exception Handling에 관한 글에서 Exception Handling의 두가지 기본적인 규칙이 마음에 들어 인용해본다.

  • Rule #1: If you can’t add helpful information to an error message, don’t catch the exception.
  • Rule #2: If you catch an exception, log it.

실제로 사람들은 너무나 바빠서 또는 게을러서 다음과 같은 짓을 한다.

} catch (YourOwnException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

이클립스의 기본 Exception Handling 코드 템플릿이다. 사람들은 그저 이클립스에 생성해주는 코드를 너무 신뢰하고 있든가, TODO 라인도 지워주기 어려울 만큼 바쁘거나 귀찮은거다.

만약 프로젝트를 시작하고 있다면 이클립스의 Exception Handling 코드 템플릿을 다음과 같이 정해주면 좋을 것이다.

} catch (YourOwnException e) {
logger.error("failed to do SOMETHING", e);
throw new RuntimeException(e);
}

Exception Handling 더 읽기"

On Swapping and Swappiness

swapping의 목적은, 자주 사용되는 디스크 블럭의 디스크 캐쉬와 자주 사용되는 애플리케이션 메모리를, 자주 사용되지 않는 애플리케이션 메모리로부터 확보해, 메모리의 사용률을 높히고, 결과적으로 전체적인 시스템의 성능을 높히는데 있다.

디스크 캐쉬가 필요로 하는 메모리의 크기는 사용하는 디스크 블럭의 크기와 같다. 디스크를 전혀 읽지 않는다면, 페이지 캐쉬는 필요없고, 디스크의 대부분을 사용한다면, 최대한 많은 메모리를 디스크 캐쉬로 이용하고 싶을 것이다.

결국, (애플리케이션이 할당한 메모리의 크기) + (사용하는 디스크 블럭의 크기)를 한정된 메모리 크기 내에서 가장 효율적으로 사용하기 위한 방법이 swapping이다. 이를 뒤집어 말하면, 모든 데이터들을 담을만큼 메모리가 크다면, swapping을 할 필요는 없다는 얘기가 된다.

커널은 페이지 별로 사용 빈도를 알고 있기 때문에, 애플리케이션이 할당한 메모리의 페이지들과 디스크 캐쉬의 페이지들 가운데 어느 것이 덜 사용되는 지를 판단할 수 있으며, 기본적으로 애플리케이션이 할당한 메모리 페이지의 빈도가 낮다면 swapping을 할 것이고, 디스크 캐쉬 페이지의 사용 빈도가 낮다면 이를 해제하고 애플리케이션 메모리로 활용할 것이다.

쓸데없이 메모리만 할당해놓고 전체 디스크를 긁어대는 애플리케이션이나, 메모리 상의 대용량 데이터를 이용한 계산을 하면서 장시간동안 로그를 기록하는 애플리케이션을 상상해보면, 그리 어렵지 않게 잘 동작할 것이라고 추측할 수 있다.

하지만, 문제는 실제로 사용하는 메모리도 많으면서 사용하는 디스크 블럭도 많은 경우 – 대용량 데이터 처리를 수행하는 디스크 바운드 애플리케이션이다. 이 때, 애플리케이션이 할당한 메모리를 swap하게 되면, 다시 필요할 때, 디스크에서 읽어오기 위해 애플리케이션이 느려진다. (Thrashing) 대신 디스크 캐쉬를 줄이면, 역시 디스크의 반응속도도 느려진다. 게다가 swapping은 디스크 I/O도 느리게 만든다. 당연하게도 이러한 애플리케이션이 느려지지 않기를 원한다면, 충분한 메모리를 확보하는 수 밖에는 없다.

Swappiness에 관한 글에서 언급했듯이 리눅스 2.6 커널은 80% 이상의 메모리가 사용 중일 때, swapping을 하기 시작한다. 이러한 동작은 (특히 데스크탑에서의) responsiveness를 보장하기 위해서 이루어지는 것으로 보인다. 부수적으로 페이지 캐쉬의 크기를 보장하는 효과도 있으리라고 보인다. 1GB의 메모리를 가진 머신에서, 20% 즉, 200MB의 페이지 캐쉬를 확보하기 위한 노력이라고 보면 당연해 보이지만, 32GB의 메모리를 가진 머신에서, 위와 같은 애플리케이션을 동작시킨다면, 6.4GB의 페이지 캐쉬를 확보하기 위해 swapping을 하는 것은 비정상적일 수도 있다. 6.4GB의 페이지 캐쉬가 적절한 정도인가는 사용되는 디스크 블럭의 크기와 사용 빈도에 따라 달라질 수 있는 판단이고, swapping이 애플리케이션의 성능에 도움이 되리라는 보장은 없는 것이다. 80%가 디폴트 동작인 것은 중요만 의미가 있는 것은 아닐 듯하고, 메모리의 크기가 커질 수록 더욱 그 의미가 이상해진다.

swapping이 애플리케이션에 해를 끼치는 경우는 극단적으로 애플리케이션이 자체적으로 디스크 캐싱을 하는 경우다. MySQL이 대표적인 경우인데, 자체 캐쉬를 줄이고, 리눅스의 디스크 캐쉬를 활용할 수도 있지만, 자체 캐쉬를 사용할 경우 좀 더 효율적인 메모리 관리를 기대할 수 있다. 그렇다면, MySQL이 자체 캐쉬를 사용할 경우에, 페이지 캐쉬의 크기를 20% 정도로 유지하는 것이 의미가 있을까? 32GB 머신에서 6.4GB를 페이지 캐쉬로 확보해둔 상태에서 swap을 해야할까? (물론 MySQL이 페이지 캐쉬를 사용하지 않도록 설정해도 마찬가지다.)

이러한 경우에 Swappiness를 튜닝할 필요성이 발생하는 것 같다. 실제로 많은 MySQL 튜닝 가이드라인은 Swappiness를 0으로 만들 것을 권장하고 있다. 개인적인 생각으로는 Swappiness를 30-50 정도만 하더라도 애플리케이션이 메모리를 모두 사용하더라도 swap_tendency가 100을 넘지 않아 swap을 시작하지 않으므로 괜찮으리라 생각되지만, distress 등의 변수를 생각하면 실제 상황에서의 실험이 필요할 듯 하다. VM에 관련된 커널 동작은 커널 버전, 심지어는 2.6 내에서도 크게 달라지는 것으로 보인다. /proc/sys/vm/*의 파라미터들도 2.6 내에서 생겼다가 사라지기도 하고, 각각의 동작을 정확하게 정의하기도 어려우며, 그 파라미터들이 어울려서 어떤 동작이 나올 것인가를 예측하기란 더욱 어렵다.

 

결론: 대용량 데이터 처리를 수행하는 디스크 바운드 애플리케이션을 대용량 메모리 상에서 수행할 경우 Swappiness 튜닝에 신경 쓰도록 하자.

On Swapping and Swappiness 더 읽기"

Swappiness in Linux 2.6

Linux 2.6의 swap 경향은 mm/vmscan.c에 의하면 다음과 같이 정의된다.

swap_tendency = mapped_ratio / 2 + distress + sc->swappiness;

이 때, mapped_ratio는 다음과 정의된다.

/*
* The point of this algorithm is to decide when to start
* reclaiming mapped memory instead of just pagecache.  Work out
* how much memory
* is mapped.
*/
mapped_ratio = ((global_page_state(NR_FILE_MAPPED) +
global_page_state(NR_ANON_PAGES)) * 100) /
vm_total_pages;

즉, mapped_ratio는 전체 메모리 크기에 대한 애플리케이션이 사용하고 있는 메모리 크기의 비율이다.

distress는 다음과 같이 정의된다.

/*
* `distress' is a measure of how much trouble we're having
* reclaiming pages.  0 -> no problems.  100 -> great trouble.
*/
distress = 100 >> min(zone->prev_priority, priority);

include/linux/mmzone.h에 따르면, priority는 priority of VM scanning이며, (아마도 free page를 확보하기 위해) 한번에 zone별 page 리스트를 스캐닝 하는 페이지의 수를 얻는데 (queue_length >> priority) 사용되며, 디폴트 값은 12로 한번에 2^12 = 4096개의 페이지를 스캔하는 것을 의미한다. (아마 free page를 확보하지 못해,) priority가 올라갈 수록 (값이 낮아질수록) 많은 페이지를 스캔하게 된다. 2.6 VM을 제대로 공부하지 않아서 이 부분은 확신할 수는 없다.

어쨌든, priority가 디폴트 값인 12일 때, distress값은 100/2^12 = 0이다.

swappiness는 sysctl이나 /proc/sys/vm/swappiness를 통해 사용자가 설정할 수 있는 파라미터이며, 0-100 사이의 값을 갖는다. swappiness의 디폴트 값은 60이다.

/*
* From 0 .. 100.  Higher means more swappy.
*/
int vm_swappiness = 60;

이런 값들로부터 계산된 swap_tendency가 100이 넘으면, mapped memory를 inactive list로 옮기기 시작한다. 다시 말하면, swap을 시작하게 된다.

예를 들어, 2GB의 메모리를 가지고 있는데, 1.8GB의 메모리를 애플리케이션이 사용하고 있고, distress가 0인 상태라면, swap_tendency는 1.8GB * 100 / 2GB / 2 + 0 + 60 = 105가 되어, swap 되기 시작한다.

결국, swap_tendency를 산출하는 식에 따르면, swappiness가 디폴트 값 60인 상태에서는 80% 이상의 메모리를 사용하기 시작하면, swap하기 시작한다. 다음과 같은 방법으로 swappiness 값을 조정함으로써, 이러한 swap behavior를 조정할 수 있다.

sysctl -w vm.swappiness=30

또는,

echo 30 >/proc/sys/vm/swappiness

References

Swappiness in Linux 2.6 더 읽기"

WebCrawler

Pinkerton, B. 1994. Finding What People Want: Experiences with the WebCrawler

이 논문은 1994년 10월, Chicago에서 열린 두번째 WWW 컨퍼런스에서 발표된 논문이다. WebCrawler 역시 웹의 resource discovery 문제 해결을 위해 웹 크롤러를 사용하는 검색엔진에 해당한다. 불과 5개월 간격이지만 첫번째 WWW 컨퍼런스에 등장한 검색엔진에 비해서 꽤 완성도 면에 있어서 나아진 면을 보인다.

WebCrawler의 디자인에 대한 설명은 웹에 대해서 잘 모르는 사람들도 이해할 수 있을 정도로 자세하고 쉽게 되어있어서, 웹 크롤러에 대한 설명이 필요하다면 참고할 만하다. WebCrawler의 architecture는 크게 Search Engine, Agents, Database, Query Server로 이루어진다. Search Engine은 크롤과 인덱싱 작업을 담당하며, 여러 프로세스 (15개)로 동시에 동작하는 Agents는 실제로 웹 페이지를 다운로드 받는 역할을 한다. Database는 문서의 메타데이터나 링크, full-text 인덱스를 저장한다. 특이한 점은 방문한 server들에 대한 정보를 가지고 있어서, 방문하지 않은 서버나 least recently visited 서버를 먼저 방문함으로써, 가능한 한 여러 서버들을 공평하게 방문하려고 시도한다는 것이다. Query Server는 사용자의 검색엔진 질의를 담당한다.

WebCrawler의 성능은 486 PC에서 1시간에 1000개의 문서를 인덱싱하는 정도라고 한다. 현재의 기계로는 1초에 그 정도 이상의 문서를 처리할 수 있음을 생각하면, 1000~10000배 이상의 성능 차이가 난다는 것을 알 수 있다. 게다가, 이러한 성능 차이는 웹 크롤러 – 검색엔진의 디자인에 많은 영향을 줄 것임에 틀림없다.

인덱싱은 IndexingKit이라는 라이브러리를 사용하는 것으로 보이고, lexical anlyzer, stop list, inverted index, vector-space model에 관한 언급들이 보인다.

이 논문에서 저자는 6개월 간의 WebCrawler 운영을 통한 교훈을 정리해놓았는데, 다음과 같다.

  • Full-text indexing is necessary
    • 당시에도 20% 가량의 웹 페이지들은 title을 가지고 있지 않았으며, title의 유효성을 인정하기 어렵기 때문에, full-text 인덱싱이 필수적이라는 견해를 보인다. 그리고 실제로 사람들이 원하는 것은 title이 아닌 내용에 대한 좀 더 나은 쿼리 기능을 원하고 있다는 면에서 full-text 인덱싱의 중요성을 설명하고 있다.
  • Precison is the limiting factor in finding documents
    • WebCrawler는 Recall을 적절히 만족시키고 있지만(adequate), Precison의 면에서 만족스럽지 못하다고 얘기하고 있다. 문서의 weighting이 도움이 되지만 false positive (irrelevant documents)를 제거하고 있지는 못했다고 한다. (이 문제는 랭킹의 문제로 보인다.) 또다른 문제는 사용자가 well-focused query를 던지지 못하는 문제를 언급하고 있으며, 사용자 쿼리의 평균 단어 수가 1.5이었다고 한다. 검색엔진이 보편화된 현재도 사용자들은 여전히 단순한 쿼리를 던지고 있다.
  • Breadth-first searching at the server level builds a useful index
    • 어떤 정보를 가지고 있는 서버만 발견할 수 있다면, 사용자들은 그 서버 내에서 좀 더 쉽게 navigate할 수 있기 때문에, Breadth-first searching이 사용자들을 만족시켜준다고 한다. navigational search라는 검색엔진의 용도라고 볼 수 있을 듯 하다. 또한, 크롤 전략 – 순서가 검색 결과에 영향을 미칠 수 있다는 인식이 보인다. 한편, BFS 크롤 전략의 다른 이익으로 politeness를 언급하고 있기도 하다.
  • Robot-like behavior is necessary now
    • 크롤러가 웹 트래픽을 낭비한다는 비판에 대해서 크롤러를 통해 오히려 웹 트래픽을 절감할 수 있다고 주장하고 있다. ALIWEB과 같이 사이트 별 인덱스를 사용하는 것이 아니라, 실제 문서를 인덱싱에 사용함으로써, 좀 더 나은 인덱싱 방법을 사용할 수 있음을 지적하고 있다.
  • Widespread client-based searching is not yet practical
    • 클라이언트에서 크롤러를 동작시켜 문서를 찾아내는 방법은 하나의 서버에서 인덱스를 만드는 방법에 비해 효율적이지 않음을 지적하고 있다. Future Work에서 클라이언트 기반 검색을 하더라도 이 결과를 사용자들이 공유하는 인덱스에 추가하는 방법을 제안하고 있다.

Future Work에서 좀 더 강력한 full-text 엔진의 기능으로, proximity searching과 query expansion & refinement를 제안하고 있다.

결론에서 크롤러에 기반한 검색엔진을 분산된 웹의 특성에 비유하고 있다. 크롤러에 기반한 검색엔진은, 웹 사이트들로부터 (아마도 인덱스 생성과 같은) 특별한 참여를 필요로 하지도 않고, 웹 프로토콜을 그대로 사용함으로써, 동작 방식과 정책의 분리가 가능해졌고, 여러 검색엔진이 여러가지 정책을 채용함으로써, 사용자는 자신의 필요에 적합한 검색엔진을 선택할 것이며, 기술은 좀 더 빠르게 성숙될 것이라고 저자는 예견하고 있다.

WebCrawler 더 읽기"

RBSE Spider

Eichmann D. 1994. The RBSE Spider – Balancing Effective Search Against Web Load

1993년은 여러가지 목적을 가진 웹 크롤러들이 출현하기 시작한 해였다. 예를 들어, 최초의 크롤러로 기록되고 있는 World Wide Web Wanderer는 웹의 크기와 성장를 측정하기 위한 용도로 만들어졌다. 한편, RBSE Spider는 웹 검색엔진을 위한 크롤러로 만들어졌으며, 제목과 헤더들만 인덱싱하던 JumpStation과 달리 Full-text 인덱싱하는 검색 엔진의 일부분이었다. (이 때는 아직 검색 엔진과 크롤러의 구분이 모호했다.) 따라서, RBSE Spider는 웹 크롤러를 사용하는 최초의 Full-Text 검색 엔진이라고 볼 수 있다.

크롤러와 인덱서의 분리

크롤러의 architecture면에서 RBSE Spider는 크롤러를 인덱서로부터 분리했다. 크롤러와 인덱서를 분리함으로써 separation of concern의 효과를 얻을 뿐만 아니라, 각각의 컴포넌트를 다른 용도로도 사용할 수 있게 되었다. 하지만, RBSE Spider는 크롤러가 웹 페이지의 발견(discovery)만을 수행할 뿐, 저장하지 않았기 때문에, 크롤러가 발견한 웹 페이지를 받아오기 위해서 인덱서가 다시 방문해야 했다.

Incremental Crawler

크롤러와 인덱서를 분리하지 않을 수 없었던 또다른 이유는 incremental한 크롤러를 만들기 위한 것이었고, 크롤러가 incremental해야만 했던 이유는 robust한 (그러니까 잘 죽지 않는) 크롤러를 만들기가 어려웠기 때문이라고 한다. 그들의 이유는 조금 우습지만, 현재는 웹의 크기가 커지면서 incremental한 크롤러는 필수적인 것이 되었다.

Politeness

당시에 여러 목적의 크롤러들이 늘어나면서 크롤러가 유발하는 웹서버의 로드에 대한 우려가 생기기 시작했고, 이는 Koster의 Guidelines for Robot Writers라는 문서의 제안으로 이어졌다. RBSE Spider는 User-Agent 필드를 이용한 identification 등 Robot exclusion protocol을 따르며, 웹 서버에 로드를 주지 않기 위해 주의하고 있다.

상세한 구현

RBSE Spider의 크롤러 부분은 두개의 프로그램으로 이루어져 있다. 그 하나는 mite라는 프로그램인데, 주어진 URL의 문서를 다운로드한 후, 문서 내에 포함된 URL들을 출력해주는 것이다. 다른 프로그램은 특정 패턴을 가진 URL들에 대한 링크를 mite를 이용해서 추출한 다음 source-target pair들을 Oracle 데이터베이스에 저장하는 역할을 한다. 데이터베이스에는 다시 방문하는 것을 방지하기 위해, 다운로드에 실패하거나 링크가 없는 경우의 로그가 있다. Robot exclusion protocol에 따라 받아서는 안되는 URL들의 패턴들의 리스트도 있다.

페이지의 텍스트를 저장하지 않는 것을 제외하고는 기초적인 크롤러의 기능들을 갖추고 있다고 볼 수 있다.

인덱서 부분은 좀 더 단순하다. 데이터베이스로부터 URL들을 가져온다음 이들을 waisindexer라는 프로그램에 집어넣으면, waisindexer는 다시 mite를 사용해서 URL에 해당하는 페이지를 다운로드한 다음 이들을 인덱싱한다. 인덱싱 부분은 거의 WAIS의 인프라를 활용함으로써 relevance feedback도 활용할 수 있다고 한다.

검색 엔진의 역할

이 paper는 크롤러에 대한 Rationale로서 크롤러가 마치 지도와 같은 역할로, 사용자가 원하는 페이지에 바로 찾아갈 수 있도록 함으로써, 네트워크 트래픽을 감소시킬 수 있다고 주장하고 있다. 이는 당시에 클라이언트 기반의 검색엔진 등이 있었다는 것을 감안하면 옳은 주장이라고 생각되나 사실 현재에는 별로 의미가 없다.

당시에 증가하고 있던, 데이터베이스로부터  동적으로 생성된 페이지들을 인덱싱하지 말아야 한다는 주장은 지금 생각하면 완전히 잘못된 것이다. 당시엔 아마도 각 사이트에서 제대로 인덱싱 되고 있는 페이지들을 따로 인덱싱하는 것은 낭비라고 생각했을 것이다.

한편, 크롤러의 역할로서 내용 유사도(semantic similarity)에 따른 가상적인 이웃 (virtual neighborhoods) 정보를 생성할 수 있다고 얘기하고 있다. 당시에는 아마도 하나의 사이트는 특정한 주제를 취급하는 사이트였을테고 그런 사이트 몇개만 알면 특정 주제에 대한 검색이 가능했겠지만, 그 때까지 그 사이트들 사이의 내용 유사성에 기초한 어떤 서비스는 없었을 것이다. 현재의 일반적인 검색 엔진에서 제공하는 유사 페이지 검색이라든가 버티컬 검색 엔진 등을 생각하면 일리가 있다. 이제는 특정 사이트 몇몇이 특정 주제에 대한 포탈이 되기 힘들고, 검색 엔진의 쿼리를 통해서 또는 버티컬 검색 엔진을 통해서 그러한 서비스를 하고 있다.

RBSE Spider 더 읽기"