RubyOnRails로 30분만에 정책 관리자 도구 만들기

회사에서 하고 있는 프로젝트에서 정책 데이터들을 MySQL 데이터베이스로 관리하고 있는데요. 보편적인 MySQL 관리툴인 phpMyAdmin 조차 사용하지 않고 그냥 손으로 관리하고 있길래 RubyOnRails로 간단한 관리 도구를 만들어보았습니다. 정확히 30분 걸렸습니다. 많은 관리 도구들이 CRUD 정도만을 필요로 하다는 것을 고려하면 RubyOnRails의 scaffolding만으로도 거의 충분하다고 볼 수 있죠. 다음에 시간이 나면 authentication/authorization 정도를 붙여볼 생각입니다.

Setting up database configuration

$ vi config/database.yml

development:
  adapter: mysql
  database: scheduler_policy_manager_development
  username: ***
  password: ***
  host: ***

Creating a model

$ ruby script/generate model address_schedule_policy

Creating a table for the model

$ vi db/migrate/001_create_address_schedule_policies.rb

class CreateAddressSchedulePolicies < ActiveRecord::Migration
  def self.up
    create_table :address_schedule_policies do |t|
        t.column
:name,     :string
        t.column
:ip,       :int
        t.column :period,   :int
    end
  end

  def self.down
    drop_table :address_schedule_policies
  end
end

$ rake db:migrate

Creating a scaffolding controller

$ ruby script/generate controller admin
$ vi app/controllers/admin_controller.rb

class AdminController < ApplicationController
    scaffold :address_schedule_policy
end

Running the web server

$ ruby script/server

=> Booting WEBrick…
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with –help for options
[2006-10-09 20:58:38] INFO  WEBrick 1.3.1
[2006-10-09 20:58:38] INFO  ruby 1.8.4 (2005-12-24) [i486-linux]
[2006-10-09 20:58:38] INFO  WEBrick::HTTPServer#start: pid=26079 port=3000

Adding some validation

$ vi app/models/address_schedule_policy.rb

class AddressSchedulePolicy < ActiveRecord::Base
    validates_presence_of :name, :ip, :period

    protected
    def validate
        errors.add(:period, “should be at
least 5000 (ms)”) if period.nil? || period < 5000    end
end

Generating scaffold

$ ruby script/generate scaffold address_schedule_policy admin

Making a small change to the table

$ script/generate migration ChangeTypeOfIpField
$ vi db/migrate/002_change_type_of_ip_field.rb

class ChangeTypeOfIpField < ActiveRecord::Migration
    def self.up
        change_column
:address_schedule_policies, :ip, :string
    end

    def self.down
        change_column
:address_schedule_policies, :ip, :int
    end
end

$ rake db:migrate

Installing streamlined generator

$ cd ~/tmp
$ wget http://streamlined.relevancellc.com/streamlined_generator-0.0.5.gem
$ sudo gem install streamlined_generator-0.0.5.gem

Generating streamlined scaffold for the model

$ ruby script/generate streamlined address_schedule_policy
# should restart WebRick to show streamlined correctly
$ script/server

Installing mongrel (the alternate web server)

$ sudo gem install mongrel

Running the new web server

$ mongrel_rails start

RubyOnRails로 30분만에 정책 관리자 도구 만들기 더 읽기"

Java Identifiers in Unicode

Java Identifiers in Unicode

요즘 The Java Programming Language를 읽고 있는데, Java는 identifier들을 Unicode로 쓸 수 있다는 점이 신기해서 만들어본 코드.

public class 메롱이 {

public void 말한다() {
System.out.println("메롱~");
}

public static void main(String [] 인자들) {
메롱이 메롱메롱이 = new 메롱이();
메롱메롱이.말한다();
}
}

Java Identifiers in Unicode 더 읽기"

Prefer Multithreading to Event-driven in Highly-Concurrent Servers

Introduction

‘서버를 만들 때 Multithreading을 사용해야하는가 아니면 Event-driven (흔히, select/poll/epoll/kqueue 사용한) 방식을 사용해야하는가’라는 질문을 자주 받는다. 대략 내가 처음 job으로서의 프로그래밍을 시작하던 2001년에만 하더라도 리눅스를 포함한 POSIX platform에서의 쓰레드 구현은 형편없었고, select나 kqueue 등을 사용하는 것이 High Performance, High Concurrency를 위한 서버를 만들기 위한, 다시 말해 C10K 문제를 해결하기 위한 거의 유일한 방법이었다. 하지만, 컴퓨팅 환경은 (언제나처럼) 급속도로 발전했고, Multithreading을 사용해 몇몇 프로덕트를 만드는데에 성공하면서, 2004년 말 정도에는 Event-driven보다 Multithreading이 더 나은 해법이 아닌가하는 의심이 들 정도였다. 이러한 의심으로부터 Multithreading이 우월한 해법이라는 것을 확신시켜준 것은 태준옹 와의 대화였고, 이후로는 위와 같은 질문에 대해서는 항상 ‘굉장히 performance-critical한 서버가 아니라면 multithreading을 사용하는 것이 좋다‘라고 답변하고 있다. 그리고, ‘굉장히 performance-critical한 서버가 아니라면’이라는 가정조차도 점점 힘을 잃어가고 있는 추세이다.

