엔지니어링 블로그 - 프록시: 초고속 검색 및 유지보수 규칙
첫 번째 엔지니어링 블로그에 오신 것을 환영합니다. 다른 블로그에 비해 조금 더 기술적인 내용일 수도 있지만, 모두가 이해할 수 있도록 최대한 노력했습니다. 이 글에서는 지속성 규칙을 적용하는 데 사용되는 Clonable 새로운 소프트웨어인 프록시에 대해 설명하겠습니다.
Fondo
Clonable 사용자라면 이미 유지 관리 규칙 기능에 익숙하실 것입니다. 이러한 검색 및 유지 규칙을 사용하여 텍스트 또는 코드 조각을 선택한 변형으로 바꿀 수 있습니다. 예를 들어 이를 사용하여 API 클레이브 또는 분석 ID를 대체하여 원본 사이트와 번역된 복제 사이트에 대해 서로 다른 분석을 생성할 수 있습니다. 내부적으로도 이러한 동일한 규칙을 사용하여 원본 도메인 이름을 클론의 도메인 이름 및 기타 여러 가지로 바꿀 수 있습니다.
Clonable 첫 번째 버전부터이 기능은 업데이트되지 않았으며, 그 자체로도 놀라운 일이 아니기 때문에 작업이 매우 잘 수행되었습니다. 그래서 렌더링과 사용 편의성 측면에서 더 나은 기능을 도입할 수 있었습니다. 때때로 Clonable 오랜 시간 동안 사용하지 않았던 제품의 일부를 생각하여 개선을 시작했습니다. 예를 들어, 작년에 우리는 더 빠르고 확장 가능하며 오류에 더 잘 견디는 데이터 인프라를 만들기 위해 모든 데이터 인프라를 재검토했으며, 그 전에 URL 번역 기능도 처음부터 재구성했습니다. 이번 3분기는 지속성 규칙으로 전환되었습니다.
이전 상황 및 제한 사항
유지 관리 규칙은 고객에게 응답을 보내기 직전에 마지막 단계의 Clonable 적용됩니다. 따라서 Clonable 첫 번째 버전은 OpenResty에서 만든 모듈 인 replace-filter-nginx-module을 사용하여 NGINX 웹 서버에서 구현하기로 선택했습니다. 이 모듈은 OpenResty에서 만든 정규식 스트리밍 모터로 sregex를 사용합니다. 이 경우 모든 응답을 메모리에 완전히 로드하고 싶지 않기 때문에 흐름 측면이 중요합니다. 그렇게 하면 대용량 응답으로 인해 NGINX가 종료되거나 사용 가능한 메모리가 충분하지 않아 종료될 수 있습니다. 스레스엑스의 두 번째 중요한 특징은 여러 개의 라인을 병렬로 처리할 수 있다는 것입니다. 이는 각 클론에 결함에 따른 몇 가지 규칙이 있고 때로는 클론을 위해 특별히 추가 된 규칙이 있기 때문입니다. 병렬로 처리할 수 없는 경우에도 첫 번째 규칙에서 두 번째 규칙으로 다시 일치시킬 수 있어야 하므로 모든 응답을 저장해 두어야 합니다.
하지만 지난 해에는 매번 더 많은 비정상적인 성능 문제가 발생했습니다. 이러한 문제는 대부분 짧은 시간 동안 발생했지만 극단적인 경우 한 페이지의 응답 시간이 초 단위로 늘어날 수 있었습니다. 그 중 한 번은 페이지가 정상적으로 다시로드되어 문제를 재현하기가 어려웠습니다. 그 후 모든 응답에 복제 가능한 타이밍 탭을 추가하여 번역 프로세스가 어느 단계에서 지연되고 있는지 대략적으로 파악할 수 있었습니다. 이것은 거의 모든 경우에 상승 흐름이 빠르게 응답하고 번역이 문제없이 수행되었음을 보여주었습니다. 그러나 번역 완료와 요청 완료 사이에는 약간의 지연이 있었으며, 이는 지속성 규칙을 통해 확인할 수 있는 한 가지 단서를 제공했습니다.
NGINX 동시 접속 모델
그러나 여전히 이러한 지연이 왜 일시적으로만 발생하는지, 그리고 서버가 용량 한도까지로드되었을 때 왜 이런 일이 발생하는지에 대한 답변은 제공하지 않습니다. 이 현상을 더 잘 이해하려면 NGINX가 작업 부하를 관리하는 방법에 대해 조금 더 자세히 알아볼 필요가 있습니다.
NGINX에는 거장적인 프로세스 아키텍처와 수많은 작업 프로세스가 있는 워크플로 아키텍처가 있습니다. 작업자 간에 연결이 배포되어 여러 작업을 동시에 처리할 수 있습니다. 그러나 이 구성에는 불편한 점이 있습니다. 한 작업자가 하나의 작업을 처리하는 동안 나머지 작업자들도 같은 작업자에게 할당된 작업을 기다려야 한다는 것입니다. 이로 인해 한 작업자는 할 수 있는 일이 없는데도 다른 작업자는 여러 작업에서 지연을 겪을 수 있습니다. 이 효과는 아래 이미지에 잘 나타나 있습니다. 이 이미지를 만드는 동안 10개의 서로 다른 연결을 통해 스트림 테스트가 실행되고 있습니다. 이미지에는 세 명의 작업자가 사용 중이고 한 칸은 아무것도 하지 않고 있음을 분명히 보여줍니다.

