Open8

Cloud Native Goのまとめ

nabetsunabetsu

Cloud Nativeという概念について

誤解

  • 特定の技術や言語に依存するもの?
    • No
  • Cloudで動かしたらCloud Native?
    • No。例えばオンプレで動いていた管理しにくくデプロイしづらいアプリケーションをKubernetesに入れて動かしても、 ずさんなアプリケーションはずさんなまま。

背景

  • インターネットを継続的に利用する人の割合は1997年に2%だったが2017年には48%になり、現在も増え続けている
  • 人々がこうした状況に慣れ、サービスに期待するレベルが上がっている(より洗練された機能や常にサービスが使えることを求めるようになった)
  • こうした要求に応えるため、より一層スケールし、複雑で(複雑な要求に応えられ)、組み合わせ可能なシステムが求められるようになった
  • AWS等のパブリッククラウドの登場により単純にサーバの数を増やしたりすることはできるようになった
  • しかし、インフラだけ増やしてもこうした新しい要求に旧来の方法では答えられない(100や1000のサーバをどうやって管理するか。動作がおかしい時どうやってデバッグするか。そもそもシステムに問題がないかどうやって知るか)
  • Cloud Nativeの概念はこうした状況を解決するための(クラウドを利用したアプリケーション構築の)ベストプラクティスとして提唱された

Cloud Nativeとは

Cloud Native Computing Foundationによる定義は以下。

Cloud native technologies empower organizations to build and run scalable applications in modern, dynamic environments such as public, private, and hybrid clouds….

These techniques enable loosely coupled systems that are resilient, manageable, and observable. Combined with robust automation, they allow engineers to make high-impact changes frequently and predictably with minimal toil6.
-Cloud Native Computing Foundation, CNCF Cloud Native Definition v1.0

ここに記載されている通り、Cloudの力を活用し、ユーザの要求事項に答えるためにはシステムとして以下の特性を持つ必要がある。

  • Scalable
  • Loosely Coupled
  • Resilient
  • Maganeable
  • Observable

また、上記にある堅牢な自動化の仕組みと組み合わせることで、インパクトの大きい変更であっても頻繁にリリース可能にするというのもポイント。(自動化という点でCI/CDの活用やDevOpsの考えも重要になる)

Scalability

急激なアクセス数の変化が発生しても、システムとして期待したとおりに動作し続ける力のこと。
あるシステムが急激なアクセス数の増加に見舞われても特に手を加える必要なしに動作し続けられたらそのシステムはScalabilityを備えていると言える。

Scalabilityの実現方法には以下2つの方法がある。

  • Vertical Scaling
    • ハードウェアリソースを増強することでアクセスの増加に対応することを指す
    • メリット
      • 比較的簡単
    • デメリット
      • 対応できる範囲に制約がある(ハードウェアの制約)
  • Horizontal Scaling
    • サービスインスタンス(サーバー等)の数を増やすことでアクセスの増加に対応することを指す
    • メリット
      • 上限に制約がない
      • 冗長性を担保できる
    • デメリット
      • 管理するインスタンスの数が増えるので設計や管理が複雑になる
      • すべてのサービスで実現できるわけではない

Loose Coupling

Loose Couplingとはシステムのコンポーネントが他のコンポーネントについて最小限の知識しかもたないように設計をすること。
一つのコンポーネントに対して加えた変更が他のコンポーネントに影響を及ぼさない時に、その2つのシステムは疎結合だと言える。

Reailence

  • 一部のコンポーネントが動作しなくなっても、システム全体では動作し続けるシステムを作る
    • システムのコンポーネントはfailするという想定をする
    • 一部のコンポーネントが動作しなくなった時の影響範囲を限定する

Manageability

Observability

システムが今どういう状態かを外部の状態からどれぐらい把握できるかという尺度。

古いアーキテクチャであればログの設定がしてあり、メトリクスが見れるダッシュボードがあり、アラートの設定があれば十分だったかもしれないが、現在の分散し、複雑化したシステムではこれまでの手法では通用せず、新しい手法が必要。

これがObservabilityという性質。

nabetsunabetsu

