<dfn id="hx5t3"><strike id="hx5t3"><em id="hx5t3"></em></strike></dfn>

    <thead id="hx5t3"></thead><nobr id="hx5t3"><font id="hx5t3"><rp id="hx5t3"></rp></font></nobr>

    <listing id="hx5t3"></listing>

    <var id="hx5t3"></var>
    <big id="hx5t3"></big>

      
      

      <output id="hx5t3"><ruby id="hx5t3"></ruby></output>
      <menuitem id="hx5t3"><dfn id="hx5t3"></dfn></menuitem>

      <big id="hx5t3"></big>

        王下邀月熊_Chevalier

        王下邀月熊_Chevalier 查看完整檔案

        上海編輯南京大學  |  計算機科學與技術 編輯公眾號  |  某熊的技術之路 編輯 github.com/wxyyxc1992 編輯
        編輯

        愛代碼 愛生活 希望成為全棧整合師
        微信公眾號:某熊的技術之路

        個人動態

        王下邀月熊_Chevalier 發布了文章 · 2月21日

        2020-我的技術之路:創業公司中的研發效能與技術賦能

        2020-我的技術之路:創業公司中的研發效能與技術賦能

        2020 年,諸多不易,大家都是披荊斬棘砥礪前行;在這一年我在技術、產品、行業認知上也是起起伏伏,在挫折、摔打中不斷地深化自己對行業的認知,融入制造團隊,打磨產品,構建更順滑的體驗與交付能力。從技術與產品的視角看,2020 我的核心關注點如下:

        • 研發效能,以盡可能小的技術團隊保障全線產品的按時上線、交付。我們的產品涵蓋了典型的 工業互聯網/MES/CRM/電商系統,跨越了 Web/移動端/小程序/桌面端等多個觸達點,服務于海內外客戶(需要維護跨地域的公/私有云及邊緣節點)。
        • 技術賦能,挖掘并驅動業務發展,單點突破與全線貫通齊頭并進,以正合,以奇勝。我們需要某些產品點打動客戶,但是如果不能給客戶提供完整的解決方案,是無法得到最好的認可。

        做時間的朋友:八大體系超千篇數百萬字技術筆記

        天地逆旅,時光飛逝,歲月如梭,年近而立也是愈發感覺有急迫感;每次回顧過去十年的職業生涯,想起自己曾經學過、做過很多,但是也忘了很多,不由地內心惶惑。此時唯有自己做的這數百萬字筆記體現了技術一途上留下的痕跡:在線閱讀:ng-tech.icu/books,書籍托管于 Github:https://github.com/wx-chevalier

        筆記匯總

        今年我會針對每個系列編寫專門的導讀文章,希望能與更多的人分享我看到的、學到的、記下的。

        既能組裝也能造輪子:模板、庫、項目的沉淀

        經歷了不同的大廠與創業團隊,對于技術人員而言需要具備極強的機動性、靈活性;在小型的創業團隊中不能墨守成規,照搬大廠的規范、流程、制度以及技術架構。另一個方面,也不能因為是小團隊就忽略了對于架構、編程規范(如 Lint)、重構(如 Code Review)等的堅持,否則隨著業務發展迅速增加的技術負債終會顯示出它的破壞力。就如筆者在《SoftwareArchitecture-Series》中關于所謂復雜性的討論,軟件架構的核心價值,即是控制系統的復雜性,將核心業務邏輯和技術細節的分離與解耦;互聯網軟件系統架構的設計不是一蹴而就,而需要漸進、持續、多次設計的。

        作為創業團隊的技術人員,核心矛盾是提高生產力,提高團隊的研發效能。我們既要能發現現有的輪子,去快速組裝他們,去支撐業務需求;也要能造輪子,去完成團隊自身的工具化與工程化。同時也不能盲目追新,很多令人激動的新技術、新特性,但是也要考慮到新技術本身的不確定性、團隊成員的學習成本。這里以 Web 開發做簡單示例,在 wx-fe 主題下大概有十來個項目,其典型包括:

        • m-fe-* 系列: 微前端工程化系統項目,包含了前端開發基礎腳手架、React/Vue/Node/Electron/Taro 以及各種微前端模板。
        • micro-components 系列:包含 Web 電子白板、Excel 全棧解決方案等一系列項目。
        • ueme-* 系列: 構建用戶體驗中臺系列項目。

        這部分筆者會在單獨的專題中進行討論,此處僅引出筆者的代碼庫的沉淀。

        雜談:程序員的職業轉折,小團隊與大團隊

        不覺入行已有十年,十年蒼狗,我卻是一直懷著對行業的焦慮前行,35 的檻一直如達摩克里斯之劍;不過回頭來看,至少對于身邊認識的很多前輩,在這個時代以 IT/編程為敲門磚進入某個行業/領域是極好的選擇。只要是真正的有心人,能夠在日常工作中進行人脈、管理、行業等等多維度的積累,是肯定能打破職業生涯的桎梏,完成轉型的。技術好的,不妨進入一些傳統行業。只要跨過了行業門檻,有公平競爭的機會,以更現代化的產品與研發效率,也是有可能進行降維打擊的。

        但是,需要特別強調的是,無論進入哪個行業,必須心懷敬畏;毫無行業經驗的人,看了幾個 PPT 就揚言要顛覆行業,不覺得是對于前人的不尊重嗎?同時不能太過畫餅,于己于人皆是如此,反對強行讓別人為自己的夢,或者錯誤買單。很多人既要獨斷專行的權利,卻不愿意承擔責任義務。

        職責的變化

        我從 2014 年開始一直陸陸續續參與創業團隊的工作,期間也在大廠工作了三年;頗有感觸的一點是,創業對于純技術背景的同學并不友好,往往技術越強,落差越大。譬如心態的轉變,很多技術背景的管理者往往會不適應類似于接口協調這樣的工作,覺得似乎是在浪費生命。但是需要慢慢地將自己從日常工作中抽身出來,為團隊保駕護航,上善若水,水利萬物而不爭;然后慢慢起身遠眺,做更偏重于協調,以業務整體績效為目標的事情。此時在團隊溝通上也需要注意技巧,良好的組織氣氛,是提升團隊研發效能的重要保障。就像玩游戲一樣,對于團隊、對于自己,想要翻越某些藩籬的時候,需要不斷地給予正向反饋。無論是公司、團隊的管理,還是自我管理,成就感都是非常不錯的活力棒與路標;而保證自己在日常工作或者 Side Project 中獲得成就感的一種前提,就是盡可能細粒度的切分任務。

        此外,研發往往有明確的目標、指標,但是在未知行業中,要提取、抽象出指標卻并非易事,并且目標也是不斷的變化;這點在大公司中往往是由 PD、PM 去屏蔽,但是在創業團隊中缺頗為考驗技術人員的辨識能力。譬如目標和過程的區分。最初我們以為目標是:客戶能夠用上我們的軟件與解決方案,后來發現這只是實現最終商業目標的過程,后來發現我們需要的過程是建立聯接而不是拘泥于軟件使用這件事。競爭意識損害競爭力,同樣達成目標的執念有時反而會損害執行力,很多開始以為的階段目標反而會成為你要征服的最高的巔峰。

        團隊的組成

        在創業型小團隊中,團隊構成不穩定。開發往往身兼數職,不僅僅實現功能,經常要處理用戶反饋和投訴,還要和產品討論需求、和設計討論界面實現,甚至有時要修電腦、裝軟件、解決疑難雜癥。同時創業期的產品可能質量要求不高。用戶量級小,即使質量稍差也能接受。做的功能亦不太考慮可擴展性,能用就行。技術視野狹隘。整體業務場景少,技術以使用為主,很少深挖底層原理和實現。產品的生命周期不可預測。做了 1、2 年的產品,可能因為各種原因而無法上線。但是,小團隊也同樣具備優勢。人數少的優勢,使得團隊易于扁平,決策層到執行層是直接關系,甚至有時執行層也參與決策。指令下達速度快,溝通成本降低。而且作為早期參與者,在渡過艱難的生存期之后,更容易成為核心人員。核心代表著股份與期權,持股干活更是動力十足。再往后,如果團隊能夠擴招,核心人員往往是管理人員的首選。

        合適的人才是團隊的基石,招聘也是團隊長久的任務與挑戰;特別是對于技術負責人,往往也需要承擔起招聘。早期的團隊往往是內部推薦,或者以人帶人,應當盡量招聘合適的人才,過低或者過高往往都會加重團隊的管理成本。在第一輪快速擴張之后的平穩期,穩定是重中之重,同時注意流水不腐,戶樞不蠹。同時團隊無論大小,即使沒有專門的 HR,也需要盡量保證面試流程的正規性,并且針對不同的面試者展示團隊不同的優勢:氛圍良好/極客文化/快速發展/行業優勢等等。不過隨著團隊的迅速擴張,人員擴充本身是熵增的過程,但是熵增也意味著混亂與無序,作為技術團隊的領導者,需要不斷地進行重新定位與角色轉變。從早期的核心開發者,到漸進的團隊協調者,再到團隊的管理者。

        健康的團隊,應該是離開任何人都可以正常運轉;反過來看,如果核心成員發現自己在團隊中的地位是無可替代的,反而需要有危機感,寧可犧牲些可用性,也要換取些分區容忍性。技術負責人首先要能夠將任務合理劃分,將業務型的與通用型的模塊化切割開來,盡可能地定義明確邊界與交互的接口協議。這樣就能夠將任務打包給兼職/實習人員,盡可能地實現調度優化。

        結語

        前兩日有校友撰文寫道:人生之路,不似揮舞劍花那般行云流水,更若一首平仄絕句,錯落有致。面對道路的蜿蜒,唯有攜著“柳暗花明又一村”的篤定堅守,才能穿過眼前橫亙的“山重水復”。國學大師陳寅恪曾說,“唯此獨立之精神,自由之思想,歷千萬祀,與天壤而同久,共三光而永光?!庇趥€人,既要失敗要乘早,窮人家的孩子承擔不起失敗的代價。不過也要隨時轉換,如多年前一次失敗的創業,創業痛苦的并不是燦爛熱烈的死去,而是將死不死,雖靜美卻無心賞秋葉。

        最后,謹以此文,致敬認識的或者不認識的創業者,也是贈言給身邊走在創業路上的朋友。

        查看原文

        贊 7 收藏 3 評論 2

        王下邀月熊_Chevalier 發布了文章 · 2020-02-23

        2019 我的技術之路:分布式系統到分布式制造

        NGTE Books

        2020 開春一場疫情,打亂了許多人的安排,也成為了此文拖延的借口;果然年年歲歲花相似,歲歲年年拖延癥啊。2020 年于我而言也是折騰的一年,離開阿里重回第三次創業的軌道;不同于之前的互聯網與大數據這種純軟的方向,我們一些五年前在 3D 打印領域折戟的小伙伴重聚,依托于母公司的設備、材料研發的能力,自己獨立地去運行幾家分布在全國的 3D 打印工廠,盡可能地貼近制造的根本,然后以此探索數字化、智能化能力在未來的 3D 浪潮中的賦能落地。

        面向 2020 的某熊知識體系

        在阿里的一年多時間,我有幸系統地學習了軟件架構以及較為完整的大規模軟件系統的開發、測試、部署與運維流程,及其間完整的支撐系統設計。其中很多的理念、思想,包括正面、反面的東西,結合著半年的實踐,讓我了解對于一家初創階段的公司,應該在技術上進行怎樣的取舍、怎樣的儲備、怎樣的有備無患。

        同時,這一年我雖然日漸憊懶,唯一堅持的就是多端連續性協作?,F在幾乎所有的文本內容都直傳到 Github,在 MAC 上使用 VSCode,在手機上使用 WorkCopy,搭建基于 Git 的個人多端協作方式。經過一年多的使用,感覺還挺順手的。

        Github Commit

        年末筆者還重構了自己的個人主頁,同時將所有的文檔利用 Docsify 工具直接發布為 Github Pages,端的也是特別方便?;诖?,筆者將所有的文章整理到了 NGTE Books 當中:

        NGTE Books

        筆者目前的沉淀分為兩大系列:

        技術視野

        本來打算在此文中多聊些分布式系統與分布式制造關聯的體悟,還請讀者移步《DistributedSystem-Series》。

        3D 打印與分布式制造

        作為典型的互聯網產品與研發團隊,我們真實地運營了多家手扳廠,同時也深入到了多個垂直終端行業。在 CCO 的時光讓我深刻認知到了服務的重要性,特別是對于制造業,最終就是由服務驅動產品、技術、制造。但是對于服務的核心,不同于互聯網的軟件產品的體感、人對人服務的溫度,制造業服務的核心還是性價比。

        淺談工業互聯網

        工業互聯網在蓬勃地發展,也必然會有光明的未來,但是前進的路途勢必曲折,螺旋式上升。這兩年參觀工博會都能夠看到很多智造的企業,特別是數控機床、激光等;工業互聯網則是將原來工廠單點變成一個全產業鏈、全價值鏈、全要素的一個融合,就像打通任督二脈,工廠本身也能利用新技術提升它的效率,變成數字化、智能化的工廠。我們能看到現在工業互聯網利用到的來自消費側、政務側的信息技術,首先就是 IoT 以及云與大數據的基礎設施,各種流程化、個性化搭建工具幫助我們的工廠快速地構建 ERP、CRM、MES 等信息系統,極大地節約了原本的 IT 設施的成本。

        其次,在數據時代,工業設備是天然的數據產生者,原本離散的工業設備可以變成在線的智能化設備,在這個基礎上我們能夠建立統一的數據中臺、制造中臺。然后,工業的場景天然是多場景連接的,我們的客戶可能在 PC 上建立工單、執行排產,然后在移動端實時接收消息,在廠區的大屏上查看運行狀態等等。不過就像很多的 ToC 側的創業者碰到的困難,從單點來考慮前途光明的事業一旦失速狂奔反而萬劫不復;工業互聯網的落地亦充滿坎坷,設備的差異暫且不說,就看似通用的 ERP 訂單部分,在不同的行業、不同的工廠都是同中有別,這也是恰恰難于下手的地方。

        工業互聯網最需要解決的還是要能夠真正地深入客戶,發現需求,梳理需求,定義需求。工業互聯網不是,也不能夠陽春白雪。我們團隊在前幾個月深度走訪了數十家工廠,我們也有自己的分布在全國各地的多個工藝技術中心、生產中心組成的分布式工廠。很多的互聯網創業者在下場的時候都會建很多的空中樓閣,不接地氣,不愿意去深入某個具體的行業、企業;這肯定是有問題的。但是另一方面,我們也看到很多做定制化 MES,做 ERP 的,他們的定制化程度、實施費用都很高,很難進行跨企業、跨行業的推廣;像 ThingWorx、根云、iSESOL 這樣的頭部企業都有快速進行行業解決方案構建的一攬子技術底座,但是具體研發效能如何,也是如人飲水冷暖自知。

        其他心得

        時間倉促,筆者未言太多,寥寥記述幾筆吧:

        • 不要感動自己:不能以考試的心態去創業,沒有明確的題目,沒有明確的答案,也不會因為你努力就給你高分。
        • 脫離互聯網思維:踏踏實實地去賺錢,用好每一分錢。
        • 脫離技術思維:對于非技術的創業而言,要學會尊重技術;對于我們這樣的開發出身,要能夠脫離技術思維。一方面不能只著眼于技術,溝通協調、商業市場都是非常的重要。

        大半年的實踐下來,感覺做 M 端的產品,即面向 M 端產品和 B、C 端的產品還是有非常大的差異的;這些會在《Product Series》中進行詳細闡述。

        二十七載始讀書

        往年都是推薦大家閱讀很多的資訊、書籍,今年我自己的知識來源除了閱讀之外,音頻播客成了最大的補充。在喜馬拉雅、得到、荔枝等等音頻的 App 上每天都會有大量的碎片時間進行收聽,譬如早起洗漱、上下班通勤或者是工作間歇休息的時候。曾經我一直有考慮自己會在哪一年能開始讀書,沒想到是今年通過聽書的方式來完成了這個夙愿。在《Financial Series》中我也打算記述有關財經與投資的相關筆記。

        年近而立,希望能有機會拿到下一段旅程的入場券;想都是問題,做才是答案,干就得了,雞湯有毒,以毒攻毒。今年,我回繼續寫文章,碼代碼,偶爾錄制一些視頻。

        查看原文

        贊 6 收藏 3 評論 2

        王下邀月熊_Chevalier 發布了文章 · 2019-09-06

        并發面試必備系列之進程、線程與協程

        坐標上海松江高科技園,誠聘高級前端工程師/高級 Java 工程師,有興趣的看 JD:https://www.lagou.com/jobs/63...

        并發面試必備系列之進程、線程與協程

        《Awesome Interviews》 歸納的常見面試題中,無論前后端,并發與異步的相關知識都是面試的中重中之重,《并發編程》系列即對于面試中常見的并發知識再進行回顧總結;你也可以前往 《Awesome Interviews》,在實際的面試題考校中了解自己的掌握程度。也可以前往《Java 實戰》、《Go 實戰》等了解具體編程語言中的并發編程的相關知識。

        在未配置 OS 的系統中,程序的執行方式是順序執行,即必須在一個程序執行完后,才允許另一個程序執行;在多道程序環境下,則允許多個程序并發執行。程序的這兩種執行方式間有著顯著的不同。也正是程序并發執行時的這種特征,才導致了在操作系統中引入進程的概念。進程是資源分配的基本單位,線程是資源調度的基本單位。

        應用啟動體現的就是靜態指令加載進內存,進而進入 CPU 運算,操作系統在內存開辟了一段棧內存用來存放指令和變量值,從而形成了進程。早期的操作系統基于進程來調度 CPU,不同進程間是不共享內存空間的,所以進程要做任務切換就要切換內存映射地址。由于進程的上下文關聯的變量,引用,計數器等現場數據占用了打段的內存空間,所以頻繁切換進程需要整理一大段內存空間來保存未執行完的進程現場,等下次輪到 CPU 時間片再恢復現場進行運算。

        這樣既耗費時間又浪費空間,所以我們才要研究多線程。一個進程創建的所有線程,都是共享一個內存空間的,所以線程做任務切換成本就很低了?,F代的操作系統都基于更輕量的線程來調度,現在我們提到的“任務切換”都是指“線程切換”。

        進程與線程

        本部分節選自 《Linux 與操作系統/進程管理》。

        在未配置 OS 的系統中,程序的執行方式是順序執行,即必須在一個程序執行完后,才允許另一個程序執行;在多道程序環境下,則允許多個程序并發執行。程序的這兩種執行方式間有著顯著的不同。也正是程序并發執行時的這種特征,才導致了在操作系統中引入進程的概念。進程是資源分配的基本單位,線程是資源調度的基本單位。

        進程(Process)

        進程是操作系統對一個正在運行的程序的一種抽象,在一個系統上可以同時運行多個進程,而每個進程都好像在獨占地使用硬件。所謂的并發運行,則是說一個進程的指令和另一個進程的指令是交錯執行的。無論是在單核還是多核系統中,可以通過處理器在進程間切換,來實現單個 CPU 看上去像是在并發地執行多個進程。操作系統實現這種交錯執行的機制稱為上下文切換。

        操作系統保持跟蹤進程運行所需的所有狀態信息。這種狀態,也就是上下文,它包括許多信息,例如 PC 和寄存器文件的當前值,以及主存的內容。在任何一個時刻,單處理器系統都只能執行一個進程的代碼。當操作系統決定要把控制權從當前進程轉移到某個新進程時,就會進行上下文切換,即保存當前進程的上下文、恢復新進程的上下文,然后將控制權傳遞到新進程。新進程就會從上次停止的地方開始。

        image

        虛擬存儲管理一節中,我們介紹過它為每個進程提供了一個假象,即每個進程都在獨占地使用主存。每個進程看到的是一致的存儲器,稱為虛擬地址空間。其虛擬地址空間最上面的區域是為操作系統中的代碼和數據保留的,這對所有進程來說都是一樣的;地址空間的底部區域存放用戶進程定義的代碼和數據。

        image

        • 程序代碼和數據,對于所有的進程來說,代碼是從同一固定地址開始,直接按照可執行目標文件的內容初始化。
        • 堆,代碼和數據區后緊隨著的是運行時堆。代碼和數據區是在進程一開始運行時就被規定了大小,與此不同,當調用如 malloc 和 free 這樣的 C 標準庫函數時,堆可以在運行時動態地擴展和收縮。
        • 共享庫:大約在地址空間的中間部分是一塊用來存放像 C 標準庫和數學庫這樣共享庫的代碼和數據的區域。
        • 棧,位于用戶虛擬地址空間頂部的是用戶棧,編譯器用它來實現函數調用。和堆一樣,用戶棧在程序執行期間可以動態地擴展和收縮。
        • 內核虛擬存儲器:內核總是駐留在內存中,是操作系統的一部分。地址空間頂部的區域是為內核保留的,不允許應用程序讀寫這個區域的內容或者直接調用內核代碼定義的函數。

        線程(Thread)

        在現代系統中,一個進程實際上可以由多個稱為線程的執行單元組成,每個線程都運行在進程的上下文中,并共享同樣的代碼和全局數據。進程的個體間是完全獨立的,而線程間是彼此依存的。多進程環境中,任何一個進程的終止,不會影響到其他進程。而多線程環境中,父線程終止,全部子線程被迫終止(沒有了資源)。

        而任何一個子線程終止一般不會影響其他線程,除非子線程執行了 exit() 系統調用。任何一個子線程執行 exit(),全部線程同時滅亡。多線程程序中至少有一個主線程,而這個主線程其實就是有 main 函數的進程。它是整個程序的進程,所有線程都是它的子線程;我們通常把具有多線程的主進程稱之為主線程。

        線程共享的環境包括:進程代碼段、進程的公有數據、進程打開的文件描述符、信號的處理器、進程的當前目錄、進程用戶 ID 與進程組 ID 等,利用這些共享的數據,線程很容易的實現相互之間的通訊。線程擁有這許多共性的同時,還擁有自己的個性,并以此實現并發性:

        • 線程 ID:每個線程都有自己的線程 ID,這個 ID 在本進程中是唯一的。進程用此來標識線程。
        • 寄存器組的值:由于線程間是并發運行的,每個線程有自己不同的運行線索,當從一個線程切換到另一個線程上時,必須將原有的線程的寄存器集合的狀態保存,以便 將來該線程在被重新切換到時能得以恢復。
        • 線程的堆棧:堆棧是保證線程獨立運行所必須的。線程函數可以調用函數,而被調用函數中又是可以層層嵌套的,所以線程必須擁有自己的函數堆棧, 使得函數調用可以正常執行,不受其他線程的影響。
        • 錯誤返回碼:由于同一個進程中有很多個線程在同時運行,可能某個線程進行系統調用后設置了 errno 值,而在該 線程還沒有處理這個錯誤,另外一個線程就在此時 被調度器投入運行,這樣錯誤值就有可能被修改。 所以,不同的線程應該擁有自己的錯誤返回碼變量。
        • 線程的信號屏蔽碼:由于每個線程所感興趣的信號不同,所以線程的信號屏蔽碼應該由線程自己管理。但所有的線程都共享同樣的信號處理器。
        • 線程的優先級:由于線程需要像進程那樣能夠被調度,那么就必須要有可供調度使用的參數,這個參數就是線程的優先級。

        image.png

        線程模型

        線程實現在用戶空間下

        當線程在用戶空間下實現時,操作系統對線程的存在一無所知,操作系統只能看到進程,而不能看到線程。所有的線程都是在用戶空間實現。在操作系統看來,每一個進程只有一個線程。過去的操作系統大部分是這種實現方式,這種方式的好處之一就是即使操作系統不支持線程,也可以通過庫函數來支持線程。

        在這在模型下,程序員需要自己實現線程的數據結構、創建銷毀和調度維護。也就相當于需要實現一個自己的線程調度內核,而同時這些線程運行在操作系統的一個進程內,最后操作系統直接對進程進行調度。

        這樣做有一些優點,首先就是確實在操作系統中實現了真實的多線程,其次就是線程的調度只是在用戶態,減少了操作系統從內核態到用戶態的切換開銷。這種模式最致命的缺點也是由于操作系統不知道線程的存在,因此當一個進程中的某一個線程進行系統調用時,比如缺頁中斷而導致線程阻塞,此時操作系統會阻塞整個進程,即使這個進程中其它線程還在工作。還有一個問題是假如進程中一個線程長時間不釋放 CPU,因為用戶空間并沒有時鐘中斷機制,會導致此進程中的其它線程得不到 CPU 而持續等待。

        線程實現在操作系統內核中

        內核線程就是直接由操作系統內核(Kernel)支持的線程,這種線程由內核來完成線程切換,內核通過操縱調度器(Scheduler)對線程進行調度,并負責將線程的任務映射到各個處理器上。每個內核線程可以視為內核的一個分身,這樣操作系統就有能力同時處理多件事情,支持多線程的內核就叫做多線程內核(Multi-Threads Kernel)。

        程序員直接使用操作系統中已經實現的線程,而線程的創建、銷毀、調度和維護,都是靠操作系統(準確的說是內核)來實現,程序員只需要使用系統調用,而不需要自己設計線程的調度算法和線程對 CPU 資源的搶占使用。

        使用用戶線程加輕量級進程混合實現

        在這種混合實現下,即存在用戶線程,也存在輕量級進程。用戶線程還是完全建立在用戶空間中,因此用戶線程的創建、切換、析構等操作依然廉價,并且可以支持大規模的用戶線程并發。而操作系統提供支持的輕量級進程則作為用戶線程和內核線程之間的橋梁,這樣可以使用內核提供的線程調度功能及處理器映射,并且用戶線程的系統調用要通過輕量級進程來完成,大大降低了整個進程被完全阻塞的風險。在這種混合模式中,用戶線程與輕量級進程的數量比是不定的,即為 N:M 的關系:

        Golang 的協程就是使用了這種模型,在用戶態,協程能快速的切換,避免了線程調度的 CPU 開銷問題,協程相當于線程的線程。

        Linux 中的線程

        在 Linux 2.4 版以前,線程的實現和管理方式就是完全按照進程方式實現的;在 Linux 2.6 之前,內核并不支持線程的概念,僅通過輕量級進程(Lightweight Process)模擬線程;輕量級進程是建立在內核之上并由內核支持的用戶線程,它是內核線程的高度抽象,每一個輕量級進程都與一個特定的內核線程關聯。內核線程只能由內核管理并像普通進程一樣被調度。這種模型最大的特點是線程調度由內核完成了,而其他線程操作(同步、取消)等都是核外的線程庫(Linux Thread)函數完成的。

        為了完全兼容 Posix 標準,Linux 2.6 首先對內核進行了改進,引入了線程組的概念(仍然用輕量級進程表示線程),有了這個概念就可以將一組線程組織稱為一個進程,不過內核并沒有準備特別的調度算法或是定義特別的數據結構來表征線程;相反,線程僅僅被視為一個與其他進程(概念上應該是線程)共享某些資源的進程(概念上應該是線程)。在實現上主要的改變就是在 task_struct 中加入 tgid 字段,這個字段就是用于表示線程組 id 的字段。在用戶線程庫方面,也使用 NPTL 代替 Linux Thread,不同調度模型上仍然采用 1 對 1 模型。

        進程的實現是調用 fork 系統調用:pid_t fork(void);,線程的實現是調用 clone 系統調用:int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...)。與標準 fork() 相比,線程帶來的開銷非常小,內核無需單獨復制進程的內存空間或文件描寫敘述符等等。這就節省了大量的 CPU 時間,使得線程創建比新進程創建快上十到一百倍,能夠大量使用線程而無需太過于操心帶來的 CPU 或內存不足。無論是 fork、vfork、kthread_create 最后都是要調用 do_fork,而 do_fork 就是根據不同的函數參數,對一個進程所需的資源進行分配。

        內核線程

        內核線程是由內核自己創建的線程,也叫做守護線程(Deamon),在終端上用命令 ps -Al 列出的所有進程中,名字以 k 開關以 d 結尾的往往都是內核線程,比如 kthreadd、kswapd 等。與用戶線程相比,它們都由 do_fork() 創建,每個線程都有獨立的 task_struct 和內核棧;也都參與調度,內核線程也有優先級,會被調度器平等地換入換出。二者的不同之處在于,內核線程只工作在內核態中;而用戶線程則既可以運行在內核態(執行系統調用時),也可以運行在用戶態;內核線程沒有用戶空間,所以對于一個內核線程來說,它的 0~3G 的內存空間是空白的,它的 current->mm 是空的,與內核使用同一張頁表;而用戶線程則可以看到完整的 0~4G 內存空間。

        在 Linux 內核啟動的最后階段,系統會創建兩個內核線程,一個是 init,一個是 kthreadd。其中 init 線程的作用是運行文件系統上的一系列”init”腳本,并啟動 shell 進程,所以 init 線程稱得上是系統中所有用戶進程的祖先,它的 pid 是 1。kthreadd 線程是內核的守護線程,在內核正常工作時,它永遠不退出,是一個死循環,它的 pid 是 2。

        Coroutine | 協程

        協程是用戶模式下的輕量級線程,最準確的名字應該叫用戶空間線程(User Space Thread),在不同的領域中也有不同的叫法,譬如纖程(Fiber)、綠色線程(Green Thread)等等。操作系統內核對協程一無所知,協程的調度完全有應用程序來控制,操作系統不管這部分的調度;一個線程可以包含一個或多個協程,協程擁有自己的寄存器上下文和棧,協程調度切換時,將寄存器上細紋和棧保存起來,在切換回來時恢復先前保運的寄存上下文和棧。

        協程的優勢如下:

        • 節省內存,每個線程需要分配一段棧內存,以及內核里的一些資源
        • 節省分配線程的開銷(創建和銷毀線程要各做一次 syscall)
        • 節省大量線程切換帶來的開銷
        • 與 NIO 配合實現非阻塞的編程,提高系統的吞吐

        比如 Golang 里的 go 關鍵字其實就是負責開啟一個 Fiber,讓 func 邏輯跑在上面。而這一切都是發生的用戶態上,沒有發生在內核態上,也就是說沒有 ContextSwitch 上的開銷。協程的實現庫中筆者較為常用的譬如 Go Routine、node-fibers、Java-Quasar 等。

        Go 的協程模型

        Go 線程模型屬于多對多線程模型,在操作系統提供的內核線程之上,Go 搭建了一個特有的兩級線程模型。Go 中使用使用 Go 語句創建的 Goroutine 可以認為是輕量級的用戶線程,Go 線程模型包含三個概念:

        • G: 表示 Goroutine,每個 Goroutine 對應一個 G 結構體,G 存儲 Goroutine 的運行堆棧、狀態以及任務函數,可重用。G 并非執行體,每個 G 需要綁定到 P 才能被調度執行。
        • P: Processor,表示邏輯處理器,對 G 來說,P 相當于 CPU 核,G 只有綁定到 P(在 P 的 local runq 中)才能被調度。對 M 來說,P 提供了相關的執行環境(Context),如內存分配狀態(mcache),任務隊列(G)等,P 的數量決定了系統內最大可并行的 G 的數量(物理 CPU 核數 >= P 的數量),P 的數量由用戶設置的 GOMAXPROCS 決定,但是不論 GOMAXPROCS 設置為多大,P 的數量最大為 256。
        • M: Machine,OS 線程抽象,代表著真正執行計算的資源,在綁定有效的 P 后,進入 schedule 循環;M 的數量是不定的,由 Go Runtime 調整,為了防止創建過多 OS 線程導致系統調度不過來,目前默認最大限制為 10000 個。

        在 Go 中每個邏輯處理器(P)會綁定到某一個內核線程上,每個邏輯處理器(P)內有一個本地隊列,用來存放 Go 運行時分配的 goroutine。多對多線程模型中是操作系統調度線程在物理 CPU 上運行,在 Go 中則是 Go 的運行時調度 Goroutine 在邏輯處理器(P)上運行。

        Go 的棧是動態分配大小的,隨著存儲數據的數量而增長和收縮。每個新建的 Goroutine 只有大約 4KB 的棧。每個棧只有 4KB,那么在一個 1GB 的 RAM 上,我們就可以有 256 萬個 Goroutine 了,相對于 Java 中每個線程的 1MB,這是巨大的提升。Golang 實現了自己的調度器,允許眾多的 Goroutines 運行在相同的 OS 線程上。就算 Go 會運行與內核相同的上下文切換,但是它能夠避免切換至 ring-0 以運行內核,然后再切換回來,這樣就會節省大量的時間。

        在 Go 中存在兩級調度:

        • 一級是操作系統的調度系統,該調度系統調度邏輯處理器占用 cpu 時間片運行;
        • 一級是 Go 的運行時調度系統,該調度系統調度某個 Goroutine 在邏輯處理上運行。

        使用 Go 語句創建一個 Goroutine 后,創建的 Goroutine 會被放入 Go 運行時調度器的全局運行隊列中,然后 Go 運行時調度器會把全局隊列中的 Goroutine 分配給不同的邏輯處理器(P),分配的 Goroutine 會被放到邏輯處理器(P)的本地隊列中,當本地隊列中某個 Goroutine 就緒后待分配到時間片后就可以在邏輯處理器上運行了。

        Java 協程的討論

        目前,JVM 本身并未提供協程的實現庫,像 Quasar 這樣的協程框架似乎也仍非主流的并發問題解決方案,在本部分我們就討論下在 Java 中是否有必要一定要引入協程。在普通的 Web 服務器場景下,譬如 Spring Boot 中默認的 Worker 線程池線程數在 200(50 ~ 500) 左右,如果從線程的內存占用角度來考慮,每個線程上下文約 128KB,那么 500 個線程本身的內存占用在 60M,相較于整個堆棧不過爾爾。而 Java 本身提供的線程池,對于線程的創建與銷毀都有非常好的支持;即使 Vert.x 或 Kotlin 中提供的協程,往往也是基于原生線程池實現的。

        從線程的切換開銷的角度來看,我們常說的切換開銷往往是針對于活躍線程;而普通的 Web 服務器天然會有大量的線程因為請求讀寫、DB 讀寫這樣的操作而掛起,實際只有數十個并發活躍線程會參與到 OS 的線程切換調度。而如果真的存在著大量活躍線程的場景,Java 生態圈中也存在了 Akka 這樣的 Actor 并發模型框架,它能夠感知線程何時能夠執行工作,在用戶空間中構建運行時調度器,從而支持百萬級別的 Actor 并發。

        實際上我們引入協程的場景,更多的是面對所謂百萬級別連接的處理,典型的就是 IM 服務器,可能需要同時處理大量空閑的鏈接。此時在 Java 生態圈中,我們可以使用 Netty 去進行處理,其基于 NIO 與 Worker Thread 實現的調度機制就很類似于協程,可以解決絕大部分因為 IO 的等待造成資源浪費的問題。而從并發模型對比的角度,如果我們希望能遵循 Go 中以消息傳遞方式實現內存共享的理念,那么也可以采用 Disruptor 這樣的模型。

        Java 線程與操作系統線程

        Java 線程在 JDK1.2 之前,是基于稱為“綠色線程”(Green Threads)的用戶線程實現的,而到了 JDK1.2 及以后,JVM 選擇了更加穩健且方便使用的操作系統原生的線程模型,通過系統調用,將程序的線程交給了操作系統內核進行調度。因此,在目前的 JDK 版本中,操作系統支持怎樣的線程模型,在很大程度上決定了 Java 虛擬機的線程是怎樣映射的,這點在不同的平臺上沒有辦法達成一致,虛擬機規范中也并未限定 Java 線程需要使用哪種線程模型來實現。線程模型只對線程的并發規模和操作成本產生影響,對 Java 程序的編碼和運行過程來說,這些差異都是透明的。

        對于 Sun JDK 來說,它的 Windows 版與 Linux 版都是使用一對一的線程模型實現的,一條 Java 線程就映射到一條輕量級進程之中,因為 Windows 和 Linux 系統提供的線程模型就是一對一的。也就是說,現在的 Java 中線程的本質,其實就是操作系統中的線程,Linux 下是基于 pthread 庫實現的輕量級進程,Windows 下是原生的系統 Win32 API 提供系統調用從而實現多線程。

        在現在的操作系統中,因為線程依舊被視為輕量級進程,所以操作系統中線程的狀態實際上和進程狀態是一致的模型。從實際意義上來講,操作系統中的線程除去 new 和 terminated 狀態,一個線程真實存在的狀態,只有:

        • ready:表示線程已經被創建,正在等待系統調度分配 CPU 使用權。
        • running:表示線程獲得了 CPU 使用權,正在進行運算。
        • waiting:表示線程等待(或者說掛起),讓出 CPU 資源給其他線程使用。

        對于 Java 中的線程狀態:無論是 Timed Waiting ,Waiting 還是 Blocked,對應的都是操作系統線程的 waiting(等待)狀態。而 Runnable 狀態,則對應了操作系統中的 ready 和 running 狀態。Java 線程和操作系統線程,實際上同根同源,但又相差甚遠。

        延伸閱讀

        您可以通過以下導航來在 Gitbook 中閱讀筆者的系列文章,涵蓋了技術資料歸納、編程語言與理論、Web 與大前端、服務端開發與基礎架構、云計算與大數據、數據科學與人工智能、產品設計等多個領域:

        此外,你還可前往 xCompass 交互式地檢索、查找需要的文章/鏈接/書籍/課程;或者在 MATRIX 文章與代碼索引矩陣中查看文章與項目源代碼等更詳細的目錄導航信息。最后,你也可以關注微信公眾號:『某熊的技術之路』以獲取最新資訊。

        查看原文

        贊 19 收藏 12 評論 0

        王下邀月熊_Chevalier 發布了文章 · 2019-09-05

        并發面試必備系列之并發基礎與內存模型

        坐標上海松江高科技園,誠聘高級前端工程師/高級 Java 工程師,有興趣的看 JD:https://www.lagou.com/jobs/63...

        并發面試必備系列之并發基礎與內存模型

        《Awesome Interviews》 歸納的常見面試題中,無論前后端,并發與異步的相關知識都是面試的中重中之重,本系列即對于面試中常見的并發知識再進行回顧總結;你也可以前往 《Awesome Interviews》,在實際的面試題考校中了解自己的掌握程度。也可以前往《Java 實戰》、《Go 實戰》等了解具體編程語言中的并發編程的相關知識。

        隨著硬件性能的迅猛發展與大數據時代的來臨,為了讓代碼運行得更快,單純依靠更快的硬件已無法滿足要求,并行和分布式計算是現代應用程序的主要內容;我們需要利用多個核心或多臺機器來加速應用程序或大規模運行它們,并發編程日益成為編程中不可忽略的重要組成部分。

        簡單定義來看,如果執行單元的邏輯控制流在時間上重疊,那它們就是并發(Concurrent)的;由此定義可擴展到非常廣泛的概念,其向下依賴于操作系統、存儲等,與分布式系統、微服務等,而又會具體落地于 Java 并發編程、Go 并發編程、JavaScript 異步編程等領域。云計算承諾在所有維度上(內存、計算、存儲等)實現無限的可擴展性,并發編程及其相關理論也是我們構建大規模分布式應用的基礎。

        并發編程

        并發與并行

        并發就是可同時發起執行的程序,指程序的邏輯結構;并行就是可以在支持并行的硬件上執行的并發程序,指程序的運?狀態。換句話說,并發程序代表了所有可以實現并發行為的程序,這是一個比較寬泛的概念,并行程序也只是他的一個子集。并發是并?的必要條件;但并發不是并?的充分條件。并發只是更符合現實問題本質的表達,目的是簡化代碼邏輯,?不是使程序運?更快。要是程序運?更快必是并發程序加多核并?。

        簡言之,并發是同一時間應對(dealing with)多件事情的能力;并行是同一時間動手做(doing)多件事情的能力。

        image.png

        并發是問題域中的概念——程序需要被設計成能夠處理多個同時(或者幾乎同時)發生的事件;一個并發程序含有多個邏輯上的獨立執行塊,它們可以獨立地并行執行,也可以串行執行。而并行則是方法域中的概念——通過將問題中的多個部分并行執行,來加速解決問題。一個并行程序解決問題的速度往往比一個串行程序快得多,因為其可以同時執行整個任務的多個部分。并行程序可能有多個獨立執行塊,也可能僅有一個。

        具體而言,早期的 Redis(6.0 版本后也引入了多線程) 會是一個很好地區分并發和并行的例子,它本身是一個單線程的數據庫,但是可以通過多路復用與事件循環的方式來提供并發地 IO 服務。這是因為多核并行本質上會有很大的一個同步的代價,特別是在鎖或者信號量的情況下。因此,Redis 利用了單線程的事件循環來保證一系列的原子操作,從而保證了即使在高并發的情況下也能達到幾乎零消耗的同步。再引用下 Rob Pike 的描述:

        A single-threaded program can definitely provides concurrency at the IO level by using an IO (de)multiplexing mechanism and an event loop (which is what Redis does).

        并發維度

        線程級并發

        從 20 世紀 60 年代初期出現時間共享以來,計算機系統中就開始有了對并發執行的支持;傳統意義上,這種并發執行只是模擬出來的,是通過使一臺計算機在它正在執行的進程間快速切換的方式實現的,這種配置稱為單處理器系統。從 20 世紀 80 年代開始,多處理器系統,即由單操作系統內核控制的多處理器組成的系統采用了多核處理器與超線程(HyperThreading)等技術允許我們實現真正的并行。多核處理器是將多個 CPU 集成到一個集成電路芯片上:

        image

        超線程,有時稱為同時多線程(simultaneous multi-threading),是一項允許一個 CPU 執行多個控制流的技術。它涉及 CPU 某些硬件有多個備份,比如程序計數器和寄存器文件;而其他的硬件部分只有一份,比如執行浮點算術運算的單元。常規的處理器需要大約 20 000 個時鐘周期做不同線程間的轉換,而超線程的處理器可以在單個周期的基礎上決定要執行哪一個線程。這使得 CPU 能夠更好地利用它的處理資源。例如,假設一個線程必須等到某些數據被裝載到高速緩存中,那 CPU 就可以繼續去執行另一個線程。

        指令級并發

        在較低的抽象層次上,現代處理器可以同時執行多條指令的屬性稱為指令級并行。實每條指令從開始到結束需要長得多的時間,大約 20 個或者更多的周期,但是處理器使用了非常多的聰明技巧來同時處理多達 100 條的指令。在流水線中,將執行一條指令所需要的活動劃分成不同的步驟,將處理器的硬件組織成一系列的階段,每個階段執行一個步驟。這些階段可以并行地操作,用來處理不同指令的不同部分。我們會看到一個相當簡單的硬件設計,它能夠達到接近于一個時鐘周期一條指令的執行速率。如果處理器可以達到比一個周期一條指令更快的執行速率,就稱之為超標量(Super Scalar)處理器。

        單指令、多數據

        在最低層次上,許多現代處理器擁有特殊的硬件,允許一條指令產生多個可以并行執行的操作,這種方式稱為單指令、多數據,即 SIMD 并行。例如,較新的 Intel 和 AMD 處理器都具有并行地對 4 對單精度浮點數(C 數據類型 float)做加法的指令。

        同步、異步、阻塞、非阻塞

        在并發與并行的基礎概念之后,我們還需要了解同步、異步、阻塞與非阻塞這幾個概念的關系與區別。

        同步即執行某個操作開始后就一直等著按部就班的直到操作結束,異步即執行某個操作后立即離開,后面有響應的話再來通知執行者。從編程的角度來看,如果同步調用,則調用的結果會在本次調用后返回。如果異步調用,則調用的結果不會直接返回。會返回一個 Future 或者 Promise 對象來供調用方主動/被動的獲取本次調用的結果。

        而阻塞與非阻塞在并發編程中,主要是從對于臨界區公共資源或者共享數據競態訪問的角度來進行區分。某個操作需要的共享資源被占用了,只能等待,稱為阻塞;某個操作需要的共享資源被占用了,不等待立即返回,并攜帶錯誤信息回去,期待重試,則稱為非阻塞。

        值得一提的是,在并發 IO 的討論中,我們還會出現同步非阻塞的 IO 模型,這是因為 IO 操作(read/write 系統調用)其實包含了發起 IO 請求與實際的 IO 讀寫這兩個步驟。阻塞 IO 和非阻塞 IO 的區別在于第一步,發起 IO 請求的進程是否會被阻塞,如果阻塞直到 IO 操作完成才返回那么就是傳統的阻塞 IO,如果不阻塞,那么就是非阻塞 IO。同步 IO 和異步 IO 的區別就在于第二步,實際的 IO 讀寫(內核態與用戶態的數據拷貝)是否需要進程參與,如果需要進程參與則是同步 IO,如果不需要進程參與就是異步 IO。如果實際的 IO 讀寫需要請求進程參與,那么就是同步 IO;因此阻塞 IO、非阻塞 IO、IO 復用、信號驅動 IO 都是同步 IO。

        并發級別

        在實際的部署環境下,受限于 CPU 的數量,我們不可能無限制地增加線程數量,不同場景需要的并發需求也不一樣;譬如秒殺系統中我們強調高并發高吞吐,而對于一些下載服務,則更強調快響應低時延。因此根據不同的需求場景我們也可以定義不同的并發級別:

        • 阻塞:阻塞是指一個線程進入臨界區后,其它線程就必須在臨界區外等待,待進去的線程執行完任務離開臨界區后,其它線程才能再進去。
        • 無饑餓:線程排隊先來后到,不管優先級大小,先來先執行,就不會產生饑餓等待資源,也即公平鎖;相反非公平鎖則是根據優先級來執行,有可能排在前面的低優先級線程被后面的高優先級線程插隊,就形成饑餓
        • 無障礙:共享資源不加鎖,每個線程都可以自有讀寫,單監測到被其他線程修改過則回滾操作,重試直到單獨操作成功;風險就是如果多個線程發現彼此修改了,所有線程都需要回滾,就會導致死循環的回滾中,造成死鎖
        • 無鎖:無鎖是無障礙的加強版,無鎖級別保證至少有一個線程在有限操作步驟內成功退出,不管是否修改成功,這樣保證了多個線程回滾不至于導致死循環
        • 無等待:無等待是無鎖的升級版,并發編程的最高境界,無鎖只保證有線程能成功退出,但存在低級別的線程一直處于饑餓狀態,無等待則要求所有線程必須在有限步驟內完成退出,讓低級別的線程有機會執行,從而保證所有線程都能運行,提高并發度。

        量化模型

        多線程不意味著并發,但并發肯定是多線程或者多進程;多線程存在的優勢是能夠更好的利用資源,有更快的請求響應。但是我們也深知一旦進入多線程,附帶而來的是更高的編碼復雜度,線程設計不當反而會帶來更高的切換成本和資源開銷。如何衡量多線程帶來的效率提升呢,我們需要借助兩個定律來衡量。

        Amdahl 定律

        Amdahl 定律可以用來計算處理器平行運算之后效率提升的能力,其由 Gene Amdal 在 1967 年提出;它描述了在一個系統中,基于可并行化和串行化的組件各自所占的比重,程序通過獲得額外的計算資源,理論上能夠加速多少。任何程序或算法可以按照是否可以被并行化分為可以被并行化的部分 1 - B 與不可以被并行化的部分 B,那么根據 Amdahl 定律,不同的并行因子的情況下程序的總執行時間的變化如下所示:

        如果 F 是必須串行化執行的比重,那么 Amdahl 定律告訴我們,在一個 N 處理器的機器中,我們最多可以加速:

        當 N 無限增大趨近無窮時,speedup 的最大值無限趨近 1/F,這意味著一個程序中如果 50% 的處理都需要串行進行的話,speedup 只能提升 2 倍(不考慮事實上有多少線程可用);如果程序的 10% 需要串行進行,speedup 最多能夠提高近 10 倍。

        Amdahl 定律同樣量化了串行化的效率開銷。在擁有 10 個處理器的系統中,程序如果有 10% 是串行化的,那么最多可以加速 5.3 倍(53 %的使用率),在擁有 100 個處理器的系統中,這個數字可以達到 9.2(9 %的使用率)。這使得無效的 CPU 利用永遠不可能到達 10 倍。下圖展示了隨著串行執行和處理器數量變化,處理器最大限度的利用率的曲線。隨著處理器數量的增加,我們很明顯地看到,即使串行化執行的程度發 生細微的百分比變化,都會大大限制吞吐量隨計算資源增加。

        Amdahl 定律旨在說明,多核 CPU 對系統進行優化時,優化的效果取決于 CPU 的數量以及系統中的串行化程序的比重;如果僅關注于提高 CPU 數量而不降低程序的串行化比重,也無法提高系統性能。

        Gustafson

        系統優化某部件所獲得的系統性能的改善程度,取決于該部件被使用的頻率,或所占總執行時間的比例。

        內存模型

        如前文所述,現代計算機通常有兩個或者更多的 CPU,一些 CPU 還有多個核;其允許多個線程同時運行,每個 CPU 在某個時間片內運行其中的一個線程。在存儲管理一節中我們介紹了計算機系統中的不同的存儲類別:

        image

        每個 CPU 包含多個寄存器,這些寄存器本質上就是 CPU 內存;CPU 在寄存器中執行操作的速度會比在主內存中操作快非常多。每個 CPU 可能還擁有 CPU 緩存層,CPU 訪問緩存層的速度比訪問主內存塊很多,但是卻比訪問寄存器要慢。計算機還包括主內存(RAM),所有的 CPU 都可以訪問這個主內存,主內存一般都比 CPU 緩存大很多,但速度要比 CPU 緩存慢。當一個 CPU 需要訪問主內存的時候,會把主內存中的部分數據讀取到 CPU 緩存,甚至進一步把緩存中的部分數據讀取到內部的寄存器,然后對其進行操作。當 CPU 需要向主內存寫數據的時候,會將寄存器中的數據寫入緩存,某些時候會將數據從緩存刷入主內存。無論從緩存讀還是寫數據,都沒有必要一次性全部讀出或者寫入,而是僅對部分數據進行操作。

        并發編程中的問題,往往源于緩存導致的可見性問題、線程切換導致的原子性問題以及編譯優化帶來的有序性問題。以 Java 虛擬機為例,每個線程都擁有一個屬于自己的線程棧(調用棧),隨著線程代碼的執行,調用棧會隨之改變。線程棧中包含每個正在執行的方法的局部變量。每個線程只能訪問屬于自己的棧。調用棧中的局部變量,只有創建這個棧的線程才可以訪問,其他線程都不能訪問。即使兩個線程在執行一段相同的代碼,這兩個線程也會在屬于各自的線程棧中創建局部變量。因此,每個線程擁有屬于自己的局部變量。所有基本類型的局部變量全部存放在線程棧中,對其他線程不可見。一個線程可以把基本類型拷貝到其他線程,但是不能共享給其他線程,而無論哪個線程創建的對象都存放在堆中。

        原子性

        所謂的原子性,就是一個或者多個操作在 CPU 執行的過程中不被中斷的特性,CPU 能保證的原子操作是 CPU 指令級別的,而不是高級語言的操作符。我們在編程語言中部分看似原子操作的指令,在被編譯到匯編之后往往會變成多個操作:

        i++
        
        # 編譯成匯編之后就是:
        # 讀取當前變量 i 并把它賦值給一個臨時寄存器;
        movl i(%rip), %eax
        # 給臨時寄存器+1;
        addl $1, %eax
        # 把 eax 的新值寫回內存
        movl %eax, i(%rip)

        我們可以清楚看到 C 代碼只需要一句,但編譯成匯編卻需要三步(這里不考慮編譯器優化,實際上通過編譯器優化可以將這三條匯編指令合并成一條)。也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。按照原子操作解決同步問題方式:依靠處理器原語支持把上述三條指令合三為一,當做一條指令來執行,保證在執行過程中不會被打斷并且多線程并發也不會受到干擾。這樣同步問題迎刃而解,這也就是所謂的原子操作。但處理器沒有義務為任意代碼片段提供原子性操作,尤其是我們的臨界區資源十分龐大甚至大小不確定,處理器沒有必要或是很難提供原子性支持,此時往往需要依賴于鎖來保證原子性。

        對應原子操作/事務在 Java 中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執行,要么不執行。Java 內存模型只保證了基本讀取和賦值是原子性操作,如果要實現更大范圍操作的原子性,可以通過 synchronized 和 Lock 來實現。由于 synchronized 和 Lock 能夠保證任一時刻只有一個線程執行該代碼塊,那么自然就不存在原子性問題了,從而保證了原子性。

        有序性

        顧名思義,有序性指的是程序按照代碼的先后順序執行?,F代編譯器的代碼優化和編譯器指令重排可能會影響到代碼的執行順序。編譯期指令重排是通過調整代碼中的指令順序,在不改變代碼語義的前提下,對變量訪問進行優化。從而盡可能的減少對寄存器的讀取和存儲,并充分復用寄存器。但是編譯器對數據的依賴關系判斷只能在單執行流內,無法判斷其他執行流對競爭數據的依賴關系。就拿無鎖環形隊列來說,如果 Writer 做的是先放置數據,再更新索引的行為。如果索引先于數據更新,Reader 就有可能會因為判斷索引已更新而讀到臟數據。

        禁止編譯器對該類變量的優化,解決了編譯期的重排序并不能保證有序性,因為 CPU 還有亂序執行(Out-of-Order Execution)的特性。流水線(Pipeline)和亂序執行是現代 CPU 基本都具有的特性。機器指令在流水線中經歷取指、譯碼、執行、訪存、寫回等操作。為了 CPU 的執行效率,流水線都是并行處理的,在不影響語義的情況下。處理器次序(Process Ordering,機器指令在 CPU 實際執行時的順序)和程序次序(Program Ordering,程序代碼的邏輯執行順序)是允許不一致的,即滿足 As-if-Serial 特性。顯然,這里的不影響語義依舊只能是保證指令間的顯式因果關系,無法保證隱式因果關系。即無法保證語義上不相關但是在程序邏輯上相關的操作序列按序執行。從此單核時代 CPU 的 Self-Consistent 特性在多核時代已不存在,多核 CPU 作為一個整體看,不再滿足 Self-Consistent 特性。

        簡單總結一下,如果不做多余的防護措施,單核時代的無鎖環形隊列在多核 CPU 中,一個 CPU 核心上的 Writer 寫入數據,更新 index 后。另一個 CPU 核心上的 Reader 依靠這個 index 來判斷數據是否寫入的方式不一定可靠。index 有可能先于數據被寫入,從而導致 Reader 讀到臟數據。

        在 Java 中與有序性相關的經典問題就是單例模式,譬如我們會采用靜態函數來獲取某個對象的實例,并且使用 synchronized 加鎖來保證只有單線程能夠觸發創建,其他線程則是直接獲取到實例對象。

        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }

        不過雖然我們期望的對象創建的過程是:內存分配、初始化對象、將對象引用賦值給成員變量,但是實際情況下經過優化的代碼往往會首先進行變量賦值,而后進行對象初始化。假設線程 A 先執行 getInstance() 方法,當執行完指令 2 時恰好發生了線程切換,切換到了線程 B 上;如果此時線程 B 也執行 getInstance() 方法,那么線程 B 在執行第一個判斷時會發現 instance != null,所以直接返回 instance,而此時的 instance 是沒有初始化過的,如果我們這個時候訪問 instance 的成員變量就可能觸發空指針異常。

        可見性

        所謂的可見性,即是一個線程對共享變量的修改,另外一個線程能夠立刻看到。單核時代,所有的線程都是直接操作單個 CPU 的數據,某個線程對緩存的寫對另外一個線程來說一定是可見的;譬如下圖中,如果線程 B 在線程 A 更新了變量值之后進行訪問,那么獲得的肯定是變量 V 的最新值。多核時代,每顆 CPU 都有自己的緩存,共享變量存儲在主內存。運行在某個 CPU 中的線程將共享變量讀取到自己的 CPU 緩存。在 CPU 緩存中,修改了共享對象的值,由于 CPU 并未將緩存中的數據刷回主內存,導致對共享變量的修改對于在另一個 CPU 中運行的線程而言是不可見的。這樣每個線程都會擁有一份屬于自己的共享變量的拷貝,分別存于各自對應的 CPU 緩存中。

        CPU 讀寫流程

        傳統的 MESI 協議中有兩個行為的執行成本比較大。一個是將某個 Cache Line 標記為 Invalid 狀態,另一個是當某 Cache Line 當前狀態為 Invalid 時寫入新的數據。所以 CPU 通過 Store Buffer 和 Invalidate Queue 組件來降低這類操作的延時。如圖:

        當一個核心在 Invalid 狀態進行寫入時,首先會給其它 CPU 核發送 Invalid 消息,然后把當前寫入的數據寫入到 Store Buffer 中。然后異步在某個時刻真正的寫入到 Cache Line 中。當前 CPU 核如果要讀 Cache Line 中的數據,需要先掃描 Store Buffer 之后再讀取 Cache Line(Store-Buffer Forwarding)。但是此時其它 CPU 核是看不到當前核的 Store Buffer 中的數據的,要等到 Store Buffer 中的數據被刷到了 Cache Line 之后才會觸發失效操作。而當一個 CPU 核收到 Invalid 消息時,會把消息寫入自身的 Invalidate Queue 中,隨后異步將其設為 Invalid 狀態。和 Store Buffer 不同的是,當前 CPU 核心使用 Cache 時并不掃描 Invalidate Queue 部分,所以可能會有極短時間的臟讀問題。當然這里的 Store Buffer 和 Invalidate Queue 的說法是針對一般的 SMP 架構來說的,不涉及具體架構。事實上除了 Store Buffer 和 Load Buffer,流水線為了實現并行處理,還有 Line Fill Buffer/Write Combining Buffer 等組件。

        典型案例:并發加

        可見性問題最經典的案例即是并發加操作,如下兩個線程同時在更新變量 test 的 count 屬性域的值,第一次都會將 count=0 讀到各自的 CPU 緩存里,執行完 count+=1 之后,各自 CPU 緩存里的值都是 1,同時寫入內存后,我們會發現內存中是 1,而不是我們期望的 2。之后由于各自的 CPU 緩存里都有了 count 的值,兩個線程都是基于 CPU 緩存里的 count 值來計算,所以導致最終 count 的值都是小于 20000 的。

        Thread th1 = new Thread(()->{
            test.add10K();
        });
        
        Thread th2 = new Thread(()->{
            test.add10K();
        });
        
        // 每個線程中對相同對象執行加操作
        count += 1;

        在 Java 中,如果多個線程共享一個對象,并且沒有合理的使用 volatile 聲明和線程同步,一個線程更新共享對象后,另一個線程可能無法取到對象的最新值。當一個共享變量被 volatile 修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。通過 synchronized 和 Lock 也能夠保證可見性,synchronized 和 Lock 能保證同一時刻只有一個線程獲取鎖然后執行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。

        Cache Line & False Sharing | 緩存行與偽共享

        緩存系統中是以緩存行(Cache Line)為單位存儲的,緩存行是 2 的整數冪個連續字節,一般為 32-256 個字節。最常見的緩存行大小是 64 個字節。當多線程修改互相獨立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享。

        image.png

        若兩個變量放在同一個緩存行中,在多線程情況下,可能會相互影響彼此的性能。如上圖所示,CPU1 上的線程更新了變量 X,則 CPU 上的緩存行會失效,同一行的 Y 即使沒有更新也會失效,導致 Cache 無法命中。同樣地,若 CPU2 上的線程更新了 Y,則導致 CPU1 上的緩存行又失效。如果 CPU 經常不能命中緩存,則系統的吞吐量則會下降。這就是偽共享問題。

        解決偽共享問題,可以在變量的前后都占據一定的填充位置,盡量讓變量占用一個完整的緩存行。如上圖中,CPU1 上的線程更新了 X,則 CPU2 上的 Y 則不會失效。同樣地,CPU2 上的線程更新了 Y,則 CPU1 的不會失效。參考 Java 內存布局可知,所有對象都有兩個字長的對象頭。第一個字是由 24 位哈希碼和 8 位標志位(如鎖的狀態或作為鎖對象)組成的 Mark Word。第二個字是對象所屬類的引用。如果是數組對象還需要一個額外的字來存儲數組的長度。每個對象的起始地址都對齊于 8 字節以提高性能。因此當封裝對象的時候為了高效率,對象字段聲明的順序會被重排序成下列基于字節大小的順序:

        doubles (8) 和 longs (8)
        ints (4) 和 floats (4)
        shorts (2) 和 chars (2)
        booleans (1) 和 bytes (1)
        references (4/8)
        <子類字段重復上述順序>

        一條緩存行有 64 字節, 而 Java 程序的對象頭固定占 8 字節(32 位系統)或 12 字節(64 位系統默認開啟壓縮, 不開壓縮為 16 字節)。我們只需要填 6 個無用的長整型補上 6*8=48 字節,讓不同的 VolatileLong 對象處于不同的緩存行, 就可以避免偽共享了;64 位系統超過緩存行的 64 字節也無所謂,只要保證不同線程不要操作同一緩存行就可以。這個辦法叫做補齊(Padding):

        public final static class VolatileLong
        {
            public volatile long value = 0L;
         ?  public long p1, p2, p3, p4, p5, p6; // 添加該行,錯開緩存行,避免偽共享
        }

        某些 Java 編譯器會將沒有使用到的補齊數據, 即示例代碼中的 6 個長整型在編譯時優化掉, 可以在程序中加入一些代碼防止被編譯優化。

        public static long preventFromOptimization(VolatileLong v) {
            return v.p1 + v.p2 + v.p3 + v.p4 + v.p5 + v.p6;
        }

        屏障

        編譯器優化亂序和 CPU 執行亂序的問題可以分別使用優化屏障 (Optimization Barrier)和內存屏障 (Memory Barrier)這兩個機制來解決:

        • 優化屏障 (Optimization Barrier):避免編譯器的重排序優化操作,保證編譯程序時在優化屏障之前的指令不會在優化屏障之后執行。這就保證了編譯時期的優化不會影響到實際代碼邏輯順序。
        • 內存屏障 (Memory Barrier)分為寫屏障(Store Barrier)、讀屏障(Load Barrier)和全屏障(Full Barrier),其作用有兩個:防止指令之間的重排序、保證數據的可見性。

        多處理器同時訪問共享主存,每個處理器都要對讀寫進行重新排序,一旦數據更新,就需要同步更新到主存上 (這里并不要求處理器緩存更新之后立刻更新主存)。在這種情況下,代碼和指令重排,再加上緩存延遲指令結果輸出導致共享變量被修改的順序發生了變化,使得程序的行為變得無法預測。為了解決這種不可預測的行為,處理器提供一組機器指令來確保指令的順序要求,它告訴處理器在繼續執行前提交所有尚未處理的載入和存儲指令。同樣的也可以要求編譯器不要對給定點以及周圍指令序列進行重排。這些確保順序的指令稱為內存屏障。具體的確保措施在程序語言級別的體現就是內存模型的定義。

        POSIX、C++、Java 都有各自的共享內存模型,實現上并沒有什么差異,只是在一些細節上稍有不同。這里所說的內存模型并非是指內存布 局,特指內存、Cache、CPU、寫緩沖區、寄存器以及其他的硬件和編譯器優化的交互時對讀寫指令操作提供保護手段以確保讀寫序。將這些繁雜因素可以籠統的歸納為兩個方面:重排和緩存,即上文所說的代碼重排、指令重排和 CPU Cache。簡單的說內存屏障做了兩件事情:拒絕重排,更新緩存。

        C++11 提供一組用戶 API std::memory_order 來指導處理器讀寫順序。Java 使用 happens-before 規則來屏蔽具體細節保證,指導 JVM 在指令生成的過程中穿插屏障指令。內存屏障也可以在編譯期間指示對指令或者包括周圍指令序列不進行優化,稱之為編譯器屏障,相當于輕量級內存屏障,它的工作同樣重要,因為它在編譯期指導編譯器優化。屏障的實現稍微復雜一些,我們使用一組抽象的假想指令來描述內存屏障的工作原理。使用 MB_R、MB_W、MB 來抽象處理器指令為宏:

        • MB_R 代表讀內存屏障,它保證讀取操作不會重排到該指令調用之后。
        • MB_W 代表寫內存屏障,它保證寫入操作不會重排到該指令調用之后。
        • MB 代表讀寫內存屏障,可保證之前的指令不會重排到該指令調用之后。

        這些屏障指令在單核處理器上同樣有效,因為單處理器雖不涉及多處理器間數據同步問題,但指令重排和緩存仍然影響數據的正確同步。指令重排是非常底層的且實 現效果差異非常大,尤其是不同體系架構對內存屏障的支持程度,甚至在不支持指令重排的體系架構中根本不必使用屏障指令。具體如何使用這些屏障指令是支持的 平臺、編譯器或虛擬機要實現的,我們只需要使用這些實現的 API(指的是各種并發關鍵字、鎖、以及重入性等,下節詳細介紹)。這里的目的只是為了幫助更好 的理解內存屏障的工作原理。

        內存屏障的意義重大,是確保正確并發的關鍵。通過正確的設置內存屏障可以確保指令按照我們期望的順序執行。這里需要注意的是內存屏蔽只應該作用于需要同步的指令或者還可以包含周圍指令的片段。如果用來同步所有指令,目前絕大多數處理器架構的設計就會毫無意義。

        延伸閱讀

        您可以通過以下導航來在 Gitbook 中閱讀筆者的系列文章,涵蓋了技術資料歸納、編程語言與理論、Web 與大前端、服務端開發與基礎架構、云計算與大數據、數據科學與人工智能、產品設計等多個領域:

        此外,你還可前往 xCompass 交互式地檢索、查找需要的文章/鏈接/書籍/課程;或者在 MATRIX 文章與代碼索引矩陣中查看文章與項目源代碼等更詳細的目錄導航信息。最后,你也可以關注微信公眾號:『某熊的技術之路』以獲取最新資訊。

        查看原文

        贊 9 收藏 6 評論 0

        王下邀月熊_Chevalier 發布了文章 · 2019-08-28

        軟件架構萬字漫談:業務架構、應用架構與云基礎架構

        軟件架構漫談:業務架構、應用架構與云基礎架構

        本部分節選自《軟件架構設計

        軟件開發就是把一個復雜的問題分解為一系列簡單的問題,再把一系列簡單的解決方案組合成一個復雜的解決方案。而軟件開發中最大的挑戰,就是即能夠快速高效地針對需求、環境的變化做出改變,也能夠持續提供穩定、高可用的服務。而軟件架構,就是軟件系統的骨骼與框架。

        所謂架構,見仁見智,很難有一個明確或標準的定義;但架構并非鏡花水月或陽春白雪,有系統的地方就需要架構,大到航空飛機,小到一個電商系統里面的一個功能組件,都需要設計和架構。抽象而言,架構就是對系統中的實體以及實體之間的關系所進行的抽象描述,是對物/信息的功能與形式元素之間的對應情況所做的分配,是對元素之間的關系以及元素同周邊環境之間的關系所做的定義。架構能將目標系統按某個原則進行切分,切分的原則,是要便于不同的角色進行并行工作,結構良好的創造活動要優于毫無結構的創造活動。

        軟件架構的核心價值,即是控制系統的復雜性,將核心業務邏輯和技術細節的分離與解耦。軟件架構是系統的草圖,它描述的對象是直接構成系統的抽象組件;各個組件之間的連接則明確和相對細致地描述組件之間的通信。在實現階段,這些抽象組件被細化為實際的組件,比如具體某個類或者對象。在面向對象領域中,組件之間的連接通常用接口來實現。架構師的職責是努力訓練自己的思維,用它去理解復雜的系統,通過合理的分解和抽象,理解并解析需求,創建有用的模型,確認、細化并擴展模型,管理架構;能夠進行系統分解形成整體架構,能夠正確的技術選型,能夠制定技術規格說明并有效推動實施落地。

        軟件架構分類

        在筆者的知識體系中,實際上將架構分為業務架構、應用架構、云基礎架構這幾大類,業務架構主要著眼于控制業務的復雜性,基礎架構著眼于解決分布式系統中存在的一系列問題。無論何種架構,都希望能實現系統的可變的同時保障業務的高可用。另一個層面,根據企業中職責的劃分,我們往往可以將軟件架構,及關聯的架構師劃分為以下幾類:

        • 業務架構/解決方案架構:核心是解決業務帶來的系統復雜性,了解客戶/業務方的痛點,項目定義,現有環境;梳理高階需求和非功能性需求,進行問題域劃分與領域建模等工作;溝通,方案建議,多次迭代,交付總體架構。
        • 應用架構:根據業務場景的需要,設計應用的層次結構,制定應用規范、定義接口和數據交互協議等。并盡量將應用的復雜度控制在一個可以接受的水平,從而在快速的支撐業務發展的同時,在保證系統的可用性和可維護性的同時,確保應用滿足非功能屬性要求(性能、安全、穩定性等)。
        • 數據架構:專注于構建數據中臺,統一數據定義規范,標準化數據表達,形成有效易維護的數據資產。打造統一的大數據處理平臺,包括數據可視化運營平臺、數據共享平臺、數據權限管理平臺等。
        • 中間件架構:專注于中間件系統的構建,需要解決服務器負載,分布式服務的注冊和發現,消息系統,緩存系統,分布式數據庫等問題,同時架構師要在 CAP 之間進行權衡。
        • 運維架構:負責運維系統的規劃、選型、部署上線,建立規范化的運維體系。
        • 物理架構:物理架構關注軟件元件是如何放到硬件上的,專注于基礎設施,某種軟硬件體系,甚至云平臺,包括機房搭建、網絡拓撲結構,網絡分流器、代理服務器、Web 服務器、應用服務器、報表服務器、整合服務器、存儲服務器和主機等。

        架構模式與架構風格

        軟件架構設計的一個核心問題是能否使用重復的架構模式,即能否達到架構級的軟件重用。也就是說,能否在不同的軟件系統中,使用同一架構。當我們討論軟件架構時,常常會提及軟件架構模式(Architectural Pattern)與軟件架構風格(Architectural Style)。

        軟件架構模式往往會用于具體地解決某個具體的重復的架構問題,而架構風格則是對于某個具體的架構設計方案的命名。軟件架構風格是描述某一特定應用領域中系統組織方式的慣用模式;架構風格反映了領域中眾多系統所共有的結構和語義特性,并指導如何將各個模塊和子系統有效組織成一個完整的系統。

        在筆者的系列文章中,CRUD、分層架構、六邊形架構、洋蔥架構、REST 以及 DDD,都算是架構風格;而 CQRS、EDA、UDLA、微服務等則被劃分到架構模式中。

        系統復雜性的來源與應對

        在軟件開發中,程序員往往能夠脫離現實規律的束縛,創造出天馬行空的世界,其也是最具有創造力的活動之一。編程唯一需要的是創造力思維和思維組織能力,這意味著在軟件開發過程中最大限制是理解我們正在創建的對象。隨著軟件的演進,加入更多的功能點,系統變得越來越復雜:各個模塊(Module)間存在著各種微妙的依賴關系。系統的復雜性隨著時間積累,對于程序員來說,修改系統時考慮周全所有的的相關因素變得越來越困難。這就會使軟件開放進度變緩慢,并且引入 Bug,而導致會進一步延緩開發進度,增加開發成本。在任何一個系統的生命周期中,復雜性不可避免會增加;系統越大,需要更多的人開發,管理系統復雜性的工作就越困難。

        Eric Evans 在 Domain‐Driven Design 一書中吐槽了所謂的意大利面式架構,即代碼確實做了有用的事,但很難解釋它是如何去執行的;他認為造成這種窘境的主要原因是,將領域問題的復雜度與技術細節的復雜度混合在了一起,最終導致整體復雜度的指數級增長。

        復雜性不是憑空而來,很多時候也不是刻意為之,這也就意味著復雜性的增加往往不會以我們的主觀意志為轉移。就像房間里的大象,我們無法逃避,也不能視而不見。復雜性的來源可能是:

        • 吸積與持續迭代:增量式設計意味著軟件設計永不結束,設計在系統的生命周期中持續發生,程序員要時刻考慮設計問題。增量開發也意味著持續重構。一個系統的初始設計幾乎從來都不是最好的方案。隨著經驗的增加,必然會發現更好的設計方案。
        • 交互且無擴展性設計:當吸積效應導致的大規模系統,結合了交互這個特性,會使技術系統更加復雜。一個技術系統除了作用于自身,還會與其它大量系統產生交互。比如下單購買一件商品,那么訂單系統,商品系統,支付系統,物流系統,卡券系統就會交互協作。這樣吸積的復雜性,由于交互特性的出現,會呈現幾何級數上升。
        • 不合理的業務封裝:不合理的業務封裝是一個相對寬泛的概念,其具體的表現譬如面向過程而不是對象、分層不合理等。
        • 缺乏統一語言:典型的敏捷開發的結構,流水線上的各個角色往往會專注于自己負責的環節,精細化的分工也限制了每個角色的全局視角;雖然我們經常提倡所謂的主人翁意識,但是在落地時又很難去推進。
        • 缺乏約束與規范:在團隊協作開發的背景下,缺少規范和約束會嚴重損害架構的一致性(Consistency),代碼的可維護性將急劇下降??赡芤幏对趯崿F層面就是命名、分包等不影響代碼運行的小問題,但是千里之堤,潰于蟻穴,正是這些微末的不注意導致了整體復雜性的雪崩。

        復雜性的應對永遠不會是一勞永逸,我們需要不斷地推陳出新,是動態、漸進的重塑自己對軟件系統的認識,不斷認識問題和尋找更優解的持續迭代。第一個控制復雜性的途徑是代碼簡單,意圖清晰(Obvious)。例如: 減少特殊場景的處理,或變量命名一致性都能降低系統復雜性。另一種方式就是對復雜問題的抽象然后分而治之。

        領域驅動設計

        領域驅動設計

        本部分節選自《領域驅動設計

        DDD 領域驅動設計,起源于 2004 年著名建模專家 Eric Evans 發表的他最具影響力的著名書籍:《Domain-Driven Design – Tackling Complexity in the Heart of Software》,Eric Evans 在該書中只是提供了一套原始理論,并沒有提供一套方法論,因此多年來對于 DDD 也是見仁見智。更早些時候 MartinFowler 曾經提出貧血模型與充血模型的概念,他認為我們大多數系統以 POJO 作為模型,只有普通的 getter、setter 方法,沒有真正的行為,好像缺少血液的人,在 Evans 看來,DDD 中模型都是以充血形式存在,也就是說在 DDD 中,我們設計的模型不僅包含描述業務屬性,還要包含能夠描述動作的方法,不同的是,領域中一些概念不能用在模型對象,如倉儲、工廠、服務等,如強加于模型中,將破壞模型的定義。

        領域驅動設計架構

        領域驅動設計的戰略核心即是將問題域與應用架構相剝離,將業務語義顯現化,把原先晦澀難懂的業務算法邏輯,通過領域對象(Domain Object),統一語言(Ubiquitous Language)轉化為領域概念清晰的顯性化表達出來。

        • 統一語言,軟件的開發人員/使用人員都使用同一套語言,即對某個概念,名詞的認知是統一的,建立清晰的業務模型,形成統一的業務語義。將模型作為語言的支柱。確保團隊在內部的所有交流中,代碼中,畫圖,寫東西,特別是講話的時候都要使用這種語言。例如賬號,轉賬,透支策略,這些都是非常重要的領域概念,如果這些命名都和我們日常討論以及 PRD 中的描述保持一致,將會極大提升代碼的可讀性,減少認知成本。。比如不再會有人在會議中對“工單”、“審核單”、“表單”而反復確認含義了,DDD 的模型建立不會被 DB 所綁架。
        • 面向領域,業務語義顯性化,以領域去思考問題,而不是模塊。將隱式的業務邏輯從一推 if-else 里面抽取出來,用通用語言去命名、去寫代碼、去擴展,讓其變成顯示概念;很多重要的業務概念,按照事務腳本的寫法,其含義完全淹沒在代碼邏輯中沒有突顯出來。
        • 職責劃分,根據實際業務合理劃分模型,模型之間依賴結構和邊界更加清晰,避免了混亂的依賴關系,進而增加可讀性、可維護性;單一職責,模型只關注自身的本職工作,避免“越權”而導致混亂的調用關系。通過建模,更好的表達現實世界中的復雜業務,隨著時間的發展,不斷增加系統對實際業務的沉淀,也將更好的通過清晰的代碼描述業務邏輯,模型的內聚增加了系統的高度模塊化,提升代碼的可重用性,對比傳統三層模式中,很有可能大量重復的功能散落在各個 Service 內部。

        微服務與云原生架構

        本部分節選自《微服務與云原生

        服務衍化

        單體分層架構

        在 Web 應用程序發展的早期,大部分工程是將所有的服務端功能模塊打包到單個巨石型(Monolith)應用中,譬如很多企業的 Java 應用程序打包為 war 包,最終會形成如下的架構:

        巨石型應用易于搭建開發環境、易于測試、易于部署;其缺陷也非常明顯,無法進行局部改動與部署,編譯時間過長,回歸測試周期過長,開發效率降低等。集中式架構分為標準的三層:數據訪問層、服務層和 Web 層。

        在 Web2.0 時代剛剛流行的時候,互聯網應用與企業級應用并沒有本質的區別,集中式架構分為標準的三層:數據訪問層、服務層和 Web 層。

        • 數據訪問層用于定義數據訪問接口,實現對真實數據庫的訪問;
        • 服務層用于對應用業務邏輯進行處理;
        • Web 層用于處理異常、邏輯跳轉控制、頁面渲染模板等。

        SOA 面向服務架構

        SOA(Service-Oriented Architecture) 面向服務架構,是在互聯網應用規模迅速增長,集中式架構已無法做到無限制地提升系統的吞吐量的背景下,產生的涉及模塊化開發、分布式擴展部署等相對寬泛的概念。

        SOA 是一個組件模型,它將應用程序的不同功能單元(稱為服務)通過這些服務之間定義良好的接口和契約聯系起來。SOA 中的接口獨立于實現服務的硬件平臺、操作系統和編程語言,采用中立的方式進行定義。這使得構建在各種各樣的系統中的服務可以以一種統一和通用的方式進行交互。面向服務架構,它可以根據需求通過網絡對松散耦合的粗粒度應用組件進行分布式部署、組合和使用。服務層是 SOA 的基礎,可以直接被應用調用,從而有效控制系統中與軟件代理交互的人為依賴性。

        實施 SOA 的關鍵目標是實現企業 IT 資產的最大化作用。要實現這一目標,就要在實施 SOA 的過程中牢記以下特征:可從企業外部訪問、隨時可用、粗粒度的服務接口分級、松散耦合、可重用的服務、服務接口設計管理、標準化的服務接口、支持各種消息模式、精確定義的服務契約。

        服務消費者(Service Consumer)可以通過發送消息來調用服務,這些消息由一個服務總線(Service Bus)轉換后發送給適當的服務實現。這種服務架構可以提供一個業務規則引(Business Rules Engine),該引擎容許業務規則被合并在一個服務里或多個服務里。這種架構也提供了一個服務管理基礎(Service Management Infrastructure),用來管理服務,類似審核,列表(billing),日志等功能。此外,該架構給企業提供了靈活的業務流程,更好地處理控制請求(Regulatory Requirement),例如 Sarbanes Oxley(SOX),并且可以在不影響其他服務的情況下更改某項服務。

        由于分布式系統十分復雜,因此產生了大量的用于簡化分布式系統開發的分布式中間件和分布式數據庫,服務化的架構設計理念也被越來越多的公司所認同。如下是 Dubbo 官方文檔公布了一張有關 SOA 系統演化過程的圖片:

        MSA 微服務架構

        微服務(Microservices Architecture Pattern)由 Martin Fowler 在 2014 年提出的,是希望將某個單一的單體應用,轉化為多個可以獨立運行、獨立開發、獨立部署、獨立維護的服務或者應用的聚合,從而滿足業務快速變化及分布式多團隊并行開發的需求。如康威定律(Conway’s Law)所言,任何組織在設計一套系統(廣義概念)時,所交付的設計方案在結構上都與該組織的通信結構保持一致,微服務與微前端不僅僅是技術架構的變化,還包含了組織方式、溝通方式的變化。

        對于微服務,不同背景的人也有不同的見解,對于熟悉 SOA 的開發者,微服務也可以認為是去除了 ESB 的 SOA 的一種實現方案;ESB 是 SOA 架構中的中心總線,設計圖形應該是星形的,而微服務是去中心化的分布式軟件架構。SOA 更多強調重用,而微服務偏向于重寫。SOA 偏向水平服務,微服務偏向垂直服務;SOA 偏向自上而下的設計,微服務偏向自下而上的設計。

        微服務與微前端原理和軟件工程,面向對象設計中的原理同樣相通,都是遵循單一職責(Single Responsibility)、關注分離(Separation of Concerns)、模塊化(Modularity)與分而治之(Divide & Conquer)等基本的原則。從巨石型應用到微服務的衍化也并非一蹴而就,如下圖也演示了簡單的漸進式替代過程:

        Cloud Native 云原生架構

        云原生是通過構建團隊、文化和技術,利用自動化和架構來管理系統的復雜性和解放生產力。
        — Joe Beda,Heotio CTO,聯合創始人

        Pivotal 是云原生應用的提出者,并推出了 Pivotal Cloud Foundry 云原生應用平臺和 Spring 開源 Java 開發框架,成為云原生應用架構中先驅者和探路者。早在 2015 年 Pivotal 公司的 Matt Stine 寫了一本叫做遷移到云原生應用架構的小冊子,其中探討了云原生應用架構的幾個主要特征:符合 12 Factors 應用、面向微服務架構、自服務敏捷架構、基于 API 的協作以及抗脆弱性。2015 年 Google 主導成立了云原生計算基金會(CNCF),起初 CNCF 對云原生(Cloud Native)的定義包含以下三個方面:應用容器化、面向微服務架構、應用支持容器的編排調度。

        云原生應用程序簡單地定義為從頭開始為云計算架構而構建應用程序;這意味著,如果我們將應用程序設計為預期將部署在分布式、可擴展的基礎架構上,我們的應用程序就是云原生的。隨著公共云將承載越來越多的算力,未來云計算將是主流的 IT 能力交付方式,CNCF 也對云原生進行了重新定義:云原生技術有利于各組織在公有云、私有云和混合云等新型動態環境中,構建和運行可彈性擴展的應用;云原生的代表技術包括容器、服務網格、微服務、不可變基礎設施和聲明式 API。

        • Codeless 對應的是服務開發,實現了源代碼托管,你只需要關注你的代碼實現,而不需要關心你的代碼在哪,因為在整個開發過程中你都不會感受到代碼庫和代碼分支的存在。
        • Applicationless 對應的是服務發布,在服務化框架下,你的服務發布不再需要申請應用,也不需要關注你的應用在哪。
        • Serverless 對應的則是服務運維,有了 Serverless 化能力,你不再需要關注你的機器資源,Servlerless 會幫你搞定機器資源的彈性擴縮容

        這些技術組合搭配,能夠構建容錯性好、易于管理和便于觀察的松耦合系統;再結合可靠的自動化手段,云原生技術能夠使工程師輕松地對系統作出頻繁和可預測的重大變更。由此可見,云原生是保障系統能力靈動性地有效抓手;云原生技術有利于各組織在公有云、私有云和混合云等新型動態環境中,構建和運行可彈性擴展的應用。微服務架構非常適合云原生應用程序;但是,云原生同樣存在著一定的限制,如果你的云原生應用程序部署在 AWS 等公有云上,則云原生 API 不是跨云平臺的。

        云原生應用的關鍵屬性包括了:使用輕量級的容器打包、使用最合適的語言和框架開發、以松耦合的微服務方式設計、以 API 為中心的交互和協作、無狀態和有狀態服務在架構上界限清晰、不依賴于底層操作系統和服務器、部署在自服務、彈性的云基礎設施上、通過敏捷的 DevOps 流程管理、自動化能力、通過定義和策略驅動的資源分配。云原生是分布式應用當下重要的發展路徑,其終態應當是 Distributionless,所有與分布式相關的問題由云平臺解,分布式應用的開發會跟傳統應用的開發一樣方便,甚至更加便捷。

        云基礎架構

        本部分節選自《分布式基礎架構之虛擬化與編排

        應用基礎架構變遷

        虛擬機

        虛擬機由某些特定的硬件和內核虛擬化組成,運行客戶操作系統。稱為管理程序的軟件創建虛擬化硬件,其可以包括虛擬磁盤,虛擬網絡接口,虛擬 CPU 等。虛擬機還包括可以與此虛擬硬件通信的賓客內核。管理程序可以托管,這意味著它是一些在主機操作系統(MacOS)上運行的軟件,如示例中所示。它也可以是裸機,直接在機器硬件上運行(替換你的操作系統)。無論哪種方式,管理程序方法都被認為是重量級的,因為它需要虛擬化多個部分(如果不是全部硬件和內核)。

        VM 需要硬件虛擬化才能實現機器級隔離,而容器則只需要在同一操作系統內進行隔離操作。 隨著隔離空間數量的增加,開銷差異變得非常明顯。

        容器

        在過去幾年里,云平臺發展迅速,但其中困擾運維工程師最多的,是需要為各種迥異的開發語言安裝相應的運行時環境。雖然自動化運維工具可以降低環境搭建的復雜度,但仍然不能從根本上解決環境的問題。

        Docker 的出現成為了軟件開發行業新的分水嶺,容器技術的成熟也標志著技術新紀元的開啟。Docker 提供了讓開發工程師可以將應用和依賴封裝到一個可移植的容器中的能力,這項舉措使得 Docker 大有席卷整個軟件行業并且進而改變行業游戲規則的趨勢,這像極了當年智能手機剛出現時的場景——改變了整個手機行業的游戲規則。Docker 通過集裝箱式的封裝方式,讓開發工程師和運維工程師都能夠以 Docker 所提供的鏡像分發的標準化方式發布應用,使得異構語言不再是捆綁團隊的枷鎖。

        容器是包含應用程序代碼,配置和依賴關系的軟件包,可提供運營效率和生產力。容器為我們提供了可預測的,可重復的和不可變的運行預期,容器的興起是 DevOps 即服務的一個巨大推動因素,可以克服當今面臨的最大安全障礙。容器化通過在操作系統級別進行虛擬化來使應用程序可移植,從而創建基于內核的隔離的封裝系統。容器化的應用程序可以放在任何地方,無需依賴項運行或需要整個 VM,從而消除了依賴關系。

        作為獨立的單元,容器能夠在任何主機操作系統,CentOS,Ubuntu,MacOS,甚至是像 Windows 這樣的非 UNIX 系統中運行。容器還充當標準化的工作或計算單元。一個常見的范例是每個容器運行單個 Web 服務器,數據庫的單個分片或單個 Spark 工作程序等,只需要擴展容器的數量就能夠便捷地擴展應用。每個容器都有一個固定的資源配置(CPU,RAM,線程數等),并且擴展應用程序需要只擴展容器的數量而不是單個資源原語。當應用程序需要按比例放大或縮小時,這為工程師提供了更容易的抽象。容器也是實現微服務架構的一個很好的工具,每個微服務只是一組協作容器。例如,可以使用單個主容器和多個從容器來實現 Redis 微服務。

        Kubernetes 與編排

        隨著虛擬化技術的成熟和分布式架構的普及,用來部署、管理和運行應用的云平臺被越來越多地提及。IaaS、PaaS 和 SaaS 是云計算的三種基本服務類型,分別表示關注硬件基礎設施的基礎設施即服務、關注軟件和中間件平臺的平臺即服務,以及關注業務應用的軟件即服務。容器的出現,使原有的基于虛擬機的云主機應用,徹底轉變為更加靈活和輕量的容器與編排調度的云平臺應用。

        然而容器單元越來越散落使得管理成本逐漸上升,大家對容器編排工具的需求前所未有的強烈,Kubernetes、Mesos、Swarm 等為云原生應用提供了強有力的編排和調度能力,它們是云平臺上的分布式操作系統。容器編排是通??梢圆渴鸲鄠€容器以通過自動化實現應用程序的過程。像 Kubernetes 和 Docker Swarm 這樣的容器管理和容器編排引擎,使用戶能夠指導容器部署并自動執行更新,運行狀況監視和故障轉移過程。

        Kubernetes 是目前世界范圍內關注度最高的開源項目,它是一個出色的容器編排系統,用于提供一站式服務。Kubernetes 出身于互聯網行業巨頭 Google,它借鑒了由上百位工程師花費十多年時間打造的 Borg 系統的理念,安裝極其簡易,網絡層對接方式十分靈活。Kubernetes 和 Mesos 的出色表現給行業中各類工程師的工作模式帶來了顛覆性的改變。他們再也不用關注每一臺服務器,當服務器出現問題時,只要將其換掉即可。業務開發工程師不必再過分關注非功能需求,只需專注自己的業務領域即可。而中間件開發工程師則需要開發出健壯的云原生中間件,用來連接業務應用與云平臺。

        Kubernetes、Service Mesh 和 Serverless 三者共同演繹不同層次的封裝和向上屏蔽下面的細節。Kubernetes 引入了不同的設計模式,實現對各種云資源全新、有效和優雅的抽象和管理模式,讓集群的管理和應用發布變成了件相當輕松且不易出錯的事。被廣泛采用的微服務軟件架構將分布式應用的各種復雜度遷移到了服務之間,如何通過全局一致、體系化、規范化和無侵入的手段進行治理就變成了微服務軟件架構下至關重要的內容。Kubernetes 細化的應用程序的分解粒度,同時將服務發現、配置管理、負載均衡和健康檢查等作為基礎設施的功能,簡化了應用程序的開發。而 Kubernetes 這種聲明式配置尤其適合 CI/CD 流程,況且現在還有如 Helm、Draft、Spinnaker、Skaffold 等開源工具可以幫助我們發布 Kuberentes 應用。

        Service Mesh 通過將各服務所共用和與環境相關的內容剝離到部署于每個服務邊上的 Sidecar 進程而輕松地做到了。這一剝離動作使得服務與平臺能充分解耦而方便各自演進與發展,也使得服務變輕而有助于改善服務啟停的及時性。Service Mesh 因為將那些服務治理相關的邏輯剝離到了 Sidecar 中且作為獨立進程,所以 Sidecar 所實現的功能天然地支持多語言,為上面的服務采用多語言開發創造了更為有利的條件。通過 Service Mesh 對整個網絡的服務流量進行技術收口,讓異地多活這樣涉及流量調度的系統工程實現起來更加優雅、簡潔與有效,也能更加方便地實現服務版本升級時的灰度、回滾而改善安全生產質量。由于技術收口,給服務流量的治理和演進、排錯、日志采集的經濟性等疑難問題創造了新的發展空間。

        延伸閱讀

        您可以通過以下導航來在 Gitbook 中閱讀筆者的系列文章,涵蓋了技術資料歸納、編程語言與理論、Web 與大前端、服務端開發與基礎架構、云計算與大數據、數據科學與人工智能、產品設計等多個領域:

        此外,你還可前往 xCompass 交互式地檢索、查找需要的文章/鏈接/書籍/課程;或者在 MATRIX 文章與代碼索引矩陣中查看文章與項目源代碼等更詳細的目錄導航信息。最后,你也可以關注微信公眾號:『某熊的技術之路』以獲取最新資訊。

        查看原文

        贊 16 收藏 12 評論 1

        王下邀月熊_Chevalier 分享了頭條 · 2019-08-25

        歷經二十載風云變幻,JavaScript 也終于成為了一流的語言,在前端開發、服務端開發、嵌入式開發乃至于機器學習與數據挖掘、操作系統開發等各個領域都有不俗的表現。而在這不斷的變化之后,也有很多語法或者模式成了明日黃花;本系列文章即是希望為讀者總結與呈現出最...

        贊 0 收藏 3 評論 0

        王下邀月熊_Chevalier 分享了頭條 · 2019-08-23

        程序員的自我學習與進階指南,Web/(大)前端/終端工程師、服務端架構工程師、測試/運維/安全工程師等質量保障、可用性保障相關的工程師、大數據/云計算/虛擬化工程師、算法工程師、產品經理等等某個或者某幾個角色。

        贊 1 收藏 3 評論 1

        王下邀月熊_Chevalier 發布了文章 · 2019-08-22

        十年鏟碼,八大體系超千篇數百萬字技術筆記系列匯總(GitBook 悅享版)

        十年鏟碼,八大體系超千篇數百萬字技術筆記系列匯總(GitBook 悅享版)

        十年鏟碼兩茫茫,縱思量,卻易忘;不覺筆者步入程序員已有十年。十年里一直浮沉沉沉沉,愈深入,愈覺得世界之大,宇宙浩瀚。十年之期,正巧筆者從阿里離開,重回 3D 打印制造業的懷抱,希望能依托于設備優勢逐步、真正構建分布式制造網絡。十年里,筆者從最初的自我摸索、摔爬滾打,到進入華為、阿里這樣的大廠,不斷地完善對于完整的技術世界觀的理解,并且嘗試對于自我產品意識的啟蒙。

        十年里,學的也很多,忘的也很多,如今發現自己還是在許多技術的門檻處徘徊。從小愚鈍,只能以筆代記,以勤補茁。洋洋灑灑百萬字,數十萬行垃圾代碼,也是筆者存在過的痕跡。同時也是借此文對過去幾年里的 TL(@Henry https://github.com/mjolnirjs)和小伙伴表示感謝,讓我不斷地體悟什么是以客戶/用戶為中心,將技術產品化,慢慢褪去過去的純技術導向的思維。人生有幸,與有趣的人,做有趣的事,下一個三年,依舊江湖再見。

        文章指北

        在這個知識大爆發的時代,筆者所見許多人面臨的問題不再是知識的匱乏,也不是缺乏學習的動力、毅力或激情,而是在浩湯洪流之中迷失了方向,也無足夠的承載知識的容器,終致學的越多,忘的越多,竹籃打水一場空。某熊的技術之路系列文章/書籍/視頻/代碼即是筆者為自己構建的知識圍欄,也是筆者蹣跚行進于這條路上的點滴印記,包含了筆者作為程序員的技術視野、知識管理與職業規劃,致力于提升開發者的學習效率與實際研發效能。

        目前筆者已有數十倉庫千篇文章百萬字,你可以在某熊的技術之路指北 ?查閱詳細的導航??梢郧巴?Awesome Lists 或者 Awesome-CS-Books 查閱分門別類的資料與書籍,抑或前往 Awesome MindMaps 查閱完整的知識體系、脈絡與要點的腦圖,或者 Awesome RoadMaps 了解不同技術領域的進階指南。最后,你還可以前往 Awesome Interviews 做最后面試前的準備,或者用常見的技術面試題去檢驗自己學習的結果。

        您可以通過以下導航來在 Gitbook 中閱讀筆者的系列文章,涵蓋了技術資料歸納、編程語言與理論、Web 與大前端、服務端開發與基礎架構、云計算與大數據、數據科學與人工智能、產品設計等多個領域:

        此外,你還可前往 xCompass 交互式地檢索、查找需要的文章/鏈接/書籍/課程;或者在 MATRIX 文章與代碼索引矩陣中查看文章與項目源代碼等更詳細的目錄導航信息。最后,你也可以關注微信公眾號:『某熊的技術之路』以獲取最新資訊。

        Awesome MindMaps

        Awesome ITCS MindMaps 是筆者所有系列文章、代碼、項目中的知識系統的腦圖形式的總結:

        • 知識脈絡

        知識脈絡

        • 知識要點

        知識要點

        Awesome RoadMaps

        當我們站在技術之路的原點,未來可能充滿了迷茫,也存在著很多不同的可能;我們可能成為 Web/(大)前端/終端工程師、服務端架構工程師、測試/運維/安全工程師等質量保障、可用性保障相關的工程師、大數據/云計算/虛擬化工程師、算法工程師、產品經理等等某個或者某幾個角色。在這個知識爆炸與終身學習/碎片化學習為主的時代,我們面臨的問題之一就是如何進行有效學習,不僅能有效平衡廣度與深度,并且能真正的積淀下來,提升自己的研發效能。于筆者而言,常常郁結于胸的就是以下三個問題:

        • 應該學習什么?這是怎樣的一個技術世界?存在著怎樣的高峰與路徑?
        • 如何克服遺忘帶來的無效學習?
        • 如何不再碎片化地學習?

        究其根本,也就是需要拓展自己的知識廣度,精進自己的知識深度,錘煉自己的編程能力。所謂知識廣度,即是為實際問題選擇合適的解決方案的能力,廣義來說也是眼界與格局的表現。它并不拘泥于某個技術方向或者行業領域,而需要對傳統/流行的各類語言、工具、框架、庫、服務等有一定的認識;能夠明晰各個方案的優劣,并在較高的層次(High Level)描述相關原理。知識廣度的拓展與保持需要建立在龐大的閱讀量與知識沉淀能力上。在能力模型一節中,我們從業務、團隊、技術這三個維度,對技術人員的五個層次進行了梳理:

        Awesome Lists 就為我們準備了精而全的技術開發學習與實踐資料索引,去蕪存菁,去重留一;譬如其中的 Awesome WebSites 一文就為我們推薦了值得閱讀的資訊、博客等站點列表。知識廣度的拓展也并非一蹴而就之事,需得循序漸進,從初窺門徑,到登堂入室,最后融會貫通,當我們感覺亂花漸欲迷人眼,太多的碎片化知識反而使自己迷失方向之際,就可以前往 Awesome CS Books Warehouse,去深入地閱讀學習各個領域的精選書籍、課程等系統化的內容。

        俗話說,好記性不如爛筆頭,當我們閱讀的多了,自然也要開始記錄;而筆者認為記錄的開始就要有自己的知識體系。在自己的知識體系下隨看隨記、定期整理。唯有建立符合自己認知方式的知識圖譜,才能有效地沉淀知識,明晰知識邊界并進行不斷地探索。上車伊始,筆者即致力于構建自己的 Awesome MindMaps,提供了軟件工程通用、前端、后端、DevOps、測試、架構師、人工智能工程師等多領域的知識圖譜、學習成長路線與面試必備內容,并在數年來不斷維護與刷新。

        筆者目前選擇的是以 MarkDown 格式記錄,并且將所有的筆記存放于 Github-文檔札記以 Git 方式進行版本管理;編輯器是直接使用的 VSCode,移動端編輯的話也是用的 GitGo/WorkCopy 這樣的 Git 應用。這些筆記即是筆者自身技術視野與認知的外化,也類比于外設之于內存,在需要的時候分頁加載到腦海中使用,以應對這知識爆炸的時代。其中的典型代表,Awesome CheatSheets,對于日常開發中用到的相關知識的備忘錄/清單進行總結, 適合快速掌握或者回顧某個語言/框架/工具的語法或使用要點。Tech Road, 我的技術之路是對于筆者多年學習與認知變遷的總結。

        先賢有云,知行合一,知是行之始,行是知之成,Linus Torvalds 也曾提到: 'Talk is cheap. Show me the code.',在閱讀與筆記之后,就是要開始實踐編碼。所謂編程能力,并不僅僅是編寫代碼(Write Code)的能力,而是包含了閱讀、編寫、理解、重構、抽象等多個方面,是所謂的代碼管理/掌控。其外在表現之一即是能夠隨時隨地用合適的語言無阻塞地實現某些功能需求,對于常見的語法,接口,算法,設計模式等能夠做到心隨意動,信手拈來。編程能力是提升研發效能的重要保障,于筆者而言也是畢生應該追求的目標與愛好之一。筆者的編程能力較弱,日常開發,特別是在多語言多框架并用的場景下,往往會需要不斷地中斷,查找以繼續工作,也是令我頗為苦惱。Coding Snippets 系列倉庫,即是對筆者在編程語言、前后端編碼過程中常用的模板、模式等代碼片、工具庫的匯聚。

        前文重在討論如何拓寬技術視野、追尋技術的廣度,但是需要銘記的是,技術深度才是技術廣度的基石,正如中國自古以來常用道術之辯,知其然,也要知其所以然;亦如 Richard Feynman 所述:”What I cannot create, I do not understand.”。所謂知識深度,即是能夠對某個方面做到深入了解,并且達到融會貫通,洞若觀火,可以隨心所欲地加以擴展、優化、創新等改造或變換。這方面則更加的見仁見智,不同的領域與方向對于深度的定義與挖掘方向也是千差萬別。Reinvent Wheels 系列倉庫即是筆者在前后端到基礎架構中重造的輪子的匯總。

        我們需要自己去從零開始造些輪子,才能深刻理解使用的框架/庫/平臺的內部原理,才能在碰到故障時快速地修復;在下文的幾乎每個章節中,我們都會提到某些筆者自造的輪子。筆者目前將所有的文檔歸類到了不同的系列中,每個系列又分為 篇-章-節-小節 這樣的層次結構。

        查看原文

        贊 44 收藏 28 評論 3

        王下邀月熊_Chevalier 發布了文章 · 2019-08-21

        K8s 實戰之概念、集群部署與服務配置

        K8s 實戰之概念、集群部署與服務配置

        本文是對于 Kubernetes 實戰系列文章的提煉。

        Kubernetes [koo-ber-nay'-tice] 是 Google 基于 Borg 開源的容器編排調度引擎,其支持多種底層容器虛擬化技術,具有完備的功能用于支撐分布式系統以及微服務架構,同時具備超強的橫向擴容能力;它提供了自動化容器的部署和復制,隨時擴展或收縮容器規模,將容器組織成組,并且提供容器間的負載均衡,提供容器彈性等特性。作為 CNCF(Cloud Native Computing Foundation)最重要的組件之一,可謂云操作系統;它的目標不僅僅是一個編排系統,而是提供一個規范,可以讓你來描述集群的架構,定義服務的最終狀態。

        設計理念

        與一般的 PaaS 平臺相比,K8s 也是支持服務部署、自動運維、資源調度、擴縮容、自我修復、負載均衡,服務發現等功能,而其獨特之處就是其對于基礎設施層進行了較好的能力抽象。K8s 并沒有處理具體的存儲、網絡這些差異性極大的部分,而是做云無關,開始實現各類 interface,做各種抽象。比如容器運行時接口(CRI)、容器網絡接口(CNI)、容器存儲接口(CSI)。這些接口讓 Kubernetes 變得無比開放,而其本身則可以專注于內部部署及容器調度。

        Kubernetes 有類似于 Linux 的分層架構,如下圖所示:

        • 基礎設施層:包括容器運行時、網絡、存儲等。
        • 核心層:Kubernetes 最核心的功能,對外提供 API 構建高層的應用,對內提供插件式應用執行環境。
        • 應用層:部署(無狀態、有狀態應用、Job 等)和路由(服務發現、負載均衡等)
        • 管理層:系統度量(如基礎設施、容器和網絡的度量),自動化(如自動擴展、動態 Provision 等)以及策略管理(RBAC、Quota、PSP、NetworkPolicy 等)
        • 接口層:kubectl 命令行工具、客戶端 SDK 以及集群聯邦
        • 生態系統:在接口層之上的龐大容器集群管理調度的生態系統,可以劃分為兩個范疇:日志、監控、配置管理、CI、CD、Workflow、FaaS、OTS 應用、ChatOps 等外部生態以及 CRI、CNI、CSI、鏡像倉庫、Cloud Provider、集群自身的配置和管理等內部生態。

        Kubernetes 中所有的配置都是通過 API 對象的 spec 去設置的,也就是用戶通過配置系統的理想狀態來改變系統,這是 Kubernetes 重要設計理念之一,即所有的操作都是聲明式(Declarative)的而不是命令式(Imperative)的。聲明式操作在分布式系統中的好處是穩定,不怕丟操作或運行多次,例如設置副本數為 3 的操作運行多次也還是一個結果,而給副本數加 1 的操作就不是聲明式的,運行多次結果就錯了。

        相對于命令式操作,聲明式操作會更穩定且更容易被用戶接受,因為該 API 中隱含了用戶想要操作的目標對象,而這些對象剛好都是名詞性質的,比如 Service、Deployment、PV 等;且聲明式的配置文件更貼近“人類語言”,比如 YAML、JSON。聲明式的設計理念有助于實現控制閉環,持續觀測、校正,最終將運行狀態達到用戶期望的狀態;感知用戶的行為并執行。比如修改 Pod 數量,應用升級/回滾等等。調度器是核心,但它只是負責從集群節點中選擇合適的 Node 來運行 Pods,顯然讓調度器來實現上訴的功能不太合適,而需要有專門的控制器組件來實現。

        組件與對象

        Kubernetes 的各種功能都離不開它定義的資源對象,這些對象都可以通過 API 被提交到集群的 Etcd 中。API 的定義和實現都符合 HTTP REST 的格式,用戶可以通過標準的 HTTP 動詞(POST、PUT、GET、DELETE)來完成對相關資源對象的增刪改查。常用的資源對象,比如 Deployment、DaemonSet、Job、PV 等。API 的抽象也意在這部分資源對象的定義。Kubernetes 有新的功能實現,一般會創建新的資源對象,而功能也依托于該對象進行實現。

        類別名稱
        資源對象Pod、ReplicaSet、ReplicationController、Deployment、StatefulSet、DaemonSet、Job、CronJob、HorizontalPodAutoscaling、Node、Namespace、Service、Ingress、Label、CustomResourceDefinition
        存儲對象Volume、PersistentVolume、Secret、ConfigMap
        策略對象SecurityContext、ResourceQuota、LimitRange
        身份對象ServiceAccount、Role、ClusterRole

        這里我們選擇幾個關鍵對象進行介紹。

        部署(Deployment)

        部署表示用戶對 Kubernetes 集群的一次更新操作。部署是一個比 RS 應用模式更廣的 API 對象,可以是創建一個新的服務,更新一個新的服務,也可以是滾動升級一個服務。滾動升級一個服務,實際是創建一個新的 RS,然后逐漸將新 RS 中副本數增加到理想狀態,將舊 RS 中的副本數減小到 0 的復合操作;這樣一個復合操作用一個 RS 是不太好描述的,所以用一個更通用的 Deployment 來描述。以 Kubernetes 的發展方向,未來對所有長期伺服型的的業務的管理,都會通過 Deployment 來管理。

        服務(Service)

        RC、RS 和 Deployment 只是保證了支撐服務的微服務 Pod 的數量,但是沒有解決如何訪問這些服務的問題。如果說 Deployment 是負責保證 Pod 組的正常運行,那么 Service 就是用于保證以合理的網絡來連接到該組 Pod。

        一個 Pod 只是一個運行服務的實例,隨時可能在一個節點上停止,在另一個節點以一個新的 IP 啟動一個新的 Pod,因此不能以確定的 IP 和端口號提供服務。要穩定地提供服務需要服務發現和負載均衡能力。服務發現完成的工作,是針對客戶端訪問的服務,找到對應的的后端服務實例。在 K8 集群中,客戶端需要訪問的服務就是 Service 對象。每個 Service 會對應一個集群內部有效的虛擬 IP,集群內部通過虛擬 IP 訪問一個服務。Service 有三種類型:

        • ClusterIP:默認類型,自動分配一個僅 Cluster 內部可以訪問的虛擬 IP。
        • NodePort:在 ClusterIP 基礎上為 Service 在每臺機器上綁定一個端口,這樣就可以通過 <NodeIP>:NodePort 來訪問該服務。
        • LoadBalancer:在 NodePort 的基礎上,借助 Cloud Provider 創建一個外部的負載均衡器,并將請求轉發到 <NodeIP>:NodePort。

        在 Kubernetes 集群中微服務的負載均衡是由 Kube-proxy 實現的。Kube-proxy 是 Kubernetes 集群內部的負載均衡器。它是一個分布式代理服務器,在 Kubernetes 的每個節點上都有一個;這一設計體現了它的伸縮性優勢,需要訪問服務的節點越多,提供負載均衡能力的 Kube-proxy 就越多,高可用節點也隨之增多。與之相比,我們平時在服務器端做個反向代理做負載均衡,還要進一步解決反向代理的負載均衡和高可用問題。

        集群部署

        Kubernetes 實戰系列中我們介紹了 Docker 本地搭建,基于 Ubuntu 手動搭建集群以及基于 Rancher 快速搭建集群等方式。使用 Rancher 可以自動和可視化的完成 Kubernetes 集群的安裝工作,省去的繁瑣的人工安裝過程,然您快速投入的業務開發中。

        $ docker run -d --restart=unless-stopped -p 80:80 -p 443:443 rancher/rancher

        先在 Master 節點安裝 Rancher server、control、etcd 和 worker。選擇網絡組件為 Flannel,同時在自定義主機運行命令中選擇主機角色、填寫主機的內網和外網 IP。

        我們需要將腳本復制到對應的機器上運行,然后 Rancher 將自動創建 Kubernetes 集群,并默認在 80 端口運行 Web Server。添加 Node 節點時只需要在 Rancher 的 Web 界面上找到您剛安裝的集群并選擇【編輯集群】并選擇節點角色為 Worker 即可增加一臺 Kubenretes 集群節點。

        Helm

        Helm 是由 Deis 發起的一個開源工具,有助于簡化部署和管理 Kubernetes 應用。在本章的實踐中,我們也會使用 Helm 來簡化很多應用的安裝操作。

        在 Linux 中可以使用 Snap 安裝 Heml:

        $ sudo snap install helm --classic
        
        # 通過鍵入如下命令,在 Kubernetes 群集上安裝 Tiller
        $ helm init --upgrade

        在缺省配置下, Helm 會利用 "gcr.io/kubernetes-helm/tiller" 鏡像在 Kubernetes 集群上安裝配置 Tiller;并且利用 "https://kubernetes-charts.storage.googleapis.com" 作為缺省的 stable repository 的地址。由于在國內可能無法訪問 "gcr.io", "storage.googleapis.com" 等域名,阿里云容器服務為此提供了鏡像站點。請執行如下命令利用阿里云的鏡像來配置 Helm:

        $ helm init --upgrade -i registry.cn-hangzhou.aliyuncs.com/google_containers/tiller:v2.5.1 --stable-repo-url https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
        
        # 刪除默認的源
        $ helm repo remove stable
        
        # 增加新的國內鏡像源
        $ helm repo add stable https://burdenbear.github.io/kube-charts-mirror/
        $ helm repo add stable https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
        
        # 查看 Helm 源添加情況
        $ helm repo list

        Helm 的常見命令如下:

        # 查看在存儲庫中可用的所有 Helm Charts
        $ helm search
        
        # 更新 Charts 列表以獲取最新版本
        $ helm repo update
        
        # 查看某個 Chart 的變量
        $ helm inspect values stable/mysql
        
        # 查看在群集上安裝的 Charts 列表
        $ helm list
        
        # 刪除某個 Charts 的部署
        $ helm del --purge wordpress-test
        
        # 為 Tiller 部署添加授權
        $ kubectl create serviceaccount --namespace kube-system tiller
        $ kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
        $ kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'

        kubectl

        信息檢索

        get 命令用于獲取集群的一個或一些 resource 信息。使用--help 查看詳細信息。kubectl 的幫助信息、示例相當詳細,而且簡單易懂。建議大家習慣使用幫助信息。kubectl 可以列出集群所有 resource 的詳細。resource 包括集群節點、運行的 pod,ReplicationController,service 等。

        $ kubectl get [(-o|--output=)json|yaml|wide|go-template=...|go-template-file=...|jsonpath=...|jsonpath-file=...] (TYPE [NAME | -l label] | TYPE/NAME ...) [flags] [flags]

        運行與管理

        kubectl run 和 docker run 一樣,它能將一個鏡像運行起來,我們使用 kubectl run 來將一個 sonarqube 的鏡像啟動起來。

        $ kubectl run sonarqube --image=sonarqube:5.6.5 --replicas=1 --port=9000
        
        deployment "sonarqube" created
        
        # 該命令為我們創建了一個 Deployment
        $ kubectl get deployment
        NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
        sonarqube   1         1         1            1           5m

        我們也可以直接以交互方式運行某個鏡像:

        $ kubectl run -i --tty ubuntu --image=ubuntu:16.04 --restart=Never -- bash -il

        K8s 將鏡像運行在 Pod 中以方便實施卷和網絡共享等管理,使用 get pods 可以清楚的看到生成了一個 Pod:

        $ kubectl get pods
        NAME                         READY     STATUS    RESTARTS   AGE
        sonarqube-1880671902-s3fdq   1/1       Running   0          6m
        
        $ 交互式運行 Pod 中的某個命令
        $ kubectl exec -it sonarqube-1880671902-s3fdq -- /bin/bash

        kubectl 可以用于刪除創建好的 Deployment 與 Pod:

        $ kubectl delete pods sonarqube-1880671902-s3fdq
        $ kubectl delete deployment sonarqube

        kubectl 通用可以基于 Yaml 文件進行應用的生命周期管理:

        # 創建
        $ kubectl create -f yamls/mysql.yaml
        
        # 刪除
        $ kubectl delete -f yamls/mysql.yaml
        
        # 同時創建多個
        $ kubectl create -f yamls/
        
        # 同時刪除多個
        $ kubectl delete -f yamls/

        上下文切換

        在 K8s 集群安裝完畢之后,可以下載集群的配置文件到本地 kubectl 配置中:

        mkdir $HOME/.kube
        scp root@<master-public-ip>:/etc/kubernetes/kube.conf $HOME/.kube/config

        然后可以來查看當前的上下文

        $ unset KUBECONFIG
        $ kubectl config current-context # 查看當前載入的上下文
        $ kubectl config get-contexts # 瀏覽可用的上下文
        $ kubectl config use-context context-name # 切換到指定上下文

        服務配置

        Kubernetes 實戰/典型應用一節中,我們介紹了許多常見的中間件的配置部署方式。這里以簡單的 HTTP 服務器為例,介紹常見的服務配置流程。

        Deployment & Service

        K8s Boilerplates 中我們定義了簡單的 Nginx 的部署與服務,分別用于集群構建與對外的服務暴露:

        # nginx-deployment-service.yaml
        ---
        apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
        kind: Deployment
        metadata:
          name: nginx
        spec:
          strategy:
            type: Recreate
          selector:
            matchLabels:
              app: nginx
          replicas: 3 # tells deployment to run 1 pods matching the template
          template: # create pods using pod definition in this template
            metadata:
              labels:
                app: nginx
            spec:
              containers:
                - name: nginx
                  image: nginx
                  ports:
                    - containerPort: 80
        ---
        apiVersion: v1
        kind: Service
        metadata:
          name: nginx
          namespace: default
          labels:
            app: nginx
        spec:
          externalTrafficPolicy: Local
          ports:
            - name: http
              port: 80
          selector:
            app: nginx
          type: NodePort
        $ kubectl create -f https://raw.githubusercontent.com/wx-chevalier/Backend-Boilerplates/master/K8s/Base/nginx-deployment-service.yaml
        
        $ kubectl get pod
        
        NAME                                             READY   STATUS    RESTARTS   AGE
        nginx-56db997f77-2q6qz                           1/1     Running   0          3m21s
        nginx-56db997f77-fv2zs                           1/1     Running   0          3m21s
        nginx-56db997f77-wx2q5                           1/1     Running   0          3m21s
        
        $ kubectl get deployment
        
        NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
        nginx                           3/3     3            3           3m36s
        
        $ kubectl get svc
        
        NAME                            TYPE           CLUSTER-IP      EXTERNAL-IP                              PORT(S)                      AGE
        kubernetes                      ClusterIP      10.43.0.1       <none>                                   443/TCP                      21h
        nginx                           NodePort       10.43.8.50      <none>                                   80:32356/TCP                 4m5s

        Ingress

        Ingress 是一種 Kubernetes 資源,也是將 Kubernetes 集群內服務暴露到外部的一種方式。ngress 只是一個統稱,其由 Ingress 和 Ingress Controller 兩部分組成。Ingress 用作將原來需要手動配置的規則抽象成一個 Ingress 對象,使用 YAML 格式的文件來創建和管理。Ingress Controller 用作通過與 Kubernetes API 交互,動態的去感知集群中 Ingress 規則變化。

        目前可用的 Ingress Controller 類型有很多,比如:Nginx、HAProxy、Traefik 等,Nginx Ingress 使用 ConfigMap 來管理 Nginx 配置。

        Helm 安裝 Ingress

        $ helm install --name nginx-ingress --set "rbac.create=true,controller.service.externalIPs[0]=172.19.157.1,controller.service.externalIPs[1]=172.19.157.2,controller.service.$
        xternalIPs[2]=172.19.157.3" stable/nginx-ingress
        
        NAME:   nginx-ingress
        LAST DEPLOYED: Tue Aug 20 14:50:13 2019
        NAMESPACE: default
        STATUS: DEPLOYED
        
        RESOURCES:
        ==> v1/ConfigMap
        NAME                      DATA  AGE
        nginx-ingress-controller  1     0s
        
        ==> v1/Pod(related)
        NAME                                            READY  STATUS             RESTARTS  AGE
        nginx-ingress-controller-5f874f7bf4-nvsvv       0/1    ContainerCreating  0         0s
        nginx-ingress-default-backend-6f598d9c4c-vj4v8  0/1    ContainerCreating  0         0s
        
        ==> v1/Service
        NAME                           TYPE          CLUSTER-IP    EXTERNAL-IP                             PORT(S)                     AGE
        nginx-ingress-controller       LoadBalancer  10.43.115.59  172.19.157.1,172.19.157.2,172.19.157.3  80:32122/TCP,443:32312/TCP  0s
        nginx-ingress-default-backend  ClusterIP     10.43.8.65    <none>                                  80/TCP                      0s
        
        ==> v1/ServiceAccount
        NAME           SECRETS  AGE
        nginx-ingress  1        0s
        
        ==> v1beta1/ClusterRole
        NAME           AGE
        nginx-ingress  0s
        
        ==> v1beta1/ClusterRoleBinding
        NAME           AGE
        nginx-ingress  0s
        
        ==> v1beta1/Deployment
        NAME                           READY  UP-TO-DATE  AVAILABLE  AGE
        nginx-ingress-controller       0/1    1           0          0s
        nginx-ingress-default-backend  0/1    1           0          0s
        
        ==> v1beta1/PodDisruptionBudget
        NAME                           MIN AVAILABLE  MAX UNAVAILABLE  ALLOWED DISRUPTIONS  AGE
        nginx-ingress-controller       1              N/A              0                    0s
        nginx-ingress-default-backend  1              N/A              0                    0s

        部署完成后我們可以看到 Kubernetes 服務中增加了 nginx-ingress-controller 和 nginx-ingress-default-backend 兩個服務。nginx-ingress-controller 為 Ingress Controller,主要做為一個七層的負載均衡器來提供 HTTP 路由、粘性會話、SSL 終止、SSL 直通、TCP 和 UDP 負載平衡等功能。nginx-ingress-default-backend 為默認的后端,當集群外部的請求通過 Ingress 進入到集群內部時,如果無法負載到相應后端的 Service 上時,這種未知的請求將會被負載到這個默認的后端上。

        $ kubectl get svc
        NAME                            TYPE           CLUSTER-IP      EXTERNAL-IP                              PORT(S)                      AGE
        kubernetes                      ClusterIP      10.43.0.1       <none>                                   443/TCP                      20h
        nginx-ingress-controller        LoadBalancer   10.43.115.59    172.19.157.1,172.19.157.2,172.19.157.3   80:32122/TCP,443:32312/TCP   77m
        nginx-ingress-default-backend   ClusterIP      10.43.8.65      <none>                                   80/TCP                       77m
        
        $ kubectl --namespace default get services -o wide -w nginx-ingress-controller
        
        NAME                       TYPE           CLUSTER-IP     EXTERNAL-IP                              PORT(S)                      AGE   SELECTOR
        nginx-ingress-controller   LoadBalancer   10.43.115.59   172.19.157.1,172.19.157.2,172.19.157.3   80:32122/TCP,443:32312/TCP   77m   app=nginx-ingress,component=controller,release=nginx-ingress

        由于我們采用了 externalIP 方式對外暴露服務, 所以 nginx-ingress-controller 會在三臺節點宿主機上的 暴露 80/443 端口。我們可以在任意節點上進行訪問,因為我們還沒有在 Kubernetes 集群中創建 Ingress 資源,所以直接對 ExternalIP 的請求被負載到了 nginx-ingress-default-backend 上。nginx-ingress-default-backend 默認提供了兩個 URL 進行訪問,其中的 /healthz 用作健康檢查返回 200,而 / 返回 404 錯誤。

        $ curl 127.0.0.1/
        # default backend - 404
        
        $ curl 127.0.0.1/healthz/
        # 返回的是 200

        后續我們如果需要創建自身的 Ingress 配置,可以參考如下方式:

        apiVersion: extensions/v1beta1
        kind: Ingress
        metadata:
          annotations:
            kubernetes.io/ingress.class: nginx
          name: example
          namespace: foo
        spec:
          rules:
            - host: www.example.com
              http:
                paths:
                  - backend:
                      serviceName: exampleService
                      servicePort: 80
                    path: /
          # This section is only required if TLS is to be enabled for the Ingress
          tls:
            - hosts:
                - www.example.com
              secretName: example-tls

        如果希望使用 TLS,那么需要創建包含證書與 Key 的 Secret:

        apiVersion: v1
        kind: Secret
        metadata:
          name: example-tls
          namespace: foo
        data:
          tls.crt: <base64 encoded cert>
          tls.key: <base64 encoded key>
        type: kubernetes.io/tls

        WordPress

        Helm 安裝完畢后,我們來測試部署一個 WordPress 應用:

        $ helm install --name wordpress-test --set "ingress.enabled=true,persistence.enabled=false,mariadb.persistence.enabled=false" stable/wordpress
        
        NAME:  wordpress-test
        ...

        這里我們使用 Ingress 負載均衡進行訪問,可以通過如下方式訪問到服務:

        $ kubectl get ingress
        
        NAME                             HOSTS             ADDRESS                                  PORTS   AGE
        wordpress.local-wordpress-test   wordpress.local   172.19.157.1,172.19.157.2,172.19.157.3   80      59m
        
        $ curl -I http://wordpress.local -x 127.0.0.1:80
        
        HTTP/1.1 200 OK
        Server: nginx/1.15.6
        Date: Tue, 20 Aug 2019 07:55:21 GMT
        Content-Type: text/html; charset=UTF-8
        Connection: keep-alive
        Vary: Accept-Encoding
        X-Powered-By: PHP/7.0.27
        Link: <http://wordpress.local/wp-json/>; rel="https://api.w.org/"

        也可以根據 Charts 的說明,利用如下命令獲得 WordPress 站點的管理員用戶和密碼:

        echo Username: user
        echo Password: $(kubectl get secret --namespace default wordpress-test-wordpress -o jsonpath="{.data.wordpress-password}" | base64 --decode)
        
        ==> v1beta1/Role
        NAME           AGE
        nginx-ingress  0s
        
        ==> v1beta1/RoleBinding
        NAME           AGE
        nginx-ingress  0s

        延伸閱讀

        某熊的技術之路指北 ? 就是對筆者不同領域方面沉淀下的知識倉庫的導航與索引,便于讀者快速地尋找到自己需要的內容。路漫漫其修遠兮,吾正上下而求索,也希望能給所有遇見過筆者痕跡的同學些許幫助,在浩瀚銀河間能順利達到一個又一個彼岸。

        您可以通過以下導航來在 Gitbook 中閱讀筆者的系列文章,涵蓋了技術資料歸納、編程語言與理論、Web 與大前端、服務端開發與基礎架構、云計算與大數據、數據科學與人工智能、產品設計等多個領域:

        此外,你還可前往 xCompass 交互式地檢索、查找需要的文章/鏈接/書籍/課程;或者在 MATRIX 文章與代碼索引矩陣中查看文章與項目源代碼等更詳細的目錄導航信息。最后,你也可以關注微信公眾號:『某熊的技術之路』以獲取最新資訊。

        查看原文

        贊 11 收藏 9 評論 2

        王下邀月熊_Chevalier 發布了文章 · 2019-07-31

        Cendertron,動態爬蟲的滑動驗證碼繞過策略

        Cendertron,動態爬蟲的滑動驗證碼繞過策略

        Cendertron 安全動態爬蟲系列中我們依次介紹了安全爬蟲的設計、爬蟲的集群搭建,本篇則是討論有關于滑動驗證碼的繞過策略。

        本文采用的策略與代碼來自 How to bypass “slider CAPTCHA” with JS and Puppeteer 一文。

        爬蟲中滑動驗證的繞過

        驗證是常見的反爬蟲策略之一,在現在的很多站點中我們會引入滑動驗證的方式,來校驗訪問者的真實性。譬如下面著名的 jQuery 滑動插件:

        在模擬登陸時,我們往往需要繞過這樣的滑動驗證,而基于 Puppeteer 的動態爬蟲也給予了便利;往往我們需要進行以下步驟:移動到滑條中間,按下鼠標,移動鼠標,釋放鼠標。

        const puppeteer = require('puppeteer');
        
        async function run() {
          const browser = await puppeteer.launch({
            headless: false,
            defaultViewport: { width: 1366, height: 768 }
          });
          const page = await browser.newPage();
        
          await page.goto('http://kthornbloom.com/slidetosubmit/');
          await page.type('input[name="name"]', 'Puppeteer Bot');
          await page.type('input[name="email"]', 'js@automation.com');
        
          let sliderElement = await page.$('.slide-submit');
          let slider = await sliderElement.boundingBox();
        
          let sliderHandle = await page.$('.slide-submit-thumb');
          let handle = await sliderHandle.boundingBox();
        
          await page.mouse.move(
            handle.x + handle.width / 2,
            handle.y + handle.height / 2
          );
          await page.mouse.down();
          await page.mouse.move(handle.x + slider.width, handle.y + handle.height / 2, {
            steps: 10
          });
          await page.mouse.up();
        
          await page.waitFor(3000);
        
          // success!
        
          await browser.close();
        }
        
        run();

        在實際的案例中,我們可以以淘寶的注冊界面為例:

        const puppeteer = require('puppeteer');
        
        async function run() {
          const browser = await puppeteer.launch({
            headless: false,
            defaultViewport: { width: 1366, height: 768 }
          });
          const page = await browser.newPage();
        
          await page.evaluateOnNewDocument(() => {
            Object.defineProperty(navigator, 'webdriver', {
              get: () => false
            });
          });
        
          await page.goto('https://world.taobao.com/markets/all/sea/register');
        
          let frame = page.frames()[1];
          await frame.waitForSelector('.nc_iconfont.btn_slide');
        
          const sliderElement = await frame.$('.slidetounlock');
          const slider = await sliderElement.boundingBox();
        
          const sliderHandle = await frame.$('.nc_iconfont.btn_slide');
          const handle = await sliderHandle.boundingBox();
          await page.mouse.move(
            handle.x + handle.width / 2,
            handle.y + handle.height / 2
          );
          await page.mouse.down();
          await page.mouse.move(handle.x + slider.width, handle.y + handle.height / 2, {
            steps: 50
          });
          await page.mouse.up();
        
          await page.waitFor(3000);
        
          // success!
        
          await browser.close();
        }
        
        run();

        另一種常見的滑塊則是如下這種拼圖性質的滑塊:

        const puppeteer = require('puppeteer');
        const Rembrandt = require('rembrandt');
        
        async function run() {
          const browser = await puppeteer.launch({
            headless: false,
            defaultViewport: { width: 1366, height: 768 }
          });
          const page = await browser.newPage();
        
          let originalImage = '';
        
          await page.setRequestInterception(true);
          page.on('request', request => request.continue());
          page.on('response', async response => {
            if (response.request().resourceType() === 'image')
              originalImage = await response.buffer().catch(() => {});
          });
        
          await page.goto('https://monoplasty.github.io/vue-monoplasty-slide-verify/');
        
          const sliderElement = await page.$('.slide-verify-slider');
          const slider = await sliderElement.boundingBox();
        
          const sliderHandle = await page.$('.slide-verify-slider-mask-item');
          const handle = await sliderHandle.boundingBox();
        
          let currentPosition = 0;
          let bestSlider = {
            position: 0,
            difference: 100
          };
        
          await page.mouse.move(
            handle.x + handle.width / 2,
            handle.y + handle.height / 2
          );
          await page.mouse.down();
        
          while (currentPosition < slider.width - handle.width / 2) {
            await page.mouse.move(
              handle.x + currentPosition,
              handle.y + handle.height / 2 + Math.random() * 10 - 5
            );
        
            let sliderContainer = await page.$('.slide-verify');
            let sliderImage = await sliderContainer.screenshot();
        
            const rembrandt = new Rembrandt({
              imageA: originalImage,
              imageB: sliderImage,
              thresholdType: Rembrandt.THRESHOLD_PERCENT
            });
        
            let result = await rembrandt.compare();
            let difference = result.percentageDifference * 100;
        
            if (difference < bestSlider.difference) {
              bestSlider.difference = difference;
              bestSlider.position = currentPosition;
            }
        
            currentPosition += 5;
          }
        
          await page.mouse.move(
            handle.x + bestSlider.position,
            handle.y + handle.height / 2,
            { steps: 10 }
          );
          await page.mouse.up();
        
          await page.waitFor(3000);
        
          // success!
        
          await browser.close();
        }
        
        run();

        這里我們采用了簡單的圖片對比的方式,即在滑動過程中,如果發現了有符合閾值的差異,則認為是已經滑動成功。

        Spider 配置

        Cendertron 中,提供了一類特殊的 Slider Captcha Monkey,在傳入的 SpiderOption 中添加如下參數即可:

        export interface SpiderOption {
          allowRedirect: boolean;
          depth: number;
          // 頁面插件
          monkies?: {
            sliderCaptcha: {
              sliderElementSelector: string;
              sliderHandleSelector: string;
            };
          };
        }

        延伸閱讀

        您可以通過以下任一方式閱讀筆者的系列文章,涵蓋了技術資料歸納、編程語言與理論、Web 與大前端、服務端開發與基礎架構、云計算與大數據、數據科學與人工智能、產品設計等多個領域:

        • 在 Gitbook 中在線瀏覽,每個系列對應各自的 Gitbook 倉庫。
        Awesome ListsAwesome CheatSheetsAwesome InterviewsAwesome RoadMapsAwesome-CS-Books-Warehouse
        編程語言理論Java 實戰JavaScript 實戰Go 實戰Python 實戰Rust 實戰
        軟件工程、數據結構與算法、設計模式、軟件架構現代 Web 開發基礎與工程實踐大前端混合開發與數據可視化服務端開發實踐與工程架構分布式基礎架構數據科學,人工智能與深度學習產品設計與用戶體驗
        查看原文

        贊 14 收藏 11 評論 2

        認證與成就

        • 獲得 3703 次點贊
        • 獲得 13 枚徽章 獲得 0 枚金徽章, 獲得 6 枚銀徽章, 獲得 7 枚銅徽章

        擅長技能
        編輯

        開源項目 & 著作
        編輯

        • Awesome-Links

          ?? Guide to Galaxy, curated, worthy and up-to-date links/reading list for ITCS-Coding/Algorithm/SoftwareArchitecture/AI. ?? ITCS-編程/算法/軟件架構/人工智能等領域的文章/書籍/資料/項目鏈接精選

        注冊于 2015-09-12
        個人主頁被 35k 人瀏覽

        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