개요기술 스택진행 계획진행 내역1주차: 2023.07.17 ~ 2023.07.232주차: 2023.07.24 ~ 2023.07.303주차: 2023.07.31 ~ 2023.08.064주차: 2023.08.07 ~ 2023.08.135주차: 2023.08.14 ~ 2023.08.20스터디 종료 이후: ~2023.09.17Rust 관련 공유 내용클로저utoipa 크레이트 사용법Rust 벡터 사용법 예시크레이트 의존성 충돌 해결
개요
Apache Ballista를 활용하여 구글 BigQuery 클론 만들기
- REST API → Apache Flight RPC 프록시 서버를 Rust 마이크로 서비스로 구현
기술 스택
- 프론트엔드
- 개발 언어 및 프레임워크: NextJS (Typescript)
- 인프라: Vercel
- 인증 (Optional): NextAuth (Google, Github, Naver, Kakao)
- 백엔드
- 개발 언어 및 프레임워크: Actix Web (Rust)
- 인프라: AWS
- API Gateway
- EKS
- S3
진행 계획
- 개인 k8s 클러스터에 Ballista 설치 및 설정
- 쿼리 웹 인터페이스 만들기 (NextJS)
- REST API 스키마 디자인 (Swagger Editor)
- Actix Web으로 REST API 서버 구현
- 인프라 아키텍처링 및 코드 작성 (AWS, Terraform)
- 통합해서 빅쿼리 같은 웹 서비스 개발 (단순 쿼리 실행 및 결과 표시)
진행 내역
1주차: 2023.07.17 ~ 2023.07.23
- Git 원격 저장소 생성 및 설정
- rsquery-web (프론트엔드)
- rsquery-restapi-server (REST API 마이크로서비스)
- rsquery-infra (인프라) → 비용 문제로 프트젝트 후반부에 셋팅
- 프론트엔드
- 도메인 설정: rsquery.taehun.dev
- Vercel Github 연동: rsquery-web Github 저장소와 Vercel 프로젝트 연동
- 백엔드
- Actix Web 템플릿 코드 작성
- Actix Web 학습
2주차: 2023.07.24 ~ 2023.07.30
- 프론트엔드
- 프로젝트에 사용할 패키지 선정
- React Textarea Code Editor → SQL 쿼리 에디터 UI
- tremor → Dashboard UI
- 백엔드
- Actix Web 학습 → 문서 번역하면서 학습하고 있습니다.
- Apache Ballista 빌드 및 배포 (0.11.0) → 참고 문서: Developer's Guide
public.ecr.aws/t7j3q4u1/arrow-ballista-benchmarks:0.11.0
public.ecr.aws/t7j3q4u1/arrow-ballista-cli:0.11.0
public.ecr.aws/t7j3q4u1/arrow-ballista-executor:0.11.0
public.ecr.aws/t7j3q4u1/arrow-ballista-scheduler:0.11.0
public.ecr.aws/t7j3q4u1/arrow-ballista-standalone:0.11.0
- REST API 엔드포인트에 Swagger 문서 자동 생성 적용
- utoipa 크레이트 사용
3주차: 2023.07.31 ~ 2023.08.06
- Actix Web 학습 → 문서 번역하면서 학습하고 있습니다.
- Apache Ballista k8s 클러스터내 설치
- Apache Ballista 예제 돌려보기
- Standalone 환경 잘됨
- Distributed 환경 (k8s)에서 예제 Job이 동작 하지 않는 이슈 트래킹 중…
4주차: 2023.08.07 ~ 2023.08.13
- Actix Web 학습 → 문서 번역 완료
- 백엔드
- REST API 인터페이스 요청/응답 스키마 정리
- Ballista Distributed 환경 (k8s)에서 예제 Job이 동작 하지 않는 이슈 해결
- 예제를 실행하는 환경에서 Worker IP로 접속 하도록 되어 있었음
- Ex> 실행 환경: 로컬, 설치 환경: k8s → 로컬에서 예제 실행시 Worker의 k8s 클러스터 IP로 접속 시도 → 로컬에서 k8s 클러스터 IP 접속이 안됨
- 프론트엔드
- UI 레이아웃 구현 (SQL 쿼리 에디터)
5주차: 2023.08.14 ~ 2023.08.20
- 백엔드
- 백엔드 인프라 설정 및 배포
- AWS 인프라: EKS, ECR, ALB, ….
- Swagger 문서: https://rsquery-api.taehun.dev/swagger-ui/
- 크레이트 의존성 이슈 해결
- 샘플 응답 메세지 추가
- 프론트엔드
- 백엔드 연동
- 응답 메세지 파싱 및 표시 추가
스터디 종료 이후: ~2023.09.17
- 백엔드
- Apache Ballista 크레이트 의존성 문제
- → 배포된 크레이트 (X), Github 최신
main
브랜치 코드로 변경 - Github r4ntix 유저의 PR이 많은 도움이 되었음 (datafusion 버전업 PR)
- ballista 0.11.0 의존성 크레이트가 구버전이라 다른 크레이트와 충돌 발생
- RSQuery 백엔드 ↔ Apache Ballista 연동
- Ballista Context를 actix-web AppState로 관리
- Ballista 스케쥴러 URL을 환경 변수로 설정 가능하게 추가
- Dockerfile 업데이트
- 크레이트 의존성이 늘어나면서,
distroless
Base 이미지 사용 불가 - Base 이미지
ubuntu
로 변경 - REST API 응답 포맷 업데이트
- Apache Ballista 쿼리 실행 결과 포맷에 맞춤
- S3 파일 로드시
MissingRegion
이슈 - Ballista Client (RSQuery 백엔드), Ballista Scheduler, Ballista Executor 모두 AWS Region 설정 환경 변수가 필요함 →
AWS_DEFAULT_REGION
환경 변수
- 프론트엔드
- UI 수정
- 테이블 형태 결과와 메세지 형태 결과 모두 표시 가능
- 변경된 응답 메세지 포맷에 맞게 파싱 루틴 업데이트
- 단축키 (cmd + Enter) 추가 시도
e.preventDefault()
가 왜 적용이 안될까?cmd + Enter
키 입력시 핸들러 실행은 되지만, 쿼리 입력 박스에 엔터키가 입력됨
- 인프라
- EKS Fargate → Graviton Spot 인스턴스
- 백엔드 배포 타겟 아키텍처가 x86 → arm64로 변경됨
- 데모 영상 녹화
- S3에 있는 CSV 원본 데이터에서 테이블 생성후 간단한 쿼리 실행
Rust 관련 공유 내용
클로저
- 자신의 환경을 캡처하는 익명 함수
- 변수에 저장하거나 다른 함수에 인자로 전달 할 수 있는 익명 함수
- 정의된 스코프(문맥)에서 값을 캡처
클로저 예제
#[derive(Debug, PartialEq, Copy, Clone)] enum ShirtColor { Red, Blue, } struct Inventory { shirts: Vec<ShirtColor>, } impl Inventory { fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor { /// self.most_stocked()를 클로저로 넘김. /// Option의 unwrap_or_else() 메소드는 클로저를 파라메터로 받음 /// -> https://doc.rust-lang.org/src/core/option.rs.html#973 user_preference.unwrap_or_else(|| self.most_stocked()) } fn most_stocked(&self) -> ShirtColor { let mut num_red = 0; let mut num_blue = 0; for color in &self.shirts { match color { ShirtColor::Red => num_red += 1, ShirtColor::Blue => num_blue += 1, } } if num_red > num_blue { ShirtColor::Red } else { ShirtColor::Blue } } } fn main() { let store = Inventory { shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue], }; let user_pref1 = Some(ShirtColor::Red); let giveaway1 = store.giveaway(user_pref1); println!( "The user with preference {:?} gets {:?}", user_pref1, giveaway1 ); let user_pref2 = None; let giveaway2 = store.giveaway(user_pref2); println!( "The user with preference {:?} gets {:?}", user_pref2, giveaway2 ); }
fn add_one_v1 (x: u32) -> u32 { x + 1 } /// 함수 let add_one_v2 = |x: u32| -> u32 { x + 1 }; /// 파라메터와 리턴 타입 명시적 클로저 let add_one_v3 = |x| { x + 1 }; /// 타입 명시가 없는 클로저 let add_one_v4 = |x| x + 1; /// 중괄호 없는 클로저
→ 타입 명시를 하지 않더라도, 사용하는 측의 자료형으로 컴파일러에 의해 정의됨. 서로 다른 타입으로 사용 불가
let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5); // Error!
클로저는 사용하는 곳의 값을 참조로 소유권을 대여함
다시보는 Rust의 참조 규칙
• Rust는 단 하나의 mutable한 참조 또는 여러개의 non-mutable 참조만 허용하며, 둘 다는 안됨.
fn main() { let mut list = vec![1, 2, 3]; println!("Before defining closure: {:?}", list); let only_borrows = || println!("From closure: {:?}", list); println!("Before calling only_borrows: {:?}", list); only_borrows(); println!("After calling only_borrows: {:?}", list); let mut borrows_mutably = || list.push(7); borrows_mutably(); println!("After calling borrows_mutably: {:?}", list); }
클로저로 소유권을 이전할때는
move
키워드 사용. → 클로저를 새 스레드에 넘길 때 데이터를 이동시켜서 새로운 스레드가 이 데이터를 소유하도록 할 때 많이 사용
use std::thread; fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {:?}", list); thread::spawn(move || println!("From thread: {:?}", list)) .join() .unwrap(); }
utoipa 크레이트 사용법
Cargo.toml
파일에 관련 의존성 크레이트를 추가 합니다.
[dependencies] serde_json = "1.0" utoipa = { version = "3.4.3", features = ["actix_extras"] } utoipa-swagger-ui = { version = "3.1.4", features = ["actix-web"] }
요청 스키마 struct에 utoipa
ToSchema
어노테이션을 추가합니다.model.rs
예시
use utoipa::ToSchema; (......) #[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] /// ToSchema 어노테이션 추가 pub struct Todo { pub id: Option<String>, pub title: String, pub content: String, pub completed: Option<bool>, #[schema(value_type = Option<String>)] pub createdAt: Option<DateTime<Utc>>, #[schema(value_type = Option<String>)] pub updatedAt: Option<DateTime<Utc>>, } (......)
응답 스키마 struct에도 utoipa
ToSchema
어노테이션을 추가합니다.response.rs
예시
use utoipa::ToSchema; (......) #[derive(Serialize, Deserialize, Clone, ToSchema)] pub(super) enum ErrorResponse { NotFound(String), Conflict(String), Unauthorized(String), }
각 핸들러에
#[utoipa::path
어노테이션을 추가합니다. 주석으로 문서화를 할 수도 있습니다.handler.rs
예시
/// Get list of todos. /// /// List todos from in-memory todo store. /// /// One could call the api endpoint with following curl. /// ```text /// curl http://localhost:8080/api/todo /// ``` #[utoipa::path( path = "/api/todos", responses( (status = 200, description = "List current todo items", body = [Todo]) ) )] #[get("/todos")] pub async fn todos_list_handler( (......)
main
함수에 OpenAPI 어노테이션을 추가하여 API 문서 객체를 생성하고, swagger 문서 경로를 추가합니다.use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; async fn main() -> std::io::Result<()> { (......) #[derive(OpenApi)] #[openapi( paths( handler::todos_list_handler, handler::create_todo_handler, handler::get_todo_handler, handler::edit_todo_handler, handler::delete_todo_handler, ), components( schemas(model::Todo, model::UpdateTodoSchema, response::ErrorResponse) ), tags( (name = "todo", description = "Todo management endpoints.") ), )] struct ApiDoc; let openapi: utoipa::openapi::OpenApi = ApiDoc::openapi(); HttpServer::new(move || { (......) .service( SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()), ) });
실행후 Swagger 문서 경로는
<엔드포인트 URL>/swagger-ui/
입니다.$ cargo run
- ref> https://github.com/juhaku/utoipa/tree/master/examples
utoipa
크레이트 예제들
→ RSQuery 프로젝트의
utoipa
로 Swagger 문서 자동 생성 커밋만 뽑은 것 입니다.Rust 벡터 사용법 예시
Leetcode의 Array 관련 문제를 Rust로 풀어보면서 배운 내용 입니다.
- 기본 예제
let mut vec = Vec::new(); vec.push(1); vec.push(2); assert_eq!(vec.len(), 2); assert_eq!(vec[0], 1); assert_eq!(vec.pop(), Some(2)); assert_eq!(vec.len(), 1); vec[0] = 7; assert_eq!(vec[0], 7); vec.extend([1, 2, 3]); for x in &vec { println!("{x}"); } assert_eq!(vec, [7, 1, 2, 3]);
vec!
매크로 예제let mut vec1 = vec![1, 2, 3]; vec1.push(4); let vec2 = Vec::from([1, 2, 3, 4]); assert_eq!(vec1, vec2);
벡터내 중복 제거
impl Solution { pub fn remove_element(nums: &mut Vec<i32>, val: i32) -> i32 { nums.retain(|&x| x != val); // () 안의 조건에 해당되는 원소만 '남김' nums.len() as i32 } }
impl Solution { pub fn remove_duplicates(nums: &mut Vec<i32>) -> i32 { nums.dedup(); // 벡터내 연속적으로 반복되는 원소를 제거 nums.len() as i32 } }
크레이트 의존성 충돌 해결
사용하는 크레이트가 늘어나면서 의존성 문제가 발생하였습니다.
$ cargo build Updating crates.io index error: failed to select a version for `proc-macro2`. ... required by package `syn v2.0.25` ... which satisfies dependency `syn = "^2.0.25"` of package `serde_derive v1.0.172` ... which satisfies dependency `serde_derive = "=1.0.172"` of package `serde v1.0.172` ... which satisfies dependency `serde = "^1.0.171"` of package `rsquery-restapi-server v0.1.0 (/Users/taehun/Projects/rsquery/rsquery-restapi-server)` versions that meet the requirements `^1.0.62` are: 1.0.66, 1.0.65, 1.0.64, 1.0.63 all possible versions conflict with previously selected packages. previously selected package `proc-macro2 v1.0.50` ... which satisfies dependency `proc-macro2 = "=1.0.50"` of package `arrow-flight v32.0.0` ... which satisfies dependency `arrow-flight = "^32.0.0"` of package `ballista-core v0.11.0` ... which satisfies dependency `ballista-core = "^0.11.0"` of package `ballista v0.11.0` ... which satisfies dependency `ballista = "^0.11.0"` of package `rsquery-restapi-server v0.1.0 (/Users/taehun/Projects/rsquery/rsquery-restapi-server)` failed to select a version for `proc-macro2` which could resolve this conflict
에러 메세지에 나와있듯이,
arrow-flight v32.0.0
크레이트에서 proc-macro2
버전이 1.0.50
으로 고정되어 있어서, 해당 크레이트를 사용하는 다른 크레이트 (serde
)의 의존성 (proc-macro2=”1.0.62”
)를 충족하지 못해서 발생하였습니다.해결 방법
최신 버전을 요구하는 크레이트(
serde
)의 버전을 다운그레이드 했습니다.