ねぎ嫌い

始業前に学んだことを小出しに。最近はHacker Newsの人気記事をまとめてみたり。

2017-10-18 Why we switched from Python to Go

原文:getstream.io

PythonからGoを使い始めた経緯のお話。

既に経験のある言語から新しい言語に切り替えるのは大きなステップが必要になる。
streamでは主要なプログラミング言語PythonからGoに切り替えた。
本記事では、なぜPythonからGoに切り替えたのかの理由を述べている。

理由1. パフォーマンス

GoのパフォーマンスはJavaC++と同じくらい良い。
彼らのチームではGoのほうがPythonより30倍も早かった。

理由2. 言語のパフォーマンス

多くのアプリケーションにおいてプログラミング言語
ただアプリケーションとデータベースをつなぎ合わせるノリにすぎない。
言語自身のパフォーマンスはそんなに問題にならない。

彼らは500以上の会社と2億人以上のエンドユーザーにAPIを供給している。
Cassandra、PostgreSQL、Redisなどで最適化しているが、言語上の限界に達してしまった。

Pythonは素晴らしい言語であるが、serialization/deserialization、
ランキング、アグリゲーションといったユースケースでパフォーマンスが出なかった。
Cassandraは1msでデータを見つけるが、Pythonは10msでオブジェクトに詰めていた。

理由3. 開発者の生産性と創造性のなさ

Goのチュートリアルにあった以下のコードを見てほしい。

package main

type openWeatherMap struct{}

func (w openWeatherMap) temperature(city string) (float64, error) {
    resp, err := http.Get("http://api.openweathermap.org/data/2.5/weather?APPID=YOUR_API_KEY&q=" + city)
    if err != nil {
        return 0, err
    }

    defer resp.Body.Close()

    var d struct {
        Main struct {
            Kelvin float64 `json:"temp"`
        } `json:"main"`
    }

    if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
        return 0, err
    }

    log.Printf("openWeatherMap: %s: %.2f", city, d.Main.Kelvin)
    return d.Main.Kelvin, nil
}

このコード例には以下の要素が詰められている。

  • 複数割当(multiple assignments)
  • 構造体
  • ポインタ
  • フォーマッティング
  • ビルトインHTTPライブラリ

もしPythonで同じことをやろうとすると、

  • MetaClassesを使ってクラスの初期化をする
  • True/Falseをスワップアウトする
  • ビルトイン関数のリストに関数を追加する
  • 魔法のメソッド経由でオペレーターをオーバーロードする

これらの機能で遊ぶのは楽しいが、多くのプログラマが同意するように、
多くの場合これらのコードは理解するために読むのが難しくなる。

Goは基本に忠実にさせる。
誰が書いたコードであっても簡単に理解するために読めるようにしてくれている。

NOTE: どれだけ簡単か、はユースケースに因る。単純なCRUD APIであればDjangoDRFRailsを使うほうが良い。

理由4. 同期実行とチャネル

言語として、Goは物事を単純に保とうとする。
goroutineとchannelによって単純な言語を実現している。
goroutineは軽量のスレッド化手法で、
channelはgoroutine間のコミュニケーションを取る優れた方法である。

goroutineは非常に低コストで生成でき、メモリもわずかしか使用しない。
そのため、一度に何百、何千ものgoroutineを走らせることが出来る。

goroutine間の通信にはchannelを使う事ができる。
Goのランタイムが全ての複雑性をコントロールしてくれる。
goroutineとchannelベースの手法が同期実行を簡単に
使用可能な全てのCPUコアを使用させ、同期IOを扱う。
Python/Javaと比べ、Goは最低限の定型文のみが必要になるだけである。

理由5. 短いコンパイル時間

JavaC++に比べて非常に短い時間でコンパイルすることが出来る。

理由6. チームを構成する力がある

Goを書ける開発者はC++Javaといった古い言語の開発者よりも多くはない。
StackOverflowによると38%の開発者はJavaを書け、19%がC++を書けるが、
Goに関して言えば4.6%しかいない。
Githubのデータも似たような傾向を示している。

幸いにもGoは非常に簡単かつシンプルに学ぶことが出来る言語である。
基本的な機能を提供し、必要なものはそれ以外にない。
その簡潔さでPythonをかける開発者を1ヶ月以内に効率的に出来る。

そのため、他の言語に比べチームを構成するのに容易い。

理由7. 強力なエコシステムがある

20人以下の規模のチームにおいてエコシステムは問題になる。
小さな改革を起こさなければ、顧客に価値を提供することは出来ない。
Goは我々の使用しているツールに対してサポートをしている。
充実したライブラリは既に揃っていて、RedisやRabitMQ、PostgreSQL
テンプレートエンジンやタスクスケジューリング、文法解析などがある。

