首頁>技術>

16年接觸了 Web 整套技術棧的我,決定在這條路上不斷攀爬,直到找到我技術上的天花板。

當時我寫了一篇《後端工程師技能樹》。現在回過頭再看,我並沒有按照葉子節點的順序去一步步做。除了系統地學習瞭解技術知識以外,我更多是在“明天上線”以及“線上掛了”的血與火的泥泊裡撲騰。

這篇就以不同的視角,記錄一下我這幾年技術上的感悟吧。由於講述的更偏 Web,所以本篇基本都是以再惠的實際開發為例。

專案、語言與框架

16年的時候,公司的業務在一個大主站裡就可以完全解決了。這單個專案不僅包括了全部後端程式碼,還包括了全部前端程式碼(不過後來前端就拆出去了)。

當時用的是 Python 2.7 + Django 1.8,不過由於 Python2 在生命的末期,後來我們就找了個週末升級到了 Python 3.5。(順帶一提,當時不升 Python 3.6 的原因是 Ubuntu 16 預設帶的是 Python 3.5)

Python 是一門非常容易上手的語言,我進再惠前其實沒有認真用過(連 virtualenv 都不知道是什麼,只會 print),但很快就可以開始參與業務開發了。

整個主站在開發週期裡,框架沒有大的變動,基本上是前人怎麼寫的,後人就怎麼寫。

當時我在語言上感興趣的幾個點,主要包括:

1)magic method 與超程式設計:

於是我實現了一套根據配置生成介面的邏輯

2)效能、併發與處理能力:

於是我們經常在小黑板上畫各類網路架構圖

3)不同框架之間的對比:

於是我們內部後面的專案嘗試使用了 flask/tornado 等各類框架

4)關於語言本身的話題:

後來業務線逐漸開始變多,我也有機會從零開始搭建一個專案。

因為是從零開始,所以我心中暗想:“以前很多東西我都是知其然不知其所以然,這次專案的所有方面我都要完全理解才行。”

於是那個月所有的業餘時間我都在讀各種文件…

不讀不知道,一讀嚇一跳。我發現我以前預設的很多習慣用法,其實都有更佳的實踐:

1)輕度使用:以前專案裡會搭配著使用 djangorestframework,但我們從來都是手寫各種序列化類,並沒有使用框架自帶的 django model 支援。而且我們自己實現了一套 schema 驗證系統,既沒有用 djangorestframework,也沒有用 marshmellow 這樣的庫。(這導致了後面支援 swagger 非常困難)

2)缺少檢查:Python 社群中有非常多的檢查工具,但我們只用到了最基礎的 flake8 來驗證 PEP8 風格。我們在合作程式設計中,因為每個人 PyCharm 配置的不一樣,解了無數次 import 的衝突。(更別提還有陣列末行加逗號的衝突了)

3)版本老舊:我們用的很多三方庫一直保持著版本更新,但我們卻一直用著舊的版本(更別說語言本身了,f-string 我們也是老後面才用上的)

這些問題在後面的專案開發中,我逐個都解決掉了:

1)輕度使用的問題:

好說,找一個哥去研究正確的用法然後去最佳化就行了。(有時這個最佳化會涉及幾百個檔案,所以需要一個 vim 用的溜的哥)

2)缺少檢查:也好說,

像 flake8/isort/pytest/pylint/yapf/black 我們都嘗試使用過,後面按照專案規模我們開了不同級別的檢查,原則上,專案越大檢查越嚴格。

3)關於語言與三方庫版本的問題:

我每週基本都會保持跟社群的更新,以人肉 dependency bot 的方式去維護程式碼。

在解決完這些明顯的問題後,19年我很開心地跟夥伴們感慨過:“我很有信心說,我們寫的這個專案就算放在開源社群也是一流的。”

框架上最終我們還是大規模使用了 Django,因為 Django 的整套 ORM 實在是對增刪改查這樣的業務太契合了。效能上我們後面嘗試並最終使用了 gevent,讓整個專案寫起來體驗一致,跑起來效能合格。

現在要我實現一個標準的 Python Web 服務的話,我會考慮使用這樣的技術組合:

使用最新的版本號,比如 Python3.8+/Django3+/Celery4.5+ 等在 CI 中開啟一系列標準檢查,比如 flake8/isort/pytest/black使用 gnumake/pipenv/drf-yasg 這樣的工具鏈使用 gunicorn+gevent 作為運營環境在語言的效能問題成為了關鍵問題時,考慮使用 golang 重寫關鍵部分(不過一般此時都要更大程度上更新架構了)平 臺

過去幾年的技術生涯裡,我最主要跟兩個平臺在打交道:一個是雲平臺 (AWS/Aliyun/Azure),另一個是業務平臺(微信開放平臺)。

