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通りを比較、検討する
- config構造体による初期化、更新
- Functional Option Patternによる初期化、更新
- 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 ¬e } 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(¬e); err != nil { return nil, err } } return ¬e, 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 FirewallがVMとして稼働している このルーターが利用できないときにYAMAHAルーターをバックアップとして自動的に利用するスクリプトを書いてみた
普通の冗長構成
HA構成(sophos XG Firewall)
HAは同機種間のみサポート。そのためHA構成を組む場合はXG Firewallがもう一台必要。我が家ではXGFirewall用にもう一台VMを用意するのがランニング的に見合わなかったので不採用
VRRP構成
標準規格なのでSophos XG - YAMAHA RTX間で組めるかなと思いきや、Sophos XG側で未対応
以上から自力で YAMAHAのlua script機能を利用して簡易的な冗長構成を作成することにした
構成
インターネットプロバイダの制約上、プロバイダ貸し出し機器以外へ直接グローバルIPを割り振れない構成上、少し特殊な構成になっている
Sophos XG側がDefaultGWになっており、このルーターがDHCP機能も兼ねている
よって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一覧は以下に一覧がある
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 FirewallのMACアドレスからYAMAHRTXのMACアドレスに書き換わる時間
ARPテーブルのキャッシュ期間はコンピューターによりまちまちで、自身の環境では * google home: キャッシュ数秒-30秒程度 * Mac: 2-3分程度
となり、実質的には上記のARPキャッシュ時間がダウンタイムの時間と大きく直結する
やってみて
殆どの人がそうだと思うが、luaスクリプトを書いた経験がなく基本文法を調べながら書く為時間が取られた。script言語であればnodejsやpythonでもかけるようにしてほしい。(パフォーマンス面でluaが優れているとの記載があったので難しいかもしれないが)
本格的にscriptを書くとなるとYAMAHA用APIの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を作成する
環境
- golang 1.14
- echo v4系
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!") }
元の原因がgolangのhttp.Request().Body
の定義によるものなので、
別のFWでも考え方は使い回せると思う
API Gateway & lambda FunctionでRedirectするEndpointを作成するサンプル
Lambda + API GatewayでAPI Gateway宛に来たリクエストをRedirectするEndpointをAWS-CDKで作成してみた
構成
- API Gateway - Lambda FunctionをつなぐAWS-CDKを作成
- codeはnodejs(typescript or javascript)12系
- "aws-cdk": "1.27.0"
全体図
API
※ 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&amp;gt; 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年可動中
構成
仮想基盤サーバー2号機
ESXi1号機のバックアップ用途
構成
バックアップサーバー
Windows+余ったHDDで週一Wake on Lan起動。バックアップ取得後はシャットダウン
構成
構成は余り物 * CPU:Core i5 3570T * MEM:16GB * HDD:3TB*6
自宅サーバーの作り方2通り
既成品を買ってくる
NEC,HP、富士通あたりからPC紛いのエントリーサーバーを購入する
メリット
* 市販PCと比べてもかなり格安で購入できる。Celeron、Pentiumクラスなら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によるデータ冗長化
NIC:チーミング等を想定してIntelやBroadcomなど信頼性の高いものが良い。NIC自体が故障するよりもケーブルの抜き差しでダウンするケースが多い
信頼性に対する持論
単機能サーバー(ここではファイルサーバーを想定)
- 停止しても影響ないのでHDDのみOSによるRAID構成。
仮想基盤サーバー
- 停止すると上位のゲストOSが軒並み停止するため、単機能サーバーより信頼性を高めた構成とする
- メモリはECC構成。NICはIntelチップを利用したデバイスを複数ポート用意。HDDはSSDをHWRAIDによるRAID1構成
- 停止して困る仮想サーバーは乗せる物理サーバーを分けた上でアプリケーションレベルでの冗長化が望ましい
結局のところ、個人用途のサーバーなので如何に故障しないかを突き詰めるよりはある程度信頼性が低いのは認識した上で、きちんとバックアップを自動取得させることが費用対効果を考えると重要だと思っている。