2017年12月13日

最近のPython-dev(2017-12)

はてなブックマークに登録

バックナンバー: 9月号 | 8月号 | 6月号 | 5月号 | 4月号 | 3月号 | 2月号 | 1月号

@methane です。 ISUCON があってしばらく間が空いてしまいました。コミットやML上の議論も追えてないのですが、1月末にPython 3.7のbeta1 (=feature freeze)が予定されているために、Python 3.7 を目標にしている PEP たちがたくさんacceptされたので、それらを紹介しておきます。

PEP 540 UTF-8 mode

https://www.python.org/dev/peps/pep-0540/

PEP 538 (locale coercion) とセットで、私が BDFL-delegate (PEP を accept する責任者) になった PEP です。

この PEP は当初はかなりのボリュームが有ったのですが、すでに PEP 538 を accept したので、それを補完する機能として大幅にシンプル化しました。

PEP 538 では、起動時に locale が C であったときに、 LC_CTYPE を C.UTF-8 などへの変更を試みます。 また、C locale では標準入出力のエラーハンドラが surrogateescape になり、例えば stdin から読んだ文字列をそのまま stdout に書く場合などに非ASCII文字に対してもバイト透過な振る舞いをするのですが、それを C.UTF-8 などの coercion ターゲットとなる locale にも適用します。

PEP 540 も、 locale を変更しない以外は全く同じ振る舞いをするようになりました。具体的には次のとおりです。

  • stdin, stdout の encoding/error handler が UTF-8/surrogateescape になる
  • sys.getfilesystemencoding() と locale.getpreferredencoding() が UTF-8 を返す

PEP 538 と違って実際の locale は変更されないので、例えば readline で日本語入力はできないままですが、C locale 以外存在しないコンテナ等で Python を動かすときにデフォルトでUTF-8を使ってほしいというような用途にはこれで十分です。

PEP 563 Postponed Evaluation of Annotations

https://www.python.org/dev/peps/pep-0563/

from __future__ import annotations を書くことで、関数アノテーションが評価されず、ただの文字列になります。とはいえ、ソースを読むときに構文としてはチェックされるので、任意の文字列がかけるわけではありません。

これにより、アノテーションを書くことによる性能のオーバーヘッドを減らす効果があるのと、アノテーション部分の名前解決のための forward references が不要になって書くのが楽になるという効果があります。

この動作は Python 4 からデフォルトになる予定なので、 Python 3.7 に移行した人は早めにこの動作を有効にすることをおすすめします。

個人的には、実行時に評価されなくなることで、Python の構文を実行時には許されない形で利用したり、あるいはアノテーション部分でしか利用できない構文を追加するという進化への道が開けたという点でも期待しています。例えば現在 Union[int, str] と書いている部分を int or str あるいは int | str と書けるようにする提案ができるかもしれません。(前者は評価するとただの int になり、後者は評価すると | が処理できずに TypeError になる)

PEP 560 Core support for typing module and generic types

https://www.python.org/dev/peps/pep-0560/

いままで type hint は Python 3 で追加された関数アノテーション以外には特別な Python に対する機能追加を必要としないように設計されてきましたが、 typing がある程度の成功を収めて来ているので、そろそろ typing の問題を解決するために Python 自体に手を入れてもいれようというのがこの PEP です。

例えば、 typing.Listclass List(list, MutableSequence[T], extra=list): ... として宣言されています。 この MutableSequence[T] の部分ですが、親クラスになるためにクラスでないといけないという制限があります。そのために実際に親クラスになってしまうので実際にメソッドを提供していないクラスが大量にMROに入りメソッド呼び出し性能のオーバーヘッドが大きくなるという問題があります。また、 MutableSequence 自体もクラスなので、それに対して [T] と書けるようにするためにメタクラスが使われています。

このために現在の typing は大量のメタクラスハックを必要とし、実行時オーバーヘッドもかかり、 import typing も遅くなり、また他のメタクラスとの衝突解決の手間が発生するという欠点を背負っています。

これを解決するために、 Python に次の機能を追加します。

  • class 文の親クラスリスト部分に、 type オブジェクトではない __mro_entries__ メソッドを持つオブジェクトを書くことができる
  • __class_getitem__ メソッドを定義すると、メタクラスを使わなくても MyClass[int] のようにクラスに添え字を書くことができる

これらの機能は typing モジュール以外から使えないというわけではありませんが、 typing 以外の用途での利用は非推奨になっています。

とはいえ、 __class_getitem__ については、最近のメタクラスを使わなくてもクラスの振る舞いをカスタマイズできる流れに添っていて黒魔術感も比較的少なめなので、本当にクラスオブジェクトに添え字アクセスが必要な場面であれば、typing 以外で使っても良いんじゃないかな。