GoのエコシステムはRustやElixirといった他の新しい言語に概ね勝っている。
Javapython、Nodeなどには負けているが、
必要としている基本的なライブラリは既に使用可能である。

理由8. コードの強制フォーマッティング

Pythonにおけるautopop8のような強制フォーマッティングツールが備わっている。
タブ - スペース論争のようなものはもう必要ない。
Gofmtが全てを解決してくれる。

理由9. gRPCとプロトコルバッファ

GoはプロトコルバッファとgRPCを快適にサポートしている。
これらの2つのツールはRPC経由でのマイクロサービスを立てるのに適している。
マニフェストを書くだけで、RPC呼び出しを使える。
サーバーとクライアントのコードはマニフェストから自動生成される。

同じマニフェストから、クライアントのコードをJava, C++, PythonRubyで生成できる。
これ以上曖昧なRESTのエンドポイントは必要ない。

Goが優れていない点

不利1. フレームワークの不足

Goはまだ支配的なフレームワークを有していない。
これはGoコミュニティ内で白熱した論争のトピックとなっているが、
今のところフレームワークを使うべきではない、という。
もしシンプルなCRUD APIを立てたかったら、 DjangoRailsを使うほうが良い。

不利2. エラーハンドリング

Goでは、呼び出し元のコードでエラーをハンドリングする必要がある。
そのため、エンドユーザーに対して意味のあるエラーを返すのを簡単に忘れてしまう。

エラーのパッケージはこの問題をエラーにコンテキストとスタックトレースを追加することで解決している。

他の問題として、簡単にアクシデントによるエラー処理を忘れてしまう。
errcheckやmegacheckのような静的解析ツールはこのミスを便利に防いでくれる。

不利3. パッケージ管理

Goのパッケージ管理は決して完璧ではない。
デフォルトでは依存するライブラリのバージョンを指定する方法はなく、
再現性のあるビルドをする方法もない。
Python, Node, Rubyのいずれも優れたパッケージ管理システムを持っている。
しかし、正しいツールを使えばGoのパッケージ管理もかなり良い。

Depを使って依存性を予定したバージョンで管理することが出来る。
それとは別にVirtual Goというオープンソースツールもある。

2017-10-16 Why PostgreSQL is better than MySQL

原文:Why PostgreSQL is better than MySQL |

PostgreSQLMySQLよりも優れているお話。

どんな製品にもバグはつきものである。
PostgreSQLのコミュニティでは、発見されたバグは直ぐに修正されるだろう。
PostgreSQLのコミュニティでは常にバグをトラッキングするかは大きな議論になるからだ。

しかし、MySQL#199にあるような単純なバグが14年間も直されずに放置されてきた。
これが明確に優れている理由である。

2017-10-16 Efficient pagination of a SQL table with 100M records

原文:allyouneedisbackend.com

巨大なデータを持つテーブルを効率的に読み込む方法についてのお話。

1億行のレコードを持つテーブルからデータを読み込む時、どのように読むべきか。

1. 明らかに間違った解決策

SELECT user_id, external_id, name, metadata, date_created FROM users;

シンプルに上のようにやってしまうと、処理は終わらない。

おそらく、取得するデータをすべてRAMに展開するためである。
あるいは、データを送る前のプリロードに時間がかかり、クエリーがタイムアウトをしている。
どちらにしろ、この方法でデータを取得することは出来ない。

2. ページング

SELECT user_id, external_id, name, metadata, date_created FROM users
ORDER BY user_id ASC LIMIT 0, 10 000;

データをページに入れて取得する方法がある。
データの取得順は物理的/論理的に保証されないため、ソートする必要がある。
これの実行時間は10 000 rows in set (0.03 sec)で行われている。

それでは、5,000ページ目を取得するとどうだろうか。

SELECT user_id, external_id, name, metadata, date_created FROM users
ORDER BY user_id ASC 
LIMIT 50 000 000, 10 000; --- 5 000th page * 10 000 page size

この実行時間は、10 000 rows in set (40.81 sec)となる。
確かに、めちゃめちゃ遅い。

さらに最後のページを取ろうとするとどうなるか。

SELECT user_id, external_id, name, metadata, date_created FROM users
ORDER BY user_id ASC
LIMIT 99 990 000, 10 000; --- 9999th page * 10 000 page size

実行時間は、10 000 rows in set (1 min 20.61 sec)となる。
実際に使うには、バックグラウンドで動くようなタスクにしか使えない。

実行時間のほかにもう1つ問題になりうるケースがある。
例えば、既に10ページめくっている状態(100,000 件のデータにアクセス)で、
次のページ(100,001 から 101,000)へアクセスしようとしている時を考える。
そのタイミングで、99,998と99,999レコードが消されてしまったら、
ページの最初の結果は、100,003からになってしまう。