프록시의 시대
이 문제를 명확하게 파악한 후 이를 개선하기 위한 계획을 세웁니다. 이 프로젝트는 RegEx와 Proxy의 합성어인 프록시라고 불렀습니다. 최종 결과물에는 세 가지 주요 요구 사항이 있었습니다:
현재 설정을 직접 대체해야 합니다. 기존 설정의 일부가 손상되지 않도록 지원 규칙을 동일한 방식으로 적용해야 합니다.
솔루션이 과도한 메모리를 사용하지 않도록 유지 관리 규칙을 전송해야 합니다.
새로운 솔루션은 현재보다 더 빨라질 것입니다.
우선, 우리는 효율적으로 요청을 처리할 수 있는 강력한 멀티힐러 런타임을 찾아야 했습니다. 여러 가지 옵션을 고려한 끝에 도쿄 실행 시간에서 Rust 프로그래밍 언어를 선택했습니다. Rust는 C++와 같은 다른 저수준 언어의 보안 위험 없이 빠르게 프로그램을 작성할 수 있는 것으로 잘 알려져 있습니다. 도쿄 실행 시간은 네트워크 애플리케이션을 위해 설계된 유연한 동기식 실행 시간입니다. 우리에게 가장 중요한 특징 중 하나는 바로 일의 속도입니다. 즉, NGINX와 달리 작업자가 작업자에 종속되는 것이 아니라 할 일이 없는 작업자가 다른 작업자에게 작업을 '로바'할 수 있다는 것을 의미합니다. 이렇게 하면 서버 리소스를 더 잘 활용할 수 있습니다.
첫 번째 프로토타입
하위 기술을 선택한 후 초기 프로토타입을 만들어 이 프로젝트에 제공할 속도 향상을 대략적으로 추정하고 그만한 가치가 있는지 판단하기로 결정했습니다. 정규식 모터(실제로 규칙을 처리하고 적용하는 부분)의 초기 구현으로 표준 정규식 라이브러리(또는 Rust 용어로 크레이트라고 함)를 사용했습니다. 이 모터는 정규식처럼 백트래킹을 하지 않으므로 ReDoS 문제의 위험이 적습니다. ReDoS에서는 정규 표현식이 입력에 직면하여 입력 평가에 걸리는 시간이 기하급수적으로 증가합니다. 이는 성능에 치명적일 수 있으며 Cloudflare가 중요한 중단을 일으켰습니다. 따라서 우리에게는 사용하는 모터가 백트래킹되지 않는 것이 중요합니다.
이 정규식 모터를 사용하여 초기 프로토타입을 만들어 보겠습니다. 이 프로토타입은 코딩된 규칙을 사용했고 정규식 모터는 응답을 전송하는 대신 모든 응답을 저장했지만 초기 렌더링 테스트에는 충분했습니다.
이 정규식 모터를 사용하여 초기 프로토타입을 만들어 보겠습니다. 이 프로토타입은 코딩된 규칙을 사용했고 정규식 모터는 모든 응답을 전송하는 대신 저장했지만 초기 렌더링 테스트에는 충분했습니다. 테스트 환경은 두 대의 가상 머신으로 구성되었으며, 그 중 하나는 이전 솔루션과 새 솔루션을 모두 실행할 수 있는 설정을 실행했습니다. 이렇게 하면 두 가지 방법을 쉽게 비교할 수 있습니다. 다른 서버는 원본 서버로 작동하고 테스트 파일을 제공하는 NGINX를 실행했습니다. 동일한 서버는 또한 우리가 로드를 생성하는 데 사용한 wrk 도구를 실행했습니다. 데이터의 경우 별도의 가상 머신에서 로드 생성기를 실행하는 것이 바람직하지만 이 가상 머신에는 NGINX와 wrk가 서로 간섭하지 않도록 충분한 리소스가 있기 때문에 이것이 전적으로 최선책은 아닙니다.
테스트의 첫 번째 결과는 매우 명확했습니다. Prexy는 단일 설정 관리에서 22배 더 빨랐습니다(아래 이미지 참조). 차이가 너무 커서 기능을 완료할 때 발생할 수 있는 성능 저하를 흡수하기에 충분한 여유가 있었기 때문에 이 점이 프로젝트에 결정적인 영향을 미쳤습니다.