PEP 561 -- Distributing and Packaging Type Information

https://www.python.org/dev/peps/pep-0561/

Typing が本格的に使われていくためには、サードパーティーライブラリの型情報をどうやって配布・利用するかを決めなければなりません。ということでそれを決めたのがこの PEP です。

PEP 562 -- Module getattr and dir

https://www.python.org/dev/peps/pep-0562/

モジュールに __getattr__ 関数を定義して遅延ロードや利用時warningなどを実現する仕組みです。

また、遅延ロードされる名前を提供するために __dir__ を利用することもできます。

個人的には、 import asyncio で芋づる式に multiprocessing まで import されているのを、 concurrent.futures.ProcessPoolExecutor を遅延ロードすることで解消したいと思っています。

PEP 565 -- Show DeprecationWarning in main

https://www.python.org/dev/peps/pep-0565/

Python には廃止予定のAPIについて警告するための DeprecatedWarning がありますが、これはPython製アプリケーションのユーザーにとってはほぼ無意味で混乱させるものなので、現在はデフォルトで表示されないようになっています。

しかし、 Python 開発者でもこの警告を有効にしていない人が多いために、DeprecationWarning に気づかれないという悩ましい状況も発生しています。

PEP 565 はこのバランスを少しだけ調整する提案です。 __main__ モジュールにおいてだけ、 DeprecationWarning をデフォルトで表示するようにします。

__main__ モジュールとは、インタラクティブシェルや Python インタプリタに渡された実行ファイルのことです。そこで直接廃止予定のAPIが呼ばれたときだけ DeprecationWarning が表示されるようになります。

これにより、開発者がAPIの使い方を調べるなどの目的でインタラクティブシェルで廃止予定のAPIを実行したときに Warning に気づけるようになると期待できます。

とはいえ、これの効果は限定的なので、Python開発者は -Wdefault オプションを使うか、 PYTHONWARNINGS=default と環境変数を設定しておきましょう。

-X dev option

https://docs.python.org/dev/using/cmdline.html#id5

上で紹介した -Wdefaultオプションに加えて、 Python 拡張モジュール開発者向けのものも含めて、幾つかの開発者向けオプションをまとめて有効にするオプションとして -X dev オプションが追加されました。

また、 PYTHONDEVMODE=1 という環境変数でも dev mode を有効にできるようになります。

PEP 557 -- Data Classes

https://www.python.org/dev/peps/pep-0557/

ちょうど Qiita に紹介記事があったのでそちらを参照してください。

Python3.7の新機能 Data Classes

songofacandy at 12:04
この記事のURLComments(0)Python 
2017年12月12日

mruby を Linux カーネル内で動作させる(2017 ver)

はてなブックマークに登録

このエントリは KLab Advent Calendar 2017(兼 mruby Advent Calendar 2017)の 12 日目の記事です。

今年は(前半は)Keepalived にフルタイムでコントリビュートしていたり(後半は)ひたすら mruby をいじっていたりと、実に OSS 充な一年だった @pandax381 です。

タイトルにある試みについては、2015 年の時点で東京大学の品川先生が「mruby を Linux カーネル内で動作させる」という素晴らしい記事を書かれていて、Kernel module に組み込んだ mruby が動作することを実証されています。

mruby をカーネル内で動作させるために大きく問題となるのが、浮動小数点演算です。mruby は Float を標準的なクラスとして提供していると共に VM 内部でも浮動小数点演算を行っている一方で、Kernel 内では原則として浮動小数点演算を行ってはならないという制約があります。そのため、先人の試みでは、全ての float を long に置き換えたり浮動小数点演算にまつわる関数をダミーに置き換えることで辻褄を合わせながらコンパイルを通す手法を取っています。そのため、mruby のコードで浮動小数点演算を行った場合、どのような挙動をするかは一切保証されないものとなっています。

それから数年が経過し、2017 年現在の mruby には MRB_WIHTOUT_FLOAT という「ビルド時に Float クラスをはじめとする全ての浮動小数点数を取り除く」ためのオプションが追加されています、というか僕が追加しました。

このエントリでは、MRB_WIHTOUT_FLOAT オプションを使って最新の mruby を Kernel module に組み込んでみようという内容です。

下準備

まず、mruby と Kernel module をビルドするためのツール群をインストールします。

$ sudo apt install build-essential git-core ruby bison
$ sudo apt install linux-headers-$(uname -r)

kmruby というディレクトリを作成し、以降はこの中で作業します。

$ mkdir ~/kmruby

mruby のクロスビルド

GitHub から mruby のコードを持ってきます。

