Go언어는 예외처리가 없으며, 에러에 대해서 가장 가까운 위치에서 명시적으로 체크하고 넘어가는 것을 권장하는 언어 이다. 이 행위를 강제하진 않기 때문에 좀 더 단순하나 문제가 발생 할 소지를 없애기 위해 에러 체킹을 강제화 하는 언어(OCaml,Scala등)에 비해 안정성은 좀 떨어진다고 말 할 수도 있겠다. 하지만 이런 것은 팀의 코딩컨벤션으로 항상 체크하고 넘어가면 되는 문제로 생각 할 수도 있을 것이다. 

참고로 예외처리는 굉장히 어려운 주제이며 예외 처리에 대한 6가지 화두 이 글을 통해서 말한바 있다.

Panic 예제

func test () int {
   arr := [] int {}
   element := arr[5]
   return element
}

func main() {
   test()
   fmt.Println("smooth exit")

}
test함수에서 인덱스 범위 밖의 배열을 참조 했으므로, panic이 발생하여 정상종료되지 못하고 뻣어버리는데 
package main

import (
   "fmt"
)

func test() int {
   arr := [] int {}
   defer func() {
      v := recover()
      fmt.Println("recovered:", v)
   }()

   x := arr[5]
   return x
}

func main() {
   result := test()
   fmt.Printf("smooth exit %d", result)

}

이렇게 내부에 recover 를 해주면 panic 이 상쇄되며 리턴값으로 타입의 기본값이 들어가게 된다.

package main

import (
   "fmt"
   "runtime/debug"
)

func r() {
   if r := recover(); r != nil {
      fmt.Println("Recovered", r)
      debug.PrintStack()
   }
}

func a() {
   defer r()
   n := []int{5, 7, 4}
   fmt.Println(n[3])
   fmt.Println("normally returned from a")
}

func main() {
   a()
   fmt.Println("normally returned from main")
}

문제가 발생 했을 당시의 스택트레이스를 확인 할 수도 있을 것이다.
근데 여기서 고민이 그럼 모든 함수에 저런것을 해야한다고 생각하면 정내미가 떨어 질 수 있을 것이다.

그럼 언제 고의적 panic을 해줘야하나? 

1. 에러인가 예외인가?
2. 죽일 것 인가? 살릴 것 인가? 
3. 가까운 곳에서 즉시 처리할 것인가? 상위로 위임할 것인가?
4. 강제적으로 처리 시킬 것인가? 아닌가?  (체크드 / 언체크드) 
5. 모두 되돌릴 것인가? 이미 벌어진 일은 무시,포기할 것인가? 
6.  리턴으로 처리 또는 예외 개념 도입  

예전에 작성한 예외처리에 대한 화두 6가지를 보면, 2번이 이것에 해당되는 내용일 거 같다. 내용은 아래와 같은데 

최근 특정 솔루션에서 나는 예외에 대해 고민하길 포기해버렸다.  따라서 예외를 단순히
 프로그램이 죽지 않게 하기위해 예외처리를 한다. 잘 죽는 부분은 이렇다.

* JSON 변환을 하는 부분  
* 소켓 처리부분, 대부분의 io 부분. (외부의 파일이 어떻게 될지 모름)
* 널포인터가 없을거라고 확신하지 못하는 부분 모두 (이건 좀 너무 광범위한데 C++ 로 개발할때  ASSERT 로 일단 도배한다.) 
* 여러 쓰레드가 접근되는 부분   
* 사용자가 주는 값에 대해 신뢰해선 안되는 부분 
* 컬렉션 처리중 먼가 냄새가 나는 부분

뭐 더 많겠지만 요즘 내가 짜는 프로그램은 이런거 같다. 이렇게 써 놓고 보니 예외를 예외로 생각하지 않고 프로그램 시퀀스의 일부분으로 좀 더 오버해서 즉 if 문 처럼 생각하고 쓰는게 아닌가 싶기도 한데 좀 더 고찰해 볼 시간은 없었다.

암튼 위의 리스트에서 느껴지듯이 쓰레드,IO 등에서 문제가 생길 소지가 많은데 그래서 웹개발/서버개발에 있어서는 죽이지 않기 위해 예외처리를 하는 경우가 많은거 같다. "일처리 프로세스 과정이 복잡하지 않고 하나의 시퀀스에 대해 완료하지 못하더라도 큰 타격을 받지 않으며 어차피  다음에 시도하면 되는 것은 그냥 생활의 일부분이야 ~~그런 사소한 이유로 프로그램을 죽일 순 없어. 프로그램 죽는것은 내가 죽는것" 이런 마음가짐이었던 거다.

근데 그냥 빨리 죽여서 문제를 빨리 찾으려는 응용프로그램도 많으며 예러가 발생해선 절대 안되는 프로그램이 있다. 공장이나 의료쪽에서 사용되는 응용프로그램들이 그러한데 이런 프로그램에서는 저 위의 예처럼 모든 경우에 대해 예외처리를 하여 자신도 모르게 넘어가면 안된다. 예외가 로그에 남겠지만 그것만으로 부족하다. 개발자에게  문제가 생겼다는것을 즉시 각인 시켜야한다. 프로그램을 그 즉시 죽임으로써 말이다. 그리고 최대한 에러/예외가 나지 발생하지  않도록 설계를 해야하며 오류를 잡아야한다. 사실 이런 프로그램 경우 웹이나 게임처럼 외부와의 잦은 커뮤니케이션이 없고 안정된 하드웨어 등 시스템  때문에  예외가 발생할 가능성은 크게 적다.  

뭐 이런 글 이었는데 여기서 적용 할 수 있을 거 같다. 즉 계속 살아 있으면 안될때 Panic을 일으켜야 한다는 것이다. 먼가 문제가 있어도 걍 넘기거나 다시 시도해도 되는 많은 경우에는 필요 없을 것이다.  즉 초기에 중요한 설정을 하는데 실패 했을 경우 라든지, 중간에 입력된 상태를 기반으로 추후의 작업을 오래 동안 해야 할 경우. 중간에 입력된 값이 nil이라면 결과에 영향을 크게 미칠 것이고, 그 결과에 따라서 커다란 재산상/신체상 위협이 가해질 경우. 


그럼 언제 recover 를 해줘야하나? 

위와 다음과 같다고 보면 될 거 같다. Panic을 반드시 일으켜야 하는 상황이 아니면서 아래처럼 예외가 발생될 확률이 매우 높은곳으로 보면 될 거 같다.

* JSON 변환을 하는 부분  
* 소켓 처리부분, 대부분의 io 부분. (외부의 파일이 어떻게 될지 모름)
* 널포인터가 없을거라고 확신하지 못하는 부분 모두 (이건 좀 너무 광범위한데 C++ 로 개발할때  ASSERT 로 일단 도배한다.) 
* 여러 쓰레드가 접근되는 부분   
* 사용자가 주는 값에 대해 신뢰해선 안되는 부분 
* 컬렉션 처리중 먼가 냄새가 나는 부분

아래는 다양한 경우에 대한 예를 살펴보자. (해당 예제는 아래 레퍼런스에서 가져왔다)

예1) 프로그램 종료되는 것을 피하자.

