날짜 레벨에서 정책의 유효 기간을 체크하고 알람하거나, 적용해야 하는 비지니스 요구사항이 꽤 많다.

하다 보면 한번씩 순수한 '날짜'를 제대로 편집하지 않아서 임계점에서 이슈가 생기기 마련이다.

일반적으로는 datetime으로 변환해서 대소 비교를 하는데, 비교점을 만들 때 시간을 replace 하는 것을 깜빡하면 불연속적인 정책에서 문제가 생긴다.

예시) 2월 3일 시작 ~ 3월 24일 끝인 정책이 있다고 할 때 3월 24일에 시간을 제거하지 않고 체크하면 이 정책은 3월 24일 기준 유효하지 않은 것으로 판정

 

따라서 datetime 객체를 비교할 때는 어느 레벨에서 비교하는지 생각하고 습관적으로 그 아래 세부 날짜 info를 모두 0으로 만들어주는 습관이 필요하다.

  1. s3fs 방식
    1. 장점: 메모리를 효율적으로 씀. 파일을 로컬에 쓰지 않은 상태에서 스트림으로 올라감
    2. 단점: 대용량 파일에서 업로드가 상대적으로 더 느림
    3. 예시
      1. df.to_parquet(s3_path, engine="pyarrow")
         to_parquet 안에서 s3fs를 사용하고 있어서 s3 경로를 입력하면 알아서 s3에 업로드
      2. 단, IAM이든 credentials든 인증 확인 필요
  2. buffer + byteIO 방식
    1. 장점: 메모리에 한번에 올려서 상대적으로 부하가 큼
    2. 단점: 업로드가 상대적으로 더 빠름. (대용량 파일 업로드에 적합)
    3. 예시
      1. 바이트 배열을 만들어서 버퍼에 담고 그걸 S3로 올리는 형태 
      2. parquet_bytes = df.to_parquet(compression='gzip', engine='pyarrow', index=False) buffer = BytesIO(parquet_bytes) boto.upload_fileobj_s3(buffer, bucket_name, parquet_path + parquet_name)

 

 

Requirements

 - 6억건 이상의 데이터를 매시간 insert하고 select 퍼포먼스도 유지해야 함(insert 시간은 < 5분)

 - No SQL은 지양하고 RDB에서 구현되어야 함(데이터 정합성이 매우 중요한 pricing 데이터)

 - dimension 확장으로 인한 record counts를 효과적으로 압축해야 함

 

클러스터링

 - 특정 dimension들의 value가 같은 경우 grouping하고 grouping key를 부여

 - metric을 적용하기 위해 dimension condition을 조회하는 경우 매핑 테이블과 엮여서 조회

 - 어떤 dimension들의 조합이 압축은 잘 되면서 확장성을 가지는지 테스트

 

클러스터링 방식의 특징

 - dimension들의 조합이 과해지면 debug가 어려워지고, 클러스터링이 된 테이블 자체의 크기가 커질 수 있음(결국 Load 부하를 분산하는 형태이기 때문)

 - dimension들이 모두 significant하게 작용해 dimension별 metric차이가 심할 경우, 효과가 떨어짐(궁극적으로는 특정 metric을 targeting해서 grouping 하기 때문. 정보량에서 손해를 본다는 cost가 전제되나 이것이 무시할 수 있을 정도임이 합의 되어야 함.)

기본적으로 신경써야 하는 부분

데이터 구조 설계, Block 우회, reverse engineering 여부, 멀티 프로세스(worker, job, instance), 파이프라인 설계

1. 데이터 구조 설계

 - 요구사항이 무엇인지 정확히 확인하는 게 좋지만, 대부분의 요구사항은 뭉뚱그려질 때가 많다. 그래서 대부분은 초기에 기본 구조로 만들고 필요시 추가하는 게 낫다.

 - 컬럼을 target schema에 추가하는 경우 type을 신경써야 한다.

 - 요구사항에서 가장 중요한 점은 index값을 무엇으로 잡느냐인 거 같다. 나머지는 어차피 미리 정해 두어도 바뀐다.

 - 필수적인 data dimension 파악을 통해 설계해야 한다.

 - target db에 insert된 시각은 있으면 좋다. (mysql의 경우, CURRENT_TIMESTAMP 이용)

 - 시점에 대한 값은 잘 구분해주는 게 디버깅할 때도 유리하다. cron 기준 트리거된 시간과 실제 worker에서 작업이 시작된 시간 등을 이용하면 실제 어떤 Q의 duration을 계산할 수 있고, 퍼포먼스 계산을 용이하게 해준다.

