工作的第二和第三年(201807~202005)

原先計劃按畢業日期算起,每個週年寫一篇生活篇和工作篇,但後來工作篇由於各種原因沒有按時寫完發佈,只有生活篇一直保持着。

工作部分的第一篇是 2018 年 7 月寫的《入職一年啦》。

本篇第二篇,是在 2020 年 7 月寫的,內容是 2018 年下半年到 2020 上半年的部分。由於當時沒寫完整,就只發在 GitHub 博客上。現在只是同步過來。

2020 年 7 月到 2022 年 7 月的部分,過一段時間會補上。這段時間都是使用 Golang 開發,而且涉及到更多的數據庫。不再是 PHP + MySQL 了。

2019 年 7 月本來想寫 201807~201907 的部分,但由於當時項目比較趕,就一拖再拖。另外由於想寫得詳細一些,需要蒐集很多信息,導致一直沒進展。以下是 201807~202005 這段時間的內容,主要使用 PHP + MySQL。包含了接口設計、日誌收集、分佈式存儲、高可用等內容。

18 年後半年

這段時間仍然是以維護和開發舊系統爲主。

比較可以一提的有:

  • 寫了套簡易的異步任務管理器(10月)
    寫這個是因爲我們系統會發送一個異步任務給外部系統,他們在執行完後,並不會回調我們系統的 API 通知我們任務狀態。所以我們每次都是在流程的節點裏面輪詢調用接口去獲取結果。我想要把輪詢檢測的部分獨立出來,轉成任務完成後再變更流程實例的狀態。
    剛好也有個新需求是外部系統會用回調通知任務狀態,所以就寫了個支持。
    我是用 crontab 完成定時查詢狀態,畢竟業務上可容忍一分鐘的延遲,所以不需要太複雜的組件。

  • 引入 IoC 容器(11月)
    原先創建一個類是直接 new 或者使用 get/set 注入,而在 11 月的時候我把 Laravel 的 Container 庫引入進來。使用服務容器獲取服務對象。主要是便於單元測試。
    在瞭解了 IoC 容器後,我在部門做了一次分享。以發展的角度,從最基礎的代碼過渡到使用 IoC 容器的九個階段,更深刻地理解 IoC 容器存在的原因以及使用場景。

  • 優化了個冗長且經常改動的 if-else(11月)
    這是一個根據多個條件判斷選取哪些數據的 if-else,包含了很多個分支,並且直接嵌在某個業務代碼裏面,其他地方無法使用。
    我把它抽取成一個函數。此外,利用類似於表驅動法的優化方式,讓它到 Json 文件中讀取所需要的數據。然後用 for 循環去依次匹配,匹配到的時候返回其對應的數據。
    原先需求方提需求的時候,會發一個 Excel 過來,然後把變更點用特殊顏色標記。開發人員根據這些變化點修改代碼,在發佈到線上後才能生效。經過我的修改,需求方只需要直接到系統界面上上傳 Excel 就能應用修改。
    其實如果有前端的小夥伴,我覺得做成界面直接配置更好。

18 年總體表現還不錯,年會上領了個公司級別的優秀員工獎。現在看來,這玩意兒最大的用處就是獎金和放老家讓我爸開心一陣了。就算面試的時候想通過這個來表示比其他人努力,也沒什麼用。

公司這會兒被阿里和騰訊折騰得很難受,所以就算拿了優秀員工獎,年終獎金也沒多少。

19 年新系統

這一年挺關鍵的。以前就有離職的想法了,但因爲有計劃重構系統,所以我就留下來爭取主導重構的機會。年初終於開始推動了。

從現在往回看,確實學到了很多東西。不過也因爲參與人數少,沒能多學一些。最初的時候只有一個同事每週花兩天左右的時間做前端,其餘的都由我負責。

說是重構,其實是重新做。新的系統大致是下面這個樣子:

接口設計

爲了設計接口,專門去了解了一下 RESTful API。主要從以下幾個方面:

  1. 指南。比如:
  2. 博客。比如:
  3. API 文檔。比如:
  4. API:
  5. RFC 標準:

瞭解完之後,給我的感覺是 RESTful 的內容大多數都是按照 RFC 標準來的,在此基礎上強調兩點:

  1. URL 儘量都用名詞
  2. 超媒體(Hypermedia)

有一次我在公司內部的 Wiki 上試圖搜索一些 RESTful 內容,結果發現有一篇文章標題寫着 RESTful,內容確是讓人不要使用 DELETE/PATCH/PUT 這些方法。並且自己設計了一套 code 和 data 的格式,所有返回的 http code 都是 200。這不就是以前那種古老的設計方法嘛。如果說 Wiki 上那篇文章和 RESTful 有一丁點關係的地方,大概就只有 URL 儘量都用名詞這一條了吧。這樣看連 RESTful like 的層次都沒達到。

超媒體這一條,我本來打算實現,但後面想想也沒多大必要。內部系統給自己用的,做到 RESTful like 就差不多了。

而對於 RESTful like 來說,URL 儘量都用名詞這一條我感覺是最考驗接口設計者的。不僅要對業務非常熟悉,而且要能夠把概念抽象出來。畢竟有時候一個操作涉及的東西特別多,普通的命名會導致 URL 地址太長。

還有一些要統一的,比如:

RFC 標準是把數據放 Body,把其他的往請求頭放。畢竟這些信息也佔不了多少空間。

花了挺多精力在 Restful 上面。不過我發現研究這些的用處並不大,畢竟在國內也不會有多少項目會去參考 RFC 標準來設計。大多數項目都是參考國內的通用設計,在此之上自己搞出一套標準出來。就算如此,也沒有一套靠譜的標準,所以還是得根據項目所處環境調整。除非換到一個一開始就參考 Restful 的要求來實現接口的公司或團隊。

以前刷微博時,看到一條吐槽無論成功與否,都返回 200 狀態碼的 API:

上圖那個鏈接點進去是:

不過難道就真的只因爲這個原因嗎?我覺得更多的人是不知道有 RFC HTTP 標準的存在。如果不是 RESTful 流行起來,估計會更少有人知道這些標準的存在。而知道標準存在的那部分有決定權的人,願意去了解,願意去應用到新項目的,就更少了。

接口文檔

有了接口,總也得寫接口文檔吧?怎麼寫接口文檔也是個問題。

最開始是寫到內部的 WiKi 上,但是寫起來不太方便。每個 API 都得寫一個頁面。

嘗試了 Swagger,語法比較多,一開始喫力。感覺如果真用 Swagger,可能會被打,就放棄了。

最後還是寫到 WiKi 上,不方便就不方便吧,也沒啥。

我們系統是前後端分離,前後端並行開發的時候,前端需要獲取接口的 Mock 數據。

我本來想用公司內部自己搞的一套 Mock 系統,但是發現太難用了。

後來自己搭了一套 Easy Mock,還可以從 Swagger 導入。但是 Easy Mock 有個毛病,就是它假設你是使用國內自己搞的那套接口標準,無論正確錯誤都返回 200 那種,上面吐槽過了。你要按照 RESTful 來做接口,它 Mock 起來返回的狀態碼或者數據就不會按你定義的來。無奈之下我去找到相關邏輯的代碼,把它們改成我想要的樣子。但這樣也不是個辦法。

最後乾脆不要了。前端自己 Mock 去吧。

GraphQL

在研究 RESTful API 怎麼設計而去 GitHub 參考它的文檔時,發現了 GitHub 的新版 API 使用的是 GraphQL。

這讓我很感興趣,爲啥 GitHub 要從 RESTful 轉向 GraphQL?於是就各種找資料瞭解 GraphQL 是啥?解決了啥問題?

這一瞭解下去,突然興奮。這不就正是能解決我們當前查詢上的問題嗎?

這個問題出在公司的 CMDB 和集羣架構平臺的接口太弱,以至於要查一些數據的時候,得調用一大堆 API。例如多對多關聯的情況下,需要調用三個接口,就像是執行三條 SQL 語句,特別噁心。之前我們這邊的應對辦法是,定期獲取所有接口的全量數據,放到 MySQL 數據庫,然後從數據庫裏面用 join 查。

