国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

通過Node.js的Cluster模塊源碼,深入PM2原理

開發 前端
Node.js無疑是走向大前端、全棧工程師技術棧最快的捷徑(但是一定要會一門其他后臺語言,推薦Golang),雖然Node.js做很多事情都做不好,但是在某些方面還是有它的優勢。

 [[285104]]

Node.js無疑是走向大前端、全棧工程師技術棧最快的捷徑(但是一定要會一門其他后臺語言,推薦Golang),雖然Node.js做很多事情都做不好,但是在某些方面還是有它的優勢。

眾所周知,Node.js中的JavaScript代碼執行在單線程中,非常脆弱,一旦出現了未捕獲的異常,那么整個應用就會崩潰。

這在許多場景下,尤其是web應用中,是無法忍受的。通常的解決方案,便是使用Node.js中自帶的cluster模塊,以master-worker模式啟動多個應用實例。然而大家在享受cluster模塊帶來的福祉的同時,不少人也開始好奇

1.為什么我的應用代碼中明明有app.listen(port);,但cluter模塊在多次fork這份代碼時,卻沒有報端口已被占用?

2.Master是如何將接收的請求傳遞至worker中進行處理然后響應的?

帶著這些疑問我們開始往下看

TIPS:

本文編寫于2019年12月8日,是最新版本的Node.js源碼

Cluster源碼解析:

  •  入口 : 
  1. const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';  
  2. module.exports = require(`internal/cluster/${childOrMaster}`); 
  •  分析

會根據一個當前的Node_UNIQUE_ID(后面會講)是否在環境變量中判斷是子進程還是主進程,然后引用不同的js代碼

NODE_UNIQUE_ID是一個唯一標示,Node.js的Cluster多進程模式,采用默認的調度算法是round-robin,其實就是輪詢.官方解釋是實踐效率非常高,穩定

之前的問題一: 為什么我的應用代碼中明明有app.listen(port);,但cluter模塊在多次fork這份代碼時,卻沒有報端口已被占用?

我在Node.js的官網找到了答案:

原來所有的net.Socket都被設置了SO_REUSEADDR

這個SO_REUSEADDR到底是什么呢?

為什么需要 SO_REUSEADDR 參數?

服務端主動斷開連接以后,需要等 2 個 MSL 以后才最終釋放這個連接,重啟以后要綁定同一個端口,默認情況下,操作系統的實現都會阻止新的監聽套接字綁定到這個端口上。

我們都知道 TCP 連接由四元組唯一確定。形式如下 

  1. {local-ip-address:local-port , foreign-ip-address:foreign-port} 

一個典型的例子如下圖

TCP 要求這樣的四元組必須是唯一的,但大多數操作系統的實現要求更加嚴格,只要還有連接在使用這個本地端口,則本地端口不能被重用(bind 調用失敗)

啟用 SO_REUSEADDR 套接字選項可以解除這個限制,默認情況下這個值都為 0,表示關閉。在 Java 中,reuseAddress 不同的 JVM 有不同的實現,在我本機上,這個值默認為 1 允許端口重用。但是為了保險起見,寫 TCP、HTTP 服務一定要主動設置這個參數為 1。

目前常見的網絡編程模型就是多進程或多線程,根據accpet的位置,分為如下場景

2種場景

(1) 單進程或線程創建socket,并進行listen和accept,接收到連接后創建進程和線程處理連接

(2) 單進程或線程創建socket,并進行listen,預先創建好多個工作進程或線程accept()在同一個服務器套接字

這兩種模型解充分發揮了多核CPU的優勢,雖然可以做到線程和CPU核綁定,但都會存在:

1.單一listener工作進程或線程在高速的連接接入處理時會成為瓶頸

2.多個線程之間競爭獲取服務套接字

3.緩存行跳躍

4.很難做到CPU之間的負載均衡

5.隨著核數的擴展,性能并沒有隨著提升

6.SO_REUSEPORT解決了什么問題

7.SO_REUSEPORT支持多個進程或者線程綁定到同一端口,提高服務器程序的性能

解決的問題:

1.允許多個套接字 bind()/listen() 同一個TCP/UDP端口

2.每一個線程擁有自己的服務器套接字

3.在服務器套接字上沒有了鎖的競爭

