やる気がストロングZERO

やる気のストロングスタイル

OSSコードリーディングをスムーズに進めるためのメモ

OSSのコードリーディングをしてみる中で、内容を把握するためにして良かったと思った事をメモっておく。

リーディング対象にしたもの: github.com ※主にはfilebeat

リーディングが上手く進まないパターン

詳細を追ってしまい帰ってこれなくなる

僕は不明点があると気になってしまい調べ始めて帰ってこれなくなる傾向がある。

例えば調査対象関数の引数に関数の返り値が使われていて、その関数を見に行ってその先でも更に不明な関数があって、、帰ってこれなくなる。(※全然気にせず読める人もいるようだ。人によるっぽい)

大体の場合、全体の感じを理解できていない状態でのこういった詳細の理解は難しく、使う時間に対してのリターンは少なく、気力だけが消費されて読むのを諦めてしまう。

内容把握を推し進めるために

ドキュメントを読む

OSSであればContributingなど、開発参加に際してのdocumentがあるのでざっくり読む。
開発環境の構築方法や構成についての説明があったりする。
たぶん質問フォーラムとかもあってこれもざっくり見たほうが良い(今回はあまり見てない。。)

読む対象を絞り、それ以外は意図的に無視する

具体的なイメージがしやすい部分を読む対象とし、それ以外は意図的に読まないようにする。
読むための気力が有限資源であることを強く意識し、目的以外の行動は極力しないようにする。

今回の場合は、filebeatが「ファイルを読み取る部分」を読みに行った。
どこかで対象ファイルをconfigファイルから特定して読み込む部分があるはずだと探した。

パーツ感を把握する

OSSだと大体いい感じに粗結合なモジュール化をされていることを期待して、全体のパーツ感を把握する。
パーツ感が把握できれば、読む対象パーツ以外を一気に無視できるようになる。

今回の場合、パーツ感はmain.goに書かれているコメントで把握できた。
https://github.com/elastic/beats/blob/master/filebeat/main.go

input, harvesterモジュールが今回の読む対象っぽい

// The basic model of execution:
// - input: finds files in paths/globs to harvest, starts harvesters
// - harvester: reads a file, sends events to the spooler
// - spooler: buffers events until ready to flush to the publisher
// - publisher: writes to the network, notifies registrar
// - registrar: records positions of files read
// Finally, input uses the registrar information, on restart, to
// determine where in each file to restart a harvester.

パーツ感がわかってくると、ディレクトリ構造に構造がなんとなく見えてきた。
使われている名前と概念が少しづつわかってきた。(扱うデータをeventという単位で表している等)

パーツ単位で動きがイメージできるようになれば「どのように使われているか」を起点にして理解範囲を広げていける

手元で動作できるようにする

ある程度読んで少しわかってきたら意図通りに処理が流れるか実際に動かして確認してみる。
動作テスト用にlocalで動作環境を用意するための方法が用意されていたりする。
今回は、Makefileを漁っていたら

testing/environments

に環境があることがわかり、コメントを読んで理解できた。
もしかしたらフォーラムとかに詳しく情報があったりするのかも。

まとめ

  • 読む範囲をとにかく絞る。わかった部分から徐々に理解を広げる。
  • ドキュメント・コメントを読む。コードから逆解析をするよりも効率がいい。
  • コードの詳細を理解しようとしない。必要になったときに初めて深く見るようにする。
  • 内容を覚えようとしない、読んで意図がわかったら次々行くほうが効率が良い。忘れたらまた読む。

Filebeat + Logstashでrails6のログをElasticsearchへ取り込む設定

FilebeatとLogstashを使ってrails6のログをElasticsearchへ取り込もうとしたがなかなか難しかったので書く。

rails6のログをそのまま取り込んでもデータとして使えない

構造化されていない

railsの出力ログを見てみたらこんな感じで、とくに構造化されていない。

