> Scaling Our AWS Infrastructure
本文由Kareem Ayesh和Yasser El-Sayed撰寫。
Meddy成立于2016年,自那時以來取得了巨大成功,這要歸功于它的規模。 2019年,我們在A輪融資?的基礎上慶祝了第100,000筆預訂和300萬名用戶服務。
在過去的四年中,Meddy經歷了許多技術更改。 本文是針對不斷成長的科技創業公司的基礎架構的技術建議。 我們將討論4年前基礎架構的起點,多年來我們面臨的所有問題以及按照實施順序實施的增量解決方案。
如果您是一家成長型初創公司的抱負CTO,那么本文將為您帶來極大的好處。
我們的整體
我記得當首席執行官問我為什么說"因為成千上萬的人使用它"時,CEO告訴我說他的代碼比我編寫的任何代碼都要好。
…他是對的。

> Monolithic Infrastructure
技術債務
單獨擁有一個整體不是問題。 對于大多數整體而言,這是很自然的,它會優雅地進化并轉變為科學怪人,而這正是我們所做的。
話雖如此,這也意味著很多問題將在項目的整個生命周期中顯現出來,并且肯定有很多問題確實存在。
但是,我們擔心的不是技術問題,而是無法解決的問題。
整體的存在意味著問題等于死胡同。 很自然地,要求的功能越多,就會表現出更多的問題,解決這些問題與創建功能一樣自然。
最初的重構和基礎結構的更新對于建立應用程序的增長至關重要,從而為各個方面的潛在問題提供了潛在的解決方案。 這樣一來,新功能請求就不會產生一系列呃和或更糟的"我認為現在不可能"。
踏腳石
在當前的整體架構中,數據持久性是一個嚴重的難題。 圖像上載,數據庫和日志都必須備份在根文件系統上,并在需要啟動新實例時使用。 由于那時沒有暫存實例,因此在生產上進行測試,并且服務器始終都在發生故障,從而危及我們的數據!
需要解決的另一個大問題是部署。 部署是通過SSH和git pull從我們的Github起源完成的,沒有任何類型的腳本可以自動啟動新進程,運行測試和報告失敗。
解決方案
顯然,我們需要一些改變。 我們決定,隨著應用程序代碼的更改,我們將進行小的基礎結構更改,從而將數據庫和文件上載移至單獨的服務。
通過使用三個AWS服務解決了這三個問題:
· 使用RDS管理Postgres數據庫
· 使用S3存儲媒體上載
· 使用ElasticBeanstalk管理部署

> Separate Data Sources ?
重申一下,我們在此項目中所做的總體更改并未帶來基礎架構的很大改善。 這種分離只是意味著應用程序中將來的問題不會損壞任何敏感內容。 此外,由于主EC2實例上只有一個應用程序,因此這意味著任何其他資源都可以存在于單獨的服務器或服務上而不會出現問題。
這不是完美的:
· Celery和redis與應用程序本身在同一服務器上
· 前端應用程序是AngularJS和Django之間的混合渲染,有時會阻塞處理器
· 日志存儲在文件服務器上。
那時,數量很少,我們沒有很多服務甚至功能,因此這些問題是可以容忍的。
盡管如此,該項目為最終峰會奠定了非常重要的墊腳石。
搜索
特別感謝Yusuf Musleh的幫助
搜索很麻煩,因為它花了很長時間并且不夠準確。 我們使用Postgres的Trigram相似性實現來實現搜索。 在大量記錄上,它不是最快的,并且在多個字段上的搜索根本不準確。
最重要的是,您無法真正控制這些搜索的行為。 我們想跟蹤這些搜索查詢所遇到的所有問題。

> A google Sheet containing all the improvements to search we want to do
每當我們出現錯誤的行為時,我們都無法通過當前的實現解決該行為。
解決方案
搜索會占用其實例中的大量資源。 我們決定不只是擴展實例并在其中運行搜索應用程序,而是決定啟動一個單獨的服務,該服務在EC2實例上本地運行ElasticSearch。 每當創建或更新新記錄時,我們都會在EC2實例中更新索引。 還有一個cronjob會定期更新整個索引。