4.內核層面實現負載均衡

5.安全層面,監聽同一個端口的套接字只能位于同一個用戶下面

其核心的實現主要有三點:

1.擴展 socket option,增加 SO_REUSEPORT 選項,用來設置 reuseport

2.修改 bind 系統調用實現,以便支持可以綁定到相同的 IP 和端口

3.修改處理新建連接的實現,查找 listener 的時候,能夠支持在監聽相同 IP 4.和端口的多個 sock 之間均衡選擇。

5.有了SO_RESUEPORT后,每個進程可以自己創建socket、bind、listen、accept相同的地址和端口,各自是獨立平等的

讓多進程監聽同一個端口,各個進程中accept socket fd不一樣,有新連接建立時,內核只會喚醒一個進程來accept,并且保證喚醒的均衡性。

總結:原來端口被復用是因為設置了SO_REUSEADDR,當然不止這一點,下面會繼續描述

回到源碼第一行

NODE_UNIQUE_ID是什么?

下面給出介紹: 

  1. function createWorkerProcess(id, env) {  
  2.   // ...  
  3.   workerEnv.NODE_UNIQUE_ID = '' + id;  
  4.   // ...  
  5.   return fork(cluster.settings.exec, cluster.settings.args, {  
  6.     env: workerEnv,  
  7.     silent: cluster.settings.silent,  
  8.     execArgv: execArgv,  
  9.     gid: cluster.settings.gid,  
  10.     uid: cluster.settings.uid  
  11.   });  
  12. ​ 

原來,創建子進程的時候,給了每個進程一個唯一的自增標示ID

隨后Node.js在初始化時,會根據該環境變量,來判斷該進程是否為cluster模塊fork出的工作進程,若是,則執行workerInit()函數來初始化環境,否則執行masterInit()函數

就是這行入口的代碼~ 

  1. module.exports = require(`internal/cluster/${childOrMaster}`); 

接下來我們需要看一下net模塊的listen函數源碼: 

  1. // lib/net.js  
  2. // ...  
  3. function listen(self, address, port, addressType, backlog, fd, exclusive) {  
  4.   exclusive = !!exclusive;  
  5.   if (!cluster) cluster = require('cluster');  
  6.   if (cluster.isMaster || exclusive) {  
  7.     self._listen2(address, port, addressType, backlog, fd);  
  8.     return;  
  9.   }  
  10.   cluster._getServer(self, {  
  11.     address: address,  
  12.     port: port,  
  13.     addressType: addressType,  
  14.     fd: fd,  
  15.     flags: 0  
  16.   }, cb);  
  17.   function cb(err, handle) {  
  18.     // ...  
  19.     self._handle = handle;  
  20.     self._listen2(address, port, addressType, backlog, fd);  
  21.   }  

仔細一看,原來listen函數會根據是不是主進程做不同的操作!

上面有提到SO_REUSEADDR選項,在主進程調用的_listen2中就有設置。

子進程初始化的每個workerinit函數中,也有cluster._getServer這個方法,

你可能已經猜到,問題一的答案,就在這個cluster._getServer函數的代碼中。它主要干了兩件事:

  •  向master進程注冊該worker,若master進程是第一次接收到監聽此端口/描述符下的worker,則起一個內部TCP服務器,來承擔監聽該端口/描述符的職責,隨后在master中記錄下該worker。
  •  Hack掉worker進程中的net.Server實例的listen方法里監聽端口/描述符的部分,使其不再承擔該職責。

對于第一件事,由于master在接收,傳遞請求給worker時,會符合一定的負載均衡規則(在非Windows平臺下默認為輪詢),這些邏輯被封裝在RoundRobinHandle類中。故,初始化內部TCP服務器等操作也在此處: 

  1. // lib/cluster.js  
  2. // ...  
  3. function RoundRobinHandle(key, address, port, addressType, backlog, fd) {  
  4.   // ...  
  5.   this.handles = [];  
  6.   this.handle = null 
  7.   this.server = net.createServer(assert.fail);  
  8.   if (fd >= 0)  
  9.     this.server.listen({ fd: fd });  
  10.   else if (port >= 0)  
  11.     this.server.listen(port, address);  
  12.   else  
  13.     this.server.listen(address);  // UNIX socket path.  
  14.   /// ...  

在子進程中: 

  1. function listen(backlog) {  
  2.     return 0;  
  3.   }  
  4.   function close() {  
  5.     // ...  
  6.   }  
  7.   function ref() {}  
  8.   function unref() {}  
  9.   var handle = {  
  10.     close: close,  
  11.     listen: listen,  
  12.     ref: ref,  
  13.     unref: unref,  
  14.   } 

由于net.Server實例的listen方法,最終會調用自身_handle屬性下listen方法來完成監聽動作,故在代碼中修改之:此時的listen方法已經被hack ,每次調用只能發揮return 0 ,并不會監聽端口 

  1. // lib/net.js  
  2. // ...  
  3. function listen(self, address, port, addressType, backlog, fd, exclusive) {  
  4.   // ...  
  5.   if (cluster.isMaster || exclusive) {  
  6.     self._listen2(address, port, addressType, backlog, fd);  
  7.     return; // 僅在worker環境下改變  
  8.   }  
  9.   cluster._getServer(self, {  
  10.     address: address,  
  11.     port: port, 
  12.      addressType: addressType,  
  13.     fd: fd,  
  14.     flags: 0  
  15.   }, cb);  
  16.   function cb(err, handle) {  
  17.     // ...  
  18.     self._handle = handle;  
  19.     // ...  
  20.   }  

這里可以看到,傳入的回調函數中的handle,已經把listen方法重新定義,返回0,那么等子進程調用listen方法時候,也是返回0,并不會去監聽端口,至此,煥然大悟,原來是這樣,真正監聽端口的始終只有主進程!

上面通過將近3000字講解,把端口復用這個問題講清楚了,下面把負載均衡這塊也講清楚。然后再講PM2的原理實現,其實不過是對cluster模式進行了封裝,多了很多功能而已~

首先畫了一個流程圖

核心實現源碼: 

  1. function RoundRobinHandle(key, address, port, addressType, backlog, fd) {  
  2.   // ...  
  3.   this.server = net.createServer(assert.fail);  
  4.   // ...  
  5.   var self = this 
  6.   this.server.once('listening', function() {  
  7.     // ...  
  8.     selfself.handle.onconnection = self.distribute.bind(self);  
  9.   });  
  10.  
  11. RoundRobinHandle.prototype.distribute = function(err, handle) {  
  12.   this.handles.push(handle);  
  13.   var worker = this.free.shift();  
  14.   if (worker) this.handoff(worker);  
  15. };  
  16. RoundRobinHandle.prototype.handoff = function(worker) {  
  17.   // ...  
  18.   var message = { act: 'newconn', key: this.key };  
  19.   var self = this 
  20.   sendHelper(worker.process, message, handle, function(reply) {  
  21.     // ...  
  22.   }); 

解析

定義好handle對象中的onconnection方法

觸發事件時,取出一個子進程通知,傳入句柄

子進程接受到消息和句柄后,做相應的業務處理: 

  1.  var accepted = server !== undefined;  
  2.   // ...  
  3.   if (accepted) server.onconnection(0, handle);// lib/cluster.js  
  4. // ...  
  5. // 該方法會在Node.js初始化時由 src/node.js 調用  
  6. cluster._setupWorker = function() {  
  7.   // ...  
  8.   process.on('internalMessage', internal(worker, onmessage));  ​  
  9.   // ...  
  10.   function onmessage(message, handle) {  
  11.     if (message.act === 'newconn')  
  12.       onconnection(message, handle);  
  13.     // ...  
  14.   }  
  15. };  
  16. function onconnection(message, handle) {  
  17.   // ...  

總結下來,負載均衡大概流程:

1.所有請求先同一經過內部TCP服務器,真正監聽端口的只有主進程。

2.在內部TCP服務器的請求處理邏輯中,有負載均衡地挑選出一個worker進程,將其發送一個newconn內部消息,隨消息發送客戶端句柄。

3.Worker進程接收到此內部消息,根據客戶端句柄創建net.Socket實例,執行具體業務邏輯,返回。

至此,Cluster多進程模式,負載均衡講解完畢,下面講PM2的實現原理,它是基于Cluster模式的封裝

PM2的使用: 

  1. npm i pm2 -g   
  2. pm2 start app.js   
  3. pm2 ls 

這樣就可以啟動你的Node.js服務,并且根據你的電腦CPU個數去啟動相應的進程數,監聽到錯誤事件,自帶重啟子進程,即使更新了代碼,需要熱更新,也會逐個替換,號稱永動機。

它的功能:

1.內建負載均衡(使用Node cluster 集群模塊)

2.后臺運行

3.0秒停機重載,我理解大概意思是維護升級的時候不需要停機.

4.具有Ubuntu和CentOS 的啟動腳本

5.停止不穩定的進程(避免無限循環)

6.控制臺檢測

7.提供 HTTP API

8.遠程控制和實時的接口API ( Nodejs 模塊,允許和PM2進程管理器交互 )

先來一張PM2的架構圖:

pm2包括 Satan進程、God Deamon守護進程、進程間的遠程調用rpc、cluster等幾個概念

如果不知道點西方文化,還真搞不清他的文件名為啥是 Satan 和 God:

撒旦(Satan),主要指《圣經》中的墮天使(也稱墮天使撒旦),被看作與上帝的力量相對的邪惡、黑暗之源,是God的對立面。

1.Satan.js提供了程序的退出、殺死等方法,因此它是魔鬼;God.js 負責維護進程的正常運行,當有異常退出時能保證重啟,所以它是上帝。作者這么命名,我只能說一句:oh my god。

God進程啟動后一直運行,它相當于cluster中的Master進程,守護者worker進程的正常運行。

2.rpc(Remote Procedure Call Protocol)是指遠程過程調用,也就是說兩臺服務器A,B,一個應用部署在A服務器上,想要調用B服務器上應用提供的函數/方法,由于不在一個內存空間,不能直接調用,需要通過網絡來表達調用的語義和傳達調用的數據。同一機器不同進程間的方法調用也屬于rpc的作用范疇。

3.代碼中采用了axon-rpc 和 axon 兩個庫,基本原理是提供服務的server綁定到一個域名和端口下,調用服務的client連接端口實現rpc連接。后續新版本采用了pm2-axon-rpc 和 pm2-axon兩個庫,綁定的方法也由端口變成.sock文件,因為采用port可能會和現有進程的端口產生沖突。

執行流程

程序的執行流程圖如下:

每次命令行的輸入都會執行一次satan程序。如果God進程不在運行,首先需要啟動God進程。然后根據指令,satan通過rpc調用God中對應的方法執行相應的邏輯。

以 pm2 start app.js -i 4為例,God在初次執行時會配置cluster,同時監聽cluster中的事件: 

  1. // 配置cluster  
  2. cluster.setupMaster({  
  3.   exec : path.resolve(path.dirname(module.filename), 'ProcessContainer.js')  
  4. });  
  5. // 監聽cluster事件  
  6. (function initEngine() {  
  7.   cluster.on('online', function(clu) {  
  8.     // worker進程在執行  
  9.     God.clusters_db[clu.pm_id].status = 'online' 
  10.   });  
  11.   // 命令行中 kill pid 會觸發exit事件,process.kill不會觸發exit  
  12.   cluster.on('exit', function(clu, code, signal) {  
  13.     // 重啟進程 如果重啟次數過于頻繁直接標注為stopped  
  14.     God.clusters_db[clu.pm_id].status = 'starting' 
  15.     // 邏輯  
  16.     ...  
  17.   });  
  18. })(); 

在God啟動后, 會建立Satan和God的rpc鏈接,然后調用prepare方法。prepare方法會調用cluster.fork,完成集群的啟動 

  1. God.prepare = function(opts, cb) {  
  2.   ...  
  3.   return execute(opts, cb);  
  4. }; 
  5.  function execute(env, cb) {  
  6.   ...  
  7.   var clu = cluster.fork(env);  
  8.   ...  
  9.   God.clusters_db[id] = clu;  
  10.   clu.once('online', function() {  
  11.     God.clusters_db[id].status = 'online' 
  12.     if (cb) return cb(null, clu);  
  13.     return true;  
  14.   });  
  15.   return clu;  

PM2的功能目前已經特別多了,源碼閱讀非常耗時,但是可以猜測到一些功能的實現:

例如

如何檢測子進程是否處于正常活躍狀態?

采用心跳檢測 

  1. 每隔數秒向子進程發送心跳包,子進程如果不回復,那么調用kill殺死這個進程  
  2. 然后再重新cluster.fork()一個新的進程 

子進程發出異常報錯,如何保證一直有一定數量子進程? 

  1. 子進程可以監聽到錯誤事件,這時候可以發送消息給主進程,請求殺死自己  
  2. 并且主進程此時重新調用cluster.fork一個新的子進程 

目前不少Node.js的服務,依賴Nginx+pm2+docker來實現自動化+監控部署,

pm2本身也是有監聽系統的,分免費版和收費版~

具體可以看官網,以及搜索一些操作手冊等進行監控操作,配置起來比較簡單,

這里就不做概述了。 

  1. https://pm2.keymetrics.io/ 

如果感覺寫得不錯,麻煩幫忙點個贊然后分享給你身邊多人,原創不易,需要支持~! 

 

責任編輯:龐桂玉 來源: segmentfault
相關推薦

2017-05-10 09:40:57

Ubuntupm2Nginx

2019-12-17 11:40:44

Node.js模塊前端

2011-09-08 14:07:28

Node.js

2020-08-31 15:00:17

Node.jsrequire前端

2021-09-26 05:06:04

Node.js模塊機制

2021-11-06 18:40:27

js底層模塊

2021-08-05 05:46:06

Node.jsInspector工具

2021-10-16 05:00:32

.js Buffer模塊

2015-07-16 09:59:55

PHP Node.js討論

2025-05-26 00:31:31

2020-04-15 15:48:03

Node.jsstream前端

2017-04-24 08:31:26

Node.jsExpress.jsHTTP

2021-07-09 00:24:10

No.jsNode.js原理

2021-08-12 01:00:29

NodejsAsync

2021-08-26 13:57:56

Node.jsEncodingBuffer

2020-11-09 10:46:35

CommonJS

2022-04-01 08:02:32

Node.js快照加速hooks

2022-04-02 06:04:03

Node.js代碼緩存V8

2023-06-30 23:25:46

HTTP模塊內存

2021-03-01 08:03:26

Node.jsStream模塊
點贊
收藏

51CTO技術棧公眾號

男女啪啪网站视频| 国产伦精品一区二区三区在线观看 | 美女视频一区| 欧美一区二区观看视频| 成人性生交大片免费看午夜| 午夜久久久影院| 全网国产福利在线播放| 中文字幕不卡在线观看| 亚洲色精品三区二区一区| 成人黄色大片在线观看| 日本a在线免费观看| 国产精品123| 2021狠狠干| 美女免费视频一区| 中国成人亚色综合网站| 日本午夜一区二区| 欧美在线3区| 另类小说综合欧美亚洲| 超薄肉色丝袜足j调教99| 国产一区二区成人久久免费影院| 久久天天东北熟女毛茸茸| 懂色av中文字幕一区二区三区| 国产一区二区四区| 99久久伊人精品| 先锋影音成人资源| 亚洲成年人网站在线观看| 黄色直播在线| 欧美一区国产二区| 国产精久久久| 亚洲精品欧美日韩| 美女www一区二区| 一道本视频在线观看| 91久久精品午夜一区二区| 91美女精品| 欧美理论片在线观看| 五月婷婷六月综合| 一区二区三区四区欧美日韩| 中文字幕乱码久久午夜不卡| 第九色区av在线| www.欧美精品| 久久久久蜜桃| 青青艹视频在线| 日本高清视频一区二区| av成人在线播放| av色综合网| 久久久久综合网| 久久77777| 97超级碰碰人国产在线观看| 日韩高清不卡在线| 蜜桃专区在线| 欧美人与性动交a欧美精品| 日本美女视频一区二区| 久久国产日韩欧美| 久久久不卡影院| 国产素人视频在线观看| 夜夜嗨av色一区二区不卡| 国产一区网站| 奇米精品在线| 国产精品素人一区二区| 日本www在线观看视频| 欧美裸体男粗大视频在线观看| 日韩av久操| 欧美日韩午夜爽爽| 日韩欧美aaa| 日韩精品免费视频一区二区三区| 91在线直播亚洲| 99久久精品国产网站| 3d成人动漫在线| 国产成人精品视频| www.色综合.com| 日本高清在线观看| 97av影视网在线观看| 亚洲三级在线免费观看| 二区三区不卡| 久久青青草综合| 日本韩国一区二区| 国内精品久久久久久久影视简单| 欧美亚洲另类色图| 国产视频在线一区二区| 制服诱惑一区二区| 中出在线观看| 国产精品极品美女在线观看免费 | 国产亚洲精品激情久久| 中文精品久久| baoyu777.永久免费视频| 日韩在线视频二区| 热久久国产精品| 色大18成网站www在线观看| 国产精品人人做人人爽| 久久先锋资源网| 久草在线中文最新视频| 精品乱子伦一区二区三区| 夜夜精品视频一区二区| 国产一区二区在线观| 欧美一级二级三级| 欧美日韩中文字幕综合视频 | 韩国国内大量揄拍精品视频| 美女视频免费一区| 日韩专区在线| 91久久国产综合久久91精品网站 | 精品一区二区三区视频在线观看 | 鲁大师成人一区二区三区| 一级特黄特色的免费大片| 2019中文字幕免费视频| 国产欧美综合色| 亚洲精品伦理| 久草资源站在线观看| 亚洲欧美国产一区二区三区| 日韩二区三区在线观看| 欧美精品videossex少妇| 久久综合给合久久狠狠色| 欧美日韩国产成人在线免费| 狠狠色狠狠色综合日日tαg| 精品三级久久久久久久电影聊斋| 91色p视频在线| 欧美性猛交xxxx乱大交蜜桃| 精品国产精品| 最近中文视频在线| 91视频8mav| 欧美日韩亚洲不卡| 性娇小13――14欧美| 黄色小说在线播放| 亚洲综合激情五月| 日韩在线观看网址| 日韩视频不卡| 国产精品四虎| 精品一区二区三区视频日产| 欧美午夜精品久久久久久超碰| 欧美激情成人在线| 日本蜜桃在线观看| 亚洲一区尤物| 色偷偷888欧美精品久久久 | 国产日韩亚洲欧美在线| 中文字幕在线观看亚洲| 久久久久国产精品麻豆| 国产亚洲欧美日韩在线观看一区二区 | 亚洲精品伦理| av网站免费观看| 国产日本欧美一区| 91精品一区二区三区在线观看| 国产乱码精品一品二品| av在线亚洲色图| 国内在线精品| 亚洲欧洲精品在线 | 亚洲欧美日韩一区在线| 国产精品福利av| 亚洲区第一页| 国产精品白丝久久av网站| 国产在线一在线二| 成人黄色av片| 亚洲最大av在线| 一道本无吗dⅴd在线播放一区 | 五月婷婷在线观看| 欧美两根一起进3p做受视频| 99re视频在线| 欧美乱大交xxxxx| 欧美一级欧美一级在线播放| 欧美激情一区二区三区四区| 一二三区精品| 亚洲精品3区| 超级碰碰久久| 成人性生交大片免费看午夜| 人妻精品无码一区二区三区 | 丝袜av一区| 欧美一区二区三区| 成人18免费| 亚洲国产精品毛片| 97国产suv精品一区二区62| 欧美日韩国产综合久久| 国产日韩欧美不卡在线| 国产农村妇女精品一区二区| 亚洲一区网址| 91禁在线看| 手机亚洲第一页| 岳毛多又紧做起爽| 久久狠狠久久综合桃花| 国内精品一区二区三区四区| 精品视频在线播放色网色视频| 亚洲成av人片一区二区| 91麻豆免费观看| 久久综合影音| 日韩一区二区在线免费| 日韩三级一区| 在线视频二区| 国产真实生活伦对白| 亚洲综合国产精品| 欧亚洲嫩模精品一区三区| 日本电影一区二区| 亚洲啪啪aⅴ一区二区三区9色| 91久久爱成人| 精品国产网站地址| 91久久精品日日躁夜夜躁欧美| 久久一夜天堂av一区二区三区| 蜜臀久久99精品久久久画质超高清 | 伊人久久国产| 青青青草网站免费视频在线观看| 欧美精品99久久| 性欧美videosex高清少妇| 国产欧美日韩精品在线观看| 欧美精品中文字幕一区|