つまり、ミュータブルなデータセットの場合、この方法は使えないということになる。

3. indexを利用したページング

一つ前と非常に似ているが、レコード数でページングをするのではなく、
indexのついたuser_idをオフセットとして利用している。
アルゴリズムとしては、以下のようになる。
1. テーブルからレコードのPAGE_SIZEを取得し、オフセットを0とする 2. 次ページのために取得したuser_idの最大値をオフセットとする 3. 現在のオフセットよりも大きいuser_idを取得する

1ページあたり10,000件のデータを持たせ、5,000ページ目を取得する場合は次のようになる。

SELECT user_id, external_id, name, metadata, date_created FROM users
WHERE user_id > 51 234 123 --- value of user_id for 50 000 000th record
ORDER BY user_id ASC
LIMIT 10 000;

驚くことに実行時間は10 000 rows in set (0.03 sec)とあるように、1000倍も速い。
user_idの値は連続的ではなく、例えば25345の次は25348となっている。
これならば、読み込む次のデータが消されても問題なく動作する。

まとめ

この記事では1億行のレコードを持つテーブルを読み込む時に、
プライマリーキーを指定しているオフセットを使うこととしている。

2017-10-14 Steve Wozniak announces tech education platform Woz U

原文:techcrunch.com

Appleの共同創立者として知られるSteve Wozniak氏が
オンライン教育のプラットフォーム「Woz U」をローンチした。

最初のカリキュラムはコンピューターサポートの専門家と
ソフトウェア開発者がターゲットになっている。
また、データサイエンス、モバイルアプリケーション、
サイバーセキュリティも近い生来追加される予定。

Wozniak氏によると、Woz Uの目的は、
働くことの出来る人にデジタルスキルの教育と訓練を
何年もかけずに行うこととしている。

Google API をJava経由で叩く時にログを出す

com.google.api.client.http.HttpRequest内でログレベルがLEVEL.CONFIGだったらログを出すとしている。

  public HttpResponse execute() throws IOException {
      //省略
      Logger logger = HttpTransport.LOGGER;
      boolean loggable = loggingEnabled && logger.isLoggable(Level.CONFIG);
      //省略
      if (loggable) {
            logger.config(logbuf.toString());
      }     
      //省略
  }

結論としては、動かしているJREのlib以下にあるlogging.propertiesを編集すれば良い。

handlers= java.util.logging.ConsoleHandler

java.util.logging.ConsoleHandler.level = CONFIG
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

こうなっていれば出るはず。

最終的に以下のような出力が得られる。
ここではGSuiteのAdmin SDKを利用した際のログを示す。

2017/10/12 17:42:04 com.google.api.client.http.HttpRequest execute
設定: -------------- REQUEST  --------------
GET https://www.googleapis.com/admin/directory/v1/groups/${group_mail}/members?maxResults=200
Accept-Encoding: gzip
Authorization: Bearer ${token}
User-Agent: GoogleApps-Test Google-API-Java-Client Google-HTTP-Java-Client/1.20.0 (gzip)

2017/10/12 17:42:04 com.google.api.client.http.HttpRequest execute
設定: curl -v --compressed -H 'Accept-Encoding: gzip' -H 'Authorization: Bearer ${token}' -H 'User-Agent: GoogleApps-Test Google-API-Java-Client Google-HTTP-Java-Client/1.20.0 (gzip)' -- 'https://www.googleapis.com/admin/directory/v1/groups/${group_mail}/members?maxResults=200'
2017/10/12 17:42:05 com.google.api.client.http.HttpResponse <init>
設定: -------------- RESPONSE --------------
HTTP/1.1 200 OK
ETag: "${etag}"
X-XSS-Protection: 1; mode=block
Expires: Thu, 12 Oct 2017 08:42:07 GMT
Server: GSE
X-Content-Type-Options: nosniff
Cache-Control: private, max-age=0, must-revalidate, no-transform
X-Frame-Options: SAMEORIGIN
Alt-Svc: quic=":443"; ma=2592000; v="39,38,37,35"
Transfer-Encoding: chunked
Vary: X-Origin
Vary: Origin
Date: Thu, 12 Oct 2017 08:42:07 GMT
Content-Encoding: gzip
Content-Type: application/json; charset=UTF-8

2017/10/12 17:42:05 com.google.api.client.util.LoggingByteArrayOutputStream close
設定: Total: 111 bytes
2017/10/12 17:42:05 com.google.api.client.util.LoggingByteArrayOutputStream close
設定: {
 "kind": "admin#directory#members",
 "etag": "\"${etag}\""
}