Linux高效能伺服器架構
1. 概述:
任何一行都有自己的軍規, 我想這篇著名的文章就是遊戲伺服器程式設計師的軍規. 也許你認為遊戲伺服器程式設計師日常並不涉及這樣底層的實現, 而只是去完成策劃提出的需求, 我覺得也有道理, 畢竟這些是我們的工作, 下面的譯文就不太適合你. 但是對於想改進現有系統, 在伺服器方面給予更好的技術支援, 那麼你在開始工作之前必須瞭解一些禁忌, 並且給出了一些解決方向上的真知灼見. 把它發在這裡, 供隨時複習. 下面給出一個我閱讀的摘要和感受:
1) 資料拷貝Data Copies – 技巧什麼的
2) 環境切換Context Switches – 理性建立執行緒
3) 記憶體分配Memory allocation – 記憶體池
4) 鎖競爭Lock contention – 沒有好的辦法, 和具體的業務特點以及軟體的設計結構有密切的聯絡
可以看出, 1~3問題通過軟體技術, 分析是比較容易解決的, 並且已經有一些成熟的現成的解決方案和實現. 但是問題4完全不是這個樣子的. 本質上, 前3個問題都比較精確的描述了具體問題, 可以針對性解決. 而第四個問題的麻煩之處是它是一個元問題, 但是真正的問題只有在設計和編碼的時候才由這個元問題製造出來. 除非你已經設計和實現了自己的軟體系統, 否者問題只是存在於理論中. 而引發它出現的原因是你的設計, 這時你想解決這個問題只有推翻你自己的設計, 這通常是不可接受的. 但是現實就是這樣, 這就是為什麼4號問題是最難解決的.
現在的程式語言都是序列模型, 連編譯器都是認為:我編譯的原始碼肯定是序列的, 以至於它會將
int i = 1;
while (i) { do; }
優化成
while(true) { do; }
呵呵, 除非你用一個volatile來修飾i. 這給編寫併發程式帶來特別特別多的困難. 現在的新語言go,erlang已經開始慢慢在語言層面原生支援併發, 我相信這就像以前的gc一樣, 在以後的不久, 將成為主流:) 但是上面的4號問題是嵌入在問題域本身的, 它和軟體工程想解決的問題是在一起的, 也就是說對4號問題, 很可能也沒有銀彈.
下面是從網上轉的譯文, 文中重要點用不同的顏色標記出來了.
2. 引言:
本文將與你分享我多年來在伺服器開發方面的一些經驗. 對於這裡所說的伺服器, 更精確的定義應該是每秒處理大量離散訊息或者請求的服務程式
本文不會涉及到多工應用程式,在單個程式裡同時處理多個任務現在已經很常見。比如你的瀏覽器可能就在做一些並行處理,但是這類並行程式設計沒有多大挑戰性. 真正的挑戰出現在伺服器的架構設計對效能產生制約時,如何通過改善架構來提升系統性能. 對於在擁有上G記憶體和G赫茲CPU上執行的瀏覽器來說,通過DSL進行多個併發下載任務不會有如此的挑戰性. 這裡,應用的焦點不在於通過吸管小口吮吸,而是如何通過水龍頭大口暢飲,這裡麻煩是如何解決在硬體效能的制約.(作者的意思應該是怎麼通過網路硬體的改善來增大流量)
一些人可能會對我的某些觀點和建議發出置疑,或者自認為有更好的方法, 這是無法避免的. 在本文中我不想扮演上帝的角色;這裡所談論的是我自己的一些經驗,這些經驗對我來說, 不僅在提高伺服器效能上有效,而且在降低除錯困難度和增加系統的可擴充套件性上也有作用. 但是對某些人的系統可能會有所不同. 如果有其它更適合於你的方法,那實在是很不錯. 但是值得注意的是,對本文中所提出的每一條建議的其它一些可替代方案,我經過實驗得出的結論都是悲觀的. 你自己的小聰明在這些實驗中或許有更好的表現,但是如果因此慫恿我在這裡建議讀者這麼做,可能會引起無辜讀者的反感. 你並不想惹怒讀者,對吧?
本文的其餘部分將主要說明影響伺服器效能的四大殺手:
1) 資料拷貝Data Copies -- 技巧什麼的
2) 環境切換Context Switches -- 理性建立執行緒
3) 記憶體分配Memory allocation -- 記憶體池
4) 鎖競爭Lock contention -- 沒有好的辦法, 和具體的業務特 點, 軟體的設計結構有密切的聯絡
在文章結尾部分還會提出其它一些比較重要的因素,但是上面的四點是主要因素. 如果伺服器在處理大部分請求時能夠做到沒有資料拷貝,沒有環境切換,沒有記憶體分配,沒有鎖競爭,那麼我敢保證你的伺服器的效能一定很出色.
2.1:資料拷貝Data Copies
本節會有點短,因為大多數人在資料拷貝上吸取過教訓. 幾乎每個人都知道產生資料拷貝是不對的,這點是顯而易見的,在你的職業生涯中, 你很早就會見識過它;而且遇到過這個問題,因為10年前就有人開始說這個詞。對我來說確實如此. 現今,幾乎每個大學課程和幾乎所有how-to文件中都提到了它. 甚至在某些商業宣傳冊中,”零拷貝” 都是個流行用語. 儘管資料拷貝的壞處顯而易見,但是還是會有人忽視它. 因為產生資料拷貝的程式碼常常隱藏很深且帶有偽裝,你知道你所呼叫的庫或驅動的程式碼會進行資料拷貝嗎?答案往往超出想象. 猜猜”程式I/O”在計算機上到底指什麼?雜湊函式是偽裝的資料拷貝的例子,它有帶拷貝的記憶體訪問消耗和更多的計算. 曾經指出雜湊演算法是一種有效的”拷貝”似乎能夠被避免,但據我所知,有一些非常聰明的人說過要做到這一點是相當困難的. 如果想真正去除資料拷貝,不管是因為影響了伺服器效能,還是想在黑客大會上展示”零複製”技術,你必須自己跟蹤可能發生資料拷貝的所有地方,而不是輕信宣傳.
有一種可以避免資料拷貝的方法是使用buffer的描述符(或者buffer chains的描述符)來取代直接使用buffer指標,每個buffer描述符應該由以下元素組成:
1、一個指向buffer的指標和整個buffer的長度
2、一個指向buffer中真實資料的指標和真實資料的長度,或者長度的偏移
3、以雙向連結串列的形式提供指向其它buffer的指標
4、一個引用計數
現在,程式碼可以簡單的在相應的描述符上增加引用計數來代替記憶體中資料的拷貝. 這種做法在某些條件下表現的相當好,包括在典型的網路協議棧的操作上,但有些情況下這做法也令人很頭大. 一般來說,在buffer chains的開頭和結尾增加buffer很容易,對整個buffer增加引用計數,以及對buffer chains的即刻釋放也很容易. 在chains的中間增加buffer,一塊一塊的釋放buffer,或者對部分buffer增加引用技術則比較困難. 而分割,組合chains會讓人立馬崩潰.
我不建議在任何情況下都使用這種技術,因為當你想在鏈上搜索你想要的一個塊時,就不得不遍歷一遍描述符鏈,這甚至比資料拷貝更糟糕. 最適用這種技術地方是在程式中大的資料塊上,這些大資料塊應該按照上面所說的那樣獨立的分配描述符,以避免發生拷貝,也能避免影響伺服器其它部分的工作. (大資料塊拷貝很消耗CPU,會影響其它併發執行緒的執行)
關於資料拷貝最後要指出的是: 在避免資料拷貝時不要走極端. 我看到過太多的程式碼為了避免資料拷貝,最後結果反而比拷貝資料更糟糕,比如產生環境切換或者一個大的I/O請求被分解了. 資料拷貝是昂貴的,但是在避免它時,是收益遞減的(意思是做過頭了,效果反而不好). 為了除去最後少量的資料拷貝而改變程式碼,繼而讓程式碼複雜度翻番,不如把時間花在其它方面.
2.2:上下文切換Context Switches
相對於資料拷貝影響的明顯,非常多的人會忽視了上下文切換對效能的影響. 在我的經驗裡,比起資料拷貝,上下文切換是讓高負載應用徹底完蛋的真正殺手. 系統更多的時間都花費線上程切換上,而不是花在真正做有用工作的執行緒上. 令人驚奇的是, (和資料拷貝相比)在同一個水平上,導致上下文切換原因總是更常見. 引起環境切換的第一個原因往往是活躍執行緒數比CPU個數多. 隨著活躍執行緒數相對於CPU個數的增加,上下文切換的次數也在增加,如果你夠幸運,這種增長是線性的,但更常見是指數增長. 這個簡單的事實解釋了為什麼每個連線一個執行緒的多執行緒設計的可伸縮性更差. 對於一個可伸縮性的系統來說,限制活躍執行緒數少於或等於CPU個數是更有實際意義的方案. 曾經這種方案的一個變種是隻使用一個活躍執行緒,雖然這種方案避免了環境爭用,同時也避免了鎖,但它不能有效利用多CPU在增加總吞吐量上的價值,因此除非程式無CPU限制(non-CPU-bound), (通常是網路I/O限制 network-I/O-bound), 應該繼續使用更實際的方案.
一個有適量執行緒的程式首先要考慮的事情是規劃出如何建立一個執行緒去管理多連線. 這通常意味著前置一個select/poll/epoll, 非同步I/O,訊號或者完成埠,而後臺使用一個事件驅動的程式框架。關於哪種前置API是最好的有很多爭論. Dan Kegel的C10K在這個領域是一篇不錯的論文. 個人認為,select/poll和訊號通常是一種醜陋的方案,因此我更傾向於使用AIO或者完成埠,但是實際上它並不會好太多. 也許除了select(),它們都還不錯. 所以不要花太多精力去探索前置系統最外層內部到底發生了什麼.
對於最簡單的多執行緒事件驅動伺服器的概念模型, 其內部有一個請求快取佇列,客戶端請求被一個或者多個監聽執行緒獲取後放到佇列裡,然後一個或者多個工作執行緒從佇列裡面取出請求並處理. 從概念上來說,這是一個很好的模型,有很多用這種方式來實現他們的程式碼. 這會產生什麼問題嗎? 引起環境切換的第二個原因是把對請求的處理從一個執行緒轉移到另一個執行緒. 有些人甚至把對請求的迴應又切換回最初的執行緒去做,這真是雪上加霜,因為每一個請求至少引起了2次環境切換. 把一個請求從監聽執行緒轉換到成工作執行緒,又轉換回監聽執行緒的過程中,使用一種”平滑”的方法來避免環境切換是非常重要的. 此時,是否把連線請求分配到多個執行緒,或者讓所有執行緒依次作為監聽執行緒來服務每個連線請求,反而不重要了.
即使在將來, 也不可能有辦法知道在伺服器中同一時刻會有多少啟用執行緒. 畢竟,每時每刻都可能有請求從任意連線傳送過來,一些進行特殊任務的”後臺”執行緒也會在任意時刻被喚醒. 那麼如果你不知道當前有多少執行緒是啟用的,又怎麼能夠限制啟用執行緒的數量呢?根據我的經驗,最簡單同時也是最有效的方法之一是:用一個老式的帶計數的訊號量,每一個執行緒執行的時候就先持有訊號量. 如果訊號量已經到了最大值,那些處於監聽模式的執行緒被喚醒的時候可能會有一次額外的環境切換, (監聽執行緒被喚醒是因為有連線請求到來, 此時監聽執行緒持有訊號量時發現訊號量已滿,所以即刻休眠), 接著它就會被阻塞在這個訊號量上,一旦所有監聽模式的執行緒都這樣阻塞住了,那麼它們就不會再競爭資源了,直到其中一個執行緒釋放訊號量,這樣環境切換對系統的影響就可以忽略不計. 更主要的是,這種方法使大部分時間處於休眠狀態的執行緒避免在啟用執行緒數中佔用一個位置,這種方式比其它的替代方案更優雅.
一旦處理請求的過程被分成兩個階段(監聽和工作),那麼更進一步,這些處理過程在將來被分成更多的階段(更多的執行緒)就是很自然的事了. 最簡單的情況是一個完整的請求先完成第一步,然後是第二步(比如迴應). 然而實際會更復雜: 一個階段可能產生出兩個不同執行路徑,也可能只是簡單的生成一個應答(例如返回一個快取的值). 由此每個階段都需要知道下一步該如何做,根據階段分發函式的返回值有三種可能的做法:
1、請求需要被傳遞到另外一個階段(返回一個描述符或者指標)
2、請求已經完成(返回ok)
3、請求被阻塞(返回”請求阻塞”)。這和前面的情況一樣,阻塞到直到別的執行緒釋放資源
應該注意到在這種模式下,對階段的排隊是在一個執行緒內完成的,而不是經由兩個執行緒中完成. 這樣避免不斷把請求放在下一階段的佇列裡,緊接著又從該佇列取出這個請求來執行。這種經由很多活動佇列和鎖的階段很沒必要.
這種把一個複雜的任務分解成多個較小的互相協作的部分的方式,看起來很熟悉,這是因為這種做法確實很老了. 我的方法,源於CAR在1978年發明的”通訊序列化程序” (Communicating Sequential Processes CSP),它的基礎可以上溯到1963時的Per Brinch Hansen and Matthew Conway–在我出生之前! 然而,當Hoare創造出CSP這個術語的時候,“程序”是從抽象的數學角度而言的,而且,這個CSP術語中的程序和作業系統中同名的那個程序並沒有關係. 依我看來,這種在作業系統提供的單個執行緒之內,實現類似多執行緒一樣協同併發工作的CSP的方法,在可擴充套件性方面讓很多人頭疼.
一個實際的例子是,Matt Welsh的SEDA,這個例子表明分段執行的(stage-execution) 思想朝著一個比較合理的方向發展. SEDA是一個很好的 “server Aarchitecture done right” 的例子,值得把它的特性評論一下:
1. SEDA的批處理傾向於強調一個階段處理多個請求,而我的方式傾向於強調一個請求分成多個階段處理.
2. 在我看來SEDA的一個重大缺陷是給每個階段申請一個獨立的在載入響應階段中執行緒”後臺”重分配的執行緒池. 結果,原因1和原因2引起的環境切換仍然很多.
3. 在純技術的研究專案中,在Java中使用SEDA是有用的,然而在實際應用場合,我覺得這種方法很少被選擇.
2.3:記憶體分配(Memory Allocator)
申請和釋放記憶體是應用程式中最常見的操作, 因此發明了許多聰明的技巧使得記憶體的申請效率更高. 然而再聰明的方法也不能彌補這種事實: 在很多場合中,一般的記憶體分配方法非常沒有效率。所以為了減少向系統申請記憶體,我有三個建議.
建議一是使用預分配. 我們都知道由於使用靜態分配而對程式的功能加上人為限制是一種糟糕的設計. 但是還是有許多其它很不錯的預分配方案. 通常認為,通過系統一次性分配記憶體要比分開幾次分配要好,即使這樣做在程式中浪費了某些記憶體. 如果能夠確定在程式中會有幾項記憶體使用,在程式啟動時預分配就是一個合理的選擇. 即使不能確定,在開始時為請求控制代碼預分配可能需要的所有記憶體也比在每次需要一點的時候才分配要好. 通過系統一次性連續分配多項記憶體還能極大減少錯誤處理程式碼. 在記憶體比較緊張時,預分配可能不是一個好的選擇,但是除非面對最極端的系統環境,否則預分配都是一個穩賺不賠的選擇.
建議二是使用一個記憶體釋放分配的lookaside list(監視列表或者後備列表). 基本的概念是把最近釋放的物件放到連結串列裡而不是真的釋放它,當不久再次需要該物件時,直接從連結串列上取下來用,不用通過系統來分配. 使用lookaside list的一個額外好處是可以避免複雜物件的初始化和清理.
通常,讓lookaside list不受限制的增長,即使在程式空閒時也不釋放佔用的物件是個糟糕的想法. 在避免引入複雜的鎖或競爭情況下,不定期的“清掃”非活躍物件是很有必要的. 一個比較妥當的辦法是,讓lookaside list由兩個可以獨立鎖定的連結串列組成: 一個”新鏈”和一個”舊鏈”.使用時優先從”新”鏈分配,然後最後才依靠”舊”鏈. 物件總是被釋放的”新”鏈上。清除執行緒則按如下規則執行:
1. 鎖住兩個鏈
2. 儲存舊鏈的頭結點
3. 把前一個新鏈掛到舊鏈的前頭
4. 解鎖
5. 在空閒時通過第二步儲存的頭結點開始釋放舊鏈的所有物件
使用了這種方式的系統中,物件只有在真的沒用時才會釋放,釋放至少延時一個清除間隔期(指清除執行緒的執行間隔),但同常不會超過兩個間隔期. 清除執行緒不會和普通執行緒發生鎖競爭. 理論上來說,同樣的方法也可以應用到請求的多個階段,但目前我還沒有發現有這麼用的.
使用lookaside lists有一個問題是,保持分配物件需要一個連結串列指標(連結串列結點),這可能會增加記憶體的使用. 但是即使有這種情況,使用它帶來的好處也能夠遠遠彌補這些額外記憶體的花銷.
第三條建議與我們還沒有討論的鎖有關係. 先拋開它不說. 即使使用lookaside list,記憶體分配時的鎖競爭也常常是最大的開銷. 解決方法是使用執行緒私有的lookasid list, 這樣就可以避免多個執行緒之間的競爭. 更進一步,每個處理器一個鏈會更好,但這樣只有在非搶先式執行緒環境下才有用. 基於極端考慮,私有lookaside list甚至可以和一個共用的鏈工作結合起來使用.
2.4:鎖競爭(Lock Contention)
高效率的鎖是非常難規劃的, 以至於我把它稱作卡律布狄斯和斯庫拉(參見附錄). 一方面, 鎖的簡單化(粗粒度鎖)會導致並行處理的序列化, 因而降低了併發的效率和系統可伸縮性; 另一方面, 鎖的複雜化(細粒度鎖)在空間佔用上和操作時的時間消耗上都可能產生對效能的侵蝕. 偏向於粗粒度鎖會有死鎖發生,而偏向於細粒度鎖則會產生競爭. 在這兩者之間,有一個狹小的路徑通向正確性和高效率,但是路在哪裡?
由於鎖傾向於對程式邏輯產生束縛,所以如果要在不影響程式正常工作的基礎上規劃出鎖方案基本是不可能的. 這也就是人們為什麼憎恨鎖,並且為自己設計的不可擴充套件的單執行緒方案找藉口了.
幾乎我們每個系統中鎖的設計都始於一個”鎖住一切的超級大鎖”,並寄希望於它不會影響效能,當希望落空時(幾乎是必然), 大鎖被分成多個小鎖,然後我們繼續禱告(效能不會受影響),接著,是重複上面的整個過程(許多小鎖被分成更小的鎖), 直到效能達到可接受的程度. 通常,上面過程的每次重複都回增加大於20%-50%的複雜性和鎖負荷,並減少5%-10%的鎖競爭. 最終結果是取得了適中的效率,但是實際效率的降低是不可避免的. 設計者開始抓狂:”我已經按照書上的指導設計了細粒度鎖,為什麼系統性能還是很糟糕?”
在我的經驗裡,上面的方法從基礎上來說就不正確. 設想把解決方案當成一座山,優秀的方案表示山頂,糟糕的方案表示山谷. 上面始於”超級鎖”的解決方案就好像被形形色色的山谷,凹溝,小山頭和死衚衕擋在了山峰之外的登山者一樣,是一個典型的糟糕爬山法;從這樣一個地方開始登頂,還不如下山更容易一些。那麼登頂正確的方法是什麼?
首要的事情是為你程式中的鎖形成一張圖表,有兩個軸:
1、圖表的縱軸表示程式碼. 如果你正在應用剔出了分支的階段架構(指前面說的為請求劃分階段),你可能已經有這樣一張劃分圖了,就像很多人見過的OSI七層網路協議架構圖一樣.
2、圖表的水平軸表示資料集. 在請求的每個階段都應該有屬於該階段需要的資料集.
現在,你有了一張網格圖,圖上每個單元格表示一個特定階段需要的特定資料集. 下面是應該遵守的最重要的規則:兩個請求不應該產生競爭,除非它們在同一個階段需要同樣的資料集. 如果你嚴格遵守這個規則,那麼你已經成功了一半.
一旦你定義出了上面那個網格圖,在你的系統中的每種型別的鎖就都可以被標識出來了. 你的下一個目標是確保這些標識出來的鎖儘可能在兩個軸之間均勻的分佈, 這部分工作是和具體應用相關的. 你得像個鑽石切割工一樣,根據你對程式的瞭解,找出請求階段和資料集之間的自然”紋理線”. 有時候它們很容易發現,有時候又很難找出來,此時需要不斷回顧來發現它. 在程式設計時,把程式碼分隔成不同階段是很複雜的事情,我也沒有好的建議,但是對於資料集的定義,有一些建議給你:
1、如果你能對請求按順序編號,或者能對請求進行雜湊,或者能把請求和事物ID關聯起來,那麼根據這些編號或者ID就能對資料更好的進行分隔.
2、有時,基於資料集的資源最大化利用,把請求動態的分配給資料,相對於依據請求的固有屬性來分配會更有優勢. 就好像現代CPU的多個整數運算單元知道把請求分離一樣.
3、確定每個階段指定的資料集是不一樣的是非常有用的,以便保證一個階段爭奪的資料在另外階段不會爭奪.
如果你在縱向和橫向上把”鎖空間(這裡實際指鎖的分佈)” 分隔了,並且確保了鎖均勻分佈在網格上,那麼恭喜你獲得了一個好方案. 現在你處在了一個好的登山點,打個比喻,你面有了一條通向頂峰的緩坡,但你還沒有到山頂. 現在是時候對鎖競爭進行統計,看看該如何改進了. 以不同的方式分隔階段和資料集,然後統計鎖競爭,直到獲得一個滿意的分隔. 當你做到這個程度的時候,那麼無限風景將呈現在你腳下.
3:其他方面
我已經闡述完了影響效能的四個主要方面. 然而還有一些比較重要的方面需要說一說,大所屬都可歸結於你的平臺或系統環境:
1、你的儲存子系統在大資料讀寫和小資料讀寫,隨即讀寫和順序讀寫方面是如何進行?在預讀和延遲寫入方面做得怎樣?
2、你使用的網路協議效率如何?是否可以通過修改引數改善效能?是否有類似於TCP_CORK, MSG_PUSH,Nagle-toggling演算法的手段來避免小訊息產生?
3、你的系統是否支援Scatter-Gather I/O(例如readv/writev)? 使用這些能夠改善效能,也能避免使用緩衝鏈(見第一節資料拷貝的相關敘述)帶來的麻煩. (說明:在dma傳輸資料的過程中,要求源實體地址和目標實體地址必須是連續的. 但在有的計算機體系中,如IA,連續的儲存器地址在物理上不一定是連續的,則dma傳輸要分成多次完成. 如果傳輸完一塊物理連續的資料後發起一次中斷,同時主機進行下一塊物理連續的傳輸,則這種方式即為block dma方式. scatter/gather方式則不同,它是用一個連結串列描述物理不連續的儲存器,然後把連結串列首地址告訴dma master. dma master傳輸完一塊物理連續的資料後,就不用再發中斷了,而是根據連結串列傳輸下一塊物理連續的資料,最後發起一次中斷. 很顯然 scatter/gather方式比block dma方式效率高)
4、你的系統的頁大小是多少?快取記憶體大小是多少?向這些大小邊界進行對起是否有用?系統呼叫和上下文切換花的代價是多少?
5、你是否知道鎖原語的飢餓現象?你的事件機制有沒有”驚群”問題?你的喚醒/睡眠機制是否有這樣糟糕的行為: 當X喚醒了Y, 環境立刻切換到了Y,但是X還有沒完成的工作?
我在這裡考慮的了很多方面,相信你也考慮過. 在特定情況下,應用這裡提到的某些方面可能沒有價值,但能考慮這些因素的影響還是有用的. 如果在系統手冊中,你沒有找到這些方面的說明,那麼就去努力找出答案. 寫一個測試程式來找出答案;不管怎樣,寫這樣的測試程式碼都是很好的技巧鍛鍊. 如果你寫的程式碼在多個平臺上都執行過,那麼把這些相關的程式碼抽象為一個平臺相關的庫,將來在某個支援這裡提到的某些功能的平臺上,你就贏得了先機.
對你的程式碼, “知其所以然”, 弄明白其中高階的操作, 以及在不同條件下的花銷. 這不同於傳統的效能分析, 不是關於具體的實現,而是關乎設計. 低級別的優化永遠是蹩腳設計的最後救命稻草. map注:下面這段文字原文沒有,這是譯者對於翻譯的理解.
附錄:奧德修斯Odysseus,又譯”奧德賽”,神話中伊塔刻島國王,《伊利亞特》和《奧德賽》兩大史詩中的主人公(公元前11世紀到公元前9世紀的希臘史稱作”荷馬時代”. 包括《伊利亞特》和《奧德賽》兩部分的《荷馬史詩》,是古代世界一部著名的傑作). 奧德修斯曾參加過著名的特洛伊戰爭,在戰爭中他以英勇善戰、足智多謀而著稱,為贏得戰爭的勝利,他設計製造了著名的”特洛伊木馬” (後來在西方成了“為毀滅敵人而送的禮物”的代名詞). 特洛伊城毀滅後,他在回國途中又經歷了許多風險,荷馬的《奧德賽》就是奧德修斯歷險的記述. “斯庫拉和卡律布狄斯”的故事是其中最驚險、最恐怖的一幕.
相傳,斯庫拉和卡律布狄斯是古希臘神話中的女妖和魔怪,女妖斯庫拉住在義大利和西西里島之間海峽中的一個洞穴裡,她的對面住著另一個妖怪卡律布狄斯. 它們為害所有過往航海的人. 據荷馬說,女妖斯庫拉長著12只不規則的腳,有6個蛇一樣的脖子,每個脖子上各有一顆可怕的頭,張著血盆大口,每張嘴有3 排毒牙,隨時準備把獵物咬碎. 它們每天在義大利和西西里島之間海峽中興風作浪,航海者在兩個妖怪之間通過是異常危險的,它們時刻在等待著穿過西西里海峽的船舶. 在海峽中間,卡律布狄斯化成一個大旋渦,波濤洶湧、水花飛濺,每天3次從懸崖上奔湧而出,在退落時將通過此處的船隻全部淹沒. 當奧德修斯的船接近卡律布狄斯大旋渦時,它像火爐上的一鍋沸水,波濤滔天,激起漫天雪白的水花. 當潮退時,海水混濁,濤聲如雷,驚天動地. 這時,黑暗泥濘的巖穴一見到底. 正當他們驚恐地注視著這一可怕的景象時,正當舵手小心翼翼地駕駛著船隻從左繞過旋渦時,突然海怪斯庫拉出現在他們面前,她一口叼住了6個同伴. 奧德修斯親眼看見自己的同伴在妖怪的牙齒中間扭動著雙手和雙腳,掙扎了一會兒,他們便被嚼碎,成了血肉模糊的一團. 其餘的人僥倖通過了卡律布狄斯大旋渦和海怪斯庫拉之間的危險的隘口. 後來又歷經種種災難,最後終於回到了故鄉——伊塔刻島.
這個故事在語言學界和翻譯界被廣為流傳。前蘇聯著名翻譯家巴爾胡達羅夫就曾把”斯庫拉和卡律布狄斯”比作翻譯中”直譯和意譯”. 他說: “形象地說,譯者總是不得不在直譯和意譯之間迂迴應變,猶如在斯庫拉和卡律布狄斯之間曲折前行,以求在這海峽兩岸之間找到一條狹窄然而卻足夠深邃的航道,以便達到理想的目的地——最大限度的等值翻譯.”
德國著名語言學家洪堡特也說過類似的話: “我確信任何翻譯無疑地都是企圖解決不可能解決的任務. 因為任何一個翻譯家都會碰到一個暗礁而遭到失敗,他們不是由於十分準確地遵守了原文的形式而破壞了譯文語言的特點,就是為了照顧譯文語言的特點而損壞了原文. 介於兩者之間的做法不僅難於辦到,而且簡直是不可能辦到.”
歷史上長久以來都認為,翻譯只能選擇兩個極端的一種:或者這種——逐字翻譯(直譯); 或者那種——自由翻譯(意譯). 就好像翻譯中的斯庫拉和卡律布狄斯一樣。如今斯庫拉和卡律布狄斯已成為表示雙重危險——海怪和旋渦的代名詞,人們常說介於斯庫拉和卡律布狄斯之間, 這就是說:處於兩面受敵的險境,比喻危機四伏,用來喻指譯者在直譯和意譯之間反覆作出抉擇之艱難.
相關推薦
Linux高效能伺服器架構
1. 概述: 任何一行都有自己的軍規, 我想這篇著名的文章就是遊戲伺服器程式設計師的軍規. 也許你認為遊戲伺服器程式設計師日常並不涉及這樣底層的實現, 而只是去完成策劃提出的需求, 我覺得也有道理, 畢竟這些是我們的工作, 下面的譯文就不太適合你. 但是對於想
Linux高效能伺服器程式設計模式
本文時間:2018-11-21,作者:krircc, 簡介:天青色 歡迎向Rust中文社群投稿,投稿地址,好文將在以下地方直接展示 Rust中文社群首頁 Rust中文社群Rust文章欄目 知乎專欄Rust語言 高效能伺服器至少要滿足如下幾個
一文詳解高效能伺服器架構設計
引言 本文從一個簡單的伺服器架構,通過討論出現的問題,進行一步一步優化,最後進化成高效能分散式伺服器架構。 初始情況:一個典型的伺服器結構 新增資料訪問層DAL,解決超出連線次數的問題 新增快取,減少與資料庫建立連線 即使添加了DAL,但是資料
Linux 高效能伺服器程式設計—— I/O複用 epoll
一 核心事件表 epoll不同於select和poll,它是使用一組函式來完成任務;同時,epoll把使用者關心的檔案描述符上的事件放在核心裡的一個事件表中,從而不像select 和 poll每次呼叫都需要重複傳入檔案描述符集或事件集。但是epoll需要一個額外的檔案描述符來表示核
Linux 高效能伺服器程式設計——I/O複用 poll
一:poll系統呼叫 同select相似,也是在指定時間內輪詢一定數量的檔案描述符,以測試其中是否有就緒者。 二:poll函式 1.函式原型: #include<poll.h> int poll(struct pollfd* fds,nfds
Linux高效能伺服器程式設計——I/O複用 select
提出背景 不管是多執行緒,或者多程序,以及執行緒池,程序池。他們都存在一定的效率問題。 1.每個程序或執行緒只能為一個客戶端進行服務,知道該客戶端結束。(如果客戶端在同一時間的訪問數量特別大呢?) 2.當客戶端傳送來資料後,分配執行緒或程序為其服務完後,就要等
Linux高效能伺服器程式設計——程序間 傳遞檔案描述符
問題的提出 一:fork()之後,父程序中開啟的檔案描述符,在子程序中仍然處於開啟狀態。所以從父程序——》子程序(傳遞檔案描述符)很方便。值得注意的是傳遞一個檔案描述符並不是傳遞一個檔案描述符的值。 二:那麼怎樣吧子程序中開啟的檔案描述符傳遞給父程序呢?通俗的說:
《Linux高效能伺服器程式設計》 讀書筆記
第一章 TCP/IP協議族 資料鏈路層可以看成是一個網絡卡驅動,為上層提供了一個統一的介面。 Telnet協議是一種遠端登入協議,它使得我們能在本地完成遠端任務。 第二章 IP協議詳解 第三章 TCP協議詳解 產生復位報文段的三種情況: 訪問不存在
高效能伺服器架構思路【不僅是思路】
在伺服器端程式開發領域,效能問題一直是備受關注的重點。業界有大量的框架、元件、類庫都是以效能為賣點而廣為人知。然而,伺服器端程式在效能問題上應該有何種基本思路,這個卻很少被這些專案的文件提及。本文正式希望介紹伺服器端解決效能問題的基本策略和經典實踐,並分為幾個部分來說明:
《Linux高效能伺服器》附帶專案springsnil原始碼解析
原始碼地址 安裝及使用 下載原始碼: git clone https://github.com/liu-jianhao/springsnail.git 然後進入springsnil目錄直接make即可生成可執行檔案 填寫配置檔案,我測試的是網站是網易雲音樂,首先我先看看網
高效能伺服器架構思路,不僅是思路
在伺服器端程式開發領域,效能問題一直是備受關注的重點。業界有大量的框架、元件、類庫都是以效能為賣點而廣為人知。然而,伺服器端程式在效能問題上應該有何種基本思路,這個卻很少被這些專案的文件提及。本文正式希望介紹伺服器端解決效能問題的基本策略和經典實踐,並分為幾個部分來
高效能伺服器架構思路(一)——緩衝策略
在伺服器端程式開發領域,效能問題一直是備受關注的重點。業界有大量的框架、元件、類庫都是以效能為賣點而廣為人知。然而,伺服器端程式在效能問題上應該有何種基本思路,這個卻很少被這些專案的文件提及。本文正式希望介紹伺服器端解決效能問題的基本策略和經典實踐,並分為幾個部分來說明:
高效能伺服器架構
本文將與你分享我多年來在伺服器開發方面的一些經驗。對於這裡所說的伺服器,更精確的定義應該是每秒處理大量離散訊息或者請求的服務程式,網路伺服器更符合這種情況,但並非所有的網路程式都是嚴格意義上的伺服器。使用“高效能請求處理程式”是一個很糟糕的標題,為了敘述起來簡單,下面將
高效能伺服器架構(二):快取清理策略
雖然使用快取思想似乎是一個很簡單的事情,但是快取機制卻有一個核心的難點,就是——快取清理。我們所說的快取,都是儲存一些資料,但是這些資料往往是會變化的,我們要針對這些變化,清理
12.Linux 高效能伺服器程式設計 --- 高效能 IO 框架庫 Libevent
1.控制代碼 IO框架庫要處理的物件,即 IO事件,訊號和定時事件,統一稱為事件源。 一個事件源通常和一個控制代碼綁在一起。控制代碼的作用是,當核心檢測到就緒事件時,它將通過 控制代碼來通知應用程式這一事件。Linux 下,IO事件對應的控制代碼就是檔案描述符,訊號事件
高效能伺服器架構思路(二)——緩衝清理策略
雖然使用快取思想似乎是一個很簡單的事情,但是快取機制卻有一個核心的難點,就是——快取清理。我們所說的快取,都是儲存一些資料,但是這些資料往往是會變化的,我們要針對這些變化,清理掉儲存的“髒”資料,卻可能不是那麼容易。 首先我們來看看最簡單的快取資料——靜態資料。這種資料
來自騰訊的高效能伺服器架構思路
在伺服器端程式開發領域,效能問題一直是備受關注的重點。業界有大量的框架、元件、類庫都是以效能為賣點而廣為人知。然而,伺服器端程式在效能問題上應該有何種基本思路,這個卻很少被這些專案的文件提及。本文正式希望介紹伺服器端解決效能問題的基本策略和經典實踐,並分為幾個部
linux c++ 高併發tcp伺服器架構
本文轉載自部落格:http://blog.csdn.net/opencpu/article/details/47175813 epoll 接受資料到佇列,執行緒池處理佇列裡的資料 具體實現方式:(只使用使用std的的資料結構,未使用boost)
高效能閘道器裝置及服務實踐(dpdk)--伺服器架構研究
針對海量的網路流量,轉發效能是我們最關鍵的一個方面,那構建高效能的後臺伺服器有哪些關鍵的技術和需要注意的地方,今天邀請了後臺開發同學童琳和鄭勝利來和大家一起談談。 一、引言 隨著網際網路的高速發展,內容量的提升以及對內容智慧的需求、雲產業的快速
描述一個高效能高可靠的伺服器架構---------如何設計一個秒殺系統
一、秒殺的應用場景 電商網站的搶購活動、12306網站的搶票、搶紅包。 二、秒殺的特點 1、秒殺時大量使用者會在同一時間同時進行搶購,網站瞬時訪問流量激增。 2、資料庫的併發讀寫衝突以及資源的鎖請求衝突非常嚴重。 3、秒殺一般是訪問請求數量遠遠大於