Foreverly

メモ帳

吉祥寺.pm13に参加してきました

こちらの勉強会で発表してきました。

吉祥寺.pm13 - connpass

吉祥寺.pmだけど今回は西新宿。

テーマが「新しい挑戦、新しい視点」だったので、新年度に向けた決意的な感じで発表してきました。 2018年の抱負は「ちゃんと調べて、ちゃんと理解する」で行こうと思います。 最近プログラミングの勉強(Python)を始めたので、 PythonでWEBアーキテクチャの実装をして、理解を深めました。

speakerdeck.com

イベント駆動や、DBもやっていきたいので頑張ります。 自分の発表内容は以下のオマージュ(パクリ)なのですが process-bookの著者様がいらっしゃり、 お世話になりました。というお気持ちで一杯になりました。 発表も盛り上がってて凄かった。

2015年Webサーバアーキテクチャ序論

process-book

イベント駆動はこれを参考にがんばりたい

イベント駆動プログラミングとI/O多重化

黒曜石を使って、あまり画面をみないで前を見て発表するを意識したけどまだだめ。 もっとアウトプットして、発表練習や発表の場を持とうと思います。 他の人のプログラミングの発表は何もわからん状態になってしまったので、 プログラミングの勉強がんばりたい。。。

吉祥寺.pm様ありがとうございました。

Python Programming for Web Architectures

タイトルをかっこよくしてみた。 最近PyQなるものを初めてPythonの書き方を覚えているのですが、 様々なライブラリがあることを知り、簡単なTCPサーバが書けることを知りました。

普段はWebサービスのサーバ管理をしていますが、 TCP/IPの通信をクライアントーサーバ間でどのような処理でされているのか、 IPやPortってどんな役割を果たしているのかなど実装を通じて学んでみました。

他にもApacheなどのWebサーバはpreforkモデルですが、 preforkってどんな処理なのか、Nginxが解決するC10K問題って何かなども Webサーバアーキテクチャのおさらいもしようと思います。

主に以下をパクリもとい参考にしています。 なので本記事を読まなくてもいいので、以下の記事や本だけでも読んでみて下さい。 2015年Webサーバアーキテクチャ序論 process-book Learning Python Network Programming

イベント駆動モデルはわからんので断念してます。

IP,Portそしてsocket

socketはエンティティがプロセス間通信を実行できる仮想エンドポイントで、 仮想エンドポイントを識別するために必要になるのが、IPとPort番号になる。 IPアドレスでホストがわかるけど、TCP/UDP通信でどのプロセスと通信をするのか決めるために Port番号が必要になる。

ss,netstat,lsofコマンドで、何気なくどのポートがなんのプロセスか、 どのプロセスがファイルを掴んでいるのかなど確認していましたが、 socket通信を理解すると、コマンドの確認結果の理解に繋がります。

3WAYハンドシェイク

TCPUDP通信ですが、ここではTCP通信について取り上げます。 クライアントとサーバーの間の3WAYハンドシェイクプロセスによってTCP接続を確立します。 SYN→SYN/ACK→ACKのやつですね。

TCPコネクションの図やTCP jokeなどをみると理解が深まります。

TCPサーバのリスニング接続を設定Pythonの関数create_listen_socket()として書いてみます。

def create_listen_socket(host, port):
    """ サーバーが接続要求を受け取るソケットを設定する """
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # アドレスファミリー、ソケットタイプ、プロトコル番号を指定して新しいソケットを作成
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((host, port))
    sock.listen(100)
    return sock

クライアントとサーバ間のメッセージのやりとりの例(ユニキャスト通信)

メッセージをsocketから受信するための関数をrecv_msg()関数として以下に定義しました。

def recv_msg(sock):
    """ データがソケットに到着するのを待ってから、メッセージの区切り文字として '¥0'を使用してメッセージに解析する """
    data = bytearray() # 新しいバイト配列を返す。
    msg = '' 
    # ソケットから4096バイトを繰り返し読み込み、デリミタが表示されるまでバイトをデータに格納する
    while not msg:
        recvd = sock.recv(4096) # ソケットからデータを受信し、結果を bytes オブジェクトで返します。一度に受信するデータは、4096bufsize
        if not recvd:
            # ソケットが途中で閉じられたら
            raise ConnectionError()
        data = data + recvd
        if b'\0' in recvd:
            # b '\ 0'をメッセージの区切り文字にする
            msg = data.rstrip(b'\0')
    msg = msg.decode('utf-8') #バイト型から文字列型へデコード
    return msg

メッセージの受信を待っている間にプログラムが必要とするものは何もないので、この関数はメッセージ全体を受信するまでループ内でsocket.recv()を呼び出します。 nullバイトを受け取ったかどうかを見るために繰り返しデータをチェックし、うけとったら、null バイトを取り除き、UTF-8からデコードして受信データを返します。

最後にsend_msg()関数とprep_msg())関数を作成しました。 これはメッセージにnullバイトの区切り文字をつけるのと、UTF-8エンコーディングして送信するための関数です。

def prep_msg(msg):
    """ メッセージとして送信する文字列を準備する """
    msg += '\0'
    return msg.encode('utf-8') # 文字列をバイト型へエンコード
def send_msg(sock, msg):
    """ 文字列をソケットに送信する準備 """
    data = prep_msg(msg)
    sock.sendall(data)

さきほど書いた関数をモジュールとして、 新たにメッセージを受けるサーバのスニペットを以下のように定義して書きました。

def handle_client(sock, addr):
    """ sockを通じてclientからデータを受けとり,echoを返す """
    try:
        msg = chatmodule.recv_msg(sock) # messageを完全に受信するまでblockする
        print('{}: {}'.format(addr, msg))
        chatmodule.send_msg(sock, msg) # 送信するまでblock
    except (ConnectionError, BrokenPipeError):   # ConnectionError のサブクラスで、もう一方の端が閉じられたパイプに書き込こもうとするか、書き込みのためにシャットダウンされたソケットに書き込こもうとした場合に発生。
         print('Socket error')
    finally:
        print('Closed connection to {}'.format(addr))
        sock.close()