굉장히 일반적인 경우의 예로  클라이언트-서버 동시성 프로그래밍에서 문제가 생겼을때 그냥 회피하는 방식이다.

package main

import "errors"
import "log"
import "net"

func main() {
	listener, err := net.Listen("tcp", ":12345")
	if err != nil {
		log.Fatalln(err)
	}
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Println(err)
		}
		// Handle each client connection in a new goroutine.
		go ClientHandler(conn)
	}
}

func ClientHandler(c net.Conn) {
	defer func() {
		if v := recover(); v != nil {
			log.Println("capture a panic:", v)
			log.Println("avoid crashing the program")
		}
		c.Close()
	}()
	panic(errors.New("just a demo.")) // a demo-purpose panic
}

클라이언트 하나 죽었다고, 모두다 죽으면 안되니깐~

예2) 고루틴 충돌시 자동적으로 재시작하기

고루틴에서 Panic이 감지되었을때 새로운 고루틴을 만들 수 있다.

package main

import "log"
import "time"

func shouldNotExit() {
	for {
		time.Sleep(time.Second) // simulate a workload
		// Simulate an unexpected panic.
		if time.Now().UnixNano() & 0x3 == 0 {
			panic("unexpected situation")
		}
	}
}

func NeverExit(name string, f func()) {
	defer func() {
		if v := recover(); v != nil { // a panic is detected.
			log.Println(name, "is crashed. Restart it now.")
			go NeverExit(name, f) // restart
		}
	}()
	f()
}

func main() {
	log.SetFlags(0)
	go NeverExit("job#A", shouldNotExit)
	go NeverExit("job#B", shouldNotExit)
	select{} // blocks here for ever
} 

예3) 롱점프 Statements 시뮬레이션을 위한panic/recover Calls

일반적으로 이러한 방법은 권장되지 않지만 때때로 우리는 crossing-function long jump statements 과 crossing-function returns 를 시뮬레이션하는 방법으로 패닉/회복을 사용할 수 있다. 이 방법은 코드 가독성과 실행 효율성 모두에 해를 끼친다. 유일한 이점은 때때로 코드를 덜 장황하게 보이게 할 수 있다는 것이다.

다음 예에서 내부 함수에 패닉이 발생하면 실행이 지연된 호출로 점프한다.

package main

import "fmt"

func main() {
	n := func () (result int)  {
		defer func() {
			if v := recover(); v != nil {
				if n, ok := v.(int); ok {
					result = n
				}
			}
		}()

		func () {
			func () {
				func () {
					// ...
					panic(123) // panic on succeeded
				}()
				// ...
			}()
		}()
		// ...
		return 0
	}()
	fmt.Println(n) // 123
}

예4) 에러체크를 줄이기 위한  panic/recover 콜 

package main

import "fmt"

func doTask(n int) {
	if n%2 != 0 {
		// Create a demo-purpose panic.
		panic(fmt.Errorf("bad number: %v", n))
	}
	return
}

func doSomething() (err error) {
	defer func() {
		// The second optional return must be present here,
		// otherwise, the assertion will panic if no errors occur.
		err, _ = recover().(error)
	}()

	doTask(22)
	doTask(98)
	doTask(100)
	doTask(53)
	return nil
}

func main() {
	fmt.Println(doSomething()) // bad number: 53
}

위 코드는 아래보다는 덜 장황하다.

func doTask(n int) error {
	if n%2 != 0 {
		return fmt.Errorf("bad number: %v", n)
	}
	return nil
}

func doSomething() (err error) {
	err = doTask(22)
	if err != nil {
		return
	}
	err = doTask(98)
	if err != nil {
		return
	}
	err = doTask(100)
	if err != nil {
		return
	}
	err = doTask(53)
	if err != nil {
		return
	}
	return
}

레퍼런스:

https://go101.org/article/panic-and-recover-use-cases.html
https://golangbot.com/panic-and-recover/

CFT 는 분산시스템에서 노드가 비정상적인 충돌에 의해 문제가 생기더라도 나머지 시스템에서 서비스를 할 수 있게 하는 작동을 말한다면, BFT는 의도적 악의에 의한 문제까지도 해결하는 방식을 말한다. 

블록체인 시스템에선 둘 모두 합의라는 방식을 거치게 되는데, 비트코인의 경우는 일반적인 CFT, BFT 보다는 훨씬 더 극한노동(?) 들어가는 신뢰 작업이 추가되는데 바로 POW이며, 콘소시엄형 블록체인 시스템(이라고 하고 하이퍼레저 패브릭) 에서는 보통 조직들이 이미 신원확인등에 의한 허가를 받은 상태에서 참여하기 때문에 악의적인 행위를 안한다고 치고 서비스를 하기에, 서버가 맛가는 경우에 대해서만 방어하는 CFT 기반의 오더링 알고리즘이 우선되고 있다. 

CFT의 대표격이며, 하이퍼레저 패브릭 2.0 버전에서 등장할 가능성이 높은 RAFT와 BFT계열의 알고리즘을 Go언어로 심플하게 구현할 계획인데 아이디어와 결과는 새로운 포스트로 올릴 예정이다.

RAFT 참고 레퍼런스는 아래와 같다.
D. Ongaro et al.의 In Search of an Understandable Consensus Algorithm 
Hyperledger Fabric 2.0 RAFT Proposal 


예제로 이용된 재료는 이전에 만든  400라인의 go코드로 구현한 하이퍼레저 패브릭 [2]- 블록전파/Gossip 프로토콜 소스(앞으로는 gossip분산 서비스로 지칭)를 사용였는데 간단한 분산 네트워킹 예제이므로 도커/쿠버네이트 공부를 위한 좋은 재료가 될 것 입니다. 이 글은 그 예제의 연속성 상에서 기획된 글이며, 도커/쿠버네이트 내용을 한번 정도 읽어 봤다는 혹은 아래 참고 링크를 공부하면서 진행한다는 가정하에 실습을 위해 정리 한 포스트임을 알려드립니다. 각각의 기술에 대해 구체적으로 알고 싶은 분은 아래 레퍼런스를 참고 하시거나, 추가 구글링을 통해 확인 하십시요.

도커

아래 처럼 Dockerfile 을 만듭니다.

# Start from a Debian image with the latest version of Go installed
# and a workspace (GOPATH) configured at /go.
FROM golang

# Copy the local package files to the container's workspace.
ADD . /go/src/github.com/wowlsh93/hyperledger-fabric-400-gossip

# Build the gossip command inside the container.
# (You may fetch or manage dependencies here,
# either manually or with a tool like "godep".)
RUN go install github.com/wowlsh93/hyperledger-fabric-400-gossip/gossip

위의 도커파일을 가지고 gossip이라는 이름의 도커이미지를 만듭니다. 

docker build -t gossip .

기반 이미지는 golang 을 사용하며, 빌드시 현재 프로젝트(예제를 확인하시고 github 에서 가져오세요)를 컨네이너의 /go/src/github.com/wowlsh93/hyperledger-fabric-400-gossip 위치에 복사해 놓습니다. 그리고 소스를 인스톨하여 go/src/gossip 에 실행파일을 위치 시켜 놓습니다.

docker images

만들어진 이미지를 확인 하는 명령어는 위와 같습니다. 

