Beancount複式記賬(四):項目管理

Beancount系列文章的前三篇已經基本覆蓋了常用的複式記賬方法。記賬本身是一門經驗的學問,不僅包括賬本身怎麼記,還包括了「賬本」怎麼整理。這篇文章不涉及複雜的會計學概念,只是從更加微觀的角度來講一講我實際記賬過程中是怎麼組織的。

版本管理

文本記賬的最大優勢就是它便於使用版本管理系統,像管理代碼一樣管理賬本。最常見的代碼版本管理工具就是git了,所以我推薦使用git管理Beancount的賬本文件。使用git的好處是提升賬本的可維護性,尤其是能夠防止不小心改錯、誤刪這樣的動作,這在重構的過程中極其重要。

git如何使用我不再贅述,對於會寫代碼的人來說屬於基本技能。即使不熟悉,網上的資料和教程也是汗牛充棟了,更有Sourcetree這樣的可視化工具。

惟一需要注意的是,你需要保管好你的git倉庫,尤其是要避免盲目上傳到Github之類的網站。賬本信息屬於非常敏感的個人隱私,因爲其中可以透露出的信息非常豐富,甚至超過了日記能包含的。如果要上傳git倉庫備份,至少要使用支持私人倉庫的服務,或者自己搭建git服務器(支持SSH即可)。最好在上傳之前對整個倉庫加密,譬如使用git-crypt

標籤

標籤是一個講交易組織歸類的方法,是開支類別之外的另一個維度。每一個交易都能加上一個或多個標籤:

2019-06-01 * "奧地利航空" "東京-維也納" #2019-07-Europe-Trip
  Expenses:Transport:Airline 600 USD
  Liabilities:US:CreditCard:Citi

標籤的作用是方便查詢,在fava和bean-query中都可以按照標籤來過濾。

爲了避免重複,Beancount還提供了標籤堆棧語法:

pushtag #2019-07-Europe-Trip

2019-06-01 * "奧地利航空" "東京-維也納"
  Expenses:Transport:Airline 600 USD
  Liabilities:US:CreditCard:Citi

2019-06-01 * "奧地利航空" "維也納-莫斯科"
  Expenses:Transport:Airline 100 USD
  Liabilities:US:CreditCard:Citi

poptag #2019-07-Europe-Trip

除了標籤,Beancount還提供了一個類似的語法^,叫做鏈接(Link),本質上和標籤是一樣的作用,但是被建議用作將財務上關聯的交易組織在一起的方法。常見的使用場合是有時間跨度的一筆交易,例如匯款和收款,短期的債務,按次記錄但是按月徵收的某些銀行手續費,或者僅僅是兩個目的一致的交易。

2016-05-03 * "Chase" "取現" ^2016-05-overdraft
  Assets:Bank:US:Chase:Checking -75 USD
  Assets:Cash:USD 50 USD
  Expenses:Finance:BankFee:Overdraft 25 USD

2016-05-05 * "Chase" "還清欠款" ^2016-05-overdraft
  Assets:Bank:US:Chase:Checking 25 USD
  Assets:Bank:US:Chase:Saving

多文件組織

到此爲止我一直假設所有的Beancount記錄都是在單一文件中的,這個文件會隨着賬目的增多越來越膨脹,直到用編輯器維護不便。使用單一文件就像把一個巨大的程序寫到一個源文件中一樣,閱讀和修改都很困難。所以Beancount提供了include文件包含語法,用法和多數編程語言一樣。

include後面緊跟着要引入的文件名,路徑是相對於當前文件的。

; main.beancount
include "accounts.beancount"
include "categories.beancount"
include "books/books.beancount"

; books/books.beancount
include "2016.beancount"
include "2017.beancount"
include "2018.beancount"
include "2019.beancount"

利用這個簡單的語法,一個巨大的賬本就可以分成若干個較小的賬本組合起來了。

要注意,到目前(Beancount 2.2.1)爲止,標籤堆棧和文件包含是不能組合使用的,也就是說標籤堆棧內的include的文件不會自動加上標籤。我在Beancount的問題列表提出了這個問題,作者的答覆是也許以後會實現。

賬本劃分

接下來我終於要講到我的經驗之談了。儘管有文件包含語法,但每條記錄到底怎麼劃分還是一個見仁見智的話題。我使用了三種分割方法,分別是按日期劃分、按類別劃分和按賬戶劃分。這三種劃分方式都有道理,各有優劣。總而言之,劃分的目的是減少錯誤的可能,降低維護成本,節約注意力資源。下面我詳細說來。

按日期劃分

按日期劃分賬本是最直接的分割方法,我們可以按年份或者月份創建文件(譬如2019.beancount),每個文件內只包含這段時間內的記賬。一般來說除非是修正錯誤或者重構,舊的賬不會再修改,使用當前的賬本來記賬,把過去的賬分割儲存是一種有效節約注意力資源的方法。

除了按照日曆時間劃分,還可以結合標籤,把某些事件提取出來,最常見的是旅行。下面這個例子是我把2019年7月關於歐洲旅行的賬目全部放到2019-07-Europe-Trip.beancount中,並且結合標籤堆棧,把整個文件中的賬目標記上#2019-07-Europe-Trip標籤。

; 2019-07-Europe-Trip.beancount
pushtag #2019-07-Europe-Trip

2019-06-01 * "奧地利航空" "東京-維也納"
  Expenses:Transport:Airline 600 USD
  Liabilities:US:CreditCard:Citi

...
poptag #2019-07-Europe-Trip

使用單獨的文件來記錄某個時間段的某類事件相關的開銷的好處是,一旦這個事件結束,這個文件就可以進入封存狀態了。此外,哪怕是不使用任何可視化工具(如fava),這個文件的可讀性也非常高,甚至可以當作旅行日記了。

資產負債表

按類別劃分

按類別劃分指的是把統一類的重複記錄整理到一起,便於根據時間縱向比較。譬如說每月的工資單、房租、水電費、定期投資、以及其他自動扣費的服務。

; Tokyo-Electric-Power.beancount

2018-04-25 * "東京電力" "電費"
  Expenses:Utility 4621 JPY
  Liabilities:JP:PrestiaVisa

2018-05-25 * "東京電力" "電費"
  Expenses:Utility 4956 JPY
  Liabilities:JP:PrestiaVisa

2018-06-25 * "東京電力" "電費"
  Expenses:Utility 6648 JPY
  Liabilities:JP:PrestiaVisa

2018-07-25 * "東京電力" "電費"
  Expenses:Utility 9394 JPY
  Liabilities:JP:PrestiaVisa

以上的例子是每月25日扣費的東京電力賬目。這樣記錄的好處是每個月的開銷一目瞭然,還可以觀察不同月份的開銷變化。如果可以從扣費的服務導出賬單,就可以把單個來源放在一個文件。如果是手動記錄,這種組織方式也可以最大程度上減少漏記的可能性。

按賬戶劃分