最新のコミットで試してみたところ SEGV を吐いてしまったため、MRB_WITHOUT_FLOAT がマージされた直後のコミット(7ae20e0785704bce1a5bf7183fc9202336ad8676)をチェックアウトします(あまり時間が取れずエントリの公開までに間に合わなかったので時間があるときに原因を探ります)。

$ git clone https://github.com/mruby/mruby.git ~/kmruby/mruby
$ cd ~/kmruby/mruby
$ git checkout 7ae20e0785704bce1a5bf7183fc9202336ad8676

Kernel 内で動作させるための mruby(libmruby.a)をクロスビルド用するためのコンフィグファイルを作成します。

$ vi ~/kmruby/build_config.rb
MRuby::Build.new do |conf|
  toolchain :gcc
  enable_debug
  conf.gembox 'default'
end

MRuby::CrossBuild.new('kernel') do |conf|
   toolchain :gcc
   conf.cc.include_paths << "../stub/include"
   conf.cc.flags << "-mcmodel=kernel -fno-PIE -mno-red-zone -fno-asynchronous-unwind-tables -fno-omit-frame-pointer"
   conf.cc.defines << %w(DISABLE_STDIO)
   conf.cc.defines << %w(MRB_WITHOUT_FLOAT)
   conf.cc.defines << %w(MRB_64BIT)
   conf.bins = []
end

次に、クロスビルド用にいくつかのヘッダファイルを作成します。

$ mkdir -p ~/kmruby/stub/include

setjmp / longjmp を GCC のビルトイン関数を利用するために、簡素な setjmp.h を作成します。

$ vi ~/kmruby/stub/include/setjmp.h
#ifndef SETJMP_H
#define SETJMP_H

typedef int jmp_buf[6];

#define setjmp(env) __builtin_setjmp(env)
#define longjmp(env, val) __builtin_longjmp(env, val)

#endif

標準の stdlib.h には float を返す関数などが含まれているため、代わりに最低限必要な関数のみを定義したものを用意してあげます。

$ vi ~/kmruby/stub/include/stdlib.h
#ifndef STDLIB_H
#define STDLIB_H

#include <stddef.h>

#define EXIT_FAILURE (-1)

void free(void *ptr);
void *realloc(void *ptr, size_t size);
void exit(int status);
void abort(void);

#endif

現状は上記の二つのヘッダファイルを用意してあげればクロスビルドできるようになります。

build_config.rb は標準のものではなくはじめに作成したものを指定して make します。

$ cd ~/kmruby/mruby
$ MRUBY_CONFIG=../build_config.rb make

Kernel module の作成

mruby のクロスビルドができたら、それを組み込むための Kernel module を作成します。

以下が Kernel module 本体のコードです。サンプルとして、Linux クラスを定義して printk というメソッドを実装していますが、これは後に作成する mruby のスクリプトから呼び出します。

$ vi ~/kmruby/main.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/ctype.h>
#include <mruby.h>
#include <mruby/irep.h>
#include <mruby/string.h>

#include "mrbcode.h"

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("kmruby");
MODULE_AUTHOR("YAMAMOTO Masaya");

static mrb_value
linux_printk (mrb_state *mrb, mrb_value self) {
  mrb_value str;
  int ret;
  mrb_value retval;

  mrb_get_args(mrb, "S", &str);
  ret = printk(KERN_INFO "kmruby: %s\n", RSTRING_PTR(str));
  retval.value.i = ret;
  return retval;
}

static int
kmruby_init (void) {
  mrb_state *mrb;
  struct RClass *Linux;
  mrb_value ret;

  pr_info("kmruby: init\n");

  mrb = mrb_open();

  Linux = mrb_define_class(mrb, "Linux", mrb->object_class);
  mrb_define_class_method(mrb, Linux, "printk", linux_printk, MRB_ARGS_REQ(1));

  ret = mrb_load_irep(mrb, code);
  pr_info("kmruby: ret = %d\n", (int)ret.value.i);

  mrb_close(mrb);

  return 0;
}

static void
kmruby_exit (void) {
  pr_info("kmruby: exit\n");
}

module_init(kmruby_init);
module_exit(kmruby_exit);

動作確認用にシンプルな mruby のスクリプトを作成します。このスクリプトは、後々 mrbc でコンパイルしてバイトコード(main.c が include している mrbcode.h)にします。

$ vi ~/kmruby/example.rb
Linux.printk "Hello World!"

このままだと、本来は libc 内に含まれている関数の実態が存在しないため、それっぽい何かを雑に実装してあげる必要があります。

$ mkdir ~/kmruby/stub/libc
$ vi ~/kmruby/stub/libc/libc.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>

