読者です 読者をやめる 読者になる 読者になる

uyamazakのブログ

仕事中の問題と解決メモ。PythonとGCPがメイン。bizoceanで新規事業の企画と開発担当。 BigQueryに全てのデータをぶち込むプロジェクト進行中 https://github.com/uyamazak/oceanus

RedisのlistとpubsubとRabbitMQを使い分けを考える

2017年1月現在、ビッグデータ処理プロジェクトoceanusは下記のようなデータの流れをしています。


f:id:uyamazak:20170120102719p:plain


GEK上でDockerを使ってアプリケーションを構成していますが、Redisのリスト型、pubsub型に加えて、最近RabbitMQも使い始めたので、どう使い分けしているかを整理してみる。

Redis list型

順番を持ったリストで、左から入れたり、右から入れたり、逆に取り出したりすることができる。

リスト型 — redis 2.0.3 documentation

用途

データを失いたくない1対1のデータ処理。

oceanusでは、armsでHTTPリスエストをバリデーション等をした後にlistに保存し、r2bq(Redis to BigQueryの略)が取り出して、BigQueryに保存している。

BigQueryに保存したらもう必要がなくなり消えるので、基本的にRedisに保存されているデータは少ない。

BigQueryが落ちているなど保存できない場合は、再度listに戻す処理を行っている。


Redis PubSub

情報発信側publisherに対して、複数の受信側subcriberが登録することができる。

Sub側は登録以前のものや接続が遮断されてる間のデータは失われる。

GoogleのCloud PubSubGoogle Cloud Pub/Sub Documentation  |  Cloud Pub/Sub  |  Google Cloud Platformは、データの再送、保持などもしており信頼性は高いけど、実際に使ってみた所、認証や通信でレスポンスが悪くWEBサーバーが遅くなるため、自分でRedis立てて使いました。

用途

流れてくるデータをいろいろな所で使いたい時。
フィルターをかけて、条件Aに合致したらメール、条件Bに合致したらスプレッドシート等。

oceanusでは、データはBigQueryにすべて保存されるので、データが失われてもいいような通知用途などに使っています。

RabbitMQ

メッセージキューイングのミドルウェア

メッセージといってもメールやチャットのような人が読むものではなく、シリアライズ化したコードなど機械同士のやりとりが基本です。

銀行などでも実績があり、貯めたタスクを処理するワーカーを別プロセスで走らせることができ、耐障害、非同期、分散、スケールなどを用意に実現できます。

AWSでは、似たようなSQSがあります。
aws.amazon.com

用途

タスクの非同期処理に使用しています。

PythonCeleryから使っており、送りたいタスクを関数にし、デコレータを付けるだけで使用できます。

CeleryはブローカーとしてRedisも使うことができますが信頼性、対応する機能などからRabbitMQが推奨されてるようです。

公式Dockerイメージを使えば、特に難しい設定もなく使うことができました。


現在は、特定の条件(エラーを意味するものとか)で、Googleスプレッドシートに保存したり、コンバージョン通知からそのユーザーの履歴をBigQueryから取ってきてメールに付けて送るなどのタスクをどんどん投げて処理させています。

ワーカーはデフォルトでは10と多くなっており、スプレッドシートにアクセスしすぎて、繋がらなくなるなどもあるので、1秒に1回などリミットを掛ける必要がありました。

リトライ、オートスケーリングなど多数の機能がありまだ使いこなせていない感。

逆にこれを使っておけばあとで困ることはなさそう。


昔はDBに保存してcronで処理などもあったと思いますが、そのロジックを自分で書くのは大変だし、スケーリングや多重起動の防止などいろいろ面倒なので、こういうミドルウェアを積極的に使った方が昔の自分に勝てます。

まとめ

最初はRedisとBigQueryだけでしたが、間にPubSub、RabbitMQを入れることにより、疎結合を保ちながら、リアルタイム処理や、重い処理の非同期処理などさまざまな形でデータを利用することができるようになりました。