if __name__ == '__main__':

    listen_sock = chatmodule.create_listen_socket(HOST, PORT)
    addr = listen_sock.getsockname() # ソケット自身のアドレスを返します。この関数は、IPv4/v6ソケットのポート番号を調べる場合などに使用。
    print('Listening on {}'.format(addr))

    while True:
        client_sock, addr = listen_sock.accept()
        print('Connection from {}'.format(addr))
        handle_client(client_sock, addr)

最初に、listen_sockをcreate_listen_socket()呼び出して定義します。 次に、クライアントからの接続要求を永久にlistenし、listen_sock.accept()をブロックするmainループに入ります。 クライアント接続が開始されると、プロトコルに従ってクライアントを処理するhandle_client()関数が呼び出されます。 部分的にメインループを整理し、この一連の操作を再利用できるように別の関数にしました。 これでサーバー側の処理が書けたので、次はクライアント側です。

クライアントのスニペットも以下に書きました。 mainループでしていることはメッセージとしてqを入力してクライアントを終了するまで永遠にループさせています。 メインループ内では、まずサーバーへの接続を作成をして、次に、ユーザーにプロンプ​​トを表示します。 送信するメッセージを入力すると、上で書いたsend_msg()関数を使ってメッセージが送信されます。 その後、サーバーの応答を待ちます。応答がきたら、それを出力します。 クライアントとサーバがこれで書けますので、参考にしてみて下さい。

if __name__ == '__main__':

    while True:
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.connect((HOST, PORT))
            print('\nConnected to {}:{}'.format(HOST, PORT))
            print("Type message, enter to send, 'q' to quit")
            msg = input()

            if msg == 'q': break
            chatmodule.send_msg(sock, msg) # 送信するまでBlock
            print('Sent message: {}'.format(msg))
            msg = chatmodule.recv_msg(sock) # messageを完全に受信するまでBlock
            print('Received echo: ' + msg)

        except ConnectionError:
            print('Socket error')
            break
        finally:
            sock.close()
            print('Closed connection to server\n')

クライアントとサーバのソケット通信の動きをまとめると

  1. サーバはsocket、bind、listenでクライアントの接続を待ち受ける
  2. 接続きたらacceptにより実際のデータの読み出しまで待つ。
  3. データがきたら、リクエストを処理して、クライアントにレスポンスを返す。
  4. クライアントとの接続をcloseで閉じて、またaccept待ち状態になりライアントからの接続をLISTENする。

これでクライアントとサーバのソケット通信がsocket、bind、listenを使用して理解できたと思います。 ただ、実行するとわかりますが、ひとつのクライアントからのリクエストしか処理できません。 クライアントからの接続を accept したあとは、ループを抜けるまでは新規のクライアント接続をブロックしてしまいます。

1:1の通信なのでユニキャスト通信です。 WEBサーバアーキテクチャとしてはシリアルモデルと言われるものになります。 Apacheは複数のユーザからのリクエストにレスポンスを返せるのはなぜでしょうか、 複数のプロセスがリクエストを返しているからですね。 それでは次はマルチプロセスモデルをみていきましょう。

マルチプロセスモデル

プロセスをforkして子プロセス子プロセスにリクエスト処理を任せるモデルです。 forkはプロセスをコピーするのでリソースを使い重いと言われますが、 しかし、Copy On Write(Cow)という仕組みで、差分のみをコピーするため実際にはそこまでリソースを使いません。 ただCoWでメモリコピー負荷が抑えられていますが、リクエスト毎にforkが発生するとリソースが使われますので 事前にforkさせておくのがpreforkモデルです。 事前に一定数の子プロセスをforkして、それらを使いまわす(MaxRequestsPerChildなど)ことで、リクエスト毎にforkをしなくてすみます。

また、マルチプロセスモデルは、プロセス間通信が必要なのでforkによるパフォーマンスが低下する可能性がありますが、 メモリを共有していないということは、コード内で競合状態が発生しないのでロックなどを気にしなくてすむので、処理を書くのが簡単になります。

今回はaccept()したあとの処理をプロセスで行いました。workerモデルと呼ばれるものです。 acceptしたプロセスからworkerプロセスにsocket接続を引き渡しますので コンテキストスイッチの負荷がかかってしまいます。

while True:
    client_sock,addr = listen_sock.accept()
    proc = Process(target=handle_client,
                       args=[client_sock, addr])
    proc.start()
    print('Connection from {}'.format(addr))
    proc.join(1)

ちなみにpreforkモデルはaccept()からclose()までの処理を各プロセスにやらせます。 accept()からclose()までの処理がシンプルに書けます。 (ただ自分は上手く書けなかったので、だれか実装例を教えてほしい)

デメリットは同時接続数がプロセスの数ということです。 同時接続数が子プロセスの数を超えると、accept()がされないので、接続は未処理となり詰まります。

Python multiprocessing 備忘録

マルチスレッドモデル

マルチスレッドのとマルチプロセスの違いはなんでしょうか。 基本的には同じでスレッドにはリクエストごとにスレッドを生成する1コネクション1スレッドのモデルと、 事前にスレッドをPoolしておくモデル(スレッドプール)があります。 スレッドの利点としてプロセスと比較した場合のメモリ占有量は軽量と言われています。 スレッドはリソースを共有しているため、複数のスレッド間で通信が可能で異なるメモリアドレスを読み書きすることができますが、 2つのスレッドがメモリを共有し始め、スレッドの実行順序を保証する方法がない場合は、間違った値を返したり、システム全体がクラッシュしたりする可能性があります。

