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

ククログ


Thunderbird 78に未対応のアドオンのThunderbird 78対応の現状と課題について

Mozillaサポート業務に従事している結城です。

去る7月17日、Thunderbird 78がリリースされました。この記事では、Flex Confirm MailのThunderbird 78対応版開発作業*1を通じて得られた知見を元に、「アドオンのThunderbird 78対応化」が可能かどうかの大まかな判断基準や、参考となる情報をご紹介します。

Thunderbird 78における従来型アドオン対応終了の経緯

Thunderbird 78では従来形式のアドオン(以下、「XULアドオン」と表記します)が一切動作しなくなり、WebExtensionsと呼ばれるAPIセット*2に基づく新形式のアドオン(以下、「WebExtensionsアドオン)のみが動作するようになりました。これは、Firefox 57において行われた、XULアドオンの廃止によるWebExtensionsアドオンへの一元化に相当する変化となります。

Thunderbirdは、対応するバージョン番号のFirefoxがベースになっているため本来であれば、XULアドオンはThunderbird 60で動作しなくなるところでした。しかし、ThunderbirdのWebExtensions APIは開発リソースの不足のために機能が揃っておらず、Thunderbird 60やThunderbird 68の時点でXULアドオンに対応しなくなると、代替手段がなくなってしまう状況でした。そのため、Firefoxでは無効化された機能を復活させたり、廃止された機能を代替する独自の互換レイヤーを実装したりして、XULアドオンの延命措置が図られていました。

今回のThunderbird 78では、WebExtensions APIの実装がある程度の完成度に達したため、XULアドオンの延命に終止符が打たれ、ようやく満を持してWebExtensionsアドオンへの全面移行が行われたということになります。

XULアドオンとWebExtensionsアドオンの相違点

WebExtensionsアドオンは、エンドユーザーからはXULアドオンと区別が付かなくても、開発者側視点ではまったくの別物です。
(FirefoxにおけるXULアドオンとWebExtensionsアドオンの差異を示した図。XULアドオンはFirefoxの内部にUIからバックエンドまで深く癒着しているのに対し、WebExtensionsアドオンはサンドボックス内に公開されたAPIのみに依存するため、疎結合となっている。)
こちらはFirefoxにおけるXULアドオンとWebExtensionsアドオンの差異を図示したものですが、この説明はThunderbirdにもそのままあてはまります。

XULアドオンは、実態としてはThunderbirdに対して動的に適用されるパッチに近い存在でした。そのため、Thunderbirdの動作を自由自在に書き換えることができ、非常に自由度が高いのが特徴でした。その反面、パッチを当てる対象となるThunderbirdの内部実装に対して極めて密結合となるため、Thunderbird側の変更の影響を受けやすく、Thunderbirdの更新の度にどこかしらの部分が動作しなくなりやすい性質がありました。

これに対しWebExtensionsアドオンは、サンドボックス化された環境の中で動作し、WebExtensions APIとして用意されたインターフェースのみを通じてThunderbirdと相互に通信する、独立した小型のソフトウェアと言えます。Web開発者の視点では実質的には、Thunderbirdというバックエンドと通信するためのWebExtensions APIというアダプターを使った、小規模なWebアプリ(のフロントエンド) とも言えるでしょう。Thunderbirdの内部実装に対しては疎結合となるため、Thunderbirdの更新で動かなくなるリスクは大幅に減じられました。その一方で、APIが用意されていないことは一切行えず、自由度の幅は大きく制約されています。

このような違いがあるため、XULアドオンからWebExtensionsアドオンへの更新作業(WebExtensions化)は、特にXULアドオンとして適切に設計されていた物ほど、「Thunderbird 78に対応するための小規模な改修」と言うよりも、「既存のアドオンの動作をリファレンスとして、それと同等の結果を得られるようなソフトウェアを新たに開発する」と言った方が実態に近い、極めて大がかりな作業となります*3

そのため、いくつかの著名アドオンについて、WebExtensions化を支援するクラウドファンディングが行われた事例もあります。このクラウドファンディングは目標金額を達成したようですが、本稿執筆時点で各アドオンのWebExtensions版は未リリースであることからも、上記の作業の大変さを窺い知れます。

WebExtensionsアドオン開発の注意点

「アドオンのWebExtensions化」の成功に必要なポイントは、

  • したいことを実現するのに必要な機能がWebExtensions APIで提供されているかどうか、WebExtensionsの仕組み上で許可されているか
  • WebExtensions APIが用意されていない部分をどのように実装するか

の2点に集約されます。

WebExtensions APIの傾向

FirefoxのWebExtensions APIの情報はMDNに集約されていますが、ThunderbirdのWebExtensions APIの情報は、OSS向けの技術文書ホスティングサイトであるRead The DocsにThunderbird WebExtension APIsとして集約されています。実際のアドオン開発にあたっては、導入部はMDNのチュートリアルを参照しつつ適宜Thunderbird向けに読み替えて、要領が掴めてきたらThunderbirdに固有のAPIをThunderbird用のドキュメントで調べる、という要領になるでしょう。

全体的な傾向としては、以下の事が言えます。

  • Thunderbirdの各ウィンドウの内容には、ブラウザの「タブ」としてアクセスする。
    • メール表示ウィンドウと編集ウィンドウにはタブは存在しないが、「タブが1つだけ開かれているウィンドウ」として扱われる。
  • ツールバーボタンは1アドオンにつき、メール表示画面用に最大1つ、メール編集画面用に最大1つまで登録できる。
  • 表示中・編集中のメールの本文は「コンテンツ」として扱われ、WebExtensions APIで言うところのコンテンツスクリプトで操作する。
  • アドレス帳やメールフォルダなどへのアクセス方法は、WebExtensionsでのブックマークの操作に似ている。
  • Thunderbird上で発生したイベント・行われた操作は、原則として事後的に通知されるため、アドオンからのキャンセルはできない。

APIの一部はFirefoxと共通で、詳細はMDNを参照するように誘導されている場合があります。ただ、Firefoxでは行えるはずのことがThunderbirdの同名のAPIでは行えない部分もいくつかあります。筆者自身、Thunderbird用のWebExtensionsアドオンの開発中にいくつかそのようなトラブルに遭遇し、以下の通りバグとして報告しました。

このように、細かい部分ではまだまだ不具合が散見されるため、APIを使う際には若干の注意と工夫が必要です。

なお、Thunderbirdの内部的な実装にアクセスする実験用APIも存在していますが、このAPIの使用はおすすめできません。今後のThunderbirdの開発ロードマップにも記されているとおり、今後はThunderbird内の古い実装はどんどん廃止されていく予定のため、このAPIに依存するアドオンでは、「Thunderbird本体が更新されたらアドオンが動かなくなる」というXULアドオン時代の問題が継続してしまうからです。

APIでカバーされない、ユーザー体験に直接関わる部分

WebExtensions APIは「アドオンを作りやすくする便利機能」ではなく「Thunderbirdをアドオンから操作するための基盤」として設計されています。昨今のWeb APIの仕様と同様、ユーザー体験に直接関わる部分はアプリケーション(アドオン)開発者側で工夫して実現する必要があります。そのような工夫を一般化した物が「ライブラリ」や「ツールキット」です*4

Webページ用としては、昔からjQuery UIなどの独自のUIツールキットがいくつも開発されてきました。WebExtensionsアドオンは実質的には小規模なWebアプリだと述べましたが、このようなWebアプリ用ツールキットをWebExtensionsアドオンでも使うことができます。

ただ、Webページ用のUIツールキットは「Webページの中でダイアログのような物を表示する」のように、良くも悪くもWebページ内で動作が閉じる前提であることが多いです。Webページよりももう少し自由度が高いWebExtensionsのニーズにはマッチしないこともありますし、設定の永続化などWeb APIではなくWebExtensions固有のAPIを使った方が適切な場合も多いです。

このような理由から筆者自身がWebExtensionsアドオン用に開発したライブラリはいくつかあり、このブログでも過去にいくつか使用法をご紹介してきました。

当社でのThunderbird向けWebExtensionsアドオンの開発過程でも、UIとして任意の内容のダイアログウィンドウを開く場面で工夫や回避策が必要であったことから、ダイアログ形式のUIの実装を支援するライブラリを新規に開発しました。

XULアドオンのWebExtensions化に取り組む際の注意点

既存のXULアドオンをWebExtensions化する際には、これらの前提を踏まえた上で、前提にフィットするようにアドオンの仕様を見直すことが必要となります。

すでに述べたとおり、XULアドオンとWebExtensionsアドオンは前提が大きく異なります。「XULアドオンを最小限の変更でWebExtensions化する」「XULアドオンで実現していた機能をすべてWebExtensionsアドオンに持ち込む」という発想のままで作業に取り組むと、「あれもできない、これもできない」と様々な壁に阻まれて、結局WebExtensions化自体を断念する他なくなってしまいます。

そうではなく、まずはアドオンの各機能を「その機能は何のために・どういう目的で用意したのか?」にまで立ち返って分析し、「WebExtensions APIの上でその目的を達成する」という発想で設計し直すことが肝心です。その上で、最終的なユーザー体験をXULアドオンでの物に近付けるよう工夫するのが、XULアドオンのWebExtensions化では有効なアプローチと言えます。

多機能なXULアドオンほど、WebExtensions APIで実現できる機能とそうでない機能が混在している場合は多いです。そのようなケースでは、機能ごとに個別のアドオンとして切り出すのも有効です。「全機能が揃わないのですべて諦める」よりは、「一部の機能だけでもThunderbird 78で使える」方がユーザー体験として望ましいことに、疑いの余地は無いでしょう。

当社開発のアドオンの中で、WebExtensions化は不可能と見込まれる物

当社がThunderbird Add-onsで公開しているThunderbirdアドオンは多数あります*5。この中で、具体的にThunderbird 78対応のための作業が進行しているのは、現時点ではFlex Confirm Mailのみとなります。

それ以外のアドオンについてWebExtensions化が可能かどうかの予備調査を実施した所、以下のアドオンは必要なAPIが存在しないために、WebExtensions化はできない可能性が高いと分かりました。前述の傾向と併せて、「どのようなアドオンであればThunderbrid 78に対応できて、どのようなアドオンは対応できないか」を見積もる際の参考にしていただければ幸いです。

なお、WebExtensions化の可否以前のこととして、以下のアドオンは必要性がなくなったため、今後の更新は行われません。

まとめ

以上、Thunderbird用のXULアドオンをThunderbird 78以降に対応させる上で必要となる情報をまとめてみました。

Firefox 57でのXULアドオン廃止は、Firefoxユーザーに未曾有の大混乱を引き起こしました。現時点ではThunderbirdではまだ深刻な混乱は発生していませんが、Thunderbird 68.12のサポート終了のタイミングでThunderbird 78への自動更新が始まるため、その時には何らかの対応が必要となります*6

当社では、業務上で重要度の高いアドオン(当社製でない物も含む)のThunderbird 78対応作業も含む、Thunderbirdの法人利用者向けの有償サポートを提供しております。全社的に使用中のアドオンがThunderbird 78で動作しないなどのトラブルでお困りの情シス担当者さまがいらっしゃいましたら、お問い合わせフォームよりご連絡下さい。

また当社では、このような「OSSの改善に関わりつつ、それを仕事とすること」に関心のある方のエンジニア採用を行っています。日常的にOSSを使用していて、自分でもOSSに関わること自体を仕事にしてみたいという方は、ぜひお問い合わせいただければ幸いです。

*1 現在も進行中で、作業は未完了です。

*2 ドキュメントによっては「MailExtensions」という表記も見られますが、多くのドキュメントではFirefoxと同じ「WebExtensions」と表記されているため、この記事ではこちらに統一することにします。

*3 XULアドオンで使用されていたコードの一部をWebExtensionsアドオンで流用できる場合はありますが、作業方針としてはあくまで「新規開発」と捉える方が実態に即しています。ただ、XULアドオンらしくない・一般的なWebアプリの作法で作られていたXULアドオンは、流用できるコードの量が多くなるとは言えます。

*4 XULアドオンで言えば「OS標準のダイアログと同様に振る舞うダイアログ」や「文字・数値の入力が可能なドロップダウンリスト」などがツールキットの領域にあたります。

*5 法人サポート契約の中で必要が生じてご依頼を頂き作成した物を一般公開しているケースについては、まだ作業のご依頼を頂いていないため現時点でThunderbird 68に未対応の状態の物もあります。

*6 自動更新を停止して古いバージョンを使い続けるユーザーも少なくないですが、安全性の観点からはお薦めできません。

タグ: Mozilla
2020-08-07