プログラマとして、使える道具はどんどん増やしていくべき。


Redis入門 インメモリKVSによる高速データ管理

Redis入門 インメモリKVSによる高速データ管理

Mastering RabbitMQ

Mastering RabbitMQ

KubernetesがServiceのIPなどを勝手に環境変数に入れてくれてる

ビッグデータ処理のプロジェクトoceanusで、セットした覚えのない環境変数がセットされており、それがたまたま他のコンテナで使っている変数名と同じだったため、原因の特定に時間がかかってしまった。



RabiitMQを使うことになって、それを使うコンテナには、RABBITMQ_PORTという自分で付けた名前の環境変数を使っていた。

ポートはデフォルトのまま5672を使っているが、あとで変更できるようにPython側では下記のように呼び出していた。

from os import environ

RABBITMQ_PORT = environ.get('RABBITMQ_PORT', 5672)

getを使うことで、RABBITMQ_PORTがない場合は、デフォルトの5672が使われるように書いていたつもりで、ローカルでは問題なく動いていた。

しかし、Kubernetes(Google Container Engine)にアップすると、RABBITMQ_PORTに下記のような、ポート番号だけでなく、Kubernetes上のローカルIPまで含めた値が入ってしまい、エラーが起きていた。

tcp://10.7.251.6:5672

調べてみたら、Kubernetesでは、同一クラスタ内のサービスに繋げるように、それぞれのホスト名、ポート番号などを自動で環境変数にいれてくれるようだ。

ドキュメントにも書いてあり、完全に見落としていた。

kubernetes.io

ドキュメントにあるように例えば、redis-masterというservice名だったら、下記の環境変数がすべてセットされる。

REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11

今回自分のアプリケーションでは、たまたまサービス名と、それから生成される環境変数が同じになってしまっていた。

ここらへんの環境変数を整理して、この環境変数名でアプリケーション側から呼び出すようにすれば、新規作成時にServiceのIPを指定しなくても、一気につながるようにできるはず。


WEBサーバー、DBサーバー、キャッシュサーバー、MQサーバーなど、Kubernetes内のネットワークが増えてきたら、この変数名を使ってyamlを書いたりするのが必須になりそう。

Kubernetes: Up and Running; Dive into the Future of Infrastructure

Kubernetes: Up and Running; Dive into the Future of Infrastructure

Pythonでリトライ処理を考える

ビッグデータ処理のために作ってるoceanusでは、受け取ったデータをRedis、BigQuery、Google SpreadSheet、SendGrid(メール)など外部に送ることが多く、残念ながら外部とのやりとりはコントロールできない不確定要素が多い。

そのため、いろんなところでリトライ処理が必要になる。

リトライ時の間隔については別記事で書いたので今回は省く。
uyamazak.hatenablog.com

これまで使ったリトライ方法3つをまとめた。
どんなアプリケーションかによって正解は異なると思う。

エラーをtryで取って、except節でリトライ

一番シンプルなのがこれ。

try:
    result = task()
except Exception as e:
    print("error and retry")
    # retry
    result = task()

もしくは処理結果をif文で。

result = task()
if not result:
    # retry
    result = task()

まあ失敗してもいいけど、一度はリトライしてみるか、程度のあんまり重要じゃないところや、2回以上リトライが必要ない場面ではこれでいいかもしれない。
欠点としては、リトライ回数を増やせないこと。

入れ子にすれば何回でもできないことはないけど、読みにくいし、ダサすぎる。

ループで回す

forなどでリトライ回数分回し、成功次第抜ける。
While True:は無限ループ恐怖症なのか避けるようにしている。

ループ回数や、ループ中の処理などは自由に書ける。

大分端折ったけど、BigQueryや外部のサーバーを使うときは下記のような処理を書いた。
リトライ中ではエラー、全部ダメだったときはクリティカルの出し分けを付けた。
実際はprintではなく、raiseやログ出力している