void *
realloc(void *ptr, size_t size) {
  return krealloc(ptr, size, GFP_KERNEL);
}

void
free (void *ptr) {
  kfree(ptr);
}

int *
__errno_location (void) {
  static int errno;
  return &errno;
}

void
abort (void) {
  panic("libc stub: abort()");
}

void
exit (int status) {
  panic("libc stub: exit(%d)", status);
}

また、ビルド時にいくつかのヘッダファイルが参照できないとエラーが出るため、ダミーのファイルを用意します。

$ mkdir ~/kmruby/stub/include/dummy
$ touch ~/kmruby/stub/include/dummy/{inttypes,limits,stdint}.h

最後に Makefile を作成します。

$ vi ~/kmruby/Makefile
ccflags-y += -DMRB_64BIT -DDISABLE_STDIO -DMRB_WITHOUT_FLOAT -I$(PWD)/mruby/include -I$(PWD)/stub/include/dummy

obj-m := kmruby.o

kmruby-objs := main.o stub/libc/libc.o mruby/build/kernel/lib/libmruby.a

kmruby: libmruby.a mrbcode.h
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) CFLAGS_MODULE=$(CFLAGS) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

mrbcode.h: example.rb
        $(PWD)/mruby/build/host/bin/mrbc -B code -o $@ $^

libmruby.a:
        cd mruby; MRUBY_CONFIG=../build_config.rb make

これでビルドすれば、kmruby.ko が生成されます。

$ cd ~/kmruby
$ make

動作確認

生成された kmruby.ko を insmod でロードしてみましょう。成功すればカーネルログに出力があるはずです。言うまでもありませんが、カーネルモジュールなので何かミスっていると最悪の場合カーネルごとお亡くなりになるので実行の際は十分注意してください。

$ sudo insmod ./kmruby.ko
$ sudo dmesg
[21323.454671] kmruby: init
[21323.455421] kmruby: Hello World!
[21323.455423] kmruby: ret = 20

おわりに

まだいくつかフォローしてあげる必要はあるものの、MRB_WITHOUT_FLOAT のおかげで 2017 年版はだいぶシンプルになったのではないでしょうか。まだ、最低限のコードが動くだけですが、本格的に mruby でカーネルモジュールの開発ができるようになると面白そうですね。

pandax381 at 18:44
この記事のURLComments(0)
2017年12月11日

「推測するな計測せよ」は「性能上がらなかったら捨てろ」ではない

はてなブックマークに登録

「推測するな計測せよ」という格言がよく知られています。この格言は(ISUCONに優勝した)Goの作者の一人でもある、 Rob Pike 氏の言葉が元になっています。

  • ルール1: プログラムがどこで時間を消費することになるか知ることはできない。ボトルネックは驚くべき箇所で起こるものである。したがって、どこがボトルネックなのかをはっきりさせるまでは、推測を行ったり、スピードハックをしてはならない。

  • ルール2: 計測すべし。計測するまでは速度のための調整をしてはならない。コードの一部が残りを圧倒しないのであれば、なおさらである。

UNIX哲学 on Wikipedia

このルールは、推測だけで高速化のための変更をすることを諌めていますが、直接に高速化の効果が無い変更をするなとは言っていません。

正しいデータ構造やアーキテクチャは、それだけでは性能が向上せず、それを利用した改善を入れて初めて効果があることがあります。 そして正しいデータ構造やアーキテクチャを選ぶには正しい理解が必要で、計測はそのために必要なものです。

もちろん、プロジェクトによっては、そのアーキテクチャ変更が将来的にどれくらい効果があるのかをだれかに納得させるために(あるいは自分で納得するために)検証目的の荒削りのものでいいからそのアーキテクチャ変更を利用した高速化を実装しないといけないかもしれません。

さて、今年のISUCONでは、オンメモリ化(プロセスメモリ上に整合性の取れたデータを残して参照クエリを撲滅する)がキーポイントの1つでした。 (ちなみにもう一つのキーポイントはデータの特性に合わせたキャッシュでした。)

ただし、今年のISUCONの課題では最初からCPUネックでした。単にオンメモリ化しただけではスコアはあがりません。初期実装はロック競合が激しいので、ロックのつまり具合でスコアが乱高下するので、計測してみたらスコアが下がった人も多かったでしょう。

でも、オンメモリ化は正しいアーキテクチャでした。ロックをDBから引き剥がす事ができるし、(一番効果があったJSONまるごとキャッシュには関係しないものの)多倍長整数の値を文字列との相互変換をせずに直接キャッシュすることもできました。

もし、オンメモリ化しても大して効果がないから変更しなかったというチームがあったとしたら、この格言が「理解せずに変更するな」であって「性能向上しない変更をするな」ではないことを覚えておいてください。