if __name__ == '__main__':
    listen_sock = chatmodule.create_listen_socket(HOST, PORT)
    addr = listen_sock.getsockname()
    print('Listening on {}'.format(addr))
    while True:
        client_sock,addr = listen_sock.accept()
        # Thread は自動的にhandle_client()関数を実行し、同時にこのwhile loopを実行
        thread = threading.Thread(target=handle_client,
                                  args=[client_sock, addr],
                                  daemon=True)
        thread.start()
        print('Connection from {}'.format(addr))

接続するクライアントごとに、handle_client()関数を実行するだけの新しいスレッドを作成されます。 スレッドが受信または送信時にブロックすると、OSは他のスレッドをチェックして、 それらがブロッキング状態から抜けたかどうかを確認し、存在する場合はそのスレッドの1つに切り替えます。 スレッドコンストラクタ呼び出しのdaemon引数をTrueに設定しました。 ctrl-cでプログラムを終了できますが、明示的にすべてのスレッドを閉じる必要はありません。 複数のクライアントでこのエコーサーバーを試すと、メッセージを接続して送信する2番目のクライアントがすぐに応答を受け取るのがわかります。

キューやロックの処理はチャットサーバなどを実装するとわかると思います。 (ただ自分は上手く書けなかったので、だれか実装例を教えてほしい)

マルチスレッドのコンテキスト切り替えに伴うコスト スレッドセーフ

C10K問題

インターネットが発展してWebサーバーが同時に1万のクライアントを処理する時代になり、 マルチプロセス/スレッドではこの問題が解決できません。 解決策として、イベント駆動アーキテクチャであるNginxが誕生しました。 Nginxが目指すものはイベントループで1つのスレッドで数万の同時接続を処理することです。

しかし、全てがイベント駆動で解決するというわけではなく、マルチスレッド・アプローチで解決できたり、 他にはスケーリングの問題としてもC10Kは使われます。

また、アーバン・エアーシップ社が単一のノードで500.000件の同時接続。 C500k問題に直面した模様。 膨大な数のモバイルデバイスに通知サービスを提供するには、非常に多くのアイドル接続を並行して処理する必要があります。

今は、1000万の同時接続である「C10Mの問題」に直面しています。 このようなものは大規模分散システムの領域でしょうか。 いろんなアーキテクチャを組み合わせないと解決できなそうですね。 これらの知識についても勉強したいです。

TCP/IP - Solving the C10K with the thread per client approach C500k in Action at Urban Airship C10M

イベント駆動型サーバーアーキテクチャ

1プロセス/スレッドでは同時に複数のブロック処理を扱えないため、 新たにプロセスやスレッドを生成し処理をさせていましたね。

イベント駆動ではイベントループで一のスレッドを複数の接続にマッピングさせて 接続、リクエストの入出力操作から発生したすべてのイベントを処理させます。 新しいイベントがキューに入れられ、スレッドはいわゆるイベントループを実行します。 キューからイベントをdequeueしてイベントを処理し、次のイベントを取得するか、新しいイベントがプッシュされるのを待ちます。 したがって、スレッドによって実行される作業は、複数の接続を単一の実行フローに多重化するスケジューラの作業と非常に似ています。 次のスライドを参考になりそうです。

イベント駆動プログラミングとI/O多重化

イベント駆動型サーバーアーキテクチャPythonで イベント駆動型プログラミングを書かないと理解が深まりそうにないので 次回は以下を調べてみようと思います。 (だれか参考になる実装例や説明教えてください)

  • イベント駆動型プログラミング
    • それは何であり、どのように機能するのですか?
  • asyncioモジュール
  • asyncioベースのプログラミング
  • Twisted
  • Gevent

オブジェクト、メソッドまとめ

Socket family:socket.AF_INET=アドレス (およびプロトコル) ファミリーを示す定数で、 socket() の 最初の引数に指定することができます。
Socket type:ソケットの種類を指定します。SOCK_STREAMとSOCK_DGRAMをそれぞれ指定すると、TCPベースのソケットとUDPベースのソケットが作成されます。
socket.bind(address):ソケットを address にbindします。
socket.listen([backlog]):サーバーを有効にして、接続を受け付けるようにします。backlog が指定されている場合、少なくとも 0 以上でなければなりません (それより低い場合、0 に設定されます)。システムが新しい接続を拒否するまでに許可する未受付の接続の数を指定します。指定しない場合、デフォルトの妥当な値が選択されます。バージョン3.5 で backlogの引数が任意になりました。
socket.setsockopt(level, optname, None, optlen: int)
level:SOL_SOCKET:level パラメータは、オプションのプロトコルレベルを指定します。オプションをソケットレベルで取得するには level パラメータに SOL_SOCKET を指定します。
TCP のようなそれ以外のレベルの場合、そのレベルのプロトコル番号を指定します。
フラグ:SO_REUSEADDR フラグは、 TIME_WAIT 状態にあるローカルソケットをそのタイムアウト期限が自然に切れるのを待つことなく再利用することをカーネルに伝えます。
socket.listen:サーバーを有効にして、接続を受け付けるようにします。backlog が指定されている場合、少なくとも 0 以上でなければなりません (それより低い場合、0 に設定されます)。システムが新しい接続を拒否するまでに許可する未受付の接続の数を指定します。指定しない場合、デフォルトの妥当な値が選択されます。バージョン 3.5 で変更: backlog 引数が任意になりました。
socket.sendall(bytes[, flags]):ソケットにデータを送信します。ソケットはリモートソケットに接続済みでなければなりません。オプション引数 flags の意味は、上記 recv() と同じです。
send() と異なり、このメソッドは bytes の全データを送信するか、エラーが発生するまで処理を継続します。
正常終了の場合は None を返し、エラー発生時には例外が発生します。
エラー発生時、送信されたバイト数を調べる事はできません。
バージョン 3.5 で変更: ソケットのタイムアウトは、データが正常に送信される度にリセットされなくなりました。
ソケットのタイムアウトは、すべてのデータを送る最大の合計時間となります。
システムコールが中断されシグナルハンドラが例外を送出しなかった場合、
このメソッドは InterruptedError 例外を送出する代わりにシステムコールを再試行するようになりました (論拠については PEP 475 を参照してください)。
socket.accept:接続を受け付けます。ソケットはアドレスにbind済みで、listen中である必要があります。戻り値は (conn, address) のペアで、 conn は接続を通じてデータの送受信を行うための 新しい ソケットオブジェクト、 address は接続先でソケットにbindしているアドレスを示します。新たに作成されたソケットは 継承不可 です。システムコールが中断されシグナルハンドラが例外を送出しなかった場合、このメソッドは InterruptedError 例外を送出する代わりにシステムコールを再試行するようになりました (論拠については PEP 475 を参照してください)。