tryのelse節(tryにエラーが無い時だけ実行する)は、初めて使ったかもしれない。あまり見ないので推奨できないかも。
最初のリトライ回数が0だと、感覚的に違うので、rangeは1から、制限回数にも+1して見やすいようにしている。

from time import sleep
CONNECTION_RETRY = 3

def task_with_retry():
    for i in range(1, CONNECTION_RETRY + 1):
        try:
            result = task()
        except Exception as e:
            print("error:{e} retry:{i}/{max}".format(e=e, i=i, max=CONNECTION_RETRY))
            sleep(i * 5)
        else:
            return True
    print("critical")
    return False
||<<


** パッケージrertyを使う

[https://pypi.python.org/pypi/retry/:embed:cite]
まんまretryというパッケージpypiに公開されている。


使用する関数にデコレータで利用できるので、コードをシンプルに保てて賢い。


>|python|
from retry import retry
@retry()
def make_trouble():
    '''Retry until succeed'''
@retry(ZeroDivisionError, tries=3, delay=2)
def make_trouble():
    '''Retry on ZeroDivisionError, raise error after 3 attempts, sleep 2 seconds between attempts.'''
@retry((ValueError, TypeError), delay=1, backoff=2)
def make_trouble():
    '''Retry on ValueError or TypeError, sleep 1, 2, 4, 8, ... seconds between attempts.'''
@retry((ValueError, TypeError), delay=1, backoff=2, max_delay=4)
def make_trouble():
    '''Retry on ValueError or TypeError, sleep 1, 2, 4, 4, ... seconds between attempts.'''
@retry(ValueError, delay=1, jitter=1)
def make_trouble():
    '''Retry on ValueError, sleep 1, 2, 3, 4, ... seconds between attempts.'''
# If you enable logging, you can get warnings like 'ValueError, retrying in
# 1 seconds'
if __name__ == '__main__':
    import logging
    logging.basicConfig()
    make_trouble()
<||

冒頭の記事に書いたリトライ間隔のジッター、loggerなども引数で指定可能
>|python|
def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger):
    """Return a retry decorator.

    :param exceptions: an exception or a tuple of exceptions to catch. default: Exception.
    :param tries: the maximum number of attempts. default: -1 (infinite).
    :param delay: initial delay between attempts. default: 0.
    :param max_delay: the maximum value of delay. default: None (no limit).
    :param backoff: multiplier applied to delay between attempts. default: 1 (no backoff).
    :param jitter: extra seconds added to delay between attempts. default: 0.
                   fixed if a number, random if a range tuple (min, max)
    :param logger: logger.warning(fmt, error, delay) will be called on failed attempts.
                   default: retry.logging_logger. if None, logging is disabled.
    """

なるべく使用するパッケージは少なくしたいけど、リトライする箇所が増えて、このオプションでなんとかなるのなら、いちいち自分で書かずにこれを使った方がいいと思う。


実践力を身につける Pythonの教科書

実践力を身につける Pythonの教科書

Effective Python ―Pythonプログラムを改良する59項目

Effective Python ―Pythonプログラムを改良する59項目

Dockerのイメージから過去のソースコード等を救出する

DockerとGKE(Google Container Engine)を開発に使うようになってから、gitだけでなく、Dockerのイメージもバージョンで保存されるようになった。

ソースコードを変更後、動作確認のためにDockerイメージをビルドし直すことが多いので、頻度としては、gitへのコミットよりも、Dockerのビルドの方が多い。

そのため、gitにコミットする前に、ファイルを消してしまった、変更してしまった場合、Dockerのイメージからファイルを救出することができる。

私の普段の開発の流れは下記のよう。
まだ自動テスト、自動デプロイが必要な規模にはなってないので、最後2つの順番は変わったりする。

  1. ソースコードの追加、修正
  2. Dockerイメージのビルド
  3. 開発環境でテスト
  4. gitコミット
  5. 本番環境へアップ


そして、月曜午前中に出社して、寝ぼけてコミット前なのに「git resetしちゃった!」というとき。