(ただし、もう一つのキーポイントであるキャッシュを攻略すれば、オンメモリ化してなくても優勝チームのスコアを超えることは難しくなかったと思います。キャッシュについてはまた別に記事を書きます。)


@methane

songofacandy at 20:22
この記事のURLComments(0)
2017年09月28日

最近のPython-dev(2017-09)

はてなブックマークに登録

バックナンバー: 8月号 | 6月号 | 5月号 | 4月号 | 3月号 | 2月号 | 1月号

今月は Sprint がありました。去年の Sprint はベータ版直前だったのでたくさんの実装が入りましたが、次の Python 3.7 のベータは来年のはじめなので、今回は実装よりも提案(PEP)が多めです。とても全部は紹介しきれない(そもそも一部を除いて議論を追えていない)ので、今月からは提案については受理されたものや受理間近のものだけ紹介していきます。

namedtuple 生成の高速化

bpo-28638: Optimize namedtuple() creation time by minimizing use of exec()

namedtuple という、タプルの要素に整数の添字ではなく属性名でアクセスできるようにするデータ構造があります。 例えば次のようにして使われます。

_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])

これは動的にクラスを作るのですが、そのためにクラス宣言の Python コードを文字列置換で生成し、 eval() することでクラスオブジェクトを生成していました。このメタプログラミングの方式は、リフレクション的な機能を使ってクラスを生成するよりも、何をやっているのかが判りやすいというメリットがあります。

しかし、通常のPythonのコードはバイトコードにコンパイルされて pyc ファイルにキャッシュされるのに対し、この方式ではクラスを生成するコードは都度コンパイルされてしまいます。そのため、 namedtuple をたくさん使っているモジュールの読み込みは遅くなります。

そこで、より一般的な type() 関数を使う方式で namedtuple が書き直されました。とはいえ、生成されたクラスのインスタンス生成速度に影響する __new__ メソッドだけは、性能を落とさずに完全な後方互換性を維持できなかったので、1関数だけのごく短いコードに対するevalはまだ残っています。それでも、CPythonでもPyPyでも数倍速くなっているので、namedtupleをたくさん利用するライブラリをインポートする時間が短縮されます。

その他、複数の namedtuple で作ったクラス間で共有できる部分を共有するなどの最適化が盛り込まれ、(インスタンスではなくて)生成されたクラスオブジェクトのメモリ使用量も削減できています。

なお、従来は生成されたクラスに ._source という属性があり、 eval 対象になったクラス定義のソースコードが入っていたのですが、今回の改良でなくなりました。

OrderedDict のコンパクト化

去年私が dict をコンパクト、かつ挿入順を維持する実装をしたのですが、まだ collections モジュールの OrderedDict は dict と別にキーの双方向リンクリストを持っていて、 dict の倍のメモリを利用します。

OrderedDict の典型的な用途は単に json などで順序を維持したいというものですが、その用途なら dict を使ったほうが、メモリ使用量は半分になり、構築も列挙も高速です。しかし、 dict の順序は実装依存であり、CPythonとPyPyは順序を維持するものの、それに依存するのはお行儀が悪いです。

そこで、OrderedDictをdictの構造をそのまま利用するように書き換えて見ました。メリットとしては典型的な操作の性能があれこれ上がっているのと、メモリ使用量が1/2に、そしてソースコードも1000行以上削ることができました。

しかし、デメリットとして OrderedDict と dict が密結合する(今は分離されているソースコードを1つにマージしてしまう)ことと、 OrderedDict だけに存在する move_to_end() というメソッドの速度が数割落ちています。他にも、要素の移動や追加削除の平均計算量は O(1) のままだけれども、最悪計算量が O(n) になってしまうようなパターンが増えている可能性もあります。

この部分のエキスパートである Raymond Hettinger さんは特に大きな書き換えに厳しい人なので、説得するには実装の磨き上げとより詳しい検証が必要です。多分 ISUCON 後になるけれど、 Python 3.7 に間に合わせたい。

PEP 539 v3: A new C API for Thread-Local Storage in CPython

Yamamoto Masayuki さんが活動されていた、スレッドローカルストレージを利用するCレベルの新APIの提案が受理されました。

旧APIはかなり古くからあるのですが、TLS key として int 型を使っていて、LinuxやメジャーなUnix、Windowsでは問題ないもののPOSIXには準拠していませんでした。そのためCygwinなどで問題が有ったらしいです。

PEP 557: Data Classes

主にデータの入れ物となることを目的としたクラスをより手軽に作れるようにするためのAPIが提案されています。