ノータブルコード11 - 空になるかもしれないCのマクロの値を正規化

最近、GNU/Linux上のGCCとVisual C++のビルドでは同じ挙動なのにMinGWのビルドでだけ挙動が異なる件を調べていた須藤です。MinGWが提供するヘッダーファイルを見ていたら「お!」と思うコードがあったので11回目のノータブルコードとして紹介します。

MinGWはGCCでVisual C++でビルドしたようなバイナリー(Windowsが提供するランタイムで動くバイナリー)を出力するためにいろいろ頑張っています。標準入出力まわりもその1つです。ただ、Windowsが提供する標準入出力機能はANSI Cで定義されている標準入力機能と仕様が異なることがあります。たとえば、snprintf()の第二引数の挙動が違います。Windowsでは第二引数は書き込める最大文字数(終端の\0を含まない)ですが、ANSI Cでは終端の\0を含んだバイト数です。(ANSI Cの仕様はこれであってる?)

たとえば、次のプログラムで違いが出ます。

#include <stdio.h>

int
main(void)
{
  char buffer[5];
  snprintf(buffer, 5, "hello");
  printf("%.*s\n", 5, buffer);
  return 0;
}

Windows上では次の結果になります。

hello

Debian GNU/Linux上では次の結果になります。

hell

この例は実は今回の話とはまったく関係ない(!)のですが、MinGWには__USE_MINGW_ANSI_STDIOというマクロで標準入出力機能の実装を切り替えることができます。__USE_MINGW_ANSI_STDIOというマクロをユーザーが指定することもあるので正規化していました。次のコードです。

/* We are defining __USE_MINGW_ANSI_STDIO as 0 or 1 */
#if !defined(__USE_MINGW_ANSI_STDIO)
#define __USE_MINGW_ANSI_STDIO 0      /* was not defined so it should be 0 */
#elif (__USE_MINGW_ANSI_STDIO + 0) != 0 || (1 - __USE_MINGW_ANSI_STDIO - 1) == 2
#define __USE_MINGW_ANSI_STDIO 1      /* was defined as nonzero or empty so it should be 1 */
#else
#define __USE_MINGW_ANSI_STDIO 0      /* was defined as (int)zero and non-empty so it should be 0 */
#endif

ここで私が「お!」と思ったのは次の部分です。

#elif (__USE_MINGW_ANSI_STDIO + 0) != 0 || (1 - __USE_MINGW_ANSI_STDIO - 1) == 2

これは、__USE_MINGW_ANSI_STDIOとして0以外の値(空の値も含む)を指定されたら真になります。(数値以外の値が指定されていたらエラーです。)

たとえば、0が指定されていれば次のようになって偽になります。

#elif (0 + 0) != 0 || (1 - 0 - 1) == 2

#elif 0 != 0 || 0 == 2

たとえば、1が指定されていれば次のようになって真になります。

#elif (1 + 0) != 0 || (1 - 1 - 1) == 2

#elif 1 != 0 || -1 == 2

たとえば、2が指定されていれば次のようになって真になります。

#elif (2 + 0) != 0 || (1 - 2 - 1) == 2

#elif 2 != 0 || -2 == 2

たとえば、空の値が指定されていれば次のようになって真になります。これがおもしろかったんです!

#elif (+ 0) != 0 || (1 - - 1) == 2

#elif 0 != 0 || (1 - (-1)) == 2

#elif 0 != 0 || 2 == 2

二項演算子に見えた式が単行演算子になって全体として妥当な式になります。Cのマクロはあまり柔軟性がありませんが、こんな風に書くと01に値を正規化できるんだなぁと感心しました。コメントがないとすぐにはピンとこないコードだと思うのでそんなに使う機会はない気がしますが。。。

今回はMinGWのヘッダーファイルにあるコードで「お!」と思った正規化方法を紹介しました。

ところで、そろそろみなさんも自分が「お!」と思ったコードを「ノータブルコード」として紹介してみたくなってきませんか?ということで、このブログへの寄稿も受け付けることにしました。まだ仕組みは整えていないのですが、とりあえず、 https://gitlab.com/clear-code/blog/issues にMarkdownっぽいマークアップで書いた原稿を投稿してもらえばいい感じに調整してここに載せます。寄稿したいという人がたくさんいるならもう少しちゃんとした仕組みを整えようと思っていますので、興味のある人はご連絡ください。寄稿してもらった記事の著作者は作者本人のままですが、ライセンスはCC BY-SA 4.0GFDL(バージョンなし、変更不可部分なし、表表紙テキストなし、裏表紙テキストなし)のデュアルライセンスにしてください。参考:ククログのライセンス

それでは、次のノータブルコードをお楽しみに!

2020-08-06

Groongaのウインドウ関数を使って特定範囲の値を集計する

データベースなどでまとまったデータを取り扱っていると、ある特定の範囲の値を集計したいことがあります。

例えば、全国展開している商店で、全店舗の売上を合計して一ヶ月の売上を算出するケースを考えてみましょう。
この場合は、PoatgreSQL等のRDBMSではGROUP BY句を、Groongaではドリルダウンを使えば集計できます。
ですが、例えば、各店舗が全体の売上に対してどの程度寄与しているかを知りたい場合、RDBMSのGROUP BY句やGroongaのドリルダウンでは集計できません。

こういったケースでは、 "各店舗の売上/全体の売上" を計算する必要があります。

具体例として、各店舗の月の売上を集計したテーブルを考えます。このテーブルを使って、各店舗の売上が全体の売上に対してどの程度寄与しているかを算出します。

この例では、各店舗の売上は既にカラムとして用意されているので、その値を使えば良いですが、全体の売上は集計する必要があります。
この時、RDBMSのGROUP BY句や、Groongaのドリルダウンでは、集計結果のみを結果として返すので、集計結果と既存の他のカラムの値を使って計算するということができません。

こういった時に使えるのがウインドウ関数と呼ばれる関数群です。
ウインドウ関数も、RDBMSのGROUP BY句や、Groongaのドリルダウンと同様、ある特定範囲の値を集計します。
異なるのは、結果を全てのレコードに追加する点です。

つまり、ウインドウ関数を使うと各レコードに全体の売上を格納するカラムが追加されます。
全体の売上がカラムとして追加されるので、レコード毎に既存のカラムと組み合わせて "各店舗の売上/全体の売上" も計算できます。

では、実際に各店舗の売上が全体の売上に対してどのくらいの割合なのかをGroongaで計算してみましょう。

まずは、各店舗の月の売上を集計したテーブルを用意します。

table_create ReportPerMonth TABLE_NO_KEY
column_create ReportPerMonth branch_name COLUMN_SCALAR ShortText
column_create ReportPerMonth sales COLUMN_SCALAR UInt32
column_create ReportPerMonth month COLUMN_SCALAR Time

load --table ReportPerMonth
[
{"branch_name": "Sayama"   , "sales": 256125  , "month": "2020-04-30 00:00:00.000000"},
{"branch_name": "Iruma"    , "sales": 211546  , "month": "2020-04-30 00:00:00.000000"},
{"branch_name": "Hakodate" , "sales": 122928  , "month": "2020-04-30 00:00:00.000000"},
{"branch_name": "Otaru"    , "sales": 1132001 , "month": "2020-04-30 00:00:00.000000"},
{"branch_name": "Umeda"    , "sales": 188122  , "month": "2020-04-30 00:00:00.000000"},
{"branch_name": "Kure"     , "sales": 120160  , "month": "2020-04-30 00:00:00.000000"},

{"branch_name": "Sayama"   , "sales": 156125  , "month": "2020-05-30 00:00:00.000000"},
{"branch_name": "Iruma"    , "sales": 71546   , "month": "2020-05-30 00:00:00.000000"},
{"branch_name": "Hakodate" , "sales": 111928  , "month": "2020-05-30 00:00:00.000000"},
{"branch_name": "Otaru"    , "sales": 1332001 , "month": "2020-05-30 00:00:00.000000"},
{"branch_name": "Umeda"    , "sales": 117122  , "month": "2020-05-30 00:00:00.000000"},
{"branch_name": "Kure"     , "sales": 1120160 , "month": "2020-05-30 00:00:00.000000"},

{"branch_name": "Sayama"   , "sales": 1561250 , "month": "2020-06-30 00:00:00.000000"},
{"branch_name": "Iruma"    , "sales": 211546  , "month": "2020-06-30 00:00:00.000000"},
{"branch_name": "Hakodate" , "sales": 199928  , "month": "2020-06-30 00:00:00.000000"},
{"branch_name": "Otaru"    , "sales": 1452001 , "month": "2020-06-30 00:00:00.000000"},
{"branch_name": "Umeda"    , "sales": 111122  , "month": "2020-06-30 00:00:00.000000"},
{"branch_name": "Kure"     , "sales": 100160  , "month": "2020-06-30 00:00:00.000000"},
]

branch_name が各店舗の名前です。 sales はその月の売上です。 month は売上月です。

データをロードしたら次にウインドウ関数を使って、月毎の全体の売上を集計しましょう。
まずは、 --columns[LABEL].window.group_keysmonth を指定して月毎にグループ化します。

全体の売上は、月毎の売上の総和なので、 --columns[LABEL].valuewindow_sum(sales)
使って計算します。
window_sumwindow.group_keys でグループ化したグループ毎に指定したカラムの値の総和をとります。

つまり以下のようなクエリーを実行します。

plugin_register functions/time

select ReportPerMonth \
  --limit -1 \
  --columns[sales_sum_per_month].stage initial \
  --columns[sales_sum_per_month].type UInt32 \
  --columns[sales_sum_per_month].value 'window_sum(sales)' \
  --columns[sales_sum_per_month].window.group_keys month \
  --output_columns '_id, branch_name, sales, sales_sum_per_month, time_format_iso8601(month)'

上記のクエリーの結果は以下の通りです。
ポイントは、月毎の全体の売上を表す sales_sum_per_month が全てのレコードに追加されているところです。
月毎に同じ値がsales_sum_per_monthに設定されていることが確認できるかと思います。

例えば、2020年4月の全体の売上は、2030882となっているので、売上月が2020-04-01T00:00:00.000000+09:00
のレコードのsales_sum_per_monthは、全て2030882が設定されています。

[
  [
    0,
    1594109666.415102,
    0.002141475677490234
  ],
  [
    [
      [
        18
      ],
      [
        [
          "_id",
          "UInt32"
        ],
        [
          "branch_name",
          "ShortText"
        ],
        [
          "sales",
          "UInt32"
        ],
        [
          "sales_sum_per_month",
          "UInt32"
        ],
        [
          "time_format_iso8601",
          null
        ]
      ],
      [
        1,
        "Sayama",
        256125,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        2,
        "Iruma",
        211546,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        3,
        "Hakodate",
        122928,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        4,
        "Otaru",
        1132001,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        5,
        "Umeda",
        188122,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        6,
        "Kure",
        120160,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        7,
        "Sayama",
        156125,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        8,
        "Iruma",
        71546,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        9,
        "Hakodate",
        111928,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        10,
        "Otaru",
        1332001,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        11,
        "Umeda",
        117122,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        12,
        "Kure",
        1120160,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        13,
        "Sayama",
        1561250,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        14,
        "Iruma",
        211546,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        15,
        "Hakodate",
        199928,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        16,
        "Otaru",
        1452001,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        17,
        "Umeda",
        111122,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        18,
        "Kure",
        100160,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ]
    ]
  ]
]

これで、月毎の全体の売上が集計できました。
次は、各店舗の売上が全体の売上の何%なのかを算出します。

月毎の全体の売上は、前述の通り sales_sum_per_month カラムに格納されています。
このカラムは全レコードに追加されているので、このカラムの値と、各店舗の売上 salessales / sales_sum_per_month のように除算することで、各店舗の売上が全体の売上の何%なのかを算出できます。

結果は、each_sales_per_allカラムに格納されます。

sales / sales_sum_per_monthの値は実数になる可能性があるので、実際のクエリーでは、sales * 1.0 / sales_sum_per_monthのようにsalesの値に* 1.0して、計算結果の型を整数から実数にしています。

select ReportPerMonth \
  --limit -1 \
  --columns[sales_month].stage initial \
  --columns[sales_month].type Time \
  --columns[sales_month].value 'time_classify_month(month)' \
  --columns[sales_sum_per_month].stage initial \
  --columns[sales_sum_per_month].type UInt32 \
  --columns[sales_sum_per_month].value 'window_sum(sales)' \
  --columns[sales_sum_per_month].window.group_keys sales_month \
  --columns[each_sales_per_all].stage output \
  --columns[each_sales_per_all].type Float \
  --columns[each_sales_per_all].value 'sales * 1.0 / sales_sum_per_month' \
  --output_columns '_id, branch_name, sales, sales_sum_per_month, each_sales_per_all, time_format_iso8601(sales_month)'
