takaha.siの技術メモ

勉強したことをお伝えします。ちょっとでも誰かの役に立てればいいな…

scalaのtraitの危険性

踏んだのでメモ

Scalaにはtraitという機能があります。

traitは「それ単体で動作することはなく、他classを拡張(mixin)するか、他classから継承されることによって動作するエンティティ」と定義して良いと思います。

JavaのInterfaceにたとえられることもありますけども、JavaのInterfaceが実装を持つことができないのにたいして、Scalaのtraitは実装を持つことができます。

Scalaのtraitは便利で、mixinしてclassに機能をどんどん追加していくという事ができます。つまり、実質、多重継承のようなこと事ができてしまいます。

が、この機能、利便性と引き換えに危険性が伴います。乱用すると複雑な継承関係を容易に生み出してしまい、コードの可読性が低下します。そして、最悪NullPointerException(ヌルポ)でプログラムが死にます。ScalaはOptionさえ適切に使ってればヌルポは根絶できるのだと無邪気に考えていた私は甘かったです。

以下にコードの例を示します:

class RPC(target: String) {
  def send(msg: String): Unit = {
    println(s"sent! ${msg} to ${target}\n")
  }
}

class RPCBinder(rpc: RPC) {
  def send(msg: String): Unit = {
    rpc.send(msg)
  }
}

trait Base {
  val rpc: RPC
  val binder: RPCBinder = new RPCBinder(rpc)
}

trait BaseDefault { base: Base =>
  val rpc = new RPC("Node0")
  def sendHello(): Unit = {
    binder.send("Hello")
  }
}

trait BaseFunc1 { base: Base =>
  def sendFunc1(): Unit = {
    binder.send("Func1")
  }
}

class BaseMode1 extends Base with BaseDefault with BaseFunc1 {
  def doSend(): Unit = {
    sendHello()
    sendFunc1()
  }
}

object Main {
  def main(args: Array[String]){
    println("start!!")
    val base = new BaseMode1()
    base.doSend()
    println("end!!")
  }
}

これは、基底のtraitであるBaseに対して、Base traitに限ってmixinできるBaseDefault, BaseFunc1を定義し、BaseMode1でBaseDefault, BaseFunc1をmixinしています。

このコード。実行すると、NullPointerExceptionが起こります。

[thoth@~/programs]$ scala bbbb.scala
start!!
java.lang.NullPointerException
        at RPCBinder.send(bbbb.scala:9)
        at BaseDefault.sendHello(bbbb.scala:21)
        at BaseDefault.sendHello$(bbbb.scala:20)
        at BaseMode1.sendHello(bbbb.scala:31)
        at BaseMode1.doSend(bbbb.scala:33)
        at Main$.main(bbbb.scala:42)
        at Main.main(bbbb.scala)

まあちょっと考えれば、このコードがなぜヌルポが引き起こすのかはすぐわかると思います。trait BaseでRPCBinderをnewしたときに(15行目)参照しているtrait Baseのval rpc(14行目)がまだ決定されておらず、RPCBinderにnullが放り込まれてnewされちゃうんですね。

trait Baseのval rpcはインプリされていないとコンパイルエラーが出るからヌルポは回避できるのでは?と思いきや、trait BaseDefaultの方でインプリ(19行目)されており、BaseModel1でmixinされてしまっている(31行目)ので、コンパイルは通ってしまうわけです。

そもそもこんな意味不明なコードを書くんじゃない!というのはもちろんそのとおりです。しかし、脳死プログラミングしているときでさえこんなコードには絶対ならない!と断言はできないと思いますし、なまじコンパイルが通ってしまうのでエラーは実行時に当該のパスを踏むまで見抜けません。厄介です。ちなみに私はリファクタリングをしているときにこれに陥りました。

というわけで、scalaのtraitは注意して使わないといけないなと思いました。mixinがされているコードをレビューするときは、こういうエラーを引き起こす潜在因子がないかどうかちょっと考えてみるべきかもしれません。

