世界線航跡蔵

Mad web programmerのYuguiが技術ネタや日々のあれこれをお送りします。

2015年04月07日

gRPC-JSON proxy

grpc-gatewayというgRPCからJSON APIへの変換プロキシ生成機を書いた。 これを使えばシステム内部ののmicroservicesはgRPCで通信しつつ公開APIはJSON APIで提供する、みたいなことが簡単になる。

なお、gRPCそのものについてはmattnさんの記事が参考になる。

背景

gRPCの良い点はいくつもある。

  • データはデフォルトでprotocol buffersで直列化される。ベストではないにせよ十分にコンパクト且つ高速だし、サイズで言えばJSONとは比べるべくもない。
  • 簡単に複数の言語でサーバーのテンプレートやクライアントを生成できる。通信の詳細はgRPCにまかせて開発者はサーバーロジックの実装に注力できる。
  • design by Googleという安心感。

gRPCの素晴らしさは認めるものの、一方では欠点もある。まず、クライアントライブラリの多くはCで書かれたバイナリ拡張を含む。 これはRuby界ではあまり歓迎されていないし、多くのユーザーにAPIを使ってもらうということを考えればどうしたってクライアントは利用言語ネイティブで書かれていた方が良い。 また、そもそもgRPCはまだそんなに普及していない。gRPCがサポートしていない言語だってなくはない。

だからコントロールの効く範囲のシステム内部の通信にgRPCを使うのは極めて妥当な選択だが、現時点で公開APIをgRPCだけで提供するっていう決断はAPI提供者としてはなかなか勇気が要る。

だったら公開APIはJSONのままでいきたいというのは自然な発想だ。 gRPCのJSONシリアライゼーション機能を使っても良いのだけど、それでもHTTP 2.0必須だし、呼び出しパスもちょっと独特なのでちょっとまだ敷居が高い。

そこで、コモディティとしての従来型のRESTful JSON API over HTTP 1.xがやっぱり必要なわけだ。grpc-gatewayはgRPCで書いたサービスを簡単にそうしたRESTful JSON APIに変換してくれる。

実装

grpc-gatewayはgRPCそのものと同じく、protoc (protocol buffers compiler)のプラグインとして実装されている。 gRPCサービスを定義してある.protoファイルに専用のカスタムオプションでちょっとだけ情報を足すと、protoc-gen-grpc-gatewayプラグインがgRPCとJSON APIを仲立ちするreverse proxyを生成してくれるようになる。

カスタムオプションには次のような情報を指定できる

  • path: このメソッド呼び出しをマッピングするHTTP requestのpath
  • method: 同じくHTTP method
  • description: 備考

生成されるproxyはgolangだが、バックエンドのgRPCサービスとはgRPCで通信さえ出来れば良い。よってgRPCサービスのほうは普段通り好きな言語で書けば良い。

現時点ではgRPCのstreaming機能はサポートしていないが、これはすぐにサポートする予定だ。サポートした

そのほかにもREADME.mdに書いてあるようないくつかの機能をサポートしていきたいと思っている。 中でもSwagger API definitionの出力は是非対応したいポイントだ。protoからAPI definitionは作るからあとはJSON API clientはswaggerで勝手に作れ、という風にしたい。

トラックバック

http://yugui.jp/articles/889/ping

現在のところトラックバックはありません

コメント閲覧/投稿

2015年03月30日

Managed VMsに移行

今までこのblogはVPS上で動作させてきたが、この度Managed VMsに移行した。 Wikiと全文検索は一時的に利用不能になっているが、他はそのまま動作しているはずだ。

主な動機はいまどき個人blogのためだけにシステムを管理するのは厭ということである。 慣れているのと安いのでManaged VMsにしたが、システムをDockerizeしてあるので、必要であれば少しの手間でElastic Beanstalkでもどこでも移動できる。

難点としては、このblogにはVMがオーバースペックということだ。Managed VMs(elastic beanstalkも同じだった筈だが)はdockerコンテナとそれを走らせるVMを1:1にmapするので、この程度の負荷だとリソースが余ってしまう。複数のコンテナ(モジュール)でシステムを構成しようとするとVMの利用料がかなり無駄である。本当はN:Mにマップして5個ぐらいに分割したモジュールを2個ぐらいのVMで走らせたいんだけど。

