Node vs. Go : Roadomatic の実装における比較

  • このエントリーをはてなブックマークに追加

   

bitmap

目次

  • 概要
  • サーバ運用
  • ラウンド1: リクエスト処理
    • UDPソケット
    • リクエストの検証
  • ラウンド2: データベースへの問い合わせ
  • ラウンド3: パフォーマンス
  • 結論

概要

前回の記事で概要を述べた通り、僕たちはUAE大学電気工学のシニア・プロジェクトで、Roadomaticというシステムを構築している。このシステムの目的は、ロケーションベースの道路交通情報を、リアルタイムでドライバーに配信できるようにすることだ。

Roadomaticは、2つの主要構成要素からなる。Android OSを対象としたクライアント・アプリケーションと、僕自身が実装した、Nodeで書かれたサーバだ。

もともと、僕たちの最優先事項は効率性だった。例えば、このシステムの通信プロトコルはUDP上のJSONで構成されているのだが、これは符号化と復号化のしやすさや、やりとりごとに使用される約200バイトのデータを考慮してのことだ。

この記事では、主にバックエンドについて話すつもりだ。特に、僕がNodeとGoで書いたサーバの、機能的には全く同じ実装2つを、比較していきたいと思う。

サーバ運用

バックエンドは2つの主要部からなる。

  • UDPサーバ
  • データストレージ用のMongoDBデータベース

サーバ運用の一般的な流れは次の通りだ。

  1. ソケットがUDPデータグラムを受信する
  2. データグラムの中身がJSONで復号化され、座標図形が保存される
  3. データベースにクエリが送信され、受信した座標が表す道路を見つける
  4. レスポンスオブジェクトが作成されたら、JSONで符号化される
  5. レスポンスのバイトを含んだデータグラムが回線を通じて送り返される

ご覧のとおり、現状、このシステムのサーバサイド処理は最小限に抑えられている。だが先々の変更で、より多くの処理をサーバに任せることになるかもしれない。

では、この処理はNodeとGoそれぞれでどのように実装されたのか?そして、この2つの実装は、互いに比較してどのくらい難しかったか?

ラウンド 1:リクエスト処理

僕たちが最初、データをJSONで送信することを選んだ理由の一つに、僕たちがかつてバックエンドで構築していた技術スタックがある。NodeとMongoDBだ。これらは両方とも、JSON(もしくは技術にウルサイ人に言わせるとBSON)に依存するところが大きい。だから案の定、データの取り扱いやデータベースへの問い合わせをNodeで行うのは非常に容易だったのだ。

しかしながら、Goで同じことを成し遂げるのは、はるかに簡単だった。Goが比較的、低級言語であることを考えると、「複雑な」オペレーションの多くがGoで簡単になるというのは、僕にとって嬉しい驚きだった。

UDPサーバ自体については、NodeもGoも標準ライブラリがあって、プロセスを大いに単純化してくれた。

UDP ソケット
始める前に、データグラム受信時におけるポートのリッスンと実行コードに関して、2つの言語を比較してみたいと思う。

// Socket setup
var socket = dgram.createSocket('udp4');
socket.bind(config.server_port, config.server_host);

socket.on('listening', () => {
  console.log('Listening on %s:%d...', config.server_host, config.server_port);
});

socket.on('message', (req, remote) => {
  console.log('Message #%d received!', (++c));

  processRequest(req, remote, socket);
});

socket.on('error', (error) => {
  console.log(error.stack);
  socket.close();
});

// Request handler
var processRequest = (req, remote, socket) => {
  // ...
}

Goでやった方法を見てみよう。

func main() {
  // Socket setup
  addr := net.UDPAddr{
    Port: PORT,
    IP: net.ParseIP(HOST),
  }

  conn, err := net.ListenUDP("udp", &addr)
  if (err != nil) {
    panic(err)
  }
  defer conn.Close()

  for {
    // Create a buffer for each request
    buf := make([]byte, 1024)

    // Read bytes into buffer (blocking)
    b, addr, err := conn.ReadFromUDP(buf)
    if (err != nil) {
      panic(err)
    }

    // Spawn a goroutine for each request
    go udpHandler(buf, b, n, conn, addr)
  }
}

func udpHandler(buf []byte, b int, ...) {
  // ...
}

リクエストの検証
次に、リクエストの妥当性チェックがどのように実装されているかを見てみよう。
両方とも、上で示した2つのコードの続きだ。
Nodeでは以下が、受信リクエストBufferが妥当かどうかをチェックするコードだ。

