반응형

가상메모리에 대해 설명하기 앞서 자바 기반 솔루션의 경우 설정한 최대 힙 메모리 이상을 사용하지 않기 때문에 최대 힙 메모리가 운영체제의 메모리를 넘기지 않도록 설정됬다면 운영체제의 메모리 부족이 발생할 가능성은 낮다. 하지만 한정된 힙 메모리 내에서 동작하기 때문에 대량의 데이터 처리와 빈번한 GC로 인한 성능 저하가 유발될 가능성이 있다. OS 메모리 용량이 많아 힙 메모리를 과하게 크게 설정한 경우에는 메모리를 청소하는 GC 작업으로 순간 순간 멈추는 시간이 길어져 서비스 안정성이 떨어질 수도 있다.

오라클 버퍼 캐시처럼 데이터베이스는 디스크에서 데이터를 읽어옮으로써 발생하는 성능 저하를 개선하기 위해 메모리에 최근에 사용한 데이터 블록을 캐시해서 재사용하는 알고리즘이 있다. 통상 이 버퍼에 대한 캐시 적중률은 90%이상 나오는 것이 일반적인데, 캐시의 메모리 크기가 너무 작게 설정된 경우 캐시 적중률이 떨어져 디스크에서 데이터 블록을 읽어와야 하는 ㅂ니도가 늘어나 성능 저하를 유발할 수도 있다. 반대로 캐시를 크게 설정해 운영체제에서 스왑이 발생하게 되면 오히려 작게 설정한 것보다 성능이 더욱 악화되어 서비스가 거의 멈추는 상태가 유발될 수도 있다.

Virtual Memory(가상메모리)

서버는 설치할 수 있는 메인 메모리 (Main memory, Physical memory)에 한계가 있고, 예상치 못하게 메모리 사용량이 크게 증가할 수도 있지만 메모리는 비싼 자원으로 언제 발생할지 모르는 상황에 대비해 메모리를 충분히 확보하기 어렵다. 그래서 운영체제는 위와 같은 제약사항을 극복하기 위해 상대적으로 값싼 디스크를 보조 메모리로 사용하는 방식을 사용하고 있다. 디스크의 일정 공간을 할당해 보조 메모리 용도로 사용하는데 이를 페이징스페이스라고 한다. 

메인 메모리(주 기억장치)와 디스크의 페이징스페이스(Pagingspace, 보조 기억장치)를 묶어 하나의 메모리처럼 동작하게 함으로써 메인 메모리 한계를 넘는 메모리 사용을 가능하게 하는 것이 가상 메모리다.

운영체제에서 관리하는 메모리 액세스 단위를 페이지(page)라고 하며 통상 4KB 단위다. 이 페이지 단위로 일부는 메인 메모리에 있을 수도 있고 또 일부는 페이징스페이스에 있을 수도 있는 것이다.

프로세스 내에서 사용 중인 가상 메모리 주소만 봐서는 해당 페이지가 메인 메모리에 있는지 페이징스페이스에 있는지 확인할 수 없다. 그렇다고 페이징스페이스가 메인 메모리와 동일한 것은 아니다. 메인 메모리의 입출력 속도가 디스크에 비해 비교가 안 될 정도로 우수하기 때문에 프로세스가 기동되면 기본적으로 메인 메모리를 사용하다가 부족해지면 페이징스페이스를 사용하게 된다. 이때도 CPU가 페이징스페이스에 있는 데이터를 메인 메모리처럼 바로 사용할 수 있는 것이 아니라 사용 빈도가 낮은 프로세스의 메인 메모리 사용 부분을 페이징스페이스 부분으로 옮겨 여유 메모리를 확보한 후 사용할 페이징스페이스 부분을 메인 메모리로 로드해서 사용하는데, 이를 스와핑이라고 한다. 그래서 CPU가 직접 접근할 수 있는 메인 메모리를 활성 가상 메모리(Active virtual memory)라 하고, 페이징스페이스를 비활성 가상 메모리(Inactive virtual memory)라고 한다. 

가상 메모리 주소는 프로세스 내에서만 유효한 주소다. 각 프로세스는 가상 메모리 주소와 실제 주소 간의 매핑 테이블 (Virtual-to-physical translation table)을 가지고 있어 가상 메모리 주소로 실제 주소를 찾아갈 수 있게 돼 있다. 실제 주소는 메인 메모리와 페이징스페이스의 주소를 가리킨다. 

 