GolangとCloud Nativeの関係性

  • Goは2007年にGoogleによって開発された
  • Goが開発された当時に広く使われていた言語は現在では当たり前となっているマルチコアプロセッサーやインターネットへのアクセスが世界的に提供される以前の時代に開発されたものであり、こうした現代では当たり前となった(そしてCloud上でのアプリケーション構築で必須となる)ものを効率的に使うことができない
  • こうした状況やエンジニアリング上の問題を踏まえて、Cloud Nativeな世界で必要とされる特性を備えた言語としてGolangを開発した

「Go言語らしさ」とは何か? Simplicityの哲学を理解し、Go Wayに沿った開発を進めることの良さ - エンジニアHub|Webエンジニアのキャリアを考える!

Composition

  • ソフトウェアに複雑さをもたらす可能性の高いInheritance(is-a関係)ではなくComposition(has-a関係)の使用が推奨される
  • 例えば以下の例ではShape interfaceでAreaメソッドを定義することで、AreaメソッドをもったものはShape interfaceを暗黙的に満たす。
type Shape interface {                  // Any Shape must have an Area
    Area() float64
}

type Rectangle struct {                 // Rectangle doesn't explicitly
    width, height float64               // declare itself to be a Shape
}

func (Rectangle r) Area() float64 {     // Rectangle has an Area method; it
    return r.width * r.height           // satisfies the Shape interface
}

nabetsunabetsu

Go Language Foundations

Basic Data Types

  • Booleans that contain only one bit of information — true or false — representing some logical conclusion or state.

  • Numeric types that represent simple — variously-sized floating point and signed and unsigned integers — or complex numbers.

  • Strings that represent an immutable sequence of Unicode code points.

Booleans

	and := true && false
	fmt.Println(and) // false
	
	or := true || false
	fmt.Println(or) // true
	
	not := !true
	fmt.Println(not) // false

Numbers

Simple Numbers
  • Signed Integer(符号付き整数)
    • int8
    • int16
    • int32
    • int64
  • Unsigned Integer(符号なし整数)
    • uint8
    • uint16
    • uint32
    • uint64
  • Floating-Point(浮動小数点)
    • float32, float64
Complex Numbers

以下2種類のデータタイプが存在するが、あまり頻繁に使うことはないので、省略。

  • complex64
  • complex128
	var x complex64 = 3.1415i
	fmt.Println(x) // (0+3.1415i)

Strings

  • GoにおけるStringはimmutableで内容を変更することは出来ない

GoにおけるStringの定義方法には以下の2つがある。

  • ダブルクォート
  • バッククォート

以下は同じ文字列を上記2つの方法で定義した場合の例。

// The interpreted form
"Hello\nworld!\n"

// The raw form
`Hello
world!`

Variables

// 定義時に値を設定
var foo int = 42

// 複数の変数を定義
var foo, bar int = 42, 1302

// 型の指定を省略
var foo = 42

// 型の指定を省略して、複数の変数を定義
var b, f, s = true, 2.3, "four"

// 変数の定義時に値を設定しない(Zero Valueが設定される)
var s string

Short Variable Declaration

以下のようにvarを省略して変数を定義することもできる。
この記法を使用した時には型の指定は出来ない

name := 'John'

Zero Values

変数の定義時に値を代入しない場合、型ごとに定められたゼロの値(Zero Values)が自動で代入される

  • Integer
    • 0
  • Floats
    • 0.0
  • Booleans
    • false
  • Strings
    • ""

Blank Identifier

Constants

nabetsunabetsu

Cloud Native Applicationとは

Cloud Nativeという単語が示すのは単にクラウドで動かすことではない(アプリケーションをコンテナに入れてKubernetesで動かすことではない)

Story

  • 1950s
    • メインフレーム
      • 一つの巨大なコンピュータ上で全てのプログラムが動作
  • 1980s
    • Multi-tier Architecture

  • 1990s
    • Microservice
  • 2000s

nabetsunabetsu

Cloud Native Patterns

L Peter Deutschがまとめた分散アプリケーションの初学者が持ちやすい誤解

  • The network is reliable: switches fail, routers get misconfigured
  • Latency is zero: it takes time to move data across a network
  • Bandwidth is infinite: a network can only handle so much data at a time
  • The network is secure: don’t share secrets in plain text; encrypt everything
  • Topology doesn’t change: servers and services come and go
  • There is one administrator: multiple admins lead to heterogeneous solutions
  • Transport cost is zero: moving data around costs time and money
  • The network is homogeneous: every network is (sometimes very) different

