Doge log

Abby CTO 雑賀 力王のオフィシャルサイトです

Docker本を書いたという話

お久しぶりです。僕です。

今回、いろいろとご縁がありまして、Docker本を書かせて頂きました。

一応どんな人が書いてるかというと以下を見てもらうといいかも知れません。ストック数 1位なのが私です。

dockerに関する164件の投稿 - Qiita

今回執筆の機会を与えてくださった技術評論社様および共著者の@yone098に感謝致します。

(95%私が書いてますけど)

夜な夜な原稿見直しをしてるとDockerのサイトがリニューアルして焦ったり、いろいろとあったのですがなんとか出す事ができました。

タイトルは入門となっていますが、ひと通りの事は書いてあるのでリファレンスに近い部分もあるかも知れません。

また、内容ですが、最新の Docker 0.10 を元に記述しています。

興味のある方、Dockerを触ってみたいという方は是非読んでみて下さい。

gihyo

https://gihyo.jp/dp/ebook/2014/978-4-7741-6504-2

Amazon Kindle

http://www.amazon.co.jp/o/asin/B00JWM4W2E/

また弊社ではエンジニアを募集しています。

気になる方は、TwitterなりFacebookなりで私、@yone098に声をかけてください。

さて、次は CoreOS か ProjectAtomic いくか…

Clojure (Java) はやはり速かったという話

Clojure (Java) はやはり速かったという話

あまりにも遅すぎなのでは?と思ったので調べたらやはり計測方法に問題があっ たみたい。

Java がこんなに遅いわけない。

遅かった原因

いつも通り nrepl 経由で適当に実行していたのが原因。

leiningen から nrepl を立ち上げていたのだが、以下を見れば遅い理由がわかる。

java -client -Xbootclasspath/a:/home/ma2/.lein/self-installs/leiningen-2.3.3-standalone.jar -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Dfile.encoding=UTF-8 -Dmaven.wagon.http.ssl.easy=false -Dleiningen.original.pwd=/home/ma2 -Dleiningen.script=/home/ma2/bin/lein -classpath /home/ma2/.clojure:/home/ma2/.lein/self-installs/leiningen-2.3.3-standalone.jar clojure.main -m leiningen.core.main repl :headless :port 60000

-client オプション久々に見た!

leiningen は前から起動がクソ遅いと文句を言われていたので jvm のオプションを指定している。

起動速度をあげるために以下のオプションで起動している。

  • -client

  • -XX:+TieredCompilation

  • -XX:TieredStopAtLevel=1

oh …

実行速度が遅くて当たり前である。

lein スクリプト内

...

export LEIN_JVM_OPTS="${LEIN_JVM_OPTS-"-XX:+TieredCompilation -XX:TieredStopAtLevel=1"}"

...

export TRAMPOLINE_FILE
"$LEIN_JAVA_CMD" -client \
    "${BOOTCLASSPATH[@]}" \
    $LEIN_JVM_OPTS \
    -Dfile.encoding=UTF-8 \
    -Dmaven.wagon.http.ssl.easy=false \
    -Dleiningen.original.pwd="$ORIGINAL_PWD" \
    -Dleiningen.script="$SCRIPT" \
    -classpath "$CLASSPATH" \

がっつり固定で書かれている…

grench から起動すればこんなの気にしなくていいので -server モードで起動するようにすべきである。

server モード で計測

lein スクリプトを修正して以下のオプションで立ちあげてみる。

java -server -Xbootclasspath/a:/home/ma2/.lein/self-installs/leiningen-2.3.3-standalone.jar -Xms512m -Xmx1024m -Dfile.encoding=UTF-8 -Dmaven.wagon.http.ssl.easy=false -Dleiningen.original.pwd=/home/ma2 -Dleiningen.script=/home/ma2/bin/lein -classpath /home/ma2/.clojure:/home/ma2/.lein/self-installs/leiningen-2.3.3-standalone.jar clojure.main -m leiningen.core.main repl :headless :port 60000

