京都でレガシーシステムのはなしをした

2/22(金)にLegacy Meetup Kyotoで発表してきました。

speakerdeck.com

ここ1年くらいでやってきた、レガシーシステムの改善に関する3つのプロジェクトを紹介し、その過程で考えたことを少し整理してお話しました。レガシーシステムと相対する人類にとって少しでも参考になれば幸いです。

Q&A

懇親会でいくつか質問をいただきました。まともに話す時間もなく答えきれなかったものを、書いておいてみます。本人に届く可能性は極めて低くとも。

Q:受託開発の場合、顧客をどう説得するか?

自分は経験がなく想像もあまりできないのでわかりません。というと終了するので、想像で答えると、説得しなくてよいのではないでしょうか。どうしても説得したければ、説得に足る情報を集めて判断してもらえばよいのでは。発表の中にある「合意」に必ずしも正しさ・清さを包含しません。人間はわかりあえないという前提のもと、自分にとって都合がよく、かつ後々不利益を被らない情報を恣意的に提供すればいいと思います。

Q:巨大すぎる技術的負債にどう立ち向かうか?

立ち向かわなくてよいのではないでしょうか。疲れるし。どうしても立ち向かいたい理由があるなら、まずはどれくらい巨大かを把握しようとしてみては。本当に巨大だったら諦めもつくでしょう。意外とそんなに巨大ではないかもしれません。そしたらラッキーですね。

Q:技術的負債はまだ残っているのか?

個人ブログなので詳しくは書きませんが、言えるとすれば、今回の内容は「レガシーシステムの端の端の末端の端っこを少しなおしてみた」ということでしかないです。「コードは書いた瞬間から技術的負債になる」ってだれかが言ってました。

様子

会場は古き良き町家をリノベーションした建物です。リノベーションといっても、畳敷きの和室はそのままです。長机と座布団を並べたその空間は、寺子屋あるいは習字教室を彷彿とさせました。「皆様、足を崩して楽な姿勢でどうぞ…」のアナウンスをもって完全に法事と化しました。

発表者の立場としては、とても話しやすかったです。東京で社内/社外、お仕事/プライベートでいくつか発表をしたことがありますが、今回はとりわけ快適でした。勉強会のテーマが特定の技術によらないエンジニアリングの共通項だったことに加え、前述のとおりリラックスできる場の共有が少なからず寄与していると思います。初対面なのに内輪ともいうべき不思議な雰囲気でした。

他の登壇者の発表もおもしろかったです。ヤフーの山本さんの発表からは新たな技術や未知を楽しむ""つよさ""を感じましたし、はてなの id:t_kyt さんの発表からは並々ならぬ""やっていき""を感じました。資料は勉強会のconnpassページにリンクがあるので、ご興味ある方はそちらをどうぞ。

懇親会では各自のレガシーどうよみたいな話をできて、「みんな大変そう」という圧倒的に深い考察を得ました。おぢさんたちが和室に集ってレガシーシステムを追悼する様子は、まさに法事そのものでした。

余談

本題です。

勉強会が金曜だったので、翌日は京都をぶらぶらし、もう一泊してから帰りました。京都オヒス近辺(徒歩5分圏内)でお店を発掘しました。オススメ3選を共有します。使命感の為せる技です。

  • Weekenders Coffee : コーヒースタンド。パーキングの奥のほうに茶室みたいな外見の建物があります。自分が学生だったころは元田中にあったのが、移転したらしいです。
  • Tato : スペイン料理。ビールがたくさんあって、スペインの家庭料理が激ウマです。狭めの店内に、スタンディング席とテーブル席があります。店主が異常に陽気です。
  • OIL : バー。雑居ビルの奥の奥にあって心理障壁がすごいです。中は意外と広くてゆっくりできます。メニューがないわりに良心的なお値段です。

f:id:kyabatalian:20190302183524j:plain
Tatoで食べたパエリア

そのほか、高倉二条のラーメン、わたなべ横丁とかキセノンという定番立ち呑みから、翌日サウナの梅湯築地など、サブカル定番コースでまったりしました。

まとめ

なにも予定がなく、なにもやりたくないとき、ひとまず川にいける京都。最高。

『レガシーソフトウェア改善ガイド』を読んだ

本を読むと何かをやった気になってしまって良くないです。でも、新たな知識を得るのは楽しいです。娯楽だと思うことにします。

どんな本?

レガシーなソフトウェアを改善するためのガイドです。たぶん。

www.shoeisha.co.jp

著者はThe GuardianのSenior DeveloperであるChris Birchall氏。訳者の吉川邦夫さんという方は、元SEで現在は翻訳を主に手がけられているっぽいです。翻訳リストをみると心なしかMS周りの技術書に多く関わられているっぽいです。

おなじ翔泳社から出ているレガシーコード改善ガイドとシリーズ物かと思いきや、全然別物です*1

なぜ読んだのか

勤務先の技術ブログに、レガシーシステムの改善に関する記事を書きました。その中で本書を紹介していたのですが、その段階でまったく読んでいませんでした。まったく読んでいない本を紹介することに対し、一定の罪悪感が拭えなかったため、渋々読むことにしました。

上記の記事で書いたこと以外にもいわゆる技術的負債と言われる領域に手をつけていく中で、もうちょっとまじめに取り組んだほうがいいよなあ、という思いが強くなってきました。やってきたことやより大きな技術的負債に対してどう立ち向かっていくべきか、指針を得られればと思いました。また、前述の『レガシーコード改善ガイド』ではなくソフトウェアのほうを選んだのは、コードベースを改善するにはまずそれ以前にソフトウェア全体やその開発プロセスの土台が必要と感じているからです。富士山よりも高い意識。

感想など

3部構成になっており、それぞれ「なにが課題なのか」「どうやって解決するか」「そのための環境づくり」について言及されているように感じました。