なお、このコードは、trait Baseを以下のように変更すれば問題なく実行されます。

trait Base {
  val rpc: RPC
  lazy val binder: RPCBinder = new RPCBinder(rpc)
}

RPCbinderをnewするときにlazyをつければ、意図したとおり、ヌルポはでずに動きます。

動くには動きますが、コードとして明示的に記述されない初期化の順番というものを、コードを読む人に意識させてしまうという点でこのコードが良くないということに変わりはないと思います。

こういうコードを書くべきではない。陥らないように注意すべきであるというそもそも論に変わりはないでしょう。

p.s.

lazyは、Scalaに対して初期化のコードの実行順番の変更を行うものでありますが、使い方によってはもっとヤバイ自体に陥ります。それはデッドロックです。以下に例が上げられています。

gist.github.com

ヌルポはまだ可愛い方で、プログラムが落ちてくれるのでわかりやすいですよね。

10GbE時代に向けたNAS環境を構築する

時代はテンジー

8年前ぐらいにQNAP社のTS-439 Pro II+を購入してから、ずっとこれで満足してました。しかし、システム領域のFlashが不穏なエラーを吐き出しはじめたのでそろそろ買い替え時かと思い立ちました。容量もそろそろいっぱいになってきましたし。。。

そこで、今回、なるべく安価に10GbEのLAN環境と10GbE対応NASを構築しましたので構成を紹介したいと思います。

もうずいぶん前に「自作PCはやんない!」と心に決めてたんですが(時間がもったいないから)Mellanox InfiniBand ConnectX-5 EDR HCAがほしい!!!!!!!!!!…じゃぁなかった10GbEが欲しいなあとかZFSを使いたいからメモリは最低最悪でも16GBは欲しいなあとか考えてると、QNAPのNASの場合なかなかヘビーなお値段になります。そこでやむなく、パーツを柔軟に取捨選択でき、結果安価に済ませられそうな自作の道をえらびました。

家庭環境のNAS10GbEなぞ必要なのか?と訝しがる人がいるかもしれません。しかし、今どきの高速なNVMe SSD、具体的にはSamsung SSD 970 EVO Plusなどですと、シーケンシャルライトで2.4GB/secぐらいの速度はふつーにでます。RAIDを組めばもっと速度は出るでしょう。これはすでに1Gbpsを優に超える速度です。

NVMe SSDRAIDを組んだNASというのは、流石に個人ユースではまだちょっとハードルが高いかもですが、HDDのRAIDであったとしても1Gbps以上の書き込み速度は普通にでます。例えば、ZFSでHDD4台のRAIDZ2だと、メモリからのコピーで、350MB/secぐらいの速度はでます。この速度もすでに1Gbpsを上回っています。

つまり、今、広く普及している1GbpsのLAN環境ではストレージへの書き込み速度を十分にカバーできているとは言えません。もはや時代は10GbEです。テンジーの時代なのです。

NASのパーツリスト

Slack上で@yoshikigと喧々諤々の議論の末。以下のパーツセットで行くことにしました

# 品名 商品名 リンクなど
1 ケース U-NAS NSC-810A Server Chassis http://www.u-nas.com/xcart/product.php?productid=17640
2 M/B Supermicro Micro ATXマザーボード X11SSH-TF-O
3 CPU Intel Xeon E3-1275 v6 3.8 GHz Quad-Core LGA 1151 Processor
4 Memory ECC U-DIMM(Unbuffered DIMM) Micron DRAM 288pin DDR4-2400 CL17 32GB(16GB x 2)
5 M.2 SSD Samsung SSD 500GB 970 EVO
6 Power Supply ザワード [Enhance] FLEX ATX規格電源 [ 80PLUS GOLD認証・最大出力450W ] ENP7145B-07YGF
7 ケーブル AINEX ATX用電源延長ケーブル [ 15cm ] WAX-2415B-BK
8 ケーブル サンワサプライ TK-PWSAD2 [シリアルATA電源変換アダプタ 15pinオス→4pinメス] https://www.yodobashi.com/product/100000001001037761/