我使用的第三種分割方法是按賬戶劃分。按賬戶劃分和按類別劃分差不多是相互對稱的兩種劃分方法,因爲這種方法是以賬目相關的賬戶爲依據,分割出了單獨的賬本。按賬戶劃分的最初目的是方便自動導入腳本,因爲許多銀行、信用卡都可以提供月對賬單,非常適合自動轉換爲Beancount格式,減少人工。

但是完全按照賬戶的缺點也顯而易見,那就是尤其和「按類別劃分」衝突,所以我的實際手段是儘可能不使用這個劃分方法。什麼時候會使用呢?我的原則是和賬戶的關聯性十分強的消費,包括銀行利息、銀行手續費、信用卡還款、返現返點、開戶獎勵、年費、賬戶間轉賬。

; AmexSPG.beancount
2019-02-01 * "American Express" "SPG年費"
  Expenses:Finance:CreditCardFee 33480 JPY
  Liabilities:JP:AmexSPG

2019-02-01 * "American Express" "Amex SPG開卡獎勵"
  Assets:Points:US:Marriott 30000 P_MRT
  Income:OpeningBonus

2019-02-22 document Liabilities:JP:AmexSPG "AmexSPG/2019-02.pdf"

2019-03-11 * "American Express" "2019年2月賬單還款"
  Assets:Bank:JP:SMBC -3254 JPY
  Liabilities:JP:AmexSPG

2019-03-22 document Liabilities:JP:AmexSPG "AmexSPG/2019-03.pdf"

2019-04-10 * "American Express" "2019年3月賬單還款"
  Assets:Bank:JP:SMBC -63935 JPY
  Liabilities:JP:AmexSPG

2019-04-22 document Liabilities:JP:AmexSPG "AmexSPG/2019-04.pdf"

2019-05-10 * "American Express" "2019年4月賬單還款"
  Assets:Bank:JP:SMBC -23718 JPY
  Liabilities:JP:AmexSPG

總結

我把我的原則總結成了以下規則,按順序判斷:

  • 如果該收支屬於一個特別的事件,那麼就將其加入這個事件對應的單獨文件,例如#2019-07-Europe-Trip
  • 如果該收支是一個週期項目,但跟某個銀行賬戶無關,則加入按類別劃分的文件,例如水電費、房租、工資。
  • 如果該收支跟某個銀行賬戶緊密相關,則加入該賬戶對應的文化,例如銀行手續費、信用卡還款、返現。
  • 不屬於以上情況,則加入按日期劃分的文件。

以上就是我目前對賬本管理的一些經驗之談,這些方法只是爲了達成我前面提到的三個目的:減少錯誤的可能,降低維護成本,節約注意力資源。如果之後我發現了更好的劃分方式,我會對賬本重構,到時候再來更新這篇文章。

在以後的文章中(如果有的話),我還會介紹更多實際的記賬方法,包括出差記賬、更細緻的資產記賬,價格追蹤,股票交易和多人記賬。歡迎加入Beancount中文討論:t.me/beancount_zh

以下鏈接是其他介紹:

Beancount複式記賬(三):結餘與資產

先前一篇文章裏我介紹了Beancount的基本記賬方法和一切常見的規範。這篇繼續講述Beancount的常用語法,主要內容是對賬和資產折舊。

對賬

Beancount的語法檢查保證了每一筆交易的借記和貸記是平衡的,這已經可以避免許多會導致「賬不平」的錯誤,但是對於數額本身的錯誤,或者某調賬目漏記並沒有辦法。這就是爲什麼我們要定期對賬。

資產負債表

所謂對賬,就是看每個賬戶的結餘(Balance)是否正確。每個賬戶的餘額可以在資產負債表(Balance Sheet)頁面中找到,可以查看樣例資產負債表

資產負債表

fava資產負債表把資產列在左邊,負債和權益列在右邊。權益即淨資產,是根據資產和負債計算出來的(除了Equity:Opening-Balances,之後會講到)。

需要注意的是Beancount的債務和權益是負數,所以並不是資產 = 負債 + 權益,而是資產 + 負債 + 權益 = 0。我之前提過一次,這是Beancount使用正數來表示借記(Debit),負數表示貸記(Credit)的結果。在傳統的複式記賬中,數字的正負號並沒有這樣的意義,無論是借記還是貸記都是正數,所以絕對值資產 = abs(負債 + 權益)也許更好理解。

在資產負債表上點擊任意賬戶,可以進入賬戶的明細界面。賬戶的明細界面列出了涉及該賬戶的每一筆交易,點開後可以看到具體的交易信息。每一行的最右側是這一筆交易後的該賬戶結餘,這個數字就是對賬的關鍵。

賬戶明細

結餘斷言

假設已知賬戶的某日結餘金額,只要在這個賬戶明細界面看一看對應的日期的最後一筆交易後結餘是否正確就可以了。如果Beancount計算出的結餘和已知的是一樣的,那麼基本上就可以確定賬沒有問題。

這個步驟看似容易,但是隨着賬目增多,對賬的負擔會很重,而且容易看錯。更嚴重的問題是,如果因爲某種原因要修改過去的賬目,已經對好的賬就不一定正確了。惟一保險的辦法是每次修改了過去日期的交易後,把涉及到的賬戶未來的結餘再全部重新對一遍。好在這個過程是可以自動化的,方法就是使用結餘斷言(Balance Assertion)。

結餘斷言就是在記賬中加入已知事實,即某個日期開始的時候的某個賬戶結餘。如果你對單元測試有瞭解,這個方法肯定不會陌生。結餘斷言的語法非常簡單,如下例所示:

2017-08-20 balance Assets:US:BofA:Checking 2298.50 USD

惟一需要留意的地方是,結餘斷言是所聲明日期開始的時候的餘額,即當日的交易不算在內。

聲明結餘斷言之後,Beancount會自動檢查斷言是否正確,如果不正確就會有錯誤出現。如果每個賬戶都有適當的結餘斷言,修改過去的交易就可以放心進行了。

文檔鏈接

有了結餘斷言,接下來就是這個斷言的事實從哪裏來。最簡單的方法當然是看看現在有多少結餘,然後直接寫上今天的日期。這種方法適合現金和其他一些不方便查詢交易記錄的賬戶。

除此之外,推薦使用銀行月結單(Statement)上面的數字。許多國家的許多銀行、信用卡都會定期發送交易明細,一般來說是每月。Beancount提供了管理這些文件的一個語法,例如:

2013-03-20 document Assets:US:BofA:Checking "path/to/statement.pdf"

在fava中這條記錄也會被顯示出來,並且提供可以點擊的鏈接。這個路徑是相對於這條記錄所在的Beancount文件的目錄,這對於多文件記賬很重要(include語法,之後會講到)。

根據個人經驗還有一個重要的提醒,就是要看清楚月結單包含哪些交易,尤其是賬單週期末的哪些。因爲很多銀行、信用卡的交易並不是即時結算的,特別是有跨國交易的時候。如果發現某些交易還沒出賬或者賬單週期開始包含了上個週期的交易,一定要注意餘額斷言的數額。

結餘調整