var processRequest = (...) => {
  try {
    parsed = JSON.parse(req);

    // Check for correct params
    if (!parsed.lat || !parsed.lng) {
      throw Error();
    }
  } catch (e) {
    console.log(e.stack);
    valid = false;
  }

  if (valid) {
    // Continue processing
  }
}

Goでは、Bufferの代わりに[]byteをチェックしているが、考え方は同じだ。

type Request struct {
  Lat float64 `json:"lat"`
  Lng float64 `json:"lng"`
}

func udpHandler(...) {
  // Slice buffer depending on read bytes, trim spaces
  clean := bytes.TrimSpace(buf[:b])

  // Parse received JSON
  r := Request{}

  err := json.Unmarshal(clean, &r)
  if err != nil {
    fmt.Println(err)
    return
  }

  // Continue processing
}

結果:引き分け
行数は同じくらいで、両バージョンとも、NodeやGoにそれほど詳しくない人にも分かりやすいと思う。

ラウンド 2:データベースへの問い合わせ

今回は、JSONをネイティブサポートしていることや、非同期プログラミング形式という点で、Nodeのほうが優れていると予想できそうだ。

まず、コードでやることを簡単に説明しよう。データベースに検索クエリを2つ作成する必要がある。

1つ目は、MongoDBの地理空間機能とGeoJSONを使って、示された座標のある道路区分を決定すること。

2つ目のクエリでは、各segmentとともに保存されているroad_idフィールドを使って、道路の詳しい情報、厳密には、通りの名前を抽出する。

Nodeの実装ではmongodbパッケージを使っており、一方Goバージョンはmgoに頼っている。

var findRoad = (parsed, db, callback) => {
  const resp = newResponse();

  // GeoJSON coordinate representation
  const loc = {
    type: 'Point',
    coordinates: [parsed.lng, parsed.lat]
  };

  var segments = db.collection(config.segments);

  // Perform query on SEGMENTS collection
  // Find the road segment that contains Point `loc`
  const q = {
    shape: {
      $geoIntersects: {
        $geometry: loc
      }
    }
  };

  segments.findOne(..., (err, segment) => {
    if (err) {
      // Collection read error
      console.log(err);
      resp.online = 0;
      callback(resp);
    } else if (!segment) {
      // No segment match!
      callback(resp);
    } else {
      // Segment match!
      var roads = db.collection(config.roads);

      // Find road name using road_id
      roads.findOne(..., (err, road) => {
        if (err || !road) {
          // Read error, return what we have1
          console.log(err);
          resp.name = "";
        } else {
          resp.found = 1;
          resp.speed = segment.speed;
          resp.name = road.name;
        }

        callback(resp);
      });
    }
  });
};

う~ん、asyincがクリーンにしてくれるとはあまり思えない。

同じことをGoでどのようにやったか見てみよう。

func findRoad(req *Request) Response  {
  r := Response{Online: 1}

  lat, lng := req.Lat, req.Lng
  loc := bson.M{
    "type": "Point",
    "coordinates": []float64{lng, lat},
  }

  // Query database, retrieve road_id
  session, err := mgo.Dial(fmt.Sprintf("mongodb://%v:%d", MONGO_HOST, MONGO_PORT))
  if err != nil {
    r.Online = 0
    fmt.Println(err)
    return r
  }
  defer session.Close()

  segments := session.DB(MONGO_DBNAME).C(MONGO_SEGMENTS)
  s := Segment{}

  filter := bson.M{"_id": 0, "speed": 1, "road_id": 1}
  query := bson.M{
    "shape": bson.M{
      "$geoIntersects": bson.M{
        "$geometry": loc,
      },
    },
  }

  err = segments.Find(query).Select(filter).One(&s)
  if err != nil {
    fmt.Println(err)
    return r
  }

  roads := session.DB(MONGO_DBNAME).C(MONGO_ROADS)
  road := Road{}

  err = roads.Find(bson.M{"_id": s.RoadId}).One(&road)
  if err != nil {
    r.Online = 0
    fmt.Println(err)
    return r
  }

  r.Found = 1
  r.Speed = s.Speed
  r.Road = road.Name

  return r
}

結果: Goの勝ち
僕はGoに熟達しているわけではないけれど、上のような例は、Goを使ったエラーチェックの素晴らしさを物語っていると思う。少なくとも僕にとっては、Goバージョンのほうがはるかにクリーンに見える。