まず、ソフトウェアの実装に関することは想像以上に、というかほぼ書かれていません。レガシーは人間から生まれるので、必然的にそうなるのかもしれません。ビッグリライトはやめとけ(意訳)とか、マイクロサービスに走るな(意訳)とか、安易にレガシーを正す主張を並べるのではなく、理路整然と価値ある判断ができるように導いてくれています。レガシーソフトウェアと対峙し感情が荒ぶりがちなエンジニアは、一度エディタを閉じて深呼吸し、帰宅して本書を一読することをおすすめします。

具体的なレガシーソフトウェアの例を示し、改善に至るまでの考え方をストーリー仕立て紹介されている箇所があり、大変わかりやすいです。「何がダメで、どうしたいのか、そうすると何が嬉しいのか」を、十分すぎるくらいに説明してくれます。知識を詰め込んだ技術書というよりは、読者にとって読みやすいように配慮されていると感じます。

最後の第3部では主に自動化を主眼に、いかにレガシーを生み出さないかに言及されています。その中で、Ansible、Vagrant、Jenkinsといった具体的なツールを使った環境整備が書かれています。このへんは疲れてきてあんまり読んでません。すみません。個人的には本書は第2部までで十分その価値を持つ、第3部は必要な人だけ読んだらいいという印象です。直近のお仕事でCIをがんばりたい所存なので、まあ、がんばろうという決意を新たにしました。

本書を通じて私は「レガシー」(legacy)を、何か悪い言葉のように扱ってきたが、そうする必要はない。望むと望まないとに関わらず、我々は次世代の開発者に遺産(legacy)を残すのだから、できるだけのことをして、誇らしい遺産にしよう。

やっていきましょう。以下ではときめいた箇所とそれぞれ思ふことをメモっておきます。

「負債」には「利子」がつく

技術的負債がやっかいなのは、ただ存在することがデメリットであることです。テストがなく仕様が曖昧なコードは、その周辺の挙動を知るために読み解く手間がかかります。また、設計が使用に沿わずレガシーとなったデータベーススキーマが存在すれば、それに適応するためにアプリケーションコードが必要以上に複雑になり、新たなレガシーを生み出します。レガシーなソフトウェアを放置すると、エンジニアや組織は利子を払い続けることになります。その意味において、技術的「負債」という言葉は、事象のメタファーとして非常に優れていると感じました。

レガシーカルチャをリファクタする

レガシー「ソフトウェア」というタイトルから、レガシーコードよりは広い領域だろうな、という想像をしていましたが、本書ではさらに開発プロセスや組織のカルチャにまで言及されています。レガシーなカルチャとして、「変化への畏怖」と「知識の孤立」を挙げています。それぞれについて、個別に取りうる方策を説明しています。使われていないはずのコードを消す恐怖を和らげるためにはアクセスログをみればいいですし、いま記録されていないならまずログを出すところが第一歩です。

本書では言及されていませんが、自分の経験からいうと、技術的負債の返却に対して、組織的・長期的なメリットだけでなく、個人のインセンティブとリスクを取れる文化が必要と感じます。新規開発のプロジェクトと比較して、ユーザよりも開発者にとっての課題のため、それに取り組むモチベーションが内発されにくいです。組織における評価者がソフトウェアの状況を理解し、長期的な価値をふまえた高度な評価をする必要があります。

まずは触ってみるしかない

この「未知への恐れ」を克服する最良の方法は、そのコードに飛び込んで、いじり始めることだ。

レガシーなソフトウェアをみた人間に内発されやすい感情として、「恐怖」と「フラストレーション」が挙げられています。自分の経験とも一致します。これらが開発効率やソフトウェアの品質にもたらす悪影響を評価することは極めて難しいですし、少なくとも定量化は困難と思われます。少なくともいい状態ではないのでなんとかしたいわけですが、なぜ自分が怖かったりイライラしたりするかと考えてみると、「知らないから」というのが多分にあります。なので、そのコードを読んで、何をやっているかを把握することが、解消に繋がります。

そのために本書では、「調査的リファクタリング」が紹介されています。何をやっているかを把握しながら、簡単なリファクタを進めていく、という手法です。人間の心理として調査自体は何かを生産している気にならずコストにみえますが、そのついでに少しでもソフトウェアを改善することで、ある意味で正当化できます。例えば、もう使っていないメソッドや参照を削除することでも、それが残り続けることに比べれば大きな前進です。まずは、小さなリファクタを始めましょう。使っていないクラス、メソッド、変数、参照をみつけたら、削除しましょう。

レガシーの源泉を紐解く

結局、最初は「醜悪な実装」に見えたコードが、実は「複雑な仕様」だった、という場合が多く、それについては、大きな改善が望めない。

複雑なコードが存在するとき、それが必要な複雑さなのかは考える必要があります。そもそもの仕様が複雑ならその実装が(エンジニアにとって)複雑であっても、妥当な複雑さです。さらに一段上のレイヤを考えて、実現したいことが複雑ならその仕様の複雑さも妥当です。ただし、ほとんどの場合、過剰な複雑さだと思います。その場合は、要件の掘り下げが足りていない可能性があります。その場合、リファクタすべきはコードではなく仕様です。

段階的にできるものはそうする

モノリスなアーキテクチャを運用するチームは、世の潮流と自身のギャップを感じ、「マイクロサービスにするぞ!」と士気が高まりがちです。しかしその間はとんでもない距離があり、いざやるとなると不相応にしんどいです。まずはフロントエンドとバックエンドを切りわける、その次にSOAに取り組む、その上でさらに必要ならマイクロサービスを検討する、というふうに進めていけるはずですし、ほとんどのケースでマイクロサービスまでやる必要はないはずです。「マイクロサービス化」を目的としてしまうと、本来の課題が解決できない恐れがあります。

また別の観点として、機能の改善とリファクタリングは切り分けるのが定石です。機能をなおしてからリファクタするのか、リファクタしてから機能をなおすのか、どちらになるかはプロジェクトの性質に依存しますが、いずれにしても同時にやるべきでないことは確かです。

すべてはユーザのため