Python入門者の集い #6 に参加してきました

f:id:oza__shu:20180126100847p:plain

突然ですが、煽られたのでこちらの勉強会にLTしてきました。

python-nyumon.connpass.com

発表したもの

最近PyQやっているので、PyQの紹介とPythonでライブラリ使ってLinuxと仲良くなりたい話をしました。 ファイルディスクリプタのところは自分でもスライド書いてて理解を整理できたので発表するのも勉強になってよい。 黒曜石を使ったのですが良かったけど、下むいて発表してしまったので、 事前の発表練習も必要だなと反省。

speakerdeck.com

その他感想

LTでPillowというライブラリを使ったモザイクアートがおもしろかった。 嫁は二次元にいると言っていて愛を感じた。 作ったものを公開するとたのしそうだし、周りからの反応もあると嬉しいだろうなあ。

懇親会で何人かに声をかけて貰えて、参考になったと言ってもらえたのは嬉しかったです。 次に発表するときは内容が濃いものにしたいので、Pythonで何か作ったものを発表したい。

発表する敷居が低いのでお勧めの勉強会です。

Apache Usedslotの見方

Apache Usedslotの見方をまとめてみた

稼働状況の表示(ステータス情報表示)

稼働中のプロセス数,待機中のプロセス数および各プロセスのステータス(R,W,Lなど)をWebブラウザに表示 この情報を基に,StartServers,MinSpareServers,MaxSpareServers,MaxClientsディレクティブなどをチューニングできる

server-statusハンドラの指定

ステータス情報の表示機能を利用するには,次に示すようにserver-statusハンドラを指定 Webサーバのステータス情報はアクセス制御して,エンドユーザには非公開にするのが一般的

<Location /server-status>
      SetHandler server-status
</Location>

URLの指定

ステータス情報を表示するには,Webブラウザやcurlで次の形式でURLを指定

http://IPアドレス/server-status?auto

autoはプレーンテキスト形式で表示できる

# curl http://127.0.0.1/server-status/?auto
Total Accesses: 4366246
Total kBytes: 77563940
CPULoad: .34465
Uptime: 150727
ReqPerSec: 28.9679
BytesPerSec: 526949
BytesPerReq: 18190.8
BusyWorkers: 13
IdleWorkers: 7
Scoreboard: K_KKKW._.K..K._.K___KK.W.K_.K..............................................(略)..........

※ステータス情報の表示機能で取得できる情報(auto指定がある場合)

  • Total accesses:合計アクセス回数
  • Total kBytes:合計通信量
  • CPU load:CPU使用率
  • Uptime:サーバプロセスの稼働時間(秒)
  • ReqperSec:1秒当たりのリクエスト数
  • BytesPerSec:1秒当たりの通信量
  • BytesPerReq:1リクエスト当たりの通信量
  • BusyWorkers:リクエスト処理中のサーバプロセス(スレッド)数
  • idle workers:リクエスト待ち状態のサーバプロセス(スレッド)数
  • Scoreboard:個々のスレッドの動作状況

※稼働中のサーバプロセスの状態の記号

  • _ : リクエスト待ち状態
  • S : 起動処理中
  • R : クライアントからのリクエストを受信中
  • W : リクエストの処理実行およびクライアントへレスポンス送信中
  • K : 持続型接続状態でリクエスト受信待ち
  • D : ルックアップ中
  • C : 接続を終了中
  • L : ログ出力処理中
  • G : graceful restartにおける処理終了待ち
  • I : スレッド停止中
  • . : 起動していない状態

見方のPoint

  • 「R」 or「W」 が多ければ待機プロセスを増やしたほうがいいので、MaxClients を増やす。
  • 「.」が多ければ、ServerLimit を減らす。
  • 「_」が多ければ、待機中プロセスが多いので、MinSpareServers と MaxSpareServers を減らす。

Apacheスコアボードの監視とチューニング

server-status

監視では

.と_を抜かした個数を分子にして、.を抜かした個数を分母にすれば以下のことがわかる。

  • Apache の同時接続数の割合を監視することができる
  • MaxClients に対しての 使用中のスロット数 の割合を監視対象の数値とすることができる

スケーラビリティの技術の基礎

サーバやアプリをクラウドに置くと安いし便利ってだけではなく、 クラウドで実現可能なことは全世界に展開するような WEBスケーラブルなシステムが作れることが可能になったこと。 技術的には大変だけど... 全世界に展開するようなWEBシステムに携わるかはわからないけど、 大規模分散システムの原理原則や考え方を知っておくのは何かの役に立つかもしれない。

複製管理における基礎技術について

べき等性

重複実行してもよい処理のこと

分散システムにおける一般的な問題

クライアントがあるリクエストに対し完了通知がこないときに、 リクエストを再送すると、すでに処理済みである場合がある