> Add ElasticSearch ?
這樣可以更快,更好地控制搜索,我們不斷地進行迭代,而無需對ElasticSearch服務上運行的圖像進行太多更改。
負載均衡
我們在每個部署上都經歷了10秒鐘的停機時間。 部署由ElasticBeanstalk負責。 在較低的級別上,EB并行運行新(ondeck)和舊(當前)應用程序,并在所有執行腳本成功后執行符號鏈接。 由于我們使用的是一臺服務器,因此此符號鏈接將導致5-10秒的停機時間需要解決。
解決方案
要解決此問題,我們需要在多臺服務器上進行滾動部署。 這是一項相對簡單的工作,只需要在由我們的ElasticBeanstalk管理的Autoscaling組前面安裝一個Application Load Balancer。

> Add Load Balancing ?
滾動部署增加了測試的開銷和更長的部署時間,但完全消除了停機時間。
渲染和seo
我們使用的是混合渲染,其中Django應用程序將提供部分渲染的html,并在客戶端上運行AngularJS應用程序。 這引起了許多問題:
我們的渲染速度很慢,并阻塞了開發團隊和服務器。 我們的服務器需要大量的處理能力才能進行渲染,這是我們響應時間不可避免的瓶頸。 甚至開發也變得困難,因為我們需要多次編寫更改代碼。 在某些時候,即使更改單個跟蹤事件也很困難。
較差的構建系統導致較高的加載時間。 因為沒有用于混合渲染的AngularJS構建管理器,所以我們使用了自己的構建系統,并且我們自己的系統存在很多捆綁不一致和緩存問題,導致未優化的構建和較高的加載時間。 這也是改變和改進的麻煩。
網站有時會掛起5-10分鐘。 當我們收到來自用戶或漫游器的大量請求時,服務器將打開與數據庫的太多連接,從而導致數據庫掛起。 在我們的A系列為Meddy帶來更多業務之后,這種情況在2019年底每周發生一次。 原因是因為Django打開數據庫連接并保持打開狀態,直到HTML渲染結束! 由于數據庫連接過多,服務器將掛起并最終為504服務。
解決方案
在系列A結束后不久,我們便著手將代碼從混合渲染器轉換為純客戶端渲染器。 為此,我們必須努力對后端服務請求的方式進行很多更改。
我們決定使用S3存儲桶作為前端的靜態托管,CloudFront分發響應。 CloudFront還為我們提供了附加Lambda函數的選項,以對我們所需的請求進行非常精細的控制。
我們進行混合渲染的主要原因之一是擔心SEO會被動態渲染弄臟。 并且我們網站被掛起的主要原因之一是由于漫游器導致的請求高峰。 這兩個問題都可以通過預渲染我們的頁面并將預渲染的頁面提供給bot來解決,這對于SEO來說是完美的。
為此,我們使用了一個名為prerender.io的服務,該服務可預先渲染請求的頁面,緩存這些頁面并將這些頁面提供給機器人。 我們在CloudFront發行版使用的Lambda Edge函數中添加了基于HTTP請求的用戶代理的自動程序檢測機制。 只要檢測到漫游器,就會將其重定向到prerender.io以獲取其緩存的頁面。

> Add Pre-rendering and Bot Detection ?
需要注意的是,一旦我們達到300,000頁并且prerender.io的成本很高,我們便決定擁有自己的本地預渲染服務。 我們使用S3和ECS Fargate實施了自己的緩存機制來托管服務。
共享緩存和計劃任務
緩存資源未在服務器之間共享。 緩存資源無法在服務器之間共享,這意味著某些請求將被緩存而其他請求將不會被緩存。 此外,我們在服務器上存在Redis的問題; Redis已在服務器上安排了數據轉儲,這兩次導致磁盤服務被另一服務吞噬時導致停機。
在一臺服務器上運行所需的計劃任務。 Celery Beat具有內置功能,可以在特定日期和時間運行任務,并提供事件流,該事件流以表的形式存儲在數據庫中。 這對我們非常有用,因為我們使用它在約會前后的特定時間間隔發送約會的提醒SMS。 如果將其保留在服務器上,則會執行重復的任務,這將主要在我們的通知模塊內引起沖突。
解決方案
由于我們現在正在負載平衡器上使用多個服務器,因此后端必須共享一個類似的緩存服務器。 為此,我們使用了ElastiCache。 設置非常簡單。 更具挑戰性的方面是我們的異步任務管理器,該管理器通過Celery Beat在每臺服務器上進行管理。
為了消除這種耦合,我們將在新服務上創建事件,該服務將通過向后端發出請求來調用這些事件。 該事件將僅存儲需要調用的函數,該函數的參數以及調用它的時間。它具有自己的數據庫,后端可以根據后端的請求填充事件。