最も重要なのは(コードについて言えることの、どれよりも、はるかに重要なのは)ユーザーにとって価値のあるソフトウェアを作ることだ。

技術的負債が負債たりうるのは、ユーザに害があるからです。エンジニアがストレスを感じ、必要以上に時間を浪費し、開発速度が落ちて新たな価値を届ける速度が落ちます。また、バグの温床にもなります。ユーザのために技術的負債を返却せねば。

*1:原著のタイトルはそれぞれ"Working Effectively With Legacy Code"と"Re-engineering Legacy Software"であり、出版社も異なる

PostgreSQLアンカンファレンスに参加した

あと発表もしました。ので日記を書きます。

pgunconf.connpass.com

日記

人の話をまったく聞けない人間なので、勉強会は基本的に発表目的あるいは発表ドリブン学習目的で参加することにしています。ただ、今回は諸々が立て込んでおり発表を諦めていました。

PostgreSQLアンカンファレンスは、「会場の入口に空のタイムテーブルが張り出され、参加者がその枠内に付箋で発表内容を貼り付ける」というスタイルでコンテンツが決まっていきます。

参加者の割に枠がけっこう空いていたのと、まあせっかく来たしっていうのと、いまから自分を追い込んだら発表までいけるのでは?というゲーマー精神により、とりあえず付箋を貼っつけました。

そこから2時間くらいで資料を作成しました*1。内容は、昨年のPostgreSQLアドベントカレンダーに書いた以下の記事をベースに、宣伝とかを散りばめました。

kyabatalian.hatenablog.com

ディスプレイケーブルの不調などありつつも(手間取ってごめんなさい)、なんとか発表は無事に終わりました。強いエンジニアの方々からご質問やご意見をいくつもいただきました。PostgreSQLの実装にまで食い込んだ議論になり、楽しかったです。ありがとうございました。

最後のセッションでは、参加者がこれまでに遭遇したクソ趣のあるクエリを共有してみんなで解決策を考える、という会が催されていました。アンカンファレンスとは別かもしれませんが、これはこれで独立した会としてあってもおもしろそうと思いました。

終了後には懇親会があったのですが、持ち前のコミュ障を遺憾なく発揮して、普通に帰宅しました。次回もしあればがんばって参加できるようにコミュ力を鍛えておきます。

ふりかえり

勢いで発表してよかったです。人前で話すのが苦手なのでよい経験になりましたし、強いエンジニアの方々と議論もできました。発表のハードルを下げることにも寄与できたと思います。

一方で、やっぱりちゃんと準備せなアカンと思いました。まず前半はスライドづくりに必死で、他の方の発表をまともに聞けていないです。すみません。また、資料の推敲や発表練習などもできておらず、全体的に荒くなってしまっていたと思います。内容としても勉強不足を感じました。発表するにしても目的とそれに応じた準備期間を設計すべきでした。承認欲求と人生に対する焦りからか、アウトプットに意識が寄りすぎていました。勉強します。

発表を聞いてくれた方、コメントしてくれた方、運営のみなさま、ありがとうございました!!!

*1:あえて公開はナシで

『エンジニアの知的生産術』を読んだ

意識高そうな本を読んで意識が高まったので書いています。たぶん寝て起きたらもとに戻っています。

どんな本か

gihyo.jp

仕事をするうえで,どのように学び,整理し,アウトプットするのか。ソフトウェアエンジニア向けに,プログラミングと執筆を具体例として,知的生産の方法を解説した書籍です。

とのこと。著者は西尾泰和さんという方で、現在はサイボウズ・ラボにお勤めで、コーディングを支える技術の著者でもあります。平たく言ってめっちゃデキる人間のようです。昨年8月に発売され、かなり話題になっていました。

なぜ読んだか

知的な生産性をあげたいからです。自分はオッサンに分類される年齢ですが、エンジニアを名乗るにはあまりに能力が足りないと自覚しており、やるべきこと、やりたいことが山積みです。それらを愚直にやっていくには人生が足りなさすぎますし、若者たちに淘汰されて終了しそうです。本書は特にエンジニア向けに書かれているとのことで、目次をチラ見してこれはイイゾとなったので読みました。

読んでどうだったか

良かったです。どこかでみたことあるような…という内容も多い気がするものの、ある程度学術的に意味のあるリファレンスと紐付いて各手法が紹介されています。また、著者が本書を書くにあたって実践した内容もあわせて紹介されており、読みやすく有益な内容にまとまっています。とはいえ、これらを実践していかないと意味がないです。ついついこういう意識高い本を読んで満足しがちなダメ人間なので、今回こそ実践していきたい気持ちでいっぱいです。

以下、印象に残った箇所のメモや感想など。

やる気のでるタスクを設計する

知的生産性の研究者である著者が、学習のサイクルを回す原動力を「やる気」というエモい言葉で表現しているのが印象的でした。その上で、脳に報酬を与えてやる気を維持するタスク設計について理路整然と書かれています。

明確な目標設定が重要です。例えば「Pythonをマスターする」とか「DDDを理解する」というのは曖昧な目標であり、やる気を阻害します。仕事など必要に迫られるのがベストですが、そうではない場合にも「ブログを書く」「人に説明する」など、アウトプットという形で達成したか否かが計測可能な目標を設定します。ここで、目標自体が大きい場合、さらに分割します。例えば、「○○という本を読んで得た知識をブログに書く」という目標の前段階として、「得た知識を付箋に100枚書き出す」という過程の目標を置きます。これがいいのは、タスクが小さいことと中断可能なことです。このように、やる気を維持するには、「達成したいこと」に対して、闇雲に向かうのではなく、タスク設計をちゃんとやる必要があります。

自分がよくやってしまうのは、「○○を検討する」とか「○○を整理する」みたいな、達成点が曖昧なことをぼんやり頭に残っている状態です。信じられないくらい進捗が無になります。時間で区切ったり、だれかに説明したり、社内共有のドキュメントにまとめたり、なるべく具体化する作業をやるべきです。頭ではわかっていますが、それ自体がだるかったりします。ちゃんとやります。