[
  [
    0,
    1594109970.315803,
    0.003779888153076172
  ],
  [
    [
      [
        18
      ],
      [
        [
          "_id",
          "UInt32"
        ],
        [
          "branch_name",
          "ShortText"
        ],
        [
          "sales",
          "UInt32"
        ],
        [
          "sales_sum_per_month",
          "UInt32"
        ],
        [
          "each_sales_per_all",
          "Float"
        ],
        [
          "time_format_iso8601",
          null
        ]
      ],
      [
        1,
        "Sayama",
        256125,
        2030882,
        0.1261151558780865,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        2,
        "Iruma",
        211546,
        2030882,
        0.1041645944963814,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        3,
        "Hakodate",
        122928,
        2030882,
        0.0605293660586878,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        4,
        "Otaru",
        1132001,
        2030882,
        0.5573937826028297,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        5,
        "Umeda",
        188122,
        2030882,
        0.09263068952307421,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        6,
        "Kure",
        120160,
        2030882,
        0.05916641144094044,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        7,
        "Sayama",
        156125,
        2908882,
        0.05367182305779334,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        8,
        "Iruma",
        71546,
        2908882,
        0.02459570377897763,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        9,
        "Hakodate",
        111928,
        2908882,
        0.03847801320232309,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        10,
        "Otaru",
        1332001,
        2908882,
        0.4579082272845719,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        11,
        "Umeda",
        117122,
        2908882,
        0.04026357892826179,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        12,
        "Kure",
        1120160,
        2908882,
        0.3850826537480723,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        13,
        "Sayama",
        1561250,
        3636007,
        0.4293858620184174,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        14,
        "Iruma",
        211546,
        3636007,
        0.05818085608746078,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        15,
        "Hakodate",
        199928,
        3636007,
        0.05498559271200523,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        16,
        "Otaru",
        1452001,
        3636007,
        0.3993394402155991,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        17,
        "Umeda",
        111122,
        3636007,
        0.03056154732375378,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        18,
        "Kure",
        100160,
        3636007,
        0.02754670164276361,
        "2020-06-01T00:00:00.000000+09:00"
      ]
    ]
  ]
]

このようにして、各店舗の売上が全体の売上のどのくらいなのかを計算できました。

ウインドウ関数を使って、ある特定範囲の値を算出し、その値を既存のカラムの値と組み合わせて計算する例を紹介しました。
この例のように、集計した値と、既存のカラムの値を組み合わせてなにか計算したい場合にウインドウ関数の使用を検討してみてはいかがでしょうか?

タグ: Groonga
2020-08-03

Apache Arrowの最新情報(2020年7月版)

Apache ArrowPMC(Project Management Commitee、プロジェクト管理チームみたいな感じ)のメンバーの須藤です。

みなさんはApache Arrowを知っていますか?最近、ついに1.0.0がリリースされたんですよ。私がApache Arrowの最新情報をまとめた2018年9月から毎年「今年中に1.0.0がでるぞ!」と言っていた1.0.0がついにリリースされたんです!

1.0.0を機に安心して使えるようになります。(どう安心なのかは後で説明します。)

Apache Arrowはすでにデータ処理界隈で重要なコンポーネントになりつつありますが、数年後にはもっと重要になっているだろうプロジェクトです。データ処理界隈に興味がある人は知っておくと役に立つはずなので毎年Apache Arrowの最新情報をまとめています。1.0.0がリリースされたので2020年7月現在の最新情報を紹介します。

私は、PMCの中では唯一の日本人で、コミット数は2番目に多いので、日本ではApache Arrowのことをだいぶ知っている方なはずです。日本語でのApache Arrowの情報があまりないので日本語で紹介します。

ちなみに、英語ではいろいろ情報があります。有用な情報源はApache Arrowの公式ブログ公式メーリングリストやそれぞれの開発者のブログ・発表などです。開発者のブログの中ではUrsa Labs Blogの隔月の開発レポート(最近は隔月の開発レポートコーナーがありませんが…)がオススメです。Ursa Labsはスポンサーを集めてフルタイムでApache Arrowの開発をしている非営利の組織です。(という説明でそんなに間違っていないはず。)

この記事ではそれらの情報へのリンクも示しながら最新情報を紹介するので、ぜひ英語の情報も活用してください。

Apache Arrowが実現すること

Apache Arrowが実現することはプロジェクト開始当初から変わっていないので、Apache Arrowの必要性から知りたいという方はApache Arrowの最新情報(2018年9月版)を参照してください。まとめると次の通りです。

Apache Arrowは大量のデータを効率的にメモリー上で処理することを目指しています。そのためにしていることは次の通りです。

  1. データ交換・高速処理しやすいApache Arrowフォーマットの仕様を定義
  2. 各種言語用のApache Arrowフォーマットを読み書きするライブラリーを開発
  3. 大量のメモリー上のデータを高速処理するためライブラリーを開発

Apache Arrowが向いている用途は次の通りです。

  • 大量データの交換
  • メモリー上での大量データの分析処理

実は、1.0.0の前からすでにApache Arrowは利用され始めています。

たとえば、Apache SparkはJVMとPython・R間でのデータ交換にApache Arrowを使って10倍以上高速化しています。(データ量が大きいほど高速化できます。)

Amazon AthenaはApache ArrowフォーマットでAmazon Athenaにデータを提供できる機能を提供しています。

GroongaはApache Arrowフォーマットでのデータロードと検索結果の返却に対応しています。

Apache Arrowの現状

それでは、2020年7月現在のApache Arrowについて説明します。

まず、2020年7月24日にApache Arrow 1.0.0がリリースされました。1.0.0の特筆する点はApache Arrowフォーマットの安定性です。

Apache Arrowフォーマットの安定性

フォーマットの安定性というは、互換性が高い低いとか問題が多い少ないとかいう話ではなく、フォーマットの互換性をどう維持する方針なのかという話です。

これまではたまに互換性が壊れていました。たとえば、Apache Arrow 0.15.0でデータをアラインするためにパディングするように変更しました。これにより0.15.0で生成したデータを0.15.0より前のバージョンで読めなくなりました。Apache SparkはApache Arrow 0.15.0より前からApache Arrowに対応していたので、Apache Sparkでこの問題に遭遇したケースが多かったはずです。

1.0.0からは後方互換性と前方互換性を次のように維持します。セマンティックバージョニングに従っています。簡単に言うと、メジャーバージョンが上がったら互換性はない、マイナー・パッチバージョンが上がっただけなら互換性がある、です。互換性が壊れるのは大変なのはわかっているのでできるだけメジャーバージョンが上がらないように運用する予定ではあります。

注意する点は「Apache Arrowフォーマットのバージョン」と「Apache Arrowライブラリーのバージョン」は別だということです。Apache Arrowライブラリー2.3.1はApache Arrowフォーマット1.1.2を使うということが起こりうるということです。

後方互換性:

  • 新しいフォーマットバージョンに対応したライブラリーでは古いフォーマットバージョン用に生成したすべてのデータを読める

    • 例:フォーマットバージョン1.0.0用に生成したデータはフォーマットバージョン2.0.0に対応したライブラリーで読める
  • フォーマットバージョンのメジャーバージョンが変わらない限り、新しいフォーマットバージョン用に生成したデータは古いフォーマットバージョンに対応したライブラリーでも読める

    • 例:フォーマットバージョン1.3.0用に生成したデータはフォーマットバージョン1.0.0に対応したライブラリーで読める
    • 例:フォーマットバージョン2.0.0用に生成したデータはフォーマットバージョン1.0.0に対応したライブラリーで読めるかもしれないし読めないかもしれない(読めることは保証しない。後述の前方互換性も参照。)

前方互換性:

  • 新しいフォーマットバージョン用に生成したデータは、古いフォーマットバージョンに対応したライブラリーでは読める、あるいは、読めないことを検出できる(おかしく読むことはない)
    • 例:フォーマットバージョン2.0.0用に生成したデータは、フォーマットバージョン1.0.0に対応したライブラリーで読める
    • 例:フォーマットバージョン2.0.0用に生成したデータは、フォーマットバージョン1.0.0に対応したライブラリーで読めない場合は読めないことを検出する(おかしく読まない)
  • フォーマットバージョンのマイナーバージョンが挙がった場合は新しい機能が追加される
    • 例:フォーマットバージョン1.1.0でフォーマットバージョン1.0.0ではなかった型を追加
  • 新しい機能を使っていない場合は新しいフォーマットバージョン用に生成したデータを古いフォーマットバージョンに対応したライブラリーで読める
    • 例:フォーマットバージョン1.1.0用に生成したデータだが1.0.0の機能しか使っていない場合はフォーマットバージョン1.0.0に対応したライブラリーで読める

参考:Format Versioning and Stability

なお、1.0.0のリリース前にいつもより多くフォーマットの変更が入っています。たとえば、データをLZ4・Zstandardで圧縮する機能が入っています。同一ホスト上でメモリー上のデータをそのまま交換している場合(たとえばmmap()で同じアドレスを共有している場合)は圧縮・展開で速度が落ちるので意味はありませんが、Apache Arrow Flightで異なるホスト間でデータを交換したり、Featherフォーマットとしてデータをストレージに保存する場合はI/Oが減って高速になる場合があります。他の変更点はApache Arrow 1.0.0のリリースアナウンスを参照してください。

Featherフォーマットと聞いてびっくりした人のために補足しておくと、Featherフォーマットバージョン1は非推奨のままです。Featherフォーマット2はApache Arrow IPCファイルフォーマットのことです。Apache ArrowライブラリーのC++実装ではFeatherフォーマットバージョン1もバージョン2も読み書きできます。

フォーマットのバージョンを説明したのでライブラリーのバージョンも説明します。

Apache Arrowライブラリーのバージョン

Apache Arrowライブラリーのバージョンもセマンティックバージョニングに従います。従うのですが、しばらくは毎回メジャーバージョンが上がる予定です。つまり、1.0.0の次のバージョンは2.0.0です。

Apache Arrowライブラリーはまだまだ活発に機能追加・変更・修正をしているため、互換性を維持しながらでは開発スピードが落ちてしまうことを懸念してこうなっています。

今のところApache Arrowライブラリーは3ヶ月に1回程度のペースでリリースされているので今年中に2.0.0がでて、来年には3.0.0が出るようなペースです。

フォーマットのバージョンのところでも説明しましたが、ライブラリーのバージョンとフォーマットのバージョンは別々に管理し、フォーマットの(メジャー)バージョンはできるだけあげない(互換性を壊さない)方針で運用します。そのため、ライブラリーのバージョンとフォーマットのバージョンはどんどん離れていきます。たとえば次のような感じです。

  • 1.0.0の次のリリース
    • ライブラリーのバージョン:2.0.0
    • データフォーマットのバージョン:1.0.0
  • 1.0.0の次の次のリリース
    • ライブラリーのバージョン:3.0.0
    • データフォーマットのバージョン:1.0.0

Apache Arrowライブラリーは頻繁に互換性が壊れる前提になるのでライブラリーを利用する人はセマンティックバージョニングに合わせて依存情報を設定したほうがよいかもしれません。たとえば、PyPIからインストールするならpyarrow==1.*と指定して同じメジャーバージョンを使うようにするといった具合です。

ただ、古いバージョンを使い続けることになるとApache Arrowライブラリーの高速化や新機能の恩恵を受けられないので、定期的にアップグレードした方がよいです。クリアコードではApache Arrow関連の開発のサポートサービスを提供しているので、Apache Arrowを使いたいけど自分たちで対応し続けるのは不安という方はご相談ください。Apache Arrowに詳しい会社としてお手伝いします。(宣伝)

Apache Arrowライブラリーはいろんな言語ですぐに使える(「batteries-included」と自分たちで言うこともある)開発プラットフォームを提供しようとしています。1.0.0で大きく変わったバージョニングの説明をしたので、次は各言語の現状を説明します。

各言語ごとのApache Arrowライブラリーの現状

Apache Arrowの最新情報をまとめはじめて3年目になりますが、毎年、ソースを見ながら各言語の状況をまとめていました。が、それも去年で終わりです。公式ドキュメントでまとめはじめたのです!

表を見るとわかるのですが、Apache Arrowの仕様をすべて満たした実装はまだありません。C++実装が一番実装しているのですが、それでもいくつか未実装の機能があります。(表の列にないC・Python・R・Ruby実装はC++実装のバインディングなので実装範囲はC++実装に準じます。)

