Flask app with boto3, uwsgi and gevent in Docker(I. uwsgi and gevent on python3.7-alpine image)

Chih Sean Hsu
十十二夜
Published in
14 min readMay 17, 2020
From: https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.osetc.com%2Fen%2Fhow-to-install-and-use-bash-shell-in-alpine-linux.html&psig=AOvVaw02fqGYmAsVssoYFUJF_Hf4&ust=1589809335998000&source=images&cd=vfe&ved=0CA0QjhxqFwoTCPCQ272Du-kCFQAAAAAdAAAAABAH

這次來分享最近使用flask寫個要去DynamoDB拿東西,用uwsgi並開啟gevent遇到的一些小問題還有解決方法。主要分享的是以下三個主題:

  1. Install uwsgi and gevent on alpine linux image
  2. use boto3 with gevent
  3. gevent v.s. threads

然後請容我像FFVII remake一樣把這些東西拆成三部曲來做說明吧,拆成章節式的方式對我有不少好處,就是我可以多賺一些文章的數量,不然以前那種一個講了好多主題,然後沒多少人看,覺得很QQ。但現在這樣拆成多個加起來的閱讀量就變多了!有一種分開賺的感覺XD(很啊Q)。當然這也不是只有對我有好處,這樣我也可以對一個小主題做更深入或詳細的講解,應該也是不錯的事吧。

以下為此文章所使用的repo,沒什麼特殊內容,單純只是提供一個範例而已。

I. Install uwsgi and gevent on Alpine Linux image

第一點主要是針對要把uwsgi及gevent包入Alpine Linux的Docker image所做的一些處理,如果沒有考慮使用alpine作為image的os其實可以不用看篇,但還是會對另外的slim跟buster進行一些簡單的評估跟比較。

但為什麼要用alpine來當作image的os呢,下圖可以很簡單的給你一個理由。

是的,使用alpine linux可以把我們image size壓到最低(以上是只有包含可以運行python3.7以及其stdLib的image),可以看到使用alpine linux的大小基本上是Debian(buster)的1/10左右的大小而已,而為什麼我們需要在意image的大小呢?

當我們使用AWS、GCP或Azure這種由cloud platform所提供的container服務時,這就是個重點了,容量以及網路流量都是在燒錢啊。(以下使用AWS ECR為範例)

好吧,說實在看起來好像沒什麼說服力。但是使用Debian的python image加上一些app所需的package起碼是1GB左右。一個image每個月就是0.1鎂了,再加上開發可能會出很多不同版本的dev或release build,算算看如果我們每個專案都留dev和rel各10個版本,然後我們有10個專案,先撇除網路流量這些額外的變數,光是儲存image這樣我們就會有2*10*10個1GB上下的image放在ECR上面,這樣就200GB了,200*0.01。好吧⋯⋯2塊美金,好像沒多少。怎麼突然有種這篇文章滿滿的客家精神。

From: https://memes.tw/maker/painter/400#_=_

這邊放上一篇我當初在看ECS Fargate時看到的一篇文章。

https://dev.to/raphael_jambalos/secret-costs-of-ecs-fargate-4j3b

他主要是在說,因為使用Fargate的關係,雖然感覺沒有EC2要管理,但其實Fargate底層還是在使用EC2來跑的,但是因為Fargate的是on-demand的服務,所以每次運行的Container都是在不同的EC2中,不像傳統ECS或EKS那種是在已經知道的node上了。所以每次health check失敗要重啟container需要從ECR重新拉image下來運行,導致大量的網路流量,損失了大量的金錢。

雖然這屬於意外狀況啦,但如果有控制好image大小,可能這種意外發生時的損失就會減到最小。雖然覺得我好像有點在找理由說明為什麼要省這點小錢。

但有可能是我目前的見識還太狹小吧,只能想像到這種流量跟容量大小。既然我都開了這個主題還是繼續把他說完吧。

那廢話不多說,我們開始準備打包我們有uwsgi、flask、gevent的docker image吧。我們來看看使用alpine能省多少錢,然後會遇到什麼問題吧。

From: https://giphy.com/

這邊我會分別產生兩種image一個是使用Debian(buster)一個是Apline,我們先從Debian開始吧。

就這樣,然後跑個docker build --tag uwsgi-gevent:buster .。很順利地,我們馬上安裝好uwsgi跟gevent,不愧是大包的image什麼東西都有不用另外安裝。真方便,什麼意外都不會發生很順利地跑完了。

我們來看看image的大小吧。

跟之前的比起來多了30MB左右,那沒意外我們的alpine應該也是會多出這些size吧。

我們來用之前的Dockerfile的樣子來建alpine的image吧。只是要把Dockerfie開頭的From換成python:3.7-alpine,原本的是使用default的image然後因為python官方是用buster來當成default的image,所以我們把tag換成alpine版的image。

好,我們再來build一下吧,docker build --tag uwsgi-gevent:alpine .

好,我們失敗了,失敗原因看看紅字,主要是因為gevent會用到大量的C的套件,像是cffi等等,那因為我們Alpine就是主打去除很多不必要的套件,讓image輕量化,所以我們需要額外安裝像是gcc及cffi等等之類的相關套件,那我們來看看error我們缺了哪些吧。