全体像を把握する

技術書を普通に通読するとかなりの時間がかかります。そこでボトルネックとなっているのは、ページ送りや目の動きではなく、脳の理解であり、さらには「理解の組み立て」です。この組み立てを効率よく行うために、まず目次を把握するという方法や、ページをパラパラと読んでキーワードを抜き出す、など、全体を把握する読み方が有効だとしています。これはコードを読む場合も同じで、READMEやドキュメントを読み、ディレクトリ構造を把握してからコードを読んだほうが効率的です。自分も本に応じて、また読書の目的に応じて、読み方を変えたいです。すぐ通読しようとしてしまいます。グッと堪えます。

自分は小~中学生のころけっこう本を読む人間でしたし、その後も主に小説を好んで読んでいたりして、通読するクセみたいなものがついている気がします。あと、脳が負荷を避けているのもありそうです。目次を読んで頭に入れたり内容を想像するのは、トータルでみると効率を上げるはずですが、思考疲れというか、文字を追うだけの読書に逃げている感覚です。その意味で、学習のための読書はかなり消耗することを自覚し、ちゃんと休みましょう。

いったんぜんぶ書き出す

「GTD」というメソッドが紹介されています。この文字列はどこかでみたことがあったのですが、内容は初めて知りました。Getting Things Doneです。「やる気が出ない人の65%はタスクを1つに絞れていない」らしいです。わかる。人間の脳はマルチタスクができないように作られていないみたいですし、自分は特にそうです。コンテキストスイッチは想像以上に消耗します。タスクを絞るためには全体を把握します。

GTDはDavid Allenというひとが開発したメソッドで、なんと日本版公式サイトもあります。概要はそちらをどうぞ。超絶雑にいうと、「いま気になっていることを全部書きだし、それを上から順番にやっていく」です。一般的なタスク管理と違うのは、「タスクかどうかにかかわらず気になることを全部書く」という点と、「優先順位をつけない」という点です。それら自体が重い作業なので、放棄する、というものです。

タスク管理はそれ自体が目的になって、きちんとやりたくなってしまいますが、そこにかけるコストを自覚してバランスをとっていきたいです。タスク管理をあまりがんばらないようにします。仕事ではいまはAsanaを使っています。一日の頭にタスクを整理する時間をとっています。この前段階としてGTDをやる時間をとってもいいかもと思いました。

不確かなときは楽観的に

GTDで優先順位をつけないとはいえ、現実にはそれぞれのタスクには時間的な制約があり、優先順位をつけざるを得ない場面がほとんどです。優先順位付けというタスクが重いのは、不確定要素のためです。「不確かなときは楽観的に」というのは、ポジティブバンザイ脳ではなく、ちゃんとした根拠があります。強化学習の領域だと「探索と利用のトレードオフ」と言われたりしますが、超絶雑には以下のとおりで、2つの勘違いは対称ではない(悲観的な勘違いのほうが深刻)ということです。

  • 楽観的な勘違い:実際に行動するため、結果をみて悪い選択だと気づける
  • 悲観的な勘違い:その選択をしないため、結果が得られず悪い選択だと気づけない

とはいえ、不確かさがなるべくない状態での判断が一番確実で負荷も少ないため、なるべく不確かさを減らす努力もするべきかもしれません。

記憶のために知識を使う

人間の脳の記憶は、よくコンピュータの記憶媒体に例えられるためか、「ファイルを保存」みたいなイメージを持ちがちです。ところが実際はむしろ、筋力トレーニングに近いらしいです。つまり、単一の知識についてインプットとアウトプットを複数回おこなうことにより、記憶が定着していきます。

人間の脳に海馬という部位があります。この中にあるニューロンという神経細胞で起こる、長期増強という現象が、記憶を司っていると言われています。その内容についてはWikipediaとかをみればいいとおもいます。重要なのは、「記憶は繰り返し使うことで増強される」ということです。とにかくアウトプットです。復習のためのテストを自分で作りそれを解くことをオススメされています。AdaBoost的な発想です。

モチベーション的にも記憶的にも、「必要なことを学ぶ」と、必然的にアウトプットするので、学習の効率がよさそうです。遠い将来を追いかけるよりもまず目先の課題と向き合おうと思いました。ごめんなさい。必要に応じて学んだ知識を体系化してまとめておくことも自分には必要だと思いました。本書でいうところの「復習のための教材をつくる」的なことです。本を読んだり、技術的な何かをやったら、咀嚼・言語化してブログに書きます。自分のために。

手作りで温もりのあるMSBuildプロジェクト

あけましておめでとうございます。

最小限のMSBuildプロジェクトファイルをテキストエディタで書いてみる回です。本当に知りたい方はこれを読むより公式ドキュメントのチュートリアルを読んでもらうのが一番いいです。おつかれさまでした。

MSBuildとは

The Microsoft Build Engine is a platform for building applications. This engine, which is also known as MSBuild, provides an XML schema for a project file that controls how the build platform processes and builds software. Visual Studio uses MSBuild, but it doesn't depend on Visual Studio. By invoking msbuild.exe on your project or solution file, you can orchestrate and build products in environments where Visual Studio isn't installed.

MSBuild - Visual Studio | Microsoft Docs

ということで、アプリケーションをビルドするためのMicrosoft製のエンジンです。.NET Framework 2.0から同梱されていて、それまで使われていたNMakeの.NET Framework版みたいなもんです*1

.NET Framework 1系までは、Visual Studioでのビルドは、IDE組み込みのdevenv.exeがビルドしている一方、コマンドラインからのビルドはcsc.exeやvbc.exeを直接使っていました。コンパイラオプションを指定するのが大変だったり、ビルド以外の処理が複雑でした。これらをまとめてスクリプト化したのがMSBuild及びプロジェクトファイルです。

