なぜ私達は Python から Go に移行したのか

Why We Switched from Python to Go

translated on

新しい言語に移行するのは常に大きな決断です。その言語をよく知る人がチームメンバーに1人しかいない時などは特にそうです。今年の初め、我々は Stream の主要言語を Python から Go に切り替えました。この記事では、なぜ私達が Python から Go に移行しようと決断したのか、その理由を説明します。

Go を使う理由

理由1 - パフォーマンス

Go は速いです! Go は極端に速い。そのパフォーマンスは Java もしくは C++ に匹敵します。私達のユースケースでは、Go は Python より30倍速いです。Go と Java を比較したベンチマークはこちらです。

理由2 - 言語パフォーマンスの問題

多くのアプリケーションにとって、プログラミング言語は、単にアプリとデータベースを繋ぐものにすぎません。言語そのもののパフォーマンスは通常あまり重要ではありません。

しかしながら Stream は、500の会社と200万人のエンドユーザ向けにニュースフィードのインフラを提供する API プロバイダーです。私達は Cassandra、PostgreSQL、Redis 等を長年最適化していますが、だんだん使用している言語の限界に近づいています。

Python は素晴らしい言語ですが、serialization/deserialization、ranking そして aggregation のような用途に関して、パフォーマンスがかなり悪いです。私達は、Cassandra がデータ取得に1ミリ秒かかり、 Python がそれをオブジェクトに変換するのにさらに10ミリ秒費やす、というようなパフォーマンス問題にしばしば出くわしました。

理由3 - 開発者の生産性が高く、いい意味で創造的でないこと

How I Start Go tutorial から持ってきた Go のコードを見てください。(これは素晴らしいチュートリアルで、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
}

あなたが Go の初心者なら、このコードを読んだ時に大して驚くことはないでしょう。この例では、複数の戻り値を変数に格納すること、データ構造、ポインタ、フォーマット、そして組み込みの HTTP ライブラリを示しています。

最初にプログラミングを始めたとき、私は Python のより進んだ機能を使うのが好きでした。Python ではとても創造的にコードを書くことができました。例えば、こんな感じです。

  • MetaClass を使って初期化時にクラスを self に登録する
  • True、False の入れ替え
  • 組込み関数のリストに関数を追加
  • 特殊関数経由で operators をオーバーロード
  • @property デコレータを使って関数をプロパティとして使う

これらの機能は使うのが楽しい反面、多くのプログラマーが同意するように、他人の書いたコードを理解するのがより難しくなります。

Go はあなたを基本に忠実にしてくれます。他人のコードがとても読みやすくなり、書いてあることがすぐに理解できます。

注: どのくらい「簡単」かは、もちろんあなたのユースケースに依存します。もしあなたが基本的な CRUD API を作成したかったら、私は Django + DRF もしくは Rails をお勧めします。

理由4 - 並列処理とチャンネル

言語として、Go は物事をシンプルに保とうと試みています。新しいコンセプトは導入していません。焦点を置いているのは、信じられないほど速く、作業がしやすいようなシンプルな言語を作ることです。革新的な唯一の分野は goroutine と channel です。(1977年にスタートした並列システムの理論 CSP のコンセプトを100%正しくしておくために、古いアイデアへのより多くの新たなアプローチがあります。)
goroutine は Go の軽量なスレッド、そして channel は、goroutine 間で通信するための方法です。

goroutine は作るのがとても簡単で、メモリを数キロバイトしか消費しません。また、goroutine はとても軽いので、同時に何百もしくは何千も動かすことが可能です。

また、channel を使うと goroutine 間の通信が可能になります。Go ランタイムが並列処理に必要な複雑な処理を代わりに担ってくれます。goroutine と channel を用いた並列処理は、複雑な開発を除いて、CPU のリソースを効率的に利用し、並列の IO を扱うことを大変簡単にします。Python や Java と比べて、goroutine は最小限のコードしか必要としません。関数呼出し時に "go" というキーワードを関数に付け加えるだけです。

package main

import (
    "fmt"
    "time"
)
func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }

}

func main() {
    go say("world")
    say("hello")
}

https://tour.golang.org/concurrency/1

Go の並列処理はとても簡単です。Node
が非同期コードを如何に制御するかに注意を払わなければならない一方で、Go は簡単に扱うことができます。

Go における並列処理のもうひとつの優れた面は、race detector です。これは、非同期コードの中に複数のスレッドによる同時アクセスで起こるデータの競合があった場合に、簡単に発見できるものです。

こちらは Go と channel を始めるのに良い情報源です。

理由5 – 速いコンパイル時間

Go で書かれた私たちの一番大きなマイクロサービスのコンパイル時間は現在6秒です。Go のコンパイル時間の速さは、コンパイル時間が遅いことで有名な Java や C++ のような言語と比較して大きく優れています。コンパイル時間が長いのでちゃんばらをする時間がありましたが、Go にするとその時間がなくなりました。自分が書いたコードが何のためだったのか憶えているうちに物事が片付く方がはるかに素晴らしいです。