@dataclass
class InventoryItem:
     name: str
     unit_price: float
     quantity_on_hand: int = 0

     def total_cost(self) -> float:
         return self.unit_price * self.quantity_on_hand

のように宣言すると、

   def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0) -> None:
       self.name = name
       self.unit_price = unit_price
       self.quantity_on_hand = quantity_on_hand
   def __repr__(self):
       return f'InventoryItem(name={self.name!r},unit_price={self.unit_price!r},quantity_on_hand={self.quantity_on_hand!r})'

のようなメソッドが自動で生成されます。

namedtuple と目的が被っているものの、 namedtuple はタプルであるために一部の用途には向かない事があります。例えば immutable だとか、普通のタプルと比較可能であるなどです。

Data Class はタプルではなく、そのためより柔軟です。同一性でなく同値性による比較可能にするかどうかなどが選べます。構文も Python 3.6 からの新しいスタイルを活用したもので、例えば namedtuple だと独自のメソッドを追加するにはもう1段継承したクラスを作るなどの面倒があったのですが、こちらは上の例の total_cost() メソッドのように普通に書くことができます。

このPEPは受理一歩手前で、あとはもう名前を決めるだけです。個人的には Record がいいな。

PEP 552 -- Deterministic pycs

pyc ファイルには py ファイルのタイムスタンプが格納されていて、 py ファイルが更新されると自動的に pyc ファイルも作り直されるようになっています。

このためにpyファイルが同じでもpycはバイナリレベルでは一致しません。これが最近の ビルド再現性(Reproducible Builds) というプラクティスには都合がよくありません。 (Debian/Ubuntu では pyc はインストール後にビルドされますが、 pyc を一緒にパッケージに含めているディストリビューションもあります)

また、Bazelというビルドシステムでもこれは都合が悪いらしいです。

この PEP ではヘッダーの形式を追加し、従来通りのタイムスタンプ+自動ビルド方式の他に、pyファイルのハッシュ値を持つ事ができるようになります。この場合、 pyc を読み込むときに py ファイルのハッシュ値を計算するコストが気になるところですが、新しいヘッダはpy ファイルのハッシュをチェックして自動リビルドするか無条件に pyc を使うかを選択するフラグを持っています。

pyファイルをチェックしない方を選択した場合も、 import 時にチェックしないだけで、コマンドを使って明示的にチェックしたりリビルドすることはできます。なので root しか書き込めないディレクトリに Python やライブラリをインストールする場合など、ユーザーがうっかり py ファイルを変更する危険が無い場合は問題ないでしょう。

このPEPはAccept直前(他に意見がなかったらAcceptするよとGuidoが宣言中)です。なお、このPEPはファイルフォーマットレベルでの問題を修正するだけで、それ以外にも pyc ファイルが一定にならない実装上の理由は幾つかあります。しかしこのPEPがAcceptされたということは、 deterministic pyc をサポートするという方向性が決まったことでもあるので、実装面の課題も今後修正されていくと思います。

起動高速化

インタプリタの起動高速化は難しい問題なのですが、実際のアプリケーションの起動ではその何十、何百倍の時間が、ライブラリのロードに消費されています。

あるモジュールを import しても、そのモジュールの全部の機能を使うわけではありません。そのモジュールの中で、あまり使用頻度が高くないと思われる関数でだけ使われる依存関係は、モジュールの先頭ではなくその関数で import することで、アプリケーションの起動時間を減らすことができるかもしれません。

一方で、それは PEP 8 違反(明確な理由やメリットがあったら違反しても良いです)ですし、その関数の実行時間は若干遅くなってしまいます。だから、利用頻度が少なく、かつその利用頻度に比べて import が重いモジュールに絞って書き換えが進んでいます。

argparse が直接、間接的に import しているモジュールが減らされました。(マージ済み) https://github.com/python/cpython/pull/1269

functools の singledispatch でしか使われないモジュールを singledispatch 内に移動します。 (accept待ち) https://github.com/python/cpython/pull/3757

uuid モジュールが、 uuid1 のために libuuid や UuidCreateSequential をロードする処理が重いのでそれを遅延する提案。(これからレビューします)

https://github.com/python/cpython/pull/3684

また、重い import を見つけるために私がローカルで使っていた import に掛かる時間を表示するパッチを、だれでも(特にライブラリやフレームワークの作者が)簡単に使えるように -X importtime オプションとして提案しています。今はその出力フォーマットについて自転車置き場の議論をしています。 https://github.com/python/cpython/pull/3490

余談ですが、昔幾つかのサードパーティーライブラリの import 時間を調べて居た所、 Jinja2 の import がすごく遅いことに気づきました。

