Lab 1 MapReduce

Go 언어로 MapReduce를 구현해보자.

Go 언어로 MapReduce의 논문 내용을 구현해보자. 아래 내용은 실시간성 시행착오


  • …일단 제일 큰 이슈. 뭐부터 해야하지?
    • 먼저 rpc.go에서 요청/응답 형태를 만들고, 응답이 왔을때 Map작업을 하는거까지부터 만들어보자.
      • 여기서 ExampleArgs가 있어서 찾아보니까, 요청/응답을 Args, Reply로 하는게 관례인 것 같다.
      • 따라서 우리도 TaskArgs, TaskReply 로 해볼까?
      • 일단 TaskReply에서, 무슨 정보가 필요할까?
        • 어떤 작업인지 알려줘야하고 (Map or Reduce)
          • 여기서 파이선 enum같은게 가능할까? 두 string중 하나로 정리하고싶은데. 아니면 bool로 할까?
        • 어떤 입력파일을 처리해야할지 알려줘야 하고
          • 이건 Map, Reduce 둘다 작업 번호로 처리할 수 있을 것 같다
        • 이 두가지면 될거같은데?
    • 이후 worker.go에서 이를 받아줘야할 것 같다.
      • CallExample이 예시로 있으니, 우리도 CallTask로 이름짓고 하면 될듯
    • 아 이게 다 붙어있네. CallTaskcoordinator.go에서 잘 반환해줘야하는데..
      • Coordinator struct에서 Task의 개수와 어디까지 보냈는지같은걸 잘 관리해야겠다
  • 실제로 실행하면서 한번 해보자.
    • go run mrcoordinator.go sock123 pg-*.txt 로 코디네이터를 만들면
      • coordinator.goMakeCoordinator 이 호출되고,
    • go run mrworker.go wc.so sock123 로 워커를 만들면
      • worker.goWorker 함수가 호출된다.
      • 구조를 보니 map, reduce 함수는 만들어져있다.
    • 두 함수를 잘 채워주자!
    • Image Description
    • 일단 동시성 없이, Map요청을 우선으로 응답하고 Reduce연산을 응답하는 구조를 만들었다.
  • 이제 작업을 붙여야한다.
    • 지금 하다보니, ExampleReply에서 filename도 필요할 것 같다.
    • 이후 mrsequential.go에서 하는것과 같이 file을 읽어들여서 mapf 함수를 먹이면, 다음과 같이 잘 수행됨을 확인할 수 있다!
    • Image Description
  • 이제 이 결과를 nReduce개의 중간 파일에 저장하자.
    • 아잇;; NReduce도 RPC에서 전달받아야했다.
    • Image Description
    • 잘 데려와서 진행하면, 중간 버킷을 이용해서 위와 같이 mr-M-R 파일명으로 모두 저장해줄 수 있다!
  • Reduce 함수도 같은 방식으로 만들어주면 된다.
    • Image Description
    • 깔끔하게 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같은걸 이용해보자.
      • 이거 문제가 아닌데..
  • 아니;; 다른 원인을 찾다보니 Reduce 작업에서 조금 실수해서 완료처리를 잘못하고 있었다.
    • 이걸 고치고 깔끔하게 완료! Image Description
  • 고친후에 깔끔히 만점!

알게된 사실들

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