Interop Madness

클라이언트 서버간 암호화  관련해서 개념 증명 + 몇 가지 성능 측정을 해야해서 몇 가지 언어 + 플랫폼에서 libsodium 1 을 호출해봤다. 그 와중에 겪은 일 정리.

C++

이쪽은 평범한 C/C++ 인터페이스니 문제 없이 끝.

Python

위에서 구현한 부분을 클라이언트 쪽 구현을 (빨리) 만들어서 테스트하는 작업을 했다.

여기서부터 문제가 생기기 시작. Ubuntu 16.04 (xenial) 이 제공하는 libsodium 래퍼가 python-nacl 2 인데, 써보니 내가 쓰려는 함수가 여럿 없다. 특히 crypto_stream* 류 함수가 없더라. 그래서 다른 래퍼가 있나하고 PyPI 에서 몇 가지 써보고는 빠른 포기.

이제 cffi 를 쓸 타이밍. C 코드로 정의한 함수를 변환해서 만든  .py 파일로 동적 라이브러리 (.so, .dll, .dylib) 에서 가져오는 방식을 썼다. 3 예~전에 ctypes 로 로딩할 때 괴로웠던 부분이 확실히 줄어들었다. ABI 대신 API 수준에서 접근하는 차이는 정말 컸다. cdata의 buffer 인터페이스도 쓸만했다.

cffi 로 libsodium 에서 필요한 함수들을 가져다 쓰기 시작한 이후로는 금방 일이 정리.

…하지만 이 시점에선 어떤 비참한 미래가 기다리는지 모르고 있었다. (?)

C#

(나로써는) 슬프게도, 클라이언트 쪽 구현 중 하나는 C#으로 해야한다. (Unity3d + mono) 게다가 이건 여러 플랫폼을 지원해야 한다:

  • Unity Editor: Windows, MacOS
  • Android: 다행히도 ARMv7만. Unity가 ARMv6는 지원하지 않는다 (?)
  • iOS: 기기 + 시뮬레이터에서 모두 돌아야 한다.

Windows는 (내가) 당장 쓸 머신이 없어서 일단 미뤄두고, 나머지에 대해서 잘 도는지 시험하기 시작. 꼬박 2일 걸린 것 같다 — 생각해보니 절반은 C++ 기반인 cocos2d 랑 같이 처리하는 것 때문이었던 것 같기도?

우선 각 플랫폼 별로 크로스 빌드를 시도…해야하나 했는데, libsoidum 저장소에 플랫폼 별 빌드 스크립트가 있다. 4 iOS/Android 쪽에 내가 쓰려던 함수 중 몇 개가 빠져서 컴파일 플래그를 조금 고친 것 말고는 별 일 없이 진행. 그리고 Unity 에서 (native) 동적 라이브러리를 어떻게 가져가나 고민했는데 경험 많은 동료분이 설명해주셔서 매우 간단한게 통과.

대략,

  • DllImport 로  C에 있는 함수들을 CLR 쪽에 노출하고
  • C# 코드로 이 함수들을 래핑하고
  • 다시 이 함수들을 써서 실제 개발자가 쓸 고수준 API로 바꾸자

라는 방향으로 진행했다. 에디터(=랩탑 위)에서 잘 도는걸 보고 android  기기에 올리기 시작. 왠걸, 초기화 (=sodium_init()) 은 성공했고 몇몇 초기화도 했는데, 그 이후에 호출하는 함수에서 크래시 (SIGSEGV).  (당연하게도) 콜 스택은 native code에서 죽는 것.

원인을 추측하기 시작:

  • C 함수 원형을 잘못보고 옮겼는지 확인 시작. C는 타입 무시하고 심볼 이름만 보니 내가 잘못 옮겼다면 크래시할 수도 있으니 우선 여길 의심하고 C 함수 원형과 하나씩 대조. 별 이상 없어 보였다 (불행의 단초)
  • 크로스 컴파일을 잘못했는가 확인: 생각해보니 C/C++ 로 올렸을 때는 잘 돌았다 (…)

…대략 정신이 멍해져오는데 다시 함수 원형을 보다가 불현듯 깨달은 사실:

C 쪽의 size_t 를 C#의 ulong 으로 지정했는데 Unity + ARMv7 은 32bit ABI를 쓰니 uint 여야하는 것. Orz.