最早接觸的雲平臺是 AWS(China),我覺得雲平臺最好的一點是運維扁平化。招人的時候我會跟候選人說,我們這個職位從網路、業務、資料到部署、監控都要接觸。

而能做到這一點的基礎,就是我們“去運維化”地讓大家直接去對接雲平臺(有些地方可能會簡單包一層)。

最早我們用的是 AWS(國服),相比於國際服,國服使用者缺少了一些非常基礎的設施(比如像 ACM/Route53 等),導致不論是像 zappa 這種跑 serveless 的庫,還是 AWS EKS 這種更高階的服務能力都是缺失的。

在18年底我司就從 AWS 切換到了 aliyun。

其實整體的架構沒有本質上的差別,感受上阿里雲的服務的確好些,不過按 @lxkaka 的說法也可以叫:“他們這個系統假如沒地方問的話說不過去啊!”

目前我們使用雲平臺的姿勢包括:

最基礎的開機器、負載均衡、域名一系列資料相關的 MySQL/Redis/Mongo/EMR 一套監控報警日誌相關係統全套託管的 K8S

隨著對雲平臺使用的更加深入,跟雲平臺強繫結的技術也會越來越多,比如像日誌系統就基本拋棄了 ELK 擁抱了阿里的日誌。

但從成本的視角上看,對工具的使用減少了冗餘的運維需求,一定程度上是解放了工程師的時間與效能。

現在要我從頭開始搭建雲平臺的基建的話,我會考慮這樣的實現組合:

拆分 VPC 網段,大部分情況分生產、測試、訪客三個網段就可以了(並輔以合適的安全組策略)以託管的 K8S 服務為核心搭建業務系統,配上配套基建(雲盤、日誌、監控等)用 LB/Gateway 約束網路入口、出口,拆分各網段之間流量,儘量減少網路上的損耗選型時優先考慮雲原生功能,如 MySQL/ES/MQ 等

在大主站時期,我們的 Python 服務以 supervisor+virtualenv 裸部署在三種機器上:

Web: nginx+uwsgi+djangoWorker: celery workerCron: celery beat

此時的更新程式碼是用 fabric 直接連入機器 git pull + supervisorctl restart 二連。

這樣的問題是無縫發版(藍綠部署)是需要自己手動實現,比如我們最早實現了一套基於 AWS LB 的動態新增、摘除節點的邏輯。

這部邏輯稱不上優雅,也需要自己維護。而且這麼做對機器環境有著強依賴,在前文的升級 Python 版本中,我們也需要一併進行系統級別的升級。

不過後來很快我們就進行了全站的 docker 化,並有過一段短暫的基於 docker network 的無縫發版實現。

此時的部署換成了 docker pull + docker(compose) restart。整條技術鏈路中我們摘掉了 supervisor/fabric/system 相關的依賴。

伴隨著平臺從 AWS 遷移到 aliyun,我們大部分服務也上了 K8S。部署也從上機器部署升級到了 k8s 相關的部署工具鏈。

大部分情況專案裡用的是手寫的 envsubst + kubectl,不過 kubectl 對版本的支援非常有限,所以很多時候我們也會附帶使用 kustomize。

helm chart 而言對業務系統提供了多餘的版本控制功能(我們一般在線上不會同時跑很多個版本,往往只會保留最新版跟灰度版)。

但 kustomize 也僅在輸出部署檔案上做的比較好,在展示部署進度上並沒有特別的功能,而且很多時候會在專案裡漏一大堆的 configmap…所以到目前,我們不少專案都使用了 kapp 進行部署。

而在構建上,我們從最早的 Jenkins CI 全線遷移到了 GitLab CI。除了整合單元測試、體驗版自動更新、灰度釋出這些核心流程以外,我們還深度嘗試了許多 GitLab CI 提供的工具整合。比如像 kaniko/minio+artifacts/gitlab+sentry 等系列自動整合自動部署的工具基本我們都用到了。

時至今日,我考慮新起的一個 Python 服務會包括以下的技術組合:

核心部署流程基於 K8S,生產測試使用相同的配置,以 namespace 區分不同組的業務不採用 helm, 而是使用 kubectl+kustomize+kapp 的方式完成部署以合適的姿勢起新三樣:

①對於 HTTP/RESTful 服務, Web 上使用 gunicorn+django (uwsgi 年久失修了)

②對於內部的 gRPC 呼叫服務,使用內部的 djangrpc 實現 (基於 django,支援一套程式碼起 http+grpc)

對於以上提到的服務,部署中考慮完整的向前相容、按流量/使用者的灰度、標準的監控日誌報警的搭建。

架 構

早期我們的網路拓撲相對簡單,流量的路線是 外部 ==> aws elb ==> nginx+uwsgi+django(單臺機器),我們僅在整條鏈路上做了少數配置,比如在 aws elb 上配置 https 的處理,在機器上做了簡單的日誌收集。