計測してみよう。

λ grench eval '(time (reduce unchecked-add (map unchecked-add (range 1000000) (range 1000000))))'
"Elapsed time: 366.023913 msecs"
999999000000

ほぼ Python と変わらないくらいになった。

僕のおんぼろ X200s だとこれぐらいですが、会社の i7 搭載マシンだと Python より速くなります。

まとめ

パフォーマンスを計測する場合、leiningen 使うとダメです! nrepl 経由で楽せずちゃんと測りましょう!

Clojure が遅いという話

Clojure が遅いという話

遅いケースもあるという認識も持っておいてもらおう。 比較のために今回はPythonと比べてみる。

Clojure

(dotimes [i 5]
  (time
   (reduce unchecked-add (map unchecked-add (range 1000000) (range 1000000)))))

Python

import timeit

def f (x):
    return reduce(lambda x, y: x + y, (map (lambda x, y: x + y, xrange(1, x), xrange(1, x))))

r = timeit.timeit("f(1000000)", "from __main__ import f",  number = 1)

print(r*1000)

結果

両者、かなり似たコードになっているが結果はどうか?

Clojure は初回どうしても遅くなるので 5 回計測。

Clojure

"Elapsed time: 936.786616 msecs"
"Elapsed time: 922.82911 msecs"
"Elapsed time: 923.171342 msecs"
"Elapsed time: 931.944958 msecs"
"Elapsed time: 935.293865 msecs"

Python

352.871894836

よく unchecked-xxx 使えみたいな話がありますが、使ってもなお遅いということを覚えておいた方がよさそうですね。

Leiningen と profile を使って設定ファイルを切り替える

Leiningen と profile を使って設定ファイルを切り替える

Leiningen には profile を切り替える機能がある。 profile を切り替えることで本番用の設定と切り替えたりできるのだが、具体例が少ないように思えたので書いておく。

開発時に使える dev-resources

ドキュメントに出ているのかよくわからないがデフォルトの profile では dev-resources が使用出来る。

デフォルトの確認

pprint で見てみる。

    $ lein pprint