초기 테스트에서는 컴파일된 정규 표현식 캐시에 저장하는 것과 같은 몇 가지 간단한 최적화를 구현해 보겠습니다. 또한 상승 흐름과의 연결을 보다 효율적으로 재사용하는 방법도 구현합니다. 이를 통해 지연 속도를 20%까지 높일 수 있을 것으로 예상합니다. 또한 최대 부하 조건에서 두 가지 솔루션을 모두 시도하여 서버에 가능한 최대 수의 요청을 보냅니다. 이 경우에도 차이가 명확하게 드러났고 Prexy는 이전 솔루션보다 25배 더 나은 성능을 보였습니다.

정규식 보안 모터
이 프로젝트의 요구 사항 중 하나는 사용 된 정규식 모터가 스트리밍을 실행하는 것이 었습니다. 프로토타입의 정규식 생성은 그렇지 않았기 때문에 이 부분에 대한 추가 작업이 필요했습니다. 그러나 정규식 상자는 결과적으로 매우 최적화되어 있었기 때문에이 구현을 스트리밍 모터의 기반으로 사용하기로 결정했습니다. 정규식 모터를 통해 데이터를 전송할 때 몇 가지 중요한 사항을 고려해야 합니다. 우선, 어떤 데이터를 전송할지 모른다는 것입니다. 따라서 아직 완전하지는 않지만 이미 1개 이상의 문자가 일치하는 부분 일치로 작업을 시작해야 합니다. 완전한 우연을 발견하면 부분적인 우연이 없는지 확인해야 하는데, 이는 이들도 우연이 될 수 있기 때문입니다(그리고 더 큰 우연은 더 작은 우연보다 더 작은 우연이 될 수 있습니다). 따라서 현재의 모든 부분적인 우연이 완전한 우연이 아닌 경우에만 우연 처리를 시작할 수 있습니다.
기타 최적화
예상했던 대로 정규식 모터 재구성은 Prexy의 성능에 부정적인 영향을 미쳤습니다. 그러나 이전 솔루션과 비교했을 때 여전히 많은 여유가 있었고 최적화를 추가하여 정규식 모터를 재구성하기 전보다 더 나은 성능을 얻을 수 있었습니다. 우리가 적용한 최적화 중 하나는 구체적인 파일에 규칙을 적용했는지 여부를 기록하는 것이었습니다. CSS 및 JS 파일과 같은 정적 요소는 거의 변경되지 않으므로 특정 파일에서 일치하지 않는 경우 매번 규칙을 실행하는 것은 쓸모가 없습니다. 이에 대한 대안은 교체 결과를 캐시에 저장하는 것이지만 이 전략의 단점은 모든 파일을 저장하는 데 많은 메모리가 필요하다는 것입니다. 어떤 규칙을 적용할지 기록할 때는 정보를 비트 맵으로 저장하는 파일당 몇 바이트만 필요합니다.
또한 대체에 사용되지 않을 때 캡처 그룹을 보존하지 않는 등 정규식 모터 최적화를 최대한 활용했습니다. 이렇게 하면 정규식 모터는 모든 우연의 시작과 끝을 기록하기만 하면 되므로 이제 작동합니다. 또한 이름이있는 캡처 그룹을 비활성화했는데, 이는 유동적 인 변형에서 매우 완전한 결과를 가져 왔으며 이전 솔루션에서도 허용했기 때문에 반드시 필요한 것은 아니 었습니다. 이 모든 최적화는 최종적으로 다음과 같은 렌더링을 제공했습니다:

최종 결과
Prexy를 철저히 테스트하여 성능이 기존 솔루션과 동일한지 확인한 후 점진적으로 제거하기 시작했습니다. 약 일주일 동안 각 클론에 대해 프록시를 활성화합니다. 모니터링 결과, 활성화 시점을 확인하기가 어려웠습니다. 응답 시간에서 50%의 부하 감소를 보는 것은 드문 일이 아니었습니다. 또한 유지 관리 규칙이 거의 없는 다른 고객과도 10%의 약간의 차이가 관찰되었습니다. 자체 웹 사이트는 30% 더 빠르게(~50밀리초) 재가동되었습니다.
우리 인프라의 맥락에서 Prexy를 도입할 때 다음과 같은 차이점을 관찰했습니다.
RAM 사용량 33% 감소
CPU 사용량 20% 감소
10%에서 최대 50%까지 더 빨라진 응답 속도
결론적으로 Prexy는 성공적인 프로젝트였다고 말할 수 있습니다. NGINX의 최신 모듈을 유지하면서 이제 모든 고객에게 뚜렷한 차별화를 제공하는 더 새롭고 효율적인 기술을 사용할 수 있습니다. 향후에는 기존 기능의 개선뿐만 아니라 새로운 기능의 측면에서 Clonable 지속적으로 개선해 나갈 것입니다.
이 개발자 블로그를 읽어 주셔서 감사합니다. 제품에 대한 이러한 유형의 기술 정보를 더 자주 받아보시길 바랍니다. 이 프로젝트에 관심이 있으신가요? 그렇다면 저희 공석 페이지를 방문해 주세요 :)