むろっちのStacking

日々の中で学んだIT知識をメモして置く場所

golangでのドメインモデルの初期化、更新パターン

DDDを実コードに取り入れようとする際に考えさせられるのが、モデル初期化をどのように記述するのかである。 モデル初期化を定義せずにDDDをコードにするとモデルの定義違反のコードが生成される可能性がある

// bad
type Note struct {
    Title     string
    Body      string
    State     NoteState
    CreatedAt time.Time
    UpdatedAt time.Time
}

func register(title, body string) error {
    // CreatedAt, UpdatedAtの定義がない。Stateが空文字になる
    note := &Note{
        Title: title,
        Body:  body,
    }
    // ...
}

ドメインモデルを実装する際にはモデルの定義の他に、新規作成、更新がモデルの定義に沿った形で行われる必要がある

今回はgolangでのモデルの初期化、更新方法として以下の3通りを比較、検討する

  1. config構造体による初期化、更新
  2. Functional Option Patternによる初期化、更新
  3. Method Chainingによる初期化、更新

1. config構造体による初期化、更新

初期化

このパターンの方針としてはNew, Update時の引数を引数専用の構造体を用意し そこに更新したい値を詰め込む方式である

type Note struct {
    Title     string
    Body      string
    State     NoteState
    CreatedAt time.Time
    UpdatedAt time.Time
}

type NoteCreateConfig struct {
    Title *string
    Body  *string
}
type NoteState string

const (
    stateDraft     NoteState = "draft"
    statePublished NoteState = "published"
)

var (
    defaultTitle = "defaultTitle"
    defaultState = stateDraft
)

var nowFunc = func() time.Time {
    return time.Now()
}


func New(conf NoteCreateConfig) *Note {
    note := Note{
        Title:     defaultTitle,
        State:     defaultState,
        CreatedAt: nowFunc(),
        UpdatedAt: nowFunc(),
    }

    if conf.Title != nil {
        note.Title = *conf.Title
    }

    if conf.Body != nil {
        note.Body = *conf.Body
    }
    return &note
}

func register() error {
    title := "hogehoge"
    note := New(NoteCreateConfig{
        Title: &title,
    })

    // ...save
    return nil
}

本パターンのメリットはconfig設定時に設定しなかった値についてはデフォルト値を使うなど、 ドメインモデルの整合性をNewメソッド内で担保できることである。

注意すべき点としてはconfig構造体のFieldをポインター型定義にする必要がある。 理由としてはgolangのゼロ値と未定義を区別する為だ。

更新

更新の場合はドメインモデルにUpdateメソッドを定義する形になる

type NoteUpdateConfig struct {
    Title *string
    Body  *string
    State *string
}

func (n *Note) Update(conf NoteUpdateConfig) error {
    if conf.Title != nil {
        n.Title = *conf.Title
    }

    if conf.Body != nil {
        n.Body = *conf.Body
    }

    if conf.State != nil {
        if err := n.updateState(*conf.State); err != nil {
            return err
        }
    }
    return nil
}

func (n *Note) updateState(s string) error {
    for _, state := range []NoteState{stateDraft, statePublished} {
        if s == string(state) {
            n.State = state
        }
    }
    return errors.New("invalid state")
}

このようにすることで、以下ができるようになる * モデルの整合性検証を行う * ゼロ値とnilを区別することで更新すべきFieldを判定する

Functional Option Patternによる初期化、更新

Functional Option Pattern(FOP)とは、golangの構造体初期化時にオプションを与える方法である。 元ネタはこの辺であると言われている

初期化

先程のNoteモデルでFunctional Option Patternにすると以下である

type Option func(*Note) error

func Title(s string) Option {
    return func(n *Note) error {
        n.Title = s
        return nil
    }
}

func Body(s string) Option {
    return func(n *Note) error {
        n.Body = s
        return nil
    }
}

func New(opts ...Option) (*Note, error) {
    note := Note{
        Title:     defaultTitle,
        State:     defaultState,
        CreatedAt: nowFunc(),
        UpdatedAt: nowFunc(),
    }

    for _, opt := range opts {
        if err := opt(&note); err != nil {
            return nil, err
        }
    }

    return &note, nil
}

