寫在前面

微服務這系列的文章暫停了三個月,不是沒興趣寫了,而是這幾個月都在 “還債” + “充電”,換了另一種形式在產出分享內容,同時 也花了些時間累積微服務的基礎建設研究及評估經驗。我想分享的都是實際的開發及導入經驗,如果沒實際做過是寫不出來的.. 因此才暫停 文章的寫作計畫一陣子。

這幾個月,工作的方向做了些轉變,不變的是我仍然在 .NET & 微服務架構開發與導入這領域努力。在這陣子,幾個單位 也不約而同的找我分享這些內容,因此就 microservices & windows containers 這領域,有這幾場現場的演講 & 互動:

  1. DevOps Taipei, 這一夜讓我們來聊聊 Microservices
    微服務的架構與觀念,以及導入時要留意的陷阱。導入微服務架構,最重要的是了解微服務能解決你現有的那些問題,最忌諱的就是一窩蜂的 改寫 + 擁抱新技術,這樣會得不償失。這個 session 則是我親身執行過的案例經驗分享。

  2. Study4.TW, Visual Studio Everywhere 台北場
    專注在 .NET 開發人員,為甚麼一定要關注 windows container 這項技術? 容器化的部署方式,已經是大家關注的焦點了。 也被認定是要落實 DevOps 的必要技術之一。不過身為 .NET 開發人員,總覺得 container 技術離 .NET 還很遙遠,因為 目前主流的容器化應用,都還是在 linux 平台上面的 docker. windows container 在去年年底推出,讓 .NET 開發人員也開始 有機會接觸容器化的部署技術了。這個 session 則是針對 .NET 開發人員的 windows container 介紹及 demo 。

  3. TibaMe - Windows Container容器技術, 線上課程
    這是我初次嘗試錄製一系列的線上課程,其中一段也進棚拍攝,算是很特別的體驗。這次的內容我挑了 windows container 入門, 想從容器化的技術基礎開始談起,說明為何容器技術對於微服務架構而言是很重要的推手。這課程內容包含 docker / microservice 的背景 介紹。課程要傳遞的概念與 VS Everywhere 的那場一樣,只是形式是以線上課程 (video) 為主。總時數約 6 hr, 因此在觀念,架構以及 實作 Labs 講得比較仔細,包含 Step by step 的操作影片教學。

  4. TibaMe - 線上小聚主題分享-Windows Container 入門與實作, 線上讀書會 (Free)
    第二次跟線上讀書會的紀相安合作,由 TibaMe 主辦的線上小聚分享活動。主題一樣是圍繞在 windows container,不過線上互動的形式, 重點就比較偏向實際的 labs 操作,與 QA 的進行。因為容器化的技術觀念並不難,一般都很容易了解。倒是 windows container 相對的 使用族群較少,很多操作上會踩到的地雷都需要一一克服,因此線上的互動可以實際 live 操作,所以我把較多的時間擺在這邊,也當作是 線上課程的延伸。希望藉著線上互動的方式,解決自己在看影片或是文章時,無法自己搞清楚的一些細節及觀念問題。

這四個場子的屬性都不大一樣,每場講的主題都圍繞在 .NET + Microservices + Windows Container, 只不過重點擺的地方都不同。 每場講完都覺得有點缺憾,好像都可以再補足些什麼… 我還是擅長用寫的啊,寫下來的東西總是比用講的精確。因此我打算用這篇文章, 把這四個場次的分享重點都串起來。沒機會參加前面幾場的朋友們,可以看這篇文章;有參加過的朋友也可以看看文章,補上了我沒機會在 現場提到的重點。

這篇文章我不打算帶到 code 或是太硬的技術,只打算帶到單純的觀點 & 經驗分享。不過別擔心,這陣子累積了一些實作的技巧,這篇 之後就會繼續看到一堆 code 的內容了 XD, 敬請期待!

前言: 微服務架構 系列文章導讀


Microservices, 一個很龐大的主題,我分成四大部分陸續寫下去.. 預計至少會有10篇文章以上吧~ 目前擬定的大綱如下,再前面標示 (計畫) ,就代表這篇的內容還沒生出來… 請大家耐心等待的意思:

  1. 微服務架構(概念說明)
  2. 實做微服務的必要技術: API & SDK Design
  3. 架構師觀點 - 轉移到微服務架構的經驗分享
    • Part #1 改變架構的動機; 2017/05/09
    • Part #2 實際改變的架構案例; 2017/05/20
    • Part #3 實際部署的考量: 微服務基礎建設; 2017/07/11
  4. 案例實作 - IP 查詢服務的開發與設計