対応策1

重複実行してもよい(べき等・Idempotent)処理か確認し、 そうであれば何度でもリクエストに対し処理を起動する(固定ファイル新規作成処理など)

対応策2

ミドルウェアまたはアプリケーションがリクエストにIDを振って、 処理完了を記録することで処理済みの処理を2回起動しないようにする(課金処理など) 処理がまだなら課金処理をして、処理済みだったら終了通知を発行させる処理を入れれば、 同じリクエストを投げても、2重課金がおきない。

ACID

複数のデータストアに対するトランザクション(ひとかたまりの論理的な操作)の性質と 書き込みなどのトランザクション実施時において、ACID実現のために必要なこと(高コスト)

  • Atomicity(原子性):含まれる操作がすべて実行されるか、すべて実行されないかのいずれかの結果になる
    • 原子性のため、全ノードで全操作の実施可否を確認し、全ノードが実施可能である時の実施
  • Consistency(一貫性):アプリケーションが定める不変条件をみたす
    • 一貫性のための判断を実施可否確認時に行う
  • Isolation(孤立性):複数のトランザクションは互いに干渉しない(外部から、途中過程や部分的に実施された状態にアクセスできることはない)
    • 孤立性のため、実施時には影響がおよぶ領域への他の読み書きをブロックする
  • Durability(耐久性):一度実施することが確定すると、その変更は永続的に反映される。

2PCプロトコル

Two-Phase Commitment(2PC)Protocol

ACIDを達成するための有名なプロトコル

参加者はcommitまたはabortに投票

クォーラム(定足数)

読み書き操作に関する一貫性のみを考えると、 必ずしも全ノードを同一の状態に揃える必要はない

  • 最新値を読み出すことができる 
  • 最新値がシステム内に複数存在することはない

過半数と多数決 書き込みと読み込みの数を変えてコストを調整できる。

2QW(書き込み数) > n かつ QW(書き込み数) + QR(読み込み数) > n(全ノード数)

2PCとクォーラムはノードの増減は想定していないけど、 実際は増減する。障害やオートスケールなど

過半数の合意を取ることでノード増減してもクォーラムなどでも対応できる。

一貫性

データストアに格納された値が最新値(書き込みの発生順序が守られている)に関して データストア(を実現する複数ノード)で合意を取ることが前提(しかし高コスト) これがそうでない場合はどうだろうか。

複数を用いたデータストアに複数のノードからの書き込みと読み込みを行うと タイミングでは書き込みの発生順序・最新値がことなるように外から見えてしまう。

一貫性モデル

データを管理するデータストアと、アプリケーションプログラムの間の約束事

  • アプリケーションプログラムが守るべきルール
  • データストアが保証する性質

順序一貫性(Sequential Consistency)

どのような捜査の結果も、すべての読み書き操作があたかも直線的な順序で行われたとしたときの結果と同一となる 複数を用いたデータストアに複数のノードからの書き込みと読み込みを行うとタイミングでは書き込みの発生順序・最新値がことなるように外から見えてしまうことは許さない!!!! 内部実装としては、複数間で「次に扱う書き込み」について常に合意しながら進めることになる

因果一貫性(Causal Consistency)

因果的に関連している可能性のある書き込みは、必ず同じ順序で観測される 順序一貫性で許されなかった複数を用いたデータストアに複数のノードからの書き込みと読み込みを行うとタイミングでは書き込みの発生順序・最新値がことなるように外から見えてしまうことは許される

許されないことは?

aを読んでbを書き込むという因果関係の例

P1:Write x:=a P2:Read x -> a その後、Write x:=b P3:Read x -> a その後、Read x->b P4:Read x -> b その後、Read x->a

P4の観測結果はaを読んでbを書いたという把握できる順序関係に反している!!!

ベクトルタイムスタンプ

イベントの発生順序を判別するために使う論理クロックの一手法 物理クロックは誤差があるかもしれず、物理時間での前後関係をすべて気にする必要もない 時間ではなくて、前後関係がわかるタイムスタンプのこと

概要

各ノードは、「自分や他のノードでいくつかのイベントが起きたか」に関する情報をもつ 自ノードでイベントが発生したらインクリメント 他ノードへのメッセージ送信時には、自身が持つ情報を添えて送り、受信側はその情報を使って自身が持つ情報を更新

A[5,2,8]<Y[5,5,10] # Yの方が後 C[7,5,8]?Y[5,5,10] # どっちが先か後か断言できない

因果一貫性を実現するには書き込みマルチキャストだけをカウントする

オーバヘッドが少ないので、負荷を気にせずできる。

弱一貫性(Weak Consistency)

アプリケーション側から一貫性を保たせるには、API側で最新値を取らせる(Lockなど)、順序一貫性よりかは軽い。

  • 順序一貫性を保証する「同期変数」への操作を提供して、アプリケーションプログラム側において、一貫性が必要な場合に制御可能にする
    • データストアへLockをとるイメージ
  • すべての先行の書き込みがすべての場所で完了するまで、同期変数への操作は許容されない
    • 読み込み前に同期操作を行えば、最新値を読み取れる
    • Lockをとってから読めば、最新値をとれる
  • すべての先行の同期変数へのオペレーションが完了するまで、データへの操作は許容されない
    • 書き込み後に同期操作を行えば、進行中・部分的な書き込み、書き込み結果の伝搬を完了できる

イベンチュアル一貫性/結果一貫性(Eventual COnsistency)

更新は全てのコピーに伝搬される(更新がなければすべての複製は同一に収束していく) 書き込み同士の衝突はなくて、読み込みが必ずしも最新でなくても構わないデータストアに適している(DNS,Webキャッシュ) クラウドでは、スケーラビリティを重視するために採用されている(書き込み同士の衝突もあるし、読み込みは最新がよいが、それでも採用したほうが良いと考える)