2. Block 우회

 - 현실적인 문제이다. 거꾸로 어떻게 bot을 인식하는가를 체크하고 피해야 한다.

 - ip와 browser_id의 조합으로 인식하는 경우가 많다. 사실 그 2개를 철저히 refresh하면 잡아내기 쉽지 않다.

 - browserid는 사실 client 입장에서 server가 나를 어떻게 판별할지 추정하기 어렵고, ip 변경은 네트워크 속도 저하라는 cost가 있다.

 - VPN을 유료로 써야 그나마 IP 변경으로 인한 속도 저하의 cost를 줄일 수 있다.

 - VPN의 주요 위험 중 하나는 VPN의 연결이 제대로 안 돼있는 상태에서 연결이 시도되는 것이다. 이런 케이스에서 중요한 ip를 block 시키는 사례가 생길 수 있으니 웬만하면 외부ip임을 확인하고 거기서만 테스트 하는 것을 권장한다.

-외부망에 크롤링 서버를 만들면, 내부 target db에 접속하기 어려워질 수 있으므로, instance 추가 시 방화벽을 고려해야 한다. 예시로 SQS에 쌓아 놓고, 접근 가능한 다른 region의 instance에서 가져가는 방식도 채택 가능하다.

 - block이라는 게 꼭 접속이 차단되는 형태는 아니다. garbage data를 주는 경우도 있다. block 인식을 잘못하면 data가 오염될 수 있다.

3. reverse engineering 여부

 - 대부분 웹에 노출되는 정보들은 api를 통해 출력되는 경우가 많다. api를 통해 파라미터만 바꿔서 가져올 수 있다면, 크롤링의 cost는 대폭 낮아진다.

 - 그래서 보안이 강한 사이트들은 대부분 api를 post 형태로 숨기거나, auth를 요구하면서 hmac 같은 걸 활용해 임시 키를 발급한다.(요새는 네이버가 많이 쓰는 듯하다)

 - 이게 가능한지에 따라 크롤링 퍼포먼스, cost, 아키텍쳐 설계가 급격하게 달라지니 꼭 확인이 필요하다.

 - 불가능해서 front로 작업해야 한다면, 크로미움이나 playwright + xvfn 조합을 많이 쓴다.

 

4. 멀티 프로세스

 - 3번에서 전체 reverse engineering이 가능하다면, 요구 수준에 따라 멀티 프로세싱이 필요하지 않을 수도 있다.

 - 다만 그렇지 못 하다면, 대부분은 크롤링의 가성비를 위해 멀티 프로세싱 처리와 그를 위한 구조 설계가 필요하다.

 - instance의 메모리를 관리하기 위한 health check를 만들어 적절한 worker 수준을 찾는 테스트도 필요하다.

 - 1 instance -> n workers -> job processing 구조를 위해 Q를 관리할 무언가가 있으면 좋다. [SQS나 카프카]

 - instance는 EC2를 쓸 수도 있지만, heavy하지 않은 경우 lightsail이 가성비가 더 좋다.

 

5. 파이프라인 설계

 - master data => Event trigger(or airflow) => SQS(엄격한 관리가 필요하고 데이터가 많다면 Kafka) => (VPN converter) => workers => target db

 - 기본 구조는 이렇게 잡고, 이걸 무엇으로 달성할 것인지를 데이터양이나 필요 수준에 따라 선택해야 한다.

 - master data는 구글 시트는 안 쓰는 것이 좋다. 써야 한다면 구글 시트를 주기적으로 db에 업데이트 하는 프로세스를 만들고 해당 테이블을 참조하는 걸 권장한다. 생각보다 gcp는 에러가 많다.

- master data를 extract할 때, gcp는 생각보다 불안정하다. gcp를 쓰더라도 retry 간격이 1분은 되어야 한다. 궁극적으로는 db table 같은 곳에 cache를 하고 쓰는 걸 권장한다. cache했다면, master data를 주로 수정하는 사람은 업데이트 주기를 인지할 필요가 있다.(수정 시점과 cache 데이터의 기준 시점이 달라 의도하지 않은 결과가 나올 수 있음)

 

 

6. 디버깅

 - 다양한 방식이 있겠지만 나와 협업한 베트남팀은 Sentry를 활용했다. Sentry는 특정 에러에 대해 topic을 정해놓으면 해당 topic으로 누적되어 추적하는 방식이다. 에러코드별로 시스템 에러인지, 비지니스 에러인지 분류해 비지니스 에러쪽을 세분화 체크했다.

