PingCAP CTO 黃東旭 :基礎軟體“好用”指南——必須跨越這兩道鴻溝!

PingCAP發表於2021-10-18

最近有一件事情讓我印象特別深刻,作為引子和大家嘮一嘮:我們在內部做一些極端的流量回歸模擬實驗時,在 TiKV(TiDB 的分散式儲存元件)上觀測到了異常的 CPU 使用率,但是從我們的 Grafana Metrics、日誌輸出裡面並沒有看到異常,因此也一度困惑了好幾天,最後靠一位老司機盲猜並結合 profiling 才找到真凶,真凶出現在誰都沒有想到的地方:Debug 用的日誌模組(澄清一下:目前這個 Bug 已經修復了,而且這個 Bug 的觸發是在非常極端壓力的場景下+日誌級別全開才會出現,請各位使用者放心)。
這篇文章並不是做 Bug 分析,我覺得更重要的是,找問題過程中我們使用的工具、老司機的思考過程。作為一個觀察者,我看到年輕的同事看著老司機熟練地操作 perf 和在各種各樣工具和介面中切換那種仰慕的眼神,我隱約覺得事情有點不對:這意味著這門手藝不能複製。
事後,我做了一些關於基礎軟體使用者體驗的調研,發現該領域的理論和資料確實挺少(大多數是 ToC 產品的研究,系統軟體相關的大概只有 UNIX 哲學流派),而且缺乏系統化,依賴於作者個人「品味」,但是軟體體驗的好和壞顯然存在,例如一個有經驗的工程師看到一個命令列工具,敲幾下就知道是否好用,是不是一個有「品味」的工具。
很多時候「品味」之所以被稱為「品味」,就是因為說不清道不明,這固然是軟體開發藝術性的一種體現,但是這也意味著它不可複製,不易被習得。我覺得這也不好,今天這篇以及可能接下來的幾篇文章(雖然後幾篇我還不知道寫啥,但是先立個 Flag)會試著總結一下好的基礎軟體體驗到底從哪裡來。
作為第一篇,本文將圍繞可觀測性和可互動性兩個比較重要的話題來談。至於為什麼把這兩點放在一起聊,我先賣個關子,最後說。

可觀測性

可觀測性是什麼?這可從我兩年前發表的《我眼中的分散式系統可觀測性》[1]一文中可見一斑,相同的內容我在這裡就不贅述。隨著在 TiDB 中對可觀測性實踐的深入,對這個話題有了更深的理解,為了更好的理解,我們首先先明確一個問題:當我們在聊可觀測的時候,到底是誰在觀測?

是誰在觀測?

很多朋友可能會一愣,心想:這還用說,肯定是人,總不能是機器。沒錯,的確是人在觀測,但就是這麼一個淺顯的道理往往會被軟體設計者忽略,所以這兩者的區別到底是什麼?為什麼強調人這個主體很重要?

要回答這個問題,需要清楚一個現實:人的短期工作記憶是很有限的。大量的心理學研究表明,人類工作記憶的容量大致只有 4,即在短期同時關注 4 項資訊[2],再多的資訊就要靠分模組的方式記憶,如我們快速記憶電話號碼的方式,以 13800001111 為例,我們通常不是一個個數字背,而是形如:138-0000-1111 進行分組。
在瞭解人的心智模型的一些基礎假設和頻寬後,我想很多系統軟體開發者大概不再會炫耀:我的軟體有 1000 多個監控項!這不僅不是好事,反而讓更多的資訊破壞了短期記憶的形成,引入了更多的噪音,讓使用者在資訊的海洋裡花很多時間找關鍵資訊,以及不自覺的分類(我相信大腦的一個不自覺的後臺任務就是對資訊建索引和分類,注意這同樣是消耗頻寬的),所以第一個結論:軟體應用一屏的介面裡面最好只有 4 個關鍵資訊。那麼,接下來的一個問題是:哪些是關鍵資訊?什麼是噪音?

區分關鍵資訊和噪音