例えば、Visual Studioで作ったプロジェクトをJenkinsなどのCIツールでもビルドしたいとき、そのままプロジェクトファイル(.csprojとか)を入力にしてMSBuild.exeを実行すれば同様の成果物が生成されます。便利ですね。

Why 手作り

前述のとおり、通常?はVisual Studio様がご創造なされますが、細々とした設定をしたいときや、ビルド環境によって切り替えたいとき、またコンソール実行で設定を渡したいときなど、けっこう触る機会があります。少しでも手を出そうとすると、結局MSBuildが何をするかを把握しておかないといけません。こういう場合、往々にして既存のプロジェクトファイルをいじるところから始めてしまって結局無為な時間を過ごしてしまいがちです。でした。私が。

主にプロパティ、項目、ターゲット、タスクという独特な概念を理解する必要があります。ドキュメントを読んで理解できるならそれで十分ですが、実際に動かすのが一番だと思います。のでやってみた次第です。

Let's 手作り

あえてテキストファイルでプロジェクトファイルを作ってみます。MSBuildでビルドする最小限からはじめて、少し遊んでみます。OSはmac OS Mojave 10.14、MSBuildは15.6.0.0ですが、超基本的なことしかやってないので、各自の環境でも再現できると思います。

最小構成でMSBuild

最小限のC#のコードを書きます。ファイル名を helloworld.cs とします。

using System;

class HelloWorld  
{  
    static void Main()  
    {
        Console.WriteLine("Hello, world!");  
    }
}

最小限のプロジェクトファイルを書きます。ファイル名は Helloworld.proj とします。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <Compile Include="helloworld.cs" />
  </ItemGroup>
  <Target Name="Build">
    <Csc Sources="@(Compile)"/>
  </Target>
</Project>

XMLで記述します。根本は <Project> 要素です。 <ItemGroup> 要素は、MSBuildへの入力となるファイルを指定します。ここでは helloworld.cs というファイルを、Compileという種類のItem(=項目)にIncludeしています。 <Target> 要素は、MSBuildが順次実行する処理(タスク)をまとめたものです。Targetの名前をBuildとしています。BuildというTargetを実行すると、配下のタスクが実行されます。ここでは、 Cscタスクに対してCompileというItemを渡しており、ItemGroupで指定したhelloworld.csのみが渡されます。Cscタスクはcsc.exeをラップしており、実際のビルド処理はこのタスクが行います。

この状態で、MSBuildを実行します。 -t スイッチでTargetを指定します。

$ msbuild Helloworld.csproj -t:Build
Microsoft (R) Build Engine version 15.6.0.0 (xplat-master/ca830585 Sun Mar 25 19:24:09 EDT 2018) for Mono
Copyright (C) Microsoft Corporation. All rights reserved.

Build started 1/5/2019 9:50:17 PM.
Project "/Dev/msbuild/HelloWorld/Helloworld.csproj" on node 1 (Build target(s)).
Build:
  /Library/Frameworks/Mono.framework/Versions/5.10.1/lib/mono/msbuild/15.0/bin/Roslyn/csc.exe /out:helloworld.exe helloworld.cs
Done Building Project "/Dev/msbuild/HelloWorld/Helloworld.csproj" (Build target(s)).

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.01

これでhelloworld.exeが出力され、実行できます。これで最低限のビルドができます。Visual Studioが自動生成するプロジェクトファイルに比べるとかなりシンプルです。

ビルドプロパティの追加

最低限のビルドはできるようになったので、もう少し細かい制御をしてみます。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>  
   <AssemblyName>MSBuildSample</AssemblyName>  
    <OutputPath>Bin\</OutputPath>  
  </PropertyGroup>  
  <ItemGroup>
    <Compile Include="helloworld.cs" />
  </ItemGroup>
  <Target Name="Build">
    <MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />
    <Csc Sources="@(Compile)" OutputAssembly="$(OutputPath)$(AssemblyName).exe" />
  </Target>
</Project>

<PropertyGroup> 要素は各種のプロパティを設定します。ここでは、出力するアセンブリ名と出力先のディレクトリを指定しています。合わせてBuildターゲットにおいて、Cscタスクの前に出力ディレクトリの作成と、Cscタスクにおいて出力アセンブリ名を指定しています。

この状態でMSBuildを実行すると、 Binディレクトリが作成され、その配下にMSBuildSample.exeが作成されます。

ビルドターゲットの追加

Build以外に、CleanとRebuildというターゲットを追加します。また、プロジェクトのデフォルトのターゲットを指定します。

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <AssemblyName>MSBuildSample</AssemblyName>  
    <OutputPath>Bin\</OutputPath>  
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="helloworld.cs" />  
  </ItemGroup>
  <Target Name="Build">
    <MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />
    <Csc Sources="@(Compile)" OutputAssembly="$(OutputPath)$(AssemblyName).exe" />
  </Target>
  <Target Name="Clean" >  
    <Delete Files="$(OutputPath)$(AssemblyName).exe" />  
  </Target>
  <Target Name="Rebuild" DependsOnTargets="Clean;Build" />  
</Project>

Cleanターゲットでは、Deleteタスクを用い、Buildターゲットで生成したアセンブリを削除します。RebuildターゲットはCleanとBuildのターゲットをこの順序で実行します。

この状態でCleanターゲットを実行してみます。

$ msbuild Helloworld.csproj -t:Clean
Microsoft (R) Build Engine version 15.6.0.0 (xplat-master/ca830585 Sun Mar 25 19:24:09 EDT 2018) for Mono
Copyright (C) Microsoft Corporation. All rights reserved.

Build started 1/5/2019 10:21:19 PM.
Project "/Dev/msbuild/HelloWorld/Helloworld.csproj" on node 1 (Clean target(s)).
Clean:
  Deleting file "Bin/MSBuildSample.exe".
Done Building Project "/Dev/msbuild/HelloWorld/Helloworld.csproj" (Clean target(s)).

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.42

Bin下のMSBuildSample.exeが削除されています。また、MSBuildを-tスイッチなしで実行すると再度MSBuildSample.exeが作成されます。

