Cybozu Inside Out | サイボウズエンジニアのブログ

 

サイボウズサマーインターン2017 報告その4〜Webサービス開発コース

こんにちは kintone開発チームの小林です。

サイボウズでは、8月から9月にかけて、5日間のサマーインターンを3回開催しました。サマーインターンは以下の5つのコースに分かれて行いました:

  • Webサービス開発
  • モバイルアプリ開発
  • UX/UIデザイナー
  • 品質保証/セキュリティ
  • Site Reliability Engineering

今回は、Webサービス開発コースについてお伝えします。

サマーインターンメンバーの集合写真

インターンの概要

Webアプリケーション開発コースは、東京、大阪、松山の3拠点で行いました。

東京

東京では、サイボウズが提供しているクラウドサービス「kintone」に対して、お客様から寄せられた要望などをもとに新機能のプロトタイプを作ってもらいました。
このプロトタイプは今後の製品への反映を目的にしています。去年のインターンでは、インターン生が実装したプロトタイプのうちの1つが実際に製品に取り込まれました。

プロトタイプの開発にあたっては、ただ機能を実装するだけではなく、

  • ユーザーストーリーやシナリオを想定して仕様を検討する
  • メンターと一緒にペア・プログラミングやコードレビューを行なう
  • 単体テストやブラウザテストを実装する

など、実際のkintoneの開発でも行われている一連のプロセスを体験してもらいました。

大阪

大阪でインターンに参加した学生には、東京と同じくkintoneの新機能のプロトタイプ開発をしてもらいました。
大阪ならではという点として、普段のkintone開発チームのリモート開発を体験していただく形になりました。
メンターが在宅勤務の日もあったり、東京とは様子が異なり大変だったこともあったとは思いますが、最終的には問題なく課題のプロトタイプを実装してくれました。
リモートで開発するのに必要なオンライン上のコミュニケーションを積極的に取ってくれたり、TV会議越しのミーティングに参加したり、実は一番サイボウズの開発らしさを感じたかもしれません。

松山

松山では、ネット連携サービスの路線検索を題材に、不具合改修と新機能のプロトタイプの作成を行いました。
不具合改修については対応したものを、レビュー、QAによる試験を通過させて本番環境に適用するところまでできました。
新機能のプロトタイプについては東京と同様、要望から仕様検討、実装、ブラウザテストまで実装してくれました。
今回の課題は実装の難易度が高めのものでしたが、5日間という短い期間のなかで素晴らしい実装をしていただきました。
実装だけでなく、この期間中に様々な社員とのコミュニケーションを取る機会があったので、その中でいろいろな学びを得たようでした。

プロトタイプ

インターン生がつくったプロトタイプをいくつか紹介します。

アプリ一覧の、アプリを名前順に並び替えて表示する機能

kintoneのポータル画面には、アプリの一覧が作成した順で表示されています。この並び順をユーザが変更することはできず、指定した順序でアプリを並び替えたいという要望を実現するのは難しい状態でした。
このプロトタイプでは、アプリの並び順を、作成順・名前順の2つから選択できるパーツを用意しました。
これによって、アプリ名に通し番号をつけるといった工夫することで、アプリの並び替えができるようにしました。

アプリを名前順に並び替えて表示する機能のスクリーンショット

書きかけのスペース本文の内容を自動保存する機能

kintoneでは、参加者を選んで「スペース」を作り、参加者同士で議論したり、情報を共有したりすることができます。
スペースには本文を設定することができますが、自動保存されることはないので、マシントラブルやブラウザの強制終了などで、編集中の本文の内容が失われてしまう可能性がありました。
このプロトタイプでは、数秒ごとに、スペース本文の内容を自動保存するようにし、不測の事態が起きても、編集中の本文の内容が復活するようにしました。

書きかけのスペース本文の内容を自動保存する機能のスクリーンショット

カレンダビューで、登録者によってレコードの色を変える機能

kintoneには、アプリに登録されたレコードをカレンダー形式で表示する「カレンダービュー」と呼ばれる機能があります。
このプロトタイプでは、研究室で予定を共有する方法として、各メンバーがアプリにレコードを登録してカレンダビューに表示することを想定し、研究室の教授など、特定の人物の予定だけ、色を指定して目立たせることができるようにしました。

カレンダビューで登録者によってレコードの色を変える機能のスクリーンショット

スケジュール

1日目

1日目は、インターンの概要についてオリエンテーションを行ったあと、サイボウズで用意していた課題のリストから課題を選ぶ作業と、kintoneを開発する上で必要な知識を身につける練習問題を解いてもらいました。

2日目

2日目は、選んだ課題に対して、ユーザストーリーや利用シナリオを検討して、仕様書にまとめる作業を行いました。

3日目〜4日目

3日目〜4日目は、実装を行いました。インターン期間中は、メンターがインターン生のとなりに座り、相談を受けたり、ペア・プログラミングやコードレビューを行いました。

5日目

5日目は、成果発表会を行いました。ひとりひとり、実装したプロトタイプの紹介やデモ、インターンを通じて学んだことなどを紹介してもらいました。

インターン生の感想

インターンに参加した方の感想をいくつか掲載します:

企業での大規模なシステム開発に携わるのは初めてで、沢山苦労することもありましたが、メンターの方々に丁寧に説明して頂き課題を終えることができました。
システム開発の知識だけではなく、サイボウズさんの社風や実務のこなし方を学ぶ事ができとても良い経験になりました。
社員の方々のチームワークも素晴らしく、実際にkintoneのアプリで連絡のやり取りを行っている点に驚きました。
サイボウズさんのサマーインターンに参加する事ができて本当に良かったです!


とても楽しくあっという間の5日間でした.
インターンの内容も,実際の業務をこなす内容だったのでCybozuで働いた際のイメージをしやすかったです.
業務もわからないことがあれば,メンターの方に教えていただけて不安なく業務をこなすことができました.
自分で調べる癖をなくすことはいけないことですが,調べても分らないことを聞くのにすごく聞きやすい雰囲気でした. 自分が聞かれた際に,ちゃんと回答できるようにこれからも勉強に励みたいです.
また,インターン中にも自分の課題を見つけることができたので,課題克服に向けて頑張りたいです.


今回のインターンでは、チーム開発のプロセスや製品テストなど普段体験出来ないようなことを沢山勉強することが出来ました。
今回使用したJavaScriptに関しては、基本的な部分を触った程度で、実務開発の経験はありませんでした。
実装課題に取り組んでみると、頻繁に躓くことが多々ありましたが、メンターの方が丁寧に説明して下さり、なんとか無事に課題をこなすことができました。
また、技術面だけではなく、ランチやイベントを通してサイボウズの社風・雰囲気を知ることができたり、社員の方がやさしく接してくれたりと充実した5日間を過ごすことができました。

まとめ

5日間という短い期間でしたが、それぞれのインターン生がしっかりユーザストーリーを考え、納得感のあるプロトタイプを作ることができました。
また、単に課題をこなすだけではなく、サイボウズ自体の文化や雰囲気にも触れてもらうことができたと思います。
学生の皆さんの今後の活躍を期待しています!

 

mdadmの検証中に発見したバグと今後の取り組み

はじめに

こんにちは、技術顧問の武内です。

Linuxにはmultiple devices(以下md)と呼ばれるソフトウェアRAID機能があります。この機能はmdadmというツールを使って管理します。サイボウズのSREチームは、Ubuntu16.04のmdadmを検証をした際に次のような2つのバグを発見しました。

  • mdのresync*1時に使うwrite intent bitmap*2(以後bitmapと表記)のサイズが所定量を超える場合、mdを構成するストレージデバイスに不良セクタを検出した際にbitmapを破壊する
  • mdのサイズ拡張時にbitmapの付与に失敗する

これに関して、本エントリでは、次のようなことをお伝えしたいと思います。

  • これらのバグが具体的にどういうものなのか
  • サイボウズはこのよう場合に、自分たちが使うものだけを修正するのではなくupstreamのOSSを修正するという方針
  • どういう思考プロセスに基づいてどのような流れで調査したのか

検出したバグ

バグ1: mdのresync時に使うbitmapのサイズが所定量を超える場合、mdを構成するデバイスに不良セクタを検出した際にbitmapを破壊する

影響

resync(正確にはmdから一度取り外したデバイスを再度追加する際のresync)をすると、resyncが正常終了するものの、実際には不正な状態になります。

原因

Ubuntu 16.04 から追加されたbad block list(以下bblと記載)というデータの配置位置の誤りによって発生しました。

bblはmdを構成する各デバイスに対して存在し、それぞれのデバイスに対するI/O時に見つかった不良セクタの一覧です。bitmap領域はデバイス先頭から8KB地点に配置されます。その一方、 bblはbitmap領域の後に配置されます。ただし、bitmap領域のサイズによらず、必ず同じ位置(デバイス先頭から40KB地点)に配置されます。このため、bitmap領域のサイズが32KBを超えると、bblへの書き込みが発生した際にbitmapを破壊します。

bitmapのサイズは次のようにして求められます。

256(bitmap領域のヘッダサイズ) + mdのサイズ/((bitmap内の1bitに対応する領域のサイズ(bitmap chunk size)。デフォルトは64MB)*8)

回避方法

以下のいずれかの方法を使います。

  • bitmapを使わない
  • bblを使わない
  • mdを構成するすべてのデバイスについて、次のような手順によってbblを再作成する
# mdadm /dev/md0 --fail /dev/sdc --remove /dev/sdc --re-add /dev/sdc --update=bbl

修正状況

upstreamにおいて修正済(commit 1b7eb672f7792313cc1517feaae8267575fc496b)です。Ubuntuでは16.10において修正済です。

今後の取り組み

Ubuntu 16.04に対してupstreamの修正をバックポートします。

バグ2: mdのサイズ拡張時にbitmapの付与に失敗する

影響

bitmap領域を使うことによるresyncのI/O量削減ができない。

原因

mdのサイズ拡張に伴ってbitmapのサイズを拡張する際に、bitmapのために使える予約領域(以後bitmap予約領域と記載)が少ないことが原因です。

bitmap予約領域のサイズはmdadm -Eコマンドによって求められます。以下20TBのmdを構成するデバイスの一つ、/dev/sdbについての値を計算する例です。

# mdadm -E /dev/sdb
...
Internal Bitmap : 8 sectors from superblock                      # (1)
... 
  Bad Block Log : 512 entries available at offset 96 sectors     # (2)