落ち着いて下記のようなコマンドを叩く。
grepでなくてもdocker imagesコマンドのfilterなどを使ってもいい

>>% sudo docker images | grep arms
asia.gcr.io/oceanus-dev/arms latest 0f8530abb371 5 weeks ago 278.5 MB
asia.gcr.io/oceanus-dev/arms v20161207-01 0f8530abb371 5 weeks ago 278.5 MB
asia.gcr.io/oceanus-dev/arms v20161124-01 f302e57a0ebb 7 weeks ago 278.5 MB
asia.gcr.io/oceanus-dev/arms v20161115-02 7f2fe84bb244 8 weeks ago 278.5 MB
asia.gcr.io/oceanus-dev/arms v20161115-01 be7d629f9186 8 weeks ago 278.5 MB
asia.gcr.io/oceanus-dev/arms v20161111-02 75ce674e8eed 9 weeks ago 278.5 MB
asia.gcr.io/oceanus-dev/arms v20161111-01 4bbecf26285f 9 weeks ago 278.6 MB
asia.gcr.io/oceanus-dev/arms v20161107-alpine03 b21bdc36165b 9 weeks ago 278.6 MB
asia.gcr.io/oceanus-dev/arms v20161107-alpine01 6a890b567b5a 10 weeks ago 278.6 MB
asia.gcr.io/oceanus-dev/arms v20161107-alpine02 6a890b567b5a 10 weeks ago 278.6 MB

【復旧済み】github落ちてる

https://github.com/

こんなん出た
f:id:uyamazak:20170112170433p:plain

unicornなのは、Rubyアプリケーションサーバー(正確にはRackWebサーバ?)のunicornのデフォルトエラー画面なのだろうか。

もはやインフラとなってるGithubクラスのサービスがこういうの出すとは以外だった

GitHub実践入門 ~Pull Requestによる開発の変革 (WEB+DB PRESS plus)

GitHub実践入門 ~Pull Requestによる開発の変革 (WEB+DB PRESS plus)

KubernetesでThe edited file had a syntax error: unable to decode "edited-file": [pos 907]: json: expect char '"' but got char '5'

Google Container Engine上のKubernetesで、kubectl edit deploy でyamlをいじっていたら、ポート番号の指定でエラーになった

The edited file had a syntax error: unable to decode "edited-file": [pos 907]: json: expect char '"' but got char '5'


該当箇所は下記

    spec:
      containers:
      - env:
        - name: RABBITMQ_HOST
          value: 10.7.251.6
        - name: RABBITMQ_PORT
          value: 5672

ポート番号はjsでいうとINT型だけど、テキストで使う""で囲わないといけないらしい。
yamlはあんまり触らないので知らないけどそういう仕様なんだろうか。
数字とドットだけどIPはそのままで行ける。

言われたとおりに下記のように、数字を""で囲ったらエラーはなくなった

    spec:
      containers:
      - env:
        - name: RABBITMQ_HOST
          value: 10.7.251.6
        - name: RABBITMQ_PORT
          value: "5672"

Kubernetes: Up and Running; Dive into the Future of Infrastructure

Kubernetes: Up and Running; Dive into the Future of Infrastructure

Go言語によるPython実装Grumpyで世界がGoogleに染まっていくところまで妄想した

opensource.googleblog.com

GoogleのGo言語によるPython実装「Grumpy」が公開され話題になっています。

github.com

www.itmedia.co.jp
news.mynavi.jp
codezine.jp


Go言語は入門書を一つ買って、ちょっと触っている程度だけど、実行速度、並列処理のしやすさ、シンプルさから今後作るサービスに取り入れていきたいと思っています。

スターティングGo言語

スターティングGo言語

でも、Go言語だけでWEBサービスを作ろうと思うと、PythonだとDjangoRubyRailsみたいな、ありがちなのは全部用意されてる感はまだ無く、例えば会員登録とか、管理画面とか、RabbitMQみたいなMySQLなどと比べると少しマイナーなミドルウェアを使おうとかになってくると非常に面倒くさく感じます。