해당 글은 '실무로 배우는 시스템 성능 최적화'의 메모리 부분에서 발췌한 내용입니다.

반응형
반응형

1. 페이징스페이스(Pagingspace)

페이징스페이스는 메인 메모리가 부족할 때 사용하는 디스크 공간으로, 스왑 스페이스(Swapspace)라고도 한다. 

1GB 메모리가 탑재된 시스템에서 이미 900MB 메모리를 운영체제와 다른 프로세스가 사용하고 있는데, 300MB 메모리를 사용하는 프로세스를 새로 실행한다고 가정해 보자. 여유 메모리가 100MB밖에 없어 신규 프로세스를 기동할 수 없는 상황으로 보인다. 그러나 운영체제는 현재 사용 중인 900MB 메모리 중 200MB 정도를 디스크에 존재하는 페이징스페이스라는 곳으로 옮김으로써 메인 메모리에서 300MB를 확보해 신규 프로세스를 수행한다.

즉, 페이징스페이스는 운영체제가 메인 메모리 크기 이상의 메모리를 사용할 수 있게 해주는 역할을 한다. 그러나 시스템 성능 관점에서 보면 페이징스페이스는 디스크에 프로세스 메모리를 쓰고, 읽는 스와핑 작업으로 인해 메모리 접근만으로 처리될 때메 비해 큰 성능 저하가 발생한다. 따라서 스와핑이 발생하지 않도록 메모리 여유율을 유지하는 것이 성능에 중요하다.

2. 페이징(Paging)

가상 메모리 체계에서는 운영체제가 인식하는 일정한 크기(4KB, 64KB)의 데이터 기록 단위를 페이지라고 한다. 메인 메모리로부터 한 페이지의 데이터를 보조 기억장치(디스크)로 복사하거나 보조 기억장치로부터 메인 메모리로 로드하는 것을 페이징이라고 한다.

3. 스와핑(Swapping)

메모리에서 페이지 혹은 세그먼트 단위 데이터를 교환하는 것으로 컴퓨터가 메모리보다 큰 프로그램을 실행하거나 메모리보다 큰 데이터 파일을 다룰 수 있게 한다. 가상 메모리 체계에서는 페이징 기법을 이용해 스와핑을 수행한다. 스와핑은 프로세스가 사용하는 메모리 일부를 페이징스페이스로 옮기거나 페이징스페이스로부터 메모리로 로드하는 것을 가리킨다.

4. 페이지 부재(Page Fault)

페이징 방식의 가상메모리에서 CPU가 사용하려는 페이지가 메인 메모리에 없는 경우다. 사용하려는 가상 메모리 주소에 해당 페이지를 매핑 테이블에서 주소 변환할 때 해당 페이지가 메인 메모리에 없다고 표시돼 있으면 페이지 부재가 발생한다. 이 경우 해당 페이지를 디스크에 있는 파일 또는 페이징스페이스에서 가져와야 하고 메인 메모리가 부족한 경우에는 다른 페이지와 교체해야 한다.

5. 페이지 인(Page In)

가상 메모리에서 페이지 부재가 일어났을 때 디스크에 있는 프로그램이나 데이터를 메모리에 로드하는 것

6. 페이지 아웃(Page out)

가상 메모리에서 페이지 부재가 발생했을 때 메모리가 부족하면 페이지 스틸러(Page stealer)는 기존 메모리를 디스크로 기록하는 페이지 아웃이라는 작업을 수행한다. 기록 대상 메모리가 프로세스의 작업 세그먼트(Work segment)영역에 있는 메모리면 페이징스페이스로 기록하고, 파일에서 읽어 들인 내용(Permanent segment)인 경우 페이지의 내용이 변경됐으면 디스크의 해당 파일에 기록하고 그렇지 않으면 소멸시킨다.

7. 페이지 스캔률(Page Scan Rate)

