GitHub 如何從單體架構遷移到微服務架構?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文介紹GitHub如何從單體架構遷移到微服務架構,並對其中一些最佳實踐做了詳細說明。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"旅程開啓"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/baike.baidu.com\/item\/Github\/10145341?fr=aladdin","title":"","type":null},"content":[{"type":"text","text":"GitHub"}]},{"type":"text","text":"創建於2008年,其宗旨是爲開發人員託管和分享代碼提供便利。GitHub的創建者也是開源貢獻者,他們在"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.cn\/article\/shCd8CJUSx0AHb7bh90m","title":"","type":null},"content":[{"type":"text","text":"Ruby"}]},{"type":"text","text":"社區非常有影響力。正因爲如此,GitHub的架構深深地紮根於"},{"type":"link","attrs":{"href":"https:\/\/baike.baidu.com\/item\/ruby%20on%20rails\/510525?fr=aladdin","title":"","type":null},"content":[{"type":"text","text":"Ruby on Rails"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在公司的整個發展歷程中,我們僱傭了世界上最好的Ruby開發人員,幫助我們擴展和優化代碼庫。如今,我們的平臺上已經有超過5000萬名開發人員,每年有超過8000萬個pull請求合併,全球各大洲有超過1億個代碼存儲庫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如你所見,這個"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.cn\/article\/LU_XTCh2NQ2X9ZClwCkO","title":"","type":null},"content":[{"type":"text","text":"單體架構"}]},{"type":"text","text":"已經帶我們走得很遠。一個演進了12年的代碼庫,每天要協調多次部署。我們有一個規模很大的平臺,每天處理10億次API調用,我們還提供了一個高性能的用戶界面,專注於完成這項工作。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"內部快速增長"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在過去18個月中,GitHub內部經歷了快速增長。我們已經有超過2000名員工,爲代碼庫做貢獻的工程師數量已經是以前的兩倍多。這種增長既包括自身的逐步發展,也包括收購,如Semmle、"},{"type":"link","attrs":{"href":"https:\/\/www.infoq.cn\/article\/oafbML7W8yeBRyT7dl61","title":"","type":null},"content":[{"type":"text","text":"npm"}]},{"type":"text","text":"、Dependabot和Pull Panda。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此外,GitHub是一個高度分散的團隊,在疫情發生前,我們就有超過70%的員工是在舊金山總部以外的地方辦公。GitHub的員工和承包商要跨六大洲展開協作,他們工作的時區各不相同。我們有1000多名內部開發人員,他們有各種各樣的開發技能,涉及到許多不同的技術。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯然,我們需要從根本上重新考慮下GitHub的軟件開發工作。讓每個人在參與開發之前都學習Ruby,讓所有人都在同一個單體代碼庫上進行開發,不再是擴展GitHub最高效、最優化的方法。根據康威定律,任何組織設計的系統,其結構都是對組織溝通結構的複製。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"反之亦然,單體架構會導致更大規模的涉衆會議,更復雜的決策過程,因爲交織的邏輯和共享的數據會影響所有團隊。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"單體 vs. 微服務"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此我們就想,是不是該從Ruby on Rails單體遷出,轉向一種微服務架構了?如果是這樣的話,我們該如何進行?單體架構和微服務架構各有所長。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在單體環境中,配置並運行應用程序更簡單,不用考慮複雜的依賴關係,拉取所有必要的依賴項。新建一個Hubber,只需幾個小時就可以在本機上配置好GitHub並運行起來。在單體架構中,代碼在有些情況下會更簡潔。例如,不用添加超時處理邏輯,也不用考慮如何優雅地處理由網絡延遲和中斷所導致的失敗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此外,由於所有人都工作在同一個技術棧上,大家對代碼庫都很熟悉,所以可以方便地將開發人員和團隊調去開發單體的其他特性,有利於實現特性的全局最優。考慮到GitHub在過去18個月中的增長情況,微服務環境的一部分優點吸引了我們。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,建立具有系統級所有權的特性團隊,通過清晰定義的API契約確立職責邊界。在遵循API契約的前提下,團隊有充分的自由選擇最適合自己的技術棧。代碼庫更小意味着閱讀更容易、啓動速度更快、問題排查更簡單。開發人員不用爲了提高生產力去理解一整個龐大的代碼庫的內部運行機制。最重要的是,服務現在可以根據各自的需求單獨擴展。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"務實——以賦能爲出發點"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在開始遷移GitHub之前,我們花了一些時間考慮爲什麼要這樣做,以及這樣做的目標是什麼。對我們來說,這是文化上的巨大轉變,需要做大量的工作。我們得想好,到底要解決什麼問題和痛點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在GitHub,這樣做可以讓超過一半的開發人員(在過去的18個月中加入)在單體代碼庫之外富有成效地開展工作。我們的目標是賦能而非替代。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲此,我們得接受這樣一個現實,GitHub未來的特性將基於一個單體-微服務混合的環境。也就是說,對於我們來說,維護和改進現有的單體代碼庫仍然很重要。有一個很好的例子是,我們最近升級到了Ruby2.7。感興趣的話,可以從GitHub官方博客上了解我們做了什麼,以及我們總體上如何改進系統。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"良好的架構始於模塊化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"良好的架構始於模塊化。拆分單體的第一步是考慮基於特性功能分割代碼和數據。這個過程可以在真正在微服務環境中拆分之前在單體中完成。使代碼庫易於管理,通常都是一種良好的架構實踐。確保每個服務都有自己的數據,並且能夠控制對這些數據的訪問,而且只能通過明確定義的API契約訪問。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我看到,在很多情況下,人們會首先抽出代碼邏輯,但仍然使用單體的共享數據庫。這往往會導致分佈式單體,這是最糟糕的單體,同時也是最糟糕的分佈式。沒有獲得任何好處(比如,單獨快速地向生產環境中部署一組特性),卻還要應對微服務的複雜性。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"數據拆分"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"正確地拆分數據是從單體架構轉向"},{"type":"link","attrs":{"href":"https:\/\/xie.infoq.cn\/article\/28c126904b6aa022daa2a68c9","title":"","type":null},"content":[{"type":"text","text":"微服務"}]},{"type":"text","text":"的基礎。這裏將稍微詳細地介紹下GitHub的做法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,我們在現有的數據庫模式中識別功能邊界,並按照這些邊界將實際的數據庫表分組。例如,我們將所有存儲庫相關的表分到一起,所有用戶相關的分到一起,所有項目相關的分到一起。我們將生成的功能分組稱爲模式域,並記錄在YAML定義文件中。現在,這個文件就成了事實來源。在數據庫模式中添加或刪除表,都要更新這個文件。我們通過一種靜態分析測試方法來提醒開發人員,在修改數據庫模式時,要更新這個文件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,對於每個模式域,我們找了一個分區鍵。這是一個共享字段,將一個功能組中的所有信息聯繫在一起。例如,存儲庫模式域(其中包含所有與存儲庫相關的數據,如問題、pull請求、評審意見)使用存儲庫ID作爲分區鍵。最終,創建數據庫模式功能組幫助我們將數據拆分到微服務架構所需的不同服務器和集羣上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於當前的跨域查詢,我們做了修復,以防數據拆分對產品造成破壞。在GitHub,我們在單體中實現了一個查詢監視器來幫助我們檢測,並在發現跨域查詢時發出告警信息。我們會根據域邊界,把這些查詢拆分並重寫成多個,並在應用程序層實現必要的連接。在劃分完功能組後,我們開始通過一個類似的過程,進一步將數據分片到相應的租戶組。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GitHub有超過5000萬用戶和1億個存儲庫,在這樣的規模下,功能組可能會變得非常大。這時,分區鍵就派上用場了。例如,一種簡單的方法是根據數值範圍將不同的用戶分配到不同的數據存儲。更常見的可能是根據每個數據集的特性(如區域和大小)所做的邏輯分組。Tenantizing是一個很好的方法,可以將數據存儲故障的爆炸半徑限制在客戶的一個子集裏,而不是一下子影響到所有人。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"從核心服務和共享資源入手"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們已經花了很多時間討論數據拆分的重要性。現在,我們換個話題,介紹下從單體中抽取服務的基礎工作。一定要記住,依賴方向只能從單體內到單體外,不能反過來,否則,我們最終會得到一個分佈式單體。也就是說,當從單體中抽取服務時,要從核心服務入手,然後逐步到特性層面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,找出開發人員在單體環境中開發時所使用的助力工具。隨着時間的推移構建一些共享工具以方便單體開發,這是很常見的。例如,我們的特性標識,可以讓單體開發者安心地將新特性從測試環境轉到生產環境,因爲在這個過程中,他們可以通過這個標識控制誰能看到這些特性。將助力工具轉移出來,讓開發人員在單體之外也可以使用這些工具。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,在新服務上線運行後,務必要刪除舊的代碼路徑。通過工具來識別誰在調用這個服務,並規劃好如何將流量全部導向新服務,這樣你就不用老是爲兩套代碼提供支持了。在GitHub,我們使用一個名爲Scientist的工具幫我們處理這種上線,我們可以用它並排運行和比較新舊代碼路徑。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"AuthN\/AuthZ抽取"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在GitHub,我們決定首先抽取的核心服務是身份驗證和授權。身份驗證相當複雜,因爲所有東西都依賴於它。網站和Git操作之間有一大堆的共享邏輯。也就是說,如果github.com宕掉了,那麼Git系統就無法訪問了,即使是使用命令行窗口,也無法執行像pull、push這樣的Git操作。這就是爲什麼把這些基礎部分抽取出來如此重要,那可以讓主要功能脫離單體而運行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於我們來說,身份驗證已經很簡單,因爲我們已經在單體外部將它重寫爲一個鏡像服務。當前的Rails應用程序(即我們的單體)使用Twirp(這是一個gRPC風格的服務到服務通信框架)和它通信,依賴方向是由內到外。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"運營變化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"監控、CI\/CD、容器化都不是什麼新概念,但爲了支持從單體到微服務的轉型,節省時間,加速向微服務的過渡,運營要做必要的改變。在修改這些工作流時,要時刻記着微服務的特性。與爲一個大型單體運行單個高度定製化的管道相比,爲衆多小型的、獨立運行的、基於不同技術棧的服務提供運營支持存在很大的差別。將監控從功能調用指標升級爲網絡指標和契約接口。推動實現自動化程度更高、更可靠的CI\/CD管道,並使其可以在服務之間共享。使用容器化技術支持各種語言和技術棧。創建工作流模板以實現重用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,在GitHub,我們創建了一個自助服務運行時平臺,可以用於微服務的打包交付。其目的是大幅減輕每個團隊創建微服務時的運營負擔。它提供了現成的Kubernetes模板,可自由使用的Ingress負載均衡設置。它可以將日誌自動提取到Splunk,並集成了我們內部的部署流程。這樣,任何團隊想要試驗或上線一個新的微服務都會更容易。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"小處着手,考慮產品\/業務價值"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到目前爲止,我們主要討論的還是結構性變化,以及從單體成功過渡到微服務架構所需要的基礎工作。此後,任何新特性都應該創建成單體外的一個微服務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下一步,找一些簡單的小特性從單體中遷移出來,例如,那些沒有複雜依賴和共享邏輯的特性。在GitHub,我們是從webhook推送和語法高亮開始的。我們希望在遷移更多更大的單體功能之前,找出常見的模式和兩種架構之間的差別。我們是根據產品和業務價值來確定微服務的大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們通過查找經常一起更改和部署的代碼和數據,來確定耦合度較高的特性或功能,並以此爲基礎,自然地劃分成可以獨立於其他部分單獨迭代和部署的分組。此外,專注於產品和業務價值,還有助於組織內跨工程團隊、產品和設計開展緊密合作。請注意,拆分得太小往往會增加不必要的複雜度和開銷。例如,需要維護單獨的部署密鑰,更多的服務檯職責,以及由於缺少知識共享而導致的單點故障。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"實現異步性和彈性代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從單體轉向微服務是重大的模式轉變。在這個過程中,不管是軟件開發流程,還是實際的代碼庫,都會發生很大的變化。在最後一部分內容中,我們將快速瞭解下服務之間的通信以及失敗機制(designing for failure),這兩個都是微服務開發中非常重要的概念。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務之間的通信方式有兩種:同步和異步。使用同步通信,客戶端在發送請求後會等待服務器的響應。使用異步通信, 客戶端在發送請求後不會等待響應,每條消息都可以由多個接收者處理。在GitHub,我們使用Twirp實現單體與單體外部核心服務(如授權)之間的同步通信。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而,隨着越來越多的服務移到單體之外,同步通信開始變得非常低效。而且,那還導致了服務之間的緊耦合,背離了遷移到微服務架構的初衷。更好的做法是創建一個共享的事件管道,協調多個生產者和消費者之間的消息。在SendGrid,我們使用的就是這種架構。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於服務不再是運行在一臺服務器上,所以考慮網絡通信中的延遲和故障非常重要。對於大部分暫時的網絡問題,使用一種簡單的重試機制,定義好重試頻率和最大重試次數,就足夠了。可以考慮使用指數退避讓重試邏輯變得更加智能。例如,隨着重試次數的增加延長等待時間,而不是間隔同樣的時間,從而緩解那些因爲過載而無法響應的服務器的壓力。作爲一種自我保護和自愈機制,還可以在服務之間增加斷路器。例如,在多次嘗試失敗之後,斷路器會打開,在服務恢復之前,不再允許額外的請求進入。爲服務設置超時時間,這樣服務就不會一直等待外部服務的響應。設法實現優雅的失敗,可以向用戶展示友好的提示信息,或者恢復到緩存中上一個已知的良好狀態。關注用戶體驗,做對企業有益的事。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"小結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文前4部分主要介紹了在開啓從單體到微服務的旅程之前應該瞭解的基礎內容。關注遷移原因。考慮模塊化和數據拆分。從核心服務和共享資源入手,做必要的運營調整。做好這些準備,整個組織的微服務轉型之旅就會更加令人愉快。接下來,我們討論了從哪裏入手,以及如何將微服務與產品和業務價值聯繫起來。最後,我們介紹了微服務的兩個關鍵概念:服務之間的通信和構建彈性系統。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於作者"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Sha Ma是GitHub軟件工程部門的副總裁,負責核心平臺和生態系統。在加入GitHub之前,她是SendGrid工程部門的副總裁,是2017年將公司上市的領導團隊的一員。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/www.infoq.com\/presentations\/github-rails-monolith-microservices\/"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章