ArchitectureをやってみたRustちょっとしたネットワークリクエストを伴うツールを書く事があり、せっかくなのでRustでレイヤードアーキテクチャでやってみることにしました。github.comフォルダを分けてモジュールレベルで分離するのではなく、Cargoのworkspaceをつかって各レイヤーをcrateレベルで分離する事で各層の分離をより強化するというアプローチをとってみました。Domain層の責務は、ドメイン層のレベルで利用するモデルのStructと、リポジトリのTrait、そしてData層での実装を意図したRepositoryのProviderのTraitです。モデルについてもTraitで提供するというのも考えましたが、完全に独立したStructで定義し、この値を構成する責任はData層に委任するという形にしてみました。このあたりは、Traitで提供するアプローチについてもやってみたいとおもいます。Data層では、以下のようなRepositoryを実際に実装した値を提供する責務のProviderを実装する事によってApplication層で使えるように提供します。RustではDIフレームワークはほとんどつかわれておらず、自分で実装するというのが一般的なようです。Trait>パターンで隠蔽する事になり、このあたりは記述量がかなり増えて大変な印象がありました。repository::SampleModelRepository>;}これを実装したRepositoryを提供するためにData層で実装して、Data層からはこのDataModuleだけを外部に公開するようにします。model);こんな感じで利用する事で、Domain層で提供されているインタフェースの実装の詳細を知らずに利用する事ができました。思いきって購入しました。DE0-CVwww.youtube.comマルツオンラインから購入しました。これらのツールはWindows環境か、Linux環境向けのみにしか提供されていなかったため、VirtualBox上にUbuntuをインストールして利用するようにしました。ディスク容量を30GBくらいに大きく用意しておく必要がある事です。安心して作業をするめる事ができるようになります。それからgpartedでファイルシステムのパーティションを拡張してやるという形になります。こちらのQiitaの記事を参考にして進めました。Editionという無料で利用できるバージョンがありますので、それを利用しています。今回私がインストールしたタイミングでは、DE0-CVに搭載されているCyclon-VというFPGAをサポートしているのは19.1というバージョンが最新でした。そもそものベースアカウントを登録するという作業と、そこに追加で、FPGAプログラムに登録するという2つの登録が必要になります。Blasterを通過させるVirtualBoxでは、VMにどのUSBデバイスが見えるようにするかというのを設定できます。USB-Blasterを許可してやるようにします。コンパイルして書きこみだけをUbuntu上でできるようになるので、開発作業が快適になって非常に良いです。Primeの使い方まで乗っているのでとても良いです。PostGisを使っているケースが多く、MySQL周りのライブラリはあまり活発ではありませんでした。更新があまりされていなくて、Rails5への対応もずっと止まっていて少し不安です。対応した別のライブラリを探すか、これを使うなら自分でPRを凄く送る等が必要かなと思います。Railsで位置情報を扱ってる情報の流通量がそもそも少ないので新しいライブラリの情報を見つけづらいというのはありますね。Record::Type::Valueを継承する必要があります。Armgでも以下のように:geometry型を新規に定義しています。:mysql2)のようにして、データ型をActiveRecordに登録してやっています。DBへのQueryに埋め込む形式に変えるために、serializeというメソッドが呼び出されます。拡張なしのActiveRecordで対応しているのはStringNumericDateTimeSymboltruefalsenilくらいだと思います、なので、通常新規の型を作るなら、これらへの変換をserializeの中でやる必要があります。Deserializer合わせて定義しました。endこのようなカスタムのSerializerを定義してやりました。GeometrySerializer.new登録してやる必要があるのですが、このタイミングが重要です。initializersのなかで適当に上記の行を実行しようとするとArmg周りの定義で死にます。do....endというブロックの中でrequire等が行われているので、initializerの段階ではまだArmgの型等がUndefinedだったりします。そんな状況でnewしようとするので当然死にます。ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.prepend(::ArmgConnectionAdapterExt)endこちらの登録についても同様にハンドラに登録してやって遅延させる必要があります。ログが死ぬここまでで本来は問題ないのですが、ここで問題が起こります。上記のserializerではWKBと呼ばれるバイナリ形式をつかってSQLにgeometryデータを流し込もうとします。これがRails上でログ解析時に異常なUTF-8文字列だと認識されて例外を吐くようになります。FactoryGirl等で書き込むときに例外を吐かれるとテストが通りません。このような文字列形式じゃない形で送る必要があります。<SRID>)という16進数とSRIDを指定する形式です。これならエラーは出ないでしょう。そこで、正常にRubyが処理できる形式でSQLに織り込んでやる必要があります。serializeの中で適宜変換してやる必要があるのですが、SQLへの埋め込みは実際にはそれではうまく行きません。そもそも前述のようにserializeから本来返せるデータ型には制限があります。今回使うのに適切そうなのはStringだと思いました。ST_GeomFromWKBのSQL関数文字列を素直にserializeから返してやればいいと。そもそもserializeは文字列型で渡されるとSQLには「その文字列をそのまま文字列としてSQLに渡そう」とします。VALUES("ST_GeomFromWKB(....)")というふうに文字列形式で吐き出されてしまってSQLの関数呼び出しではなくなってしまいます。AdapterExtでquoteに手を入れるそのため、Type::Valueのレベルではハンドリングできないことがわかりました。ユーザーはSQLは理解してくれるはずの独自形式を作れないのでしょうか?そのようなときに使うのがDBへのコネクタへの拡張です。上のコードでActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.prepend(::ArmgConnectionAdapterExt)の用にしているのがソレです。DBへのコネクタが値をSQLに変換するまでの過程に手を加えることができます。column_=nil)というメソッドが重要になってきます。そのまま埋め込んで良い文字列に変換するためのメソッドです。イメージとしては返された文字列が直接SQLに結合されるというイメージです。埋め込まれるのではなくhogeとSQLの一部として埋め込まれるのです。これを使えば、SQLの中に直接先程のST_GeomFromWKBを埋め込むことができます。そこで、先程のGeometrySerializerを以下のように変更します。そのまま返すようにしました。定義して、ソレ以外は親に任せるように定義してやります。ラップされた形で書き込まれるようになります。以下のような感じです。1これで無事にログ上にも正常にSQLが表示されるようになり、例外は発生することはなくなりました。5.1で利用する場合は上のコードの引数も修正してください。ラップされている事があるようになりました。endendといった形で取りだしてやる事で対処できます。以下のようなscopeも書けるようになります。lat))}この?の部分で先程のquoteが使われています。自然にSQLで位置情報を扱えるようになりました。Geometryライブラリを使って地球上の円形を覆っていました。poketo7878-dev.hatenablog.comその時、「まだ多角形ポリゴンのサポートはされていない」と書いていました。その後、そのバージョンアップが対応され、内部に穴を持たない多角形(ループと呼ばれていますs2.Loop)のセル計算がサポートされました。今回はそのあたりについて調査してみましたのでまとめます。エンコードされたポリラインのデコードGoogleMapではポリゴンのデータ等をエンコードして文字列に変換することができます。データベース等に格納する際はこの形式を検討してみても良いかもしれません。そんなエンコードされたデータをデコードして、もとの緯度経度のリストに復元してくれるGoライブラリは以下のものがあります:github.comループループはS2ライブラリ上での「最初と最後を結んで閉じられる緯度経度のループ」を意味しています。緯度経度の順序はループの縁に沿って反時計回り(CCW)に設定されている前提で扱われており、ループの内側がループの覆っている領域ということになります。このループを作るには、緯度経度をまずはs2.Pointに変換し、そのポイントのリストからLoopを作る必要があります。そのポイントをpointsにためておいて、s2.LoopFromPointsに渡してやることでs2.Loopが手に入っています。上のような領域をループにしていたとすると、以下のようにして、ループの中に緯度経度が含まれるかどうか判定できます。139.701636)....<先程のコード等>loop.ContainsPoint(s2.PointFromLatLng(shibuya))shibuya変数が指しているのは渋谷駅の緯度経度なので、当然結果はtrueとなります。セルのリストに変換します。最大上限200の場合ほぼ綺麗に覆われているように見えます。前回から今の間にループのバグが修正されたので、無事多角形領域についてもセルに変換して検索することができるようになりました。これらを使って得られた知見があったら共有いたします。位置情報を扱いながら高速に検索する必要がある場合、DB上で範囲検索が容易にできると便利です。GeoHashなどの位置情報のハッシュ計算をしておいて、ハッシュで範囲検索をするとインデックスを活用できて良いです。一方でドキュメントの少ないS2ライブラリについて調査したので、ざっくりまとめました。Codeに上がっているのが分かる程度で、あまり情報はないように思えます。アルゴリズム解説のドキュメントは、http://blog.christianperone.com/2015/08/googles-s2-geometry-on-the-sphere-cells-and-hilbert-curve/こちらの記事がわかりやすく、平面状をヒルベルト曲線で埋めていくことや、エンコードされたCellIDの包含判定が前方一致(厳密には末尾1ビットを削る等の処理は必要だが)でできる特徴などがわかりやすく解説されているので良いです。S2他言語へのポートS2のライブラリをGoに移植したものや、Pythonに移植したものが盛んに開発されているので利用すると良いと思います。今回はGoへの移植版を利用してみます。R3の部分、S1の一部等は完了しているのですが、S2の部分はまだ本格的に利用するには機能が足りない部分等が多く見られますが、活発に開発はされているようなので、遠くない未来にいい感じになると思います。PRを送ってみるのもいいかと思います。実際には球の表面に沿って曲がっているのでキャップのような形になっています。点と点の角度や、球の大円に対する比率で扱います。引数は、キャップの頂点となる球面上の点と、キャップの縁の大円に対する比率で表現します。図ですと、赤い部分が球を切ったときのCapです。これがちょうどみかんの皮のように球の表面を完全にキャップが覆っている状態です。地球のサイズの球を想定して比率計算をする必要があります。6371.01になります。s1.Angle(ratio))}こんな風にする必要があります。次はS2の機能を利用してこの円をCellで埋めてみましょう。計算するための型です。30というところで、S2が扱える最小サイズのセルまで使ってもよいと指定し、さらにMaxCellsで被覆するにに使うセル数の最大数を指定しています。}}でどんなCellで円を埋められるのか計算できます。この結果をGoogleMapで可視化してみたものが以下になります。GoogleMapの機能で渋谷駅から半径4kmを描いてみたものです。はみ出しているところが目立ちますねセル上限100の場合すこしはみ出しが減りました。セル上限200の場合かなりはみ出しが改善しました。ユースケースたとえば、データベース上に大量の位置情報を格納しておいて、必要に応じて高速に円の中に含まれているデータを取り出したい場合、あらからじめ位置情報をS2でCellIDに変換しておき、今回の円で得られたCellIDと前方一致で調査するという形になります。これがある地形内(ポリゴン内)に含まれている全データ等を求めたいときにデータベースの範囲インデックスを効かせて計算できるのは非常に高速に求められると思います。残念ながらポリゴンのサポートはまだGo版のS2ではできていませんが、Python版のライブラリもありますし、MongoDBやUber、Forsquareなどのサービスでも利用されているので、位置情報を扱う何かをするときには検討してみても良いかもしれません。まえからS2は気になっていて、今日調査してみたのでまとめてみました。何か成果があれば追ってまとめます。APIサーバーの応答時間が増加してしまうという問題がありました。ロードアベレージがこの時最大16とかです。ネットワークの問題というよりは何らかのハードウェア資源が不足してるという状況が考えられます。このときCPU負荷は常に90%以上を推移していてCPUが何らか圧迫されているように見えました。SARをみたsysstatというパッケージを入れておくと定期的にCPUが何に取られているかログを取ってくれるで便利です。headしてみたら、%sysや%iowaitは低かったのですが、%userが40%近い値になっていました。これはつまり何らかユーザープロセスがCPUを圧迫しているということです。topでざっくりと負荷を確認topコマンドは常識だと思いますが、topでしばらくCPUの負荷を観察したところ、定期的にbundleコマンドの何かが3つほど呼び出されてそれら個別に20%ずつほどCPUを食っているようでした。-n4で、全プロセスをCPU処理を食っている割合で降順にソートし、トップ3を取り出してみました。定期的にユーザー端末のヘルスチェックを行ったり、定期的なログを吐き出すようなジョブたちがCPUの大部分を占めていました。他のプロセスは基本的にCPU負荷は5%にも届かないような状態が多く、圧倒的にこれらのプロセスがCPUを食っていました。以前から気がかりだった定期的な処理とAPIサーバーが同じところに乗っているという問題は早急に解決しないと行けないレベルになったと判断しました。APIサーバーから追い出そうと決め、とりあえずGoのスクリプト等がcronで呼び出されていたサーバー(以下バッチサーバー)に移動しようと決めました。ソースコードをバッチサーバにもデプロイデプロイ処理を現在はCapistranoで行っていたこともあり、rbenvの登録や必要なライブラリのインストール等を適宜バッチサーバーに実施してやり、デプロイ対象にバッチサーバーを登録してやったところすんなりCapistranoでソースコードをバッチサーバーにデプロイできました。%w(<サーバー接続情報>)を登録してやりました。これで本番環境のデプロイ時にバッチサーバーにもソースコードがアップロードされるようになりました。:batchという感じで設定してやればwheneverのcron登録は先程のbatchロールにだけ実施されるようなりました。上記の話はプロダクション環境のみに関係のある話でステージング環境は以前すべて同じサーバーで問題ありません。:batchを書いておいてやって、具体的なbatchロールに属するサーバーはproduction.rbとstaging.rbで設定する形にしておくと楽です。staging.rbではたとえばメインのサーバーと同一の接続情報にしておくことで同じサーバーですべて実施されます。ジョブを正しくキューするにはRedisもどこかに移さなくてはなりません。APIサーバーはAPIのみに注力しようと考え、暫定的にバッチサーバーにRedisも移動することにしました。redis接続可能になったらOKです。このとき、APIサーバーからもRedisにつなげておくようにしておくと、Sidekiqに付属しているWebコンソール経由で見張れるので便利です。"<どこか適切なパス>"この段階で、cron経由やAPIサーバー経由でジョブがバッチサーバー上のredisに登録される様になりました。(こちらも本来はSidekiq用の専用サーバーを立ててやってAutoScalingとかするのが良いと思います)Sidekiqをバッチサーバー上でのみ実施するこちらもCapistranoのお陰で超簡単です。:sidekiqとsidekiq_roleを設定してやって、wheneverの時と同様に各環境ごとにサーバーを設定してやれば完了です。これで、デプロイ時にバッチサーバー上でSidekiqが実行されるようになります。<pid>で止めて、Webの監視画面からsidekiqが正常に動いていることを確認したら無事完了です。FluentdをバッチサーバーにインストールJobの中にはFluentd経由でログを収集しているインスタンス向けてRails内部からログを送信するタイプのものもありました。バッチサーバー上のsidekiqのログを見ていたらFluentdとの接続エラーが出ていたので、適宜Fluentdの設定をしました。これはAPIサーバー上にFluentdを設定したときのItamaeのレシピが残っていたので、それを適宜再利用してすんなり完了しました。今回の移行で負荷が増大する懸念があったので、インスタンスタイプの変更を検討しました。-rssの結果を確認したところこれらはunicornのワーカーが使用していもので、バッチジョブとは無関係でした。CPUの消費はほぼバッチによるものでした。バッチの定期的なものだけでなく、オンラインで実施されるJobも含めて検討するとサービスの実施時間帯中のスポットで一気にユーザーの需要に合わせて高まる傾向が多く、ずっとCPUを消費するというよりは必要なときにパワーの出るタイプが望ましいと判断しました。サービス拡大の時期であり、需要の上限が用意には見積もれず、またオートスケールを導入してしまうよりは一旦サービス中の負荷を確認できたほうが良いだろうと判断し、一旦大きめのインスタンスタイプで余裕を持つことにし、インスタンスタイプはt2.mediumを選択しました。なったCloudWatchで見張ったその晩は果たして上記の判断が正しいものだったのか不安だったので、CloudWatchでAPIサーバー及びバッチサーバーを見張ることにしました。実施直後ロードアベレージが一気に下がったまず、上記の施策を実施した時点でロードアベレージががくんと下がりました施策の実施直前は色々とサーバー内で必要なプロセスを止めたり、色々と作業をしていたので一時的にロードアベレージは上がっていますが、実施後に、一気に平均のロードアベレージが下がりました。負荷の高い時間帯でなくても平均してロードアベレージは0.4程度あったのですが、施策を実施した直後0.04程度に一気に下がりました。サービス時間帯中もロードアベレージが異常事態を迎えなくなった実施して一晩たった記録ですが、最近発生してきていた異常な状態がなくなったことがわかります。サービス時間野メイン時間には多少ロードアベレージが上がっていますが、一番高いもので1.99です。2日連続で通信エラーに悩まされていて眠れない夜が続いていたのですが、今回の施策を実施して、一気に異常なピークが発生しなくなりました。メモリ使用率は30%程度です。t2.microはコアが2つなので、実際は24%~30%程度ですので、ベースラインパフォーマンスの40%は常に下回っている状態となりました。T2インスタンスT2インスタンスはベースラインパフォーマンスを下回っていると自動的にCPUクレジットが溜まっていき、必要なときに頑張ってくれるのですが、現状のインスタンスタイプだとピーク時にもクレジットはたまり続けて居ますね。いえ、まだ一日しか様子は見ていないので、ここで小さくするのは早計だと思うので、一週間から一ヶ月程度様子を見た上で判断したいと思います。開発環境はDockerでビルドできているので、本番環境でもDockerに移行してECS等で管理したい。用意に対応できると思う。形の強制アップデートを要求したいケースがあります。バージョンをどうするか悩まなくてもどういう変更の入ったリリースかからある程度自動的に決まるので考えなくて済みます。レスポンスを返すようにします。rubyだと以下のgemがセマンティックバージョンの処理をしてくれるので便利でした。Connectから情報を引っ張ってくる機能があります。"2.0.0"]のようなバージョン番号の配列が入ります。動的に最低バージョンの保証を変更することができるようになります。確実に存在するバージョンを選べるようにしておくと安心です。app.live_version.versionとやると、ストアに出ている最新バージョンが取得できます。上記で取得したバージョンにフィルタリングしたり選ぶときの候補名の装飾に使うと便利だと思います。gemのSpaceshipを使ってiOSアプリのバージョン番号のリストを取得するTweet広告を非表示にするもっと読むコメントを書く次のページプロフィールid:Pocket7878_devプログラミング好きで、仕事の傍らで趣味で書いたもの等があればちょびちょび更新していきます。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。