但是這對開發人員來說很不友好。當然有一部分原因是舊項目沒有 ORM。如果有的話,就不會這麼難了。由於沒有 ORM,導致每個查詢都需要寫純 SQL。而且之前的同事又一直沒有複用的概念,基本上每次要查詢都重新寫一個 SQL,最多是把以前寫的 SQL 語句複製過來,然後稍微改改。於是一大堆的表,要經過很長一段時間才知道各表之間的關聯關係。

GraphQL 可以先定義好各表之間的關係,然後使用 HTTP 把想要的數據及其關聯的數據一起拿到。由於它自帶 API 文檔,可以在一個名爲 playground 界面中查看所有 API 及從某個字段找到關聯的其他表的字段。

我把 GraphQL 引入重構項目,後來也在分享會上給部門的小夥伴介紹。

使用過後的感受可以說是好壞都有。好的一點是查詢比較靈活,而且不會獲取多餘的字段。壞處是有時候獲取的字段的數據會有重複,GraphQL 的基礎庫沒有提供這種支持。另外 GraphQL 由於每個數據的每個字段都要執行一次 resolve() 函數,量一大就會消耗很多資源。也有一些其他的優缺點。但我發現好多介紹 GraphQL 的文章,都不喜歡談它的缺點。

不過雖然引入了 GraphQL,我還是用它去查數據庫裏面的數據,而不是用來封裝對 CMDB 接口的調用。但是使用 GraphQL 就爲以後將查詢切換爲接口提供了方便。這是按字段 resolve() 所帶來的靈活性。

流程引擎

之前寫過《流程引擎爲什麼選 Camunda》和《Camunda 流程引擎的一種 Adapter 層實現》。最開始設計系統的時候,大概花了一個月的時間把 Camunda 瞭解了一遍,然後根據 Camunda 已有接口組合出我們業務需要的樣子。查了很多文檔(特別是官方文檔)和做了 N 多實驗,有時候爲了解決一個問題,幹到凌晨兩三點才下班。也算是體會了一把傳說中的加班到十二點後。

Laravel

當時選它的原因估計是因爲旁邊有個大佬(目前在微信支付)用了很長時間的 Laravel,有一次在我們的每週分享上專門吹了一把 Laravel。另外也因爲稍微嘗試過 Yii2,感覺不太喜歡這種很固定 MVC 的方式。

很早就聽說 Laravel 這框架很重,學起來很難。不過我當時在用的時候,沒感覺到難在哪。雖然有那麼一次出現問題,調試的時候在框架代碼跳來跳去,不過這也沒什麼,而且只有一兩次。之後越用越覺得好用,越覺得 Laravel 牛逼。

比較直接的是兩點:

  1. IoC 容器
  2. 服務註冊

作爲一個菜雞,當時也沒啥經驗。此處羨慕一下學 JAVA 的同學,天生就接觸了這倆概念。

在接觸 Laravel 之前,由於不知道這兩者,讓我走了不少彎路。

首先是 IoC 容器。用容器來獲取對象非常靈活,而且對單元測試有很大的幫助。

以前我寫過一篇單元測試的博客《單元測試學習筆記》,主要是參考《單元測試的藝術》。我當時的說法是“如果要單元測試,首先要保證代碼是可測試的”。而所謂的“可測試的”就表示需要使用依賴注入。當時其實也沒理解清楚,依賴注入也只瞭解了常見的幾種。後面找個時間重新寫一篇關於單元測試的博客。

我上一篇《入職一年啦》提到過重構一個基於 Zend Framework 框架寫的項目。當時我還不知道有容器,所以大量使用 getter&setter 注入。

後來接觸了 Laravel,然後專門有一次講了 IoC 容器的由來。不過這由來是我自己推理來的。當時用的代碼示例在:

https://github.com/schaepher/DependencyInjection