次に実装範囲が多いのがJava実装でC++実装ほどではないですがほとんど網羅しています。

Go・JavaScript・C#・Rust実装は基本機能はほぼ網羅しているといった感じです。(Rust実装に1つもチェックが入っていないのはまだ誰も埋めていないからなだけで、Rust実装はアクティブに開発が進んでいる実装の1つです。)

自分が使いたい言語の実装に自分が使いたい機能は実装されていたでしょうか?もし、まだ実装されていなくても大丈夫です。実装すればいいのです!Apache Arrowは「みんなバラバラに基盤を実装するんじゃなくて一緒に協力して基盤を実装した方がみんながうれしくなるよね!」という発想で開発が始まっています。未実装の機能があったら一緒に実装しましょう!

未実装の機能を自分たちで実装してApache Arrowを使い始めた例を紹介します。網屋さんが開発しているALogというログ収集製品ではデータのやり取りの一部にApache Arrowを検討していました。高速化のためです。しかし、C#実装にまだリスト型が実装されていませんでした。そこで、網屋の橋田さんは業務の一環としてリスト型の対応などを実装し、ALogで必要な機能を使えるようにしました。その結果、ALogでApache Arrowを使った高速化を実現できました。今後、日本でもApache Arrowの開発に参加する企業は増えていくでしょうが、網屋さんは先行している企業の1つです。

ちなみに、クリアコードは網屋さんがApache Arrowの開発に参加することを支援しました。未実装の機能を実装したいから支援して欲しいという方はご相談ください。(宣伝)

各言語の実装の状況をざっと説明したのでこの1年での変更点もざっと説明します。まずは計算関数です。

計算関数

Apache Arrowライブラリーは高速なデータ交換方法だけではなく、高速なデータ処理方法も提供しようとしています。高速なデータ処理に関しては、今のところ、2つのアプローチの開発が進んでいます。1つはGandivaという名前の式コンパイラーです。条件式や計算式をJITコンパイルして高速に実行できます。もう1つは計算関数(compute functions)と呼んでいる関数群です。計算関数は各計算処理を個別に使います。

Gandivaではユーザーがa + b * cという式を入力として実行できますが、計算関数はユーザーがa + bを実行し、その結果とcの掛け算を実行します。Gandivaの方が高レベルな機能を提供するのですが、Gandivaレベルでも計算関数が提供しているのと同レベルの機能(たとえば数値の足し算)を提供しているので、重複している部分があります。これらの機能の棲み分けが今後どうなってくるのかは。。。私もよくわかっていません。なんどか議論はされているのですが、結局どうなるのかピンときていませんし、現時点ではまだ目立った動きもありません。

参考:Compute kernels and Gandiva operators

Gandivaに関してはこの1年でそんなに目立った動きはない(使える関数は少しずつ増えていて開発は進んでいます)ので、動きがあった計算関数について説明します。

最近、計算関数の仕組みが書き直されました。これまでは静的に使う(コンパイル時にどの関数を使うか決める)方式だったのですが、動的に使う(実行時にどの関数を使うか決める)方式になりました。具体的にどう使い方が変わったかと言うと、実行時に名前で関数を探して、見つけた関数を実行するようになりました。名前は単なる文字列なので実行時に生成できます。

使い方が変わってどううれしいかピンとこないと思うので、ユーザー視点でうれしいこと例を示します。たとえば、PythonユーザーはC++実装に計算関数が増えたらPython実装を変えずに新しい計算関数を使えます。これまでは、C++実装に計算関数が増えたらPython実装にもその計算関数を使えるようにする処理が実装されないと使えませんでした。C++実装に「すごい計算関数」が追加されたらすぐにPythonスクリプトから「すごい計算関数」を使えるということです。

現時点では、C++実装とPython実装は同じタイミングでリリースされ、Python実装の漏れはほとんどありませんが、今後、C++実装にプラグインの仕組みができた場合はかなりうれしくなります。自分でC++で実装した計算関数をすぐにPythonスクリプトから使えるようになります。(伝わる?)

まだ計算関数をプラグインで追加できるようにするという話はあがっていませんが、別の機能をプラグインで拡張できるようにしない?という提案はあがっています。近い将来、プラグインの仕組みができるかもしれません。

計算関数もだいぶ増えました。複数カラムでのソートなど足りない機能はまだあるのですが、どんどん増えています。現在使える関数については計算関数のドキュメント(再掲)を参照してください。

計算関数の仕組みが書き直されたタイミングで計算関数を実装しやすくなっています。計算関数を実装したくなったらcpp/src/arrow/compute/の下をのぞいてください。

高速なデータ処理に関することだけでなく、高速なデータ交換に関することでも追加機能があったので説明します。

Cデータインターフェイス

Apache Arrowはネットワーク越し・プロセス越しなどいろいろなスケールでの高速なデータ交換を実現しようとしています。この1年でCデータインターフェイスという、同一プロセス内の別システム間でのデータ交換用のインターフェイスが増えました。

Apache Arrowを使うためには公式の実装を使うか自分たちが必要な分だけの自分たちの実装を作る必要があります。前述の通り、公式の実装はしばらく頻繁に互換性が壊れる想定なのでアップグレードに追従するコストが少なからず発生します。一方、独自に実装すると依存ライブラリーが増えなかったりアップグレードに追従するコストはありませんが、実装・メンテナンスはそれなりに大変です。(ちなみに、PG-StromのArrow_fdwは後者のアプローチです。)

Cデータインターフェイスを使うと、Apache Arrowの実装に依存せずに同一プロセス内の別システムとApache Arrowデータ(カラムナーデータ)を交換できます。ただし、単にデータを交換できるだけでApache Arrowフォーマットでの入出力や、Apache Arrowデータに対する高速なデータ処理機能はありません。たとえば、前述のArrow_fdwはApache Arrowフォーマットでの入出力があるのでCデータインターフェイスは使えません。

ユースケースの1つは、同一プロセス名で別言語が動いていてそれらの言語間でデータ交換したいケースです。たとえば、Introducing the Apache Arrow C Data Interface | Apache ArrowではRと同じプロセス内でPythonを動かし、RとPythonでデータをやり取りする例を紹介しています。(Cデータインターフェイス単体でではなく、Apache Arrowライブラリーと一緒に使用しています。)

私は、SQLite3がCデータインターフェイスでデータを提供するAPIを用意するとうれしいケースがあるのではないかと考えています。

CデータインターフェイスはCの構造体定義・関数定義・仕様だけを提供しています。この定義をコピーして、仕様に合わせて動くようにすればApache Arrowデータを交換できます。詳細はCデータインターフェイス(再掲)を参照してください。

この1年の大きな機能面での変更を説明したので、次はビルド関連・パッケージ関連について説明します。

C++実装のビルドオプション

Apache Arrowライブラリーはすぐに使える(batteries-included)開発プラットフォームを提供しようとしているため、たくさんの有用な機能が詰め込まれています。すべての機能を自前で実装しているわけではなく、すでに有用な実装がある場合はライブラリーとしてその機能を組み込んで提供します。そのため、フル機能のApache Arrowライブラリーには依存ライブラリーが多いです。

しかし、すべてのユースケースでフル機能のApache Arrowライブラリーが必要なわけではありません。そのため、C++実装では必要に応じて各機能をオン・オフできるようになっています。

もともと、デフォルトではほぼオンになっていたのですが、デフォルトでオフになりました。必要に応じてオンにするようになりました。1.0.0がリリースされたから久しぶりに自分でビルドしてみるか!という人はCMakeのオプションを再確認してください。

依存ライブラリーを動的にリンクするか静的にリンクするかも個別に選べるようになりました。(選べるようにする作業がそろそろ完了しそうです。)

また、静的にリンクしたApache Arrowライブラリーを簡単に使えるようになりました。(使えるようにする作業がそろそろ完了しそうです。)

静的にリンクしたApache Arrowライブラリーをリンクする場合は、Apache Arrowライブラリーにリンクするだけではなく、Apache Arrowライブラリーに静的にリンクしている依存ライブラリーも一緒にリンクする必要があります。これまではユーザーが依存ライブラリーを自分で追加しないといけなく、現実的には使えない状態だったのが、次の方法でユーザーが特になにもしなくても使えるようになりました。

Apache Arrowライブラリーにバンドルされている依存ライブラリーを使った場合:

  • バンドルされている依存ライブラリーをすべてまとめたlibarrow_bundled_dependencies.aを生成してインストール
  • Apache ArrowライブラリーのCMakeパッケージでは必要に応じてlibarrow_bundled_dependencies.aをリンクフラグに追加
    • pkg-configは未対応(これを書いていて対応するのを忘れていたことに気づいた)

システムの依存ライブラリーを使った場合:

  • Apache ArrowライブラリーのCMakeパッケージが必要に応じて依存ライブラリーの情報を集めてリンクフラグに追加(実装中)
    • pkg-configは未対応(↑の実装が終わったらやらないと)

このあたりの話や次に説明するパッケージ関連の話も書いてあるMaking Arrow C++ Builds Simpler, Smaller, and Faster | Apache Arrowも読んでみてください。

パッケージ関連

パッケージ関連もこの1年でさらによくなりました。

1年ほど前はPython用のwheelパッケージはもうメンテナンスできないよ…となっていたのですが、再び頑張ってメンテナンスするようになりました。メンテナンスを諦めていた理由は次の通りでした。

  • やる人がいない
  • 開発している人がやると時間がとられて開発が進まない
    • wheelのメンテナンスをするより開発を進める方が優先度が高い

wheelのメンテナンスのためにApache Arrowの開発に参加する人もいたのですが、なかなかタスクを完了するところまでいけませんでした。たとえば、Python 3.8対応のケースです。前述の通りApache Arrowライブラリーは依存関係が多く、すべての依存ライブラリーを同梱しないといけないwheelの作成は難しい(というか面倒)です。それに加えて、新しいバージョンのPythonに対応するのはまた難しい(というか面倒)だったのです。Python 3.8対応については私が引き取って完了させました。(どーん!)

Python 3.8対応の後は私も含めてコアの機能を作っている人たちがちまちま手を入れながらメンテナンスしています。

なお、wheelのメンテナンスに時間を割くようになったのはwheelユーザーが多かったからでした。(月間700万ダウンロードとかそういう感じ。)

1.0.0ではGandiva対応が削除されました。これはwheelのサイズを減らすためです。今後はpyarrowにすべてを入れるのではなくサブパッケージにわけて必要な人が必要な機能をインストールできるようにする予定です。wheelユーザーの人はこういう作業とかで一緒にメンテナンスしませんか?

Python実装ののwheel以外のパッケージもよくなっています。

Debian GNU/Linux、Ubuntu、CentOS、Amazon Linux用のパッケージはARM用のパッケージも提供するようになりました。(私も結構作業しました!)

MSYS2のパッケージではGandivaを使えるようになりました。Homebrewでも有効にしたかったのですが、libstdc++のリンクの問題があって有効にできませんでした。解決策はあるので次のリリースでは有効にできるといいな。MSYS2とHomebrewのパッケージでGandivaを有効にするために@naitohと私で最新のLLVMでもGandivaを使えるようにする作業を進めていました。1.0.0に間に合ってよかった。

R実装のパッケージもインストールしやすくなっています。R実装はC++実装のバインディングなのでC++実装が必要なのですが、ビルド済みのC++実装を使えるケースを増やしています。C++実装のビルド時に問題が発生することが多いので、これで問題が減るはずです。

インストールページの説明を更新できていないのですが、C#実装のパッケージが「オフィシャル」扱いになりました。Apache ArrowのPMCが投票して受理されたものが「オフィシャル」扱いなのですが、これまではC#実装のソースだけが「オフィシャル」扱いでビルド済みのパッケージは「アンオフィシャル」扱い(投票されていない)でした。

次の1年でRuby実装のパッケージも「オフィシャル」扱いにしたいな。

まとめ

2020年7月時点のApache Arrowの最新情報を、2019年9月からの差分という形でまとめました。Apache Arrowは1.0.0がついにリリースされたことが大きなニュースでした。1.0.0リリースによってデータ処理界隈で重要なコンポーネントになることでしょう。日本でもApache Arrowのことを知っている人が増えるといいと思うので日本語でまとめました。Apache Arrowを使う人が増えるといいなぁと思います。さらに言えば開発に参加する人も増えるといいなぁと思います。

私が知っていることはまとめたつもりですが、もしかしたらカバーできていない話があるかもしれません。もし、「○○についても知りたい!」という方がいたらApache Arrowのことを日本語で話せるチャットで声をかけてください。この記事に追加します。

