株式会社クリアコード > ククログ

ククログ


2020-03-12開催のRubyセミナー松江で講演予定:「Rubyと仕事と自由なソフトウェア」

株式会社クリアコード代表取締役の須藤です。

2020-03-12開催のRubyセミナー松江で「Rubyと仕事と自由なソフトウェア」というタイトルの話をします。松江に住んでいる人も松江に住んでいない人も私のファンの人は来てね。

Rubyアソシエーションの前田さんに声をかけてもらえたので快諾しました。前田さんに声をかけてもらえてうれしかったです。

主な参加者は「業務でRubyを使っているプログラマ」で内容はまかせてくれるということだったので「Rubyと仕事と自由なソフトウェア」にしました。業務でRubyを使っているプログラマには自由なソフトウェアのことを考えて欲しいなと思ったからです。

前に似たような話をしたことはあったかな?と思い返してみると関西Ruby会議2017での「株式会社クリアコード」が少し近いかも。が、なんか、こういう感じじゃなくて、プログラマが活用できるような話にできるといいな。なぜ、オープンソースは自由ソフトウェアの的を外すのかのような内容を少しでも伝えたいな。

松江でみなさんに会えることを楽しみにしています!

2020-01-24

GitHub Actionsを使ったパッケージ作成の自動化

2019年の11月にGitHub Actionsが正式にリリースされました。
GitHub ActionsはGitHubに組み込まれたCI/CD機能でpush等のGitHub上のイベントをトリガーに任意のアクションを実行できるものです。
GitHub ActionsではDockerが使用できるので、様々な環境上でテストの実行やビルドなどができます。

CIサービスは、他にもAppVeyorやTravis CIやがありますが、AppVeyorは無料のプランだと、ワーカーがプロジェクトにつき1つなので、Groongaのように複数のパッケージを作成するためにジョブが多くなるプロジェクトだと、ビルドとテストの完了に時間がかかってしまい効率的ではありませんでした。
GitHub Actionsでは、リポジトリーにつき20まで並列で実行できます。

また、Travis CIでは、ビルドした成果物を保存する場所がデフォルトで用意されていないため、正常にビルドできるかの確認やテスト実行はできますが、リリース用のパッケージを作成して置いておくということがやりにくいです。
GitHub Actionsではアクションを実行した結果、生成されたファイルを置いておく場所がデフォルトで用意されているため、GitHub Actions上でパッケージを作成し、作成したパッケージを取得してリリース用のパッケージとしてアップロードするということがやりやすいです。

上記のようなメリットがあったため、Groongaプロジェクトでは、CI/CDをTravis CIやAppveyorからGitHub Actionsへ移行しています。

いままで、GroongaやMroonga、PGroongaのパッケージは開発者の手元でビルド、署名、アップロードを実施していました。
GroongaとMroongaは毎月末にリリースがあるため、毎月末に開発者はパッケージを作る作業を実施しなければなりません。
また、この際にパッケージの作成に失敗するような問題が発覚すると、問題の修正作業にも追われることになります。

GitHub Actionsはpushトリガーでアクションを実行できるので、変更がリポジトリにpushされた段階でパッケージ作成することで、問題の早期発見に繋がり、問題を発生させた変更もすぐに特定できます。
また、GitHub Actionsでパッケージを作成すると開発者はパッケージを署名、アップロードするだけでよくなるので、毎月発生したパッケージ作成時間をなくし、その時間を別の作業に充てることができます。

GitHub Actionsを活用すると、上記のような様々なメリットが発生するので、GroongaとMroonga、PGroongaのパッケージの作成をGitHub Actionsで行うようにしました。この記事は、その過程で得た知見を共有するために記載しています。

まずは、GitHub Actionsの使い方について記載します。

使い方

GitHub ActionsのアクションはYAML形式で記述します。
YAML形式なので、GitHub上でコードとして管理できます。

ここからは、Groongaのパッケージ作成のアクションを例に説明していきます。
Groongaのパッケージ作成のアクションは、以下の場所にあります。

https://github.com/groonga/groonga/blob/master/.github/workflows/package.yml

トリガー

何のイベントをトリガーにしてアクションを実行するかを決めるには、on:を使います。
Groongaのアクションではpush毎にアクションが実行されてほしいので、on:にはpushを指定します。

on:
  push:

GitHub Actionsでは、イベントが起こった対象を指定することができます。
上記のように定義すると、リポジトリーのどのブランチ、タグにpushされてもアクションが実行されます。

全てのブランチやタグが対象ではなく、特定のブランチ、タグにpushされたときだけアクションを実行したい場合は以下のようにします。

on:
  push:
    branches:
      - branch
    tags:
      - tag

push等のイベントトリガーではなく、定期的に実行したいアクションがあるケースもあります。
その場合には、schedule:を使用します。

schedule:の書式はcronの書式で指定できます。ここで指定した時刻はUTCなので、UTCと時差がある日本の場合は時差を考慮に入れて設定する必要があることに注意してください。

Groongaでは以下のように設定し、毎朝9時にアクションが実行されるようにしています。

  schedule:
    - cron: |
        0 0 * * *

onに指定できるイベントは他にもあり、以下のページにまとまっているので、必要に応じて参考にしてください。
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows

アクションの定義

実行するアクションはjobs:という単位で定義できます。
複数のジョブを作成してそれぞれ並列に実行することもできますし、あるジョブの完了を待ってから実行するというようにジョブ同士の依存関係を定義することもできます。

Groongaでは、現状、パッケージを作成するというアクションのみを行っていますので、以下のようにbuildというジョブを1つだけ定義しています。

jobs:
  build:

ジョブの中ではいくつかのステップに分けてアクションを定義します。
GitHub Actionsのジョブはステップごとに成功、失敗を表示するので、ジョブをステップに分割しておくと、ジョブが失敗したときの原因特定が容易になります。

Groongaでは、パッケージ作成を以下のステップに分割して実行しています。

  • 依存パッケージの配置
    • aptを用いたインストール
    • pip3を用いたインストール(ドキュメントビルド用に使うSphinxをインストールしています)
    • パッケージのビルドに必要なソースコードをClone
  • configureの生成
  • ソースアーカイブ作成用にconfigure実行
  • ソースアーカイブをビルド
  • debian/changelogの更新
  • Docker上でパッケージをビルド
  • ビルドされた成果物をGitHubへアップロード