...

(2)の行の96から(1)の行の8を引いた88がbitmap予約領域のセクタ単位のサイズです。セクタの大きさは512バイトなので、バイト単位に直すと44KBです。bitmap chunk sizeを64MBとすると

64*1024**2*44*1024*8 = 24189255811072 [バイト]

約22TBほどまでしか拡張の余地が無いことがわかります。

修正状況

upstreamにおいても未修正です。Ubuntu 16.10以降にも同じバグが存在します。

回避方法

後述するbad block listを無効にすれば、bitmap予約領域は最大128KBまで増やせます。

今後の取り組み

upstreamのmdadmに対して修正を作成して、取り込んでもらう予定です。

サイボウズはOSSにフリーライドするのではなく、このような場合は積極的にupstream版を修正するという方針があります。過去にもMySQLやNginxを修正したという実績があります。

調査の流れ

注意: やったことを事細かに書くのではなく、重要な点に絞って、思考プロセスに重きを置いて説明します。

検証環境

  • ソフトウェアのバージョン

    • Ubuntu: 16.04
    • mdadm: 3.3
  • ストレージの構成

    • 2つのiSCSIデバイス(以下/dev/sd[bc]と記載)上に構築したRAID1のmd(以下/dev/md0と記載)
    • /dev/sd[bc]のサイズはそれぞれ20TB

バグ1の検出

/dev/md0を作成した後にbitmap領域のサイズとbbl領域の位置を確認すると、辻褄が合わないことがわかりました。

# ./mdadm --create /dev/md0 --level=raid1 --bitmap=internal --size=$((20*1024))G --raid-devices 2 /dev/sdb /dev/sdc
...
# mdadm -E /dev/sdc
...
Internal Bitmap : 8 sectors from superblock                          # bitmap領域の位置
...
  Bad Block Log : 512 entries available at offset 72 sectors  # bblの位置
       Checksum : d2f4290b - correct
...

mdを構成する全てのデバイスには、先頭から4KBの時点に、サイズ4KBのsuperblockというデータが仕様上存在します。bitmap領域、およびbbl領域のsuperblock先頭からの相対的な位置はsuperblock内の所定の場所に記録されています。それによると、bitmap領域のために使えるのは72-8=64セクタ、つまり32KBだということがわかります。

しかし、bitmapの実際のサイズを前述の式によって計算してみると、32KBを超えていることがわかりました。

$ ruby -e "puts 256 + 20*1024**4/(64*1024**2*8)"
41216                                          # 約40KB。81セクタを占める

これは、/dev/sdcに対するI/Oにおいてbblにデータが書き込まれると、bitmapの32KB以降のデータが破壊されることがわかります。

バグ1の修正状況確認

問題があることはわかったので、次はupstreamのmdadmにおいて問題が修正されているかどうかを確認することにしました。

$ git clone git://git.kernel.org/pub/scm/utils/mdadm/mdadm.git
...
$ cd mdadm
$ make -j16 >/dev/null
...
$ sudo
# ./mdadm --create /dev/md0 --level=raid1 --bitmap=internal --size=$((20*1024))G --raid-devices 2 /dev/sdb /dev/sdc
# mdadm -E /dev/ram1
...
Internal Bitmap : 8 sectors from superblock
...
  Bad Block Log : 512 entries available at offset 96 sectors
...
# 

今度はbitmap領域とbbl領域が重なっていませんでした。

次に、具体的にどのような修正なのかを確認しました。Ubuntu 16.04のmdadmはバージョン3.3にいくつかパッチを当てたものなので、mdadmの当該バージョンからHEADまでの間に修正が存在するかどうかを見ました。すべてのcommitを調査するのは大変なので、commit logのフィルタリングによって一次調査するcommitを絞り込みます。

$ git log --oneline mdadm-3.3.. | grep -i "bad.*block.*log"
e4467bc imsm: 4kn support for bad block log
c07a5a4 imsm: clear bad block from bad block log
6f50473 imsm: record new bad block in bad block log
bbab094 imsm: write bad block log on metadata sync
8d67477 imsm: parse bad block log in metadata on startup
1b7eb67 super1: fix setting bad block log offset in write_init_super1()                                                                                                                      86a406c super1: Do not create bad block log for clustered devices.                                                                                                                           968d2a3 md.4: replace "bad block log" with "bad block list"
$ 

怪しそうなcommit(1b7eb67 super1: fix setting bad block log offset in write_init_super1())を見つけたので、詳細調査することにしました。