docker run -p 28000:28000 --name gossip1 --rm gossip /go/bin/gossip -name 127.0.0.1:28000 -ip 127.0.0.1 -port 28000 -leader

실행은 run 명령어를 사용하며 옵션이 의미하는 바는  

-p 옵션으로 외부포트:내부포트를 포워딩 하고
-name : 이름은 gossip1 으로 임의로 정합니다.
-rm :  옵션으로 컨테이너가 내려가면 자동으로 삭제되게 하고 
컨테이너 실행시  /go/bin/gossip -name 127.0.0.1:28000 -ip 127.0.0.1 -port 28000 -leader 명령을 실행해 리더피어 서비스를 시작해 줍니다.  

두번째 터미널을 열어서 리더피어 말고 일반 피어를 위해 컨테이너를 띄어 봅시다. 
docker run -p 28001:28001 --net host --name  gossip2 --rm 
gossip /go/bin/gossip -name 127.0.0.1:28001 -ip 127.0.0.1 -port 28001  -bootstrap 127.0.0.1:28000
근데 컨테이너를 띄우면 에러가 나는데  컨테이너간 네트워킹을 할 수 없기 때문이며 이를 해결하기 위해 네트워킹 설정을 해야 합니다. 

도커 네트워킹 

1) 같은 노드에 있는 컨테이너 실습 - host 방식 

도커를 처음 설치하면 none, host, bridge 세 가지 종류의 네트워크가 자동으로 설정되는데  none방식(-net=none) 으로 하면 격리된 네트워킹을 갖기 때문에 안되고,  -net 옵션으로 host를 주면 모든 컨테이너가 호스트 네트워크를 같이 사용하게 되기 때문에 컨테이너간에 네트워킹이 가능해 집니다.

docker run -p 28000:28000 --net host --name gossip1 --rm gossip /go/bin/gossip -name 127.0.0.1:28000 -ip 127.0.0.1 -port 28000 -leader

위 처럼 -net host 를 각 컨테이너를 시작 할 때 넣어 주면 됩니다. 동일한 네트워크에 있기 때문에 포트만으로도 통신이 되는거죠.


2) 같은 노드에 있는 컨테이너 실습 - bridge 방식 

브리지방식은 도커의 디폴트 방식이기에 -net 옵션 없으면 자동으로 선택되는데 브리지 방식으로는 각각의 컨테이너가 다른 사설IP 를 갖게 되므로 포트만을 구분하는 예제(Gossip 서비스)에서는 안되는게 당연하며 추가정보가 필요합니다. 

docker network create --driver bridge gossip-network

일단 bridge형식의 네트워크를 하나 추가로 만듭니다. 이름은 gossip-network 로 하구요.

docker network ls

NETWORK ID NAME DRIVER SCOPE d87103af938c bridge bridge local 659368a11889 composer_default bridge local 17b35b1ad8c5 gossip-network bridge local 541d637a227c host host local de0e0715ded6 net_basic bridge local 6b6f56779a9a none null local

docker network ls 명령어로 gossip-network 가 만들어진것을 확인합니다.

docker run -it --network=gossip-network --name gossip1 gossip /go/bin/gossip -name gossip1:28000 -ip gossip1 -port 28000 -leader
docker run -it --network=gossip-network --name gossip2 gossip /go/bin/gossip -name gossip2:28001 -ip gossip2 -port 28001 -bootstrap gossip1:28000
docker run -it --network=gossip-network --name gossip3 gossip /go/bin/gossip -name gossip2:28002 -ip gossip3 -port 28002 -bootstrap

이제 각 도커 컨테이너를 시작시 옵션으로 -network=gossip-network 로 네트워크 설정을 하고, 불편한 IP주소 대신 gossip1~3처럼 컨네이너 명으로 컨테이너 끼리 통신 하게 할 수 있습니다. 컨테이너 끼리 NAT 테이블 안에서 브리지로 통신하기 때문에 굳이 -p 옵션으로 포트포워딩은 필요 없습니다. 참고로 다시 시작 할 때 컨네이터가 중단된 채 존재하기 때문에 안될 수 있는데 삭제 한 후에 해야합니다. 전체 컨테이너 삭제 명령어는 docker rm `docker ps -a -q` 입니다.


3) 같은 노드에 있는 컨테이너 실습 -  Docker Compose 

version: '2'
services:
leader:
image: gossip
container_name: gossip1
command: /go/bin/gossip -name gossip1:28000 -ip gossip1 -port 28000 -leader
restart:
on-failure


peer1:
image: gossip
container_name: gossip2
command: /go/bin/gossip -name gossip2:28001 -ip gossip2 -port 28001 -bootstrap gossip1:28000
restart:
on-failure
depends_on:
- leader
peer2:
image: gossip
container_name: gossip3
command: /go/bin/gossip -name gossip3:28002 -ip gossip3 -port 28002 -bootstrap gossip1:28000
restart:
on-failure
depends_on:
- leader

도커 컴포즈는 여러개의 컨테이너를 한방에 시작 시킬때 유용합니다. docker-compose.yaml  파일을 만들어 위의 내용을 작성합니다. 각 서비스를 구분했으며  옵션은 쉽게 이해 될 것입니다. 주의 할 점은 일반 피어는 리더피어가 생성된 후에 떠야하기때문에 depends_on옵션이 추가되었습니다. 

docker-compose -p gossipnetwork up -d

-p옵션으로 네트워크를 설정해주며,  -d 는 detached mode으로 로그가 안 보이기 때문에. -d 옵션을 빼거나 

docker logs -t -f gossip1

같은 명령어로 각 컨테이너들의 stdout 로그를 확인합니다.


4) 다른 노드에 있는 컨테이너 실습 - Overlay Network by etcd

일단 만들어진 이미지를 도커허브에 올려서 각 노드에서 편하게 사용 하려고 합니다.  참고

이제 어디서에나 도커만 있다면 이미지를 다운로드 (이미지 이름이 gossip 에서 wowlsh93/gossip:1 로 변경됨) 받아서 실행 할 수 있습니다.

docker pull wowlsh93/gossip:1

도커(컨테이너)의 장점이 gossip 서비스를 돌리기 위한 어떤 환경(go컴파일러등)이 갖추어져 있지 않더라도, 도커만 있는 곳이라면 실행 할 수 있어서 인프라 설치를 위한 공을 들일 필요가 없는거니까요.

이제 본격적으로 여러 노드에서 gossip분산 서비스를 gossip-network 를 만들어서 실행시켜 보시면 잘 될리가 없습니다. 각 네트워크는 각 노드 내부에서만 활동하기 때문인데요. 외부 네트워크주소를 입력하는 부분도 없잖아요. 당연한거죠. 다른 방법이 필요 합니다. 

여기서 다룰 오버레이 네트워크는 여러 머신에서 각각의 네트워크에서 돌아가는 서비스들이 하나의 네트워크에 있는 것 처럼 만들어 주는 방식(docker swarm, hadoop yarn, apache mesos, kubernetes 등) 으로 다양하게 이루어 질 수 있는데, 여기서는 Docker 자체에서 제공하는 방식으로 만들어 볼 예정입니다.