具体的には以下のように定義しています。
name:で分割したステップに名前をつけます。
run:で実行するアクションを定義します。

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master
      - name: Install dependencies
        run: |
          sudo apt update
          sudo apt -V install \
            autoconf-archive \
            bison \
            devscripts \
            python3-pip \
            ruby
      - name: Install Sphinx
        run: |
          sudo pip3 install -v sphinx
      - name: Clone dependencies
        run: |
          cd ..
          git clone --depth 1 https://github.com/groonga/groonga.org.git
          git clone --depth 1 https://github.com/clear-code/cutter.git
      - name: Generate configure
        run: |
          ./autogen.sh
      - name: Configure for archive
        run: |
          ./configure \
            --enable-document \
            --enable-mruby \
            --with-cutter-source-path=../cutter \
            --with-groonga-org-path=../groonga.org \
            --with-ruby
      - name: Build archive
        run: |
          make dist
      - name: Update version
        run: |
          OLD_RELEASE=$(grep -E -o '[0-9.]+' packages/debian/changelog | \
                          head -n1)
          OLD_RELEASE_DATE_FULL="$(grep '^ -- ' packages/debian/changelog | \
                                     head -n1 | \
                                     sed -E -e 's/ -- .+<[^>]+>  //')"
          OLD_RELEASE_DATE=$(date --date="${OLD_RELEASE_DATE_FULL}" +%Y-%m-%d)
          make update-latest-release \
            OLD_RELEASE=${OLD_RELEASE} \
            OLD_RELEASE_DATE=${OLD_RELEASE_DATE} \
            NEW_RELEASE_DATE=$(date +%Y-%m-%d)
      - name: Build with docker
        run: |
          cd packages
          rake ${{ matrix.rake_arguments }}
        env:
          APACHE_ARROW_REPOSITORY: ../../arrow
      - uses: actions/upload-artifact@master
        with:
          name: packages-${{ matrix.id }}
          path: ${{ matrix.repositories_path }}

どのOSでアクションを実行するかはruns-on:で指定します。Groongaではruns-on:ubuntu-latestと定義して、Ubuntuの最新版でアクションが実行されるようにしています。これ以外にも、windows-latest(Windows Serverの最新版)やmacos-latestなどがサポートされています。

実行するコマンドはruns-on:で定義したOSに応じて定義する必要があります。Ubuntuでアクションを実行する場合は、デフォルトでBashが使われるので、Bashで実行できるコマンドを使って定義しています

windows-latestを指定した場合は、デフォルトでアクションがPowerShellで実行されるので、アクションはPowerShellで実行できるコマンドで定義する必要があります。(コマンドプロンプトを使うように指定することもできます。)

アクションを実行するシェルの指定については、以下のページにも説明があるので、必要に応じて参照してください。
https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#using-a-specific-shell

GitHub Actionsでは、よくある処理をまとめて公開共有することが可能で、それらの処理はsteps:の中で呼び出せます。
Groongaでは、リポジトリからソースコードをチェックアウトするのにactions/checkoutを、ビルドしたパッケージをアップロードするのにactions/upload-artifactを使用しています。

actions/checkout@以下にチェックアウトしたいタグやブランチを指定することができます。Groongaでは、masterのソースコードを使いたいので- uses: actions/checkout@masterと指定しています。

actions/checkoutactions/upload-artifact以外にも様々な処理がactionとして以下のURLで公開されています。
https://github.com/marketplace?type=actions

複数の環境でアクションを実行する

複数の環境(複数のOSであったり、ソフトウェアの複数のバージョン)に対して同じアクションを実行したいケースがあります。
もちろん、それぞれの環境用にワークフローを定義しても良いのですが、同じアクションが複数のワークフローにあると、修正漏れが発生しやすくなってしまい、メンテナンスが煩雑になります。

同じアクションを複数の環境で実行したい場合は、matrix:という定義が使えます。

Groongaも各OS毎にパッケージを作成するため、このmatrix:を使ってDebianの各バージョン(stretchの32bit, 64bit、busterの32bit, 64bit)、CentOSの各バージョン(CentOS 6,7,8)に対して同じアクションを実行しています。

具体的には以下のように定義しています。

    strategy:
      matrix:
        label:
          - Debian GNU/Linux stretch amd64
          - Debian GNU/Linux stretch i386
          - Debian GNU/Linux buster amd64
          - Debian GNU/Linux buster i386
          - CentOS 6
          - CentOS 7
          - CentOS 8
        include:
          - label: Debian GNU/Linux stretch amd64
            id: debian-stretch-amd64
            rake_arguments: apt:build APT_TARGETS=debian-stretch
            repositories_path: packages/apt/repositories/
          - label: Debian GNU/Linux stretch i386
            id: debian-stretch-i386
            rake_arguments: apt:build APT_TARGETS=debian-stretch-i386
            repositories_path: packages/apt/repositories/
          - label: Debian GNU/Linux buster amd64
            id: debian-buster-amd64
            rake_arguments: apt:build APT_TARGETS=debian-buster
            repositories_path: packages/apt/repositories/
          - label: Debian GNU/Linux buster i386
            id: debian-buster-i386
            rake_arguments: apt:build APT_TARGETS=debian-buster-i386
            repositories_path: packages/apt/repositories/
          - label: CentOS 6
            id: centos-6
            rake_arguments: yum:build YUM_TARGETS=centos-6
            repositories_path: packages/yum/repositories/
          - label: CentOS 7
            id: centos-7
            rake_arguments: yum:build YUM_TARGETS=centos-7
            repositories_path: packages/yum/repositories/
          - label: CentOS 8
            id: centos-8
            rake_arguments: yum:build YUM_TARGETS=centos-8
            repositories_path: packages/yum/repositories/

strategy:の中にmatrix:を定義します。matrix:の中に変数を定義してそれをsteps:の中で${{matrix.xx.xx}}の形で参照できます。

例えば、Groongaの場合各OS向けのパッケージでビルドしたパッケージの格納場所が異なるため、${{ matrix.repositories_path }}を参照して各OSごとのパッケージをアップロードできるようにしています。

${{ matrix.repositories_path }}とすると、matrix:の定義の中にあるrepositories_path:を参照するので、

      - uses: actions/upload-artifact@master
        with:
          name: packages-${{ matrix.id }}
          path: ${{ matrix.repositories_path }}

は、packages/apt/repositories/packages/yum/repositories/配下のファイルをアップロードしていることになります。

まとめ

Groongaのパッケージ作成に使っている内容を中心にGituHub Actionsを使ったパッケージ作成の自動化の方法について記載しました。
GitHub Actionsに興味はあるが、使っていないという方は、この記事に記載されている内容を参考にして自身のプロジェクトで使用してみてはいかがでしょうか。

タグ: Groonga
2020-01-23

Prometheus Meetup Tokyo #3でLTをしました

はじめに

Prometheus Meetup Tokyo #3でLTをしてきた畑ケです。
Prometheusは近年注目されるメトリクスモニタリングツールです。特徴としては非常にスケーラビリティが高く、Pull型でメトリクスを収集するため、Prometheusのインスタンス1つで1万インスタンスのサーバー群を監視できます。このPrometheus Meetup Tokyo #3ではPrometheus本体というよりもそれを取り巻くエコシステムを対象としたミートアップでした。

筆者は最近Prometheusのエコシステムの一つでGrafana Labが出しているLokiというログ基盤を触ったことがあり、触った時の成果の一つがLTのネタになるのではないかとのことでLTに応募してみたところ、LTに採択されました。

イベントの内容

まず他の方のされた発表をご紹介します。

Remote Write API と Thanos を活用したメトリクス永続化
  • Moto Ishizawa 氏(@summerwind), Z Lab Corporation