接下來說一說賬對不上的情況。事實上,這是一種常態,人畢竟不是完美的,錯記漏記實在是太正常了。一旦結餘斷言失敗,當然是先看自己有沒有記錯什麼,如果實在困難,或者金額差距不大自己沒那麼在乎,可以考慮用結餘調整。

結餘調整並不是非要用什麼特別的語法不可,實際上只是一種規範。這個規範是使用Equity:Opening-Balances來表示初始結餘。下面是一個例子:

2015-01-01 * "賬戶初始"
  Assets:US:BofA:Checking 3490.52 USD
  Equity:Opening-Balances -3490.52 USD

這個例子的意思是,在2015年1月1日給Assets:US:BofA:Checking增加3490.52美元結餘。假設這個賬戶之前沒有記錄的話,那麼它現在的結餘就是3490.52美元,如果有就是在原來的基礎上加上3490.52美元。這3490.52美元從哪裏來呢?Beancount的規範是使用Equity:Opening-Balances

Equity:Opening-Balances是權益類別下面的賬戶,它是淨資產的一部分。Beancount中權益是負數,所以數字減少代表了淨資產的增加。這一部分資產的來源可以理解爲表外資產,即來源不明確,在Beancount賬本中沒有更詳細的記錄。

Beancount還提供了一個更加簡易的語法pad,結合了結餘斷言,功能是把結餘調整到使得下個餘額斷言滿足。用法如下所示:

2015-01-01 pad Assets:US:BofA:Checking Equity:Opening-Balances
2015-01-02 balance Assets:US:BofA:Checking 3490.52 USD

以上的效果是,無論Assets:US:BofA:Checking之前餘額多少,在2015年1月2日開始之前都調整到3490.52 USD,差額從Equity:Opening-Balances來。

一般來說除非是調試錯誤或者導入數據過程中,否則我不建議使用pad這個語法,因爲它會讓結餘斷言喪失一定的準確性。使用pad的風險是,如果自動調整的數額過大,當修改了過去其他的賬目導致需要調整的數額發生改變的時候,Beancount並不會出現任何警告和錯誤。我更傾向於把需要調整的金額明確寫出來,這樣一旦變化就會有錯誤提醒,避免更大的錯誤。

資產購入與折舊

生活中有一些交易我們需要考慮到底要記爲花費,還是資產的購入。在光譜的兩端一般沒什麼爭議,譬如喫飯肯定是消費,買房無疑是購入資產。中間許多類別就不一定了,這取決於個人的偏好和目的。

拿買汽車作爲例子,無論是新車還是舊車,許多人在開一段時間以後會選擇賣掉。如果我們把買車記爲消費,賣車記爲收入,這本身並沒有任何錯誤。問題是,對很多人來說汽車還是一筆不可忽略的資產,如果直接記爲消費,沒有對應的資產入賬,那就意味着淨資產突然大幅減值。幾年後賣出,淨資產又突然增加。

要解決這個問題,我們就要把汽車記爲一項資產,下面是例子:

2015-01-01 * "豐田汽車" "買入Corolla"
  Assets:Car:ToyotaCorolla 20000 USD
  Assets:US:BofA:Checking -5000 USD
  Liabilities:Loan:Car -15000 USD

2019-01-01 * "二手車商" "賣出Corolla"
  Assets:Car:ToyotaCorolla -20000 USD
  Assets:US:BofA:Checking 10000 USD
  Expenses:CarUsage 10000 USD

上面這個例子是2015年1月1日貸款買入了價格爲$20000的汽車,並計入資產Assets:Car:ToyotaCorolla。中間省略還貸款的過程,四年後2019年1月1日,把車賣給了二手商,獲得$10000,剩下的$10000就是三年來用車的消費了。

這個方法對買賣交易之間的這段時間內淨資產計算仍然不夠準確,並沒有完全解決淨資產跳變。因爲汽車的使用是三年來平均花費出去的,而不是最後賣的時候一下子花了$10000。要解決這個問題就要引入定期進行折舊(Depreciation)計算。一般的會計方法中把因爲資產使用或者隨着時間自然減值稱爲折舊,我記爲Expenses:Depreciation:CarUsage

接下來需要決定的是折舊的週期,即把車的使用費按照多大的粒度來記錄。這個完全因人而異,也因金額的大小而異。對於車我可以選擇按年折舊,因爲在許多國家(譬如美國),工作原因的資產折舊是可以按年抵稅的。如果希望每個月的花銷更加細緻,那麼按月折舊也是一個方案,只是需要多記錄幾筆而異。

2015-12-31 * "Corolla折舊"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

2016-12-31 * "Corolla折舊"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

2017-12-31 * "Corolla折舊"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

2018-12-31 * "Corolla折舊"
  Assets:Car:ToyotaCorolla -2500 USD
  Expenses:Depreciation:CarUsage

下一個問題是,每次折舊減值多少。這個問題就是會計上可以操作的空間了,因爲實際的價格只有在出售的時候纔知道。一般會計準則是預估一個折舊年限,然後以此爲根據來折舊。譬如說,我們預期汽車的壽命是10年,即10年後該車的價值清零,這樣我們可以按照每年10%的折舊率每年減記。最終當實際賣出的時候,我們再根據賣出價格做調整,或者差額記爲其他類別。如果有特別的事件發生,還可以另外單獨折舊。例如發生了車禍,車的估值大幅下降,可以在此時額外減值。

Beancount並沒有自動折舊的功能,每一筆都是要自己寫的。如果怕忘了,其實可以把未來日期的折舊也寫上。另外還有第三方的插件beancount-interpolate可以嘗試使用。

到此爲止我差不多已經講到了日常使用中的大部分語法。如果把這個系列文章看成一個編程語言教程,那麼語法已經差不多講完了。下一篇文章我會講述「真是世界」裏的Beancount使用方法——把Beancount賬本看成一個項目,該如何管理。歡迎加入Beancount中文討論:t.me/beancount_zh

Beancount複式記賬(二):借貸記賬法

上一篇文章介紹了爲什麼要複式記賬,以及Beancount的基本特點。這篇言歸正轉,直接從實踐開始。

賬戶類別

複式記賬的最基本的特點就是以賬戶爲核心,Beancount的系統整體上就是圍繞賬戶來實現的。之前提到的會計恆等式中有資產、負債和權益三大部分,現在我們再增加兩個類別,分別是收入和支出。Beancount系統中預定義了五個分類:

  • Assets 資產
  • Liabilities 負債
  • Equity 權益(淨資產)
  • Expenses 支出
  • Income 收入

這五類是Beancount的約定,除此了Equity之下一些特殊的賬戶外,沒有任何預先定義的賬戶。用戶可以定義各種各樣的賬戶,Beancount對賬戶的組織是樹形的,譬如我分別有這些資產賬戶:

Assets:Cash:JPY
Assets:Cash:USD
Assets:Bank:CH:UBS
Assets:Bank:CN:BoC
Assets:Bank:US:Chase:Checking
Assets:Bank:US:Chase:Saving
Assets:Bank:JP:SMBC:JPY
Assets:Bank:JP:SMBC:USD
Assets:Broker:US:IB
Assets:Points:Airline:JAL
Assets:Points:Airline:United