而現在我們的網路拓撲有多種路徑,以其中相對標準的阿里雲上託管的 K8S Web 服務為例:外部 ==> ali slb ==> k8s ingress ==> nginx ==> gunicorn+django.

可以比較發現除了 k8s ingress 層外,nginx 層也被單獨拆分了出來。這樣的網路拓撲我們對其的控制粒度更加精細,不僅可以在每一層單獨處理 IP/流量/日誌/行為 等邏輯,而且每一層也都是可拆卸可更換的。

比如目前我們的叢集中,就有使用到 nginx-ingress-controller 的,也有使用 kong-ingress-controller 的。而在整體的服務架構上,我們拆分了三層的服務:

1)頂層是 Web 層,這些服務主要對外部提供服務,走的主要是公網流量的 RESTful 呼叫;2)中間是 Service 層,這些服務主要對內部提供服務,走的主要是內網流量的 RESTful/gRPC 呼叫(我們正在使用 gRPC 逐步替換內網 RESTful);3)底層是 Tool 層,包括了一系列我們維護的中介軟體、工具服務或者是包了一層的雲原生服務。

以我現在的認識,在一箇中型規模的技術團隊(100人規模),我會採取這樣的架構技術組合:

1)以網路為邊界拆分內外部流量,外部使用 RESTful HTTP 呼叫,內部使用 gRPC 呼叫。

2)在業務合適的情況下,使用比如類似 Kong 這樣的技術作為閘道器,處理鑑權、灰度、分流等系列邏輯。

3)內部服務之間不限制選型(前提是做好人員梯度培養),但要劃分清晰的服務邊界,進行合適的分層。

4)區分不同層服務的級別,以定義好穩定性要求、創新餘地與網路拓撲。

協 作

團隊協作的核心,就是人跟人的交流。

因為業線相對較多,我們基本上是以 two pizza team 的粒度來拆分團隊的(two pizza team 的意思就是點外賣時,兩份披薩可以讓整個團隊吃飽)。

每個相對較小的團隊會負責數個獨立的服務,組內成員互為 backup、互相學習、共同成長。

最早我們的 git 開發流是基於 commit diff 的,換句話說只要你的改動是正確的,那基本就可以合併進主幹分支了。——不過我們很快嚐到了苦頭(這個很快≈三年)

一些老的程式碼因為當時的產品也沒有留下成建制的 PRD,而且我們公司做的是B端產品,邏輯有時又巨特麼合理的繞,導致後人在 blame history 時,經常需要去分析這究竟是 bug 還是 feature。

到目前,我們整個團隊(強行被)達成了一致,使用的是“一個 PR 只有一個 Commit 只做一件事情”的基於 rebase 的協作流,我們這樣產出了接近於線性的完美 git 歷史。

而另一方面,關於版本控制我們基於 git tag 使用了內部的小機器人來管理。因為我們不需要考慮舊版本的相容維護問題,所以大部分情況我們用的是日期化的格式 (v2020.07.01)。

基於 git tag 我們又跟 sentry-release/ticket-system 做了一系列的工具鏈,包括自動生成版本之間的 changelog,自動對每個版本的釋出內容進行歸類分析等。

整套使用 git rebase 開發,使用 git tag 釋出的協作機制讓我們獲益不少。而為了達到這樣的效果,我們在團隊內部達成了這樣的約定:

1)認知上,專案以 rebase 為開發基礎

沒有什麼“我不會用 git”的介面,不會可以學。個人當然可以喜歡 merge,那請在個人專案裡用,團隊專案大家統一規範。

2)行動上,就做到我們設想的那樣好

① 每個 PR 只包含一個 Commit,每個 Commit 只修改一類內容;

3)工具上,我們需要有個哥來解決協作工具完善的問題

① 我們優化了 Pipeline 的速度,跑完 97% 覆蓋率的單元測試+所有檢查只要 3 分鐘左右;

② 針對線性歷史,我們提供了一系列發版、合併、變動檢測的機器人助手功能。

除了基於 git 的整套開發流,我們還共同維護著一整個新手村任務(在以前的文章裡有講過),而且我們推行的 Buddy 制度會讓一個有經驗的同學完全手把手地帶新人(不過這個具體要看每個人用心的程度了)

瞭解的技術越多,我就越覺得技術世界的廣博與好玩。其實做技術就像玩遊戲一樣,本質上都是打怪升級穿裝備。

本文裡講的,也可能只是我在這世界的一隅,提筆能想起來的一些只言碎語。但假如我要把整篇文章都刪掉,只能留下一句話,那我會毫不猶豫地留下這句話:

不會可以學。

14
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 三步為你的Springboot整合Actuator監控功能