コンテナを基盤とする環境でPrometheusを運用していると、例えば、k8sのクラスターを作り直したときにPrometheusで収集したメトリクスが消失してしまう問題があるそうです。
このメトリクスがk8sクラスターに紐づいてしまっている問題を解消するのに永続化ストレージを採用し、永続化を試みていました。ThanosというPrometheusを高可用性にし、長期間のログ保存を可能にするソフトウェアを用いて方法をPrometheusで収集したメトリクスを永続化する方法を丹念に調査していた発表でした。

Victoria Metricsで作りあげる大規模・超負荷システムモニタリング基盤
  • 入江 順也 (GitHub: inletorder)氏, 株式会社コロプラ

Victoria MetricsというこちらもPrometheusで収集したメトリクスを長期間永続化するソフトウェアを使って、Prometheusを高負荷環境にも耐えられえるようにした試行錯誤を発表されていました。このソフトウェアにたどり着くまでにいくつかのPrometheus関連のメトリクス永続化ストレージを試されたそうです。Victoria Metricsはいくつかのコンポーネント(VMStorage, VMSelect, VMInsert)に分かれており、そのうちVMStorageは持つデータによって状態を持つのでk8sではStatefulSetとしてデプロイする必要があるそうです。k8sのマルチテナント構成では合計1万Pod以上の監視を安定的に行えるようになったそうです。

次世代のログ基盤 Grafana Lokiを始めよう!
  • 仲亀 拓馬氏(@kameneko1004, さくらインターネット 株式会社), 上村 真也氏(@uesyn, Z Lab Corporation)

本ククログで何回か筆者が開発者視点で取り上げているGrafana Lokiについての発表です。Lokiについてのデモを通じてどのようなソフトウェアなのかを解説するのが前半の発表でした。後半はpromtail特集でした。promtailはpromtail.yamlにてPrometheusの設定と同様の設定を流し込むことで設定できるそうです。Prometheusと同様にGrafana Lokiも時系列データを保持するのにTSDBを使用しており、これに入れたデータをクエリするのにラベルが必要になるのですが、このラベル設計がうまくないと後に目的の時系列データをクエリするのに苦労するようです。

LTの内容

以下は筆者が行ったLTの内容です。

Grafana Lokiの開発元にFluent BitのGo製のLokiプラグインをフィードバックしてみた話をしました。
このFluent BitのGo製LokiプラグインはFluent BitからGrafana Lokiに転送する方法をまとめたときに紹介しました。
Grafana Lokiは開発が活発なソフトウェアということもあり、ドキュメントがあまり見当たりませんでした。
そのため、promtailがやっていることをソースコードを見つつFluent BitのGo製Lokiプラグインとして仕上げました。

LTではフリーソフトウェアにフィードバックする作法を軽く触れました。
Grafana Lokiもフリーソフトウェアです。フリーソフトウェアの開発は通常開発者同士が同時に同じ場所に集まっているのではなく、住んでいる国や文化、ひいては暮らしているタイムゾーンも異なる場合があります。
フィードバックするには言葉で説明しなければいけません。

まずは、動機や今困っていることを説明します。

  • なぜこの機能が必要か?なぜこの問題を報告したのか?
  • このIssueチケットやプルリクエストでは何を問題にするのか?

プルリクエストは小粒なtypo修正ではない限り、Issueチケットに関連付けられるものとして出す方が良いと筆者は考えています。しかし、この方針はプロジェクトによって異なります。プルリクエストを出す時はプロジェクトの方針を確認してみてください。

  • プルリクエストでは方針を議論するよりも提出したパッチが前もって議論した方針に合っているか、このプロジェクトに受け入れられる品質となっているか?を議論する場だからです。

プルリクエストやIssueチケットにチェックリストが付いているのであれば一通り確認すべきです。

  • 問題が発生している報告者の環境を開発者が再現するには十分な情報が書き込まれている必要があります。
  • よいIssueチケットは開発者が見た時にどのようにすればこの問題が再現できるか?がチケットを見ただけで理解できるチケットです。
  • レビュアーが見るべき箇所が発散しておらず、実現したい機能が実現できているか、パッチの変更は妥当かのレビューに集中できるものがよいプルリクエストです。

Grafana LokiにFluent BitのGo製プラグインをフィードバックしてみたところ、ユーザーもそこそこ出てきたようです。

Grafana Loki自体は一個のバイナリですし、promtailもバイナリ一個で済みます。そのため、Dockerコンテナに載せやすいです。
これらの特徴に加え、Grafana LokiはPrometheusファミリーということもありk8sと非常に親和性がよいです。

k8sに載せるにはまずDockerコンテナ化しないといけないということで、Dockerコンテナ化の要望が新たにIssueチケットとして切られました。
このFluent BitのGo製のLokiプラグインDockerイメージもGrafana Lokiの公式イメージとして提供されることになりました。

k8sでは複数のサービスを連携して動作させる必要がありますが、手動で連携させるには面倒な場合があります。
この煩雑さを解決するソフトウェアはいくつか出ています。Grafana Lokiの開発元からはこの煩雑さを解決するソフトウェアのhelmを用いたFluent BitのGo製Lokiプラグインのレシピが提供されることになりました。

筆者はこのhelmのレシピがきっかけでk8sをより深く理解することになりました。自身の成果をフリーソフトウェアにフィードバックするだけで終わりではなく、フィードバックすることにより学びのきっかけを頂けました。

まとめ

日本ではあまり事例の少ないFluent BitとGrafana Lokiを題材にしてフリーソフトウェアの開発元にフィードバックする作法をLTしました。
フリーソフトウェアの問題を手元で回避するのではなく、開発元にフィードバックするのはクリアコードが普段実践している開発スタイルです。

フリーソフトウェアを普段使っている方でもフリーソフトウェアの開発元にフィードバックする方法が分からず、手元で問題を回避していたり、手元でパッチを持ったままになっている方もいると思います。
その時には本記事のフリーソフトウェアであるGrafana Lokiの開発元へフィードバックした事例をヒントにしてフィードバックに挑戦してみてはいかがでしょうか?

また、フリーソフトウェアの開発にまだ参加したことがない人を対象にしてOSS Gateワークショップを開催しています。こちらも併せて検討してみてください。

タグ: Fluentd
2020-01-17

ノータブルコード2 - 斬新なコメントでコードの可読性を高める

この新しいコーナー「ノータブルコード」では、私たちが開発の折々に目にした興味深いコードをご紹介しています。世の中の実際のプロジェクトから、興味深い素材を肩の凝らない形でご紹介していきたいと思いますので、楽しみにしていてください。

Brubeck - 創造的なコメントで見通しを良くする

第2回目に紹介するのは、GitHubが開発したStatsD互換サーバー Brubeck からの一コマです。