クラウドサービスにおける設計

AmazonSQS

Amazon Simple Queue Service (SQS) マイクロサービス、分散システム、およびサーバーレスアプリケーション用の完全マネージド型メッセージキュー。

  • メッセージの送受信のためのキュー機能を提供する。
    • キューは複製されている。キューが飛んでデータが吹っ飛ばないようにしている。
    • 全てのキューが同じデータを持つ状態に「収束」していく(イベンチュアル一貫性)
    • キューを読み出した時、その前におくられたメッセージが含まれるとは限らない(FIFOとは言っていない。)

キューもworkerも落ちる可能性がある。

  • 疎結合アーキテクチャに寄与する
    • 提供者と利用者の待ち合わせなし
    • 空いたインスタンスが随時タスクを請負可能
  • 少なくとも一回実行のための仕様(2回以上しないという仕様は含まれていない)
    • 受信されたメッセージは、キューから取り除かれるけど、一定時間後に再びキューに含まれる
    • メッセージを受信してタスクを請け負ったプロセスが落ちる可能性を想定
    • タスクが完了したら、明示的にメッセージ削除をする
    • メッセージを重複して受信、処理がされる可能性がある。(処理中なのにメッセージが復活するケースなど)

AmazonSimpleDB/DynamoDB

イベンチュアル一貫性のあるデータストアで、Read/Writeするとなにがおきるか説明されてあるドキュメント。

ConsistencyのAWSのドキュメント

Amazon DynamoDB

  • 整合性があり、10 ミリ秒未満のレイテンシーを必要
  • すべての規模のアプリケーションに対応した、高速かつフレキシブルなNoSQLデータベースサービス
  • 完全マネージド型のクラウドデータベース
  • ドキュメントとキー値のストアモデルの両方をサポート
  • 支払いはスループットに発生する。1秒間に何回書き込みできるようにするか。

Amazon DynamoDB

料金概要

仮想的な測定指標に対してお金を払う。わかりづらい。

1 つの書き込みキャパシティーユニット (WCU) は、1 秒あたり最大 1 回の書き込みを提供 1 つの読み込みキャパシティーユニット (RCU) は、1 秒あたり最大 2 回の読み込みを提供

1 日に 500 万回の書き込みと500万回の結果的に整合性のある読み込みを実行する必要があるとします

結果的に整合性のある読み込み 常に最新というわけではない。誤差は出てくる。 1秒間に500万アクセスきても大丈夫だし、値段もわかる。 オートスケールさせると値段が見えにくくなってくる。

Amazon DynamoDB では、ユーザー定義プライマリキーを使用したGET/PUTオペレーションがサポート - BatchGetItems - BatchGetItems演算は、複数の項目の属性を複数のテーブルから、それぞれのプライマリキーを使用して返す。16件の応答のサイズ制限は1MBで、最大100個の項目を返す。強い整合性と結果整合性の両方をサポート - 16件の応答のサイズ制限は1MBで、最大100個の項目というクラウドとしては微小量の割合だけど、重い処理をしたときに他のユーザへ影響がでないようにしている。 - BatchWriteItem – 1 回のトランザクションとしてではなく、1回のリクエストにより複数のテーブル間で複数の項目を挿入、置換、削除。PutまたはDeleteで最大 25 項目のバッチをサポートし、合計リクエストサイズは最大16MB。 - UpdateItem – 既存の項目の属性を編集。条件付き演算子を使用して、項目の属性値が所定の条件に一致する場合にのみ更新を実行することも可能 - カウンターのようなもの。GETして値をインクリメントしてPUTする。他の処理でインクリメントされていた場合、上書きするおそれがある。 - この問題にはマルチスレッドではLockをとって対処する。 - クラウドではLockは重いのでやらない、計算式を送り込む。計算式を送り込んで、サーバ側でロックをしてインクリメントすると負荷が低い。

Amazon DynamoDB では、スカラー値でアトミックなインクリメント演算とデクリメント演算を行うことができる。 アトミックとは今の値を読み込んで、+1して書き込むではなく、一度の処理でやる。 その他の書き込みなどは防がない。 タイムアウトなどしたら、書き込みはべき等ではない。 UpdateItemは正確な値を取りたいときには辞めたほうがいい。

Amazon DynamoDBはデータストアだけど、並行システム、マルチスレッドシステムの知識がないと使えない。 ユーザ側に並行システムを持つことに、これらの知識が求められている。

条件付きの書き込み

Aliceとボブがいる。今の価格が10とする。 Aliceが価格を8に更新する。Bobが後で価格を12に変更。するとAliceの価格が上書きされる。 値引きと値上げがタイミングによって起きている。 これを防ぐためにはユーザのプログラミングでUPDATEを投げる時には状況が変わっていることを前提にする。 もし価格が10なら8にする。もし価格が10なら12にする。というAPIを呼ぶ。 こういう可能性が知っていないと、気づけ無いし対応できない。 マルチスレッドプログラミングとおなじ。並行プログラミングもしっていないとだめ。 Lockすれば解決できるけど、ブロックしたくない、誰かのために他のひとを待たせたくないからこういう仕様。 悲観的、楽観的な発想。

DynamoDBにはScanもある。 1回のScanリクエストで、最大1MBのデータを取得 一度の処理で全データをみるのはやめてほしい。

ブロックさせない、他のシステムが遅くさせることはさせない。 プログラミングの不備があると、不整合によってユーザの手元のデータが消える。ユーザに試されている。