운영체제는 일정량의 여유 페이지(Free page)를 확보해서 필요할 경우 즉시 제공하기 위해 페이지 스캔 (Page scan)이라는 검색을 통해 해제함으로써 여분으로 확보할 페이지를 찾는다. 여유 페이지 목록(Free page list) 임계치보다 여유 페이지 수가 줄어들면 메모리 관리자는 기존의 페이지 인된 것 중에서 LRU 알고리즘 검색을 통해 페이지 아웃이 가능한 것을 찾아 페이지 아웃시킨 후 여유 페이지로 변경한다. 여기서 페이지 아웃이 가능한 페이지를 탐색하는 빈도를 의미하는 페이지 스캔률이 나오는데 빈도가 높다는 것은 해제할 페이지가 넉넉하지 못하기 때문에 자주 메모리를 탐색해야 한다는 의미로 볼 수 있다. 

 

해당 내용은 '실무로 배우는 시스템 성능 최적화' 책의 내용중 일부를 발췌한 것입니다.

 

반응형
반응형

이전에 읽었던 좋은 글이 있어 소개해보려 한다.

글 하나는 조대협님의 블로그의 특정 포스팅에서 발췌했었던 내용이고 하나는 임백준님의 컬럼(개인적으로 너무 좋아함)에서 발췌한 내용입니다. 개발 6년차인 지금 봐도 너무나도 개인적으로는 와닿는 문구들이라 소개해 보려 합니다.

조대협님의 블로그 포스팅 글 (정확한 글의 주소는 모름...너무 오래전 발췌해놓아서)

'30대는 실력으로 먹고 살고, 40대는 30 쌓은 명성으로 먹고 살고 50대는 인맥으로 먹고 산다

오늘 상무님께서 담배피러가자고 하셔서 따라갔다가 재미있는 이야기를 하나 들어서 정리해 봅니다.

직장생활이 마치 보잉 747 이륙 과정과 같다고 하더군요.

30대에는 이륙 준비를 위해서 연료를 채우는 시기랍니다. 이때 커리어, 자기 관리, 인맥들을 해놓은 사람들은 준비가 된것입니다. 40대가 되면 활주로에 서게되는데 이때 30대에 준비해놓은 사람들은 이륙해서 50대에 하늘로 비행을 하는겁니다. (정상에서 만납시다!!) 라는 메세지와 함께 이륙하는거지요.

준비를 해놓지 못한 사람은 40대에 이륙하지 못하고, 브레이크를 잡거나 (계속 활주로에 머물러 있는 겁니다. 기존 회사에 남아 있는 건데, 이것도 뒤에서 비행기 오면 비켜줘야져..) 아니면 계속 액셀 밟아서 낭떨어지로 떨어지거나 시궁창에 쳐박히는 겁니다. (좌천이나 작은 회사로 이전이져..)

가끔 브레이크 밟아놓은 상태에서 비상팀(인맥) 가지 활주로를 내주기도 하는데, 길이가 너무 짧다는 거져...

공감이 100%가는 이야기인데, 비유가 적절합니다. 몇년 남지 않은 30 연료(영어,글로벌인맥, 근데 학력은 어쩌나...) 채우는데 집중해봐야겠습니다.

자바 잘한다고, 프로그래밍 잘한다고 메니져가 없다. CIO 더더욱 없다.

미들웨어를 잘안다고, 팀장이 없고, 데이타 베이스를 잘안다고해서 승진 없다.

결국은 비지니스를 알고, 소통알고.. 돈을 벌어다 주는 사람이 위로 올라간다..

간단한건데.. 지금까지 잊고 살았을까?

임백준님의 컬럼중 '실력은 고통의 총합이다.'

1. 지금 다니고 있는 회사에서 하는 일을 잘하기 위해서 노력하는 것이 가장 좋은 공부다.

2. 회사에서 하는 일과 개인적으로 공부하는 내용을 최대한 근접시키기 위해서 노력하라.

3. 새로운 기술을 익히는 최선의 방법은 스스로 문제를 정의한 다음, 새로운 기술을 이용해서 문제를 풀어보는 것이다. 책을 읽거나 동영상을 보는 것은 그보다 하위수준의 방법이다.

4. 신기술을 좇는 메뚜기가 되지 말라.

5. 모든 것을 알아야 한다는 강박을 버려라. 미리 획득하는 지식의 99% 무용지물이다. 필요할 필요한 기술을 익힐 있는 것이 능력이다. 능력을 키워라.

6. 이상한 나라의 앨리스에 나오는 토끼굴(rabbit hole) 피하라. 카테고리이론을 알아야 함수형 언어를 있는게 아니고, 선형대수학을 공부해야 머신러닝을 있는게 아니다. 토끼굴에 빠져서 한없이 들어가다보면 비본질적인 공부에 시간을 허비하게 된다.