Beancount對這些賬戶的組織形式如下圖:

賬戶

接下來是聲明賬戶的語法。Beancount要求每個使用的賬戶必須聲明開戶時間,格式是YYYY-mm-dd。之後是關鍵詞open,表示在這個日期開戶(或者開始記賬)。接下來是賬戶名稱,格式用:隔開的樹形語法,最後是(可以省略的)賬戶的貨幣種類。貨幣種類不需要事先定義,也沒有系統內部的定義,一般來說我們使用三字母的貨幣代碼,但其實可以用任何的名字(惟一的限制是大寫字母和下劃線)。我在這個例子中使用了USDJPYCNYCHF四個貨幣,以及我自定義的P_JALP_UA表示不同航空公司的里程。

2019-01-01 open Assets:Cash:JPY JPY
2019-01-01 open Assets:Cash:USD USD
2019-01-01 open Assets:Bank:CH:UBS CHF
2019-01-01 open Assets:Bank:CN:BoC CNY
2019-01-01 open Assets:Bank:US:Chase:Checking USD
2019-01-01 open Assets:Bank:US:Chase:Saving USD
2019-01-01 open Assets:Bank:JP:SMBC:JPY JPY
2019-01-01 open Assets:Bank:JP:SMBC:USD USD
2019-01-01 open Assets:Broker:US:IB USD, JPY, CHF
2019-01-01 open Assets:Points:Airline:JAL P_JAL
2019-01-01 open Assets:Points:Airline:United P_UA

賬戶如何組織分類完全看個人需求和喜好,譬如我先分賬戶類型,再分國家,然後是金融機構名,最後是具體賬戶。分類的作用是可視化的時候,可以看某個非字節點下面所有賬戶的彙總。

接下來是負債類別的賬戶,最常見的就是信用卡,組織方式和使用方法和資產賬戶沒有什麼區別,譬如:

2019-01-01 open Liabilities:CreditCard:US:Discover USD
2019-01-01 open Liabilities:CreditCard:JP:Rakuten JPY

在開始記賬之前,還差最後一步,就是收入和支出類別的定義。Beancount把收入支出的類別也想象成了一個賬戶,在語法上和資產、負債類賬戶沒有區別。譬如下面例子:

2019-01-01 open Expenses:Clothing
2019-01-01 open Expenses:Food:Dinner
2019-01-01 open Expenses:Transport:Airline
2019-01-01 open Expenses:Transport:Railway
2019-01-01 open Income:Salary
2019-01-01 open Income:Rebate

基本借貸記賬

有了以上定義的賬戶以後,我們終於可以開始實踐記賬了。複式記賬又叫作「借貸記賬」。之所以這麼叫,是因爲每一條記錄都至少有一條借記(Debit)和一條貸記(Credit)。可以看下面這個例子:

2019-01-01 * "日本航空" "紐約-東京"
  Expenses:Transport:Airline 1000 USD
  Liabilities:CreditCard:US:Discover -1000 USD

這個例子表示我在日本航空購買了紐約-東京的機票,消費1000美元(貸記),付款的信用卡Discover扣款1000美元(借記)。

Beancount基本的語法如下所示:

YYYY-mm-dd * ["payee"] "description"
  posting 1
  posting 2
  ...

第一行要有日期,接下來是*。收款者payee是可選的,如果*後面只有一個字符串,那就是省略了payee。從第二行開始,每一行開頭空兩個縮進,然後是賬戶名以及金額、貨幣。這裏有一個要點:要保證所有條目的總和是0,否則就會出現Transaction does not balance: (xxx USD)這樣的錯誤。這個要求很好理解,因爲花出去的錢必須和賬戶上減少的錢一樣,否則就是所謂的「賬目不平」了。

Beancount語法的靈活性在於每個記賬單元可以有任意多個條目(借記和貸記),只要保證它們的總和是0就可以。於是我們還可以這樣記錄:

2019-01-01 * "Walmart" "在超市買兩件衣服和晚餐"
  Expenses:Clothing 20 USD
  Expenses:Clothing 10 USD
  Expenses:Food:Dinner 10 USD
  Liabilities:CreditCard:US:Discover -40 USD

更加複雜的例子可能是這樣的:

2016-01-01 * "Google" "工資"
  Assets:Bank:US:Chase:Checking 500 USD
  Assets:Bank:US:Chase:Saving 1839.35 USD
  Assets:Pension:US:401k:PreTax 419.23 USD
  Assets:Pension:US:401k:PreTax 209.62 USD
  Expenses:Health:Insurance:Dental 3.14 USD
  Expenses:Finance:Insurance:TermLife 7.67 USD
  Expenses:Health:Insurance:Vision 0.98 USD
  Expenses:Tax:US:Federal 763.26 USD
  Expenses:Tax:US:Medicare 60.84 USD
  Expenses:Tax:US:SocialSecurity 260.15 USD
  Expenses:Tax:US:State:NY 212.59 USD
  Expenses:Tax:US:City:NYC 131.57 USD
  Expenses:Tax:US:State:NYDisability 1.2 USD
  Income:Salary:Regular -4192.31 USD
  Income:Allowance:TermLife -7.67 USD
  Income:Salary:401kMatch -209.62 USD

實際的記賬中,一進一出的兩個賬戶佔了絕大多數。這個時候把正負的金額寫兩遍未免有點羅嗦了,所以Beancount還提供了金額插值的功能。簡單說就是假設總和一定是0,在有N個賬戶的時候,只要求N-1個賬戶聲明金額。於是最初的例子還可以寫成:

2019-01-01 * "日本航空" "紐約-東京"
  Expenses:Transport:Airline 1000 USD
  Liabilities:CreditCard:US:Discover

在我的實踐中,我只在正好是兩個賬戶的時候纔使用這個功能,因爲可以避免重複的數字。但有多個賬戶的時候,把每個金額都寫出來有助於避免錯誤,和「防禦性編程」的理念一樣。

在上面的例子中,我們還可以看出來,所有Expenses類別的賬戶都是正數,所有Income類別的賬戶都是負數。這是Beancount及類似工具使用正數來表示借記(Debit),負數表示貸記(Credit)的結果。同理,通常Assets類是正數,Liabilities類是負數。本質上每個賬戶的數值只有絕對值有意義,正負號並沒有實際含義。

貨幣轉換

如果在去國外旅遊,免不了要進行貨幣轉換。事實上Beancount本身沒有定義任何貨幣,這也意味着你可以定義任何貨幣(或商品)。所以哪怕不出國,只要是記錄了非主要貨幣類資產,譬如投資品,代金券,航空公司里程,那麼就需要貨幣轉換了。

在使用多個貨幣之前,需要先定義「工作貨幣」。工作貨幣可以不止一個,例如:

option "operating_currency" "JPY"
option "operating_currency" "USD"

定義了工作貨幣以後,在fava界面中可以看到工作貨幣單獨列出的欄目。

Beancount貨幣轉換的語法有兩種,一種是使用@記錄單位貨幣的轉換價格,例如:

2019-01-01 * "日本航空" "紐約-東京"
  Expenses:Transport:Airline 1000 USD @ 110 JPY
  Liabilities:CreditCard:JP:Rakuten -110000 JPY

另一種方式我更常用,使用@@記錄轉換後的總額:

2019-01-01 * "日本航空" "紐約-東京"
  Expenses:Transport:Airline 1000 USD @@ 110000 JPY
  Liabilities:CreditCard:JP:Rakuten -110000 JPY

貨幣轉換不一定只在一個賬戶上。下面的這個例子是以2.5日圓每點的價格,買了10000日本航空里程,但是付款的信用卡是以美元計價的,所以兩遍都可以轉換爲25000日圓來平衡。

2019-01-01 * "日本航空" "購買里程"
  Assets:Points:Airline:JAL 10000 P_JAL @ 2.5 JPY
  Liabilities:CreditCard:US:Discover -220.0 USD @@ 25000 JPY

借貸管理

複式記賬的強大之處是每個賬戶都有狀態,而且每個操作都是原子的,這對複雜的資金進出記錄非常有幫助。

生活中一個常見的例子是朋友之間的借錢和相互墊付,就拿我最近遇到一個例子來說吧,我和X、Y三人一起出遊,從東京附近的橫須賀坐船到猿島,費用是每人1300日圓的船票和200日圓的登島費,其中船票可以用信用卡支付,而登島費只能付現金。我們一共需要付4500日圓,但是正好誰都沒有這麼多現金,於是決定我用信用卡付三人的船票,X用現金付三人的登島費,最後再結算。

2019-05-25 * "猿島" "渡輪"
  Expenses:Transport:Ferry 1300 JPY ; 個人渡輪費用
  Assets:Receivables:X 1300 JPY ; 對X應收賬款
  Assets:Receivables:Y 1300 JPY ; 對Y應收賬款
  Liabilities:CreditCard:JP:Rakuten -3900 JPY

2019-05-25 * "猿島" "登島費"
  Expenses:Transport:Attraction 200 JPY ; 登島費
  Liabilities:Payable:X -200 JPY ; 欠X的錢

第二天,三人結算完畢,X付給我現金,Y轉賬給我。

2019-05-26 * "猿島" "費用結算"
  Assets:Receivables:X -1300 JPY ; X償還債務
  Liabilities:Payable:X 200 JPY  ; 償還對X的債務
  Assets:Cash:JPY 1100 JPY ; X實際付給我的錢到賬
  Assets:Receivables:Y -1300 JPY ; Y償還債務
  Assets:Bank:JP:SMBC:JPY 1300 JPY ; Y實際付給我的錢到賬

最終可以看出來,我在Expenses:Transport:Ferry類別消費1300 JPY,在Expenses:Transport:Attraction類別消費200 JPY,信用卡扣款3900 JPY,收到了1100 JPY的現金,Assets:Bank:JP:SMBC:JPY收到了1300 JPY的轉賬。

以上這種記賬方法可以讓資金的流動一目瞭然,類別和金額也準確無誤。記錄對他人的債權(應收賬款)和欠他人的債務(應付賬款),我分別使用了Assets:ReceivablesLiabilities:Payable下面的賬戶。

應收賬款和應付賬款的另一個用途是區分付款和到貨時間。一般來說交易是當場進行的,一個賬戶的借記和另一個賬戶的貸記同時發生,但是有些時候付款和到貨並不是同時發生的,如果需要精確區分發生的時間的話,可以用這種方法把他們分成兩筆記錄。

下面這個例子是,用信用卡買日本航空里程,但是不知道爲什麼過了1個月纔到賬:

2019-01-01 * "日本航空" "購買里程"
  Assets:Receivables:JAL 10000 P_JAL @@ 25000 JPY
  Liabilities:CreditCard:US:Discover -220.0 USD @@ 25000 JPY

2019-02-01 * "日本航空" "里程到賬"
  Assets:Points:Airline:JAL 10000 P_JAL
  Assets:Receivables:JAL

這篇文章到此爲止,Beancount最基本的用法已經介紹完畢了。用這些簡單的語法,我們已經可以滿足大部分的記賬需求了,但Beancount的強大之處遠遠不止於此。

下一篇我會繼續介紹Beancount的更多用法,包括如何高效對賬和資產折舊。同時歡迎加入Beancount中文討論:t.me/beancount_zh

Beancount複式記賬(一):爲什麼

我在在Google的這四年(三)這篇文章中提到了一個「祕密武器」Beancount以及「複式記賬」的概念。我說過我要專門寫一篇文章來分享我的經驗,這篇文章就是我承諾的兌現了。

如何實現財務自由

這個章節標題有點吸引人眼球,像是成功學教程。但是我保證我下面說的內容都是嚴肅,而且有明確定義和方法的。

爲什麼要記賬,我來用一句話總結就是爲了提升對自我的認識。記賬是個人理財的基礎,更是通往財務自由的必經之路。財務自由這件事情固然是根本需要賺錢來實現,但並不是只有一夜暴富纔能實現的。財務自由其實並不是一件虛無縹緲的事情,而是每個人都可以努力達到的一種狀態,換句話說就是「退休」,只不是有人能在三十多歲退休,有人要六十歲,有人則要到八十歲。

財務自由的一般定義是由資產產生的收入不少於生活開銷。如果不知道自己有多少開銷,甚至不知道自己有多少資產、收入,即便是一夜暴富,財務自由是一件不可能的事情。

接下來我來解釋爲什麼財務自由是可以實現的,而且應該是每個人的目標。首先需要強調的概念是資產(Assets)淨資產(Net Assets)的區別,雖然我之後還會詳細解釋,但是這裏需要記住,財務自由並非對個人淨資產的要求,這也就是爲什麼不需要暴富就能實現。像所有投資都有期限一樣,個人的壽命也是有期限的。每當我想到我總有一天會死,都會覺得有些憂傷,但這是無法改變的自然規律。只要在預期的壽命之內資產(而不是淨資產)產生現金流能夠滿足生活所需的開銷,這就是財務自由。我對資產產生現金流的定義較爲寬泛,不僅包括利息、分紅、租金、版稅這類收入,還包括了直接通過資產折現的收入,即淨資產減少。

要想達到財務自由,需要三點要求:對支出的預期、對資產和收入的瞭解、對壽命的期望。這三點都是說起來容易,做起來難的,但是記賬可以幫助你很大程度上解決至少前兩個問題。至於第三個問題,纔是財務自由的根本困難所在,不過這個問題的解決方案已經超出了本文探討的內容了。

爲什麼要複式記賬

記賬這件事是那種容易讓人因爲一時衝動開始,但是很快就放棄的事情。每個記過賬人都有不同的原因和契機,但能堅持下來的則鳳毛麟角。記賬的好處是提升對自己的瞭解,解決那種不知道自己賺的錢都到哪裏去了的問題。但記賬的困難也顯而易見,那就是麻煩,還容易遺漏、錯誤。一般來說普通人對記賬的理解就是每筆消費都幹什麼了,所以就是每一筆消費都有一個金額和類別就可以了。這種記賬方式一開始簡單,但能帶來的價值有限,只是開支記錄而已,長期看來難以說服自己爲了這些價值而忍受麻煩的記賬過程。