그래서 이걸 iOS / Android 플래그로 ulong / uint 처리하게 했다가, iOS도 아직 32bit인게 남아있는 걸 보고는 (iPhone 5 및 그 이전 + iPhone 5C) UIntPtr 로 처리하고 강제 캐스팅하게 수정해서 마무리했다. 그러고나니 잘 돌더라.

 

3줄 요약

  • ABI 맞추기는 삽질이다. 어떻게든 API 수준으로 해결할 수 있는 법을 찾자 — python의 cffi 처럼. 아니면 기계적으로 처리할 방법을 찾아야?
  • (부득이하게) ABI를 맞춰야 한다면 가능한 arch 목록을 잘 확인하자. C/C++에서 편했던 platform dependent type들이 대재앙
  • 여러 플랫폼 지원하는 라이브러리는 정말 인생에 도움이 된다. 잘 쓰고나면 널리 알려줍시다 (?)

 


  1. libsodium 페이지. 현 시점에 적합한 대칭키 암호화 + AEAD, ECC 함수들, system random source 를 래핑한 함수, side channel 공격을 피하는 비교, 해시, … 등등을 제공하는 잘 만든 라이브러리. 인터페이스도 보고 바로 쓰는게 그다지 어렵지 않다. 
  2. GitHub 저장소. libsodium 이 fork(?)한 NaCl 을 래핑한 것 같은 이름이지만 libsodium 을 래핑한다. 
  3. API 수준에서, 소스 코드 밖에서 정의 부분을 참고해서 작성했다. 
  4. 저장소의 dist-build 디렉터리 의 스크립트들이 이에 해당한다. 

프로그래머의 일상: 험난한 python 패키징

제목처럼 거창한 건 아니지만.

웹 페이지에서 JSON web token (jwt) 를 쓸 일이 있어서 pyjwt 를 가져다 쓰기 시작했다. 근데 이걸 서비스 중인 vm들에 배포하려면 .deb 패키지로 묶어야 한다. Python 라이브러리를 .deb (간단하게) 패키징 할 때는 python-stdeb 패키지를 이용해서 간단히 묶어 버리고 있다 – 이건 기회가 되면 나중에 설명할 일이 있었으면 한다.

하지만 문제 발생:

$ python setup.py  --command-packages=stdeb.command bdist_deb
...
Traceback (most recent call last):
  File "setup.py", line 77, in 
    'jwt = jwt.__main__:main'
  File "/usr/lib/python2.7/distutils/core.py", line 151, in setup
    dist.run_commands()
  File "/usr/lib/python2.7/distutils/dist.py", line 953, in run_commands
    self.run_command(cmd)
  File "/usr/lib/python2.7/distutils/dist.py", line 972, in run_command
    cmd_obj.run()
  File "/usr/lib/python2.7/dist-packages/stdeb/command/bdist_deb.py", line 23, in run
    self.run_command('sdist_dsc')
  File "/usr/lib/python2.7/distutils/cmd.py", line 326, in run_command
    self.distribution.run_command(command)
  File "/usr/lib/python2.7/distutils/dist.py", line 972, in run_command
    cmd_obj.run()
  File "/usr/lib/python2.7/dist-packages/stdeb/command/sdist_dsc.py", line 139, in run
    remove_expanded_source_dir=self.remove_expanded_source_dir,
  File "/usr/lib/python2.7/dist-packages/stdeb/util.py", line 1061, in build_dsc
    -- %(maintainer)s  %(date822)s\n"""%debinfo.__dict__)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3: ordinal not in range(128)

문제의 원인이 좀 어이 없었는데 – 달력을 확인한다면 데미지 두 배,

  • 저자 이름에 non-ascii 문자가 포함되어 있고 (저자의 이름이 José)
  • stdeb 에서 문자열 치환할 때 이 처리를 하지 않고 있는 것

해당 버그는 아직 처리 중인듯.

그래서 문제를 우회할 방법이 두 가진데,

  1. stdeb 버그를 수정한다; 이 경우엔 저 위의 오류난 곳의 __dict__ 의 unicode 타입인 걸 하나 하나 찾아서 encode('utf-8') 하는 것
  2. 문제를 부정한다 (?): …이러면 안될 것 같지만 저자 이름을 수정

일단 2로 우회했는데 – stdeb 도 우분투 패키지라 이걸 덮어쓰게 배포하기가 찜찜해서 – 외부에 배포할 일이 생기거나 하면 결국 1안으로 가야할 듯 하다. 혹은 더 복잡한 다른 패키지 .deb로 만들 때 처럼 debian 파일들을 다 만들거나.

요약 (?)

  • Python 모듈을 .deb 로 변환하는 python-stdeb 패키지가 있다.
  • 근데 이 패키지가 unicode 를 제대로 못 다루는 부분이 있다.
  • 엄한 우회책은 있지만 매우 괴롭다. 지금 2016년인데…

Python 으로 JSON 빨리 처리하기

지난 며칠 동안 모 모바일 게임의 알파테스트가 있었다. 그리고 갑이 뽑아달라는 ad-hoc 통계를 뽑느라 로그 처리를 왕창 할 일이 있었다.

대략,

  • 로그 데이터는 하나의 JSON 문서로, 각 파일에 한 줄씩 차지하고 있다
  • 파일은 날짜 별로 쪼개져서 서버 로그 디렉터리에 남아있다

내부 테스트나 첫날 정도까지는 로그 처리가 순식간에 끝날 수준이라 (k초 수준; k < 10) 별로 신경 안 썼는데, 막상 데이터가 커지기 시작하니 오래 걸리기 시작.
(오래 걸린다 == 실행하고 결과 나올 때까지 걸리는 시간이 내 집중력이 유지되는 시간보다 길다)
그리고 ad-hoc 통계를 뽑으려다 보니 내가 실행하는 횟수가 계속 늘어서 걸리는 시간 줄이려고 삽질한 기록이 아래와 같다.

진행

대략 로그가 1 GiB 정도 쌓인 시점에서, 로그 처리를 완료하고 통계 데이터가 나올 때까지 55초 정도 걸리더라. (i7-2600 / 메모리 16 GiB / 샘숭 840pro)
일단 아래 두 가지로 급한 불은 껐고, 알파테스트가 끝나갈 때야 왜 빨라졌는지 알게 되었다.

일단 PyPy 를 끼얹어 본다.

메모리야 남아돌고, CPU가 100%로 튀는 상황이니 일단 PyPy 를 가져다가 써봤다.
Ubuntu 14.04 에는 PyPy 2.2.1 이 있다. 이걸 설치하고 동작하니 대략 33초까지 줄더라. (40% 향상)

최신 버전 릴리즈되는 PPA가 없나 검색해보니, 있더라: https://launchpad.net/~pypy.
여기서 PyPy 4.0.1을 가져다 쓰면 28초까지 실행 시간이 줄어들더라. (49% 향상)

mmap 도 끼얹어 봅니다.

이전에 하재승 군이 mmap 언급했던 게 생각나서 적용해보았다.
조금 더 빨라진다. 이젠 23초 수준.

여하튼 이 상태로 알파테스트 종료까지 갔더니 대략 로그 2GiB 처리하는데 53~55초 수준. 아마 처음 상태로 끝까지 갔으면 (끝나는 날 오후에도 갑이 통계 추가해달랬으니..) 내 멘탈은 더욱더 남아나지 않았을 듯 (…).

왜 빨라졌는가

왜 빨라졌는지 파악 하지 못하고 있었으니 측정해보자.
vmprof 프로파일러 를 붙여서 돌려봤다. (2:20 소요)

Python 2.7 실행 결과

loads 밑 그 하위 함수들 (decode, raw_decode) 들에서 잔뜩 시간을 쓰고 있다. 즉, 대부분의 시간을 JSON 디코딩하는데 쓰고 있다는 소리 (json.loads 함수라서)

PyPy 로 구동한건 다음과 같다. 0:55 소요.

PyPy 4.0.1 실행 결과

작은 루프를 계속해서 돌기 때문인지 대부분의 구간이 JIT 번역된 코드를 실행하고 있다. 여기서도 json.loads 호출의 비율이 크지만 Python 2.7 보다는 그 비율이 낮다.
왜 JSON 성능이 PyPy 쪽이 좋은지 확인해보니 PyPy 2.2.0 릴리즈 노트에 그런 내용이 있더란.

JSON decoding is now very fast (JSON encoding was already very fast)

근데 JSON 성능은 (보통은?) 외부 C library 에 의존해서 성능을 올린 경우가 많다. 그래서 전통의(?) simplejson 과 ujson 을 붙여서 테스트해봤다.
simplejson 으론 성능 향상이 없더라. (PyPy 의 경우 오히려 느려졌다)
C기반 JSON 구현체의 python 바인딩인 ujson 을 시도해봤다.

Python 2.7 + ujson 실행 결과

… ujson C 코드 디버깅 심볼이 없어서인지 제대로 프로파일링 안된 것 같긴한데, 같은 Python 2.7이면서도 시간은 대폭 줄었다. 1:19. (거의 절반)
그래도 통계 데이터를 실제로 해석하는 작은 루프들 성능이 JIT 쪽이 나은지 전체 시간은 여전히 PyPy 쪽이 더 좋다.

요약

  • vmprof 좋아요 씁시다.
  • Python 표준 라이브러리의 JSON은 너무 느리다. CPython을 써야한다면 ujson, 그게 아니면 PyPy 쓰자.

Scrapy로 웹사이트 크롤링 해보기

웹 사이트 몇 개(=네xx카페)를 지속적으로 크롤링 할 일이 생겨서 몇 가지 라이브러리를 뒤적여봤다.

처음 시도한 것은 selenium. 하지만 UI 요소에 의미있는 텍스트가 없거나, 반복해서 나타나는데 문맥 의존적으로 해석하게되면 완전 노가다. 그리고 내가 아는 범위 내에선 서버 데몬을 따로 띄우지 않고는 안되는 녀석이라 따로 돌리기 불편해서 중도 포기.

다음으론 항상 잘 써먹던, 비교적 규모가 작은 웹사이트에서 잘 써먹던 requests + html5lib. 근데 이것도 UI 요소에 의미있는 클래스나 아이디가 없어서 불편하긴 하더라. 그리고 동적으로 링크를 발견하고, 이걸 다시 특정 형태로 크롤링하게 하려니 작업량이 많아서 작업 중단.

마지막으로 scrapy를 썼고, 이런 류의 작업이 맞춘 툴이어서 이걸로 완성.
다음과 같은 방식으로 동작한다:

1. 시작 url 지정
2. 페이지를 긁고, 이것에 대한 parse 콜백
2-a. HTTP 요청에 대한 응답을 xpath로 뒤져서 적당한 값을 읽기
2-b. 추가로 크롤할 url을 yield. (별도로 파싱할 때 쓸 콜백도 지정)
2-c. 원하는 값을 찾으면 이걸 미리 지정한 아이템 타입으로 yield.
3. 처리할 url이 남으면 2로.

2-b, c를 혼용할 수 있는게 정말 편했다.

더불어, 실재로 작업을 하는 2-a~c만 쓸 수 있는 scrapy shell {URL} 명령이 무진장 유용했음. 해당 url에 대해서 딱 콜백 시작 부분까지 진행한 ipython shell이 뜨더라. 여기서 이거저거 테스트해보고 파싱 코드를 작성하면 되더라.

그리고 작업하다가 깨달음을 하나 얻었는데, 모바일 페이지를 크롤링하는게 몇 배 쉽다. 노가다 좀 하면 requests + html5lib 로도 가능할 듯.

세 줄 요약:

– scrapy가 웹 크롤링 작업의 반복적인 부분을 줄여주고 꽤 편하게 만들어주더라
– scrapy shell {URL or file path} 완전 편함
– 모바일 페이지가 있고, 필요한 정보가 다 나온다면 이쪽을 크롤링하는게 쉽다

gerrit 남은 리뷰 수 시각화하기

회사에서 gerrit 이라는 git 기반의 코드 리뷰 도구를 쓴다.

gerrit 최근 버전들이 RESTful API를 제공하기 시작했는데, 이를 이용해서 각 날짜 별로 남아있는 리뷰 수를 보여주는 스크립트를 작성했다.

예를 들어 android 프로젝트의 하위 프로젝트인 platform/sdk 에 대해서 그려보면 이런 그래프가 나온다.

gerrit-graph

대략 작년 1월부터 오늘까지 남은 리뷰 수 통계다 — 실제론 merge 된 것만 계산해서 최근 통계는 좀 안 맞을 수도 있다.

해당 코드는 https://github.com/reinkim/gerrit-graph 에 올려두었다. 좀 naive하긴하지만 svg로 그리니 그리긴 참 편하다 (…).

git 통계 / jenkins 통계 / gerrit 통계가지고 조금 손보면 적당히 발표할만한 주제를 잡을 수 있을 것 같다.

Python 기반의 static page 생성기로 블로그를 옮기려는 중

원래 내 의도는 최근에 읽은 책 (=”해커를 위한 디자인 레슨”) 과 게임 (=”어쌔신 크리드 2 및 그 확장팩, 3″)에 대한 리뷰를 쓰려는 거였다.
하지만 블로그 관리자 페이지 로딩이 너무 느려서 — WordPress 버전이 올라갈 수록 더 심한듯? — 좀 괴롭다.

(일반 유저가 볼) 페이지 로딩 자체는 큰 차이가 없어서 그냥 쓸 생각이 많았는데 오늘 좀 추가 많이 기운 듯 하다.

그래서 Pelican 이란 도구를 다음과 같은 이유로 시도해봤다: (이 순서대로 고려한 것은 아님)

  • OpenSource
  • History가 길다 (+5yr)
  • 현재도 개발이 진행되고 있다
  • Python 기반
  • Markdown 지원 (rst가 주 언어지만)
  • WordPress import 지원

이걸 가지고 내 블로그를 import 해보니 몇 가지 문제가 있다: (WordPress 가 정의한 xml을 rst로 변환하는 도구가 포함되어 있다)

  • 멀티바이트 인코딩된 블로그 제목이면 rst 의 #### 이 부분을 짧게 계산해서 망한다. (글자 수를 세서 그런가)
  • 일부 글에서 글자를 해석 못해서 망하고 있음
  • 글 제목(정확히는 WordPress의 slug 값)을 이용해서 .rst 를 만드는데 urlencode 당한 문자열을 쓴다 -_-

딱히 다른 대안이 없으면 importer를 고치고 어떻게 해봐야겠다;

2012, 내 프로그래밍 언어는…

작년에 이어, 올해에도 twitter에 적은 #code212에 덧붙여 글 하나 쓰기.

올해 주로 사용한 언어는,

  • Python
  • Bash script
  • C++
  • JavaScript
  • C#
  • Go

정도의 순서인 듯 하다.

작년의 순서와는 상당히 달라졌는데 그 배경은 역시 이직.

Windows 에서 C++ 코드베이스에서 작업하던 환경에서, linux 위에서 python 코드베이스를 작업하니 바뀐 게 많지(…).

 

Python

회사를 옮긴 이후로 C++ 코드는 한 줄도 안 짜고 python을 주 언어로 작업했다.

분산 서비스 (w/ restful interface)와 그 프런트엔드에 해당하는 웹 앱, 그리고 이를 테스트하는 코드들을 다 python으로 구성했다. 올해는 정말 python은 징그럽게(?)만진 듯.

감상을 적자면,

  • 편하다; 사실 이건 작년의 주 언어가 C++이라?
  • 생각을 코드로 표현하는데 부분에 걸리는 시간엔 거의 C++ 수준에 다다를 정도로 쓴 것 같다
  • 상대적으로 “뭘 만들지”에 집중하고 “어떻게 만들지”에는 조금 덜 신경 쓴다

여하튼 python으로 작성한 서비스가 내년 중엔 아마 라이브로 전환 할 듯…

Bash script

Python으로 프로그램을 만들긴 하지만 이걸 실제로 배포하고, 설치하고, 설정하는 부분은 전부 bash script. 특히나 debian packaging하면서 갈수록 bash script 짜는 기술이 늘어가는 것 같다. 어흑

C++

이전 회사에선 주로 C++을 썼다. 그리고 2월 말까지는 C++로 코드 짰음 (사실 3월에 남은 날 수 출근할 때 C++ 코드 디버깅도 했다(…) )

JavaScript (w/ html, css, …)

Python으로 짠 서비스의 프런트엔드는 결국 html + JavaScript + css.

들어가서 바닥부터 만든 한 프런트엔드의 css를 직접 짜며 삽질하다가 bootstrap으로 바꾸고 신세계가 펼쳐졌다 (…).
내가 작성한 JavaScript는 IE에서 안 돌기도 하고 – 나중에 합류한 프런트엔드 엔지니어 분이 해결해 주심 – 하여간 삽질이 Orz.

초반엔 사람이 없어서 짰는데 요즘은 거의 손 댈 일이 없는 듯. 그래도 어제 포스팅 한 일 같은 류를 처리하려면 어느 정도 알긴 해야 할 것 같은데;

C#

작년에 C# 배우면서 짰던 프로그램을 퇴사하기 전까지 유지 보수하다가 나왔음. 지금은 그 회사의 A모 게임과 B모 게임이 쓰고 있을 거라던데 난 모르겠다 (야).

이젠 Windows 환경이 아니라서 더 이상 만질 일은 없을 듯. (작년 감상을 보고 생각해도 이건 잘 된 일?)

Go

Go로 동작하는 서비스를 작성하겠다는 야망을 품었으나… (내년에 계속?)

 

정리:

  • Python 코드베이스를 작성하는 일로 옮겨왔다
  • Linux로 옮겨왔으니 bash 나 기타 등등 만질 일이 늘어남
  • Go로 서비스 작성하고 싶다 (…)
  • 내년엔 C++ 만질 일이 생길지도…

WebApp 안에 다른 WebApp 넣어서 보여주기

최근 몇 일간 이미 만들어진 WebApp A안에 다른 WebApp B이 하나의 페이지처럼 들어가 보이도록 하는 작업을 했다. 사용자는 A에는 접근할 수 있지만, B에는 (대부분) 접근할 수 없는 상태.

정석(?)은 사실 이런 식으로 해야 할 것 같지만:

  • B를 RESTful API 혹은 어떤 web API를 갖는 서비스로 작성하고 (Service B)
  • B의 RESTful API를 보여주는 view에 해당하는 WebApp을 작성하고 (App B)
  • A는 ‘Service B’를 써서 특정 페이지를 만든다

하지만 그렇게 하지는 않고, 여름에 인턴으로 잠시 일했던 puzzlet군이 사용했던 방법을 좀 고쳐서 썼다.

Web B가 html5로 작성한거라서 http proxy 비슷한거 만들기는 편했다.

인증

다행히 이 두 웹앱은 인증 서비스는 공유한다. 그래서 A에서 인증한 shared-secret을 가지고 만든 데이터를 B에게 주고 이걸로 인증하게 했다.
그리고 B에 접속할 떄 사용하는 세션 키를 A의 세션에 저장하고 꺼내서 쓰게 했다.

페이지 접근

A 의 특정 URL 밑에 오는 것은 B의 URL로 접근하게 했다. 다만 일부 접근할 수 없는 URI에 대한 처리는 있다.
이런게 없었다면 그냥 reverse proxy쓰거나 하면 될 듯도.

web_app_A_host/page_B/… 이면 실제론  B의 페이지에 접근하게 했다. 실제론 URI가 완전히 맵핑되는 건 아니고, 특정 변환을 해주긴해야 해서 reverse-proxy를 쓰진 않았다.

HTTP 응답 rewrite

http 응답은 몇 가지 조건을 걸고 rewrite했다.

1.  http 헤더에 Location 필드가 있는 경우

30x나 201, 202인 경우 이게 붙어서 오는데, 이번 경우엔 201/202는 안 써서 그냥 30x에 Location field를 rewrite하게 했다.

2. html 페이지

href, src 등으로 오는 것 전부 rewrite.

그리고 나서 css, js, 실제 내용에 해당하는 부분을 찾았다. 그리곤 다음과 같이 합쳐서 내보냈다. html5lib이 xpath 로 DOM에 접근할 수 있어서 엄청 간단히 했다.

rewrite-html

3. css 추가 처리 

CSS의 경우 골치 아픈 문제가 생기더라. B에서 온 CSS 셀렉터에 해당하는게 A 안에도 있어서 레이아웃까지 깨지고 있었다. Orz.

그래서 위에서 한 것처럼 특정 class 밑에 B의 내용이 오게 강제하고, B에서 오는 모든 css 에는 셀렉터마다 (…) 맨 앞에 저 embedded가 붙게 했다.
cssutils로 해석한 다음 각 셀렉터를 하나하나 업데이트하는 코드를 짰다.

약간 느려지긴 하던데 어차피 저건 정적으로 캐싱할 수 있으니까 신경 끄기로 (야).

 

결론(?)

  • http 요청하는 클라이언트 흉내내기는 재밌더라
  • CSS 때문에 귀찮아…
  • B가 사실 상 페이지 1개짜리 앱이니 그냥 했지, 안 그랬으면 RESTful API로 서비스 분리하는게 훨씬 속 편할 것 같다.

django 기반 웹 사이트 배포 방법?

django 기반의 웹 사이트를 배포해야 하는데, 타겟 머신이 확정되지 않은 상태에서도 설치할 파일들은 저장할 수 있어야하고 + 설치 시기도 적당히 제어할 수 있어야 하는 상황.

일단 다음과 같은 가정으로 설치하게 했다:

  1. (Dev) django app은 데비안 패키지(.deb)로 묶는다
  2. (Deploy) .deb은 chroot-jailed sftp에 전송
  3. (Deploy) 올린 애를 apt 저장소 형식으로 내보냄
  4. (Service front-end) 이걸 자동으로 받거나, 원격 스크립트, 혹은 로컬에서 직접 설치
  5. (Service front-end) 설치 작업 자체는 .deb의 postinst 를 이용한다:
    • 사이트 실제 코드는 .deb으로 포장된 python package 형식으로 설치
    • 사이트에서 읽을 일부 스태틱 데이터도 이 .deb에 포함.
    • 매우 간단한 wsgi application 정의하는 파일과 django 설정 파일을 특정 디렉터리에 복사하고
    • 기본 설정 파일(=django가 읽을 settings module)을 서버 정보를 이용해서 sed로 수정
    • static file 들은 적당한 mount 지점에 가도록 symlink 추가
    • 처음에 설치한 디렉터리를 이용해서 띄우는 설정을 /etc/apache/available에 추가하고 apache 재시작

…를 하는 상황.

근데 바꿀 수 있는 부분이 대략 이렇다:

  • django: 하지만 python 기반 (뭐 wsgi 기반의…) 이어야 함.  ((Go로 만들고 싶은 생각이 약간 있긴 하지만 그건 근 미래에는 힘들 듯))
  • apache: 맘 편하게 gunicorn + nginx reverse proxy 같은 고전적인 조합을 생각하는 중
  • .deb 전송 방식으로 sftp[1] : shell 권한을 주기 곤란해서 고른 것인데, 그냥 http put 으로 넣는게 더 나을지도?

 

django 기반의 사이트를 배포해 보신 분들의 의견을 구합니다. 아는 게 없는 동네라 뭔가 엄한 -_- 조합을 만들었다 싶기도 하고.

혹은 다른 WSGI 프레임웍을 썼을 때 좀 더 배포가 편했다거나 — flask나 cherrypy만 써도 이보다 간단할 것 같은데… — 하는 경험 있으면 들려주세요. 흑흑
상식적인(?) 방법은 fabric이나 puppet으로 배포하는 쪽인 거 같긴하지만; (혹은 chef?)

  1. chroot jail이라 upload 디렉터리만 보이는 정도로 []

NDC 참관기: 실시간 HTTP 양방향 통신

수요일(2012-04-25) 오후에 있었던 이승재 군의 발표.

발표 자료는 http://ricanet.com/new/view.php?id=blog/120425 에서.

HTTP Protocol의 한계

http는 request/response protocol이다. 이로 인해서 클라이언트 쪽의 이벤트는
서버 쪽에 쉽게 전달할 수 있지만, 서버 쪽의 이벤트는 클라이언트가 요청하기
전에는 전달할 수 없다.

그런데 현재의 웹 쪽 트렌드를 보자면, twitter나 facebook, google mail (or talk)
같은 경우 실시간으로 메시지를 보낸다. 어떻게 하는걸까?

이를 극복하기 위한 기법들

기본적으로 꼼수다 — 프로토콜의 한계 때문에.

Polling

웹 브라우저에서 주기적으로 요청을 보낸다. (클라이언트 쪽 타이머 이용)
문제:

  • 트래픽이 많다 / 서버 쪽 부하가 걸린다
  • 지연 시간이 크다; 이벤트-클라이언트 요청 시간 차이 때문에. 그렇다고지연 시간을 줄이면 트래픽/서버 부하가 더 커진다.

Long-polling

클라이언트가 미리 요청을 보내고, 서버는 이벤트가 생기는 순간까지 기다렸다가
이를 모아서 (이미 온 요청에) 응답을 보낸다.
Facebook 메신저 구현이 이를 이용한다.
문제:

  • 구현 복잡도. 서버가 요청이 이미 와 있는 경우와 그렇지 않은 경우를 구분해서 처리해야하는 등.
  • 지연 시간; 응답을 보낸 후 클라이언트가 다시 요청을 보내올 때까지의 시차.

Hidden iframe

페이지에 숨겨 놓은 iframe을 통해서 메시지를 계속 흘려 보낸다. (서버가 응답을
끝내지 않는다) 그래서 http streaming이라고 부르기도 한다.
Gmail messenger(=google talk)에서 사용하는 방법이다.

문제:

  • 응답이 끝나지 않기 때문에 클라이언트 쪽 웹 브라우저에 메모리 릭이 생긴다.
  • 프록시 / 보안 프로그램이 응답이 완전히 끝나야만 이를 넘겨주는 경우가 있다.

Browser plugin

Flash, ActiveX 등을 이용. 사실 상 TCP 소켓처럼 쓸 수 있다.
호환성 문제가 너무 커서…

WebSocket (html5)

javascript 단에서 진짜 소켓을 쓸 수 있다. 표준화가 아직이라 (protocol은
결정되었지만, API가 아직 unstable).
기존 웹브라우저와의 호환성 문제.

WebIRC (=IRC web-bridge for smartphones) 만들기

Long-polling이 호환성이 좋고 (최소한 iOS, android를 지원해야 했음),
구현이 재밌어 보여서(…) 이를 구현함.

프로토콜

  • InitLog: 각 채널 최근 로그 k 줄을 요청
  • UpdateLog(last\[\]): 각 채널에서내가 마지막으로 받은 로그 ID가 last일 때 그 이후 로그를 요청

서버 구현 이슈

HTTP 핸들러가 새로운 내용이 없으면 대기하는 것.
vs.
Event listener가 새로운 내용이 왔을 때 대기 중인 스레드(의 요청)를 깨우는 것
사이의 레이스 컨디션.[1]

node.js 류를 사용하면 오히려 쉬웠을지도 모르지만 이미 python으로 구현.

Webserver Architecture

동시에 살아있는 연결이 많아서,

  • 커넥션 당 1 스레드 때문에 메모리 병목
  • 고전적인 웹 서버의 경우 더 중요한 페이지 뷰보다 동시 접속 수가 문제
  • Gevent로 쉽게 해결
  • Long-polling에 맞춰서 구현했더니 stateless 프로토콜이 되어 네트워크 단절에 강해졌다.
  • 채팅 보내는 것이 네트워크 불안정엔 약했음. 이걸 극복하려니 사실상 TCP 류의 프로토콜을 HTTP 위에 재구현하는 형태가 되어버리게 되서…

Middlewares

앞에서 언급한 방법들엔 정답이 없다. 각 방법의 장단점이 다름.

orbited2, socket.io 같은 미들웨어가 있다.
그렇지만 정작 제작 시점에 사용하려던 orbited 사이트가 내려가 있어서
long-polling을 직접 구현.

Orbited

orbited.js + *application.js* — orbited server — application server

위의 형태로 구성. Web-router + firewall의 역할. Legacy TCP 응용프로그램
서버인 경우에도 쉽게 통신할 수 있다.

socket.io

nopde.js 기반. 메시지 단위 전송을 지원한다.

확인해야 할 사항

  • 연결 당 서버 리소스 소모량
  • 연결이 불안정하면 어떻게 되는가?
  • 웹 브라우저를 껐다켜도 괜찮은가?

웹-게임 연동

이런 기술을 쓰면 웹 게임에서도 네트워크 제약(http 문제)이 *거의* 없어진다.

WoW 전정실, 아이온, 마비의 거래소, 넥슨 홈이나 스팀의 업적 시스템 구현이라거나,
게임 웹 채팅, 친구 게임 참여 알리기, 우편 도착 알림 등의 실시간 기능이 가능해질듯.

결론(?): 취미 프로젝트 좋아요(…)

  1. 내 생각, 그리고 최치선 군의 의견을 조합하자면, 저건 pthread condvar 류의 동기화 도구를 사용하면 해결 될듯 함. 치선 군이 실 서비스에서 사용하고 있는 추천 구현체는 gevent의 Queue (monkey-patching하는). []