Go 언어로 MapReduce의 논문 내용을 구현해보자. 아래 내용은 실시간성 시행착오
- …일단 제일 큰 이슈. 뭐부터 해야하지?
- 먼저
rpc.go에서 요청/응답 형태를 만들고, 응답이 왔을때 Map작업을 하는거까지부터 만들어보자.- 여기서
ExampleArgs가 있어서 찾아보니까, 요청/응답을Args,Reply로 하는게 관례인 것 같다. - 따라서 우리도
TaskArgs,TaskReply로 해볼까? - 일단
TaskReply에서, 무슨 정보가 필요할까?- 어떤 작업인지 알려줘야하고 (Map or Reduce)
- 여기서 파이선 enum같은게 가능할까? 두 string중 하나로 정리하고싶은데. 아니면 bool로 할까?
- 어떤 입력파일을 처리해야할지 알려줘야 하고
- 이건 Map, Reduce 둘다 작업 번호로 처리할 수 있을 것 같다
- 이 두가지면 될거같은데?
- 어떤 작업인지 알려줘야하고 (Map or Reduce)
- 여기서
- 이후
worker.go에서 이를 받아줘야할 것 같다.CallExample이 예시로 있으니, 우리도CallTask로 이름짓고 하면 될듯
- 아 이게 다 붙어있네.
CallTask는coordinator.go에서 잘 반환해줘야하는데..Coordinatorstruct에서 Task의 개수와 어디까지 보냈는지같은걸 잘 관리해야겠다
- 먼저
- 실제로 실행하면서 한번 해보자.
go run mrcoordinator.go sock123 pg-*.txt로 코디네이터를 만들면coordinator.go의MakeCoordinator이 호출되고,
go run mrworker.go wc.so sock123로 워커를 만들면worker.go의Worker함수가 호출된다.- 구조를 보니 map, reduce 함수는 만들어져있다.
- 두 함수를 잘 채워주자!

- 일단 동시성 없이, Map요청을 우선으로 응답하고 Reduce연산을 응답하는 구조를 만들었다.
- 이제 작업을 붙여야한다.
- 지금 하다보니,
ExampleReply에서filename도 필요할 것 같다. - 이후
mrsequential.go에서 하는것과 같이 file을 읽어들여서mapf함수를 먹이면, 다음과 같이 잘 수행됨을 확인할 수 있다! 
- 지금 하다보니,
- 이제 이 결과를 nReduce개의 중간 파일에 저장하자.
- 아잇;; NReduce도 RPC에서 전달받아야했다.

- 잘 데려와서 진행하면, 중간 버킷을 이용해서 위와 같이
mr-M-R파일명으로 모두 저장해줄 수 있다!
- Reduce 함수도 같은 방식으로 만들어주면 된다.