相比普通的流水賬,複式記賬的核心理念是賬戶之間的進出關係,要求所有的記錄全部入賬,它可以保證賬目的完整性和一致性。複式記賬可以提供除了開支記錄之外的損益表、資產負債表、現金流量表、試算平衡表等報表。複式記賬還可以把投資和消費輕易區分,譬如購入電腦、手機,可以作爲資產項目入賬並定期折舊。同理對各種代金券、點數積分的購入一樣要算入資產而不是消費。

雖然有各種各樣的手機應用號稱可以簡化記賬,它們把用戶交互做得更加簡單友善,有的還可以從銀行賬戶、信用卡公司直接抓取數據,降低心理障礙,但與此同時它們卻帶來了另一個問題,就是數據所有權、安全性和持久性的疑慮。這些工具大多都是把數據存儲在雲端的,泄漏隱私的可能性不言而喻。更麻煩的是,它們幾乎都是自己的專有格式,無法導出保存,或者哪怕可以導出也難以使用。這也意味着你不得不一直用一個產品,直到有一天倒閉或者服務關閉爲止。這對我來說是不可接受的,因爲在互聯網時代能活過十年的產品非常稀少,無論是像Google這樣隨意關閉服務的大公司,還是隨時有可能倒閉的小公司。複式記賬的價值在於數據的完整性,我對其數據壽命的要求是二十年以上。

複式記賬是一種劃時代的發明,這個發明被認爲是源於中世紀的地中海城邦(意大利或者埃及猶太人)。複式記賬技術成爲中世紀至大航海時代複雜貿易的支柱性工具,使得複雜的合約、信貸成爲可能。後世會計學、金融學的許多概念都來自於複式記賬,可以說體系化的資本主義是從複式記賬的實踐中誕生的。

如何複式記賬

網上對於複式記賬的文章也是汗牛充棟了,但各種晦澀難懂的名詞可能會嚇跑人。我不打算在這裏介紹什麼是複式記賬,因爲很難一句話說清楚,說得複雜了又沒有意義。其實你並不需要懂很多纔能開始複式記賬,只需要一些最基本的概念就可以了。這個概念就是會計恆等式。

會計恆等式

作爲會計學的核心,複式記賬是一種實踐中誕生的技術。除了最基本的會計恆等式之外,複式記賬只有規範,沒有對錯。那麼什麼是會計恆等式呢?最基本的形式就是:

資產 = 負債 + 權益(淨資產)

這個等式對於沒有接觸過會計學的人來說可能不太容易理解,但是記賬實踐過馬上會明白的。理解的難點在於,會計學上的「資產(Assets)」和一般人對一個人富有程度的理解不太一樣,因爲資產是負債和權益的總和,負債(Liabilities)也是資產的一部分。

更容易理解的部分其實是權益(Equity),在個人理財的上下文中,又和淨資產(Net Assets)是等價的,也就是經常說的「個人淨值」。淨資產需要由總資產排除負債,一個人到底是否富有,看的是淨資產,而不是資產

舉例說明,一個人首付20萬美元,貸款80萬美元,買了價格爲100萬美元的房產。假設這個人沒有別的資產和負債,那麼他的資產就是100萬美元,負債是80萬美元,而淨資產是20萬美元。在房價沒有變化的情況下,買房對一個人的淨資產沒有什麼影響(忽略手續費的話),而他的資產和負債都暴增,也就是所謂的「擴大資產負債表」。

如果你理解了會計恆等式,那麼複式記賬的一切知識障礙已經掃除了。

使用Beancount

再說一遍,複式記賬在於實踐,在開始實踐之前學很多艱深的會計學概念沒有任何意義。要開始實踐,就要有工具上手了。在衆多工具中我推薦使用Beancount,原因如下:

  1. Beancount是一個開源工具,用Python實現的,可以本地運行。
  2. 賬本是一套基於文本的語法,方便存儲和管理,個人擁有全部的數據,還可以使用Git管理。
  3. 賬本的語法很規範,也具備靈活度,像編程語言一樣可以嵌套引入,也有語法高亮和代碼檢查工具。
  4. 有完整的命令行工具鏈和可視化工具fava,還有基於SQL的查詢和報表生成。
  5. 沒有預先定義的類別、貨幣等現實世界概念,可以輕鬆實現多幣種記賬,包括各種點數、虛擬貨幣。

除了Beancount,Plain Text Accounting網站還列出了其他的開源工具比較,類似的具有競爭力的開源工具還有Ledger和hledger,其中Ledger是這類工具的開創者。無論使用裏面介紹的哪個工具,其基本理念都差不多,即記錄賬戶之間的資金流動。最重要的是,你是賬本的所有者,如果不喜歡一個工具了,可以輕易轉換到另一個工具。雖然它們語法有些許區別,但寫一個腳本來轉換不難。

使用Beancount之前首先需要Python 3運行環境。Beancount可以輕易從PyPI獲得,使用以下命令:

pip install beancount fava

上面的命令其中beancount是核心包,包括了命令行工具,fava是網頁可視化工具。這裏是一個示例賬本:鏈接。示例文件可以在Beancount的Bitbucket上下載

下載之後可以在命令行中運行:

fava example.beancount

即可看到:

Running Fava on http://localhost:5000

Beancount

至此爲止Beancount環境就已經設置好了。

這篇文章到此爲止,下一篇我會介紹Beancount具體的使用方法和操作經驗。同時歡迎加入Beancount中文討論:t.me/beancount_zh

在Google的這四年(五)

十六、熟悉而又陌生的國度

2018年2月,我正式登陸日本,在這個熟悉而又陌生的國度開始新的生活。

雖然之前來過日本旅遊兩次,但是理解都浮於表面,只是驚嘆於日本街道的乾淨和優良的秩序。哪怕現在我也很難說我對日本有多麼深刻的認識,只是浸淫在這樣的文化中,體驗更加真實。日本是我生活的第五個國家(前四個是中國、英國、瑞士、美國),也是第一個中國以外的東方國家。

日本是一個熟悉而又陌生的國度。熟悉之處在於日本深受中國古典文化的影響,近代至今則反過來向中國輸出了大量文化。陌生之處在於,相比漢字文化圈的朝鮮半島和越南,日本對中國文化的吸收總是有分寸。這種感覺日本文化中隱隱約約有大唐遺風,但又絕對不是另一個「小中華」。日本文化的複雜性就像日文漢字和假名混合書寫一樣,一個完全不懂日文的中國人總是能看懂一部分,而其關鍵卻藏在由假名構成的語法細節中。

除去中國文化的倒影之外,日本還是東亞第一個全盤西化的國家。所謂「東方國家」只是地理上的定義,日本在精神上早在一百年前就「脫亞入歐」了。

百年前東京鳥瞰