インクリメンタルビルド

ターゲットが依存しているファイルのタイムスタンプを見て、変更があった場合にのみビルドします。BuildターゲットにInputsとOutputsを指定します。

  <Target Name="Build" Inputs="@(Compile)" Outputs="$(OutputPath)$(AssemblyName).exe">>
    <MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />
    <Csc Sources="@(Compile)" OutputAssembly="$(OutputPath)$(AssemblyName).exe" />
  </Target>

この状態で msbuild -v:d を実行します。ビルドの詳細を出力するスイッチをしています。

Skipping target "Build" because all output files are up-to-date with respect to the input files.
Input files: helloworld.cs
Output files: Bin/MSBuildSample.exe

入力ファイルhelloworld.csに変更がないので、Buildターゲットがスキップされています。

ビルドしない

ここまでくるとわかるかと思いますが、上記のプロジェクトにおいて、実際にビルドを実行しているのは結局Cscタスクであり、その前後は付随する処理でしかありません。つまり、MSBuildが行うのはビルドだけでなくその前後を含めたプロセスといってよさそうです。ビルドしないMSBuildプロジェクトもありえます。

例として、まったく意味のないプロジェクトを作ってみます。

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Target Name="Greeting">
    <Message Text="Hello" Importance="low" />
    <Message Text="World" />
    <Message Text="!!!!!" Importance="high" />
  </Target>
  <Target Name="Copy">
    <Touch Files="File1.cs;File2.cs" AlwaysCreate="true" />
    <Copy SourceFiles="File1.cs" DestinationFolder="Bin\" />
  </Target>
</Project>

GreetingターゲットとCopyターゲットの2つがあります。まずGreetingターゲットは、Messageタスクを使って、文字列を出力するだけです。実行してみます。

$ msbuild NoBuild.csproj -t:Greeting -v:d
(略)
Target "Greeting" in project "/Dev/msbuild/HelloWorld/NoBuild.csproj" (entry point):
Using "Message" task from assembly "Microsoft.Build.Tasks.Core, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a".
Task "Message"
  Hello
Done executing task "Message".
Task "Message"
  World
Done executing task "Message".
Task "Message"
  !!!!!
Done executing task "Message".
Done building target "Greeting" in project "NoBuild.csproj".
Done Building Project "/Dev/msbuild/HelloWorld/NoBuild.csproj" (Greeting target(s)).
(略)

「Hello」「World」「!!!!!」が出力されています。HelloはImportanceがlowなので、 -v:dオプションがないと出力されません。

次に、Copyターゲットを実行してみます。Touchタスクを使ってFile1.csとFile2.csを作成した上で、CopyタスクでFile1.csをBin配下にコピーします。

$ msbuild NoBuild.csproj -t:Copy
(略)
Project "/Dev/msbuild/HelloWorld/NoBuild.csproj" on node 1 (Copy target(s)).
Copy:
  Touching "File1.cs".
  Touching "File2.cs".
  Copying file from "File1.cs" to "Bin/File1.cs".
Done Building Project "/Dev/msbuild/HelloWorld/NoBuild.csproj" (COpy target(s)).
(略)

まとめ

MSBuildプロジェクトファイルを0から手で書いてみました。ほとんど公式チュートリアルの丸パクリです。本年度のブログのハードルを下げていく方針なので大成功です。

Visual Studioを使って普通に開発を進めていくと気づいたらプロジェクトファイルが巨大になっていて面くらいがちです。でもやっていることは単純なことがわかります。基本的に手で書くものではないですが、上記内容くらいは知っておいて損はない気がします。他にもいろいろなタスクがあったり、条件指定ができたり、細かい制御ができます。そのあたりはぜひリファレンスを参照ください。

*1:MSBuildの開発コード名は「XMake」でした

2018年の振り返り

帰省中です。雪だし寒いしもうダメです。

2018年の振り返り

技術ブログ

このブログだと、2018年は技術関係のエントリが5本だけでした。残念の極みです。2019年は5000本くらい書きたいです。また、勤務先の公式ブログに記事を書きました*1。はてブのホットエントリになった、ということを聞きました。そこそこ褒めてもらえたので、たぶんいいことなのだと思います。

勉強会

技術勉強会は発表側にならないと何も学ばず時間を浪費してしまう自覚があるので、だいたい発表前提で参加します。2018年は勉強会自体にほとんど参加しませんでした。ただ、こちらも初めて勤務先オフィシャルな場で社員として登壇しました*2。中身は課題だらけ過ぎて忘れたいです。

技術書典

ノリで応募して勉強しながら書きすすめましたが、結果的に楽しく充実した体験ができました。BOOTHにも出品しており、現時点で126冊のご購入をいただいております。ありがとうございます。次回は春頃に開催されるはずです。一般参加よりもサークル参加を強くおすすめします*3

kyabatalian.hatenablog.com

扁桃切除手術

扁桃切除の手術をうけるため、10日間の入院をしました。以降、扁桃炎にはなっていないのですが、手術による改善はまあそれほど体感できていません。食べ物が飲み込みやすくなったような気がします。入院中にキングダムを読破し、中華統一への思いが高まりました。

kyabatalian.hatenablog.com

旅行

宿泊を伴う観光でいうと、京都と石川に行きました。京都へはGWと夏(祇園祭の直前頃)の2回行きました。常に最高です。石川は和倉温泉に行きました。技術書典の前日かつ台風が来ていて怖かったです。こちらも最高でした。2019年は海外に行きたいです。

kyabatalian.hatenablog.com

お笑い

趣味です。2018年は主にド地下のライブに足繁く通っていたのですが、2019年はわりとメジャーどころも観に行くようになりました。推しの単独ライブ、最高でした*4。直接みにいったライブに限らず、キングオブコントやM-1グランプリも楽しみました。2018年の私的ベストは、5月にルミネでみた、かまいたちの漫才でした。異次元の面白さでした。

kyabatalian.hatenablog.com