그러던 와중에, CN님이 2005년 4월 30일에 쓰신 ‘프로그래머에게 해로운 두가지 1: 쓰래드’라는 글로 Multithreading에 비해 Event-driven을 선호해야한다는 글을 쓰셨는데, 해당 글의 코멘트에서 논쟁을 벌이다가 Multithreading을 방어하기 위한 글을 따로 써야겠다는 마음을 먹었으나, 이를 1년 6개월 만에 실행에 옮기게 된 것이 바로 이 글이다.

Performance

경험 있는 서버 개발자들이 주로 Multithreading에 비해 Event-driven을 선호하는 가장 큰 이유는 performance일 것이다. 실제로 옛날에는 Event-driven 방식을 사용하는 것이 상대적으로 엄청나게 뛰어난 performance를 보여주었다. 하지만, 어디까지나 그건 ‘옛날 이야기’다. 이렇게 된 이유는,

  • OS의 Multithreading 지원과 이를 통한 더 나은 쓰레드 구현
  • CPU 속도의 증가 경향
  • Multiprocessor의 보편화 경향(CPU 수의 증가 경향)과 OS의 지원

정도로 들 수 있을 것 같다.

Multithreading을 사용할 때 performance에 대해 걱정하는 점들은 더이상 문제가 되지 않는다. 하나씩 짚어보면,

  1. 쓰레드가 많아지면 schedule하는데 부담이 크지 않을까?
    • Linux 2.6 커널은 O(1) 알고리즘의 scheduler를 가지고 있다. (Solaris나 Windows에서도 반영된다고 했으나, 현재 상태는 알 수 없다.) 쓰레드 수가 많다고 해서 scheduling overhead가 증가하는 일은 없는 것이다. 또한, 대화하다보면 sleeping 상태의 쓰레드도 scheduling 오버헤드에 포함될 것이라고 생각하는 사람도 많이 있는 것 같은데, 적어도 Linux에서는 그렇지 않다.
  2. 쓰레드가 많아지면 context switching cost가 많이 들지 않을까?
    • 현대의 architecture, 적어도 x86은 cpu context가 그다지 크지 않게(몇몇 register를 메모리에 save/restore하는 정도로) 설계되어있다. 실용성은 떨어지지만 32bit x86은 thread에 대한 CPU 레벨의 지원도 포함하고 있을 정도다. (물론, Linux에서는 사용하지 않는다.) 더군다나 CPU 속도가 빨라지면서 context switching의 비용은 점점 중요하지 않게될 것이다. P4 3GHz 머신에서 lmbench의 결과로는 2-3 microseconds 정도의 context switch latency가 있을 뿐이다.

여기에 Multiprocessor 문제가 추가되면 더더욱 Multithreading의 손을 들어줄 수 밖에 없다. Consumer PC에서도 Multiprocessor가 일반화되어가는 추세라는 것은 대부분의 사람들이 이제는 동의할 것이다. 프로그래머가 user-level에서 어떤 Multiprocessor를 사용할 것인가, 또는 각 processor에 연결된 메모리를 어떻게 효율적으로 사용할 수 있는가(NUMA )와 같은 문제까지 신경쓸 수는 없다. 결국, Multiprocessor를 효율적으로 사용하기 위해서는 processor 자원의 분배를 OS에 의존하는 Multithreading이 가장 좋은 방법이 되었다. 최근 수년간 (특히, Linux/FreeBSD에서는) Multiprocessor를 지원하기 위해 OS 디자인에서도 변화가 생기는 등 여러가지 노력들이 보이고 있다.

Event-driven 방식은 원래 하나의 쓰레드에서 event dispatcher와 handler가 함께 동작하는 방식이었으나, Multiprocessor를 활용하기 위해서는 event dispatcher를 processor 수에 따라 나누던가, handler가 쓰레드 풀 상에서 동작하도록 하는 방식을 채용하는 수 밖에 없다. 이렇게 되면, Event-driven 방식을 주장하는 사람들이 좋아하지 않는 쓰레드의 온갖 단점들이 Event-driven 방식에도 도입되게 된다. 그럼에도 불구하고 Multithreading이 Event-driven에 비해 state의 효율적인 관리가 불가능(이를테면, idle connection을 handle하기 위한 thread의 불필요한 stack 차지)하다는 주장을 할 수 있으나, 이 문제는 다음 섹션을 보도록 하자.

State Management

최근에 이루어진 컴퓨팅 환경의 변화 중 또다른 하나의 커다란 축은 바로 64bit 주소 공간의 도입일 것이다. 일단 주소 공간이 충분히 넓어졌기 때문에 쓰레드 수에 따른 주소 공간의 낭비는 크게 의미가 없게 되었다. Linux와 같은 현대의 OS는 스택에서도 페이지 폴트가 발생할 때 물리 메모리를 할당하는 방식을 취하므로 물리 메모리의 낭비 또한 거의 없다. 설령 32bit 머신을 사용하고 있어서 주소 공간이 부족하다고 하더라도, Linux의 쓰레드 구현인 NPTL은 쓰레드별로 스택 크기를 조정할 수 있으며, 최악의 경우에는 Event-driven과 같이 대부분의 state를 heap에 두면 대부분의 문제를 해결할 수 있다.

Synchronization

대부분의 서버의 경우 connection 단위의 세션들이 서로 공유해야하는 것이 별로 없는 경우가 많기 때문에 Synchronization 이슈가 생각만큼 크지는 않다. 하지만, 분명히 복잡한 logic을 가지고 synchronization을 많이 사용해야하는 경우도 존재할 것이다.