這個問題沒有標準答案。對於系統軟體來說,我的經驗是:跟著關鍵資源走。軟體其實很簡單,本質就是對硬體資源的使用和分配,講究平衡的藝術。關鍵的硬體資源無非也就下面幾個,對於下面每一個關鍵資源在某個取樣時間段(單點沒有太多意義),都可以通過一些簡單的問題的詢問,得到對系統執行狀態的大致圖景:

  • CPU:哪些執行緒在工作?這些執行緒都在幹嘛?這些執行緒各自消耗了多少 CPU Time?
  • 記憶體:當前記憶體中儲存了哪些東西?這些東西的命中率情況?(通常我們更關注業務快取)?
  • 網路 I/O:QPS/TPS 有異常嗎?當前主要的網路 I/O 是由什麼請求發起的?頻寬還夠嗎?請求延遲?長連結還是短連結(衡量 syscall 的開銷)?
  • 磁碟 I/O:磁碟在讀寫檔案嗎?讀寫哪些檔案?大多數的讀寫是什麼 Pattern?吞吐多大?一次 I/O 延遲多大?
  • 關鍵日誌:不是所有日誌都有用,只有包含特定關鍵字的日誌,人們才會關心。所以,有沒有特定關鍵字的日誌出現?

通過以上標準問題的靈魂拷問,必定可以對系統執行狀態有一定的瞭解。

  • 更進一步的關鍵是,這些系統的指標一定要和業務上下文聯絡在一起才能好用,舉例說明,對於一個支援事務的資料庫來說,假設我們看到 CPU 執行緒和 call stack,發現大量的 CPU 時間花在了 wait sleep idle 之類的事情上,同時也沒有其他 I/O 資源瓶頸,此時,如果只看這些的數字可能會一臉懵,但是結合事務的衝突率來看可能柳岸花明,甚至能直接給出這些 lock 的等待時間都花在了哪些事務,甚至哪些行的衝突上,這對觀測者是更有用的資訊。
    也並不是說其他的資訊就沒用,而是相當多的資訊的價值是後驗的,例如:絕大多數的 debug 日誌,或者那些為了證實猜想的輔助資訊,其實在解決未知問題時候幾乎沒有幫助,而且還需要觀察者有大量的背景知識,這類資訊最好的呈現方式還是摺疊起來,眼不見為淨的好。
    如果開啟 TiDB 的內部 Grafana 就會看到大量這樣的指標,如 stall-conditions-changed-of-each-cf(雖然我知道這個指標的含義,但是我猜 TiDB 的使用者裡 99% 的人不知道),而且從名字裡面我看到了寫下這個名字的工程師內心的掙扎,他一定很想讓其他人(或者自己)看懂這個名字指的是什麼,但是比較遺憾,至少在我這裡沒有成功。
    觀察的下一步是什麼?作出行動。在做出行動之前想想,有行動的前提是什麼?我們處理問題的行動大致會遵循下面模式(我自己總結的,但任何一本認知心理學的書都會有類似的概念):觀察—>發現動機—>猜想—>驗證猜想—>形成計劃—>行動,然後再回到觀察,反覆迴圈。
    這個裡面人(或者是老司機的經驗)體現比較重要地方是在從觀察到猜想這個環節,至於觀察的動機而言無非有兩種:
    解決眼前的故障;

規避潛在的風險(避免未來的故障)。

假設系統沒有問題,也不太需要做出改變。 我覺得這兩步之所以重要,是因為基本上其他環節都可以用自動化,唯獨這兩步很難,因為需要用到:人的知識/經驗和直覺。
對於一個擁有好的可觀測性的系統,通常都是能很好利用人直覺的高手,舉個小的例子:當開啟一個系統後臺介面時,我們試著不去關注具體的文字資訊,如果介面中的紅色黃色的色塊比較多,我們的直覺會告訴自己這個系統可能處於不太健康的狀態,更進一步如果紅色和黃色大致都聚集在螢幕的某個具體位置上,我們的注意力一定會聚焦到這個位置;如果一個介面上全是綠色,那應該是比較健康的狀態。
怎麼最大化利用人的直覺?或者說要引導到什麼地方?我認為最好的點是:風險的預判。

人的直覺用在哪?風險的預判

此處需要利用一些先驗知識。在聊這個話題之前,我想分享一個我之前聽過的小故事,當年福特工廠裡有個電機壞了,然後找了個老師傅,他聽了聽聲音,看了看機器運轉情況,最後用粉筆在電機上畫了一條線,說這個地方的線圈多繞了多少多少圈,將信將疑的工人們照做,果然問題解決了,然後老師傅開了個 1 萬美元的維修費(當時算是天價),福特的老闆問他憑啥畫一條線就收那麼多錢,老師傅開了個賬單:畫線 1 美元,知道在哪畫這條線 9999 美元。