上面這幅繪畫我還以爲是歐洲某個城市的鳥瞰圖,但這竟然是一百年前的東京市中心!儘管中國也有上海、天津租界,但西方建築的規模和宏偉程度都遠遜東京,更別說東京根本不是殖民地租界了。而這些建築下落很遺憾,在太平洋戰爭期間美軍密集的大轟炸之下已經蕩然無存。

十七、初上陸

跟以前幾次不一樣,來到日本的第一個挑戰就是語言障礙。之前在英國和美國因爲是英語國家所以幾乎沒有什麼交流問題,即使有也在短時間內就克服了。瑞士雖然講瑞士德語,但幾乎人人都會英語,再加上蘇黎世三成以上的外國人口(Google蘇黎世更是絕大部分),語言也一直不是問題。然而英語在日本的通行非常有限,儘管這幾年旅遊業有所改善,但生活中完全不會還是不行的。Google東京的絕大部分人還是講英語的,所以工作並沒有問題。出於希望深入瞭解這個國家的動機,我日語還是非學不可的。

我學習日語也算差不多完全從零開始,哪怕我提前認全了假名,而且研究過中古漢語(韻典網),對日語漢字音讀的規律在某種程度上是掌握的,但也僅限於此了。一年後的今天,我的日語聽說能力還是非常有限。問題很明確是在於日語的輸入還是不夠多,畢竟工作講英語,我又不看動漫、日劇、偶像團體,我對日本文化的吸收大多是閱讀日文。這裏有幾個美國人同事,他們出於對日本文化(ACG)的熱愛來日本生活。對於他們來說,日語聽說完全不是問題,難點只是在漢字而已。這也是爲什麼喜歡日本文化的中國人可以很快學會日語,因爲既有直接輸入,又天生對漢字難關免疫。

以我現在的能力,生活中和人交談是非常尷尬的局面。譬如說,在買東西、吃飯等場合,我一般都會被默認爲日本人講日語,直到我開口暴露自己身份爲止。我開口講日語後,很快對方會說出一些我聽不懂的句子,只有少數人具備簡化語法和用詞的能力調整到簡單的日語,另一些人會切換到非常蹩腳的英語,剩下的人則會直接結束交談。只有隨著日語能力的進步,盡量減少我聽不懂的部分,問題纔可以解決。

與之相反,我的美國朋友抱怨另一種局面,即許多日本人看見他們的西方臉孔就直接講英語,哪怕他日語不錯,而對方英語很差,對方也堅持講英語。如果以歐美的社會文化背景來揣度,這種現象容易被解讀爲日本人排外或者「種族主義」。但我認爲這可能是出於日本服務業接人待客的一種規範,叫做「御持て成し(Omotenasi)」,大致說就是全心全意爲客人考慮。只是由於文化背景的不同,這種爲他人考慮的結果可能事與願違,進而產生誤解。

十八、法律與秩序

在2016年美國總統大選的一場電視辯論上,主持人提到了如何解決美國大城市如芝加哥的貧困和暴亂問題。特朗普的回答簡單明瞭,就是實現法律和秩序(Law and order)。他的這個說辭被反對者批評爲暴力鎮壓,忽視背後真正的問題云云,一直到今天還有人在爭論。且不說「法律與秩序」是否真的被特朗普帶回到了芝加哥,日本真的是一塊「法律與秩序」之地。

迄今爲止日本給我最大的感受就是對秩序的崇拜。在日本,不僅僅是法律,道德和未明言的社會規範都被奉爲圭臬。有些人把這說成是日本的陰暗面,乃至是日本社會壓抑的原因。我的觀點相反,這正是日本社會最不可多得的優點。日本法律的執行非常嚴格,幾乎可以算是說到做到,按紙面最保守的方式解讀,而不像美國法律有諸多輾轉騰挪的餘地。

沒有人否認日本是一個秩序井然的社會,這體現在方方面面:包括一塵不染的大街小巷,永遠準時的公共交通,嚴守紀律的學生,各行各業一絲不苟的職員。這種對秩序的遵守大大降低了社會的摩擦,使得人與人之間相處融洽又保持必要的距離。嚴格的秩序感大幅提升了社會運轉的效率,使得現實與預期不會有過大的出入。舉例說明,人人都準時使得只有日本可以做到國內航班在起飛前15分鐘還能通過安檢,從容地走到登機口。

十九、同質與多元

在日本社會秩序的背後,支撐這些規則的是整個社會的高度同質性。同質的社會裏每個人的差異不大,有相似的價值觀和道德感,因此人和人更容易相互理解,紙面上未書寫的社會規範則更有約束力;人和人也有更高的相互信任感,信任則會降低社會成本,給全社會帶來更高的效率。

支持多元文化主義的人還總是提到多元文化會促進創新,但這個結論並沒有實際的證據支持。先不說文化衝突帶來的社會成本,多種不同的文化混合在一起是否更能更創新,都是個值得商榷的問題。

與高度同質的日本相反的另一端是五彩繽紛的印度。這塊神奇的次大陸上有數百種不同的語言、宗教和族羣。這些不同文化背景的各色人在印度聯邦制度下有限度地和平共處,內部秩序非常薄弱。族羣複雜的印度難以有內生的秩序,歷史上一直需要外來統治階級的不斷入侵來建立秩序,從雅利安人到大英帝國殖民歷經數千年。這也就是爲什麼印度人總是說,如果當年沒有英國,今天的印度不可能是一個統一的國家。凡是去過印度的人都會對那裏無處不在的混亂留下深刻的印象,這是其多元文化帶來的後果。

多彩的印度

日本從來不會爲了多元化而多元化, 哪怕是如今逐漸放寬移民政策,也要求移民融入日本。這爲崇尚多元文化主義的英美國家提供了一個色彩鮮明的另類樣本,在某些方面與瑞士不謀而合。

同質的日本

下一篇:在Google的這四年(六)(待續)

在Google的這四年(四)

十二、現金流壓力

從瑞士到美國以後,工資大減近四成。雖然這是預期之中的事情,畢竟瑞士的基本工資真的很高,而稅又很低。但真正到美國看到自己收入大幅縮水以後,還是有些難以接受。剛剛搬過來的時候還有各種事情紛至沓來,讓我一度情緒低落。不過幾個月後我終於適應了新的環境,同時因爲升職和調薪我的收入超過了之前在瑞士的收入。我在上一篇在Google的這四年(三)裏面提到美國的生活給我帶來的一大收穫就是對資本、投資、現金流的理解,不得不說其實也是因爲一開始的資金壓力給我帶來的。

我的資金壓力有很大一部分原因其實是投資過於貪婪帶來的。我是年中到美國的,但又想把一年的401k(稅前和稅後)、IRA、HSA這些賬戶全部交滿,結果就是每次工資到手只有幾百美元。雖然有了複式記賬以後從賬面上看我的淨資產是在增加的,但現金流的確非常可憐,困難到信用卡難以償還的地步,我可謂是陷入了「流動性危機」。當然我還是不會信用卡最低還款繳納鉅額利息的,因爲我通過學習瞭解到了一個強大的金融工具:「保證金借貸(Margin loan)」。保證金借貸其實就是把自己的金融資產作爲抵押,獲得一筆貸款,這筆貸款的利率通常非常低,甚至低於政府隱性擔保的房貸。我印象中在2016年我付出的年化利率在2.5%左右。這個方法對我來說非常實用,因爲我有股票、基金這樣高流動性的產品,雖然迫不得已我可以變現,但有了保證金借貸以後我並不需要變現了。在美國如果有賬面盈利變現是要繳資本利得稅的。保證金借貸本質上和提高投資槓桿是一樣的,只要我借得不太多(槓桿不太高),市場波動風險就不會影響到我。