I, [2020-11-13T17:17:40.347350 #12138]  INFO -- : [a2bb9deb-e759-4768-9b90-c8d7bd4f521c] Started GET "/favicon.ico" for 127.0.0.1 at 2020-11-13 17:17:40 +0900
...

このままElasticsearchへ取り込んでもサーチャブルなデータにならない。

また、ログにはそれが実行された時間が記録されているが、これを認識できないので「Logstashが取り込んだ時間」が基準となってElasticsearchへ記録される。
Logstashが取り込むにはタイムラグがあるので10秒ほど誤差が出る。 また、サーバーメンテナンスなどで一時的にログ記録を停止した後再開したりすると、一気にログが流れ込み、全てその時間で記録されることになる。

事実と異なるデータになってしまう。

複数行で意味をなしているものがある

こんなやつ。

I, [2020-11-13T17:17:40.347350 #12138]  INFO -- : [a2bb9deb-e759-4768-9b90-c8d7bd4f521c] Started GET "/favicon.ico" for 127.0.0.1 at 2020-11-13 17:17:40 +0900
F, [2020-11-13T17:17:40.350699 #12138] FATAL -- : [a2bb9deb-e759-4768-9b90-c8d7bd4f521c]   
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] ActionController::RoutingError (No route matches [GET] "/favicon.ico"):
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c]   
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] actionpack (6.0.0) lib/action_dispatch/middleware/debug_exceptions.rb:36:in `call'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] actionpack (6.0.0) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] railties (6.0.0) lib/rails/rack/logger.rb:38:in `call_app'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] railties (6.0.0) lib/rails/rack/logger.rb:26:in `block in call'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] activesupport (6.0.0) lib/active_support/tagged_logging.rb:80:in `block in tagged'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] activesupport (6.0.0) lib/active_support/tagged_logging.rb:28:in `tagged'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] activesupport (6.0.0) lib/active_support/tagged_logging.rb:80:in `tagged'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] railties (6.0.0) lib/rails/rack/logger.rb:26:in `call'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] actionpack (6.0.0) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] actionpack (6.0.0) lib/action_dispatch/middleware/request_id.rb:27:in `call'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] rack (2.0.7) lib/rack/method_override.rb:22:in `call'
[a2bb9deb-e759-4768-9b90-c8d7bd4f521c] rack (2.0.7) lib/rack/runtime.rb:22:in `call'

そのまま取り込むと複数行になってしまう。

また、フォーマットも違っていて時間の記載もない。

上記と同じ問題で、そのままとりこむとlogstashへの取り込み時間が基準時間になってしまう。

対応設定(具体的な設定ファイルは下記します)

構造化して取り込みたい

Logstashのfilter設定で構造化する。

grokプラグインを使えば正規表現を使ってログへ意味付けを行える。

※logrageというgemを使えばrailsのログをjson形式で出す事も出来たようだが以下理由で最終的にはやらなかった。

・全てのログがjson形式で出るわけではない(ググってみるとみんな悩んでいるっぽい)
・logrageの仕組みがなんか無理やりやってるぽくってちょっとあとで問題になりそうで怖かった。

ログの時間を基準時間として取り込みたい

上記grokで構造化して取り出した日時データを基準時間として設定する。 Logstashのdateプラグインで出来た。

今回railsログは日本時間で出ていたけど、そのまま取り込むとUTCと判断されるのでtimezone指定して取り込まないといけない。

複数行で意味をなすものは、1行として取り込みたい

FilebeatのMultiline optionsで出来た

railsログとnginxログは別indexとして取り込みたい

今回railsのログとnginxのログをfilebeatで送出している。

$ sudo filebeat modules enable nginx # nginxログ送出設定の有効化

なにもしないとこれら2つがElasticsearchの同じindexに記録されてデータが混ざってしまうので分けたかった。

Logstashの設定でif文を使って分けた。(nginxログもgrok等を使って構造化して取り込む設定も入れている。参考)Use Logstash pipelines for parsing | Logstash Reference [7.10] | Elastic

複雑化しそうだったらMultiple Pipelinesを使ったほうが良さげ 絶対的に使った方がいいLogstashのMultiple Pipelinesについて書いてみた - Qiita

設定ファイルサンプル

こんな感じになった。

(railsサーバー側)
$ sudo vim /etc/filebeat/filebeat.yml

- type: log

  # Change to true to enable this input configuration.
  enabled: true

  # Paths that should be crawled and fetched. Glob based paths.
  paths:
    - /rails_app_dir/log/production.log


  ### Multiline options

  # Multiline can be used for log messages spanning multiple lines. This is common
  # for Java Stack Traces or C-Line Continuation

  # The regexp Pattern that has to be matched. The example pattern matches all lines starting with [
  multiline.pattern: ^\[

  # Defines if the pattern set under pattern should be negated or not. Default is false.
  multiline.negate: false

  # Match can be set to "after" or "before". It is used to define if lines should be append to a pattern
  # that was (not) matched before or after or as long as a pattern is not matched based on negate.
  # Note: After is the equivalent to previous and before is the equivalent to to next in Logstash
  multiline.match: after
(Logstashサーバー側)
$ sudo vim /etc/logstash/conf.d/filebeat.conf

input {
    beats {
        port => 5044
    }
}

filter {
    # nginx access 参考:https://www.elastic.co/guide/en/logstash/current/logstash-config-for-filebeat-modules.html#parsing-nginx
    if [event][dataset] == "nginx.access" {
        grok {
            match => { "message" => ["%{IPORHOST:[nginx][access][remote_ip]} - %{DATA:[nginx][access][user_name]} \[%{HTTPDATE:[nginx][access][time]}\] \"%{WORD:[nginx][access][met
hod]} %{DATA:[nginx][access][url]} HTTP/%{NUMBER:[nginx][access][http_version]}\" %{NUMBER:[nginx][access][response_code]} %{NUMBER:[nginx][access][body_sent][bytes]} \"%{DATA:[ngi
nx][access][referrer]}\" \"%{DATA:[nginx][access][agent]}\""] }
            remove_field => "message"
        }
        mutate {
            add_field => { "read_timestamp" => "%{@timestamp}" }
        }
        date {
            match => [ "[nginx][access][time]", "dd/MMM/YYYY:H:m:s Z" ]
            remove_field => "[nginx][access][time]"
        }
        useragent {
            source => "[nginx][access][agent]"
            target => "[nginx][access][user_agent]"
            remove_field => "[nginx][access][agent]"
        }
        geoip {
            source => "[nginx][access][remote_ip]"
            target => "[nginx][access][geoip]"
        }
    }

    else if [event][dataset] == "nginx.error" {
        grok {
            match => { "message" => ["%{DATA:[nginx][error][time]} \[%{DATA:[nginx][error][level]}\] %{NUMBER:[nginx][error][pid]}#%{NUMBER:[nginx][error][tid]}: (\*%{NUMBER:[nginx
][error][connection_id]} )?%{GREEDYDATA:[nginx][error][message]}"] }
            remove_field => "message"
        }
        mutate {
            rename => { "@timestamp" => "read_timestamp" }
        }
        date {
            match => [ "[nginx][error][time]", "YYYY/MM/dd H:m:s" ]
            remove_field => "[nginx][error][time]"
        }
    }


    else if [log][file][path] == "/rails_app_dir/log/production.log" {
        grok {
            patterns_dir => ["./patterns"]
            match => { "message" => "(?<timestamp>%{YEAR}-%{MONTHNUM}-%{MONTHDAY}T%{HOUR}:%{MINUTE}:%{SECOND}) #(?<pid>\d+)]\s+(?<level>\w+) -- : \[(?<tid>\w+-\w+-\w+-\w+-\w+)] (?<mess>.+)" }
        }
        mutate {
            rename => { "@timestamp" => "read_timestamp" }
        }
        date {
            match => [ "timestamp", "YYYY-MM-dd'T'HH:mm:ss.SSSSSS" ]
            timezone => "Asia/Tokyo"
            target => "@timestamp"
        }
    }
}

output {
    if [event][module] == "nginx" {
        elasticsearch {
            manage_template => false
            index => "nginx-%{+YYYY.MM.dd}"
        }
    }
    else if [log][file][path] == "/rails_app_dir/log/production.log" {
        elasticsearch {
            index => "rails-%{+YYYY.MM.dd}"
        }
    }
}

Elastic Stackのセッティングメモ

Elastic Stackのセッティングをしてみたのでメモ。

対象OSはUbuntu18.04

参考)

www.elastic.co

Elastic Stackで作るBI環境 Ver.7.4対応改訂版 (技術の泉シリーズ(NextPublishing))

Elastic Stackで作るBI環境 Ver.7.4対応改訂版 (技術の泉シリーズ(NextPublishing))

  • 作者:石井 葵
  • 発売日: 2019/11/29
  • メディア: オンデマンド (ペーパーバック)

Elastic Stackとはサーバーの状態を見やすく管理するためのツール郡

サーバー運用していると、問題が起きていないかアクセスログやcpu負荷などを継続的にチェックする必要がある。
確認するにはlogファイルを開いたり、topコマンドを叩いてload averageを見たりしなければならないが、Elastic Stackを導入することでwebインターフェースから様々な視点で検索・一覧して確認できるようになり便利。

今回導入したツール郡

  • Elastic Search
  • Logstash
  • Kibana
  • Beats(Metricbeat)

Elastic Search

Elastic Searchはオープンソースの RESTful 分散検索/分析エンジンで、アクセスログやload average等の指数を保存する場所として使う。 Kibanaでのデータを表示するときにいい感じにデータを吐き出してくれる。

インストール方法

参考) Install Elasticsearch with Debian Package | Elasticsearch Reference [7.10] | Elastic

elastic stackのリポジトリ周りの設定。(1度やればOK)

$ wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -

$ sudo apt-get install apt-transport-https

$ echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list

$ sudo apt-get update

elastic searchのインストール

$ sudo apt-get install elasticsearch

設定

モリー使用率修正

マシンのメモリが少ない場合、javaの最大ヒープサイズを調整しないと起動しないとのこと。 僕の場合1GのVPSでやってるので256MBにした。(あとでLogstashも入れるので少なめに指定)

$ sudo vim /etc/elasticsearch/jvm.options

-Xms2g
-Xmx2g

↓以下に変更
-Xms256m
-Xmx256m

起動

$ sudo systemctl start elasticsearch.service

自動起動設定

$ sudo systemctl daemon-reload
$ sudo systemctl enable elasticsearch.service

動作確認

$ curl -XGET 'localhost:9200/?pretty' # jsonがかえってこればOK

Kibana

Kibanaとはデータを検索したり表示したりするためのWebインターフェース。Elastic Searchから取得したデータをいい感じにグラフ化して表示してくれる。

インストール方法

$ sudo apt-get install kibana

設定

デフォルトだとローカルのelastic search(localhost:5601)に接続されるようになっている。もし、違う場所にあるならkibana.ymlを編集する

$ sudo vim /etc/kibana/kibana.yml

起動

$ sudo systemctl start kibana.service

自動起動設定

$ sudo systemctl daemon-reload
$ sudo systemctl enable kibana.service

Logstash

Logstashはサーバーからデータ(アクセスログやcpu使用状況など)を継続的に取得し、整形し、データストア(今回はElastic Search)へ送出するためのツール。

インストール方法

今回僕は計測対象サーバー(アプリケーションサーバー)にelastic searchやkibanaをインストールしてるので(本来は別サーバーにしたほうが良い)Logstashも同じサーバーへインストールしている。

$ sudo apt-get install logstash

設定

モリー使用率修正

elastic searchと同じ理由でメモリー使用率を修正する

$ sudo vim /etc/logstash/jvm.options

-Xms2g
-Xmx2g

↓以下に変更
-Xms256m
-Xmx256m

railsログを取り込んでElastic searchへ送り込む設定

$ sudo vim /etc/logstash/conf.d/rails.conf


input {
    file {
        path => "/railsAppDir/log/production.log"
        start_position => "beginning" # 追記されたログを取り込む
        tags => "rails_log"
    }
}

filter {
    grok {
        patterns_dir => ["./patterns"]
        match => { "message" => "(?<timestamp>%{YEAR}-%{MONTHNUM}-%{MONTHDAY}T%{HOUR}:%{MINUTE}:%{SECOND}) #(?<pid>\d+)]  (?<level>\w+) -- : \[(?<tid>\w+-\w+-\w+-\w+-\w+)] (?<mess>.+)" } # railsログをある程度構造化して取り込むための設定(もっといいやり方があったら教えてほしい)
    }
}

output {
    elasticsearch {
        index => "rails-log-%{+YYYY.MM.dd}"
    }
}

起動

$ sudo systemctl start logstash.service

自動起動設定

$ sudo systemctl daemon-reload
$ sudo systemctl enable logstash.service

Beats(Metricbeat)

Beatsはサーバーからデータを取得し、データストアで送出するための軽量なツール郡。
対象データ毎に[○○beat]という感じで別ツールになっている。(対象データがcpu使用状況ならMetricbeat、fileデータならFilebeat、という感じ)
今回はcpu使用率などを計測するMetricbeatを使う。

Logstashよりも低機能だが軽量

Beatsはデータに対してあまり細かい設定はできない。「取得して送出」するだけ。
ただし軽量なので、
「計測対象サーバーに常駐させて、別サーバーで動いているLogstashに対して送出し、Logstashでデータ整形を行ってElastic Searchへストアする 」
というような使い方が良さそう。

インストール方法

今回僕は計測対象サーバー(アプリケーションサーバー)にelastic searchやkibanaをインストールしてるので(本来は別サーバーにしたほうが良い)Metricbeatも同じサーバーへインストールしている。

$ sudo apt-get install metricbeat

設定

デフォルトでlocalhostのElastic searchにデータ送出先が向いている。
変更の必要があれば設定ファイルを編集する

$ sudo vim /etc/metricbeat/metricbeat.yml

起動

$ sudo systemctl start metricbeat

自動起動設定

$ sudo systemctl enable metricbeat

kibanaへの接続制限どうするか(SSHポートフォワードにしてみた)

kibanaへの接続に制限をかけたい。ぱっと良い方法が浮かばなかったのでSSHポートフォワードで接続するようにしてみた。

参考)

SSHポートフォワードメモ - やる気がストロングZERO

$ ssh username@hostip -L 5601:localhost:5601 -N

上記を実行してから、ローカルマシンでlocalhost:5601へアクセスする。

GKEからHerokuに引越したまとめ

実験・練習としてnekone.loveというtwitterに投稿されてる猫画像をひたすら表示しているサイトを運用している。
以前、k8sの使用感とかの確認のためにGKEを使って運用してたが、今回Herokuに移行したのでその時の作業メモなどを書く。

k8s(GKE)はなんだかんだで難しく規模の大きなシステム向き

k8sを触りたくてk8sで運用してたけど、わかったのはk8sはなんだかんだで難しいということだった。
「相当な専門性を持ってあたらないと、とても片手間で制御しきれるものではない」という印象だった。

たぶん、k8sを使う事でメリットの方が大きくなるのは、ある程度大きい規模のネットワーク構成によって構成されているシステムの場合だと思った。

k8sでこれを組んでいれば、デプロイの仕組みや、監視、ネットワーク構成を含めたシステムまるごとがk8sで提供され、特定のホスティングサービスにロックインされない。(全てk8sが提供する機能なのでk8sが動くならどこでも動く。ただし実際はちょっとそうでもない部分もあるっぽい)
各々サーバーの設定や関連についてもコードで管理されるので「どういう設定になっているか不明」のような事にもならないし、基盤の移行もやりやすくなる。

ただし、k8sでプロダクションで使えるようにシステムを組むには、もともとインフラ畑でやってきた人のノウハウが必須であると感じた。
例えばログに関しても、何もしなければ消えていってしまうのでfluentdなどを使って永続化する仕組みを作らないといけないし、監視についても検討構築する必要がある。

このあたりの情報はデファクトスタンダードのようなものもあまりなさそうで、アプリケーションを主戦場にしている僕ではベストな選択はまだ出来そうもなかった。

個人レベルのサービスならHerokuがちょうどよかった

僕はアプリケーションエンジニアなので、インフラ周りの知識が薄い。
漠然と 「DBはレプリケーションしてほしい・定期的にバックアップをとってほしい」 「負荷があがったときはwebサーバーがスケールしてほしい」
くらいの知識はあるが、それを実現するための細かい設定や、気をつけるポイントなどはあまり知らない。

Herokuを初め、Paasを使えばこのあたりをまるっと提供してくれているのでアプリケーション構築に集中できるし、自分が不足した知識で構築するよりも信頼性が高い。 (paasがそういう物なのでそらそうだという感じだが、、)

Paasを使うことでロックインの危険もあるが、スケーリングや監視などの主要な機能はどのPaasも提供してくれているし(設定をし直す必要はあるが、、)、そもそも個人レベルの規模の小さいアプリケーションなら問題は少ない。
アプリケーションもDockerコンテナで動くように意識して作っていればある程度のモビリティもキープできる印象だった。

金額

GKEだとプリエンプティブのインスタンスを3つ使った構成で3700円(税込み)くらいだった。 なんかロードバランサーの金額が一番高かった。

herokuだと、Hobbyのdynoで$7(自動更新ssl使う為)、postgresで$9(データ量が10Mを超えた為)

Herokuへの載せ替え

アプリケーションコードgitのディレクトリで作業する

・heroku Cliのインストール

The Heroku CLI | Heroku Dev Center

・アプリ作成

$ heroku apps:create nekonelove

・git remoteにherokuを登録

$ heroku git:remote

・Procfileの用意

$ vim Procfile

web: bundle exec rails s -e production -p $PORT

・デプロイ(postgresqlアドオンかってに設定された)

$ git push heroku master

マイグレーション実行

$ heroku run bash // ログイン

// heroku実行環境内にて
$ bundle exec rails db:migrate

・dbデータをインポート

$ heroku pg:psql < dump.sql

・cron設定
アドオン[Heroku Scheduler]を追加して設定

・カスタムドメイン設定

$ heroku domains:add nekone.love

// dns target 確認
$ heroku domains

// この後、DNSをこのdns targetに向ける

ssl設定

$ heroku certs:auto:enable

「シンプルな構造にすれば保守性があがる」わけではないという話

こんがらがったシステムの保守開発で苦労した経験から新規開発では「シンプルな構成・シンプルな実装にしよう」という話が出る。
目指す方向としては間違って無いように思えるのに、それだけだとうまく行かない。

「保守しやすい構造」とはそれなりに多くの知識が必要なので、これらの知識を持たず単純に「シンプルな構成・シンプルな実装」を目指すと失敗する。

「保守しやすい構造」を作るには「どういう構造が保守しやすいのか?」を学ぶ必要がある。

これについて書く。

「シンプル」を目指したつもりが複雑になって失敗する例

まずは、ありがちな失敗例を示す。

実装する処理を減らしてシンプルにしたら複雑化した

コードの複雑さを減らすため、実装する機能を絞った。
コード量が減ったのでシンプルになった。

しかしここで削られた機能の処理は結局はどこかでやる必要があった為、後日別の場所(連携する別システム側など)に実装された。
やるべき所でやってないのでデータの受け渡し等の必要性が発生しコードが複雑化した。

テーブル数を少なくしてシンプルにしたら複雑化した

テーブル数は少ないほうがread / wrightのコードがシンプルになる。
また、1テーブルを見ればどういうデータが入ってるか解るので、パッと見で理解しやすくシンプルであると思えた。

しかし、データ不整合が発生するので、それを防止するための処理コードが実装された。 また、どこか一部の処理がこの防止処理を通していなかったようで、すでに不整合なデータがテーブルに入ってしまい、データも信用できない状態になってしまった。。

・・・
これらは「局所的にシンプル(簡単)にできたが、全体としては複雑化してしまった例」である。

保守しやすくするためにシンプルを目指したのに、真逆の結果になってしまった。

シンプルな方が保守しやすいとは限らない

上記失敗例でも見て取れるように「シンプルにする」をとにかく実行すれば保守しやすい構造になっていくわけではない。

「シンプルにする」は複雑性を下げる為の1要素ではあるが、それだけでは駄目だという事だ。

例えば、DIやDIコンテナはシステムにある程度の「複雑性」を持ち込む事になるが、コードを単体テストしやすくなったり、依存度が下がったりして保守性があがる。

つまり、複雑になったのに保守しやすい構造になることもある。

「局所的なシンプル」ではなく「全体としてのシンプル」を目指すために、ある程度の複雑性を取捨選択しながら計画的に構造を構築する必要がある。

にわか知識で構造に手を出すと痛い目にあう

こちらの動画を見てほしい。

これはミノ駆動さんの「クソコード動画シリーズ」。
面白いのでたまに繰り返し見てます。

注目してほしいのは、最初この登場人物は別に「複雑にしてやろう」とか思って無い所だ。
良かれと思ってやったことが結果的にどうしようもない構造を作り出してしまっている。

上記の失敗例も同じく、みんな良かれと思ってやっている。

過去の自分もやってしまったことがあるし、既存コードにそういう部分を見かけるのは「あるある」ではなかろうか?

保守しやすい構造にするにはそのための知識が必要だ。
知識が無い状態でオレオレ構造を作ると失敗する危険性が大きい。

保守しやすい構造に必要なものを既存知識から学ぶ

話がズレるが「ランダムな値を返す関数」が必要になった時、自分で実装することはしない。
既に存在する関数を使う。

なぜなら、それは専門家により多くの時間をかけて設計・実装された関数なので、自分で作るものよりもはるかに信頼できるからだ。

「保守しやすい構造」についても同じく、既に偉い人々が考えて指針を出してくれている。
我々入門者が改めて、オリジナルな独自の視点で「どういう構造が良いか」についてあーだこーだと議論するのは「車輪の再開発」と同じ種類のものだと思う。

指針も色々あるが「プリンシプル オブ プログラミング」に一通り記載されていてわかりやすかった。

一部だけ引用すると以下

  • KISS(シンプルにしておけ)
  • DRY(コピペするな)
  • YAGNI(それはきっと必要にならない)
  • PIE(意図を表現してプログラミングせよ)
  • SLAP(抽象化レベルの統一)
  • OCP(拡張に対し開き、修正に対して閉じている)
  • ...他にも色々ある

もちろん、この本以外にもこれら重要な指針は様々な書籍で繰り返し出てくる。

つまり「保守しやすいシステム構造」を作るための指針は「シンプルに実装する」だけでは全然足りないという事だ。

こういった指針等を知識として吸収して初めて目指すことができる。

「保守しやすい構造」について議論する時、単に自分が思った意見を投げ合うのではなく「これらの指針を満たしているのでこういう構造はどうか?」のように、指針ベースで議論しないと雰囲気だけで誤った結論に導かれてしまう可能性が大きい。

(補足)保守しやすいテーブル設計には正規化が必要

テーブル設計では正規化することが重要。 IPA基本情報技術者試験(※応用ではない!基本!)の試験範囲にもなっている。

IPA 独立行政法人 情報処理推進機構:試験要綱・シラバス など

データベース設計
【目標】
・データの分析,データベースの設計の考え方を理解し,担当する事項に適用する。
・データの正規化の目的,手順を理解し,担当する事項に適用する。
・データベースの物理設計における留意事項を理解し,担当する事項に適用する。

このあたりの書籍がよかった。

達人に学ぶDB設計 徹底指南書

達人に学ぶDB設計 徹底指南書

保守しやすい構造の例としてアーキテクチャを学ぶ

KISSやDRYやOCPのような指針を理解したならば、原理的にはそれを満たすようにシステム構造を組み立てれば保守しやすい構造になるはず。

次に「では具体的にどういう構造にすればよいのか?」という問題にぶつかるのだが、これも偉い人が検討して色々案(デザインパターン・クリーンアーキテクチャ・DDDなど)を出してくれているので、まずはそれらを学ぶのが手っ取り早い。

実践ドメイン駆動設計

実践ドメイン駆動設計

これらの本もなかなか抽象的な話が多くて「結局どういうコードにすれば良いのだ??」という悩みが尽きなかったが、nrsさんがコードベースでの説明をたくさんしてくれていて非常に参考になる。

nrslib.com

nrslib.com

これらをベースにして、もし自分のプロジェクトに合わない部分があればその部分だけをフィットするようにカスタマイズするのが良い。
ただし、本当に理解した上でカスタマイズしないと、そこが負債になってしまう危険性はあるので注意。

まとめ

「保守しやすい構造」を作るには「どういう構造が保守しやすいのか?」を学ぶ必要がある。

リファクタしてて「やっぱ静的言語はいいよなぁ〜」と思った件

DDDの実践練習として作成している趣味プロダクト(Go言語)で、ちょっと設計をミスったのでリファクタをした。
リファクタ内容は「2つのエンティティとして扱っていたものを、1つの集約として扱うようにする」。

エンティティはシステム内で広く使われるデータ型なので修正範囲が結構大きかった。

作業風景をyoutubeにアップしてて、以下の「契約管理システムを作る #115」から「#122」くらいまでがその作業にあたる。

youtu.be

なんとかリファクタ完了して「やっぱ静的言語はいいよなぁ〜(respect 志村けん)」と思ったので書いてみる。

ビルド時エラーが対応必要リストになる

とりあえずエンティティを変更すると、ビルドが失敗しまくる。
一気に大量のタスクが押し寄せるので精神的に「うっ、、」となるけど、対応すべき箇所がリストアップされているのと同義なので淡々とこなしていけばいい。
これが動的言語だったら(実際は動的言語だったら、というよりデータの扱いの設計によると思った※下記します)変更が必要な箇所を自分で(もしくはIDEの静的解析機能で)洗い出ししなければならず、もし漏れたら実行されるまで漏れている事に気がつけない。

「もう対応漏れはないか」の確認にかなりのコストを払うんではないかと思った。

動的言語が悪いのか?

動的言語が問題なのではなく「動的言語で組まれたシステムはデータをざっくりと扱いがち」なのが問題なのだと思った。

今回DDDを参考にアーキテクチャ設計をしているのでシステム内で扱うデータをきっちり型で定義している。(これがエンティティ)

一方、動的言語で組まれたシステムはシステムで扱うデータが型として明確に定義されず、配列や辞書データでなんとなく持ち回っているパターンが多い。
扱うデータを型で定義しないから、場所場所で微妙に違う形でデータを扱ってる。(一見似たようなデータだが、ある場所では1つ2つ項目が多い、等)
今回のようにあるデータの扱いを変えようとしたところで、似たそのデータ全てを手動で対応していかないといけない。
また、データの内容が似ているからといって同じく変更してもよいかどうかはその文脈や処理によったりするので、前後のロジックを確認する必要があったりもする。

一つでも対応が漏れたらバグになる。結構きつい。。

動的言語でもしっかりアーキテクチャ設計をして、扱うデータを意識して設計してやってたらそこまで差はない気がする(静的解析も効きやすいし)

しかしそこまで型を意識してやるなら静的言語をつかったほうがやりやすい。
動的言語のメリットが活きるのはこういう事をする場合では無い気がする。

gomockを使って「N回目呼ばれたらエラーを返す」を実現する方法

mockを使ってのテストは主に「起こしにくいエラーを再現する」場合にのみ使っている。
gomockにて、「N回目呼ばれたらエラーを返す(それ以外は実際の処理が実行される)」モックの用意の仕方をメモする。

スタブを使う。
github.com

ctrl := gomock.NewController(t)
defer ctrl.Finish()

// 商品リポジトリのモック作成
productRep := mock_interfaces.NewMockIProductRepository(ctrl)

callCount := 0 // カウンタを用意する
productRep.EXPECT().GetById(gomock.Any(), gomock.Any()).DoAndReturn(func(id int, executor gorp.SqlExecutor) (*entities.ProductEntity, error) {
    callCount += 1
    if callCount == 3 {
        // 3回目にエラーを発生させる
        return nil, errors.New("Productデータの取得に失敗しました")
    } else {
        // それ以外は本物で実際に処理を行う
        rep := repositories.NewProductRepository()
        return rep.GetById(id, executor)
    }
}).AnyTimes()