$ git show 1b7eb67
commit 1b7eb672f7792313cc1517feaae8267575fc496b
Author: Artur Paszkiewicz <artur.paszkiewicz@intel.com>
Date:   Thu Nov 10 11:50:54 2016 +0100

    super1: fix setting bad block log offset in write_init_super1()

    Commit f79bbf4f6904 ("super1: don't put the bblog at the end of the free
    space.") changed the location of the bad block log to be after the
    write-intent bitmap, but a fixed offset was used and it can make bbl
    overlap with the bitmap, especially when using a small bitmap chunk.
    This patch changes it to use the actual offset and size of the bitmap.
    It also joins the cases for v1.1 and v1.2 superblock because the code
    was very similar.

何やらそれらしい修正です。この後具体的にソース調査をした結果、このパッチで間違いないことを確認しました。このバグについてはupstreamに存在するpatchをバックポートすれば解決しそうだということがわかりました。

バグ2の検出

バグ1の修正パッチをバックポートしたものを適用したmdadmを使って次のようなmdのリサイズ処理の検証をしました。

  1. bitmap(後述)を無効化: mdadm --grow /dev/md0 --bitmap=none
  2. iSCSIデバイスの容量を拡張: 今回の場合は4TB拡張
  3. mdのリサイズ: mdadm --grow /dev/md0 --size=max
  4. bitmapの再有効化: mdadm --grow /dev/md0 --bitmap=internal

すると、処理4において次のようなエラーメッセージを出して異常終了しました。

failed to create internal bitmap - chunksize problem.

ソース調査

このメッセージでソースを検索すると、Grow.c内のGrow_addbitmap()という関数内で失敗していることがわかりました。

...
                                        if (st->ss->add_internal_bitmap(
                                                    st,
                                                    &s->bitmap_chunk, c->delay, s->write_behind,
                                                    bitmapsize, offset_setable,
                                                    major)
                                                )                                              # この関数が失敗した
                                                st->ss->write_bitmap(st, fd2);
                                        else {
                                                pr_err("failed to create internal bitmap"
                                                       " - chunksize problem.\n");             # このメッセージが出た
                                                close(fd2);
                                                return 1;
                                        }
...

さらに調査したところ、st->ss->add_internal_bitmap()というのはsuper1.c内のadd_internal_bitmap1()という関数だということ、およびその中で呼ばれるbitmap予約領域の最大サイズは128KBに制限されていることがわかりました。

static int
add_internal_bitmap1(struct supertype *st,
                     int *chunkp, int delay, int write_behind,
                     unsigned long long size,
                     int may_change, int major)
{
...
                if (creating) {
                        offset = 4*2;
                        room = choose_bm_space(__le64_to_cpu(sb->size));      # ここでbitmap予約領域を計算
                        bbl_size = 8;
...
...
static unsigned long choose_bm_space(unsigned long devsize)
{                                                                                                                                                                                                    
        /* if the device is bigger than 8Gig, save 64k for bitmap usage,
         * if bigger than 200Gig, save 128k
         * NOTE: result must be multiple of 4K else bad things happen
         * on 4K-sector devices.
         */                                                                                                                                                                                          
        if (devsize < 64*2) return 0;
        if (devsize - 64*2 >= 200*1024*1024*2)
                return 128*2;
        if (devsize - 4*2 > 8*1024*1024*2)
                return 64*2;
        return 4*2;
}
...

さらにbblはbitmap予約領域の後に配置されるのではなく、上記add_internal_bitmap1()の後に呼ばれるwrite_init_super1()において、現在のbitmap領域の直後に配置されることがわかりました。

static int write_init_super1(struct supertype *st)
{ 
        ...
                /* work out how much space we left for a bitmap */
                if (sb->feature_map & __cpu_to_le32(MD_FEATURE_BITMAP_OFFSET)) {        # ここは真
                        bitmap_super_t *bms = (bitmap_super_t *)
                                        (((char *)sb) + MAX_SB_SIZE);
                        bm_space = calc_bitmap_size(bms, 4096) >> 9;                    # bm_spaceは現在のbitmapのサイズ
                        bm_offset = (long)__le32_to_cpu(sb->bitmap_offset);
                } else {
                        bm_space = choose_bm_space(array_size);
                        bm_offset = 8;
                }
        ...
                               sb_offset = st->minor_version == 2 ? 8 : 0;
                        sb->super_offset = __cpu_to_le64(sb_offset);
                        if (data_offset == INVALID_SECTORS)
                                data_offset = sb_offset + 16;

                        sb->data_offset = __cpu_to_le64(data_offset);
                        sb->data_size = __cpu_to_le64(dsize - data_offset);
                        if (data_offset >= sb_offset+bm_offset+bm_space+8) {            # ここは真
                                sb->bblog_size = __cpu_to_le16(8);
                                sb->bblog_offset = __cpu_to_le32(bm_offset +
                                                                 bm_space);             # bitmap予約領域ではなく現在のbitmapのサイズをもとにbblのオフセットを計算している

          ...
}

upstreamにおける修正確認

upstreamのmdadmにおいて実機確認をしたところ、Ubuntu16.04と同じ結果になりました。このため、このバグは未修正であることがわかりました。

修正案の検討

仕様(RAID superblock formats - Linux Raid Wiki)の確認によって、bblはどこに配置してもいいことがわかりました。この仕様の話と、これまでの調査の結果から、bblがbitmap領域の直後ではなく、bitmap予約領域の後に配置されるようにするという改善案が考えられます。詳細については今後upstreamのmdadmの開発者達と協議して決める予定です。

 

Cy-PSIRTの紹介と脆弱性報奨金制度の近況──「Cybozu Meetup #7 セキュリティ」開催報告

こんにちは。 コネクト支援チームの風穴(かざあな)です。

今回は、8月22日に開催した「Cybozu Meetup #7 セキュリティ」についてレポートします。

f:id:cybozuinsideout:20171015150653j:plain

Cybozu Meetupとは?

「Cybozu Meetup」は、サイボウズのエンジニアらと気軽に交流する場として企画している、ミートアップイベントシリーズです。会場はサイボウズのオフィス(今のところ東京と大阪)なので、会社の雰囲気や社員の様子を、実際に肌で感じて頂ける機会でもあります。

東京オフィスは毎月1回、大阪オフィスは3カ月に1回程度のペースで開催していて、これまでに以下のようなテーマで計6回開催してきました。

これまで開催されたCybozu Meetupのレポートは、「カテゴリ:Meetup」で一覧することもできます。

セキュリティ

今回のテーマは、Cybozu Meetup初登場の「セキュリティ」。

最初に、品質保証部の山西 生恵(@MtIkutea)が、Cy-PSIRT(Cybozu Inc. Product Security Incident Response Team=サイボウズ製品のセキュリティ品質向上のために活動しているチーム)の活動内容を紹介しました。

f:id:cybozuinsideout:20171015150810j:plain

続いて、急遽お休みをいただいた大塚に代わって再び山西が、サイボウズが2014年から実施している「脆弱性報奨金制度」の近況を報告しました。

f:id:cybozuinsideout:20171015150905j:plain

トークセッションの後は、立食形式の懇親会へ。今回は、著名なバグハンターの方々が参加してくださったこともあって、とても熱量の高い懇親会となりました。

f:id:cybozuinsideout:20171015150934j:plain

f:id:cybozuinsideout:20171015150950j:plain

f:id:cybozuinsideout:20171015151005j:plain

今後も、東京オフィスでは、毎月開催を予定しています。ご興味ある方は、connpassグループ「Cybozu Inside Out」をフォローしてくださいませ。

「こんな話を聞いてみたい!」というご要望も、随時、受け付けています。お気軽にお寄せください。

f:id:cybozuinsideout:20171015151027j:plain

この日のtweetを以下にまとめてみました。参考までに。

ではまた。

 

サイボウズサマーインターン2017 報告その3~SREコース

みなさんこんにちは。SREチームの内田(@uchan_nos)です。

今年もサイボウズではエンジニア向けのサマーインターンを行いました。 今年から「SREコース」が新設されたので、その実施報告を行います。

f:id:cybozuinsideout:20171019191621j:plain

インターンの概要

インターンの全体的な雰囲気は 報告その1~品質保証・セキュリティコース にあります。全体では3回に分けて実施されましたが、SREコースはもろもろの事情により第1回目(8月21日~8月25日)と第3回目(9月25日~9月29日)で実施しました。

SREコースでは、いくつかこちらでネタ案を提示し、学生さんに選んでもらうようにしました。 3人の学生さんに、この3ネタをやっていただきました。

  • 初動調査システムに機能を追加する
  • cybozu-go/kkokを使って、人間にやさしい通知システムを作る
  • 各種メトリクスを利用してボリュームの枯渇時期を予測する

次に参加される方の参考になるかもしれないので、3つのネタを簡単に紹介します。

初動調査システムに機能を追加する

f:id:cybozuinsideout:20171019191742j:plain:medium:left

初動調査システムは、SREチームが自社DC向けに開発しているシステムです。 障害1が発生すると、障害発生個所に関連した機材の状態を自動的に観測し、人間に通知してくれます。

このシステムに、サーバ機のハードウェア状態を検査するコマンドを実行させる機能を追加するというネタをやってもらいました。 ついでに、少し前から発生していた「VMへのSSHがなぜか失敗する」というバグの修正もやってもらいました。

異常が無くても通知する

もともとのネタの内容は、初動調査システムがハードウェア状態の調査コマンドを実行し、その結果を通知内容に含める、というものでした。

ネタの設計をするために初動調査システムのソースコードを読んでもらうと、あることに気付きました。 それは 初動調査システムはハードウェア状態の調査コマンドを実行しているが、普段はエラーが無いので何も出力がない という事実です。

ということで、内容を急きょ「エラーが無いときでも検査した事実を載せる」と方針転換し、実装してもらうことに。 ここから得られる教訓は 異常が無くても、異常が無いことを明示的に出力すべし ということでしょう。 システムが自動でやってくれていることを、人間が再実行する無駄をなくすために大切なことですね。

kkokを使って、人間にやさしい通知システムを作る

f:id:cybozuinsideout:20171019192212j:plain:medium:left

cybozu-go/kkok2とは、SREチームが作っているOSSの1つで、メッセージをイイ感じに配送するサービスです。 障害発生時などに警告メッセージを送る需要があるのですが、そのメッセージングを制御したいという需要から生まれました。 例えば、今からメンテナンスするので警告が発生することが分かりきっている、というような場合に、手軽に警告メッセージを抑制したいです。

メッセージが3日連続したら通知する

cronで定期的に何らかの検査をして、エラーが見つかったら通知する、ということはよくあります。 本当はエラーを自動解決できるのが理想的ですが、そうなっていない場合も多々あります。

エラーがクリティカルなものであれば、エラーが発見されたときに通知を飛ばせば良いでしょう。 しかし、1回だけエラーになってもクリティカルではないが、エラーが一定以上続いたら気づきたい、というケースもままあります。

そういうケースで人間が通知を受け取ったときに「ええと、昨日も来てたっけ?」と、過去の通知を探すのは割とつらい作業です。 kkokを使って、N日間連続で同じ警告が来た時に人間に通知を飛ばす、というフィルタを試作していただきました。

ボリュームの枯渇時期を予測する

f:id:cybozuinsideout:20171019192242j:plain:medium:left

サイボウズクラウドには、毎日お客様のデータが溜まっていきます。 当然、ボリューム3の空き容量はどんどん減っていくので、ある閾値以下になったらボリュームを自動的に拡張するシステムが動いています。

ボリューム拡張をお客様のアクセスが多い時間帯に行うと性能が劣化する場合があるので、 自動拡張システムはアクセスが多くなる日中帯は避けて拡張を行います。 また、負荷が高い作業が行われているときは拡張しないなどの配慮がなされています。

自動拡張システムを監視する

まれに、それらの配慮が何日も発生してしまい、ボリューム拡張がされないことがあるかもしれません。 また、実は自動拡張システムにバグがあって全然拡張されない、という事態が発生する可能性もあります。 自動拡張が間に合わないペースでボリュームが消費されることもあり得ます。

ということで、ボリュームの空き容量の時系列データを利用し、向こう1カ月以内にボリュームが枯渇するかどうかを予測するプログラムを作成していただきました。 Jupyter Notebookで試作品を作ってもらい、良い感じに予測するプログラムが出来上がりました。

インターンの感想

どのネタもコーディングを含むもので、一部は我々のコードベースを読む必要があるものでした。 それを実質3日間でやり遂げる必要がありましたが、3人の学生さんはちゃんと成果を残していただけました。素晴らしい! しかも、一部の成果物は実際に本番環境に取り込まれ、現在も元気に動いております。

たくさんの学生さんと触れ合うことができ、メンターとしても非常に楽しいインターンでした。 3日間で何らかの成果を出してもらうためのマネジメント方法を探求したりだとか、いろいろ学ばせていただきました。

3人の写真を見てみるとペンギンが高確率で出てきますね。 エンジニアはペンギンと触れ合いたくなるものだということでしょうか?


  1. モニタリング用URLへのアクセスのエラー率が閾値を超えたりすると障害とみなされます。

  2. 読み方は「ケーコック」。もちろん「警告」に由来しています。

  3. LinuxのLVMを利用して構成した論理ボリュームです。

 

(日本語訳) Migrating Garoon codebase to PHP 7

(※注 この記事は2017年8月18日に公開したMigrating Garoon codebase to PHP 7の日本語訳です。)

Garoon開発チームは 一年前にGaroonのソースコードのPHP 7移行作業を開始しました。 Garoonはソースコードも多かったり、もともとPHP 4で書いていたものをPHP 5に移行していたりするので、 移行作業は大変でしたが、移行することで大きな改善が行えました。 メリットの一つは性能改善です。GaroonをPHP 7 で動かしたときのベンチマークは、PHP 5.6と比べると33%改善しました。

そしてまた、php.netによると、PHP 5.6のアクティブサポートは2017年1月19日に終了しています。 ソース: http://php.net/supported-versions.php

この記事では、Garoon開発チームの遭遇した現象と、どうやってPHP 7に移行したかをお伝えします。

互換性のない変更を検出するためのツール

PHP 5から PHP 7に移行するとき、ソースコード全体から"下位互換性のない変更"を見つける必要がありました。私たちのコードがPHP 7で正しく動作することを確認するためです。 ソースコードを調べてPHP 7で動かすために修正が必要なコードを指摘してくれるツールがいくつかあります。その中にPhanとLintがあります

Phan

PhanはPHP用の静的解析ツールです。MITライセンスで公開されています。

環境の準備

PHP 7がインストールされた CentOSマシンを用意します。それから、以下のコマンドでcomposerをインストールします:

curl \-sS https://getcomposer.org/installer \| php \-\- \--install-dir=/usr/local/bin \--filename=composer

Phanとphp-ast拡張のインストール

まず、php-astのソースコードをcloneします:

git clone https://github.com/nikic/php-ast.git

次に、php-ast拡張をビルドして、phpに追加します。以下のコマンドを順番に実行してください。

cd php-ast
phpize
./configure
make install
echo 'extension=ast.so' > /etc/php.ini

それから、Phanをインストールして以下のコマンドで実行してください:

git clone https://github.com/etsy/phan.git
cd phan
composer install
./test
ln \-s /phan/phan /usr/local/bin/phan

Phanを実行して下位互換性のない変更を見つける

私たちは以下の内容のようなbashファイル(例: testPhan.sh)を作成しました:

COMMAND="phan \--backward-compatibility-checks \--ignore-undeclared \--quick"
find /repo/source \-type f \-regex '.*\.\(php\)' \-print0 \| xargs \-0 \-n1 \-P 1 \-I % bash \-c "$COMMAND % "

ファイル中の"/repo/source"をチェック対象のソースコードのパスに変更してください。それから実行します:

sh testPhan.sh

Lint

LintはPHPのコマンドラインオプションです。PHPの文法チェックに使われます。

前述のCentOSのマシンで以下のコマンドを実行し、PHPのソースコードをLintでチェックしました:

find . -name "*.php" -print0 | xargs -0 -n1 -P8 php -l

E_STRICT通知の深刻度の変更

E_STRICT通知の深刻度がPHP 7で変更されています。 すべてのE_STRICT通知がほかのレベルに移動しています。

状況 新しいレベル / 挙動
Indexing by a resource E_NOTICE
Abstract static methods Notice removed, triggers no error
"Redefining" a constructor Notice removed, triggers no error
Signature mismatch during inheritance E_WARNING
Same (compatible) property in two used traits Notice removed, triggers no error
Accessing static property non-statically E_NOTICE
Only variables should be assigned by reference E_NOTICE
Only variables should be passed by reference E_NOTICE
Calling non-static methods statically E_DEPRECATED

私たちのコードでもこれらのいくつかは影響がありました。

Signature mismatch during inheritance

"Signature mismatch during inheritance"通知は以下のようなケースで通常発生します: 一つ目のケースは、関数/メソッドのパラメータの数が継承時に異なる場合です。例:

<?php
class Foo{
    function method($a){

    }
}
class Bar extends Foo{
    function method(){

    }
}

上記の例の出力結果はこちらです:

Warning: Declaration of Bar::method() should be compatible with Foo::method($a) in /in/YoDHq on line 11

この例の実行結果はこちらで確認できます。

二つ目のケースは、関数/メソッドのパラメータの型が継承時に異なる場合です。例:

<?php
class Foo{
    function method($a){

    }
}
class Bar extends Foo{
    function method(array $a){

    }
}

上記の例の出力結果はこちらです:

Warning: Declaration of Bar::method(array $a) should be compatible with Foo::method($a) in /in/fSIbS on line 11

この例の実行結果はこちらで確認できます。

私たちはPhanを使ってコードをチェックし、PhanSignatureMismatch や PhanSignatureMismatchInternal として検知された問題を修正しました。

このタイプの問題の修正方法は、変更対象の基底クラスやサブクラスの中のロジックに依存します。

Only variables should be assigned by reference

例:

<?php
error_reporting(E_ALL);
function foo(){
    return 'string';
}
$temp =& foo();

上記の例の出力結果は以下のようになります:

Notice: Only variables should be assigned by reference in /in/vXUIZ on line 6

この例の実行結果はこちらで確認できます。

上の例では、'&'演算子が"Only variables should be assigned by reference"というE_NOTICEを引き起こしています。PHP 7の環境でコードがコンパイルされた場合は、エラーが検出されてしまうので、以下のように'&'演算子を削除して修正する必要があります:

<?php
error_reporting(E_ALL);
function foo(){
    return 'string';
}
$temp = foo();

しかし、私たちのコードには'&'演算子がたくさん使われており、すべて修正するには時間がかかりそうなことがわかりました。 この問題を解決するため、CentOS上にあるツールを導入しました。 まずはじめに、E_NOTICEメッセージをphp_error.logに出力するため、以下の手順を行います:

1. 以下のスクリプトをwebアプリケーションに追加して、実行します:

set_error_handler(function($errno, $errstr, $errfile, $errline){
  file_put_contents('/tmp/php_error.log', implode(',', [$errno, $errstr, $errfile, $errline]) . PHP_EOL, FILE_APPEND);
});

2. "php_error.log"は以下のようになっているはずです:

2048,Only variables should be assigned by reference,/var/www/project/code/xxx1.php,12
2048,Only variables should be assigned by reference,/var/www/project/code/xxx2.php,121
2048,Only variables should be assigned by reference,/var/www/project/code/xxx3.php,26
2048,Only variables should be assigned by reference,/var/www/project/code/xxx.php,194

このツールは以下の手順を実行します:

  1. tmpフォルダの中からsystem-private-から始まるフォルダの最新のものを見つけます。
  2. tmpフォルダ中のphp_error.logから"assigned by reference"という文字列をgrepで検索します。そして、ファイル名と行番号を一時的な結果として出力します。
  3. 2.の結果をもとに、"=&"、"= &"、"= & "を"="に置換します。
#!/bin/bash

systemd_private_name=$(ls -t1F /tmp/ | grep systemd-private-* | head -n1)
php_error_path="/tmp/${systemd_private_name}tmp/php_error.log"

if [ ! -f $php_error_path ]; then
    echo "File is not found"
    exit;
fi


grep "assigned by reference"  $php_error_path  | sort | uniq -c | while IFS=',' read -ra line ; do
    sed -ri ${line[3]}'s/(\$.+)(\s+=\s*&\s*)(.+())/\1 = \3/g'  ${line[2]}
done

Only variables should be passed by reference

PHP 7では、関数のパラメータを参照渡しにした場合、エラーが発生します。 この問題はPHPのフレームワークやユーザ定義メソッドを使用するときに発生します。

この問題を解決するため、私たちは関数の値を保存するための変数を宣言し、 直接呼ばれていた箇所にこれを渡しました。以下がその例です:

例: explodeで作られた配列をarray_popに渡すと、Strict Standards警告が発生する:

<?php
   $fruit = array_pop(explode(",", "Orange,Apple,strawberry"));
   echo $fruit;

修正後のコード:

<?php
   $fruits = explode(",", "Orange,Apple,strawberry");
   $fruit = array_pop($fruits);
   echo $fruit;

この例で注目していただきたいのは、explode関数用に変数を割り当ててからその変数の参照をarray_popに渡すことで、Strict Standards警告を回避している点です。

PHP フレームワークのメソッド

例:

<?php
   $file_name = "abc.txt";
   $file_extension = end(explode('.', $file_name));

上記の例の出力結果はこちらです:

Notice: Only variables should be passed by reference in /in/Cv65t on line 3

この例の実行結果はこちらで確認できます。

修正後のコード:

<?php
   $tmp = explode('.', $file_name);
   $file_extension = end($tmp);

私たちはPhanを使ってコードをチェックし、PhanTypeNonVarPassByRef として検知された問題を修正しました。 しかし、Phanが調べるメソッドはPHPフレームワークのものだけです。 ユーザ定義関数は PhanTypeNonVarPassByRef として検知されません。以下の例をご覧ください。

ユーザ定義メソッド

例:

<?php
function getArray() {
    return [1, 2, 3];
}

function squareArray(array &$a) {
    foreach ($a as &$v) {
        $v **= 2;
    }
}

// Generates a warning in PHP 7.
squareArray((getArray()));

上記の例の出力結果はこちらです:

Notice: Only variables should be passed by reference in in /in/HYONN on line 14

この例の実行結果はこちらで確認できます。

プロジェクト内のユーザ定義メソッドに対して調査する際は、notepad++などのエディタで正規表現による検索が利用できます。

function.*\&\s+\$.*\)$

上記の例の出力結果はこちらです:

E:\yourproject\comment_util.php (3 hits)
    Line 30:     function getFollowByUser( & $user, & $article )
    Line 61:     function _setNameRowSet( & $rowset, $inverse = FALSE )
    Line 119:     function _row2object( & $row )

参照メソッドを検索結果から見つけた後は、手動で関数呼び出しを修正します。

Calling non-static methods statically

staticではないメソッドのstatic呼び出しはPHP 7で非推奨になり、将来的には削除される可能性があります。

例:

<?php
class foo {
    function bar() {
        echo 'I am not static!';
    }
}

foo::bar();

上記の例の出力結果はこちらです:

Deprecated: Non-static method foo::bar() should not be called statically in - on line 8
I am not static!

私たちはPhanを使ってコードをチェックし、PhanStaticCallToNonStatic として検知された問題を修正しました。 一番簡単な修正は、staticではない関数をすべてstaticな関数に変更することです。

PHP 4コンストラクタ

PHP 4コンストラクタは、クラス内で定義されているメソッドでクラスと同じ名前のものです。

PHP 7ではPHP 4コンストラクタが定義されていると、E_DEPRECATEDが発生します。 メソッド名がクラス名と一致し、クラスが名前空間内になく、PHP 5のコンストラクタ(__construct)がない場合、E_DEPRECATEDが発生します。

例:

<?php

class Filter {
    function Filter() {

    }
}

new Filter();

出力:

Deprecated: Methods with the same name as their class will not be constructors in a future version of PHP; Filter has a deprecated constructor in /in/IDpi7 on line 3

この例の実行結果はこちらで確認できます。

以下のように、PHP 5のコンストラクタを使用する必要があります:

<?php

class Filter {
    function __construct() {

    }
}

new Filter();

大きなプロジェクトの場合、手動での修正は時間がかかります。PHP-CS-Fixerという便利なツールがあります。これを使うと自動で修正することができます。 GaroonではPHP-CS-Fixerを使って500以上のファイルを修正しました。

このツールでPHP 4コンストラクタを修正する方法を記載します。下記のコードをPHP-CS-Fixer 2.2.1で実行してください。

1. まず、PHP-CS-Fixerの設定ファイルを作成します

.php_cs
<?php

$source_dir = 'c:\repo\projectX\source';
$finder = PhpCsFixer\Finder::create()
    ->in($source_dir)
    ->files()->name('*.csp')->name('*.php')
;

return PhpCsFixer\Config::create()
    ->setRiskyAllowed(true)
    ->setRules(array(
        'no_php4_constructor' => true
    ))
    ->setFinder($finder)
;

注意: Garoonでは、PHPファイルの拡張子として".csp"を使用しているので、 ".csp"を設定ファイルに追加しています。

2. PHP-CS-Fixerを実行します

php php-cs-fixer.phar fix --config=.php_cs --verbose

このツールの詳細はこちらをご参照ください: https://github.com/FriendsOfPHP/PHP-CS-Fixer

内部関数の変更

PHP 7ではいくつかの内部関数が変更されています。substr()関数もその一つです。

例:

<?php
$foo = substr("foo",3);
if($foo !== FALSE){
    echo "PHP 7\n";
    echo gettype($foo); //string
}
else{
    echo "PHP 5\n";
    echo gettype($foo); //boolean
}

上記のコードの$foo変数の型は、PHP 5ではbooleanですが、PHP 7ではstringです

この例の実行結果はこちらで確認できます。

この問題を解決するため、strlen関数で文字長をチェックしました。

<?php
$foo = substr("foo",3);
if(strlen($foo) == 0){
    echo "Run on both PHP 5 and PHP 7\n";
    echo gettype($foo); //boolean on PHP 5 and string on PHP 7
}

この例の実行結果はこちらで確認できます。

まとめ

GaroonではPHP 5.6からPHP 7に移行することでパフォーマンスが向上し、コードも読みやすくなりました: 例えば、社内の性能検証では33%の性能向上が確認できています。 私たちは、移行作業を分けて一つずつ実施していました。これにより、起こりえる問題に対処しながら、短期間で移行作業を完了できました。