– 本文開始

“微服務” 是什麼?

先寫在前面,微服務只是個架構的設計參考方向,他並不是個 “問題”,也不應該是個 “目標”,他只是解決某些問題的 “方法”。因此, 若你不清楚你面對的是什麼問題,也不清楚你期望的目標是什麼,就只因為聽到微服務很棒,就要毅然決然地把整套系統砍掉重練,改成 微服務架構,那是很不切實際的。錯誤的決定,很有可能拖垮整個團隊。因此說明清楚這些前因後果,就是我這個段落主要的目的。

我先簡單的對 “微服務” 下個定義吧。字面上來說,就是把單體式的系統 (monolitch), 拆解成多個獨立的小服務, 再靠 API 把彼此 串聯起來的架構,就是 “微服務” 了。

微服務會變熱門,基本 (理想的) 概念就是: 現今軟體的複雜度越來越高,越大型的軟體,維護的成本是呈指數上升的。一個規模是 N 倍的軟體,維護成本可能是 N^2 .. 若把它拆成 N 個獨立的服務,那麼維護的成本就變成線性的 N 倍了.. (想像一下 N^2 跟 N 的成長幅度差異)

不過別忘了,微服務本身就是個分散式系統,分散式系統有它先天的複雜度,包含網路通訊不可靠,要維持資料的一致性就是個考驗,加上 跨服務的 bug 追查難度更高等等,都是個挑戰,都會讓維護的成本上升。

貼一下我在 DevOps #4 這場分享的幾張投影片:

圖1: 微服務的定義

就上面講的特性,我把常見的幾個關鍵字抓出來了。如果你對這些微服務的設計 guideline 有興趣,那右邊那本書很推薦,值得一讀。

圖2: 小巧、專注、自主性

圖3: 模組的相依性

這兩張投影片,我想表達的是同一件事,就是你的商業邏輯,到底要按照什麼樣的法則來切割才洽當? 整個微服務架構導入的過程中,我認為 最困難的就是這點了。這才是關鍵啊! 在談論這點之前,先來看看有那些部分是技術上 “適合” 切割的?

若你檢視你的系統架構,發現有明顯的系統邊界 (如上圖動畫所示),有某些模組之間有很複雜的連結 (高內聚),某些模組之間的關聯則很鬆散 (低耦合), 那麼這些地方就是技術上適合切割微服務的點。問題是,只要有這種狀況,都是適合切割的地方嗎? 每一個地方都應該去切割它嗎?

如果你相關文章 (微服務) 看得越多,你可能越傾向能拆的就拆,因為書上講 “要切割到不能再切割為止” … 不過這是要代價的,切割過頭的話 你只會付出更多成本,卻沒有回收對應的效益。我的答案當然是否定的,我認為只要切割到夠用,剛剛好就好了。其餘只要保持你的 source code 是 健康的,隨時準備好將來需要時再多補幾刀就好。切割過頭,這是很容易犯的錯誤,別說是新手了,連我都踩進這個陷阱好幾次。很多時候,技術導向的 團隊,很容易把一切決策都導向到技術角度來判斷。我問這個問題很明顯是個陷阱,真正的狀況應該是: 你要評估切割之後,是否有解決你的問題? 康威定律提到一點,系統的架構會跟組織的架構有高度相關,你的組織怎麼分工的,也會連帶影響到你的系統架構設計。這裡就是一例,你應該回頭 檢視,切割之後是否你的系統架構,會更貼近你組織的運作方式? 如果答案是肯定的,這樣的切割才會對你的整體運作有幫助。

其實這不是新課題,也不是新觀念;只是因為微服務,這些議題被放大重新拿出來討論罷了。在 DevOps #4 這個場子,我提了一個觀點,也是我 過去廿年來一直有的想法。其實軟體工程講求的原則一直都沒變啊! 我永遠記得當年大學時代,我初次接觸 C++ 也初次接觸物件導向技術時,當時 我的啟蒙書籍就是那本知名的 “世紀末軟體革命”。裡面講到很重要的一點,物件導向就是在 “模擬世界、加以處理”。