速いコンパイル時間

理由6 – チームを構築する能力

これは明らかな事実ですが、C++ や Java のような古い言語に比べて、 Go の開発者の数は多くありません。StackOverflow によると、開発者の 38% が Java を知っており、C++ は 19.3%、Go は 4.6% しか知りませんでした。GitHub のデータも同様の傾向を示しています。Go は Erlang、Scala や Elixir のような言語よりも幅広く使われていますが、Java や C++ ほど人気はありません。

幸いなことに、Go はとてもシンプルで習得しやすい言語です。Go は基本的な機能を提供するだけで、他には何もありません。Go が導入した新しいコンセプトは、defer 文と並列処理を管理する組み込みの goroutine と channel です。(注 : Go はこれらのコンセプトを実装した最初の言語ではなく、これらを普及させた最初の言語ということです。) Go はシンプルなので、Python 、Elixir 、C++ 、Scala もしくは Java の開発者なら誰でも1ヶ月以内に Go が使えるようになるでしょう。

私達は、他の言語と比較しても Go 開発者のチームを構築するのが容易であることに気が付きました。もし Boulder や Amsterdam のような競争が激しい地域で採用活動をしているなら、これは重要な利点です。

理由7 - 強いエコシステム

20人程度までのチームの話です。もし、全ての小さな機能まで再実装しなければならないとしたら、顧客に対して価値を提供することはできません。Go は我々が使うライブラリ(Redis、RabbitMQ、PostgreSQL、テンプレートのパース、タスクスケジューリング、語句解析、RocksDB)をすでに用意していました。

Go のエコシステムは、Rust もしくは Elixir のような他の新しい言語と比較して優れています。それはもちろん Java、 Python、Node ほど良くはありませんが、高品質のパッケージの中からあなたの必要なものを見つけることができるでしょう。

理由8 – Gofmt による強制的なコードフォーマット

Gofmt が何かというところから始めましょう。Gofmt は Go のコードを整形のためにコンパイラに入っている素晴らしいコマンドラインユーティリティです。機能面から言えば、それは Python の autopep8 にとても良く似ています。我々のほとんどはタブとスペースについての議論が好きではありません。フォーマットが一貫していることは重要ですが、実際のどのフォーマットを使うのかはそれほど重要ではありません。Gofmt はコードをフォーマットするための公式ツールを用意することで、この議論を回避しています。

理由9 – gRPC と Protocol Buffers

Go は Protocol Buffers と gRPC サポートが素晴らしいです。これらの2つのツールは、RPC 経由で通信する必要のあるマイクロサービスに対して非常に機能します。あなたは、RPC call と引数を定義した manifest を書くだけです。サーバとクライアントのコードはこの manifest から自動的に作成されます。作成されたコードは共に速く、非常に小さなネットワーク帯域で、使い易いです。

同じ manifest から、 C++、Java、 Python、Ruby といった他の言語向けの RPC クライアントコードを作成することができます。そのため、クライアントとサーバで毎回ほとんど同じコードを書く必要はなく、内部トラフィック向けの REST エンドポイントも必要ありません。

Go を使うことによる不利益

不利益1 – フレームワークの欠如

Go には Ruby に対する Rails、Python に対する Django、PHP に対する Laravel のような主要なフレームワークがありません。多くの人々が最初にフレームワークを使うべきではないと主張しているので、これは Go コミュニティの中でも激論となっています。いくつかのユースケースについては、この考えが正しいことに全面的に賛成です。しかし、もしシンプルな CRUD API を構築したいときは、Django/DRF、Rails、Laravel もしくは Phoenix を使った方がはるかに簡単です。

追記: コメントで指摘があるように、Go のフレームワークは存在します。Revel、Iris、Echo、Macaron、Buffalo などがメジャーです。ですが、Stream のユースケースではフレームワークを利用しないことを選んでいます。シンプルな CRUD API を提供しようとしているプロジェクトではフレームワークの欠如は深刻な問題でしょう。

不利益2 – エラーハンドリング

Go は、関数がエラーを返し、呼び出す側のコードがそのエラーを制御してくれる前提でエラーハンドリングしています(もしくはそれをスタックに返す)。このアプローチは有効ですが、ユーザーに対して具体的なエラー内容を伝えることはできません。errors package は、errors に context と stack trace を加えることによってこの問題を解決します。

もう一つの問題は、エラーハンドリングを忘れてしまうことです。errcheck や megacheck のような静的分析ツールはこれらのミスを手軽に防ぐことができます。

これらの方法はうまく機能しますが、それが正しいとは思いません。言語自身の適切なエラーハンドリングのサポートを期待しましょう。

不利益3 – パッケージ管理

Go のパッケージ管理は決して完璧ではありません。デフォルトでは、依存パッケージのバージョンを指定することができないので、再現可能なビルドを作成する方法もありません。Python、Node、Ruby は良いパッケージ管理のシステムを持っています。しかしながら、適切なツールを使えば、Go のパッケージ管理は極めてうまく機能します。