Event-driven 방식에서는 Synchronization의 문제가 자동적으로(for free) 해결된다고 하지만, 이는 Uniprocessor라는 가정하에서 뿐이다. Multiprocessor가 보편화된 환경을 고려하자면 Event-driven 방식이라고 해서 Synchronization 문제를 해결하지 않는 것은 어리석은 일이다. 더구나 Event-driven 방식에서 Synchronization을 하기 위해서는 state가 적어도 하나 더 추가되어야 한다. 아래에서도 설명하겠지만, Event-driven 방식에서 state 하나가 추가되는 것은 state 수 이상의 복잡도를 발생시킨다. 결국은 Synchronization을 하지 않는 디자인을 하는 수 밖에 없고, 이것은 말그대로 Synchronization을 하지 않는 것 뿐이고, 이러한 디자인 하에서는 Multithreading 방식으로 구현하더라도 Synchronization이 필요 없음을 의미할 뿐이다.

Why Events Are A Bad Idea에서 지적되었던 것처럼 Event-driven 방식 자체가 Synchronization 이슈를 해결하는 것이 아니라는 점을 되새길 필요가 있다. Multithreading에서 복잡한 Synchronization이 필요하다면 Event-driven 방식에서도 마찬가지다.

Complexity

Event-driven 방식 사용을 자제해야하는 가장 커다란 이유는 Complexity에 있다. Multithreading에 반대하는 많은 사람들이 Synchronization의 복잡도 때문에 Event-driven 방식을 사용해야한다고 주장하지만, Event-driven 방식을 제대로 경험해보지 못한 사람의 생각이라고 얘기하고 싶다.

2001년에 나는 당시로서는 서버 프로그래밍 방법 중 최고의 성능을 보여준다고 하는 kqueue()를 사용해서 Web Cache내의 Event-driven 방식 서버 엔진을 만든 경험을 가지고 있다. Web Cache는 단순한 서버가 아니라 서버와 클라이언트를 모두 포함하고 있기 때문에, 비교적 복잡한 Network I/O 패턴을 가지고 있다. 여기에 ICP(Internet Cache Protocol)나 DNS resolving과 같은 또다른 Network I/O나 cache 페이지를 접근하기 위해 Disk I/O가 더해지면 state 수가 불어나면서 엄청난 복잡도를 가지고 온다. Dispatcher와 State/Session 관리 부분을 모두 프레임웍화 한다고 하더라도 Event-driven 방식 자체에 내재한 복잡도는 state가 하나씩 추가될 때마다 state의 수 이상으로 증가한다. 다행히, 프로덕트는 나왔고, 외국의 벤치마크에서 좋은 평가를 받고, 상용화도 되었고, 어느 정도 팔렸으나, 차후에 자체적으로 개발한 쓰레드 패키지를 이용한 쓰레드 방식으로 재개발 되었다. 그 정도로 Event-driven 방식으로 만들어진 코드의 관리는 지속 불가능했다는 것이다.

이 후에, 다른 회사로 옮겨간 이후에도 서버 프로그래밍을 계속 해오면서 느낀 점 중 하나는, 많은 프로그래머들은 Event-driven 방식을 이해하기 힘들어 한다는 것이었다. 이것이 Event-driven 방식에 대한 회의를 느끼게 된 커다란 이유 중의 하나가 되었다. 나름대로 내린 결론은 기본적으로는 Why Events Are A Bad Idea에서도 지적되었던대로 Non-linearity다. 클라이언트-서버 패턴에서 서버는 클라이언트로부터의 요청을 처리해서 답변을 되돌려준다는 linear한 개념을 기반으로 하기 때문에, Event-driven이 서버에 도입하는 non-linearity는 클라이언트-서버 패턴을 구현하는데 중대한 장벽이 된다.

이 문제를 좀 더 자세히 들여다보면, decomposition 문제로 치환해서 생각해볼 수도 있다. 일반적으로 좋은 프로그램들은 logical한 단위의 decomposition을 사용한다. 그것이 structured이든 object-oriented이든 말이다. Event-driven 방식은 이벤트의 발생이 decomposition의 단위가 된다. 이벤트가 특정 애플리케이션에서 중요한 의미를 가지고 있다면 별다른 문제가 되지 않는다. 하지만, 적어도 서버에서는 일반적으로 이러한 이벤트의 발생이 자연스러운 기준이 되기는 힘들다. Event-driven 방식의 프로그래밍을 직접 해보면 깨닫게 되지만, 뭔가를 기다려야하는 operation, 즉 blocking operation이 도입될 때마다, 기다리는 상태와 깨어난 상태를 나타내는 두개의 state와 기다리는 상태에서 벗어나기 위한 이벤트가 하나씩 추가되어야 한다. 서버에서 이러한 blocking operation의 전형적이고 가장 자주 나타나는 예는 모든 종류의 I/O (Network I/O, Disk I/O, …)라고 볼 수 있다. 여기서 질문을 하나 던진다면, I/O 이벤트가 과연 좋은 decomposition 단위일까? 서버에서 I/O가 얼마나 높은 수준의 의미를 가질까? 이러한 blocking operation에 I/O만 존재한다면 그나마 행복한 편이다. Timeout을 상상해보라. Lock, Condition variable과 같은 Synchronization을 Event-driven 방식에서 구현한다고 상상해보라. (위에서 언급했듯이 Event-driven이라고 하더라도 Multiprocessor에서는 Synchronization이 필요하다.) I/O할 곳이 추가될 때마다, Synchronization이 추가될 때마다 서버의 복잡도는 지수적으로 증가할 것이다. 얼마지나지 않아 그 서버는 본인도 알아보기 힘들고, 아무도 건드리려하지 않는 괴물이 될 뿐이다. Web Cache를 만들던 시절에는 이러한 단점을 보완하기 위해서 I/O를 동반하지 않은 state를 만들거나 substate를 만들기도 했으나, 이러한 시도는 Why Events Are A Bad Idea에 언급되었던 ‘Just Fixing Events’에 지나지 않는다.