実は今年のはじめにメモリ使用量を解析していたところ、 Jinja2 は Python 3 ではユニコードのシンボルも(テンプレートエンジン内の変数名として)使えるようにしようと、シンボル名として有効な名前を表現する正規表現をかなり強引な方法で作っていたのを見つけて報告していたのですが、その正規表現のコンパイルが import 時に実行されていて遅かったのです。

すでに Jinja2 の開発ブランチではずっと良い実装に切り替わっているので、次の Jinja2 のリリースを楽しみにしています。

songofacandy at 18:52
この記事のURLComments(0)Python 
2017年09月26日

LVSの高負荷対策 その2 ~障害の再現とその原因~

はてなブックマークに登録

こんにちは。インフラ担当の岡村です。
LVSの高負荷対策 その1 ~障害発生~」の記事で、大量のSYNパケットを受信した際にロードバランサの再起動が発生したことと、その緊急の対策についてご紹介しました。
今回は、再現確認を行い判明した再起動の原因と、LVSに備わっている高負荷対策の機能についてご紹介します。

検証

前回ご紹介した通り、障害発生時のログからメモリ周りが怪しそうでした。 そこで、ロードバランサにSYNパケットを送り、メモリの使用量の推移を観察しながら、再起動が発生するかどうかを確認しました。

検証環境の構成は次のようになります。

検証環境の構成
dsas_lvs_test_environment

パケット送信用サーバを複数台、ロードバランサを1台、Webサーバを1台使用し検証を行いました。
ロードバランサの検証を行う上で、本番環境と同様にロードバランスの処理をさせたかったため、LVSに振り分け先のWebサーバのIPアドレスを複数登録しています。
DSASのロードバランサではDSR(Direct Server Return) 方式を採用しており、Webサーバからの戻りパケットはLVSを経由しません。本検証を行う上ではWebサーバはパケットを返す必要はないので、ロードバランサから振られたパケットはWebサーバでdropしています。
パケット送信にはhping3を使用しました。オプションが豊富で様々なパケットを送信することができます。今回は障害時に検出したパケットに似せて、次のコマンドでwindowサイズ0のSYNパケットをロードバランサのVirtual Service宛に大量に送信しました。

hping3 -S -w 0 “Virtual ServiceのIPアドレス” -p 80 --rand-source --flood

DSASのロードバランサは、64bitOSのものだけではなく、32bitOSのものが残っている状態でした。今回の問題が発生したのは32bitOSの方だったため、まず32bitOSで検証を行いました。

32bitOSでの検証

上述の通りhping3を使用して、複数のサーバからロードバランサに向けて一斉にSYNパケットを送信します。
SYNパケットが届くと、IPVS ( LVSのレイヤー4の負荷分散機能を提供するカーネルモジュール ) はSYN_RECV状態のエントリを作成します。そのためSYN_RECV状態のエントリがどんどん増加していき、それに伴いLowメモリの使用量が増加していきました。
Lowメモリの使用量の推移を確認したところ、パケット送信前の状態では約750MB あったLowメモリの空き容量は刻々と減少し、最終的に枯渇してカーネルパニックが起こり、再起動が発生しました。

Kernel panic - not syncing: Out of memory and no killable processes...

64bitOSではLowメモリの制限がなくなるので耐性は向上すると予想できます。しかしメモリが枯渇すれば同様に再起動が起こりそうです。64bitOSを使用して、もう少し詳しくロードバランサの挙動を追っていきます。

64bitOSでの検証

結論から言うと、IPVSのエントリが増加しメモリが枯渇すると、やはり再起動が発生しました。ログを確認してみると本番環境での再起動の時と同様、再起動の直前でoom-killerが多発しており、また、カーネルのバージョンの違いから若干出力は異なりますが、IPVSのメモリ割り当てのエラーが出ていました。

IPVS: ip_vs_conn_new(): no memory

本番環境で起きた再起動も、IPVSのエントリが増加することによるメモリ枯渇に起因すると考えて良さそうです。
負荷試験中のSYN_RECV状態のエントリ数と消費メモリの推移を確認し、グラフにしました。(グラフ1)
今回の検証で使用したサーバはメモリを8GB搭載しており、負荷をかける前のMemFree値がおよそ7GBであったことから、以下の全てのグラフの「消費メモリ」の値は、「 7GB - 観測したMemFree値 」で算出しています。

dsas_lvs_graph1

負荷をかけ始めてから約49秒で消費メモリが7GB、すなわち、8GBのメモリを使い切り、再起動が発生しました。
さて、IPVSの挙動を確認するため、先の試験より負荷を抑え、再起動が起きなかった場合のグラフを見てみましょう。(グラフ2)