まずケースを決定。@yoshikigが見つけてきてくれました。U-NAS社のNSC-810A。このケースにはホットスワップできるSATAのHDDスロットが8ポートついています。大きさも家庭に置けるレベルのサイズ感。つや消しのマットブラック塗装が醸し出す高級感。剛性。すべて文句なしです。

次にこのケースに乗るM/Bを決定。SupermicroのX11SSH-TF-Oをチョイス。このM/BにはデフォルトでRJ45の10GbEが2ポートもついてます。また、それとは別に1Gのポートも一個ついてます。そしてなんといってもIPMIが使用できます。これは外せません。

しかし、欠点もあります。使えるCPUは7th/6th GenのCore i3か、CeleronPentiumXeon E3-1200 v6/v5となんだかビミョーなラインナップです。Core i7も動かせるらしいという未確認情報もありますが、試してないのでわかりません。

また M.2 SSDインターフェイスフォームファクタが2260というこれまたビミョーなIFです。2260のフォームファクタSSDは選択肢がほとんどなく、あったとしても怪しい無名メーカのものばかりです。さて、ここでカンのいい人は気づいたかもですが、上述のパーツリストにあるSamsung SSD 500GB 970 EVOのフォームファクタは2280です。つまり、このM/Bには乗っけられません。でも、大丈夫です。なんとかなりました。これはあとで解説します。

CPUは無難にXeon E3-1275 v6。まぁこのラインナップのなかだとXeon以外の選択はないかなという感じ。

メモリは32GB積みます。NASにしてはちょっと多めですが、メモリバカ食いのZFSを使用するつもりですので、多いにこしたことはありません。本来であれば、X11SSH-TF-Oがサポートする最大容量である64GBまで入れたかったですが、予算の関係上断念しました。

ちなみにメーカーはArkがやたら推してるSanMaxメモリ。ECC付きにしました。ちなみに、この構成でECCは必須ではありません。このM/BにXeonでもECCなしのメモリで動作します(@yoshikigはこの構成で動かしてます)ECCを外せばもっと安くできます、が、僕は宇宙線が怖いのでECCにしました。ブラックホールも視認できたことですし、ECC付きがよろしいでしょう。

電源のチョイスはよくわかりません。会社の上司が薦めてくれたものをそのまま買いました。80PLUS GOLDなのでいいんじゃないの?よくしらんけど?程度の認識です。

ここで紹介したM/Bとケースと電源の構成ですと、ザワードのATX電源コネクタがM/Bに届かないのでATXの電源延長ケーブル(部品表7)を別途用意する必要があります。また、この電源は4pinのペリフェラル電源が一つしか出てません。U-NAS NSC-810A Server Chassisは外部ファンで一つ、SATAのバックポート電源で一つ。計2つのペリフェラル4pin電源コネクタが必要なので、変換コネクタ(部品表8)が別途一つ必要です

価格

締めて215,563円。HDD抜きでこの価格。QNAP社で同じく8ベイで同じくらいのスペックのモデルを買えば40万円ぐらい(HDD抜きで)します。が、メモリは16GBぐらい。CPUもXeonでー4C8Tでーとかできません。ましてやIMPIなんてついてません。また、ZFS snapshotも使えません(LVMスナップショットは使えますがね)

たしかにリッチなWebUIはついてこないですが、Linuxのオペレーションに十分に慣れた人で、そこの管理コストを負担しても良い人であれば断然安いと言えるでしょう。まあ、簡単な管理WebUIなら自分で書いてもいいですしね。ちなみに、私はZFS snapshotを管理するための超簡単なWebUIをRailsで作りました。

組み立て

粛々と。

f:id:tkh86:20190426130738j:plain
パーツ一覧