Key-Value Storeにおける思想

  • RDB
    • 依存関係の整理(正規化)に基づく設計,保存データ量の最小化
      • ex)「ID→氏名、会社名」「会社名→会社住所」
    • 実行時にSQLクエリに応じた複雑な処理が発生
      • 上の2テーブルを結合してIDから会社住所をひっぱってくる
    • 実行時にコストをとるような考え
  • KVS
    • あらかじめクエリパターンを考慮して、実行時には高速なget/putだけで済むような設計
      • ex)頻発するなら、「ID→会社名、会社住所」DBも、「会社住所→ID,氏名」DBもつくればいい
    • keyから引っ張ってくる、冗長的、書き込む時は書き直さないと正確に値を引っ張ってこれない、

内部版のDynamo

Amazonが使っていたDynamo

要求・仮定

  • 1MB未満くらいのデータの読み書き(keyへのget/put)ができればいい
    • 複数のデータにまたがる操作はない
    • Amazonの通販サイトの多くの部分がこれで動く
  • 可用性のために一貫性をゆるめる(ACIDのC)
    • 一つのkeyに対する読み書きだけ考えていて、一連操作性(I)のための保証は考えていない
  • 分布の99.9%以上に対し、読み書き数百msec未満
  • 一貫性や耐久性と性能などトレードオフは設定可能にしてある
  • 常に書き込み可能に
    • 2フェーズコミットや、イベンチュアル一貫性も使っていない
    • 古典的には、読み込みを軽くし、一貫性のための調整コスト(ブロックや失敗)は書き込みが追う
      • ex)書き込み全・読み込み1のQuorum
  • 一貫性のための競合解決方法は、アプリ開発者が決める柔軟性を残す
    • 「物理時間で最新のものを残す」などだけではなく
  • 少しずつノードを追加可能・対称性を重視(どのノードも同じ)・非中央集権・ノードは異種混合になっている

読み書きの実装

  • "Sloppy"(だらしない)Quorum
    • 読み書きリクエストを受け取ったノードは、N個の複製に送信し、読み書きそれぞれに定められた一定数(R,W)以上の確認をもって成功とする ※必ずR+W >N(全ノード数), 2W>Nにするとは言っていない
  • 書き込みの周知が想定ノードに届かなかった場合、別の代替ノードに送信 (可能であれば元のノードに書き戻す)
  • 耐久性への保証を犠牲にして性能を上げる設定
    • 基本的にはメモリ上のバッファを使って、そこへの書き込み完了を持って確認を返信する
    • バックグラウンドで随時二次記憶に書き込む(その前にクラッシュするとその複製では書き込みが失われる)
    • N個の書き込み周知先のうち、1つだけ二次記憶にすぐに書かせることで耐久性を少し向上

書き込み競合

  • 常に書き込み可能
    • 書き込みを受け付けた後で複製間の同期を行う
    • 並行する書き込みや一時的なネットワーク分断により、複数の「最新」値が存在しうる
  • ベクトルタイムスタンプを使ってバージョンを管理する
    • 把握できる因果順序がある場合、自動的に競合解決可能
  • そうでない場合は、次の書き込み手に意味を考慮した競合解決が委ねられる
  • アプリ依存の競合解決の例
    • ショッピングカート内の商品リストへ追加
    • セッションではなく二次記憶に保存している
  • 以上は論文から読み取れる内容
  • 同じ仕組みで「ショッピングカート内の商品リストから削除」を実現するとどうなるか
    • 削除はどうしているかは不明
    • 操作ログだけをもっているのかもしれない
  • 実際の発生率(複数の最新値が存在するときは)
    • 最新値が1つ:99.94%
    • 2つ:0.0005%
    • 3つ:0.00047%
    • 4つ:0.00009%
    • 最新値2つ以上は殆どbotのせいらしい

CAP定理

  • 複数を用いるアプリケーションは、下記のうち3つのうち2つのみを実現することができる
  • 定理よりかは原理原則
    • Consistency(一貫性):複数全体での結果一致を守ってのデータ更新やその耐久性(Durability)、一貫性の程度もある
    • Availability(可用性):(十分に早く)結果を返答する
    • Partition Tolerance(ネットワーク分断への耐性):システムの一部が分断・故障しても機能する

CAP定理とクラウド

  • First Tier:リクエスト処理を行うWEBページ
    • すぐに応答を返さないといけない。一貫性を妥協して古い情報をみせるとか。APが重要でCを妥協する。
    • ユーザ体験を大事にする気持ち。
  • Second Tier:スケーラブルなKey-Valueデータ
  • Inner Tiers/Backend:データベースやインデックスバッチ分析処理など

  • フロントエンド側において、CよりAPを優先する例

    • バックエンド側と繋がらなくなっても、ユーザにはキャッシュやエラー時用の情報などを見せる
    • 予約状況DBとは別に、残室情報DBをもって、「残り◯室」といった宣伝表示に使うとしても、この残室情報DBは正確な最新値とは限らない
      • ユーザには最新ではない可能性と表示する
      • フロントエンドでキャッシュをもっててもいいかもしれない
    • 人気の限定商品の開始時に、在庫数がある程度減るまでは、システム全体で正確な在庫数を共有することはせず、各複製サーバがどんどん売ってしまう
      • ほんとはACIDやらないといけないけど、ある程度まではだいだいあるってことでガンガン売る
      • Amazonのあれとか

CAP定理の位置づけ

  • バックエンド側ではCが必要
  • Cのための技術は確立されている(RDBとか)のに、Cを実現すると、APが犠牲になるのか
    • 今のRDBははやいので、十分APを実現している論
    • いやいや、想定しているスケーラビリティが違うし、コストが違うと反論
  • CAP定理とは高いスケーラビリティを実現するために一貫性を弱める事を受け入れることが低コストな解であることが多いということ

BASEトランザクション

ACIDすてたもの ACIDに対するBASE

  • Basic Availability;基本的に常に利用可能,部分的にでもリクエストが返ってる(すべての機能ではないかもしれないが)
  • Soft-State Replication:耐久性のあるメモリ上のデータストア(キャッシュ)を活用、2次記憶に書き込むのが正義ではない
  • Eventual Consistency:イベンチュアル一貫性(書き込み操作完了時には、その最新値がシステム内で合意されたとは限らない)