Apache Arrowについて講演して欲しいという方はお問い合わせください。

私はデータ処理ツールの開発という仕事をしたいと思っています。その中にはもちろんApache Arrowの開発も含まれています。一緒に仕事をしたい!(自社サービスをApache Arrow対応したいとか)という方はお問い合わせください。

2020-07-31

Fluentd-kubernetes-daemonsetのElasticsearchイメージでILMを使う

はじめに

クリアコードはFluentdの開発に参加しています。

Fluentdにはプラグインというしくみがあり、たくさんのプラグインが開発されています。
Fluentdはログ収集ソフトウェアということからkubernetes(以下k8sと略記)にも載せることができます。
Fluentdの開発元が公式に出しているk8sでのログ収集の仕組みの一つとしてFluentdのDaemonSetを提供しています。

筆者畑ケはElasticsearchのILM対応を最近fluent-plugin-elasticsearchに入れました。*1
筆者が対応したILMをFluentdのDaemonSetでも有効化して動かすことができたので、報告します。

ILMを有効化していると、古くなったインデックスを定期的に消すというオペレーションをElasticsearch自体に任せることができ、Elasticsearchのクラスターの管理の手間を減らせます。

k8sのkind

まず、Fluentdのログ収集を解説する前に、k8sに少し触れておきます。
この記事ではminikube v1.11.0、Kubernetes v1.18.3を想定しています。
k8sではいくつかのリソースの管理方法があります。リソースはオブジェクト毎に名前が付けられており、yamlでオブジェクトの振る舞いを決定します。
ここで、単純なコンテナをk8sにデプロイするには、例えば以下のようにnginxのDeploymentを作成します。

apiVersion: apps/v1
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

これを

$ kubectl apply -f ngix-deployment.yaml
deployment.apps/nginx-deployment created

とする事で、nginxのコンテナがk8s上で動作し始めます。

$ kubectl get pods
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-6b474476c4-vhtmn   1/1     Running   0          34s
nginx-deployment-6b474476c4-xqr4f   1/1     Running   0          34s

動作確認が終わったらDeploymentを片付けておきましょう。

$ kubectl delete deployment nginx-deployment
deployment.apps "nginx-deployment" deleted

k8sのDaemonSetとは

k8sにはDaemonSetというkindがあり、これはクラスターを構成するNode上にDaemonSetが構成するPodを自動的に配置するために使用されるkindです。

DaemonSetのこの性質を用いる事で、各Node上のログを集めるPodを自動的に配置するFluentdのDaemonSetが作成できます。

FluentdのDaemonSet

FluentdのDaemonsetは公式では https://github.com/fluent/fluentd-kubernetes-daemonset にて開発がされています。
執筆時点では以下のイメージがDaemonSet用に提供されています。

  • debian-elasticsearch7
  • debian-elasticsearch6
  • debian-loggly
  • debian-logentries
  • debian-cloudwatch
  • debian-stackdriver
  • debian-s3
  • debian-syslog
  • debian-forward
  • debian-gcs
  • debian-graylog
  • debian-papertrail
  • debian-logzio
  • debian-kafka
  • debian-kinesis

この記事では、debian-elasticsearch7のdocker imageを参照しているfluentd-kubernetes-daemonsetの設定を元にして、ILMを有効化したロギング環境を構築します。

FluentdのElasticsearch7 Daemonset

Fluentdでログ収集をした後に、Elasticsearchを用いてログをストアするDaemonSetは例えば、 fluentd-daemonset-elasticsearch.yamlです。この設定ではRBACを使っていませんが、簡単のためこのDaemonSetをもとにして構成します。
また、この記事で使用するdebian-elasticsearchのimageのtagはfluent/fluentd-kubernetes-daemonset:v1.11.1-debian-elasticsearch7-1.3 または fluent/fluentd-kubernetes-daemonset:v1-debian-elasticsearchを用いています。
記事が公開されるタイミングではどちらのタグを使用しても大丈夫です。

元のDaemonSetの構成ではElasticsearchのテンプレート設定が入っていないため、テンプレートの設定をConfigMapで表現することにします。

apiVersion: v1
data:
  index_template.json: |-
    {
        "index_patterns": [
            "logstash-default*"
        ],
        "settings": {
            "index": {
                "number_of_replicas": "3"
            }
        }
    }
kind: ConfigMap
metadata:
  name: es-template
  namespace: kube-system
---

ConfigMapをk8sではpodからストレージボリュームとして参照することが出来ます。

      volumes:
      - name: es-template
        configMap:
          name: es-template

k8sのvolumeオブジェクトはConfigMapの名前を指定してボリュームとしてPodから認識させます。
このオブジェクトをマウントします。

        volumeMounts:
        - name: es-template
          mountPath: /host
          readOnly: true

fluent-plugin-elasticsearchのILMの設定を入れていきます。

          # ILM parameters
          # ==============
          - name: FLUENT_ELASTICSEARCH_ENABLE_ILM
            value: "true"
          - name: FLUENT_ELASTICSEARCH_ILM_POLICY
            value: '{ "policy": { "phases": { "hot": { "min_age": "0ms", "actions": { "rollover": { "max_age": "1d", "max_size": "5g
b" } } }, "delete": { "min_age": "2d", "actions": { "delete": {}}}}}}'
          - name: FLUENT_ELASTICSEARCH_TEMPLATE_FILE
            value: /host/index_template.json
          - name: FLUENT_ELASTICSEARCH_TEMPLATE_NAME
            value: "logstash-default"

上記の設定を入れればElasticsearchのILMの機能を有効化する為に必要な設定が入れられました。
変更の全体はこのコミットで見ることができます。

実際に適用してみる

適用する前に、以下の設定を実際のElasticsearchが動いているサーバーの値に書き換えておいてください。

          - name:  FLUENT_ELASTICSEARCH_HOST
            value: "elasticsearch-master.default.svc"
          - name:  FLUENT_ELASTICSEARCH_PORT
            value: "9200"
          - name: FLUENT_ELASTICSEARCH_SCHEME
この記事で実際に使用しているElasticsearchとそのデプロイ方法の概要

この記事で使用しているElasticsearchはElastic社がメンテナンスしているhelm chartsの7.8.0タグを用いてminikubeで作成したk8sにデプロイしました。

k8sの外からElasticsearchのAPIを叩けるように9200ポートのポートフォワードを設定しておきます。

$ kubectl port-forward svc/elasticsearch-master 9200 
Forwarding from 127.0.0.1:9200 -> 9200
Forwarding from [::1]:9200 -> 9200
...

別のターミナルからElasticsearchの応答を確認します。

