使用 AppSwitch 解構 Istio
使用 AppSwitch 自動化應用程式加入和延遲最佳化。
Sidecar 代理方法實現了許多驚人的功能。Sidecar 位於微服務之間的資料路徑中,可以精確地知道應用程式想要做什麼。它可以監控和檢測協定流量,不是在網路層的深處,而是在應用程式層級,以實現深度可見性、存取控制和流量管理。
然而,如果我們仔細觀察,會發現資料在進行高價值的應用程式流量分析之前,必須經過許多中間層。其中大多數層是基本管線基礎架構的一部分,它們只是為了傳輸資料。這樣做會增加通訊的延遲和整體系統的複雜性。
多年來,人們在實作網路資料路徑層內的積極細粒度最佳化方面付出了許多集體努力。每次迭代可能會再減少幾微秒。但是,這些層本身的真正必要性尚未受到質疑。
不要最佳化層,移除它們
我相信,最佳化某些東西是不得已的退路,應該直接移除其需求。這是我最初在作業系統層級虛擬化方面工作的目標 (失效連結:https://apporbit.com/a-brief-history-of-containers-from-reality-to-hype/
),這導致了 Linux 容器的出現,它有效地移除了虛擬機器,直接在主機作業系統上執行應用程式,而不需要中間的訪客作業系統。長期以來,業界都在打錯仗,專注於最佳化虛擬機器,而不是完全移除額外的層。
我看到微服務的連線和一般網路也重複著相同的模式。網路正在經歷十年前實體伺服器所經歷的變化。正在引入新的層和結構。它們被深深地嵌入協定堆疊甚至矽晶片中,而沒有充分考慮低接觸的替代方案。或許有辦法完全移除這些額外的層。
我已經思考這些問題一段時間了,並認為可以將與容器概念類似的方法應用於網路堆疊,從根本上簡化應用程式端點如何在許多中間層的複雜性中連線。我已將容器原始工作的相同原則重新應用於建立 AppSwitch。與容器提供應用程式可以直接使用的介面類似,AppSwitch 直接插入應用程式目前使用的定義明確且普遍存在的網路 API,並直接將應用程式用戶端連接到適當的伺服器,跳過所有中間層。最後,這就是網路的全部意義。
在深入探討 AppSwitch 如何承諾從 Istio 堆疊中移除不必要的層之前,讓我簡要介紹一下它的架構。更多詳細資訊請參閱 文件 頁面。
AppSwitch
與容器執行時間類似,AppSwitch 由用戶端和常駐程式組成,它們透過 REST API 使用 HTTP 通訊。用戶端和常駐程式都以一個獨立的二進位檔案 `ax` 形式建置。用戶端會透明地插入應用程式,並追蹤其與網路連線相關的系統呼叫,並將發生的情況通知常駐程式。例如,假設應用程式向 Kubernetes 服務的服務 IP 發出 `connect(2)` 系統呼叫。AppSwitch 用戶端會攔截 connect 呼叫,使其失效,並將其發生情況以及包含系統呼叫參數的某些上下文通知常駐程式。然後,常駐程式會處理系統呼叫,可能會代表應用程式直接連線到上游伺服器的 Pod IP。
請務必注意,AppSwitch 用戶端和常駐程式之間不會轉發任何資料。它們旨在透過 Unix 域套接字交換檔案描述符 (FD),以避免複製資料。另請注意,用戶端不是單獨的程序。相反,它直接在應用程式本身的上下文中執行。應用程式和 AppSwitch 用戶端之間也沒有資料複製。
解構堆疊
現在我們已經了解 AppSwitch 的作用,讓我們來看看它從標準服務網格中最佳化的層。
網路去虛擬化
Kubernetes 為其執行的微服務應用程式提供簡單且定義明確的網路結構。然而,為了支援它們,它對底層網路施加了特定的要求。滿足這些要求通常並不容易。通常採用新增一層的常用解決方案來滿足這些要求。在大多數情況下,額外的層由位於 Kubernetes 和底層網路之間的網路覆蓋層組成。應用程式產生的流量在來源端被封裝,並在目標端被解封裝,這不僅消耗網路資源,還佔用運算核心。
由於 AppSwitch 透過其與平台的接觸點來仲裁應用程式所看到的内容,因此它向應用程式投射底層網路的一致虛擬視圖,類似於覆蓋層,但不會在資料路徑中引入額外的處理層。就像容器一樣,容器的內部看起來和感覺就像虛擬機器。然而,底層實作不會干預低階中斷等高發生率的控制路徑。
AppSwitch 可以被注入到標準的 Kubernetes 資訊清單中(類似於 Istio 注入),這樣應用程式的網路將由 AppSwitch 直接處理,繞過底層的任何網路覆蓋層。稍後將提供更多詳細資訊。
容器網路的產物
將網路連線從主機延伸到容器中一直是一個主要挑戰。專門為此目的發明了新的網路管線層。因此,在容器中執行的應用程式只不過是主機上的程序。然而,由於應用程式預期的網路抽象與容器網路命名空間公開的抽象之間存在根本的不一致,該程序無法直接存取主機網路。應用程式以套接字或會話的形式思考網路,而網路命名空間則公開裝置抽象。一旦置於網路命名空間中,該程序會突然失去所有連線。發明 veth 對和對應工具只是為了彌合這一差距。資料現在必須從主機介面傳輸到虛擬交換器,然後透過 veth 對傳輸到容器網路命名空間的虛擬網路介面。
AppSwitch 可以有效地移除連線兩端的虛擬交換器和 veth 對層。由於連線是由主機上執行的常駐程式使用主機上已有的網路建立的,因此無需額外的管線將主機網路橋接到容器中。在主機上建立的套接字 FD 會傳遞給 Pod 網路命名空間中執行的應用程式。當應用程式收到 FD 時,所有控制路徑工作(安全性檢查、連線建立)已經完成,並且 FD 可以進行實際的 IO 了。
跳過共置端點的 TCP/IP
TCP/IP 是一種通用協定媒介,幾乎所有的通訊都發生在其上。但是,如果應用程式端點恰好在同一主機上,真的需要 TCP/IP 嗎?畢竟,它確實做了很多工作,而且非常複雜。Unix 套接字是專門為主機內通訊設計的,AppSwitch 可以透明地切換通訊,以便在共置端點上透過 Unix 套接字進行。
對於應用程式的每個監聽套接字,AppSwitch 會維護兩個監聽套接字,每個套接字分別用於 TCP 和 Unix。當用戶端嘗試連線到恰好位於同一位置的伺服器時,AppSwitch 常駐程式會選擇連線到伺服器的 Unix 監聽套接字。結果在每一端產生的 Unix 套接字會傳遞到各自的應用程式。一旦返回完全連線的 FD,應用程式就會簡單地將其視為位元管線。協定並不重要。應用程式偶爾可能會進行特定於協定的呼叫,例如 `getsockname(2)`,而 AppSwitch 會相應地處理它們。它會呈現一致的回應,以便應用程式繼續運行。
資料推送代理
當我們繼續尋找要移除的層時,讓我們也重新考慮代理層本身的需求。有時候,代理的角色可能會退化為單純的資料推送器
- 可能不需要任何協定解碼
- 代理可能無法識別協定
- 通訊可能經過加密,且代理無法存取相關標頭
- 應用程式 (redis、memcached 等) 可能對延遲過於敏感,而無法承擔中間代理的成本
在所有這些情況下,代理與任何低階管線層沒有什麼不同。事實上,引入的延遲可能會高得多,因為代理無法使用相同層級的最佳化。
為了用一個範例來說明這一點,請考慮下圖所示的應用程式。它由一個 Python 應用程式和一組位於其後面的 memcached 伺服器組成。根據連線時間路由選擇上游 memcached 伺服器。速度是這裡的主要考慮因素。
如果我們查看此設定中的資料流程,Python 應用程式會連線到 memcached 的服務 IP。它會被重新導向到用戶端 sidecar。Sidecar 會將連線路由到其中一個 memcached 伺服器,並在兩個套接字之間複製資料,一個連線到應用程式,另一個連線到 memcached。並且,伺服器端 sidecar 和 memcached 之間也會發生相同的情況。此時代理的角色只不過是在兩個套接字之間進行乏味的位元傳輸。然而,它最終會大幅增加端到端連線的延遲。
現在讓我們想像一下,該應用程式以某種方式直接連線到 memcached,那麼就可以跳過兩個中間代理。資料將直接在應用程式和 memcached 之間流動,而無需任何中間躍點。AppSwitch 可以透過在 Python 應用程式發出 `connect(2)` 系統呼叫時透明地調整其傳遞的目標位址來安排這一點。
無代理協定解碼
這裡的情況會變得有點奇怪。我們已經看到,對於不涉及查看應用程式流量的情況,可以繞過代理。但是,即使對於其他情況,我們是否有任何辦法?結果證明,是可以的。
在微服務之間的典型通訊中,許多重要的資訊是在初始的標頭中交換。標頭之後是主體或酬載,通常代表大部分的通訊內容。而代理伺服器在這部分通訊中,又淪為數據推送器。AppSwitch 提供了一個巧妙的機制來跳過這些情況的代理。
雖然 AppSwitch 不是代理伺服器,但它確實仲裁應用程式端點之間的連線,而且它確實可以存取相應的 socket FD。通常,AppSwitch 只是將這些 FD 傳遞給應用程式。但它也可以使用 socket 上的 recvfrom(2)
系統呼叫的 MSG_PEEK
選項來窺視連線上收到的初始訊息。這讓 AppSwitch 可以在不實際將資料從 socket 緩衝區中移除的情況下檢查應用程式流量。當 AppSwitch 將 FD 返回給應用程式並退出資料路徑時,應用程式將對連線進行實際的讀取。AppSwitch 使用這種技術來對應用程式層級的流量進行更深入的分析,並實作複雜的網路功能,如下一節所述,所有這些都不需要進入資料路徑。
零成本負載平衡器、防火牆和網路分析器
負載平衡器和防火牆等網路功能的典型實作需要一個中間層,該層需要存取資料/封包流。例如,Kubernetes 的負載平衡器實作 (kube-proxy
) 通過 iptables 將探針引入封包流,而 Istio 則在代理層實作相同的功能。但是,如果僅需要在策略的基礎上重新導向或丟棄連線,則實際上不需要在整個連線過程中都待在資料路徑中。AppSwitch 可以通過簡單地在 API 層級操作控制路徑來更有效率地處理這個問題。鑑於它與應用程式的密切關係,AppSwitch 也可以輕鬆存取各種應用程式層級的指標,例如堆疊和堆積使用情況的動態、服務何時啟動、活動連線的屬性等,所有這些都可能形成用於監控和分析的豐富訊號。
更進一步來說,AppSwitch 也可以根據從 socket 緩衝區獲得的協定資料執行 L7 負載平衡和防火牆功能。它可以綜合協定資料和來自 Pilot 的策略資訊,以實作高效的路由和存取控制執行。它基本上可以「影響」應用程式連線到正確的後端伺服器,而無需對應用程式或其配置進行任何變更。就好像應用程式本身被注入了策略和流量管理智慧一樣。只是在這種情況下,應用程式無法逃脫這種影響。
還有一些更神奇的技術可以實現修改應用程式資料流而不進入資料路徑,但我將在後續文章中介紹。如果用例需要修改應用程式協定流量,目前 AppSwitch 的實作會使用代理伺服器。對於這些情況,AppSwitch 提供了一個高度最佳化的機制來將流量吸引到代理伺服器,如下一節所述。
流量重新導向
在 sidecar 代理伺服器可以查看應用程式協定流量之前,它首先需要接收連線。目前,進出應用程式的連線重新導向是通過封包篩選層完成的,該層會重寫封包,使其進入各自的 sidecar。建立表示重新導向策略可能需要大量的規則,這很繁瑣。而且,隨著 sidecar 要捕獲的目標子網路的變化,應用和更新規則的過程也很昂貴。
儘管 Linux 社群正在解決一些效能問題,但還有另一個與權限相關的問題:每當策略變更時,都需要更新 iptables 規則。鑑於目前的架構,所有特權操作都在 init 容器中執行,該容器僅在實際應用程式降級權限之前在最開始執行一次。由於更新 iptables 規則需要 root 權限,因此在不重新啟動應用程式的情況下無法執行此操作。
AppSwitch 提供了一種無需 root 權限即可重新導向應用程式連線的方法。因此,非特權應用程式已經能夠連線到任何主機(模組防火牆規則等),並且應該允許應用程式的所有者變更其應用程式通過 connect(2)
傳遞的主機位址,而無需額外權限。
Socket 委派
讓我們看看 AppSwitch 如何幫助重新導向連線而無需使用 iptables。想像一下,應用程式以某種方式自願將其用於通訊的 socket FD 傳遞給 sidecar,那麼就不需要 iptables 了。AppSwitch 提供了一個稱為socket 委派的功能,它正是這樣做的。它允許 sidecar 透明地存取應用程式用於其通訊的 socket FD 的副本,而無需對應用程式本身進行任何變更。
以下是在 Python 應用程式範例中實現此目的的步驟順序。
- 應用程式向 memcached 服務的服務 IP 發起連線請求。
- 來自用戶端的連線請求會轉發到 daemon。
- daemon 會建立一對預先連線的 Unix socket (使用
socketpair(2)
系統呼叫)。 - 它將 socket 對的一端傳遞到應用程式中,以便應用程式將該 socket FD 用於讀/寫。它還通過介入所有查詢連線屬性的呼叫來確保應用程式始終將其視為合法的 TCP socket。
- 另一端通過另一個 Unix socket 傳遞到 sidecar,其中 daemon 會公開其 API。應用程式連線的原始目標等資訊也會通過相同的介面傳達。
一旦應用程式和 sidecar 連線,其餘的操作將照常進行。Sidecar 會發起到上游伺服器的連線,並在從 daemon 收到的 socket 和連線到上游伺服器的 socket 之間代理資料。這裡的主要區別在於,sidecar 將通過 daemon 在 Unix socket 上獲取連線,而不是像正常情況下通過 accept(2)
系統呼叫獲取連線。除了通過正常的 accept(2)
通道監聽來自應用程式的連線之外,sidecar 代理伺服器還將連線到 AppSwitch daemon 的 REST 端點並通過這種方式接收 socket。
為了完整起見,以下是伺服器端發生的步驟順序
- 應用程式接收連線
- AppSwitch daemon 代表應用程式接受連線
- 它使用
socketpair(2)
系統呼叫建立一對預先連線的 Unix socket - socket 對的一端通過
accept(2)
系統呼叫返回給應用程式 - socket 對的另一端以及 daemon 代表應用程式最初接受的 socket 會傳送給 sidecar
- Sidecar 將提取兩個 socket FD,一個是連線到應用程式的 Unix socket FD,另一個是連線到遠端用戶端的 TCP socket FD
- Sidecar 將讀取 daemon 提供的關於遠端用戶端的元數據,並執行其通常的操作
「感知 Sidecar」的應用程式
對於明確感知 sidecar 並希望利用其功能的應用程式,socket 委派功能非常有用。它們可以通過使用相同的功能將其 socket 傳遞給 sidecar 來自願委派其網路互動。在某種程度上,AppSwitch 將每個應用程式都透明地轉換為感知 sidecar 的應用程式。
這一切是如何結合在一起的?
退一步來說,Istio 將應用程式的常見連線問題卸載到 sidecar 代理伺服器,該代理伺服器代表應用程式執行這些功能。AppSwitch 通過繞過中間層並僅在真正需要時才調用代理伺服器來簡化和最佳化服務網格。
在本節的其餘部分中,我將概述如何基於非常粗略的初始實作將 AppSwitch 與 Istio 整合。這並不是要像設計文件一樣,沒有探索所有可能的整合方式,也沒有解決所有細節。目的是討論實作的高階方面,以呈現這兩個系統如何結合在一起的大致概念。關鍵是 AppSwitch 將充當 Istio 和真實代理伺服器之間的緩衝區。對於無需調用 sidecar 代理伺服器即可更有效率地執行的情況,它將充當「快速路徑」。對於使用代理伺服器的情況,它將通過切斷不必要的層來縮短資料路徑。請查看此部落格,以更詳細地了解整合過程。
AppSwitch 用戶端注入
與 Istio sidecar-injector 類似,一個名為 ax-injector
的簡單工具會將 AppSwitch 用戶端注入到標準 Kubernetes 清單中。注入的用戶端會透明地監控應用程式,並將應用程式產生的控制路徑網路 API 事件通知 AppSwitch daemon。
如果使用 AppSwitch CNI 外掛程式,則可以不需要注入並使用標準 Kubernetes 清單。在這種情況下,CNI 外掛程式會在收到初始化回呼時執行必要的注入。但是,使用注入器確實有一些優勢:(1) 它可以在嚴格控制的環境(如 GKE)中運作 (2) 可以輕鬆擴展以支援其他框架,如 Mesos (3) 同一個叢集將能夠同時執行標準應用程式和「啟用 AppSwitch」的應用程式。
AppSwitch DaemonSet
AppSwitch daemon 可以設定為作為 DaemonSet
執行,也可以作為直接注入到應用程式清單中的應用程式的擴展。無論哪種情況,它都會處理來自它支援的應用程式的網路事件。
用於獲取策略的代理
這是將 Istio 決定的策略和配置傳達給 AppSwitch 的元件。它實作 xDS API 來監聽來自 Pilot 的訊號,並呼叫適當的 AppSwitch API 來設定 daemon。例如,它允許將 istioctl
指定的負載平衡策略轉換為等效的 AppSwitch 功能。
適用於 AppSwitch「自動管理」服務註冊表的平台適配器
鑑於 AppSwitch 位於應用程式網路 API 的控制路徑中,它可以隨時存取叢集中服務的拓撲。AppSwitch 以服務註冊表的形式公開該資訊,該服務註冊表會隨著應用程式及其服務的來來去去而自動(幾乎)同步更新。AppSwitch 除了 Kubernetes、Eureka 等之外的新平台適配器還將向上游服務提供 Istio 的詳細資訊。這並不是絕對必要的,但它確實使關聯 AppSwitch 代理在上面從 Pilot 收到的服務端點變得更加容易。
代理伺服器整合和鏈接
確實需要深度掃描和修改應用程式流量的連線會通過前面討論的 socket 委派機制傳遞給外部代理伺服器。它使用擴展版本的 proxy protocol。除了 proxy protocol 支援的簡單參數外,還會將各種其他元數據(包括從 socket 緩衝區獲得的初始協定標頭)和即時 socket FD(表示應用程式連線)轉發到代理伺服器。
代理伺服器可以查看元數據並決定如何進行。它可以通過接受連線來執行代理,或者通過指示 AppSwitch 允許連線並使用快速路徑,或者只是丟棄連線來回應。
這個機制的有趣之處在於,當代理程式從 AppSwitch 接收到一個 socket 時,它可以接著將該 socket 委派給另一個代理程式。事實上,AppSwitch 目前就是這樣運作的。它使用一個簡單的內建代理程式來檢查元數據,並決定是否在內部處理連線,還是將其轉交給外部代理程式 (Envoy)。相同的機制有可能被擴展,以允許多個外掛程式串聯,每個外掛程式都尋找特定的簽章,而鏈中的最後一個外掛程式則執行真正的代理工作。
不只是關於效能
移除資料路徑上的中間層不僅僅是為了提高效能。效能是一個很棒的附加效果,但它的確是一個附加效果。API 層級的方法有許多重要的優勢。
自動應用程式載入和政策撰寫
在微服務和服務網格出現之前,流量管理是由負載平衡器完成,而存取控制則由防火牆強制執行。應用程式是透過 IP 位址和 DNS 名稱來識別,這些位址和名稱相對靜態。事實上,這仍然是大多數環境的現狀。這些環境將從服務網格中獲益良多。然而,需要提供一個實用且可擴展的橋樑,才能通往新的世界。轉型上的困難並非完全是因為缺乏功能,而是因為需要重新思考並重新實作整個應用程式基礎架構所需的投入。目前,大多數的政策和組態都以負載平衡器和防火牆規則的形式存在。在提供採用服務網格模型的可擴展路徑時,必須以某種方式利用現有的環境脈絡。
AppSwitch 可以大幅簡化載入流程。它可以將相同的網路環境投射到目標應用程式,如同其目前的來源環境。如果傳統應用程式具有複雜的組態檔,其中硬編碼了靜態 IP 位址或特定的 DNS 名稱,那麼沒有任何協助通常是行不通的。AppSwitch 可以協助捕獲這些應用程式及其現有的組態,並將它們連接到服務網格,而無需進行任何變更。
更廣泛的應用程式和協定支援
HTTP 顯然在現代應用程式領域中佔據主導地位,但當我們談論到傳統應用程式和環境時,我們會遇到各種協定和傳輸方式。特別是,對 UDP 的支援變得不可避免。諸如 IBM WebSphere 等傳統應用程式伺服器大量依賴 UDP。大多數多媒體應用程式使用 UDP 媒體串流。當然,DNS 可能是最廣泛使用的 UDP「應用程式」。AppSwitch 在 API 層級支援 UDP,方式與 TCP 非常相似,當它偵測到 UDP 連線時,可以在其「快速路徑」中透明地處理它,而不是將其委派給代理程式。
客戶端 IP 保留和端對端原則
保留來源網路環境的相同機制也可以保留伺服器看到的客戶端 IP 位址。如果使用了 sidecar 代理程式,連線請求會來自代理程式,而不是客戶端。因此,伺服器看到的連線對等位址 (IP:port) 將會是代理程式的位址,而不是客戶端的位址。AppSwitch 可確保伺服器看到正確的客戶端位址,正確記錄它,並且任何基於客戶端位址所做的決策仍然有效。更廣泛地來說,AppSwitch 保留了 端對端原則,否則該原則會被混淆真實底層環境的中間層破壞。
透過存取加密標頭來增強應用程式訊號
加密流量完全破壞了服務網格分析應用程式流量的能力。API 層級的介入可能會提供一種解決方法。目前 AppSwitch 的實作方式是在系統呼叫層級取得應用程式的網路 API 的存取權。然而,原則上可以在堆疊中較高的 API 邊界影響應用程式,在該邊界中,應用程式資料尚未加密或已經解密。最終,資料總是會由應用程式以明文形式產生,然後在傳輸之前被加密。由於 AppSwitch 直接在應用程式的記憶體環境中執行,因此可以存取堆疊中較高層次的資料,其中資料仍然以明文形式保存。要使此方法生效,唯一的要求是,用於加密的 API 應該是明確定義的,並且易於介入。特別是,它需要存取應用程式二進位檔的符號表。需要明確的是,AppSwitch 目前尚未實作此功能。
那麼結果是什麼?
AppSwitch 從標準服務網格堆疊中移除了一些層和處理。所有這些在效能方面轉化為什麼?
我們執行了一些初步實驗,以根據先前討論的 AppSwitch 初始整合來描述最佳化的機會範圍。實驗是在 GKE 上使用 fortio-0.11.0
、istio-0.8.0
和 appswitch-0.4.0-2
執行的。在無代理測試中,AppSwitch 精靈程式是以 DaemonSet
的形式在 Kubernetes 叢集上執行,並且修改了 Fortio pod 規格以注入 AppSwitch 客戶端。這些是對設定所做的唯二變更。該測試的設定是用於測量 100 個並行連線的 GRPC 請求的延遲。
初步結果顯示,使用和不使用 AppSwitch 的 p50 延遲差異超過 18 倍 (3.99 毫秒對 72.96 毫秒)。當停用 mixer 和存取日誌時,差異約為 8 倍。顯然,差異是由於繞過了資料路徑上的所有中間層造成的。在 AppSwitch 的情況下,未觸發 Unix socket 最佳化,因為客戶端和伺服器 pod 被排程到不同的主機上。如果客戶端和伺服器碰巧位於同一位置,AppSwitch 案例的端對端延遲會更低。本質上,在 Kubernetes 叢集的各自 pod 中執行的客戶端和伺服器,是透過 GKE 網路上的 TCP socket 直接連接的 – 沒有通道、橋接器或代理程式。
總結
我從 David Wheeler 看似合理的引述開始,他說增加另一層並不能解決層數過多的問題。而且我透過這篇網誌的大部分內容指出,目前的網路堆疊已經有太多層,應該將它們移除。但是 AppSwitch 本身不也是一層嗎?
是的,AppSwitch 顯然是另一層。然而,它可以移除其他多個層。在這樣做的過程中,它將新的服務網格層與傳統網路環境的現有層無縫地結合在一起。它抵消了 sidecar 代理程式的成本,並且隨著 Istio 升級到 1.0,它為現有的應用程式及其網路環境提供了一個橋樑,以便轉移到服務網格的新世界。
也許 Wheeler 的引述應該寫成
致謝
感謝 Mandar Jog (Google) 關於 AppSwitch 對 Istio 的價值的多次討論,並感謝以下人士(依字母順序排列)審閱本網誌的早期草稿。
- Frank Budinsky (IBM)
- Lin Sun (IBM)
- Shriram Rajagopalan (VMware)