물론, Event-driven 방식이 완전히 쓸모없는 decomposition 방식인 것은 아니다. 애플리케이션 특성 상, 이벤트가 높은 수준의 abstraction을 가지는 경우, 특히 GUI 애플리케이션과 같이 어떤 이벤트가 일어났을 때 어떤 작업을 해야하는가를 생각하는 것이 자연스러운 경우가 분명히 존재한다.

Idle Connection Management

Introduction에서 했던 답변에 항상 덧붙여서 하는 얘기 중 하나는 바로 idle connection의 관리에 관한 얘기다. 서버를 Multithreading으로 구현한다고 하더라도 idle connection과 같이 하나의 thread를 할당하기가 불필요한 정도는 Event-driven 방식으로 따로 관리해서 thread의 낭비를 막는 방법 정도는 사용해볼만 할 것이다. Multithreading을 선호하라고 해서, 무조건 Event-driven 방식을 쓰지말라는 얘기는 아니라는 얘기다.

Cooperative Threads

Why Events Are A Bad Idea에서는 Synchronization이나 Scheduling에서의 오버헤드를 방지하기 위해서 User-level의 Cooperative Threads 구현을 사용해서 해결책을 내놓고 있지만, 위에서 기술한대로 Kernel-level 쓰레드 구현을 사용하더라도 크게 문제가 없거나 오히려 더 나은 해결책이 될 수 있다.

Standard Thread API

Linux에서도 기존의 구현 (linuxthreads)은 signal과 관련된 동작들이 제대로 정의된 최근의 POSIX 표준을 따르지 않았기 때문에 여러가지 문제들이 많았다. 특히, 커널의 지원이 미비한 상황에서 구현의 한계상 signal과 관련한 표준들이 제대로 지켜지기 힘들었다. 하지만, Linux 2.6에서 지원되는 NPTL은 구현시부터 POSIX compliance가 상당히 중요한 목표로 책정되어 개발되었다.

Conclusions

기존에는 Event-driven방식이 Highly-Concurrent 서버를 개발하기 위한 최적의 방법이라고 생각되었으나, 이제는 Multiprocessor의 보편화, 저가의 메모리, 64bit 메모리 스페이스, OS와 쓰레드 구현의 진화 등 컴퓨팅 환경의 변화로 인해서 Event-driven방식 보다는 Multithreading 방식을 선호하는 것이 타당하다고 생각된다.

Prefer Multithreading to Event-driven in Highly-Concurrent Servers 더 읽기"

Why Events Are A Bad Idea

HotOS IX 에 발표된 ‘Why Events Are A Bad Idea‘는 이벤트 기반 프로그래밍(event-based programming)에 비해 쓰레드 프로그래밍(thread programming)을 옹호하는 입장을 취하고 있는 논문 중의 하나다. 이 문서는 위 논문의 한글 요약이다.

1 Introduction

Highly concurrent application에 있어서 그동안 쓰레드를 사용한 시도들이 실패해왔기 때문에, 연구자들 사이에 이벤트 기반 프로그래밍이 쓰레드 프로그래밍에 비해 더 나은 선택이라고 결론 내려지고 있는데, 그 이유를 다음과 같이 요약하고 있다.

  • cooperative multitasking으로 인해 synchronization cost가 저렴하다.
  • state를 관리하기 위한 오버헤드가 낮다. (스택이 없음)
  • appplication 수준의 정보를 이용하여 보다 나은 스케줄링과 locality를 얻을 수 있다.
  • 보다 유연한 control flow.

이 논문의 기본적인 생각은

  • 쓰레드가 Highly concurrent application에 대해 좀 더 자연스러운 abstraction을 제공하며,
  • 컴파일러와 쓰레드 시스템 (패키지)의 약간의 개선을 통해서 쓰레드에 비해 이벤트를 사용해야만 했던 이유 (주로, 성능)가 없다는 것

을 지적하며, 쓰레드 프로그래밍이 이벤트 기반 프로그래밍에 비해 더 선호되어야 한다고 주장하고 있다.

2 Threads vs. Events

2.1 Duality Revisited

Lauer와 Needham은 ‘On the duality of operation system structures‘에서 쓰레드(threaded) 방식과 메시지 전달(message-passing, i.e., event-based) 방식은 대등하고 동등한 성능(equivalent performance)을 가지고 있기 때문에, 대상 애플리케이션에 더 자연스러운 방식을 선택해야한다고 주장한다. 이 논문의 저자는 (대상 애플리케이션이) high-concurrency server의 경우에는 쓰레드에 기반한 접근이 더 낫다고 생각한다.