人生

このブログを書くにあたり、1年間のツイートをさかのぼってみたところ、前半はかなりつらそうでした。お仕事がしんとかったのか、何かあったのだと思うのですが、まったく思い出せないので、人生そんなものなのかもしれません。後半は今後の人生について考える機運が高まり、それはそれでハードでした。2019年は行動にうつしていきたいです。

2019年の抱負

技術をちゃんとやります。自分がもし第三者だったらいまの自分をみて「いいから黙ってコード書けよ」と言いたくなりそうなので黙ってコード書きます。

そのためには健康第一です。来年もよろしくおねがいします。良いお年を。

拡張統計情報とテーブル結合

PostgreSQL Advent Calendar 2018、23日目の記事です。

今更感満載ですが、PostgreSQL 10で導入された拡張統計情報について、直感と異なる挙動だったので調査してみました。小ネタですみません。

TL;DR

  • PostgresSQLのオプティマイザは、カラム間の関数従属性を考慮しない
  • 拡張統計情報を使えば、テーブル探索で関数従属性を考慮できる
  • テーブル結合では考慮されない

行数推定の課題

PostgreSQLのオプティマイザはコストベースであり、アクセスパスツリーを生成して最もコストの低いパスを選択します。

このコスト計算において、条件句が複雑な場合に、実体と沿わないコストを算出してしまう場合があります。具体的には、条件句に AND で複数の条件が指定され、かつ両条件に指定されたカラム間に関数従属性がある場合、実際の行数よりも推定行数が少なく見積もられます。

この時点で???となった方は、ググるか、恐縮ながら以下をさらっとみてもらえるといいかもしれません。

PostgreSQL:行数推定を読み解く/row-estimation - Speaker Deck

拡張統計情報

任意のテーブルがもつカラム間の相関を取得し、オプティマイザに使わせる事ができる機能です。PostgreSQLのバージョン10で導入されました。

以下ではまず、この機能がどういうものか確認してみます。公式ドキュメントの例を使います。

-- テスト用のテーブルt1を作成する
CREATE TABLE t1 (a int, b int);

-- t1にデータを挿入する
INSERT INTO t1
  SELECT i/100, i/500
  FROM generate_series(1,1000000) s(i);

こうすることにより、t1のデータは以下のようになります。

i a b
1 0 0
2 0 0
3 0 0
99 0 0
100 1 0
101 1 0
102 1 0
199 1 0
200 2 0
201 2 0
499 4 0
500 5 1
501 5 1
999 9 1
1000 10 2
1001 10 2

つまり、というかSQLのとおりなんですが、aは100ごとに、bは500ごとに値がインクリメントされます。例えばaが12とわかればbは2と確定しますし、逆にbが2とわかればaは10以上20未満であるとわかります。このとき、カラムaとカラムbは相互に関数従属であるといいます。

この状態で、カラムa、bの両方に対する条件句をもつSQLの実行計画をみてみます。

-- 統計情報を収集する
ANALYZE t1;

-- 実行計画を取得する
EXPLAIN ANALYZE SELECT * FROM t1 WHERE (a = 1) AND (b = 0);
Seq Scan on t1  (cost=0.00..19425.00 rows=1 width=8) (actual time=0.034..113.435 rows=100 loops=1)
  Filter: ((a = 1) AND (b = 0))
  Rows Removed by Filter: 999900
Planning time: 0.155 ms
Execution time: 113.467 ms

t1に対するSeq Scanの実際の結果は100行にもかかわらず、推定行数が1行になっています。これは、カラムaとbの間の関数従属性が考慮されていないためです。つまり、各条件句を独立とみなし、それぞれ別のSQLで取得した結果の和集合としています。

この問題に対する解決策として、拡張統計情報があります。ためしてみます。

-- 拡張統計情報を作成する
CREATE STATISTICS s1 (dependencies) ON a, b FROM t1;

-- 統計情報を収集する
ANALYZE t1;

-- 実行計画を取得する
EXPLAIN ANALYZE SELECT * FROM t1 WHERE (a = 1) AND (b = 0);
Seq Scan on t1  (cost=0.00..19425.00 rows=98 width=8) (actual time=0.020..89.729 rows=100 loops=1)
  Filter: ((a = 1) AND (b = 0))
  Rows Removed by Filter: 999900
Planning time: 0.083 ms
Execution time: 89.750 ms

今度はカラム間の関数従属性が考慮され、行数は正確に見積もられています。

テーブル結合

本題、テーブル結合です。t1と全く同様の方法で、t2とt3という2つのテーブルを作り、これらをクロス結合して実行計画をみてみます。

CREATE TABLE t2 (a2 int, b2 int);
CREATE TABLE t3 (a3 int, b3 int);

INSERT INTO t2 SELECT i/100, i/500 FROM generate_series(1,1000000) s(i);
INSERT INTO t3 SELECT i/100, i/500 FROM generate_series(1,1000000) s(i);

単一条件の場合

まず、t2に対する条件句が単一の場合の実行計画を見てみます。

EXPLAIN ANALYZE SELECT * FROM t2, t3 WHERE (t2.a2 = 1) AND (t2.b2 = t3.b3);
Hash Join  (cost=30832.00..52282.01 rows=48922 width=16) (actual time=614.425..622.979 rows=49900 loops=1)
  Hash Cond: (t2.b2 = t3.b3)
  ->  Seq Scan on t2  (cost=0.00..16925.00 rows=98 width=8) (actual time=0.030..85.213 rows=100 loops=1)
        Filter: (a2 = 1)
        Rows Removed by Filter: 999900
  ->  Hash  (cost=14425.00..14425.00 rows=1000000 width=8) (actual time=475.964..475.965 rows=1000000 loops=1)
        Buckets: 131072  Batches: 16  Memory Usage: 3334kB
        ->  Seq Scan on t3  (cost=0.00..14425.00 rows=1000000 width=8) (actual time=0.024..146.287 rows=1000000 loops=1)