我給定了九個階段,然後發現自己在見到 Laravel 之前才處於第三階段。靠自己的話,可能還要花上不少時間才能提到更高的階段吧。再次羨慕 JAVA 同學,直接跳過摸索階段,節約了好多時間。

另一個是服務註冊。在接觸它之前,我曾經爲寫 PHP 基礎庫的時候配置文件如何加載的問題而煩惱過。

這個基礎庫是爲了將 PHP 調用轉化爲基於命令行的 RPC 調用。原本負責這個功能的代碼全都放在一個文件裏面,配置也固定在裏面。我把它抽出來成爲一個基礎庫。但是配置怎麼加載就成了一個問題。

爲什麼呢?因爲服務提供者有很多個組件,我得提供一個 Factory 類來創建不同服務提供者的對象,但是不想每次在創建的時候都在構造方法上設置配置文件,這會影響到調用方的體驗。

最開始的做法是業務代碼裏面寫一個類繼承 Factory 類,然後在業務層實現 loadConfig() 方法,用於加載配置。後來接觸到 Laravel 的服務註冊,就學它的做法。在庫裏面寫一個 Config 類,提供 loadConfig(),把配置加載到類的靜態屬性上。這樣項目在加載的時候,入口處就可以把配置文件加載進去。後來甚至把它做成一個通用的服務加載器,還是參考 Laravel,定義一個包含 register()boot() 方法的接口,讓庫去實現。

現在回想起來,當時自己想出用服務註冊的方式也不會很難。思路到底卡在哪裏了呢?我再琢磨琢磨。

總之就是 Laravel 用得越多,會覺得代碼就該這麼寫,框架就該有這些功能。也難怪小夥伴經常說 Laravel 是最優雅的框架。

後續寫一篇博客,講講 Laravel。雖然我現在轉 Golang 了,但感覺 Laravel 還是挺有研究價值的。如果還有時間,那就再去跟 JAVA 的那套對比看看。

日誌 ELK 套餐

在真正用到項目中之前,也看過幾篇寫 ELK 使用的博客,但一直覺得很難。直到實踐了才覺得如果只是簡單地使用,實在是太簡單了。如果要說那些博客讓我看着怕怕的,感覺也挺過分。除非我能寫出一篇令人滿意的博客。後續試試看!

在實踐 ELK 的過程中,還了解到了輕量級的日誌收集工具 Filebeat。

關於日誌,也是有不少可以討論的。例如日誌的輪轉,日誌文件的命名規則,日誌內容的格式。

日誌的量大,不可能只寫在一個文件裏面。用 Laravel 比較方便,可以配置每天寫一個文件。日誌的文件名會自動加上時間。我覺得這種方式挺好的。

因爲有另外一種更常見但是也比較麻煩的日誌文件命名方式。就是最近的一個文件名爲 myapp.log,然後輪轉時加上時間或者加上序號。舉幾個我在各種項目中見到的例子(以當前日期爲例):

類型 示例 輪轉時
日期按月後置 myapp-202007.log myapp-202007.log1 或者 myapp-202007-1.log
日期按月前置 202007-myapp.log 202007-myapp.log1 或者 202007-myapp-1.log
不要日期 myapp.log myapp-20200713.log 或者 myapp.log.20200713

還有一種比較特別的方式,就是創建的文件帶日期,但程序實際寫入的文件名不帶日期。然後創建一個不帶日期的軟鏈接,指向帶日期的文件。感受一下:

[hello@localhost test]$ ll
total 0
-rw-rw-r--. 1 hello hello  0 Jul 12 16:57 myapp-20200713.log
lrwxrwxrwx. 1 hello hello 18 Jul 12 16:57 myapp.log -> myapp-20200713.log

文件名會引起什麼問題呢?這要看日誌收集軟件是怎麼寫的了。日誌收集軟件會記錄當前追蹤到哪個文件的哪個位置。

日誌輪轉的時候,會將當前文件名重命名,然後創建一個新的空文件。此時日誌收集軟件有兩種方式:

  1. 在 Linux 下,可以記錄文件的 inode 及已讀取的日誌行的最新位置。(Filebeat)
  2. 記錄文件的名稱及已讀取的日誌行的最新位置。(Rsyslog 待驗證)