這句話我到現在都還記得,因為這就是物件導向之所以能解決問題的核心概念啊! 不知各位有沒有想過,為何物件導向技術,是我們應付複雜與多變 的軟體需求的利器? 不是強在 OOP 的語法有多強,而是透過這樣的精神開發出來的軟體架構 (前提是你的分析是正確的) 能夠跟實際世界的事物 運作有良好的對應。

我們常在抱怨老闆或是客戶隨意更改規格,但是探究背後的原因,客戶或許真的認為改個按鈕的位置或是功能只是很小的一個改變而已 (而不是真的要 找工程師的麻煩)。在他們認知的商業流程內,這樣的改變是很合理的,但是為何到了軟體開發就完全不是那麼一回事? 因為軟體的運作流程跟商業 流程沒有對應! 只是剛好一樣的 input 會有一樣的 output 而已,中間的過程也許完全不一樣。這種情況下任何的需求異動都會毀掉這個專案…。

物件導向很巧妙地解決這個問題,他從根本的 “模擬世界,加以處理” 的角度來提高軟體的適應能力。只要模擬的夠確實,現實世界的需求異動 就不會是問題。只要有正確的對應,你的軟體就能很輕易地適應客戶對需求的更改,因為你的架構跟他思考的流程已經是一致的了。同樣的, 微服務也是一樣,只是你從 code 的角度來看世界,提升到從服務的角度看世界而已,服務之間的關係若能反映實際流程之間的關係,那你的 系統也就會有很好的適應能力。

OOP 講的是在單一軟體系統架構內,你程式碼組織的方式。把這個想法擴大到整個分散式系統,那麼原本 OOP 講的 object / class, 不就也對應的擴大到 microservices 了嗎? 如果你的服務與服務之間的架構,能跟組織裡的運作邏輯一一對應的話,那這架構就是適合你的架構。

我很常做的一件事,就是拿 UML use case diagram (雖然這是老技術了,不過你能用的透徹的話還是很好用的),拿來跟我的微服務架構圖來對照。 如果設計得當的話,兩張圖應該是能夠對應得起來的。因此,該怎麼切割? 應該先從這個觀點來看才對! 如果架構上適合,技術上也容易執行,那是 最完美的狀況了,沒什麼好考慮的了。

若是流程上適合切割的地方,技術上卻有不少困難,那代表背後的架構跟實際的組織流程並沒有很好的對應。這時不要急著做微服務的切割 (這就是 我一直說的本末倒置),而是先將系統重構到合理的狀況後,再來進行切割。

看到這邊,你有掉進這個問題的陷阱嗎?

使用微服務的理由 - 容許技術異質性

這張架構圖是我很喜歡舉的例子,因為我覺得她很有代表性,尤其是 stackoverflow.com 這網站在開發人員心中的價值更是無可取代。stackoverflow.com 在 2016/04 公布的這張架構圖,給我很大的啟發。為何這個全球開發人員眼中指標性的網站,願意採用這麼特別的架構? 用 .NET 開發網站的核心部分, 卻在部署時大量使用了 Linux / Open Source 的其他服務 (Redis, Elastic, HAProxy…) ?

這在我之前幾篇文章也聊過,他們絕對不會是用 “Linux免費” 這種小兒科的理由,而是有更好的理由說服他們的架構師做這樣的決定。我看了微服務架構 之後就瞬間了解了 (雖然 stackoverflow.com 還稱不上是 100% 的微服務,我把他稱作 microservice ready),當它們能克服異質性的系統維護及部署 的門檻時,它們就會願意以架構為主,在每個不同的服務都採用業界最佳的 solution, 而不是優先考量你用的是甚麼平台? 是 Java or .NET? 思考的高度 不同,高下立判。

微服務 + 容器技術,直接把這個特色發揮到淋漓盡致了。容器化的技術大幅降低部署的困難度。就使用上來看,容器跟虛擬機器有很多雷同的地方,但是 容器的執行效能逼近原生的效能,這是 VM 比不上的;同時容器的映象檔也比 VM 來的高效 & 容易產生 (容器映像檔可以直接用 dockerfile build, 但是 VM 映像檔通常得自行安裝後取出),大大的降低使用門檻,同時 registry 的出現也讓容器映像檔的交換與散布變得容易。這樣的 Eco System 都是 VM 遠遠不及的。