7. 겉만 핥는 것은 경박하지만 토끼굴에 빠지는 것은 우매하다. 사이의 적당한 지점에서 균형을 잡는 것이 개발자의 능력이다.

8. 머리에 들어오지 않는 어려운 개념이나 용어는 자투리 시간을 이용해서 반복적으로 읽고 암기하라. 나중에 그림을 공부할 도움이 된다.

9. 항상 겸손해야 하지만 동시에 자긍심을 가져라. 그대가 지금 작성한 코드, 지금 읽은 , 지금 공부한 내용을 그대보다 아는 사람은 지구상에 없다. 모든걸 알고 있는 것처럼 보이는 다른 사람들도 그대와 마찬가지로 불안해하고, 위축되고, 두려워하면서 살아가고 있다. 

자긍심이란 그런 타인을 돕고자 하는 마음가짐의 다른 이름이다.

10. 혼자 하지 말고 함께 공부하라.

ref : http://www.zdnet.co.kr/column/column_view.asp?artice_id=20170616090644&type=det&re=

임백준님의 컬럼은 하나를 읽다보면 다른 컬럼글들도 기본 세 네 개 이상 읽게 된다는....글쓰는 실력 향상을 위해 조금씩이라도 시간을 투자해야 겠다는 생각을 해본다.

반응형
반응형

DB에 넣으려는 값의 타입과 자바 모델 객체의 타입이 다를 때 발생

아래 에러로그를 보면 video_id 컬럼에서 에러가 나는데

DB에는 long(bigint) type인데 application에서는 video_id를 string type으로 처리하고 있었고

string type의 video_id를 DB에 insert 하려다 보니 에러 발생