故事的真假暫且不聊,假設是真的,我們可以看到直覺和經驗,真的是能產生很多的價值,我當時聽到這個故事的第一反應是,這個老師傅肯定這種情況見的多了(廢話),而且這個問題一定是常見問題。
其實解決問題最難部分是通過觀察(尤其是一些特徵點)排除掉絕大多數不靠譜的方向,另外要相信常見故障的原因是會收斂的。這時一個具有良好可觀測性系統的第一步就是能給使用者的直覺指引方向,這個方向就需要前人的知識來給出可能性最大的故障點以及相關的指標(例如 CPU 使用率等);第二步就是通過一些心理學小技巧把它展現出來。
下面以 TiDB 中即將會引入的一個小功能 TopSQL 加以佐證。這個功能說起來也很簡單,我們發現很多使用者故障都和少量的 SQL 相關,這類的 SQL 的特徵是擁有和別的 SQL 有明顯不同的 CPU footprint,但是每一條 SQL 的 footprint 獨立看起來還挺正常的,所以 TopSQL 的功能就是回答:CPU 到底消耗了多少?在哪些 SQL 上?我試著不去解讀下面這個截圖,我猜聰明的你馬上就能知道怎麼用:


你的直覺會告訴你,後半段那段密集的綠色佔比好像和其他有什麼不一樣,將整體的 CPU 使用率推高了,感覺有問題的樣子,沒錯,這大概就是正確的方向,好的視覺化能夠利用人的直覺快速定位主要矛盾。

什麼叫做 “一個操作”?識別操作的真正的生命週期

剛才寫第一點的時候想到還有一個經常被人忽略的關鍵資源:時間。本來想把時間放到關鍵資源那節裡面,但是想了想放在這裡可能更加合適。

稍微形而上一點來看,我們現在的計算機都是圖靈機的實現,我小學就知道圖靈完備語言的最小功能集合:讀/寫變數,分支,迴圈。用文學一點的說法是:所謂程式就是無數個輪迴,大輪迴巢狀著小輪迴(迴圈),每個輪迴中根據現狀(變數)不斷的做出選擇(分支)。
我說到這裡可能聰明的讀者會猜到我想說什麼:如果我們討論可觀測性脫離了週期,就毫無意義。而週期的定義又是靈活的,對於人而言,大週期顯然是一輩子,小週期可以是一年一日,甚至週期可以不用時間跨度作為單位,比如一份工作的週期…
對於一個資料庫軟體而言,什麼是一個合理的週期?是一條 SQL 的執行週期?還是一個事務從 Begin 到 Commit ?這裡沒有標準答案,但是我個人建議,週期越貼近終端使用者的使用場景越實用。
譬如,在資料庫中,選擇單條 SQL 的執行作為週期不如選擇事務的週期,事務週期不如應用程式一個請求全鏈路的週期。其實 TiDB 在很早就引入了 OpenTracing 來追蹤一個 SQL 的執行週期內到底呼叫了哪些函式,花費多少時間,但最早只應用在了 TiDB 的 SQL 層內部(熟悉我們的朋友應該知道我們的 SQL 和儲存是分離的),沒有在儲存層 TiKV 實現,所以就會出現一條 SQL 語句的執行過程往下追到 TiKV 就到了一個斷頭路;
後來我們實現了把 TraceID 和 SpanID 傳到了 TiKV 內部這個功能才算初步可用,至少把一個週期的圖景變得更加完整了,本來我們打算就止步於此,但是後來發生了一個小事情,某天一個客戶說:為什麼我的應用訪問 TiDB 那麼慢?然後我一看 TiDB 的監控,沒有啊,SQL 到資料庫這邊基本都是毫秒就返回了,但是客戶說:你看我這個請求也沒幹別的呀,兩邊怎麼對不上?後來我們把 Tracer 加進來以後才知道客戶這邊的網路出了點問題。
這個案例提醒了我,如果能做到全鏈路的 Tracing,這裡的全鏈路應該是從業務端請求開始計算,去看待生命週期才有意義。所以在此之後我們在 TiDB 裡面通過擴充 Session Variable,能夠支援使用者將 OpenTracing 協議的 Tracer 資訊通過 Session Varible 傳入到 TiDB 的體系中,打通業務層和資料庫層,能夠真正實現的一個全生命週期的跟蹤,這個功能也會在很近的未來的版本中和大家見面。
說了這麼多,總結幾點:

時間也是重要資源。
抓 Sample 也好,做 Trace 也好,選對週期很重要。
週期越貼近業務的週期越有用。

可觀測效能救命的時刻:事後觀測