func register() error {
    note, err := New(
        Title("hoge"),
    )
    // ...save
    return nil
}

ポイントとしてはNewの引数が対象構造体を引数にとり、破壊的に更新するインターフェース型であるということだ 具体的には以下の部分

func Body(s string) Option {
    return func(n *Note) error {
        n.Body = s
        return nil
    }
}

インターフェース型にすることにより以下のメリットがある * 引数を可変長引数型にまとめることができる * 引数ごとにバリデーションを定義することができる

更新

FOPで厳密に更新する場合はUpdateとNew時のインターフェースを分けることで、利用可能なOptionを分離する必要がある。 以下はOption引数をNew時と分離した例である。

func U_Title(s string) UpdateOption {
    return func(n *Note) error {
        n.Title = s
        return nil
    }
}

func U_Body(s string) UpdateOption {
    return func(n *Note) error {
        n.Body = s
        return nil
    }
}

func U_State(s string) UpdateOption {
    return func(n *Note) error {
        return n.updateState(s)
    }
}

func (n *Note) Update(opts ...UpdateOption) error {
    for _, opt := range opts {
        if err := opt(n); err != nil {
            return err
        }
    }
    return nil
}

この形にすると、NewとUpdate両方でOption引数を定義するのでコード記述量が増える。 そのため妥協案としてNew時とUpdate時のOption引数を共通化して使うケースが普段使いでは多い。

これでもモデルの各Fieldのバリデーションは行われるので問題になるケースはあまりないと思う

// OptionをNewと共通化
func (n *Note) Update(opts ...Option) error {
    for _, opt := range opts {
        if err := opt(n); err != nil {
            return err
        }
    }
    return nil
}

3. MethodChainingに初期化、更新

MethodChainingとはモデル生成用のBuilderを用意し、Methodにモデル生成用の値を詰めながら自分自身を返すことで任意の数のOption設定ができるようにするパターンである

初期化

type Builder struct {
    title *string
    body  *string
    state *NoteState
    _err  error
}

func NewBuilder() *Builder {
    return &Builder{}
}

func (b *Builder) Title(s string) *Builder {
    if len(s) > 255 {
        if b._err == nil {
            b._err = errors.New("title exceed maxlength")
        } else {
            b._err = fmt.Errorf("title exceed maxlength: %w", b._err)
        }
        return b
    }
    b.title = &s
    return b
}

func (b *Builder) Body(s string) *Builder {
    if len(s) > 1024 {
        if b._err == nil {
            b._err = errors.New("body exceed maxlength")
        } else {
            b._err = fmt.Errorf("body exceed maxlength: %w", b._err)
        }
        return b
    }

    b.body = &s
    return b
}

func register() error {
    builder := NewBuilder()
    note, err := builder.
        Title("hogehoge").
        Body("fugafuga").
        Build()
        // ... save
    return nil
}

このパターンで実装する場合は、途中で発生するエラーをどのようにハンドリングするかが実装上で考えさせられる 今回はerrorをWrapする形で記述したが、記述をシンプルにするのであれば初回のエラーのみ格納する等でも 取り回し上差し支えないと思う。

更新

実装したい事柄はChainingに取り入れたOptionパラメーターの反映である。そのため以下の形での実装となる

func (b *Builder) Update(note *Note) error {
    if b.title != nil {
        note.Title = *b.title
    }

    if b.body != nil {
        note.Body = *b.body
    }

    if b.state != nil {
        note.State = *b.state
    }

    return b._err
}

終わりに

今回は主にDDDのモデルに着眼したgoの構造体初期化、更新パターンを考えてみたが 普段利用する中では以下のような使い分けで考えている。

  • コード記述量を減らしたい: config構造体
  • 丁寧にバリデーションを書きたい: Functional Option Pattern, MethodChaining

YAMAHAルーター+lua scriptで簡易的な冗長構成を組んでみた

VRRPやHAが組めない異機種間で冗長構成を組みたかったのでlua scriptでなんとかしてみた

はじめに

我が家ではメインルーターとしてSophos XG FirewallVMとして稼働している このルーターが利用できないときにYAMAHAルーターをバックアップとして自動的に利用するスクリプトを書いてみた


普通の冗長構成

HA構成(sophos XG Firewall)