[the Fallacies of Distributed Computing] L Peter Deutsch

さらにこの書籍の著者が以下を追加で挙げている。

  • Services are reliable: services that you depend on can fail at any time

上記のポイントについて実際にGolangでのアプリケーション構築を通じて学んでいく。

この本ではアプリケーションレベルにフォーカスするためBulkheadGatekeeperについては扱わない。

インフラレベルでのパターンについては以下の書籍参考。

  • Cloud Native Infrastructure by Justin Garrison and Kris Nova (O’Reilly)
  • Designing Distributed Systems by Brendan Burns (O’Reilly).

Contextの活用

アプリケーションの構築に当たってGo1.7から導入されたcontext packageを活用する。
contextの仕組みを使うことでリクエストなどを受け取った際に状況をサブリクエスト(例えばDBへのリクエストなど)と共有することができる。

具体的に役立つ場面として、ユーザがリクエストを途中でキャンセルした場合にアプリケーションのプロセスやDBへのリクエストなど、ユーザからのリクエストに付随して行われる処理を途中でキャンセルすることが簡単にでき、無駄なリソースを使わずに済む。

また、実装に当たって重要な点として、Contextの値はスレッドセーフであり、gorutinesによる並列処理でも意図しない挙動が発生する恐れがない。

Contextの作成

  • Background() Context
    • キャンセルされることのない空の(no values, no deadline)Contextを返す
    • 一般的には以下の用途で使われる
      • main function
      • 初期化(initialization)
      • テスト
      • リクエストのトップレベル
  • Todo() Context

DeadlinesとTimeoutsの設定

  • func WithDeadline(Context, time.Time)(Context CancelFunc)
  • func WithTimeout(Context, time.Duration)(Context CancelFunc)
  • func WithCancel(Context)(Context CancelFunc)

Request-Scoped Valueの定義

Stability Patterns

Circuit Breaker

Applicability

エラーやFailは常に発生する可能性があり、分散システムにおいてそれを拒否することはできなくて、(サービスは設定ミスが発生し、データベースはクラッシュし、ネットワークは繋がらなくなる)それを受け入れるしかない。

実装(Implementation)

Breakerは以下2つの状態(state)になりうる。

  • closed
    • 全てが問題なく動作している
    • Breakerが受信したリクエストは全て内容を変えずにそのままCircuitに渡される
  • open
    • 何かに問題が発生した状態
    • BreakerはCircuitにリクエストを送らず、エラーメッセージを返す

Circuit側でエラーを返した場合(あらかじめ定めた閾値を超えた場合)には、Breaker側でstateをopenに変更する。

Debounce

Debounce limits the frequency of a function invocation so that only the first or last in a cluster of calls is actually performed.

Retry

Throttle

Timeout

Concurrency Patterns

Fan-In

Fan-Out

Future(Promises or Delays)

Sharding

nabetsunabetsu

Building a Cloud Native Service

key-value storeを題材にCloud Native Serviceを作る

Requirements

  • Key-valueペアを格納できる
  • Key-valueペアをput, get, deleteするためのエンドポイントが提供されている
  • データを永続的に格納できる
  • 冪等性(idempotent)を持たせる

冪等性(idempotent)

プログラミングの世界では1回読んでも何回読んでも同じ結果(作用)が得られるとき冪等性(idempotent)があると呼ぶ。

  • x=1

  • Put Endpoint

  • x+=1

  • 冪等性を持つ処理は安全

    • あるサービスに対してリクエストを送って結果が返ってこなかったとき、おそらくほとんどの人はもう1度リクエストを送る
    • このとき冪等性があれば何も影響を及ぼさない。しかし、もしそうでなければ何かしらの影響が発生する
  • 冪等性を持つ処理はシンプル

    • 受け取った値で更新するだけのPUTともし同じ値が存在すればエラーを返すCREATEを比較してみるとPUTのロジックが非常にシンプルなことがわかる
    • CREATEを実装しようとすると、エラーハンドリングや分散システムでのロックなどが必要になってくる
  • 冪等性を持つ処理は宣言的

    • 結果