int brubeck_statsd_msg_parse(struct brubeck_statsd_msg *msg, char *buffer, char *end)
{
    *end = '\0';

    /**
     * Message key: all the string until the first ':'
     *
     *      gaugor:333|g
     *      ^^^^^^
     */
    {
        msg->key = buffer;
        msg->key_len = 0;
        while (*buffer != ':' && *buffer != '\0') {
            /* Invalid metric, can't have a space */
            if (*buffer == ' ')
                return -1;
            ++buffer;
    ...

https://github.com/github/brubeck/blob/master/src/samplers/statsd.c#L140-L241

このコードが面白いのは、ずばりコメントの付け方です。

そもそもC言語でパーサを書くのは、非常に面倒な仕事です。RubyやPythonなどのより高級な言語と比べると、その手間のかかり具合は歴然としています。例えば、メッセージを単語に分割する処理ひとつ取っても、Pythonならmsg.split(" ")と書けば簡潔に記述できるのに対して、C言語だと「文字列の先頭から空白文字を探索し、見つかったらノードを初期化して連結リストに追加して次に進む。見つからなければ...」というように 『手続きの積み重ね』 として表現する必要があります。

これは書くのも面倒なら、読むのも大変なコードです。プログラムの作成者の意図は手続きの積み重ねの中に隠れてしまっているので、他の開発者はコードを地道に追うことでそれを読み取るしかないからです。

Brubeckのコードが斬新なのは、このC言語の技術的な制約をコメントの付け方によって回避している点です。冒頭にコードの一部を引用していますが、Brubeckの開発者はブロックごとに「いまここではプロトコルのこの部分を解析しています」というコメントを付与することで、書き手の意図を読み手に伝える戦略を選びました。

ポイントをはっきりさせるため、コードの一部を取り出して構成を以下に図示します。冒頭のコメントが後続する処理の宣言になっていることが明解に見て取れると思います。

Brubeckのコード解説

私たちは、このコードに感銘を受けたので、自分たちのStatsDパーサでも同じようなブロックコメントを付けることにしました

開発の現場では、時としてコメントは「おまけ」として扱われがちですが、創造的な使い方をすることで、コードの読みやすさを一変させることができるのだ、というのが今回の連載の要点です。

補足: 関連するテクニックについて

このBrubeckの手法に似ているものとして、コミットメッセージの中で記号を使って、変更内容を視覚的に示すというテクニックがあります。

こちらについては、過去記事「わかりやすいコミットメッセージの書き方」 の中で紹介しているので(記事中の「英語だけで表現することにこだわらない」の節を参照)、興味のある方はご一読ください。

2020-01-15

ノータブルコード1 - printfの縦揃え

リーダブルコードの解説の著者の須藤です。リーダブルコードの本編ではプログラマそれぞれがリーダブルなコードを書く方法を紹介しています。リーダブルコードの解説ではチームでリーダブルなコードを書く方法を紹介しています。

では、その次のステップはなんでしょう。それは「チーム外とリーダブルコードの知見を共有する」です。情報を提供するところに情報は集まります。自分たちがリーダブルコードの知見を発信することで知見が集まります。集まるのを待つだけではなく自分から知見を探しにいけばさらに知見を集められます。チーム内だけで知見を共有していると、限られたコンテキストでの知見しか得られなくなってしまいます。チーム外の知見も活用することでチームに新しい知見を導入できます。

たとえば、Ruby on Railsを使ってWebアプリケーションだけを書いている場合はそのコンテキストの知見ばかり集まります。しかし、その知見は、Pythonで機械学習するコードを書いたときに得られる知見とは違います。Pythonで機械学習するコードを書いたときに得られる知見のうちRuby on Railsを使ってWebアプリケーションを書いているときに活かせる知見もあるものです。違うコンテキストの知見を集められればもっとリーダブルなコードを書けるようになります。

実際に自分が違うコンテキストのコードを書かなくても、違うコンテキストで書かれたコードを読むだけでも知見を得られます。(もちろん、自分が書いた方が得られる知見は大きいです。)そのため、クリアコードのメンバーは他の人が書いたコードを読んで学んでいます。野生のリーダブルコードを見つけて学んでいるということです。クリアコードが大事にしているフリーソフトウェアには「プログラムがどのように動作しているか研究し、必要に応じて改造する自由 (第一の自由)」があるので野生のリーダブルコードを学び放題です。

前置きが長くなりましたが、クリアコードのメンバーが普段やっている「他の人が書いたコードから学んだこと」を「ノータブルコード(Notable Code)」としてこのブログで紹介することにしました。前から社内では紹介していたのですが、社内で閉じる必要はないので公開している場所で紹介することにしました。

「ノータブルコード」という名前にした理由は次の通りです。

  • 「リーダブルコード」っぽい!(語感が似ている)
  • 「よい悪い」というより「単に私はここに注目したよ!」というだけだから(「note」ってそういうニュアンスだよね?)
    • 「notable」には「重要な」という「よい」ニュアンスがあるみたいなので本当はアレなんですけど。。。

それでは最初のノータブルコードを紹介します。

Apache ArrowのRのLinux向けパッケージを改良するプルリクエストで見つけたものです。

sprintfを使って文字列をフォーマットしているRのコードです。Rのコードですが、sprintfはいろんな言語にある機能なので、Rを読み書きできない人でも読めるはずです。

https://github.com/apache/arrow/pull/6068/files#diff-3875fa5e75833c426b36487b25892bd8R153-R155

  env_vars <- sprintf(
    "SOURCE_DIR=%s BUILD_DIR=libarrow/build DEST_DIR=%s CMAKE=%s",
    src_dir,                                dst_dir,    cmake
  )

sprintfはフォーマット文字列に実際の値を埋め込んで最終的な文字列を生成しますが、どの値がどこに対応するかわかりにくくなりがちです。このコードでは縦に埋め込み場所と埋め込まれる値を揃えて対応をわかりやすくしています。

他のやり方として、文字列内に式を埋め込む方式(Rubyなら"SOURCE_DIR=#{src_dir}"と書く)や、埋め込み場所に名前を使う方式(Rubyなら"SOURCE_DIR=%{src_dir}" % {src_dir: src_dir}と書ける)がありますが、このスタイルは初めてみました。1行が長すぎると使えないとか、埋め込む値の式が長いと使えないとか使える機会は限定される気がしますがおもしろいなぁと思いました。

おもしろくて思わずコメントしてしまいました。このコードを書いた人によるとリーダブルなコードになるようにいろんな方法を実験していたとのことでした。やっぱり工夫して書いたものだったんですね。

2020年はみなさんも気になったコードを「ノータブルコード」として共有してみませんか?

2020-01-14

Apache Arrow東京ミートアップ2019 参加レポート

Apache Arrow 1.0.0の正式リリースを間近に控えた先週12月11日に、第二回目となるApache Arrow東京ミートアップが開催されました。
今回の会場は株式会社Speeeの六本木オフィスをご提供いただきました。O'Reilly本が並んだ書棚を背景に、シックな木目の家具で統一された会場で、とても素敵な会場でした。

Apache Arrow Tokyo Meetup 2019

ミートアップの内容としては、まずプログラムの冒頭で弊社の須藤から簡単なガイダンスがあった後、7人の講演者による発表がありました。
本記事の作者(藤本)の聴講メモより、各発表の要点をまとめます(箇条書き外は藤本の感想です)。

Apache Arrowの最新情報 (株式会社クリアコード/須藤)

スライド資料リンク (Rabbit)

  • Apache Arrowとは多言語に対応したオンメモリのデータ形式(とそれに関連する一連のデータ処理ライブラリ)である。
  • 列指向の考え方を採用していて、できるだけコンピューターがそのまま扱えるような形式で作られている。
  • 用途としては、プロセス・ホスト間のデータの受け渡しに使われている。シリアライズとパースの手間が減るので、相当効率化できる(具体的には、後の山室さんの発表を参照のこと)。
  • 技術提供できるので、使ってみたい企業の方はお問い合わせください。
Apache Arrow Datasets C++ (株式会社Speee/村田さん)

スライド資料リンク (Speaker Deck)

  • Datasetsとは、様々な場所にあるファイルデータ(ローカルディスクやAmazon S3など)を読み出して、Apache ArrowのTableオブジェクトに統合してくれるレイヤ。
  • パスによって階層的にデータを保存する設計になっている。これで一部のデータのみを読み出したいという要望にも応えられる。
  • 現時点でファイル形式はApache Parquetのみ対応しているが、今後CSVやJSONなどの各種形式の対応が計画されている。
  • これを基盤として、SQLライクなクエリを実行するクエリエンジンや、集計処理を実行するデータフレームが作られていく予定。

Apache Arrowプロジェクトが単なるデータ形式にとどまらず、ストレージや計算実行まで拡大しつつある状況がよく分かる報告でした。また「まだ全体的に議論が進んでいる段階で、実装の手が届いていないところも多いので、積極的に開発に関与すると喜ばれるかもしれない」とのことでした。

RとApache Arrow (湯谷さん)

スライド資料リンク (Speaker Deck)

  • RとApache Arrowの統合が格段に進みつつあるという報告。
  • 一つの進展として、Apache Sparkのライブラリ sparklyr にApache Arrow対応が入った。これでApache Sparkとの間でApache Arrowで直接やりとりできるようになった。
  • また、Apache Parquetの読み書きもできるようになった。Pythonなどの他の言語との連携が期待できる。
  • その他にも、CRANで手軽にインストールできるようになった等の進展がある。
PostgreSQLとApache Arrowの利用事例 (ヘテロDB株式会社/海外さん)

スライド資料リンク (SlideShare)

  • PostgreSQLのForeign Data WrapperでApache Arrow形式のデータを扱えるようにしたという報告。
  • 列指向なので、参照されているデータのみを読み出せるなどの良い特性がある。GPUメモリバスの特性から、演算実行も高速。
  • 自社製品のPG-Stormと組み合わせることで、シングルノードで10億レコード/秒程度の性能を出せることが実証できた。
Apache SparkとApache Arrowの連携 (NTT/山室さん)

スライド資料リンク (SlideShare)

  • Apache SparkでApache Arrowを活用できる箇所は大きく二つある。1つはプログラムからクラスタにデータを投入する箇所、もう1つはクラスタの内部でデータを受け渡している箇所。
  • 1つ目のユーザーとのやりとりは@pandas_udfというデコレータを使うだけでApache Arrow形式を使えるようになっている。ベンチマークの結果から、これでかなり高速化できることが分かっている。
  • 2つ目のクラスタ内部でのデータのやりとりは、設定で有効化できるようになっている。一部の型が未対応なのでデフォルトではオフになっている。
  • 未対応の型などの問題も、今後のバージョンで解消されていく予定。

なぜApache Arrowが開発されたのかとても納得がいく発表でした。要するに、並列計算のパラダイムのシステムでは、設計上、計算を実行するノード間で膨大なデータのコピーが発生する。このため、パースの手間を可能な限り省いたApache Arrowのようなデータ形式を採用することで、飛躍的な速度の向上が見込めるのです。

TensorFlowとApache Arrowの連携 (日本アイ・ビー・エム株式会社/石崎さん)
  • 同僚であるIBM Spark Technology Centerのブライアンさんの代理報告とのことでした。
  • Tensorflow I/Oというデータ入出力をサポートするライブラリがあり、この中でApache Arrowもサポートされている。
  • Apache Arrowの中に高速なCSVの読み出し実装もあるので、この点でも効率化をはかることができる。
  • 具体的なベンチマークはもう少しブラッシュアップしてから公表したい、とのことでした。
TensorFlowとBigQuery Storage APIとApache Arrowの連携評価事例 (SENSY株式会社/漆山さん)

スライド資料リンク (Google Docs)

  • 「感性を学習する」をテーマに、数百GBから数TBのデータの購買データを分析されているとのことでした。
  • データの保存にBigQueryを使っていたが、このStorage APIにApache Arrow対応が入ったので、これを利用されているとのこと。
  • ベンチマークをとってみると、Apache Arrowを使うことで、データ取得に要する時間はもちろん、その後の学習も高速化されたとのことでした。

まとめ

ここで講演は終了して、残りの時間ではSpeeeさんにご提供いただいた夕食を食べながらの懇親会がありました。立食形式で興味のある講演者の方と直接お話することができるようになっていて、それぞれのテーブルでは活発に議論が交わされていました。

以上でApache Arrow東京ミートアップ2019 は終了しました。3時間という枠で様々な側面からの発表が聞けた充実した会だったと思います。会場を提供頂いた株式会社Speeeさんをはじめとする関係者および講演者のみなさまには改めてお礼申し上げます。ありがとうございました。

2019-12-16

Apache Arrow東京ミートアップ2019:Apache Arrow 2019 #ArrowTokyo

必要に応じてイベント企画もがんばる須藤です。

2019年12月11日にApache Arrow東京ミートアップ2019を開催しました。去年のApache Arrow東京ミートアップ2018に引き続き2回目の開催です。今回もSpeeeさんに会場提供・懇親会用飲食物提供などもろもろお世話になりました。ありがとうございます!

関連リンク:

内容

Apache Arrowの最新情報(2019年9月版)で紹介した通り、Apache Arrow 1.0.0がリリースされようとしています。おそらく、2020年2月くらいにリリースされるのではないかと思います。そこで、今年のApache Arrow東京ミートアップはApache Arrowユーザーを増やすことを目指して企画しました。

私のパートでは、Apache Arrowをまだ知らない人向けにApache Arrowが実現しようとしていることを紹介しました。技術的な詳細にはあまり立ち入らず、要点を理解してもらえるといいなという感じでまとめたつもりです。Apache Arrowについてを知るために最初にざっと確認する資料として活用できるものになっているといいな。

ユーザーを増やす取り組み

去年のイベントではApache Arrowの開発に参加する人を増やそうと企画しましたが、今年は1.0.0のリリース(初のメジャーリリース!)が近いということもあり、Apache Arrowのユーザーを増やすキッカケになるように企画しました。

発表内容を次のような内容にしたのはユーザーを増やすことを目指したためです。

  • Apache Arrowをまだ知らなかった人でもわかる内容
  • Apache Arrowの最新情報を紹介する内容
  • すでにApache Arrowを活用しているプロダクトを紹介する内容
  • 実際にApache Arrowを使ってみた事例を紹介する内容

参加した方はApache Arrowを使ってみたくなったでしょうか?参加できなかった方は以下に資料が公開されているので参考にして使い始めてみてください。

このような情報を提供する場にすることでApache Arrowを使い始める最初の敷居を下げることを狙いました。

さらに、もっと敷居を下げる施策として次の2つを実施しました。

  • 懇親会で詳しい人に相談するように促す
  • クリアコードも含めてPoCから始める案を提示する

クリアコードはApache Arrow関連の技術的詳細に詳しいです。まずはPoCから始めたいという方を支援できるのではないかという考えからこの案を提示しました。今回のイベントに参加していない方からのPoC支援も受け付けていますので、興味のある方はお問い合わせください。

まとめ

Apache Arrow 1.0.0のリリースが近づいてきたのでApache Arrowユーザーを増やすためにApache Arrow東京ミートアップ2019を開催しました。イベント開催を全面的に支援してくれたSpeeeさん、発表してくれたみなさん、参加者のみなさんには今回も大変お世話になりました。またお世話になると思うのでそのときもまたよろしくおねがいします!

2019-12-11

FirefoxでInternet Explorerのwindow.open()周りの挙動を再現する

日本におけるFirefoxの法人利用の代表的なニーズは「Internet Explorer向けに作られた古いWebベースの社内システムがあるためにIEを捨てられず、社外のWebを見に行くためにFirefoxを使いたい」というものです。しかし、それとは逆のパターンとして、「IE向けに作られたWebベースのシステムをFirefoxで使いたい」という場面もあります。後者の場合にはIEとFirefoxの細かな挙動の違いが問題となることがあり、window.open() でウィンドウを開くときの挙動の違いもその一つです。

名前付きの子ウィンドウ(タブ)の挙動の違い

window.open() はJavaScriptから利用できるブラウザの機能(いわゆるDOM0)で、子ウィンドウ(タブ)を開く物です。第2引数には「子ウィンドウの名前」を指定することができ、この方法を使って名前を付けて開かれた子ウィンドウ(タブ)は、<a target="ウィンドウ名" href="..."> のようにtarget属性を明示したリンクの読み込み先や、window.open() で同じウィンドウ名を指定した場合の読み込み先として再利用されることになります。

Internet Explorerでは、このような場面で以下のような挙動を取ります。

  • すでにその名前の子ウィンドウ(タブ)が開かれていた場合、とにかくそれを読み込み先として使う。
  • 読み込み先の子ウィンドウが最小化状態であれば、通常の状態に復帰させる。他のウィンドウの背後に隠れている場合であれば、最前面に持ってくる。

他方、Firefoxでは同じ場面で以下のような挙動を取ります。

  • 子ウィンドウはそれぞれのタブに対して個別に管理される。2つのタブそれぞれで子ウィンドウを名前を指定して開いた場合、それぞれのタブに対して別々の子ウィンドウが開かれる。その後のウィンドウ名を指定した読み込みも同様に取り扱われる。
    • →IEの挙動を前提に作られたWebアプリだと、無駄にたくさん子ウィンドウが開かれてしまう。
  • 読み込み先の子ウィンドウが最小化状態や他のウィンドウの背後に隠れている場合でも、そのままの状態で読み込みを行う。
    • →IEの挙動を前提に作られたWebアプリだと、「リンクやボタンをクリックしても(最小化された、または背後にあるウィンドウの中に読み込まれる場合があり)結果を画面上で確認できないという事が起こる。

Firefoxアドオンでの解決

Webアプリとしては、このような挙動の違いを吸収したり、Firefoxのような挙動でも問題が起こらないようにしたりと対策を取れればそれに越したことはありません。しかし、様々な事情からそれができない場合もあります。

そのようなお客さまからのご相談を受けて、Firefox上でIEの上記の挙動を再現するためのアドオンを開発しました。以下のリンクからインストールできます。

  • Merge Named Browser Windows:ウィンドウ名を指定して子ウィンドウ(タブ)が開かれた場合に、親ウィンドウ(タブ)を問わずその名前の既存のタブを探して閉じ、特定の名前のタブが1つだけ開かれているという状態を維持する。
  • Unminimize Browser Window On Load:最小化状態や背面にあるウィンドウにコンテンツが読み込まれた場合に、ウィンドウを復帰させフォーカスを与える。

主にセキュリティ向上のために、現代のWebブラウザはウィンドウ(タブ)間での情報の分離を徹底する方向で進化しています。また、迷惑な広告やいわゆるブラクラの類にユーザーが煩わされる事が無いよう、コンテンツ側からの操作によるウィンドウのフォーカス制御も制限されるようになっています。それらの変化は一般的なWebサイトを閲覧するユーザーにとって好ましいものですが、特定のWebアプリの使用頻度が高くなる法人利用では却って困った結果になる事もあります。

そのような特殊な場面での暫定的な対処のための方法として、一般的なWebアプリよりも強力な権限が与えられているアドオン(拡張機能)は有用な選択肢となる場合があります。Firefoxの法人利用で似たような事にお悩みの企業担当者さまがいらっしゃいましたら、お問い合わせフォームより是非ご連絡下さい。

タグ: Mozilla
2019-11-26

2019年、fat gemをやめる

fat gemを簡単に作れるようにするgemであるrake-compilerをメンテナンスしている須藤です。過去にfat gemの作り方をまとめたこともあります。

fat gemが有用な時代もあったのですが、今はメリットよりもデメリットの方が大きいのでfat gemをやめたらどうか、という話をします。

fat gemについて

fat gemとはビルド済みバイナリーが入ったgemのことです。Pythonで言えばwheelのようなものです。

RubyはC言語でRuby用のライブラリーを実装することができます。これを拡張ライブラリーと呼びます。拡張ライブラリーの用途は主に高速化(Rubyで実装したよりCで実装した方が速い)とバインディング(C・C++言語で実装されたライブラリーをRubyから使えるようにするライブラリー)です。

拡張ライブラリーをインストールするにはC言語のプログラムをビルドしなければいけません。ビルドするためにはインストール時にC言語のビルド環境が必要になります。ユーザーの環境にC言語のビルド環境がないとインストールできません。つまり、インストールの敷居が高くなります。

このインストールの敷居が高い問題を解決するものがfat gemです。fat gemにはビルド済みのバイナリーが含まれているためユーザーはC言語のビルド環境を用意しなくてもいいのです。やったー!

fat gemの問題点

fat gemのおかげでユーザーはハッピーになれそうですね。でも、本当にそうでしょうか?実際に、fat gemを作ってきた経験からfat gemの問題点を説明します。

新しいRubyを使えるまでにタイムラグがある

Rubyは毎年クリスマスに新しいバージョンがリリースされます。(メンテナンスリリースは必要に応じて随時行われています。)

fat gemにはビルド済みのバイナリーが含まれています。これは、だれかが事前にビルドしてくれているということです。だれかというのはgemの開発者です。

Rubyはバージョン間でのABIの互換性を保証していません。たとえば、Ruby 2.6用の拡張ライブラリーはRuby 2.7では使えません。そのため、新しいバージョンのRubyがリリースされたらそのバージョンのRuby用にビルドしないといけません。(メンテナンスリリースでは互換性があるのでRuby 2.6.0用の拡張ライブラリーはRuby 2.6.5でも使えます。)

つまり、新しいRubyがでてもgemの開発者が新しいRuby用のfat gemをビルドしてくれていなければ新しいRubyを使えません。すべてのgemがRubyのリリースにあわせて開発スケジュールを立てているわけではないので、クリスマスから数ヶ月遅れて新しいRuby用のfat gemをリリースすることも十分ありえます。

リリースされるならまだいいですが、メンテナンスモードになっているgemは重大な問題がなければ数ヶ月経ってもリリースされないかもしれません。もし、リリースされないgemが変更無しで新しいRubyでビルドできたとしてもユーザーは使えません。(RubyのC APIはそんなに劇的に変わらないのでだいたいビルドできます。)

もし、ユーザーが複数のfat gemに依存している場合はすべてのfat gemが新しいRubyに対応しなければ新しいRubyを使えません。1つでも新しいRubyに対応していないとインストールできないからです。

脆弱性対応までにタイムラグがある

バインディングをfat gemにするということはバインディング対象のライブラリーもfat gem内に入れていることになります。もし、バインディング対象のライブラリーに脆弱性があった場合は迅速に修正版をリリースするべきです。そうしないとユーザーが危険な状態が伸びてしまうからです。

しかし、すべてのfat gem開発者がそんなにタイミングよく作業できるわけではありません。そのため、脆弱性対応リリースが遅くなりがちです。

念のため補足しておくと、これはfat gemにしていない場合でも発生しえます。たとえば、Nokogiriのようにデフォルトで特定バージョンの依存ライブラリーをビルドするタイプのgemでも発生しえます。指定したバージョンのライブラリーに脆弱性があったらバージョンを更新してリリースしないと脆弱なバージョンを使ったままのユーザーが増えてしまいます。明示的に--use-system-librariesを指定すればNokogiriのバージョンを上げなくても対応できるのですが、残念ながら多くのユーザーはそこまで頑張ってくれないでしょう。

新しい依存ライブラリーを使えるまでにタイムラグがある

これもバインディングの場合ですが、バインディング対象のライブラリーが新しいバージョンをリリースしてもfat gemを更新しなければユーザーは新しいバージョンを使えません。

fat gemに対応するとrequireが遅くなる

fat gemに対応するには次のようなコードを入れる必要があります。2.6/io/console.so(ビルド済みバイナリー)があればそっちを優先し、なければio/console.so(自分でビルドしたバイナリー)を読み込むというロジックです。

begin
  require "#{RUBY_VERSION[/\d+\.\d+/]}/io/console.so"
rescue LoadError
  require 'io/console.so'
end

すべてのケースでfat gemを使うなら↓だけで大丈夫です。

require "#{RUBY_VERSION[/\d+\.\d+/]}/io/console.so"

しかし、それではユーザーが自分でビルドして使うという選択肢がなくなります。逆に言うと、開発者がすべてのプラットフォーム向けにfat gemを用意する覚悟を決める必要があります。

それは現実的ではないので、普通は前述のように自分でビルドしたバイナリーにフォールバックします。

そうすると、fat gemを提供していない環境では必ずfat gem用のrequireが失敗します。この分requireが遅くなるということです。$LOAD_PATHにたくさんのパスが入っている環境では無視できないくらい遅くなります。gemをたくさんインストールしているとその分$LOAD_PATHも大きくなります。たとえばRuby on Railsアプリケーションではたくさんのgemを使うことになるので影響が大きいです。

これを回避するために、fat gemを提供している環境でだけフォールバックする対策をとっているgemもあります。(ありました。)

fat gemのリリースを忘れる

多くのgemはrake releaseだけでgemをリリースできるようにしています。そしてこれはすぐに完了します。.gemファイルを作ってrubygems.orgにアップロードするだけだからです。(他にもgit tagをするとかちょろっとしたことをしています。)

しかし、fat gemをリリースするにはもう一手間必要です。各環境用のバイナリーをビルドしてそれぞれの環境毎にfat gemを作り、それらをrubygems.orgにアップロードします。

各環境用のバイナリーは大変です。rake-compiler-dockを使えば楽になりますが、それでも面倒です。

その結果どうなるかというとリリースが億劫になったり、fat gemのリリースを忘れたりします。たとえば、Ruby-GNOMEはリリースが億劫だなと思っていました。たとえば、io-console 0.4.8はfat gemのリリースを忘れていました

fat gemのリリースが大変

fat gemのリリースは大変なんです。特にバインディングのfat gemのリリースは大変です。

私はRuby-GNOMEでWindows用のfat gemを作っていました。バインディング対象のGTKなどのライブラリーはLinux上でMinGWを使ってクロスコンパイルしていました。これがすごく大変です。というのは、クロスコンパイルしている人がほとんどいないので、バインディング対象のライブラリーのバージョンを上げるとビルドエラーになることがよくあるからです。Ruby-GNOMEをリリースするたびにアップストリームにパッチを送っていたものです。ただ、librsvgがRustを使うようになってクロスコンパイルできなくなったときにfat gemをやめる決心をしました。

fat gemはそんなにポータブルじゃない(気がする)

fat gemは主にWindows向けに提供されていますが、Linux向けに提供している野心的なgemもあります。たとえば、sasscです。

Windowsはバージョンが限られていますし、後方互換性があるので古いWindows向けにビルドしていればいろんなWindowsでもだいたい大丈夫です。

しかし、Linuxディストリビューションはたくさんあり、使っているlibcも違います。スタティックリンクしたバイナリーを用意すれば大丈夫なのかというとそうでもない気がします。どうなんでしょうか。。。?

Pythonのwheelではmanylinuxという(だいたいの)Linux環境で動く仕組み(?)を用意しているので、このくらい頑張れば大丈夫なのかもしれません。が、私としては、この方向で頑張っちゃうの。。。?という気持ちになります。RubyGemsはそうなって欲しくないなという気持ちです。

fat gemの問題点の解決方法

ここまででfat gemの問題点をまとめました。それではfat gemの問題点を解決する方法を示します。それはfat gemをやめることです。どーん!

そもそもfat gemが必要だったのはユーザーがビルド環境を持っていないことが多かったからです。しかし、今は状況が変わっています。ユーザーがビルド環境を持っていないプラットフォームの代表はWindowsでしたが、今はRubyInstaller for Windowsがほぼ標準でDevKitを提供しています。Ruby 2.3以前はそうではなかったですが、Ruby 2.3がEOLになったので、今はWindowsユーザーでもビルド環境があるのです。

Linuxではパッケージをインストールすればすぐにビルド環境を整えられます。

macOSでもXcodeをインストールすればビルド環境を整えられます。Homebrewを使っている人はすでに整っているはずです。

他の環境(たとえば*BSD)でもビルド環境はすぐに整えられるでしょう。

つまり、今はユーザーがビルド環境を持っていると仮定してもよい状態になっています。そのため、fat gemを提供しなくてもユーザーがインストールできる状態が整っています。実際、私はWindowsユーザーがいるRuby-GNOMEでfat gemをやめましたが、最近はWindowsでのインストールトラブルはほとんど報告されていません。

それではfat gemをやめるとうれしいことをまとめます。

新しいRubyをすぐに使える

新しいバージョンのRubyは以前のバージョンのRubyとC APIが変わっている可能性があります。たとえば、Ruby 2.7ではrb_f_notimplement()が変わります。(Ruby 2.7の対応が必要な例)

しかし、多くのC APIは互換性があるのでなにも変更しなくても新しいRubyで動くことが多いです。その場合は、特になにもしなくてもすぐに新しいRubyを使えます。ユーザーは単に新しいRubyを使ってgemをインストールすればよいだけだからです。

また、もしRuby 2.7で動かない場合でも事前にプレビュー版で動作確認し、Ruby 2.7より前にRuby 2.7対応版をリリースしておくこともできます。こうすればクリスマス後にリリースしなくてもよくなるのでgem開発者に余裕があります。

fat gemを使った場合でも、プレビュー版でバイナリーを作ってリリースしておくことができなくはありませんが、rake-compiler-dockなど各種ツールが事前に対応していないと難しいです。

脆弱性対応をシステムに任せられる

バインディングをシステムのライブラリーを使ってビルドするようにしていた場合、バインディング対象のライブラリーに脆弱性があってもシステムのライブラリーを更新すれば対策できます。gemの開発チームよりシステムのライブラリーをメンテナンスしている人たちの方が層が厚いいので迅速に脆弱性に対応してもらえます。

なお、Nokogiriのようにデフォルトで依存ライブラリーを自前で管理するタイプのgemはfat gemでもそうでなくても関係ありません。

新しい依存ライブラリーをすぐに使える。。。こともある

バインディングをシステムのライブラリーを使ってビルドするようにしていた場合、バインディング対象のライブラリーの更新はシステムのパッケージシステムが面倒をみてくれます。Debian GNU/Linux sid、Fedora Rawhide、ArchLinux、Homebrew、MSYS2などのように最新のバージョンに随時アップデートされるシステムではgemの更新を待たずに新しいライブラリーを使えます。

ただし、ライブラリーのバージョンアップでAPIが変わった場合はgemの更新が必要です。

requireが遅くならない

fat gem用のrequireがいらなくなるので失敗するrequireを実行しなくてもよくなります。これによりrequireが遅くなりません。

bigdecimalがfat gemのサポートをやめたのはこれが理由です。

開発コストが下がる

面倒なfat gemのリリースをしなくてよくなるので開発者は本来の開発にリソースを注力できます。

最適化ビルドできる

fat gemは事前にビルドしたバイナリーをすべてのユーザーが共通で使うことになるので最大公約数の最適化しかできません。

しかし、fat gemをやめて各ユーザーごとにインストールする場合はその環境毎に最適化できません。たとえば、速度が非常に重要な拡張ライブラリーをGCCでビルドする場合は-O3 -march=nativeというオプションをつけてビルドするとその環境向けに最適化されます。たとえば、CPUがSIMDをサポートしていればSIMDを使ったバイナリーを生成することもあります。

fat gemをやめたときの問題点と解決策

fat gemをやめるとユーザーも開発者もハッピーになれそうですね。でも、本当にそうでしょうか?fat gemをやめたときの問題点とその解決策をまとめます。

インストール時間が長くなる

fat gemの場合はビルド済みのバイナリーをコピーするだけなのですぐにインストールは完了します。しかし、fat gemをやめるとインストールするたびにビルドすることになるので時間がかかります。

解決策は。。。特にありません。。。

依存ライブラリーがなくてインストールが失敗しやすくなる

バインディングはバインディング対象のライブラリーがないとインストールに失敗します。たとえば、RMagickはImageMagickがないとインストールに失敗します。Nokogiriがデフォルトで自分で依存ライブラリーをビルドするようになっているのはこの失敗を防ぐためです。

たしかに、自分でビルドしてしまうというのはこの問題の解決策の1つではあります。ただ、そんなに筋がよいとは思えません。脆弱性があったときの対応に関する問題があるからです。

私がオススメする方法はシステムのパッケージシステムを使って自動で足りない依存ライブラリーをインストールする方法です。このための便利gemがnative-package-installerです。私が開発しています。

native-package-installerはpkg-config gemと一緒に使うことを想定していて、extconf.rbに次のように書いておけば、cairoがインストールされていなければ自動でインストールします。

require "pkg-config"
require "native-package-installer"

unless PKGConfig.have_package("cairo")
  unless NativePackageInstaller.install(:arch_linux => "cairo",
                                        :debian => "libcairo2-dev",
                                        :homebrew => "cairo",
                                        :macports => "cairo",
                                        :msys2 => "cairo",
                                        :redhat => "cairo-devel")
    exit(false)
  end
  unless PKGConfig.have_package("cairo")
    exit(false)
  end
end

なお、RubyInstaller for Windows用のRubyではgemのメタデータにMSYS2のパッケージを指定しておくことで同じ機能(自動で依存ライブラリーをインストールする機能)を実現できます。以下はcairo.gemspecでの例です。

gemspec.metadata["msys2_mingw_dependencies"] = "cairo"

参考:MSYS2 library dependencies - For gem developers - onclick/rubyinstaller2 Wiki

ビルドに失敗してインストールが失敗しやすくなる

fat gemではすでにビルド済みなのでビルドが失敗することはありません。開発者が用意した環境でビルドが成功すればOKです。

一方、ユーザーの環境でビルドする場合は、開発者の環境では成功しているのにユーザーの環境では失敗することがあります。

解決策は、CIでサポートしている環境を常にテストすることです。Travis CIやGitHub Actionsなどを使えば、いろんな環境でテストできます。Linuxの亜種はDockerを使うとよいでしょう。

まとめ

検討するべき項目が他にもある気がしますが、一通りまとめたので公開します。fat gemをやめたくなりましたか?それともfat gemはやめないで!という気持ちになりましたか?

もし、これはどうなの?という項目があったらなんらかの手段で私に聞いてください。回答します。

タグ: Ruby
2019-11-22

「Fluentd」のサポートサービスを開始しました。

クリアコードでは2015年からFluentdの開発コミュニティに参加し、Fluentd本体とプラグインの開発、サポート、各種ドキュメントの整備を行っています。
2016年以降、既存の取引先様に対してFluentdのサポートを提供していましが、この度満を持してサポートサービスの開始を宣言しました。
サービスの概要はこちらです。
先日プレスリリースも出しましたので、併せてご参照ください

Fluentdに関するお問い合わせはこちらまで。

タグ: Fluentd
2019-11-20

タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|