- 대부분 시스템 에러는 봇 상태나 크롤링 대상 사이트의 상태 이슈이고 비지니스 에러의 경우 해당하는 조건으로 조회 시 item이 없다든가 혹은 결과에서 특정 조건이 충족되지 않았다든가 하는 케이스였다. display 되는 랭킹과 내부 가격을 수집해야 했기 때문에 중간중간 시스템 연결성 조건이 요구되어 까다로운 편이었다는 생각이 든다.

- 주의할 점은 초반에는 비지니스 에러를 명확한 근거를 가지고 분류를 해야 한다는 점이다. 초반부터 급하게 추정을 해서 에러를 네이밍하다 보면 기존에 추정했던 것으로는 설명할 수 없는 에러가 발생하기 마련이다. 더 문제는 이 에러의 네이밍 때문에 back단의 로직을 구성하고 결과를 분석하는 사람들이 현재 상태를 오해할 수 있다는 점이다. 따라서 현상 그대로를 description으로 하여 분류하고 좀 더 명확한 추정이 되었을 경우에 label을 붙이는 것을 권장한다.

현상: 특정 작업 완료 후 app을 taskkill.exe로 종료했더니, 해당 위치에서 무한 펜딩. Executor 프로그램은 켜져있고, 오케스트레이터에서도 해당 job을 running으로 인식.

 

조치1(activity의 버그로 추정)

 - start process의 타임아웃을 명시적으로 2000ms로 부여

조치1을 deploy했음에도 여전히 간헐적으로 증상 발생

조치2(taskkill의 연쇄작용으로 추정)

 - taskkill 대신에 kill process activity로 변경

 - 현재까지 이상 없음

1. 내가 할 수 있는 일과 내가 해야 되는 일의 범주 차이

  일의 주체가 고민되는 이유는 궁극적으로 R&R이 명확하지 않아서이다. R&R이 명확하지 않다는 것은 다음과 같은 케이스로 나눠보자.

i) 업무 프로세스 자체가 실적, 책임 소재의 회색 영역에 있는 경우

ii) 업무 프로세스는 명확하지만, 각 프로세스별 R&R이 없는 경우

iii) 업무 프로세스가 명확하지 않은 경우

  i의 경우엔 일의 임팩트가 강하지 않기 때문에 어디로든 배정되지 않았을 확률이 높다. 내가 목마른 사람이 아니라면 굳이 우물을 팔 필요가 없는 일이다. ii의 경우 R&R을 정할만한 윗 사람을 찾아서 issue raise를 해야 한다. R&R이 의도대로 정리되길 원한다면 명확한 프로세스라도 issue raise를 할 때, 내가 의도하고 이해하는 형태로 정리할 필요가 있다. iii의 경우는 서순에 따른 task를 정리하고 각 Role의 자리를 대략적으로라도 만들면서 이해당사자들과 각 role의 이해도를 맞춰야 한다. ii, iii의 경우 결국 process가 정리되는데 이때 내가 해야 되는 일인지에 더 초점을 맞춰야 한다.

  개발자(실무 수행자)에 빌붙는 중간자가 많은 이유는, 할 수 있기 때문에 해줬는데 그 공을 다른 사람이 가져가기 때문이다. 결국 실무 수행자는 실제 생산해내는 가치에 비해 과소평가 되기 십상인데, 이렇게 되는 본질적인 이유는 '내가 할 수 있기 때문에 해줘서'라고 본다. 할 수 있기 때문에 해주는 순간 그 process의 책임자는 개발자가 되고 performance는 process를 관리했던 사람이 내는 것으로 인식되는 경향이 있기 때문이다.

 

2. 할 수 있지만 해서는 안 되는 일

  따라서 개발자는 할 수 있는 것과 해야 되는 일을 명확히 구분해야만, 덜 괴로울 수 있다. 이것을 위해 문서를 정리해야 한다면 기꺼이 정리해야만 공을 뺏기고 과를 뒤집어 씌워지는 경우를 피할 수 있다. 특히 할 수 있지만 해서는 안 되는 일은, 프로세스 자체가 장기적인 변화 관리가 필요한데 내가 해야 되는 일은 아닌 경우이다. 생각보다 이런 프로세스들의 변화 관리는 한번에 찾아오고, 이를 소화하기 위한 리소스는 매번 내가 해야 할 일이 급할 때 한번에 온다. 이 일이 내가 해야 되는 일인지 파악되기 전에 무작정 하게 되면, 피라미들에게만 유명한 만능 잡부가 된다.

  만능 잡부가 되는 게 좋지 않은 이유는, '잡'일만 그렇게 찾아오기 때문이다. 그게 진정 실적이 되는 일이라면, 여기로 넘어오지 않을 확률이 높다. 리소스는 많이 드는데 실적은 잘 안 되는, 하지만 소소하게 피라미들에게는 그럭저럭 먹을만한 정도의 귀찮은 일들은 많이 한다고 스스로에게 도움이 되지는 않는다.