上述しましたが、X11SSH-TF-OはM.2のフォームファクタが2260です。一方で購入したSamsung SSD 500GB 970 EVOのフォームファクタは2280です。つまり、ハマりません。じゃあどうするのか?テープで固定します。これで動きます。なにも問題ありません。

f:id:tkh86:20190426130920j:plain
SSDはテープで無理やり固定

ファンはCPU付属のリテールファンでギリギリ収まります。

f:id:tkh86:20190426130815j:plain
Xeonのリテールファンで収まります

配線完了。ちなみに、SATAケーブル八本はケースに付属しています。ご丁寧に各ケーブルにラベルまでついてますのでわかりやすいです。M/Bを横切ってる黒いケーブルが上述したATX電源の延長ケーブルです。写真の通り、延長ケーブルがないとM/Bまで電源が届きませんので必須です。

f:id:tkh86:20190426131938j:plain
配線完了

OSのインストール

無難にUbuntu 18.04.2 LTSを選択。何の問題もなくサクッとインストール完了。しかし、インストール後に問題発生。OS起動時に高確率でVGAの出力が死にます。IPMIの画面の方でもブラックアウトしてしまい何も写りません。

これはあくまでVGA出力が死んでるだけです。裏ではOSはちゃんと動いており、つまり、SSHとかsambaとかではつなぐことができます。まあ、NASなので、VGAなんて緊急時以外は使わないわけですが、気持ちが悪いです。どうも、SupermicroのX11SSH-TF-OのVGAは難ありみたいです。ちなみにCentOS 7ではインストールの段階でVGA出力が死んでしまってにっちもさっちもいきませんでした。

とはいえ、VGAが死ぬのはいやなので、なんとかします。どうもVGAが高解像度のモードに切り替わるときに死んでるくさいので、レガシー解像度モードに変更しちゃいます。/etc/default/grubファイルをエディタで開いてnomodesetを追記します。

GRUB_CMDLINE_LINUX_DEFAULT="maybe-ubiquity nomodeset"

こんな感じ。あとは

$ sudo update-grub

とやってgrubの設定を変更。これでVGA出力は死ななくなります。

当然VGA出力はレガシーモードになりますので解像度は低くなります。ですが、繰り返しますが、NASなのでVGA出力なんて緊急時以外使いません。すべての操作はSSHでやるわけですからこれで何も問題ありません。

10GbEのLAN環境構築

次に10GbEのLANです。10GbEのスイッチは、まあまだちょっと高いかなあという感じですね。QNAP社のスイッチで5万円ちょっと。NETGEARので6万ちょっと。家庭用のスイッチとしてはちょっと悩む値段です。

ですが、これまた@yoshikigがいいものを見つけてきてくれました。MikroTikのCRS305-1G-4S+IN。アップリンクの1Gポートが一つと、10Gの4つのSFPポートがついたスイッチです。お値段117USD。同じくMikroTik社のS+RJ10モジュールが一個50USDなので、NASとメインマシンをつなげる分でSFPモジュールを2つ買っても合計200USDちょっと。2万円ちょいぐらいですね。送料と関税を入れても、QNAPのスイッチの5万円よりは安くすみます。eurodkで購入。ちなみに日本国内では売ってないです、たぶん。

www.eurodk.com

f:id:tkh86:20190426132724j:plain
MikroTik CRS305-1G-4S+IN

ちなみに、われわれ以外にも購入して使ってる人がいます。 www.bluecore.net

blog.gaftnochec.net

NASにつなげるメインマシンの方にも10GbEカードが必要です。IntelEthernet Server Adapter X520-1を購入。これ、正規品を買うと5万円ぐらいしますが、AliExpressで怪しげなOEM品が5000円ぐらい売っています。

ja.aliexpress.com

1/10の値段なので、パチモンの可能性もあるかなと思ったのですが、購入してみました。

f:id:tkh86:20190426133457j:plain
Intel Ethernet Server Adapter X520-1

f:id:tkh86:20190426142356p:plain
無事Intel NICとして認識