저자는 Lauer와 Needham의 메시지 전달 시스템이 다음과 같은 점들에서 현재의 이벤트 시스템(modern event systems)과 정확하게 대응하지는 않는다고 얘기하고 있다.

  • 이벤트가 synchronization을 위해 사용하는 cooperative scheduling을 무시하고 있다.
  • 대부분의 이벤트 시스템들은 shared memory와 global data structure를 사용하고 있으나, Lauer와 Needham은 이것이 전형적이지 않은 (atypical) 것으로 표현하고 있다.
  • Lauer와 Needham이 주장한 동등한 성능(performance equivalence)은 제대로 된(equally good) 구현을 필요로 한다. 저자는 very high concurency에 적합한 쓰레드 구현이 존재하지 않다고 생각한다.

동등한 성능을 주장함에 있어서 Lauer와 Needham은 blocking/yielding point가 node이고 그러한 point들 사이에서 실행되는 code가 edge인 그래프를 사용하고 있는데, 두 방식은 본질적으로 같은 그래프를 가지고 있다고 얘기하고 있다. 저자는 이러한 사실이 쓰레드의 성능과 사용성에 대한 비판이 특정한 쓰레드 구현의 문제에서 나온 것이지 일반적인 쓰레드에서 발생한 것이 아니라고 보고 있다.

2.2 “Problems” with Threads

이 섹션에서 저자들은 쓰레드에 관련된 비판을 하나씩 반박하고 있다.

Performance

Criticism: Many attempts to use threads for high concurrency have not performed well.

저자들은 좋지 않은 쓰레드 구현 때문이라고 주장한다. 특히 현재의 쓰레드 구현들은 high concurrency나 blocking operation를 위해 설계되지 않았다고 지적하고 있다.

쓰레드 구현에서의 오버헤드의 원인은

  • 쓰레드의 수에 따른 O(n) operation의 존재와
  • 이벤트에 비해 상대적으로 높은 context switch 오버헤드 (preemtion을 위한 context 저장 오버헤드와 커널 쓰레드의 커널 진입에 의한 오버헤드)

라고 한다.

저자들은 이러한 단점들은 역시 쓰레드의 기본적인 특성은 아니라고 얘기하고 있다. 저자들이, O(n) operation들을 대부분 제거한 GNU Pth user-level 쓰레드 구현을 사용한 SEDA 쓰레드 서버 벤치마크를 반복해본 결과, 10만개의 쓰레드에 대해서도 충분히 scale하며, 성능도 이벤트 기반 서버와 거의 동등하다고 한다.

Control Flow

Criticism: Threads have restrictive control flow.

쓰레드를 사용하는 프로그래머는 control flow에 대해 너무 linear하게 생각하므로 효율적인 control flow 패턴을 사용하지 못한다는 주장에 대해, 저자들은 복잡한 control flow 패턴은 실제로는 드물다고 얘기하고 있다. 저자들이 실제 애플리케이션들의 코드 구조를 분석한 결과 control flow 패턴은 call/return, parallel calls, pipelines의 세가지로 떨어진다고 한다. 그리고 이 패턴들은 쓰레드로 보다 자연스럽게 표현할 수 있다.

저자들은 이보다 복잡한 패턴들은 제대로 사용하기가 어렵기 때문에 사용되지 않는다고 주장한다. 이벤트 시스템에서 자주 일어나는 비본질적인 비선형성은 애초에 이해하기 힘들며, 미묘한 에러가 발생하기 쉽게 만든다고 한다. 의도적인 복잡한 control flow도 마찬가지라고 한다.

multicast나 publish/subscribe 애플리케이션에서 사용되는 dynamic fan-in/fan-out 패턴은 쓰레드보다는 이벤트 시스템과 더 잘 어울리는데, 조사한 high concurrency 서버들에서는 어떤 서버도 이 패턴을 사용하지 않았다고 한다.

Synchronization

Criticism: Thread synchronization mechanisms are too heavyweight.

이벤트 시스템은 cooperative multitasking으로 인해서 synchronization을 위한 mutex 등의 오버헤드가 없다고 여겨지나, Adya 등은 Cooperative task management without manual stack management에서 이러한 장점은 이벤트 자체에서 얻어지는 것이 아니라 cooperative multitasking에 있다고 한다. 따라서, cooperative 쓰레드 시스템에서도 같은 이득을 볼 수 있다. 하지만, 이러한 이득은 단일 프로세서에서만 얻어지는 것이고 일반적으로 high concurrency 서버들이 실행되는 다중 프로세서에서는 얻을 수 없는 것이다.

State Management

Criticism: Thread stacks are an ineffective way to manage live state.

쓰레드 시스템은 stack overflow와 virtual address space 사이의 tradeoff 문제를 가지고 있다. 저자들은 dynamic stack growth를 사용해서 이 문제를 해결할 것을 제안한다. 이벤트 시스템의 경우 쓰레드를 거의 사용하지 않으므로 이러한 문제가 없지만, live state의 관리를 프로그래머가 모두 직접해주어야 한다. 반면, 쓰레드 시스템은 stack을 통한 자동적인 관리가 이루어진다.

Scheduling

Criticism: The virtual processor model provided by threads forces the runtime to be too generic and prevents it from making optimal scheduling decisons.

이벤트 시스템은 애플리케이션 레벨에서 이벤트를 스케줄링할 수 있으므로 몇가지 최적화가 가능할 것이다. 또한, Larus와 Parkes에 의하면, Using cohort sched
uling to enhance server perf
ormance
에서는 같은 종류의 이벤트를 한번에 처리함으로써 code locality를 개선함으로써 얻는 이익도 있을 수 있다고 한다. 하지만, 이러한 모든 이점들은 cooperatively scheduled threads를 사용함으로써 똑같이 얻을 수 있는 것들이다.