Generation 0: The Core Functionality

どのWebフレームワークを使うか考える前にコアな機能を構築する

  • Key-valueペアを格納する
  • put, get, deleteする

Simple API

Generation1: The Monolith

コア機能の構築が終わったので、サービスとして提供するための構築を行う。
選択肢として以下の3つが考えられるが、現在のアプリケーションはそれほど複雑なデータを扱うわけでもないので、もっともシンプルに実現できるRESTをまずは採用する。

  • GraphQL
  • RPC(gRPC)
  • REST

net/httpを使ったHTTP Serverの構築

GoではPythonでいうDjangoやFlaskのように洗練されたWeb Frameworkはない?が、80%程度のユースケースをカバーしてくれるスタンダードライブラリが提供されている。
また、そうしたスタンダードライブラリは拡張可能なように設計されているため、多くのWeb Frameworkがスタンダードライブラリの機能を拡張して作られている。

HTTP Handlerの仕組みを構築するのに使用するスタンダードライブラリはnet/http
以下がもっとも基本的な実装。

func helloGoHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello net/http!\n"))
}

func main() {
	http.HandleFunc("/", helloGoHandler)

	log.Fatal(http.ListenAndServe(":8080", nil))
}
# サーバの起動
$ go run main.go

# リクエストの送信
$ curl http://localhost:8080
Hello net/http!

前述のコードを紐解くと、http.HandleFuncでリクエストと処理とのマッピングを定義でき、また、http.HandleFuncは以下の定義がされているので、それに準じた実装を呼び出すFunction側で行う必要がある。

type HandlerFunc func(http.ResponseWriter, *http.Request)

Gorillaを使った改善

For many web services the net/http and DefaultServeMux will be perfectly sufficient. However, sometimes you’ll need the additional functionality provided by a third-party web toolkit. A popular choice is Gorilla, which, while being relatively new and less fully developed and resource-rich than something like Django or Flask, does build on Go’s standard net/http package to provide some excellent enhancements.

The gorilla/mux package—one of several packages provided as part of the Gorilla web toolkit—provides an HTTP request router and dispatcher that can fully replace DefaultServeMux, Go’s default service handler, to add several very useful enhancements to request routing and handling. We’re not going to make use of these features just yet, but they will come in handy going forward. If you’re curious and/or impatient, however, you can take a look at the gorilla/mux documentation for more information.

Gorilla/muxを使った実装はHandleFuncの定義の前にRouterの定義を追加するだけ。

import (
	"errors"
	"log"
	"net/http"

	"github.com/gorilla/mux"
)

func helloMuxHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello gorilla/mux!\n"))
}

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/", helloMuxHandler)

	log.Fatal(http.ListenAndServe(":8080", nil))
}

Gorillaの機能: Variables in URI Paths

Pathの指定に変数を使える。

r := mux.NewRouter()
r.HandleFunc("/products/{key}", ProductHandler)
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)

// mux.Varsでhandler Function側で変数のmapを取得できる
vars := mux.Vars(request)
category := vars["category"]

Gorillaの機能: Matchers

Host名やメソッドなど、より細かなマッチングの指定ができる。

r := mux.NewRouter()

r.HandleFunc("/products", ProductsHandler).
    Host("www.example.com").                // Only match a specific domain
    Methods("GET", "PUT").                  // Only match GET+PUT methods
    Schemes("http")                         // Only match the http scheme

Gorilla - Matching Routes

RESTful Serviceの構築

Functionality Method Possible Statuses
Key-value PairをStoreに格納する PUT 201(Created)
StoreからKey-value Pairを取得する PUT 200(OK), 404(Not Found)
Key-value PairをStoreから削除する DELETE 200(OK)

Create Functionの実装

  • Status Codeは201か500で返す
    • ステータスコードの設定自体はhttpパッケージで定義されたものを使える
func main() {
	r := mux.NewRouter()
	r.HandleFunc("/v1/{key}", keyValuePutHandler).Methods("PUT")

	log.Fatal(http.ListenAndServe(":8080", r))
}

func keyValuePutHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	key := vars["key"]

	// Request Bodyからキーの値を取得する
	value, err := io.ReadAll(r.Body)
	defer r.Body.Close()

	// キーの値の取得に失敗したらInternal Server Errorを返す
	if err != nil {
		http.Error(w,
			err.Error(),
			http.StatusInternalServerError)
		return
	}

	// Putに失敗したらInternal Server Errorを返す
	err = Put(key, string(value))
	if err != nil {
		http.Error(w,
			err.Error(),
			http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusCreated)
}
% curl -X PUT -d 'Hello, key-value store!' -v http://localhost:8080/v1/key-a
...
< HTTP/1.1 201 Created

% curl -X GET -d 'Hello, key-value store!' -v http://localhost:8080/v1/key-a
...
< HTTP/1.1 405 Method Not Allowed

Read Functionの実装

% curl -X PUT -d 'こんにちは' -v http://localhost:8080/v1/key-a
% curl -X GET -d 'こんにちは' -v http://localhost:8080/v1/key-a
...
< HTTP/1.1 200 OK
< Date: Fri, 14 May 2021 10:24:35 GMT
< Content-Length: 15
< Content-Type: text/plain; charset=utf-8
< 
* Connection #0 to host localhost left intact
こんにちは* Closing connection 0

Deleteの作成

同じ要領でDelete Functionも作成する

func keyValueDeleteHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	key := vars["key"]

	err := Delete(key)
	if err != nil {
		http.Error(w,
			err.Error(),
			http.StatusInternalServerError)
		return
	}

	w.Write([]byte("Delete Compeleted"))

}

  • GoにおけるMapsは並列処理ではsafeではない
  • 一般的にgoroutine等のより並列処理でreadもwriteも行われる場合には、mutex(lock)を用いる
  • 具体的にはsyncパッケージのRWMtexがこうした機能を提供してくれる

mutexの基本的な仕組み

Writeの場合
// 定義
var myMap = struct{
    sync.RWMutex
    m map[string]string
}{m: make(map[string]string)}

// 実際に使う
myMap.Lock()                                // Take a write lock
myMap.m["some_key"] = "some_value"
myMap.Unlock()    
readの場合
myMap.RLock()                               // Take a read lock
value := myMap.m["some_key"]
myMap.RUnlock()                             // Release the read lock

fmt.Println("some_key:", value)

muxの組み込み

storeの改善

Lockの仕組みを取り入れるためまずはstoreのデータ型をstructに変えて、sync.RWMutexを持つようにする

// var store = make(map[string]string)
var store = struct {
	sync.RWMutex
	m map[string]string
}{m: make(map[string]string)}

Functionの修正

各Functionでstoreのアクセス前後にLockとリリースの処理を追加する。
storeの型が変わったのでアクセスの方法も変更する

func Put(key string, value string) error {
	// store[key] = value
	store.Lock()
	store.m[key] = value
	store.Unlock()

	return nil
}


func Get(key string) (string, error) {
	// value, ok := store[key]
	store.Lock()
	value, ok := store.m[key]
	store.Unlock()

	if !ok {
		return "", ErrorNoSuchKey
	}

	return value, nil
}

後はcurl等で機能が変わっていないことを確認する

% curl -X PUT -d 'こんにちは' -v http://localhost:8080/v1/key-a # 201で返ってくる
% curl -X GET -d 'こんにちは' -v http://localhost:8080/v1/key-a # 200で返ってくる
% curl -X DELETE -v http://localhost:8080/v1/key-a             #
% curl -X GET -d 'こんにちは' -v http://localhost:8080/v1/key-a # 404 no such keyと返ってくる
nabetsunabetsu

Generation 2: Persisting Resource State

分散されたCloud Native Applicationでの難しい問題はどうやってstate(状態)を保存するか。
複数のサービス間で状態を管理する方法にはいくつものテクニックがあるが、まずはMVPということで以下の2つの方法を使用する。

  • ログファイルにStateを保存
  • データベースにStateを保存

以下、まだ消化できていないが、stateについてなぜ、stateを持つことが問題なのか、また、stateといってもapplication stateとresource stateの2つがあり、よく混同されてしまうことに触れている

Transaction Log

データストアに加えられた変更を記録するもの。
もしサービスがクラッシュしたときに変更を再現し、元の状態に復旧できるよう作られるもの。

一般的にはDBMSで使われるものだが、今回の用途ではシンプルなものを作る。