十三、搜索基礎架構

我繼續參與了Google搜索的項目,但是從前端轉到了後端。所謂Google搜索的前端,簡而言之指的是用戶查詢時在線即時計算的部分。這包括關鍵字的語意理解、信息檢索、排名算法等等。後端則是離線的部分,指的是網址發現、頁面抓取、索引建立等這些不在用戶查詢時即時計算的工作。這個劃分非常粗略,也不準確,Google的真實搜索系統要複雜得多。

在Google,大規模的離線數據處理系統也可以做到低延時,我做的恰恰是其中最實時的部分——Google搜索的即時索引系統。這個系統負責爲新聞、Twitter這種高頻率、低延時的內容建立索引。相比之前在瑞士的時候參與的語意理解項目,這個工作偏向於系統架構的設計。Google大規模數據處理的引擎很多,絕大多數都是爲海量數據高吞吐量設計的,而這個系統則對實時性的要求很高,工作內容極具挑戰。因爲高實時性的要求,而且對Google搜索結果影響十分關鍵,這個系統有專門的運維工程師(Site Reliability Engineer)團隊。此外我也不得不身兼一部分運維工程師的工作,某些時候要隨傳隨到(Oncall)。作爲開發工程師我的工作的重要部分就是讓這個系統儘量穩定,這樣也可以減輕自己的工作量。

即時索引

Google搜索對即時新聞的支持早在十多年前就開始有了。這是一個歷史悠久的系統,代碼經過多代演進,一環扣一環。它的學習曲線要比之前的項目(語義理解)陡峭得多,光是把系統的每個部分在幹什麼搞明白我就花了差不多大半年。

在這個項目的兩年裏,我的工作可以分兩類,一類是維護現有的系統和在現有系統上開發新功能,另一類是開發下一代的系統取代現有系統。Google的許多重要項目每幾年就要有一次更新換代。這樣的項目還是很吸引人的,因爲一旦成功不僅會有個人的成就感,還有更大的升職機會,無論是對於普通工程師還是項目的管理層。

十四、項目的風險

新項目是有風險的,因爲成熟的項目通常都有巨大的技術負債,而且又十分重要,新的系統必須在各個方面都要比原來更好纔能說服決策層下決心取代舊的系統。這其實對決策層來說也是一個長遠利益和短期利益平衡的問題。Google搜索的任何大改動都有可能會給這個市值幾千億美元的公司帶來重大風險,對短期來說可能又好處有限。而長期來看項目都是有生命週期的,隨着「老化」會讓開發越來越困難,陷入停滯,所以必須再適當的時候引入新的系統。新項目失敗的風險總是存在的,可能來自技術難關——新的系統並沒有比舊的更好;可能來自利益衝突——新的系統使某些團隊的利益遭到損害;可能來自資源的投入——決策層可能會在某個時候改變人選或者改變注意,撤回對項目的支持。這些原因都會導致項目的流產,但對於基層的工程師來說基本上是不可控的。

技術原因的失敗並不罕見,因爲Google前人開發的系統已經在各個方面都很優異了,可能只是使用的技術棧比較舊而已。新的技術棧並不一定就比舊的更好。要證明新的系統比舊的在某關鍵方面要好很多,而且其他各個方面也都不能差,是一件很難的事情。如果不可能,就要退而求其次,證明新的系統「利大於弊」。這就更難了,什麼是利什麼是弊,對誰來說是利還是弊,都是需要考慮的問題。

好在目前看了這個機制還是在運轉的。在升職及個人成就感的激勵之下,Google的各種系統一直在不斷迭代更新,始終引領技術發展,或者至少保持在前沿。這樣的激勵同時有一個副作用,即很難有十分穩定的系統。Google內外都有很多批評的聲音,聲討Google「隨意」關停項目,令用戶失望。系統的不斷更新甚至關停對於公司內部的下游使用者來說也是個痛苦的難題。有人這麼總結「Google的系統要麼是沒有文檔(快速開發中),要麼是已經廢棄了」。歷史悠久的產品面臨的一個重大挑戰是它們依賴的子系統不時就會有通知說即將關閉,必須要在某個截止日期前遷移到新的系統。這樣的通知意味着憑空出現的額外工作,而完成這樣的工作吃力不討好,沒法說明自己工作的影響力。反過來說,上游系統開發者必須考慮新系統給下游帶來的遷移成本。如果這個成本過高,又無法提供足夠的技術支持,必然會有來自下游團隊的激烈反對。這種情況就可能會帶來所謂的「利益衝突」。

我在這個項目的兩年可以說是很幸運的。因爲一方面我的工作讓Google搜索成功應對了AMP的爆發式增長,另一方面我們還成功發佈了新系統,完整取代了運行十多年的老古董。在這個項目中,我除了積累了個人經驗,還鍛鍊了重要的協作能力。如前文所述新項目會帶來團隊之間的利益衝突,這個項目也不例外。慢慢地我發現工程師的協作能力的很大程度上決定了一個人的高度,這是一個老生常談的問題,但只有自己體會過纔能理解爲什麼。個人技術能力呢?那當然更重要了,否則一切都是浮雲。

十五、路徑依賴

我大概是在2017年10月決定離開美國的,這回目的地是日本。這個決定可以說比離開瑞士更瘋狂,更難被人理解。其實我自己也更難下決心,因爲我離開那個團隊可能並非當下職業發展的最佳打算。與此同時我還要再次面臨收入的削減,會比上次從瑞士到美國幅度更大,即便是再次晉升也無法抹平的差距。最終,出於對不同的文化探索的熱情以及一點冒險的基因,我下定了決心。

我是這麼說服自己的,我畢竟當時纔畢業工作三年而已,未來有無限可能,這些可能性要趁早嘗試。許多順風順水的人在某個時候都會陷入路徑依賴——儘管每個局部選擇可能都是最優的,但最終歸於普通。這就像貪心算法的問題——取決於初值,沿着梯度優化最終只能得到局部最優解。這相當於最終能達到的高度在一開始就被決定了。

破解這種局面的算法之一是模擬退火。模擬退火算法的理念是在初期引入隨機性,隨着迭代次數的增加而減小隨機因子,這樣最終收斂到全局最優解的機率更大。跟隨這個理念來考慮職業的選擇,我決定跳出現有的路徑,哪怕是這個路徑的前方一片光明,現在只是爲了體驗不同的選擇。

模擬退火

以上是模擬退火算法尋求全局最值,圖片來自維基百科

下一篇:在Google的這四年(五)