本文將與你分享我多年來在服務器開發方面的一些經驗。對於這裏所說的服務器,更精確的定義應該是每秒處理大量離散消息或者請求的服務程序,網絡服務器更符合這種狀況,但並不是全部的網絡程序都是嚴格意義上的服務器。使用「高性能請求處理程序」是一個很糟糕的標題,爲了敘述起來簡單,下面將簡稱爲「服務器」。 html
本文不會涉及到多任務應用程序,在單個程序裏同時處理多個任務如今已經很常見。好比你的瀏覽器可能就在作一些並行處理,可是這類並行程序設計沒有多大挑戰性。真正的挑戰出如今服務器的架構設計對性能產生制約時,如何經過改善架構來提高系統性能。對於在擁有上G內存和G赫茲CPU上運行的瀏覽器來講,經過DSL進行多個併發下載任務不會有如此的挑戰性。這裏,應用的焦點不在於經過吸管小口吮吸,而是如何經過水龍頭大口暢飲,這裏麻煩是如何解決在硬件性能的制約.(做者的意思應該是怎麼經過網絡硬件的改善來增大流量) 算法
一些人可能會對個人某些觀點和建議發出置疑,或者自認爲有更好的方法, 這是沒法避免的。在本文中我不想扮演上帝的角色;這裏所談論的是我本身的一些經驗,這些經驗對我來講, 不只在提升服務器性能上有效,並且在下降調試困難度和增長系統的可擴展性上也有做用。可是對某些人的系統可能會有所不一樣。若是有其它更適合於你的方法,那實在是很不錯. 可是值得注意的是,對本文中所提出的每一條建議的其它一些可替代方案,我通過實驗得出的結論都是悲觀的。你本身的小聰明在這些實驗中或許有更好的表現,可是若是所以慫恿我在這裏建議讀者這麼作,可能會引發無辜讀者的反感。你並不想惹怒讀者,對吧? 瀏覽器
本文的其他部分將主要說明影響服務器性能的四大殺手: 緩存
1) 數據拷貝(Data Copies) 服務器
2) 環境切換(Context Switches) 網絡
3) 內存分配(Memory allocation) 多線程
4) 鎖競爭(Lock contention) 架構
在文章結尾部分還會提出其它一些比較重要的因素,可是上面的四點是主要因素。若是服務器在處理大部分請求時可以作到沒有數據拷貝,沒有環境切換,沒有內存分配,沒有鎖競爭,那麼我敢保證你的服務器的性能必定很出色。 併發
本節會有點短,由於大多數人在數據拷貝上吸收過教訓。幾乎每一個人都知道產生數據拷貝是不對的,這點是顯而易見的,在你的職業生涯中, 你很早就會見識過它;並且遇到過這個問題,由於10年前就有人開始說這個詞。對我來講確實如此。現今,幾乎每一個大學課程和幾乎全部how-to文檔中都提到了它。甚至在某些商業宣傳冊中,"零拷貝" 都是個流行用語。 框架
儘管數據拷貝的壞處顯而易見,可是仍是會有人忽視它。由於產生數據拷貝的代碼經常隱藏很深且帶有假裝,你知道你所調用的庫或驅動的代碼會進行數據拷貝嗎?答案每每超出想象。猜猜「程序I/O」在計算機上到底指什麼?哈希函數是假裝的數據拷貝的例子,它有帶拷貝的內存訪問消耗和更多的計算。曾經指出哈希算法是一種有效的「拷貝+」彷佛可以被避免,但據我所知,有一些很是聰明的人說過要作到這一點是至關困難的。若是想真正去除數據拷貝,無論是由於影響了服務器性能,仍是想在黑客大會上展現"零複製」技術,你必須本身跟蹤可能發生數據拷貝的全部地方,而不是輕信宣傳。
有一種能夠避免數據拷貝的方法是使用buffer的描述符(或者buffer chains的描述符)來取代直接使用buffer指針,每一個buffer描述符應該由如下元素組成:
l 一個指向buffer的指針和整個buffer的長度
l 一個指向buffer中真實數據的指針和真實數據的長度,或者長度的偏移
l 以雙向鏈表的形式提供指向其它buffer的指針
l 一個引用計數
如今,代碼能夠簡單的在相應的描述符上增長引用計數來代替內存中數據的拷貝。這種作法在某些條件下表現的至關好,包括在典型的網絡協議棧的操做上,但有些狀況下這作法也使人很頭大。通常來講,在buffer chains的開頭和結尾增長buffer很容易,對整個buffer增長引用計數,以及對buffer chains的即刻釋放也很容易。在chains的中間增長buffer,一塊一塊的釋放buffer,或者對部分buffer增長引用技術則比較困難。而分割,組合chains會讓人立馬崩潰。
我 不建議在任何狀況下都使用這種技術,由於當你想在鏈上搜索你想要的一個塊時,就不得不遍歷一遍描述符鏈,這甚至比數據拷貝更糟糕。最適用這種技術地方是在 程序中大的數據塊上,這些大數據塊應該按照上面所說的那樣獨立的分配描述符,以免發生拷貝,也能避免影響服務器其它部分的工做.(大數據塊拷貝很消耗CPU,會影響其它併發線程的運行)。
關於數據拷貝最後要指出的是:在避免數據拷貝時不要走極端。我看到過太多的代碼爲了不數據拷貝,最後結果反而比拷貝數據更糟糕,好比產生環境切換或者一個大的I/O請求被分解了。數據拷貝是昂貴的,可是在避免它時,是收益遞減的(意思是作過頭了,效果反而很差)。爲了除去最後少許的數據拷貝而改變代碼,繼而讓代碼複雜度翻番,不如把時間花在其它方面。
相 對於數據拷貝影響的明顯,很是多的人會忽視了上下文切換對性能的影響。在個人經驗裏,比起數據拷貝,上下文切換是讓高負載應用完全完蛋的真正殺手。系統更 多的時間都花費在線程切換上,而不是花在真正作有用工做的線程上。使人驚奇的是,(和數據拷貝相比)在同一個水平上,致使上下文切換緣由老是更常見。引發 環境切換的第一個緣由每每是活躍線程數比CPU個數多。隨着活躍線程數相對於CPU個數的增長,上下文切換的次數也在增長,若是你夠幸運,這種增加是線性的,但更常見是指數增加。這個簡單的事實解釋了爲何每一個鏈接一個線程的多線程設計的可伸縮性更差。對於一個可伸縮性的系統來講,限制活躍線程數少於或等於CPU個數是更有實際意義的方案。曾經這種方案的一個變種是隻使用一個活躍線程,雖然這種方案避免了環境爭用,同時也避免了鎖,但它不能有效利用多CPU在增長總吞吐量上的價值,所以除非程序無CPU限制(non-CPU-bound),(一般是網絡I/O限制 network-I/O-bound),應該繼續使用更實際的方案。
一個有適量線程的程序首先要考慮的事情是規劃出如何建立一個線程去管理多鏈接。這一般意味着前置一個select/poll, 異步I/O,信號或者完成端口,然後臺使用一個事件驅動的程序框架。關於哪一種前置API是最好的有不少爭論。 Dan Kegel的C10K在這個領域是一篇不錯的論文。我的認爲,select/poll和信號一般是一種醜陋的方案,所以我更傾向於使用AIO或者完成端口,可是實際上它並不會好太多。也許除了select(),它們都還不錯。因此不要花太多精力去探索前置系統最外層內部到底發生了什麼。
對於最簡單的多線程事件驅動服務器的概念模型, 其內部有一個請求緩存隊列,客戶端請求被一個或者多個監聽線程獲取後放到隊列裏,而後一個或者多個工做線程從隊列裏面取出請求並處理。從概念上來講,這是一個很好的模型,有不少用這種方式來實現他們的代碼。這會產生什麼問題嗎?引發環境切換的第二個緣由是把對請求的處理從一個線程轉移到另外一個線程。有些人甚至把對請求的迴應又切換回最初的線程去作,這真是雪上加霜,由於每個請求至少引發了2次環境切換。把一個請求從監聽線程轉換到成工做線程,又轉換回監聽線程的過程當中,使用一種「平滑」的方法來避免環境切換是很是重要的。此時,是否把鏈接請求分配到多個線程,或者讓全部線程依次做爲監聽線程來服務每一個鏈接請求,反而不重要了。
即便在未來,也不可能有辦法知道在服務器中同一時刻會有多少激活線程.畢竟,每時每刻均可能有請求從任意鏈接發送過來,一些進行特殊任務的「後臺」線 程也會在任意時刻被喚醒。那麼若是你不知道當前有多少線程是激活的,又怎麼可以限制激活線程的數量呢?根據個人經驗,最簡單同時也是最有效的方法之一是: 用一個老式的帶計數的信號量,每個線程執行的時候就先持有信號量。若是信號量已經到了最大值,那些處於監聽模式的線程被喚醒的時候可能會有一次額外的環 境切換,(監聽線程被喚醒是由於有鏈接請求到來, 此時監聽線程持有信號量時發現信號量已滿,因此即刻休眠), 接 着它就會被阻塞在這個信號量上,一旦全部監聽模式的線程都這樣阻塞住了,那麼它們就不會再競爭資源了,直到其中一個線程釋放信號量,這樣環境切換對系統的 影響就能夠忽略不計。更主要的是,這種方法使大部分時間處於休眠狀態的線程避免在激活線程數中佔用一個位置,這種方式比其它的替代方案更優雅。
一旦處理請求的過程被分紅兩個階段(監聽和工做),那麼更進一步,這些處理過程在未來被分紅更多的階段(更多的線程)就是很天然的事了。最簡單的狀況是一個完整的請求先完成第一步,而後是第二步(好比迴應)。然而實際會更復雜:一個階段可能產生出兩個不一樣執行路徑,也可能只是簡單的生成一個應答(例如返回一個緩存的值)。由此每一個階段都須要知道下一步該如何作,根據階段分發函數的返回值有三種可能的作法:
l 請求須要被傳遞到另一個階段(返回一個描述符或者指針)
l 請求已經完成(返回ok)
l 請求被阻塞(返回"請求阻塞")。這和前面的狀況同樣,阻塞到直到別的線程釋放資源
應該注意到在這種模式下,對階段的排隊是在一個線程內完成的,而不是經由兩個線程中完成。這樣避免不斷把請求放在下一階段的隊列裏,緊接着又從該隊列取出這個請求來執行。這種經由不少活動隊列和鎖的階段很不必。
這種把一個複雜的任務分解成多個較小的互相協做的部分的方式,看起來很熟悉,這是由於這種作法確實很老了。個人方法,源於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是有用的,然而在實際應用場合,我以爲這種方法不多被選擇。
申請和釋放內存是應用程序中最多見的操做, 所以發明了許多聰明的技巧使得內存的申請效率更高。然而再聰明的方法也不能彌補這種事實:在不少場合中,通常的內存分配方法很是沒有效率。因此爲了減小向系統申請內存,我有三個建議。
建 議一是使用預分配。咱們都知道因爲使用靜態分配而對程序的功能加上人爲限制是一種糟糕的設計。可是仍是有許多其它很不錯的預分配方案。一般認爲,經過系統 一次性分配內存要比分開幾回分配要好,即便這樣作在程序中浪費了某些內存。若是可以肯定在程序中會有幾項內存使用,在程序啓動時預分配就是一個合理的選 擇。即便不能肯定,在開始時爲請求句柄預分配可能須要的全部內存也比在每次須要一點的時候才分配要好。經過系統一次性連續分配多項內存還能極大減小錯誤處 理代碼。在內存比較緊張時,預分配可能不是一個好的選擇,可是除非面對最極端的系統環境,不然預分配都是一個穩賺不賠的選擇。
建議二是使用一個內存釋放分配的lookaside list(監視列表或者後備列表)。基本的概念是把最近釋放的對象放到鏈表裏而不是真的釋放它,當不久再次須要該對象時,直接從鏈表上取下來用,不用經過系統來分配。使用lookaside list的一個額外好處是能夠避免複雜對象的初始化和清理.
一般,讓lookaside list不受限制的增加,即便在程序空閒時也不釋放佔用的對象是個糟糕的想法。在避免引入複雜的鎖或競爭狀況下,不按期的「清掃"非活躍對象是頗有必要的。一個比較穩當的辦法是,讓lookaside list由兩個能夠獨立鎖定的鏈表組成:一個"新鏈"和一個"舊鏈".使用時優先從"新"鏈分配,而後最後才依靠"舊"鏈。對象老是被釋放的"新"鏈上。清除線程則按以下規則運行:
1. 鎖住兩個鏈
2. 保存舊鏈的頭結點
3. 把前一個新鏈掛到舊鏈的前頭
4. 解鎖
5. 在空閒時經過第二步保存的頭結點開始釋放舊鏈的全部對象。
使用了這種方式的系統中,對象只有在真的沒用時纔會釋放,釋放至少延時一個清除間隔期(指清除線程的運行間隔),但同常不會超過兩個間隔期。清除線程不會和普通線程發生鎖競爭。理論上來講,一樣的方法也能夠應用到請求的多個階段,但目前我尚未發現有這麼用的。
使用lookaside lists有一個問題是,保持分配對象須要一個鏈表指針(鏈表結點),這可能會增長內存的使用。可是即便有這種狀況,使用它帶來的好處也可以遠遠彌補這些額外內存的花銷。
第三條建議與咱們尚未討論的鎖有關係。先拋開它不說。即便使用lookaside list,內存分配時的鎖競爭也經常是最大的開銷。解決方法是使用線程私有的lookasid list, 這樣就能夠避免多個線程之間的競爭。更進一步,每一個處理器一個鏈會更好,但這樣只有在非搶先式線程環境下才有用。基於極端考慮,私有lookaside list甚至能夠和一個共用的鏈工做結合起來使用。
高效率的鎖是很是難規劃的, 以致於我把它稱做卡律布狄斯和斯庫拉(參見附錄)。一方面, 鎖的簡單化(粗粒度鎖)會致使並行處理的串行化,於是下降了併發的效率和系統可伸縮性; 另外一方面, 鎖的複雜化(細粒度鎖)在空間佔用上和操做時的時間消耗上均可能產生對性能的侵蝕。偏向於粗粒度鎖會有死鎖發生,而偏向於細粒度鎖則會產生競爭。在這二者之間,有一個狹小的路徑通向正確性和高效率,可是路在哪裏?
因爲鎖傾向於對程序邏輯產生束縛,因此若是要在不影響程序正常工做的基礎上規劃出鎖方案基本是不可能的。這也就是人們爲何憎恨鎖,而且爲本身設計的不可擴展的單線程方案找藉口了。
幾乎咱們每一個系統中鎖的設計都始於一個"鎖住一切的超級大鎖",並寄但願於它不會影響性能,當但願落空時(幾乎是必然), 大鎖被分紅多個小鎖,而後咱們繼續禱告(性能不會受影響),接着,是重複上面的整個過程(許多小鎖被分紅更小的鎖), 直到性能達到可接受的程度。一般,上面過程的每次重複都回增長大於20%-50%的複雜性和鎖負荷,並減小5%-10%的鎖競爭。最終結果是取得了適中的效率,可是實際效率的下降是不可避免的。設計者開始抓狂:"我已經按照書上的指導設計了細粒度鎖,爲何系統性能仍是很糟糕?"
在個人經驗裏,上面的方法從基礎上來講就不正確。設想把解決方案當成一座山,優秀的方案表示山頂,糟糕的方案表示山谷。上面始於"超級鎖"的解決方案就好像被形形色色的山谷,凹溝,小山頭和死衚衕擋在了山峯以外的爬山者同樣,是一個典型的糟糕登山法;從這樣一個地方開始登頂,還不以下山更容易一些。那麼登頂正確的方法是什麼?
首要的事情是爲你程序中的鎖造成一張圖表,有兩個軸:
l 圖表的縱軸表示代碼。若是你正在應用剔出了分支的階段架構(指前面說的爲請求劃分階段),你可能已經有這樣一張劃分圖了,就像不少人見過的OSI七層網絡協議架構圖同樣。
l 圖表的水平軸表示數據集。在請求的每一個階段都應該有屬於該階段須要的數據集。
如今,你有了一張網格圖,圖上每一個單元格表示一個特定階段須要的特定數據集。下面是應該遵照的最重要的規則:兩個請求不該該產生競爭,除非它們在同一個階段須要一樣的數據集。若是你嚴格遵照這個規則,那麼你已經成功了一半。
一旦你定義出了上面那個網格圖,在你的系統中的每種類型的鎖就均可以被標識出來了。你的下一個目標是確保這些標識出來的鎖儘量在兩個軸之間均勻的分佈, 這 部分工做是和具體應用相關的。你得像個鑽石切割工同樣,根據你對程序的瞭解,找出請求階段和數據集之間的天然「紋理線」。有時候它們很容易發現,有時候又 很難找出來,此時須要不斷回顧來發現它。在程序設計時,把代碼分隔成不一樣階段是很複雜的事情,我也沒有好的建議,可是對於數據集的定義,有一些建議給你:
l 若是你能對請求按順序編號,或者能對請求進行哈希,或者能把請求和事物ID關聯起來,那麼根據這些編號或者ID就能對數據更好的進行分隔。
l 有時,基於數據集的資源最大化利用,把請求動態的分配給數據,相對於依據請求的固有屬性來分配會更有優點。就好像現代CPU的多個整數運算單元知道把請求分離同樣。
l 肯定每一個階段指定的數據集是不同的是很是有用的,以便保證一個階段爭奪的數據在另外階段不會爭奪。
若是你在縱向和橫向上把「鎖空間(這裏實際指鎖的分佈)" 分 隔了,而且確保了鎖均勻分佈在網格上,那麼恭喜你得到了一個好方案。如今你處在了一個好的爬山點,打個比喻,你面有了一條通向頂峯的緩坡,但你尚未到山 頂。如今是時候對鎖競爭進行統計,看看該如何改進了。以不一樣的方式分隔階段和數據集,而後統計鎖競爭,直到得到一個滿意的分隔。當你作到這個程度的時候, 那麼無限風景將呈如今你腳下。
我已經闡述完了影響性能的四個主要方面。然而還有一些比較重要的方面須要說一說,大所屬均可歸結於你的平臺或系統環境:
l 你的存儲子系統在大數據讀寫和小數據讀寫,隨即讀寫和順序讀寫方面是如何進行?在預讀和延遲寫入方面作得怎樣?
l 你使用的網絡協議效率如何?是否能夠經過修改參數改善性能?是否有相似於TCP_CORK, MSG_PUSH,Nagle-toggling算法的手段來避免小消息產生?
l 你的系統是否支持Scatter-Gather I/O(例如readv/writev)? 使用這些可以改善性能,也能避免使用緩衝鏈(見第一節數據拷貝的相關敘述)帶來的麻煩。(說明:在dma傳輸數據的過程當中,要求源物理地址和目標物理地址必須是連續的。但在有的計算機體系中,如IA,連續的存儲器地址在物理上不必定是連續的,則dma傳輸要分紅屢次完成。若是傳輸完一塊物理連續的數據後發起一次中斷,同時主機進行下一塊物理連續的傳輸,則這種方式即爲block dma方式。scatter/gather方式則不一樣,它是用一個鏈表描述物理不連續的存儲器,而後把鏈表首地址告訴dma master。dma master傳輸完一塊物理連續的數據後,就不用再發中斷了,而是根據鏈表傳輸下一塊物理連續的數據,最後發起一次中斷。很顯然 scatter/gather方式比block dma方式效率高)
l 你的系統的頁大小是多少?高速緩存大小是多少?向這些大小邊界進行對起是否有用?系統調用和上下文切換花的代價是多少?
l 你是否知道鎖原語的飢餓現象?你的事件機制有沒有"驚羣"問題?你的喚醒/睡眠機制是否有這樣糟糕的行爲: 當X喚醒了Y, 環境馬上切換到了Y,可是X還有沒完成的工做?
我 在這裏考慮的了不少方面,相信你也考慮過。在特定狀況下,應用這裏提到的某些方面可能沒有價值,但能考慮這些因素的影響仍是有用的。若是在系統手冊中,你 沒有找到這些方面的說明,那麼就去努力找出答案。寫一個測試程序來找出答案;無論怎樣,寫這樣的測試代碼都是很好的技巧鍛鍊。若是你寫的代碼在多個平臺上 都運行過,那麼把這些相關的代碼抽象爲一個平臺相關的庫,未來在某個支持這裏提到的某些功能的平臺上,你就贏得了先機。
對你的代碼,「知其因此然」, 弄明白其中高級的操做, 以及在不一樣條件下的花銷.這不一樣於傳統的性能分析, 不是關於具體的實現,而是關乎設計. 低級別的優化永遠是蹩腳設計的最後救命稻草.
(map注:下面這段文字原文沒有,這是譯者對於翻譯的理)
[附錄:奧德修斯(Odysseus,又譯「奧德賽」),神話中伊塔刻島國王,《伊利亞特》和《奧德賽》兩大史詩中的主人公(公元前11世紀到公元前9世紀的希臘史稱做「荷馬時代」。包括《伊利亞特》和《奧德賽》兩部分的《荷馬史詩》,是古代世界一部著名的傑做)。奧德修斯曾參加過著名的特洛伊戰爭,在戰爭中他以英勇善戰、神機妙算而著稱,爲贏得戰爭的勝利,他設計製造了著名的「特洛伊木馬」(後來在西方成了「爲毀滅敵人而送的禮物」的代名詞)。特洛伊城毀滅後,他在回國途中又經歷了許多風險,荷馬的《奧德賽》就是奧德修斯歷險的記述。「斯庫拉和卡律布狄斯」的故事是其中最驚險、最恐怖的一幕。
相傳,斯庫拉和卡律布狄斯是古希臘神話中的女妖和魔怪,女妖斯庫拉住在乎大利和西西里島之間海峽中的一個洞穴裏,她的對面住着另外一個妖怪卡律布狄斯。它們爲害全部過往航海的人。據荷馬說,女妖斯庫拉長着12只不規則的腳,有6個蛇同樣的脖子,每一個脖子上各有一顆可怕的頭,張着血盆大口,每張嘴有3 排毒牙,隨時準備把獵物咬碎。它們天天在乎大利和西西里島之間海峽中興風做浪,航海者在兩個妖怪之間經過是異常危險的,它們時刻在等待着穿過西西里海峽的船舶。在海峽中間,卡律布狄斯化成一個大旋渦,波濤洶涌、水花飛濺,天天3次 從懸崖上奔涌而出,在退落時將經過此處的船隻所有淹沒。當奧德修斯的船接近卡律布狄斯大旋渦時,它像火爐上的一鍋沸水,波濤滔天,激起漫天雪白的水花。當 潮退時,海水混濁,濤聲如雷,驚天動地。這時,黑暗泥濘的巖穴一見到底。正當他們驚恐地注視着這一可怕的景象時,正當舵手當心翼翼地駕駛着船隻從左繞過旋 渦時,忽然海怪斯庫拉出如今他們面前,她一口叼住了6個同伴。奧德修斯親眼看見本身的同伴在妖怪的牙齒中間扭動着雙手和雙腳,掙扎了一下子,他們便被嚼碎,成了血肉模糊的一團。其他的人僥倖經過了卡律布狄斯大旋渦和海怪斯庫拉之間的危險的隘口。後來又歷經種種災難,最後終於回到了故鄉——伊塔刻島。
這個故事在語言學界和翻譯界被廣爲流傳。前蘇聯著名翻譯家巴爾胡達羅夫就曾把 「斯庫拉和卡律布狄斯」比做翻譯中「直譯和意譯」。他說:「形象地說,譯者老是不得不在直譯和意譯之間迂迴應變,猶如在斯庫拉和卡律布狄斯之間曲折前行,以求在這海峽兩岸之間找到一條狹窄然而卻足夠深邃的航道,以便達到理想的目的地——最大限度的等值翻譯。」
德國著名語言學家洪堡特也說過相似的話:「我確信任何翻譯無疑地都是企圖解決不可能解決的任務。由於任何一個翻譯家都會碰到一個暗礁而遭到失敗,他們不是因爲十分準確地遵照了原文的形式而破壞了譯文語言的特色,就是爲了照顧譯文語言的特色而損壞了原文。介於二者之間的作法不只難於辦到,並且簡直是不可能辦到。」
歷史上長久以來都認爲,翻譯只能選擇兩個極端的一種:或者這種——逐字翻譯(直譯);或者那種——自由翻譯(意譯)。就好像翻譯中的斯庫拉和卡律布狄斯」同樣。現在 「斯庫拉和卡律布狄斯」已成爲表示雙重危險——海怪和旋渦的代名詞,人們常說「介於斯庫拉和卡律布狄斯之間」,這就是說:處於兩面受敵的險境,比喻「危機四伏」,用來喻指譯者在直譯和意譯之間反覆做出抉擇之艱難。]