> Move Redis and Celery to Separate Services ?
我們稱此新服務為Celery Beet,是因為我們認為Celery Beat庫錯過了這么好的機會。
這有一個主要的缺點:調用請求是通過HTTP完成的,因此您無法泡菜。 但是,由于Celery建議不要使用Pickle,因此我們從未在代碼中使用它。
重定向和鏈接縮短
縮短鏈接的重定向發生在客戶端應用程序上。 轉移到純客戶端應用程序意味著服務器無法在鏈接上提供301服務,因為所有請求都發送到了S3存儲桶。 當必須從/ x重定向到/ y時,客戶端應用程序必須向服務器檢查有關/ x的信息,并通過向DOM添加一個額外的參數來通知Prerenderer該頁面將被重定向。 為了確保Prerenderer能夠繼續使用,我們創建了一個頁面,上面寫著"重定向您,請稍等片刻",并添加了額外的參數。
這對于內部重定向來說很好,因為我們沒有那么多。 對于縮短鏈接而言,這不是理想的選擇,因為我們希望用戶無需等待就可以直接進入那些縮短的鏈接,尤其是當我們需要在Meddy外部重定向時。
解決方案
為了解決這個問題,我們創建了兩個Lambda函數,這些函數提供了具有哈希和鏈接的Dynamo表。 一個Lambda函數的任務是縮短,而另一個Lambda函數的任務是重定向縮短的鏈接。
此外,我們使用API網關作為重定向lambda函數的入口,并將其附加到這些URL的新(較短)域中。

> Add Link Redirection using Lambda ?
這非常有效,因為Lambda實際上不提供維護,使用情況圖,并且我們的數據庫不必包含這些重定向的大型表。
記錄與監控
迫切需要更好的日志記錄。 服務器請求數量的增加意味著我們不再依賴旋轉文件系統上的日志,因為文件旋轉得如此之快。 最重要的是,我們正在使用的服務數量正在增加,并且從不同文件系統中提取文件是一場噩夢。 為所有服務使用集中式日志記錄系統是一個挑戰。
解決方案
我們決定使用Grafana Loki構建日志記錄和監視服務。 Loki是受Prometheus啟發的開源水平可伸縮的多租戶日志聚合系統。 而Grafana是開源分析和交互式可視化軟件。 本質上,我們使用Loki作為日志聚合工具,使用Grafana可視化這些日志。
想法是讓我們的不同服務將日志推送到在Loki服務中打開的HTTP端點。 然后,Loki將為它們建立索引并將日志以二進制格式存儲在S3中,并將索引存儲在DynamoDB表中。 這為我們提供了一種在日志中查找某些標簽的方法,例如特定的狀態代碼或特定的日志級別。 它還使我們能夠查詢日志,甚至可以對它們應用聚合功能,例如計算最近5分鐘內具有特定標簽的日志數量。

> Add Logging & Monitoring using Grafana Loki ?
對于事件監視,我們使用Grafana,但這次使用AWS CloudWatch。 Grafana充當CloudWatch的客戶端,并按需輪詢數據以使其可視化。
為了能夠如上所述將日志推送到Loki,我們必須使用來自不同服務的不同技術,例如在應用程序服務器上構建處理程序以及附加到數據轉儲的Lambda函數。 Lambda函數用于解析S3中的Load Balancer和CloudFront日志,然后將它們推送到Loki。

> One of our many screens on Grafana
結合使用數據處理程序和CloudWatch,我們能夠為基礎架構中的所有內容提供集中式日志記錄和監視系統,您可以在一個位置監視所有內容。
全部一起

> All done!
這可能是迄今為止我們最喜歡的文章! 由于我們經歷了許多挑戰,因此顯示出了很大的增長。 最令人興奮的是,這仍然是Meddy的開始。
感謝您的閱讀! 敬請期待更多!
(本文翻譯自Yasser的文章《Scaling Our AWS Infrastructure》,參考:https://medium.com/swlh/scaling-our-aws-infrastructure-9e64e6817b8c)