如果是第一種,那就沒問題,畢竟重命名文件的時候不會變更 inode 號。但是第二種就麻煩了,會導致軟件再次收集新文件名對應文件的內容,結果是 Elastic Search 裏面有重複的日誌記錄。

除了文件名,還有個日誌內容格式。這個就更多了,千奇百怪。

比如最基本的日誌產生時間。至少有以下這麼幾種:

  • 28/Jun/2020:06:41:26 +0000
  • 2020-07-13 01:10:00
  • 2020-07-13T01:10:00
  • 2020-07-13T01:10:00.52+08:00
  • 2020-07-13T01:10:00+08:00
  • 20200713011000
  • UNIX 時間戳(不帶毫秒)
  • UNIX 時間戳(帶毫秒)

有的在日期兩旁用 [] 包起來,這裏就不列出來了。

我個人喜歡 2020-07-13T01:10:00.52+08:00 這種,也就是 RFC 3339 裏面的。兼顧了可讀性和通用性。

至於日期後面的,最常見的是用 TAB 分割的內容。這些內容沒有字段名,所以得到文檔裏面看它們分別代表什麼。也有一些加字段名的,也是有很多種方式。

比如 key0:value0||||key1:value1||||....,和 key0:value0 key1:value1

我在這個項目裏面使用的是把整條日誌放 Json 裏面,因爲 Json 解析完直接丟到 ES,可以方便地做各個字段的查詢。不過我後來發現這樣也有問題。

日誌變大是肯定的,因爲每條日誌都會多出字段名。另一個問題是 ES 文檔每個字段在第一次出現的時候就固定其值的類型,如果後面新加入的日誌中,某個個字段值的類型與原先不一樣,就會報錯。這樣就得在寫日誌的代碼裏面多做一些工作。

從使用上來說,查詢具體字段其實並沒有用到。而且 ES 本身是支持全文索引的,所以不必要求日誌用 Json。以後就直接丟一個文本給 ES,使用全文搜索就能滿足大部分的日誌查詢需求了。

另外關於 Filebeat ,有件有意思的事情。我在後來嘗試把 ELK 也引入到舊系統的時候,發現舊系統沒法用 Filebeat。因爲它要求 CentOS 版本要 6 以上。而我們舊系統是跑在 CentOS 5.8 的。我是想升級系統的,但是升級的風險很大,需要很多時間驗證。後來我是用 rsyslog 讓舊系統的日誌發送到 RabbitMQ,才成功將舊系統的日誌接入到 ELK。

由於舊系統的日誌都保存在本機,系統部署在兩臺設備上,以前查日誌還得跑不同系統上查詢和聚合,因爲同一個流程實例的每個節點可能由不同的服務器執行。查詢的方式又是基於 grep,查詢歷史日誌的時候巨慢,每次等得很痛苦。用上 ELK 套餐後,日誌集中存儲,有了索引查詢起來飛快,而且日誌按時間順序排序了。

但是也有一個問題,舊系統記日誌的時候,時間只記錄到秒。而一秒內可以執行不止一個流程實例節點,就導致了在 Kibana 上查詢日誌按時間排序的時候,順序會亂掉。這就是爲啥日誌時間最好帶上毫秒。

分佈式文件存儲

舊系統有好幾臺機器提供服務,用戶上傳的文件存儲在其訪問的機器上,這就導致要獲取的時候比較麻煩。另外還有一個是登錄遠程機器執行命令的時候,如果不用我寫的那個工具,而是用以前的寫法,會導致保存結果的文件和消費這個文件的服務器不是同一臺。