2.3 Summary

쓰레드도 high concurrency에 대해 적어도 이벤트 만큼 성능을 낼 수 있으며 이벤트에 중요한 질적인 이점은 없다. 이벤트 스타일을 많이 추구하게 된 것은 쓰레드의 기본적인 특성이라기보다는 scalable한 user-level 쓰레드가 없었기 때문이다.

3 The Case for Threads

다음의 두가지 이유에 기반하여 쓰레드가 high-concurrency 서버에 보다 적합한 abstraction이라고 얘기하고 있다.

  • 동시에 발생하는 request는 거의 독립적이다.
  • 각각의 request를 다루는 코드는 보통 순차적이다.
Control Flow

High-concurrency 시스템들에서 이벤트 기반 프로그래밍은 애플리케이션의 control flow을 이해하기 힘들게 만든다. 이벤트 시스템에서는 이벤트를 다른 모듈에 보냄으로써 어떤 메서드를 “call”하고, 그 메서드에서의 “return”을 기대한다. 프로그래머는 마음속으로 이러한 call/return pair를 파악하고 있어야한다. 더구나 프로그래머는 call/return pair를 위해 live state를 저장하고 복구해주어야 한다. Adya는 이를 “stack ripping”이라고 부르는데, 이벤트 시스템을 사용하려는 프로그래머의 가장 큰 부담이라고 얘기한다.

쓰레드 시스템에서는 control flow나 state를 좀 더 자연스러운 방식으로 표현할 수 있다. call과 return은 문법적으로 grouping이 되므로 1:1의 인과 관계를 이해하기 쉽고, call stack이 모든 live state를 encapsulate한다.

Exception Handling and State Lifetime

예외 상황 또는 정상적인 종료 이후, task의 state를 clean up하는 것은 쓰레드 시스템에서 더욱 간단하다. 이벤트 시스템에서는 task state가 일반적으로 heap에 할당되어 있기 때문에, control flow가 분기를 통해 복잡해지면, 적절한 시점에 이를 지우는 것은 매우 힘들다. Ninja난 SEDA와 같은 이벤트 시스템에서는 이 문제를 해결하기 위해 garbage collection을 사용하지만, Java의 general-purpose garbage collection 메커니즘은 high-performance 시스템에 부적합하다고 한다. Inktomi의 Traffic Server는 reference counting을 사용하고 있지만 정확한 counting은 어렵다고 한다.

Existing Systems

저자들이 만든 Ninja 시스템은 recovery와 같은 복잡한 부분에서 이벤트를 사용해 정확한 동작을 얻기가 거의 불가능했기 때문에, 결국 쓰레드를 사용했다고 한다. 이 외에도 high concurrency를 필요로 하지 않는 애플리케이션은 항상 쓰레드를 사용하고 있다.

Just Fix Events?

쓰레드를 사용하는 대신 이벤트 시스템의 문제를 해결하는 도구와 언어를 만들어야한다는 주장은 결국 쓰레드의 문법과 run-time behavior를 복제할 뿐이다. Adya가 기술한 cooperative task management 기술은 이벤트 시스템의 사용자로 하여금 쓰레드와 같은 코드를 쓰게 하고, 이것이 blocking call들 사이의 continuation으로 변환되도록 한다. 결국 대부분의 경우 이벤트의 문제를 수정하는 것은 쓰레드로 전환하는 것과 같다.

4 Compiler Support for Threads

4.1 Dynamic Stack Growth

run time에 stack의 크기가 조정될 수 있도록 하는 메커니즘을 통해 고정된 크기의 스택 때문에 stack overflow와 address space의 낭비 사이에서의 tradeoff 문제를 제거할 수 있다. compiler의 분석을 통해서 각각의 function을 호출할 때 필요한 stack 크기에 대한 upper bound를 알 수 있으므로 호출하는 곳에서 stack growth가 필요한가도 판단할 수 있다. recursive function이나 function pointer가 추가적인 문제를 던져주지만 이것도 추가적인 분석을 통해 해결할 수 있다.

4.2 Live State Management

function 호출을 하기전에 컴파일러가 불필요한 state를 쉽게 제거할 수 있다. (e.g. tail call optimization) 하나의 blocking call에 많은 양의 state가 걸려있다면 컴파일러가 프로그래머에게 경고해줄 수 있다.

4.3 Synchronization

컴파일 시간의 분석을 통해 프로그래머에게 race condition을 경고해줌으로써 버그를 줄일 수 있다. TinyOS에 기반한 networked sensor를 위한 언어인 nesC는 atomic section에 대해 지원하고 concurrency model에 대해서 이해한다. atomic section이 동시에 수행되어도 안전한지를 결정하는데 도움을 줄 수도 있다. 다중 프로세서에 대해서도 마찬가지고 libasync에서 직접 코딩한 graph coloring 기술을 자동화하는 것이다.

5 Evaluation

<생략>

6 Relative Work

<생략>

7 Conclusions