dsas_lvs_graph2

およそ60秒間はエントリ数とメモリ消費量は増え続けますが、それ以降は一定の値で推移しているのがわかります。
これは、SYNパケットが届くとIPVSはSYN_RECV状態のエントリを作成しますが、SYN_RECV状態のままだと60秒でタイムアウトになり、エントリが削除されるためです。
今回の試験では一定の強さの負荷をかけ続けたため、60秒以降は「タイムアウトで削除されるエントリ数」と「新規に作成されるエントリ数」が釣り合い、一定になったと考えられます。
(ピークが60秒から少しずれているのは、複数のサーバからパケットを送って負荷をかけているため、それぞれのサーバで負荷の開始時刻の誤差があったためです)

対策について

64bitOSでも再起動は発生したものの、32bitOSの場合と比較すると大幅に負荷耐性が向上しました。32bitOSでLowメモリが制限されてしまっている環境の場合は、64bitOSに変更することで耐性を上げられます。その上でメモリを追加すれば更に負荷耐性が上がり、再起動の対策になりますね。
では、他に有効な対策はないでしょうか?
調べてみると、LVSは高負荷対策の機能をいくつか備えているようです。(参考)
今回は、そのうちの機能の一つであるdrop entry機能を使用してみたのでご紹介します。

drop entry 機能の紹介

IPVSのエントリをランダムに削除してくれる機能です。
SYN-RECV状態とSYNACK状態のエントリを削除するアルゴリズムは、毎秒IPVSのコネクションハッシュテーブルからランダムに選んだ範囲(全体の32分の1)をスキャンし、その中のSYN-RECV状態、SYNACK状態のエントリを削除する、というもののようです。
ESTABLISHED状態のエントリとUDPのエントリは共に、次の2つの条件を両方とも満たしている場合に削除される可能性があります。
・最後のパケットが届いてから60秒以上経過
・最初のパケットが届いてからの受信パケットの合計数が8以下
受信パケットが8以下の場合でも、受信パケット数が大きくなるに連れて削除される可能性は低くなるようです。

drop entryは、使用可能なメモリ量が設定した閾値を下回ったときに、自動で有効にすることが可能です。その場合は、以下に1(または2)を設定します。

/proc/sys/net/ipv4/vs/drop_entry

メモリの空きが閾値を下回ると自動的に値が2になり、機能が有効になります。
( 閾値を下回っていないときに、2を設定すると、自動で1になります。)
また、閾値によらず常にdrop entryを有効にしたい場合は、3 を設定します。
閾値は以下で設定可能です。

/proc/sys/net/ipv4/vs/amemthresh

単位は memory pageなので、例えばメモリが残り1GB(=1048576KB)を下回った時に有効にしたい場合は、"1048576KB÷(ページサイズの)4KB"を計算して、262144 を設定します。

drop entry有効時の動作確認

閾値を6GB,3GB,1GBとし、SYNパケットを送って負荷をかけたときの、SYN_RECV状態のエントリ数と消費メモリの値の推移をそれぞれグラフにしました。

dsas_lvs_graph3
dsas_lvs_graph4

負荷は上の64bitOSの検証で再起動を発生させたときと同じ強さなので、drop entryなしでは再起動してしまいます(グラフの青線)。しかし、drop entryを有効にすると、3つのどの閾値の場合も消費メモリを抑えることができ、再起動は起こりませんでした!
例えば閾値を3GBに設定したときの推移(グラフの黄線)を見ると、消費メモリが4GB、すなわち残りのメモリが3GBになった時点から、エントリ数とメモリ消費量の増加が緩やかになっているのがわかります。
閾値を大きくすればするほど、消費メモリの最大値は小さくなり、耐性が向上することも見て取れます。

ただ、drop entryを有効にすれば必ずしも再起動を防げるというわけではなく、負荷に対して閾値を小さくし過ぎると、例えば上の環境で閾値を150MBにすると、drop entryが間に合わず再起動が起きました。また、負荷と比較して搭載メモリが少ない場合は、drop entryを常に有効にしていてもメモリが枯渇する可能性がある点も注意が必要です。

しかし、メモリ増強などの追加投資なしで、すぐに耐性を上げることができるので、とても便利な機能です。
もちろんdrop entry有効時には、IPVSを経由するどの接続もエントリ削除の影響を受ける可能性がありますが、ロードバランサが落ちてサービス停止してしまうよりはずっと良いのではないかと思います。LVSを使用されている方は是非お試しください。

次回、LVSの高負荷対策 その3でも、引き続きdrop entryの検証結果を紹介しようと思っています。

okamura_h at 11:57
この記事のURLComments(0)lvs 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント
Syndicate this site