說實(shí)話又一年沒寫博客了,不過終于畢業(yè)了,也許以后可以有更多的時間寫寫博客(tao。
趁著還沒入職的間隙,做了一些博客改造的開發(fā)工作,其中運(yùn)維:JavaScript:Python:Java 工作量占比大概 ,總共間斷的開發(fā)了兩周多。
?? Motivations
想必各位 NLPer 看蘇神博客的時候一定被其豐富的知識量和閱讀體驗(yàn)極佳的公式震撼過。
出于這點(diǎn)考慮,本期的學(xué)術(shù)化改造的一個目的就是提供良好的公式編譯能力,增強(qiáng)對 的支持。
其二,作為學(xué)術(shù)類博客,需要提升評論及引用能力,提供相應(yīng)的 。
其三,更清晰的 Tag 維護(hù)策略,Tag 之間存在層級關(guān)系。
除此之外,放開靜態(tài)文件爬取限制,收緊 API 接口權(quán)限,實(shí)現(xiàn)動態(tài)管理,(其實(shí)這個一直想做的,只是沒想到做起來那么花時間。
?? Developing
前三條,都是 JavaScript 的工作。
由于歷史原因,本博客用的還是 Vuepress@0.16,看了一下 v1 版本 lib 代碼改動不大,等 v2 發(fā) beta 版了有空再改吧(能用就將就用著。
MathJax
有別于蘇神使用 php 開發(fā)的博客,Vuepress 是一個基于 markdown-it 的框架。
而 Markdown 天生就和$\LaTeX$不搭,比如說_在 Markdown 中表示 $x_i + y_i$ 會被編譯為x<em>i + y</em>i。
而我們的目標(biāo)是支持 MathJax 而不是 katex 這種閹割版本。
參考 Yihui Xie 在The Best Way to Support LaTeX Math in Markdown with MathJax提供的思路,
利用<code>標(biāo)簽提供一個不會被 markdown-it 侵入的環(huán)境,再對 markdown-it 編譯好的 body 做 code 標(biāo)簽解除,以便 MathJax 的 JavaScript 代碼能夠渲染相應(yīng)公式區(qū)域。
replaceLatexCode(){
var i, text, code, codes = document.getElementsByTagName('code');
for (i = 0; i < codes.length;) {
code = codes[i];
if (code.parentNode.tagName !== 'PRE' && code.childElementCount === 0) {
text = code.textContent;
if (/^\$[^$]/.test(text) && /[^$]\$$/.test(text)) {
text = text.replace(/^\$/, '\\(').replace(/\$$/, '\\)');
code.textContent = text;
}
if (/^\\\((.|\s)+\\\)$/.test(text) || /^\\\[(.|\s)+\\\]$/.test(text) ||
/^\$(.|\s)+\$$/.test(text) ||
/^\\begin\{([^}]+)\}(.|\s)+\\end\{[^}]+\}$/.test(text)) {
code.outerHTML = code.innerHTML; // remove <code></code>
continue;
}
}
i++;
}
},
不過需要注意的是 Vuepress 中聯(lián)系訪問頁面是 SPA,進(jìn)入下一頁之后需要重新做上述操作。
getMathJax() {
const script1 = document.createElement('script');
script1.src = 'https://xxxx/MathJax-2.7.4/AMS-setcounter.js';
script1.type = 'text/javascript';
script1.id = "ams-counter";
setTimeout(() => document.body.appendChild(script1), 500);
const script2 = document.createElement('script');
script2.type = 'text/javascript';
script2.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-AMS-MML_HTMLorMML';
script2.id = "tex-ams";
setTimeout(() => document.body.appendChild(script2), 700);
setTimeout(() => document.getElementById("ams-counter").remove(), 2000);
setTimeout(() => document.getElementById("tex-ams").remove(), 2000);
},
(其中AMS-setcounter.js是借鑒蘇神的配置。
然后公式的顏色感覺還是藍(lán)色最合適,其他看的都容易困(主要是太菜了
使用的時候一定要帶上在反引號中帶上 dollar 符或者\begin{}\end{} (單行公式在博客里閱讀體驗(yàn)++
$bib\TeX$ 和 Utterence
(可能是畢業(yè)論文寫傻了,自然而然的想到應(yīng)該給博客加一個的功能。
這點(diǎn)技術(shù)難度幾乎沒有,只不過為了更好的支持中文人名(Last Name, First Name)的形式,將原先 config.js 中的 author 拓展成 authorLastName 和 authorFirstName。
需要注意的是同樣由于 SPA 也需要對反復(fù)渲染,(我這邊實(shí)現(xiàn)的比較丑陋,沒有用成 vue 中的 watch。
此外對于評論模塊做了遷移,之前一直使用的是 Gitalk,也在利用 Gitalk 給 Vuepress 搭建的 blog 增加評論功能介紹了 Vuepress 中的實(shí)現(xiàn)。
它確實(shí)樣式美觀,GitHub Issue 的方式既能控制評論權(quán)限也能方便數(shù)據(jù)遷移。
但是它的缺點(diǎn)十分明顯:
- 暴露 Github Token;
- 必須每篇 blog 都建立一個 Issue 即使是沒有人評論的;
第二點(diǎn)是我不太能忍的,主要是有一直看 Issue 的習(xí)慣。大家也知道,我以前一直是用一個 Issue 管理所有博客的評論,這對之前評論的人也是一種打擾。
我設(shè)想中應(yīng)該是主動 Push 型的,有人評論再去建 Issue,本來已經(jīng)想自己實(shí)現(xiàn)了,結(jié)果發(fā)現(xiàn)有一個叫 Utterence 的插件完美的實(shí)現(xiàn)了我的需求。
借助 Bot 的形式也能規(guī)避 GitHub Token 的暴露。
不過它使用 iframe 的形式,讓自定義 style 變成了不可能,所幸 PC 端和移動端的樣式也還能接受。
同樣因?yàn)?SPA 需要反復(fù)渲染。
Multi-level Tags
考慮到 Tags 這個東西實(shí)際上是類似于 Key Word 的東西,像 ACM 系列的會議/期刊都會按多級的方式顯示以便不同興趣的學(xué)者能快速定位到相應(yīng)的文章。
之前實(shí)現(xiàn)的是單一級的 Tags,導(dǎo)致 Tags 并不能展示文章的分類信息。
于是實(shí)現(xiàn)了一個多級(考慮到實(shí)際使用需求最大級數(shù)定為 3 級),并支持葉子層多 Tags 的方式,例如NLP/LM/(KnowledgeInject#KnowledgeBase)就表示了NLP/LM/KnowledgeInject和NLP/LM/KnowledgeBase兩條三級路徑。
實(shí)際上是一個多叉樹結(jié)構(gòu),需要對樹做建樹和深度遍歷(Tags 頁線性展示),大概是一個 LeetCode Easy 的題。
export function dfsTagGraph(root, tagG, done) {
const queue = [];
const res = [];
queue.push([root, 1]);
while (queue.length) {
var top = queue.pop();
if (done.has(top[0])) {
continue;
}
done.add(top[0]);
res.push(top);
const children = tagG[top[0]] || [];
children.forEach(c => {
if (!done.has(c)) {
queue.push([c, top[1] + 1]);
}
})
}
return res;
}
同樣需要注意的是需要反復(fù)渲染。
權(quán)限管理
(事先說明,這個策略還是很簡陋的,請各位安全大佬手下留情 ??
下面來到最重頭的權(quán)限管理,這其實(shí)是我一直想做的,之前從Nginx 配置和Nginx 日志分析兩方面做了限制。
但是這個限制是天粒度的,定時跑腳本也行,但是對于時間邊界整體是不敏感的。
究其原因,是因?yàn)?Log 是以文件形式存放,失去了流屬性和時間屬性。
ELK
調(diào)研之后,使用 ElasticSearch + Kibana + Filebeat 方案。
Filebeat 監(jiān)聽 file 當(dāng) file 開始更新的時候會分配一個 registrar 去執(zhí)行相應(yīng) pipeline。
Filebeat 是極其輕量的,可以定義一些 js 腳本處理格式等問題,自身也提供 Module 以供進(jìn)行解析常見軟件 log。
推薦這種方式,以 Nginx Access Log 為例,還會對 Ip 進(jìn)行 geo 分析,對 User-Agent 也會進(jìn)行解析,這節(jié)省了我們大量分析工作。
(當(dāng)然 Nginx 自身也提供 Geo 不過需要你安裝特定的插件。
由于 Filebeat 是輕量的,這些 machine_learning Jobs 都是推到 ElasticSearch 上由 Java 來執(zhí)行的。
所以如果你想利用 Module 由 Filebeat 傳到 Redis 的話,那得自己做 geo 和 UA 解析。
整體架構(gòu)如下圖所示,
大部分時間花在搭建和配置上了,即使是用 docker。貼一下搭建過程中踩的坑
- ES 和 Kibana 默認(rèn)只開放本地操作權(quán)限,如要 remote 操作,需要設(shè)定 host:"0.0.0.0";
- 我是用一臺機(jī)子放 ES + Kibana,然后兩臺機(jī)子 Filebeat 到 ES 機(jī)子上。通過 Nginx 轉(zhuǎn)發(fā) ES 和 Kibana 請求,大部分均正常,只有在 Kibana 下少部分 POST 會返回 413,這就導(dǎo)致 FileBeat 連接帶不同主機(jī)(不同內(nèi)網(wǎng)域)的 Kibana 時候必須開個端口用域名連接。(此處控制變量判斷問題源)
- Kibana 和 ES,F(xiàn)ilebeat 的密碼相關(guān)設(shè)定不要明文寫在文件中,但是不同的 keystore 接口不同,Kibana 的 KEY 直接是 config 的 key(也只能是)
ELK 之后還可以做很多應(yīng)用,比如說數(shù)據(jù)上報(bào)實(shí)時展示,通過 Kibana 的 timelion 進(jìn)行繪圖,(這又是另外一個坑了
除了 ES 提供的數(shù)據(jù),還需要進(jìn)一步擴(kuò)充數(shù)據(jù)源。
- 明確 IP 對應(yīng)服務(wù)器用途,利用 Ip2Proxy 查詢 geo 解析得到的 asn 所對應(yīng)的臨近 IP 端用途,如果 IP 端較近則認(rèn)為用途一致;
- 收集所有 Header 信息,這個需要配置 Nginx 啟動 Lua,參考StackOverFlow.
- 相對應(yīng)的 Filebeat Inject 也需要做對應(yīng)的修改
nginx.conf 配置如下
log_format myformat escape=none '$remote_addr $host $hostname $remote_user [$time_local] '
'"$request" $status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$http_cookie" @$request_headers@';
access_log logs/access.log myformat;
set_by_lua_block $request_headers{
local h = ngx.req.get_headers()
local request_headers_all = ""
for k, v in pairs(h) do
local rowtext = ""
rowtext = string.format("[%s:%s] ", k, v)
request_headers_all = request_headers_all .. rowtext
end
return request_headers_all
}
相對應(yīng)的 Filebeat 的 Inject Pipeline 配置
(%{NGINX_HOST} )?\"?(?:%{NGINX_ADDRESS_LIST:nginx.access.remote_ip_list}|%{NOTSPACE:source.address}) (-|%{DATA:url.host}) (-|%{DATA:user.name}) \\[%{HTTPDATE:nginx.access.time}\\] \"%{DATA:nginx.access.info}\" %{NUMBER:http.response.status_code:long} %{NUMBER:http.response.body.bytes:long} \"(-|%{DATA:http.request.referrer})\" \"(-|%{DATA:user_agent.original})\" \"(-|%{DATA:header.x_forward})\" \"(-|%{DATA:header.cookie})\" @(-|%{DATA:header.original})@
當(dāng)獲得數(shù)據(jù)之后,定時獲取區(qū)間數(shù)據(jù),進(jìn)行策略分析(需要控制度)和 Ipset 封禁。
此外還在服務(wù)端增設(shè) Cookies 用以輔助權(quán)限管理。
總結(jié)
本文針對本博客從 MathJax、Tags、評論、四方面做學(xué)術(shù)化改造,并利用 ElasticSearch + Kibana + Filebeat 提出一種權(quán)限管理策略。
水平有限,歡迎討論。