なので今のところ新規WEBサービスでは、個人の力量的にメインはPythonで書いて、局所的に処理が重い、またはレスポンス速度を求められるところはGoで書き、APIとしてマイクロサービス的に組み込んでいくのがいいと思っています。マイクロサービスは、Dockerと合わせれば管理もしやすいし。そういえばDocker自身もGo言語で書かれています。

またはGoogleの記事のYoutubeのようにすでに大量のユーザーを抱えている場合は、Goの処理速度と並列処理を活かすことができるので、費用対効果は非常にいいと思います。同時アクセスユーザーが数十人程度のサービスでは、サーバー台数も少ないし、他のところのオーバーヘッドが大きくてペイできないと思う。


で話がそれたけど、どのように世界(大げさ、インターネット周りといった方がいいか)がGoogleに染まっていくかの妄想を書いてみます。

1、C言語で書かれていたものがGo言語になっていく。

Go言語はGoogleが作った新しいC言語だと思っています。C言語からGo言語に変えて得られるのは、JavaのようなVMいらずで、Cと同等(未検証)の処理速度を持ちながら、使いやすい並列処理、現代的なパッケージ周りの便利さなどがあると思います。

DockerやKubernetesなど、OSに深く関わるアプリもGoで書かれ普及し始めています。

今までCが最適だったものがGoになるのはそう遠くないと思います。Go言語は、WEB開発者にも広がってきているので、ブラウザ専用からNode.js等サーバー用途にまで広がったJavaScriptのように、動く場所とともに需要も増えていきそうです。

Goでインタプリタを書くのも出てきてる。
interpreterbook.com


将来Goで書かれたOSなんかも出てきたらすごい。

2、PythonもGoになっていく

Googleの記事で、Pythonで書かれたYouTubeGrumpyでGo言語に変換とされていたので、しばらくはPythonで書いていくのかもしれませんが、そのうち最初からGo言語でいいじゃん、になる時が来そうです。
Pythonの方がいろいろ便利な点はまだまだありますが、2言語を使いわけるというコストが、1言語で済むというメリットを越す場面は、どんどん出てきそう。

研究用途ではNumpyなど大量の資産があるために、Pythonの用途すべてがGoになるのは今はまだ想像できませんが。

Go言語によるWebアプリケーション開発

Go言語によるWebアプリケーション開発

3、そして世界がGoogleに染まっていく

間を省きすぎで大げさですが、インターネット周りの技術はすでにGoogle抜きでは語れません。

話題のDockerもGoogleが作ったGo言語で書かれているだけでなく、Dockerが使っているコンテナ技術の一部cgroupもGoogle製。今後中心になりそうなHTTP/2もGoogleのSPDYが元になったり。

また、ビックデータでは、HadoopGoogleMapReduceが元だし、データの解析基盤ではコストと速度でBigQueryが抜きん出ていて、機械学習でもTensorFlowもGoogleが公開し使用例も増えてきました。

身近なところではメールはGmail、携帯もAnoroid、ニューラルネットワークで人間の言語まで高精度な翻訳を行うことでGoogleが入ってきて、[http://jp.techcrunch.com/2016/11/23/20161122googles-ai-translation-tool-seems-to-have-invented-its-own-secret-internal-language/:title=内部では新たな言語まで作り始めました。

子会社だけどポケモンGOユーザーはGPS情報を常にONにし、Google(のサーバー)に送るようになりました。


そう、スマホでもパソコンでもIOT機器でも、いまではなんでもGoogleの技術が入っています。

あらゆる端末から人間に入っている情報が、Googleを直接的に間接的に通っていると思うと、世界の一部はすでに染められているといっていいかもしれません。

クラウド開発徹底攻略 (WEB+DB PRESS plus)

クラウド開発徹底攻略 (WEB+DB PRESS plus)