我相信沒有人會沒事天天看著監控介面,其實仔細想想,當我們需要可觀測性的時候,多數是已經出現了可感知的故障或者很明確的風險。此時的系統可能已經“病入膏肓”,或者在火燒眉毛的時候還不知道啥原因導致,其中的根因或是之前某個時間的一些不太顯然的異常變化,這時候發現之前除了正常的 Metrics 外並沒有更多的資訊,我們當然不會永遠開著 CPU Profiler,通常 Profiler 都是手動觸發,但是如果是在事後覆盤原因的時候,能夠有事發之前的 CPU Profile 記錄,對於問題的解決和歸因會有巨大的幫助,所以一個比較好的方案是:在一個相對短的時間間隔下(比如分鐘級)自動的開啟 Profiler,自動把診斷結果儲存下來,就像定期做一個深度體檢記錄一樣,老的記錄定期刪除就好了,萬一出事,可以快速往前回溯,救命的效率會更高。

另外相信我,做 Profile 其實也不會有什麼明顯的效能損耗(何況還是間歇性的),這個功能我們叫做:Continuous Profiling,這個功能很實用,也會很快和大家見面。
根據我們的經驗,結合上面一節,有了完善的 Tracing 系統,大部分的 Debug 過程在 Tracing + Log 就能找到問題的根因。

最好的可觀測性是能夠指導使用者: “我接下來該做什麼?”

上文中提到了行動,我在觀察老師傅處理問題的時候發現一個特別有意思的現象:有經驗的開發者總是能夠很快通過觀測,決定自己接下來該做什麼,不需要查閱資料什麼或者等著別人指導,完全處於一個心流的狀態(例如在 TiDB 裡面看到資料在叢集內部分佈不均或者有熱點,就知道去修改排程策略或者手工 split region),但是新人在這一步總是會卡著,要麼去 Google 要麼去翻文件,內心 OS:「我看到問題了,然後怎麼辦?」,如果這個時候,系統能夠給一些接下來應該觀測哪些指標,或者行動建議,會更加友好,目前能做到這一點的系統不多,如果能做到這一點,相信你的系統已經在可觀測性上做得很棒了。把這個點放在可觀測性的最後其實是想借著這個話題引出可互動性。

可互動性

在聊基礎軟體的可互動性之前,我想先和大家回顧一下計算機的歷史,在我看來計算機歷史的一個側寫就是人機互動的進化史:從第一張圖,看著一堆線我也不知道怎麼操作,到現在我從來沒看過 iPhone 的說明書就能夠熟練使用,這個背後其實是多個學科的進步(包括不限於心理學、認知科學神經科學、哲學、電腦科學)。





回到我們這個領域,基礎軟體這個領域因為離大眾確實有點遠,過去很多設計是由工程師完成的,我們這類人,普遍有點缺乏對人性的理解(no offense ),一個很典型的邏輯是:“我自己是人,所以我瞭解人。我的設計自己能理解,因為我是人,所以別的人也能理解。如果別人不會用,就去看看文件就好了(此時還有一個嫌棄臉)”。

當我們覆盤一些故障時,經常會得出「使用者操作不當」的結論,但是這真的是根因嗎?我在之前的公司曾經歷過一個事故給我留下了深刻的印象:當時內部有一個自己做的分散式檔案系統,就像所有的檔案系統一樣,它有一個 shell,可以支援一些 UNIX Style 的命令操作。
有一次,一個工程師執行了一行命令:rm -rf usr local/...(注意 usr 後邊的空格),然後系統很聽話的開始刪除自己...最後這件事情的覆盤並沒有責怪這個操作者,而是懲罰了這個系統的設計者(當時那個公司的老闆),因為這是個壞的互動設計,哪怕在刪除重要資料夾前確認一下或者通過許可權系統保護一下都不至於發生這個事情,機器確實在按照邏輯工作,這個地方也沒有 Bug(甚至這個刪除還很高效,畢竟分散式系統 LOL)。
在後來作為工程師漫長的歲月中,我漸漸理解到一個道理:最好的工程師能在邏輯和感性中間找到一個平衡,良好的設計源於對技術和心理的理解,畢竟我們是在為人寫程式。
作為軟體的使用者,我們與其說是在使用,不如說我們是在和軟體「對話」。那既然是對話,那麼就意味著這是一個互動的過程,什麼是一個好的互動體驗呢?我試著總結一些寫給軟體設計者的原則,試著第一次幹這事,不排除以後會補充。

沒人讀文件:一條命令啟動和探索式學習

承認吧,沒有人會看說明書。我們拿到一部新的 iPhone 時候,第一反應一定是開機(很神奇吧,我們似乎下意識就知道開機鍵在哪)肯定不是看說明書找開機按鈕,開機就開始通過手指來探索新的世界,很淺顯的道理,為什麼在系統軟體領域就要先熟讀文件才能上崗呢?