- 깔끔하게 10개의 out파일이 잘 나온다!
- 이제 자랑스럽게
make mr을 돌려보면, 다음과같은 오류들을 볼 수 있다.
WARNING: DATA RACE
Read at 0x00c00013f1f0 by main goroutine:
6.5840/mr.(*Coordinator).Done()
- 이는 채점 환경이 Worker을 3개 돌리는 고루틴 환경이기 때문!
- 우리는 이제 동시성도 제어해야한다.
- mutex같은걸로 보호하면서 수행해보자.
- 뭘 보호해야하지?
- DATA RACE가 나는 상황을 보면, TaskRPC에서 Coordinator을 참조할때 문제가 생기는 것 같다.
- 따라서 TaskRPC 하나가 돌동안 잠궈주면 되지 않을까?
- 여기까지 잘 하면
--- PASS: TestWc (7.95s)를 일단 받을 수 있었다!
- 이제 문제는 실패한 태스트 혹은 할당의 문제..
- 현재 구조에서는 한번 보낸 태스크들은 모두 버그없이 완료되었다고 생각한다.
- Midx정도로만 관리했기 때문
- 이제 현재 업무가 어떤 상태인지도 정의해야할 것 같은데..
- 두가지 아이디어가 생각난다.
- 첫번째는 배열/슬라이스로, $[M_1, M_2, M_3, ..., M_{m-1}, M_M]$ 에 대해 각 $(0, 1, 2)$ 같은 값을 부여해서, 할당 없음 / 작업중 / 작업 완료 세가지로 나누는 것이다!
- 두번째는 set.. 이지만 Go에는 없으므로 map같은걸로 위의 세가지 그룹에 대한 집합을 관리하는 것이다.
- 뭐가됐든 RPC는 하나가 더 필요할 것 같다. 성공을 관리하는거!
- 그리고 set은 어떻게 구현할지 바로 생각이 안나니까 일단 슬라이스로 시작해보자.
- 두가지 아이디어가 생각난다.
- 현재 구조에서는 한번 보낸 태스크들은 모두 버그없이 완료되었다고 생각한다.
- 슬라이스로 관리하고, 함수 하나를 정의해야해서 일이 있으면 해당 인덱스, 할일은 없지만 작업중인 일이 남아있으면 -1, 모든 일이 끝났으면 -2를 반환하도록 하였다.
- 추가적으로 실수가 있었던 kv레코드 / json인코딩 저장 방식까지 수정하니까 (공식 가이드에있었음) 다음과 같이 꽤 좋은 결과를 얻을 수 있었다!
=== RUN TestWc
--- PASS: TestWc (10.08s)
=== RUN TestIndexer
--- PASS: TestIndexer (5.87s)
=== RUN TestMapParallel
--- PASS: TestMapParallel (8.04s)
=== RUN TestReduceParallel
--- PASS: TestReduceParallel (10.05s)
=== RUN TestJobCount
--- PASS: TestJobCount (12.06s)
=== RUN TestEarlyExit
cmp: EOF on mr-wc-initial.txt
util.go:163: reduce output changed: mr-wc-initial.txt vs mr-wc-final.txt
--- FAIL: TestEarlyExit (7.05s)
=== RUN TestCrashWorker
^Csignal: interrupt
FAIL 6.5840/mr 263.011s
make: *** [mr] Error 1
- 아마도 EarlyExit, CrashWorker 둘다 이름에서 알 수 있다시피 내결함성에 관한걸 테스트해야할 것 같다.
- 만약에 worker이 일을 물고 죽었다면? 어떡하지? 같은거지.
- 이것도 사이트를 보면 10초정도 후에 백업 태스크가 살아야한다고 알려준다. 이를 구현해보자.
- 저걸 해서 CrashWorker까진 고쳤지만 EarlyExit이 안돈다! 이게 무슨 일일까?
- EarlyExit은 worker가 Task를 받자마자 죽어버리는거같은 일이다.
- 그런데 아예 죽지 않고 살아있다면?
- 그래서 동시에 두 Worker이 같은 일을 하고있어서,
os.Create를 같이 써버린다면?- 분명 내용이 유실되겠지!
- 그렇다면, 임시파일에 쭈루룩 쓰다가 모두 쓴 다음에
os.Rename같은걸 이용해보자.- 이거 문제가 아닌데..
- EarlyExit은 worker가 Task를 받자마자 죽어버리는거같은 일이다.
- 아니;; 다른 원인을 찾다보니 Reduce 작업에서 조금 실수해서 완료처리를 잘못하고 있었다.
- 이걸 고치고 깔끔하게 완료!

- 이걸 고치고 깔끔하게 완료!
- 고친후에 깔끔히 만점!
알게된 사실들
- struct 내부의 메서드를 대문자로 시작하지 않으면 다른데서 접근조차 안된다! 저장부터 안들어가는듯
- Mutex Lock를 걸면 자동으로 해당 데이터에 대한 접근을 막는게 아니라, 다음 Mutex의 Lock까지를 대기한다.
- 따라서 쓰기에서만 Lock을 걸면 안되고, 읽을때도 하나하나 다 걸어줘야한다!
- 함수
func1이 함수func2를 호출하는 경우, 그리고 둘다c를 사용한다고 생각해보자.- 이때 둘다 Mutex Lock을 걸어버리면, Deadlock이 발생한다.
- 따라서, 이럴때는 내부 헬퍼함수같은 느낌으로 진행할 수 있겠다.
- 이때 헬퍼함수는 뒤에 Locked같은 접미사를 붙이는게 관례라고 한다.
- 공식 가이드에도 있는 내용인데, json으로 저장하기 위해선 k-v 텍스트 레코드대신 json 인코딩된 레코드로 저장하는게 좋다고 한다. 왜지?
- 텍스트 포맷은 구분자에 의존한다 (개행)
- 그런데 Key나 Value에 이미 그런 구분자가 들어있다면?
- 따라서 json형태로 저장하면 훨씬 안전하다!