* 사실 우리 프로그램 경우 그냥 실행시 name 옵션에서 정확한 외부 ip 를 입력해주면 잘 동작합니다. 굳이 이딴거(오버레이/스웜/쿠버네티스) 사용 할 필요도 없긴합니다. 실제 하이퍼레저 패브릭도 마찬가지입니다. 도커,쿠버네티스 같은거 사용 안해도 돌아가는 건 상관이 없어요. 각 호스트에서 디펜던시를 따로 다 설치하는 귀찮음이 있긴 하지만 오히려 첨에 실습해보는데는 더 클리어 하구요. 다만 초기 실습이 아니라 일이 되면 여러모로 추상화/자동화가 편하겠지요. 

도커 없이 ip만 입력해 주면 됨) 
 gossip -name  192.168.0.5:28001 -ip  192.168.0.5 -port 28001  -bootstrap   192.168.0.2:28000 

도커만 사용해도 ip만 입력해 주면 됨) 
 docker run -p 28001:28001 --net host --name  gossip1 --rm wowlsh93/gossip:1 /go/bin/gossip -name   192.168.0.5::28001 -ip   192.168.0.5  -port 28001  -bootstrap  192.168.0.2:28000

                                          (이미지 출처: https://blog.naver.com/alice_k106/220772125819)

위 그림처럼 각각의 서버에 위치하더라도 하나의 네트워크처럼 작동하도록 하기 위해 이 글에서는  etcd (분산 key-value store로 zookeeper 같은 분산코디네이팅 역할을 함) 라는 것을 사용 할 건데요. 5개의 서버를 활용하여 3개에는 도커를 설치하여 우리가 만든 이미지를 다운로드 해주시고, 2개에는 etcd를 설치합니다.  저 같은 경우는 VirtualBox에 Host-only-adapter 방식으로 5개의 호스트 네트워크를 구성하였습니다. bridge 로 해도 되고 서로 ping 날려서 확인만 되면 됩니다. 

etcd 설치 - etcd node 1

ubuntu@docker-node1:~$ wget https://github.com/coreos/etcd/releases/download/v3.0.12/etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node1:~$ tar zxvf etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node1:~$ cd etcd-v3.0.12-linux-amd64

ubuntu@docker-node1:~$ nohup ./etcd --name docker-node1 --initial-advertise-peer-urls http://192.168.205.10:2380 \
--listen-peer-urls http://192.168.205.10:2380 \
--listen-client-urls http://192.168.205.10:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.205.10:2379 \
--initial-cluster-token etcd-cluster \
--initial-cluster docker-node1=http://192.168.205.10:2380,docker-node2=http://192.168.205.11:2380 \
--initial-cluster-state new&


etcd 설치 - etcd node 2

ubuntu@docker-node2:~$ wget https://github.com/coreos/etcd/releases/download/v3.0.12/etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node2:~$ tar zxvf etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node2:~$ cd etcd-v3.0.12-linux-amd64/

ubuntu@docker-node2:~$ nohup ./etcd --name docker-node2 --initial-advertise-peer-urls http://192.168.205.11:2380 \
--listen-peer-urls http://192.168.205.11:2380 \
--listen-client-urls http://192.168.205.11:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.205.11:2379 \
--initial-cluster-token etcd-cluster \
--initial-cluster docker-node1=http://192.168.205.10:2380,docker-node2=http://192.168.205.11:2380 \
--initial-cluster-state new&

ubuntu@docker-node2:~/etcd-v3.0.12-linux-amd64$ ./etcdctl cluster-health
member 21eca106efe4caee is healthy: got healthy result from http://192.168.205.10:2379
member 8614974c83d1cc6d is healthy: got healthy result from http://192.168.205.11:2379
cluster is healthy


ondocker-node1
if docker version >= 17.09

ubuntu@docker-node1:~$ sudo service docker stop ubuntu@docker-node1:~$ sudo /usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-store=etcd://192.168.205.10:2379 --cluster-advertise=192.168.205.12:2375

On docker-node2

ubuntu@docker-node2:~$ sudo service docker stop ubuntu@docker-node2:~$ sudo /usr/bin/docker daemon -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-store=etcd://192.168.205.10:2379 --cluster-advertise=192.168.205.13:2375

On docker-node3

ubuntu@docker-node2:~$ sudo service docker stop ubuntu@docker-node2:~$ sudo /usr/bin/docker daemon -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-store=etcd://192.168.205.10:2379 --cluster-advertise=192.168.205.14:2375


모든 설치가 끝나고, 아무 도커호스트에서 아래처럼 오버레이 네트워크를 만듭니다.

docker network create -d overlay gossip-overlay

모든 노드에서 동일한 네트워크가 만들어 졌음을 확인합니다. 이제 해당 네트워크를 통해서 2)번의 명령어와 동일하게 실행 할 수 있습니다. 물론 네트워크이름은 바꿔야죠.

docker run -it --network=gossip-overlay --name gossip1 gossip /go/bin/gossip -name gossip1:28000 -ip gossip1 -port 28000 -leader
docker run -it --network=gossip-overlay --name gossip2 gossip /go/bin/gossip -name gossip2:28001 -ip gossip2 -port 28001 -bootstrap gossip1:28000
docker run -it --network=gossip-overlay --name gossip3 gossip /go/bin/gossip -name gossip2:28002 -ip gossip3 -port 28002 -bootstrap


5) 다른 노드에 있는 컨테이너 실습 - Docker Swarm

..진행중..


참고:

도커 / 도커허브
https://subicura.com/2017/02/10/docker-guide-for-beginners-create-image-and-deploy.html

도커 네트워크
https://mesosphere.com/blog/networking-docker-containers/

도커 컴포즈 네트워크
https://medium.com/@caysever/docker-compose-network-b86e424fad82

도커 오버레이 네트워크 by etcd
https://docker-k8s-lab.readthedocs.io/en/latest/docker/docker-etcd.html

쿠버네티스 개론 및 실습 동영상
https://www.youtube.com/watch?v=l42GttmnnZ4

쿠버네티스 설치
https://medium.com/@dirty49374/kubeadm%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-kubernetes-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0-ubuntu-18-04-61710f0b4db8



High Performance Go Workshop
- 항상 가능한 최고로 단순하게 코드를 짜라. 컴파일러는 일반적인 코드에 대해 옵티마이즈 된다. 
- 짧은 코드는 빠른 코드이다.
- 짧은 코드는 작은 코드이다,. CPU 캐쉬에 중요하다.
- 할당에 주의를 기울이고, 필요없는 할당을 피하기 위해 노력하라.
- 추측하지말고 보틀넥을 프로파일링 해봐라.

Going Infinite, handling 1M websockets connections in Go



요즘 소위 https 차단(?) 문제로 시끄럽다. 이건 기술적으로는 아무 문제도 없는 것인데.. 내용 살펴보겠다는 것도 아니고..

대중들은 이 기술에 대해 깊숙히 알 수는 없을 것이고, 그냥 먼가 자유를 침해 당한다고 판단하게 만드는 것 같다. 나쁜놈이 사용하면 큰일 나는 것으로...그러니 부정적인 여론 일색 일 수 밖에 없고 진짜 해야 할 논의 진행이 되질 않는다. 그럼 진짜 논의 해야 할 것은 무엇일까? 누구나 알지만 말하지 않는 것인데

