出退勤管理システムのAPI endpointをCrystalに置き換える
背景
前回の記事でDjangoとNFCリーダーを用いた簡単な出退勤管理システムを紹介しました。
しばらく動作させていたところ、Raspberry piのCPU使用率が天井に張り付いてしまっていました。(動作がめちゃくちゃ重くなった)
CPU使用率は常時50%となっていましたが、熱対策のためのスロットリングが働いて50%が天井に設定されていると考えられます。実際本体をさわってみると長時間は触っていられないくらいの温度になっていました・・・
対策
各プロセスのCPU使用率を見てみるとDjangoが30%、NFCリーダーのプログラムが15~20%、ngrokとmysqlが数%、といった感じになっていました。*1
DjangoとNFCリーダーのプログラムを両方移行したかったのですが、NFCリーダーについてはnfcpy以外にまともに使えるライブラリがないのでひとまずDjangoをCrystalに置き換えてみることにします。さらにmysqlを利用するほどのことでもないと思い、これもjsonファイルに置き換えました。
CrystalにはARMアーキテクチャ向けコンパイラは用意されていませんが、x86_64環境でクロスコンパイルをすることにしました。
環境構築
まず、手元のMacbookにCrystalのコンパイラをインストールします。
$ brew install crystal
また、Raspberry piにはLLVM3.8がインストールしてあり、この環境でCrystalの実行形式ファイルをリンクするには1.0系のopensslが必要だったため、これもインストールしました。
$ sudo apt install libssl1.0.2
プログラム
製作したプログラムはGithubに公開しています。
Crystalというプログラミング言語はマイナーではありますが最近とある界隈で話題になっていて*2日本語の記事も少しずつ増えているので探してみてください。
Crystalは静的型付き言語なので、JSONのような型が不明なデータをあつかうのが意外と大変です。CrsytalではJSON#parse
によってJSON::Any
というクラスに変換してからif文とis_a?
メソッドをつかって型を決定していく方法と、JSON#mapping
によって予めJSONの型を指定しておく方法があります。今回はLINEのAPIドキュメントによってあらかじめJSONの型がわかるようになっているのでJSON#mapping
を書いておきます。
json_map.cr
require "json" module Response class Result JSON.mapping({ events: Array(Event), }) end class Event JSON.mapping({ type: String, timestamp: Int64, source: Source, replyToken: { type: String, nilable: true }, message: { type: Message, nilable: true }, }) end class Source JSON.mapping({ type: String, userId: { type: String, nilable: true }, groupId: { type: String, nilable: true }, roomId: { type: String, nilable: true }, }) end class Message JSON.mapping({ id: String, type: String, text: { type: String, nilable: true }, fileName: { type: String, nilable: true }, fileSize: { type: String, nilable: true }, title: { type: String, nilable: true }, address: { type: String, nilable: true }, latitude: { type: Float64, nilable: true }, longitude: { type: Float64, nilable: true }, packageId: { type: String, nilable: true }, stickerId: { type: String, nilable: true }, }) end class Reply JSON.mapping({ replyToken: String, messages: Array(ReplyMessage), }) def initialize(@replyToken : String, @messages : Array(ReplyMessage)) end end class Push JSON.mapping({ to: String, messages: Array(ReplyMessage), }) def initialize(@to : String, @messages : Array(ReplyMessage)) end end class ReplyMessage JSON.mapping({ type: String, text: { type: String, nilable: true }, fileName: { type: String, nilable: true }, fileSize: { type: String, nilable: true }, title: { type: String, nilable: true }, address: { type: String, nilable: true }, latitude: { type: Float64, nilable: true }, longitude: { type: Float64, nilable: true }, packageId: { type: String, nilable: true }, stickerId: { type: String, nilable: true }, }) def initialize(@type : String) end def add_text(@text : String) self end end class Cardid JSON.mapping({ cardid: String, }) end class CardNames JSON.mapping({ cardNames: Array(CardName), }) end class CardName JSON.mapping({ idm: String, name: String, }) end end
上記のソースがJSONのパースの仕方を記述したものです。含まれていないかもしれない値にはnilable
というクラス変数にtrue
を代入して置かなければなりません。
さらに、nilである可能性がある値を利用する場合にはnilでないことが保証されなければなりません。
以下が利用例になります。
event = Response::Result.from_json(body.gets_to_end).events[0] message = event.message if message.is_a?(Response::Message) puts "message event!" groupId = event.source.groupId roomId = event.source.roomId if groupId.is_a?(String) puts "Group ID:#{groupId}" text = message.text replyToken = event.replyToken if text.is_a?(String) && replyToken.is_a?(String) replyMessage(replyToken, text, list) end elsif source.roomId.is_a?(String) puts "Room ID:#{roomId}" else puts "User ID:#{event.source.userId}" end else puts "other event!" end
結果
DjangoとMySQLを停止した結果、CPU使用率が17,8%程度まで抑えられました。Herokuにデプロイするという選択肢もありましたが、最近Crystalを主に書いていたのと、Raspberry piでCrystalのプログラムを動かしてみたかったのでこの選択肢を選んでいます。
NFCリーダーについても、未だ高めのCPU使用率になっているので、他の選択肢があるなら随時置き換えていきたいと考えています。