12 ~ 1월 업무일지

공부는 혼자하는 거·2024년 1월 30일
0

업무

목록 보기
13/15

알리바바 클라우드 포팅

기존에 AWS로 호스팅하는 서비스를 알리바바 클라우드로 포팅하는 작업이 가장 메인이다. 중국 쪽 서비스를 진행할려면, 비안이 필요한데. 중국 내에 위치한 서버에게만 도메인 주소로 비안이 발급되기 때문. 그 외에도 행정적인 문제가 참 많은데, 아무튼 결국 정상적으로 서비스를 할려면 중국 내로 서버를 전진배치시키는 방법 외에는 없다고 결론나왔다. 이 과정에서 중국의 AWS라는 알리바바 클라우드를 사용하기로 결정했다.

서비스를 포팅하는 작업이 알리바바 클라우드에 문외한인 2명 뿐 (나 포함) 이라는 게 처음에는 막막했다. 기존 AWS로 구축했던 서비스를 최대한 비슷하게 알리로 포팅해야됐다. 다행히 파트너사와 계약을 맺어 자문을 구할 수 있었다.

대응되는 서비스 찾기

중국 내 리전으로 먼저 만들면, 비안이 발급되지 않은 상태에서 테스트를 하다 자칫 GFW에 의해 차단될 수 있다는 파트너사의 조언으로 홍콩 리전에 먼저 인프라를 구축하고 후에 상해 리전으로 이미지를 떠서 복붙하는 식으로 진행하기로 했다. 대략적인 AWS와 대응되는 서비스 맵핑은 아래링크에서 비교할 수 있다.

https://osamaoracle.com/2020/06/19/services-mapping-aws-azure-gcp-oc-ibm-and-alibab-cloud/

이걸 기준으로 대략적인 맵핑 서비스를 뽑아냈다.