성인에게 포르노에 대한 개방을 할 때가 되지 않았냐는 거다. 이런것도 자유롭게 못보게 하니...빡칠수 밖에.. 결국 이런 중요한(?) 것에 대한 논의를 하지 않으니, 불법도박사이트 같은 누구나 불법으로 간주하는것들에 대한 SNI 필터링에 대해 오버하게 되는 것이다. 심각한 자유침해니 뭐니..그냥 문정부는 보수라고 인정하고, 다음엔 더 진보를 뽑던가 하자. 


p.s

위는 개인적인 생각이고, 각자 아래의 내용을 가지고 판단하면 될 거 같다. 물론 정답은 없다.

1. SNI 가 내용인가 아닌가?  (전 NO)
2. 내용이던 아니던, 정부가 개인의 어떤 정보든 엿보는것은 안된다. (전 NO)
3. 성인물에 대한 규제가 필요하다. (전 NO)  


'소프트웨어 사색' 카테고리의 다른 글

'망할' 에이콘 출판사  (2) 2019.04.16
개발자 면접 방식을 바꾸자  (2) 2019.04.12
의존성 주입  (0) 2019.02.09
망할 IBM 클라우드  (0) 2018.12.07
[고전 유머] 프로그래밍 언어별 특징 한줄 요약  (0) 2018.08.24

그거 정말 별거 없는데...ㅎㅎ 

이렇게 쉬운 걸 쓸때없이 어려운 용어/사상/구분짓기로 떡칠을 해서,  오히려 개발자들에게 짐을 지우는게 아닌가 싶은..

그냥 객체(모듈,개체,구조체등등) 이 있다고 할 때, 특정 역할을 외부에서 객체/포인터/함수등 무엇이든 주입받아서 해결하는 방식(근데 애들 관리가 힘들어 질 수도..숨박꼭질 하는 애들 종적찾기란..) 주입받는 방식은 기술,언어마다 다를 수도 있으며 생성자를 이용하던, 세터를 이용하던 채널을 이용하던 소켓이나 파이프를 이용하던, 설정파일에 적혀있던 리플렉션을 이용하던 뭐던간에 자신이 다 구현해서 하드코딩되는 것보다 유연해지겠지요. 

이게 특정 프레임워크나 언어에서 유행했다고 해서, 그것이 정답 및 진정한 xxx 류가 될 수 없으며 그냥 유연하게 설계 하기 위한 모든 스프트웨어 세상에서의 나타나는 일반적인 패턴 정도로 바라보면 편할 듯 싶네요. 

한글 위키백과 보니깐 "의존성 주입(Dependency InjectionDI)은 프로그래밍 에서 구성요소간의 의존 관계가 소스코드  내부가 아닌 외부의 설정파일 등을 통해 정의되게 하는 디자인 패턴  중의 하나이다." 이렇게 나오는데, 누가 정의 했는지 정말 지엽적으로 지껄여 놨구나.... 그냥 그런식으로 하는 방식, 또는  life cycle 까지 관리해주는 프레임워크가 있는것 일뿐..잘못된 정의

아래 장,단점도 가져왔는데요. 전 한번 읽어봤는데 이딴거 읽을 필요도 없을거 같아요. ㅎㅎ
웬만한 프로젝트엔 저 단점이 장점보다 훨씬 크지 않을까 의심스러운데.. 뭐 일일이 확인 해 볼 순 없지만서도..암튼 소는 누가 갈고~코딩은 어느세월에 합니까~이런거에 발목 잡혀서...

문법 실수에 겁내서 말하기를 즐기지 못했더라면  오성식은 유창하게 영어를 할 수 없었다.

장점[edit ]

  • Dependency injection allows a client the flexibility of being configurable. Only the client's behavior is fixed. The client may act on anything that supports the intrinsic interface the client expects.
  • Dependency injection can be used to externalize a system's configuration details into configuration files, allowing the system to be reconfigured without recompilation. Separate configurations can be written for different situations that require different implementations of components. This includes, but is not limited to, testing.
  • Because dependency injection doesn't require any change in code behavior it can be applied to legacy code as a refactoring . The result is clients that are more independent and that are easier to unit test  in isolation using stubs  or mock objects  that simulate other objects not under test. This ease of testing is often the first benefit noticed when using dependency injection.
  • Dependency injection allows a client to remove all knowledge of a concrete implementation that it needs to use. This helps isolate the client from the impact of design changes and defects. It promotes reusability, testability and maintainability.[22] 
  • Reduction of boilerplate code  in the application objects, since all work to initialize or set up dependencies is handled by a provider component.[22] 
  • Dependency injection allows concurrent or independent development. Two developers can independently develop classes  that use each other, while only needing to know the interface the classes will communicate through. Plugins  are often developed by third party shops that never even talk to the developers who created the product that uses the plugins.
  • Dependency Injection decreases coupling between a class and its dependency.[23] [24] 

단점[edit ]

  • Dependency injection creates clients that demand configuration details be supplied by construction code. This can be onerous when obvious defaults are available.
  • Dependency injection can make code difficult to trace (read) because it separates behavior from construction. This means developers must refer to more files to follow how a system performs.
  • Dependency injection frameworks are implemented with reflection or dynamic programming. This can hinder use of IDE automation, such as "find references", "show call hierarchy" and safe refactorings.
  • Dependency injection typically requires more upfront development effort since one can not summon into being something right when and where it is needed but must ask that it be injected and then ensure that it has been injected.
  • Dependency injection forces complexity to move out of classes and into the linkages between classes which might not always be desirable or easily managed.[25] 
  • Dependency injection can encourage dependence on a dependency injection framework.[25] [26] [27] 

술한잔하고 누군가의  잘 모르겠다는 넉두리에 주저리 주저리 해봄.

go언어에서 가장 재밌으며, 강력한 키워드인 select/채널의 다양한 패턴을 살펴봄으로써 우리의 코딩력을 향상시켜 보겠습니다. 이러한 go특유의 핑퐁스타일의 코딩에 빠져들면 헤어나오기 힘들겁니다 :-)

switch 

먼저 형제 관계에 있는 switch를 통해 몸풀기를 좀 하구요.

1. switch (1)

// 일반 switch
func main(){
   i := "korea"
   switch i {
   case "korea":
      fmt.Println("korea")
   case "usa":
      fmt.Println("usa")
   case "japan":
      fmt.Println("japan")
   }
}

switch문은 보통 우리가 생각하는 듯 그러합니다.

2. switch (2)

func main(){
   t := time.Now()
   switch {
   case t.Hour() < 12:
      fmt.Println("It's before noon")
   default:
      fmt.Println("It's after noon")
   }
}

비교문을 케이스에 넣을 수도 있습니다.

3. switch (3)

func WhiteSpace(c rune) bool {
	switch c {
		case ' ', '\t', '\n', '\f', '\r':
		return true
	}
	return false
}

케이스가 리스트가 될 수도 있구요.

4. switch (4)

func main(){

Loop:
   for _, ch := range "a b\nc" {
      switch ch {
      case ' ': // skip space
         break
      case '\n': // break at newline
         break Loop
      default:
         fmt.Printf("%c\n", ch)
      }
   }

}

// a
// b

break 문으로 switch문을 탈출 할 수도 있으며, 지정된 위치까지 탈출도 가능합니다. 위의 Loop로 탈출하면 for문을 벗어나게 됩니다.