HAは同機種間のみサポート。そのためHA構成を組む場合はXG Firewallがもう一台必要。我が家ではXGFirewall用にもう一台VMを用意するのがランニング的に見合わなかったので不採用

VRRP構成

標準規格なのでSophos XG - YAMAHA RTX間で組めるかなと思いきや、Sophos XG側で未対応

以上から自力で YAMAHAlua script機能を利用して簡易的な冗長構成を作成することにした


構成

インターネットプロバイダの制約上、プロバイダ貸し出し機器以外へ直接グローバルIPを割り振れない構成上、少し特殊な構成になっている

Sophos XG側がDefaultGWになっており、このルーターDHCP機能も兼ねている

f:id:murochi:20210111132502p:plain
nw構成

よってYAMAHA RTXが障害時に担う役割は以下である * DefaultGWのIPを引き継ぐ * DHCPサーバー機能を稼働する


障害時の想定

障害時にはSophox XG側のlan側IP、wan側のIPが不通となる想定


設定

YAMAHA config

DHCP設定のみ投入し、サービスは無効 IPは.2設定(≒非defaultGW)

ip lan1 address 192.168.0.2/24

dhcp server rfc2131 compliant except remain-silent
dhcp scope 1 192.168.0.50-192.168.0.100/24 gateway 192.168.0.1

-- recovery用スクリプト。後述
schedule at 1 startup * lua /scripts/recovery.lua

※ 関連箇所のみ抜粋

lua script

以下の動作を想定したスクリプト Primaryに昇格 1. default GW宛に疎通確認。疎通確認が取れない場合2へ 2. YAMAHA RTX側が外部疎通できるか確認。確認が取れたら3へ 3. IPをdefault GW(192.168.0.1)に差し替え、DHCP機能ON

Secondaryに降格 1. Sophos XG、WAN側IPへ疎通確認。疎通確認が取れた場合は2へ 2. YAMAHARTX側のIPを192.168.0.2へ、DHCP機能をOFF

recovery.lua

function check_loss_rate(ipaddress, count)
  command = "ping -c "..count.." "..ipaddress
  rt.syslog("debug","command:"..command)
  res, text = rt.command(command)

  rt.syslog("debug", "result:"..text)
  loss = string.match(text, "(%d+)%.%d+%%")

  if (loss ~= nil) then
    return tonumber(loss)
  end

  return 0
end

function get_promoted()
  rt.command("dhcp service server")
  rt.command("ip lan1 address 192.168.0.1/24")
  rt.syslog("info", "primaryに昇格しました")
end

function get_relegated()
  rt.command("no dhcp service server")
  rt.command("ip lan1 address 192.168.0.2/24")
  rt.syslog("info", "secondaryに降格しました")
end

local local_gw = "192.168.0.1"
local external = "8.8.8.8"
local primary_wan_addr = "192.168.254.253"

function execute()
  mode = "secondary"
  rt.syslog("debug", "start script mode:"..mode)

  while true do
    if mode == "secondary" then
      -- 内部接続チェック
      internal_loss_rate = check_loss_rate(local_gw, 5)
      if internal_loss_rate > 50 then
        -- 外部接続チェック
        external_loss_rate = check_loss_rate(external, 5)
        if external_loss_rate == 0 then
          get_promoted()
          mode = "primary"
        end
      end
    else
      -- primary稼働中
      loss_rate = check_loss_rate(primary_wan_addr, 30)
      if loss_rate == 0 then
        get_relegated()
        mode = "secondary"
      end
    end
  end
end

execute()

RTX向けのAPI一覧は以下に一覧がある

www.rtpro.yamaha.co.jp

scriptのアップロード

今回はSFTP機能でアップロードした

# SFTP接続
sftp 192.168.0.2

# script用dir作成
mkdir scripts

# scriptの設置
put [ローカルのスクリプトDir]/recovery.lua recovery.lua

scriptのスケジュール(先述のconfigに記載済み)

今回は常駐型のscriptで記載しているのでルーター起動時に1度のみ起動すればOK そのため以下の記載とした

-- recovery用スクリプト。後述
schedule at 1 startup * lua /scripts/recovery.lua

YAMAHA上でlua scriptの操作

# scriptの実行
lua /scripts/recovery.lua

