{"componentChunkName":"component---src-templates-post-template-jsx","path":"/works/posts/2024-07-14--001","result":{"data":{"site":{"siteMetadata":{"title":"Blog by Eunyoung","subtitle":"작업 기록 블로그","copyright":"© All rights reserved.","author":{"name":"EunYoung","twitter":"#"},"disqusShortname":"","url":"https://ssongey.github.io"}},"markdownRemark":{"id":"339f60e4-8b58-5e52-a2e1-af82ddbf9bd3","html":"<p>이번에 톡사원증이 오픈을 하게 되었다.<br/>\n톡사원증은 특정 증명서 발급을 통해 회사 이력을 카드로 인증해주는 서비스이다.<br/>\n요 기쁜 소식을 사내에 오픈 공지를 올렸을때, 순간적으로 트래픽이 살짝 높아지게 되었는데, 이때 증명서 발급 신청 건이 우수수 실패가 되었다.</p>\n<p>맙소사… 아직 대외로 오픈소식을 알리기도 전인데 이게 왠 날벼락인가…<br/>\n이대로라면 프로모션시에 큰 장애가 날 듯 싶어 급하게 rate limiter 를 개발하게 되었다.</p>\n<h2>요구사항은,</h2>\n<ol>\n<li>증명서 별로, 발급 신청시 rate limit 을 적용한다.</li>\n<li>분산환경에서 동작해야 한다.</li>\n<li>발급신청시에 요청수 체크 및 카운트를 증가 시키고, 그외 특정 api 들에서도 요청수 체크를 할 수 있어야 한다.</li>\n</ol>\n<br/>\n<h2>구현방법</h2>\n<p>rate limit 를 구현하는 방법은 다양하다.</p>\n<p>nginx에서 사용자 IP 별 요청을 제한하거나 어플리케이션 단에서 queue를 이용하는 방법, 또는 redis 를 이용하는 방법 등 여러가지 방법이 있는데, 이중 최대한 쉽게 구현할 수 있는 redis 를 사용하기로 했다. (예시도 많고..)</p>\n<p>그리고 방법은 2가지를 정할 수 있는데,</p>\n<h3>구현방법1) 고정 윈도 카운터</h3>\n<ul>\n<li>타임라인을 고정된 윈도우로 나누고, 각 윈도우마다 카운트 체크를 한다.</li>\n<li>카운트가 임계치를 넘어가면 새로운 윈도우가 열릴때까지 요청은 버려진다.</li>\n</ul>\n<p>\n  <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/devHistoryBlog/static/328e1684ea378d8b8658a13d91b33830/bc3ae/01.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n  \n  <span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block;  max-width: 960px; margin-left: auto; margin-right: auto;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 52.916666666666664%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAABP0lEQVQoz31SCY6DMAzk/z/rH2C3UFigpRwNN5SpJ1ojWLFYshxn7ImPOBB5v99Wj4T38zyf4ootywKHh2marKMBPKulkFATtrjeMX9HWFUVmqZBnucIggBRFOF2uyHLMgzDgDRNMY6jtbwPw9Cq5tHuCEngui7u9zviOLaaJIklIHa5XKzl3RZnvOd5FtPuLCGr0PaORFs6EuYxf1dh23XoJamoXngWJUzbEl2T2JbOTlVnSDLiO8LumaH3v2CCK4z/jS4MMFw95DLDUuaTPR6n1fd9v/rOLC9VsoxGdBl6zFLdIgF5/APf99f5FkVht83lkISWfivxXIp+Laeua5RlieplUDctjJRvxNbGoBGMOMmM+PWvv1WSMZ9t8yHbMre5LXsrfJlV/vexOUPdsm1Zt/R36Nvh87EzXH8J9QPqQ1i0wS7TXAAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n    >\n      <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width: 100%; height: 100%; margin: 0; vertical-align: middle; position: absolute; top: 0; left: 0; box-shadow: inset 0px 0px 0px 400px white;\"\n        alt=\"01\"\n        title=\"\"\n        src=\"/devHistoryBlog/static/328e1684ea378d8b8658a13d91b33830/d9199/01.png\"\n        srcset=\"/devHistoryBlog/static/328e1684ea378d8b8658a13d91b33830/8ff5a/01.png 240w,\n/devHistoryBlog/static/328e1684ea378d8b8658a13d91b33830/e85cb/01.png 480w,\n/devHistoryBlog/static/328e1684ea378d8b8658a13d91b33830/d9199/01.png 960w,\n/devHistoryBlog/static/328e1684ea378d8b8658a13d91b33830/bc3ae/01.png 1268w\"\n        sizes=\"(max-width: 960px) 100vw, 960px\"\n      />\n    </span>\n  </span>\n  \n  </a>\n    </p>\n<h3>고정 윈도 카운터의 단점</h3>\n<ul>\n<li>이 방법은 redis 공식 문서에도 소개된 만큼 구현하기 쉬운 장점이 있지만, (<a href=\"https://redis.io/docs/latest/commands/incr/\">https://redis.io/docs/latest/commands/incr/</a>)</li>\n<li>아래와 같이 윈도우 경계부근에서 일시적으로 많은 트래픽이 몰려드는 경우에는, 기대했던 시스템의 처리 한도보다 최대 2배의 많은양을 처리하게 된다.</li>\n</ul>\n<p>\n  <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/devHistoryBlog/static/a90e342e346ec010d2d9e208f08ed059/7b1dc/02.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n  \n  <span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block;  max-width: 956px; margin-left: auto; margin-right: auto;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 52.083333333333336%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABKElEQVQoz41Sa2+DMAzs//9x3ecCQUC7VcB4Bcobbr5MQWys6iydsHzkcrZzWtcVjL7vMc8z0jQ1SJIEn1mGaRjQdR3mcURVVRsXxzHatsUo9WmajAa1TlbQ8zxcr1f4vm9yQkn+HkV4O5/hOReEQbBxxO12g+M4KIriKEgXz2IQ92L/T47u6PIgSPvMiWVZvscg7bZNDS2t6rJEXWvDWZjL5J+XgrbWNA1cpXDxFFQY4uN+f+3QEs9antMED+WiVQ5KWZKua7Mcghfmeb6dNYJUtwS/P/B4oJWBl0mMWlBJzsUFshzXdRHJwniulHGwQ47BtEzbLPLZ7GdkUWltntV+JHaGFNLC27ppmUK8yf70O3JplQdtW/tgPRP+sBQbexf7Bf2HY3wBs48Jbk/2R8YAAAAASUVORK5CYII='); background-size: cover; display: block;\"\n    >\n      <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width: 100%; height: 100%; margin: 0; vertical-align: middle; position: absolute; top: 0; left: 0; box-shadow: inset 0px 0px 0px 400px white;\"\n        alt=\"02\"\n        title=\"\"\n        src=\"/devHistoryBlog/static/a90e342e346ec010d2d9e208f08ed059/7b1dc/02.png\"\n        srcset=\"/devHistoryBlog/static/a90e342e346ec010d2d9e208f08ed059/8ff5a/02.png 240w,\n/devHistoryBlog/static/a90e342e346ec010d2d9e208f08ed059/e85cb/02.png 480w,\n/devHistoryBlog/static/a90e342e346ec010d2d9e208f08ed059/7b1dc/02.png 956w\"\n        sizes=\"(max-width: 956px) 100vw, 956px\"\n      />\n    </span>\n  </span>\n  \n  </a>\n    </p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">ts <span class=\"token operator\">=</span> CURRENT_UNIX_TIME<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\nkeyname <span class=\"token operator\">=</span> key+<span class=\"token string\">\":\"</span>+ts\nMULTI\n    INCR<span class=\"token punctuation\">(</span>keyname<span class=\"token punctuation\">)</span>\n    EXPIRE<span class=\"token punctuation\">(</span>keyname,10<span class=\"token punctuation\">)</span>\nEXEC\ncurrent <span class=\"token operator\">=</span> RESPONSE_OF_INCR_WITHIN_MULTI\nIF current <span class=\"token operator\">></span> <span class=\"token number\">100</span> THEN\n    ERROR <span class=\"token string\">\"too many requests per second\"</span>\nELSE\n    PERFORM_API_CALL<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\nEND</code></pre></div>\n<h3>구현방법2) 이동 윈도우</h3>\n<ul>\n<li>매번 현재시간을 기준으로 윈도우를 나누고, 각 윈도우마다 카운트 체크를 한다.</li>\n</ul>\n<p>\n  <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/devHistoryBlog/static/40b2d0601a8bd4b5cf80c9f7ba40486a/7960f/03.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n  \n  <span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block;  max-width: 960px; margin-left: auto; margin-right: auto;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 57.50000000000001%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAABZklEQVQoz5VTi26DMAzs///gNE0rpWODUsqrBEhIuPmyggjaQ4tkSM7JxT47h3mesR1c77EFz/McaZpCa71iWz/HYQ/+RtjUNcrbDWaafibkZ5INzjmiXHgH14yI1nXdljkg4L5pc4EnVEqtaaRvZyRJgizLEEUR4jjG8XhEdrkgPp1QF0VASDKeDwiHYYAxxoNaddDjCCsbrbVwYnVVQfU9RrFJfHvCXvCAkIAnfGwy8i/rBq3czGS0kK5D5vN/InTpO4Y4Qvn6gjY+QSdn5M9PuElBKimI7tXfEZJwERaSMozo6exXNOOAD9ExFU2pbXG9BtWnLAuhrzKBtm09SFIjGhnrfGvQrFSxvd8xCj5JFoby0Cdz7mcwTdOsnXJg/pWITlBJdJ0cZpvQ7jKnvyxLf6nqQj+N53iec17iUy6kFcZH9b5raL6OVZLdYIZsseClbDXcvhYa02CUPLj3LYT0+4ch4xPnrqtRlSRpzAAAAABJRU5ErkJggg=='); background-size: cover; display: block;\"\n    >\n      <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width: 100%; height: 100%; margin: 0; vertical-align: middle; position: absolute; top: 0; left: 0; box-shadow: inset 0px 0px 0px 400px white;\"\n        alt=\"03\"\n        title=\"\"\n        src=\"/devHistoryBlog/static/40b2d0601a8bd4b5cf80c9f7ba40486a/d9199/03.png\"\n        srcset=\"/devHistoryBlog/static/40b2d0601a8bd4b5cf80c9f7ba40486a/8ff5a/03.png 240w,\n/devHistoryBlog/static/40b2d0601a8bd4b5cf80c9f7ba40486a/e85cb/03.png 480w,\n/devHistoryBlog/static/40b2d0601a8bd4b5cf80c9f7ba40486a/d9199/03.png 960w,\n/devHistoryBlog/static/40b2d0601a8bd4b5cf80c9f7ba40486a/7960f/03.png 1274w\"\n        sizes=\"(max-width: 960px) 100vw, 960px\"\n      />\n    </span>\n  </span>\n  \n  </a>\n    </p>\n<h3>이동 윈도우의 단점</h3>\n<ul>\n<li>위 그림만 봐서는 고정 윈도우의 단점을 해결 할 수 있을 듯.. 보였지만</li>\n<li>실제 구현 방법을 보면 이 방법도 함정이 보인다..ㅠㅜ</li>\n</ul>\n<div class=\"gatsby-highlight\" data-language=\"text\"><pre class=\"language-text\"><code class=\"language-text\">current = 0\nMULTI\nZREMRANGEBYSCORE $key 0 ($currentTime - $slidingwindow)\ncurrent = ZRANGE $key 0 -1\nEXEC\n\nIF current &gt; 100 THEN\nERROR &quot;too many requests per second&quot;\nELSE\nPERFORM_API_CALL()\nMULTI\nZADD $key $currentTime\nEXPIRE $key $slidingwindow\nEXEC</code></pre></div>\n<ul>\n<li>위 코드는 sorted set를 이용한 이동 윈도우 구현 방법이고, 데이터는 현재 시간이 들어간다.</li>\n<li>sorted set 이기 때문에 시간순으로 정렬이 되며, ZREMRANGEBYSCORE 명령어를 이용하여 현재 윈도우에서 제외되는 데이터를 지운다.</li>\n</ul>\n<p>\n  <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/devHistoryBlog/static/eff7c4de3be3b5ede70b15ea88480466/fbf76/04.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n  \n  <span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block;  max-width: 960px; margin-left: auto; margin-right: auto;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 63.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAABfUlEQVQ4y31Ta2+DMAzs//9rkzbtOxOshfIYBcqbJHDzRaQqjM7SCcePi+2YE0SWZbFw4jRnJ/I8R5Km6LsOlehGa+xz+T29InwmI4qiQLoS1rd/CDdEO+Ij6YTwJuRt227sLs9WqJSCMYZWTNME1fcYBdSzLEMYhrherxiGAWVV4Sa2QfwULZUyf9NyvyZTiqJE4Hn49n07M1++npz5jYU0CAKkUfSojHn9Sv4g5M28iXNhO3mS2GqPxLY2z1gErjvm/6nQla0ZyFnuiQSksE+xIzyskA6GNP4Xuo836OgMlcbow7OF+Ukx+R6az3eM0QXzetFhhTTwUdg255OHFyh5xbFpkPEcxzAS01clksBHIbM1a4XM2RDSUNe1XQfOcBLMKznbnyWI0GJT1m8wawUlj8EY5jGf+izxJxq4tJWsA/VGqiK4Zw7dk97St/oZf7/fUZal1UlqW+aujeN4+KoMiqTtVwvPdrmjTuyf4srd/24E7Rz8kc/5me/OvzpZ98YG8RBHAAAAAElFTkSuQmCC'); background-size: cover; display: block;\"\n    >\n      <img\n        class=\"gatsby-resp-image-image\"\n        style=\"width: 100%; height: 100%; margin: 0; vertical-align: middle; position: absolute; top: 0; left: 0; box-shadow: inset 0px 0px 0px 400px white;\"\n        alt=\"04\"\n        title=\"\"\n        src=\"/devHistoryBlog/static/eff7c4de3be3b5ede70b15ea88480466/d9199/04.png\"\n        srcset=\"/devHistoryBlog/static/eff7c4de3be3b5ede70b15ea88480466/8ff5a/04.png 240w,\n/devHistoryBlog/static/eff7c4de3be3b5ede70b15ea88480466/e85cb/04.png 480w,\n/devHistoryBlog/static/eff7c4de3be3b5ede70b15ea88480466/d9199/04.png 960w,\n/devHistoryBlog/static/eff7c4de3be3b5ede70b15ea88480466/fbf76/04.png 1252w\"\n        sizes=\"(max-width: 960px) 100vw, 960px\"\n      />\n    </span>\n  </span>\n  \n  </a>\n    </p>\n<p>이때, ZREMRANGEBYSCORE 명령어와 ZADD 명령어는 한 트랜잭션에 존재하지 않기 때문에 정말 짧은 순간에 트래픽이 몰렸을 경우, 방어가 되지 않는 문제가 생긴다.</p>\n<br/>\n<h2>그래서 결론은?</h2>\n<p>각 단점을 다시 정리하면,</p>\n<ul>\n<li><strong>고정윈도우는, 정해진 시간(윈도우)에</strong> burst traffic 발생시에 최대 2배 처리가 되고,</li>\n<li><strong>이동윈도우는, 동일하거나 정말 짧은 시간에</strong> burst traffic 발생시 아예 방어가 안된다.</li>\n</ul>\n<p>하지만 전자증명서 서비스 특성상, 정말 짧은시간에 burst traffic이 발생할 확률보단 정해진 시간에 burst traffic 이 발생할 확률이 높아 두번째 방법을 선택하게 되었다.</p>\n<p>아직 개발 전이지만.. 시간이 너무 촉박하다ㅠㅜ 어휴</p>","fields":{"tagSlugs":["/tags/rate-limit/"],"slug":"/works/posts/2024-07-14--001"},"frontmatter":{"title":"rateLimit 고민 기록","tags":["rateLimit"],"date":"2024-07-14","description":""}}},"pageContext":{"slug":"/works/posts/2024-07-14--001"}},"staticQueryHashes":[]}