2020校招面試
個人情況
崗位:后臺 golang 開發(fā)
公司:字節(jié) shopee 拼多多百度阿里快手都有
途徑:提前批內(nèi)推
學(xué)歷:某 985 大三
實習(xí)情況:一段小廠實習(xí)
項目和準(zhǔn)備:實習(xí)時候做的是一個分布式存儲系統(tǒng)以及一個 kv 數(shù)據(jù)庫,這也是面試的重點
計算機(jī)網(wǎng)絡(luò)
1,tcp 中 timewait 狀態(tài)的作用,為什么要等待兩個 msl
2,tcp 中三次揮手開啟連接,四次握手關(guān)閉連接的流程
3,聊聊 tcp 的滑動窗口
4,ssl 建立過程
5,輸入一個 url 的過程
6,大文件傳輸
1,答:為了確認(rèn)被動關(guān)閉端接受到最后一個ack,避免主動關(guān)閉端重新在相同端口啟動連接后發(fā)送syn后被動關(guān)閉端認(rèn)為上一個連接沒有完全關(guān)閉,進(jìn)而返回rst終止連接
2,答:就是那個著名的連接建立圖
3,答:滑動窗口由接受窗口和擁塞窗口中的最小值,然后就是reno的慢啟動,擁塞控制,快速重傳三個步驟。然后我還談了談cubic算法。
4,答:很詳細(xì)的描述。從客戶端發(fā)送clienthello包括ssl版本,對稱算法,第一個不重數(shù),mac算法,公鑰算法。重點是一共生成了三個不重復(fù)數(shù),從主密鑰解出了四個密鑰,兩個用于會話加密,兩個用于mac加密。為什么是兩個呢,因為一個用于客戶端到服務(wù)器的會話加密,另一個用于服務(wù)器到客戶端的會話加密。這里要提醒證書機(jī)制并不是完全安全的,因此有EXPECT_CT這個瀏覽器的頭,防止證書頒發(fā)機(jī)構(gòu)被劫持。
5,答:從dns從瀏覽器,操作系統(tǒng)host文件解析。到http的hsts連接建立過程(302,307等狀態(tài)碼),到瀏覽器緩存etag,以及dom樹和css樹解析,繪圖和渲染,js事件循環(huán)https://juejin.im/post/6844903922084085773。
6,答:文件分塊。服務(wù)端返回206表示部分?jǐn)?shù)據(jù),416表示范圍出錯。添加一個Range header表示發(fā)送的數(shù)據(jù)的范圍。
linux io
首先需要對 linux 五種 io 模型和 epoll 有一定的了解,這里推薦一篇文章https://juejin.im/post/5c725dbe51882575e37ef9ed。
在傳輸文件的時候,有 sendfile 語意,用于高效的傳輸文件。
減少了內(nèi)核上下文切換以及 cpu 復(fù)制的損耗。
這里引用一篇文章https://juejin.im/post/6844903949359644680#heading-17。
在 go 語言里頭,當(dāng)我們使用 io.copy 的時候,會判斷目標(biāo)是否實現(xiàn) readerFrom 接口。
如果實現(xiàn)了就會調(diào)用 readerFrom,那么 readerFrom 和普通磁盤 io 的區(qū)別在哪呢?
// copyBuffer is the actual implementation of Copy and CopyBuffer.
// if buf is nil, one is allocated.
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
// If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy.
if wt, ok := src.(WriterTo); ok {
return wt.WriteTo(dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rt, ok := dst.(ReaderFrom); ok {
return rt.ReadFrom(src)
}
在 go 語言里頭,tcpconn 實現(xiàn)了這個接口。我們會發(fā)現(xiàn)它會用 splice 和 sendfile 兩個系統(tǒng)調(diào)用去獲取數(shù)據(jù)。如果 sendfile 系統(tǒng)也不支持,那么就會做一個優(yōu)雅降級的處理,轉(zhuǎn)換為普通的 io.Copy(隱藏 readerFrom 接口)。
func (c *TCPConn) readFrom(r io.Reader) (int64, error) {
if n, err, handled := splice(c.fd, r); handled {
return n, err
}
if n, err, handled := sendFile(c.fd, r); handled {
return n, err
}
return genericReadFrom(c, r)
}
普通的磁盤 io 傳輸。
一,發(fā)起 read 系統(tǒng)調(diào)用,從用戶態(tài)切換到內(nèi)核態(tài)。
二,cpu 通過控制 dma,將數(shù)據(jù)從硬件緩沖區(qū) copy 到內(nèi)核緩沖區(qū)。再將內(nèi)核緩沖區(qū)的數(shù)據(jù) copy 到用戶緩沖區(qū)。
三,系統(tǒng)調(diào)用結(jié)束,從內(nèi)核態(tài)切換為用戶態(tài)。
四,發(fā)起 write 系統(tǒng)調(diào)用,同上。
整個操作兩次 cpu copy,兩次 dma copy,四次上下文切換,兩次系統(tǒng)調(diào)用。

sendfile
一,sendfile 系統(tǒng)調(diào)用,從用戶態(tài)切換到內(nèi)核態(tài)
二,數(shù)據(jù)從硬件緩沖區(qū)通過 dma copy 復(fù)制到內(nèi)核緩沖區(qū);網(wǎng)卡的硬件緩沖區(qū)直接從磁盤對應(yīng)數(shù)據(jù)的內(nèi)核緩沖區(qū)讀取數(shù)據(jù)
三,sendfile 調(diào)用結(jié)束,從內(nèi)核態(tài)切換為用戶態(tài)
整個過程兩次 dma copy,兩次上下文切換,0 次 cpu copy,一個系統(tǒng)調(diào)用。

socket
而關(guān)于 tcp 這塊,socket 編程也需要了解,下面我們就來看看 go 中 socket 編程的流程。
net.dialTcp 是如何包裝 linux 系統(tǒng)調(diào)用的 socket 的。
客戶端
使用 socket 建立連接
linux 操作系統(tǒng)把各種 tcp,udp 連接抽象化成 socket,而 go 通過調(diào)用 linux 系統(tǒng)調(diào)用來建立連接。linux 中一切節(jié)文件,SYS_SOCKET 返回的一個數(shù)字就代表著文件的進(jìn)程打開文件描述符的句柄。type 表示 socket 類型,我們建立一個 tcp 連接,就使用的是 SOCK_STREAM,表示一個流式連接;proto 表示協(xié)議,IPPROTO_TCP 表示連接傳輸協(xié)議。domain 表示協(xié)議域,AF_INET、AF_INET6 表示 ip4,ip6 的協(xié)議。
func socket(domain int, typ int, proto int) (fd int, err error) {
r0, _, e1 := RawSyscall(SYS_SOCKET, uintptr(domain), uintptr(typ), uintptr(proto))
fd = int(r0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
將 socket 注冊為 nonblock 和 closeonexec 形式。當(dāng)我們向 socket 讀取數(shù)據(jù)的時候,是從設(shè)備緩沖區(qū)到內(nèi)核緩沖區(qū)再到用戶緩沖區(qū),而 nonblock 會當(dāng)設(shè)備緩沖區(qū)中數(shù)據(jù)沒有準(zhǔn)備好時返回一個 EAGAIN 錯誤。closeonexec 可以參考這篇文章https://blog.csdn.net/ljxfblog/article/details/41680115
syscall.CloseOnExec(s)
syscall.SetNonblock(s, true)
在 epoll 注冊 socket。當(dāng)向 socket 寫入數(shù)據(jù)的時候,首先使用系統(tǒng)調(diào)用 write,如果返回 eagain 錯誤,則休眠當(dāng)前 goroutine,等待 epoll 喚醒。
注冊為 nodelay(禁止 nigle 算法)
dodialtcp 用于建立一條 tcp 連接,包括本端地址和外目標(biāo)地址。internetsocket 就是完成我們之前所說的事。之后的錯誤處理主要是因為,當(dāng)建立連接時如果沒有源端口,那么就會隨機(jī)選擇一個端口,由于 tcp 能夠同時建立連接;那么很可能出現(xiàn)一條連接,目的端口和接受端口一致而且目的地址和接受地址一樣的情況。我們這里就是為了避免出現(xiàn)這種情況
func (sd *sysDialer) doDialTCP(ctx context.Context, laddr, raddr *TCPAddr) (*TCPConn, error) {
fd, err := internetSocket(ctx, sd.network, laddr, raddr, syscall.SOCK_STREAM, 0, "dial", sd.Dialer.Control)
// TCP has a rarely used mechanism called a 'simultaneous connection' in
// which Dial("tcp", addr1, addr2) run on the machine at addr1 can
// connect to a simultaneous Dial("tcp", addr2, addr1) run on the machine
// at addr2, without either machine executing Listen. If laddr == nil,
// it means we want the kernel to pick an appropriate originating local
// address. Some Linux kernels cycle blindly through a fixed range of
// local ports, regardless of destination port. If a kernel happens to
// pick local port 50001 as the source for a Dial("tcp", "", "localhost:50001"),
// then the Dial will succeed, having simultaneously connected to itself.
// This can only happen when we are letting the kernel pick a port (laddr == nil)
// and when there is no listener for the destination address.
// It's hard to argue this is anything other than a kernel bug. If we
// see this happen, rather than expose the buggy effect to users, we
// close the fd and try again. If it happens twice more, we relent and
// use the result. See also:
// https://golang.org/issue/2690
// https://stackoverflow.com/questions/4949858/
//
// The opposite can also happen: if we ask the kernel to pick an appropriate
// originating local address, sometimes it picks one that is already in use.
// So if the error is EADDRNOTAVAIL, we have to try again too, just for
// a different reason.
//
// The kernel socket code is no doubt enjoying watching us squirm.
for i := 0; i < 2 && (laddr == nil || laddr.Port == 0) && (selfConnect(fd, err) || spuriousENOTAVAIL(err)); i++ {
if err == nil {
fd.Close()
}
fd, err = internetSocket(ctx, sd.network, laddr, raddr, syscall.SOCK_STREAM, 0, "dial", sd.Dialer.Control)
}
if err != nil {
return nil, err
}
return newTCPConn(fd), nil
}
服務(wù)器
使用 socket 建立一條連接,使用 listen 將 socket 注冊為 listen 狀態(tài),bind 系統(tǒng)調(diào)用綁定端口
通過 accept 從 socket 接受連接。accept 首先通過系統(tǒng)調(diào)用 accept 等待連接,如果返回 eagain 錯誤則 epoll 等待。如果是 connectaborted 錯誤,則重試。connectionaborted 表示 tcp“三次握手” 后,又發(fā)送了一個 rst 中斷連接。
對于通過 accept 接受到的連接,也僅僅是一個 int 類型的 fd,我們要通過 epoll 包裝連接。
然后設(shè)置 keepalive 為默認(rèn)的 15s 間隔。keepalive 設(shè)置原則可以參考這里https://github.com/golang/go/issues/23459keepcnt 的不同,因此我們將 keepalive 設(shè)置為 15s,在 keepcnt 最大為 9 的 linux 系統(tǒng)上,超時事件為 150s。主要為了適應(yīng)不同操作系統(tǒng)上 < 3min。
func (ln *TCPListener) accept() (*TCPConn, error) {
fd, err := ln.fd.accept()
if err != nil {
return nil, err
}
tc := newTCPConn(fd)
if ln.lc.KeepAlive >= 0 {
setKeepAlive(fd, true)
ka := ln.lc.KeepAlive
if ln.lc.KeepAlive == 0 {
ka = defaultTCPKeepAlive
}
setKeepAlivePeriod(fd, ka)
}
return tc, nil
}
學(xué)習(xí)經(jīng)驗
書籍推薦《計算機(jī)網(wǎng)絡(luò)-自頂向下》。
一些業(yè)界比較新的算法也要會:cubic,bbr,quic。quic 推薦這篇文章https://zhuanlan.zhihu.com/p/32553477bbr 推薦這篇文章 https://www.jianshu.com/p/08eab499415a。,
tcp 的一些 option 字段要了解:sack,timestamp。
關(guān)于學(xué)習(xí) http 中繁瑣的 header,這里推薦使用 chrome 瀏覽器觀察 GitHub 的 http 連接建立的過程,觀察使用了哪些 header,如何使用 cookie 的(ps:可以學(xué)到很多的瀏覽器安全比如 xss 等知識)
而很多知識比如說 socket 的連接建立其實光看博客很抽象化,最好去看看 go 語言內(nèi)部的源碼。而往往在看 go 源碼的時候又會順藤摸瓜學(xué)習(xí)到很多知識。
面試經(jīng)驗
一,一些重要的數(shù)據(jù)結(jié)構(gòu)題還是要背一背的,不然面試官說讓你寫一個堆排序,事實上是讓你寫一個 for 循環(huán)的,考慮到 int 類型溢出的排序,這里沒有提前的準(zhǔn)備,很難當(dāng)場寫好。
二,有些問題可能你覺得自己專門準(zhǔn)備過,準(zhǔn)備好滔滔不絕顯示能力。但很多時候面試官不喜歡你說的太多,因為說的太多很像背面經(jīng)。。。因此首先要簡略的回答出重點,再看看面試官的反應(yīng)如何,考慮是否需要詳細(xì)的說。
三,針對項目,要準(zhǔn)備的非常深,詳細(xì)到重要參數(shù)的大小,為什么這樣設(shè)置?當(dāng)然,實際上很多參數(shù)其實也沒有個明確標(biāo)準(zhǔn)。。。比如我做存儲項目的時候,要在一個目錄下存儲很多大文件,然后文件分片成 256kb。。。面試官就問為什么分片成 256kb,為什么不能分片成 4kb 或者 16kb??當(dāng)時就懵了,后面網(wǎng)上查了查,發(fā)現(xiàn)創(chuàng)作者也是照著其他開源軟件的標(biāo)準(zhǔn)設(shè)置的。。。
四,總有一些問題你從來就沒見過的。遇見不要慌,說回自己熟悉的領(lǐng)域。比如說面試官曾經(jīng)問我 c++ 里頭怎么做 io 隔離?當(dāng)時想了半天也沒弄明白到底什么是 io 隔離,所以我感覺 go 中沒有這個問題。于是我就只好說抱歉我不懂 c++,但我可以談?wù)?go 語言是如何封裝 linux 系統(tǒng)文件系統(tǒng)調(diào)用,我們項目是如何處理文件的讀寫。。。然后在我們這種情況下,應(yīng)該是不存在 io 隔離的問題等等。最后也順利過了面試。
最后,希望每個 gopher 都能在求職季收獲自己滿意的 offer。