我經常教育我們年輕的產品經理:“你的使用者充其量會在你的 GitHub 首頁或者文件的 Quick Start 部分上停留 10 秒,甚至連看完這個文件的耐心都沒有,他們的潛意識會尋找「深色背景的字」(shell 命令),然後把裡面東西複製到自己的終端裡看會發生什麼,除此之外啥都不會做,如果這第一條命令失敗了,不會再有後面什麼事了,所以記住你只有一次機會”。
一個小例子就是當時在做 TiUP(TiDB 的安裝部署工具)的時候,我反覆告誡 TiUP 的產品經理,首頁裡不要廢話,就一句命令,貼進去就能用:

TiUP 的首頁(tiup.io)截圖

其實這個例子可以更延展一點,我記得疫情之前有一年我在布魯塞爾參加 FOSDEM,晚上在會場附近的酒吧和一位來自英國的 DevOps 聊天,可能也是喝多了,他說:“不能用一個 apt-get install 就安裝成功的系統軟體不是一個好軟體。”,話糙理不糙。
那你可能要問,如果確實有一些資訊或者概念需要傳遞給使用者,如果用認知心理學裡面的概念,可稱之為構建 Mental Model(心智模型),最好的方式是什麼呢?我自己的經驗是:探索式的學習。支援這種認知構建模式的系統通常需要有 Self-Explanatory 的能力,即告訴使用者第一步(例如 iPhone 的開機)之後使用者的每一步都能夠利用上一步行為的輸出,決定下一步的行為完成學習。
舉個例子:MySQL 的系統表想必 MySQL 的使用者都不會陌生,你只要用一個互動式的 mysql-client 連結到一個例項上,也不用等著系統告知 INFORMATION_SCHEMA 裡面有什麼,只需要使用者 SHOW TABLES 一下就知道了,然後再使用 SELECT * FROM 語句就可以一步步探索 INFORMATION_SCHEMA 裡面具體表的內容。這就是一個 Self-Explanatory 的絕佳例子(這個例子裡面有個前提就是 SQL 作為統一的互動語言)。
另一個特別好的例子是 Telegram 的 Botfather,我相信給 Telegram 寫過機器人的朋友一定會對 Botfather 的好用程度印象深刻,我放一張圖你就懂了:

用 Telegram 的 botfather 建立聊天機器人的過程

Telegram 是一個聊天軟體,Botfather 巧妙的利用了 IM 的互動模式應用到了一個相對枯燥的 bot 開發流程裡面,而不是冷冰冰的丟給使用者一個 URL https://core.telegram.org/bot... ,讓使用者自己研究去。
這一節最後一句話想送給大家,有一個無從考究的都市傳說是這麼說的:魚的記憶時間只有 7s,我想說,人也一樣。祝你做出一個 “魚” 都能用好的軟體。

幫使用者多想一步,告訴使用者半步,讓使用者自己走半步

我很喜歡看科幻小說,很多科幻小說探索的一個終極哲學話題:我們是否真的有自我意識?儘管我們認為我們有,但是在軟體輸出 Unknown Error 的時候,你肯定希望有一個聲音告訴你接下來該怎麼辦,對吧?一個優秀的基礎軟體,在輸出負向反饋的時候,最好的做法就是建議開發者接下來該幹嘛。我舉一個很經典的例子,所有的 Rust 開發者都有過被編譯器調教的日子,但是這個過程嚴格來說其實並不痛苦,比如,看下面的截圖:

Plain Text
error[E0596]: cannot borrow immutable borrowed content `*some_string` as  mutable
 --> error.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- use `&mut String` here to make  mutable
8 |      some_string.push_str(", world");
  |     ^^^^^^^^^^^ cannot borrow as mutable

之所以不痛苦是因為編譯器明確告訴了你哪裡有問題、原因,以及下一步應該幹嘛,普通編譯器可能列印一個 cannot borrow as mutable 就仁至義盡了,但是一個好體驗的編譯器會多幫你想一步。