1. EC2  ⇒ ECS
2. RDS (AURORA MYSQL) ⇒ Polar DB
3. NAT 게이트웨이 ⇒ NAT GW
4. SES([Amazon Simple Email Service] ⇒ Direct Mail
5. SNS([Simple Notification Service] ⇒ MNS 
6. S3 ⇒ OSS
7. Load Balncer ⇒ SLB
8. CloudFront ⇒ CDN
9. MediaConvert ⇒ [ApsaraVideo for Media Processing (MPS)]
10. elasticache ⇒ alibaba Redis
11. ACM([Certificate Manager] ⇒  SSL Certificate  
12. Route53 ⇒ Alibaba Cloud DNS
13. CodeDeploy ⇒ EDAS
14. AWS Auto Scalling ⇒  EDAS Auto Scalling
15. IAM => RAM

대부분 AWS와 비스무리해서 크게 포팅하는 데 어려움이 있지는 않았다. 다만 UI가 훨씬 더 구리다. AWS도 UI 구리다고 욕 많이 먹는데, AWS와 비교해서도 훨씬 더 구리다. 2000년대 초반 감성을 느낄 수가 있다. 암튼 비슷하기에 차이점이 눈에 더 들어오게 되는데, Internet Gateway 가 그렇다. 알리바바에서는 별도로 IG를 설정하는 부분이 없고, ECS 를 만들면 기본적으로 Private 망 에 위치한다. 외부 인터넷 망에 노출시키면 별도의 EIP 를 할당하던가, CDN이나 LB에 붙이는 식이다. OSS는 Data Management - Static Page 에서 기본 홈페이지 경로를 설정할 수 있다.

VPN 서버 셋팅 (WireGuard)

Private 망에 위치한 ECS들에게 접근하기 위해 중간에 public 망의 ECS 하나를 띄우고 VPN 서버를 설치해 그걸 통해 접속하기로 했다. OpenVpn 및 여러가지 옵션을 고려하다. WireGuard가 대세라길래 설치했다. 편하게 셋팅하기 위해서, 웹 UI를 지원하는 wg-easy docker image를 받아서 설치했다.

https://github.com/wg-easy/wg-easy

EDAS

무중단 배포 파이프라인을 구축하러 CodeDeploy와 비슷한 솔루션을 찾아 헤메다, 비슷한 걸로 EDAS가 있다. EDAS에서 ASG를 만들어도 되고 ECS 란에 별도로 ASG 그룹 생성란이 있는데 뭔 차이인지 모르겠다. Alibaba Cli을 통해서, 깃허브 액션에 aliyun/setup-aliyun-cli-action@v1 패키지를 사용해 액션에서 바로 ali cli를 사용할 수 있다.

MPS

HLS 포맷으로 트랜스코딩하는데는 MPS 솔루션을 사용했다. MNS TOPIC을 통해서 트랜스코딩 결과값을 콜백받게 했다.

FileNotFoundException, multipartfile.transferto(file)

이건 또 희한한 장애라서 기록해둔다. 내부적으로 multipart form 으로 파일을 받으면, 임시로 파일 객체를 만들고 거기로 멀티파트파일 내용을 옮기는데 나중에 만든 임시파일 객체를 가져올려니 File이 없다고 런타임 에러가 뜨는 게 아닌가?

File.createTempFile() 메서드는 사용하지 않았다. 파일이름을 기존포맷과 공통적으로 통일시켜줘야 돼서, 아무튼 아래와 같이 바꿔주었다.

https://deonggi.tistory.com/123

https://stackoverflow.com/questions/76286304/spring-boot-multipartfile-transferto-not-working

AWS SI 적용

1년 견적 뽑고 EC2 Saving Plan 적용

NLB 포트 포워딩

포트포워딩할려면 해당 포워딩하려는 EC2에 그 포트도 보안그룹에서 열어줘야 됨. 처음에는 몰라서, 살짝 당황했음.

Route53 - CloudFront - LB - Backend API

정적 콘텐츠를 서빙하는데 있어 CDN 이 효율적이라는 거는 이해가 되는 부분이다. 그런데 동적 컨텐츠를 내려주는 Backend API 까지 CloudFront를 앞단에 놓아야 될지는 모르겠다. 그래서 이때까지, 정적파일은 CDN을 사용했지만, Backend API 서버는 ALB로 바로 호스팅하도록 설정해주었다. 그러다 API 성능 개선을 위해서 여러가지 아티클을 읽어보던 중, 연결시켜주는 게 여러모로 나은 방향이라고 알게 되었다.

https://www.reddit.com/r/aws/comments/jelo7r/should_i_one_put_their_application_load_balancer/

https://stackoverflow.com/questions/53655625/what-is-the-benefit-of-adding-aws-cloudfront-on-top-of-aws-application-lb/67815119#67815119

https://www.reddit.com/r/aws/comments/jelo7r/should_i_one_put_their_application_load_balancer/

결론적으로 말하자면 CloudFront는 아무것도 캐싱하지 않더라도 전 세계 네트워크의 지연 시간을 개선할 수 있다는 거다. CloudFront를 사용하면 지리적으로 가장 가까운 CloudFront 상호 접속 위치(Point of Presence)로 라우팅된 다음 AWS 네트워킹 인프라를 통해 전송된다. CloudFront를 사용하지 않고 직접 ALB로 쏠 경우, 각 요청이 ISP를 통해 라우팅되며 트래픽 요금도 alb로 직접 들어올 경우에는 DataTransfer로 분류되지만, cf 통해서 들어오면 cf 별도 요금제를 타기 때문에 cfrc 같은 할인 제도를 통해서 비용적인 면에서도 이득이다. 또한 WAF 같은 방화벽 설정하기도 더 용이하다. 기존 LB에도 따로 붙일 수 있지만, 리전마다 다르면, 따로따로 설정해줘야 되는 반면, CF에서는 전역적으로 붙이고 관리의 포인트가 한 곳으로 좁혀지기 때문에 이 또한 장점이다. 그래서 기존 ALB들을 다 앞단에 CF를 두도록 고쳤다.

캐시정책

원본요청 정책

쿼리 문자열 모두 포함 선택해라.. 갑자기 전달 안 되서 놀랐네..

응답헤더 정책

Managed-CORS-With-Preflight

희한한 건 WAF를 적용하면 multipart/form-data API 가 안 된다. 원인은 아직 모르겠다. 일단 해당 서버 방화벽 WAF 해제.. 원인은 아래 링크에

https://stackoverflow.com/questions/64853122/aws-waf-getting-403-forbidden-error-while-trying-to-upload-an-image

https://repost.aws/knowledge-center/waf-explicit-allow-file-uploads

grafana login loop.. cdn으로 grafana 서버 앞단에 달아줬더니 login 성공해도 다시 login page로.. 아 짜증나

CodeDeploy - TG 이슈

codeDeploy Blue Green 배포를 실행했다. 테스트까지 끝낸 뒤, 퇴근을 했는데, 같은 VPC 내부의 배포된 서버의 주소로 통신하는 다른 서버들이 있는데, 통신장애를 겪었다. 알고보니 CodeDeploy 옵션에 깜빡하고, TG 그룹을 추가하지 않은 것이다. 테스트를 할 당시에는 기존 그룹이 아직 종료되기 전이라, 성공했다고 착각한 것이다. 어차피 같은 DB를 사용하니, 결과 값은 종료 예정인 원본 서버그룹이 받아서 업데이트를 한 것이고, 사소한 실수가 큰 서버장애로 이루어질 수 있다..

File Upload

또 한가지 주요 이슈는 파일 업로드에 대한 속도의 문제였다. 내 서버에는 Multipart-form으로 파일을 업로드받는 API 가 있는데, 이 부분에서 네트워크 병목지점이 눈에 띄게 드러났다. 만든 API는 글로벌 국가를 대상으로 서비스하는 데, 서버는 정작 서울리전 하나에서 모두 커버를 치고 있으니 당연한 일이다. 목표는 서버를 증설시키지 않고, 최대한 해당 조건 내에서 파일 업로드의 시간을 단축하는 거다. 관련 이슈로 검색을 하다가 Multipart Upload 라는 키워드가 눈에 들어왔다.

https://techblog.woowahan.com/11392/

https://develop-writing.tistory.com/129

https://velog.io/@sangwoo-sean/spring-AWS-S3-Multipart-Upload

https://aws.amazon.com/ko/blogs/compute/uploading-large-objects-to-amazon-s3-using-multipart-upload-and-transfer-acceleration/

S3 Multipart Upload 병렬처리

AWS 공식문서에서는 100MB 이상의 파일을 업로드 할 때, Multipart Upload를 권장하고 있다. 나는 그 정도까지는 아니더라도, 네트워크 지연률을 생각해서, 여러개의 파일을 청크로 짤라서 병렬적으로 업로드하는 게 효율적이라고 생각이 들었다. VPN을 키고 홍콩에서 우리 API 로 파일을 쏘는 속도테스트를 해봤을 시 100MB 파일을 전송하고 응답받는데,

12분이 넘게 걸린다.. 어마어마한 속도 차이다. (vpn off 시 12.93s 정도) 물론 개발서버라 네트워크 대역폭이 낮은 서버스펙을 사용하는 것도 있고, 이 테스트가 정확한 테스트가 아닐지라도 타국에서는 느리게 돌아가는 건 확실하다. 해당 API가 전 세계를 커버하는 지라, 각국의 네트워크 대역폭과 망 위치에 따라 속도는 천차만별일 거라 예상된다. 이 네트워크를 쏘는 중에 브라우저를 새로고침하거나, 다른 페이지로 이동하면 파일을 온전히 받지 못하고, 다음과 같은 Exception이 터진다.

org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request] with root cause
java.io.EOFException: null

가장 좋은 것은 각국 해당 리전에 서버를 복제하고 DNS에서 가장 가까운 리전으로 라우팅하도록 하는 건데, 현실적으로는 비용문제 때문에 서비스를 제 궤도에 올리기 전까지 증축은 힘들고, 최대한 이 시간을 단축시키는 방법을 찾아야 했다.
일단 해당 네트워크 요청에서 응답이 오기 전까지 페이지를 이탈하거나, 새로고침을 할 시, 파일이 손실 될 수 있다는 경고창을 띄우기로, UI에서 기획을 했다.

다음은 위의 아티클을 보면서, 병렬처리로 단축시키기로 해봤다. 기존에는 파일을 해당 API 서버에서 통째로 직접 받고 전처리를 해준다음, S3로 올리는 식이였다면, 바뀐 로직은 클라이언트에서 파일을 청크 단위로 짤라서, 한꺼번에 s3로 직접 올리고, 완료 신호를 API 서버에게 쏘면, API 서버는 그 정보를 바탕으로 S3에서 파일을 다운받고 후처리를 하는 식으로. 일단 간단한 예제코드와 테스트 페이지를 만들고 테스트해보았다.

Backend

Test Page

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>S3 upload Test</title>
    <style>
        body {
            height: 100vh;
            background-color: #212121;
            margin: 0;
            width: 100%;
            font-family: Arial, Helvetica, sans-serif;
        }

        h1, input {
            color: rgb(202, 202, 202);
        }

        hr {
            margin-top: 50px;
            margin-bottom: 50px;
        }
    </style>
</head>
<body>
<h1>S3 멀티파트 업로드 병렬처리 테스트하기 (최대 3GB)</h1>
<input type="file" id="multipartInput">
<button id="multipartInputBtn">send file</button>

<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"
        integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ=="
        crossorigin="anonymous"></script>
<script>
    document.getElementById('multipartInputBtn').addEventListener('click', async () => {
        const multipartInput_fileInput = document.getElementById('multipartInput');
        // const targetId = document.getElementById('tutorialId').value;
        const file = multipartInput_fileInput.files[0];
        const filename = file.name;
        const fileSize = file.size;

        // 3GB가 넘어가는 파일 업로드 제한
        if (fileSize > 3 * 1024 * 1024 * 1024) {
            alert('The file you are trying to upload is too large. (under 3GB)');
            return;
        }

        const url = `http://localhost:2002/v2`;

        try {
            let start = new Date();
            // 1. Spring Boot 서버로 멀티파트 업로드 시작 요청합니다.
            let res = await axios.post(`${url}/initiate-upload`, {filename: filename});
            const uploadId = res.data.data.uploadId;
            const newFilename = res.data.data.newFilename; // 서버에서 생성한 새로운 파일명
            console.log(res);

            // 세션 스토리지에 업로드 아이디와 파일 이름을 저장합니다.
            sessionStorage.setItem('uploadId', uploadId);
            sessionStorage.setItem('filename', newFilename);

            // 청크 사이즈와 파일 크기를 통해 청크 개수를 설정합니다.
            const chunkSize = 10 * 1024 * 1024; // 10MB
            const chunkCount = Math.floor(fileSize / chunkSize) + 1;
            console.log(`chunkCount: ${chunkCount}`);


            const promiseList = [];

            for (let uploadCount = 1; uploadCount < chunkCount + 1; uploadCount++) {
                // 청크 크기에 맞게 파일을 자릅니다.
                let start = (uploadCount - 1) * chunkSize;
                let end = uploadCount * chunkSize;
                let fileBlob = uploadCount < chunkCount ? file.slice(start, end) : file.slice(start);

                // 3. Spring Boot 서버로 Part 업로드를 위한 미리 서명된 URL 발급 바듭니다.
                let getSignedUrlRes = await axios.post(`${url}/upload-signed-url`, {
                    filename: newFilename,
                    partNumber: uploadCount,
                    uploadId: uploadId
                });

                let preSignedUrl = getSignedUrlRes.data.data.preSignedUrl;
                console.log(`preSignedUrl ${uploadCount} : ${preSignedUrl}`);
                console.log(fileBlob);

                // 3번에서 받은 미리 서명된 URL과 PUT을 사용해 AWS 서버에 청크를 업로드합니다,
                let uploadChunckPromiss = fetch(preSignedUrl, {
                    method: 'PUT',
                    body: fileBlob
                }).then((response) => {
                    return response
                }).then((res) => {
                    let EtagHeader = res.headers.get('ETag').replaceAll('\"', '');
                    var modifyData = {
                        awsETag: EtagHeader,
                        partNumber: uploadCount,
                    };
                    return modifyData
                }).catch((err) => console.error(err));

                promiseList.push(uploadChunckPromiss);
            }


            const multiUploadArray = await Promise.all(promiseList)
                .then((datas) => {
                    return datas
                })
                .catch((err) => {
                	//todo 하나라도 실패하면 실패했다고 벡엔드에게 알려줘야 됨
                    console.log("e==?>", err)
                });


            console.log("multiUploadArray", multiUploadArray);
            // 6. 모든 청크 업로드가 완료되면 Spring Boot 서버로 업로드 완료 요청을 보냅니다.
            // 업로드 아이디 뿐만 아니라 이 때 Part 번호와 이에 해당하는 Etag를 가진 'parts'를 같이 보냅니다.
            const completeUpload = await axios.post(`${url}/complete-upload`, {
                filename: newFilename,
                parts: multiUploadArray,
                uploadId: uploadId,
            });
            let end = new Date();
            console.log("파일 업로드 하는데 걸린 시간 : " + (end - start) + "ms")
            console.log(completeUpload.data, ' 업로드 완료 응답값');


        } catch (err) {
            console.log(err, err.stack);
                            	//todo 실패하면 실패했다고 벡엔드에게 알려줘야 됨
        }

    });

 
</script>
</body>
</html>

이번엔 4KB 정도의 파일로 테스트를 해보니, 확실히 눈에 띄게 속도개선이 되었다.

Promise.all 을 적용하지 않았을 때.

Promise.all 을 적용했을 때

S3 Transfer Acceleration

이번에는 S3의 전송 가속화를 활성화하고 가속화 엔드포인트로 테스트 해보았다.

VPN을 키고 테스트를 해보았는데, 가속화 엔드포인트를 활용한 업로드 시간이 더 오래걸린다.. 이유를 모르겠다. 누구 짐작가는 원인이 있으면 댓글 좀

https://stackoverflow.com/questions/77601432/upload-to-s3-using-transfer-acceleration-enabled

https://medium.com/@chochoswim98/aws-sa-s3-transfer-acceleration-1dd549f00cd8

APM 재구축

grafana 대쉬보드를 재구성했다. 간단한 구조는 개별 스프링부트로 구성된 서버는 Spring Actuator를 통해서 매트릭 지표를 오픈해두고, 프로메테우스 서버는 그 엔드포인트들로 지표를 수집한다. 그라파나 서버에 그 프로메테우스 서버를 데이터 소스로 등록해두고, 대쉬보드를 구성하면 된다. 문제는 로그파일인데, Actuator에서 로그파일을 공개하도록 구성할 수 있지만 프로메테우스에서 그것까지 수집을 못 한다. 결국 로그를 수집할 전용 Loki Server를 설치하고, 그 Loki Server가 로그를 수집하도록 하고, 그라파나 서버가 해당 Loki Server를 데이터소스로 등록해서 대쉬보드를 꾸며줘야 된다. 여기서 검색을 했을 때, 대부분은 해당 로그파일이 적재되는 인스턴스에 promtail 서버를 각각 깔아서 로그파일을 주기적으로 Loki Server에게 쏴주는 식으로 설명이 되었는데, 좀 더 찾아보니, loki-logback-appender 라는 괜찮은 오픈소스를 발견했다. 그래서 부트 앱에서 직접 Loki Server로 쏴주도록 설정했다.

https://loki4j.github.io/loki-logback-appender/

https://neilwhite.ca/spring-boot-3-observeability/

https://github.com/blueswen/spring-boot-observability

AWS SDK V2 마이그레이션

https://github.com/aws/aws-sdk-java-v2/blob/master/docs/LaunchChangelog.md#411-s3-operation-migration

https://medium.com/@iamcrypticcoder/spring-boot-services-for-aws-java-sdk-v2-a3b8bc1c1b12

버킷 이름에 .이 들어가면 Failed to load: resource: net::ERR_CERT_COMMON_NAME_INVALID 오류가 발생한다. 버킷 이름에 .이 들어갈 경우 postman은 잘 되는데 구글은 안된다던가, 구글은 되는데 네이버 웨일은 안된다던가 하는 문제가 발생할 수 있으니 버킷이름에 .은 지양하도록 하자

profile
시간대비효율

0개의 댓글