TECH PLAY

BASE株式会社

BASE株式会社 の技術ブログ

574

この記事は、「BASE Advent Calendar 2018」の12日目の記事です。 devblog.thebase.in こんにちは、BASE BANKインターンの かわごえ( @heart_breakers2 )です。 今回は少し趣向を変えて、「鶴岡さん観察日記」と題して、BASE 代表取締役CEOおよびBASE BANK 代表取締役CEOである鶴岡の普段の様子をイラストを交えてカジュアルに伝えていきたいと思います。 (みなさんにもカジュアルな雰囲気で読んでいただきたいので、記事内ではあえて「鶴岡さん」と表記しています。) 私がインターンとしてBASE BANKにジョインし、鶴岡さんのななめ前の席に配属されてから半年が経ちました。 みなさんがきっと知らない鶴岡さんの素顔や、BASEの好きなところをまとめてみました。 この記事を読んでいただき鶴岡さんやBASEに興味を持っていただけると嬉しいです。 はじめに みなさんは、BASE代表の鶴岡さんをご存知ですか? BASEが提供するネットショップ作成サービス「BASE」は、「お母さんも使える」をコンセプトとしたネットショップを作成できるサービスで、現在は60万人を超える オーナーズ に利用されています。 鶴岡さんはBASEの創業者。 Forbes JAPANが発表した「 日本の起業家ランキング2019 」で3位に選出された、日本で最も注目されている起業家の一人です。 「60万ショップも使っているサービスを作っているんだ!すごい!」 「Forbesでも注目されているんだ!すごい!」 と思った方もいるのではないでしょうか。 私から見た鶴岡さんは、傾聴力と本質を見抜く力が圧倒的に高く、どんな人でも一緒にいて心地よい存在で社内外にファンが多いようです。 しかし、鶴岡さんが遠い存在と思われてしまっているのか、↓のようなツイートにだれもツッコめない状況になっているような気がします。 くだもの食べながら炭酸水飲んだら、ファンタになる — 鶴岡裕太 (@0Q7) 2018年11月10日 この記事では、社内での鶴岡さんはどのような様子なのか?何を大切にして事業を進めているのか? BASEの素敵な制度などもご紹介しつつ、鶴岡さんにまだお会いしたことがない方でも身近に感じていただけるよう、イラストと写真で紹介していきます💫 鶴岡さん観察日記 AM 11:00  Be Hopefulキャンディー 鶴岡さんの朝は、たいてい午前11時のミーティングで始まります。 鶴岡さんはミーティング中どんな感じなの?と気になる方も多いのではないでしょうか。 社内のミーティングの様子を覗いてみましょう。 鶴岡さんとの社内ミーティングは社長室で行われることが多く、鶴岡さんが座る席の近くにはBe Hopefulキャンディーが入ったガラス瓶が置かれています。 ミーティングではキャンディーを手に取り頬張っている鶴岡さんですが、発言はいつも本質的で、 「オーナーズのためになるどうか」 「オーナーズにとってわかりやすいかどうか」 をいつも大切にしているので、私たちもどのような判断をすべきかがクリアになります。 ちなみに、BASEの 行動指針 は Be Hopeful Move Fast Speak Openly の3つで、どれも欠かせない重要な指針となっています。 実際のBe Hopefulキャンディー。 行動指針の浸透のために作られたもので、キャンディーの中に"Be Hopeful"と書かれていてかわいいです。 キャンディーの色は、「BASE」のサービスロゴであるティピのマルチカラーを使用していて、社内のいたるところに置いてあります。 PM 0:30  みんなの食堂 先日からBASEでは、いわゆる社員食堂のサービスが毎週水曜日に提供されています。 鶴岡さんも毎週この社食を利用していて、メンバーと楽しく会話しながらランチをしています! 社食うまい pic.twitter.com/Qf3p0RwL7V — 鶴岡裕太 (@0Q7) 2018年11月15日 (鶴岡さんの写真は撮り忘れちゃいましたが、みなさんが食堂を楽しんでいる様子です💕) 現在は30食限定でいつもすぐに売り切れてしまうのですが、500円で定食を食べることができます!! (BASEは37階にあるので、ランチにいくにも時間がかかってしまうのです) ココロータス 別の日の様子です。 鶴岡さんはお昼頃になると、定期的に、大好きな神宮司さん(執行役員/Product Manager)にこう語りかけます。 ココロータス とは、アジア料理を中心に、多国籍のランチを提供するキッチンカーです。またココロータスは、お支払いアプリ「PAY ID」の加盟店で、スマホでQRコード決済が可能なのでお財布を持たなくてもランチを買うことができます。 メニューが提供されるのを今か今かと待ちわびているようです。 ココロータスのメニューはどれもボリューミーかつ1000円以内で注文できます。企業の社長はいつも豪華な食事をしているというイメージの方もいらっしゃるかもしれませんが、鶴岡さんのランチはいたって庶民的なんです。 ココロータスのキッチンカーは現在、江戸川橋と六本木三丁目で販売を行っています。六本木周辺に勤務されている方はぜひチェックしてみてくださいね! PM 3:00  カップスープ ある日出社すると、隣の机に大量のカップスープが置かれていました。 置いたのはもちろん鶴岡さん。 とのことだったので、3時のおやつにたまごスープをいただきました。 この時は特に何かのお祝いやイベント事があったわけではないですが、特に理由がなくともさらっとメンバーに差し入れをする様子を見て鶴岡さんの優しさや心遣いを感じました。まだ食べきれていないほど、大量に購入されていました! ちなみに、鶴岡さんが好きなのはコーンポタージュ。 PM 4:00  「順調?」 夕方になると、近くにいる人を中心に「順調?」と話しかけるのが鶴岡さんの癖です。 「え!?社長に進捗確認されるとかプレッシャー!!」と感じる方もいるのではないでしょうか。 でもBASEでは違います。 私から見ると、鶴岡さんの「順調?」という発言はメンバーにプレッシャーをかけたいという意図ではなく、「困ったことがあったらいつでも聞いて大丈夫だよ」という心配りなのではないかと思います。 なので、鶴岡さんに「順調?」と聞かれて正直に答えないメリットはありません。順調ならそう答えればいいし、何か詰まっている点があるなら打ち明けた方がよいです。 私自身も実際にこの言葉をかけられて、「ユーザーからの反応があまりなくて、次の施策をどうするか悩んでます!」と相談したことがあります。 なぜなら先ほども書きましたが、鶴岡さんは傾聴力が優れていて、本質的なことを見抜く力が高く、私たちの想像を超えるようなフィードバックをしてくれるからです。 BASEの行動指針に「Speak Openly」とあるように、たとえ物事が順調に進んでいない場合でも、正直に話すことで鶴岡さんから的確なフィードバックを得られるため本質を見失わずにプロダクトを作り続けることができます。 お気に入りのゴジラをつかみながら喋る鶴岡さん PM 5:00  ジャック・ドーシー氏 鶴岡さんが12/3に日経BP社によって行われたイベント「 スモールビジネス新潮流 「ポスト2020」を見据えて 」に登壇が決まった時の話です。 テーマは「スモールビジネスが秘める可能性と未来」について。Twitter、Squareの創業者であるジャックドーシー氏も参加するセッションです。 中小企業や個人で活動しているオーナーズをどのような方法で応援できるかについて、鶴岡さんの考えやBASEの取り組みを話したそうです。当日の様子が気になる方はぜひ、Twitterで「#ポスト2020」と調べてみてください! ちなみに鶴岡さんはかねてからジャックドーシー氏の大ファンで、このイベントでの登壇が決まったときは本当に嬉しそうでした。 It was great meeting you, @jack ! pic.twitter.com/EWtlZUYJ9x — 鶴岡裕太 (@0Q7) 2018年12月3日 (鶴岡さんの目がキラキラしてます!!) おわりに いかがでしたか。 鶴岡さんの普段の様子が伝わるように意識して書き上げたので、鶴岡さんの温かい人柄、飾らない素直な性格、そしてBASEの良さが伝わっていると嬉しいです。 明日は、沖中さんがカバレッジレポートについて書きます!
アバター
先日の締め会の様子 これは「BASE Advent Calendar」11日目の記事です。 devblog.thebase.in こんにちは。BASEで代表をしている鶴岡( @0Q7 )です。 11日目の記事を担当しています。 ある日、Slackでアドベントカレンダーを書く人を募集していたので、調子に乗りそれとなく書く予定にしていたら、書く内容が全く思い付かず悩みすぎたので今はすごく後悔しています。 本日12/11はBASEの設立記念日で、6年が経ちました。早いような遅いような。 ついこの前までは「今日のGMVは100万円は行きたい、そしていつかは500万くらいはいけるようなサービスにしたい。」なんて思いながら毎日帰宅していた気がしますが、その目標から桁が2つくらいは増えたので少しは成長できてるのかなと思います。 ただ何かを成し遂げたわけではないので昔話をしたいわけでもなく、とはいえエモくビジョンの話をするのもなんなので、この6年で実際に感じた学びを書こうと思います。 内容は個人的な主観ですが、誰かの役に立てれば嬉しいです。 殆どの戦いは負けても良い 社会人になって一番の学びかもしれないですが、殆どの戦いには負けても大丈夫なんだなと思いました。 戦いとは、議論や意思決定のことを指していますが、最終的に成し遂げたい目標があったときに、その途中で訪れる論点を全て自分の思い通りにする必要もないんだなと。全ての勝負に勝とうとするにはあまりにも今の人生では短いので、全部の駒を取る事が勝利なのではなく、王将を取る事が勝利なのだとあらゆる場面でゲームをしっかり理解する能力を磨きたいものです。そして王将だけは必ず取りましょう。 感情とは上手く付き合いたい 特に怒りという感情とは上手く付き合った方がいいかなと思いました。 怒りという感情を露わにする事が、物事を最短で進めるための手段になる事は殆どないです。自分の思う通りにしたければまず感情をコントロールできる人間になりたいです。 プロダクトが全て 自分たちが作っているプロダクトが全てを癒してくれます。学歴も地位も立場も関係ないです。最高のプロダクトを作ったチームが一番かっこいいです。「BASE」を使ってくれている60万のショップをみてもそう思います。最高のプロダクトを作ることが出来れば市場が評価してくれます。世の中にあった方がいいプロダクトは必ず成功すると思いますが、タイミングだけが変数です。諦めずチームで最高のプロダクトを作りましょう。 インプットは難しい 自分に合っていない情報のインプットがあると無駄な時間を使ってしまう事があります。情報がこれだけ溢れている昨今において、自分に適している情報はすごく少ないです。Twitterで流れてくる情報は自分のフェーズには早かったりしてほぼ役に立たないなと感じます。世の中で起きている事実だけを把握し、自ら考え自分が思うままに行動するのが一番かなと思います。 評論家にはなりたくない 評論するよりされる側の方がかっこいい!というのも勿論ありますが、これは評論家を批判してるわけではなく、自分のメンタルを保つためにあまりオススメしません。自分を追い込むことになる言動はなるべく慎みたいなと思います。 テクノロジーには超楽観的に テクノロジーがあらゆるものを便利にしていきます。プラットフォームがあらゆるリスクを巻き取っていきます。今不便だと感じる事は全て解決されるんだろうなと感じます。そうやって訪れる未来を幸せと呼ぶのだと信じています。 徳を積む そもそも徳が何かも分かっていないのと、どうやったら徳を積めるのか僕もよくまだ分かっていないですが、お金では買えないものである事は確かです。徳が高い人は凄いなと思います。日々の言動や所作を周りの方々に見ていただいて、自ずと積まれていくものだと思うので、常日頃から意識して生きていきたいです。必ずその徳に助けられる日が訪れると思います。 パッと思いつくところだと、こんな感じでしょうか。 こういうのを書くタイプでは無いのですごく疲れました。。。また黙々とプロダクト作りに励もうと思います。 普段は オーナーズ の為にEC・金融・決済領域でプロダクトを作っています。 もっと話聞いてみたい!BASEってどんな会社なの!会社に遊びに行ってみたい!と思っていただける方がいらっしゃればぜひ メッセ をいただければと思います。 明日はBASE BANKインターンのマリーちゃんです。 ではまた。
アバター
これは「BASE Advent Calendar」10日目の記事です。 devblog.thebase.in こんにちは。BASEで採用広報を担当している米田( @AiYoneda )です。普段はL'Arc-en-CielのHYDEさんの追っかけをしていて、来週にはきっと東京ドームでその御姿を拝んでいることでしょう・・・ さて、HYDEさんの話は置いておいて、私はテックブログこと「BASE開発チームブログ」の編集部として運営に関わっています。今回は、約8ヵ月続けてきたこの「BASE開発チームブログ」の運営についてのノウハウを余すことなく書いていこうと思います。 テックブログはエンジニア・デザイナー組織のプレゼンス向上や採用に効果があるなどと聞いているけど、なかなかうまく運営できていないという方もいらっしゃるかと思います。そういった方にこちらを読んでいただいて少しでも参考になれば幸いです。 テックブログって本当に効果あるの?約8ヵ月間、編集部として運営を続けてきた効果、成果 BASEでは2018年4月にテックブログ編集部を作り、以降テックブログの運営を担っています。下記は編集部が立ち上がって以降の効果や成果について記載したものになります。 公開記事数がめちゃくちゃ増えて30本以上公開できた 2017年:8本 ↓ 2018年4月以降:31本! (このAdvent Calendarで今年あと15本公開予定です!) 2017年は半年くらい更新していない期間もあり、かなりさみしい感じでしたが、2018年4月以降は月2本以上の更新ペースを維持できています。またこの数字はむやみに増やしたわけではなく、ひとつひとつの記事の内容やクオリティは一定に保った上での成果です(実際に記事を読んでいただければわかると思います)。 執筆に関わったメンバーの数ですと、このアドベントカレンダーを含めると約30名のメンバーに執筆してもらうことができました。またブログの読者数(はてなブログの機能で購読していただいている読者の数)は約3倍になりました。 イベント、勉強会での発表機会ができた 大規模Webサービスの開発にせまる「BASE Geek Night #1」 base.connpass.com レガシーコード改革!UT/CIでWebサービスの技術的負債を解消する取り組み base.connpass.com テックブログで書いてもらったネタをプレゼン形式にして勉強会やmeetupで発表してもらうことができました。上記は実際に開催したイベントです。中にはこのイベントで「人生で初めてLTをやった」というメンバーの声もあり、外部での発表機会の創出につながりました。 採用面談、面接で「テックブログを見てます!」との声が増えた 正確に数値を計測してはいないんですが、採用候補者とのカジュアル面談や面接を担当するメンバーより、候補者の方から「テックブログ見てます」と言っていただけることが明らかに増えたとのことです。 何より、このAdvent Calendarを実施できた! 日々の運営の積み重ねが実ったのか、2018年はBASEで初のアドベントカレンダーを開催することができました。 11月末に編集長がSlackに下記のように投稿したところ、 これから数時間後… 次々とメンバーが手を挙げてくれて、あっという間に25枠がすべて埋まりました…この怒涛の投稿の様子には正直かなり感動しました。 しかもアドベントカレンダーの開始は12/1、この投稿は11/28とかなりタイトな依頼だったのにメンバーが次々と手を挙げてくれて、BASEの行動指針の一つである「 Move Fast 」を体現していてすばらしいです。 テックブログの運営で挫折しないためのTips ここからは実際の運営のTipsを書いていきます。 テックブログの運営でネックなのは、運営担当者自身が人事など現場とは別部署に在籍していたり、入社したばかりという方や年上の方が多い方の場合、周りのメンバーをうまく巻き込めるか、途中で挫折しないか不安だという部分ではないでしょうか。私自身も入社してすぐの、まだメンバーの顔も覚えきれていないような状態で編集部として運営に携わるようになり、かつ前職とは業界や職種も違う状況で初挑戦なことばかりでした。 そんな私でも実行できたTipsですので、特に人事や広報、若手社員の方に試していただけると嬉しいです。 テックブログ運営の成功体験をもつメンバーを運営メンバーに入れる このメリットは下記の2つだと考えています。 運営ノウハウを転用できる エバンジェリストとして運用のモチベーションを保ってくれる いきなりテックブログの運営経験がない私が運営を始めてもスベるのは目に見えています。なのでそういう時には素直に成功体験を持つメンバーから運営ノウハウを教えてもらうのが得策だと思います。 また、テックブログ運営の成功体験のある人物とともに運用していくことで、自らのモチベーションを保つことができ、運用中の挫折を回避することができます。自分自身に成功体験がないと運営していく中でどうしても「これは本当に効果があるのだろうか?」、「現場メンバーには迷惑ではないか」と疑心暗鬼になってしまうこともあるのですが、成功体験のあるメンバーはそのような不安を解消してくれる心強い存在で、「 Be Hopeful 」に運営を続けることができます。 サポート体制を作る BASEのテックブログでは、現在編集部が中心となり運営をしています。 編集部が行っているのは下記です。 月1の編集会議でのネタ出しと、予備も含めてその月に公開したい記事を決める 該当のメンバーに執筆依頼 記事チェック(編集やタイトル案の相談など) 画像の作成・撮影のサポート 執筆マニュアルのメンテナンス 記事案の管理はスプレッドシートで行っています。「常設」がいわゆる予備記事です。 他の企業では、テックブログは書きたい人が書きたいときに自由に記事を公開するという運営方法を取っている場合もあるかと思います。もちろんBASEの場合も「このイベントで登壇したからその内容をブログ記事にしたい」、「こんな取り組みをやってみたんだけど、ブログネタとしてどう?」といった流れで記事を書いてもらうことはあります。 運営方法は様々だと思いますが、編集部が主導する体制を作ることのメリットは下記だと考えています。 投稿ペースを維持できる 社内のいろんな人に書いてもらいやすい 記事のテイストを一定に保てるため読者が読みやすい まず投稿ペースに関してです。現在は月に2本は必ず公開するという方針の下、編集部で予備も含めて公開したい記事を決めています。自由投稿だと投稿の頻度にバラつきが出てしまう可能性もあり、そうなるとブログの見栄えとしてはあまり好ましくありません。編集部がペースメーカーとなって更新頻度を維持することでブログの活気を保つことができると考えています。 「社内のいろんな人に書いてもらいやすい」についてですが、自由投稿の場合ですと、社内で素晴らしい取り組みを行っていたり、目覚ましい成果を上げているけどもブログはあまり書かないという人もいて、機会損失が発生する可能性もあります。編集部体制では、そういった取り組みを行っている人に注目して執筆依頼をしています。先述のように2018年4月以降は約30名のメンバーに執筆してもらうことができ、記事内容もPHPでのWebサービス開発からデザイン思想、データベースや機械学習などバラエティに富んだものになっていると思います。 もちろん、理想はどのメンバーからも「これブログで書きたいんだけど」と次々に提案をもらえ、さまざまな記事が投稿される状態だと思いますし、今回のアドベントカレンダーはまさに理想の形で開催することができ、とても嬉しかったです。 そして記事テイストの統一に関してですが、こちらは編集部でテックブログの執筆マニュアルを用意していたり、公開前に編集部が内容を確認することで実現できているかと思います。とはいえこれは最低限のチェックを行うのみで、執筆者の個性を最大限に活かすよう意識しています。 お祭り感を出す 前述のように、現在は編集部から記事の執筆を依頼する場合があり、依頼の仕方によっては執筆者が義務やタスクのようにネガティブに捉えてしまう可能性もあります。 そこで、記事の公開に関する要所要所でお祭り感を出すことで、執筆を依頼された人もまだ執筆したことのない人も、記事の執筆という行為をポジティブに捉えてもらうことができると考えています。 まず、当たり前ですが記事を公開したらメンバー全員がいるSlackチャンネルでお知らせします。記事を公開したら、Slackの #general で執筆した人に「こんなの書いたよ」と投稿してもらいます。そうすると代表の鶴岡やCTOの藤川も含め各メンバーがSNSでシェアしてくれるのでちょっとしたお祭り感が出ます。 またはてブのホットエントリ入りした時や、媒体で取り上げられたりしたときにはメンバーが随時共有してくれるので、執筆した本人も嬉しいですよね。 こんな感じでシェアしてくれます。 おわりに こうやって積極的なテックブログの運営ができるのも、現場のメンバーが日々のプロダクトづくりで様々なことにチャレンジしているからこそです。そして私がこうやって記事がかけるのも編集部のメンバーのサポートのおかげです。この場を借りて記事を執筆してくれるメンバー、編集部のメンバーにはお礼を伝えたいと思います。 今後ともみなさまにお楽しみいただけるようなテックブログの運営をコツコツと続けていきます。 BASEのメンバーは素敵な人たちばかりです。このテックブログを読んでBASEにご興味をもってくださった方は、ぜひ下記をご覧ください!一緒にBASEではたらきましょう! binc.jp 明日はBASE株式会社が創業して6周年ということで、代表の鶴岡が記事を書きます!乞うご期待!
アバター
この記事は、「BASE Advent Calendar 2018」の9日目の記事です。 devblog.thebase.in 前日は id:match_1 でした。こんにちは、10日ほど前に データベース移行についての記事 を書かせていただいた植木です。 今回はインデックスとBtreeのパフォーマンスチューニング系のお話をしたいと思います。 概要 Btreeの構造などを説明している資料は他にもあるし、SQLのアンチパターンもある程度あるけど、何故アンチパターンになるのかなど構造を理解しながらの説明が無いように思っていました。今回はBtreeの構造を説明しながら、どのように動いてパフォーマンスに影響しているのかを説明しようかと思います。SQLのパフォーマンスチューニングはBtreeの構造を理解しているかどうかで全く違うものになります。構造を意識しつつ、誤解しやすいところや何故この処理でインデックスが効くのかなど考えてみましょう。 Btreeの仕組み Btreeとは 情報処理試験などで出ますから知ってるよって人も多いですよね。ソート処理と木構造によって検索の高速化をするアルゴリズムとなります。このBtree構造がDBのインデックスの内部構造となります。弊社だとMySQL系ですので若干カスタマイズしたB+treeとなります。 下の図を見てほしいのですが、データを格納する単位であるノード内にはインデックス対象のキー値とそれ以外にリーフには実データへのポインタ情報(MySQLだとPK)が入っています。木構造を使った分岐で対象のリーフノードを選択したらポインタ経由で実テーブルにアクセスしてデータを取得します。 B+treeがBtreeと違う点はリーフノードにもブランチノードと同じ情報が入る事とリーフノードに次のリーフのポインタが入る事になります。これらの対応をする事で範囲検索やorder by、group by等の対応がしやすくなります。今回はこのB+treeを使っての説明をいたします。以降、Btreeと書いているものはB+treeです。 今回はわかりやすくしたいのでノード内に1データしか入らないと仮定してキー値に1−10のデータを入れた場合を想定します。実際にはノードに複数レコードが入りますが、ノードの先頭データと末尾データを使って同じような構造と比較をします。 また、参考に普通のBtreeだとこうなります。 何故この構造にデータを並べると早いの? 例えば、今回のように1〜10まであるデータの中で6を探したいと思った場合、通常なら10回6という数字と格納データとを比較する必要があります。ユニーク制約をつける事ができたとしても6回です。それに対して、木構造にしておけば、まずルートノードの5と比較して大きいので右のブランチへ行き、7と比較して小さいので左に行けばもう目当てのデータに辿りつきます。 インデックスをつけない状態なら検索はデータ量に比例した処理時間が必要ですが、木構造にする事で必要な時間はデータ量の対数に比例します。 実際のSQLでよくある問題などなど 範囲検索 下記のような検索をした場合に誤解している方がいます。 where id between 6 and 8 この時、Btree上で6と8の位置を検索してその間のデータを全て取るなんて器用な事はできません。今回の上図データだとおそらくDBが6を選んで検索し、6以降の全データを取得して一つずつ8より小さくないかを比較します。また、その際に6と8どちらかの数字を選択するかを判断するには統計情報を使います。 likeでインデックスが効く場合 ここで少しBtree構造がわかった事だと思うので、前方一致ならlike検索でもインデックスが効く事を説明してみます。例えば、yamada、tanaka、suzuki、yamakawaさんがいた場合にBtreeの構造にしたらsuzuki、tanaka、yamada、yamakawaの順番になりますね。この時に下記のような条件で検索すればyamadaさんの直前の位置まで木構造の動きで辿りつく事ができ、それ以降のデータを取得する動きができるという事になります。「%yama%」のような中間一致や後方一致では、Btreeのソート順が活かせないためBtree検索はできません。 where name like 'yama%' 検索キーに関数や演算を使うとインデックスが効かない Btreeはデータをソートしている事が重要な訳ですが、例えば、下記のように関数を使った場合、ソート順が変わる可能性があります。 where date(update_time)='2018-01-01' もっとわかりやすくreverse関数を使えばソート順は真逆になります。一つ一つの関数や演算の特徴までDBは把握しきれないためインデックスを利用できません。 複合インデックスの構造 続いて複合インデックスの場合のBtree構造です。購入などの情報をイメージしてもらえると良いかと思います。今回はユーザーと購入日をイメージしてみます。(user_id,update_dt)にインデックスをつけてuser_idが0001〜0004の人が1/1から1/3まで何らかの操作をした場合、インデックス内部のBtreeはこのような並びになります。 ここで下記のように検索すれば木構造の動きで(0003,2018-01-02)まで到達します(黄色塗り)。 where user_id='0003' and update_dt>='2018-01-02' 後はそれ以降の第一キーが0003のリーフを取得すればいいわけです。(本当はINSERT順考えたらこの木構造にはならないけど、今回は気にしない) 複合インデックスの順番誤り 日付とキーの順番を逆にしている場合がよくあります。(update_dt,userid)でインデックスをつけているわけです。そうなるとこういった構造になります。 この状態で先ほどと同じく「user_id='0003' and update_dt>='2018-01-02'」を検索しても、第一キーであるupdate_dtが'2018-01-02'の先頭(黄色塗り)を検索して、それ以降の8つのデータを全て取得し、user_idが0003と同じかを比較するという処理になります。このようにキーの順番を間違えると非効率な処理となります。 キー飛ばし ソート順が重要なので、基本的に第一キーを飛ばして第二キーのみを指定したクエリを実行してもインデックスは機能しません。第三キーまであるインデックスを作ってwhere句で第二キーを指定しない場合は第一キーまでのソート順が利用できるため第一キーだけのインデックスと同じ動きとなります。 最適な複合インデックスの順番づけ そんなものはありません。まず、=で検索するキーを上位に置きます。続いて上で書いたような範囲検索で使うキー(よくあるのが日付)やinで取得するようなキーを指定します。必須であればあるほど上位のキーにする事が重要です。そして範囲検索やINでの検索はそれによってどれだけ絞り込みができるかが重要なのでアプリケーション仕様にまで踏み込まないとなかなか判断は難しいです。 アプリケーション仕様によって、キーが指定される場合とされない場合などがありますので、そういう場合は(key1,key2,key3)のインデックス以外に(key1,key3)のインデックスも作るなどアプリケーションが指定するキーと実行頻度を考えて最適なインデックス構成を考える必要があります。 最後に Btreeの構造を理解すればわかるSQLパフォーマンスに関わる事をいくつかあげてみました。パフォーマンスチューニングにはジョインアルゴリズムの理解なども大きく響くのですが、まずはBtreeの理解をするのが一番だと思います。むしろ、ここを理解していないと確信を持ってのチューニングはできません。内部構造を理解して説明できる方が増えればSQL、DBのパフォーマンスもよくなりますので、是非ご理解いただけると良いかと思います。 BASEのSREチームではお客様に安心して買い物をしていただくために、サービスの安定性に責任を持ち、インフラ・アーキテクチャの改善に日々取り組み続けております。 ご興味を持たれた方いらっしゃいましたら是非ご連絡いただければ幸いです。 binc.jp
アバター
この記事は、「BASE Advent Calendar 2018」の8日目の記事です。 devblog.thebase.in こんにちは、BASE Design Groupマネージャーの早川( @match0129 )です。 今日は、ネットショップ作成サービス「BASE」の管理画面のリニューアルプロジェクトである「次世代管理画面」の話を少ししてみたいと思います。 次世代管理画面とは? ネットショップ作成サービス「BASE」は、現在「次世代管理画面プロジェクト」として大規模なリニューアルの真っ最中です。 プロジェクトが最初にはじまったのは今年の1月で、今は最初の大きな区切りのリリースに向けて開発が佳境を迎えているところです。 キックオフから早1年経とうとしているこのプロジェクトについて、やってきたことを少し振り返ってみたいと思います。 プロジェクトの方向性とコンセプトの定義 そもそもなぜ今リニューアルか?というのは以前 「BASE U」の記事 になっているので詳細はそちらをご覧いただきたいのですが、いくつかキーワードをピックアップして紹介します。 これまでの5年と、これからの5年 「BASE」はサービス開始から昨年11月で5周年を迎えました。5年間、機能をより早くリリースすることが優先されてきた中で、山積してきたUI/UX上の課題。 これからの5年もショップオーナーに使い続けてもらう管理画面とするためには、個々の課題をつぎはぎ的に修正していくのでは限界があり、全体のUXの見直しから入る必要があるということになりました。 「お母さんも使える」から「難しいことを簡単に」 「BASE」は、もともと代表の鶴岡が「ネットショップをやりたい」と言った自分のお母さんのために作ったサービスということもあり、「お母さんも使える」というコンセプトの元、シンプルなUIで誰でも簡単にネットショップが作れるサービスとして開発されてきました。 5年たった今、60万店舗の多種多様なニーズがある中で、「BASE」本来の「シンプル」さを担保しつつ、どのように機能を提供していくか。例えばインスタ連携など、これまでは「お母さんも使えるか」の判断から実装しなかったような機能も、「どうしたら簡単に提供できるか」という切り口で考えていく必要があります。 機能からパートナーへ 次世代管理画面の方向性として、以下のコンセプトを定義しました。 【ものづくりを行う全て人にとって無くてはならないパートナー】 「BASE」の管理画面は、「機能」から「パートナー」へ。 0から1を生み出せるのはオンリーワンな能力。そんな方々がそこだけに集中するために、 その能力でしっかり生きていってもらい豊かな世の中を創造してもらうために。 本質以外を全て巻きとるパートナー。 課題の抽出 最初の課題の整理だけでも、かなり時間を使いました。これまでに上がってきたユーザーからの声やredmineチケットの整理と分類をしつつ、デザイナー、エンジニア、ディレクター、マーケティングなど部署をまたいだチームでワークショップを実施し、中心的に使われる機能についてのカスタマージャーニーマップを作成。 自分としては初めての取り組みで進行上の反省点も多かったですが、チームの中にはガチでショップを運営しているメンバーもいたりして、ショップを運営してみないとわからない目からウロコのUXの問題も多数発覚し、大きな収穫となりました。 まだ全機能でやりきれてないので、またタイミングみて実施したいと思っています。 BASEらしさの定義とUIコンポーネントの作成 デザイン上で大きな課題だったのが、 昨日の北村の記事 にもあったように、「BASE」全体を通して統一されていないボタンやフォームのコンポーネントのデザイン。次世代管理画面に向けて、UIコンポーネントのデザインを再定義することに。 デザインに入る前に、Design Groupで1日社内合宿をし、「BASE」らしさとは?についてディスカッションをしました。そのときに作成したのが、 以前の記事 でも紹介したこの図です。「誠実」が中央にあるのがポイント! ちなみに、この社内合宿当時にはまだなかったのですが、10月に60万店舗を達成したときに「 We are Owners 」というLPをリリースしました。 このWe are Ownersのメッセージ自体は代表の鶴岡の発案ですが、デザインチームで話し合った「BASEらしさ」とスムーズにシンクロした感があり、個人的にはかなり腹落ちしました。 UIコンポーネントはAtomic Designの考え方をベースに。Sketchライブラリとして作成することで、その後のデザイン作成の効率が大幅にアップしました。このあたりの詳細は後日デザイナー小山が記事にしてくれる予定です。 デザインとプロトタイピング デザイン作業自体の効率はかなり上がったとはいえ、抽出された課題をいかにデザインに落とし込むかはやはり骨の折れる作業です。Sketchのプロトタイピング機能を使い、実際に画面遷移や動きをみながらのプロトタイピングを繰り返しながら進めています。 デザインをする中でこのコンポーネント必要だね、ということが出てくる場合も多々あり。コンポーネント化するかどうかの線引きは、現状「3回以上それが出現するかどうか」を基準としています。 フロントエンドチームの誕生、開発のスタート これまで「BASE」のフロントエンドは、デザイナーがjQueryを頑張って書いて担保するという状態でしたが、同じような重複コードの頻発、CSSのクラス・id名との闇依存、jQuery複数バージョンの共存など、サービス開始から5年分の負債が地獄絵図の様相を呈していました。この辺の負債をどうするかというのも次世代の課題の一つでした。 今回のリニューアルにあたってはフロントエンドチームが新たに発足し、フロントエンドエンジニアのメンバーが中心となって過去の負債の返済と、Vue.js + TypeSrcript の新しいアーキテクチャの構築を同時に進めてくれています。 フロントエンドだけでなく、バックエンド側もAPI化などかなりの大掛かりな開発作業が進行中です。 新しいUIコンポーネントは、Vue.jsのコンポーネントとして実装。スタイルガイドにはStorybookを導入しています。 自分もこれまでは「 漢は黙ってjQuery 」!的なデザイナーでしたが、今回ようやくVue.jsに着手しました。 jQuery脳からの脱却にしばらく時間がかかってしまいましたが、ようやく慣れてきたと思える今日この頃です。 プロジェクトのこれから 今後5年に向けて、デザインだけでなくフロントエンド・バックエンドも含めたフルリニューアルとなっており、この規模のリニューアルに関わるのは自分も初めてで、この1年を振り返ると試行錯誤の連続であっという間でした。 リリースは段階的にしていきますが、「BASE」の管理画面全体でいうとまだまだ入り口です。 一つめの大きな機能の実装である程度パターンが見えてきたところで、来年以降はデザイン・開発ともに関わるメンバーも増やして複数機能を同時に進められるようにして開発効率を上げていければと考えています。 デザインチームとしても、今後はページの表示を少し直すだけでもVue.jsのコンポーネントを触らざるを得なくなるので、その開発まで全員ができる必要はないものの、ある程度Vue.jsに慣れていってもらいたいなと思っています。 ということで、来週はデザイナー向け「Vue bootcamp」がフロントエンド 三佐和教官 !によって実施される予定。 こちらの様子もアドベントカレンダーの記事になる予定なので、お楽しみに☆ 明日はデータベースエンジニアの植木が書きます。こちらもお楽しみに!!
アバター
この記事は、「BASE Advent Calendar 2018」の7日目の記事です。 devblog.thebase.in こんにちは、BASEのデザインチームに所属している北村( @naomi_kun )です。 BASEのデザイン管理ツールに「 Abstract 」を導入してちょうど1年が経ったので、導入する前を振り返りながら、良かったところの所感をまとめたいと思います。 はじめに 突然ですが、デザインデータの管理ってみなさんどうしていますか? 共有するのが大変だったり、バージョン管理のルールを厳格にしておかないとメンテがしづらかったりと、デザインデータの管理って非常に面倒ですよね。 BASEもGoogleDriveやDropboxを用いてデータ共有していたものの、ファイルの置き場が煩雑になったり、新しいデータがどれなのか毎回他のデザイナーに確認しなければならないなど、多くの課題を抱えていました。 デザインデータもGit管理したかった! デザインデータもGit管理したい、と当時同じプロジェクトを進めていたエンジニアさんに相談したところ「 Abstractってツールが良いらしいよ! 」と教えていただいたので、まずはそのプロジェクト内で1ヶ月試験導入してみました。 Abstract導入前の課題と問題点 データの管理が全くされていない😱 誰がどのデータを持っているのかすら分からない😱 複数人でデータを共有できる体制になっていない😱 UIコンポーネントも管理されていなかったので、人によってデザインが違う😱 この他にも、デザインレビューの際に毎回GitHubにissueを建てなければならず、アプリなど短期間で細かな改修を行うプロジェクトではデザイナーの負担が大きかったので、デザイン画面上にレビュー出来るシステムが欲しかったという背景もあります。 導入して1年、現在の状況 Abstractに登録されているプロジェクト一覧 現在はネットショップ作成サービス「BASE」、ショッピングアプリ「BASE」含む全てのプロジェクトをAbstract上で管理しています。 無料アカウントでもコメント機能が使えるので、ディレクターさん、エンジニアさんも含めると既に約40人規模での運用になっています。 Abstract導入後の改善点 データのバージョン管理ができる🤗 複数人で同一データ編集・共有できる体制になった🤗 誰がどのデータを触ったのか分かるようになった🤗 デザインシステムのおかげで統一されたUIでデザインされるようになった🤗 導入してみて変化したところ 実際の開発フロー 新規開発タスクが発生次第、MasterからFeastureブランチを切る ブランチ名は機能ベースの名前にする こまめにコミット Abstract上でデザインレビューする デザインがFixしたら完成形をZeplinへアップする デザイン作業終了後、Masterへマージ diffの確認がとても楽 AbstractでDiffを確認 Gitのように前回のコミットとの差分が分かるので、作業内容の確認がとても楽になりました。 エンジニアさんにお願いするときの共有方法も楽ちんです。 レビューがAbstract上で出来るようになった 今までのデザインレビューはGitHubのissueに画面に直接コメントできるので、 コミュニケーションコストも、作業効率も劇的に改善されました。 実際のレビュー画面 エンジニアさんもコメントしてくれます。 できる! Abstract使っていてもツライことがある Abstractでの管理体制が整ってきているものの、Abstractだけでの運用ではまだまだかゆいところに手が届かない状況です。 複数人のデザイナーが同時に作業するので頻繁にコンクリフトしたり、マージするとアートボードが重なってしまったり…。 先日のAdobeMAX Japanで、XDもバージョン管理機能が出来るようになると発表があったので、今後もウォッチしたいです。 まとめ Abstractを導入して1年が経ちましたが、デザインチーム内での意識改善はもちろん、エンジニアも巻き込んでのAbstract運用フローが根付きつつあると思います。 チーム内の作業効率も格段に上がったので、Abstractを導入していないチームは是非検討してみてはいかがでしょうか。 明日は id:match_1 さんです。お楽しみに! devblog.thebase.in
アバター
これは、「BASE Advent Calendar 2018」の6日目の記事です。 DataStrategyの齋藤( @pigooosuke )が担当します。 devblog.thebase.in はじめに 機械学習エンジニアの人は、分類や回帰などの課題に取り組むにあたって、偉い人や導入先の部門から「その予測どれぐらい外れるの?」「学習モデルの予測に対してどうリスク評価をすればいいの?」と尋ねられることはありませんか? そのような場面で活躍するかもしれないQuantile Regression(分位点回帰)のお話をします。 回帰モデルの評価 カテゴリーを予測するような分類問題では、各クラスでの精度を確認することはできます。しかし、売上や何かしらの値を予測する回帰問題では、そのモデルにおける特定地点での単一の予測値しか出力することが出来ず、その分布を把握することは単純ではありません。 # scikit-learnの線形回帰サンプル >>> import numpy as np >>> from sklearn.linear_model import LinearRegression >>> X = np.array([[ 1 , 1 ], [ 1 , 2 ], [ 2 , 2 ], [ 2 , 3 ]]) # 入力値のセット >>> y = np.dot(X, np.array([ 1 , 2 ])) + 3 # 正解データ >>> reg = LinearRegression().fit(X, y) >>> reg.predict(np.array([[ 3 , 5 ]])) # 入力[3, 5]に対して、予測値は1つ array([ 16. ]) ここで説明にあたって予測の確度という言葉を使うと定義が甘いのでもう少し分解します。回帰予測の範囲を把握する上でよく出てくるのは、この2種類ではないでしょうか。混同しないように事前に再確認しましょう。 信頼区間 Confidence interval ex. 95%信頼区間。こちらは、同じ条件で100回測定したら、計算した回帰曲線がその信頼区間内に収まった測定が95回ありましたという意味になります。 予測区間 Prediction interval ex. 95%予測区間。こちらは、今後観測しうるデータが100回出現したら、予測区間内に収まる観測が95回ありましたという意味になります。 下図は、標本に対して回帰曲線を計算したものです。 回帰曲線は信頼区間に収まり、標本は予測区間に収まります。 基本的には予測区間のほうが信頼区間の外側に位置します。 使い分けとしては、 モデルそのものの精度を測りたいのであれば 信頼区間 モデルにおける特定地点での予測値のばらつきを調べたいのであれば 予測区間 が目安になると思います。 今回お話をするQuantile Regressionは、予測区間を説明するために利用します。 Quantile Regression ~ 分位点回帰 ~ Quantileとは、日本語で四分位のことです。データをソートして区切った場合、それぞれのデータが上位何%に位置するのかを表現するときに使います。 2 quantileは、中央値と一致します。 0 quantile = 0 %ile (percentile: パーセンタイル) 1 quantile = 25 %ile 2 quantile = 50 %ile = median(中央値) 3 quantile = 75 %ile 4 quantile = 100 %ile Quantile Regressionは、線形回帰の損失関数を拡張したもので、通常のように二乗誤差を求めて平均値を最適化するのではなく、予め設定したquantile(percentile) での損失関数を最適化していきます。年収など偏りがある分布を平均値ではなく、中央値で確認したい場合に利用されます。用途としても、単純に各分位点での予測値の開きをみるだけでなく、例えば、年収or家賃の大小によって格差を図る指標がどれぐらい変化するのかを比較することによって変数の理解に繋がることもあり、社会学や経済学の分野で活用される事例が多くあります。 ちなみに、Quantile Regressionの推定には、check functionと呼ばれる常に絶対値を取るインジケーター関数を利用して、 以下の損失関数を最適化していきます。 手短に説明すると、正解値と予測値の差が正のときに 、 負のとき(反転するので正確には正)に が重み付けされた和をLossとしています。 導出を詳しく知りたい人はこちらのリンクが詳しいです。 原著(pdf) QUANTILE REGRESSION : ROGER KOENKER wikipedia, Quantile Regression Statistics/Numerical Methods/Quantile Regression 理論的な部分の解説 リンク先を見ても細かな部分の省略が多かったので、勉強ついでに自分でも導出をやってみました。興味がなければ読み飛ばし推奨です。 を確率変数、 を平均値、 をquantile(percentile)、 を分布の中心として定義した場合、平均は誤差の平方和の最小値で表せるように、中央値は絶対偏差和の最小値で表すことができます。 が 以下のときの を以下のように累積分布関数 を用いて定義します。 これを逆関数で表すとこうなります。 上で中央値を定義しましたが、これを一般化すると、 ] ここでの が最初に出てきた非負となるインジケーター関数です。 このインジケーター関数を一行で表現するとこうなります。 確率変数 のときにおける、確率密度関数を 、累積分布関数を とします。そして、離散値として で、連続値として で表現すると、それぞれ以下の通りになります。 上記に対し、 で微分し、左辺を0と置いたとき、 となり、 と分位点 が等しくなりました。 Pythonで試してみる Python系ライブラリのいくつかでは、このQuantile Regressionの損失関数がサポートされており、サンプルとして少し紹介してみます。 みんな大好きscikit-learn import numpy as np import matplotlib.pyplot as plt from sklearn.ensemble import GradientBoostingRegressor #### sin波にノイズを加えたデータセットでやってみます。 n = 300 # 標本数 noise = 0.2 # noiseの強さ np.random.seed( 1 ) x = np.linspace( 0 , 2 *np.pi, n) y = np.sin(x) + noise * np.random.randn(n) x = x.reshape(- 1 , 1 ) # 95%予測区間を求めるため、上限・下限ともに5%を2で等分した0.025, 0.975の位置で予測する alpha = 0.975 # model 定義 clf = GradientBoostingRegressor(loss= 'quantile' , alpha=alpha, n_estimators= 250 , max_depth= 3 , learning_rate= .1 , min_samples_leaf= 9 , min_samples_split= 9 ) # 上限の予測 clf.fit(x, y) y_upper = clf.predict(x) # alphaを反転して下限の予測 clf.set_params(alpha= 1.0 - alpha) clf.fit(x, y) y_lower = clf.predict(x) # 損失関数を最小2乗法に設定して予測 clf.set_params(loss= 'ls' ) clf.fit(x, y) y_pred = clf.predict(x) # vizualization fig = plt.figure(figsize=( 8 , 4 )) plt.plot(x, y, 'b.' , markersize= 10 , label= "標本" ) plt.plot(x, y_upper, 'k-' ) plt.plot(x, y_lower, 'k-' ) plt.plot(x, y_pred, 'r-' , label= '予測値' ) plt.fill(np.concatenate([x, x[::- 1 ]]), np.concatenate([y_upper, y_lower[::- 1 ]]), alpha= .5 , fc= 'b' , ec= 'None' , label= '95%予測区間' ) plt.legend(loc= 'upper right' ) plt.show() 現在は、アンサンブルの GradientBoostingRegressor でしか、この 損失関数 は設定出来ません。 ちなみに、以前から線形モデルに対してQuantile Regressionを追加しようという動きはあるようです。続報が待たれますね。 https://github.com/scikit-learn/scikit-learn/issues/3148 勾配ブースティングのLightGBM import lightgbm as lgb # 95%予測区間を求めるため、上限・下限ともに5%を2で等分した0.025, 0.975の位置で予測する alpha = 0.975 # model 定義 clf = lgb.LGBMRegressor(objective= 'quantile' , alpha=alpha, n_estimators= 250 , max_depth= 3 , learning_rate= .1 , min_samples_leaf= 9 , min_samples_split= 9 ) # 上限の予測 clf.fit(x, y) y_upper = clf.predict(x) # alphaを反転して下限の予測 clf.set_params(alpha= 1.0 - alpha) clf.fit(x, y) y_lower = clf.predict(x) # 損失関数を最小2乗法に設定して予測 clf.set_params(objective= 'regression' ) clf.fit(x, y) y_pred = clf.predict(x) # vizualization fig = plt.figure(figsize=( 8 , 4 )) plt.plot(x, y, 'b.' , markersize= 10 , label= "標本" ) plt.plot(x, y_upper, 'k-' ) plt.plot(x, y_lower, 'k-' ) plt.plot(x, y_pred, 'r-' , label= '予測値' ) plt.fill(np.concatenate([x, x[::- 1 ]]), np.concatenate([y_upper, y_lower[::- 1 ]]), alpha= .5 , fc= 'b' , ec= 'None' , label= '95%予測区間' ) plt.legend(loc= 'upper right' ) plt.show() 実際、scikit-learnに実装されている損失関数自体も中身は pred = pred.ravel() diff = y - pred alpha = self.alpha mask = y > pred if sample_weight is None : loss = (alpha * diff[mask]. sum () - ( 1.0 - alpha) * diff[~mask]. sum ()) / y.shape[ 0 ] と、非常にシンプルな構造になっています。 ちなみに、LightGBMの実装は以下のようになっていました。 class QuantileMetric : public RegressionMetric<QuantileMetric> { public : explicit QuantileMetric( const Config& config) :RegressionMetric<QuantileMetric>(config) { } inline static double LossOnPoint(label_t label, double score, const Config& config) { double delta = label - score; if (delta < 0 ) { return (config.alpha - 1.0f ) * delta; } else { return config.alpha * delta; } } inline static const char * Name() { return "quantile" ; } }; それぞれのleaf_valueでの正解と予測の差に対して重みを付けているだけなので、根本は同じですね。 ただし、もう一つの勾配ブースティング代表格のXgboostでは標準実装されておらず、自分で損失関数を設定する必要がありそうです。 興味がある人は自作してみると面白いかもしれませんね。 今回は、回帰モデルの出力を説明するという点でQuantile Regressionを紹介しました。 回帰モデルの出力に関して説明を求められたときの一つの手法として覚えておくと良さそうですね。 明日は、 id:lllitchi さんがデザインについて語ってくれます。お楽しみに!
アバター
この記事は、「BASE Advent Calendar 2018」の5日目の記事です。 devblog.thebase.in Backend Engineer の田中( @tenkoma )です。 「BASE」の裏側で動いているアプリケーションは CakePHP 2 を使っています。 そのCakePHP 2にPHP7.3対応のプルリクエストを送ろうとしたけど先を越されてしまった話をします。 CakePHPのPHP7.3対応状況 PHPはPHP7.0以降、大きな機能追加のあるバージョンが年1回リリース *1 されるようになりました。 リリースサイクルは公開されており( PHP: todo:php73 )、毎年9月中までにはRC版が公開されているようです。活発なPHP製OSSプロジェクトなら、ビルドスクリプトにPHPの新しいバージョンを追加して、互換性が確認できているか確かめられるようになっているかもしれません。 CakePHP 3はテストスイートがすべてパスしたブランチを2018年10月2日に master マージしていたようです。 参考: Pull Request #12488 Add php7.3 to build matrix in allow failure mode. ではCakePHP 2の状況は? PHP 7.2でだいたい動作することは自分で確かめていたので、PHP 7.3 RC3が出た頃に状況を確認しました。 すると、PHP 7.3互換性対応のプルリクエストがマージされたことがあるが、全テストケースがパスしているかはわからない状況みたいでした。 PHP 7.3 互換性対応: Pull Request #12487 Fixes: a few issues found when running PHP 7.3 メンテナーがローカルでテスト実行して問題ないことを確認したようです。 PHP 7.3の勉強にもなるし、やってみるか〜ということで作業することにしました。 ほぼ同じ修正をコミットしてたのに…悔 まずは、テストケースが失敗するとしたら、どれが失敗するか把握する必要があるので、PHP 7.3でテストを実行するためのプルリクエストをつくり、マージしてもらいました。 Pull Request #12707 [2.x]PHP7.3 added to the build setting. マージ後のビルドログ を見ると、270 Errorsと表示されましたが、ほぼ同種のエラーがいろいろなテストに発生しているだけで、修正はたいしたことありません。 自分のアカウントにフォークしたリポジトリもTravisの設定を追加していたので、一つ一つ修正してはプッシュしてエラーを確認していったところ、残り1箇所まで減らすことができました。 エラーが残り1箇所に減ったビルドログ: Job #90.13 - tenkoma/cakephp - Travis CI 残る一つは mb_strtoupper() のPolyfillのテストでした。 CakePHP 2には、mbstring拡張が利用できない場合でも mb_strtoupper() , mb_strtolower() が使えるよう、関数が用意されています。(実際のコードは Multibyte クラスで実装されています。) mb_strtoupper() のテストも用意されていますが、Travis CIでmbstring拡張が有効になっており、組み込み関数が呼び出されて、そこでテストが失敗していました。 There was 1 failure: 1) MultibyteTest::testUsingMbStrtoupper Failed asserting that two strings are equal. --- Expected +++ Actual @@ @@ -'ԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀՁՂՃՄՅՆՇՈՉՊՋՌՍՎՏՐՑՒՓՔՕՖև' +'ԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀՁՂՃՄՅՆՇՈՉՊՋՌՍՎՏՐՑՒՓՔՕՖԵՒ' PHP 7.3 RC版の不具合かとも思いましたが、これは文字ケース変換の機能拡張のようです。 https://t.co/TbDfTo9ycq 仕様っぽいな…7.2.9 と 7.3.0RC5 で挙動が変わる — Koji Tanaka (@tenkoma) 2018年11月11日 PHP 7.3で仕様が変わることはわかりましたが、どう直すか悩みました。いまさら Multibyte クラスの仕様をPHPにあわせて拡張するのもどうかと思いましたし(実装難しそう…)互換性を諦めて、テストを削除してしまえばいいか、とも考えました。 それから何もせず1日経ったところで次のプルリクエストを見つけました。 Pull Request #12718: Fix some PHP 7.3 errors コミットの内容はほぼ一緒で、 mb_strtoupper() のテスト失敗はそのままでしたが、見事に先を越されてしまいました。 うお〜先を越された。ともあれPHP7.3もサポート出来そうですね https://t.co/dA56Rm9zDw — Koji Tanaka (@tenkoma) 2018年11月12日 先にプルリクエストを出して、スレッドで相談しながら決めれば良かったんですよね。 行動指針「Move Fast」にしたがって、早く出せば良かったな、と思いました。 補足: PHP 7.3対応で修正されたCakePHP 2の互換性 実装コードの修正を2点紹介します。 compact() で未定義の変数を配列にしようとするとNotice Errorになる 7.2 まで、 compact() では未定義変数名を含めることができました。(戻り値にキーは無い) $defined = 'defined'; $arr = compact ('defined', 'undefined'); var_dump($arr); // output: // array(1) { // 'defined' = > // string(7) "defined" // } 7.3からは以下のようなNotice Errorになります。 PHP Notice: compact(): Undefined variable: path in /home/travis/build/tenkoma/cakephp/lib/Cake/Utility/Debugger.php on line 255 switch文の break; の代わりに continue; を使うとWarning Errorになる 7.2 まで switch文ではbreak文の代わりにcontinue文を使うことができましたが、switch文の外側のループを対象にした命令のつもりでも、switchへのbreak文になる問題がありました。 foreach($items as $item) { switch($item['type']) { case 'A': continue; // foreachループの先頭に戻らず、switch文を抜けるだけ } } PHP 7.3ではWarning Errorとなります。PHP 8では構文エラーになるそうです。 参考: PHP 7.3 曖昧なcontinue文の禁止 – yohgaki's blog まとめ プルリクエストはある程度できたら出しちゃったほうがいいこともあるよ! 明日は id:pigooosuke です。お楽しみに! 「BASE Advent Calendar」の記事一覧はこちら。 devblog.thebase.in *1 : 毎年12月初旬頃
アバター
これは、「BASE Advent Calendar 2018」4日目の記事です。 devblog.thebase.in BASEでサーバーサイドエンジニアをやっている、東口( @hgsgtk )です。 BASE BANK というBASEの子会社にて 金融事業の立ち上げ を行っています。 以前投稿した、 Goを運用アプリケーションに導入する際のレイヤ構造模索の旅路 | Go Conference 2018 Autumn 発表レポート という記事の中で、レイヤ構造を成長させていくためのユニットテストについて言及させていただいていました。こちらのエントリーにて進めていたコードベースでは、全体で約85%程度のテストカバレッジとなっています。本日は、そんなGoのユニットテストについての内容です。 ユニットテストの知見 現在、Goのユニットテストについての知見は数多く見られ、 テーブル駆動テスト ・ サブテスト などといった基本的なところから、 非公開(unexported)な機能を使ったテスト や GoのAPIのテストにおける共通処理 で紹介されているようなインテグレーションテストの知見など、充実した情報量となっていると思います。 ただ、資料を漁っていて「どのようなテストヘルパーを作っているか」・「テーブル駆動テストをどのように活用しているか」といった、現場のノウハウはまだ探しづらいなと感じています。 そのため、今回の記事では、筆者がAPI開発のために使っていたテストTipsや共通化のためのテストヘルパーについて紹介してみます。 具体的には、「 http.Handlerのテスト 」・「 実行ごとに結果が異なる処理のテスト 」・「 外部ライブラリの活用 」の3点についてそれぞれ紹介していきます。 http.Handlerのテスト http.Handlerのユニットテストでは、 httptest パッケージを活用して、ハンドラーに対してリクエストを送り、レスポンスを検証するといったテストを書きます。その際、期待値定義をどのようにするか分かれるところかと思いますが、筆者は対象ハンドラーに対する リクエスト・レスポンスの期待値をJSONで定義する ようにしています。 実際に次のように、ユーザーのリソースの取得を行うようなHandlerコードをのテスト例を紹介します。 . ├── interfaces │   └── controller │ ├── testdata │   │ └── user_controller │   │ ├── request.json │   │ └── response.golden │   ├── user_controller.go │   └── user_controller_test.go └── testutil   └── handler.go 筆者のプロジェクトでは、現在、テストヘルパー群を testutil というサブパッケージにまとめており、各テストケースからは、 testutil パッケージが提供する関数を使用する構造となっています。 実際に、ユーザーのリソース取得を行うHandlerを例に見てみます。 func TestUserController_Handler(t *testing.T) { // prepare http.Request and http.ResponseWriter w := httptest.NewRecorder() r := httptest.NewRequest( "GET" , "/v1/users" , nil ) // execute the target method c := controller.UserController{} c.Handler(w, r) res := w.Result() defer res.Body.Close() testutil.AssertResponse(t, res, http.StatusOK, "./testdata/user_controller/response.golden" ) } testutil.AssertResponse というResponse Assertion用のテストヘルパーを提供しています。 // AssertResponse assert response header and body. func AssertResponse(t *testing.T, res *http.Response, code int , path string ) { t.Helper() AssertResponseHeader(t, res, code) AssertResponseBodyWithFile(t, res, path) } testutil パッケージで定義した AssertResponse は、レスポンスヘッダー・ボディを検証する機能を持っています。 下記がヘッダー検証関数 AssertResponseHeader の中身です。 // AssertResponseHeader assert response header. func AssertResponseHeader(t *testing.T, res *http.Response, code int ) { t.Helper() // ステータスコードのチェック if code != res.StatusCode { t.Errorf( "expected status code is '%d', \n but actual given code is '%d'" , code, res.StatusCode) } // Content-Typeのチェック if expected := "application/json; charset=utf-8" ; res.Header.Get( "Content-Type" ) != expected { t.Errorf( "unexpected response Content-Type, \n expected: %#v, \n but given #%v" , expected, res.Header.Get( "Content-Type" )) } } ステータスコード・Content-Typeをチェックしています。レスポンス検証の際、おまじないのようにこちらのアサーションは書くことになるので、共通化しておくと新規のhandler実装も楽になるかと思います。 次にBody検証関数 AssertResponseBodyWithFile です。 // AssertResponseBodyWithFile assert response body with test file. func AssertResponseBodyWithFile(t *testing.T, res *http.Response, path string ) { t.Helper() rs := GetStringFromTestFile(t, path) body, err := ioutil.ReadAll(res.Body) if err != nil { t.Fatalf( "unexpected error by ioutil.ReadAll() '%#v'" , err) } var actual bytes.Buffer err = json.Indent(&actual, body, "" , " " ) if err != nil { t.Fatalf( "unexpected error by json.Indent '%#v'" , err) } assert.JSONEq(t, rs, actual.String()) } 筆者のプロジェクトでは、 別定義したJSON文字列との比較 によってレスポンスボディを検証しています。使い方の流れは下記になります。 testdata 以下に期待値となるJSONを .golden 拡張子で配置 テストケースにファイルパスを指定。(今回の場合、 "./testdata/user_controller/response.golden" となります。) AssertResponseBodyWithFile にて対象ファイルと実レスポンスを検証 .golden 拡張子の使用方法については、 Testing with golden files in Go を参考にさせていただきました。テストのOutputの期待値を testdata 内に別ファイルとして定義する際に、標準ライブラリ内でよく使用されている方法のようです。 { " user ": { " id ": 1 } } この際、JSON同士の比較の可視性のために、部分的に stretchr/testify の assert.JSONEq を利用しています。 そして、ファイルから文字列を取得する実装 GetStringFromTestFile では、 ioutil.ReadFile を利用することで実現しています。 // GetStringFromTestFile get string from test file. func GetStringFromTestFile(t *testing.T, path string ) string { t.Helper() bt, err := ioutil.ReadFile(path) if err != nil { t.Fatalf( "unexpected error while opening file '%#v'" , err) } return string (bt) } これまで紹介していたレスポンスボディを別ファイル定義する手法は、リクエストボディが必要なテストケースでも同様に使用しています。リクエストで必要なjsonファイルは、Outputの期待値として定義していないため、 .json 拡張子としています。 func TestUserController_Handler(t *testing.T) { // prepare http.Request and http.ResponseWriter w := httptest.NewRecorder() r := httptest.NewRequest( "POST" , "/v1/users" , strings.NewReader(testutil.GetStringFromTestFile(t, "./testdata/user_controller/request.json" )) // execute the target method c := controller.UserController{} c.Handler(w, r) res := w.Result() defer res.Body.Close() testutil.AssertResponse(t, res, http.StatusOK, "./testdata/user_controller/response.golden" ) } 実行ごとに結果が異なる処理のテスト UUIDや時刻を扱うコードなど実行ごとに結果が異なる処理の場合は、実装自体を「 テスト可能な実装 」にする必要があります。 時刻を扱うテスト 本章冒頭でも述べたとおり、時刻を使う処理がある場合はテストごとに結果が変わってしまいます。そのため、筆者は、サブパッケージとして clock パッケージを作成し、テストケース内で実行時刻を固定できるようにしています。 clock パッケージの中身はこれだけです。 package clock import "time" // Now is current time. // テスト時に差し替え可能にするため、グローバル変数として定義している var Now = time.Now テスト時に差し替え可能とするため、グローバル変数にtime.Nowを設定しています。テスト時には、こちらの変数に任意の time.Time を設定することで時間を固定します。 なお、この実装の方法については、 外部環境への依存をテストする という資料を参考にさせていただきました。そして、時間を期待値に固定するテストヘルパーを提供しています。 package testutil // SetFakeTime set fake time to clock package. func SetFakeTime(t time.Time) { clock.Now = func () time.Time { return t } } テストケース内では、 testutil.SetFakeTime を使用することでテストケース内で任意の時間に設定できるようにしています。 UUID UUIDに関してもテスト実行ごとに値が変わってしまいます。筆者はUUID生成に、 github.com/satori/go.uuid を利用していますが、本ケースでは、go.uuidから使用するメソッドのみを必要としたInterfaceを作成しています。 package uuidgen import ( "github.com/satori/go.uuid" ) // UUIDGenerator is interface to generate uuid. type UUIDGenerator interface { V4() string } // UUID has generating method. type UUID struct { } // V4 wrap satori.uuid.NewV4() func (*UUID) V4() string { return uuid.NewV4().String() } そして、UUID生成する実装では、 UUIDGenerator interfaceを受け入れる型として定義することでモック差し替え可能にしています。 例えば、次のように引数に UUIDGenerator interfaceを指定した場合は、 func GetSampleUUID(g uuidgen.UUIDGenerator) { return g.V4() } 次のようにモック実装を作成して差し替えます。 type mockUUID struct {} func (*mockUUID) V4() string { return "sample-uuid-string" } func TestGetSampleUUID(t *testing.T) { actual := GetSampleUUID(&mockUUID{}) if diff := cmp.Diff( "sample-uuid-string" , actual); diff != "" { t.Errorf( "differs: (-want +got) \n %s" , diff) } } 外部ライブラリの活用 Goのユニットテストを書いていくにあたり、標準の testing パッケージでどれだけやるのか、テストフレームワークを利用するのかは序盤で検討するかと思います。筆者は、基本的に標準ライブラリを使用していますが、補助的に次のライブラリを利用しました。それぞれ使用感も含めて紹介いたします。 google/go-cmp github.com/google/go-cmp はGoogle非公式の値比較のライブラリです。構造体などの大きめな値比較をする際に使っている方が多いかと思います。 実際の使い方は 公式のexample が参考になりますが、例えば、構造体のアサーションをする場合には、次のように書くことができます。 func TestFuncHoge(t *testing.T) { expected := Hoge{ Moji: "hogehoge" , AnotherStruct: Huga{ Moji: "hugahuga" , }, Num: 1 , Flag: false , } res := FuncHoge() if diff := cmp.Diff(res, expected); diff != "" { t.Errorf( "Hogefunc differs: (-got +want) \n %s" , diff) } } テスト結果が FAIL の場合は次のような表示になります。 === RUN TestFuncHoge --- FAIL: TestFuncHoge (0.00s) main.go:56: Hogefunc differs: (-got +want) {Hoge}.AnotherStruct.Moji: -: "hugahuga" +: "hugahuga_diff" FAIL 構造体をそのまま比較できるので便利です。特に使って不便になることはないので、迷っていらっしゃる方がいれば使っていくのが良いかと思います。 DATA-DOG/go-sqlmock.v1 gopkg.in/DATA-DOG/go-sqlmock.v1 は、DBを使うコードをテストする際にsql.Driverをモックするものです。 実際にSQLを実行してデータを取得するような技術的実装を行う関数のテストにおいて、こちらのライブラリを使っています。 go-sqlmockは、各人が愚直に使うと少々辛いところがあったので、こちらのライブラリを不便少なく使うためにヘルパーを作成しています。 実際には、SQL自体をGoファイル内で定義していくのは見通しが悪く感じたので、次のようにSQLファイルを testdata ディレクトリに配置してテストで利用しています。 下記より、user情報を保存するコードに対するテストコードを例に紹介します。 . ├── infrastructure │   └── datastore │ ├── testdata │   │ └── user │   │ └── save.sql │   ├── user.go │   └── user_test.go └── testutil ├── error.go   └── sqlmock.go まず、テストヘルパーを利用したテストコードは次のようになります。 package datastore_test func TestUserStore_Save(t *testing.T) { // create database mock db, mock := testutil.NewMockSQLDB(t) // Helper 1 defer db.Close() // set expectation of mock query := testutil.GetSQLFromFile(t, "./testdata/user/save.sql" ) // Helper 2 mock.ExpectPrepare(query). ExpectExec(). WithArgs( 1 , 1 , testutil.GetTestTime(t), testutil.GetTestTime(t)). WillReturnResult( 1 ). WillReturnError( nil ) // execute the target method s := datastore.UserStore{} result, err := s.Save(db, 1 , 1 ) // assertion testutil.AssertMockExpectation(t, mock) // Helper 3 testutil.AssertError(t, err, tt.expectedErr) if tt.expected != result { t.Errorf( "expected: %#v, \n given: %#v" , tt.expected, result) } } 上記のテストコードに対して、関連するもので3つHelperを提供しています。 package testutil // Helper 1 // NewMockSQLDB create a new instance sql.DB for test mocking func NewMockSQLDB(t *testing.T) (*sql.DB, sqlmock.Sqlmock) { t.Helper() db, mock, err := sqlmock.New() if err != nil { t.Fatalf( "failed to create sqlmock '%#v'" , err) } return db, mock } 1つ目が、sqlmockのinstanceを生成するヘルパーです。ライブラリ系では、どうやって生成するか毎回思い出すのも面倒なのと、テスト用インスタンスの生成のエラーハンドリングにてテストコードが冗長になりがちなため、テストヘルパーとして提供しました。 次に、期待値となるSQL文を準備するためのヘルパーです。 package testutil // GetSQLFromFile get sql string which is quoted meta to use in sql-mock. func GetSQLFromFile(t *testing.T, path string ) string { t.Helper() mq := GetStringFromTestFile(t, path) return regexp.QuoteMeta(mq) } こちらでは、別ファイルのパスを受け取り、string型の文字列を返すヘルパーを作成しています。 sqlmockは、テスト時のSQLを正規表現で期待値との比較するのですが、「正規表現での比較」という点をテストコード実装者に意識してほしくないので、このようなヘルパーを用意しています。 テストコード実装者は、次のように testdata ディレクトリ以下に期待値となるSQLをSQLファイルとして設置すればOKです。 INSERT INTO users (last_name,first_name,created,modified) VALUES (?,?,?,?) 最後に、sqlmockで設定した期待値と一致しているかを確認するテストヘルパーを提供しています。 // AssertMockExpectation assert mock expectation func AssertMockExpectation(t *testing.T, mock sqlmock.Sqlmock) { if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf( "there were unfulfilled expectations: %s" , err) } } これは、sqlmockの sqlmock.Sqlmock の持つ関数をwrapしています。ライブラリ系の特殊なAssertionも、書き方を忘れがちなのと、設定するエラーメッセージは共通ですので、テストヘルパーとして仕様をWrapすると、各人でばらつきも少なくなるかと思います。 sqlmockの使用感ですが、昨今実データベースでテストするべきという言説が増えている通り、sqlmockで保護できるのが「期待したSQLが発行されているか」のみとなり、「そのSQLが正しいかどうか」についてはテスト保護対象外となる点が難点です。 筆者は、「SQLの妥当性」をチェックしやすいように、SQLのみ別ファイル定義する方法で、ミスを見逃すリスクの軽減に努めましたが、時間が許すのであればFixture機構を用意して実データベースでのテストを行うのがより安全かと思います。 実データベースを用いたテストについては、 Golang API Testing the HARD way という資料にて紹介していただいています。 golang/mock github.com/golang/mock は、テスト時のモックを生成する選択肢として使われることが多いかと思います。 基本的な使い方として、下記のようなコマンドを実行することでモックを自動生成してくれます。 mockgen -source hoge.go -destination mock_hoge.go 自動生成したモックは、本ライブラリが提供するモックコントローラによって期待値を設定することができます。 ctrl := gomock.NewController(t) mockHoge := mock_hoge.NewMockHoge(ctrl) mockHoge.EXPECT(). Fuga( "hoge" , "huga" ). Times( 1 ). Return( "hogehuga" , nil ) 筆者は、テーブル駆動テストでgomockを利用しているのですが、それぞれのテストケースで設定したい期待値が異なるため、期待値設定用の構造体を定義して動的に期待値を変えています。 type mockExpectedHogeFuga struct { callTime int arg string result string err error } func TestHoge_Fuga(t *testing.T) { tests := [] struct { name string mockExpected mockExpectedHogeFuga expected bool }{ { name: "success_to_print_hogehuga" , mockExpected: mockExpectedHogeFuga{ callTime: 1 , arg: "hoge" , result: "hogefuga" , err: nil , }, expected: true , }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { ctrl := gomock.NewController(t) mockHoge := mock_service.NewMockHoge(ctrl) mockHoge.EXPECT(). Fuga(tt.mockExpected.arg). Times(tt.mockExpected.callTime). Return(tt.mockExpected.result, tt.mockExpected.err) // テスト対象関数へモック注入し実行... }) } } テストケースごとに期待値を変えることによって、テーブル駆動テストでも使いやすいようにしています。この手法を使うことでモックをテストケース内で柔軟に使うことができるため重宝しています。 gomock自体の使用感ですが、モック量が多くなってきたタイミングでは、gomockに任せてしまえばよいので、テスト効率は上がりました。ただし、Interfaceに対するモック実装に慣れていない状態では使わないほうが良いかと筆者は思います。モック実装を自前で作れる状態になった上で 効率化のためのモック自動生成手段 として導入するプロセスが、Goらしいコードを学ぶ上で有益だったと実感しています。 まとめ 今回は、Goのユニットテストの実践しているTipsについて書きました。テストヘルパーは、ライブラリ使用状況・コード構成によって、提供の仕方はそれぞれ微妙に異なるかもしれませんが、一つアイデアとして参考になれば幸いです。 明日は、 id:tenkoma です!お楽しみに!
アバター
この記事は、「BASE Advent Calendar 2018」の3日目の記事です。 devblog.thebase.in 前の日 は id:yaakaito でした。Suggested change、便利ですよね。プルリクエストのレビューであまり重要ではないけれど気になる些細なコメントをするかどうか迷うというときに、悩まずにコメント代わりにパッチをサクッと送れるので、レビューに関わる人たちみんなが幸せになれるすごく良い機能だと思いました。ガンガン使っていきたいです! 3日目の今日は、エンジニアリングマネージャを担当している加賀谷が、「新任エンジニアリングマネージャ(私自身)を支えてくれた本と言葉」と題して、いくつか紹介していきたいと思います。 駆け出しマネジャーの成長論 - 7つの挑戦課題を「科学」する https://www.amazon.co.jp/dp/4121504933/ 駆け出しマネジャーの成長論 - 7つの挑戦課題を「科学」する (中公新書ラクレ) 作者: 中原 淳 発売日: 2014/05/09 メディア: 新書 私は2018年、エンジニアのグループマネージャとして活動してきました。それまではサーバサイドエンジニアとして、アプリのバックエンドを作ってきました。なので、まずはマネージャとは何をすればいいのか?というところからのスタートでした。そんなときに救われたのが人事をしている同僚の言葉と、その時にオススメされたこの本でした。 「例えるなら、サッカー選手から管理栄養士へのジョブチェンジです。加賀谷さんは今までサッカー選手としてメンバーとともに試合に出て、時に自分で点を取り試合に勝ってきました。これからは管理栄養士としてチームを勝たせて下さい。」 選手ではないことはまだ受け止められたのですが、チームの管理栄養士です。まだ、監督だったら戦術をどうしようとか、直接ボールを蹴らなくてもまだ試合を動かす術があります。それもできません。試合(開発現場)からかなり遠いイメージを持ちました。 結果的に「サッカー選手から管理栄養士へのジョブチェンジ」という比喩が、すごくしっくりきました。本の中で語られていた、マネージャの仕事 = Getting things done through others(他者を通じて何かを成し遂げる仕組みをつくること)とリンクしたからです。 手を動かさなくていいのか。自分でコードを書いた方がはやいのでは。その葛藤、分かるよ。というところから始まり、マネジメントとはなんぞや、全部並べるとこういう仕事がある、ただいきなり全部やらなくていいんだぞ(むしろやれないし、今まで周りにそんなマネージャいなかったでしょ)、これからマネージャになる君とその周りにはこういうことが起きるぞ、だからこうやって乗り越えていこうー。 少しチクっとするけどこれから現れる症状を和らげてくれる、ワクチン接種のような優しい本です。 エンジニアが「明日からマネジメントして」と言われたら – FiNC Engineering Blog https://medium.com/finc-engineering/12908151a41e BASEのプロダクトマネージャーは思想を持って戦略を立て、何を作り何を作らないのかをずっと意志決定し続けています。ディレクターはプロジェクトチームが最速で目的達成するために、やりたいことを具体的な戦術として言語化とタスク化し、チームの今とこれからの課題を明確にして優先度を決め進捗を管理しています。 そんな中、具体的な数値目標をOKRとして設定して日々追っていく事業系グループのマネージャと、私が担当しているエンジニアのグループのマネージャでは少し違うような気がしていました。グループ単体で達成できるようなOKRの設定が難しく、具体的に何をマネージメントするのかがまだピンとはきていませんでした。 Getting things done through others、だとしたらエンジニアのグループのマネージャとして何をすればいいのか。迷っていたときに、こちらの記事と図を見てすごくしっくりきました。 私がやるべきことは、この図の中のピープルマネジメントでした。各メンバーが活躍できる場所を整え、フロー状態に持って行くのです。そのためにまず、プロダクトマネージャーと密にコミュニケーションを取り、立ち上げるべきプロジェクトの検討とアサインを考える必要がありました。そして、我々が向かう目的地と次の電柱の場所が見えるように目標を咀嚼して伝え、メンバーにかける期待を上司や役員とすりあわせた上で伝えること、プロジェクト内のメンバーが健全な対話と共同作業ができているか見続けて、お互いの期待のズレがないかすりあわせることが仕事のうちの1つと考えました。 OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法 https://www.amazon.co.jp/dp/4822255646 OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法 作者: クリスティーナ・ウォドキー 発売日: 2018/03/15 メディア: 単行本 BASEでは目標設定のフレームワークとしてOKRを導入しています。各マネージャが全社OKRを因数分解するようにOKRを設定します。OKRとはなんなのか、良いOKRとはを知るためにはこの本が読みやすくて良かったです。茶葉を販売する架空のスタートアップに様々な困難が起き、エンジェル投資家のすすめでOKRを導入したがうまくいかず、そこからだんだんと変わっていくストーリーにも親近感が持てました。 Team Geek ―Googleのギークたちはいかにしてチームを作るのか https://www.amazon.co.jp/dp/4873116309 Team Geek ―Googleのギークたちはいかにしてチームを作るのか 作者: Brian W. Fitzpatrick , Ben Collins-Sussman 発売日: 2013/07/20 メディア: 単行本(ソフトカバー) 優れたソフトウェアはチームで作る、だとしたら優れたチームはどうすれば作れるのか。ソフトウェア開発はチームスポーツであり、技術と同じように人を考えることが大事。謙虚(Humility)、尊敬(Respect)、信頼(Trust)の3本柱を身につけよう、という本です。マネージャになって読み返したのですが、自分がいちエンジニアの時に読んだのとは見え方が全く異なっていました。1章に書いてあるうちの、優れたプロダクトを作る上で必要なプロセスの一部である、建設的な批判の仕方と受け入れ方、失敗からの学び方、集団のエゴを考える大切さについては特にです。 採用基準 https://www.amazon.co.jp/dp/B00B42SX70/ 採用基準 作者: 伊賀 泰代 発売日: 2012/11/09 メディア: 単行本(ソフトカバー) リーダーとマネージャの境が分からなくなってきたときに気づきを与えてくれました。 リーダーは引っ張る人でも何でもなく、チームの使命を達成するために必要なことをやる人のことで、そのリーダーが持つ問題解決リーダーシップは、メンバー全員が持つべき。その方が高い成果を生み出せるチームになる。なぜなら、お互いに背中を任せた上で何をすべきか自発的に動けるからです。 Team Geekにも書いてありましたが、時にマネージャはリーダーであることも期待されますが、リーダーは必ずしもマネージャである必要はないです。マネージャはメンバーのリーダーシップを育んでいくことで、よいチームになりプロダクトにより大きく寄与できるのではないかと思います。 エラスティックリーダーシップ ―自己組織化チームの育て方 https://www.amazon.co.jp/dp/4873118026 エラスティックリーダーシップ ―自己組織化チームの育て方 作者: Roy Osherove 発売日: 2017/05/13 メディア: 単行本(ソフトカバー) 同時に複数のエンジニアグループのマネージャをさせていただいたのですが、時にグループごとに課題がちがい、同じ振る舞いではうまくいかないことを学びました。この本ではチームの状態を、サバイバルフェーズ、学習フェーズ、自己組織化フェーズ3つで定義し、それぞれのフェーズでとるべきリーダーシップの方法を紹介しています。 ヤフーの1on1―――部下を成長させるコミュニケーションの技法 https://www.amazon.co.jp/dp/4478069786 ヤフーの1on1―――部下を成長させるコミュニケーションの技法 作者: 本間 浩輔 発売日: 2017/03/25 メディア: 単行本(ソフトカバー) メンバーと定期的に1on1をすることになり、1on1ってなんぞ?と読み始めた本です。マネージャになって最もお世話になった本かもしれません。 そもそも何を目的としてどうやったらいいかを良く理解していなかったので、この本のまんま通りにやってみて場数をこなすところから始めました。90度に近い配置で座り「最近話したいこと話せてますか」と話をしてみるところからでした。 最初は全くうまくいきませんでした。何を話していいのか分からず、場は沈黙ばかりでしたし(最初の頃、その沈黙が経験学習を促すために必要と勘違いもしました)、メンバーには1on1の必要性すら届きませんでした。 1on1は1回あたり30分、頻度は多いときは1週間に1度行っています。話している内容をMarkdownでメモでとりながら話し、最後に必ずマネージャにできることがないかリクエストの確認をします。1on1の最後にはメモをSlackのDMで本人だけに送り、気づきの確認と、次のアクションの確認をします。もらったリクエストはその日のうちに行動して、どうなったかを本人に連絡しています。 1on1のメモは MacのメモツールのDayOne でとっていたのですが、そのメモを数えたら311回分ありました。1on1をしていく上で大切だと思った準備や質問がいくつかできてきたので、それはまた別の機会に紹介したいと思います。 Getting things done through others、自分が直接携わらないサッカーの試合を勝利に導く上で、1on1はマネージャの強力なツールの1つだと思います。いいプロダクトを作りショップオーナーの支援につながる事が上のサッカーの試合の勝利だとすると、そのためにはチームメンバーがそれぞれ活躍していて、高く評価されていることが必要です。 エンジニアがやりたいことやできることの円と、プロダクトづくりでやりたいことの円があったとき、両方の円が重なったところが組織と個人の課題や目的が一致したところで、メンバーが活躍できることと考えることができます。なので、メンバーが活躍するためには、この両方の円を近づけていくように整えていく事がエンジニアリングマネージャの仕事の1つかなと、今は思います。また、両方の円を大きくすることでも重なる面が増えるので、こちらも仕事の1つと考えています。 これがエンジニアリングマネージャとしてのレバレッジの効かせ方のひとつ、自身のアウトプットの与える影響範囲を広範囲に及ばせる、ということなのではないでしょうか。 全然別の話ですが、日々1on1をしていくなかで金八先生やルーキーズの川藤先生を思い出しました。赴任してきていきなりみんなの前でああしようこうしようと理想を語ったところで、次の日皆がそれぞれの居場所を見つけて充実した顔をしている卒業式のようにはならないわけです。ドラマで1話ごとに1人と向き合い、強みと課題を見つけて成果を出せるように行動変容を促すようにサポートし続ける姿勢はマネージャとして見習わなければならないと思いました。 今年1年、気付けば1on1を通じてたくさんの経験学習をさせてもらっていたのは自分のほうでした。メンバーには感謝の気持ちでいっぱいです。ありがとうございました。 明日は id:khigashigashi です。お楽しみに! 「BASE Advent Calendar」の記事一覧はこちら。 devblog.thebase.in
アバター
これは「BASE Advent Calendar 2018」の2日目の記事です。 devblog.thebase.in Native Application Group の右京です。 ネットショップ作成サービス「BASE」では日々機能追加や改善の為、無数の Pull Request が作成され、レビューされています。今日はそんな中で知っているととっても便利な機能を紹介します。 レビューをしていたら typo をみつけてしまった... ありませんか?前後に真面目なレビューがあるとなんとなく指摘するのを躊躇してしまいますね...。シレッと修正するにも編集するなりブランチを切るなりして...はコンフリクトの可能性などもあり、コストが高く面倒ですよね。 そんなときはこれ、「Suggested change」です。 Incorporating feedback in your pull request - User Documentation これは最近 GitHub に追加された機能(public beta)なのですが、僕はこれを知ってからというものおすすめの機能の一つになりました。では、実際にこれを使って見つけてしまった typo をそっと修正してみます。 修正を送る側はとっても簡単 レビューでコメントをつけるときと同じ要領で行を選択して、ここクリックすると... 変更点に対してこのようなコメントが挿入されます ```suggestion function niec() { ``` この状態ではまだ typo したままなので、正しい内容に修正してプレビューしてみると... このように「Suggested Change」が作られます。そしてこれをコメントかレビューとして送信すると... こうなります!これで、この変更をパッチとして送っている状態になりました、取り込まれるのを待ちましょう。 取り込む側もすごく簡単 パッチをもらった側は、変更が妥当ならそれを取り込むことができます。「Commit Suggestion」から「Commit changes」で変更をコミットできます。 変更が取り込まれて、コミットが作られました。今回は自作自演したので僕だけがコミットしたことになっていますが、実際はこんな風に提案した側、取り込んだ側の両方がコミットに記録されます。 お互いに手間も無駄な気遣いも少なく、コードを少し良くすることができました。小さなことからコツコツと! Move Fast 今回は typo を例に出しましたが、文章の推敲、設定ファイルに対する小さな修正など、BASE のコードレビューシーンではすでにいろんなところで「Suggested changes」が活用されています。「提案を即座に取り込んでいける」という面が本当便利で、行動指針である「Move Fast」を体現した小回りの効く機能だと感じています。パッチをもらった側は uniposで #MoveFast すれば完璧 ですね! 以上、「Suggested changes」の紹介でした、この機能であなたも BASE にパッチを送りませんか? 明日は id:yuhei_kagaya です!楽しみですね! 「BASE Advent Calendar」の記事一覧はこちら。 devblog.thebase.in
アバター
これは BASE アドベントカレンダー 1日目の記事です。 devblog.thebase.in CTOの藤川です。ネットではえふしんと名乗っていて、会社でもえふしんさんと呼ぶ人が大多数です。 今年はテックリードの働きかけをきっかけとして、BASE社でもアドベントカレンダーを書いてみよう!という話になりました。皆様よろしくお願いいたします。 最近、エンジニアリングマネージャという役割が急速に普及し始めているので、今回はマネジメントの話について書いてみたいと思います。 IT業界にはエンジニア35歳定年説という都市伝説がまかり通っています。開発技術が進化してコードを書く労力が劇的に下がったにも関わらず、このことに恐怖してる人は少なくないようですし、何よりエンジニアがコードを書かなくなるような状態は望ましい状態ではないとされています。 一方で、成長するビジネスにおいては組織を作っていかないといけないのですが、エンジニアは、エンジニア出身のリーダーに評価してもらいたいという部分と、コードを書かないとエンジニアとして死ぬというイメージに矛盾があって、35歳定年説の不安と相まって、エンジニアのマネジメントのキャリアパスというのはどこか淀んでいる印象すらあります。 要は、マネージャの存在を絶対的に必要としているのに、自分がやるのは不安だという矛盾をはらんでいます。 これまでの転職志望者でも、現職で管理職になってしまったが、まだ技術の最先端に携わりたいと言う理由で面接にお越しいただく方もいて、転職後にマッチングに失敗するかもしれないリスクを取ってでも、転職せざるを得ない状況に追い込まれるというのは、エンジニアである僕らのキャリアにとっては辛い状況とも言えなくもないです。 この状況をポジティブに解決するのがエンジニアリングマネージャという役割だなと思い始めています。 エンジニアリングマネージャとは? エンジニアリングマネージャとは、ピープルマネジメントの一形態です。エンジニアリングマネージャの仕事を一言で言うと、 事業、プロダクトに貢献しながら、チームのエンジニアの活躍にコミットすることで、メンバーの評価を上げる仕事 だと考えています。チームのメンバーに活躍してもらって、高い評価を得られることが、マネージャ自身も評価され、それができなかったらエンジニアリングマネージャとしては評価が下がる仕事として考えられます。 チームを持ってる以上、事業の推進やプロダクトの開発責任も担うわけですが、自分がコードを書いて問題を解決してもエンジニアリングマネージャとしては評価されないわけです。 会社によっては、そもそもエンジニアリングマネージャは組織図上の上長ではない立ち位置であったりもするようです。メンターみたいな立ち位置でしょうか。 当社においては、組織図上の上司にあたるピープルマネージャがその任を担います。いずれにせよ、ネット系企業におけるピープルマネージャの役割は、サーバント・リーダーシップにのっとりメンバーを下支えすることで、メンバーの活躍をメイクできなければ、すぐにでも他社に転職されてしまう昨今の状況において、チームを支える、メンバーの活躍を導くという役割を担っていることを自覚しているかどうかこそが重要です。 メンバーの給料は成果に応じて昇給する会社がほとんどだと思います。その前提において、エンジニアリングマネージャはメンバーの状況を常にトラッキングし、1on1などを通じてメンタリングしながら、活躍をメイクし、どういう成果が評価可能なのか?を明確にし、今後はどういう期待をかけていくべきか?を共有し、メンバー自身がそれを実現していきます。 昇給というものは、結果そのものに支払われるのではなく、結果によってもたらされる次なる期待に対し人的投資として支払われるものですので、そのお膳立てをする役割だと考えます。評価権限を持っていることで、メンバーと踏み込んだ話をすることができるし、それだけの責任を負うことになります。 エンジニアリングマネージャは常に挙動をメンバーから評価されている エンジニアリングマネージャがメンバーの評価権限を持つ以上は、メンバーからすると、評価する人に値するか否かというのは常に評価されることになります。 その基準は、 評価する人としての人間性は期待できるか 自分のスキルを判断できる技術力、技術判断力を持っているか 日常の活躍をしっかり観察してくれている人か しっかり自分を高めてくれる指導をしてくれるか など、緩すぎても厳しすぎても成立しませんし、日常の言葉一つ一つにおいて常にチェックされていると言っても過言ではありません。 コードを書くような知的労働においては、ポジティブに脳内物質の分泌が行われるか否かが生産性の大多数を支配します。目的意識を持っていないとか、楽しくないという状況で良いアウトプットができるわけはありません。それが一文字直すだけの簡単な仕事でも、アーキテクチャから作り変えるような難しい仕事でも、やる意義を引っ張ったり、勇気を振り絞って困難に立ち向かうように問題をほぐしていくのはエンジニアリングマネージャの重要な仕事です。 このマネージャの下にいると自分は評価されないと思った時に取りうる行動は、他部署に異動するか、会社を辞めるかのどちらかであることが多く、マネージャと戦い上司を教育してまで状況をよくしようとしてくれる人は稀であることを考えると、マネージャは常に緊張感を持ってメンバーから信頼に値する評価を得られているのか?について考えていなければいけません。 よく言われる、マネージャになるとコードを書く書かない問題は、チームメンバーの成長にコミットする役割において、メンバーや会社から自分が評価されるために、目の前のコードを自分が書くことのトレードオフを考えて、状況状況でマネージャが最適な手立てを判断してもらえば良いと思います。どうしても危機的状況や問題がある時、または背中を見せる時に率先してコードを書くシーンもあるでしょうから、采配として手段を使い分けるのはエンジニアのリーダーだからこそできることです。 また、技術はどんどん変わってもいくので、メンバーから評価されるためのキャッチアップが必要と考えると、むしろメンバーのプルリクエストを通じて最新技術やエンジニアリングを学ぶ姿勢が必要であるとも考えます。そのためにもプロダクトや事業と連動した技術マネジメントを行う必要も出てくるハズで、基本的にエンジニアとして前向きな役割でしかないと僕は考えますが、皆さんはいかがでしょうか? エンジニアリングマネージャは「偉い」立場ではない ここまで書けば、エンジニアリングマネージャは、シニアなエンジニアみたいな役割に近く、旧来型の組織である「上司・部下」「偉い人」というイメージではないことに気がついてもらえるのではないでしょうか? むしろプロジェクトマネージャやディレクターに近い役割で、とても現場感が求められる仕事で、エンジニアリングにより踏み込んだ形でエンジニアの成長にコミットする役割であると考えます。 この役割になれるスキルはエンジニアとしてのスキルや事業に対する姿勢が信頼されていなければ、エンジニアであるメンバーから評価されることはないわけで、エンジニアのキャリアパスの一つであると考えられます。 そう書くと、この役割にはエンジニアのスキルがものすごく必要なのでは?と思われがちですが、ミニCTOとして技術で引っ張るテックリードとは違います。しかし、テックリードは卓越した技術力でチームへの影響を与え、如何に高いアウトプットを出すか?と言う部分にコミットする役割であると考えると、人を導くという点においてはエンジニアリングマネージャと向いているベクトルには大差はなく、何を「とくいわざ」とするかだけの話で、それぞれがチームで働くエンジニアに求められる職能として考えることが可能です。 そして、ここからが大事なことですが、必ずしもエンジニアリングマネージャは永遠にその立場でいる必要もなく、一定期間で別の人に入れ替わっても良いと考えています。期待するのは大人としてチームの活躍を支えるチームディレクションとしての役割です。ディレクションかプレーヤーかというのは、その都度、役割を入れ替えることはあってもよいと思います。 なによりそうすることで、たくさんのエンジニアが人の成長を支える仕事の難しさを知ることができるし、一度、エンジニアリングマネージャを経験した人は、より広い視野での仕事を期待できるわけなので、改めて現場でコードを書く役割にコミットしながら、開発プロジェクトマネジメントやメンバーの育成においてもエンジニアリングマネジメント力を発揮することが期待できます。 そうこうしてるうちに新しい事業アイディアが出てきた時には、そういう人が率先してエンジニアリングマネージャとしてチームを作れるようになることで組織のスケーラビリティは向上します。 このようにエンジニア組織全体としては、立場を入れ替え可能であることと、マネージャという役割をエンジニアとしてのキャリアの幅や柔軟性を高める役割として定義することで、プログラマ35歳定年説に代表されるような、コードを書かなくなって、エンジニアとしての死を迎えるみたいな不安な世界を終わらせることができるのではないかと考えています。 なお、これは、今自分が携わっているCTOにおいても全く同じです。あくまでも役割分担の一つとして柔軟に人を入れ替え、抜擢可能なチャレンジ文化を作ることは組織の柔軟性とスケーラビリティに影響が出るし、新たな役割への期待を常に作り続けることで、ビジネスが成長することを大前提とした組織運営の考え方として捉えています。 以上、えふしんでした! 明日は id:yaakaito です!お楽しみに! 「BASE Advent Calendar」の記事一覧はこちら。 devblog.thebase.in
アバター
こんにちは、BASEでサーバーサイドエンジニアをやっている、東口( @hgsgtk )です。つい最近、 Goを運用アプリケーションに導入する際のレイヤ構造模索の旅路 | Go Conference 2018 Autumn 発表レポート - BASE開発チームブログ というGoに関するエントリを書きましたが、PHPもしっかり書いてます。 今回は、特に弊社のメインプロダクトであるネットショップ作成サービス「BASE」のフレームワークの大部分を占めるCakePHPの国際カンファレンス CakeFest の協賛についてです。 さて、この度、BASE株式会社は、 CakeFest 2019 に、シルバースポンサー・ランヤードスポンサーとして協賛いたします。 cakefest.org CakeFest 2019 とは、PHP製フレームワークの一つであるCakePHPの国際カンファレンスです。年に一回開催されており、今回は初めて日本での開催です。 冒頭でも述べましたが、弊社はCakePHPをメインプロダクトのフレームワークとして採用し、日々CakePHPをベースとしたプロダクト開発を行っています。そのため、CakePHPコミュニティに貢献したいという想いから、初の日本開催となるCakeFest 2019に協賛することを決定しました。 また、CakeFestについて、日本で開催されると決定される前から、弊社の @tenkoma が、PHPカンファレンス関西2018にてCakeFestを紹介しておりました。 私も日本開催を待望しており、今回協賛という形でCakeFestに参加できることをとても嬉しく思います。 speakerdeck.com CakeFestに参加しましょう! 肝心な開催時期ですが、 @CakeDC の発表では、2019年11月の開催を予定しているとのことです。 We will be sponsoring #CakeFest2019 (November in Japan!) - and we hope that you will join us. Sponsorship opportunities are available! Get details here: https://t.co/TrndH05dtD #cakephp #cakefest #cakedc #php #phpconference pic.twitter.com/3BYksH59j6 — CakeDC (@CakeDC) 2018年9月19日 国際カンファレンスが日本開催される機会はそこまで多くないと思いますので、普段CakePHPを触っていない方もこれを機会に参加してみると新たな発見があるかと思います。 トーク応募始まってます! また、現在トーク応募が開始されています。 Have you submitted your talk for #CakeFest2019 ? Call For Speakers is now open!! If you are keen to be apart of the speaker line up for CakeFest next year, then NOW is the time to apply! CFP's close Jan 7th #php #phpc #Japan #CakePHP https://t.co/ghvbY0hho8 pic.twitter.com/3CPn12Wp2Y — CakePHP (@cakephp) 2018年11月27日 普段足が運びづらい国際カンファレンスが日本で開催されるということで、海外で英語で発表したいといった目標のある方はとても良い機会になるのではないでしょうか!
アバター
こんにちは。BASEで採用広報を担当している米田( @aiyoneda )です。 今年も残り一か月となりました。今年は、BASEとして初のAdvent Calendarを実施する運びとなりました。こちらの「BASE開発チームブログ」にて、12/1(土)から12/25(火)まで毎日記事を公開していく予定です! 記事一覧はこちらです。随時更新していきます。 devblog.thebase.in 初となる「BASE Advent Calendar」では、技術的な内容だけにとどまらず、BASEのプロダクトや開発方針、開発組織の運営に関わる内容を中心にカジュアルな内容も含めて幅広くお届けしようと思っています。 これをきっかけにBASEという会社やメンバーの雰囲気、またBASEの開発部門での取り組みについて知っていただき、興味を持っていただければ幸いです。 記念すべき初日は、取締役CTOの藤川( @fshin2000 )に筆を執ってもらいます!お楽しみに!
アバター
こんにちは、BASEランニング部で10kmマラソンなどに参加し、3kgほど体重が落ちたSRE Groupに所属しているデータベースエンジニアの植木です。おかげで甘いものが美味しいです。ちなみに次はハーフマラソンに挑戦です! 今回は会社のブログなどを書いてみます。弊社では、ネットショップ作成サービス「BASE」およびショッピングアプリ「BASE」を運営していますが、11月にメインDBをRDS for MySQL5.6からAurora(MySQL5.6コンパチブル)に変更しましたので、そちらの話を書かせていただきます。 何故Aurora? まず、弊社でAWSをメインに使っていたという背景があります。入社した際にはRDS for MySQLを使用しており、CTOの藤川も「AWSを使うならAuroraにしたい」という要望を持っていました。私自身、AWSをメインに使い続けるつもりであればAuroraは良い選択だと思っていました。 MySQLを使う場合、オンプレミスやMySQL on EC2の方がスピードや機能面でメリットがありますが一定以上のスキルのエンジニアを複数人確保し続ける必要があり、運用にそれなりの稼働が取られます。RDSを選択して運用負荷を下げる方針であるならば、いっそRDSだからこそ使えるAuroraのメリットを受ける方が良いと考えています。下記2点がOKならAuroraの選択を候補に入れて良いかと思います。 ・AWSにベンダーロックインしても大丈夫か ・MySQLの最新バージョンの機能よりもRDBの基本処理のスループットを重視するか 弊社運用を考えて、特にメリットと思った点 レプリカラグが発生しにくい ストレージの使い方が大きく変わっていますのでレプリケーションラグが発生しにくいです。 クラスターエンドポイントとカスタムエンドポイント HAproxyを使っての複数台レプリカのロードバランスを検討していたため、カスタムエンドポイントが移行直前にリリースされた事は助かりました。リーダーエンドポイントは弊社のように分析用レプリカを作っている場合にはそちらも参照先に入ってしまいます。書き込みはクラスタエンドポイントを指定して、読み込みはサービス用インスタンスだけを指定したカスタムエンドポイントを作成して使用しましょう。 クエリキャッシュ クエリキャッシュの作りがMySQLとは全く違うためかなり有用になっています。 無停止パッチ 稀にAWS側要件のパッチが発生するので助かります。 スループットの向上 MySQLの5倍程度との事。そこまでのスループットはまだ必要ではないですが、後述のログや監視機能を有効にするためにスループットが向上されている事は良いです。 Performance Insights MySQL Performance Schema設定を有効にする必要があるので、スループットに15%程度の影響が出るとの事ですが、専門じゃなくても負荷原因特定などが簡単にできるようになっていてとても便利です。MySQLでも使えるのですがスループットへの影響があるので躊躇っていたものをAurora移行を機に利用開始しました。 監査ログ ECというサービスを考えると監査は必要です。スループットに影響するのですがMySQLの監査設定と比べるとAuroraでは格段に影響が少なくなっているため利用する決断がしやすくなりました。ただし、ユーザー単位での記録くらいしかログ対象の選択設定ができないためCloudWatch Logが増える事は認識しておきましょう。 費用面 一台ずつの料金はMySQLの方が安いですが、AuroraのマルチAZは参照可能になっています。MySQL利用時にはサービス用のマスターレプリカ以外に分析・集計用DBを用意していましたが、AuroraではマルチAZ用インスタンスをそちらに割り振ることができます。そのため4台分必要だった費用が3台で良くなり費用削減ができます。 高速クローン 本番相当のDBをQA用に用意する事を進めているため、そちらで高速クローンが利用できると考えています。 Aurora注意点 続いて、Auroraの注意点です。基本的にMySQL互換ではありますが、少しAuroraで利用できない操作や違いがあります。 テーブルの圧縮機能が無い 明確に圧縮非サポートと書いてあるドキュメントは無かったのですが、非サポートです。移行時のスナップショットからのリストア時に非サポートと書いてあるドキュメントはあります。 圧縮コマンドは通るのですが、実際には圧縮されず気づきにくいのでご注意ください。 Parameter Groupが複数存在する インスタンス用とクラスタ用とに分かれます。Auroraはインスタンスとストレージが分かれているのでクラスタ用=ストレージ用と考えるとわかりやすいです。 Aurora Serverlessはどうなのか? テスト環境としてAurora Serverlessを利用できないか考えましたが、仕組み的にクラスタ用パラメータグループしか存在せずインスタンス用パラメータグループにあるinnodb_file_formatでbarracuda指定ができないのが痛いです。おそらく、アクセス時にdefaultパラメータグループを利用してインスタンスが立ち上がってくるためAWS側でdefaultの設定を変えてくれるか、barracudaがデフォルトになっている5.7互換のAuroraでServerlessを出してくれるかに今後期待です。 Aurora MySQL5.7互換はどうなのか? Aurora最新は5.7互換ですがPerformance Insightsなど新しい機能リリースが5.7にはまだされていません。また、移行パスがスナップショットからのリストアしかないため後述のレプリケーションを利用した移行ができず、移行時間が長くかかります。そのような状況ですので、今回は5.6互換のAuroraが最適だと判断しました。 事前作業 作業は大きく事前作業と当日作業とに分けられます。比率としては事前作業が8、9割で移行当日はそれほど作業がありません。重要なのは事前作業です。 テストに関して テストに関しては、開発環境・Staging環境を事前に移行してそこを使用して開発をやってもらう事で網羅テストとしました。開発環境を移行後に最低限の確認を各開発チームでしてもらい、その後二ヶ月ほど開発環境として使ってもらいました。 また、開発・Staging環境については後述のような移行構成は作らずに空いている時間にデータベースの変更処理でアップグレードするのが簡単で良いです。 本番環境事前構成の作成 オンプレミスでMySQLをある程度扱っていた方ならわかると思いますが、レプリケーションを利用した移行の事前構成です。下記のような構成を作ります。RDS for MySQLでは子レプリカまでしか作成できませんが、Auroraでは孫以下のレプリカ作成が可能になっています。そのためこのような構成を作る事でサービスに利用しているMySQLには影響を与えずに事前にAuroraの構成を作っておく事が可能です。 この際にほんのちょっと工夫するところとして、テンポラリの書き込み可能MySQLレプリカを間に入れる事をお勧めします。サービスに影響しない書き込み可能レプリカを入れることで、「mysql.rds_stop_replication」と「mysql.rds_start_replication」コマンドにてレプリケーションを操作し、レプリケーションを止めている間にテンポラリに対して巨大テーブルへのALTER文等を実施して事前にAuroraのテーブル定義をカスタマイズできます。弊社で言えば大きめのテーブルへのインデックス追加はこのやり方で実施しておきました。row_formatをDynamicに書き換えておきたい場合などはこの方法で実施すれば夜間メンテなどで長い時間サービスを停止する必要なく実施できます。 当日作業 当日の流れ 他社事例を見ると、ノンストップでの移行なども見受けられましたが、ネットショップ作成サービス「BASE」は60万店舗のショップに使っていただいているサービスのため、一時的に参照だけ可能になる事などが許される状況ではありません。そのため各ショップに連絡し、夜間メンテナンスタイムを入れさせてもらいました。ざっと下記のようなスケジュールです。 2:00:メンテイン作業開始 2:30:移行処理およびその他の追加作業開始 4:00:メンテ明け作業開始 4:30:社内限定アクセステスト 5:00:メンテ明け&最終テスト 5:15:終了 当日の移行作業 AuroraのMasterインスタンスを昇格します。その後、CNAME内のエンドポイント情報を全てAurora側に向ければ完了です。また、旧MySQLインスタンスは万一の場合にすぐに戻しができる必要があるため当日の削除はしません。どこからもアクセスできないSecurity Groupを作って入れ替えるかインスタンス名自体を変更してエンドポイント情報が変わるようにしてあげると良いです。弊社では後者を実施しました。インスタンス名変更は短時間でできますし、AWSコンソール上から見て名前が変わっている事が一目でわかるところが良いです。こちらで無事5時過ぎには作業完了しました。 使用感 マスターはCPU利用率が高くなります。CPUを使い切る最近の流れですので、そこはあまり気にせず。レプリカのCPUはそんなに変わりませんが、タイムラグは本当に減りました。レプリケーションの仕組みが以前と違うのでわかっていたとは言え、ここはとても嬉しいところです。マスターインスタンスのCPU利用率が高めになったため、監視システムのワーニング値はレプリカと分けるなど見直しをした方がいいですが、全体に急激なコネクション増加なども減り、良い結果に繋がっております。 下記が移行前後のCPU利用率の変化です。 緑:旧マスター 赤:旧レプリカ 青:新マスター 黄:新レプリカ また、下記がレプリカのラグです。単位がミリセコンドなので、非常に安定しています。 最後に 入社後、二ヶ月ほどでRDS for MySQL5.5からRDS for MySQL5.6への移行を実施し、それから1年経ってAuroraにたどり着きました。ようやく、一つ重めの荷物を下ろす事ができるような気がしています。まあ、そう言いながら来年はAurora MySQL 5.7互換やってたりしそうな気もしますが・・・。データベースとの付き合いはコードとの付き合いより長いと言います。今後もうまく回していきたいと思います。 BASEのSREチームではお客様に安心して買い物をしていただくために、サービスの安定性に責任を持ち、インフラ・アーキテクチャの改善に日々取り組み続けております。 ご興味を持たれた方いらっしゃいましたら是非ご連絡いただければ幸いです。 jobs.binc.jp
アバター
お久しぶりです、BASEでサーバーサイドエンジニアをやっている、東口( @hgsgtk )です。 BASE BANK というBASEの子会社にて 金融事業の立ち上げ を行っています。今回は、BASE BANKで行っているGo言語でのチーム開発について書こうと思います。 なお、このエントリーの内容について、2018年11月25日に開催された Go Conference 2018 Autumn にてLT登壇してきました。 発表資料については次のURLで公開しています。 https://go-talks.appspot.com/github.com/higasgt/gocon-lt/architecture.slide#1 今回、LTをCFPで2つ提出して、偶然2つ採択いただいたので、併せてDocker開発環境の整備についても次のスライドで発表させていただきました。 https://go-talks.appspot.com/github.com/higasgt/gocon-lt/realize.slide#1 Go製のApplicationをDocker上で開発する際に環境整備が喫緊の課題としてある方がいらっしゃればこちらの資料も併せてご覧いただくと参考にしていただけるかもしれません。 社内のGo言語の使用状況 今年の5月23日、「Go言語勉強会を始めたよ」といったエントリーを投稿していました。 devblog.thebase.in その際には、基本的にBASEのプロダクトはほとんどがPHPで構成されており、各開発者が一時的に使用するスクリプトをGoで書くケースがあるよといった使用状況でした。 現在は、新規機能をGo製APIとして実装・運用するプロダクト機能が出てきており、実際に運用開始しているものもあります。今回紹介する事例についても運用開始に向けて進めているものになります。 レイヤ構造模索の旅路 Go言語で運用を前提としたサービスを実装する場合に最初に頭を悩ませる、レイヤ構造実現のためのプロセスについてです。世の中には、Clean Architectureなどレイヤ構造についての解説記事が多数ありますが、結局は自分たちの要件にあったものを実現していく必要に迫られます。それをどう探っていったかという少し泥臭い話をしようかと思います。 なお、注記しておきますが、今回紹介する構成が「本当に正しいLayered Architectureである」などといった概念を正確に実現できているかという点に関しては保証できませんので予めご了承ください。 なぜレイヤ構造が必要だったか 様々必要・不要意見があるかと思いますが、筆者の環境では次のような理由にて必要だと感じておりました。 チームの開発成果物に統一的な構造をもたせて、どこで何が書いてあるかわかりやすくしたい 運用後のプロダクト改善を見越して、ビジネスロジックを入出力から分離した変更容易な設計としたい 運用を前提としない単純なスクリプトなどであれば、コードベースにレイヤ構造をもたせるといった検討は不要かもしれません。 しかし、今回は実際に運用しながらプロダクトとしてブラッシュアップしていく必要があるものであったため、上記の理由からレイヤ構造を考えていきました。 どのように進めたか 構造の概念図を仮定義 まず、最初に概念的な構成を図として定義するところからはじめました。実際に次のような簡易的な概念図を作成しています。 Layered Architecture を参考にしつつ、単一な依存方向となる構成とし、HTTPなどアプリケーションの外側の処理とビジネスロジックを分離する当初の目論見を形にできそうな構成を作成しています。 作っては壊す 上記の図を書いたからと言って実際に実装に落とし込み、かつ機能をそこに乗せてみないと本当に自分たちにとって必要なものだったのかわかりません。そのため、概念図をチームに説明した上で、作っては壊していくことを宣言していました。 スキルを上げていく チームが扱えるレベルを上げていかないことには、変更容易な設計となる成果物は難しいと考えます。そのため、その時点のチームで、ちょっとずつ背伸びしていく方針としました。そのため、後述しますが最初はシンプルな構成から仮定義した地点に近づけていく作業をしています。 実装を進めるにあたって ユニットテストに対する意味付け 今回、進めるにあたってユニットテストに対して次の意味付けをしていました。 設計に対するフィードバック材料 ユニットテストを書くことによって、「疎結合な実装になっているか」といったコード設計に対するフィードバックがある程度得られると考えます。そのため、「実装の肌感を増やすためのユニットテスト」という意味合いをもたせました。 大胆なリファクタリング 「作っては壊す」と上で書いたとおり、都度都度大胆なリファクタリングが発生しうります。大胆にコードベースをいじるためにユニットテストを書いています。(しかし、これは同時に筆者に大きく反省を残す瞬間が後ほど現れます。) パッケージの使用 Go言語を実際に扱うに当たり、標準パッケージでどれだけやるのかという議論はよく耳にするかと思います。筆者のケースでは、Go言語自体を覚えれば書ける状態にしたかったため、なるべく標準パッケージを用いる方針にしています。 後続のエンジニアに対する配慮という意味合いもありますが、同時に自分たちが、インターフェースの使い方など「Goらしく」実装するために必要な技法をプロジェクトを通じてある程度体得している状態としたかったためです。 そのため、最初は愚直に書いていき、理解度に合わせて順次便利になるライブラリ・機構を導入していっています。 実際のレイヤ構造 初版:Simple Model-Controller 一番最初の構造です。非常にシンプルなcontrollerとmodelしかない構造です。 ├── model └── controller この際は、GoのAPI開発にまずチーム全体で慣れるというところから始めたい意図があり、最小限の構成となっています。 model内には、データベースに対する技術的実装なども含まれており、インターフェースの使い方はまだこれからです。 第二版:Model-Controller + DIP 次に移動した構造です。repositoryとdatastoreという2つのパッケージが増えています。 ├── domain │ ├── model │ └── repository // + ├── infrastructure │ └── datastore // + └── controller ここで、model内にべた書きされていた、データベースに技術的実装をdatastoreパッケージとよんでいる箇所に移動し、repositoryという抽象レイヤを挟む構成となりました。実装イメージとしては次のようになります。 package repository type UserRepository interface { Save(userID int ) error } package datastore type UserStore struct { DB *sql.DB } func (s *UserStore) Save(userID int ) error { // 処理 } ここにて、インターフェースの扱い方について習熟するため、ユニットテストでも自前でモック実装を作成してもらい実装の流れを掴むということをしていました。 第三版:Layered Architecture + DIP ├── config ├── domain │ ├── model │ └── repository ├── infrastructure │ ├── datastore │ └── router ├── interface │ ├── controller │ └── middleware └── service // + layered architectureのapplication layerに該当 ここで、最初の概念図の構成要素がほとんど用意された状態になります。単一な依存方向となるようなLayered Architectureの構成とし、レイヤ間をより疎結合にするためドメインレイヤにインフラストラクチャが依存する抽象を設置しています。 また、処理の複雑化に伴いアプリケーションレイヤに相当する(※1)serviceパッケージを作成しています。これはトランザクションを扱うことが喫緊の課題として取り組みましたが後に記述する反省点の一つとなりました。 ※1 MVC + Serviceと捉えた場合はServiceレイヤと同等とも言えるかも知れません。 現在:Layered Architecture + DIP 2018年11月26日現在の構成です。 ├── common // + 全レイヤー共通で利用する機能群 │ ├── uuidgen │ └── validation ├── config ├── domain │ ├── model │ └── repository ├── infrastructure │ ├── clock │ ├── datastore │ ├── logger │ └── router ├── interface │ ├── controller │ └── middleware ├── service └── testutil // + テストヘルパー群 全レイヤ共通で利用するUUID生成などをcommonディレクトリとして集約し、テストヘルパー群をtestutil packageに集約しています。 実践した上での反省点 トランザクションの扱い トランザクションをどう扱うかを最初から考えておいたほうが良かったと反省しております。トランザクションをどう扱うによって大きくインターフェースが変わってくるかと思います。筆者の例の場合は次のような扱い方にしています。 package service type SimpleServiceImpl struct { DB repository.DBConnector User repository.UserRepository } func (s *SimpleServiceImpl) Run(userID int ) error { tx, err := s.DB.Begin() if err != nil { return errors.Wrap(err, "failed to begin transaction" ) } if err = s.User.Save(tx, userID); err != nil { // 引数に*Txを渡す wrapRollback(tx) return errors.Wrap(err, "failed to save user" ) } if err = wrapCommit(tx); err != nil { wrapRollback(tx) return errors.Wrap(err, "failed to commit transaction" ) } return nil } トランザクションはservice packageで扱います。その際、service package内で生成された*sql.Tx構造体をそれぞれのrepositoryの引数に渡す構造としています。 package repository type DBHandler interface { Execer Querier Preparer } type UserRepository interface { Save(db DBHandler, userID int ) error } その上で、repositoryでは、*sql.DB/Txをwrapした抽象型を作成し利用しています。なお、この構成については、 @codehex さんの もう一度テストパターンを整理しよう(WebApp編) - Speaker Deck という資料を大変参考にさせていただいています。 そして、この実装構成にするために、本当に大胆なリファクタリングが必要になり、チーム全員でそれぞれ自身の実装した機能をリファクタリングするという工数が発生してしまいました。 世の中で公開されているレイヤ構造のサンプルコードでなかなかトランザクションを扱っているものは少ない気がしていますが、実際にやるのであれば、最初にトランザクションを扱うコードをサンプルとして作ってみることはおすすめしておきます。 追記:Contextの扱いについて 頂いたリアクションの中で、トランザクションに合わせてcontextの扱いについての話が出ておりましたので追記しておきます。 こちらもトランザクションと同様にメソッドの第一引数に context.Context を渡す実装にしたほうがよいかと思います。 筆者が実際に context パッケージを用いた実装を行う際には、 @deeeet さんの次の記事を非常に参考にさせていただきました。 Go1.7のcontextパッケージ | Taichi Nakashima Golangのcontext.Valueの使い方 | Taichi Nakashima テストヘルパーを先回りして用意しておく ユニットテストを愚直に書いていくという方針は当初理解度を深める上ではよかったのですが、進んでいく中で共通で毎回やるアサーションなどがコピペ作業になっている状態が発生し、「テストを書くのが億劫になる」といった状態が生まれていました。 先回りして共通的なものをテストヘルパーとして提供して開発体験を損ねないということが重要だったなと思います。 実際例として、ResponseHeaderのアサーションなどのヘルパーなどを随時用意していっています。 // Example assertion response header func AssertResponseHeader(t *testing.T, res *http.Response, code int ) { t.Helper() if code != res.StatusCode { t.Errorf( "expected status is '%#v', \n but actual is '%#v'" , code, res.StatusCode) } if expected := "application/json; charset=utf-8" ; res.Header.Get( "Content-Type" ) != expected { t.Errorf( "expected Content-Type is '%#v', \n but given '%#v'" , expected, res.Header.Get( "Content-Type" )) } } また、テストに関するTipsは今回のGo Conference 2018 Autumnにて、 @ timakin さんが発表されていた、 Golang API Testing the HARD way - Speaker Deck という資料が大変勉強になるかと思います。 まとめ 今回のエントリでは、最初に構想を決め、実践しながら拡張して構想に近づけていく事例を紹介させていただきました。 すでにGo言語をバリバリ使用している現場の方であれば過程の振り返りに、これから使用を検討されている方にとっては一歩踏み出す参考となれば幸いです。
アバター
どうもお久しぶりです。BASEビール部部長の氏原です。最近急に涼しくなりましたね。ハイアルなベルギービールでも飲んで温まるといい季節ですよ。 さて、今回もビールの話はとりあえず置いておいて現在Data Storategy Groupで取り組んでいる内容として、今年に出たらしい論文「 Adversarially Learned One-Class Classifier for Novelty Detection 」を実装して商品画像フィルタにならないか試してみたことについてお話しようと思います。 One-Class Classifierとはあるクラスに属するか否かの判別器です。例えばある画像に写っているものがQRコードか否かとか、水着か否かとかです。 今回の話は一行で言えば、ショッピングアプリ「BASE」の商品検索などから意図と異なる画像をフィルタリングするためにAdversarially Learned One-Class Classifier(ALOCC)が使えないか試したという内容です。 背景 皆さんショッピングアプリ「BASE」を使ったことはおありでしょうか?数多くの商品が並んでいて、私は米とか肉とか買ってます。そんな中こんな商品たちを見かけたことはないでしょうか? またはおすすめショップのところのこういうのとか こちらは、お知らせをひとつの商品として登録するとこのように表示されます。本来的な商品画像の使用法とは異なりますが、登録自体は可能です。 これらの商品ではない商品がショップのページに出てくるのは特に問題ないのですが、もし検索とかで出てきたら探している商品と違うなぁと思ってしまうのではないでしょうか? この問題を解決するために商品ではない商品を自動で発見して検索等には出ないようにする機能を開発しています。そしてQRコード画像については現在検索等からの除外を開始しています。 今回お話するのはお知らせ等の文字画像の検出について今取り組んでいることの紹介です。 Adversarially Learned One-Class Classifier(ALOCC) for Novelty Detectionについて 単純に文字画像か否かの判別器を作ることを考えると以下のような問題があります。 文字画像ではない画像、という教師データをどう集める? 文字画像以外の画像では幅が広すぎる 文字画像とはなにか?がはっきりとわからない。人によって違う。 文字中心のポスターや、文字っぽいロゴや、サイズ表はどう扱うべきか そもそも文字画像のサンプルが少ない 圧倒的に教師データが不足している これらの問題が Adversarially Learned One-Class Classifier for Novelty Detection では軽減できそうだったので、使えそうかどうか試してみました。 実験結果に行く前にこの手法を簡単に解説しておきます。 ネットワーク構造 論文に乗ってた画像そのまま上げます。 ( M Sabokrou, "Adversarially Learned One-Class Classifier for Novelty Detection", arXiv.org, 2018 ) 構造としてはGANにインスパイアされたものになります。 D がDiscriminatorなのはGANと同じで、GANでGeneratorだった部分がReinforcerとなっています。ReInforcerは画像(元の画像にノイズを追加したもの)を与えられて、何かしらの画像を出力します。 DiscriminatorはReinforcerが出力した画像と元の画像を見分けるように学習させ、ReinforcerはDiscriminatorを騙すように学習します。 学習の目的 さて、では上記ネットワーク構造は何を意図しているのでしょうか?これは元画像との違いを検出する検出器を作ろうとしているのです。 学習に使う画像は検出したいターゲットのクラスの画像だけ、今回の私の用途で言えば文字画像だけいいのです。文字画像以外の画像というものを教師として揃える必要がありません。これはありがたいです。 教師を作るためにこれは文字画像、これは文字画像ではない…と延々とやってるとこれはどっちだろうかという画像が出てきてだんだん混乱してきます。なのでこれは文字画像にしたいなってものを集めるだけでいいのはとても楽です。 R はターゲットに似た画像を生成するようになります。でも特定のターゲットクラスの画像しか学習してないので、ターゲットのクラスではない画像を渡すとぐちゃぐちゃな画像が生成される、ということを期待しています。今回は文字画像を学習させるので文字画像っぽいものは綺麗に再構築されるけど、それ以外はぐちゃっとなってほしいというわけです。 D は再構築された画像、つまりターゲットから少しでも違う画像を見分けようとするため、違いに敏感になるように学習されます。この結果 D はターゲットとの違い、つまり新規性の検出器となります。 文字画像を1として学習させれば、 D が返す結果が1に近いほど新規性がない、つまり学習した文字画像に近い画像ということになり、0に近いほど新規性の高い見たことがない画像ということになります。 ( M Sabokrou, "Adversarially Learned One-Class Classifier for Novelty Detection", arXiv.org, 2018 ) 論文ではペンギン画像を学習させています。 画像を R に通した結果、ペンギン画像はペンギン画像として再構築されていますが、ペンギンでない画像はなにか汚い画像になっています。この再構築した画像を D に渡すと元の画像を渡すよりも綺麗にペンギンとそれ以外を判別できるようになると主張されています。 実装 さて、一応論文の 著者の方のgithub はあったのですが、tensorflowがゴリゴリでわかりづらかったのでpytorchを使って実装してみました。 Reinforcer Reinforcerの中身は受け取った画像をConv2dで畳み込むencoderと、Deconvするdecorderです。GANだとパラメータ受け取ってDeconvしていくだけなので、ここがちょっと違います。 import torch.nn as nn class Reinforcer (nn.Module): def __init__ (self): super (Reinforcer, self).__init__() self.encoder = nn.Sequential( nn.Conv2d( 3 , 64 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 64 , 0.8 ), nn.Conv2d( 64 , 128 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 128 , 0.8 ), nn.Conv2d( 128 , 256 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 256 , 0.8 ), nn.Conv2d( 256 , 512 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 512 , 0.8 ), ) self.decoder = nn.Sequential( nn.ConvTranspose2d( 512 , 256 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 256 , 0.8 ), nn.ConvTranspose2d( 256 , 128 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 128 , 0.8 ), nn.ConvTranspose2d( 128 , 64 , 3 , stride= 1 ), nn.ReLU(), nn.BatchNorm2d( 64 , 0.8 ), nn.ConvTranspose2d( 64 , 3 , 3 , stride= 1 ), nn.Tanh(), ) def forward (self, x): x = self.encoder(x) x = self.decoder(x) return x Discriminator DiscriminatorはGANと何も変わりません。受け取った画像を畳み込んで全結合層に渡すだけですね。 import torch.nn as nn class Discriminator (nn.Module): def __init__ (self): super (Discriminator, self).__init__() self.model = nn.Sequential( nn.Conv2d( 3 , 64 , 3 , stride= 2 ), nn.BatchNorm2d( 64 , 0.8 ), nn.LeakyReLU( 0.2 , inplace= True ), nn.Dropout2d( 0.25 ), nn.Conv2d( 64 , 128 , 3 , stride= 2 ), nn.BatchNorm2d( 128 , 0.8 ), nn.LeakyReLU( 0.2 , inplace= True ), nn.Dropout2d( 0.25 ), nn.Conv2d( 128 , 256 , 3 , stride= 2 ), nn.BatchNorm2d( 256 , 0.8 ), nn.LeakyReLU( 0.2 , inplace= True ), nn.Dropout2d( 0.25 ), nn.Conv2d( 256 , 512 , 3 , stride= 2 ), nn.BatchNorm2d( 512 , 0.8 ), nn.LeakyReLU( 0.2 , inplace= True ), nn.Dropout2d( 0.25 ), ) self.adv_layer = nn.Sequential( nn.Linear( 512 * 9 , 1 ), nn.Sigmoid(), ) def forward (self, img): out = self.model(img) out = out.view(out.shape[ 0 ], - 1 ) validity = self.adv_layer(out) return validity 学習 学習の仕方もGANとほぼ変わりません。 import torch import torch.nn as nn import torch.optim as optim import torchvision.datasets as dset ... device = torch.device( "cuda:0" ) netR = Reinforcer().to(device) netD = Discriminator().to(device) dataset = dset.ImageFolder(....) dataloader = torch.utils.data.DataLoader(dataset) criterion = nn.BCELoss() optimizerR = optim.Adam(netR.parameters(), lr=..., betas=...) optimizerD = optim.Adam(netD.parameters(), lr=..., betas=...) ... for epoch in range (n_epochs): for data in dataloader: images = data[ 0 ] # dset.ImageFolderのtransformでtorchvision.transforms.Lambdaつかって # 元画像とノイズ画像両方とれるようにごにょごにょしてる real = images[ "real" ].to(device) # 元画像 noised = images[ "noisy" ].to(device) # ノイズかけた画像 batch_size = real.size( 0 ) ############################ # Update D network ########################### netD.zero_grad() # 本物 label = torch.full((batch_size, 1 ), 1 , device=device) output = netD(real) errD_real = criterion(output, label) errD_real.backward() # 偽物 fake = netR(noised) label.fill_( 0 ) output = netD(fake.detach()) errD_fake = criterion(output, label) errD_fake.backward() optimizerD.step() ############################ # Update R network ########################### netR.zero_grad() # 偽物でDを騙す label.fill_( 1 ) output = netD(fake) errR = criterion(output, label) errR.backward() optimizerR.step() 結果 文字画像を学習させてみた結果、文字画像はそこそこ綺麗に再構築され、それ以外は結構ぐちゃっとなるようになったようです。 accuracy false positive false negative D ( X ) 0.92 0.06 0.02 D ( R ( X )) 0.87 0.03 0.10 むしろ判別性能は R をかました方が落ちてます。ただ、false positive、つまり本来文字画像じゃないのに文字画像と認識してしまった率は多少改善されました。文字画像は出したくないのですが、本来出したい商品画像が落とされてしまうのは避けたいのでfalse positiveはできる限り抑えたいところです。そういう意味ではそこそこ意味はあるのかなと思います。 感想 文字画像はそこまで綺麗にいかない、というか論文の結果の画像が綺麗にできすぎ。何か書いてない(もしくは私がちゃんと読めてない)工夫があるのかも。 Reinforcerは論文にはない工夫を追加した(まだ実験中なので内容は内緒)。そうしないとただのGANにしかならない。ターゲット画像以外の画像渡しても普通にそこそこ綺麗な画像が再構築される。 Batch Normarizationは偉大。 まとめ 文字画像を正確に判別できるようになるまではまだまだ道のりが長そうです。 false positive 3%といっても画像は1日に何万枚と上げられてきますので、間違いの絶対数はそこそこ多くなります。理想的には桁をもう一つ下げたいところです。 でも今回の判別器では、単純に文字画像判別器を作ったときに弾かれがちだった文字Tシャツはかなり高精度で文字画像ではなく商品であると認識できるようになりました。 こういうやつ ( RACCOONS ONLINE STORE より) 引き続き良きユーザー体験を提供できるように頑張っていきます。
アバター
BASEでエンジニアリングマネージャーを担当している加賀谷です。普段は採用に携わったり、1on1での経験学習の促進などを通じて、個人と組織のアウトプットが大きくなるようにサポートする仕事をしています。また、サービス開発に関わる体験を良くしていくこともしています。その中で今回は、静的コンテンツのCI/CDでしていることを紹介したいと思います。 静的コンテンツのホスティング 静的コンテンツは、サーバサイドでリクエストに応じてレスポンスする内容を作成しないデータです。主に、サイト内で使う画像、CSS、JS、ランディングページなどのHTMLファイルになります。これらのファイルはよく、AWSのS3に置いてホスティングして前段にはCDNを配置し、Webブラウザの同時接続数を考慮してサービスとは別のホストに分散したりしますが、BASEでもそうしています。 静的コンテンツ用のGitリポジトリを用意 CSSやJSが成果物となる開発はBASEにおいては主にデザイナーとフロントエンドエンジニアが担っています。以前はPHPもJSもCSSも同じリポジトリで開発していたのですが、今ではサービスのメインGitリポジトリとは別のリポジトリで開発〜デプロイをするようにしています。もちろんメインGitリポジトリからいっさいのJSやCSSを無くしているわけではなく、TwitterのBootstrapのようにコンポーネントとなるCSSやJS、ランディングページなど、メインから独立できるファイル群をこのような別リポジトリに入れています。 git pushでCircleCIからaws s3 syncする デプロイ先はS3で、 git push を契機にCircleCI上から aws s3 sync しています。以前は手動で本番S3に画像をアップロードする属人的な場面も少なからずあったのですが、今ではリポジトリの中に画像を入れてCircleCI経由でS3に配置するようにしています。 開発ワークフローと環境別のURL 静的コンテンツリポジトリの開発ワークフローはメインのGitリポジトリと同じようにしています。 develop ブランチから開発用ブランチ feature/xxx を切って開発 develop へプルリクエスト&マージ リリース時は develop から master へ git-pr-release でリリース用プルリクエストを作成 master を本番環境へデプロイ 環境ごとに静的コンテンツのURLが欲しいので、S3はそれぞれ用意してCircleCIが回るブランチで aws s3 sync 先を変えています。 develop ブランチの時にはステージング用のS3、 master ブランチの時には本番、それ以外のブランチは開発用S3へ対応させています。 .circleci/config.yml CircleCIではだいたい以下のようなことをしています。 checkout awscliをインストール リポジトリ内の特定ディレクトリ配下の各ファイルにACLとcache-control、content-typeをつけて aws s3 sync syncしたファイルのパスのCloudFrontのキャッシュを削除 また、アップロードされたファイルのURLなどをSlackへ通知して気付けるようにもしています。 version : 2 jobs : build : docker : - image : docker:17.12.0-ce-git environment : - TZ : "/usr/share/zoneinfo/Asia/Tokyo" - SYNC_OPTIONS : "--cache-control \" max-age=86400 \" --acl public-read --size-only --no-progress --delete" - S3 : "static-example-net" steps : - checkout - run : name : Install dependencies command : | apk add --no-cache \ py-pip=9.0.1-r1 \ curl \ curl-dev \ openssl pip install \ awscli==1.14.40 - run : name : Upload files to S3 command : | set -x tmpfile=`mktemp` for i in `cat .s3ignore | grep -v "^#" ` do IGNORE_OPTIONS="${IGNORE_OPTIONS} --exclude \ "**/${i} \" " done # リポジトリ内webroot/配下の各ファイルを適切なcontent-typeをつけてsync eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.css "${IGNORE_OPTIONS}" text/css` | tee -a $tmpfile eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.js "${IGNORE_OPTIONS}" application/javascript` | tee -a $tmpfile eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.json "${IGNORE_OPTIONS}" application/json` | tee -a $tmpfile eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.html "${IGNORE_OPTIONS}" text/html` | tee -a $tmpfile eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.png "${IGNORE_OPTIONS}" image/png` | tee -a $tmpfile eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \" * \" --include \" %s \" %s --content-type \" %s \" " "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.jpg "${IGNORE_OPTIONS}" image/jpeg` | tee -a $tmpfile if [ -s $tmpfile ] ; then tmpdir=`mktemp -d` split -l 30 $tmpfile $tmpdir/ for splited in `find $tmpdir -maxdepth 1 -type f`; do # CloudFrontからのキャッシュを削除 PATHS=`grep -ao "s3://${S3}/.*$" ${splited} | sed "s/^s3:\/\/${S3}//g" ` aws cloudfront create-invalidation ${AWS_PROFILE} --distribution-id ${CDN_DISTRIBUTION_ID} --paths ${PATHS} done fi サーバサイドのリポジトリと分けて開発、デプロイするメリット 1日に何度も本番環境へデプロイをする状況下においても、1度にデプロイするコード量が減り確認範囲が狭くなることで、よりカジュアルにデプロイできることがメリットかなと思います。サーバサイドと同じリポジトリで開発していたときには、例えばランディングページのちょっとしたスタイル変更にも、CircleCIでサーバサイドの全テストを回してBlue-Green Deploymentリリースフローをする流れを必要とするために比較的変更の反映に時間がかかっていましたが、これもなくなり手続き的にも早いデプロイができるようになりました。 Slackからgit-pr-release リリース用プルリクエストを作るときに、 git-pr-release コマンドを実行していますが、これをSlack Botでもできるようにしています。スマホのSlackアプリからも実行できるので便利です。GitHubのアカウントをSlackのアカウントに変換してメンションさせたり、リリースのサマリをSlackへ通知して変更がざっくり共有できるように工夫しています。 1. Botにメンションするとセレクトボックスを返すのでデプロイするリポジトリを選ぶ 2. 選択したリポジトリをその場で確認される 3. Yesを押すとgit-pr-releaseを実行 4. スレッドでメンションが返ってくる 5. 作成されたリリース用プルリクエストのURLが通知されるので、確認してmasterマージするとCircleCI経由で本番デプロイ まとめ 今回は、GitHubとCircleCIでS3へデプロイするワークフローを紹介しました。まだリポジトリ内には画像のようなバイナリファイルがあまり多くないのでリポジトリが肥大化してgit pull/pushが苦になることはありませんが、多くなってきたら Git LFS も検討してみたいと思います。 BASEの開発チームでは良いサービスをつくっていくために開発体験の改善も楽しみながら活動しています。ご興味を持たれた方いらっしゃいましたら是非ご連絡いただければ幸いです。 jobs.binc.jp
アバター
DataStrategyの齋藤( @pigooosuke )です。 ネットショップ作成サービス「BASE」は60万店舗のショップが利用しており、ショッピングアプリ「BASE」のユーザーは、新着商品、キーワード検索、関連商品、商品特集などを介して気になる商品を見つけることができます。今回、新機能として、検索ワードに関連するキーワードを表示することで、ユーザーの興味のありそうな商品にたどり着ける動線を機械学習を活用して実装しました。 DataStrategyチームは発足して間もなく、サービスドメインに適応した単語辞書がなかったので、新規で作成するところから始まりました。機械学習におけるデータセットのアノテーションについての知見が共有される機会が少ない印象もあり、折角なので今回私達が行ったデータ作りから実装までの流れをご紹介します。 概要 今回、どんなキーワードも意味的に近ければ、サジェストしても良いとはせず、ホワイトリストに登録されたキーワードのみをサジェストし、サービス品質を保てることを最低条件としています。また、関連キーワードにはいくつか定義が存在しますが、今回は類義語をサジェストする関連キーワードの開発を行いました。 大まかな手順は以下となります。 「BASE」で過去検索されたキーワードのデータからキーワードのリストを取得 前処理として、不適切な検索キーワード、優先度が低いキーワードを一定のルールで削除 有用なキーワードに対して、アプリに表示可能な単語かをアノテーション チェックがOKだったキーワードを登録したMeCab辞書を活用し、商品のタイトル・紹介文を分かち書きに分解 商品情報(タイトル+説明文)に対してWord2vecを通し、類似度の高い単語を調査し、関連キーワードとして表示 Word2vecについて 文字の通り、word(文字)をvector(ベクトル:n次元)で表現するために深層学習を行うモデルです。それぞれの単語はその周辺に出現する単語によって決められているという仮説に基づいて学習を行います。今回の記事のテーマではないので詳細は割愛します。 # 学習語彙での類似度の高い単語を出力 model.wv.most_similar(positive= 'ワンピース' , topn= 6 ) > [( 'フレアワンピース' , 0.8115994334220886 ), > ( 'ロングワンピース' , 0.7681916952133179 ), > ( 'ミニワンピース' , 0.7396647930145264 ), > ( 'aラインワンピース' , 0.7346851825714111 ), > ( 'vネックワンピース' , 0.7129236459732056 ), > ( 'レースワンピース' , 0.7060198783874512 )] アノテーション 実際にアノテーションを進めていく前に、きちんとルールを決めておくことがかなり重要です。 いくらルールを厳格に決めておいても、「このケースはOKで通してみたけれど、アノテーションを進めて全体感がつかめてくると、あれはNGのケースだった。」という手戻りは少なからず発生してしまいます。 ましてや、集団でアノテーションを実施する時の難易度はかなり高くなると感じています。 今回は、(幸いなことに?)アノテーション作業を一人で行ったので、可能な限りルールの統一が出来たと思います。 参考までに、今回設定したルールとしては下記の通りになります。 今回は、各キーワードについて3つのラベルをつけていきます。 NG単語ラベル アプリで表示するに耐えうる表現・ユーザーのUXを損なうことのない表現かをチェックしていきます。 OKとなった(ホワイトリストに登録された)キーワードのみアプリ上で表示されます。 基本的に最小商品単位になりうるものはOK 修飾語が3語以上つながっているものはNG(バックロゴ付き薄手コート: バック , ロゴ付き , 薄手 ) ブランド名が含まれているものは、社内ルール的にNG( ルイヴィトン , ディズニー ) 年号・数字・期間限定のキーワードが含まれているものはNG(例外で、季節感のあるものはOKとしている サマーサンダル , スプリングコート ) 色を表すものはNG(全色対応など、候補のノイズになる可能性が高いため) 口語表現がついているものはNG( ゆったり , もこもこ ) 単語単体で何を検索しているのか不明瞭なものはNG( 星 , クーポン , 名刺 ) ゆらぎ表現ラベル 単語表現のゆらぎは検索エンジンにはつきものです。類似表現が出てきた時、両単語を表示することはせず、どちらかの単語のみを表示します。これを ゆらぎ表現 として別途登録して入出力制御に活用します。 例: Pコート , ピーコート , キャミソール , キャミ など。 ゆらぎ表現 は検索件数が多いものを正解とする 鞄でいう、 〇〇バッグ , XXバック のように語尾が統一されないジャンルが存在するが、検索実績が多いキーワードを正解とする カテゴリーラベル これは個人の主観が大きく入るのと、全体のバランスを見つつゼロからラベルをつけていくので、大きく手戻りが発生する作業なかなか難しい作業です。こちらのラベルも出力制御に活用します。BASEで設定しているカテゴリーを参考にしつつ作業を進めました。 各単語について、何のカテゴリーなのかを手動でラベリング( コート , 皿 , 肉 ) カテゴリー階層は最大3階層まで アノテーションデータはこのように構成されています 入出力の制御(アノテーションラベルの活用) Word2vecは学習に使用された語彙に対して、それぞれベクトルが設定されています。上図のようにアノテーションで属性が判明している単語もあれば、まだ調査していない未知語も入力として入ってくる可能性があります。未知語はシャットアウトすれば良いという判断もありますが、対応語を増やすため未知語でも入力を認めることにしました。ただ、Word2vecにより大量の文章を学習しており、未知語の関連語として何が出力されるのかブラックボックス化されてしまう懸念がありました。この不透明さを解消するために活用したのがこれまでアノテーションしてきたラベルです。 NGフィルタは、ホワイトリストに登録された単語のみを出力するように配置されています。ゆらぎ表現フィルタは、ホワイトリスト内の単語を片寄せするために配置されています。カテゴリーフィルタは、出力するカテゴリーを統一させ、出力ノイズを減らすために設置されています。カテゴリーフィルタは、特に未知語や学習が不十分な語彙に対して大きく影響を与えます。 下でカテゴリーフィルタ設定前後を比較しています。 # カテゴリー補正前 model.wv.most_similar(positive= 'ポット' , topn= 6 ) > [( 'ピッチャー' , 0.6643306016921997 ), > ( 'ティーポット' , 0.62236487865448 ), > ( 'ボウル' , 0.6212266087532043 ), > ( '鉢' , 0.5690277814865112 ), > ( 'ティーカップ' , 0.5430924296379089 ), > ( '花器' , 0.5429580211639404 ), # カテゴリー補正後 model.wv.most_similar(positive= 'ポット' , topn= 6 ) > [( 'ピッチャー' , 0.6643306016921997 ), > ( 'ティーポット' , 0.62236487865448 ), > ( 'ボウル' , 0.6212266087532043 ), > ( 'ティーカップ' , 0.5430924296379089 ), > ( 'コーヒードリッパー' , 0.5426920056343079 ), > ( 'トレイ' , 0.5033057332038879 ), 鉢 や 花器 といった、容器ではあるものの、キッチン周りにはふさわしくない園芸向けのキーワードの出現が抑制されるようになっています。結果、類似度の閾値制御だけでは弾くことが難しいケースもカテゴリーを設定することによって解決することができました。 まとめ 今回は、アノテーションを機械学習の制御に活用してみたという内容をお送りしました。機械学習の学習結果をそのまま信用して活用するのではなく、アノテーションの結果を活用することで品質向上を行うことができました。ただ、アノテーションを闇雲に行うのではなく、どう活用していくのか事前の課題設定をきちんと立てることも重要そうですね。 BASEでは一緒にネットショップ作成サービスを開発・改善するエンジニアを募集してます。 機械学習のチームでは、様々なデータや技術を使ってECならではの開発を続けています。 ご興味のある方はぜひ遊びにきてください!! jobs.binc.jp
アバター
こんにちは、BASEのDesign Groupに所属している 吉岡 です。 ネットショップ作成サービス「BASE」のデザインや、2018年1月に設立されたBASE株式会社の100%子会社であるBASE BANKの株式会社立ち上げにデザイナーとして携わっています。 BASE BANK株式会社は、「銀行をかんたんに、全ての人が挑戦できる世の中に」をミッションとし、現在は関連事業の立ち上げを行っています。代表はBASE社と同じく鶴岡裕太が務め、メンバーはBASEと兼務している者もいます。オフィスも同じフロアにあり、カルチャーや思想なども含めてBASE社とかなり近いと言えます。 先日、BASE BANK社のロゴを作成しましたのでデザインプロセスの経緯を書き留めておきたいと思います。この記事を通じてBASE BANKがどのような会社なのか伝われば嬉しいです。 デザインの経緯 はじめに仮置きされていたロゴは下記画像の右のロゴでした。 左側がBASEのコーポレートロゴで、BASEロゴに合わせて、タイポグラフィをアウトライン化して入れたものでした。 'SE'に対して、BANKの'NK'がフォントボディが大きく、上手くはまりません。 綺麗にデザインしよう 、ということでここからロゴの作成をスタートします。代表の鶴岡に時間をもらいながらヒアリング/調整を行なっていきました。 まず、BASE BANKの立ち位置と印象を考える ヒアリングしたところの概要をまとめると、 「銀行をかんたんに」をミッションとするので安心感を与えるロゴにしたい ただ、固すぎないような自由なデザインも欲しい 印象としてはストリート感が少し欲しい という内容でした。 それを受けてデザインした初稿がこちらです。 なんとなく、Aが良いかな...という感じで、もう少し安定感のあるフォントが良いという流れに。ただ、この段階では、 しっくりくる提案ができていないような感触 がありました。 迷うロゴデザイン ここから2週間ほどかけて3回ほどアップデートしていくのですが、なかなか良い形で決めることができませんでした。 フィードバックとしては、 BASEとの親和性が感じられない ロゴの立ち位置が中途半端になっているのでフォントを変える意味がなさそう などでした。 この時点でスタートから既に3週間は掛かっていました。しかし、BASE社のタイポグラフィやテイストに捉われすぎて、壊せていない部分があったので、 今まで作ったロゴは全て壊してしまおう という結果に。 他のプロジェクトと並行して行なってきたのですが、この段階でロゴの作成に注力していきます。 コンセプト設計へ立ち返る BASE BANK のロゴは どういう立ち位置なのか。パーソナリティとしてどういう人なのか? を定めて、それに対しロゴを考えるフローに戻しました。 BASE BANKは何をする会社なのか? MTGやブレストで上がった内容から、BASE BANKはどういう会社なのかまとめました。 もっと銀行を簡単にしたい ITとデータを活用する 多くの人にチャレンジしてほしい 成長をフォローしていく 新しく立ち上げた会社 パーソナリティを決める どういう性格なのか、を決めます。 革新的(銀行を簡単にする、新しく立ち上げた会社) クール(ITとデータの活用) 安心感(チャレンジして欲しい、成長をフォローしていく) 以上の3点を踏まえた上で、ロゴを再考しました。 ラフスケッチしていく まずは手書きでラフスケッチしていきます。 パーソナリティを考慮し、すっきりとしつつ、安定感のあるようなロゴの形を簡単にスケッチしていきます。一旦思いつくままに書き込みます。 あまり綺麗ではありませんが...100~150個位は書いたかと思います。 後々の閃きにも繋がってくるので、どんなアイデアでも書き留めることが重要です。 データに落とし込む 手書きラフから、イメージに合うものを選び、Illustratorで描いていきます。手書きでは問題なさそうな案も、データにしてみるとイメージが違った..などの案はこの段階で絞り込んでいきます。 ベースと、方向性の決定 brandon grotesqueという割と新しいフォント。1930年代のgeometricフォントの歴史的なものを継承しつつ、ディテールに新しさを感じます。可読性もあり、安定感と革新性を感じられるものを選びました。 このベースを元にデザインの方向性を決定しました。 洗練された印象を持たせる白をベースに、フォントの持つ安定感は残し、角丸は消す BASEとの親和性を持たせるためにスクエアにこだわる 成長をフォローしていくイメージで、斜めのラインモチーフを入れる ロゴだけに注力し始めて2週間...出来上がったアウトプットがこちらです。安定感のあるボディと革新的な部分を共存させるようなイメージで作り上げました。 ここから、角度をつけた部分やディテールを調整していきました。 角度をつけた部分が急に見えるので30度から15度に変更したり、安定感が欲しい...ということで'A'のバー位置を下げたり、もう少し落ち着かせたい...角度をつけるロゴは1つに限定する、などの変更をしました。 スタートから1ヶ月半、遂に完成したロゴはこちらです...! アウトプットとしては、単にロゴだけなのですがBASE BANK社のこれから成し遂げたい事や思いを全て詰め込んでロゴを作れたかな..と思います。会社について、ひたすら考えることができた1ヶ月半でした。 文字領域と四角の領域は黄金比率1:1.618を使ってレイアウトを組んでいます。 全てをグリッドに合わせ過ぎると、小さく見える文字があるので若干調整しつつ、斜めの角度を0.5%単位、線幅など最終0.4ptの細かい調整をしています。 最後に コーポレートのロゴを作成する、という経験はインハウスデザイナーでもなかなかできないので良い経験になりました...!自ら手を上げれば、色々なデザインに携わることができる環境だと思います。 この記事でBASE BANKのチームで一緒に働くことに興味が出たという方は以下の募集からご連絡ください! jobs.binc.jp
アバター