3. 사일로 완화를 위한 프로세스 관리

  이런 일들을 많이 해주면 개인적인 보람이 있을지도 모르겠다. 다만 조직의 관점에서는 생산성 없는 앵무새들에게 유인을 제공하는 행위가 될 수 있다. 성과가 없는 사람이 self-manage를 하지 않으면, 그 사람은 도태되도록 두어야 마땅하다. 이것이 실력주의나 공리주의처럼 들릴 수도 있다. 하지만 대부분 장기적인 성과는 일에 재능이 있느냐 보다도, 일에 얼마나 몰입하느냐에 더 의존한다. 조직은 그어놓은 선때문에 흥하고 그 선에 의해 쪼개진다. 쪼개지는 것을 막기 위해선, 더 몰입하고 실제 생산성을 추구할 수 있게 유인을 제공해야 한다.

   실제 생산성을 관찰하기 위해서는 업무 방향성을 관찰할 수 있는 도구가 필요하다. 또한 그 도구는 업무 투명성의 '비용'을 효과적으로 낮출 수 있어야 한다. 누가 어떤 일을 했는지, 그 일의 결과를 통해 어떤 것들을 할 수 있는지, 나는 왜 이 일을 해야 하는지 쉽게 알 수 있어야 한다. 그래야 협력 단계에서 각자의 할 일과 성과를 공정하게, 기꺼이 지게 할 수 있기 때문이다. Jira, confluence, redmine 등등 업무 관리 툴은 그런 방향성을 가지고 관리, 업데이트 될 필요가 있다.

현상: AR봇이 특정 시간대에 api로 수행시키는 경우, 중간에 돌다가 로깅을 남기지 않고 봇에서 펜딩되는 현상이 생김. 해당 시간대가 아니면 발생하지 않고, 오케스트레이터에서는 정상 수행중으로 로깅

refer: 이런 케이스가 UR에서 발생하는 경우 job max timeout을 설정하여 trigger하는 케이스를 들었음. 다만 이렇게 체크되는 경우 오랜 시간을 기다려 retry되게 구성할 수밖에 없음. (근본적인 원인 해결은 아님) + AR에서는 job 단위 parameter를 따로 전송하지 못 함.(process level까지만 전달. job 단위 trigger parameter는 오케 -> bot 으로만 내려감)

 

추정 원인

1. 오케스트레이터와의 인증 패킷을 교환하는 과정에서 방화벽 감시 툴이 패킷을 block함

2. ar의 경우 process 단위를 연결해 uirobot을 실행시키더라도 인증 상태의 유효기간(timeout)이 실시간으로 체크되지 않는 경향성이 있어, 중간에 패킷이 block되어 인증이 실패하더라도 오케스트레이터 입장에서는 그냥 잘 돌고 있다고 간주

 

 

디버그 방식

1. nslookup cloud.uipath.com 으로 ip 체크

2. wireshark를 깔고 해당 시간대 레코딩

3. ip.src, ip.dst로 필터링해 결과 체크

 

=> 관련 target 방화벽 해제 후 이슈 없음

 

 

상황: Sentry로 부터 오는 error log의 Json 파싱을 하는 과정에서 이슈별 파싱되는 컬럼이 달라, get & transform하는 function에서 explode한 후, 컬럼들을 표준화할 필요가 생김

col_list = ['a', 'b', 'c']

df = df.explode('d')
df = pd.concat([df.drop(['d'], axis=1), df['d'].apply(pd.Series)], axis=1)
df = df.reindex(columns=col_list, fill_value=0)

이 상황에서 대부분은 for문을 써도 크게 퍼포먼스가 떨어질 것 같지는 않지만, index를 다시 정리하는 걸 이용해 이렇게 추가할 수 있다는 게 신기해서 남겨둔다.

주의 사항: fill_value를 None으로 설정하면 인식 못 하고 pd.NaN이 된다. None을 채워야 한다면 후속 fillna 처리가 필요하다.

 

일반적인 reindex 사용은 row index의 결측값에 대해 ffill(직전값)이나 bfill(직후값)을 채우는 방식으로 보인다.

+ Recent posts