もちろん、Nodeバージョンでもう少しエレガントなコードを書けたのでは?と思う方は、遠慮なく下にコメントを残していってほしい。というかいっそのこと、Githubで僕たちにプルリクエストを送って!

ラウンド 3:パフォーマンス

これは、テスト結果をまとめた表だ。応答時間にはクライアント(ここにある)からサーバ(ここにある)までのRTT(ラウンドトリップタイム)が含まれていることにご留意いただきたい。

メモリ使用量 応答時間 コンパイル サイズ 依存関係
Node 25 MB 150 ms mongodb
Go 1.5 MB 150 ms 6 MB なし

結果: Goの勝ち

今回は明らかに、Goの圧勝だ。

Goの実装でのメモリ使用量には、ただただ驚くばかりだ。制限のあるサーバでバックエンドの実行ができる。僕たちの場合、512MBのコンピュータでNodeバージョンを実行しているが、メモリ使用はほぼ限界ギリギリだ。MongoDBに必要なメモリを考えると、できることなら数MBを節約したいところだ。

結論

要するに、サーバベースのアプリケーションを書くなら、NodeもGoも素晴らしい選択だ。しかし、僕たちの特殊な使用事例においては、Goがほぼあらゆる点においてとにかく優れていた。

僕たちは現在、まだNode実装をテストに使用しているが、最終的にRoadomaticを製品展開していくとなれば、Goバージョンに換えていくことになりそうだ。

いずれにせよ、NodeからGoにシステムを移植したことは個人的に素晴らしい経験だった。僕はいつもGoを学びたいと思っていたし、今回のこの経験は、とにかくやってみるには最高の口実になった。

原文:http://assil.me/2015/11/07/roadomatic-node-vs-go.html (2016-1-15)
※元記事の筆者には直接翻訳の許可を頂いて、翻訳・公開しております。

 -Tech, ,

FAworksではプロのコンサルタントが案件をお探しします

  関連記事

Criteoにおける大規模機械学習の仕組み

Criteoの事業の核を担うのは、機械学習です。当社は、広告を表示させたいときの選択や、個別の製品レ

基本的なシステム性能とOSジッタを計測するためのツールキット

Linux サーバの基本的なシステム性能とOSジッタを計測するためのツールキット

Jean Dagenaisは、mechanical-sympathyのスレッドで、Gil Teneの

Javaアプリケーションのパフォーマンスを(ほぼ)自動的に上げる方法

Javaアプリケーションのパフォーマンスを(ほぼ)自動的に上げる方法

コードを書き換えずに簡単な手順をいくつか踏むだけで、複雑なJavaアプリケーションを10%以上スピー

広告あるある 〜第一弾〜

by Klearchos Kapoutsis こんにちは、今回はネット広告について、あるあるを書きま

待ち遠しい次の祝日がコマンドラインでわかる!‐cal‐ 端末にカレンダーを表示しよう

待ち遠しい次の祝日がコマンドラインでわかる!‐cal‐ 端末にカレンダーを表示しよう

これは“コマンドライン・マンデー”シリーズの最初の投稿です。このシリーズでは、毎週月曜日に使えるコマ

もう二度と、絶対にMongoDBを使うべきじゃない理由

もう二度と、絶対にMongoDBを使うべきじゃない理由

MongoDBは悪だ。なぜならそれは… …データを無くす(ソース:1、2)。 …実際、長期間、デフォ

エンジニアとして実力を持って生きていくなら絶対取るべき資格

エンジニアとして何年か生きているのですが、そんな中で「これは受けて良かった! 勉強して良かった!」と

【比較表あり】非エンジニアの人にも知ってほしい。エンジニアに優しいチャット・コミュニケーションツールまとめ

エンジニアに合ったコミュニケーションツール プロジェクトを円滑に進行させるためにも、チームでのコミュ

MySQL Cluster―MySQLはいかに2億QPSのスケーリングを実現したか

この記事は、オラクル社MySQL主任プロダクトマネージャ、アンドリュー・モルガン氏からのゲスト投稿で

継続的デリバリがもたらす効果と価値とは~ソフトウェア業界全体の「対応力を高める」トレンドを追え~

継続的デリバリがもたらす効果と価値 ~ソフトウェア業界全体のトレンド “React” を追え~

あなたが「リリース」という言葉を聞いた時、どのような感情が呼び起こされるだろうか?安堵?高揚感?ある