結果、一応Windowsのデバイスマネージャー上ではIntelEthernet Server Adapter X520-1として認識してますし、ドライバもIntelの公式のものが使えます。また、iperfで負荷を10分間連続してかけ続けてみても、概ね9Gbit/secの速度で通信しつづけてくれます。途中で劇的に速度低下したり落ちたりする様子はありません。スイッチ、NICともに問題ないようです。

f:id:tkh86:20190426142241p:plain
10分間連続転送

計測

iperfで計測

サーバ側

$ iperf -p 4000 -s

クライアント側:

$ iperf.exe -w 10M -p 4000 -c 192.168.1.38

f:id:tkh86:20190426124211p:plain
iperf測定結果

結果は9.00Gbits/sec。十分速度出てますね。ちなみに、Windows版のiperfは

iPerf - Download iPerf3 and original iPerf pre-compiled binaries

を使いましょう。Ubuntu for Linuxのiperfはめっちゃ遅いです。9Gbit/secも出ません。I/Oエミュレーションレイヤーが遅いのかな?なんかWSLはI/O周りが遅い感じがしますよね。とはいえ、上のwindows版のiperfもcygwinのdllが同梱されてるんで…条件はWSLと同じだと思うんですがね…

sambaでの計測

次にsmbdを動かして、10GbEのLANでWindowsからNASに向かってデータをコピーしてみました。smbdのエクスポート領域をNVMeのSSDSamsung SSD 500GB 970 EVO)でext4にすると

f:id:tkh86:20190426124626p:plain
sambaでのコピー速度 (NVMe領域)

1.05 GB/s。8Gbits/secぐらいでてますね。sambaのプロトコルを噛ましてもこの速度です。これはWindows上でファイルを一度全部読み込んで、オンメモリにした状態でのコピーです(余談ですが、私のWindowsマシンのRAMは128GBまであります)

次に、smbdのエクスポート領域をHDD4台のZFS RAIDZ2にします。

f:id:tkh86:20190426125102p:plain
sambaでのコピー速度(HDD4 ZFS RAIDZ2)

同じくこれはファイルを一度全部読み込んでオンメモリにした状態からのコピーです。281MB/s。ローカルでのddのシーケンシャルライトの速度に肉薄する勢いの速度です。sambaのプロトコルは十分速いですね

ちなみに、S+RJ10モジュール。発熱がすごいです。手で触るとやけどするレベルで熱くなります。さすがに壊れるんじゃないかこれ?と思い@yoshikigが冷却するため5Gや2.5Gにリンクダウンしてみて実験してみましたが、この場合、ジャンボフレームがDisableになります。あまりS+RJ10のSFPモジュールはオススメできないかなという感じです。DACケーブルかファイバーにするべきかもしれません。

雑感

f:id:tkh86:20190426133806j:plain
設置したところ

U-NAS社のケースはオススメです。剛性もあり、高級感もあり、細かな点で気が利いていて言うことなし。

SupermicroのX11SSH-TF-Oは5万とちょい高いですけど10GbEが2ポート + 1GbEが1ポート。それにIPMIが使えることを考えれば全然安い気がします。ですが、内蔵VGAの挙動が怪しいですね。。。UbuntuCentOSともにVGA出力が高確率で死にます。買うならば、SSH経由で使うのが前提のってことで。割り切ったほうがいいかもしれません

テンジーは素晴らしい。UNICACA AN8599の怪しげなOEM NICはフツーにIntel NICとして使えます。MikroTikのCRS305-1G-4S+INもやすいですがふつうに10GbEとして実用的に使えます。

が。。。上述したとおりS+RJ10のSFPモジュールが尋常ではないぐらい発熱します…触るとやけどするレベルの発熱です。あまりの発熱から、リンクダウンして冷やそうかと思うと今度はジャンボフレームがDisableになります。

いろいろな人の話を聞くと、そもそもRJ45のSFPモジュールなんぞ使うべきではないらしいです。DACケーブルか、ファイバーにするべきだそうです。というわけで次はそれかなあ。