# scriptの実行ステータス確認
show status lua

# scriptの強制終了
terminate lua file /scripts/recovery.lua

注意点

本構成はあくまで簡易的な冗長構成のため、ダウンタイム数分程度発生する。 具体的には * sophos XG Firewallがダウンしたと検知されるまでの時間 * YAMAHA RTXがIP差し替え後、コンピューターのARPテーブルがXG FirewallMACアドレスからYAMAHRTXのMACアドレスに書き換わる時間

ARPテーブルのキャッシュ期間はコンピューターによりまちまちで、自身の環境では * google home: キャッシュ数秒-30秒程度 * Mac: 2-3分程度

となり、実質的には上記のARPキャッシュ時間がダウンタイムの時間と大きく直結する


やってみて

殆どの人がそうだと思うが、luaスクリプトを書いた経験がなく基本文法を調べながら書く為時間が取られた。script言語であればnodejsやpythonでもかけるようにしてほしい。(パフォーマンス面でluaが優れているとの記載があったので難しいかもしれないが)

本格的にscriptを書くとなるとYAMAHAAPIのmockを書く必要があり、mock用のライブラリや開発環境を用意してくれれば実機コピー後に意図しない動作等を防ぐことができるのでなお嬉しい。

rt.commandで殆どすべてのコマンドが実行可能なのでできることはかなり広いが、上記のテスタビリティの理由によりユーザー向けには公式で出している設定例以外のスクリプトはあまり利用したくないなといった印象だった

Echoで複数回Bind可能なBinderを作る

背景


echoではBind構文によりstructにRequestのQueryやBody部をマッピングすることができる
しかしながらBody部は2回目以降Bindしようとすると以下のようにErrorが発生する

"code=400, message=EOF, internal=EOF"

これを複数回Bindできるようにしようと言うのが今回の趣旨である

原因


HeaderやURL.Queryはmap型map[string][]stringで定義されているが、
Bodyはio.ReadCloser型なので読み取り開始位置の移動exp) f.Seek(0,0)ができない

解決法


一度Body部を読み出してしまい、Read後に再度未使用のio.ReadCloser型のstructをBodyに代入するCustomBinderを作成する

環境

CustomBinder作成

module/binder.go

package module

import (
    "bytes"
    "io/ioutil"

    "github.com/labstack/echo/v4"
)

type CustomBinder struct {
    binder echo.DefaultBinder
}

func NewBinder() *CustomBinder {
    return &CustomBinder{
        binder: echo.DefaultBinder{},
    }
}
func (cb *CustomBinder) Bind(i interface{}, c echo.Context) (err error) {
    var b []byte
    if b, err = ioutil.ReadAll(c.Request().Body); err != nil {
        return
    }
    c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(b))

    err = cb.binder.Bind(i, c)

    req := c.Request()
    req.Body = ioutil.NopCloser(bytes.NewBuffer(b))
    c.SetRequest(req)

    return
}

main.go

package main

import (
    "fmt"
    "net/http"
    "sample1/module"

    "github.com/labstack/echo/v4"
)

func main() {
    // Echo instance
    e := echo.New()

      // customBinderでdefaultBinderを上書き
    e.Binder = module.NewBinder()

    // Routes
    e.POST("/", hello)

    // Start server
    e.Logger.Fatal(e.Start(":1323"))
}

type form struct {
    UserID int    `json:"user_id"`
    Name   string `json:"name"`
}

// Handler
func hello(c echo.Context) error {
    f1 := form{}
    if err := c.Bind(&f1); err != nil {
        return c.JSON(http.StatusBadRequest, err.Error())
    }
    fmt.Println("f1", f1.UserID)

    f2 := form{}
    if err := c.Bind(&f2); err != nil {
        return c.JSON(http.StatusBadRequest, err.Error())
    }
    fmt.Println("f2", f2.UserID)
    return c.String(http.StatusOK, "Hello, World!")
}

元の原因がgolanghttp.Request().Bodyの定義によるものなので、
別のFWでも考え方は使い回せると思う

API Gateway & lambda FunctionでRedirectするEndpointを作成するサンプル

Lambda + API GatewayAPI Gateway宛に来たリクエストをRedirectするEndpointをAWS-CDKで作成してみた