Planning time: 0.331 ms
Execution time: 626.888 ms

t2に対するSeq Scanの推定行数が98となっており、実際には100なので、乖離はありません。結合条件としてはHash Joinが選択されています。

複合条件の場合(拡張統計情報なし)

テーブルt2のカラムa2、b2それぞれに対して条件句を指定します。関数従属性により、実行計画が狂ってしまうのを確認します。

EXPLAIN ANALYZE SELECT * FROM t2, t3 WHERE (t2.a2 = 1) AND (t2.b2 = 0) AND (t2.b2 = t3.b3);
Nested Loop  (cost=0.00..36354.86 rows=486 width=16) (actual time=0.056..8576.923 rows=49900 loops=1)
  ->  Seq Scan on t2  (cost=0.00..19425.00 rows=1 width=8) (actual time=0.032..79.398 rows=100 loops=1)
        Filter: ((a2 = 1) AND (b2 = 0))
        Rows Removed by Filter: 999900
  ->  Seq Scan on t3  (cost=0.00..16925.00 rows=486 width=8) (actual time=0.007..84.883 rows=499 loops=100)
        Filter: (b3 = 0)
        Rows Removed by Filter: 999501
Planning time: 0.113 ms
Execution time: 8580.450 ms

前項のt1と同じく、t2は推定行数が大きくずれ、Seq Scanとなっています。結合に関しては、t2が1行となる予測なので、Nested Loopが選択されています。

複合条件の場合(拡張統計情報あり)

テーブルt2のカラムa2、b2に対して、拡張統計情報を作成します。

CREATE STATISTICS s2 (dependencies) ON a2, b2 FROM t2;
ANALYZE t2;
EXPLAIN ANALYZE SELECT * FROM t2, t3 WHERE (t2.a2 = 1) AND (t2.b2 = 0) AND (t2.b2 = t3.b3);
Nested Loop  (cost=0.00..36945.60 rows=47628 width=16) (actual time=0.045..231.646 rows=49900 loops=1)
  ->  Seq Scan on t3  (cost=0.00..16925.00 rows=486 width=8) (actual time=0.019..119.208 rows=499 loops=1)
        Filter: (b3 = 0)
        Rows Removed by Filter: 999501
  ->  Materialize  (cost=0.00..19425.49 rows=98 width=8) (actual time=0.000..0.205 rows=100 loops=499)
        ->  Seq Scan on t2  (cost=0.00..19425.00 rows=98 width=8) (actual time=0.022..97.727 rows=100 loops=1)
              Filter: ((a2 = 1) AND (b2 = 0))
              Rows Removed by Filter: 999900
Planning time: 0.194 ms
Execution time: 235.587 ms

実行時間が8580msから246msに改善しました。実行計画の中身をみてみると、t2の推定行数が98になっており、実行結果の100行と僅差になっています。それにより、Materializeされるようになりました。Materializeは、t2をSeq Scanした結果をメモリにのせます。これにより、t3とのNested Loopにおいてt2の再スキャンを高速化します。

考察

拡張統計情報によって見事に実行速度が改善したわけですが、Nested Loopではなく単一条件の場合と同じくHash Joinが選択されてもよさそうです。なぜNested Loopのままなのか、PostgreSQLのコードを見てみます。

拡張統計情報を参照するのは clauselist_selectivity() の以下の箇所です。該当コードはここ

 /*
    * Determine if these clauses reference a single relation.  If so, and if
    * it has extended statistics, try to apply those.
    */
    rel = find_single_rel_for_clauses(root, clauses);
    if (rel && rel->rtekind == RTE_RELATION && rel->statlist != NIL)
    {
        /*
        * Perform selectivity estimations on any clauses found applicable by
        * dependencies_clauselist_selectivity.  'estimatedclauses' will be
        * filled with the 0-based list positions of clauses used that way, so
        * that we can ignore them below.
        */
        s1 *= dependencies_clauselist_selectivity(root, clauses, varRelid,
                                                  jointype, sjinfo, rel,
                                                  &estimatedclauses);

        /*
        * This would be the place to apply any other types of extended
        * statistics selectivity estimations for remaining clauses.
        */
    }

    /*
    * Apply normal selectivity estimates for remaining clauses. We'll be
    * careful to skip any clauses which were already estimated above.

スキャン方法の選択時、 src/backend/optimizer/path/costsize.c がこのコードを使っています。

void
set_baserel_size_estimates(PlannerInfo *root, RelOptInfo *rel)
{
    double     nrows;

    /* Should only be applied to base relations */
    Assert(rel->relid > 0);

    nrows = rel->tuples *
        clauselist_selectivity(root,
                               rel->baserestrictinfo,
                               0,
                               JOIN_INNER,
                               NULL);

一方、結合条件について。PostgreSQLのオプティマイザはデフォルトでNested Loopのパスを作り、そのあとにMerge Join、Hash Joinとそれぞれを検討して、そっちのほうが速そうならそっちに置き換えます。 このへんでHash Joinを選択するか判断しているのですが、この段階で上記コードを参照していません。つまり、結合条件の選択に、拡張統計情報はつかわれません。

改めて問題

何が困るかというと、まあそのままなのですが、「巨大なテーブル同士をNested Loopでジョインしてしまう」ことです。これについては、バージョン10以前と同様にpg_hint_planをつかって結合ルールを手動制御せざるを得なさそうです。

そんな人はいないと思いますが、「関数従属な複数カラムに対する条件句」と「テーブル結合」の両方を持つSQLがたくさんあり、pg_hint_planでがんじがらめになっているサービスを運用しており、拡張統計情報をメシア視している方がもし仮に存在するなら、要注意です。

まとめ

普通に結合条件でも考慮されてほしいと思いました。拡張統計情報という壮大な名前なので、今後さらに賢くなっていくことが期待できます。と思っていたらバージョン11では特にアップデートがなかったです。悲しいです。以降、「文句言ってないでOSSなんだから自分でなおせよ」禁止。

参考