select 

select문은 switch와 비슷하지만 case에 채널이 사용됩니다. 덕분에 동기화 코딩을 위해 매우 화려한 코딩을 할 수 있습니다. 예를 보면 바로 느낄 수 있을 꺼에요.


패턴-1

func main() {

   c1 := make(chan string)
   c2 := make(chan string)

   go func() {
      for{
         time.Sleep(5 * time.Second)
         c1 <- "one"
      }
   }()
   go func() {
      for{
         time.Sleep(10 * time.Second)
         c2 <- "two"
      }
   }()

   for{
      fmt.Println("start select------------------")
      select {
      case msg1 := <-c1:
         fmt.Println("received", msg1)
      case msg2 := <-c2:
         fmt.Println("received", msg2)
      }
      fmt.Println("end select-------------------\n\n")
   }
}

case 문에서 채널로부터의 이벤트를 받는데요, 여기서 중요한 것은 case 문의 채널에 값이 들어 올 때 까지 select문에서 블록된다는 점입니다. 앞으로의 모든 패턴들이 이런 느낌에서 출발합니다.

default:
   fmt.Println("default")
}

하지만 이렇게 default문을 넣어주면 블록되지 않고 default문을 처리하고 다시 순회됩니다. 


패턴-2

package main

import (
   "fmt"
   "time"
)
func process(ch chan string) {
   time.Sleep(10 * time.Second)
   ch <- "process successful"
}

func scheduling(){
   //do something
}
func main() {
   ch := make(chan string)
   go process(ch)
   for {
      time.Sleep(1 * time.Second)
      select {
      case v := <-ch:
         fmt.Println("received value: ", v)
         return
      default:
         fmt.Println("no value received")
      }

      scheduling()
   }
}

어떤 생산자가 결과를 줄 때 까지 기다리는 방식의 코드를 구성 할 수 있습니다. idle 타임에는 주구장장 기다리는게 아니라 어떤 다른 로직을 수행 할 수 도 있을 겁니다. 물론 default가 빠지면 계속 기다리도록 할 수도 있을테구요.


패턴-3
func server1(ch chan string) {
	ch <- "from server1"
}
func server2(ch chan string) {
	ch <- "from server2"
}

func main() {
  output1 := make(chan string)
  output2 := make(chan string)
  go server1(output1)
  go server2(output2)
  time.Sleep(1 * time.Second)
  select {
  case s1 := <-output1:
  	fmt.Println(s1)
  case s2 := <-output2:
  	fmt.Println(s2)
  }
}

case s1 이 선택될지, s2가 선택될지는 모릅니다. 랜덤 선택으로 사용 될 수 있습니다.


패턴-4

package main

import (
"fmt"
"time"
)


func consuming (scheduler chan string){
  select {
  case <- scheduler:
  	fmt.Println("이름을 입력받았습니다.")
  case <-time.After(5 * time.Second):
  	fmt.Println("시간이 지났습니다.")
  }
}

func producing(scheduler chan string){
  var name string
  fmt.Print("이름:")
  fmt.Scanln(&name)
  scheduler <- name
}

func main() {
  scheduler := make(chan string)
  go consuming(scheduler)
  go producing(scheduler)

  time.Sleep(100 * time.Second)
}
 case <- scheduler:

scheduler 채널로부터 무엇인가 받아 올 수 있다면 (즉 채널에 무엇인가 입력되었다면) 

 case <-time.After(5 * time.Second):

5초가 지났다면 


패턴-5

func main() {
  scheduler := make(chan string, 1)
  prompt := "HAMA"

  select {
  case scheduler <- prompt:
 	 fmt.Println("이름은: ", <-scheduler)
  case <-time.After(time.Second):
 	 fmt.Println("시간이 지났습니다.")
  }

  time.Sleep(100 * time.Second)
}

채널에 값이 들어가는 것으로도 case 문을 충족시킵니다.


패턴-6

package main

import (
"fmt"
"time"
)

var scheduler chan string

func consuming (prompt string){ fmt.Println("consuming 호출됨")
  select {
  case scheduler <- prompt:
  	fmt.Println("이름을 입력받았습니다 : ", <- scheduler)
  case <-time.After(5 * time.Second):
 	 fmt.Println("시간이 지났습니다.")
  }
}

func producing (console chan string) {
  var name string
  fmt.Print("이름:")
  fmt.Scanln(&name)
  console <- name
}
func main() {
  console := make(chan string, 1)
  scheduler = make(chan string, 1)

  go func(){
  consuming(<-console)
  }()

  go producing(console)

  time.Sleep(100 * time.Second)
}

조금 더 응용하여, 아래와 같은 함수에 매개변수로 

func consuming (prompt string){
.. }
consuming(<-console)

이런식으로 넣어주는 방식도 있습니다. 참고로 이런 방식일 경우 <- console을 통하여 string 매개변수를 받지 못하는 동안에는 consuming 함수 자체가 블록됩니다. 


패턴-7

wg := sync.WaitGroup{}
errC := make(chan error)
done := make(chan bool, maxParallelFiles)
for i, entry := range list {
   select {
   case done <- true:
      wg.Add(1)
   case <-quitC:
      return fmt.Errorf("aborted")
   }
   go func(i int, entry *downloadListEntry) {
      defer wg.Done()
      err := retrieveToFile(quitC, fs.api.fileStore, entry.addr, entry.path)
      if err != nil {
         select {
         case errC <- err:
         case <-quitC:
         }
         return
      }
      <-done
   }(i, entry)
}

for문을 주구장창 도는게 아니라, list만큼만 돌면서 내부의 고루틴을 처리하는데, 위에 select문으로 스케쥴링을 하고 있습니다. done채널에는 maxParallelFiles 만큼만 true가 들어 갈 수 있기 때문에 처리량이 꽉 차면 case done <- true에서 블록되고 다음 고루틴을 시작하지 않게 됩니다. 고루틴 내부의 <- done 을 통해 버퍼가 해소되면 진행됩니다.


패턴-8

package main

import "fmt"
import "time"

func main() {

  requestChan := make(chan chan string)

  go goroutineC(requestChan)
  go goroutineD(requestChan)

  time.Sleep(time.Second)

}

func goroutineC(requestChan chan chan string) {

  responseChan := make(chan string)
  requestChan <- responseChan
  response := <-responseChan

  fmt.Printf("Response: %v\n", response)
}

func goroutineD(requestChan chan chan string) {
  responseChan := <-requestChan
  responseChan <- "wassup!"
}

채널에 채널을 넣습니다.
즉 생산자가 소비자한테 채널을 통해 데이터를 보내는게 아니라, 소비자가 생산자한테 이제 나 준비됬으니깐, 니가 만든 것을 내가 준 채널을 통해서 보내줘~ 라는 의미입니다. 적극적인 소비자지요. 

아래처럼 select문에서 처리 될 수 있습니다.

collectSig         chan chan string

collectSig는 string 타입의 채널의 채널로 정의됩니다.

// 리모트 peer로 부터 내 피어 정보가 요청됨.

