int32のエイリアス型で、Unicodeのコードポイントを表す。
Unicodeは符号化文字集合や、文字符号化方式などを定めた文字コードの標準規格で、世界の多様な言語や書式、記号に番号を割り当てたもの。
符号化文字集合とは、コンピューター上で扱う文字や記号を重複しないように集めた文字セットのことであり、文字と文字に割り当てられた番号の対応表を指す。文字符号化方式は、文字に割り当てられた番号とコンピューターが扱うデータ形式を指す。
ex. 符号化文字集合:Unicode、符号化形式:UTF-8
Unicodeなどに割り当てられた番号をコードポイントと呼び、rune型はこのコードポイントを表すための型となる。
例えばUnicode U+0041(16進数では0041)は、基本ラテン文字に分類される文字'A'のコードポイントであり、rune型で表すと、次のようになる。
package main
import "fmt"
func main() {
var a rune = 'A' // シングルクォートだとruneが型
fmt.Printf("%T %U %#v\n", a, a) // int32 U+0041 65
}
int32型の65という数字は、0041の10進数表記したものであり、Unicodeのコードポイントを表している。
Goではstring型は読み取り専用のbyteのスライスである。
package main
import "fmt"
func main() {
s := "Hello, 世界"
fmt.Printf("%T %v\n", s, s) // string Hello, 世界
fmt.Println(s[:5]) // Hello
}
string型はrune型と異なり、コードポイントを保持しているわけではない。
string型はループの仕方によって挙動が異なる。
package main
import "fmt"
func main() {
s := "Hello"
// byteが取得できる
for i := 0; i < len(s); i++ {
fmt.Println(s[i]) // 72 101 108 108 111
}
// range loopの場合はruneが取得できる
for i, v := range s {
fmt.Printf("idx %d: %T %U %#v\n", i, v, v, v)
// idx 0: int32 U+0048 72
// idx 1: int32 U+0065 101
// idx 2: int32 U+006C 108
// idx 3: int32 U+006C 108
// idx 4: int32 U+006F 111
}
}
マルチバイトを扱う場合はループの挙動に注意を払う必要がある。
package main
import "fmt"
func main() {
s := "あ"
for i := 0; i < len(s); i++ {
fmt.Println(s[i]) // 227 129 130
}
// range loopの場合はruneが取得できる
for i, v := range s {
fmt.Printf("idx %d: %T %U %#v\n", i, v, v, v)
// idx 0: int32 U+3042 12354
}
}
日本語1文字は3バイトであるため、rangeを使わないループの場合は、1文字であっても3回ループが回る。
文字列数を取得するときもstring型がバイトのスライスであることに注意を払う。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "あ"
r := `あ`
fmt.Println(len(s)) // 3
fmt.Println(len(r)) // 3
fmt.Println(utf8.RuneCountInString(s)) // 1
}
lenはバイト長を返すため、文字数を取得したい場合はutf8.RuneCountInStringを使う。
go run $(go env GOROOT)/src/crypto/tls/generate_cert.go -rsa-bits 2048 -host localhost
cert.pemとkey.pemが用意できる。
openssl使ったりmkcert使ったりしていたけどGo使っていたらこれで良さそう。
]]>リクエストの遅延を乗算的に増加させる(リトライ間隔を遅延させていく)形で失敗したリクエストを定期的に再試行(リトライ)する手法。
ex. 1回目のリトライは1秒後、2回目は2秒後、3回目は4秒後、4回目は8秒後...
リトライ設計においては、バックオフのみに依存するのではなく、リトライ上限やタイムアウト(接続タイムアウトとリクエストタイムアウト)も考慮する必要がある。
指数バックオフのリトライ間隔にランダムな値を加えることで、同時に失敗したリクエストが同時に再試行するのを防ぐ手法。
単純な指数的間隔だとリトライ間隔が同じになってしまうため、時間的ゆらぎを持たせるために導入される。
簡易的に実装するとしたらこんな感じだろうか。
package main
import (
"fmt"
"log"
"math"
"math/rand"
"time"
)
// Retryer is a retryer.
type Retryer struct {
MaxRetryCount int
RetryCount int
Jitter *Jitter
}
func NewRetryer(mrc int) *Retryer {
return &Retryer{
MaxRetryCount: mrc,
RetryCount: 0,
Jitter: &Jitter{
base: 10,
cap: 100,
sleep: 10,
},
}
}
func (r *Retryer) Retry(ja string, f func() error) {
for i := r.RetryCount; i < r.MaxRetryCount; i++ {
var d time.Duration
switch ja {
case jitterAlgoFull:
d = r.Jitter.FullJitter(r.RetryCount)
case jitterAlgoEqual:
d = r.Jitter.EqualJitter(r.RetryCount)
case jitterAlgoDecorrelated:
d = r.Jitter.DecorrelatedJitter()
}
time.Sleep(d)
err := f()
log.Printf("retry %d times\n", i)
if err != nil {
log.Println(err)
// エラーなのでretryを継続
continue
}
}
log.Println("retry done")
return
}
const jitterAlgoFull = "full"
const jitterAlgoEqual = "equal"
const jitterAlgoDecorrelated = "decorrelated"
// Jitter is a retryer with jitter.
type Jitter struct {
base int
cap int
sleep int /// for decorrelated jitter
}
// FullJitter is a full jitter algo.
// sleep = random_between(0 min(cap, base * 2 ** attempt))
// see: https://aws.typepad.com/sajp/2015/03/backoff.html
func (j *Jitter) FullJitter(retryCount int) time.Duration {
sleep := rand.Intn(min(j.cap, (j.base * int(math.Pow(2, float64(retryCount))))))
return time.Duration(sleep) * time.Second
}
// EqualJitter is a full equal algo.
// temp = min(cap, base * 2 ** attempt)
// sleep = temp / 2 + random_between(0, temp / 2)
// see: https://aws.typepad.com/sajp/2015/03/backoff.html
func (j *Jitter) EqualJitter(retryCount int) time.Duration {
temp := rand.Intn(min(j.cap, (j.base * int(math.Pow(2, float64(retryCount))))))
sleep := (int(math.Ceil(float64(temp/2))) + rand.Intn(int(math.Ceil(float64(temp/2)))))
return time.Duration(sleep) * time.Second
}
// DecorrelatedJitter is a decorrelated jitter algo.
// sleep = min(cap, random_between(base, sleep * 3))
// see: https://aws.typepad.com/sajp/2015/03/backoff.html
func (j *Jitter) DecorrelatedJitter() time.Duration {
randomBetween := func(min, max int) int {
return rand.Intn(max-min) + min
}
sleep := min(j.cap, randomBetween(j.base, j.sleep*3))
j.sleep = sleep
return time.Duration(sleep) * time.Second
}
func init() {
rand.New(rand.NewSource(time.Now().UnixNano()))
}
func main() {
r := NewRetryer(5)
r.Retry(jitterAlgoFull, func() error {
return fmt.Errorf("retry error")
})
r.Retry(jitterAlgoEqual, func() error {
return fmt.Errorf("retry error")
})
r.Retry(jitterAlgoDecorrelated, func() error {
return fmt.Errorf("retry error")
})
}
ジッターのアルゴリズムは下記記事を参考にしたが、ちゃんと正しく反映できているかちょっと自信がない。ファジーなのでロジックに考慮漏れがある。 cf. aws.amazon.com - Exponential Backoff And Jitter
実装雑にしてしまったが雰囲気はわかった!
なんでこんな大遅刻かというと、唯一埋まっていなかった19日の枠を急遽埋めようと思って投稿したため。元々掴んでいた枠ではないので遅刻ではない。
去年くらいからGoのnet/httpに含まれるServeMuxの機能拡張の提案が出ていてウォッチしていたのだが、最近Closedになったらしい。
cf. net/http: enhanced ServeMux routing #61410 cf. GoでServeMuxの機能拡張を提案するProposalがAcceptedになった アプリケーション
この機能拡張はGo1.22に含まれる想定らしく、Go1.22rc2がリリースされたので試してみた。(Go1.22rc1から含まれているようだが、スルーしていた。)
tip.golang.org/doc/go1.22#net/httpに記載されている内容を見ると、以下のような変更点があるようだ。
ここを見た限りだとルーティングの仕様がどんな感じになったのかよくわからないので、ドキュメントの方を参照してみると色々書かれている。
cf. pkg.go.dev/net/http@go1.22rc2#ServeMux
Proposalの内容は全て追えていないのだが、Proposalで提案されていたルーティングのパターンマッチングの仕様がサードパーティのルーターのように充実した模様。
目立つところだけ端的にいうと、HTTPメソッド名やパスパラメータを使ったパターンマッチができるようになった感じ。
go.dev/dl - All releasesよりgo1.22rc2をダウンロードして触ってみる。
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
// HTTPメソッドがないパターンは全てのメソッドに一致する。
// {$}は特別なワイルドカードで、URLの末尾にのみ一致する。
// /{$}は/のみに一致するが、/は全てのパスに一致してしまう。
mux.HandleFunc("/{$}", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("/{$}"))
})
// GETメソッドはGETとHEADに一致する。
// {bar}はワイルドカード
mux.HandleFunc("GET /foo/{bar}", func(w http.ResponseWriter, r *http.Request) {
// パスパラメータの取得
v := r.PathValue("bar")
w.Write([]byte("GET /foo/" + v))
})
// {bar}はワイルドカード
mux.HandleFunc("POST /foo/{bar}", func(w http.ResponseWriter, r *http.Request) {
// パスパラメータの取得
v := r.PathValue("bar")
w.Write([]byte("POST /foo/" + v))
})
http.ListenAndServe(":8080", mux)
}
{$}やメソッドとルートを一緒に定義する形はサードパーティのルーターではあんまり見ないかもしれない。(自分が知らないだけかも...)
下位互換性については、pkg.go.dev/net/http@go1.22rc2#hdr-Compatibilityに記載されている。
Go1.21の仕様を引き継ぎたい場合は、httpmuuxgo121=1をGODEBUG環境変数に設定する必要があるとのこと。
自作ルーターであるgoblinはもはや使う必要がなくなってしまった気がするので、個人プロジェクトは新しいServeMuxを使っていくように切り替えていこうかと思う。
]]>3人以上のメンバーで1台のコンピューターを共有してプログラミングする手法のこと。
通常コードを書くドライバーと、それを支えるナビゲーターの二手に分かれて作業をする。
VS CODEのlive-shareという拡張が良くできていてとても使いやすい。
モブプログラミングの経験が浅いのでまだ練度が低いのだが、いくつか感じたことがある。
Goのマルチモジュール構成を便利するための機能。
次のような構成を用意する。
.
├── bar
│ └── bar.go
└── foo
└── foo.go
// foo.go
package foo
func Foo() string {
return "foo"
}
// bar.go
package bar
func Bar() string {
return "bar"
}
fooディレクトリで次のコマンドを実行して、go.modをセットアップする。
go mod init example.com/foo
同じく、barディレクトリで次のコマンドを実行して、go.modをセットアップする。
go mod init example.com/bar
次に、cmdディレクトリを作成して、main.goファイルを次のように作成する。
package main
import (
"example.com/bar"
"example.com/foo"
)
func main() {
println(foo.Foo())
println(bar.Bar())
}
cmdディレクトリでも同様にgo.modをセットアップする。
go mod init example.com/cmd
ここまでで、次のような構成になる。
.
├── bar
│ ├── bar.go
│ └── go.mod
├── cmd
│ ├── go.mod
│ └── main.go
└── foo
├── foo.go
└── go.mod
ルートのディレクトリにて、次のコマンドを実行し、workspaceの設定を行う。
go work init foo bar cmd
go.workというファイルが作成される。
go 1.21.1
use (
./bar
./cmd
./foo
)
go run cmd/main.go
が実行できることを確認。
// 実行結果
foo
bar
続いて、bazというモジュールを追加してみる。
.
├── bar
│ ├── bar.go
│ └── go.mod
├── baz
│ ├── baz.go
│ └── go.mod
├── cmd
│ ├── go.mod
│ └── main.go
├── foo
│ ├── foo.go
│ └── go.mod
└── go.work
// baz.go
package baz
func Baz() string {
return "baz"
}
go mod init example.com/baz
を実行して他のモジュールと同じようにgo.modを生成する。
続いて、main.goにbazを追加する。
package main
import (
"example.com/bar"
"example.com/baz"
"example.com/foo"
)
func main() {
println(foo.Foo())
println(bar.Bar())
println(baz.Baz())
}
ルートに戻り、go work use baz
を実行し、モジュールを追加すると、go.workにbazが追加される。
go 1.21.1
use (
./bar
./baz
./cmd
./foo
)
go run cmd/main.go
を実行して、出力にbazが追加されていることを確認する。
// 実行結果
foo
bar
baz
マルチモジュール構成が手間だなぁと思っていたので簡単になって良いなと思った。
言語依存のない形でHTTP APIの仕様を定義するためのフォーマット。YAMLまたはJSONで記述する。
Swagger SpecificationはOpenAPI Specificationの前身である。
スキーマ駆動による開発プロセスの効率化ができる。
致命的なデメリットはないように思える。
Dockerが利用できるのでDockerで試してみる。
cf. github.com - OpenAPITools/openapi-generator
docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate \
-i https://raw.githubusercontent.com/openapitools/openapi-generator/master/modules/openapi-generator/src/test/resources/3_0/petstore.yaml \
-g go \
-o /local/out/go
API仕様のドキュメントはVSCodeであればOpenAPI (Swagger) Editorが良さそうだった。
生成のオプションとかちゃんと見れていないので、そのへん確認できたら個人開発のプロジェクトで利用してみようと思う。
何を自動生成して、何を自動生成させないかを上手く調整するのが導入時の課題かなと感じた。
10年以上運用しているサービスのPHPとFuelPHPのアップデートを行ったプロジェクトのレポーティングをする。
PHP7.3は2021年12月にEOLを迎え、セキュリティアップデートが終了しており、アップデートを検討する必要があった。PHPに依存するFuelPHP1.8.2も同様の状況であった。
今回のプロジェクトでは単純にアップデートをすること自体が最適化どうかを検討する必要があった。以前のアップデートプロジェクト時とは異なり、FuelPHPの開発状況が停滞しており、FuelPHPを今後も利用し続けていくことが技術戦略上適切か検討しなくてはならなかった。
※FuelPHPの状況については以前、FuelPHPの2023年3月現在の現況にまとめたのでそちらを参照。
プロジェクト開始前の事前調査として、以下のような調査を行った。
調査事項に基づいて修正が必要な箇所を洗い出し、プロジェクト全体にかかる工数の見積もりを行った。
メインサービスで利用されているアプリケーションはいわゆるモノリスであり、複数の開発チームが開発を行っている。
開発組織の戦略としてこのモノリスをどのように刷新していくか?ということがここ数年大きな課題となっている。
メインサービスで使っているPHPやFuelPHPといった技術スタックやそのアーキテクチャは今後どのように刷新されていくか?ということも含めて、アップデートの戦略を定めた。
今回のアップデートの戦略として次のような複数のプランを検討した。
これらはそれぞれメリット・デメリットがあり(詳細は割愛)、比較検討した結果、「FuelPHPを継続して利用」のプランを選択することとした。
選択した理由としては次のようなものがある。
FuelPHPを継続利用していくことを決定したため、FuelPHP及びPHPをどのバージョンまでにアップデートするかを調査に基づいて決定した。
プロジェクト計画段階では、PHP7.3からのアップデートバージョンの候補としては、PHP7.4、PHP8.0、PHP8.1があった。(PHP8.2はまだリリースされていなかった。)
PHP7.4は2022年11月にEOLを迎えるため、プロジェクト期間中にEOLとなるため対象外とした。
PHP8.0は2023年11月がEOLであるが、プロジェクト完了後から短命でEOLを迎える見込みになるため同様に対象外とした。
FuelPHP1.8.2は公式のリリース情報によるとPHP7.3までのサポートとなっているため、FuelPHPもPHP8.1をサポートしているバージョンにアップデートする必要があった。しかし、利用していたPHP1.8.2が最新であり、次期バージョンがリリースされていない状況であった。
そこで2つほどプランを検討した。
検討の結果、下記の理由からFuelPHP1.9-developを利用することとした。
FuelPHP1.9-developはまだ正式なリリースがされていないが、調査してみると採用する余地があると判断できた。
FuelPHPをforkするよりも1.9-developを利用したほうが開発コストが低く抑えられるとも考えた。
ただし、「フレームワーク本体のテストカバレッジがかなり低いこと」や「リリースされても近い将来リリース予定のPHP8.2対応は期待できない可能性が高い」などといったリスクやデメリットはある。
別のアプローチとして、FuelPHPのコミッターになるというパワープレイも検討したが、FuelPHPのリリースサイクルを早めることにどこまで貢献できるか不確実性が高く、未知数であったため断念した。
アップデートプロジェクトの改修方針して、次のような方針を定めた。
方針に基づく上では、PHP7.3からPHP8.1へのアーキテクチャ変更を段階的に行うことが望ましいと考えた。
そのためにはPHP7.3とPHP8.1の両方の環境を並行して運用できる構成を構築する必要があった。
そのような構成の実現のため、5段階のフェーズを設け、段階的なアーキテクチャ変更を達成できるように計画した。
ステージング環境においてPHP7.3とPHP8.1を並行稼働環境を開始できる状態をつくるための準備期間としてのフェーズ。
このフェーズでは次のようなことを行った。
ステージング環境のみPHP7.3とPHP8.1の並行稼働環境として運用を開始するフェーズ。
QAの実施や負荷試験のテストを実施し、プロダクション環境を構築する前段階の検証を行った。
特に並行稼働のインフラ構成の仕組みを検証し、プロダクションでの運用で問題が発生しないかを重点的に検証した。
ステージング環境における並行稼働環境をプロダクション環境にも同様に展開し、運用を開始するフェーズ。
監視や運用開始で生じたバグ対応などを行い、プロダクション環境での運用を安定させ、PHP8.1への完全切り替えができる状態を目指すことを目的としている。
PHP7.3とPHP8.1の並行稼働状態であるステージング・プロダクション環境をPHP8.1のみ稼働状態に切り替え、運用が安定するか検証していくフェーズ。
Phase2.5の段階である程度は安定していることを予測しているが、PHP8.1のみの運用環境となることでトラフィック量が増えるため、慎重を期してこのフェーズを設けた。
このフェーズでは、PHP7.3環境関連のインフラリソースを残存させておくことで、PHP7.3環境への切り戻しを行うことができるようにしておいた。
PHP7.3環境関連の各種インフラリソースや、並行稼働のために残存させていたPHPバージョン分岐のコード等を削除し、PHP8.1環境への完全切り替えを行うフェーズ。
このフェーズでは、PHP7.3環境への切り戻しは基本的に不可能となる。(やろうと思えばできるが、手早い切り戻しはできない。)
依存パッケージを除くアプリケーションのソースコードは大きく分けて2つのパターンがあった。
前者は単純に改修するだけで良いが、後者はPHPのバージョンで条件分岐を行い、それぞれのバージョンで正常に動作するように改修を行う必要があった。
// コード例
if (version_compare(PHP_VERSION, '7.4.0') < 0){
// 7.4.0未満のコード
}
if (version_compare(PHP_VERSION, '8.1.0') >= 0) {
// 8.1.0対応コード
}
このような条件分岐をヘルパー関数として定義し、一種のフィーチャートグルのような形で各改修箇所にて利用した。
一方で依存パッケージについては3つのパターンがあった。
1つ目のパターンは単純にアップデートするだけで良く、それ以外のパターンはそれぞれの対応が必要となった。
2つ目のパターンはPHP7.3とPHP8.1のそれぞれの環境向けにcomposer.jsonのファイルを用意することで対応した。
この対応により、Phase3.0まではそれぞれのcomposer.jsonファイルには同じ依存パッケージを指定する必要が生じてしまうが、ライブラリ追加は頻繁に発生しなかったため、大きな手間とはならなかった。
3つ目のパターンは2件該当するケースがあった。
1つはruflin/ElasticaというPHPのElasticsearchクライアントライブラリのfork対応であった。
サービスが利用しているElasticsearchのバージョンがかなり古く、PHP8.1対応バージョンのruflin/Elasticaを利用することができなかった。
そのため、ruflin/Elasticaをforkし、PHP8.1対応を行うことで対応した。(アップデートプロジェクト完了から半年頃、Elasticsearchを使っている一部機能がElasticsearchのクライアントライブラリを必要としなくなったため、forkしたリポジトリはお役御免となった。)
もう1つは社内ライブラリのfork対応であった。
社内ライブラリはメインサービスのアプリケーションとはPHP7.3で運用されている別の社内サービスでも利用されていたため、PHP7.3とPHP8.1でそれぞれ動作するように社内ライブラリを運用する必要があった。
社内ライブラリではcomposer.jsonファイルをPHPのバージョン別に分けて、PHPのバージョンで条件分岐するように処理を振り分けるような改修をすることができればforkする必要はなかったが、良いアプローチが思いつかずforkする対応となった。
この対応により、Phase3.0まではfork元とfork先で同じ仕様が保たれるように同期する気をつける手間が生じてしまったが、頻繁に仕様変更が入るようなライブラリではなかったため、あまり大きな手間とはならなかった。
アップデートプロジェクトの完了後は、fork元とfork先はそれぞれ別のものとして運用していくことがサービスの仕様上許容できたため、同期対応はPhase3.5以降は不要となった。
PHP7.3とPHP8.1の並行稼働環境を構築するにあたり、既存の実行環境に手を加える必要があった。
既存の実行環境はALB+ECSで構成されており、WebサーバーとしてNginxを利用している。
この既存の実行環境を次の要件を満たすように改修した。
改修のアプローチとして、
などいくつか方法を検討したが、最終的にはCloudFrontのContinuous Deploymentという当時リリースされて間もなかった機能を利用することにした。
cf. Using CloudFront continuous deployment to safely test CDN configuration changes
この機能はCloudFrontのDistributionをPrimaryとStagingの2つに分けてトラフィックを分散することができるという点で要件に合致していた。
トラフィックの分散条件はWeight-basedという重み付けを行う方法を採用した。
この方法は振り分け先に対して全リクエストの0~15%までしか割り振ることができないという制約があるが、最大15%の状態で一定期間トラフィックを受け付けることで切り替え判断に必要な十分なトラフィックが集まると判断した。
最終的な構成は次のようになった。
PHP7.3とPHP8.1の並行稼働環境を運用する都合上、メインブランチをPHP7.3とPHP8.1の両方で実行するようにコード改修を行ったため、PHP7.3とPHP8.1の両方の実行環境においてQAを実施する必要が生じた。
QAの実施にあたっては、サービス全体の網羅的なテストケースを用意し、UI上でそれを実施していく形を取った。
PHP8.1へのアップデートにより性能劣化がないことを検証するため、k6を使った負荷試験を実施した。
想定するトラフィックのパターンごとにテストを実施したところ、レスポンスタイムはおおよそ25%程度の改善が見られた。
計画的に実施したおかげか大きな問題が起きることもなく(解決が困難な問題は一部あった)、アップデートプロジェクトを完遂することができた。
このプロジェクトは当初自分含めて3名のエンジニアで構成されたチームで走り出したのだが、うち2名とも育休で各々別々のタイミング途中離脱し、他メンバーへの引き継ぎを行うというリレーのようなプロジェクトであった。
そのような体制であってもドキュメンテーションや社内周知等の徹底により、プロジェクトの遂行への影響は最小限に抑えることができたように思う。
複数のチームが触るモノリスのアプリケーションのアップデートは関係者が多く、コミュニケーションコストが高くなりがちだと思うが、適切に計画することで難なくアップデートができることを実感した。
一方で今後の課題と感じる部分も少なくなかった。
テストカバレッジの不十分さ、デッドコードの多さ、古いまま更新されない依存ライブラリ、QA実施の効率性など、アップデートプロジェクトを通して今後日頃の改善が必要だと感じる部分がいくつかあった。
自分は現職では2回目となるアップデートプロジェクトであったが、前回とは組織の構成やアーキテクチャの構成、アプリケーションの状態も異なり、気を付けることが一層増えたように感じた。(これも課題である・・)
次回のアップデートはどうなるか(forked FuelPHPを考えるのか、別の戦略を取るか・・)、FuelPHPの未来はどうなるのか(次のリリースはあるか・・)、不透明感が拭えないが、今回のアップデートプロジェクトの知見を次回にも活かせるよう努めたい。
]]>今年も1年を振り返って来年の抱負を記す。
昨年やれなかったことを今年はやれたか振り返ってみる。
Kubernetes→CKAの取得には至らなかったが、勉強にはある程度時間を使った。
AWSやGCPの知識の幅を広げる→AWSは実務で多少はできた。GCPはGCP Associate Cloud Engineerを取得した。
ISUCON→今年は参加することができた。
外部カンファレンス登壇→実は採択されたイベントがあったが、諸事情により辞退した。来年度のイベントで採択されたCFPがあるので、来年は登壇できそう。
大体昨年やれなかったことは今年ある程度やることができたと言えそう。
毎年自分のやりたいこと(興味・関心に基づくこと)、やるべきこと(今後のキャリアのためにやっておきたいこと)、やる必要のあること(仕事上やったほうが良いこと)を整理して、1年間のやることや目標を決めているのだが、今年はこんな感じだった。
3以外は時間を使うことができたので、ある程度やりたいことはやったかなという感覚がある。
色々とインプットしたことをアウトプットすることを日常化できたし(インプットとアウトプットをセットにすることを当たり前化できた)、コーディングクイズをルーティンにすることなどもできた。日々の積み重ねが実を結ぶ系のことはサボりがちだが、モチベーションや時間の使い方を上手くコントロールすることで持続的に取り組むことができるようになった。
一方で、やれなかったこととしては、作りたいソフトウェアを1つ作り上げることに時間を使うことがあまりできなかった。コーディングクイズでコードを書く機会はあったものの、それ以外であまりコードを書けていないかも...
今年は育児休業を半年取得していたため、仕事は半年間だけだった。
半年なのでやれたことは例年より限られるが、以前から着手したかったプロジェクトに漸く着手することができたのは良かった。
半年も仕事から離れたのは久しぶりだったのだが、思った以上にすんなり復帰できたり、復帰前より少し成長を感じた部分などもあって全くネガティブなことはなかった。(育休中隙間時間に短時間集中で爪を研いでおいたのが無駄ではなかった)
育休をきっかけに、できるだけ効率良くやることをこれまで以上に意識するようになったり、生活リズムがより健康的になったのは良かった。今後も継続したい。
家庭の事情にも拠るだろうが自分は育休を取れたことはとても良かった。子どもの成長は驚くほど早いのでその成長を近くで見守れるのはかけがえのない時間は沢山確保したい。
2023年の抱負はこう書いていた。
「武器をつくる」を抱負にしたい。 これは2021年の抱負の延長線上にあるものではあるが、少し違うことを意識しようと思う。 ソフトスキルではなく、ハードスキルで自分の得意とする領域を何か持つ、ということを具体的には意識している。 誰にも負けない、その領域の先駆者、専門家、というレベルの到達は1年程度でできると考えていないので、あくまで自分が得意だと言い切れる> > ものを1つ以上持てるようにしたいと思っている。
抱負にはこう書いていたものの、何かを深めるというより、幅広く浅くやっていた年だったので抱負とはかけ離れた方向に走っていた気がするが、得意といえるものを掴むための種まきくらいはできたように思う。
「武器を増やす」を抱負にしたい。
ソフトスキルかハードスキルかは問わず、リスクヘッジとしてのスキルの掛け合わせをより強固にするには、自分が得意、あるいはできるといえることを増やす、あるいは伸ばすしかないかなと漠然と考えている。
向こう10年を短期(~35歳)と長期(35歳~40歳)に分けて、どういうキャリアにしたいか?どういうポジション、成果を出したいか?を考えているのだが、それについては今年も考えがブレる部分は少なく、キャリアについての考えが安定してきている。
一方で、キャリアプランに向けて自分がどういうアクションを取るべきか?ということに関しては、今年大きく台頭した生成AIのおかげで迷いが生じている部分がある。
とはいえ、生成AIの進化に応じて今後どういうスタンスを取るべきか?ということについてある程度の考え※を自分の中で整理はしているので、それに賭けつつ、今後を見据えていきたい。
来年はライフステージの変化で、可処分時間をどう捻出するか、時間をどう効率的に消費するかということがよりシビアになりそうな予感がしている。
時間的な話だけではなく、人生における価値観や、優先度、ライフプランなど色んなことに変化が生じると思う。
そんな中で自分がソフトウェアエンジニアとしてどう在りたいか?という姿も変わっていくのではないだろうかと思う。
来年も公私ともに良い年になることを願いたい。
※ざっくりいうと、生成AIを扱うには生成AIが生成するものを理解できる程度の能力と生成AIに理解してもらえるだけの指示が出せる能力が人間には必要と考えている。知識を何らかの成果物に変換するのをAIに任せても、知識の源泉自体は人間なので、学びを捨ててはいけないだろうと考えている。すべてをAIに任せられるくらいの社会が来るならそれはシンギュラリティだと思うのでその時の未来は考えていない。。。
プライベートでやっておかねばならないことがいくつかあった。
30歳の誕生日を迎えてからしばらくはまだまだ20代のつもりで過ごしていた時期もあったが、この1年で心身ともに老いたように感じる。。。
人生をより堅実なものにしていかないといけないようなプレッシャーとか保守さが少しずつ湧き出しているの気がするので、フレッシュさを保てるよう日々色んな刺激を得るようにしたい。
]]>「LeanとDevOpsの科学」は読んでおきたい。
]]>ソフトウェアアーキテクチャと組織の両方の視点で設計論を言語化するには色々学びが足りないと感じた。多分、今後のキャリアでそういうことに時間を使って向き合うことがもしかしたらあるかもしれないので、視野は広げておきたい。
「チームトポロジー」や「エンジニアリング組織論への招待」などは一読しておきたいかも。
あと組織設計という方だけに着目するなら経営学をベースにした研究とか、事例とか色々ありそう。そのへんは興味あるけど不勉強なのでどこかのタイミングで学んでみたい。
]]>ざっくり調べた感じ次のような特徴があると分かった。
GrafanaでContact Pointsがプロビジョニングがいつの間にかできるようになっていたので、プロビジョニングを設定してみる。
雑メモなのでDocker Composeを使ったビルド周りの説明は割愛する。bmf-san/gobel-exampleを参照。
provisioning/alerting
配下にcontact-points.ymlとpolilcies.ymlを追加する。ファイル名は任意で良い。
└── provisioning
└──alerting
├── alert-rules.yml
├── contact-points.yml
└── policies.yml
下記はwebhookでSlackに通知する設定例。
apiVersion: 1
contactPoints:
- orgId: 1
name: Slack
receivers:
- uid: abc1234
type: slack
settings:
recepient: alert-slack-channel-name
url: [webhook url]
disableResolveMessage: false
uidはプロビジョニングする場合は自分で採番可能であるので何でもOK。
recepientはSlack通知先チャンネル名。
urlはwebhook urlを設定。tokenを使う形にもできる。
cf. grafana.com - #provision-contact-points
Policiesもプロビジョニングするようにしないとデフォルトで用意されているものが利用されてしまうため、プロビジョニングしたContact Pointsの設定を反映し、Alertの通知を行うためにはPolliciesのプロビジョニングも必要になる。(他にもやりようがあるのかは詳しく 調べなかった。)
下記はSlackという名前でContact Pointsを作った場合の設定例。
apiVersion: 1
policies:
- orgId: 1
receiver: Slack
receiverはContact Points名を指定する。
設定内容は最低限のみ。
cf. grafana.com - #provision-notification-policies
このような形でプロビジョニングすれば任意のContact PointsにAlertの通知が飛ぶように設定ができる。
今まで手動で設定していたので楽になった。
]]>ソフトウェア開発のプロジェクトにおける不確実性について、自分が日々どのように向き合っているかを言語化してみる。
アジャイル関連の本を読み漁ったり、PMBOKの勉強をしたりと体系的に学んできたわけではないので、経験に基づいた主観的な話になる。
自分がどのような環境で不確実性に向き合っているかを前提として話を進めたい。
私は技術基盤を設計・開発・運用するチームに所属しており、チームリーダー(プロジェクトをリードする責務とチーム内アーキテクトとしての役目を持っている)を務めている。
チームが取り組むプロジェクトは、
といった特徴がある。
受託開発であったり、よりエンドユーザーに近い開発を行うようなチームの場合は、不確実性に対するアプローチは異なるのではないかと思うところがあるが、根幹的な部分はどんなプロジェクトでも通ずるところがあるのではないかと思っている。(たぶん)
不確実性への向き合い方について言語化する前に、不確実性についての定義を確認しておきたい。
不確実性とは一般に、「未来の出来事や状況を正確に予測ができない、難しい状態」のことを指す。
ソフトウェア開発におけるプロジェクトの不確実性もこの定義に当てはまると考えて良さそうに思う。
プロジェクトにおける不確実性の要因は様々だが、大別すると「外部要因」と「内部要因」があると考えている。
プロジェクトの外側にあり、プロジェクトに影響を与える要因を外部要因と考えている。
市場の変化や法規制、自然災害などコントロールすることが難しい、あるいは不可能である要因のことを指す。
プロジェクトのメンバーやステークホルダーが頑張っても調整がつかないようなものは外部要因に分類される。
内部要因はプロジェクトの内側にある要因で、プロジェクトのメンバーのスキルや、プロジェクトのスケジュール、プロジェクトの予算など、プロジェクトのメンバーやステークホルダーがコントロールできる要因のことを内部要因と考えている。
チームがプロジェクトにおいて向き合うべきは大抵この内部要因であると考えている。
外部要因と内部要因の間に位置づけされそうな要因もあると思うが、それはコントロールできるかどうか、優先的に向き合う必要があるかどうかを見極めて対処する必要があると思う
例がイマイチだったかもしれないが、外部や内部といった分類は重要ではなくて、不確実性への対処や防止のアクションができそうかどうかという点が一番の考慮事項かと思う。
不確実性の高い・低いはプロジェクトの計画性に影響を与えると考えている。
逆にいうと、プロジェクトの計画性が高い状態だと不確実性を排除できている可能性が高く、低い状態だと排除できていない可能性が高いのではないかと考えられると思う。(もしかしたらそういう研究があるかもしれないが、ソースは探していないのであくまで自論である。)
計画性が高いと、リソース(人・金・時間など)の最適化、変更(計画、仕様、その他なんでも)への柔軟性、ステークホルダーの信頼関係向上などいくつものメリットを生む。
不確実性を極力取り払い、計画性を高めることがプロジェクトを成功させる上での鍵の1つだと思う。
不確実性を取り除くための前提として、計画性を常に観測・評価できるようにしておく必要がある。
例えば、プロジェクトの計画を表にしたロードマップやガントチャートなどプロジェクトの進捗を視覚的に把握・観察・評価できるものをチームの活動に合わせて用意する。
プロジェクトの計画性を評価するタイミング(スクラムならスプリントレトロスペクティブのイベントが丁度良いのではないだろうか。)で、何が計画性に影響を与えたのか?改善するためには何が必要か?などを振り返り、プロジェクトの進行に合わせて計画性を高めるためのアクションを実施する。特に何が不確実だったのか?を明らかにすることがチームにとって計画性を高めていくための学びであると考えている。
実務での例を何か取り上げたかったが、前提として共有することが多くなりそうで手間がかかりそうなので割愛する。
プロジェクトの計画性を評価するタイミングはチームによって様々だろうが、自分の場合は常に観察・評価するようにしている。いち早く遅延になりそうなこと(≒不確実性が高そうなこと)に気づけば、時間的な優位性を持って計画を見直したり、作業を調整したりすることができると考えているからである。
不確実性を排除すれば計画性が高まり、ハッピーになれると語ってきたが、肝心の不確実性はどのように発見することができるのか考えてみる。
普段私はプロジェクトの中で、「わかっていないことを見つけること」を意識しているのだが、それを少し掘り下げてみると、要は「既知の未知」と「未知の未知」を見つけることなのではないかと考えた。
「わかっていないということをわかっていること」というのが既知の未知である。
例えば、ある程度仕様を把握している既存システムを別のシステムと連携する際に、利用すべきAPIやその呼び出しのシーケンスなどは見えているが、実際に連携した際にどのような不具合が生じるかわからないといったことが該当する。
既知の未知は、わかっていないことをどのようにしたらわかるようになるかを考えることができるため対処しやすい。
「わかっていないということをわかっていない」ということが未知の未知である。
例を上げると、プロジェクトの進行途中で突然仕様変更の要求が発生するといったことが該当する。
未知の未知は、もはや予期しないことであるため、対処しにくい。事前に多角的な予測をする、時間的な余裕(バッファ)を持つなど投資的な対処が必要となる。
プロジェクトの中で「既知の未知」と「未知の未知」を発見・察知するために常に目を光らせておくことが大事であると考えている。
実際にどうやって発見・察知するかいくつかパッと思いつくところを書き出してみた。
多分、演繹的だったり、帰納的だったりそういう思考プロセスをしているのではないかなと思うが、言語化するほど考えを整理できていないので、今後の課題にする。
不確実性を見つけることができたら、対処を検討する必要がある。
発見・察知した不確実性の対処方法について、いくつか持っているアイデアを言語化してみる。
ケースバイケースだと考えているせいかあんまり思いつかなかった。。。
不確実性への対処例として、タスクのプランニングを取り上げてみる。
小さくタスクを分解することで見積もりがしやすくなるため、精度が上がる可能性がある。
タスクの依存関係も整理できていればタスクの並列化も考えやすくなる。 (タスクの分解可能性が低い場合はこの限りではない。)
タスクが大きければゴールやタスクでやることの認識もブレやすいので、そのような不確実性は極力排除したい。
タスクがどういう状態になったら完了なのかを明確にすることは進捗の追跡、品質担保、リソース最適化(どのタスクを優先するか、時間を投資するかなど)などに役立つ。
タスクの完了定義を決めづらい、決めれれない場合はそのタスクの分解粒度が適切ではないか、あるいは不確実性が眠っている可能性があるのではないかと思う。
一定のまとまりのタスクを完遂するにあたり、チームメンバー間の共通認識のブレという不確実性があると思う。
タスクの分解粒度を考える上で、共通認識を形成することを意識すると不確実性の排除につながると思う。
例えば、何かの調査をするタスクがあったとする。
ex. 「〇〇の調査をする。〇〇の調査結果をドキュメントにまとめ、調査結果から〇〇を決める。」
このようなタスクにおけるブレとなるポイントは、調査の観点や調査結果のドキュメントの形式、調査結果から〇〇を決める際の妥当性、判断軸などがある。
このタスクは、
などタスクを分解し、共通認識を持つマイルストーンを設定することでブレ(不確実性)を無くすことに繋がると思う。
あくまで一例なので、チームによって適切なアプローチは異なると思う。(上記は自チームでの実例。)
不確実性への向き合い方について言語化してみたが、思ったより感覚的な部分に頼っているように感じた。
どこかで体系的に学ぶ機会があればまた思考を整理してみたい。
構成図レベルでのアーキテクチャの設計をする時の進め方として個人的に良いと思ったやり方を1つ取り上げてまとめておく。
事前に要件定義や調査がある程度済んでいるという状態を前提として、アーキテクチャの構成図を書きながら設計を進めていく作業の取り組み方法について書く。
といってとても単純で、「最初からゴールの絵を描こうとせずに、段階を踏んで設計する」というだけである。
上図のように、要件に基づいて段階的に構成図を書いていくことで次のようなメリットがあると思っている。(上図はあくまで一例であり、雑に書いたものなので具体的なシステムを意識していない。)
設計に関わる人が多いほどこのアプローチは有用なのではないかと思っている。
人の認識を揃えるというのは不確実性の高いことの1つなので、段階的に前提を揃えながら考えていくというのは効果的なのではないかと思う。
]]>GoReleaserは、Go言語で書かれたアプリケーションのビルド、パッケージング、およびリリースを自動化するツール。
クロスコンパイル、バイナリの圧縮、アーカイブの作成、GitHubなどのプラットフォームへのアーティファクトのアップロードができる。
Github ActionsにGoReleaserの公式Actionが用意されているので、それを使うことができる。GoReleaserは設定ファイルを用意することもできるが、特に用意しなくても使うことができる。
cmdディレクトリ配下をビルドする想定でworkflowの実装例を記載する。
ビルドしてバイナリを配布することができるかどうかCIのプロセスに組み込んでおくと、リリースする際に配布できなかった・・なんてことが避けれる。
name: Dry run GoReleaser
on: [push]
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.21.x' ]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Dry run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --rm-dist --skip-publish --snapshot
workdir: cmd
タグリリース時にバイナリ配布を実行する。このジョブが完了すると、GitHubのリリースタグのページに成果物が添付される。
name: GoReleaser
on:
push:
tags:
- '*'
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.21.x' ]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --clean
workdir: cmd
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
これはまだ開発中のアプリケーションだが、こんな感じで配布することできる。
https://github.com/bmf-san/gondola/releases/tag/0.0.3
アプリケーションの実装がツールに依存することもなく、簡単に使うことができるので気に入った。
類似のツールは他にもあるが、とりあえずGoReleaserをしばらく使ってみようと思う。
]]>.PHONY:help
help: ## Print help.
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
define ADR_TEMPLATE
# TITLE
## 背景
## 決定
## ステータス
提案済み
<!--
提案済み/承認済み/棄却...
-->
## 結果
endef
export ADR_TEMPLATE
.PHONY: adr
adr: ## Create a new ADR. ex. make adr title=タイトル
@if [ -z "$(title)" ]; then \
echo "タイトルが設定されていません。 'ex. make adr title=タイトル'"; \
exit 1; \
fi
adr_number=$$(ls adr/ADR*-*.md 2>/dev/null | awk -F- '/ADR[0-9]+-/{match($$0, /[0-9]+/); print substr($$0, RSTART, RLENGTH)}' | sort -n | tail -n 1); \
adr_name=ADR$$(($$adr_number + 1))-$(title); \
echo "$$ADR_TEMPLATE" | sed -e "s/\TITLE/$$adr_name/g;" > adr/$$adr_name.md; \
echo "New ADR created: adr/$$adr_name.md"
ADRのファイル命名規則をADR<インクリメント可能な数値>-タイトルとしているので、adrディレクトリ配下のファイルを見て適切なファイル名でADRのテンプレートファイルを生成するコマンドになっている。
ADR1-foo.mdというファイルがあれば、ADR2-bar.mdといった感じで数値をインクリメントしてくれる。
ADRをgit管理下にすると、ADRごとのステータスを把握しづらくなるので何かしらの対応が必要かもしれない。
ステータスごとにリストアップするコマンドを用意する、ステータスごとにディレクトリを分ける、ステータスをファイル名に含めるなど工夫が必要。
]]>GraphQLの素振りをしていたので調べたことについてまとめておく。
親切なチュートリアルが用意されており、始めやすい。 cf. www.howtographql.com
Meta社によって開発されたWeb API開発のためのクエリ言語。
GraphQLはGraphQL Foundationによって管理されており、Meta社はその一員である。
GraphQLの仕様と全ての関連プロジェクトはOSSとして公開されている。
いくつかピックアップしたものだけ記載。
クエリの型定義。
type Query {
user: User
}
データ取得のためのクエリ。
query {
user {
name
}
}
データ更新のためのクエリ。
mutation {
updateUser {
name
}
}
データの変更を監視するためのクエリ。
subscription {
user {
name
}
}
クエリにわたす引数。
{
user(id: 123) {
username
email
}
}
gRPC、OpenAPI、Swagger、oData、SOAP、GraphQL等々のAPI仕様で実装されたAPIへのゲートウェイサーバー(GraphQL Gateway)。
API仕様さえあればAPIに対してGraphQLクエリでアクセスできる。
cf. the-guild.dev
OpenAPIに基づいたAPI仕様をGraphQLのSchemaに変換する。
cf. github.com - IBM/openapi-to-graphql
GraphQLのSchemaを作成するための便利ツール。モックを作成することもできる。
cf. github.com - ardatan/graphql-tools
Restful APIと比較して始めるのに少し手間はかかりそうだが、得られる恩恵は大きそう。
GraphQLクライアントは色々種類があるようで、選定には悩みそう。
cf. user-first.ikyu.co.jp - あなたのプロダクトに Apollo Client は必要ないかもしれない
サービスメッシュについて調べたことをまとめる。
サービス間(分散システム)の通信を管理するためのネットワークインフラストラクチャのこと。
一般的にはサービスにプロキシをサイドカーとして追加することで構成する。
以下は一般的なプロキシを必要とするサービスメッシュにおけるデメリット。 プロキシレスなサービスメッシュ(ex. Traffic Director)はその限りではない。
CircleCIでphpunitの並列テストを行うアプローチについてかく。
#!/bin/sh
basePath="/foo/bar"
testFiles=$@
xmlFileStringData=""
for file in $testFiles; do
xmlFileStringData="${xmlFileStringData}<file>${basePath}/${file}</file>\n"
done
testFileString="$xmlFileStringData"
template="<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<phpunit colors=\"true\" stopOnFailure=\"false\" stopOnError=\"false\" failOnWarning=\"false\" stderr=\"true\" bootstrap=\"path/to/bootstrap\">
<php>
<ini name=\"memory_limit\" value=\"1G\"/>
<ini name=\"realpath_cache_size\" value=\"1M\"/>
</php>
<testsuites>
<testsuite name=\"Test Suite\">
${testFileString}
</testsuite>
</testsuites>
</phpunit>"
echo "$template" > "path/to/ci_phpunit.xml"
こんな感じのスクリプトを用意して、設定ファイルを自動生成する。
shellscriptを書いているのはテストの実行をコンテナで行っている都合上、CIのjobでコンビニエンスイメージを使っているため、他に都合良い言語がなかった。
先程用意したスクリプトをgenerate_phpunit.shとして、次のようなスクリプトで並列化の準備ができる。
circleci tests glob "path/to/testdir/**/*.php" | circleci tests split | xargs sh +x generate_phpunit.sh
後はテスト実行時に生成した設定ファイルを指定すれば、複数コンテナでテストが実行され、並列化ができる。
path/to/phpunit -c path/to/ci_phpunit.xml
docker-composeでテストする場合の一例。
docker-compose -f docker/docker-compose.test.yml run test ash -c "path/to/phpunit -c path/to/ci_phpunit.xml"
これは実際に業務でトライしたことだったのだが、テスト間の実行順に依存関係があるらしく、並列化でのテスト実行を簡単に実現することができなかった...
まずは依存関係を何とかする必要がある...
2023年10月現在、latestである9.3.5をインストールしてもPHP8.2の互換性チェックはできない。
9.3.5は2019年リリースで、最近のPHPのバージョンにまだ対応していないらしい・・・
もしかして開発止まってる?と一瞬思ったが、その様子はない。
developにはコミットが積まれているのでどうやらdevelopを使えばよいらしい。
cf. Should I use develop or 9.3.5 sniffs? #1653
100%の互換性チェックをサポートしているわけではないと思うが、便利なツールなので今後も使っていきたい。
]]>Goでlog/slogを使ったcontextual loggingについてまとめる。
Go1.21で追加された構造化ロギングのためのパッケージ。
構造化ロギングとは、ログを構造化されたデータとして出力すること。
今まではGoで構造化ロギングをする場合はサードパーティのパッケージを利用するか、自前でスクラッチするかしか手段がなかったが、今後は標準パッケージも視野に入るようになった。
デフォルトではテキスト形式またはJson形式で出力することができる。
slogはcontextを持たせることができるため、リクエストベースの情報を詰めることもできる。
ログにリクエストベースの情報を持たせるLoggerを作成するコードを書いてみる。
以下は、contextにトレースIDを持たせてログ出力を想定したコード。
slog.Handlerインターフェースを実装することで、自前のHandlerを作成することができる。[]
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"github.com/google/uuid"
)
// TraceIDHandler represents the singular of trace id handler.
type TraceIDHandler struct {
slog.Handler
}
type ctxTraceID struct{}
var ctxTraceIDKey = ctxTraceID{}
// Handle implements slog.Handler interface.
func (t TraceIDHandler) Handle(ctx context.Context, r slog.Record) error {
tid, ok := ctx.Value(ctxTraceIDKey).(string)
if ok {
r.AddAttrs(slog.String("trace_id", tid))
}
return t.Handler.Handle(ctx, r)
}
// WithTraceID returns a context with a trace id.
func WithTraceID(ctx context.Context) context.Context {
uuid, _ := uuid.NewRandom()
return context.WithValue(ctx, ctxTraceIDKey, uuid.String())
}
func main() {
mux := http.NewServeMux()
handler := TraceIDHandler{slog.NewJSONHandler(os.Stdout, nil)}
logger := slog.New(handler)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
ctx := WithTraceID(r.Context())
logger.InfoContext(ctx, "Log with TraceID")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "Contextual Logging!")
})
http.ListenAndServe(":8085", mux)
}
上記のコードを実行すると、次のようなログが出力される。
{"time":"2023-10-08T17:06:44.423859+09:00","level":"INFO","msg":"Log with TraceID","trace_id":"4f9a0bb6-cf8d-4eef-82ea-2385b76d3a74"}
構造化ロギングには自前のパッケージを使っていたが、go1.21のリリースとともに差し替えをした。
尺取り法についてまとめる。
英語だと、Two Pointer ApproachまたはTwo Pointer Techniqueと呼ばれる。
データセット(数列や文字列など)の右端と左端のインデックスを保持して、条件によって左右のインデックスを移動させることで、条件を満たすデータを探索するアルゴリズム。
特定の条件を満たすデータを区間の中から探索したいような時に役立つ。
配列nの中から指定された数値m未満になる数字のペアがいくつあるかを求める関数を書く。
ex. n = [1, 3, -1, 2] m = 4 の場合、(1, -1)、(1, 2)、(3, -1)、(-1, 2)の4つのペアが該当するので4を返す。
関数を愚直に実装した場合のコードは次の通り。
func findPairs(n []int, m int) int {
rslt := 0
for i := 0; i < len(n); i++ {
for j := i+1; j < len(n); j++ {
if (n[i] + n[j]) < m {
rslt++
}
}
}
return rslt
}
これだとO(N^2)の計算量になってしまうので、尺取り法を使ってO(N log N)にする。
func findPairs(n []int, m int) int {
sort.Ints(n)
cnt, l, r := 0, 0, len(n)-1
for l < r {
if n[l] + n[r] < m {
cnt += r-l
l++
continue
}
r--
}
return cnt
}
左右のインデックスが重なり合うまで繰り返すことで、条件を満たすペアを全て探索できる。
ソートを行っているのは、効率的にペアを見つけやすいようにするためである。
r-lが直感に反するような気がするが、ペアを探すためのコードなので、配列内の値そのものではなくインデックスに注目してペアをカウントすれば良いため、このようなコードになる。
モジュラモノリスについて調べたことをメモする。
モジュラモノリスとして開発し、マイクロサービスとしてデプロイするためのツールとして、GoogleがService Weaverというものをリリースしている。
モジュラモノリスというものに少しの夢を見ていたのだが、所感としてはポエムを書きたくなったので綴った。
組織の拡大に合わせてアーキテクチャも進化が必要になってくるのが筋だと思うが、組織のスケーラビリティに柔軟に対応できる、あるいはコストがかかり過ぎない銀の弾丸のようなアーキテクチャがないかなぁと思ったりした。(ない。)
組織は拡大したり、縮小したり、変化しなかったりするような時もあるとは思うが、会社は成長を前提とするので組織のスケーラビリティには前のり気味で投資していくのが良いのかなぁなどと思った。
このへんの話に関して引用したい文章があったので、そちらを記載して締めとする。
しかし、ここで注目しなければいけないのは、両者のライフサイクルの違いです。 組織やチーム配置は、その気になれば会社の方針次第で翌日から変更できます。 しかしアーキテクチャやシステムは、組織のようにすぐ変更することが困難です。
cf. eh-career.com - モジュラモノリスに移行する理由 ─ マイクロサービスの自律性とモノリスの一貫性を両立させるアソビューの取り組み大規模
CQRSについて調べたことをメモ。
このパターンを導入すると決めるのは結構ハードルの高さを感じる。。。
海外では事例が多そうだったが、日本ではまだまだ事例が少ないというのが分かった。
Sagaパターンについて調べたことをメモ。
マイクロサービスの整合性を保つ別のパターンとして、Sagaパターン同じく結果整合性を利用するTCC(Try-Confirm/Cancel)パターンというものもある。
2phase commitに似ているが、TCCパターンでは、各サービスがトランザクションの準備、確認、キャンセルの3つのステップを持つ。
TCCパターンは補償トランザクションのようなロールバックは行わず、不整合の生じる処理を行わないことにすることで整合性を担保する。
Goのresponse.WriteHeaderの副作用についてメモする。
以下は愚直な例だが、次のようにWriteHeaderを複数回呼ぶと、http: superfluous response.WriteHeader call from main.handler
というエラーが出る。
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.WriteHeader(http.StatusInternalServerError) // Error!
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8888", nil)
}
このように複数回呼ぶと、最初の呼び出しのステータスコードが採用され、最後の呼び出しのステータスコードは無視される。(HTTPの仕様に関係している?)
この辺を見ると仕様の雰囲気が掴める。
上記の愚直な例では、素直にWriteHeaderを一度だけ呼び出すような実装に調整すれば良いだけだが、例えば実装上の都合でWriteHeaderが複数回セットされてしまう場合があるとする。
import (
"bytes"
"html/template"
"net/http"
)
// エラーページの描画のために呼び出される関数
func ExecuteTpl(w http.ResponseWriter) error {
err := template.Must(template.ParseFiles("index.html")).Execute(w, nil)
w.WriteHeader(http.StatusInternalServerError)
if err != nil {
return err
}
return nil
}
この関数の前に既にWriteHeaderが呼び出され、エラーが出てしまう状況だとする。
このような状況でエラーを回避するには、バッファに一度書き出してから、最後にWriteHeaderを呼び出すようにすると回避できる。
package main
import (
"bytes"
"html/template"
"net/http"
)
func ExecuteTpl(w http.ResponseWriter) error {
var buf bytes.Buffer
w.WriteHeader(http.StatusInternalServerError)
err := template.Must(template.ParseFiles("index.html")).Execute(w, nil)
if err != nil {
return err
}
buf.WriteTo(w)
return nil
}
templateのExecuteは呼び出し時にエラーがあった際、実行は停止するがレスポンスへの書き込みが一部開始する可能性があるらしく、Executeよりも前にWriteHeaderしたほうが良さそう。(多分)
最終的な仕様がどうなるかはわからないが、静的なルーティングの機能(/foo/barのような固定値のルーティング)しか持っていないServeMuxに動的なルーティング(/foo/:idのようなパスパラメータを使ったルーティング)の機能が少なくとも追加されそうな雰囲気。
自分がこのProposalを気にしている理由は、ルーティングのライブラリを個人的に開発しているである。
cf. bmf-san/goblin cf. bmf-tech.comの関連記事
goblinは有名所のルーティングライブラリと同じく、ServeMuxの機能を拡張するライブラリで、静的・動的なルーティングに対応している。動的なルーティングのパスパラメータは正規表現もサポートしている。
goblinの内部のデータ構造はTrie木をベースにしたロジックで、自分としてできる限りの最適化をしたりしている。(より良いパフォーマンスを求めるならデータ構造を根本から変える必要があるが・・)
今回のproposalに記載されている参照実装とのパフォーマンス比較をやってみたが(リリースされる実際の実装とは異なる可能性があるとは思うが、参考程度にやってみた)、どっこいどっこいという感じ.. cf. Add jba/muxpatterns #23
ServeMuxの機能拡張がリリースされたらgoblinはおそらく使う理由がなくなりそうだなぁと思っている。そもそも使っているのは自分だけで、自分が個人的なアプリケーションで使っているだけだとは思うが・・
goblinは継続的にメンテナンスしていつかは日の目を見る日が・・と思っていたが、今回のproposalで一区切りつきそうw
今後サードパーティのルーティングを採用するかどうかについては、標準のServeMuxが大きな一つの選択肢になってくるのかなと思っている。
サードパーティのルーティングを採用するかどうかの基準としては、標準のServeMuxと比較して、
パッと思いつくところはこのへん。
サードパーティからServeMuxへの乗り換えのしやすさとかは気になるが、net/httpで定義されているinterface(Handlerとか)に準拠しているかどうかあたりネックかなと思うので、準拠していれば大きな問題はなさそう。準拠していないライブラリからの乗り換えは面倒かもしれない。(ほとんどは準拠しているが一部準拠していないオレオレ実装になっていたりする)
Proposalのコメントに依ると、これから具体的な実装を検討していくようなので、引き続きウォッチしていこうと思う。
]]>BFFについて調べたことをまとめる。
Backends For Frontendsの略。Best Friends Forever(ズッ友だよ)ではない。 名前の通り、フロントエンドのためのバックエンドサーバーのことで、フロントエンドのためのAPIやHTMLをレスポンスするなどUI・UXのための役割を担っている。 クライアント(サーバーの呼び出し側)の多様性に応えるのが難しいという問題を、BFFはクライアントごとの要求を整理する形で解決することができる。
BFF自体は知っていたのでさらっとググって終わろうと思っていたのだが、アーキテクチャの可用性や、ビジネスロジックの扱い、クライアントの適切な集約、組織構成との関連など色々考えるポイントが多く面白かった。
自分としてはBFFは結構慎重にならないと落とし穴が多そうという印象を持った。罠みたいなところは見えるけどそれに引っかからないようにうまく作るのは難しそうという感覚を持った。
もしBFFを検討する機会があれば振り返ってみようと思う。
]]>仕事でもプライベートでも何年かGoを触っているが、今一度このタイミングで学び直してみると効果的ではないかなと思って色々学び直した。 その際に読んだ記事をリストアップしておく。
基本的なことの復習、仕様で拾い切れていなかった部分や新機能のキャッチアップ、tips周りを拾って、Goのコーディング力を上げるためのベースを鍛え直したい。
仕様理解に関連する記事をgo.devを中心に読み漁った。
Go1.18で追加されたGenericsの仕様について今一度キャッチアップした。
Goの良いところや得意なところ、苦手なところや不得手なところってどこだろうというのを整理しようと思って読み漁った記事。
育ったきた土壌が違うと見方も変わるので、色んな人の意見を見ると為になる。
自分自身は多くの言語に触れてきた身ではないので、言語の設計思想の深いところに触れた洞察はできないが、Goのシンプルさ(シンプルに見える、というほうが的確かもしれない)を保つ思想に特に好感を持っている。色んな書き方ができる機能性の高い言語を使っているときはどう書くべきかということに悩むこともあるが、Goの場合は素直に書けると感じていて、書いていて楽しさを感じる言語であると思っている。
3冊ほどピックアップして読んだ。他にも読もうかと思った本があるが、今回の目的に沿いそうな本を厳選した。(特に並行処理周りはいい加減履修しないと思っているが、それだけに集中する必要があると思ったので、別の機会とした。。。)
全部良い本だが、特におすすめしたいのは実用Go言語。
自分のようにある程度Goを触ってきたけど今一度知識を整理したいという場合に学びがある本だと思う。
記憶の引き出しに色々としまえたので、どこかで引き出すときがきたら役立つはず。
あとはまだ理解しきれていないことも多いので、またどこかの節目にでも学びなおす。
]]>通知基盤の構築に関してざっくりと考えたことや調べたことなどをまとめておく。
ユーザーに通知(メール・プッシュ・SMS・音声など)を行うためのシステム基盤。
クライアント(通知を依頼するシステム)からリクエストを受けて、送信先・送信内容など通知に関する処理を担うシステム。
考えることが一杯ありそうだと思ったので、思いついた順で雑に書いた。整理できていない。
Salesforce、Braze、Airship、OneSignal、SendGridなど色々ある。
単に複数チャンネルの通知が送信できるだけではなく、顧客管理やマーケティングツールだったりなどと連携した通知ができる。
マルチチャンネルをサポートしているサービスは探すと結構色々出てくる。
複数チャンネルの通知をサポートしているプラットフォームとしてAWS Pinpointがある。
国内外の事例を漁ってみた。
個人的に気になっているAWS Pinpointについて個別に調べてみた。
誰が(運営、管理者、マーケティング担当者、開発者・・etc)、何を(メッセージ内容)、誰に、どの通知チャンネルで、いつ(いつまでに)通知したいのか、通知の総量はどれくらいなのか、ということあたりがまず整理されている必要があると思った。(そりゃそうだという感じだけど・・)
]]>Goのhttp.RoundTripperについてかく。
HTTPクライアントの通信を担っているインターフェース。
cf. pkg.go.dev - net/http#RoundTripper
HTTPクライアントにおいてリクエストからレスポンスを受け取るまでの間の処理をカスタマイズすることができる。
HTTPクライアントにおけるミドルウェアというイメージ。
ソースコードはgithub.comにも置いてある。
RoundTripperインターフェースを実装して、http.Clientに渡すだけでカスタマイズすることができる。
package main
import (
"fmt"
"net/http"
"time"
)
// CustomRoundTripper is a custom implementation of http.RoundTripper
type CustomRoundTripper struct {
Transport http.RoundTripper
}
func (c *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
start := time.Now()
fmt.Printf("Requesting %s %s\n", req.Method, req.URL)
resp, err := c.Transport.RoundTrip(req)
elapsed := time.Since(start)
fmt.Printf("Received response in %v\n", elapsed)
return resp, err
}
func main() {
client := &http.Client{
Transport: &CustomRoundTripper{
Transport: http.DefaultTransport,
},
}
resp, err := client.Get("https://www.example.com")
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
fmt.Println("Status Code:", resp.Status)
}
$ go run main.go
Requesting GET https://www.example.com
Received response in 530.885709ms
Status Code: 200 OK
HTTPクライアント側でミドルウェア的に何か処理を挟みたいときに使う。
などHTTPクライアントで統一的な処理を設定したいときに使えそう。
特定のエンドポイントに対して処理を挟みたいときなどは自前のミドルウェアを用意するほうが柔軟性が高そう。
並行処理のパターンであるfan-in、fan-outをGoで実装する。
fan-inは、複数の入力を1つにまとめる処理で、fan-outは、1つの入力を複数に分ける処理である。
fan-inはデータを集約させ、fan-outはデータを分散させる。
Goではchannelとgoroutineを使って実現することができる。
ソースコードはgithubにも置いてある。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func producer(id int, out chan<- int) {
for i := 0; i < 5; i++ {
value := rand.Intn(100)
fmt.Printf("Producer %d: Sending %d\n", id, value)
out <- value
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
}
close(out)
}
func fanIn(inputs []<-chan int, out chan<- int) {
var wg sync.WaitGroup
wg.Add(len(inputs))
for _, input := range inputs {
go func(ch <-chan int) {
for value := range ch {
out <- value
}
wg.Done()
}(input)
}
go func() {
wg.Wait()
close(out)
}()
}
func main() {
rand.Seed(time.Now().UnixNano())
// Fan-Out
numProducers := 3
inputs := make([]chan int, numProducers)
for i := 0; i < numProducers; i++ {
inputs[i] = make(chan int)
go producer(i+1, inputs[i])
}
// Convert channels to <-chan int
inputChans := make([]<-chan int, numProducers)
for i := 0; i < numProducers; i++ {
inputChans[i] = inputs[i]
}
// Fan-In
result := make(chan int)
go fanIn(inputChans, result)
// Consume the merged values
for value := range result {
fmt.Printf("Consumer: Received %d\n", value)
}
fmt.Println("All done!")
}
fan-outの処理でデータを分散して、fan-inの処理でデータを集約している。
並行処理は自身がないので勉強しないといけない。。。
何年かGoを触っているが知らなかったり忘れていたことに気づけて大変勉強になった。
ErrNotImplemented := errors.New("Not Implemented!")
.
と_
とtestdata
という名前のフォルダはコンパイル対象から外れるgo test -shuffle=on
でテストの実行順がシャッフルされるGoFの振る舞いに関するパターンであるTemplate MethodパターンとStrategyパターンについてまとめる。
大枠の処理を上位のクラスで決めておき、具体的な処理の流れを下位のクラスに任せる設計パターン。
package main
type Game interface {
Init()
Start()
End()
}
type BaseBall struct{}
func (b *BaseBall) Init() {
println("BaseBall Init")
}
func (b *BaseBall) Start() {
println("BaseBall Start")
}
func (b *BaseBall) End() {
println("BaseBall End")
}
func (b *BaseBall) Play() {
b.Init()
b.Start()
for i := 0; i < 9; i++ {
println("Top & Bottom")
}
b.End()
}
func main() {
b := &BaseBall{}
b.Play()
}
実行時に処理を選択することができるような設計パターン。
Templateパターンと似ているが、Strategyパターンは処理の全てをまとめて切り替えるような構成で、Templateパターンは特定の処理は固定で他が可変といったイメージ。
package main
type PaymentStrategy interface {
Pay(amount int)
}
type CreditCard struct{}
func (cc *CreditCard) Pay(amount int) {
println("CreditCard Pay")
}
type Cash struct{}
func (c *Cash) Pay(amount int) {
println("Cash Pay")
}
type Cart struct {
paymentMethod PaymentStrategy
}
func (c *Cart) Checkout(amount int) {
c.paymentMethod.Pay(amount)
}
func main() {
cc := &CreditCard{}
c := &Cash{}
cart := &Cart{paymentMethod: cc}
cart.Checkout(100)
cart.paymentMethod = c
cart.Checkout(100)
}
Template MethodパターンもStrategyパターンも継承を使うか委譲を使う方は実装に任させれているので、どちらを使わないといけないということはない。
docker-composeで構成されている個人開発のアプリケーションをKubernetes(k3s)へ移行するという試みの際に、multipassを使ってみたのでそれについてメモを残す。
結局移行はしなかったが...
k3sはCNCFに認証されたKubernetesディストリビューション。IoTやエッジコンピューティング用途向け。メモリを節約したい、Kubernetesほどのスケールは不要、あるいは気軽にKubernetesを触ってみたいときなどにも有用なので、個人がVPSなどでKubernetesを導入してみたいときの選択肢にもなりうると思う。
cf. K3s on ConoHa
k3sは機能的にはKurbenetesとほとんど変わらないが、いくつか制約がある。詳しくはドキュメント参照。
cf. docs.k3s.io
Ubuntuの仮想環境を気軽に構築できるツール。Linux、macOS、Windowsに対応している。
k3sの実行環境として、macOS上で仮想環境を容易する必要があったため。
cf. Can I install k3s on macos (big sur) with m1 chip?
いくつか代替手段はあるが、気軽に簡単に触れそうだったmultipassを使ってみた。
macOSならbrewでmultipassをインストールして、以下のステップだけでk3sを動かすことができるようになる。
cf.
]]>gRPCとProtocol Buffersについて改めておさらいしておきたかった。
日本語の記事をいくつか読み漁ったが、まだまだ日本語情報は少ないので、海外の事例記事とか読み漁ったほうが良いかも。
モノレポについてまとめる。
モノレポとは複数プロジェクトのコードを単一のレポジトリで管理したもの。対して複数のレポジトリで管理するものをポリレポ、またはマルチレポと呼ぶ。 マイクロサービスの管理方針の1つではあるものの、マイクロサービスを前提としたものではない。 モノリスとは同義ではない。
モノレポの運用における観点を整理してみた。
複数チームで運用する場合、チーム管轄外のコードも変更できてしまう。
GitHubであれば、CODEOWNERSによる管理で管轄範囲を整備できる。何かしらのツールに頼ったレギュレーションを敷く必要がありそう。
コード間の依存関係が複雑化することによる泥団子化。
これも何かしらのツールに頼った解決が必要そう。例えば、Nxではpublic APIを持つライブラリを作ることができたり、依存関係をグラフで可視化することができる。
コード量が増加していくあたり、ビルドやテスト、デプロイが遅くなる、Gitの管理上の問題が発生する。
前者については、個々に実行可能なCIパイプラインやデプロイフロー等の整備により解決できる。ツールによる解決が可能な範囲である。
後者については、少し悩み所かもしれない。cloneやpullが辛くなる状態を迎える時は何か対応を検討する必要がありそう。
Microsoftが開発しているGVFSというスケーラブルなGitを使う、Git LFSを活用する、諦めてリポジトリを分割するなど。
特定の技術(プログラミング言語など)に縛られるという制約は基本的にはない。
コード管理が単一であるだけで、CI・CD等は複数のパイプライン管理を想定しているので特に懸念にはならなそう。
ビルドツールが対応していない言語や環境があると技術選択の幅が制限されるという可能性があるかもしれない。
featureブランチとの相性があまり良くないので、トランクベースの戦略を導入するのが望ましそう。フィーチャートグルも合わせて導入したいところ。
GitHubであれば、IssueやPullRequestの運用方針を整備することに気を配る必要があるかもしれない。
monorepo.tools - Many solutions, for different goalsに詳しく整理されている。
Bazel、Nx、Pants辺りが有力候補なのかなといった印象。
いくつかの自分のブログ記事に参考リンクを貼ったりしていたせいで分散していたので集約した。
HTTP Routerの開発で参考にした資料リスト。
bmf-tech.comに投稿している記事。
グラフを表現するためのデータ構造である隣接リストと隣接リストについてまとめる。
隣接リストや隣接行列は有向グラフでも無向グラフでも利用できる。
各頂点(ノード)ごとに隣接する頂点をリストとして持つデータ構造。
// 無向グラフの例
A---B
| / |
| / |
C---D
// 隣接リストで表現すると、次のような形になる
A: [B, C]
B: [A, C, D]
C: [A, B, D]
D: [B, C]
空間計算量: O(V+E) ※Vは頂点数、Eは辺の数 特定の頂点の隣接する頂点を見つける: O(1) 特定の辺の存在を判定する: O(degree) ※degreeは隣接する辺の数 全ての頂点の隣接する頂点を列挙する: O(V+E)
隣接リストは辺の数が少ないグラフだと計算量が効率なデータ構造。
ソースコードはadjacency_list。
// See: https://www.youtube.com/watch?v=JDP1OVgoa0Q
// See: https://www.youtube.com/watch?v=bSZ57h7GN2w
package main
import "fmt"
// Graph represents an adjacency list graph.
type graph struct {
vertices []*vertex
}
// Vertex represents a graph vertex.
type vertex struct {
key int
adj []*vertex
}
// addVertex adds a vertext to the graph.
func (g *graph) addVertex(k int) {
if contains(g.vertices, k) {
fmt.Println(fmt.Errorf("Vertex %v not added because it is an existing key", k))
return
}
g.vertices = append(g.vertices, &vertex{key: k})
}
// addEdge adds an edge to the graph.
func (g *graph) addEdge(from, to int) {
// get vertex
fromVertex := g.getVertex(from)
toVertex := g.getVertex(to)
// check error
if fromVertex == nil || toVertex == nil {
fmt.Println("Invalid edge")
return
}
// check if edge already exists
if contains(fromVertex.adj, to) {
fmt.Println("Existing edge")
return
}
// add edge
fromVertex.adj = append(fromVertex.adj, toVertex)
}
// getVertex returns a pointer to the vertex.
func (g *graph) getVertex(k int) *vertex {
for _, v := range g.vertices {
if k == v.key {
return v
}
}
return nil
}
// contains returns true if the key exists in the slice.
func contains(s []*vertex, k int) bool {
for _, v := range s {
if k == v.key {
return true
}
}
return false
}
// print prints the adjacency list.
func (g *graph) print() {
for _, v := range g.vertices {
fmt.Printf("\nVertex %v : ", v.key)
for _, v := range v.adj {
fmt.Printf("%v ", v.key)
}
}
}
func main() {
g := &graph{}
for i := 0; i < 5; i++ {
g.addVertex(i)
}
g.addEdge(1, 2)
g.addEdge(1, 3)
g.addEdge(2, 3)
g.addEdge(4, 1)
g.addEdge(4, 2)
g.addEdge(4, 3)
g.print()
}
辺(エッジ)を足す処理が複雑である。
頂点(ノード)間の接続関係を2次元の行列として表現するデータ構造。
頂点間の辺(エッジ)の有無は0または1の値が使われる。
// 無向グラフの例
A---B
| / |
| / |
C---D
// 隣接行列で表現すると次のようになる。
A B C D
A 0 1 1 0
B 1 0 1 1
C 1 1 0 1
D 0 1 1 0
空間計算量: O(V^2) 特定の頂点の隣接する頂点を見つける: O(V) 特定の辺の存在を判定する: O(1) 全ての頂点の隣接する頂点を列挙する: O(V^2)
隣接行列は辺の数が多いグラフや辺の存在を頻繁に判定するような必要がある場合に効率的なデータ構造。
ソースコードはadjacency_matrix。
package main
import (
"fmt"
)
// graph represents an adjacency matrix graph.
type graph struct {
matrix [][]int
size int
}
// newGraph returns a new graph with the given size.
func newGraph(size int) *graph {
matrix := make([][]int, size)
for i := range matrix {
matrix[i] = make([]int, size)
}
return &graph{matrix: matrix, size: size}
}
// addEdge adds an edge to the graph from -> to.
func (g *graph) addEdge(from, to int) {
if from < 0 || to < 0 || from >= g.size || to >= g.size {
return
}
g.matrix[from][to] = 1
}
// print prints the adjacency matrix.
func (g *graph) print() {
for _, row := range g.matrix {
fmt.Println(row)
}
}
func main() {
size := 5
graph := newGraph(size)
graph.addEdge(0, 1)
graph.addEdge(0, 3)
graph.addEdge(1, 2)
graph.addEdge(2, 4)
graph.addEdge(3, 4)
graph.print()
}
辺(エッジ)を足す部分の条件が少しややこしいかもしれない。
どのノードにおいても、左の子ノード<親ノード<右の子ノードとなるような木。
ex.
5
/ \
3 8
/ \ / \
1 4 6 9
それぞれのノード探索順はwww.momoyama-usagi.com - うさぎでもわかる2分探索木 後編 2分探索木における4つの走査方法に記載されている方法が覚えやすいのでそちらのリンクを記載する。
ルートノードから走査を開始し、左部分木、右部分木の順で再帰的に走査する。
rootがpre(事前)なので根→左→右と覚えておく良い。(根の位置だけ覚えておけば良い。左、右は必ず左が先になる。)
木を一筆書きで括ったときに、ノードの左側を通った順がpreorder。
5
/ \
3 8
/ \ / \
1 4 6 9
5 -> 3 -> 1 -> 4 -> 8 -> 6 -> 9
左部分木から再帰的に走査し、ルートノード、右部分木の順に走査する。
rootがin(間)なので左→根→右と覚えておく良い。(根の位置だけ覚えておけば良い。左、右は必ず左が先になる。)
木を一筆書きで括ったときに、ノードの下側を通った順がinorder。
5
/ \
3 8
/ \ / \
1 4 6 9
1 -> 3 -> 4 -> 5 -> 6 -> 8 -> 9
左部分木から再帰的に走査し、右部分木、ルートノードの順で走査する。
rootがpost(事後)なので左→右→根と覚えておく良い。(根の位置だけ覚えておけば良い。左、右は必ず左が先になる。)
余談だが、逆ポーランド記法はスタックで解くこともできるが、二分木をpostorderでも解くことができる。
木を一筆書きで括ったときに、ノードの右側を通った順がpostorder。
5
/ \
3 8
/ \ / \
1 4 6 9
1 -> 4 -> 3 -> 6 -> 9 -> 8 -> 5
深さごとに走査する。
5
/ \
3 8
/ \ / \
1 4 6 9
5 -> 3 -> 8 -> 1 -> 4 -> 6 -> 9
ソースコードはbinary_search_treeにある。
package main
import "fmt"
// a binary search tree.
type tree struct {
root *node
}
// a node for binary search tree.
type node struct {
val string
l *node
r *node
}
// insert a value to tree.
func (t *tree) insert(v string) {
t.root = t.root.insertNode(v)
}
// insert a node to tree.
func (n *node) insertNode(v string) *node {
if n == nil {
return &node{val: v}
}
if v < n.val {
n.l = n.l.insertNode(v)
} else if v > n.val {
n.r = n.r.insertNode(v)
}
return n
}
// search a value from tree.
func (t *tree) search(v string) bool {
return t.root.searchNode(v)
}
// search a node from tree.
func (n *node) searchNode(v string) bool {
if n == nil {
return false
}
if v == n.val {
return true
} else if v < n.val {
return n.l.searchNode(v)
} else {
return n.r.searchNode(v)
}
}
// remove a value from tree.
func (t *tree) remove(v string) {
t.root = t.root.removeNode(v)
}
// remove a node from tree.
func (n *node) removeNode(v string) *node {
if n == nil {
return nil
}
if v < n.val {
n.l = n.l.removeNode(v)
return n
} else if v > n.val {
n.r = n.r.removeNode(v)
return n
} else {
// node has no children
if n.l == nil && n.r == nil {
return nil
}
// node has only right child
if n.l == nil {
return n.r
}
// node has only left child
if n.r == nil {
return n.l
}
// node has both children
leftmostrightside := n.r
for leftmostrightside.l != nil {
leftmostrightside = leftmostrightside.l
}
n.val = leftmostrightside.val
n.r = n.r.removeNode(n.val)
return n
}
}
// breadth first search - preorder
//
// 5
// / \
// 3 8
// / \ / \
// 1 4 6 9
//
// 5 -> 3 -> 1 -> 4 -> 8 -> 6 -> 9
func (t *tree) preorder(n *node, f func(string)) {
if n != nil {
// root → left → right
f(n.val)
t.preorder(n.l, f)
t.preorder(n.r, f)
}
}
// breadth first search - inorder
//
// 5
// / \
// 3 8
// / \ / \
// 1 4 6 9
//
// 1 -> 3 -> 4 -> 5 -> 6 -> 8 -> 9
func (t *tree) inorder(n *node, f func(string)) {
if n != nil {
// left → root → right
t.inorder(n.l, f)
f(n.val)
t.inorder(n.r, f)
}
}
// breadth first search - postorder
//
// 5
// / \
// 3 8
// / \ / \
// 1 4 6 9
//
// 1 -> 4 -> 3 -> 6 -> 9 -> 8 -> 5
func (t *tree) postorder(n *node, f func(string)) {
if n != nil {
// left → right → root
t.postorder(n.l, f)
t.postorder(n.r, f)
f(n.val)
}
}
// depth first search
//
// 5
// / \
// 3 8
// / \ / \
// 1 4 6 9
//
// 5 -> 3 -> 8 -> 1 -> 4 -> 6 -> 9
func (t *tree) dfs(n *node, f func(string)) {
if n != nil {
s := []*node{n}
for len(s) > 0 {
crtn := s[0]
f(crtn.val)
s = s[1:]
if crtn.l != nil {
s = append(s, crtn.l)
}
if crtn.r != nil {
s = append(s, crtn.r)
}
}
}
}
func main() {
t := &tree{}
t.insert("5")
t.insert("3")
t.insert("8")
t.insert("1")
t.insert("4")
t.insert("6")
t.insert("9")
t.insert("11")
fmt.Println(t.search("11")) // true
t.remove("11")
fmt.Println(t.search("11")) // false
f := func(v string) {
fmt.Println(v)
}
t.preorder(t.root, f) // 5314869
fmt.Println("-----")
t.inorder(t.root, f) // 1345689
fmt.Println("-----")
t.postorder(t.root, f) // 1436985
fmt.Println("-----")
t.dfs(t.root, f) // 5381469
}
幅優先探索は、根の探索開始位置(preorderなら根→左→右)だけ覚えておけば再帰処理はそれに従って書ける。
深さ優先探索は少し面倒。後は二分探索木に限らずノードの削除処理はもっと面倒...。
探索パターンはとりあえずこうやって覚えておけば頭には留められそう。
スライスを使ったパターンと連結リストを使ったパターンをそれぞれ実装している。
個人的にはスライスを使ったパターンの方が実装は楽かなと思う。
スタックのpush、pop、キューのenqueue、dequeueの時間計算量はそれぞれO(1)で実装できるが、一部サボってO(N)になってしまっているものがある。
ソースコード:stack
package main
import "fmt"
// LIFO stack by using linked list.
type stack struct {
top *node
}
type node struct {
val int
next *node
}
// Remove data from the top of the stack.
func (s *stack) pop() {
// LIFO
s.top = s.top.next
}
// Add the item to the top of the stack.
func (s *stack) push(item int) {
// LIFO
s.top = &node{
val: item,
next: s.top,
}
}
// Returns the top item from the stack.
func (s *stack) peek() *node {
return s.top
}
// Returns true if the stack is empty.
func (s *stack) isEmpty() bool {
return s.top == nil
}
func (s *stack) traverse() {
crt := s.top
for {
if crt == nil {
break
}
fmt.Printf("%#v\n", crt)
crt = crt.next
}
}
func main() {
s := &stack{
top: &node{
val: 1,
next: &node{
val: 2,
next: &node{
val: 3,
},
},
},
}
s.pop()
s.traverse()
fmt.Println("----") // 2 3
s.pop()
s.traverse() // 3
fmt.Println("----")
s.pop()
s.traverse() // nil
s.push(1)
s.traverse() // 1
fmt.Println("----")
s.push(2)
s.push(3)
s.traverse() // 3 2 1
fmt.Println("----")
fmt.Printf("%#v\n", s.peek()) // 3
s2 := &stack{}
fmt.Println(s2.isEmpty()) // true
}
単純な連結リストで特に難しいところはないかなという印象。
package main
import "fmt"
type stack struct {
nodes []*node
}
type node struct {
val int
}
// Remove data from the top of the stack.
func (s *stack) pop() {
// LIFO
s.nodes = s.nodes[1:]
}
// Add the item to the top of the stack.
func (s *stack) push(item int) {
// LIFO
s.nodes = append(
[]*node{
&node{
val: item,
},
},
s.nodes...,
)
}
// Returns the top item from the stack.
func (s *stack) peek() *node {
return s.nodes[0]
}
// Returns true if the stack is empty.
func (s *stack) isEmpty() bool {
return len(s.nodes) == 0
}
func (s *stack) traverse() {
for _, n := range s.nodes {
fmt.Printf("%#v\n", n)
}
}
func main() {
s := &stack{
nodes: []*node{
&node{
val: 1,
},
&node{
val: 2,
},
&node{
val: 3,
},
},
}
s.pop()
s.traverse()
fmt.Println("----") // 2 3
s.pop()
s.traverse() // 3
fmt.Println("----")
s.pop()
s.traverse() // nil
s.push(1)
s.traverse() // 1
fmt.Println("----")
s.push(2)
s.push(3)
s.traverse() // 3 2 1
fmt.Println("----")
fmt.Printf("%#v\n", s.peek()) // 3
s2 := &stack{}
fmt.Println(s2.isEmpty()) // true
}
スライスの操作で実装できる。スライスの先頭に要素を追加する書き方はちょっと慣れが必要かも。
ソースコード:queue
package main
import "fmt"
// FIFO queue by using linked.
type queue struct {
top *node
}
type node struct {
val int
next *node
}
// Add the item to the end of the queue.
func (q *queue) enqueue(item int) {
// FIFO
if q.top == nil {
q.top = &node{
val: item,
}
return
}
crt := q.top
for {
if crt.next == nil {
crt.next = &node{
val: item,
}
break
}
crt = crt.next
}
}
// Remove item from the front of the queue.
func (q *queue) dequeue() {
// FIFO
q.top = q.top.next
}
// Returns the front item from the queue.
func (q *queue) peek() *node {
return q.top
}
// Returns true if the queue is empty.
func (q *queue) isEmpty() bool {
return q.top == nil
}
func (q *queue) traverse() {
crt := q.top
for {
if crt == nil {
break
}
fmt.Printf("%#v\n", crt)
crt = crt.next
}
}
func main() {
q := &queue{
top: &node{
val: 1,
next: &node{
val: 2,
next: &node{
val: 3,
},
},
},
}
q.dequeue()
q.traverse()
fmt.Println("----") // 2 3
q.dequeue()
q.traverse() // 3
fmt.Println("----")
q.dequeue()
q.traverse() // nil
q.enqueue(1)
q.traverse() // 1
fmt.Println("----")
q.enqueue(2)
q.enqueue(3)
q.traverse() // 1 2 3
fmt.Println("----")
fmt.Printf("%#v\n", q.peek()) // 3
q2 := &queue{}
fmt.Println(q2.isEmpty()) // true
}
enqueueがキューの長さに依存してしまって、O(N)になってしまっている。
末尾のキューやキューの長さをデータ構造(queue)に持たせる実装にすればO(1)になるので、そっちのほうが望ましい。
package main
import "fmt"
// FIFO queue by using slice.
type queue struct {
nodes []*node
}
type node struct {
val int
}
// Add the item to the end of the queue.
func (q *queue) enqueue(item int) {
// FIFO
q.nodes = append(q.nodes, &node{
val: item,
})
}
// Remove item from the front of the queue.
func (q *queue) dequeue() {
// FIFO
q.nodes = q.nodes[1:]
}
// Returns the front item from the queue.
func (q *queue) peek() *node {
return q.nodes[0]
}
// Returns true if the queue is empty.
func (q *queue) isEmpty() bool {
return len(q.nodes) == 0
}
func (q *queue) traverse() {
for _, n := range q.nodes {
fmt.Printf("%#v\n", n)
}
}
func main() {
q := &queue{
nodes: []*node{
&node{
val: 1,
},
&node{
val: 2,
},
&node{
val: 3,
},
},
}
q.dequeue()
q.traverse()
fmt.Println("----") // 2 3
q.dequeue()
q.traverse() // 3
fmt.Println("----")
q.dequeue()
q.traverse() // nil
q.enqueue(1)
q.traverse() // 1
fmt.Println("----")
q.enqueue(2)
q.enqueue(3)
q.traverse() // 1 2 3
fmt.Println("----")
fmt.Printf("%#v\n", q.peek()) // 3
q2 := &queue{}
fmt.Println(q2.isEmpty()) // true
}
単純なスライスの操作。連結リストである必要がない場合はこっちのほうが良いかもしれないが、スライスのメモリ効率(アロケーションとかコピーの発生とか)には気をつけたほうが良さそう。
両方並行して実装しているとどっちがLIFOなのかFIFOなのか頭が混乱することがあるw
]]>世界で闘うプログラミング力を鍛える本 ~コーディング面接189問とその解法で紹介されていて初めて知った。
連結リストの先頭から走査していくポインタと、そのポインタより先から走査していくポインタの2種類を用意して、同時に走査していく方法。
これが何に役立つかというと、例えば次のような例題を解くのに役立つ。
単方向連結リストの末尾からn番目の要素を見つけるアルゴリズムを実装しなさい。
package main
import "fmt"
type node struct {
val string
next *node
}
type list struct {
head *node
}
// 末尾からn番目のノードを探す
func (l *list) search(n int) *node {
n1 := l.head
n2 := l.head
// n1はk番目のノードに設定
for i := 0; i < n; i++ {
if n1 == nil {
return nil
}
n1 = n1.next
}
// n2は先頭ノードから、n1はk番目のノードから走査する。
// n1が末尾ノードに到達したらn2は末尾から数えてn番目のノードである。
for n1 != nil {
n1 = n1.next
n2 = n2.next
}
return n2
}
func main() {
l := &list{
head: &node{
val: "a",
next: &node{
val: "b",
next: &node{
val: "c",
next: &node{
val: "d",
},
},
},
},
}
fmt.Printf("%+v\n", l.search(1)) // d
fmt.Printf("%+v\n", l.search(2)) // c
fmt.Printf("%+v\n", l.search(3)) // b
fmt.Printf("%+v\n", l.search(4)) // a
}
このようにランナーテクニックを使うと時間計算量はO(N)、空間計算量はO(1)で解くことができる。
連結リストのノード数が決まっていればわざわざポインタを2つ用意しなくとも、(全ノード数-n)が末尾からn番目となるので単純に解けるが、そうでない場合はこのように解くか、再帰で解くことになる。再帰の場合は計算量が増えるはず・・
コーディングクイズで使えるシーンがありそうなので頭の片隅に留めておきたい。
]]>日頃再帰処理を書く機会といえばコーディングクイズくらいで実際のところあまり書く機会がない..
再帰処理はケースによってはメモ化や末尾最適化までエレガントにやらないと計算量が大きくなったり、メモリを食うだけのコードになってしまうが、アルゴリズムによってはシンプルなコードになるというメリットがある。
が、認知負荷の高さは変わらない気がする。
何が認知負荷を高めているのか、苦労させているのかということを考えてみたのだが、自分の中で2つ気づいたことがあるのでそれについて書き残しておく。
何がいつreturnされるかわかりづらい、掴みづらい。
例えば次のようなケースは単純なコードなのでわかりやすいが、再帰ケースが複数になったりすると認知負荷が高くなる。
package main
import "fmt"
func fact(n int) int {
// 基本ケース
if n < 2 {
return 1
}
// 再帰ケース
return n * fact(n-1)
}
func main() {
fmt.Println(fact(5)) // 120
}
returnで混乱する場合は愚直に処理を書き出してみるのが良いと思う。他に良い方法あるかな。
再帰関数の評価はコールスタックに積まれていく。スタックなのでLIFO。
評価が完了するとスタックからデータを取り出して処理していく。
ということが理解できていないとデバッガでコードを追っかけても途中で迷子になってしまう。
例えば次のような単純なコードだと考えやすい。
package main
import "fmt"
func proc(n int) {
if n == 0 {
return
} else {
fmt.Printf("%d", n)
proc(n - 1)
fmt.Printf("%d", n)
return
}
}
func main() {
proc(5) // 5432112345
}
とても基本的なことかもしれないけど、コードが複雑になるほどわけのわからないことになるのでそういうときはこの基本に立ち返りたい。
漸化式を導出して数学的に考えることができる、慣れていれば難しく感じる度合いが減りそうな気がするが、再帰はやっぱり苦手さが拭えないので鍛錬が必要。。。
forのループでまず書いてみて、その後再帰処理に書き換えてみるというアプローチもアリかなと思うが、問題によっては苦労するケースもあると思っている。
鍛錬して再帰が得意になったらまたこの記事を振り返ってみる。
]]>プログラマ脳という本は、認知科学のアプローチを用いて、コーディングという創造的なプロセスを科学していく本で、脳が筋肉でできている人が読むべき内容だった。
脳筋の下りは半分冗談として、最近コーディングクイズの日課を再開したのだが、データ構造やアルゴリズムの勉強以外に何か根っこの部分も鍛える・改善する方法はないだろうか?と思って色々調べていたらたどり着いたのがこの本だった。
書評としてこの本を読んで気になった点を書きまとめておく。
ワーキングメモリというワードはこの本では重要なキーワードになっており、最後の章まで度々登場する。
人間がコーディングするとき、脳内では長期記憶・短期記憶・ワーキングメモリという3つの認知プロセスが働いている。
長期記憶・短期記憶はそれぞれ記憶の特性・保持期間によるストレージの違いで、脳内の長期記憶なストレージ・短期記憶なストレージからの読み取りプロセス。
ワーキングメモリは長期記憶や短期記憶のデータを利用する脳内CPUの処理プロセス。
ワーキングメモリへの負荷が高まると対峙しているコードへの混乱が生じる。
負荷を減らすためには、長期記憶や短期記憶それぞれのストレージの性能を上げたり、パフォーマンスを最適化するためのアプローチを講じると良い。
記憶が長期なのか?短期なのか?によってその記憶を引き出すためのアプローチを考える、鍛えるというのが良いらしい。
色々なアプローチについては本を参照ということで割愛する。
コードを読むことと自然言語の文章を読むことは共通点が多いという研究結果がある。
自然言語の学習能力はプラグラミングの学習能力にも影響する部分があり、また自然言語に文章理解におけるアプローチはコードの理解におけるアプローチにも応用できる可能性がある、という。
これは聞いたことのある話ではあったが、なるほどなぁと思った。
コードがよく書ける人、読める人は自然言語の能力も高いと感じることがよくあるので納得感が大きい。
仕様がややこしくで理解するのが大変なアプリケーションのコードリーディングの際に、自然言語による文章と合わせてコードを読み解いていくアプローチを取ることが偶にあるが、あれは科学的に効果的だったんだなぁと思った。
最近はエディタも賢いし、デバッガーも優秀なのでコードを読むのはそういったツールを使えば読みやすいので自然言語でわざわざ何かするのは面倒だなぁと考えていたが、深い理解のためには自然言語でのアプローチもちゃんと考えると良いなと思った。
脳の仕組みを理解すれば、鍛え方やハックする方法も見えたりする、というのが学んだ本だった。こういうアプローチでコーディングを考える本を他に読んだことがなかったので新鮮だった。なんとなく感じていたことが科学的には説明されているという発見がいくつかあった。
長期記憶は鍛える方法がある(繰り返し学習したり、暗記したり)けど、短期記憶はどうやって鍛えるのだろう?
短期記憶は鍛えなくても色々なアプローチで性能をカバーすることができるだろうけど、短期記憶を鍛えればパワー(筋肉)が付くなぁ。
ワーキングメモリを鍛える方法はあるらしい。
脳の「ワーキングメモリ」を鍛える方法。仕事の能力、勉強の効率アップには、ワーキングメモリの強化と解放が効く!
昔流行った「脳トレ」(今も流行っている・・?)とかもワーキングメモリを鍛える系の類いかな?
]]>コーディングクイズを解く日課を再開するに当たって、リハビリを兼ねてアルゴリズムとデータ構造の基本について復習。
操作 | 計算量 |
---|---|
アクセス | O(1) |
検索 | O(n) |
検索(ソート済み) | O(log(n)) |
挿入 | O(n) |
挿入(末尾) | O(1) |
削除 | O(n) |
削除(末尾) | O(1) |
実装によるので割愛
操作 | 計算量 |
---|---|
アクセス | N/A |
検索 | O(1)※ |
挿入 | O(1)※ |
削除 | O(1)※ |
※平均ケース
実装によるので割愛
アルゴリズム | 時間計算量 | 空間計算量 |
---|---|---|
バブルソート | O(n^2) | O(1) |
挿入ソート | O(n^2) | 0(1) |
選択ソート | O(n^2) | O(1) |
クイックソート | O(nlog(n)) | O(log(n)) |
マージソート | O(nlog(n)) | O(n) |
ヒープソート | O(nlog(n)) | O(1) |
カウントソート | O(n+k) | O(k) |
基数ソート | O(nk) | O(n+k) |
アルゴリズム | 計算量 |
---|---|
二分探索 | O(log(n)) |
package main
import "fmt"
func main() {
matrix := [][]int{[]int{0, 1}, []int{2, 3}, []int{4, 5}}
for i, v := range matrix {
for j, _ := range v {
// iがrowでjがcolumn
matrix[i][j] = 0
}
}
fmt.Println(matrix)
}
操作 | 計算量 |
---|---|
アクセス | O(n) |
検索 | O(n) |
挿入 | O(1) |
削除 | O(1) |
操作 | 計算量 |
---|---|
エンキュー | O(1) |
デキュー | O(1) |
操作 | 計算量 |
---|---|
プッシュ | O(1) |
ポップ | O(1) |
Vは頂点、Eはエッジの数とする。
アルゴリズム | 計算量 |
---|---|
深さ優先検索 | O(V+E) |
幅優先検索 | O(V+E) |
トポロジカルソート | O(V+E) |
操作 | 計算量 |
---|---|
挿入 | O(log(n)) |
削除 | O(log(n)) |
mは文字列の長さとする。
操作 | 計算量 |
---|---|
検索 | O(m) |
挿入 | O(m) |
削除 | O(m) |
データ構造とアルゴリズムの勉強に当たって参考にした記事。
]]>ソートアルゴリズムの中でも比較を使わずにソートする変わった?アルゴリズム。
比較せずに要素をカウントすることでソートができる。
カウントしてソートすることができるというのは不思議!と思ったので調べてみた。
累積和について知っておく必要がある。
ソースコードは以下。
package main
import "fmt"
func countSort(s []int, maxVal int) []int {
count := make([]int, maxVal+1)
// カウント
for _, num := range s {
count[num]++
}
// 累積和計算
// ex. [1, 2, 3, 4, 5] → [1, 3, 6, 10, 15]
for i := 1; i <= maxVal; i++ {
count[i] += count[i-1]
}
sorted := make([]int, len(s))
// 元の配列を末尾から走査し、要素をソート結果に配置
for i := len(s) - 1; i >= 0; i-- {
num := s[i]
count[num]--
sorted[count[num]] = num
}
return sorted
}
func main() {
s := []int{5, 3, 2, 1, 2, 3, 4}
maxVal := 5
r := countSort(s, maxVal)
fmt.Println(r)
}
// 出力
[1 2 2 3 3 4 5]
処理の流れとしては、
直感的ではなく何というかすごく数学的な感じがする。
指定された制約を満たすような組み合わせを探索するアルゴリズム。
重複しない組み合わせ(nCr
)を全て探索するようなときに使える。
ソースコードは以下。
与えられた配列からN個の重複しないサブシーケンスを取得する処理。 以下は4C3の例。
package main
import "fmt"
func backtrack(rslt *[][]int, tmp []int, items []int, start int, k int) {
if k == 0 {
combination := make([]int, len(tmp))
copy(combination, tmp)
*rslt = append(*rslt, combination)
return
}
for i := start; i < len(items); i++ {
tmp = append(tmp, items[i])
backtrack(rslt, tmp, items, i+1, k-1)
tmp = tmp[:len(tmp)-1]
}
}
func get(items []int, k int) [][]int {
rslt := [][]int{}
tmp := []int{}
backtrack(&rslt, tmp, items, 0, k)
return rslt
}
func main() {
items := []int{1, 2, 3, 4}
k := 3
combination := get(items, k)
fmt.Println(combination)
}
// 出力
[[1 2 3] [1 2 4] [1 3 4] [2 3 4]]
再帰処理のところで脳内メモリがパンクしそうになるが、データを木構造にして考えると分かりやすい。
1, 2, 3, 4からなるデータが渡されるとき、kが0~4でそれぞれどういう結果が得られるか考えてみる。
k = 0
N/A
k = 1
1 2 3 4
k = 2
1 2 3
/ | \ / \ |
2 3 4 3 4 4
k = 3
1 2
/ \ |
2 3 3
/ \ | |
3 4 4 4
k = 4
1
2
3
4
要するに、kは木構造の深さで、4C3の3は深さであるといえる。
組み合わせをバックトラッキングを使わずに愚直に解こうとすると、for文の多重ループになってしまう。
それを再帰処理で上手く書くアプローチの一つがバックトラッキングだったりする。
leetcode.com - subsetsはいくつか解法がある問題だが、バックトラッキングで解くことができる。
配列のサブアレイを”ウィンドウ(サブセット)”をずらすしていくように探索するアルゴリズム。
ウィンドウサイズは固定または動的。
実例としては、レートリミッターで使われたりする。
ソースコードは以下。
与えられた配列から合計がN以上になるサブアレイを探索する関数。
package main
import "fmt"
func SlidingWindow(s []int, sum int) [][]int {
rslt := [][]int{}
windowSum := 0
windowStart := 0
for windowEnd := 0; windowEnd < len(s); windowEnd++ {
windowSum += s[windowEnd]
// サブアレイを見つける
for windowSum >= sum {
rslt = append(rslt, s[windowStart:windowEnd+1])
windowSum -= s[windowStart]
windowStart++
}
}
return rslt
}
func main() {
s := []int{1, 3, 2, 6, 4, 9, 9, 5}
sum := 9
subAry := SlidingWindow(s, sum)
for _, sa := range subAry {
fmt.Println(sa)
}
}
// 出力
[1 3 2 6]
[3 2 6]
[2 6 4]
[6 4]
[4 9]
[9]
[9]
SlidingWindow関数の処理の流れとしてはこんな感じ。
ウィンドウがずれていくイメージが次の通り。
[1 3 2 6] ← 最初に見つかった条件を満たすウィンドウ
[3 2 6] ←条件を満たすウィンドウ内で更に探索
[2 6 4] ←探索開始位置をズラして次のウィンドウを確保
[6 4] ←ウィンドウ内を更に探索
以下繰り返し・・・
[4 9]
[9]
[9]
配列を扱う問題で応用が効く。
最適解ではないが、leetcode.com - two-sumはスライディングウィンドウを使って解くこともできる。
この動画が分かりやすかった。
youtube.com - Solve subarray problems FASTER (using Sliding Windows)
ウインドウサイズが固定の場合、動的な場合の解説がある。
お気持ちも交えつつ書くのでこれはポエム。
ソフトウェアアーキテクトはコードも書くし、プロジェクトのリードもするし、ビジネス的な視点でも思考を巡らせたりもする。
ビジネス・技術・ユーザーの3つの要素の中心に立つ存在である。
ソフトウェアアーキテクトが果たすべき責任はDesign Itでは概ね次のように定義されている。
システムの分割と責務の割当は、必ずしもマイクロサービスが意識しているわけではなくて、単にシステムのサイズ感を小さくできると良いよね、という程度のことだと思う。たぶん。
チームのスキル向上という教育的な責任もあるというのはちょっと新鮮だった。
ソフトウェアアーキテクチャは、「品質特性やその他の性質を発揮するためにソフトウェアをどのように構築するかということに対する設計判断の集まり」と定義される。
つまり、ソフトウェアアーキテクトはシステムが求める特性の理解や特性を発揮するための設計判断をする力が必要だと考えられる。
ソフトウェアの構築にはやるべきことや気をつけるべきことが沢山あるが、ソフトウェアアーキテクトが気をつけるべきことの1つは、「コストのかかる間違いを避ける」こと。
これを間違えると後で大変なことになる、という部分を察知、回避できるようにリスクコントロールを図るということだが、結構経験の差が大きく現れる部分だろうなぁと感じる。
これに関連して、ソフトウェアアーキテクトの行動指針として、「設計判断を最大限遅延させる」というのがある。
重要な設計判断(≒コストのかから間違いの発生がありえる判断)は可能な限り決定することを後ろ倒しにするということ。
人生でもそういうことあるよね、ソフトウェアアーキテクチャは人生。としみじみ思った。
どうしても今決めないと前に進めないような問題に関しては、選択肢を残して判断することが大事かなと思った。これもまた人生。
ソフトウェアアーキテクチャについての深掘りした話は、ソフトウェアアーキテクチャの基礎 ―エンジニアリングに基づく体系的アプローチを参照しても良い思う。近しいことが書かれている。ソフトウェアアーキテクチャの特性についても色々と整理されている。
続編のソフトウェアアーキテクチャ・ハードパーツ ―分散アーキテクチャのためのトレードオフ分析は発展的内容だが、こちらも面白いと思う。余談だが書籍のレビューにボランティアとして参加した本でもあるので思い入れがある。
Design ItのP.214 ”プログラマーは日々アーキテクチャに関する判断を下す”に書かれていたことだが、たった1行のコードでもアーキテクチャの品質特性を左右する設計判断になり得るので、プログラマであってもソフトウェアアーキテクトと言えるよねという話。
職責が違おうが、肩書が変わろうがソフトウェアに向き合うならアーキテクトとしての振る舞いを意識することは大事だよねと改めて思った。
いつかはソフトウェアアーキテクトでと胸を張って言えるようになりたいと思った。
MySQLのロックについてまとめる。 MySQLのバージョンは8系を想定する。
検証に使う環境はdocker-composeで用意した。(1コンテナだけなのでcomposeを使わなくも良いのだが..)
.
├── docker-compose.yml
└── initdb.d
└── 1_schema.sql
docker-compose.yml
version: '3'
services:
mysql:
image: mysql:8.0.33
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: example
TZ: "Asia/Tokyo"
command: mysqld
ports:
- 3306:3306
volumes:
- ./initdb.d:/docker-entrypoint-initdb.d
1_schema.sql
CREATE DATABASE IF NOT EXISTS example;
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` varchar(255) NOT NULL UNIQUE
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
docker compose up
でお手元にMySQL8系のコンテナが用意できる。
MySQLにおける排他制御の手法としては、行レベルロックとテーブルレベルロックがある。
cf. dev.mysql.com - 8.11.1 内部ロック方法
cf. dev.mysql.com - 15.7.1 InnoDB ロック
共有ロックは、データのREADは可能だが、WRITEはできないロック。Shared lock(IS)。
TX1でトランザクションを開始、共有ロックをかける
// TX1
mysql> INSERT INTO users(name) VALUES('foo'); // 初期データ投入
mysql> START TRANSACTION;
mysql> SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
TX2でトランザクションを開始、WRITEを行う
// TX2
mysql> START TRANSACTION;
mysql> UPDATE users SET name = 'bar' WHERE id = 1;
TX1がCOMMITするまでTX2の更新はロックされる。
排他ロックは、データのREADもWRITEもできないロック。Exclusive lock(IX)。
TX1にてトランザクションを開始、占有ロックをかける
// TX1
mysql> INSERT INTO users(name) VALUES('foo'); // 初期データ投入
mysql> START TRANSACTION;
mysql> SELECT * FROM users WHERE id = 1 FOR UPDATE;
TX2でトランザクションを開始、READ、WRITEを行う
// TX2
mysql> START TRANSACTION;
mysql> SELECT * FROM users WHERE id = 1; // これは許容されるが
mysql> SELECT * FROM users WHERE id = 1 FOR UPDATE; // 許容されない
mysql> UPDATE users SET name = 'bar' WHERE id = 1; // 許容されない
TX1のロックが解放されるまでTX2ではREAD(単純なSELECT以外)やWRITEができないことが確認できる。
トランザクションがテーブルの行に必要とするロックタイプ(共有または排他)を示すテーブルレベルのロック。 行ロックとテーブルロックの共存をサポートするために用意されている。
インテンションロックには、
の2つがある。
SQLで明示的に操作できるものではなく、基本的にはデータベース内部で管理されるものであるので、検証は割愛。
いくつか検証パターンがあるが、以下の記事で色々と検証されている。
cf. qiita.com - MySQLのロックについて公式ドキュメントを読みながら動作検証してみた〜行レベルロック: インテンションロック〜
インデックスレコードのロック。インデックスレコードとはクラスタインデックスとセカンダリインデックスのこと。スキャンしたインデックスに対してロックする。
データベースの内部的な動作であるため割愛。
インデックスレコード間のギャップのロック。または、インデックスレコードの前または後ろのギャップのロック。
TX1でトランザクション開始、READを行う
// TX1
mysql> INSERT INTO users(id, name) VALUES(1, 'foo'), (2, 'bar'), (4, 'qux'), (5, 'quux'), (6, 'corge'); // 初期データ投入
mysql> START TRANSACTION;
mysql> SELECT * FROM users WHERE ID between 1 AND 5 FOR UPDATE;
TX2でトランザクション開始、WRITEを行う
// TX2
mysql> START TRANSACTION;
mysql> INSERT INTO users(id, name) VALUES(3, 'baz');
行単位のロックかと思いきや、範囲でロックされているのが確認できる。
インデックスレコードのレコードロックとインデックスレコードの前のギャップのギャップロックの組み合わせ。
TX1でトランザクション開始、READを行う
// TX1
mysql> INSERT INTO users(id, name) VALUES(1, 'foo'), (2, 'bar'), (3, 'baz'), (4, 'qux'); // 初期データ投入
mysql> START TRANSACTION;
mysql> SELECT * FROM users WHERE ID < 5 FOR UPDATE;
TX2でトランザクション開始、WRITEを行う
// TX2
mysql> START TRANSACTION;
mysql> INSERT INTO users(id, name) VALUES(5, 'quux');
idが5未満の行だけでなく、末尾のインデックス値を持つ行の後のギャップもロックされることが確認できる。
行の挿入前のINSERTによって設定されるギャップロックのタイプ。INSERTのインテンションロック。
データベースの内部的な動作であるため割愛。
こちらの記事で検証されているので参照。 cf. MySQLのロックについて公式ドキュメントを読みながら動作検証してみた〜レコードロック / ギャップロック / ネクストキーロック / 他〜
AUTO_INCREMENTカラムを含むテーブルに挿入されるトランザクションによって取得されるテーブルロック。 TX1でのトランザクションでINSERTするためにAUTO_INCREMENTの値を取得している間はTX2でのAUTO_INCREMENTの値を取得できないようするロック。
内部的な動作である&再現方法が分からなかったので割愛。
これはドキュメント参照。(空間インデックスに触りなれていないのものあってイマイチ分からなかった。。。)
cf. 空間インデックスの述語ロック
ロックは以下のクエリで確認することができる。
// ロックの状態確認
SELECT * FROM performance_schema.data_locks;
// ロック件数確認+スレッドID
SHOW ENGINE INNODB STATUS;
// ロック件数確認
SELECT trx_id, trx_rows_locked, trx_mysql_thread_id FROM information_schema.INNODB_TRX;
デッドロックを確認するには、SHOW ENGINE INNODB STATUS
を実行し、LATEST DETECTED DEADLOCK
と記載されている部分を確認する。
MySQLには明示的・暗黙的にロックされるパターンがある。
何が(行なのかテーブルなのか)対象なのか、範囲はどこまでなのかといったことにまずは目を向けると良さそう。
MySQLのトランザクションのアノマリーについてまとめる。 MySQLのバージョンは8系を想定する。
検証に使う環境はdocker-composeで用意した。(1コンテナだけなのでcomposeを使わなくも良いのだが..)
.
├── docker-compose.yml
└── initdb.d
└── 1_schema.sql
docker-compose.yml
version: '3'
services:
mysql:
image: mysql:8.0.33
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: example
TZ: "Asia/Tokyo"
command: mysqld
ports:
- 3306:3306
volumes:
- ./initdb.d:/docker-entrypoint-initdb.d
1_schema.sql
CREATE DATABASE IF NOT EXISTS example;
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` varchar(255) NOT NULL UNIQUE
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
docker compose up
でお手元にMySQL8系のコンテナが用意できる。
MySQLのInnoDBでは、ANSI/ISO SQL標準で定められている4つのトランザクション分離レベルが提供されている。
分離レベル | ダーティリード | インコンシステントリード | ロストアップデート | ファントムリード |
---|---|---|---|---|
READ UNCOMMITTED | ○ | ○ | ○ | ○ |
READ COMMMITED | × | ○ | ○ | ○ |
REPEATABLE READ※1 | × | × | ○ | ○※ |
SERIALIZABLE | × | × | × | × |
※1MySQLではREPEATABLE READがデフォルトとなっている。
※2上記では○になっているが、MySQLではREPEATABLE READにおいてファントムリードが発生しないようになっている。
トランザクションの分離レベルはREAD UNCOMMITTEDが一番低く、SERIALIZABLEが一番高い。上記は上から低い順となっている。基本的には分離性が高いほど性能が低下する傾向にある。
トランザクションについては、トランザクション概観にもまとめている。
トランザクションにおけるアノマリーについてMySQLで再現してみる。
アノマリーとは、「トランザクションの分離レベルや処理順序によって生じる期待しない結果や不整合」のこと。
アノマリーはANSI SQL標準やISO/IEC 9075によって定義されているものがあり、ここで取り上げるアノマリー以外にも色々ある。
インコンシステントリードについてはそれらの標準に定義されたものではない。(どこで定義されているのかはわからなかった。。.)
トランザクションはTXと表記する。複数トランザクションを区別するために数字を使う。(ex. TX1、TX2)
ダーティリードは、TX1がTX2のCOMMIT前のデータを読み取ってしまう現象。
すべてのセッションはREAD UNCOMMITEDで行う。
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
TX1、TX2にてトランザクションを開始
// TX1
mysql> START TRANSACTION;
// TX2
mysql> START TRANSACTION;
TX2にてデータ追加
// TX2
mysql> INSERT INTO users(name) VALUES('foo');
TX2にてデータを追加、COMMITはしない。
// TX1
mysql> SELECT * FROM users; // 1 row in set
TX1でTX2のCOMMIT前のデータが読み取れてしまっている。
インコンシステントリードは、読み取るデータに一貫性がない現象。
いろんなAnomaly#Inconsistent Read Anomalyを参照とした。
これについては正確な定義がちょっと分からなかったので、理解が正しいか怪しい。。
COMMIT後の一貫性のなさということなので、インコンシステントリードはファジーリードやファントムリードの上位概念??な感じがするが、厳密はおそらく違うはず・・。
すべてのセッションはREAD UNCOMMITEDで行う。
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
TX1にてトランザクション開始、データ読み取り
// TX1
mysql> START TRANSACTION;
mysql> SELECT * FROM users; // Empty set
TX2にてトランザクション開始、データ追加
// TX2
mysql> START TRANSACTION;
mysql> INSERT INTO users(name) VALUES('foo');
mysql> COMMIT;
TX1にて再度読み取り
// TX1
mysql> SELECT * FROM users; // 1 row in set
最初に読み取った結果と違う結果(TX2の処理結果)が取得され、一貫性がなくなっていることが確認できる。
ファジーリードは、TX1が他のTX2にて更新したデータを参照できてしまう現象。
すべてのセッションはREAD COMMITTEDで行う。
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
// TX1
mysql> INSERT INTO users(name) VALUES('foo'); // 初期データ投入
mysql> START TRANSACTION;
mysql> SELECT * FROM users; // 1 row in set
初期データ投入結果。
+-----+------+
| id | name |
+-----+------+
| 1 | foo |
+-----+------+
// TX2
mysql> START TRANSACTION;
mysql> UPDATE users SET name = 'bar' WHERE id = 1;
mysql> COMMIT;
mysql> SELECT * FROM users; // 1 row in set
更新が完了。
+-----+------+
| id | name |
+-----+------+
| 1 | bar |
+-----+------+
// TX1
mysql> SELECT * FROM users; // 1 row in set
TX2のCOMMITが影響し、TX1の読み取り結果が変わったことが確認できる。
+-----+------+
| id | name |
+-----+------+
| 1 | bar |
+-----+------+
ファントムリードは、TX2が新規追加または削除をCOMMITした場合にTX1が読み取るデータが変わってしまう現象。 ファジーリードは更新処理、ファントムリードは新規追加または削除が対象とした現象である。
すべてのセッションはREAD COMMITTEDで行う。
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
TX1にてトランザクション開始、データ読み取り
// TX1
mysql> START TRANSACTION;
mysql> SELECT * FROM users; // Empty set
TX2にてデータを追加、COMMIT
// TX2
mysql> START TRANSACTION;
mysql> INSERT INTO users(name) VALUES('foo');
mysql> COMMIT;
mysql> SELECT * FROM users;
追加が完了。
+-----+------+
| id | name |
+-----+------+
| 1 | foo |
+-----+------+
// TX1
mysql> SELECT * FROM users; // 1 row in set
TX2のCOMMITが影響し、TX1の読み取り結果が変わったことが確認できる。
+-----+------+
| id | name |
+-----+------+
| 1 | foo |
+-----+------+
ロストアップデートは、TX1とTX2が同じデータを更新する際に競合が発生し、一部の更新が失われる現象。
すべてのセッションはREPEATABLE READで行う。
mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
TX1でトランザクション開始、データ読み取り
// TX1
mysql> INSERT INTO users(name) VALUES('foo'); // 初期データ投入
mysql> START TRANSACTION;
mysql> SELECT * FROM users; // 1 row set
TX2にてトランザクション開始、データ読み取り
// TX2
mysql> START TRANSACTION;
mysql> SELECT * FROM users; // 1 row set
TX1、TX2にそれぞれデータを更新
// TX1
mysql> UPDATE users SET name = 'tx1' WHERE id = 1;
// TX2 mysql> UPDATE users SET name = 'tx2' WHERE id = 1;
4. TX1、TX2をそれぞれCOMMIT
```sql
// TX1
mysql> COMMIT;
// TX2
mysql> COMMIT;
mysql> SELECT * FROM users; 1 row set
TX1のCOMMITが失われてTX2にCOMMITが反映されていることが確認できる。
+-----+------+
| id | name |
+-----+------+
| 1 | tx2 |
+-----+------+
トランザクションの分離レベルによって、発生するアノマリーは異なる。
アノマリーはCOMMIT前後でのデータの読み取りや一貫性が変わる現象としていくつかのパターンがある。
トランザクションのアノマリーについて詳しく学ぶにはトランザクションに関する本か何かを参照したほうが良さそう。
モニタリングのシステムにおけるPull型とPush型のアプローチの違いについてまとめる。
監視サーバー側で監視対象ホストについて設定をし、監視サーバーから監視ホストにデータを取得しにいく。 ex. Prometheus、Nagios、Zabbixなど
PrometheusはPull型の欠点を補うかのように、exporterというPush型のようなアプローチのような仕組みがある。exporterを監視対象にホストに導入、監視サーバーがexporterのデータを取得するといった感じ。
監視対象ホストにエージェントをインストールする。ホストにインストールされたエージェントが監視サーバーにデータを送信する形。 ex. DatadogやMackerelなど
以下それぞれの観点で比較してみたが、サービスによって異なる可能性があるため、必ずしもそうではない場合もあると思われる。
観点 | Pull型 | Push型 |
---|---|---|
導入コスト | 監視対象ホストごとに設定が必要なため手間がかかる | 監視対象ホストにエージェントを導入するだけなので容易 |
管理コスト | 監視対象ホストは監視サーバー側で把握できる | 監視サーバー側では監視対象ホストを把握しない |
データの取得制御 | 監視サーバー側で調整 | 監視対象ホスト側で調整 |
リソース効率 | 監視サーバーは必要なときにデータを取得できるため、調整しやすい | 監視対象サーバーからデータ取得が頻繁な場合、監視対象ホストはリソースを多くを消費する |
リアルタイム性 | 監視サーバーのデータ要求タイミングに基づく | リアルタイムでデータを送信可能 |
サーバー負荷 | 監視サーバー側で一元的に調整しやすい | 監視対象ホスト側での調製になるため、対象が増えると管理コストが高い |
通信コスト | 監視サーバーが監視対象ホストから取得できるデータが無いような場合は浪費することになる | 監視対象ホストからPushされたものを監視サーバーが受け取るだけなので通信効率が良い |
エラー処理 | 監視対象ホストの異常に気づきやすい | 監視対象ホストが対象から外れたのか、異常発生なのか判断がし辛い |
上記の表は一般的なメリットとデメリットを示しており、実際の状況によって異なる場合がある。
モニタリングシステムを選択する際には、要件やシステムの特性に合わせて、Pull型またはPush型を選択する必要がある。
キャッシュの書き込み方式についてまとめる。
データがキャッシュに書き込まれると同時にメインメモリにも書き込む方式。 書き込み操作がキャッシュとメインメモリ両方に対して発生する。 データの一貫性を保ちやすいが、書き込みの遅延が発生する可能性がある。
データがキャッシュに書き込まれた後、メインメモリに書き込みをするまでキャッシュ内にデータが保持される方式。 書き込み操作はキャッシュのみ発生し、データの必要性に応じてメインメモリに書き込みが発生する。 書き込みの遅延を隠すことができるが、データの一貫性を維持する対応が必要となる。
データがメインメモリに直接書き込まれる方式で、キャッシュはスルーする。 キャッシュへの書き込み負荷が軽減する。 キャッシュは読み取りのみ使われるパターン。
時々どっちがどっちか混乱してしまうので、メモを残す。
項目 | シャーディング | パーティショニング |
---|---|---|
データの分割方法 | 水平方向(≒水平パーティショニング) ex. 行 | 垂直方向 ex. テーブル、データベース、カラム |
メリット | パフォーマンスの向上、スケーラビリティの向上 | パフォーマンスの向上、データの検索性向上 |
デメリット | データや管理の複雑化 | データの分離・整合性の問題 |
適したアプリケーション | データベースのサイズが大きく、パフォーマンスが低下しているアプリケーション | データベースのアクセスパターンが特定のカラムに偏っているアプリケーション |
デプロイ戦略についてまとめる。
デプロイ戦略の前提知識として言葉の定義を明確にしておく。
デプロイとは、「実行環境に実行可能なプログラムを配置すること」 リリースとは、「ユーザーがアクセスできる状態にすること」 ロールバックとは、「古いバージョンのリリースをリリースすること」
代表的と思われるものを取り上げる。
既存環境に新しいバージョンを直接デプロイする手法。
余談だが、bmf-tech.comはdocker-composeを使っているのだが、デプロイはインプレースデプロイである...。 cf. [bmf-techを支える技術(https://bmf-tech.com/posts/bmf-tech%e3%82%92%e6%94%af%e3%81%88%e3%82%8b%e6%8a%80%e8%a1%93#5-%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)
シンボリックリンクを使って、新旧バージョンを切り替える手法。
環境をブルーとグリーンの2つを用意し、新バージョンを片方にデプロイし、新旧バージョンの両方を一時的に展開する手法。 新バージョンに問題がなければ、旧バージョンから新バージョンにトラフィックを切り替える。 ブルーとグリーンそれぞれの環境は保たれる。
ブルーグリーンと概ね同じ手法だが、トラフィック切り替え後に旧環境の方を削除する点が異なる。
一定数ずつ新しいバージョンをデプロイ、リリースしていく手法。 全数完了するまでは新旧両方の環境へのトラフィックが有効な状態が続く。
一部のユーザーやトラフィックに対してのみ新しいバージョンを展開することができるような形で、一部だけ新しいバージョンをデプロイ、リリースする手法。
それぞれのデプロイ戦略について、選定する際に重要となりそうな観点をピックアップし、表にまとめる。
デプロイ手法 | ゼロダウンタイム | 本番環境のテスト | ロールバック時間 | 運用コスト |
---|---|---|---|---|
インプレースデプロイ | × | × | 高 | 低 |
シンボリックリンクデプロイ | ○ | × | 低 | 中 |
ブルーグリーンデプロイ | ○ | ○ | 低(トラフィック切り替え前後でも低) | 高 |
イミュータブルデプロイ | ○ | ○ | 低(旧環境削除するまでに限る) | 高 |
ローリングデプロイ | ○ | × | 低 | 中 |
カナリアデプロイ | ○ | ○ | 中 | 高 |
プロキシサーバー(フォワードプロキシサーバー)・リバースプロキシサーバー・ゲートウェイサーバーの違いについてまとめる。
クライアントとサーバーの間に配置され、両者の通信を代理で行う(中継する)サーバー。 クライアントからのリクエストを代理をする。
クライアント→プロキシサーバー→インターネット→サーバー
メリットは次のようなものがある。
クライアントとサーバーの間に配置され、両者の通信を代理で行う(中継する)サーバー。 サーバーからのリクエストの代理をする。
クライアント→インターネット→リバースプロキシ→サーバー
メリットは次のようなものがある。
クライアントとサーバーの間に配置され、両者のネットワークやプロトコルの変換・制御を中継するサーバー。
プロキシサーバー(フォワードプロキシサーバー)やリバースプロキシサーバーとの違いは、プロキシが通信を”代理”するもので、ゲートウェイは通信を”成立”させるものであるといえる。
プロキシサーバー(フォワードプロキシサーバー)・リバースプロキシサーバー・ゲートウェイサーバーは、配置される場所が異なり、通信を代理するのか、制御するのかといった違いがある。
Google Cloud認定アソシエイトクラウドエンジニアの試験に向けた勉強をしている際の雑多な覚え書き。
cf. https://blog.g-gen.co.jp/entry/associate-cloud-engineer
IAMを安全に使用するを参照
タグ | ラベル | |
---|---|---|
リソース構造 | 個別のリソース | リソースのメタデータ |
定義 | 組織レベル | 各リソースごと |
アクセス制御 | 管理と取り付けにはIAMロールが必要 | リソースに応じて異なるIAMロールが必要 |
取り付け前提条件 | タグがリソースに取り付け可能になるにはタグキーとタグ値の定義が必要 | 前提条件なし |
削除の要件 | タグのタグバインディングが存在しない場合は削除できない | 条件なし |
IAMポリシーのサポート | IAMポリシーの条件で利用可能 | サポートなし |
組織のポリシーのサポート | 組織のポリシーの条件付き成約で利用可能 | サポートなし |
Cloud Billingの統合 | チャージバック、監査、その他のコスト割当分析を行い、Cloud BillingのコストデータをBigQueryにエクスポート | Cloud Billingでリソースをラベルでフィルタし、Cloud BillingデータをBigQueryにエクスポート |
タグは何かの制約に柔軟に利用できるが、ラベルは単なるラベル、といったイメージ。
種別 | 特徴 | マシンタイプ |
---|---|---|
汎用 | コスト最適化 | E2 |
汎用 | バランス(コストパフォーマンス) | N2, N2D, N2 |
汎用 | スケールアウト最適化 | Tau T2D, Tau T2A |
最適化されたワークロード | メモリ最適化 | M3, M2, M1 |
最適化されたワークロード | コンピューティング最適化 | C2, C2D |
最適化されたワークロード | アクセラレータ最適化 | A2 |
種別 | 特徴 | データ冗長性 |
---|---|---|
ゾーン標準PD | 効率的・信頼性をブロックストレージ | ゾーン |
リージョン標準PD | リージョン内の2つのゾーンで同期レプリケーション、効率的・信頼性を持つブロックストレージ | マルチゾーン |
ゾーンバランスPD | 費用対効果・信頼性を持つブロックストレージ | ゾーン |
リージョンバランスPD | リージョン内の2つのゾーンで同期レプリケーション、費用対効果・信頼性を持つブロックストレージ | マルチゾーン |
ゾーンSSD PD | 高速・高信頼性のブロックストレージ | ゾーン |
リージョンSSD PD | リージョン内の2つのゾーンで同期レプリケーション、高速・高信頼性のブロックストレージ | マルチゾーン |
ゾーンエクストリームPD | 最高パフォーマンスの永続ブロックストレージ | ゾーン |
ローカルSSD | 高パフォーマンスのローカルブロックストレージ。サーバーに物理的に接続されるため、起動ディスクとして設定はできない。 | なし |
Cloud Storage バケット | 低価格のオブジェクトストレージ | リージョン、デュアルリージョン、マルチゾーン |
ゾーンXXX → 冗長性がゾーン リージョン→冗長性がマルチゾーン
cf. https://cloud.google.com/compute/docs/disks?hl=ja#disk-types
次のLBはIPv4とIPv6の両方の外部IPアドレスを構成できる。
env: flex
inbound_services:
- warmup
スタンダード環境 | フレキシブル環境 | |
---|---|---|
インスタンス起動時間 | ミリ秒単位 | 分単位 |
デプロイ時間 | 秒 | 分 |
バックグランドプロセス | ✗ | ○ |
SSHアクセス | ✗ | ○ |
WebSocket | ✗ | ○ |
スケーリング | 手動、基本、自動 | 手動、自動 |
ゼロにスケーリング | ○ | ✗(最小1インスタンス) |
ランタイムの変更 | ✗ | ○(Dockerfile経由) |
ローカルディスクへの書き込み | ✗ | ○ |
サードパーティバイナリのサポート | ✗ | ○ |
ネットワークアクセス | App Engineのサービス経由 | ○ |
料金設定モデル | 1日当たりの無料使用量を超えるとインスタンスクラスごとに料金発生 | 1時間当たりのリソース(vCPU、メモリ、永続ディスク)割当量に基づいて料金発生 |
自動シャットダウン | ○ | ✗ |
cf. https://cloud.google.com/appengine/docs/the-appengine-environments?hl=ja
フレキシブル環境は機能や構成、課金体系に柔軟性がある。 インスタンス起動時間やデプロイ時間はスタンダード環境のほうが短い。 スタンダード環境のほうがより柔軟にスケーリングができる。
全部マルチゾーンなのがリージョナルクラスタ。
本番環境ではRegularまたはStableが推奨される。
Autopilotクラスタではサージアップグレードが利用される。Standardクラスタでは、自動アップグレードにおいてはサージアップグレード、手動アップグレードではサージアップグレードまたはBlue/Greenアップグレードが利用可能。
基本的にはK8Sの機能に基づいたり、それを拡張した機能となっている
第1世代では、Eventarcトリガーがなく、Firebase関連のトリガーがサポートされている
種別 | 特徴 | 可用性SLA |
---|---|---|
Standard | 最もアクセス頻度が高いものに利用 。最小保存期間なし。検索料金なし | 99.95% |
Nearline | 月1回以下のアクセス、最低保存期間30日間。検索料金あり | 99.0% |
Coldline | 四半期に1回程度のアクセス、障害復旧用など。最低保存期間90日。検索料金あり | 99.0% |
Architve | 年1回以下のアクセス、監査ログ・アーカイブなど。最小保存期間365日。検索料金あり | 99.0% |
保存料金 Standard > Nearline > ColdLine > Archive
取出し料金 Standard < Nearline < Coldline < Archive
バケット作成後もストレージクラスは変更可能。
バケット作成後はロケーションタイプの変更はできない。 データを別のロケーションに移動することはできる。
一部抜粋
サービス名 | 種別 | 特徴 |
---|---|---|
BigTable | NoSQL(列指向型) | 低レイテンシ高スループット |
FireStore | NoSQL(ドキュメント型) | Web, Native App, IoTなど |
Firebase Realtime Database | NoSQL(ドキュメント型) | リアルタイム同期 |
Memorystore | NoSQL(Key-Value型) | Redis/Memcachedと互換 |
BigQuery | データウェアハウス | 大規模データセットやクエリ |
Bare metal Solution | RDB稼働のためのハードウェア | 特殊な要件 |
MySQLにはだいぶ前から全文検索が使えるになっているが、最近まで全然触ってもいなかったので軽く素振りしてみた。
MySQLで全文検索を利用するのはElasticSearchよりも圧倒的に手間が掛からない。
検索対象としたいカラムにFULLTEXT INDEXを付与、MATCH (col1,col2,...) AGAINST (expr [search_modifier]) で検索クエリを投げるだけで全文検索がお手軽にできてしまう。
ex.
// FULLTEXT INDEX付与対象のカラムを持つテーブル
CREATE TABLE `posts` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`title` varchar(255) DEFAULT NULL,
`body` longtext DEFAULT NULL,
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
// FULLTEXT INDEXを付与
ALTER TABLE posts ADD FULLTEXT INDEX index_title_md_body (title, md_body) WITH PARSER ngram;
// MATCH ... AGINSTで検索クエリ
SELECT
*
FROM
posts
WHERE MATCH (title, body)
AGAINST ("MySQLで全文検索" IN BOOLEAN MODE)
FULLTEXT INDEXはALTER TABLE以外にもCREATE TABLEやCREATE INDEXで付与することもできる。
MySQLの全文検索のパーサーにはngramとMeCabに対応している。
デフォルトではngramが設定される。
MeCabを使いたい場合はプラグインの導入が必要。
3つのモードがあり、使いたいモードを指定することができる。
モードによって検索結果に差が出るので、どういう検索体験にしたいかによって選択の余地がある。
パーサーや全文検索のモード以外の検索の性質を調整するアプローチとしては、
などある模様。
MySQL の全文検索の微調整も参照。
このブログにもMySQLの全文検索機能を取り入れてみた。
LIKE検索より性能が良いと思われるが、実際どこまでパフォーマンスが維持できるかは環境ごとに性能検証が必要と思われるが、要件が満たせるのであれば十分に使える機能だということが分かった。
]]>PyroscopeというContinous Profilingのツールを導入してみた。
Continous Profilingについてはこちら参照。 What is continuous profiling?
今年に入ってからGrafanaが買収したらしい。
Grafana Labs が Pyroscope を買収してコード プロファイリング機能を追加
買収してからは、Grafana Pyroscopeが正式名称?ぽい。
Grafanaにプラグインが用意されているので連携することもできるが、Pyroscope単体にもUIが用意されている。
Demoが用意されているので触ってみると何が見れるか分かりやすいかも。
OSSとしてコードが公開されているので実装が気になる場合は見に行くこともできる。
構成について先に目を通しておくと良い。
DockerHubにイメージがプッシュされているのでこちらを利用することができる。
Dockerの導入ガイドはこちら。
Kubernetesの導入ガイドもある。
アプリケーション側でプロファイリングの設定およびエージェントのインストール。
基本はPush型での対応になるが、Goの場合はPull型の対応があり、Pyroscopeサーバーでターゲットの管理ができる。
GoでPull型で導入する場合は、次のようにターゲットを管理することができる。
---
scrape-configs:
- job-name: pyroscope
scrape-interval: 60s
enabled-profiles: [cpu, mem, goroutines, mutex, block]
static-configs:
- application: foo
spy-name: gospy
targets:
- foo:80
- application: bar
spy-name: gospy
targets:
- bar:81
ここでは設定していないが、データ保持期間は設定したほうが良さそう。デフォルトでは無制限に保持するらしい。 cf. Data retention
examplesに色々例が用意されている。
自分の管理しているアプリケーションで導入した例は下記。
PyroscopeにはAPI KEYやOAuth2、パスワード認証の仕組みが用意されている。
パスワード認証で初期の認証情報設定をする際にドキュメントを読み違えて少しハマった。
初期の認証情報をセットするには次のような感じで設定ファイルに記載する。
auth:
internal:
admin:
name: USERNAME
password: PASSWORD
enabled: true
ちゃんと読むとconfiguring-built-in-admin-userのところで記載があるのだが、CLIでしか設定変更できないものだと勘違いしてムダにハマってしまった...。
Pyroscope側の話ではなく、Goのアプリケーション側の話だが、pprofの設定でハマった。
それについてはDefaultServeMux以外でpprofを使う方法に記事を書いたのでそちらを参照。
OSSで利用できるプロファイルングツールを以前から探し求めていたのだが、Pyroscopeは導入がしやすく、使いやすいUIが用意されていて良さそう。GoならPull型も対応していて良い。
]]>net/http/pprofをDefaultServeMux以外(Goの標準のルーター以外)で使う方法についてメモ。
pprofをblank importするだけではだめ。
package main
import (
_ "net/http/pprof"
)
DefaultServeMux以外のルーターを使う場合はblank importするだけではpprofが利用できるようにならない。
net/http/pprofを参照すると、下記のように記載されている。
If you are not using DefaultServeMux, you will have to register handlers with the mux you are using.
下記は自分の自作ルーターbmf-san/goblinを使った例。
package main
import (
"net/http/pprof"
)
func main() {
r.Methods(http.MethodGet).Handler("/debug/pprof/", http.HandlerFunc(pprof.Index))
r.Methods(http.MethodGet).Handler("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
r.Methods(http.MethodGet).Handler("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
r.Methods(http.MethodGet).Handler("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
r.Methods(http.MethodGet).Handler("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
r.Methods(http.MethodGet).Handler("/debug/pprof/goroutine", pprof.Handler("goroutine"))
r.Methods(http.MethodGet).Handler("/debug/pprof/heap", pprof.Handler("heap"))
r.Methods(http.MethodGet).Handler("/debug/pprof/mutex", pprof.Handler("mutex"))
r.Methods(http.MethodGet).Handler("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
r.Methods(http.MethodGet).Handler("/debug/pprof/block", pprof.Handler("block"))
}
上述のようにルーティングを自分で設定し、pprofのHanderを設定してあげる必要がある。
httprouterの場合であれば、下記issueが参考になる。 pprof issue with httprouter #236
pyroscopeでGoのプロファイリングをPull型で設定しようとしたときにハマった。
]]>Google Cloud認定アソシエイトクラウドエンジニアを受験して合格したので、再受験するときや別の試験を受けるときのために勉強した過程を振り返っておく。
ソフトウェアエンジニア7~8年目くらい。 GCPの経験は2年くらいあるかな・・どちらかというとAWSのほうが触っている感じ。
業務でAWSとGCPの両方を触る機会があり、AWSの方はアソシエイトレベルの認定を取得していたのだが、GCPの方はまだだったので取得に向けて勉強することにした。
AWSのほうは2年くらい前に取得していた。 cf. AWS認定ソリューションアーキテクトアソシエイトを受験した
元々AWSの認定の後に流れでGCPの方も受験しようと考えていたのだが、なんやかんやあってこのタイミングになってしまった。
今年はKubernetesをいい加減ちゃんとキャッチアップしようと考えていて、K8Sの運用ならEKSよりGKEがいいかな(※個人の感想)と思うところがあり、GKE含めGCPの各種サービスについて知り、GCP上でアーキテクチャ設計ができるようになりたいという動機も受験を後押しした。
AWSの方は資格の有効期限が3年あるが、GCPの方は2年とやや短いが、それについては特にモチベーションに影響しなかった。
当日は横浜の試験センターで受験した。
AWSと同じくオンラインでも受験できるのだが、オンサイトのほうが環境について気にすること、気をつけることが少ないのでオンサイトにした。
平日だったこともあって全然人がいなくて思いっきり集中できた。
試験内容のあれこれについては言えないので、一言だけ感想をいうと、勉強したことは十分発揮できたように感じた。
正式な試験結果が予定よりも遅延したのだが、どうやら近い時期に受験した人も同じく遅延したらしい。 cf. https://note.com/aiue408/n/n8d5587f7362a
期間としては2~3ヶ月くらい。
育休のすきま時間で勉強していたが、以前から少し勉強を進めていたのものあって実際は2ヶ月弱くらいな気がする。
3ヶ月以内の期間で合格する計画を立てていたのだが、2週間くらい予定を前倒しできて良かった。
勉強した内容の覚え書きはGCPについての覚え書きにまとめた。要点を頭にインプットするようの内容でラフに書きまとめておいたのだが、結構役立った。
公式の情報は充実しているが、模擬試験の問題集(参考書)がAWSよりは少ないので、試験の素振り回数が少なくなるがちなのが不安になる部分だった。
出題範囲、傾向を把握して、公式の情報をベースに勉強を重ねていけばなんとかなった。
GCPの教科書は3も出版されているが、Cloud AIプロダクトについてはあまり深い問いがなさそうだったので読むのをサボっている。。
まず入門したいという感じであれば読んでも良いが、最短で試験勉強ということであれば読まなくても良いかなと思う。
以下はWorkbookでも関連資料として記載があったコース。
Preparing for Your Associate Cloud Engineer Journey | Google Cloud Skills Boostで紹介のあった下記コースも複数消化した。
上記のコースで学びつつ、苦手な領域はQwicklabsでカバーするようにした。
QwiklabsでGCP関連のものをいくつか取り組んだ。
手を動かすほうが頭に入るし、実践的なので良い。AWSの試験のときにもお世話になった。
試験ではgcloudのコマンドが問われる設問もあるため、Qwicklabsでコマンドに触れておくのは有意義だと思う。
後は触ったことがなかったサービスの理解を深めるためにも役に立った。
個別のクレジット購入よりサブスクリプションのほうがコスパが良かったので課金した。かれこれ人生で4回くらい課金しているが、年間プランをサブスクする程ではなかったりするので都度課金している。。。(年中触らないので・・)
一発で合格して良かった。
AWSの認定試験と違って、再受験ポリシーというものが設けられているため、不合格になるとペナルティような制限(最大受験回数や再受験までのクールタイムがある。また再受験は再度受験費用を負担することになる。)を受けるので、その度にプレッシャーが高まる。
不合格だったとしても、出題傾向や難易度を把握できるのでもしも再受験することになったら合格率は高まるかなとポジティブに考えるようにして、不合格を意識しないようにしていたが、試験の回答を提出するときだけはちょっと手が震えたw
AWSやGCPの認定試験は単なる暗記試験ではなく、実用的な知識が問われる試験であると思っているので、基礎知識の獲得と表明のためにコスト(時間・お金)を払う価値が十分にあると考えているので、今後も継続的に試験を受けたいと思う。
]]>html/templateを使っているときに、テンプレートに渡すURLをエンコードさせたくなかった。
Goのhtml/templateを使って、URLをテンプレートに渡すとエンコードされてしまう仕様になっている。
cf. https://pkg.go.dev/html/template#hdr-Contexts
セキュリティ上の理由でこのような仕様になっていると思うが、HTML上でこれを回避したいようなケースがあると思う。
そういうときはtemplate.URL
を使うと回避できる。
package main
import (
"html/template"
"os"
)
func main() {
const tpl = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>bmf-tech.com</title>
</head>
<body>
<a href="{{ .URL }}">bmf-tech.com</a>
</body>
</html>`
t, _ := template.New("index").Parse(tpl)
data := struct {
URL template.URL
}{
URL: template.URL("http://bmf-tech/posts/search?keyword=something"),
}
t.Execute(os.Stdout, data)
}
地味にハマった。
]]>プライベートで開発しているアプリケーションのイメージをクロスコンパイルする必要性に駆られて(ローカルの開発環境と本番の環境でアーキテクチャが異なっていることが起因)対応したのでメモ。
Docker Desktopにはbuildxが標準で備わっているのでそちらを利用する。
buildxを使うことでマルチアーキテクチャ対応のイメージを簡単に作ることができる。
こんな感じのDockerfileがあったとする。(実際に使っているDockerfileなのだが..)
FROM --platform=$BUILDPLATFORM golang:1.20.0-alpine as builder
WORKDIR /go/gobel-api/app
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG TARGETOS
ARG TARGETARCH
COPY . .
RUN apk update && \
apk upgrade && \
apk add --no-cache libc-dev gcc git openssh openssl bash
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o app
FROM --platform=$TARGETPLATFORM alpine
COPY --from=builder /go/gobel-api/app ./
ENTRYPOINT ["/app"]
環境変数については下記参照。 cf. https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/builder/
buildとpushはこんな感じ。platformは複数指定することができる。
// ビルダーインスタンスの作成
docker buildx create --name buildx-builder
docker buildx use buildx-builder
// ビルドしてdockerhubにpush
docker buildx build --no-cache --push --platform linux/amd64,linux/arm64 --file app/Dockerfile --tag bmfsan/gobel-api app/
MySQLのオフィシャルイメージがいつの間にかARMにも対応するようになっていた。 M1ユーザーは嬉しい。
カンファレンスなどの登壇イベントでCFPを提出することがあるが、自分がどのようにCFPを書き上げ、提出しているかについてポエムを書いておく。
過去CFPを提出した回数が7回、採択率は100%(うち1件は諸事情により辞退)であった。
登壇したイベントは、PHPのカンファレンスとGoのカンファレンスで、PHPのカンファレンスのほうが登壇回数が多い。
cf. https://speakerdeck.com/bmf_san
正直なところ、登壇を始めた初期の頃の発表内容やCFPについては不甲斐ない部分が多々あるのだが、ここ最近はだいぶ自信がついてきたので、一家言残しておくかと思った次第である。
CFPを書く前までにいつもやっていることを整理しようと思う。
登壇することで自分が得たいものは何か?が決まっていると登壇のモチベーションになる。
自分にとっても聴衆にとっても価値が見いだせそうなことであるかどうかという観点を意識している。自分にとってのメリットだけ考えて、CFPの採択によって聴衆にとっても価値があると評価された、と考えるやり方でも悪いとは思わないが、色んな視点で考えておいたほうが発表テーマの幅も広がって自らが得られるものも増えると思っている。
自分が過去参加したイベントであれば雰囲気やテーマなどにイメージを持ちやすいと思うが、そうではないイベントの場合は、過去の開催模様を調べたり、実際にイベントに参加してみたほうが良いと思う。
どういうテーマ、レベル感の発表があるのか、聴衆の特徴やスポンサー企業等はどんな感じか?といったことを知っておくと、実際に登壇するときにいくらか自信を持つことに繋がると思うので、割と大事な下準備だと思っている。
自分は過去の経験もあって登壇して人前で話すということに全く緊張しない、むしろ楽しめるのだが、こうした事前の調査を済ませておくことがメンタリティの保全にも繋がっているからという側面はあるように思う。
これも当たり前の話ではあるのだが、CFPが採択されてもスケジュールが合わない、なんてことになってしまうと悲しいので、スケジュール調整を事前に済ませておけると良い。
CFPを提出する時点で発表のイメージが概ね出来上がっている、というのが自分的な取り組み方かもしれない。
CFPを書き始める時点で、発表テーマのアウトラインがある程度整理されていて、スライドを書き始めることがほぼできる状態になっている。
人に依るだろうが、自分の場合は、”ネタ”が先に用意されていて、「このネタは発表できそう」というものが見えてから登壇を検討する、というのがいつもの流れになっている。(キーワードからネタを作って、登壇準備する、みたいなケースとかあると思う。登壇駆動的だったり、それに近かったり。)
自分がどういうステップでCFPを提出しているか順を追って説明していく。
ネタがないと何も始まらない。
興味・関心に基づいてプライベートで行っていたことをそのままネタにすることもあれば、特定の技術に関連してネタを考えるところから始めることもあるが、殆ど前者の場合が多い。
CFPを提出するのにブログを書くとはなんぞやといった感じかもしれないが、自分にとってこのプロセスが非常に重要である。
ブログを書く≒CFP書けた、スライドできたも同然に近い。
ブログを書き上げておくことで、ネタを発散させることができるため、CFP執筆もスライド作成もスムーズに進めることができる。
ただのメモ書きではなくて、構成をちゃんと考えてブログを執筆しておくことが重要。
ブログを書いてからCFPを書く、というのが自分のやり方になっている。
いきなりCFPを書くのではなく、書くために色々な整理をする。
ここ最近はmiroを使って整理している。
下図のような感じで整理している。
自分がCFPを書き上げるために用意しているフォーマットはこちら。
このフォーマットを順不同で仕上げることでCFPが完成する。
このフォーマットの大元のネタは、昔勤めていた会社の先輩エンジニアに教わったものをある程度ベースにしているが、自分なりの手を色々加えている。
Go Conference 2021に提出した準備内容を例に、フォーマットについてそれぞれ解説する。
セッションのタイトルを書く。
ネタが用意できている、ブログが書けている時点でセッションタイトルは概ね定めることができるが、それらがまだの準備できていない場合は、仮のタイトルを先に決めておいたりする。
タイトルの考え方としては、一息で端的に発表内容のイメージが伝わるか?だけを考えている。コーディングするときに関数や変数の命名作業の考え方に近い。
イベントに関する情報を確認・記載しておく。
イベントタイトル・イベント日時・CFPを提出しようとしている登壇枠詳細などを記載。
提出するCFPの文章を書く。
他のパートが整理できたあとで書き上げることもあれば、先にラフに書き上げることもある。
CFPは文字数がある程度限られている気がするが、そうでなかったとしても、なるべく端的に書くように意識している。
採択する側にとって必要な情報を盛り込んでいれば、手短に書き上げれば良いはずと考えている。
自分は次のようなアウトラインでCFPを書き上げている。
イベントの採択基準について確認・記載しておく。
大体のカンファレンスでは採択基準が設けられている。
これはレギュレーションなので、よく読んで確認しておく。
採択基準だけでなく、行動指針などもちゃんと読んでおくべきである。
話すこと・話さないことを書く。
特に話さないことをちゃんと書いておくのが大事だったりする。一番伝えたいテーマを優先として、関連性の低いことを話しすぎてしまうと、内容が発散して伝わりづらくなるため、話す必要がないようなことを明確にしておく。
伝えたいことが何か書く。
いくつ書いても良いと思うが、1テーマにつき1つに焦点を当てるようにしたほうが内容はまとまりやすい。
発表内容に関連するキーワードを拾い上げて書く。
これはアイデアを広げるためというよりかは、整理して要点を見つけるためにやるというニュアンスが大きい。
書き出してみるとこのキーワードが重要だというのが見えるので、それを元に伝えたいことや主張をまとめやすくなる。
裏を返すと、触れる必要がなさそうなキーワードも見えたりするので、エクササイズがてら書き出してみるようにしている。
発表内容のアウトラインについて書く。
これは書いたり書かなかったりする。ブログが仕上がっている場合は書かなかったり、仕上がっていない場合はブログを書くために書いたりする。
ブログが仕上がっている場合は、ブログの内容をベースにアウトラインを整理している。
スライドのラフイメージを付箋ベースで書く。
これはアウトラインからスライドにどう起こすかをイメージして書くのだが、最近はスライド作成ソフトを起動して、そちらでスライドを付箋代わりにアウトラインを作って、スライドを作っているので、やらなくても良かったりする。だが、やっておくと発表の流れを意識できるので、違和感や深堀りすべき箇所に気づいたりするので、やっておく価値はある。
後で入念に調べておきたい、深堀りしておきたいこと等があれば書いておく。
サンプル実装を用意したり、技術検証をしたりといった調査事項等をまとめてメモしておく。
主張したいことの論理や事実確認等の確認事項なども書いておいたりする。
実際に提出したCFP例を記載しておく。
Goの標準パッケージであるnet/httpを使ってHTTPルーターを自作する方法についてお話します。net/httpを使った簡単なサーバーを起動するコードの読み解きから始めて、HTTPルーターの自作方法、アルゴリズムについて解説します。 優秀なルーターがOSSとして存在しているため、あまり自作するような機会はないかもしれません。しかし、自作を通して、net/httpや木構造への理解を深めることができるかと思います本LTはGoの入門者をターゲットとしてお話しします。
本LTの主旨となる資料を記載します。
## Github
- [github.com - bmf-san/goblin](https://github.com/bmf-san/goblin)
## ブログ
- [GolangのHTTPサーバーのコードリーディング](https://bmf-tech.com/posts/Golang%E3%81%AEHTTP%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89%E3%83%AA%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0)
- [URLルーティング自作入門 エピソード1](https://bmf-tech.com/posts/URL%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E8%87%AA%E4%BD%9C%E5%85%A5%E9%96%80%E3%80%80%E3%82%A8%E3%83%94%E3%82%BD%E3%83%BC%E3%83%89%EF%BC%91)
- [URLルーティング自作入門 エピソード2](https://bmf-tech.com/posts/URL%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E8%87%AA%E4%BD%9C%E5%85%A5%E9%96%80%E3%80%80%E3%82%A8%E3%83%94%E3%82%BD%E3%83%BC%E3%83%89%EF%BC%92)
- [Introduction to URL router from scratch with Golang](https://dev.to/bmf_san/introduction-to-url-router-from-scratch-with-golang-3p8j)
## スライド
- [GolangでURLルーターをつくった](https://speakerdeck.com/bmf_san/golangteurlrutawotukututa)
こちらは採択されたのだがどうしても調整のできない諸事情のため辞退させて頂いたイベントになる。本当に参加したかったという悔しさと運営の皆様方への申し訳無さが募ります。。。 このネタはまだスライドに起こしていない未発表作ではあるので、次回再チャレンジしようと思います。
Goには標準でルーティングに使える機能(マルチプレクサ)が用意されていますが、実際の開発では何かしらのHTTP Routerのパッケージを利用することが多いのではないかと思います。
私はHTTP Routerを自作しているのですが、自作パッケージと世に有るパッケージとの性能差を比較してみたくなりました。
そこでベンチマーカーを実装し、性能差の比較に取り組んでみました。
いくつかのパッケージをピックアップし、比較してみた結果とそこから得られた学びについてお話しようと思っています。
このLTでは次のようなアウトラインを考えています。
モチベーション
前提(性能比較における前提条件)
計測手法および計測対象
計測の結果
学び
「どのHTTP Routerがもっとも性能が出たのか?(俺調べ)」
「標準のマルチプレクサとの性能差は?」
「性能だけをHTTP Routerの選定基準として考えることができるのか?」
などといった疑問についての私なりの回答を示したいと思います。
// 以下はCFP本体ではなく、補足情報的な枠で提出したテキスト
この発表の本筋は、「天下一HTTP Router武闘会」という過去の発表に基づきます。
cf. https://speakerdeck.com/bmf_san/tian-xia-httprouterwu-dou-hui
こちらの発表で当初内容を整理しきれなかった部分を整理し、ブラッシュアップした内容を今回の発表テーマとして考えています。
武闘会だと物騒で、武闘会2だと謎なので舞踏会にしてみました。
その他関連情報
https://dev.to/bmf_san/implemented-a-bench-marker-to-compare-gos-http-router-146p
HTTP Routerの比較についてまとめた記事です
https://github.com/bmf-san/goblin
自作しているHTTP Routerです
裏のテーマとしては、HTTP Router実装の面白みなんかも聴衆に伝わると良いかなとも考えています。
自分がCFPを書くときのポイントというかコツというか要点としては、「ブログを書き上げておく」ことに尽きるかなと思う。
文章でアウトプットできていればプレゼンテーションの準備は6~7割くらい済んでいるというお気持ちでいる。
普段CFPを仕上げるときはこんな感じの流れで書いているが、回を重ねるごとにやり方がラフになったりするのだが、ブログを書くというとこの作業だけはマストで外さないようにしている。
CFPが採択されれば後は、流れに乗ってスライドを作ったり、煮詰めたりするだけなので、しなやかに作業を進めることができる。
多少形式ばったところはあるが、準備を怠らないことで作業はスムーズになるし、採択率も上がるのではないか(個人的主観)と思う。
そういえば過去一度だけ運営として採択側を務めたイベントがあるが、やはり採択側の気持ちになるというのも一定大事な点かなと思う。
こういうビジネススキルみたいなものは勉強する余地があるなぁと常に思っているが、怠惰ゆえにやれていない...
伝え方一つで印象が左右されるようなものなので、磨きをかけた分のリターンは大きいと思ってはいるが、最近はChatGPTが良い師になってくれそうな予感がしている。
CFPだけの話に留めようと思っていたが、余談を付け加えておく。
登壇イベントのスライド作成で意識していることをいくつか整理してみた。
元々いくつかのNewSQLのDBについての比較をしてみたいと思って漁っていたのだが、内部で使われている技術について知っておく必要があったので、関連技術についての記事が多めになっている。
実際にDB選定する際は要件に合わせた検証や性能比較が必要と思われるが、比較する際のポイントがいくつか分かった。
dockerコマンドをcronで実行しようとしたら"the input device is not a TTY"と怒られてしまった。
cronに設定しようとした内容例は以下。
* * * * * user docker exec -it container-name mysqldump dbname -uuser -ppassword > backup.sql
-t
がTTY割当、-i
が標準入力を開くオプションだが、cronの実行では不要だった。
-it
のオプションを削除すれば解決。
2023年3月現在のFuelPHPの現況についてざっくり調べたことをまとめる。 独自に調べた情報なので正確性には欠けるかもしれない。
PHPのフレームワークはLaravelが圧倒的に人気があるように感じるが、FuelPHPもまだ現役で使っている人がいるかもしれないので、何か一助になれば嬉しい。
独自に調べたFuelPHPの情報をまとめる。
Version | Supported PHP Version | Changelog | 補足 |
---|---|---|---|
1.8.0 | >=5.3.3 | Changelog v1.8 Changelog v1.8.0 hotfix1 | 多分PHPは7.0までをサポートしていそう |
1.8.1 | >=5.3.3 | Changelog v1.8.1 | 多分PHPは7.3までをサポート |
1.8.2 | >=5.4 | Changelog v1.8.2 | リリース情報によると7.3までをサポート |
1.9/develop | >=5.4 | N/A | Forumやcommitを見る限り、PHP8.0をサポートしていそう。 |
2.0(非公開) | N/A | N/A | Forumによると設計が刷新されたバージョンで、PHPは7.4以上8.0未満をサポートするらしい |
FuelPHPのそれぞれのバージョンにおいて、いつまでセキュリティのサポートをするか、EOL予定等は明記されていない模様(1系に関してはEOLかもしれない)。
現行の最新バージョンはFuelPHP1.8.2となるが、サポートしているPHPのバージョンは既にEOLを迎えている。
上の表では8.0まで対応しているらしいと書いたが、実は8.1対応もある程度なされているらしい。
これに関して、中の人(Harro)にいくつか伺ってみたことがあるので、サマリを以下に記載する。(質問した時期は2022年4月頃) なお原文は英文であるため、ニュアンスを上手く汲み取れていない可能性がある(英語力高くないので・・)がご了承いただきたい。
Q. FuelPHP 1.9について何かアップデート情報はあるか?
A. 我々のアプリケーション(Harroが業務で扱っているアプリケーションと思われる)ではPHP8.1で動いているので、1.9-devを利用することは可能だと思う
Q. 1.9-devはPHP8.1で動作するのか?
A. 8.1での動作確認を十分に行えていない、8系でしかテスト実行できていない。
Q. 1.9-devのリリースのために何か手伝えることがあるか?
A. PRのテストができていないので手伝ってほしい。
(このPRだけ対応すればリリースできるわけではなさそうに見えるので、手伝えそうな作業を教えて頂いたのだと思う)
Q. 金銭的なサポートが必要か?寄付が何かFuelPHPの開発を助けることができるか?(1.9/developのリリースを早める手段になり得るか気になったので聞いてみた)
A. 現時点で必要ではない。私(Harro)の会社ですべて必要なインフラを賄えているし、全てのアプリをFuelで開発しているため、バグフィックスも暗黙的にサポートされている。リソースが足りないわけではない。
1.9/developはあくまで正式リリース版ではないが、PHP8.1ではある程度動作担保されているように思える。
実際に個人的な調査(FuelPHPで動いているアプリケーションのPHP・FuelPHPバージョンを上げて動作確認)をしてみたが、結構動いた。
1.9/developのテストカバレッジはかなり低いのが怖いところではあるが、1.8.2以下のバージョンを使っていてPHPのバージョンを8.0〜8.1にどうしても上げたい場合には1.9/developを使うという選択肢もあるのではないかと思った。(個人の感想です。自己責任で・・)
会話のやりとりで知ったことだが、FuelPHP1系は既にEOLを過ぎているらしい。それ故か1系にはもはや注力していかない方針だということも聞いた。
ちょうどそんなやりとりをしている後日、ForumやTwitterに投稿があり、どうやら1系に力を入れずに2.0に力を注いでいくという方針であることが分かった。
以前は、「2.0は大きな変更になるので、かなり時間がかかっている、PHPのEOLが来てしまったので先に1.9/developの方をリリースする」、といった方向性だったように思っていたのだが、どうやらその認識は間違っていたようだ。
1.9/developが正式にリリースされるかどうかは見えないが、2.0のalphaリリースは期待しても良いのかもしれない。
2.0はまだprivateでソースコードがpublicになっていないので全貌はわからないが、静的インターフェスがなくなってDIコンテナが使われるようになるのでテストが書きやすくなるらしいので、テストのカバレッジについても期待を持っても良いのかもしれない。
FuelPHPにロックインしているアプリケーションをどのように刷新していくかということに課題を持っているサービスはきっと世の中まだ一杯あると思うが、各所どういう戦略を取っているだろうか・・気になる。 特に別のアプリケーション、アーキテクチャにリプレイスすることが現実的ではない、かなりの痛みを伴うといった場合において、取るべき選択は結構限られるはず・・・。
]]>なぜ色々と調べてみたかというと、システム設計について体系的に学ぶことができないか知りたかった、学んでみたかったからである。 もっというと、システム設計についての能力を高める糸口が欲しかったといったところだ。
システム設計の能力というと、職人芸というか、経験に依るところが大きいと思っているのだが、知識として知っておいたほうが良いかも多々有るだろうと思ったので、幅広く色んな記事に目を通してみた。
当然読んでみたからといって能力が上がったわけではないし、そんな気は全くないが、知るべきことや考えるポイントみたいなところは結構学び得ることができたように思うので、今後にお役立ちな情報を得たような気はする。
Datadog continuous testingについて調べたことのメモ。
E2Eの運用を楽にし、テストの信頼性を保つ仕組みを備えている、ようだ。
Chrome拡張のDatadog test recorderが必要。
それ以外の準備は不要ですぐに利用開始できる。
レコーディングする際は、popupで開くほうが良さそう(レコーディング画面内のUIはiframeなので)。
テスト実行に関するあらゆるオプションの設定。
Basic認証の対応ができたり、Cookie、リクエストヘッダの設定などもできる。
cf. テストコンフィギュレーション
Browser Testsで利用できるテスト項目についてそれぞれ見てみる。
DOM周りをチェックできるアサーション。
遷移系。
UIに関する操作系。
任意の変数が定義できる。
既存のブラウザテストを別のブラウザテストの中でも再利用できる。 最大2階層まで入れ子にして再利用することができる。
ブラウザテスト内でHTTPリクエストを実行できる。
実際どれくらいのまでの修復が可能なのか細かいところはわかってない。 UIの変更があった際に自動的に変更を検知して探索するとは書かれている。
If there is a UI change that modifies an element (e.g., moves it to another location), the test will automatically locate the element again based on the points of reference that were not affected by the change. Once the test completes successfully, Datadog will recompute, or “self-heal,” any broken locators with updated values. This ensures that your tests do not break because of a simple UI change and can automatically adapt to the evolution of your application’s UI. In the next section, we’ll look at how you can fine-tune your test notifications to ensure that you are only notified of legitimate failures.
cf. https://www.datadoghq.com/ja/blog/test-maintenance-best-practices/
あくまで”単純なUIの変更”に限るので、あまり期待を高めないようにするのが良さそう。
テストが正常に実行されると、ブラウザテストは壊れたロケータを更新された値で再計算 (または「自己修復」) し、単純な UI の更新でテストが壊れることがなく、テストがアプリケーションの UI に自動的に適応することを保証します。
cf. https://docs.datadoghq.com/ja/synthetics/browser_tests/advanced_options/
Syntheticの設定から並列化を設定できる。
cf. https://docs.datadoghq.com/ja/continuous_testing/settings/
Synthetic MonitoringとContinuous Testingの結果(CI Batches)およびテスト実行結果(Test Runs)を検索できる。 cf. https://docs.datadoghq.com/ja/continuous_testing/explorer/?tab=cibatches
RUM から収集したブラウザデータと Synthetic ブラウザのテスト結果を使用して、RUM アプリケーションのテストカバレッジ全体に関する洞察を提供 cf. https://docs.datadoghq.com/ja/synthetics/test_coverage/
どこがテストされていないのかといったことが分析できそう。テストケースの網羅性の改善に役立ちそう。 カバレッジの推移を追っていく
Synthetic testsは各種CIと連携できる。
用意されているインテグレーション。
cf. https://docs.datadoghq.com/continuous_testing/cicd_integrations/
cf. https://registry.terraform.io/providers/DataDog/datadog/latest/docs/resources/synthetics_test
DOMベースのテストになるのでコードからテストケースを起こすということは厳しいと思う。。。
Syntheticsとしての管理下にあるので通知周りは特に懸念なし。
1000回/$12(オンデマンドだと$18) cf. https://www.datadoghq.com/ja/pricing/?product=continuous-testing#continuous-testing
安価だと思う。 1000回以下は無料??
シナリオが沢山用意されたときに破綻しないか?
テストシナリオの管理方針としては、
テストケース自体の管理方針としては、DRYにテストを作っていくことが推奨されており、サブテストを積極的に利用したほうが良さそう。 cf. https://docs.datadoghq.com/ja/synthetics/browser_tests/actions/?tab=%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%A7%E8%A6%81%E7%B4%A0%E3%82%92%E3%83%86%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B#%E3%82%B5%E3%83%96%E3%83%86%E3%82%B9%E3%83%88
解決
2023年1月2日正午頃、https://bmf-tech.com/
にアクセスするとレスポンスが遅い、500エラーが常に返却されることに気づき、発覚。
grafanaにログインして調査をしようとしたが、ログインができなかった。
一部コンテナが何らかの原因でダウンした可能性を考慮して、デプロイを実施したが、no space left on device
のエラーログを確認したため、別の原因であることを推察。
bmf-techの全サービスが利用できない状況となった。
Nginxのリクエスト状況を確認するに、2023年1月2日午前5時48分頃からサービスダウンしていた。
復旧は同日12時40分頃。
2023年1月2日午前5時48分頃~12時40分頃までの間に58件の500エラーが発生。 ※ある程度のユーザー数を計測したかったが、ログやGA4などから集計できるように調整していないため調査しづらい。
ファイルシステムに空きがないのが原因であった。
ファイルシステム容量を圧迫しているデータを削除し、空き容量を確保。
df -h
/var/lib/docker/
配下であると特定。du -sh /var/lib/docker*
docker system prune -a
journalctl --vacuum-size=200M
上記の対応だけでは不十分だった。
/var/lib/docker/containers
配下に溜まっているログファイルが容量を大きく取っていたので、コンテナのログローテーションを調整することで対応した。
ex.
app:
container_name: "app"
logging:
driver: "json-file"
options:
tag: "{{.ImageName}}|{{.Name}}|{{.ImageFullID}}|{{.FullID}}"
max-size: "10m" // ロールオーバーするファイルサイズ
max-file: "3" // ログが何回ロールオーバされたら破棄するか
この対応により容量に大きく余裕を持つことができた。一番のボトルネックがここであったということ。。。
ファイルシステムの利用率をアラートに追加。事前に逼迫を検知して対処できるようにした。
削除できるデータやローテーションすべきデータを洗い出して節約できるようにしたい。
bmf-techをリプレースして以来、初めての障害であった。
]]>毎年恒例の振り返りと抱負について書く。
自分の中で大きめの課題であった、このbmf-techのリプレースを完了したことが一番達成感があったことかもしれない。ソフトウェアエンジニアリング総合格闘技みたいな楽しさも享受できた。
これは長々と取り組んできていたことだったので、ようやく区切りがついたのは良かった。
これに固執して囚われていた時期もあったが、このシステムを通じて様々なことを学ぶきっかけになったりしたので、時間を使った価値はあった。
それ以外ではツールを作ったり、ブログを書いたり、本を読んだり、友人と勉強会したりといったことを例年程度にやっていた。
学びという面では、仕事と結びついて特定の技術についてキャッチアップする機会がよくあったのが良かった。
後は去年データ構造とアルゴリズムについて学んでいた延長で、HackerRankに集中的に取り組んでいた時期もあった。
コードを書く力を養ったり、アルゴリズムを学んだりと良い取り組みであることを体感できたので、今後また再開する予定ではある。
あとはシステムアーキテクチャについての勉強も割と力を入れて取り組んでいた。といっても色々な読み物をこなすだけではあるのだが・・
参考になりそうな記事等を色々と見つけて読んでいるので、今度記事にしようと思っている。量が多くてまだ未だ読み切れていないものも多い。。。
読んでいるだけでは力がつかないなぁと思うが、引き出しを作っておくことは無駄でないなぁと感じていることも多かったので拾い集めた記事に関しては年末年始中には一通り読み終えたい。
やろうとしていたけどやれなかったこと。
今年一番の成果は、チームのリーダーとして1年近く続いたプロジェクトのリリースを完遂できたことだと思う。
技術的な学びもあったが、リードエンジニアとしての経験値を大きく得ることができたというのも大きな収穫だった。
メインはチームのプロジェクトだが、それ以外にサイドプロジェクトに2件ほど関わったり、認定スクラムマスターの研修・資格取得したり、社内ツール作ったり、輪読会を開催したり、社内LTやったり、アドカレやったり、アドカレ間違えて削除しちゃったり、やりたいことに沢山関われた年だった。
やりたいことは多々あるし、やりがいもあって、今年も楽しくやることができた。
いつもお世話になっている方々に感謝しつつ、来年も健康に楽しく仕事ができることを願いたい。
「何ができるのか?ということに対して明確な答えを増やす」というのが2021年の抱負であった。
いくつかぼんやりとした答えは持てた気がするのだが、明確なものを1つ上げるなら、「リードエンジニアとしてチームの立ち上げから稼働までリーディングすることができる」というソフトスキルが上げれそう。
ちゃんと言語化するのがまだ難しいが、経験と実績が自信につながった部分があるので、自分が仕事をする上で発揮できる価値の1つに計上したい。
今後自信を打ち砕かれるようなこともあるかもしれないが、そのときは頑張る。
「武器をつくる」を抱負にしたい。
これは2021年の抱負の延長線上にあるものではあるが、少し違うことを意識しようと思う。
ソフトスキルではなく、ハードスキルで自分の得意とする領域を何か持つ、ということを具体的には意識している。
誰にも負けない、その領域の先駆者、専門家、というレベルの到達は1年程度でできると考えていないので、あくまで自分が得意だと言い切れるものを1つ以上持てるようにしたいと思っている。
これは来年一番投資をする技術は何かという問いでもあるので、来年頭に少し時間を使ってから決めたい。今のところは何かしらの言語かコンテナ技術*、あるいは特定のシステムアーキテクチャなど関心の強い領域に投資したいと考えている。
来年は30歳という節目というか、区切りを迎えるので、先の10年どう戦うか?といったことも意識していたりする。
自分が今考えている数年先(5~10年スパン)の活躍のイメージのメモ(言語化が足りていない)。
// こういう活躍ができるエンジニアになりたい
- アーキテクト
- システムデザインのスキル
- 知識、経験...
- 組織とシステムアーキテクチャのスケーラビリティを考えることができる(技術+α?)
- 事業のスケールに応じて組織・システムのスケーラビリティを支える(経営の視点がありそう。CTOっぽい?)
- より難しいアプリケーションの開発ができるようになりたい
- コーディング能力、コードリーディング能力、高度な仕様理解(RFCを解釈する、複雑な仕様を読み解く、理解する、とか?)
- 高度なソフトウェア開発の能力
- 理解できるレベルを上げていきたいイメージ
- CSを学んでおくことで底上げできる部分があるのではないか
- 世の中の既存技術で解決できない、しづらいシーンなどに直面したときに技術的な解決ができる力がほしい。また技術的な課題を解決できるアプローチを多くもつことで、組織や事業に貢献できる可能性も高まるのではないか
毎年分野ごとに学ぶことを自分の中のwill(やりたいこと。興味・関心。)・can(できること。willとmustの中間でやりたいことみたいなイメージ)・must(仕事で必要、やらないとキャリア的にリスク、とか)に照らし合わせて整理して、目標を決める、毎月振り返る、というのを数年やっているが、年々ブレがなくなってきた(キャリアやスキルについての考え方がブレなくなった)気がする。
目標の達成度合いというと中々満足できないところがあるが、持てる可処分時間をどう効率良く使うかという問題に帰結するので悩ましい。
今年は社会や経済の辛さや厳しさを感じた年であり、来年以降もどうなんだろう・・という不安が例年になくあるが、前向きに頑張っていきたい。
今年のホッテントリ。
社内LTの勢いで用意したスライドが...w https://b.hatena.ne.jp/entrylist?url=https%3A%2F%2Fspeakerdeck.com%2Fbmf_san%2Ftian-xia-httprouterwu-dou-hui
去年はzennで出した本。 https://b.hatena.ne.jp/entry/s/zenn.dev/bmf_san/books/3f41c5cd34ec3f
せっかくなので来年も何かがホッテントリに入ることを期待したい。
]]>GoのHTTP Routerのパフォーマンスを比較するためのベンチマーカーを実装した。
現在のところ以下のHTTP Routerを比較対象としている。
一部のテストケースでは、Goの標準であるnet/http#ServeMuxも対象となっている。
bmf-san/goblinというHTTP Routerを自作している。
bmf-san/goblinはTrie木をベースとしている必要最低限の機能を持ったシンプルなHTTP Routerである。
bmf-san/goblinと他のHTTP Routerのパフォーマンスを比較することで、bmf-san/goblinの改善のヒントを得たいというのが動機である。
別の理由としては、julienschmidt/go-http-routing-benchmark の代わりになるベンチマーカーを自分でメンテナンスできるようにしたいと考えていたという動機もある。
julienschmidt/go-http-routing-benchmarkはjulienschmidt/httprouterのメンテナーが管理しているリポジトリだが、近年でメンテナンスが止まってしているように見えたので、それならば自分で作ってみようという気持ちになり、ベンチマーカーを実装することにした。
前提として、bmf-san/go-router-benchmarkはHTTP Routerのパフォーマンスを完全に比較するものではないということを明言しておきたい。
理由としては次の通りである。
従って、ベンチマークテストはHTTP Routerの特定の機能・仕様のみを対象としたテストケースになってしまうが、一定の性能差を測り知ることはできる。
bmf-san/go-router-benchmarkでは、ルーティングの処理部分を性能測定している。
具体的にいうと、http#Handlerの ServeHTTP
関数をテストしている。
ルーティングを定義する処理についてテスト対象としていない。 ルーティングを定義する処理とは、ルーティング処理に必要なデータを登録する処理である。
package main
import (
"fmt"
"net/http" )
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler) // here
ListenAndServe(":8080", mux)
}
ルーティング処理のテストケースとしては次の2つのテストケースを用意している。
それぞれのテストケースについて説明する。
静的なルートとは、/foo/bar
のような可変のパラメータを持たないルートのことを指す。
このルートのテストでは次の4パターンの入力を用意している。
/
/foo
/foo/bar/baz/qux/quux
/foo/bar/baz/qux/quux/corge/grault/garply/waldo/fred
このテストケースではGoの標準であるnet/http#ServeMuxも比較対象としている。
パスパラメータを使ったルートとは、/foo/:bar
のうような可変のパラメータを持つルートのことを指す。
このルートのテストでは次の3パターンの入力を用意している。
/foo/:bar
/foo/:bar/:baz/:qux/:quux/:corge
/foo/:bar/:baz/:qux/:quux/:corge/:grault/:garply/:waldo/:fred/:plugh
HTTP Routerによっては、可変のパラメータのシンタックスが異なるため、それぞれのシンタックスに対応することも考慮している。
ex. pathparam.go#L15
ベンチマークテストの実施結果は
ベンチマークテストの実行環境は次の通り。
ベンチマーク結果の見方としては次の通り。
cf. bmf-tech.com - Goで始めるコードのパフォーマンス改善
それぞれのテストケースの結果について記載する。
静的なルートについては、標準のnet/http#ServeMuxよりも性能が良いか、等しいかが一つの比較ポイントであるように思う。 パフォーマンスの良さを謳っているHTTP Routerはやはり標準よりよい結果を出している。
time | static-routes-root | static-routes-1 | static-routes-5 | static-routes-10 |
---|---|---|---|---|
servemux | 24301910 | 22053468 | 13324357 | 8851803 |
goblin | 32296879 | 16738813 | 5753088 | 3111172 |
httprouter | 100000000 | 100000000 | 100000000 | 72498970 |
chi | 5396652 | 5350285 | 5353856 | 5415325 |
gin | 34933861 | 34088810 | 34136852 | 33966028 |
bunrouter | 63478486 | 54812665 | 53564055 | 54345159 |
httptreemux | 6669231 | 6219157 | 5278312 | 4300488 |
beegomux | 22320199 | 15369320 | 1000000 | 577272 |
gorillamux | 1807042 | 2104210 | 1904696 | 1869037 |
bon | 72425132 | 56830177 | 59573305 | 58364338 |
denco | 90249313 | 92561344 | 89325312 | 73905086 |
echo | 41742093 | 36207878 | 23962478 | 12379764 |
gocraftweb | 1284613 | 1262863 | 1000000 | 889360 |
gorouter | 21622920 | 28592134 | 15582778 | 9636147 |
ozzorouting | 31406931 | 34989970 | 24825552 | 19431296 |
techbook13-sample | 8176849 | 6349896 | 2684418 | 1384840 |
nsop | static-routes-root | static-routes-1 | static-routes-5 | static-routes-10 |
---|---|---|---|---|
servemux | 50.44 | 54.97 | 89.81 | 135.2 |
goblin | 36.63 | 69.9 | 205.2 | 382.7 |
httprouter | 10.65 | 10.74 | 10.75 | 16.42 |
chi | 217.2 | 220.1 | 216.7 | 221.5 |
gin | 34.53 | 34.91 | 34.69 | 35.04 |
bunrouter | 18.77 | 21.78 | 22.41 | 22 |
httptreemux | 178.8 | 190.9 | 227.2 | 277.7 |
beegomux | 55.07 | 74.69 | 1080 | 2046 |
gorillamux | 595.7 | 572.8 | 626.5 | 643.3 |
bon | 15.75 | 20.17 | 18.87 | 19.16 |
denco | 14 | 13.03 | 13.4 | 15.87 |
echo | 28.17 | 32.83 | 49.82 | 96.77 |
gocraftweb | 929.4 | 948.8 | 1078 | 1215 |
gorouter | 55.16 | 37.64 | 76.6 | 124.1 |
ozzorouting | 42.62 | 34.22 | 48.12 | 61.6 |
techbook13-sample | 146.1 | 188.4 | 443.5 | 867.8 |
bop | static-routes-root | static-routes-1 | static-routes-5 | static-routes-10 |
---|---|---|---|---|
servemux | 0 | 0 | 0 | 0 |
goblin | 0 | 16 | 80 | 160 |
httprouter | 0 | 0 | 0 | 0 |
chi | 304 | 304 | 304 | 304 |
gin | 0 | 0 | 0 | 0 |
bunrouter | 0 | 0 | 0 | 0 |
httptreemux | 328 | 328 | 328 | 328 |
beegomux | 32 | 32 | 32 | 32 |
gorillamux | 720 | 720 | 720 | 720 |
bon | 0 | 0 | 0 | 0 |
denco | 0 | 0 | 0 | 0 |
echo | 0 | 0 | 0 | 0 |
gocraftweb | 288 | 288 | 352 | 432 |
gorouter | 0 | 0 | 0 | 0 |
ozzorouting | 0 | 0 | 0 | 0 |
techbook13-sample | 304 | 308 | 432 | 872 |
allocs | static-routes-root | static-routes-1 | static-routes-5 | static-routes-10 |
---|---|---|---|---|
servemux | 0 | 0 | 0 | 0 |
goblin | 0 | 1 | 1 | 1 |
httprouter | 0 | 0 | 0 | 0 |
chi | 2 | 2 | 2 | 2 |
gin | 0 | 0 | 0 | 0 |
bunrouter | 0 | 0 | 0 | 0 |
httptreemux | 3 | 3 | 3 | 3 |
beegomux | 1 | 1 | 1 | 1 |
gorillamux | 7 | 7 | 7 | 7 |
bon | 0 | 0 | 0 | 0 |
denco | 0 | 0 | 0 | 0 |
echo | 0 | 0 | 0 | 0 |
gocraftweb | 6 | 6 | 6 | 6 |
gorouter | 0 | 0 | 0 | 0 |
ozzorouting | 0 | 0 | 0 | 0 |
techbook13-sample | 2 | 3 | 11 | 21 |
パスパラメータを使ったルートについては、パラメータの数が増えるに従って性能が大きく劣化していくものと、控えめに劣化していくグループに別れた。
time | pathparam-routes-1 | pathparam-routes-5 | pathparam-routes-10 |
---|---|---|---|
goblin | 1802690 | 492392 | 252274 |
httprouter | 25775940 | 10057874 | 6060843 |
chi | 4337922 | 2687157 | 1772881 |
gin | 29479381 | 15714673 | 9586220 |
bunrouter | 37098772 | 8479642 | 3747968 |
httptreemux | 2610324 | 1550306 | 706356 |
beegomux | 3177818 | 797472 | 343969 |
gorillamux | 1364386 | 470180 | 223627 |
bon | 6639216 | 4486780 | 3285571 |
denco | 20093167 | 8503317 | 4988640 |
echo | 30667137 | 12028713 | 6721176 |
gocraftweb | 921375 | 734821 | 466641 |
gorouter | 4678617 | 3038450 | 2136946 |
ozzorouting | 27126000 | 12228037 | 7923040 |
techbook13-sample | 3019774 | 917042 | 522897 |
nsop | pathparam-routes-1 | pathparam-routes-5 | pathparam-routes-10 |
---|---|---|---|
goblin | 652.4 | 2341 | 4504 |
httprouter | 45.73 | 117.4 | 204.2 |
chi | 276.4 | 442.8 | 677.6 |
gin | 40.21 | 76.39 | 124.3 |
bunrouter | 32.52 | 141.1 | 317.2 |
httptreemux | 399.7 | 778.5 | 1518 |
beegomux | 377.2 | 1446 | 3398 |
gorillamux | 850.3 | 2423 | 5264 |
bon | 186.5 | 269.6 | 364.4 |
denco | 60.47 | 139.4 | 238.7 |
echo | 39.36 | 99.6 | 175.7 |
gocraftweb | 1181 | 1540 | 2280 |
gorouter | 256.4 | 393 | 557.6 |
ozzorouting | 43.66 | 99.52 | 150.4 |
techbook13-sample | 380.7 | 1154 | 2150 |
bop | pathparam-routes-1 | pathparam-routes-5 | pathparam-routes-10 |
---|---|---|---|
goblin | 409 | 962 | 1608 |
httprouter | 32 | 160 | 320 |
chi | 304 | 304 | 304 |
gin | 0 | 0 | 0 |
bunrouter | 0 | 0 | 0 |
httptreemux | 680 | 904 | 1742 |
beegomux | 672 | 672 | 1254 |
gorillamux | 1024 | 1088 | 1751 |
bon | 304 | 304 | 304 |
denco | 32 | 160 | 320 |
echo | 0 | 0 | 0 |
gocraftweb | 656 | 944 | 1862 |
gorouter | 360 | 488 | 648 |
ozzorouting | 0 | 0 | 0 |
techbook13-sample | 432 | 968 | 1792 |
allocs | pathparam-routes-1 | pathparam-routes-5 | pathparam-routes-10 |
---|---|---|---|
goblin | 6 | 13 | 19 |
httprouter | 1 | 1 | 1 |
chi | 2 | 2 | 2 |
gin | 0 | 0 | 0 |
bunrouter | 0 | 0 | 0 |
httptreemux | 6 | 9 | 11 |
beegomux | 5 | 5 | 6 |
gorillamux | 8 | 8 | 9 |
bon | 2 | 2 | 2 |
denco | 1 | 1 | 1 |
echo | 0 | 0 | 0 |
gocraftweb | 9 | 12 | 14 |
gorouter | 4 | 4 | 4 |
ozzorouting | 0 | 0 | 0 |
techbook13-sample | 10 | 33 | 59 |
性能の良いHTTP Routerは、各テストケースにおいて性能劣化が少ないことが分かる。 これは実装が最適化されていることを示す明確な傾向であると思われる。
パフォーマンスの良いHTTP Routerの実装をいくつか調べると、より高度な木構造を採用していることが分かった。 例えば、Echo,gin,httprouter,bon,chiはRadix tree (Patricia trie)を、dencoはdouble arrayを採用している。
bmf-san/goblinについて、トライツリーを独自に拡張したもので、あまり最適化されておらず、他のHTTP Routerに比べて性能が低いことがよく分かった。(改善できるよう頑張る...)
一方で、性能が低いと思われるHTTP Routerの中には、多機能さが性能を落としている可能性がありそうであった。
テストケースを追加することでHTTP Routerごとの性能傾向が更に得られそうだと感じたため、時間があれば対応してみようと思う。
]]>オレオレTechnology Radarの作り方についてかく。
Technology Radarとは、ThoughtWorks社(ソフトウェア開発やコンサルティングをグローバル展開している企業。マーティン・ファウラー氏が所属している。)が発信しているソフトウェア開発における技術調査の分析レポート。
年に2回ほど更新されているレポートで、技術トレンドを知ることができる。
Archiveも見ることできる。
Technology Radarは次の4象限で構成されている。
それぞれの象限は次の4つのリングに分類される。
www.thoughtworks.com/radarのサイトでは、各々技術について評価コメントやブリップ(リング間の移動)の履歴なども見ることができる。
Technology Radarを自作する方法が用意されているため、簡単に自作することができる。
2つほど方法があるので紹介する。
radar.thoughtworks.comにて、Google Spreadsheetのリンクを入力することで生成することができる。
この方法で生成した場合はパブリックになってしまうので注意。
github.com - thoughworks/build-your-own-radarのリポジトリを利用することでRadarをセルフホスティングすることもできる。
dockerイメージも用意されているのでdockerイメージを使って触ってみる方法を紹介する。
オリジナルのリポジトリをforkするなりcloneするなりしてもよいが、試しやすいようにリポジトリを用意した。
github.com - bmf-san/technology-radar-boilerplate
github.com - bmf-san/technology-radar-boilerplate をクローン
make run
http://localhost:8080
にアクセス、http://localhost:8080/files/radar.json
と入力して、Build My Radar
を押下。
Radarが生成されると次のようなリンクに飛ぶ。
http://localhost:8080/?sheetId=http%3A%2F%2Flocalhost%3A8080%2Ffiles%2Fradar.json
./files/radar.json
を編集することでRadarに表示するコンテンツを調整することができる。
(本当はjsonファイルをプロビジョンできるようにしたかったがフロントエンドのビルドの都合で厳しそうだった。。。)
Radarは右上のPrint this radar
から印刷することもできる。
組織やチームにおいて採用している技術スタックや検証した技術を内外に公表できる形でこういったRadarを作成する試みは良さそう。 技術選定理由や選定・評価のプロセスを定義するといったことに活用できたり、技術のポートフォリオとして何に投資するのかといった考えを明らかにすることに役立ちだと思った。
www.oreilly.co.jp - ソフトウェアアーキテクチャの基礎にも書いてあったが、あとは個人としてもこういうRadarを作成して定期的に更新するといった取り組みも良さそうだと感じた。自分が追っている技術をマッピングしてみたらいかに視野が狭いかということを気付かされそうだが・・
]]>自作HTTP Routerのgoblinのパフォーマンス改善をしよう思った際に、Goのパフォーマンス改善について取り組んでみたので、その際のアプローチと実践した取り組みについて書く。
より奥深いチューニングをする上ではもっと必要な知識があると思うが、最低限必要なことだけリストアップ。
前提として、パフォーマンスを改善する必要性がある(可読性を犠牲にする価値があるか、そもそもアプリケーションがボトルネックだと断定できているのか、など改善すべき理由があるか)かどうかという部分があるが、必要性があるという前提のもとで話を進める。
コードのパフォーマンスを改善する方法として、
などいくつか思い浮かぶことがあるが、改善策を講じる前に計測や分析を行う。 (計測よりもそもそもパフォーマンス改善が必要性というのが前提にあるが各々のニーズによるのでここでは触れない。)
Goで計測や分析を行うパッケージやツールの紹介をする。
Goではコードのベンチマークを取得するためのBenchmarksが標準パッケージであるtestingに含まれている。
例えば次のようなコードをgo test -bench=. -benchmem
というコマンドで実行するとベンチマークを取得することできる。
package main
import (
"math/rand"
"testing"
)
func BenchmarkRandIn(b *testing.B) {
for i := 0; i < b.N; i++ { // b.Nはベンチマークが信頼できる回数を自動的に指定する
rand.Int() // 計測したい関数
}
}
出力結果は次のような形になる。
goos: darwin
goarch: amd64
pkg: bmf-san/go-perfomance-tips
cpu: VirtualApple @ 2.50GHz
BenchmarkRandIn-8 87550500 13.53 ns/op 0 B/op 0 allocs/op
PASS
ok bmf-san/go-perfomance-tips 1.381s
ここから読み取れるベンチマークの結果は次の通り。
Goではこのように簡単にベンチマークを取得することができる。
その他のGoのベンチマークの機能についてはドキュメント参照。 Benchmarks
ベンチマークの結果を比較するツールとしてbenchstatというパッケージが使うと、ベンチマークの結果がどれくらい改善されたか割合を表示してくれるので良い。
自分が管理しているbmf-san/goblinではCIに組み込んでコミット前後の結果を比較できるようにしている。
// これは何も改善されていない例だが・・
go test -bench . -benchmem -count 1 > new.out
benchstat old.out new.out
name old time/op new time/op delta
Static1-36 248ns ± 0% 246ns ± 0% ~ (p=1.000 n=1+1)
Static5-36 502ns ± 0% 495ns ± 0% ~ (p=1.000 n=1+1)
Static10-36 874ns ± 0% 881ns ± 0% ~ (p=1.000 n=1+1)
WildCard1-36 1.60µs ± 0% 1.50µs ± 0% ~ (p=1.000 n=1+1)
WildCard5-36 4.49µs ± 0% 4.92µs ± 0% ~ (p=1.000 n=1+1)
WildCard10-36 7.68µs ± 0% 7.58µs ± 0% ~ (p=1.000 n=1+1)
Regexp1-36 1.38µs ± 0% 1.48µs ± 0% ~ (p=1.000 n=1+1)
Regexp5-36 4.30µs ± 0% 4.11µs ± 0% ~ (p=1.000 n=1+1)
Regexp10-36 7.66µs ± 0% 7.18µs ± 0% ~ (p=1.000 n=1+1)
パフォーマンス劣化を絶対に許さない!みたいな場合はCIをFailさせるような仕組みにすると良いかもしれない。
このようなベンチマークの結果を見て、実際のメモリ割り当ての様子を確認したい場合には、buildオプションを指定してビルドすることで確認することできる。 -gcflagsに指定する-mの数を増やすとより詳細な結果が得られる。
package main
import "fmt"
// Run build with go build -o example -gcflags '-m' gcflagsexample.go
func main() {
a := "hello"
b := "world"
fmt.Println(a + b)
}
go build -o example -gcflags '-m' gcflagsexample.go
と実行すると次のような出力が得られる。
# command-line-arguments
./gcflagsexample.go:9:13: inlining call to fmt.Println
./gcflagsexample.go:9:13: ... argument does not escape
./gcflagsexample.go:9:16: a + b escapes to heap
./gcflagsexample.go:9:16: a + b escapes to heap
これは単純な例なので一目瞭然だが、このようにしてヒープへの割当を特定し、ヒープ割当を減らすことによりメモリアロケーションを改善することができるため、分析の方法としても有用である。
関数レベルでどこにボトルネックがあるかというのを分析するためのツールとしてGoにはpprofというツールがある。
package main
import (
"sort"
"testing"
)
func sortAlphabetically() {
s := []string{"abc", "aba", "cba", "acb"}
sort.Strings(s)
}
func BenchmarkSortAlphabetically(b *testing.B) {
for i := 0; i < b.N; i++ {
sortAlphabetically()
}
}
CPUのプロファイルがみたいとき以下を実行。
go test -test.bench=BenchmarkSortAlphabetically -cpuprofile cpu.out && go tool pprof -http=":8888" cpu.out
メモリのプロファイルがみたいときは以下を実行。
go test -test.bench=BenchmarkSortAlphabetically profilingexample_test.go -memprofile mem.out && go tool pprof -http=":8889" mem.out
pprofのUIを活用することでどこの処理にボトルネックがあるか特定しやすくなる。
自作HTTP Routerのgoblinの改善例を上げる。
題材としているPRはこちら。 Reduce the memory allocation by refactoring explodePath method #68
goblinはトライ木をベースとしたnet/httpのインターフェースと相性の良いHTTP Routerである。
機能としては、ルーティングに必要と思われる最低限のものは持っている。 cf. goblin#features
まずはパフォーマンスを計測するためにベンチマークテストを実行する。
go test -bench=. -cpu=1 -benchmem
ベンチマークテストは、静的なルーティング(ex. /foo/bar)、動的なルーティング(ex. /foo/:bar)、正規表現を使ったルーティング(ex. /foo/:bar[^\d+$])のテストケースをそれぞれ3パターンほど用意している。
ルーティングの処理として、
といった流れになるが、このテストケースでは後者のみを計測するようになっている。
出力結果は以下の通り。
goos: darwin
goarch: amd64
pkg: github.com/bmf-san/goblin
cpu: VirtualApple @ 2.50GHz
BenchmarkStatic1 5072353 240.1 ns/op 128 B/op 4 allocs/op
BenchmarkStatic5 2491546 490.0 ns/op 384 B/op 6 allocs/op
BenchmarkStatic10 1653658 729.6 ns/op 720 B/op 7 allocs/op
BenchmarkWildCard1 1602606 747.3 ns/op 456 B/op 9 allocs/op
BenchmarkWildCard5 435784 2716 ns/op 1016 B/op 23 allocs/op
BenchmarkWildCard10 246729 5033 ns/op 1680 B/op 35 allocs/op
BenchmarkRegexp1 1647258 733.2 ns/op 456 B/op 9 allocs/op
BenchmarkRegexp5 456652 2641 ns/op 1016 B/op 23 allocs/op
BenchmarkRegexp10 251998 4780 ns/op 1680 B/op 35 allocs/op
PASS
ok github.com/bmf-san/goblin 14.304s
実行回数、1回あたりの実行回数、実行ごとのメモリサイズ、メモリアローケーション回数のそれぞれにいくつか傾向が読み取れる。
静的なルーティングであってもメモリアローケーションが発生しているのが個人的には気になるところである。(他のHTTP Routerのベンチマークを見ると0 allocsだったりする。)
次にpprofを使ってプロファイルを取得する。
今回はメモリだけにフォーカスしてプロファイルを取得。
go test -bench . -memprofile mem.out && go tool pprof -http=":8889" mem.out
Graphの出力結果。
ボックスが一番大きい(メモリを一番使っている)処理がexplodePath
だと分かる。
Top(実行時間の長い順のリスト)を見てもexplodePath
が最上位にいる。
Flatは関数の処理時間、Cumは待ち時間も含めた処理時間となる。
さらにSourceを実際に関数内のどのあたりの処理が重いかの確認。
Search
はルーターのマッチング処理を担う根幹の処理なので、そこが一番ネックだろうとは思っていたが、その中の特定の処理であるexplodePath
がネックになっているということが分かった。
explodePath
は受け取った文字列を/
で分割して[]string型にして返すという処理になっている。
// explodePath removes an empty value in slice.
func explodePath(path string) []string {
s := strings.Split(path, pathDelimiter)
var r []string
for _, str := range s {
if str != "" {
r = append(r, str)
}
}
return r
}
仕様が分かりやすいようにテストコードも記載。
func TestExplodePath(t *testing.T) {
cases := []struct {
actual []string
expected []string
}{
{
actual: explodePath(""),
expected: nil,
},
{
actual: explodePath("/"),
expected: nil,
},
{
actual: explodePath("//"),
expected: nil,
},
{
actual: explodePath("///"),
expected: nil,
},
{
actual: explodePath("/foo"),
expected: []string{"foo"},
},
{
actual: explodePath("/foo/bar"),
expected: []string{"foo", "bar"},
},
{
actual: explodePath("/foo/bar/baz"),
expected: []string{"foo", "bar", "baz"},
},
{
actual: explodePath("/foo/bar/baz/"),
expected: []string{"foo", "bar", "baz"},
},
}
for _, c := range cases {
if !reflect.DeepEqual(c.actual, c.expected) {
t.Errorf("actual:%v expected:%v", c.actual, c.expected)
}
}
}
[]string型で定義されている変数r
は容量が定義されていないため、メモリ効率が悪そうなことが推測される。
以下は検証用に用意したsliceにappendを追加するベンチマークテストとその結果。
package main
import "testing"
func BenchmarkSliceLen0Cap0(b *testing.B) {
var s []int
b.StartTimer()
for i := 0; i < b.N; i++ {
s = append(s, i)
}
b.StopTimer()
}
func BenchmarkSliceLenN(b *testing.B) {
var s = make([]int, b.N)
b.StartTimer()
for i := 0; i < b.N; i++ {
s = append(s, i)
}
b.StopTimer()
}
func BenchmarkSliceLen0CapN(b *testing.B) {
var s = make([]int, 0, b.N)
b.StartTimer()
for i := 0; i < b.N; i++ {
s = append(s, i)
}
b.StopTimer()
}
goos: darwin
goarch: amd64
pkg: example.com
cpu: VirtualApple @ 2.50GHz
BenchmarkSliceLen0Cap0 100000000 11.88 ns/op 45 B/op 0 allocs/op
BenchmarkSliceLenN 78467056 23.60 ns/op 65 B/op 0 allocs/op
BenchmarkSliceLen0CapN 616491007 5.057 ns/op 8 B/op 0 allocs/op
PASS
ok example.com 6.898s
bmf@bmfnoMacBook-Air:~/Desktop$
この結果から、容量を指定してあげることでいくらか効率の良いコードになりそうなことが伺える。
そこでexplodePath
を次のように修正。
func explodePath(path string) []string {
s := strings.Split(path, "/")
// var r []string
r := make([]string, 0, strings.Count(path, "/")) // 容量を指定
for _, str := range s {
if str != "" {
r = append(r, str)
}
}
return r
}
もう少し踏み込んで実装を改善。
func explodePath(path string) []string {
splitFn := func(c rune) bool {
return c == '/'
}
return strings.FieldsFunc(path, splitFn)
}
元のexplodePath
の実装、sliceの容量を確保した実装、strings.FieldFunc
を利用した実装の3パターンでベンチマークを比較してみる。
package main
import (
"strings"
"testing"
)
func explodePath(path string) []string {
s := strings.Split(path, "/")
var r []string
for _, str := range s {
if str != "" {
r = append(r, str)
}
}
return r
}
func explodePathCap(path string) []string {
s := strings.Split(path, "/")
r := make([]string, 0, strings.Count(path, "/"))
for _, str := range s {
if str != "" {
r = append(r, str)
}
}
return r
}
func explodePathFieldsFunc(path string) []string {
splitFn := func(c rune) bool {
return c == '/'
}
return strings.FieldsFunc(path, splitFn)
}
func BenchmarkExplodePath(b *testing.B) {
paths := []string{"", "/", "///", "/foo", "/foo/bar", "/foo/bar/baz"}
b.StartTimer()
for i := 0; i < b.N; i++ {
for _, v := range paths {
explodePath(v)
}
}
b.StopTimer()
}
func BenchmarkExplodePathCap(b *testing.B) {
paths := []string{"", "/", "///", "/foo", "/foo/bar", "/foo/bar/baz"}
b.StartTimer()
for i := 0; i < b.N; i++ {
for _, v := range paths {
explodePathCap(v)
}
}
b.StopTimer()
}
func BenchmarkExplodePathFieldsFunc(b *testing.B) {
paths := []string{"", "/", "///", "/foo", "/foo/bar", "/foo/bar/baz"}
b.StartTimer()
for i := 0; i < b.N; i++ {
for _, v := range paths {
explodePathFieldsFunc(v)
}
}
b.StopTimer()
}
goos: darwin
goarch: amd64
pkg: example.com
cpu: VirtualApple @ 2.50GHz
BenchmarkExplodePath 1690340 722.2 ns/op 432 B/op 12 allocs/op
BenchmarkExplodePathCap 1622161 729.5 ns/op 416 B/op 11 allocs/op
BenchmarkExplodePathFieldsFunc 4948364 239.5 ns/op 96 B/op 3 allocs/op
PASS
ok example.com 5.685s
strings.PathFieldFunc
を使った実装が一番パフォーマンスが良さそうなので採用。
explodePath
の実装を改善した後の結果を確認してみる。
# 改善前
goos: darwin
goarch: amd64
pkg: github.com/bmf-san/goblin
cpu: VirtualApple @ 2.50GHz
BenchmarkStatic1 5072353 240.1 ns/op 128 B/op 4 allocs/op
BenchmarkStatic5 2491546 490.0 ns/op 384 B/op 6 allocs/op
BenchmarkStatic10 1653658 729.6 ns/op 720 B/op 7 allocs/op
BenchmarkWildCard1 1602606 747.3 ns/op 456 B/op 9 allocs/op
BenchmarkWildCard5 435784 2716 ns/op 1016 B/op 23 allocs/op
BenchmarkWildCard10 246729 5033 ns/op 1680 B/op 35 allocs/op
BenchmarkRegexp1 1647258 733.2 ns/op 456 B/op 9 allocs/op
BenchmarkRegexp5 456652 2641 ns/op 1016 B/op 23 allocs/op
BenchmarkRegexp10 251998 4780 ns/op 1680 B/op 35 allocs/op
PASS
ok github.com/bmf-san/goblin 14.304s
# 改善後
go test -bench=. -cpu=1 -benchmem -count=1
goos: darwin
goarch: amd64
pkg: github.com/bmf-san/goblin
cpu: VirtualApple @ 2.50GHz
BenchmarkStatic1 10310658 117.7 ns/op 32 B/op 1 allocs/op
BenchmarkStatic5 4774347 258.1 ns/op 96 B/op 1 allocs/op
BenchmarkStatic10 2816960 435.8 ns/op 176 B/op 1 allocs/op
BenchmarkWildCard1 1867770 653.4 ns/op 384 B/op 6 allocs/op
BenchmarkWildCard5 496778 2484 ns/op 928 B/op 13 allocs/op
BenchmarkWildCard10 258508 4538 ns/op 1560 B/op 19 allocs/op
BenchmarkRegexp1 1978704 608.4 ns/op 384 B/op 6 allocs/op
BenchmarkRegexp5 519240 2394 ns/op 928 B/op 13 allocs/op
BenchmarkRegexp10 280741 4309 ns/op 1560 B/op 19 allocs/op
PASS
ok github.com/bmf-san/goblin 13.666s
改善前後を比較するに全体的に改善された傾向にあると言えそう。
pprofのGraph。
pprofのTop。
ボトルネックがexplodePath
内で呼び出しているstrings.FieldsFunc
に移動したのが分かる。
goblinに他にも改善を加えていってリリースされたタグがこちら。 6.0.0
データ構造やアルゴリズムの大きな改善をしていない、いわば小手先の改善ではあるので目を見張るほどの改善は残念ながら見られない。
なんとなく今採用しているデータ構造やアルゴリズムだとやはり難しいのだろうなという感じがする。(他所のルーター見ているともっと高度な木を採用しているのでそれはそうだという気がするが・・)
本題とはややずれるが、他のルーターとの比較をして改善のヒントが得られないかと思ってベンチマーカーを作成した。
比較してみると面白くて、ボロ負けなのがよく分かる。泣いた。
他ルーターの実装を研究したり、以前挫折した高度な木構造についての勉強等やって改善につなげていきたい。
最近スクラムチームのプロセス改善について頻繁に思いを馳せており、カイゼンのヒントが何か得られないだろうかと思ってアジャイルメトリクスを読んでみた。
原著は多分Agile Metrics in Action。何年か前に出版された本。
アジャイルメトリクスとは、アジャイルなチームのパフォーマンスを測定するためのメトリクスのこと。
メトリクスは、チームにとってのパフォーマンスの定義に関連するデータをチームのプロセスから収集する。具体的にはチームの活動で利用しているプロジェクト管理、ソースコード管理、ビルドシステム、監視などのツールが持っているデータをソースとして収集するといった感じ。
アジャイルメトリクスはパフォーマンスの改善を目的とするとものであって、Weponaize(チームや個人を非難するために活用すること)やGaming(メトリクスを改善することだけに集中してしまうこと)といった活用は推奨されない。
メトリクスからはチームの状況や起きている変化を観察し、改善につなげていくという学習を繰り返すことが重要になる。
アジャイルメトリクスでは次のような問への答えがヒントが語られていた。
チームのパフォーマンスを計測するには、チームのパフォーマンスの何を計測するのか?チームにとって良いパフォーマンスとは何か?など因数分解して考えることが大事なことで、まず最初に取り組むべきことだと思った。
アクションにつながるようなメトリクスを定義するには、データからどんなインサイトが得られそうか?というある程度の予想を持つ必要がありそう。システムの監視と違ってアジャイルのメトリクスで一番難しいなと思う部分。経験から学ぶべき?これについてはもっと事例とか知りたいところ。
単一のデータからでも分析の仕方次第で色々見えるのだろうが、あちこちからデータを収集するとなるとやはりその基盤を用意するのは面倒だなと再三思った。。。便利なサービスがあることにはあるみたいだが・・。
]]>リソースやコンポーネント、データの共有を志向している、ESBという存在があるというのがSOAの大きな特徴だと感じた。
]]>Homebrewでインストールするパッケージで過去のバージョンを指定してインストールしたいときがたまにある。 Homebrewは最新版のみ保持する方針になったらしく、過去バージョンをインストールするときはひと手間かかったのでメモ。
今回vim9系からvim8系のダウングレードをしたかったので、そのときの手順を例に上げる。
手順は以下。
brew tap-new bmf-san/vim8
brew extract vim bmf-san/vim8 --version 8.2.5150
brew install bmf-san/vim8/vim@8.2.5150
brew unlink vim
brew link vim@8.2.5150
vim --version // 8.2.5150になっている
自分でtap用のリポジトリを用意する必要がある。 githubのリポジトリでも良いが、tap-newというコマンドを使うこともできるのでtap-newを利用。名前は任意。
brew tap-new bmf-san/vim8
古いformulaをtapに展開するために、extractする。 このとき指定するバージョンは何でも良いのかもしれないが、過去homebrewで管理していたバージョンを取ってくるのが無難な気がするので、homebrewのリポジトリがほしいバージョンのコミットを漁ってきた。 漁ってきたコミットは以下。
github.com/Homebrew/homebrew-core/blob/2e3d51340f2f9b47d680f656e712fbee77cbcf79/Formula/vim.rb
brew extract vim bmf-san/vim8 --version 8.2.5150
tapからインストールして、シンボリックリンクをlink&unlinkで調整。
brew install bmf-san/vim8/vim@8.2.5150
brew unlink vim
brew link vim@8.2.5150
vimをダウングレードしたかったのはvim-lspがどうやら9系で動作しないような雰囲気を感じたため...
ADR(Architecture Decision Record)について調べた。
2011年にMichael Nygardによって紹介されたアーキテクチャに関する決定事項を記録したドキュメントのこと。 cf. cognitect.com - DOCUMENTING ARCHITECTURE DECISIONS
Michael Nygardは提案するフォーマットは次の通り。 cf. cognitect.com - DOCUMENTING ARCHITECTURE DECISIONS
ドキュメントは1~2ページ程度の読みやすい長さにする。
などアーキテクチャの何かしらの判断・決定をする際はADRを書く機会がある。
Design Docsについて調べてみた。
Design Docsはソフトウェア設計のためのドキュメント。
決まった形式を持たず、プロジェクトにとって意味ある形で書くことをルールとしている
Design Docsは開発プロセスにおいて、以下のようないくつかのメリットを持つ。
特定の形式は持たないが、設計のコンテキストやスコープ、目標や非目標を明確にすることが推奨される。
ドキュメントの長さについては、忙しい人でも手短に読める程度の長さが推奨される。
Design Docsを書くべきかどうかは、Design Docsを書くメリットがDesign Docsを運用するコストよりも上回るかどうかが基準となる。
Design Docsは次のようなライフサイクルを持っている。
Design Docsの例には例えば次のようなものがある。
SLI・SLO・SLAについて色々調べてみたことをまとめる。
SLO、SLI、SLAとは、サービスレベル(Service Level)に関わる指標、目標、合意のことである。 サービスレベルとは一定の期間内で提供されたサービスを特定の方法で測定して表したものである。
NewRelicが提唱しているベストプラクティスが取り組みやすくて良いと思う。
newrelic.com - モダンなシステムにSLI/SLOを設定するときのベストプラクティス
システム境界を定義、境界ごとの機能定義、機能ごとの可用性の定義、可用性計測のためのSLI定義といった感じでSLI・SLOを策定する方法が紹介されている。
SLI・SLOの運用を始めるときは、なるべくシンプルに、緩めの値で運用を開始していくというのが推奨される。
cf. sre.google - Chapter 4 - Service Level Objectives
実際に自分が業務でSLI・SLOを策定したときは、このNewRelicのプラクティスに従ったが、機能単位のところは調整して余り細かくならないようにした。
機能の単位を最初から細かくしてしまうと運用が大変になってしまうので、運用していく中で適宜必要に応じて粒度を調整していくのが良いのではないかと思う。
SLI・SLOに関連するキーワードについてのTips。
稼働率 | 年間停止時間 | 月間停止時間 |
---|---|---|
99.0% | 87.6時間 | 7.6時間 |
99.5% | 43.8時間 | 3.65時間 |
99.9% | 8.76時間 | 43.8分 |
99.95% | 4.38時間 | 21.9分 |
99.99% | 52.56秒 | 4.38分 |
99.999% | 5.256秒 | 26.28秒 |
99.9999% | 31.536秒 | 2.628秒 |
エラーに対する予算で、SLOを基準として算出される許容可能な信頼性の指標のこと。 ex. SLO 99.99% → エラーバジェット 0.01%以下
サービスレベルを測定可能にすることで、サービス利用者(ユーザーあるいはシステム)がサービスを満足に提供できるているかどうか観測可能になり、またサービス提供者にとってサービスレベルの改善が必要かどうかの指標になり得ると思った。
先日認定スクラムマスターの研修を受けたのだが、スクラムマスターとプロダクトオーナーの違いについて改めて整理しておく必要があると感じたので、記事にする。
スクラムマスターはプロセスに関心(≒How)を持つのに対し、プロダクトオーナーはプロダクトの価値とビジョン(WhatとWhy)に関心を持つ。
スクラムを実践する上で、スクラムを理解し、チームに布教し、プロセス改善を行うのがスクラムマスターであり、プロダクトの価値を最大化させるための意思決定を行うのプロダクトオーナーである。
個人的な主観として、スクラムマスターとプロダクトオーナーを兼任することは推奨されないが、スクラムマスターと開発者を兼任することは許容されるような雰囲気を感じるが、それぞれの立場の責任を十分に発揮できるかというと厳しい気はする。 スクラムマスターを専任でおけるほどのリソースがないような場合において、スクラムマスター(専任。スクラムマスターとしての責任を持つ。複数チームのスクラムマスターを兼任している。)+開発者兼スクラムマスター(スクラムマスターを補佐するリーダー的な立場)といった形はどうだろうか(自分が経験している環境に近かったりする)。
GoでClean Architectureのレイヤーを静的解析する方法についてのメモ。
静的解析のツールを自作しても良かったが、簡単に導入できるツールがあったのでこちらを使ってみた。
自作CMSのgobel-apiに導入してみた。 cf. PR
go install github.com/roblaszczak/go-cleanarch@latest
でインストール。
レイヤーのネーミングがデフォルトと異なるので、オプションをつけてチェック実行。
go-cleanarch -application usecase
レイヤーの依存関係に違反するとチェックに引っかかってエラーになる。
エラーになると、Uncle Bob is not happy.
と怒られる。
そのうち自作したいと思っているが当分はこのツールにお世話になりたい。
アプリケーションの設計、構造を保守していくためにこういった静的解析ツールは早い段階から導入すべきだと思った。
Goなら静的解析のツールが実装しやすいと思うので、Clean Architectureに限らず、特定のレイヤーの構造を維持していきたいのであれば積極的にこういったツールを自作していくのが良さそう。
]]>Cloud Functionsを使ってSlack AppのSlash Commandを実装する。
今回作ったボイラープレートはこちら。
go-slack-app-on-cloud-functions-boilerplate
Slash Commandの使えるSlack Appを作る方法は色々あるが、安く、簡単に、サーバーレスで作れるということでCloudFront Functionsを使ってみた。
Cloud Functionsのコンソールにて、関数を作成しておく。 トリガータイプはHTTP、認証は未認証の呼び出しを許可を選択、HTTPSが必須をチェックする。
あとで関数をデプロイした後に、Cloud Functionsの関数の詳細 > トリガーに記載されているトリガーURLを使うので、メモしておく。
Create an appにて、From scratchを押下する。
App Nameを入力する。
Pick a workspace to develop your app in: にてワークスペースを選択して、 Create Appを押下する。
設定画面(ex. https://api.slack.com/app/****) にて、Slash Commandsを選択する。
Create New Commandを押下し、Command、Short Description、Usage Hint、Escape channels, users, and links sent to your appを任意で入力する。
Request URLは先程メモしたトリガーURLを入力する。 トリガーURLはhttps://REGION-NAME-PROJECT-ID.cloudfunctions.net/FUNCTION_NAMEという形式になっている。
入力できたらSaveを押下する。
設定画面(ex. https://api.slack.com/apps/****) にて、Install Appを押下する。
Install to workspaceを押下して、任意のワークスペースにAppwoインストールする。
設定画面(ex. https://api.slack.com/apps/****) にて、Basic Infomationを押下する。
App Credentialsという項目にSigning Secretがあるので、値をメモしておく。
Cloud Functionsにデプロイする関数を実装する。
若干ハマりどころ(go mod vendorする部分とか)はあったりするが、実装詳細は割愛する。
ソースコードは以下参照。
go-slack-app-on-cloud-functions-boilerplate
go-slack-app-on-cloud-functions-boilerplateのREADMEに従って、環境変数の準備とデプロイを実施する。
作成したSlash CommandをSlackで使ってみる。
ex. /hello Bob
Slack Appを作る部分をコード化できると嬉しい。
仕事で勤怠管理のためのSlack Commandを作りたかったのでgo-slack-app-on-cloud-functions-boilerplateをベースに、akashi-slack-slash-commandというのを作ってみた。
この実装では、ストレージをSpreadsheetにしているが、Google Workspaceを使っていると権限周りでSpreadsheetの共有設定が柔軟に調整できないという管理上の問題があって、職場ではストレージをSpreadsheetからCloud Storageに差し替えて実装を調整して使っている。
勤怠管理でAkashi、チャットツールでSlackを使っている組織があれば簡単に利用できるSlack Commandになっていると思う。
運用コストも大してかからず、スケーラビリティはちょっと怪しいかも。
数千人超えるような組織でなければ多分問題なく使えると思う。多分。
]]>マークダウン形式のファイルをPDFファイルに変換したいという要望に応えるための簡易的なドキュメント管理ツールを作った。
bmf-san/docs-md-to-pdf-example
特に深く考えることなくあり物のライブラリを活用して作ったので、あまりサステナビリティを感じない構成になっている。
単にマークダウン形式のファイルをPDFファイルに変換するだけであれば、md-to-pdfというライブラリを使うだけで良い。
このライブラリは、レジュメで管理でもお世話になっている。 cf. Githubでレジュメを管理するようにした
mermaid記法の対応やunicodeに登録されている絵文字以外の絵文字を使いたかったりという希望があったので、それに対応する形のものを作りたかった。
vscodeの拡張であるvscode-markdown-pdfを使えば簡単に解決することができるのだが、vscodeが必要になるので、人によってはvscodeのインストールが必要になってしまう。
変換のためだけにvscodeを使うというのはナンセンスだと思ったので実装してみた。
md-to-pdfというライブラリは使いやすく素晴らしいライブラリなのだが、現状以下の機能が標準でサポートされていない。
md-to-pdfはmarkedjs/markedの設定拡張が可能であるため、どれもmd-to-pdfをカスタマイズすることで実現可能そうではある。
TOCについてはサポートされる予定があるらしい。 Generate TOC (table of contents) #74
md-to-pdfを使うでも良かったが、やや手間がかかりそうだったので、なるべくハッカソンのような感じで手短に実装したかったので、md-to-pdf-ngというライブラリを使うことにした。
これはmd-to-pdfを拡張してmermaid記法に対応させたライブラリで、あまりメンテナンスされていないようではあるが、一応問題なく使用できる。
md-to-pdf-ngをベースに、emoji対応はnode-emojifyを、TOC生成はdoctocというライブラリを使って実現する形とした。
以下をnpmでインストール。
※おまけでtextlintを入れているがそのへんは割愛。
emoji対応はmarkedを拡張するような形で対応するので、次のような設定ファイルを用意。
const marked = require('marked');
const { emojify } = require('node-emoji');
const renderer = new marked.Renderer();
renderer.text = emojify;
module.exports = {
marked_options: { renderer },
};
package.jsonのscriptsに次のようなコマンドを定義。
doctoc --notitle md/ && md-to-pdf md/*.md --config-file config.js && mv md/*.pdf pdf/
まずdoctocでTOCを生成、次にマークダウンをPDFに変換、最後にディレクトリを移動といった感じ。
md-to-pdfの生成したpdfのアウトプット先をディレクトリ単位で指定できると良いのだがそういうオプションはなさそうだったので、mv md/*.pdf pdf/
という安直な方法で対応している。
この手のものを作ろうとするとやはり結構外部のライブラリに依存しがちになってしまう。 できれば自分で全て実装したいがかなり大変そうに思う。 そのうち機会があればPDFのデータ構造を学んだり、Goで似たようなCLIツールを作ったりしてみたい。
]]>ちなみに認定期間が1年らしいので、認定を継続するには年に1回小テストを受けて更新する必要があるらしい。
普段、チームでスクラム開発を行っているが、開発者(というかチームリーダー)としてスクラムについて理解を深めて、チームのプロセス改善とプロダクトの価値向上に貢献したかった。
スクラムについての知識は、スクラムガイドやSCRUM BOOT CAMP THE BOOKを読んだりしたくらいで、後は実践の中でスクラムマスターから学びを得たくらい(経験主義というやつ?)。
自分の所属するチームのスクラムの運用自体は結構上手くいっている方だと思うのだが、改善できるポイントを改善して更に成果を貪欲に求めていくことができればと考えている。
スクラムマスターの認定は、LSMの他、CSMとPSMというのがあるらしいが、会社で推奨してもらった研修がLSMだったのでLSMを受けた。
研修は2日間朝9時から夕方17:30まで長丁場で実施される。
内容は座学が60%、ワーク(ディスカッションなど)が40%といった形。
研修後は認定試験(30問、時間制限なし)を受験することができ、合格すると認定証がダウンロードできる。
座学は事例なども交えつつ、スクラムのエッセンスを学ぶことができる。ワークはスクラムのパターンやプロセスを実践形式で体感することができる。
普段行っているスクラムの形は間違っていないんだなぁという実感が得られた(スクラムマスターに教えてもらっているので当たり前だと思うが...)。一方で改善できそうなポイントなども見つかったので良いお土産が得られた。見積もりのパターンだったり、スウォーミングの考え方だったり、試してみたいことがいくつか発見できた。
しばらく何年かは毎年更新して、スクラムについて学びをアップデートする時間を設けていこうかと思う。
]]>このブログ(bmf-tech.com)を支える技術スタックについてかく。
まずは現行のbmf-techはよりも1世代前の構成について。
前世代運用していたアプリーケーションが初代自作CMSであるRubel。
これは何年運用したか覚えていないけど、多分3~5年くらいだと思う。
Rubelを運用する前は、Wordpressでオリジナルのテーマを作ってブログを運用していた。
Wordpress(オリジナルテーマ1)→Wordpress(オリジナルテーマ2)→Rubel→今に至るといった感じ。
bmf-tech.comのドメイン年齢を確認してみたら2015年11月2日が取得日であった。
いつからブログ運用を始めたか忘れてしまったが、ドメイン年齢に基づくなら7年近く運用していることになる。
現行のbmf-techの構成についてかく。
同じ構成でサンプルコードをgobel-exampleにて公開している。
システムをリプレースしたい理由がいくつかあった。
それらの理由から新しいシステムの設計方針をざっくり考えた。
設計方針を元に構築したアーキテクチャ構成が以下。
デプロイは特に複雑なことはしていない。
コンテナ構成に基づいてソースコード管理がどういう形になっているか示した図が以下。
API、Client(ユーザー側画面)、Admin(管理画面)それぞれのアプリケーションについて。
基本的にはRubelのDB設計をそのまま引き継いでいるが、論理・物理削除を見直して再設計した部分がいくつかある。 後はカラムのデータ型やサイズを見直したりといった感じ。
データの移行は自前のデータ移行ツールを書いて対応した。
DB設計に大きな差がなかったので移行ツールは2~3日くらいで実装することができた。
サーバー移行については、大したことはしていない。 移行先のサーバー環境を整えて、動作確認するための検証用のドメインを取得し、移行先サーバー環境で各種動作確認を行った。 作っては壊しを繰り返してIaCに問題ないかもチェックした。
リリース作業時はDNSを切り替えるだけで新環境への移行が完了。
旧環境は新環境を1ヶ月くらい運用して問題ないことを確認してから削除、契約終了の手続きをした。
監視ダッシュボードやアラートはGrafanaで作成・設定した。
監視ダッシュボードの方はjsonファイル形式でデータを管理し、プロビジョニングできるようになっているが、アラートはGrafanaのUIから設定している。
(アラートの方はまだプロビジョンニングに対応していないため cf. github.com - grafana/issues/36153 →対応されたのでプロビジョニングできるように対応した。
大してトラフィックがないのに設定するのは虚しい気もするが、トラフィックがどうこうというより、一定の可用性を安定して保つことができるか観測するという意味で設定したい。が、まだ未対応。
ちょっとやってみたいと思っているので検討中。
新bmf-techをリリースするまでに作ったものをまとめる。
上記色々作る中でブログに書いたり、LTしたり他に色々やったりしていたので、新bmf-techのリリースにはかなりの時間を使ってしまった。
偶に開発の手を止めたり、道を外れたり、何回か途中でWordpressとか別の既存システムに変えようかと、迷ったことが幾度があったが、無事運用できる形になって肩の荷が下りた。
やれていないことややりたいことは色々といくつかissueを積んでいるので、趣味程度に片手間に対応していきたい。
ただ作るのではなく、どう作るのか、どう運用するのかということに磨きをかけていきたいと思っているので、そのための投資をこのブログシステムを通じてやっていこうと考えている。
自分が自作ブログを運用している理由は学びの要素が強い。実際結構多くのことを学ぶ機会になったり、今後も更に学ぶことができそうである。
当分は現行のシステムを運用し続けていくことができると考えているので気長にやっていこうと思っている。
]]>bmf-tech.comはWordpress→Laravel製自作CMS→Go製自作CMSと3世代目のアップデートをすることができた。
振り返りの記事をそのうち書く予定。
]]>これが個人的に良い取り組みだったので記事にしておく。
ここでいうレジュメは、自分のキャリアのあれこれがテキストベースでまとめられたものを指す。
Google Driveでファイルをアップロードして管理していたが、イマイチ更新しづらかったり、更新する気力が沸かなかったりした。
差分がわかりやすいようににしたり、気軽に公開できる方法のほうがより望ましいと考えていた。
レジュメ自体をそもそもなぜ管理するかというと、理由は2つある。
1つは、”キャリアの不安に対する備え”。自分のキャリアを定期的に
棚卸しして、経験を振り返って、今後のキャリアで何を志向していくかと、自己分析に使う材料として持っておきたいため。それから自分は何ができる人なのかということを語る材料としても用意しておきたい。これは2つ目の理由にもつながる。
2つ目は、”転職への備え”。エンジニアという専門職の性質上だったり、社会や経済の情勢だったり、いついかなる時にでも転職できるように日頃から備えておく必要があると考えているため。
Githubに非公開リポジトリを作成してレジュメを管理できるようにしている。(公開リポジトリにしたいと考えているが、まだ作成したばっかで公開できる情報と非公開にしておきたい情報の選別ができていない。)
大本はgithub.com - kawamataryo/resumeを参考にさせてもらっている。
こんな感じになっている。
.
├── README.md
├── docs
│ ├── certification
│ ├── md
│ ├── pages
│ └── pdf
├── package-lock.json
└── package.json
certification
は資格関連のファイルを置いている。
md
はマークダウン形式で各種レジュメを記述している。
自分は以下のようなカテゴリーでレジュメを書いている。
このへんのカテゴリとか各ファイルの形式は転職ドラフトのレジュメを大いに参考にしている。レジュメとして必要な情報が十分カバーされていると思っている。
職務経歴書は、github上で自分が関連しているPRをCSV形式でダウンロードする自作ツールを使って用意したCSVファイルを参考にしながら記載している。
PRベースだとアウトプットが明確で自分がやったことが分かりやすいのでレジュメが書きやすい。もちろんPR以外にもアウトプットがあったりなかったりするが、そのへんは記憶頼りで頑張っている。
md形式記述した各種レジュメはpdf形式に変換して(md-to-pdf)pdf
配下にファイルが生成される。
pages
はpages形式の履歴書を保存している。これは惰性なものなのでmd形式に変更したいと思っている。
npm run textlint
npm run md-to-pdf
ざっくりこんな感じ。
日々色んな業務や雑多な個人活動をやっているとそれをレジュメに反映するのが億劫になるが、運用が改善されたので大分楽になった気がする。
]]>Cloud Spannerの知見を漁ったのでメモ。走り書きなのでカテゴライズしていない。
設計を間違うとspannerのスケーラビリティを活かせず十分なパフォーマンスが出ない。 計画メンテやスキーマ変更にダウンタイムなしは運用上の大きなメリット。 Splitの気持ちになってデータの分散を意識しないと運用時最大限のパフォーマンスを発揮できない。 日本語の記事ばかり漁ってたので海外の記事ももっと見ておいたほうが良いかも。
DDD本を輪読会で読み終わったので読書メモを残す。
N/A
N/A
]]>PHPの知識が7.3から止まっているので8.1までの新機能を駆け足でキャッチアップする。
<?php
class Person {
public int $age; // 指定の型だけ代入できるように強制される
public string $name;
}
?>
<?php
// これが
$a = 1
$func_7_3 = function($b) use ($a) {
return $a + $b;
}
echo $func_7_3(1); // 2
// 7.4からはこのように書ける
$func_7_4 = fn($b) => $a + $b; // 暗黙的な値スコープを持つ
echo $func_7_4(1); // 2
?>
<?php
// これが
$name = isset($name) $name : getName();
// 7.4からはこのように書ける
$name ??= getName();
?>
<?php
$values = ['a', 'b'];
$all = [...$values, 'c']; // a, b, c
?>
PHPから別言語を呼び出せる。
Goのcgoみたいなやつ。
opcacheにスクリプトを事前ロードする機能が追加された。
// php.ini
opcache.preload=preload.php
<?php
function namedFunc($foo, $bar, $baz) {
echo $foo . $bar . $baz;
}
// 引数の順番は関係なく、名前付きで引数をセットできる
namedFunc(baz: "baz", foo: 'foo', bar: "bar"); // bazfoobar
?>
アノテーションの新機能。
<?php
#[Attribute]
class Person
{
public $name;
public function __construct($name)
{
$this->name = $name;
}
}
#[Person(name: "John")]
class Man
{
}
// Reflection APIでアトリビュートにアクセスできる
function output($reflection) {
foreach ($reflection->getAttributes() as $attribute) {
var_dump($attribute->getName());
var_dump($attribute->getArguments());
var_dump($attribute->newInstance());
}
}
output(new ReflectionClass(Man::class));
// string(6) "Person"
// array(1) {
// 'name' =>
// string(4) "John"
// }
// class Person#3 (1) {
// public $name =>
// string(4) "John"
// }
?>
引数、戻り地、プロパティに対してunion型を宣言できるようになった。
<?php
function unionFunc(int|string $value): int|string
return $value;
}
unionFunc(1);
unionFunc("Hello World");
?>
<?php
function matchFunc($value) {
// switch文とは異なり、厳密な比較(===)となる
return match($value) {
"one" => 1,
"two" => 2,
"three" => 3,
};
};
echo matchFunc("one"); // 1
?>
null安全なコードが書きやすくなる。
<?php
class User
{
public $name;
public function __construct()
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
class Account
{
public User|null $user = null;
}
$account = new Account();
// $account->user->getName(); // PHP Fatal error: Uncaught Error: Call to a member function getName() on null
$account->user?->getName();
?>
<?php
$a1 = ["a" => 1];
$a2 = ["a" => 2];
var_dump(["a"=>0, ...$a1, ...$a2]); // ["a" => 2] キーは後勝ちになるっぽい。
?>
<?php
enum ColorCode: string
{
case BLUE = "#0000ff";
case YELLOW = "#ffff00";
case RED = "#ff0000";
}
function enumFunc(ColorCode $color) {
echo $color->name;
echo $color->value;
}
enumFunc(ColorCode::BLUE); // BLUE blue
// enumFunc("green"); // PHP Fatal error: Uncaught TypeError: enumFunc(): Argument #1 ($color) must be of type ColorCode, string given
?>
完全なスタックを持つ、停止可能な関数。コールスタックのどこからでも停止・再開が可能。
非同期処理が書ける。
unionが型のORなら、交差型は型のAND。
<?php
function check(Foo&Bar $intersection)
{
return;
}
class Foo{}
class Bar{}
class Baz{}
$foo = new Foo();
$foo->check(new Foo());
$foo->check(new Bar());
// $foo->check(new Baz()); // PHP Fatal error: Uncaught Error: Call to undefined method Foo::check()
?>
戻り値にのみ指定できる型。関数がexit()するか例外を投げるか終了しないかを示す。
voidとは違う。
どんどん型を意識したコードが書きやすくなってきている印象。
下位互換性は流し読みしかしていないのでアプデ対応のときでも再度読み直したい。
証明書の取得はDNS-01方式(ドメインのTXTレコードに認証局が発行したワンタイムトークンを登録して検証する)で取得したかったので、ConohaのAPIを使って、TXTレコードを登録、削除するようなスクリプトを組んで対応(cf. github.com - k2snow/letsencrypt-dns-conoha)していたが、スクリプトの管理が面倒だったので、もっと単純なやり方を模索していたところ、go-acme/legoというLets' Encryptのクライアントツールを見つけたので使ってみた。
legoはGo製のLets't Encryptクライアント&ACMEのライブラリ。
Conoha以外にも様々なDNS Providersが用意されている。
インストールはdockerでもパッケージマネージャーでもGoでも良い。
dockerを使う場合のコマンドはこんな感じ(Ansibleのコードそのまま持ってきた)。
docker run --rm -e CONOHA_POLLING_INTERVAL=30 -e CONOHA_PROPAGATION_TIMEOUT=3600 -e CONOHA_TTL=3600 -e CONOHA_REGION={{ conoha_region }} -e CONOHA_TENANT_ID={{ conoha_tenant_id }} -e CONOHA_API_USERNAME={{ conoha_username }} -e CONOHA_API_PASSWORD={{ conoha_password }} -v /home/{{ ssh_user_name }}/lego:/lego goacme/lego --path /lego --email {{ email }} --dns conoha --domains *.{{ domain }} --domains {{ domain }} --accept-tos run
ConohaのDNSはTXTレコードの反映が遅い?か何かあるらしく、デフォルトの設定ではtimeoutのエラーを吐くので、CONOHA_PROPAGATION_TIMEOUT
、CONOHA_PROPAGATION_TIMEOUT
、CONOHA_TTL
は設定値を上記のようにセットしたほうが良い。
スクリプトで対応していたときもDNSの挙動にハマって上手く行かないことが多かった。なぜだろう..
すごい楽。証明書の更新もlegoでOK。
]]>去年のやつ。 2020年の振り返りと来年の抱負
今年も振り返りを書いておこうと思うが、年々面倒になってきたのでざっくりにかく。
2021は自分の人生で一番出費があった年だったと思う。
あと何か色々細々としたものを買ったような気がする。
どちらかといえばケチなタイプだが、有意義な出費ができた年であった。
忙しいな!と思うことが多くなってきた(元々暇だったとかいうわけではないが・・)。
色々とやっているけど、それについてはレジュメやら公開記事やらにまかせるとする。
形に残るようなことといえば、AWS SAA取得した。2022年はSAPを取得したい。 コーディングについては、ぼちぼちGo書いていた。GoCon初参加・初登壇したりもした。 2020年に続き、2021年もISUCon出た。結果は予選落ち... インプットを意識した年だったのでTrelloを振り返るとそういうタスクがちらほらある。 アウトプットは少なかった。そういう意味だとボヤボヤした年だったかもしれない。 2021年もアプリケーション以外の領域の学びを意識する年になると思う。
「不得意を得意に」というのが2021年の抱負だった。 インフラ周りの苦手意識を軽減するという点においてはまずまずだったと思う。
今年は標語っぽい感じではなくて、具体的な感じで「何ができるのか?ということに対して明確な答えを増やす」とした。 数えで30歳を迎える年であり、次の5年、10年に向けた長期的な目線での投資が必要だなという感覚を持っており、その一手として、自分の武器を明確しようということを意識しようと考えている。
日々色々考えてきたことを1年分まとめるには時間が足りないという言い訳をしておきたい。
]]>この記事はMakuake Advent Calendar 2021の24日目の記事です。(大遅刻しました・・)
ラウンドロビンで負荷分散するロードバランサーをGolangで自作してみるという話です。
ロードバランサーはリクエストを複数のサーバーへ振り分けて負荷分散する(ロードバランシング)機能を持ったサーバーです。
サービスの可用性を高めてくれるリバースプロキシの一種です。
ロードバランサーの種類は大きく分けて2種類あります。アプリケーション層で負荷分散するL7ロードバランサーと、トランスポート層で負荷分散するL4ロードバランサーです。
ロードバランサーは、ロードバランシングの他、パーシステンス(セッション維持)とヘルスチェックの機能を兼ね備えています。
負荷分散には静的な方式と動的な方式のものでそれぞれ種類があります。
静的なものの代表的な方式としては、リクエストを均等に振り分けるRound Robinという方式があります。
動的なものの代表的な方式としては、リクエストの未処理数が最小のサーバーに振り分けるLeast Connectionという方式があります。
パーシステンスはロードバランサーの複数の振り分け先のサーバー間でセッションを維持するための機能です。
大きく分けてSource address affinity persistenceという方式とCookie persistenceという2つの方式があります。
Source address affinity persistenceは送信元IPアドレスを見て振り分け先のサーバーを固定する方式です。
Cookie persistenceはセッション維持のためのCookieを発行して、Cookieを見て振り分け先のサーバーを固定する方式です。
ヘルスチェックはロードバランサーが振り分け先のサーバーの稼働状況を確認する機能です。
ロードバランサーから振り分け先のサーバーにヘルスチェックするアクティブ型のヘルスチェック方式と、クライアントからのリクエストに対するレスポンスを監視する方式です。
アクティブチェックは利用するプロトコルによってはL3チェック、L4チェック、L7チェックといった種類に分別することができます。
L4ロードバランサーをパッケージとして実装します。
ロードバランシングの種類はラウンドロビンで、ヘルスチェックはアクティブチェック・パッシブチェックのそれぞれ対応します。
パーシステンスはの対応はしません。
今回実装したコードはgithub.com/bmf-san/godonにあります。
ロードバランサーはリバースプロキシの一種です。まずは簡単なリバースプロキシの実装から始めます。
Golangではhttputil
を利用することで簡単に実装することができます。
package godon
import (
"log"
"net/http"
"net/http/httputil"
)
func Serve() {
director := func(request *http.Request) {
request.URL.Scheme = "http"
request.URL.Host = ":8081"
}
rp := &httputil.ReverseProxy{
Director: director,
}
s := http.Server{
Addr: ":8080",
Handler: rp,
}
if err := s.ListenAndServe(); err != nil {
log.Fatal(err.Error())
}
}
ここでは説明を省きますが、pkg.go.dev/net/http/httputil#ReverseProxyをよく読んでおくと良いかと思います。
簡単なロードバランサーなので複雑な設定を持ちませんが、jsonから設定を読み込むような設定の機能を実装しておきます。
{
"proxy": {
"port": "8080"
},
"backends": [
{
"url": "http://localhost:8081/"
},
{
"url": "http://localhost:8082/"
},
{
"url": "http://localhost:8083/"
},
{
"url": "http://localhost:8084/"
}
]
}
// ...
// Config is a configuration.
type Config struct {
Proxy Proxy `json:"proxy"`
Backends []Backend `json:"backends"`
}
// Proxy is a reverse proxy, and means load balancer.
type Proxy struct {
Port string `json:"port"`
}
// Backend is servers which load balancer is transferred.
type Backend struct {
URL string `json:"url"`
IsDead bool
mu sync.RWMutex
}
var cfg Config
// Serve serves a loadbalancer.
func Serve() {
// ...
data, err := ioutil.ReadFile("./config.json")
if err != nil {
log.Fatal(err.Error())
}
json.Unmarshal(data, &cfg)
// ...
}
次にラウンドロビンの実装をします。
均等にバックエンドのサーバーにリクエストを振り分けるのみで、バックエンドのサーバーの生死は問わない形で実装します。
// ...
var mu sync.Mutex
var idx int = 0
// lbHandler is a handler for loadbalancing
func lbHandler(w http.ResponseWriter, r *http.Request) {
maxLen := len(cfg.Backends)
// Round Robin
mu.Lock()
currentBackend := cfg.Backends[idx%maxLen]
targetURL, err := url.Parse(cfg.Backends[idx%maxLen].URL)
if err != nil {
log.Fatal(err.Error())
}
idx++
mu.Unlock()
reverseProxy := httputil.NewSingleHostReverseProxy(targetURL)
reverseProxy.ServeHTTP(w, r)
}
// ...
var cfg Config
// Serve serves a loadbalancer.
func Serve() {
data, err := ioutil.ReadFile("./config.json")
if err != nil {
log.Fatal(err.Error())
}
json.Unmarshal(data, &cfg)
s := http.Server{
Addr: ":" + cfg.Proxy.Port,
Handler: http.HandlerFunc(lbHandler),
}
if err = s.ListenAndServe(); err != nil {
log.Fatal(err.Error())
}
}
sync.Mutex
を利用しているのは、複数のGoroutineが変数にアクセスすることによる競合状態を回避するためです。
試しにsync.Mutex
を外してgo run -race server.go
でサーバー起動、複数端末から同時にリクエストするとrace conditionを確認することができます。
ここまでの実装では、ロードバランサーは異常なバックエンドに対してもリクエストを転送するようなロジックとなっています。
実際のユースケースでは異常なバックエンドにわざわざリクエストを転送してほしくはないので、異常なバックエンドを検知して、振り分け先から外れるようにします。
// Backend is servers which load balancer is transferred.
type Backend struct {
URL string `json:"url"`
IsDead bool
mu sync.RWMutex
}
// SetDead updates the value of IsDead in Backend.
func (backend *Backend) SetDead(b bool) {
backend.mu.Lock()
backend.IsDead = b
backend.mu.Unlock()
}
// GetIsDead returns the value of IsDead in Backend.
func (backend *Backend) GetIsDead() bool {
backend.mu.RLock()
isAlive := backend.IsDead
backend.mu.RUnlock()
return isAlive
}
var mu sync.Mutex
var idx int = 0
// lbHandler is a handler for loadbalancing
func lbHandler(w http.ResponseWriter, r *http.Request) {
maxLen := len(cfg.Backends)
// Round Robin
mu.Lock()
currentBackend := cfg.Backends[idx%maxLen]
if currentBackend.GetIsDead() {
idx++
}
targetURL, err := url.Parse(cfg.Backends[idx%maxLen].URL)
if err != nil {
log.Fatal(err.Error())
}
idx++
mu.Unlock()
reverseProxy := httputil.NewSingleHostReverseProxy(targetURL)
reverseProxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, e error) {
// NOTE: It is better to implement retry.
log.Printf("%v is dead.", targetURL)
currentBackend.SetDead(true)
lbHandler(w, r)
}
reverseProxy.ServeHTTP(w, r)
}
var cfg Config
// Serve serves a loadbalancer.
func Serve() {
data, err := ioutil.ReadFile("./config.json")
if err != nil {
log.Fatal(err.Error())
}
json.Unmarshal(data, &cfg)
s := http.Server{
Addr: ":" + cfg.Proxy.Port,
Handler: http.HandlerFunc(lbHandler),
}
if err = s.ListenAndServe(); err != nil {
log.Fatal(err.Error())
}
}
ロードバランサーがバックエンドにリクエストを転送したときにエラーを検知すると呼び出されるErrorHandler
を実装します。ErrorHandler
では、正常にレスポンスを返さないバックエンドにフラグを立てて、もう一度ロードバランサーにリクエストを転送してもらうような形にしています。
ロードバランサーはフラグの立っているバックエンドにはリクエストを転送しないようにロジックを調整しています。
最後にパッシブチェックの実装をします。
パッシブチェックは、インターバルを指定してバックエンドサーバーのレスポンスを監視するだけです。
異常が検知されたバックエンドは、アクティブチェックのときと同じようにフラグが立てられます。
パッシブチェックを実装し終えた全てのコードは以下になります。
package godon
import (
"encoding/json"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"time"
)
// Config is a configuration.
type Config struct {
Proxy Proxy `json:"proxy"`
Backends []Backend `json:"backends"`
}
// Proxy is a reverse proxy, and means load balancer.
type Proxy struct {
Port string `json:"port"`
}
// Backend is servers which load balancer is transferred.
type Backend struct {
URL string `json:"url"`
IsDead bool
mu sync.RWMutex
}
// SetDead updates the value of IsDead in Backend.
func (backend *Backend) SetDead(b bool) {
backend.mu.Lock()
backend.IsDead = b
backend.mu.Unlock()
}
// GetIsDead returns the value of IsDead in Backend.
func (backend *Backend) GetIsDead() bool {
backend.mu.RLock()
isAlive := backend.IsDead
backend.mu.RUnlock()
return isAlive
}
var mu sync.Mutex
var idx int = 0
// lbHandler is a handler for loadbalancing
func lbHandler(w http.ResponseWriter, r *http.Request) {
maxLen := len(cfg.Backends)
// Round Robin
mu.Lock()
currentBackend := cfg.Backends[idx%maxLen]
if currentBackend.GetIsDead() {
idx++
}
targetURL, err := url.Parse(cfg.Backends[idx%maxLen].URL)
if err != nil {
log.Fatal(err.Error())
}
idx++
mu.Unlock()
reverseProxy := httputil.NewSingleHostReverseProxy(targetURL)
reverseProxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, e error) {
// NOTE: It is better to implement retry.
log.Printf("%v is dead.", targetURL)
currentBackend.SetDead(true)
lbHandler(w, r)
}
reverseProxy.ServeHTTP(w, r)
}
// pingBackend checks if the backend is alive.
func isAlive(url *url.URL) bool {
conn, err := net.DialTimeout("tcp", url.Host, time.Minute*1)
if err != nil {
log.Printf("Unreachable to %v, error:", url.Host, err.Error())
return false
}
defer conn.Close()
return true
}
// healthCheck is a function for healthcheck
func healthCheck() {
t := time.NewTicker(time.Minute * 1)
for {
select {
case <-t.C:
for _, backend := range cfg.Backends {
pingURL, err := url.Parse(backend.URL)
if err != nil {
log.Fatal(err.Error())
}
isAlive := isAlive(pingURL)
backend.SetDead(!isAlive)
msg := "ok"
if !isAlive {
msg = "dead"
}
log.Printf("%v checked %v by healthcheck", backend.URL, msg)
}
}
}
}
var cfg Config
// Serve serves a loadbalancer.
func Serve() {
data, err := ioutil.ReadFile("./config.json")
if err != nil {
log.Fatal(err.Error())
}
json.Unmarshal(data, &cfg)
go healthCheck()
s := http.Server{
Addr: ":" + cfg.Proxy.Port,
Handler: http.HandlerFunc(lbHandler),
}
if err = s.ListenAndServe(); err != nil {
log.Fatal(err.Error())
}
}
リトライの実装やパーシステンスの対応などができていませんが、Golangでは比較的簡単にロードバランサーを実装できることが分かったかと思います。
気づけば入社して丸3年が経ち、会社のアドベントカレンダーも3回目の参戦です。
ここ1年はRe-ArchitectureチームというMakuakeのサービス基盤の開発・運用を行うチームに所属し、色々と奮闘してきました。
来年もきっとあれこれと奮闘するでしょう。
Re-Architectureチームの求人はこちら。
【Go/マイクロサービス】「Makuake」の基盤を刷新するRe-Architectureチームのエンジニア募集!
さて、今年の一本目の記事(24日にもう一本かく、たぶん)は「コンテナで始めるモニタリング基盤構築」です。
特に本業とは関係なく、趣味で作っているアプリケーションのモニタリング基盤をコンテナでいい感じにしてみたいと思ってあれこれ試していたので、その時の知見(というほどでもないですが・・・)を公開しようと思います。
今回構築するモニタリング基盤を構成するアプリケーションは以下です。
とりあえず構築して遊んでみたい人向けにざっくり構成してみました。
これらのアプリケーションをdocker-composeで構築します。
全ての実装はbmf-san/docker-based-monitoring-stack-boilerplateに置いてあります。
cloneしたら.env
を作ってdocker-compose up
するだけですぐに触れるようになっています。
ちなみにM1だとcadvisorが起動しないため、コンテナメトリクスが収集できません。intel macやubuntuでは動作確認済みです。
ディレクトリ構成は以下の通りです。1コンテナずつ解説していきます。
.
├── app
│ ├── Dockerfile
│ ├── go.mod
│ └── main.go
├── cadvisor
│ └── Dockerfile
├── docker-compose.yml
├── elasticsearch
│ └──Dockerfile
├── .env.example
├── fluentd
│ ├── Dockerfile
│ └── config
│ └── fluent.conf
├── grafana
│ ├── Dockerfile
│ └── provisioning
│ ├── dashboards
│ │ ├── containor_monitoring.json
│ │ ├── dashboard.yml
│ │ └── node-exporter-full_rev21.json
│ └── datasources
│ └── datasource.yml
├── kibana
│ ├── Dockerfile
│ └── config
│ └── kibana.yml
├── node-exporter
│ └── Dockerfile
└── prometheus
├── Dockerfile
└── template
└── prometheus.yml.template
1つひとつ解説していきたいと思います。
最初にログを吐く雑なアプリケーションコンテナを作ります。
├── app
│ ├── Dockerfile
│ ├── go.mod
│ └── main.go
アプリケーションはこんな感じです。”OK”とログを吐いて、"Hello World"とレスポンスするだけのサーバーです。
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
log.Println("OK")
fmt.Fprintf(w, "Hello World")
}))
http.ListenAndServe(":8080", mux)
}
Dockerfileはソースをビルドしてバイナリを実行するだけの単純なもので、特に補足はありません。
docker-compose.ymlのほうは以下のような形になります。
version: '3.9'
services:
app:
container_name: "${APP_CONTAINER_NAME}"
environment:
- APP_IMAGE_NAME=${APP_IMAGE_NAME}
- APP_IMAGE_TAG=${APP_IMAGE_TAG}
- ALPINE_IMAGE_NAME=${APP_IMAGE_NAME}
build:
context: "./app"
dockerfile: "Dockerfile"
args:
PLATFORM: "${PLATFORM}"
APP_IMAGE_NAME: "${APP_IMAGE_NAME}"
APP_IMAGE_TAG: "${APP_IMAGE_TAG}"
ALPINE_IMAGE_NAME: "${ALPINE_IMAGE_NAME}"
ports:
- ${APP_HOST_PORT}:${APP_CONTAINER_PORT}
command: ./app
logging:
driver: "fluentd"
options:
fluentd-address: ${FLUENTD_ADDRESS}
fluentd-async-connect: "true"
tag: "${APP_LOGGING_TAG}"
logging driverにfluentdを指定して、ログをfluentdに転送します。
fluent-async-connect
はfluentdと接続が確立できるまでログをバッファリングする設定で、trueの場合は接続が確立していなくてもログをバッファリングしてくれます。
アプリケーションのログ転送先であるfluentdのコンテナについて解説します。
├── fluentd
│ ├── Dockerfile
│ └── config
│ └── fluent.conf
Dockerfileは下記です。
ARG FLUENTD_IMAGE_NAME=${FLUENTD_IMAGE_NAME}
ARG FLUENTD_IMAGE_TAG=${FLUENTD_IMAGE_TAG}
ARG PLATFORM=${PLATFORM}
FROM --platform=${PLATFORM} ${FLUENTD_IMAGE_NAME}:${FLUENTD_IMAGE_TAG}
USER root
RUN gem install fluent-plugin-elasticsearch
USER fluent
fluentdで使っているgemはelasticsearchと連携するためのfluent-plugin-elasticsearchだけです。
USERをrootにして、最後にfluentに戻しているのは、fluentdのイメージの実行ユーザーがfluentな為です。
fluentdのconfは以下のように設定します。
<source>
@type forward
port "#{ENV['FLUENTD_CONTAINER_PORT']}"
bind 0.0.0.0
</source>
<match "#{ENV['APP_LOGGING_TAG']}">
@type copy
<store>
@type elasticsearch
host elasticsearch
port "#{ENV['ELASTICSEARCH_CONTAINER_PORT']}"
user "#{ENV['ELASTICSEARCH_ELASTIC_USERNAME']}"
password "#{ENV['ELASTICSEARCH_ELASTIC_PASSWORD']}"
logstash_format true
logstash_prefix "#{ENV['FLUENTD_LOGSTASH_PREFIX_APP']}"
logstash_dateformat %Y%m%d
include_tag_key true
type_name "#{ENV['FLUENTD_TYPE_NAME_APP']}"
tag_key @log_name
flush_interval 1s
</store>
</match>
fluentdのconfでは#{...}
という形式で環境変数を埋め込むことができるので、envsubstなどを利用しなくても変数を埋め込むことができ便利です。
docker-compose.ymlの方は特記事項がないため割愛します。
elasticsearchはシングルーノードで起動するように設定します。
他に特記することがないので詳細は省きます。
├── elasticsearch
│ └──Dockerfile
続いて、アプリケーションログの可視化をするkibanaについてです。
Dockerfileについては特記事項がないので記載を割愛して、kibanaのconfから説明します。
server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://${ELASTICSEARCH_CONTAINER_NAME}:${ELASTICSEARCH_CONTAINER_PORT}" ]
xpack.monitoring.ui.container.elasticsearch.enabled: true
elasticsearch.username: ${ELASTICSEARCH_ELASTIC_USERNAME}
elasticsearch.password: ${ELASTICSEARCH_ELASTIC_PASSWORD}
xpack.monitoring.ui.container.elasticsearch.enabled
は、elasticsearchがコンテナで実行されている場合は有効化して置く必要のあるオプションです。
kibanaのdocker-compose.ymlについては特記事項がないため割愛します。
node-exporterとcadvisorについてマウントするディレクトリや起動オプションについて意識する程度なので説明は割愛します。
続いてprometheusです。
envsubstを使ってprometheusの設定ファイルを書きたかったので、Dockerfileを下記のようにしています。
# NOTE: see https://www.robustperception.io/environment-substitution-with-docker
ARG ALPINE_IMAGE_NAME=${ALPINE_IMAGE_NAME}
ARG ALPINE_IMAGE_TAG=${ALPINE_IMAGE_TAG}
ARG PROMETHEUS_IMAGE_NAME=${PROMETHEUS_IMAGE_NAME}
ARG PROMETHEUS_IMAGE_TAG=${PROMETHEUS_IMAGE_TAG}
ARG PLATFORM=${PLATFORM}
FROM --platform=${PLATFORM} ${PROMETHEUS_IMAGE_NAME}:${PROMETHEUS_IMAGE_TAG} as build-stage
FROM --platform=${PLATFORM} ${ALPINE_IMAGE_NAME}:${ALPINE_IMAGE_TAG}
RUN apk add gettext
COPY --from=build-stage /bin/prometheus /bin/prometheus
RUN mkdir -p /prometheus /etc/prometheus \
&& chown -R nobody:nogroup etc/prometheus /prometheus
COPY ./template/prometheus.yml.template /template/prometheus.yml.template
USER nobody
VOLUME [ "/prometheus" ]
WORKDIR /prometheus
docker-compose.ymlは下記のようになります。
prometheus:
container_name: "${PROMETHEUS_CONTAINER_NAME}"
environment:
- PROMETHEUS_IMAGE_NAME=${PROMETHEUS_IMAGE_NAME}
- PROMETHEUS_IMAGE_TAG=${PROMETHEUS_IMAGE_TAG}
- PROMETHEUS_CONTAINER_NAME=${PROMETHEUS_CONTAINER_NAME}
- PROMETHEUS_CONTAINER_PORT=${PROMETHEUS_CONTAINER_PORT}
- CADVISOR_CONTAINER_NAME=${CADVISOR_CONTAINER_NAME}
- CADVISOR_CONTAINER_PORT=${CADVISOR_CONTAINER_PORT}
- NODE_EXPORTER_CONTAINER_NAME=${NODE_EXPORTER_CONTAINER_NAME}
- NODE_EXPORTER_CONTAINER_PORT=${NODE_EXPORTER_CONTAINER_PORT}
build:
context: "./prometheus"
dockerfile: "Dockerfile"
args:
PLATFORM: "${PLATFORM}"
PROMETHEUS_IMAGE_NAME: "${PROMETHEUS_IMAGE_NAME}"
PROMETHEUS_IMAGE_TAG: "${PROMETHEUS_IMAGE_TAG}"
ALPINE_IMAGE_NAME: "${ALPINE_IMAGE_NAME}"
ALPINE_IMAGE_TAG: "${ALPINE_IMAGE_TAG}"
ports:
- ${PROMETHEUS_HOST_PORT}:${PROMETHEUS_CONTAINER_PORT}
command:
- /bin/sh
- -c
- |
envsubst < /template/prometheus.yml.template > /etc/prometheus/prometheus.yml
/bin/prometheus \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/prometheus
restart: always
prometheusの設定ファイルは次のように書いています。
scrape_configs:
- job_name: 'prometheus'
scrape_interval: 5s
static_configs:
- targets:
- ${PROMETHEUS_CONTAINER_NAME}:${PROMETHEUS_CONTAINER_PORT}
- job_name: 'cadvisor'
static_configs:
- targets:
- ${CADVISOR_CONTAINER_NAME}:${CADVISOR_CONTAINER_PORT}
- job_name: 'node-exporter'
static_configs:
- targets:
- ${NODE_EXPORTER_CONTAINER_NAME}:${NODE_EXPORTER_CONTAINER_PORT}
スクレイプしたいジョブ名とターゲットについてだけ書いています。Alertmanagerを使ったアラート通知を設定したい場合はAlertmanagerの設定もこの設定ファイルに追記することになります。
最後にgrafanaです。
docker-compose.ymlは次のようになります。
grafana:
container_name: "${GRAFANA_CONTAINER_NAME}"
environment:
- GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER}
- GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP="${GF_USERS_ALLOW_SIGN_UP}"
- GF_USERS_ALLOW_ORG_CREATE="${GF_USERS_ALLOW_ORG_CREATE}"
- DS_PROMETHEUS=${DS_PROMETHEUS}
build:
context: "./grafana"
dockerfile: "Dockerfile"
args:
PLATFORM: "${PLATFORM}"
GRAFANA_IMAGE_NAME: "${GRAFANA_IMAGE_NAME}"
GRAFANA_IMAGE_TAG: "${GRAFANA_IMAGE_TAG}"
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
ports:
- ${GRAFANA_HOST_PORT}:${GRAFANA_CONTAINER_PORT}
restart: always
provisioning
はデータソースやダッシュボードのプロジョニングで使うファイルを置いておくディレクトリです。
データソースにはprometheusを利用するので、datasources/datasource.ymlにprometheusの設定を記載しています。
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
url: http://prometheus:9090
basicAuth: false
isDefault: true
editable: true
ダッシュボードはコンテナメトリクス用とOSメトリクス用のダッシュボードの設定ファイルを用意しています。
ダッシュボードの設定ファイルはgrafana.com - grafana/dashboardsで公開されているものを利用することができます。
ダッシュボードはゼロから組み立てるとそこそこ大変なので、ベースになるものを探してそれを調整するのが良さそうに思います。
公開されているものはかなり充実しているので、色々触ってみると面白いです。
設定値のほとんどを環境変数で調整できるように構成しています。
bmf-san/docker-based-monitoring-stack-boilerplateの.env.example
を.env
としてコピーしたら、docker-compose up
で起動できます。
.env.example
の設定では以下のようにポート番号を振っています。
Application | URL |
---|---|
app | http://localhost:8080/ |
prometheus | http://localhost:9090/graph |
node-exporter | http://localhost:9100/ |
mysqld-exporter | http://localhost:9104/ |
grafana | http://localhost:3000/ |
kibana | http://0.0.0.0:5601/ |
割と簡単に構築できたのではないでしょうか(コンテナの恩恵かな)。
それぞれのアプリケーションのアーキテクチャ構成は奥深いので一通り触ったら仕組みをみてみるというのも面白いかと思います。
まだ実際に運用できていないので、早い所運用に乗せてみたい所存です。
StreamYardを使ってプレゼンする機会があったのでやり方をメモしておく。
Keynoteの発表者ディスプレイを表示しつつ、再生中のスライドだけをStreamYardで共有する方法について手順をまとめる。
ディスプレイが2枚必要。
再生中のウインドウ画面右上、発表者ディスプレイをウインドウで表示。
発表者ディスプレイが表示されるので確認。
StreamYardでShare > Share Screen > ウインドウと進み、再生中のスライドショーを共有する。
再生中のスライドショーを片方のディスプレイで共有しつつ、もう片方のディスプレイで発表者ディスプレイが見れる。
]]>OAuth2、OIDCのキャッチアップで読み漁った資料など。
ブログ記事については投稿日時が古く、更新されていないものもあるので気をつけたい。
フォローさせてもらっているアカウント。
]]>本記事では、Golangの標準パッケージであるnet/httpを用いて、HTTPルーターを自作する方法について解説します。
標準パッケージはあまり多くのルーティングの機能を提供していません。
例えばHTTPメソッドごとのルーティングの定義ができなかったり、URLをパスパラメータとして利用できなかったり、正規表現を利用したルーティングの定義ができなかったりします。
その為、実際のアプリケーション開発ではより高機能なHTTPルーターを導入していることが少なくないのではないでしょうか。
そんなHTTPルーターですが、自作してみると次のようなメリットを享受できます。
本記事では次のような構成でHTTPルーターの自作について解説します。
各章について本筋から離れる内容についてはコラムとして記載しています。
本記事は以下のような読者にとって有意義な内容となることを想定しています。
本記事を読むに当たっては、次のような前提知識があれば内容を十分に理解できます。
筆者はbmf-san/goblinというHTTPルーターのパッケージを公開しています。
ぜひコードを見たり、使ってみたりしてみてください。コントリビュートも大歓迎です。
HTTPルーターはURLルーターと呼ばれたり、単にルーターと呼ばれたりもしますが、本記事ではHTTPルーターと呼称を統一することにします。
HTTPルーターは次の図のように、リクエストされたURLとレスポンスの処理を結びつけるアプリケーションです。
HTTPルーターはURLとレスポンスの処理がマッピングされたデータ(以下、ルートマップ)を元にすることで、ルーティングを行うことができます。
Request URL | Handler |
---|---|
GET / | IndexHandler |
GET /foo | FooHandler |
POST /foo | FooHandler |
GET /foo/:id | FooHandler |
POST /foo/:id | FooHandler |
GET /foo/:id/:name | FooHandler |
POST /foo/:id/:name | FooHandler |
GET /foo/bar | FooBarHandler |
GET /foo/bar/:id | FooBarHandler |
GET /foo/bar/:id/:name | FooBarHandler |
GET /foo/bar/baz | FooBarBazHandler |
GET /bar | BarHandler |
GET /baz | BazHandler |
HTTPルーターの内部では、定義されたルートマップはルーティングに最適化されたデータ構造となります。
データ構造については、次の章で解説します。
本記事では、ルートマップを元に、リクエストのURLに応じたレスポンスの処理を探し出すことを「ルーティング」と定義します。
また、HTTPにおいてルーティングを行うアプリケーションのことを「HTTPルーター」と定義します。
URLは、インターネット上のページのアドレスを表し、Uniform ResourceLocatorの略語です。
URL文字列の形式は次のように定義されています。
<scheme>:<scheme-specific-part>
この部分にはhttp、https、ftpなどのプロトコル名がよく使用されますが、プロトコル名以外のスキーマ名も定義されています。
<scheme-specific-part>
の部分では、スキーマに基づく文字列が定義されています。
例えば、httpおよびhttpsスキームの場合、ドメイン名とパス名(またはディレクトリ名)が定義されるという規則があります。
詳細なURL仕様については、RFC 1738を参照してください。
RFC 1738は、インターネット標準(STD1)として位置付けられています。
以下は第1章で例示したルートマップです。
Request URL | Handler |
---|---|
GET / | IndexHandler |
GET /foo | FooHandler |
POST /foo | FooHandler |
GET /foo/:id | FooHandler |
POST /foo/:id | FooHandler |
GET /foo/:id/:name | FooHandler |
POST /foo/:id/:name | FooHandler |
GET /foo/bar | FooBarHandler |
GET /foo/bar/:id | FooBarHandler |
GET /foo/bar/:id/:name | FooBarHandler |
GET /foo/bar/baz | FooBarBazHandler |
GET /bar | BarHandler |
GET /baz | BazHandler |
URLに着目すると階層構造であることが見て取れます。
階層構造は木構造と相性が良いので、ルートマップを木構造で表現することを考えます。
グラフ理論における木の構造をしたデータ構造のことです。
木構造は階層構造を表現するのに適したデータ構造です。
木を構成する要素をノード(節)、一番上位に親のないノードをルート(根)、最下位にある子のないノードをリーフ(葉)と呼びます。ノードとノードの繋がりはエッジ(枝)と呼びます。
木にノードを追加することを挿入、木からノードを探し出すことを探索と言います。
木構造の中でも基本的な木である二分探索木の実装例を次に示します。
package main
import (
"fmt"
)
// Node is a node of a tree.
type Node struct {
Key int
Left *Node
Right *Node
}
// BST is a binary search tree.
type BST struct {
Root *Node
}
// insert insert a node to tree.
func (b *BST) insert(key int) {
if b.Root == nil {
b.Root = &Node{
Key: key,
Left: nil,
Right: nil,
}
} else {
recursiveInsert(b.Root, &Node{
Key: key,
Left: nil,
Right: nil,
})
}
}
// recursiveInsert insert a new node to targetNode recursively.
func recursiveInsert(targetNode *Node, newNode *Node) {
// if a newNode is smaller than targetNode, insert a newNode to left child node.
// if a newNode is a bigger than targetNode, insert a newNode to right childe node.
if newNode.Key < targetNode.Key {
if targetNode.Left == nil {
targetNode.Left = newNode
} else {
recursiveInsert(targetNode.Left, newNode)
}
} else {
if targetNode.Right == nil {
targetNode.Right = newNode
} else {
recursiveInsert(targetNode.Right, newNode)
}
}
}
// remove remove a key from tree.
func (b *BST) remove(key int) {
recursiveRemove(b.Root, key)
}
// recursiveRemove remove a key from tree recursively.
func recursiveRemove(targetNode *Node, key int) *Node {
if targetNode == nil {
return nil
}
if key < targetNode.Key {
targetNode.Left = recursiveRemove(targetNode.Left, key)
return targetNode
}
if key > targetNode.Key {
targetNode.Right = recursiveRemove(targetNode.Right, key)
return targetNode
}
if targetNode.Left == nil && targetNode.Right == nil {
targetNode = nil
return nil
}
if targetNode.Left == nil {
targetNode = targetNode.Right
return targetNode
}
if targetNode.Right == nil {
targetNode = targetNode.Left
return targetNode
}
leftNodeOfMostRightNode := targetNode.Right
for {
if leftNodeOfMostRightNode != nil && leftNodeOfMostRightNode.Left != nil {
leftNodeOfMostRightNode = leftNodeOfMostRightNode.Left
} else {
break
}
}
targetNode.Key = leftNodeOfMostRightNode.Key
targetNode.Right = recursiveRemove(targetNode.Right, targetNode.Key)
return targetNode
}
// search search a key from tree.
func (b *BST) search(key int) bool {
result := recursiveSearch(b.Root, key)
return result
}
// recursiveSearch search a key from tree recursively.
func recursiveSearch(targetNode *Node, key int) bool {
if targetNode == nil {
return false
}
if key < targetNode.Key {
return recursiveSearch(targetNode.Left, key)
}
if key > targetNode.Key {
return recursiveSearch(targetNode.Right, key)
}
// targetNode == key
return true
}
// depth-first search
// inOrderTraverse traverse tree by in-order.
func (b *BST) inOrderTraverse() {
recursiveInOrderTraverse(b.Root)
}
// recursiveInOrderTraverse traverse tree by in-order recursively.
func recursiveInOrderTraverse(n *Node) {
if n != nil {
recursiveInOrderTraverse(n.Left)
fmt.Printf("%d\n", n.Key)
recursiveInOrderTraverse(n.Right)
}
}
// depth-first search
// preOrderTraverse traverse by pre-order.
func (b *BST) preOrderTraverse() {
recursivePreOrderTraverse(b.Root)
}
// recursivePreOrderTraverse traverse by pre-order recursively.
func recursivePreOrderTraverse(n *Node) {
if n != nil {
fmt.Printf("%d\n", n.Key)
recursivePreOrderTraverse(n.Left)
recursivePreOrderTraverse(n.Right)
}
}
// depth-first search
// postOrderTraverse traverse by post-order.
func (b *BST) postOrderTraverse() {
recursivePostOrderTraverse(b.Root)
}
// recursivePostOrderTraverse traverse by post-order recursively.
func recursivePostOrderTraverse(n *Node) {
if n != nil {
recursivePostOrderTraverse(n.Left)
recursivePostOrderTraverse(n.Right)
fmt.Printf("%v\n", n.Key)
}
}
// breadth-first search
// levelOrderTraverse traverse by level-order.
func (b *BST) levelOrderTraverse() {
if b != nil {
queue := []*Node{b.Root}
for len(queue) > 0 {
currentNode := queue[0]
fmt.Printf("%d ", currentNode.Key)
queue = queue[1:]
if currentNode.Left != nil {
queue = append(queue, currentNode.Left)
}
if currentNode.Right != nil {
queue = append(queue, currentNode.Right)
}
}
}
}
func main() {
tree := &BST{}
tree.insert(10)
tree.insert(2)
tree.insert(3)
tree.insert(3)
tree.insert(3)
tree.insert(15)
tree.insert(14)
tree.insert(18)
tree.insert(16)
tree.insert(16)
tree.remove(3)
tree.remove(10)
tree.remove(16)
fmt.Println(tree.search(10))
fmt.Println(tree.search(19))
// Traverse
tree.inOrderTraverse()
tree.preOrderTraverse()
tree.postOrderTraverse()
tree.levelOrderTraverse()
fmt.Printf("%#v\n", tree)
}
ここでは詳細に説明することは割愛しますが、二分探索木は木構造の基本的なアルゴリズムを学ぶにちょうど良い木です。
木構造には二分探索木の他にも様々な種類があります。その中でもトライ木(プレフィックス木ともいわれる。本記事ではトライ木と呼称します)と呼ばれる木構造は文字列の探索がしやすいという特徴があります。
トライ木を利用することによりルーティングで扱いやすいデータ構造を表現できます。
トライ木は、IPアドレス探索や形態素解析などでも利用されている木構造の一種です。
各ノードは単一または複数の文字列あるいは数値を持ち、根ノードから葉に向かって探索して値をつなげていくことで単語を表現します。
アルゴリズムを可視化して動的に理解できるサービスがあるので、そちらを見てみるとトライ木のデータ構造を理解しやすいです。
cf. Algorithm Visualizations - Trie (Prefix Tree)
トライ木は比較的簡単に実装できます。
次のコードは探索と挿入だけ実装されたトライ木のコード例です。
package main
import "fmt"
// Node is a node of tree.
type Node struct {
key string
children map[rune]*Node
}
// NewTrie is create a root node.
func NewTrie() *Node {
return &Node{
key: "",
children: make(map[rune]*Node),
}
}
// Insert is insert a word to tree.
func (n *Node) Insert(word string) {
runes := []rune(word)
curNode := n
for _, r := range runes {
if nextNode, ok := curNode.children[r]; ok {
curNode = nextNode
} else {
curNode.children[r] = &Node{
key: string(r),
children: make(map[rune]*Node),
}
}
}
}
// Search is search a word from a tree.
func (n *Node) Search(word string) bool {
if len(n.key) == 0 && len(n.children) == 0 {
return false
}
runes := []rune(word)
curNode := n
for _, r := range runes {
if nextNode, ok := curNode.children[r]; ok {
curNode = nextNode
} else {
return false
}
}
return true
}
func main() {
t := NewTrie()
t.Insert("word")
t.Insert("wheel")
t.Insert("world")
t.Insert("hospital")
t.Insert("mode")
fmt.Printf("%v", t.Search("mo")) // true
}
このトライ木をベースにすることで、ルーティングに最適化したデータ構造を検討します。
トライ木の考え方をベースにルートマップのデータ構造を考えます。
以下は筆者が開発しているbmf-san/goblinで採用しているデータ構造となります。
goblinでは、ミドルウェアやパスパラメータをサポートしているため、それらに対応したデータ構造となっています。
このデータ構造は次のようなルートマップを表現しています。
Request URL | Handler | Middleware |
---|---|---|
GET / | IndexHandler | none |
GET /foo | FooHandler | FooMws |
POST /foo | FooHandler | FooMws |
GET /foo/bar | FooBarHandler | none |
GET /foo/bar/:id | FooBarHandler | none |
GET /foo/baz | FooBazHandler | none |
GET /foo/bar/baz | FooBarBazHandler | none |
GET /baz | BazHandler | none |
観点としては、以下の二点に集約されます。
前者はルーティングの性能を決める部分であり、処理時間やメモリ効率などを追求する場合はより高度な木構造の採用を検討する必要があります。
後者はHTTPルーターの機能に関わる部分なので、提供したい機能によって様々です。
今回紹介したトライ木をベースとした木構造は、あくまで筆者が考えた木構造に過ぎません。
HTTPルーターの実装要件によりデータ構造はそれぞれです。
次の章でこのデータ構造をHTTPルーターに組み込むため上で知っておきたいことについて説明します。
文字列を格納するトライ木を更に発展させた木構造に基数木という木構造があります。
基数木はパフォーマンスに配慮したHTTPルーターでは良く使われているのを筆者は観測しています。
Golangのstringsパッケージの内部でも使われているようです。
https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/strings/strings.go;l=924
HTTPルーターの実装について説明する前に、次のようなnet/httpを利用したHTTPサーバーのコードを例に、HTTPルーターを実装する上で知っておきたいことについて説明します。
必要に応じて以下のリンクを参照してください。
https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
http.ListenAndServe(":8080", mux)
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}
このコードは単純であるものの、HTTPルーターを自作する上で示唆に富んだコードです。
このコードはマルチプレクサの呼び出し、ハンドラの登録、サーバーの起動という流れの処理になっています。
それぞれについて順番に見ていきます。
まず最初のコードは、http.ServeMux
という構造体を生成しています。
mux := http.NewServeMux()
net/httpのドキュメントでは、http.ServeMux
はHTTPリクエストマルチプレクサ(以下、マルチプレクサ)であると説明がなされています。
このマルチプレクサは、リクエストのURLを登録済みのパターンと照らし合わせて、最もマッチするハンドラ(レスポンスを返却する関数)を呼び出すという役目を持っています。
http.ServeMux
はつまり、ルーティングのための構造体であるということが言えます。
このhttp.ServeMux
にはServeHTTP
というメソッドが実装されています。
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
cs.opensource.google - go1.17.2:src/net/http/server.go;l=2415
ServeHTTP
の以下の部分を更に読み進めていくと、ServeHTTP
のルーティングの処理が見えてきます。
h, _ := mux.Handler(r)
順々にコードジャンプしていくと、マッチするハンドラを探して返却する関数にたどり着きます。
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
// Check for exact match first.
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}
// Check for longest valid match. mux.es contains all patterns
// that end in / sorted from longest to shortest.
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
マッチするハンドラが見つかった場合は、そのハンドラのServeHTTP
を呼びだすことで、レスポンスのための処理を呼び出します。
それがhttp.ServeMux
に実装されたServeHTTP
メソッドの末尾にある処理です。
h.ServeHTTP(w, r)
HTTPルーターを自作するためには、標準のマルチプレクサに取って代われるように、http.Handler
型を満たした(≒ServeHTTP
を実装した)マルチプレクサを実装してあげる必要があります。
続いて次のコードでは、マルチプレクサにハンドラを登録しています。
mux.HandleFunc("/", handler)
マルチプレクサに登録されるハンドラは、http.Handler
型を満たす(≒ServeHTTP
が実装される)必要があります。
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
handler := foo{}
mux.Handle("/", handler)
http.ListenAndServe(":8080", mux)
}
type foo struct{}
// Satisfy the http.Handler type by implementing ServeHTTP.
func (f foo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}
あるいは、http.HandlerFunc
型を実装する形でもハンドラを作成できます。
http.HandlerFunc
型はfunc(ResponseWriter, *Request)
を型として定義したもので、ServeHTTP
メソッドを実装しています。
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/net/http/server.go;l=2045
従ってhttp.HandlerFunc
型を使う場合は次のようにハンドラを作成できます。
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(handler))
http.ListenAndServe(":8080", mux)
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}
HTTPルーターを実装する上では、http.Handler
型をサポートするように実装を意識すると、ハンドラの作成方法に柔軟性をもたせることができるため、扱いやすいパッケージになります。
最後のコードでは、サーバーを起動するポート番号とマルチプレクサを関数に渡して、HTTPサーバーを起動しています。
http.ListenAndServe(":8080", mux)
内部的には、http.Server
型のListenAndServe
が呼び出されています。
この関数では、第2引数がnilのときはhttp.DefaultServeMux
というデフォルトのhttp.ServeMux
が利用されるようになっています。
つまり、マルチプレクサを拡張したいようなケース以外では、マルチプレクサをわざわざ生成しなくても良いということです。
HTTPルーターを実装していく上では、話の前触れとして必要だったため、マルチプレクサをわざわざ生成するようなコードを例として上げました。
HTTPルーターを実装する上での必要なコードリーディングができたので、次の章から実装の解説をします。
URLの末尾に付与される/
はドメイン名末尾と、サブディレクトリ末尾のケースでそれぞれ違いがあります。
ドメイン名末尾の場合、一般的なブラウザでは/
が無い場合は、/
が有るURLにリクエストします。
https://bmf-tech.com
→ https://bmf-tech.com/
にリクエストhttps://bmf-tech.com/
→ https://bmf-tech.com
にリクエストドメイン名末尾の場合は/
の有無にあまり違いはありませんが、サブディレクトリ末尾場合は明確な違いがあります。
https://bmf-tech.com/posts
→ ファイルへのリクエストhttps://bmf-tech.com/posts/
→ ディレクトリへのリクエストより詳しく仕様について知りたい場合はRFCを参照してください。
HTTPルーターを実装する上では、URLのパス部分をどう解釈するかという点で気にしておく必要がある部分です。
筆者が開発したbmf-san/goblinでは、末尾/
有無は同じルーティングの定義として扱う仕様としています。
HTTPルーターの実装をするための準備ができたので、実装の解説をします。
今回は標準パッケージよりも少しだけ高機能なルーターを実装します。
具体的には次の2つの特徴を備えたルーターになります。
標準パッケージの機能では、HTTPメソッド別にルーティングを登録できません。
HTTPメソッド別にルーティングをしたい場合はハンドラーの中でHTTPメソッドごとの条件分岐をするような実装が必要となります。
// ex.
func indexHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// do something...
case http.MethodPost:
// do something...
...
default:
ハンドラーにこのような条件分岐を定義せずとも、メソッドごとにルーティングを定義できる機能を実装します。
メソッドベースでルーティングを定義可能なHTTPルーターのアルゴリズムとしては、HTTPルーターのデータ構造で説明したトライ木をベースとした木構造を採用します。
今回実装するHTTPルーターのソースコードは以下にあります。
bmf-san/introduction-to-golang-http-router-made-with-net-http
テストコードは実装過程で書くことを推奨しますが、テストコードについて解説は行いません。
CIについても同様です。
Golangのバージョンは1.17を利用しています。
実装手順としてはまず、トライ木をベースとしたルーティングのアルゴリズムを実装するところから始めます。
その後でメソッドベースのルーティングをサポートするための実装をします。
それでは早速実装していきます。
ここで実装するコードは全て以下で参照できます。
bmf-san/introduction-to-golang-http-router-made-with-net-http/blob/main/trie.go
今回は、goblinのデータ構造を簡素化した以下のような木構造を採用することにします。
この木構造で表現されるルートマップは以下の通りです。
Request URL | Handler |
---|---|
GET / | IndexHandler |
GET /foo | FooHandler |
POST /foo | FooHandler |
GET /foo/bar | FooBarHandler |
GET /foo/bar/baz | FooBarBazHandler |
GET /bar | BarHandler |
GET /baz | BazHandler |
上記の木構造を表現するために、まずは必要なデータを定義していくところから書き始めます。
trie.go
というファイルを作成し、構造体を定義してください。
package myrouter
// tree is a trie tree.
type tree struct {
node *node
}
type node struct {
label string
actions map[string]*action // key is method
children map[string]*node // key is a label o f next nodes
}
// action is an action.
type action struct {
handler http.Handler
}
// result is a search result.
type result struct {
actions *action
}
tree
は木そのもの、node
は木を構成する要素で、label
、actions
、children
を持ちます。
label
はURLのパス、actions
はHTTPメソッドとハンドラーのマップを表現します。children
はlabel
とnode
のマップで、子ノードを表現します。
result
は木からの探索結果を表現します。
続いてこれらの構造体を生成する関数を定義しておきます。
// newResult creates a new result.
func newResult() *result {
return &result{}
}
// NewTree creates a new trie tree.
func NewTree() *tree {
return &tree{
node: &node{
label: pathRoot,
actions: make(map[string]*action),
children: make(map[string]*node),
},
}
}
では、木へノードを追加する部分の処理を実装します。
tree
をポインタレシーバとしたInsert
メソッドを定義します。
func (t *tree) Insert(methods []string, path string, handler http.Handler)) {
//
}
この関数の引数のポイントとしては、HTTPメソッドを複数渡せるように引数を定義している点です。
HTTPメソッドごとに単一のハンドラーを定義できるだけでなく、複数のメソッドに対して同一のハンドラーを定義できるようになっています。
// ex.
func indexHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// do something...
case http.MethodPost:
// do something...
...
default:
実装の方針によっては、ハンドラーの中でHTTPメソッドの条件分岐をしたいというケースもあるという可能性を考慮して、汎用性を持たせています。
続いて、Insert
の中身ですが、最初にスタート地点となるノードを変数として定義しています。
func (t *tree) Insert(methods []string, path string, handler http.Handler)) {
curNode := t.node
}
次に探索する対象が/
(ルート)の場合の条件分岐を処理します。
const (
pathRoot string = "/"
)
func (t *tree) Insert(methods []string, path string, handler http.Handler)) {
curNode := t.node
if path == pathRoot {
curNode.label = path
for _, method := range methods {
curNode.actions[method] = &action{
handler: handler,
}
}
return nil
}
}
/
の場合は、後続のループ処理をする必要がないので、ここで木へのノード追加して終了するように処理します。
/
以外の場合は処理を継続します。
URLのパスを/
で分解して、[]string型のスライスにパスの文字列を格納する処理を行います。
const (
...
pathDelimiter string = "/"
)
func (t *tree) Insert(methods []string, path string, handler http.Handler)) {
...
ep := explodePath(path)
}
// explodePath removes an empty value in slice.
func explodePath(path string) []string {
s := strings.Split(path, pathDelimiter)
var r []string
for _, str := range s {
if str != "" {
r = append(r, str)
}
}
return r
}
[]string型のスライスは、ノード追加する位置を見つけるために、rangeで走査します。
ここでの処理はHTTPルーターのデータ構造で説明したトライ木の実装がベースとしています。
子ノードが見つからなかったときにノードを追加するようにします。
ルーティングの定義が重複するようなケースとなった場合は、後勝ちとなる仕様になるように処理しています。
// Insert inserts a route definition to tree.
func (t *tree) Insert(methods []string, path string, handler http.Handler) error {
...
for i, p := range ep {
nextNode, ok := curNode.children[p]
if ok {
curNode = nextNode
}
// Create a new node.
if !ok {
curNode.children[p] = &node{
label: p,
actions: make(map[string]*action),
children: make(map[string]*node),
}
curNode = curNode.children[p]
}
// last loop.
// If there is already registered data, overwrite it.
if i == len(ep)-1 {
curNode.label = p
for _, method := range methods {
curNode.actions[method] = &action{
handler: handler,
}
}
break
}
}
return nil
}
最終的なInsert
の実装は次のようになります。
// Insert inserts a route definition to tree.
func (t *tree) Insert(methods []string, path string, handler http.Handler) error {
curNode := t.node
if path == pathRoot {
curNode.label = path
for _, method := range methods {
curNode.actions[method] = &action{
handler: handler,
}
}
return nil
}
ep := explodePath(path)
for i, p := range ep {
nextNode, ok := curNode.children[p]
if ok {
curNode = nextNode
}
// Create a new node.
if !ok {
curNode.children[p] = &node{
label: p,
actions: make(map[string]*action),
children: make(map[string]*node),
}
curNode = curNode.children[p]
}
// last loop.
// If there is already registered data, overwrite it.
if i == len(ep)-1 {
curNode.label = p
for _, method := range methods {
curNode.actions[method] = &action{
handler: handler,
}
}
break
}
}
return nil
}
// explodePath removes an empty value in slice.
func explodePath(path string) []string {
s := strings.Split(path, pathDelimiter)
var r []string
for _, str := range s {
if str != "" {
r = append(r, str)
}
}
return r
}
これで木への挿入の処理が実装できたので、次は木からの探索の処理を実装します。
挿入と比べて探索は比較的シンプルなので、一度で説明します。
func (t *tree) Search(method string, path string) (*result, error) {
result := newResult()
curNode := t.node
if path != pathRoot {
for _, p := range explodePath(path) {
nextNode, ok := curNode.children[p]
if !ok {
if p == curNode.label {
break
} else {
return nil, ErrNotFound
}
}
curNode = nextNode
continue
}
}
result.actions = curNode.actions[method]
if result.actions == nil {
// no matching handler was found.
return nil, ErrMethodNotAllowed
}
return result, nil
}
探索の場合も挿入と同じく、URLのパスが/
か否かでループ処理に進むかどうか決まります。
ループ処理に進む場合は、子ノードを見ていき対象のノードが存在するか探索していくだけです。
対象のノードが存在する場合は、リクエストのHTTPメソッドとマッチするハンドラを探して、result
を返します。
ここで実装するコードの全体像は以下になります。
bmf-san/introduction-to-golang-http-router-made-with-net-http/blob/main/router.go
ここではHTTPルーターとしての機能を提供するための実装も合わせて行います。
まずは構造体の定義と生成用の関数です。
// Router represents the router which handles routing.
type Router struct {
tree *tree
}
// route represents the route which has data for a routing.
type route struct {
methods []string
path string
handler http.Handler
}
func NewRouter() *Router {
return &Router{
tree: NewTree(),
}
}
Router
はnet/httpでいうhttp.ServeMux
に当たります。
route
はルーティングの定義のためのデータを持ちます。
次に、Router
に次の3つのメソッドを実装します。
...
func (r *Router) Methods(methods ...string) *Router {
tmpRoute.methods = append(tmpRoute.methods, methods...)
return r
}
// Handler sets a handler.
func (r *Router) Handler(path string, handler http.Handler) {
tmpRoute.handler = handler
tmpRoute.path = path
r.Handle()
}
// Handle handles a route.
func (r *Router) Handle() {
r.tree.Insert(tmpRoute.methods, tmpRoute.path, tmpRoute.handler)
tmpRoute = &route{}
}
Methods
はHTTPメソッドのセッター、Handler
はURLのパスとハンドラーのセッターでHandle
を呼び出します。Handle
では先程実装した木への挿入の処理を呼び出します。
Methods
やHandler
はHTTPルーターを利用する側の可読性を意識して、メソッドチェインとして実装しています。
メソッドベースのルーティングは木と組み合わせてこれで実現できます。
最後は、Router
にServeHTTP
を実装させたら完成です。
...
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
method := req.Method
path := req.URL.Path
result, err := r.tree.Search(method, path)
if err != nil {
status := handleErr(err)
w.WriteHeader(status)
return
}
h := result.actions.handler
h.ServeHTTP(w, req)
}
func handleErr(err error) int {
var status int
switch err {
case ErrMethodNotAllowed:
status = http.StatusMethodNotAllowed
case ErrNotFound:
status = http.StatusNotFound
}
return status
}
今回実装したHTTPルーターは次のように使うことができます。
サーバーを起動してそれぞれのエンドポイントにリクエストして動作確認してみてください。
package main
import (
"fmt"
"net/http"
myroute "github.com/bmf-san/introduction-to-golang-http-router-made-with-net-http"
)
func indexHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "GET /")
})
}
func fooHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
fmt.Fprintf(w, "GET /foo")
case http.MethodPost:
fmt.Fprintf(w, "POST /foo")
default:
fmt.Fprintf(w, "Not Found")
}
})
}
func fooBarHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "GET /foo/bar")
})
}
func fooBarBazHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "GET /foo/bar/baz")
})
}
func barHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "GET /bar")
})
}
func bazHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "GET /baz")
})
}
func main() {
r := myroute.NewRouter()
r.Methods(http.MethodGet).Handler(`/`, indexHandler())
r.Methods(http.MethodGet, http.MethodPost).Handler(`/foo`, fooHandler())
r.Methods(http.MethodGet).Handler(`/foo/bar`, fooBarHandler())
r.Methods(http.MethodGet).Handler(`/foo/bar/baz`, fooBarBazHandler())
r.Methods(http.MethodGet).Handler(`/bar`, barHandler())
r.Methods(http.MethodGet).Handler(`/baz`, bazHandler())
http.ListenAndServe(":8080", r)
}
駆け足気味になってしまいましたが、実装の解説は以上です。
HTTPルーターのパフォーマンス比較に興味があるのであれば、以下のリポジトリを見てみてください。
julienschmidt/go-http-routing-benchmark
筆者はこのリポジトリにgoblinのパフォーマンス比較のPRを出しました。
本記事ではHTTPルーターを自作するまでのアプローチについて解説しました。
第1章では、HTTPルーターは何をするアプリケーションなのかについて整理しました。
第2章では、HTTPルーターにおけるデータ構造について、例を混じえながら解説しました。
第3章では、net/http使ったHTTPサーバーのコードについて深堀りしました。
そして、第4章ではHTTPルーターの実装方法についてコードとともに説明しました。
本記事を通じて、何か1つでも読者の役に立つことがあったり、興味を持ってもらえることがあれば幸いです。
また。拙作であるbmf-san/goblinについてもコードを見てもらえるきっかけになれば嬉しいです。
質問や修正依頼、フィードバック等あればぜひ教えて下さい。
ECS on Fargate環境でDatadog APMを導入したときの雑メモ。
php-fpmのイメージをベースとしたカスタムイメージを使っている。
datadog-php-tracerをが必要なので以下のような感じでイメージに組み込んでいる。
ENV DDTRACE_VERSION=0.65.1
RUN curl -Lo datadog-php-tracer.apk https://github.com/DataDog/dd-trace-php/releases/download/${DDTRACE_VERSION}/datadog-php-tracer_${DDTRACE_VERSION}_noarch.apk \
&& apk add datadog-php-tracer.apk --allow-untrusted \
&& rm datadog-php-tracer.apk
fpmの設定では環境変数を読み取れるように設定。
// fpm.www.conf
clear_env = yes
; Datadog APM
env[DD_AGENT_HOST] = $DD_AGENT_HOST
env[DD_SERVICE] = $DD_SERVICE
env[DD_VERSION] = $DD_VERSION
env[DD_ENV] = $DD_ENV
env[DD_TRACE_PHP_BIN] = $DD_TRACE_PHP_BIN
cf. https://docs.datadoghq.com/agent/amazon_ecs/apm/?tab=ec2metadataendpoint#php-fpm
PHPとDatadogそれぞれに必要な環境をセット。
[
{
// PHP
"environment": [
{
"name": "DD_AGENT_HOST",
"value": "${DATADOG_AGENT_HOST}" // DatadogはサイドカーなのでここはlocalhostでOK
},
{
"name": "DD_SERVICE",
"value": "${DATADOG_SERVICE}"
},
{
"name": "DD_VERSION",
"value": "${DATADOG_VERSION}"
},
{
"name": "DD_ENV",
"value": "${DATADOG_ENV}"
},
{
"name": "DD_TRACE_PHP_BIN",
"value": "${DATADOG_TRACE_PHP_BIN}"
}
]
},
{
// Datadog
"environment": [
{
"name": "ECS_FARGATE",
"value": "true"
},
{
"name": "DD_APM_ENABLED",
"value": "true"
}
]
}
これでAPMは稼働する。
最低限の設定としてはこんな感じだろうか。
上手く動かなかったときは、phpinfo()
でdatadogのセクションを見るといい。
長文の執筆をする際にテキスト校正を自動化しておきたかったのでやってみた。
テキストはGithub上で管理するようにしており、ディレクトリ構成は以下のようになっている。
├── .circleci
│ └── config.yml
├── README.md
├── documents
│ ├── はじめに.md
│ └── おわりに.md
├── images
├── .textlintrc
├── package-lock.json
└── package.json
初期設定。
npm init -y
textlintとtextlintで使用するルールをインストール。
npm install --save-dev textlint textlint-rule-preset-ja-spacing textlint-rule-preset-ja-technical-writing textlint-rule-spellcheck-tech-word textlint-rule-preset-jtf-style textlint-rule-preset-japanese
.textlintrc
{
"filters": {},
"rules": {
"preset-ja-spacing": true,
"preset-ja-technical-writing": true,
"preset-japanese": true,
"preset-jtf-style": true,
"spellcheck-tech-word": true
}
}
Githubでrepo
だけを許可したトークンを発行して、REVIEWDOG_GITHUB_API_TOKEN
という名前で環境変数をセットしておく。
config.ymlの設定は以下の通り。
version: 2
jobs:
build:
docker:
- image: vvakame/review:latest
environment:
REVIEWDOG_VERSION: latest
steps:
- checkout
- restore_cache:
keys:
- npm-cache-{{ checksum "package-lock.json" }}
- run:
name: Setup
command: npm install
- save_cache:
key: npm-cache-{{ checksum "package-lock.json" }}
paths:
- ./node_modules
- run:
name: install reviewdog
command: "curl -sfL https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh| sh -s $REVIEWDOG_VERSION"
- run:
name: lint for ja
command: "$(npm bin)/textlint -f checkstyle documents/*.md | tee check_result"
- run:
name: reviewdog
command: >
if [ -n "$REVIEWDOG_GITHUB_API_TOKEN" ]; then
cat check_result | ./bin/reviewdog -f=checkstyle -name=textlint -reporter=github-pr-review
fi
when: on_fail
textlintに引っかかるとreviewdogがコメントしてくれる。
// 分割したところを指定。対象commitをeditする。
git rebase -i HEAD~5
// 対象commitがunstageされる
git reset HEAD~
// 任意の粒度でadd&commit
git add ~
git commit ~
git rebase --continue
]]>Ubuntu 20.04.2 LTSでmysqlコンテナを起動しようとと以下のようなエラーが出てコンテナ起動に失敗する。
Could not open file '/var/log/mysql/mysql-error.log' for error logging: Permission denied”
問題が発生したdockerfile。
docker-compose.yml(一部抜粋)
version: '3.2'
services:
mysql:
container_name: "example-mysql"
env_file: ./mysql/.env
build:
context: "./mysql"
dockerfile: "Dockerfile"
ports:
- "3306:3306"
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/initdb.d:/docker-entrypoint-initdb.d
- ./mysql/log:/var/log/mysql
Dockerfile
FROM --platform=linux/amd64 mysql:8.0.26
ADD ./conf.d/my.cnf /etc/mysql/conf.d/my.cnf
CMD ["mysqld"]
$ ls -la mysql
drwxrwxrwx 7 systemd-coredump example-app 4096 Sep 12 23:10 data
systemd-coredump
という見慣れないユーザーが。
ホストでユーザーを確認すると、systemd-coredump
はuid 999。
$ cat /etc/passwd | grep systemd-coredump
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
mysqlのコンテナ内のユーザーがuid 999を持っているのがおそらく原因?
docker-compose.ymlにuser: 1000:1000
を追加。
docker-compose.yml(一部抜粋)
version: '3.2'
services:
mysql:
container_name: "example-mysql"
env_file: ./mysql/.env
build:
context: "./mysql"
dockerfile: "Dockerfile"
ports:
- "3306:3306"
volumes:
- ./mysql/data:/var/lib/mysql
- ./mysql/initdb.d:/docker-entrypoint-initdb.d
- ./mysql/log:/var/log/mysql
user: 1000:1000
ハードコードしないでホストからuidとgidを渡すようにしたほうが良い気はする。
docker for macではこの問題は発生していなかったので、気づくことができてよかった。
エラーはこんな感じ。
runtime: failed to create new OS thread (have 2 already; errno=22)
goのエラーだったので、アーキテクチャの何かしらの問題で動いていないのだろうと推測。
とりあえずdocker hubを見て8.0.17より最新のバージョンを探してみると8.0.26の最新パッチバージョンまでリリースされているのを確認できた。
ちょうど2日前にリリースされたらしい。
MySQL8.0.26でM1の対応が入った?ぽいので多分これで動くのでは。
cf. https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-26.html
macOS: It is now possible to build MySQL for macOS 11 on ARM (that is, for Apple M1 systems). (Bug #32386050, Bug #102259)
--platoform指定して、イメージは8.0.26指定。
FROM --platform=linux/amd64 mysql:8.0.26
ADD ./conf.d/my.cnf /etc/mysql/conf.d/my.cnf
CMD ["mysqld"]
とりあえずこれで動いた。
]]>去年に続き、今年も同じメンバーで参加してきた。
ISUCON参加はこれで3度目になる。
bmf-tech.com - ISUCON10に参加してきた
この1年間はメンバーと去年のKPTを元に、ボトルネックの調査までのフローやオペレーションの練習、全ISUCON過去問を見て出題傾向や解法のパターンなどの勉強に専念してきたが、予選敗退という結果に・・
これまで勉強してきたことや練習したことはそれなりに実になっており、本番でもそつなく作業することができたり、想定していたタイムスケジュールどおりに動けたりしていたので、去年よりは良い動きはできたと思うが、スコア伸ばすためのアプローチが中途半端な結果に終わってしまった。時間が足りなかったorz
去年はオフラインで参加したが、今年は時世を考慮してオンラインで参加したので、情報共有のツールを検討する必要があった。
オンラインホワイトボード系のツールは色々あるが、会社でもよく使っており、手に馴染みやすいmiroを使ってみた。
当日の調査状況だったり、終了後の振り返りなどに活用。
チャットツールはDiscordつなぎっぱ。AirPodsだと電池切れするので、有線のイヤホンも用意しておくことで音声周りで時間を無駄にすることはなかった。
アプリケーション側を中心の作業メンバーが2人、インフラ+アプリケーション少々が1人(自分)という感じで以下作業した。
レギュレーションやマニュアルをちゃんと読み合わせ時間と、ボトルネック把握のための準備・調査時間(改善のインパクトが小さいところに時間を大きく使わないようにすることを強く意識してた)を十分に取るようスケジュールを組んだ。
チューニング作業自体は午後13時くらいから始めた。(チューニング作業をもう少し早めに始められるようにするのは次回までの宿題の一つ)
まだできることが全然あるのに作業が間に合わないという悔しい思いをした。
この1年間、2~3週間に1回はチームでオンライン勉強会してきたのだが、こんなに時間が立つのが早いとは・・
来年も参加したい気持ちなのでまた1年間精進して力をつけて参加する。
来年こそはスコアをがしがし上げれるように・・
P.S.
運営の皆様、ありがとうございました。
今年もすごく楽しかったです!
]]>New RelicからDatadog APMへの乗り換え検討時に機能比較をしたのでメモ。
※APM以外の部分も含めている。
New Relic | Datadog | 比較 | |
---|---|---|---|
サーバー監視 | クラウド、コンテナ、OS、ミドルウェア、ネットワーク、OSS等のメトリクス収集可能CPU、メモリ、ストレージ、ネットワーク、プロセス等のメトリクス収集可能 | New Relicと同等なので割愛 | 特に差はなさそう。 |
アプリケーション | レスポンスタイム、スループット、エラー率トランザクションスレッドプロファイルクロスアプリケーショントレーシングトランザクション分析サービスマップ外部サービスのパフォーマンス監視トランザクションのメトリクスとトレース分散トレーシングデプロイ分析 | サービスマップサービスパフォーマンスダッシュボードデプロイ追跡アプリケーションログ、RUM、Syntheticとトレース連携プロファイラ(CPU、メモリ、I/O消費するコード行特定)分散型トレーシング | NewRelicは多機能に見えるが、Datadogでもほぼ同等のことができそう。 |
データベース | データベースコールの消費時間スロークエリデータベースとキャッシュ操作の表示データベースコールのレスポンスタイム、スループットSQLクエリ分析(パフォーマンスの悪いクエリとそのスタックトレースの表示) | クエリスループット、パフォーマンス、接続数、他メトリクス取得可能 | 概ね大差はなさそう。 DatadogはMySQLのインテグレーション導入が必要。 NewRelicはAPM連携でログのドリルダウンができる。 DatadogもおそらくRUMと連携して同様のことができるはず・・ |
RUM (Realtime User Monitoring) | ページロードパフォーマンスブラウザパフォーマンスAJAX分析JavaScriptエラー分析New Relic Mobileと連携可能 | ページビューに関連するメトリクス収集可能RUMエクスプローラー(アプリケーションから収集した全データにアクセス可能、監視やエラー解決に活用可能)ログ、APM、プロファイラと連携可能エラー追跡ios・androidはSDKで対応可能 | フロントエンドはNewRelicのほうが充実していそう。 DatadogはJS周りはjsのtracerの追加が別途必要そう。 cf. https://docs.datadoghq.com/ja/tracing/setup_overview/setup/nodejs/?tab=%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A |
Synthetic | ロケーション(世界各国)別にテスト実施可能1日単位、1分ごとなど指定可能可用性モニタリングスクリプトブラウザーインタラクション(謎)APIテストAPMとも連携可能?(おそらく)CI・CD連携可能 | ブラウザテストプライベートロケーション(内部用アプリケーションの監視、インターネットから接続できないプライベートURLの監視)CI・CD連携APMと連携可能 | 大きく差はなさそう。 |
レポート | カスタムダッシュボード可用性キャパシティ分析SLAデプロイ追跡ホストごとの使用状況分析拡張性 | カスタムダッシュボード予測値モニター異常検知モニターデプロイメントの追跡SLO | Datadogはモニタリングに、New Relicは分析に力を入れている感じかなぁ。 |
統合機能 | 各クラウドのサービスや各種ミドルウェアと連携できる | New Relicと同等なので割愛 | 拡張性はどちらもある。 |
APM導入方法 | 各言語ごとにagentをインストール | Datadog Agentのインストール。Datadog Agentの設定でAPMを有効化する言語ごとにトレーサーの設定をする | NewRelic APMはagentのCPU負荷について情報があるが、Datadog APMは不明。要検証。 |
AWS認定ソリューションアーキテクトアソシエイトを受験して合格したので取り組んだことなどを記録しておく。
自分について。
ソフトウェアエンジニアとして5年目くらい。
AWSは会社で利用しており、普段から触る機会はあるが、設計とかはあまりやっていない。(やりたい)
クラウドの知見が足りないことを自覚(クラウドを使ったアーキテクチャを自分で考えるときに、自分の引き出しが少ないと感じたり、理解が浅い状態で使っているサービスがあったり..などなど)していたので、ちゃんと時間を使って勉強する機会を作ろうと考えていた。
単なる暗記試験ではなく、実務にも十分有益な学習ができると感じたので受験を決めた。
トータル4ヶ月程度。
忙しく勉強していたのは試験前の1ヶ月前くらいの期間。模擬試験に取り組んで理解が及んでいなかったところを復習しまくった。
当日は渋谷のテストセンターで試験を受けた。
オンラインは準備が面倒そうな気がしたので、環境が整っているテストセンターで受験することにしていた。
受験時間まではテストセンター近くのカフェで模擬試験の振り返りをしてたり参考書を読み直したりしてた。
問題の回答ペースなどはUdemy 【SAA-C02版】AWS 認定ソリューションアーキテクト アソシエイト模擬試験問題集(6回分390問で感覚を掴めていたので、特にあせることもなく、順調に回答することができた。
問題の難易度はudemyの模擬試験よりは優しいが、AWS公式の30分の模擬試験(AWS Certified Solutions Architect - Associate Practice)よりは難しかった気がする。
見直しフラグを付けながら全問回答した後、残り時間を存分に見直しに使って回答時間を使い切った。
2~3問くらい見直しで回答を変えた問題がある。見直し大事。
全部回答が終わった後にすぐ合否が分かるのは良い。(正式な合否通知は後日ではあるが)
間違った問題は復習したいがその機会はなさそう。
AWSの基本的なことは分かるぞって少し自信を持って言えるようになった気がする。
合格のための勉強であれば、参考書、Udemy、ドキュメント、よくある質問あたり周回するだけで十分な気はする。実際それだけでも勉強にはなると思う。
試験日程を前もって決めて逆算して計画的に勉強するほうが良いと思う。
試験日は柔軟に選択できるので気軽に先延ばしにしやすい。
自分はあれもこれも勉強しなきゃと色々やっていたせいで試験日を先延ばしにしてしまったので当初の受験計画よりも1~2ヶ月くらい遅延してしまった。
次はプロフェッショナルを受験したいが、その前にGCPの方の試験に取り組む予定。
]]>Goでrouterを作ったときにHTTPサーバーのコードの内部を読んだので、その時のメモ。
Goに入門したとによく見るであろう形のコード。
色々なものが省略されてこの形になっている。
package main
import (
"net/http"
)
func main() {
http.HandlerFunc("/index", func(w http.ReponseWriter, req *http.Request) {
w.Write([]byte("hello world"))
})
http.ListenAndServe(":8080")
}
先程の基本形を省略しないで書いた形。
どのような実装を経て基本形になるのか1つずつ確認していく。
package main
import (
"net/http"
)
func main() {
// マルチプレクサ。URLマッチングをするための構造体。静的なルーティングのみ解決する。
mux := http.NewServeMux()
ih := new(indexHandler)
// muxにルーティングを登録する
mux.Handle("/index", ih)
srv := http.Server{
Addr: ":8080",
Handler: mux,
}
srv.ListenAndServe()
}
// Handlerインターフェースを実装した構造体。
type indexHandler struct{}
// ServeHTTPを実装
func (i *indexHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("helo world"))
}
まずは、Handlerの置き換え。
ServeHTTPは関数型のaliasであるHandlerFuncに置き換えることができる。
cf.
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
// ただの関数をHandlerFunc型にキャストすれば良い。Handlerインターフェースを満たせる。
mux.Handle("/index", http.HandlerFunc(indexHandler))
s := http.Server{
Addr: ":8080",
Handler: m,
}
s.ListenAndServe()
}
// 適当な構造体を用意して、ServeHTTPを実装しなくても良い。
func indexHandler(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("hello world"))
}
muxをDefaultServeMuxで代用。
DefaultServeMuxはServeMux型の構造体を持っている。
HandlerFuncというmuxにルーティングを登録する関数を実装している。
cf.
package main
import (
"net/http"
)
func main() {
// muxを作らなくてもこれだけでOK
http.HandleFunc("/index", indexHandler)
s := http.Server{
Addr: ":3000",
// net/httpがデフォルトで持っている変数。DefaultServeMuxはServeMux型の構造体を持っている。HandlerFuncというmuxにルーティングを登録する関数を実装している。
Handler: http.DefaultServeMux,
}
s.ListenAndServe()
}
func indexHandler(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("hello world"))
}
Server構造体(http.Server{})を作らずとも、ListenAndServe()を代用することができる。
cf.
package main
import (
"net/http"
)
func main() {
http.HandlerFunc("/index", func(w http.ReponseWriter, req *http.Request) {
w.Write([]byte("hello world"))
})
// Server構造体(http.Server{})を作らなくても大丈夫
http.ListenAndServe(":8080")
}
これで最初の基本形に到達。
APIサーバー作るときとか普段余り意識しないと思うが、知っておくと何か拡張したいときに役に立つ、はず。
routerを作るときはhttp.Handlerのインターフェースを意識してmuxを作って上げれば良い。
AWSの代表的なサービスの分類をメモ。
arn:partition:service:region:account-id:resource-id
arn:partition:service:region:account-id:resource-type/resource-id
arn:partition:service:region:account-id:resource-type:resource-id
Apsrairr
最後のrは/
か:
。
最近、自作ルーティングのgoblinをアップデートしたのでその記録を書き残しておく。
以下は過去ルーティングについて書き残した記事。他にも実装検討フェーズの記事があるが、内容があまり良くないので割愛。
基本的な動作をするバージョンを1.0.0としてリリースしていた。
実際に自分で利用する中で、バグを見つけたり、機能の物足りなさを感じて、後方互換性のない変更を何回か経て(行き当たりばったりな実装をしていたツケが回った)、現在は5.0.1が最新のバージョンになっている。
具体的には、ミドルウェアの機能をサポートするようになったことが一番の変更点で、それに伴い内部のデータ構造を見直したり、DSLを見直したり、バグを改修したりした。
ミドルウェアはルーターを利用する側で自由に対応できると考えていたが、実際には制約があった。
利用側でミドルウェアを実装しても、ルーティングのマッチングの処理(≒パスとHTTP
メソッドが登録済みのルーティングにマッチするかどうかの処理)が完了した後にミドルウェアの処理が実行されるような形になるため、HTTPメソッドのマッチング前にミドルウェアを適用したいようなケースに対応できないという制約を設けてしまっていた。
これはPreflightのリクエスト(CORS対応など)を捌きたいときに不便なため、根本的に解決するために、ミドルウェアをサポートする判断をした。
そのようなケースを考慮する上で厄介だったのが、ルーティングが内部的に持つデータ構造で、パスとHTTPメソッドの一致を前提とするようなデータ構造になってしまっていたため、そこの見直しからする必要があった。
なので、データ構造を以下のように変更をして、ミドルウェアのサポートを実装した。
Before
After
静的なルーティングだけ対応したベンチマークを書いていたが、他のライブラリとの比較を動的なルーティングのテストと合わせて比較したみたかったので一番充実していそうなgithub.com - julienschmidt/go-http-routijng-benchmarkを使ってベンチマークテストを実施してみた。
以下は最新のスコア。
#GithubAPI Routes: 203
Goblin: 80864 Bytes
#GPlusAPI Routes: 13
Goblin: 7856 Bytes
#ParseAPI Routes: 26
Goblin: 8688 Bytes
#Static Routes: 157
Goblin: 34488 Bytes
goos: darwin
goarch: amd64
pkg: github.com/julienschmidt/go-http-routing-benchmark
cpu: Intel(R) Core(TM) i5-8210Y CPU @ 1.60GHz
BenchmarkGoblin_Param 738289 1964 ns/op 128 B/op 4 allocs/op
BenchmarkGoblin_Param5 754988 1920 ns/op 368 B/op 6 allocs/op
BenchmarkGoblin_Param20 56145 23260 ns/op 3168 B/op 58 allocs/op
BenchmarkGoblinWeb_ParamWrite 304082 4610 ns/op 648 B/op 11 allocs/op
BenchmarkGoblin_GithubStatic 1156518 2745 ns/op 128 B/op 4 allocs/op
BenchmarkGoblin_GithubParam 125570 9985 ns/op 816 B/op 15 allocs/op
BenchmarkGoblin_GithubAll 2232 622376 ns/op 49424 B/op 1018 allocs/op
BenchmarkGoblin_GPlusStatic 1000000 1298 ns/op 80 B/op 3 allocs/op
BenchmarkGoblin_GPlusParam 417717 2893 ns/op 664 B/op 11 allocs/op
BenchmarkGoblin_GPlus2Params 274990 4551 ns/op 824 B/op 15 allocs/op
BenchmarkGoblin_GPlusAll 95580 14536 ns/op 2208 B/op 57 allocs/op
BenchmarkGoblin_ParseStatic 1651083 707.0 ns/op 128 B/op 4 allocs/op
BenchmarkGoblin_ParseParam 413840 2876 ns/op 728 B/op 12 allocs/op
BenchmarkGoblin_Parse2Params 260120 4119 ns/op 808 B/op 15 allocs/op
BenchmarkGoblin_ParseAll 54518 21692 ns/op 4656 B/op 120 allocs/op
BenchmarkGoblin_StaticAll 26689 46104 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/julienschmidt/go-http-routing-benchmark 37.270s
goblinのベンチマーク対応を追加したPRを投げてみている。
github.com - julienschmidt/go-http-routing-benchmark Add a new router goblin #97
ようやく人並みのルーター?になったような気がする。
まだまだ改善点はあるので今後も継続的にメンテしていく。
たまに混乱するので用語集としてメモ。
障害が発生したら安全な状態に移行する仕組み。
稼働中システムに障害は発生したら代替のシステムに自動的に機能を引き継ぎ、処理を続行する仕組み。
代替システムから元のシステムに処理を引き継いで元の状態に戻すこと。
フェイルオーバーの逆。
障害が発生したら障害箇所を取り除き、影響範囲を狭めて運転を継続すること。
障害が発生したら代替システムに切り替えたりすることで正常稼働状態を維持すること。
システムの故障や障害が生じないように原因となる養素排除する考え方。
障害が発生したら機能や性能を限定したり、別の系統に切り替えるなどして、システムが利用可能な状態を維持すること。
縮退運転。縮退運用。
システム利用者が誤った操作や危険な利用方法ができないように設計すること。またそのような仕組みや構造のこと。
障害が発生しても影響が広がらないような仕組みにすること。
同じような気持ちの記事があった。
zenn.dev - 混ざりやすい専門用語 〜 フェール〇〇、フォールト〇〇
qiita.comm - フォールトトレラント、フェールソフト、諸々の違い
commitを分割したいときの手順についてメモ。
# 分割したいところを指定してrebase。対象のcommitをeditにする。
git rebase -i HEAD~5
# unstageする
git rebase HEAD~
# unstageしたものを分割したい粒度で再commitする。
git add & git commit
# commitが完了したらrebase --continue
git rebase --continue
# log確認
git log
# force push
git push -f origin HEAD
これで分割できるはず。
]]>cf. docs.docker.com - Compose CLI Tech Preview
Tech Previewなのでまだプロダクションでの利用推奨されていないとのこと。
互換性についてはこちら。
docs.docker.com - Compose command compatibility with docker-compose
Docker composeはpythonで実装されているが、今回はサポートされるDocker Composeはgolang製らしい。
cf. https://github.com/docker/compose
cf. https://github.com/docker/compose-cli
TerraformとAnsibleを使ってKubernetes環境構築に取り組んだ。
自作アプリの運用をKubernetesに乗っけてみたいという気持ちから環境を構築するところから初めてみた。
プライベートでの開発なので、せいぜい月2000円前後くらいの予算に留めたいところ。
クラウドかVPSか、マネージドか、ノンマネージドかといったところが大きな観点だが、そのへんはコスト感と運用メリットを考慮しつつ決めれば良いのでそんなに悩まないと思う。後述するが、一番の悩みのタネはロードバランサーだった・・。
今回候補に上がったのは3つ。
上記以外にマネージドk3sを提供しているcivo.comという選択肢も考えたが、k8sを触りたかったので検討外とした。
Digital OceanとConohaで迷ったが、従量課金なしの安心の料金体系に心奪われたのでConohaを選定した。
GKEやDigital OceanはKubernetesをさっと構築して勉強するにはちょうどよい環境が整っていると思うので、そういった目的で利用を検討していく判断をした。
マネージドKubernetesを利用しない選択をしたので、セルフでKubernetesを構築することにした。
構築のツールとしてはkubeadmを採用。
TerraformとAnsibleを使って、インスタンスの構築から初期セットアップ(ユーザー作成、ssh鍵調整など)、kubeadmを使ったKubernetesの構築までコード化したものがこちら↓
github.com - bmf-san/setup-kubernetes-cluster-on-vps-boilerplate
masterノード1台、workerノードは複数台想定になっている。
ConohaはOpenstackをサポートしたAPIを用意しているので、Openstackをサポートしている他サーバー(ex. Digital Ocean)であれば、書き換えも楽なはず。
kubeadmによるKubernetesの構築は、Kubernetesの公式ドキュメントを一読して構築の前提条件を把握しておけばそれほど難しくなかった。
ロードバランサーの対応ができなかったので、アプリケーションを公開してKubernetes運用をするまでに至らなかった。
自前Kubernetesクラスタの場合、クラウドが用意しているロードバランサーが使えないのでOSSのものを自分で用意する必要があるのだが、そのセットアップが上手く行かず断念・・
1週間近く睡眠時間を削ったが歯が立たなかった..w
解決できなかった問題はこれ。
https://github.com/kubernetes/ingress-nginx/issues/5401
自作アプリは一旦docker-composeでの運用をする方向に転換して、Kubernetesの運用はもう少し理解を深めてからにしようと思う。。。
]]>spannerのDBマイグレーションで、golang-migrateを使ったのでメモ。
dockerで使う想定。
dockerではなくバイナリで実行していたが、ホストマシンのopensslのバージョンに依存して動作しない可能性あるようなので、コンテナ実行が無難だと思う。
MIGRATE_VERSION='v4.14.1'
docker run -v /migrations:/migrations -v ~/.config/gcloud/:/root/.config/gcloud --network host migrate/migrate:${MIGRATE_VERSION} -path=/migrations/ -database spanner://projects/<PROJECT ID>/instances/<INSTANCE>/databases/<DATABASE>?x-clean-statements=True <COMMAND>
COMMANDには、up、down、versionなど指定する。
cf. github.com - golang/migrrate/migrate/tree/aster/cmd/migrate#usage
ワンライナーで実行コマンドを書いたので見づらいと思うが、特に難しいとこはないと思う。
マウントしているやつは以下2つ。
-v /migrations:/migrations -v ~/.config/gcloud/:/root/.config/gcloud
/migrations
にはマイグレーション対象のsqlファイルを用意している。それをコンテナの/migrations
にマウント。
~/.config/gcloud/:/root/.config/gcloud
はgcloudの認証を通すため。
credentialファイルをマウントして、環境変数GOOGLE_APPLICATION_CREDENTIALS
をセットする形でも認証を通せるが、この方が楽なので・・
直近のgolang-migrateのバージョンではspannerの接続情報にはクエリパラメータが必要。
spanner://projects/<PROJECT ID>/instances/<INSTANCE>/databases/<DATABASE>?x-clean-statements=True
経緯はこのへん。
]]>プライベートの開発で学習も兼ねてk8sを利用したく、色々検討した結果、DigitalOceanが良さそうだったので、利用してみた。
www.digitalocean.com - The best managed Kubernetes service is the one that’s made for you
新規に始める場合は、プロモリンクやクーポンを使うと良いと思う。
自分はプロモリンクから登録するのを忘れて最初クレジットをもらい損ねたが、問い合わせしたら良きに図らってもらえた(クレジットもらった)。ありがたや。
一応リファラルリンクを貼っておく。
https://m.do.co/c/9fbf85c22695
マネージドKubernetesの話しの前に、Digital Oceanの良いところを書いておく。
Kubernetesを利用しなくともDigitalOceanを利用したくなる機能が充実している。
www.digitalocean.com - docs/kubernetesを参考に概要をまとめておく。
実際にDigitalOceanのKubernetesマネージドサービスを触ってみる。
以下のような構成でKubernetesクラスターを起動した。
月額料金はこれくらい。
MONTHLY RATE $20.00/month $0.03/hour
kubectlは以下参照。
kubernetes.io - install-kubectl
brew install doctl
ダッシュボードのAPIメニューからPersonal access tokensを確認できる。
初回だとREADのみなのでWRITEも付与して、tokenを控えておく。
※初回だとtokenが生成されていないようなので、Regenerate tokenをすることをtokenを払い出す必要があるぽい。あるいは新規トークン作成でも良いと思う。
まずは認証をする。
doctl auth init
クラスター一覧を確認してみる。
doctl kubernetes cluster list
接続するクラスター名を指定してcontextを追加する。(./kube/configが更新される)
doctl kubernetes cluster kubeconfig save CLUSTER_NAME
ノードを確認してみる。
kubectl get no
これでサンプルのアプリをデプロイする準備ができたので、github.com - digitalocean/doks-exampleあたり試してみると良さそう。ロードバランサーが作成されて、ロードバランサーの課金が発生するので注意が必要。
GKEで最安構成のk8s環境を作るのも魅力的だが、個人利用でマネージド使うならDigital Oceanのほうが良いかも。
ちょっと神経質かもしれないけど、転送量が従量課金なのは気になっちゃうので、Conohaも検討を続ける。
pkg.go.dev - cloud.google.com/go/spannerでReadOnlyTransaction
を使ったときにハマったところについてメモ。
数万件のデータを複数回のリクエストに分けて処理するようなバッチ処理のコードを書いていた。ReadOnlyTransaction
を使った処理を以下のように書いていた。
for {
// 〜略〜
// cは*spanner.Client
iter := c.ReadOnlyTransaction().Query(ctx, stmt)
defer iter.Stop()
// 〜略〜
}
一見問題なさそうに見えたのでバッチ処理を走らせていたのだが、特定件数を超えると処理が止まる問題が発生した。
spannerのgoクライアントにはセッション管理の仕組みがあるのだが、トランザクションの終了処理が漏れていたことにより、セッションプールが枯渇、リクエストがタイムアウトしていたらしい。
内部的にはセッション管理の仕組みがReadOnlyTransactionの実行をブロックしているような形になってしまっていたらしい。
トランザクションの終了処理を呼び出すように変更する。
for {
// 〜略〜
// cは*spanner.Client
tx := c.ReadOnlyTransaction()
defer tx.Close()
iter := tx.Query(ctx, stmt)
defer iter.Stop()
// 〜略〜
}
トランザクションの終了処理がないとトランザクション実行の度に新規セッションが生成され、セッションプールが枯渇してしまう。
処理が毎回同じ件数で止まっていたのは、SPANNER_SESSION_POOL_MAX_OPEND
の値制限に引っかかったからの模様。計算してみると帳尻が合う。
ドキュメントをちゃんと読むということ以外には、ツールを使った解決方法もある。
github.com - gcpug/zagane
後はGCPのモニタリングでcloudspannerのsession countをウォッチするというのも有りかもしれない。
年明けてしまったが書いておく。
2020年は毎月振り返りのようなことをTrelloベースでやっていたので、書くつもりはなかったが、文字起こししておきたいことがあったので書く。
去年のやつ。
bmf-tech.com - 2019年の振り返りと来年の抱負
結構ざっくりした内容でこれを見ても何のコメントも浮かばないが、去年計画していたことはTrelloにすべて落としこんでいたので、振り返りはTrelloで完結するようになっている。
というわけでTrelloを見つつ、2020年やれたことを振り返ってみる。
URLルーターをつくった。
趣味だが、木構造のアルゴリズムの勉強も兼ねて自作した。
ヘッドレスCMSの開発が概ねできた。趣味半分、勉強半分でつくっているアプリケーション。
リリースはまだできていない。準備(k8s運用)が完了しなかった。
割とコード書いたと思う。
たかがブログされどブログ、やりたいことを追求してスクラッチした部分が多いので結構勉強になった。
あとISUConに2~3年ぶりくらいに参加した。
これについては別途振り返りを書いた気がするので割愛。
基本のアルゴリズムについて一通りの勉強をした。が、素で書けるほどの訓練ができていない。
それからコーディングクイズとかもそこまでやれなかった・・・
時間がかかるので優先度を上げないと中々身にならない。
C++を学ぶことも合わせて取り組んだが、これはあまり良い作戦ではなかった。
言語を学ぶのとアルゴリズムを学ぶのは別にすべきだった。
Goで始めていたら時間の使い方違っていたかもしれない。
基本情報試験を受けておこうかと思って一通りの勉強をしていたが、コロナの影響で中止になってしまったので、受験を取りやめた。
基本情報を教科書に、ちらほらと勉強していたが、そこまでの時間を割いてはいない。
今後も継続的に学びを続ける必要があると考えているので、定期的に本を読んだり、情報試験関連の勉強をしてみたりと、文系なりの勉強を継続していこうと考えている。
もっと大きな意識の変化があれば、講座や大学(院)といったアカデミックの門戸を叩くことも考えるかもしれないが、今はまだそこまでの優先度で考えていない。
コンテナの本番環境運用のための色々をやった。色々。
Gobelの監視環境構築やアプリケーションをコンテナで運用するための整備に取り組む中で、コンテナの運用のための基本的な知見を得たと思う。
Trello見る限りはもっと色んなタスクを消化しているが、2020年プライベートで時間を割いたことのほとんどは書いた。
毎年そうだが、2020年もアプリケーションに時間を使い過ぎたと思う。自作しているものが個人にとってはそこそこ大きいという要因もあると思うが..
仕事の方に関しても振り返りがあるが、そちらはレジュメという形に起こして振り返っているので割愛。
去年から自分がやりたいこと、やるべきこと、やったほうがよさそうなことを分野別(システム・アーキテクチャ、コンピューターサイエンス、アルゴリズムとデータ構造、などなど)にカテゴライズしてタスクを起票、優先度を月ごとに検討してタスクに取り組むような運用にアップデートしたこともあって、割と計画的に行動することができたように思う。
消化できていないタスクがあるのは時間的な都合が主たる原因なので、それ自体は問題視していないが、優先度が正しかったか、思い描いていたパス(成長曲線だったり、キャリアだったり。そのへんは感覚)が実現できたかということを特に意識しており、毎月その振り返りを月末に行っている。
トータルで振り返ってみると、大きな結果こそ出せなかったかもしれないが、計画自体に大きな間違いはなかったかなという感じがする。
なので2021年も継続的に今の運用を続けていくことにする。
2021年の抱負としては、「不得意を得意に」という進◯ゼミのようなテーマにすることにした。
これまでアプリケーションに関わる部分にばかり投資してきたので、それ以外の領域(得にインフラ)に積極的に時間を割くことにしようと考えている。
]]>この記事はMakuake Advent Calendar 2020の20日目の記事です。
TerraformとAnsibleを組み合わせてVPS上でサーバー構築をしてみたのでその手順をまとめておこうと思います。
趣味で開発しているアプリケーションのインフラ環境をIaCで整備したかったので、勉強を兼ねてTerraformを使ってみました。
ローカルでTerraformを実行してConoHa VPSにサーバーを建てたり、壊したりします。
OpenStackはIaaS環境を構築するためのOSSです。
ConoHaのVPSはOpenStackを採用しており、OpenStack準拠のAPIが用意されています。
cf. www.slideshare.net - "ConoHa" VPS-KVM; OpenStack Grizzly based service
TerraformでOpenStackのproviderを利用することでConoHa VPSにサーバーを構築することができます。
cf. conoha.jp - API
今回はTerraformでOpenStackのproviderを使いますが、AnsibleにもOpenStack Ansibleモジュールというのがあるので、同様のことはAnsibleだけでも実現可能だとは思います。試してはいないですが・・
cf. docs.ansible.com - OpenStack Ansible モジュール
今回作成したソースコードは、github.com - bmf-san/terraform-ansible-openstack-boilerplateに置いてあります。
Terraformでサーバー構築をして、Ansibleでサーバーの初期セットアップをします。
TerraformとAnsibleを両方使う場合は、TerraformからAnsibleを呼ぶのか、AnsibleからTerraformを呼び出すべきなのか迷う気がしますが、下記の記事ではどちらでも良い、正解不正解は特にないという見解でした。
cf. www.redhat.com - HASHICORP TERRAFORM AND RED HAT ANSIBLE AUTOMATION
Terraformはインフラリソースの設定管理、Ansibleはサーバー内の構成管理にそれぞれ強みがあるイメージなので、それぞれが得意な領域を担当できるようには意識しつつ、Terraform内でAnsibleを実行する構成にしてみました。
Terraform、Ansibleそれぞれの役割を意識した上で、コードをどう管理していきたいかという方針によっては逆のパターンが良いという場合もあるのではないかなと思います。
大まかな流れは以下の通りです。
ConoHaのAPI利用のためのAPIトークン取得
↓
利用したいイメージ、VMプランを決める
↓
Terraformのコードを書く
↓
Ansibleのコードを書く
まずはConoHaのAPIを利用するためのAPIトークンを取得します。
APIのエンドポイントはユーザーごとに異なるのでConoHaコントロールパネルのAPI情報にあるエンドポイントのリストを適宜参照してください。
curl -X POST \
-H "Accept: application/json" \
-d '{"auth":{"passwordCredentials":{"username":"USER_NAME","password":"PASSWORD"},"tenantId":"TENANT_ID"}}' \
https://identity.tyo2.conoha.io/v2.0/tokens \
| jq ".access.token.id"
取得したAPIトークンを使ってそれぞれの情報を取得して、利用したいイメージとVMプランを決めます。
利用可能なイメージ一覧を取得します。
curl -X GET \
-H 'Content-Type: application/json' \
-H "Accept: application/json" \
-H "X-Auth-Token: API_TOKEN" \
https://compute.tyo2.conoha.io/v2/TENANT_ID/images \
| jq ".images | sort_by(.name) | map(.name)"
今回はvmi-ubuntu-20.04-amd64-30gb
を使いました。
利用可能なVMプラン一覧を取得します。
curl -X GET \
-H 'Content-Type: application/json' \
-H "Accept: application/json" \
-H "X-Auth-Token: API_TOKEN" \
https://compute.tyo2.conoha.io/v2/TENANT_ID/flavors \
| jq ".flavors | sort_by(.name) | map(.name)"
今回はg-1gb
を選択しました。
1gb以下のプランだとディスクサイズが足りずに構築エラーになるようです。(g-512mb
で試しましたがダメでした。)
必要な情報が揃ったのでコードを書いていきます。
今回は以下のようなディレクトリ構成にしました。
.
├── ansible.cfg
├── main.tf
├── playbooks
├── templates
│ └── playbooks
│ ├── hosts.tpl
│ └── setup.tpl
├── terraform.tfvars
└── variable.tf
3 directories, 12 files
今回はやることが少ないのでtfファイルは特に細かく分割していません。
tfstateファイルの管理については、backendを使って外部ストレージで管理するのが良いかと思いますが、今回はローカルからの実行なので.gitignore
対象に含めるだけになっています。(ローカルとはいえちゃんとやっておきたい部分ではありますが..)
後述しますが、playbooks
にはterraformがtemplates
から生成するhosts
ファイルとsetup
ファイル(yml)が配置されます。
ゼロからインスタンスを構築するので、構築過程でIPアドレスの値を拾ってTerraformからAnsibleに値を渡してあげる必要があるため、hosts
ファイルについてはテンプレ化しておく意義があるかなと思うのですが、setup
ファイル(yml)についてはタスクと変数定義を分けて、変数定義をするファイルをテンプレ化したほうが良いかなと思います。今回は端折って分割していません。
Terraformに寄せすぎると後でAnsibleを切り出したいとなった時などに腰が重くなるような気がするので、この辺りは色んな事例を知りたいところです。
main.tf
の中身はこんな感じです。
terraform {
required_version = ">= 0.14"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "1.33.0"
}
}
}
provider "openstack" {
user_name = (var.user_name)
password = (var.password)
tenant_name = (var.tenant_name)
auth_url = (var.auth_url)
}
resource "openstack_compute_keypair_v2" "example_keypair" {
name = (var.keypair_name)
public_key = file(var.path_to_public_key_for_root)
}
resource "openstack_compute_instance_v2" "example_instance" {
name = (var.instance_name)
image_name = (var.image_name)
flavor_name = (var.flavor_name)
key_pair = (var.keypair_name)
security_groups = [
"gncs-ipv4-ssh",
"gncs-ipv4-web",
]
metadata = {
instance_name_tag = (var.instance_name_tag)
}
}
data "template_file" "hosts" {
template = file("./templates/playbooks/hosts.tpl")
vars = {
host = (var.host)
ip = (openstack_compute_instance_v2.example_instance.access_ip_v4)
}
depends_on = [openstack_compute_instance_v2.example_instance]
}
resource "local_file" "save_hosts" {
content = (data.template_file.hosts.rendered)
filename = "./playbooks/hosts"
depends_on = [openstack_compute_instance_v2.example_instance]
}
data "template_file" "setup" {
template = file("./templates/playbooks/setup.tpl")
vars = {
host = (var.host)
new_user_name = (var.new_user_name)
new_user_password = (var.new_user_password)
shell = (var.shell)
new_user_public_key = file(var.path_to_public_key)
port = (var.port)
}
depends_on = [openstack_compute_instance_v2.example_instance]
}
resource "local_file" "save_setup" {
content = (data.template_file.setup.rendered)
filename = "./playbooks/setup.yml"
depends_on = [openstack_compute_instance_v2.example_instance]
}
resource "null_resource" "example_provisoner" {
provisioner "local-exec" {
command = "ansible-playbook ./playbooks/setup.yml -i ./playbooks/hosts --private-key=${var.path_to_private_key_for_root}"
}
depends_on = [openstack_compute_instance_v2.example_instance]
}
公式でopenstackのproviderがあるのでそれを使っています。
provider "openstack" {
user_name = (var.user_name)
password = (var.password)
tenant_name = (var.tenant_name)
auth_url = (var.auth_url)
}
user_name
、password
はConoHaで作成したAPIユーザーの情報になります。tenant_name
は文字通りテナント名です。auth_url
はわかりづらいのですが、ここではConoHaのIdentity APIのエンドポイント(ex. https://identity.tyo2.conoha.io/v2.0
)になります。
インスタンス構築時にrootユーザーが利用する公開鍵・秘密鍵のキーペアのセットアップです。
cf. registry.terraform.io - openstack_compute_keypair_v2
resource "openstack_compute_keypair_v2" "example_keypair" {
name = (var.keypair_name)
public_key = file(var.path_to_public_key_for_root)
}
公開鍵を指定しない場合は公開鍵・秘密鍵のキーペアが自動で生成され仕組みになっています。
鍵情報はtfstateファイルに出力されるため、実環境で実行する場合はtfstateファイルを適切に管理する必要があります。
公開鍵認証が前提になっていますが、パスワード認証を可能にする方法も無いこともないみたいです。
cf. noaboutsnote.hatenablog.com - 【Openstack】インスタンスOSにパスワードログインできるようする
インスタンスのイメージやVMプラン、ネットワーク構成などインスタンスを構築するためのセットアップです。
cf. registry.terraform.io - openstack_compute_instance_v2
resource "openstack_compute_instance_v2" "example_instance" {
name = (var.instance_name)
image_name = (var.image_name)
flavor_name = (var.flavor_name)
key_pair = (var.keypair_name)
security_groups = [
"gncs-ipv4-ssh",
"gncs-ipv4-web",
]
metadata = {
instance_name_tag = (var.instance_name_tag)
}
}
instance_name
は任意の名前、image_name
は文字通りイメージ名です。flavor_name
は初見だと察しが付きづらいですが、ここではVMプラン名になります。
instance_name_tag
の部分は、ConoHaのコントロールパネルで表示されるネームタグになります。
今回は使用していませんが、user_dataを指定すればcloud-initを使うこともできます。
ex.
user_data = data.template_file.user_data.rendered
data "template_file" "user_data" {
template = file("user_data.sh")
}
null_resourceは他のresourceをトリガとしてプロビジョニングを行うresourceです。トリガはdepends_onで指定します。
構築したインスタンスにAnsibleでプロビジョニングを行いたいので、インスタンスの構築完了(Terraformの実行が完了というのが正確かもしれません。Terraformの実行が終了してもインスタンスの構築が完了しているわけではないので、後述しますがAnsibleでインスタンスの構築を待つ処理を用意しています。)をトリガとしています。
resource "null_resource" "example_provisoner" {
provisioner "local-exec" {
command = "ansible-playbook ./playbooks/setup.yml -i ./playbooks/hosts --private-key=${var.path_to_private_key_for_root}"
}
depends_on = [openstack_compute_instance_v2.example_instance]
}
今回はローカルで実行するのでlocal-exec
を使っています。
ちょうど良い感じのresourceがないかと調べたところ、github.com - jonmorehouse/terraform-provisioner-ansibleというのがありましたが、現在はメンテナンスされていないようでした。
今回はtemplates/playbooks
配下にテンプレートを用意して、Terraform実行時にテンプレを元に実ファイルを生成、生成したファイルを使ってAnsibleを実行する形を取りました。
プロビジョニングの内容は、実行ユーザーの作成、ssh周りの設定調整くらいです。
インスタンスの疎通を待たずしてプロビジョニングしようとしてハマりました...(wait_for_connection
を使って対応しました。)
terraformコマンドオンリーで完結です。
terraform init
terraform plan
terraform apply
terraform show
ssh username@ipaddress -i path_to_private_key
terraform destroy
初Terraformだったので良い勉強になりました。
TerraformはともかくOpenStackは面白い技術だなと思ったのでもう少し深堀りする機会を作りたいです。
docker-compose.ymlのserviceの1つにenv_file
を指定し、環境変数を設定したが、サービスがbuildするコンテナ内(Dockerfile側)では参照できなかった。
vueのアプリケーションをコンテナ内でnpmを使ってビルドしており、アプリケーション側でprocess.env.VUE_APP_API_ENDPOINT
という形でアプリケーションのビルド時に環境変数を参照させたかった。
docker-compose.ymlで指定するenv_file
やenvironment
といったキーはコンテナのビルド後に参照できるようになるため、それらのキーを利用するだけではコンテナビルド中では参照することができない。
docker-compose.ymlでargs
キーを指定し、変数をコンテナに渡すことで解決した。
.env
VUE_APP_API_ENDPOINT="http://gobel-api.local"
Dockerfile
FROM node:14.3.0-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# 引数を受け取ってコンテナ内で環境変数を定義
ARG VUE_APP_API_ENDPOINT
ENV VUE_APP_API_ENDPOINT=${VUE_APP_API_ENDPOINT}
# アプリケーションのビルド。環境変数を参照できる。
RUN npm run local-build
FROM nginx:1.19.0-alpine
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./nginx/conf.d/gobel-admin-client.conf /etc/nginx/conf.d/gobel-admin-client.conf
COPY --from=build-stage /app/dist /var/www/html
docker-compose.yml
version: "3.8"
services:
app:
container_name: "gobel-admin-client"
# 環境変数はファイルから読み込む
env_file: ".env"
build:
context: "./app"
dockerfile: "Dockerfile"
# 変数をコンテナのビルド時に渡す
args:
VUE_APP_API_ENDPOINT: $VUE_APP_API_ENDPOINT
ports:
- "82:80"
networks:
- gobel_link
networks:
gobel_link:
external: true
参考までにビルド時に環境変数を参照したいアプリケーション側のコードを記載。
const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_ENDPOINT,
headers: {
"Content-Type": "application/json"
},
responseType: "json"
});
最近認証サービスの開発に携わっているので今一度基本的なことを再確認しておく意味でまとめる。
Software Design 2020 11月号の認証・認可の特集を参考にしている。
Software Design 2020 11月号の認証・認可の特集が分かりやすかった。
]]>ネットワークに関して知識が曖昧なワードをまとめる。
cf.
cf.
cf.
cf.
cf.
cf.
cf.
cf.
cf.
cf.
]]>ISUCON環境を利用してシステムメトリクスをちゃんと見れるようなろうという勉強会を定期的に行っているので、そのまとめを残す。
Webエンジニアが知っておきたいインフラの基本 インフラの設計から構成、監視、チューニングまでを参考図書とし、第5章以降の内容を実際に手を動かして確認するような形で行っている。
環境はConohaでISUCON8のイメージを利用してサーバーを立てている。
プラン:メモリ 512MB/CPU 1Core
[root@160-251-16-96 ~]# cat /etc/redhat-release
CentOS Linux release 7.5.1804 (Core)
cf. 第5章
サーバーの待受ポートとファイアウォールの設定を確認して、開放されているポートを把握する。
iptables -nv -L
今回の環境では22と80が開放されていることが確認できた。
ubuntuでプリインストールされているufw(Uncomplicated FireWall)はiptablesのラッパー。
続いて、外部からの待受ポートを確認する。
ネットワークの状態確認コマンドであるss(旧netstat)を使って確認。
ss -lnp
h2oが80でIPアドレスなしで待受、isuconアプリケーションが8080でIPアドレスなしで待受、mysqldが3306でIPアドレスなしで待受、sshdが22でIPアドレスなしで待受しているのが確認できた。
Local Address:Portの項目で最初に::がついているものはIPv6でも待受していることになる。
ssの代用でlsofを使うでも良いと思う。
lsof -i
lsof -i:ポート番号
プロセスを見て起動コマンドを確認。
ps aufx | grep -v grep | grep -e isucon -e h2o -e mysql
isuconがisuconユーザーで/home/isucon/torb/webapp/perl/local/bin/plackup
、h2oがrootでperl -x /usr/share/h2o/start_server --pid-file=/var/run/h2o/h2o.pid --log-file=/var/log/h2o/error.log --port=0.0.0.0:80 -- /usr/sbin/h2o -c /etc/h2o/h2o.conf
、mysqlがmysqlユーザーで/bin/sh /usr/bin/mysqld_safe --basedir=/usr
で起動されていることが確認できた。
ディスク使用量の確認。
df -h
ディスク容量は30Gで22%使用されていることが確認できた。
こうすると容量を使っているディレクトリをリストアップできる。
df -sh /*
CPU利用率、メモリ利用量、CPU使用率が高いプロセスを確認。
top -b -d 1 -n 1
CPU使用率、ネットワーク利用量、ディスクI/O量、ページング量と推移を確認。
dstat -taf 1 10
ベンチマーカー実行しながら確認するとディスクI/Oに負荷がかかっていることが確認できた。
// TODO:: 随時更新中
]]>Kubernetesを本格的にキャッチアップしていくためにドキュメントを読んだので、オレオレメモを残す。
全部は長いのでメモ書きはコンセプトの章だけにする。
cf. Kubernetesとは何か?
cf. Kubernetes API
cf. オブジェクトの名前とID
cf. Namespace(名前空間
cf. ノード
cf. Pooの外観
cf. ReplicaSet
cf. Deployment
cf. StatefulSet
cf. DaemonSet
cf. Job
cf. Service
cf. 設定
cf. https://kubernetes.io/ja/docs/concepts/security/
メモはかなり端折っている。ドキュメント読みきるのにそこそこ時間を使った。。。
Kubernetesドキュメントを読み進めていく中で外部資料にも目を通したので参考になったものをメモ。
]]>Golangのインメモリキャッシュのライブラリは良さそうなものが存在するが、軽量でシンプルなもので十分だったので自前で実装してみた。
※github.com - bmf-san/go-snippets/architecture_design/cache/cache.goに置いてあるが、転載。
最初に思いついた感じで実装したもの。
package main
import (
"fmt"
"log"
"sync"
"time"
)
// Cache is a struct for caching.
type Cache struct {
value sync.Map
expires int64
}
// Expired determines if it has expired.
func (c *Cache) Expired(time int64) bool {
if c.expires == 0 {
return false
}
return time > c.expires
}
// Get gets a value from a cache. Returns an empty string if the value does not exist or has expired.
func (c *Cache) Get(key string) string {
if c.Expired(time.Now().UnixNano()) {
log.Printf("%s has expired", key)
return ""
}
v, ok := c.value.Load(key)
var s string
if ok {
s, ok = v.(string)
if !ok {
log.Printf("%s does not exists", key)
return ""
}
}
return s
}
// Put puts a value to a cache. If a key and value exists, overwrite it.
func (c *Cache) Put(key string, value string, expired int64) {
c.value.Store(key, value)
c.expires = expired
}
var cache = &Cache{}
func main() {
fk := "first-key"
sk := "second-key"
cache.Put(fk, "first-value", time.Now().Add(2*time.Second).UnixNano())
s := cache.Get(fk)
fmt.Println(cache.Get(fk))
time.Sleep(5 * time.Second)
// fk should have expired
s = cache.Get(fk)
if len(s) == 0 {
cache.Put(sk, "second-value", time.Now().Add(100*time.Second).UnixNano())
}
fmt.Println(cache.Get(sk))
}
ロックの処理を気にしなくてよいsync.Map
が便利で良いなぁと思っていたのだが、データ構造や機能的に要件を満たせていないので却下。
※github.com - bmf-san/go-snippets/architecture_design/cache/cache.goに置いてあるが、転載。
要件を満たす実装をしたバージョン。
package main
import (
"fmt"
"log"
"net/http"
"sync"
"time"
)
// item is the data to be cached.
type item struct {
value string
expires int64
}
// Cache is a struct for caching.
type Cache struct {
items map[string]*item
mu sync.Mutex
}
func New() *Cache {
c := &Cache{items: make(map[string]*item)}
go func() {
t := time.NewTicker(time.Second)
defer t.Stop()
for {
select {
case <-t.C:
c.mu.Lock()
for k, v := range c.items {
if v.Expired(time.Now().UnixNano()) {
log.Printf("%v has expires at %d", c.items, time.Now().UnixNano())
delete(c.items, k)
}
}
c.mu.Unlock()
}
}
}()
return c
}
// Expired determines if it has expires.
func (i *item) Expired(time int64) bool {
if i.expires == 0 {
return true
}
return time > i.expires
}
// Get gets a value from a cache.
func (c *Cache) Get(key string) string {
c.mu.Lock()
var s string
if v, ok := c.items[key]; ok {
s = v.value
}
c.mu.Unlock()
return s
}
// Put puts a value to a cache. If a key and value exists, overwrite it.
func (c *Cache) Put(key string, value string, expires int64) {
c.mu.Lock()
if _, ok := c.items[key]; !ok {
c.items[key] = &item{
value: value,
expires: expires,
}
}
c.mu.Unlock()
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fk := "first-key"
sk := "second-key"
cache := New()
cache.Put(fk, "first-value", time.Now().Add(2*time.Second).UnixNano())
fmt.Println(cache.Get(fk))
time.Sleep(10 * time.Second)
if len(cache.Get(fk)) == 0 {
cache.Put(sk, "second-value", time.Now().Add(100*time.Second).UnixNano())
}
fmt.Println(cache.Get(sk))
})
http.ListenAndServe(":8080", nil)
}
sync.Map
が便利なので使いたかったのだが、sync.Map
にキャッシュデータを保持させるとキャッシュキー指定なしにキャッシュデータの期限切れチェック・削除が難しいため、キャッシュデータの保持にはmap
を採用することにした。
期限切れチェックはticker
を使ってインターバルを置いてチェックするようにしている。
上記では1秒毎のインターバルとなっている。
上記の実装ではキャッシュ期限+1秒経過するまではキャッシュにアクセスすることができてしまうので、実際のキャッシュ期限はexpiresで指定した時間+インターバルになっている。
Golangでの並行処理やロックに入門する良い機会だった。
github.com - CodeReviewCommentsを読んでメモしておきたいことをまとめる。
math/rand
を使ってはいけない。crypto/rand
のReader
を使う。// 長さ0のスライス
t := []string{}
よりも、
// nilのスライス
var t []string
のほうを使うようにする。
JSONオブジェクトのエンコード時、nil
はnull
に変換されるが、[]string{}
は[]
に変換される。
インターフェース設計では両者を区別しないほうがよい。分かりづらいミスを誘発する可能性があるため。
panic
を使うのは避け、error
型を含んだ複数の値を返すようにする。import _ "pkg"
ではパッケージをインポートした際の副作用を利用することができる。package foo_test
import (
"bar/testutil" // fooでもimportされている
. "foo" // foo_testをfooの一部のように見せる
)
メソッドのレシーバをポインタにするか、値にするかの基準。
迷う場合はポインタにしておく。
map
、func
、channel
ならポインタは避ける。slice
で、メソッドがslice
を作り直さない場合は、ポインタを避ける。time.Time
)、変更するフィールドやポインタがない構造体や配列、あるいはint
やstring
のような型の場合は、レシーバが値であるほうが良い場合もある。sync.Mutex
か、同期するようなフィールドを持つ構造体ならレシーバはポインタにする。テストが失敗したときに伝えるべきメッセージ。
# 参考
]]>前回は2人チームで参加したが、今回は3人チームで参加した。
予選敗退。前回はfailで0点だったが、今回は点数を残せたので僅かでも成長しただろうか。
序盤のオペレーションにだいぶ慣れを感じたり、(半年ちょい前くらいからチームでISUCON練習会をしていた)ボトルネックと格闘するところ(解消ができていないが・・)でもちょっとは成長を感じたが、まだまだやれることとやるべきことをやりきれていないので修行が必要・・
チームでKPTをしたら来年に向けた修行を開始する予定なのでまた来年もよろしくお願いします・・!
運営の皆様、ありがとうございました!!
JWTについて調べたことをまとめておく。
OAuthやOpen ID Connectなど実際の利用事例については触れない。
JWT(JSON Web Token)は、JSONデータ構造を用いたURLセーフなClaim(JWTを用いたJSONオブジェクトはClaim Setと呼ばれる)を表現するフォーマット。
JWTでは、デジタル署名またはメッセージ認証コード(MAC)を用いたJWS(JSON Web Signature)、あるいは暗号化を用いたJWE(JSON Web Encryption)が利用される。
JWT、JWS、JWEのそれぞれのRFCは下記の通り。
ietf.org - rfc7519 JSON Web Token
ietf.org - rfc7516 JSON Web Encryption
ietf.orf - rfc7515 JSON Web Signature
その他の関連するRFCとしては、以下のようなものもある。
ietf.org - rfc7517 JSON Web Key
JWSやJWE,JWKの仕様で仕様される暗号化アルゴリズムと識別子に関する仕様。
ietf.org - rfc7518 JSON Web Algorithm
上記の仕様をまとめてJWxと呼ばれることがあるらしい。
jwt.ioでJWTのエンコードとデコードをUIで体験することができる。
JWTの例が下記。
ピリオドで区切られた3つのセクションはそれぞれ、ヘッダー.ペイロード.署名
の役割を担っている。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
ヘッダー、ペイロード、署名の順にデコードしたものが下記。
{
"alg": "HS256",
"typ": "JWT",
"alg": "HS256"
}
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
ヘッダーには、署名の検証を行うためのデータ(JSONをBase64エンコードした文字列)が含まれる。
ペイロードはClaim(JSONをBase64エンコードした文字列)を含む。
Claimには以下の3種類がある。
www.iana.org - jwtに登録済みのClaim。
必須ではなく、推奨。
Claimの種類はietf.org - rfc7519 JSON Web Tokenを参照。
JWTを使用するユーザーが自由に定義することができるClaimだが、衝突防止のため、www.iana.org - jwtに登録するか、別途対応をする必要がある。
JWTを使用する当事者間で自由に定義することができる。Registerd ClaimやPublic Claimで予約されているもの以外に限る。
トークンの改ざん検証のためのデータを含む。
JWTの扱いについての注意点は以下の記事がよくまとまっているので一読しておきたい。
auth0.com - JWT の最新ベスト プラクティスに関するドラフトを読み解く
JWSを用いたJWTの実装をGolangでやってみる。
コードはgithub.com - bmf-san/go-snippetsにも置いてある。
GolangでJWTを扱うためにgithub.com - dgrijalva/jwt-goというパッケージを利用している。
// Refered to https://github.com/EricLau1/go-api-login
package main
import (
"encoding/json"
"fmt"
"html"
"log"
"net/http"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"golang.org/x/crypto/bcrypt"
)
const (
// Actualy, thease values comes from a form or something for getting user infomations.
//These values are require validation.
userName = "bmf"
userEmail = "foobar@example.com"
userPass = "password"
)
var secretKey = []byte("thisisexampleforauthjwt")
// ex.
// curl -X POST -H 'Content-Type:application/json' http://localhost:9999/login
func login(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
// Actualy, you need to get values from a form or somthing. ex. name, email, password.
token, err := signIn(userEmail, userPass)
if err != nil {
toJSON(w, err.Error(), http.StatusUnauthorized)
return
}
toJSON(w, token, http.StatusOK)
return
}
toJSON(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
func toJSON(w http.ResponseWriter, data interface{}, statusCode int) {
w.Header().Set("Content-type", "application/json; charset=UTF8")
w.WriteHeader(statusCode)
err := json.NewEncoder(w).Encode(data)
if err != nil {
log.Fatal(err)
}
}
func signIn(userEmail string, userPass string) (string, error) {
// Actualy, this values stored in a something storage so you need to get it from a something storage by using a something key.
// ex. user := model.GetByEmail(email) → user.password
// Here, hash a userPass for password verification(bcrypt.VerifyPassword).
hashedUserPass, err := bcrypt.GenerateFromPassword([]byte(userPass), bcrypt.DefaultCost)
if err != nil {
return "", err
}
err = bcrypt.CompareHashAndPassword([]byte(hashedUserPass), []byte(userPass))
if err != nil {
return "", err
}
// If password verification is ok, creates and returns a jwt.
jwt, err := generateJWT()
if err != nil {
return "", err
}
return jwt, nil
}
func generateJWT() (string, error) {
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["authorized"] = true
claims["user_email"] = userEmail
claims["exp"] = time.Now().Add(time.Minute * 30).Unix()
return token.SignedString(secretKey)
}
func jwtExtract(r *http.Request) (map[string]interface{}, error) {
headerAuthorization := r.Header.Get("Authorization")
bearerToken := strings.Split(headerAuthorization, " ")
tokenString := html.EscapeString(bearerToken[1])
claims := jwt.MapClaims{}
_, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return secretKey, nil
})
if err != nil {
return nil, err
}
return claims, nil
}
// ex.
// curl -H http://localhost:9999/public
func public(w http.ResponseWriter, r *http.Request) {
toJSON(w, "public page", http.StatusOK)
return
}
// ex.
// curl -H 'Content-Type:application/json' -H "Authorization:Bearer <JWT>" http://localhost:9999/private
func private(w http.ResponseWriter, r *http.Request) {
jwtParams, err := jwtExtract(r)
if err != nil {
toJSON(w, err.Error(), http.StatusUnauthorized)
return
}
email, ok := jwtParams["user_email"].(string)
if !ok {
toJSON(w, "payload invalid", http.StatusUnauthorized)
return
}
toJSON(w, email, http.StatusOK)
return
}
func middlewareAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authorizationHeader := r.Header.Get("Authorization")
if authorizationHeader != "" {
bearerToken := strings.Split(authorizationHeader, " ")
if len(bearerToken) == 2 {
token, err := jwt.Parse(bearerToken[1], func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unauthorized")
}
return secretKey, nil
})
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(err.Error()))
return
}
if token.Valid {
next.ServeHTTP(w, r)
}
} else {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
}
}
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/login", login)
mux.HandleFunc("/public", public)
mux.HandleFunc("/private", middlewareAuth(private))
if err := http.ListenAndServe(":9999", mux); err != nil {
fmt.Println(err)
}
}
GolangでgRPCに入門する。
gRPCとは、Googleが開発したRPC※実現のためのプロトコル。HTTP/2の利用を前提としている。
gRPCではGoogleが開発しているProtocol BuffersというIDL(インターフェース定義言語)でAPI仕様を定義するシリアライズフォーマットを使用する。
gRPCの通信方式にはHTTP/2の仕様に則った4つのパターンがある。
gRPCのメリットについては以下のような点がある。
一方でデメリットについては以下のような点が考えられる。
上記のようなメリット・デメリットからマイクロサービス間の通信などでよく採用されているケース(通信速度や複数言語の採用など相性が良い)が見受けられる。
※RPCについてはWikipediaの一文を引用する。
遠隔手続き呼出し(英: remote procedure call、リモートプロシージャコール、略してRPC)とは、プログラムから別のアドレス空間(通常、共有ネットワーク上の別のコンピュータ上)にあるサブルーチンや手続きを実行することを可能にする技術
感覚的な説明を加えるとすると、「あるホストから別のホスト上のプログラム定義されたメソッドを実行することができる」というな感じだろうか。この辺はコードを見るとわかりやすかと思う。
ソースコードはgithub.com - golang-grpc-example
.
├── LICENSE
├── README.md
├── client
│ └── main.go
├── go.mod
├── go.sum
├── pkg
│ ├── proto
│ │ └── user
│ │ ├── user.pb.go
│ │ └── user.proto
│ └── service
│ └── user.go
└── server
└── main.go
6 directories, 9 files
前提として、Golangのバージョンは1.6以上である必要ある。
gRPCのインストールgo get -u google.golang.org/grpc
Protocol Buffers v3のインストール(Mac以外の場合はgithub.com/protocolbuffers/protobuf/releases
を参照)brew install protobuf
Golangのprotocプラグインをインストールgo get -u github.com/golang/protobuf/protoc-gen-go
protoにAPI定義をする。
pkg/proto/user/user.proto
syntax = "proto3";
service User {
rpc GetUser (GetUserRequest) returns (GetUserResponse) {}
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
string name = 1;
}
protoc --go_out=plugins=grpc:./ user.proto
上記を実行すると以下のコードが生成される。
pkg/proto/user/user.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.25.0
// protoc v3.13.0
// source: user.proto
package user
import (
context "context"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
type GetUserRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
}
func (x *GetUserRequest) Reset() {
*x = GetUserRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_user_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetUserRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUserRequest) ProtoMessage() {}
func (x *GetUserRequest) ProtoReflect() protoreflect.Message {
mi := &file_user_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead.
func (*GetUserRequest) Descriptor() ([]byte, []int) {
return file_user_proto_rawDescGZIP(), []int{0}
}
func (x *GetUserRequest) GetType() string {
if x != nil {
return x.Type
}
return ""
}
type GetUserResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *GetUserResponse) Reset() {
*x = GetUserResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_user_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetUserResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetUserResponse) ProtoMessage() {}
func (x *GetUserResponse) ProtoReflect() protoreflect.Message {
mi := &file_user_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetUserResponse.ProtoReflect.Descriptor instead.
func (*GetUserResponse) Descriptor() ([]byte, []int) {
return file_user_proto_rawDescGZIP(), []int{1}
}
func (x *GetUserResponse) GetName() string {
if x != nil {
return x.Name
}
return ""
}
var File_user_proto protoreflect.FileDescriptor
var file_user_proto_rawDesc = []byte{
0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x24, 0x0a, 0x0e,
0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12,
0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79,
0x70, 0x65, 0x22, 0x25, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x32, 0x36, 0x0a, 0x04, 0x55, 0x73, 0x65,
0x72, 0x12, 0x2e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0f, 0x2e, 0x47,
0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e,
0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_user_proto_rawDescOnce sync.Once
file_user_proto_rawDescData = file_user_proto_rawDesc
)
func file_user_proto_rawDescGZIP() []byte {
file_user_proto_rawDescOnce.Do(func() {
file_user_proto_rawDescData = protoimpl.X.CompressGZIP(file_user_proto_rawDescData)
})
return file_user_proto_rawDescData
}
var file_user_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_user_proto_goTypes = []interface{}{
(*GetUserRequest)(nil), // 0: GetUserRequest
(*GetUserResponse)(nil), // 1: GetUserResponse
}
var file_user_proto_depIdxs = []int32{
0, // 0: User.GetUser:input_type -> GetUserRequest
1, // 1: User.GetUser:output_type -> GetUserResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_user_proto_init() }
func file_user_proto_init() {
if File_user_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetUserRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_user_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetUserResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_user_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_user_proto_goTypes,
DependencyIndexes: file_user_proto_depIdxs,
MessageInfos: file_user_proto_msgTypes,
}.Build()
File_user_proto = out.File
file_user_proto_rawDesc = nil
file_user_proto_goTypes = nil
file_user_proto_depIdxs = nil
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6
// UserClient is the client API for User service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type UserClient interface {
GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error)
}
type userClient struct {
cc grpc.ClientConnInterface
}
func NewUserClient(cc grpc.ClientConnInterface) UserClient {
return &userClient{cc}
}
func (c *userClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) {
out := new(GetUserResponse)
err := c.cc.Invoke(ctx, "/User/GetUser", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// UserServer is the server API for User service.
type UserServer interface {
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
}
// UnimplementedUserServer can be embedded to have forward compatible implementations.
type UnimplementedUserServer struct {
}
func (*UnimplementedUserServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented")
}
func RegisterUserServer(s *grpc.Server, srv UserServer) {
s.RegisterService(&_User_serviceDesc, srv)
}
func _User_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServer).GetUser(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/User/GetUser",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServer).GetUser(ctx, req.(*GetUserRequest))
}
return interceptor(ctx, in, info, handler)
}
var _User_serviceDesc = grpc.ServiceDesc{
ServiceName: "User",
HandlerType: (*UserServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetUser",
Handler: _User_GetUser_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "user.proto",
}
ドキュメントを生成したい場合は、github.com - pseudomuto/protoc-gen-docというツールを使うと便利。
user.pb.goの以下のインターフェースを満たすようにサービス(実処理部分)を実装していく。
pkg/proto/user/user.pb.go
// UserServer is the server API for User service.
type UserServer interface {
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
}
user_service.go
package service
type UserService struct {}
func (s *UserService) GetUser(ctx context.Context, message *pb.GetUserRequest) (*pb.UserResponse, error) {
switch message.Id {
case "admin":
return &pb.GetUserResponse{
Name: "admin_user",
}, nil
case "general":
return &pb.GetUserResponse{
Name: "general_user",
}, nil
}
return nil, errors.New("No user")
}
user.pb.goを参照して、サーバーとクライアントを実装する。
server/main.go
package main
import (
"log"
"net"
"github.com/bmf-san/golang-grpc-example/pkg/proto/user"
"github.com/bmf-san/golang-grpc-example/pkg/service"
grpc "google.golang.org/grpc"
)
func main() {
var p net.Listener
var err error
if p, err = net.Listen("tcp", ":19003"); err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
userService := &service.UserService{}
user.RegisterUserServer(s, userService)
s.Serve(p)
}
client/main.go
package main
import (
context "context"
"fmt"
"log"
"github.com/bmf-san/golang-grpc-example/pkg/proto/user"
grpc "google.golang.org/grpc"
)
func main() {
var conn *grpc.ClientConn
var err error
if conn, err = grpc.Dial("127.0.0.1:19003", grpc.WithInsecure()); err != nil {
log.Fatal(err)
}
defer conn.Close()
c := user.NewUserClient(conn)
req := &user.GetUserRequest{
Type: "admin",
}
res, err := c.GetUser(context.TODO(), req)
fmt.Printf("result:%#v \n", res.Name)
fmt.Printf("error::%#v \n", err)
}
サーバー起動go run server/main.go
クライアント実行go run client/main.go
result:"admin_user"
error::<nil>
何となくの雰囲気は掴めた。
コード生成で楽ができるのでAPIの本質的な部分に集中しやすそう。
テストやデバッグ周りは少し慣れが必要になのかなという印象。
リトルエンディアンとビッグエンディアンの違いについてまとめる。
Dockerを触っていたらorphan(孤児の意)というプロセスの存在を知ったのでゾンビプロセスとの違いを調べてみた。
ps aux
でstatがZ、末尾がdefunctのものがゾンビプロセスps -ef | grep defunct
でゾンビプロセスだけ出力ps -elf | head -1; ps -elf | awk '{if ($5 == 1 && $3 != "root") {print $0}}' | head
Golang×chromedp×slack botでslackの絵文字自動生成ボットをつくってみた。
slackでbotにパラメータを付けたメンションを飛ばすと画像を生成してくれるだけのもの。
内部的には、パラメータを元にcanvasで画像を生成、ヘッドレスブラウザでスクショを撮って画像を保存、slackに投稿、といった感じ。
github.com - emoji-generator-slack-app
使い方等はREADMEを見ればなんとなくわかるはず...
週末にハッカソン的ノリでつくったため、バグが残ってしまっている...
https://github.com/bmf-san/emoji-generator-slack-app/issues/1
Golangには画像処理の一通りの機能が充実しているimageという標準パッケージがある。
モザイク処理を施したり、画像を合成したり、トリミングをしたり、テキストを描画したりといったことが比較的に簡単にできる。(はず。色々みた限りでは。)
ほとんど実用性がないが、例えばベタ塗りされた画像を生成したいなら次のような数行のコードで実現できる。
package main
import (
"image"
"image/color"
"image/jpeg"
"log"
"os"
)
func main() {
x, y := 0, 0
width, height := 400, 400
quality := 100
img := image.NewRGBA(image.Rect(x, y, width, height))
for i := img.Rect.Min.Y; i < img.Rect.Max.Y; i++ {
for j := img.Rect.Min.X; j < img.Rect.Max.X; j++ {
img.Set(j, i, color.RGBA{255, 255, 255, 255})
}
}
file, err := os.Create("sample.jpg")
if err != nil {
log.Println(err)
}
defer file.Close()
if err = jpeg.Encode(file, img, &jpeg.Options{quality}); err != nil {
log.Println(err)
}
}
ベタ塗りの画像ではつまらないので、画像にテキストを描画したければ、次のようなコードで実現できる。
package main
import (
"image"
"image/draw"
"image/jpeg"
"io/ioutil"
"log"
"os"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
)
func main() {
baseFile, err := os.Open("./image/base.jpg")
if err != nil {
log.Println(err)
}
defer baseFile.Close()
baseImage, _, err := image.Decode(baseFile)
if err != nil {
log.Println(err)
}
fontFile, err := ioutil.ReadFile("./font/Roboto-Regular.ttf")
if err != nil {
log.Println(err)
}
parsedFont, err := truetype.Parse(fontFile)
if err != nil {
log.Println(err)
}
r := baseImage.Bounds()
rgbaImage := image.NewRGBA(image.Rect(0, 0, r.Dx(), r.Dy()))
draw.Draw(rgbaImage, rgbaImage.Bounds(), baseImage, r.Min, draw.Src)
drawer := font.Drawer{
Dst: rgbaImage,
Src: image.Black,
}
drawer.Face = truetype.NewFace(parsedFont, &truetype.Options{
Size: 20,
DPI: 350,
})
drawText := "Hello World"
drawer.Dot = fixed.Point26_6{
X: (fixed.I(r.Dx()) - drawer.MeasureString(drawText)) / 2,
Y: fixed.I(r.Dy() / 2),
}
file, err := os.Create("sample_text.jpg")
if err != nil {
log.Println(err)
}
drawer.DrawString(drawText)
if err = jpeg.Encode(file, drawer.Dst, &jpeg.Options{Quality: 100}); err != nil {
log.Println(err)
}
}
上記のコードのように描画のパラメータを上手に調整することで任意の画像を生成することができる。
複雑な幾何学的な模様を作ってみたければ、調整すべきパラーメータの数は増えるし、計算するのも一苦労になるだろうと思う。
slack用のemojiをつくるくらいだったら、imageパッケージでも十分実現できそうな気はするが、パラメータの調整が面倒な気がしたので、もっとわかりやすい形で実現する方法を探っていたところ、ヘッドレスブラウザを用いた画像生成を紹介している記事を見かけたので、ヘッドレスブラウザを使った形で今回は実現してみることにした。
cf. note.com - Goでheadless browserを用いた動的画像生成
上記の記事を見て知ったのだが、imageパッケージが描画できるfont形式はtruetypeしかサポートされていないらしい。
今回はデザインに凝りたいわけではないのであまり気に留めていないが、サービスに合わせてフォントを調整したい場合は注意が必要。
要は、ヘッドレスブラウザを起動してスクショを撮って画像生成とする方法。
imageパッケージを使った方法と比べて、サーバーサイドではなくフロントエンドで画像の調整ができるため、CSSで画像を調整したり、ブラウザが対応するfontを自由に使用したりとデザインの柔軟性が高い。
あとはスクレイピングとかもできるので汎用性が高い。OGP自動生成とか相性が良さそう。
Golangでヘッドレスブラウザ(chrome)を使うためのパッケージとして、今回は、chromedpを利用した。
github.com - chromedp
chromedpはchromeを操作するためのプロトコルであるchrome devtools protocolをサポートしており、SeleniumやPhantomJSといった外部依存なしに、UI付きでもヘッドレスでもchromeを操作することができるパッケージ。
chrome devtools protocol
chromedpを使ってスクリーンショットをヘッドレスで撮影するコードはこんな感じに書くことできる。
// https://github.com/bmf-san/emoji-generator-slack-appの一部のコードを抜粋
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var buf []byte
if err := chromedp.Run(ctx, chromedp.Tasks{
chromedp.Navigate(`http://localhost:9999/generator?` + query.Encode()),
chromedp.Sleep(2 * time.Second),
chromedp.WaitVisible(`#target`, chromedp.ByID),
chromedp.Screenshot(`#target`, &buf, chromedp.NodeVisible, chromedp.ByID),
}); err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to take a screen shot."))
return
}
後はchromedpを使って撮影したスクショをファイルに書き込んで、データをslackに投稿すればbotの一連の仕事ができあがる。
slack botの開発については割愛する。
こちらの記事が分かりやすい。
cf. qiita.com - Go で Slack Bot を作る (2020年3月版)
今回はパラメータ付きのメンションが来たら、パラメータを入力として受け取って、画像生成、画像を投稿するだけのbotを作成した。
dialogやslash commandといった機能は使わず、event subscriptionだけ。
dialogを使うのがUX的に良さそうだと思ったが、中々に面倒かつサンプルも少ないので時間が掛かりそうだったのでメンションに反応するだけのbotという形で実装した。
先に完成形を紹介してから実装について触れる。
こんな感じにメンションを飛ばすと、1行、もしくは2行の形のslack絵文字画像(128px×128px)を生成してレスポンスしてくれるだけのbot。
botが受け取る入力は以下の通り。@botname [color] [bgColor] [line1] [line2(optional)]
この入力データを元に画像生成するのだが、画像の生成にはcanvasを使用した。
本当はcanvasを使わずcssだけで上手いことやりたかったのだが、スクショに余白が含まれてしまう(余白を含まずエリア選択のような形でスクショを取る方法がわからなかった)のでcanvasを使ってみたら期待通りの形になったのでcanvasで実装することにした。
入力データはテンプレートファイル(tpl)に流し込み、canvasが画像生成するようにする。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body onLoad="draw()">
<canvas id="target" height="128" width="128"></canvas>
<script>
function draw() {
var element = document.getElementById("target");
var context = element.getContext("2d");
var maxWidth = element.width;
element.style.background = {{.BgColor}};
context.clearRect(0, 0, element.width, element.height);
context.textAlign = "center";
context.font = "bold 64px Arial";
context.fillStyle = {{.Color}};
context.fillText({{.Line1}}, element.width*0.5, 56, maxWidth);
context.fillText({{.Line2}}, element.width*0.5, 115, maxWidth);
}
</script>
</body>
</html>
文字数は特に指定しないので横幅をいい感じになるようにx軸を調整。y軸についてはいい感じの値を見つけて設定した(どんな感じで計算したら良いか考えたほうが良いと思うが面倒だった...)。
結局canvasを使ってx軸y軸と向き合ってしまっているので、imageパッケージでも・・と思わないこともないが、比較的に楽に実現できたようには思う。
画像生成用のテンプレートができたら、クエリストリングで画像が生成されるAPIとしてエンドポイントを用意する。
ex. http://localhost:9999/generator?color=red&bgColor=green&line1=foo&line2=bar
後は、botのメンションに反応して、メンションからパラーメータを読み取り、chromedpでヘッドレスブラウザを起動、画像生成用のエンドポイントを叩いて画像作成、作成された画像をslackに投稿するコードを書くだけ。
諸々省略するが、コードを一部抜粋。
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var buf []byte
// スクショを取る
if err := chromedp.Run(ctx, chromedp.Tasks{
chromedp.Navigate(`http://localhost:9999/generator?` + query.Encode()),
chromedp.Sleep(2 * time.Second),
chromedp.WaitVisible(`#target`, chromedp.ByID),
chromedp.Screenshot(`#target`, &buf, chromedp.NodeVisible, chromedp.ByID),
}); err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to take a screen shot."))
return
}
// 画像書き込み
if err := ioutil.WriteFile("result.png", buf, 0644); err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to take a screen shot."))
return
}
// 画像投稿
r := bytes.NewReader(buf)
_, err = api.UploadFile(
slack.FileUploadParameters{
Reader: r,
Filename: "upload file name",
Channels: []string{event.Channel},
})
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to post a image."))
return
}
これでメンションに反応して画像を生成してくれるbotができたわけだが、残念がバグが残ってしまっている。。。
https://github.com/bmf-san/emoji-generator-slack-app/issues/1
chromedp使わなくても良かった気はする。バグはどうやって直したものか...
vscodeでgoのLanguage Serverの設定を有効にしたらコード定義元へのジャンプができなくなってしまったので原因を調査した。
settings.json
"go.useLanguageServer": true,
go.mod
がプロジェクトのルートに存在している必要がある。
cf. stackoverflow - How to properly use go modules in vscode?
vscodeでフォルダを開くときに、こうではなく、
.
├── app
├── go.mod
こう開くようにしないとパスが良しなに解決されないせいか、コードジャンプできなかった。
.
├── go.mod
参考までに、go.modの内容。
module github.com/bmf-san/gobel-api/app
go 1.14
require (
github.com/bmf-san/goblin v0.0.0-20200718124906-8b3133b538d6
github.com/bmf-san/golem v0.0.0-20200718182453-066c8e70e46e
github.com/go-redis/redis/v7 v7.4.0
github.com/go-sql-driver/mysql v1.5.0
github.com/satori/go.uuid v1.2.0
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
)
module github.com/bmf-san/gobel-api/app
と書いているのでこれを読み取って良しなに解釈してくれるのかと思ったらそうではないらしい。
vscodeのターミナルを開いて、OUTPUT>gopls(server)を選択。コードジャンプしてみると、エラーログを確認することができる。
エラーログから推測するに、パスが怪しかったので色々調べてみたら上述のstackoverflowに当たりがあった。
パッと思いつく限りの対策は以下。
goplsやvscodeの設定等で調整できるのかもしれないのが、すぐ見つけられなくて時間かかりそうだったのでlanguage serverの設定をオフにする方向でとりあえず対応した...
まだ枯れた設定ではないと思うので、そのうち類似の事例やベストな解決策が見つかると思う多分...
何かわかったら追記予定。
DBドキュメントを自動生成できるツールの有名所でいうと、MySQL Workbenchが筆頭に上がると思うが、それ以外にも良いOSSがないか漁ってみてちょっと触ってみたので感想を残す。
DBに接続してhtmlでDBドキュメントを生成してくれるJava製のツール。
Dockerhubにイメージがあるので、それを使って簡単に試してみることができる。
MySQL5.7(多分5.8も大丈夫だと思う・・)は、こんな感じでいけるはず。docker run -v "$PWD/schema:/output" --net="host" schemaspy/schemaspy:latest \
-t mysql -host {DBHOST}:{DBPORT} -db {DBNAME} -u {DBUSER} -p {DBPASSWORD}
MySQL5.6環境下ではコマンドをちょっといじる必要がある。docker run -v "$PWD/schema:/output" --net="host" schemaspy/schemaspy:latest -t mysql -host {DBHOST}:{DBPORT} -db {DBNAME} -u {DBUSER} -p {DBPASSWORD} -connprops useSSL\\=false -s {DBNAME}
いずれもワンライナーでお試しできるので簡単。
もちろんmysql以外でもOK。
CIフレンドリーなDBドキュメンテーションツールで、markdownでドキュメントを生成してくれる。
depでもrpmでもbrewでもgoでもdockerでもインストールできる。
使い方は簡単なのでgithubのREADME参照。
ドキュメントはmarkdownですべて管理したいので個人のアプリケーションのドキュメンテーションに採用している。
クラス設計の外観を把握したい時にUMLを自動生成してくれるツールが欲しかった。
phpstormなら標準でいい感じにdiagramを生成してくれる機能があるらしいが、vscodeに入信してしまったのでいい感じのツールを探すしかない。
ぐぐると色々ツールはあるのだが、簡単に使えそうなやつを探してみた。
github.com - MontealegreLuisphuml
ドキュメント
本家?github.com - jakobwsthoff/phumlはメンテ終了しているようなのだが、探してみると上記のfork版のようなやつが見つかった。
スターは少なくてあまり使う人いないのかな・・?という印象だが、ちゃんと使えそうだったので触ってみた。
phpのバージョン対応は^7.1
。
自分は7.3環境で使ってみた。
$ wget https://montealegreluis.com/phuml/phuml.phar
$ wget https://montealegreluis.com/phuml/phuml.phar.pubkey
$ chmod +x phuml.phar
$ mv phuml.phar /usr/local/bin/phuml
$ mv phuml.phar.pubkey /usr/local/bin/phuml.pubkey
composerでインストールすることもできる。
composer require phuml/phuml
インストールできたら、
vendor/bin/phuml phuml:diagram -r -a -i -o -p dot path/to/classes example.png
こんな感じの怪しげなオプションをいっぱいつけるとクラス図を生成してくれる。
オプションはドキュメントで確認。
phUML - Generate a class diagram
出力したくないアクセサをオプションで指定したりできるぽい。
大きめの設計を把握したい時に概要把握のために重宝しそう。
vscodeのプラグインであったら嬉しいが今の所はなかった。
GolangのアプリケーションをVPSでDocker-Composeを使って本番運用してみたかったので、トライ
してみた。
実際に自分がトライした環境をまとめておく。
サンプルをつくった。
github - bmf-san/go-production-boilerplate
本番環境のサーバーではユーザー作成とかポートの開放くらいやっておけば、とりあえずデプロイできるはず・・・
ちなみにデプロイでダウンタイムが発生してしまうのでそちらは別途考慮が必要。
docker-machineを使ったデプロイについては、こちらの記事がわかりやすいので参考にした。
Qiita - Docker MachineでMacからVPS上のDockerへアプリをデプロイしよう
Let's Encrypt周りはちょっとハマったが、コンテナだからハマるという部分ではないところでハマったので、特に解決が難しい問題ではなかった。
docker-machineのgenericドライバーを使えば気軽にデプロイできる。ダウンタイムの対策が必要かなと思うのが、プライベートのアプリケーションの運用であれば、考慮の1つになるかなと思う。
]]>アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
package main
import (
"fmt"
"math/rand"
)
func quickSort(n []int) []int {
if len(n) <= 1 {
return n
}
pivot := n[rand.Intn(len(n))]
low := make([]int, 0, len(n))
high := make([]int, 0, len(n))
middle := make([]int, 0, len(n))
for _, i := range n {
switch {
case i < pivot:
low = append(low, i)
case i == pivot:
middle = append(middle, i)
case i > pivot:
high = append(high, i)
}
}
low = quickSort(low)
high = quickSort(high)
low = append(low, middle...)
low = append(low, high...)
return low
}
func main() {
n := []int{2, 5, 7, 1, 3, 9}
fmt.Println(quickSort(n))
}
アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
// cf. https://github.com/TheAlgorithms/Go/blob/master/sorts/merge_sort.go
package main
func merge(a []int, b []int) []int {
r := make([]int, len(a)+len(b))
i := 0
j := 0
for i < len(a) && j < len(b) {
if a[i] <= b[j] {
r[i+j] = a[i]
i++
} else {
r[i+j] = b[j]
j++
}
}
for i < len(a) {
r[i+j] = a[i]
i++
}
for j < len(b) {
r[i+j] = b[j]
j++
}
return r
}
func mergeSort(n []int) []int {
if len(n) < 2 {
return n
}
var middle = len(n) / 2
a := mergeSort(n[:middle])
b := mergeSort(n[middle:])
return merge(a, b)
}
アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
package main
import "fmt"
// Heap is a heap.
type Heap struct {
values []int
size int
maxsize int
}
// newHeap creates a heap.
func newHeap(maxsize int) *Heap {
return &Heap{
values: []int{},
size: 0,
maxsize: maxsize,
}
}
// leaf checks whether index is a leaf.
func (h *Heap) leaf(index int) bool {
if index >= (h.size/2) && index <= h.size {
return true
}
return false
}
// parent checks whether index is a parent.
func (h *Heap) parent(index int) int {
return (index - 1) / 2
}
// leftchild checks whether index is a leftchild.
func (h *Heap) leftchild(index int) int {
return 2*index + 1
}
// rightchild checks whether index is a rightchild.
func (h *Heap) rightchild(index int) int {
return 2*index + 2
}
// insert inserts a item to a heap.
func (h *Heap) insert(item int) error {
if h.size >= h.maxsize {
return fmt.Errorf("Heal is ful")
}
h.values = append(h.values, item)
h.size++
h.upHeapify(h.size - 1)
return nil
}
// swap swaps two values.
func (h *Heap) swap(first, second int) {
temp := h.values[first]
h.values[first] = h.values[second]
h.values[second] = temp
}
// upHeapify reconstruct a heap for up.
func (h *Heap) upHeapify(index int) {
for h.values[index] < h.values[h.parent(index)] {
h.swap(index, h.parent(index))
}
}
// downHeapify reconstruct a heap for down.
func (h *Heap) downHeapify(current int) {
if h.leaf(current) {
return
}
smallest := current
leftChildIndex := h.leftchild(current)
rightRightIndex := h.rightchild(current)
if leftChildIndex < h.size && h.values[leftChildIndex] < h.values[smallest] {
smallest = leftChildIndex
}
if rightRightIndex < h.size && h.values[rightRightIndex] < h.values[smallest] {
smallest = rightRightIndex
}
if smallest != current {
h.swap(current, smallest)
h.downHeapify(smallest)
}
return
}
// buildMinHeap builds a min heap.
func (h *Heap) buildMinHeap() {
for index := ((h.size / 2) - 1); index >= 0; index-- {
h.downHeapify(index)
}
}
// remove removes a value.
func (h *Heap) remove() int {
top := h.values[0]
h.values[0] = h.values[h.size-1]
h.values = h.values[:(h.size)-1]
h.size--
h.downHeapify(0)
return top
}
func heapSort(n []int) []int {
h := newHeap(len(n))
for i := 0; i < len(n); i++ {
h.insert(n[i])
}
h.buildMinHeap()
var r []int
for i := 0; i < len(n); i++ {
r = append(r, h.remove())
}
return r
}
func main() {
n := []int{2, 5, 7, 1, 3, 9}
fmt.Println(heapSort(n))
}
アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
package main
import "fmt"
func insertionSort(n []int) []int {
for i := 1; i < len(n); i++ {
for j := 0; j < i; j++ {
if n[i-j-1] > n[i-j] {
n[i-j-1], n[i-j] = n[i-j], n[i-j-1]
} else {
break
}
}
}
return n
}
func main() {
n := []int{2, 1, 5, 7, 9}
fmt.Println(insertionSort(n))
}
アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
package main
func selectionSort(n []int) []int {
for i := 0; i < len(n); i++ {
min := i
// Compare the smallest value in the data with the first value
for j := i + 1; j < len(n); j++ {
if n[j] < n[min] {
min = j
}
}
// Swap
n[i], n[min] = n[min], n[i]
}
return n
}
func main() {
n := []int{2, 1, 5, 7, 9}
fmt.Println(selectionSort(n))
}
アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
package main
import "fmt"
func bubbleSort(n []int) []int {
for i := 0; i < len(n)-1; i++ {
for j := 0; j < len(n)-i-1; j++ {
// Compare adjacent values
if n[j] > n[j+1] {
// Swap adjacent values
n[j], n[j+1] = n[j+1], n[j]
}
}
}
return n
}
func main() {
n := []int{2, 1, 5, 7, 9}
fmt.Println(bubbleSort(n))
}
GolangでURLルーターを自作したので実装するまでの過程をメモしておく。
URLルーターを実装する際に行った下準備をまとめる。
URLをどのようにマッチングさせるか、というロジックについて検討する。
多くのライブラリでは、データ構造として木構造がよく扱われているので、どんな種類の木構造を採用するかを考えてみた。
文字列探索に特化した木の中で、時間的・メモリ的計算量がよりベストなものを選定しようとすると、基数木というのが良さそうに見えるので最初はそれを採用しようとしていたのだが、実装が難し過ぎて挫折をした。
もう少し身近でシンプルなものをということでトライ木を採用することにした。
net/http
のコードリーディングnet/http
が持つマルチプレクサの拡張として実装を行うため、内部の仕組みについてある程度理解しておく必要がある。
参考>リポジトリ参照。
過去URLルーティングについてまとめた記事。
bmf-tech.com/posts/tags/URLルーティング
基本はトライ木を使いやすい形に変えていくだけではあるのだが、パスパラメータの扱いに何度か格闘した。
正規表現の対応はそれほど面倒ではなく、DSLを用意してあげるだけでどうにかなるので、DSLの扱いにセンスが問われる。
実装過程では、テストを並行して書いたり、ステップ実行のデバッグを繰り返したりして、データ構造が常にどう変化しているかキャッチしながらやっていたので、段々脳内デバッグ力が上がっていたような気がする。
普段あまり書かないようなロジックなので、コーディングの良いトレーニングになったのは間違いなさそう。
今後の課題はリポジトリに起票してあるissueの通りで、暇な時にでもブラッシュアップを重ねようかと思っている。
設計とか実装の参考にさせてもらったリポジトリ
実装時に参照した記事。
]]>アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
package main
import (
"fmt"
)
// Node is a node of a tree.
type Node struct {
Key int
Left *Node
Right *Node
}
// BST is a binary search tree.
type BST struct {
Root *Node
}
// insert insert a node to tree.
func (b *BST) insert(key int) {
if b.Root == nil {
b.Root = &Node{
Key: key,
Left: nil,
Right: nil,
}
} else {
recursiveInsert(b.Root, &Node{
Key: key,
Left: nil,
Right: nil,
})
}
}
// recursiveInsert insert a new node to targetNode recursively.
func recursiveInsert(targetNode *Node, newNode *Node) {
// if a newNode is smaller than targetNode, insert a newNode to left child node.
// if a newNode is a bigger than targetNode, insert a newNode to right childe node.
if newNode.Key < targetNode.Key {
if targetNode.Left == nil {
targetNode.Left = newNode
} else {
recursiveInsert(targetNode.Left, newNode)
}
} else {
if targetNode.Right == nil {
targetNode.Right = newNode
} else {
recursiveInsert(targetNode.Right, newNode)
}
}
}
// remove remove a key from tree.
func (b *BST) remove(key int) {
recursiveRemove(b.Root, key)
}
// recursiveRemove remove a key from tree recursively.
func recursiveRemove(targetNode *Node, key int) *Node {
if targetNode == nil {
return nil
}
if key < targetNode.Key {
targetNode.Left = recursiveRemove(targetNode.Left, key)
return targetNode
}
if key > targetNode.Key {
targetNode.Right = recursiveRemove(targetNode.Right, key)
return targetNode
}
if targetNode.Left == nil && targetNode.Right == nil {
targetNode = nil
return nil
}
if targetNode.Left == nil {
targetNode = targetNode.Right
return targetNode
}
if targetNode.Right == nil {
targetNode = targetNode.Left
return targetNode
}
leftNodeOfMostRightNode := targetNode.Right
for {
if leftNodeOfMostRightNode != nil && leftNodeOfMostRightNode.Left != nil {
leftNodeOfMostRightNode = leftNodeOfMostRightNode.Left
} else {
break
}
}
targetNode.Key = leftNodeOfMostRightNode.Key
targetNode.Right = recursiveRemove(targetNode.Right, targetNode.Key)
return targetNode
}
// search search a key from tree.
func (b *BST) search(key int) bool {
result := recursiveSearch(b.Root, key)
return result
}
// recursiveSearch search a key from tree recursively.
func recursiveSearch(targetNode *Node, key int) bool {
if targetNode == nil {
return false
}
if key < targetNode.Key {
return recursiveSearch(targetNode.Left, key)
}
if key > targetNode.Key {
return recursiveSearch(targetNode.Right, key)
}
// targetNode == key
return true
}
// depth-first search
// inOrderTraverse traverse tree by in-order.
func (b *BST) inOrderTraverse() {
recursiveInOrderTraverse(b.Root)
}
// recursiveInOrderTraverse traverse tree by in-order recursively.
func recursiveInOrderTraverse(n *Node) {
if n != nil {
recursiveInOrderTraverse(n.Left)
fmt.Printf("%d\n", n.Key)
recursiveInOrderTraverse(n.Right)
}
}
// depth-first search
// preOrderTraverse traverse by pre-order.
func (b *BST) preOrderTraverse() {
recursivePreOrderTraverse(b.Root)
}
// recursivePreOrderTraverse traverse by pre-order recursively.
func recursivePreOrderTraverse(n *Node) {
if n != nil {
fmt.Printf("%d\n", n.Key)
recursivePreOrderTraverse(n.Left)
recursivePreOrderTraverse(n.Right)
}
}
// depth-first search
// postOrderTraverse traverse by post-order.
func (b *BST) postOrderTraverse() {
recursivePostOrderTraverse(b.Root)
}
// recursivePostOrderTraverse traverse by post-order recursively.
func recursivePostOrderTraverse(n *Node) {
if n != nil {
recursivePostOrderTraverse(n.Left)
recursivePostOrderTraverse(n.Right)
fmt.Printf("%v\n", n.Key)
}
}
// breadth-first search
// levelOrderTraverse traverse by level-order.
func (b *BST) levelOrderTraverse() {
if b != nil {
queue := []*Node{b.Root}
for len(queue) > 0 {
currentNode := queue[0]
fmt.Printf("%d ", currentNode.Key)
queue = queue[1:]
if currentNode.Left != nil {
queue = append(queue, currentNode.Left)
}
if currentNode.Right != nil {
queue = append(queue, currentNode.Right)
}
}
}
}
func main() {
tree := &BST{}
tree.insert(10)
tree.insert(2)
tree.insert(3)
tree.insert(3)
tree.insert(3)
tree.insert(15)
tree.insert(14)
tree.insert(18)
tree.insert(16)
tree.insert(16)
tree.remove(3)
tree.remove(10)
tree.remove(16)
fmt.Println(tree.search(10))
fmt.Println(tree.search(19))
// Traverse
tree.inOrderTraverse()
tree.preOrderTraverse()
tree.postOrderTraverse()
tree.levelOrderTraverse()
fmt.Printf("%#v\n", tree)
}
アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
package main
import "fmt"
// Heap is a heap.
type Heap struct {
values []int
size int
maxsize int
}
// newHeap creates a heap.
func newHeap(maxsize int) *Heap {
return &Heap{
values: []int{},
size: 0,
maxsize: maxsize,
}
}
// leaf checks whether index is a leaf.
func (h *Heap) leaf(index int) bool {
if index >= (h.size/2) && index <= h.size {
return true
}
return false
}
// parent checks whether index is a parent.
func (h *Heap) parent(index int) int {
return (index - 1) / 2
}
// leftchild checks whether index is a leftchild.
func (h *Heap) leftchild(index int) int {
return 2*index + 1
}
// rightchild checks whether index is a rightchild.
func (h *Heap) rightchild(index int) int {
return 2*index + 2
}
// insert inserts a item to a heap.
func (h *Heap) insert(item int) error {
if h.size >= h.maxsize {
return fmt.Errorf("Error!")
}
h.values = append(h.values, item)
h.size++
h.upHeapify(h.size - 1)
return nil
}
// swap swaps two values.
func (h *Heap) swap(first, second int) {
temp := h.values[first]
h.values[first] = h.values[second]
h.values[second] = temp
}
// upHeapify reconstruct a heap for up.
func (h *Heap) upHeapify(index int) {
for h.values[index] < h.values[h.parent(index)] {
h.swap(index, h.parent(index))
}
}
// downHeapify reconstruct a heap for down.
func (h *Heap) downHeapify(current int) {
if h.leaf(current) {
return
}
smallest := current
leftChildIndex := h.leftchild(current)
rightRightIndex := h.rightchild(current)
if leftChildIndex < h.size && h.values[leftChildIndex] < h.values[smallest] {
smallest = leftChildIndex
}
if rightRightIndex < h.size && h.values[rightRightIndex] < h.values[smallest] {
smallest = rightRightIndex
}
if smallest != current {
h.swap(current, smallest)
h.downHeapify(smallest)
}
return
}
// buildMinHeap builds a min heap.
func (h *Heap) buildMinHeap() {
for index := ((h.size / 2) - 1); index >= 0; index-- {
h.downHeapify(index)
}
}
// remove removes a value.
func (h *Heap) remove() int {
top := h.values[0]
h.values[0] = h.values[h.size-1]
h.values = h.values[:(h.size)-1]
h.size--
h.downHeapify(0)
return top
}
func main() {
inputArray := []int{6, 5, 3, 7, 2, 8}
h := newHeap(len(inputArray))
for i := 0; i < len(inputArray); i++ {
h.insert(inputArray[i])
}
h.buildMinHeap()
for i := 0; i < len(inputArray); i++ {
fmt.Println(h.remove())
}
fmt.Scanln()
}
トランザクションについてまとめる。
データを正しく保つための手法。DB固有の概念ではなく、一つの理論として独立している。
多数のクライアントからDBサーバーに対して同時アクセスが発生するような状況や、DBサーバーまたはアプリケーションが更新処理途中にクラッシュするという状況などからデータの整合性を守りたい時に必要とされる。
トランザクションが提供する機能は2点ある。
データ不整合を起こさないためには、処理の並列化を行わず、1つずつ順番に直列化されたスケジュールで実行することで実現可能であるが、多数のトランザクションが同時実行されている状況において直列化されたスケジュールで実行することは現実的ではない。
データが正しく保存されている状態とは、この直列化されたスケジュールでトランザクションが実行されたときと同じ結果になっている状態であると定義することができる。
現実的な実行制御のためには、直列化されたスケジュールと同じ結果になるスケジュールを選択することができるかどうかというスケジューラの性能が影響する。
スケジューラの性能は、
が主な指標となる。
RDBでは、ロッキングスケジューラというロックを用いたスケジューラが一般的に広く用いられている。
トランザクション処理において求められれる特性。
RDBでなくとも、これらの特性を満たしているものはトランザクションを実装していると言える。
トランザクションに含まれる操作がすべて成功(Success)か失敗(Abort※)になる性質。
※SQLではROLLBACKだがトランザクションではABORTという。
トランザクションは成功することが保証されているのではなく、失敗したら全て取り消されることが保証されているに過ぎない。
原子性は、トランザクションがエラーになった場合にロールバックできること、と言い換えることができる。
Abortされたときのエラー処理(リトライ)を実装していないアプリケーションはトランザクションの扱いが間違っているということになる。
トランザクション実行前後においてデータの一貫性が損なわれてはならないという性質。
トランザクション実行後、DBはデータの変更があってもデータの不整合がない状態を保つ必要がある。
トランザクション実行後、DBはある一貫性の状態から別の一貫性のある状態へ遷移すること、と言い換えることができる。
データの一貫性を保証するのはDBではなく、アプリケーションとなる。RDBであれば、RDBのデータモデルであるリレーショナルモデルが一貫性があるかどうかの判断ロジックとなる。
同時に実行している復数のトランザクションが互いに影響を与えないという性質。
個々のトランザクションの実行結果は、直列で実行されたトランザクションと同じ結果でなければならない、と言い換えることができる。
分離性はトランザクションの同時実行制御をよく表現している。
一旦コミットが完了したトランザクションが消失されないという性質。
確定したトランザクションが取り消しされないという実装があり、クラッシュしてもリカバリによってクラッシュ前のデータの状態まで復元可能であるという性質である。
クラッシュリカバリ後はコミットしたデータだけが残るため、クラッシュリカバリが完了すればコミット完了時点においてDBの一貫性が保証された状態になる。
トランザクションにとって、同時実行制御の観点からあってはならない状態について列挙する。
以下トランザクションはTXと省略する。
TX1が書いたデータと、同じデータを別のTX2が更新するとき、TX2はTX1が書いた結果を見て、次のデータを決める必要があるが、TX2がTX1が更新する前のデータを元に同じデータを更新すると、TX1によって行われた更新は消失してしまう。
TXの実行結果が別のTXの実行結果に影響を与えると、TXを読み取ったデータの一貫性が失われてしまう。
別のTXでコミットされていないデータが読み取れしまう現象。
TX1の更新後、コミットしていないデータを別のTX2が読み取った場合、TX1がAbortした際にTX2が読み取ったデータは正しいものではなくなってしまう。
別のTXで更新されたデータを読み取ることで、データの一貫性が失われる現象。
1つのTX内で同じデータを複数回読み取っている途中で、TXが書き込みをしていないにも関わらず、データが変わってしまう現象。
別のTXで挿入されたデータが見えることで、データの一貫性が失われる現象。
TX1で一定範囲を読み取っていると途中で、TX2でデータを追加または削除してコミットした際、TX1で幻のようにデータが反映されてしまう。
トランザクションが防ぐべき異常は、本来実行してはいけないものであり、そのようなスケジュール発生を防ぐ必要がある。
RDBでは、ロック使った排他制御が一般的に用いられている。
ロックは、操作対象の行を、操作が行われる前にロックをかけることによってクエリ実行の際のデータの一貫性を守ることができる。
ロックがかかることにより、ロックがかかった行へのアクセスを必要とするトランザクションはブロックされる。
RDBでは、行レベルロックあるいはページロックといった実装においてデッドロックという問題が発生し得る。
デッドロックとは、2つのトランザクションが互いをブロックし合う状態になり、処理が進まなくなる状態のことをいう。
回避策は実装により異なる。
共有・占有はDBの機能で、楽観的・悲観的ロックは方針。
データのREADをする時に使うロック。他のトランザクションはWRITEができなくなる。
データをWRITEする時に使うロック。他のトランザクションはREADもWRITEもできなくなる。
データへの同時アクセスは発生しないだろうという楽観的な前提に基づく方式。
更新対象のデータがデータ取得時と同じ状態であることを確認してから更新することでデータ不整合を防ぐ。
データが同じ状態であるか判断するためのカラムをロックキーという。
データへの同時アクセスが頻繁に発生するだろうという悲観的な前提に基づく方式。
更新対象のデータを取得する際に、別のトランザクションから更新されないようにロックをかけることでデータ不整合を防ぐ。
分離レベル | 分離性 | ダーティリード | インコンシステントリード | ロストアップデート | ファントムリード |
---|---|---|---|---|---|
READ-UNCOMMITTED | 低い | ○ | ○ | ○ | ○ |
READ-COMMMITED | ↓ | × | ○ | ○ | ○ |
REPEATABLE-READ | ↓ | × | × | ○ | ○ |
SERIALIZABLE | 高 | × | × | × | × |
今年も残すところ1週間とちょっとくらいになったので、今年の振り返りと来年の抱負をポエムっとく。
ここ3年間くらい右肩上がりで公私ともに良い機会、良い経験に恵まれている。
今年は特に良い年だったと思う。
めんどくさいので雑に箇条書きにしていく。
個別の事柄をピックアップして書きたいけど、多すぎて大変面倒なことになるので総括だけ。
その他技術サイドのあれこれは日々のアウトプットやレジュメにひそかにまとめているので個別的な話は面倒なので割愛。(そっちの話のほうが一杯かくことあるが...)
公私ともに今日よりも良い明日をつくれるように努めて、その結果来年も右肩上がりにしていきたい。
で、来年の技術的な方面での抱負は、
「脱文系」
というのがピッタリだと思っている。
来年学びたい領域のことを考えるとそういう方向性だなぁという感じ。
おわり。
]]>この記事はURLルーティング自作入門 エピソード1の続きで、Makuake Development Team Advent Calendar 2019の15日目となります。
前回の続きです。
ルーターを自作するにあたり、ルーターがどういった処理を行うのかデータ構造の観点から考えてみます。
ルーターがどんなInputを受け取って、どんなOutputを返すのか、動的なルーティングの場合の例を図示してみました。
URLのパス部分をInputとして受け取り、パスとマッチするデータを判定してOuputして次の処理につなげる、というのルーターの役割です。
どのようにパスのマッチングを行うのかというのが実装の根幹となる部分になります。
ルーターを導入する上でのコンテキスト(アプリケーションの規模感だったり、アーキテクチャの方向性だったり、、)によってルーターに求めるパフォーマンスは様々かと思いますが、ここでは正規表現だけによるマッチング処理ではなく、木構造を用いたマッチング処理を実装していく方向で考えてみたいと思います。※
※その昔正規表現ベースのbmf-san/bmf-react-routerというReactで使うライブラリを書いたことがある。path-to-regexp
という便利なライブラリの恩恵に授かっただけなので、マッチング処理がラッピングされた単なるコンポーネントですが...
では木構造について簡単に説明します。
木構造とは、根(Root)、枝(Edge)、節点(Node)、葉(Leaf 終端の節点のこと)を持つデータ構造のことです。データの格納方法や探索方法のパターンにより様々な種類の木構造があります。URLルーティングでは文字列を主に扱いたいので、文字列探索に特化したトライ木というデータ構造を採用します。
トライ木についてはbmf-tech.com - A Trie implementation in Golangを参照してください。
トライ木をカスタマイズして、URLのパスマッチングのためのデータを以下のように格納します。
例として、GETのみに対応するルーティングを定義しています。
/foo/
/baz/
/foo/bar/
/foo/:name[string]/
/foo/bar/:id[int]
根の直下にHTTPメソッド分ノードを生やし、それぞれのHTTPメソッドごとにパスを格納している形になります。:
から始まるノードはパスパラメータを保存するノードで、[int]
というDSLは正規表現をそのパラメータに対応させるためのものになります。Outputにつながる値(基本的には値を関数呼び出しするところまでルーターの責務かと思います)は各ノードが保持していたり、保持していなかったりします。これは予め定義するルーティングの内容次第です。
ルーティングの定義を元に、上で定義したデータ構造を再現するコードをかくことができれば、あとはルーターを利用するクライアントのためのコードを書くことで理論上は自作できます。理論上は。※
※実装しているものがあるのだが、まだ途中なので・・Qiita - Go6 Advent Calendar 2019の20日目までに頑張る。
後半は飛ばし気味で雑になってしまいましたが、イメージが分かればコードを書くだけなので、自作できたも同然です()。
今回はトライ木のオレオレ拡張みたいなデータ構造を説明しましたが、メモリ効率とかをより意識するならパトリシア木という木構造を採用すると良さそうです。Golangの実装を漁っていたとき採用しているのを散見しました。
筆者はパトリシア木の実装に挑戦したことがあるのですが、一度挫折してしまったのでそのうちまた挑戦したいと思います。。。※
※パトリシア木のコードを読めばわかると思いきや、単純なトライ木とは違って実装パターンが「みんな違ってみんないい」みたいになっているので、パトリシア木というデータ構造をちゃんと腹落ちさせて実装すべきだろうと思って色々試みたのですが、難しすぎました...
ルーターはOSSのライブラリとしては競合が多いジャンルだと思いますが、後発のものでも肩を並べられるようなものができればより楽しさも見える世界もまた変わると思うので、今後も飽きが来ない限りは付き合っていきたい分野です。
GolangでURLルーター実装しました。
]]>この記事はMakuake Development Team Advent Calendar 2019の14日目の記事です。
趣味で駆け出し※URLルーティング自作マンをやっているので、URLルーティング自作界隈※に入門したい人に向けた記事となれば幸いです。
※駆け出しというキーワードが今年はWeb界隈で流行り?ましたね。私は去年末からURLルーティング自作を始めたので駆け出しだと思います。
※そんな界隈があるのかは知らないが、世界は広いのでたぶんある。
見るに耐えないものではあるが、色々試行錯誤した過程を晒しておきます。
記事
スライド
試行錯誤してきた割にURLルーティングとしての機能を満たせない単純なミス※に最近気づいてしまったので、懺悔の気持ちを込めて今一度URLルーティングの自作について整理する目的も兼ねて記事を書きます。
※URLのパスパラーメータの扱いに関して残念なミスを発見してしまった。
URLルーティングを自作するモチベーションがどこにあるかという話しについて、思いついたことをリストアップしてみます。
リストアップしたものの、ほとんど結果論で、楽しそうだからとりあえずやってみたというのが正直な感想だったりします。
言葉通りに受け取ると、URlが名詞でルーティングが動詞なので"URLをルーティングする"モノが「URLルーティング」と呼ばれるわけですが、"URLをルーティングする"とはどういうことでしょうか。
この記事は入門を謳ってしまったので一つずつ言葉の解釈をしていくことにします。
URLとは、ブラウザ上でよく見る"/"がいっぱいついてあるあの文字列のことです。ex. https://www.google.com/
インターネット上でのページのアドレス(住所)を表すもので、Uniform Resource Locatorの略です。
URLの文字列の形式は、以下のように定義されています。
<scheme>:<scheme-specific-part>
<scheme>
の部分は、http・https・ftpといったプロトコル名がよく用いられますが、プロトコル名以外のスキーマ名も定義されています。 cf. Uniform Resource Identifier (URI) Schemes
<sceme-specific-part>
の部分は、スキーマに基づく文字列が定義されます。例えば、httpやhttpsのスキームの場合は、ドメイン名とパス名(またはディレクトリ名)が定義されるルールになっています。
URLの詳しい仕様はRFC1738を参照してください。cf. rfc1738 - Uniform Resource Locators (URL)
RFC1738はインターネット標準(STD1)として位置づけられています。
ルーティング(Routing)とは、和訳すると経路制御という意味なります。
OSI参照モデルのネットワーク層の話しでいえば、ネットワーク間の通信を中継するルーターが"ルーティング"をの役目を担っています。このルーターは、ルーティングテーブルという経路表を持っていて、経路表に従ってパケットの受け渡しを仲介する、"ルーティング"を行っています。
URLルーティングの話しはネットワーク層の話しではないので、話しをアプリケーション層に戻します。
アプリケーション層での"ルーティング"※は、URLという情報をブラウザのリクエストから受け取り、受け取ったURLに基づいてデータを処理するために、URLと処理の仲介を行うことだと定義することができます。
※アプリケーション層の"ルーティング"という書き方は語弊があるかも。"URLルーティング"とは、に読み替えてもらうほうが無難。
ネットワーク層のルーターは経路表というデータ構造を持っていますが、アプリケーション層のルーター(URLルーター)も、URLに基づいて処理を振り分けられるようなデータ構造を持っています。どんなデータ構造かという話しは後述します。
URLルーティングを自作する、というよりURLルーターを自作するというほうが言葉的に正しいような気がしますが、同義として扱うことにします。
Routerの位置づけはこんな感じです。
ここでルーターの要件についてざっくりと定義しておきます*。
/foo/bar/
というURLに対して、/foo/bar/
という静的なルーティングの定義にマッチさせ、処理を返却することができること。/foo/bar/123
というURLに対して、/foo/bar/:id
といった動的なパスパラメータを持つルーティングの定義とマッチさせ、パスパラメータのデータを利用することができる形で処理を返却することができること。最低限上記の要件を満たしていればルーターとしての基本的な機能を満たすことができるかと思います。
要件に元づいた仕様については実装次第で色々調整できるのでここでは明確に定義しておかないことにします。※
※GolangでのURLルーティングの実装について、Qiita - Go6 Advent Calendar 2019の20日目で書く予定です。
では、ルーターを自作するために、 ルーターがどんな処理を行っているか、データ構造の観点から考えてみます。
と、ここまで書いた時点で日を跨いでしまいそうなので、続きは明日の記事にします。
To Be Continued..
]]>アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
以下はハッシュ値の衝突を考慮していない粗雑なハッシュマップ。
package main
import "fmt"
// A HashMap is hash map.
type HashMap struct {
data map[int]string
}
// hash is create a hash key.
func hash(key int) int {
return key % 5
}
// put is add key to hash map.
func (h HashMap) put(key int, value string) {
hash := hash(key)
if h.data == nil {
h.data = make(map[int]string)
}
h.data[hash] = value
}
// get is get a value from hash map.
func (h HashMap) get(key int) string {
var hash int = hash(key)
return h.data[hash]
}
func main() {
h := &HashMap{
data: make(map[int]string),
}
h.put(1, "foo")
h.put(2, "bar")
fmt.Printf("%#v\n", h.get(1))
fmt.Printf("%#v\n", h.get(2))
}
2019年に日本国内で開催された全ての公式PHPカンファレンスにしたので参加記録をまとめる。
仙台に始まり、東京で終わるまでの道のり。
東京以外のカンファレンスは全て初参加。
各カンファレンスのセッションを振り返ってみると、今年はアプリケーション設計やテストについてセッションが比較的多かったのではないかと思う。
全国各地にPHPに対する熱量を持った人たちがちゃんといて、同じような興味を持っていたり、問題に直面していたりするということを肌で感じることができたのと同時に、国内のPHPコミュニティの繋がりみたいなモノを体感できたような気がする。
参加をサポートして頂いた弊社に深く感謝している。
来年は海外カンファレンスに・・(ry
PHPカンファレンススタンプラリーコンプリート。
スタンプコンプリートで頂いた景品。かわいい。
アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
配列や連結リストなど実装形式による。
package main
// Queue is a queue.
type Queue struct {
nodes []*Node
}
// Node is a item of a stack.
type Node struct {
value string
}
// newQueue create a Stack.
func newQueue() *Queue {
return &Queue{}
}
// enqueue adds an node to the end of the queue.
func (s *Queue) enqueue(n *Node) {
s.nodes = append(s.nodes, n)
}
// dequeue removes an node from the top of the queue.
func (s *Queue) dequeue() {
s.nodes = s.nodes[1:len(s.nodes)]
}
アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
配列や連結リストなど実装形式による。
package main
// Stack is a stack.
type Stack struct {
nodes []*Node
}
// Node is a item of a stack.
type Node struct {
value string
}
// newStack create a Stack.
func newStack() *Stack {
return &Stack{}
}
// push adds an node to the top of the stack.
func (s *Stack) push(n *Node) {
s.nodes = append(s.nodes[:len(s.nodes)], n)
}
// pop removes an node from the top of the stack.
func (s *Stack) pop() {
s.nodes = s.nodes[:len(s.nodes)-1]
}
この記事はQiita - Go6 Advent Calendar 2019の20日目の記事です。
GolangでHTTPサーバーを立てるコードの詳細を追ってコードリーディングします。
コードリーディングしていく実装はこちら。
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
handler := new(HelloHandler)
mux.Handle("/", handler)
s := http.Server{
Addr: ":3000",
Handler: mux,
}
s.ListenAndServe()
}
type HelloHandler struct{}
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
}
冗長に書いているこのコードを一行ずつ追ってコードを簡略化しつつ、リーディングしていきます。
まずは、
type HelloHandler struct{}
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
}
この部分から見ていきます。
ServeHTTP(w ResponseWriter, r *Request)
はHandler
インターフェースの実装になります。
// url: https://golang.org/src/net/http/server.go?s=61586:61646#L1996
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
// url: https://golang.org/src/net/http/server.go?s=61586:61646#L79
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
参考実装では、ServeHTTP(w ResponseWriter, r *Request)
のためにHelloHandler
構造体を用意していますが、HandlerFunc
を利用することでより簡潔に書き直すことができます。
// url: https://golang.org/src/net/http/server.go?s=61509:61556#L1993
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
参考実装を書き直すとこんな感じです。
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(hello))
s := http.Server{
Addr: ":3000",
Handler: mux,
}
s.ListenAndServe()
}
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
}
ServeHTTP(w ResponseWriter, r *Request)
を使っていた部分を書き換えることができました。
ちなみにmux.Handle
の中身はこんな実装になっています。
// url: https://golang.org/src/net/http/server.go?s=75321:75365#L2390
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
if pattern == "" {
panic("http: invalid pattern")
}
if handler == nil {
panic("http: nil handler")
}
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
先程短くした部分を更に見ていきます。
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(hello))
s := http.Server{
Addr: ":3000",
Handler: mux,
}
mux.Handle("/", http.HandlerFunc(hello))
の部分はHandleFunc
を使うと一部を内部的に処理させることができるので、
より短く書くことができます。
url: https://golang.org/src/net/http/server.go?s=75575:75646#L2448
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
url: https://golang.org/src/net/http/server.go?s=75575:75646#L2435
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
上記を加味して書き直すとこんな感じになります。
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", hello)
s := http.Server{
Addr: ":3000",
Handler: http.DefaultServeMux,
}
s.ListenAndServe()
}
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
}
DefaultServeMux
は、内部的にはServeMux
構造体のポインタが格納された変数になります。HandleFunc
はDefaultServeMux
へのurlパターンマッチの登録ができるメソッドになっています。
url: https://golang.org/src/net/http/server.go?s=75575:75646#L2207
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
url: https://golang.org/src/net/http/server.go?s=68149:68351#L2182
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
最後に見ていくのはこの部分。
s := http.Server{
Addr: ":3000",
Handler: http.DefaultServeMux,
}
s.ListenAndServe()
s.ListenAndServe()
の中身。
url: https://golang.org/src/net/http/server.go?s=68149:68351#L3093
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
Server
に細かい設定値を与える必要がないときはListenAndServe()
を使うことで短く書くことができる。Server
の設定値についてはgolang.org - server.goを参照。
url: https://golang.org/src/net/http/server.go?s=68149:68351#L3071
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
短く書くとこんな感じです。
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe(":3000", nil)
}
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
}
無名関数を使って使うとこんな感じです。
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World"))
})
http.ListenAndServe(":3000", nil)
}
golangでhttp routerのパッケージを自作しようとしていて、net/httpの内部的な実装に触れておく必要があったので軽く調べてみました。
見た感じ拡張しやすそうなので自作はしやすいイメージがあります。
URLルーター実装しました。
表題のとおり。
昨年のやつは非常に雑ではあるが、日々色々と考えながら過ごしていたので実際はそれなりによく行動できていた気がする。
今年のやったことの振り返りから。
今年はエンジニア歴でいうと3年目か4年目くらいだったのだが、インプットの足りなさを自覚していたので、インプットに注力することを意識していた。
なので例年に比べるとアウトプット量は少ない気がする。
あとは、今後のキャリアについてあれこれ考えをめぐらせた年でもあったので、今後の方向性みたいなイメージが結構明確になったような気がする。
今までやってきたことに対して少しの自信がついてきたので、日々ビジョンの調整をしつつも、今後のイメージに向かって引き続きやっていこうという良い感じのモチベーションを持って次の年につながるような年末を迎えることができたと思う。
毎年新しい言語を何か1つは学ぶというのが毎年の課題だが、今年はgolangを勉強することができた。
具体的なアウトプットは出せていないのでそれについては来年に持ち越しではあるが・・
コンパイル言語をやったことない自分はgolangを勉強してみて、今までやってきたphpの見え方が変わったり、プログラミングの考えた方の幅が広がったよう
な気がする。
言語化するのは難しいのだが、なんとなくそんな感じがあるので、それをもっと感じることができるように引き続きgolangはやっていく。
サーバーの設計、構成、監視、チューニング等に関連する本を数冊。
元々持っていた本を読み返しただけで新規に購入した本は積まれていった・・
OS関連の書籍を数冊。
熟読と反復しないとまるで頭に入らないので、基礎的なやつを読み直したりした。
本当に基本的な部分の最低限の知識はついたと思う・・・
TCP/IPに関する本を買ったのだがまだ読んでない・・
DNSの本とか読んだ記憶。
正直なことをいうとこの分野にはあまり興味が沸かなくて進んで読もうという気にはなれないのだが、
知らないことが多すぎる分野だと思うので、きっかけを作って勉強していきたい。
DDDとかクリーンアーキテクチャーとかマイクロサービスとかそういったものに手を出そうと思ったが、その前にそれらの前提となるような部分についてもっと勉強すべきだと感じたので、一旦ステイした。
何かコミットしたけど記憶がない・・
[https://github.com/bmf-san/Rubel:title]
今年は何か新しいものを作るよりは新しい技術をちょろっと触ったり、言語の勉強したりとそういう感じのコミットばかりだった。
ISUconに出場した。ボロボロだったが学ぶべきことは多かった。
来年も出たいと思っているので年明けから対策を講じる。
LaraCafeを数回開催した。
[https://laracafe.connpass.com/:title]
LaraCafeの運営については、年明けに運営陣とMTGする予定なので、今後の活動は現在のところ未定。
PHPカンファレンス2018@東京のLT枠に今年も登壇した。
2年連続となると来年は是が非でも登壇したいという気持ちが高まる。
昨年に引き続き技術からやや離れたところにテーマを設定して登壇したので、来年からは技術的なテーマで登壇機会を増やしていきたいと考えている。
登壇機会があると半強制力が働いてインプットとアウトプット力が高まる感じがあるので、自分の成長に良いと感じている。
登壇駆動とまではいかないが、自分が勉強したことで発表できるものは積極的に発表していきたいと考えている。
発表するためにテーマを用意するというのは本末転倒になりかねないので、そのへんは目的を見失わないようにしたい。
あと社内LTを一本やった。
去年より書いた気がする。
質が高い内容をかけていると思わないが、調べたことややったことをある程度のクオリティでちゃんとまとめることができているかなぁとは思う。
1記事がそのまま1LTに転用できるようなボリューム感で書くとそこそこの学習時間・内容を担保した上で記事を書くことできる感じがあるので、そんなペースでやっていきたい。
本は数十冊くらい買った気がする。(インフラ、OS、ネットワーク、ソフトウェアアーキテクチャ、言語 etc...)
数冊読んだが積読が解消されない・・
新書はともかく古典的な本はどんどん読み倒していきたい。
技術誌2つ購読した。WEB+DBとSoftware Design。
雑誌の持つ情報力は侮れない。
筋トレガチ勢ではないのだが、趣味として成立するくらいには通っていると思う。
ジム通いを1年ほど続けて、筋トレの追い込み方をちょっと掴めてきたような気がするので、引き続き頑張る。
継続しているせいか、健康面とか精神面とかいい影響を与えているような気がしないでもない。
効果の程はよくわからないが、「私は強い(根拠のない自信)」みたいな気持ちになったりするので、たぶん日々のモチベーションとかにいい影響を与えていると思う。
エンジニアは心体バランス良くないと技は育たないと思っているので、今年はジム通いを継続することができて良かったと思う。
基本的には今年やり残したインプットの続きをやる。
しかしインプットばかりでは厳しいのでいくつかアウトプット(具体的には開発と登壇)を出す。
golangは引き続きやる。
関数型言語を一つピックアップして触ってみようかと考えている。
golangで小さいものを何かちょこちょことつくる。
あとはこやつのCMS機能を小さくgolangでリプレースしていくのがちょうど良い勉強になる気がするし、実はやりたかったことでもあるのでやりたい。
[https://github.com/bmf-san/Rubel:title]
あとは実務でgolangかきたい・・今は難しいだろうけど・・がんばる
100万ほしい、というのは夢であって、現実的な目標は予選突破。
いや、今のままではとても現実的ではない・・・w
もちろんコンテストで目指すべきは優勝だが、「ビジネスのグロースと共にスケールアップしていくアプリケーションのパフォーマンスを支えていくことができるようになる」というのが当面の自分の目標なので、参加するに当たってはその目標に近づいたかどうかを持ち帰れるようにしたいと考えている。
主催している勉強会については先述の通り。
自分の触っている言語あるいはフレームワークのコミュニュティには積極的に参加または主催していきたい。
勉強会では技術はもちろんそれ以外の有益な情報を拾えたりするので、こちらは今までよりもより積極的に参加していきたい。
これは先述の通り、登壇の目的を見失わずに登壇機会を増やしていきたい。
社内LT大会があるので必然的には増えるだろうが、外部の登壇機会を特に増やしていきたい。
これは引き続き継続。
学んだこと、試したことがあればLTができるくらいのアウトラインで記事をかく。
本の優先順位を考えて積読を解消していく。
ほしい本があれば躊躇わずに買っていくストロングスタイルは継続する。(なんとくなくこのスタイルをやめると情報収集しようとか情報感度が鈍くなりそうな気がしている)
背中を全然鍛えていないので背中を鍛えることと、夏までに腹筋を割りたい()
これまでは毎年「挑戦」がテーマで、公私共に毎年新しいことをやるというのがテーマだったのだが、今年は変えてみた。
プライベートではやりたいと思えるような新しいことを探すのが難しくなってきたし、技術面ではそもそも毎年当たり前に新しいことをやっている気がするし、仕事面では最近転職して新しい環境になったしで、「挑戦」というテーマが新鮮味を持たなくなってきたので新しいテーマを考えてみた。
ということで、来年のテーマは、「温故知新」としたい。
「挑戦」ではなく「温故知新」。
深みはないが、エンジニアとして一通りの分野を勉強したので、今度は深みを出していこうという、これまで学んだことをもう一度学び直していこうという考えがあるので、「温故知新」がふさわしいかと考えた。
プライベートだとジムとかもっと"深み(筋肉)"出していく余地があるし、仕事だとまだオンボーディング中ではあるが今後"深み"を出し行く必要がある部分が大いにあると思うので、ちょうどよいテーマだと思う。
関係者各位、今年もお世話になりました。
来年もどうぞよろしくお願いいたします。
]]>もうすぐ2018年が終わってしまうので今年買って良かったものをリストアップする。
今年は去年よりも充実していて色んなモノを買った年であった。
順位をつけるのはめんどくさいのでテキトーにリストアップしていく。
馬の革のいい感じの長財布を買った。(正確には買ってもらったw)
外側は馬で内側はヌメ革で、使い込むほどいい感じに味がでるというやつだ。
まだ1年弱しか使用していないが、革がいい感じで非常に気に入っている。
[https://www.noi-japan.com/noijapan/7.1/105625/:title]
これは長財布と同じブランドのモノで購入したのだが、ちゃんとした革製品ということで使いやすい。
長財布には基本的には小銭を入れないので、小銭入れを別で用意しているのだが、こちらも気に入っている。
[https://www.noi-japan.com/noijapan/7.1/106274/:embed:cite]
主に電子書籍を読むために買ったのだが、非常に良い。
人によっては若干重さを感じるかもしれないが、自分は日頃筋トレしているので大して苦痛ではない。
サイズ感も読書するにはいい感じ。
もっと早く買うべきだったとむしろ悔やんでいる。
速い。快適。楽しい。
ママチャリにはもう戻れない。
実家暮らしのとき、駅までママチャリで10分くらいだったのだが、クロスバイクだと5分くらいに短縮された。
移動時間を極力削りたかったのだが、今は駅まで徒歩2~3分のところに住んでいるので最近は乗る機会がない・・・
が、乗っていて楽しいので時間のある時はサイクリングしたりしている。
[http://www.japan.bianchi.com/category.cgi?mode=category_detail&bik_Code_prm=JP19BS67084332&big_code=03&mdl_code=02:embed:cite]
(購入したのは確か2018年モデル)
中華系のwifiスピーカー。
音質はまぁまぁだが、ポータビリティの良さと割と壊れにくそうな感じでコスパが良い。
最近の中華系オーディオ機器は質が良いと感じているので信頼している。
[https://www.amazon.co.jp/gp/product/B0787KGCM1/ref=oh_aui_detailpage_o01_s00?ie=UTF8&psc=1:title]
買ったというのはちょっと違うが、実家から出て某都内に引っ越しました。
職場までドアトゥードアで20分以内なので通勤時間は圧倒的に短くなりました。
転職を期に、より時間を捻出していきたいと思っていたので、多少割を食うもののトータルで見ると良き投資だった思っている。
家のデスクはいわゆる学習デスクをずーーっと愛用しているのだが、机上台を置くことで机上がスッキリしたのでおすすめ。
[https://www.amazon.co.jp/gp/product/B0142Q0Z52/ref=oh_aui_detailpage_o08_s00?ie=UTF8&psc=1]
一人暮らしを期によく知らないブランドのソファを買った。
値段相応ではあるが、この値段のクオリティとしては納得感があるので割と気に入っている。
[https://www.amazon.co.jp/gp/product/B07C15RCLR/ref=oh_aui_detailpage_o06_s00?ie=UTF8&psc=1]
以前購入したリュックサックが良い感じだったので、ショルダーバックも購入した。
丈夫そうな作りになっているので、macやipad等の電子機器の持ち運びに安心感がある。
楽天で中古の美品がリーズナブルな価格で転がっているのおすすめ。
購入というか入会なのだが、、
引っ越ししたのでそれに伴いジムも変えた。
渋谷駅周辺でジムを探したときに自分の求める条件に合うのがtipxだった。
ちょっと高いと感じるが、ジムのモチベーションの維持はできているの満足している。
部屋が乾燥する、空気が淀みやすい?というのもあって購入した。
ホテルとかで空気清浄機が設置されているところがよくあると思うが、体感してみると結構空気が良い感じ(気のせいかもしれないが・・)して、思わず買ってしまった家電。
加湿器機能付きなので乾燥も防げる!
部屋のニオイとかホコリとかもスッキリしている気がする。
[https://www.amazon.co.jp/gp/product/B01M09SIHT/ref=oh_aui_detailpage_o05_?ie=UTF8&psc=1]
今までappleのcinema display(古いやつ)を使っていたのだが、引っ越しに際して買い換えることにした。
コスパ良いので特に不満はない。
[https://www.amazon.co.jp/gp/product/B077SBB5SH/ref=oh_aui_detailpage_o04_s00?ie=UTF8&psc=1:title]
スマブラがやってみたくて勢い余って買ってしまった。(ついでにスプラトゥーン2も買った)
ゲーム機など10年ぶりくらいに買うのだが、最近のゲームはすごい。ニンテンドーすごい。UIUX素晴らしい。
普段はコード書くか、読書するかの日常だが、ストレス発散とかに一役買うと思っているので、やりすぎないように留意したい。
便利。
玄関のドアにくっつけて使っている。
2018年は例年よりもだいぶモノを買った年であった。
家具とかはもうちょっと買い揃えたモノがあって、紹介できるものはあったのだが切りがないのでやめた。
そういえばもう一つ高い買い物をしたのだが、それは別の機会に。
一通り色んなモノを揃えたので来年はそんなにモノを買わないだろうなぁという感じはあるがどうだろうか・・
]]>この記事は、bmfカレンダー Advent Calendar 2017 - Adventarの19日目の記事です。
2017年購入してよかったなぁというモノを振り返ってみます。
個人的にQOLが向上したなぁと思うものをピックアップしました。
Bluetoothに対応したイヤホンです。
お値段手頃です。
この手のイヤホンは線があるやつないやつありますが、線があるほうがなくしにくいと思って線アリタイプのやつを買いました。
ランニングする時とか、ジムとか、電車の中とか重宝しています。
Max充電でも4時間くらいで切れてしまうのがちょっと難点ですが、コスパは良いです。
iPodクラシックをBluetooth対応させようと購入しました。
こちらもお手頃価格です。
トランスミッターにもレシーバーにもなるのでコスパが良いです。
最近はiPhoneで音楽聞くようになってきたのでお役御免になってきました..
Cote&Ciel コートエシエル Isar Rucksack M
丈夫でかっこいいバッグです。
服装を選ばず使えるデザインだと思います。
コートにも合います。
に入会しました。
ジムです。
ジムは健康や理想の身体への投資です。
ランニングしたり、筋トレを続けるだけで心身の健康がある程度保証されます。(たぶん)
以前は「運動する時間ってもったいないよなぁー好きなことしたいよなぁ」なんて思っていたのですが、運動しないほうがもったいないです。
日々のパフォーマンスの維持・向上のために運動はある程度必要だと思います。(体感しました。)
二回ほどお祭りがあってオイシカッタ。
auからmineoに乗り換えました。
格安スマフォ、もっと早く検討しておけばよかったと後悔するくらいに安いです。
通信とか機能制限とか今のところ特に問題はないです。
まだまだありそうなのですが、思い出せないのでこの辺で。
安くとも良いものは結構あるんだなぁ
]]>アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
配列に格納されているデータ数をnとする。
package main
import (
"errors"
"fmt"
)
// A Array is array implemented by slice.
type Array struct {
data []string
length int // Keep a array memory size
}
// Insert is insert a data to array.
func (a *Array) insert(index int, value string) error {
if a.length == int(cap(a.data)) {
return errors.New("a array is full")
}
if index != a.length && index >= a.length {
return errors.New("out of index range")
}
// shift data
for i := a.length; i > index; i-- {
a.data[i] = a.data[i-1]
}
// insert a value to target index
a.data[index] = value
// update the length
a.length++
return nil
}
// delete is delete a target data by index.
func (a *Array) delete(index int) (string, error) {
if index >= a.length {
return "", errors.New("out of index range")
}
// target value for deleting
v := a.data[index]
for i := index; i < a.length-1; i++ {
a.data[i] = a.data[i+1]
}
// unset
a.data[a.length-1] = ""
// update the length
a.length--
return v, nil
}
// get is get a target data by index.
func (a *Array) get(index int) (string, error) {
if index >= a.length {
return "", errors.New("out of index range")
}
// random access
return a.data[index], nil
}
func main() {
a := &Array{
data: make([]string, 10, 10),
length: 0,
}
cases := []struct {
index int
value string
}{
{
index: 0,
value: "foo",
},
{
index: 1,
value: "bar",
},
{
index: 2,
value: "foobar",
},
}
for _, c := range cases {
if err := a.insert(c.index, c.value); err != nil {
fmt.Printf("index: %v value: %v is error. %v\n", c.index, c.value, err)
}
}
if s, err := a.delete(2); err != nil {
fmt.Printf("index: 0 is error. %v\n", err)
} else {
fmt.Printf("%v is deleted.", s)
}
if r, err := a.get(0); err != nil {
fmt.Printf("index: 0 is error. %v", err)
} else {
fmt.Printf("%v", r)
}
}
アルゴリズム図鑑を参考に、アルゴリズムとデータ構造を学ぶ。
実装はgithub - bmf-san/road-to-algorithm-masterにも置いてある。
リストに格納されているデータ数をnとする。
package main
import (
"errors"
"fmt"
)
// A node is a node of list.
type node struct {
value string
next *node
}
// A list is a singly linked list.
type list struct {
head *node
}
// add add a node to tail of a list.
func (l *list) add(newn *node) {
if l.head == nil {
l.head = newn
newn.next = nil
return
}
// sequential access
for n := l.head; n != nil; n = n.next {
if n.next == nil {
n.next = newn
return
}
}
return
}
// insert a node before a particular node of a list.
func (l *list) insert(newn *node, v string) error {
if l.head == nil {
return errors.New("a target node is not exists")
}
// sequential access
for n := l.head; n.next != nil; n = n.next {
if n.next.value == v {
newn.next = n.next
n.next = newn
return nil
}
}
return errors.New("a target node is not exists")
}
// display display all nodes of a list.
func (l *list) display() {
// sequential access
for n := l.head; n != nil; n = n.next {
fmt.Println(n.value, n.next)
}
}
func main() {
l := &list{}
first := &node{"first", nil}
second := &node{"second", nil}
third := &node{"third", nil}
l.add(first)
l.add(second)
l.add(third)
between := &node{"between", nil}
l.insert(between, "second")
l.display()
fmt.Printf("%#v\n", l)
}
先月に続きPHPカンファレンスに参加してきました。
沖縄は6月末に宮古島にいった以来で、少し気温の変化が感じられました。
社内勉強会でOOPとCleanArchitectureとDDDを勉強し始めたというお話
東京からの参加者が半数くらいだったらしいです。
沖縄でも技術者のコミュニティがあって、年々活発に活動している、との話を聞きました。
初開催で色々大変なことがあったと思いますが、スタッフの皆さんありがとうございました。
次のPHPカンファレンスはCakeFestです。
]]>2ヶ月ぶりのPHPカンファレンスに参加してきました。(前回はPHPカンファレンス福岡2019に参加・登壇してきました)
北海道は子供の頃数年ほど住んでいたり、祖父母が住んでいたりと縁の地なので、他のカンファレンスとはちょっと違った感情を感じて、ポエミーでエモい気持ちになりながら現地で過ごしていました。(昔住んでいた家やよく遊んでいた公園を見に行ったりしました。)
Slim v4 に学ぶ PHP with PSR の今/これから
GoでできることはPHPでできる
今回は時間的都合で懇親会に参加しなかったため、カンファレンスの雰囲気を十分に味わうことができなかったかもしれません。
現地のエンジニアと全く交流が持てなかったのは残念でした...
PHPカンファレンススタンプラリーも残り3つになりました。
次のカンファレンスは沖縄です。
]]>FuelPHP1.8.0→1.8.2、PHP5.6→PHP7.3へのバージョンアップ対応をした。
業務でアプリケーションのバージョンアップ対応を行ったので、取り組みをまとめておく。
※ミドルウェアのバージョンとかは割愛
※OSはAmazon Linux(2ではない)
FuelPHP1.8.0はPHP7.2まで対応しているが、1.8.2は7.3まで対応している。
バージョンアップに取り掛かる2週間くらい前に突如リリースされた。
fuelphp.com - Fuel releases 1.8.2
PHP7.2はアクティブサポートが2019年11月30日、セキュリティサポートが2020年11月30日となっており、
PHP7.3は2020年12月6日、2021年12月6日となっている。
サポートの期間が伸びるためFuelPHP1.8.2がリリースされたのは大変感謝すべき一件だったと思う。
約1ヶ月半
最初の2週間は合宿という形でオフィスから離れた場所で集中的に作業に取り組んだ。同期間中は、コードフリーズ期間として、緊急対応以外のリリースを原則停止した。
パフォーマンス要件というよりもセキュリティ対応の意図が大きかった。
プロダクトのセキュリティを担保できないというのは事業にとってのリスクになり得るだろう。
masterから派生させたバージョンアップ対応用のreleaseブランチを用意した。
コードフリーズ期間は2週間設けたが、フリーズ解消後はmasterブランチでのリリースを可能とするため、masterへの機能改修、追加等があった場合は、releaseブランチへ都度変更をrebaseで取り込むようにした。
FuelPHPもPHPのバージョンアップも作業の進め方としては概ね同じで、
※1 動作確認テストは過去の資産(以前のバージョンアップ対応で使用したテスト項目)をアップデートして利用した。
※2 FuelPHPのバージョンアップ対応が終わってから一度動作確認を行い、PHPのバージョンアップ対応をしてから再度動作確認を行った。
(FuelPHPの動作確認が終わった段階で一旦リリースを挟んでも良かった気がする。)
といった流れで進行した。
違いはPHPの対応の場合は事前にstg環境とCIの実行環境をphp7.3に対応しておく必要があるくらい。
あとは朝会をやってハマりどころとか進捗の共有等を毎朝行ったり、みんなでランチにいったりした。楽しかった(小並感)。
Unit Testがある程度ちゃんと用意されていたので地獄を見ることはなかったように思う。(テスト大事)
作業完了後に発生したバグはテストで担保できていない部分(ファットなコントローラーとかE2Eがあれば検知そうなところとか)が主で、テストケースが足りないといったところは殆どなかった。
自分はプロジェクトリーダーのような役割を担っていたので、進捗管理やタスクのハンドリング等を行いつつ、実作業もやるみたいなスタンスで色々やっていたが、FuelとPHPのバージョンアップの開発作業よりもインフラ側の作業の方に時間を使っていた気がする。(開発作業は殆どレビュー中心の作業しかしていないかも、、いくつか対応したもののあるが、、)
Laravelとかと違い頻繁にアップデートされているFWではないので、PHP7.3対応は本当に万全なのか懐疑的なところ(リリースされて間もないので実績が少ない。)がちょっとあったが、結果として問題ないようだった。
「マイナーアップデートとはなんだったのか」みたいな変更があってエモい気持ちになるところがあった。
v3.1.30→v3.1.33へのマイナーアップデートで発生した問題が2つほどあった。
PHP7系から1年ほど離れていたせいか、割と忘れている機能とか変更とかあって学びがあった。
場当たり的な対応をしてしまっている節がある気がする。より丁寧に対応するにはデータ構造から見直したり、メソッドの外観を調整する必要があったかもしれない。
count(null)
でもWarningになるisset
や!empty
でnullをチェックしたり、該当箇所の引数を配列またはオブジェクトに変更するなどして対応カナリアリリースで対応した。
PHP5.6のインスタンスがぶら下がっているターゲットグループにPHP7.3のインスタンスを少しずつぶら下げて並行稼働、7.3の台数を増やしつつ、5.6の台数を減らしていった。
全部切り替え終わった段階で完全切替(releaseブランチをmasterにマージ)。
一部本番環境で調査したエラーについては、ロードバランサーのリスナーの設定を活用した。
送信元IPを社内IPで指定して、社内からのアクセスを特定のインスタンスに振り分けて検証する方法を取った。(ダークカナリアリリース?)
並行稼動期間中は度々エラーが発生し、LBからインスタンス切り離したりつけ直したりと障害対応しつつ、安定稼働、完全切替まで2週間を要した。
リリースのフェーズでテストや動作確認で検知できなかったバグがいくつか発生した。
中には解決が難しいものや対応が厄介なモノ等あったが、何かあったときは切り戻し作業(LBからインスタンスを切り離すだけ)を早急に行いつつ、エラーログの収束、安定稼働に2週間弱ほと格闘した。
自分の目線ではバージョンアップ対応よりもリリース作業のほうが大変だった印象がある。
PHPバージョンアップ関連のブログやスライドを漁ってみた。
バージョンアップ時に対応が必要な箇所はプロダクトによって様々だろうが、基本的な作業の流れの結構似ていると思った。
GameWithさんは環境が酷似しているのですごくエモい気持ちになった。
5年以上PHP5で運用されていたFuelPHPで動くGameWithをPHP7.3にバージョンアップしました! #GameWith #TechWith
テストコードが無くてPHP7へのバージョンアップが出来ない?ボットで解決しました!
3ヶ月でphp5.5から7.2にバージョンアップした現在と今後の向き合い方
CPU・メモリ使用率には大幅改善があったが、レスポンスタイムには大きな変化が見られなかった。
まだちゃんと計測できていない部分があるので調査中。
バージョンアップ対応は人海戦術が有効な部分が大きいと思うので、集中的に取り組むと比較的短期間で終わるのかもしれない。
良い経験になった。
FuelPHP1.8.2が急にリリースされた奇跡があった一方で、合宿終盤でAWSの大規模障害に直面したという稀有な出来事もあった。(stg環境での動作検証に多少の影響があった)
]]>Docker Composeを使ってgolangのtest実行していたら、Operation not permittedというエラーに遭遇した。
Docker Documentation - runtaime-privilege-and-linux-capabilities
Dockerコンテナの特権設定をいじると解決する。
gobel_test_db:
container_name: "gobel_test_db"
build: ./docker/mysql
ports:
- "3305:3306"
volumes:
- mysql_gobel_test_db:/var/lib/mysql:delegated
- ./docker/mysql/initdb.d/gobel_test_db:/docker-entrypoint-initdb.d
environment:
- MYSQL_DATABASE=gobel_test
- MYSQL_ROOT_PASSWORD=password
privileged: true // add a this option
上記だとセキュリティ的にどうなのかイマイチよくわかっていないので、もう少し権限を絞るような設定にした。
gobel_test_db:
container_name: "gobel_test_db"
build: ./docker/mysql
ports:
- "3305:3306"
volumes:
- mysql_gobel_test_db:/var/lib/mysql:delegated
- ./docker/mysql/initdb.d/gobel_test_db:/docker-entrypoint-initdb.d
environment:
- MYSQL_DATABASE=gobel_test
- MYSQL_ROOT_PASSWORD=password
cap_add:
- SYS_ADMIN
security_opt:
- seccomp:unconfined
cap_add
はLinux capabilitiesを追加するオプションで、ここではsystem administration operationsの権限を追加している。
Linux capabilitiesはスーパーユーザーの権限を細分化する機能。
seccomp
はLinuxカーネルのシステムコール発行の制限をするセキュリティ関連の機能。
ここでは、unconfined、無効化の設定をしている。
unconfinedは直訳すると"監禁されていない"という意味らしい。
コンテナことは前にちらっと勉強したが、まだまだ理解が浅い。
トライ木のアルゴリズムと実装についてかく。
bmf-san/road-to-algorithm-master
トライ木(プレフィックス木ともいう。英語はそれぞれ、trie、prefix tree)は文字列の集合を扱う木構造の一種。
各ノードは単一または複数の文字列あるいは数値を持ち(ノードは必ずしも値を持つ必要はない)、根ノードから葉に向かって探索して値をつなげていくことで単語を表現する。
ネットワークのレイヤーならIPアドレスの探索、アプリケーションレイヤーならhttpのルーティングに、機械学習とかの文脈であれば形態素解析といったところでトライ木の応用が見受けられる。
言葉で説明するよりもビジュアルのほうが頭に入りやすい。
Algorithm Visualizations - Trie (Prefix Tree)
メモリ効率が悪いので、メモリ効率がボトルネックとなるような場合はRadix Tree(Patricia Trie)のような文字列のプレフィックスを効率的に格納する木構造を検討したほうが良さそう。
検索キーの長さをmとしたとき、最悪計算量はO(m)となる。検索も挿入も同じである。
github.com/bmf-san/road-to-algorithm-master/tree/master/data_structures/tree/trieにおいてある。
検索キーの挿入と検索だけ実装した。
package main
import "fmt"
// Node is a node of tree.
type Node struct {
key string
children map[rune]*Node
}
// NewTrie is create a root node.
func NewTrie() *Node {
return &Node{
key: "",
children: make(map[rune]*Node),
}
}
// Insert is insert a word to tree.
func (n *Node) Insert(word string) {
runes := []rune(word)
curNode := n
for _, r := range runes {
if nextNode, ok := curNode.children[r]; ok {
curNode = nextNode
} else {
curNode.children[r] = &Node{
key: string(r),
children: make(map[rune]*Node),
}
}
}
}
// Search is search a word from a tree.
func (n *Node) Search(word string) bool {
if len(n.key) == 0 && len(n.children) == 0 {
return false
}
runes := []rune(word)
curNode := n
for _, r := range runes {
if nextNode, ok := curNode.children[r]; ok {
curNode = nextNode
} else {
return false
}
}
return true
}
func main() {
t := NewTrie()
t.Insert("word")
t.Insert("wheel")
t.Insert("world")
t.Insert("hospital")
t.Insert("mode")
fmt.Printf("%v", t.Search("mo")) // true
}
構造体Nodeのchildrenはruneをmapのキーとしているが、stringでも問題ない気がする。
Insertのアルゴリズムは単純で、Insertしたい文字列の文字数分ループして、子ノードに一致するキーがあるかチェック、なければノードを追加するだけである。
子ノードに一致するキーがあるかどうかはGolangでは、v, ok:=map[key]
のイディオムを使って書くことができる。(初心者過ぎて知らなかったせいでちょっとハマった)
SearchのアルゴリズムはInsertのアルゴリズムが書ければ理解できる。というより殆ど考え方は同じ。
トライ木の応用であるRadix Treeという木構造の実装に取り組んでいたのだが、あと一歩みたいなところで何度も躓いて挫折した。。。
ちょっと遠回りしてまた再挑戦する。
GolangでTrieを使ってroutingを作ろうとしている。
https://github.com/bmf-san/bmf-go-router
トライ木はちょっとしたサジェスト機能なんかの実装とかにも使える気がするのでJavaScriptとかで実装してみたい。
]]>Mackerelの監視対象から誤ったホストを退役させてしまったり、意図せず監視対象から外してしまったときなどに復帰させる方法についてメモっておく。
mackerel-agentを再起動しても自動で復帰しないのでホストに割り振られたhostIdを更新させる必要がある。
service mackerel-agent stop
cd /var/lib/mackerel-agent
mv id /tmp/
service mackerel-agent start
たまに焦るので気をつけたい...
]]>Dockerizeを使ってDocker Composeで起動するコンテナの順番を制御する方法についての覚え書き。
github.com - jwilder/dockerize
Dockerizeではなく、wait-for-it というピュアなbash scriptを使った方法も採用することもできる。
cf. Docker-docs-ja - Compose の起動順番を制御
Dockerizeを使う目的は複数コンテナを起動する際、コンテナの起動順を意図的に制御したいようなときである。
例えば、アプリケーション用のコンテナとテスト用のデータベースコンテナがあったとして、アプリケーション側のコンテナがDBを使用したテストを行うようなとき、データベースのコンテナがアプリケーションのコンテナよりも先に起動されている必要がある。
要は、コンテナ間の起動順の依存関係を解決するような目的であるかと思う。
docker-composeにはdepends_on
やlinks
といったオプションがあるが、depends_on
はコンテナの作成順序を、links
はdepends_on
の機能に加えてコンテナ間の名前解決を行うもので、どちらも起動の順番はまでは制御しない。
後で知ったがlinks
はversion2以降では自動的に実行されるらしく、レガシーになったらしい。
筆者の某アプリを実例にあげておく。
docker-compose.yml
version: "3"
services:
gobel_app:
container_name: "gobel_app"
build: ./docker/go
volumes:
- ./app:/go/src/github.com/bmf-san/Gobel/app
ports:
- "8080:8080"
depends_on:
- gobel_db
- gobel_test_db
entrypoint:
- dockerize
- -timeout
- 10s
- -wait
- tcp://gobel_test_db:3306
command: realize start
gobel_db:
container_name: "gobel_db"
build: ./docker/mysql
ports:
- "3306:3306"
volumes:
- mysql_gobel_db:/var/lib/mysql:delegated
- ./docker/mysql/initdb.d/gobel_db:/docker-entrypoint-initdb.d
environment:
- MYSQL_DATABASE=gobel
- MYSQL_ROOT_PASSWORD=password
gobel_test_db:
container_name: "gobel_test_db"
build: ./docker/mysql
ports:
- "3305:3306"
volumes:
- mysql_gobel_test_db:/var/lib/mysql:delegated
- ./docker/mysql/initdb.d/gobel_test_db:/docker-entrypoint-initdb.d
environment:
- MYSQL_DATABASE=gobel_test
- MYSQL_ROOT_PASSWORD=password
volumes:
mysql_gobel_db:
driver: local
mysql_gobel_test_db:
driver: local
gobel_appコンテナはgobel_test_dbの起動を待ちたい。
entrypointにはdockerizeのコマンドを指定している。
gobel_appコンテナのビルドに使用しているDockerfile。
FROM golang:1.13.0-alpine
WORKDIR /go/src/github.com/bmf-san/Gobel/app/
RUN apk add --no-cache git \
binutils-gold \
curl \
g++ \
gcc \
gnupg \
libgcc \
linux-headers \
make
RUN go get github.com/go-sql-driver/mysql
RUN go get github.com/oxequa/realize
RUN go get -u golang.org/x/lint/golint
ENV DOCKERIZE_VERSION v0.6.0
RUN apk add --no-cache openssl \
&& wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz
dockerizeのインストールを含めるようになっている。
手軽に導入できる。
wait-for-itのようなスクリプトを自前で用意しても良さそうな気がするがコンテナ管理が複雑化することを見込んでこうしたdockerizeを入れておくのはありかなと思った。
GolangでClean Architectureの実装に挑戦したみたので整理しておく。
内容は概ねスライドの内容を踏襲している。
理解しきれていないところがあったり、自分の解釈、考えを記述しているので、正しくない部分もあるかもしれない。
LTをする機会があったのでスライドを貼っておく。
Dive to clean architecture with golang
ソースはこれ。
bmf-san/go-clean-architecture-web-application-boilerplate
MVCのパターンでの実装もtagを切って残してある。
github - bmf-san/Rubelという今はメンテはしていないが、このブログを運用しているCMSアプリケーションがある。
このアプリケーションをリプレースすべく、選定した言語がgoで、アーキテクチャも見直そうということでClean Architectureを採用する方針となった。
なぜClean Architectureを採用しようと考えたかというと、個人で長い付き合いをしていくことのできるアプリケーションのアーキテクチャとして、
ライブラリやその他技術に依存しないようなアーキテクチャパターンが最適解なのではないかと考えたからである。
RubelはLaravelやReactといったフレームワークを採用しているが、フレームワークにどっしりと乗っかる形で実装してしまっているため、比較的モダンで変化(バージョンアップ)の早いそれらのフレームワークのバージョンアップに追随していく時間が惜しく感じた。
本来、CMSの機能追加や機能改善に力を注ぎたいはずが、今後長く運用していていきたいアプリケーションで、本質的ではない部分の開発に時間を注ぐのは合理的だと考えることができなかった。
フレームワークやライブラリ、その他技術への依存を極力減らして、goの標準ライブラリを十分に使いつつ、開発していくことができれば、保守性の高いアプリケーションがつくれるのではないかと考えた。
スクラッチこそ大正義みたいな気概を持っていたりするが、サービス開発のようなビジネス要件に即対応が求められるようなそういったアプリケーションではないのと、開発目的が学習要素を含む部分もあるのである程度理にかなっているとは思う。
自分の目的としては個人開発で取るべき最適に近い戦略が取れているような気はしているが、運用フェーズに乗っかってからでないと見えないところはまぁあるだろうと思っている。
現在鋭意開発中のRubelリプレースはこちら。
Clean Architectureの考えが生まれる前まで、過去いくつかのアーキテクチャのアイデアが存在していた。
これらのアイデアは共通して「関心事の分離」という目的を持っていて、
といったあらゆるものへの依存性脱却とテスタビリティを追求していく。
Clean Architectureで調べるとよく見るあの図はこちらの元ネタを参照。
cleancoder.com - The Clean Architecture
各レイヤーについて説明していく。
上記のレイヤー間の制約について。
冒頭で紹介したソースと同じだが再掲。
bmf-san/go-clean-architecture-web-application-boilerplate
./app/
├── database
│ ├── migrations
│ │ └── schema.sql
│ └── seeds
│ └── faker.sql
├── domain
│ ├── post.go
│ └── user.go
├── go_clean_architecture_web_application_boilerplate
├── infrastructure
│ ├── env.go
│ ├── logger.go
│ ├── router.go
│ └── sqlhandler.go
├── interfaces
│ ├── post_controller.go
│ ├── post_repository.go
│ ├── sqlhandler.go
│ ├── user_controller.go
│ └── user_repository.go
├── log
│ ├── access.log
│ └── error.log
├── main.go
└── usecases
├── logger.go
├── post_interactor.go
├── post_repository.go
├── user_interactor.go
└── user_repository.go
8 directories, 22 files
レイヤーとディレクトリの対応は以下ような形になっている。
Layer | Directory |
---|---|
Frameworks & Drivers | infrastructure |
Interface | interfaces |
Usecases | usecases |
Entities | domain |
Clean Architectureを実装する前にDIP(依存関係逆転の原則)というルールを知っておく必要がある。
SOLID原則の一つで、抽象は詳細に依存すべきではないというモジュール間の制約についてルールである。
このルールの詳細については割愛するが、Clean Architectureの文脈では、このルールは依存方向を外側から内側に保つため、インターフェースを活用することでDIPを守り、レイヤー間の制約も守る。
愚直に各レイヤーのルールに従って実装すると依存方向が内側から外側に向いてしまうような事態が発生する。
その事態のときにインターフェースを定義し、抽象への依存をすることで依存方向を守っていく、というのは実装の肝になる部分である。
Golangには「インターフェースを受け入れて、構造体を返す」という考え方がある。
これはDIPの実装に親和性がある考え方だと思う。
package examples
// Logger is an interface which will be used for an argument of a function.
type Logger interface {
Printf(string, ...interface{})
}
// FooController is a struct which will be returned by function.
type FooController struct {
Logger Logger
}
// NewFooController is a function for an example, "Accept interfaces, return structs".
// Also, this style of a function take on a role of constructor for struct.
func NewFooController(logger Logger) *FooController {
return &FooController{
Logger: logger,
}
}
Golangではよく見かける基本的な実装パターンかと思う。
インターフェースに依存させることで変更に強く、テストの書きやすいコードを書くことができる(はず)
GolangでのDIP例。
DIPではないコード。
package examples
// sqlHandler is a struct for handling sql.
type sqlHandler struct{}
// Execute is a function for executing sql.
func (sqlHandler *sqlHandler) Execute() {
// do something...
}
// FooRepository is a struct depending on details.
type FooRepository struct {
sqlHandler sqlHandler
}
// Find is a method depending on details.
func (ur *FooRepository) Find() {
// do something
ur.sqlHandler.Execute()
}
DIPを考慮したコード。
package examples
// SQLHandler is an interface for handling sql.
type SQLHandler interface {
Execute()
}
// sqlHandler is a struct which will be returned by function.
type sqlHandler struct{}
// NewSQLHandler is a function for an example of DIP.
// This function depend on abstruction(interface).
// This pattern is an idiom of constructor in golang.
// You can do DI(Dependency Injection) by using nested struct.
func NewSQLHandler() SQLHandler {
// do something ...
// sqlHandler struct implments SQLHandler interface.
return &sqlHandler{}
}
// Execute is a function for executing sql.
// A sqlHanlder struct implments a SQLHandler interface by defining Execute().
func (s *sqlHandler) Execute() {
// do something...
}
// FooRepository is a struct depending on an interface.
type FooRepository struct {
SQLHandler SQLHandler
}
// Find is a method of FooRepository depending on an interface.
func (ur *FooRepository) Find() {
// do something
ur.SQLHandler.Execute()
}
インターフェースを間に挟むことで依存関係が変化し、結果として依存の方向性が逆転するような形になっている。
Before
SQLHandler
↑
FooRepository
After
SQLHandler
↓
SQLHandler Interface
↑
FooRepository
Clean Architectureの実例では、infrastructureとinterfacesのコードがそれに当たる。
bmf-san/go-clean-architecture-web-application-boilerplate
実際にClean Architectureに取り組む際はいきなり実装しないでまずはコードリーディングや写経から入ると実装を理解しやすいのではないかと思う。
コードリーディングする際は自分は外側から内側に向かってコードを読んでいくのがわかりやすいと感じた。
main.go
↓
router.go・・・Infrastructure
↓
user_controller.go・・・Interfaces
↓
user_interactor.go・・・Use Cases
↓
user_repository.go・・・Use Cases
↓
user.go・・・Domain
クエリはこんな感じ。
DROP TABLE IF EXISTS `tests`;
CREATE TABLE `tests` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`value` int(5) NOT NULL DEFAULT 0,
PRIMARY KEY (id)
);
INSERT INTO tests(value)
VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10);
DROP TABLE IF EXISTS `posts`;
CREATE TABLE `posts` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`title` varchar(255) DEFAULT NULL,
`body` text DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
FOREIGN KEY (admin_id) REFERENCES admins(id),
FOREIGN KEY (category_id) REFERENCES categories(id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
INSERT INTO posts(title, body, created_at, updated_at)
SELECT
(@rownum := @rownum + 1),
@rownum,
CONCAT(@rownum, 'title'),
CONCAT(@rownum, 'md_body'),
CONCAT(@rownum, 'html_body')
FROM
tests AS t1,
tests AS t2,
tests AS t3,
tests AS t4,
(SELECT @rownum := 0) AS v;
ユーザー定義変数を使って行番号を取りつつ、直積(CROSS JOIN)とINSERT INTO ... SELECT
を使ってレコードを生成する方法。
色々なパターンがあったがこれが比較的わかりやすい、というか書きやすい気がする。
パッと見て何をしているのか動作をイメージするのが難点ではある。
PHPカンファレンス福岡に初めて参加してきました。
今までCFPに落ち続けていたのですが、今年度は無事採択されることができたのでスピーカーとして参加しました。
東京以外のカンファレンスに参加するのはPHPカンファレンス仙台に続いて2回目でした。
登壇資料はこちら。
PHPでURLルーティングを自作する
PHPerKaigiでトークをしたネタでしたが、多少の進捗とフィードバックを踏まえた形で改めてトークをしました。
(今年度のPHPカンファレンスCFPはルーティングの話だけです。自分としては今年度付き合い続けるテーマになっているのでちょっとずつアップデートしていければと思っています)
スライドの内容については過去の記事等で語っているのでここでは割愛します。
トークの最後にちらっと話をしましたが、現在はルーティングのデータ構造とアルゴリズムを見直して、文字列探索木のお勉強をして、当初の目的であったgolangでのルーティング自作に取り組んでいます。(まだ1コミットもしていないですが、実装のイメージはできているんです・・)
2泊3日のプランで福岡に前乗りしていました。
1週間ほど休暇を取っていて、前日まで宮古島にいました。羽田に戻った翌日に福岡にいくという自分としては中々アグレッシブなスケジュールでした。
前日は、カンファレンス前日準備のノベルティの袋詰めのお手伝いに参加していたのですが、お手伝いさんが多くてスループットが高すぎてすぐ終わってしまいました。
スタッフ、参加者関係なくカンファレンスを支えていこうという志のある人が多く参加しているであろうイベントなのだろうなーと感じました。
当日拝聴したセッション。
PHPerの採用面接で 僕らは何をつたえあうべきか #phpconfuk
自分は採用する立場の人間ではないですが、採用する立場の話は求職者の側からも知っておきたい話だと思って拝聴しました。
カジュアル面談だと言われて面談にいったら志望動機を聞かれた・・なんて話は身に覚えがありますね・・
自分の経験ではカジュアル面談の後日、選考結果がきたことがあります()
カジュアル面談は実質的に選考材料になり得るものだとは思いますが、真摯さはお互いに持っておくべきじゃなかろうか、なんて思います。
最後のスライドの言葉を借りて、"元気に会いたい"ですよね。
Laravel でやってみるクリーンアーキテクチャ #phpconfuk
最近、goでclean architecture実装に取り組んでいて、興味があったので拝聴しました。
ビジネスロジックを自然言語に落とし込むのは自分もやってみようかなと思いました。
背景が気になってATSでお話しさせて頂きました。
負債解消の文脈でプロジェクトチーム発足、といった流れではなく、本人がリーダーシップを持って草の根的に取り組んでいったそうですが、そのモチベーションに感服しました。
deprecatedなコードはあまり対象ではなく、あくまで使われている、いないの判断で消していったそうです。
deprecatedまで対応していくとリファクタリングが発生して大変なことになりますね...
サービス分割とか言語やパッケージのバージョンアップといったタイミングで、不要なコードもそういった対応をしてしまうのは徒労になってしまうので、まずはリストアップからでも一歩踏み出していく勇気が大事だなと思いました。
Password-less Web applications created with WebAuthn.
最近話題の認証だと思って拝聴しました。
人間がパスワードを生成しない、という考えは、なるほだなーと思いました。
当日は他にも見たいセッションが色々ありましたが、自分の登壇と被っていたり、時間的都合で拝聴できなかったりと残念でしたが、スライドのほうをじっくり見たいと思います。
PHPの関数実行とその計測
プロファイラの話だけかと思いきやがっつり処理系の話から始まって大変興味深かったですが、途中でついていけなくなりました。。。
概念や仕組みはなんとなくわかる気がするのですが、実際の処理系の実装を追うにはC言語とかアセンブラとかそのへんの力が足りませんでした。
コンパイラ自作はいつかやる(意気込み)
LTは全部見ました! (リンクは割愛させて頂きますが・・・)
途中で時間切れとなってしまいましたが、競技プログラミング始めませんか? / PHP Conference Fukuoka 2019に一番興味を持っていました。(後日スライド全部見ました)
自分はルーティングの自作を通じてアルゴリズムの勉強をようやく始めたのですが、データ構造やアルゴリズムの思考はプログラミングの基礎体力になり得るので、アルゴリズムを使う使わないではなく、その考え方やパターンが実務で応用力となり得るんじゃないかと最近よく思っています。
今後もちゃんと勉強していこうというモチベーションが得られました!
それから某社の人が4人も参加していて相変わらずすごいなぁと思いました笑
前日の前夜祭、エールズ祭り、3次会、当日の懇親会、エールズ祭り(2回目)と計5回も乾杯していたので、色々な方々と面識を深めることができました。
カンファレンスに限らず勉強会等でも同じだとつくづく思うのですが、横のつながりができると普段公にならないような話とかちらっと聞けたり、近しい課題や疑問を持った人と議論できるので良いですね。
カンファレンスは特にそういったチャンスが広がっているように感じるので今後も積極的に参加していきたいと思っています。
PHPカンファレンスのスタッフの方々、セッションにお越し頂いた参加の方々、ありがとうございました。
来年もぜひ参加したいと思っています! 楽しかったです!!
]]>execコマンドは現在のプロセスを実行するコマンドで置き換えるコマンドだが、引数無しで使うとリダイレクトの動的変更ができる。
ちょうどmaster直プッシュの際にプロンプトで確認するようにするで
#!/bin/sh
exec < /dev/tty
read ANSWER
というコードが出てきてよくわからなかったので調べてみたのがきっかけ。
#!/bin/sh
echo "Output to stdout" // 標準入力
exec > redirect.txt // ファイルディスクリプタを変更
echo "Output to file" // ファイルに出力される
master直プッシュの際にプロンプトで確認するようにするに出てきたコードは、
#!/bin/sh
exec < /dev/tty
read ANSWER
現在の端末(/dev/tty
)の入力をexecコマンドの標準入力に渡している。(わかりづらい。。。)
何らかの事情で標準入力をreadできないときとかに使えそう。
gitでmasterブランチへの直pushを未然に防ぐためのセーフーティネットの作り方。
github上でmasterブランチへのpushを禁止すれば良いのだが、DevOpsの都合上でgithubの設定では問題があったのでhooksを使う方向で設定した。
グローバルに設定したいので~/.git_template/hooks
配下にpre-pushファイルを作成する。.git_template
ディレクトリが存在しない場合は作成する。
なお、グローバルに設定しても既存のリポジトリには反映されないので、既存リポジトリに反映したい場合は既存リポジトリの./git/hooks
配下にpre-push
を用意し、そちらに同じソースを記述する必要がある。
pre-push
に記述する内容は gitのpre-push hookでmasterブランチにpushする際にプロンプトで確認するようにするを参照。
answer部分はyesだけにしておくほうがより安全かもしれない。
新規に作成したpre-push
には実行権限を与えておくchmod +x pre-push
以上でセットアップ完了。
dockerでコンテナとイメージを削除してもvolumeが削除されていなくてちょいちょい忘れてハマるのでメモっておく。
普段は、docker-composeを使っている。
docker-compose build
docker-compose up -d
して、
docker rm **
docker rmi **
という感じにお片付けしているのだが、どうやらマウントしているvolumeを削除するオプションがあったらしい。
ボリュームが残っているか確認。docker volume ls
docker volume rm **
docker-composeでdocker-compose.ymlに記述されているコンテナとネットワークイメージとボリュームを一気に片付ける方法があった。
docker-compose down --rmi all -v
PHPで学ぶデザインパターン Advent Calendar 2018で間に合わなかった記事。
状態をクラスで用意することで振る舞いを切り替えることができるようなパターン。
スイッチのオンオフの状態を切り替えるような例を想定した。
シングルトンを使ったほうが良い気がする。
<?php
class OnState
{
public function getState()
{
return 'ON';
}
public function getNextState()
{
return new OffState();
}
}
class OffState
{
public function getState()
{
return 'OFF';
}
public function getNextState()
{
return new OnState();
}
}
class Light
{
public function __construct()
{
// デフォルトの状態クラスをセット
$this->state = new OffState();
}
public function getState()
{
return $this->state->getState();
}
public function toggle()
{
$this->state = $this->state->getNextState();
}
}
$light = new Light();
echo $light->getState(); // OFF
echo $light->toggle();
echo $light->getState(); // ON
echo $light->toggle();
echo $light->getState(); // OFF
状態クラスは状態固有の処理を持つイメージ。
複数の状態があり、それぞれ固有の処理が複雑してきた時に検討できそうなパターンかと思う。
デザインパターンの中でもなぜだか一番好きなパターン。
使ってみたくなるような面白さがある気がする。
以前書いたソフトウェア開発の法則 の雑メモをベースにLTをしたのでスライド内容を補足する形でまとめる。
スライドは↓
ゴリラで学ぶソフトウェアの法則10選
ゴリラで学ぶには無理があったのでスクリプトを書き残しておく。
ソフトウェアの文脈で語られる法則に縛らず、他分野での法則でもソフトウェアに当てはまるであろうものを”ソフトウェア開発の法則”としている。
経験則に基づくものが多いが、経営工学だったり心理学だったり、はたまたどこかの論文だったり引用元は様々である。
「仕事の量は、完成のために与えられた時間を全て満たすまで増大する」
有名どころ。
イギリスの歴史学者・政治学者、シリル・ノースコート・パーキンソン氏が著書、「パーキンソンの法則:進歩の追求」で論じたもの。
ちなみに第2原則は、「支出額は収入額に達するまで膨張する」というもの。
コンピューターの世界だと、データ量と記憶装置の関係性に法則が応用される。
データ量は記憶装置の上限まで膨張する、といった感じ。
与えられた枠を満たすまで増加する余地のあるモノは膨張していく、ということだろうだが、そういった関係性にある要素はソフトウェア開発においても結構あるんじゃなかろうか。
増え続けていくものに対峙していくにはどうしたらいいか、という話だが、細かいゴール設定や計画立てが有効らしいが・・
「遅延しているソフトウェア開発のプロジェクトへの人員追加はプロジェクトをさらに遅延させる」
アメリカのソフトウェア技術者、フレデリック・ブルックスが著書「人月の神話」で論じた法則。
「銀の弾丸はない」という名言を生み出した人物でもある。
遅延しているプロジェクトに人員を追加するとチーム内のコミュニケーションコストや情報のキャッチアップにボトルネックとなり、結果として生産性を高めることができず、更に遅延するというもの。
追加される人員が100人力くらいのスーパーエンジニアだったら遅延は免れるのではないかと思ったりするかもしれないが、それはこの法則が成り立ってしまう理由を述べている「人月の神話」で反省(確認)すると良いと思う。
「ソフトウェアの構造は組織構造を反映する」
イギリスのプログラマー、メルヴィン・コンウェイ。
コルーチンを発明したことで有名で、コルーチンについての論文内で提唱した法則。
この法則を逆手にとって逆コンウェイ戦略というのがある。
良いソフトウェアの構造は良い組織構造を反映するだろうという戦略だが、ビジネスモデルと組織構造とソフトウェア構造が三位一体になるためにはソフトウェアの構造が主導権を握ると良い、みたいな考えだろうか。(語弊がありそう。勘違いもありそう。)
「1つの重大事故の裏には29の軽微な事故があり、さらにその裏に300の異常が存在する」
アメリカの保険会社に勤務しているハーバード・ウィリアム・ハインリッヒという人が提唱した労働災害における経験則の1つ。
危機管理のお話で、ソフトウェアの世界に当てはめるとインシデントやバグの背景には複数の”異常”があるはず、事前にそれを察知できるように努めよう、という教訓が得られるのではないだろうか。
目玉の数さえ足りていればすべてのバグを見つけ出すことができる
アメリカのプログラマのエリック・レイモンドが著書、「伽藍とバザール」で論じた一節。
要はソフトウェアの利用者(というかコントリビューター??)が充分にいればバグはそれほど深刻な問題ではないだろう、という意味だと思うが、法則というよりなんというかマインド的な話だろうか....
伽藍とは寺院の建物のことらしい。
作業には予測以上の時間がかかるものである
アメリカの学者、ダグラス・ホフスタッターが著書、「 ゲーデル、エッシャー、バッハ」で論じた一節。
誰もが実感したことのある法則ではないだろうか。
計画の立て方を工夫してみる、というのが法則に抗う1つの方法だと思う。
意思決定に要する時間は選択肢の数に比例する
イギリスの心理学者、ウィリアム・ヒックが提唱した法則。
UIにおいて考慮されるべき法則であると思う。
意識決定にかかる時間を求める公式があるがここで説明を省く。
失敗する可能性のあるものは全て失敗する
法則が生まれた経緯は諸説あるらしい。
スピリチュアルな側面を持つ法則だが、ソフトウェアの文脈でいえばフェイルセーフの考え方につながる部分がある。
変化するシステムの複雑性は上昇し続ける
出典はこちらの論文
IEEE XPlore
論文にはちゃんと目を通せていないが、進化的アーキテクチャにつながる部分があるのではないかと思う。
システムは変化を受け入れざるを得ないモノだが、何も工夫をしなければ変化するたびに複雑性は増してしまう、という話。
最初に思いついたアイデアこそが ベスト・プラクティスである
パッと思いついた経験則を形にしてみた。
複数のパターンを考慮しても結局は最初に思いついたパターンが最適解だったりする、ってことがたまにありますよね??あれです。
似たようなことわざとか理論あると思うが...
社内のLT会では恋愛学とか囲碁の世界ではどうやら近しい法則があるらしいが、ソフトウェアの世界ではどうだろうか...
原典を読むことができていないので機会があれば読みたい。
法則にとらわれると思考停止気味になる気がするが、難しい問題に直面した時に、客観的な判断材料として法則に従ってみるのも良いだろうと思った。
]]>github - oxequa/realizeを使ってみたメモ。
go get github.com/oxequa/realize
./demo/
├── .realize.yaml
└── main.go
.realize.yaml
settings:
legacy:
force: false
interval: 0s
schema:
- name: demo
path: .
commands:
run:
status: true
watcher:
extensions:
- go
paths:
- /
ignored_paths:
- .git
- .realize
- vendor
main.go
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
http.ListenAndServe(":8080", mux)
}
demoディレクトリにて、realize start --server
をすると、監視用サーバーが立ち上がり、ホットリロードができるようになる。
監視サーバーhttp://localhost:5002/#/demo
log、error、outputが見れる。
main.goで起動されるサーバーhttp://localhost:8080
簡単に導入できて便利なのでほっとリロードはこれ使っていこうかーという気持ち。
]]>PHPerKaigiに初参加&登壇してきました。
fortee - PHPでURLルーティングをつくる
Speaker Deck - PHPでURLルーティングをつくる
過去登壇した会場よりも客席が近い・スライドが大きい!で結構緊張しました...
cfpを提出した段階ではまだネタが仕上がっていなくて実装を焦っていました..
採択されてからは本番までそこそこの時間を準備に使っていて、ようやく荷が降りたなぁという気持ちになりました。
充実したトークができたかどうか正直は自信はないのですが、登壇前後で良い声をいくつか頂いていて、自分としては非常に嬉しかったです。
QAとかAsk the speakerは過去ほとんどLTした登壇してこなかった自分としては初めての体験だったのですが、ちゃんとコミュニケーションが取れて良かったです。
QAもAsk the speakerも2~3人の方に声をかけていただけて嬉しかったです。
拙い発表ながらもちゃんと聞いてもらえているということに嬉しさと身が引き締まる思いを感じました。
何度か脳内リハをした際は15分ちょいくらいかかっていたのですが、本番では緊張のせいか走って13分くらいで終ってしまいました。。。
15分というトーク時間に不慣れなのもあって、もっとしっかり準備しておければなぁと反省しました。。。
追記:
forteeにフィードバックが上がっていました。
頂いたフィードバックを見る限り、一番伝えたいと思っていた部分は伝わっていたのかなぁという印象です。
ルーティングのアルゴリズムに関心がある方、その前段階の設計の話に関心持ってくださった方がいらっしゃるようでした。
今回は前段階の話に焦点を当てて話をしていましたが、次回はアルゴリズムの話に焦点を当ててみるのも良いかなぁと思いました。(改めてちゃんと勉強しないと話せない部分ではあるので難しいですが....)
今回ルーティングを自作したのは、世の中の人に使ってもらえるライブラリとして生きるモノではなくて、どちらかという自分の経験や勉強のためという部分が強くて、そのへんは割り切っていました。
もちろん、ライブラリとして使ってもらえるようなモノを作りたいとは思いますが、そのためにはもっと様々なことを沢山キャッチアップする必要があると考えています。
今回、自分の経験や勉強のために取り組んだことが、他の技術や実務面に活かせる部分が多いにあるんじゃなかろうかと思い、PHPerKaigiではCFPを提出させて頂きました。
フィードバックの中に、実装前段階の話が他の設計や実装にも役立つプロセスだと思ったというお話があったのですが、”一番伝えたいと思っていた部分”は実はこの辺の話でした。
フィードバックを頂いた方々、ありがとうございました!
次はPHPカンファレンス福岡にて登壇します!
一番印象に残ったセッションは、
@uzullaさんの帰ってきた!平成最後のオレオレフレームワークの作り方でした。
トークも内容を面白く、オレオレフレームワークをつくるモチベーションを大変刺激されました。
1Speakerとしても1参加者としてもとてもホスピタリティが高いイベントだと強く感じました。
トーク前はスタッフの方に発表しやすくなるように配慮頂いたり、トーク前後について丁寧に案内していただきました。
会場のスタッフの対応を始め、イベントの進行、会場の設営から色々と気を使っている部分が見受けられて素晴らしいなぁと思いました。
特にドリンクの配置を適宜調整していたのが印象的でした。
あとはノベルティが豪華でした。笑
ペチパーカーは普段使いしやすいデザインで非常に気に入りました!
来年の開催も楽しみにしております。
委員長を始めスタッフの皆様、ありがとうございました!
今年の2月に開催されたLaravel JP Conferenceのスタッフをやっていたのですが、その時メンバーでPHPerKaigiのスタッフをやっていらっしゃる方が何名もいて、国内のPHPカンファレンスは献身的な人によってサポートされているのだなぁと改めて感謝の念を感じました。
]]>Burp SuiteをChormeで使う際の諸々の設定について。
脆弱性診断や対応時にburpをchromeで使えるようにしたかった。
Mac OS
chrome extension - proxy switchsharpでProxy Profilesを設定する。
Profile NameをBurp(何でもよいがBurp用のプロキシ設定なのでBurpにしておく)
Manual Configurationを選択して、HTTP Proxyに 127.0.0.1
を設定、Portは各自の環境でバッティングしないように設定。
Saveを押してProfileを保存。
proxy switchsharpを使うのは毎回プロキシ設定を変更するのが手間なので拡張機能でシャッとプロキシ設定を変更できるようにするため。
プロキシ設定の変更はChromeの右上の拡張機能が並んでいるところからproxy switchsharpを選択して任意のProfileを選択することで変更することができる。
特にプロキシ設定をいじる必要がない普段はDirect Connectionを選択。
Burpを起動。
chromeのプロキシ設定が上で設定したProfileになっているか確認。(proxy switchsharpでプロキシ設定を保存しただけでは設定が有効になっていないので、chromeブラウザ右上の拡張機能が並んでいるところがproxy switchsharpを選択してProfileを選択して有効にする必要がある)
Burpをデフォルトの設定で起動した場合は、http://127.0.0.1:8080
にアクセスする。
右上のCA Certificateをクリックし、証明書をダウンロード。
ダウンロードした証明書をKeychain accessで開き、証明書を常に信頼
に設定
する。Port Swigger CA
という証明書名なはず。
以上の作業でChromeでBurpが使えるようになっているはず。
Chromeだとlocalhostをinterceptするには、proxy switchsharpのProfile Detail > No Proxy Forで<-loopback>
を追加する必要がある。
Burp InterceptionがChromeのローカルホストで機能しない
URLルーティングをつくる エピソード1 とURLルーティングをつくる エピソード2 でURLルーティングの自作について試行錯誤の過程を記してきたが、ようやく一段落させることができたので完結編という形で締括くりたい。
完結、といっても課題はいくらでもあるし突き詰めるとこればっかりに時間をかけることができるようなモノであるということは承知している。。。
エピソード1 では、ルーティングのデータ構造を考えたり、とりあえず手を動かして実装のイメージを掴もうとした。(動くところまで持っていけなかった。。。)
エピソード2では、データ構造を見直したり、参考になりそうなリポジトリを漁って動く形まで持っていった。
そして今回のエピソード3では、URLルーティングをつくる エピソード2 でやり残した部分の実装を完了させた。
具体的にいうと、URLルーティングをつくる エピソード2 までは、ルーティングマップを生成する処理を実装せずに、予めクライアント側でルーティングマップを用意する手を抜いた形でルーティングを実現していたが、今回はその部分の実装をした。
今回の内容はちょうどphperkaigi2019で登壇するのでそのためのまとめ記事という側面もあるので、エピソード1 とURLルーティングをつくる エピソード2 の内容も含んでいる。
プロポーザル↓
fortee - PHPでURLルーティングをつくる by bmf_san
スライドはこちら↓
Speakerdeck ー URLルーティングをつくる
記事の内容はスライドの補足のような感じなのでスライドを見るほうがわかりやすいかもしれない..
リポジトリとパッケージを公開している。
リクエストされたURLに対して、実行したい処理を返すもの
URLのパス部分をパースして、任意の値を返せるようなロジックが実装できればURLルーティングとしての最低限の機能を満たせるはず。
パス(/foo/bar/1)のパースには正規表現や文字列探索のアルゴリズムを使ったりする。
有名所だとこんな感じ・・?
etc...
FastRouteは確かSlimで採用されていた気がする。
とりあえず動くモノにする、というのは最低限の条件として他の言語への移植しやすさを考慮してPHPの標準関数を極力さけるような実装を検討した。(あとでGo書き直したいので...
あとは単純にオレオレアルゴリズムでゼロから考えながら実装してみたかったので純粋なロジックでかくことを前提とした。(なので正規表現も使わない)
ルーティングとしての最低限の条件を満たせるであろう仕様とした。
実装に入る前にI/Oを確認しておく。
Router(ルーティングを行うクラス)がどんなデータを受け取って、どういう形のデータを返すのか整理する意図。
Input
Output
Routerが内部的に扱うデータ構造を検討する。
内部的に扱うデータ=Routing Map
Routing Mapというワードは定義のある言葉ではなさそうなので説明しておくと、
URIと返却したい処理をマッピングしたデータのことである。
このパスにリクエストされたらこの処理を行う、というルールをまとめておくもので、Routerは事前に定義されたルート定義からこのRouting Mapを生成して、ルーティングを行う際にこのRouting Mapの探索を行い、処理を返す。
ここでいうルート定義とは、ルーティングの設定ファイル等でアプリケーションが扱うエンドポイントと処理をライブラリのAPIに従って記述している設定のことをルート定義と呼んでいる。
例えばLaravelだったらこういう感じで定義するやつ。
<?php
Route::get('/home', 'HomeController@index);
ルート定義はRouting Mapをつくるための情報となる。
このRouting Mapのデータ構造について考える。
ルーティングで探索したい対象であるパスの階層構造に着目して、ルート定義を木構造で表現する。
木構造のアルゴリズムには色々な種類があるが、今回は基数木という木構造を参考にしてみた。(厳密に基数木であるとは言えないかもしれない。そのへんはちゃんと勉強できていない。)
パス部分を木構造で表現し、Leafとなる部分をActionとして扱うことで、木構造を探索した結果がルーティングの返却すべき値(Leafの値)となるような構造にしてみた。
この辺はテキストではわかりづらいので、スライドを参照してもらいたい。
木構造を採用したRouting Mapは、PHPでは多次元配列で表現する。
ざっとこんな感じ。
<?php
$routeMap = [
'/' => [
'END_POINT' => [
'GET' => 'IndexController@index',
],
'posts' => [
'END_POINT' => [
'GET' => 'PostController@getPosts',
],
':id' => [
'END_POINT' => [
'GET' => 'PostController@edit',
'POST' => 'PostController@update',
],
':token' => [
'END_POINT' => [
'GET' => 'PostController@preview',
],
],
],
':category' => [
'END_POINT' => [
'GET' => 'PostController@getPostsByCategory',
],
],
],
'profile' => [
'END_POINT' => [
'GET' => 'ProfileController@getProfile',
],
],
],
];
データ構造が検討できたらあとは愚直に実装するのみ・・・!
ルーティングに関わる処理に責務を持つRouterクラスを実装する。
このRouterクラスに必要な処理は2つで、
仕様が単純なのでこれだけ。(実装はやや面倒だが・・)
具体的な実装はgithub - bmf-san/ahi-routerを参照。
ここでは要所だけ記載する。
Routerを利用するclient側の実装はこんな感じ。
<?php
require_once("../src/Router.php");
$router = new bmfsan\AhiRouter\Router();
$router->add('/', [
'GET' => 'IndexController@index',
]);
$router->add('/posts', [
'GET' => 'PostController@getPosts',
]);
$router->add('/posts/:id', [
'GET' => 'PostController@edit',
'POST' => 'PostController@update',
]);
$router->add('/posts/:id/:token', [
'GET' => 'PostController@preview',
]);
$router->add('/posts/:category', [
'GET' => 'PostController@getPostsByCategory',
]);
$router->add('/profile', [
'GET' => 'ProfileController@getProfile',
]);
$result = $router->search('/posts/1/token', 'GET', [':id', ':token']);
var_dump($result);
// array(2) {
// 'action' =>
// string(22) "PostController@preview"
// 'params' =>
// array(2) {
// ':id' =>
// string(1) "1"
// ':token' =>
// string(5) "token"
// }
// }
Path、Method、ActionのデータセットからRouting Mapを更新していく処理を実装する。
/**
* Add routing to route map
*
* @param string $route
* @param array $handler
* @return void
*/
public function add($route, $handler)
{
// 再帰処理と参照(&)を駆使してルーティングマップにルーティングを追加していく
}
参照を駆使して多次元配列を動的に生成するようなロジックをかいている。
雑に簡略化した例は下記の通り。
<?php
$routeMap = [
'/'
];
$ref = &$routeMap['/'];
$ref = [
'/posts' => [
'END_POINT' => [
'GET' => 'PostController@getPosts'
]
]
];
var_dump($routeMap);
// array(2) {
// [0] =>
// string( 1) "/"
// '/' =>
// array( 1) {
// '/posts' =>
// array( 1) {
// 'END_POINT' =>
// array( 1) {
// 'GET' =>
// string( 23) "PostController@getPosts"
// }
// }
// }
// }
Path、Medthod、Parameterのデータセットを元にルーティングマップから該当するLeafを探索する処理を実装する。
/**
* Search a path and return action and parameters
*
* @param string $requestUri
* @param string $requestMethod
* @param array $targetParams
* @return array
*/
public function search($requestUri, $requestMethod, $targetParams = []): array
{
// ルーティングマップを探索していく処理
}
下記のような処理で愚直に実装している。
極力PHPの標準関数を避けるのを前提しているのでパワープレイとなっている。。。
<?php
$request_uri = '/posts';
$routing_path = '/posts'; // ルーティングマップに定義されたパス
// 以下は説明上簡略している部分がある
for ($i = 0; $i < str_length($routing_path); $i++) {
if ($request_uri{$i} === $routing_path{$i}) { // 一文字ずつパスを比較している
// something to do
}
}
エラーハンドリングと実行速度の考慮がなされていないのでライブラリとしてはちょっと残念。。。
前者についてはともかく、後者については文字列探索のアルゴリズムの選定が必要なのでやや難易度が高いように思う。(勉強せねば・・・)
今回は単純な機能のみで実装することを目指したが、ネームルート(ルート定義のグルーピング)やパスパラーメーターに正規表現を使えるにしたり、ミドルウェアとのつなぎ込みを実装できたりすると便利なルーティングライブラリとして扱えるんじゃないかと思った。
ルーティングの処理のパフォーマンスについては、N数となる部分の想定次第で許容できる閾値が変わってくるはずなので、スマートな実装でなくても実用レベルでは耐えうる可能性もあるはず。。。
今回はルーティング数やパラーメーター情報が増えると計算量が比例して増えていくようなアルゴリズムになってしまっている。
あとは木構造でなくとも正規表現で実装されているライブラリもあるので木構造がスタンダートな実装というわけでもなさそうではある。
「推測するな、計測せよ」という言葉に従ってベンチマークをとったほうが良い。(今回はサボったが...)
良い記事を見つけたのでメモ。
]]>普段、カンファレンスは参加者側なのですが、PHPのコミュニュティに対して微力でも貢献する機会であり、自分の好きなFWのカンファレンスでもあったので、コアスタッフとして参加しました。
(ほぼ一日中受付スタッフをやっていました。)
自分がエンジニアとしてなんとかやれているのは、包容力のあるPHPコミュニュティによるところが大きいと感じているので、少しでも還元できたら良いなぁと思っています。
スタッフとして参加してみて、カンファレンス運営の大変さが身に沁みたので、今後もカンファレンス運営の方々に感謝しつつもコミュニュティに貢献できるようになりたいなぁと思っています。
スタッフメンバーの皆さんが良い人たちばかりで、とても楽しく過ごせました! ありがとうございました!
最近10分程度のLTをする機会に馴染んだせか5分で収めることができませんでした...
Duskを掘り下げた話にテーマを持っていったほうが良かったかなと思いましたが、最近Laravelから離れていてDuskを触るモチベーションはありませんでした。。。
]]>ちょいちょい忘れてるのでメモ。
不足があれば随時追加。
conohaでubuntuサーバーを用意し、rootログインできることを確認しておく。
秘密鍵と公開鍵を作成。
ssh-keygen -t rsa
ssh root@<ip address>
アップデートしておく。
sudo apt update && sudo apt upgrade -y
sudo権限を持ったユーザーを作成しておく。
adduser <username>
usermod -aG sudo <username>
wheelに所属しているか確認しておく。groups <username>
※ユーザー一覧確認cat /etc/passwd
作成したユーザーにログイン。su <username>
.ssh
ディレクトリの準備。mkdir .ssh
touch .ssh/authorized_keys
chmod 700 .ssh
chmod 600 .ssh/authorized_keys
./ssh/authorized_keys
にクライアント側で作成しておいた公開鍵を貼り付ける。
sshの設定を変更する。
sudo vi /etc/ssh/sshd_config
Port 5005 // デフォルト22から任意の番号に変更
PermitRootLogin no // yes → noに変更
PubkeyAuthentication yes // no → yesに変更
PasswordAuthentication no // yes → noに変更
UserPAM no // yes → noに変更
ssh再起動。sudo /etc/init.d/ssh restart
続けてポート開放。
sudo ufw allow 5005
sudo ufw allow 443
sudo ufw default deny // デフォルトの設定でdenyかも...
sudo ufw enable
ポート設定を確認。sudo ufw status
~/.ssh/config
ファイルをこんな感じに編集。
ServerAliveInterval 300
TCPKeepAlive yes
AddKeysToAgent yes
ForwardAgent yes
UseKeychain yes
Host conoha-demo
Hostname <ip address>
User <username>
Port 5005 // 上で設定した任意のポート番号
IdentityFile ~/.ssh/<pubkey name>
ssh conoha-demo
でもssh接続できるか確認。
昔centosを初めて触ったときも似たようなメモ書いた気がする。
]]>プライベートの時間にやるタスクを可視化しているTrelloとオレオレスプリントの計測データを管理しているspreadsheetを公開設定にしてみた。
→現在は非公開です。
自分のエンジニアとしての活動は可能な限りオープンにしていきたいとなんとなく考えていて、思い切って公開にしてみた。
そんなふうに考えるのは自分がエンジニアになる前もなった今も世のエンジニアが普段何を勉強しているのか、何を考えているのかというのが知りたくて、人のを見せてもらう前に自分のを公開せねばみたいな気持ちがどっかにあるかもしれない。
Trelloでタスクを可視化すること自体は2~3年前くらいからやっているのだが、今年からラベリングとタスク分け、見積もりをある程度ちゃんとやるような運用に変えて、オレオレスプリントで計測してみる試みを始めた。
そのような運用を始めた理由は、自分のスキルアップをもっと効率化していきたいと思ったからだ。
今までの運用は単なるTodoリストみたいな形で、優先順位とかもそこまで意識せず、やりたいようにタスクをこなしていた。
あれもやりたいこれもやりたいみたいな状況で、色々手を出して最終的に完了したのは1〜2つみたいな残念なタスク処理状況が割とよくあり、自分の時間の使い方としても、精神衛生的な側面からも良くないと感じていた。
そんな雑運用が続く中、スクラム開発をゆるく体験したり、あれやこれやと人生には時間が足りないことを日々実感して、もっと効率的に、バランスよく運用をしていく必要性を感じたので運用の見直しを図ることにした。
自分が決めたスプリント(1週間)の中で、ざっくり見積もったタスクをどれくらいこなせるのか計測していく。
見積もり工数はざっくり時間ではなく日単位。計測の正確性のためにも時間単位でタスクを区切って見積もりできるようにしたほうが良いと思ったが、プライベートの時間というのは会社での時間よりも差し込み案件(急用とか家事とかあれこれ)が多いので、時間に融通が効くようで効かなかったりすることがある。(独り身でこれだから家庭を築くとより状況は困難になるはず)
なので、時間単位で見積もらずに、バッファを大きく取るという意味で日単位にしてみた。
この時間の見積もりをストーリーポイントのような扱いにしてしまって、1週間のうちにどれくらいのタスク数をこなせたか、その合計の見積もり合計点数はいくらかとSpreadsheetに可視化するようにしてみた。
1週間は7日しかないので、合計の点数が7以上いけばその週は頑張ったのだなというざっくりした結果を得られる。
今回計測を始めた目的としては、スプリント内でタスクをこなす量を増やすこと(以下スループット)ではなく、スプリント内で安定してタスクをこなせるようになることが目的である。
1週間で1週間分以上の成果が出せたことは良いことだが、毎週それくらいの成果を出そうというのは現実的な目標ではなく、不安定なプライベートの時間内で、スループットをいかに最大化できるようになるかというのが今後自分が重視すべき課題だと認識している。
今週は外食の予定が色々あるからこれくらいのタスクだったらこなせるだろう→こなせた!というサイクルをちゃんと回せるようにすることで、自分の時間の使い方を効率よくして、スキルアップもいい感じにしていきたいと考えている。
プロダクトのスプリントレビューの目的はプロダクトの価値を最大化するところにあると思うが、個人タスクのスプリントレビュー(ただの計測だが・・)自分のスキルの最大化にあると考えることができるかと思っているので、1年運用を続けてどうなるか試してみたい。(その間に細かい運用方針は変わるかもしれないが)
タスクの起票について。
やりたいこと、やるべきこと、やったほうがよいことがざっくり頭の中にあるのでそれらを起票するようにしている。
特定の日に起票するわけではなく、仕事中でも出先でもやろうと思ったことは起票している。
やりたいことは、技術的好奇心をそそられること。
やるべきことは、仕事で必要なこととかやらないと何かしら問題が発生すること。
やったほうがよいことは、やっておくと今後良いことがありそうなこととかキャリアに影響を与えそうなことなど。投資的活動。
この3つの重なり合うタスクが自分にとってベストなタスクだと考えているが、割とやりたいことばっかりやっている気がする。
]]>Laravelでの機能テストの始め方と簡単な使い方について紹介する。
入門レベルに限るのでより実践的な内容については触れない。
※LTの元ネタ程度でメモくらいの内容。
テストを書いたことがない人向け。
テストを書いたことがなくても機能テストであればアプリケーションの仕様さえわかっていれば比較的に誰にでも楽に何を書くのかわかりやすいと思う。
特にLaravelは機能テストで使える便利なAPIやツールが充実しているので、テストに慣れていなくともテストに取り組みやすいはず。
雑に環境を用意しておいた。
github - bmf-san/laravel-test-handson
READMEの手順どおりコマンドを実行すればDocker上でLaravelの環境がセットアップできる。
テストを実行できる環境の準備として、テスト用のdbを用意し、アプリケーション側ではphpunit.xml、config/database.phpをいじった。
機能テストで使う便利なAPIな使いたいのでgithub - laravel/browser-kit-testingを導入している。
こちらはLaravel5.3まではLaravel本体に組み込まれていたものだったが、5.4くらいから別パッケージになってしまったらしい。
Laravel5.3以降からキャッチアップしていなかったので、今回5.7を触ってから気づいた・・・
大した問題ではないが、別パッケージを導入しなくてもすぐに始められる!という売り文句がいえなくなってしまった、、
実際のコードはここに置いてあるので一部を紹介する。
github - laravel/browser-kit-testing
記事を新規作成する画面があったとする。
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<form method="POST" action="/post/store">
@csrf
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label text-md-right">Title</label>
<div class="col-md-6">
<input id="title" type="text" class="form-control{{ $errors->has('title') ? ' is-invalid' : '' }}" name="title" value="{{ old('title') }}" required autofocus>
@if ($errors->has('title'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('title') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label text-md-right">Body</label>
<div class="col-md-6">
<textarea class="form-control{{ $errors->has('body') ? ' is-invalid' : '' }}" id="body" name="body" required>{{ old('body') }}</textarea>
@if ($errors->has('body'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('body') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
Submit
</button>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
ルーティングがこんな感じ。
Route::get('post/create', 'PostController@create');
Route::post('post/store', 'PostController@store');
するとこんな感じの直感的な機能テストがかける。
<?php
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\User;
use App\Post;
class PostTest extends TestCase
{
use DatabaseMigrations, DatabaseTransactions;
public function testCreatePost()
{
$user = factory(User::class)->create();
$this->actingAs($user);
$this->visit("/post/create");
$this->type("title", "title");
$this->type("body", "body");
$this->press("Submit");
$this->seePageIs("/post");
}
}
LTのネタメモなのでものすごい雑。
このブログのDBバックアップを原始人のごとく手動でやっていたのでコマンド一発でバックアップをリモートからローカルにバックアップを取れるツールをgoでつくってみた。
ざっくり動く形まで実装してみた。Goに不慣れなので愚直な感じになっている。。。
あとテストがかけていない。
package main
import (
"net"
"time"
"io/ioutil"
"golang.org/x/crypto/ssh"
"github.com/BurntSushi/toml"
)
type Config struct {
SSH SSH
Mysql Mysql
}
type SSH struct {
IP string
Port string
User string
IdentityFile string
}
type Mysql struct {
MysqlConf string
Database string
DumpDir string
DumpFilePrefix string
}
func dump() {
var config Config
if _, err := toml.DecodeFile("config.toml", &config); err != nil {
panic(err)
}
buf, err := ioutil.ReadFile(config.SSH.IdentityFile)
if err != nil {
panic(err)
}
key, err := ssh.ParsePrivateKey(buf)
if err != nil {
panic(err)
}
conn, err := ssh.Dial("tcp", config.SSH.IP+":"+config.SSH.Port, &ssh.ClientConfig{
User: config.SSH.User,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(key),
},
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
})
if err != nil {
panic(err)
}
defer conn.Close()
session, err := conn.NewSession()
if err != nil {
panic(err)
}
defer session.Close()
byte, err := session.Output("sudo mysqldump --defaults-file="+config.Mysql.MysqlConf+" "+config.Mysql.Database+" "+"--quick --single-transaction")
if err != nil {
panic(err)
}
ioutil.WriteFile(config.Mysql.DumpDir+config.Mysql.DumpFilePrefix+time.Now().Format("2006-01-02")+".sql", byte, 0644)
}
func main() {
dump()
}
置いといた。
とりあえずgoの色んな実装をみて知見を貯めていく...
PHPで学ぶデザインパターン Advent Calendar 2018で間に合わなかった記事。
機能拡張のためのスーパークラスと実装拡張のためのサブクラスを用意し、機能の橋渡しをするようなパターン。
<?php
interface Connector
{
public function __construct(Converter $converter);
public function connect();
}
class IphoneConnector implements Connector
{
private $converter;
public function __construct(Converter $converter)
{
$this->converter = $converter;
}
public function connect()
{
echo 'Iphone connect by using ' . $this->converter->getTerminalName();
}
}
class AndroidConnector implements Connector
{
private $converter;
public function __construct(Converter $converter)
{
$this->converter = $converter;
}
public function connect()
{
echo 'Android connect by using ' . $this->converter->getTerminalName();
}
}
interface Converter
{
public function getTerminalName();
}
class LightningConverter implements Converter
{
public function getTerminalName()
{
return 'lightning';
}
}
class TypeCConverter implements Converter
{
public function getTerminalName()
{
return 'type-c';
}
}
$lightingConveter = new LightningConverter();
$typeCConverter = new TypeCConverter();
// どっちのconverterを使ってもよい→実装を切り替えられる
$iphoneConnector = new IphoneConnector($lightingConveter);
$androidConnector = new AndroidConnector($typeCConverter);
$iphoneConnector->connect(); // connect by using lighting
$androidConnector->connect(); // connect by using type-c
これは単純なインターフェースの使い方ではないかと思ってしまったのはまだ理解が浅いからな気がする。
PHPで学ぶデザインパターン Advent Calendar 2018で間に合わなかった記事。
元となるクラスに修正を加えることなくインターフェースを変更することができるパターン。
異なるインターフェース間の互換性を調整するようなAdapterクラスを用意することで実現する。
<?php
interface Bird
{
public function fly();
}
class SmallBird implements Bird
{
public function fly()
{
echo 'fly short time';
}
}
class BigBird implements Bird
{
public function fly()
{
echo 'fly long time';
}
}
class Human
{
public function eat(Bird $bird)
{
echo 'Yummy!';
}
}
class MiddleBird
{
public function jump()
{
echo 'jump like flying';
}
}
// Adapter
class MiddleBirdAdapter implements Bird
{
private $middleBird;
public function __construct(MiddleBird $middleBird)
{
$this->middleBird = $middleBird;
}
// MiddleBirdのメソッドをラップする
public function fly()
{
$this->middleBird->jump();
}
}
$human = new Human();
$smallBird = new SmallBird();
$human->eat($smallBird); // Yummy
$middleBird = new MiddleBird();
$middleBirdAdapter = new MiddleBirdAdapter($middleBird);
$human->eat($middleBirdAdapter); // Yummy
インターフェースに定義されたメソッドで振る舞いをラップするメソッドをつくる感じ。
使い所を慎重に考える必要がありそう。
PHPで学ぶデザインパターン Advent Calendar 2018で間に合わなかった記事。
仲介者・調停者の意味。
オブジェクトの振る舞いに関するデザインパターンで、オブジェクト間のやりとり調整するためのパターン。
オブジェクト同士のやり取りが複雑化し、関係性が見えにくくなるような時に有用かもしれない。
<?php
// Mediator
class Receptionist
{
public function checkIn(User $user, $message) // 振る舞いの操作を任せたいオブジェクトを保持
{
echo $message . ' ' . $user->getName();
}
}
class User
{
private $name;
private $receptionist;
public function __construct($name, Receptionist $receptionist) // Mediatorを持つ
{
$this->name = $name;
$this->receptionist = $receptionist;
}
public function getName()
{
return $this->name;
}
public function checkIn($message)
{
$this->receptionist->checkIn($this, $message); // $this!!
}
}
$receptionist = new Receptionist();
$john = new User('John', $receptionist);
$bob = new User('Bob', $receptionist);
$john->checkIn('Welcome!'); // Welcome! John
$bob->checkIn('Hi!'); // Hi! Bob
クラス間のやりとりが複雑化しそうなときにオブジェクトの振る舞いをまとめて管理したいときに思い出したいパターン。
PHPカンファレンス仙台2019に参加レポート。
違和感から設計判断
レガシー度が”圧倒的”でモチベーションが湧きづらそうだと思った.....
2泊3日で行った。前日から参加者同士の交流が毎度のごとく有ったみたいなのでもう少し早い時間にいけば良かったなと・・
仙台初開催ということで運営の方々は大変なことが多かったと思うが、参加費500円は安すぎると思えるくらい良い内容だった。
地元組と遠征組が半々くらいだった印象で、遠征組は東京から来ているよく見かける方々が多かった。
割と色んな人と積極的に話をしたつもりではあるが時間的都合だろうか、地元から参加されている方とお話があまりできなかったのは悔やまれる・・
grep雰囲気で使っているマンだったのでとざっくり調べてみた。l
grep 検索正規表現 ファイル名
ワイルドカードが使えるので、例えばカレントディレクトリ内の全ファイルを対象とした場合は、
grep "foo" ./*
といった具合にできる。
カレントディレクトリ以下のディレクトリも対象にしたい場合は、-r
オプションを使う。
grep -r "foo" ./*
よく使いそうなものだけピックアップした。
grep "foo\|bar" ./*
\
でエスケープが必要。
grep "foo" ./* | grep "bar"
grep "foo" ./* --exclude-dir=vendor
コンテナ技術についてのまとめ。 Dockerを使わずにコンテナをつくって触ってみる。
1979年 UNIX OSにchrootが登場。
2000年 FreeBSD jailsがFreeBSD 4.0に登場。chrootの発展系。
2001年 VServer Projectを通じてLinuxにもLinuxコンテナのベースとなる技術が登場。
2004年、LXC1.0がリリース。 Linux Containers
2008年、Dockerが登場
コンテナ技術は上記以外にもVirtuozzo、OpenVZ、HP-UX Container、Solaris Containerなど存在する。
コンテナ
仮想化
bmf-tech - Dockerとはにもざっくりまとめている。
以前までDockerはlxcを使っていたが、v0.9からgoで実装されたlibcontainerを使っているらしい。(cf. Docker blog - DOCKER 0.9: INTRODUCING EXECUTION DRIVERS AND LIBCONTAINER github - opencontainers/runc/libcontainer/)
Open Container Initiativeはコンテナとランタイムに関する業界標準の作成を目的として組織。
以下の仕様を定義している。
OCIはローレベルランタイムの仕様に関わっている。 ex. runC、gVisor、Kata Containers、Nabla Containers etc...
CRIは、kubeletとコンテナランタイム間の通信のインタフェースを規定している。
CRIはハイレベルランタイムの仕様に関わっている。 ex. docker、containerd、cri-o
Makuake LT Party(社内LT大会)にてLTをした。
とりあえず動く形のものを仕上げてpackagist - ahi-routerという名前でパッケージ公開した。
エピソード1では、データ構造に木構造を採用してルーティングを作ろうというと試みた。
パフォーマンスが考慮されているライブラリでは、木構造を生成するロジックを用意して、最適化された探索アルゴリズムを実装するような形になっているようだが、木構造を生成するロジックをかくのはめん(ry 時間がかかりそうだったので、探索部分だけ頑張る方向性でやってみることにした。
前回はルーティング定義のデータ構造を、
<?php
$routes = [
'/' => [
'GET' => 'HomeController@get',
],
'/users' => [
'/' => [
'GET' => 'UserController@get',
],
'/:user_id' => [
'/' => [
'GET' => 'UserController@get',
'POST' => 'UserController@post',
],
'/events' => [
'/' => [
'GET' => 'EventController@get',
],
'/:id' => [
'GET' => 'EventController@get',
'POST' => 'EventController@post',
],
]
],
'/support' => [
'/' => [
'GET' => 'SupportController@get',
],
]
],
];
としていたが、
<?php
$routes = [
'/' => [
'END_POINT' => [
'GET' => 'IndexController@getIndex',
],
'posts' => [
'END_POINT' => [
'GET' => 'PostController@getPosts',
],
':title' => [
'END_POINT' => [
'GET' => 'PostController@getPostByPostTitle',
'POST' => 'PostController@postPostByPostTitle',
],
':token' => [
'END_POINT' => [
'GET' => 'PostController@getPostByToken',
],
],
],
':category_name' => [
'END_POINT' => [
'GET' => 'PostController@getPostsByCategoryName',
],
],
],
],
];
こんな感じに定義し直した。
変更点としては、
前回は関数で頑張ろうとしたが色々辛かったのでオブジェクトで戦うことにしたらすんなり実装できた。
データ構造を変更したのも実装のしやすさに影響を与えたと思う。
<?php
namespace bmfsan\AhiRouter;
class Router
{
/**
* Path parameters
* @var array
*/
private $params = [];
/**
* Create array for search path from current path
*
* @param string $currentPath
* @return array
*/
public function createArrayFromCurrentPath($currentPath): array
{
$currentPathLength = strlen($currentPath);
$arrayFromCurrentPath = [];
for ($i=0; $i < $currentPathLength; $i++) {
if ($currentPathLength == 1) {
// ルートの時
if ($currentPath{$i} == '/') {
$arrayFromCurrentPath[] = '/';
}
} else {
if ($currentPath{$i} == '/') {
$arrayFromCurrentPath[] = '';
$target = count($arrayFromCurrentPath) - 1;
} else {
$arrayFromCurrentPath[$target] .= $currentPath{$i};
}
}
}
return $arrayFromCurrentPath;
}
/**
* Search a path and return action and parameters
*
* @param array $routes
* @param array $arrayFromCurrentPath
* @param string $requestMethod
* @param array $targetParams
* @return array
*/
public function search($routes, $arrayFromCurrentPath, $requestMethod, $targetParams = []): array
{
$i = 0;
while ($i < count($arrayFromCurrentPath)) {
if ($i == 0) {
$targetArrayDimension = $routes['/'];
}
// Condition for root
if ($arrayFromCurrentPath[$i] == '/') {
$result = $targetArrayDimension['END_POINT'];
break;
}
foreach ($targetArrayDimension as $key => $value) {
if (isset($arrayFromCurrentPath[$i])) {
if (isset($targetArrayDimension[$arrayFromCurrentPath[$i]])) {
$targetArrayDimension = $targetArrayDimension[$arrayFromCurrentPath[$i]];
} else {
// Condition for parameters
$targetArrayDimension = $this->createParams($targetParams, $targetArrayDimension, $arrayFromCurrentPath[$i]);
}
}
// Condition for last loop
if ($i == count($arrayFromCurrentPath) - 1) {
$result = $targetArrayDimension['END_POINT'];
}
$i++;
}
}
return [
'action' => $result[$requestMethod],
'params' => $this->params,
];
}
/**
* Create parameter data
*
* @param array $targetParams
* @param array $targetArrayDimension
* @param string $targetPath
* @return array
*/
private function createParams($targetParams, $targetArrayDimension, $targetPath)
{
for ($i=0; $i < count($targetParams); $i++) {
if (isset($targetArrayDimension[$targetParams[$i]])) {
$this->params[$targetParams[$i]] = $targetPath;
return $targetArrayDimension[$targetParams[$i]];
}
}
}
}
// こんな感じに使う
$currentPath = '/posts/1/abc123!@#';
$currentMethod = 'GET';
$currentParams = [
':title',
':token',
];
$router = new Router();
$currentPathArray = $router->createArrayFromCurrentPath($currentPath);
$router->search($routes, $currentPathArray, $currentMethod, $currentParams);
計算量はざっくりO(n)なっているので、n(ルート定義)が増えるほど計算量は比例して増えていく残念アルゴリズム。
ちゃんとつくるならやっぱり木構造の探索のアルゴリズムはかじっておくべきだろう。察していたが反省した。
アルゴリズムの重要さが身に沁みたような気がする。(小並感)
日頃こんなにグルグルとしたコードを書かないので頭の体操にはなった。(不定期でこういう体操をしてアルゴリズムに慣れていくのは良いと思った)
割とメジャーなルーティングライブラリでも、正規表現を使用していたり、最適化されていないアルゴリズムで実装されていたりするっぽいので今後も色んな実装に目を通したり、アルゴリズムの勉強をしたりしてそのうちルーティングの実装に再挑戦してみたい。
以前、Reactで非常に軟弱なルーティング(cf. ReactとHistory APIを使ってrouterを自作する)を作ったが、改めてそこそこにちゃんとしたルーティングを自作したいと思い、挑戦することにした。
きっかけは、最近触っているGolangだ。
Golangでは標準ライブラリを駆使することでアプリーケーションをうすーく実装できるようだが、ルーティング周りは標準ライブラリがパワー不足なのものあって、外部のライブラリに依存するケースが多いらしい。
そんなこともあってルーティングを自作できるようになるとGolangでもそれ以外でもルーティングを自前で用意できて世界が広がる気がしたので重い腰を上げてやってみることにした。
リクエストされるURLに対して、実行したい処理は何か判定させるもの。
必要に応じて、パスパラメータやクエリパラーメータのデータを処理の実行時に扱えるようにする。
大まかに2パターン。
ルーティングがアプリケーションの実行速度に与える影響の割合はそこまでないかもしれないが、なるべく速いに越したことはないはず。
言語問わずメモリ使用量、計算量の最適化されたアルゴリズムで実装すべし。
今回は木構造で実装するパターンを選択する。
パフォーマンスを測定したわけではないが、正規表現よりも計算量が最適か木構造のアルゴリズムを用いるほうがパフォーマンス的にはよろしい気がするので、木構造にする。
実際、木構造で実装されているライブラリは多い。
グラフ理論という数学の分野で定義されている木の構造を持つデータ構造のこと。
グラフ理論で定義されている木とは、複数の点(nodeまたはvertex)と複数の辺(edge)で構成されたグラフのことである。
○ ・・・根(root)
/ | ・・・枝(edge)
◯ ◯ ・・・節点(noteまたはvertex)
\
◯
\
○ ・・・葉(leaf)
ノードの性質や木の高さなどによって色々な種類の木構造があるが、ここでは割愛。
何を木構造として扱うか?
これはもちろん、ルート定義のリストを木構造として扱う。
実装の流れをざっくり説明すると、ルート定義と現在のURL(パス)をインプットとして与えられたときに、
ルート定義から木構造を生成し、現在のURL(パス)をターゲットとして木構造を探索し、マッチしたデータを返すというだけ。
木構造を扱う際はノードの追加や削除等の処理も実装する場合があるが、URLルーティングの場合はとりあえず不要なので実装しない。
ルーティング定義のDSLを先に決める。
多くのライブラリではシンプルなDSLが提供されているが、今回は複数階層あるちょっと複雑なDSLを定義する。
$routes = [
'/' => [
'GET' => 'HomeController@get',
],
'/users' => [
'/' => [
'GET' => 'UserController@get',
],
'/:user_id' => [
'/' => [
'GET' => 'UserController@get',
'POST' => 'UserController@post',
],
'/events' => [
'/' => [
'GET' => 'EventController@get',
],
'/:id' => [
'GET' => 'EventController@get',
'POST' => 'EventController@post',
],
]
],
'/support' => [
'/' => [
'GET' => 'SupportController@get',
],
]
],
];
先程ルート定義から木構造を生成すると書いたが、ルート定義そのものを最初から木構造となるような形で定義することにする。
なぜこのような形をとったかというと単純に木構造を生成するアルゴリズムを書くのが面倒そうだったからであるが、逆に考えてみるとむしろ余計なアルゴリズムが減ってパーフォマンス的に良いではという気がしているがいかに・・
さほどわかりにくくないルート定義だと思うが、一般的なルーティングライブラリのDSLがこのようになっていないのは何か理由があるはずだとは思っている。
木構造の終端ノードとなる部分(葉)がちょうどHTTPメソッドになる。
木構造とは別にHTTPメソッドのリストを用意しておく。
Golangだとnet/httpに最初から定義されていて楽ですね。今回はPHPでやりますが・・
$methods = [
'GET',
'POST',
// more...
];
インプットして与えられる現在のURL(パス)を木構造の探索の際に使いやすいように配列に加工する関数とその配列とルート定義の配列を引数としてマッチングしたパスのデータを返す関数の2つを実装する。
なお今回はクエリパラーメータは特に考慮していない。
実装方針として、他の言語への移植性を考慮し、ビルトイン関数の使用を極力避けて実装する。
function createCurrentPathArray($routes) {
$currentPath = '/users/1'; // 現在のパス
$currentPathLength = strlen($currentPath);
$currentPathArray = [];
for ($i=0; $i < $currentPathLength; $i++) {
if ($currentPathLength == 1) {
$currentPathArray[] = '/';
} else {
if ($currentPath{$i} == '/') {
$currentPathArray[] = '/';
$target = count($currentPathArray) - 1;
} else {
$currentPathArray[$target] .= $currentPath{$i};
}
}
}
return $currentPathArray;
}
// 探索
// ルート定義と検索対象であるルートの配列を比較して該当するデータを返す。
// リーフに到達したら探索終了
function urlMatch($routes, $currentPathArray) {
// TODO 実装中・・・
}
$currentPathArray = createCurrentPathArray($routes);
$result = urlMatch($routes, $currentPathArray);
var_dump($result); // マッチしたパスのデータが返るはず・・・
実装途中というわけでエピソード1はこれにて終幕。
最初からパトリシア木とかなんとか木とかからやろうとすると大やけどする。
参考になりそうな実装も色々見てみたが、一つ一つを理解するのは中々ハードなので、まずはアルゴリズムのイメージを掴むことと考えながら手を動かすことから始めてみたが、数学的素養が乏しいと辛いところはある。
実装途中ではあるが、割とゴールが見えるような気がしないでもない。
が、こんな感じで実運用に使えそうなベースまで持っていけるか自信はない。
Makuake LT Party(社内LT大会)にてLTをした。
speaker-deck - URLルーティングをつくるエピソード1
「20代が考えるエンジニアキャリア論」というテーマでLTをしてきた。
昨年のPHPカンファレンス2017に引き続き、今年もLT枠で登壇することができてよかったが、来年度こそは25分枠で採択されたい。
PHPカンファレンス参加歴は今年で3回目となるが、毎度PHPのコミュニュティの良さを感じる。
昨年度のPHPカンファレンスから一度も登壇していなかったこともあって、このカンファレンスが1年の締めくくりというか、起点というか自分の中ではそういう位置づけになってきている。
参加回数を重ねるごとになんとなく自分の成長を感じたりすることもある。
(今まで共感できなかった、理解できなかったことがわかるようになってきたり・・)
今年はGrowthがカンファレンスのテーマであったが、自分のこの1年間を表しているように思う。
去年度の振り返りから今年はインプットに重きを置いたが、来年度のテーマであるbeyondにつながるような1年になったような気がする。
来年は登壇駆動開発というほどではないが、技術的なテーマでの登壇機会を増やして、インプットしてきたことを外に出したり、半強制的にアウトプットを生んでいくようなアプローチでやっていきたいと考えている。
Growthからbeyondへ、来年は25分枠での登壇を目指したい。
この記事はPHPで学ぶデザインパターン Advent Calendar 2018の記事です。
今回はStrategyパターンについてかきます。
Strategyパターンは、アルゴリズムの切り替えを容易にするようなパターンです。
異なる処理をそれぞれ別のクラスに定義するため、 処理を動的に選択できるだけでなく、条件分岐を減らすことも可能としてします。
OCP(open/closed principle)に忠実なパターンの一つでもあります。
単純な例でStrategyパターンの実装を見てみます。
<?php
class Context
{
private $notification;
public function __construct(NotificationInterface $notification)
{
$this->notification = $notification;
}
public function execute()
{
return $this->notification->notify();
}
}
interface NotificationInterface
{
public function notify();
}
class SlackNotification implements NotificationInterface
{
public function __construct($message)
{
$this->message = $message;
}
public function notify()
{
echo $this->message . '- sent by Slack';
}
}
class EmailNotification implements NotificationInterface
{
public function __construct($message)
{
$this->message = $message;
}
public function notify()
{
echo $this->message . '- sent by Email';
}
}
$message = "Hello World!";
$slack = new SlackNotification($message);
$context = new Context($slack);
$context->execute(); // Hello World - sent by Slack
$email = new EmailNotification($message);
$context = new Context($email);
$context->execute(); // Hello World - sent by Email
Contextクラスの実装をinterfaceを依存させることで"戦略”をクライアント側から切り替えられるようにしています。
これはよく見る実装なのではないでしょうか?
振る舞いが分離されているので、OCPに従った形になっているはず。
拡張に対しては、NotificationInterfaceの実装を追加するだけ、修正に対しては、NotificationInterfaceの実装を修正するだけ、という感じになっているかと思います。
GoFは知らずしらずに利用していることがあるのでちゃんと覚えておいても損はないなーと思いました。
anyenvでインストールしたrbenvでbundlerをinstallしたときにパスでハマった話。
anyenvでrbenvをインストールしてrubyを使っているのですが、bundlerをインストールする際に、
gem install bundler
と何も考えずに打つと、bundlerが/usr/local/bin/
以下に配置されてしまう。
意図したパスでないためgemでinstallしたchefとか使おうとするとコケる。
rbenv exec gem install bundler
rbenvで導入しているrubyのgemを実行するように指定する。
パスを冷静に確認していればrubyに不慣れでもすぐわかったはず...
Factory・Factory Method・Abstract Factoryについてかきます。
まずはFactoryパターンについてざっくり説明します。
interface Robot
{
public function say();
}
class BlueRobot implements Robot
{
private $color;
public function __construct($color)
{
$this->color = $color;
}
public function say()
{
echo $this->color;
}
}
class YellowRobot implements Robot
{
private $color;
public function __construct($color)
{
$this->color = $color;
}
public function say()
{
echo $this->color;
}
}
class RobotFactory
{
public function create($color)
{
if ($color === 'blue') {
return new BlueRobot($color);
}
if ($color === 'yellow') {
return new YellowRobot($color);
}
throw new Exception("Can't create an instance");
}
}
$robotFactory = new RobotFactory();
try {
$blueRobot = $robotFactory->create('blue');
$blueRobot->say(); // blue
$yellowRobot = $robotFactory->create('yellow');
$yellowRobot->say(); // yellow
$greenRobot = $robotFactory->create('green');
$greenRobot->say(); // Cat't create an instance
} catch (\Exception $e) {
echo $e->getMessage();
}
その名の通り、工場(Factory)はオブジェクトの”生成”を担います。
オブジェクトの生成と利用を分離することで、利用側はオブジェクトの生成順や種類を知らなくてもオブジェクトを生成することができます。
オブジェクトの生成場所を1箇所にまとめることができるので、生成順や種類等の変更が容易になります。
上述のFactoryパターンでは、生成したいオブジェクトのクラスが増えると条件分岐が増えてしまい、辛い未来が見えてきそうです。
そこで、オブジェクトのクラスを指定することなく、オブジェクトを生成するようにしようというのがFactory Methodパターンです。
Factoryを抽象化して、オブジェクトの生成処理をサブクラスに任せることで、条件分岐をなくすことができます。
生成するオブジェクトを変更する場合はFactoryの切り替えを行う形になります。
interface Robot
{
public function say();
}
class BlueRobot implements Robot
{
public function say()
{
echo 'Blue';
}
}
class YellowRobot implements Robot
{
public function say()
{
echo 'Yellow';
}
}
abstract class RobotFactory
{
abstract protected function create();
public function do()
{
$robot = $this->create();
$robot->say();
}
}
class BlueRobotFactory extends RobotFactory
{
protected function create()
{
return new BlueRobot();
}
}
class YellowRobotFactory extends RobotFactory
{
protected function create()
{
return new YellowRobot();
}
}
$blueRobot = new BlueRobotFactory();
$blueRobot->do(); // blue
$yellowRobot = new YellowRobotFactory();
$yellowRobot->do(); // yellow
FactoyパターンのときのコードをFactory Methodパターンを適用した形に変更してみました。
引数によって生成されるオブジェクトを切り替えていた部分が大きく変わりました。
FactoryパターンとFactory Methodパターンはオブジェクトの生成パターンが異なるものですが、両者を混同している記事がいくつか見受けられて厄介だなーと思いました。
Abstract Factoryパターンは、複数のFactoryを共通のテーマによってグループ化するようなパターンです。
interface Robot
{
public function say();
}
class BlueRobot implements Robot
{
public function say()
{
echo 'Blue';
}
}
class YellowRobot implements Robot
{
public function say()
{
echo 'Yellow';
}
}
interface RobotCreator
{
public function work();
}
class BlueRobotCreator implements RobotCreator
{
public function work()
{
echo '青いロボットつくるよ';
}
}
class YellowRobotCreator implements RobotCreator
{
public function work()
{
echo '黄色いロボットつくるよ';
}
}
interface RobotFactory
{
public function createRobot();
public function createRobotCreator();
}
class BlueRobotFactory implements RobotFactory
{
public function createRobot()
{
return new BlueRobot();
}
public function createRobotCreator()
{
return new BlueRobotCreator();
}
}
class YellowRobotFactory implements RobotFactory
{
public function createRobot()
{
return new YellowRobot();
}
public function createRobotCreator()
{
return new YellowRobotCreator();
}
}
$blueRobotFactory = new BlueRobotFactory();
$blueRobot = $blueRobotFactory->createRobot();
$blueRobotCreator = $blueRobotFactory->createRobotCreator();
$blueRobot->say(); // blue
$blueRobotCreator->work(); // 青いロボットつくるよ
$yellowRobotFactory = new YellowRobotFactory();
$yellowRobot = $yellowRobotFactory->createRobot();
$yellowRobotCreator = $yellowRobotFactory->createRobotCreator();
$yellowRobot->say(); // yellow
$yellowRobotCreator->work(); // 青いロボットつくるよ
Abstract FactoryはFactory Methodの発展した感じという印象です。
Factoryのインターフェースを定義しているという点で、より上位概念からの解釈ができると思うのですが、今回はニュアンスを掴むまでに留めておきます。
それぞれのパターンのニュアンスの違いがわかったような気がします。
もう少しオブジェクト指向の真髄を知っていたらより深い解釈ができそうです。
正規表現の基本。
ERE(Extended regular expression)で扱える記法の中でよく使いそうなやつをまとめる。
#量指定子
試して理解 Linuxの仕組みのメモリ管理の章を読んでいて理解の乏しい単語があったのでいくつかピックアップしてまとめる。
2進数、10進数、16進数をそれぞれ変換するための計算方法についてまとめる。
計算する前の前提として、重みについて理解する。
重みとはそれぞれの桁を表す数のこと。
ex. 10進数1234
10^04 = 4
10^13 = 30
10^22 - 200
10^31 = 1000
sum 1234
10^0、10^1、10*2...が重み。
ex. 2進数1101
2^01 = 1
2^10 = 0
2^21 = 4
2^31 = 8
sum 13
2^0、2^1、2^2...が重み。
2進数なら2、10進数なら10、16進数なら16。
重みとそれぞれの桁の数字を乗算し、全てを和算する。
ex. 1010
2^00 = 0
2^11 = 2
2^20 = 0
2^31 = 8
sum 10
ちょっと変わった割り算をする。
10進数から2進数にするには、2の割り算をし、余りがあれば1、なければ0とし、最後に計算した結果から余りを並べる。
ex. 100
100/2 = 50 余りなし 0
50/2 = 25 余りあり 0
25/2 = 12 余りなし 1
12/2 = 6 余りなし 0
6/2 = 3 余りあり 1
3/2 = 1 余りあり 1 // 最後が1になったら終了
下から並べる 110100
Ans. 110100
2進数の3桁は2^3=8
2進数を8進数に変換するには3桁ずつ区切って計算する
最後にそれぞれ区切って計算した結果を並べる。
ex. 100100
100
2^0*0 = 0
2^1*0 = 0
2^2*1 = 4
sum 4
100
2^0*0 = 0
2^1*0 = 0
2^2*1 = 4
sum 4
Ans. 44
ex. 1100
100
2^0*0 = 0
2^1*0 = 0
2^2*1 = 4
sum 4
1
sum 1
Ans. 14
各桁の数字を3桁の2進数で表す。最後に上位の0は省略する。
ex. 117
7 → 111
1 → 001
1 → 001
001001111 → 100111
Ans. 100111
2進数の4桁は2^4=16
2進数を10進数に変換するには4桁ずつ区切って計算する
最後にそれぞれ区切って計算した結果を並べる。
16進数
0 1 2 3 4 5 6 7 8 9 A B C D E F 10
10進数
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
ex. 11001100
1100
2^0*0 = 0
2^1*0 = 0
2^2*1 = 4
2^3*1 = 8
sum 12 → C
1100
2^0*0 = 0
2^1*0 = 0
2^2*1 = 4
2^3*1 = 8
sum 12 → C
Ans. CC
ex. 1100
1100
2^0*0 = 0
2^1*0 = 0
2^2*1 = 4
2^3*1 = 8
sum 12 → C
Ans. C
各桁の数字を4桁の2進数で表す。最後に上位の0は省略する。
ex. 8B6
6 → 0110
B → 1011
8 → 1000
Ans. 100010110110
この記事はPHP Advent Calendar 2018の記事です。(ちょっと早めに投稿しています)
インターフェースはメソッドの実装を保証する”契約”的意味合いの他、タイプヒンティングによって実装を抽象に依存させる(=実装の切り替えをしやすくする)こともできる。
基本的なインターフェースの定義と実装。
<?php
interface Action
{
public function say();
}
class Superman implements Action
{
public function say()
{
echo "Hello World";
}
}
$obj = new Superman();
$obj->say();
タイプヒンティングでインターフェース型を指定すると実装に柔軟性を持たせることができる。
<?php
interface HeroAction
{
public function say();
}
class Superman implements HeroAction
{
public function say()
{
echo "I'm a Superman";
}
}
class Human
{
public function say()
{
echo "I'm a Human";
}
}
class Bot
{
public function do(HeroAction $heroAction) // 引数にインターフェース型を指定
{
$heroAction->say();
}
}
$superMan = new SuperMan();
$human = new Human();
$bot = new Bot();
$bot->do($superMan); // I'm a Superman
$bot->do($human); // PHP Fatal error: Uncaught TypeError: Argument 1 passed to Bot::do() must implement interface HeroAction, instance of Human given, called in ....
Supermanの実装を取りやめて、Hypermanの実装に切り替える。
<?php
interface HeroAction
{
public function say();
}
// class Superman implements HeroAction
// {
// public function say()
// {
// echo "I'm a Superman";
// }
// }
class Hyperman implements HeroAction
{
public function say()
{
echo "I'm a Hyperman";
}
}
class Human
{
public function say()
{
echo "I'm a Human";
}
}
class Bot
{
public function do(HeroAction $heroAction) // 引数にインターフェース型を指定
{
$heroAction->say();
}
}
// $superMan = new SuperMan();
$hyperMan = new HyperMan();
$human = new Human();
$bot = new Bot();
// $bot->do($superMan); // I'm a Superman
$bot->do($hyperMan); // I'm a Hyperman
$bot->do($human); // PHP Fatal error: Uncaught TypeError: Argument 1 passed to Bot::do() must implement interface HeroAction, instance of Human given, called in ....
もし、Botクラスのdoメソッドがインターフェースではなく、Supermanクラスに依存していた場合、実装を交換する手間が増えてしまう。
class Bot
{
public function do(Superman $superman) // 引数にインターフェース型を指定
{
$superman->say();
}
}
Golangのインターフェースについてまとめる。
type <型名> interface {
<メソッド名(<引数の型>, ...)(<戻り値の型>, ...)
}
// Ex.
type Human interface {
say() string
}
interface型で宣言された変数はどんな型の値でも代入ができる。
var i interface{}
i = 123
i = "Hello World"
i = []int{1, 2, 3} // etc...
interface型を引数にすると、どんな型の値でも渡すことができる。
package main
import "fmt"
type Human struct {
Name string
Age int
}
func printType(i interface{}) {
fmt.Printf("%T\n", i)
}
func main() {
h := Human{
Name: "John",
Age: 20,
}
printType(h) // main.Human
}
型アサーションの構文。
<変数>.(<型>)
使い方は変数2つを取るような形で使う。
s, ok := i.(Human)
変数iがHuman型であった場合は、変数sはHuman型の変数iの実際の値が、変数okにはtrueが格納される。
逆に、変数iがHuman型ではない場合は、変数sにはHuman型のゼロ値が格納される。
package main
import "fmt"
type Human struct {
Name string
Age int
}
type Alien struct {
Name string
Age int
}
func printOnlyHuman(i interface{}) {
s, ok := i.(Human)
if !ok {
fmt.Printf("%v\n", "Human型ではない")
fmt.Printf("%v\n", s)
return
}
fmt.Printf("%v\n", "Human型である")
fmt.Printf("%v\n", s)
}
func main() {
h := Human{
Name: "John",
Age: 20,
}
a := Alien {
Name: "Tom",
Age: 200000,
}
printOnlyHuman(h) // Human型である。{John 20}
printOnlyHuman(a) // Human型ではない。{ 0}
}
Golangのインターフェースのポピュラーな使用方法である、「異なる型に共通の性質を付与する」使い方の例。
package main
import "fmt"
type Action interface {
say()
}
type Human struct {}
type Alien struct {}
func (h *Human) say() {
fmt.Println("I'm Human")
}
func (a *Alien) say() {
fmt.Println("I'm Alien")
}
func do(a Action) { // Action型を受け取る
a.say()
}
func main() {
ha := []Action{
&Human{},
&Alien{},
}
for _, v := range ha {
do(v)
}
}
Golangのポインタの基本では参照渡しと値渡しの違いの観点からポインタについて書いたが、それ以前にポインタを扱っているうちに混乱してきたため、ポインタの概要をまとめる。
var s *string // 変数sはstring型のポインタ。型は*string
package main
import "fmt"
func main() {
/**
* ポインタ型の定義
*/
var pointer *string // *string型のポインタ変数
var s string
s = "Hello World"
/*
* アドレス演算子でポインタを扱う
*/
fmt.Printf("%T\n", pointer) // *string
fmt.Printf("%v\n", pointer) // <nil> ※初期化されていないポインタ型の値はnil
fmt.Printf("%T\n", &pointer) // **string ※pointerは*string型のポインタ変数なので、&pointerは*stringのポインタを生成する
fmt.Printf("%v\n", &pointer) // 0xc00000c028 ※&を使ってポインタのアドレスを得る
fmt.Printf("%T\n", s) // string
fmt.Printf("%v\n", s) // Hello World
pointer = &s // ※*string型のポインタ変数(値は初期化前なのでnil)にsのポインタ型を生成し、アドレスを代入。(変数pointerは*string型のポインタ変数として定義済みなので、&を使って変数sのポインタ型を生成し、代入。)
fmt.Printf("%T\n", pointer) // *string ※pointerは*string型のポインタのまま
fmt.Printf("%v\n", pointer) // 0xc00000e1e0
fmt.Printf("%v\n", *pointer) // Hello World ※pointer(*string型ポインタ変数)から値を参照する
fmt.Printf("%T\n", s) // string
fmt.Printf("%v\n", s) // Hello World
fmt.Printf("%T\n", &s) // *string
fmt.Printf("%v\n", &s) // 0xc00000e1e0
fmt.Printf("%v\n", *s) // invalid indirect of s (type string) ※sはポインタ型変数ではないためエラー。indirectは間接の意
*pointer = "New World" // ※*pointerは変数なので代入できる。ポインタの変数定義は*を使う。
fmt.Printf("%T\n", *pointer) // string ※*pointerはポインタ変数から値を参照している
fmt.Printf("%v\n", *pointer) // New World
}
package main
import "fmt"
type Human struct {
Name string
Age int
}
func main() {
var h Human
fmt.Printf("%v\n", h.Name) // "" ※stringのゼロ値
fmt.Printf("%v\n", h.Age) // 0 ※intのゼロ値
h = Human{
Name: "Tom",
Age: 20,
}
fmt.Printf("%v\n", h) // {Tom 20}
}
package main
import "fmt"
type Human struct {
Name string
Age int
}
func main() {
var h *Human // ポインタ型変数の定義
h = &Human{ // ※変数hは*Human型のポインタ型なので、&を使って構造体Humanから*Human型のポインタ型を使って代入。
Name: "John",
Age: 20,
}
fmt.Printf("%T\n", h) // *main.Human ※&HumanでHumanのポインタ型を生成している
fmt.Printf("%v\n", h) // &{John 20} ※*main.Human型のポインタ
fmt.Printf("%v\n", *h) // {John 20} ※値の取り出し
h.Name = "Tom"
h.Age = 40
fmt.Printf("%T\n", h) // *main.Human
fmt.Printf("%v\n", h) // &{Tom 40}
fmt.Printf("%v\n", *h) // {John 40} ※値の取り出し
}
package main
import "fmt"
type Human struct {
Name string
Age int
}
func main() {
h := new(Human)
h.Name = "John"
h.Age = 20
fmt.Printf("%T\n", h) // *main.Human
fmt.Printf("%v\n", h) // &{John 20}
h.Name = "Tom"
h.Age = 40
fmt.Printf("%T\n", h) // *main.Human
fmt.Printf("%v\n", h) // &{Tom 40}
fmt.Printf("%v\n", *h) // {Tom 40} ※値を取り出した
}
package main
import "fmt"
type Human struct {
Name string
Age int
}
func (h Human) say(msg string) {
fmt.Printf("%v(%v) said %v\n", h.Name, h.Age, msg)
}
func main() {
var h Human // Human型の変数hを定義
fmt.Printf("%T\n", h) // main.Human
fmt.Printf("%v\n", h) // { 0} ※NameとAgeのゼロ値
h = Human{
Name: "Taro",
Age: 20,
}
fmt.Printf("%T\n", h) // main.Human
fmt.Printf("%v\n", h) // {Taro 20}
h.say("Hello") // Taro(20) said Hello
}
package main
import "fmt"
type Human struct {
Name string
Age int
}
// ポインタレシーバ
func (h *Human) say(msg string) {
fmt.Printf("%v(%v) said %v\n", h.Name, h.Age, msg)
}
func main() {
var h *Human // ※ポインタ型の定義
fmt.Printf("%T\n", h) // *main.Human
fmt.Printf("%v\n", h) // <nil> ※ポインタ型のゼロ値
h = &Human{
Name: "Taro",
Age: 20,
}
fmt.Printf("%T\n", h) // *main.Human ※&Human構造体のポインタを生成し、hに代入済み
fmt.Printf("%v\n", h) // &{Taro 20}
h.say("Hello") // Taro(20) said Hello
}
package main
import "fmt"
type Human struct {
Name string
Age int
}
func (h Human) setDataForValue(name string, age int) {
h.Name = name
h.Age = age
}
func (h *Human) setDataForPointer(name string, age int) {
h.Name = name
h.Age = age
}
func main() {
// 値関数の呼び出し
var hForValue Human
hForValue = Human{
Name: "Taro",
Age: 20,
}
hForValue.setDataForValue("Jiro", 40)
fmt.Printf("Name: %v\n Age: %v\n", hForValue.Name, hForValue.Age) // Name: Taro Age: 20
// ポインタ関数の呼び出し
var hForPointer *Human
hForPointer = &Human{
Name: "Taro",
Age: 20,
}
hForPointer.setDataForPointer("Jiro", 40)
fmt.Printf("Name: %v\n Age: %v\n", hForPointer.Name, hForPointer.Age) // Name: Jiro Age: 40
}
関数内で構造体のフィールドの値を変更したいときはポインターレシーバを使う。
mapやchanのような参照型をレシーバーに扱う場合は値レシーバでも良い。
ただし、厳密にパフォーマンスを考慮したりする場合には使い分けはこの限りではない。
前半に記述したポインタ型の定義、演算子の役割について思い出す。
Golangでの変数定義・宣言のパターンをまとめる
var i int
fmt.Printf("%T", i) // int
var a, b, c string
fmt.Printf("%T", a) // string
fmt.Printf("%T", b) // string
fmt.Printf("%T", c) // string
var s = "Hello World"
fmt.Printf("%T", s) // string
var x, y, z int = 1, 2, 3
fmt.Printf("%T", x) // int
fmt.Printf("%T", y) // int
fmt.Printf("%T", z) // int
var (
a string
x int
y, z = 2, 3
)
fmt.Printf("%T", a) // string
fmt.Printf("%T", x) // int
fmt.Printf("%T", y) // int
fmt.Printf("%T", z) // int
var i, j = 1, 2
fmt.Printf("%T", i) // int
fmt.Printf("%T", j) // int
i := 1
fmt.Printf("%T", i) // int
i, j := 1, 2
fmt.Printf("%T", i) // int
fmt.Printf("%T", j) // int
ElasticSearchについての説明とDockerでの環境構築についてざっくりとまとめる。
ElasticsearchとKibanaが使える環境を構築する。
docker-compose.yml
elasticsearch:
image: elasticsearch:5
ports:
- 9200:9200
- 9300:9300
volumes:
- ./elasticsearch/data:/usr/share/elasticsearch/data/
kibana:
image: kibana:5
ports:
- 5601:5601
links:
- elasticsearch
environment:
- ELASTICSEARCH_URL=http://127.0.0.1:9200
最近、ブログの流入率が少しずつ増加してきているので、分析しつつ、施策を考えてみようかと思い、よく見る指標をまとめてみた。
Webマーケティングの知識が浅いの勉強したり、指標とにらめっこして色々思考してみたい。
]]>Golangの関数において、以下3つについてまとめる。
package main
import (
"fmt"
"testing"
)
func sayHi() string {
return "Hello"
}
func main() {
greetA := sayHi()
greetB := sayHi
fmt.Println(greetA)
fmt.Println(greetB())
}
package main
import "fmt"
// コールバック関数
func add(n int) int {
return n
}
func sum(v int, r func(int) int) int {
return r(v)
}
func main() {
fmt.Println(sum(1, add))
}
関数sum
は2つの引数を定義している。
ちなみにmain関数内で実行されているaddにはアドレスが格納されている。
fmt.Println("%v", add) // 0x10936d0
PHPでは可変変数を利用したり、call_user_funcを使ってコールバックを実現していた。
無名関数を関数値として扱う場合の例
package main
import "fmt"
func main() {
sum := func (n int) int {
return n + 1
}
fmt.Println(sum(1))
}
無名関数としてクロージャーとして定義する場合の例
package main
import "fmt"
func count() func() int {
var count int
return func() int {
count++
return count
}
}
func main() {
countUp := count()
fmt.Println(countUp()) // 1
fmt.Println(countUp()) // 2
fmt.Println(countUp()) // 3
}
クロージャーを使うとスコープ範囲がオープンになるので、countの値をキープできる。
雰囲気でコールバック関数を使っている節があるので、コールバック関数の仕組みを深掘りしたいと思った。
]]>Jestを使ってJavaScriptのテストをかいてみる。
jestとESModulesを使いたいのでbabel-preset-2015をインストールしておく。
(babel-jestはjestに用意されている。)
npm install --save-dev jest babel-preset-2015
.babelrc
の中身はこんな感じ。
{
"presets": ["es2015"]
}
package.json
はこんな感じ。
{
"scripts": {
"test": "jest"
},
"devDependencies": {
"babel-preset-es2015": "^6.24.1",
"jest": "^23.6.0"
}
}
ディレクトリ構成はこんな感じ。tree -a -I "node_modules"
.
├── .babelrc
├── package-lock.json
├── package.json
├── src
│ ├── esmodules
│ │ └── calc.js
│ └── native
│ └── calc.js
└── test
├── esmodules
│ └── calc.test.js
└── native
└── calc.test.js
6 directories, 7 files
テストファイルの作成方法については2パターンある。
__tests__
という名前のディレクトリ以下に存在するファイルをテストファイルとするパターン*.spec.js
または*.test.js
を拡張子とするファイルをテストファイルとするパターン今回は後者の形式をとり、test
ディレクトリにテストファイルを設置する。
足し算引き算をする関数を実装する。
./src/native/calc.js
const counter = 1
const add = function add(num) {
return counter + num
}
const subtract = function subtract(num) {
return counter - num
}
module.exports = {
add, subtract
}
それぞれの関数が正しい計算結果を返すかテストする。
./test/native/calc.test.js
const calc = require("../../src/native/calc")
describe('Calc - native', () => {
test('add', () => {
expect(calc.add(1)).toBe(2)
})
test('subtract', () => {
expect(calc.subtract(1)).toBe(0)
})
})
describe(name, fn)
は複数のテストをテストスイートしてグループ化したブロックを作成する。
テストを実行。npm test ./test/native/calc.test.js
テスト結果。
> @ test /Users/k.takeuchi/localdev/project/til/javascript/test/jest
> jest "./test/native/calc.test.js"
PASS test/native/calc.test.js
Calc - native
✓ add (2ms)
✓ subtract (1ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.887s, estimated 1s
Ran all test suites matching /.\/test\/native\/calc.test.js/i.
足し算引き算をするメソッドを実装したクラスを作成する。
./src/esmodules/calc.js
export default class Calc {
constructor(counter) {
this.counter = counter
}
add(num) {
this.counter += num
return this.counter
}
subtract(num) {
this.counter -= num
return this.counter
}
}
それぞれのメソッドが正しい計算結果を返すかテストする。
./test/esmodules/calc.test.js
import Calc from "../../src/esmodules/calc"
describe('Calc - esmodules', () => {
test('add', () => {
const calc = new Calc(1)
expect(calc.add(1)).toBe(2)
})
test('subtract', () => {
const calc = new Calc(1)
expect(calc.subtract(1)).toBe(0)
})
})
Jest - Expectを参照。
初見でもわかりやすいようにjestのAPIは整理されていると思う。
ドキュメントも読みやすかった。
思ったよりも簡単にテストが始められたので、JavaScriptのテストを積極的に書いていきたい気持ち。
ssh接続のセットアップ方法についてメモ。
サーバーに接続してwheelグループに所属するユーザーを作成しておく
~/.ssh/
にてssh-keygen
で公開鍵・秘密鍵を作成。
ここでは公開鍵をid_rsa.pub
、秘密鍵をid_rsa
として作成する。
公開鍵の中身をコピーしておく。
~/.ssh/
にてconfig
ファイルを作成しておく。
Ex.
Host bmf
HostName 123.45.679.012
User bmf
Port 22
IdentityFile ~/.ssh/id_rsa
~/.ssh/
が存在しない場合はディレクトリを作成する。
パーミッションは700を指定。mkdir .ssh && chmod 700 .ssh
続いて、~/.ssh/
にてauthorized_keys
という名前のファイルを作成。
パーミッションは600を指定。authorized_keys
には公開鍵の中身を貼り付ける。
次に/etc/ssh/sshd_config
の設定を調整する。
以下の設定を調整。
ssh接続で使用するポート番号が空いている確認。firewall-cmd --list-all
空いていない場合は開放。firewall-cmd --permanent --zone=public --add-port=22/tcp
リロード。
firewall-cmd --reload
ssh bmf
PrometheusとGrafanaでリソース監視の環境を構築したのでメモ。
Prometheus - Getting Startedに従ってprometheusをダウンロードする。
ダウンロードしたらprometheusを起動、ダッシュボードにアクセスできることを確認しておく。
ポートが開放されていない場合は開放しておく。
Prometheus - node_exporter からnode_exporterをダウンロード。
こちらも起動しておく。
ポートの確認も同様。
Prometheus.yml
のstatic_configs
の項目に以下を追加する。
- targets: ['localhost:9100']
killall prometheus
してからprometheusを再起動する。
Grafana - Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)に従ってgrafanaをダウンロードする。
Yum Repositoryを追加してインストールした。
インストールしたらgrafanaを起動、アクセスできることを確認しておく。
こちらもポートが開放されていない場合は開放しておく。
Grafanaのインストールが完了したら、Grafanaにアクセスして、まずはログインする。
初期のログイン情報はusernameがadmin、passwordがadmin。
ログイン後にログイン情報は変更できるので適宜調整。
左側メニューにWindowsみたいなアイコンがあるので、それをクリックしてDashboards→Homeをクリック。
Data Sourceの設定をするのでAdd data sourceを選択。
設定方法はさくらのナレッジ - PrometheusとGrafanaを組み合わせて監視用ダッシュボードを作るを参照。
HTTP settingsのURLがplacefolderのデザインのせいでデフォルトで指定されるものだと勘違いしていて、未設定のまま作業を進めていたらグラフがちゃんと生成されなかった。
設定するのを忘れずに。
Grapana LabsでPrometheus用のダッシュボードテンプレートを用意する。
Prometheus systemby Thomas CheronneauでCopy ID to Clipboard
をクリック。
左側メニューの+アイコン→Dashboards→Importを選択。
Grafana.com DashboardにIDをペース→Loadをクリック。
OptionsのData sourceでprometheus(PrometheusのData source)を選択。
ざっと雑にまとめたがこれで監視ができるはず。
アラートとかもちゃんと設定できるらしいのでそのうちやってみたい。
Golangのポインタ基礎についてまとめる。
変数とメモリの関係についてイメージできる程度の知識
ポインタは変数のアドレスを指す。
変数のアドレスを通じて呼び出し元の変数の値を変更することができる。
GolangではC言語ライクなポインタは用意されている。
Golangでのポインタでは、変数Tのポインタは*T型で、ゼロ値はnilとなる。
package main
import "fmt"
func main() {
i := 10
// &(アドレス演算子)を使い、変数のアドレスにアクセスする
// 変数pointerに変数iのアドレスを格納
pointer := &i
// アドレスが同じことを確認する
fmt.Println(&i)
fmt.Println(pointer)
// ポインタを示す変数pointerから*を使って値を取り出す
// 呼び出し元の変数iの値が変更されていることを確認
*pointer = 100
fmt.Println(*pointer) // 100
fmt.Println(i) // 100に変更されている
}
C言語ではポインタ演算があるが、Golangではない。
関数に変数を引数として渡すときの方法のひとつ。
値渡しは変数の値は別のアドレスにコピーされる。
別のアドレスにコピーされるため呼び出し元の変数に影響はない。
package main
import (
"fmt"
)
func foo(x int) (int) {
return x
}
func main() {
x := 1
foo(x) // 値渡し 変数xの値がfoo関数の仮引数xに別アドレスとしてコピーされる
fmt.Println(x) // 10
}
Golangでは関数への引数の渡し方は基本的には値渡しだが、スライスやマップを使うときはこの限りではない。(スライスの場合、スライスの変数がスライス自体ではなくスライスへの参照を保持しているため)
参照渡しされて変数は、呼び出し元の変数の値、アドレスすべて同じになる。
別アドレスに変数の値をコピーする値渡しと異なり、コピーではないための呼び出し元の関数に影響がある。
ポインタとの違いは、ポインタはアドレスと値の操作を別々に行うのに対し、参照はアドレスも値も同時に操作する点。
Golangには参照渡しの概念はあるが、参照渡しの書き方はないらしい。
参照型が参照渡しの挙動をする。
&でアドレスを渡す、*でアドレスを渡された変数から値を取り出す。
より深く理解するには、変数とメモリの関係についての深掘りとC言語のポインタを学び直すのが良さそう。
値渡し、参照渡し、ポインタそれぞれについてメモリがどのようにデータを扱うかイメージを持っておくと理解しやすい。
構造体を扱う際にポインタを理解しておく必要がありそうなので、ちゃんと勉強しておく。
Docker for Macのマウントが遅い。
npmとかスロー過ぎて辛い。
メモ書き。
Dockerのスタッフの方のコメントを参照。(リンク先中段)
Docker - File access in mounted volumes extremely slow, CPU bound
MacOSのファイルシステムのAPIが関連しているらしい。
Linux使いたいなという気持ちになった。
]]>CircleCi2.0でPHPUnitのコードカバレッジを出力する
カバレッジの対象としたいソースを指定する。
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
<exclude>
<directory>./app/Providers</directory>
<directory>./app/Exceptions</directory>
<directory>./app/Http/Middleware</directory>
<directory>./app/Providers</directory>
<file>./app/Console/Kernel.php</file>
<file>./app/Http/Kernel.php</file>
<file>./app/Http/Controllers/Controller.php</file>
</exclude>
</whitelist>
</filter>
こんな感じで記述する。
phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="bootstrap/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
<exclude>
<directory>./app/Providers</directory>
<directory>./app/Exceptions</directory>
<directory>./app/Http/Middleware</directory>
<directory>./app/Providers</directory>
<file>./app/Console/Kernel.php</file>
<file>./app/Http/Kernel.php</file>
<file>./app/Http/Controllers/Controller.php</file>
</exclude>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="mysql_test"/>
</php>
</phpunit>
xdebugを使うよりも速いらしいのでphpdbgを使ってhtml形式のカバレッジレポートを作成する。
CI上でメモリ不足で落ちたため、メモリリミットを指定するようにしている。
phpdbg -qrr vendor/bin/phpunit -d memory_limit=512M --coverage-html /tmp/artifacts"
CircleCIのテスト実行後に、artifactsからカバレッジレポートを見れるようにしたいので、store_artifacts
を使ってカバレッジレポートをartifacts
にアップロードするタスクを記述する。
今回はdocker上でテストを行っているため、ホスト側とdocker側でカバレッジレポートのソースをマウントさせる必要があった。
やっつけかもしれないが、docker cp
コマンドでファイルコピーする方法で実行してみた。
.circleci/config.yml
version: 2
jobs:
build:
machine: true
steps:
- checkout
- run:
name: Create a artifacts directory
command: mkdir -p /tmp/artifacts
- run:
name: core-app - Run tests and create a code coverage report
command: docker exec -it rubel_php /bin/sh -c "cd core-app/ && phpdbg -qrr vendor/bin/phpunit -d memory_limit=512M --coverage-html /tmp/artifacts"
- run:
name: Copy the coverage report to host directory
command: docker cp rubel_php:/tmp/artifacts /tmp
- store_artifacts:
path: /tmp/artifacts
カバレッジを出力することができたので各種Webサービス(codacyとかcoverallとか)と連携させてみたい。
ファイルのマウントが必要なことに気づかず時間をかかってしまった....
複数のテーブルに対し、多対1でテーブルが関係付くときのテーブル設計のパターンについてまとめる。
以下のようなケースのデータ設計を例とする。
issues
pullrequests
comments
comments
がissues
、pullrequests
のどちらに対しても多対一で関係付くようなケース。
issues
pullrequests
comments
comments
にtarget_table
とtarget_id
というカラムを追加し、issues
とpullrequests
のどちらに結びつくか判断させようとするテーブル設計。
SQLアンチパターンではアンチパターンの一つとして取り上げられている。
target_id
がtarget_table
を見ないとissues
とpullrequests
のどちらに関連付くかわからないため、外部キー制約が使えない。
したがって、このパターンではテーブル間の整合性保持はアプリケーションのロジックに依存することなる。
LaravelやRailsのORMではポリモーフィック関連がサポートされているので実装が楽なので、このようなパターンを検討する余地はゼロではないが、なるべく避けたいパターンではある。
issues
pullrequests
issues_comments
pullrequests_comments
comments
issues
とpullrequests
に交差テーブルを用意して、外部キー制約を使えるようにするパターン。
issues
とissues_comments
は1対多、issues_comments
とcomments
は多対1となる。pullrequests
に関しても同様。
アプリケーションの要件次第ではあるが、1コメントがissues
とpullrequests
のどちらかだけに関連付くようにという制約を保証できない。
外部キーが使えるため、ポリモーフィック関連よりは整合性を保つことができる。
issues
pullrequests
posts
comments
issues
、pullrequests
、comments
の共通の親となるテーブルを用意するパターン。
posts
はクラステーブル継承の考え方に基づいて定義するのが良さそう。(要は基底クラスと考える)
(参考:単一テーブル継承・クラステーブル継承・具象クラス継承について
PofEAA)
issues
とposts
が1対1、posts
とcomments
が1対多で関連付く。pull_requests
も同様。posts
とcomments
は1対多で関連付く。
1コメントは1postsに関連付くという制約を保証できるが、issues
とpullrequests
のどちらかだけに関連付くという制約を保証できない。
issues
pullrequests
issue_comments
pullrequest_comments
これはそもそもの前提を疑う話ではあるが、comments
を1つのテーブルにまとめておくのではなく、別々のcomments
テーブルをそれぞれ用意してテーブルを分割しておけば良いのではないかというパターンである。
アプリケーション側のロジックに依存することは、ヒューマンエラーの可能性を高めるので、テーブル構造にロジックを依存させる設計方針が基本的には良いパターンではないかと思う。
アプリケーションの要件に加えて、クエリの気持ちを考えて最適なパターンを選択できるようにしたい。
ER図のリレーションシップの種類についてまとめる。
ER図のリレーションシップには3つの種類がある。
リレーションシップの種類 | テーブル関係 | 線の種類 |
---|---|---|
依存リレーションシップ | 子テーブルが親テーブルに依存(テーブル間に親子関係がある) | 実線(親→子) |
非依存リレーションシップ | 子テーブルが親テーブルに依存していない(テーブル間に親子関係がない) | 点線(親→子) |
多対多リレーションシップ | 多対多のテーブル関係 | 実線(親↔子) |
ユーザー
ユーザープロフィール
子テーブルであるユーザープロフィールは、親テーブルであるユーザーのレコードが存在しなければ、ユーザープロフィールは存在しないため、子テーブルは親テーブルに依存しているといえる。
ユーザー
ユーザープロフィール
企業
先程説明したとおり、ユーザーとユーザープロフィールの関係は依存関係にある。
ユーザーと企業の関係は、ユーザーが存在しなくとも顧客は存在でき、
逆も然りなので、非依存関係にあるといえる。
ユーザー
権限
いわゆる交差(中間、ピボット)テーブルが必要となるリレーション。
書籍を読み漁ってもう少し深掘りしたい。
MySQLのJOINの種類とUNIONについてまとめる
指定カラムの値が一致するレコード同士を結合する。
指定したカラムの値が一致しない場合は結合しない。
(両方のテーブルに一致するデータだけ結合される。)
users
+------+--------+------+
| id | sex | name |
+------+--------+------+
| 0 | male | John |
| 1 | female | Risa |
| 2 | male | Taro |
+------+--------+------+
accounts
+------+---------+---------------------+
| id | user_id | created_at |
+------+---------+---------------------+
| 0 | 0 | 2018-07-18 14:47:41 |
| 1 | 1 | 2018-07-18 14:48:01 |
| 3 | 3 | 2018-07-18 15:07:37 |
+------+---------+---------------------+
SELECT * FROM users INNER JOIN accounts ON users.id = accounts.user_id
+------+--------+------+------+---------+---------------------+
| id | sex | name | id | user_id | created_at |
+------+--------+------+------+---------+---------------------+
| 0 | male | John | 0 | 0 | 2018-07-18 14:47:41 |
| 1 | female | Risa | 1 | 1 | 2018-07-18 14:48:01 |
+------+--------+------+------+---------+---------------------+
usersテーブルのidが2のレコードはaccoutsテーブルに一致するものが含まれないので結合されない。
accountsテーブルのuser_idが3のレコードはusersテーブルに一致するものが含まれないので結合されない。
指定のカラムの値が一致するレコード同士を結合する。
左のテーブルに存在し、右のテーブルに存在しない値はNULLでパディングされる。
(左のテーブルに存在するレコードは全て結合される)
users
+------+--------+------+
| id | sex | name |
+------+--------+------+
| 0 | male | John |
| 1 | female | Risa |
| 2 | male | Taro |
+------+--------+------+
accounts
+------+---------+---------------------+
| id | user_id | created_at |
+------+---------+---------------------+
| 0 | 0 | 2018-07-18 14:47:41 |
| 1 | 1 | 2018-07-18 14:48:01 |
| 3 | 3 | 2018-07-18 15:07:37 |
+------+---------+---------------------+
SELECT * FROM users LEFT OUTER JOIN accounts ON users.id = accounts.id
+------+--------+------+------+---------+---------------------+
| id | sex | name | id | user_id | created_at |
+------+--------+------+------+---------+---------------------+
| 0 | male | John | 0 | 0 | 2018-07-18 14:47:41 |
| 1 | female | Risa | 1 | 1 | 2018-07-18 14:48:01 |
| 2 | male | Taro | NULL | NULL | NULL |
+------+--------+------+------+---------+---------------------+
左のテーブルであるusersテーブルには、idが2のレコードがあるが、右のテーブルであるaccountテーブルには一致するものがないので、NULLでパディングして結合されている。
LEFT OUTER JOINの逆。
指定のカラムの値が一致するレコード同士を結合する。
右のテーブルに存在し、左のテーブルに存在しない値はNULLでパディングされる。
(右のテーブルに存在するレコードは全て結合される)
users
+------+--------+------+
| id | sex | name |
+------+--------+------+
| 0 | male | John |
| 1 | female | Risa |
| 2 | male | Taro |
+------+--------+------+
accounts
+------+---------+---------------------+
| id | user_id | created_at |
+------+---------+---------------------+
| 0 | 0 | 2018-07-18 14:47:41 |
| 1 | 1 | 2018-07-18 14:48:01 |
| 3 | 3 | 2018-07-18 15:07:37 |
+------+---------+---------------------+
SELECT * from users RIGHT OUTER JOIN accounts ON users.id = accounts.id
+------+--------+------+------+---------+---------------------+
| id | sex | name | id | user_id | created_at |
+------+--------+------+------+---------+---------------------+
| 0 | male | John | 0 | 0 | 2018-07-18 14:47:41 |
| 1 | female | Risa | 1 | 1 | 2018-07-18 14:48:01 |
| NULL | NULL | NULL | 3 | 3 | 2018-07-18 15:07:37 |
+------+--------+------+------+---------+---------------------+
右のテーブルであるaccountsテーブルには、user_idが3のレコードがあるが、左のテーブルであるusersテーブルには一致するものがないので、NULLでパディングして結合されている。
MySQLでは、CROSS JOINとINNER JOINは構文上同等。(参考:MySQL 8.2.1.11 ネストした結合の最適化)
テーブルとテーブルの重複を省いた形で結合する。
条件として、列数が同じがテーブルでなければならない。
users
+------+--------+------+
| id | sex | name |
+------+--------+------+
| 0 | male | John |
| 1 | female | Risa |
| 2 | male | Taro |
+------+--------+------+
accounts
+------+---------+---------------------+
| id | user_id | created_at |
+------+---------+---------------------+
| 0 | 0 | 2018-07-18 14:47:41 |
| 1 | 1 | 2018-07-18 14:48:01 |
| 3 | 3 | 2018-07-18 15:07:37 |
+------+---------+---------------------+
SELECT * FROM users UNION SELECT * FROM accoounts
+------+--------+---------------------+
| id | sex | name |
+------+--------+---------------------+
| 0 | male | John |
| 1 | female | Risa |
| 2 | male | Taro |
| 0 | 0 | 2018-07-18 14:47:41 |
| 1 | 1 | 2018-07-18 14:48:01 |
| 3 | 3 | 2018-07-18 15:07:37 |
+------+--------+---------------------+
Linuxのパーミッションについてまとめる
最初の1文字は、ファイル種別を表している。
以降3文字単位でファイルのオーナー別に権限を表している。
2~4文字目 ユーザー ファイルの所有者に対する権限
5~7文字目 グループ ファイルの所有グループに対する権限
8~10文字目 その他 その他に対する権限
権限の種類は以下の3つある。
r 読み取り
w 書き込み
x 実行
ファイルとディレクトリのどちらかによって意味合いが変わるので注意。
r 読み取り ファイルの内容を読み出すことができる
w 書き込み ファイルの内容を編集可能
x 実行 ファイル自体をプログラムとして実行可能
r 読み取り ディレクトリ配下のファイル一覧を表示できる
w 書き込み ディレクトリ配下でのファイル作成や削除ができる※1
x 実行 ディレクトリに移動することができる※2
※1 ディレクトリ配下のファイルは書き込み権限がなくとも削除ができる
※2 ディレクトリに実行権限が与えられていない場合、そのディレクトリ移動できなくなる
ls -l hoge.md
-rw-r--r-- 1 bmf staff 652 Jul 18 11:45 hoge.md
hoge.mdのパーミッション
ファイル種別:ファイル
ユーザーに対する権限:読み込み/書き込み
グループに対する権限:読み込み
その他に対する権限:読み込み
数値で指定する方法とアルファベットで指定する方法がある。
パーミッションの設定にはchmodコマンドを使用する。
chmod モード 対象ファイル名
3つの数字を使って、3桁でパーミッションを指定する。
100の位がユーザー、10の位がグループ、1の位がその他。
4 r 読み取り
2 w 書き込み
1 x 実行
複数の権限を与えたい場合は、数字の合計を指定する。
例えば、読み取りと書き込みの権限を与えたい場合は、6を、全ての権限を与えたい場合は、7を指定する。
ls -l hoge.md
-rw-r--r-- 1 bmf staff 652 Jul 18 11:45 hoge.md
chmod 766 hoge.md
-rwxrw-rw- 1 bmf staff 1788 Jul 18 11:57 hoge.md
chmod 変更対象+変更方法+変更内容 対象ファイル
変更対象と変更方法と変更内容をアルファベットと記号で指定する。
u ユーザー
g グループ
o その他
a 全て
複数指定ができる。
例えばユーザーとグループを対象としたい場合は、ugと指定する。
= 指定した権限にする
r 読み取り
w 書き込み
x 実行
複数指定ができる。
例えば、読み取りと書き込みを指定したい場合は、rwと指定する。
ls -l hoge.md
-rw-r--r-- 1 bmf staff 652 Jul 18 11:45 hoge.md
chmod a+rw hoge.md
-rw-rw-rw- 1 k.takeuchi staff 2877 Jul 18 12:24 hoge.md
ls -l hoge.md
-rw-r--r-- 1 bmf staff 652 Jul 18 11:45 hoge.md
ls -lの結果で出力されるパーミッションの項目の次に表示される1という数字は、ハードリンク数を意味している。
]]>ISUcon出場に向けて準備したことを記す。
会社の同僚を誘い、2人チームで参戦。
mysql> use database;
mysql> SELECT table_name, engine, table_rows, avg_row_length, floor((data_length+index_length)/1024/1024) as allMB, floor((data_length)/1024/1024) as dMB, floor((index_length)/1024/1024) as iMB FROM information_schema.tables WHERE table_schema=database() ORDER BY (data_length+index_length) DESC;
初参戦して人権を失ってきた。
練習量が足りていないのはともかく、それ以前に基本的なことができていなかったので、来年はちゃんと戦えるように準備、成長していきたい。
自分の課題を見つめ直したり、今後のモチベーションに大きな影響を与えたりする機会となってそういった意味でも参加することができて良かったと思う。
なんでこれが無料なんだろうというくらいの大会で、運営の皆様方に大変感謝しています。
JavaScriptでアルゴリズムを学ぶ。
リストや配列のデータに対して、先頭から順番に比較を行っていくアルゴリズム。
配列の長さ分処理を繰り返し、目的のデータに到達したら処理を終了する。
目的とするデータが後ろにあるほど処理が遅くなる。
const targetData = 5;
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
(function () {
for (let i = 0; i < data.length; i++) {
if (targetData == data[i]) {
alert(i + '番目でデータを発見');
return;
}
}
alert('データがありません');
}());
ソート済みのリストや配列に対し、中央値との大小関係を判定条件とし、探索範囲を狭めながらデータを探索していく。
初めに中央値を求め、目的のデータと中央値の大小比較を探索範囲の先頭が探索範囲の後尾を上回るまで繰り返す。
const targetData = 5;
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let head = 0; // 探索範囲の先頭
let tail = data.length; // 探索範囲の後尾
(function () {
while (head <= tail) {
let center = Math.floor((head + tail) / 2);
if (data[center] == targetData) {
alert('配列の' + center + '番目でデータを発見');
return;
} else if (data[center] < targetData) {
head = center + 1;
} else {
tail = center - 1;
}
}
alert('データがありません');
}());
目的のデータより中央値のほうが小さい場合は、中央値+1を先頭の値とし、大きい場合は、中央値ー1を後尾の値とする。
ちょっと混乱するが、処理の1回目、2回目、3回目...と順を追って考えてみるとすぐ理解できる。
先頭から順番に並べ替えていくアルゴリズム。
const data = [10, 1, 5, 7, 8, 2];
for (let i = 0; i < data.length-1; i++) { // 配列の長さ分処理を繰り返す
let min = data[i];
let head = i;
for (let headNext = i+1; headNext < data.length; headNext++) {
if (min > data[headNext]) {
min = data[headNext];
head = headNext;
}
}
let tmp = data[i];
data[i] = data[head];
data[head] = tmp;
}
console.log(data);
こちらも処理の1回目、2回目、3回目...と順を追って考えてみると理解しやすい。
先頭の値と先頭+1から末尾までの値の比較を繰り返し、先頭の値より小さければ、先頭の値を置き換える、という処理を配列の長さ分繰り返す。
隣合う値同士を比較してデータを並べ替えるアルゴリズム。
const data = [9, 7, 1, 10, 5];
for (let i = 0; i < data.length; i++) {
for (let dataNext = data.length-1; dataNext > i; dataNext--) {
if (data[dataNext] < data[dataNext-1]) {
let tmp = data[dataNext];
data[dataNext] = data[dataNext-1];
data[dataNext-1] = tmp;
}
}
}
console.log(data);
]]>Unixコマンドのメモ。
JSON形式のデータを加工するコマンド。
echo '[{"name": "Tom", "age": 20}}]' | jq .
Pretty Print以外にもオブジェクトからプロパティを指定してデータを取り出したり、オブジェクトの長さを取得したり、色々な使い方がある。
標準入力を標準出力とファイルの両方に出力する。
sudoが使える。
オプションなしで上書き、オプション-aで追記。
echo 'hello world' | sudo tee ./sample.txt
リダイレクトの場合は、>が上書き、>>が追記。sudoは使えない。
コマンドの実行時刻を予約することができる。
at -f ./sample.txt 2230
日時の部分はフォーマットが色々ある。
ランダムな名前で/tmpディレクトリ以下にファイルを作成する。
mktemp
List of open files
プロセスがオープンしているファイルを出力する。
-i
ポート番号に絞って出力。
-i tcp or udp
TCPやUDPに絞って出力。
-P
ポート番号を数字で出力。
-n
IPアドレスをホスト名に逆引き変換せずに出力。
ネットワーク経由で対象とするホストのポート状況を調べる。
nmap 192.168.33.10
ファイルシステムのI/Oを一時的に停止させるコマンド。
ファイルシステムのI/Oを一時的に停止する(フリーズ処理)。
fsfreeze -f /data
ファイルシステムのI/Oを解除する(アンフリーズ処理)。
fsfreeze -u /data
マウントしているファイルシステムの情報を出力する。
]]>プロセスとスレッドの違いについてまとめる
プロセスがメモリに保持しているデータ構造は2つのセグメントに分かれている。
テキストセグメント
データセグメント
プログラムを並列に実行した時のプロセスとスレッドの違い
エンドポイントに対して、HTTP動詞でリクエストを投げる
curl https://api.bmf-tech.com/v1/configs
[
{
"id": 1,
"name": "title",
"alias_name": "Title",
"value": "bmf-tech",
"created_at": "2017-09-25 23:08:23",
"value": "bmf-tech",
"deleted_at": null
}
]
単一のエンドポイントに対し、クエリを投げる
curl https://api.bmf-tech.com/api
configs {
id,
name,
alias_name
value,
created_at,
updated_at,
deleted_at
}
[
{
"id": 1,
"name": "title",
"alias_name": "Title",
"value": "bmf-tech",
"created_at": "2017-09-25 23:08:23",
"value": "bmf-tech",
"deleted_at": null
}
]
REST API | GraphQL | |
---|---|---|
エンドポイント | 複数 | 単一 |
HTTP動詞 | 依存している | 依存していない |
型システム | 無し | 有り |
バージョニングの必要 | 有り | 無し |
ドキュメントの必要性 | 有り | 無し |
リソース制限 | コール回数が主 | リソース量に応じて対応 |
単一エンドポイントに対して欲しいデータを柔軟に指定
リソース制限には工夫が必要
ドキュメントの必要性がほぼない
ライブラリ依存になる
必ずしもREST APIよりパフォーマンスがよくなるわけではなさそう
モニタリング
キャッシュ周り
プログラミングで音楽をつくってみたいと思い、音響プログラミングに手を出してみた。
Mac、Git、Atom、Homebrewは既に用意されている前提で話を進める。
brew install ghc
brew install cabal-install
cabal update
cabal install cabal-install
cabal install tidal
Atom - tidalcyclesをAtomにインストール。
SuperColliderからCurrent Versionをインストール。
インストールが完了したら、SuperColliderを起動して以下のコマンドを実行する。(command+enter)
include("SuperDirt")
SuperDirt.start
でSuperDirtを起動。
Atomでtidalcyclesを起動する。.tidal
を拡張子としてファイルを作成し、以下のコマンドを実行する。(tidalcyclesのeval)
d1 $ sound "bd sn"
さくらVPS上にDocker環境を構築する。
サーバーの初期設定等は割愛。
操作はすべてsudo権限を持った一般ユーザーで行うものとする。
Dockerには無償のCE版と商用版のEE版があるが、今回はCE版を使用する。
sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
sudo yum-config-manager \
--add-repo \ https://download.docker.com/linux/centos/docker-ce.repo
sudo yum-config-manager --enable docker-ce-edge
sudo yum-config-manager --enable docker-ce-test
今回はstableだけ使いたいので--disable
で無効化しておく。
sudo yum-config-manager --disable docker-ce-edge
sudo yum-config-manager --disable docker-ce-test
sudo yum install docker-ce
以下のコマンドでインストール可能なバージョンを確認できる。
yum list docker-ce --showduplicates | sort -r
指定したバージョンをインストールするには次のようにバージョンを指定してインストールする。
sudo yum install docker-ce-<VERSION STRING>
sudo systemctl start docker
起動しているか確認。
sudo docker run hello-world
sudo yum remove docker-ce
dockerのイメージやボリューム、コンテナ等や設定ファイルなどは自動で削除されないので、以下のディレクトリを手動で削除する。
sudo rm -rf /var/lib/docker
以前、LaravelにSPAを組み込む時に考えたディレクトリ構成とnginxのconfファイルというタイトルの記事を書いたが、そこで記載したnginxのconfが不十分だったため、改めて問題点を整理、解決した。
リロードしても常にindex.htmlを返すように設定する必要がある。
こんな感じでtry_filesを使ってconfを設定する。
location / {
try_files $uri $uri/ /index.html;
}
index.htmlでjsファイルのパスを
<script type="text/javascript" src="./dist/bundle.js"></script>
と指定していため、/dashboard/post
などにアクセスすると
/dashboard/post/dist/bundle.js
とリソースを返すようになってしまっていた。
URIに関係なく常にbundle.jsを参照できるように絶対パスを指定するようにした。
<script type="text/javascript" src="/dist/bundle.js"></script>
割と解決に時間がかかったが、nginx側なのか、アプリーケーション側なのか問題を切り分けて考えてみるとすぐに理解できた。
DIとService Locatorの違いについてまとめる
DIパターン(コンストラクタインジェクション)を実装してみる。
なお、DIパターンには、コンストラクタインジェクション、セッターインジェクション、メソッドインジェクションなどコンストラクタ以外からDIする方法もある。
比較のためにDIではないパターンとDIのパターンの両方を実装する。
<?php
class SlackNotification
{
public function notify(string $message)
{
echo $message;
return $this;
}
}
class Application
{
protected $message;
public function __construct()
{
$this->notification = new SlackNotification();
}
public function alert(string $message)
{
$this->notification->notify($message);
}
}
// client
$application = new Application();
$application->alert('slack alert');
<?php
interface NotificationInterface
{
public function notify(string $message);
}
class SlackNotification implements NotificationInterface
{
public function notify(string $message)
{
echo $message;
return $this;
}
}
class Application
{
protected $message;
public function __construct(NotificationInterface $notification) // DI
{
$this->notification = $notification;
}
public function alert(string $message)
{
$this->notification->notify($message);
}
}
// client
$slackNotification = new SlackNotification();
$application = new Application($slackNotification); // DI
$application->alert('slack alert!');
Application
はSlackNotification
の責務を持たず、NotificationInterface
のみに依存している。Application
はコンスタラクタでSlackNotification
を受け入れる(依存する)形になっている。
依存注入部分はモック化できるのでテストしやすくなっている。
// For test
$mockNotification = new MockNotification(); // NotificationInterfaceを実装したMockNotification
$application = new Application($mockNotification);
$application->alert('mock alert!');
こんな感じのやつ
<?php
class Application
{
public function __construct($container)
{
$this->slackNotification = $container['slack.notification'];
}
}
$application = new Applicaion($container);
シンボリックリンクとハードリンクの違いについてまとめる
ls -i1 /
またはstat /
でinode番号を確認できるtouch a.md
ln -s a.md a_symbolic_link.md // シンボリックリンクを作成
ls -i1 a.md a_symbolic_link.md // inodeが違うことが確認できる
touch a.md
ln a.md a_hardlink.md // ハードリンクを設定
ls -i1 a.md a_hardlink.md // inodeが同じことが確認できる
開発の効率化を図り、vimを取り入れ、開発環境諸々を刷新したのでまとめておく。
各ツールの細かい設定や導入しているプラグイン詳細などは省く。
Atom
Vim
vimの思考を受け入れ、vimのキーバインドをあらゆるツール上で適用したことで幸せになれた気がする。
]]>端末多重化ソフトウェアであるtmuxのコトハジメ
tmux起動
tmux or tmux new-session
セッション中での新規セッション作成
prefix+:new
セッション一覧
tmux ls
セッションのデタッチ(tmuxから抜ける)
prefix+d
セッションのアタッチ
tmux attach(a)
任意のセッションにアタッチ
tmux attach(a) -t 0(name)
セッションの削除
tmux kill-session
任意のセッションを削除
tmux kill-session -t 0
全てのセッションを’削除
tmux kill-server
セッションのリネーム
prefix+$
新規ウィンドウ
prefix+c
次のウィンドウに切り替え
prefix+n
前のウィンドウに切り替え
prefix+p
任意のウィンドウに切り替え
prefix+0
ウィンドウ一覧
prefix+w
ウィンドウの削除
prefix+&
ペインの削除
prefix+x
ペイン入れ替え(前方向)
prefix+{
ペイン入れ替え(後方向)
prefix+}
コピーモード
prefix+[
コピー範囲選択(コピーモード中)
vまたはspace
コピー(コピーモード中)
yまたはenter
macのterminalでtmuxを使っているとき、マウスを使ってテキストを選択してもコピー(cmd+c)ができない。(tmuxのショートカットでvimライクなコピーはできる)
マウスで選択した範囲をコピーしたい場合は、cmd+rでterminalのAllow mouse reportingの設定をトグルする必要がある。
N+1問題の説明と対応についてまとめる。
SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "posts"."id" = 1"
SELECT "users".* FROM "users"
SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (1, 2, 3, 4, 5)
OS周りの勉強をしている時に調べたこと。
メモリが足りない時にメモリの中身をハードディスクに移す機能のこと
ダック・タイピングについてまとめる
ポリモーフィズムについてまとめる
はじめて学ぶソフトウェアのテスト技法の第1章を要約します。
参考ページをサブタイトル横にメモします。
「ソフトウェアの品質の測定・改善を目的とし、テストウェアの開発・利用・保守と平行しながら進めるライフサイクルプロセスのこと」
Bories Beizerはテストを成熟度別に5段階のレベルに分けている。
レベル | 成熟度 |
---|---|
0 | デバッグとテストに違いはない |
1 | ソフトウェアが動作することを示す |
2 | ソフトウェアが動かないことを示す |
3 | プログラムが動作しないことによる危険性を許容範囲に抑える |
4 | 精神的な規律 |
レベル4はテストの容易なソフトウェアを開発することに重点を置こうとするものです。
宗教的なミーニングはありません。たぶん。
「最小限の時間と労力でエラー検出率が最大化されるようにテストケースを設計する」というのがテストの目的である。
テストケースは入力、出力、実行の順番の3つで構成されている。
実行の順番には、順番通りのケースと、互いに独立したケースがある。
ブラックボックステスト・・・テスト対象のソフトウェアの構造や実装の知識を必要とせず、要件や仕様に基づいてテストをする
ホワイトボックステスト・・・テスト対象のソフトウェアの構造や実装の知識に基づいて、テストをする
グレーボックステスト・・・テスト対象のソフトウェアの実装を大まかに把握し、ブラックボックステストケースを効果的に選択し、テストをする
書籍とは別に経験ゼロでもできるプログラミング現場の単体テスト の記事も参考にさせて頂きました。
単体テスト・・・プログラム単体のテスト。言語によって単体の定義は異なる。(EX. C言語での単体は関数)
統合テスト・・・単体を統合して行うテスト。単体間のデータの受け渡しを確認。
システムテスト・・・高レベルの統合時をテスト。機能性、ユーザビリティ、セキュリティ、国際性など様々。
受け入れテスト・・・顧客がソフトウェアを受け入れて、代金を支払うためのテスト。
全てのパターンをテストすることはできない。ゆえに最適化されたテスト設計が求められる。
これからテストを書き始める上のの基本のキくらいは学べた気がします。。。
]]>Vimmerになるために覚えていったコマンドを書き連ねていく。
h 論理行で左
j 論理行で下
k 論理行で上
l 論理行で右
^ 先頭に移動
0 インデントを無視して先頭に移動
$ 末尾に移動
+ 下の行の先頭に移動
- 上の行の先頭に移動
:3 3行目に移動
w 次の単語の先頭に移動(空白を含む)
b 前の単語の先頭に移動(空白を含む)
e 次の単語の末尾に移動(空白を含まない)
ge 前の単語の末尾に移動(空白を含まない)
% (, [, { などに対応する閉じタグに飛ぶ
ctrl+f 画面1つ分先へ
ctrl+b 画面1つ分戻る
ctrl+d 画面半分先へ
ctrl+u 画面半分戻る
H 画面最上部へカーソルを飛ばす
M 画面中部にカーソルを飛ばす
L 画面最下部へカーソルを飛ばす
{ 上の空行へ
} 下の空行へ
ctrl+y カーソルを固定して上へスクロール
ctrl+e カーソルを固定して下へスクロール
zEnter or zt カーソルがある行を画面最上部にする
zz カーソルがある行を画面中央にする
z- or zb カーソルがある行を画面最下部にする
fWord Word(任意の文字)へジャンプ
tWord Word(任意の文字)の1つ手前へジャンプ
FWord Word(任意の文字)へジャンプ(逆方向)
tWord Word(任意の文字)の1つ手前へジャンプ(逆方向)
;/, 任意の文字の検索結果に進む/戻る
. 繰り返し
; 繰り返し(逆方向)
v visual mode
ctrl+v visual block mode
V visual line mode
i insert mode
I 先頭からinsert mode
a カーソルの1つ後ろからinsert mode
A 行末からinsert mode
l 行頭からinsert mode
o 1つ次の行からinsert mode
O 1つ前の行からinsert mode
s カーソル上の一文字を消してからinsert mode
S カーソル上の行を削除してからinsert mode
r 一文字編集(Enter後normal mode)
R 複数文字編集(ESC後normal mode)
dd カーソル行の削除
3+dd 3行分削除
d$ 末尾まで削除
d^ 先頭まで削除
dw 次の単語の先頭まで削除(空白を含む)
db 前の単語の先頭まで削除(空白を含む)
de 次の単語の末尾まで削除(空白を含まない)
dge 前の単語の末尾まで削除(空白を含まない)
diw カーソル上の単語を削除からinsert mode
daw カーソル上の単語と後続の空白も削除してからinsert mode
u やり直し
ctrl+r 再実行
p カットまたはコピーした行を貼り付ける
yy カーソル行をヤンク
yi( 記号で囲まれた中身をヤンク
cw 単語の変更、insert mode
c$ 末尾まで変更、insert mode
c^ 先頭まで変更、insert mode
c0 インデントを含まない先頭まで変更、insert mode
ci( 記号で囲まれた中身を変更
cit タグの中身を削除してinsert mode
>> インデントを1つ下げる
<< インデントを1つ上げる
ctrl+p vimで開かれている全てのファイルに出現した単語補完/前の補完候補を選択
ctrl+n 次の補完候補を選択
ctrl+y 選択中の補完候補に確定
ctrl+x ctrl+l 行補完(開かれているファイルの行とマッチするものを補完)
ctrl+x ctrl+f ファイルパス補完
:s/thee/the カーソルのある行で最初に見つかったワードを置換
:s/thee/the/g カーソルのある行全体で見つかったワードを全て置換
:1,100s/thee/the 1から100行目のそれぞれの行で最初に見つかったワードを置換
:1,100s/thee/the/g 1から100行目内で見つかったワードを全て置換
:%s/thee/the/g ファイル全体から見つかったワードを全て置換
:%s/thee/the/gc ファイル全体から見つかったワードを確認しながら置換
/ 語句検索 nで前方に移動、Nで後方に移動
ctrl+o 一度だけノーマルモードのコマンドを実行した後挿入モードに戻る
テーブルに格納されているレコードを高速に取り出すための仕組み
以下のようなO(n)問題を抱えたクエリがあるとする。
SELECT * FROM users WHERE first_name = ‘Tom’
このクエリのパフォーマンスを上げるためには、以下のようにIndexを貼る。
ALTER TABLE users ADD INDEX (first_name)
データの作成・更新時には、同時にインデックスの追加・更新も行われるため、上記のようなデメリットが生じる。
ALTER TABLE users ADD INDEX (first_name)
容量の増加を抑えつつ、性能を向上させたい時に有効なパターン
最初の4バイトだけにインデックスを貼る例
ALTER TABLE users ADD INDEX (first_name(4))
ALTER TABLE users ADD INDEX (last_name, first_name)
MySQLでは、1つのクエリを実行する際、1テーブルについて1インデックスしか使用できないが、マルチインデックスを適用していれば、有効なインデックスがクエリ実行の際に選択される。
マルチカラムインデックスの先頭に指定するカラムはカーディナリティの高いものにしておくのが通常良い。
NULLを除いて値が重複して出現しなくなる。 レコードの作成・更新において、全ての値を調べて同じ値が既に存在しないことを確認する。 MySQLでは、ユニークキーを指定するとユニークインデックスも指定される。
ALTER TABLE users ADD UNIQUE (first_name)
EXPLAIN句でクエリの実行計画を確認。
EXPLAIN SELECT * FROM users WHERE first_name = ‘Tom’
以下の項目を確認
インデックスを検討したほうが良いかもしれない判断基準をリストアップ。 あくまで推測するための基準なので、EXPLAINでの計測をしたほうが良い。
以下に該当するインデックスがクラスタインデックスとなる。
クラスタインデックス以外のインデックスをセカンダリインデックスという。 セカンダリインデックスには主キーの値が含まれている。 EXPLAINで計測することが前提がだが、主キーの値が含まれているので、カバンリングインデックスを狙う際は複合インデックスに主キーを含めなくてもセカンダリインデックスだけでカバリングインデックスになる可能性を覚えておくと良いかも。 cf. 知って得するInnoDBセカンダリインデックス活用術!
クエリの実行結果に必要なすべてのカラムを含むインデックスのこと。
データファイルを読まず、インデックスだけでカバーできるので、検索が高速化される。
アルゴリズムの演算性能をざっくりと計算するO記法と計算量の求め方についての前提知識をまとめる。
それぞれ計算時間を記述するものだが、学術的な意味の違いについてまとめる。
アルゴリズムの実行時間を表す3パターンについてまとめる。
多くのアルゴリズムでは、最悪ケースと期待ケースは同じになるようなことが多い。
処理時間が短い順に代表的なものをまとめる。
O記法 | 計算理論における名称 | 概要 |
---|---|---|
O(₁) | 定数時間 | データ量が増加しても処理時間が増加しない |
O(log n) | 対数時間 | データ量が増えても計算時間がほとんど増えない。増えても計算量の増え幅は小さくなる。 |
O(n) | 線形時間 | データ量が増加した分だけ処理時間が増える |
O(n log n) | 準線形、線形対数時間 | O(n)よりやや重い程度 |
O(n²) | 二乗時間 | 要素から全ての組み合わせペアについて調べるような処理。データ量が増えるほど計算量の増え幅が大きくなる |
O(n³) | 多項式時間 | 三重ループ |
O(kⁿ) | 指数時間 | 要素から全ての組み合わせを取得するような処理 |
O(n!) | 階乗時間 | nの階乗に比例した時間がかかる |
ステップ数を計算して、その合計を元に計算量を求める。
計算量を求める際に、以下の二点は重要度が低いため、省略する。
ステップの計算で処理の実行時間を足すか掛けるかは、それぞれの処理が同時に起きるか、起きないかで考える。
同時に起きない場合は実行時間を足すケース。
for (condition) {
// do something
}
for (condition) {
// do something
}
同時に起きる場合は、実行時間を掛けるケース。
for (condition) {
for (condition) {
// do something
}
}
const targetData = 4; // 1回実行
const data = [1, 2, 3, 4, 5]; // 1回実行
for (let i = 0; i < data.length; i++) {
if (targetData == data[i]) {
console.log(`${i}番目でデータを発見した`); // data.length回実行される→n回実行
return;
}
}
console.log('目的のデータはない'); // 1回実行
上記のコードの場合、ステップ数の合計は1+1+n+1=3nとなる。
係数は除くので、計算量はO(n)となる。
const data = [1, 2, 3, 4, 5]; // 1回実行
for (let i = 0; i < data.length; i++) {
console.log(`${i}回目の処理`); // 1回実行される
for (let j = 0; j < data.length; j++) {
console.log(j); // 4 * 4 回実行される→n²回実行
}
}
上記の場合は、1+1+n²=2n²のステップ数となるので、計算量は係数を除き、O(n²)となる。
const n = 10; // 1回実行される
for (let i = 0; i < n; i = i * 2) {
console.log(i++); // log2ⁿ回実行される
}
nが1の時
ループ回数 1
nが4の時
ループ回数 2
nが8の時
ループ回数 3
ループ回数はlog2ⁿで求められる。
1+log2ⁿのステップ数となるので、諸々を省略して計算量はlog nととなる。
golangの勉強をする時に参考にした本とリンクのまとめ。
TLSというプロトコルが提供する機能
HTTPS化の推奨
Homesteadを触る機会があったのでざっくりまとめる。
以下のツールは用意しておきましょう。
laravelのインストールとcomposer install
を実行しましょう。
composer create-project "laravel/laravel=5.5.*" projectname
cd app
composer install
Vagrant boxを用意します。
vagrant box add laravel/homestead
homesteadを利用するためのリポジトリをクローンし、初期化スクリプトを実行します。
cd ~
git clone https://github.com/laravel/homestead.git Homestead
bash init.sh
YAMLファイルで仮想環境の設定を調整します。
vi Homestead.yaml
ip: "192.167.10.99" // edit
memory: 2048
cpus: 1
provider: virtualbox
authorize: ~/.ssh/id_rsa.pub
keys:
- ~/.ssh/id_rsa
folders:
- map: ~/localdev/project/laravel // edit
to: /home/vagrant/code
sites:
- map: laravel // edit
to: /home/vagrant/code/public
databases:
- homestead
# blackfire:
# - id: foo
# token: bar
# client-id: foo
# client-token: bar
# ports:
# - send: 50000
# to: 5000
# - send: 7777
# to: 777
# protocol: udp
次に、ホストファイルを編集します。
vi /etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
192.167.10.99 laravel // edit
192.167.10.99 homestead # VAGRANT: e78b975204ccde53ef11f1ffe284d4f4 (homestead-7) / b212bb82-8dc8-405b-8507-fa284ddb9aa8
cd ~/Homestead
vagrant up
以下にアクセスするとLaravelのデフォルトのウェルカムページが見れるはずです。
http://laravel
テストケースの基本的な種類と洗い出し方についてのざっくりまとめ。
ユニットテスト
インテグレーションテスト
ユニットテスト、インテグレーションテストのテストケースの洗い出しは、上記の観点の考慮に加えて、ビジネス的な要因(品質や工数)を考慮した形で行う。
Goの環境を構築します。
インストール手段は省略します。私はanyenvというツールでインストールしています。
.bashrc
または.bash_profile
にGOPATHを指定します。
export GOPATH=$HOME/localdev/project/go_dev // 好きなように設定してください
PATH=$PATH:$GOPATH/bin
ローカル環境でのディレクトリ構成は公式ドキュメントに従う形で構成していきます。
go_dev/
├── bin
├── pkg
└── src
go_devというGoの開発用ディレクトリを用意し、その中に公式ドキュメントのディレクトリ構成に準拠した役割ごとの3つのディレクトリを用意する形になっています。
binには実行可能なコマンドが、pkgにはパッケージが、srcにはソースがそれぞれ配置されます。
git管理するのはsrc/です。
試しにここまでの設定が上手できているかの確認でパッケージを作ってみます。
src/
にtest/
を用意し、main.go
というファイルを以下の通り作成します。
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
go build main.go
でコンパイルし、バイナリファイルを作成、go install
でbin/
にtest
というバイナリファイルが生成されていればOKです。
Linuxのコンテナ技術を使用
コンテナは、ホストマシンのカーネルを利用し、プロセスやユーザーなどを隔離する
ミドルウェアや各種環境設定をコード化して管理できる(=Infrastructure as Code)
ローカル・本番環境問わず
Dockerの正体
Docker for Mac
システムのその他の部分から分離された一連のプロセス
OSとカーネルを共有し、アプリケーションプロセスをシステムの他の部分から独立させる(単一のOSで実行される)
構成
[ホストOS]
[ハードディスク]
構成
[ゲストOS]
[仮想化ソフト]
[ホストOS]
[ハードディスク]
メリット
デメリット
構成
[ゲストOS]
[ハイパーバイザ]
[ハードディスク]
メリット
デメリット
[コンテナ管理ソフトウェア]
[ホストOS]
[ハードディスク]
メリット
デメリット
Docker repository(Ex. Docker Hub)
↓ (pull)
Dockerfile → Docker Image → Docker Container
(build) (run)
↓ (commit)
Docker Image → Docker Container
(run)
※ホストOS型とハイパーバイザ型の違い
Think IT - ホスト型とハイパーバイザー型の違いは何?VMware vSphere Hypervisor の概要が参考になる。
docker build
docker run
docker commit
カーネルとは
OSの基本機能の役割を担うソフトウェア
カーネル+ソフトウェア = ディストリビューション
ProxyはECMAScript 2015から追加されたオブジェクトで、オブジェクトが持つ機能をラップすることにより、オブジェクトの機能をカスタマイズすることができます。
Proxyを知る上で必要な用語です。
handler
・・・トラップを入れるためのオブジェクトで、プレースホルダ的な扱いをされます。
trap
・・・Proxyがプロパティへのアクセスを実装するためのメソッド。
target
・・・プロキシするオブジェクト。
invariant
・・・オブジェクトの機能をカスタマイズした時に変更しない不変的な条件のこと。
基本的な構文は以下の通りです。
let proxy = new Proxy(target, handler);
target
にはラップしたいオブジェクトまたは関数を定義します。
handler
には関数をプロパティとして持つオブジェクトを定義します。オブジェクトに定義された関数がProxyの操作が行われた時の挙動となります。handler
の中でラップ前に本来の挙動を呼び出したい時はReflect
というオブジェクトを呼び出します。
オブジェクトに渡された値にバリデーションをかけるような簡単な例です。
const handler = {
get: function(target, prop) {
if (target[prop] === 'foo') {
return target[prop];
}
return 'Default Value';
}
};
const proxy = new Proxy({}, handler);
proxy.foo = 'foo';
proxy.bar = 'bar';
console.log(proxy.foo); // foo
console.log(proxy.bar); // Default Value
getトラップが実装されたhandlerオブジェクトを定義し、Proxyオブジェクトである{}(空のオブジェクト)
がオブジェクトのプロパティを取得した際に、取得された値によって条件分岐するという処理になっています。
JavaScriptの仕様で用意されていないオブジェクトを独自に実装したい時や本来のオブジェクトの挙動を変更したいときなどに便利そうですね。
#参考
]]>async function
はAsync Functionオブジェクトを返す関数です。
async
とawait
というキーワードを使って、Promiseよりも簡潔に非同期処理を書くことができます。
ES2017で仕様が定義されています。
使い方はカンタンです。
async
関数を関数定義の時に頭につけるだけです。
Promise以外の値を返すように定義した場合は、その値で解決された形でPromiseが返されます。
async function asyncFunc() {
return 'すごい!';
}
asyncFunc().then((result) => {
console.log(result);
});
async function asyncFuncB(text) {
return 'すごい!' + text;
}
asyncFuncB('さすが!').then((result) => {
console.log(result);
});
もちろん、Promiseを返すこともできます。
async function asyncFuncC() {
return new Promise((resolve, reject) => {
resolve('すばらしい!');
});
}
asyncFuncC().then((result) => {
console.log(result);
});
ちなみに、上は下のように書き換え可能です。
async function asyncFuncC() {
return Promise.resolve('すばらしい!')
}
asyncFuncC().then((result) => {
console.log(result);
});
また、async
関数内では、await
キーワードを使うことができます。
await
キーワードはPromiseの結果が返されるまで処理を止めることができる演算子です。
await
キーワードを使うことでPromise.then()~
の部分を省略して記述することができます。
async function awaitFunc() {
return 'ワンダフル!';
}
async function asyncFuncD() {
let result = await awaitFunc();
console.log(result);
}
asyncFuncD();
Promiseを一々書かなくともasync
キーワードを使うと簡潔にPromiseを返す関数が作れる上に、非同期処理がより実装しやすくなりましたね。
まずはHistory APIを理解しておきます。GO TO MDN。
忙しい人はpushState
とwindow.popstate
だけ理解しておけばなんとかなるはず。
このrouterでは、以下のようなURLに対応します。
/post
/post/:id
/post/:id/:title
クエリパラメータには対応しません。
React周りは省略します。
React以外で使うパッケージは1つだけです。
URL部分の正規表現を良しなにやってくれるパッケージです。
そのうち自分で正規表現書きたいですが、今回はパッケージに頼っちゃいます。
ナビゲーション、ナビゲーションにそれぞれ対応するコンポーネントを用意しておきます。
src/
├── App.js
├── Dashboard.js
├── Home.js
├── Post.js
└── Profile.js
ルーティングを実装していきます。
コンポーネントは、Router
とRoute
という2つを用意します。
Router
はURLに応じて描画切り替えを行うコンポーネントです。
Route
はaタグをラップしただけのコンポーネントです。
それからルーティング規約を記述するファイルとして、routes.js
を用意します。
routes.js
はパスと、パスに対応するコンポーネントの対応をオブジェクトの配列で記述したものです。
ここまででおおよそ察しがつくかと思いますが、ルーティングの一連の処理としては、
初期状態(ファーストビュー)
①現在のURL情報を取得
②現在のURL情報に一致するコンポーネントを描画
URL情報をStateとして持ちます。
遷移
①クリックされたリンクのパスを取得
②History APIのpushState
で履歴を追加・遷移
③コンポーネントを再描画
Stateが更新され、コンポーネントが再描画されます。
各コンポーネントの実装はこんな感じです。
Route.js
import React, {Component} from 'react';
const history = window.history;
class Route extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
event.preventDefault();
const info = {
'url': event.target.href,
'path': event.target.pathname
};
this.handlePush(info.url);
this.props.handleRoute(info);
}
handlePush(url) {
// Create a history, and transition to next url
history.pushState(null, null, url);
}
render() {
return (<React.Fragment>
<a href={this.props.path} onClick={this.handleClick}>{this.props.text}</a>
</React.Fragment>);
}
}
export default Route;
Router.js
import React, {Component} from 'react';
import toRegex from 'path-to-regexp';
class Router extends Component {
handleComponent() {
const routes = this.props.routes;
const info = this.props.info;
for (const route of routes) {
const keys = [];
const string = new String(route.path);
const pattern = toRegex(string, keys);
const match = pattern.exec(info.path);
if (!match) {
continue;
}
const params = Object.create(null);
for (let i = 1; i < match.length; i++) {
params[keys[i - 1].name] = match[i] !== undefined
? match[i]
: undefined;
}
if (match) {
return route.action(Object.assign(info, {"params": params}));
}
}
return 'Not Found';
}
render() {
return (this.handleComponent());
}
}
export default Router;
routes.js
import React, {Component} from "react";
import Home from "./Home";
import Dashboard from "./Dashboard";
import Profile from "./Profile";
import Post from "./Post";
const HomeComponent = (params) => (<Home {...params}/>);
const DashboardComponent = (params) => (<Dashboard {...params}/>);
const ProfileComponent = (params) => (<Profile {...params}/>);
const PostComponent = (params) => (<Post {...params}/>);
export const routes = [
{
path: "/",
action: HomeComponent
}, {
path: "/dashboard",
action: DashboardComponent
}, {
path: "/profile",
action: ProfileComponent
}, {
path: "/post/:id",
action: PostComponent
}
];
App.js
import React, {Component} from 'react';
import Router from './Router';
import Route from './Route';
import {routes} from './routes';
class App extends Component {
constructor(props) {
super(props);
this.state = {
'url': '', // current url
'path': '' // current path
};
this.handleRoute = this.handleRoute.bind(this);
}
handleRoute(info) {
// Update url info
this.setState(info);
}
render() {
return (<React.Fragment>
<p>Current URL: {this.state.url}</p>
<p>Current Path: {this.state.path}</p>
{/* Navigation */}
<ul>
<li>
<Route path="/" text="Top" handleRoute={this.handleRoute}/>
</li>
<li>
<Route path="/dashboard" text="Dashboard" handleRoute={this.handleRoute}/>
</li>
<li>
<Route path="/profile" text="Profile" handleRoute={this.handleRoute}/>
</li>
<li>
<Route path="/post/9" text="Post-Id" handleRoute={this.handleRoute}/>
</li>
</ul>
{/* Router Component */}
<Router routes={routes} info={this.state}/>
</React.Fragment>);
}
}
export default App;
※jsxの改行がなんか変なのは多分eslintをちゃんと設定していないからだと思います...
You might not need React Router
を結構参考にしました。
実装する上で厄介だった部分は、「パラメータ(:id)の情報をどうやって取得するか、保持するか」という点でしたが、path-to-regexp
というawesomeなライブラリのおかげで、その点は克服できました。
今回のソース置いておきます。
npmにも公開しています。
EventEmitterやObserverをつかったらもっと綺麗になる気が・・(勉強不足)
Promiseとは・・
The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value. MDN - Promise
だそうです。
ざっくりまとめると、Promiseとは、非同期処理やその結果をいい感じにしてくれるオブジェクトのことです
Promiseを使うと主に以下のようなメリットが得られます。
Promiseについて例を上げて確認してみます。
Promiseを使わないコールバックを使った非同期処理の例です。
// Promiseを使わない高階関数の例
const asyncSayHi = (greet, callback) => {
setTimeout(function () {
callback(greet);
}, 1000);
};
asyncSayHi('Hello', (value) => {
console.log(value);
});
// 出力:Hello
asyncSayHi
を連続して呼び出したい時は、こんな感じのいわゆるコールバック地獄になってしまいます。
// callback地獄
asyncSayHi('Hello', (value) => {
console.log(value);
asyncSayHi('こんにちは', (value) => {
console.log(value);
asyncSayHi('你好', (value) => {
console.log(value);
// callback loop is forever...
});
});
});
// 出力:Hello こんにちは 你好
先程のコールバックを使った非同期処理の例をPromiseを使って書き直すとこんな感じになります。
// Promiseの実装
const asyncPromiseSayHi = function (greet) {
return new Promise((resolve, reject) => {
if (greet) {
resolve(greet);
} else {
reject('挨拶してください');
}
})
};
// 非同期処理の実行
asyncPromiseSayHi('Hello').then((value) => {
console.log(value);
}).catch((error) => {
console.log(error);
});
// Hello
// 連続して非同期処理を実行
asyncPromiseSayHi('Hello').then((value) => {
console.log(value);
return new asyncPromiseSayHi(value);
}).then((value) => {
console.log(value);
return new asyncPromiseSayHi(value);
}).then((value) => {
console.log(value);
return new asyncPromiseSayHi(value);
}).catch((error) => {
console.log(error);
});
// Hello Hello Hello
並列で処理を複数実行したい場合は、Promise.all
というメソッドが用意されています。
const asyncPromiseSayHi = function (greet) {
return new Promise((resolve, reject) => {
if (greet) {
resolve(greet);
} else {
reject('挨拶してください');
}
})
};
const asyncPromiseSalute = function (salute) {
return new Promise((resolve, reject) => {
if (salute) {
resolve(salute);
} else {
reject('敬礼してください');
}
});
};
// 連続して非同期処理を実行
Promise.all(['asyncPromiseSayHi', 'asyncPromiseSalute']).then((value) => {
asyncPromiseSayHi('Hello').then((value) => {
console.log(value);
});
asyncPromiseSalute('Attention').then((value) => {
console.log(value);
});
});
// Hello Attention
コールバックのことを理解していればPromiseはさほど難しく感じないかと思います。
ここで紹介していないPromiseのメソッドはMDNで確認してみてください。
ビルド環境のセットアップが面倒なので、今回はFacebook公式のcreate-react-appというツールを使います。
npm install -g create-react-app
md-editorというアプリ名で環境を用意することにします。
create-react-app md-editor
次に、今回使うライブラリのインストールをしておきます。
cd ./md-editor
npm install --save marked
npm install
最後にサーバーを起動したら準備OKです。npm start
実装に入る前に今回使用しない不要なファイルを削除しておきましょう。
App.css
App.test.js
logo.svg
src/index.js
と`src/App.js
で上記ファイルをインポートしている部分を削除しておきます。
それからsrc/App.js
のほうはreturn文の中身を空にしておきましょう。(ビルド時にreturn文が空で怒られますが一旦無視します。)
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<App/>, document.getElementById('root'));
registerServiceWorker();
src/App.js
import React, {Component} from 'react';
class App extends Component {
render() {
return ();
}
}
export default App;
src
以下にMarkdown.js
というファイルを作成します。
このファイルにはマークダウンのコンポーネントを実装していきます。
src/Markdown.js
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import marked from 'marked';
class Markdown extends Component {
constructor(props) {
super(props);
this.state = {
html: ''
};
this.updateMarkdown = this.updateMarkdown.bind(this);
}
updateMarkdown(event) {
this.setState({
html: marked(event.target.value)
});
}
render() {
const html = this.state.html;
return (<div>
<h1>Markdown Input</h1>
<textarea onChange={this.updateMarkdown}></textarea>
<h1>Markdown Output</h1>
<div dangerouslySetInnerHTML={{
__html: html
}}></div>
</div>);
}
}
export default Markdown;
ほんの数行です。
これだけとりあえずマークダウンとして機能します。
ほぼ生のJSですね。
React特有なのはJSXくらいでしょうか。
最後にMardown.js
をApp.js
内でインポートしましょう。
import React, {Component} from 'react';
import Markdown from './Markdown';
class App extends Component {
render() {
return (<Markdown/>);
}
}
export default App;
ソースコードをハイライトしたい時にはisagalaev/highlight.js - githubを使ってmarkedをカスタマイズするといい感じになります。
ソースコードはbmf-san/til/javascript/md-editor/ - githubに置いてあります。
Reactは素のJSに近い形でコーディングできるので、フレームワークに知識がロックインされづらいので好きです。
コードの説明はほとんど省きましたが、モダンなJSの話 by @bmf_sanの記事を見て頂れば大体わかるのではないかと思います。
]]>分割代入とは、配列またはオブジェクトのデータをそれぞれ別個の変数に代入する式のことです。
文章ではイメージがつきにくいかと思います。
それぞれの例を見て確認してみましょう。
let a, b, c;
[a, b, c] = [1, 2, 3]
console.log(a, b, c) // 1 2 3
let color = [1, 2, 3]
const [red, green, yellow] = color
console.log(red, green, yellow) // 1 2 3
直感的に理解できるかと思います。
分割代入時に配列から取り出した要素がundefinedだった場合の既定値を設定することもできます。
const [red=4, green=5, yellow=6] = [1, 2] // yellowがundefinedの場合
console.log(red, green, yellow) // 1, 2, 6
引数のデフォルト値を指定するような感じですね。
({a, b} = {a:'foo', b:'bar'}) // 分割代入により、aという変数にaにfooが、bという変数にbarが格納される
console.log(a, b) // foo bar
代入文の周りの(..)については以下の引用文をご参照ください。
代入文の周りの ( .. ) は宣言のないオブジェクトリテラル分割代入を使用するときに必要な構文です。<br>
{a, b} = {a:1, b:2} は有効なスタンドアロンの構文ではありません。というのも、左辺の {a, b} はブロックでありオブジェクトリテラルではないと考えられるからです。<br>
しかしながら、({a, b} = {a:1, b:2}) 形式は有効です。var {a, b} = {a:1, b:2} と考えられるためです。<br>
分割代入 - JavaScript | MDNより引用
オブジェクトの分割代入は、Reactなどでよくこんな感じの使われ方をしているのを見ます。
let state = {
value: 'foo'
}
const {value} = state // 分割代入により、valueという変数にstate.valueが格納される
console.log(value) // foo
直感的にかくと、こんな感じです。
const {value} = {value: 'foo'}
console.log(value) // foo
オブジェクトの分割代入も規定値を指定することができます。
const {foo=3, bar=4} = {foo: 1} // barがundefinedの場合
console.log(foo, bar) // 1, 4
さらに、別の名前の変数へ値を代入することもできます。
const {value: value2} = {value: 'foo'} // valueという変数から値を取り出してvalue2という変数に値を代入
console.log(value2) // foo
初見で見るとconst {value} = state
がなんのこっちゃという感じですが、分割代入を知っていると理解できますね!
便利でよく使うので覚えておくと幸せになれるかもしれません。
JavaScriptの分割代入について、コード例を中心に説明しました。
直感的に理解しやすい分野かと思いますので、積極的に使っていきたいですね!
export
は、指定のファイルから関数、変数、オブジェクト、クラス(クラスはプロトタイプベース継承の糖衣構文であり、関数の一種。詳しくは モダンなJSの話──クラス)などを受け取り、任意のファイルでそれらを使えるようにするための文です。
exportには主に2種類の使い方があります。
export
したい要素の名前を付けてexport
する方法です。
export { fooFunction };
export { fooFunction, barFunction, ... };
export const foo = 'bar';
export let foo, bar, ...;
export class foo{...};
こんな感じで要素をexport
することができます。
変数のexport
はvar
、let
も使うことができます。
export
したいデフォルトの要素を決めておきたいときにdefaultキーワードを使ってexport
する方法です。
export default fooFunction() {}
export default class {}
var
、let
、const
はexport default
で使うことができないので注意です。
import
は、別ファイルからexportされた関数や変数、オブジェクトを読み込み、それらを使えるようにするための文です。
import { foo } from "Foo";
import { foo, bar } from "FooBar";
import { foo as bar } "Foo"; // エイリアスを指定することができる
import { foo as bar, bar as foo, ... } "FooBar";
import "FooBar"; // 全てインポート
import
された要素のスコープについてですが、原則的には現在のスコープ(ローカルスコープ)になります。
シンプルにdefaultを呼び出す場合はこんな感じです。
import fooDefault from "Bar";
名前付きの要素を一緒にimport
したい場合は、default import
の後に定義します。
import fooDefault, { foo, bar } "FooBar";
クラスをexport
する場合はimport
先またはexport
先でnew
呼び出しをするのを忘れないようにしましょう。
import
先でnew
呼び出しする例はこんな感じです。
export.js
export class foo {
fooFunction() {
return 'foo';
}
}
export default class bar {
barFunction() {
return 'bar';
}
}
import.js
import { foo } from 'export'; // {}がないとdefaultのbarが呼ばれてしまう
import bar from 'export';
const objFoo = new foo;
const objBar = new bar;
console.log(objFoo.fooFunction()); // foo
console.log(objBar.barFunction()); // bar
<br/>
呼び出し元でnew
呼び出ししておく場合はこんな感じです。
export.js
class foo {
fooFunction() {
return 'foo';
}
}
function createFoo() {
return new foo();
}
export default createFoo;
import.js
import createFoo from 'export';
console.log(createFoo.fooFunction()); // foo
Vue.jsやReactといった最近のフレームワークでは当たり前のように使われているので今一度仕様をしっかりと理解しておくと良いでしょう。
ECMAScript6以前ではnew演算子やprototypeプロパティを使ってクラスに近い機能を実現していましたが、ECMAScript6からはclassキーワードでクラスを定義できるようになりました。<br>
classキーワードはこれまでのnew演算子やprototypeプロパティによるクラス定義のシンタックスシュガーです。
classキーワードを使ったクラス定義の方法には、クラス宣言とクラス式の2種類があります。
クラス宣言によるクラス定義の例:
class Human {
constructor (age, name) {
this.age = age;
this.name = name;
}
sayAge() {
return this.age;
}
sayName() {
return this.name;
}
}
const humanInstance = new Human(24, "Bob");
console.log(humanInstance.sayAge()); // 24
console.log(humanInstance.sayName()); // Bob
クラス式によるクラス定義の例:
const Human = class Human {
constructor(age, name) {
this.age = age;
this.name = name;
}
sayAge() {
return this.age;
}
sayName() {
return this.name;
}
}
const humanInstance = new Human(24, "Bob");
console.log(humanInstance.sayAge());
console.log(humanInstance.sayName());
クラス式の場合、クラス名はあってもなくてもOKです。
関数宣言ではホイスティングが行われますが(関数式ではされません)、class宣言やクラス式ではホイスティングされません。<br>
したがって、クラス宣言やクラス式を使う場合はクラスを呼び出す前に、呼び出すクラスを先に宣言する必要があります。
※ホイスティングの概念について→モダンなJSの話──var/let/const
クラス宣言やクラス式で定義されたクラスは全てstrictモードになります。
strictモードの詳細についてはMDN - Strictモードをご参照ください。
初期化を行うメソッドであるconstructorの定義は一度だけしか定義できません。<br>
2回以上定義された場合はSyntax Errorが返されます。
#クラスのメソッド定義
ゲッターとセッターはそれぞれgetキーワードとsetキーワードで定義することができます。。<br>
ゲッターはプロパティアクセスされたときに実行されるメソッドで、セッターはプロパティに値が代入された時に実行されるメソッドです。<br>
getとsetの例:
class Human {
constructor(age, name) {
this.age = age;
this.name = name;
}
get echoProp() {
return `Age: ${this.age} Name: ${this.name}`;
}
set prop(prop) {
this.age = prop.age;
this.name = prop.name;
}
}
const humanInstance = new Human(24, "Bob");
console.log(humanInstance.echoProp); // Age: 24 Name: Bob
humanInstance.prop = {age: 30, name: "John"};
console.log(humanInstance.echoProp); // Age: 30 Name: John
staticキーワードを用いることで静的メソッドをクラス内に定義することができます。<br>
staticメソッドは、クラス名.静的メソッド名
で呼び出すことができます。
静的メソッドの例:
class Human {
constructor(age, name) {
this.age = age;
this.name = name;
}
static sayAge(humanInstance) {
return `I'm ${humanInstance.age} years old.`;
}
static sayName(humanInstance) {
return `I'm ${humanInstance.name}.`;
}
}
const humanInstance = new Human(24, "Bob");
console.log(Human.sayAge(humanInstance)); // I'm 24 years old.
console.log(Human.sayName(humanInstance)); // I'm Bob.
メソッドを静的メソッドにすることはできますが、プロパティをメソッドにすることはできません。
(TypeScriptではプロパティをstaticにすることができるらしいです。)
継承はextendsキーワードで定義することができます。
親クラスのメソッドを呼び出したい場合は、superキーワードを使うことで呼び出し可能です。
継承の例:
class Gorilla {
constructor(iq) {
this.iq = 2; // ちょっと優秀なゴリラ。ゴリラのIQの平均は1.5~1.8。
}
speak() {
return 'ウホ!';
}
}
class Human extends Gorilla {
constructor(age, name, iq) {
super(); // 仕様上super()をthisを使う前にsuper()を呼び出しておく必要がある。
this.age = age;
this.name = name;
this.iq = 100; // 親の値を上書き。ちなみに100は人間の平均的なIQ。
}
sayAge() {
return this.age;
}
sayName() {
return this.name;
}
sayIq() {
return this.iq;
}
speak() {
return `ゴリラは${super.speak()}, 人間はこんにちは!`;
}
}
const humanInstance = new Human(24, "Bob");
console.log(humanInstance.sayAge()); // 24
console.log(humanInstance.sayName()); // Bob
console.log(humanInstance.sayIq()); // 100
console.log(humanInstance.speak()); // ゴリラはウホ!, 人間はこんにちは!
ちなみに、super()
がないとUncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
とエラーがでます。
classキーワードの登場によってJavaScriptでのOOPのあり方が変わったのではないでしょうか。<br>
とはいえアクセス修飾子が実装されていなかったり、他のクラスベースの言語のOOPほど機能が充実していないため、まだ発展途上といった印象があります。
JavaScriptのオブジェクトモデルについて知識(cf. オブジェクトモデルの詳細)があるとより深い理解が得られます。
本題に入る前に、スコープの定義について確認しておきましょう。
スコープとは、変数名や関数名が参照可能な範囲のことです。
スコープの種類は色々ありますが、ここでは主に3つのスコープについて表で説明します。
スコープ名 | 範囲 | 備考 |
---|---|---|
グローバル | 関数の外側 | どこからでもアクセスできる。 |
ローカル(関数) | 関数の内側 | ローカルスコープ内からでしかアクセスできない。 |
ブロック | ブロック({ })の内側 | if, for, switchなど |
ブロックスコープはJavaScriptには元々ありませんでしたが、letとconstの登場によってブロックスコープが使えるようになりました。
再宣言 | 再代入 | スコープ |
---|---|---|
○ | ○ | ローカル |
varは再宣言も再代入もできます。
var a = 1;
var a = 2; // 再宣言可能
function sayNum()
{
a = 100; // 再代入可能
return a;
}
console.log(sayNum()); // 100
以前はvarしか変数宣言がありませんでしたが、後述するletやconstの登場により、varで変数を宣言する必要性はほとんどなくなりました。
一部の特殊なケースやパフォーマンスチューニング、ブラウザ対応等を考慮するような状況を除いて、varを使う機会はほぼ無いでしょう。
#####巻き上げ(hoisting)
varには巻き上げ(hoisting)という概念があり、変数宣言はコードの実行よりも前に処理されるという特徴があります。
これが、
a = 1;
var a;
こういうふうに処理されます。
var a;
var a = 1;
ちなみに、letやconstで同様のコードを試してみると・・・
let a;
let a = 1; // Uncaught ReferenceError: a is not defined
letの場合は、ReferenceError
が投げられますが、巻き上げ自体は実は行われているようです。これについてはconstも同様です。
ECMAScript 2015 では let は変数をブロックの先頭へ引き上げます。しかし、その変数を宣言より前で参照することは ReferenceError を引き起こします。ブロックの始めから変数宣言が実行されるまで、変数は "temporal dead zone" の中にいるのです。
MDN - letより引用
これについてはconstも同様です。
let に対する "temporal dead zone" の懸念事項はすべて、const にも適用されます。
MDN - constより引用
a = 1;
const a; // Uncaught SyntaxError: Missing initializer in const declaration
constの場合は、そもそも初期化(const a = 1;
)をしないとシンタックスエラーが投げられます。
constの巻き上げを確認してみます。
const a = 1;
function sayNum()
{
console.log(a); // Uncaught ReferenceError: a is not defined
const a = 100;
}
sayNum();
巻き上げを意識使うようなシーンは少ないような気がしますが、varとlet、constで巻き上げの挙動が異なることは覚えておいたほうが良さそうです。
再宣言 | 再代入 | スコープ |
---|---|---|
✗(同スコープ内での再宣言) | ○ | ブロック |
letは再宣言は不可ですが、再代入は可能です。
let a = 1;
let a = 2; // Uncaught SyntaxError: Identifier 'a' has already been declared => 同スコープ内での再宣言不可
let a = 1;
function sayNum()
{
a = 100 // 再代入可能
return a;
}
console.log(sayNum()); // 100
letを使うシーンというのは、これまでのvarを使っていた部分になりますが、とりわけ再代入する可能性がある部分に限られます。
再宣言 | 再代入 | スコープ |
---|---|---|
✗(同スコープ内での再宣言) | ✗ | ブロック |
constは再宣言も再代入も不可能です。
const a = 1;
const a = 2; // Uncaught SyntaxError: Identifier 'a' has already been declared => 同スコープ内での再宣言不可能
const a = 1;
function sayNum()
{
a = 100 // Uncaught TypeError: Assignment to constant variable. => 再代入不可能
return a;
}
console.log(sayNum()); // 100
宣言 | 再宣言 | 再代入 | スコープ |
---|---|---|---|
var | ○ | ○ | ローカル |
let | ✗(同スコープ内での再宣言) | ○ | ブロック |
const | ✗(同スコープ内での再宣言) | ✗ | ブロック |
基本的にはconstを使って変数を宣言するようにして、再代入する可能性がある部分に関してはletを使う、それ以外の特殊なケースに関してはvarを検討するという方針で良さそうです。
変数のスコープ汚染はバグの元やコードリーディングの妨げになるので、しっかりと使い分けていきたいですね!
ざっとまとめると、
アロー(=>)を使ってかく関数式で、"thisの値を語彙的に束縛することができる"という点が大きなポイントです。
アロー関数を使うと、今までこう書いていたものが・・・
const foo = function() {
console.log(this);
}
foo();
こんな感じでかけます。
const foo = () => {
console.log(this);
}
foo();
ちなみに、引数を取らない場合は丸括弧()が必要で、引数を1個しか取らない場合は丸括弧は任意です。
// 引数がないときは丸括弧必須
const foo = () => {
console.log(this);
}
foo();
// 引数が一つしかないときは丸括弧は任意
const foo = (value) => {
console.log(value);
}
foo('Hello!');
即時関数にしたい場合はこんな感じでかけます。
(() => {
console.log('Hello!');
})();
これはちょっと混乱しそうですね・・・
使えるところは積極的にアロー関数に置き換えていく方針で良いかと思いますが、thisが何を指すのかだけは意識しておいたほうがいいです。
例えば、下のようなケースの場合はどうでしょうか。
const objA = {
value: 'foo! foo!',
sayHi: function() {
console.log(this.value);
}
}
objA.sayHi();
const objB = {
value: 'bar! bar!',
sayHi: () => {
console.log(this.value);
}
}
objB.sayHi();
1つ目のthisはオブジェクト内のvalueを、2つ目のthisはグローバルオブジェクトを返します。
このようなケースを見ると、function式とアロー関数式を使い分ける必要性があるケースもいくつかあるような気がします。
JavaScriptのthisの詳しい話はMDN - thisをご参考ください。
"thisの値が何を指すのか"を理解しておくとよりアロー関数への理解、JSへの理解が深まります。
Vue.jsやらReactやらフレームワークを使っているとコード量が多くなりがちだったり、thisがあっちらこっちら散らばって何がなんやらという状態になりやすい気がします。
アロー関数を使って関数部分をシンプルに記述することができれば、コードの見通しも良くなると思います。
2017年10月8日、PHPカンファレンス2017にて、LT初登壇してきました。
初めてのLTでPHPカンファレンスという舞台に立てて、自分としてはとても良い経験になりました。
Speaker Deck - 3年目エンジニアOSSをはじめる by bmf_san
「3年目エンジニアOSSをはじめる」というテーマで、自分のOSSプロダクトを紹介しつつ、OSSに取り組んだ背景や学び得たことなどについてお話しました。
Japan PHP Conference Track1 (LT) - LT, クロージング
内容をもっとブラッシュしておくべきだったと反省しています。。。
今回のLTを終えてからRubelのstar数が18から50になりました!
今後ともモチベーションを上げて開発を継続していきたい所存です。
]]>リレーショナルなデータベースは継承をサポートをしていないので、オブジェクトの継承関係をデータベースにどのように表現するのか考慮する必要があります。
それを表現する3つのパターン、単一テーブル継承・クラステーブル継承・具象クラス継承とはについて説明します。
※各パターンの実装におけるメリット・デメリット等には触れません。
今回想定する登場するクラスは4つです。
Party PeopleがRich Peopleを継承するという構造はちょっとわかりづらいかもしれませんが、イメージが伝われば良しとします。
全クラスに共通する属性を持っています。
良識を持った善良なる一般ピーポーです。
お金と土地を持っているリッチな人々です。
moneyはお金です。
landは土地です。
※単位とか細かいことは考慮していません。
パーリーピーポー。
単一テーブル継承は、オブジェクトの継承関係を1つのテーブルで表現します。
テーブルにはサブクラスを判断するためのカラム(type)を持たせます。
RailsでSTIの実装がサポートされているようです。
クラステーブル継承は、オブジェクトの継承関係をクラスごとに1テーブルを用意することで表現します。
スーパークラスのテーブルにはスーパークラスの持つカラムを、サブクラスのテーブルにはサブクラスの持つカラムのみを持たせます。
具象テーブル継承は、オブジェクトの継承関係を具象クラスだけ対応したテーブルを用意することで表現します。
各テーブルにはスーパークラスが持つカラムを共通属性として持たせます。
どのパターンを実装するかはテーブル設計のメリット・デメリットとアプリケーション側のロジックのコストの検討次第でしょうか。
何か語弊がある部分や間違いがある場合はご指摘ください。
AnsibleでさくらVPSの初期セットアップを自動化します。
さくらVPSのコンソール画面からOSインストール>カスタムOSインストール
を選択してCentOS7をインストールしておきます。
インストールが開始されると、CentOS7のインストール用コンソール画面(VNCコンソールのHTML5版かJava Applet版)を開くことができるので、環境に合わせて好きな方を選びます。
CentOS7のインストールでは、言語設定やディスクの初期化など行う必要があります。
rootユーザーのパスワード設定と新規ユーザー作成をする画面がありますが、新規ユーザー作成はAnsibleで行うので、rootユーザーのパスワード設定のみだけでOKです。
次に、公開鍵をansibleホスト側からさくらVPSに送ります。(鍵は事前に作成しておいてください。ここでは割愛します。)
こちらの鍵はAnsibleで新規に作成するユーザー用の鍵です。ssh-copy-id -i ~/.ssh/id_rsa.pub root@123.45.678.910
ssh root@123.45.678.901
でさくらVPSにssh接続できれば準備OKです。
hosts
[sakura]
123.45.678.910 ansible_ssh_user=root ansible_ssh_private_key_file=~/.ssh/id_rsa
タスク内容はこんな感じです。
1点注意点があります。ssh_user_password
はopenssl
で暗号化したものを指定する必要があります。
openssl passwd -salt hoge -1 moge
Playbookは参考サイトを大いに参考にさせて頂きました。m( )m
init.yml
---
- hosts: sakura
become: yes
user: root
vars:
ssh_user: bmf
ssh_user_password: hogehogemogemoge
ssh_port: 50055
tasks:
- name: Add a new user
user:
name="{{ ssh_user }}"
groups=wheel
password="{{ ssh_user_password }}"
generate_ssh_key=yes
ssh_key_bits=2048
- name: Create an authorize_keys file
command: /bin/cp /home/{{ ssh_user }}/.ssh/id_rsa.pub /home/{{ ssh_user}}/.ssh/authorized_keys
- name: Change attributes of an authorized_keys file
file:
path: /home/{{ ssh_user }}/.ssh/authorized_keys
owner: "{{ ssh_user }}"
group: "{{ ssh_user }}"
mode: 0600
- name: Allow wheel group to use sudo
lineinfile:
dest: /etc/sudoers
state: present
insertafter: "^# %wheel\\s+ALL=\\(ALL\\)\\s+NOPASSWD:\\s+ALL"
line: "%wheel ALL=(ALL) NOPASSWD: ALL"
validate: "visudo -cf %s"
backup: yes
- name: Forbid root to access via ssh
lineinfile:
dest: /etc/ssh/sshd_config
state: present
regexp: "^PermitRootLogin without-password"
line: "PermitRootLogin no"
backrefs: yes
validate: "sshd -T -f %s"
backup: yes
notify:
- restart sshd
- name: Permit only specific user to access via ssh
lineinfile:
dest: /etc/ssh/sshd_config
state: present
insertafter: "^PasswordAuthentication no"
regexp: "^AllowUsers"
line: "AllowUsers {{ ssh_user }}"
validate: "sshd -T -f %s"
backup: yes
notify:
- restart sshd
- name: Change ssh port number
lineinfile:
dest: /etc/ssh/sshd_config
state: present
insertafter: "^#Port 22"
regexp: "^Port"
line: "Port {{ ssh_port }}"
validate: "sshd -T -f %s"
backup: yes
notify:
- restart sshd
- name: Change acceptable tcp port for ssh on iptables
firewalld: port={{ ssh_port }}/tcp permanent=true state=enabled immediate=yes
- name: shutdown ssh port
firewalld: service=sshd permanent=true state=disabled immediate=yes
- name: disable selinux
selinux: state=disabled
ハンドラーを定義します。
main.yml
---
- name: restart sshd
service: name=iptables start restarted
ansible-playbook sakura.yml -i hosts -k -c paramiko
タスクの実行が全て完了したら、サーバーを一度再起動して完了です。
MySQLの設定ファイルにログサイズを設定したら直りました。
innodb_log_file_size=5M
無茶はやめよう!
"/etc/udev/rules.d/70-persistent-net.rules" is not a file
==> default: Configuring and enabling network interfaces...
The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!
# Down the interface before munging the config file. This might
# fail if the interface is not actually set up yet so ignore
# errors.
/sbin/ifdown 'eth1'
# Move new config into place
mv -f '/tmp/vagrant-network-entry-eth1-1485326655-0' '/etc/sysconfig/network-scripts/ifcfg-eth1'
# attempt to force network manager to reload configurations
nmcli c reload || true
# Restart network (through NetworkManager if running)
if service NetworkManager status 2>&1 | grep -q running; then
service NetworkManager restart
else
service network restart
fi
Stdout from the command:
Shutting down interface eth0: [ OK ]
Shutting down loopback interface: [ OK ]
Bringing up loopback interface: [ OK ]
Bringing up interface eth0:
Determining IP information for eth0... done.
[ OK ]
Bringing up interface eth1: Determining if ip address 192.168.33.10 is already in use for device eth1...
[ OK ]
Bringing up interface eth2: Device eth2 does not seem to be present, delaying initialization.
[FAILED]
Stderr from the command:
bash: line 10: nmcli: command not found
色々調べたところ、ネットワーク周りの設定ファイルみたいなやつで引っかかっているらしいです。
解決に至る対応策は見当たらなかったので勘で対応しました()
cd /etc/sysconfig/network-scripts
mv ifcfg-eth2 eth2-ifcfg
一旦テキトーな名前に変更しておくvagrant reload
問題なければ先程のファイルを削除アンドvagrant reload
rm -rf eth2-ifcfg
(edited)
1.7.4のときはeth0とeth1だけで、eth2は存在していなかった気がします。
eth2の中身を確認するとeth1とダブっていたので、「これいらんやろ」と消したら直ったというわけです。
勘とはいえ参考サイトをヒントに考えた結果なのですが、この対応で問題ないのかちょっと不安ですw
vagrant-hostupdaterのインストール(sudoまたはroot権限が必要かもです)vagrant plugin install vagrant-hostsupdater
Vagrantfileの編集
ファイルの末尾に以下のような記述を追加。
# vagrant-hostupdater
config.vm.network :private_network, ip: "192.168.33.10"
config.vm.hostname = "localdev"
config.hostsupdater.aliases = ["dev", "hoge"]
hostnameとaliasは自由に設定してください。aliasは後でapacheのconfファイルで利用します。ドメイン名といったところでしょうか。
設定が終わったらvagrantを起動するなり再起動するなりしてください。
ターミナルで/private/etc/hostsを見るか、Hostsをインストールしている方はHostsを見ると自動でHostが設定されているのを確認できると思います。
VPSで手馴れている人もいるかと思います。同じ手順です。
/etc/httpd/confのhttpd.confの#NameVirtualHost *:80
の#を外してコメントアウトしてください。
viまたはvimでhttpd.confを開いて/NameVとか打ってenter押してnを数回たたいたとこにあるやつです。
そのままhttpd.confにバーチャルホストの設定をかいてもいいのですが、/etc/httpd/conf.dに設定をまとめることにします。
aliasesにdevとhogeを設定したのでそれぞれ設定ファイルを作成します。
/etc/httpd/conf.d/dev.conf
<VirtualHost *:80>
ServerName dev
ServerAdmin localhost
DocumentRoot /var/www/html/dev
<Directory "/var/www/html/dev">
AllowOverride All
</Directory>
</VirtualHost>
/etc/httpd/conf.d/hoge.conf
<VirtualHost *:80>
ServerName hoge
ServerAdmin localhost
DocumentRoot /var/www/html/hoge
<Directory "/var/www/html/hoge">
AllowOverride All
</Directory>
</VirtualHost>
ログファイルの出力先は省略します。(省略するとデフォルトの出力先になります。)
/var/www/htmlは共有フォルダとかシンボリックリンクとかVagrantの厄介なやつです。各自の環境に合わせてください。
ブラウザでdev/とhoge/それぞれ表示できればOKです。
もっと早くやっておけよと自分を責めました(´・ω・`)
最近、Boxを再構築した際に、「Vagrant開発環境のワークフローをちゃんとまとめておきたい」と思ったのでまとめてみました。
レポジトリにほぼ同じワークフローメモがあります。
github - bmf-san/vagrant-development-workflow
以下のアプリケーションがホストマシン(Mac)にインストールされていること
開発環境ディレクトリにて、Vagrantfileを作成する
vagrant init
Boxテンプレートを取得し、Boxを作成する
vagrant box add BOX_NAME /path/to/box/url
vagrant-hostupdaterのインストール
vagrant plugin install vagrant-hostsupdater
Vagrantfileの編集
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
# Box Name
config.vm.box = "centos6.7"
# Network
config.vm.network "private_network", ip: "192.168.33.10"
# Synced Folder
config.vm.synced_folder "/path/to/directory", "/var/www/html",
:owner => "apache",
:group => "apache",
:mount_options => ["dmode=775,fmode=664"]
# Provider(Optional)
config.vm.provider "virtualbox" do |vb|
vb.customize ["modifyvm", :id, "--paravirtprovider", "kvm"]
end
# Host Updater
config.vm.network :private_network, ip: "192.168.33.10"
config.vm.hostname = "localdev"
config.hostsupdater.aliases = ["localdev-hoge"]
# xdebug(Optional)
config.vm.network :forwarded_port, host: 3000, guest: 3000
end
Vagrantの起動と接続
vagrant up
ー起動vagrant ssh
ーssh接続vagrant reload
ー再起動vagrant halt
ー停止vagrant provision
ープロビジョニング(ホストの更新)Apacheのインストール
yum install httpd
ーApacheのインストールservice httpd start
ーサーバー起動chkconfig httpd on
ーログイン時自動起動設定Apacheの設定
cd /etc/httpd/conf.d
vim localdev-hoge.conf
ーホスト別の設定ファイルを作成(localdev-hoge
でアクセスできるように設定)<VirtualHost *:80>
ServerName localdev-hoge
ServerAdmin localhost
DocumentRoot /var/www/html/path/to/directory
<Directory "/var/www/html/path/to/directory">
AllowOverride All
</Directory>
</VirtualHost>
service httpd restart
ーサーバー再起動で設定を反映AnsibleでVagrantの環境構築をする最初の一歩です。
プロビジョニングができる環境を整えます。
任意のディレクトリ(例として今回はcentos7.3)にてVagrant環境を構築します。
vagrant box add https://atlas.hashicorp.com/centos/boxes/7
vagrant init
ここまでのディレクトリ構成
centos7.3/
├── .vagrant.d
├── Vagrantfile
※デフォルトのbox名にスラッシュが入っているのでリネームしたほうがいいかもです。
Homebrewかpipかgithubからソースを持ってくるか色々やり方があります。
いずれかの方法でansbileをホストOS側にインストールします。
私は何となくpipでインストールしました。
インストールは割愛します。
Ansibleのインストールが完了したら、provisioning
ディレクトリを用意して、hosts
、site.yml
の2つのファイルを作成します。
それから、ansibleでvagrantにsshをするので、sshの設定ファイルを開発ディレクトリ直下に用意しておきます。vagrant ssh-config > ssh.config
※ssh.configの場所は任意の場所でOK
hostsの中身
[vagrants]
127.0.0.1 ansible_ssh_port=2200 ansible_ssh_user=vagrant ansible_ssh_private_key_file=.vagrant/machines/default/virtualbox/private_key
site.ymlの中身
---
- hosts: vagrants
become: true
user: vagrant
tasks:
- name: install packages zsh
ping:
ここまでのディレクトリ構成
centos7.3/
├── Vagrantfile
├── provisioning
│ ├── hosts
│ └── site.yml
└── ssh.config
※ssh.configは~/.ssh/configに記述するなど必ずしもこのディレクトリ内でなくともいいと思います。
プロビジョニングを実行してみます。
vagrant provision
$ vagrant provision
==> default: [vagrant-hostsupdater] Checking for host entries
==> default: Running provisioner: ansible...
default: Running ansible-playbook...
PLAY [vagrant] *****************************************************************
TASK [setup] *******************************************************************
ok: [127.0.0.1]
TASK [check ping] **************************************************************
ok: [127.0.0.1]
PLAY RECAP *********************************************************************
127.0.0.1 : ok=2 changed=0 unreachable=0 failed=0
すごーーい! たのしーーー!!
vagrantにansibleでsshする時に結構ハマったのですが、teratailの質問に助けられました。
vagrantにansbileでsshしようとすると失敗する
とりあえずローカル環境でAnsibleを使ったプロビジョニングができる環境が整ったので、
タレづくりに専念できそうです。
VPSなどホスト別にプロビジョニングができるように設定したり、ベストプラクティスをかじっておく必要がありそうです。
次回オレオレLaravel環境のタレを作って記事にしたいと思います(予定)
vagrant destroy
を実行して vagrant up
で再構築すると、ssh-config
のポート番号が変わる場合があるみたいです。
ある日突然プロビジョニングできなくなった!なんて時はssh接続情報を確認してみると良いかもです。
今回は対話形式のコマンド実行ではなく、cronで定期的にShellScriptを実行、Slackに出力結果を報告しようという試みです。
意外とこの類いのコピペでパクれるリスペクトできる参考ソースが調べても出てこなかったので、途中で挫折仕掛けましたw
API叩いてcrontabで回すという無難な方法もありましたが、せっかくつくったhubotを活躍させたかったので頑張りました。
注意: この記事を執筆している時は確かに動いていたのですが、LogWatchの出力テキストに何かしらの問題があるらしく、正しく実行されない可能性があるようです。Slack APIのAttachment APIを利用した場合もやってみたのですが、同様でした。原因はよくわかっていません。。
なお筆者はCoffeeScriptに関して無知な模様(:3」∠)
node-cronというパッケージを使用するので、npm install node-cron
でインストールしておいてください。
logwatch.coffee
cron = require('cron').CronJob
module.exports = (robot) ->
new cron '*/1 * * * *', () =>
@exec = require('child_process').exec
command = "/YourHubotName/scripts/shell/logwatch.sh"
@exec command, (error, stdout, stderr) ->
robot.send {room: "logwatch"}, stdout
, null, true, "Asia/Tokyo"
CoffeeScriptは、cronがちゃんと動作するかすぐに確認したかったので1分ごとにcronを回すよう指定しています。
cronの時間指定方法は通常のcronと同じなので各自自由に設定してください。
slackのchannel指定についてですが、# をつけても実行されるようですが、private channelの場合は実行されないようです。(# はそもそもpublic channelという意味??)
hubot-slackのバージョンによってもchannel指定が異なるようですが、その辺は私の環境ではハマらなかったのでハマった方は各自調べてください。m( )m
logwatch.sh
# !/bin/bash
logwatch --print
logwatchでログを出力するだけのカンタンなスクリプトです。
これで“一分ごとにlogwatch --print
の出力結果をSlackのlogwatchチャンネルに投稿する”というタスクをhubotに担当させることができました。
logwatchは今までメールで出力を送信していたのですが、Slackに送信できるようになったので煩わしさが減りました\(^o^)/
追記:Slackに投稿されるテキストが長すぎると分割されて投稿されるようです。ダブって投稿されているなあ〜と思ったらlogwatchの出力テキストが長すぎたためでした。
追記の追記:Attachmentsがhubot-slack v3.3.0から使えるらしいです!
hubot-slackでattachmentsを使う
GMOクラウドサポートガイド さんが親切に説明されています。
logwatchの設定ファイルでPrint=Yesを設定するとメール送信が停止されます。
実行権限を変更するやり方だと何らかの拍子に権限が変わるからあんまり良くないという記事をどっかで見かけたので、私はこのやり方でメール停止しました。
CoffeeScriptなんか嫌いです(泣)
slackでShellScriptを実行するという記事を前回書きましたが、“特定のチャンネルで、特定のユーザーのみ”という条件を加えるには一工夫いるようです。
少し調べてみたのですが、まだ良くわかっていないので追々手をつけていきたいと思います。
redux-formでサーバーサイドのバリデーションを実装している時に、promissをいじってredux-formのSubmissionError
を投げていたらUncaught (in promise) error
と怒られた話です。
return
がなかっただけでした。
修正前
class Categories extends Component {
onSubmit(props) {
const {createCategory, fetchCategories, reset} = this.props;
createCategory(props).then((res) => {
if (res.error) {
console.log('error');
throw new SubmissionError({name: 'User does not exist', _error: 'Login failed!'});
} else {
console.log('success');
reset();
fetchCategories();
}
});
}
// 以下色々省略
}
修正後
class Categories extends Component {
onSubmit(props) {
const {createCategory, fetchCategories, reset} = this.props;
return createCategory(props).then((res) => {
if (res.error) {
console.log('error');
throw new SubmissionError({name: 'User does not exist', _error: 'Login failed!'});
} else {
console.log('success');
reset();
fetchCategories();
}
});
}
// 以下色々省略
}
jsむずい。promissまだ良くわかっていない。(コールバックを楽にするためのもの程度の認識。。。)
Laravel×ReactでSPAつくっているよーという方、ぜひLara Cafe
にご参加ください!(助けてぇ)
Redux Form -Submit Validation Example
throw new SubmissionError() causing Uncaught (in promise) error
Reduxというアーキテクチャの概念を理解するには、日本語情報がそこそこ充実していました。
実際につくりたいモノの参考になりそうなソースを探すには少し手間がかかりました。
js仕様の違い、コンパイラやタスクランナーなどビルド環境の違い、Reactそのもののバージョンの違い、関連パッケージのバージョンの違いなどペチパー一筋だった自分には中々しんどかったです。。。
色々調べてみてようやく手を動かせるようになったので、その時参考になったリポジトリや記事をリストアップしておきます。
※都度更新していきますー
github - onerciller/react-redux-laravel
https://github.com/onerciller/react-redux-laravel
github - rajaraodv/react-redux-blog
https://github.com/rajaraodv/react-redux-blog
github - mustafawm/blogapp
https://github.com/mustafawm/blogapp
github - mzabriskie/axios
https://github.com/mzabriskie/axios#handling-errors
React-Redux をわかりやすく解説しつつ実践的に一部実装してみる
http://ma3tk.hateblo.jp/entry/2016/06/20/182232
React + ReduxのプロジェクトでRedux Formを使ったので使い方のまとめと注意点
http://ichimaruni-design.com/2016/10/react-redux-form/
Reduxでコンポーネントを再利用する
http://qiita.com/kuy/items/869aeb7b403ea7a8fd8a
【Redux入門】 React + Redux の考え方を理解する
http://okakacacao.wpblog.jp/technology/what-is-redux
Redux入門 6日目 ReduxとReactの連携(公式ドキュメント和訳)
http://qiita.com/kiita312/items/d769c85f446994349b52
Reduxでのクライアントサイドvalidationをどこでやるべきか?
http://qiita.com/inuscript/items/5bed7812b3c1447b7b60
Reduxの実装とReactとの連携を超シンプルなサンプルを使って解説
http://mae.chab.in/archives/2885
React-Redux をわかりやすく解説しつつ実践的に一部実装してみる
http://ma3tk.hateblo.jp/entry/2016/06/20/182232
Redux入門 3日目 Reduxの基本・Reducers(公式ドキュメント和訳)
http://qiita.com/kiita312/items/7fdce94912d6d9c801f8
【React/Redux】わたしもみている | Container Components
http://kenjimorita.jp/read1/
Reduxで非同期処理をしたいときに、なぜMiddlewareを使わないといけないのか
http://qiita.com/enshi/items/557dcd7df60e6128249e
react+reduxで非同期処理を含むtodoアプリを作ってみる
http://qiita.com/halhide/items/a45c7a1d5f949596e17d
もうはじめよう、ES6~ECMAScript6の基本構文まとめ(JavaScript)~
http://qiita.com/takeharu/items/cbbe017bbdd120015ca0
React-Reduxを使った開発でのディレクトリ構成をどうしたらいいのか的なことから、こうやって組んだらいいんじゃないか的なお話
http://watanabeyu.blogspot.jp/2016/08/react-redux.html
Connecting Mapdisptachtoprops in V6 reduxForm()
https://github.com/erikras/redux-form/issues/1050
react-routerでURLパラメータを指定した際、URL直打ちだと404になります
https://teratail.com/questions/26245
redux-form Multiple field errors?
https://github.com/erikras/redux-form/issues/639
Redux Form -Submit Validation Example
http://redux-form.com/6.0.0-alpha.4/examples/submitValidation/
throw new SubmissionError() causing Uncaught (in promise) error
https://github.com/erikras/redux-form/issues/2269
redux-form textarea value not updating
http://stackoverflow.com/questions/40970691/redux-form-textarea-value-not-updating
もうちょっとgithubのコードをあさってみたいところです。
]]>まずReactのフォームにはcontrolled formとuncontrolled formという2つのパターンがあることを先に理解しておくと良いかもしれません。参考:React ドキュメント
私はまだ理解が及んでいないところも多々ありますが、Reactの実装例が少ないので少しでも刺激になればという感じで記事を公開したいと思います。
(もっと楽なやり方とかこうした方がいいといった指摘があると幸いです。)
今回の実装にあたり色々ググったのですが、inputが一つしかない実装例ばかりで結構しんどかったです。。。
複数のinputを扱う実装例はbindを使用したものがほとんどだと思います。
ドキュメントに載っている実装例や検索するとでてくる大抵の実装例にはbindを使用したケースが多かったです。
しかし、このbindが今ひとつ理解できない・・・。
ネストしていないシンプルなオブジェクトのstateを用意した例は多々あったのですが、ネストしたオブジェクトを利用した場合、どうすればいいのかよくわかりませんでした(泣)
そこで調べまくって見つけたのがこの記事でした。
Stack Overflow - Best practice for ReactJS form components
というわけで上記を参考にしつつ実装します。
htmlはこんな感じです。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>React Multi Input Form</title>
</head>
<body>
<div id="multi-input-form"></div>
<!-- scripts -->
<script src="path/to/react.js"></script>
<script src="path/to/react-dom.js"></script>
<script src="path/to/browser.min.js"></script>
</body>
</html>
そしてReactはこんな感じです。
var FormApp = React.createClass({
getInitialState: function () {
// ネストされたオブジェクトを用意
return {
data: {
name: '',
email: ''
}
};
},
handleChange: function (event) {
// ネストされたオブジェクトのdataまでアクセスしておく
var data = this.state.data;
// eventが発火したname属性名ごとに値を処理
switch (event.target.name) {
case 'name':
data.name = event.target.value;
break;
case 'email':
data.email = event.target.value;
break;
}
// 状態を更新
this.setState({
data: data
});
},
handleSubmit: function () {
console.log(this.state.data.name);
console.log(this.state.data.email);
},
render: function () {
return (
<form action="javascript:void(0)" onSubmit={this.handleSubmit}>
{/* Name */}
<label htmlFor="name">お名前</label>
<input type="text" name="name" value={this.state.name} onChange={this.handleChange} />
{/* Email */}
<label htmlFor="email">メールアドレス</label>
<input type="email" name="email" value={this.state.email} onChange={this.handleChange} />
{/* Submit Button */}
<button type="submit">送信</button>
</form>
);
}
});
ReactDOM.render(
<FormApp />,
document.getElementById('multi-input-form')
);
冒頭で面倒だと言いましたが、案外シンプルな気がしてきました。笑
特筆すべき点は、、、特になさそうですね。
動作はこんな感じです。
Reactの記事を色々調べていて思ったのですが、Reactの触りしかない記事ともうほんとに前線でReactやってみますみたいな記事の二極化が結構進んでいるように感じました。
Javascriptの前提知識が不足気味な私には生意気な物言いですが、もう少し過程にフォーカスした記事があるといいなーと思いました。
同じReactでも人によってはESなんちゃらだったりstrictモードだったり、ビルド環境が違ってたり、、フロントエンドのぬかるみにはまっている自分には何が何やらです(_)
毎度Reactネタは悲壮感漂っている気がしますが、Reactをかくのは楽しいです。(`・ω・´)ゞ
ツ◯ッターのフォローボタンをパクったリスペクトしたものをつくります。仕様はだいたい同じだと思いますが、仕組みは異なります。
クリックでフォロー/フォロー中とテキストが切り替わる、フォロー中の時にホバーした場合は解除というテキストを出す。これだけです。
やや装飾にこだわって全体に無駄なCSSが設定されていますが、その辺は適宜スタイルシートを調整してください。
※パスは適宜調整してください! (直すのめんどくさかった)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="component">
<p><span><i class="fa fa-twitter fa-4x"></i></span></p>
<h1>React Follow Button Component</h1>
<div id="content"></div>
</div><!-- .component -->
<!-- scripts -->
<script src="build/react.js"></script>
<script src="build/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
<script type="text/babel" src="follow.js"></script>
</body>
</html>
html {
height: 100%;
margin: 0;
height: 100vh;
}
body {
color: #F1F1F1;
font-family: 'Open Sans',Arial,Helvetica Neue,sans-serif;
min-width: 100%;
min-height: 100%;
margin-top: 200px;
background: linear-gradient(230deg, #a24bcf, #4b79cf, #4bc5cf);
background-size: 300% 300%;
/*-webkit-animation: bodyBg 60s ease infinite;
-moz-animation: bodyBg 60s ease infinite;
animation: bodyBg 60s ease infinite;*/
}
@-webkit-keyframes bodyBg {
0%{background-position:0% 84%}
50%{background-position:100% 16%}
100%{background-position:0% 84%}
}
@-moz-keyframes bodyBg {
0%{background-position:0% 84%}
50%{background-position:100% 16%}
100%{background-position:0% 84%}
}
@keyframes bodyBg {
0%{background-position:0% 84%}
50%{background-position:100% 16%}
100%{background-position:0% 84%}
}
.component {
text-align: center;
}
h1 {
line-height: 0.8;
letter-spacing: 3px;
font-weight: 300;
text-align: center;
margin-bottom: 40px;
}
/*
Follow Button
*/
.follow-button {
display: block;
margin: 0 auto;
width: 200px;
color: white;
font-size: 20px;
padding: 10px 40px 10px 40px;
border: solid white 1px;
border-radius: 2px;
cursor: pointer;
}
.follow-button:hover {
transition: .3s;
color: #43cea2;
background-color: white;
}
id名contentのdiv内にフォローボタンのコンポーネントを生成していきます。
cssのfollow-buttonのクラスは生成するフォローボタンのスタイルです。
(挙動だけ確認したい方はcssはスルーしても問題ないでしょう)
var FollowButton = React.createClass({
getInitialState: function () {
return {
value: "フォロー",
toggle: false
};
},
handleClick: function () {
if (this.state.toggle) {
this.setState({
value: "フォロー",
toggle: false
});
} else {
this.setState({
value: "フォロー中",
toggle: true
});
};
},
handleMouseOver: function () {
if (this.state.toggle) {
this.setState({
value: "解除",
});
};
},
handleMouseOut: function () {
if (this.state.toggle) {
this.setState({
value: "フォロー中",
});
};
},
render: function () {
return (
<span className="follow-button" onClick={this.handleClick} onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
{this.state.value}
</span>
);
},
});
ReactDOM.render(
<FollowButton />,
document.getElementById('content')
);
Reactチュートリアル並のコンポーネントをつくれるようになるにはまだ場数が必要なようです(:3」∠)
[BABEL] Note:The code generator has deoptimised the styling of "D:/path/to/hoge.js" as it exceeds the max of "100KB"
というエラーなわけですが、babelを使っていてファイルが大きすぎると発生するエラーのようです。
特に気にすることはないみたいですが、警告を非表示にしたいのであれば、babelのcompactというoptionをfalseにすればいいみたいです。
Laravel-Elixirのbrowserifyを使った時のbabelのpluginsの編集方法
これはpluginsの編集方法についてですが、optionに関しても参考になるかと思います。
しかし警告を消してしてもファイルは大きいままなので、ファイルを圧縮させることで対応しました。
gulp --production
optionの変更の仕方がよくわからなかったですヽ(´ー`)ノ
]]>Reactで作るSPAの簡易的なボイラープレートをつくりました。
最近のフロントエンドには何とかついていくだけで精一杯なため、ソースに自信はありませんが、一応形にはなっていると思いますと言い訳だけしておきます(_)
※ほとんどが現時点での最新版を使っていると思いますが、react-routerなんかは一つ前のバージョンだと思います。
これだけです(--)
実際にそこそこのSPAを構築すると、さらに外部のライブラリを追加したり、コンポーネントが複雑化したりしてカオスになりますが、そこは自分の設計力が足りないとこなんだと思います。
]]>A Better File Structure For React/Redux Applications
シンプルかつベタなパターンです。SPA開発ならこれが定番でしょうか。
actions/
CommandActions.js
UserActions.js
components/
Header.js
Sidebar.js
Command.js
CommandList.js
CommandItem.js
CommandHelper.js
User.js
UserProfile.js
UserAvatar.js
containers/
App.js
Command.js
User.js
reducers/
index.js
command.js
user.js
routes.js
ドメインが複数ある時に、真っ先に思い浮かびそうなパターン。
スッキリしていますが、コンパイルとか面倒くさくなりそうな予感。SPAならこれでもOK??
各ディレクトリ内でドメインでグルーピングしてディレクトリきっても良さそう。
actions/
CommandActions.js
ProductActions.js
UserActions.js
components/
Header.js
Sidebar.js
Command.js
CommandList.js
CommandItem.js
CommandHelper.js
Product.js
ProductList.js
ProductItem.js
ProductImage.js
User.js
UserProfile.js
UserAvatar.js
containers/
App.js
Command.js
Product.js
User.js
reducers/
index.js
foo.js
bar.js
product.js
routes.js
トップのディレクトリをドメインできって、action,container,reducerやらを接尾辞で管理していくパターン。
MVCのサーバーサイドフレームワークに導入してする際は、このパターンが馴染みそう。
app/
Header.js
Sidebar.js
App.js
reducers.js
routes.js
command/
Command.js
CommandContainer.js
CommandActions.js
CommandList.js
CommandItem.js
CommandHelper.js
commandReducer.js
product/
Product.js
ProductContainer.js
ProductActions.js
ProductList.js
ProductItem.js
ProductImage.js
productReducer.js
user/
User.js
UserContainer.js
UserActions.js
UserProfile.js
UserAvatar.js
userReducer.js
色々な記事やリポジトリを拝見しましたが、環境によってバラバラなようです・・・
]]>ソースコードの大部分はReact入門を参考にさせていただきました。
雑なgifサンプルはこちら(:3」∠)
markedとhighlight.jsをbowerでインストール
bower install marked
bower install highlightjs
それぞれご自分の環境にインストールしてパスの設定までしておいてください。
bower install highlightではなく、highlightjsです。
両者は別物ようで、私はこれを間違えていたせいて小一時間ハマりました・・・・(泣)
htmlはこんな感じで↓
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<link href="path/to/monokai.css" rel="stylesheet">
<link href="path/to/style.css" rel="stylesheet">
</head>
<body>
<div class="markdown-component">
<h1>React Markdown Editor</h1>
<div id="content"></div>
</div><!-- .component -->
<!-- scripts -->
<script src="path/to/react.js"></script>
<script src="path/to/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
<script src="path/to/marked.min.js"></script>
<script src="path/to/highlight.pack.min.js"></script>
<script type="text/babel" src="path/to/markdown.js"></script>
</body>
</html>
シンタックスハイライトに使用するカラーテーマはmonokaiが好きなのでmonokaiのスタイルシートを設定しました。
babelについては今回CDNを使用していますが、bowerでインストールしてもOKです。
さて、Reactコンポーネントを作っていきますが、冒頭でも述べたように、大部分はReact入門を参考にしているので、こちらを一読しておくとよろしいかと思います。
参考ソースにhighlight.jsの設定コードだけ追加した感じです。(全然仕事していないww)
markdown.js
var App = React.createClass({
getInitialState: function() {
return {
markdown: ""
};
},
updateMarkdown: function(markdown) {
this.setState({
markdown: markdown
});
},
render: function() {
return (
<div>
<TextInput onChange = {this.updateMarkdown}/>
<Markdown markdown = {this.state.markdown}/>
</div>
);
}
});
var TextInput = React.createClass({
propTypes: {
onChange: React.PropTypes.func.isRequired
},
_onChange: function(e) {
this.props.onChange(e.target.value);
},
render: function() {
return (
<textarea onChange = {this._onChange}></textarea>
);
}
});
var Markdown = React.createClass({
componentDidUpdate: function() {
marked.setOptions({
highlight: function(code, lang) {
return hljs.highlightAuto(code, [lang]).value;
}
});
},
propTypes: {
markdown: React.PropTypes.string.isRequired
},
render: function() {
var html = marked(this.props.markdown);
return (
<div dangerouslySetInnerHTML={{__html: html}}></div>
);
}
});
ReactDOM.render(
<App />,
document.getElementById("content")
);
テキストの入力部分のコンポーネント、 マークダウンの出力部分のコンポーネント、それらを統合するコンポーネントの3つに分割されています。
マークダウンのパースはmarkedという関数で行っています。
このmarked関数のオプションをcomponentDidUpdateのところでhighlight.jsを使うよう設定しています。
オプションの設定方法についてはhighlight.jsのREADMEにかいてあります。
dangerouslySetInnerHTMLというのはxss対策でデータをサニタイズするプロパティです。
初めてエディタつくったのですが、ライブラリでパパっと出来てしまうのですね〜(:3」∠)
先日、ES6を勉強したので書き換えてみました。propsTypeの対応方法はよくわからなかったので省略してしまいましたw
/**
*
* Editor
*
*/
import React from 'react';
import ReactDOM from 'react-dom';
export default class Editor extends React.Component{
constructor(props) {
super(props);
this.state = {
markdown: ''
};
this.updateMarkdown = this.updateMarkdown.bind(this);
}
updateMarkdown(markdown) {
this.setState({
markdown: markdown
});
}
render() {
return (
<div>
<TextInput onChange={this.updateMarkdown}/>
<Markdown markdown={this.state.markdown}/>
</div>
);
}
};
class TextInput extends React.Component{
constructor(props) {
super(props);
this._onChange = this._onChange.bind(this);
}
_onChange(e) {
this.props.onChange(e.target.value);
}
render() {
return (
<textarea onChange={this._onChange}></textarea>
);
}
};
class Markdown extends React.Component{
constructor(props) {
super(props);
}
componentDidUpdate() {
marked.setOptions({
highlight: function(code, lang) {
return hljs.highlightAuto(code, [lang]).value;
}
});
}
render() {
var html = marked(this.props.markdown);
return (
<div dangerouslySetInnerHTML={{__html: html}}></div>
);
}
};
]]>npmでReact Tag Autocompleteを導入します。
npm install --save react-tag-autocomplete
インクルードの仕方は色々あるかと思いますが、今回の環境ではrequireを使います。
var ReactTags = require('react-tag-autocomplete');
これで準備OKです。
// 色々省略
<div id="react-tag-autocomplete"></div>
githubにUsageがありますが、ちょっと加工してapiをたたいてデータを取得してきたケースを想定してみます。(ここではsuperagentを使っています。)
apiではこんな感じのJsonレスポンスを返しています。
[{"id":1,"name":"プログラミング"},{"id":2,"name":"家事"},{"id":3,"name":"自宅警備"},{"id":4,"name":"早寝早起き"},{"id":5,"name":"三日坊主"}]
res.body.skillsのデバッグ
var ReactTags = require('react-tag-autocomplete');
var App = React.createClass({
getInitialState: function () {
return {
tags: [],
suggestions: []
}
},
componentDidMount: function () {
request
.get('/api/v1/user/config')
.end(function(err, res){
if (err) {
alert('通信エラーです。リロードしてください。');
}
this.setState({
suggestions: res.body.skills
});
}.bind(this));
},
handleDelete: function (i) {
var tags = this.state.tags
tags.splice(i, 1)
this.setState({ tags: tags })
},![tags.gif](https://qiita-image-store.s3.amazonaws.com/0/124495/173c6de9-b87a-6200-65ed-506e181f565e.gif)
![tags.gif](https://qiita-image-store.s3.amazonaws.com/0/124495/a3372702-2a85-9b80-0b53-ede2c9c3c486.gif)
handleAddition: function (tag) {
var tags = this.state.tags
tags.push(tag)
this.setState({ tags: tags })
},
render: function () {
return (
<ReactTags
tags={this.state.tags}
suggestions={this.state.suggestions}
handleDelete={this.handleDelete}
handleAddition={this.handleAddition} />
)
}
})
ReactDOM.render(
<App />,
document.getElementById('react-tag-autocomplete')
);
動作確認(余計なものが映っていますが・・)
cssは設定していないのですごくダサいですねw
他のオプションやcssのクラス名などgithubに丁寧に明記してあります。
便利な時代だなぁヽ(´ー`)ノ
]]>今は絶版になっているPHPによるデザインパターン入門を教科書にして、PHPでデザインパターンを学びます。(※Amazonで中古がありますが、定価の倍以上の値段が付いているようです。)
本連載で扱うコードはgithubにまとめていきます。
本来であれば、OOPの先駆けである言語でデザインパターンを学びたいところでしたが、PHP以外の言語の素養がなかったことと、PHPでデザインパターンを解説している本に出会ったことから、PHPでデザインパターンを学んでみることにしました。
PHPによるデザインパターン入門
Do You PHP はてな
Github shimooka/PhpDesignPattern
パーフェクトPHP
* プログラミングPHP 第3版
パターンごとの参考は、それぞれの記事に記載します。
デザインパターンを学ぶ目的は2つあります。
フレームワークへの理解を深めるため
FWの設計には随所にデザインパターンが活用されています。パターンを知ることで、FWへの理解が深まり、FWを最大限に活用することができると思います。
良質なコードをかくため
デザインパターンは先人たちが築いたオブジェクト指向の集大成と形容することができると思います。デザインパターンをいきなり取り入れて使うことは難しいかもしれませんが、"考え方"を学ぶことでオブジェクト指向への理解が深まり、日々のソースコードの質を上げることにつながるのではないかと思います。
本題に入る前に、オブジェクト指向の基本について復習しておきます。
PHPによるデザインパターン入門は2006年に発行された本のため、当初と現在ではパターンの用途について考え方が変わってきている部分もあるかと思います。
それについては初稿段階において触れることができない点だけご了承ください。
<?php
class ClassName extends BaseClassName implements InterfaceName
{
use TraitName;
public $public_property;
protected $protected_property;
private $private_property;
function methodName()
{
// do something
}
}
オブジェクト指向におけるクラス宣言のフォーマットです。
継承やインターフェース、トレイトはもちろん任意指定です。
メソッド宣言やアクセス修飾子の詳細については割愛します。
<?php
class StaticClass
{
static $property = 'This is a static property.';
static function methodA()
{
return self::$property;
}
public function methodB()
{
return self::methodA();
}
}
echo StaticClass::methodB(); // This a is static property.
実際はこんなややこしいことはしないとは思いますが・・・
静的プロパティや静的メソッドは、スコープ定義演算子(::)やselfキーワードで呼び出しが行えるということを一纏めに説明したかった次第です。
<?php
class SuperClass
{
public function superClassMethod()
{
echo 'This is a super class method.';
}
}
class SubClass extends SuperClass
{
public function subClassMethod()
{
echo 'This is a sub class method.';
}
}
$subClassObject = new SubClass();
echo $subClassObject->subClassMethod(); // This is sub class method.
echo $subClassObject->superClassMethod(); // This is a super class method.
派生クラス(サブクラス)で親クラスのメソッドを明示的に使用するには、parentキーワードを使うことができます。
extendsキーワードで複数のクラスを継承すること(=多重継承)はできません。
<?php
abstract class Human
{
abstract function getAbility();
public function run()
{
echo 'Run';
}
}
class SuperHuman extends Human
{
public function getAbility()
{
echo 'Fly';
}
}
$super_human_instance = new SuperHuman();
echo $super_human_instance->run(); // Run
echo $super_human_instance->getAbility(); // Fly
抽象クラスはインターフェースと同じく、全ての定義済み抽象メソッドを派生クラスで実装する必要があります。
インターフェースでは実装を記述できませんが、抽象クラスでは実装を記述することができます。
抽象クラスとインターフェースの使い分けはPHPのinterfaceとabstractを正しく理解して使い分けたいぞーがわかりやすく解説されています。
<?php
interface Human
{
public function eat();
public function sleep();
public function walk();
}
class Boy implements Human
{
public function eat()
{
echo 'Eat';
}
public function sleep()
{
echo 'Sleep';
}
public function walk()
{
echo 'Walk';
}
public function fly()
{
echo 'Fly';
}
}
$boy_instance = new Boy();
echo $super_human_instance->eat(); // Eat.
echo $super_human_instance->walk(); // Walk.
echo $super_human_instance->sleep(); // Sleep.
echo $super_human_instance->fly(); // Fly.
インターフェースはクラスの振る舞い(メソッドのみ)を定義するだけで、実装は行いません。
抽象クラスは、ベースとなるクラスの一部を拡張するように定義・実装を行いますが、インターフェースは定義されたメソッドを全て実装しなくてはなりません。
インターフェースを使った擬似的多重継承は可能です。
<?php
trait Authorize
{
public function register()
{
echo 'Registration';
}
}
class User
{
use Authorize;
}
$user_instance = new User();
echo $user_instance->register();
traitはクラス階層を超えたコードの再利用を可能します。また、traitを使用することで多重継承を行うことができます。
今回はデザインパターンを学ぶ意図とオブジェクト指向の基礎について説明しました。
次回から各デザインパターンの紹介をします。
仕組みがわかっても実際に使ってみる(設計する)のが難しいオブジェクト指向は、繰り返し手を動かして学び直す必要がありそうです。
]]>似たような処理を枠組み(型)としてスーパークラスで定義し、より具体的な処理内容をサブクラスで実装するというパターンです。
単なる継承ではなく、具体的な処理内容を抽象メソッドとして定義することで、スーパークラスのメソッドの実装を保証し、クラスの振る舞いをサブクラスによって定義させる継承を利用したパターンです。
処理の枠組みを定義するクラスで、枠組みを定義するメソッド(template method)とそれを利用するメソッドを含みます。
AbstractClassを継承するサブクラスで、Abstractクラスで定義された抽象メソッドを実装します。
各サブクラスで必要な処理をスーパークラスに集約できるため、サブクラスでの共通実装部分が減ります。
大枠をスーパークラスで定義することにより、具体的な処理内容は柔軟にサブクラスで実装することができます。
「ん・・・以前にも同じようなクラスをつくったような・・・」
そんな時は共通化できるメソッドを抜き出すことでパターンを適用できるかもしれません。
<?php
abstract class AbstractArticle {
public function __construct($data)
{
$this->title = $data['title'];
$this->author = $data['author'];
}
/**
* Template Method
*/
public function display()
{
return "Title:{$this->getTitle()}<br />Author:{$this->getAuthor()}<br />Content:{$this->getContent()}";
$this->getTitle();
$this->getAuthor();
$this->getContent();
}
/**
* Common Method
*/
public function getTitle()
{
return $this->title;
}
/**
* Common Method
*/
public function getAuthor()
{
return $this->author;
}
/**
* Abstract Method
*/
protected abstract function getContent();
}
<?php
require_once 'AbstractArticle.php';
/**
* Concrete Class
*/
class CorporateArticle extends AbstractArticle {
protected function getContent()
{
return 'This is a Corporate Article. Here write your things.';
}
}
<?php
require_once 'AbstractArticle.php';
/**
* Concrete Class
*/
class UserArticle extends AbstractArticle {
protected function getContent()
{
return 'This is a User Article. Here write your things.';
}
}
<?php
require_once 'AbstractCorporateArticle.php';
require_once 'AbstractUserArticle.php';
$data = [
"title" => "What is the Template Method?",
"author" => "Qiita Tarou."
];
$corporate_article = new CorporateArticle($data);
$user_article = new UserArticle($data);
echo $corporate_article->display();
// 出力
Title:What is the Template Method?
Author:Qiita Tarou.
Content:This is a Corporate Article. Here write your things.
インスタンス生成のコストを制御するために、インスタンスが1つしかないことを保証するパターンです。
priavateのコンストラクタとインスタンスを1つだけ返すstaticメソッドと自分自身のインスタンスを保持するためのstatic変数を用意するだけです。
Singletonパターンが保持する自分自身へのアクセスをprivateに制限しているためクライアント側のコードからのアクセスを制御することができます。
生成されるインスタンスの数を2つ以上に変更することも可能です。
<?php
class SingletonConfig {
private $config;
/**
* a single variable
*/
private static $instance;
private function __construct()
{
$this->config = 'AUTO';
}
/**
* Create a only instance
*/
public static function getInstance()
{
if (!isset(self::$instance)) {
self::$instance = new SingletonConfig();
}
return self::$instance;
}
public function getConfig()
{
return $this->config;
}
public final function __clone()
{
throw new RuntimeException('Clone is not allowed against' . get_class($this));
}
}
<?php
require_once 'SingletonConfig.php';
$instanceA = SingletonConfig::getInstance();
$instanceB = SingletonConfig::getInstance();
if ($instanceA->getConfig() === $instanceB->getConfig()) {
echo 'True';
} // true
API(互換性のないインターフェース)同士を適合させるためのパターンです。既存のコードの変更をせずに、再利用することで新しい機能を提供するというものです。再利用するコードには変更を加えないというのが特徴です。
主にコードを再利用するためという後天的理由から成り立っているパターンです。(設計段階でラッパーを用意するパターンはBridgeパターンです。)
##TargetClass
API(インターフェース)の定義をします。
TargetClassに適合させる既存のAPIを提供します。
AdapteeClassのAPIをTargetClassから利用できるように変換します。
既存のクラスにラッピングする形で実装するため、既存のコードを修正する必要がありません。
要は既存APIの変更がクライアント側の変更に影響しないということです。
APIを適合させる際にAPIのアクセスを制限することができます。
既存の実績あるクラスを再利用したい時など。
<?php
class ShowData {
private $data;
public function __construct($data)
{
$this->data = $data;
}
public function showOriginalData()
{
echo $this->data;
}
public function showProcessedData()
{
echo $this->data . 'How are you?';
}
}
<?php
interface ShowSourceData {
public function show();
}
<?php
require_once 'ShowSourceData.php';
require_once 'ShowData.php';
class ShowSourceDataImpl extends ShowData implements ShowSourceData {
public function __construct($data)
{
parent::__construct($data);
}
public function show()
{
parent::showProcessedData();
}
}
<?php
require_once 'ShowSourceDataImpl.php';
$show_data = new ShowSourceDataImpl('Hello! Mr. Data.');
$show_data->show();
ラッパー部分が異なるだけで、クライアント側コードは同じです。
委譲とは、具体的な処理を別のクラスに任せるという意味です。
DIのような・・といっては語弊があるでしょうか・・・(゜-゜)
<?php
require_once '../ShowSourceData.php';
require_once '../ShowData.php';
class ShowSourceDataImpl implements ShowSourceData {
private $show_data;
public function __construct($data)
{
$this->show_data = new ShowData($data);
}
public function show()
{
$this->show_data->showProcessedData();
}
}
php7のインストールについてはこちらを参照にしました。
ちなみに私の環境は・・・
yum install yum install --enablerepo=remi,remi-php70 php-mysqlnd
これで解決できたっぽいです(:3」∠)
もしかしたら足りないパッケージもあるかもしれませんが、laravelをphp7でcomposerを使っていく分には不足ない気がします。
yum -y install --enablerepo=remi-php70 php php-mbstring php-pear php-fpm php-mcrypt php-devel php-xml
参考
php7にしたら気の所為かもしれないけど早くなった気がします。いやあきらかに体感速度が変わったような。。
]]>随分前に設定したのでうろ覚えのところもあるかもしれませんがご了承ください。
おおまかに仕組みをいうと、
Nginxでリクエストを受け付けてApcheの指定ポートにリクエストを流すという感じです()
バーチャルホストの設定はApache側で設定しておきます。Nginxが右から左へムーディ勝山するだけです。
wgetしてyumで落としてくるやり方がカンタンでした。ここでは割愛するので各自の艦橋に合わせてインストールしておいてください。
インストールが完了したら、一度Apacheを停止してNginxの動作確認をしておきましょう。
Nginxでは80番ポートを使うことにします。
Apache側ではそれ以外のポートを指定しましょう。
ここでは8080番ポートを使用することにします。
/etc/httpd/conf/httpd.conf
NameVirtualHost *:8080
<VirtualHost *:8080>
hogehogehogehoge...
</VirtualHost>
補足
iptablesの確認は、iptables -L
iptablesの場所は、
/etc/sysconfig/iptables
独自ドメインのバーチャルホストの設定をしている場合は、そっちの方のポートも変更しておきましょう。
Ex. /etc/httpd/conf.d/hoge.com.conf
# Domain
<VirtualHost *:8080>
ServerName hoge.com
DocumentRoot "/var/www/html/hoge"
DirectoryIndex index.html index.php
ErrorLog /var/log/httpd/error_log
CustomLog /var/log/httpd/access_log combined
AddDefaultCharset UTF-8
<Directory "/var/www/html/hoge">
AllowOverride All
</Directory>
</VirtualHost>
# Sub Domain
<VirtualHost *:8080>
ServerName sub-hoge.hoge.com
DocumentRoot "/var/www/html/sub-hoge"
DirectoryIndex index.html index.php
ErrorLog /var/log/httpd/error_log
CustomLog /var/log/httpd/access_log combined
AddDefaultCharset UTF-8
<Directory "/var/www/html/sub-hoge">
AllowOverride All
</Directory>
</VirtualHost>
ApacheとNginxを共存して徐々に移行するを参考にさせていただきました。
/etc/nginx/conf.d/reverse_proxy.conf
"reverse_proxy.conf" 14L, 392C
server {
listen 80;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
最後にApacheとNginxを再起動して完了です!
気になるパフォーマンスはいかほどに・・・といった感じですが、多少は早くなったかな?という感じです。。
インフラの構築は不勉強なところが多々あるので今後頑張ってみようと思います。
]]>私の環境では、/usr/local/bin/
に色々置いてあるので、そこにanyenvをインストールすることにします。
cd /usr/local/bin
git clone https://github.com/riywo/anyenv
export PATH="/usr/local/bin/anyenv/bin:$PATH"
export ANYENV_ROOT=/usr/local/bin/anyenv
eval "$(anyenv init -)"
anyenvはルートディレクトリ直下にインストールすることを想定しているせいか、ANYENV_ROOT
を任意のディレクトリに指定しないとanyenvコマンドが正しく実行されませんでした。
それからeval "$(anyenv init -)"
を書き忘れるとインストールしたパッケージのコマンドが実行できないなど不具合が起きるので忘れないようにしましょう。
これでインストールは完了です。 anyenvコマンド各種使えるようになっているかと思います。
letsencryptの証明書を自動更新するスクリプトの紹介です。
以前から作ってはいたのですが、色々と問題があったり、サーバー環境が変わったりで完全版を残せていなかったので改めてまとめました。
※letsencryptのインストールやshell scriptの実行方法等には触れません。
月に一回証明書の有効期限を問わず更新し(--force-renew
)、更新結果(成功または失敗)をslack通知するスクリプトです。
slackの設定値は外部ファイルで管理しています。
#!/bin/sh
# Import config
. /home/bmf/scripts/conf/slack.conf
# Stop Nginx
/usr/sbin/service nginx stop
# POST
if ! /home/bmf/certbot/certbot-auto renew --force-renew ; then
sleep 15
# Slack Title
TITLE=${TITLE:-"Let's Encrypt更新エラー通知"}
# Slack Message
MESSAGE=${MESSAGE:-"証明書の更新に失敗しました。"}
#POST
curl -s -S -X POST --data-urlencode "payload={
\"channel\": \"${SL_CH_LETSENCRYPT}\",
\"username\": \"${SL_BOTNAME}\",
\"attachments\": [{
\"color\": \"danger\",
\"fallback\": \"${TITLE}\",
\"title\": \"${TITLE}\",
\"text\": \"${MESSAGE}\"
}]
}" ${SL_WEBHOOKURL} > /dev/null
else
sleep 15
# Slack Title
TITLE=${TITLE:-"Let's Encrypt更新完了通知"}
# Slack Message
MESSAGE=${MESSAGE:-"証明書を更新しました!"}
#POST
curl -s -S -X POST --data-urlencode "payload={
\"channel\": \"${SL_CH_LETSENCRYPT}\",
\"username\": \"${SL_BOTNAME}\",
\"attachments\": [{
\"color\": \"danger\",
\"fallback\": \"${TITLE}\",
\"title\": \"${TITLE}\",
\"text\": \"${MESSAGE}\"
}]
}" ${SL_WEBHOOKURL} > /dev/null
fi
# Start nginx
/usr/sbin/service nginx start
成功すると、
失敗すると、
成功しても失敗しても赤なのはナンセンスですね。。。
]]>letsencryptの証明書を自動更新させるのを長らく忘れていたのでshellとcronで設定します。
shellをかきます。
shellの保存場所は適宜設けてください。
#!/bin/sh
service nginx stop
/root/letsencrypt/letsencrypt-auto certonly --standalone --renew-by-default -d DOMAIN_NAME
service nginx start
/letencrypt-autoまでのパスは適宜指定。
オプションについて同様です。(これが結構面倒な気がします。。。)
crontab -e
でcron登録します。
cronの実行確認のテストで証明書更新しまくっていたら、「更新リクエスト多すぎやめちくり〜」というエラーがでました。
「制限に引っかかったら月曜日まで待たないといけないようです。
週に20個の証明書までOKだそうなので、それを超えないようにテストしましょう。
更新には時間がかからないようですが、nginxを一度停止する必要があるのでサービス環境によってその辺り考慮する必要はある気がします。
作成したshellにSlack APIたたいて更新通知投げるのもいいと思いました。
ただエラーをどうやってキャッチすればいいかわからないです_| ̄|○
あった→Let's Encryptで自動更新したらSlackに通知したかった
普通にif文でかけばいいみたいです。
というわけでやっつけでshellをつくりました。
更新完了か失敗か通知を送るだけです。更新日時等の出力はしません。
letsencrypt.sh
#!/bin/sh
# Stop Nginx
service nginx stop
# WebHookUrl
WEBHOOKURL="SLACK_WEBHOOK_URL"
# Slack Channel
CHANNEL=${CHANNEL:-"#letsencrypt"}
# Slack Bot Name
BOTNAME=${BOTNAME:-"ssl-bot"}
if ! /root/letsencrypt/letsencrypt-auto certonly --standalone --renew-by-default -d DOMAIN_NAME ; then
# Slack Title
TITLE=${TITLE:-"Let's Encrypt更新エラー通知"}
# Slack Message
MESSAGE=${MESSAGE:-"証明書の更新に失敗しました。"}
#POST
curl -s -S -X POST --data-urlencode "payload={
\\"channel\\": \\"${CHANNEL}\\",
\\"username\\": \\"${BOTNAME}\\",
\\"attachments\\": [{
\\"color\\": \\"danger\\",
\\"fallback\\": \\"${TITLE}\\",
\\"title\\": \\"${TITLE}\\",
\\"text\\": \\"${MESSAGE}\\"
}]
}" ${WEBHOOKURL} >/dev/null
else
# Slack Title
TITLE=${TITLE:-"Let's Encrypt更新完了通知"}
# Slack Message
MESSAGE=${MESSAGE:-"証明書の更新が完了しました!"}
#POST
curl -s -S -X POST --data-urlencode "payload={
\\"channel\\": \\"${CHANNEL}\\",
\\"username\\": \\"${BOTNAME}\\",
\\"attachments\\": [{
\\"color\\": \\"danger\\",
\\"fallback\\": \\"${TITLE}\\",
\\"title\\": \\"${TITLE}\\",
\\"text\\": \\"${MESSAGE}\\"
}]
}" ${WEBHOOKURL} >/dev/null
fi
# Restart Nginx
service nginx start
letsencryptありがたや
そこでスクリプトを見直し、何とか正しく動作するように改良してみました。
筆者はnginx+apacheのサーバー構成です。基本的には--webrootオプションを使って証明書発行や更新を行っています。
注:letsencryptのオプションについては各自の環境に読み替えて下さい。
#!/bin/sh
# WebHookUrl
WEBHOOKURL="*************************"
# Slack Channel
CHANNEL=${CHANNEL:-"#ChannelName"}
# Slack Bot Name
BOTNAME=${BOTNAME:-"BotName"}
if ! /path/to/certbot-auto renew --force-renew ; then
sleep 15
# Slack Title
TITLE=${TITLE:-"Let's Encrypt更新エラー通知"}
# Slack Message
MESSAGE=${MESSAGE:-"証明書の更新に失敗しました。"}
#POST
curl -s -S -X POST --data-urlencode "payload={
\"channel\": \"${CHANNEL}\",
\"username\": \"${BOTNAME}\",
\"attachments\": [{
\"color\": \"danger\",
\"fallback\": \"${TITLE}\",
\"title\": \"${TITLE}\",
\"text\": \"${MESSAGE}\"
}]
}" ${WEBHOOKURL} > /dev/null
else
sleep 15
# Slack Title
TITLE=${TITLE:-"Let's Encrypt更新完了通知"}
# Slack Message
MESSAGE=${MESSAGE:-"証明書を更新しました!"}
#POST
curl -s -S -X POST --data-urlencode "payload={
\"channel\": \"${CHANNEL}\",
\"username\": \"${BOTNAME}\",
\"attachments\": [{
\"color\": \"danger\",
\"fallback\": \"${TITLE}\",
\"title\": \"${TITLE}\",
\"text\": \"${MESSAGE}\"
}]
}" ${WEBHOOKURL} > /dev/null
fi
前回との違いは、--force-renew
というオプションを採用したところでしょうか。証明書の残りの有効期限に関係なく更新するというものです。
それからsleepという動作を指定時間停止させる処理を追記しました。
証明書発行に時間がかかることを考慮し、slackやnginxの再起などが問題なく行なわれることよう配慮したものですが、効果の程はわかりません。。。(どこかのブログで見たので真似してみました)
以上を注意深く行えばもう少し早く解決できたような気がします。
composer global require "laravel/installer"
MacOSならこれでいけると思います。(Winは知りません・・・)
export PATH="~/.composer/vendor/bin:$PATH"
laravel new PROJECTNAME
最新版のLaravelちゃんがカレントディレクトリにインストールされます。
ドキュメントにもかいてありますが、composerを使うよりも早く動くようです。
早くて楽。
MacOSのホームディレクトリの.bash_profileにexport PATH="~/.composer/vendor/bin:$PATH"
を書き足しておく。
.bash_profileがない場合はつくる。 .bashrcとの違いが知りたい場合はググる。
今回のテーブル
通常のテーブルは
って感じでデフォルトの規則通りリレーションを貼ればいいのですが、ちょっと癖のある名前にすると少し気をつけるところがあるようです。
ほうほう第2引数をもたせてあげればいいんだなー
public function eventTags()
{
// 第2引数はPivotテーブル!
return $this->belongstoMany('App\Modles\EventTag', 'event_tag_event)->withTimestamps();
}
public function events()
{
return $this->belongsToMany('App\Models\Events');
}
tinkerを立ち上げて確認すると・・
SQLSTATE[42000]: Syntax error or access violation: 1066 Not unique table/alias on relationship
怒られます。ヽ(´ー`)ノ
もしかしてPivotテーブル名を指定するのでは・・?
SQLSTATE[42000]: Syntax error or access violation: 1066 Not unique table/alias on relationship
public function eventTags()
{
// 第二引数はPivotテーブル!
return $this->belongstoMany('App\Modles\EventTag', 'event_tag_event)->withTimestamps();
}
public function events()
{
return $this->belongsToMany('App\Models\Events');
}
怒られませんでした。ヽ(´ー`)ノ
最近はバッグエンドよりもフロントエンドが気になって夜も寝れませんヽ(´ー`)ノ
]]>Laravelのフォームリクエストで、バリデーションされる値をカスタマイズする方法です。
APIのエンドポイントが/post/:id/delete
の時に、ルートパラメーターにフォームリクエストのバリデーションをかけたい・・なんて時に有効かもしれません。
Laravel APIにあるvalidationData
をいじります。
ルートパラメーターのidにバリデーションをかける例です。
<?php
namespace App\Http\Requests\Api\v1\Category;
use Illuminate\Foundation\Http\FormRequest;
class DeleteCategoryRequest extends FormRequest
{
const NOT_FOUND_CODE = 400;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'id' => 'numeric',
];
}
/**
* Get data to be validated from the request.
*
* @return array
*/
protected function validationData()
{
return array_merge($this->request->all(), [
'id' => $this->route('id'),
]);
}
/**
* Get the error messages for the defined validation rules.
*
* @return array
*/
public function messages()
{
return [
'id.numeric' => 'This id is not number',
];
}
/**
* Get the proper failed validation response for the request.
*
* @param array $errors
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function response(array $errors)
{
$response['messages'] = $errors;
return response()->json($response, (int) self::NOT_FOUND_CODE);
}
}
結局このカスタマイズは何となく気持ち悪くて使っていません()
もしあなたがLaravelユーザーならシンプルでカンタンに使えるデプロイツールがあります。
一般的なデプロイツールと比較して、細々としたことはできなさそうですが、必要最低限のデプロイタスクは行えるかと思います。
ドキュメントにもありますが・・composer global require "laravel/envoy=~1.0"
envoy.blade.phpというファイルを用意し、その中にタスクをblade記法に従って記述していきます。
タスクはshellコマンドを@taskの中に直接記述することができるので楽です。
@servers(['web' => '123.45.678.912'])
@macro('deploy')
composer
git
artisan
slack
@endmacro
@task('composer')
cd /var/www/html/Hoge
composer update
composer install --no-dev --optimize-autoloader
@endtask
@task('git')
cd /var/www/html/Hoge
git pull origin master
@endtask
@task('artisan')
cd /var/www/html/Hoge
php artisan down
php artisan migrate
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:clear
php artisan up
@endtask
@task('slack')
cd /var/www/html/Hoge
# WebHookUrl
WEBHOOKURL="https://hooks.slack.com/services/hogehogehogehogehogehoge"
# Slack Channel
CHANNEL=${CHANNEL:-"#prod-deploy"}
# Slack Bot Name
BOTNAME=${BOTNAME:-"Hoge-bot"}
# Slack Title
TITLE=${TITLE:-"本番環境デプロイ通知"}
cd /var/lib/git/Hoge.git
# Slack Message
MESSAGE=`git log -1 master`
#POST
curl -s -S -X POST --data-urlencode "payload={
\"channel\": \"${CHANNEL}\",
\"username\": \"${BOTNAME}\",
\"attachments\": [{
\"color\": \"danger\",
\"fallback\": \"${TITLE}\",
\"title\": \"${TITLE}\",
\"text\": \"${MESSAGE}\"
}]
}" ${WEBHOOKURL} >/dev/null
@endtask
composerとartisanはLaravelのデプロイに最適化したつもりですが、あんまり良くわかっていませんw
gitやslackは気にしないでください・・あくまで一例です。く(`・ω・´)
envoy run deploy
でデプロイできます。
多少端折りましたが、導入から利用までお手軽にセットアップできるので、ちょっとしたプロジェクトならこれで十分なのでは!
]]>laravel5.1でも5.2でも使えました。
composer require barryvdh/laravel-debugbar --dev
からの
composer install
インストールするだけでもデバッグツールとして問題なく使えますが、より詳細にデバッグしたい場合はfacadeで使えるようにしておくと便利です。
app.phpのproviderとalias部分に以下をそれぞれ指定。
\Debugbar::error();
\Debugbar::disable();
Debugbar::startMeasure();
Debugbar::stopMeasure();
その他色々。
#感想
大変便利です(゜レ゜)
public function getIndex()
{
return redirect()->to('hoge');
}
今まで何となくこっちを使っていましたが、
public function getIndex()
{
return redirect('hoge');
}
こっちでも問題なく動作するのでredirectヘルパーの実装について調べてみました。
if (!function_exists('redirect')) {
/**
* Get an instance of the redirector.
*
* @param string|null $to
* @param int $status
* @param array $headers
* @param bool $secure
* @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
*/
function redirect($to = null, $status = 302, $headers = [], $secure = null)
{
if (is_null($to)) {
return app('redirect');
}
return app('redirect')->to($to, $status, $headers, $secure);
}
}
どうやら引数が空だとインスタンスを呼び出してくれるみたいです。
ドキュメントにもそう書いてありましたw
呼び出されるインスタンスのapiはここ
toメソッドの実装は以下の通り。
/**
* Create a new redirect response to the given path.
*
* @param string $path
* @param int $status
* @param array $headers
* @param bool $secure
* @return \Illuminate\Http\RedirectResponse
*/
public function to($path, $status = 302, $headers = [], $secure = null)
{
$path = $this->generator->to($path, [], $secure);
return $this->createRedirect($path, $status, $headers);
}
redirect('hoge')
とredirect()->to('hoge')
は同じ。
単純にリダイレクトだけならredirect('hoge')
、フラッシュデータを持たせたり、コントローラーのメソッドにリダイレクトさせたい時などは空でインスタンスを返すredirect()
を使う。
# 所管
Laravel使い始めた頃から染み付いて無意識にかいているコードの実装は一度くらい確認しようと思ったφ(..)
Laravelで作っているアプリケーションに管理画面だけSPAを実装しようとした時、Laravelのディレクトリ構成とnginxのconfファイルをちょっとだけいじった話です。
初めての試みだったのでメモがてらまとめました。
バックエンドで完結するアプリをbackend-app
、フロントエンドで完結するアプリをfrontend-app
とし、ディレクトリを大きく分けました。backend-app
ではユーザー側の画面やAPIやバックエンドの処理を担当し、フロントエンドはSPAの管理画面を担当しています。
ユーザー側の画面もfrontend-app
の範疇な気がしますが、その辺は追々切り出していくことにします。
バラバラに切り出すならフルスタックなフレームワークから脱却した方がいいのかもしれませんね。。。
とりあえず今回はフロントはフロント、バックはバックで管理しやすいような構成にしてみました。
ちなみこの構成はgithubで「Laravel SPA」とか「Laravel React」とかで調べていたらよく見受けられた構成を参考にしたものです。
.
├── backend-app
│ ├── app
│ ├── bootstrap
│ ├── config
│ ├── database
│ ├── node_modules
│ ├── public
│ ├── resources
│ ├── routes
│ ├── storage
│ ├── tests
│ └── vendor
└── frontend-app
├── _components
├── dist
├── node_modules
└── src
locationディレクティブをbackend-app
とfrontend-app
で分けました。
serverディレクティブで分ける場合はサブドメインを切る運用方法になるかと思いますが、それだと今回のアプリケーションの場合微妙だと思ったのでやめました。
もっと設定を頑張る必要がある気がしますが勘弁してください(_)。。。
server {
listen 80;
server_name laravel-spa;
root /var/www/html/project/laravel-spa/backend-app/public;
charset UTF-8;
# Error
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# backend-app
location / {
index index.php index.html index.htm;
try_files $uri /index.php?$query_string;
}
# frontend-app
location /dashboard {
alias /var/www/html/project/laravel-react-redux-blog-boilerplate/frontend-app;
index index.html index.html;
try_files $uri $uri/ /dashboard//index.html;
}
# php-fpm
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
include fastcgi_params;
}
}
※修正(2017/4/2追記)
spaでルート以下のURL(ex. laravel-app/dashboard/post)を直打ちまたはリロードすると404エラーが発生するので、修正しました。
修正前
# frontend-app
location /dashboard {
alias /var/www/html/project/laravel-react-redux-blog-boilerplate/frontend-app;
index index.html index.html;
}
修正後
# frontend-app
location /dashboard {
alias /var/www/html/project/laravel-react-redux-blog-boilerplate/frontend-app;
index index.html index.html;
try_files $uri $uri/ /dashboard//index.html;
}
オススメの構成や参考になりそうなリポジトリあったら教えてください〜
普段、フロントエンドはjQueryで開発しているのですが、最近の流行りに乗じてReactを使ってみることにしました。
LaravelならVue.jsにしとくのが無難かなと考えたのですが、Reactが今一番伸びがある(らしい)のでReactにしました。
AngularJSとで迷ったのですが、あくまでjQueryの代わりになるかつViewだけ担当するものを考えていたのでReactを選択しました。
それぞれのFWの技術的な利用価値について説明できるほどフロントエンドマンではないので、ぶっちゃけよくわかっていませんが・・・w
Reactのインストールは公式ではnpmが推奨?されているようですが、bowerの方が何となく親しみがあるので、今回はbowerでインストールします。(bowerよりnpmの方がパッケージが豊富??)
後日談:色々調べてみるとnpmのほうがスタンダードな感じがありますね。
公式サイトのチュートリアルやネットに転がっているソースを眺めたり写経するうちに何となくわかってくるかと思いますが、babelやjsx,browerifyなど最近のカオスなフロントエンド界隈事情を知っておくと良いでしょう。
bower install react --save
bower install babel --save
以下のファイルを使います。
※animationとか使いたい場合は、react-with-addons.jsをreact.jsの代わりに読み込んでください。
Reactを使う準備はこれだけです。
<!DOCTYPE html>
<html>
<head>
<script src="path/to/react.min.js"></script>
<script src="path/to/react-dom.min.js"></script>
<script src="path/to/browser.min.js"></script>
<script src="path/to/example.js" type="text/babel"></script>
</head>
<div id="example"></div>
ReactDOM.render(
<h1>Hello React Boy and Girl!</h1>
document.getElementId("example")
);
jsはbodyタグの終了前に記述してもOKです。
Reactの日本語情報はLaravelよりも少ないように感じました。(Laravelは今年に入って急激に増えた気がしますが。。)
ただ注目度は高いようなので今後に期待です。
Reactでrequire()を使用したい場合はbrowserifyやwebpackを使用します。 require()ってなに?
laravelにはデフォルトでbrowerserifyが組み込まれているのでそちらを使用するのが楽かと思いますが、環境に合わせて選択してください。
Laravelのエラーページを共通のテンプレートで対応する方法について説明します。
app/Exceptions/Handler.php
でrenderHttpException
メソッドをオーバーライドします。
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Auth\Access\AuthorizationException::class,
\Symfony\Component\HttpKernel\Exception\HttpException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Illuminate\Session\TokenMismatchException::class,
\Illuminate\Validation\ValidationException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Exception $exception
* @return void
*/
public function report(Exception $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
return parent::render($request, $exception);
}
/**
* Override default method - render the given HttpException.
*
* @param \Symfony\Component\HttpKernel\Exception\HttpException $e
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function renderHttpException(\Symfony\Component\HttpKernel\Exception\HttpException $e)
{
$status = $e->getStatusCode();
$errorMessages = $this->handleErrorMessages($status);
view()->replaceNamespace('errors', [
resource_path('views/errors'),
__DIR__.'/views',
]);
if (view()->exists("errors.index")) {
return response()->view("errors.index", ['errorMessages' => $errorMessages], $status); // viewを指定する
} else {
return $this->convertExceptionToResponse($e);
}
}
/**
* Handle error messages.
*
* @param int $status
* @return array $errorMessages
*/
private function handleErrorMessages($status)
{
$errorMessages['status'] = $status;
switch ($status) {
case '401':
return $errorMessages['message'] = 'Unauthorized';
break;
case '403':
return $errorMessages['message'] = 'forbidden';
break;
case '404':
$errorMessages['message'] = 'Not Found';
break;
case '500':
$errorMessages['message'] = 'Internal Server Error';
break;
case '503':
$errorMessages['message'] = 'Service Unavailable';
break;
}
return $errorMessages;
}
}
もっとキレイな書き方がある気がしますが、とりあえずこれで。
後は任意のviewファイルで変数を受け取って出力するだけです。
職人さんの朝は早い・・・php artisan make:controller HogeController --resource
職人が仕事するとこんなコントローラーをつくってくれます。
<?php
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
use App\\Http\\Requests;
class HogeController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \\Illuminate\\Http\\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \\Illuminate\\Http\\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \\Illuminate\\Http\\Request $request
* @return \\Illuminate\\Http\\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param int $id
* @return \\Illuminate\\Http\\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \\Illuminate\\Http\\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \\Illuminate\\Http\\Request $request
* @param int $id
* @return \\Illuminate\\Http\\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \\Illuminate\\Http\\Response
*/
public function destroy($id)
{
//
}
}
ではAPIを早速つくります。index()のところをいじります。
/**
* Display a listing of the resource.
*
* @return \\Illuminate\\Http\\Response
*/
public function index()
{
$user = \\Auth::user();
return \\Response::json($user);
}
Responseでjsonをかえすだけです(:3」∠)
//-------------------------------
// API
//-------------------------------
Route::group(['prefix' => 'api'], function () {
Route::resource('user', 'Resource\\UserAuthController');
});
※Laravel5.3からはrouteがディレクトリになってweb.phpとかapi.phpって感じにファイルが分かれていると思います。api.phpにかくのがベタだと思います。
/apiにアクセスするとjsonレスポンスが出力されていると思います。
apiを直接たたくようなスケベェな人を避けたい時や、APIを外部に公開したい時は認証を設けましょう。
ここではmiddlewareで認証を行う方法を例にあげたいと思います。
Route::group(['middleware' => 'auth.user'], function () {
Route::get('/userlist', 'UserList\\UserListController@getIndex');
//-------------------------------
// API
//-------------------------------
Route::group(['prefix' => 'api'], function () {
Route::resource('user', 'Resource\\UserAuthController');
});
});
AuthenticateOfApiとかいうAPI利用のためのミドルウェアを作ることにします。
一部Laravelエキスパート養成読本を参考にさせて頂きました。
<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
class AuthenticateOfApi
{
/**
* @var string
*/
const APPLICATION_TOKEN = 'x-application-token';
/**
* API Authenticate
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
// Cookie認証の場合
if (真偽値を返すログインチェック) {
// ユーザー情報を返したり、ほげほげ、、、
}
// API Token認証の場合
if (ヘッダにx-application-tokenが含まれているか判定) {
// ユーザー情報を返したり、ほげほげ、、、
}
if (ログインしていない、x-application-tokenもない) {
return abort(401);
}
return $next($request);
}
}
内部でAPIを利用したい時にAPITokenをヘッダに含めてしまうと認証の意味がなくなってしまいます。(ユーザーにヘッダが丸見えなのでトークンをパクられたら誰でもAPIが利用できてしまいます。)
従って内部でAPIを利用する際はログインと同じ認証方法をとると良いかと思います。Auth::guard('users')->check()
とかでログインチェック!
外部からのAPIの利用については、JavaScriptでヘッダにトークンを入れてPOSTすることで認証させることができます。
※APIの認証については他の記事をご参照ください。
LaravelでAPIをつくる・使うのは簡単ですが、API設計とやらが中々奥深そうです。
自分でつくったAPIを自分で使うというのは結構楽しいのでちょっと頑張ってみます。
今回は基本的なイベントとリスナーの定義の仕方についてはすっ飛ばし、一つのリスナークラスで複数のイベントを設定できるイベント購読について扱います。
まずはイベントを定義しましょう。
例として“ユーザー登録完了時”というイベントを作成することにします。
イベントクラスはartisanコマンドで生成することができます。
php artisan make:event UserRegistrationComplete
<?php
namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class UserRegistrationComplete extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the channels the event should be broadcast on.
*
* @return array
*/
public function broadcastOn()
{
return [];
}
}
artisanコマンドを実行するとこのようなクラスが自動生成されます。
コンストラクタの中にはイベントで使用するデータを設定しておきましょう。
今回はユーザー登録に関わるイベントなので、Userモデルを呼び出しておくことにします。ここで呼び出したデータはリスナークラスで使用することができます。
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}
ちなみにbroadcastOnというのは、リアルタイムに実行したいユーザーインターフェースを実装したい時に使用するものなので今回はスルーです。
次にリスナーを定義しましょう。
リスナーもartisanコマンドで生成することができます。
php artisan make:listener UserAuthEventListenerListener --event UserRegistrationComplete
リスナーを生成するときは、eventオプションで結び付けたいイベントを設定することができます。
<?php
namespace App\Listeners;
use App\Events\UserRegistrationComplete;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class UserAuthEventListenerListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param UserRegistrationComplete $event
* @return void
*/
public function handle(UserRegistrationComplete $event)
{
//
}
}
イベント購読は、一つのリスナークラスに複数のイベントを設定できるので、リスナー名は複数あるイベントのカテゴリー名のような形で命名すると良いでしょう。
今回の例でいくと、“ユーザー登録完了イベントはユーザー認証のグループに属す”といったところでしょうか。
生成したリスナーでイベント発火時の処理とイベント購読の登録を行います。
<?php
namespace App\Listeners;
class UserAuthEventListener
{
// イベント発火時の処理
public function onConfirm($event)
{
// 処理
}
// 複数追加できます
public function onHogeHoge($event)
{
// 処理
}
// イベント購読の登録
public function subscribe($events)
{
$events->listen(
'App\Events\UserRegistrationComplete',
'App\Listeners\UserAuthEventListener@onConfirm'
);
}
// 複数登録できます
public function subscribe($events)
{
$events->listen(
'App\Events\UserHogeHoge',
'App\Listeners\UserAuthEventListener@onHogeHoge'
);
}
}
イベント追加したい場合は、先ほどのartisanコマンドでイベントを生成してやればOKです。
登録ばっかりでややこしいかと思いますが、これで最後です。
app\Providersにデフォルトで存在しているEventServiceProviderを使用します。
サービスプロバイダというのはアプリケーションの初期起動処理を行うクラスです。詳しくはドキュメント
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
// ここにリスナーを登録していく
protected $subscribe = [
'App\Listeners\UserAuthEventListener',
'App\Listeners\HogeHogeListener',
];
}
さて、イベント購読の登録が完了したら後は自由にイベントを発火させましょう。
イベントを発火させるには、Eventファサードとfireメソッドを使います。
fireメソッドにはイベントのインスタンスを渡してあげます。
/**
* コントローラーのとあるメソッド
*/
private function hogehoge(User $user)
{
// 登録確認イベントの発火
\Event::fire(new UserAuthRegistrationComplete($user));
}
こんな感じで登録したイベントが火を吹きます :fire:
イベント定義(データ保持)→リスナー定義(イベントで使用するメソッド管理とsubscribeメソッド実装)→イベント購読登録→:fire:
Laravel5.3からNotificationというパッケージが導入されたので今回のような通知の管理はNotificationを利用した方が楽かもしれません。
そういった意図があるのか、5.3ではEventやListener, Jobとったディレクトリがデフォルトでは存在しなくなりました。
気になる方はLaravelのリポジトリやドキュメントをご覧ください。
LaravelでCORS(Cross-Origin Resource Sharing)に対応する方法をまとめました。
クライアントサイドはReact, axiosを使用します。
前提知識としては、CORSのリクエスト形態、シンプルなリクエスト方法と、preflightを使用するリクエスト方法の違いを抑えておけば良いかと思います。
RESTfulAPIの場合は基本的にはpreflightを使用するリクエスト形式かと思います。
この記事では、preflightを使用するリクエストに対応する例を取り上げます。
CORSなので当たり前ですが、apiとwebでドメインを用意しています。
api.hogehogedomain
とadmin.hogehogedomain
みたいな感じでドメインが用意されていて、adminの方から別ドメインで管理されているAPIをコールする、といった感じです。
APIを提供している側、Laravel側でAPIリクエスト時にヘッダ情報を調整するmiddlewareを用意します。
と、ここで自作のミドルウェアを作成したかったのですが、何故か更新系のメソッドだけ上手く動作しなかったので、barryvdh/laravel-corsを使うことにします。
セットアップはREADME通りです。
composer require barryvdh/laravel-cors
config/app.php
のprovider配列に以下を指定Barryvdh\Cors\ServiceProvider::class,
app/Http/Kernel.php
のapiミドルウェアグループにcorsミドルウェアを設定
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
\Barryvdh\Cors\HandleCors::class, // <-Here!
],
];
設定ファイルをパブリッシュして編集。php artisan vendor:publish --provider="Barryvdh\Cors\ServiceProvider"
config/cors.php
return [
/*
|--------------------------------------------------------------------------
| Laravel CORS
|--------------------------------------------------------------------------
|
| allowedOrigins, allowedHeaders and allowedMethods can be set to array('*')
| to accept any value.
|
*/
'supportsCredentials' => true, // change false to true !
'allowedOrigins' => ['*'],
'allowedHeaders' => ['Content-Type', 'X-Requested-With'],
'allowedMethods' => ['*'], // ex: ['GET', 'POST', 'PUT', 'DELETE']
'exposedHeaders' => [],
'maxAge' => 0,
]
クッキーの送信およびBasic認証の許可しておきたいので、 supportsCredentials
をtrue
にしておきます。
サーバー側の設定は以上です。
axiosでヘッダ情報の定義をしておきます。
action/index.js
const api = axios.create({
baseURL: 'http://api.rubel/v1',
timeout: 10000,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
export function createCategory(props) {
const request = api.post(`/categories`, props);
return {type: CREATE_CATEGORY, payload: request};
}
クライアント側では、X-Requested-With
ヘッダをセットするだけで、後は普通にapiを叩くだけです。
自作ミドルウェアがなぜ上手くいかなかったのか解決できていないので消化不良ですが、一旦はこれで問題ないでしょう。。
ヘッダにTokenを含める方法もありますが、ハードコーディング感があるので、VerifyCsrfToken.phpで該当URLを除外する方法がスマートだと思うのでそちらを記載します。
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
'api/*'
];
}
ワイルドカードも使えちゃいます。
この手の記事が多々あるのでそれにまんまと引っかかった自分を殴り飛ばしたいです。。。
]]>Laravel5.4でsqliteの使ってテストをかく準備をします。
以下3行を追加します。 sqliteのインメモリ機能を使います。
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
<env name="DB_CONNECTION" value="sqlite"/> // Add
<env name="DB_DATABASE" value=":memory:"/> // Add
<env name="ADMIN_DOMAIN" value="localhost"/> // Add
</php>
ここは良しなに準備してください。略。
<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
デフォルトでphpunit.xmlにtestsディレクトリ以下の〇〇Test.phpを実行するという設定になっているので、適当にテストファイルを用意します。
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class PostTest extends TestCase
{
use DatabaseMigrations; // Run migration
public function testIndex()
{
$user = factory(App\User::class)->create();
var_dump($user->first()); // Check the data
// Here is your tests...
}
}
DatabaseMigrations
というトレイトを設定すると、テスト実行の度にマイグレーションが実行されます。
./vendor/bin/phpunit
というテストを実行する時のコマンドをcomposerのscriptsに記述しておくと楽できます。
これでテストがかけるマン。
]]>php artisan make:command Repository
/CommandsにRespository.phpというコマンド用のファイルが生成されます。
Repository.phpを編集します。
handleメソッド部分はCreating file using Artisan Command in Laravel 5.1のコードをお借りして、少しカスタマイズしました。(偶然同じことをやろうとしている方がいたので・・)
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class Repository extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'make:repository {modelName : The name of the model}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create respository files.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$modelName = $this->argument('modelName');
if ($modelName === '' || is_null($modelName) || empty($modelName)) {
$this->error('Model name invalid..!');
}
if (! file_exists('app/Http/Repositories/Contracts') && ! file_exists('app/Http/Repositories/Eloquent')) {
mkdir('app/Http/Repositories/Contracts', 0775, true);
mkdir('app/Http/Repositories/Eloquent', 0775, true);
$contractFileName = 'app/Http/Repositories/Contracts/' . $modelName . 'RepositoryContract.php';
$eloquentFileName = 'app/Http/Repositories/Eloquent/' . $modelName . 'Repository.php';
if(! file_exists($contractFileName) && ! file_exists($eloquentFileName)) {
$contractFileContent = "<?php\n\nnamespace App\\Http\\Repositories\\Contracts;\n\ninterface " . $modelName . "RepositoryContract\n{\n}";
file_put_contents($contractFileName, $contractFileContent);
$eloquentFileContent = "<?php\n\nnamespace App\\Http\\Repositories\\Eloquent;\n\nuse App\\Repositories\\Contracts\\".$modelName."RepositoryContract;\n\nclass " . $modelName . "Repository implements " . $modelName . "RepositoryContract\n{\n}";
file_put_contents($eloquentFileName, $eloquentFileContent);
$this->info('Repository files created successfully.');
} else {
$this->error('Repository files already exists.');
}
}
}
}
RepositoryコマンドをKernel.phpに登録します。
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class Repository extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'make:repository {modelName : The name of the model}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create repository files.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$modelName = $this->argument('modelName');
$contractFileName = 'app/Repositories/Contracts/' . $modelName . 'RepositoryContract.php';
$eloquentFileName = 'app/Repositories/Eloquent/' . $modelName . 'Repository.php';
if ($modelName === '' || is_null($modelName) || empty($modelName)) {
$this->error('Model name invalid..!');
}
if (! file_exists('app/Repositories/Contracts') && ! file_exists('app/Repositories/Eloquent')) {
mkdir('app/Repositories/Contracts', 0775, true);
mkdir('app/Repositories/Eloquent', 0775, true);
$this->createFiles($modelName, $contractFileName, $eloquentFileName);
} else {
$this->createFiles($modelName, $contractFileName, $eloquentFileName);
}
}
public function createFiles($modelName, $contractFileName, $eloquentFileName)
{
if(! file_exists($contractFileName) && ! file_exists($eloquentFileName)) {
$contractFileContent = "<?php\n\nnamespace App\\Repositories\\Contracts;\n\ninterface " . $modelName . "RepositoryContract\n{\n}";
file_put_contents($contractFileName, $contractFileContent);
$eloquentFileContent = "<?php\n\nnamespace App\\Repositories\\Eloquent;\n\nuse App\\Repositories\\Contracts\\".$modelName."RepositoryContract;\n\nclass " . $modelName . "Repository implements " . $modelName . "RepositoryContract\n{\n}";
file_put_contents($eloquentFileName, $eloquentFileContent);
$this->info('Repository files created successfully.');
} else {
$this->error('Repository files already exists.');
}
}
}
php artisan make:repository Hoge
Repositories
├── Contracts
│ └── HogeRepositoryContract.php
└── Eloquent
└── HogeRepository.php
こんな感じでファイルが生成されるかと思います。
# 所感
今回はRepositoryパターンの実装用のコマンドを作成しましたが、これを応用して色々なコマンドが作れそうですね。
今回つくったコマンドは、現在開発しているプロダクトに導入しています。
Notificationとはナンゾヤ?
Laravel.com - Notificaions
Laravel Notification Channel
Laravel5.3以前では通知の管理などはEventとListenerを活用することで何とかゴニョゴニョやっていたかと思います。
それがNotificationによって、より便利に通知管理を行えるようになりました。
※名前の通り、通知に特化したものなのでEventやListenerの"代用"というわけではないかと思います。
ではでは、インストールします。composer require laravel-notification-channels/backport
Laravel5.3の場合はデフォルトで組み込まれているため、composerでわざわざインストールする必要はありませんが、〜5.2はマニュアルです。
なのでfacadeを使いたい場合はfacadeのクラスを追加してあげます。
※FacadeでNotification使わないよーという場合は飛ばしてください。
vendor/laravel/framework/src/Illuminate/Support/FacedesにNotification.phpを作成して、以下を記述。
<?php
namespace Illuminate\Support\Facades;
/**
* @see \Illuminate\Notifications\ChannelManager
*/
class Notification extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'Illuminate\Notifications\ChannelManager';
}
}
それから、app.phpのprovidersとaliasに以下をそれぞれ追記。
Illuminate\Notifications\NotificationServiceProvider::class
'Notification' => Illuminate\Support\Facades\Notification::class
ここまでで下準備OKです。
まずは通知クラスを職人さんで作成しましょう。
例としてユーザーが登録されたときの通知を管理するクラスを作成することにします。
php aritsan make:notification UserRegistered
Notificationディレクトリが自動生成されて、その中にUserRegistered.phpが生成されていると思います。
生成を確認したら、通知の際に利用したいモデルに名前空間を追加します。
ここではUser.phpを使用することにします。
User.phpuse Illuminate\Notifications\Notifiable;
クラスに以下を追加。use Notifiable;
参考までに↓
<?php
use Hogehoge\hogehoge
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable
public fuction hoge()
{
//
}
先ほど生成したUserRegistered.phpのtoMailメソッド内をいじります。いじらなくともデフォルトのメールテンプレートが送信できますので飛ばしてもOKです。
public function toMail($notifiable)
{
$user_name = $notifiable->name;
$token = $notifiable->confirmation_token;
$url = url('/home');
return (new MailMessage)
->view('path/to/mailTemplate')
->subject('ユーザー登録完了通知')
->line("{$user_name}さん登録ありがとう!")
->action('homeにもどる', "$url")
->line('今後ともよろしくでござる');
}
メソッドについてはNotificationのAPIをご覧ください。
メールテンプレート参考
@foreach ($introLines as $line)
<p>
{{ $line }}
</p>
@endforeach
<p>
下記にアクセスするとhome画面にもどるでござる。
</p>
<a href="{{ $actionUrl }}" target="_blank">
{{ $actionText }}
</a>
@foreach ($outroLines as $line)
<p>
{{ $line }}
</p>
@endforeach
SimpleMessageとかいう仕様に従った形なのですが、ちょっとだけわかりづらい気がするでござる。
ここまで完了したら後はコントローラで通知クラスを呼び出すだけです。
名前空間とFacedeを使って呼び出すことにします。Facedeを使わない場合はドキュメントをご参考に。
HogehogeController.php
<?php
namespace App\Http\Controllers\Hogehoge;
use Hoge\Hogehoge;
use App\Models\User;
use App\Notifications\UserRegistered;
class HogehogeController extends Controller
{
public function hoge()
{
$user = new User();
\Notification::send($user, new UserRegistered());
}
}
こんな感じでNotificationを使うことができます。
便利さが伝わったでしょうか・・・?
EventとListenerでSubscriberを使ってゴニョゴニョするよりは遥かに楽になったと思います。
通知だけならNotificationを利用した方が便利そうですね(:3」∠)
package.jsonに以下を追記、または書き換え。
"laravel-elixir": "^6.0.0-9",
"laravel-elixir-browserify-official": "^0.1.3",
"laravel-elixir-webpack-official": "^1.0.2"
npm install
どのバージョンからかは失念しましたが、おそらくlaravel5.3からbrowerifyの扱いが変わるようで、個別にインストールしてあげないとbrowserifyを使えないそうです。
laravel-elixirだけアップデートしてgulp走らせるとbrowerifyのタスクがある場合は怒られるので直ぐに気づくとは思いますが。。。
AjaxライブラリとしてSuperagentを採用しているのは、jQueryから脱却したいのと、jQueryのAjaxよりも分かりやすかったからです。
プロミスとかいう難しい概念があるらしいですが、それは横に置いておいてもとりあえずは使えそうです。
Web標準の観点からするとFetchAPIがイケてるらしいのですが、各ブラウザベンダーの実装にばらつきがあるようなので避けました。
フロントエンドってつくづくカオスだなーとボヤキつつも話を進めていきたいと想います。
順番テキトーですが、ご了承ください。。。
Route::group(['prefix' => 'api/v1'], function () {
Route::get('/api/user', 'HogeController@index');
Route::post('/api/user', 'HogeController@update');
});
色々省略しちゃいます。
こんな感じでコンポーネント召喚しますよーという体だけです。
<div id="form-component" class="mdl-cell mdl-cell--12-col"></div>
実際はResouceControllerでAPIつくって、Restな感じに仕立てているのですが、詳しい作り方は省きます。
<?php
// NameSpace
class ConfigController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
// Jsonを返すAPIを用意
$users = \Auth::user();
return \Response::json($users);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(ConfigRequest $request)
{
// Update処理例
$user_name = \Auth::guard('users')->user()->name;
$users = User::where('name', $user_name)->first();
$users->fill(\Input::all())->save();
// 配列つくってJsonにポイー
$response['status'] = 'success';
$response["message"] = ['入力に問題ありません!'];
return \Response::json($response, '200');
}
}
<?php
namespace App\Http\Requests\User;
use App\Http\Requests\Request;
class HogeRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
if ($this->form_type == 'name') {
return [
'user_name' => 'max:5|required',
'email' => 'email|required'
];
}
// デフォルト(nullの時)
return [];
}
//
public function response(array $errors)
{
$response['status'] = 'error';
$response['message'] = $errors;
return \Response::json($response, 200);
}
}
FormReqeustでエラーをJsonで返す方法ですが、Illuminate/Foundation/Http/FormRequestのresponseメソッドをオーバーライドしてあげるだけです。
それで使い方はいつものFormRequestと同じです。
エラーがあればJsonResponseでエラーメッセージを返してくれます。
VerifyCsrfToken.phpで設定を忘れずに済ませておきます。
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
// ワイルドカード使えます。
'api/*'
];
}
Laravel側はこれで完了です。
// strictモードは雰囲気だけです。。。
"use strict";
var request = require('superagent');
var ConfigNameForm = React.createClass({
getInitialState: function () {
return {
// フォームの値
data: {
user_name: '',
email: '',
},
// メッセージ
message: {
// 入力エラーがない場合はコントローラーのレスポンスが、ある場合はフォームリクエストのレスポンスがそれぞれ代入されます。(もし単純にしたい場合はフォームリクエストのバリデーションをやめてコントローラーでバリデーションロジックを組んだ方がいいかもしれないです。)
user_name: '',
email: ''
}
}
},
// API-GET
componentDidMount: function () {
request
.get('/api/user')
.set('Content-Type', 'application/json')
.end(function(err, res){
if (err) {
alert('通信エラーです。リロードしてください。');
}
this.setState({
data: {
user_name: res.body.user_name,
email: res.body.email
}
});
}.bind(this));
},
handleChange: function (event) {
var data = this.state.data;
switch(event.target.name) {
case 'user_name':
data.user_name = event.target.value;
break;
case 'email':
data.email = event.target.value;
break;
}
this.setState({
data: data
});
},
// API-POST
handleSubmit: function () {
request
.post('/api/user')
.set('Content-Type', 'application/json')
.send({
user_name: this.state.data.user_name,
email: this.state.data.email
})
.end(function(err, res){
if (res.ok) {
var message = this.state.message;
switch (res.body.status) {
case 'success':
// ここは野暮ったいですが、適当に調整してください。
message.user_name = res.body.message;
message.email = res.body.message;
break;
case 'error':
message.user_name = res.body.message.user_name;
message.email = res.body.message.email;
break;
}
this.setState({
message: message;
});
} else {
alert('通信エラーです。もう一度お試しください。')
}
}.bind(this));
},
render: function () {
// 野暮ったい。。.
var msgOfName = false;
if (this.state.message.name.length > 0) {
var msgOfName = this.state.message.name.map(function (msg) {
return (
<p key={msg}>{msg}</p>
);
});
}
var msgOfEmail = false;
if (this.state.message.email.length > 0) {
var msgOfEmail = this.state.message.email.map(function (msg) {
return (
<p key={msg}>{msg}</p>
);
});
}
return (
<div>
{/* Message */}
{msgOfName}
{msgOfEmail}
{/* Form */}
<form action="javascript:void(0)" method="POST" onSubmit={this.handleSubmit}>
{/* Name */}
<label htmlFor="user_name">名前</label>
<input type="text" name="user_name" id="user_name" value={this.state.data.user_name} onChange={this.handleChange} disabled />
{/* Email */}
<label htmlFor="email">メールアドレス </label>
<input type="text" name="email" id="email" value={this.state.data.email} onChange={this.handleChange} />
<button type="submit">更新</button>
</form>
</div>
);
}
});
ReactDOM.render(
<FormApp />,
document.getElementById('form-compoent')
);
結構雑につくったので手直しすべきところは多そうです。
アーキテクチャも大事ですが、モダンなJavaScriptの書き方はもっと勉強して柔軟にかけるようにすべきだと思いました。
ES5からES6で書き方が色々変わるのでその辺の改修がちょっと面倒でしたが、さほど難しいことはないので気負いしなくとも良さそうです。
npm i react react-dom -D
elixir(function(mix) {
mix
.browserify('hoge.js', 'hogehoge.js')
});
こちらがとても参考になります。
ES5のReact.jsソースをES6ベースに書き換える
トランスパイラを利用。babelとか。
ただのメモ書きでしたφ(..)
]]>データの操作に関連するロジックをビジネスロジックから切り離し、抽象化したレイヤに任せることで保守や拡張性を高めるパターンです。
(必ずしもDB操作のロジックのみを留めるパターンというわけではないそうです。)
Laravelにリポジトリパターンを取り入れることで、
といったメリットを得ることができます。
Modelと同じ単位でRepositoryディレクトリを作成します。(賛否両論あるかもです)
今回は以下のような構成でリポジトリパターンを実装していきます。
.
├── Models
│ ├── User.php
│
├── Repositories
└── User
├── UserRepository.php
└── UserRepositoryInterface.php
まずはインターフェースを設計します。
<?php
namespace App\Repositories\User;
interface UserRepositoryInterface
{
/**
* Nameで1レコードを取得
*
* @var string $name
* @return object
*/
public function getFirstRecordByName($name);
}
続いて実装クラスを用意します。
ここでは対応するモデルのDIとメソッドの実装を行います。
<?php
namespace App\Repositories\User;
use App\Models\User;
class UserRepository implements UserRepositoryInterface
{
protected $user;
/**
* @param object $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* 名前で1レコードを取得
*
* @var $name
* @return object
*/
public function getFirstRecordByName($name)
{
return $this->user->where('name', '=', $name)->first();
}
}
ここから更にService層を用意してクラスを追加し、抽象度を高める場合もあるようですが、今回はこの2つのクラスのみで実装していくことにします。
AppServiceProvider.phpにインターフェースと実装クラスを登録します。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
// User
$this->app->bind(
\App\Repositories\User\UserRepositoryInterface::class,
\App\Repositories\User\UserRepository::class
);
}
}
実装したリポジトリパターンを使用します。
<?php
namespace App\Http\Controller\User;
use App\Repositories\User\UserRepositoryInterface;
class UserController extends Controller
{
public function __construct(UserRepositoryInterface $user_repository)
{
$this->user_repository = $user_repository;
}
public function index()
{
return $this->user_repository->getFirstRecordByName($name);
}
}
インターフェースをインジェクションするだけです!
モデルもコントローラーもすっきりしました。
これを機にDDDの勉強もしたいです。
デフォルトのブックマークが非表示で、Bookolio(ブクマを見やすくするやつ)とかいうプラグインを使っているニッチな人だと多少便利なプラグインかもしれません←自分
プラグインの種類は色々ありますが、今回つくるのはこれです↓
【プラグインの画像】
プラグインのアイコンを押すと、Googleの英語版を新規タブで開いてくれるだけの超単純な機能です。
超単純なだけに伸びしろのある仕様ですね()
先にフォルダとファイルを作っておきます。
└── search_by_english
├── background.js
├── icons
│ ├── icon128.png
│ ├── icon16.png
│ └── icon48.png
└── manifest.json
※アイコンは適宜用意してください。
background.jsというのはbackgroundで動作するJavaScriptです()
詳しくはDeveloper's Guideをご覧ください。
manifest.json
{
"name": "Open A Google English Edition In A New Tab",
"version": "1.0",
"manifest_version": 2,
"description": "Open a Google English Edition in a new tab.",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"browser_action": {
"default_icon": "icons/icon48.png"
},
"background": {
"scripts": [
"background.js"
]
}
}
プラグインの種類によって記述が変わります。これといって難しいものではないので詳細はDeveloper's Guideをご覧ください。
background.js
コードはChrome extension: open link in new tab?を参考にさせて頂きました。
見ると何となくわかるかと思います。
詳しくはドキュメン(ry Developer's Guide
chrome.browserAction.onClicked.addListener(function(activeTab){
var newURL = "https://www.google.co.jp/?hl=en&gws_rd=cr&ei=O2OgV4jODcS30gSs1Ihw";
chrome.tabs.create({ url: newURL });
});
基本はjavascriptでホイホイかくわけですが、ブラウザの動作等に関してGoogleが用意するAPIを活用していく感じでしょうか。
Atomのプラグイン開発とかもやってみたいです。
.bashrcにgitのショートカットコマンドをつくったでgitのエイリアスコマンドをつくったのですが、中途半端だったので改良しました。
前回のエイリアスでもそこそこにgitコマンドが快適になりましたが、gitコマンドを叩く度にブランチ名をタイプしないといけない仕様は改善すべき点だと思ったので、select
を使って解決しました。
git branch
の値をselect
で回せばいいと思っていたのですが、ブランチ名だけではなくファイル名とか取得されてしまうので加工する必要がありました。
ちょうど同じようなことを実践している記事があり、そちらを参考にさせていただきました。
作ったコマンドは
です。
#!/bin/sh
#checkout a local branch
function gitCheckoutLocalBranch() {
branches=`git branch | grep -v -e"^\*" | tr -d ' '`
PS3="Select branch > "
echo 'Branch list:'
select branch in ${branches}
do
stty erase ^H
git checkout ${branch}
break
done
}
alias g-c=gitCheckoutLocalBranch
# create a new branch and checktout a remote branch
function gitCreateAndCheckoutRemoteBranch() {
branches=`git branch -r | grep -v -e"^\*" | tr -d ' '`
PS3="Select branch > "
echo 'Branch list:'
select branch in ${branches}
do
stty erase ^H
echo -n "What is the new branch name?"
read new_branch_name
git checkout -b ${new_branch_name} ${branch}
break
done
}
alias g-c-b-r=gitCreateAndCheckoutRemoteBranch
# delete a local branch
function gitDeleteLocalBranch() {
branches=`git branch | grep -v -e"^\*" | tr -d ' '`
PS3="Select branch > "
echo 'Branch list:'
select branch in ${branches}
do
stty erase ^H
git branch -D ${branch}
break
done
}
alias g-b-d=gitDeleteLocalBranch
select
で出力される選択肢の色を変えたかったのですが、ちょっとわからなかったので後回しにしました。
今回のソースはgithub - bmf-san/my-scriptsにおいてあります。
#参考
]]>案外同じことを考えている人がいたようで、リファレンス漁るよりも先に結果が出ました。
Multiple Forms, Multiple Requests?
FormRequestのrulesメソッド内でゴニョゴニョします。
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
if ($this->hogehoge) {
return [
'alias_name' => 'max:50|required|unique:users',
];
}
if ($this->mogemoge) {
return [
'self_introduction' => 'max:200'
];
}
// デフォルト
return [];
}
hogehoge、mogemogeのところはそれぞれリクエストに渡される値です。(なんといえばいいのでしょうか汗)
$request->hogeって感じでリクエストの値を取得できますが、その$requestがフレームワークの実装でごにょごにょされて、$thisに代わったといった感じです。(代わったというのはかなり語弊がある気がしますが、裏側の実装を見ていないのでボキャ貧でごめんなさいということにしてください。。。)
1番最後のreturn []
はnullがリクエストで渡ってきた時のためです。
これがないとnullの時エラーになります。
特にありませぬ。
]]>export 文は、指定したファイル (またはモジュール) から関数、オブジェクト、プリミティブをエクスポートするために使用 引用元:MDN - Export
ここでいうエクスポートとは、何かを定義するという意味合いに近いかと思います。
エクスポートには2種類の方法があります。
export { hogeFunction }; // 宣言済みの関数をエクスポート
export const hoge = 1; // 定数をエクスポート letやvarも可。
fromを使ってエクスポートすることもできます。
export * from 'Hoge'; // ワイルドカード
export { hoge, moge, huge } from 'hogemogehuge'; // 複数エクスポート
export { importHoge as hoge, importMoge as moge } from 'hogemoge'; // エイリアス
export default function() {}
export default class() {}
defaultとは、 「importする際に特に指定がなければそのクラスや関数を呼ぶ」というものです。
importする際にdefault以外のクラスや関数を呼び出したいときは、{}でクラスやファイル名を指定して呼び出してあげます。
import 文 は、外部モジュールや他のスクリプトなどからエクスポートされた関数、オブジェクト、プリミティブをインポートするために使用します。 引用元:MDN - Import
インポートにも2種類の方法があります。
### 名前指定のインポート
import * as hoge from "Hoge"; // ワイルドカード
import {hoge} from "Hoge"; // 指定の1つだけをインポート
import {hoge, moge} from "HogeMoge"; // 複数インポート
import {hogeHoge as aliasHoge} from "HogeHoge"; // ※1
import {hogeHoge as aliasHoge, mogeMoge as aliasMoge} from "MogeMoge"; // ※2
import "Hoge"; // 全モジュールをインポート
// ※1 全モジュールをインポートし、さらに一部のメンバーを指定する。
// ※2 メンバー名を指定してインポートする。
厳密にはスコープの話が関わってきますが、そちらについては参考サイトをご確認ください。
import hoge from "Hoge"; // defaultのメンバーが呼び出される。
デフォルト指定されたメンバーを名前指定でインポートするとエラーになります。
モダンなjavascriptはまだまだキャッチアップしきれていない気がするので勉強しなくては・・(´・ω・`)
.env
ファイルに用意された環境変数をクライアントサイドでも利用したい時に便利なライブラリです。
npm install dotenv-webpack --save-dev
webpack.config.js
にpluginとして設定します。
const Dotenv = require('dotenv-webpack');
module.exports = [
~~~ゴニョゴニョゴニョ~~~
{
plugins: [new Dotenv({
path: 'path/to/.env',
safe: false
})]
}
~~~ゴニョゴニョゴニョ~~~
];
path
は.env
ファイルへのパス、safe
は.env_example
を読み込むか否かを設定します。
DOMAIN=hereisyourdomain
config.log(process.env.DOMAIN) // hereisyourdomain
便利だけどセキュリティ的なところは問題ないのだろうか?
zabbixを導入しようと色々試行錯誤していたら以下のようなエラーがでてyumが使えなくなりました。
http://mirror.centos.org/centos/6/SCL/x86_64/repodata/repomd.xml: [Errno 14] PYCURL ERROR 22 - "The requested URL returned error: 404 Not Found"
他のミラーを試します。
エラー: Cannot retrieve repository metadata (repomd.xml) for repository: scl. Please verify its path and try again
こうなってしまってはOSに疎い私は絶望しましたが、以下のリンクが参考になり、解決に至ったのでシェアします。
[tips][Linux]旧バージョンCentOSでyum更新できなくなった時
ちなみにzabbixはphpの設定周りで面倒なことになって結局インストールできていません。。。
サーバー監視ツールとかプロファイラとかサーバーにインストールして使う系のプログラムはインストールの段階で敷居が高いです。。。
]]>cd hogehoge
とかパスやらディレクトリやらタイプするのが面倒になるという怠惰っぷりを発揮してきたので、シェルスクリプトを使って少し楽できるようにしました。
#!/bin/sh
# cd by selecting numbers
function cdSelect() {
dirs=`ls -a`
PS3="Select directory > "
echo 'Directory list:'
select dir in ${dirs}
do
stty erase ^H
cd ${dir}
break
done
}
alias cd-s=cdSelect
cd-s
と打つと、
Directory list:
1) .
2) ..
3) hoge_a
4) hoge_b
5) hoge_c
Select directory > 3
こんな感じになります。
ディレクトリが多い時大変そうですが、cdコマンドのストレスが軽減されました。
vimバージョンもつくろうかと。
bitflyerが用意しているAPIをちょっと使ってみたかったので資産状況を返すAPIをたたいてみました。
nodejsでAPIをたたきます。
レスポンスデータは恥ずかしいので見せません。////
なおソースコードの大部分はドキュメントの例を参考にしています()
var request = require('request');
var crypto = require('crypto');
var key = 'your_bitflyer_api';
var secret = 'your_bitflyer_secret';
var timestamp = Date.now().toString();
var method = 'GET';
var path = '/v1/me/getbalance';
var text = timestamp + method + path;
var sign = crypto.createHmac('sha256', secret).update(text).digest('hex');
var options = {
url: 'https://api.bitflyer.jp' + path,
method: method,
headers: {
'ACCESS-KEY': key,
'ACCESS-TIMESTAMP': timestamp,
'ACCESS-SIGN': sign,
'Content-Type': 'application/json'
}
};
request(options, function (err, response, payload) {
var hoge = 'hoge';
var data = JSON.stringify({"text": payload, "username": "your_bot_name", "icon_url": "your_icon_url","channel": "#channel_name"});
var options = {
url: 'your_slack_webhookurl',
form: 'payload=' + data,
json: true
};
request.post(options, function(error, response, body){
if (!error && response.statusCode == 200) {
console.log(body.name);
} else {
console.log('error: '+ response.statusCode + body);
}
});
});
jsonレスポンスは好きなように整形してください。(怠惰)
ソースおいてあります。bmf-san/bitflyer-private-api-and-slack-api-sample
WebSocketとか組み合わせてリアルタイムなアプリケーションを構築してみたいのですが、WebSocketの実装がどうにも腰が上がりません。。。
]]>APIを複数叩く必要があったのでasyncを使いました。
エラー拾っているところは参考サイト(失念しました)を真似ています。
なんだか見通しの悪いコードになってしまいました・・・
var request = require('request');
var crypto = require('crypto');
var async = require('async');
var key = 'YOUR_KEY';
var secret = 'YOUR_SECRET';
var timestamp = Date.now().toString();
var sign = crypto.createHmac('sha256', secret).update(timestamp + 'GET' + '/v1/me/getbalance').digest('hex');
var requests = [{
url: 'https://api.bitflyer.jp/v1/me/getbalance',
headers: {
'ACCESS-KEY': key,
'ACCESS-TIMESTAMP': timestamp,
'ACCESS-SIGN': sign,
'Content-Type': 'application/json'
}
}, {
url: 'https://api.bitflyer.jp/v1/getexecutions'
}];
async.map(requests, function(obj, callback) {
request(obj, function(error, response, body) {
if (!error && response.statusCode == 200) {
var body = JSON.parse(body);
callback(null, body);
} else {
callback(error || response.statusCode);
}
});
}, function(err, results) {
if (err) {
console.log(err);
} else {
var jpy_available = parseInt(results[0][0]['available']);
var btc_available = results[0][1]['available'];
var price = parseInt(results[1][1]['price']);
var total_assets = Math.floor(btc_available*price);
var data = JSON.stringify({
"attachments": [{
"fallback": "bitflyer資産情報",
"color": "danger",
"title": "bitflyer資産情報",
"text": "現在保有しているBTCは" + btc_available + "です",
"fields": [{
"title": "BTC総資産",
"value": total_assets,
"short": true
}, {
"title": "JPY(残高)",
"value": jpy_available + '円',
"short": true
}]
}],
"username": "your-bot-name",
"icon_url": "/path/to/img",
"channel": "#yourslackchannel"
});
var options = {
url: 'https://hooks.slack.com/services/WEBHOOK_TOKEN',
form: 'payload=' + data,
json: true
};
request.post(options, function(error, response, body) {
if (!error && response.statusCode == 200) {
console.log(body.name);
} else {
console.log('error: ' + response.statusCode + body);
}
});
}
});
総資産は所有するビットコイン×直近の取引価格(円)
で計算しているのですが、総資産って一発データで取れないんですかね・・?
浮動小数部分の数値の計算が雑すぎるためか、誤差が生じていますw
なのでその辺しっかりやりたいです。。。。
AWS(Elasticbeanstalk)で立ち上げたインスタンス(m4)のモニタリングをしていたら、レイテンシーがやたら高く、1分に一回くらいの頻度でタイムアウトしているユーザーがいるような状況でした。(アベレージは5秒くらいだった・・かな)
アプリケーション側にネックがあるのかなぁと思ったのですが、以前テストで立ち上げたインスタンスの環境(ほぼほぼ同じ環境)よりも明らかに悪かったので、応急処置としてクローンを作成してそちらで運用することにしました。
原因究明のため、AWSに問い合わせたところ・・・・AWSから謝罪が来ました。
原因はAWS側に起因するもので、ELBノードに異常があったからだそうです。
ELBノードを入れ替えることで対応するとのことでした。
以上、こんなこともあるんだなぁという話でした。(AWS側に起因するような問題って結構あるのでしょうか・・・?)
]]>Ansibleでローカルにあるファイル(ディレクトリの中身)をリモートにコピーするタスクです。
---
- hosts: vps
become: yes
user: root
tasks:
- name: Copy a directory
copy:
src: /path/to/directory/
dest: /usr/local/bin/
mode: u+x
ディレクトリの中身をリモートの/usr/local/bin
以下に全てコピーするタスクです。パーミッションも指定しています。
ドキュメント通りで特にハマるようなポイントはなさそうです。
Ansibleでリモートのファイルに書き込みをするタスク。よく使うやつ。
---
- hosts: vps
become: yes
user: root
tasks:
- name: Add text
blockinfile:
dest: /path/to/file
insertafter: '^# Add Here'
content: |
# New Line
Here is a new line.
さらっとかけますねー
VagrantのCentOS7.3に開発環境をAnsibleで構築します。
ベストプラクティスをある程度模倣した形のディレクトリです。
ansible/
├── group_vars
│ └── vagrant.yml
├── host
├── roles
│ ├── common
│ │ └── tasks
│ │ ├── add_remi_repo.yml
│ │ ├── install_common.yml
│ │ ├── install_epel_release.yml
│ │ └── main.yml
│ ├── composer
│ │ └── tasks
│ │ ├── install_composer.yml
│ │ └── main.yml
│ ├── mailcatcher
│ │ └── tasks
│ │ ├── install_mailcatcher.yml
│ │ └── main.yml
│ ├── mysql
│ │ └── tasks
│ │ ├── install_mysql.yml
│ │ └── main.yml
│ ├── nginx
│ │ ├── tasks
│ │ │ ├── install_nginx.yml
│ │ │ └── main.yml
│ │ └── templates
│ │ ├── bmf-tech.com.conf
│ │ └── localdev.conf
│ ├── php
│ │ └── tasks
│ │ ├── install_php.yml
│ │ └── main.yml
│ ├── python
│ │ └── tasks
│ │ ├── install_python.yml
│ │ └── main.yml
│ ├── redis
│ │ └── tasks
│ │ ├── install_redis.yml
│ │ └── main.yml
│ └── ruby
│ └── tasks
│ ├── install_ruby.yml
│ └── main.yml
├── site.retry
└── site.yml
github - my-ansible-vagrantにソースを上げているので中身はそちらをご参照ください。
Vagrantfileはこんな感じです。
# -*- mode: ruby -*-
# vi: set ft=ruby :
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "centos7.3"
config.vm.network "private_network", ip: "192.168.33.10"
config.vm.synced_folder "/path/to/directory", "/var/www/html",:mount_options => ["dmode=775,fmode=664"]
config.vm.provision "ansible" do |ansible|
ansible.playbook = "ansible/site.yml"
ansible.inventory_path = "ansible/host"
ansible.limit = 'all'
end
config.vm.network :private_network, ip: "192.168.33.10"
config.vm.hostname = "localdev"
config.hostsupdater.aliases = ["localdev"]
end
vagrant provision
でプロビジョニングを実行できます。
nginxでphp7を使うにはphp-fpmとかいうCGIをかます必要があるらしいのですが、これがハマりやすかったです。500エラーが出た時などは、このあたりを設定を見直すと解決するかもです。
VAGRANTにてCENTOS7にNGINX+PHP-FPM+PHP7でLARAVELの開発環境構築(前編)
構築できたものの、Vagrantfileに指定したipにアクセスできずに結構ハマりました。
以下の記事を参考にipの設定を見直したり、firewalldの設定を調整したら何とか解決できました。
(vagrant1.9.0のバグを踏んでしまっていたのが原因だったみたいです。)
CentOS7はそれまでのOSバージョンと異なる部分が結構あるのですが、その対応にはそんなにハマりませんでした。
むしろ、MySQL5.7の対応にハマりました。
とりあえず動くきますが、まだまだ改善の余地があるかと思います。
Ansibleでcronを設定するタスクです。
以下は毎分タスクを実行する設定の例です。
---
- hosts: vps
become: yes
user: root
tasks:
- name: Output recently logined users
cron:
name: last.sh
job: last.sh
minute: "*/1"
注意点としては、*/1
と書きたい場合はダブルクォーテーションで囲う必要がある点です。囲わないとシンタックスエラーが出ます。(YAMLの勉強不足。。w)
cronの設定も問題なくすんなりいけました。
基本的なgitコマンドしか使わないのですが、毎回コマンド叩くの面倒くさい、楽したいということでエイリアスをつくってみました。
#git branch
alias git-b='git branch'
#git checkout
function gitCheckout() {
stty erase ^H
echo -n "What is the new branch name"?
stty echo
read var1
git checkout ${var1}
}
alias git-c=gitCheckout
#git checkout -b
function gitCheckoutBranch() {
stty erase ^H
echo -n "What is the new branch name for checkout?"
stty echo
read var1
git checkout -b ${var1}
}
alias git-c-b=gitCheckoutBranch
#git pull
function gitPull() {
stty erase ^H
echo -n "What is the remote repository name?"
stty echo
read var1
git pull origin ${var1}
}
alias git-p=gitPull
#git set
function gitSet() {
stty erase ^H
echo -n "What file name do you add?"
read var1
git add ${var1}
echo -n "What is the commit message?"
read var2
git commit -m\'${var2}\'
echo -n "What is the branch name?"
stty echo
read var3
git push origin ${var3}
}
alias git-set=gitSet
.bashrcが読み込まれない
エイリアスコマンド実行後の入力受付時にでbackspace(delete)キーが変な文字列に変換されてしまう
git config
とかいうgit用のエイリアス設定コマンドがあったのを忘れていました。
よく使うgitコマンドのエイリアスを設定して開発効率をアップする
数ヶ月前くらいからOSSとしてソースコードをgithubに公開しながらCMSを開発しています。 何の目的で始めたかについてダラダラとかきます。
OSSの定義についてWikipediaを参照してみます。
オープンソースソフトウェア(英: Open-source software, 略称: OSS)とは、ソースコードが利用可能で、著作権保持者がどんな目的のためでもソフトウェアを、学習、変更、そして配布するための権利を提供するというライセンスに基づいたソフトウェアである[1]。オープンソースソフトウェアは何れも共同で開発されている。オープンソースソフトウェアは最も著名なオープンソース開発の例であり、しばし(技術的に定義される)消費者生成メディアや(法的に定義される)オープンコンテント運動と比較される[2]。 Wikipediaより引用
三行でまとめると、
という特徴を持ったソフトウェアのことです。
予め言っておくと、現在私が開発しているプロダクトは"共同開発"という要件を満たしきれていないので、厳密にはOSSとはいい難いのかもしれませんが、細かいことは気にしない方向でお願いします。
#何を開発しているのか マークダウンで記事を書くことのできるCMSを開発しています。 バックエンドはLaravel、フロントエンドはReactで開発しています。
「作りたいから作る、作るのが楽しい、その結果がスキルアップにつながれば良いなぁ」というそんな感じのスタンスではじめました。
実用的かつ色んな技術やアイデアを試す余地が多いので、開発しがいがあって楽しいです。
自分の抱える問題解決が他者の問題解決に繋がる、貢献する可能性を秘めているということで自己本位な気持ちでOSSを始めることに後ろめたさを感じる必要はないのかなーと思いました。
LaravelとReactの開発事例がgithubにあまり多くなかったので、色んなツッコミが入るといいなぁと思っています。
実はまだv1.0.0のリリースができていません。もうちょっとです。。。(震え声)
なのでまずは、v1.0.0をリリースすることが当面の目標です。
その上で
上記のような我欲を満たしていきたいです。
v1.0.0をリリースしました。
まだまだバグが多いですが、今後アップデートしていくつもりです。
Rubel ISSUEでもPRでもどんなささないことでもぶん投げてもらえるとうれしいです。(タイポからコードレビュー、機能要望その他何でも)
それから、自分もOSSやってるぜーソース公開してるぜーという方がいましたら遠慮なく是非ともコメントにリンク貼ってください、OSSフレンズになりましょう。
Rubelという名前はネーミングツールでLaravelとReactの2単語をベースにいい感じのワードを生成して決めました。 dotmator 個人で開発しているプロダクト名を決める時に悩みがちな人にはオススメのツールです。
]]>