そのへんの柔軟性についてはKubernetesを使いたかったが今回は諦めた。 というのは、Kubernetesクラスタを走らせたままKubernetes管理daemonをバージョンアップする仕組みがまだできていないので、本業ならともかく片手間に管理するにはミドルウェアの更新コストが高いためだ。 いちいちクラスタをもう一個立ち上げて、システムをre-deployしてDNSを更新して古いやつを消すのは面倒すぎる。 この問題(kubernetes#6075)もいずれは解決するであろうから、そのときはGoogle Container Engineに再度移行するだろう。

トラックバック

http://yugui.jp/articles/888/ping

現在のところトラックバックはありません

コメント閲覧/投稿

2015年03月24日

言語処理100本ノックを敢えてRubyで (3)

引き続き、言語処理100本ノックを敢えてRubyで解いている。

第3章は正規表現がテーマだが、正規表現はそこまで得意ではないので辛い。マッチの効率とか深く考えていない。

20. JSONデータの読み込み

require 'json'
uk = File.foreach("jawiki-country.json").
  map(&JSON.method(:parse)).
  find {|article| article['title'] == 'イギリス' }
raise "no article about イギリス" unless uk
puts uk['text']

感覚的なものだけど、この場合は&obj.method(:mid)形式の簡潔さが見慣れない分のデメリットを上回る気がした。

21. カテゴリ名を含む行を抽出

File.foreach('uk.txt').grep(/\[\[Category:/) do |line|
  puts line
end

この辺はRubyのほうがPythonよりsedやawkの影響が色濃く残っていて簡潔に書けるかなぁ。

22. カテゴリ名の抽出

pattern = /
  \[\[
    Category:
    ([^|\]]+)
    (?:\|[^\]]*)?
  \]\]
/x
File.read("uk.txt").scan(pattern) do |cat,|
  puts cat
end

正規表現がとても読みづらくなったのでせめてxオプションで複数行に分けてみた。

23. セクション構造

File.foreach("uk.txt") do |line|
  next unless m = /^(={2,})\s*([^=]+)\s*\1/o.match(line)
  puts "level=#{m[1].size - 1}, name=#{m[2]}"
end

24. ファイル参照の抽出

pattern = /
  \[\[
    File:
    ([^|\]]+)
    (?:\|[^\]]*)*
  \]\]
/x
File.read("uk.txt").scan(pattern) do |cat,|
  puts cat
end

Categoryの場合とほとんど変わらないと思うんだが、出題意図を読み違えてないよね?

25. テンプレートの抽出

def params
  templatePattern = /
    {{基礎情報\s+国\s+
      (?<params>
        (?<markup>
          {{\g<markup>}} |
          [^{}]*
        )*
      )
    }}
  /x

  article = File.read("uk.txt")
  raise "no 基礎情報 found" unless m = templatePattern.match(article)
  base = m[:params]

  Hash.new.tap {|result|
    base.split(/^\|/).each do |entry|
      next if entry.empty?
      name, value = entry.split(/\s*=\s*/, 2)
      result[name] = value.chomp
    end
  }
end

if $0 == __FILE__
  p params
end

String#scanで切り出すのとnamed captureはあまり相性が良くないことが判明。 最初に基礎情報を取り出す部分は田中哲スペシャルのおかげで簡潔に再帰構造をサポートできている。

一方、テンプレートパラメータの抽出はMediaWikiの文法を正確に表してはいない。だが、今回はパラメータ区切りは常に行頭から始まるのでとりあえず十分である。

26. 強調マークアップの除去

require_relative '25.rb'

def params2
  Hash.new.tap {|result|
    params.each do |name, value|
      result[name] = value.gsub(/('{2,3}|'{5})([^']+)\1/, '\2')
    end
  }
end

if $0 == __FILE__
  p params2
end

27. 内部リンクの除去

require_relative '26.rb'

def params3
  Hash.new.tap {|result|
    params2.each do |name, value|
      result[name] = value.gsub(/\[\[ ([^\|\]:]+) (?:\|([^\]]+))? \]\]/x) do
        $2 || $1
      end
    end
  }
end

if $0 == __FILE__
  p params3
end

28. MediaWikiマークアップの除去

require_relative '27.rb'

def params4
  Hash.new.tap {|result|
    params3.each do |name, value|
      value = value.gsub(/{{([^\|}]+)(?:\|([^\|}]+)?)*}}/) { $2 || $1 }
      value = value.gsub(%r!<ref(?:\s[^/>]+)?>.*?</ref>!m, '')
      value = value.gsub(%r!<ref(?:\s[^/>]+)?/>!, '')
      value = value.gsub(%r!<[[:alpha:]]+\s*/>!, '')
      value = value.gsub(%r!&pound;!, '£')
      result[name] = value
    end
  }
end

if $0 == __FILE__
  p params4
end

MediaWiki記法は結構いろいろあるのでやり出すとキリが無い。とりあえずこんな感じだろうか。 遊びでやるにはそろそろ辛くなってきた。

実務でやるなら、目的を満たす程度のminimalなMediaWiki parserを書いてちゃんとunit testを書くべきだろう。

29. 国旗画像のURLを取得する

require 'json'
require 'open-uri'
require_relative '28.rb'

name = params4['国旗画像']
url = "http://ja.wikipedia.org/w/api.php?format=json&action=query&continue=&titles=Image:%s&prop=imageinfo&iiprop=url" % URI.escape(name)
result = JSON.parse(open(url) {|f| f.read })
puts result['query']['pages']['-1']['imageinfo'].first['url']

むぅ。MediaWiki APIってのがあるのか。

トラックバック

http://yugui.jp/articles/887/ping

現在のところトラックバックはありません

コメント閲覧/投稿

ご案内

最近の記事

タグ一覧

過去ログ

  1. 2015年04月
  2. 2015年03月
  3. 2015年02月
  4. 2014年11月
  5. 過去ログ一覧

フィード

フィードとは

その他

Powered by "rhianolethe" the blog system