$ curl localhost:9200
{
  "name" : "elasticsearch-master-2",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "Vskz9aTjTZSCg8klQMz5mg",
  "version" : {
    "number" : "7.8.0",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "757314695644ea9a1dc2fecd26d1a43856725e65",
    "build_date" : "2020-06-14T19:35:50.234439Z",
    "build_snapshot" : false,
    "lucene_version" : "8.5.1",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

Elasticsearch v7.8.0が動作していることが確認できました。

kubectlを用いて実際に適用

では、kubectlで実際に適用してみます。

$ kubectl apply -f fluentd-daemonset-elasticsearch.yaml
configmap/es-template created
daemonset.apps/fluentd created

Podが動作しているかを確認します。

$ kubectl get pods -n=kube-system
NAME                               READY   STATUS    RESTARTS   AGE
coredns-66bff467f8-pswng           1/1     Running   1          13d
etcd-minikube                      1/1     Running   0          6d1h
fluentd-hqh9n                      1/1     Running   0          7s
kube-apiserver-minikube            1/1     Running   0          6d1h
kube-controller-manager-minikube   1/1     Running   1          13d
kube-proxy-kqllr                   1/1     Running   1          13d
kube-scheduler-minikube            1/1     Running   1          13d
storage-provisioner                1/1     Running   3          13d

FluentdのDaemonSetが動作しているのを確認できました。
Fluentdが正常に動いているかをPodのログを見て確認してみます。

$ % kubectl logs fluentd-hqh9n -n=kube-system
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-dedot_filter' version '1.0.0'
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-detect-exceptions' version '0.0.13'
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-elasticsearch' version '4.1.1'
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-grok-parser' version '2.6.1'
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-json-in-json-2' version '1.0.2'
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-kubernetes_metadata_filter' version '2.3.0'
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-multi-format-parser' version '1.0.0'
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-prometheus' version '1.6.1'
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-record-modifier' version '2.0.1'
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-rewrite-tag-filter' version '2.2.0'
2020-07-22 08:06:15 +0000 [info]: gem 'fluent-plugin-systemd' version '1.0.2'
2020-07-22 08:06:15 +0000 [info]: gem 'fluentd' version '1.11.1'
2020-07-22 08:06:16 +0000 [info]: using configuration file: <ROOT>
...
<snip>
...
2020-07-22 08:06:16 +0000 [info]: starting fluentd-1.11.1 pid=6 ruby="2.6.6"
2020-07-22 08:06:16 +0000 [info]: spawn command to main:  cmdline=["/usr/local/bin/ruby", "-Eascii-8bit:ascii-8bit", "/fluentd/vendor/bundle/ruby/2.6.0/bin/fluentd", "-c", "/fluentd/etc/fluent.conf", "-p", "/fluentd/plugins", "--gemfile", "/fluentd/Gemfile", "-r", "/fluentd/vendor/bundle/ruby/2.6.0/gems/fluent-plugin-elasticsearch-4.1.1/lib/fluent/plugin/elasticsearch_simple_sniffer.rb", "--under-supervisor"]
2020-07-22 08:06:16 +0000 [info]: adding match in @FLUENT_LOG pattern="fluent.**" type="null"
2020-07-22 08:06:16 +0000 [info]: adding filter pattern="kubernetes.**" type="kubernetes_metadata"
2020-07-22 08:06:16 +0000 [info]: adding match pattern="**" type="elasticsearch"
2020-07-22 08:06:17 +0000 [info]: adding source type="systemd"
2020-07-22 08:06:17 +0000 [info]: adding source type="systemd"
2020-07-22 08:06:17 +0000 [info]: adding source type="systemd"
2020-07-22 08:06:17 +0000 [info]: adding source type="prometheus"
2020-07-22 08:06:17 +0000 [info]: adding source type="prometheus_output_monitor"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: adding source type="tail"
2020-07-22 08:06:17 +0000 [info]: #0 starting fluentd worker pid=18 ppid=6 worker=0
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/kube-scheduler-minikube_kube-system_kube-scheduler-986d31752d921b9cee830917de6372781bd418c4674e7c890ef2ccb082292f50.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/elasticsearch-master-1_default_configure-sysctl-f8b971b868b6536e084453d8890a7677640446a20b8dc6397ed7715ada823be5.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/fluentd-hqh9n_kube-system_fluentd-65a884bccf20d3134d96f836a3a1a9e1116bee9fd01a8298206b54baf9340f84.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/elasticsearch-master-1_default_elasticsearch-6393ceacc5dc158689f5f4c20a3b072c91badab6974469e2652882eadc8b0964.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/elasticsearch-master-0_default_elasticsearch-7a47537855db7869b905cff73434348b7244a3f2a0a2f31b7703fdff864d3838.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/storage-provisioner_kube-system_storage-provisioner-1a2e641e4dfa4470903315c22c369cd02f43b819a44911130cb775b771bf2f42.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/kube-proxy-kqllr_kube-system_kube-proxy-c756169c6e4a9c348719703e49630442f48327552cbaf33a7a61bf6d60ffa3f8.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/elasticsearch-master-0_default_configure-sysctl-5fee5b507865277bf15f8f5412d72f977038b21e657fa39d51f73705edfe8b6b.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/kube-scheduler-minikube_kube-system_kube-scheduler-fdec557596b5c9c38040b9a04753b3407fa51f3e07bd1fea441b8c72bcc33f6a.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/kube-proxy-kqllr_kube-system_kube-proxy-68c7040be16381fa6e8179312482f36c521bb0588053c879138199e2c89deca5.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/etcd-minikube_kube-system_etcd-8ece6d2d408533810f2bc33e9aeeb534ea2781259c8046331b297c446ec24fe9.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/kube-apiserver-minikube_kube-system_kube-apiserver-ffa76a89be8bf37a82e68a36ea165d4db957b2156c277131f592d7b1b9497279.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/coredns-66bff467f8-pswng_kube-system_coredns-3f5612e80916072da46574a47f2b782dce33a536c1fa62d95450d1673ff63105.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/kube-controller-manager-minikube_kube-system_kube-controller-manager-015b2b7520797e279dd4783eeb68fa8b8a26db6ecc1d684f67bfdc13411791e3.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/kube-controller-manager-minikube_kube-system_kube-controller-manager-9a1b2e08d497b4ca2d144e42972b23fad15bcc1beb996a2b8e6fab0aee3999c0.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/elasticsearch-master-2_default_elasticsearch-c179b54e4fdddbb20068269b75cd88325658c98847d6d906d3a4a954860885af.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/coredns-66bff467f8-pswng_kube-system_coredns-3a7f59d8a9cec5ed207b061d89853cd19c3f2388a09a628c590d079c76af0323.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/storage-provisioner_kube-system_storage-provisioner-73c205f99d913ad3915df4004aeab5a03a549c7d00dbc47288bfec9cdedfdcf8.log
2020-07-22 08:06:17 +0000 [info]: #0 [in_tail_container_logs] following tail of /var/log/containers/elasticsearch-master-2_default_configure-sysctl-a4ffd4fa272b2eaaa4bb6daf8e2618ee109fdb9aba51ce9130226823f1be08a0.log
2020-07-22 08:06:17 +0000 [info]: #0 fluentd worker is now running worker=0

#0 fluentd worker is now running worker=0 となるので正常に動作しています。

動作させていくらか経ったElasticsearchのインデックスの状態を確認します。

$ curl localhost:9200/_cat/indices
green open logstash-default-2020.07.22-000004 1BNxJGy2R_u2ETFog8hI5g 1 3     0 0    624b   208b
green open logstash-default-2020.07.25-000002 yesiqdW4Qdi_PD5JEGwctw 1 3     0 0    624b   208b
green open logstash-default-2020.07.27-000002 y0-NGO_3SmmcRRnLNNy9cw 1 3     0 0    624b   208b
green open logstash-default-2020.07.22-000003 B8Z41XVvTpCIjrLewEutMA 1 3    61 0 153.5kb 51.1kb
green open logstash-default-2020.07.21-000001 5JzBvzkgSve-pJBOl-_AOA 1 3  3662 0   4.5mb  1.5mb
green open logstash-default-2020.07.22-000002 hsojlNScTOCaMDKCLlTEjQ 1 3     0 0    624b   208b
green open logstash-default-2020.07.25-000001 L9mPpFjrSYmex27-oUlqsQ 1 3 22798 0    17mb  5.6mb
green open logstash-default-2020.07.28-000001 kYvZ7R_fSVaXcIMBdP7OgA 1 3   132 0 518.8kb  185kb
green open logstash-default-2020.07.27-000001 CJTNpX4lQf6frlc5v1aHIg 1 3 62061 0  45.2mb 15.1mb
green open logstash-default-2020.07.25-000003 g_oSzQTRS42Pn5DZm8NSMA 1 3     0 0    624b   208b

logstash-default-2020.07.22-000001のインデックスは作成されてから2日以上経っているので消されていることがわかります。
インデックスがILMで管理されているかどうかを確認します。

$ curl localhost:9200/logstash-2020.07.28/_ilm/explain | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   550  100   550    0     0  36666      0 --:--:-- --:--:-- --:--:-- 36666
{
  "indices": {
    "logstash-default-2020.07.28-000001": {
      "index": "logstash-default-2020.07.28-000001",
      "managed": true,
      "policy": "logstash-policy",
      "lifecycle_date_millis": 1595898121210,
      "age": "4.37h",
      "phase": "hot",
      "phase_time_millis": 1595898121544,
      "action": "rollover",
      "action_time_millis": 1595898216494,
      "step": "check-rollover-ready",
      "step_time_millis": 1595898216494,
      "phase_execution": {
        "policy": "logstash-policy",
        "phase_definition": {
          "min_age": "0ms",
          "actions": {
            "rollover": {
              "max_size": "5gb",
              "max_age": "1d"
            }
          }
        },
        "version": 7,
        "modified_date_in_millis": 1595398857585
      }
    }
  }
}

"managed": true,とあるため、このインデックスはILMにより管理されています。
試しにElasticsearchに検索リクエストを飛ばしてみましょう。

$ curl -XGET -H "Content-Type: application/json" localhost:9200/logstash-2020.07.28/_search -d '{"size": 2, "sort": [{"@timestamp": "desc"}]}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2502  100  2457  100    45   5906    108 --:--:-- --:--:-- --:--:--  6014
{
  "took": 391,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 242,
      "relation": "eq"
    },
    "max_score": null,
    "hits": [
      {
        "_index": "logstash-default-2020.07.28-000001",
        "_type": "_doc",
        "_id": "OXzek3MBhNn_Y01M22q7",
        "_score": null,
        "_source": {
          "log": "2020-07-28 05:21:57 +0000 [info]: #0 [filter_kube_metadata] stats - namespace_cache_size: 2, pod_cache_size: 2, namespace_cache_api_updates: 4, pod_cache_api_updates: 4, id_cache_miss: 4\n",
          "stream": "stdout",
          "docker": {
            "container_id": "65a884bccf20d3134d96f836a3a1a9e1116bee9fd01a8298206b54baf9340f84"
          },
          "kubernetes": {
            "container_name": "fluentd",
            "namespace_name": "kube-system",
            "pod_name": "fluentd-hqh9n",
            "container_image": "fluent/fluentd-kubernetes-daemonset:v1.11.1-debian-elasticsearch7-1.3",
            "container_image_id": "docker-pullable://fluent/fluentd-kubernetes-daemonset@sha256:af33317d3b8723f71843b16d1721a3764751b1f57a0fe4242a99d1730de980b0",
            "pod_id": "fdfd5807-8164-4907-a1a9-9b782c3eb97e",
            "host": "minikube",
            "labels": {
              "controller-revision-hash": "57997fd64d",
              "k8s-app": "fluentd-logging",
              "pod-template-generation": "1",
              "version": "v1"
            },
            "master_url": "https://10.96.0.1:443/api",
            "namespace_id": "5c1df0cc-d72b-4c24-a4af-a8d595d62713"
          },
          "@timestamp": "2020-07-28T05:21:57.002062084+00:00",
          "tag": "kubernetes.var.log.containers.fluentd-hqh9n_kube-system_fluentd-65a884bccf20d3134d96f836a3a1a9e1116bee9fd01a8298206b54baf9340f84.log"
        },
        "sort": [
          1595913717002
        ]
      },
      {
        "_index": "logstash-default-2020.07.28-000001",
        "_type": "_doc",
        "_id": "gWLek3MByRVHYKQQ2yC5",
        "_score": null,
        "_source": {
          "log": "2020-07-28 05:21:56.801939 I | mvcc: finished scheduled compaction at 59275 (took 3.825907ms)\n",
          "stream": "stderr",
          "docker": {
            "container_id": "8ece6d2d408533810f2bc33e9aeeb534ea2781259c8046331b297c446ec24fe9"
          },
          "kubernetes": {
            "container_name": "etcd",
            "namespace_name": "kube-system",
            "pod_name": "etcd-minikube",
            "container_image": "k8s.gcr.io/etcd:3.4.3-0",
            "container_image_id": "docker-pullable://k8s.gcr.io/etcd@sha256:4afb99b4690b418ffc2ceb67e1a17376457e441c1f09ab55447f0aaf992fa646",
            "pod_id": "27093604-c8b7-4b22-a0df-c7eebe63afb3",
            "host": "minikube",
            "labels": {
              "component": "etcd",
              "tier": "control-plane"
            },
            "master_url": "https://10.96.0.1:443/api",
            "namespace_id": "5c1df0cc-d72b-4c24-a4af-a8d595d62713"
          },
          "@timestamp": "2020-07-28T05:21:56.802153342+00:00",
          "tag": "kubernetes.var.log.containers.etcd-minikube_kube-system_etcd-8ece6d2d408533810f2bc33e9aeeb534ea2781259c8046331b297c446ec24fe9.log"
        },
        "sort": [
          1595913716802
        ]
      }
    ]
  }
}

確かにElasticsearchにk8s内部で発生したログがストアされていることが確認出来ました。

まとめ

FluentdのDaemonSetにより、k8s内部のログをILMを有効化してElasticsearchにストアするやり方を解説しました。
この記事の方法でElasticsearchのILMを有効化する場合、helmを使っているのであればElasticvsearchをデプロイする際に注意点があります。helmの公式リポジトリでデプロイできるElasticsearchは古く、
Elasticsearchの開発元のものを使ってhelmでElasticsearchのクラスターをデプロイしてください。

また、記事中で使用しているILMのポリシーは2日経ったらインデックスを消去するという単純なものですが、実際のプロダクション環境ではhotのあとにwarmやcold状態を挟んでdeleteに移行するポリシーを作成するよう検討してください。

当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。

*1 fluent-plugin-elasticsearch v4.1.1でILM関連のバグは直しました。

タグ: Fluentd
2020-07-29

gbpを使ったDebianパッケージでパッチのファイル名を明示的に指定するには

はじめに

Debianパッケージのメンテナンスではgbpコマンドがしばしば使われます。
gbpを使ったワークフローでは、従来やや面倒だったquiltを意識せずにパッチを管理できます。

ただし、特に指定をしなければコミットメッセージからファイル名が自動的に設定されてしまいます。*1

debian/patches以下にあるパッチのファイル名を明示的に指定するにはどうすればよいでしょうか。

gbp pqコマンドを利用したパッチ管理をする

gbpにはgbp pqというパッチ管理のためのコマンドがあります。
gbp pqを使うと、冒頭で述べたようにquiltを意識せずにパッチを管理できます。*2

仮にdebian/unstableブランチでunstable向けのパッケージをメンテンナンスをしているときに次のコマンドを実行すると、
パッチを管理するためのブランチをrebaseできます。

gbp pq rebase

rebase後のブランチ名はpatch-queue/debian/unstableです。
このブランチではdebian/unstableブランチにdebian/patches/以下のパッチがコミットされている状態となります。

Gbp-Pq:タグを使う

gbp-pq(1)を読むと書いてありますが、コミットメッセージにGbp-Pq:タグを含めます。

Gbp-Pq:タグを含んだコミットとは例えば次のようなコミットです。

commit 9f956f8232cce926eed52ab3fb2ed3f951d40652
Author:     Steven Chamberlain <stevenc@debian.org>
AuthorDate: Sat Jul 16 23:52:50 2016 +0100
Commit:     Kentaro Hayashi <kenhys@gmail.com>
CommitDate: Mon Jul 20 21:37:15 2020 +0900

    Use _GNU_SOURCE on GNU/kFreeBSD

    Define _GNU_SOURCE not only on GNU/Hurd, but also other glibc-based
    platforms including GNU/kFreeBSD.

    See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=826061#10

    Gbp-Pq: Name fix-nginx-FTBFS-on-kfreebsd.patch
---
 vendor/nginx-1.17.9/src/os/unix/ngx_posix_config.h | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/vendor/nginx-1.17.9/src/os/unix/ngx_posix_config.h b/vendor/nginx-1.17.9/src/os/unix/ngx_posix_config.h
index 2a8c413e..03f7e0a5 100644
--- a/vendor/nginx-1.17.9/src/os/unix/ngx_posix_config.h
+++ b/vendor/nginx-1.17.9/src/os/unix/ngx_posix_config.h
@@ -21,10 +21,13 @@
 #endif

-#if (NGX_GNU_HURD)
+#if defined(__GLIBC__)
 #ifndef _GNU_SOURCE
 #define _GNU_SOURCE             /* accept4() */
 #endif
+#endif
+
+#if (NGX_GNU_HURD)
 #define _FILE_OFFSET_BITS       64
 #endif

この状態でgbp pq exportを実行すると、debian/patches/fix-nginx-FTBFS-on-kfreebsd.patchが生成されます。簡単ですね。

まとめ

今回は、gbpを使ったパッチ管理においてファイル名を明示的に指定する方法(Gbp-Pq: Name)を紹介しました。
patches以下にパッチを貯めるのはあまりおすすめできませんが、Debianパッケージをgbpでメンテナンスするときに必要になったら参考にしてみてください。

*1 git format-patchみたいな挙動といえばわかりやすいかもしれません。

*2 普通にgitのブランチを扱う感覚でパッチも管理できる

2020-07-22

ノータブルコード10 - 文芸的設定ファイル

第10回目のノータブルコードで紹介するのは、ドナルド・クヌースの設定ファイルです。

文芸的プログラミングとは何か

ドナルド・クヌースは『The Art of Computer Programming』の作者、そして組版ソフトウェアTeXの開発者として世界的に名を知られた伝説的な人物ですが、彼はまた「文芸的プログラミング」の概念を提唱したことでも知られています。今日の記事のテーマは、彼の業績のうちの文芸的プログラミングに関する部分です。