構成


全体図

f:id:murochi:20200607175000p:plain
構成

API

f:id:murochi:20200607175718p:plain

API Gatewayとカスタムドメインマッピングは含まない

コード


流れ

  • redirect用Functionをを作成①
  • aws-cdkのstackクラス実装②
  • cdkのentryPoint作成③
# Directory構成

./bin
├── redirect.ts  //cdkのエントリポイント ③

./lib
├── redirect
│   ├── index.ts // stack Classの実装 ②
│   └── resources
│       └── main.js //lambda用 ①

redirect用Functionを作成 ①


  • queryString、pathをredirect先に引き継ぐ形
// main.js

const qs = require('querystring');

exports.handler = async (event) => {
    const {
        multiValueQueryStringParameters: qsParams,
        pathParameters,
    } = event;
    
    const queryString = qs.stringify(qsParams);
    const path = pathParameters ? pathParameters.proxy : '';
    let redirectURL;
    
    if (qsParams) {
        redirectURL = `https://anotherdomain.com/${path}?${queryString}`;
    } else {
        redirectURL = `https://anotherdomain.com/${path}`;
    }
    
    const response = {
        statusCode: 301,
        headers: {
            Location: redirectURL
        }
    };
    return response;
};

aws-cdkのstackクラス実装②


  • 上記で作成したコードはresources/main.jsに配置
// redirect/index.ts

import * as apigateway from "@aws-cdk/aws-apigateway";
import * as lambda from "@aws-cdk/aws-lambda";
import * as s3 from "@aws-cdk/aws-s3";
import * as cdk from "@aws-cdk/core";

export class RedirectService extends cdk.Construct {
  constructor(scope: cdk.Construct, id: string) {
    super(scope, id);

    const bucket = new s3.Bucket(this, "RedirectLambdaHandlerBucket", {
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });

    const handler = new lambda.Function(this, "RedirectHandler", {
      runtime: lambda.Runtime.NODEJS_12_X,
      code: lambda.AssetCode.asset("lib/redirect/resources"),
      handler: "main.handler"
    });

    bucket.grantReadWrite(handler);

    const api = new apigateway.RestApi(this, "redirect-api", {
      restApiName: "redirect-api"
    });

    const redirectIntegration = new apigateway.LambdaIntegration(handler, {
      requestTemplates: { "application/json": '{ "statusCode": "200" }' }
    });

    api.root.addMethod("ANY", redirectIntegration);
    api.root.addProxy({
      anyMethod: true,
      defaultIntegration: redirectIntegration
    });
  }
}

export class RedirectStack extends cdk.Stack {
  constructor(app: cdk.App, id: string, props?: cdk.StackProps) {
    super(app, id, props);
    new RedirectService(this, "RedirectService");
  }
}

cdk Entry Point作成③


#!/usr/bin/env node
import * as cdk from "@aws-cdk/core";
import { RedirectStack } from "../lib/redirect";

const ACCOUNT = "123456890"; // dummy
const app = new cdk.App();

new RedirectStack(app, "RedirectStack", {
  env: {
    region: "ap-northeast-1",
    account: ACCOUNT
  }
});

app.synth();

実行

npx cdk -a 'npx ts-node bin/redirect.ts' deploy

Windows7環境下で共有フォルダアクセスが遅い場合のチューニング

共有フォルダアクセスが遅いときには様々な複合的要因が重なって発生しているケースが多い。 普段対応する際に考えているトラブルシュートをリストしてみた

  • ストレージのパフォーマンスが遅い
  • ネットワークのパフォーマンスが遅い
  • 2拠点間のレイテンシが大きい
  • 2拠点間の帯域が細い
  • SMBプロトコルの動作が悪い
  • SMBプロトコルが意図せず古いverを利用している
  • その他SMBパラメータのチューニング

ストレージのパフォーマンスが遅い


共有元、共有先のローカルディスクのRead,Write性能を確認することができる 簡単に使えるソフトとしてはCrystalDiskMarkが有名だ 起動して速度を図りたいドライブを指定する