func (p * Peer) collect() {
  cs := make(chan string)
  p.node.pm.collectSig <- cs
  collected_peers := <- cs
  ....
  }

  collectSig에 string 타입의 채널을 태워 보냅니다.

  func (pm * PeerManager) run(){
    for {
      select {
      case collchan := <- pm.collectSig:
          collchan <- pm.getCollect() 
    }
    time.Sleep(10*time.Millisecond)
  }
}

case문에서 collchan 을 통해 string타입의 채널을 받아서, 그 채널에 getCollect를 통해 얻은 데이터를 보내줍니다.


패턴-8

checkInterrupt := func() bool {
   select {
   case <-stop:
      return true
   default:
      return false
   }
}

stop 이라는 이벤트가 있으면 true 아니면 false를 반환하는 함수 


패턴-9

func (t *udp) handleReply(from NodeID, ptype byte, req packet) bool {
   matched := make(chan bool, 1)
   select {
   case t.gotreply <- reply{from, ptype, req, matched}:
      // loop will handle it
      return <-matched
   case <-t.closing:
      return false
   }
}

t.gotreply 채널에 reply 개체를 넣어줍니다. t.gotreply 채널의 버퍼가 충분해서 들어가면 matched 채널을 통해 데이터가 들어 올 때까지 기다렸다가 리턴해 줍니다. 아마도 t.gotreply 채널에 들어온 reply개체를 통해서 어떤 작업을 하다가 결과로 matched 채널에 값을 넣어 주겠지요. 대기중에 closing 이벤트가 먼저 날라오면 false를 리턴하네요. 


패턴-10

ticker := time.NewTicker(time.Millisecond * 50)

for {
  select {
  case <-ticker.C:

  	//... 50초에 한번씩 어떤 작업을 합니다 ...

  case <-done:
 	 return
  }
}

주기적으로 실행되는 로직을 위한 코드입니다. done 채널을 통해 멈출 수도 있습니다.


패턴-11

select {
case <-notifier.Closed():
   return
default:
}

함수 호출을 통해서 특정 채널을 리턴 받고, 그 채널에 값이 들어오는 순간을 기다릴수 있습니다.


패턴-12

func (p *Peer) handle(msg Msg) error {
  switch {
  case msg.Code == pingMsg:
  	msg.Discard()
  	go SendItems(p.rw, pongMsg)
  case msg.Code == discMsg:
  	var reason [1]DiscReason
  	// This is the last message. We don't need to discard or
  	// check errors because, the connection will be closed after it.
  	rlp.Decode(msg.Payload, &reason)
  	return reason[0]
  case msg.Code < baseProtocolLength:
  	// ignore other base protocol messages
  	return msg.Discard()
  default:
  	// it's a subprotocol message
  	proto, err := p.getProto(msg.Code)
  	if err != nil {
  		return fmt.Errorf("msg code out of range: %v", msg.Code)
  	}
  	select {
  	case proto.in <- msg:
  		return nil
  	case <-p.closed:
  		return io.EOF
  	}
  }
  return nil
}

switch 문의 default 문 안에 select 를 위치 시킵니다. msg가 어떤 프로토콜을 위한 것인데 알아낸후에 해당 프로토콜의 in 채널에 msg를 넣어주고 있습니다.


패턴-13

func (rw *protoRW) WriteMsg(msg Msg) (err error) {
  select {
  case <-rw.wstart:
  	err = rw.w.WriteMsg(msg)
  	rw.werr <- err
  case <-rw.closed:
  	err = ErrShuttingDown
  }
  return err
}

wstart 채널에 값이 들어와서 쓰기가 가능해지면 씁니다. 다 쓰고 난 후에는  werr 채널에 알려주어서 다음 쓰기가 가능하게 합니다. 이 코드에는 안보이지만 다음 쓰기가 가능하게 하는 방법은 wstart 채널에 값을 넣는 것입니다.


패턴-14

func (p *MsgPipeRW) WriteMsg(msg Msg) error {
  if atomic.LoadInt32(p.closed) == 0 {
    consumed := make(chan struct{}, 1)
    msg.Payload = &eofSignal{msg.Payload, msg.Size, consumed}
    select {
    case p.w <- msg:
      if msg.Size > 0 {
      // wait for payload read or discard
      select {
     	 case <-consumed:
      	 case <-p.closing:
      	}
      }
      return nil
    case <-p.closing:
    }
  }
  return ErrPipeClosed
}

select 안에 select가 들어 갈 수 도 있습니다. msg를 받아서, p.w채널에 넣어주는데 성공한후에 p.w 채널이 받아서 무엇인가 처리 할 때까지 다음 select문에서 <- consumed: 를 통해서 기다리게 됩니다. 이런 것은 보통 여러 모듈이 공평하게 OS의 I/O를 나누어 같기 위해 사용됩니다. framming이라고도하죠.


패턴-15

type peerOpFunc func(map[discover.NodeID]*Peer)
peerOp     chan peerOpFunc
// PeerCount returns the number of connected peers.
func (srv *Server) PeerCount() int {
   var count int
   select {
   case srv.peerOp <- func(ps map[discover.NodeID]*Peer) { count = len(ps) }:
      <-srv.peerOpDone
   case <-srv.quit:
   }
   return count
}


채널에는 함수자체를 넣을 수 도 있습니다. 따라서 관심사의 분리/의존성 주입을 이렇게 할 수 있게 되지요.



레퍼런스:

https://github.com/ethereum/go-ethereum 
https://golangbot.com/select/ 

       go로 간략히 구현한 블록전파/Gossip Protocol

지난 글(400라인의 go코드로 구현한 하이퍼레저 패브릭)에 이어 이번에는 하이퍼레저 패브릭의 블록전파/가쉽프로토콜을 추가 구현 해 보는 시간을 갖겠습니다. 이번 설연휴동안 여유있게 코딩했는데 역시나 세세한 부분이 많이 생략되습니다만 중요맥락에 대해서는 대략 비슷하게 동작되게 하는 목표로 구현하였습니다. (더 줄일 수도 있었지만, 앞으로 추가될 기능들을 위한 기반으로 600라인이 넘어갔습니다.) 

1개의 리더피어와 여러개의 일반 Commit 피어가 작동하게 될 것이며, 각 피어는 포트로 구분 될 것입니다. 리더피어가 오더러에게 메세지를 10초에 한번씩 가져와서 몇개의 피어에만 전파시키면 결국은 모든 피어가 모두 동일한 데이터를 담고 있게 할 것입니다. 시작되는 피어는 부트스트랩노드에서 초기 피어정보를 가져올 것이며, 30초에 한번씩 주변 노드로부터 노드정보들을 가져와서 업데이트 할 것입니다. 중간에 몇몇개의 피어가 멈추어도 동작 할 것이며,  그 피어 중 하나를 다시 살리면, (혹은 새로운 피어가 들어가도) 그 피어는 그 동안의 모든 데이터를 이웃으로 부터 받게 될 것입니다. 리더피어가 멈추면, 리더를 선출하게 됩니다. 

400 라인 하이퍼레저 패브릭 구성 요소 복습 