이벤트 시스템이 high concurrent 시스템에서 좋은 성능을 얻는데 사용되어왔지만, 쓰레드를 사용해서 비슷하거나 더 나은 성능을 얻을 수 있다. 간단한 프로그래밍 모델과 더 나은 컴파일러 분석이 가능하다는 점은 쓰레드에 중요한 이점을 제공한다. 저자들은 컴파일러와 쓰레드 시스템의 결합을 통해서 더욱 뛰어난 성능을 얻는 동시에 깔끔하고 간단한 인터페이스를 가진 프로그래밍 모델을 프로그래머에게 제공해줄 수 있을 것이라고 주장한다.

Why Events Are A Bad Idea 더 읽기"

사용하고 있는 Firefox Extension

사용하고 있는 Firefox Extension

현재 사용하고 있는 Firefox Extension들의 리스트입니다. (원문은 제 위키의 Firefox Extension 페이지) Firefox 2.0으로 가기 전에 호환성도 체크해야 해서 한번 정리해봅니다. Tab Mix Plus 빼고는 대부분 호환되는 것 같군요. 그래서, Tab Mix Plus가 2.0을 지원하기 전까지는 2.0으로 가지 않을 것 같군요.

General

[edit]

Testing

[edit]

Development

사용하고 있는 Firefox Extension 더 읽기"

Threads Considered Harmful

Threads Considered Harmful

Summary

쓰레드(thread)의 문제점을 지적한 것으로 상당히 유명한 글 중에 하나가 바로 Kuro5hin에 올라온 ‘Threads Considered Harmful ‘이라는 글이다. 이 글의 주요 논지는 shared state에 대한 synchronization을 제대로 하는 것과 dead lock을 방지하는 것이 어렵고, 이를 탐지하기가 어렵기 때문에, 쓰레드(thread)를 사용하지 말아야하고, 그 대안으로 Multiple processes, Event-based programming, Co-operative threads (co-routines) 등의 방법을 사용해야한다는 것이다.

Detailed Review

맨 첫 문단에서 이 글의 저자는 다음과 같이 얘기하고 있다.

once you have spawned a thread, there is no way to know anymore which line is being executed in parallel with yours.

이 얘기는 ‘Go To Considered Harmful‘에서 Dijkstra가 주장하고 있는 프로세스의 진행 정도를 표현할 수 있는 의미있는 좌표가 존재해야한다는 아이디어를 thread에 적용한 것이다. 하지만, 적어도 구조적인(structured) 프로그램을 수행하는 여러 thread의 진행 정도를 표현하는 의미있는 좌표는 존재한다. ‘Go To Considered Harmful’ 논문에 대한 리뷰 글에서 설명한 방식을 빌려오자면, 우리가 여러 쓰레드를 사용하는 프로그램을 디버깅하기 위해 멈췄을 때, 우리는 모든 쓰레드의 진행 정도를 각 쓰레드의 stack trace를 통해서 알 수 있다. 글의 저자는 문제를 약간 잘못 설명하고 있다. 진짜 문제는, 서로 간에 side-effect를 미칠 수 있는 쓰레드들이 어떤 식으로 수행되느냐에 따라 결과가 달라지기 때문에, 프로세스의 진행 정도를 알 수 있다고 해서 디버깅을 할 수 없게된 것이다. Dijkstra가 위의 논문 처음에서 내세웠던, 정적인 프로그램과 동적인 프로세스 간의 상응성을 단순하게 만들기 위해서는 프로세스의 진행 정도만으로는부족한것이다.

예를 들어, a-b-c-d라는 프로그램을 수행하는 두 thread가 있어서 어떤 시점의 프로세스들의 진행 정도가 둘다 d라는 걸 알더라도, 실제로 수행된 순서가 ababcc인지 aabbcc인지를 알지 못하면 디버깅을 불가능한 것이다.

여담으로, 쓰레드를 하나의 프로세스로 보지 않고,모든 thread를 각 쓰레드가 수행하는 프로그램이 랜덤하게 수행되는 하나의 프로세스로 본다면, 프로세스의 진행 정도를 표현할 수 있는 좌표 자체가 존재하지 않는다고 볼 수도있다. 이 문제에 여러 프로세서(SMP)의 문제까지 끼어들면 더욱 복잡한 모델이 된다.

한편, 다음 문장이 여러 쓰레드를 사용하는 프로그램의 디버깅이 힘든 이유를 한마디로 표현하고 있다.

The chief problem with threads are race conditions and dead locks. Both tend to occur randomly.

친절하게도 자세한 예를 들어 synchronization의 어려움과 dead lock을 방지하는 것의 어려움을 강조하고 있고, dead lock을 방지하기 위해 lock들의 순서를 지키는 것이 부담스러워서 coarse-grained lock을 쓴다면 performance 저하의 문제가 있음을 지적한다.

저자가 쓰레드를 사용하는 것에 어떠한 이익도 없다고 결론 내리고 대안으로 내세운 것은 Multiple processes, Event-based programming, Co-operative threads (co-routines)가 있다.