上から順に以下のようなテスト意味合い

  • Seq Q32T1: マルチキュー&スレッドによるシーケンシャルリード/ライトテスト (Block Size=128KiB)
  • 4K Q8T8: マルチキュー&スレッドによるランダムリード/ライトテスト (Block Size=4KiB)
  • 4K Q32T1: マルチキュー&スレッドによるランダムリード/ライトテスト (Block Size=4KiB)
  • 4K Q1T1: マルチキュー&スレッドによるランダムリード/ライトテスト (Block Size=4KiB)

基本的にシーケンシャルが一番速度が出るので、このディスクの最大スループットはシーケンシャル程度と認識できる

注意

ただCrystalDiskMarkは厳密な速度測定には向かないかもしれない。速度を図る際にストレージコントローラ側の キャッシュに収まるサイズのデータしか指定しない場合はパフォーマンスが異常に高い数値がでるケースもある。あくまで ディスクパフォーマンスのブラックボックステストとして考えたほうがいいだろう
 

ベンチマーク時に同時に確認してほしい項目

タスクマネージャ > パフォーマンス > リソースモニター > ディスク のディスクキュー項である。ディスクにどの程度Read/Writeの命令が溜まっているかという指標である。 指標の数値が大きければ、命令に対してディスクの処理が追い付いていないということになる

この値は通常はRAID本数以下程度が望ましいとされている

ネットワークのパフォーマンスが悪い


ネットワークのパフォーマンスが共有アクセスに影響する要因としてはレイテンシと、帯域2つの観点がある SMBアクセスはTCPパケットにて動作しているため、レイテンシが大きい環境下ではTCPパケットの正常性確認パケット待ちのため 思ったようにパフォーマンスが出ないケースも多い

2拠点間のレイテンシが大きい

素直にPingで図ろう。時間のところを確認してもらえればいい

192.168.0.1 に ping を送信しています 32 バイトのデータ:
192.168.0.1 からの応答: バイト数 =32 時間 <1ms TTL=64
192.168.0.1 からの応答: バイト数 =32 時間 <1ms TTL=64
192.168.0.1 からの応答: バイト数 =32 時間 <1ms TTL=64
192.168.0.1 からの応答: バイト数 =32 時間 <1ms TTL=64
2拠点間の帯域が細い iperfというツールを使う

このツールはクライアント、サーバーの2つに分かれており クライアントがiperfサーバーに接続する形でパフォーマンスが確認できる。 以前はWindows版がなかったのだが、気づいたらWindows版が出ている模様 細かいオプションは多くあるが、一旦以下で測定できる

サーバー側

iperf -s

クライアント側

iperf -c server_ip

SMBプロトコルの動作が悪い

SMBプロトコルが意図せず古いverを利用している

Windows8以降であればPowerShellにて現在接続中のSMB情報が確認できる。 管理者権限でPowerShellを立ち上げたうえで

PS C:\WINDOWS\system32> Get-SMBConnection

ServerName ShareName UserName Credential Dialect NumOpens
---------- --------- -------- ---------- ------- --------
smb-sv share1 TEST\test TEST\test 3.0.2 3
smb-sv share2 TEST\test TEST\test 3.0.2 3
smb-sv share3 TEST\test TEST\test 3.0.2 3

これで確認すべきは Windows7系:SMB2系の表示であるか Windows8以降:SMB3系の表示であるか

それぞれのVerが各OSで利用できるSMBの最大Verのため、数値が異なる場合は何かしらの理由にて下位のSMBしか利用できない 状況になっている。

その他SMBパラメータのチューニング

Microsoftから以下のようなファイルが公開されている Performance Tuning Guidelines for previous versions of Windows Server

ざっくりな設定方針としてはSMBサーバー、クライアントのレジストリをいじってチューニングしてくださいとのこと

以下のガイドからチューニング例抜粋

ガイド記載のチューニング例

  • サーバー側
Parameter Value
NtfsDisable8dot3NameCreation 1
TreatHostAsStableStorage 1
AdditionalCriticalWorkerThreads 64
MaximumTunnelEntries 32
MaxThreadsPerQueue 64
RequireSecuritySignature 0
MaxMpxCt (not applicable with  SMB 2 clients) 32768

   

  • クライアント側