文芸的プログラミングの哲学を、一言で説明するのは難しいのですが、その基本的な発想は、プログラムを、単に機械が解釈できるフォーマルな命令の羅列ではなく、人間が読むことができる自然な文章としても提示しようというアイデアにありました(クヌース自身はこれを「自然言語である英語と、CやLispのような形式言語の間で切り替えをし、まとめ上げられる自然なフレームワーク」*1と表現しています)。 この哲学の実践的なメリットは、その原理が正しく実行されれば、真の意味で「人間が読むことのできるプログラム」を生み出せることです。

クヌースが真に偉大なのは言行一致であること、すなわち自分が提唱しているアイデアを、自ら実践しているところです — これが今日の記事の本題につながります。クヌースはTeXのような公開を意図したプログラムはもちろんのこと、個人的に書くプログラムでも文芸的なスタイルを採用していると語っています。

そして、そのスタイルの適用範囲は、驚くべきことに、設定ファイルにも及ぶのです。

クヌースの設定ファイル

実際の例を見てみましょう。以下に引用するのは、クヌース自身の手になるFVWM2向け設定ファイルの冒頭部分です。

https://www-cs-faculty.stanford.edu/~knuth/programs/.fvwm2rc

# This Fvwm2 setup file provides the basic emacs-centered environment
# that I have found most comfortable on my standalone machine at home.
# Basically it gives me a big Emacs window at the left and a slightly
# smaller XTerm at the right, together with a clock and CPU monitor
# and a few buttons for accessing independent desktops.

# I've tried to write lots of comments because I will certainly forget
# most of the details of Fvwm2's syntax and semantics before long.

# My display screen is 1440 pixels wide and 900 high.

# First, make sure that Exec chooses tcsh instead of bash:
ExecUseShell /bin/tcsh

# Next, specify the paths for all icons (.xpm files) that I'm using:
# PixmapPath /usr/share/icons:/home/icons
ImagePath /home/icons:/usr/share/icons

# I tried mxn desktops and didn't like them.
DeskTopSize 1x1

全文は274行あるので、ぜひリンク先も見てください。この記事では、次の3つのポイントを指摘しておきたいと思います。

  1. まず、この設定ファイルはごく自然な文章として読むことができます。「文芸的」の名前に違わず、プログラム向けの設定と、人間向けの説明がごく自然に統合されています。

  2. また、設定の背景にある前提は何かが明示されているのもポイントです。例えば、普段はEmacsとXTermを利用していること、設定の対象となるディスプレイが1440x900のサイズであること、などの背後にある隠れた重要な前提がきちんと読者に示されています。

  3. さらに見逃せないポイントは、設定の構成自体が、自然言語の文章の流れに沿うように組み立てられている点です。これは、プログラムのいわば後付けとしてコメントを付与する一般的な流儀とは、明確に発想を異にしている点です。

もちろん、このスタイルをすべての人が実践できるか、というと議論があるところです。というのも、明らかにこの手法は (1) プログラマとしての能力に加えて (2) 優れた文章の書き手としての能力を兼ね備えていないと実践できないからです。

それでも、この設定ファイルが「ちょっと文芸的スタイルを試してみようかな」と読み手に関心を引かせるほどの魅力を持っているのは事実です。ともすると、プログラミングを数理科学の一つとして位置づける現在主流の考え方は根本的に間違っていて、本当は人文科学としての国語の一分野なのかもしれない、そう思わせるほどの説得力が、このコードにはあります。

まとめ

今日の記事では、文芸的プログラミングの実践例としての(文芸的)設定ファイルを紹介しました。

この設定ファイルを読んで、皆さんはどう思いましたか?ご意見ご感想があればぜひお寄せください。

*1 ピーター・サイベル著・青木靖訳「Coders at Work プログラミングの技をめぐる探求」(オーム社, 2011年)p558

2020-07-20

Fluentdのベンチマークツールの開発

はじめに

クリアコードはFluentdの開発に参加しています。

Fluentdにはプラグインというしくみがあり、たくさんのプラグインが開発されています。
Fluentdのプラグインでは各種APIを使用しており、プラグインによって消費するリソースの傾向が異なるということがあります。
今回そのリソースの傾向はどの程度なのかを知るために筆者畑ケがベンチマークツールを開発し、傾向を測定しました。

Windows EventLogを扱うプラグイン

Windows EventLogを引っ張ってくるプラグインは https://github.com/fluent/fluent-plugin-windows-eventlog にて開発されています。Fluentdの開発チームはwin32-eventlog gemでは対処しきれないEventLogの形式があることから、winevt_c gemを開発しています。
このgemは基本的にCで書かれているため、大きなボトルネックになることはありませんが、リソースの消費傾向を把握するのは重要と考えています。

Fluentdのベンチマークの考え方

FluentdのInputプラグインやOutputプラグインは基本的にある間隔で動作します。また、Outputプラグインはすぐに送ることはせずにbufferingをします。このことから、Fluentdが消費しているリソースを時間ごとにモニタリングした生データを単にプロットするだけではどの程度のリソース消費をするかが判りにくくなります。
リソースの消費傾向は中央値(メジアン)・25パーセンタイル〜75パーセンタイルの範囲、そして99パーセンタイル程度の測定値がどの程度の範囲内にあるかを図示した方がリソースの消費傾向が分かりやすくなります。

箱ヒゲ図(Box Plot)とは

箱ヒゲ図とは、データのばらつきを分かりやすく表現するためのグラフの一種です。 例として以下の図にあるような箱ヒゲ図を見てみます。

(https://towardsdatascience.com/understanding-boxplots-5e2df7bcbd51 より引用)

この箱ヒゲ図では、真ん中の箱の範囲に25パーセンタイルから75パーセンタイルの中位50パーセンタイルの数値が入ります。
また、下ヒゲから箱の下までが下位24.65パーセンタイル、上ヒゲから箱の上までが上位24.65パーセンタイルの数値が入ります。下ヒゲから上ヒゲまでが99.3パーセンタイルの数値が入ります。時折生じるリソース消費のスパイク現象を除いたリソースの消費量を見るには、下ヒゲから上ヒゲの99.3パーセンタイルの範囲の数値をみると良いことになります。

この箱ヒゲ図を正規分布に対応させると以下のようになります。

(https://towardsdatascience.com/understanding-boxplots-5e2df7bcbd51 より引用)

ただし、この箱ヒゲ図には重要な仮定があります。値の分布が正規分布*1 に従っている*2という条件があります。

ベンチマーク環境の作成

こちらはTerraformを用いてAzureにベンチマーク環境を整えることで実施しました。
また、ベンチマーク後のグラフの描画にはmatplotlibを基にしたseabornを用いています。

Windows EventLogのベンチマークを実施する
ベンチマークの準備

まず、Python3の環境をセットアップします。ベンチマーク環境を整備するにはpython3のインストールが必要です。
ここでは、ホスト環境がUbuntuであると仮定します。

$ sudo apt install python3 python3-venv build-essentials

ベンチマーク環境をセットアップするスクリプトをgit cloneします。

$ git clone https://github.com/fluent-plugins-nursery/fluentd-benchmark-azure-environment.git
$ cd fluentd-benchmark-azure-environment

venvを使ってシステムのPython3環境と分離します。

$ python3 -m venv management
$ source management/bin/activate

requirements.txtを使って必要なPython3のライブラリをインストールします。

$ pip3 install -r requrirements.txt

Ubuntuで実行可能なTerraformを https://www.terraform.io/downloads.html からダウンロードして来てインストールします。
この記事ではWindows EventLogのベンチマークの実行を例にするため、winevtlog_benchディレクトリにcdします。

$ cd winevtlog_bench

Terraformを初期化して、必要なProviderをダウンロードします。

$ terraform init

terraform.tfvars.sampleをコピーします。

$ cp terraform.tfvars.sample terraform.tfvars

以下の変数の値を実際に使用するものに書き換えます。

linux-username       = "admin"
linux-password       = "changeme!"
region               = "Japan East"
windows-username     = "admin"
windows-password     = "changeme"
ssh-private-key-path = "/path/to/private_key"
resource-group       = "ExampleGroup"

ssh-keygenを用いてid_rsa_azureというRSA 2048ビットの秘密鍵と公開鍵を生成します。id_rsa_azure.pubを、azure_keyというディレクトリに格納します。

Azureの認証情報は Terraformの導入 - 検証環境をコマンドで立ち上げられるようにする その1 を参考に取得します。
env.shをコピーし、

$ cp env.sh.sample env.sh
#!/bin/sh

echo "Setting environment variables for Terraform"
export ARM_SUBSCRIPTION_ID=<SUBSCRIPTION_ID>
export ARM_CLIENT_ID=<APP_ID>
export ARM_CLIENT_SECRET=<APP_PASSWORD>
export ARM_TENANT_ID=<TENANT_ID>

スクリプト中の角かっこの値を埋めて、env.shを読み込みます。

$ source env.sh

また、AzureのCLIでのログインは事前に行っておいてください。

ここまででTerraformを使ったベンチマーク用のAzureインスタンスを建てる準備が整いました。

ベンチマークの実施

ベンチマークの実施方法はMakefileとAnsible Playbookに集約されているので順番に実行していけば良いです。

$ make apply

により、Azure上にベンチマーク用のインスタンスが建ちます。

$ make provision

により、ベンチマークに必要なライブラリやツールが建てたAzureのインスタンスにダウンロードされ、インストールされます。

Windows EventLogの単純なベンチマークは、

$ make windows-bench

により実施されます。このコマンドを実行すると、裏ではAnsible Playbook化されたタスクが走ります。
このタスクはWindows上で採取されたデータを収集する部分まで含まれます。

ベンチマーク結果の可視化には次のコマンドも実行します。

$ make visualize

このコマンドにより、ベンチマーク結果を箱ヒゲ図で可視化できます。
ベンチマークが終わった後は、ベンチマークに使ったAzureインスタンスを破棄してしまいましょう。

$ make clean

ベンチマーク結果の一例

CPUの消費傾向を箱ヒゲ図で見てみます。
また、記事中では解説していませんが、外れ値があるかどうかもチェックしたいため、strip plotで実際の値も箱ヒゲ図に重ねてプロットしています。小数点以下第3位で四捨五入したメジアンの値ラベルについても、箱ヒゲ図に重ねてプロットしています。
およそ12分で120000イベントのWindows EventLogをin_windows_eventlog2で受け取った場合のFluentdのワーカーのCPU使用率です。

おおよそ1分間に159イベント程度を決められたチャンネルに書き込む流量があります。
このベンチマークでは、イベントの流量とサイズが大きくないため、ワーカープロセスのCPU使用率には差が出ていません。

ワーカープロセスのメモリ使用量はどうでしょうか。

こちらは、受け取ったメッセージサイズに多少影響を受けるところが見て取れます。

まとめ

Fluentdのプラグインのベンチマークの方法を解説してみました。
Windows向けに開発したプラグインでは、Linux向けとは違うリソースを消費する傾向になってしまう事があります。
Windows EventLogを扱う際にはWindowsが提供するAPI経由となるため、Cで書いている箇所に関しては大幅なボトルネックとなってしまう箇所が少ない事が確認できました。
また、ある程度の流量にも耐えられうる状態で提供できていることも確認できました。

*1 https://www.mathsisfun.com/data/standard-normal-distribution.html

*2 実際には正規分布というよりもリソースは有限なので下位側に潰れた分布になりますが、議論を簡単にするためこの仮定を置いて問題はないでしょう。

タグ: Fluentd
2020-07-07

ノータブルコード9 - C言語で文字列を扱うときはNULL終端されているかどうかに注意する

全文検索エンジンGroongaの他にPostgreSQLで高速に全文検索できる拡張PGroongaの開発にも参加している堀本です。
今回は、PGroongaの開発中に「注意しないと危ないな」と思ったコードを紹介します。

PGroongaはPostgreSQLの拡張として開発されています。
そのため、当然ですが、PostgreSQLとデータのやり取りを行います。
PostgreSQLには、PostgreSQLの型があり、以下のような型が組み込みの型として用意されています。

https://www.postgresql.jp/document/12/html/datatype.html#DATATYPE-TABLE

この中で文字列を格納する型としてよく使われるのがtext型(長さ制限のない可変長文字列)です。
今回PGroongaに新しい関数を実装するのに、以下のようにエラーログを出力する処理を書きました。

if (desc->natts <= i)
{
	ereport(ERROR,
		(errcode(ERRCODE_INTERNAL_ERROR),
		 errmsg("pgroonga: an invlid value was specified for column name: %s",
			 columnNameData)));
}

新しく実装した関数は以下のようなインターフェースを持っていて、PGroongaのインデックスを設定したカラムの名前を与えると、それに対応するPGroongaが内部で管理しているテーブルの名前を返すようになっています。

text pgroonga_index_column_name_string(indexName text, columnName text)

冒頭のコードは、この関数の第二引数に存在しないカラムの名前を指定された場合にエラーログを出力するようにしています。
ここで、ログに出力しているcolumnNameDataは、以下のようにtext型の変数からVARDATA_ANYマクロでデータを取り出しています。

const text *columnName = PG_GETARG_TEXT_PP(1);
const char *columnNameData = VARDATA_ANY(columnName);

エラーログで出力しているcolumnNameDataの方はchar *型なので、%sを使って出力するので問題ないように見えますが、PostgreSQLのtext型はNULL終端されていないので、%sを使って出力すると、意図しない領域まで出力してしまう可能性があります。
このようなバグを防ぐため、PostgreSQLのtext型を使用する場合は以下のように、必ず長さ指定をする必要があります。

if (desc->natts <= i)
{
	ereport(ERROR,
		(errcode(ERRCODE_INTERNAL_ERROR),
-		 errmsg("pgroonga: an invlid value was specified for column name: %s",
-			 columnNameData)));
+		 errmsg("pgroonga: an invlid value was specified for column name: %.*s",
+				(const int)columnNameSize,
+				columnNameData)));
}