My Opinion

  1. 쓰레드의 필요 뿐만 아니라 쓰레드의 대안들에서 오는 복잡성은 모두 concurrently shared state (또는 resource)의 필요에서 발생한다. concurrently shared state가 전혀 없다면 물론 쓰레드를 사용할 필요는 없다. 글의 저자가 주장한 것과 같이 하나 또는 그 이상의 프로세스를 사용해도 되고, 설령 쓰레드를 사용한다고 하더라도 글의 저자가 우려하는 것과 같은 사태는 절대 발생하지 않는다. 하지만, 많은 프로그램은 어느 정도의 concurrently shared state를 필요로 한다. 그것이 메모리 조각이든, 객체든, 파일과 같은 커널 리소스든, 그것이 공유되기 시작하면, 여러 프로세스에서도 똑같은 문제가 발생하기 시작한다. 복잡성의 원인은 해결법(쓰레드)에 있는 것이 아니라 궁극적으로 우리가 풀어야하는 문제 자체에 내재하고 있는 것이다.
  2. shared state vs. messaging 문제. shared state와 messaging 사이에 어느 것을 선택할 것이냐 하는 것은 컴퓨팅의 역사에서 항상 반복되어온 문제다. OS의 디자인이나 구현을 보다보면, 어떤 resource (또는 state)에 대한 접근을 strict하게 제한할 것이냐, 아니면 시스템이 fragile하지 않는 한 느슨하게 허용할 것이냐의 trade-off 문제가 상당히 자주 등장한다. 여기에는 성능이나 디자인 등의 문제가 복잡하게 게재된다. 쓰레드를 쓸 것이냐의 문제도 trade-off 문제에 불과하다고 생각한다.
  3. 적절한 대안의 부재: 프로그래밍은 계속 더욱 높은 레벨의 abstraction을 지향해서 언젠가는 이런 고민을 하지 않아도 될 날이 올 것이다. 하지만, 현재로서는 만족할만한 대안이 없는 것이 사실이다. 프로세스는 concurrently shared state가 없다는 가정하에서 쓰레드와 다를 바는 없고, 이벤트 기반 프로그래밍은 쓰레드 만큼의 어쩌면 더 많은 복잡성을 프로그램 내에 도입할 뿐만 아니라, 쓰레드 지원이 허름하던 시절의 성능 이점을 가져다 주지도 않는다. (쓰레드 프로그래밍과 이벤트 기반 프로그래밍의 비교에 대해서는 다음 기회에 글을 써보도록 하겠다.) Parallel programming 문맥에서의 Co-routine은 go to를 대체하는 loop와 같은 programming constructs의 역할과 비슷한다고 생각되는데, 주요 프로그래밍 언어에서 구현되어 있지 않을 뿐만 아니라, (그 때문에) 보편적으로 practice가 확립되어있지 않다. (학교에서 가르치지 않는다.)

Conclusion

concurrently shared state라는 문제 자체의 복잡성이 쓰레드 프로그래밍이나 이벤트 기반 프로그래밍의 복잡성을 야기하고, 어느 해결책을 선택하느냐는 trade-off의 문제이나, 현재로서는 쓰레드가 더 나은 해결책으로 보인다. go to를 대체하는 loop constructs와 같이 쓰레드를 대체하는 프로그래밍 언어 레벨의 해결책이 필요하다는 것에는 크게 공감한다.

Threads Considered Harmful 더 읽기"

Prefer java.net.URI to java.net.URL

Prefer java.net.URI to java.net.URL

Problem of java.net.URL

URL의 host 부분의 equivalence가 host string의 equivalence를 기준으로 하는 것이 아니라, IP address의 equivalence를 기준으로 한다. 또한, equivalence 관련 메서드 (equils(), hashCode())이 일어날 때마다, IP address resolving을 동반한다.
equivalence 관련 operation이 사용되지 않는다면 별로 문제가 없으나, equivalence 관련 메서드를 자주 사용하면, 상당히 느릴 수 있으며, HashMap의 key로 java.util.URL을 사용할 경우, host 부분만 다르고 각 host들의 IP가 같다면, HashMap에는 하나의 key만이 등록되는 의도하지 않은 문제가 발생할 수 있다.

Solution

1. Overriding java.net.URLStreamHandler

java.util.URL 클래스의 equivalence 관련 구현은 모두 java.util.URLStreamHandler의 구현에 위임되어있다. 그리고 java.util.URL 클래스는 static 메서드인 setURLStreamHandler()를 통해, 이 구현을 변경할 수 있다.

2. Using java.net.URI

Java 1.4에 java.net.URI가 추가되었고, 이 클래스의 equivalence는 String representation의 equivalence와 동일하다. java.net.URL의 hashCode() 메서드는 각 파트에 해당하는 String.hashCode()의 합인 반면, java.net.URI의 hashCode() 구현은 제대로 된 hash 값을 구하도록 구현되어있다.

References

Prefer java.net.URI to java.net.URL 더 읽기"

스팸 필터들의 마이너리티 리포트

저는 MovableType에 MT용 Akismet 플러그인을 설치해서 사용하고 있는데요. 난감한 상황이 벌어졌습니다.

Final Feedback Rating: 0
Test Score Results
SpamLookup IP Lookup -1.0 213.140.56.3 found on service bsb.spamlookup.net
Akismet +1.0 Akismet says ham

어떤 comment에 대해서 MT의 SpamLookup 플러그인은 spam이라고 판정했는데, Akismet은 ham이라고 판정한 상황입니다. 결국 스팸 rating이 0이되어 ham으로 통과되어버린 상황입니다. Spam positive 가중치를 높혀서 문제점을 해결한 상태입니다만, 스팸 필터가 예언자 수준의 신뢰도를 준다면 영화 ‘마이너리티 리포트‘에서처럼 세 가지의 스팸 필터를 사용해야할지도 모르겠네요. 그럼에도 불구하고 ‘마이너리티 리포트’가 옳았다라는 더욱더 난감한 상황이 벌어질지도 모르겠지만요.

스팸 필터들의 마이너리티 리포트 더 읽기"