Pros and Cons

  • Pros:

    • No downstream dependency
      • There’s no dependency on an external service that could fail or that we can lose access to.
    • Technically straightforward
      • The logic isn’t especially sophisticated. We can be up and running quickly.
  • Cons:

    • Harder to scale
      • You’ll need some additional way to distribute your state between nodes when you want to scale.
    • Uncontrolled growth
      • These logs have to be stored on disk, so you can’t let them grow forever. You’ll need some way of compacting them.

要件

  • 時刻順で上から下に流れていく(同じ状態が再現できるように)
  • 追加しかできない(過去の記録を削除したりしない)

以下の項目を持つ

  • sequence number
  • Event type(PUT or DELETE)
  • Key
  • Value

Loggerのinterfaceを定義

type TransactionLogger interface {
	WriteDelete(key string)
	WritePut(key, value string)
}

設計(Prototype)

  • シンプルさのため、プレインテキストで書き込む(binaryなどの選択肢もあるが、)
  • 行ごとに書き込む
  • 前述の4つの項目をタブ区切りで書き込む

先ほどinterfaceで定義したWritePutWriteDeleteを実装する

type FileTransactionLogger struct {
	// something
}

func (l *FileTransactionLogger) WritePut(key, value string) {
	// something
}

func (l *FileTransactionLogger) WriteDelete(key, value string) {
	// something
}

ログに書き込む内容をEventとして定義する。
ここではiotaの機能を利用して利用可能なEventの種類(EventType)を管理する。

type EventType byte

const (
	_                     = iota
	EventDelete EventType = iota
	EventPut
)

type Event struct {
	Sequence  uint64
	EventType EventType
	Key       string
	Value     string
}

ログの組み込み

最終的には以下のようにmain functionにログの処理を組み込む

func main() {
	// Initializes the transaction log and loads existing data, if any.
	// Blocks until all data is read.
	err := initializeTransactionLog()
	if err != nil {
		panic(err)
	}

	// Create a new mux router
	r := mux.NewRouter()

	r.Use(loggingMiddleware)

	r.HandleFunc("/v1/{key}", keyValueGetHandler).Methods("GET")
        ...

この状態でHTTPサーバを起動してリクエストを送ると、以下のようにログが出力される。

% go run .               
2021/05/20 07:29:44 0 events replayed
2021/05/20 07:30:20 PUT /v1/key-a
2021/05/20 07:30:20 PUT key=key-a value=こんにちは
2021/05/20 07:30:40 GET /v1/key-a
2021/05/20 07:30:40 GET key=key-a
2021/05/20 07:30:51 DELETE /v1/key-a
2021/05/20 07:30:51 DELETE key=key-a
2021/05/20 07:31:27 GET /v1/key-a

現時点は最小限の実装であり、まだ以下の問題がある。

  • There aren’t any tests.
  • There’s no Close method to gracefully close the file.
  • The service can close with events still in the write buffer: events can get lost.
  • Keys and values aren’t encoded in the transaction log: multiple lines or whitespace will fail to parse correctly.
  • The sizes of keys and values are unbound: huge keys or values can be added, filling the disk.
  • The transaction log is written in plain text: it will take up more disk space than it probably needs to.
  • The log retains records of deleted values forever: it will grow indefinitely.

外部のDBにStateを保存する

GoではコアライブラリにSQLとのやりとりを行うためのパッケージ(sql)が含まれている。

  • Pros
    • アプリケーションの状態を外部に保存できる
      • 分散したアプリケーションそれぞれの状態を気にする必要がなくなり、クラウドネイティブに近づく
    • スケールしやすい
  • Cons
    • ボトルネックを導入してしまう
    • upstream dependencyを導入してしまう
    • 初期化が必要になる
    • 複雑性が増す

GOにおけるDBとのやりとり

sql.DB

利用するDBに合わせてDBドライバーを選択する必要があり、Goでは40個以上のDBドライバーが提供されている

実装

  • sql.DBを作成するため、PostgresTransactionLoggerのConstruction fucntionを作成する
    • ファイルの時と異なり、DBとのコネクションを確立し、失敗した時にはエラーを返す
func NewPostgresTransactionLogger(host, dbName, user, password string)
    (TransactionLogger, error) { ... }