了解了這些,你就不難體會,身為十多年的 .NET developer 的我 (我從 C# 還在 beta 就開始用了,應該沒有資歷比我老的了吧 XD) 也很想用這樣的 部署方式啊! 因此我才會在一年多前,研究 .NET core + Linux, 之後才把重心轉移到 windows container 上。

Windows Container 的出現,更把 stackoverflow.com 使用的這種異質性架構的門檻降到最低了。這時平台是 windows or linux 已經不會再是左右你選擇的主要考量了,你大可挑選業界最佳的個別服務,來組裝你的 application。包括採用的基礎建設,挑選現成的服務,以及決定自行開發的服務及平台。 這是很大的突破,轉眼間 .NET / Java / PHP 不再是派別的對立,而是可以讓團隊自由選擇採納的開發平台。唯一的考量是: 這平台適合拿來開發這服務嗎? 還有我的團隊對這個平台是否有足夠的掌握能力?

在寫這篇文章的同時,DockerCon 2017 才剛落幕,會議上 Docker 就丟出個震撼彈: 收購 Unikernel 後端出的第一套解決方案 - LinuxKit, 同時 Microsoft 也不示弱,拋出了 windows container 將會透過 hyper-v container 的方式,支援 Linux container。換句話說,以後你可以直接在 windows 10 / 2016 上面,只要下個 docker run ... 指令,就可以在 pure windows 環境下執行 Linux container. Linux 的容器化 程度遠高於 Windows, 有什麼 Linux 軟體沒有對應的 docker 版? Microsoft + Docker 下的這步棋,遠遠比當年 Java 標榜的 “Write Once, Run Everywhere” 來的高明多了。以後的 application, 直接透過 hyper-v container 就可以跨越了底層作業系統的隔閤,用 接近原生的速度執行。

這邊的細節,我會在這系列文章講到微服務的 deployment 及 infrastructure 實再深入說明,這邊先點到為止就好。

重構: 找出服務邊界,將之切割為獨立的服務

我一項都是從 top -> down 的方向來思考的,這樣才不會被細微的技術考量牽著鼻子跑。當我看到微服務長遠帶來的效益時,接下來就不用考量 要不要做了,而是該怎樣跨出第一步?

很多有經驗的架構師都說,微服務不是 “設計” 出來的,而是調整修改,分割出來的。這講的一點都沒錯,別急著把你既有服務通通丟掉重寫 (這樣一定會失敗 XD) 而是好好地檢視你現有的系統,評估看看該如何進行重構,將他調整成適合的樣子?

在開始之前,先來看看,在軟體開發的領域內,有哪些方法可以 “reuse” 既有的服務? level 從低階到高階,分別是:

  • share codes (concepts)
  • share library (binaries, components)
  • component (service process)
  • service (service instance)

最低階的,就是 sample code 這種 level 了。我寫一段 sample 給你參考,你拿回去修修改改,放在你自己的 application 內使用,這就是最 低階的 code reuse. reuse 的部分直接緊密的混雜在你的 code 內,完全沒有隔離或是保護管理的機制。我們常常上 stackoverflow.com 查問題 就是這種模式。看到有高手的 solution, 我們就貼回來測看看,成功了就用了… 只是,往後的更新就要自己負責了。

再高階一點,就是 share library 了。這模式之下,開始有專門的人負責維護這個 library, 不定時的釋出更新版本。你不需要重新修改程式,只 需要更新 DLL 就可以 (除非有 breaking change, 必須調整你的 code 之外)。reuse 的部分通常會有 language 或是 runtime 的機制保護隔離, 例如不同 library 之間,宣告為 internal 的東西就不能跨越 library 使用。更新或部署,也是以 DLL 檔案為單位,你只要管理好版本及相依姓, 你是可以個別維護及更新 library 的。這模式下的維護成本就下降了不少,你只要記得不定期更新 DLL 即可。在一些套件管理工具的協助下 (如: NuGet),這件事相對得更容易,你可以輕易地取得最新版本的 library, 放在你的 application 內。

再往上一階,我把他稱作 component, 有用過當年的 COM+,或是 EJB 的朋友,可能比較能了解我想表達的。要共用的服務,已經被包裝成 component, 並且有專屬的 hosting 環境來管理你的 component. 隔離的層級更高了,通常至少都會是獨立的 process 來運行 component, 彼此支籤再透過 IPC (inter process communication) 或是 RPC 的機制溝通。只要他執行起來,就會有專屬的 process 來服務你。某種程度下,這模式已經 開始嘗試解決 SOA , container 等等新技術想解決的問題了,只是層次還差太遠,終究現在逐漸的沒落了。

最上一層,就是獨立的 service 了。跟 component 類似,他是完全獨立於你的 application 的,有可能只是不同的 process, 但是更常見的是 不同的 host, 透過網路的呼叫來溝通。這種層級之下,跨越機器的協作,靠的是約定好的 network protocol. 通常這條件下,不會限制你使用的 OS, 開發平台等等,只要大家講的是同一個 protocol 就夠了。這是最高的層級,不但能夠跨越機器,還能採用 scale out, 採用大量的服務 instance 來 提高整體效能。

為何要講這麼多? 因為這是我歸納軟體 reuse 既有的 code 的方法。微服務講求的是將 application 切割成多個獨立的服務,每個服務可以部署多個 instance. 如果你無法一步到位,你可以試著思考看看,能不能往上 (較低階) 一個層級,檢視看看是否有辦法在上一個層級完成切割? 例如你已經把 邏輯獨立成 library, 之後要將他在獨立成 componet or service, 難度就會降低了。

用這方式講不大好理解,我換 NGINX 的系列文章來說明。其中有一篇講到如何將單體式 application 重構成微服務架構的說明:

重構測略 #1, 別再繼續擴大你的單體式APP了:

這張圖很清楚的說明改善策略了,當你想在既有的單體式 APP 增加新功能時,切記別再繼續讓狀況惡化了。你可以有計畫地進行重構。 新的功能,就直接開發成獨立的服務,同時在前端加上 request router (各種型態的 routing 都可以考慮! 例如 reverse proxy, 或是 API gateway 等等),把要存取的新功能部份 request,重新導向到新的 service 上面。存取既有的服務的 request 則繼續導向到既有的 service 上。

若是新的服務,還有部分功能要依賴既有的服務,那就額外寫一段轉接的 code (就是圖上的 Glue Code), 再轉接回原本的服務吧! 這邊用 Glue Code 來代表,而不用 API 來呈現,主要是你打算怎麼看待這段轉接的 code? Glue Code 通常是不公開的介面,的要求是只要能正常 運作即可,必要時甚至是透過 DB 交換資料等等看起來不大上道的方法都可以考慮。因為它是讓你度過過渡期的臨時做法,等將來整體都完全 微服務化之後,這些是要拋棄掉的部分。若改採用 API 的話,則背後的要求就很多了,包含 interface 定義是否洽當? 是否好維護? API 是否 做到向前版本相容等等議題都要考慮。

這樣的執行策略,只能讓單體式架構的部分不會再繼續擴大而已。若要將既有的 application 也微服務化,得繼續看下去

重構測略 #2, 切割為前後端服務

這個策略比上一個策略來的主動多了,主動將既有的單體式架構一分為二,分為前端及後端兩個服務。這個做法雖然看起來粗魯了些,不過老實說 還蠻有效的。幾年前我就用這個手法,成功的把兩層式架構 (WEB + DB) 切割為三層式架構 (WEB + AP + DB)。同樣的手法在微服務的時代也 能派上用場。

首先一樣,你必須先想好你打算在哪邊劃下那一刀? 在哪裡切下這一刀,永遠是最關鍵的問題。因為切下去,API 就要定義出來,一旦 API 公開了, 你就必須對她負責,要長期的維持 API 的相容性才行。反倒是 API 後面的實作,你還有很多機會可以持續改進,重寫。

決定好在哪邊切開後,就要對應的定義好 API 了。原本的單體式架構 APP 只要複製兩份 CODE,前端的那部分要存取後端的部分,就要改用 界接的部分,呼叫後端的 API 來達到原本的功能。而分裂後的後端部分,則要實作 API,讓他能順利地讓前端呼叫使用。

其他重複的 code, 就留著再慢慢清理重構就行了。用這個方法,你可以很有系統,很有步驟地在最短時間完成切割服務的步驟。只要你構思好 切割的方式之後,不斷重複這個步驟,總有一天你就能完成微服務化的目標。

重構測略 #3, 將內部的元件抽取出來成微服務

來看個複雜一點的情境,上圖左側是軟體的現況,有 X Y Z 三個模組。X 會呼叫 Z 的服務,而 Z 又會呼叫 Y 的服務。今天當你判定想把 Z 的模組切割出來成為獨立的服務,那該怎麼做?

這張圖說明了執行的方式,比照重構策略 #2 的概念,把整個架構複製一份出來。左方的 X 要呼叫 Z 的部分,改用 REST CLIENT 轉接到 右方的 REST API (接到 Z 模組)。而 Z 模組要呼叫 X 模組的部分也比照辦理。兩邊都串接完成之後,除了多了一些冗餘的 CODE 存在左右 兩方之外 (這你可以後續持續優化重構將之移除),基本上你已經完成了架構上的切割。

不過這種方法的前提是,這樣的切割是真的 “合適” !! 尤其是重構的策略 #3 更需要注意。不知大家對這數字有沒有概念? 假設 local method invoke 每秒可以執行 100 萬次的話,換成 local host REST API call, 大約只會剩下 10000 次不到的效能。除了效能折損之外,額外的延遲、 還有網路不可靠等因素造成的 Exception 更是大問題。

因此,這樣的重構只是讓你架構調整的第一步能夠較容易地跨出去,你還是必須持續優化這樣的架構,將他真正調整成適合微服務的架構才行。

微服務架構的進入門檻

我們再把主題拉回架構的設計與評估。微服務的好處這麼多,那就直接用就好了,還有什麼好考慮的? 其實他還是有缺點的,就是進入門檻很高。 你至少要先確定你的團隊現況有足夠資格,已經取得門票後,再開始進行微服務的架構改版。

在 3/11 visual studio everywhere 台北場,有幸現場聽到徐磊前輩的架構分享,他提到微服務對開發人員是個搞死自己的坑,能不要用就 不要用。一個 project 能搞定的事情,就別把它切成 10 個 projects。一個資料庫能處理好的問題,就別把它切割成 10 個。本地端呼叫 能解決的問題,就別用遠程呼叫…. 這些原因都一樣啊,過度的架構設計,在帶來甜頭之前,你會先嘗到苦頭。甚至很有可能永遠看不到盡頭, 只有在微服務化是真的必要時,你才值得這麼做。

任何事情都會有黑暗面,看過 StarWars 系列的就知道這張圖是啥意思 XD。

微服務因為它先天就是分散式系統,因此有些必須要跨過的門檻,請你先評估團隊具備這樣的能力之後,你才有資格繼續往下評估下去,看看你們 是否適合用微服務?

我這邊列三點關鍵因素:

  1. (分散式) 系統架構: 分散式就代表複雜度高。包括問題排除 (你以前只要除錯一個 app, 現在你得同時對多個 app 進行除錯),還有 log 追蹤查詢等等。遠端的呼叫 可靠度跟效能也不如本地端的呼叫,正確率也會大大的降低 (本地端呼叫過程中幾乎不會出問題)

  2. 開發流程: 由於你要開發的系統變多了 (雖然每個系統都變小變簡單了),開發的管理,開發後的部署,服務跟服務之間的套件,API 介面的版本及相容性要求, 自動化測試、CI / CD 等等 DevOps 探討的課題,在這邊都是必要的。沒做到這些,你在微服務的發展過程一定會很痛苦,只要碰到幾次問題,你 浪費在追查問題的過程中浪費的時間,就足以抵銷掉微服務帶來的好處。
    若不顧好 API 的相容性問題,若你異動單一服務,相關的服務都得一併更新升級的話,那這樣你還算享受到微服務的好處嗎? 回想一下: 微服務 可以個別升級系統中的個別服務,不會影響到整體系統的運作。這是必須建立在 API 隨時維持新舊版的相容性為前提。

  3. 營運流程: 開發之後就是維運。微服務化,就是用很多規模較小的系統,用大量的 instance 來取代單一的 app, 因此你需要部署的 app 數量會暴增。 此時若沒有改善 app 的部署方式,同時改善執行環境的基礎建設,結果也是一樣,慘不忍睹。

這三項都是跨入微服務架構一定會碰到的門檻啊,請先確定你的團隊的軟體工程的水準能輕鬆應付他們!

– 想寫的東西太多了,先到這裡告一段落,敬請期待續篇 :D






Search

    Post Directory

    Facebook Pages