回到自我意識的問題,我之前聽過一個段子:一個測試工程師走進一家酒吧,要了 NaN 杯 Null,一個測試工程師化裝成老闆走進一家酒吧,要了500杯啤酒並且不付錢,一萬個測試工程師在酒吧門外呼嘯而過,一個測試工程師走進一家酒吧,要了一杯啤酒';DROP TABLE,最後測試工程師們滿意地離開了酒吧,然後一名顧客點了一份炒飯,酒吧炸了 LOL。這個故事告訴我們,作為軟體設計者,你永遠沒有辦法窮舉使用者的想法,與其讓使用者放飛想象力,不如你自己設計好故事線,一步步讓使用者跟著你的思路走。但是為什麼還要留半步?
我的答案:
「參與感」會帶來幸福感,人有時候挺矛盾的,一邊希望機器自動幹完所有的事,一邊還期待自己有主動權。有時候即軟體已經知道下一步一定是做某些事情,但是留下臨門一腳讓操作者完成相當於把成就感都賦予了操作者。
選擇的權利交給操作者,尤其在面對一些單向門的決定時,go or no-go 還是應該交給人。
對於這點,我還有幾個小建議:

  1. 對於一些操作可能會引發多個連續操作的模式(例如 terraform 的部署指令碼,或者叢集變更之類的功能),提供一個 Dry Run 模式是必要的,只輸出操作,不執行操作。
  2. 對於上面這種批處理型的操作,儘可能設計 save point,不用每次都重新來(類似斷點續傳),體驗會好很多。
  3. 遇到真的 Unknown Error 要輸出各種幫助 Debug 的上下文資訊,最後在錯誤日誌裡提示使用者到哪個連結提 Github Issue,然後最好在 URL Link 裡幫使用者把 Issue Title 填好(讓使用者自己決定是不是發 Issue)。

    統一語言:控制器和控制物件

我訪談過很多系統工程師,我有個必問的問題:你心中最好用的(資料庫) cli 工具是哪個?絕大多數幾乎下意識的回答 redis-cli。其實我自己也會給出同樣的答案,後來我想這是為什麼呢?

「控制器」-「被控制物件」是一個在基礎軟體中非常常見的模式,就像我們在操作電視機的時候,絕大多數時間是通過遙控器一樣,所以可以認為使用者對電視機的第一和大多數觸點其實是遙控器,所以類比到基礎軟體中,對於控制器的設計其實非常關鍵,做好控制器,我覺得關鍵點是:

  1. 構建統一的互動語言
  2. 自洽且簡潔的概念模型

我稍微用 redis-cli 作為例子解讀一下。使用過 redis-cli 的朋友都知道,所有的操作都遵循 [CMD] [ARG1] [ARG2] ... 的模式,在 redis-cli 沒有例外,不管是運算元據,還是修改配置,所有的一切都在一個統一的互動語言下,而且這個語言一目瞭然,而且這個語言裡面有一些很自然的約定,例如命令(CMD)永遠是幾個不包含符號的字母組成。

Bash
redis 127.0.0.1:6379> SET k v
OK
redis 127.0.0.1:6379> DEL k
(integer) 1
redis 127.0.0.1:6379> CONFIG SET loglevel "notice"
OK
redis 127.0.0.1:6379> CONFIG GET loglevel
1) "loglevel"
2) "notice"

redis-cli 的互動例子

其實這點在剛才提到探索式學習那節 MySQL 的例子也是一樣的,SQL 本身就是一個統一的互動語言,只是沒有 Redis 這麼直觀。
第二點是概念模型,Redis 的優勢在於它是一個 Key-Value 資料庫,所以概念很簡單:一切都是 Key-Value,觀察它的 cli 工具,你仔細品一品就知道,作者在嘗試將所有的功能和互動都往這個 Key-Value 的模型上對映,這個是很自然的,因為我們之所以會使用 redis-cli,首先是我們接受了 Redis 是一個 KV 資料庫的現實,所以在使用 redis-cli 的時候的一個自動就成立心智假設就是 Key-Value 模式,這在使用 cli 的時候一切的操作都會變得很自然。這一點在很多優秀的資料庫軟體裡面應用的很多,例如 Oracle,理論上可以依賴 SQL 來對軟體本身做所有操作,因為使用者只要在使用 Oracle 就預設應該是知道關係模型和 SQL。
說了正面的例子,我們聊個反例:大家知道 TiDB 主專案(不包括其他工具,例如 cdc、binlog)至少有 3 個 Controller 工具:tidb-ctl tikv-ctl pd-ctl,雖然 TiDB 確實是一個由多個元件組成的分散式系統,但是對於使用者來說,多數時候使用物件其實是 TiDB 作為一個整體(資料庫軟體),但幾個 ctl 的使用方式都不太一樣,比如說 pd-ctl 是一個可互動式的控制器,而且影響的範圍大概是 pd 本身和 TiKV,tikv-ctl 的功能上也有一些交集,但是隻是針對單個 TiKV 例項使用,這點太令人費解了,TiKV 明明是一個分散式系統,但是 tikv-ctl 卻是一個針對單點的控制器?那麼控制 TiKV 到底應該用的哪個 ctl 呢?答案:多數時候用 pd-ctl(驚不驚喜,意不意外?)。
就像你有一個電視機,但是需要用三個遙控器來控制,而且真正控制電視的那個遙控器叫做:機頂盒,這種問題在日常生活中大家都認為是一個理所應當的設計問題,但是在基礎軟體領域大家的容忍度怎麼似乎突然就變高了?

