一年多沒更新博客園內容了,core已經發生了翻天覆地的變化,想起2014年這時候,我就開始了從當時還叫k的那套preview都不如的vnext搭建這套系統,陸陸續續它每一次升級,我也相應地折騰,大約4個月前,我開始把生產環境的一部分從 windows server 遷移到 centos 7 上,觀察... ...
序:一年多沒更新博客園的內容了,core已經發生了翻天覆地的變化,想起2014年這時候,我就開始了從當時還叫k的那套preview都不如的vnext搭建這套系統,陸陸續續它每一次升級,我也相應地折騰,大約4個月前,我開始把生產環境的一部分從 windows server 遷移到 centos 7 上,觀察了幾個月,覺得可以全面遷移了,於是總結了折騰的路上幾點經驗,與大家共勉。雖說我今天早已不是全職程式員,但是這套系統在我有空的時候總會維護與更新,它的運作與我目前的工作相輔相成,並且會一直更新下去。
這系統乾什麼的及地址就不說了,免得人家說我這不是碼農的碼農還賴在博客園發軟文。
為什麼要遷移,江湖上傳說windows server的穩定性不如某某某,這類議題與八卦新聞沒兩樣,不談,如果windows的價錢能夠和linux相同或者差異不至於那麼大,我才懶得換,因為窮,這才是重點。
涉及IO路徑拼接,一定要Path.Combine, 反正我自己眼力比較差,手工拼接的話,有時候多一個或少一個斜杠,或者斜杠方向反了,Linux系統都會出錯,搞半天不知道錯在哪,如果大小寫都搞錯卻沒註意或者一時不知的話,神仙都救不了。
註意用System.Environment.NewLine代替硬編碼的換行符,String.Empty代替"",這兩個看不見的東西,發作的時候喝腦白金都沒用。
而core在初始化的時候,會遍歷wwwroot文件夾包含的所有內容,在windows中無所謂你文件夾怎麼命名,在中文centos也無所謂你文件夾怎麼命名。但是,如果你的生產伺服器是英文系統,恰好在wwwroot中有中文文件夾的話,你都不知道為什麼會全局報錯。關於中文文件夾(在某些時刻會演變為亂碼文件夾),我也不知哪裡來的,有些客戶端編輯器組件,就是包含了非英文的文件夾。所以搭建虛擬機測試環境的時候,guest系統需要安裝英文版本來驗證。
newtonsoft的json在反序列化枚舉類型的時候,得到的是名,微軟自家的json在反序列化枚舉的時候,得到的是值,雖然可以通過特性來定義,但是本人對特性相當反感,全套代碼不會用也不可能會用,所以有時候需要用class來代替enum。
Microsoft.AspNetCore.NodeServices 是個好東西,就簡單來說,正則表達式驗證庫,只寫一次,客戶端和服務端都可以共用。
Program.Main方法里,可以建立後臺線程,裡面放一個全局靜態的ConcurrentQueue<Action>,while(true)定時監視它,今天我們編寫的是進程級的asp.net,你可以讓它做很多IIS做不到的真正非同步,比如說記錄某些日誌,扔action進去就是了,不必等待。
事情還可以做得更絕一些:開闢新進程,把代價高昂的、不確定是否會出錯的、卻需要立即清理記憶體的操作丟給它,操作完了自動關閉,在這一層面上,你可以藐視.net的垃圾回收。
如果你實在玩不來gulp也沒關係,裝個BundlerMinifier就行了,右鍵選幾個js後自動壓縮
能用PocoController的時候,儘量用,比如說某些頻繁的API,它的資源占用最小。
為HttpClient建立池,static ConcurrentQueue<HttpClient>,兩三個就夠了,一般不會出錯,但是誰也不能保證網路請求,錯了一個就讓下一個來代替,然後把出錯的扔到上面的ConcurrentQueue<Action>中讓它愛什麼時候銷毀什麼時候銷毀。
文件級資料庫,比如說SQLite,也需要ConcurrentQueue<Action>來封裝寫入邏輯,絕對的保證同一時刻只有一個線程寫,同時做好定時自動備份並且是每小時、每半天、每天、每星期這樣的多個備份,否則不可預料的IO把你的資料庫搞壞,喊天天不應。
總會遇到有人和你勸說XXX是世界上最好的語言,遇到這類事情,只要給他一份最新的島國女演員名單,他就會改口說:其實XXX也不是那麼好。
windows上你不能覆蓋一個正在運行中的dll,但是linux上可以,先覆蓋,再重啟,但是如果連重啟都不允許的話,就開啟新的埠,讓前段伺服器熱切換,nginx的reload命令是實時的,如果你連前nginx也沒有,直接kestrel面向公眾,卻又不允許一秒鐘以上的暫停服務,那就新建伺服器來等待功能變數名稱DNS切換吧。
傳統ASP.NET的Bundle功能現在見不到了,需要在開發階段手動通過gulp來壓縮,但是如果遇到需要實時構造js的情況怎麼辦? 現在既然有了NodeServices,服務端壓縮在理論上不是問題,目前我還沒實現,正在研究。
別折騰與XML相關的東西,凡是XML做的事,都可以用JSON來代替,光是頭部一大車的uri垃圾代碼與脫褲子放屁的namespace兩項,足以讓我徹底拋棄它。
該緩存的東西,用記憶體來緩存,比如說某些不可能上萬條目的目錄, 建一個靜態字典才1M不到,卻可以少做一次inner join。
該不該內聯javascript,對此我分兩個階段,項目初期是內聯幾句最簡單的代碼,動態生成script標簽來請求真正需要的javascript,然後把內容記錄到本地indexedDB中,以後再也不請求js代碼了,從本地資料庫中讀即可,head加一個meta標簽來標識javascript是否需要重新讀取,css也是同樣的道理,如此一來內聯的代碼也就十多行,這價錢花得起。到了第二階段,內聯和本地資料庫也省了,因為有了http2,服務端推送結合max-age,一次TCP連接也是花費得起的(註意這裡說的是TCP, TCP!),它同樣是僅一次真正傳輸內容。
關於 public async Task<IActionresult> xxxx() ,此非同步非彼非同步,具體就不詳解了,請自行搜索。有時候我們需要在一個請求上下文中同時做到等同於桌面應用中的“真非同步”,你就不要await,雖然visual studio會給出綠色提示說讓你為某個task加上await,加上的話你才是錯了。而是應該TASK的start()後乾別的事情,在方法最後統一waitall。當然此方式涉及到線程開銷,要根據具體問題來取捨。
可能你會需要這一段代碼,否則MVC自帶的 return Json(某對象) 會被強制修改為小寫,據說那是JSON規範,少來,雞毛規範和我一點關係沒有,我只認識過度的自作聰明是找罵。
services.AddMvc().AddJsonOptions(opt => { var resolver = opt.SerializerSettings.ContractResolver; if (resolver != null) { var res = resolver as DefaultContractResolver; res.NamingStrategy = null; // <<!-- 修正預設JSON全部變成小寫的問題 } });
同理,關閉一切殺毒軟體,包括windows defender,我就遭遇到windows defender要求上傳一份sqlite的操作代碼到它的伺服器作分析,而這份代碼是來自最新的core 1.1,又不是我寫的,傳或不傳呢?這不重要,重要的是我因此把windows defender永久禁用。還好它沒刪東西,但是換做別家,可能就沒這麼好說話了,到時你的kestrel一直報500都不知道為什麼。
通過某時間段的請求頻率確定惡意刷新後,立即永久屏蔽IP,IP列表保留在記憶體中,Configre的app.UseMiddleware階段就進行判斷,10M的IP列表,加上集群負載均衡,壓力再大的話請求azure的防火牆加持,這一切都應該是程式進行(azure有相應API),看誰玩得狠。
註意檢查伺服器日誌,某些阿三搜索引擎、自詡為你安全著想的防火牆、三流雲監測、等等,會加大你的伺服器壓力,從IP和UserAgent上兩方面屏蔽它們。
註意識別請求路徑,比如說遇到請求“/phpmyadmin”的,絕對是加入永久屏蔽IP列表。
(個人問題,不具備參考價值,請勿仿效):資料庫架構儘可能簡單,從前基於SQLServer,什麼亂七八糟的功能都用上,但是同時也造就了遷移過程中一道巨大的壁壘,解決辦法有三:
- 只遷移應用程式,不遷移資料庫,但是如果說應用程式運行在Linux,卻依然依賴於Windows上的SQLServer的話,對於系統的遷移目的來說,是掩耳盜鈴。
- Linux版SQLServer,且先不說它目前是RC,重點依然是個人原因,需要一臺4G記憶體的主機,窮,買不起。當年谷歌通過廉價PC構造集群的思路,始終值得學習。
- 【採用】重構資料庫架構,什麼存儲過程、地理編碼、表間關聯…… 通通不要了(個人問題,不具備參考價值,請勿仿效),由應用程式來代替之,資料庫只保留最基本的表存儲、最基本的數據類型、做到最大的相容性,於是現在伺服器A基於SQLServer、伺服器B基於SQLite、伺服器C基於MySQL、它們共用相同的應用程式、分佈在三個地理位置,組成API集群,定時相互同步數據,讓各種各樣的前端伺服器、移動端應用程式根據地理位置的變化、實時熱切換數據源,同時使用三個不同的資料庫來運作,是出於實驗目的,打算再觀察幾個月,看看它們分別有什麼表現,為以後能否在經濟上優化作參考。
同時,幾台www伺服器組成另一個集群,處理各種客戶端的交互,包括與APP交互,www集群節點也具備集群中某一節點崩潰時自動重建功能,但是不與資料庫直接打交道,只與API集群交互,不可感知API伺服器的再後端使用了什麼資料庫。如此就具備了熱切換API節點的能力,為什麼要構造“兩道轉輪”式的架構呢,因為頻繁的功能更新、BUG修複,只要保證通信協議的不變,無論是WWW集群或API集群中任一節點下線,都不會影響整套系統的運行,功能變數名稱的DNS服務層面上監視著WWW集群的健康狀態,最終客戶正在請求的某個www節點下線時,DNS伺服器可感知,便會實時把最終客戶的功能變數名稱請求調度到下一個健康的www節點的IP,而www集群所有節點也記錄了所有api集群節點的地址,可以在當前與API的通信崩潰時自動篩選到下一健康節點嘗試連接,篩選的過程是地理位置優先,然後是健康狀態。它們跨洋分佈在三個國家,除非三個國家都同時停電,否則系統將會完好運行,最終客戶不會感知到變化。
下麵是我用一段JSON偽代碼來描述它們之間的關係,註意顏色對應,x表示會變動的下標:
{ client: {member:[html-web, winform, ios, android], request:wwwserver[x]}, wwwserver: [ node1:{kestrel1:{server:www, request: apiserver[x]}, kestrel2:fileserver}, node2:{kestrel1:{server:www, request: apiserver[x]}, kestrel2:fileserver}, node3:{kestrel1:{server:www, request: apiserver[x]}, kestrel2:fileserver} ], apiserver: [ node1:{database:sqlite}, node2:{database:sqlserver}, node3:{database:mysql} ] }
下麵再來說說文件伺服器,它們是第三層集群,也就是上面偽代碼中的fileserver:
因為我的伺服器中有大量的照片,它們的流量是個很大的問題,說要租CDN嗎,1T流量一年300塊錢左右,平均下來每個月850M,我某個主題就包含200多張照片,要花50M的流量,為什麼這麼大,過去1280寬度的屏幕,照片就得2560寬度,然後CSS縮小50%,通過這樣間接的超採樣,才能照顧retina。這 850M/每月 夠塞幾次牙縫?加流量吧,10T每年需要3000塊錢,而這價錢可以買上好幾台性能不錯的伺服器了,而且免流量。而且10T還不一定夠,再加? 窮。 所以在很久以前的最前期,我弄了個投機辦法,但不是長久之計:淘寶賣家圖片空間,支持外鏈,它分佈全國的cdn不用說了吧。我在博客園同樣也上傳有很多照片,不過請站長放心,我沒乾那事,以後也不打算乾。
- 淘寶圖片空間:總有一天它會關閉或者加某些限制的,所以目前已經不再添加新的東西進去了,但是在有些頁面上,還在鏈接著,懶,有空再改。
- 【目前的方案】自建文件伺服器,目前它們分佈在浙江、武漢、山東、成都、美人希,www伺服器會根據請求者的地理位置,返回 <img src="https://最近的文件伺服器IP" />,依然是基於成本考慮,國內這四個地區伺服器的總運營成本每年2000塊錢左右,而且免流量。而在國外的伺服器基於azure,也是買的最廉價版本,每月14.88美元,按照7的匯率換算,約104每月,每月1T流量,看似還是不夠,但是要知道在網路zi-you區,綁定cloudflare的免費cdn就行了。 國內的四個文件伺服器直接請求IP,反正img的src是動態的,一來免去了那道你懂的手續,二來沒有dns,也相對的提升了速度。當然也留有一手後路:一個總開關統一請求美人希(反正那時候就不是快與慢的問題了)。這幾個文件伺服器之間也自發性的組成一個集群,它們會相互同步文件,壞了沒關係,集群中的第一個感知到的節點會通過API嘗試重啟,三次重啟失敗的話會銷毀舊的,同時建立一個新的伺服器,然後再自動同步。
這下知道我為什麼要把代碼放到linux了吧,便宜,你仔細找還是可以找到的,windows不可能有這價錢,壞了拉倒,立即重建,整個自動化重建過程不到5分鐘。重建後伺服器間的同步填充,通過http2的流式連接,快得很。而通過core的selfhost編譯出來的應用程式,你的裸機根本不需要裝運行時,sftp傳上去就立即啟動運作,azure上就更方便了,我之前建好iso,基於iso啟動就完事,傳程式都免了,只需同步內容數據。
說到這,又驗證了一個道理:這個世界,只分為兩處:牆內和牆外。
這樣一來每年總的運營成本3400塊錢左右,基本上不需要再為流量操心。當然,光是這麼做還不夠,為了緩解瞬間併發壓力,客戶端緩存是必不可少的,Cache-Control 的 max-age 設置了儘可能的大,反正二進位文件不可能更改的,要改也是改img的src。同時,也通過http2來優化連接效率,關於http2,以下是我給出的配置,從一臺centos 7.1裸機起步,假設已經是root:
暫時利用一下nginx:
添加:/etc/yum.repos.d/nginx.repo 內容: [nginx] name=nginx repo baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ gpgcheck=0 enabled=1 yum install nginx #目前是1.10.2,先讓它自動安裝。重點是它會替你去折騰各種配置。 然後: yum install vim wget lsof gcc gcc-c++ bzip2 -y yum install net-tools bind-utils -y yum install expat-devel yum install git wget http://sourceforge.net/projects/pcre/files/pcre/8.36/pcre-8.36.tar.gz wget http://zlib.net/zlib-1.2.8.tar.gz wget https://www.openssl.org/source/openssl-1.0.2h.tar.gz wget http://nginx.org/download/nginx-1.10.2.tar.gz tar xzvf pcre-8.36.tar.gz tar zxf openssl-1.0.2h.tar.gz tar xzvf zlib-1.2.8.tar.gz tar xzvf nginx-1.10.2.tar.gz git clone git://github.com/yaoweibin/ngx_http_substitutions_filter_module.git git clone https://github.com/arut/nginx-dav-ext-module git clone https://github.com/gnosek/nginx-upstream-fair git clone https://github.com/openresty/echo-nginx-module cd nginx-1.10.2 ./configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-file-aio --with-threads --with-ipv6 --with-http_addition_module --with-http_auth_request_module --with-zlib=../zlib-1.2.8 --with-pcre=../pcre-8.36 --with-openssl=../openssl-1.0.2h --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_ssl_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic' --add-module=../echo-nginx-module --add-module=../nginx-upstream-fair --add-module=../nginx-dav-ext-module --add-module=../ngx_http_substitutions_filter_module make && make install
現在就可以按照你自己的需求配置nginx了。下麵是https的配置:
yum -y install git bc git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt /opt/letsencrypt/letsencrypt-auto --help systemctl stop nginx /opt/letsencrypt/letsencrypt-auto certonly --standalone -d example.com -d www.example.com -d foo.example.com # -d後面是你的各種功能變數名稱,註意修改 systemctl start nginx openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
到此,你的nginx已經可以支持http2和http2了。只強調兩點:1,openssl 必須是 1.0.2h 或者更高,2,如果你的伺服器身在牆內,yum下載各種包的時候,有可能會遇到網路問題,這不是你的錯。
哪怕你不需要http2,但是你一定會需要https,從2017年開始,chrome會對普通http提示“這是不安全網路”,有些心理毛病的客戶會據此猜測你的網站有病毒、甚至還會演變為你的網站會盜他支付寶的錢、甚至他家的雞少下一個蛋都會說你網站有輻射,你會去和這類人解釋什麼叫ssl嗎?我是不會的。 蘋果AppStore不再接受請求http的app發佈,意味著如果你的伺服器同時是app的api,就必須升級。
下麵是一段server的例子:
server { listen 443 ssl http2; server_name www.*; ssl_certificate /etc/letsencrypt/live/你的具體路徑/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/你的具體路徑/privkey.pem; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_dhparam /etc/ssl/certs/dhparam.pem; ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_stapling on; ssl_stapling_verify on; add_header Strict-Transport-Security max-age=15768000; location / { proxy_pass http://你的kestrel的實際運行地址; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; } }
暫時先總結到此,文中的azure指另一個世界的azure。至於鎮內的四台伺服器可隨時亂搞,愛怎麼換就怎麼換。
鎮內的運營商有時候不得不做一些違背經營之道的事,也是迫不得已,體諒他們的同時,自己也不得不留一手後備之路。也幸虧他們,從前一個單機web小系統,演變為今天如此複雜的分散式。
目前還待解決的問題
1: 為什麼我說要暫時利用一下nginx, 雖然說kestrel自身也可以承載https,但是目前每一個伺服器節點上都運行著兩個kestrel實例,如果讓它們直接面向公眾,就出現埠衝突問題,所以也必須通過nginx來做反向代理,否則我也不願意折騰這事情,目前也正在想法怎麼可以拋棄前端代理,現在思路是:
- 合併兩個服務的代碼,kestrel就只有一個了。
- 分離到兩台伺服器,成本翻倍,窮。
- 等你補充
為了整套系統的全面控制,這事情是一定要做的,而且我也不相信前端伺服器必備的論點,什麼DDOS,歡迎來搞。
2:TypeScript的服務端運行,通過Google搜索: typescript run on server ,你會發現很多有趣的討論,為了更好的融合客戶端與服務端。
3:GPU計算一直是我嚮往的事情,在我認為,Core的優勢不僅僅是跨平臺,“進程級”才是它的重點,目前我們處於進程級,就可以操控GPU,而且Azure已經提供了搭配Tesla處理器的主機,而目前我的這套系統也與地理坐標有著業務關聯,待我收集多一些數據,再來開啟這套技術怎麼應用。
4:僅存於疑問階段:根據P2P打洞原理,伺服器探測到多個請求者存在於同一個相距很近的地理區域時,互相介紹兩個請求者建立連接,然後這兩個請求者之間互相同步內容數據,這一切都是基於javascript+indexedDB來實現。前提是他們都在打開某個不會短時間內關閉的連接。反正還待研究。
5:目前我還卡在windows的visual studio上,是因為項目庫用的是visual studio online 的 git 同步備份代碼,明明兩個月前我還在mac的vs code上可以同步代碼,現在我在非windows visual studio的一切git客戶端都登不上去了,包括windows版的vs code也登不上,不是網路問題,我用戶名密碼也沒錯,連接VPN也是一樣,我搜了一下說要在項目主頁裡面尋找一個叫Secret什麼的開關,我找到了,開了也沒用,而且我也沒動過這些相關的東西,如果是一開始就不行,我也就認了,現在是中途不讓登了,不是我的錯,搞了半小時不搞了,以後有時間自建伺服器。
6:升級到1.1後,visual studio可以編譯,但是不讓發佈了,不知原因,輸出界面沒有任何提示,反正就是點擊了發佈之後,瞬間完成動作,實際卻什麼都沒做。 現在通過自建命令行cmd文件來發佈,反正以後切換平臺也是命令行編譯發佈,也就無所謂了。
我靠,為了做文中的幾個截圖,多開了幾個瀏覽器,再次回到這裡後臺編輯的時候,edge重新載入了,第一次遇到這類事情,幸虧博客園後臺有自動保存,嚇出一身阿富汗。
最後,既然azure支持上傳iso搭建自定義系統,那麼還請各位推薦一款資源最小的linux,我想研究一下能不能把core放上去。
今天我看到一則新聞:佳能參與日本宇航局的火箭研發。 時代已經進步到這份上了,奧巴馬也快下臺了,而你還在憂心asp.net的第一次預熱? core沒有這概念。這就又說回我在張善友兄的某篇博文的評論中給某位仁兄的回覆,某些人必須等到2028年才會相信core可以做事(可能還不夠,2048吧),人的觀點都是長久形成並固化的,就像XXX是世界上最好的語言一樣,不必勸,也不必爭,但是如果你不是這一類,現在就可以開始:進程級、跨平臺、開源自定義。