下面說說幾種方案:

  • 按需跨主機取
    舊系統的做法是存儲文件的時候,把文件名和所在機器信息存儲到數據庫裏面。等需要獲取的時候,查詢數據庫得到目標,然後從目標服務器傳送過來。

  • 通過 rsync 將文件同步到各臺機器。
    但是至少有三個問題:

    • 一個文件要傳輸 N - 1 次,給那臺機器造成比較大的負擔。當然也可以嘗試專門一臺機器作爲同步中心。
    • 每臺機器上都要存儲一份拷貝,浪費硬盤空間。
    • 每次擴展一臺機器都要全部拷貝一遍。
  • NFS

    • 存在單點故障問題
    • 需要自己做同步
    • 連接管理麻煩
  • HDFS

    • 不太適合用於存儲小文件
  • MinIO

    • 雖然是對象存儲,但存文件也沒問題,而且輕量。
    • 自動同步。
    • 與 AWS S3 使用同一個協議 (S3) 協議。不過對於內部項目的好處不明顯。主要是可以前期用 AWS,後期不用改代碼就能切回自己搭建的 MinIO。

其他的沒有去了解。MinIO 足夠簡單,而且夠用。

Docker && Docker Swarm

18 年用了一段時間的 Docker,爲老項目創建了 Docker 鏡像,要搭建測試環境方便很多,但是沒有用到生產環境上。

新項目則是一開始就使用 Docker 部署,生產環境上也是用 Docker。

爲啥選 Docker Swarm 而不是 k8s 呢?因爲 Docker Swarm 使用起來簡單,k8s 比 Docker Swarm 維護起來麻煩多。畢竟很多人卡在了安裝 k8s 這一步。總之對於小項目, Docker Swarm 就夠用了。

我是挺想用 k8s 的,但我們這項目沒有專業運維,我要是走了,留下一堆運維複雜的系統,怕是會被人天天罵。

負責一個完整的系統的時候,不得不考慮各種因素。感覺有點像打仗,有句話叫“外行談武器,內行談後勤”。一大堆牛逼的技術堆在一起,會提升系統的複雜度。所以通常都得做出妥協,選擇夠用的且對系統複雜度增加較少的方案。

高可用

雖然集羣內部有負載均衡,但如果只把域名解析到一臺機器,到時候這臺機器掛了就得修改域名解析。

關於高可用,以前瞭解的東西少,所以就只從域名解析入手。域名解析有兩種方式:

  1. 只解析到一臺機器,當機器掛掉的時候,修改域名解析,解析到另一臺正常的機器。
  2. 解析到所有可用的機器,當一臺機器掛掉的時候,把這臺機器對應的解析去除掉。

這倆方案都有一個問題,就是域名解析是會有緩存的。用戶使用域名訪問後,其解析結果會緩存在系統裏面,過一段時間纔會失效。所以這會導致用戶在域名解析過期之前無法訪問。

後來接觸並對 CDN 瞭解得比較多之後,知道了有 Virtual IP + keepalive 這種組合。

這種方式的特點是兩臺提供服務的機器上除了有屬於自己的 IP 外,還有第三個 IP。這第三個 IP 稱爲虛 IP,(Virtual IP,簡稱 VIP)。

VIP 會綁定到兩臺服務器的其中一臺,兩臺服務器都會裝上 keepalive,然後將域名解析到 VIP 上面。當 VIP 所在機器掛掉後,正常機器的 keepalive 會檢測到對方的機器掛掉了,並讓他自己這臺機器掛載這個 VIP,接着通過 VRRP 協議讓上一層路由器知道 IP 和 MAC 地址的綁定關係發生了變更。這樣當新的請求過來後,路由器會將請求轉發到正常機器。

keepalive 通常不設置開機啓動。這是因爲當故障的機器重啓後,機器上的應用可能是數據庫,需要先同步數據。或者一些應用啓動需要初始化,也需要等待。等待機器的狀態修復爲能正常提供服務時才啓動 keepalive。

這個方案只需要配置一次,而且切換的速度極快。

不過切換速度快也會出現一些問題。例如檢測故障的條件是一定時間沒有受到心跳包,可能是因爲網絡擁塞或者機器的 CPU 高,會被誤認爲出現故障而執行切換。如果是雙主單活數據庫,在極端條件下會出現自增 ID 衝突的問題。如果想要解決這個問題,就得使用分佈式 ID 生成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章