No Surprise: 不怕麻煩,就怕驚喜(驚嚇)

我不知道是否是一個普遍現象,基礎軟體的使用者在面對錯誤(尤其是因為壞互動造成的),通常會先自責和內疚,認為是自己的問題,很少會歸因於軟體。尤其是當能夠比較熟練的操作一些複雜又分裂的軟體的時候,很多人會覺得這是一種「技能」,畢竟沒有人願意別人看著自己的笨拙操作。

這背後其實有著很深層次原因(Hacker Culture 裡面多少有點崇尚複雜的傾向),但是我想說:這就是的軟體的問題!就像我從不避諱說我就不會用 gdb,不是因為我智商不行而是因為這個東西真是太難用了。
但是我見過很多人真的是以熟練使用命令列 gdb 作為炫耀的資本,回到前面提到的那個反例,我在一個 TiDB 的深度使用者那邊觀察他們的操作員做日常的運維,這個操作員非常熟練的在各種 ctl 之間切換和操作,他不覺得有啥問題,甚至覺得有點厲害,後來我想了下,人的適應性還是很強的,真正讓人困擾的事其實並不是麻煩,而是當你在對系統做出一個操作的時候,通常會帶著一個下意識的假設,例如一個功能的名字叫「xx 開關」的時候,使用者在開啟開關的時候的預期應該是有一個正反饋,但是如果結果並不是這樣的話,使用者會非常有挫敗感。這裡有個真實的故事,我們在 TiDB 5.0 裡面引入了一個新功能,叫做 MPP (Massively Parallel Processing),即大規模並行處理,我們有個開關配置叫做:tidb_allow_mpp


不知道大家有沒有注意到問題:作為一個開關型的配置,當設定成 OFF 的時候,是一個 100% 的負反饋,這沒有問題,但是問題在設定成 ON 的時候,這個功能是否啟用會依賴優化器的判斷,也就是有一定概率 MPP 功能不會生效,這就像一個房間裡有個控制燈的開關,當你關的時候,燈一定不會亮,當你開開關的時候,燈不一定亮(燈覺得房間內的光線足夠,沒必要亮...),你一定不會覺得這個燈智慧,你一定會覺得燈壞了。上面這個配置的一個更好的寫法應該是:

tidb_mpp_mode = ON | OFF | AUTO

這個寫法我都不用解釋,你也不用看文件,是不是一眼就明白怎麼用?好配置應該是自解釋的。通常來說,配置項是破壞使用者體驗的重災區,後邊講反饋的時候展開講講。

UNIX 哲學裡面有一條「安靜原則」,說的是如果程式沒什麼特別事情要表達,應該保持安靜。具體的一個表現就是鼓勵命令列程式如果成功執行,不需要輸出東西的話,就直接以 0 作為 return code 退出就好了,其實對於這一點我是持保留意見的,使用者的行為如果是符合預期的結果,應該用一個明確的正向反饋作為獎勵(例如列印一個 Success 都好),不要忘了人性大師巴普洛夫。

反饋:暴露進展,不要暴露內部細節

剛才正好提到了反饋,我覺得將反饋稱為好體驗中最重要的一環都不為過。學過控制論的朋友的都知道反饋是非常重要的概念,前面提到的 Self-Explanatory 之所以是個好體驗就是因為反饋的及時性。