看來我們沒有可以執行的gcc,所以我們把Dockerfile加一下gcc,alpine很方便地把一些compile需要的東西都包到build-base裡,所以我們在pip gevent前先把要的東西裝起來吧。改完後Dockerfile長這樣。

FROM python:3.7-alpineLABEL maintainer="s8901489@gmail.com"WORKDIR /rootRUN apk update && apk add \
build-base \
&& pip install --upgrade pip \
&& pip install --no-cache-dir uwsgi gevent flask\
&& rm -rf /tmp/*

這次一樣會失敗,我們可以追著error在找到一個明顯的錯誤。

少了ffi.h,我們需要安裝libffi-dev

FROM python:3.7-alpineLABEL maintainer="s8901489@gmail.com"WORKDIR /rootRUN apk update && apk add \
build-base \
libffi-dev \
&& pip install --upgrade pip \
&& pip install --no-cache-dir uwsgi gevent flask\
&& rm -rf /tmp/*

再跑一次docker build --tag uwsgi-gevent:alpine . ,這次跑完我們成功了。

成功了,不知道為什麼這次這麼快就成功了,可能是我當初在試的時候是一個一個套件裝,但後來發現了build-base這個套件,可以把我要的一次全包起來,方便很多。再來我們看看image大小吧。

看起來多了好多,因為我們額外裝了不少套件,通常在用alpine的image會把沒有用的套件都刪掉。我們來刪掉他們吧。

刪完之後image size變成132了。雖然增加的size比buster版多了一點,但整個image的size還是不在同一個量級的呢。所以如果要減少image size的話還是很建議使用alpine版。

另外我這邊順便也把slim的image也出一個吧。

然後讓我們把buster、slim和alpine擺在一起看看吧。

看起來是有很多的差異,但slim版本增加了100多MB,我想是在安裝gcc的時候另外裝了不少dependency,如果有在做優化的話我猜可以到300以下吧,但我這邊就不特別在繼續做下去了。

其實在要不要用alpine linux當os算是一個滿常遇到到的爭議點,撇除一些腩以查找的bug之外,就是build的時間了。讓我們來看看build時間吧。以下是只有抓一次,沒有取多個樣本在平均,但是從數字上看來很難有偏頗啦。

先看看buster版build的時間吧。

再來是slim版的時間。

最後是alpine要出一個image所需要的時間。

但這是以os及python套件要重新安裝的前提下(而且其實大部份主要都是花在安裝gevent及gcc這類的套件),如果套件沒有更動Dockerfile也有寫好把COPY src進去放在最後,我相信出image的時間應該是幾秒的時間而已。

好,出完image了總要試看看能不能跑吧,這邊就簡單寫個flask app來試跑看看能不能順利run flask uwsgi並開啟gevent模式。以下我就不說太多flask還有docker-compose怎麼寫了。

Folder layout長這樣,src要放到container內的/root中,最外層放各個dockerfile還有docker-compose.yml。src內部放一個entrypoint.sh以及兩種版本(threads及gevent,這個是下一章節要用到的)uwsgi的config,另外就是app裡面放個api裡面有flask的app。

首先是docker-compose.yml,會開啟3個container,也就是前面的buster, slim, alpine這三個版本的image。這邊我用一些container內的環境變數來決定是否要開啟gevent及process等等的設定。

entrypoint.sh:就簡單地由環境變數決定要用哪個config開啟uwsgi,因為gevent跟threads模式沒辦法共存,所以要這樣分開使用。

兩個uwsgi.ini,基本上差別就在於有沒有gevent及thread而已。

最後就是__init__.py、views.py、logger.py,基本上就是一些簡單的flask的東西而已。

好,接著我們就跑起這個簡單的app吧,到專案的根目錄,然後docker-compose up 順利有出完每個image的話,他就會跑起來了。

(如果有顯示TypeError: ‘module’ object is not callable不用去理他,這個好像是個bug,請看這個PR https://github.com/gevent/gevent/issues/1556

基本上你就會看到一堆相同的東西,至於要怎麼證明我們gevent有成功開啟呢,可以看到上面的紅匡cores有100以及preforking+async。另外就是直接打我們開啟api path,我分別把三個container開啟10080、10081和10082port可以打localhost:port/ping看看log長什麼樣子。

我有把log的infomation讓thread-name給顯示出來,基本上gevent也就是coroutine這類的會是使用DummyThread,因為其實他們根本沒有開thread都是單一的,只是很像multithread的行為而已。如果是用threading模式的話會長下面這樣。

到這裡,這個章節就這樣結束了。

來個結論吧,如果真的要節省開銷用很客家的方式來管理image的話請用alpine但這樣就得忍受很長的build time。想要快速且Docker Registry可以自己開或是根本不管錢的話就用buster來做吧。最後折衷就是使用slim版來做image base就是可以容量不算多也不會太久的一個選擇。

下一回我們就來討論boto3要如何使用gevent來call AWS上面的服務吧,順便一起來看看gevent的monkey patch要怎麼用。為什麼不能直接在import boto3前直接patch而要用botocore這種底層的來呼叫AWS。

References

--

--