* Endorse Peer  -  클라이언트가 발생시킨 트랜잭션을 계산/보증한 후 Read/Write Set을 리턴
* Commit  Peer -  Orderer가 보내준 블럭을 장부(블록체인 및 상태저장소) 에 기록
* Orderer         Read/Write Set 을 받아 정렬 한후 블록으로 만들어 Commit Peer에 전달.
* Kafaka           -  Orderer를 도와서 정렬작업을 합니다. 
* Fabric CA       -  각 Peer 와 사용자(조직)등에 대한 암호화 재료를 만들어 주며, CA의 역할을 합니다.
* MSP             -  각 조직및 사용자에 대한 신원검증을 처리 합니다.
* Ledger          -  append only인 블록체인과 상태저장소를 가지고 있습니다.
* LevelDB         -  key,value 맵으로 상태를 저장하고 있습니다.

심플하게 구현된 하이퍼레저 패브릭은 위의 구성요소로 되어 있으며, 6번 오더러(O)에서 리드피어로 블록을 주면, 리드피어는 블록을 저장하고 끝났습니다. 근데 원래는 리드피어(C)는 자신의 동료 Commit피어들에게 가쉽프로토콜로 블록을 전파하거든요. 그 부분이 이번에 새로 추가한 코드입니다. 

해당 부분을 위해 간략하게 정리한 아이디어는 아래와 같습니다. (참고로 완전하고 효율적으로 가쉽프로토콜을 작성하는것은 매우 어려운 도전적인 과제같습니다. 참고로 저는 잘 모름) 

블록전파/가쉽프로토콜 아이디어 정리

1. 암호화 핸드쉐이크 (TLS)  - 생략
2. 프로토콜 핸드쉐이크  - 생략
3. 스트리밍 인코딩/디코딩 - 생략
4. UDP 및 NAT Traversal - 퍼블릭 블록체인 아니면 필요가 없음. 
5. 서로의 이름만 확인 후 간략한 패킷정의로 TCP 스트리밍으로 통신 (RPC사용안함)

6. 부트스트랩 피어/리더 피어/ 오더러 설정 - 하드코딩 /  채널,조직 등 추상개념 제외 
7. 각 피어는 시작시부트스트랩 피어에 접속하여, 부트스트랩 피어들이 가지고 있는 다른 피어정보들을 가져와서 저장.
8. 3초에 한번씩 자신이 살아있다는 정보를 모든 피어에 알림.- 생략
9. 자신이 가지고 있는 피어에게 alive메세지가 5초안에 안날라오면, V에서 제거하여 H로 이동.- 생략
10. Alive메세지를 받으면 그게 내 V에 없는 피어라면 V에 넣는다. 있는데 seq,시작시간가 둘다 크면 업데이트. - 생략
11. 리더에게 alive메세지가 안오면, 피어들은 부트스트랩에 랜덤으로 리더로 선출해서 서로에게 알림. 모든 피어는 서로의 투표를 받아서, 가장 많이 투표된 피어를 선출하고, 그 피어는 스스로 리더피어가되서 오더러에서 데이터를 가져옴. - 생략

12. 리더피어는 5초에 한번씩 블록을 가져옴. (오더로에서 가져왔다고 시뮬레이션함.실제 연결안함)
13  부트스트랩으로 부터 초기 연결정보를 가져오고, 후에는 30초에 한번씩 주변피어정보를 수집함. 
14. 수집된 피어 중 자신이 가지고 있지 않은 피어에 즉시 연결 요청함. 
15. 블록 받으면 이웃노드들중 랜덤 3개에 전파 (Push모델) 
16. 한번 받은 블록을 기록해두고, 똑같은 것을 받으면 버림. (같은 블록 처리를 방지) 
17. 램덤 Push 모델로는 구멍이 생기기 마련, Pull로 자신의 구멍을 메우는 호출도 함. 생략
17. 새로 발견된 이웃이 블록을 적게 가지고 있으면 블록 전파  - 생략
18. 새로 접속한 피어는 주변에 alive메세지와 함께 블록 정보도 뭍혀서 날림. 주변 피어들은 자신들의 블록 id를 리턴해주고 랜덤선택하여 블록 요청 (가장 큰 블록 id를 가진 피어한테 요청하면 그 피어가 병목이 될 수도) - 생략
19. 자신의 블록id와 2이상 차이가 날 경우에만 블록 요청 - 생략

구성도

이런식으로 리더피어가 동채널/동조직 commit피어집단에 모두 전파하는게 아니며 


  이런식으로 주변 랜덤n개에만 전파하면 각각 그들도 주변에 전파합니다. 

Node : 서비스의 메인모듈이며, 리모트피어로 부터의 접속을 받아드리고, 접속을 수행하는 역할을 합니다. 최종연결된 피어에 대해 단독개체를 만들어서 PeerManager에게 전달합니다.
PeerManager: 리모트 피어들의 생존을 관리하며, 브로드케스트 같은 전체 피어에게 행하는 행위를 합니다.
Peer : 리모트 피어 각각과 연결된 개체입니다.

소스코드

https://github.com/wowlsh93/hyperledger-fabric-400-gossip

실행
터미널 7개 정도 띄운후 
터미널1:  go run peer.go -name 127.0.0.1:28000 -ip 127.0.0.1 -port 28000 -leader
터미널2:  go run peer.go -name 127.0.0.1:28001 -ip 127.0.0.1 -port 28001 -bootstrap 127.0.0.1:28000
터미널3:  go run peer.go -name 127.0.0.1:28002 -ip 127.0.0.1 -port 28002 -bootstrap 127.0.0.1:28000
터미널4:  go run peer.go -name 127.0.0.1:28003 -ip 127.0.0.1 -port 28003 -bootstrap 127.0.0.1:28000
터미널5:  go run peer.go -name 127.0.0.1:28004 -ip 127.0.0.1 -port 28004 -bootstrap 127.0.0.1:28000
터미널6:  go run peer.go -name 127.0.0.1:28005 -ip 127.0.0.1 -port 28005 -bootstrap 127.0.0.1:28000
터미널7:  go run peer.go -name 127.0.0.1:28006 -ip 127.0.0.1 -port 28006 -bootstrap 127.0.0.1:28000


* 리드피어는 오더러에서 블록을 가져 온다.


* 이웃피어로부터 정보를 수집한다. 

* 블록을 주변에 전파한다.  (전파하는 와중에도 동일한 블록이 들어 올 수 있으나, 이미 받은 블록이면 버림) 


- 다음 시리즈 

400라인의 go코드로 구현한 하이퍼레저 패브릭 [3]- 도커/쿠버네이트로 디플로이



읽을꺼리
https://www.ibm.com/developerworks/community/wikis/form/anonymous/api/wiki/0be57f75-2769-40b2-9ea4-99fbec0f9073/page/5145e8c8-4833-4e5b-b623-d73d579c562e/attachment/e978b030-1d7a-4f76-8ad6-ef3dde66c409/media/Fabric%E4%BB%A3%E7%A0%81%E8%A7%A3%E6%9E%90%E7%AC%AC%E4%BA%8C%E8%AE%B2.pdf
https://hyperledger-fabric.readthedocs.io/en/release-1.4/gossip.html
https://jira.hyperledger.org/browse/FAB-170
http://www.inf.u-szeged.hu/~jelasity/ddm/gossip.pdf
http://disi.unitn.it/~montreso/ds/papers/montresor17.pdf
https://munin.uit.no/bitstream/handle/10037/13115/thesis.pdf?sequence=2&isAllowed=y

+ Recent posts