但是我驚訝的是,很多基礎軟體在互動反饋部分設計得糟糕得令人髮指,舉一個我熟悉的例子,某些資料庫軟體在接收到一個複雜查詢的時候,當敲下回車,通常就 Hang 在那裡了,可能確實資料庫程式在後邊辛苦的檢索和掃描資料,然後隔了幾分鐘直接返回一個結果(或者掛了),過程中並沒有反饋掃描了多少資料和預期要掃描多少資料,其實這個體驗是很差的,因為這個資訊就是進展(這點上 ClickHouse 做得很好)。反饋是需要精心設計的,我的幾個經驗是:
反饋一定要即時,最好是敲完回車後 200ms 內一定要有反饋(人的生理反應時間,超過這個時間反饋人就會有卡頓感),順滑的感覺是靠反饋創造的。
反饋進展,不要反饋細節,不要反饋需要上下文才能讀懂的細節(除非是 Debug Mode),這裡給出一個我們自己的反例(https://asktug.com/t/topic/2017):

Bash
MySQL [test]> SELECT COUNT(1) AS count, SUM(account_balance) AS  amount, trade_desc AS type FROM b_test WHERE member_id = 「22792279001」 AND detail_create_date >= 「2019-11-19 17:00:00」 AND detail_create_date < 「2019-11-28 17:00:00」 group by trade_desc;
ERROR 9005 (HY000): Region is unavailable

這個 Case 壞在哪裡呢?很顯然,對使用者來說,Region 是一個 TiDB 內部概念,一個很自然的問題是:什麼是 Region(我在前面埋了個伏筆,不知道你注意到沒有)?為什麼 Select 資料和 Region 相關?為什麼 Region is unavailable?我該怎麼解決這個問題?暴露給使用者這個資訊是無用的,反而給使用者創造了噪音。這個 Case 的原因是 TiKV 太忙,無法返回需要的資料,一個更好反饋應該是:具體的哪臺 TiKV 因為哪些資料(用使用者能理解的形式,如:哪張表,哪些行)讀取不出來是因為 TiKV 太忙,最好還能告訴使用者為什麼忙,怎麼解決,實在解決不了至少貼個 FAQ 的連結(我見過有軟體直接貼 StackOverflow 的 Search URL 的 LOL)。

對正反饋設定一些 milestone,例如一個伺服器程式開始正常對外提供服務的時候,列印一個 Ascii Art,不同日誌級別用一些帶顏色 Label,這是給使用者一個明確訊號,這點 redis-server 做得很好。通常對於可互動命令列程式的反饋還是容易設計的,一個非常麻煩的事情是,基礎軟體通常非常依賴配置檔案,配置的問題就是修改配置到確認生效的反饋週期通常很長,一個經常的場景是:修改配置 - 重啟 - 觀察效果,而且通常配置是儲存在配置檔案裡面,這也造成修改檔案操作的反饋感是極差的,因為使用者也不知道到底這個操作有沒有生效,尤其是一些配置的生效並不是太明顯,一些比較好的實踐如:程式在啟動的時候列印一下讀取了哪個配置檔案以及這個配置檔案的內容是什麼;設計一個類似 print-default-config 之類的命令列功能,直接輸出模板配置,省得使用者自己 Google。

另外對於分散式系統來說,配置的問題更加複雜,因為存在並不是本地配置和全域性配置的區別,以及更新後的配置分發的問題,包括滾動重啟的問題(重啟程式才能讓配置生效本身就不是一個好設計),老實說目前我還沒有特別好的方案,可能的思路是是使用類似 etcd 這樣的分散式全域性配置中心或者(對於資料庫來說)通過一些全域性的配置表來實現。但是總體的原則是:集中比分散好;即時生效比重啟生效好;統一互動(修改和讀取配置的方式)比多種方式互動好。

寫在最後

終於寫得差不多了,但是這篇文章我覺得僅僅是拋磚引玉,一定還有很多好的實踐沒有總結出來,也希望有想法朋友找我一起探討,我揭曉一下最開篇留下的一個懸念,為什麼要在第一篇文章中將可觀測性和可互動性放在一起寫,其實這個是來自經典的認知心理學中的人行動的模型[3]:

當使用者使用軟體時,需要面對的兩個鴻溝:一個是執行的鴻溝,在這裡,使用者要弄清楚如何操作,與軟體「對話」;另一個是評估的鴻溝,使用者要弄清楚操作的結果。我們作為設計師的使命就是幫助使用者消除這兩個鴻溝,正是對應到文章中的可觀測性和可互動性。

設計出使用起來令人愉悅的軟體是一門藝術,也不見得比設計出一個精妙的演算法或者健壯的程式簡單,從某種意義上來說更加難,因為這要求設計者真的要有對人和軟體兩者都有深入的理解以及傾注感情,最後送給大家一段來自 Steve Jobs 的話共勉:
The design is not just what it looks like and feels like. The design is how it works.

參考:
[1] 我眼中的分散式系統可觀測性, 黃東旭, 2020
[2] Overtaxed Working Memory Knocks the Brain Out of Sync | Quanta Magazine
[3] The Design of Everyday Things, Donald Norman, 1988

相關文章