java.sql.SQLException: Data truncated for column 'video_id' at row 1 at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129) ~[mysql-connector-java-8.0.17.jar:8.0.17] at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.17.jar:8.0.17] at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.17.jar:8.0.17] at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953) ~[mysql-connector-java-8.0.17.jar:8.0.17] at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1092) ~[mysql-connector-java-8.0.17.jar:8.0.17] at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1040) ~[mysql-connector-java-8.0.17.jar:8.0.17] at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1340) ~[mysql-connector-java-8.0.17.jar:8.0.17] at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1025) ~[mysql-connector-java-8.0.17.jar:8.0.17] at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61) ~[HikariCP-3.2.0.jar:na] at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java) ~[HikariCP-3.2.0.jar:na] at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:175) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3192) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3706) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:90) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:478) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:356) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1483) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:512) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3321) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2517) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:447) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:178) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:39) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:271) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:104) ~[hibernate-core-5.3.12.Final.jar:5.3.12.Final] at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:533) ~[spring-orm-5.1.10.RELEASE.jar:5.1.10.RELEASE] at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:746) ~[spr
반응형
반응형

해당 글은 '실무로 배우는 시스템 성능 최적화' 책의 내용을 발췌한 내용입니다. 최근 해당 책을 읽다가 이전에 내가 했던 고민과 비슷한 사례에서 비롯한 성능 저하 사례가 있어 포스팅 해본다. 보통 스프링부트에서 API를 만들 때 RestTemplate을 만들어서 사용하실텐데 매번 RestTemplate객체를 new로 생성해서 사용해야하는지 싱글톤 객체로 만들어 놓고 사용해야 하는지 고민하던 때가 있었다. 처음 멋 모를 때는 API Controller마다 new로 생성해서 사용했었는데 RestTempalate가 스레드 간 공유해도 안전한 클래스라는걸 알고 난 이후에 @Bean으로 싱글톤 객체를 생성해 사용했었다. new 객체를 매번 생성할 때도 나는 아래와 같은 장애상황을 겪진 못했지만 아래와 같이 내부락이 발생할 수 있으므로 싱글톤 객체로 만들어 사용할 수 있도록 하자. 또한 @Bean 애노테이션으로 만든 싱글톤 객체보다 빈 대신 static을 사용한 싱글턴 구조를 만드는게 더 좋다고 하는데 궁금하시다면 조치부분을 참고.

스프링 프레임워크의 객체를 잘못 사용한 성능 저하 사례

현상

오픈 전 6시간 동안 진행하는 안정성 테스트 과정에서 1시간이 경과한 후 응답시간이 급격히 저하되는 현상이 발생했다.

접근 방식

오라클 JVM을 사용하고 있어 응답시간이 느려졌을 때 jstack으로 스택을 5회 수집해서 분석했다. 성능 저하 시 전체 사용자 요청을 처리 중인 스레드 중 90% 이상이 애플리케이션 내부 락 대기 중이었다.

시스템의 애플리케이션 서버는 프론트 서버와 백엔드 서버로 구성돼 있는데, 프론트 서버에 들어온 서비스 요청은 다시 HTTP를 사용해 다시 백엔드 업무 서버를 호출하는 구조다. 내부 락은 프론트 서버 내에서 백엔드 서버의 서비스를 호출할 때 사용하는 RestTemplate 객체를 생성하는 과정(RestTeamplate.init())에서 발생하고 있었다. 이에 RestTemplate 클래스에 대해 인터넷 검색을 한 결과, 애플리케이션에서 RestTemplate을 잘못 사용하고 있음을 알 수 있었다.

원인

스프링 프레임워크에서 제공하는 HTTP 클라이언트 템플릿인 RestTemplate 클래스는 객체 생성 시 내부에 무거운 Charset.availableCharsets 메서드가 실핸된다. 더구나 Charset.availableCharsets 메서드는 여러 스레드가 동시에 사용했을 때 스레드 간에 락 경합이 생겨서 급격한 성능 저하를 유발한다. HTTP로 백엔드 시스템의 서비스를 호출할 때마다 RestTemplate객체를 새로 생성해서 사용함으로써 Charset.availableCharsets 메서드에 의한 성능 저하가 발생한 것이다.

조치

RestTemplate은 스레드 간에 공유해도 안전한 클래스이므로 싱글턴 구조로 객체를 한 개만 생성한 후 스레드 간에 공유해서 사용하도록 수정한다. 그래서 객체 생성이 인스턴스마다 한 번만 이뤄져 락 경합이 제거됐다(RestTemplate 클래스를 스프링 프레임워크의 빈으로 사용할 수도 있으나 RestTemplate 클래스를 사용하는 MobileClient 클래스 또한 프레임워크의 일부라서 빈 대신 static을 사용한 싱글턴 구조를 만들었다.

결과

6시간 안정성 테스트 동안 응답시간이 증가하는 현상 없이 일정하게 유지됐다.

ref : 실무로 배우는 시스템 성능 최적화

반응형
반응형

'실무로 배우는 시스템 성능 최적화' 책을 읽다가 L4설정 오류에 따른 알아두면 좋을 법한 성능 저하 사례가 있어 포스팅 해보려고 한다. 해당내용은 모두 '실무로 배우는 시스템 성능 최적화' 책 내용에 기반한다.

[ 현상 ]

웹 포털 화면을 사용할 때마다 대부분 정적 콘텐츠를 반복해서 내려받아 응답시간이 오래 걸리는 성능 저하 현상이 발생했다.

[ 접근 방식 ]

피들러로 모니터링한 결과, 동일 정적 콘텐츠를 기존에 내려받았음에도 다시 내려받는 현상이 발생하고 있음을 확인하고 HTTP 응답을 분석한 결과, 동일 정적 콘텐츠의 변경일자가 각기 다른 것을 발견했다. 이것은 한 웹 서버에 요청이 들어간다면 발생할 수 없는 상황이므로 웹 서버 액세스 로그를 통해 한 PC에서 들어오는 HTTP 요청이 여러 웹 서버에 골고루 들어온다는 것을 확인했다. 이에 웹 서버 파일시스템 구성과 L4의 분배 방식을 확인해 원인을 찾았다.

[ 원인 ]

-L4에서 단순 라운드 로빈(Round-robin) 형태로 HTTP 요청을 분배시켜 한 사용자의 요청에 대해 4대의 웹 서버에서 서비스가 이루어짐

-웹 서버의 문서 루트는 NAS 같은 공유 파일시스템이 아닌 로컬 파일시스템 내에 개별적으로 존재하고, 같은 콘텐츠임에도 변경일자가 각 서버마다 달랐다.

-위 두 가지 원인으로 한 브라우저에서 이뤄지는 HTTP 요청이 4대의 서버에 무작위로 분배되고, 각 서버 동일 콘텐츠의 변경일자가 모두 달라서 매번 변경된 콘텐츠로 인식됐다.

-이로 인해 브라우저 캐시가 정상적으로 효과를 발휘하지 못하고 매번 콘텐츠 내용을 내려받음으로써 성능 저하가 발생했다.

[ 조치 ]

L4 분배 방식을 해시(HASH)로 변경해 PC의 IP가 동일하면 동일한 웹 서버로 접속이 이뤄지도록 개선했다(NAS 같은 공유 파일시스템을 사용하면 L4의 분배 방식에 상관없이 브라우저 캐시가 동작하게 할 수도 있다).

[ 결과 ]

브라우저 캐시가 정상적으로 동작해 매번 콘텐츠를 내려받지 않음으로써 화면 응답시간이 개선됐다.

반응형
반응형

최근 1월 중순쯤부터 Chrome Samesite이슈에 대응한다고 설연휴전까지 시간이 굉장히 빠르게 지나간 것 같다.

아마 서비스에서 쿠키를 클라이언트들의 브라우저에 심어 무엇인가를 처리하고 있었던 분들이라면 해당 이슈에 대해 이미 파악하고 대처 했을 거라 생각된다. 그럼 구글에서 크롬(Google Chrome)에 어떤 정책을 적용했고 SameSite란 무엇인가? 에 대해 간단히 정리해보려 한다.

SameSite 한 마디로 지금 현재 사이트(도메인, First Party)의 쿠키만 클라이언트 브라우저에 심도록 허용하겠다! 

이 말인 즉슨 기존 크롬 버전 80이전 버전에서는 도메인이 다른 (Third Party)의 서버에서 response.addCookie만 해주면 third party쿠키를 쉽게 심을 수 있었다. 관련한 내용은 이전 포스팅을 참고 바란다.

"다른 도메인 내부에 쿠키 생성하기(이게 가능해?)" https://brocess.tistory.com/146?category=715032

 

[ Spring ] 다른 도메인 내부에 쿠키 생성하기 (이게 가능해???)

다른 도메인 내부에 쿠키 생성하기 (이게 가능해???) 최근 쿠키매칭시스템 설계를 위해 광고 프로세스 전반에 대한 내용을 학습중 우리 도메인 쿠키 내부에 다른 도메인 쿠키값들이 박혀있는 걸 발견하였다. 뭐야..

brocess.tistory.com

그렇다 이전에는 그냥 막 심어도 문제 없었다. 

근데 구글에서 보안 이슈를 명목(사이트간 쿠키기반 요청 위조(CSRF - Cross site request forgery)으로 크롬(Google Chrome) 80버전 부터는 third party(웹서비스와 도메인이 다른 서버)에서 쿠키 심는걸 default로 막겠다는 것이다. 2020년 2월 4일 부터 점차 크롬을 사용하는 사용자들의 브라우저들이 79버전에서 80으로 자동으로 차차 업데이트 되어질거라고 말했었다.

이게 자기들 도메인의 웹서비스에 쿠키를 심어서 사용하는 서비스에는 별문제가 없겠지만 광고서비스와 같이 해당 클라이언트의 쿠키의 특정ID값을 기반으로 유저를 구분해 타겟팅하는 서비스들에서는 큰 이슈거리일 수 밖에 없다. 😱😱😱😱😱😱😱😱

하지만 구글에서는 아예 third party 쿠키를 심지못하도록 원천봉쇄하지는 않았다. 쿠키를 다른 도메인의 서비스에 심을 때 'SameSite=none', 'secure'설정을 쿠키에 하면 기존처럼 허용해주겠다는 것이다!!!! 크롬 버전 80이상부터는 기본적으로 크롬 브라우저의 설정이 'SameSite=Lax'가 되어 버린다. (Lax보다 한 단계 높은 보안 수준인 'strict'도 존재한다.)

 

그럼 어떤식으로 해결을 해주어야 할까?

크롬 Samesite설정을 강제적으로 80버전과 같은 보안수준으로 올리고 테스트를 해보았다. 해당 설정은 크롬 브라우저에 chrome://flags라고  치면 확인할 수 있다.

크롬 80이전 버전에서는 모두 Default로 되어있다.(SameSite=none)이라고 생각하면 된다.

위의 Default값을 Enable로 변경하면 크롬 80버전의 SameSite의 상황을 재현할 수 있다.

크롬80부터는 기본이 SameSite=Lax상태가 되어버린다.

따라서 이렇게 해놓고 테스트를 했을 때 기존에 클라이언트들의 브라우저에 심어놓았던 쿠키를 아예 읽어올 수 없게 되었다.😱😱😱😱

 

그럼 어떻게 해결을 했나?

일단 유저들의 크롬 브라우저가 80으로 업데이트 되기전 클라이언트들의 브라우저에 심긴 우리 서비스의 식별 ID값에 "samesite=none", "secure"설정을 하여 동일한 값으로 쿠키를 덮어씌워줘야 했다. but, "secure"설정이 된 쿠키는 "https"가 설정된 서버에서만 읽을 수 있기 때문에 수 많은 광고주나 매체주에 심겨 우리쪽 서버를 호출하는 url이 http로 되어있을 경우 읽지 못하는 문제가 발생할 수 있었다. 이에 서버 자체적으로 http로 들어오는 요청을 https로 리다이렉트 시키도록 하였다.

추가 검토사항으로는 구글이 크롬관련 작성한 문서에는 다음과 같이 씌어져 있다. 

Versions of Chrome from Chrome 51 to Chrome 66 (inclusive on both ends).
These Chrome versions will reject a cookie with `SameSite=None`

즉, 크롬버전 51~66사이의 버전에서는 우리가 아무리 쿠키에 'SameSite=None', 'secure'설정을 한다고 하더라도 reject시켜 버린다는 것이다....

https://www.chromium.org/updates/same-site/incompatible-clients

 

SameSite=None: Known Incompatible Clients - The Chromium Projects

Home of the Chromium Open Source Project

www.chromium.org

그래서 어떡하지라는 생각이 들어 일단 우리 서비스에 요청오는 request들의 user-agent데이터를 기반으로 해당 버전에 속하는 요청이 얼마나 되는지 확인해보았다. 그랬더니 다행히도 해당 버전으로 부터 오는 요청은 1~2%내외였고 이 1~2%를 위해 무엇인가를 작업하기엔 trade-off가 되는 것들이 너무 많았다. 서버에서 하나 같이 user-agent의 버전을 확인해 해당 버전에서는 별도의 처리를 해주어야 했고 결국에 해당 버전에서 SameSite=None을 reject시켜버리기 때문에 손을 쓰기 힘든 상황이었기에 해당 버전을 위해 별도의 처리는 따로 하지 않기로 결정되었다.

그리고 스프링, 자바 어플리케이션에서 많이 사용하는 javax.servlet.http.cookie class에서는 SameSite관련 API를 지원하지 않기 때문에 SameSite속성을 추가하기 위해서는 HttpServletResponse 객체에 Set-Cookie헤더(header)를 추가해서 처리했어야 했다.

어플리케이션 사이드나 Web Server설정 관련해서 처리한 부분은 잘정리된 포스팅이 있어 해당 블로그를 참고하면 좋을 것 같다.👍(굉장히 잘 정리되어 있다.)

https://ifuwanna.tistory.com/223

 

Cookie SameSite 설정하기 (Chrome 80 쿠키 이슈)

20년 2월 4일 릴리즈된 구글 크롬(Google Chrome)80버전부터 새로운 쿠키 정책이 적용 되어 Cookie의 SameSite 속성의 기본값이 "None"에서 "Lax"로 변경되었습니다. SameSite 를 None 으로 설정할 경우 모든 도..

ifuwanna.tistory.com

작업을 하며 느낀점은....구글이 갑이다....지금도 구글의 영향력이 쌔지만 앞으로는 점점 더 그 파워가 막강해질것이다....그럼 구글 주식을 사야겠다😅......여기서 이만...

반응형
반응형

mac로컬 환경에서 가끔 스프링부트 어플리케이션 실행시 다음과 같은 에러가 발생한다.

Unable to start embedded Tomcat servlet container 
_org.springframework.boot.context.embedded.EmbeddedServletContainerException_: Unable to start embedded Tomcat servlet container
org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer.start(_TomcatEmbeddedServletContainer.java:165_)
org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.startEmbeddedServletContainer(_EmbeddedWebApplicationContext.java:293_)
.
.
.
org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer.start(_TomcatEmbeddedServletContainer.java:159_)

... 10 common frames omitted

해결방법

maven clean, build 다시 하고 문제가 발생할 경우 다음과 같이 처리해준다. 

터미널 켜서 명령어 : lsof -n -i4TCP:8080 치고 pid확인해서 (어플리케이션 포트 설정이 8080인 경우)
kill -9 pid 로 해당 프로세스를 강제종료시켜주고 다시 실행해보자. 
반응형
반응형

이전 부터 프로그래밍이나 개발자 관련 좋은 문구들이 있을 때 마다 로컬 메모장에 모아놓은 것들을 정리할겸 포스팅해보려고 한다.

사실 2월 한 달 동안 1일 1포스팅을 목표로 했는데....2월 10일이 지난지금 소재는 다 떨어져가고 공부해서 포스팅할 시간은 많이 부족하다보니 기존 메모장에 적힌 내용들을 하나 둘 들쳐내본다. 그러다가 개발과 프로그래밍에 대해 와닿았던, 그 당시 새로운 인사이트를 주었던 문구들이 있어 정리해 본다.  따라서 정확한 내용의 출처들은 모르는 것들이 대부분이다.ㅠㅠ

[ 프로그래밍, 개발자 관련 좋았던 문구 정리 ]

- 안정적이고 완벽한 코드를 짜는 것도 중요하지만 때로는 시간과 타협해서 돌아가는 코드를 짜는 것만으로 만족해야 할 때가 있다.

 

- 프로그래머는 일하는 과정 자체가 콘텐츠가 되고 결과물도 콘텐츠가 되기 때문에 이러한 것들을 보다 쉽게 다른 사람들과 공유할 있다는 것만으로도 좋은 직업이라고 생각한다.  것을 공개함으로써 내가 동기부여가 되고 업그레이드되는 것들은 다른 직군도 가능하겠지만, 프로그래머가 활발하다고 생각한다.

 

- “대표이사는 시간의 80% 이상을 좋은 사람을 채용하는데 사용해야하고, 나머지 시간은 지금 있는 사람들이 회사에 남도록 하는데 사용해야한다 당신이 지금 힘들게 채용해서 만드는 team 바로 당신이 만들 회사 자체임을 잊지 말아라이다.

 

- 좋은 읽었습니다. 장기 레이스에서 현재 알고 있는 지식의 양은 크게 중요하지 않죠. 계속 좋은 방법을 찾아보고 개선해가려는 태도가 프로그래머에게는 정말 중요한 같습니다. 한편으로는, 실력차를 떠나서 동료들과의 협업도 중요합니다. 사람들과 소통하고 토론하면서 배우는 만큼 기억에 오래가는 것이 없습니다.

 

- 간단한 버그를 잡았는데, 버그를 만들었습니다. 하하하...

 

- 앞으로 남은 6개월 하고 2 동안은 프로그래밍을 좋아하는지 자꾸 묻거나 좋아하려고 노력하기보다는 그냥 잘하려고 한다. 잘하면 재미있을 것이고 재미있으면 좋아할 것임으로, 그러면 다시 잘해질테니까. 그렇게 남은 2019년의 목표를프로그래밍을 좋아할 있을 정도로 잘해지는 으로 삼았다.

 

- 만드는 사람이 수고로우면 쓰는 사람이 편하고 만드는 사람이 편하면 쓰는 사람이 수고롭다.

 

- 머신러닝을 비즈니스에 적용한다는 것은 두가지 종류의 불확실성과 싸워나가는 일이다. 하나는 데이터 부족으로 인한 불확실성이고, 다른 하나는 세상 자체가 가진 불확실성이다.

 

- 머신러닝은 시대의 꽃이다. 가장 화려하고 모두가 가지고 싶어한다. 하지만 꽃을 땅에 심어 자라나게 만드는 데에는 손에 흙을 묻히는 일이 필요하다.

 

- 실제의 사용자들은 배치의 학습주기보다 빠르게 움직이고, 쉽게 싫증을 내는 존재이기 때문이다. 사용자가 모델의 최선의 결과물을 보았음에도 반응하지 않았다면, 그에 불만 족한 것이므로 빠르게 컨텐츠를 전환하여야 한다. 

 

- 의료와 같이 잘못된 판단이 치명적인 분야라면 정확도를 기준으로 모델을 골라야 한다. 하지만 마케팅이나 푸시등 오판에 대한 비용이 적은 분야는 커버리지가 우선시 있다. 기술은 성능(performance) 추구하지만, 비즈니스는 이익(profit) 추구하기 때문이다.

반응형

+ Recent posts