「D.Pritchett(EBay),Base An Acid Alternative,2008」

※少しの不整合やACID違反を「正常動作」と定義することを真剣に検討すべき [Vogles(AmazonCTO)] 何らかの不整合がでることも0ではない

今までの話はクラウドでしかできないスケーラビリティがある。 サーバやアプリをクラウドに置くと安いってだけの話ではない。 技術的には大変だけど全世界に展開するようなWEBスケーラブルなシステムが作れることが可能になった。

REST(Representational State Transfer)

REST(Representational State Transfer)

Webシステムアーキテクチャの原則を定義したもの

原則

  • Client-server
  • Stateless
  • Cacheable
  • Uniform
  • interface
  • Layered system
  • Code-on-demand(optional)

こういう考え方にしたがって作ろうよっていうもの Webシステムのデザインといってよい

以下のものが求められている

  • 可用性
    • いつでもちゃんと返事が来る,応答待ちにならない
  • スケーラビリティー
    • 大量のリクエストを捌ける
  • 耐故障性
    • サーバ側でマシンが落ちていても構わない

WEB系企業が推奨しているけど、原則6個を全て使えとは言っていない。

原則6個が言っていること

  • Client-server:ユーザインタフェースとデータストレージの関心事を分離
  • Stateless:リクエスト内にそれを理解するための情報を全て含み、サーバ側に状態を持たせない
    • スケーラビリティ、監視の容易性、故障からの復帰の容易性
    • ステートフルだとやりとりしていたサーバが落ちたら情報が消える。
  • Cacheable:キャッシュ可能なものを明確に区別
    • 効率、スケーラビリティ
  • Uniform interface:単一インターフェースに統一
    • 単純さ、インターフェースと機能の分離
    • オブジェクト指向プログラミングのメソッドが沢山でてくる。
    • インターフェースはGET、PUT、POST、DELETEの4種類にする。
    • デザインをするのはGET、PUT、POST、DELETEのでしよう。
  • Layered system: 階層構造による不要な詳細の隠蔽
    • 複雑さの軽減、依存性の低減
    • 自分の直下のレイヤーだけを知っていればいい
  • Code-on-demand(optional):必要な機能の、クライアント側へのオンデマンドでの読み込み(オプション)
    • 実行時の拡張性

シェルスクリプト②(declare,配列,連想配列)

declareでの変数宣言

オプションをつけることで、変数に属性を付与できる

オプション 意味
-r 変数を読み取り専用
-i 変数を整数とする
-a 変数を配列とする
-A 変数を連想配列とする

指定しない場合は、型は文字列になる

読み取り専用ならreadonlyコマンドをつかうとよい

配列

配列は要素を並べて格納し、インデックスという番号で参照できるデータ構造。 インデックスは0からはじまる。

  • 配列の作成

配列変数の右辺に()を書いて、要素を記述する。これを複合代入と呼びます。

# fruit=(apple orange banana)

空の配列

# list=()

declareでの配列宣言

# declare -a arr1
  • 要素の参照

配列の要素の参照は${配列名[インデックス]}

# echo ${arr1[0]}

インデックスを指定しない場合は、インデックス0が参照される。

要素の数を取得するにはインデックス"@"を指定 ${#配列名[@}}

# echo ${#arr1[@]}
  • インデックスを利用した代入

bashの配列は連続している必要がないので、空のある配列は複合代入がつくれる。 以下だとインデックス2と4が空文字列の配列ができる。

# arr2=(test0 test1 [3]=test3 [5]=test5 test6 )
  • 配列の中身を変更
# test3=(hoge1 hogehoge)
# test3[1]=hoge2
# 
# echo ${test3[0]}
# echo ${test3[1]}
  • 要素の削除
# unset test3[1]
  • すべての要素の参照

配列のすべての要素を参照する場合は、インデックスに*や@を指定

# test4=(hoge0 hoge1 hoge2 hoge3 hoge4)
# echo ${test4[*]}
# echo ${test4[@]}

""でくくった違いも"$@"と"$"と同じで、 "${配列[@]}"は要素が個別の文字列として展開される "${配列[]}"は配列全体をIFS変数の最初の1文字(たいていはスペース)で連結した1つの文字列として展開される 基本的には"${配列[@]}"を使うのがよい。

配列のコピーに使える

# test4_2=("${test4[@]}")
  • 要素の追加

"${配列[@]}"の他の使い方は、元の配列の先頭や末尾に要素を追加した、新しい配列の作成です

# test4_3=(hoge5 hoge6 "${test4[@]}") # 配列の先頭に要素を追加
# test4_4=("${test4[@]}" hoge7 hoge8) # 配列の末尾に要素を追加
  • 値の存在するインデックスの取得

${!配列名[*]}や&{!配列名[@]}みたいに、配列名の前に!をつけると、配列の中で値が存在するインデックス一覧を取得できる

# echo ${!arr2[@]}

連想配列

連想配列はkeyとvalueのデータ構造 hashとかmapとかdictとか言われるやつ

declare -Aで連想配列宣言をする。 これを忘れると配列になってしまうので、注意。

# declare -A user=([id]=1 [name]=testuser)
  • 要素の参照
# echo ${user[id]}
# echo ${user[name]}
# echo ${#user[@]}
  • 要素の代入

連想配列ではキーを指定して、キーに対応する値があれば上書き。なければキーと値を追加します。

# user[name]=testuser2 # 上書き
# user[country]=Japan  # 新規にキーと値を追加
  • 要素の削除
    • unsetコマンド
# unset user[name]
  • すべての値の参照

  • キー一覧の取得