結果:

    {:compile-path
     "/home/ma2/lib/codebox/clojure/projects/prof/target/classes",
     :group "prof",
     :license
     {:name "Eclipse Public License",
      :url "http://www.eclipse.org/legal/epl-v10.html"},
     :global-vars {},
     :checkout-deps-shares
     [:source-paths
      :test-paths
      :resource-paths
      :compile-path
      #<Var@77b52d12: 
        #<classpath$checkout_deps_paths leiningen.core.classpath$checkout_deps_paths@47f6473>>],
     :dependencies
     ([org.clojure/clojure "1.5.1"]
      [sonian/carica "1.0.3" :exclusions ([cheshire/cheshire])]
      [org.clojure/tools.nrepl "0.2.3" :exclusions ([org.clojure/clojure])]
      [clojure-complete/clojure-complete
       "0.2.3"
       :exclusions
       ([org.clojure/clojure])]),
     :plugin-repositories
     [["central" {:snapshots false, :url "http://repo1.maven.org/maven2/"}]
      ["clojars" {:url "https://clojars.org/repo/"}]],
     :test-selectors {:default (constantly true)},
     :target-path "/home/ma2/lib/codebox/clojure/projects/prof/target",
     :name "prof",
     :deploy-repositories
     [["clojars"
       {:username :gpg,
        :url "https://clojars.org/repo/",
        :password :gpg}]],
     :root "/home/ma2/lib/codebox/clojure/projects/prof",
     :offline? false,
     :source-paths ("/home/ma2/lib/codebox/clojure/projects/prof/src"),
     :certificates ["clojars.pem"],
     :version "0.1.0-SNAPSHOT",
     :jar-exclusions [#"^\."],
     :prep-tasks ["javac" "compile"],
     :url "http://example.com/FIXME",
     :repositories
     [["central" {:snapshots false, :url "http://repo1.maven.org/maven2/"}]
      ["clojars" {:url "https://clojars.org/repo/"}]],
     :resource-paths
     ("/home/ma2/lib/codebox/clojure/projects/prof/dev-resources"
      "/home/ma2/lib/codebox/clojure/projects/prof/resources"),
     :aot :all,
     :uberjar-exclusions [#"(?i)^META-INF/[^/]*\.(SF|RSA|DSA)$"],
     :main prof.core,
     :jvm-opts ["-XX:+TieredCompilation" "-XX:TieredStopAtLevel=1"],
     :eval-in :subprocess,
     :plugins
     ([lein-pprint/lein-pprint "1.1.1"]
      [lein-environ/lein-environ "0.4.0"]
      [lein-midje/lein-midje "3.0.0"]
      [lein-ancient/lein-ancient "0.4.4"]
      [lein-ritz/lein-ritz "0.7.0"]),
     :native-path
     "/home/ma2/lib/codebox/clojure/projects/prof/target/native",
     :description "FIXME: write description",
     :test-paths ("/home/ma2/lib/codebox/clojure/projects/prof/test"),
     :clean-targets [:target-path],
     :aliases nil}

確かに resource-paths の先頭に dev-resources が追加されている。 dev-resources はデフォルトの leiningen のテンプレートでは作成されないので自分で用意する必要がある。

実際に切り替えてみよう

実際に切り替えてみよう。 構成は

    .
    ├── LICENSE
    ├── README.md
    ├── dev-resources
    │   └── config.clj
    ├── doc
    │   └── intro.md
    ├── project.clj
    ├── resources
    │   ├── config.clj
    │   └── test.properties
    ├── src
    │   └── prof
    │       └── core.clj
    └── test
        └── prof
            └── core_test.clj

dev-resources/config.clj :

    {:name "TEST"}

resources/config.clj :

    {:name "production"}

main 関数で表示してみる。

    (ns prof.core
      (:gen-class)
      (:use
       [carica.core]))
    
    (defn -main [& args]
      (println (config :name)))

設定を carica で読み込んでどちらが表示されるか確認する。

    $ lein run
    Compiling prof.core
    TEST

デフォルトでは開発リソースである dev-resources の設定が読み込まれてい る。 他のプロファイルに切り替えてみる。

    $ lein with-profile production run
    Performing task 'run' with profile(s): 'production'
    production

デフォルトの profile 以外では dev-resources ではなく resources が優先 になっている。 結果、開発時には dev-resources を使えばよい。 実際には何も指定してない場合には :base プロファイルが使われている。 dev-resources は :base プロファイルで設定されているのでそのまま使用できる。

uberjar

配布時にはどうなるのか? uberjar で試してみる。

    $ lein uberjar
    Created /home/ma2/lib/codebox/clojure/projects/prof/target/prof-0.1.0-SNAPSHOT.jar
    Created /home/ma2/lib/codebox/clojure/projects/prof/target/prof-0.1.0-SNAPSHOT-standalone.jar
    $ java -jar target/prof-0.1.0-SNAPSHOT-standalone.jar
    production

uberjar の場合は :uberjar プロファイルがデフォルトで使われるので dev-resources は有効にならないようだ。 開発版を配布したい場合には明示的に指定することで対応できる。 :base プロファイルを追加してみる。

    $lein with-profile +base uberjar
    Performing task 'uberjar' with profile(s): 'default,base'
    Warning: skipped duplicate file: config.clj
    Created /home/ma2/lib/codebox/clojure/projects/prof/target/prof-0.1.0-SNAPSHOT.jar
    Created /home/ma2/lib/codebox/clojure/projects/prof/target/prof-0.1.0-SNAPSHOT-standalone.jar
    $ java -jar target/prof-0.1.0-SNAPSHOT-standalone.jar
    TEST

無事開発版の設定ファイルが読み込まれている。

Jegaをリリースした話

先日、やっとリリースしました。

tobikko時代から考えると数年がかりいじってた気がします。

Jegaについて

concurrent networking and cooperative multitasking library for Python3. わかりやすく言うとgeventの後継ライブラリになります。 Python3.x系しかサポートしていません。

URL:

https://github.com/mopemope/jega

特徴

Jegaはgevent、evergreen, PEP3156を参考に作られています。

機能的なところはほぼevergreenと同様です。 主な特徴は以下です。

  • picoevベースの高速なイベントループ
  • greenletベースの協調スレッド
  • c-aresよるDNS lookupの非同期化
  • 非同期処理はFutureにより管理(専用のconcurrent.futuresの実装)
  • 協調スレッド間でコミュニケーションを取るためのchannel
  • socketなどを非同期処理に置き換えるpatch
  • Cによる高速化

最近の流れとしては非同期処理から結果を取り出す際にFutureを使うのが主流になっています。

またevergreen, PEP3156などではイベントループを意識し、イベントループを明示的に走らせないといけ ないのですが、Jegaは限定的ですが裏側でイベントループが自動的に走るようになっています。 Futureからの結果の取得、patch部コードは裏で自動でイベントループが走り処理をしてくれます。 loop.until_run_completeとかいう長いメソッドを呼ばなくもよいので楽です。

またevergreenもそうなんですが、最近はgoを意識してるのかchannelをサポートするという流 れになっています。なので合わせてchannelも実装しています。

本来はPEP3156に合わせたかったのですが、あの仕様だと問題があって

  • 高速化が難しい
  • loopを意識するのが面倒 というのがあって採用していません。

loopのメソッドの一部はPEP3156に合わせてはいますが。

例: task実行

import jega
import functools
size = 1024

def _call(a):
    return a
futures = []
for i in range(size):
    f = jega.spawn(_call, i)
    futures.append(f)
r = functools.reduce(lambda x, y: x + y.result(), futures, 0)
r2 = functools.reduce(lambda x, y: x + y, range(size), 0)
assert(r == r2)
del futures
print("OK")
jega.get_event_loop().stop()

シンプルなtaskなどはお決まりのパターンでspawnで起こします。 返ってくるのがFutureなのでresultメソッドで値を取得します。

例: channel

import jega

loop = jega.get_event_loop()
c = loop.make_channel()

def _send(chan):
    print("** start send")
    a = 1
    chan.send(a)
    print("** sended:%s" % a)
    return a


def _recv(chan):
    print("** start receive")
    r = chan.recv()
    # r = 1
    print("** received:%s" % r)
    return r

f1 = jega.spawn(_send, c)
f2 = jega.spawn(_recv, c)

assert(f1.result() == f2.result())

channelもシンプルにsend, recvでやり取りします。

その他にサーバーなどを書く際のヘルパーがあります。

例: echo server

import jega
from jega import patch
patch.patch_socket()
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_DEFER_ACCEPT, 1)
s.bind(("", 5000))
s.listen(1024)
loop = jega.get_event_loop()

def echo(sock, addr):
    try:
        recv = sock.recv
        send = sock.send
        while 1:
            buf = recv(4096)
            if not buf:
                return
            send(buf)
    finally:
        sock.close()

loop.set_socket_acceptor(s, echo)
loop.run()

set_socket_acceptorにlistenしてるsocketとacceptした結果を受け取るハンドラーをセットします。 ハンドラーはaccept毎に独立したgreenlet上で実行されます。

高速化

基本的に頻繁に使われるであろう箇所はCで書かれています。

  • イベントループ
  • 協調スレッドのExecutor
  • 協調スレッド間で使用するlock、channelなどの制御モジュール

Pythonはチマチマバイトコードを実行するので関数内のコード量が多いとそれなりの時間がかかり ます。

Cで書かれている箇所はFrameオブジェクトを生成しないのでそれだけでも高速化につながります。 また、関数(メソッド)や属性値も展開済みの形で実装されているので高速に動作します。

特に協調スレッドのスケジューリング部はevergreenに比べてもかなり高速に動作します。

デバッグ

Jegaはビルド時に環境変数を参照し、DEBUGマクロを有効化します。

export JEGA_DEVELOP=1

とセットしてからビルド、インストールすると実行時にDEBUGログが出ます。

例:DEBUGログ

src/loop.c                   init_loop_module                    2284: switch_value:0x7f330ee42040
src/loop.c                   init_loop_module                    2285: loop_switch_value:0x7f330f2d75a0
src/loop.c                   LoopObject_init                     1113: self:0x7f330e0fcea0
src/loop.c                   init_main_loop                       193: self:0x7f330e0fcea0
src/loop.c                   LoopObject_init                     1136: self:0x7f330e0fcea0 pendings:0x7f3312468628
src/loop.c                   LoopObject_add_reader               1629: self:0x7f330e0fcea0
src/loop.c                   LoopObject_add_reader               1631: args size 2
src/loop.c                   make_callback_arg                   1208: alloc callback_arg_t:0x7f330f28ecc0
src/loop.c                   call_fork_loop                       235: self:0x7f330e0fcea0
src/loop.c                   register_fd_callback                1344: add event loop:0x16f9a00 fd:5 event:1 timeout:0
src/executor.c               ExecutorObject_init                  126: self:0x7f330e0f7308

DEBUGしない場合には0をセットして下さい。

おまけ greenletの問題

Jegaを作る前にtobikkoというgeventコンパチなPython3系で動作するライブラリを作成していました。

tobikkoはアグレッシブにsocket moduleの再実装を行なっていたのですが、そこで問題が発生しました。 greenletはC-APIも提供しておりCからも使用することができます。

greenletはCスタックのコピー/リストアを行うことによって協調スレッドを実現しています。 問題はCスタックで、通常の値ならば問題ないんですが、積まれてる値がポインタのポインタである場 合、スタックをコピーした時と、リストアした時で実際のアドレスに入っているものがズレてしまいます。 特にCPythonはメモリープールを持っているのでアドレスに別のオブジェクトが入っていたりすることが 多々あります。

例:

static ssize_t
internal_recvfrom(SocketObject *self, char *buf, int bufsize, int flags, PyObject** addrobj)
{
    struct sockaddr addr;
    ssize_t r = -1;
    socklen_t addrlen;

    *addrobj = NULL;

    if(getsockaddrlen(self->family, &addrlen) == -1){ 
        return -1;
    }    

    memset(&addr, 0, addrlen);
    DEBUG("addrsize fd %d: addrlen:%d", self->fd, addrlen);

    while(1){
        Py_BEGIN_ALLOW_THREADS
        r = recvfrom(self->fd, buf, bufsize, flags, &addr, &addrlen);
        Py_END_ALLOW_THREADS

        if(r < 0){
            if(errno == EAGAIN || errno == EWOULDBLOCK) {
                DEBUG("errno:%d", errno);
                //ここでgreenletにスイッチ
                if(io_trampoline(self->fd, PICOEV_READ, self->timeout, socket_timeout) < 0){
                    return -1;
                }
                //戻ってくると&addrがズレる
            }else{
                PyErr_SetFromErrno(socket_error);
                return -1;
            }    
        }else{
            DEBUG("read %d: addrlen:%d", (int)r, (int)addrlen);
            if(!(*addrobj = getaddrtuple(&addr, addrlen))){
                return -1;
            }
            //不正な処理になる

            return r;
        }    

Common LispにThreaded Macroを実装する

PythonからいいところをひとつもってこれたのでClojureからももってくる。
Clojureでは数珠つなぎで処理する場所はわかりやすくThreaded Macroを使って書くことが多い。
これはわかりやすいのでCommon Lispでも実装しておく。

(defmacro -> (x &optional form &rest more)
  (cond ((not (null more))
         `(-> (-> ,x ,form) ,@more))
        ((not (null form))
         (if (listp form)
             `(,(first form) ,x ,@(rest form))
           (list form x)))
        (t x)))

(defmacro ->> (x form &rest more)
  (cond ((not (null more))
         `(->> (->> ,x ,form) ,@more))
        (t (if (listp form)
               `(,(first form) ,@(rest form) ,x)
             (list form x)))))

適当に例をあげる

(-> 25 sqrt (* 2) (- 3))

結果は

7.0

Clackなどのmiddlewareを書く際にもThreaded Macroの方がわかりやすいんじゃないかなと思う。
(ringのMiddlewareはThreaded Macroを使って書く事が多い)

これでまたひとつClojureにあるライブラリを移植しやすくなった。

Common LispでPythonのgeneratorを実装する

結局Lisp書かないとダメだということで本格的にPythonから移行しようと思ってます。
まず、今使ってるツールなどをLispに置き換えようと思ったらまあgeneratorが無くてめんどいことに。
というわけでgeneratorを実装しておく。
この手はみんな実装してるので珍しくもなんともない。
定番のcl-contを使う。

(require 'cl-cont)

(defun mkstr (&rest args)
  (with-output-to-string (s)
    (dolist (a args) (princ a s))))

(defun symb (&rest args)
  (values (intern (apply #'mkstr args))))

(defun flatten (x)
  (labels ((rec (x acc)
             (cond ((null x) acc)
                   ((atom x) (cons x acc))
                   (t (rec
                       (car x)
                       (rec (cdr x) acc))))))
    (rec x nil)))

(defun g!-symbol-p (s)
  (if (symbolp s)
      (let ((str (symbol-name s)))
        (string= str "#" :start1 (1- (length str))))))

(defun o!-symbol-p (s)
  (if (symbolp s)
      (let ((str (symbol-name s)))
        (string= str "%" :start1 (1- (length str))))))

(defun o!-symbol-to-g!-symbol (s)
  (let ((str (symbol-name s)))
    (symb (subseq str 0 (1- (length str)))
          "#")))

(defmacro defmacro/g! (name args &body body)
  (let ((symbs (remove-duplicates
                (remove-if-not #'g!-symbol-p
                               (flatten body)))))
    `(defmacro ,name ,args
       (let ,(mapcar
              (lambda (s)
                `(,s (gensym ,(subseq
                               (symbol-name s)
                               2))))
              symbs)
         ,@body))))

(defmacro defmacro* (name args &body body)
  (let* ((os (remove-if-not #'o!-symbol-p args))
         (gs (mapcar #'o!-symbol-to-g!-symbol os)))
    `(defmacro/g! ,name ,args
       `(let ,(mapcar #'list (list ,@gs) (list ,@os))
          ,(progn ,@body)))))

(defmacro* make-generator (&body body)
  `(let (,cont#)
     (cl-cont:with-call/cc
       (labels ((,(intern "YIELD") (&rest values)
                  (cl-cont:let/cc k
                    (setf ,cont# k)
                    (apply #'values values)
                    )))
         (,(intern "YIELD") (lambda () (cl-cont:call/cc ,cont#)))
         ,@body
         (loop (,(intern "YIELD") :done))))))

(defmacro* defgenerator (name args &body body)
  `(defun ,name ,args
     (make-generator ,@body)))

(declaim (inline next))
(defun next (g)
  (funcall g))

(defmacro* nif (expr pos zero neg)
  `(let ((,g# ,expr))
       (cond ((plusp ,g#) ,pos)
             ((zerop ,g#) ,zero)
             (t ,neg))))

(defmacro* alambda (args &body body)
  `(labels ((self ,args ,@body))
     #'self))

まあよく使うutilとかもまるっと書いてあるけど。 make-generatorだけみればOK。
generatorが作れたのでPythonのitertoolsも多分実装できます。
itertools.countはこんな感じ。

(defgenerator counter (n)
  (funcall (alambda (n)
             (yield n)
             (self (incf n))) n))


(setf foo (counter 10))
(loop repeat 50 collect (next foo))

結果

(10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
  34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
   58 59)

足りない機能を実装できるのでLispはやはり強力だなあと思う