Parameter Value
DisableBandwidthThrottling 1
EnableWsd 0
RequireSecuritySignature 0
FileInfoCacheEntriesMax 32768
DirectoryCacheEntriesMax 4096
FileNotFoundCacheEntriesMax 32768
MaxCmds 32768
DormantFileLimit [Windows XP only] 32768
ScavengerTimeLimit [Windows XP only] 60
DisableByteRangeLockingOnReadOnlyFiles [Windows XP only] 1

 

細かいレジストリの場所はドキュメントで参照してほしい。キーがなければ作成等する必要がある

ただ、私の環境で実施したところWindows7クライアントとしては以下の値を変更するのが効果が大きかった。

  • DisableLargeMtu HKLM\system\CurrentControlSet\Services\LanmanWorkstation\Parameters (REG_DWORD)

この値は規定1なので0にするとLargeMtuが利用できる。大体2割程度のパフォーマンスが改善した

Djangoテンプレートのfilterを自作する

テンプレートのフィルターで3桁ごとに数値の区切りが欲しかったので filterの自作をしてみた

 

フィルタ関数の作成

APP名/templatetags/tags.py

from django import template
register = template.Library()

@register.filter(name='num_delimiter')
def num_delimiter(value):
    return '{:,}'.format(int(value))
  • APP名/templatetags配下に.pyファイルを作成。そこに登録したい関数を作成する。引数はvalue,[args]が利用できる * value: "|"より前の文字 * args: フィルタ名:argsの形で取る引数
  • テンプレート登録用のクラス Libruaryを作成
  • 登録用メソッドデコレータを利用して登録する

 

Viewでの使い方
  • load フィルタタグファイル名でloadする
  • 通常のフィルタと同じように利用する
{% extends "base.html" %}
{% load tags %}
{% block content %}

<div class="container-fluid">



{{ '1234567890'|num_delimiter }}



{{ ''|total }}

できあがり

 

templateにモデル層を組み込む

フィルタを templateで利用できる関数として考えると、複数テンプレートで共通の機能を 提供するフィルタ(関数)を作成できる

templatetags/tags.py

from django import template
register = template.Library()

from todo.models import Todo
@register.filter(name='total')
def total(value):
    return len(Todo.objects.all())

index.html

{% extends "base.html" %}
{% load tags %}
{% block content %}

<div class="container-fluid">



{{ '1234567890'|num_delimiter }}



{{ ''|total }}

この形であればルーティングベースで利用できる変数が異なる場合でも 一定の動作をする機能が利用できる

自宅サーバーを自作するときに検討すべき要件

はじめに

最近はAWSとかGCPとかクラウドが流行っているのでわざわざ自宅にサーバーを持つ人は減っていると思う。
だが元々PC自作が趣味だったので自然な流れでに自宅サーバーを構築しようと考えていた

自宅サーバーを構築すると自分の懐から出ていくので費用と性能、信頼性の肌感覚が身につく。 また物理レイヤーから自由にできる環境はOSなどの学習に最適だ。
いっとき構築するだけでなく、運用することで見えてくる課題があり、そこが学びにつながる

自宅のサーバー群


今は物理サーバー4台で稼働中。仮想化により省エネ&台数削減できた

録画サーバー
WindowsServer+PT3で構築。リモート予約機能を搭載。構成を変えつつ約5年運用
構成

  • CPU Celeron
  • MEM:32GB
  • HDD:4TB2+6TB2

仮想基盤サーバー1号機
ESXiで構築後述の2号機と合わせて約10台のVMが稼働中。我が家ではルーターVMとして動作するため要のサーバー。約1年可動中
構成

  • CPU:Xeon 2620v3
  • MEM:32GB ECC
  • HDD:SSD1TB*2

仮想基盤サーバー2号機
ESXi1号機のバックアップ用途

構成

  • CPU :Xeon E3 1240v6
  • MEM:ECC 32GB
  • HDD:SSD1TB*2

バックアップサーバー
Windows+余ったHDDで週一Wake on Lan起動。バックアップ取得後はシャットダウン

構成

構成は余り物 * CPU:Core i5 3570T * MEM:16GB * HDD:3TB*6

 

自宅サーバーの作り方2通り

既成品を買ってくる

NEC,HP、富士通あたりからPC紛いのエントリーサーバーを購入する

