文/胡揚(yáng)帆
筆者作為技術(shù)實(shí)驗(yàn)室自主開(kāi)發(fā)小組成員,近幾年使用Node.js相關(guān)技術(shù),參與了新華社綜合評(píng)審系統(tǒng)、神筆大俠闖江湖在線(xiàn)游戲、中國(guó)重大外交表述語(yǔ)料庫(kù)等多個(gè)社內(nèi)創(chuàng)新項(xiàng)目的技術(shù)設(shè)計(jì)和開(kāi)發(fā)工作。本文通過(guò)總結(jié)Node.js技術(shù)在應(yīng)用開(kāi)發(fā)工作中的經(jīng)驗(yàn)與思路,旨在進(jìn)一步提高自身能力素質(zhì),同時(shí)就Node.js技術(shù)如何在我社技術(shù)系統(tǒng)中全面推行,更好地應(yīng)用于核心業(yè)務(wù)系統(tǒng)的建設(shè)進(jìn)行了探討,提出了思路和建議。
Node.js是一個(gè)開(kāi)源跨平臺(tái)的服務(wù)器端JavaScript運(yùn)行環(huán)境。它以Google Chrome V8解析引擎為基礎(chǔ),采用類(lèi)似Nginx的以事件循環(huán)驅(qū)動(dòng)的異步I/O模型提高運(yùn)行效率,通常認(rèn)為,異步I/O的并發(fā)性能是同步I/O的近10倍。Node.js的核心優(yōu)勢(shì)在于通用、靈活、高性能。
Node.js高性能的本質(zhì),在于它使用了底層庫(kù)libuv與操作系統(tǒng)內(nèi)核配合處理異步事件,libuv封裝了對(duì)各操作系統(tǒng)高效事件輪詢(xún)函數(shù)的調(diào)用,如Linux的epoll,Unix的kqueue,Windows的 IOCP,libuv將Node.js中高時(shí)延的網(wǎng)絡(luò)I/O和磁盤(pán)I/O的請(qǐng)求通過(guò)系統(tǒng)調(diào)用注冊(cè)給操作系統(tǒng)內(nèi)核,操作系統(tǒng)內(nèi)核準(zhǔn)備好請(qǐng)求數(shù)據(jù)后回調(diào)libuv所注冊(cè)的用戶(hù)函數(shù)將數(shù)據(jù)送回用戶(hù)空間完成I/O。通過(guò)這種單線(xiàn)程非阻塞的異步分層協(xié)作機(jī)制,既解放了CPU的高速運(yùn)算能力,又避免了多進(jìn)程或多線(xiàn)程模型容易造成的資源調(diào)度與開(kāi)銷(xiāo)問(wèn)題,使得Node.js可以像Nginx那樣單個(gè)線(xiàn)程就能處理成千上萬(wàn)的網(wǎng)絡(luò)請(qǐng)求。
Node.js暴露給開(kāi)發(fā)者的編程接口十分簡(jiǎn)潔,開(kāi)發(fā)者只需以回調(diào)函數(shù)的方式操作I/O返回?cái)?shù)據(jù)就能實(shí)現(xiàn)異步I/O。就這樣Node.js把易學(xué)易用的JavaScript和強(qiáng)大的Unix網(wǎng)絡(luò)編程模型結(jié)合在一起,在龐大的開(kāi)源社區(qū)以及活躍的技術(shù)委員會(huì)的帶動(dòng)下,逐漸從各類(lèi)技術(shù)體系中脫穎而出。
Node.js的不足在于它單線(xiàn)程的運(yùn)行方式不能有效利用多核CPU進(jìn)行運(yùn)算,雖然調(diào)用C++擴(kuò)展模塊或Java Jar包可以彌補(bǔ),但在原生語(yǔ)言方面它的效率不如多線(xiàn)程的靜態(tài)編譯型語(yǔ)言,因而Node.js通常采用Master-Worker的主-從多進(jìn)程模式進(jìn)行部署,以彌補(bǔ)單線(xiàn)程對(duì)CPU利用率的不足。
筆者從語(yǔ)言生態(tài)、開(kāi)源社區(qū)和Node.js官方組織的報(bào)告等方面調(diào)研了Node.js技術(shù)的現(xiàn)狀,近兩年Node.js相關(guān)技術(shù)被越來(lái)越多的開(kāi)發(fā)者或企業(yè)所接受,在社區(qū)生態(tài)中呈現(xiàn)出前所未有的發(fā)展速度。具體情況見(jiàn)附件部分。
美聯(lián)社早在2014年就用了Node.js技術(shù)對(duì)外提供新聞圖片查詢(xún)的公共接口服務(wù)。紐約時(shí)報(bào)在2017年6月改版了包括PC端和移動(dòng)端的新聞發(fā)布產(chǎn)品線(xiàn),新版后臺(tái)以Node.js服務(wù)提供統(tǒng)一支撐,前端使用React技術(shù),整個(gè)新聞發(fā)布業(yè)務(wù)應(yīng)用Node.js和JavaScript全棧技術(shù),這使他們將多語(yǔ)言分別維護(hù)的代碼倉(cāng)庫(kù)合并成了一份,業(yè)務(wù)變得更加高效、精簡(jiǎn)。
阿里巴巴作為國(guó)內(nèi)最早一批研究Node.js的互聯(lián)網(wǎng)企業(yè),自2015年開(kāi)始采用Node.js技術(shù)分擔(dān)“雙十一”線(xiàn)上大流量以來(lái),已經(jīng)積累了相當(dāng)多的經(jīng)驗(yàn)。Node.js被運(yùn)用于阿里內(nèi)部的分布式基礎(chǔ)設(shè)施之上,替換了原來(lái)使用PHP或Java 開(kāi)發(fā)的應(yīng)用層的程序,利用其靈活高效和高性能的特點(diǎn)服務(wù)于數(shù)據(jù)I/O并發(fā)量高,運(yùn)營(yíng)和業(yè)務(wù)相關(guān)需求多變的應(yīng)用訴求。使用Node.js進(jìn)行全棧開(kāi)發(fā)提高了開(kāi)發(fā)效率并使前后端合作邊界后移。
在阿里的Java / C++和Node.js多語(yǔ)言生態(tài)中,靜態(tài)編譯型語(yǔ)言開(kāi)發(fā)的底層系統(tǒng)應(yīng)對(duì)數(shù)據(jù)庫(kù)事物ACID、分布式和計(jì)算密集場(chǎng)景,業(yè)務(wù)相對(duì)成熟穩(wěn)定;以Node.js為主的上層應(yīng)用系統(tǒng)應(yīng)對(duì)靈活多變的業(yè)務(wù)訴求和數(shù)據(jù)密集的場(chǎng)景,各取所長(zhǎng)。除了阿里,國(guó)外的 Paypal、Netflix 等體量比較大的公司也使用 Node.js 來(lái)做類(lèi)似的分工。
應(yīng)用層的主要任務(wù)是與各種數(shù)據(jù)源和底層系統(tǒng)使用基于 HTTP 或者 RPC 的 API 交流,加上一定的業(yè)務(wù)邏輯,對(duì)數(shù)據(jù)做適當(dāng)?shù)奶幚砗?,渲?HTML 或者拼裝 JSON與客戶(hù)端溝通。應(yīng)用層的特點(diǎn)在于業(yè)務(wù)需求多變,接口并發(fā)需求高。
圖1 Node.js在阿里的應(yīng)用場(chǎng)景
首先,在語(yǔ)言方面,強(qiáng)類(lèi)型靜態(tài)語(yǔ)言在需要大量處理HTML渲染或JSON拼裝的場(chǎng)景下,效率先天不如弱類(lèi)型動(dòng)態(tài)語(yǔ)言。比如當(dāng)應(yīng)用需要操作一個(gè)API返回的JSON數(shù)據(jù)時(shí),Java需要預(yù)先定義好與這個(gè)JSON對(duì)應(yīng)的POJO類(lèi)(包括成員類(lèi)型定義和get/set方法定義),并進(jìn)行實(shí)例化后才能進(jìn)行操作,而JavaScript可以直接將JSON字符串實(shí)例化成對(duì)象進(jìn)行操作,當(dāng)返回JSON數(shù)據(jù)的結(jié)構(gòu)或類(lèi)型發(fā)生變化時(shí),Java相關(guān)代碼的維護(hù)量將比JavaScript多很多。
在框架方面,Java Spring追求用嚴(yán)格的代碼檢查和層層的封裝規(guī)定來(lái)保障穩(wěn)定性,面對(duì)應(yīng)用層MVC數(shù)據(jù)、視圖、控制器各層模型都需要靈活定制或擴(kuò)展情況下,過(guò)多的儀式化代碼犧牲了開(kāi)發(fā)效率。在沒(méi)有事務(wù)性的復(fù)雜要求,或者事務(wù)被服務(wù)進(jìn)行封裝的情況下,傳統(tǒng)框架的穩(wěn)定性?xún)?yōu)勢(shì)難以體現(xiàn)。而Node.js從開(kāi)發(fā)、測(cè)試、部署、冷啟用方面的速度更快,效率更高。
在接口性能方面,以同步阻塞I/O為主的多線(xiàn)程Spring框架在處理大量網(wǎng)絡(luò)請(qǐng)求時(shí)的并發(fā)效率與異步I/O不在一個(gè)量級(jí),只是目前多進(jìn)程多服務(wù)器的集群部署方式在一定范圍內(nèi)掩蓋了其并發(fā)性能的劣勢(shì)。
從2014年以來(lái),筆者所在的開(kāi)發(fā)小組從最初的實(shí)踐項(xiàng)目《輕易輕應(yīng)用制作工具》中開(kāi)發(fā)提煉了一套自研的技術(shù)開(kāi)發(fā)框架,并應(yīng)用到近幾年的各類(lèi)項(xiàng)目中。由于當(dāng)時(shí)的早期社區(qū)沒(méi)有類(lèi)似Java Spring的成熟型企業(yè)級(jí)框架,因此我們進(jìn)行了大量的Web系統(tǒng)相關(guān)技術(shù)的研究和選型,涉及內(nèi)容包括登錄與鑒權(quán),Cookie與Session,數(shù)據(jù)庫(kù)ORM,異常處理,定時(shí)任務(wù),模板渲染,日志,Web安全等方面。逐漸形成了一套相對(duì)完善的自有框架,可按需進(jìn)行配置、集成和擴(kuò)展。這個(gè)過(guò)程讓我們對(duì)開(kāi)發(fā)框架的認(rèn)識(shí)從過(guò)去的單方面依賴(lài)上升到了可自主掌控的程度,深入理解了框架的作用。
在《新華社綜合評(píng)審系統(tǒng)》項(xiàng)目中,我們使用有限狀態(tài)機(jī)模型管理投票的狀態(tài)與狀態(tài)轉(zhuǎn)換規(guī)則,使用WebSocket實(shí)現(xiàn)端到端的實(shí)時(shí)雙向通信,保證評(píng)委端在投票環(huán)節(jié)之外的所有頁(yè)面自動(dòng)化切換,以數(shù)據(jù)庫(kù)存儲(chǔ)為中心的計(jì)算與I/O分離策略保證投票狀態(tài)的持久化和恢復(fù)能力,以確保在現(xiàn)場(chǎng)出現(xiàn)任何技術(shù)故障時(shí)可以做到服務(wù)的自動(dòng)恢復(fù)和評(píng)委頁(yè)面的自動(dòng)恢復(fù)。最終該項(xiàng)目以實(shí)際表現(xiàn)出來(lái)的穩(wěn)定可靠和快捷易用受到廣泛好評(píng)。
在《神筆大俠闖江湖微信H5游戲》項(xiàng)目中,采用數(shù)據(jù)庫(kù)的存儲(chǔ)類(lèi)型優(yōu)化和稀疏化索引數(shù)據(jù)的手段提高積分排名接口對(duì)實(shí)時(shí)計(jì)算的性能要求,以單進(jìn)程部署服務(wù)進(jìn)行測(cè)試,復(fù)雜接口在5秒500并發(fā)的壓力下平均響應(yīng)時(shí)間在2秒以?xún)?nèi),符合性能預(yù)期。最終游戲靜態(tài)資源采用CDN分發(fā),接口服務(wù)采用了單臺(tái)虛擬機(jī)部署,上線(xiàn)后接口服務(wù)快速平穩(wěn),應(yīng)用內(nèi)存占用平均300M左右,沒(méi)有觸及性能天花板。
在后續(xù)項(xiàng)目中我們開(kāi)始實(shí)踐了整合度更高的全棧開(kāi)發(fā),例如使用流行的MVVM框架Vue.js進(jìn)行前端業(yè)務(wù)的工程化開(kāi)發(fā),對(duì)接Node.js提供的RESTful接口服務(wù),對(duì)全棧開(kāi)發(fā)與集成的高效性和統(tǒng)一性有實(shí)際體會(huì)。
憑借Node.js技術(shù)的靈活特性和動(dòng)態(tài)語(yǔ)言靈活易用的特點(diǎn),各項(xiàng)開(kāi)發(fā)工作在編碼、測(cè)試、部署、構(gòu)建和冷啟動(dòng)等方面效率都很高,使得開(kāi)發(fā)小組在人手不足,任務(wù)緊急而艱巨的情況下經(jīng)常能達(dá)到早于預(yù)期的進(jìn)度,并且保障工作任務(wù)的完成質(zhì)量。
隨著我社新聞業(yè)務(wù)向互聯(lián)網(wǎng)思路轉(zhuǎn)型,整體業(yè)務(wù)構(gòu)建于互聯(lián)網(wǎng)數(shù)字化新聞的采集、編輯、發(fā)布體系之上,技術(shù)特點(diǎn)趨向于數(shù)據(jù)交互密集,新聞業(yè)務(wù)模式亦從穩(wěn)定趨向于靈活創(chuàng)新。其中采集端重在上傳效率,編輯端重在協(xié)同、檢校與流轉(zhuǎn),發(fā)布端重在高并發(fā),稿件全域追溯與流程控制重在各系統(tǒng)服務(wù)間的高度整合。數(shù)據(jù)交互環(huán)節(jié)多,流程長(zhǎng),涉及數(shù)據(jù)存取、API網(wǎng)關(guān)、多協(xié)議通信、統(tǒng)一認(rèn)證、實(shí)時(shí)消息、流式傳輸和模板渲染、協(xié)同編輯、持續(xù)集成等,屬于Node.js相關(guān)全棧技術(shù)擅長(zhǎng)的應(yīng)用范圍。
對(duì)于社內(nèi)業(yè)務(wù)穩(wěn)定的基礎(chǔ)組件和基礎(chǔ)中間件,計(jì)算密集型業(yè)務(wù),以及對(duì)事務(wù)性操作有極高一致性要求的業(yè)務(wù)應(yīng)繼續(xù)保持Java/C++等靜態(tài)語(yǔ)言的既有優(yōu)勢(shì)。其他以功能靈活,數(shù)據(jù)交互為主的,要求快速開(kāi)發(fā)、高性能的Web服務(wù),都可以選擇Node.js技術(shù)進(jìn)行開(kāi)發(fā)。
軟件的組織形式在向著靈活發(fā)展,無(wú)論從部署形式的IaaS、CaaS、PaaS、SaaS到現(xiàn)在的FaaS(函數(shù)即服務(wù),Serverless架構(gòu)),從軟件組織形式的單一應(yīng)用到微服務(wù)架構(gòu),指導(dǎo)思想都是盡量將變化約束到最小的獨(dú)立單元,以穩(wěn)定的體系去支撐靈活多變的獨(dú)立自治的業(yè)務(wù)單元。因此,對(duì)技術(shù)選型的要求也從過(guò)去的大一統(tǒng)技術(shù)框架變成了依據(jù)業(yè)務(wù)特點(diǎn)靈活選擇。Node.js及其npm的開(kāi)源生態(tài)踐行了單一職責(zé)原則的“微服務(wù)”的思想,各類(lèi)技術(shù)框架都體現(xiàn)了極簡(jiǎn)易集成的特點(diǎn),因此在當(dāng)前的微服務(wù)架構(gòu)之下,Node.js可以很好地與不同語(yǔ)言的服務(wù)管理框架進(jìn)行集成接入。
在如今開(kāi)源社區(qū)繁榮生態(tài)的支撐下,各類(lèi)通用性的開(kāi)發(fā)、測(cè)試和部署的工具庫(kù)百花齊放,開(kāi)箱即用,應(yīng)用開(kāi)發(fā)人員的核心任務(wù)逐漸聚焦于自身業(yè)務(wù)的實(shí)現(xiàn)。通常,我們可以參考工具庫(kù)提供的應(yīng)用案例來(lái)實(shí)現(xiàn)應(yīng)用功能,可以參考業(yè)界高性能架構(gòu)案例來(lái)實(shí)現(xiàn)更高性能的服務(wù)。但是,業(yè)務(wù)模型和代碼架構(gòu)只能由我們自己深入理解業(yè)務(wù)需求后自行設(shè)計(jì),因此應(yīng)用開(kāi)發(fā)的難點(diǎn)在于深度理解業(yè)務(wù)模型,并對(duì)框架和業(yè)務(wù)代碼進(jìn)行高度抽象以形成高內(nèi)聚,低耦合的易復(fù)用易擴(kuò)展的結(jié)構(gòu)。
為了更好地將Node.js技術(shù)應(yīng)用于系統(tǒng)建設(shè),應(yīng)努力做到工程的結(jié)構(gòu)清晰、功能靈活、部署穩(wěn)定、代碼便于擴(kuò)展與維護(hù),主要體現(xiàn)在以下四個(gè)方面:
從業(yè)務(wù)層面,要聯(lián)系實(shí)際抓業(yè)務(wù)重點(diǎn),分清主次,了解使用場(chǎng)景,掌控工程實(shí)現(xiàn)細(xì)節(jié)和粒度,廣泛了解各種技術(shù)框架的特點(diǎn)并做到合理選型,整合人力資源,技術(shù)資源,時(shí)間資源做切實(shí)可行的開(kāi)發(fā)計(jì)劃。
從技術(shù)層面,要有在開(kāi)源庫(kù)中找“輪子”,選“輪子”和集成“輪子”的能力;靈活利用開(kāi)發(fā)框架提供的結(jié)構(gòu)性?xún)?yōu)勢(shì);明確業(yè)務(wù)代碼與框架的職責(zé)邊界;梳理業(yè)務(wù)代碼的層次并實(shí)現(xiàn)通用代碼的復(fù)用;在保證工具、架構(gòu)和開(kāi)發(fā)流程穩(wěn)定的情況下實(shí)現(xiàn)功能和性能的擴(kuò)展;配置統(tǒng)一化標(biāo)準(zhǔn)化;使用云計(jì)算或容器進(jìn)行應(yīng)用部署;實(shí)現(xiàn)持續(xù)交付。
從運(yùn)行層面,要考察系統(tǒng)各項(xiàng)APM性能監(jiān)測(cè)指標(biāo),包括運(yùn)行進(jìn)程、線(xiàn)程的穩(wěn)定性,接口性能,日志追蹤,內(nèi)存占用以及Web安全。
從迭代層面,要根據(jù)系統(tǒng)運(yùn)行指標(biāo)或者新業(yè)務(wù)訴求對(duì)系統(tǒng)進(jìn)行持續(xù)升級(jí)改進(jìn),當(dāng)功能依賴(lài)開(kāi)源工具時(shí),按需做到隨工具的升級(jí)而更新。
綜合Node.js相關(guān)技術(shù)以及JavaScript技術(shù)通用、靈活、高效的特點(diǎn),全球性的高速發(fā)展態(tài)勢(shì),以及本社新媒體創(chuàng)新業(yè)務(wù)對(duì)技術(shù)系統(tǒng)提出的高效性、靈活性、可控性要求,Node.js相關(guān)技術(shù)的應(yīng)用潛力巨大。
截止2018年2月,根據(jù)modulecounts.com發(fā)布的各語(yǔ)言包管理器公共模塊數(shù)量統(tǒng)計(jì)排名,Node.js包管理工具npm誕生3年后在2015年超越了Java Maven成為第一,當(dāng)前npm的模塊數(shù)量已經(jīng)數(shù)倍于其后的Java和PHP。
圖1 公共模塊數(shù)量統(tǒng)計(jì)排名(來(lái)自modulecounts.com)
根據(jù) GitHub.com發(fā)布的 Octoverse報(bào)告,JavaScript成為2017 GitHub 上最受歡迎的編程語(yǔ)言,JavaScript技術(shù)棧的項(xiàng)目數(shù)量超過(guò)Python或Java的兩倍。
圖2 2017年最受歡迎的編程語(yǔ)言排名(來(lái)自github.com)
2017年 Stack Overflow 年度開(kāi)發(fā)者報(bào)告中最受開(kāi)發(fā)者歡迎的技術(shù)框架部分Node.js排名第一。
圖3 最受開(kāi)發(fā)者歡迎的技術(shù)框架(來(lái)自Stack Overflow)
Node.js基金會(huì)發(fā)布的Node.js 2017用戶(hù)調(diào)查報(bào)告顯示,Node.js覆蓋了前端、后端以及全棧應(yīng)用的開(kāi)發(fā),最多涉及Node.js的工具和技術(shù)包括數(shù)據(jù)存儲(chǔ),前端框架,后端框架,負(fù)載均衡,云和容器,持續(xù)集成工具,Node.js在Web應(yīng)用開(kāi)發(fā)中最受歡迎,占比達(dá)84%。
圖4 涉及Node.js開(kāi)發(fā)的工具技術(shù)
圖5 Node.js開(kāi)發(fā)場(chǎng)景類(lèi)型
Node.js繼續(xù)保持著前所未有的發(fā)展速度,如今它已經(jīng)成為一個(gè)通用的開(kāi)發(fā)框架,被廣泛地應(yīng)用于各種數(shù)據(jù)數(shù)字化的應(yīng)用程序中。