Dep を使うことで、バージョンを指定し、依存関係を管理することができます。それとは別に、Go で書かれた複数のプロジェクトの作業を容易にする VirtualGo と呼ばれるオープンソースツールもあります。

VirtualGo

Python vs Go

私達は、Python で書かれたランク付けされたフィードを Go で再実装するという実験をしました。以下の ranking method の例を見てください。

{
    "functions": {
        "simple_gauss": {
            "base": "decay_gauss",
            "scale": "5d",
            "offset": "1d",
            "decay": "0.3"
        },
        "popularity_gauss": {
            "base": "decay_gauss",
            "scale": "100",
            "offset": "5",
            "decay": "0.5"
        }
    },
    "defaults": {
        "popularity": 1
    },
    "score": "simple_gauss(time)*popularity"
}

Python も Go もこの ranking method を実装するには以下の処理が必要です。

  1. score 用の語句を解析する。この例では、"simple_gauss(time)*popularity" という文字列の部分を、activity を入力に、score を出力で返す関数に変換したい。
  2. JSON の設定に基づいた部分関数を作成する。例えば、"decay_gauss" を呼び出す "simple_gauss" を5日間、1日のオフセットと 0.3 の減衰係数の尺度で生成したい。
  3. activity 上で field 定義されていない場合に fallback できるよう、"defaults" の設定を解析する。
  4. feed 内の全ての activities を得るため、ステップ1から関数を使う。

Python バージョンの開発には約3日かかりました。それにはコードの生成、ユニットテストとドキュメントの作成が含まれます。次に、コードを最適化するのに約2週間かかりました。最適化のひとつは score expression (simple_gauss(time)*popularity) を抽象構文木に変換することでした。また、特定の時間に score を事前に計算するようなキャッシュロジックを実装しました。

それに対し、このコードの Go バージョンの開発は、約4日かかりました。パフォーマンスとしてはそれ以上の最適化は必要ありませんでした。そのため、初期バージョンの開発は Python のほうが早くすみましたが、最終的にチームでの作業量は Go ベースのほうが圧倒的に少なくなりました。加えて、Go のコードは高度に最適化された Python のコードよりも約40倍速くなりました。

以上が Python から Go に移行したことで経験したパフォーマンスに関する1つの例です。もちろん、単純に比較はできません:

  • ranking コードは Go で書かれた最初のプロジェクトでした。
  • Go のコードは Python のコードよりも後に開発されたので、ユースケースは良く理解されていました。
  • Go の語句解析のライブラリは非常に優れていました。

我々のシステムの他のコンポーネントは、Go で開発する方が Python に比べて実質的に時間がかかりました。一般的には、Go での開発は若干努力が必要です。しかし、パフォーマンスのためにコードを最適化する時間は圧倒的に少ないでしょう。

Elixir vs Go

私たちが比較したもう一つの言語は Elixir です。Elixir は Erlang 仮想マシン上でビルドされます。Elixir は興味深い言語で、チームメンバーの1人が Erlang を多く経験しているので、やってみることになりました。

私たちのユースケースでは、Go の素のパフォーマンスのほうが遥かに上でした。Go も Elixir も何千もの並行処理のリクエストをこなすでしょう。しかし、個別のリクエストのパフォーマンスを見れば、Go はユースケース上、実質的に速いです。Elixir よりも Go を選ぶもう一つの理由は、エコシステムです。私たちが要求したコンポーネントでは、Go はより成熟したライブラリを持っていましたが、多くのケースで Elixir のライブラリは本番運用の準備が整っていませんでした。また、Elixir で作業する開発者をトレーニングしたり見つけたりするのが難しいのです。

これらの理由から Go を採用することにしました。ですが、Elixir 向けの Phoenix フレームワークは素晴らしいように見えますし、見る価値はあります。

結論

Go は並列処理をサポートするとても効率の良い言語です。C++ や Java のような言語とほぼ同じくらい速いです。Python や Ruby に比べて開発するのに少し時間がかかりますが、コードを最適化するのにとても多くの時間を節約できるでしょう。

Stream は、2億人以上のエンドーユーザー向けにニュースフィードを提供する小さな開発チームです。優れたエコシステム新規開発者の容易な参入高速なパフォーマンス並列制御のサポート、そして生産的なプログラミング環境の組み合わせにより、Go は素晴らしい選択肢といえるでしょう。

Stream は、ダッシュボード、ウェブサイト、そしてパーソナライズされた feed に Python を使用しています。私たちはすぐに Python に別れを告げるつもりはありませんが、パフォーマンスを重視するコードは Go で書かれることになるでしょう。もし Go についてもっと知りたければ、下記のブログ記事をチェックしてください。Stream についてもっと知るために、このチュートリアルが素晴らしい出発点になります。

Go への移行についてもっと知る

Go を学ぶ