メリット
* 市販PCと比べてもかなり格安で購入できる。CeleronPentiumクラスなら10万以下も多く、Xeon E3シリーズでも10万円台で購入できるものが多数あるNTT-X Store * マネジメントポートによるOSレス監視機能が標準で搭載しているため、OSハング時のリモート操作が可能。ただしUIからの操作は別途ライセンスが必要なケースが多い

デメリット
* 専用M/B等構成が特殊なため、市販拡張ボードを購入し、挿すことは未サポート * OSやHWファームのアップデートがサポート契約を結ばないとダウンロードできないケースがある * ラック型の場合、動作音はかなりうるさい * ケースは変えられない

パーツを購入して自分で組み立てる

サーバー用途で利用できるパーツを自分で調べて購入する。

メリット
* 自分の好きなケースや構成で組める。

デメリット
* サーバー構築に必要とする機能を満たすか、またサーバー用途OSが検討している構成で動くかは動作未サポートがほとんど。慣れていないと入れたいOSがインストールできない構成で組んでしまうこともある * 実は対して安くない * パーツの購入が難しい。M/BはAsrockRackやSupermicroに良さそうなものがあるが、実購入できる場所が少ない

自作PCとサーバーの違い


ITパスポートに出てきそうなレベルの内容だがサーバーと自作PCの違いは

  • 性能
  • 信頼性

をどうするかに尽きる

性能

CPU:現行のPCのスペック自体非常に高く、CPUがネックになるケースは少ない
MEM:個人で立てる用途であれば仮想基盤用途を除きメモリは8GBもあれば十分

信頼性

サーバーにする上で信頼性の担保が最も難しい

CPU

Xeon上位モデルにはRAS(Intel Run Sure Technology)を搭載しているが機能としてメモリアドレスのミラーリング等有効な状況が限られる。Xeon E5から機能自体はつくが個人用途で導入するメリットは低い。気にすべきはCPUがECCに対応するか否か

参考:https://japan.zdnet.com/article/35103104/2/

MEM
ECCメモリにするか否かECCの場合、nonECCと比べて最低でも1.5-2倍程度の価格になる ECCの効果はなかなか実感しにくいが、Googleによる調べでは以下のようにメモリ訂正の頻度は予想よりも遥かに多い。また経年劣化が10-18ヶ月で発生し始めるとの記載もある。 https://japan.cnet.com/article/20401367/

ストレージ
HDD&ストレージ:気休めではあるがHDDのMTBF平均故障間隔)値を参考にするのが王道 有名どころのWesternDigitalのHDDラインナップでは WD Green:30万時間 WD Red(市販NAS向け):60万時間 SSDの一例 Intel SSD 540s(コンシューマ向け)160万時間 容量気にしなければSSDのほうが信頼性が高い

RAID:3通りの考え方がある

拡張ボード追加によるハードウェアRAID

  • 信頼性が高い
  • 値段が高い。RAID5相当を組めるモデルはその他パーツより高くなる
  • 参考:Amazon

オンボードチップによるソフトウェアRAID

  • OSが動作しない状況でも使える
  • M/Bが故障した場合に別M/Bに載せ替えてデータが確認できるか不安

OSによるデータ冗長化

  • Windowsだと記憶域プール
  • LinuxだとLVM機能
  • ただで利用できるがOS故障時のアクセスがしにくい

NIC:チーミング等を想定してIntelBroadcomなど信頼性の高いものが良い。NIC自体が故障するよりもケーブルの抜き差しでダウンするケースが多い

信頼性に対する持論


単機能サーバー(ここではファイルサーバーを想定)

  • 停止しても影響ないのでHDDのみOSによるRAID構成。

仮想基盤サーバー

  • 停止すると上位のゲストOSが軒並み停止するため、単機能サーバーより信頼性を高めた構成とする
  • メモリはECC構成。NICIntelチップを利用したデバイスを複数ポート用意。HDDはSSDをHWRAIDによるRAID1構成
  • 停止して困る仮想サーバーは乗せる物理サーバーを分けた上でアプリケーションレベルでの冗長化が望ましい

結局のところ、個人用途のサーバーなので如何に故障しないかを突き詰めるよりはある程度信頼性が低いのは認識した上で、きちんとバックアップを自動取得させることが費用対効果を考えると重要だと思っている。