C言語ではNULL終端の文字列として文字列を扱う場合(C言語の標準の文字列関数)とデータとデータの長さで文字列を扱う場合(printf%.*sを使う場合など)と文字列の開始位置のポインターと終了位置のポインターで文字列を扱う場合があり、扱いを間違うと思わぬバグを仕込むことになってしまいます。

C言語の文字列に関わる問題は多いですが、改めてC言語の文字列の扱いには注意が必要だと感じたコードの紹介でした。

2020-06-19

Firefox ESR68の法人ユーザーは、Firefox ESR78にどう備えるべきか?

来たる6月30日、Firefox ESR78がいよいよ正式にリリースされます(予定)。ESR版としてや1年ぶりのメジャーアップデートです。現時点で既に(ESR版ではなく一般向けの)ベータ版が利用可能になっているため、そろそろ検証を始められている情シス担当の方も増えてきているのではないでしょうか。

Firefox ESR68からの変更点は多岐に渡りますが、その中でも、この記事では法人利用の場面において影響があると考えられる変更点をご紹介します。

移行作業のスケジュール

最初に、スケジュールについておさらいしましょう。

詳細はMozilla Wikiのリリースカレンダーに記載されていますが、端的には、Firefox ESR78への移行は日本時間で2020年9月23日までに完了させることが強く推奨されます

Firefox ESR78.0正式版がリリースされた後も、Firefox ESR68のサポートはしばらく継続します。具体的には通常版のFirefoxのリリース3回分*1まではFirefox ESR68のセキュリティアップデートが提供される予定となっています*2。このサポート期限が太平洋標準時で2020年9月22日、日本時間では9月23日ということになります。

トラッキング保護機能の強化・改善

Firefoxには以前からトラッキング保護機能が含まれていて、Webサイトによる行動履歴の収集を回避できるようになっています。しかし、この機能はESR68までは初期状態では無効で、有効になるのはプライベートブラウジングウィンドウを使っている時だけでした。

Firefox ESR78では、トラッキング保護機能がプライベートブラウジングウィンドウ以外でも初期状態で有効になっています。また、ブロック対象が拡大され、以下のトラッキング手法もブロックされるようになりました。

  • サードパーティのトラッキングCookie
  • 暗号通貨の採掘スクリプト(トラッキングとは別の文脈ですが、トラッキング保護の一環としてブロックされます)
  • フィンガープリンティングのためのスクリプト
  • SNSによるトラッキングのためのスクリプト(Facebook、Twitter、LinkdIn)

訪問したWebページでどのようなトラッキング手法が実際にブロックされているかは、アドレスバーの盾アイコンをクリックすると確認できます。
(トラッキング保護の状態を表示した様子のスクリーンショット)

この機能は法人利用では、プライバシー保護よりはトラフィック削減の観点から有用性が期待できます。というのも、昨今の多くのWebページにはトラッキングスクリプトが含まれており、この事がネットワーク帯域の消費量増につながっているからです。

ただし、トラッキング保護機能の動作はブロックリストに基づいており、機能を使うにはリストが定期的に取得・更新されている必要があります。法人利用では、バックグラウンドで行われる無用な通信の削減を目的として、ブロックリストの取得も禁止している場合がありますが、その状態ではトラッキングブロック機能は有効に機能しないので、運用ルールの見直しが必要になります。

定期的に取得されるブロックリストのデータサイズは、本稿執筆時点では非圧縮状態で250KB程度でした。ブロックリストは1時間に1回の頻度で更新がチェックされ、リストが更新されていた時にだけ実際にダウンロードが行われます。このブロックリストによって実際にどの程度のトラフィック削減効果を得られるかは、状況や閲覧するWebサイトによって変わりますので、できれば試験運用で効果を測定してから判断したい所です。

OSの認証機構や証明書データベースとの連携の強化

Firefox ESR78ではWeb Authentication HmacSecret拡張により、Windows 10の2019年5月版以降で、Windows Helloによる指紋認証や顔認証などのパスワード非依存の認証をWebサイトでも使えるようになりました。実際に使うにはWebサイト側の対応が必要なので、パスワード認証のWebサイトが突然顔認証可能になるというわけではないのですが、SaaS形式のWebサービスなどでは今後利用可能になっていくと考えられます。

OSの認証処理との連携強化は、パスワードマネージャの動作にも反映されています。具体的には、パスワードマネージャに保存したパスワードを閲覧する際にOSの認証を求められるようになり、その際には顔認証などを利用できるようになっています。

また、実験的な機能として、security.osclientcerts.autoloadを有効化することによって、WindowsとmacOSでOSの証明書ストアにあるクライアント証明書を利用できるようになりました。これまでFirefoxでクライアント証明書を使うためには、既にWindows自体の証明書データベースにクライアント証明書がある場合でも、それとは別にFirefoxの証明書マネージャを使って手動で証明書を登録する必要がありましたが、このような運用を簡略化することができます。OSの証明書ストアにあるエンタープライズのルート証明書は、Firefox ESR60以前からすでに自動インポート可能になっていましたので、OSの証明書ストアとの連携がさらに強化されたと言えます。

その他、セキュリティに関わる変更

セキュリティに関する変更の中で目立つのは、SSL/TLSで接続しているWebサイトの表示の仕方が変わったという点です。
(スクリーンショット、リンク先記事から引用)

  • 安全でないHTTP→斜線で打ち消された錠前アイコン
  • EV SSL→グレーの錠前アイコン

これは、Googleなどによって進められている「常時SSL化」の流れを承けての、より安全な状態を標準的な状態として取り扱うようにする変更です。Webサイトの安全性そのものが変化したわけではありませんが、「偽サイトに誘導されてしまったのか?」とユーザーが誤解する恐れはりますので、問い合わせが増加しても大丈夫なように備えておきましょう。

逆に、反映が見送られたセキュリティに関わる変更としては、「TLS1.2以上の強制(TLS 1.0/1.1を無効)」があります。これは、医療情報を提供する公のWebサイトにまだTSL 1.0/1.1で提供されている物がある可能性があるためで、新型コロナウィルスの感染拡大とそれに伴う混乱が収束するまでは変更を保留する、という判断に基づいています。

とはいえ、TLS 1.0/1.1には脆弱性があり、暗号化された通信内容が盗聴されてしまう恐れがある、という事実は変わりません。自社のWebサイトでTLS 1.0/1.1を使っていないかを改めて確認し、もしあれば早急にTLS 1.2以上へ移行するようにしましょう。

DNSでの名前解決をより安全にする「DNS over HTTPS」については、米国地域の英語版Firefoxユーザーに対しては設定が既定で有効化されますが、それ以外の地域・言語では無効となっています。この点はFirefox ESR78でも同様です。とはいえ、正式リリースまでの間に判断が変わる可能性も無いとは言い切れませんので、運用上の懸念がまだ残っている場合は、過去記事で紹介した無効化手順に則って機能を無効化するのがお薦めです。

Flashや動画/音声を使用したWebページの動作について

Firefoxのプラグイン対応は段階的に廃止が進んでおり、Firefox ESR68の時点ではAdobe Flash以外のプラグインは機能しなくなっています。この一環としてFirefox ESR78では、従来可能であった「ページを読み込んだ時点で自動的にFlashコンテンツを再生する」動作が廃止されました。今後は、Flashコンテンツの再生には必ずユーザーによる明示的なクリック操作が必要となります。業務でFlashを使用していて、クリック操作無しでの再生を前提に運用している場合は、影響に注意してください。

Firefox ESR78では、音声を伴う動画のみ自動再生を禁止するという既定の動作に加えて、音声を伴わない動画も含めてすべての自動再生を禁止する動作が可能になりました。media.autoplay.default5 に設定すると、すべての動画の自動再生をブロックするようになります。この設定は、ネットワークトラフィックの削減に有効である可能性があります。

動画に関連した変更としては、専用のアプリケーションの導入なしにFirefoxだけでビデオ会議サービスの「Zoom」を利用できるようになった、という改善もあります。ビデオ会議通話の需要が増加した昨今なので、この変更は運用コストの低減に繋がるかもしれません。

外部のアプリケーションやシステム管理者がアドオンを組み込む際の方法の変更

Firefox ESR68までのバージョンでは、C:\Program Files\Mozilla Firefox\browser\extensions にアドオンのファイルを設置したり、Windowsのレジストリに情報を記載したりすることで、ユーザーがアンインストールできない形でアドオンをインストールさせる事ができました*3。Firefox ESR78からは、この形式でのインストールが制限されるようになります。

具体的には、この方法でインストールされたアドオンは、従来であればそのまま読み込まれていましたが、今後は「ユーザーがそのアドオンをインストールしたのと同じ状態」に自動的に変換されるようになります。ユーザーが任意でアドオンをアンインストール可能になるので、アンインストールされては困るアドオンを全社的に使用している場合、過去記事で紹介した代替手法による対策が必要です。

実は、更新チャンネルがESR版に設定されているビルドでは従来通りのサイドローディングが有効な状態になっています。そのため、従来通りの運用を続ける事も不可能ではありません。

しかしながら、サイドローディングを有効にするかどうかはFirefoxのビルド時の指定で決定されるため、Firefox 78のベータ版では動作を確認できません。サイドローディングを使った運用を継続したい場合には、動作の検証はFirefox ESR78.0の正式版がリリースされるのを待って行う必要があります。

まとめ

以上、Firefox ESR68からFirefox ESR78の間での変更点のうち、法人利用に影響があると考えられるトピックをご紹介しました。

Firefox ESR78より未来のことは現時点では未知数の部分が多いですが、注視する必要がある今後の変更の1つとして、Windowsのタスクスケジューラを使ったバックグラウンドでの自動更新があります。恐らく、来年リリースされるであろう次のESR版からはこの変更が反映されるものと予想されます。法人利用では自動更新を無効化して運用する事例が多いので、タスクスケジューラに基づく自動更新の停止方法は気になる所でしょう。当社のお客さまにも影響があるため、今後何か新しい事が分かり次第、改めて調査してこのブログで解説する予定です。

当社の法人向けFirefoxサポートサービスでは、Firefoxの新しいバージョンがお客さま環境での運用状況にどのように影響を与えるかなど、総合的な調査も必要に応じ承っております。Firefoxを運用していて、先手先手で新バージョンへの対応を進めていきたいとお考えの企業さまがいらっしゃいましたら、お問い合わせフォームよりご相談ください。

*1 以前は通常版のリリース2回分まででしたが、リリースサイクルの短縮と帳尻を合わせるためか、リリース3回分に延長されています。ただし、日数としてはFirefox ESR68のリリースからFirefox ESR60のサポート終了までが105日間あったのに対し、Firefox ESR78のリリースからFirefox ESR68のサポート終了までは84日間となっていて、実質的な期間は短くなっています。

*2 なので、例えば「9月21日にFirefox ESR68.13がリリースされる」という可能性も0ではありません。

*3 このようなインストール方式を「サイドローディング」と呼びます。

タグ: Mozilla
2020-06-12

タグ:
年・日ごとに見る
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|02|03|04|05|06|07|08|