2022.11.01 ~ 2022.12.13
약 한달간 진행했던 프로젝트가 끝났다.
우리가 만든 EyesTalk서비스를 정리해보자
WebRTC 는 P2P ( peer to peer Network) 로 단말간 데이터를 주고받는 표준기술이다. 웹/앱에서 카메라, 마이크를 이용하여 실시간 커뮤니케이션을 제공한다.
WebRTC를 사용한 이유
- P2P 방식으로 미디어 스트림을 송수신 할 수 있는 유일한 표준이기 때문이다
- P2P 방식이 아닌 다른 방식으로도 미디어 스트림을 송수신 할 수 있는 방식이 존재하기는 하지만, 오픈소스가 아니기 때문에 라이센스가 필요 없다
- WebRTC 의 가장 큰 장점 - 실시간에 가까운 낮은 지연시간 때문이다
Mesh 연결방식을 사용한 이유
MCU는 실시간성 저해라는 치명적인 단점 때문에 고려하지 않았고, Mesh와 SFU 방식중에 고민했다
- SFU의 가장 큰 단점 - 서버 비용에 대한 부담
SFU는 더 많은 클라이언트끼리의 통신을 할 수 있다라는 장점이 있지만, 그만큼 서버의 트래픽이 늘어난다. 즉, 중앙 미디어 서버에 대한 비용이 가장 큰 부담으로 생각되었다.- Mesh 형태의 장점 - 낮은 서버 비용과 실시간에 가까운 낮은 지연시간
Mesh의 가장 큰 장점 2가지를 뽑자면 낮은 서버 비용과 실시간에 가까운 낮은 지연시간이다. 이러한 두가지 장점이 현재 우리상황에서 가장 알맞는 장점이라는 생각이 들었다.
일정 관리를 보면 2주이상 webRTC이론 이해가 할애된 것을 볼 수 있다.
WebRTC의 연결방식과, 어떤 기술스택을 사용하는지 시행착오를 겪고 난 뒤 본격적으로 서비스를 제작하게되었다.시행착오를 겪으면서 작성한 벨로그
https://velog.io/@jupiter-j/Project-webrtc-ec2-실행
https://velog.io/@jupiter-j/프로젝트-수행-webRTC-ec2업로드2
https://velog.io/@jupiter-j/프로젝트-수행-webRTC-ec2업로드3
11/11 처음으로 조원 모두가 카메라에 나왔다
EyesTalk에 사용된 기술 스택이다
react, WebRTC, SocketIO
python, Flask, SocketIO
Springboot
MariaDB, Redis
Google-Coturn
서비스 흐름
검증 요청을 하기 위해 SpringBoot를 사용했다. 검증요청이 성공하면 클라이언트의 브라우저에서 socket-join event가 발생한다
이때 시그널링 서버는 소켓 ID와 기타 정보들을 Spring API에 전송해 유저에 대한 데이터들을 데이터 베이스에 전송한다
이후 소켓 ID와 stun 서버를 사용하여 서로의 ip, port등 Peer to Peer 커넥션에 필요한 정보를 얻고 커넥션이 맺어진다.
NAT의 보안레벨에 따라 성공하지 못할 때가 있는데, 이때는 구글의 Coturn을 사용한 우리 서버를 사용하여 통신이 가능하도록 만들어준다Turn Server의 역할
일반적인 sturn 서버만 있어도 Peer 끼리 Connection 을 맺을 수 있지만, 일부 특수한 상황에 의해 sturn 서버만으로 연결을 할 수 없는 경우가 있다. 이러한 경우 connection 을 중개해줄 서버가 필요한데, 이러한 역할을 하는것이 Turn 서버다. Turn 서버는 구글에서 제공하는 오픈소스인 coturn 서버를 사용하여 사설 turn 서버를 구축했다
Redis가 사용된 이유
EyesTalk 서비스는 화상통화와 채팅서비스가 있다. 여기서 채팅서버는 클라이언트가 채팅방 안에 속해있는 다른 참여자들에게 메시지를 전달하기 위한 공통 통신 채널이 필요하다. 이를 구축하기위해 Redis가 사용되었다.
Signarling Server 는
python, Flask, Socket.Io, Redis
를 사용했다
- 초기 WebRTC 커넥션 하기 위한 정보인 socketId 를 반환한다
- 같은방에 있는 유저들끼리 채팅을 하기 위한 socket event 로직을 처리한다
- 채팅에 대한 메타데이터를 ElasticSearch 에 저장하기위한 로직을 처리한다
event | 이름 | 설명 |
---|---|---|
create-room | 유저 방 생성 | 유저가 방을 생성하면 client로 부터 이벤트를 수신, 방 정보를Spring API 서버로 전송, 방 생성 메타데이터 elasticsearch 로 저장 |
join-room | 유저 방 입장 | 유저가 방을 입장할때 유저에 대한 정보를 Client로부터 수신, 이후 유저정보를 SpringAPI 서버로 전송 및 response로 받은 user_list에 대한 로직 처리 및 client로 전송, 유저 입장 메타데이터를 Elasticsearch에 저장 |
disconnect | 유저 퇴장 | 유저가 방을 나갈때 SpringAPI 에 유저 정보 삭제 요청, 유저 퇴장 메타데이터를 Elasticsearch 로 저장 |
data | connection 데이터 송수신 | WebRTC connection 을 맺기위한 peer 데이터 송수신 |
chatting | 채팅 송수신 | Client로 부터 받은 채팅데이터를 ElasticSearch에 저장 |
Front Server는
react, WebRTC, SocketIO
를 사용했다
- https://eyestalk.site로 접근 시 MainPage 렌더링한다
socket.connect()
메서드를 통해 connect가 시작된다- 사용자 정보를 담고 세션에 저장한다. 새로운 멤버가 접속 할 때마다
rooms_sid[자신의 소켓 아이디]
의 값을 room_id,names_sid[자신의 소켓 아이디]
의 값을 유저 이름으로 할당한다.user-list
에는 방의 모든 인원에 대한 소켓 아이디가 담아져있다.- 소켓 연결이 된 이후
start_webrtc()
를 시작한다. 연결된 적이 없다면 인자로 받아온 peer_id를 이용하여_peer_list[peer_id]에 RTCPeerConnection(PC_CONFIG)
를 할당한다. 이 때 PC_CONFIG는 google ice 서버의 주소를 사용한다컴포넌트 구조
Backend Server는
SpringBoot
를 사용했다
- Signarling Server가 커넥션을 맺기 전에 유저 검증 및 방 생성 검증, 데이터베이스에 저장을 하기 위해 사용된다
- Swagger을 통하여 프론트와 실시간 API 확인가능하다
기능 | Method | URI | Input Vlaue | Output Value |
---|---|---|---|---|
방 생성 | GET | /room | userNickname, roomName, roomPassword,roomCapacity | userNickname, roomId, roomName, roomPassword,roomCapacity, roomEnterUser, roomCreateAt |
방 검색 | POST | /room/{roomName} | roomId, roomName, roomPassword, roomCapacity,roomEnterUser, roomCreateAt | |
방 입장 + 유저 생성 | PATCH | /room/enter | userNickname, roomName, roomPassword,roomCapacity, socketId | socketId:userNickname |
사용자 나가기 + 없으면 방 종료 | POST | /room/exit?socketId={socketId} | ||
사용자 방생성 검증 | POST | /room/valid/create | roomName | True/False |
사용자 입장 검증 | POST | /room/valid/enter | userNickname, roomId,roomPassword | password_error, capacity_error, nickname_error |
전체 방 조회 | GET | /room | roomId, roomName, roomPassword, roomCapacity,roomEnterUser, roomCreateAt |
방 생성 | 사용자는 방 제목(roomName), 수용인원(roomCapactiy)을 입력하여 회의실를 생성할 수 있다. 이때 사용자의 데이터도 작성해야 한다 닉네임(userNickName) |
---|---|
room 테이블에 저장되는 데이터 {roomId, roomName(100), roomCapacity, roomPassword(100), roomCreateAt, roomEnterUser}user 테이블에 저장되는 데이터 {userId, userNickName, userCreateAt, roomEntity} | |
1. 동일한 방 이름이 있는지 체크한다. 중복이 있을경우, DUPLICATE_RESOURCE 에러코드 반환 | |
2. 동일한 닉네임으로 생성된 룸이 있는지 체크한다, 중복이 있을 경우 INVALID_NICKNAME 에러코드 반환 | |
3. 위의 모든 조건이 충족되면 유저정보를 저장한다이때 방 정보에서 현재 방에 입장한 인원(roomEnterUser)값을 1 증가시킨다변경된 방 정보를 저장후 저장된 데이터들을 반환한다 | |
방 검색 | 사용자는 방 이름(roomName)으로 방을 검색할 수 있다. |
1. 방 이름이 없는 경우 ROOM_IS_EMPTY에러코드 반환 | |
2. 방 이름을 찾으면 데이터 값을 반환한다 | |
방 입장 | 사용자는 닉네임(userNickName), 비밀번호(userPassword)를 입력하여 방에 입장할 수 있다. 이때 PatchVariable로 roomId값이 필요하다 |
1. 방 아이디가 있는지 체크한다. 없을경우, ROOM_IS_EMPTY 에러코드 반환 | |
2. 방 비밀번호가 맞는지 체크한다. 맞지 않을경우, INVALID_PASSWORD 에러코드 반환 | |
3. 동일한 닉네임으로 생성된 룸이 있는지 체크한다, 중복이 있을 경우 INVALID_NICKNAME 에러코드 반환 | |
4. 수용인원과 현재 입장한 인원수를 비교한다. 수용인원이 찼을경우 FULL_CAPACITY 에러코드 반환 | |
5. 위의 모든 조건이 만족되면 유저를 생성하고 저장한 유저의 값과 방의 데이터를 반환한다 | |
사용자 나가기 & 방 삭제 | 현재 방에 입장한 인원(roomEnterUser)이 1일때 사용자 나가기 요청이 오면 방은 사용자와 함께 자동으로 삭제된다 입장한 인원이 2명이상 남아 있을 때 사용자만 나가게된다 |
1. 방 아이디(roomId)가 없을경우 ROOM_IS_EMPTY 에러코드 반환 | |
2. 유저 아이디 (userId)가 없을경우 MEMBER_NOT_FOUND 에러코드 반환 | |
3. 현재 방에 입장한 인원(roomEnterUser)이 1보다 클때 방의 입장한 인원을 1명 줄이고 update된 방 정보를 저장 후 유저의 값을 삭제한다 입장인원이 1보다 작을경우, 유저와 방을 모두 삭제한다 | |
방 생성 검증 | 사용자가 확인버튼을 누르면 방을 생성할 수 있는지 검증한다. |
1. 방이름이 중복되는 경우에는 false 를 반환 | |
2. 방이름이 중복되지 않는다면 True 를 반환 | |
방 입장 검증 | 사용자가 확인버튼을 누르면 방을 입장할 수 있는지 검증한다. |
1. 같은방에 이미 같은 닉네임의 사용자가 있다면 nickname_error 를 반환한다. | |
2. 방의 정원이 이미 가득차있는 상태라면 capacity_error 를 반환한다. | |
3. 방의 비밀번호를 틀렸다면 password_error 를 반환한다. | |
4. 1~3 의 조건을 통과했다면 200 반환 | |
전체 방 조회 | 전체방 리스트를 조회한다. |
자세한 내용 확인
WebRTC는 HTTPS환경에서 카메라가 작동되기 때문에 Route53과 ACM을 사용하여 도메인을 인증받았다
자세한 내용 확인
VPC네트워크는 Terraform
을, 클러스터 구축은 Cloudformation
을 사용하여 구축했다.
서울 리전 내에 가용영역을 ap-northeast-2a, ap-northeast-2b, ap-northeast-2c, ap-northeast-2d로 구성했다. VPC CIDR 대역을 10.2.0.0/16으로 구성하였으며 subnet은 public subnet4개(10.2.1.0/24, 10.2.2.0/24, 10.2.3.0/24, 10.2.4.0/24), private subnet4개(10.2.16.0/22, 10.2.20.0/22, 10.2.24.0/22, 10.2.28.0/22)로 구성되어있다
1.23버전의 eks 사용, m5.large 타입의 인스턴스를 사용하여 워커노드를 4개 사용했다. (최소사이즈:4, 최대 사이즈:10, 각각의 volume size: 50)
개발자의 개발 접속 환경을 만들어 주기 위하여 northeast-2a-public-subnet에 baston host를 생성했다. 나머지 조원은 Cloud9을 사용해 접속하여 작업했다.
인스턴스 타입
- m5.large는 ElasticSearch가 올라가면서 파드 용량 부족으로 사용하게 되었다
(노드당 평균 18개를 사용함)- c6g.medium은 Signarling Server의 네트워크 대역폭 부족으로 사용하게 되었다
네임스페이스(NameSpace)를 활용하여 Pod관리
- ArgoCD, Prometheus 등 특정 파드를 묶어 관리하기 위해 사용했다
- ArgoCd Namespace
자세한 내용 확인
EKS 성능 모니터링(CloudWatch)과 애플리케이션의 로그 모니터링(Kibana)을 나눠서 구축했다
- EKS성능을 확인 할 수 있는 모니터링을 CloudWatch에 구축했다.
- helm으로 Promethus, Dashboard를 설치하였으나 CloudWatch가 이를 대신하고 있기 때문에 발표에서는 제외시키기로함.
- Distro를 설치하여 CloudWatch에서 Container Insight로 배포된 애플리케이션을 모니터링이 가능하다
- 비용 절감을 위해서 CPU사용 일정 수준이 넘어가면 Amazon Simple Notification Service(SNS)를 사용하여 메일이 오도록 했다
Container Insight
Cloud Watch 대시보드
CloudWatch 경보 알람 메일 확인
- 원하는 로그가 발생되고 있는 python 코드에 수집하고 싶은 로그를 저장한 후 인덱스를 만들어서 elasticSearch에 저장되도록 했다
- 수집한 로그를 시각화하기 위해 elasticSearch와 kibana를 연동해 로그 대시보드를 커스텀하여 모니터링했다
- ElasticSearch replicas:1, Fluentd replicas:1, Kibana: Daemonset 을 사용하여 구축
- Kibana 대시보드를 만들어 24 시간 내 생성된 방 사용량, 방 생성 수, 서비스 접속인원, room별 채팅 개수, room 별 채팅 내역을 볼 수 있도록 만들었다
로그 | 설명 |
---|---|
des | 수집한 로그에 대한 설명 (create room, New member joined, user-disconnect,chatting) |
user_nickname | 사용자가 방에 입장할 때 설정한 닉네임 |
chatting message | 사용자가 보낸 채팅 메시지 (RSA 암호화 처리됨) |
room_id | 방 생성 시 설정된 방 이름 |
@timestamp | 로그가 발생한 시간 |
대시보드 | 설명 |
---|---|
24H 방 생성 사용량 | 24시간동안 어느 시간대에 방이 많이 생성되는 지를 확인할 수 있다. |
Today 방 생성 수 | 하루동안 방이 얼마나 생성되는 지를 확인할 수 있다. |
room 별 채팅 개수 | 방에서 채팅의 수가 어느정도 되는지 확인할 수 있다. |
room 별 채팅 내역 | RSA 암호화 됨 |
Today 서비스 접속 인원 | 하루동안 해당 서비스에 몇명의 사람이 접속하는지 확인할 수 있다. |
자세한 내용 확인
Eyestalk 프로젝트는 서버 환경이 복잡한 만큼 에러를 해결하기 위하여 실시간으로 개발 코드가 수정이 되었다. EKS환경에서 빠른 테스트를 하기 위해 CI/CD 파이프라인을 구축했다
GithubAction, Kustomize, ArgoCD, ECR을 사용했다
자세한 내용 확인
EyesTalk에는 기존의 Application 구조에서 볼 수 없었던 Redis 와 별도의 ALB 가 추가되었다
Sig Deployment 와 Front Deployment 가 별도의 ALB 를 가지고 있다. 별도의 ALB 를 둔 이유는 Sig Deployment 에서 주로 사용중인 socketIO 를 위한 Sticky Session 옵션을 사용하기 위해서다Sticky Session을 사용한 이유
우리가 사용한 socketIO 라이브러리는 socket 끼리 제대로 커넥션이 맺혀져 있는지 10초에 한번씩 소켓서버에 pooling 을 보내게된다. 여기서 문제는 실제 커넥션이 맺혀져 있는 서버가 아닌 다른 서버로 Connection 확인작업을 하게되면서 400에러가 떴다
서로 다른 서버가 pooling 요청을 받기때문에 해당 요청이 비정상적인 요청(HTTP 400), 즉 커넥션이 끊겨있다고 판단하고 새로운 커넥션을 맺기를 시도한다. 이러한 과정이 지속적으로 반복되기 때문에 socket 의 connection 이 제대로 맺혀있는지 판단할수가 없다. 따라서 socketIO 의 공식문서에서는 분산서버에서 이러한 문제점을 해결하기 위한 방법으로 sticky session과 기존 pooling 요청을 wss로 변경하라는 옵션이 있다
코드의 수정을 최소한으로 하고 해당 문제를 해결하기 위해서 StickySession을 적용시키는 방식으로 선택했다alb.ingress.kubernetes.io/target-group-atrtributes: stickiness.enabled=true,stickiness.lb_cookie.duration_seconds=3600 alb.ingress.kubernetes.io/target-type: ip # using IP routing policy of ALB
EyesTalk의 변천사...
파란색
채팅으로 보인다회색
이다붉은색
이다일정대로라면 EyesTalk 서비스는 k8s 환경에서부터 EKS환경까지 서비스를 구축하는 것이었다. 하지만 커넥션 문제로 인해 k8s환경은 구축하였으나 서비스 연결부분에 문제가 생겨 EKS환경으로 바로 넘어간게 아쉽다.
또한 WebRTC에 대한 문제를 빨리 파악했다면 일정관리를 좀더 여유롭게 잡을 수 있었을 텐데 WebRTC연결방식과 이론적인 이해로 시간을 상당부분 할애하게 되었다.
연쇄적인 문제로 뒤로갈수록 시간이 부족했기 때문에 전체적인 EKS성능을 테스트하고 고도화 시키지 못한게 아쉽다.
그럼에도 불구하고 팀원 모두가 처음인 기술을 맡아도 열심히 노력했고, 팀워크가 좋았기 때문에 이렇게 완성도 있는 화상통화 서비스를 만들 수 있었다고 생각한다
EyesTalk 우리팀 모두 한달간 너무 고생했다 별다섯개 꾹꾹 ⭐⭐⭐⭐⭐
EyesTalk 프로젝트 주소 https://github.com/muji-StudyRoom